Skip to content

Commit c13c90f

Browse files
committed
feat: make term testable
1 parent d7d1c64 commit c13c90f

File tree

13 files changed

+242
-22
lines changed

13 files changed

+242
-22
lines changed

.helix/languages.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,8 @@ lens.enable = false
2424
hover.actions.enable = false
2525

2626
[language-server.rust-analyzer.config.cargo]
27-
features = ["auth_static_fut", "introspection"]
27+
features = ["introspection", "integration"]
28+
29+
[language-server.rust-analyzer.config.check]
30+
command = "clippy"
31+
features = "all"

Cargo.lock

+36
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

justfile

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ fmt: fmt-toml
1717
fmt-toml:
1818
taplo fmt **.toml
1919

20+
# Run integration test
21+
integration:
22+
RUST_LOG="syndterm,integration=debug" cargo nextest run --package syndterm --features integration --test integration --no-capture
2023

2124
update-gql-schema:
2225
@graphql-client introspect-schema http://localhost:5959/graphql \

syndapi/Cargo.toml

-1
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,3 @@ pin-project = "1.1.4"
4949

5050
# Enable graphql introspection
5151
introspection = []
52-
auth_static_fut = []

syndterm/Cargo.toml

+19-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ license.workspace = true
77
categories.workspace = true
88
repository.workspace = true
99

10+
[[bin]]
11+
name = "synd"
12+
path = "src/main.rs"
13+
1014
[dependencies]
1115
anyhow = { workspace = true }
1216
clap = { workspace = true, features = ["derive", "string"] }
@@ -27,13 +31,24 @@ reqwest = { workspace = true }
2731
serde = { workspace = true, features = ["derive"] }
2832
serde_json = "1.0.111"
2933
synd = { path = "../synd" }
30-
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync"] }
34+
tokio = { workspace = true, features = [
35+
"macros",
36+
"rt-multi-thread",
37+
"sync",
38+
"time",
39+
] }
3140
tracing = { workspace = true }
3241
tracing-appender = "0.2.3"
3342
tracing-subscriber = { workspace = true }
3443
url = { workspace = true }
3544
unicode-segmentation = "1.10.1"
3645

37-
[[bin]]
38-
name = "synd"
39-
path = "src/main.rs"
46+
[features]
47+
# Integration test
48+
integration = []
49+
50+
[dev-dependencies]
51+
serial_test = { version = "3.0.0", default_features = false, features = [
52+
"async",
53+
"file_locks",
54+
] }

syndterm/src/application/mod.rs

+62-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
use std::{pin::Pin, time::Duration};
2+
3+
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind};
4+
use futures_util::{FutureExt, Stream, StreamExt};
5+
use tokio::time::{Instant, Sleep};
6+
17
use crate::{
28
auth::{
39
self,
@@ -18,8 +24,6 @@ use crate::{
1824
theme::Theme,
1925
},
2026
};
21-
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind};
22-
use futures_util::{FutureExt, Stream, StreamExt};
2327

2428
mod direction;
2529
pub use direction::{Direction, IndexOutOfRange};
@@ -58,10 +62,17 @@ pub struct Application {
5862
jobs: Jobs,
5963
state: State,
6064
theme: Theme,
65+
idle_timer: Pin<Box<Sleep>>,
66+
6167
should_render: bool,
6268
should_quit: bool,
6369
}
6470

71+
#[derive(PartialEq, Eq)]
72+
pub enum EventLoopControlFlow {
73+
Quit,
74+
}
75+
6576
impl Application {
6677
pub fn new(terminal: Terminal, client: Client) -> Self {
6778
let state = State {
@@ -82,6 +93,7 @@ impl Application {
8293
jobs: Jobs::new(),
8394
state,
8495
theme: Theme::new(),
96+
idle_timer: Box::pin(tokio::time::sleep(Duration::from_millis(250))),
8597
should_quit: false,
8698
should_render: false,
8799
}
@@ -112,8 +124,6 @@ impl Application {
112124
{
113125
self.terminal.init()?;
114126

115-
self.render();
116-
117127
self.event_loop(input).await;
118128

119129
self.terminal.exit()?;
@@ -122,6 +132,19 @@ impl Application {
122132
}
123133

124134
async fn event_loop<S>(&mut self, input: &mut S)
135+
where
136+
S: Stream<Item = std::io::Result<CrosstermEvent>> + Unpin,
137+
{
138+
self.render();
139+
140+
loop {
141+
if self.event_loop_until_idle(input).await == EventLoopControlFlow::Quit {
142+
break;
143+
}
144+
}
145+
}
146+
147+
pub async fn event_loop_until_idle<S>(&mut self, input: &mut S) -> EventLoopControlFlow
125148
where
126149
S: Stream<Item = std::io::Result<CrosstermEvent>> + Unpin,
127150
{
@@ -135,6 +158,9 @@ impl Application {
135158
Some(command) = self.jobs.futures.next() => {
136159
Some(command.unwrap())
137160
}
161+
_ = &mut self.idle_timer => {
162+
Some(Command::Idle)
163+
}
138164
};
139165

140166
if let Some(command) = command {
@@ -148,11 +174,12 @@ impl Application {
148174
}
149175

150176
if self.should_quit {
151-
break;
177+
break EventLoopControlFlow::Quit;
152178
}
153179
}
154180
}
155181

182+
#[tracing::instrument(skip_all,fields(%command))]
156183
fn apply(&mut self, command: Command) {
157184
tracing::debug!("Apply {command:?}");
158185

@@ -166,6 +193,9 @@ impl Application {
166193
Command::ResizeTerminal { .. } => {
167194
self.should_render = true;
168195
}
196+
Command::Idle => {
197+
self.handle_idle();
198+
}
169199
Command::Authenticate(method) => self.authenticate(method),
170200
Command::DeviceAuthorizationFlow(device_authorization) => {
171201
self.device_authorize_flow(device_authorization)
@@ -429,6 +459,7 @@ impl Application {
429459
.device_authorize_request()
430460
.await
431461
.unwrap();
462+
432463
Ok(Command::DeviceAuthorizationFlow(res))
433464
}
434465
.boxed();
@@ -472,3 +503,29 @@ impl Application {
472503
self.set_auth(auth);
473504
}
474505
}
506+
507+
impl Application {
508+
fn handle_idle(&mut self) {
509+
self.reset_idle_timer();
510+
511+
#[cfg(feature = "integration")]
512+
{
513+
self.should_render = true;
514+
self.should_quit = true;
515+
}
516+
}
517+
518+
fn reset_idle_timer(&mut self) {
519+
// https://github.com/tokio-rs/tokio/blob/e53b92a9939565edb33575fff296804279e5e419/tokio/src/time/instant.rs#L62
520+
self.idle_timer
521+
.as_mut()
522+
.reset(Instant::now() + Duration::from_secs(86400 * 365 * 30));
523+
}
524+
}
525+
526+
#[cfg(feature = "integration")]
527+
impl Application {
528+
pub fn assert_buffer(&self, expected: &ratatui::buffer::Buffer) {
529+
self.terminal.assert_buffer(expected)
530+
}
531+
}

syndterm/src/command.rs

+13
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::fmt::Display;
2+
13
use crate::{
24
application::{AuthenticateMethod, Direction},
35
auth::device_flow::{DeviceAccessTokenResponse, DeviceAuthorizationResponse},
@@ -9,6 +11,7 @@ use crate::{
911
pub enum Command {
1012
Quit,
1113
ResizeTerminal { columns: u16, rows: u16 },
14+
Idle,
1215

1316
Authenticate(AuthenticateMethod),
1417
DeviceAuthorizationFlow(DeviceAuthorizationResponse),
@@ -36,3 +39,13 @@ pub enum Command {
3639

3740
HandleError { message: String },
3841
}
42+
43+
impl Display for Command {
44+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45+
match self {
46+
Command::UpdateSubscription(_) => f.write_str("UpdateSubscription"),
47+
Command::UpdateEntries(_) => f.write_str("UpdateEntries"),
48+
cmd => write!(f, "{:?}", cmd),
49+
}
50+
}
51+
}

syndterm/src/main.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,7 @@ async fn main() {
5454
let _guard = init_tracing(log).unwrap();
5555

5656
let mut app = {
57-
let terminal =
58-
Terminal::from_stdout(std::io::stdout()).expect("Failed to construct terminal");
57+
let terminal = Terminal::new().expect("Failed to construct terminal");
5958
let client = Client::new(endpoint).expect("Failed to construct client");
6059
Application::new(terminal, client)
6160
};

syndterm/src/terminal/backend.rs

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
use ratatui::backend::CrosstermBackend;
2+
3+
pub type TerminalBackend = CrosstermBackend<std::io::Stdout>;
4+
5+
pub fn new_backend() -> TerminalBackend {
6+
CrosstermBackend::new(std::io::stdout())
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use ratatui::backend::TestBackend;
2+
3+
pub type Buffer = ratatui::buffer::Buffer;
4+
5+
pub type TerminalBackend = TestBackend;
6+
7+
pub fn new_backend() -> TerminalBackend {
8+
TestBackend::new(10, 10)
9+
}

syndterm/src/terminal.rs syndterm/src/terminal/mod.rs

+22-9
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,32 @@
11
use anyhow::Result;
22
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
3-
use ratatui::{backend::CrosstermBackend, Frame};
3+
use ratatui::Frame;
44
use std::io;
55

6-
type TerminalBackend = ratatui::Terminal<CrosstermBackend<io::Stdout>>;
6+
#[cfg(not(feature = "integration"))]
7+
mod backend;
8+
#[cfg(not(feature = "integration"))]
9+
pub use backend::{new_backend, TerminalBackend};
10+
11+
#[cfg(feature = "integration")]
12+
mod integration_backend;
13+
#[cfg(feature = "integration")]
14+
pub use integration_backend::{new_backend, Buffer, TerminalBackend};
715

816
/// Provide terminal manipulation operations.
917
pub struct Terminal {
10-
backend: TerminalBackend,
18+
backend: ratatui::Terminal<TerminalBackend>,
1119
}
1220

1321
impl Terminal {
14-
pub fn new(backend: TerminalBackend) -> Self {
15-
Self { backend }
22+
/// Construct Terminal with default backend
23+
pub fn new() -> anyhow::Result<Self> {
24+
let backend = new_backend();
25+
Ok(Terminal::with(ratatui::Terminal::new(backend)?))
1626
}
1727

18-
/// Construct Terminal from stdout
19-
pub fn from_stdout(out: io::Stdout) -> Result<Self> {
20-
let backend = CrosstermBackend::new(out);
21-
Ok(Terminal::new(ratatui::Terminal::new(backend)?))
28+
pub fn with(backend: ratatui::Terminal<TerminalBackend>) -> Self {
29+
Self { backend }
2230
}
2331

2432
/// Initialize terminal
@@ -59,4 +67,9 @@ impl Terminal {
5967
self.backend.draw(f)?;
6068
Ok(())
6169
}
70+
71+
#[cfg(feature = "integration")]
72+
pub fn assert_buffer(&self, expected: &Buffer) {
73+
self.backend.backend().assert_buffer(expected)
74+
}
6275
}

0 commit comments

Comments
 (0)