Upgrade test infrastructure

- Enhance (de)serialization logic and slim down generated JSON
 - Add more commands to `bolt-test` and improve support for aliases
This commit is contained in:
Sam Vervaeck 2020-06-07 08:36:15 +02:00
parent 608728ade9
commit a86324a3c9
9 changed files with 306 additions and 116 deletions

10
package-lock.json generated
View file

@ -419,6 +419,11 @@
"@types/node": "*"
}
},
"@types/uuid": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.0.0.tgz",
"integrity": "sha512-xSQfNcvOiE5f9dyd4Kzxbof1aTrLobL278pGLKOZI6esGfZ7ts9Ka16CzIN6Y8hFHE1C7jIBZokULhK1bOgjRw=="
},
"@types/yargs": {
"version": "15.0.5",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz",
@ -4976,6 +4981,11 @@
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
},
"uuid": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz",
"integrity": "sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg=="
},
"v8-compile-cache": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz",

View file

@ -10,7 +10,7 @@
"watch": "webpack --watch --config webpack.dev.js",
"prepare": "webpack --config webpack.dev.js",
"test": "node build/bin/bolt-test.js",
"update-lkg": "node build/bin/bolt-test.js snapshot lkg"
"update-lkg": "node build/bin/bolt-test.js create-snapshot lkg"
},
"author": "Sam Vervaeck <vervaeck.sam@skynet.be>",
"license": "GPL-3.0",
@ -24,6 +24,7 @@
"@types/node": "^14.0.11",
"@types/ora": "^3.2.0",
"@types/semver": "^7.2.0",
"@types/uuid": "^8.0.0",
"@types/yargs": "^15.0.5",
"chalk": "^4.0.0",
"fs-extra": "^9.0.1",
@ -37,6 +38,7 @@
"reflect-metadata": "^0.1.13",
"semver": "^7.3.2",
"source-map-support": "^0.5.19",
"uuid": "^8.1.0",
"yargs": "^15.3.1"
},
"devDependencies": {

View file

@ -4,7 +4,7 @@ import { TextSpan } from "./text"
import { Value } from "./evaluator"
import { Package } from "./package"
import { Diagnostic } from "./diagnostics";
import { serializeTag, serialize, JsonObject } from "./util";
import { serializeTag, serialize } from "./util";
let nextNodeId = 1;
@ -31,13 +31,14 @@ export abstract class Syntax {
}
[serializeTag]() {
const result: JsonObject = {};
const result: any[] = [];
for (const key of Object.keys(this)) {
if (key === 'parentNode' || key === 'errors' || key === 'type' || key === 'id') {
continue;
}
result[key] = serialize((this as any)[key]);
result.push((this as any)[key]);
}
result.push(this.span);
return result;
}

View file

@ -8,7 +8,7 @@ import { Package } from "./package";
import { Diagnostic } from "./diagnostics";
import { serializeTag, serialize, JsonObject } from "./util";
import { serializeTag, serialize } from "./util";
let nextNodeId = 1;
@ -27,13 +27,14 @@ export abstract class SyntaxBase {
this.id = nextNodeId++;
}
[serializeTag]() {
const result: JsonObject = {};
const result: any[] = [];
for (const key of Object.keys(this)) {
if (key === 'parentNode' || key === 'errors' || key === 'type' || key === 'id') {
continue;
}
result[key] = serialize((this as any)[key]);
result.push((this as any)[key]);
}
result.push(this.span);
return result;
}
*preorder() {
@ -3253,6 +3254,8 @@ export function kindToString(kind: SyntaxKind): string { if (SyntaxKind[kind] ==
export type Syntax = EndOfFile | BoltStringLiteral | BoltIntegerLiteral | BoltIdentifier | BoltOperator | BoltAssignment | BoltComma | BoltSemi | BoltColon | BoltColonColon | BoltDot | BoltDotDot | BoltRArrow | BoltRArrowAlt | BoltLArrow | BoltEqSign | BoltGtSign | BoltExMark | BoltLtSign | BoltVBar | BoltWhereKeyword | BoltQuoteKeyword | BoltFnKeyword | BoltForeignKeyword | BoltForKeyword | BoltLetKeyword | BoltReturnKeyword | BoltLoopKeyword | BoltYieldKeyword | BoltMatchKeyword | BoltImportKeyword | BoltExportKeyword | BoltPubKeyword | BoltModKeyword | BoltMutKeyword | BoltEnumKeyword | BoltStructKeyword | BoltTypeKeyword | BoltTraitKeyword | BoltImplKeyword | BoltParenthesized | BoltBraced | BoltBracketed | BoltSourceFile | BoltQualName | BoltTypeOfExpression | BoltReferenceTypeExpression | BoltFunctionTypeExpression | BoltLiftedTypeExpression | BoltTypeParameter | BoltBindPattern | BoltTypePattern | BoltExpressionPattern | BoltTuplePatternElement | BoltTuplePattern | BoltRecordFieldPattern | BoltRecordPattern | BoltQuoteExpression | BoltTupleExpression | BoltReferenceExpression | BoltMemberExpression | BoltFunctionExpression | BoltCallExpression | BoltYieldExpression | BoltMatchArm | BoltMatchExpression | BoltCase | BoltCaseExpression | BoltBlockExpression | BoltConstantExpression | BoltReturnStatement | BoltConditionalCase | BoltConditionalStatement | BoltResumeStatement | BoltExpressionStatement | BoltLoopStatement | BoltParameter | BoltModule | BoltFunctionDeclaration | BoltVariableDeclaration | BoltPlainImportSymbol | BoltImportDirective | BoltPlainExportSymbol | BoltExportDirective | BoltTraitDeclaration | BoltImplDeclaration | BoltTypeAliasDeclaration | BoltRecordField | BoltRecordDeclaration | BoltMacroCall | JSIdentifier | JSString | JSInteger | JSFromKeyword | JSReturnKeyword | JSTryKeyword | JSFinallyKeyword | JSCatchKeyword | JSImportKeyword | JSAsKeyword | JSConstKeyword | JSLetKeyword | JSExportKeyword | JSFunctionKeyword | JSWhileKeyword | JSForKeyword | JSOperator | JSCloseBrace | JSCloseBracket | JSCloseParen | JSOpenBrace | JSOpenBracket | JSOpenParen | JSSemi | JSComma | JSDot | JSDotDotDot | JSMulOp | JSAddOp | JSDivOp | JSSubOp | JSLtOp | JSGtOp | JSBOrOp | JSBXorOp | JSBAndOp | JSBNotOp | JSNotOp | JSBindPattern | JSConstantExpression | JSMemberExpression | JSCallExpression | JSBinaryExpression | JSUnaryExpression | JSNewExpression | JSSequenceExpression | JSConditionalExpression | JSLiteralExpression | JSReferenceExpression | JSCatchBlock | JSTryCatchStatement | JSExpressionStatement | JSConditionalCase | JSConditionalStatement | JSReturnStatement | JSParameter | JSImportStarBinding | JSImportAsBinding | JSImportDeclaration | JSFunctionDeclaration | JSArrowFunctionDeclaration | JSLetDeclaration | JSSourceFile;
export const NODE_TYPES = { EndOfFile, BoltStringLiteral, BoltIntegerLiteral, BoltIdentifier, BoltOperator, BoltAssignment, BoltComma, BoltSemi, BoltColon, BoltColonColon, BoltDot, BoltDotDot, BoltRArrow, BoltRArrowAlt, BoltLArrow, BoltEqSign, BoltGtSign, BoltExMark, BoltLtSign, BoltVBar, BoltWhereKeyword, BoltQuoteKeyword, BoltFnKeyword, BoltForeignKeyword, BoltForKeyword, BoltLetKeyword, BoltReturnKeyword, BoltLoopKeyword, BoltYieldKeyword, BoltMatchKeyword, BoltImportKeyword, BoltExportKeyword, BoltPubKeyword, BoltModKeyword, BoltMutKeyword, BoltEnumKeyword, BoltStructKeyword, BoltTypeKeyword, BoltTraitKeyword, BoltImplKeyword, BoltParenthesized, BoltBraced, BoltBracketed, BoltSourceFile, BoltQualName, BoltTypeOfExpression, BoltReferenceTypeExpression, BoltFunctionTypeExpression, BoltLiftedTypeExpression, BoltTypeParameter, BoltBindPattern, BoltTypePattern, BoltExpressionPattern, BoltTuplePatternElement, BoltTuplePattern, BoltRecordFieldPattern, BoltRecordPattern, BoltQuoteExpression, BoltTupleExpression, BoltReferenceExpression, BoltMemberExpression, BoltFunctionExpression, BoltCallExpression, BoltYieldExpression, BoltMatchArm, BoltMatchExpression, BoltCase, BoltCaseExpression, BoltBlockExpression, BoltConstantExpression, BoltReturnStatement, BoltConditionalCase, BoltConditionalStatement, BoltResumeStatement, BoltExpressionStatement, BoltLoopStatement, BoltParameter, BoltModule, BoltFunctionDeclaration, BoltVariableDeclaration, BoltPlainImportSymbol, BoltImportDirective, BoltPlainExportSymbol, BoltExportDirective, BoltTraitDeclaration, BoltImplDeclaration, BoltTypeAliasDeclaration, BoltRecordField, BoltRecordDeclaration, BoltMacroCall, JSIdentifier, JSString, JSInteger, JSFromKeyword, JSReturnKeyword, JSTryKeyword, JSFinallyKeyword, JSCatchKeyword, JSImportKeyword, JSAsKeyword, JSConstKeyword, JSLetKeyword, JSExportKeyword, JSFunctionKeyword, JSWhileKeyword, JSForKeyword, JSOperator, JSCloseBrace, JSCloseBracket, JSCloseParen, JSOpenBrace, JSOpenBracket, JSOpenParen, JSSemi, JSComma, JSDot, JSDotDotDot, JSMulOp, JSAddOp, JSDivOp, JSSubOp, JSLtOp, JSGtOp, JSBOrOp, JSBXorOp, JSBAndOp, JSBNotOp, JSNotOp, JSBindPattern, JSConstantExpression, JSMemberExpression, JSCallExpression, JSBinaryExpression, JSUnaryExpression, JSNewExpression, JSSequenceExpression, JSConditionalExpression, JSLiteralExpression, JSReferenceExpression, JSCatchBlock, JSTryCatchStatement, JSExpressionStatement, JSConditionalCase, JSConditionalStatement, JSReturnStatement, JSParameter, JSImportStarBinding, JSImportAsBinding, JSImportDeclaration, JSFunctionDeclaration, JSArrowFunctionDeclaration, JSLetDeclaration, JSSourceFile };
export enum SyntaxKind {
EndOfFile,
BoltStringLiteral,

View file

@ -1,27 +1,33 @@
import "source-map-support/register"
import "reflect-metadata"
import * as fs from "fs-extra"
import * as path from "path"
import * as crypto from "crypto"
import { v4 as uuidv4 } from "uuid"
import yargs from "yargs"
import yaml from "js-yaml"
import yaml, { FAILSAFE_SCHEMA } 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 { Json, serialize, JsonObject, MapLike, upsearchSync, deepEqual, serializeTag, deserializable, deserialize } from "../util"
import { DiagnosticIndex, DiagnosticPrinter, E_TESTS_DO_NOT_COMPARE, E_INVALID_TEST_COMPARE } 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 diagnostics = new DiagnosticPrinter();
const spinner = ora(`Initializing test session ...`).start();
// TODO move some logic from TestSession to TestSuite
// TODO hash the entire code base and have it serve as a unique key for TestSession
function toArray<T>(value: T | T[]): T[] {
if (Array.isArray(value)) {
return value;
@ -29,6 +35,7 @@ function toArray<T>(value: T | T[]): T[] {
return value === undefined || value === null ? [] : [ value ]
}
@deserializable()
class Test {
public key: string;
@ -45,6 +52,15 @@ class Test {
this.key = hash([text, data]);
}
[serializeTag]() {
return [
this.span,
this.type,
this.text,
this.data,
]
}
}
interface LoadTestsOptions {
@ -52,6 +68,14 @@ interface LoadTestsOptions {
exclude: string[];
}
class TestSuite {
constructor(private tests: Test[]) {
}
}
class TestSession {
private failCount = 0;
@ -59,7 +83,7 @@ class TestSession {
public key: string;
constructor(private tests: Test[] = []) {
this.key = `${Date.now().toString()}-${Math.random()}`;
this.key = uuidv4();
}
public getAllTests() {
@ -108,9 +132,13 @@ class TestSession {
}
public save() {
fs.mkdirpSync(path.join(STORAGE_DIR, 'tests'));
for (const test of this.tests) {
fs.writeFileSync(path.join(STORAGE_DIR, 'tests', test.key), JSON.stringify(serialize(test)), 'utf8');
}
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');
fs.writeFileSync(path.join(STORAGE_DIR, 'snapshots', this.key, test.key), JSON.stringify(serialize(test.result)), 'utf8');
}
}
@ -120,14 +148,24 @@ class TestSession {
}
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 compare(actualKey: string, expectedKey: string) {
for (const testKey of fs.readdirSync(path.join(STORAGE_DIR, 'snapshots', actualKey))) {
const test = deserialize(readJson(path.join(STORAGE_DIR, 'tests', testKey)))
const actualTestData = deserialize(readJson(path.join(STORAGE_DIR, 'snapshots', actualKey, testKey))!);
const expectedTestData = deserialize(readJson(path.join(STORAGE_DIR, 'snapshots', expectedKey, testKey)));
if (!deepEqual(actualTestData.result, expectedTestData.result)) {
diagnostics.add({
message: E_TESTS_DO_NOT_COMPARE,
severity: 'error',
node: test,
})
}
}
}
function isWhiteSpace(ch: string): boolean {
@ -250,9 +288,34 @@ function* loadTests(filepath: string): IterableIterator<Test> {
}
function findSnapshot(name: string): string | null {
// If `name` directly refers to a snapshot, we don't have any more work to do.
if (fs.existsSync(path.join(STORAGE_DIR, 'snapshots', name))) {
return name;
}
// Try to read an alias, returning early if it was indeed found
const ref = tryReadFileSync(path.join(STORAGE_DIR, 'aliases', name));
if (ref !== null) {
return ref;
}
// We don't support any more refs at the moment, so we indicate failure
return null;
}
function readJson(filename: string): Json | null {
const contents = tryReadFileSync(filename);
if (contents === null) {
return null;
}
return JSON.parse(contents);
}
function tryReadFileSync(filename: string, encoding: BufferEncoding = 'utf8'): string | null {
try {
return JSON.parse(fs.readFileSync(filename, 'utf8'));
return fs.readFileSync(filename, encoding);
} catch (e) {
if (e.code === 'ENOENT') {
return null
@ -261,6 +324,16 @@ function readJson(filename: string): Json | null {
}
}
function tryUnlinkSync(filepath: string): void {
try {
fs.unlinkSync(filepath);
} catch (e) {
if (e.code !== 'ENOENT') {
throw e;
}
}
}
function hash(value: Json) {
const hasher = crypto.createHash('sha256');
hasher.update(JSON.stringify(value));
@ -323,6 +396,9 @@ yargs
.array('exclude')
.describe('exclude', 'Files to never scan for tests')
.default('exclude', [])
.array('alias')
.describe('alias', 'Save the test results under the given alias')
.default('alias', [])
, args => {
const session = new TestSession();
@ -334,13 +410,25 @@ yargs
return;
}
const expectedKey = fs.readFileSync(path.join(STORAGE_DIR, 'aliases', 'lkg'), 'utf8')
for (const alias of args.alias) {
fs.mkdirpSync(path.join(STORAGE_DIR, 'aliases'))
fs.writeFileSync(path.join(STORAGE_DIR, 'aliases', alias), session.key, 'utf8')
}
const expectedKey = tryReadFileSync(path.join(STORAGE_DIR, 'aliases', 'lkg'), 'utf8');
if (expectedKey === null) {
spinner.fail(`An alias for 'lkg' was not found.`);
process.exit(1);
}
compare(session.key, expectedKey)
}
)
.command(['snapshot [name]'], 'Create a new snapshot from the output of the current compiler',
.command(['create-snapshot [alias..]'], 'Create a new snapshot from the output of the current compiler',
yargs => yargs
.array('alias')
.describe('alias', 'A user-friendly name to refer to the snapshot.')
.default('alias', [])
.array('include')
.describe('include', 'Files to scan for tests')
.default('include', ['test/**/*.md'])
@ -355,14 +443,53 @@ yargs
session.run();
session.save();
// Set the tests that we have run as being the new 'lkg'
// Add any aliases that might have been requested for this snapshot
fs.mkdirpSync(path.join(STORAGE_DIR, 'aliases'));
fs.writeFileSync(path.join(STORAGE_DIR, 'aliases', 'lkg'), session.key, 'utf8')
for (const alias of args.alias) {
fs.writeFileSync(path.join(STORAGE_DIR, 'aliases', alias), session.key, 'utf8')
}
}
)
.command('inspect <pattern..>', 'Inspect the ouput of a given test',
.command('compare [snapshot-a] [snapshot-b]', 'Compare the output of two given tests',
yargs => yargs
, args => {
const keyA = findSnapshot(args['snapshot-a'] as string);
if (keyA === null) {
spinner.fail(`A test snapshot named '${keyA}' was not found.`)
return 1;
}
const keyB = findSnapshot(args['snapshot-b'] as string);
if (keyB === null) {
spinner.fail(`A test snapshot named '${keyB}' was not found.`)
return 1;
}
compare(keyA, keyB);
}
)
.command( 'clean', 'Clean up test snapshots that are unused',
yargs => yargs
.array('keep')
.default('keep', ['lkg'])
.describe('keep', 'Keep the given aliases and anything they refer to')
, args => {
const snapshotsToKeep = new Set();
for (const alias of fs.readdirSync(path.join(STORAGE_DIR, 'aliases'))) {
if (args.keep.indexOf(alias) !== -1) {
const snapshotKey = tryReadFileSync(path.join(STORAGE_DIR, 'aliases', alias));
if (snapshotKey !== null && !fs.existsSync(path.join(STORAGE_DIR, 'snapshots', snapshotKey))) {
spinner.info(`Removing dangling alias ${alias} because the test snapshot it refers to is missing.`)
tryUnlinkSync(path.join(STORAGE_DIR, 'aliases', alias));
} else {
snapshotsToKeep.add(snapshotKey);
}
}
}
for (const snapshotKey of fs.readdirSync(path.join(STORAGE_DIR, 'snapshots'))) {
if (!(snapshotKey in snapshotsToKeep)) {
fs.removeSync(path.join(STORAGE_DIR, 'snapshots', snapshotKey));
}
}
spinner.succeed('Cleanup complete.')
}
)
.version()

View file

@ -17,11 +17,16 @@ import {
FunctionBodyElement
} from "./ast";
import { BOLT_SUPPORTED_LANGUAGES } from "./constants"
import {FastStringMap, enumOr, escapeChar, assert} from "./util";
import { FastStringMap, enumOr, escapeChar, assert, registerClass, Newable } from "./util";
import { TextSpan, TextPos, TextFile } from "./text";
import { Scanner } from "./scanner";
import { convertNodeToSymbolPath } from "./resolver";
import { TYPE_ERROR_MESSAGES } from "./diagnostics";
import { NODE_TYPES } from "./ast"
for (const key of Object.keys(NODE_TYPES)) {
registerClass((NODE_TYPES as any)[key]);
}
export function getSourceFile(node: Syntax) {
while (true) {

View file

@ -6,6 +6,8 @@ import {format, MapLike, FormatArg, countDigits, mapValues, prettyPrint, assert}
import { BOLT_DIAG_NUM_EXTRA_LINES } from "./constants";
import { TextPos, TextFile, TextSpan } from "./text";
export const E_INVALID_TEST_COMPARE = "The given test results cannot be compared because they use different specifications."
export const E_TESTS_DO_NOT_COMPARE = "This test does not compare with its expected output."
export const E_NO_BOLTFILE_FOUND_IN_PATH_OR_PARENT_DIRS = 'No Boltfile found in {path} or any of its parent directories.'
export const E_SSCAN_ERROR = "Got an unexpected {char}"
export const E_STDLIB_NOT_FOUND = "Package 'stdlib' is required to build the current source set but it was not found. Use --no-std if you know what you are doing."
@ -45,7 +47,7 @@ export interface Diagnostic {
message: string;
severity: string;
args?: MapLike<FormatArg>;
node?: Syntax;
node?: { span: TextSpan | null };
nested?: Diagnostic[];
position?: TextPos,
file?: TextFile,

View file

@ -1,8 +1,9 @@
import * as path from "path"
import * as fs from "fs"
import { serializeTag, serialize } from "./util";
import { serializeTag, serialize, deserializable } from "./util";
@deserializable()
export class TextFile {
private cachedText: string | null = null;
@ -16,7 +17,7 @@ export class TextFile {
}
[serializeTag]() {
return this.origPath;
return [ this.origPath ];
}
public getText(encoding: BufferEncoding = 'utf8'): string {
@ -30,6 +31,7 @@ export class TextFile {
}
@deserializable()
export class TextPos {
constructor(
@ -45,11 +47,11 @@ export class TextPos {
}
[serializeTag]() {
return {
offset: this.offset,
line: this.line,
column: this.column,
}
return [
this.offset,
this.line,
this.column,
]
}
public advance(str: string) {
@ -66,6 +68,7 @@ export class TextPos {
}
@deserializable()
export class TextSpan {
constructor(
@ -81,11 +84,11 @@ export class TextSpan {
}
[serializeTag]() {
return {
file: serialize(this.file),
start: serialize(this.start),
end: serialize(this.end),
}
return [
this.file,
this.start,
this.end,
]
}
}

View file

@ -6,6 +6,7 @@ import * as os from "os"
import moment from "moment"
import chalk from "chalk"
import { LOG_DATETIME_FORMAT } from "./constants"
import { NODE_TYPES } from "./ast"
export function isPowerOf(x: number, n: number):boolean {
const a = Math.log(x) / Math.log(n);
@ -196,44 +197,45 @@ export function isPrimitive(value: any): boolean {
return (typeof(value) !== 'function' && typeof(value) !== 'object') || value === null;
}
export const serializeTag = Symbol('serializer tag');
export const serializeTag = Symbol('serialize tag');
export const deserializeTag = Symbol('deserialize tag');
const serializableClasses = new Map<string, Newable<{ [serializeTag](): Json }>>();
const deserializableClasses = new Map<string, Newable<{ [serializeTag](): Json }>>();
export function registerClass(cls: Newable<any>) {
deserializableClasses.set(cls.name, cls)
}
const TYPE_KEY = '__type'
export function serialize(value: any): Json {
if (isPrimitive(value)) {
if (typeof(value) === 'bigint') {
return {
type: 'bigint',
[TYPE_KEY]: 'bigint',
value: value.toString(),
}
} else {
return {
type: 'primitive',
value,
}
return value;
}
} else if (Array.isArray(value)) {
return {
type: 'array',
elements: value.map(serialize)
}
return value.map(serialize);
} else if (isObjectLike(value)) {
if (isPlainObject(value)) {
return {
type: 'object',
elements: [...map(values(value), element => serialize(element))]
};
const result: MapLike<Json> = {};
for (const key of Object.keys(value)) {
result[key] = serialize(value[key]);
}
return result;
} else if (value[serializeTag] !== undefined
&& typeof(value[serializeTag]) === 'function'
&& typeof(value.constructor.name) === 'string') {
return {
type: 'class',
[TYPE_KEY]: 'classinstance',
name: value.constructor.name,
data: value[serializeTag](),
args: value[serializeTag]().map(serialize),
}
} else {
console.log(value);
throw new Error(`Could not serialize ${value}: it was a non-primitive object and has no serializer tag.`)
}
} else {
@ -241,69 +243,103 @@ export function serialize(value: any): Json {
}
}
export type TransparentProxy<T> = T & { updateHandle(value: T): void }
export function deserialize(data: Json): any {
if (isPrimitive(data)) {
return data;
}
if (Array.isArray(data)) {
return data.map(deserialize);
}
if (isPlainObject(data)) {
if (data[TYPE_KEY] === 'bigint') {
return BigInt(data.value);
}
if (data[TYPE_KEY] === 'classinstance') {
const cls = deserializableClasses.get(data.name as string);
if (cls === undefined) {
throw new Error(`Could not deserialize ${data.name}: class not found.`)
}
const args = (data.args as JsonArray).map(deserialize);
return new cls(...args)
}
const result: MapLike<any> = {};
for (const key of Object.keys(data)) {
result[key] = deserialize(data[key]);
}
return result;
}
throw new Error(`I did not know how to deserialize ${data}'.`)
}
export function createTransparentProxy<T extends object>(value: T): TransparentProxy<T> {
const handlerObject = {
__HANDLE: value,
__IS_HANDLE: true,
updateHandle(newValue: T) {
if (newValue.__IS_HANDLE) {
newValue = newValue.__HANDLE;
export function deserializable() {
return function (target: any) {
deserializableClasses.set(target.name, target);
}
value = newValue;
handlerObject.__HANDLE = newValue;
}
};
return new Proxy<any>({}, {
getPrototypeOf(target: T): object | null {
return Reflect.getPrototypeOf(value);
},
setPrototypeOf(target: T, v: any): boolean {
return Reflect.setPrototypeOf(value, v);
},
isExtensible(target: T): boolean {
return Reflect.isExtensible(value);
},
preventExtensions(target: T): boolean {
return Reflect.preventExtensions(value);
},
getOwnPropertyDescriptor(target: T, p: PropertyKey): PropertyDescriptor | undefined {
return Reflect.getOwnPropertyDescriptor(value, p);
},
has(target: T, p: PropertyKey): boolean {
return Reflect.has(value, p);
},
get(target: T, p: PropertyKey, receiver: any): any {
if (hasOwnProperty(handlerObject, p)) {
return Reflect.get(handlerObject, p);
}
return Reflect.get(value, p, receiver)
},
set(target: T, p: PropertyKey, value: any, receiver: any): boolean {
return Reflect.set(value, p, value);
},
deleteProperty(target: T, p: PropertyKey): boolean {
return Reflect.deleteProperty(value, p);
},
defineProperty(target: T, p: PropertyKey, attributes: PropertyDescriptor): boolean {
return Reflect.defineProperty(value, p, attributes);
},
enumerate(target: T): PropertyKey[] {
return [...Reflect.enumerate(value)];
},
ownKeys(target: T): PropertyKey[] {
return Reflect.ownKeys(value);
},
apply(target: T, thisArg: any, argArray?: any): any {
return Reflect.apply(value as any, thisArg, argArray);
},
construct(target: T, argArray: any, newTarget?: any): object {
return Reflect.construct(value as any, argArray, newTarget);
}
});
}
//export type TransparentProxy<T> = T & { updateHandle(value: T): void }
//export function createTransparentProxy<T extends object>(value: T): TransparentProxy<T> {
// const handlerObject = {
// __HANDLE: value,
// __IS_HANDLE: true,
// updateHandle(newValue: T) {
// if (newValue.__IS_HANDLE) {
// newValue = newValue.__HANDLE;
// }
// value = newValue;
// handlerObject.__HANDLE = newValue;
// }
// };
// return new Proxy<any>({}, {
// getPrototypeOf(target: T): object | null {
// return Reflect.getPrototypeOf(value);
// },
// setPrototypeOf(target: T, v: any): boolean {
// return Reflect.setPrototypeOf(value, v);
// },
// isExtensible(target: T): boolean {
// return Reflect.isExtensible(value);
// },
// preventExtensions(target: T): boolean {
// return Reflect.preventExtensions(value);
// },
// getOwnPropertyDescriptor(target: T, p: PropertyKey): PropertyDescriptor | undefined {
// return Reflect.getOwnPropertyDescriptor(value, p);
// },
// has(target: T, p: PropertyKey): boolean {
// return Reflect.has(value, p);
// },
// get(target: T, p: PropertyKey, receiver: any): any {
// if (hasOwnProperty(handlerObject, p)) {
// return Reflect.get(handlerObject, p);
// }
// return Reflect.get(value, p, receiver)
// },
// set(target: T, p: PropertyKey, value: any, receiver: any): boolean {
// return Reflect.set(value, p, value);
// },
// deleteProperty(target: T, p: PropertyKey): boolean {
// return Reflect.deleteProperty(value, p);
// },
// defineProperty(target: T, p: PropertyKey, attributes: PropertyDescriptor): boolean {
// return Reflect.defineProperty(value, p, attributes);
// },
// enumerate(target: T): PropertyKey[] {
// return [...Reflect.enumerate(value)];
// },
// ownKeys(target: T): PropertyKey[] {
// return Reflect.ownKeys(value);
// },
// apply(target: T, thisArg: any, argArray?: any): any {
// return Reflect.apply(value as any, thisArg, argArray);
// },
// construct(target: T, argArray: any, newTarget?: any): object {
// return Reflect.construct(value as any, argArray, newTarget);
// }
// });
//}
export const getKeyTag = Symbol('get key of object');
function getKey(value: any): string {
@ -648,6 +684,7 @@ export function format(message: string, data: MapLike<FormatArg>) {
}
export function deepEqual(a: any, b: any): boolean {
if (isPrimitive(a) && isPrimitive(b)) {
return a === b;