Skip to content

Commit

Permalink
Migrate expression tests to jest (#965)
Browse files Browse the repository at this point in the history
  • Loading branch information
birkskyum authored Feb 8, 2022
1 parent aa8ed9d commit 151bc9d
Show file tree
Hide file tree
Showing 9 changed files with 263 additions and 361 deletions.
1 change: 0 additions & 1 deletion .github/workflows/test-expression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,4 @@ jobs:
node-version: 16
architecture: x64
- run: npm ci
- run: npm run build-dev
- run: npm run test-expression
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
266 changes: 128 additions & 138 deletions test/integration/expression/expression.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -96,79 +81,14 @@ 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;
outputs?: any;
serialized?: any;
roundTripOutputs?: any;
} = {compiled: {}, recompiled: {}};

const expression = (() => {
if (isFunction(fixture.expression)) {
return createPropertyExpression(convertFunction(fixture.expression, spec), spec);
Expand All @@ -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
Expand All @@ -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;
}
}
Loading

0 comments on commit 151bc9d

Please sign in to comment.