-
Notifications
You must be signed in to change notification settings - Fork 526
Expand file tree
/
Copy pathpyodideRunner.ts
More file actions
195 lines (183 loc) · 7.24 KB
/
Copy pathpyodideRunner.ts
File metadata and controls
195 lines (183 loc) · 7.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import CodebridgeRegistry from '@codebridge/CodebridgeRegistry';
import ConsoleManager from '@codebridge/Console/ConsoleManager';
import {
getSystemMessage,
getTimestampMessage,
} from '@codebridge/Console/MessageHelpers';
import {MiniApps} from '@codebridge/constants';
import {AnyAction, Dispatch} from 'redux';
import {MAIN_PYTHON_FILE} from '@cdo/apps/lab2/constants';
import Lab2Registry from '@cdo/apps/lab2/Lab2Registry';
import ProgressManager from '@cdo/apps/lab2/progress/ProgressManager';
import {getFileByName} from '@cdo/apps/lab2/projects/utils';
import {MultiFileSource, ProjectFile} from '@cdo/apps/lab2/types';
import {SVG_ID} from '@cdo/apps/maze/constants';
import pythonlabI18n from '@cdo/apps/pythonlab/locale';
import {getStore} from '@cdo/apps/redux';
import {captureThumbnailFromSvgPythonlabNeighborhood} from '@cdo/apps/util/thumbnail';
import {getValidationFromSource, RunType} from '../codebridge';
import PythonValidationTracker from './progress/PythonValidationTracker';
import {
asyncRun,
restartPyodideIfProgramIsRunning,
} from './pyodideWorkerManager';
import {runStudentTests, runValidationTests} from './pythonHelpers/scripts';
const appName = 'pythonlab';
export async function handleRunClick(
runTests: boolean,
dispatch: Dispatch<AnyAction>,
source: MultiFileSource | undefined,
progressManager: ProgressManager | null,
validationFile?: ProjectFile
) {
const consoleManager = CodebridgeRegistry.getInstance().getConsoleManager();
if (!source) {
const runType = runTests
? validationFile
? RunType.VALIDATION
: RunType.TEST
: RunType.RUN;
consoleManager?.writeConsoleMessage(getTimestampMessage(runType));
handleRunEndedUnexpectedly(consoleManager, pythonlabI18n.noCode());
return;
}
if (runTests) {
await runAllTests(source, dispatch, progressManager, validationFile);
} else {
// Run main.py
consoleManager?.writeConsoleMessage(getTimestampMessage(RunType.RUN));
const code = getFileByName(source.files, MAIN_PYTHON_FILE)?.contents;
if (code === undefined) {
handleRunEndedUnexpectedly(
consoleManager,
pythonlabI18n.noFileToRun({
fileName: MAIN_PYTHON_FILE,
})
);
return;
}
await runPythonCode(code, source);
if (isNeighborhoodLevel()) {
setProjectThumbnail();
}
}
}
export async function runPythonCode(
mainFile: string,
source: MultiFileSource,
validationFile?: ProjectFile
) {
try {
const isNeighborhoodRun = isNeighborhoodLevel();
if (isNeighborhoodRun) {
CodebridgeRegistry.getInstance().getNeighborhood()?.reset();
CodebridgeRegistry.getInstance().getNeighborhood()?.onRun();
}
// We only send all output to the neighborhood if this is a neighborhood level and
// we are not running validation, as validation does not render to the neighborhood.
const outputToNeighborhood = isNeighborhoodRun && !validationFile;
return await asyncRun(
mainFile,
source,
validationFile,
outputToNeighborhood
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
console.log(
`Error in pyodideWorker at ${e.filename}, Line: ${e.lineno}, ${e.message}`
);
}
}
export function stopPythonCode() {
if (isNeighborhoodLevel()) {
CodebridgeRegistry.getInstance().getNeighborhood()?.onStop();
}
// This will terminate the worker and create a new one if there is a running program.
restartPyodideIfProgramIsRunning();
}
export async function runAllTests(
source: MultiFileSource,
dispatch: Dispatch<AnyAction>,
progressManager: ProgressManager | null,
validationFile?: ProjectFile
) {
// We default to using the validation file passed in. If it does not exist,
// we check the source for the validation file (this is the case in start mode).
const validationToRun = validationFile || getValidationFromSource(source);
const consoleManager = CodebridgeRegistry.getInstance().getConsoleManager();
if (validationToRun) {
consoleManager?.writeConsoleMessage(
getTimestampMessage(RunType.VALIDATION)
);
// We do a bit of a hack around the progress manager system here.
// We reset validation to set all tests to pending, then updateProgress to propagate
// the pending status into the progress state (only on updateProgress does the progress manager
// check the validation state). After running we will update progress again to get the test results.
// This makes it so we can show the test names with a pending status while the tests are running after the
// first run for a level, which is a cleaner UI than wiping the tests completely.
progressManager?.resetValidation();
progressManager?.updateProgress();
// We only send the separate validation file, because otherwise the
// source already has the validation file.
const result = await runPythonCode(
runValidationTests(validationToRun.name),
source,
validationFile
);
if (result?.message) {
// Get validation test results
// After parsing, message is an array of objects {name: string, result: string}
// where "name" is the name of the test and "result" is one of
// "PASS/FAIL/ERROR/SKIP/EXPECTED_FAILURE/UNEXPECTED_SUCCESS"
// See this PR for details: https://github.com/code-dot-org/pythonlab-packages/pull/5
const testResults = JSON.parse(result.message);
if (progressManager) {
PythonValidationTracker.getInstance().setValidationResults(testResults);
progressManager.updateProgress();
}
}
} else {
consoleManager?.writeConsoleMessage(
getSystemMessage(getTimestampMessage(RunType.TEST))
);
// Otherwise, we look for files that follow the regex 'test*.py' and run those.
await runPythonCode(runStudentTests(), source);
}
}
function isNeighborhoodLevel() {
return (
getStore().getState().lab2Project.projectSources?.labConfig?.miniApp
?.name === MiniApps.Neighborhood
);
}
function handleRunEndedUnexpectedly(
consoleManager: ConsoleManager | null,
message: string
) {
consoleManager?.writeConsoleMessage(getSystemMessage(message, appName));
if (isNeighborhoodLevel()) {
// We reset, run, and close the neighborhood to ensure that the neighborhood
// properly resets the run button back to run (from stop), and to reset the
// neighborhood to its original state.
CodebridgeRegistry.getInstance().getNeighborhood()?.reset();
CodebridgeRegistry.getInstance().getNeighborhood()?.onRun();
CodebridgeRegistry.getInstance().getNeighborhood()?.onClose();
} else {
consoleManager?.writeConsoleMessage('');
}
}
async function setProjectThumbnail() {
const neighborhood = CodebridgeRegistry.getInstance().getNeighborhood();
neighborhood?.onClose();
const projectManager = Lab2Registry.getInstance().getProjectManager();
const shouldCapture = projectManager?.getShouldCaptureThumbnail();
if (!shouldCapture) return;
await neighborhood?.waitUntilDone(); // Wait for neighborhood signal processing to be completed.
const svg = document.getElementById(SVG_ID);
const svgArg = svg instanceof SVGSVGElement ? svg : null;
if (svgArg) {
const pngBlob = await captureThumbnailFromSvgPythonlabNeighborhood(svgArg);
projectManager?.setThumbnail(pngBlob);
}
}