From 83ed34659e6b89939d3b8fbdca7c82ddde586f4c Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Mon, 7 Feb 2022 18:49:39 +0100 Subject: [PATCH 01/14] Move expression tests to jest --- .github/workflows/test-expression.yml | 1 - package.json | 2 +- .../integration/expression/expression.test.ts | 330 ++++++++---------- test/integration/expression/expression.ts | 200 ----------- test/integration/expression/lib/geometry.ts | 63 ++++ test/integration/expression/lib/util.ts | 71 ++++ .../expression/resultItemTemplate.ts | 17 - test/integration/lib/harness.ts | 2 +- 8 files changed, 290 insertions(+), 396 deletions(-) delete mode 100644 test/integration/expression/expression.ts create mode 100644 test/integration/expression/lib/geometry.ts create mode 100644 test/integration/expression/lib/util.ts delete mode 100644 test/integration/expression/resultItemTemplate.ts 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..7ea6da9419 100644 --- a/test/integration/expression/expression.test.ts +++ b/test/integration/expression/expression.test.ts @@ -1,194 +1,172 @@ -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'; - -const ignores = {}; - -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; -} - -function convertPoint(coord, canonical, out) { - out.push([getPoint(coord, canonical)]); -} - -function convertPoints(coords, canonical, out) { - for (let i = 0; i < coords.length; i++) { - convertPoint(coords[i], canonical, out); - } -} - -function convertLine(line, canonical, out) { - const l = []; - for (let i = 0; i < line.length; i++) { - l.push(getPoint(line[i], canonical)); - } - out.push(l); -} - -function convertLines(lines, canonical, out) { - for (let i = 0; i < lines.length; i++) { - convertLine(lines[i], canonical, out); - } -} - -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); - } - } - } -} - -let tests; - -// @ts-ignore -const __filename = fileURLToPath(import.meta.url); - -if (process.argv[1] === __filename && process.argv.length > 2) { - tests = process.argv.slice(2); -} - -run('js', {ignores, tests}, (fixture) => { - const spec = Object.assign({}, fixture.propertySpec); - let availableImages; - let canonical; - - if (!spec['property-type']) { - spec['property-type'] = 'data-driven'; - } - - if (!spec['expression']) { - spec['expression'] = { - 'interpolated': true, - 'parameters': ['zoom', 'feature'] - }; - } - - 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 || []) { +import {getGeometry} from './lib/geometry'; +import {stringify, deepEqual, stripPrecision} from './lib/util'; + +const expressionTestFileNames = glob.sync('**/test.json', {cwd: __dirname});//, {cwd: __dirname}); +describe('expression', () => { + + expressionTestFileNames.forEach((expressionTestFileName: any) => { + test(expressionTestFileName, (done) => { + + const fixture = JSON.parse(fs.readFileSync(path.join(__dirname, expressionTestFileName), 'utf8')); + 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; + const spec = Object.assign({}, fixture.propertySpec); + let availableImages; + let canonical; + + if (!spec['property-type']) { + spec['property-type'] = 'data-driven'; } - if ('id' in input[1]) { - feature.id = input[1].id; + if (!spec['expression']) { + spec['expression'] = { + 'interpolated': true, + 'parameters': ['zoom', 'feature'] + }; } - if ('geometry' in input[1]) { - if (canonical !== null) { - getGeometry(feature, input[1].geometry, canonical); + + 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); } else { - feature.type = input[1].geometry.type; + return createPropertyExpression(fixture.expression, spec); } + })(); + + result.outputs = evaluateExpression(expression, result.compiled); + if (expression.result === 'success') { + // @ts-ignore + result.serialized = expression.value._styleExpression.expression.serialize(); + result.roundTripOutputs = evaluateExpression( + createPropertyExpression(result.serialized, spec), + result.recompiled); + // Type is allowed to change through serialization + // (eg "array" -> "array") + // Override the round-tripped type here so that the equality check passes + result.recompiled.type = result.compiled.type; } - let value = expression.evaluateWithoutErrorHandling(input[0], feature, {}, canonical, availableImages); + if (process.env.UPDATE) { + fixture.expected = { + compiled: result.compiled, + outputs: stripPrecision(result.outputs), + serialized: result.serialized + }; - if (type.kind === 'color') { - value = [value.r, value.g, value.b, value.a]; + delete fixture.metadata; + + const dir = path.join(__dirname, expressionTestFileName); + fs.writeFile(path.join(dir, 'test.json'), `${stringify(fixture)}\n`, done); + return; } - evaluationResult.push(value); - } catch (error) { - if (error.name === 'ExpressionEvaluationError') { - evaluationResult.push({error: error.toJSON()}); - } else { - evaluationResult.push({error: error.message}); + + 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); } + + expect(compileOk).toBeTruthy(); + expect(evalOk).toBeTruthy(); + expect(recompileOk).toBeTruthy(); + expect(roundTripOk).toBeTruthy(); + expect(serializationOk).toBeTruthy(); + + done(); + } catch (e) { + done(e); } - } - - 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); - } else { - return createPropertyExpression(fixture.expression, spec); - } - })(); - - result.outputs = evaluateExpression(expression, result.compiled); - if (expression.result === 'success') { - // @ts-ignore - result.serialized = expression.value._styleExpression.expression.serialize(); - result.roundTripOutputs = evaluateExpression( - createPropertyExpression(result.serialized, spec), - result.recompiled); - // Type is allowed to change through serialization - // (eg "array" -> "array") - // Override the round-tripped type here so that the equality check passes - result.recompiled.type = result.compiled.type; - } - - return result; + + }); + }); + }); 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 += `

${label}

\n${html}`; - } - }; - - if (!compileOk) { - diffJsonWrap('Compiled', expected.compiled, result.compiled); - } - if (compileOk && !serializationOk) { - diffJsonWrap('Serialized', expected.serialized, result.serialized); - } - if (compileOk && !recompileOk) { - diffJsonWrap('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); - } - }); -} diff --git a/test/integration/expression/lib/geometry.ts b/test/integration/expression/lib/geometry.ts new file mode 100644 index 0000000000..997b0fd7c1 --- /dev/null +++ b/test/integration/expression/lib/geometry.ts @@ -0,0 +1,63 @@ + +import MercatorCoordinate from '../../../../src/geo/mercator_coordinate'; +import Point from '@mapbox/point-geometry'; + +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; +} + +function convertPoint(coord, canonical, out) { + out.push([getPoint(coord, canonical)]); +} + +function convertPoints(coords, canonical, out) { + for (let i = 0; i < coords.length; i++) { + convertPoint(coords[i], canonical, out); + } +} + +function convertLine(line, canonical, out) { + const l = []; + for (let i = 0; i < line.length; i++) { + l.push(getPoint(line[i], canonical)); + } + out.push(l); +} + +function convertLines(lines, canonical, out) { + for (let i = 0; i < lines.length; i++) { + convertLine(lines[i], canonical, out); + } +} + +export 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); + } + } + } +} diff --git a/test/integration/expression/lib/util.ts b/test/integration/expression/lib/util.ts new file mode 100644 index 0000000000..9fce8b86da --- /dev/null +++ b/test/integration/expression/lib/util.ts @@ -0,0 +1,71 @@ +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 +export 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; + +export 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; + } +} + +export 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; +} diff --git a/test/integration/expression/resultItemTemplate.ts b/test/integration/expression/resultItemTemplate.ts deleted file mode 100644 index ace8fbc599..0000000000 --- a/test/integration/expression/resultItemTemplate.ts +++ /dev/null @@ -1,17 +0,0 @@ -// eslint-disable-next-line no-unused-expressions -(meta) => `
-

${meta.r.status} ${meta.r.id}

-
${meta.r.expression}
- - ${meta.r.error ? `

Error: ${meta.r.error.message}

` : ''} - - ${meta.r.difference ? ` - Difference: -
${meta.r.difference}
- ` : ''} - - ${meta.r.serialized ? ` - Serialized: -
${meta.r.serialized}
- ` : ''} -
`; diff --git a/test/integration/lib/harness.ts b/test/integration/lib/harness.ts index 2043a1c173..e8cc99667a 100644 --- a/test/integration/lib/harness.ts +++ b/test/integration/lib/harness.ts @@ -17,7 +17,7 @@ export default function (directory, implementation, options, run) { const tests = options.tests || []; const ignores = options.ignores || {}; - + console.log(directory); let sequence = glob.sync(`**/${options.fixtureFilename || 'style.json'}`, {cwd: directory}) .map(fixture => { const id = path.dirname(fixture); From 22a370203af06be73421adbc166019beae931713 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Mon, 7 Feb 2022 18:51:22 +0100 Subject: [PATCH 02/14] Remove console.log --- test/integration/lib/harness.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/lib/harness.ts b/test/integration/lib/harness.ts index e8cc99667a..2043a1c173 100644 --- a/test/integration/lib/harness.ts +++ b/test/integration/lib/harness.ts @@ -17,7 +17,7 @@ export default function (directory, implementation, options, run) { const tests = options.tests || []; const ignores = options.ignores || {}; - console.log(directory); + let sequence = glob.sync(`**/${options.fixtureFilename || 'style.json'}`, {cwd: directory}) .map(fixture => { const id = path.dirname(fixture); From 7b9c90185650f8d583e5e59440196ebec655f072 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Mon, 7 Feb 2022 18:59:27 +0100 Subject: [PATCH 03/14] Reduce indentation --- .../integration/expression/expression.test.ts | 224 +++++++++--------- 1 file changed, 115 insertions(+), 109 deletions(-) diff --git a/test/integration/expression/expression.test.ts b/test/integration/expression/expression.test.ts index 7ea6da9419..ddf251d29c 100644 --- a/test/integration/expression/expression.test.ts +++ b/test/integration/expression/expression.test.ts @@ -18,115 +18,7 @@ describe('expression', () => { const fixture = JSON.parse(fs.readFileSync(path.join(__dirname, expressionTestFileName), 'utf8')); try { - const spec = Object.assign({}, fixture.propertySpec); - let availableImages; - let canonical; - - if (!spec['property-type']) { - spec['property-type'] = 'data-driven'; - } - - if (!spec['expression']) { - spec['expression'] = { - 'interpolated': true, - 'parameters': ['zoom', 'feature'] - }; - } - - 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); - } else { - return createPropertyExpression(fixture.expression, spec); - } - })(); - - result.outputs = evaluateExpression(expression, result.compiled); - if (expression.result === 'success') { - // @ts-ignore - result.serialized = expression.value._styleExpression.expression.serialize(); - result.roundTripOutputs = evaluateExpression( - createPropertyExpression(result.serialized, spec), - result.recompiled); - // Type is allowed to change through serialization - // (eg "array" -> "array") - // Override the round-tripped type here so that the equality check passes - result.recompiled.type = result.compiled.type; - } + const result = evaluateFixture(fixture); if (process.env.UPDATE) { fixture.expected = { @@ -170,3 +62,117 @@ describe('expression', () => { }); }); + +function evaluateFixture(fixture) { + const spec = Object.assign({}, fixture.propertySpec); + let availableImages; + let canonical; + + if (!spec['property-type']) { + spec['property-type'] = 'data-driven'; + } + + if (!spec['expression']) { + spec['expression'] = { + 'interpolated': true, + 'parameters': ['zoom', 'feature'] + }; + } + + 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); + } else { + return createPropertyExpression(fixture.expression, spec); + } + })(); + + result.outputs = evaluateExpression(expression, result.compiled); + if (expression.result === 'success') { + // @ts-ignore + result.serialized = expression.value._styleExpression.expression.serialize(); + result.roundTripOutputs = evaluateExpression( + createPropertyExpression(result.serialized, spec), + result.recompiled); + // Type is allowed to change through serialization + // (eg "array" -> "array") + // Override the round-tripped type here so that the equality check passes + result.recompiled.type = result.compiled.type; + } + + return result; +} From 978abfbafa7b7d044a487ff79bee860511685a6f Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Mon, 7 Feb 2022 22:08:47 +0100 Subject: [PATCH 04/14] Factor out evaluateExpression --- .../integration/expression/expression.test.ts | 142 +++++++++--------- 1 file changed, 72 insertions(+), 70 deletions(-) diff --git a/test/integration/expression/expression.test.ts b/test/integration/expression/expression.test.ts index ddf251d29c..9c9eb030c0 100644 --- a/test/integration/expression/expression.test.ts +++ b/test/integration/expression/expression.test.ts @@ -65,8 +65,6 @@ describe('expression', () => { function evaluateFixture(fixture) { const spec = Object.assign({}, fixture.propertySpec); - let availableImages; - let canonical; if (!spec['property-type']) { spec['property-type'] = 'data-driven'; @@ -79,72 +77,6 @@ function evaluateFixture(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; @@ -161,11 +93,11 @@ function evaluateFixture(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 @@ -176,3 +108,73 @@ function evaluateFixture(fixture) { return result; } + +function evaluateExpression (fixture, 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; + } +} From 80f156b321a38e8609686cea6c4b1bd44a2ad6e7 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Mon, 7 Feb 2022 22:09:08 +0100 Subject: [PATCH 05/14] Remove comment --- test/integration/expression/expression.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/expression/expression.test.ts b/test/integration/expression/expression.test.ts index 9c9eb030c0..4ebc736a71 100644 --- a/test/integration/expression/expression.test.ts +++ b/test/integration/expression/expression.test.ts @@ -9,7 +9,7 @@ import {CanonicalTileID} from '../../../src/source/tile_id'; import {getGeometry} from './lib/geometry'; import {stringify, deepEqual, stripPrecision} from './lib/util'; -const expressionTestFileNames = glob.sync('**/test.json', {cwd: __dirname});//, {cwd: __dirname}); +const expressionTestFileNames = glob.sync('**/test.json', {cwd: __dirname}); describe('expression', () => { expressionTestFileNames.forEach((expressionTestFileName: any) => { From 261327dda1e6ba1c112df1486bbfbc5b890cfb41 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 8 Feb 2022 10:09:47 +0100 Subject: [PATCH 06/14] Convert out param to return --- test/integration/expression/lib/geometry.ts | 40 ++++++++++++--------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/test/integration/expression/lib/geometry.ts b/test/integration/expression/lib/geometry.ts index 997b0fd7c1..fcee48a36d 100644 --- a/test/integration/expression/lib/geometry.ts +++ b/test/integration/expression/lib/geometry.ts @@ -1,61 +1,67 @@ import MercatorCoordinate from '../../../../src/geo/mercator_coordinate'; import Point from '@mapbox/point-geometry'; +import {CanonicalTileID} from '../../../../src/source/tile_id'; -function getPoint(coord, canonical) { +function getPoint(coord, canonical: CanonicalTileID): Point { 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; } -function convertPoint(coord, canonical, out) { - out.push([getPoint(coord, canonical)]); +function convertPoint(coord, canonical: CanonicalTileID): Point[] { + return [getPoint(coord, canonical)]; } -function convertPoints(coords, canonical, out) { +function convertPoints(coords, canonical: CanonicalTileID) { + const o: Point[][] = []; for (let i = 0; i < coords.length; i++) { - convertPoint(coords[i], canonical, out); + o.push(convertPoint(coords[i], canonical)); } + + return o; } -function convertLine(line, canonical, out) { - const l = []; +function convertLine(line, canonical: CanonicalTileID) { + const l: Point[] = []; for (let i = 0; i < line.length; i++) { l.push(getPoint(line[i], canonical)); } - out.push(l); + return l; } -function convertLines(lines, canonical, out) { +function convertLines(lines, canonical: CanonicalTileID) { + const l: Point[][] = []; for (let i = 0; i < lines.length; i++) { - convertLine(lines[i], canonical, out); + l.push(convertLine(lines[i], canonical)); } + return l; } -export function getGeometry(feature, geometry, canonical) { +export function getGeometry(feature, geometry, canonical: CanonicalTileID) { if (geometry.coordinates) { const coords = geometry.coordinates; const type = geometry.type; feature.type = type; feature.geometry = []; if (type === 'Point') { - convertPoint(coords, canonical, feature.geometry); + feature.geometry.push(convertPoint(coords, canonical)); } else if (type === 'MultiPoint') { feature.type = 'Point'; - convertPoints(coords, canonical, feature.geometry); + feature.geometry.push(...convertPoints(coords, canonical)); } else if (type === 'LineString') { - convertLine(coords, canonical, feature.geometry); + feature.geometry.push(...convertLine(coords, canonical)); } else if (type === 'MultiLineString') { feature.type = 'LineString'; - convertLines(coords, canonical, feature.geometry); + feature.geometry.push(...convertLines(coords, canonical)); } else if (type === 'Polygon') { - convertLines(coords, canonical, feature.geometry); + feature.geometry.push(...convertLines(coords, canonical)); } else if (type === 'MultiPolygon') { feature.type = 'Polygon'; for (let i = 0; i < coords.length; i++) { const polygon = []; - convertLines(coords[i], canonical, polygon); + polygon.push(...convertLines(coords[i], canonical)); feature.geometry.push(polygon); } } From ee027a0f91951aca508436dd36e3ebba6e8a369c Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 8 Feb 2022 10:17:13 +0100 Subject: [PATCH 07/14] add types --- test/integration/expression/lib/geometry.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/expression/lib/geometry.ts b/test/integration/expression/lib/geometry.ts index fcee48a36d..8732c4892f 100644 --- a/test/integration/expression/lib/geometry.ts +++ b/test/integration/expression/lib/geometry.ts @@ -14,7 +14,7 @@ function convertPoint(coord, canonical: CanonicalTileID): Point[] { return [getPoint(coord, canonical)]; } -function convertPoints(coords, canonical: CanonicalTileID) { +function convertPoints(coords, canonical: CanonicalTileID): Point[][] { const o: Point[][] = []; for (let i = 0; i < coords.length; i++) { o.push(convertPoint(coords[i], canonical)); @@ -23,7 +23,7 @@ function convertPoints(coords, canonical: CanonicalTileID) { return o; } -function convertLine(line, canonical: CanonicalTileID) { +function convertLine(line, canonical: CanonicalTileID): Point[] { const l: Point[] = []; for (let i = 0; i < line.length; i++) { l.push(getPoint(line[i], canonical)); @@ -31,7 +31,7 @@ function convertLine(line, canonical: CanonicalTileID) { return l; } -function convertLines(lines, canonical: CanonicalTileID) { +function convertLines(lines, canonical: CanonicalTileID): Point[][] { const l: Point[][] = []; for (let i = 0; i < lines.length; i++) { l.push(convertLine(lines[i], canonical)); From 79632b3b6b6c8a8b25438bc302a05bd405f8db8b Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 8 Feb 2022 10:22:10 +0100 Subject: [PATCH 08/14] add types --- test/integration/expression/lib/geometry.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/integration/expression/lib/geometry.ts b/test/integration/expression/lib/geometry.ts index 8732c4892f..4b4191f10f 100644 --- a/test/integration/expression/lib/geometry.ts +++ b/test/integration/expression/lib/geometry.ts @@ -2,19 +2,20 @@ import MercatorCoordinate from '../../../../src/geo/mercator_coordinate'; import Point from '@mapbox/point-geometry'; import {CanonicalTileID} from '../../../../src/source/tile_id'; +import {LngLatLike} from '../../../../src/geo/lng_lat'; -function getPoint(coord, canonical: CanonicalTileID): Point { +function getPoint(coord: LngLatLike, canonical: CanonicalTileID): Point { 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; } -function convertPoint(coord, canonical: CanonicalTileID): Point[] { +function convertPoint(coord: LngLatLike, canonical: CanonicalTileID): Point[] { return [getPoint(coord, canonical)]; } -function convertPoints(coords, canonical: CanonicalTileID): Point[][] { +function convertPoints(coords: LngLatLike[], canonical: CanonicalTileID): Point[][] { const o: Point[][] = []; for (let i = 0; i < coords.length; i++) { o.push(convertPoint(coords[i], canonical)); @@ -23,7 +24,7 @@ function convertPoints(coords, canonical: CanonicalTileID): Point[][] { return o; } -function convertLine(line, canonical: CanonicalTileID): Point[] { +function convertLine(line: LngLatLike[], canonical: CanonicalTileID): Point[] { const l: Point[] = []; for (let i = 0; i < line.length; i++) { l.push(getPoint(line[i], canonical)); @@ -31,7 +32,7 @@ function convertLine(line, canonical: CanonicalTileID): Point[] { return l; } -function convertLines(lines, canonical: CanonicalTileID): Point[][] { +function convertLines(lines: LngLatLike[][], canonical: CanonicalTileID): Point[][] { const l: Point[][] = []; for (let i = 0; i < lines.length; i++) { l.push(convertLine(lines[i], canonical)); From b85a3193441b0ada421a6fe0a22cc7d880646906 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 8 Feb 2022 10:29:36 +0100 Subject: [PATCH 09/14] Remove comment --- test/integration/expression/lib/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/expression/lib/util.ts b/test/integration/expression/lib/util.ts index 9fce8b86da..17cac9ebf1 100644 --- a/test/integration/expression/lib/util.ts +++ b/test/integration/expression/lib/util.ts @@ -4,7 +4,7 @@ import compactStringify from 'json-stringify-pretty-compact'; // edge case, and we don't want UPDATE=1 to mess with them export 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'); } From bc9c267b093c03ac14e8d533db7f5b5e4eecbf1b Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 8 Feb 2022 10:33:07 +0100 Subject: [PATCH 10/14] add type --- test/integration/lib/json-diff.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/lib/json-diff.ts b/test/integration/lib/json-diff.ts index 8c4d95e2ca..dab36244f5 100644 --- a/test/integration/lib/json-diff.ts +++ b/test/integration/lib/json-diff.ts @@ -12,7 +12,7 @@ export function generateDiffLog(expected, actual) { }).join(''); } -export function deepEqual(a, b) { +export function deepEqual(a, b): boolean { if (typeof a !== typeof b) return false; if (typeof a === 'number') From 832e260957e0ffd6bd4f6197dfb6f961c8d04ca0 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 8 Feb 2022 10:33:52 +0100 Subject: [PATCH 11/14] add type --- test/integration/expression/lib/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/expression/lib/util.ts b/test/integration/expression/lib/util.ts index 17cac9ebf1..79cb81decd 100644 --- a/test/integration/expression/lib/util.ts +++ b/test/integration/expression/lib/util.ts @@ -45,7 +45,7 @@ export function stripPrecision(x) { } } -export function deepEqual(a, b) { +export function deepEqual(a, b): boolean { if (typeof a !== typeof b) return false; if (typeof a === 'number') { From e0191452fb77b94cc1410bab59334ce3221cf814 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 8 Feb 2022 10:55:56 +0100 Subject: [PATCH 12/14] fix array handling error --- test/integration/expression/lib/geometry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/expression/lib/geometry.ts b/test/integration/expression/lib/geometry.ts index 4b4191f10f..dc485f2fbd 100644 --- a/test/integration/expression/lib/geometry.ts +++ b/test/integration/expression/lib/geometry.ts @@ -52,7 +52,7 @@ export function getGeometry(feature, geometry, canonical: CanonicalTileID) { feature.type = 'Point'; feature.geometry.push(...convertPoints(coords, canonical)); } else if (type === 'LineString') { - feature.geometry.push(...convertLine(coords, canonical)); + feature.geometry.push(convertLine(coords, canonical)); } else if (type === 'MultiLineString') { feature.type = 'LineString'; feature.geometry.push(...convertLines(coords, canonical)); From 02d833a93e61bae02012c7c89a12a82471b1319d Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 8 Feb 2022 11:18:39 +0100 Subject: [PATCH 13/14] Reuse deepEqualFunc --- .../integration/expression/expression.test.ts | 17 +++--- test/integration/expression/lib/util.ts | 56 ------------------- test/integration/lib/json-diff.ts | 38 +++++++++++-- 3 files changed, 44 insertions(+), 67 deletions(-) diff --git a/test/integration/expression/expression.test.ts b/test/integration/expression/expression.test.ts index 4ebc736a71..87774284ef 100644 --- a/test/integration/expression/expression.test.ts +++ b/test/integration/expression/expression.test.ts @@ -7,7 +7,10 @@ import convertFunction from '../../../src/style-spec/function/convert'; import {toString} from '../../../src/style-spec/expression/types'; import {CanonicalTileID} from '../../../src/source/tile_id'; import {getGeometry} from './lib/geometry'; -import {stringify, deepEqual, stripPrecision} from './lib/util'; +import {stringify} from './lib/util'; +import {deepEqual, stripPrecision} from '../lib/json-diff'; + +const decimalSigFigs = 6; const expressionTestFileNames = glob.sync('**/test.json', {cwd: __dirname}); describe('expression', () => { @@ -23,7 +26,7 @@ describe('expression', () => { if (process.env.UPDATE) { fixture.expected = { compiled: result.compiled, - outputs: stripPrecision(result.outputs), + outputs: stripPrecision(result.outputs, decimalSigFigs), serialized: result.serialized }; @@ -35,16 +38,16 @@ describe('expression', () => { } const expected = fixture.expected; - const compileOk = deepEqual(result.compiled, expected.compiled); - const evalOk = compileOk && deepEqual(result.outputs, expected.outputs); + 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); - recompileOk = compileOk && deepEqual(result.recompiled, expected.compiled); - roundTripOk = recompileOk && deepEqual(result.roundTripOutputs, expected.outputs); + serializationOk = compileOk && deepEqual(expected.serialized, result.serialized, decimalSigFigs); + recompileOk = compileOk && deepEqual(result.recompiled, expected.compiled, decimalSigFigs); + roundTripOk = recompileOk && deepEqual(result.roundTripOutputs, expected.outputs, decimalSigFigs); } expect(compileOk).toBeTruthy(); diff --git a/test/integration/expression/lib/util.ts b/test/integration/expression/lib/util.ts index 79cb81decd..667ca3da6a 100644 --- a/test/integration/expression/lib/util.ts +++ b/test/integration/expression/lib/util.ts @@ -13,59 +13,3 @@ export function stringify(v) { } return s; } - -const decimalSigFigs = 6; - -export 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; - } -} - -export function deepEqual(a, b): boolean { - 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; -} diff --git a/test/integration/lib/json-diff.ts b/test/integration/lib/json-diff.ts index dab36244f5..86784f3999 100644 --- a/test/integration/lib/json-diff.ts +++ b/test/integration/lib/json-diff.ts @@ -12,11 +12,12 @@ export function generateDiffLog(expected, actual) { }).join(''); } -export function deepEqual(a, b): boolean { +export function deepEqual(a, b, decimalSigFigs = 10): boolean { if (typeof a !== typeof b) return false; - if (typeof a === 'number') - return Math.abs(a - b) < 1e-10; + if (typeof a === 'number') { + return stripPrecision(a, decimalSigFigs) === stripPrecision(b, decimalSigFigs); + } if (a === null || typeof a !== 'object') return a === b; @@ -30,8 +31,37 @@ export function deepEqual(a, b): boolean { kb.sort(); for (let i = 0; i < ka.length; i++) - if (ka[i] !== kb[i] || !deepEqual(a[ka[i]], b[ka[i]])) + if (ka[i] !== kb[i] || !deepEqual(a[ka[i]], b[ka[i]], decimalSigFigs)) return false; return true; } + +export function stripPrecision(x, decimalSigFigs = 10) { + // 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; + } +} From b8c2926dd2ff018de4e4cf90975e7790005e2de5 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 8 Feb 2022 14:23:41 +0100 Subject: [PATCH 14/14] Add expression fixture type structure --- test/integration/expression/expression.test.ts | 3 ++- test/integration/expression/fixture-types.ts | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 test/integration/expression/fixture-types.ts diff --git a/test/integration/expression/expression.test.ts b/test/integration/expression/expression.test.ts index 87774284ef..331bba72c0 100644 --- a/test/integration/expression/expression.test.ts +++ b/test/integration/expression/expression.test.ts @@ -9,6 +9,7 @@ import {CanonicalTileID} from '../../../src/source/tile_id'; import {getGeometry} from './lib/geometry'; import {stringify} from './lib/util'; import {deepEqual, stripPrecision} from '../lib/json-diff'; +import {ExpressionFixture} from './fixture-types'; const decimalSigFigs = 6; @@ -112,7 +113,7 @@ function evaluateFixture(fixture) { return result; } -function evaluateExpression (fixture, expression, compilationResult) { +function evaluateExpression (fixture: ExpressionFixture, expression, compilationResult) { let availableImages; let canonical; diff --git a/test/integration/expression/fixture-types.ts b/test/integration/expression/fixture-types.ts new file mode 100644 index 0000000000..c6fa9effa7 --- /dev/null +++ b/test/integration/expression/fixture-types.ts @@ -0,0 +1,15 @@ +export type ExpressionFixture = { + expression: any[]; + inputs:any[]; + expected: { + compiled?: { + result?: any; + isFeatureConstant?: any; + isZoomConstant?: any; + type?: any; + }; + outputs? : any; + serialized?: any; + }; +} +