diff --git a/.gitignore b/.gitignore index 427043b..fd66f45 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ *.swp tests/*.diff +spec-script +spec-template diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4729540 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "spec"] + path = spec + url = https://github.com/mustache/spec diff --git a/README.md b/README.md index 307c546..0101b49 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ Requirements * The "coreutils" package (`basename` and `cat`) * ... that's it. Why? Because bash **can**! +If you intend to develop this and run the official specs, you also need node.js. + Concessions ----------- @@ -46,6 +48,8 @@ Developing Check out the code and hack away. Please add tests to show off bugs before fixing them. New functionality should also be covered by a test. +To run against the official specs, you need to make sure you have the "spec" submodule. If you see a `spec/` folder with stuff in it, you're already set. Otherwise run `git submodule update --init`. After that you need to install node.js and run `npm install async` (no, I didn't make a package.json to just list one dependency). Finally, `./run-spec.js spec/specs/*.json` will run against the official tests - there's over 100 of them. + License ------- diff --git a/run-spec.js b/run-spec.js new file mode 100755 index 0000000..b44556e --- /dev/null +++ b/run-spec.js @@ -0,0 +1,201 @@ +#!/usr/bin/env node + +var async, exec, fs, summary, specFiles; + +function makeShellString(value) { + if (typeof value === 'string') { + return JSON.stringify(value); + } + + if (typeof value === 'number') { + return value; + } + + return 'ERR_CONVERTING'; +} + +function addToEnvironment(name, value) { + var result; + + if (Array.isArray(value)) { + result = [ + '(' + ]; + value.forEach(function (subValue) { + result.push(makeShellString(subValue)); + }); + result.push(')'); + + return name + '=' + result.join(' '); + } + + if (typeof value === 'object') { + return '# ' + name + ' is an object and will not work in bash'; + } + + if (typeof value === 'boolean') { + if (value) { + return name + '="true"'; + } + + return name + '=""'; + } + + return name + '=' + makeShellString(value); +} + +function runTest(test, done) { + var output, script; + + script = [ + '#!/bin/bash' + ]; + + Object.keys(test.data).forEach(function (name) { + script.push(addToEnvironment(name, test.data[name])); + }); + script.push('. mo spec-template'); + test.script = script.join('\n'); + async.series([ + function (taskDone) { + fs.writeFile('spec-script', test.script, taskDone); + }, + function (taskDone) { + fs.writeFile('spec-template', test.template, taskDone); + }, + function (taskDone) { + exec('bash spec-script', function (err, stdout) { + if (err) { + return taskDone(err); + } + + output = stdout; + taskDone(); + }); + }, + function (taskDone) { + fs.unlink('spec-script', taskDone); + }, + function (taskDone) { + fs.unlink('spec-template', taskDone); + } + ], function (err) { + if (err) { + return done(err); + } + + done(null, output); + }); + + return ''; +} + +function prepareAndRunTest(test, done) { + async.waterfall([ + function (taskDone) { + console.log('### ' + test.name); + console.log(''); + console.log(test.desc); + console.log(''); + runTest(test, taskDone); + }, + function (actual, taskDone) { + test.actual = actual; + test.pass = (test.actual === test.expected); + + if (test.pass) { + console.log('Passed.'); + } else { + console.log('Failed.'); + console.log(''); + console.log(test); + } + + console.log(''); + taskDone(); + } + ], done); +} + +function specFileToName(file) { + return file.replace(/.*\//, '').replace('.json', '').replace('~', '').replace(/(^|-)[a-z]/g, function (match) { + return match.toUpperCase(); + }); +} + +function processSpecFile(specFile, done) { + fs.readFile(specFile, 'utf8', function (err, data) { + var name; + + if (err) { + return done(err); + } + + name = specFileToName(specFile); + data = JSON.parse(data); + console.log(name); + console.log('===================='); + console.log(''); + console.log(data.overview); + console.log(''); + console.log('Tests'); + console.log('-----'); + console.log(''); + async.series([ + function (taskDone) { + async.eachSeries(data.tests, prepareAndRunTest, taskDone); + }, + function (taskDone) { + summary[name] = {}; + data.tests.forEach(function (test) { + summary[name][test.name] = test.pass; + }); + taskDone(); + } + ], done); + }); +} + +// 0 = node, 1 = script, 2 = file +if (process.argv.length < 3) { + console.log('Specify spec files on the command line'); + process.exit(); +} + +async = require('async'); +fs = require('fs'); +exec = require('child_process').exec; +summary = {}; +async.eachSeries(process.argv.slice(2), processSpecFile, function () { + var fail, pass; + + console.log(''); + console.log('Summary'); + console.log('======='); + console.log(''); + pass = 0; + fail = 0; + Object.keys(summary).forEach(function (name) { + var groupPass, groupFail, testResults; + + testResults = []; + groupPass = 0; + groupFail = 0; + Object.keys(summary[name]).forEach(function (testName) { + if (summary[name][testName]) { + testResults.push(' * pass - ' + testName); + groupPass += 1; + pass += 1; + } else { + testResults.push(' * FAIL - ' + testName); + groupFail += 1; + fail += 1; + } + }); + testResults.unshift('* ' + name + ' (failed ' + groupFail + ' out of ' + (groupPass + groupFail) + ' tests)'); + console.log(testResults.join('\n')); + }); + + console.log(''); + console.log('Failed ' + fail + ' out of ' + (pass + fail) + ' tests'); +}); diff --git a/spec b/spec new file mode 160000 index 0000000..934db98 --- /dev/null +++ b/spec @@ -0,0 +1 @@ +Subproject commit 934db98d8ba854574d18aa59a01d2c9aee41944d