diff --git a/.github/workflows/test-expression.yml b/.github/workflows/test-expression.yml
index 7a458ee72c..7c6c45dfcc 100644
--- a/.github/workflows/test-expression.yml
+++ b/.github/workflows/test-expression.yml
@@ -18,5 +18,4 @@ jobs:
node-version: 16
architecture: x64
- run: npm ci
- - run: npm run build-dev
- run: npm run test-expression
diff --git a/package.json b/package.json
index 2812ec2242..a2e241f1f2 100644
--- a/package.json
+++ b/package.json
@@ -174,7 +174,7 @@
"test-browser": "jest ./test/integration/browser",
"test-render": "node --loader ts-node/esm --experimental-specifier-resolution=node --experimental-json-modules --max-old-space-size=2048 test/integration/render/render.test.ts",
"test-query": "jest test/integration/query",
- "test-expression": "node --loader ts-node/esm --experimental-specifier-resolution=node test/integration/expression/expression.test.ts",
+ "test-expression": "jest test/integration/expression",
"test-unit": "jest ./src",
"codegen": "npm run generate-style-code && npm run generate-struct-arrays && npm run generate-style-spec && npm run generate-shaders",
"benchmark": "node --loader ts-node/esm --experimental-specifier-resolution=node bench/run-benchmarks.ts",
diff --git a/test/integration/expression/expression.test.ts b/test/integration/expression/expression.test.ts
index f96de945c4..331bba72c0 100644
--- a/test/integration/expression/expression.test.ts
+++ b/test/integration/expression/expression.test.ts
@@ -1,89 +1,74 @@
-import {fileURLToPath} from 'url';
-
-import {run} from './expression';
+import path from 'path';
+import fs from 'fs';
+import glob from 'glob';
import {createPropertyExpression} from '../../../src/style-spec/expression';
import {isFunction} from '../../../src/style-spec/function';
import convertFunction from '../../../src/style-spec/function/convert';
import {toString} from '../../../src/style-spec/expression/types';
import {CanonicalTileID} from '../../../src/source/tile_id';
-import MercatorCoordinate from '../../../src/geo/mercator_coordinate';
-import Point from '@mapbox/point-geometry';
+import {getGeometry} from './lib/geometry';
+import {stringify} from './lib/util';
+import {deepEqual, stripPrecision} from '../lib/json-diff';
+import {ExpressionFixture} from './fixture-types';
-const ignores = {};
+const decimalSigFigs = 6;
-function getPoint(coord, canonical) {
- const p: Point = canonical.getTilePoint(MercatorCoordinate.fromLngLat({lng: coord[0], lat: coord[1]}, 0));
- p.x = Math.round(p.x);
- p.y = Math.round(p.y);
- return p;
-}
+const expressionTestFileNames = glob.sync('**/test.json', {cwd: __dirname});
+describe('expression', () => {
-function convertPoint(coord, canonical, out) {
- out.push([getPoint(coord, canonical)]);
-}
+ expressionTestFileNames.forEach((expressionTestFileName: any) => {
+ test(expressionTestFileName, (done) => {
-function convertPoints(coords, canonical, out) {
- for (let i = 0; i < coords.length; i++) {
- convertPoint(coords[i], canonical, out);
- }
-}
+ const fixture = JSON.parse(fs.readFileSync(path.join(__dirname, expressionTestFileName), 'utf8'));
-function convertLine(line, canonical, out) {
- const l = [];
- for (let i = 0; i < line.length; i++) {
- l.push(getPoint(line[i], canonical));
- }
- out.push(l);
-}
+ try {
+ const result = evaluateFixture(fixture);
-function convertLines(lines, canonical, out) {
- for (let i = 0; i < lines.length; i++) {
- convertLine(lines[i], canonical, out);
- }
-}
+ if (process.env.UPDATE) {
+ fixture.expected = {
+ compiled: result.compiled,
+ outputs: stripPrecision(result.outputs, decimalSigFigs),
+ serialized: result.serialized
+ };
-function getGeometry(feature, geometry, canonical) {
- if (geometry.coordinates) {
- const coords = geometry.coordinates;
- const type = geometry.type;
- feature.type = type;
- feature.geometry = [];
- if (type === 'Point') {
- convertPoint(coords, canonical, feature.geometry);
- } else if (type === 'MultiPoint') {
- feature.type = 'Point';
- convertPoints(coords, canonical, feature.geometry);
- } else if (type === 'LineString') {
- convertLine(coords, canonical, feature.geometry);
- } else if (type === 'MultiLineString') {
- feature.type = 'LineString';
- convertLines(coords, canonical, feature.geometry);
- } else if (type === 'Polygon') {
- convertLines(coords, canonical, feature.geometry);
- } else if (type === 'MultiPolygon') {
- feature.type = 'Polygon';
- for (let i = 0; i < coords.length; i++) {
- const polygon = [];
- convertLines(coords[i], canonical, polygon);
- feature.geometry.push(polygon);
- }
- }
- }
-}
+ delete fixture.metadata;
-let tests;
+ const dir = path.join(__dirname, expressionTestFileName);
+ fs.writeFile(path.join(dir, 'test.json'), `${stringify(fixture)}\n`, done);
+ return;
+ }
-// @ts-ignore
-const __filename = fileURLToPath(import.meta.url);
+ const expected = fixture.expected;
+ const compileOk = deepEqual(result.compiled, expected.compiled, decimalSigFigs);
+ const evalOk = compileOk && deepEqual(result.outputs, expected.outputs, decimalSigFigs);
+
+ let recompileOk = true;
+ let roundTripOk = true;
+ let serializationOk = true;
+ if (expected.compiled.result !== 'error') {
+ serializationOk = compileOk && deepEqual(expected.serialized, result.serialized, decimalSigFigs);
+ recompileOk = compileOk && deepEqual(result.recompiled, expected.compiled, decimalSigFigs);
+ roundTripOk = recompileOk && deepEqual(result.roundTripOutputs, expected.outputs, decimalSigFigs);
+ }
-if (process.argv[1] === __filename && process.argv.length > 2) {
- tests = process.argv.slice(2);
-}
+ expect(compileOk).toBeTruthy();
+ expect(evalOk).toBeTruthy();
+ expect(recompileOk).toBeTruthy();
+ expect(roundTripOk).toBeTruthy();
+ expect(serializationOk).toBeTruthy();
-run('js', {ignores, tests}, (fixture) => {
+ done();
+ } catch (e) {
+ done(e);
+ }
+
+ });
+ });
+
+});
+
+function evaluateFixture(fixture) {
const spec = Object.assign({}, fixture.propertySpec);
- let availableImages;
- let canonical;
if (!spec['property-type']) {
spec['property-type'] = 'data-driven';
@@ -96,72 +81,6 @@ run('js', {ignores, tests}, (fixture) => {
};
}
- const evaluateExpression = (expression, compilationResult) => {
- if (expression.result === 'error') {
- compilationResult.result = 'error';
- compilationResult.errors = expression.value.map((err) => ({
- key: err.key,
- error: err.message
- }));
- return;
- }
-
- const evaluationResult = [];
-
- expression = expression.value;
- const type = expression._styleExpression.expression.type; // :scream:
-
- compilationResult.result = 'success';
- compilationResult.isFeatureConstant = expression.kind === 'constant' || expression.kind === 'camera';
- compilationResult.isZoomConstant = expression.kind === 'constant' || expression.kind === 'source';
- compilationResult.type = toString(type);
-
- for (const input of fixture.inputs || []) {
- try {
- const feature: {
- properties: any;
- id?: any;
- type?: any;
- } = {properties: input[1].properties || {}};
- availableImages = input[0].availableImages || [];
- if ('canonicalID' in input[0]) {
- const id = input[0].canonicalID;
- canonical = new CanonicalTileID(id.z, id.x, id.y);
- } else {
- canonical = null;
- }
-
- if ('id' in input[1]) {
- feature.id = input[1].id;
- }
- if ('geometry' in input[1]) {
- if (canonical !== null) {
- getGeometry(feature, input[1].geometry, canonical);
- } else {
- feature.type = input[1].geometry.type;
- }
- }
-
- let value = expression.evaluateWithoutErrorHandling(input[0], feature, {}, canonical, availableImages);
-
- if (type.kind === 'color') {
- value = [value.r, value.g, value.b, value.a];
- }
- evaluationResult.push(value);
- } catch (error) {
- if (error.name === 'ExpressionEvaluationError') {
- evaluationResult.push({error: error.toJSON()});
- } else {
- evaluationResult.push({error: error.message});
- }
- }
- }
-
- if (fixture.inputs) {
- return evaluationResult;
- }
- };
-
const result: {
compiled: any;
recompiled: any;
@@ -169,6 +88,7 @@ run('js', {ignores, tests}, (fixture) => {
serialized?: any;
roundTripOutputs?: any;
} = {compiled: {}, recompiled: {}};
+
const expression = (() => {
if (isFunction(fixture.expression)) {
return createPropertyExpression(convertFunction(fixture.expression, spec), spec);
@@ -177,11 +97,11 @@ run('js', {ignores, tests}, (fixture) => {
}
})();
- result.outputs = evaluateExpression(expression, result.compiled);
+ result.outputs = evaluateExpression(fixture, expression, result.compiled);
if (expression.result === 'success') {
// @ts-ignore
result.serialized = expression.value._styleExpression.expression.serialize();
- result.roundTripOutputs = evaluateExpression(
+ result.roundTripOutputs = evaluateExpression(fixture,
createPropertyExpression(result.serialized, spec),
result.recompiled);
// Type is allowed to change through serialization
@@ -191,4 +111,74 @@ run('js', {ignores, tests}, (fixture) => {
}
return result;
-});
+}
+
+function evaluateExpression (fixture: ExpressionFixture, expression, compilationResult) {
+
+ let availableImages;
+ let canonical;
+
+ if (expression.result === 'error') {
+ compilationResult.result = 'error';
+ compilationResult.errors = expression.value.map((err) => ({
+ key: err.key,
+ error: err.message
+ }));
+ return;
+ }
+
+ const evaluationResult = [];
+
+ expression = expression.value;
+ const type = expression._styleExpression.expression.type; // :scream:
+
+ compilationResult.result = 'success';
+ compilationResult.isFeatureConstant = expression.kind === 'constant' || expression.kind === 'camera';
+ compilationResult.isZoomConstant = expression.kind === 'constant' || expression.kind === 'source';
+ compilationResult.type = toString(type);
+
+ for (const input of fixture.inputs || []) {
+ try {
+ const feature: {
+ properties: any;
+ id?: any;
+ type?: any;
+ } = {properties: input[1].properties || {}};
+ availableImages = input[0].availableImages || [];
+ if ('canonicalID' in input[0]) {
+ const id = input[0].canonicalID;
+ canonical = new CanonicalTileID(id.z, id.x, id.y);
+ } else {
+ canonical = null;
+ }
+
+ if ('id' in input[1]) {
+ feature.id = input[1].id;
+ }
+ if ('geometry' in input[1]) {
+ if (canonical !== null) {
+ getGeometry(feature, input[1].geometry, canonical);
+ } else {
+ feature.type = input[1].geometry.type;
+ }
+ }
+
+ let value = expression.evaluateWithoutErrorHandling(input[0], feature, {}, canonical, availableImages);
+
+ if (type.kind === 'color') {
+ value = [value.r, value.g, value.b, value.a];
+ }
+ evaluationResult.push(value);
+ } catch (error) {
+ if (error.name === 'ExpressionEvaluationError') {
+ evaluationResult.push({error: error.toJSON()});
+ } else {
+ evaluationResult.push({error: error.message});
+ }
+ }
+ }
+
+ if (fixture.inputs) {
+ return evaluationResult;
+ }
+}
diff --git a/test/integration/expression/expression.ts b/test/integration/expression/expression.ts
deleted file mode 100644
index 9586f8a18e..0000000000
--- a/test/integration/expression/expression.ts
+++ /dev/null
@@ -1,200 +0,0 @@
-import path, {dirname} from 'path';
-import {diffJson} from 'diff';
-import fs from 'fs';
-import harness from '../lib/harness';
-import compactStringify from 'json-stringify-pretty-compact';
-import {fileURLToPath} from 'url';
-
-// @ts-ignore
-const __dirname = dirname(fileURLToPath(import.meta.url));
-
-// 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 implementation - identify the implementation under test; used to
- * deal with implementation-specific test exclusions and fudge-factors
- * @param options
- * @param options.tests - array of test names to run; tests not in the array will be skipped
- * @param options.ignores - array of test names to ignore.
- * @param runExpressionTest - a function that runs a single expression test fixture
- * @returns terminates the process when testing is complete
- */
-export function run(implementation: string, options: {
- tests?: any; ignores?: any; fixtureFilename?: any;
-}, runExpressionTest) {
- const directory = path.join(__dirname);
- 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)}\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 diffJsonWrap = (label, expectedJson, actualJson) => {
- let text = '';
- let html = '';
- diffJson(expectedJson, actualJson)
- .forEach((hunk) => {
- if (hunk.added) {
- text += `+ ${hunk.value}`;
- html += ` ${hunk.value}`;
- } else if (hunk.removed) {
- text += `- ${hunk.value}`;
- html += ` ${hunk.value}`;
- } else {
- text += ` ${hunk.value}`;
- html += ` ${hunk.value}`;
- }
- });
- if (text) {
- diffOutput.text += `${label}\n${text}`;
- diffOutput.html += `