feat: Properly handle node async output

This commit is contained in:
Jeremy Wall 2024-04-03 17:10:03 -04:00
parent 975d7f22af
commit 431b25cdfd
3 changed files with 90 additions and 60 deletions

View File

@ -21,10 +21,16 @@
/** @implements TapRenderer */
class NodeRenderer {
/** @type {Array<PromiseLike>} */
#thunks = [];
out(text) {
import('node:process').then(loaded => {;
this.#thunks.push(
// Because this is a ECMAScript module we have to do dynamic module loads
// of the node ecosystem when running in Node.js.
import('node:process').then(loaded => {
loaded.stdout.write(text + "\n");
})
}));
}
comment(lines) {
@ -32,6 +38,14 @@ class NodeRenderer {
this.out('# ' + line);
}
}
// This gives us a way to block on output. It's ghetto but async is a harsh task master.
async renderAll() {
for (var thunk of this.#thunks) {
await thunk;
}
}
}
/** @implements TapRenderer */
@ -90,28 +104,40 @@ class Tap {
/** @type Number */
failed = 0;
/** @type TapRenderer */
#renderer
renderer
/**
* Construct a new Tap Suite with a renderLine function.
* @param {TapRenderer}
*/
constructor(renderer) {
this.#renderer = renderer;
this.renderer = renderer;
}
/**
* @return {{"Renderer": BrowserRenderer, "Tap": Tap}}
*/
static Browser() {
var r = new BrowserRenderer();
return {"Renderer": r, "Tap": new Tap(r)};
return new Tap(new BrowserRenderer());
}
/**
* @return {{"Renderer": NodeRenderer, "Tap": Tap}}
*/
static Node() {
return new Tap(new NodeRenderer());
var r = new NodeRenderer();
return {"Renderer": r, "Tap": new Tap(r)};
}
isPass() {
return this.passed != 0;
}
/** Renders output for the test results */
out(text) {
this.#renderer.out(text);
this.renderer.out(text);
};
/**
@ -120,8 +146,8 @@ class Tap {
* @param {boolean} ok
* @param {string=} description
*/
mk_tap(ok, description){
if(!this.planned){
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.");
}
@ -130,12 +156,12 @@ class Tap {
};
comment(msg){
if(!msg) {
comment(msg) {
if (!msg) {
msg = " ";
}
var lines = msg.split("\n");
this.#renderer.comment(lines);
this.renderer.comment(lines);
};
/** Render a pass TAP output message.
@ -162,7 +188,7 @@ class Tap {
var self = this;
var tapper = self.mk_tap;
self.mk_tap = function(ok, desc) {
tapper.apply(self, [ok, "# TODO: "+desc]);
tapper.apply(self, [ok, "# TODO: " + desc]);
}
func();
self.mk_tap = tapper;
@ -182,8 +208,8 @@ class Tap {
self.mk_tap = function(ok, desc) {
tapper.apply(self, [ok, desc]);
}
for(var i = 0; i < count; i++) {
self.fail("# SKIP "+reason)
for (var i = 0; i < count; i++) {
self.fail("# SKIP " + reason)
}
self.mk_tap = tapper;
} else {
@ -200,7 +226,7 @@ class Tap {
* @param {Number=} testCount
*/
plan(testCount) {
if(this.planned){
if (this.planned) {
throw new Error("you tried to set the plan twice!");
}
if (!testCount) {
@ -211,9 +237,9 @@ class Tap {
}
};
#pass_if(func, desc){
#pass_if(func, desc) {
var result = func();
if(result) { this.pass(desc) }
if (result) { this.pass(desc) }
else { this.fail(desc) }
}
@ -233,8 +259,8 @@ class Tap {
try {
func();
}
catch(err) {
errormsg = err+'';
catch (err) {
errormsg = err + '';
}
this.like(errormsg, msg, 'code threw [' + errormsg + '] expected: [' + msg + ']');
}
@ -253,8 +279,8 @@ class Tap {
try {
func();
}
catch(err) {
errormsg = err+'';
catch (err) {
errormsg = err + '';
msg = true;
}
this.ok(msg, 'code died with [' + errormsg + ']');
@ -273,7 +299,7 @@ class Tap {
try {
func();
}
catch(err) {
catch (err) {
errormsg = false;
}
this.ok(errormsg, msg);
@ -287,9 +313,9 @@ class Tap {
can_ok(obj) {
var desc = 'object can [';
var pass = true;
for (var i=1; i<arguments.length; i++) {
if (typeof(obj[arguments[i]]) != 'function') {
if (typeof(obj.prototype) != 'undefined') {
for (var i = 1; i < arguments.length; i++) {
if (typeof (obj[arguments[i]]) != 'function') {
if (typeof (obj.prototype) != 'undefined') {
var result = typeof eval('obj.prototype.' + arguments[i]);
if (result == 'undefined') {
pass = false;
@ -303,7 +329,7 @@ class Tap {
desc += ' ' + arguments[i];
}
desc += ' ]';
this.#pass_if(function(){
this.#pass_if(function() {
return pass;
}, desc);
@ -317,7 +343,7 @@ class Tap {
* @param {string} desc
*/
is(got, expected, desc) {
this.#pass_if(function(){ return got == expected; }, desc);
this.#pass_if(function() { return got == expected; }, desc);
};
@ -328,7 +354,7 @@ class Tap {
* @param {string} desc
*/
ok(got, desc) {
this.#pass_if(function(){ return !!got; }, desc);
this.#pass_if(function() { return !!got; }, desc);
};
@ -340,8 +366,8 @@ class Tap {
* @param {string} desc
*/
like(string, regex, desc) {
this.#pass_if(function(){
if(regex instanceof RegExp) {
this.#pass_if(function() {
if (regex instanceof RegExp) {
return string.match(regex)
} else {
return string.indexOf(regex) != -1
@ -357,7 +383,7 @@ class Tap {
* @param {string} desc
*/
unlike(string, regex, desc) {
this.#pass_if(function(){
this.#pass_if(function() {
return !string.match(regex);
}, desc)
}

View File

@ -14,6 +14,10 @@ class FakeRenderer {
}
}
import('./suite.mjs').then(m => {
m.runTest(m.Tap.Node(), "Tap dogfood test suite", m.tapSuite);
import('./suite.mjs').then(async m => {
const pair = m.Tap.Node();
m.runTest(pair.Tap, "Tap dogfood test suite", m.tapSuite);
// Note output requires some async machinery because it uses some dynamic inputs.
await pair.Renderer.renderAll();
process.exit(pair.Tap.isPass() ? 0 : 1);
});

View File

@ -1,5 +1,5 @@
/** @implements TapRenderer */
import {Tap, runTest} from '../src/TAP.mjs';
import {Tap, runTest, NodeRenderer, BrowserRenderer} from '../src/TAP.mjs';
class FakeRenderer {
output = "nothing yet";