• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

source-academy / py-slang / 28742804235

05 Jul 2026 01:44PM UTC coverage: 78.303% (-0.4%) from 78.683%
28742804235

push

github

web-flow
Renaming variables to match SICPy and remove unnecessary functions (#193)

* renaming

* Update docs/md/README_1.md

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* gemini review responses

* format

* new failing test case corrected

* adding is_number, needed by SICP afterall

* addressed Shrey's review

* format

* getting test coverage back up

* fix(stepper): update preprocessing-error test expectations after rename

The is_int->is_integer rename (and map_linked_list->map) shifted the
fast-levenshtein "perhaps you meant" suggestions that two getSteps
preprocessing tests asserted verbatim, breaking CI:

- `missing` no longer suggests 'is_int' (renamed to is_integer, now out of
  suggestion range) -> the suggestion line is gone.
- `map` in §1 now suggests 'max' (map became a §2 list-library name) -> a
  suggestion line is added.

Re-derived both expected strings from the actual analyzer output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Remove int/float/bool builtins (#184 Removals); restore len and §1 MISC docs

Per #184's Removals section, drop the int()/float()/bool() conversion builtins
from both evaluators, the tests and the LaTeX spec:
- src/stdlib/misc.ts: remove the three static builtins, plus the now-unused
  ControlItem/isFalsy/ValueError imports
- src/conductor/stepper/builtins.ts: remove the three entries, the now-dead
  truthy() helper, and their BUILTIN_MIN_ARGS / docstring / comment mentions
- docs/specs/python_misc.tex: drop int/float/bool (keep complex/str)
- tests: prune the int/float/bool cases; re-derive two levenshtein "perhaps you
  meant" suggestions that shifted now that `int` is gone (llist -> 'print')

Also fix two accidental doc regressions this branch introduced (flagged in
review, unrelated to #184):
- docs/lib/misc.js: restore the len() stub (it should not have moved at all)
- docs/specs/python_1.tex: restore \input python_misc (the MISC library i... (continued)

2460 of 3408 branches covered (72.18%)

Branch coverage included in aggregate %.

52 of 54 new or added lines in 6 files covered. (96.3%)

7 existing lines in 2 files now uncovered.

5884 of 7248 relevant lines covered (81.18%)

13817.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.53
/src/tests/utils.ts
1
import { ConductorError, ErrorType } from "@sourceacademy/conductor/common";
11✔
2
import { StmtNS } from "../ast-types";
3
import { Context } from "../engines/cse/context";
11✔
4
import { CSEResultPromise, evaluate, IOptions } from "../engines/cse/interpreter";
11✔
5
import { Stash, Value } from "../engines/cse/stash";
11✔
6
import { displayError } from "../engines/cse/streams";
11✔
7
import { SVMLCompiler } from "../engines/svml/svml-compiler";
11✔
8
import { SVMLInterpreter } from "../engines/svml/svml-interpreter";
11✔
9
import { RuntimeSourceError } from "../errors";
11✔
10
import { parse } from "../parser/parser-adapter";
11✔
11
import { Resolver } from "../resolver";
11✔
12
import math from "../stdlib/math";
11✔
13
import misc from "../stdlib/misc";
11✔
14
import { Group } from "../stdlib/utils";
15
import { PyComplexNumber, RecursivePartial, Result } from "../types";
11✔
16
import { makeValidatorsForChapter } from "../validator";
11✔
17
import Stmt = StmtNS.Stmt;
18

19
/**
20
 * Test-local replacement for the deleted pyRunner.runInContext.
21
 * Orchestrates: load groups → parse → resolve → evaluate → wrap result.
22
 */
23
async function runInContext(
24
  code: string,
25
  context: Context,
26
  options: RecursivePartial<IOptions> = {},
×
27
): Promise<Result> {
28
  // Load groups into context (builtins + preludes)
29
  if (!options.isPrelude && options.groups) {
1,752✔
30
    let prelude = "";
1,300✔
31
    for (const group of options.groups as Group[]) {
1,300✔
32
      for (const [name, value] of group.builtins) {
4,032✔
33
        context.nativeStorage.builtins.set(name, value);
106,101✔
34
      }
35
      prelude += group.prelude + "\n";
4,032✔
36
    }
37
    if (prelude.trim()) {
1,300✔
38
      await runInContext(prelude, context, { ...options, isPrelude: true, groups: [] });
452✔
39
    }
40
  }
41

42
  // Parse
43
  let pyAst: Stmt;
44
  try {
1,752✔
45
    const script = code + "\n";
1,752✔
46
    pyAst = parse(script);
1,752✔
47
    if (!options.isPrelude) {
1,752✔
48
      const resolver = new Resolver(
1,300✔
49
        script,
50
        pyAst,
51
        makeValidatorsForChapter(options.variant ?? 1),
1,300!
52
        (options.groups as Group[]) ?? [],
1,300!
53
        Object.keys(context.runtime.environments[0].head),
54
      );
55
      const errors = resolver.resolve(pyAst);
1,300✔
56
      if (errors.length > 0) throw errors[0];
1,300✔
57
    }
58
  } catch (error) {
59
    await displayError(context, error, ErrorType.EVALUATOR_SYNTAX);
2✔
60
    return CSEResultPromise(context, { type: "error", message: String(error) });
2✔
61
  }
62

63
  // Evaluate
64
  const result = await evaluate(code, pyAst, context, options);
1,750✔
65
  return CSEResultPromise(context, result);
1,750✔
66
}
67

68
// eslint-disable-next-line @typescript-eslint/no-explicit-any
69
type Class<T> = new (...args: any[]) => T;
70

71
export type TestOutputValue =
72
  | bigint
73
  | number
74
  | boolean
75
  | string
76
  | null
77
  | PyComplexNumber
78
  | TestOutputValue[];
79

80
export type TestErrorValue = Class<RuntimeSourceError> | Class<Error>;
81

82
export type TestExpectedValue = TestOutputValue | TestErrorValue;
83
/**
84
 * TestCases is a mapping from arguments to `describe` blocks, which map to an array of tuples of the form [code, expected, output], where:
85
 * - `code` is the code to be executed
86
 * - `expected` is the expected value of the expression, which can be a primitive value, null (for None), or an error class (for expected errors)
87
 * - `output` is the expected output to be printed to the console, or null if no output is expected
88
 */
89
export type TestCases = Record<string, [string, TestExpectedValue, string[] | null][]>;
90

91
export function toPythonAst(text: string): Stmt {
11✔
92
  const script = text + "\n";
99✔
93
  return parse(script);
99✔
94
}
95

96
export function toPythonAstAndResolve(text: string, variant: number): Stmt {
11✔
97
  const script = text + "\n";
99✔
98
  const ast = toPythonAst(text);
99✔
99
  const resolver = new Resolver(script, ast, makeValidatorsForChapter(variant), [misc, math]);
91✔
100
  const errors = resolver.resolve(ast);
91✔
101
  if (errors.length > 0) {
91✔
102
    throw errors[0];
65✔
103
  }
104
  return ast;
26✔
105
}
106

107
type InternalTestCase = {
108
  label: TestExpectedValue;
109
  code: string;
110
  expected: TestExpectedValue;
111
  output: string[] | null;
112
};
113

114
export const createInternalTestCases = (tests: TestCases[string]): InternalTestCase[] => {
11✔
115
  return tests.map(([code, expected, output]) => ({
1,300✔
116
    label:
117
      JSON.stringify(code) +
118
      " should " +
119
      (expected instanceof Function &&
3,059✔
120
      (expected.prototype instanceof RuntimeSourceError || expected.prototype instanceof Error)
121
        ? "throw " + expected.name
122
        : "return " + expected),
123
    code,
124
    expected,
125
    output,
126
  }));
127
};
128

129
type OutputType =
130
  | {
131
      type: "stdout";
132
      value: string;
133
    }
134
  | {
135
      type: "stderr";
136
      value: ConductorError;
137
    };
138
export const generateMockStreams = (context: Context, output: OutputType[]) => {
11✔
139
  const stdOutStream = new WritableStream<string>({
1,300✔
140
    write: (data: string) => {
141
      output.push({ type: "stdout", value: data });
154✔
142
    },
143
  });
144

145
  const stdErrStream = new WritableStream<ConductorError>({
1,300✔
146
    write: (data: ConductorError) => {
147
      output.push({ type: "stderr", value: data });
456✔
148
    },
149
  });
150

151
  const stdinStream = new ReadableStream<string>({
1,300✔
152
    start() {
153
      // No-op: we won't be pushing any data to stdin in our tests
154
    },
155
    pull() {
156
      // No-op
157
    },
158
    cancel() {
159
      // No-op
160
    },
161
  });
162
  context.streams = {
1,300✔
163
    initialised: true,
164
    stdout: {
165
      stream: stdOutStream,
166
      writer: stdOutStream.getWriter(),
167
    },
168
    stderr: {
169
      stream: stdErrStream,
170
      writer: stdErrStream.getWriter(),
171
    },
172
    stdin: {
173
      stream: stdinStream,
174
      reader: stdinStream.getReader(),
175
    },
176
  };
177
};
178

179
/**
180
 * Generates test cases for a given variant of the CSE evaluator based on the provided TestCases object.
181
 * @param testCases The test cases to generate, organized by function name and consisting of tuples of [code, expected, output].
182
 * @param variant The variant of the CSE evaluator to test (e.g., 1 for Python §1)
183
 * @param groups The groups to load into the context before running the test cases (e.g., [`linkedList`, `list`]).
184
 */
185
export const generateTestCases = (testCases: TestCases, variant: number, groups: Group[]) => {
11✔
186
  for (const [funcName, tests] of Object.entries(testCases)) {
33✔
187
    describe(funcName, () => {
149✔
188
      afterEach(() => {
149✔
189
        jest.restoreAllMocks(); // Automatically restores all spyOn mocks
1,300✔
190
      });
191
      test.each(createInternalTestCases(tests))(`$label`, async ({ code, expected, output }) => {
149✔
192
        const spy = jest.spyOn(Stash.prototype, "pop");
1,300✔
193
        const context = new Context();
1,300✔
194

195
        const outputLst: OutputType[] = [];
1,300✔
196
        generateMockStreams(context, outputLst);
1,300✔
197
        const result = await runInContext(code, context, { variant, groups });
1,300✔
198
        expect(result.status).toBe("finished");
1,300✔
199

200
        if (typeof expected === "function" && expected.prototype instanceof RuntimeSourceError) {
1,300✔
201
          expect(context.errors.length).toBeGreaterThan(0);
453✔
202
          expect(context.errors[0]).toHaveProperty("constructor", expected);
453✔
203
          return;
453✔
204
        }
205
        if (typeof expected === "function" && expected.prototype instanceof Error) {
847✔
206
          expect(result).toHaveProperty("value.message", expect.stringContaining(expected.name));
3✔
207
          return;
3✔
208
        }
209
        expect(outputLst.filter(item => item.type === "stderr")).toStrictEqual([]);
844✔
210

211
        expect(result.status).not.toHaveProperty("value.type", "error");
844✔
212
        if (output !== null) {
844✔
213
          expect(outputLst).toEqual(output.map(line => ({ type: "stdout", value: line + "\n" })));
154✔
214
        }
215

216
        const generateExpectedValueAssertion = (expected: TestOutputValue): Value => {
844✔
217
          if (expected === null) {
895✔
218
            return { type: "none" };
91✔
219
          }
220

221
          if (typeof expected === "bigint") {
804✔
222
            return { type: "bigint", value: expected };
236✔
223
          }
224

225
          if (typeof expected === "number") {
568✔
226
            if (isNaN(expected)) {
58!
UNCOV
227
              return { type: "number", value: NaN };
×
228
            }
229
            return { type: "number", value: expect.closeTo(expected) };
58✔
230
          }
231

232
          if (typeof expected === "boolean") {
510✔
233
            return { type: "bool", value: expected };
350✔
234
          }
235

236
          if (expected instanceof PyComplexNumber) {
160✔
237
            return {
94✔
238
              type: "complex",
239
              value: expect.objectContaining({
240
                real: isNaN(expected.real) ? NaN : expect.closeTo(expected.real),
94!
241
                imag: isNaN(expected.imag) ? NaN : expect.closeTo(expected.imag),
94✔
242
              }),
243
            };
244
          }
245

246
          if (Array.isArray(expected)) {
66✔
247
            return {
19✔
248
              type: "list",
249
              value: expected.map(generateExpectedValueAssertion),
250
            };
251
          }
252

253
          return { type: "string", value: expected };
47✔
254
        };
255
        expect(spy).toHaveLastReturnedWith(
844✔
256
          expect.objectContaining(generateExpectedValueAssertion(expected as TestOutputValue)),
257
        );
258
        return;
844✔
259
      });
260
    });
261
  }
262
};
263

264
// ---------------------------------------------------------------------------
265
// SVML test utilities
266
// ---------------------------------------------------------------------------
267

268
// eslint-disable-next-line @typescript-eslint/no-explicit-any
269
type ErrorClass = new (...args: any[]) => Error;
270

271
/**
272
 * Expected value for an SVML test case.
273
 * SVML's toJSValue returns JS primitives directly, so no bigint or PyComplexNumber.
274
 * `undefined` means the expression should evaluate to Python None / no return value.
275
 */
276
export type SVMLTestExpectedValue = number | boolean | string | null | undefined | ErrorClass;
277

278
/**
279
 * Same shape as TestCases but with SVML-compatible expected values.
280
 * Each tuple: [code, expected, output].
281
 *   - expected: a JS primitive, undefined, null, or an Error subclass (for expected throws)
282
 *   - output: expected print outputs, or null if none expected
283
 */
284
export type SVMLTestCases = Record<string, [string, SVMLTestExpectedValue, string[] | null][]>;
285

286
export const generateSVMLTestCases = (testCases: SVMLTestCases) => {
11✔
287
  for (const [sectionName, tests] of Object.entries(testCases)) {
5✔
288
    describe(sectionName, () => {
18✔
289
      test.each(
18✔
290
        tests.map(([code, expected, output]) => ({
55✔
291
          code,
292
          expected,
293
          output,
294
          label: typeof expected === "function" ? expected.name : JSON.stringify(expected),
55✔
295
        })),
296
      )("$code → $label", ({ code, expected, output }) => {
297
        const source = code.endsWith("\n") ? code : code + "\n";
55!
298
        if (typeof expected === "function") {
55✔
299
          expect(() => {
6✔
300
            const ast = parse(source);
6✔
301
            const program = SVMLCompiler.fromProgram(ast).compileProgram(ast);
6✔
302
            new SVMLInterpreter(program).execute();
6✔
303
          }).toThrow(expected);
304
          return;
6✔
305
        }
306

307
        const outputs: string[] = [];
49✔
308
        const ast = parse(source);
49✔
309
        const program = SVMLCompiler.fromProgram(ast).compileProgram(ast);
49✔
310
        const interpreter = new SVMLInterpreter(program, {
49✔
311
          sendOutput: msg => outputs.push(msg),
4✔
312
        });
313
        const result = SVMLInterpreter.toJSValue(interpreter.execute());
49✔
314

315
        if (expected === undefined) {
49✔
316
          expect(result).toBeUndefined();
4✔
317
        } else if (expected === null) {
45!
318
          expect(result).toBeNull();
×
319
        } else if (typeof expected === "number" && !Number.isInteger(expected)) {
45!
320
          expect(result).toBeCloseTo(expected);
×
321
        } else {
322
          expect(result).toBe(expected);
45✔
323
        }
324

325
        if (output !== null) {
49✔
326
          expect(outputs).toEqual(output);
2✔
327
        }
328
      });
329
    });
330
  }
331
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc