Files
typikon/tests/typikon.rs
T

628 lines
25 KiB
Rust
Raw Normal View History

2026-09-18 11:13:10 -04:00
//! Pinned against published calendars and against dates whose answer is not
//! open to interpretation.
use typikon::calendars::{Date, JulianDate, Weekday};
use typikon::day::{describe, week, year_of};
use typikon::export::to_ics;
use typikon::fasting::{Level, Obligation};
use typikon::jurisdictions::{resolve, Calendar, MenologionKind, CALENDARS, JURISDICTIONS};
use typikon::menologion::Rank;
use typikon::paschalion::{gregorian_pascha, julian_pascha, Paschalion};
use typikon::seasons::Season;
fn d(s: &str) -> Date {
Date::parse_iso(s).expect("a real date")
}
fn at(s: &str, j: &str) -> typikon::day::Day {
describe(d(s), resolve(Some(j)).expect("a real jurisdiction"))
}
// ───────────────────────────── the paschalion ─────────────────────────────
#[test]
fn julian_pascha_matches_the_published_dates() {
for (year, expected) in [
(2024, "2024-05-05"), (2025, "2025-04-20"), (2026, "2026-04-12"),
(2027, "2027-05-02"), (2028, "2028-04-16"), (2030, "2030-04-28"),
] {
assert_eq!(julian_pascha(year).iso(), expected, "julian pascha {year}");
}
}
#[test]
fn gregorian_pascha_matches_the_published_dates() {
for (year, expected) in [
(2024, "2024-03-31"), (2025, "2025-04-20"), (2026, "2026-04-05"),
(2027, "2027-03-28"), (2028, "2028-04-16"),
] {
assert_eq!(gregorian_pascha(year).iso(), expected, "gregorian pascha {year}");
}
}
#[test]
fn the_two_paschalions_agree_in_2025_and_differ_by_a_week_in_2026() {
assert_eq!(julian_pascha(2025), gregorian_pascha(2025));
assert_eq!(julian_pascha(2026) - gregorian_pascha(2026), 7);
}
#[test]
fn the_paschalion_invariants_hold_for_two_centuries() {
for year in 1900..=2100 {
let (j, g) = (julian_pascha(year), gregorian_pascha(year));
assert_eq!(j.weekday(), Weekday::Sunday, "julian {year}");
assert_eq!(g.weekday(), Weekday::Sunday, "gregorian {year}");
assert!(
Date::from_ymd(year, 4, 4).unwrap() <= j && j <= Date::from_ymd(year, 5, 8).unwrap(),
"orthodox pascha {year} out of range: {j}"
);
assert!(
Date::from_ymd(year, 3, 22).unwrap() <= g && g <= Date::from_ymd(year, 4, 25).unwrap(),
"western pascha {year} out of range: {g}"
);
}
}
// ──────────────────────────── calendar drift ──────────────────────────────
#[test]
fn every_civil_date_round_trips_through_the_julian_calendar() {
let mut day = d("1800-01-01");
let last = d("2200-12-31");
while day <= last {
let j = day.to_julian();
assert_eq!(j.to_civil(), day, "round trip {day}");
day = day + 1;
}
}
#[test]
fn julian_lags_thirteen_days_now() {
assert_eq!(d("2026-09-18").julian_offset(), 13);
let j = d("2026-09-18").to_julian();
assert_eq!((j.year, j.month, j.day), (2026, 9, 5));
}
#[test]
fn the_julian_offset_widens_the_day_after_a_julian_leap_day() {
for (before, after) in [
("1900-03-13", "1900-03-14"),
("2100-03-14", "2100-03-15"),
("2200-03-15", "2200-03-16"),
] {
assert_eq!(
d(after).julian_offset(),
d(before).julian_offset() + 1,
"offset should widen at {after}"
);
}
}
#[test]
fn old_calendar_nativity_falls_on_7_january() {
assert_eq!(
JulianDate { year: 2026, month: 12, day: 25 }.to_civil().iso(),
"2027-01-07"
);
}
#[test]
fn old_and_new_disagree_about_the_day_not_the_date() {
let old = at("2027-01-07", "rocor");
let new = at("2026-12-25", "goarch");
assert_eq!(old.church_date, "25 December");
assert_eq!(new.church_date, "25 December");
assert_eq!(old.feast, new.feast);
assert_eq!(old.rank, Some(Rank::Great));
assert_eq!(new.rank, Some(Rank::Great));
}
// ───────────────────────── the Orthodox fast ──────────────────────────────
#[test]
fn great_fast_weekdays_are_xerophagy_and_weekends_wine_and_oil() {
assert_eq!(at("2026-02-25", "new").fast, Level::Xerophagy);
assert_eq!(at("2026-02-28", "new").fast, Level::WineOil);
assert_eq!(at("2026-03-01", "new").fast, Level::WineOil);
}
#[test]
fn the_two_strictest_days_of_the_year() {
assert_eq!(at("2026-02-23", "new").fast, Level::Strict);
assert_eq!(at("2026-04-10", "new").fast, Level::Strict);
// The one Saturday kept without oil.
assert_eq!(at("2026-04-11", "new").fast, Level::Xerophagy);
}
#[test]
fn annunciation_and_palm_sunday_admit_fish_inside_the_great_fast() {
let a = at("2026-03-25", "new");
assert_eq!(a.season, Some(Season::GreatFast));
assert_eq!(a.fast, Level::Fish);
assert_eq!(at("2026-04-05", "new").fast, Level::Fish);
}
#[test]
fn the_annunciation_is_never_lost_to_great_friday() {
for (when, j) in [
("2016-03-25", "western"), ("2005-03-25", "western"),
("2029-04-07", "rocor"), ("2018-04-07", "rocor"),
] {
let day = at(when, j);
assert_eq!(day.church_date, "25 March", "{when}");
assert!(day.feast.as_deref().unwrap_or("").contains("Annunciation"), "{when}");
assert_eq!(day.rank, Some(Rank::Great), "{when}");
assert_eq!(day.fast, Level::WineOil, "{when}");
}
}
#[test]
fn kyriopascha_and_the_annunciation_in_bright_week_stay_fast_free() {
assert_eq!(at("2035-03-25", "western").fast, Level::None);
assert_eq!(at("2075-04-07", "rocor").fast, Level::None);
}
#[test]
fn palm_sunday_and_lazarus_saturday_are_still_the_fast() {
for j in ["ruthenian", "ugcc", "melkite", "goarch", "rocor"] {
for year in [2025, 2026, 2027] {
let jur = resolve(Some(j)).unwrap();
let (mv, _) = year_of(year, jur);
for when in [mv.lazarus_saturday, mv.palm_sunday] {
let day = describe(when, jur);
assert!(day.season.is_some(), "{j} {when}: no season");
assert_ne!(day.fast, Level::None, "{j} {when}");
assert!(!day.reason.is_empty(), "{j} {when}: no reason");
}
}
}
}
#[test]
fn a_ranked_saint_relaxes_a_weekday_of_a_fasting_season() {
// The Nativity of the Forerunner, a great feast inside the Apostles' Fast.
assert_eq!(at("2020-06-24", "goarch").fast, Level::Fish);
// The Forty Martyrs of Sebaste, the textbook Lenten relaxation.
assert_eq!(at("2022-03-09", "goarch").fast, Level::WineOil);
// St Nicholas, a vigil saint on a Wednesday of the Nativity Fast.
assert_eq!(at("2023-12-06", "goarch").fast, Level::WineOil);
}
#[test]
fn no_saint_brings_fish_into_the_great_fast_or_lifts_great_week() {
for day in week(d("2022-03-07"), 40, resolve(Some("goarch")).unwrap()) {
if day.church_date == "25 March" {
continue;
}
if day.season == Some(Season::GreatFast) {
assert!(day.fast >= Level::Strict, "{}", day.date);
assert!(day.fast <= Level::WineOil, "{} got {:?}", day.date, day.fast);
}
if day.season == Some(Season::GreatWeek) {
assert!(day.fast <= Level::WineOil, "{} got {:?}", day.date, day.fast);
}
}
}
#[test]
fn the_eves_relax_when_they_fall_on_a_weekend() {
assert_eq!(at("2022-12-24", "goarch").fast, Level::WineOil);
assert_eq!(at("2023-12-24", "goarch").fast, Level::WineOil);
assert_eq!(at("2025-01-05", "goarch").fast, Level::WineOil);
// A weekday eve is still xerophagy.
assert_eq!(at("2024-12-24", "goarch").fast, Level::Xerophagy);
}
#[test]
fn cheesefare_week_keeps_dairy_on_wednesday_and_friday() {
let day = at("2026-02-18", "new");
assert_eq!(day.season, Some(Season::CheesefareWeek));
assert_eq!(day.fast, Level::Dairy);
}
#[test]
fn the_fast_free_stretches() {
assert_eq!(at("2026-04-17", "new").fast, Level::None); // Bright Friday
assert_eq!(at("2026-01-02", "new").fast, Level::None); // Christmastide
assert_eq!(at("2026-06-03", "new").fast, Level::None); // after Pentecost
// Fast-free precisely because the Pharisee boasted of fasting twice a week.
assert_eq!(at("2026-02-06", "new").fast, Level::None);
}
#[test]
fn nativity_fast_tightens_for_its_last_five_days() {
assert_eq!(at("2026-12-12", "new").fast, Level::Fish);
assert_eq!(at("2026-12-19", "new").fast, Level::Fish);
assert_eq!(at("2026-12-22", "new").fast, Level::WineOil);
assert_eq!(at("2026-12-24", "new").fast, Level::Xerophagy);
}
#[test]
fn ordinary_wednesday_and_friday_are_xerophagy() {
let day = at("2026-09-04", "new");
assert!(day.season.is_none());
assert!(day.rank.is_none());
assert_eq!(day.fast, Level::Xerophagy);
}
#[test]
fn a_polyeleos_saint_lifts_an_ordinary_friday_to_wine_and_oil() {
let day = at("2026-10-09", "new");
assert_eq!(day.weekday, Weekday::Friday);
assert_eq!(day.rank, Some(Rank::Polyeleos));
assert_eq!(day.fast, Level::WineOil);
}
#[test]
fn the_apostles_fast_is_empty_when_pascha_is_late() {
let goarch: Vec<_> = week(d("2024-05-01"), 120, resolve(Some("goarch")).unwrap())
.into_iter().filter(|x| x.season == Some(Season::Apostles)).collect();
let rocor: Vec<_> = week(d("2024-05-01"), 120, resolve(Some("rocor")).unwrap())
.into_iter().filter(|x| x.season == Some(Season::Apostles)).collect();
assert!(goarch.is_empty(), "goarch 2024 should have no Apostles' Fast");
assert!(!rocor.is_empty(), "rocor 2024 should still have one");
}
#[test]
fn the_orthodox_rule_never_reports_an_obligation() {
for day in week(d("2026-01-01"), 365, resolve(Some("new")).unwrap()) {
assert_ne!(day.obligation, Some(Obligation::Obligatory), "{}", day.date);
}
}
// ──────────────────── Byzantine Catholic particular law ───────────────────
#[test]
fn ruthenian_pascha_is_the_gregorian_one() {
assert_eq!(at("2026-06-01", "ruthenian").pascha.iso(), "2026-04-05");
}
#[test]
fn ruthenian_clean_monday_is_16_february_2026() {
assert!(at("2026-02-16", "ruthenian").reason.starts_with("the first day"));
}
#[test]
fn only_the_great_fast_binds_under_ruthenian_law() {
let obligatory: Vec<_> = week(d("2026-01-01"), 365, resolve(Some("ruthenian")).unwrap())
.into_iter().filter(|x| x.is_obligatory()).collect();
let dates: Vec<String> = obligatory.iter().map(|x| x.date.iso()).collect();
assert!(dates.contains(&"2026-02-16".to_string()));
assert!(dates.contains(&"2026-04-03".to_string()));
for day in &obligatory {
assert!(
matches!(day.season, Some(Season::GreatFast) | Some(Season::GreatWeek)),
"{} binds outside the Fast", day.date
);
}
}
#[test]
fn the_voluntary_seasons_never_report_as_binding() {
for when in ["2026-11-20", "2026-08-05"] {
assert_eq!(at(when, "ruthenian").obligation, Some(Obligation::Customary));
}
}
#[test]
fn christmas_on_a_friday_does_not_ask_you_to_abstain() {
let day = at("2026-12-25", "ruthenian");
assert_eq!(day.weekday, Weekday::Friday);
assert_eq!(day.fast, Level::None);
}
#[test]
fn ruthenian_strict_days_bind_strictly_and_the_rest_bind_to_meat() {
assert_eq!(at("2026-02-16", "ruthenian").binds, Level::WineOil);
assert_eq!(at("2026-04-03", "ruthenian").binds, Level::WineOil);
assert_eq!(at("2026-02-18", "ruthenian").binds, Level::Dairy);
assert_eq!(at("2026-10-09", "ruthenian").binds, Level::None);
}
// ──────────────────────────── the menologion ──────────────────────────────
#[test]
fn every_day_of_a_leap_year_has_a_commemoration() {
for day in week(d("2024-01-01"), 366, resolve(Some("new")).unwrap()) {
assert!(!day.commemorations.is_empty(), "{}", day.date);
}
}
#[test]
fn the_great_feasts_are_all_present() {
for (month, day) in [(9, 8), (9, 14), (11, 21), (12, 25), (1, 6), (2, 2), (3, 25), (8, 6), (8, 15)] {
let x = describe(Date::from_ymd(2026, month, day).unwrap(), resolve(Some("goarch")).unwrap());
assert_eq!(x.rank, Some(Rank::Great), "{month}-{day}");
}
for when in ["2026-04-05", "2026-05-21", "2026-05-31"] {
assert_eq!(at(when, "goarch").rank, Some(Rank::Great), "{when}");
}
}
#[test]
fn a_movable_feast_outranks_the_fixed_calendar() {
assert!(at("2026-04-12", "new").feast.as_deref().unwrap_or("").contains("Pascha"));
}
// ──────────────────────────────── output ──────────────────────────────────
#[test]
fn ics_content_lines_stay_within_seventy_five_octets() {
for j in ["goarch", "rocor", "ruthenian"] {
let days = week(d("2026-01-01"), 365, resolve(Some(j)).unwrap());
let text = to_ics(&days, "Typikon", "20260101T000000Z");
for line in text.split("\r\n") {
assert!(line.len() <= 75, "{j}: {} octets: {line}", line.len());
}
}
}
#[test]
fn ics_is_well_formed() {
let days = week(d("2026-04-10"), 7, resolve(Some("new")).unwrap());
let text = to_ics(&days, "Typikon", "20260101T000000Z");
assert!(text.starts_with("BEGIN:VCALENDAR"));
assert!(text.trim_end().ends_with("END:VCALENDAR"));
assert_eq!(text.matches("BEGIN:VEVENT").count(), text.matches("END:VEVENT").count());
}
// ─────────────────────────── every jurisdiction ───────────────────────────
#[test]
fn every_jurisdiction_holds_together_for_twenty_years() {
for jur in JURISDICTIONS.iter().chain(CALENDARS.iter()) {
for day in week(d("2020-01-01"), 366 * 21 / 4, jur) {
assert!(!day.commemorations.is_empty(), "{} {}", jur.key, day.date);
assert!(!day.title().is_empty(), "{} {}", jur.key, day.date);
// `binds` may never be stricter than the level it sits under.
assert!(day.binds >= day.fast, "{} {}: binds {:?} > fast {:?}",
jur.key, day.date, day.binds, day.fast);
}
}
}
#[test]
fn a_calendar_name_resolves_without_naming_a_church() {
assert_eq!(resolve(Some("old")).unwrap().menologion, MenologionKind::Julian);
assert_eq!(resolve(Some("new")).unwrap().menologion, MenologionKind::RevisedJulian);
assert_eq!(resolve(Some("western")).unwrap().paschalion, Paschalion::Gregorian);
assert_eq!(resolve(None).unwrap().key, "new");
assert!(resolve(Some("atlantis")).is_err());
assert_eq!(resolve(Some("rocor")).unwrap().calendar(), Calendar::Old);
}
// ─────────────────────── the levels are an ordered set ────────────────────
#[test]
fn a_feast_can_only_relax_a_fast_never_tighten_it() {
// Strictest to laxest, so `<` means stricter.
assert!(Level::Strict < Level::Xerophagy);
assert!(Level::Xerophagy < Level::WineOil);
assert!(Level::Dairy < Level::None);
// Relaxing to fish inside the Great Fast stops at wine and oil.
assert_eq!(Level::Xerophagy.relaxed_to(Level::Fish, Level::WineOil), Level::WineOil);
// A cap stricter than the day never tightens the day.
assert_eq!(Level::Fish.relaxed_to(Level::WineOil, Level::WineOil), Level::Fish);
// No rank means no change.
assert_eq!(Level::Xerophagy.relaxed_to(Level::Xerophagy, Level::Fish), Level::Xerophagy);
}
2026-09-18 11:15:22 -04:00
// ───────────────────────── the delimited formats ──────────────────────────
#[test]
fn csv_has_a_header_and_one_row_per_day() {
use typikon::export::to_delimited;
let days = week(d("2026-01-01"), 5, resolve(Some("rocor")).unwrap());
let text = to_delimited(&days, ',', &[]);
let lines: Vec<&str> = text.trim_end().split('\n').collect();
assert_eq!(lines.len(), 6, "header plus five days");
assert!(lines[0].starts_with("date,weekday,"));
assert!(lines[1].starts_with("2026-01-01,Thursday,rocor,old,"));
}
#[test]
fn csv_quotes_the_fields_that_need_it() {
use typikon::export::to_delimited;
// Commemorations are full of commas, and joined with semicolons besides.
let days = week(d("2026-09-18"), 1, resolve(Some("rocor")).unwrap());
let text = to_delimited(&days, ',', &["title".to_string()]);
let row = text.trim_end().split('\n').nth(1).unwrap();
assert!(row.starts_with('"'), "a title with a comma must be quoted: {row}");
assert!(row.ends_with('"'));
// A quote inside a field doubles rather than escaping.
assert_eq!(row.matches("\"\"").count(), 0);
}
#[test]
fn tsv_uses_tabs_and_a_chosen_column_order() {
use typikon::export::to_delimited;
let days = week(d("2026-04-12"), 1, resolve(Some("new")).unwrap());
let cols = vec!["fast".to_string(), "date".to_string()];
let text = to_delimited(&days, '\t', &cols);
let lines: Vec<&str> = text.trim_end().split('\n').collect();
assert_eq!(lines[0], "fast\tdate");
assert_eq!(lines[1], "none\t2026-04-12");
}
#[test]
fn an_unknown_column_is_caught_before_anything_is_printed() {
use typikon::export::csv::unknown_column;
assert!(unknown_column(&["date".to_string(), "fast".to_string()]).is_none());
assert_eq!(
unknown_column(&["date".to_string(), "nonsense".to_string()]).map(String::as_str),
Some("nonsense")
);
}
#[test]
fn every_advertised_column_actually_renders() {
use typikon::export::{csv::COLUMNS, to_delimited};
let days = week(d("2026-02-16"), 1, resolve(Some("ruthenian")).unwrap());
let cols: Vec<String> = COLUMNS.iter().map(|c| c.to_string()).collect();
let text = to_delimited(&days, '\t', &cols);
let lines: Vec<&str> = text.trim_end().split('\n').collect();
// Every column produces a cell, even the empty ones.
assert_eq!(lines[0].split('\t').count(), COLUMNS.len());
assert_eq!(lines[1].split('\t').count(), COLUMNS.len());
}
// ───────────────────────── Greek and Slavic usage ─────────────────────────
#[test]
fn the_protection_is_a_great_feast_in_slavic_usage_only() {
use typikon::jurisdictions::{Jurisdiction, Tradition};
let base = *resolve(Some("goarch")).unwrap();
let slavic: &'static Jurisdiction =
Box::leak(Box::new(Jurisdiction { tradition: Tradition::Slavic, ..base }));
let greek: &'static Jurisdiction =
Box::leak(Box::new(Jurisdiction { tradition: Tradition::Greek, ..base }));
assert_eq!(describe(d("2026-10-01"), slavic).rank, Some(Rank::Great));
assert_eq!(describe(d("2026-10-01"), greek).rank, Some(Rank::Vigil));
}
#[test]
fn greek_usage_admits_fish_in_the_nativity_fast_where_slavic_does_not() {
// A Tuesday before 17 December.
assert_eq!(at("2026-12-01", "rocor").fast, Level::WineOil);
assert_eq!(at("2026-12-01", "goarch").fast, Level::Fish);
// Wednesday is a fast day in both.
assert!(at("2026-12-02", "goarch").fast <= Level::WineOil);
// And after the 17th the Greek relaxation stops.
assert_eq!(at("2026-12-22", "goarch").fast, Level::WineOil);
}
#[test]
fn a_jurisdiction_carries_the_usage_its_books_follow() {
use typikon::jurisdictions::Tradition;
for key in ["moscow", "rocor", "serbian", "oca", "ruthenian", "ukrainian"] {
assert_eq!(resolve(Some(key)).unwrap().tradition, Tradition::Slavic, "{key}");
}
for key in ["goarch", "greece", "constantinople", "antiochian", "cyprus"] {
assert_eq!(resolve(Some(key)).unwrap().tradition, Tradition::Greek, "{key}");
}
}
#[test]
fn every_rank_has_a_mark_and_they_are_distinct() {
let signs: Vec<&str> = [Rank::Great, Rank::Vigil, Rank::Polyeleos]
.iter().map(|r| r.sign()).collect();
assert!(signs.iter().all(|s| !s.is_empty()));
assert_eq!(signs.iter().collect::<std::collections::HashSet<_>>().len(), 3);
}
#[test]
fn tradition_parses_the_names_people_use() {
use typikon::jurisdictions::Tradition;
assert_eq!(Tradition::parse("Slavic"), Some(Tradition::Slavic));
assert_eq!(Tradition::parse("russian"), Some(Tradition::Slavic));
assert_eq!(Tradition::parse("GREEK"), Some(Tradition::Greek));
assert_eq!(Tradition::parse("coptic"), None);
}
// ─────────────── the law that binds vs the discipline shown ───────────────
fn ruthenian_at_typikon() -> &'static typikon::jurisdictions::Jurisdiction {
use typikon::jurisdictions::{Jurisdiction, RuleKind};
let base = *resolve(Some("ruthenian")).unwrap();
Box::leak(Box::new(Jurisdiction { discipline: Some(RuleKind::Orthodox), ..base }))
}
#[test]
fn the_typikon_discipline_can_be_shown_without_losing_the_obligation() {
let jur = ruthenian_at_typikon();
// Clean Monday: the Typikon's total fast, still binding under particular law.
let clean = describe(d("2026-02-16"), jur);
assert_eq!(clean.fast, Level::Strict);
assert_eq!(clean.obligation, Some(Obligation::Obligatory));
assert_eq!(clean.binds, Level::WineOil);
// A Wednesday of the Great Fast: xerophagy kept, meat alone binding.
let wed = describe(d("2026-02-18"), jur);
assert_eq!(wed.fast, Level::Xerophagy);
assert_eq!(wed.obligation, Some(Obligation::Obligatory));
assert_eq!(wed.binds, Level::Dairy);
// A Thursday of the Great Fast: kept, and binding on no one.
let thu = describe(d("2026-02-19"), jur);
assert_eq!(thu.fast, Level::Xerophagy);
assert_eq!(thu.obligation, Some(Obligation::Customary));
assert_eq!(thu.binds, Level::None);
}
#[test]
fn showing_the_typikon_never_invents_an_obligation() {
let jur = ruthenian_at_typikon();
let native = resolve(Some("ruthenian")).unwrap();
for n in 0..365 {
let day = describe(d("2026-01-01") + n, jur);
let plain = describe(d("2026-01-01") + n, native);
// Exactly the same days bind, whichever discipline is displayed.
assert_eq!(day.is_obligatory(), plain.is_obligatory(), "{}", day.date);
assert_eq!(day.binds, plain.binds, "{}", day.date);
}
}
#[test]
fn the_typikon_discipline_is_never_laxer_than_the_law_it_reports() {
let jur = ruthenian_at_typikon();
for n in 0..365 {
let day = describe(d("2026-01-01") + n, jur);
assert!(day.binds >= day.fast, "{}: binds {:?} stricter than kept {:?}",
day.date, day.binds, day.fast);
}
}
#[test]
fn the_override_changes_nothing_for_a_jurisdiction_already_on_the_typikon() {
use typikon::jurisdictions::{Jurisdiction, RuleKind};
let base = *resolve(Some("rocor")).unwrap();
let forced: &'static Jurisdiction =
Box::leak(Box::new(Jurisdiction { discipline: Some(RuleKind::Orthodox), ..base }));
for n in 0..200 {
let a = describe(d("2026-01-01") + n, resolve(Some("rocor")).unwrap());
let b = describe(d("2026-01-01") + n, forced);
assert_eq!(a.fast, b.fast, "{}", a.date);
assert_eq!(a.obligation, b.obligation, "{}", a.date);
}
}
2026-09-18 12:29:57 -04:00
// ────────────────────────── what gets remembered ──────────────────────────
#[test]
fn the_config_round_trips_all_three_axes() {
use typikon::config::{self, Saved};
// A scratch XDG_CONFIG_HOME, so the test cannot touch a real one.
let dir = std::env::temp_dir().join(format!("typikon-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
// SAFETY: single-threaded test, and the variable is restored below.
let prev = std::env::var_os("XDG_CONFIG_HOME");
unsafe { std::env::set_var("XDG_CONFIG_HOME", &dir) };
config::write(&Saved {
jurisdiction: Some("ruthenian".into()),
tradition: Some("slavic".into()),
typikon: true,
})
.unwrap();
let back = config::saved();
assert_eq!(back.jurisdiction.as_deref(), Some("ruthenian"));
assert_eq!(back.tradition.as_deref(), Some("slavic"));
assert!(back.typikon);
// Remembering only a jurisdiction leaves the other two unset, rather than
// inventing a default that would disagree with the flags.
config::write(&Saved { jurisdiction: Some("rocor".into()), ..Default::default() }).unwrap();
let plain = config::saved();
assert_eq!(plain.jurisdiction.as_deref(), Some("rocor"));
assert_eq!(plain.tradition, None);
assert!(!plain.typikon);
unsafe {
match prev {
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
None => std::env::remove_var("XDG_CONFIG_HOME"),
}
}
let _ = std::fs::remove_dir_all(&dir);
}