mirror of
https://github.com/SeCherkasov/util-linux-cal.git
synced 2026-03-31 00:11:47 +03:00
Compare commits
3 Commits
0bea3004b4
...
bugfix/mtr
| Author | SHA1 | Date | |
|---|---|---|---|
| 6eb630abb8 | |||
| 2cadb74fd2 | |||
| 2ff92f6a52 |
333
Cargo.lock
generated
333
Cargo.lock
generated
@@ -126,9 +126,15 @@ version = "3.19.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "cal"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"assert_cmd",
|
||||
"chrono",
|
||||
@@ -259,17 +265,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
@@ -305,15 +300,6 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
@@ -333,13 +319,29 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "holiday_highlighter"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"libc",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
@@ -364,114 +366,18 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||
dependencies = [
|
||||
"idna_adapter",
|
||||
"smallvec",
|
||||
"utf8_iter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna_adapter"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
|
||||
dependencies = [
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.85"
|
||||
@@ -490,9 +396,9 @@ checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.9"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
||||
checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-link",
|
||||
@@ -514,12 +420,6 @@ version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
@@ -581,15 +481,6 @@ version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "predicates"
|
||||
version = "3.1.4"
|
||||
@@ -802,18 +693,6 @@ version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@@ -837,17 +716,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.4.3"
|
||||
@@ -884,16 +752,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
@@ -902,9 +760,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
@@ -914,37 +772,38 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "2.12.1"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
|
||||
checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"flate2",
|
||||
"log",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"url",
|
||||
"webpki-roots 0.26.11",
|
||||
"ureq-proto",
|
||||
"utf-8",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
name = "ureq-proto"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
|
||||
checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"base64",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
@@ -1012,15 +871,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||
dependencies = [
|
||||
"webpki-roots 1.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.6"
|
||||
@@ -1245,91 +1095,8 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cal"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2024"
|
||||
description = "Calendar display utility"
|
||||
authors = ["Se.Cherkasov@yahoo.com"]
|
||||
@@ -9,9 +9,9 @@ authors = ["Se.Cherkasov@yahoo.com"]
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.43", features = ["unstable-locales"] }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
unicode-width = "0.1"
|
||||
unicode-width = "0.2"
|
||||
terminal_size = "0.4"
|
||||
libloading = { version = "0.8", optional = true }
|
||||
libloading = { version = "0.9", optional = true }
|
||||
shellexpand = { version = "3.1", optional = true }
|
||||
libc = { version = "0.2", optional = true }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "holiday_highlighter"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2024"
|
||||
description = "Holiday highlighting plugin for cal using isdayoff.ru API"
|
||||
|
||||
@@ -10,5 +10,5 @@ name = "holiday_highlighter"
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.43", features = ["unstable-locales"] }
|
||||
ureq = "2.12"
|
||||
ureq = "3.2"
|
||||
libc = "0.2"
|
||||
|
||||
@@ -157,21 +157,32 @@ pub unsafe extern "C" fn plugin_get_year_holidays(
|
||||
}
|
||||
|
||||
/// Fetch holiday data for a month from isdayoff.ru API.
|
||||
fn fetch_holidays(year: i32, month: u32, _country: &str) -> Option<String> {
|
||||
let url = format!("{}?year={}&month={:02}&pre=1", API_URL_MONTH, year, month);
|
||||
fn fetch_holidays(year: i32, month: u32, country: &str) -> Option<String> {
|
||||
let url = format!(
|
||||
"{}?year={}&month={:02}&cc={}&pre=1",
|
||||
API_URL_MONTH,
|
||||
year,
|
||||
month,
|
||||
country.to_lowercase()
|
||||
);
|
||||
|
||||
match ureq::get(&url).call() {
|
||||
Ok(response) => response.into_string().ok(),
|
||||
Ok(response) => response.into_body().read_to_string().ok(),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch holiday data for entire year from isdayoff.ru API.
|
||||
pub fn fetch_holidays_year(year: i32, _country: &str) -> Option<String> {
|
||||
let url = format!("{}?year={}&pre=1", API_URL_YEAR, year);
|
||||
pub fn fetch_holidays_year(year: i32, country: &str) -> Option<String> {
|
||||
let url = format!(
|
||||
"{}?year={}&cc={}&pre=1",
|
||||
API_URL_YEAR,
|
||||
year,
|
||||
country.to_lowercase()
|
||||
);
|
||||
|
||||
match ureq::get(&url).call() {
|
||||
Ok(response) => response.into_string().ok(),
|
||||
Ok(response) => response.into_body().read_to_string().ok(),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
//! Integration tests for holiday_highlighter plugin.
|
||||
|
||||
use holiday_highlighter::{
|
||||
PLUGIN_NAME, PLUGIN_VERSION, get_country_from_locale, plugin_get_name, plugin_get_version,
|
||||
};
|
||||
use std::ffi::CStr;
|
||||
|
||||
#[test]
|
||||
fn test_get_country_from_locale_ru() {
|
||||
unsafe {
|
||||
std::env::set_var("LC_ALL", "ru_RU.UTF-8");
|
||||
}
|
||||
assert_eq!(get_country_from_locale(), "RU");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_country_from_locale_us() {
|
||||
unsafe {
|
||||
std::env::set_var("LC_ALL", "en_US.UTF-8");
|
||||
}
|
||||
assert_eq!(get_country_from_locale(), "US");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_country_from_locale_by() {
|
||||
unsafe {
|
||||
std::env::set_var("LC_ALL", "be_BY.UTF-8");
|
||||
}
|
||||
assert_eq!(get_country_from_locale(), "BY");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_country_from_locale_fallback() {
|
||||
unsafe {
|
||||
std::env::set_var("LC_ALL", "");
|
||||
std::env::set_var("LC_TIME", "");
|
||||
std::env::set_var("LANG", "");
|
||||
}
|
||||
assert_eq!(get_country_from_locale(), "RU");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plugin_metadata_from_cargo() {
|
||||
assert_eq!(PLUGIN_NAME, "holiday_highlighter");
|
||||
assert_eq!(PLUGIN_VERSION, "0.1.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plugin_get_name_version() {
|
||||
unsafe {
|
||||
let name = CStr::from_ptr(plugin_get_name()).to_str().unwrap();
|
||||
let version = CStr::from_ptr(plugin_get_version()).to_str().unwrap();
|
||||
assert_eq!(name, "holiday_highlighter");
|
||||
assert_eq!(version, "0.1.0");
|
||||
}
|
||||
}
|
||||
80
plugins/holiday_highlighter/tests/unit_tests.rs
Normal file
80
plugins/holiday_highlighter/tests/unit_tests.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
//! Unit tests for holiday_highlighter plugin.
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use holiday_highlighter::get_country_from_locale;
|
||||
|
||||
/// Mutex to serialize tests that modify environment variables.
|
||||
/// `set_var` is not thread-safe, so locale tests must not run in parallel.
|
||||
/// We use `lock().unwrap_or_else(|e| e.into_inner())` to recover from poison.
|
||||
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn lock_env() -> std::sync::MutexGuard<'static, ()> {
|
||||
ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// Reset all locale env vars to a clean state, then set `LC_ALL` to the given value.
|
||||
fn set_locale(lc_all: &str) {
|
||||
unsafe {
|
||||
std::env::set_var("LC_ALL", lc_all);
|
||||
std::env::remove_var("LC_TIME");
|
||||
std::env::remove_var("LANG");
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove all locale env vars.
|
||||
fn clear_locale() {
|
||||
unsafe {
|
||||
std::env::remove_var("LC_ALL");
|
||||
std::env::remove_var("LC_TIME");
|
||||
std::env::remove_var("LANG");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Country detection from locale
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn country_from_locale_ru() {
|
||||
let _guard = lock_env();
|
||||
set_locale("ru_RU.UTF-8");
|
||||
assert_eq!(get_country_from_locale(), "RU");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn country_from_locale_us() {
|
||||
let _guard = lock_env();
|
||||
set_locale("en_US.UTF-8");
|
||||
assert_eq!(get_country_from_locale(), "US");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn country_from_locale_by() {
|
||||
let _guard = lock_env();
|
||||
set_locale("be_BY.UTF-8");
|
||||
assert_eq!(get_country_from_locale(), "BY");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn country_from_locale_kz() {
|
||||
let _guard = lock_env();
|
||||
set_locale("kk_KZ.UTF-8");
|
||||
assert_eq!(get_country_from_locale(), "KZ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn country_from_locale_fallback_to_us() {
|
||||
let _guard = lock_env();
|
||||
clear_locale();
|
||||
// When no locale vars are set, the function defaults to "en_US.UTF-8" -> "US"
|
||||
assert_eq!(get_country_from_locale(), "US");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn country_from_locale_lc_time_fallback() {
|
||||
let _guard = lock_env();
|
||||
clear_locale();
|
||||
unsafe { std::env::set_var("LC_TIME", "tr_TR.UTF-8") };
|
||||
assert_eq!(get_country_from_locale(), "TR");
|
||||
}
|
||||
@@ -13,7 +13,7 @@ use crate::types::{
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "cal")]
|
||||
#[command(about = "Displays calendar for specified month or year", long_about = None)]
|
||||
#[command(version = "1.0.0")]
|
||||
#[command(version)]
|
||||
#[command(after_help = HELP_MESSAGE)]
|
||||
pub struct Args {
|
||||
/// Week starts on Sunday (default is Monday).
|
||||
|
||||
@@ -47,7 +47,13 @@ impl CalContext {
|
||||
let k: i32 = year_i % 100;
|
||||
let j: i32 = year_i / 100;
|
||||
|
||||
let h = (q + (13 * (m as i32 + 1)) / 5 + k + k / 4 + j / 4 - 2 * j) % 7;
|
||||
let h = if year < self.reform_year {
|
||||
// Julian calendar: no century correction
|
||||
(q + (13 * (m as i32 + 1)) / 5 + k + k / 4 + 5).rem_euclid(7)
|
||||
} else {
|
||||
// Gregorian calendar
|
||||
(q + (13 * (m as i32 + 1)) / 5 + k + k / 4 + j / 4 - 2 * j).rem_euclid(7)
|
||||
};
|
||||
// h: 0=Sat, 1=Sun, 2=Mon, 3=Tue, 4=Wed, 5=Thu, 6=Fri
|
||||
match h {
|
||||
0 => Weekday::Sat,
|
||||
|
||||
@@ -1,700 +0,0 @@
|
||||
//! Integration tests for calendar calculation logic.
|
||||
|
||||
use chrono::Weekday;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use cal::formatter::parse_month;
|
||||
use cal::types::{CalContext, ColumnsMode, MonthData, ReformType, WeekType};
|
||||
|
||||
fn test_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(),
|
||||
..test_context()
|
||||
}
|
||||
}
|
||||
|
||||
fn gregorian_context() -> CalContext {
|
||||
CalContext {
|
||||
reform_year: ReformType::Gregorian.reform_year(),
|
||||
..test_context()
|
||||
}
|
||||
}
|
||||
|
||||
mod leap_year_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_gregorian_leap_year_divisible_by_400() {
|
||||
let ctx = gregorian_context();
|
||||
assert!(ctx.is_leap_year(2000));
|
||||
assert!(ctx.is_leap_year(2400));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gregorian_leap_year_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 test_gregorian_not_leap_year_divisible_by_100() {
|
||||
let ctx = gregorian_context();
|
||||
assert!(!ctx.is_leap_year(1900));
|
||||
assert!(!ctx.is_leap_year(2100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_julian_leap_year() {
|
||||
let ctx = julian_context();
|
||||
assert!(ctx.is_leap_year(2024));
|
||||
assert!(ctx.is_leap_year(2028));
|
||||
assert!(ctx.is_leap_year(1900));
|
||||
assert!(!ctx.is_leap_year(2023));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_year_1752_leap_year() {
|
||||
let ctx = test_context();
|
||||
assert!(ctx.is_leap_year(1752));
|
||||
}
|
||||
}
|
||||
|
||||
mod days_in_month_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_31_day_months() {
|
||||
let ctx = test_context();
|
||||
for month in [1, 3, 5, 7, 8, 10, 12] {
|
||||
assert_eq!(ctx.days_in_month(2024, month), 31);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_30_day_months() {
|
||||
let ctx = test_context();
|
||||
for month in [4, 6, 9, 11] {
|
||||
assert_eq!(ctx.days_in_month(2024, month), 30);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_february_leap_year() {
|
||||
let ctx = test_context();
|
||||
assert_eq!(ctx.days_in_month(2024, 2), 29);
|
||||
assert_eq!(ctx.days_in_month(2028, 2), 29);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_february_non_leap_year() {
|
||||
let ctx = test_context();
|
||||
assert_eq!(ctx.days_in_month(2023, 2), 28);
|
||||
assert_eq!(ctx.days_in_month(2025, 2), 28);
|
||||
}
|
||||
}
|
||||
|
||||
mod first_day_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_first_day_known_dates() {
|
||||
let ctx = test_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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first_day_september_1752() {
|
||||
let ctx = test_context();
|
||||
assert_eq!(ctx.first_day_of_month(1752, 9), Weekday::Fri);
|
||||
}
|
||||
}
|
||||
|
||||
mod reform_gap_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_reform_gap_detection() {
|
||||
let ctx = test_context();
|
||||
|
||||
assert!(ctx.is_reform_gap(1752, 9, 3));
|
||||
assert!(ctx.is_reform_gap(1752, 9, 13));
|
||||
assert!(ctx.is_reform_gap(1752, 9, 8));
|
||||
|
||||
assert!(!ctx.is_reform_gap(1752, 9, 2));
|
||||
assert!(!ctx.is_reform_gap(1752, 9, 14));
|
||||
|
||||
assert!(!ctx.is_reform_gap(1752, 8, 5));
|
||||
assert!(!ctx.is_reform_gap(1752, 10, 5));
|
||||
assert!(!ctx.is_reform_gap(1751, 9, 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_reform_gap_gregorian() {
|
||||
let ctx = gregorian_context();
|
||||
assert!(!ctx.is_reform_gap(1752, 9, 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_reform_gap_julian() {
|
||||
let ctx = julian_context();
|
||||
assert!(!ctx.is_reform_gap(1752, 9, 5));
|
||||
}
|
||||
}
|
||||
|
||||
mod day_of_year_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_day_of_year_non_leap() {
|
||||
let ctx = test_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 test_day_of_year_leap() {
|
||||
let ctx = test_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 test_day_of_year_reform_gap() {
|
||||
let ctx = test_context();
|
||||
|
||||
assert_eq!(ctx.day_of_year(1752, 9, 2), 235);
|
||||
assert_eq!(ctx.day_of_year(1752, 9, 14), 247);
|
||||
}
|
||||
}
|
||||
|
||||
mod month_data_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_month_data_january_2024() {
|
||||
let ctx = test_context();
|
||||
let month = MonthData::new(&ctx, 2024, 1);
|
||||
|
||||
assert_eq!(month.year, 2024);
|
||||
assert_eq!(month.month, 1);
|
||||
assert_eq!(month.days.len(), 42);
|
||||
|
||||
// January 2024 starts on Monday
|
||||
assert_eq!(month.days[0], Some(1));
|
||||
assert_eq!(month.days[30], Some(31));
|
||||
assert_eq!(month.days[31], None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_month_data_february_2024_leap() {
|
||||
let ctx = test_context();
|
||||
let month = MonthData::new(&ctx, 2024, 2);
|
||||
|
||||
// February 2024 starts on Thursday
|
||||
assert_eq!(month.days[0], None);
|
||||
assert_eq!(month.days[1], None);
|
||||
assert_eq!(month.days[2], None);
|
||||
assert_eq!(month.days[3], Some(1));
|
||||
assert_eq!(month.days[31], Some(29));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_month_data_september_1752_reform() {
|
||||
let ctx = test_context();
|
||||
let month = MonthData::new(&ctx, 1752, 9);
|
||||
|
||||
assert!(month.days.contains(&Some(2)));
|
||||
assert!(!month.days.contains(&Some(3)));
|
||||
assert!(!month.days.contains(&Some(13)));
|
||||
assert!(month.days.contains(&Some(14)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_month_data_weekday_alignment() {
|
||||
let ctx = test_context();
|
||||
|
||||
for month in 1..=12 {
|
||||
let month_data = MonthData::new(&ctx, 2024, month);
|
||||
|
||||
for (i, day) in month_data.days.iter().enumerate() {
|
||||
if day.is_some() {
|
||||
assert!(month_data.weekdays[i].is_some());
|
||||
} else {
|
||||
assert!(month_data.weekdays[i].is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod weekend_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_weekend() {
|
||||
let ctx = test_context();
|
||||
|
||||
assert!(ctx.is_weekend(Weekday::Sat));
|
||||
assert!(ctx.is_weekend(Weekday::Sun));
|
||||
assert!(!ctx.is_weekend(Weekday::Mon));
|
||||
assert!(!ctx.is_weekend(Weekday::Tue));
|
||||
assert!(!ctx.is_weekend(Weekday::Wed));
|
||||
assert!(!ctx.is_weekend(Weekday::Thu));
|
||||
assert!(!ctx.is_weekend(Weekday::Fri));
|
||||
}
|
||||
}
|
||||
|
||||
mod week_number_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_iso_week_number() {
|
||||
let mut ctx = test_context();
|
||||
ctx.week_type = WeekType::Iso;
|
||||
|
||||
assert_eq!(ctx.week_number(2024, 1, 1), 1);
|
||||
|
||||
let week = ctx.week_number(2024, 12, 30);
|
||||
assert!(week == 1 || week == 53);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_us_week_number() {
|
||||
let mut ctx = test_context();
|
||||
ctx.week_type = WeekType::Us;
|
||||
|
||||
assert_eq!(ctx.week_number(2024, 1, 1), 1);
|
||||
}
|
||||
}
|
||||
|
||||
mod context_validation_tests {
|
||||
use super::*;
|
||||
use cal::args::Args;
|
||||
use clap::Parser;
|
||||
|
||||
#[test]
|
||||
fn test_context_creation_default() {
|
||||
let args = Args::parse_from(["cal"]);
|
||||
let ctx = CalContext::new(&args);
|
||||
assert!(ctx.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_creation_with_options() {
|
||||
let args = Args::parse_from(["cal", "-y", "-j", "-w"]);
|
||||
let ctx = CalContext::new(&args);
|
||||
assert!(ctx.is_ok());
|
||||
let ctx = ctx.unwrap();
|
||||
assert!(ctx.julian);
|
||||
assert!(ctx.week_numbers);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_mutually_exclusive_options() {
|
||||
let args = Args::parse_from(["cal", "-y", "-n", "5"]);
|
||||
let result = CalContext::new(&args);
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(err.contains("mutually exclusive"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_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 test_context_sunday_start() {
|
||||
let args = Args::parse_from(["cal", "-s"]);
|
||||
let ctx = CalContext::new(&args).unwrap();
|
||||
assert_eq!(ctx.week_start, Weekday::Sun);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_color_settings() {
|
||||
let args = Args::parse_from(["cal"]);
|
||||
let ctx = CalContext::new(&args).unwrap();
|
||||
assert!(!ctx.color);
|
||||
|
||||
let args = Args::parse_from(["cal", "--color"]);
|
||||
let ctx = CalContext::new(&args).unwrap();
|
||||
assert!(!ctx.color);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_reform_types() {
|
||||
let args = Args::parse_from(["cal", "--reform", "gregorian"]);
|
||||
let ctx = CalContext::new(&args).unwrap();
|
||||
assert_eq!(ctx.reform_year, i32::MIN);
|
||||
|
||||
let args = Args::parse_from(["cal", "--reform", "julian"]);
|
||||
let ctx = CalContext::new(&args).unwrap();
|
||||
assert_eq!(ctx.reform_year, i32::MAX);
|
||||
|
||||
let args = Args::parse_from(["cal", "--iso"]);
|
||||
let ctx = CalContext::new(&args).unwrap();
|
||||
assert_eq!(ctx.reform_year, i32::MIN);
|
||||
}
|
||||
}
|
||||
|
||||
mod parse_month_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_month_numeric() {
|
||||
for (input, expected) in [
|
||||
("1", Some(1)),
|
||||
("2", Some(2)),
|
||||
("12", Some(12)),
|
||||
("0", None),
|
||||
("13", None),
|
||||
("abc", None),
|
||||
] {
|
||||
assert_eq!(parse_month(input), expected, "Failed for input: {}", input);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_month_english_names() {
|
||||
assert_eq!(parse_month("january"), Some(1));
|
||||
assert_eq!(parse_month("January"), Some(1));
|
||||
assert_eq!(parse_month("JANUARY"), Some(1));
|
||||
assert_eq!(parse_month("february"), Some(2));
|
||||
assert_eq!(parse_month("december"), Some(12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_month_english_short() {
|
||||
assert_eq!(parse_month("jan"), Some(1));
|
||||
assert_eq!(parse_month("feb"), Some(2));
|
||||
assert_eq!(parse_month("mar"), Some(3));
|
||||
assert_eq!(parse_month("apr"), Some(4));
|
||||
assert_eq!(parse_month("jun"), Some(6));
|
||||
assert_eq!(parse_month("jul"), Some(7));
|
||||
assert_eq!(parse_month("aug"), Some(8));
|
||||
assert_eq!(parse_month("sep"), Some(9));
|
||||
assert_eq!(parse_month("oct"), Some(10));
|
||||
assert_eq!(parse_month("nov"), Some(11));
|
||||
assert_eq!(parse_month("dec"), Some(12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_month_russian() {
|
||||
assert_eq!(parse_month("январь"), Some(1));
|
||||
assert_eq!(parse_month("февраль"), Some(2));
|
||||
assert_eq!(parse_month("декабрь"), Some(12));
|
||||
}
|
||||
}
|
||||
|
||||
mod layout_tests {
|
||||
use super::*;
|
||||
use cal::formatter::{
|
||||
format_month_grid, format_month_header, format_weekday_headers, get_weekday_order,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_month_header_format() {
|
||||
let header = format_month_header(2026, 2, 20, true, false);
|
||||
assert!(header.contains("Февраль"));
|
||||
assert!(header.contains("2026"));
|
||||
assert!(header.width() >= 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_month_header_without_year() {
|
||||
let header = format_month_header(2026, 2, 20, false, false);
|
||||
assert!(header.contains("Февраль"));
|
||||
assert!(!header.contains("2026"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_month_header_with_color() {
|
||||
let header = format_month_header(2026, 2, 20, true, true);
|
||||
assert!(header.contains("\x1b[96m"));
|
||||
assert!(header.contains("\x1b[0m"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weekday_header_structure_monday_start() {
|
||||
let ctx = test_context();
|
||||
let header = format_weekday_headers(&ctx, false);
|
||||
|
||||
assert!(header.contains("Пн"));
|
||||
assert!(header.contains("Вт"));
|
||||
assert!(header.contains("Ср"));
|
||||
assert!(header.contains("Чт"));
|
||||
assert!(header.contains("Пт"));
|
||||
assert!(header.contains("Сб"));
|
||||
assert!(header.contains("Вс"));
|
||||
|
||||
let mon_pos = header.find("Пн").unwrap();
|
||||
let tue_pos = header.find("Вт").unwrap();
|
||||
assert!(mon_pos < tue_pos);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weekday_header_structure_sunday_start() {
|
||||
let mut ctx = test_context();
|
||||
ctx.week_start = chrono::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 test_weekday_header_with_week_numbers() {
|
||||
let mut ctx = test_context();
|
||||
ctx.week_numbers = true;
|
||||
let header = format_weekday_headers(&ctx, false);
|
||||
assert!(header.len() > 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weekday_header_with_julian() {
|
||||
let mut ctx = test_context();
|
||||
ctx.julian = true;
|
||||
let header = format_weekday_headers(&ctx, false);
|
||||
assert!(header.starts_with(" "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_month_grid_line_count() {
|
||||
let ctx = test_context();
|
||||
let month = MonthData::new(&ctx, 2024, 1);
|
||||
let grid = format_month_grid(&ctx, &month);
|
||||
|
||||
assert!(grid.len() >= 8);
|
||||
assert!(grid.len() <= 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_month_grid_first_line_is_header() {
|
||||
let ctx = test_context();
|
||||
let month = MonthData::new(&ctx, 2024, 1);
|
||||
let grid = format_month_grid(&ctx, &month);
|
||||
|
||||
assert!(grid[0].contains("Январь"));
|
||||
assert!(grid[0].contains("2024"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_month_grid_second_line_is_weekdays() {
|
||||
let ctx = test_context();
|
||||
let month = MonthData::new(&ctx, 2024, 1);
|
||||
let grid = format_month_grid(&ctx, &month);
|
||||
|
||||
assert!(grid[1].contains("Пн"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_month_grid_contains_day_1() {
|
||||
let ctx = test_context();
|
||||
let month = MonthData::new(&ctx, 2024, 1);
|
||||
let grid = format_month_grid(&ctx, &month);
|
||||
|
||||
let days_area: String = grid[2..].join("\n");
|
||||
assert!(days_area.contains(" 1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_month_grid_contains_last_day() {
|
||||
let ctx = test_context();
|
||||
let month = MonthData::new(&ctx, 2024, 1);
|
||||
let grid = format_month_grid(&ctx, &month);
|
||||
|
||||
let days_area: String = grid[2..].join("\n");
|
||||
assert!(days_area.contains("31"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_month_grid_february_leap_year() {
|
||||
let ctx = test_context();
|
||||
let month = MonthData::new(&ctx, 2024, 2);
|
||||
let grid = format_month_grid(&ctx, &month);
|
||||
|
||||
let days_area: String = grid[2..].join("\n");
|
||||
assert!(days_area.contains("29"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_month_grid_february_non_leap_year() {
|
||||
let ctx = test_context();
|
||||
let month = MonthData::new(&ctx, 2023, 2);
|
||||
let grid = format_month_grid(&ctx, &month);
|
||||
|
||||
let days_area: String = grid[2..].join("\n");
|
||||
assert!(days_area.contains("28"));
|
||||
assert!(!days_area.contains("29"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vertical_layout_weekday_order() {
|
||||
let ctx = test_context();
|
||||
let weekday_order = get_weekday_order(ctx.week_start);
|
||||
|
||||
assert_eq!(weekday_order[0], chrono::Weekday::Mon);
|
||||
assert_eq!(weekday_order[6], chrono::Weekday::Sun);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vertical_layout_days_in_columns() {
|
||||
let ctx = test_context();
|
||||
let month = MonthData::new(&ctx, 2024, 1);
|
||||
|
||||
let day1_pos = month.days.iter().position(|&d| d == Some(1)).unwrap();
|
||||
let day8_pos = month.days.iter().position(|&d| d == Some(8)).unwrap();
|
||||
|
||||
assert_eq!(day8_pos - day1_pos, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_three_months_structure() {
|
||||
let ctx = test_context();
|
||||
let prev = MonthData::new(&ctx, 2024, 1);
|
||||
let curr = MonthData::new(&ctx, 2024, 2);
|
||||
let next = MonthData::new(&ctx, 2024, 3);
|
||||
|
||||
assert_eq!(prev.month, 1);
|
||||
assert_eq!(curr.month, 2);
|
||||
assert_eq!(next.month, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_three_months_year_boundary() {
|
||||
let ctx = test_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!(curr.year, 2024);
|
||||
assert_eq!(next.year, 2024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_color_codes_in_header() {
|
||||
let header = format_month_header(2024, 1, 20, true, true);
|
||||
assert!(header.starts_with("\x1b[96m"));
|
||||
assert!(header.ends_with("\x1b[0m"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_color_codes_when_disabled() {
|
||||
let header = format_month_header(2024, 1, 20, true, false);
|
||||
assert!(!header.contains("\x1b[96m"));
|
||||
assert!(!header.contains("\x1b[0m"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weekday_header_color_placement() {
|
||||
let mut ctx = test_context();
|
||||
ctx.color = true;
|
||||
let header = format_weekday_headers(&ctx, false);
|
||||
|
||||
assert!(header.starts_with("\x1b[93m"));
|
||||
assert!(header.ends_with("\x1b[0m"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weekday_header_no_color_when_disabled() {
|
||||
let mut ctx = test_context();
|
||||
ctx.color = false;
|
||||
let header = format_weekday_headers(&ctx, false);
|
||||
|
||||
assert!(!header.contains("\x1b[93m"));
|
||||
assert!(!header.contains("\x1b[0m"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_header_width_consistency() {
|
||||
let width = 20;
|
||||
let header1 = format_month_header(2024, 1, width, true, false);
|
||||
let header2 = format_month_header(2024, 12, width, true, false);
|
||||
|
||||
assert_eq!(header1.width(), width);
|
||||
assert_eq!(header2.width(), width);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_day_alignment_in_grid() {
|
||||
let ctx = test_context();
|
||||
let month = MonthData::new(&ctx, 2024, 1);
|
||||
let grid = format_month_grid(&ctx, &month);
|
||||
|
||||
let expected_width = grid[2].width();
|
||||
for (i, line) in grid.iter().enumerate().skip(2) {
|
||||
assert_eq!(
|
||||
line.width(),
|
||||
expected_width,
|
||||
"Line {} has inconsistent width",
|
||||
i
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod get_display_date_tests {
|
||||
use cal::args::{Args, get_display_date};
|
||||
use clap::Parser;
|
||||
|
||||
#[test]
|
||||
fn test_single_year_argument() {
|
||||
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 test_single_month_argument() {
|
||||
let args = Args::parse_from(["cal", "2"]);
|
||||
let (_year, month, _day) = get_display_date(&args).unwrap();
|
||||
assert_eq!(month, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_month_year_arguments() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
923
tests/unit_tests.rs
Normal file
923
tests/unit_tests.rs
Normal file
@@ -0,0 +1,923 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user