Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate expression tests to jest #965

Merged
merged 14 commits into from
Feb 8, 2022
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) {
HarelM marked this conversation as resolved.
Show resolved Hide resolved
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