diff --git a/bin/tessel-2.js b/bin/tessel-2.js index 582104ee..9125ad03 100755 --- a/bin/tessel-2.js +++ b/bin/tessel-2.js @@ -270,12 +270,24 @@ makeCommand('list') .help('Lists all connected Tessels and their authorization status.'); parser.command('init') - .callback(init) + // .callback(init) + .callback((opts) => { + // We have to wrap the function in an anonymous callback + // or nomnom shields it from being stubbed for unit tests + return init.initProject(opts) + .then(module.exports.closeSuccessfulCommand, module.exports.closeFailedCommand); + }) .option('interactive', { flag: true, abbr: 'i', help: 'Run in interactive mode' }) + .option('lang', { + metavar: 'LANG', + abbr: 'l', + default: 'js', + help: 'The language to use . Javascript by default' + }) .help('Initialize repository for your Tessel project'); makeCommand('wifi') diff --git a/lib/init/index.js b/lib/init/index.js new file mode 100644 index 00000000..fd3a5517 --- /dev/null +++ b/lib/init/index.js @@ -0,0 +1,59 @@ +// System Objects +var path = require('path'); +// Third Party Dependencies + +// Internal +var languages = { + js: require('./javascript'), + rs: require('./rust'), + py: require('./python'), +}; + +var exportables = {}; + +// Initialize the directory given the various options +exportables.initProject = (opts) => { + + // Set a default directory if one was not provided + opts.directory = opts.directory || path.resolve('.'); + + // Detect the requested language + var lang = exportables.detectLanguage(opts); + + // If a language could not be detected + if (lang === null) { + // Return an error + return Promise.reject(new Error('Unrecognized language selection.')); + } else { + // Otherwise generate a project in that language + return lang.generateProject(opts); + } +}; + +// Determine the langauge to initialize the project with +exportables.detectLanguage = (opts) => { + + // If somehow a language option wasn't provided + if (!opts.lang) { + // Return JS as default + return languages['js']; + } + + // Iterate through each of the langauges + for (var key in languages) { + // Pull out the language info + var lang = languages[key]; + + // Check if the language option is within the available language keywords + if (lang.meta.keywords && + lang.meta.keywords.indexOf(opts.lang.toLowerCase()) > -1) { + // If it is, return that language + return lang; + } + } + + // If not, someone has requested a language that is not supported + return null; +}; + +module.exports = exportables; diff --git a/lib/init.js b/lib/init/javascript.js similarity index 55% rename from lib/init.js rename to lib/init/javascript.js index 18ed316c..8c10d5e8 100644 --- a/lib/init.js +++ b/lib/init/javascript.js @@ -7,12 +7,19 @@ var PZ = require('promzard').PromZard; var NPM = require('npm'); // Internal -// ... +var logs = require('../logs'); var packageJson = path.resolve('./package.json'); + var pkg, ctx, options; -function loadNpm() { +var exportables = {}; + +exportables.meta = { + keywords: ['javascript', 'js'] +}; + +exportables.loadNpm = () => { // You have to load npm in order to use it programatically return new Promise(function(resolve, reject) { NPM.load(function(error, npm) { @@ -23,22 +30,24 @@ function loadNpm() { resolve(npm); }); }); -} +}; -function getNpmConfig(npm) { +// Resolve an npm cofig list, or nothing (existance is not needed) +exportables.getNpmConfig = (npm) => { // Always resolve, we don't care if there isn't an npm config. return Promise.resolve(npm.config.list || {}); -} +}; -function buildJSON(npmConfig) { +// Builds the package.json file and writes it to the directory +exportables.buildJSON = (npmConfig) => { return new Promise(function(resolve, reject) { // Path to promzard config file var promzardConfig; ctx.config = npmConfig; // Default to auto config - promzardConfig = path.resolve(__dirname + '/../', 'resources/init-default.js'); + promzardConfig = path.resolve(__dirname, './../../', 'resources/javascript/init-default.js'); if (options.interactive) { - promzardConfig = path.resolve(__dirname + '/../', 'resources/init-config.js'); + promzardConfig = path.resolve(__dirname, '/../../', 'resources/javascript/init-config.js'); } // Init promozard with appropriate config. @@ -55,7 +64,7 @@ function buildJSON(npmConfig) { } }); - console.log('Created package.json.'); + logs.info('Created package.json.'); resolve(data); }); @@ -64,9 +73,10 @@ function buildJSON(npmConfig) { reject(error); }); }); -} +}; -function getDependencies(pkg) { +// Returns the dependencies of the package.json file +exportables.getDependencies = (pkg) => { if (!pkg.dependencies) { return []; } @@ -75,76 +85,67 @@ function getDependencies(pkg) { dependencies.push(mod + '@' + pkg.dependencies[mod]); } return dependencies; -} +}; +// Installs npm and dependencies +exportables.npmInstall = (dependencies) => { + return new Promise((resolve, reject) => { -function npmInstall(dependencies) { - return new Promise(function(resolve, reject) { - // Install the dependencies + // If there are no depencencies resolve if (!dependencies.length) { return resolve(); } - // Load npm to get the npm object - loadNpm() - .then(function(npm) { + // load npm to get the npm object + exportables.loadNpm() + .then((npm) => { npm.commands.install(dependencies, function(error) { if (error) { reject(error); } resolve(); }); - }); }); -} - -function generateSample() { - var filename = 'index.js'; - - // If an index.js already exists - fs.exists(filename, function(exists) { - // just return - if (exists) { - return; - } +}; - // If not and rust was requested - if (options.rust) { - // Place the rust example - filename = 'index.rs'; - } - // If python was requested - if (options.python) { - // Place the python example - filename = 'index.py'; - } +// // Generates blinky for JavaScript +exportables.generateJavaScriptSample = () => { + return new Promise((resolve) => { + var filename = 'index.js'; + // If an index.js already exists + fs.exists(filename, function(exists) { + // just return + if (exists) { + return resolve(); + } - // Pipe the contents of the default file into a new file - fs.createReadStream(path.resolve(__dirname + './../resources/' + filename)) - .pipe(fs.createWriteStream(filename)); + // Pipe the contents of the default file into a new file + fs.createReadStream(path.resolve(__dirname, './../../', 'resources/javascript', filename)) + .pipe(fs.createWriteStream(filename)); - console.log('Wrote "Hello World" to index.js'); + logs.info('Wrote "Hello World" to index.js'); + return resolve(); + }); }); -} +}; -function createTesselinclude() { +exportables.createTesselinclude = () => { var tesselinclude = '.tesselinclude'; - return new Promise((resolve) => { fs.exists(tesselinclude, (exists) => { if (exists) { return resolve(); } - fs.copySync(path.resolve(__dirname + './../resources/' + tesselinclude), tesselinclude); - console.log('Created .tesselinclude.'); + fs.copySync(path.resolve(__dirname, './../../', 'resources/javascript', tesselinclude), tesselinclude); + logs.info('Created .tesselinclude.'); resolve(); }); }); -} +}; -function readPackageJson() { +exportables.readPackageJson = () => { return new Promise(function(resolve, reject) { fs.readFile(packageJson, 'utf8', function(err, data) { if (err) { @@ -154,31 +155,30 @@ function readPackageJson() { resolve(data); }); }); -} +}; -function writePackageJson(data) { +exportables.writePackageJson = (data) => { return new Promise(function(resolve, reject) { fs.writeFile(packageJson, data, function(err) { if (err) { return reject(err); } - resolve(); }); }); -} +}; -function prettyPrintJson(data) { +exportables.prettyPrintJson = (data) => { return JSON.stringify(data, null, 2); -} +}; -module.exports = function(opts) { - // Set interactive boolean off of CLI flags - options = opts; +exportables.generateProject = (opts) => { - console.log('Initializing Tessel 2 Project...'); + // Make the options global + options = opts; - readPackageJson() + logs.info('Initializing new Tessel project for JavaScript...'); + return exportables.readPackageJson() .then(function(data) { // Try to parse current package JSON @@ -204,18 +204,20 @@ module.exports = function(opts) { ctx.version = undefined; return ctx; }) - .then(loadNpm) - .then(getNpmConfig) - .then(buildJSON) - .then(prettyPrintJson) - .then(writePackageJson) - .then(readPackageJson) + .then(exportables.loadNpm) + .then(exportables.getNpmConfig) + .then(exportables.buildJSON) + .then(exportables.prettyPrintJson) + .then(exportables.writePackageJson) + .then(exportables.readPackageJson) .then(JSON.parse) - .then(getDependencies) - .then(npmInstall) - .then(createTesselinclude) - .then(generateSample) + .then(exportables.getDependencies) + .then(exportables.npmInstall) + .then(exportables.createTesselinclude) + .then(exportables.generateJavaScriptSample) .catch(function(error) { - console.error(error); + logs.error(error); }); }; + +module.exports = exportables; diff --git a/lib/init/python.js b/lib/init/python.js new file mode 100644 index 00000000..8e3a8970 --- /dev/null +++ b/lib/init/python.js @@ -0,0 +1,18 @@ +// System Objects + +// Third Party Dependencies + +// Internal +var logs = require('../logs'); + +var exportables = {}; + +exportables.meta = { + keywords: ['py', 'python'] +}; + +exportables.generateProject = () => { + logs.info(`Sorry, Python project generation isn't implemented yet. Contributions welcome!`); +}; + +module.exports = exportables; diff --git a/lib/init/rust.js b/lib/init/rust.js new file mode 100644 index 00000000..58cb8e09 --- /dev/null +++ b/lib/init/rust.js @@ -0,0 +1,82 @@ +// System Objects +var path = require('path'); +var fs = require('fs-extra'); +var cp = require('child_process'); + +// Third Party Dependencies + +// Internal +var logs = require('../logs'); + +var exportables = {}; + +var options; + +exportables.meta = { + keywords: ['rust', 'rs'] +}; + +exportables.generateProject = (opts) => { + + // Save the options so they are accessible from all functions + options = opts; + + return exportables.verifyCargoInstalled() + .then(exportables.generateRustSample); +}; + +exportables.generateRustSample = () => { + return new Promise((resolve, reject) => { + // Files, directories, and paths + var file_toml = 'Cargo.toml'; + var file_src = 'main.rs'; + var dir_src = path.resolve(options.directory, 'src/'); + var path_toml = path.resolve(options.directory, file_toml); + var path_src = path.resolve(dir_src, file_src); + + // Error functions (just to reduce copied text everywhere) + function exists_err(filepath) { + return new Error(`Looks like this is already a Cargo project! (${filepath} already exists)`); + } + + function mkdir_err(dir) { + return new Error('Could not create ' + dir); + } + + // Generate the toml and the src file + fs.exists(dir_src, (exists) => { + if (exists) { + return reject(exists_err(dir_src)); + } + fs.exists(path_toml, (exists) => { + if (exists) { + return reject(exists_err(path_toml)); + } + fs.mkdir(dir_src, (err) => { + if (err) { + return reject(new Error(mkdir_err(dir_src))); + } + // Copy over config file, the blinky main, and the toml file + fs.createReadStream(path.resolve(__dirname, './../../resources/rust/', file_toml)).pipe(fs.createWriteStream(path_toml)); + logs.info('Initialized Cargo project...'); + fs.createReadStream(path.resolve(__dirname, './../../resources/rust/', file_src)).pipe(fs.createWriteStream(path_src)); + logs.info(`Wrote "Hello World" to ${path_src}`); + }); + }); + }); + }); +}; + +// Verify the user has Cargo, reject if they do not +exportables.verifyCargoInstalled = () => { + return new Promise((resolve, reject) => { + cp.exec('cargo', (err, stdout, stderr) => { + if (err || stderr) { + return reject(new Error('Rust or Cargo is not installed properly. You can re-install with: "curl -sf -L https://static.rust-lang.org/rustup.sh | sh"')); + } + return resolve(); + }); + }); +}; + +module.exports = exportables; diff --git a/resources/.tesselinclude b/resources/javascript/.tesselinclude similarity index 100% rename from resources/.tesselinclude rename to resources/javascript/.tesselinclude diff --git a/resources/index.js b/resources/javascript/index.js similarity index 100% rename from resources/index.js rename to resources/javascript/index.js diff --git a/resources/init-config.js b/resources/javascript/init-config.js similarity index 100% rename from resources/init-config.js rename to resources/javascript/init-config.js diff --git a/resources/init-default.js b/resources/javascript/init-default.js similarity index 100% rename from resources/init-default.js rename to resources/javascript/init-default.js diff --git a/resources/rust/Cargo.toml b/resources/rust/Cargo.toml new file mode 100644 index 00000000..3ab75039 --- /dev/null +++ b/resources/rust/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "blinky" +version = "0.0.1" +authors = ["Your Name "] + +[dependencies.rust_tessel] +git="https://github.com/tessel/rust-tessel.git" \ No newline at end of file diff --git a/resources/rust/main.rs b/resources/rust/main.rs new file mode 100644 index 00000000..16cfe2f2 --- /dev/null +++ b/resources/rust/main.rs @@ -0,0 +1,29 @@ +/// A blinky example for Tessel + +// Import the tessel library +extern crate rust_tessel; +// Import the Tessel API +use rust_tessel::Tessel; +// Import sleep from the standard lib +use std::thread::sleep; +// Import durations from the standard lib +use std::time::Duration; + +fn main() { + // Create a new Tessel + let mut tessel = Tessel::new(); + + // Turn on one of the LEDs + tessel.led[2].on().unwrap(); + + println!("I'm blinking! (Press CTRL + C to stop)"); + + // Loop forever + loop { + // Toggle each LED + tessel.led[2].toggle().unwrap(); + tessel.led[3].toggle().unwrap(); + // Re-execute the loop after sleeping for 100ms + sleep(Duration::from_millis(100)); + } +} diff --git a/test/.jshintrc b/test/.jshintrc index 52e85c1d..ea955bbb 100644 --- a/test/.jshintrc +++ b/test/.jshintrc @@ -50,6 +50,10 @@ "glob": true, "Ignore": true, "inquirer": true, + "init": true, + "initJavaScript": true, + "initPython": true, + "initRust": true, "IS_TEST_ENV": true, "lan": true, "LAN": true, diff --git a/test/common/bootstrap.js b/test/common/bootstrap.js index a6dc0723..c8aa2efe 100644 --- a/test/common/bootstrap.js +++ b/test/common/bootstrap.js @@ -55,6 +55,10 @@ global.logs = require('../../lib/logs'); global.updates = require('../../lib/update-fetch'); global.lan = require('../../lib/lan-connection'); global.usb = require('../../lib/usb-connection'); +global.init = require('../../lib/init'); +global.initJavaScript = require('../../lib/init/javascript'); +global.initRust = require('../../lib/init/rust'); +global.initPython = require('../../lib/init/python'); // ./lib/usb/* global.Daemon = require('../../lib/usb/usb-daemon'); diff --git a/test/unit/bin-tessel-2.js b/test/unit/bin-tessel-2.js index 14184d67..84871389 100644 --- a/test/unit/bin-tessel-2.js +++ b/test/unit/bin-tessel-2.js @@ -736,11 +736,9 @@ exports['--output true/false'] = { exports['Tessel (cli: crash-reporter)'] = { + setUp: function(done) { this.sandbox = sinon.sandbox.create(); - this.warn = this.sandbox.stub(logs, 'warn'); - this.info = this.sandbox.stub(logs, 'info'); - this.closeSuccessful = this.sandbox.stub(cli, 'closeSuccessfulCommand'); this.closeFailed = this.sandbox.stub(cli, 'closeFailedCommand'); @@ -878,5 +876,65 @@ exports['Tessel (cli: crash-reporter)'] = { test.done(); }); }, +}; + +exports['Tessel (init)'] = { + setUp: function(done) { + this.sandbox = sinon.sandbox.create(); + this.warn = this.sandbox.stub(logs, 'warn'); + this.info = this.sandbox.stub(logs, 'info'); + + this.successfulCommand = this.sandbox.stub(cli, 'closeSuccessfulCommand'); + this.failedCommand = this.sandbox.stub(cli, 'closeFailedCommand'); + this.initProject = this.sandbox.spy(init, 'initProject'); + this.detectLanguage = this.sandbox.spy(init, 'detectLanguage'); + this.generateJavaScriptProject = this.sandbox.stub(initJavaScript, 'generateProject').returns(Promise.resolve()); + this.generateRustProject = this.sandbox.stub(initRust, 'generateProject').returns(Promise.resolve()); + done(); + }, + + tearDown: function(done) { + this.sandbox.restore(); + done(); + }, + + noOpts: function(test) { + test.expect(5); + // Call the CLI with the init command + cli(['init']); + + setImmediate(() => { + // Ensure it calls our internal init function + test.equal(this.initProject.callCount, 1); + // Ensure it checks the language being requested + test.equal(this.detectLanguage.callCount, 1); + // It should generate a js project + test.equal(this.generateJavaScriptProject.callCount, 1); + // It should not generate a Rust project + test.equal(this.generateRustProject.callCount, 0); + // Ensure it continues to call our exit function + test.equal(this.successfulCommand.callCount, 1); + test.done(); + }); + }, + rustOpts: function(test) { + test.expect(5); + // Call the CLI with the init command + cli(['init', '--lang=rust']); + + setImmediate(() => { + // Ensure it calls our internal init function + test.equal(this.initProject.callCount, 1); + // Ensure it checks the language being requested + test.equal(this.detectLanguage.callCount, 1); + // Ensure it does not attempt to generate a Rust project + test.equal(this.generateJavaScriptProject.callCount, 0); + // Ensure it does generate a Rust project + test.equal(this.generateRustProject.callCount, 1); + // Ensure it continues to call our exit function + test.equal(this.successfulCommand.callCount, 1); + test.done(); + }); + }, }; diff --git a/test/unit/init.js b/test/unit/init.js new file mode 100644 index 00000000..a234bceb --- /dev/null +++ b/test/unit/init.js @@ -0,0 +1,162 @@ +exports['init (language args)'] = { + setUp: (done) => { + done(); + }, + tearDown: (done) => { + done(); + }, + javascriptArgs: function(test) { + test.expect(4); + // No language arg indicates JavaScript by default + test.ok(init.detectLanguage({}) === initJavaScript); + // Can request JavaScript with explicit name + test.ok(init.detectLanguage({ + lang: 'javascript' + }) === initJavaScript); + // Can request JavaScript with abbr + test.ok(init.detectLanguage({ + lang: 'js' + }) === initJavaScript); + // This won't request JavaScript init + test.ok(init.detectLanguage({ + lang: 'something else' + }) !== initJavaScript); + test.done(); + }, + rustArgs: function(test) { + test.expect(4); + // No language arg does not mean Rust + test.ok(init.detectLanguage({}) !== initRust); + // Can request Rust with explicit name + test.ok(init.detectLanguage({ + lang: 'rust' + }) === initRust); + // Can request Rust with abbr + test.ok(init.detectLanguage({ + lang: 'rs' + }) === initRust); + // This won't request Rust init + test.ok(init.detectLanguage({ + lang: 'whitespace' + }) !== initRust); + test.done(); + }, + pythonArgs: function(test) { + test.expect(4); + // No language arg does not mean Rust + test.ok(init.detectLanguage({}) !== initPython); + // Can request Rust with explicit name + test.ok(init.detectLanguage({ + lang: 'python' + }) === initPython); + // Can request Rust with abbr + test.ok(init.detectLanguage({ + lang: 'py' + }) === initPython); + // This won't request Rust init + test.ok(init.detectLanguage({ + lang: 'morse-code' + }) !== initPython); + test.done(); + } +}; + +exports['init --lang rust'] = { + setUp: (done) => { + this.sandbox = sinon.sandbox.create(); + this.logsWarn = this.sandbox.stub(logs, 'warn', function() {}); + this.logsInfo = this.sandbox.stub(logs, 'info', function() {}); + done(); + }, + tearDown: (done) => { + this.sandbox.restore(); + done(); + }, + cargoVerifySucceed: (test) => { + test.expect(3); + // Stub our own exec so we don't try running cargo on the host cpu + this.exec = this.sandbox.stub(cp, 'exec', (command, callback) => { + // Ensure the command is cargo + test.equal(command, 'cargo'); + // Reject to simulate no Rust or Cargo install + callback(); + }); + + // Stub the generating sample code so we don't write to fs + this.generateRustSample = this.sandbox.stub(initRust, 'generateRustSample').returns(Promise.resolve()); + + // Attempt to initialize a Rust projec + init.initProject({ + lang: 'rust' + }) + // It should not succeed + .then(() => { + // Ensure exec was called just once + test.equal(this.exec.callCount, 1); + test.equal(this.generateRustSample.callCount, 1); + test.done(); + }) + .catch(() => { + test.ok(false, 'a rejection should not be served if cargo is installed'); + }); + }, + + cargoVerifyFail: (test) => { + test.expect(4); + + // Stub our own exec so we don't try running cargo on the host cpu + this.exec = this.sandbox.stub(cp, 'exec', (command, callback) => { + // Ensure the command is cargo + test.equal(command, 'cargo'); + // Reject to simulate no Rust or Cargo install + callback(new Error('undefined command: cargo')); + }); + + // Stub the generating sample code so we don't write to fs + this.generateRustSample = this.sandbox.stub(initRust, 'generateRustSample').returns(Promise.resolve()); + + // Attempt to initialize a Rust projec + return init.initProject({ + lang: 'rust' + }) + // It should not succeed + .then(() => { + test.ok(false, 'a rejection should be served if cargo is not installed'); + }) + .catch((err) => { + // Ensure exec was called just once + test.equal(this.exec.callCount, 1); + // Ensure we did not attempt to generate Rust code + test.equal(this.generateRustSample.callCount, 0); + // Ensure we received an error + test.ok(err instanceof Error); + test.done(); + }); + } +}; + +exports['init --lang javascript'] = { + setUp: (done) => { + this.sandbox = sinon.sandbox.create(); + this.logsWarn = this.sandbox.stub(logs, 'warn', function() {}); + this.logsInfo = this.sandbox.stub(logs, 'info', function() {}); + done(); + }, + tearDown: (done) => { + this.sandbox.restore(); + done(); + }, +}; + +exports['init --lang python'] = { + setUp: (done) => { + this.sandbox = sinon.sandbox.create(); + this.logsWarn = this.sandbox.stub(logs, 'warn', function() {}); + this.logsInfo = this.sandbox.stub(logs, 'info', function() {}); + done(); + }, + tearDown: (done) => { + this.sandbox.restore(); + done(); + }, +};