uspec.js: first public release

This commit is contained in:
Loic Nageleisen 2013-08-19 18:44:10 +02:00
commit ec516c106c
8 changed files with 769 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.js

24
LICENSE Normal file
View file

@ -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.

22
Makefile Normal file
View file

@ -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

4
README.mdown Normal file
View file

@ -0,0 +1,4 @@
# µSpec
µSpec is a minimalist, easy to use spec system that runs in various
environments, from nodejs to browsers.

4
fake_module.coffee Normal file
View file

@ -0,0 +1,4 @@
root = global || window
root.exports = {}
root.require = -> exports

205
uspec.coffee Normal file
View file

@ -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

398
uspec.js Normal file
View file

@ -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);

111
uspec_spec.coffee Normal file
View file

@ -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'