From 6be3251086b9011fe66ddfbf33a66be30a9d764b Mon Sep 17 00:00:00 2001 From: Joseph Dylan Stewart Date: Mon, 27 Jun 2022 10:37:17 -0700 Subject: [PATCH 1/6] Simplify cache items for easy serialization --- src/UserCodeRunner.ts | 61 ++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/src/UserCodeRunner.ts b/src/UserCodeRunner.ts index 6642270..66a89de 100644 --- a/src/UserCodeRunner.ts +++ b/src/UserCodeRunner.ts @@ -5,7 +5,7 @@ import { defaultErrorCodeMessageMappers } from './defaultErrorCodeMessageMappers import { createMapDiagnosticMessage } from './utils/errorMessageMapping.js'; import ts from 'typescript'; import { parse, StackFrame } from 'stack-trace'; -import { BasicSourceMapConsumer, IndexedSourceMapConsumer, SourceMapConsumer } from 'source-map'; +import { SourceMapConsumer } from 'source-map'; import LRUCache from 'lru-cache'; import { Result } from './utils/monads.js'; import { TypeGuard } from './utils/typeGuardCombinators'; @@ -18,22 +18,21 @@ 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; + 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 user_file_cache: LRUCache>; private readonly mapDiagnosticMessage: ReturnType; constructor(options?: UserCodeRunnerOptions) { - this.user_file_cache = new LRUCache({ + this.user_file_cache = new LRUCache>({ max: 500, ttl: 1000 * 60 * 30, ...options?.cacheOptions @@ -88,8 +87,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 +108,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 +157,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,8 +171,7 @@ export class UserCodeRunner { return Result.Ok({ jsFileMap, - tsFileMap, - sourceMap, + userCodeSourceMap: userCodeSourceMap!, }); } @@ -198,13 +193,16 @@ export class UserCodeRunner { 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()); + this.user_file_cache.set(userCodeHash, result); + } + + const result = this.user_file_cache.get(userCodeHash)!; + + if (result.isErr()) { + return result; } - const { jsFileMap, tsFileMap, sourceMap } = this.user_file_cache.get(userCodeHash)!; + const { jsFileMap, userCodeSourceMap } = result.unwrap(); // Put args and result into context context.__args = args; @@ -212,11 +210,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 +234,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(userCodeSourceMap))]); } } } @@ -347,14 +345,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 +406,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); } } From 025e20185096be49904ce82cb72f2bf5e836186b Mon Sep 17 00:00:00 2001 From: Joseph Dylan Stewart Date: Mon, 27 Jun 2022 10:42:11 -0700 Subject: [PATCH 2/6] Organize tests into regression and behavior --- test/UserCodeRunner.spec.ts | 1734 +++++++++++++++++------------------ 1 file changed, 866 insertions(+), 868 deletions(-) diff --git a/test/UserCodeRunner.spec.ts b/test/UserCodeRunner.spec.ts index 479c176..b8d0200 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,31 +21,31 @@ 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 () => { + it('should produce runtime errors from additional files', async () => { const userCode = ` export default function MyDSLFunction(thing: string): string { throwingLibraryFunction(); @@ -88,8 +89,8 @@ it('should produce runtime errors from additional files', async () => { }); }); -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,252 +243,530 @@ 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'); -}); - -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() + ], + vm.createContext({ + someGlobalFunction: (thing: string) => 'hello ' + thing, // Implementation injected to global namespace here + }), + ); - 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, + // expect(result.isOk()).toBeTruthy(); + expect(result.unwrap()).toBe('hello hello world other'); }); - 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: [], - }); -}) + it('should handle unnamed arrow function default exports', async () => { + const userCode = ` + type ExpansionProps = { activity: ActivityType }; -test('Aerie undefined node test', async () => { - const userCode = ` - export default function BakeBananaBreadExpansionLogic( - props: { - activityInstance: ActivityType; - }, - context: Context - ): ExpansionReturn { - return [ - PREHEAT_OVEN(props.activityInstance.temperature), - PREPARE_LOAF(props.activityInstance.tbSugar, props.activityInstance.glutenFree), - BAKE_BREAD, - ]; + 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, - [{ 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, - ); + 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'), + ]); - 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, + [{ 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 BakeBananaBreadExpansionLogic(6:4) - `.trimTemplate()) - 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(` - at BakeBananaBreadExpansionLogic(8:41) + expect(result.unwrapErr()[0].stack).toBe(` + at (3:1) `.trimTemplate()) - expect(result.unwrapErr()[1].location).toMatchObject({ - line: 8, - column: 41, + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 3, + column: 1, + }); }); - expect(result.unwrapErr()[2].message).toBe(` - TypeError: TS2339 Property 'tbSugar' does not exist on type 'ParameterTest'. + + 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()[2].stack).toBe(` - at BakeBananaBreadExpansionLogic(9:41) + expect(result.unwrapErr()[0].stack).toBe(` + at (11:1) `.trimTemplate()) - expect(result.unwrapErr()[2].location).toMatchObject({ - line: 9, - column: 41, + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 11, + column: 1, + }); }); - expect(result.unwrapErr()[3].message).toBe(` - TypeError: TS2339 Property 'glutenFree' does not exist on type 'ParameterTest'. + + 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).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:232:24) + at Object. (/Users/jdstewar/gitRepos/jpl/mpcs/aerie/aerie-ts-user-code-runner/test/UserCodeRunner.spec.ts:614:7) + `.trimTemplate()); + } + }); +}) + + +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 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: [], + }); + }) + + test('Aerie undefined node test', async () => { + const userCode = ` + export default function BakeBananaBreadExpansionLogic( + props: { + activityInstance: ActivityType; + }, + context: Context + ): ExpansionReturn { + return [ + PREHEAT_OVEN(props.activityInstance.temperature), + PREPARE_LOAF(props.activityInstance.tbSugar, props.activityInstance.glutenFree), + 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 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(` + at BakeBananaBreadExpansionLogic(6:4) + `.trimTemplate()) + 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(` + at BakeBananaBreadExpansionLogic(8:41) + `.trimTemplate()) + 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(` + at BakeBananaBreadExpansionLogic(9:41) + `.trimTemplate()) + 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 +782,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, - ); + 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, - }] + 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 +838,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, - ); + 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(` + 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, - ); + 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(` + 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 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, - ); + 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(); -}); + 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 +960,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, - ); + 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(` + 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, - ); + 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(` + 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 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, - ); + 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'), + ]); - 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 +1123,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 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, - ); + 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(` + 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, + expect(result.unwrapErr()[0].location).toMatchObject({ + line: 5, + 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(); + 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 +1207,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 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 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(` + 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, + }); }); }); From c765b1217b17310f176b4d240593ad14cee553fc Mon Sep 17 00:00:00 2001 From: Joseph Dylan Stewart Date: Mon, 27 Jun 2022 11:17:38 -0700 Subject: [PATCH 3/6] Make monads serializable --- src/utils/monads.ts | 71 +++++++++++++++++++++++++++++++++++++++++++++ test/monads.spec.ts | 25 ++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 test/monads.spec.ts 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/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); + }); +}); From 38375bcec07af0f8a6fe81123e23d79d9308de3f Mon Sep 17 00:00:00 2001 From: Joseph Dylan Stewart Date: Fri, 8 Jul 2022 11:57:17 -0700 Subject: [PATCH 4/6] Deprecate caching Deprecating caching because it is better handled by the calling code. --- src/UserCodeRunner.ts | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/UserCodeRunner.ts b/src/UserCodeRunner.ts index 66a89de..9808d93 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 { SourceMapConsumer } from 'source-map'; -import LRUCache from 'lru-cache'; import { Result } from './utils/monads.js'; import { TypeGuard } from './utils/typeGuardCombinators'; @@ -23,20 +21,13 @@ export interface CacheItem { } 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); } @@ -175,10 +166,6 @@ export class UserCodeRunner { }); } - private static hash(str: string): string { - return crypto.createHash('sha1').update(str).digest('base64'); - } - public async executeUserCode( userCode: string, args: ArgsType, @@ -188,15 +175,7 @@ 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('')}`); - - if (!this.user_file_cache.has(userCodeHash)) { - const result = await this.preProcess(userCode, outputType, argsTypes, additionalSourceFiles); - - this.user_file_cache.set(userCodeHash, result); - } - - const result = this.user_file_cache.get(userCodeHash)!; + const result = await this.preProcess(userCode, outputType, argsTypes, additionalSourceFiles); if (result.isErr()) { return result; From b903aba3c854f630795c7d2109fd5cf073b4436f Mon Sep 17 00:00:00 2001 From: Joseph Dylan Stewart Date: Fri, 8 Jul 2022 11:57:50 -0700 Subject: [PATCH 5/6] Add method to execute code from JS files and user code source map --- src/UserCodeRunner.ts | 13 ++++- test/UserCodeRunner.spec.ts | 106 ++++++++++++++++++++++-------------- 2 files changed, 78 insertions(+), 41 deletions(-) diff --git a/src/UserCodeRunner.ts b/src/UserCodeRunner.ts index 9808d93..65fa3d0 100644 --- a/src/UserCodeRunner.ts +++ b/src/UserCodeRunner.ts @@ -183,6 +183,17 @@ export class UserCodeRunner { 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; context.__result = undefined; @@ -213,7 +224,7 @@ export class UserCodeRunner { }); return Result.Ok(context.__result); } catch (error: any) { - return Result.Err([UserCodeRuntimeError.new(error as Error, await new SourceMapConsumer(userCodeSourceMap))]); + return Result.Err([UserCodeRuntimeError.new(error as Error, await new SourceMapConsumer(sourceMap))]); } } } diff --git a/test/UserCodeRunner.spec.ts b/test/UserCodeRunner.spec.ts index b8d0200..ea6deb5 100644 --- a/test/UserCodeRunner.spec.ts +++ b/test/UserCodeRunner.spec.ts @@ -46,48 +46,48 @@ describe('behavior', () => { }); it('should produce runtime errors from additional files', async () => { - const userCode = ` - export default function MyDSLFunction(thing: string): string { - throwingLibraryFunction(); - return thing + ' world'; - } - `.trimTemplate(); + 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 = ` @@ -634,13 +634,39 @@ describe('behavior', () => { 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:232:24) + at UserCodeRunner.executeUserCodeFromArtifacts (/Users/jdstewar/gitRepos/jpl/mpcs/aerie/aerie-ts-user-code-runner/src/UserCodeRunner.ts:222:24) at Object. (/Users/jdstewar/gitRepos/jpl/mpcs/aerie/aerie-ts-user-code-runner/test/UserCodeRunner.spec.ts:614:7) `.trimTemplate()); } }); -}) + 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'); + }); +}); describe('regression tests', () => { test('Aerie command expansion throw Regression Test', async () => { From 49c0e03e0dd6995e1d880524c7192e3ea2781f90 Mon Sep 17 00:00:00 2001 From: Joseph Dylan Stewart Date: Tue, 30 Aug 2022 11:00:38 -0700 Subject: [PATCH 6/6] Refactor expectation to be machine-agnostic --- test/UserCodeRunner.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/UserCodeRunner.spec.ts b/test/UserCodeRunner.spec.ts index ea6deb5..9f65612 100644 --- a/test/UserCodeRunner.spec.ts +++ b/test/UserCodeRunner.spec.ts @@ -630,13 +630,13 @@ describe('behavior', () => { Inherited from: This is a test error `.trimTemplate()); - expect(err.stack).toBe(` + expect(err.stack).toContain(` Error: This is a test error at additionalFile:1:7 at SourceTextModule.evaluate (node:internal/vm/module:224:23) - at UserCodeRunner.executeUserCodeFromArtifacts (/Users/jdstewar/gitRepos/jpl/mpcs/aerie/aerie-ts-user-code-runner/src/UserCodeRunner.ts:222:24) - at Object. (/Users/jdstewar/gitRepos/jpl/mpcs/aerie/aerie-ts-user-code-runner/test/UserCodeRunner.spec.ts:614:7) `.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/); } });