From b541ae3034205b11f2d079de71c76ed45dd872f0 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sun, 31 Mar 2024 19:35:02 -0400 Subject: [PATCH] Use JSDoc and convert to Javascript Classes --- src/TAP.js | 356 ---------------------------------------------- src/TAP.mjs | 323 +++++++++++++++++++++++++++++++++++++++++ tests/01_tap.t.js | 321 +++++++++++++++++++---------------------- 3 files changed, 471 insertions(+), 529 deletions(-) delete mode 100644 src/TAP.js create mode 100644 src/TAP.mjs diff --git a/src/TAP.js b/src/TAP.js deleted file mode 100644 index cdaf8c3..0000000 --- a/src/TAP.js +++ /dev/null @@ -1,356 +0,0 @@ -//grab the global scope -var testtop = this; - -Test = function() {}; - -Test.prototype.top = function() { - return testtop; -} - -Test.prototype.out = function(text) { - this.print(text); -}; - -Test.prototype.diag = function(msg){ - if (!msg) { - msg = " "; - } - this.out('# ' + msg); -}; - -Test.prototype.mk_tap = function(ok, description){ - if(!this.planned){ - this.out("You tried to run tests without a plan. Gotta have a plan."); - throw new Error("You tried to run tests without a plan. Gotta have a plan."); - } - this.counter++; - this.out(ok + ' ' + this.counter + ' - ' + description); -}; - -/* - -=head1 NAME - - Test.TAP - a 0 dependency TAP compliant test library useable from the commandline - -=head1 Synopsis - - var t = new Test.TAP; - t.plan(3); - - t.ok(true, 'True is True'); # test will pass - t.is(1, 2, 'one is two'); # test will fail - - var obj = {}; - obj.method1 = function() { return true; }; - - t.can_ok(obj, 'method1'); # test will pass - -=head1 DESCRIPTION - -Test.TAP is a javascript testing library that meets the needs of TDD for a commandline environment. - -=head1 METHODS - -=cut - -*/ - - -Test.TAP = function(out) { - this.planned = 0; - this.counter = 0; - this.passed = 0; - this.failed = 0; - this.print = out || function(text) { - if(typeof document == 'undefined') { - document = {}; - } - if(typeof document.write == 'undefined') { - document.write = print; - } - if (typeof print == 'undefined' - || document.write != print) { - text += '\n'; - } - document.write(text); - }; -}; - -Test.TAP.prototype = new Test; - -Test.TAP.prototype.pass = function(description) { - this.mk_tap('ok', description); -}; - -Test.TAP.prototype.fail = function(description) { - this.mk_tap('not ok', description); -}; - -Test.TAP.prototype.todo = function(func) { - var self = this; - var tapper = self.mk_tap; - self.mk_tap = function(ok, desc) { - tapper.apply(self, [ok, "# TODO: "+desc]); - } - func(); - self.mk_tap = tapper; -} - -Test.TAP.prototype.skip = function(crit, reason, count, func) { - var self = this; - if (crit) { - var tapper = self.mk_tap; - self.mk_tap = function(ok, desc) { - tapper.apply(self, [ok, desc]); - } - for(var i = 0; i < count; i++) { - self.fail("# SKIP "+reason) - } - self.mk_tap = tapper; - } else { - func(); - } -} - -/* - -=head2 plan() - - t.plan(3); - -Sets the test plan. Once set this can not be reset again. An attempt to change the plan once already set will throw an exception. - -=cut - -*/ - -Test.TAP.prototype.plan = function(tests) { - if (tests == 'no_plan') { - this.planned = tests; - } else { - if(this.planned > 0 || this.planned == 'no_plan'){ - throw new Error("you tried to set the plan twice!"); - } - this.planned = tests; - this.out('1..' + tests); - } -}; - -Test.TAP.prototype._pass_if = function(func, desc){ - var result = func(); - if(result){ this.pass(desc) } - else { this.fail(desc) } -} - - -/* - -=head2 diag() - - t.diag('a diagnostic message'); - -prints out a TAP compliant diagnostic message. - -=cut - -*/ - - -/* - -=head2 is() - - t.is(got, expected, 'test description'); - -tests that what we got matches what we expected. An equality test. - -=cut - -*/ - -Test.TAP.prototype.is = function(got, expected, desc) { - this._pass_if(function(){ return got == expected; }, desc); -}; - - -/* - -=head2 ok() - - t.ok(expression, 'test description'); - -Test that expression evaluates to true value - -=cut - -*/ - -Test.TAP.prototype.ok = function(got, desc) { - this._pass_if(function(){ return !!got; }, desc); -}; - - -/* - -=head2 like() - - t.like('string', /regex/, 'test description'); - -Tests that a string matches the regex. - -=cut - -*/ - -Test.TAP.prototype.like = function(string, regex, desc) { - this._pass_if(function(){ - if(regex instanceof RegExp) { - return string.match(regex) - } else { - return string.indexOf(regex) != -1 - } - }, desc) -} - -/* - -=head2 unlike() - - t.unlike('string', /regex/, 'test description'); - -The opposite of like. tests that the string doesn't match the regex - -=cut - -*/ - -Test.TAP.prototype.unlike = function(string, regex, desc) { - this._pass_if(function(){ - return !string.match(regex); - }, desc) -} - -/* - -=head2 can_ok() - - t.can_ok(obj, 'method1', method2'); - -tests that the object has the list of methods. outputs diagnostics about which ones are missing if the test fails. - -=cut - -*/ - - -Test.TAP.prototype.can_ok = function(obj) { - var desc = 'object can ['; - var pass = true; - for (i=1; i -L - -=head1 AUTHOR - -Jeremy Wall L<< jeremy@marzhillstudios.com >> - -=head1 COPYRIGHT AND LICENSE - -Copyright (C) 2007 Jeremy Wall - -This library is free software; you can redistribute it and/or modify -it under the same terms as Perl itself, either Perl version 5.8.4 or, -at your option, any later version of Perl 5 you may have available. - -=cut - -*/ - diff --git a/src/TAP.mjs b/src/TAP.mjs new file mode 100644 index 0000000..32b605e --- /dev/null +++ b/src/TAP.mjs @@ -0,0 +1,323 @@ +/** +* Tap - a 0 dependency TAP compliant test library useable from the commandline +* +* ```js +* import { Tap } from './src/TAP.mjs'; +* var t = new Tap; +* t.plan(3); +* +* t.ok(true, 'True is True'); # test will pass +* t.is(1, 2, 'one is two'); # test will fail +* +* var obj = {}; +* obj.method1 = function() { return true; }; +* +* t.can_ok(obj, 'method1'); # test will pass +* ``` +* +* @module TAP +* @license Artistic-2.0 +*/ + +/** + * The Tap Test Class helper. + */ +class Tap { + /** @type Number? */ + planned = null; + /** @type Number */ + counter = 0; + /** @type Number */ + passed = 0; + /** @type Number */ + failed = 0; + /** @type function(TestOutput) */ + #renderLine + + /** + * Construct a new Tap Suite with a renderLine function. + * @param {function(string)} + */ + constructor(renderFunc) { + this.#renderLine = renderFunc; + } + + + static Browser() { + return new Tap(function(text) { + // TODO(zaphar): Handle output in a Browser context. + }); + } + + static Node() { + return new Tap(function(text) { + import('node:process').then(loaded => {; + loaded.stdout.write(text + "\n"); + }) + }); + } + + /** Renders output for the test results */ + out(text) { + this.#renderLine(text); + }; + + /** + * Construct a Tap output message. + * + * @param {boolean} ok + * @param {string=} description + */ + mk_tap(ok, description){ + if(!this.planned){ + this.out("You tried to run tests without a plan. Gotta have a plan."); + throw new Error("You tried to run tests without a plan. Gotta have a plan."); + } + this.counter++; + this.out(ok + ' ' + this.counter + ' - ' + (description || "")); + }; + + + /** Diagnostic formatter for TAP Output. + * + * @param msg {string} + */ + diag(msg){ + if (!msg) { + msg = " "; + } + var lines = msg.split("\n"); + for (var line of lines) { + this.out('# ' + msg); + } + }; + + /** Render a pass TAP output message. + * @param {string} description + */ + pass(description) { + this.mk_tap('ok', description); + }; + + /** Render a fail TAP output message. + * @param {string} description + */ + fail(description) { + this.mk_tap('not ok', description); + }; + + /** Run a function as a TODO test. + * + * @param {function(this:Tap, boolean, description)} func + */ + todo(func) { + var self = this; + var tapper = self.mk_tap; + self.mk_tap = function(ok, desc) { + tapper.apply(self, [ok, "# TODO: "+desc]); + } + func(); + self.mk_tap = tapper; + } + + /** Run a function as a skip Test. + * + * @param {boolean} criteria + * @param {string} reason + * @param {number} count - The number of tests to skip + * @param {function(this:Tap, boolean, description)} func + */ + skip(criteria, reason, count, func) { + var self = this; + if (criteria) { + var tapper = self.mk_tap; + self.mk_tap = function(ok, desc) { + tapper.apply(self, [ok, desc]); + } + for(var i = 0; i < count; i++) { + self.fail("# SKIP "+reason) + } + self.mk_tap = tapper; + } else { + func(); + } + } + + /** Sets the test plan. + * Once set this can not be reset again. Any attempt to change the plan once already + * set will throw an exception. + * + * Call with no arguments if you don't want to specify the number of tests to run. + * + * @param {Number=} testCount + */ + plan(testCount) { + if(this.planned){ + throw new Error("you tried to set the plan twice!"); + } + if (!testCount) { + this.planned = 'no_plan'; + } else { + this.planned = testCount; + this.out('1..' + testCount); + } + }; + + #pass_if(func, desc){ + var result = func(); + if(result) { this.pass(desc) } + else { this.fail(desc) } + } + + // exception tests + + /** + * Tests that a function throws with a given error message. + * + * @param {function()} func + * @param {RegExp} msg + */ + throws_ok(func, msg) { + var errormsg = ' '; + if (typeof func != 'function') + this.diag('throws_ok needs a function to run'); + + try { + func(); + } + catch(err) { + errormsg = err+''; + } + this.like(errormsg, msg, 'code threw [' + errormsg + '] expected: [' + msg + ']'); + } + + /** + * Tests that a function throws. + * + * @param {function()} func + */ + dies_ok(func) { + var errormsg = ' '; + var msg = false; + if (typeof func != 'function') + this.diag('throws_ok needs a function to run'); + + try { + func(); + } + catch(err) { + errormsg = err+''; + msg = true; + } + this.ok(msg, 'code died with [' + errormsg + ']'); + } + + /** + * Tests that a function does not throw an exception. + * + * @param {function()} func + */ + lives_ok(func, msg) { + var errormsg = true; + if (typeof func != 'function') + this.diag('throws_ok needs a function to run'); + + try { + func(); + } + catch(err) { + errormsg = false; + } + this.ok(errormsg, msg); + } + + /** + * Tests that an object has a given method or function. + * + * @param {*} obj + */ + can_ok(obj) { + var desc = 'object can ['; + var pass = true; + for (var i=1; i { + var out = "nothing yet"; + var diag = ""; + var t = m.Tap.Node(); + t.plan(27); - //mock a fake object to run test against - var obj = new Object; - obj.run = function() {}; - var method = 'run'; - - // begin real tests! - f.can_ok(obj, 'not_there'); - t.like(out, /not ok 1 - object can \[ not_there \]/, 'can_ok failed'); - f.can_ok(obj, method); - diag = ''; - self.like(out, /ok 2 - object can \[ run \]/, 'can_ok passed'); - - //Now we need to test the whole prototype method assignment thing - - function MockObj() { - this.attr = 1; - } - - MockObj.prototype.fakeme = function () {}; - - f.can_ok(MockObj, 'fakeme'); - diag = ''; - self.like(out, /^ok .* \[ fakeme \]/, - 'can_ok recognized prototype methods'); - f.can_ok(MockObj, 'fakeme2'); - diag = ''; - self.like(out, /^not ok .* \[ fakeme2 \]/, - 'can_ok prototype recognization doesnt break methods'); -} - -t.testClassTests = function() { - var self = this; - self.ok(Test.TAP.Class, 'Test.TAP.Class namespace exists'); - - var rout = ''; - var fun = function (value) { - rout += value; - } - - var testclass = new Test.TAP.Class(fun); - testclass.plan('no_plan'); - testclass.out = fun; - self.is(testclass.print, fun, 'testclass has our own printer'); - self.is(testclass.planned, 'no_plan', 'testclass has no plan'); - - testclass.testMyTest = function() { - testclass.ok(1 === 1, 'it worked'); - } - testclass.run_tests(); - //self.diag("here is rout"); - //self.diag(rout); - self.like(rout, /ok 1 - it worked/, 'we printed the correct output'); -} - -t.testDescApears = function() { - var self = this; - // setup fake test object - var f = new Test.TAP(); // the TAP that's failing - f.out = function(newout) { out = newout }; - f.plan(1); - self.id = "t"; - f.id = "f"; - - // begin real tests! - f.like("hello", /hello/, "hello matches hello"); - self.like(out, /ok 1 - hello matches hello/, 'got description in TAP output'); -} - -t.testDiag = function() { - // setup fake test object - var f = new Test.TAP(); // the TAP that's failing - f.out = function(newout) { out = newout }; - f.plan(10); - // begin real tests! - f.diag("hello"); - t.like(out, /# hello/, 'got hello'); -} - -t.testException = function() { - // setup fake test object - var f = new Test.TAP(); // the TAP that's failing - f.out = function(newout) { out = newout }; - f.plan(2); - - // begin real tests! - f.throws_ok(function() {throw new Error('I made a boo boo')}, 'I made a boo boo'); - //t.diag(out); - this.like(out, /ok 1 - code threw \[Error: I made a boo boo\]/, 'uncaught exception'); - f.throws_ok(function() {}, 'I made a boo boo'); - //t.diag(out); - this.like(out, /not ok 2 - code threw \[ \]/, 'false failed'); -} - -t.testFails = function() { - // setup fake test object - var f = new Test.TAP(); // the TAP that's failing - f.out = function(newout) { out = newout }; - f.plan(3); - - // begin real tests! - f.ok(false, 'false fails'); - t.like(out, /not ok 1 - false fails/, 'false failed'); - - f.ok(0, 'zero fails'); - t.like(out, /not ok 2 - zero fails/, '0 failed'); - - f.is(0, 1, 'zero is one'); - t.like(out, /not ok 3 - zero is one/, '0 != 1'); -} - -t.testPass = function() { - this.ok(true, 'true is true'); - this.is(1,1, '1 is 1'); - this.pass('pass passes'); - this.like("hello world", /hel+o/, 'regexen work'); - this.unlike("hello there", /world/, 'no world'); -} - -t.testPlan = function() { - var self = this; - // setup fake test object - var f = new Test.TAP(); // the TAP that's failing - f.out = function(newout) { out = newout }; - f.plan(2); - - // begin real tests! - f.ok(false, 'false fails'); - self.is(f.counter, 1, 'counter increments by one'); - self.is(f.planned, 2, 'planned = 2'); -} - -t.testTodoSkip = function() { - var self = this; - var out; - self.can_ok(Test.TAP, 'todo', 'skip'); - var f = new Test.TAP(); // the TAP that's failing - f.out = function(newout) { out = newout }; - f.plan(4); - - f.todo(function() { - f.ok(true, 'true is true'); - }); - self.like(out, /ok 1 - # TODO: true is true/g, - 'the non todo output is suitably formatted'); - f.ok(!false, 'not false is true'); - self.like(out, /ok 2 -/g, 'the regular output is suitably formatted'); - - f.skip(true, 'because I said so', 1, - function() { - f.is(1, 2, 'one is two'); + var testCan = function () { + // setup fake test object + var f = new m.Tap(function(newout) { out = newout }); // the TAP thats failing + f.out = function(newout) { out = newout }; + f.diag = function(newdiag) { diag += newdiag }; + f.plan(4); + + //mock a fake object to run test against + var obj = new Object; + obj.run = function() {}; + var method = 'run'; + + // begin real tests! + f.can_ok(obj, 'not_there'); + t.like(out, /not ok 1 - object can \[ not_there \]/, 'can_ok failed'); + f.can_ok(obj, method); + diag = ''; + t.like(out, /ok 2 - object can \[ run \]/, 'can_ok passed'); + + //Now we need to test the whole prototype method assignment thing + + function MockObj() { + this.attr = 1; } - ); - self.like(out, /^not ok 3 - # SKIP because I said so$/, - 'the skipped output is suitably formatted'); - f.is(1, 1, 'one is one'); - self.like(out, /ok 4 - one is one/, - 'the non skipped output is suitable formatted'); -}; - -return t; -})() + + MockObj.prototype.fakeme = function () {}; + + f.can_ok(MockObj, 'fakeme'); + diag = ''; + t.like(out, /^ok .* \[ fakeme \]/, + 'can_ok recognized prototype methods'); + f.can_ok(MockObj, 'fakeme2'); + diag = ''; + t.like(out, /^not ok .* \[ fakeme2 \]/, + 'can_ok prototype recognization doesnt break methods'); + }; + + var testLike = function() { + // setup fake test object + var f = new m.Tap(function(newout) { out = newout }); // the TAP that's failing + f.out = function(newout) { out = newout }; + f.plan(1); + + // begin real tests! + f.like("hello", /hello/, "hello matches hello"); + t.like(out, /ok 1 - hello matches hello/, 'got description in TAP output'); + }; + + var testDiag = function() { + // setup fake test object + var f = new m.Tap(function(newout) { out = newout }); // the TAP that's failing + f.out = function(newout) { out = newout }; + f.plan(10); + // begin real tests! + f.diag("hello"); + t.like(out, /# hello/, 'got hello'); + }; + + var testException = function() { + // setup fake test object + var f = new m.Tap(function(newout) { out = newout }); // the TAP that's failing + f.out = function(newout) { out = newout }; + f.plan(2); + + // begin real tests! + f.throws_ok(function() {throw new Error('I made a boo boo')}, 'I made a boo boo'); + //t.diag(out); + t.like(out, /ok 1 - code threw \[Error: I made a boo boo\]/, 'uncaught exception'); + f.throws_ok(function() {}, 'I made a boo boo'); + //t.diag(out); + t.like(out, /not ok 2 - code threw \[ \]/, 'false failed'); + }; + testException(); + + var testFails = function() { + // setup fake test object + var f = new m.Tap(function(newout) { out = newout }); // the TAP that's failing + f.out = function(newout) { out = newout }; + f.plan(3); + + // begin real tests! + f.ok(false, 'false fails'); + t.like(out, /not ok 1 - false fails/, 'false failed'); + + f.ok(0, 'zero fails'); + t.like(out, /not ok 2 - zero fails/, '0 failed'); + + f.is(0, 1, 'zero is one'); + t.like(out, /not ok 3 - zero is one/, '0 != 1'); + }; + testFails(); + + var testPass = function() { + t.ok(true, 'true is true'); + t.is(1,1, '1 is 1'); + t.pass('pass passes'); + t.like("hello world", /hel+o/, 'regexen work'); + t.unlike("hello there", /world/, 'no world'); + }; + testPass(); + + var testPlan = function() { + // setup fake test object + var f = new m.Tap(function(newout) { out = newout }); // the TAP that's failing + f.out = function(newout) { out = newout }; + f.plan(2); + + // begin real tests! + f.ok(false, 'false fails'); + t.is(f.counter, 1, 'counter increments by one'); + t.is(f.planned, 2, 'planned = 2'); + }; + testPlan(); + + var testTodoSkip = function() { + var out; + t.can_ok(m.Tap, 'todo', 'skip'); + var f = new m.Tap(); // the TAP that's failing + f.out = function(newout) { out = newout }; + f.plan(4); + + f.todo(function() { + f.ok(true, 'true is true'); + }); + t.like(out, /ok 1 - # TODO: true is true/g, + 'the non todo output is suitably formatted'); + f.ok(!false, 'not false is true'); + t.like(out, /ok 2 -/g, 'the regular output is suitably formatted'); + + f.skip(true, 'because I said so', 1, + function() { + f.is(1, 2, 'one is two'); + } + ); + t.like(out, /^not ok 3 - # SKIP because I said so$/, + 'the skipped output is suitably formatted'); + f.is(1, 1, 'one is one'); + t.like(out, /ok 4 - one is one/, + 'the non skipped output is suitable formatted'); + }; + testTodoSkip(); + + return t; +});