Skip to content

Commit abecbd9

Browse files
feat: reinstate support for object syntax (#409)
* Revert "fix: handle invalid python syntax (#408)" This reverts commit 2cb62d8. * Revert "feat: drop support for object form python tests (#401)" This reverts commit d92cf4a. * feat: stop supporting simple tests + input arrays
1 parent 2cb62d8 commit abecbd9

File tree

3 files changed

+123
-108
lines changed

3 files changed

+123
-108
lines changed

packages/python-evaluator/src/python-test-evaluator.ts

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import { format } from "../../shared/src/format";
2222
import { ProxyConsole } from "../../shared/src/proxy-console";
2323
import { createAsyncIife } from "../../shared/src/async-iife";
2424

25+
type EvaluatedTeststring = {
26+
test: () => Promise<unknown>;
27+
};
28+
2529
const READY_MESSAGE: ReadyEvent["data"] = { type: "ready" };
2630

2731
function isProxy(raw: unknown): raw is PyProxy {
@@ -98,30 +102,64 @@ class PythonTestEvaluator implements TestEvaluator {
98102

99103
/* eslint-enable @typescript-eslint/no-unused-vars */
100104

101-
try {
102-
// If input isn't reassigned, it will throw when called during testing.
103-
runPython(`
105+
// If input is not faked, tests can fail with io exceptions.
106+
runPython(`
104107
def __fake_input(arg=None):
105108
return ""
106109
107110
input = __fake_input
108111
`);
109-
eval(opts.hooks?.beforeEach ?? "");
110112

111-
const iifeTest = createAsyncIife(testString);
113+
try {
114+
eval(opts.hooks?.beforeEach ?? "");
115+
// Eval test string to get the dummy input and actual test
116+
const evaluatedTestString = await new Promise<unknown>(
117+
(resolve, reject) => {
118+
try {
119+
const test: unknown = eval(testString);
120+
resolve(test);
121+
} catch (err) {
122+
const isUsingTopLevelAwait =
123+
err instanceof SyntaxError &&
124+
err.message.includes(
125+
"await is only valid in async functions and the top level bodies of modules",
126+
);
127+
128+
if (isUsingTopLevelAwait) {
129+
const iifeTest = createAsyncIife(testString);
130+
131+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
132+
eval(iifeTest).then(resolve).catch(reject);
133+
} else {
134+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
135+
reject(err);
136+
}
137+
}
138+
},
139+
);
140+
141+
// If the test string does not evaluate to an object, then we assume that
142+
// it's a standard JS test and any assertions have already passed.
143+
if (typeof evaluatedTestString !== "object") {
144+
// Execute afterEach hook if it exists
145+
if (opts.hooks?.afterEach) eval(opts.hooks.afterEach);
112146

113-
// Evaluates the learner's code so that any variables they
114-
// define are available to the test.
147+
return { pass: true, ...this.#proxyConsole.flush() };
148+
}
115149

116-
try {
117-
runPython(opts.source ?? "");
118-
} catch {
119-
// Tests should not automatically fail if there's an error in the
120-
// source. Various tests are only interested in using regexes on the
121-
// code.
150+
if (!evaluatedTestString || !("test" in evaluatedTestString)) {
151+
throw Error(
152+
"Test string did not evaluate to an object with the 'test' property",
153+
);
122154
}
123155

124-
await eval(iifeTest);
156+
const { test } = evaluatedTestString as EvaluatedTeststring;
157+
158+
// Evaluates the learner's code so that any variables they define are
159+
// available to the test.
160+
runPython(opts.source ?? "");
161+
162+
await test();
125163

126164
return { pass: true, ...this.#proxyConsole.flush() };
127165
} catch (err) {

packages/tests/integration-tests/index.test.ts

Lines changed: 57 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,7 +1205,11 @@ assert(mocked.find('.greeting').length === 1);
12051205
type: "python",
12061206
source,
12071207
});
1208-
return runner?.runTest(`assert.equal(runPython('get_five()'), 5)`);
1208+
return runner?.runTest(
1209+
`({
1210+
test: () => assert.equal(runPython('get_five()'), 5),
1211+
})`,
1212+
);
12091213
}, source);
12101214

12111215
expect(result).toEqual({ pass: true });
@@ -1224,7 +1228,11 @@ assert(mocked.find('.greeting').length === 1);
12241228
const result = await page.evaluate(async () => {
12251229
const runner = window.FCCTestRunner.getRunner("python");
12261230
await runner?.init({}, 1000);
1227-
return runner?.runTest(`assert.equal(runPython('get_five()'), 5)`);
1231+
return runner?.runTest(
1232+
`({
1233+
test: () => assert.equal(runPython('get_five()'), 5),
1234+
})`,
1235+
);
12281236
});
12291237

12301238
expect(result).toMatchObject({
@@ -1243,7 +1251,9 @@ assert(mocked.find('.greeting').length === 1);
12431251
type: "python",
12441252
});
12451253
return runner?.runTest(
1246-
`assert.equal(runPython('__name__'), '__main__')`,
1254+
`({
1255+
test: () => assert.equal(runPython('__name__'), '__main__'),
1256+
})`,
12471257
);
12481258
});
12491259

@@ -1274,6 +1284,26 @@ assert(mocked.find('.greeting').length === 1);
12741284
});
12751285
});
12761286

1287+
it("should reject testStrings that evaluate to an invalid object", async () => {
1288+
const result = await page.evaluate(async () => {
1289+
const runner = await window.FCCTestRunner.createTestRunner({
1290+
type: "python",
1291+
});
1292+
return runner?.runTest(`({ invalid: 'test' })`);
1293+
});
1294+
1295+
expect(result).toEqual({
1296+
err: {
1297+
message:
1298+
"Test string did not evaluate to an object with the 'test' property",
1299+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
1300+
stack: expect.stringContaining(
1301+
"Error: Test string did not evaluate to an object with the 'test' property",
1302+
),
1303+
},
1304+
});
1305+
});
1306+
12771307
it("should make user code available to the python code as the _code variable", async () => {
12781308
const result = await page.evaluate(async () => {
12791309
const runner = await window.FCCTestRunner.createTestRunner({
@@ -1283,9 +1313,9 @@ assert(mocked.find('.greeting').length === 1);
12831313
},
12841314
});
12851315

1286-
return runner?.runTest(
1287-
`assert.equal(runPython('_code'), "test = 'value'")`,
1288-
);
1316+
return runner?.runTest(`({
1317+
test: () => assert.equal(runPython('_code'), "test = 'value'")
1318+
})`);
12891319
});
12901320

12911321
expect(result).toEqual({ pass: true });
@@ -1296,9 +1326,9 @@ assert(mocked.find('.greeting').length === 1);
12961326
const runner = await window.FCCTestRunner.createTestRunner({
12971327
type: "python",
12981328
});
1299-
return runner?.runTest(
1300-
`assert.equal(runPython('_Node("x = 1").get_variable("x")'), 1)`,
1301-
);
1329+
return runner?.runTest(`({
1330+
test: () => assert.equal(runPython('_Node("x = 1").get_variable("x")'), 1)
1331+
})`);
13021332
});
13031333

13041334
expect(result).toEqual({ pass: true });
@@ -1316,7 +1346,9 @@ assert(mocked.find('.greeting').length === 1);
13161346
},
13171347
});
13181348

1319-
return runner?.runTest("");
1349+
return runner?.runTest(`({
1350+
test: () => {}
1351+
})`);
13201352
});
13211353

13221354
expect(result).toEqual({ pass: true });
@@ -1342,7 +1374,9 @@ assert(mocked.find('.greeting').length === 1);
13421374
const runner = await window.FCCTestRunner.createTestRunner({
13431375
type: "python",
13441376
});
1345-
return runner?.runTest(`assert.equal(runPython('1 + "1"'), 2)`);
1377+
return runner?.runTest(`({
1378+
test: () => assert.equal(runPython('1 + "1"'), 2)
1379+
})`);
13461380
});
13471381
expect(result).toEqual({
13481382
err: {
@@ -1368,7 +1402,9 @@ pattern = re.compile('l+')
13681402
type: "python",
13691403
source,
13701404
});
1371-
return runner?.runTest(`assert.equal(runPython('str(pattern)'), "l+")`);
1405+
return runner?.runTest(`({
1406+
test: () => assert.equal(runPython('str(pattern)'), "l+")
1407+
})`);
13721408
}, source);
13731409
expect(result).toEqual({
13741410
err: {
@@ -1395,9 +1431,9 @@ pattern = re.compile('l+')`;
13951431
});
13961432
// Since the comparison includes a PyProxy object, that will be
13971433
// posted back to the caller and cause a DataCloneError
1398-
return runner?.runTest(
1399-
`assert.equal(__userGlobals.get("pattern"), "l+")`,
1400-
);
1434+
return runner?.runTest(`
1435+
({ test: () => assert.equal(__userGlobals.get("pattern"), "l+") })
1436+
`);
14011437
}, source);
14021438

14031439
expect(result).toEqual({
@@ -1415,39 +1451,14 @@ pattern = re.compile('l+')`;
14151451
});
14161452
});
14171453

1418-
it("should support runPython in simple tests", async () => {
1419-
const result = await page.evaluate(async () => {
1420-
const runner = await window.FCCTestRunner.createTestRunner({
1421-
type: "python",
1422-
});
1423-
return runner?.runTest(`assert.equal(runPython('1 + 1'), 2)`);
1424-
});
1425-
expect(result).toEqual({ pass: true });
1426-
});
1427-
1428-
it("should evaluate user code before running tests", async () => {
1429-
const result = await page.evaluate(async () => {
1430-
const runner = await window.FCCTestRunner.createTestRunner({
1431-
type: "python",
1432-
source: "x = 3",
1433-
});
1434-
return runner?.runTest(`assert.equal(runPython('x + 1'), 5)`);
1435-
});
1436-
1437-
expect(result).toMatchObject({
1438-
err: {
1439-
actual: 4,
1440-
expected: 5,
1441-
},
1442-
});
1443-
});
1444-
1445-
it("should not throw when input is called during a test", async () => {
1454+
it("should not throw io exceptions when input is called in a test", async () => {
14461455
const result = await page.evaluate(async () => {
14471456
const runner = await window.FCCTestRunner.createTestRunner({
14481457
type: "python",
14491458
});
1450-
return runner?.runTest(`assert.equal(runPython('input("test")'), "")`);
1459+
return runner?.runTest(
1460+
`({ test: () => assert.equal(runPython('input("test")'), "") })`,
1461+
);
14511462
});
14521463

14531464
expect(result).toEqual({ pass: true });
@@ -1463,7 +1474,7 @@ pattern = re.compile('l+')`;
14631474
},
14641475
});
14651476
return runner?.runTest(
1466-
`assert.equal(runPython('input()'), "test input")`,
1477+
`({ test: () => assert.equal(runPython('input()'), "test input") })`,
14671478
);
14681479
}, beforeEach);
14691480

@@ -1483,7 +1494,7 @@ pattern = re.compile('l+')`;
14831494
},
14841495
});
14851496
return runner?.runTest(
1486-
`assert.equal(runPython('name'), "mocked input")`,
1497+
`({ test: () => assert.equal(runPython('name'), "mocked input") })`,
14871498
);
14881499
},
14891500
source,
@@ -1492,50 +1503,5 @@ pattern = re.compile('l+')`;
14921503

14931504
expect(result).toEqual({ pass: true });
14941505
});
1495-
1496-
it("should not automatically fail tests if the source raises an error", async () => {
1497-
const source = `
1498-
def func():
1499-
pass
1500-
`;
1501-
const result = await page.evaluate(async (source) => {
1502-
const runner = await window.FCCTestRunner.createTestRunner({
1503-
type: "python",
1504-
source,
1505-
});
1506-
return runner?.runTest(`assert.equal(1, 2)`);
1507-
}, source);
1508-
1509-
expect(result).toMatchObject({
1510-
err: {
1511-
expected: 2,
1512-
actual: 1,
1513-
},
1514-
});
1515-
});
1516-
1517-
it("should be possible to re-run user code inside a test to detect errors", async () => {
1518-
const source = `
1519-
def func():
1520-
pass
1521-
`;
1522-
1523-
const result = await page.evaluate(async (source) => {
1524-
const runner = await window.FCCTestRunner.createTestRunner({
1525-
type: "python",
1526-
source,
1527-
code: {
1528-
contents: source,
1529-
},
1530-
});
1531-
return runner?.runTest(`runPython(code)`);
1532-
}, source);
1533-
1534-
expect(result).toMatchObject({
1535-
err: {
1536-
type: "IndentationError",
1537-
},
1538-
});
1539-
});
15401506
});
15411507
});

packages/tests/integration-tests/timeout.test.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ def run():
8686
source,
8787
});
8888
const result = await runner?.runTest(
89-
`assert.equal(runPython('loop()'), 1)`,
89+
`({
90+
test: () => assert.equal(runPython('loop()'), 1)
91+
})`,
9092
10,
9193
);
9294
return result;
@@ -100,7 +102,11 @@ def run():
100102
},
101103
source,
102104
});
103-
return runner?.runTest(`assert.equal(runPython('run()'), 1)`);
105+
return runner?.runTest(
106+
`({
107+
test: () => assert.equal(runPython('run()'), 1)
108+
})`,
109+
);
104110
}, source);
105111

106112
expect(timeoutResult).toEqual({
@@ -126,7 +132,12 @@ while True:
126132
},
127133
source,
128134
});
129-
return runner?.runTest(`assert.equal(runPython('1'), 1)`, 10);
135+
return runner?.runTest(
136+
`({
137+
test: () => assert.equal(runPython('1'), 1)
138+
})`,
139+
10,
140+
);
130141
}, source);
131142

132143
expect(result).toEqual({

0 commit comments

Comments
 (0)