commit ec516c106c35d2a66b124a2a5497e17d5377575b Author: Loic Nageleisen Date: Mon Aug 19 18:44:10 2013 +0200 uspec.js: first public release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6c7c28 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f070a65 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2013, Loic Nageleisen +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7ecb7a7 --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +all: release + +release: uspec.js + +%.js: %.coffee + coffee --compile $< + +run_spec.js: fake_module.js uspec.js uspec_spec.js + cat $^ > $@ + +node_spec: uspec_spec.js uspec.js + node $< + +phantom_spec: run_spec.js + phantomjs $< + +spec: node_spec phantom_spec + +clean: + @rm -f *.js + +.PHONY: release node_spec phantom_spec spec diff --git a/README.mdown b/README.mdown new file mode 100644 index 0000000..b0ac2ab --- /dev/null +++ b/README.mdown @@ -0,0 +1,4 @@ +# µSpec + +µSpec is a minimalist, easy to use spec system that runs in various +environments, from nodejs to browsers. diff --git a/fake_module.coffee b/fake_module.coffee new file mode 100644 index 0000000..032d1b8 --- /dev/null +++ b/fake_module.coffee @@ -0,0 +1,4 @@ +root = global || window +root.exports = {} +root.require = -> exports + diff --git a/uspec.coffee b/uspec.coffee new file mode 100644 index 0000000..4bd0c89 --- /dev/null +++ b/uspec.coffee @@ -0,0 +1,205 @@ +### +# uspec.js v0.5 +# (c) 2013 Loic Nageleisen +# Licensed under 3-clause BSD +### + +class Ansi + @CSI = "\x1B[" + @END = "m" + + @BLACK = 0 + @RED = 1 + @GREEN = 2 + @YELLOW = 3 + @BLUE = 4 + @MAGENTA = 5 + @CYAN = 6 + @WHITE = 7 + + @FORE = 30 + @BACK = 40 + + @SGR_RESET = @CSI + @END + @SGR_BLACK = @CSI + (@FORE + @BLACK) + @END + @SGR_RED = @CSI + (@FORE + @RED) + @END + @SGR_GREEN = @CSI + (@FORE + @GREEN) + @END + @SGR_YELLOW = @CSI + (@FORE + @YELLOW) + @END + @SGR_BLUE = @CSI + (@FORE + @BLUE) + @END + @SGR_MAGENTA = @CSI + (@FORE + @MAGENTA) + @END + @SGR_CYAN = @CSI + (@FORE + @CYAN) + @END + @SGR_WHITE = @CSI + (@FORE + @WHITE) + @END + + +indent = (str, level = 0) -> + str = indent(str, level - 1) if level > 0 + str.replace(/^/gm, ' ') + + +all_examples = {} + + +describe = (target) -> + [setup, teardown, examples] = switch arguments.length + when 2 then [(-> {}), (->), arguments[1]] + when 3 then [arguments[1], (->), arguments[2]] + else [arguments[1], arguments[2], arguments[3]] + + all_examples[target] = + setup: setup + teardown: teardown + examples: examples + + +class AssertionError + constructor: (@message) -> + toString: -> "AssertionError: #{@message}" + + +class PendingError + constructor: (@message) -> + toString: -> "PendingError: #{@message}" + + +assert = (callback) -> + unless callback() == true + throw new AssertionError "assertion failed:\n#{indent callback.toString()}" + + +assert_throws = (exception, callback) -> + try + callback() + catch e + return if e instanceof exception + throw e + throw new AssertionError "assertion failed:\n#{indent callback.toString()}" + + +pending = (message = '') -> + throw new PendingError message + + +run = (log = new Log(new AnsiWriter)) -> + for target, info of all_examples + log.current target + for example, test of info.examples + try + throw new PendingError if test.toString() == (->).toString() + test.call(info.setup()) + info.teardown() + log.pass example + catch e + if e instanceof AssertionError + log.fail example, e + else if e instanceof PendingError + log.pending example, e + else + log.fail example, e + + log.results + + +PASS = 'pass' +FAIL = 'fail' +PENDING = 'pending' + + +class Log + constructor: (@writer = new NaiveWriter) -> + @results = {} + + log: (example, status, message = '') -> + @results[@target][example] = + status: status + message: message + + current: (target) -> + @target = target + @results[@target] = {} + @writer.current(target) + + pass: (example) -> + this.log(example, PASS) + @writer.pass(example) + + fail: (example, e) -> + this.log(example, FAIL, e.message) + @writer.fail(example) + + pending: (example, p) -> + this.log(example, PENDING) + @writer.pending(example, p.message) + + +summary = (results) -> + total = + pass: 0 + fail: 0 + pending: 0 + for target, examples of results + for example, result of examples + switch result.status + when PASS then total.pass += 1 + when FAIL then total.fail += 1 + when PENDING then total.pending += 1 + total.all = total.pass + total.fail + total.pending + + console.log '' + console.log 'Failures:\n' if total.fail > 0 + counter = 0 + for target, examples of results + for example, result of examples + if result.status == FAIL + counter += 1 + console.log indent "#{counter}) #{Ansi.SGR_RED}#{target} #{example}#{Ansi.SGR_RESET}" + unless result.message is undefined + console.log indent(result.message, 2) + "\n" + + console.log "#{total.all} examples, #{total.fail} failure, #{total.pending} pending" + + total.fail == 0 + + +class AnsiWriter + constructor: -> + current: (target) -> console.log "\n#{target}" + pass: (example) -> console.log " #{Ansi.SGR_GREEN}#{example}#{Ansi.SGR_RESET}" + fail: (example) -> console.log " #{Ansi.SGR_RED}#{example}#{Ansi.SGR_RESET}" + pending: (example) -> console.log " #{Ansi.SGR_YELLOW}#{example}#{Ansi.SGR_RESET}" + + +class AnsiDotWriter + current: (target) -> + pass: (example) -> console.log "#{Ansi.SGR_GREEN}.#{Ansi.SGR_RESET}" + fail: (example) -> console.log "#{Ansi.SGR_RED}F#{Ansi.SGR_RESET}" + pending: (example) -> console.log "#{Ansi.SGR_YELLOW}##{Ansi.SGR_RESET}" + + +class DotWriter + current: (target) -> + pass: (example) -> console.log "." + fail: (example) -> console.log "F" + pending: (example) -> console.log "#" + + +class NaiveWriter + current: (target) -> console.log "#{target}" + pass: (example) -> console.log " #{example}: pass" + fail: (example) -> console.log " #{example}: fail" + pending: (example) -> console.log " #{example}: pending" + +class NullWriter + current: -> + pass: -> + fail: -> + pending: -> + + +exports.run = run +exports.describe = describe +exports.assert = assert +exports.assert_throws = assert_throws +exports.pending = pending +exports.summary = summary +exports.AssertionError = AssertionError +exports.PendingError = PendingError diff --git a/uspec.js b/uspec.js new file mode 100644 index 0000000..e9e13b6 --- /dev/null +++ b/uspec.js @@ -0,0 +1,398 @@ +// Generated by CoffeeScript 1.6.2 +/* +# uspec.js v0.5 +# (c) 2013 Loic Nageleisen +# Licensed under 3-clause BSD +*/ + + +(function() { + var Ansi, AnsiDotWriter, AnsiWriter, AssertionError, DotWriter, FAIL, Log, NaiveWriter, NullWriter, PASS, PENDING, PendingError, all_examples, assert, assert_throws, describe, indent, pending, run, summary; + + Ansi = (function() { + function Ansi() {} + + Ansi.CSI = "\x1B["; + + Ansi.END = "m"; + + Ansi.BLACK = 0; + + Ansi.RED = 1; + + Ansi.GREEN = 2; + + Ansi.YELLOW = 3; + + Ansi.BLUE = 4; + + Ansi.MAGENTA = 5; + + Ansi.CYAN = 6; + + Ansi.WHITE = 7; + + Ansi.FORE = 30; + + Ansi.BACK = 40; + + Ansi.SGR_RESET = Ansi.CSI + Ansi.END; + + Ansi.SGR_BLACK = Ansi.CSI + (Ansi.FORE + Ansi.BLACK) + Ansi.END; + + Ansi.SGR_RED = Ansi.CSI + (Ansi.FORE + Ansi.RED) + Ansi.END; + + Ansi.SGR_GREEN = Ansi.CSI + (Ansi.FORE + Ansi.GREEN) + Ansi.END; + + Ansi.SGR_YELLOW = Ansi.CSI + (Ansi.FORE + Ansi.YELLOW) + Ansi.END; + + Ansi.SGR_BLUE = Ansi.CSI + (Ansi.FORE + Ansi.BLUE) + Ansi.END; + + Ansi.SGR_MAGENTA = Ansi.CSI + (Ansi.FORE + Ansi.MAGENTA) + Ansi.END; + + Ansi.SGR_CYAN = Ansi.CSI + (Ansi.FORE + Ansi.CYAN) + Ansi.END; + + Ansi.SGR_WHITE = Ansi.CSI + (Ansi.FORE + Ansi.WHITE) + Ansi.END; + + return Ansi; + + })(); + + indent = function(str, level) { + if (level == null) { + level = 0; + } + if (level > 0) { + str = indent(str, level - 1); + } + return str.replace(/^/gm, ' '); + }; + + all_examples = {}; + + describe = function(target) { + var examples, setup, teardown, _ref; + + _ref = (function() { + switch (arguments.length) { + case 2: + return [ + (function() { + return {}; + }), (function() {}), arguments[1] + ]; + case 3: + return [arguments[1], (function() {}), arguments[2]]; + default: + return [arguments[1], arguments[2], arguments[3]]; + } + }).apply(this, arguments), setup = _ref[0], teardown = _ref[1], examples = _ref[2]; + return all_examples[target] = { + setup: setup, + teardown: teardown, + examples: examples + }; + }; + + AssertionError = (function() { + function AssertionError(message) { + this.message = message; + } + + AssertionError.prototype.toString = function() { + return "AssertionError: " + this.message; + }; + + return AssertionError; + + })(); + + PendingError = (function() { + function PendingError(message) { + this.message = message; + } + + PendingError.prototype.toString = function() { + return "PendingError: " + this.message; + }; + + return PendingError; + + })(); + + assert = function(callback) { + if (callback() !== true) { + throw new AssertionError("assertion failed:\n" + (indent(callback.toString()))); + } + }; + + assert_throws = function(exception, callback) { + var e; + + try { + callback(); + } catch (_error) { + e = _error; + if (e instanceof exception) { + return; + } + throw e; + } + throw new AssertionError("assertion failed:\n" + (indent(callback.toString()))); + }; + + pending = function(message) { + if (message == null) { + message = ''; + } + throw new PendingError(message); + }; + + run = function(log) { + var e, example, info, target, test, _ref; + + if (log == null) { + log = new Log(new AnsiWriter); + } + for (target in all_examples) { + info = all_examples[target]; + log.current(target); + _ref = info.examples; + for (example in _ref) { + test = _ref[example]; + try { + if (test.toString() === (function() {}).toString()) { + throw new PendingError; + } + test.call(info.setup()); + info.teardown(); + log.pass(example); + } catch (_error) { + e = _error; + if (e instanceof AssertionError) { + log.fail(example, e); + } else if (e instanceof PendingError) { + log.pending(example, e); + } else { + log.fail(example, e); + } + } + } + } + return log.results; + }; + + PASS = 'pass'; + + FAIL = 'fail'; + + PENDING = 'pending'; + + Log = (function() { + function Log(writer) { + this.writer = writer != null ? writer : new NaiveWriter; + this.results = {}; + } + + Log.prototype.log = function(example, status, message) { + if (message == null) { + message = ''; + } + return this.results[this.target][example] = { + status: status, + message: message + }; + }; + + Log.prototype.current = function(target) { + this.target = target; + this.results[this.target] = {}; + return this.writer.current(target); + }; + + Log.prototype.pass = function(example) { + this.log(example, PASS); + return this.writer.pass(example); + }; + + Log.prototype.fail = function(example, e) { + this.log(example, FAIL, e.message); + return this.writer.fail(example); + }; + + Log.prototype.pending = function(example, p) { + this.log(example, PENDING); + return this.writer.pending(example, p.message); + }; + + return Log; + + })(); + + summary = function(results) { + var counter, example, examples, result, target, total; + + total = { + pass: 0, + fail: 0, + pending: 0 + }; + for (target in results) { + examples = results[target]; + for (example in examples) { + result = examples[example]; + switch (result.status) { + case PASS: + total.pass += 1; + break; + case FAIL: + total.fail += 1; + break; + case PENDING: + total.pending += 1; + } + } + } + total.all = total.pass + total.fail + total.pending; + console.log(''); + if (total.fail > 0) { + console.log('Failures:\n'); + } + counter = 0; + for (target in results) { + examples = results[target]; + for (example in examples) { + result = examples[example]; + if (result.status === FAIL) { + counter += 1; + console.log(indent("" + counter + ") " + Ansi.SGR_RED + target + " " + example + Ansi.SGR_RESET)); + if (result.message !== void 0) { + console.log(indent(result.message, 2) + "\n"); + } + } + } + } + console.log("" + total.all + " examples, " + total.fail + " failure, " + total.pending + " pending"); + return total.fail === 0; + }; + + AnsiWriter = (function() { + function AnsiWriter() {} + + AnsiWriter.prototype.current = function(target) { + return console.log("\n" + target); + }; + + AnsiWriter.prototype.pass = function(example) { + return console.log(" " + Ansi.SGR_GREEN + example + Ansi.SGR_RESET); + }; + + AnsiWriter.prototype.fail = function(example) { + return console.log(" " + Ansi.SGR_RED + example + Ansi.SGR_RESET); + }; + + AnsiWriter.prototype.pending = function(example) { + return console.log(" " + Ansi.SGR_YELLOW + example + Ansi.SGR_RESET); + }; + + return AnsiWriter; + + })(); + + AnsiDotWriter = (function() { + function AnsiDotWriter() {} + + AnsiDotWriter.prototype.current = function(target) {}; + + AnsiDotWriter.prototype.pass = function(example) { + return console.log("" + Ansi.SGR_GREEN + "." + Ansi.SGR_RESET); + }; + + AnsiDotWriter.prototype.fail = function(example) { + return console.log("" + Ansi.SGR_RED + "F" + Ansi.SGR_RESET); + }; + + AnsiDotWriter.prototype.pending = function(example) { + return console.log("" + Ansi.SGR_YELLOW + "#" + Ansi.SGR_RESET); + }; + + return AnsiDotWriter; + + })(); + + DotWriter = (function() { + function DotWriter() {} + + DotWriter.prototype.current = function(target) {}; + + DotWriter.prototype.pass = function(example) { + return console.log("."); + }; + + DotWriter.prototype.fail = function(example) { + return console.log("F"); + }; + + DotWriter.prototype.pending = function(example) { + return console.log("#"); + }; + + return DotWriter; + + })(); + + NaiveWriter = (function() { + function NaiveWriter() {} + + NaiveWriter.prototype.current = function(target) { + return console.log("" + target); + }; + + NaiveWriter.prototype.pass = function(example) { + return console.log(" " + example + ": pass"); + }; + + NaiveWriter.prototype.fail = function(example) { + return console.log(" " + example + ": fail"); + }; + + NaiveWriter.prototype.pending = function(example) { + return console.log(" " + example + ": pending"); + }; + + return NaiveWriter; + + })(); + + NullWriter = (function() { + function NullWriter() {} + + NullWriter.prototype.current = function() {}; + + NullWriter.prototype.pass = function() {}; + + NullWriter.prototype.fail = function() {}; + + NullWriter.prototype.pending = function() {}; + + return NullWriter; + + })(); + + exports.run = run; + + exports.describe = describe; + + exports.assert = assert; + + exports.assert_throws = assert_throws; + + exports.pending = pending; + + exports.summary = summary; + + exports.AssertionError = AssertionError; + + exports.PendingError = PendingError; + +}).call(this); diff --git a/uspec_spec.coffee b/uspec_spec.coffee new file mode 100644 index 0000000..4fae772 --- /dev/null +++ b/uspec_spec.coffee @@ -0,0 +1,111 @@ +minispec = require('./uspec') + +describe = minispec.describe +assert = minispec.assert +assert_throws = minispec.assert_throws +pending = minispec.pending +run = minispec.run +summary = minispec.summary + + +describe 'AssertionError', + 'should be exported': -> + assert -> typeof minispec.AssertionError isnt 'undefined' + 'should take a message': -> + assert -> (new minispec.AssertionError 'foo').message == 'foo' + 'should render as an error string': -> + str = (new minispec.AssertionError 'foo').toString() + assert -> str == "AssertionError: foo" + + +describe 'PendingError', + 'should be exported': -> + assert -> typeof minispec.PendingError isnt 'undefined' + 'should take a message': -> + assert -> (new minispec.PendingError 'foo').message == 'foo' + 'should render as an error string': -> + str = (new minispec.PendingError 'foo').toString() + assert -> str == "PendingError: foo" + + +describe 'pending', + 'should be exported': -> + assert -> typeof minispec.pending isnt 'undefined' + + 'should throw a PendingError containing a blank message': -> + try + pending() + catch e + assert -> e instanceof minispec.PendingError + assert -> e.message == '' + + 'should throw a PendingError containing a message': -> + try + pending('foo') + catch e + assert -> e instanceof minispec.PendingError + assert -> e.message == 'foo' + + +describe 'assert', + 'should be exported': -> + assert -> typeof minispec.assert isnt 'undefined' + + 'should pass without throwing when assertion returns true': -> + do -> + minispec.assert -> true + + 'should throw AssertionError when assertion does not return true': -> + try + minispec.assert -> false + catch e + unless e instanceof minispec.AssertionError + throw new AssertionError + + 'should rethrow exception when assertion throws an exception': -> + try + minispec.assert -> throw new Error 'foo' + catch e + unless e instanceof Error + throw new AssertionError + unless e.message == 'foo' + throw new AssertionError + + +describe 'assert_throws', + 'should be exported': -> + assert -> typeof minispec.assert_throws isnt 'undefined' + + 'should pass when block throws the expected exception': -> + class FooError + do -> + minispec.assert_throws FooError, -> throw new FooError + + 'should throw AssertionError when block does not throw any exception': -> + class FooError + try + minispec.assert_throws FooError, -> 42 + catch e + assert -> e instanceof minispec.AssertionError + + 'should rethrow exception when block throws an unexpected exception': -> + class FooError + class BarError + try + minispec.assert_throws FooError, -> throw new BarError + catch e + assert -> e instanceof BarError + +describe 'describe', + 'should be exported': -> + assert -> typeof minispec.describe isnt 'undefined' + +describe 'run', + 'should be exported': -> + assert -> typeof minispec.run isnt 'undefined' + +results = run() +rc = if summary(results) then 0 else 1 + +phantom.exit(rc) unless typeof phantom is 'undefined' +process.exit(rc) unless typeof process is 'undefined'