diff --git a/Cargo.lock b/Cargo.lock index ec263af..ce1bfed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,6 +97,29 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "bindgen" +version = "0.66.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" +dependencies = [ + "bitflags 2.5.0", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.60", + "which", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -157,6 +180,25 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -184,6 +226,17 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "clang-sys" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "compact_str" version = "0.7.1" @@ -353,6 +406,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + [[package]] name = "find-crate" version = "0.6.3" @@ -531,6 +600,114 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.5.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "greetd-stub" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab65c37f7c4b962b69a7976229214c20cd52d7467545d5f4776cd78a898871c" +dependencies = [ + "getopts", + "greetd_ipc", + "libfprint-rs", + "tokio", +] + [[package]] name = "greetd_ipc" version = "0.10.0" @@ -560,12 +737,27 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "i18n-config" version = "0.4.6" @@ -576,7 +768,7 @@ dependencies = [ "serde", "serde_derive", "thiserror", - "toml 0.8.12", + "toml 0.8.2", "unic-langid", ] @@ -724,12 +916,55 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libfprint-rs" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad7e3357d1bf578ec1623422cefeec2852a2764eecdc0cb5e78399922d2e5ba" +dependencies = [ + "gio", + "glib", + "libfprint-sys", +] + +[[package]] +name = "libfprint-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b721a5311ecd51c2c6e3f838e91dd7723c714f349a372129c3b05aec634ae8d" +dependencies = [ + "bindgen", + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets 0.52.5", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + [[package]] name = "locale_config" version = "0.3.0" @@ -783,6 +1018,12 @@ version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -816,6 +1057,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -940,6 +1191,12 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -952,6 +1209,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "powerfmt" version = "0.2.0" @@ -964,6 +1227,26 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "prettyplease" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550" +dependencies = [ + "proc-macro2", + "syn 2.0.60", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime", + "toml_edit", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1157,6 +1440,19 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustversion" version = "1.0.15" @@ -1259,6 +1555,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.17" @@ -1368,7 +1670,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", @@ -1396,6 +1698,37 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "textwrap" version = "0.16.1" @@ -1526,9 +1859,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.12" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ "serde", "serde_spanned", @@ -1538,18 +1871,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.12" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ "indexmap", "serde", @@ -1641,9 +1974,11 @@ dependencies = [ "crossterm", "futures", "getopts", + "greetd-stub", "greetd_ipc", "i18n-embed", "i18n-embed-fl", + "indoc", "lazy_static", "nix", "rand", @@ -1651,12 +1986,14 @@ dependencies = [ "rust-embed", "rust-ini", "smart-default", + "tempfile", "textwrap", "tokio", "tracing", "tracing-appender", "tracing-subscriber", "unic-langid", + "unicode-width", "uzers", "zeroize", ] @@ -1715,9 +2052,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "uzers" @@ -1735,6 +2072,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.4" @@ -1811,6 +2154,18 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1992,9 +2347,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.6" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 23f75fe..67e43ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,3 +47,9 @@ tracing = "0.1.40" [profile.release] lto = true + +[dev-dependencies] +greetd-stub = "0.1.0" +indoc = "2.0.5" +tempfile = "3.10.1" +unicode-width = "0.1.12" diff --git a/src/greeter.rs b/src/greeter.rs index 85d846d..60bfd90 100644 --- a/src/greeter.rs +++ b/src/greeter.rs @@ -193,6 +193,7 @@ impl Greeter { selected: 0, }; + #[cfg(not(test))] greeter.parse_options().await; greeter.sessions = Menu { diff --git a/src/integration/common/backend.rs b/src/integration/common/backend.rs new file mode 100644 index 0000000..6f30666 --- /dev/null +++ b/src/integration/common/backend.rs @@ -0,0 +1,216 @@ +#![allow(unused_must_use)] + +/* + Copied and adapted from the codebase of ratatui. + + Repository: https://github.com/ratatui-org/ratatui + License: https://github.com/ratatui-org/ratatui/blob/main/LICENSE + File: https://github.com/ratatui-org/ratatui/blob/f4637d40c35e068fd60d17c9a42b9114667c9861/src/backend/test.rs + + The MIT License (MIT) + + Copyright (c) 2016-2022 Florian Dehau + Copyright (c) 2023-2024 The Ratatui Developers + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +use std::{ + fmt::Write, + io, + sync::{Arc, Mutex}, +}; + +use tokio::sync::mpsc; +use unicode_width::UnicodeWidthStr; + +use tui::{ + backend::{Backend, ClearType, WindowSize}, + buffer::{Buffer, Cell}, + layout::{Rect, Size}, +}; + +#[derive(Clone)] +pub struct TestBackend { + tick: mpsc::Sender, + width: u16, + buffer: Arc>, + height: u16, + cursor: bool, + pos: (u16, u16), +} + +pub fn output(buffer: &Arc>) -> String { + let buffer = buffer.lock().unwrap(); + + let mut view = String::with_capacity(buffer.content.len() + buffer.area.height as usize * 3); + for cells in buffer.content.chunks(buffer.area.width as usize) { + let mut overwritten = vec![]; + let mut skip: usize = 0; + view.push('"'); + for (x, c) in cells.iter().enumerate() { + if skip == 0 { + view.push_str(c.symbol()); + } else { + overwritten.push((x, c.symbol())); + } + skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1); + } + view.push('"'); + if !overwritten.is_empty() { + write!(&mut view, " Hidden by multi-width symbols: {overwritten:?}").unwrap(); + } + view.push('\n'); + } + view +} + +impl TestBackend { + pub fn new(width: u16, height: u16) -> (Self, Arc>, mpsc::Receiver) { + let buffer = Arc::new(Mutex::new(Buffer::empty(Rect::new(0, 0, width, height)))); + let (tx, rx) = mpsc::channel::(10); + + let backend = Self { + tick: tx, + width, + height, + buffer: buffer.clone(), + cursor: false, + pos: (0, 0), + }; + + (backend, buffer, rx) + } +} + +impl Backend for TestBackend { + fn draw<'a, I>(&mut self, content: I) -> io::Result<()> + where + I: Iterator, + { + let mut buffer = self.buffer.lock().unwrap(); + + for (x, y, c) in content { + let cell = buffer.get_mut(x, y); + *cell = c.clone(); + } + + let sender = self.tick.clone(); + + std::thread::spawn(move || { + sender.blocking_send(true); + }); + + Ok(()) + } + + fn hide_cursor(&mut self) -> io::Result<()> { + self.cursor = false; + Ok(()) + } + + fn show_cursor(&mut self) -> io::Result<()> { + self.cursor = true; + Ok(()) + } + + fn get_cursor(&mut self) -> io::Result<(u16, u16)> { + Ok(self.pos) + } + + fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { + self.pos = (x, y); + Ok(()) + } + + fn clear(&mut self) -> io::Result<()> { + self.buffer.lock().unwrap().reset(); + Ok(()) + } + + fn clear_region(&mut self, clear_type: tui::backend::ClearType) -> io::Result<()> { + let buffer = self.buffer.clone(); + let mut buffer = buffer.lock().unwrap(); + + match clear_type { + ClearType::All => self.clear()?, + ClearType::AfterCursor => { + let index = buffer.index_of(self.pos.0, self.pos.1) + 1; + buffer.content[index..].fill(Cell::default()); + } + ClearType::BeforeCursor => { + let index = buffer.index_of(self.pos.0, self.pos.1); + buffer.content[..index].fill(Cell::default()); + } + ClearType::CurrentLine => { + let line_start_index = buffer.index_of(0, self.pos.1); + let line_end_index = buffer.index_of(self.width - 1, self.pos.1); + buffer.content[line_start_index..=line_end_index].fill(Cell::default()); + } + ClearType::UntilNewLine => { + let index = buffer.index_of(self.pos.0, self.pos.1); + let line_end_index = buffer.index_of(self.width - 1, self.pos.1); + buffer.content[index..=line_end_index].fill(Cell::default()); + } + } + Ok(()) + } + + fn append_lines(&mut self, n: u16) -> io::Result<()> { + let (cur_x, cur_y) = self.get_cursor()?; + + // the next column ensuring that we don't go past the last column + let new_cursor_x = cur_x.saturating_add(1).min(self.width.saturating_sub(1)); + + let max_y = self.height.saturating_sub(1); + let lines_after_cursor = max_y.saturating_sub(cur_y); + if n > lines_after_cursor { + let rotate_by = n.saturating_sub(lines_after_cursor).min(max_y); + + if rotate_by == self.height - 1 { + self.clear()?; + } + + self.set_cursor(0, rotate_by)?; + self.clear_region(ClearType::BeforeCursor)?; + self.buffer.lock().unwrap().content.rotate_left((self.width * rotate_by).into()); + } + + let new_cursor_y = cur_y.saturating_add(n).min(max_y); + self.set_cursor(new_cursor_x, new_cursor_y)?; + + Ok(()) + } + + fn size(&self) -> io::Result { + Ok(Rect::new(0, 0, self.width, self.height)) + } + + fn window_size(&mut self) -> io::Result { + // Some arbitrary window pixel size, probably doesn't need much testing. + static WINDOW_PIXEL_SIZE: Size = Size { width: 640, height: 480 }; + Ok(WindowSize { + columns_rows: (self.width, self.height).into(), + pixels: WINDOW_PIXEL_SIZE, + }) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} diff --git a/src/integration/common/mod.rs b/src/integration/common/mod.rs new file mode 100644 index 0000000..8294fcb --- /dev/null +++ b/src/integration/common/mod.rs @@ -0,0 +1,81 @@ +mod backend; + +use std::sync::{Arc, Mutex}; + +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use libgreetd_stub::SessionOptions; +use tempfile::NamedTempFile; +use tokio::{ + sync::mpsc::{self, Receiver, Sender}, + task::JoinHandle, +}; +use tui::buffer::Buffer; + +use crate::{ + event::{Event, Events}, + ui::sessions::SessionSource, + Greeter, +}; + +pub use self::backend::{output, TestBackend}; + +pub async fn run_test_greeter(opts: SessionOptions<'static>, builder: Option) -> (JoinHandle<()>, JoinHandle<()>, Arc>, mpsc::Sender, mpsc::Receiver) { + let socket = NamedTempFile::new().unwrap().into_temp_path().to_path_buf(); + + let (backend, buffer, tick) = TestBackend::new(200, 200); + let events = Events::new().await; + let sender = events.sender(); + + let server = tokio::task::spawn({ + let socket = socket.clone(); + + async move { + libgreetd_stub::start(&socket, &opts).await; + } + }); + + let client = tokio::task::spawn(async move { + let mut greeter = Greeter::new(events.sender()).await; + + if let Some(builder) = builder { + builder(&mut greeter); + } + + if greeter.config.is_none() { + greeter.config = Greeter::options().parse(&[""]).ok(); + } + + greeter.logfile = "/tmp/tuigreet.log".to_string(); + greeter.socket = socket.to_str().unwrap().to_string(); + greeter.events = Some(events.sender()); + greeter.session_source = SessionSource::Command("uname".to_string()); + greeter.connect().await; + + let _ = crate::run(backend, greeter, events).await; + }); + + (server, client, buffer, sender, tick) +} + +#[allow(unused_must_use)] +pub async fn send_key(sender: &Sender, key: KeyCode) { + sender.send(Event::Key(KeyEvent::new(key, KeyModifiers::empty()))).await; +} + +#[allow(unused_must_use)] +pub async fn send_modified_key(sender: &Sender, key: KeyCode, modifiers: KeyModifiers) { + sender.send(Event::Key(KeyEvent::new(key, modifiers))).await; +} + +#[allow(unused_must_use)] +pub async fn send_text(sender: &Sender, text: &str) { + for char in text.chars() { + sender.send(Event::Key(KeyEvent::new(KeyCode::Char(char), KeyModifiers::empty()))).await; + } + + sender.send(Event::Key(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()))).await; +} + +pub async fn wait(tick: &mut Receiver) { + tick.recv().await; +} diff --git a/src/integration/mod.rs b/src/integration/mod.rs new file mode 100644 index 0000000..a076d38 --- /dev/null +++ b/src/integration/mod.rs @@ -0,0 +1,112 @@ +// #![allow(unused_must_use)] + +mod common; + +use std::time::Duration; + +use crossterm::event::{KeyCode, KeyModifiers}; +use futures::future; +use libgreetd_stub::SessionOptions; + +use self::common::{output, send_key, send_modified_key, send_text, wait}; + +#[tokio::test] +async fn exit() { + let opts = SessionOptions { + username: "apognu", + password: "password", + mfa: false, + fingerprint: false, + }; + + let (server, client, _buffer, sender, mut tick) = common::run_test_greeter(opts, None).await; + + let events = tokio::task::spawn(async move { + send_modified_key(&sender, KeyCode::Char('x'), KeyModifiers::CONTROL).await; + wait(&mut tick).await; + + future::pending::<()>().await; + }); + + let mut exited = false; + + tokio::select! { + _ = tokio::time::sleep(Duration::from_secs(5)) => {} + _ = server => {} + _ = client => { exited = true; }, + _ = events => {}, + } + + assert!(exited, "tuigreet did not exit"); +} + +#[tokio::test] +async fn simple_authentication_ok() { + let opts = SessionOptions { + username: "apognu", + password: "password", + mfa: false, + fingerprint: false, + }; + + let (server, client, buffer, sender, mut tick) = common::run_test_greeter(opts, None).await; + + let events = tokio::task::spawn(async move { + send_key(&sender, KeyCode::F(12)).await; + wait(&mut tick).await; + + assert_eq!(output(&buffer).contains("F12"), true); + assert_eq!(output(&buffer).contains("Power options"), true); + + send_key(&sender, KeyCode::Esc).await; + send_text(&sender, "apognu").await; + wait(&mut tick).await; + + assert_eq!(output(&buffer).contains("Password:"), true); + + send_text(&sender, "password").await; + + future::pending::<()>().await; + }); + + let mut exited = false; + + tokio::select! { + _ = tokio::time::sleep(Duration::from_secs(5)) => {} + _ = server => {} + _ = client => { exited = true; }, + _ = events => {}, + } + + assert!(exited, "tuigreet did not exit"); +} + +#[tokio::test] +async fn simple_authentication_bad_password() { + let opts = SessionOptions { + username: "apognu", + password: "password", + mfa: false, + fingerprint: false, + }; + + let (server, client, buffer, sender, mut tick) = common::run_test_greeter(opts, None).await; + + let events = tokio::task::spawn(async move { + send_text(&sender, "apognu").await; + wait(&mut tick).await; + + assert_eq!(output(&buffer).contains("Password:"), true); + + send_text(&sender, "password2").await; + + assert_eq!(output(&buffer).contains("Invalid credentials"), true); + }); + + tokio::select! { + _ = tokio::time::sleep(Duration::from_secs(5)) => {} + _ = server => {} + _ = client => {}, + _ = events => {}, + } +} diff --git a/src/main.rs b/src/main.rs index 4fc5c05..c51ffc5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,9 @@ mod keyboard; mod power; mod ui; +#[cfg(test)] +mod integration; + use std::{error::Error, fs::OpenOptions, io, process, sync::Arc}; use crossterm::{ @@ -29,7 +32,11 @@ use self::{event::Events, ipc::Ipc}; #[tokio::main] async fn main() { - if let Err(error) = run().await { + let backend = CrosstermBackend::new(io::stdout()); + let events = Events::new().await; + let greeter = Greeter::new(events.sender()).await; + + if let Err(error) = run(backend, greeter, events).await { if let Some(AuthStatus::Success) = error.downcast_ref::() { return; } @@ -38,11 +45,10 @@ async fn main() { } } -async fn run() -> Result<(), Box> { - let mut events = Events::new().await; - let mut greeter = Greeter::new(events.sender()).await; - let mut stdout = io::stdout(); - +async fn run(backend: B, mut greeter: Greeter, mut events: Events) -> Result<(), Box> +where + B: tui::backend::Backend, +{ let _guard = init_logger(&greeter); tracing::info!("tuigreet started"); @@ -50,11 +56,11 @@ async fn run() -> Result<(), Box> { register_panic_handler(); enable_raw_mode()?; - execute!(stdout, EnterAlternateScreen)?; + execute!(io::stdout(), EnterAlternateScreen)?; - let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; + #[cfg(not(test))] terminal.clear()?; let ipc = Ipc::new(); @@ -116,7 +122,9 @@ async fn exit(greeter: &mut Greeter, status: AuthStatus) { AuthStatus::Cancel | AuthStatus::Failure => Ipc::cancel(greeter).await, } + #[cfg(not(test))] clear_screen(); + let _ = execute!(io::stdout(), LeaveAlternateScreen); let _ = disable_raw_mode(); @@ -127,7 +135,9 @@ fn register_panic_handler() { let hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { + #[cfg(not(test))] clear_screen(); + let _ = execute!(io::stdout(), LeaveAlternateScreen); let _ = disable_raw_mode(); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index e90f143..979beca 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -17,7 +17,6 @@ use std::{ use chrono::prelude::*; use tokio::sync::RwLock; use tui::{ - backend::CrosstermBackend, layout::{Alignment, Constraint, Direction, Layout}, style::Modifier, text::{Line, Span}, @@ -39,11 +38,14 @@ const STATUSBAR_INDEX: usize = 3; const STATUSBAR_LEFT_INDEX: usize = 1; const STATUSBAR_RIGHT_INDEX: usize = 2; -pub(super) type Backend = CrosstermBackend; -pub(super) type Term = Terminal; +// pub(super) type Backend = dyn tui::backend::Backend; +pub(super) type Term = Terminal; pub(super) type Frame<'a> = CrosstermFrame<'a>; -pub async fn draw(greeter: Arc>, terminal: &mut Term) -> Result<(), Box> { +pub async fn draw(greeter: Arc>, terminal: &mut Term) -> Result<(), Box> +where + B: tui::backend::Backend, +{ let mut greeter = greeter.write().await; let hide_cursor = should_hide_cursor(&greeter);