diff --git a/test/test_dev.ml b/test/test_dev.ml index 7dca928..715a091 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -3044,11 +3044,66 @@ let () = | None -> fail "(probe-one) produced no value") | Some r -> fail "(probe-one): %s %s" (status r) (message r) | None -> ()); - (try - ignore (Wire.send c "(:op \"close\")"); - ignore (Wire.recv c) - with _ -> ()); - (try Unix.close c with Unix.Unix_error _ -> ()) + (* ── An editor that leaves before its reply does ──────────────── *) + (* The failure this closes was a flake in test_emacs, and it read like + nothing it was: two break-loop checks failing, and then + "cannot reconnect: Connection refused" on a socket path that plainly + existed. A write into a socket whose reader has gone is SIGPIPE, and + SIGPIPE's default action is to kill the process — which in the merged + build is the program, the compiler and the listener at once, and + leaves the socket file behind for the next client to be refused on. + It is not a contrived shape: reconnecting tears the old connection + down, and a reply already on its way out lands in the gap. + + [c] is closed first because the daemon serves one connection at a + time: a second one would sit in the backlog until this one ended, and + then there would be no reply in flight to lose. *) + (try Unix.close c with Unix.Unix_error _ -> ()); + let hit_and_run () = + let s = Unix.socket Unix.PF_UNIX Unix.SOCK_STREAM 0 in + (try + Unix.connect s (Unix.ADDR_UNIX rsock); + Wire.send s "(:op \"describe\")" + with Unix.Unix_error _ -> ()); + (try Unix.close s with Unix.Unix_error _ -> ()) + in + (* Several, because the first write into a freshly closed socket can + still land in a buffer nobody will read; the second is the one that + is refused. *) + for _ = 1 to 8 do hit_and_run (); ignore (Unix.select [] [] [] 0.02) done; + let c = + match Unix.waitpid [ Unix.WNOHANG ] rpid with + | 0, _ -> Some (connect rsock) + | _, st -> + (* Named rather than numbered, because [WSIGNALED] carries OCaml's + own signal numbering and "signal -8" would send the next reader + to the wrong page. SIGPIPE is the answer this check expects to + see when it fails. *) + fail "an editor that left before its reply did ended the daemon (%s)" + (match st with + | Unix.WEXITED n -> Printf.sprintf "exit %d" n + | Unix.WSIGNALED n when n = Sys.sigpipe -> "killed by SIGPIPE" + | Unix.WSIGNALED n -> Printf.sprintf "signal %d" n + | Unix.WSTOPPED n -> Printf.sprintf "stopped on %d" n); + None + | exception Unix.Unix_error _ -> Some (connect rsock) + in + (match c with + | None -> () + | Some c -> + (match Wire.parse (Wire.send c "(:op \"describe\")"; Wire.recv c) with + | r -> + if not (contains_sub (Form.to_string (Option.get (Wire.field r "fns"))) + "probe-one") + then fail "the session lost track of the program after a client left" + | exception e -> + fail "the session did not answer after a client left: %s" + (Printexc.to_string e)); + (try + ignore (Wire.send c "(:op \"close\")"); + ignore (Wire.recv c) + with _ -> ()); + (try Unix.close c with Unix.Unix_error _ -> ())) end; (try Unix.kill rpid Sys.sigkill with Unix.Unix_error _ -> ()); (try ignore (Unix.waitpid [] rpid) with Unix.Unix_error _ -> ());