test262-stream
Version:
A Node.js API for traversing the Test262 test suite
188 lines (163 loc) • 6.88 kB
JavaScript
'use strict';
const fs = require('fs');
const path = require('path');
const Readable = require('stream').Readable;
const klaw = require('klaw');
const compile = require('./compile');
const paths = Symbol('paths');
const compileOpts = Symbol('includesDir');
const fileStream = Symbol('fileStream');
const onFile = Symbol('onFile');
const pendingOps = Symbol('pendingOps');
const fileStreamDone = Symbol('fileStreamDone');
const acceptVersion = Symbol('acceptVersion');
const supportedVersion = /^[12345]\./;
const fixturePattern = /_FIXTURE/;
/**
* A Node.js readable stream that emits an object value for every test in a
* given filesystem directory. This object has the following properties:
*
* - file {string} - the path to the file from which the test was derived,
* relative to the provided Test262 directory
* - contents {string} - the complete source text for the test; this contains
* any "includes" files specified in the frontmatter,
* "prelude" content if specified (see below), and any
* "scenario" transformations
* - attrs {object} - an object representation of the metadata declared in the
* test's YAML-formatted "frontmatter" section
* - copyright {string} - the licensing information included within the test
* (if any)
* - scenario {string} - name describing how the source file was interpreted to
* create the test
* - insertionIndex {number} - numeric offset within the `contents` string at
* which one or more statements may be inserted
* without necessarily invalidating the test
*
* @param {string} test262Dir - filesystem path to a directory containing
* Test262
* @param {object} [options]
* @param {string} [options.includesDir] - directory from which to load
* "includes" files (defaults to the
* appropriate subdirectory of the
* provided `test262Dir`
* @param {Array<string>} [options.paths] - file system paths refining the set
* of tests that should be produced;
* only tests whose source file
* matches one of these values (in the
* case of file paths) or is contained
* by one of these paths (in the case
* of directory paths) will be
* created; all paths are interpreted
* relative to the root of the
* provided `test262Dir`
* @param {string} [options.prelude] - string contents to inject into each
* test that does not carry the `raw`
* metadata flag; defaults to the empty
* string (e.g. no injection)
* @param {boolean} [options.omitRuntime] - flag to disable the insertion of
* code necessary to execute the test
* (e.g. assertion functions and
* "include" files); defaults to
* `false`
* @param {string} [options.acceptVersion] - by default, this stream will emit
* an error if the provided version
* of Test262 is not supported; this
* behavior may be disabled by
* providing a value of the expected
* version. Use of this option may
* cause the stream to emit invalid
* tests; consider updating the
* library instead.
*
* @returns {object} readable stream
*/
module.exports = class TestStream extends Readable {
constructor(test262Dir, options) {
options = Object.assign({}, options);
options.objectMode = true;
super(options);
this[test262Dir] = test262Dir;
this[paths] = (options.paths || ['test'])
.map(relativePath => path.join(test262Dir, relativePath));
this[compileOpts] = {
test262Dir,
includesDir: options.includesDir,
omitRuntime: !!options.omitRuntime
};
this[fileStream] = null;
this[pendingOps] = 0;
this[fileStreamDone] = false;
this[acceptVersion] = options.acceptVersion;
}
_read() {
if (this[fileStream]) {
return;
}
const packagePath = path.join(this[compileOpts].test262Dir, 'package.json');
fs.readFile(packagePath, 'utf-8', (error, contents) => {
if (error) {
if (error.code === 'ENOENT') {
this.traverse();
return;
}
this.emit('error', error);
return;
}
let packageData;
try {
packageData = JSON.parse(contents);
} catch (error) {
this.emit('error', error);
return;
}
const version = packageData.version;
if (!supportedVersion.test(version) && this[acceptVersion] !== version) {
this.emit(
'error', new Error(`Unsupported version of Test262: '${version}'`)
);
return;
}
this.traverse();
});
}
traverse() {
this[fileStream] = klaw(this[paths].shift());
this[fileStream].on('data', (item) => this[onFile](item));
this[fileStream].on('error', (error) => this.emit('error', error));
this[fileStream].on('end', () => {
if (this[paths].length) {
this.traverse();
} else {
this[fileStreamDone] = true;
// If the final file visited is not a test and compilation for all
// other tests has completed, the stream should signal completion.
if (this[pendingOps] === 0) {
this.push(null);
}
}
});
}
[onFile](item) {
if (!item.stats.isFile()) {
return;
}
if (path.basename(item.path)[0] === '.') {
return;
}
if (fixturePattern.test(item.path)) {
return;
}
this[pendingOps] += 1;
compile(item.path, this[compileOpts], (err, tests) => {
this[pendingOps] -= 1;
if (err) {
this.emit('error', err);
return;
}
tests.forEach((test) => this.push(test));
if (this[pendingOps] === 0 && this[fileStreamDone]) {
this.push(null);
}
});
}
};