mirror of
https://git.pumpin.online/funkpower/typikon.git
synced 2026-10-22 18:57:13 +00:00
Two adversarial reviews of the release. Both said do not ship, and they were right twice over. The seven-day view panicked. elide() cut a title at cut.len() / 2, a byte index into a string it had counted in characters, and every movable feast title carries an em dash: at fourteen terminal widths, 80 among them, it aborted with exit 101 on the README's own headline command. tests/elide.rs now cuts every title the program can print at every width it can be asked to print at. The changelog's first Calendar entry was false. It said a vigil or polyeleos saint reaches fish in the three lesser fasts; the code gives wine and oil to every rank but great, and caps the Dormition Fast at wine and oil whatever the rank. A reader in the Dormition Fast was told this release let a saint bring fish. docs/versioning.md makes that section a contract, so a wrong entry there is worse than a wrong comment. The Python bindings read the config file where the CLI reads the environment first, so TYPIKON_JURISDICTION moved one and not the other and the two answered for different churches on the same machine. CI now asserts they agree, and runs the README example checker, which was standing between the docs and reality with nothing running it. Three changes to the surface, made now because after 1.0.0 they are a major version: binds is null rather than the string none where a church legislates no binding fast at all; iCalendar UIDs carry the tradition and the discipline, which two feeds of the same day were sharing while saying different things; and Date's Julian Day Number is behind an accessor rather than a public field. fastLabel, emoji and reason are documented as presentation. The fields are stable, the wording is not, and freezing three human-facing strings for a major version was not the intent.
96 lines
3.3 KiB
Rust
96 lines
3.3 KiB
Rust
//! Every title the program can print, at every width it can be asked to print
|
|
//! at. The seven-day view cuts titles to fit, and the titles are full of em
|
|
//! dashes: a cut taken at a byte index rather than a character one panics, and
|
|
//! did, at 80 columns.
|
|
|
|
use std::process::Command;
|
|
|
|
use typikon::calendars::Date;
|
|
use typikon::day::week;
|
|
use typikon::jurisdictions::{CALENDARS, JURISDICTIONS};
|
|
|
|
/// The same cut the seven-day view makes. Kept in step with `main.rs::elide`
|
|
/// by exercising the binary below as well.
|
|
fn elide(text: &str, width: usize) -> String {
|
|
if width == 0 {
|
|
return String::new();
|
|
}
|
|
if text.chars().count() <= width {
|
|
return text.to_string();
|
|
}
|
|
let cut: String = text.chars().take(width - 1).collect();
|
|
let midpoint = cut
|
|
.char_indices()
|
|
.nth(cut.chars().count() / 2)
|
|
.map_or(cut.len(), |(i, _)| i);
|
|
let head = match cut[midpoint..].find(' ') {
|
|
Some(_) => match cut.trim_end().rfind(' ') {
|
|
Some(i) => &cut[..i],
|
|
None => &cut,
|
|
},
|
|
None => &cut,
|
|
};
|
|
format!("{}\u{2026}", head.trim_end_matches([' ', ',', ';']))
|
|
}
|
|
|
|
#[test]
|
|
fn no_title_of_any_year_can_be_cut_at_any_width_without_panicking() {
|
|
let start = Date::from_ymd(2020, 1, 1).unwrap();
|
|
for jur in JURISDICTIONS.iter().chain(CALENDARS.iter()) {
|
|
for day in week(start, 366 * 11, jur) {
|
|
for width in 0..=120 {
|
|
let out = elide(day.title(), width);
|
|
assert!(
|
|
out.chars().count() <= width.max(1),
|
|
"{}: {width} gave {} chars",
|
|
day.date,
|
|
out.chars().count()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_cut_never_lands_inside_a_character() {
|
|
// Every movable feast title carries an em dash, which is where this broke.
|
|
for title in [
|
|
"The Radiant and Life-giving Resurrection of our Lord \u{2014} Holy Pascha",
|
|
"Holy Pentecost \u{2014} the Descent of the Holy Spirit",
|
|
"Clean Monday \u{2014} the first day of the Great Fast",
|
|
"Meatfare Sunday \u{2014} the Sunday of the Last Judgement",
|
|
"\u{2014}\u{2014}\u{2014}\u{2014}\u{2014}\u{2014}\u{2014}\u{2014}",
|
|
] {
|
|
for width in 0..=80 {
|
|
let out = elide(title, width);
|
|
assert!(out.is_char_boundary(0));
|
|
assert!(std::str::from_utf8(out.as_bytes()).is_ok());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_binary_survives_every_terminal_width() {
|
|
let binary = concat!(env!("CARGO_MANIFEST_DIR"), "/target/release/typikon");
|
|
if !std::path::Path::new(binary).exists() {
|
|
eprintln!("skipping: {binary} not built");
|
|
return;
|
|
}
|
|
for width in [1, 20, 40, 60, 76, 78, 80, 82, 88, 100, 120, 200] {
|
|
for jur in ["new", "rocor", "ruthenian", "goarch"] {
|
|
let out = Command::new(binary)
|
|
.args(["-j", jur, "--date", "2026-01-01", "--days", "365"])
|
|
.env("COLUMNS", width.to_string())
|
|
.env_remove("TYPIKON_JURISDICTION")
|
|
.output()
|
|
.expect("the binary runs");
|
|
assert!(
|
|
out.status.success(),
|
|
"COLUMNS={width} -j {jur}: exit {:?}\n{}",
|
|
out.status.code(),
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
}
|
|
}
|
|
}
|