CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/backend/execute-code.test.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2024 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
process.env.COCALC_PROJECT_MONITOR_INTERVAL_S = "1";
7
// default is much lower, might fail if you have more procs than the default
8
process.env.COCALC_PROJECT_INFO_PROC_LIMIT = "10000";
9
10
import { executeCode } from "./execute-code";
11
12
describe("hello world", () => {
13
it("runs hello world", async () => {
14
const { stdout } = await executeCode({
15
command: "echo",
16
args: ["hello world"],
17
});
18
expect(stdout).toBe("hello world\n");
19
});
20
});
21
22
describe("tests involving bash mode", () => {
23
it("runs normal code in bash", async () => {
24
const { stdout } = await executeCode({ command: "echo 'abc' | wc -c" });
25
// on GitHub actions the output of wc is different than on other machines,
26
// so we normalize by trimming.
27
expect(stdout.trim()).toBe("4");
28
});
29
30
it("reports missing executable in non-bash mode", async () => {
31
try {
32
await executeCode({
33
command: "this_does_not_exist",
34
args: ["nothing"],
35
bash: false,
36
});
37
} catch (err) {
38
expect(err).toContain("ENOENT");
39
}
40
});
41
42
it("reports missing executable in non-bash mode when ignoring on exit", async () => {
43
try {
44
await executeCode({
45
command: "this_does_not_exist",
46
args: ["nothing"],
47
err_on_exit: false,
48
bash: false,
49
});
50
} catch (err) {
51
expect(err).toContain("ENOENT");
52
}
53
});
54
55
it("ignores errors otherwise if err_on_exit is false", async () => {
56
const { stdout, stderr, exit_code } = await executeCode({
57
command: "sh",
58
args: ["-c", "echo foo; exit 42"],
59
err_on_exit: false,
60
bash: false,
61
});
62
expect(stdout).toBe("foo\n");
63
expect(stderr).toBe("");
64
expect(exit_code).toBe(42);
65
});
66
});
67
68
describe("test timeout", () => {
69
it("kills if timeout reached", async () => {
70
const t = Date.now();
71
try {
72
await executeCode({ command: "sleep 60", timeout: 0.1 });
73
expect(false).toBe(true);
74
} catch (err) {
75
expect(err).toContain("killed command");
76
expect(Date.now() - t).toBeGreaterThan(90);
77
expect(Date.now() - t).toBeLessThan(500);
78
}
79
});
80
81
it("doesn't kill when timeout not reached", async () => {
82
const t = Date.now();
83
await executeCode({ command: "sleep 0.1", timeout: 0.5 });
84
expect(Date.now() - t).toBeGreaterThan(90);
85
});
86
87
it("kills in non-bash mode if timeout reached", async () => {
88
try {
89
await executeCode({
90
command: "sh",
91
args: ["-c", "sleep 5"],
92
bash: false,
93
timeout: 0.1,
94
});
95
expect(false).toBe(true);
96
} catch (err) {
97
expect(err).toContain("killed command");
98
}
99
});
100
});
101
102
describe("test longer execution", () => {
103
it(
104
"runs 5 seconds",
105
async () => {
106
const t0 = Date.now();
107
const { stdout, stderr, exit_code } = await executeCode({
108
command: "sh",
109
args: ["-c", "echo foo; sleep 5; echo bar"],
110
err_on_exit: false,
111
bash: false,
112
});
113
expect(stdout).toBe("foo\nbar\n");
114
expect(stderr).toBe("");
115
expect(exit_code).toBe(0);
116
const t1 = Date.now();
117
expect((t1 - t0) / 1000).toBeGreaterThan(4.9);
118
},
119
10 * 1000,
120
);
121
});
122
123
describe("test env", () => {
124
it("allows to specify environment variables", async () => {
125
const { stdout, stderr, type } = await executeCode({
126
command: "sh",
127
args: ["-c", "echo $FOO;"],
128
err_on_exit: false,
129
bash: false,
130
env: { FOO: "bar" },
131
});
132
expect(type).toBe("blocking");
133
expect(stdout).toBe("bar\n");
134
expect(stderr).toBe("");
135
});
136
});
137
138
describe("async", () => {
139
it("use ID to get async result", async () => {
140
const c = await executeCode({
141
command: "sh",
142
args: ["-c", "echo foo; sleep .5; echo bar; sleep .5; echo baz;"],
143
bash: false,
144
timeout: 10,
145
async_call: true,
146
});
147
expect(c.type).toEqual("async");
148
if (c.type !== "async") return;
149
const { status, start, job_id } = c;
150
expect(status).toEqual("running");
151
expect(start).toBeGreaterThan(1);
152
expect(typeof job_id).toEqual("string");
153
if (typeof job_id !== "string") return;
154
await new Promise((done) => setTimeout(done, 250));
155
{
156
const s = await executeCode({ async_get: job_id });
157
expect(s.type).toEqual("async");
158
if (s.type !== "async") return;
159
expect(s.status).toEqual("running");
160
// partial stdout result
161
expect(s.stdout).toEqual("foo\n");
162
expect(s.elapsed_s).toBeUndefined();
163
expect(s.start).toBeGreaterThan(1);
164
expect(s.exit_code).toEqual(0);
165
}
166
167
await new Promise((done) => setTimeout(done, 900));
168
{
169
const s = await executeCode({ async_get: job_id });
170
expect(s.type).toEqual("async");
171
if (s.type !== "async") return;
172
expect(s.status).toEqual("completed");
173
expect(s.stdout).toEqual("foo\nbar\nbaz\n");
174
expect(s.elapsed_s).toBeGreaterThan(0.1);
175
expect(s.elapsed_s).toBeLessThan(3);
176
expect(s.start).toBeGreaterThan(Date.now() - 10 * 1000);
177
expect(s.stderr).toEqual("");
178
expect(s.exit_code).toEqual(0);
179
}
180
});
181
182
it("error/err_on_exit=true", async () => {
183
const c = await executeCode({
184
command: ">&2 echo baz; exit 3",
185
bash: true,
186
async_call: true,
187
err_on_exit: true, // default
188
});
189
expect(c.type).toEqual("async");
190
if (c.type !== "async") return;
191
const { job_id } = c;
192
expect(typeof job_id).toEqual("string");
193
if (typeof job_id !== "string") return;
194
await new Promise((done) => setTimeout(done, 250));
195
const s = await executeCode({ async_get: job_id });
196
expect(s.type).toEqual("async");
197
if (s.type !== "async") return;
198
expect(s.status).toEqual("error");
199
expect(s.stdout).toEqual("");
200
expect(s.stderr).toEqual("baz\n");
201
// any error is code 1 it seems?
202
expect(s.exit_code).toEqual(1);
203
});
204
205
// without err_on_exit, the call is "completed" and we get the correct exit code
206
it("error/err_on_exit=false", async () => {
207
const c = await executeCode({
208
command: ">&2 echo baz; exit 3",
209
bash: true,
210
async_call: true,
211
err_on_exit: false,
212
});
213
expect(c.type).toEqual("async");
214
if (c.type !== "async") return;
215
const { job_id } = c;
216
expect(typeof job_id).toEqual("string");
217
if (typeof job_id !== "string") return;
218
await new Promise((done) => setTimeout(done, 250));
219
const s = await executeCode({ async_get: job_id });
220
expect(s.type).toEqual("async");
221
if (s.type !== "async") return;
222
expect(s.status).toEqual("completed");
223
expect(s.stdout).toEqual("");
224
expect(s.stderr).toEqual("baz\n");
225
expect(s.exit_code).toEqual(3);
226
});
227
228
it("trigger a timeout", async () => {
229
const c = await executeCode({
230
command: "sh",
231
args: ["-c", "echo foo; sleep 1; echo bar;"],
232
bash: false,
233
timeout: 0.1,
234
async_call: true,
235
});
236
expect(c.type).toEqual("async");
237
if (c.type !== "async") return;
238
const { status, start, job_id } = c;
239
expect(status).toEqual("running");
240
expect(start).toBeGreaterThan(1);
241
expect(typeof job_id).toEqual("string");
242
if (typeof job_id !== "string") return;
243
await new Promise((done) => setTimeout(done, 250));
244
// now we check up on the job
245
const s = await executeCode({ async_get: job_id });
246
expect(s.type).toEqual("async");
247
if (s.type !== "async") return;
248
expect(s.status).toEqual("error");
249
expect(s.stdout).toEqual("");
250
expect(s.elapsed_s).toBeGreaterThan(0.01);
251
expect(s.elapsed_s).toBeLessThan(3);
252
expect(s.start).toBeGreaterThan(1);
253
expect(s.stderr).toEqual(
254
"killed command 'sh -c echo foo; sleep 1; echo bar;'",
255
);
256
expect(s.exit_code).toEqual(1);
257
});
258
259
it(
260
"long running async job",
261
async () => {
262
const c = await executeCode({
263
command: "sh",
264
args: ["-c", `echo foo; python3 -c '${CPU_PY}'; echo bar;`],
265
bash: false,
266
err_on_exit: false,
267
async_call: true,
268
});
269
expect(c.type).toEqual("async");
270
if (c.type !== "async") return;
271
const { status, job_id } = c;
272
expect(status).toEqual("running");
273
expect(typeof job_id).toEqual("string");
274
if (typeof job_id !== "string") return;
275
await new Promise((done) => setTimeout(done, 5500));
276
// now we check up on the job
277
const s = await executeCode({ async_get: job_id, async_stats: true });
278
expect(s.type).toEqual("async");
279
if (s.type !== "async") return;
280
expect(s.elapsed_s).toBeGreaterThan(5);
281
expect(s.exit_code).toBe(0);
282
expect(s.pid).toBeGreaterThan(1);
283
expect(s.stats).toBeDefined();
284
if (!Array.isArray(s.stats)) return;
285
const pcts = Math.max(...s.stats.map((s) => s.cpu_pct));
286
const secs = Math.max(...s.stats.map((s) => s.cpu_secs));
287
const mems = Math.max(...s.stats.map((s) => s.mem_rss));
288
expect(pcts).toBeGreaterThan(10);
289
expect(secs).toBeGreaterThan(1);
290
expect(mems).toBeGreaterThan(1);
291
expect(s.stdout).toEqual("foo\nbar\n");
292
// now without stats, after retrieving it
293
const s2 = await executeCode({ async_get: job_id });
294
if (s2.type !== "async") return;
295
expect(s2.stats).toBeUndefined();
296
// and check, that this is not removing stats entirely
297
const s3 = await executeCode({ async_get: job_id, async_stats: true });
298
if (s3.type !== "async") return;
299
expect(Array.isArray(s3.stats)).toBeTruthy();
300
},
301
10 * 1000,
302
);
303
});
304
305
// the await case is essentially like the async case above, but it will block for a bit
306
describe("await", () => {
307
const check = (s) => {
308
expect(s.type).toEqual("async");
309
if (s.type !== "async") return;
310
expect(s.status).toEqual("completed");
311
expect(s.elapsed_s).toBeGreaterThan(1);
312
expect(s.elapsed_s).toBeLessThan(3);
313
expect(s.exit_code).toBe(0);
314
expect(s.pid).toBeGreaterThan(1);
315
expect(s.stdout).toEqual("foo\n");
316
expect(s.stderr).toEqual("");
317
};
318
319
it("returns when a job finishes", async () => {
320
const c = await executeCode({
321
command: "sleep 2; echo 'foo'",
322
bash: true,
323
err_on_exit: false,
324
async_call: true,
325
});
326
expect(c.type).toEqual("async");
327
if (c.type !== "async") return;
328
const { status, job_id, pid } = c;
329
expect(status).toEqual("running");
330
expect(pid).toBeGreaterThan(1);
331
const t0 = Date.now();
332
const s = await executeCode({
333
async_await: true,
334
async_get: job_id,
335
async_stats: true,
336
});
337
const t1 = Date.now();
338
// This is the main test: it really waited for at least a second until the job completed
339
expect((t1 - t0) / 1000).toBeGreaterThan(1);
340
check(s);
341
if (s.type !== "async") return;
342
expect(Array.isArray(s.stats)).toBeTruthy();
343
});
344
345
it("returns immediately if already done", async () => {
346
const c = await executeCode({
347
command: "sleep 1.1; echo 'foo'",
348
bash: true,
349
err_on_exit: false,
350
async_call: true,
351
});
352
expect(c.type).toEqual("async");
353
if (c.type !== "async") return;
354
const { status, job_id, pid } = c;
355
expect(status).toEqual("running");
356
expect(pid).toBeGreaterThan(1);
357
await new Promise((done) => setTimeout(done, 2000));
358
const s = await executeCode({
359
async_await: true,
360
async_get: job_id,
361
async_stats: true,
362
});
363
check(s);
364
if (s.type !== "async") return;
365
expect(s.elapsed_s).toBeLessThan(1.5);
366
});
367
368
it("deal with unknown executables", async () => {
369
const c = await executeCode({
370
command: "random123unknown99",
371
err_on_exit: false,
372
async_call: true,
373
});
374
expect(c.type).toEqual("async");
375
if (c.type !== "async") return;
376
const { job_id, pid } = c;
377
expect(pid).toBeUndefined();
378
const s = await executeCode({
379
async_await: true,
380
async_get: job_id,
381
async_stats: true,
382
});
383
expect(s.type).toEqual("async");
384
if (s.type !== "async") return;
385
expect(s.exit_code).toBe(1);
386
expect(s.stderr).toContain("ENOENT");
387
expect(s.status).toBe("error");
388
});
389
390
it("returns an error", async () => {
391
const c = await executeCode({
392
command: "sleep .1; >&2 echo baz; exit 3",
393
bash: true,
394
err_on_exit: false,
395
async_call: true,
396
});
397
expect(c.type).toEqual("async");
398
if (c.type !== "async") return;
399
const { status, job_id, pid } = c;
400
expect(status).toEqual("running");
401
expect(pid).toBeGreaterThan(1);
402
const t0 = Date.now();
403
const s = await executeCode({
404
async_await: true,
405
async_get: job_id,
406
async_stats: true,
407
});
408
expect((Date.now() - t0) / 1000).toBeGreaterThan(0.05);
409
expect(s.type).toEqual("async");
410
if (s.type !== "async") return;
411
expect(s.stderr).toEqual("baz\n");
412
expect(s.exit_code).toEqual(3);
413
expect(s.status).toEqual("completed");
414
});
415
416
it("react to a killed process", async () => {
417
const c = await executeCode({
418
command: "sh",
419
args: ["-c", `echo foo; sleep 1; echo bar;`],
420
bash: false,
421
err_on_exit: false,
422
async_call: true,
423
});
424
expect(c.type).toEqual("async");
425
if (c.type !== "async") return;
426
const { job_id, pid } = c;
427
await new Promise((done) => setTimeout(done, 100));
428
await executeCode({
429
command: `kill -9 -${pid}`,
430
bash: true,
431
});
432
const s = await executeCode({
433
async_await: true,
434
async_get: job_id,
435
async_stats: true,
436
});
437
expect(s.type).toEqual("async");
438
if (s.type !== "async") return;
439
expect(s.stderr).toEqual("");
440
expect(s.stdout).toEqual("foo\n");
441
expect(s.exit_code).toEqual(0);
442
expect(s.status).toEqual("completed");
443
});
444
});
445
446
// we burn a bit of CPU to get the cpu_pct and cpu_secs up
447
const CPU_PY = `
448
from time import time
449
t0=time()
450
while t0+5>time():
451
sum([_ for _ in range(10**6)])
452
`;
453
454