Files
xxditb/tests/harness.app.htm
2026-09-24 11:16:57 -05:00

2629 lines
74 KiB
HTML

/// <script>
/// import { DependencySniffer } from "./DependencySniffer.js";
/// import { InaftPathResolver } from "./InaftPathResolver.js";
/// import { SimpleStringParser } from "./SimpleStringParser.js";
/// import { Sourcerer } from "./Sourcerer.js";
/// export
class TestRunner {
constructor(systemHelper) {
this.config = null;
this.preloads = new Map();
this.system = systemHelper;
this._pending = [];
this.onbadconfig = null;
this.onfinish = null;
this.onpathsknown = null;
this.ontestexecute = null;
this.ontestskip = null;
}
runTests() {
return this.system.read("tests/config.json").then((text) =>
void this._initConfigFromText(text).then((testPaths) =>
void this._runNextTest(testPaths)
));
}
_initConfigFromText(text) {
let testPaths, pending = [];
if (text) {
try {
this.config = this._validateConfig(text);
} catch (ex) {
if (ex instanceof TestRunner.ConfigError) {
this._dispatch(this.onbadconfig, ex);
return Promise.resolve([]);
}
throw ex;
}
let { preload, tests } = this.config;
if (typeof(preload) != "undefined") {
pending.push(this._promisePreloadText(preload));
}
if (typeof(tests) != "undefined") {
testPaths = tests.map((x) => this._getTestFilePath(x));
}
}
if (!testPaths) {
pending.push(this.system.list("tests/").then((kids) => {
let $ok = (x) => (!x.startsWith(".") && x.endsWith("/"));
testPaths = kids.filter($ok).map((x) => this._getTestFilePath(x));
}));
}
return Promise.all(pending).then(() => {
let stop = testPaths.indexOf(null);
if (stop >= 0) testPaths.splice(stop, Infinity);
this._dispatch(this.onpathsknown, testPaths);
return Promise.resolve(testPaths);
}).catch((err) => {
if (err instanceof TestRunner.ConfigError) {
this._dispatch(this.onbadconfig, err);
return Promise.resolve([]);
} else {
Promise.reject(err); // Unknown/unexpected
}
});
}
_dispatch(handler, ...args) {
if (handler) handler.apply(null, args);
}
_getTestFilePath(directoryName) {
if (!directoryName) return null;
if (!directoryName.endsWith("/")) {
directoryName += "/";
}
return "tests/" + directoryName + "index.js";
}
_validateConfig(text) {
// TODO Actually validate this (e.g., de-dupe entries)
try {
var config = JSON.parse(text);
} catch (ex) {
const { PARSE_ISSUE } = TestRunner.ConfigError;
throw new TestRunner.ConfigError(PARSE_ISSUE, ex);
}
return config;
}
_promisePreloadText(paths) {
let blockers = [];
for (let i = 0; i < paths.length; ++i) {
blockers.push(
this.system.read(paths[i]).then((contents) => {
if (contents != null) {
this.preloads.set(paths[i], { path: paths[i], text: contents });
return Promise.resolve();
} else {
const { PRELOAD_ISSUE } = TestRunner.ConfigError;
let inner = new Error("No such file: " + paths[i]);
return Promise.reject(
new TestRunner.ConfigError(PRELOAD_ISSUE, inner)
);
}
})
);
}
return Promise.all(blockers);
}
_runNextTest(testPaths, index = 0) {
let path = testPaths[index];
if (path) {
this._pending.push(new Promise((end) => {
this.system.createContext().then((context) =>
void this._promiseEvalPreloads(context).then(() =>
void this._evalTestScript(context, path).then(() =>
void (end(), this._runNextTest(testPaths, index + 1))
)));
}));
} else {
Promise.all(this._pending).then(() => {
this._dispatch(this.onfinish);
});
}
}
_promiseEvalPreloads(context) {
if (!this.preloads.size) return Promise.resolve();
let specs = this.config.preload;
let blockers = [];
for (let i = 0; i < specs.length; ++i) {
let { path, text } = this.preloads.get(specs[i]);
blockers.push(
this._rewriteImports(context, text, path).then((phonyText) =>
this.system.loadScript(context, path, phonyText)
)
);
}
return Promise.all(blockers);
}
_evalTestScript(rootContext, path) {
const timeBudget = 2500;
var end; // can (will) manage contention for `onfinish` dispatch
this._pending.push(new Promise((resolve) => void(end = resolve)));
return this.system.read(path).then((scriptText) => {
if (scriptText) {
this.system.createContext(rootContext, true).then((subContext) =>
void this._rewriteImports(subContext, scriptText).then((phonyText) =>
void this.system.startCountdown(subContext, timeBudget).then(() =>
void this.system.loadScript(subContext, path, phonyText).then(() =>
void (end(), this._dispatch(this.ontestexecute, rootContext, path))
))))
} else {
end(), this._dispatch(this.ontestskip, path);
}
});
}
_rewriteImports(context, text, basePath = null) {
let sniffer = new DependencySniffer(text);
let imports = sniffer.getImports();
let blockers = [];
let nextContext = context.parent || context;
for (let i = 0, n = imports.length; i < n; ++i) {
let relativePath = SimpleStringParser.eval(imports[i].specifier);
let dep = context.pathResolver.handle(relativePath, basePath);
if (dep != InaftPathResolver.HELPER_PATH) {
blockers.push(
this.system.read(dep).then((text) => {
if (text != null) return (
this._rewriteImports(nextContext, text, dep).then((fake) => {
return this.system.loadScript(nextContext, dep, fake);
})
);
}
));
}
}
return Promise.all(blockers).then(() => {
return Promise.resolve(
Sourcerer.rewrite(text, imports, sniffer.getExports())
);
});
}
}
TestRunner.ConfigError = class ConfigError extends Error {
constructor(message, inner) {
super(message);
this.innerException = inner;
}
}
TestRunner.ConfigError.PARSE_ISSUE = "Couldn't parse tests' config.json";
TestRunner.ConfigError.PRELOAD_ISSUE = "Couldn't preload file";
/// </script>
/// <script>
/// export
class SimpleScanner {
constructor(text) {
this.text = text;
this.position = 0;
}
get tip() {
let unit = this.text.codePointAt(this.position);
if (unit == undefined) return "";
return String.fromCodePoint(unit);
}
getGenericToken() {
if (this.tip == "") return null; // EOF
let result = { position: this.position };
if (this.isWhitespace(this.tip)) {
result.content = "";
do {
result.content += this.readCodePoint();
} while (this.isWhitespace(this.tip));
result.type = SimpleScanner.WHITESPACE;
} else {
result.type = SimpleScanner.UNKNOWN;
}
return result;
}
readCodePoint() {
let result = this.tip;
this.position += result.length;
return result;
}
isWhitespace(x) {
return (x == " " || x == "\t" || x == "\r" || x == "\n");
}
}
SimpleScanner.UNKNOWN = -1;
SimpleScanner.WHITESPACE = 0;
/// </script>
/// <script>
/// import { SimpleScanner } from "./SimpleScanner.js";
/// import { SimpleStringParser } from "./SimpleStringParser.js";
/// export
class DependencySniffer extends SimpleScanner {
constructor(text, preserveWhitespace) {
super(text);
this._findings = null;
this._preserveWhitespace = !!preserveWhitespace;
}
getImports() {
if (!this._findings) this._findings = this._sniff();
return this._findings.imports;
}
getExports() {
if (!this._findings) this._findings = this._sniff();
return this._findings.exports;
}
getToken(includeGaps = this._preserveWhitespace) {
const { WHITESPACE, UNKNOWN } = SimpleScanner;
do {
var result = this.getGenericToken();
} while (result && result.type == WHITESPACE && !includeGaps);
if (result && result.type == UNKNOWN) {
switch (this.tip) {
case ";":
result.type = DependencySniffer.SEMICOLON;
result.content = this.readCodePoint();
break;
case ",":
result.type = DependencySniffer.COMMA;
result.content = this.readCodePoint();
break;
case "{":
result.type = DependencySniffer.OPENED_CURLY;
result.content = this.readCodePoint();
break;
case "}":
result.type = DependencySniffer.CLOSED_CURLY;
result.content = this.readCodePoint();
break;
case "*":
result.type = DependencySniffer.ASTERISK;
result.content = this.readCodePoint();
break;
case "'":
case '"':
result.content = this._scanString();
result.type = DependencySniffer.STRING;
break;
default:
if (this.tip == "$" || this.tip == "_" ||
this._isLetter(this.tip)) {
result.content = this._scanWord();
switch (result.content) {
case "export":
result.type = DependencySniffer.EXPORT;
break;
case "from":
result.type = DependencySniffer.FROM;
break;
case "import":
result.type = DependencySniffer.IMPORT;
break;
default:
result.type = DependencySniffer.WORD;
break;
}
} else {
// Still UNKNOWN, but we need to advance.
result.content = this.readCodePoint();
}
break;
}
}
return result;
}
_scanString() {
const offset = this.position;
// NB: This relies on the parser's implementation details.
let parser = new SimpleStringParser({
codePointAt: (index) => (!parser.literal) ?
this.text.codePointAt(offset + index) :
undefined
});
let { literal } = parser.run();
this.position += literal.length;
return literal;
}
_scanWholeLine() {
while (this.tip != "\n" && this.tip != "") {
this.readCodePoint();
}
}
_scanWord() {
let result = this.readCodePoint();
while (this._isLetter(this.tip) || this._isDigit(this.tip) ||
this.tip == "$" || this.tip == "_") {
result += this.readCodePoint();
}
return result;
}
_isLetter(x) {
return ("A" <= x && x <= "Z") || ("a" <= x && x <= "z");
}
_isDigit(x) {
return ("0" <= x && x <= "9");
}
_sniff() {
if (this.position != 0) throw new Error("Re-entrant parse disallowed");
let result = { imports: [], exports: [] };
let token = this.getToken();
if (token) {
do {
if (token.type == DependencySniffer.IMPORT) {
result.imports.push(this._parseImportDeclaration(token));
} else if (token.type == DependencySniffer.EXPORT) {
result.exports.push(this._parseExportDeclaration(token));
} else {
this._scanWholeLine();
}
token = this.getToken();
} while (token);
}
return result;
}
_parseImportDeclaration(token) {
let start = token.position;
let specifier, list = [];
token = this._expectToken(this.getToken());
if (token.type != DependencySniffer.OPENED_CURLY) {
throw new Error("Only explicitly named imports supported");
}
token = this._expectToken(this.getToken());
if (token.type == DependencySniffer.ASTERISK) {
throw new Error("Wildcard imports not supported");
}
list.push(this._expectToken(token, DependencySniffer.WORD).content);
token = this._expectToken(this.getToken());
while (token.type != DependencySniffer.CLOSED_CURLY) {
if (token.type == DependencySniffer.WORD && token.content == "as") {
throw new Error("Namespace rebinding for imports not supported");
}
this._expectToken(token, DependencySniffer.COMMA);
let word = this._expectToken(this.getToken(), DependencySniffer.WORD);
list.push(word.content);
token = this._expectToken(this.getToken());
}
this._expectToken(token, DependencySniffer.CLOSED_CURLY);
token = this._expectToken(this.getToken(), DependencySniffer.FROM);
token = this._expectToken(this.getToken(), DependencySniffer.STRING);
specifier = token.content;
if (this.tip && this.tip != "\n") {
token = this._expectToken(this.getToken(), DependencySniffer.SEMICOLON);
}
let end = token.position + token.content.length;
return { list: list, specifier: specifier, span: [ start, end ] };
}
_parseExportDeclaration(token) {
let start = token.position;
let mark = this.position;
token = this._expectToken(this.getToken(), DependencySniffer.WORD);
let kind = token.content;
if (kind != "class" && kind != "function" && kind != "const" &&
kind != "var" && kind != "let") {
throw new Error("Unsupported export: " + JSON.stringify(kind));
}
token = this._expectToken(this.getToken(), DependencySniffer.WORD);
let name = token.content;
let end = token.position + token.content.length;
return { kind: kind, name: name, span: [ start, end ], mark: mark };
}
_expectToken(token, type) {
if (token == null) throw new Error("Unexpected EOF");
if (typeof(type) != "undefined" && token.type != type) {
throw new Error("Unexpected token: " + JSON.stringify(token.content));
}
return token;
}
}
DependencySniffer.WORD = 1;
DependencySniffer.SEMICOLON= 2;
DependencySniffer.COMMA = 3;
DependencySniffer.OPENED_CURLY = 4;
DependencySniffer.CLOSED_CURLY = 5;
DependencySniffer.ASTERISK = 6;
DependencySniffer.STRING = 9;
DependencySniffer.IMPORT = 11;
DependencySniffer.FROM = 12;
DependencySniffer.EXPORT = 21;
/// </script>
/// <script>
/// import { SimpleScanner } from "./SimpleScanner.js";
/// export
class SimpleStringParser {
constructor(input) {
this._scanner = new SimpleScanner(input);
this.literal = null;
this.value = null;
}
static eval(input) {
let parser = new SimpleStringParser(input);
parser.run();
return parser.value;
}
run() {
let quote = this._scanner.readCodePoint();
let isEscaped = false;
let result = quote;
let jsonSafe = "";
while (!(this._scanner.tip == quote && !isEscaped)) {
if (this._scanner.tip == "") throw new Error("Unexpected EOF");
if (quote == "'") {
if (this._scanner.tip == "\"" && !isEscaped) {
jsonSafe += "\\";
} else if (this._scanner.tip == "'" && isEscaped) {
jsonSafe = jsonSafe.substring(0, jsonSafe.length - 1);
}
}
if (!isEscaped) {
if (this._scanner.tip == "\u2028" || this._scanner.tip == "\u2029") {
throw new Error("Unterminated string literal");
}
if (this._scanner.tip == "\\") isEscaped = true;
} else {
isEscaped = false;
}
jsonSafe += this._scanner.tip;
result += this._scanner.readCodePoint();
}
result += this._scanner.readCodePoint();
this.value = JSON.parse("\"" + jsonSafe + "\"");
this.literal = result;
if (this._scanner.tip) throw new Error(
"Unexpected token " + this._scanner.tip + " after string literal"
);
return this;
}
}
/// </script>
/// <script>
/// export
class InaftPathResolver {
constructor(withMagic = false) {
this.isMagic = withMagic;
}
static normalizePath(path, reference = "") {
if (!InaftPathResolver.isRelative(path)) return path;
let normalized = [];
let parts = reference.split("/");
parts.pop();
parts = parts.concat(path.split("/"));
for (let i = 0; i < parts.length; ++i) {
switch (parts[i]) {
case ".":
// no-op
break;
case "..":
let popped = normalized.pop();
if (popped == undefined) throw new Error(
"Path forays outside local root: " + JSON.stringify(path) + " " +
"(relative to " + JSON.stringify(reference) + ")"
);
break;
default:
normalized.push(parts[i]);
break;
}
}
return normalized.join("/");
}
static isRelative(path) {
return path.substr(0, 2) == "./" || path.substr(0, 3) == "../";
}
static getRealNameForMagicPath(id) {
let magic = "$";
if (!id.startsWith(magic)) return null;
for (let i = magic.length; i < id.length;) {
let codePoint = String.fromCodePoint(id.codePointAt(i));
if (codePoint == "/") break;
magic += codePoint;
if (codePoint != "_") {
if (codePoint < "A") return null;
if (codePoint > "Z") return null;
}
i += codePoint.length;
}
switch (magic) {
case InaftPathResolver.HELPER_PATH:
return id;
case InaftPathResolver.PROJECT_DIR:
return id.substr((InaftPathResolver.PROJECT_DIR + "/").length);
}
return null;
}
handle(path, relativeTo) {
if (this.isMagic) {
return InaftPathResolver.getRealNameForMagicPath(path);
}
return InaftPathResolver.normalizePath(path, relativeTo);
}
}
InaftPathResolver.WITH_MAGIC = true;
InaftPathResolver.HELPER_PATH = "$TEST_HELPER";
InaftPathResolver.PROJECT_DIR = "$PROJECT";
/// </script>
/// <script>
/// import { TextSplicer } from "./TextSplicer.js";
/// import { TripleScanner } from "./TripleScanner.js";
/// export
class Sourcerer {
constructor(text) {
this._text = text;
}
static makeDataURI(text, contentType = "text/plain") {
return "data:" + contentType + ";base64," + btoa(text);
}
static makeIIFE(source, argumentName = "", argumentValue = "") {
let fn = new Function(argumentName, source);
return "(" + fn.toString() + ")(" + argumentValue + ");\n";
}
static rewrite(text, imports, exports) {
let result = new TextSplicer(text);
let trailer = "";
let $itransform = (importInfo) => {
let { list, specifier } = importInfo;
let result = "var { " + list.join(", ") + " } = " +
"require(" + specifier + ");";
let [ start, end ] = importInfo.span;
let lineCount = text.substring(start, end).split(/\r\n|\n|\r/).length;
while (lineCount > 1) {
result += "\n";
--lineCount;
}
return result;
};
for (let i = 0, n = imports.length; i < n; ++i) {
let [ start, end ] = imports[i].span;
result.splice(start, end, $itransform(imports[i]));
}
let $etransform = (exportInfo) => {
let { name } = exportInfo;
trailer += "\nexports." + name + " = " + name + ";"; // XXX do better
return "/** @exports */";
};
for (let i = 0, n = exports.length; i < n; ++i) {
let [ start ] = exports[i].span, end = exports[i].mark;
result.splice(start, end, $etransform(exports[i]));
}
return String(result) + trailer;
}
extractClassDefinition(constructor) {
let scanner = new TripleScanner(this._text);
var current = scanner.getNextToken();
while (current != null) {
if (current.type == TripleScanner.DELIMITER_OPEN) {
let source = "";
current = scanner.getNextToken();
// assert(current.type == TripleScanner.WHITESPACE)
current = scanner.getNextToken();
// assert(current.type == TripleScanner.TEXT)
while (current.content.startsWith("///")) {
while (scanner.tip == "\n") {
scanner.readCodePoint();
}
current = scanner.getNextToken();
}
if (current.content != "class") {
continue;
}
source += current.content;
current = scanner.getNextToken();
// assert(current.type == TripleScanner.WHITESPACE)
source += current.content;
current = scanner.getNextToken();
if (current.content != constructor.name) {
continue;
}
do {
if (current == null) throw new Error("Unexpected EOF");
source += current.content;
current = scanner.getNextToken();
} while (current.type != TripleScanner.DELIMITER_CLOSE);
return source;
}
current = scanner.getNextToken();
}
throw new Error("Class not found: " + constructor.name);
}
}
/// </script>
/// <script>
/// export
class TextSplicer {
constructor(text) {
this._source = text;
this._splices = [];
}
splice(start, end, contents) {
this._splices.push(new TextSplicer.Splice(start, end, contents));
}
toString() {
this._splices = this._splices.sort((a, b) => (a.start - b.start));
let parts = [];
// NB: `prefaceMark` and `end` may end up with value `undefined` in a few
// different cases. It's okay; `substring` will still work how we want.
for (let i = 0, n = this._splices.length; i < n; ++i) {
let prefaceMark = end;
var { start, end } = this._splices[i];
if (start < prefaceMark) throw new Error("Overlapping splices");
parts.push(this._source.substring(prefaceMark, start));
parts.push(this._splices[i].contents);
}
parts.push(this._source.substring(end));
return parts.join("");
}
}
TextSplicer.Splice = class Splice {
constructor(start, end, contents) {
this.start = start;
this.end = end;
this.contents = contents;
}
}
/// </script>
/// <script>
/// import { SimpleScanner } from "./SimpleScanner.js";
/// export
class TripleScanner extends SimpleScanner {
getNextToken() {
let result = this.getGenericToken();
if (result && result.type == SimpleScanner.UNKNOWN) {
result.content = this._scanText();
if (result.content == "/// \x3Cscript\x3E" ||
result.content == "// \x3Cscript\x3E") {
result.type = TripleScanner.DELIMITER_OPEN;
} else if (result.content == "/// \x3C/script\x3E" ||
result.content == "// \x3C/script\x3E") {
result.type = TripleScanner.DELIMITER_CLOSE;
} else {
result.type = TripleScanner.TEXT;
}
}
return result;
}
_scanText() {
let result = "";
while (this.tip && !this.isWhitespace(this.tip)) {
result += this.readCodePoint();
}
if (result.startsWith("//")) {
while (this.tip && this.tip != "\n") {
result += this.readCodePoint();
}
}
return result;
}
}
TripleScanner.WHITESPACE = SimpleScanner.WHITESPACE;
TripleScanner.TEXT = 1;
TripleScanner.DELIMITER_OPEN = 2;
TripleScanner.DELIMITER_CLOSE = 3;
/// </script>
/// <script>
/// import { BrowserWorkerHandler } from "./BrowserWorkerHandler.js";
/// import { BrowserQuirksHack } from "./BrowserQuirksHack.js";
/// import { BrowserUI } from "./BrowserUI.js";
/// import { ExecutionContext } from "./ExecutionContext.js";
/// import { FileCache } from "./FileCache.js";
/// import { FileUtils } from "./FileUtils.js";
/// import { Sourcerer } from "./Sourcerer.js";
/// import { System } from "./System.js";
/// import { TestMediator } from "./TestMediator.js";
/// import { TestRunner } from "./TestRunner.js";
/// import { WebkitFileIngester } from "./WebkitFileIngester.js";
/// import { credits } from "./credits.js";
/// export
function BrowserHandler() {
if (typeof(window) == "undefined") {
throw new System.ShuntingError("not a browser environment");
}
this._quirksHack = new BrowserQuirksHack(window);
this._location = window.location;
this._domRoot = window.document;
this._workOrders = new Map();
this._ui = null;
this._uiMediator = null;
this._directoryInput = null;
this._fileSource = null;
this._contextToWorkerMap = new WeakMap();
let scriptSourceCode = this._blankPage();
this._sourcerer = new Sourcerer(scriptSourceCode);
this._workerURI = BrowserWorkerHandler.getWorkerURI(this._sourcerer);
}
{
let $proto = BrowserHandler.prototype;
$proto._blankPage = function() {
try {
return this._domRoot.body.innerHTML;
} finally {
this._domRoot.body.textContent = "";
}
}
$proto.run = function() {
this._domRoot.defaultView.onload = () => {
if (this._quirksHack.wrap()) return;
this._ui = new BrowserUI(this, this._domRoot.body);
this._domRoot.defaultView.addEventListener("message", this);
this._directoryInput = this._domRoot.createElement("input");
this._directoryInput.setAttribute("type", "file");
this._directoryInput.setAttribute("webkitdirectory", "true");
this._domRoot.head.appendChild(this._directoryInput);
this._directoryInput.onchange = this._onDirectoryChange.bind(this);
}
}
// NB: internal use only (for UI; not part of the abstract system interface)
$proto.getFiles = function() {
this._directoryInput.click();
}
$proto._onDirectoryChange = function(event) {
this._ui.acknowledgeTests(null);
this._fileSource = WebkitFileIngester.cache(event.target.files);
let harness = new TestRunner(this);
harness.onbadconfig = this._ui.showConfigProblem.bind(this._ui);
harness.onpathsknown = this._ui.acknowledgeTests.bind(this._ui);
harness.ontestskip = this._ui.markTestAsSkipped.bind(this._ui);
harness.ontestexecute = this._getResults.bind(this);
harness.runTests();
}
$proto.createContext = function(parent, wantMagic = false) {
let context = new ExecutionContext(parent, wantMagic);
if (!parent) {
let worker = new Worker(this._workerURI);
worker.addEventListener("message", this);
this._contextToWorkerMap.set(context, worker);
}
return Promise.resolve(context);
}
$proto.startCountdown = function(context, millis) {
let message = new TestMediator.StartCountdownWorkerMessage(millis);
let rootContext = ExecutionContext.getRoot(context);
let worker = this._contextToWorkerMap.get(rootContext);
worker.postMessage(message.toString());
return Promise.resolve(); // XXX fire-and-forget is okay here, right?
}
$proto.loadScript = function(context, fileName, contents) {
return new Promise((resolve) => {
let message = new TestMediator.EvalScriptWorkerMessage(
fileName, contents, context.isMagic, this._workOrders
);
this._workOrders.set(message.id, {
resolve: resolve,
context: context,
path: fileName
});
let rootContext = ExecutionContext.getRoot(context);
let worker = this._contextToWorkerMap.get(rootContext);
worker.postMessage(message.toString());
});
}
$proto.handleEvent = function(event) {
switch (event.type) {
case "message": return void(this._onMessage(event));
default: throw new Error("Unexpected event: " + event.type);
}
}
$proto._onMessage = function(event) {
try {
var payload = JSON.parse(event.data);
if (!("type" in payload)) return;
} catch (ex) { }
switch (payload.type) {
case TestMediator.RESOLVE_EVAL:
return void(this._onResolveEval(payload));
case TestMediator.REVIEW_RESULTS:
return void(this._onResultsReady(payload));
case TestMediator.READ_FILE:
return void(this._onFileReadRequest(payload, event.target));
}
}
$proto._pullOrder = function(id) {
let result = this._workOrders.get(id);
this._workOrders.delete(id);
return result;
}
$proto._onResolveEval = function(message) {
let order = this._pullOrder(message.ref);
if (message.error != null) {
let { context, path } = order;
let { error, stack } = message;
let exception = new BrowserHandler.Exception(path, error, stack);
let rootContext = ExecutionContext.getRoot(context);
rootContext.recordException(path, exception);
}
order.resolve();
}
$proto._getResults = function(context, path) {
const { RequestResultsWorkerMessage } = TestMediator;
let message = new RequestResultsWorkerMessage(this._workOrders);
let rootContext = ExecutionContext.getRoot(context);
this._workOrders.set(message.id, { path: path, context: rootContext });
let worker = this._contextToWorkerMap.get(rootContext);
worker.postMessage(message.toString());
}
$proto._onResultsReady = function(message) {
let order = this._pullOrder(message.ref);
message.value.metafailures = this._getMetafailures(order.context);
this._ui.showResults(order.path, message.value);
}
$proto._getMetafailures = function(context) {
let lists = Array.from(context.exceptions.values());
return lists.reduce((all, some) => all.concat(some), []);
}
$proto._onFileReadRequest = function(payload, worker) {
const { ResolveReadWorkerMessage } = TestMediator;
this.read(payload.value, payload.raw).then((contents) => {
if (payload.raw) {
contents = Array.from(new Uint8Array(contents));
}
let response = new ResolveReadWorkerMessage(contents, payload.id);
worker.postMessage(String(response));
});
}
$proto.read = function(path, raw) {
let file = this._fileSource.recall(path);
return WebkitFileIngester.readBlob(file, raw);
}
$proto.list = function(path) {
let kids = FileUtils.list(this._fileSource, path);
return Promise.resolve(kids);
}
}
BrowserHandler.Exception = class Exception {
constructor(path, message, stack) {
// NB: Instances MUST remain stringify-able.
this.path = path;
this.message = message;
this.stack = stack;
}
}
/// </script>
/// <script>
/// import { ExecutionContext } from "./ExecutionContext.js";
/// import { InaftPathResolver } from "./InaftPathResolver.js";
/// import { InaftTestHelper } from "./InaftTestHelper.js";
/// import { Loader } from "./Loader.js";
/// import { Require } from "./Require.js";
/// import { Sourcerer } from "./Sourcerer.js";
/// export
class BrowserWorkerHandler {
constructor(worker) {
this._worker = worker;
this._workOrders = new Map();
this._rootContext = ExecutionContext.createRoot(this);
}
static getWorkerURI(sourcerer) {
return Sourcerer.makeDataURI(
Sourcerer.makeIIFE(
sourcerer.extractClassDefinition(ExecutionContext) + "\n" +
sourcerer.extractClassDefinition(ExecutionTimeout) + "\n" +
sourcerer.extractClassDefinition(InaftPathResolver) + "\n" +
sourcerer.extractClassDefinition(InaftTestHelper) + "\n" +
sourcerer.extractClassDefinition(Loader) + "\n" +
sourcerer.extractClassDefinition(Require) + "\n" +
sourcerer.extractClassDefinition(TestMediator) + "\n" +
sourcerer.extractClassDefinition(BrowserWorkerHandler) + "\n" +
"BrowserWorkerHandler.install(worker);",
"worker", "this"
)
);
}
static install(worker) {
let handler = new BrowserWorkerHandler(worker);
worker.addEventListener("message", handler);
}
read(path, raw) {
return new Promise((resolve) => {
const { ReadFileWorkerMessage } = TestMediator;
let message = new ReadFileWorkerMessage(path, raw, this._workOrders);
this._workOrders.set(message.id, { resolve: resolve, raw: raw });
this._worker.postMessage(String(message));
});
}
handleEvent(event) {
switch (event.type) {
case "message": return void(this._onMessage(event));
default: throw new Error("Unexpected event: " + event.type);
}
}
_onMessage(event) {
try {
var payload = JSON.parse(event.data);
if (!("type" in payload)) return;
} catch (ex) { }
switch (payload.type) {
case TestMediator.EVAL_SCRIPT:
return void(this._onEvalScriptRequest(payload, event.target));
case TestMediator.REQUEST_RESULTS:
return void(this._onTestResultsRequested(payload, event.target));
case TestMediator.RESOLVE_READ:
return void(this._onResolveRead(payload));
case TestMediator.START_COUNTDOWN:
return void(this._onStartCountdown(payload));
}
}
_onEvalScriptRequest(message, target) {
const { ResolveEvalWorkerMessage } = TestMediator;
const { name, value, magic } = message;
let context = new ExecutionContext(this._rootContext, magic);
let exception = ExecutionContext.evaluate(context, name, value);
if (exception) {
var error = exception.name + ": " + exception.message;
var stack = exception.stack;
}
let response = new ResolveEvalWorkerMessage(message.id, error, stack);
target.postMessage(response.toString());
}
_onTestResultsRequested(message, target) {
const { ReviewResultsWorkerMessage } = TestMediator;
ExecutionContext.preemptTimerIfDone(this._rootContext);
return this._rootContext.getTestResults().then((results) => {
let response = new ReviewResultsWorkerMessage(results, message.id);
target.postMessage(response.toString());
});
}
_onResolveRead(message) {
let order = this._workOrders.get(message.ref);
if (!order) throw new Error("Unexpected response: " + message);
this._workOrders.delete(message.ref);
if (order.raw) {
order.resolve(Uint8Array.from(message.value));
} else {
order.resolve(message.value);
}
}
_onStartCountdown(message) {
ExecutionContext.setUpTimer(this._rootContext, ($finish) => {
let ref = this._worker.setTimeout($finish, message.value);
return () => void(this._worker.clearTimeout(ref));
});
}
}
/// </script>
/// <script>
/// import { ExecutionTimeout } from "./ExecutionTimeout.js";
/// import { InaftPathResolver } from "./InaftPathResolver.js";
/// import { InaftTestHelper } from "./InaftTestHelper.js";
/// import { Loader } from "./Loader.js";
/// export
class ExecutionContext {
constructor(parent, isMagic, loader = null) {
this.parent = parent;
this.isMagic = isMagic;
this.loader = loader;
this.pathResolver = new InaftPathResolver(isMagic);
if (parent) {
if (loader) throw new Error("Non-root execution context");
this.exceptions = null;
} else {
this.exceptions = new Map();
}
this.timer = null;
}
static createRoot(system, withMagic = false, loader = new Loader()) {
let result = new ExecutionContext(null, withMagic, loader);
result.timer = new ExecutionTimeout();
let helperModule = Loader.createModule(InaftPathResolver.HELPER_PATH);
helperModule.exports = new InaftTestHelper(system, result.timer);
loader.cacheModule(helperModule);
return result;
}
static setUpTimer(context, $init) {
let rootContext = ExecutionContext.getRoot(context);
++rootContext.timer.contention;
rootContext.timer.start($init)
return Promise.resolve();
}
static preemptTimerIfDone(context) {
let rootContext = ExecutionContext.getRoot(context);
--rootContext.timer.contention;
if (!rootContext.timer.contention) {
rootContext.timer.burn();
}
return rootContext.timer;
}
recordException(path, exception) {
if (this.parent) throw new Error("Non-root execution context");
let list = this.exceptions.get(path) || [];
if (!list.length) {
this.exceptions.set(path, list);
}
list.push(exception);
}
getTestResults() {
if (!this.loader) throw new Error("Disassociated execution context");
let help = this.loader.getCachedModule(InaftPathResolver.HELPER_PATH);
return help.exports.getResults();
}
static evaluate(context, name, text) {
var resolver = context.pathResolver;
var rootContext = ExecutionContext.getRoot(context);
try {
rootContext.loader.load(name, text, resolver);
} catch (ex) {
rootContext.recordException(name, ex);
return ex;
}
return null;
}
static getRoot(context) {
while (context.parent) {
context = context.parent;
}
return context;
}
}
/// </script>
/// <script>
/// export
class ExecutionTimeout {
constructor() {
this.$cleanup = null;
this._promise = new Promise((resolve) => {
this._resolve = resolve;
});
this.contention = 0;
}
start($init) {
this.$cleanup = $init(this.burn.bind(this));
}
burn() {
if (this.$cleanup) {
this.$cleanup.call();
}
this._resolve();
}
then(...args) {
return this._promise.then.apply(this._promise, args);
}
}
/// </script>
/// <script>
/// import { InaftPathResolver } from "./InaftPathResolver.js";
/// export
class InaftTestHelper {
constructor(readable, timer) {
this._readable = readable;
this._timer = timer;
this.read = this.$read.bind(this);
this.test = this.$test.bind(this);
this.test.is = this.is = this.$is.bind(this);
this.test.ok = this.ok = this.$ok.bind(this);
this._blockers = [];
this._currentID = 1;
this._infos = [];
}
$read(path, wantRaw = false) {
let realPath = InaftPathResolver.getRealNameForMagicPath(path);
if (!realPath) {
throw new Error("Illegal read for path " + JSON.stringify(path));
}
return this._readable.read(realPath, wantRaw);
}
$is(a, b) {
if (a !== b) throw new Error(
"expected " + b + " got " + a
);
}
$ok(x) {
if (!x) throw new Error(
"expected true expression"
);
}
$test(callback) {
++this._timer.contention;
let label = callback.name;
let info = new InaftTestHelper.TestInfo(this._currentID++, label);
this._infos.push(info);
let $checkSyncAndAsyncErrors = (() => {
try {
return Promise.resolve(callback());
} catch (ex) {
return Promise.reject(ex);
}
});
this._blockers.push(
new Promise((resolve) => {
$checkSyncAndAsyncErrors().catch((err) => {
console.log(">>>>>err", err); // XXX
info.failure = new InaftTestHelper.AbnormalExceptionError(err);
}).then(() => { // XXX read as `finally` (not yet widely supported)
info.stopped = true;
--this._timer.contention;
if (!this._timer.contention) {
this._timer.burn();
}
});
this._timer.then(() => {
if (!info.stopped) {
info.failure = new InaftTestHelper.TestTimeoutError();
info.stopped = true;
}
resolve();
});
})
);
}
getResults() {
return Promise.all(this._blockers).then(() => {
return new Promise((resolve) => {
resolve({
passed: !this._infos.filter((x) => !!x.failure).length,
details: this._infos.map((x) => x.toJSONObject())
});
})
});
}
}
InaftTestHelper.TestInfo = class TestInfo {
constructor(id, label) {
this.id = id;
this.label = label;
this.failure = null;
this.stopped = false;
}
toJSONObject() {
let result = Object.assign({ }, this);
if (this.failure) {
result.failure = {
message: this.failure.message,
stack: this.failure.stack
}
}
return result;
}
}
InaftTestHelper.AbnormalExceptionError =
class AbnormalExceptionError extends Error
{
constructor(err) {
super("Abnormal exception from rejected promise or weird error type");
if (typeof(err) == "object" && err != null) {
var { message, stack } = err;
}
if (typeof(message) != "string") {
let $serialize = (err) => {
if (err === "") return "\"\" [empty string]";
if (!err) return String(err);
return JSON.stringify(err) || "definitely weird";
}
this.stack = null;
this.message += ": (" + $serialize(err) + ")";
err = this;
} else if (typeof(stack) != "string") {
err = Object.assign({ stack: null }, err);
}
return err;
}
}
InaftTestHelper.TestTimeoutError = class TestTimeoutError extends Error {
constructor() {
super("Inaft test timeout");
}
}
/// </script>
/// <script>
/// import { Require } from "./Require.js";
/// export
class Loader {
constructor() {
this._modules = new Map();
}
static createModule(id) {
return { id: id, exports: {} };
}
load(id, text, resolver) {
let module = this.getCachedModule(id);
if (module) return module;
module = this.cacheModule(Loader.createModule(id));
let require = new Require(module, this, resolver);
let $ = (new Function("require", "module", "exports", text));
try {
$(require, module, module.exports);
} catch (ex) {
this._modules.delete(id);
throw ex;
}
return module;
}
cacheModule(module) {
if (this.getCachedModule(module.id)) {
throw new Error("Module already cached: " + module.id);
}
this._modules.set(module.id, module);
return module;
}
getCachedModule(id) {
return this._modules.has(id) ?
this._modules.get(id) :
null;
}
}
/// </script>
/// <script>
/// export
class Require {
constructor(module, loader, resolver) {
this._module = module;
this._loader = loader;
this._resolver = resolver;
return this.$require.bind(this);
}
$require(originalID) {
if (this._module) {
var relativeTo = this._module.id;
}
let module, id = this._resolver.handle(originalID, relativeTo);
if (id) {
module = this._loader.getCachedModule(id);
if (!module) {
module = this._loader.getCachedModule(id + ".js");
}
}
if (!module) {
let error = new Error(
"Cannot resolve module " + JSON.stringify(originalID)
);
if (this._module) {
error.message += " required by " + JSON.stringify(this._module.id)
}
throw error;
}
return module.exports;
}
}
/// </script>
/// <script>
/// export
class BrowserQuirksHack {
constructor(window) {
this._document = window.document;
this._location = window.location;
this._frame = null;
this._isOuter = window.parent == window;
}
get inner() {
if (!this._isOuter) return null;
return this._frame.contentWindow;
}
static buildMarkup(name, content = []) {
let result = "<" + name + ">";
if (content != null) {
result += content.join("");
result += "<" + "/" + name + ">";
}
return result;
}
wrap() {
if (this._isOuter) {
if (this._location.search != "") {
throw new Error("Unexpected query string in URI");
}
let { body } = this._document;
body.style.margin = "0";
body.style.padding = "0";
body.innerHTML = BrowserQuirksHack.buildMarkup("iframe");
this._frame = body.firstChild;
this._frame.style.width = "100vw";
this._frame.style.height = "100vh";
this._frame.style.border = "0";
this._frame.style.margin = "0";
this._frame.src = this._location + "?";
} else {
let $$ = BrowserQuirksHack.buildMarkup;
let substitute =
$$("!doctype html", null) + "\n" +
$$("html", [
$$("head") + $$("body")
]);
this._document.write(substitute);
}
return this.inner;
}
}
/// </script>
/// <script>
/// import { SimpleScanner } from "./SimpleScanner.js";
/// import { TestMediator } from "./TestMediator.js";
/// export
class BrowserUI {
constructor(system, container) {
this._system = system;
const doc = container.ownerDocument;
BrowserUI.applySkin(doc.head);
BrowserUI.injectUIElements(container);
this._results = new Map();
this._uiState = new BrowserUI.UIState();
let $getItem = (selector) => doc.querySelectorAll(selector)[0];
this._overviewListBox = $getItem("#test-list");
this._overviewFailedLabel = $getItem("#overview-pane .failed.label");
this._overviewPassedLabel = $getItem("#overview-pane .passed.label");
this._overviewFailedSegment = $getItem("#overview-pane .failed.segment");
this._overviewEntriesBox = $getItem("#overview-pane .contents");
this._overviewLoadButton = $getItem("#file-source-chooser");
this._detailsTitleBox = $getItem("#details-pane .title");
this._detailsFailedLabel = $getItem("#details-pane .failed.label");
this._detailsPassedLabel = $getItem("#details-pane .passed.label");
this._detailsFailedSegment = $getItem("#details-pane .failed.segment");
this._detailsEntriesBox = $getItem("#details-pane .contents");
this._overviewLoadButton.onclick = () => void(this._system.getFiles());
this._overviewLoadButton.focus();
this._overviewListBox.addEventListener("click", this);
this._outer = doc.defaultView.parent;
doc.defaultView.addEventListener("message", this)
}
_updateUI(delta) {
const $ = JSON.stringify;
let oldState = this._uiState;
this._uiState = new BrowserUI.UIState(oldState, delta);
let properties = Object.keys(this._uiState);
for (let i = 0, n = properties.length; i < n; ++i) {
let name = properties[i];
if ($(this._uiState[name]) != $(oldState[name])) {
switch (name) {
case "configProblem":
this._alertBadConfig();
break;
case "newResults":
case "testPaths":
this._populateOverview();
break;
case "visibleDetails":
this._populateDetails();
break;
}
}
}
}
_populateOverview() {
this._overviewEntriesBox.classList.remove("beckoning");
const $ = BrowserUI.createMarkup;
const $escape = BrowserUI.escapeAsHTML;
const $attr = (type, path) => BrowserUI.concat(
'class="' + type + ' test-set"',
'data-test-path="' + $escape(path) + '"'
);
// NB: this may end up null here, due to a hack to give us a small bump
// for UI responsiveness.
const { testPaths } = this._uiState;
let total = testPaths && testPaths.length;
if (total == null) return;
let passCount = 0;
let failCount = 0;
let markup = new BrowserUI.TextSplice();
for (let i = 0, n = total; i < n; ++i) {
let type = "";
let results = this._results.get(testPaths[i]);
if (results && !results.skipped) {
if (results.passed && !results.metafailures.length) {
type = "passed";
++passCount;
} else {
type = "failed";
++failCount;
}
}
if (!results || !results.skipped) {
markup.append(
$('li', $attr(type, testPaths[i]), [
$('span', 'class="label"', [
$escape(BrowserUI.getShortNameFromTestPath(testPaths[i]))
])
])
);
}
if (results && results.skipped) {
--total;
}
}
if (passCount + failCount < total) {
this._overviewPassedLabel.classList.add("absent");
this._overviewFailedLabel.classList.add("absent");
this._changeFailBar(this._overviewFailedSegment);
} else {
this._overviewPassedLabel.textContent = passCount + " passed";
this._overviewPassedLabel.classList.remove("absent");
this._overviewFailedLabel.textContent = failCount + " failed";
this._overviewFailedLabel.classList.toggle("absent", !failCount);
this._changeFailBar(this._overviewFailedSegment, failCount / total);
}
this._overviewListBox.innerHTML = markup;
}
static getShortNameFromTestPath(path) {
return BrowserUI.getTestPathParts(path)[1];
}
static getTestPathParts(path, end = undefined) {
const start = "tests/".length;
if (end == undefined) end = path.indexOf("/", start);
let shortname = path.substring(start, end)
return [
path.substring(0, start),
shortname,
path.substring(start + shortname.length)
];
}
_changeFailBar(segment, ratio) {
if (ratio == undefined) {
segment.style.width = 0;
segment.parentNode.classList.add("pending");
} else {
segment.style.width = (ratio * 100) + "%";
segment.parentNode.classList.remove("pending");
}
}
_populateDetails() {
const path = this._uiState.visibleDetails;
const results = this._results.get(path);
this._detailsTitleBox.innerHTML = this._createPathMarkup(path);
let content = new BrowserUI.TextSplice();
if (results) {
for (let i = 0, n = results.metafailures.length; i < n; ++i) {
let metafailure = results.metafailures[i];
content.append(this._createDetailItemMarkup(path, metafailure));
}
const details = results.details.slice().sort((a, b) => (a.id - b.id));
const total = details.length;
let failCount = 0;
for (let i = 0; i < total; ++i) {
const { id, failure, label } = details[i];
content.append(this._createDetailItemMarkup(path, failure, id, label));
if (failure) ++failCount;
}
if (results.metafailures.length > 0) {
this._changeFailBar(this._detailsFailedSegment, 1);
this._detailsFailedLabel.textContent = "failed";
this._detailsFailedLabel.classList.remove("absent");
this._detailsPassedLabel.classList.add("absent");
} else {
this._detailsFailedLabel.textContent = failCount + " failed";
this._detailsFailedLabel.classList.toggle("absent", !failCount);
if (!total) {
this._changeFailBar(this._detailsFailedSegment, 0);
this._detailsPassedLabel.textContent = "passed";
} else {
let passCount = total - failCount;
this._changeFailBar(this._detailsFailedSegment, failCount / total);
this._detailsPassedLabel.textContent = passCount + " passed";
}
this._detailsPassedLabel.classList.remove("absent");
}
} else {
this._detailsFailedLabel.classList.add("absent");
this._detailsPassedLabel.classList.add("absent");
this._changeFailBar(this._detailsFailedSegment);
}
this._detailsEntriesBox.innerHTML = content;
}
_createPathMarkup(path, end = undefined) {
const $ = BrowserUI.createMarkup;
let parts = BrowserUI.getTestPathParts(path, end);
parts[1] = $('span', 'class="shortname"', [
BrowserUI.escapeAsHTML(parts[1])
]);
return String(new BrowserUI.TextSplice(parts));
}
_createDetailItemMarkup(path, failure, id = 0, label = null) {
const $ = BrowserUI.createMarkup;
const $escape = BrowserUI.escapeAsHTML;
if (failure) {
var errorHTML = $('div', 'class="error-info"', [
$escape(failure.message + "\n\n" + (failure.stack || failure.extra))
]);
var type = "failed";
} else {
var type = "passed";
}
if (id > 0) {
if (!label) label = $escape(path) + " test #" + $escape(id);
var topbarHTML = $('div', 'class="item-info"', [
$('span', 'class="sigil"'),
$('span', 'class="label"', [ label ])
]);
}
return $('div', 'class="' + type + ' test-detail"', [
topbarHTML || "",
errorHTML || ""
]);
}
_alertBadConfig() {
this._changeFailBar(this._overviewFailedSegment, 1);
this._overviewEntriesBox.classList.remove("beckoning");
const path = "tests/config.json"; // XXX
this._detailsTitleBox.innerHTML = this._createPathMarkup(path, Infinity);
this._changeFailBar(this._detailsFailedSegment, 1);
this._detailsEntriesBox.innerHTML = this._createDetailItemMarkup(path, {
message: this._uiState.configProblem.outer,
extra: this._uiState.configProblem.inner
});
}
handleEvent(event) {
switch (event.type) {
case "click": return void(this._onClick(event));
default: throw new Error("Unexpected event: " + event.type);
}
}
_onClick(event) {
this._selectTestSetFromClickedBox(event.target);
}
_selectTestSetFromClickedBox(node) {
let target = this._getHitTestSetItem(node);
let items = this._overviewListBox.querySelectorAll("li.test-set");
for (let i = 0, n = items.length; i < n; ++i) {
items[i].classList.toggle("selected", items[i] == target);
}
this._updateUI({
visibleDetails: target.dataset.testPath
});
}
_getHitTestSetItem(node) {
while (node) {
if (node.parentNode == this._overviewListBox &&
node.classList.contains("test-set")) {
return node;
}
node = node.parentNode;
}
return null;
}
acknowledgeTests(paths) {
this._updateUI({ testPaths: paths });
}
showConfigProblem(error) {
const outer = error.message;
const inner = error.innerException.message;
this._updateUI({ configProblem: { outer: outer, inner: inner } });
}
markTestAsSkipped(path) {
this._results.set(path, { skipped: true });
// Force an update (use new array slice to thwart identity comparison)
this._updateUI({ testPaths: this._uiState.testPaths.slice() });
}
showResults(path, results) {
this._results.set(path, results);
this._updateUI({ newResults: path });
}
static createChildElement(parent, childName) {
return parent.appendChild(parent.ownerDocument.createElement(childName));
}
static createMarkup(name, attributes = "", content = []) {
return (
"<" + name + " " + attributes + ">" +
content.join("") +
"<" + "/" + name + ">"
);
}
static concat() {
return Array.from(arguments).join("\n");
};
static escapeAsHTML(content) {
let result = "";
let scanner = new SimpleScanner(String(content));
while (scanner.position < scanner.text.length) {
let codePoint = scanner.readCodePoint();
switch (codePoint) {
case "<":
result += "&lt;";
break;
case ">":
result += "&gt;";
break;
case "&":
result += "&amp;";
break;
case " ":
result += "&nbsp;";
break;
case "\"":
result += "&quot;";
break;
case "'":
result += "&#x27;";
break;
default:
result += codePoint;
break;
}
}
return result;
}
static injectUIElements(body) {
const $ = BrowserUI.createMarkup;
body.innerHTML = BrowserUI.concat(
$('div', 'id="overview-pane" class="vertical-fill"', [
$('div', 'class="header"', [
$('span', 'class="title"', [ 'Test Sets' ]),
$('span', 'class="failed label"'),
$('span', 'class="passed label"')
]),
$('div', 'class="pending separator"', [
$('div', 'class="failed segment"')
]),
$('div', 'class="beckoning contents"', [
$('ul', 'id="test-list"'),
$('div', 'class="load-area"', [
$('button', 'id="file-source-chooser"', [ 'Open\u2026' ])
])
])
]),
$('div', 'id="details-pane" class="vertical-fill"', [
$('div', 'class="header"', [
$('span', 'class="title"'),
$('span', 'class="failed label"'),
$('span', 'class="passed label"')
]),
$('div', 'class="pending separator"', [
$('div', 'class="failed segment"')
]),
$('div', 'class="contents"')
])
);
}
static applySkin(head) {
let styleElement = BrowserUI.createChildElement(head, "style");
let $ = styleElement.sheet.insertRule.bind(styleElement.sheet);
$("body {" +
" background-color: #141618;" +
" color: rgba(255, 255, 255, 0.8);" +
" font-family: sans;" +
" font-weight: 500;" +
" margin: 0.5em;" +
"}");
$(".absent {" +
" display: none;" +
"}");
$(".vertical-fill {" +
/* NB: this factor must match container (body)'s margin above; also need
* to include offset for top+bottom border--it's not sufficient merely
* to set `box-sizing: border-box` for some reason... */
" height: calc(100vh - 2 * 0.5em - 2 * 1px);" +
" overflow-y: auto;" +
"}");
$("#overview-pane {" +
" min-width: 32ch;" +
" float: left;" +
/* NB: top/bottom must match border width from .vertical-fill above */
" border: 1px solid #101314;" +
" background-color: #1c2022;" +
"}");
$("#details-pane {" +
" background-color: #24282a;" +
/* NB: top/bottom must match border width from .vertical-fill above */
" border: 1px solid #101314;" +
" border-left: 0;" +
" border-right: 0;" +
"}");
$(".contents {" +
" padding: 1em;" +
"}");
$(".header {" +
" padding: 1em;" +
" font-weight: 600;" +
" font-stretch: semi-condensed;" +
" color: rgba(255, 255, 255, 0.4);" +
"}");
$(".load-area {" +
" text-align: center;" +
" padding: 4em 0;" +
"}");
$("#file-source-chooser {" +
" width: calc(100% - 2 * 1em - 2 * 1em);" +
" padding: 0.5em 1em;" +
" background-color: #161a1b;" +
" border: 1px solid #161a1b;" +
" border-radius: 2px;" +
" color: rgba(255, 255, 255, 0.8);" +
"}");
$("#file-source-chooser:hover {" +
" border: 1px solid #2b6287;" +
" color: rgba(255, 255, 255, 1.0);" +
"}");
$("#overview-pane .contents:not(.beckoning) .load-area {" +
" display: none;" +
"}");
$(".header .title {" +
" margin-right: 1.5em;" +
"}");
$(".header .failed.label {" +
" color: rgba(242, 119, 119, 0.8);" +
" margin-left: 0.75em;" +
"}");
$(".header .passed.label {" +
" color: rgba(93, 167, 0, 0.8);" +
" margin-left: 0.75em;" +
"}");
$(".separator {" +
" width: 100%;" +
"}");
$(".separator .failed.segment {" +
" width: 0;" +
" height: 2px;" +
"}");
$("#overview-pane .separator {" +
" background-color: rgba(93, 167, 0, 0.8);" +
"}");
$("#overview-pane .pending.separator {" +
" background-color: rgba(255, 255, 255, 0.4);" +
"}");
$("#overview-pane .separator .failed.segment {" +
" background-color: rgba(242, 119, 119, 0.8);" +
"}");
$("#details-pane .separator {" +
" background-color: rgba(93, 167, 0, 1.0);" +
/* NB: this left/right margin should match header padding */
" margin: 0 1em;" +
/* NB: the factor here should match the left/right margin above */
" width: calc(100% - 2 * 1em);" +
"}");
$("#details-pane .pending.separator {" +
" background-color: rgba(255, 255, 255, 0.5);" +
"}");
$("#details-pane .separator .failed.segment {" +
" background-color: rgba(242, 119, 119, 1.0);" +
"}");
$("#details-pane .header {" +
" color: rgba(255, 255, 255, 0.5);" +
"}");
$("#details-pane .header .shortname {" +
" color: rgba(255, 255, 255, 0.8);" +
" font-weight: 600;" +
"}");
$("#details-pane .header .failed.label {" +
" color: rgba(242, 119, 119, 1.0);" +
"}");
$("#details-pane .header .passed.label {" +
" color: rgba(93, 167, 0, 1.0);" +
"}");
$("#overview-pane ul, #overview-pane .contents {" +
" padding: 0;" +
"}");
$("#overview-pane .beckoning.contents #test-list {" +
" display: none;" +
"}");
$(".test-set {" +
" list-style-type: none;" +
" line-height: 2.25em;" +
" border-left: 2px solid transparent;" +
" cursor: pointer;" +
" color: rgba(255, 255, 255, 0.5);" +
"}");
$(".test-set:hover:not(.selected) {" +
" border-left-color: #2b6287;" +
" background-color: #161a1b;" +
"}");
$(".test-set.selected {" +
" background-color: #141618;" +
" border-left-color: #40a9f3;" +
"}");
$(".test-set:before {" +
" margin-left: 1em;" +
" content: '\\2739';" +
"}");
$(".failed.test-set {" +
" color: rgba(242, 119, 119, 0.8);" +
"}");
$(".failed.test-set:before {" +
" content: '\\2718';" +
"}");
$(".passed.test-set {" +
" color: rgba(93, 167, 0, 0.8);" +
"}");
$(".passed.test-set:before {" +
" content: '\\2714';" +
"}");
$(".test-set .label {" +
" margin: 1em;" +
" color: rgba(255, 255, 255, 0.8);" +
"}");
$(".test-detail {" +
" background-color: #1c2022;" +
" margin: 1em 0;" +
" border-radius: 2px;" +
"}");
$(".test-detail .item-info {" +
" line-height: 2em;" +
"}");
$(".test-detail .sigil {" +
" background-color: #191c1d;" +
" width: 2.25em;" +
" display: inline-block;" +
" text-align: center" +
"}");
$(".test-detail .label {" +
" padding: 0 0.75em;" +
"}");
$(".failed.test-detail .sigil:after {" +
" color: rgba(242, 119, 119, 1.0);" +
" content: '\\2718';" +
"}");
$(".failed.test-detail .error-info {" +
" color: rgba(255, 255, 255, 0.8);" +
" background-color: rgba(0, 0, 0, 0.5);" +
" font-family: monospace;" +
" white-space: pre-wrap;" +
" line-height: 2em;" +
" padding: 1em;" +
" overflow-x: auto;" +
"}");
$(".passed.test-detail .sigil:after {" +
" color: rgba(93, 167, 0, 1.0);" +
" content: '\\2714';" +
"}");
}
}
BrowserUI.TextSplice = class TextSplice {
constructor(parts = []) {
this._parts = parts;
}
append(markup) {
this._parts.push(markup);
}
toString() {
return this._parts.join("");
}
}
BrowserUI.UIState = class UIState {
constructor(oldState = null, delta = null) {
Object.assign(this, oldState, delta);
}
}
BrowserUI.UIState.prototype.configProblem = null;
BrowserUI.UIState.prototype.newResults = null;
BrowserUI.UIState.prototype.testPaths = null;
BrowserUI.UIState.prototype.visibleDetails = null;
/// </script>
/// <script>
/// export
class TestMediator {
static forgeID(requests) {
do {
var id = (Math.random() * 0xFDB97531) >>> 0;
} while (requests.has(id));
return id;
}
}
TestMediator.EVAL_SCRIPT = "eval-script";
TestMediator.READ_FILE = "read-file";
TestMediator.REQUEST_RESULTS = "request-results";
TestMediator.RESOLVE_EVAL = "resolve-eval";
TestMediator.RESOLVE_READ = "resolve-read";
TestMediator.REVIEW_RESULTS = "review-results";
TestMediator.START_COUNTDOWN = "start-countdown";
TestMediator.WorkerMessage = class WorkerMessage {
toString() {
return JSON.stringify(this);
}
}
TestMediator.EvalScriptWorkerMessage =
class extends TestMediator.WorkerMessage {
constructor(fileName, contents, wantMagic, workOrders) {
super();
this.type = TestMediator.EVAL_SCRIPT;
this.value = contents;
this.name = fileName;
this.magic = wantMagic;
this.id = TestMediator.forgeID(workOrders);
}
}
TestMediator.ReadFileWorkerMessage =
class extends TestMediator.WorkerMessage {
constructor(path, raw, workOrders) {
super();
this.type = TestMediator.READ_FILE;
this.value = path;
this.raw = raw;
this.id = TestMediator.forgeID(workOrders);
}
}
TestMediator.RequestResultsWorkerMessage =
class extends TestMediator.WorkerMessage {
constructor(workOrders) {
super();
this.type = TestMediator.REQUEST_RESULTS;
this.id = TestMediator.forgeID(workOrders);
}
}
TestMediator.ResolveEvalWorkerMessage =
class extends TestMediator.WorkerMessage {
constructor(evalRequestID, error = null, stack = null) {
super();
this.type = TestMediator.RESOLVE_EVAL;
this.error = error;
this.stack = stack;
this.ref = evalRequestID;
}
}
TestMediator.ResolveReadWorkerMessage =
class extends TestMediator.WorkerMessage {
constructor(contents, readRequestID) {
super();
this.type = TestMediator.RESOLVE_READ;
this.value = contents;
this.ref = readRequestID;
}
}
TestMediator.ReviewResultsWorkerMessage =
class extends TestMediator.WorkerMessage {
constructor(results, resultsRequestID) {
super();
this.type = TestMediator.REVIEW_RESULTS;
this.value = results;
this.ref = resultsRequestID;
}
}
TestMediator.StartCountdownWorkerMessage =
class extends TestMediator.WorkerMessage {
constructor(millis) {
super();
this.type = TestMediator.START_COUNTDOWN;
this.value = millis;
}
}
/// </script>
/// <script>
/// export
class FileCache {
constructor() {
this._blobs = new Map();
}
static normalizePath(path) {
if (path.startsWith("/")) return path.substr(("/").length);
if (path.startsWith("./")) return path.substr(("./").length);
return path;
}
store(blob, path) {
path = FileCache.normalizePath(path);
this._blobs.set(path, blob);
}
recall(path) {
path = FileCache.normalizePath(path);
return this._blobs.get(path);
}
list() {
return Array.from(this._blobs.keys());
}
}
/// </script>
/// <script>
/// import { FileCache } from "./FileCache.js";
/// export
class FileUtils {
static list(cache, path) {
path = FileCache.normalizePath(path);
if (!path.endsWith("/")) path += "/";
let kids = new Set();
let all = cache.list();
for (let i = 0, n = all.length; i < n; ++i) {
if (!all[i].startsWith(path)) continue;
let relative = all[i].substr(path.length);
let idx = relative.indexOf("/");
if (idx < 0) { // file
var name = relative;
} else { // subdirectory
var name = relative.substr(0, idx + 1);
// assert(name.endsWith("/"))
}
if (!kids.has(name)) {
kids.add(name);
}
}
return Array.from(kids.values());
}
}
/// </script>
/// <script>
/// export
class System {
static create(system) {
let result = null;
try {
result = new system;
} catch (ex) {
if (!(ex instanceof System.ShuntingError)) {
throw ex;
}
}
return result;
}
}
System.ShuntingError = class ShuntingError extends Error { };
/// </script>
/// <script>
/// import { FileCache } from "./FileCache.js";
/// export
class WebkitFileIngester {
static cache(fileList, cache = new FileCache()) {
if (!fileList.length) return null;
let $prefixOf = (path) => path.substr(0, path.indexOf("/", 1));
let prefix = $prefixOf(fileList[0].webkitRelativePath);
for (let i = 0; i < fileList.length; ++i) {
// assert($prefixOf(fileList[i].webkitRelativePath) == prefix)
let path = fileList[i].webkitRelativePath.substr(prefix.length);
cache.store(fileList[i], path);
}
return cache;
}
static readBlob(blob, raw = false) {
if (!blob) return Promise.resolve(null);
if (typeof(FileReader) != "undefined") {
let reader = new FileReader();
return new Promise(function(resolve) {
reader.addEventListener("loadend", resolve);
if (raw) {
reader.readAsArrayBuffer(blob);
} else {
reader.readAsText(blob);
}
}).then(function(event) {
return event.target.result;
});
}
throw new Error("FileReader API not implemented");
}
}
/// </script>
/// <script>
/// export ...
var credits =
` ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' '
The primary work is inaft (0.9.8).
Available under MIT License, unless otherwise noted.
NB: Any auxiliary development tools, including but not limited to those
distributed alongside the project source code and used to build the primary
work, are separate works used under license compatible with this use and are
not considered a part of the primary work.
The primary work also incorporates elements from other works, which are
indentified below.
(The preceding notes are informational, and reproducing them is not itself
intended to be a prerequisite for redistribution, although the inclusion of
some of the content below may be required in order to redistribute the primary
work in whole or in part.)
Authorship and contributions
============================
* Colby Russell
* Ives van Hoorne
Works involved
==============
codesandbox.io test viewer visual design
(Inspired Inaft's in-browser UI. Used with permission.)
Copyright (C) 2018 Ives van Hoorne
Licenses compatible for use in the primary work
===============================================
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' `
/// </script>
/// <script>
/// import { ExecutionContext } from "./ExecutionContext.js";
/// import { FileReadAdapter } from "./FileReadAdapter.js";
/// import { Sourcerer } from "./Sourcerer.js";
/// import { TestRunner } from "./TestRunner.js";
/// import { Loader } from "./Loader.js";
/// import { InaftPathResolver } from "./InaftPathResolver.js";
/// import { InaftTestHelper } from "./InaftTestHelper.js";
/// import { FixedWidthResultsFormatter } from "./FixedWidthResultsFormatter.js";
/// import { System } from "System.js";
/// import { credits } from "./credits.js";
/// export
class NodeJSHandler {
constructor() {
if (typeof(process) == "undefined") {
throw new System.ShuntingError("not a NodeJS environment");
}
if (process.argv.indexOf("--version") >= 0) {
console.log(credits);
process.exit(0);
}
this._nodeFS = process.mainModule.require("fs");
this._nodePath = process.mainModule.require("path");
this._nodeVM = process.mainModule.require("vm");
this._process = process;
this._fileName = process.mainModule.filename;
let workDir = ".";
if (process.argv.length > 2) {
workDir = process.argv[2];
}
this._workDir = workDir;
this._table = null;
this._files = new FileReadAdapter(this._nodeFS, this._nodePath, workDir);
this._sourcerer = new Sourcerer(
this._nodeFS.readFileSync(this._fileName, { encoding: "utf8" })
);
}
run() {
let harness = new TestRunner(this);
harness.ontestexecute = this._showResults.bind(this);
harness.onpathsknown = (paths) => {
this._table = new FixedWidthResultsFormatter();
this._table.addColumn("status", { align: "center" });
this._table.addColumn("test set", { entries: paths });
this._table.addColumn("counts", { align: "right" });
console.log(this._table.formatHeader());
};
harness.onbadconfig = (error) => {
console.log(error.message + "\n\n" + error.innerException);
};
harness.runTests();
}
read(path, raw) {
return this._files.read(path, raw);
}
list(path) {
if (!path.startsWith("/")) {
path = this._nodePath.join(this._workDir, path);
}
return new Promise((resolve) => {
this._nodeFS.readdir(path, (err, fileNames) => {
if (err) return void(resolve(null));
resolve(fileNames.map((name) => {
let stats = this._nodeFS.statSync(path + name);
if (stats.isDirectory()) return name + "/";
return name;
}));
});
});
}
createContext(parent, wantMagic = false) {
if (parent) {
var context = new ExecutionContext(parent, wantMagic);
} else {
let sandbox = this._nodeVM.createContext({ console: console });
let iife = Sourcerer.makeIIFE(
this._sourcerer.extractClassDefinition(Loader) + "\n" +
this._sourcerer.extractClassDefinition(Require) + "\n" +
this._sourcerer.extractClassDefinition(InaftPathResolver) + "\n" +
"return new Loader();"
);
let loader = this._nodeVM.runInContext(iife, sandbox);
var context = ExecutionContext.createRoot(this, wantMagic, loader);
}
return Promise.resolve(context);
}
startCountdown(context, millis) {
return ExecutionContext.setUpTimer(context, ($finish) => {
let ref = setTimeout($finish, millis);
return () => void(clearTimeout(ref));
});
}
loadScript(context, fileName, fileText) {
ExecutionContext.evaluate(context, fileName, fileText);
return Promise.resolve();
}
_showResults(context, path) {
ExecutionContext.preemptTimerIfDone(context);
context.getTestResults().then((results) => {
this._printResults(path, results, context.exceptions);
});
}
_printResults(path, results, metafailures) {
const details = results.details;
const failed = details.filter((x) => (!!x.failure));
let status, count, countExtra = "";
if (results.passed && !metafailures.size) {
status = "pass";
if (details.length) {
count = details.length - failed.length;
} else {
count = "--";
}
} else {
status = "fail";
if (!metafailures.size) {
count = String(details.length - failed.length);
countExtra = " (of " + details.length + ")";
} else {
count = "!!!";
}
}
console.log(
this._table.formatRowText([ status, path, count ]) + countExtra
);
}
}
/// </script>
/// <script>
/// export
class FileReadAdapter {
constructor(fs, path, workDir) {
this._nodeFS = fs;
this._nodePath = path;
this._workDir = workDir;
}
read(path, raw) {
path = this._nodePath.join(this._workDir, path);
if (!raw) {
var options = { encoding: "utf8" };
}
return new Promise((resolve) => {
void this._nodeFS.readFile(path, options, (err, contents) => {
if (raw) {
resolve(Uint8Array.from(contents))
} else {
resolve(contents)
}
});
});
}
}
/// </script>
/// <script>
/// export
class FixedWidthResultsFormatter {
constructor() {
this._columns = [];
}
addColumn(header, options = {}) {
let width = header.length;
if (options.entries) {
for (let i = 0; i < options.entries.length; ++i) {
let current = options.entries[i];
if (current.length > width) {
width = current.length;
}
}
}
this._columns.push({
header: header,
width: width,
align: options.align || "left",
});
}
formatHeader() {
let values = [];
for (let i = 0; i < this._columns.length; ++i) {
values.push(this._columns[i].header);
}
return this.formatRowText(values);
}
formatRowText(cellValues) {
let result = "";
for (let i = 0; i < cellValues.length; ++i) {
if (i) result += " ";
let value = String(cellValues[i]);
let columnInfo = this._columns[i];
if (columnInfo.align == "center") {
let lefted = this._pad(value, columnInfo.width, true);
let halfPad = ((lefted.length - value.length) / 2) | 0;
result += lefted.substr(halfPad) + lefted.substr(0, halfPad);
} else {
result += this._pad(
value, columnInfo.width, columnInfo.align == "right"
);
}
}
return result;
}
_pad(text, width, left) {
let padding = "";
while (width --> text.length) padding += " ";
if (left) {
return padding + text;
}
return text + padding;
}
}
/// </script>
/// <script>
/// import { BrowserHandler } from "./BrowserHandler.js";
/// import { NodeJSHandler } from "./NodeJSHandler.js";
/// import { System } from "./System.js";
void function main() {
if (!system) var system = System.create(BrowserHandler);
if (!system) var system = System.create(NodeJSHandler);
if (!system) throw new System.ShuntingError("unknown environment");
system.run();
} ();
/// </script>