Skip to content

Commit 5bec50f

Browse files
authored
feat(pop-cli): --json support for test command (#992)
* feat(pop-cli): add --json support for test commands * fix(pop-cli): emit json envelope for test in no-feature build * fix(pop-cli): scope json cli to feature-enabled test subcommands * fix(pop-cli): split json test cfg handling for contract-only build * test(pop-cli): add contract-only json test envelope regression * fix(pop-cli): use typed prompt-required errors in json mode
1 parent 1357653 commit 5bec50f

File tree

13 files changed

+569
-63
lines changed

13 files changed

+569
-63
lines changed

crates/pop-chains/src/try_runtime/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ pub fn run_try_runtime(
141141
// Check if the command failed.
142142
handle_command_error(&output, Error::TryRuntimeError)?;
143143
if output.status.success() {
144-
println!("{}", String::from_utf8_lossy(&output.stderr));
144+
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
145145
}
146146
Ok(())
147147
}

crates/pop-cli/src/cli.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -465,17 +465,14 @@ impl traits::Cli for JsonCli {
465465
}
466466
}
467467

468-
#[allow(dead_code)]
469-
const JSON_PROMPT_ERR: &str = "interactive prompt required but --json mode is active";
470-
471468
#[allow(dead_code)]
472469
struct JsonConfirm;
473470
impl traits::Confirm for JsonConfirm {
474471
fn initial_value(self, _initial_value: bool) -> Self {
475472
self
476473
}
477474
fn interact(&mut self) -> Result<bool> {
478-
Err(std::io::Error::other(JSON_PROMPT_ERR))
475+
Err(crate::output::prompt_required_io_error())
479476
}
480477
}
481478

@@ -486,7 +483,7 @@ impl traits::Input for JsonInput {
486483
self
487484
}
488485
fn interact(&mut self) -> Result<String> {
489-
Err(std::io::Error::other(JSON_PROMPT_ERR))
486+
Err(crate::output::prompt_required_io_error())
490487
}
491488
fn placeholder(self, _value: &str) -> Self {
492489
self
@@ -506,7 +503,7 @@ impl traits::Input for JsonInput {
506503
struct JsonMultiSelect<T>(std::marker::PhantomData<T>);
507504
impl<T: Clone + Eq> traits::MultiSelect<T> for JsonMultiSelect<T> {
508505
fn interact(&mut self) -> Result<Vec<T>> {
509-
Err(std::io::Error::other(JSON_PROMPT_ERR))
506+
Err(crate::output::prompt_required_io_error())
510507
}
511508
fn item(self, _value: T, _label: impl Display, _hint: impl Display) -> Self {
512509
self
@@ -523,7 +520,7 @@ impl<T: Clone + Eq> traits::MultiSelect<T> for JsonMultiSelect<T> {
523520
struct JsonPassword;
524521
impl traits::Password for JsonPassword {
525522
fn interact(&mut self) -> Result<String> {
526-
Err(std::io::Error::other(JSON_PROMPT_ERR))
523+
Err(crate::output::prompt_required_io_error())
527524
}
528525
}
529526

@@ -534,7 +531,7 @@ impl<T: Clone + Eq> traits::Select<T> for JsonSelect<T> {
534531
self
535532
}
536533
fn interact(&mut self) -> Result<T> {
537-
Err(std::io::Error::other(JSON_PROMPT_ERR))
534+
Err(crate::output::prompt_required_io_error())
538535
}
539536
fn item(self, _value: T, _label: impl Display, _hint: impl Display) -> Self {
540537
self

crates/pop-cli/src/commands/mod.rs

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::cli::Cli;
66
use crate::cli::traits::Cli as _;
77
use crate::{
88
cache,
9-
output::{OutputMode, reject_unsupported_json},
9+
output::{CliResponse, OutputMode, reject_unsupported_json},
1010
};
1111
#[cfg(any(feature = "chain", feature = "contract"))]
1212
use pop_common::templates::Template;
@@ -118,6 +118,7 @@ impl Command {
118118
match self {
119119
Self::Hash(_) |
120120
Self::Convert(_) |
121+
Self::Test(_) |
121122
Self::Clean(_) |
122123
Self::Completion(_) |
123124
Self::Upgrade(_) => true,
@@ -274,26 +275,85 @@ impl Command {
274275
},
275276
Self::Test(args) => {
276277
env_logger::init();
278+
if output_mode == OutputMode::Json {
279+
#[cfg(feature = "chain")]
280+
{
281+
let mut json_cli = crate::cli::JsonCli;
282+
match &mut args.command {
283+
None => {
284+
let output = test::Command::execute(args, output_mode).await?;
285+
CliResponse::ok(output).print_json();
286+
return Ok(());
287+
},
288+
Some(cmd) => {
289+
let runtime_output = match cmd {
290+
test::Command::OnRuntimeUpgrade(cmd) =>
291+
cmd.execute(&mut json_cli, output_mode).await?,
292+
test::Command::ExecuteBlock(cmd) =>
293+
cmd.execute(&mut json_cli, output_mode).await?,
294+
test::Command::CreateSnapshot(cmd) =>
295+
cmd.execute(&mut json_cli, output_mode).await?,
296+
test::Command::FastForward(cmd) =>
297+
cmd.execute(&mut json_cli, output_mode).await?,
298+
};
299+
CliResponse::ok(runtime_output).print_json();
300+
return Ok(());
301+
},
302+
}
303+
}
304+
305+
#[cfg(all(feature = "contract", not(feature = "chain")))]
306+
{
307+
let output = test::Command::execute(args, output_mode).await?;
308+
CliResponse::ok(output).print_json();
309+
return Ok(());
310+
}
311+
312+
#[cfg(not(any(feature = "contract", feature = "chain")))]
313+
{
314+
let output = test::Command::execute(args, output_mode).await?;
315+
CliResponse::ok(output).print_json();
316+
return Ok(());
317+
}
318+
}
277319

278320
#[cfg(any(feature = "contract", feature = "chain"))]
279321
match &mut args.command {
280-
None => test::Command::execute(args).await,
322+
None => {
323+
test::Command::execute(args, output_mode).await?;
324+
Ok(())
325+
},
281326
Some(cmd) => match cmd {
282327
#[cfg(feature = "chain")]
283-
test::Command::OnRuntimeUpgrade(cmd) => cmd.execute(&mut Cli).await,
328+
test::Command::OnRuntimeUpgrade(cmd) => {
329+
cmd.execute(&mut Cli, output_mode).await?;
330+
Ok(())
331+
},
284332
#[cfg(feature = "chain")]
285-
test::Command::ExecuteBlock(cmd) => cmd.execute(&mut Cli).await,
333+
test::Command::ExecuteBlock(cmd) => {
334+
cmd.execute(&mut Cli, output_mode).await?;
335+
Ok(())
336+
},
286337
#[cfg(feature = "chain")]
287-
test::Command::CreateSnapshot(cmd) => cmd.execute(&mut Cli).await,
338+
test::Command::CreateSnapshot(cmd) => {
339+
cmd.execute(&mut Cli, output_mode).await?;
340+
Ok(())
341+
},
288342
#[cfg(feature = "chain")]
289-
test::Command::FastForward(cmd) => cmd.execute(&mut Cli).await,
343+
test::Command::FastForward(cmd) => {
344+
cmd.execute(&mut Cli, output_mode).await?;
345+
Ok(())
346+
},
290347
#[cfg(not(feature = "chain"))]
291348
_ => Ok(()),
292349
},
293350
}
294351

295352
#[cfg(not(any(feature = "contract", feature = "chain")))]
296-
test::Command::execute(args).await
353+
{
354+
test::Command::execute(args, output_mode).await?;
355+
Ok(())
356+
}
297357
},
298358
Self::Hash(args) => {
299359
env_logger::init();
@@ -564,4 +624,9 @@ mod tests {
564624
.supports_json()
565625
);
566626
}
627+
628+
#[test]
629+
fn test_command_supports_json() {
630+
assert!(Command::Test(test::TestArgs::default()).supports_json());
631+
}
567632
}

crates/pop-cli/src/commands/test/contract.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub(crate) struct TestContractCommand {
1818
pub(crate) path: PathBuf,
1919
/// Run end-to-end tests
2020
#[arg(short, long)]
21-
e2e: bool,
21+
pub(crate) e2e: bool,
2222
/// Path to the contracts node binary to run e2e tests [default: none]
2323
#[arg(short, long)]
2424
node: Option<PathBuf>,

crates/pop-cli/src/commands/test/create_snapshot.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
use crate::{
44
cli::{self, traits::Input},
5+
commands::test::RuntimeTestOutput,
56
common::{
67
prompt::display_message,
78
try_runtime::{ArgumentConstructor, check_try_runtime_and_prompt, collect_args},
89
urls,
910
},
11+
output::{OutputMode, build_error_with_details},
1012
};
1113
use clap::Args;
1214
use console::style;
@@ -37,7 +39,11 @@ pub(crate) struct TestCreateSnapshotCommand {
3739

3840
impl TestCreateSnapshotCommand {
3941
/// Executes the command.
40-
pub(crate) async fn execute(&mut self, cli: &mut impl cli::traits::Cli) -> anyhow::Result<()> {
42+
pub(crate) async fn execute(
43+
&mut self,
44+
cli: &mut impl cli::traits::Cli,
45+
output_mode: OutputMode,
46+
) -> anyhow::Result<RuntimeTestOutput> {
4147
cli.intro("Creating a snapshot file")?;
4248
cli.warning(
4349
"NOTE: `create-snapshot` only works with the remote node. No runtime required.",
@@ -74,7 +80,11 @@ impl TestCreateSnapshotCommand {
7480
// Display the `create-snapshot` command.
7581
cli.info(self.display())?;
7682
if let Err(e) = result {
77-
return display_message(&e.to_string(), false, cli);
83+
if output_mode == OutputMode::Json {
84+
return Err(build_error_with_details("Failed to create snapshot", e.to_string()));
85+
}
86+
display_message(&e.to_string(), false, cli)?;
87+
return Ok(RuntimeTestOutput::success("create-snapshot", self.snapshot_path.clone()));
7888
}
7989
display_message(
8090
&format!(
@@ -90,7 +100,7 @@ impl TestCreateSnapshotCommand {
90100
true,
91101
cli,
92102
)?;
93-
Ok(())
103+
Ok(RuntimeTestOutput::success("create-snapshot", self.snapshot_path.clone()))
94104
}
95105

96106
async fn run(&self, cli: &mut impl cli::traits::Cli) -> anyhow::Result<()> {
@@ -180,6 +190,18 @@ mod tests {
180190
Ok(())
181191
}
182192

193+
#[tokio::test]
194+
async fn create_snapshot_requires_flags_in_json_mode() -> anyhow::Result<()> {
195+
let mut command = TestCreateSnapshotCommand::default();
196+
let error = command
197+
.execute(&mut crate::cli::JsonCli, OutputMode::Json)
198+
.await
199+
.unwrap_err()
200+
.to_string();
201+
assert!(error.contains("interactive prompt required but --json mode is active"));
202+
Ok(())
203+
}
204+
183205
#[test]
184206
fn display_works() {
185207
let mut command = TestCreateSnapshotCommand::default();

crates/pop-cli/src/commands/test/execute_block.rs

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use crate::{
44
cli,
5+
commands::test::RuntimeTestOutput,
56
common::{
67
prompt::display_message,
78
try_runtime::{
@@ -10,6 +11,7 @@ use crate::{
1011
update_live_state, update_runtime_source,
1112
},
1213
},
14+
output::{OutputMode, build_error_with_details, invalid_input_error},
1315
};
1416
use clap::Args;
1517
use pop_chains::{
@@ -51,15 +53,20 @@ pub(crate) struct TestExecuteBlockCommand {
5153
}
5254

5355
impl TestExecuteBlockCommand {
54-
pub(crate) async fn execute(&mut self, cli: &mut impl cli::traits::Cli) -> anyhow::Result<()> {
55-
self.execute_block(cli, std::env::args().skip(3).collect()).await
56+
pub(crate) async fn execute(
57+
&mut self,
58+
cli: &mut impl cli::traits::Cli,
59+
output_mode: OutputMode,
60+
) -> anyhow::Result<RuntimeTestOutput> {
61+
self.execute_block(cli, std::env::args().skip(3).collect(), output_mode).await
5662
}
5763

5864
async fn execute_block(
5965
&mut self,
6066
cli: &mut impl cli::traits::Cli,
6167
user_provided_args: Vec<String>,
62-
) -> anyhow::Result<()> {
68+
output_mode: OutputMode,
69+
) -> anyhow::Result<RuntimeTestOutput> {
6370
cli.intro("Testing block execution")?;
6471
if let Err(e) = update_runtime_source(
6572
cli,
@@ -71,12 +78,20 @@ impl TestExecuteBlockCommand {
7178
)
7279
.await
7380
{
74-
return display_message(&e.to_string(), false, cli);
81+
if output_mode == OutputMode::Json {
82+
return Err(invalid_input_error(e.to_string()));
83+
}
84+
display_message(&e.to_string(), false, cli)?;
85+
return Ok(RuntimeTestOutput::success("execute-block", None));
7586
}
7687

7788
// Prompt the update the live state.
7889
if let Err(e) = update_live_state(cli, &mut self.state, &mut None) {
79-
return display_message(&e.to_string(), false, cli);
90+
if output_mode == OutputMode::Json {
91+
return Err(invalid_input_error(e.to_string()));
92+
}
93+
display_message(&e.to_string(), false, cli)?;
94+
return Ok(RuntimeTestOutput::success("execute-block", None));
8095
};
8196

8297
// Prompt the user to select the try state if no `--try-state` argument is provided.
@@ -95,9 +110,17 @@ impl TestExecuteBlockCommand {
95110
// Display the `execute-block` command.
96111
cli.info(self.display(user_provided_args)?)?;
97112
if let Err(e) = result {
98-
return display_message(&e.to_string(), false, cli);
113+
if output_mode == OutputMode::Json {
114+
return Err(build_error_with_details(
115+
"Failed to execute block tests",
116+
e.to_string(),
117+
));
118+
}
119+
display_message(&e.to_string(), false, cli)?;
120+
return Ok(RuntimeTestOutput::success("execute-block", None));
99121
}
100-
display_message("Block executed successfully!", true, cli)
122+
display_message("Block executed successfully!", true, cli)?;
123+
Ok(RuntimeTestOutput::success("execute-block", None))
101124
}
102125

103126
async fn run(
@@ -248,10 +271,22 @@ mod tests {
248271
);
249272
let mut command = TestExecuteBlockCommand::default();
250273
command.build_params.no_build = true;
251-
command.execute(&mut cli).await?;
274+
command.execute(&mut cli, OutputMode::Human).await?;
252275
cli.verify()
253276
}
254277

278+
#[tokio::test]
279+
async fn execute_block_requires_flags_in_json_mode() -> anyhow::Result<()> {
280+
let mut command = TestExecuteBlockCommand::default();
281+
let error = command
282+
.execute(&mut crate::cli::JsonCli, OutputMode::Json)
283+
.await
284+
.unwrap_err()
285+
.to_string();
286+
assert!(error.contains("interactive prompt required but --json mode is active"));
287+
Ok(())
288+
}
289+
255290
#[tokio::test]
256291
async fn execute_block_invalid_uri() -> anyhow::Result<()> {
257292
source_try_runtime_binary(

0 commit comments

Comments
 (0)