Files
util-linux-cal/tests/unit_tests.rs
SeCherkasov 6eb630abb8 Bump to 0.1.1: fix bugs, rewrite tests
Bug fixes:
- Fix Zeller's formula for Julian calendar (separate branch without
  century correction) and use rem_euclid for negative modulo safety
- Pass country code (cc=) to isdayoff.ru API in holiday plugin
- Sync clap --version with Cargo.toml (was hardcoded "1.0.0")

Tests:
- Rename integration_tests.rs to unit_tests.rs (they are unit tests)
- Fix race condition on env vars in plugin tests (Mutex + remove_var)
- Fix incorrect assertions (color tty detection, locale fallback)
- Add missing test cases: Julian/Gregorian Zeller, display date
  parsing, Sunday offset, week numbers, edge cases (87 tests total)
- Remove brittle version-checking tests
2026-02-19 13:51:24 +03:00

924 lines
26 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Unit tests for calendar calculation logic, formatting, and argument parsing.
use std::io::IsTerminal;
use chrono::{Datelike, Weekday};
use unicode_width::UnicodeWidthStr;
use cal::args::{Args, get_display_date};
use cal::formatter::{
format_month_grid, format_month_header, format_weekday_headers, get_weekday_order, parse_month,
};
use cal::types::{CalContext, ColumnsMode, MonthData, ReformType, WeekType};
use clap::Parser;
// ---------------------------------------------------------------------------
// Test context helpers
// ---------------------------------------------------------------------------
fn base_context() -> CalContext {
CalContext {
reform_year: ReformType::Year1752.reform_year(),
week_start: Weekday::Mon,
julian: false,
week_numbers: false,
week_type: WeekType::Iso,
color: false,
vertical: false,
today: chrono::NaiveDate::from_ymd_opt(2026, 2, 18).unwrap(),
show_year_in_header: true,
gutter_width: 2,
columns: ColumnsMode::Auto,
span: false,
#[cfg(feature = "plugins")]
holidays: false,
}
}
fn julian_context() -> CalContext {
CalContext {
reform_year: ReformType::Julian.reform_year(),
..base_context()
}
}
fn gregorian_context() -> CalContext {
CalContext {
reform_year: ReformType::Gregorian.reform_year(),
..base_context()
}
}
// ===========================================================================
// Leap year
// ===========================================================================
mod leap_year {
use super::*;
#[test]
fn gregorian_divisible_by_400() {
let ctx = gregorian_context();
assert!(ctx.is_leap_year(2000));
assert!(ctx.is_leap_year(2400));
}
#[test]
fn gregorian_divisible_by_4_not_100() {
let ctx = gregorian_context();
assert!(ctx.is_leap_year(2024));
assert!(ctx.is_leap_year(2028));
assert!(!ctx.is_leap_year(2023));
assert!(!ctx.is_leap_year(2025));
}
#[test]
fn gregorian_century_not_leap() {
let ctx = gregorian_context();
assert!(!ctx.is_leap_year(1900));
assert!(!ctx.is_leap_year(2100));
assert!(!ctx.is_leap_year(2200));
}
#[test]
fn julian_every_4th_year() {
let ctx = julian_context();
assert!(ctx.is_leap_year(2024));
assert!(ctx.is_leap_year(1900)); // Julian: 1900 IS leap
assert!(ctx.is_leap_year(100));
assert!(!ctx.is_leap_year(2023));
}
#[test]
fn year_1752_reform_boundary() {
let ctx = base_context(); // reform_year = 1752
// 1752 is before reform -> Julian rules -> divisible by 4 -> leap
assert!(ctx.is_leap_year(1752));
// 1751 not divisible by 4
assert!(!ctx.is_leap_year(1751));
}
}
// ===========================================================================
// Days in month
// ===========================================================================
mod days_in_month {
use super::*;
#[test]
fn months_with_31_days() {
let ctx = base_context();
for month in [1, 3, 5, 7, 8, 10, 12] {
assert_eq!(ctx.days_in_month(2024, month), 31, "month {month}");
}
}
#[test]
fn months_with_30_days() {
let ctx = base_context();
for month in [4, 6, 9, 11] {
assert_eq!(ctx.days_in_month(2024, month), 30, "month {month}");
}
}
#[test]
fn february_leap() {
let ctx = base_context();
assert_eq!(ctx.days_in_month(2024, 2), 29);
assert_eq!(ctx.days_in_month(2000, 2), 29);
}
#[test]
fn february_non_leap() {
let ctx = base_context();
assert_eq!(ctx.days_in_month(2023, 2), 28);
assert_eq!(ctx.days_in_month(2025, 2), 28);
}
}
// ===========================================================================
// First day of month (Zeller's congruence)
// ===========================================================================
mod first_day_of_month {
use super::*;
#[test]
fn known_gregorian_dates() {
let ctx = base_context();
assert_eq!(ctx.first_day_of_month(2024, 1), Weekday::Mon);
assert_eq!(ctx.first_day_of_month(2025, 1), Weekday::Wed);
assert_eq!(ctx.first_day_of_month(2024, 2), Weekday::Thu);
assert_eq!(ctx.first_day_of_month(2026, 2), Weekday::Sun);
assert_eq!(ctx.first_day_of_month(2000, 1), Weekday::Sat);
}
#[test]
fn september_1752_reform() {
let ctx = base_context();
assert_eq!(ctx.first_day_of_month(1752, 9), Weekday::Fri);
}
#[test]
fn julian_calendar_dates() {
let ctx = julian_context();
// Under pure Julian, 1900 is a leap year (divisible by 4).
// Julian Zeller for 1 March 1900: Monday
assert_eq!(ctx.first_day_of_month(1900, 3), Weekday::Mon);
// Julian and Gregorian agree for dates well after reform.
// Verify that Julian context still computes early dates without panic.
let _ = ctx.first_day_of_month(500, 6);
}
#[test]
fn gregorian_calendar_dates() {
let ctx = gregorian_context();
// Under pure Gregorian, 1 March 1900 is a Thursday.
let day = ctx.first_day_of_month(1900, 3);
assert_eq!(day, Weekday::Thu);
// 1 Jan 2024
assert_eq!(ctx.first_day_of_month(2024, 1), Weekday::Mon);
}
#[test]
fn january_and_february_use_previous_year_in_formula() {
let ctx = gregorian_context();
// January 2023 starts on Sunday
assert_eq!(ctx.first_day_of_month(2023, 1), Weekday::Sun);
// February 2023 starts on Wednesday
assert_eq!(ctx.first_day_of_month(2023, 2), Weekday::Wed);
}
}
// ===========================================================================
// Reform gap (September 1752)
// ===========================================================================
mod reform_gap {
use super::*;
#[test]
fn days_inside_gap() {
let ctx = base_context();
for day in 3..=13 {
assert!(
ctx.is_reform_gap(1752, 9, day),
"day {day} should be in gap"
);
}
}
#[test]
fn days_outside_gap() {
let ctx = base_context();
assert!(!ctx.is_reform_gap(1752, 9, 2));
assert!(!ctx.is_reform_gap(1752, 9, 14));
}
#[test]
fn wrong_month_or_year() {
let ctx = base_context();
assert!(!ctx.is_reform_gap(1752, 8, 5));
assert!(!ctx.is_reform_gap(1752, 10, 5));
assert!(!ctx.is_reform_gap(1751, 9, 5));
assert!(!ctx.is_reform_gap(2024, 9, 5));
}
#[test]
fn no_gap_in_pure_gregorian() {
let ctx = gregorian_context();
assert!(!ctx.is_reform_gap(1752, 9, 5));
}
#[test]
fn no_gap_in_pure_julian() {
let ctx = julian_context();
assert!(!ctx.is_reform_gap(1752, 9, 5));
}
}
// ===========================================================================
// Day of year
// ===========================================================================
mod day_of_year {
use super::*;
#[test]
fn non_leap_year() {
let ctx = base_context();
assert_eq!(ctx.day_of_year(2023, 1, 1), 1);
assert_eq!(ctx.day_of_year(2023, 1, 31), 31);
assert_eq!(ctx.day_of_year(2023, 2, 1), 32);
assert_eq!(ctx.day_of_year(2023, 12, 31), 365);
}
#[test]
fn leap_year() {
let ctx = base_context();
assert_eq!(ctx.day_of_year(2024, 1, 1), 1);
assert_eq!(ctx.day_of_year(2024, 2, 29), 60);
assert_eq!(ctx.day_of_year(2024, 3, 1), 61);
assert_eq!(ctx.day_of_year(2024, 12, 31), 366);
}
#[test]
fn reform_gap_adjustment() {
let ctx = base_context();
// Before gap
assert_eq!(ctx.day_of_year(1752, 9, 2), 235);
// After gap: 11 days removed
assert_eq!(ctx.day_of_year(1752, 9, 14), 247);
}
}
// ===========================================================================
// Week numbers
// ===========================================================================
mod week_numbers {
use super::*;
#[test]
fn iso_week_jan_1() {
let mut ctx = base_context();
ctx.week_type = WeekType::Iso;
// 2024-01-01 is Monday, ISO week 1
assert_eq!(ctx.week_number(2024, 1, 1), 1);
}
#[test]
fn iso_week_year_end() {
let mut ctx = base_context();
ctx.week_type = WeekType::Iso;
// 2024-12-30 is Monday — could be week 1 of 2025 or week 53 of 2024
let wk = ctx.week_number(2024, 12, 30);
assert!(wk == 1 || wk == 53);
}
#[test]
fn us_week_jan_1() {
let mut ctx = base_context();
ctx.week_type = WeekType::Us;
assert_eq!(ctx.week_number(2024, 1, 1), 1);
}
#[test]
fn us_week_mid_year() {
let mut ctx = base_context();
ctx.week_type = WeekType::Us;
// Sanity: week number grows through the year
let wk = ctx.week_number(2024, 7, 1);
assert!(wk > 25);
}
}
// ===========================================================================
// Weekend detection
// ===========================================================================
mod weekend {
use super::*;
#[test]
fn saturday_and_sunday_are_weekends() {
let ctx = base_context();
assert!(ctx.is_weekend(Weekday::Sat));
assert!(ctx.is_weekend(Weekday::Sun));
}
#[test]
fn weekdays_are_not_weekends() {
let ctx = base_context();
for day in [
Weekday::Mon,
Weekday::Tue,
Weekday::Wed,
Weekday::Thu,
Weekday::Fri,
] {
assert!(!ctx.is_weekend(day), "{day:?}");
}
}
}
// ===========================================================================
// MonthData construction
// ===========================================================================
mod month_data {
use super::*;
#[test]
fn january_2024_starts_monday() {
let ctx = base_context();
let m = MonthData::new(&ctx, 2024, 1);
assert_eq!(m.year, 2024);
assert_eq!(m.month, 1);
assert_eq!(m.days.len(), 42); // 6 weeks * 7
// Monday start, Jan 1 2024 is Monday -> first cell is day 1
assert_eq!(m.days[0], Some(1));
assert_eq!(m.days[30], Some(31));
assert_eq!(m.days[31], None);
}
#[test]
fn february_2024_leap_offset() {
let ctx = base_context();
let m = MonthData::new(&ctx, 2024, 2);
// Feb 2024 starts Thursday -> 3 empty cells (Mon, Tue, Wed)
assert_eq!(m.days[0], None);
assert_eq!(m.days[1], None);
assert_eq!(m.days[2], None);
assert_eq!(m.days[3], Some(1));
assert_eq!(m.days[31], Some(29));
}
#[test]
fn september_1752_reform_gap() {
let ctx = base_context();
let m = MonthData::new(&ctx, 1752, 9);
assert!(m.days.contains(&Some(1)));
assert!(m.days.contains(&Some(2)));
// Days 3-13 should be missing
for day in 3..=13 {
assert!(!m.days.contains(&Some(day)), "day {day} should be absent");
}
assert!(m.days.contains(&Some(14)));
assert!(m.days.contains(&Some(30)));
}
#[test]
fn days_and_weekdays_aligned() {
let ctx = base_context();
for month in 1..=12 {
let m = MonthData::new(&ctx, 2024, month);
for (i, day) in m.days.iter().enumerate() {
if day.is_some() {
assert!(m.weekdays[i].is_some(), "month {month}, idx {i}");
} else {
assert!(m.weekdays[i].is_none(), "month {month}, idx {i}");
}
}
}
}
#[test]
fn sunday_start_offset() {
let mut ctx = base_context();
ctx.week_start = Weekday::Sun;
// Jan 2024: starts Monday. With Sunday start, offset = 1 (Sunday empty)
let m = MonthData::new(&ctx, 2024, 1);
assert_eq!(m.days[0], None); // Sunday slot empty
assert_eq!(m.days[1], Some(1)); // Monday = day 1
}
#[test]
fn week_numbers_when_enabled() {
let mut ctx = base_context();
ctx.week_numbers = true;
let m = MonthData::new(&ctx, 2024, 1);
// First actual day should have a week number
let first_day_idx = m.days.iter().position(|d| d.is_some()).unwrap();
assert!(m.week_numbers[first_day_idx].is_some());
}
}
// ===========================================================================
// Context creation from Args
// ===========================================================================
mod context_creation {
use super::*;
#[test]
fn default_args() {
let args = Args::parse_from(["cal"]);
let ctx = CalContext::new(&args).unwrap();
assert_eq!(ctx.week_start, Weekday::Mon);
assert!(!ctx.julian);
assert!(!ctx.week_numbers);
}
#[test]
fn year_julian_week_numbers() {
let args = Args::parse_from(["cal", "-y", "-j", "-w"]);
let ctx = CalContext::new(&args).unwrap();
assert!(ctx.julian);
assert!(ctx.week_numbers);
}
#[test]
fn mutually_exclusive_display_modes() {
// -y and -n conflict
let args = Args::parse_from(["cal", "-y", "-n", "5"]);
let err = CalContext::new(&args).unwrap_err();
assert!(err.contains("mutually exclusive"));
}
#[test]
fn invalid_columns() {
let args = Args::parse_from(["cal", "-c", "0"]);
assert!(CalContext::new(&args).is_err());
let args = Args::parse_from(["cal", "-c", "abc"]);
assert!(CalContext::new(&args).is_err());
}
#[test]
fn valid_columns() {
let args = Args::parse_from(["cal", "-c", "4"]);
let ctx = CalContext::new(&args).unwrap();
match ctx.columns {
ColumnsMode::Fixed(n) => assert_eq!(n, 4),
_ => panic!("expected Fixed columns"),
}
}
#[test]
fn sunday_start() {
let args = Args::parse_from(["cal", "-s"]);
let ctx = CalContext::new(&args).unwrap();
assert_eq!(ctx.week_start, Weekday::Sun);
}
#[test]
fn color_depends_on_terminal() {
// Without --color: color = is_terminal (true in tty, false in CI)
let args = Args::parse_from(["cal"]);
let ctx = CalContext::new(&args).unwrap();
assert_eq!(ctx.color, std::io::stdout().is_terminal());
// With --color: color is always disabled
let args = Args::parse_from(["cal", "--color"]);
let ctx = CalContext::new(&args).unwrap();
assert!(!ctx.color);
}
#[test]
fn reform_gregorian() {
let args = Args::parse_from(["cal", "--reform", "gregorian"]);
let ctx = CalContext::new(&args).unwrap();
assert_eq!(ctx.reform_year, i32::MIN);
}
#[test]
fn reform_julian() {
let args = Args::parse_from(["cal", "--reform", "julian"]);
let ctx = CalContext::new(&args).unwrap();
assert_eq!(ctx.reform_year, i32::MAX);
}
#[test]
fn iso_overrides_reform() {
let args = Args::parse_from(["cal", "--iso"]);
let ctx = CalContext::new(&args).unwrap();
assert_eq!(ctx.reform_year, i32::MIN);
}
#[test]
fn vertical_mode_narrow_gutter() {
let args = Args::parse_from(["cal", "-v"]);
let ctx = CalContext::new(&args).unwrap();
assert!(ctx.vertical);
assert_eq!(ctx.gutter_width, 1);
}
#[test]
fn span_mode() {
let args = Args::parse_from(["cal", "-S", "-n", "6"]);
let ctx = CalContext::new(&args).unwrap();
assert!(ctx.span);
}
}
// ===========================================================================
// parse_month
// ===========================================================================
mod parse_month_tests {
use super::*;
#[test]
fn numeric_valid() {
for n in 1..=12 {
assert_eq!(parse_month(&n.to_string()), Some(n));
}
}
#[test]
fn numeric_invalid() {
assert_eq!(parse_month("0"), None);
assert_eq!(parse_month("13"), None);
assert_eq!(parse_month("-1"), None);
assert_eq!(parse_month("999"), None);
}
#[test]
fn english_full_names() {
let names = [
"january",
"february",
"march",
"april",
"may",
"june",
"july",
"august",
"september",
"october",
"november",
"december",
];
for (i, name) in names.iter().enumerate() {
assert_eq!(parse_month(name), Some(i as u32 + 1), "{name}");
}
}
#[test]
fn english_case_insensitive() {
assert_eq!(parse_month("January"), Some(1));
assert_eq!(parse_month("JANUARY"), Some(1));
assert_eq!(parse_month("jAnUaRy"), Some(1));
}
#[test]
fn english_abbreviations() {
let abbrevs = [
("jan", 1),
("feb", 2),
("mar", 3),
("apr", 4),
("jun", 6),
("jul", 7),
("aug", 8),
("sep", 9),
("oct", 10),
("nov", 11),
("dec", 12),
];
for (abbr, expected) in abbrevs {
assert_eq!(parse_month(abbr), Some(expected), "{abbr}");
}
}
#[test]
fn russian_names() {
let names = [
("январь", 1),
("февраль", 2),
("март", 3),
("апрель", 4),
("май", 5),
("июнь", 6),
("июль", 7),
("август", 8),
("сентябрь", 9),
("октябрь", 10),
("ноябрь", 11),
("декабрь", 12),
];
for (name, expected) in names {
assert_eq!(parse_month(name), Some(expected), "{name}");
}
}
#[test]
fn garbage_input() {
assert_eq!(parse_month("abc"), None);
assert_eq!(parse_month(""), None);
assert_eq!(parse_month("hello"), None);
}
}
// ===========================================================================
// get_display_date
// ===========================================================================
mod display_date {
use super::*;
#[test]
fn no_arguments_returns_today() {
let args = Args::parse_from(["cal"]);
let (year, month, day) = get_display_date(&args).unwrap();
let today = chrono::Local::now().date_naive();
assert_eq!(year, today.year());
assert_eq!(month, today.month());
assert_eq!(day, None);
}
#[test]
fn single_arg_four_digit_year() {
let args = Args::parse_from(["cal", "2026"]);
let (year, _month, day) = get_display_date(&args).unwrap();
assert_eq!(year, 2026);
assert_eq!(day, None);
}
#[test]
fn single_arg_month_number() {
let args = Args::parse_from(["cal", "2"]);
let (_year, month, _day) = get_display_date(&args).unwrap();
assert_eq!(month, 2);
}
#[test]
fn single_arg_month_name() {
let args = Args::parse_from(["cal", "march"]);
let (_year, month, _day) = get_display_date(&args).unwrap();
assert_eq!(month, 3);
}
#[test]
fn two_args_month_year() {
let args = Args::parse_from(["cal", "2", "2026"]);
let (year, month, day) = get_display_date(&args).unwrap();
assert_eq!(year, 2026);
assert_eq!(month, 2);
assert_eq!(day, None);
}
#[test]
fn two_args_month_name_year() {
let args = Args::parse_from(["cal", "february", "2026"]);
let (year, month, _day) = get_display_date(&args).unwrap();
assert_eq!(year, 2026);
assert_eq!(month, 2);
}
#[test]
fn three_args_day_month_year() {
let args = Args::parse_from(["cal", "15", "3", "2026"]);
let (year, month, day) = get_display_date(&args).unwrap();
assert_eq!(year, 2026);
assert_eq!(month, 3);
assert_eq!(day, Some(15));
}
#[test]
fn invalid_single_arg() {
let args = Args::parse_from(["cal", "xyz"]);
assert!(get_display_date(&args).is_err());
}
#[test]
fn invalid_month_in_two_args() {
let args = Args::parse_from(["cal", "13", "2026"]);
assert!(get_display_date(&args).is_err());
}
#[test]
fn invalid_year_range() {
let args = Args::parse_from(["cal", "1", "0"]);
assert!(get_display_date(&args).is_err());
let args = Args::parse_from(["cal", "1", "10000"]);
assert!(get_display_date(&args).is_err());
}
#[test]
fn invalid_day_range() {
let args = Args::parse_from(["cal", "0", "1", "2026"]);
assert!(get_display_date(&args).is_err());
let args = Args::parse_from(["cal", "32", "1", "2026"]);
assert!(get_display_date(&args).is_err());
}
}
// ===========================================================================
// Formatting: headers
// ===========================================================================
mod formatting {
use super::*;
#[test]
fn month_header_with_year() {
let header = format_month_header(2026, 2, 20, true, false);
assert!(header.contains("2026"));
assert_eq!(header.width(), 20);
}
#[test]
fn month_header_without_year() {
let header = format_month_header(2026, 2, 20, false, false);
assert!(!header.contains("2026"));
}
#[test]
fn month_header_color_codes() {
let colored = format_month_header(2026, 2, 20, true, true);
assert!(colored.starts_with("\x1b[96m"));
assert!(colored.ends_with("\x1b[0m"));
let plain = format_month_header(2026, 2, 20, true, false);
assert!(!plain.contains("\x1b["));
}
#[test]
fn header_width_consistent_across_months() {
for month in 1..=12 {
let h = format_month_header(2024, month, 20, true, false);
assert_eq!(h.width(), 20, "month {month}");
}
}
#[test]
fn weekday_header_monday_start() {
let ctx = base_context();
let header = format_weekday_headers(&ctx, false);
let mon_pos = header.find("Пн").unwrap();
let sun_pos = header.find("Вс").unwrap();
assert!(mon_pos < sun_pos);
}
#[test]
fn weekday_header_sunday_start() {
let mut ctx = base_context();
ctx.week_start = Weekday::Sun;
let header = format_weekday_headers(&ctx, false);
let sun_pos = header.find("Вс").unwrap();
let mon_pos = header.find("Пн").unwrap();
assert!(sun_pos < mon_pos);
}
#[test]
fn weekday_header_color() {
let mut ctx = base_context();
ctx.color = true;
let header = format_weekday_headers(&ctx, false);
assert!(header.starts_with("\x1b[93m"));
assert!(header.ends_with("\x1b[0m"));
ctx.color = false;
let header = format_weekday_headers(&ctx, false);
assert!(!header.contains("\x1b["));
}
#[test]
fn weekday_header_julian_mode_has_extra_space() {
let mut ctx = base_context();
ctx.julian = true;
let header = format_weekday_headers(&ctx, false);
assert!(header.starts_with(' '));
}
#[test]
fn weekday_order_monday_start() {
let order = get_weekday_order(Weekday::Mon);
assert_eq!(order[0], Weekday::Mon);
assert_eq!(order[6], Weekday::Sun);
}
#[test]
fn weekday_order_sunday_start() {
let order = get_weekday_order(Weekday::Sun);
assert_eq!(order[0], Weekday::Sun);
assert_eq!(order[6], Weekday::Sat);
}
}
// ===========================================================================
// Month grid formatting
// ===========================================================================
mod month_grid {
use super::*;
#[test]
fn grid_structure() {
let ctx = base_context();
let m = MonthData::new(&ctx, 2024, 1);
let grid = format_month_grid(&ctx, &m);
// Header + weekdays + up to 6 week rows = 8 lines
assert!(grid.len() >= 8 && grid.len() <= 9);
// First line: month name header
assert!(grid[0].contains("Январь"));
assert!(grid[0].contains("2024"));
// Second line: weekday names
assert!(grid[1].contains("Пн"));
}
#[test]
fn grid_contains_all_days() {
let ctx = base_context();
let m = MonthData::new(&ctx, 2024, 1);
let grid = format_month_grid(&ctx, &m);
let body: String = grid[2..].join("\n");
assert!(body.contains(" 1"));
assert!(body.contains("15"));
assert!(body.contains("31"));
}
#[test]
fn grid_february_leap() {
let ctx = base_context();
let m = MonthData::new(&ctx, 2024, 2);
let grid = format_month_grid(&ctx, &m);
let body: String = grid[2..].join("\n");
assert!(body.contains("29"));
}
#[test]
fn grid_february_non_leap() {
let ctx = base_context();
let m = MonthData::new(&ctx, 2023, 2);
let grid = format_month_grid(&ctx, &m);
let body: String = grid[2..].join("\n");
assert!(body.contains("28"));
assert!(!body.contains("29"));
}
#[test]
fn grid_day_rows_consistent_width() {
let ctx = base_context();
let m = MonthData::new(&ctx, 2024, 1);
let grid = format_month_grid(&ctx, &m);
let expected_width = grid[2].width();
for (i, line) in grid.iter().enumerate().skip(2) {
assert_eq!(line.width(), expected_width, "line {i}");
}
}
#[test]
fn grid_with_week_numbers() {
let mut ctx = base_context();
ctx.week_numbers = true;
let m = MonthData::new(&ctx, 2024, 1);
let grid = format_month_grid(&ctx, &m);
// Week number column adds 3 chars, so wider than 20
assert!(grid[2].width() > 20);
}
#[test]
fn three_months_boundary() {
let ctx = base_context();
let prev = MonthData::new(&ctx, 2023, 12);
let curr = MonthData::new(&ctx, 2024, 1);
let next = MonthData::new(&ctx, 2024, 2);
assert_eq!(prev.year, 2023);
assert_eq!(prev.month, 12);
assert_eq!(curr.year, 2024);
assert_eq!(curr.month, 1);
assert_eq!(next.year, 2024);
assert_eq!(next.month, 2);
}
}