Skip to content

Commit

Permalink
Implemented build process, added documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
mde committed Jul 17, 2010
1 parent 42bd6e6 commit fb82834
Show file tree
Hide file tree
Showing 13 changed files with 369 additions and 30 deletions.
33 changes: 33 additions & 0 deletions Makefile
@@ -0,0 +1,33 @@
#
# Logan client-/server-side JavaScript test runner
# Copyright 2112 Matthew Eernisse (mde@fleegix.org)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

.PHONY: all build install clean uninstall test

all: build

build:
@mkdir -p ./dist; cp -r lib/* dist; echo 'Logan built.'

install:
@./scripts/jake -f `pwd`/scripts/Jakefile default

clean:
@rm -fr dist

uninstall:
@rm -fr dist; rm -fr ~/.node_libraries/logan; rm -f /usr/local/bin/logan; echo 'Logan uninstalled.'

Empty file removed README
Empty file.
143 changes: 143 additions & 0 deletions README.markdown
@@ -0,0 +1,143 @@
## Logan: a minimal test runner for both server- and client-side JavaScript
- - -

### Installing

Prerequisites: Logan requires Node.js. (<http://nodejs.org/>)

Get Logan:

git clone git://github.com/mde/logan.git

Build Logan:

cd logan && make && sudo make install

### Basic usage

logan [browser|node|racer]

### Description

Logan is a minimal test runner for both client- and
server-side JavaScript. It uses the Node.js assert library.

Logan supports the following envrironments:
* Browsers (via HTTP global-eval/script-append)
* Node.js
* TheRubyRacer

### Test syntax

A Logan test file looks like this:

var testNamespace = new function () {
this.testFoo = function () {
assert.ok(true);
};

this.testBar = function () {
assert.equal(1, 1);
};

}();

logan.run(testNamespace);

Pretty simple.

### Sharing client- and server-side JavaScript code and tests

For Logan's testing purposes, "server-side" means in Node.js,
which uses CommonJS modules. "Client-side" includes the
browsers, and TheRubyRacer, where there is no facility for
loading code modules from within the runtime.

There are a few simple rules to follow when creating a module
to use both in client and server:

1. Name your namespace object in side the module file the same
name as the variable you'll be setting it to when you load it
via CommonJS require.

2. Only export your namespace in the CommonJS environment.

For example, in the module file, foo.js:


if (typeof foo != 'undefined') { foo = {}; }

foo.bar = new function () {
this.a = 1;
this.b = function () {};
}();

if (typeof module != 'undefined') { module.exports = foo.bar; }


Or, if you are exporting a constructor:

if (typeof foo != 'undefined') { foo = {}; }

foo.Baz = function () {
this.a = 2;
this.b = [];
};

if (typeof exports != 'undefined') { exports.Baz = foo.Baz; }

3. When you call require in your source code, use the same
name as in the module.

4. Create any top-level namespace objects non-destructively.

In your tests, if you're creating top-level namespaces to hang
your required objects on, use something to create them that
checks for their existence before creating.

Logan includes a utility function, `logan.namespace`, to help
with this. In your own app's code, you'll either have to use
a utility function for doing this, or simply check with a
`typeof` check for `undefined`.

Here's how to load and use the previous two examples:

logan.namespace('foo');
foo.bar = require('./path/to/foo');
foo.Bar = require('./path/to/bar').Bar;

var fooTests = new function () {
this.testFooBarAIsTruthy = function () {
assert.ok(foo.bar.a);
};

this.testFooBazAIsNumber = function () {
var barInstance = new foo.Bar();
assert.equal(typeof barInstance.a, 'number');
};

}();

logan.run(fooTests);

The reason for these special requirements is the difference
between the way that code loads on the client and the way it
loads via CommonJS.

In the client environment (including TheRubyRacer), there
is no module-level scope, and the mechanism for loading code
doesn't return any value, so namespace creation has to happen
inside the module file, and has to be careful not to rewrite
anything other modules might have done.

Logan makes this work with CommonJS `require` syntax in your
tests by doing source-code transformation -- it uses the
require statements to know what code to load, and rewrites
the statements so they don't stomp on the results of the
eval operation.

### Author

Matthew Eernisse, mde@fleegix.org


19 changes: 16 additions & 3 deletions lib/browser/adapter.js
@@ -1,8 +1,20 @@

var BrowserAdapter = function () {};
var BrowserAdapter = function (logan) {
this.logan = logan;
};

BrowserAdapter.prototype = new function () {
this.files = [];

this.register = function (files) {
this.files = files;
var file;
while (file = this.files.shift()) {
this.loadTestFile(file);
}
this.logan.reportFinal();
};

this.loadTestFile = function (file) {
this.dirname = file.replace(/\/([^\/]+)\.js$/, '');
var content = this.getFile(file);
Expand Down Expand Up @@ -101,8 +113,9 @@ BrowserAdapter.prototype = new function () {
$('results').innerHTML += '<div class="' + cl + '">' + str + '</div>';
};

this.reportFinal = function (total, successes) {
var results = 'Tests run: ' + total + ' total, ' + successes + ' successes';
this.reportFinal = function (total, successes, failures) {
var results = 'Tests run: ' + total + ' total, ' +
successes + ' successes, ' + failures + ' failures';
$('header').innerHTML += results;
};

Expand Down
4 changes: 2 additions & 2 deletions lib/browser/index.html
Expand Up @@ -8,9 +8,9 @@
<script type="text/javascript" src="deps/fleegix.js"></script>
<script type="text/javascript" src="deps/assert.js"></script>
<script type="text/javascript">
logan.adapter = new BrowserAdapter();
logan.adapter = new BrowserAdapter(logan);
window.onload = function () {
logan.register(@@testfiles@@);
logan.adapter.register(@@testfiles@@);
};
</script>
<style type="text/css">
Expand Down
2 changes: 1 addition & 1 deletion lib/browser/runner.js
Expand Up @@ -53,7 +53,7 @@ exports.BrowserRunner = function (runner) {
}
});
server.listen(3030);
sys.puts('Logan running on localhost port 3030');
sys.puts('Logan ready to run tests on localhost port 3030');

};

Expand Down
42 changes: 28 additions & 14 deletions lib/logan.js
@@ -1,21 +1,11 @@

if (typeof logan == 'undefined') { logan = {}; }

var loganBase = new function () {
this.files = [];
this.dirname = null;
this.filename = null;
this.total = 0;
this.successes = 0;

this.register = function (files) {
this.files = files;
var file;
while (file = this.files.shift()) {
this.adapter.loadTestFile(file);
}
this.reportFinal(this.total, this.successes);
};

this.run = function (namespace) {
for (var p in namespace) {
if (/^test/.test(p) && typeof namespace[p] == 'function') {
Expand All @@ -26,7 +16,6 @@ var loganBase = new function () {
this.successes++;
}
catch (e) {
//console.log(e);
var msg = e.message || '';
msg = msg ? '(' + msg + ')' : '';
this.reportTest('Failure: ' + p + ' ' + msg);
Expand All @@ -43,8 +32,33 @@ var loganBase = new function () {
return this.adapter.reportTest(str, success);
};

this.reportFinal = function (total, successes) {
return this.adapter.reportFinal(total, successes);
this.reportFinal = function () {
var failures = this.total - this.successes;
return this.adapter.reportFinal(this.total, this.successes, failures);
};

this.namespace = function (namespaceString, explicitContext) {
var spaces = namespaceString.split('.')
, ctxt = explicitContext
, key;
if (!ctxt) {
if (typeof window != 'undefined') {
ctxt = window;
}
if (typeof global != 'undefined') {
ctxt = global;
}
}
if (!ctxt) {
throw new Error('No context available for creating a namespace object.');
}
for (var i = 0, ii = spaces.length; i < ii; i++) {
key = spaces[i];
if (!ctxt[key]) {
ctxt[key] = {};
}
ctxt = ctxt[key];
}
};

}();
Expand Down
9 changes: 6 additions & 3 deletions lib/node/adapter.js
@@ -1,15 +1,18 @@
var sys = require('sys');

var NodeAdapter = function () {};
var NodeAdapter = function (logan) {
this.logan = logan;
};

NodeAdapter.prototype = new function () {
this.reportTest = function (str, success) {
var cl = success ? 'success' : 'failure';
sys.puts(str);
};

this.reportFinal = function (total, successes) {
var results = 'Tests run: ' + total + ' total, ' + successes + ' successes';
this.reportFinal = function (total, successes, failures) {
var results = 'Tests run: ' + total + ' total, ' +
successes + ' successes, ' + failures + ' failures';
sys.puts(results);
};

Expand Down
5 changes: 4 additions & 1 deletion lib/node/runner.js
@@ -1,3 +1,5 @@
var sys = require('sys');

/**
* The runner for Node.js -- this is ridiculously easy. Just load up
* the tests and run them.
Expand All @@ -13,11 +15,12 @@ exports.NodeRunner = function (runner) {
// requires from the test source files in the other environments
global.logan = logan;
global.assert = require('assert');
sys.puts(list);
// Require each of the test files -- this will run the tests
for (var i = 0, ii = list.length; i < ii; i++) {
require(process.cwd() + list[i].replace(/^\./, '').replace(/\.js$/, ''));
}
logan.reportFinal(logan.total, logan.successes);
logan.reportFinal();
};
};

33 changes: 27 additions & 6 deletions lib/runner.js
Expand Up @@ -4,6 +4,20 @@ var http = require('http')
, ch = require('child_process')
, args = process.argv.slice(2);

var die = function (str) {
sys.puts(str);
process.exit();
}

var usage = ''
+ 'Logan client-/server-side JavaScript test runner\n'
+ '{Usage}: logan [browser|node|racer]'
+ '';

if (!args.length) {
die(usage);
}

var runner = new function () {
// List of available environments to run tests in
var _envMap = {browser: true, node: true, racer: true,};
Expand All @@ -28,8 +42,13 @@ var runner = new function () {
if (stderr) {
throw new Error(stderr);
}
_this.testList = _this.getTestList(stdout);
_this.run();
_this.testList = _this.getTestList(stdout);
if (_this.testList.length) {
_this.run();
}
else {
sys.puts('No test directories in this directory, no tests to run.');
}
});
};

Expand All @@ -38,10 +57,12 @@ var runner = new function () {
*/
this.getTestList = function (stdout) {
var files = []
, paths = stdout.replace(/\s$/, ''); // Trim trailing newline
paths = paths.split('\n');
for (var i = 0; i < paths.length; i++) {
files.push(paths[i]);
, paths = stdout.replace(/^\s|\s$/, ''); // Trim
if (stdout.length) {
paths = paths.split('\n');
for (var i = 0; i < paths.length; i++) {
files.push(paths[i]);
}
}
return files;
};
Expand Down

0 comments on commit fb82834

Please sign in to comment.