upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/test_utils/src
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2023-09-01 00:00:00 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2023-09-13 09:24:49 +0000
commit6423baebd92e45c9be85157c443dff42e65d8d14 (patch)
tree6548edfd80d0cd9d1267378ebe816ec95e394137 /test_utils/src
parent5c5feaa732363e32e2a980a887fa42b4394b1a5e (diff)
refactor: rebuild app skeleton
Create skeleton for a complete rebuild of the prototype as a production ready product. Includes design patterns for: - dependency injection - unit testing with dependency mocking - integration testing - error handling - config storage BREAKING-CHANGE: ground-up redesign with incompatible protocol standards
Diffstat (limited to 'test_utils/src')
-rw-r--r--test_utils/src/lib.rs280
1 files changed, 280 insertions, 0 deletions
diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs
new file mode 100644
index 0000000..495e8d2
--- /dev/null
+++ b/test_utils/src/lib.rs
@@ -0,0 +1,280 @@
1use std::ffi::OsStr;
2
3use anyhow::{ensure, Context, Result};
4use dialoguer::theme::{ColorfulTheme, Theme};
5use directories::ProjectDirs;
6use rexpect::session::{Options, PtySession};
7use strip_ansi_escapes::strip_str;
8
9pub static TEST_KEY_1_NSEC: &str =
10 "nsec1ppsg5sm2aexq06juxmu9evtutr6jkwkhp98exxxvwamhru9lyx9s3rwseq";
11
12pub static TEST_KEY_2_NSEC: &str =
13 "nsec1ypglg6nj6ep0g2qmyfqcv2al502gje3jvpwye6mthmkvj93tqkesknv6qm";
14
15/// wrapper for a cli testing tool - currently wraps rexpect and dialoguer
16///
17/// 1. allow more accurate articulation of expected behaviour
18/// 2. provide flexibility to swap rexpect for a tool that better maps to
19/// expected behaviour
20/// 3. provides flexability to swap dialoguer with another cli interaction tool
21pub struct CliTester {
22 rexpect_session: PtySession,
23 formatter: ColorfulTheme,
24}
25
26impl CliTester {
27 pub fn expect_input(&mut self, prompt: &str) -> Result<CliTesterInputPrompt> {
28 let mut i = CliTesterInputPrompt {
29 tester: self,
30 prompt: prompt.to_string(),
31 };
32 i.prompt(false).context("initial input prompt")?;
33 Ok(i)
34 }
35
36 pub fn expect_input_eventually(&mut self, prompt: &str) -> Result<CliTesterInputPrompt> {
37 let mut i = CliTesterInputPrompt {
38 tester: self,
39 prompt: prompt.to_string(),
40 };
41 i.prompt(true).context("initial input prompt")?;
42 Ok(i)
43 }
44}
45
46pub struct CliTesterInputPrompt<'a> {
47 tester: &'a mut CliTester,
48 prompt: String,
49}
50
51impl CliTesterInputPrompt<'_> {
52 fn prompt(&mut self, eventually: bool) -> Result<&mut Self> {
53 let mut s = String::new();
54 self.tester
55 .formatter
56 .format_prompt(&mut s, self.prompt.as_str())
57 .expect("diagluer theme formatter should succeed");
58 s.push(' ');
59
60 ensure!(
61 s.contains(self.prompt.as_str()),
62 "dialoguer must be broken as formatted prompt success doesnt contain prompt"
63 );
64
65 if eventually {
66 self.tester
67 .expect_eventually(sanatize(s).as_str())
68 .context("expect input prompt eventually")?;
69 } else {
70 self.tester
71 .expect(sanatize(s).as_str())
72 .context("expect input prompt")?;
73 }
74
75 Ok(self)
76 }
77
78 pub fn succeeds_with(&mut self, input: &str) -> Result<&mut Self> {
79 self.tester.send_line(input)?;
80 self.tester
81 .expect(input)
82 .context("expect input to be printed")?;
83 self.tester
84 .expect("\r")
85 .context("expect new line after input to be printed")?;
86
87 let mut s = String::new();
88 self.tester
89 .formatter
90 .format_input_prompt_selection(&mut s, self.prompt.as_str(), input)
91 .expect("diagluer theme formatter should succeed");
92 if !s.contains(self.prompt.as_str()) {
93 panic!("dialoguer must be broken as formatted prompt success doesnt contain prompt");
94 }
95 let formatted_success = format!("{}\r\n", sanatize(s));
96
97 self.tester
98 .expect(formatted_success.as_str())
99 .context("expect immediate prompt success")?;
100 Ok(self)
101 }
102}
103
104impl CliTester {
105 pub fn new<I, S>(args: I) -> Self
106 where
107 I: IntoIterator<Item = S>,
108 S: AsRef<OsStr>,
109 {
110 Self {
111 rexpect_session: rexpect_with(args).expect("rexpect to spawn new process"),
112 formatter: ColorfulTheme::default(),
113 }
114 }
115
116 pub fn restart_with<I, S>(&mut self, args: I) -> &mut Self
117 where
118 I: IntoIterator<Item = S>,
119 S: AsRef<OsStr>,
120 {
121 self.rexpect_session
122 .process
123 .exit()
124 .expect("process to exit");
125 self.rexpect_session = rexpect_with(args).expect("rexpect to spawn new process");
126 self
127 }
128
129 pub fn exit(&mut self) -> Result<()> {
130 match self
131 .rexpect_session
132 .process
133 .exit()
134 .context("expect proccess to exit")
135 {
136 Ok(_) => Ok(()),
137 Err(e) => Err(e),
138 }
139 }
140
141 /// returns what came before expected message
142 pub fn expect_eventually(&mut self, message: &str) -> Result<String> {
143 let before = self
144 .rexpect_session
145 .exp_string(message)
146 .context("exp_string failed")?;
147 Ok(before)
148 }
149
150 pub fn expect(&mut self, message: &str) -> Result<&mut Self> {
151 let before = self.expect_eventually(message)?;
152 ensure!(
153 before.is_empty(),
154 format!(
155 "expected message \"{}\". but got \"{}\" first.",
156 message.replace('\n', "\\n").replace('\r', "\\r"),
157 before.replace('\n', "\\n").replace('\r', "\\r"),
158 ),
159 );
160 Ok(self)
161 }
162
163 pub fn expect_end(&mut self) -> Result<()> {
164 let before = self
165 .rexpect_session
166 .exp_eof()
167 .context("expected immediate end but got timed out")?;
168 ensure!(
169 before.is_empty(),
170 format!(
171 "expected immediate end but got '{}' first.",
172 before.replace('\n', "\\n").replace('\r', "\\r"),
173 ),
174 );
175 Ok(())
176 }
177
178 pub fn expect_end_with(&mut self, message: &str) -> Result<()> {
179 let before = self
180 .rexpect_session
181 .exp_eof()
182 .context("expected immediate end but got timed out")?;
183 assert_eq!(before, message);
184 Ok(())
185 }
186 pub fn expect_end_eventually(&mut self) -> Result<String> {
187 self.rexpect_session
188 .exp_eof()
189 .context("expected end eventually but got timed out")
190 }
191
192 pub fn expect_end_eventually_with(&mut self, message: &str) -> Result<()> {
193 self.expect_eventually(message)?;
194 self.expect_end()
195 }
196
197 fn send_line(&mut self, line: &str) -> Result<()> {
198 self.rexpect_session
199 .send_line(line)
200 .context("send_line failed")?;
201 Ok(())
202 }
203}
204
205/// sanatize unicode string for rexpect
206fn sanatize(s: String) -> String {
207 // remove ansi codes as they don't work with rexpect
208 strip_str(s)
209 // sanatize unicode rexpect issue 105 is resolved https://github.com/rust-cli/rexpect/issues/105
210 .as_bytes()
211 .iter()
212 .map(|c| *c as char)
213 .collect::<String>()
214}
215
216pub fn rexpect_with<I, S>(args: I) -> Result<PtySession, rexpect::error::Error>
217where
218 I: IntoIterator<Item = S>,
219 S: AsRef<std::ffi::OsStr>,
220{
221 let mut cmd = std::process::Command::new(assert_cmd::cargo::cargo_bin("ngit"));
222 cmd.args(args);
223 // using branch for PR https://github.com/rust-cli/rexpect/pull/103 to strip ansi escape codes
224 rexpect::session::spawn_with_options(
225 cmd,
226 Options {
227 timeout_ms: Some(2000),
228 strip_ansi_escape_codes: true,
229 },
230 )
231}
232
233/// backup and remove application config and data
234pub fn before() -> Result<()> {
235 backup_existing_config()
236}
237
238/// restore backuped application config and data
239pub fn after() -> Result<()> {
240 restore_config_backup()
241}
242
243/// run func between before and after scripts which backup, reset and restore
244/// application config
245///
246/// TODO: fix issue: if func panics, after() is not run.
247pub fn with_fresh_config<F>(func: F) -> Result<()>
248where
249 F: Fn() -> Result<()>,
250{
251 before()?;
252 func()?;
253 after()
254}
255
256fn backup_existing_config() -> Result<()> {
257 let config_path = get_dirs().config_dir().join("config.json");
258 let backup_config_path = get_dirs().config_dir().join("config-backup.json");
259 if config_path.exists() {
260 std::fs::rename(config_path, backup_config_path)?;
261 }
262 Ok(())
263}
264
265fn restore_config_backup() -> Result<()> {
266 let config_path = get_dirs().config_dir().join("config.json");
267 let backup_config_path = get_dirs().config_dir().join("config-backup.json");
268 if config_path.exists() {
269 std::fs::remove_file(&config_path)?;
270 }
271 if backup_config_path.exists() {
272 std::fs::rename(backup_config_path, config_path)?;
273 }
274 Ok(())
275}
276
277fn get_dirs() -> ProjectDirs {
278 ProjectDirs::from("", "CodeCollaboration", "ngit")
279 .expect("rust directories crate should return ProjectDirs")
280}