# ez/quiet: bend's own chatter dropped from a run's output, so a test sees only # what its program printed. Since 2.0.32 bend prints no check report in front # of a program it runs: a verdict (`ALL PROOFS CHECK` or `SOME PROOFS FAIL`) # is printed only for a file with no main, and a run that fails its check # fails the test on what it printed, which is kept. The report lines older # releases printed in front of every run are no longer dropped. # # bend also nags about its own updates: `bend 2.0.21 is available: run bend # update`, printed whenever a newer release exists and the network can be # reached. That line is not the program's output either, and leaving it in # fails every test in the tree the day upstream cuts a release. It is worth # being precise about how bad that is: the gate went from green to 33 of 52 # here without a line of ez changing, and it did it the moment a laptop # regained its network, so it is invisible offline and arrives looking like a # regression someone just introduced. It is dropped. import Base # whether this line is bend telling us a newer bend exists. Anchored at both # ends rather than matched loosely: a test that legitimately prints the words # `bend update` in prose should still be compared on them. def is_nag(+line: String) -> Bool: Bool.and(String.starts_with(line, "bend "), String.ends_with(line, " is available: run bend update")) # a trailing run of blank lines, being dropped from the right. `live` turns true # at the first line that survives, after which nothing is dropped. type Tail is Data: Tail{live: Bool, out: List<&2, String>} # a line kept or dropped def put(drop: Bool, line: String, rest: List<&2, String>) -> List<&2, String>: match drop: case True{}: rest case False{}: line <> rest # the lines with the nag dropped def go(ls: List<&2, String>) -> List<&2, String>: match ls: case []: [] case +h <> t: put(is_nag(h), h, go(t)) # a blank line: kept once something after it has survived, dropped while the # run of trailing blanks is still open def tail.blank(live: Bool, out: List<&2, String>) -> Tail: match live: case True{}: Tail{True{}, "" <> out} case False{}: Tail{False{}, out} # one line offered to the tail. The line is taken apart rather than asked about, # because a fold's step may not declare a `+` parameter to duplicate it. def tail.line(line: String, live: Bool, out: List<&2, String>) -> Tail: match line: case SNil{}: tail.blank(live, out) case SCon{c, t}: Tail{True{}, SCon{c, t} <> out} # a fold from the right: until a line survives, a blank one is dropped def tail.step(line: String, st: Tail) -> Tail: Tail{live, out} = st tail.line(line, live, out) # what a finished fold collected def tail.out(st: Tail) -> List<&2, String>: Tail{_live, out} = st out # the fold from the right, written out. Base's List.foldr takes its step as an # erased argument, and the termination check cannot read through one. def tail.walk(ls: List<&2, String>) -> Tail: match ls: case []: Tail{False{}, []} case h <> t: tail.step(h, tail.walk(t)) # the lines with the blank ones at the end dropped, the way a shell's `$(...)` # drops a trailing newline, so a comparison never turns on one def chomp.lines(ls: List<&2, String>) -> List<&2, String>: tail.out(tail.walk(ls)) # the text with its trailing blank lines dropped def chomp(text: String) -> String: String.join(chomp.lines(String.lines(text)), "\n") # a run's output as a test's `#|` trailer states it: no update nag, no # trailing newline def filter(text: String) -> String: String.join(chomp.lines(go(String.lines(text))), "\n")