mirror of
https://github.com/apognu/tuigreet.git
synced 2024-09-11 07:25:29 +03:00
Added infrastructure to do integration testing.
This commit is contained in:
parent
71cf19e233
commit
e0ec54d1e5
125
Cargo.lock
generated
125
Cargo.lock
generated
@ -97,12 +97,6 @@ dependencies = [
|
|||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bitflags"
|
|
||||||
version = "1.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@ -253,7 +247,7 @@ version = "0.27.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
|
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.5.0",
|
"bitflags",
|
||||||
"crossterm_winapi",
|
"crossterm_winapi",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"libc",
|
"libc",
|
||||||
@ -353,6 +347,22 @@ version = "1.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
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]]
|
[[package]]
|
||||||
name = "find-crate"
|
name = "find-crate"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
@ -531,6 +541,17 @@ version = "0.28.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "greetd-stub"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ea0c791b4529519211981d4bf5c8bf77b95ac91816b7cde5348888b11469c28"
|
||||||
|
dependencies = [
|
||||||
|
"getopts",
|
||||||
|
"greetd_ipc",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "greetd_ipc"
|
name = "greetd_ipc"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@ -546,9 +567,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.3"
|
version = "0.14.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"allocator-api2",
|
"allocator-api2",
|
||||||
@ -726,9 +747,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.153"
|
version = "0.2.154"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.4.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "locale_config"
|
name = "locale_config"
|
||||||
@ -745,9 +772,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.11"
|
version = "0.4.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
|
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
@ -810,7 +837,7 @@ version = "0.28.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.5.0",
|
"bitflags",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"libc",
|
"libc",
|
||||||
@ -913,9 +940,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.1"
|
version = "0.12.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
|
checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lock_api",
|
"lock_api",
|
||||||
"parking_lot_core",
|
"parking_lot_core",
|
||||||
@ -923,15 +950,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot_core"
|
name = "parking_lot_core"
|
||||||
version = "0.9.9"
|
version = "0.9.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
|
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"windows-targets 0.48.5",
|
"windows-targets 0.52.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1048,7 +1075,7 @@ version = "0.26.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80"
|
checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.5.0",
|
"bitflags",
|
||||||
"cassowary",
|
"cassowary",
|
||||||
"compact_str",
|
"compact_str",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
@ -1064,11 +1091,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.4.1"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
|
checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1157,6 +1184,19 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "0.38.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
@ -1201,18 +1241,18 @@ checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.198"
|
version = "1.0.199"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
|
checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.198"
|
version = "1.0.199"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
|
checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1323,9 +1363,9 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.5.6"
|
version = "0.5.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
|
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
@ -1396,6 +1436,18 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "textwrap"
|
name = "textwrap"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
@ -1641,6 +1693,7 @@ dependencies = [
|
|||||||
"crossterm",
|
"crossterm",
|
||||||
"futures",
|
"futures",
|
||||||
"getopts",
|
"getopts",
|
||||||
|
"greetd-stub",
|
||||||
"greetd_ipc",
|
"greetd_ipc",
|
||||||
"i18n-embed",
|
"i18n-embed",
|
||||||
"i18n-embed-fl",
|
"i18n-embed-fl",
|
||||||
@ -1651,12 +1704,14 @@ dependencies = [
|
|||||||
"rust-embed",
|
"rust-embed",
|
||||||
"rust-ini",
|
"rust-ini",
|
||||||
"smart-default",
|
"smart-default",
|
||||||
|
"tempfile",
|
||||||
"textwrap",
|
"textwrap",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-appender",
|
"tracing-appender",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"unic-langid",
|
"unic-langid",
|
||||||
|
"unicode-width",
|
||||||
"uzers",
|
"uzers",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@ -1715,9 +1770,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.1.11"
|
version = "0.1.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
|
checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uzers"
|
name = "uzers"
|
||||||
@ -1829,9 +1884,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.7"
|
version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "134306a13c5647ad6453e8deaec55d3a44d6021970129e6188735e74bf546697"
|
checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
@ -1992,9 +2047,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.6.6"
|
version = "0.6.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352"
|
checksum = "14b9415ee827af173ebb3f15f9083df5a122eb93572ec28741fb153356ea2578"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
@ -47,3 +47,8 @@ tracing = "0.1.40"
|
|||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
greetd-stub = "0.2.0"
|
||||||
|
tempfile = "3.10.1"
|
||||||
|
unicode-width = "0.1.12"
|
||||||
|
12
src/event.rs
12
src/event.rs
@ -1,12 +1,15 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crossterm::event::{Event as TermEvent, EventStream, KeyEvent};
|
use crossterm::event::{Event as TermEvent, KeyEvent};
|
||||||
use futures::{future::FutureExt, StreamExt};
|
use futures::{future::FutureExt, StreamExt};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
process::Command,
|
process::Command,
|
||||||
sync::mpsc::{self, Sender},
|
sync::mpsc::{self, Sender},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
use crossterm::event::EventStream;
|
||||||
|
|
||||||
use crate::AuthStatus;
|
use crate::AuthStatus;
|
||||||
|
|
||||||
const FRAME_RATE: f64 = 2.0;
|
const FRAME_RATE: f64 = 2.0;
|
||||||
@ -31,7 +34,14 @@ impl Events {
|
|||||||
let tx = tx.clone();
|
let tx = tx.clone();
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
|
#[cfg(not(test))]
|
||||||
let mut stream = EventStream::new();
|
let mut stream = EventStream::new();
|
||||||
|
|
||||||
|
// In tests, we are not capturing events from the terminal, so we need
|
||||||
|
// to replace the crossterm::EventStream with a dummy pending stream.
|
||||||
|
#[cfg(test)]
|
||||||
|
let mut stream = futures::stream::pending::<Result<TermEvent, ()>>();
|
||||||
|
|
||||||
let mut render_interval = tokio::time::interval(Duration::from_secs_f64(1.0 / FRAME_RATE));
|
let mut render_interval = tokio::time::interval(Duration::from_secs_f64(1.0 / FRAME_RATE));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
@ -193,6 +193,7 @@ impl Greeter {
|
|||||||
selected: 0,
|
selected: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
greeter.parse_options().await;
|
greeter.parse_options().await;
|
||||||
|
|
||||||
greeter.sessions = Menu {
|
greeter.sessions = Menu {
|
||||||
|
214
src/integration/common/backend.rs
Normal file
214
src/integration/common/backend.rs
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
#![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<bool>,
|
||||||
|
width: u16,
|
||||||
|
buffer: Arc<Mutex<Buffer>>,
|
||||||
|
height: u16,
|
||||||
|
cursor: bool,
|
||||||
|
pos: (u16, u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output(buffer: &Arc<Mutex<Buffer>>) -> 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<Mutex<Buffer>>, mpsc::Receiver<bool>) {
|
||||||
|
let buffer = Arc::new(Mutex::new(Buffer::empty(Rect::new(0, 0, width, height))));
|
||||||
|
let (tx, rx) = mpsc::channel::<bool>(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<Item = (u16, u16, &'a Cell)>,
|
||||||
|
{
|
||||||
|
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()?;
|
||||||
|
|
||||||
|
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<Rect> {
|
||||||
|
Ok(Rect::new(0, 0, self.width, self.height))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
173
src/integration/common/mod.rs
Normal file
173
src/integration/common/mod.rs
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
mod backend;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
panic,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
use libgreetd_stub::SessionOptions;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
use tokio::{
|
||||||
|
sync::{
|
||||||
|
mpsc::{Receiver, Sender},
|
||||||
|
RwLock,
|
||||||
|
},
|
||||||
|
task::{JoinError, JoinHandle},
|
||||||
|
};
|
||||||
|
use tui::buffer::Buffer;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
event::{Event, Events},
|
||||||
|
ui::sessions::SessionSource,
|
||||||
|
Greeter,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use self::backend::{output, TestBackend};
|
||||||
|
|
||||||
|
pub struct IntegrationRunner(Arc<RwLock<_IntegrationRunner>>);
|
||||||
|
|
||||||
|
struct _IntegrationRunner {
|
||||||
|
server: Option<JoinHandle<()>>,
|
||||||
|
client: Option<JoinHandle<()>>,
|
||||||
|
|
||||||
|
pub buffer: Arc<Mutex<Buffer>>,
|
||||||
|
pub sender: Sender<Event>,
|
||||||
|
pub tick: Receiver<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for IntegrationRunner {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
IntegrationRunner(Arc::clone(&self.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntegrationRunner {
|
||||||
|
pub async fn new(opts: SessionOptions<'static>, builder: Option<fn(&mut Greeter)>) -> IntegrationRunner {
|
||||||
|
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;
|
||||||
|
greeter.session_source = SessionSource::Command("uname".to_string());
|
||||||
|
|
||||||
|
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.connect().await;
|
||||||
|
|
||||||
|
let _ = crate::run(backend, greeter, events).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
IntegrationRunner(Arc::new(RwLock::new(_IntegrationRunner {
|
||||||
|
server: Some(server),
|
||||||
|
client: Some(client),
|
||||||
|
buffer,
|
||||||
|
sender,
|
||||||
|
tick,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn join_until_client_exit(&mut self, mut events: JoinHandle<()>) {
|
||||||
|
let (mut server, mut client) = {
|
||||||
|
let mut runner = self.0.write().await;
|
||||||
|
|
||||||
|
(runner.server.take().unwrap(), runner.client.take().unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut exited = false;
|
||||||
|
|
||||||
|
while !exited {
|
||||||
|
tokio::select! {
|
||||||
|
_ = tokio::time::sleep(Duration::from_secs(5)) => break,
|
||||||
|
_ = (&mut server) => {}
|
||||||
|
_ = (&mut client) => { exited = true; },
|
||||||
|
ret = (&mut events), if !events.is_finished() => rethrow(ret),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(exited, "tuigreet did not exit");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn join_until_end(&mut self, events: JoinHandle<()>) {
|
||||||
|
let (server, client) = {
|
||||||
|
let mut runner = self.0.write().await;
|
||||||
|
|
||||||
|
(runner.server.take().unwrap(), runner.client.take().unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = tokio::time::sleep(Duration::from_secs(5)) => {},
|
||||||
|
_ = server => {}
|
||||||
|
_ = client => {},
|
||||||
|
ret = events => rethrow(ret),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub async fn wait_until_buffer_contains(&mut self, needle: &str) {
|
||||||
|
loop {
|
||||||
|
if output(&self.0.read().await.buffer).contains(needle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.wait_for_render().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused, unused_must_use)]
|
||||||
|
pub async fn send_key(&self, key: KeyCode) {
|
||||||
|
self.0.write().await.sender.send(Event::Key(KeyEvent::new(key, KeyModifiers::empty()))).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused, unused_must_use)]
|
||||||
|
pub async fn send_modified_key(&self, key: KeyCode, modifiers: KeyModifiers) {
|
||||||
|
self.0.write().await.sender.send(Event::Key(KeyEvent::new(key, modifiers))).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused, unused_must_use)]
|
||||||
|
pub async fn send_text(&self, text: &str) {
|
||||||
|
for char in text.chars() {
|
||||||
|
self.0.write().await.sender.send(Event::Key(KeyEvent::new(KeyCode::Char(char), KeyModifiers::empty()))).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.0.write().await.sender.send(Event::Key(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()))).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub async fn wait_for_render(&mut self) {
|
||||||
|
self.0.write().await.tick.recv().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn output(&self) -> String {
|
||||||
|
output(&self.0.read().await.buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rethrow(result: Result<(), JoinError>) {
|
||||||
|
if let Err(err) = result {
|
||||||
|
if let Ok(panick) = err.try_into_panic() {
|
||||||
|
panic::resume_unwind(panick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
src/integration/exit.rs
Normal file
26
src/integration/exit.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use crossterm::event::{KeyCode, KeyModifiers};
|
||||||
|
use libgreetd_stub::SessionOptions;
|
||||||
|
|
||||||
|
use super::common::IntegrationRunner;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn exit() {
|
||||||
|
let opts = SessionOptions {
|
||||||
|
username: "apognu",
|
||||||
|
password: "password",
|
||||||
|
mfa: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut runner = IntegrationRunner::new(opts, None).await;
|
||||||
|
|
||||||
|
let events = tokio::task::spawn({
|
||||||
|
let mut runner = runner.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
runner.send_modified_key(KeyCode::Char('x'), KeyModifiers::CONTROL).await;
|
||||||
|
runner.wait_for_render().await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.join_until_client_exit(events).await;
|
||||||
|
}
|
157
src/integration/menus.rs
Normal file
157
src/integration/menus.rs
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
use crossterm::event::{KeyCode, KeyModifiers};
|
||||||
|
use libgreetd_stub::SessionOptions;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
power::PowerOption,
|
||||||
|
ui::{common::menu::Menu, power::Power, sessions::Session},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::common::IntegrationRunner;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn change_command() {
|
||||||
|
let opts = SessionOptions {
|
||||||
|
username: "apognu",
|
||||||
|
password: "password",
|
||||||
|
mfa: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut runner = IntegrationRunner::new(opts, None).await;
|
||||||
|
|
||||||
|
let events = tokio::task::spawn({
|
||||||
|
let mut runner = runner.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
runner.wait_until_buffer_contains("Username:").await;
|
||||||
|
runner.send_key(KeyCode::F(3)).await;
|
||||||
|
runner.wait_for_render().await;
|
||||||
|
|
||||||
|
assert!(runner.output().await.contains("CMD uname"));
|
||||||
|
|
||||||
|
runner.send_key(KeyCode::F(2)).await;
|
||||||
|
runner.wait_for_render().await;
|
||||||
|
|
||||||
|
assert!(runner.output().await.contains("Change session command"));
|
||||||
|
assert!(runner.output().await.contains("New command: uname"));
|
||||||
|
|
||||||
|
runner.send_modified_key(KeyCode::Char('u'), KeyModifiers::CONTROL).await;
|
||||||
|
runner.send_text("mynewcommand").await;
|
||||||
|
runner.send_key(KeyCode::Enter).await;
|
||||||
|
runner.wait_for_render().await;
|
||||||
|
|
||||||
|
assert!(runner.output().await.contains("CMD mynewcommand"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.join_until_end(events).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn session_menu() {
|
||||||
|
let opts = SessionOptions {
|
||||||
|
username: "apognu",
|
||||||
|
password: "password",
|
||||||
|
mfa: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut runner = IntegrationRunner::new(
|
||||||
|
opts,
|
||||||
|
Some(|greeter| {
|
||||||
|
greeter.sessions = Menu::<Session> {
|
||||||
|
title: "List of sessions".to_string(),
|
||||||
|
options: vec![
|
||||||
|
Session {
|
||||||
|
name: "My Session".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Session {
|
||||||
|
name: "Second Session".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selected: 0,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let events = tokio::task::spawn({
|
||||||
|
let mut runner = runner.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
runner.wait_until_buffer_contains("Username:").await;
|
||||||
|
runner.send_key(KeyCode::F(3)).await;
|
||||||
|
runner.wait_for_render().await;
|
||||||
|
|
||||||
|
assert!(runner.output().await.contains("List of sessions"));
|
||||||
|
assert!(runner.output().await.contains("My Session"));
|
||||||
|
assert!(runner.output().await.contains("Second Session"));
|
||||||
|
|
||||||
|
runner.send_key(KeyCode::Down).await;
|
||||||
|
runner.send_key(KeyCode::Down).await;
|
||||||
|
runner.send_key(KeyCode::Enter).await;
|
||||||
|
runner.wait_for_render().await;
|
||||||
|
|
||||||
|
assert!(runner.output().await.contains("CMD Second Session"));
|
||||||
|
|
||||||
|
runner.send_key(KeyCode::F(3)).await;
|
||||||
|
runner.wait_for_render().await;
|
||||||
|
runner.send_key(KeyCode::Up).await;
|
||||||
|
runner.send_key(KeyCode::Up).await;
|
||||||
|
runner.send_key(KeyCode::Enter).await;
|
||||||
|
runner.wait_for_render().await;
|
||||||
|
|
||||||
|
assert!(runner.output().await.contains("CMD My Session"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.join_until_end(events).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn power_menu() {
|
||||||
|
let opts = SessionOptions {
|
||||||
|
username: "apognu",
|
||||||
|
password: "password",
|
||||||
|
mfa: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut runner = IntegrationRunner::new(
|
||||||
|
opts,
|
||||||
|
Some(|greeter| {
|
||||||
|
greeter.powers = Menu::<Power> {
|
||||||
|
title: "What to do?".to_string(),
|
||||||
|
options: vec![
|
||||||
|
Power {
|
||||||
|
action: PowerOption::Shutdown,
|
||||||
|
label: "Turn it off".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Power {
|
||||||
|
action: PowerOption::Reboot,
|
||||||
|
label: "And back on again".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selected: 0,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let events = tokio::task::spawn({
|
||||||
|
let mut runner = runner.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
runner.wait_until_buffer_contains("Username:").await;
|
||||||
|
runner.send_key(KeyCode::F(12)).await;
|
||||||
|
runner.wait_for_render().await;
|
||||||
|
|
||||||
|
assert!(runner.output().await.contains("What to do?"));
|
||||||
|
assert!(runner.output().await.contains("Turn it off"));
|
||||||
|
assert!(runner.output().await.contains("And back on again"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.join_until_end(events).await;
|
||||||
|
}
|
7
src/integration/mod.rs
Normal file
7
src/integration/mod.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
mod common;
|
||||||
|
|
||||||
|
mod exit;
|
||||||
|
mod menus;
|
||||||
|
mod movement;
|
||||||
|
mod remember;
|
||||||
|
mod simple;
|
49
src/integration/movement.rs
Normal file
49
src/integration/movement.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
use crossterm::event::{KeyCode, KeyModifiers};
|
||||||
|
use libgreetd_stub::SessionOptions;
|
||||||
|
|
||||||
|
use super::common::IntegrationRunner;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn keyboard_movement() {
|
||||||
|
let opts = SessionOptions {
|
||||||
|
username: "apognu",
|
||||||
|
password: "password",
|
||||||
|
mfa: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut runner = IntegrationRunner::new(opts, None).await;
|
||||||
|
|
||||||
|
let events = tokio::task::spawn({
|
||||||
|
let mut runner = runner.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
runner.wait_until_buffer_contains("Username:").await;
|
||||||
|
for char in "apognu".chars() {
|
||||||
|
runner.send_key(KeyCode::Char(char)).await;
|
||||||
|
}
|
||||||
|
runner.wait_for_render().await;
|
||||||
|
|
||||||
|
assert!(runner.output().await.contains("Username: apognu"));
|
||||||
|
|
||||||
|
runner.send_key(KeyCode::Left).await;
|
||||||
|
runner.send_key(KeyCode::Char('l')).await;
|
||||||
|
runner.send_key(KeyCode::Right).await;
|
||||||
|
runner.send_key(KeyCode::Char('r')).await;
|
||||||
|
runner.send_modified_key(KeyCode::Char('a'), KeyModifiers::CONTROL).await;
|
||||||
|
runner.send_key(KeyCode::Char('a')).await;
|
||||||
|
runner.send_modified_key(KeyCode::Char('e'), KeyModifiers::CONTROL).await;
|
||||||
|
runner.send_key(KeyCode::Char('e')).await;
|
||||||
|
runner.wait_for_render().await;
|
||||||
|
|
||||||
|
assert!(runner.output().await.contains("Username: aapognlure"));
|
||||||
|
|
||||||
|
runner.send_key(KeyCode::Left).await;
|
||||||
|
runner.send_modified_key(KeyCode::Char('u'), KeyModifiers::CONTROL).await;
|
||||||
|
runner.wait_for_render().await;
|
||||||
|
|
||||||
|
assert!(runner.output().await.contains("Username: "));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.join_until_end(events).await;
|
||||||
|
}
|
43
src/integration/remember.rs
Normal file
43
src/integration/remember.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
use crossterm::event::KeyCode;
|
||||||
|
use libgreetd_stub::SessionOptions;
|
||||||
|
|
||||||
|
use crate::ui::common::masked::MaskedString;
|
||||||
|
|
||||||
|
use super::common::IntegrationRunner;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn remember_username() {
|
||||||
|
let opts = SessionOptions {
|
||||||
|
username: "apognu",
|
||||||
|
password: "password",
|
||||||
|
mfa: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut runner = IntegrationRunner::new(
|
||||||
|
opts,
|
||||||
|
Some(|greeter| {
|
||||||
|
greeter.remember = true;
|
||||||
|
greeter.username = MaskedString::from("apognu".to_string(), None);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let events = tokio::task::spawn({
|
||||||
|
let mut runner = runner.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
runner.wait_until_buffer_contains("Username:").await;
|
||||||
|
|
||||||
|
assert!(runner.output().await.contains("Username: apognu"));
|
||||||
|
|
||||||
|
runner.wait_until_buffer_contains("Password:").await;
|
||||||
|
runner.send_key(KeyCode::Esc).await;
|
||||||
|
runner.wait_for_render().await;
|
||||||
|
|
||||||
|
assert!(runner.output().await.contains("Username: "));
|
||||||
|
assert!(!runner.output().await.contains("Password:"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.join_until_end(events).await;
|
||||||
|
}
|
82
src/integration/simple.rs
Normal file
82
src/integration/simple.rs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
use libgreetd_stub::SessionOptions;
|
||||||
|
|
||||||
|
use super::common::IntegrationRunner;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn authentication_ok() {
|
||||||
|
let opts = SessionOptions {
|
||||||
|
username: "apognu",
|
||||||
|
password: "password",
|
||||||
|
mfa: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut runner = IntegrationRunner::new(opts, None).await;
|
||||||
|
|
||||||
|
let events = tokio::task::spawn({
|
||||||
|
let mut runner = runner.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
runner.wait_until_buffer_contains("Username:").await;
|
||||||
|
runner.send_text("apognu").await;
|
||||||
|
runner.wait_until_buffer_contains("Password:").await;
|
||||||
|
runner.send_text("password").await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.join_until_client_exit(events).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn authentication_bad_password() {
|
||||||
|
let opts = SessionOptions {
|
||||||
|
username: "apognu",
|
||||||
|
password: "password",
|
||||||
|
mfa: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut runner = IntegrationRunner::new(opts, None).await;
|
||||||
|
|
||||||
|
let events = tokio::task::spawn({
|
||||||
|
let mut runner = runner.clone();
|
||||||
|
|
||||||
|
{
|
||||||
|
async move {
|
||||||
|
runner.wait_until_buffer_contains("Username:").await;
|
||||||
|
runner.send_text("apognu").await;
|
||||||
|
runner.wait_until_buffer_contains("Password:").await;
|
||||||
|
runner.send_text("password2").await;
|
||||||
|
runner.wait_for_render().await;
|
||||||
|
|
||||||
|
assert!(runner.output().await.contains("Authentication failed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.join_until_end(events).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn authentication_ok_mfa() {
|
||||||
|
let opts = SessionOptions {
|
||||||
|
username: "apognu",
|
||||||
|
password: "password",
|
||||||
|
mfa: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut runner = IntegrationRunner::new(opts, None).await;
|
||||||
|
|
||||||
|
let events = tokio::task::spawn({
|
||||||
|
let mut runner = runner.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
runner.wait_until_buffer_contains("Username:").await;
|
||||||
|
runner.send_text("apognu").await;
|
||||||
|
runner.wait_until_buffer_contains("Password:").await;
|
||||||
|
runner.send_text("password").await;
|
||||||
|
runner.wait_until_buffer_contains("7 + 2 =").await;
|
||||||
|
runner.send_text("9").await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.join_until_client_exit(events).await;
|
||||||
|
}
|
37
src/main.rs
37
src/main.rs
@ -12,11 +12,14 @@ mod keyboard;
|
|||||||
mod power;
|
mod power;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod integration;
|
||||||
|
|
||||||
use std::{error::Error, fs::OpenOptions, io, process, sync::Arc};
|
use std::{error::Error, fs::OpenOptions, io, process, sync::Arc};
|
||||||
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, LeaveAlternateScreen},
|
||||||
};
|
};
|
||||||
use event::Event;
|
use event::Event;
|
||||||
use greetd_ipc::Request;
|
use greetd_ipc::Request;
|
||||||
@ -24,12 +27,19 @@ use tokio::sync::RwLock;
|
|||||||
use tracing_appender::non_blocking::WorkerGuard;
|
use tracing_appender::non_blocking::WorkerGuard;
|
||||||
use tui::{backend::CrosstermBackend, Terminal};
|
use tui::{backend::CrosstermBackend, Terminal};
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
use crossterm::terminal::{enable_raw_mode, EnterAlternateScreen};
|
||||||
|
|
||||||
pub use self::greeter::*;
|
pub use self::greeter::*;
|
||||||
use self::{event::Events, ipc::Ipc};
|
use self::{event::Events, ipc::Ipc};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn 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::<AuthStatus>() {
|
if let Some(AuthStatus::Success) = error.downcast_ref::<AuthStatus>() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -38,23 +48,25 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run() -> Result<(), Box<dyn Error>> {
|
async fn run<B>(backend: B, mut greeter: Greeter, mut events: Events) -> Result<(), Box<dyn Error>>
|
||||||
let mut events = Events::new().await;
|
where
|
||||||
let mut greeter = Greeter::new(events.sender()).await;
|
B: tui::backend::Backend,
|
||||||
let mut stdout = io::stdout();
|
{
|
||||||
|
|
||||||
let _guard = init_logger(&greeter);
|
let _guard = init_logger(&greeter);
|
||||||
|
|
||||||
tracing::info!("tuigreet started");
|
tracing::info!("tuigreet started");
|
||||||
|
|
||||||
register_panic_handler();
|
register_panic_handler();
|
||||||
|
|
||||||
enable_raw_mode()?;
|
#[cfg(not(test))]
|
||||||
execute!(stdout, EnterAlternateScreen)?;
|
{
|
||||||
|
enable_raw_mode()?;
|
||||||
|
execute!(io::stdout(), EnterAlternateScreen)?;
|
||||||
|
}
|
||||||
|
|
||||||
let backend = CrosstermBackend::new(stdout);
|
|
||||||
let mut terminal = Terminal::new(backend)?;
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
terminal.clear()?;
|
terminal.clear()?;
|
||||||
|
|
||||||
let ipc = Ipc::new();
|
let ipc = Ipc::new();
|
||||||
@ -116,7 +128,9 @@ async fn exit(greeter: &mut Greeter, status: AuthStatus) {
|
|||||||
AuthStatus::Cancel | AuthStatus::Failure => Ipc::cancel(greeter).await,
|
AuthStatus::Cancel | AuthStatus::Failure => Ipc::cancel(greeter).await,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
clear_screen();
|
clear_screen();
|
||||||
|
|
||||||
let _ = execute!(io::stdout(), LeaveAlternateScreen);
|
let _ = execute!(io::stdout(), LeaveAlternateScreen);
|
||||||
let _ = disable_raw_mode();
|
let _ = disable_raw_mode();
|
||||||
|
|
||||||
@ -127,7 +141,9 @@ fn register_panic_handler() {
|
|||||||
let hook = std::panic::take_hook();
|
let hook = std::panic::take_hook();
|
||||||
|
|
||||||
std::panic::set_hook(Box::new(move |info| {
|
std::panic::set_hook(Box::new(move |info| {
|
||||||
|
#[cfg(not(test))]
|
||||||
clear_screen();
|
clear_screen();
|
||||||
|
|
||||||
let _ = execute!(io::stdout(), LeaveAlternateScreen);
|
let _ = execute!(io::stdout(), LeaveAlternateScreen);
|
||||||
let _ = disable_raw_mode();
|
let _ = disable_raw_mode();
|
||||||
|
|
||||||
@ -135,6 +151,7 @@ fn register_panic_handler() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
pub fn clear_screen() {
|
pub fn clear_screen() {
|
||||||
let backend = CrosstermBackend::new(io::stdout());
|
let backend = CrosstermBackend::new(io::stdout());
|
||||||
|
|
||||||
|
@ -17,7 +17,6 @@ use std::{
|
|||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tui::{
|
use tui::{
|
||||||
backend::CrosstermBackend,
|
|
||||||
layout::{Alignment, Constraint, Direction, Layout},
|
layout::{Alignment, Constraint, Direction, Layout},
|
||||||
style::Modifier,
|
style::Modifier,
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
@ -39,11 +38,12 @@ const STATUSBAR_INDEX: usize = 3;
|
|||||||
const STATUSBAR_LEFT_INDEX: usize = 1;
|
const STATUSBAR_LEFT_INDEX: usize = 1;
|
||||||
const STATUSBAR_RIGHT_INDEX: usize = 2;
|
const STATUSBAR_RIGHT_INDEX: usize = 2;
|
||||||
|
|
||||||
pub(super) type Backend = CrosstermBackend<io::Stdout>;
|
|
||||||
pub(super) type Term = Terminal<Backend>;
|
|
||||||
pub(super) type Frame<'a> = CrosstermFrame<'a>;
|
pub(super) type Frame<'a> = CrosstermFrame<'a>;
|
||||||
|
|
||||||
pub async fn draw(greeter: Arc<RwLock<Greeter>>, terminal: &mut Term) -> Result<(), Box<dyn Error>> {
|
pub async fn draw<B>(greeter: Arc<RwLock<Greeter>>, terminal: &mut Terminal<B>) -> Result<(), Box<dyn Error>>
|
||||||
|
where
|
||||||
|
B: tui::backend::Backend,
|
||||||
|
{
|
||||||
let mut greeter = greeter.write().await;
|
let mut greeter = greeter.write().await;
|
||||||
let hide_cursor = should_hide_cursor(&greeter);
|
let hide_cursor = should_hide_cursor(&greeter);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user