From 75813f76668fba0bac21f44126d1430d7e8bdb3e Mon Sep 17 00:00:00 2001 From: Joseph Dylan Stewart Date: Wed, 3 Aug 2022 16:53:09 -0700 Subject: [PATCH 1/2] Add regression tests for throws outside default function execution path --- test/UserCodeRunner.spec.ts | 71 +++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/test/UserCodeRunner.spec.ts b/test/UserCodeRunner.spec.ts index ef6046d..f45a41f 100644 --- a/test/UserCodeRunner.spec.ts +++ b/test/UserCodeRunner.spec.ts @@ -1089,6 +1089,77 @@ it('should handle unnamed arrow function default exports assignment', async () = 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:234:24) + at Object. (/Users/jdstewar/gitRepos/jpl/mpcs/aerie/aerie-ts-user-code-runner/test/UserCodeRunner.spec.ts:1090:5) + `.trimTemplate()); + } +}); + test('Aerie incorrect stack frame assumption regression test', async () => { const userCode = ` export default () => { From ca5cd7502a368209500e7aad50a03d8a1afcd47d Mon Sep 17 00:00:00 2001 From: Joseph Dylan Stewart Date: Wed, 3 Aug 2022 16:53:43 -0700 Subject: [PATCH 2/2] Add fix for throws without the usercode in the stack --- src/UserCodeRunner.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/UserCodeRunner.ts b/src/UserCodeRunner.ts index 96efb2b..9d07d7d 100644 --- a/src/UserCodeRunner.ts +++ b/src/UserCodeRunner.ts @@ -4,7 +4,7 @@ import path from 'path'; import { defaultErrorCodeMessageMappers } from './defaultErrorCodeMessageMappers.js'; import { createMapDiagnosticMessage } from './utils/errorMessageMapping.js'; import ts from 'typescript'; -import { parse } from 'stack-trace'; +import { parse, StackFrame } from 'stack-trace'; import { BasicSourceMapConsumer, IndexedSourceMapConsumer, SourceMapConsumer } from 'source-map'; import LRUCache from 'lru-cache'; import { Result } from './utils/monads.js'; @@ -348,12 +348,19 @@ 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) { 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) { + this.error.message = 'Error: Runtime error detected outside of user code execution path. This is most likely a bug in the additional library source.\nInherited from:\n' + this.error.message; + throw this.error; + } } public get message(): string { @@ -361,8 +368,7 @@ export class UserCodeRuntimeError extends UserCodeError { } public get stack(): string { - const stack = parse(this.error); - const stackWithoutHarness = stack + const stackWithoutHarness = this.stackFrames .filter(callSite => callSite.getFileName()?.endsWith(USER_CODE_FILENAME)) .filter(callSite => { if (callSite.getFileName() === undefined) { @@ -390,10 +396,7 @@ export class UserCodeRuntimeError extends UserCodeError { public get location(): { line: number; column: number } { const stack = parse(this.error); - const userFileStackFrame = stack.find(callSite => callSite.getFileName() === USER_CODE_FILENAME); - if (userFileStackFrame === undefined) { - throw new Error('Runtime error detected outside of user code execution path. This is most likely a bug in the additional library source.'); - } + const userFileStackFrame = stack.find(callSite => callSite.getFileName() === USER_CODE_FILENAME)!; const originalPosition = this.sourceMap.originalPositionFor({ line: userFileStackFrame.getLineNumber()!, column: userFileStackFrame.getColumnNumber()!,