Add a minimally working test infrastructure
This commit is contained in:
parent
3dcf91c520
commit
608728ade9
10 changed files with 802 additions and 37 deletions
137
package-lock.json
generated
137
package-lock.json
generated
|
@ -403,6 +403,14 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.11.tgz",
|
||||
"integrity": "sha512-lCvvI24L21ZVeIiyIUHZ5Oflv1hhHQ5E1S25IRlKIXaRkVgmXpJMI3wUJkmym2bTbCe+WoIibQnMVAU3FguaOg=="
|
||||
},
|
||||
"@types/ora": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ora/-/ora-3.2.0.tgz",
|
||||
"integrity": "sha512-jll99xUKpiFbIFZSQcxm4numfsLaOWBzWNaRk3PvTSE7BPqTzzOCFmS0mQ7m8qkTfmYhuYbehTGsxkvRLPC++w==",
|
||||
"requires": {
|
||||
"ora": "*"
|
||||
}
|
||||
},
|
||||
"@types/semver": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.2.0.tgz",
|
||||
|
@ -656,7 +664,6 @@
|
|||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^1.9.0"
|
||||
}
|
||||
|
@ -1211,6 +1218,19 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
|
||||
"requires": {
|
||||
"restore-cursor": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"cli-spinners": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.3.0.tgz",
|
||||
"integrity": "sha512-Xs2Hf2nzrvJMFKimOR7YR0QwZ8fc0u98kdtwN1eNAZzNQgH3vK2pXzff6GJtKh7S5hoJ87ECiAiZFS2fb5Ii2w=="
|
||||
},
|
||||
"cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
|
@ -1221,6 +1241,11 @@
|
|||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"clone": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
|
||||
"integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4="
|
||||
},
|
||||
"collection-visit": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
|
||||
|
@ -1235,7 +1260,6 @@
|
|||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
|
@ -1243,8 +1267,7 @@
|
|||
"color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
|
||||
"dev": true
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
|
@ -1576,6 +1599,14 @@
|
|||
"type-detect": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
|
||||
"integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
|
||||
"requires": {
|
||||
"clone": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"define-properties": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
|
||||
|
@ -1802,8 +1833,7 @@
|
|||
"escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
|
||||
"dev": true
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
||||
},
|
||||
"eslint-scope": {
|
||||
"version": "4.0.3",
|
||||
|
@ -2273,8 +2303,7 @@
|
|||
"has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
|
||||
"dev": true
|
||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
|
||||
},
|
||||
"has-symbols": {
|
||||
"version": "1.0.1",
|
||||
|
@ -2615,6 +2644,11 @@
|
|||
"is-extglob": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"is-interactive": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
|
||||
"integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="
|
||||
},
|
||||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
|
@ -2798,7 +2832,6 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
|
||||
"integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^2.4.2"
|
||||
},
|
||||
|
@ -2807,7 +2840,6 @@
|
|||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
|
@ -2818,7 +2850,6 @@
|
|||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
|
@ -3055,8 +3086,7 @@
|
|||
"mimic-fn": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
|
||||
},
|
||||
"minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
|
@ -3320,6 +3350,11 @@
|
|||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
|
||||
"dev": true
|
||||
},
|
||||
"mute-stream": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
|
||||
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
|
||||
},
|
||||
"nanomatch": {
|
||||
"version": "1.2.13",
|
||||
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
|
||||
|
@ -3556,6 +3591,62 @@
|
|||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"onetime": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
|
||||
"integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
|
||||
"requires": {
|
||||
"mimic-fn": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"ora": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/ora/-/ora-4.0.4.tgz",
|
||||
"integrity": "sha512-77iGeVU1cIdRhgFzCK8aw1fbtT1B/iZAvWjS+l/o1x0RShMgxHUZaD2yDpWsNCPwXg9z1ZA78Kbdvr8kBmG/Ww==",
|
||||
"requires": {
|
||||
"chalk": "^3.0.0",
|
||||
"cli-cursor": "^3.1.0",
|
||||
"cli-spinners": "^2.2.0",
|
||||
"is-interactive": "^1.0.0",
|
||||
"log-symbols": "^3.0.0",
|
||||
"mute-stream": "0.0.8",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wcwidth": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
|
||||
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
|
||||
"requires": {
|
||||
"@types/color-name": "^1.1.1",
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
|
||||
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"os-browserify": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
|
||||
|
@ -3992,6 +4083,15 @@
|
|||
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
|
||||
"dev": true
|
||||
},
|
||||
"restore-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
|
||||
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
|
||||
"requires": {
|
||||
"onetime": "^5.1.0",
|
||||
"signal-exit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"ret": {
|
||||
"version": "0.1.15",
|
||||
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
|
||||
|
@ -4134,8 +4234,7 @@
|
|||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
|
||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
|
||||
},
|
||||
"snapdragon": {
|
||||
"version": "0.8.2",
|
||||
|
@ -5135,6 +5234,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"wcwidth": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
|
||||
"integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=",
|
||||
"requires": {
|
||||
"defaults": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"webpack": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-4.43.0.tgz",
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
"scripts": {
|
||||
"watch": "webpack --watch --config webpack.dev.js",
|
||||
"prepare": "webpack --config webpack.dev.js",
|
||||
"test": "mocha lib/test"
|
||||
"test": "node build/bin/bolt-test.js",
|
||||
"update-lkg": "node build/bin/bolt-test.js snapshot lkg"
|
||||
},
|
||||
"author": "Sam Vervaeck <vervaeck.sam@skynet.be>",
|
||||
"license": "GPL-3.0",
|
||||
|
@ -21,6 +22,7 @@
|
|||
"@types/microtime": "^2.1.0",
|
||||
"@types/minimist": "^1.2.0",
|
||||
"@types/node": "^14.0.11",
|
||||
"@types/ora": "^3.2.0",
|
||||
"@types/semver": "^7.2.0",
|
||||
"@types/yargs": "^15.0.5",
|
||||
"chalk": "^4.0.0",
|
||||
|
@ -30,6 +32,7 @@
|
|||
"microtime": "^3.0.0",
|
||||
"minimist": "^1.2.5",
|
||||
"moment": "^2.26.0",
|
||||
"ora": "^4.0.4",
|
||||
"pegjs": "^0.11.0-master.b7b87ea",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"semver": "^7.3.2",
|
||||
|
|
370
src/bin/bolt-test.ts
Normal file
370
src/bin/bolt-test.ts
Normal file
|
@ -0,0 +1,370 @@
|
|||
|
||||
import "source-map-support/register"
|
||||
|
||||
import * as fs from "fs-extra"
|
||||
import * as path from "path"
|
||||
import * as crypto from "crypto"
|
||||
|
||||
import yargs from "yargs"
|
||||
import yaml from "js-yaml"
|
||||
import { sync as globSync } from "glob"
|
||||
import ora from "ora"
|
||||
|
||||
import { Parser } from "../parser"
|
||||
import { Scanner } from "../scanner"
|
||||
import { SyntaxKind, Syntax } from "../ast"
|
||||
import { Json, serialize, JsonObject, MapLike, upsearchSync, deepEqual } from "../util"
|
||||
import { DiagnosticIndex } from "../diagnostics"
|
||||
import { TextFile, TextPos, TextSpan } from "../text"
|
||||
|
||||
const PACKAGE_ROOT = path.dirname(upsearchSync('package.json')!);
|
||||
const STORAGE_DIR = path.join(PACKAGE_ROOT, '.test-storage');
|
||||
|
||||
const spinner = ora(`Initializing test session ...`).start();
|
||||
|
||||
function toArray<T>(value: T | T[]): T[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
return value === undefined || value === null ? [] : [ value ]
|
||||
}
|
||||
|
||||
class Test {
|
||||
|
||||
public key: string;
|
||||
|
||||
public result?: Json;
|
||||
public error: Error | null = null;
|
||||
|
||||
constructor(
|
||||
public readonly span: TextSpan,
|
||||
public readonly type: string,
|
||||
public readonly text: string,
|
||||
public readonly data: JsonObject,
|
||||
) {
|
||||
this.key = hash([text, data]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface LoadTestsOptions {
|
||||
include: string[];
|
||||
exclude: string[];
|
||||
}
|
||||
|
||||
class TestSession {
|
||||
|
||||
private failCount = 0;
|
||||
|
||||
public key: string;
|
||||
|
||||
constructor(private tests: Test[] = []) {
|
||||
this.key = `${Date.now().toString()}-${Math.random()}`;
|
||||
}
|
||||
|
||||
public getAllTests() {
|
||||
return this.tests[Symbol.iterator]();;
|
||||
}
|
||||
|
||||
public scanForTests(options?: LoadTestsOptions) {
|
||||
const includes = options?.include ?? ['test/**/*.md'];
|
||||
const excludes = options?.exclude ?? [];
|
||||
spinner.text = 'Scanning for tests [0 found]';
|
||||
for (const include of includes) {
|
||||
for (const filepath of globSync(include, { ignore: excludes })) {
|
||||
spinner.info(`Found file ${filepath}`)
|
||||
for (const test of loadTests(filepath)) {
|
||||
this.tests.push(test);
|
||||
spinner.text = `Scanning for tests [${this.tests.length} found]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public run() {
|
||||
let i = 1;
|
||||
//let failed = [];
|
||||
for (const test of this.tests) {
|
||||
spinner.text = `Running tests [${i}/${this.tests.length}]`
|
||||
const runner = TEST_RUNNERS[test.type]
|
||||
if (runner === undefined) {
|
||||
spinner.warn(`Test runner '${test.type}' not found.`)
|
||||
continue;
|
||||
}
|
||||
let result;
|
||||
try {
|
||||
test.result = runner(test);
|
||||
} catch (e) {
|
||||
test.error = e;
|
||||
this.failCount++;
|
||||
//failed.push(test);
|
||||
spinner.warn(`The following test from ${path.relative(process.cwd(), test.span.file.fullPath)} failed with "${e.message}":\n\n${test.text}\n`)
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (this.failCount > 0) {
|
||||
spinner.fail(`${this.failCount} tests failed.`)
|
||||
}
|
||||
}
|
||||
|
||||
public save() {
|
||||
fs.mkdirpSync(path.join(STORAGE_DIR, 'snapshots', this.key))
|
||||
for (const test of this.tests) {
|
||||
fs.writeFileSync(path.join(STORAGE_DIR, 'snapshots', this.key, test.key), JSON.stringify(test.result), 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
public hasFailedTests() {
|
||||
return this.failCount > 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function compare(actual: string, expected: string) {
|
||||
for (const testKey of fs.readdirSync(path.join(STORAGE_DIR, 'snapshots', actual))) {
|
||||
const actualTestResult = readJson(path.join(STORAGE_DIR, 'snapshots', actual, testKey))!;
|
||||
const expectedTestResult = readJson(path.join(STORAGE_DIR, 'snapshots', expected, testKey));
|
||||
if (!deepEqual(actualTestResult, expectedTestResult)) {
|
||||
spinner.warn(`Test ${testKey} does not compare to its expected value.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isWhiteSpace(ch: string): boolean {
|
||||
return /[\n\r\t ]/.test(ch);
|
||||
}
|
||||
|
||||
interface TestFileMetadata {
|
||||
type: string;
|
||||
expect: string;
|
||||
}
|
||||
|
||||
function* loadTests(filepath: string): IterableIterator<Test> {
|
||||
|
||||
const file = new TextFile(filepath);
|
||||
const contents = file.getText('utf8');
|
||||
|
||||
let i = 0;
|
||||
let column = 1
|
||||
let line = 1;
|
||||
let atNewLine = true;
|
||||
|
||||
assertText('---');
|
||||
let yamlStr = '';
|
||||
i += 3;
|
||||
while (!lookaheadEquals('---')) {
|
||||
yamlStr += contents[i++];
|
||||
}
|
||||
i += 3;
|
||||
const metadata = yaml.safeLoad(yamlStr);
|
||||
|
||||
while (i < contents.length) {
|
||||
skipWhiteSpace();
|
||||
if (atNewLine && column >= 5) {
|
||||
const startPos = new TextPos(i, line, column);
|
||||
const text = scanCodeBlock();
|
||||
const endPos = new TextPos(i, line, column);
|
||||
if (metadata['split-lines']) {
|
||||
for (const line of text.split('\n')) {
|
||||
if (line.trim() !== '') {
|
||||
yield new Test(new TextSpan(file, startPos.clone(), endPos), metadata.type, line, metadata);
|
||||
startPos.advance(line);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
yield new Test(new TextSpan(file, startPos, endPos), metadata.type, text, metadata);
|
||||
}
|
||||
} else {
|
||||
getChar();
|
||||
}
|
||||
}
|
||||
|
||||
function getChar() {
|
||||
const ch = contents[i++];
|
||||
if (ch === '\n') {
|
||||
column = 1;
|
||||
line++;
|
||||
atNewLine = true;
|
||||
} else {
|
||||
if (!isEmpty(ch)) {
|
||||
atNewLine = false;
|
||||
}
|
||||
column++;
|
||||
}
|
||||
return ch;
|
||||
}
|
||||
|
||||
function assertText(str: string) {
|
||||
for (let k = 0; k < str.length; k++) {
|
||||
if (contents[i+k] !== str[k]) {
|
||||
throw new Error(`Expected '${str}' but got ${contents.substr(i, i+str.length)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isEmpty(ch: string): boolean {
|
||||
return /[\t ]/.test(ch);
|
||||
}
|
||||
|
||||
function lookaheadEquals(str: string): boolean {
|
||||
for (let k = 0; k < str.length; k++) {
|
||||
if (contents[i+k] !== str[k]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function scanCodeBlock() {
|
||||
let out = ''
|
||||
while (i < contents.length) {
|
||||
const ch = getChar();
|
||||
if (ch === '\n') {
|
||||
out += ch;
|
||||
skipWhiteSpace();
|
||||
continue;
|
||||
}
|
||||
if (column < 5) {
|
||||
break;
|
||||
}
|
||||
out += ch;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function skipWhiteSpace() {
|
||||
takeWhile(isWhiteSpace)
|
||||
}
|
||||
|
||||
function takeWhile(pred: (ch: string) => boolean) {
|
||||
let out = '';
|
||||
while (true) {
|
||||
const c0 = contents[i];
|
||||
if (!pred(c0)) {
|
||||
break
|
||||
}
|
||||
out += getChar();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function readJson(filename: string): Json | null {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filename, 'utf8'));
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
return null
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function hash(value: Json) {
|
||||
const hasher = crypto.createHash('sha256');
|
||||
hasher.update(JSON.stringify(value));
|
||||
return hasher.digest('hex');
|
||||
}
|
||||
|
||||
type TestRunner = (test: Test) => Json;
|
||||
|
||||
const TEST_RUNNERS: MapLike<TestRunner> = {
|
||||
|
||||
scan(test: Test): Json {
|
||||
const diagnostics = new DiagnosticIndex;
|
||||
const scanner = new Scanner(test.span.file, test.text, test.span.start);
|
||||
const tokens = []
|
||||
while (true) {
|
||||
const token = scanner.scan();
|
||||
if (token.kind === SyntaxKind.EndOfFile) {
|
||||
break;
|
||||
}
|
||||
tokens.push(token);
|
||||
}
|
||||
return serialize({
|
||||
diagnostics: [...diagnostics.getAllDiagnostics()],
|
||||
tokens,
|
||||
});
|
||||
},
|
||||
|
||||
parse(test: Test): Json {
|
||||
const kind = test.data.expect ?? 'SourceFile';
|
||||
const diagnostics = new DiagnosticIndex;
|
||||
const parser = new Parser();
|
||||
const tokens = new Scanner(test.span.file, test.text);
|
||||
let results: Syntax[];
|
||||
switch (kind) {
|
||||
case 'SourceFile':
|
||||
results = [ parser.parseSourceFile(tokens) ];
|
||||
break;
|
||||
case 'SourceElements':
|
||||
results = parser.parseSourceElements(tokens);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`I did not know how to parse ${kind}`)
|
||||
}
|
||||
return serialize({
|
||||
diagnostics: [...diagnostics.getAllDiagnostics()],
|
||||
results,
|
||||
})
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
yargs
|
||||
.command(['$0 [pattern..]', 'run [pattern..]'], 'Run all tests on the current version of the compiler',
|
||||
yargs => yargs
|
||||
.array('pattern')
|
||||
.describe('pattern', 'Only run the tests matching the given pattern')
|
||||
.array('include')
|
||||
.describe('include', 'Files to scan for tests')
|
||||
.default('include', ['test/**/*.md'])
|
||||
.array('exclude')
|
||||
.describe('exclude', 'Files to never scan for tests')
|
||||
.default('exclude', [])
|
||||
, args => {
|
||||
|
||||
const session = new TestSession();
|
||||
session.scanForTests(args as LoadTestsOptions);
|
||||
session.run();
|
||||
session.save();
|
||||
|
||||
if (session.hasFailedTests()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expectedKey = fs.readFileSync(path.join(STORAGE_DIR, 'aliases', 'lkg'), 'utf8')
|
||||
compare(session.key, expectedKey)
|
||||
|
||||
}
|
||||
)
|
||||
.command(['snapshot [name]'], 'Create a new snapshot from the output of the current compiler',
|
||||
yargs => yargs
|
||||
.array('include')
|
||||
.describe('include', 'Files to scan for tests')
|
||||
.default('include', ['test/**/*.md'])
|
||||
.array('exclude')
|
||||
.describe('exclude', 'Files to never scan for tests')
|
||||
.default('exclude', [])
|
||||
, args => {
|
||||
|
||||
// Load and run all tests, saving the results to disk
|
||||
const session = new TestSession();
|
||||
session.scanForTests(args as LoadTestsOptions);
|
||||
session.run();
|
||||
session.save();
|
||||
|
||||
// Set the tests that we have run as being the new 'lkg'
|
||||
fs.mkdirpSync(path.join(STORAGE_DIR, 'aliases'));
|
||||
fs.writeFileSync(path.join(STORAGE_DIR, 'aliases', 'lkg'), session.key, 'utf8')
|
||||
}
|
||||
)
|
||||
.command('inspect <pattern..>', 'Inspect the ouput of a given test',
|
||||
yargs => yargs
|
||||
, args => {
|
||||
}
|
||||
)
|
||||
.version()
|
||||
.help()
|
||||
.argv;
|
|
@ -52,6 +52,7 @@ import {
|
|||
createBoltExMark,
|
||||
createBoltWhereKeyword,
|
||||
} from "./ast"
|
||||
import { outputFile } from "fs-extra";
|
||||
|
||||
export enum PunctType {
|
||||
Paren,
|
||||
|
@ -111,11 +112,11 @@ function isNewLine(ch: string) {
|
|||
}
|
||||
|
||||
function isIdentStart(ch: string) {
|
||||
return /[_\p{L}]/u.test(ch)
|
||||
return /[_\p{ID_Start}]/u.test(ch)
|
||||
}
|
||||
|
||||
function isIdentPart(ch: string) {
|
||||
return /[_\p{L}\p{Nd}]/u.test(ch)
|
||||
return /[_\p{ID_Continue}]/u.test(ch)
|
||||
}
|
||||
|
||||
function isSymbol(ch: string) {
|
||||
|
@ -181,6 +182,86 @@ export class Scanner {
|
|||
return text;
|
||||
}
|
||||
|
||||
private scanHexDigit(): number {
|
||||
const c0 = this.peekChar();
|
||||
let out;
|
||||
switch (c0) {
|
||||
case '0': out = 0; break;
|
||||
case '1': out = 1; break;
|
||||
case '2': out = 2; break;
|
||||
case '3': out = 3; break;
|
||||
case '4': out = 4; break;
|
||||
case '5': out = 5; break;
|
||||
case '6': out = 6; break;
|
||||
case '7': out = 7; break;
|
||||
case '8': out = 8; break;
|
||||
case '9': out = 9; break;
|
||||
case 'a': out = 10; break;
|
||||
case 'b': out = 11; break;
|
||||
case 'c': out = 12; break;
|
||||
case 'd': out = 13; break;
|
||||
case 'e': out = 14; break;
|
||||
case 'f': out = 15; break;
|
||||
case 'A': out = 10; break;
|
||||
case 'B': out = 11; break;
|
||||
case 'C': out = 12; break;
|
||||
case 'D': out = 13; break;
|
||||
case 'E': out = 14; break;
|
||||
case 'F': out = 15; break;
|
||||
default:
|
||||
throw new ScanError(this.file, this.currPos.clone(), c0);
|
||||
}
|
||||
this.getChar();
|
||||
return out;
|
||||
}
|
||||
|
||||
private scanEscapeSequence(): string {
|
||||
this.assertChar('\\')
|
||||
const c0 = this.peekChar();
|
||||
switch (c0) {
|
||||
case 'a':
|
||||
this.getChar();
|
||||
return '\n'
|
||||
case 'b':
|
||||
this.getChar();
|
||||
return '\b'
|
||||
case 'f':
|
||||
this.getChar();
|
||||
return '\f'
|
||||
case 'n':
|
||||
this.getChar();
|
||||
return '\n'
|
||||
case 'r':
|
||||
this.getChar();
|
||||
return '\r'
|
||||
case 't':
|
||||
this.getChar();
|
||||
return '\t'
|
||||
case 'v':
|
||||
this.getChar();
|
||||
return '\v'
|
||||
case '0':
|
||||
this.getChar();
|
||||
return '\0'
|
||||
case 'u':
|
||||
{
|
||||
const d0 = this.scanHexDigit();
|
||||
const d1 = this.scanHexDigit();
|
||||
const d2 = this.scanHexDigit();
|
||||
const d3 = this.scanHexDigit();
|
||||
return String.fromCharCode(d0 * (16 ** 3) + d1 * (16 ** 2) + d2 * 16 + d3);
|
||||
}
|
||||
case 'x':
|
||||
{
|
||||
const d0 = this.scanHexDigit();
|
||||
const d1 = this.scanHexDigit();
|
||||
return String.fromCharCode(d0 * 16 + d1);
|
||||
}
|
||||
default:
|
||||
throw new ScanError(this.file, this.currPos.clone(), c0);
|
||||
}
|
||||
}
|
||||
|
||||
public scan(): BoltToken {
|
||||
|
||||
while (true) {
|
||||
|
@ -227,15 +308,17 @@ export class Scanner {
|
|||
let text = ''
|
||||
|
||||
while (true) {
|
||||
const c1 = this.getChar();
|
||||
const c1 = this.peekChar();
|
||||
if (c1 === EOF) {
|
||||
throw new ScanError(this.file, this.currPos.clone(), EOF);
|
||||
}
|
||||
if (c1 === '"') {
|
||||
this.getChar();
|
||||
break;
|
||||
} else if (c1 === '\\') {
|
||||
this.scanEscapeSequence()
|
||||
text += this.scanEscapeSequence()
|
||||
} else {
|
||||
this.getChar();
|
||||
text += c1
|
||||
}
|
||||
}
|
||||
|
@ -246,7 +329,7 @@ export class Scanner {
|
|||
|
||||
} else if (isDigit(c0)) {
|
||||
|
||||
const digits = this.takeWhile(isDigit)
|
||||
const digits = this.takeWhile(isDigit);
|
||||
const endPos = this.currPos.clone();
|
||||
return createBoltIntegerLiteral(BigInt(digits), new TextSpan(this.file, startPos, endPos));
|
||||
|
||||
|
|
37
src/text.ts
37
src/text.ts
|
@ -1,6 +1,7 @@
|
|||
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import { serializeTag, serialize } from "./util";
|
||||
|
||||
export class TextFile {
|
||||
|
||||
|
@ -14,11 +15,15 @@ export class TextFile {
|
|||
return path.resolve(this.origPath)
|
||||
}
|
||||
|
||||
public getText(): string {
|
||||
[serializeTag]() {
|
||||
return this.origPath;
|
||||
}
|
||||
|
||||
public getText(encoding: BufferEncoding = 'utf8'): string {
|
||||
if (this.cachedText !== null) {
|
||||
return this.cachedText;
|
||||
}
|
||||
const text = fs.readFileSync(this.fullPath, 'utf8');
|
||||
const text = fs.readFileSync(this.fullPath, encoding);
|
||||
this.cachedText = text;
|
||||
return text
|
||||
}
|
||||
|
@ -39,6 +44,26 @@ export class TextPos {
|
|||
return new TextPos(this.offset, this.line, this.column)
|
||||
}
|
||||
|
||||
[serializeTag]() {
|
||||
return {
|
||||
offset: this.offset,
|
||||
line: this.line,
|
||||
column: this.column,
|
||||
}
|
||||
}
|
||||
|
||||
public advance(str: string) {
|
||||
for (const ch of str) {
|
||||
if (ch === '\n') {
|
||||
this.line++;
|
||||
this.column = 1;
|
||||
} else {
|
||||
this.column++;
|
||||
}
|
||||
this.offset++;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class TextSpan {
|
||||
|
@ -55,5 +80,13 @@ export class TextSpan {
|
|||
return new TextSpan(this.file, this.start.clone(), this.end.clone());
|
||||
}
|
||||
|
||||
[serializeTag]() {
|
||||
return {
|
||||
file: serialize(this.file),
|
||||
start: serialize(this.start),
|
||||
end: serialize(this.end),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
60
src/util.ts
60
src/util.ts
|
@ -6,14 +6,12 @@ import * as os from "os"
|
|||
import moment from "moment"
|
||||
import chalk from "chalk"
|
||||
import { LOG_DATETIME_FORMAT } from "./constants"
|
||||
import { E_CANDIDATE_FUNCTION_REQUIRES_THIS_PARAMETER } from "./diagnostics"
|
||||
import { isPrimitive } from "util"
|
||||
|
||||
export function isPowerOf(x: number, n: number):boolean {
|
||||
const a = Math.log(x) / Math.log(n);
|
||||
return Math.pow(a, n) == x;
|
||||
}
|
||||
|
||||
|
||||
export function some<T>(iterator: Iterator<T>, pred: (value: T) => boolean): boolean {
|
||||
while (true) {
|
||||
const { value, done } = iterator.next();
|
||||
|
@ -194,15 +192,31 @@ export type Newable<T> = {
|
|||
new(...args: any): T;
|
||||
}
|
||||
|
||||
export function isPrimitive(value: any): boolean {
|
||||
return (typeof(value) !== 'function' && typeof(value) !== 'object') || value === null;
|
||||
}
|
||||
|
||||
export const serializeTag = Symbol('serializer tag');
|
||||
|
||||
const serializableClasses = new Map<string, Newable<{ [serializeTag](): Json }>>();
|
||||
|
||||
export function serialize(value: any): Json {
|
||||
if (isPrimitive(value)) {
|
||||
if (typeof(value) === 'bigint') {
|
||||
return {
|
||||
type: 'bigint',
|
||||
value: value.toString(),
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: 'primitive',
|
||||
value,
|
||||
}
|
||||
}
|
||||
} else if (Array.isArray(value)) {
|
||||
return {
|
||||
type: 'primitive',
|
||||
value,
|
||||
type: 'array',
|
||||
elements: value.map(serialize)
|
||||
}
|
||||
} else if (isObjectLike(value)) {
|
||||
if (isPlainObject(value)) {
|
||||
|
@ -219,13 +233,9 @@ export function serialize(value: any): Json {
|
|||
data: value[serializeTag](),
|
||||
}
|
||||
} else {
|
||||
console.log(value);
|
||||
throw new Error(`Could not serialize ${value}: it was a non-primitive object and has no serializer tag.`)
|
||||
}
|
||||
} else if (Array.isArray(value)) {
|
||||
return {
|
||||
type: 'array',
|
||||
elements: value.map(serialize)
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Could not serialize ${value}: is was not recognised as a primitive type, an object, a class instance, or an array.`)
|
||||
}
|
||||
|
@ -503,8 +513,8 @@ export function upsearchSync(filename: string, startDir = '.') {
|
|||
if (fs.existsSync(filePath)) {
|
||||
return filePath
|
||||
}
|
||||
const { root, dir } = path.parse(currDir);
|
||||
if (dir === root) {
|
||||
const { root, dir } = path.parse(currDir);
|
||||
if (currDir === root) {
|
||||
return null;
|
||||
}
|
||||
currDir = dir;
|
||||
|
@ -638,3 +648,29 @@ export function format(message: string, data: MapLike<FormatArg>) {
|
|||
|
||||
}
|
||||
|
||||
export function deepEqual(a: any, b: any): boolean {
|
||||
if (isPrimitive(a) && isPrimitive(b)) {
|
||||
return a === b;
|
||||
} else if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (!deepEqual(a[i], b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else if (isPlainObject(a) && isPlainObject(b)) {
|
||||
const unmarked = new Set(Object.keys(b));
|
||||
for (const key of Object.keys(a)) {
|
||||
if (!deepEqual(a[key], b[key])) {
|
||||
return false;
|
||||
}
|
||||
unmarked.delete(key);
|
||||
}
|
||||
return unmarked.size === 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
53
test/scan/000-bolt-identifier.md
Normal file
53
test/scan/000-bolt-identifier.md
Normal file
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
type: scan
|
||||
expect: BoltIdentifier
|
||||
split-lines: true
|
||||
---
|
||||
|
||||
The most simple identifiers are those made out of ASCII letters:
|
||||
|
||||
Foo
|
||||
Bar
|
||||
Baz
|
||||
|
||||
However, they may also contain digits as long as they do not begin with a
|
||||
digit:
|
||||
|
||||
Var1
|
||||
Var2
|
||||
Var10029384
|
||||
|
||||
Identifiers may be as long as you want:
|
||||
|
||||
ThisIsALongAndValidIdentifier
|
||||
ThisIsAnEvenLongButStilCompletelyValidIdentifier
|
||||
|
||||
Moreover, they may have arbitrary underscores (`_`) in their names.
|
||||
|
||||
a_valid_identifier
|
||||
another__0000__valid_identfier
|
||||
_1
|
||||
__2
|
||||
___3
|
||||
|
||||
They may even be nothing more than underscores:
|
||||
|
||||
_
|
||||
__
|
||||
___
|
||||
|
||||
All identifiers starting with a `ID_Start` character are valid identifiers,
|
||||
including `Other_ID_Start`:
|
||||
|
||||
℘rototype
|
||||
℮llipsis
|
||||
|
||||
Likewise, the following code points using `Other_ID_Continue` are also valid:
|
||||
|
||||
α·β
|
||||
ano·teleia
|
||||
|
||||
And, of course, the combination of `ID_Start` and `ID_Continue`:
|
||||
|
||||
alfa·beta
|
||||
|
20
test/scan/001-bolt-string-literal.md
Normal file
20
test/scan/001-bolt-string-literal.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
type: scan
|
||||
expect: BoltStringLiteral
|
||||
split-lines: true
|
||||
---
|
||||
|
||||
A string may hold arbirary ASCII characters, including spaces:
|
||||
|
||||
"Foo!"
|
||||
"Once upon a time ..."
|
||||
|
||||
Special ASCII characters have no effect, other than that they are appended to
|
||||
the contents of the string:
|
||||
|
||||
"S+me w3!rd @SCII ch@r$"
|
||||
|
||||
Some special escape sequences:
|
||||
|
||||
"\n\r"
|
||||
"\n"
|
57
test/scan/002-bolt-integer-literal.md
Normal file
57
test/scan/002-bolt-integer-literal.md
Normal file
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
type: scan
|
||||
expect: BoltIntegerLiteral
|
||||
split-lines: true
|
||||
---
|
||||
|
||||
All decimal digits are valid integers.
|
||||
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
0
|
||||
|
||||
Any combination of decimal digits are valid integers, including integers
|
||||
prefixed with an abirary amount of zeroes.
|
||||
|
||||
12345
|
||||
99
|
||||
10
|
||||
01
|
||||
000
|
||||
0010
|
||||
|
||||
In binary mode, integers are read in base-2.
|
||||
|
||||
0b0
|
||||
0b1
|
||||
0b10010
|
||||
0b00100
|
||||
0b00000
|
||||
|
||||
This means the following expressions are invalid in binary mode:
|
||||
|
||||
0b20001
|
||||
0b12345
|
||||
0b00003
|
||||
|
||||
In octal mode, integers are read in base-8.
|
||||
|
||||
0o0
|
||||
0o00000
|
||||
0o007
|
||||
0o706
|
||||
0o12345
|
||||
|
||||
This means the following expressions are invalid in octal mode:
|
||||
|
||||
0o8
|
||||
0o9
|
||||
0o123456789
|
||||
|
|
@ -5,9 +5,12 @@ const path = require("path");
|
|||
module.exports = {
|
||||
target: 'node',
|
||||
mode: 'development',
|
||||
entry: './src/bin/bolt.ts',
|
||||
entry: {
|
||||
'bolt': './src/bin/bolt.ts',
|
||||
'bolt-test': './src/bin/bolt-test.ts',
|
||||
},
|
||||
output: {
|
||||
filename: 'bin/bolt.js',
|
||||
filename: 'bin/[name].js',
|
||||
path: path.resolve(__dirname, 'build'),
|
||||
},
|
||||
resolve: {
|
||||
|
|
Loading…
Reference in a new issue