diff --git a/src/UserCodeRunner.ts b/src/UserCodeRunner.ts index 6642270..65fa3d0 100644 --- a/src/UserCodeRunner.ts +++ b/src/UserCodeRunner.ts @@ -1,12 +1,10 @@ import vm from 'vm'; -import crypto from 'crypto'; import path from 'path'; import { defaultErrorCodeMessageMappers } from './defaultErrorCodeMessageMappers.js'; import { createMapDiagnosticMessage } from './utils/errorMessageMapping.js'; import ts from 'typescript'; import { parse, StackFrame } from 'stack-trace'; -import { BasicSourceMapConsumer, IndexedSourceMapConsumer, SourceMapConsumer } from 'source-map'; -import LRUCache from 'lru-cache'; +import { SourceMapConsumer } from 'source-map'; import { Result } from './utils/monads.js'; import { TypeGuard } from './utils/typeGuardCombinators'; @@ -18,26 +16,18 @@ const EXECUTION_HARNESS_FILENAME = '__execution_harness'; const USER_CODE_FILENAME = '__user_file'; export interface CacheItem { - jsFileMap: Map; - tsFileMap: Map; - sourceMap: BasicSourceMapConsumer | IndexedSourceMapConsumer; + jsFileMap: {[key: string]: string}; + userCodeSourceMap: string; } export interface UserCodeRunnerOptions { - cacheOptions?: LRUCache.Options; typeErrorCodeMessageMappers?: {[errorCode: number]: (message: string) => string | undefined },// The error code to message mappers } export class UserCodeRunner { - private readonly user_file_cache: LRUCache; private readonly mapDiagnosticMessage: ReturnType; constructor(options?: UserCodeRunnerOptions) { - this.user_file_cache = new LRUCache({ - max: 500, - ttl: 1000 * 60 * 30, - ...options?.cacheOptions - }); this.mapDiagnosticMessage = createMapDiagnosticMessage(options?.typeErrorCodeMessageMappers ?? defaultErrorCodeMessageMappers); } @@ -88,8 +78,8 @@ export class UserCodeRunner { tsFileMap.set(removeExt(additionalSourceFile.fileName), additionalSourceFile); } - const jsFileMap = new Map(); - const sourceMapMap = new Map(); + const jsFileMap = {} as {[key: string]: string}; + let userCodeSourceMap: string; const defaultCompilerHost = ts.createCompilerHost({}); const customCompilerHost: ts.CompilerHost = { @@ -109,12 +99,11 @@ export class UserCodeRunner { writeFile: (fileName, data) => { const filenameSansExt = removeExt(fileName); if (fileName.endsWith('.map')) { - sourceMapMap.set(removeExt(filenameSansExt), ts.createSourceFile(removeExt(filenameSansExt), data, ts.ScriptTarget.ESNext)); + if (removeExt(filenameSansExt) === USER_CODE_FILENAME) { + userCodeSourceMap = ts.createSourceFile(removeExt(filenameSansExt), data, ts.ScriptTarget.ESNext).text + } } else { - jsFileMap.set( - filenameSansExt, - ts.createSourceFile(filenameSansExt, data, ts.ScriptTarget.ESNext, undefined, ts.ScriptKind.JS), - ); + jsFileMap[filenameSansExt] = ts.createSourceFile(filenameSansExt, data, ts.ScriptTarget.ESNext, undefined, ts.ScriptKind.JS).text; } }, readFile(fileName: string): string | undefined { @@ -159,8 +148,6 @@ export class UserCodeRunner { const emitResult = program.emit(); - const sourceMap = await new SourceMapConsumer(sourceMapMap.get(USER_CODE_FILENAME)!.text); - emitResult.diagnostics.forEach(diagnostic => { if (diagnostic.file) { sourceErrors.push(UserCodeTypeError.new(diagnostic, tsFileMap, typeChecker, this.mapDiagnosticMessage)); @@ -175,15 +162,10 @@ export class UserCodeRunner { return Result.Ok({ jsFileMap, - tsFileMap, - sourceMap, + userCodeSourceMap: userCodeSourceMap!, }); } - private static hash(str: string): string { - return crypto.createHash('sha1').update(str).digest('base64'); - } - public async executeUserCode( userCode: string, args: ArgsType, @@ -193,18 +175,24 @@ export class UserCodeRunner { additionalSourceFiles: ts.SourceFile[] = [], context: vm.Context = vm.createContext(), ): Promise> { - const userCodeHash = UserCodeRunner.hash(`${userCode}:${outputType}:${argsTypes.join(':')}${additionalSourceFiles.map(f => `:${f.text}`).join('')}`); + const result = await this.preProcess(userCode, outputType, argsTypes, additionalSourceFiles); - if (!this.user_file_cache.has(userCodeHash)) { - const result = await this.preProcess(userCode, outputType, argsTypes, additionalSourceFiles); - - if (result.isErr()) { - return result; - } - this.user_file_cache.set(userCodeHash, result.unwrap()); + if (result.isErr()) { + return result; } - const { jsFileMap, tsFileMap, sourceMap } = this.user_file_cache.get(userCodeHash)!; + const { jsFileMap, userCodeSourceMap } = result.unwrap(); + + return this.executeUserCodeFromArtifacts(jsFileMap, userCodeSourceMap, args, timeout, context); + } + + public async executeUserCodeFromArtifacts( + jsFileMap: {[key: string]: string}, + sourceMap: string, + args: ArgsType, + timeout: number = 5000, + context: vm.Context = vm.createContext(), + ): Promise> { // Put args and result into context context.__args = args; @@ -212,11 +200,11 @@ export class UserCodeRunner { // Create modules for VM const moduleCache = new Map(); - for (const jsFile of jsFileMap.values()) { + for (const [fileName, content] of Object.entries(jsFileMap)) { moduleCache.set( - jsFile.fileName, - new vm.SourceTextModule(jsFile.text, { - identifier: jsFile.fileName, + fileName, + new vm.SourceTextModule(content, { + identifier: fileName, context, }), ); @@ -236,7 +224,7 @@ export class UserCodeRunner { }); return Result.Ok(context.__result); } catch (error: any) { - return Result.Err([UserCodeRuntimeError.new(error as Error, sourceMap, tsFileMap)]); + return Result.Err([UserCodeRuntimeError.new(error as Error, await new SourceMapConsumer(sourceMap))]); } } } @@ -347,14 +335,12 @@ export class UserCodeTypeError extends UserCodeError { export class UserCodeRuntimeError extends UserCodeError { private readonly error: Error; private readonly sourceMap: SourceMapConsumer; - private readonly tsFileCache: Map; private readonly stackFrames: StackFrame[]; - protected constructor(error: Error, sourceMap: SourceMapConsumer, tsFileCache: Map) { + protected constructor(error: Error, sourceMap: SourceMapConsumer) { super(); this.error = error; this.sourceMap = sourceMap; - this.tsFileCache = tsFileCache; this.stackFrames = parse(this.error); const userCodeFrame = this.stackFrames.find(frame => frame.getFileName() === USER_CODE_FILENAME); if (userCodeFrame === undefined) { @@ -410,9 +396,8 @@ export class UserCodeRuntimeError extends UserCodeError { public static new( error: Error, sourceMap: SourceMapConsumer, - tsFileCache: Map, ): UserCodeRuntimeError { - return new UserCodeRuntimeError(error, sourceMap, tsFileCache); + return new UserCodeRuntimeError(error, sourceMap); } } diff --git a/src/utils/monads.ts b/src/utils/monads.ts index c1bc8fa..4df14b4 100644 --- a/src/utils/monads.ts +++ b/src/utils/monads.ts @@ -9,6 +9,20 @@ class ErrorWithContents extends Error { this.contents = contents; } } + +export enum SerializedResultType { + Ok = 'Result.Ok', + Err = 'Result.Err', +} + +export type SerializedResult = { + $$type: SerializedResultType.Ok; + $$value: T; +} | { + $$type: SerializedResultType.Err; + $$value: E; +} + /** * Result is a type used for returning and propagating errors. It has the variants, Ok(T), representing success * and containing a value, and Err(E), representing error and containing an error value. @@ -263,6 +277,41 @@ export class Result { } return `Err(${this.unwrapErr()})`; } + + public toJSON(): SerializedResult { + if (this.isOk()) { + return { + $$type: SerializedResultType.Ok, + $$value: this.unwrap() + }; + } + return { + $$type: SerializedResultType.Err, + $$value: this.unwrapErr() + }; + } + + public static fromJSON(json: SerializedResult): Result { + if (json.$$type === SerializedResultType.Ok) { + return Result.Ok(json.$$value); + } + else if (json.$$type === SerializedResultType.Err) { + return Result.Err(json.$$value); + } + throw new Error(`Invalid JSON serialization of Result: ${JSON.stringify(json)}`); + } +} + +export enum SerializedOptionType { + Some = 'Some', + None = 'None' +} + +export type SerializedOption = { + $$type: SerializedOptionType.Some, + $$value: T +} | { + $$type: SerializedOptionType.None } /** @@ -533,5 +582,27 @@ export class Option { } return `None()`; } + + public toJSON(): SerializedOption { + if (this.isSome()) { + return { + $$type: SerializedOptionType.Some, + $$value: this.unwrap() + }; + } + return { + $$type: SerializedOptionType.None, + }; + } + + public static fromJSON(json: SerializedOption): Option { + if (json.$$type === SerializedOptionType.Some) { + return Option.Some(json.$$value); + } + else if (json.$$type === SerializedOptionType.None) { + return Option.None() + } + throw new Error(`Invalid JSON serialization of Option: ${JSON.stringify(json)}`); + } } diff --git a/test/UserCodeRunner.spec.ts b/test/UserCodeRunner.spec.ts index 479c176..9f65612 100644 --- a/test/UserCodeRunner.spec.ts +++ b/test/UserCodeRunner.spec.ts @@ -8,8 +8,9 @@ import ts from 'typescript'; import * as vm from "vm"; import * as fs from "fs"; -it('should produce runtime errors', async () => { - const userCode = ` +describe('behavior', () => { + it('should produce runtime errors', async () => { + const userCode = ` export default function MyDSLFunction(thing: string): string { subroutine(); return thing + ' world'; @@ -20,76 +21,76 @@ it('should produce runtime errors', async () => { } `.trimTemplate(); - const runner = new UserCodeRunner(); + const runner = new UserCodeRunner(); - const result = await runner.executeUserCode( - userCode, - ['hello'], - 'string', - ['string'], - ); + const result = await runner.executeUserCode( + userCode, + ['hello'], + 'string', + ['string'], + ); - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(1); - expect(result.unwrapErr()[0].message).toBe(` + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(1); + expect(result.unwrapErr()[0].message).toBe(` Error: This is a test error `.trimTemplate()); - expect(result.unwrapErr()[0].stack).toBe(` + expect(result.unwrapErr()[0].stack).toBe(` at subroutine(7:8) at MyDSLFunction(2:2) `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 7, - column: 8, + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 7, + column: 8, + }); }); -}); -it('should produce runtime errors from additional files', async () => { - const userCode = ` - export default function MyDSLFunction(thing: string): string { - throwingLibraryFunction(); - return thing + ' world'; - } - `.trimTemplate(); + it('should produce runtime errors from additional files', async () => { + const userCode = ` + export default function MyDSLFunction(thing: string): string { + throwingLibraryFunction(); + return thing + ' world'; + } + `.trimTemplate(); - const runner = new UserCodeRunner(); + const runner = new UserCodeRunner(); - const result = await runner.executeUserCode( - userCode, - ['hello'], - 'string', - ['string'], - 1000, - [ - ts.createSourceFile('globals.ts', ` - declare global { - function throwingLibraryFunction(): void; - } - export function throwingLibraryFunction(): void { - throw new Error("Error in library code") - } - - Object.assign(globalThis, { throwingLibraryFunction }); - `.trimTemplate(), ts.ScriptTarget.ESNext, true), - ], - ); + const result = await runner.executeUserCode( + userCode, + ['hello'], + 'string', + ['string'], + 1000, + [ + ts.createSourceFile('globals.ts', ` + declare global { + function throwingLibraryFunction(): void; + } + export function throwingLibraryFunction(): void { + throw new Error("Error in library code") + } + + Object.assign(globalThis, { throwingLibraryFunction }); + `.trimTemplate(), ts.ScriptTarget.ESNext, true), + ], + ); - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(1); - expect(result.unwrapErr()[0].message).toBe(` - Error: Error in library code - `.trimTemplate()); - expect(result.unwrapErr()[0].stack).toBe(` - at MyDSLFunction(2:2) - `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 2, - column: 2, + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(1); + expect(result.unwrapErr()[0].message).toBe(` + Error: Error in library code + `.trimTemplate()); + expect(result.unwrapErr()[0].stack).toBe(` + at MyDSLFunction(2:2) + `.trimTemplate()) + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 2, + column: 2, + }); }); -}); -it('should produce return type errors', async () => { - const userCode = ` + it('should produce return type errors', async () => { + const userCode = ` export default function MyDSLFunction(thing: string): string { subroutine(); return thing + ' world'; @@ -100,31 +101,31 @@ it('should produce return type errors', async () => { } `.trimTemplate(); - const runner = new UserCodeRunner(); + const runner = new UserCodeRunner(); - const result = await runner.executeUserCode( - userCode, - ['hello'], - 'number', - ['string'], - ); + const result = await runner.executeUserCode( + userCode, + ['hello'], + 'number', + ['string'], + ); - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(1); - expect(result.unwrapErr()[0].message).toBe(` + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(1); + expect(result.unwrapErr()[0].message).toBe(` TypeError: TS2322 Incorrect return type. Expected: 'number', Actual: 'string'. `.trimTemplate()); - expect(result.unwrapErr()[0].stack).toBe(` + expect(result.unwrapErr()[0].stack).toBe(` at MyDSLFunction(1:55) `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 1, - column: 55, + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 1, + column: 55, + }); }); -}); -it('should produce input type errors', async () => { - const userCode = ` + it('should produce input type errors', async () => { + const userCode = ` export default function MyDSLFunction(thing: string, other: number): string { subroutine(); return thing + ' world'; @@ -135,31 +136,31 @@ it('should produce input type errors', async () => { } `.trimTemplate(); - const runner = new UserCodeRunner(); + const runner = new UserCodeRunner(); - const result = await runner.executeUserCode( - userCode, - ['hello'], - 'string', - ['string'], - ); + const result = await runner.executeUserCode( + userCode, + ['hello'], + 'string', + ['string'], + ); - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(1); - expect(result.unwrapErr()[0].message).toBe(` + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(1); + expect(result.unwrapErr()[0].message).toBe(` TypeError: TS2554 Incorrect argument type. Expected: '[string]', Actual: '[string, number]'. `.trimTemplate()); - expect(result.unwrapErr()[0].stack).toBe(` + expect(result.unwrapErr()[0].stack).toBe(` at MyDSLFunction(1:39) `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 1, - column: 39, + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 1, + column: 39, + }); }); -}); -it('should handle no default export errors', async () => { - const userCode = ` + it('should handle no default export errors', async () => { + const userCode = ` export function MyDSLFunction(thing: string, other: number): string { subroutine(); return thing + ' world'; @@ -170,31 +171,31 @@ it('should handle no default export errors', async () => { } `.trimTemplate(); - const runner = new UserCodeRunner(); + const runner = new UserCodeRunner(); - const result = await runner.executeUserCode( - userCode, - ['hello'], - 'string', - ['string'], - ); + const result = await runner.executeUserCode( + userCode, + ['hello'], + 'string', + ['string'], + ); - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(1); - expect(result.unwrapErr()[0].message).toBe(` + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(1); + expect(result.unwrapErr()[0].message).toBe(` TypeError: TS1192 No default export. Expected a default export function with the signature: "(...args: [string]) => string". `.trimTemplate()); - expect(result.unwrapErr()[0].stack).toBe(` + expect(result.unwrapErr()[0].stack).toBe(` at (1:1) `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 1, - column: 1, + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 1, + column: 1, + }); }); -}); -it('should handle no export errors', async () => { - const userCode = ` + it('should handle no export errors', async () => { + const userCode = ` function MyDSLFunction(thing: string, other: number): string { subroutine(); return thing + ' world'; @@ -205,31 +206,31 @@ it('should handle no export errors', async () => { } `.trimTemplate(); - const runner = new UserCodeRunner(); + const runner = new UserCodeRunner(); - const result = await runner.executeUserCode( - userCode, - ['hello'], - 'string', - ['string'], - ); + const result = await runner.executeUserCode( + userCode, + ['hello'], + 'string', + ['string'], + ); - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(1); - expect(result.unwrapErr()[0].message).toBe(` + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(1); + expect(result.unwrapErr()[0].message).toBe(` TypeError: TS2306 No default export. Expected a default export function with the signature: "(...args: [string]) => string". `.trimTemplate()); - expect(result.unwrapErr()[0].stack).toBe(` + expect(result.unwrapErr()[0].stack).toBe(` at (1:1) `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 1, - column: 1, + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 1, + column: 1, + }); }); -}); -it('should handle default export not function errors', async () => { - const userCode = ` + it('should handle default export not function errors', async () => { + const userCode = ` const hello = 'hello'; export default hello; function MyDSLFunction(thing: string, other: number): string { @@ -242,168 +243,472 @@ it('should handle default export not function errors', async () => { } `.trimTemplate(); - const runner = new UserCodeRunner(); + const runner = new UserCodeRunner(); - const result = await runner.executeUserCode( - userCode, - ['hello'], - 'string', - ['string'], - ); + const result = await runner.executeUserCode( + userCode, + ['hello'], + 'string', + ['string'], + ); - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(1); - expect(result.unwrapErr()[0].message).toBe(` + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(1); + expect(result.unwrapErr()[0].message).toBe(` TypeError: TS2349 Default export is not a valid function. Expected a default export function with the signature: "(...args: [string]) => string". `.trimTemplate()); - expect(result.unwrapErr()[0].stack).toBe(` + expect(result.unwrapErr()[0].stack).toBe(` at (2:1) `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 2, - column: 1, + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 2, + column: 1, + }); }); -}); -it('should produce internal type errors', async () => { - const userCode = ` + it('should produce internal type errors', async () => { + const userCode = ` export default function MyDSLFunction(thing: string): number { const other: number = 'hello'; return thing + ' world'; } `.trimTemplate(); - const runner = new UserCodeRunner(); + const runner = new UserCodeRunner(); - const result = await runner.executeUserCode( - userCode, - ['hello'], - 'number', - ['string'], - ); + const result = await runner.executeUserCode( + userCode, + ['hello'], + 'number', + ['string'], + ); - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(2); - expect(result.unwrapErr()[0].message).toBe(` + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(2); + expect(result.unwrapErr()[0].message).toBe(` TypeError: TS2322 Type 'string' is not assignable to type 'number'. `.trimTemplate()); - expect(result.unwrapErr()[0].stack).toBe(` + expect(result.unwrapErr()[0].stack).toBe(` at MyDSLFunction(2:9) `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 2, - column: 9, - }); - expect(result.unwrapErr()[1].message).toBe(` + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 2, + column: 9, + }); + expect(result.unwrapErr()[1].message).toBe(` TypeError: TS2322 Type 'string' is not assignable to type 'number'. `.trimTemplate()); - expect(result.unwrapErr()[1].stack).toBe(` + expect(result.unwrapErr()[1].stack).toBe(` at MyDSLFunction(3:3) `.trimTemplate()) - expect(result.unwrapErr()[1].location).toMatchObject({ - line: 3, - column: 3, + expect(result.unwrapErr()[1].location).toMatchObject({ + line: 3, + column: 3, + }); }); -}); -it('should return the final value', async () => { - const userCode = ` + it('should return the final value', async () => { + const userCode = ` export default function MyDSLFunction(thing: string): string { return thing + ' world'; } `.trimTemplate(); - const runner = new UserCodeRunner(); + const runner = new UserCodeRunner(); - const result = await runner.executeUserCode( - userCode, - ['hello'], - 'string', - ['string'], - ); + const result = await runner.executeUserCode( + userCode, + ['hello'], + 'string', + ['string'], + ); - expect(result.isOk()).toBeTruthy(); - expect(result.unwrap()).toBe('hello world'); -}); + expect(result.isOk()).toBeTruthy(); + expect(result.unwrap()).toBe('hello world'); + }); -it('should accept additional source files', async () => { - const userCode = ` + it('should accept additional source files', async () => { + const userCode = ` import { importedFunction } from 'other-importable'; export default function myDSLFunction(thing: string): string { return someGlobalFunction(thing) + importedFunction(' world'); } `.trimTemplate(); - const runner = new UserCodeRunner(); + const runner = new UserCodeRunner(); - const result = await runner.executeUserCode( - userCode, - ['hello'], - 'string', - ['string'], - 1000, - [ - ts.createSourceFile('globals.d.ts', ` + const result = await runner.executeUserCode( + userCode, + ['hello'], + 'string', + ['string'], + 1000, + [ + ts.createSourceFile('globals.d.ts', ` declare global { function someGlobalFunction(thing: string): string; } export {}; `.trimTemplate(), ts.ScriptTarget.ESNext, true), - ts.createSourceFile('other-importable.ts', ` + ts.createSourceFile('other-importable.ts', ` export function importedFunction(thing: string): string { return thing + ' other'; } `.trimTemplate(), ts.ScriptTarget.ESNext, true) - ], - vm.createContext({ - someGlobalFunction: (thing: string) => 'hello ' + thing, // Implementation injected to global namespace here - }), - ); - - // expect(result.isOk()).toBeTruthy(); - expect(result.unwrap()).toBe('hello hello world other'); + ], + vm.createContext({ + someGlobalFunction: (thing: string) => 'hello ' + thing, // Implementation injected to global namespace here + }), + ); + + // expect(result.isOk()).toBeTruthy(); + expect(result.unwrap()).toBe('hello hello world other'); + }); + + it('should handle unnamed arrow function default exports', async () => { + const userCode = ` + type ExpansionProps = { activity: ActivityType }; + + export default (props: ExpansionProps): ExpansionReturn => { + const { activity } = props; + const { biteSize } = activity.attributes.arguments; + + return [ + AVS_DMP_ADC_SNAPSHOT(biteSize) + ]; + } + `.trimTemplate() + + const runner = new UserCodeRunner(); + const [commandTypes, activityTypes, temporalPolyfill] = await Promise.all([ + fs.promises.readFile(new URL('./inputs/command-types.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/activity-types.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/TemporalPolyfillTypes.ts', import.meta.url).pathname, 'utf8'), + ]); + + const context = vm.createContext({ + Temporal, + }); + const result = await runner.executeUserCode( + userCode, + [{ activity: null}], + 'Command[] | Command | null', + ['{ activity: ActivityType }'], + 1000, + [ + ts.createSourceFile('command-types.ts', commandTypes, ts.ScriptTarget.ESNext, true), + ts.createSourceFile('activity-types.ts', activityTypes, ts.ScriptTarget.ESNext, true), + ts.createSourceFile('TemporalPolyfillTypes.ts', temporalPolyfill, ts.ScriptTarget.ESNext, true), + ], + context, + ); + + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(3); + expect(result.unwrapErr()[0].message).toBe(` + TypeError: TS2322 Incorrect return type. Expected: 'Command[] | Command | null', Actual: 'ExpansionReturn'. + `.trimTemplate()); + expect(result.unwrapErr()[0].stack).toBe(` + at (3:1) + `.trimTemplate()) + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 3, + column: 1, + }); + }); + + it('should handle exported variable that references an arrow function', async () => { + const userCode = ` + type ExpansionProps = { activity: ActivityType }; + + const myExpansion = (props: ExpansionProps): ExpansionReturn => { + const { activity } = props; + const { biteSize } = activity.attributes.arguments; + + return [ + AVS_DMP_ADC_SNAPSHOT(biteSize) + ]; + }; + export default myExpansion; + `.trimTemplate() + + const runner = new UserCodeRunner(); + const [commandTypes, activityTypes, temporalPolyfill] = await Promise.all([ + fs.promises.readFile(new URL('./inputs/command-types.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/activity-types.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/TemporalPolyfillTypes.ts', import.meta.url).pathname, 'utf8'), + ]); + + const context = vm.createContext({ + Temporal, + }); + const result = await runner.executeUserCode( + userCode, + [{ activity: null}], + 'Command[] | Command | null', + ['{ activity: ActivityType }'], + 1000, + [ + ts.createSourceFile('command-types.ts', commandTypes, ts.ScriptTarget.ESNext, true), + ts.createSourceFile('activity-types.ts', activityTypes, ts.ScriptTarget.ESNext, true), + ts.createSourceFile('TemporalPolyfillTypes.ts', temporalPolyfill, ts.ScriptTarget.ESNext, true), + ], + context, + ); + + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(3); + expect(result.unwrapErr()[0].message).toBe(` + TypeError: TS2322 Incorrect return type. Expected: 'Command[] | Command | null', Actual: 'ExpansionReturn'. + `.trimTemplate()); + expect(result.unwrapErr()[0].stack).toBe(` + at (11:1) + `.trimTemplate()) + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 11, + column: 1, + }); + }); + + it('should handle exported variable that references a function', async () => { + const userCode = ` + type ExpansionProps = { activity: ActivityType }; + + const myExpansion = function(props: ExpansionProps): ExpansionReturn { + const { activity } = props; + const { biteSize } = activity.attributes.arguments; + + return [ + AVS_DMP_ADC_SNAPSHOT(biteSize) + ]; + }; + export default myExpansion; + `.trimTemplate() + + const runner = new UserCodeRunner(); + const [commandTypes, activityTypes, temporalPolyfill] = await Promise.all([ + fs.promises.readFile(new URL('./inputs/command-types.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/activity-types.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/TemporalPolyfillTypes.ts', import.meta.url).pathname, 'utf8'), + ]); + + const context = vm.createContext({ + Temporal, + }); + const result = await runner.executeUserCode( + userCode, + [{ activity: null}], + 'Command[] | Command | null', + ['{ activity: ActivityType }'], + 1000, + [ + ts.createSourceFile('command-types.ts', commandTypes, ts.ScriptTarget.ESNext, true), + ts.createSourceFile('activity-types.ts', activityTypes, ts.ScriptTarget.ESNext, true), + ts.createSourceFile('TemporalPolyfillTypes.ts', temporalPolyfill, ts.ScriptTarget.ESNext, true), + ], + context, + ); + + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(3); + expect(result.unwrapErr()[0].message).toBe(` + TypeError: TS2322 Incorrect return type. Expected: 'Command[] | Command | null', Actual: 'ExpansionReturn'. + `.trimTemplate()); + expect(result.unwrapErr()[0].stack).toBe(` + at (11:1) + `.trimTemplate()) + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 11, + column: 1, + }); + }); + + it('should handle unnamed arrow function default exports assignment', async () => { + const userCode = ` + type ExpansionProps = { activity: ActivityType }; + + const myExpansion = (props: ExpansionProps) => { + const { activity } = props; + const { primitiveLong } = activity.attributes.arguments; + + if (true) { + return undefined; + } + + return [ + PREHEAT_OVEN(primitiveLong) + ]; + }; + export default myExpansion; + `.trimTemplate() + + const runner = new UserCodeRunner(); + const [commandTypes, activityTypes, temporalPolyfill] = await Promise.all([ + fs.promises.readFile(new URL('./inputs/command-types.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/activity-types.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/TemporalPolyfillTypes.ts', import.meta.url).pathname, 'utf8'), + ]); + + const context = vm.createContext({ + Temporal, + }); + const result = await runner.executeUserCode( + userCode, + [{ activity: { attributes: { arguments: { primitiveLong: 1 } } } }], + 'Command[] | Command | null', + ['{ activity: ActivityType }'], + 1000, + [ + ts.createSourceFile('command-types.ts', commandTypes, ts.ScriptTarget.ESNext, true), + ts.createSourceFile('activity-types.ts', activityTypes, ts.ScriptTarget.ESNext, true), + ts.createSourceFile('TemporalPolyfillTypes.ts', temporalPolyfill, ts.ScriptTarget.ESNext, true), + ], + context, + ); + + expect(result.isOk()).toBeTruthy(); + }); + + it('should handle throws in user code but outside default function execution path', async () => { + const userCode = ` + export default function MyDSLFunction(thing: string): string { + return thing + ' world'; + } + + throw new Error('This is a test error'); + `.trimTemplate(); + + const runner = new UserCodeRunner(); + + const result = await runner.executeUserCode( + userCode, + ['hello'], + 'string', + ['string'], + ); + + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(1); + expect(result.unwrapErr()[0].message).toBe(` + Error: This is a test error + `.trimTemplate()); + expect(result.unwrapErr()[0].stack).toBe(` + at null(5:6) + `.trimTemplate()) + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 5, + column: 6, + }); + }); + + it('should handle throws in library code outside default function execution path with an explicit error', async () => { + const userCode = ` + export default function MyDSLFunction(thing: string): string { + return thing + ' world'; + } + `.trimTemplate(); + + const runner = new UserCodeRunner(); + + try { + await runner.executeUserCode( + userCode, + ['hello'], + 'string', + ['string'], + 1000, + [ + ts.createSourceFile('additionalFile.ts', ` + export {} + throw new Error('This is a test error'); + `.trimTemplate(), ts.ScriptTarget.ESNext, true), + ], + ); + } catch (err: any) { + expect(err.message).toBe(` + Error: Runtime error detected outside of user code execution path. This is most likely a bug in the additional library source. + Inherited from: + This is a test error + `.trimTemplate()); + expect(err.stack).toContain(` + Error: This is a test error + at additionalFile:1:7 + at SourceTextModule.evaluate (node:internal/vm/module:224:23) + `.trimTemplate()); + expect(err.stack).toMatch(/at UserCodeRunner\.executeUserCodeFromArtifacts \(\S+src\/UserCodeRunner\.ts:222:24/); + expect(err.stack).toMatch(/at Object\. \(\S+test\/UserCodeRunner\.spec\.ts:614:7/); + } + }); + + it('should allow preprocessing of user code and subsequent execution', async () => { + const userCode = ` + export default function MyDSLFunction(thing: string): string { + return thing + ' world'; + } + `.trimTemplate(); + + const runner = new UserCodeRunner(); + + const result = await runner.preProcess( + userCode, + 'string', + ['string'], + ); + + expect(result.isOk()).toBeTruthy(); + + const result2 = await runner.executeUserCodeFromArtifacts( + result.unwrap().jsFileMap, + result.unwrap().userCodeSourceMap, + ['hello'], + ); + + expect(result2.isOk()).toBeTruthy(); + expect(result2.unwrap()).toBe('hello world'); + }); }); -test('Aerie command expansion throw Regression Test', async () => { - const userCode = ` +describe('regression tests', () => { + test('Aerie command expansion throw Regression Test', async () => { + const userCode = ` export default function SingleCommandExpansion(props: { activity: ActivityType }): Command { const duration = Temporal.Duration.from('PT1H'); return BAKE_BREAD; } `.trimTemplate() - const runner = new UserCodeRunner(); - const [commandTypes, activityTypes, temporalPolyfill] = await Promise.all([ - fs.promises.readFile(new URL('./inputs/command-types.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/activity-types.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/TemporalPolyfillTypes.ts', import.meta.url).pathname, 'utf8'), - ]); + const runner = new UserCodeRunner(); + const [commandTypes, activityTypes, temporalPolyfill] = await Promise.all([ + fs.promises.readFile(new URL('./inputs/command-types.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/activity-types.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/TemporalPolyfillTypes.ts', import.meta.url).pathname, 'utf8'), + ]); - const context = vm.createContext({ - Temporal, - }); - const result = await runner.executeUserCode( - userCode, - [{ activity: null}], - 'Command[] | Command | null', - ['{ activity: ActivityType }'], - 1000, - [ - ts.createSourceFile('command-types.ts', commandTypes, ts.ScriptTarget.ESNext, true), - ts.createSourceFile('activity-types.ts', activityTypes, ts.ScriptTarget.ESNext, true), - ts.createSourceFile('TemporalPolyfillTypes.ts', temporalPolyfill, ts.ScriptTarget.ESNext, true), - ], - context, - ); - - expect(result.unwrap()).toMatchObject({ - stem: 'BAKE_BREAD', - arguments: [], - }); -}) + const context = vm.createContext({ + Temporal, + }); + const result = await runner.executeUserCode( + userCode, + [{ activity: null }], + 'Command[] | Command | null', + ['{ activity: ActivityType }'], + 1000, + [ + ts.createSourceFile('command-types.ts', commandTypes, ts.ScriptTarget.ESNext, true), + ts.createSourceFile('activity-types.ts', activityTypes, ts.ScriptTarget.ESNext, true), + ts.createSourceFile('TemporalPolyfillTypes.ts', temporalPolyfill, ts.ScriptTarget.ESNext, true), + ], + context, + ); -test('Aerie undefined node test', async () => { - const userCode = ` + expect(result.unwrap()).toMatchObject({ + stem: 'BAKE_BREAD', + arguments: [], + }); + }) + + test('Aerie undefined node test', async () => { + const userCode = ` export default function BakeBananaBreadExpansionLogic( props: { activityInstance: ActivityType; @@ -418,76 +723,76 @@ test('Aerie undefined node test', async () => { } `.trimTemplate() - const runner = new UserCodeRunner(); - const [commandTypes, activityTypes, temporalPolyfill] = await Promise.all([ - fs.promises.readFile(new URL('./inputs/command-types.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/activity-types.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/TemporalPolyfillTypes.ts', import.meta.url).pathname, 'utf8'), - ]); + const runner = new UserCodeRunner(); + const [commandTypes, activityTypes, temporalPolyfill] = await Promise.all([ + fs.promises.readFile(new URL('./inputs/command-types.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/activity-types.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/TemporalPolyfillTypes.ts', import.meta.url).pathname, 'utf8'), + ]); - const context = vm.createContext({ - Temporal, - }); - const result = await runner.executeUserCode( - userCode, - [{ activityInstance: null}, {}], - 'Command[] | Command | null', - ['{ activityInstance: ActivityType }', 'Context'], - 1000, - [ - ts.createSourceFile('command-types.ts', commandTypes, ts.ScriptTarget.ESNext, true), - ts.createSourceFile('activity-types.ts', activityTypes, ts.ScriptTarget.ESNext, true), - ts.createSourceFile('TemporalPolyfillTypes.ts', temporalPolyfill, ts.ScriptTarget.ESNext, true), - ], - context, - ); - - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(4); - expect(result.unwrapErr()[0].message).toBe(` + const context = vm.createContext({ + Temporal, + }); + const result = await runner.executeUserCode( + userCode, + [{ activityInstance: null }, {}], + 'Command[] | Command | null', + ['{ activityInstance: ActivityType }', 'Context'], + 1000, + [ + ts.createSourceFile('command-types.ts', commandTypes, ts.ScriptTarget.ESNext, true), + ts.createSourceFile('activity-types.ts', activityTypes, ts.ScriptTarget.ESNext, true), + ts.createSourceFile('TemporalPolyfillTypes.ts', temporalPolyfill, ts.ScriptTarget.ESNext, true), + ], + context, + ); + + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(4); + expect(result.unwrapErr()[0].message).toBe(` TypeError: TS2322 Incorrect return type. Expected: 'Command[] | Command | null', Actual: 'ExpansionReturn'. `.trimTemplate()); - expect(result.unwrapErr()[0].stack).toBe(` + expect(result.unwrapErr()[0].stack).toBe(` at BakeBananaBreadExpansionLogic(6:4) `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 6, - column: 4, - }); - expect(result.unwrapErr()[1].message).toBe(` + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 6, + column: 4, + }); + expect(result.unwrapErr()[1].message).toBe(` TypeError: TS2339 Property 'temperature' does not exist on type 'ParameterTest'. `.trimTemplate()); - expect(result.unwrapErr()[1].stack).toBe(` + expect(result.unwrapErr()[1].stack).toBe(` at BakeBananaBreadExpansionLogic(8:41) `.trimTemplate()) - expect(result.unwrapErr()[1].location).toMatchObject({ - line: 8, - column: 41, - }); - expect(result.unwrapErr()[2].message).toBe(` + expect(result.unwrapErr()[1].location).toMatchObject({ + line: 8, + column: 41, + }); + expect(result.unwrapErr()[2].message).toBe(` TypeError: TS2339 Property 'tbSugar' does not exist on type 'ParameterTest'. `.trimTemplate()); - expect(result.unwrapErr()[2].stack).toBe(` + expect(result.unwrapErr()[2].stack).toBe(` at BakeBananaBreadExpansionLogic(9:41) `.trimTemplate()) - expect(result.unwrapErr()[2].location).toMatchObject({ - line: 9, - column: 41, - }); - expect(result.unwrapErr()[3].message).toBe(` + expect(result.unwrapErr()[2].location).toMatchObject({ + line: 9, + column: 41, + }); + expect(result.unwrapErr()[3].message).toBe(` TypeError: TS2339 Property 'glutenFree' does not exist on type 'ParameterTest'. `.trimTemplate()); - expect(result.unwrapErr()[3].stack).toBe(` + expect(result.unwrapErr()[3].stack).toBe(` at BakeBananaBreadExpansionLogic(9:73) `.trimTemplate()) - expect(result.unwrapErr()[3].location).toMatchObject({ - line: 9, - column: 73, + expect(result.unwrapErr()[3].location).toMatchObject({ + line: 9, + column: 73, + }); }); -}); -test('Aerie Scheduler test', async () => { - const userCode = ` + test('Aerie Scheduler test', async () => { + const userCode = ` export default function myGoal() { return myHelper(ActivityTemplates.PeelBanana({ peelDirection: 'fromStem', @@ -503,52 +808,51 @@ test('Aerie Scheduler test', async () => { } `.trimTemplate(); - const runner = new UserCodeRunner(); - const [schedulerAst, schedulerEdsl, modelSpecific] = await Promise.all([ - fs.promises.readFile(new URL('./inputs/scheduler-ast.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/scheduler-edsl-fluent-api.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/mission-model-generated-code.ts', import.meta.url).pathname, 'utf8'), - ]); + const runner = new UserCodeRunner(); + const [schedulerAst, schedulerEdsl, modelSpecific] = await Promise.all([ + fs.promises.readFile(new URL('./inputs/scheduler-ast.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/scheduler-edsl-fluent-api.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/mission-model-generated-code.ts', import.meta.url).pathname, 'utf8'), + ]); - const context = vm.createContext({ - }); - const result = await runner.executeUserCode( - userCode, - [], - 'Goal', - [], - undefined, - [ - ts.createSourceFile('scheduler-ast.ts', schedulerAst, ts.ScriptTarget.ESNext), - ts.createSourceFile('scheduler-edsl-fluent-api.ts', schedulerEdsl, ts.ScriptTarget.ESNext), - ts.createSourceFile('mission-model-generated-code.ts', modelSpecific, ts.ScriptTarget.ESNext), - ], - context, - ); - - expect(result.unwrap()).toMatchObject({ - __astNode: { - activityTemplate: { - activityType: 'PeelBanana', - args: { - peelDirection: 'fromStem', - duration: 60 * 60 * 1000 * 1000, - fancy: { - subfield1: 'value1', - subfield2: [{ - subsubfield1: 1, - }] + const context = vm.createContext({}); + const result = await runner.executeUserCode( + userCode, + [], + 'Goal', + [], + undefined, + [ + ts.createSourceFile('scheduler-ast.ts', schedulerAst, ts.ScriptTarget.ESNext), + ts.createSourceFile('scheduler-edsl-fluent-api.ts', schedulerEdsl, ts.ScriptTarget.ESNext), + ts.createSourceFile('mission-model-generated-code.ts', modelSpecific, ts.ScriptTarget.ESNext), + ], + context, + ); + + expect(result.unwrap()).toMatchObject({ + __astNode: { + activityTemplate: { + activityType: 'PeelBanana', + args: { + peelDirection: 'fromStem', + duration: 60 * 60 * 1000 * 1000, + fancy: { + subfield1: 'value1', + subfield2: [{ + subsubfield1: 1, + }] + } } - } - }, - interval: 60 * 60 * 1000 * 1000, - kind: 'ActivityRecurrenceGoal', - } + }, + interval: 60 * 60 * 1000 * 1000, + kind: 'ActivityRecurrenceGoal', + } + }); }); -}); -test('Aerie Scheduler TS2345 regression test', async () => { - const userCode = ` + test('Aerie Scheduler TS2345 regression test', async () => { + const userCode = ` export default function myGoal() { return myHelper(ActivityTemplates.PeelBanana({ peelDirection: 'fromStem' })) } @@ -560,123 +864,120 @@ test('Aerie Scheduler TS2345 regression test', async () => { } `.trimTemplate(); - const runner = new UserCodeRunner(); - const [schedulerAst, schedulerEdsl, modelSpecific] = await Promise.all([ - fs.promises.readFile(new URL('./inputs/scheduler-ast.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/scheduler-edsl-fluent-api.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/dsl-model-specific--2345.ts', import.meta.url).pathname, 'utf8'), - ]); + const runner = new UserCodeRunner(); + const [schedulerAst, schedulerEdsl, modelSpecific] = await Promise.all([ + fs.promises.readFile(new URL('./inputs/scheduler-ast.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/scheduler-edsl-fluent-api.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/dsl-model-specific--2345.ts', import.meta.url).pathname, 'utf8'), + ]); - const context = vm.createContext({ - }); - const result = await runner.executeUserCode( - userCode, - [], - 'Goal', - [], - undefined, - [ - ts.createSourceFile('scheduler-ast.ts', schedulerAst, ts.ScriptTarget.ESNext), - ts.createSourceFile('scheduler-edsl-fluent-api.ts', schedulerEdsl, ts.ScriptTarget.ESNext), - ts.createSourceFile('mission-model-generated-code.ts', modelSpecific, ts.ScriptTarget.ESNext), - ], - context, - ); - - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(1); - expect(result.unwrapErr()[0].message).toBe(` + const context = vm.createContext({}); + const result = await runner.executeUserCode( + userCode, + [], + 'Goal', + [], + undefined, + [ + ts.createSourceFile('scheduler-ast.ts', schedulerAst, ts.ScriptTarget.ESNext), + ts.createSourceFile('scheduler-edsl-fluent-api.ts', schedulerEdsl, ts.ScriptTarget.ESNext), + ts.createSourceFile('mission-model-generated-code.ts', modelSpecific, ts.ScriptTarget.ESNext), + ], + context, + ); + + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(1); + expect(result.unwrapErr()[0].message).toBe(` TypeError: TS2345 Argument of type '{ peelDirection: "fromStem"; }' is not assignable to parameter of type '{ duration: number; fancy: { subfield1: string; subfield2: { subsubfield1: number; }[]; }; peelDirection: "fromTip" | "fromStem"; }'. Type '{ peelDirection: "fromStem"; }' is missing the following properties from type '{ duration: number; fancy: { subfield1: string; subfield2: { subsubfield1: number; }[]; }; peelDirection: "fromTip" | "fromStem"; }': duration, fancy `.trimTemplate()); - expect(result.unwrapErr()[0].stack).toBe(` + expect(result.unwrapErr()[0].stack).toBe(` at myGoal(2:48) `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 2, - column: 48, + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 2, + column: 48, + }); }); -}); -test("Aerie Scheduler wrong return type no annotation regression test", async () => { - const userCode = ` + test("Aerie Scheduler wrong return type no annotation regression test", async () => { + const userCode = ` export default function myGoal() { return 5 } `.trimTemplate(); - const runner = new UserCodeRunner(); - const [schedulerAst, schedulerEdsl, modelSpecific] = await Promise.all([ - fs.promises.readFile(new URL('./inputs/scheduler-ast.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/scheduler-edsl-fluent-api.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/mission-model-generated-code.ts', import.meta.url).pathname, 'utf8'), - ]); + const runner = new UserCodeRunner(); + const [schedulerAst, schedulerEdsl, modelSpecific] = await Promise.all([ + fs.promises.readFile(new URL('./inputs/scheduler-ast.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/scheduler-edsl-fluent-api.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/mission-model-generated-code.ts', import.meta.url).pathname, 'utf8'), + ]); - const context = vm.createContext({ - }); - const result = await runner.executeUserCode( - userCode, - [], - 'Goal', - [], - undefined, - [ - ts.createSourceFile('scheduler-ast.ts', schedulerAst, ts.ScriptTarget.ESNext), - ts.createSourceFile('scheduler-edsl-fluent-api.ts', schedulerEdsl, ts.ScriptTarget.ESNext), - ts.createSourceFile('mission-model-generated-code.ts', modelSpecific, ts.ScriptTarget.ESNext), - ], - context, - ); - - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(1); - expect(result.unwrapErr()[0].message).toBe(` + const context = vm.createContext({}); + const result = await runner.executeUserCode( + userCode, + [], + 'Goal', + [], + undefined, + [ + ts.createSourceFile('scheduler-ast.ts', schedulerAst, ts.ScriptTarget.ESNext), + ts.createSourceFile('scheduler-edsl-fluent-api.ts', schedulerEdsl, ts.ScriptTarget.ESNext), + ts.createSourceFile('mission-model-generated-code.ts', modelSpecific, ts.ScriptTarget.ESNext), + ], + context, + ); + + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(1); + expect(result.unwrapErr()[0].message).toBe(` TypeError: TS2322 Incorrect return type. Expected: 'Goal', Actual: 'number'. `.trimTemplate()); - expect(result.unwrapErr()[0].stack).toBe(` + expect(result.unwrapErr()[0].stack).toBe(` at myGoal(1:1) `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 1, - column: 1, + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 1, + column: 1, + }); }); -}); -test("literal type regression test", async () => { - const userCode = ` + test("literal type regression test", async () => { + const userCode = ` export default function myGoal() { return 5 } `.trimTemplate(); - const runner = new UserCodeRunner(); - const [schedulerAst, schedulerEdsl, modelSpecific] = await Promise.all([ - fs.promises.readFile(new URL('./inputs/scheduler-ast.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/scheduler-edsl-fluent-api.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/mission-model-generated-code.ts', import.meta.url).pathname, 'utf8'), - ]); + const runner = new UserCodeRunner(); + const [schedulerAst, schedulerEdsl, modelSpecific] = await Promise.all([ + fs.promises.readFile(new URL('./inputs/scheduler-ast.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/scheduler-edsl-fluent-api.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/mission-model-generated-code.ts', import.meta.url).pathname, 'utf8'), + ]); - const context = vm.createContext({ + const context = vm.createContext({}); + const result = await runner.executeUserCode( + userCode, + [], + 'number', + [], + undefined, + [ + ts.createSourceFile('scheduler-ast.ts', schedulerAst, ts.ScriptTarget.ESNext), + ts.createSourceFile('scheduler-edsl-fluent-api.ts', schedulerEdsl, ts.ScriptTarget.ESNext), + ts.createSourceFile('mission-model-generated-code.ts', modelSpecific, ts.ScriptTarget.ESNext), + ], + context, + ); + + expect(result.isOk()).toBeTruthy(); }); - const result = await runner.executeUserCode( - userCode, - [], - 'number', - [], - undefined, - [ - ts.createSourceFile('scheduler-ast.ts', schedulerAst, ts.ScriptTarget.ESNext), - ts.createSourceFile('scheduler-edsl-fluent-api.ts', schedulerEdsl, ts.ScriptTarget.ESNext), - ts.createSourceFile('mission-model-generated-code.ts', modelSpecific, ts.ScriptTarget.ESNext), - ], - context, - ); - - expect(result.isOk()).toBeTruthy(); -}); -test("branching return regression test", async () => { - const userCode = ` + test("branching return regression test", async () => { + const userCode = ` export default function myGoal() { if (true) { return '4' @@ -685,154 +986,152 @@ test("branching return regression test", async () => { } `.trimTemplate(); - const runner = new UserCodeRunner(); - const [schedulerAst, schedulerEdsl, modelSpecific] = await Promise.all([ - fs.promises.readFile(new URL('./inputs/scheduler-ast.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/scheduler-edsl-fluent-api.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/mission-model-generated-code.ts', import.meta.url).pathname, 'utf8'), - ]); + const runner = new UserCodeRunner(); + const [schedulerAst, schedulerEdsl, modelSpecific] = await Promise.all([ + fs.promises.readFile(new URL('./inputs/scheduler-ast.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/scheduler-edsl-fluent-api.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/mission-model-generated-code.ts', import.meta.url).pathname, 'utf8'), + ]); - const context = vm.createContext({ - }); - const result = await runner.executeUserCode( - userCode, - [], - 'string', - [], - undefined, - [ - ts.createSourceFile('scheduler-ast.ts', schedulerAst, ts.ScriptTarget.ESNext), - ts.createSourceFile('scheduler-edsl-fluent-api.ts', schedulerEdsl, ts.ScriptTarget.ESNext), - ts.createSourceFile('mission-model-generated-code.ts', modelSpecific, ts.ScriptTarget.ESNext), - ], - context, - ); - - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(1); - expect(result.unwrapErr()[0].message).toBe(` + const context = vm.createContext({}); + const result = await runner.executeUserCode( + userCode, + [], + 'string', + [], + undefined, + [ + ts.createSourceFile('scheduler-ast.ts', schedulerAst, ts.ScriptTarget.ESNext), + ts.createSourceFile('scheduler-edsl-fluent-api.ts', schedulerEdsl, ts.ScriptTarget.ESNext), + ts.createSourceFile('mission-model-generated-code.ts', modelSpecific, ts.ScriptTarget.ESNext), + ], + context, + ); + + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(1); + expect(result.unwrapErr()[0].message).toBe(` TypeError: TS2322 Incorrect return type. Expected: 'string', Actual: '"4" | 5'. `.trimTemplate()); - expect(result.unwrapErr()[0].stack).toBe(` + expect(result.unwrapErr()[0].stack).toBe(` at myGoal(1:1) `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 1, - column: 1, + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 1, + column: 1, + }); }); -}); -test("literal return regression test", async () => { - const userCode = ` + test("literal return regression test", async () => { + const userCode = ` export default function myGoal() { return 5 } `.trimTemplate(); - const runner = new UserCodeRunner(); - const [schedulerAst, schedulerEdsl, modelSpecific] = await Promise.all([ - fs.promises.readFile(new URL('./inputs/scheduler-ast.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/scheduler-edsl-fluent-api.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/mission-model-generated-code.ts', import.meta.url).pathname, 'utf8'), - ]); + const runner = new UserCodeRunner(); + const [schedulerAst, schedulerEdsl, modelSpecific] = await Promise.all([ + fs.promises.readFile(new URL('./inputs/scheduler-ast.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/scheduler-edsl-fluent-api.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/mission-model-generated-code.ts', import.meta.url).pathname, 'utf8'), + ]); - const context = vm.createContext({ - }); - const result = await runner.executeUserCode( - userCode, - [], - '4', - [], - undefined, - [ - ts.createSourceFile('scheduler-ast.ts', schedulerAst, ts.ScriptTarget.ESNext), - ts.createSourceFile('scheduler-edsl-fluent-api.ts', schedulerEdsl, ts.ScriptTarget.ESNext), - ts.createSourceFile('mission-model-generated-code.ts', modelSpecific, ts.ScriptTarget.ESNext), - ], - context, - ); - - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(1); - expect(result.unwrapErr()[0].message).toBe(` + const context = vm.createContext({}); + const result = await runner.executeUserCode( + userCode, + [], + '4', + [], + undefined, + [ + ts.createSourceFile('scheduler-ast.ts', schedulerAst, ts.ScriptTarget.ESNext), + ts.createSourceFile('scheduler-edsl-fluent-api.ts', schedulerEdsl, ts.ScriptTarget.ESNext), + ts.createSourceFile('mission-model-generated-code.ts', modelSpecific, ts.ScriptTarget.ESNext), + ], + context, + ); + + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(1); + expect(result.unwrapErr()[0].message).toBe(` TypeError: TS2322 Incorrect return type. Expected: '4', Actual: 'number'. `.trimTemplate()); - expect(result.unwrapErr()[0].stack).toBe(` + expect(result.unwrapErr()[0].stack).toBe(` at myGoal(1:1) `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 1, - column: 1, + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 1, + column: 1, + }); }); -}); -test('Aerie command expansion invalid count regression test', async () => { - const userCode = ` + test('Aerie command expansion invalid count regression test', async () => { + const userCode = ` export default function SingleCommandExpansion(): ExpansionReturn { return DDM_CLOSE_OPEN_SELECT_DP; } `.trimTemplate() - const runner = new UserCodeRunner(); - const [commandTypes, activityTypes, temporalPolyfill] = await Promise.all([ - fs.promises.readFile(new URL('./inputs/command-types.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/activity-types.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/TemporalPolyfillTypes.ts', import.meta.url).pathname, 'utf8'), - ]); + const runner = new UserCodeRunner(); + const [commandTypes, activityTypes, temporalPolyfill] = await Promise.all([ + fs.promises.readFile(new URL('./inputs/command-types.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/activity-types.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/TemporalPolyfillTypes.ts', import.meta.url).pathname, 'utf8'), + ]); - const context = vm.createContext({ - Temporal, - }); - const result = await runner.executeUserCode( - userCode, - [{ activity: null}], - 'Command[] | Command | null', - ['{ activity: ActivityType }'], - 1000, - [ - ts.createSourceFile('command-types.ts', commandTypes, ts.ScriptTarget.ESNext, true), - ts.createSourceFile('activity-types.ts', activityTypes, ts.ScriptTarget.ESNext, true), - ts.createSourceFile('TemporalPolyfillTypes.ts', temporalPolyfill, ts.ScriptTarget.ESNext, true), - ], - context, - ); - - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(3); - expect(result.unwrapErr()[0].message).toBe(` + const context = vm.createContext({ + Temporal, + }); + const result = await runner.executeUserCode( + userCode, + [{ activity: null }], + 'Command[] | Command | null', + ['{ activity: ActivityType }'], + 1000, + [ + ts.createSourceFile('command-types.ts', commandTypes, ts.ScriptTarget.ESNext, true), + ts.createSourceFile('activity-types.ts', activityTypes, ts.ScriptTarget.ESNext, true), + ts.createSourceFile('TemporalPolyfillTypes.ts', temporalPolyfill, ts.ScriptTarget.ESNext, true), + ], + context, + ); + + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(3); + expect(result.unwrapErr()[0].message).toBe(` TypeError: TS2322 Incorrect return type. Expected: 'Command[] | Command | null', Actual: 'ExpansionReturn'. `.trimTemplate()); - expect(result.unwrapErr()[0].stack).toBe(` + expect(result.unwrapErr()[0].stack).toBe(` at SingleCommandExpansion(1:51) `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 1, - column: 51, - }); - expect(result.unwrapErr()[1].message).toBe(` + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 1, + column: 51, + }); + expect(result.unwrapErr()[1].message).toBe(` TypeError: TS2554 Incorrect argument type. Expected: '[{ activity: ActivityType }]', Actual: '[]'. `.trimTemplate()); - expect(result.unwrapErr()[1].stack).toBe(` + expect(result.unwrapErr()[1].stack).toBe(` at SingleCommandExpansion(1:1) `.trimTemplate()) - expect(result.unwrapErr()[1].location).toMatchObject({ - line: 1, - column: 1, - }); - expect(result.unwrapErr()[2].message).toBe(` + expect(result.unwrapErr()[1].location).toMatchObject({ + line: 1, + column: 1, + }); + expect(result.unwrapErr()[2].message).toBe(` TypeError: TS2304 Cannot find name 'DDM_CLOSE_OPEN_SELECT_DP'. `.trimTemplate()); - expect(result.unwrapErr()[2].stack).toBe(` + expect(result.unwrapErr()[2].stack).toBe(` at SingleCommandExpansion(2:10) `.trimTemplate()) - expect(result.unwrapErr()[2].location).toMatchObject({ - line: 2, - column: 10, + expect(result.unwrapErr()[2].location).toMatchObject({ + line: 2, + column: 10, + }); }); -}); -test('Aerie scheduler unmapped harness error on missing property return type', async () => { - const userCode = ` + test('Aerie scheduler unmapped harness error on missing property return type', async () => { + const userCode = ` interface FakeGoal { and(...others: FakeGoal[]): FakeGoal; or(...others: FakeGoal[]): FakeGoal; @@ -850,358 +1149,82 @@ test('Aerie scheduler unmapped harness error on missing property return type', a } `.trimTemplate() - const runner = new UserCodeRunner(); - const [schedulerAst, schedulerEdsl, modelSpecific] = await Promise.all([ - fs.promises.readFile(new URL('./inputs/scheduler-ast.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/scheduler-edsl-fluent-api.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/mission-model-generated-code.ts', import.meta.url).pathname, 'utf8'), - ]); - - const context = vm.createContext({ - }); - const result = await runner.executeUserCode( - userCode, - [], - 'Goal', - [], - undefined, - [ - ts.createSourceFile('scheduler-ast.ts', schedulerAst, ts.ScriptTarget.ESNext), - ts.createSourceFile('scheduler-edsl-fluent-api.ts', schedulerEdsl, ts.ScriptTarget.ESNext), - ts.createSourceFile('mission-model-generated-code.ts', modelSpecific, ts.ScriptTarget.ESNext), - ], - context, - ); - - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(1); - expect(result.unwrapErr()[0].message).toBe(`TypeError: TS2741 Incorrect return type. Expected: 'Goal', Actual: 'FakeGoal'.`); - expect(result.unwrapErr()[0].stack).toBe(` - at (5:1) - `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 5, - column: 1, - }); -}); - -it('should handle unnamed arrow function default exports', async () => { - const userCode = ` - type ExpansionProps = { activity: ActivityType }; - - export default (props: ExpansionProps): ExpansionReturn => { - const { activity } = props; - const { biteSize } = activity.attributes.arguments; - - return [ - AVS_DMP_ADC_SNAPSHOT(biteSize) - ]; - } - `.trimTemplate() - - const runner = new UserCodeRunner(); - const [commandTypes, activityTypes, temporalPolyfill] = await Promise.all([ - fs.promises.readFile(new URL('./inputs/command-types.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/activity-types.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/TemporalPolyfillTypes.ts', import.meta.url).pathname, 'utf8'), - ]); - - const context = vm.createContext({ - Temporal, - }); - const result = await runner.executeUserCode( - userCode, - [{ activity: null}], - 'Command[] | Command | null', - ['{ activity: ActivityType }'], - 1000, - [ - ts.createSourceFile('command-types.ts', commandTypes, ts.ScriptTarget.ESNext, true), - ts.createSourceFile('activity-types.ts', activityTypes, ts.ScriptTarget.ESNext, true), - ts.createSourceFile('TemporalPolyfillTypes.ts', temporalPolyfill, ts.ScriptTarget.ESNext, true), - ], - context, - ); - - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(3); - expect(result.unwrapErr()[0].message).toBe(` - TypeError: TS2322 Incorrect return type. Expected: 'Command[] | Command | null', Actual: 'ExpansionReturn'. - `.trimTemplate()); - expect(result.unwrapErr()[0].stack).toBe(` - at (3:1) - `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 3, - column: 1, - }); -}); - -it('should handle exported variable that references an arrow function', async () => { - const userCode = ` - type ExpansionProps = { activity: ActivityType }; - - const myExpansion = (props: ExpansionProps): ExpansionReturn => { - const { activity } = props; - const { biteSize } = activity.attributes.arguments; - - return [ - AVS_DMP_ADC_SNAPSHOT(biteSize) - ]; - }; - export default myExpansion; - `.trimTemplate() - - const runner = new UserCodeRunner(); - const [commandTypes, activityTypes, temporalPolyfill] = await Promise.all([ - fs.promises.readFile(new URL('./inputs/command-types.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/activity-types.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/TemporalPolyfillTypes.ts', import.meta.url).pathname, 'utf8'), - ]); - - const context = vm.createContext({ - Temporal, - }); - const result = await runner.executeUserCode( - userCode, - [{ activity: null}], - 'Command[] | Command | null', - ['{ activity: ActivityType }'], - 1000, - [ - ts.createSourceFile('command-types.ts', commandTypes, ts.ScriptTarget.ESNext, true), - ts.createSourceFile('activity-types.ts', activityTypes, ts.ScriptTarget.ESNext, true), - ts.createSourceFile('TemporalPolyfillTypes.ts', temporalPolyfill, ts.ScriptTarget.ESNext, true), - ], - context, - ); - - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(3); - expect(result.unwrapErr()[0].message).toBe(` - TypeError: TS2322 Incorrect return type. Expected: 'Command[] | Command | null', Actual: 'ExpansionReturn'. - `.trimTemplate()); - expect(result.unwrapErr()[0].stack).toBe(` - at (11:1) - `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 11, - column: 1, - }); -}); + const runner = new UserCodeRunner(); + const [schedulerAst, schedulerEdsl, modelSpecific] = await Promise.all([ + fs.promises.readFile(new URL('./inputs/scheduler-ast.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/scheduler-edsl-fluent-api.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/mission-model-generated-code.ts', import.meta.url).pathname, 'utf8'), + ]); -it('should handle exported variable that references a function', async () => { - const userCode = ` - type ExpansionProps = { activity: ActivityType }; - - const myExpansion = function(props: ExpansionProps): ExpansionReturn { - const { activity } = props; - const { biteSize } = activity.attributes.arguments; - - return [ - AVS_DMP_ADC_SNAPSHOT(biteSize) - ]; - }; - export default myExpansion; - `.trimTemplate() - - const runner = new UserCodeRunner(); - const [commandTypes, activityTypes, temporalPolyfill] = await Promise.all([ - fs.promises.readFile(new URL('./inputs/command-types.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/activity-types.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/TemporalPolyfillTypes.ts', import.meta.url).pathname, 'utf8'), - ]); - - const context = vm.createContext({ - Temporal, - }); - const result = await runner.executeUserCode( - userCode, - [{ activity: null}], - 'Command[] | Command | null', - ['{ activity: ActivityType }'], - 1000, - [ - ts.createSourceFile('command-types.ts', commandTypes, ts.ScriptTarget.ESNext, true), - ts.createSourceFile('activity-types.ts', activityTypes, ts.ScriptTarget.ESNext, true), - ts.createSourceFile('TemporalPolyfillTypes.ts', temporalPolyfill, ts.ScriptTarget.ESNext, true), - ], - context, - ); - - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(3); - expect(result.unwrapErr()[0].message).toBe(` - TypeError: TS2322 Incorrect return type. Expected: 'Command[] | Command | null', Actual: 'ExpansionReturn'. - `.trimTemplate()); - expect(result.unwrapErr()[0].stack).toBe(` - at (11:1) - `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 11, - column: 1, - }); -}); - -it('should handle unnamed arrow function default exports assignment', async () => { - const userCode = ` - type ExpansionProps = { activity: ActivityType }; - - const myExpansion = (props: ExpansionProps) => { - const { activity } = props; - const { primitiveLong } = activity.attributes.arguments; - - if (true) { - return undefined; - } - - return [ - PREHEAT_OVEN(primitiveLong) - ]; - }; - export default myExpansion; - `.trimTemplate() - - const runner = new UserCodeRunner(); - const [commandTypes, activityTypes, temporalPolyfill] = await Promise.all([ - fs.promises.readFile(new URL('./inputs/command-types.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/activity-types.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/TemporalPolyfillTypes.ts', import.meta.url).pathname, 'utf8'), - ]); - - const context = vm.createContext({ - Temporal, - }); - const result = await runner.executeUserCode( - userCode, - [{ activity: { attributes: { arguments: { primitiveLong: 1 } } } }], - 'Command[] | Command | null', - ['{ activity: ActivityType }'], - 1000, - [ - ts.createSourceFile('command-types.ts', commandTypes, ts.ScriptTarget.ESNext, true), - ts.createSourceFile('activity-types.ts', activityTypes, ts.ScriptTarget.ESNext, true), - ts.createSourceFile('TemporalPolyfillTypes.ts', temporalPolyfill, ts.ScriptTarget.ESNext, true), - ], - context, - ); - - expect(result.isOk()).toBeTruthy(); -}); - -it('should handle throws in user code but outside default function execution path', async () => { - const userCode = ` - export default function MyDSLFunction(thing: string): string { - return thing + ' world'; - } - - throw new Error('This is a test error'); - `.trimTemplate(); - - const runner = new UserCodeRunner(); - - const result = await runner.executeUserCode( - userCode, - ['hello'], - 'string', - ['string'], - ); + const context = vm.createContext({}); + const result = await runner.executeUserCode( + userCode, + [], + 'Goal', + [], + undefined, + [ + ts.createSourceFile('scheduler-ast.ts', schedulerAst, ts.ScriptTarget.ESNext), + ts.createSourceFile('scheduler-edsl-fluent-api.ts', schedulerEdsl, ts.ScriptTarget.ESNext), + ts.createSourceFile('mission-model-generated-code.ts', modelSpecific, ts.ScriptTarget.ESNext), + ], + context, + ); - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(1); - expect(result.unwrapErr()[0].message).toBe(` - Error: This is a test error - `.trimTemplate()); - expect(result.unwrapErr()[0].stack).toBe(` - at null(5:6) + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(1); + expect(result.unwrapErr()[0].message).toBe(`TypeError: TS2741 Incorrect return type. Expected: 'Goal', Actual: 'FakeGoal'.`); + expect(result.unwrapErr()[0].stack).toBe(` + at (5:1) `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 5, - column: 6, + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 5, + column: 1, + }); }); -}); -it('should handle throws in library code outside default function execution path with an explicit error', async () => { - const userCode = ` - export default function MyDSLFunction(thing: string): string { - return thing + ' world'; - } - `.trimTemplate(); + test('Aerie incorrect stack frame assumption regression test', async () => { + const userCode = ` + export default () => { + return Real.Resource("state of charge").lessThan(0.3).split(0) + } + `.trimTemplate() - const runner = new UserCodeRunner(); + const runner = new UserCodeRunner(); + const [constraintsAst, constraintsEdsl, modelSpecific] = await Promise.all([ + fs.promises.readFile(new URL('./inputs/missing-location/constraints-ast.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/missing-location/constraints-edsl-fluent-api.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/missing-location/mission-model-generated-code.ts', import.meta.url).pathname, 'utf8'), + ]); - try { - await runner.executeUserCode( + const result = await runner.executeUserCode( userCode, - ['hello'], - 'string', - ['string'], - 1000, + [], + 'Constraint', + [], + undefined, [ - ts.createSourceFile('additionalFile.ts', ` - export {} - throw new Error('This is a test error'); - `.trimTemplate(), ts.ScriptTarget.ESNext, true), + ts.createSourceFile('constraints-ast.ts', constraintsAst, ts.ScriptTarget.ESNext), + ts.createSourceFile('constraints-edsl-fluent-api.ts', constraintsEdsl, ts.ScriptTarget.ESNext), + ts.createSourceFile('mission-model-generated-code.ts', modelSpecific, ts.ScriptTarget.ESNext), ], ); - } catch (err: any) { - expect(err.message).toBe(` - Error: Runtime error detected outside of user code execution path. This is most likely a bug in the additional library source. - Inherited from: - This is a test error - `.trimTemplate()); - expect(err.stack).toBe(` - Error: This is a test error - at additionalFile:1:7 - at SourceTextModule.evaluate (node:internal/vm/module:224:23) - at UserCodeRunner.executeUserCode (/Users/jdstewar/gitRepos/jpl/mpcs/aerie/aerie-ts-user-code-runner/src/UserCodeRunner.ts:234:24) - at Object. (/Users/jdstewar/gitRepos/jpl/mpcs/aerie/aerie-ts-user-code-runner/test/UserCodeRunner.spec.ts:1134:5) - `.trimTemplate()); - } -}); - -test('Aerie incorrect stack frame assumption regression test', async () => { - const userCode = ` - export default () => { - return Real.Resource("state of charge").lessThan(0.3).split(0) - } - `.trimTemplate() - const runner = new UserCodeRunner(); - const [constraintsAst, constraintsEdsl, modelSpecific] = await Promise.all([ - fs.promises.readFile(new URL('./inputs/missing-location/constraints-ast.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/missing-location/constraints-edsl-fluent-api.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/missing-location/mission-model-generated-code.ts', import.meta.url).pathname, 'utf8'), - ]); - - const result = await runner.executeUserCode( - userCode, - [], - 'Constraint', - [], - undefined, - [ - ts.createSourceFile('constraints-ast.ts', constraintsAst, ts.ScriptTarget.ESNext), - ts.createSourceFile('constraints-edsl-fluent-api.ts', constraintsEdsl, ts.ScriptTarget.ESNext), - ts.createSourceFile('mission-model-generated-code.ts', modelSpecific, ts.ScriptTarget.ESNext), - ], - ); - - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(1); - expect(result.unwrapErr()[0].message).toBe(`Error: .split numberOfSubWindows cannot be less than 1, but was: 0`); - expect(result.unwrapErr()[0].stack).toBe(` - at default(2:56) - `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 2, - column: 56, + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(1); + expect(result.unwrapErr()[0].message).toBe(`Error: .split numberOfSubWindows cannot be less than 1, but was: 0`); + expect(result.unwrapErr()[0].stack).toBe(` + at default(2:56) + `.trimTemplate()) + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 2, + column: 56, + }); }); -}); -test('Unterminated string literal regression', async () => { - const userCode = - `export default () => + test('Unterminated string literal regression', async () => { + const userCode = + `export default () => Sequence.new({ seqId: 'seq0', metadata: {}, @@ -1210,43 +1233,44 @@ test('Unterminated string literal regression', async () => { ], });`.trimTemplate(); - const runner = new UserCodeRunner(); - const [commandTypes, temporalPolyfill] = await Promise.all([ - fs.promises.readFile(new URL('./inputs/command-types.ts', import.meta.url).pathname, 'utf8'), - fs.promises.readFile(new URL('./inputs/TemporalPolyfillTypes.ts', import.meta.url).pathname, 'utf8'), - ]); - - const result = await runner.executeUserCode( - userCode, - [], - 'Sequence', - [], - 1000, - [ - ts.createSourceFile('command-types.ts', commandTypes, ts.ScriptTarget.ESNext), - ts.createSourceFile('TemporalPolyfillTypes.ts', temporalPolyfill, ts.ScriptTarget.ESNext), - ], - vm.createContext({ - Temporal, - }), - ); + const runner = new UserCodeRunner(); + const [commandTypes, temporalPolyfill] = await Promise.all([ + fs.promises.readFile(new URL('./inputs/command-types.ts', import.meta.url).pathname, 'utf8'), + fs.promises.readFile(new URL('./inputs/TemporalPolyfillTypes.ts', import.meta.url).pathname, 'utf8'), + ]); - expect(result.isErr()).toBeTruthy(); - expect(result.unwrapErr().length).toBe(2); - expect(result.unwrapErr()[0].message).toBe(`TypeError: TS1002 Unterminated string literal.`); - expect(result.unwrapErr()[0].stack).toBe(` + const result = await runner.executeUserCode( + userCode, + [], + 'Sequence', + [], + 1000, + [ + ts.createSourceFile('command-types.ts', commandTypes, ts.ScriptTarget.ESNext), + ts.createSourceFile('TemporalPolyfillTypes.ts', temporalPolyfill, ts.ScriptTarget.ESNext), + ], + vm.createContext({ + Temporal, + }), + ); + + expect(result.isErr()).toBeTruthy(); + expect(result.unwrapErr().length).toBe(2); + expect(result.unwrapErr()[0].message).toBe(`TypeError: TS1002 Unterminated string literal.`); + expect(result.unwrapErr()[0].stack).toBe(` at (6:70) `.trimTemplate()) - expect(result.unwrapErr()[0].location).toMatchObject({ - line: 6, - column: 70, - }); - expect(result.unwrapErr()[1].message).toBe(`TypeError: TS1005 ',' expected.`); - expect(result.unwrapErr()[1].stack).toBe(` + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 6, + column: 70, + }); + expect(result.unwrapErr()[1].message).toBe(`TypeError: TS1005 ',' expected.`); + expect(result.unwrapErr()[1].stack).toBe(` at (7:9) `.trimTemplate()) - expect(result.unwrapErr()[1].location).toMatchObject({ - line: 7, - column: 9, + expect(result.unwrapErr()[1].location).toMatchObject({ + line: 7, + column: 9, + }); }); }); diff --git a/test/monads.spec.ts b/test/monads.spec.ts new file mode 100644 index 0000000..64fb901 --- /dev/null +++ b/test/monads.spec.ts @@ -0,0 +1,25 @@ +import { Option, Result, SerializedOptionType, SerializedResultType } from '../src/utils/monads'; + +describe('Result', () => { + it('should be serializable and deserializable', () => { + const result = Result.Ok(true); + const serializedResult = result.toJSON(); + expect(serializedResult).toEqual({ + $$type: SerializedResultType.Ok, + $$value: true, + }); + expect(Result.fromJSON(serializedResult)).toEqual(result); + }); +}); + +describe('Option', () => { + it('should be serializable and deserializable', () => { + const option = Option.Some(true); + const serializedOption = option.toJSON(); + expect(serializedOption).toEqual({ + $$type: SerializedOptionType.Some, + $$value: true, + }); + expect(Option.fromJSON(serializedOption)).toEqual(option); + }); +});