//! Forward fence, the hand-written stylesheet ratchet. //! //! Every page ships both frontends: the hand-written sheets and the sheets //! makeover-build generates. The hand-written half dwarfs the generated one and //! is paid on every page load by every visitor, signed-out ones included, which //! is the largest standing cost of the conversion. //! //! # Parked //! //! The ratchet below is `#[ignore]`d until the conversion is finished and the //! duplicated CSS is gone. See the test's own doc comment for what turns it back //! on. `the_two_lists_agree` is unaffected and still runs. //! //! # Why this seal exists rather than a note saying the same thing //! //! The conversion's payoff is the hand-written sheet shrinking. Without a //! number, "we converted a screen" and "we removed its cost" are the same //! claim. A seal makes the second claim checkable, the same way //! `frontend_globals` makes the script half //! checkable and `DEAD_VOCABULARY_HIGH_WATER` (in `build.rs`) makes the //! generated half checkable. This is the third of the three. //! //! # Kibibytes rather than bytes //! //! A byte-exact seal on a hand-edited file fails on whitespace, and a guard //! that fires on a reflow is a guard people learn to edit rather than read. A //! KiB is coarse enough that ordinary editing is free and fine enough that a //! deleted rule block shows up. //! //! # One-sided, and which side //! //! Growing fails. Under the feature freeze, adding to a hand-written sheet //! wants an argument, and the argument belongs in the commit that raises the //! number rather than nowhere. //! //! Shrinking fails too, asking for the seal to be lowered. That is the half //! that makes it a ratchet rather than a ceiling: a conversion batch that //! deletes a screen's CSS and leaves the seal where it was has left the next //! batch room to grow back into. //! //! `check_vocabulary_use` deliberately only warns on the way down, and the //! reasoning does not carry here: dead vocabulary falls as a side effect of //! work aimed elsewhere, and this number falls only when somebody deleted CSS //! on purpose. Somebody who did that can lower a constant. use std::fs; /// Total hand-written CSS, in whole kibibytes, that this repo may ship. /// /// Nothing about this number is a target: it is the record of where the /// conversion started, and it is what the finished conversion gets measured /// against. /// /// Lower it whenever the total falls. Raising it is a decision, not a fix. const CSS_KIB_HIGH_WATER: usize = 337; /// The sheets this repo writes by hand. /// /// Kept in step with `HAND_WRITTEN_CSS` in `build.rs`, which is the list the /// breakpoint and vocabulary guards read. A sheet in one list and not the other /// is a sheet that can take bytes from a sealed neighbour and read as a /// deletion, so `the_two_lists_agree` below holds them together. /// /// `layout.css`, `geometry.css` and `embed-geometry.css` are excluded because /// they are generated: their size is makeover's answer, and shrinking them is /// not this repo's work to do. const SHEETS: [&str; 4] = [ "static/style.css", "static/wizard.css", "static/media-player.css", "static/no-js.css", ]; /// Total bytes across [`SHEETS`], and the per-sheet breakdown for the message. fn measure() -> (usize, Vec<(&'static str, usize)>) { let each: Vec<(&str, usize)> = SHEETS .iter() .map(|path| { let bytes = fs::read(path) .unwrap_or_else(|e| panic!("read {path}: {e}")) .len(); (*path, bytes) }) .collect(); (each.iter().map(|(_, bytes)| bytes).sum(), each) } /// Parked, and the module doc above argues for enforcing it, so the argument /// gets answered rather than ignored. /// /// What the doc says: a guard people edit rather than read is not a guard, and /// without a number "we converted a screen" and "we removed its cost" are the /// same claim. Both points stand. What they do not survive is the assertion /// pair below: this test fails on any decrease as well as any increase, so mid /// conversion it fires on work going in the direction it exists to reward, and /// the number gets re-baselined every batch. A seal that is re-baselined on /// schedule measures nothing, and it teaches exactly the editing habit the doc /// warns about. /// /// So the number is kept and the enforcement is suspended, rather than the seal /// being quietly lowered batch by batch. /// /// WHAT BRINGS IT BACK: the conversion complete and the duplicated CSS removed. /// At that point the number stops moving in both directions, and the decision /// deferred with it becomes worth taking: seal the minified size rather than raw /// bytes. `lightningcss` is already a dependency of this repo, so that costs no /// new dependency. #[test] #[ignore = "parked until the conversion is finished and the duplicated CSS is gone: see this test's doc comment"] fn hand_written_css_does_not_grow() { let (total, each) = measure(); let kib = total / 1024; let breakdown = each .iter() .map(|(path, bytes)| format!(" {path}: {bytes}")) .collect::>() .join("\n"); assert!( kib <= CSS_KIB_HIGH_WATER, "hand-written CSS grew to {kib} KiB ({total} bytes), above the sealed \ {CSS_KIB_HIGH_WATER}:\n{breakdown}\n\n\ The conversion is supposed to move this number down. If a rule really \ belongs in a hand-written sheet rather than in makeover-webview, raise \ CSS_KIB_HIGH_WATER to {kib} and say why in the same commit." ); assert_eq!( kib, CSS_KIB_HIGH_WATER, "hand-written CSS fell to {kib} KiB ({total} bytes). Lower \ CSS_KIB_HIGH_WATER to {kib} so it cannot grow back:\n{breakdown}" ); } #[test] fn the_two_lists_agree() { // `build.rs` is not a module this test can import, so the list is compared // as text. Crude, and it is the only thing standing between the two lists // drifting apart, which would let bytes move from a weighed sheet to an // unweighed one and read as a deletion. let build = fs::read_to_string("build.rs").expect("read build.rs"); let (start, _) = build .split_once("const HAND_WRITTEN_CSS") .expect("build.rs declares HAND_WRITTEN_CSS"); // On `= [` rather than `[`, because the declaration's first bracket is the // type annotation (`: [&str; 4]`) and matching that reads the length back // as the list. let declared = &build[start.len()..]; let list = declared .split_once("= [") .and_then(|(_, rest)| rest.split_once(']')) .map(|(inner, _)| inner) .expect("HAND_WRITTEN_CSS is an array literal"); for sheet in SHEETS { assert!( list.contains(sheet), "{sheet} is weighed by this seal but is not in build.rs's \ HAND_WRITTEN_CSS, so it is not breakpoint- or vocabulary-checked." ); } let in_build = list.matches("static/").count(); assert_eq!( in_build, SHEETS.len(), "build.rs's HAND_WRITTEN_CSS lists {in_build} sheets and this seal \ weighs {}. A sheet in one list and not the other can take bytes from \ a sealed neighbour and read as a deletion.", SHEETS.len() ); }