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