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
262 changes: 124 additions & 138 deletions test/integration/expression/expression.test.ts
Original file line number Diff line number Diff line change
@@ -1,89 +1,70 @@
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, deepEqual, stripPrecision} from './lib/util';

const ignores = {};
const expressionTestFileNames = glob.sync('**/test.json', {cwd: __dirname});
describe('expression', () => {

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;
}
expressionTestFileNames.forEach((expressionTestFileName: any) => {
test(expressionTestFileName, (done) => {

function convertPoint(coord, canonical, out) {
out.push([getPoint(coord, canonical)]);
}
const fixture = JSON.parse(fs.readFileSync(path.join(__dirname, expressionTestFileName), 'utf8'));

function convertPoints(coords, canonical, out) {
for (let i = 0; i < coords.length; i++) {
convertPoint(coords[i], canonical, out);
}
}
try {
const result = evaluateFixture(fixture);

function convertLine(line, canonical, out) {
const l = [];
for (let i = 0; i < line.length; i++) {
l.push(getPoint(line[i], canonical));
}
out.push(l);
}
if (process.env.UPDATE) {
fixture.expected = {
compiled: result.compiled,
outputs: stripPrecision(result.outputs),
serialized: result.serialized
};

function convertLines(lines, canonical, out) {
for (let i = 0; i < lines.length; i++) {
convertLine(lines[i], canonical, out);
}
}
delete fixture.metadata;

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);
}
}
}
}
const dir = path.join(__dirname, expressionTestFileName);
fs.writeFile(path.join(dir, 'test.json'), `${stringify(fixture)}\n`, done);
return;
}

let tests;
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);
}

// @ts-ignore
const __filename = fileURLToPath(import.meta.url);
expect(compileOk).toBeTruthy();
expect(evalOk).toBeTruthy();
expect(recompileOk).toBeTruthy();
expect(roundTripOk).toBeTruthy();
expect(serializationOk).toBeTruthy();

if (process.argv[1] === __filename && process.argv.length > 2) {
tests = process.argv.slice(2);
}
done();
} catch (e) {
done(e);
}

run('js', {ignores, tests}, (fixture) => {
});
});

});

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 +77,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 +93,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 +107,74 @@ run('js', {ignores, tests}, (fixture) => {
}

return result;
});
}

function evaluateExpression (fixture, expression, compilationResult) {
HarelM marked this conversation as resolved.
Show resolved Hide resolved

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