Skip to content

Commit 39635a9

Browse files
武宮誠武宮誠
authored andcommitted
update tests
1 parent d57b2f5 commit 39635a9

11 files changed

Lines changed: 427 additions & 135 deletions

File tree

crates/iroha_cli/tests/cli_smoke.rs

Lines changed: 177 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@
77
#![allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::restriction)]
88

99
use std::{
10+
ffi::OsStr,
1011
fs,
12+
io::{self, Read},
1113
path::{Path, PathBuf},
12-
process::Command,
14+
process::{Command, ExitStatus, Output, Stdio},
1315
sync::LazyLock,
16+
thread,
17+
time::{Duration, Instant},
1418
};
1519

1620
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
@@ -359,8 +363,124 @@ fn settlement_instruction(instruction: &InstructionBox) -> &SettlementInstructio
359363
.expect("settlement instruction payload")
360364
}
361365

362-
fn command() -> Command {
363-
let mut cmd = Command::new(cli_binary());
366+
const CLI_COMMAND_TIMEOUT: Duration = Duration::from_secs(30);
367+
const CLI_COMMAND_POLL_INTERVAL: Duration = Duration::from_millis(10);
368+
369+
struct TestCommand {
370+
inner: Command,
371+
timeout: Duration,
372+
}
373+
374+
impl TestCommand {
375+
fn new(program: impl AsRef<OsStr>) -> Self {
376+
Self {
377+
inner: Command::new(program),
378+
timeout: CLI_COMMAND_TIMEOUT,
379+
}
380+
}
381+
382+
fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
383+
self.inner.arg(arg);
384+
self
385+
}
386+
387+
fn args<I, S>(&mut self, args: I) -> &mut Self
388+
where
389+
I: IntoIterator<Item = S>,
390+
S: AsRef<OsStr>,
391+
{
392+
self.inner.args(args);
393+
self
394+
}
395+
396+
fn current_dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
397+
self.inner.current_dir(dir);
398+
self
399+
}
400+
401+
fn env(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {
402+
self.inner.env(key, value);
403+
self
404+
}
405+
406+
fn output(&mut self) -> io::Result<Output> {
407+
self.inner
408+
.stdin(Stdio::null())
409+
.stdout(Stdio::piped())
410+
.stderr(Stdio::piped());
411+
let mut child = self.inner.spawn()?;
412+
let stdout = child.stdout.take().map(read_pipe);
413+
let stderr = child.stderr.take().map(read_pipe);
414+
let Some(status) = wait_for_exit(&mut child, self.timeout)? else {
415+
let _ = child.kill();
416+
let _ = child.wait();
417+
let _ = join_pipe(stdout);
418+
let _ = join_pipe(stderr);
419+
return Err(command_timeout_error(&self.inner, self.timeout));
420+
};
421+
422+
Ok(Output {
423+
status,
424+
stdout: join_pipe(stdout),
425+
stderr: join_pipe(stderr),
426+
})
427+
}
428+
429+
fn status(&mut self) -> io::Result<ExitStatus> {
430+
self.inner.stdin(Stdio::null());
431+
let mut child = self.inner.spawn()?;
432+
let Some(status) = wait_for_exit(&mut child, self.timeout)? else {
433+
let _ = child.kill();
434+
let _ = child.wait();
435+
return Err(command_timeout_error(&self.inner, self.timeout));
436+
};
437+
Ok(status)
438+
}
439+
}
440+
441+
fn read_pipe<R>(mut pipe: R) -> thread::JoinHandle<Vec<u8>>
442+
where
443+
R: Read + Send + 'static,
444+
{
445+
thread::spawn(move || {
446+
let mut bytes = Vec::new();
447+
let _ = pipe.read_to_end(&mut bytes);
448+
bytes
449+
})
450+
}
451+
452+
fn join_pipe(handle: Option<thread::JoinHandle<Vec<u8>>>) -> Vec<u8> {
453+
handle
454+
.and_then(|handle| handle.join().ok())
455+
.unwrap_or_default()
456+
}
457+
458+
fn wait_for_exit(
459+
child: &mut std::process::Child,
460+
timeout: Duration,
461+
) -> io::Result<Option<ExitStatus>> {
462+
let started = Instant::now();
463+
loop {
464+
if let Some(status) = child.try_wait()? {
465+
return Ok(Some(status));
466+
}
467+
let elapsed = started.elapsed();
468+
if elapsed >= timeout {
469+
return Ok(None);
470+
}
471+
thread::sleep(CLI_COMMAND_POLL_INTERVAL.min(timeout - elapsed));
472+
}
473+
}
474+
475+
fn command_timeout_error(command: &Command, timeout: Duration) -> io::Error {
476+
io::Error::new(
477+
io::ErrorKind::TimedOut,
478+
format!("command {command:?} timed out after {timeout:?}"),
479+
)
480+
}
481+
482+
fn command() -> TestCommand {
483+
let mut cmd = TestCommand::new(cli_binary());
364484
// Disable ANSI color codes so the assertions can match plain text.
365485
cmd.env("NO_COLOR", "1");
366486
cmd.env("CLICOLOR", "0");
@@ -4949,7 +5069,7 @@ fn address_convert_outputs_i105_by_default() {
49495069
let expected_i105 =
49505070
encode_account_id_to_i105_for_discriminant(&account, 753).expect("i105 string");
49515071

4952-
let output = Command::new(cli_binary())
5072+
let output = command()
49535073
.args(["tools", "address", "convert", &expected_i105])
49545074
.output()
49555075
.expect("run address convert");
@@ -4983,7 +5103,7 @@ fn address_convert_json_summary_contains_i105_and_canonical_hex() {
49835103
let i105 = encode_account_id_to_i105_for_discriminant(&account, 753).expect("i105 string");
49845104
let canonical = encode_account_id_to_canonical_hex(&account).expect("canonical");
49855105

4986-
let output = Command::new(cli_binary())
5106+
let output = command()
49875107
.args([
49885108
"tools",
49895109
"address",
@@ -5051,7 +5171,7 @@ fn address_convert_rejects_domain_suffix() {
50515171
let i105 = encode_account_id_to_i105_for_discriminant(&account, 753).expect("i105");
50525172
let literal = format!("{i105}@{domain}");
50535173

5054-
let output = Command::new(cli_binary())
5174+
let output = command()
50555175
.current_dir(workspace_root())
50565176
.args([
50575177
"--config",
@@ -5085,7 +5205,7 @@ fn address_convert_json_rejects_domain_suffix() {
50855205
let i105 = encode_account_id_to_i105_for_discriminant(&account, 753).expect("i105");
50865206
let literal = format!("{i105}@universal");
50875207

5088-
let output = Command::new(cli_binary())
5208+
let output = command()
50895209
.current_dir(workspace_root())
50905210
.args([
50915211
"--config",
@@ -5121,7 +5241,7 @@ fn address_convert_json_summary_is_domainless() {
51215241
let account = AccountId::new(key_pair.public_key().clone());
51225242
let i105 = encode_account_id_to_i105_for_discriminant(&account, 753).expect("i105");
51235243

5124-
let output = Command::new(cli_binary())
5244+
let output = command()
51255245
.current_dir(workspace_root())
51265246
.args([
51275247
"--config",
@@ -5167,7 +5287,7 @@ fn address_audit_reports_parsed_and_errors() {
51675287
let contents = format!("# sample addresses\n{local_i105}\n{default_i105}\ninvalid-address\n");
51685288
fs::write(&input_path, contents).expect("write addresses");
51695289

5170-
let output = Command::new(cli_binary())
5290+
let output = command()
51715291
.current_dir(workspace_root())
51725292
.args([
51735293
"--config",
@@ -5219,7 +5339,7 @@ fn address_audit_rejects_domain_suffix() {
52195339
let path = temp_dir.path().join("addresses.txt");
52205340
fs::write(&path, format!("{literal}\n")).expect("write addresses");
52215341

5222-
let output = Command::new(cli_binary())
5342+
let output = command()
52235343
.current_dir(workspace_root())
52245344
.args([
52255345
"--config",
@@ -5258,7 +5378,7 @@ fn address_audit_supports_csv_output() {
52585378
let path = temp_dir.path().join("addresses.txt");
52595379
fs::write(&path, format!("{i105}\ninvalid-address\n")).expect("write addresses");
52605380

5261-
let output = Command::new(cli_binary())
5381+
let output = command()
52625382
.current_dir(workspace_root())
52635383
.args([
52645384
"--config",
@@ -5430,7 +5550,7 @@ fn space_directory_manifest_audit_bundle_cli() {
54305550
let bundle_dir = temp_dir.path().join("bundle");
54315551
let bundle_dir_str = bundle_dir.to_str().expect("bundle path utf-8").to_owned();
54325552

5433-
let status = Command::new(cli_binary())
5553+
let status = command()
54345554
.args([
54355555
"app",
54365556
"space-directory",
@@ -5496,13 +5616,16 @@ mod torii_mock_support {
54965616
net::{TcpListener, TcpStream},
54975617
path::{Path, PathBuf},
54985618
process::{Command, Stdio},
5619+
sync::mpsc,
54995620
thread,
5500-
time::{SystemTime, UNIX_EPOCH},
5621+
time::{Duration, SystemTime, UNIX_EPOCH},
55015622
};
55025623

55035624
use norito::json;
55045625
use url::Url;
55055626

5627+
const MOCK_STARTUP_TIMEOUT: Duration = Duration::from_secs(10);
5628+
55065629
#[derive(Debug)]
55075630
pub enum SpawnError {
55085631
PythonUnavailable,
@@ -5575,35 +5698,66 @@ mod torii_mock_support {
55755698
.stdout
55765699
.take()
55775700
.ok_or_else(|| SpawnError::Setup("missing stdout pipe".into()))?;
5578-
let mut reader = BufReader::new(stdout);
5579-
let mut line = String::new();
5580-
match reader.read_line(&mut line) {
5581-
Ok(0) => {
5701+
let (line_tx, line_rx) = mpsc::channel();
5702+
let startup_thread = thread::spawn(move || {
5703+
let mut reader = BufReader::new(stdout);
5704+
let mut line = String::new();
5705+
let result = reader.read_line(&mut line).map(|read| (read, line, reader));
5706+
let _ = line_tx.send(result);
5707+
});
5708+
match line_rx.recv_timeout(MOCK_STARTUP_TIMEOUT) {
5709+
Ok(Ok((0, _, _))) => {
5710+
let _ = child.kill();
5711+
let _ = child.wait();
5712+
let _ = startup_thread.join();
55825713
last_error = Some(io::Error::new(
55835714
io::ErrorKind::UnexpectedEof,
55845715
"torii mock exited early",
55855716
));
5586-
let _ = child.wait();
55875717
}
5588-
Ok(_) => {
5589-
let base_url = parse_base_url(line.trim())?;
5590-
let captured_url = base_url.clone();
5718+
Ok(Ok((_, line, reader))) => {
5719+
let _ = startup_thread.join();
5720+
let base_url = match parse_base_url(line.trim()) {
5721+
Ok(base_url) => base_url,
5722+
Err(err) => {
5723+
let _ = child.kill();
5724+
let _ = child.wait();
5725+
return Err(err);
5726+
}
5727+
};
55915728
let stdout_thread = thread::spawn(move || {
55925729
let mut reader = reader;
55935730
let mut sink = io::sink();
55945731
let _ = io::copy(&mut reader, &mut sink);
55955732
});
55965733
return Ok(Self {
55975734
child,
5598-
base_url: captured_url,
5735+
base_url,
55995736
stdout_thread: Some(stdout_thread),
56005737
});
56015738
}
5602-
Err(err) => {
5739+
Ok(Err(err)) => {
5740+
let _ = startup_thread.join();
56035741
let _ = child.kill();
56045742
let _ = child.wait();
56055743
return Err(SpawnError::Io(err));
56065744
}
5745+
Err(mpsc::RecvTimeoutError::Timeout) => {
5746+
let _ = child.kill();
5747+
let _ = child.wait();
5748+
let _ = startup_thread.join();
5749+
return Err(SpawnError::Setup(format!(
5750+
"torii mock did not announce base_url within {MOCK_STARTUP_TIMEOUT:?}"
5751+
)));
5752+
}
5753+
Err(mpsc::RecvTimeoutError::Disconnected) => {
5754+
let _ = child.kill();
5755+
let _ = child.wait();
5756+
let _ = startup_thread.join();
5757+
return Err(SpawnError::Setup(
5758+
"torii mock startup reader stopped before base_url".into(),
5759+
));
5760+
}
56075761
}
56085762
}
56095763
if let Some(err) = last_error {

0 commit comments

Comments
 (0)