|
| 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 | +}) |
0 commit comments