diff --git a/src/plugins/terminal/src/android/TerminalService.java b/src/plugins/terminal/src/android/TerminalService.java index 1d8281ee8..e98b466b0 100644 --- a/src/plugins/terminal/src/android/TerminalService.java +++ b/src/plugins/terminal/src/android/TerminalService.java @@ -263,11 +263,24 @@ private void stopProcess(String pid) { } } - private void isProcessRunning(String pid, Messenger clientMessenger) { - Process process = processes.get(pid); - String status = process != null && ProcessUtils.isAlive(process) ? "running" : "not_found"; - sendMessageToClient(pid, "isRunning", status); + private void isProcessRunning(String pid, Messenger clientMessenger) { + boolean running = + processes.containsKey(pid) && + ProcessUtils.isAlive(processes.get(pid)); + + try { + Message reply = Message.obtain(); + Bundle bundle = new Bundle(); + bundle.putString("id", pid); + bundle.putString("action", "isRunning"); + bundle.putString("data", running ? "running" : "stopped"); + reply.setData(bundle); + clientMessenger.send(reply); + } catch (RemoteException e) { + // nothing else to do } +} + private void cleanup(String id) { processes.remove(id); diff --git a/src/test/exec.tests.js b/src/test/exec.tests.js new file mode 100644 index 000000000..90e8c8523 --- /dev/null +++ b/src/test/exec.tests.js @@ -0,0 +1,130 @@ +import { TestRunner } from "./tester"; + +export async function runExecutorTests(writeOutput) { + const runner = new TestRunner("Executor API Tests"); + + runner.test("Executor available", async (test) => { + test.assert( + typeof Executor !== "undefined", + "Executor should be available globally", + ); + }); + + runner.test("Background Executor available", async (test) => { + test.assert( + typeof Executor.BackgroundExecutor !== "undefined", + "Background Executor should be available globally", + ); + }); + + runner.test("execute()", async (test) => { + test.assert( + (await Executor.execute("echo test123")).includes("test123"), + "Command output should match", + ); + }); + + runner.test("execute() (BackgroundExecutor)", async (test) => { + test.assert( + (await Executor.BackgroundExecutor.execute("echo test123")).includes( + "test123", + ), + "Command output should match", + ); + }); + + runner.test("start()", async (test) => { + let stdout = ""; + + const uuid = await Executor.start("sh", (type, data) => { + if (type === "stdout") stdout += data; + }); + + await Executor.write(uuid, "echo hello\n"); + await new Promise((r) => setTimeout(r, 200)); + await Executor.stop(uuid); + + await new Promise((r) => setTimeout(r, 200)); + + test.assert(stdout.includes("hello"), "Shell should echo output"); + }); + + runner.test("start() (BackgroundExecutor)", async (test) => { + let stdout = ""; + + const uuid = await Executor.BackgroundExecutor.start("sh", (type, data) => { + if (type === "stdout") stdout += data; + }); + + await Executor.BackgroundExecutor.write(uuid, "echo hello\n"); + await new Promise((r) => setTimeout(r, 200)); + await Executor.BackgroundExecutor.stop(uuid); + + await new Promise((r) => setTimeout(r, 200)); + + test.assert(stdout.includes("hello"), "Shell should echo output"); + }); + + runner.test("start/stop() (BackgroundExecutor)", async (test) => { + let stdout = ""; + + const uuid = await Executor.BackgroundExecutor.start( + "sh", + (type, data) => {}, + ); + + await new Promise((r) => setTimeout(r, 200)); + + const isRunning = await Executor.BackgroundExecutor.isRunning(uuid); + + test.assert(isRunning === true, "Executor must be running"); + + await new Promise((r) => setTimeout(r, 200)); + + await Executor.BackgroundExecutor.stop(uuid); + + await new Promise((r) => setTimeout(r, 200)); + + test.assert( + isRunning !== (await Executor.BackgroundExecutor.isRunning(uuid)), + "Executor must be stopped", + ); + test.assert( + (await Executor.BackgroundExecutor.isRunning(uuid)) === false, + "Executor must be stopped", + ); + }); + + runner.test("start/stop()", async (test) => { + let stdout = ""; + + const uuid = await Executor.start("sh", (type, data) => {}); + + await new Promise((r) => setTimeout(r, 200)); + + const isRunning = await Executor.isRunning(uuid); + + test.assert(isRunning === true, "Executor must be running"); + + await new Promise((r) => setTimeout(r, 200)); + + await Executor.stop(uuid); + + await new Promise((r) => setTimeout(r, 200)); + + test.assert( + (await Executor.isRunning(uuid)) === false, + "Executor must be stopped", + ); + }); + + runner.test("FDROID env variable", async (test) => { + const result = await Executor.execute("echo $FDROID"); + + const isSet = result.trim().length > 0; + + test.assert(isSet, "FDROID env variable should be set"); + }); + + return await runner.run(writeOutput); +} diff --git a/src/test/tester.js b/src/test/tester.js index a51206523..62dfa163b 100644 --- a/src/test/tester.js +++ b/src/test/tester.js @@ -1,4 +1,5 @@ import { runAceEditorTests } from "./editor.tests"; +import { runExecutorTests } from "./exec.tests"; import { runSanityTests } from "./sanity.tests"; export async function runAllTests() { @@ -16,6 +17,7 @@ export async function runAllTests() { // Run unit tests await runSanityTests(write); await runAceEditorTests(write); + await runExecutorTests(write); write("\x1b[36m\x1b[1mTests completed!\x1b[0m\n"); } catch (error) { @@ -80,6 +82,7 @@ class TestRunner { this.passed = 0; this.failed = 0; this.results = []; + this.skipped = 0; } /** @@ -104,6 +107,10 @@ class TestRunner { } } + skip(reason = "Skipped") { + throw new SkipTest(reason); + } + async _runWithTimeout(fn, ctx, timeoutMs) { return new Promise((resolve, reject) => { let finished = false; @@ -161,31 +168,49 @@ class TestRunner { try { await delay(200); - - await this._runWithTimeout(test.fn, this, 3000); + await this._runWithTimeout(test.fn, this, 10000); stopSpinner(); this.passed++; - this.results.push({ name: test.name, status: "PASS", error: null }); + this.results.push({ name: test.name, status: "PASS" }); line(` ${COLORS.GREEN}✓${COLORS.RESET} ${test.name}`, COLORS.GREEN); } catch (error) { stopSpinner(); - this.failed++; - this.results.push({ - name: test.name, - status: "FAIL", - error: error.message, - }); - line( - ` ${COLORS.RED}✗${COLORS.RESET} ${test.name}`, - COLORS.RED + COLORS.BRIGHT, - ); - line( - ` ${COLORS.DIM}└─ ${error.message}${COLORS.RESET}`, - COLORS.RED + COLORS.DIM, - ); + if (error instanceof SkipTest) { + this.skipped++; + this.results.push({ + name: test.name, + status: "SKIP", + reason: error.message, + }); + + line( + ` ${COLORS.YELLOW}?${COLORS.RESET} ${test.name}`, + COLORS.YELLOW + COLORS.BRIGHT, + ); + line( + ` ${COLORS.DIM}└─ ${error.message}${COLORS.RESET}`, + COLORS.YELLOW + COLORS.DIM, + ); + } else { + this.failed++; + this.results.push({ + name: test.name, + status: "FAIL", + error: error.message, + }); + + line( + ` ${COLORS.RED}✗${COLORS.RESET} ${test.name}`, + COLORS.RED + COLORS.BRIGHT, + ); + line( + ` ${COLORS.DIM}└─ ${error.message}${COLORS.RESET}`, + COLORS.RED + COLORS.DIM, + ); + } } } @@ -194,15 +219,19 @@ class TestRunner { line("─────────────────────────────────────────────", COLORS.GRAY); const total = this.tests.length; - const percentage = total ? ((this.passed / total) * 100).toFixed(1) : "0.0"; + const effectiveTotal = total - this.skipped; + + const percentage = effectiveTotal + ? ((this.passed / effectiveTotal) * 100).toFixed(1) + : "0.0"; const statusColor = this.failed === 0 ? COLORS.GREEN : COLORS.YELLOW; line( ` Tests: ${COLORS.BRIGHT}${total}${COLORS.RESET} | ` + - `${statusColor}Passed: ${this.passed}${COLORS.RESET} | ` + + `${COLORS.GREEN}Passed: ${this.passed}${COLORS.RESET} | ` + + `${COLORS.YELLOW}Skipped: ${this.skipped}${COLORS.RESET} | ` + `${COLORS.RED}Failed: ${this.failed}${COLORS.RESET}`, - statusColor, ); line( @@ -226,4 +255,11 @@ class TestRunner { } } +class SkipTest extends Error { + constructor(message = "Skipped") { + super(message); + this.name = "SkipTest"; + } +} + export { TestRunner };