Add page to Developer Docs on Render Tests (#3377)

This commit is contained in:
Bart Louwers
2025-04-11 17:18:56 +02:00
committed by GitHub
parent b862d6f149
commit 97490cb3c1
25 changed files with 124 additions and 27244 deletions

View File

@@ -1,150 +0,0 @@
These integration tests verify the correctness and consistency of [MapLibre Native](https://github.com/maplibre/maplibre-native) rendering.
## Organization
Tests are contained in a directory tree, generally organized by [style specification](https://maplibre.org/maplibre-style-spec/)
property: `background-color`, `line-width`, etc., with a second level of directories below that for individual tests. For example, the test for specifying a literal `circle-radius` value lives in [`test/integration/render-tests/circle-radius/literal/`](https://github.com/mapbox/mapbox-gl-js/tree/master/test/integration/render-tests/circle-radius/literal).
Within a leaf directory is a `style.json` file (e.g. [`circle-radius/literal/style.json`](https://github.com/mapbox/mapbox-gl-js/blob/master/test/integration/render-tests/circle-radius/literal/style.json)), which contains the minimal style needed for the given test case. The style can specify the map size, center, bearing, and pitch, and additional test metadata (e.g. output image dimensions).
The expected output for a given test case is in `expected.png`, e.g. [`circle-radius/literal/expected.png`](https://github.com/mapbox/mapbox-gl-js/blob/master/test/integration/render-tests/circle-radius/literal/expected.png).
Supporting files -- glyphs, sprites, and tiles -- live in their own respective subdirectories at the top level. The test
harness sets up the environment such that requests for these resources are directed to the correct location.
The contents of vector tile fixtures can be read using the [`vt2geojson`](https://github.com/mapbox/vt2geojson) tool (see below).
## Running tests
To run the entire integration test suite (both render or query tests), from within the maplibre-native directory on Linux run the command:
```
./build/mbgl-render-test-runner --manifestPath metrics/linux-clang8-release-style.json
```
To run only the render/query tests:
```
npm run test-render
```
### Running specific tests
To run a subset of tests or an individual test, you can pass a specific subdirectory to the `test-render` script. For example, to run all the tests for a given property, e.g. `circle-radius`:
```
$ npm run test-render circle-radius
...
* passed circle-radius/antimeridian
* passed circle-radius/default
* passed circle-radius/function
* passed circle-radius/literal
* passed circle-radius/property-function
* passed circle-radius/zoom-and-property-function
6 passed (100.0%)
Results at: ./test/integration/render-tests/index.html
Done in 2.71s.
```
Or to run a single test:
```
$ npm run test-render circle-radius/literal
...
* passed circle-radius/literal
1 passed (100.0%)
Results at: ./test/integration/render-tests/index.html
Done in 2.32s.
```
### Viewing test results
During a test run, the test harness will use GL-JS to create an `actual.png` image from the given `style.json`, and will then use [pixelmatch](https://github.com/mapbox/pixelmatch) to compare that image to `expected.png`, generating a `diff.png` highlighting the mismatching pixels (if any) in red.
After the test(s) have run, you can view the results graphically by opening the `index.html` file generated by the harness:
```
open ./test/integration/render-tests/index.html
```
or
```
open ./test/integration/query-tests/index.html
```
## Running tests in the browser
Query tests can be run in the browser, the server for serving up the test page and test fixtures starts when you run
```
npm run start
```
OR
```
npm run start-debug
```
If you want to run only the test server run:
```
npm run watch-query
```
Then open the following url in the browser of your choice to start running the tests.
```
http://localhost:7357/
```
### Running specific tests
A filter can be specified by using the `filter` query param in the url. E.g, adding
```
?filter=circle-pitch
```
to the end of the url will only run the tests that contain `circle-pitch` in the name.
### Build Notifications
The terminal window can be very noisy with both the build and the test servers running in the same session.
So the server uses platform notifications to inform when the build has finished. If this behaviour is annoying, it can be disabled by setting the following env-var
```
DISABLE_BUILD_NOTIFICATIONS=true
```
## Writing new tests
To add a new render test:
1. Create a new directory `test/integration/render-tests/<property-name>/<new-test-name>`
2. Create a new `style.json` file within that directory, specifying the map to load. Feel free to copy & modify one of the existing `style.json` files from the `render-tests` subdirectories.
3. Generate an `expected.png` image from the given style by running the new test with the `UPDATE` flag enabled:
```
$ UPDATE=1 npm run test-render <property-name>/<new-test-name>
```
The test will appear to fail, but you'll now see a new `expected.png` in the test directory.
4. Manually inspect `expected.png` to verify it looks as expected, and optionally run the test again without the update flag (`npm run test-render <property-name>/<new-test-name>`) to watch it pass (enjoy that dopamine kick!)
5. Commit the new `style.json` and `expected.png` :rocket:
## Reading Vector Tile Fixtures
Install `vt2geojson`, a command line utility which turns vector tiles into geojson, and `harp`, a simple file server.
```
npm install -g vt2geojson harp
```
Start a static file server
```
harp server .
```
Read the contents of an entire vector tile
```
vt2geojson -z 14 -y 8803 -x 5374 http://localhost:9000/tiles/14-8803-5374.mvt
```
Read the contents of a particular layer in a vector tile
```
vt2geojson --layer poi_label -z 14 -y 8803 -x 5374 http://localhost:9000/tiles/14-8803-5374.mvt
```

View File

@@ -1,123 +0,0 @@
class NullIsland {
constructor() {
this.id = 'null-island';
this.type = 'custom';
this.renderingMode = '2d';
}
onAdd(map, gl) {
const vertexSource = `
uniform mat4 u_matrix;
void main() {
gl_Position = u_matrix * vec4(0.5, 0.5, 0.0, 1.0);
gl_PointSize = 20.0;
}`;
const fragmentSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}`;
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexSource);
gl.compileShader(vertexShader);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentSource);
gl.compileShader(fragmentShader);
this.program = gl.createProgram();
gl.attachShader(this.program, vertexShader);
gl.attachShader(this.program, fragmentShader);
gl.linkProgram(this.program);
}
render(gl, matrix) {
gl.useProgram(this.program);
gl.uniformMatrix4fv(gl.getUniformLocation(this.program, "u_matrix"), false, matrix);
gl.drawArrays(gl.POINTS, 0, 1);
}
}
class Tent3D {
constructor() {
this.id = 'tent-3d';
this.type = 'custom';
this.renderingMode = '3d';
}
onAdd(map, gl) {
const vertexSource = `
attribute vec3 aPos;
uniform mat4 uMatrix;
void main() {
gl_Position = uMatrix * vec4(aPos, 1.0);
}
`;
const fragmentSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexSource);
gl.compileShader(vertexShader);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentSource);
gl.compileShader(fragmentShader);
this.program = gl.createProgram();
gl.attachShader(this.program, vertexShader);
gl.attachShader(this.program, fragmentShader);
gl.linkProgram(this.program);
gl.validateProgram(this.program);
this.program.aPos = gl.getAttribLocation(this.program, "aPos");
this.program.uMatrix = gl.getUniformLocation(this.program, "uMatrix");
const x = 0.5 - 0.015;
const y = 0.5 - 0.01;
const z = 0.01;
const d = 0.01;
const vertexArray = new Float32Array([
x, y, 0,
x + d, y, 0,
x, y + d, z,
x + d, y + d, z,
x, y + d + d, 0,
x + d, y + d + d, 0]);
const indexArray = new Uint16Array([
0, 1, 2,
1, 2, 3,
2, 3, 4,
3, 4, 5
]);
this.vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertexArray, gl.STATIC_DRAW);
this.indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexArray, gl.STATIC_DRAW);
}
render(gl, matrix) {
gl.useProgram(this.program);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
gl.enableVertexAttribArray(this.program.a_pos);
gl.vertexAttribPointer(this.program.aPos, 3, gl.FLOAT, false, 0, 0);
gl.uniformMatrix4fv(this.program.uMatrix, false, matrix);
gl.drawElements(gl.TRIANGLES, 12, gl.UNSIGNED_SHORT, 0);
}
}
export default {
"tent-3d": Tent3D,
"null-island": NullIsland
};

View File

View File

@@ -1,194 +0,0 @@
import path from 'path';
import * as diff from 'diff';
import fs from 'fs';
import harness from './harness';
import compactStringify from 'json-stringify-pretty-compact';
// we have to handle this edge case here because we have test fixtures for this
// edge case, and we don't want UPDATE=1 to mess with them
function stringify(v) {
let s = compactStringify(v);
// http://timelessrepo.com/json-isnt-a-javascript-subset
if (s.indexOf('\u2028') >= 0) {
s = s.replace(/\u2028/g, '\\u2028');
}
if (s.indexOf('\u2029') >= 0) {
s = s.replace(/\u2029/g, '\\u2029');
}
return s;
}
const decimalSigFigs = 6;
function stripPrecision(x) {
// Intended for test output serialization:
// strips down to 6 decimal sigfigs but stops at decimal point
if (typeof x === 'number') {
if (x === 0) { return x; }
const multiplier = Math.pow(10,
Math.max(0,
decimalSigFigs - Math.ceil(Math.log10(Math.abs(x)))));
// We strip precision twice in a row here to avoid cases where
// stripping an already stripped number will modify its value
// due to bad floating point precision luck
// eg `Math.floor(8.16598 * 100000) / 100000` -> 8.16597
const firstStrip = Math.floor(x * multiplier) / multiplier;
return Math.floor(firstStrip * multiplier) / multiplier;
} else if (typeof x !== 'object') {
return x;
} else if (Array.isArray(x)) {
return x.map(stripPrecision);
} else {
const stripped = {};
for (const key of Object.keys(x)) {
stripped[key] = stripPrecision(x[key]);
}
return stripped;
}
}
function deepEqual(a, b) {
if (typeof a !== typeof b)
return false;
if (typeof a === 'number') {
return stripPrecision(a) === stripPrecision(b);
}
if (a === null || b === null || typeof a !== 'object')
return a === b;
const ka = Object.keys(a);
const kb = Object.keys(b);
if (ka.length !== kb.length)
return false;
ka.sort();
kb.sort();
for (let i = 0; i < ka.length; i++)
if (ka[i] !== kb[i] || !deepEqual(a[ka[i]], b[ka[i]]))
return false;
return true;
}
/**
* Run the expression suite.
*
* @param {string} implementation - identify the implementation under test; used to
* deal with implementation-specific test exclusions and fudge-factors
* @param {Object} options
* @param {Array<string>} [options.tests] - array of test names to run; tests not in the array will be skipped
* @param {Array<string>} [options.ignores] - array of test names to ignore.
* @param {} runExpressionTest - a function that runs a single expression test fixture
* @returns {undefined} terminates the process when testing is complete
*/
export function run(implementation, options, runExpressionTest) {
const directory = path.join(__dirname, '../expression-tests');
options.fixtureFilename = 'test.json';
harness(directory, implementation, options, (fixture, params, done) => {
try {
const result = runExpressionTest(fixture, params);
const dir = path.join(directory, params.id);
if (process.env.UPDATE) {
fixture.expected = {
compiled: result.compiled,
outputs: stripPrecision(result.outputs),
serialized: result.serialized
};
delete fixture.metadata;
fs.writeFile(path.join(dir, 'test.json'), `${stringify(fixture, null, 2)}\n`, done);
return;
}
const expected = fixture.expected;
const compileOk = deepEqual(result.compiled, expected.compiled);
const evalOk = compileOk && deepEqual(result.outputs, expected.outputs);
let recompileOk = true;
let roundTripOk = true;
let serializationOk = true;
if (expected.compiled.result !== 'error') {
serializationOk = compileOk && deepEqual(expected.serialized, result.serialized);
recompileOk = compileOk && deepEqual(result.recompiled, expected.compiled);
roundTripOk = recompileOk && deepEqual(result.roundTripOutputs, expected.outputs);
}
params.ok = compileOk && evalOk && recompileOk && roundTripOk && serializationOk;
const diffOutput = {
text: '',
html: ''
};
const diffJson = (label, expectedJson, actualJson) => {
let text = '';
let html = '';
diff.diffJson(expectedJson, actualJson)
.forEach((hunk) => {
if (hunk.added) {
text += `+ ${hunk.value}`;
html += `<ins> ${hunk.value}</ins>`;
} else if (hunk.removed) {
text += `- ${hunk.value}`;
html += `<del> ${hunk.value}</del>`;
} else {
text += ` ${hunk.value}`;
html += `<span> ${hunk.value}</span>`;
}
});
if (text) {
diffOutput.text += `${label}\n${text}`;
diffOutput.html += `<h3>${label}</h3>\n${html}`;
}
};
if (!compileOk) {
diffJson('Compiled', expected.compiled, result.compiled);
}
if (compileOk && !serializationOk) {
diffJson('Serialized', expected.serialized, result.serialized);
}
if (compileOk && !recompileOk) {
diffJson('Serialized and re-compiled', expected.compiled, result.recompiled);
}
const diffOutputs = (testOutputs) => {
return expected.outputs.map((expectedOutput, i) => {
if (!deepEqual(expectedOutput, testOutputs[i])) {
return `f(${JSON.stringify(fixture.inputs[i])})\nExpected: ${JSON.stringify(expectedOutput)}\nActual: ${JSON.stringify(testOutputs[i])}`;
}
return false;
})
.filter(Boolean)
.join('\n');
};
if (compileOk && !evalOk) {
const differences = `Original\n${diffOutputs(result.outputs)}\n`;
diffOutput.text += differences;
diffOutput.html += differences;
}
if (recompileOk && !roundTripOk) {
const differences = `\nRoundtripped through serialize()\n${diffOutputs(result.roundTripOutputs)}\n`;
diffOutput.text += differences;
diffOutput.html += differences;
}
params.difference = diffOutput.html;
if (diffOutput.text) { console.log(diffOutput.text); }
params.expression = compactStringify(fixture.expression);
params.serialized = compactStringify(result.serialized);
done();
} catch (e) {
done(e);
}
});
}

View File

@@ -1,120 +0,0 @@
/* eslint-disable import/no-commonjs */
const path = require('path');
const fs = require('fs');
const glob = require('glob');
const localizeURLs = require('./localize-urls');
const OUTPUT_FILE = 'fixtures.json';
exports.generateFixtureJson = generateFixtureJson;
exports.getAllFixtureGlobs = getAllFixtureGlobs;
/**
* Analyzes the contents of the specified `path.join(rootDirectory, suiteDirectory)`, and inlines
* the contents into a single json file which can then be imported and built into a bundle
* to be shipped to the browser.
*
* @param {string} rootDirectory
* @param {string} suiteDirectory
* @param {boolean} includeImages
*/
function generateFixtureJson(rootDirectory, suiteDirectory, outputDirectory = 'test/integration/dist', includeImages = false) {
const globs = getAllFixtureGlobs(rootDirectory, suiteDirectory);
const jsonPaths = globs[0];
const imagePaths = globs[1];
//Extract the filedata into a flat dictionary
const allFiles = {};
let allPaths = glob.sync(jsonPaths);
if (includeImages) {
allPaths = allPaths.concat(glob.sync(imagePaths));
}
//A Set that stores test names that are malformed so they can be removed later
const malformedTests = {};
for (const fixturePath of allPaths) {
const testName = path.dirname(fixturePath);
const fileName = path.basename(fixturePath);
const extension = path.extname(fixturePath);
try {
if (extension === '.json') {
let json = parseJsonFromFile(fixturePath);
//Special case for style json which needs some preprocessing
if (fileName === 'style.json') {
json = processStyle(testName, json);
}
allFiles[fixturePath] = json;
} else if (extension === '.png') {
allFiles[fixturePath] = pngToBase64Str(fixturePath);
} else {
throw new Error(`${extension} is incompatible , file path ${fixturePath}`);
}
} catch (e) {
console.log(`Error parsing file: ${fixturePath}`);
malformedTests[testName] = true;
}
}
// Re-nest by directory path, each directory path defines a testcase.
const result = {};
for (const fullPath in allFiles) {
const testName = path.dirname(fullPath).replace(rootDirectory, '');
//Skip if test is malformed
if (malformedTests[testName]) { continue; }
//Lazily initaialize an object to store each file wihin a particular testName
if (result[testName] == null) {
result[testName] = {};
}
//Trim extension from filename
const fileName = path.basename(fullPath, path.extname(fullPath));
result[testName][fileName] = allFiles[fullPath];
}
const outputStr = JSON.stringify(result, null, 4);
const outputPath = path.join(outputDirectory, OUTPUT_FILE);
return new Promise((resolve, reject) => {
fs.writeFile(outputPath, outputStr, {encoding: 'utf8'}, (err) => {
if (err) { reject(err); }
resolve();
});
});
}
function getAllFixtureGlobs(rootDirectory, suiteDirectory) {
const basePath = path.join(rootDirectory, suiteDirectory);
const jsonPaths = path.join(basePath, '/**/*.json');
const imagePaths = path.join(basePath, '/**/*.png');
return [jsonPaths, imagePaths];
}
function parseJsonFromFile(filePath) {
return JSON.parse(fs.readFileSync(filePath, {encoding: 'utf8'}));
}
function pngToBase64Str(filePath) {
return fs.readFileSync(filePath).toString('base64');
}
function processStyle(testName, style) {
const clone = JSON.parse(JSON.stringify(style));
// 7357 is testem's default port
localizeURLs(clone, 7357);
clone.metadata = clone.metadata || {};
clone.metadata.test = Object.assign({
testName,
width: 512,
height: 512,
pixelRatio: 1,
recycleMap: false,
allowed: 0.00015
}, clone.metadata.test);
return clone;
}

View File

@@ -1,210 +0,0 @@
/* eslint-disable no-process-exit */
import path from 'path';
import fs from 'fs';
import glob from 'glob';
import {shuffle} from 'shuffle-seed';
import {queue} from 'd3';
import colors from 'chalk';
import template from 'lodash.template';
import createServer from './server';
export default function (directory, implementation, options, run) {
const q = queue(1);
const server = createServer();
const tests = options.tests || [];
const ignores = options.ignores || {};
let sequence = glob.sync(`**/${options.fixtureFilename || 'style.json'}`, {cwd: directory})
.map(fixture => {
const id = path.dirname(fixture);
const style = require(path.join(directory, fixture));
server.localizeURLs(style);
style.metadata = style.metadata || {};
style.metadata.test = Object.assign({
id,
ignored: ignores[`${path.basename(directory)}/${id}`],
width: 512,
height: 512,
pixelRatio: 1,
recycleMap: options.recycleMap || false,
allowed: 0.00015
}, style.metadata.test);
return style;
})
.filter(style => {
const test = style.metadata.test;
if (tests.length !== 0 && !tests.some(t => test.id.indexOf(t) !== -1)) {
return false;
}
if (implementation === 'native' && process.env.BUILDTYPE !== 'Debug' && test.id.match(/^debug\//)) {
console.log(colors.gray(`* skipped ${test.id}`));
return false;
}
if (/^skip/.test(test.ignored)) {
console.log(colors.gray(`* skipped ${test.id} (${test.ignored})`));
return false;
}
return true;
});
if (options.shuffle) {
console.log(colors.white(`* shuffle seed: `) + colors.bold(`${options.seed}`));
sequence = shuffle(sequence, options.seed);
}
q.defer(server.listen);
sequence.forEach(style => {
q.defer((callback) => {
const test = style.metadata.test;
try {
run(style, test, handleResult);
} catch (error) {
handleResult(error);
}
function handleResult (error) {
if (error) {
test.error = error;
}
if (test.ignored && !test.ok) {
test.color = '#9E9E9E';
test.status = 'ignored failed';
console.log(colors.white(`* ignore ${test.id} (${test.ignored})`));
} else if (test.ignored) {
test.color = '#E8A408';
test.status = 'ignored passed';
console.log(colors.yellow(`* ignore ${test.id} (${test.ignored})`));
} else if (test.error) {
test.color = 'red';
test.status = 'errored';
console.log(colors.red(`* errored ${test.id}`));
} else if (!test.ok) {
test.color = 'red';
test.status = 'failed';
console.log(colors.red(`* failed ${test.id}`));
} else {
test.color = 'green';
test.status = 'passed';
console.log(colors.green(`* passed ${test.id}`));
}
callback(null, test);
}
});
});
q.defer(server.close);
q.awaitAll((err, results) => {
if (err) {
console.error(err);
setTimeout(() => { process.exit(-1); }, 0);
return;
}
const tests = results.slice(1, -1);
if (process.env.UPDATE) {
console.log(`Updated ${tests.length} tests.`);
process.exit(0);
}
let passedCount = 0,
ignoreCount = 0,
ignorePassCount = 0,
failedCount = 0,
erroredCount = 0;
tests.forEach((test) => {
if (test.ignored && !test.ok) {
ignoreCount++;
} else if (test.ignored) {
ignorePassCount++;
} else if (test.error) {
erroredCount++;
} else if (!test.ok) {
failedCount++;
} else {
passedCount++;
}
});
const totalCount = passedCount + ignorePassCount + ignoreCount + failedCount + erroredCount;
if (passedCount > 0) {
console.log(colors.green('%d passed (%s%)'),
passedCount, (100 * passedCount / totalCount).toFixed(1));
}
if (ignorePassCount > 0) {
console.log(colors.yellow('%d passed but were ignored (%s%)'),
ignorePassCount, (100 * ignorePassCount / totalCount).toFixed(1));
}
if (ignoreCount > 0) {
console.log(colors.white('%d ignored (%s%)'),
ignoreCount, (100 * ignoreCount / totalCount).toFixed(1));
}
if (failedCount > 0) {
console.log(colors.red('%d failed (%s%)'),
failedCount, (100 * failedCount / totalCount).toFixed(1));
}
if (erroredCount > 0) {
console.log(colors.red('%d errored (%s%)'),
erroredCount, (100 * erroredCount / totalCount).toFixed(1));
}
const resultsTemplate = template(fs.readFileSync(path.join(__dirname, '..', 'results.html.tmpl'), 'utf8'));
const itemTemplate = template(fs.readFileSync(path.join(directory, 'result_item.html.tmpl'), 'utf8'));
const stats = {};
for (const test of tests) {
stats[test.status] = (stats[test.status] || 0) + 1;
}
const unsuccessful = tests.filter(test =>
test.status === 'failed' || test.status === 'errored');
const resultsShell = resultsTemplate({unsuccessful, tests, stats, shuffle: options.shuffle, seed: options.seed})
.split('<!-- results go here -->');
const p = path.join(directory, options.recycleMap ? 'index-recycle-map.html' : 'index.html');
const out = fs.createWriteStream(p);
const q = queue(1);
q.defer(write, out, resultsShell[0]);
for (const test of tests) {
q.defer(write, out, itemTemplate({r: test, hasFailedTests: unsuccessful.length > 0}));
}
q.defer(write, out, resultsShell[1]);
q.await(() => {
out.end();
out.on('close', () => {
console.log(`Results at: ${p}`);
process.exit((failedCount + erroredCount) === 0 ? 0 : 1);
});
});
});
}
function write(stream, data, cb) {
if (!stream.write(data)) {
stream.once('drain', cb);
} else {
process.nextTick(cb);
}
}

View File

@@ -1,37 +0,0 @@
import diff from 'diff';
export function generateDiffLog(expected, actual) {
return diff.diffJson(expected, actual).map((hunk) => {
if (hunk.added) {
return `+ ${hunk.value}`;
} else if (hunk.removed) {
return `- ${hunk.value}`;
} else {
return ` ${hunk.value}`;
}
}).join('');
}
export function deepEqual(a, b) {
if (typeof a !== typeof b)
return false;
if (typeof a === 'number')
return Math.abs(a - b) < 1e-10;
if (a === null || typeof a !== 'object')
return a === b;
const ka = Object.keys(a);
const kb = Object.keys(b);
if (ka.length !== kb.length)
return false;
ka.sort();
kb.sort();
for (let i = 0; i < ka.length; i++)
if (ka[i] !== kb[i] || !deepEqual(a[ka[i]], b[ka[i]]))
return false;
return true;
}

View File

@@ -1,102 +0,0 @@
/* eslint-disable import/no-commonjs */
const path = require('path');
const fs = require('fs');
const colors = require('chalk');
module.exports = function localizeURLs(style, port) {
localizeStyleURLs(style, port);
if (style.metadata && style.metadata.test && style.metadata.test.operations) {
style.metadata.test.operations.forEach((op) => {
if (op[0] === 'addSource') {
localizeSourceURLs(op[2], port);
} else if (op[0] === 'setStyle') {
if (typeof op[1] === 'object') {
localizeStyleURLs(op[1], port);
return;
}
let styleJSON;
try {
const relativePath = op[1].replace(/^local:\/\//, '');
if (relativePath.startsWith('mapbox-gl-styles')) {
styleJSON = fs.readFileSync(path.join(path.dirname(require.resolve('mapbox-gl-styles')), '..', relativePath));
} else {
styleJSON = fs.readFileSync(path.join(__dirname, '..', relativePath));
}
} catch (error) {
console.log(colors.blue(`* ${error}`));
return;
}
try {
styleJSON = JSON.parse(styleJSON);
} catch (error) {
console.log(colors.blue(`* Error while parsing ${op[1]}: ${error}`));
return;
}
localizeStyleURLs(styleJSON, port);
op[1] = styleJSON;
op[2] = {diff: false};
}
});
}
};
function localizeURL(url, port) {
return url.replace(/^local:\/\//, `http://localhost:${port}/`);
}
function localizeMapboxSpriteURL(url, port) {
return url.replace(/^mapbox:\/\//, `http://localhost:${port}/`);
}
function localizeMapboxFontsURL(url, port) {
return url.replace(/^mapbox:\/\/fonts/, `http://localhost:${port}/glyphs`);
}
function localizeMapboxTilesURL(url, port) {
return url.replace(/^mapbox:\/\//, `http://localhost:${port}/tiles/`);
}
function localizeMapboxTilesetURL(url, port) {
return url.replace(/^mapbox:\/\//, `http://localhost:${port}/tilesets/`);
}
function localizeSourceURLs(source, port) {
for (const tile in source.tiles) {
source.tiles[tile] = localizeMapboxTilesURL(source.tiles[tile], port);
source.tiles[tile] = localizeURL(source.tiles[tile], port);
}
if (source.urls) {
source.urls = source.urls.map((url) => localizeMapboxTilesetURL(url, port));
source.urls = source.urls.map((url) => localizeURL(url, port));
}
if (source.url) {
source.url = localizeMapboxTilesetURL(source.url, port);
source.url = localizeURL(source.url, port);
}
if (source.data && typeof source.data == 'string') {
source.data = localizeURL(source.data, port);
}
}
function localizeStyleURLs (style, port) {
for (const source in style.sources) {
localizeSourceURLs(style.sources[source], port);
}
if (style.sprite) {
style.sprite = localizeMapboxSpriteURL(style.sprite, port);
style.sprite = localizeURL(style.sprite, port);
}
if (style.glyphs) {
style.glyphs = localizeMapboxFontsURL(style.glyphs, port);
style.glyphs = localizeURL(style.glyphs, port);
}
}

View File

@@ -1,56 +0,0 @@
function handleOperation(map, operations, opIndex, doneCb) {
const operation = operations[opIndex];
const opName = operation[0];
//Delegate to special handler if one is available
if (opName in operationHandlers) {
operationHandlers[opName](map, operation.slice(1), () => {
doneCb(opIndex);
});
} else {
map[opName](...operation.slice(1));
doneCb(opIndex);
}
}
export const operationHandlers = {
wait(map, params, doneCb) {
const wait = function() {
if (map.loaded()) {
doneCb();
} else {
map.once('render', wait);
}
};
wait();
},
idle(map, params, doneCb) {
const idle = function() {
if (!map.isMoving()) {
doneCb();
} else {
map.once('render', idle);
}
};
idle();
}
};
export function applyOperations(map, operations, doneCb) {
// No operations specified, end immediately adn invoke doneCb.
if (!operations || operations.length === 0) {
doneCb();
return;
}
// Start recursive chain
const scheduleNextOperation = (lastOpIndex) => {
if (lastOpIndex === operations.length - 1) {
// Stop recusive chain when at the end of the operations
doneCb();
return;
}
handleOperation(map, operations, ++lastOpIndex, scheduleNextOperation);
};
scheduleNextOperation(-1);
}

View File

@@ -1,86 +0,0 @@
/* eslint-env browser */
/* global tape:readonly, mapboxgl:readonly */
/* eslint-disable import/no-unresolved */
// fixtures.json is automatically generated before this file gets built
// refer testem.js#before_tests()
import fixtures from '../dist/fixtures.json';
import ignores from '../../ignores.json';
import {applyOperations} from './operation-handlers';
import {deepEqual, generateDiffLog} from './json-diff';
for (const testName in fixtures) {
if (testName in ignores) {
tape.skip(testName, testFunc);
} else {
tape(testName, {timeout: 20000}, testFunc);
}
}
function testFunc(t) {
// This needs to be read from the `t` object because this function runs async in a closure.
const currentTestName = t.name;
const style = fixtures[currentTestName].style;
const expected = fixtures[currentTestName].expected;
const options = style.metadata.test;
const skipLayerDelete = style.metadata.skipLayerDelete;
window.devicePixelRatio = options.pixelRatio;
//1. Create and position the container, floating at the bottom right
const container = document.createElement('div');
container.style.position = 'fixed';
container.style.bottom = '10px';
container.style.right = '10px';
container.style.width = `${options.width}px`;
container.style.height = `${options.height}px`;
document.body.appendChild(container);
//2. Initialize the Map
const map = new mapboxgl.Map({
container,
style,
classes: options.classes,
interactive: false,
attributionControl: false,
preserveDrawingBuffer: true,
axonometric: options.axonometric || false,
skew: options.skew || [0, 0],
fadeDuration: options.fadeDuration || 0,
localIdeographFontFamily: options.localIdeographFontFamily || false,
crossSourceCollisions: typeof options.crossSourceCollisions === "undefined" ? true : options.crossSourceCollisions
});
map.repaint = true;
map.once('load', () => {
//3. Run the operations on the map
applyOperations(map, options.operations, () => {
//4. Perform query operation and compare results from expected values
const results = options.queryGeometry ?
map.queryRenderedFeatures(options.queryGeometry, options.queryOptions || {}) :
[];
const actual = results.map((feature) => {
const featureJson = JSON.parse(JSON.stringify(feature.toJSON()));
if (!skipLayerDelete) delete featureJson.layer;
return featureJson;
});
const testMetaData = {
name: t.name,
actual: map.getCanvas().toDataURL()
};
const success = deepEqual(actual, expected);
if (success) {
t.pass(JSON.stringify(testMetaData));
} else {
testMetaData['difference'] = generateDiffLog(expected, actual);
t.fail(JSON.stringify(testMetaData));
}
//Cleanup WebGL context
map.remove();
delete map.painter.context.gl;
document.body.removeChild(container);
t.end();
});
});
}

View File

@@ -1,153 +0,0 @@
import path from 'path';
import fs from 'fs';
import * as diff from 'diff';
import {PNG} from 'pngjs';
import harness from './harness';
function deepEqual(a, b) {
if (typeof a !== typeof b)
return false;
if (typeof a === 'number')
return Math.abs(a - b) < 1e-10;
if (a === null || typeof a !== 'object')
return a === b;
const ka = Object.keys(a);
const kb = Object.keys(b);
if (ka.length !== kb.length)
return false;
ka.sort();
kb.sort();
for (let i = 0; i < ka.length; i++)
if (ka[i] !== kb[i] || !deepEqual(a[ka[i]], b[ka[i]]))
return false;
return true;
}
/**
* Run the query suite.
*
* @param {string} implementation - identify the implementation under test; used to
* deal with implementation-specific test exclusions and fudge-factors
* @param {Object} options
* @param {Array<string>} [options.tests] - array of test names to run; tests not in the
* array will be skipped
* @param {queryFn} query - a function that performs the query
* @returns {undefined} terminates the process when testing is complete
*/
export function run(implementation, options, query) {
const directory = path.join(__dirname, '../query-tests');
harness(directory, implementation, options, (style, params, done) => {
query(style, params, (err, data, results) => {
if (err) return done(err);
const dir = path.join(directory, params.id);
if (process.env.UPDATE) {
fs.writeFile(path.join(dir, 'expected.json'), JSON.stringify(results, null, 2), done);
return;
}
const expected = require(path.join(dir, 'expected.json'));
params.ok = deepEqual(results, expected);
if (!params.ok) {
const msg = diff.diffJson(expected, results)
.map((hunk) => {
if (hunk.added) {
return `+ ${hunk.value}`;
} else if (hunk.removed) {
return `- ${hunk.value}`;
} else {
return ` ${hunk.value}`;
}
})
.join('');
params.difference = msg;
console.log(msg);
}
const width = params.width * params.pixelRatio;
const height = params.height * params.pixelRatio;
const color = [255, 0, 0, 255];
function scaleByPixelRatio(x) {
return x * params.pixelRatio;
}
if (!Array.isArray(params.queryGeometry[0])) {
const p = params.queryGeometry.map(scaleByPixelRatio);
const d = 30;
drawAxisAlignedLine([p[0] - d, p[1]], [p[0] + d, p[1]], data, width, height, color);
drawAxisAlignedLine([p[0], p[1] - d], [p[0], p[1] + d], data, width, height, color);
} else {
const a = params.queryGeometry[0].map(scaleByPixelRatio);
const b = params.queryGeometry[1].map(scaleByPixelRatio);
drawAxisAlignedLine([a[0], a[1]], [a[0], b[1]], data, width, height, color);
drawAxisAlignedLine([a[0], b[1]], [b[0], b[1]], data, width, height, color);
drawAxisAlignedLine([b[0], b[1]], [b[0], a[1]], data, width, height, color);
drawAxisAlignedLine([b[0], a[1]], [a[0], a[1]], data, width, height, color);
}
const actualJSON = path.join(dir, 'actual.json');
fs.writeFile(actualJSON, JSON.stringify(results, null, 2), () => {});
const actualPNG = path.join(dir, 'actual.png');
const png = new PNG({
width: params.width * params.pixelRatio,
height: params.height * params.pixelRatio
});
png.data = data;
png.pack()
.pipe(fs.createWriteStream(actualPNG))
.on('finish', () => {
params.actual = fs.readFileSync(actualPNG).toString('base64');
done();
});
});
});
}
function drawAxisAlignedLine(a, b, pixels, width, height, color) {
const fromX = clamp(Math.min(a[0], b[0]), 0, width);
const toX = clamp(Math.max(a[0], b[0]), 0, width);
const fromY = clamp(Math.min(a[1], b[1]), 0, height);
const toY = clamp(Math.max(a[1], b[1]), 0, height);
let index;
if (fromX === toX) {
for (let y = fromY; y <= toY; y++) {
index = getIndex(fromX, y);
pixels[index + 0] = color[0];
pixels[index + 1] = color[1];
pixels[index + 2] = color[2];
pixels[index + 3] = color[3];
}
} else {
for (let x = fromX; x <= toX; x++) {
index = getIndex(x, fromY);
pixels[index + 0] = color[0];
pixels[index + 1] = color[1];
pixels[index + 2] = color[2];
pixels[index + 3] = color[3];
}
}
function getIndex(x, y) {
return (y * width + x) * 4;
}
}
function clamp(x, a, b) {
return Math.max(a, Math.min(b, x));
}

View File

@@ -1,183 +0,0 @@
import path from 'path';
import fs from 'fs';
import {PNG} from 'pngjs';
import harness from './harness';
import pixelmatch from 'pixelmatch';
import * as glob from 'glob';
/**
* Run the render test suite, compute differences to expected values (making exceptions based on
* implementation vagaries), print results to standard output, write test artifacts to the
* filesystem (optionally updating expected results), and exit the process with a success or
* failure code.
*
* Caller must supply a `render` function that does the actual rendering and passes the raw image
* result on to the `render` function's callback.
*
* A local server is launched that is capable of serving requests for the source, sprite,
* font, and tile assets needed by the tests, and the URLs within the test styles are
* rewritten to point to that server.
*
* As the tests run, results are printed to standard output, and test artifacts are written
* to the filesystem. If the environment variable `UPDATE` is set, the expected artifacts are
* updated in place based on the test rendering.
*
* If all the tests are successful, this function exits the process with exit code 0. Otherwise
* it exits with 1. If an unexpected error occurs, it exits with -1.
*
* @param {string} implementation - identify the implementation under test; used to
* deal with implementation-specific test exclusions and fudge-factors
* @param {Object<string>} [ignores] - map of test names to disable. A key is the relative
* path to a test directory, e.g. `"render-tests/background-color/default"`. A value is a string
* that by convention links to an issue that explains why the test is currently disabled. By default,
* disabled tests will be run, but not fail the test run if the result does not match the expected
* result. If the value begins with "skip", the test will not be run at all -- use this for tests
* that would crash the test harness entirely if they were run.
* @param {renderFn} render - a function that performs the rendering
* @returns {undefined} terminates the process when testing is complete
*/
export function run(implementation, ignores, render) {
const options = {ignores, tests:[], shuffle:false, recycleMap:false, seed:makeHash()};
// https://stackoverflow.com/a/1349426/229714
function makeHash() {
const array = [];
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 10; ++i)
array.push(possible.charAt(Math.floor(Math.random() * possible.length)));
// join array elements without commas.
return array.join('');
}
function checkParameter(param) {
const index = options.tests.indexOf(param);
if (index === -1)
return false;
options.tests.splice(index, 1);
return true;
}
function checkValueParameter(defaultValue, param) {
const index = options.tests.findIndex((elem) => { return String(elem).startsWith(param); });
if (index === -1)
return defaultValue;
const split = String(options.tests.splice(index, 1)).split('=');
if (split.length !== 2)
return defaultValue;
return split[1];
}
if (process.argv.length > 2) {
options.tests = process.argv.slice(2).filter((value, index, self) => { return self.indexOf(value) === index; }) || [];
options.shuffle = checkParameter('--shuffle');
options.recycleMap = checkParameter('--recycle-map');
options.seed = checkValueParameter(options.seed, '--seed');
}
const directory = path.join(__dirname, '../render-tests');
harness(directory, implementation, options, (style, params, done) => {
render(style, params, (err, data) => {
if (err) return done(err);
let stats;
const dir = path.join(directory, params.id);
try {
stats = fs.statSync(dir, fs.R_OK | fs.W_OK);
if (!stats.isDirectory()) throw new Error();
} catch (e) {
fs.mkdirSync(dir);
}
const expectedPath = path.join(dir, 'expected.png');
const actualPath = path.join(dir, 'actual.png');
const diffPath = path.join(dir, 'diff.png');
const width = Math.floor(params.width * params.pixelRatio);
const height = Math.floor(params.height * params.pixelRatio);
const actualImg = new PNG({width, height});
// PNG data must be unassociated (not premultiplied)
for (let i = 0; i < data.length; i++) {
const a = data[i * 4 + 3] / 255;
if (a !== 0) {
data[i * 4 + 0] /= a;
data[i * 4 + 1] /= a;
data[i * 4 + 2] /= a;
}
}
actualImg.data = data;
// there may be multiple expected images, covering different platforms
const expectedPaths = glob.sync(path.join(dir, 'expected*.png'));
if (!process.env.UPDATE && expectedPaths.length === 0) {
throw new Error('No expected*.png files found; did you mean to run tests with UPDATE=true?');
}
if (process.env.UPDATE) {
fs.writeFileSync(expectedPath, PNG.sync.write(actualImg));
} else {
// if we have multiple expected images, we'll compare against each one and pick the one with
// the least amount of difference; this is useful for covering features that render differently
// depending on platform, i.e. heatmaps use half-float textures for improved rendering where supported
let minDiff = Infinity;
let minDiffImg, minExpectedBuf;
for (const path of expectedPaths) {
const expectedBuf = fs.readFileSync(path);
const expectedImg = PNG.sync.read(expectedBuf);
const diffImg = new PNG({width, height});
const diff = pixelmatch(
actualImg.data, expectedImg.data, diffImg.data,
width, height, {threshold: 0.1285}) / (width * height);
if (diff < minDiff) {
minDiff = diff;
minDiffImg = diffImg;
minExpectedBuf = expectedBuf;
}
}
const diffBuf = PNG.sync.write(minDiffImg, {filterType: 4});
const actualBuf = PNG.sync.write(actualImg, {filterType: 4});
fs.writeFileSync(diffPath, diffBuf);
fs.writeFileSync(actualPath, actualBuf);
params.difference = minDiff;
params.ok = minDiff <= params.allowed;
params.actual = actualBuf.toString('base64');
params.expected = minExpectedBuf.toString('base64');
params.diff = diffBuf.toString('base64');
}
done();
});
});
}
/**
* @callback renderFn
* @param {Object} style - style to render
* @param {Object} options
* @param {number} options.width - render this wide
* @param {number} options.height - render this high
* @param {number} options.pixelRatio - render with this pixel ratio
* @param {boolean} options.shuffle - shuffle tests sequence
* @param {String} options.seed - Shuffle seed
* @param {boolean} options.recycleMap - trigger map object recycling
* @param {renderCallback} callback - callback to call with the results of rendering
*/
/**
* @callback renderCallback
* @param {?Error} error
* @param {Buffer} [result] - raw RGBA image data
*/

View File

@@ -1,50 +0,0 @@
/* eslint-disable import/no-commonjs */
const path = require('path');
const fs = require('fs');
const st = require('st');
const {createServer} = require('http');
const localizeURLs = require('./localize-urls');
module.exports = function () {
const port = 2900;
const integrationMount = st({path: path.join(__dirname, '..')});
const mapboxGLStylesMount = st({path: path.dirname(require.resolve('mapbox-gl-styles')), url: 'mapbox-gl-styles'});
const mapboxMVTFixturesMount = st({path: path.dirname(require.resolve('@mapbox/mvt-fixtures')), url: 'mvt-fixtures'});
const server = createServer((req, res) => {
if (req.method === 'POST' && req.url === '/write-file') {
let body = '';
req.on('data', (data) => {
body += data;
});
req.on('end', () => {
//Write data to disk
const {filePath, data} = JSON.parse(body);
fs.writeFile(path.join(process.cwd(), filePath), data, 'base64', () => {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('ok');
});
});
}
return mapboxMVTFixturesMount(req, res, () => {
return mapboxGLStylesMount(req, res, () => {
return integrationMount(req, res);
});
});
});
return {
listen(callback) {
server.listen(port, callback);
},
close(callback) {
server.close(callback);
},
localizeURLs(style) {
return localizeURLs(style, port);
}
};
};

View File

@@ -1,10 +0,0 @@
<div class="test <%- r.status %> <%- (hasFailedTests && /passed/.test(r.status) || /ignored/.test(r.status)) ? 'hide' : '' %>">
<h2><span class="label" style="background: <%- r.color %>"><%- r.status %></span> <%- r.id %></h2>
<% if (r.status !== 'errored') { %>
<img src="data:image/png;base64,<%- r.actual %>">
<% } %>
<% if (r.error) { %><p style="color: red"><strong>Error:</strong> <%- r.error.message %></p><% } %>
<% if (r.difference) { %>
<pre><%- r.difference.trim() %></pre>
<% } %>
</div>

View File

@@ -1,73 +0,0 @@
<style>
body { font: 18px/1.2 -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; padding: 10px; }
h1 { font-size: 32px; margin-bottom: 0; }
button { vertical-align: middle; }
h2 { font-size: 24px; font-weight: normal; margin: 10px 0 10px; line-height: 1; }
img { margin: 0 10px 10px 0; border: 1px dotted #ccc; }
.stats { margin-top: 10px; }
.test { border-bottom: 1px dotted #bbb; padding-bottom: 5px; }
.tests { border-top: 1px dotted #bbb; margin-top: 10px; }
.diff { color: #777; }
.test p, .test pre { margin: 0 0 10px; }
.test pre { font-size: 14px; }
.label { color: white; font-size: 18px; padding: 2px 6px 3px; border-radius: 3px; margin-right: 3px; vertical-align: bottom; display: inline-block; }
.hide { display: none; }
</style>
<% if (unsuccessful.length) { %>
<h1 style="color: red;">
<%- unsuccessful.length %> tests failed.
<% } else { %>
<h1 style="color: green;">
All tests passed!
<% } %>
<button id='toggle-sequence'>Toggle test sequence</button>
<button id='toggle-passed'>Toggle passed tests</button>
<button id='toggle-ignored'>Toggle ignored tests</button>
</h1>
<p class="stats"><%- Object.keys(stats).map(status => stats[status] + ' ' + status).join(', ') + '.' %></p>
<div id='test-sequence' class='hide'>
<% if (unsuccessful.length) { %>
<p><strong>Failed tests:</strong>
<% for (const r of unsuccessful) { %><%- r.id %> <% } %></p>
<% } %>
<p><strong>Test sequence:</strong>
<% for (const s of tests) { %><%- s.id %> <% } %></p>
<% if (shuffle) { %><p><strong>Shuffle seed</strong>: <%- seed %></p><% } %>
</div>
<script>
document.addEventListener('mouseover', handleHover);
document.addEventListener('mouseout', handleHover);
function handleHover(e) {
var el = e.target;
if (el.tagName === 'IMG' && el.dataset.altSrc) {
var tmp = el.src;
el.src = el.dataset.altSrc;
el.dataset.altSrc = tmp;
}
}
document.getElementById('toggle-passed').addEventListener('click', function (e) {
for (const row of document.querySelectorAll('.test.passed')) {
row.classList.toggle('hide');
}
});
document.getElementById('toggle-ignored').addEventListener('click', function (e) {
for (const row of document.querySelectorAll('.test.ignored')) {
row.classList.toggle('hide');
}
});
document.getElementById('toggle-sequence').addEventListener('click', function (e) {
document.getElementById('test-sequence').classList.toggle('hide');
});
</script>
<div class="tests">
<!-- results go here -->
</div>

View File

@@ -1,14 +0,0 @@
import {plugins} from '../../build/rollup_plugins';
export default {
input: 'test/integration/lib/query-browser.js',
output: {
name: 'queryTests',
format: 'iife',
sourcemap: 'inline',
indent: false,
file: 'test/integration/dist/query-test.js'
},
plugins: plugins(false, false),
external: [ 'tape', 'mapboxgl' ]
};

View File

@@ -1,169 +0,0 @@
/* eslint-disable no-global-assign */
/* eslint-disable import/no-commonjs */
/* eslint-disable flowtype/require-valid-file-annotation */
require = require("esm")(module);
const {generateFixtureJson, getAllFixtureGlobs} = require('./lib/generate-fixture-json');
const createServer = require('./lib/server');
const buildTape = require('../../build/test/build-tape');
const runAll = require('npm-run-all');
const chokidar = require('chokidar');
const rollup = require('rollup');
const notifier = require('node-notifier');
const rollupDevConfig = require('../../rollup.config').default;
const rollupTestConfig = require('./rollup.config.test').default;
const rootFixturePath = 'test/integration/';
const suitePath = 'query-tests';
const fixtureBuildInterval = 2000;
let beforeHookInvoked = false;
let server;
let fixtureWatcher;
const rollupWatchers = {};
module.exports = {
"test_page": "test/integration/testem_page.html",
"src_files": [
"dist/mapbox-gl-dev.js",
"test/integration/dist/query-test.js"
],
"launch_in_dev": [],
"launch_in_ci": [ "Chrome" ],
"browser_args": {
"Chrome": {
"mode": "ci",
"args": [ "--headless", "--disable-gpu", "--remote-debugging-port=9222" ]
}
},
"proxies": {
"/tiles":{
"target": "http://localhost:2900"
},
"/glyphs":{
"target": "http://localhost:2900"
},
"/tilesets":{
"target": "http://localhost:2900"
},
"/sprites":{
"target": "http://localhost:2900"
},
"/data":{
"target": "http://localhost:2900"
},
"/write-file":{
"target": "http://localhost:2900"
}
},
"before_tests"(config, data, callback) {
if (!beforeHookInvoked) {
server = createServer();
const buildPromise = config.appMode === 'ci' ? buildArtifactsCi() : buildArtifactsDev();
buildPromise.then(() => {
server.listen(callback);
}).catch((e) => {
callback(e);
});
beforeHookInvoked = true;
}
},
"after_tests"(config, data, callback) {
if (config.appMode === 'ci') {
server.close(callback);
}
}
};
// helper method that builds test artifacts when in CI mode.
// Retuns a promise that resolves when all artifacts are built
function buildArtifactsCi() {
//1. Compile fixture data into a json file, so it can be bundled
generateFixtureJson(rootFixturePath, suitePath);
//2. Build tape
const tapePromise = buildTape();
//3. Build test artifacts in parallel
const rollupPromise = runAll(['build-query-suite', 'build-dev'], {parallel: true});
return Promise.all([tapePromise, rollupPromise]);
}
// helper method that starts a bunch of build-watchers and returns a promise
// that resolves when all of them have had their first run.
function buildArtifactsDev() {
return buildTape().then(() => {
// A promise that resolves on the first build of fixtures.json
return new Promise((resolve, reject) => {
fixtureWatcher = chokidar.watch(getAllFixtureGlobs(rootFixturePath, suitePath));
let needsRebuild = false;
fixtureWatcher.on('ready', () => {
generateFixtureJson(rootFixturePath, suitePath);
//Throttle calls to `generateFixtureJson` to run every 2s
setInterval(() => {
if (needsRebuild) {
generateFixtureJson(rootFixturePath, suitePath);
needsRebuild = false;
}
}, fixtureBuildInterval);
//Flag needs rebuild when anything changes
fixtureWatcher.on('all', () => {
needsRebuild = true;
});
// Resolve promise once chokidar has finished first scan of fixtures
resolve();
});
fixtureWatcher.on('error', (e) => reject(e));
});
}).then(() => {
//Helper function that starts a rollup watcher
//returns a promise that resolves when the first bundle has finished
function startRollupWatcher(name, config) {
return new Promise((resolve, reject) => {
const watcher = rollup.watch(silenceWarnings(config));
rollupWatchers[name] = watcher;
watcher.on('event', (e) => {
if (e.code === 'START') {
notify('Query Tests', `${name} bundle started`);
}
if (e.code === 'END') {
notify('Query Tests', `${name} bundle finished`);
resolve();
}
if (e.code === 'FATAL') {
reject(e);
}
});
});
}
return Promise.all([
startRollupWatcher('mapbox-gl', rollupDevConfig),
startRollupWatcher('query-suite', rollupTestConfig),
]);
});
}
function silenceWarnings(config) {
function addEmptyWarningHandler(configObj) {
configObj["onwarn"] = function() {};
return configObj;
}
if (Array.isArray(config)) {
return config.map(addEmptyWarningHandler);
} else {
return addEmptyWarningHandler(config);
}
}
function notify(title, message) {
if (!process.env.DISABLE_BUILD_NOTIFICATIONS) {
notifier.notify({title, message});
}
}

View File

@@ -1,14 +0,0 @@
<html>
<head>
<title>
Mapbox GL JS Integration Tests
</title>
</head>
<body>
<script src="/testem.js"></script>
<script src="/test/integration/dist/tape.js"></script>
<script src="/dist/mapbox-gl-dev.js"></script>
<script src="/test/integration/dist/query-test.js"></script>
</body>
</html>