Skip to content

Commit b4de94d

Browse files
committed
Refactor runners to include execution method tests
1 parent 95ccecd commit b4de94d

File tree

7 files changed

+626
-284
lines changed

7 files changed

+626
-284
lines changed

src/runner/__tests__/execMethod.ts

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
import { JestAssertionError } from 'expect'
2+
import runners, { type RunnerTypes } from '../sourceRunner'
3+
import { Chapter, type ExecutionMethod, Variant } from '../../types'
4+
import type { Runner } from '../types'
5+
import { DEFAULT_SOURCE_OPTIONS, runCodeInSource } from '..'
6+
import { mockContext } from '../../utils/testing/mocks'
7+
import { getChapterName, objectKeys, objectValues } from '../../utils/misc'
8+
import { asMockedFunc } from '../../utils/testing/misc'
9+
import { parseError } from '../..'
10+
11+
jest.mock('../sourceRunner', () => ({
12+
default: new Proxy({} as Record<string, Runner>, {
13+
get: (obj, prop: string) => {
14+
if (!(prop in obj)) {
15+
const mockRunner: Runner = (_, context) =>
16+
Promise.resolve({
17+
status: 'finished',
18+
value: '',
19+
context
20+
})
21+
22+
obj[prop] = jest.fn(mockRunner)
23+
}
24+
return obj[prop]
25+
}
26+
})
27+
}))
28+
29+
// Required since Typed variant tries to load modules
30+
jest.mock('../../modules/loader')
31+
32+
beforeEach(() => {
33+
jest.clearAllMocks()
34+
})
35+
36+
interface TestCase {
37+
chapter?: Chapter
38+
variant?: Variant
39+
code?: string
40+
/**
41+
* Set this to simulate the options having
42+
* a specific execution method set
43+
*/
44+
optionMethod?: ExecutionMethod
45+
/**
46+
* Set this to simulate the context having a specific
47+
* execution method set
48+
*/
49+
contextMethod?: ExecutionMethod
50+
}
51+
52+
interface FullTestCase extends TestCase {
53+
/**
54+
* Which runner was expected to be called
55+
*/
56+
expectedRunner: RunnerTypes
57+
58+
/**
59+
* Should the runner have evaluated the prelude?
60+
*/
61+
expectedPrelude: boolean
62+
63+
verboseErrors?: boolean
64+
}
65+
66+
const sourceCases: FullTestCase[] = [
67+
{
68+
chapter: Chapter.SOURCE_1,
69+
variant: Variant.DEFAULT,
70+
expectedRunner: 'native',
71+
expectedPrelude: true
72+
},
73+
{
74+
chapter: Chapter.SOURCE_2,
75+
variant: Variant.DEFAULT,
76+
expectedRunner: 'native',
77+
expectedPrelude: true
78+
},
79+
{
80+
chapter: Chapter.SOURCE_3,
81+
variant: Variant.DEFAULT,
82+
expectedRunner: 'native',
83+
expectedPrelude: true
84+
},
85+
{
86+
chapter: Chapter.SOURCE_4,
87+
variant: Variant.DEFAULT,
88+
expectedRunner: 'native',
89+
expectedPrelude: true
90+
},
91+
{
92+
contextMethod: 'native',
93+
variant: Variant.NATIVE,
94+
expectedRunner: 'native',
95+
expectedPrelude: false
96+
}
97+
]
98+
99+
// These JS cases never evaluate a prelude,
100+
// nor ever have verbose errors enabled
101+
const fullJSCases: TestCase[] = [
102+
{ chapter: Chapter.FULL_JS },
103+
{ chapter: Chapter.FULL_TS }
104+
]
105+
106+
// The alt langs never evaluate a prelude,
107+
// always use fullJS regardless of variant,
108+
// but we don't need to check for verbose errors
109+
const altLangCases: Chapter[] = [Chapter.PYTHON_1]
110+
111+
type TestObject = {
112+
code: string
113+
chapter: Chapter
114+
variant: Variant
115+
expectedPrelude: boolean
116+
expectedRunner: RunnerTypes
117+
optionMethod?: ExecutionMethod
118+
contextMethod?: ExecutionMethod
119+
}
120+
121+
function expectCalls(count: number, expected: RunnerTypes) {
122+
const unexpectedRunner = objectKeys(runners).find(runner => {
123+
const { calls } = asMockedFunc(runners[runner]).mock
124+
return calls.length > 0
125+
})
126+
127+
switch (unexpectedRunner) {
128+
case undefined:
129+
throw new JestAssertionError(`Expected ${expected} to be called ${count} times, but no runners were called`)
130+
case expected: {
131+
expect(runners[expected]).toHaveBeenCalledTimes(count)
132+
return asMockedFunc(runners[expected]).mock.calls
133+
}
134+
default: {
135+
const callCount = asMockedFunc(runners[unexpectedRunner]).mock.calls.length
136+
throw new JestAssertionError(`Expected ${expected} to be called ${count} times, but ${unexpectedRunner} was called ${callCount} times`)
137+
}
138+
}
139+
}
140+
141+
async function testCase({
142+
code,
143+
chapter,
144+
variant,
145+
contextMethod,
146+
optionMethod,
147+
expectedPrelude,
148+
expectedRunner
149+
}: TestObject) {
150+
const context = mockContext(chapter, variant)
151+
if (contextMethod !== undefined) {
152+
context.executionMethod = contextMethod
153+
}
154+
155+
// Check if the prelude is null before execution
156+
// because the prelude gets set to null if it wasn't before
157+
const shouldPrelude = expectedPrelude && context.prelude !== null
158+
const options = { ...DEFAULT_SOURCE_OPTIONS }
159+
160+
if (optionMethod !== undefined) {
161+
options.executionMethod = optionMethod
162+
}
163+
164+
await runCodeInSource(code, context, options)
165+
166+
if (context.errors.length > 0) {
167+
console.log(parseError(context.errors))
168+
}
169+
170+
expect(context.errors.length).toEqual(0)
171+
172+
if (shouldPrelude) {
173+
// If the prelude was to be evaluated and the prelude is not null,
174+
// the runner should be called twice
175+
const [call0, call1] = expectCalls(2, expectedRunner)
176+
177+
// First with isPrelude true
178+
expect(call0[2].isPrelude).toEqual(true)
179+
180+
// and then with isPrelude false
181+
expect(call1[2].isPrelude).toEqual(false)
182+
} else {
183+
// If not, the runner should only have been called once
184+
const [call0] = expectCalls(1, expectedRunner)
185+
186+
// with isPrelude false
187+
expect(call0[2].isPrelude).toEqual(false)
188+
}
189+
}
190+
191+
function testCases(desc: string, cases: FullTestCase[]) {
192+
describe(desc, () =>
193+
test.each(
194+
cases.map(({ code, verboseErrors, contextMethod, chapter, variant, ...tc }, i) => {
195+
chapter = chapter ?? Chapter.SOURCE_1
196+
variant = variant ?? Variant.DEFAULT
197+
const context = mockContext(chapter, variant)
198+
if (contextMethod !== undefined) {
199+
context.executionMethod = contextMethod
200+
}
201+
202+
const chapterName = getChapterName(chapter)
203+
let desc = `${i + 1}. Testing ${chapterName}, Variant: ${variant}, expected ${tc.expectedRunner} runner`
204+
code = code ?? ''
205+
if (verboseErrors) {
206+
code = `"enable verbose";\n${code}`
207+
desc += ' (verbose errors)'
208+
}
209+
210+
return [desc, { code, chapter, variant, ...tc }]
211+
})
212+
)('%s', async (_, to) => testCase(to))
213+
)
214+
}
215+
216+
describe('Ensure that the correct runner is used for the given evaluation context and settings', () => {
217+
testCases('Test regular source cases', sourceCases)
218+
testCases(
219+
'Test source verbose error cases',
220+
sourceCases.map(
221+
tc => ({
222+
...tc,
223+
verboseErrors: true,
224+
expectedRunner: 'cse-machine'
225+
})
226+
)
227+
)
228+
229+
testCases(
230+
'Test source cases with debugger statements',
231+
sourceCases.map(
232+
tc => ({
233+
...tc,
234+
code: 'debugger;\n' + (tc.code ?? ''),
235+
expectedRunner: 'cse-machine'
236+
})
237+
)
238+
)
239+
240+
testCases('Test explicit control variant', sourceCases.map(tc => ({
241+
...tc,
242+
variant: Variant.EXPLICIT_CONTROL,
243+
expectedRunner: 'cse-machine'
244+
})))
245+
246+
testCases(
247+
'Test FullJS cases',
248+
fullJSCases.flatMap((tc): FullTestCase[] => {
249+
const fullCase: FullTestCase = {
250+
...tc,
251+
verboseErrors: false,
252+
expectedPrelude: false,
253+
expectedRunner: 'fulljs'
254+
}
255+
256+
const verboseErrorCase: FullTestCase = {
257+
...fullCase,
258+
verboseErrors: true
259+
}
260+
261+
return [fullCase, verboseErrorCase]
262+
})
263+
)
264+
265+
testCases(
266+
'Test alt-langs',
267+
altLangCases.flatMap(chapter =>
268+
objectValues(Variant).map(
269+
(variant): FullTestCase => ({
270+
code: '',
271+
variant,
272+
chapter,
273+
expectedPrelude: false,
274+
expectedRunner: 'fulljs',
275+
verboseErrors: false
276+
})
277+
)
278+
)
279+
)
280+
281+
test('if optionMethod is specified, verbose errors is ignored', () => testCase({
282+
code: '"enable verbose"; 0;',
283+
optionMethod: 'native',
284+
chapter: Chapter.SOURCE_4,
285+
variant: Variant.DEFAULT,
286+
expectedPrelude: true,
287+
expectedRunner: 'native'
288+
}))
289+
290+
test('if optionMethod is specified, debubger statements are ignored', () => testCase({
291+
code: 'debugger; 0;',
292+
optionMethod: 'native',
293+
chapter: Chapter.SOURCE_4,
294+
variant: Variant.DEFAULT,
295+
expectedPrelude: true,
296+
expectedRunner: 'native'
297+
}))
298+
299+
test('if contextMethod is specified, verbose errors is ignored', () => testCase({
300+
code: '"enable verbose"; 0;',
301+
contextMethod: 'native',
302+
chapter: Chapter.SOURCE_4,
303+
variant: Variant.DEFAULT,
304+
expectedPrelude: true,
305+
expectedRunner: 'native'
306+
}))
307+
308+
test('if contextMethod is specified, debugger statements are ignored', () => testCase({
309+
code: 'debugger; 0;',
310+
contextMethod: 'native',
311+
chapter: Chapter.SOURCE_4,
312+
variant: Variant.DEFAULT,
313+
expectedPrelude: true,
314+
expectedRunner: 'native'
315+
}))
316+
317+
test('optionMethod takes precedence over contextMethod', () => testCase({
318+
code: '0;',
319+
contextMethod: 'native',
320+
optionMethod: 'cse-machine',
321+
chapter: Chapter.SOURCE_4,
322+
variant: Variant.DEFAULT,
323+
expectedPrelude: true,
324+
expectedRunner: 'cse-machine'
325+
}))
326+
327+
test('debugger statements require cse-machine', () => testCase({
328+
code: 'debugger; 0;',
329+
chapter: Chapter.SOURCE_4,
330+
variant: Variant.DEFAULT,
331+
expectedPrelude: true,
332+
expectedRunner: 'cse-machine'
333+
}))
334+
})

src/runner/__tests__/modules.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { mockContext } from '../../utils/testing/mocks'
22
import { Chapter, Variant } from '../../types'
33
import { stripIndent } from '../../utils/formatters'
44
import { expectFinishedResultValue } from '../../utils/testing/misc'
5-
import { runCodeInSource } from '../sourceRunner'
5+
import { runCodeInSource } from '..'
6+
import { getChapterName } from '../../utils/misc'
67

78
jest.mock('../../modules/loader/loaders')
89

@@ -57,7 +58,7 @@ describe.each(describeCases)(
5758
'Ensuring that %s chapters are able to load modules',
5859
(_, chapters, variants, code) => {
5960
const chapterCases = chapters.map(chapterVal => {
60-
const [chapterName] = Object.entries(Chapter).find(([, value]) => value === chapterVal)!
61+
const chapterName = getChapterName(chapterVal)
6162
const index = chapters.indexOf(chapterVal)
6263
const variant = variants[index]
6364
return [`Testing ${chapterName}`, chapterVal, variant] as [string, Chapter, Variant]

src/runner/fullJSRunner.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ import { generate } from 'astring'
33
import type es from 'estree'
44
import { RawSourceMap } from 'source-map'
55

6-
import type { Result } from '..'
76
import { NATIVE_STORAGE_ID } from '../constants'
87
import { RuntimeSourceError } from '../errors/runtimeSourceError'
9-
import type { ImportOptions } from '../modules/moduleTypes'
108
import { parse } from '../parser/parser'
119
import {
1210
evallerReplacer,
@@ -19,6 +17,7 @@ import * as create from '../utils/ast/astCreator'
1917
import { getFunctionDeclarationNamesInProgram } from '../utils/uniqueIds'
2018
import { toSourceError } from './errors'
2119
import { resolvedErrorPromise } from './utils'
20+
import { Runner } from './types'
2221

2322
function fullJSEval(code: string, nativeStorage: NativeStorage): any {
2423
if (nativeStorage.evaller) {
@@ -46,11 +45,7 @@ function containsPrevEval(context: Context): boolean {
4645
return context.nativeStorage.evaller != null
4746
}
4847

49-
export async function fullJSRunner(
50-
program: es.Program,
51-
context: Context,
52-
importOptions: ImportOptions
53-
): Promise<Result> {
48+
const fullJSRunner: Runner = async (program, context) => {
5449
// prelude & builtins
5550
// only process builtins and preludes if it is a fresh eval context
5651
const prelude = preparePrelude(context)
@@ -91,3 +86,5 @@ export async function fullJSRunner(
9186
return resolvedErrorPromise
9287
}
9388
}
89+
90+
export default fullJSRunner

0 commit comments

Comments
 (0)