239 lines
12 KiB
OCaml
239 lines
12 KiB
OCaml
(* The plumbing every test binary in this directory needs, in one place.
|
|
|
|
Nothing here tests anything. It is the scaffolding the cases stand on: a
|
|
failure counter and the three lines that report it, a poll, a socket
|
|
connect that survives the bind/listen race, the wait for a [flan dev]
|
|
daemon to come up, a substring search, and the Load → Check → Reach.link
|
|
front half of a compile.
|
|
|
|
It exists because each of those had been written out again in every file
|
|
that wanted it — [listening] three times byte for byte apart from one
|
|
comment, the substring search ten times, a report tail in every suite — and
|
|
two of those copies carried a comment saying 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, which is exactly the module between them. This
|
|
file is the second such module, named in all four of test/dune's stanzas
|
|
the way watchdog is in three of them.
|
|
|
|
No [let () = ...] at the top level here on purpose. A module linked into
|
|
ten binaries must not do anything on the way in; the watchdog is armed by
|
|
each test's own first line, where the seconds are that test's decision. *)
|
|
|
|
open Flan
|
|
|
|
(* ── Failures, and the report of them ─────────────────────────────── *)
|
|
|
|
(* One counter per process, and every [fail] goes through it. Each binary
|
|
binds its own [failures] to this ref rather than making one of its own, so
|
|
[report] below sees what the file's own [incr failures] did. *)
|
|
let failures = ref 0
|
|
|
|
let fail fmt =
|
|
Printf.ksprintf (fun s -> incr failures; print_endline ("FAIL " ^ s)) fmt
|
|
|
|
(* What a build that raised says, for a [fail] line. [Failure] carries its
|
|
text bare; anything else, a [Loc.Error] most of all, goes through the
|
|
printer [Loc] registers, which is the diagnostic itself. A sweep that caught
|
|
only [Failure] died on the first source error it met — "no package at ...",
|
|
from a package the build tree was missing — with no FAIL line and every row
|
|
after it unrun. *)
|
|
let raised = function Failure m -> m | e -> Printexc.to_string e
|
|
|
|
(* The tail every suite ends on: a single line when nothing failed, and a
|
|
count plus a nonzero exit when something did. The label names the suite,
|
|
because these binaries run under dune's parallelism and their output is
|
|
interleaved — "all tests passed" on its own says nothing about which.
|
|
[?label] is optional for the one caller that has no label to give: the
|
|
last of test_flan.ml's three tails covers the whole file and has always
|
|
printed the bare sentence, and this keeps that byte for byte.
|
|
|
|
Not every tail in this directory is this one, and the others are left
|
|
where they are: test_repl.ml quotes the daemon's exit status underneath
|
|
the count, test_web.ml says "web: ok", and the two sweep binaries print a
|
|
count without exiting on the spot. Those differ because they report
|
|
different things, not because they drifted. *)
|
|
let report ?label () =
|
|
if !failures = 0 then
|
|
print_endline
|
|
(match label with
|
|
| Some l -> l ^ ": all tests passed"
|
|
| None -> "all tests passed")
|
|
else begin
|
|
Printf.printf "\n%d failure(s)\n" !failures;
|
|
exit 1
|
|
end
|
|
|
|
(* ── Strings ──────────────────────────────────────────────────────── *)
|
|
|
|
(* Substring search, hand-written because Str is a dependency the rest of
|
|
this directory does not take (test_valgrind.ml is the one exception, and
|
|
it takes it for a regexp over memcheck's summary line).
|
|
|
|
An empty needle is contained in anything, which is the answer the majority
|
|
of the copies this replaces gave and the one that agrees with every other
|
|
[contains] in the world. Three inline copies in test_dev.ml answered
|
|
[false] instead; every needle passed to any of them was a non-empty string
|
|
literal, so no call site could tell the difference. *)
|
|
let contains hay needle =
|
|
let n = String.length needle and h = String.length hay in
|
|
let rec go i = i + n <= h && (String.sub hay i n = needle || go (i + 1)) in
|
|
go 0
|
|
|
|
(* ── Scratch files ────────────────────────────────────────────────── *)
|
|
|
|
let scratch = Own_tmp.dir
|
|
|
|
(* [tmp prefix name] is a path in the scratch directory. The prefix is the
|
|
caller's, not a default, and that is the point: these binaries run at the
|
|
same time under dune, so "flan-agent-dev.sock" and "flan-repl-dev.sock"
|
|
being different files is what keeps two suites from unlinking each other's
|
|
sockets. *)
|
|
let tmp prefix name =
|
|
(* A socket goes without the prefix: [scratch] is this binary's alone, and a
|
|
unix socket path is short (see [Wire.max_socket_path]), which an emacs
|
|
client connecting by the plain path cannot get around. *)
|
|
if Filename.check_suffix name ".sock" then Filename.concat scratch name
|
|
else Filename.concat scratch (prefix ^ name)
|
|
|
|
(* ── Toolchain probes ─────────────────────────────────────────────── *)
|
|
|
|
(* Whether a program is on PATH. Missing tools are a skip with the reason in
|
|
this directory, never a red test — the compiler does not depend on emacs,
|
|
emscripten or valgrind being installed. *)
|
|
let have prog =
|
|
Sys.command (Printf.sprintf "command -v %s > /dev/null 2>&1" prog) = 0
|
|
|
|
(* ── Waiting ──────────────────────────────────────────────────────── *)
|
|
|
|
(* Poll for a condition rather than sleeping a fixed time: a program has to
|
|
bind its socket before there is anything to connect to, and how long that
|
|
takes is not ours to predict. Five milliseconds a turn, [ms] milliseconds
|
|
in total, [false] if the budget ran out.
|
|
|
|
Five seconds by default, which is what test_dev.ml's copy had and what its
|
|
several dozen bare call sites were written against. The callers that want
|
|
another budget pass one. *)
|
|
let rec await ?(ms = 5000) f =
|
|
if f () then true
|
|
else if ms <= 0 then false
|
|
else begin ignore (Unix.select [] [] [] 0.005); await ~ms:(ms - 5) f end
|
|
|
|
(* The socket file appears at [bind], which is a moment before [listen], so a
|
|
connect can lose that race and get ECONNREFUSED. Retry rather than sleep.
|
|
|
|
ECONNREFUSED and not any [Unix_error], which is the narrower of the two
|
|
spellings this had grown. The wide one retried ENOENT and EACCES to the
|
|
full timeout as well, so a path that was never going to exist cost seconds
|
|
before raising something a reader still had to interpret; narrowed, those
|
|
two come straight back out of [Unix.connect] with their own name on them.
|
|
Every caller here connects to a socket [listening] has already seen on
|
|
disk, so the race this does catch is the only one left. *)
|
|
let rec connect ?(ms = 5000) path =
|
|
let s = Unix.socket Unix.PF_UNIX Unix.SOCK_STREAM 0 in
|
|
match Wire.connect_socket s path with
|
|
| () -> s
|
|
| exception Unix.Unix_error (Unix.ECONNREFUSED, _, _) when ms > 0 ->
|
|
Unix.close s;
|
|
ignore (Unix.select [] [] [] 0.005);
|
|
connect ~ms:(ms - 5) path
|
|
|
|
(* Waiting for a daemon to listen is waiting for two different things with one
|
|
timer: [flan dev] compiles the whole program first, and only then binds. The
|
|
old message, "the daemon never listened", named the second and was almost
|
|
always the first — which is a wrong diagnosis, and a wrong diagnosis costs
|
|
more than no message at all.
|
|
|
|
So this says which. It cannot separate the two waits without a signal from
|
|
[flan dev] that the build is done (see TODO.org, "test_dev daemons fail to
|
|
bind under load"), but it can separate the two *failures*, and that is what
|
|
actually gets read: a daemon still running when
|
|
the timer expires was building, and a daemon that is gone bound nothing
|
|
because it died. The second no longer costs the whole timeout either —
|
|
the poll watches the process as well as the socket, so a crash fails in
|
|
milliseconds instead of in half a minute, which is the part that makes a
|
|
suite worth trusting.
|
|
|
|
Thirty seconds, down from a minute, because the object cache is durable now
|
|
(Build.cachedir) and the build this waits on is warm: 0.48s idle against
|
|
2.0s cold, and the worst ever measured under dune's own parallelism was
|
|
6.8s — cold. Each caller's watchdog is still what bounds its run.
|
|
|
|
[!listen_why] carries the reason to the caller so each site can keep its own
|
|
name for its daemon. One ref is enough even though three binaries now share
|
|
this code: they are three processes, each single-threaded, and the next
|
|
thing after a failed wait is always the report of it. *)
|
|
let listen_why = ref ""
|
|
|
|
(* A signal as [WSIGNALED] carries it, by name. OCaml numbers the signals it
|
|
knows negatively and in its own order — [Sys.sigterm] is -11 and
|
|
[Sys.sigsegv] is -10 — so printing the number reads as the wrong signal to
|
|
anyone who knows the POSIX table: a daemon terminated by SIGTERM was once
|
|
recorded here as "a transient signal 11", a segfault that never happened. *)
|
|
let signal_name n =
|
|
let known =
|
|
[ Sys.sigabrt, "SIGABRT"; Sys.sigalrm, "SIGALRM"; Sys.sigfpe, "SIGFPE";
|
|
Sys.sighup, "SIGHUP"; Sys.sigill, "SIGILL"; Sys.sigint, "SIGINT";
|
|
Sys.sigkill, "SIGKILL"; Sys.sigpipe, "SIGPIPE"; Sys.sigquit, "SIGQUIT";
|
|
Sys.sigsegv, "SIGSEGV"; Sys.sigterm, "SIGTERM"; Sys.sigusr1, "SIGUSR1";
|
|
Sys.sigusr2, "SIGUSR2"; Sys.sigchld, "SIGCHLD"; Sys.sigbus, "SIGBUS";
|
|
Sys.sigtrap, "SIGTRAP"; Sys.sigxcpu, "SIGXCPU" ]
|
|
in
|
|
match List.assoc_opt n known with
|
|
| Some s -> s
|
|
| None -> Printf.sprintf "signal %d" n
|
|
|
|
let listening ?(ms = 30000) ~pid path =
|
|
let died = ref None in
|
|
ignore
|
|
(await ~ms (fun () ->
|
|
Sys.file_exists path
|
|
||
|
|
(* Reaped only once it is already gone, and only on the path that ends
|
|
in a failure, so a teardown's own [waitpid] is unaffected. *)
|
|
match Unix.waitpid [ Unix.WNOHANG ] pid with
|
|
| 0, _ -> false
|
|
| _, st -> died := Some st; true
|
|
| exception Unix.Unix_error _ -> false));
|
|
if Sys.file_exists path then true
|
|
else begin
|
|
listen_why :=
|
|
(match !died with
|
|
| Some (Unix.WEXITED n) ->
|
|
Printf.sprintf "exited with status %d before binding %s" n path
|
|
| Some (Unix.WSIGNALED n) ->
|
|
Printf.sprintf "was killed by %s before binding %s" (signal_name n) path
|
|
(* Unreachable without WUNTRACED, and here only for exhaustiveness. *)
|
|
| Some (Unix.WSTOPPED n) ->
|
|
Printf.sprintf "stopped on %s without binding %s" (signal_name n) path
|
|
| None ->
|
|
Printf.sprintf
|
|
"was still running after %ds without binding %s, so it was the \
|
|
build that did not finish, not the socket"
|
|
(ms / 1000) path);
|
|
false
|
|
end
|
|
|
|
(* ── The front half of a compile ──────────────────────────────────── *)
|
|
|
|
(* Read, load and check a program, through [Front], which is the path
|
|
[flan build] takes too. Without [~all], so a refusal raises the first error
|
|
rather than the list. Through [Load], so a program with an (import ...) is
|
|
buildable here — it brings back the package's declarations as well as the
|
|
file's own. *)
|
|
let checked path = snd (Front.checked path)
|
|
|
|
(* And the link, which is where the binaries below actually differ from
|
|
one another: [Reach.link] decides the link from the checked program — a
|
|
package nothing reachable calls into hands over no C and no linker
|
|
argument, and its functions are not emitted — and returns the program
|
|
together with the C sources and linker flags the build needs.
|
|
|
|
This stops at [Build.executable] deliberately. Every caller passes a
|
|
different [Build.opts] (a sanitized build, a -O0 one, an x86 one, a web
|
|
one) and several want the exception rather than the executable, so the
|
|
options record is the one part that is genuinely theirs. *)
|
|
let linked ?dev path =
|
|
let f = Front.linked ?dev path in
|
|
(f.Front.program, f.Front.csrcs, f.Front.lflags)
|