Skip to content

Commit

Permalink
Allow complete control over stdio (#17)
Browse files Browse the repository at this point in the history
* Add results of stdio tests

* Add ipc tests

* Allow complete control over stdio

* Add more debug info to test failure

* Rename extension to ps1
  • Loading branch information
JordanMartinez authored Nov 14, 2023
1 parent fda2064 commit 9d8b11c
Show file tree
Hide file tree
Showing 16 changed files with 959 additions and 105 deletions.
2 changes: 1 addition & 1 deletion packages.dhall
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ in upstream
, "prelude"
, "unsafe-coerce"
]
with node-child-process.version = "v11.0.0"
with node-child-process.version = "v11.1.0"
with node-child-process.dependencies =
[ "exceptions"
, "node-event-emitter"
Expand Down
1 change: 1 addition & 0 deletions spago.dhall
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
, "either"
, "exceptions"
, "foldable-traversable"
, "foreign"
, "foreign-object"
, "functions"
, "integers"
Expand Down
2 changes: 2 additions & 0 deletions src/Node/Library/Execa.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export function setTimeoutImpl(timeout, cb) {
return t.unref ? t : { unref: () => {} };
}

const undefinedVal = undefined;
export { undefinedVal as undefined };
397 changes: 309 additions & 88 deletions src/Node/Library/Execa.purs

Large diffs are not rendered by default.

26 changes: 16 additions & 10 deletions test/Test/Node/Library/Execa.purs
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,27 @@ import Prelude

import Data.Maybe (Maybe(..))
import Data.Time.Duration (Milliseconds(..))
import Data.Traversable (for_)
import Effect.Class (liftEffect)
import Effect.Exception as Exception
import Node.Buffer as Buffer
import Node.ChildProcess as CP
import Node.ChildProcess.Types (Exit(..), fromKillSignal', intSignal, stringSignal)
import Node.Encoding (Encoding(..))
import Node.Library.Execa (execa, execaCommand, execaCommandSync, execaSync)
import Node.Library.HumanSignals (signals)
import Node.Path as Path
import Node.UnsafeChildProcess.Safe as SafeCP
import Test.Node.Library.Utils (isWindows, itNix)
import Test.Spec (Spec, describe, it)
import Test.Spec.Assertions (fail, shouldEqual)
import Test.Spec.Assertions as Assertions
import Test.Spec.Assertions.String (shouldContain)

spec :: Spec Unit
spec = describe "execa" do
itNix "`echo test` should fail due to a Node.js bug" do
spawned <- execa "echo" [] identity
spawned.stdin.writeUtf8End "test"
for_ spawned.stdin \s -> s.writeUtf8End "test"
result <- spawned.getResult
case result.stdinError of
Nothing -> fail "Expected EPIPE error"
Expand All @@ -36,7 +38,7 @@ spec = describe "execa" do
_ -> fail result.message
itNix "input is buffer" do
spawned <- execa "cat" [ "-" ] identity
spawned.stdin.writeUtf8End "test"
for_ spawned.stdin \s -> s.writeUtf8End "test"
result <- spawned.getResult
case result.exit of
Normally 0 -> result.stdout `shouldEqual` "test"
Expand All @@ -49,7 +51,7 @@ spec = describe "execa" do
describe "using sleep files" do
let
shellCmd = if isWindows then "pwsh" else "sh"
sleepFile = Path.concat [ "test", "fixtures", "sleep." <> if isWindows then "cmd" else "sh" ]
sleepFile = Path.concat [ "test", "fixtures", "sleep." <> if isWindows then "ps1" else "sh" ]
describe "kill works" do
it "basic cancel produces error" do
spawned <- execa shellCmd [ sleepFile, "1" ] identity
Expand Down Expand Up @@ -80,12 +82,16 @@ spec = describe "execa" do
result <- spawned.getResult
case result.exit of
Normally 64 | isWindows -> do
sig <- liftEffect $ CP.signalCode spawned.childProcess
sig `shouldEqual` (Just "SIGTERM")
result.timedOut `shouldEqual` true
sig <- liftEffect $ SafeCP.signalCode spawned.childProcess
when (sig /= (Just "SIGTERM")) do
Assertions.fail $ "Didn't get expected kill signal. Result was\n" <> show result
unless (result.timedOut) do
Assertions.fail $ "Result didn't indicate time out. Result was\n" <> show result
BySignal sig -> do
sig `shouldEqual` (stringSignal "SIGTERM")
result.timedOut `shouldEqual` true
when (sig /= (stringSignal "SIGTERM")) do
Assertions.fail $ "Didn't get expected kill signal. Result was\n" <> show result
unless (result.timedOut) do
Assertions.fail $ "Result didn't indicate time out. Result was\n" <> show result
_ ->
fail $ "Timeout should work: " <> show result
describe "execaSync" do
Expand All @@ -111,7 +117,7 @@ spec = describe "execa" do
_ -> fail result.message
itNix "input is buffer" do
spawned <- execaCommand "cat -" identity
spawned.stdin.writeUtf8End "test"
for_ spawned.stdin \s -> s.writeUtf8End "test"
result <- spawned.getResult
case result.exit of
Normally 0 -> result.stdout `shouldEqual` "test"
Expand Down
2 changes: 0 additions & 2 deletions test/fixtures/outErr.cmd

This file was deleted.

4 changes: 0 additions & 4 deletions test/fixtures/outErr.sh

This file was deleted.

File renamed without changes.
11 changes: 11 additions & 0 deletions test/ipc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# ipc

This folder contains a small script for seeing when `ipc` stdio value can be used.

Conclusions:

| Function | Result |
| - | - |
| `spawn` | works |
| `spawnSync` | runtime error |
| `fork` | works |
28 changes: 28 additions & 0 deletions test/ipc/child.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import process from "node:process";

console.log("Script started");

const tc = (msg, f) => {
try {
f();
console.log(`${msg}: Success`);
} catch (e) {
console.log(`${msg}: Failure - ${e}`);
}
};

process.on("message", (msg) => {
console.log(`Child got parent message: ${msg}`);
});

tc("Child -> Parent Message", () => {
process.send("Hello from child");
});

setTimeout(() => {
tc("Child disconnect from parent", () => {
process.disconnect();
});

console.log("Script finished");
}, 100);
26 changes: 26 additions & 0 deletions test/ipc/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@


Spawn - IPC usage: Success
Parent receives message: Success
Parent -> Child: Success
Script started
Child -> Parent Message: Success
Parent received message: Hello from child
Child got parent message: Hello from Parent
Child disconnect from parent: Success
Script finished


SpawnSync - IPC usage: Failure - Error [ERR_IPC_SYNC_FORK]: IPC cannot be used with synchronous forks


Fork - IPC usage: Success
Parent receives message: Success
Parent -> Child: Success
Script started
Child -> Parent Message: Success
Parent received message: Hello from child
Child got parent message: Hello from Parent
Child disconnect from parent: Success
Script finished
Finished
52 changes: 52 additions & 0 deletions test/ipc/parent.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import child_process from "node:child_process";

const stdio = [ "ignore", "inherit", "inherit", "ipc"];

const tc = (msg, f) => {
try {
f();
console.log(`${msg}: Success`);
} catch (e) {
console.log(`${msg}: Failure - ${e}`);
}
};

const go = (idx) => {
var child;
if (idx === 1) {
tc("\n\nSpawn - IPC usage", () => {
child = child_process.spawn("node", ["child.mjs"], { stdio });
});
} else if (idx === 2) {
tc("\n\nSpawnSync - IPC usage", () => {
child = child_process.spawnSync("node", ["child.mjs"], { stdio });
});
} else if (idx === 3) {
tc("\n\nFork - IPC usage", () => {
child = child_process.fork("child.mjs", { stdio });
});
}
if (!child) {
if (idx !== 4) {
go(idx+1);
} else {
console.log("Finished");
}
} else {
tc("Parent receives message", () => {
child.on("message", (msg) => {
console.log(`Parent received message: ${msg}`);
});
});
child.on("spawn", () => {
tc("Parent -> Child", () => {
child.send("Hello from Parent");
});
});
child.on("exit", () => {
go(idx + 1);
});
}
};

go(1);
17 changes: 17 additions & 0 deletions test/stdio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# stdio

This folder contains a small script for seeing what is needed to terminate a child process with the different `stdio` options. It also helps verify when a `stream` will have a value and when it will be `null`.

Conclusions:

| stdio | Stream | Read | Write | Program terminates when |
| - | - | - | - | - |
| `inherit` | `stdin` | Error | Error | `Ctrl+D` pressed |
| `ignore` | `stdin` | Error | Error | Event Loop finishes |
| `pipe` | `stdin` | Error | Success | `stdin.end()` called |
| `inherit` | `stdout` | Error | Error | Event Loop finishes |
| `ignore` | `stdout` | Error | Error | Event Loop finishes |
| `pipe` | `stdout` | Success | Error | Event Loop finishes |
| `inherit` | `stderr` | Error | Error | Event Loop finishes |
| `ignore` | `stderr` | Error | Error | Event Loop finishes |
| `pipe` | `stderr` | Success | Error | Event Loop finishes |
22 changes: 22 additions & 0 deletions test/stdio/child.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import process from "node:process";

console.log("Script started");
process.stdin.once("data", (str) => {
console.log(`Got data from stdin: ${str}`);
});

const ms = 100;

setTimeout(() => {
console.log("log 1");
setTimeout(() => {
console.error("error 1");
setTimeout(() => {
console.log("log 2");
setTimeout(() => {
console.error("error 2");
console.log("Script terminated");
}, ms);
}, ms);
}, ms);
}, ms);
Loading

0 comments on commit 9d8b11c

Please sign in to comment.