From 6099746a4f28838e38c5684923d6ca65fe9bce56 Mon Sep 17 00:00:00 2001 From: johnnyman727 Date: Wed, 9 Sep 2015 20:47:23 -0700 Subject: [PATCH] Adding @nplus11's work on Rust init --- bin/tessel-2.js | 8 +- lib/controller.js | 2 +- lib/init/index.js | 59 ++++++++ lib/{init.js => init/javascript.js} | 133 +++++++++-------- lib/init/python.js | 18 +++ lib/init/rust.js | 82 +++++++++++ resources/{ => javascript}/.tesselinclude | 0 resources/{ => javascript}/index.js | 0 resources/{ => javascript}/init-config.js | 0 resources/{ => javascript}/init-default.js | 0 resources/rust/Cargo.toml | 7 + resources/rust/main.rs | 29 ++++ test/.jshintrc | 4 + test/common/bootstrap.js | 4 + test/unit/bin-tessel-2.js | 61 ++++++++ test/unit/init.js | 162 +++++++++++++++++++++ 16 files changed, 500 insertions(+), 69 deletions(-) create mode 100644 lib/init/index.js rename lib/{init.js => init/javascript.js} (62%) create mode 100644 lib/init/python.js create mode 100644 lib/init/rust.js rename resources/{ => javascript}/.tesselinclude (100%) rename resources/{ => javascript}/index.js (100%) rename resources/{ => javascript}/init-config.js (100%) rename resources/{ => javascript}/init-default.js (100%) create mode 100644 resources/rust/Cargo.toml create mode 100644 resources/rust/main.rs create mode 100644 test/unit/init.js diff --git a/bin/tessel-2.js b/bin/tessel-2.js index fdd38698..f0fdf516 100755 --- a/bin/tessel-2.js +++ b/bin/tessel-2.js @@ -41,7 +41,7 @@ if (!isRoot()) { } } -controller.t2Init = init; +controller.t2Init = init.initProject; controller.crashReporter = function(options) { var cr = Promise.resolve(); @@ -333,6 +333,12 @@ parser.command('init') 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/controller.js b/lib/controller.js index bc9cac0e..83e849d3 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -594,7 +594,7 @@ controller.renameTessel = function(opts) { .then(function executeRename() { return controller.standardTesselCommand(opts, function(tessel) { log.info(`Renaming ${tessel.name} to ${opts.newName}`); - return tessel.rename(opts); + return tessel.rename(opts); }); }); }; 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 62% rename from lib/init.js rename to lib/init/javascript.js index 5ffcdff2..04aa7d57 100644 --- a/lib/init.js +++ b/lib/init/javascript.js @@ -7,13 +7,19 @@ var PZ = require('promzard').PromZard; var NPM = require('npm'); // Internal -var log = require('./log'); - +var log = require('../log'); 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) { @@ -24,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,6 +63,7 @@ function buildJSON(npmConfig) { pkg[k] = data[k]; } }); + log.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,63 +85,52 @@ 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() { - return new Promise(resolve => { +// // 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; - } - - // 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'; + return resolve(); } // Pipe the contents of the default file into a new file - fs.createReadStream(path.resolve(__dirname + './../resources/' + filename)) + fs.createReadStream(path.resolve(__dirname, './../../', 'resources/javascript', filename)) .pipe(fs.createWriteStream(filename)); log.info('Wrote "Hello World" to index.js'); - resolve(); + return resolve(); }); }); -} +}; -function createTesselinclude() { +exportables.createTesselinclude = () => { var tesselinclude = '.tesselinclude'; return new Promise((resolve) => { fs.exists(tesselinclude, (exists) => { @@ -139,15 +138,14 @@ function createTesselinclude() { return resolve(); } - fs.copy(path.resolve(__dirname + './../resources/' + tesselinclude), tesselinclude, () => { - log.info('Created .tesselinclude.'); - resolve(); - }); + fs.copySync(path.resolve(__dirname, './../../', 'resources/javascript', tesselinclude), tesselinclude); + log.info('Created .tesselinclude.'); + resolve(); }); }); -} +}; -function readPackageJson() { +exportables.readPackageJson = () => { return new Promise(function(resolve, reject) { fs.readFile(packageJson, 'utf8', function(err, data) { if (err) { @@ -157,9 +155,9 @@ function readPackageJson() { resolve(data); }); }); -} +}; -function writePackageJson(data) { +exportables.writePackageJson = (data) => { return new Promise(function(resolve, reject) { fs.writeFile(packageJson, data, function(err) { if (err) { @@ -168,19 +166,19 @@ function writePackageJson(data) { 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) => { - log.info('Initializing Tessel 2 Project...'); + // Make the options global + options = opts; - return readPackageJson() + log.info('Initializing new Tessel project for JavaScript...'); + return exportables.readPackageJson() .then(function(data) { // Try to parse current package JSON @@ -206,19 +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(() => {}) - .catch((error) => { + .then(exportables.getDependencies) + .then(exportables.npmInstall) + .then(exportables.createTesselinclude) + .then(exportables.generateJavaScriptSample) + .catch(function(error) { log.error(error); }); }; + +module.exports = exportables; diff --git a/lib/init/python.js b/lib/init/python.js new file mode 100644 index 00000000..c00e6cef --- /dev/null +++ b/lib/init/python.js @@ -0,0 +1,18 @@ +// System Objects + +// Third Party Dependencies + +// Internal +var log = require('../log'); + +var exportables = {}; + +exportables.meta = { + keywords: ['py', 'python'] +}; + +exportables.generateProject = () => { + log.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..0146a79d --- /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 log = require('../log'); + +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)); + log.info('Initialized Cargo project...'); + fs.createReadStream(path.resolve(__dirname, './../../resources/rust/', file_src)).pipe(fs.createWriteStream(path_src)); + log.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 767f6ff3..d0e32a6f 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 b52c0a0c..4e8bfaae 100644 --- a/test/common/bootstrap.js +++ b/test/common/bootstrap.js @@ -58,6 +58,10 @@ global.log = require('../../lib/log'); 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 912f2e31..fa7b88ec 100644 --- a/test/unit/bin-tessel-2.js +++ b/test/unit/bin-tessel-2.js @@ -757,6 +757,7 @@ exports['--output true/false'] = { exports['Tessel (cli: crash-reporter)'] = { + setUp: function(done) { this.sandbox = sinon.sandbox.create(); this.spinnerStart = this.sandbox.stub(log.spinner, 'start'); @@ -901,5 +902,65 @@ exports['Tessel (cli: crash-reporter)'] = { test.done(); }); }, +}; + +exports['Tessel (init)'] = { + setUp: function(done) { + this.sandbox = sinon.sandbox.create(); + this.warn = this.sandbox.stub(log, 'warn'); + this.info = this.sandbox.stub(log, 'info'); + + this.successfulCommand = this.sandbox.stub(cli, 'closeSuccessfulCommand'); + this.failedCommand = this.sandbox.stub(cli, 'closeFailedCommand'); + this.initProject = this.sandbox.spy(controller, 't2Init'); + 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..2579f9d2 --- /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.logWarn = this.sandbox.stub(log, 'warn', function() {}); + this.logInfo = this.sandbox.stub(log, '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.logWarn = this.sandbox.stub(log, 'warn', function() {}); + this.logInfo = this.sandbox.stub(log, 'info', function() {}); + done(); + }, + tearDown: (done) => { + this.sandbox.restore(); + done(); + }, +}; + +exports['init --lang python'] = { + setUp: (done) => { + this.sandbox = sinon.sandbox.create(); + this.logWarn = this.sandbox.stub(log, 'warn', function() {}); + this.logInfo = this.sandbox.stub(log, 'info', function() {}); + done(); + }, + tearDown: (done) => { + this.sandbox.restore(); + done(); + }, +};