Skip to content

Commit 7e9234c

Browse files
committed
test(stdune): add spawn test suite
Port the upstream spawn tests into stdune, preserving the upstream test directory layout for helpers and pgid checks. This covers cwd handling, environment passing, process groups, close-on-exec stdout inheritance, relative programs, and sigprocmask behavior. Signed-off-by: Rudi Grinberg <me@rgrinberg.com>
1 parent e37da48 commit 7e9234c

9 files changed

Lines changed: 297 additions & 0 deletions

File tree

otherlibs/stdune/test/spawn/dune

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
(library
2+
(name stdune_spawn_tests)
3+
(inline_tests
4+
(deps
5+
exe/hello.exe
6+
exe/list_files.exe
7+
exe/print_env.exe
8+
pgid_test/checkpgid.exe
9+
(sandbox always)))
10+
(libraries
11+
stdune
12+
unix
13+
;; This is because of the (implicit_transitive_deps false)
14+
;; in dune-project
15+
ppx_expect.config
16+
ppx_expect.config_types
17+
base
18+
ppx_inline_test.config)
19+
(preprocess
20+
(pps ppx_expect)))
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
(executables
2+
(names hello list_files print_env))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
let () = print_endline "Hello, world!"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
let () =
2+
Sys.readdir "." |> Array.to_list |> List.sort String.compare |> List.iter print_endline
3+
;;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
let () =
2+
match Sys.getenv "FOO" with
3+
| exception _ -> print_endline "None"
4+
| str -> Printf.printf "Some %S\n" str
5+
;;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
external getpgid : int -> int = "test_getpgid"
2+
3+
let () =
4+
if not Sys.win32
5+
then (
6+
let pid = Unix.getpid () in
7+
let pgid = getpgid pid in
8+
if pid <> pgid then failwith "pgid and pid not equal")
9+
;;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
(executable
2+
(name checkpgid)
3+
(libraries unix)
4+
(foreign_stubs
5+
(language c)
6+
(names stubs)))
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#include <caml/mlvalues.h>
2+
#include <caml/unixsupport.h>
3+
4+
#if !defined(_WIN32)
5+
6+
#include <sys/types.h>
7+
#include <unistd.h>
8+
9+
CAMLprim value test_getpgid(value pid)
10+
{
11+
return Val_int(getpgid(Int_val(pid)));
12+
}
13+
14+
#else
15+
16+
CAMLprim value test_getpgid(value pid)
17+
{
18+
unix_error(ENOSYS, "getpgid", Nothing);
19+
}
20+
21+
#endif
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
module Exn = Stdune.Exn
2+
module Pid = Stdune.Pid
3+
module Spawn = Stdune.Spawn
4+
5+
let show_raise f =
6+
try ignore (f ()) with
7+
| exn ->
8+
let s =
9+
match exn with
10+
| Unix.Unix_error _ ->
11+
(* For compat with Windows *)
12+
"Unix.Unix_error _"
13+
| exn -> Printexc.to_string exn
14+
in
15+
Printf.printf "raised %s" s
16+
;;
17+
18+
let%expect_test "non-existing program" =
19+
show_raise (fun () -> Spawn.spawn () ~prog:"/doesnt-exist" ~argv:[ "blah" ]);
20+
[%expect
21+
{|
22+
raised Unix.Unix_error _
23+
|}]
24+
;;
25+
26+
let%expect_test "non-existing dir" =
27+
show_raise (fun () ->
28+
Spawn.spawn
29+
()
30+
~prog:"/bin/true"
31+
~argv:[ "true" ]
32+
~cwd:(Spawn.Working_dir.Path "/doesnt-exist"));
33+
[%expect
34+
{|
35+
raised Unix.Unix_error _
36+
|}]
37+
;;
38+
39+
let wait pid =
40+
match snd (Unix.waitpid [] (Pid.to_int pid)) with
41+
| WEXITED 0 -> ()
42+
| WEXITED n -> Printf.ksprintf failwith "exited with code %d" n
43+
| WSIGNALED n -> Printf.ksprintf failwith "got signal %d" n
44+
| WSTOPPED n -> Printf.ksprintf failwith "stopped with signal %d" n
45+
;;
46+
47+
let list_files = Filename.concat (Sys.getcwd ()) "exe/list_files.exe"
48+
49+
let () =
50+
Unix.mkdir "sub" 0o777;
51+
close_out (open_out "sub/foo");
52+
close_out (open_out "sub/bar")
53+
;;
54+
55+
let%expect_test "cwd:Path" =
56+
wait
57+
(Spawn.spawn
58+
()
59+
~prog:list_files
60+
~argv:[ "list_files.exe" ]
61+
~cwd:(Spawn.Working_dir.Path "sub"));
62+
[%expect
63+
{|
64+
bar
65+
foo
66+
|}]
67+
;;
68+
69+
let%expect_test "cwd:Fd" =
70+
if Sys.win32
71+
then print_endline "bar\nfoo"
72+
else (
73+
let fd = Unix.openfile "sub" [ O_RDONLY ] 0 in
74+
wait
75+
(Spawn.spawn
76+
()
77+
~prog:list_files
78+
~argv:[ "list_files.exe" ]
79+
~cwd:(Spawn.Working_dir.Fd fd));
80+
Unix.close fd);
81+
[%expect
82+
{|
83+
bar
84+
foo
85+
|}]
86+
;;
87+
88+
let%expect_test "cwd:Fd (invalid)" =
89+
show_raise (fun () ->
90+
if Sys.win32
91+
then raise (Unix.Unix_error (ENOTDIR, "fchdir", ""))
92+
else
93+
Spawn.spawn
94+
()
95+
~prog:"/bin/pwd"
96+
~argv:[ "pwd" ]
97+
~cwd:(Spawn.Working_dir.Fd Unix.stdin));
98+
[%expect
99+
{|
100+
raised Unix.Unix_error _
101+
|}]
102+
;;
103+
104+
module Program_lookup = struct
105+
let path_sep = if Sys.win32 then ';' else ':'
106+
let exe_ext = if Sys.win32 then ".exe" else ""
107+
108+
let split_path s =
109+
let rec loop i j =
110+
if j = String.length s
111+
then [ String.sub s i (j - i) ]
112+
else if s.[j] = path_sep
113+
then String.sub s i (j - i) :: loop (j + 1) (j + 1)
114+
else loop i (j + 1)
115+
in
116+
loop 0 0
117+
;;
118+
119+
let path =
120+
match Sys.getenv "PATH" with
121+
| exception Not_found -> []
122+
| s -> split_path s
123+
;;
124+
125+
let find_prog prog =
126+
let rec search = function
127+
| [] -> Printf.ksprintf failwith "Program %S not found in PATH!" prog
128+
| dir :: rest ->
129+
let fn = Filename.concat dir prog ^ exe_ext in
130+
if Sys.file_exists fn then fn else search rest
131+
in
132+
search path
133+
;;
134+
end
135+
136+
let%expect_test "inheriting stdout with close-on-exec set" =
137+
(* CR-soon jdimino for jdimino: the test itself seems to pass, however there
138+
seem to be another issue related to ppx_expect and Windows. *)
139+
if Sys.win32
140+
then print_string "hello world"
141+
else (
142+
Unix.set_close_on_exec Unix.stdout;
143+
let shell, arg = if Sys.win32 then "cmd", "/c" else "sh", "-c" in
144+
let prog = Program_lookup.find_prog shell in
145+
wait (Spawn.spawn () ~prog ~argv:[ shell; arg; {|echo "hello world"|} ]));
146+
[%expect {| hello world |}]
147+
;;
148+
149+
let%expect_test "prog relative to cwd" =
150+
if Sys.win32
151+
then print_string "Hello, world!"
152+
else
153+
wait
154+
(Spawn.spawn
155+
()
156+
~prog:"./hello.exe"
157+
~argv:[ "hello" ]
158+
~cwd:(Spawn.Working_dir.Path "exe"));
159+
[%expect {| Hello, world! |}]
160+
;;
161+
162+
let%expect_test "env" =
163+
let tst v =
164+
let env =
165+
match v with
166+
| None -> Spawn.Env.of_list []
167+
| Some v -> Spawn.Env.of_list [ "FOO=" ^ v ]
168+
in
169+
wait
170+
(Spawn.spawn
171+
()
172+
~env
173+
~prog:"./print_env.exe"
174+
~argv:[ "print_env" ]
175+
~cwd:(Spawn.Working_dir.Path "exe"))
176+
in
177+
tst (Some "foo");
178+
[%expect {| Some "foo" |}];
179+
tst None;
180+
[%expect {| None |}];
181+
tst (Some "");
182+
[%expect {| Some "" |}]
183+
;;
184+
185+
let%expect_test "pgid tests" =
186+
wait
187+
(Spawn.spawn
188+
~setpgid:Spawn.Pgid.new_process_group
189+
()
190+
~prog:"pgid_test/checkpgid.exe"
191+
~argv:[]);
192+
[%expect {||}]
193+
;;
194+
195+
let%expect_test "sigprocmask" =
196+
if not Sys.win32
197+
then (
198+
let run ?sigprocmask expected_signal =
199+
let prog = Program_lookup.find_prog "sleep" in
200+
let pid = Spawn.spawn ?sigprocmask ~prog ~argv:[ "sleep"; "60" ] () in
201+
let pid_int = Pid.to_int pid in
202+
Unix.kill pid_int Sys.sigusr1;
203+
Unix.kill pid_int Sys.sigkill;
204+
match Unix.waitpid [] pid_int with
205+
| _, WSIGNALED signal when signal = expected_signal -> ()
206+
| _ -> failwith "unexpected"
207+
in
208+
run Sys.sigusr1;
209+
run ~sigprocmask:(SIG_BLOCK, [ Sys.sigusr1 ]) Sys.sigkill;
210+
let old_signals = Unix.sigprocmask SIG_BLOCK [ Sys.sigusr1 ] in
211+
Exn.protect
212+
~finally:(fun () -> ignore (Unix.sigprocmask SIG_SETMASK old_signals : int list))
213+
~f:(fun () ->
214+
(* The blocking of [sigusr1] is only propagated to the child process if
215+
the sigprocmask is [SIG_BLOCK] or [SIG_UNBLOCK]. *)
216+
run Sys.sigusr1;
217+
run ~sigprocmask:(SIG_BLOCK, []) Sys.sigkill;
218+
run ~sigprocmask:(SIG_UNBLOCK, []) Sys.sigkill;
219+
(* Unblocking sigusr1 in the child process. *)
220+
run ~sigprocmask:(SIG_UNBLOCK, [ Sys.sigusr1 ]) Sys.sigusr1;
221+
run ~sigprocmask:(SIG_SETMASK, []) Sys.sigusr1));
222+
[%expect {||}]
223+
;;
224+
225+
(* This should be at the end to clean up the test environment *)
226+
let () =
227+
Unix.unlink "sub/foo";
228+
Unix.unlink "sub/bar";
229+
Unix.rmdir "sub"
230+
;;

0 commit comments

Comments
 (0)