1 # ez/quiet: bend's own chatter dropped from a run's output, so a test sees only 2 # what its program printed. Since 2.0.32 bend prints no check report in front 3 # of a program it runs: a verdict (`ALL PROOFS CHECK` or `SOME PROOFS FAIL`) 4 # is printed only for a file with no main, and a run that fails its check 5 # fails the test on what it printed, which is kept. The report lines older 6 # releases printed in front of every run are no longer dropped. 7 # 8 # bend also nags about its own updates: `bend 2.0.21 is available: run bend 9 # update`, printed whenever a newer release exists and the network can be 10 # reached. That line is not the program's output either, and leaving it in 11 # fails every test in the tree the day upstream cuts a release. It is worth 12 # being precise about how bad that is: the gate went from green to 33 of 52 13 # here without a line of ez changing, and it did it the moment a laptop 14 # regained its network, so it is invisible offline and arrives looking like a 15 # regression someone just introduced. It is dropped. 16 import Base 17 18 # whether this line is bend telling us a newer bend exists. Anchored at both 19 # ends rather than matched loosely: a test that legitimately prints the words 20 # `bend update` in prose should still be compared on them. 21 def is_nag(+line: String) -> Bool: 22 Bool.and(String.starts_with(line, "bend "), 23 String.ends_with(line, " is available: run bend update")) 24 25 # a trailing run of blank lines, being dropped from the right. `live` turns true 26 # at the first line that survives, after which nothing is dropped. 27 type Tail is Data: 28 Tail{live: Bool, out: List<&2, String>} 29 30 # a line kept or dropped 31 def put(drop: Bool, line: String, rest: List<&2, String>) -> List<&2, String>: 32 match drop: 33 case True{}: 34 rest 35 case False{}: 36 line <> rest 37 38 # the lines with the nag dropped 39 def go(ls: List<&2, String>) -> List<&2, String>: 40 match ls: 41 case []: 42 [] 43 case +h <> t: 44 put(is_nag(h), h, go(t)) 45 46 # a blank line: kept once something after it has survived, dropped while the 47 # run of trailing blanks is still open 48 def tail.blank(live: Bool, out: List<&2, String>) -> Tail: 49 match live: 50 case True{}: 51 Tail{True{}, "" <> out} 52 case False{}: 53 Tail{False{}, out} 54 55 # one line offered to the tail. The line is taken apart rather than asked about, 56 # because a fold's step may not declare a `+` parameter to duplicate it. 57 def tail.line(line: String, live: Bool, out: List<&2, String>) -> Tail: 58 match line: 59 case SNil{}: 60 tail.blank(live, out) 61 case SCon{c, t}: 62 Tail{True{}, SCon{c, t} <> out} 63 64 # a fold from the right: until a line survives, a blank one is dropped 65 def tail.step(line: String, st: Tail) -> Tail: 66 Tail{live, out} = st 67 tail.line(line, live, out) 68 69 # what a finished fold collected 70 def tail.out(st: Tail) -> List<&2, String>: 71 Tail{_live, out} = st 72 out 73 74 # the fold from the right, written out. Base's List.foldr takes its step as an 75 # erased argument, and the termination check cannot read through one. 76 def tail.walk(ls: List<&2, String>) -> Tail: 77 match ls: 78 case []: 79 Tail{False{}, []} 80 case h <> t: 81 tail.step(h, tail.walk(t)) 82 83 # the lines with the blank ones at the end dropped, the way a shell's `$(...)` 84 # drops a trailing newline, so a comparison never turns on one 85 def chomp.lines(ls: List<&2, String>) -> List<&2, String>: 86 tail.out(tail.walk(ls)) 87 88 # the text with its trailing blank lines dropped 89 def chomp(text: String) -> String: 90 String.join(chomp.lines(String.lines(text)), "\n") 91 92 # a run's output as a test's `#|` trailer states it: no update nag, no 93 # trailing newline 94 def filter(text: String) -> String: 95 String.join(chomp.lines(go(String.lines(text))), "\n")