Last active
November 28, 2018 21:49
-
-
Save chjj/4fc87c2b3e882c9d240a544488639f7e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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