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