Skip to content

Instantly share code, notes, and snippets.

@chjj
Last active November 28, 2018 21:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chjj/4fc87c2b3e882c9d240a544488639f7e to your computer and use it in GitHub Desktop.
Save chjj/4fc87c2b3e882c9d240a544488639f7e to your computer and use it in GitHub Desktop.
#!/usr/bin/env node
/* eslint no-control-regex: "off" */
'use strict';
const assert = require('assert');
const Path = require('path');
const {
Date,
clearTimeout,
Error,
Math,
process,
Promise,
setTimeout,
String
} = global;
const cwd = process.cwd();
const {argv, stdout, stderr, exit} = process;
/**
* Mocha
*/
class Mocha {
constructor(files) {
this.files = files;
this.start = Date.now();
this.errors = [];
this.passing = 0;
this.failing = 0;
}
log(str, depth) {
if (!stdout.isTTY)
str = str.replace(/\x1b\[[^m]*?m/g, '');
str = indent(str, depth);
stdout.write(str + '\n');
}
error(id, desc, name, err) {
if (err == null || typeof err !== 'object')
err = String(err);
if (typeof err === 'string')
err = new Error(err);
const stack = formatStack(err.stack);
this.log(`${id}) ${desc}`, 1);
this.log(` ${name}:`, 3);
this.log('');
this.log(`\x1b[31m${err.name}: ${err.message}\x1b[m`, 3);
if (err.code === 'ERR_ASSERTION') {
this.log('\x1b[32m+ expected\x1b[m \x1b[31m- actual\x1b[m', 3);
this.log('');
this.log(`\x1b[31m-${err.actual}\x1b[m`, 3);
this.log(`\x1b[32m+${err.expected}\x1b[m`, 3);
}
this.log('');
this.log(`\x1b[90m${stack}\x1b[m`, 3);
this.log('');
}
async run() {
for (const file of this.files) {
const name = Path.resolve(cwd, file);
const suite = new Suite(this);
try {
require(name);
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND')
throw new Error(`Could not find ${file}.`);
throw e;
}
await suite.run();
}
const elapsed = Math.ceil((Date.now() - this.start) / 1000);
const passed = `\x1b[32m${this.passing} passing\x1b[m`;
const time = `\x1b[90m(${elapsed}s)\x1b[m`;
this.log('');
this.log(`${passed} ${time}`, 1);
if (this.failing > 0)
this.log(`\x1b[31m${this.failing} failing\x1b[m`, 1);
this.log('');
for (const [i, [desc, name, err]] of this.errors.entries())
this.error(i + 1, desc, name, err);
}
}
/**
* Suite
*/
class Suite {
constructor(mocha) {
this.mocha = mocha;
this.descs = [];
this.depth = 1;
this.init();
}
init() {
global.describe = this.describe.bind(this);
}
describe(name, func) {
const desc = new Desc(this, name, func);
this.descs.push(desc);
desc.init();
}
async run() {
for (const desc of this.descs)
await desc.run();
}
}
/**
* Desc
*/
class Desc {
constructor(suite, name, func) {
this.mocha = suite.mocha;
this.suite = suite;
this.name = name;
this.func = func;
this.depth = suite.depth;
this.timeout = 2000;
this.befores = [];
this.afters = [];
this.beforeEaches = [];
this.afterEaches = [];
this.tests = [];
this.api = {
timeout: (ms) => {
this.timeout = ms >>> 0;
}
};
}
log(str) {
this.mocha.log(str, this.depth);
}
push(name, err) {
this.mocha.errors.push([this.name, name, err]);
}
get id() {
return this.mocha.errors.length;
}
pass() {
this.mocha.passing += 1;
}
fail() {
this.mocha.failing += 1;
}
before(func) {
assert(typeof func === 'function');
this.befores.push(func);
}
after(func) {
assert(typeof func === 'function');
this.afters.push(func);
}
beforeEach(func) {
assert(typeof func === 'function');
this.beforeEaches.push(func);
}
afterEach(func) {
assert(typeof func === 'function');
this.afterEaches.push(func);
}
it(name, func) {
assert(typeof name === 'string');
assert(typeof func === 'function');
this.tests.push([name.slice(0, 100), func]);
}
init() {
global.before = this.before.bind(this);
global.after = this.after.bind(this);
global.beforeEach = this.beforeEach.bind(this);
global.afterEach = this.afterEach.bind(this);
global.it = this.it.bind(this);
this.suite.depth += 1;
try {
this.func.call(this.api);
} catch (e) {
this.push(this.name, e);
}
this.suite.depth -= 1;
}
async run() {
this.log('');
this.log(`${this.name}`);
for (const before of this.befores) {
try {
await before();
} catch (e) {
this.push('before hook', e);
return;
}
}
for (const [name, func] of this.tests) {
const start = Date.now();
let failed = false;
for (const before of this.beforeEaches) {
try {
await before();
} catch (e) {
this.push(name, e);
failed = true;
break;
}
}
if (!failed) {
try {
await this.runTest(func);
} catch (e) {
this.push(name, e);
failed = true;
}
}
if (!failed) {
for (const after of this.afterEaches) {
try {
await after();
} catch (e) {
this.push(name, e);
failed = true;
break;
}
}
}
const elapsed = Date.now() - start;
if (failed) {
this.log(` \x1b[31m${this.id}) ${name}\x1b[m `);
this.fail();
} else {
let suffix = '';
if (elapsed > 100)
suffix = `\x1b[31m (${elapsed}ms)\x1b[m`;
else if (elapsed > 20)
suffix = `\x1b[33m (${elapsed}ms)\x1b[m`;
this.log(` \x1b[32m✓\x1b[m \x1b[90m${name}\x1b[m${suffix}`);
this.pass();
}
}
for (const after of this.afters) {
try {
await after();
} catch (e) {
this.push('after hook', e);
return;
}
}
}
runTest(func) {
return new Promise((resolve, reject) => {
let timeout = this.timeout;
let timer = null;
const ctx = {
timeout: (ms) => {
timeout = ms >>> 0;
}
};
const cleanup = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
};
if (func.length > 0) {
const cb = (err, result) => {
cleanup();
if (err) {
reject(err);
return;
}
resolve(result);
};
try {
func.call(ctx, cb);
} catch (e) {
reject(e);
return;
}
} else {
let promise;
try {
promise = func.call(ctx);
} catch (e) {
reject(e);
return;
}
if (!(promise instanceof Promise)) {
resolve(promise);
return;
}
promise.then((result) => {
cleanup();
resolve(result);
}).catch((err) => {
cleanup();
reject(err);
});
}
if (timeout !== 0) {
timer = setTimeout(() => {
timer = null;
reject(new Error(`Timeout of ${timeout}ms exceeded.`));
}, timeout);
}
});
}
}
/*
* Helpers
*/
function indent(str, depth) {
if (depth == null)
depth = 0;
if (depth === 0)
return str;
let spaces = '';
for (let i = 0; i < depth * 2; i++)
spaces += ' ';
return str.replace(/^/gm, spaces);
}
function formatStack(stack) {
let str = String(stack);
// str = str.replace(/^[^\0]*?\n +(at )/g, '\1');
const index = str.indexOf('\n at ');
if (index !== -1)
str = str.substring(index + 1);
return str.replace(/^[ \t]+/gm, '');
}
/*
* Main
*/
(async () => {
const files = [];
for (let i = 2; i < argv.length; i++) {
const arg = argv[i];
switch (arg) {
case '-R':
case '--reporter':
i += 1;
break;
default:
if (arg.length === 0 || arg[0] === '-') {
console.error(`Invalid option: ${arg}`);
exit(1);
}
files.push(arg);
break;
}
}
process.on('unhandledRejection', (err, promise) => {
stderr.write('Unhandled rejection:\n');
stderr.write('\n');
if (err && err.stack)
err = String(err.stack);
stderr.write(err + '\n');
exit(1);
});
const mocha = new Mocha(files.sort());
await mocha.run();
if (mocha.failing > 0)
exit(mocha.failing);
})().catch((err) => {
stderr.write('An error occurred outside of the test suite:\n');
stderr.write('\n');
if (err && err.stack)
err = String(err.stack);
stderr.write(err + '\n');
exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment