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