Ten test binaries share a directory and had shared nothing in it but watchdog.ml. Everything else each one needed it wrote out again: the failure counter and its FAIL line, the three-line report tail, a poll, a socket connect, the wait for a [flan dev] daemon to bind, a substring search, and the Load -> Check -> Reach.link front half of a compile. [listening] was the clearest case. Three copies, byte for byte apart from one comment, and two of them said in that comment that they were kept separate because "these three files have no module between them". That was not true when it was written: watchdog.ml was already named in the same (modules ...) stanzas. test_support.ml is the second such module, wired the same way, and those two sentences go with the copies they were explaining. test_repl.ml's [quote] was Wire.quote character for character, in a file that already links Wire and already names Wire.quote in a comment about what the case below it is checking. It is Wire.quote now. One real behaviour change, and it is a fix. [connect] existed twice over with different retries: the agent's narrowed to ECONNREFUSED with a comment saying why -- the socket file appears at bind, a moment before listen -- while dev's and repl's retried any Unix_error, which meant an ENOENT or an EACCES was retried to the full timeout before raising something the reader still had to interpret. The shared one takes the narrow version. Every caller connects to a socket [listening] has already seen on disk, so the race it does catch is the only one left. The rest is left where it is, on purpose. The three output-capturing [run]s differ in what they wrap -- a pid suffix, a sanitizer environment, a valgrind invocation -- and are not the same function. The report tails in test_repl, test_web and the two sweep binaries print different things for different reasons. The per-file scratch prefixes are the feature that keeps two suites running at once from unlinking each other's sockets, so the shared helper takes the prefix rather than choosing one. And the [match Sys.command "command -v clang ..."] probes stay as they are: their skip lines are output this suite pins. bin/main.ml has the compile pipeline written out twice more. Left alone -- this was a test/-scoped change and bin/ should not be reaching into a test module -- and noted in FIX.org as what it actually needs, which is the pipeline moving into lib/. dune test: exit 0, and its output is the same line for line once the temp-directory hash and the millisecond counts are normalised.
150 lines
7.2 KiB
OCaml
150 lines
7.2 KiB
OCaml
(* The Emacs client, against a real daemon and a real running program.
|
|
|
|
test_dev.ml proves the daemon answers correctly. This proves the elisp
|
|
actually talks to it, which is not the same claim: the framing is in bytes
|
|
and Emacs counts characters, `beginning-of-defun' has to find a Flan
|
|
top-level form through Flan's own syntax table, and a reply is read with
|
|
`read'. A mistake in any of those passes the OCaml test and fails here.
|
|
|
|
Skipped, not failed, where there is no emacs — the compiler does not depend
|
|
on one. *)
|
|
|
|
(* The watchdog first: a hang is the one failure mode that reports
|
|
nothing at all. See watchdog.ml. *)
|
|
let () = Watchdog.arm ~seconds:600 "test_emacs"
|
|
(* The scratch paths, the poll and the daemon wait are in test_support.ml. The
|
|
long note on what [listening] can and cannot tell apart is there with it. *)
|
|
let tmp n = Test_support.tmp "flan-emacs-" n
|
|
let await = Test_support.await
|
|
let listen_why = Test_support.listen_why
|
|
let listening = Test_support.listening
|
|
|
|
let () =
|
|
let have = Test_support.have in
|
|
if not (have "emacs") then print_endline "emacs: skipped (no emacs on PATH)"
|
|
else if not (have "clang" && have "llc") then
|
|
print_endline "emacs: skipped (no clang or llc on PATH)"
|
|
else begin
|
|
let sock = tmp "dev.sock" and out = tmp "prog.out" and err = tmp "dev.err" in
|
|
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) [ sock; out; err ];
|
|
let fd = Unix.openfile out [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in
|
|
(* The daemon's stderr to a file rather than inherited: it carries the
|
|
"ready on" line and any OCaml backtrace, and a failure here wants both
|
|
quoted next to the client's complaint rather than interleaved with
|
|
whatever else dune was running. *)
|
|
let efd = Unix.openfile err [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in
|
|
let flan = "../bin/main.exe" in
|
|
(* Absolute, because the client starts its own daemon at the end of the run
|
|
and does it from the program's directory rather than from this one. *)
|
|
let flan_abs = try Unix.realpath flan with Unix.Unix_error _ -> flan in
|
|
(* And the program itself, for the same reason: the client starts a daemon
|
|
of its own at the end of the run, and the copy it edits is in a
|
|
temporary directory with no package collections above it. *)
|
|
let program =
|
|
let p = "programs/dev-repl.flan" in
|
|
try Unix.realpath p with Unix.Unix_error _ -> p
|
|
in
|
|
(* [--llvm], and it is not incidental: this suite drives the inspector --
|
|
backtrace, locals, globals -- and those are frames on the dev shadow
|
|
stack, which [lib/x86.ml] does not push. The default backend for [flan
|
|
dev] is x86 now, so a flagless daemon here would answer every one of
|
|
those with the refusal naming this flag, and the client half of the
|
|
break protocol would go untested. The client's own daemon at the end of
|
|
the run asks for it through [flan-daemon-args] for the same reason,
|
|
which is also where that setting is proved to reach a command line. *)
|
|
let pid =
|
|
Unix.create_process flan
|
|
[| flan; "dev"; "programs/dev-repl.flan"; "-s"; sock; "--llvm" |]
|
|
Unix.stdin fd efd
|
|
in
|
|
Unix.close fd;
|
|
Unix.close efd;
|
|
(* Thirty seconds and not the 8s default: this waits on an llc-and-link of
|
|
the whole program, ~0.5s warm and 2s cold, worst measured at 6.8s with
|
|
this suite's other binaries running beside it. The watchdog bounds the
|
|
run; a crash no longer waits for either. *)
|
|
if not (listening ~pid sock) then begin
|
|
print_endline ("FAIL the daemon " ^ !listen_why);
|
|
(try Unix.kill pid Sys.sigkill with Unix.Unix_error _ -> ());
|
|
exit 1
|
|
end;
|
|
(* A copy, because the test edits the buffer it sends from. Removed first
|
|
and made writable after: the original comes out of a build directory
|
|
read-only, so copying onto a leftover copy would fail and leave the old
|
|
one in place. *)
|
|
let buf = tmp "buf.flan" in
|
|
(try Sys.remove buf with Sys_error _ -> ());
|
|
ignore
|
|
(Sys.command
|
|
(Printf.sprintf "cp %s %s" (Filename.quote "programs/dev-repl.flan")
|
|
(Filename.quote buf)));
|
|
(try Unix.chmod buf 0o644 with Unix.Unix_error _ -> ());
|
|
let code =
|
|
Sys.command
|
|
(Printf.sprintf
|
|
"emacs -Q --batch -L ../../../emacs -l ../../../emacs/test-flan.el \
|
|
-- %s %s %s %s 2>&1"
|
|
(Filename.quote sock) (Filename.quote buf) (Filename.quote flan_abs)
|
|
(Filename.quote program))
|
|
in
|
|
(* The client disconnects at the end, which is what ends the daemon. If it
|
|
did not get that far — because it failed — nothing else will, so it is
|
|
stopped here rather than left waiting on a socket nobody will use.
|
|
|
|
The wait *status* is kept, and not thrown away as it used to be, because
|
|
it is the one fact that tells the two failure shapes apart. A daemon
|
|
that exited on its own runs its cleanup and unlinks the socket, and the
|
|
client then says "nothing is listening"; a daemon killed by a signal
|
|
never gets that far, leaves the socket file behind, and the client gets
|
|
an ECONNREFUSED it cannot explain. Two diagnoses of this flake have
|
|
already been wrong for want of this line. *)
|
|
let status = ref None in
|
|
if not (await ~ms:3000 (fun () ->
|
|
match Unix.waitpid [ Unix.WNOHANG ] pid with
|
|
| 0, _ -> false
|
|
| _, st -> status := Some st; true
|
|
| exception Unix.Unix_error _ -> true))
|
|
then begin
|
|
(try Unix.kill pid Sys.sigterm with Unix.Unix_error _ -> ());
|
|
(try ignore (Unix.waitpid [] pid) with Unix.Unix_error _ -> ())
|
|
end;
|
|
let how =
|
|
match !status with
|
|
| Some (Unix.WEXITED n) -> Printf.sprintf "exited with status %d" n
|
|
| Some (Unix.WSIGNALED n) ->
|
|
Printf.sprintf "was killed by signal %d — nothing ran its cleanup, so \
|
|
the socket file is stale rather than gone" n
|
|
| Some (Unix.WSTOPPED n) -> Printf.sprintf "stopped on signal %d" n
|
|
| None -> "was still running when the client finished, and was terminated"
|
|
in
|
|
(* Read before the removal below, because on a failure this is the evidence
|
|
and the removal is what destroyed it last time. *)
|
|
let tail_of path =
|
|
match open_in_bin path with
|
|
| ic ->
|
|
let n = in_channel_length ic in
|
|
let want = min n 4000 in
|
|
seek_in ic (n - want);
|
|
let s = really_input_string ic want in
|
|
close_in ic;
|
|
s
|
|
| exception Sys_error _ -> "<no such file>"
|
|
in
|
|
let sock_left = Sys.file_exists sock in
|
|
let prog_out = if code = 0 then "" else tail_of out in
|
|
let daemon_err = if code = 0 then "" else tail_of err in
|
|
List.iter
|
|
(fun f -> try Sys.remove f with Sys_error _ -> ())
|
|
[ sock; out; err; buf ];
|
|
if code = 0 then print_endline "emacs: all tests passed"
|
|
else begin
|
|
Printf.printf "\nemacs client exited %d\n" code;
|
|
Printf.printf "the daemon %s\n" how;
|
|
Printf.printf "%s was %s after the run\n" sock
|
|
(if sock_left then "still there" else "gone");
|
|
Printf.printf "\n-- the program's own output, last 4k --\n%s\n" prog_out;
|
|
Printf.printf "\n-- the daemon's stderr, last 4k --\n%s\n" daemon_err;
|
|
exit 1
|
|
end
|
|
end
|