Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Unreleased

* fix: `icp canister call --json` no longer produces blank output.

# v0.2.4

* feat: `icp identity delegation request/sign/use` now permit creating and importing identity delegations
Expand Down
11 changes: 4 additions & 7 deletions crates/icp-cli/src/commands/canister/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E
};

// catch errors, because the json result should be printed regardless of errors
let res = (|| {
let res: Result<(), anyhow::Error> = (|| {
match args.output {
CallOutputMode::Auto => {
if let Ok(ret) = try_decode_candid(&res, declared_method.as_ref()) {
Expand All @@ -251,11 +251,9 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E
json_response.response_text = Some(s.to_string());
} else {
writeln!(term, "{s}")?;
term.flush()?;
}
} else if !args.json {
writeln!(term, "{}", hex::encode(&res))?;
term.flush()?;
}
}
CallOutputMode::Candid => {
Expand All @@ -276,18 +274,16 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E
json_response.response_text = Some(s.to_string());
} else {
writeln!(term, "{s}")?;
term.flush()?;
}
}
CallOutputMode::Hex => {
writeln!(term, "{}", hex::encode(&res))?;
term.flush()?;
}
};
anyhow::Ok(())
})();
if args.json {
let write_result = serde_json::to_writer(term, &json_response);
let write_result = serde_json::to_writer(&term, &json_response);
if let Err(write_err) = write_result {
if let Err(decode_err) = res {
error!("failed to write JSON response: {write_err}");
Expand All @@ -298,6 +294,8 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E
}
}
res?;
// term is buffered; this single flush covers all output paths (json and non-json).
term.flush()?;

Ok(())
}
Expand Down Expand Up @@ -337,7 +335,6 @@ pub(crate) fn print_candid_for_term(term: &mut Term, args: &IDLArgs) -> io::Resu
} else {
writeln!(term, "{args}")?;
}
term.flush()?;
Ok(())
}

Expand Down
66 changes: 66 additions & 0 deletions crates/icp-cli/tests/canister_call_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,72 @@ async fn canister_call_output_modes() {
);
}

#[tokio::test]
async fn canister_call_json_output() {
let ctx = TestContext::new();
let project_dir = ctx.create_project_dir("icp");
let wasm = ctx.make_asset("example_icp_mo.wasm");
let pm = formatdoc! {r#"
canisters:
- name: my-canister
build:
steps:
- type: script
command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH"

{NETWORK_RANDOM_PORT}
{ENVIRONMENT_RANDOM_PORT}
"#};
write_string(&project_dir.join("icp.yaml"), &pm).expect("write manifest");
let _g = ctx.start_network_in(&project_dir, "random-network").await;
ctx.ping_until_healthy(&project_dir, "random-network");
ctx.icp()
.current_dir(&project_dir)
.args([
"deploy",
"my-canister",
"--environment",
"random-environment",
])
.assert()
.success();

let out = ctx
.icp()
.current_dir(&project_dir)
.args([
"canister",
"call",
"--environment",
"random-environment",
"--json",
"my-canister",
"greet",
"(\"world\")",
])
.output()
.expect("run call");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
// Regression: stdout was blank because the buffered term was never flushed.
assert!(
!out.stdout.is_empty(),
"stdout must not be blank with --json"
);
let json: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("stdout must be valid JSON");
// Candid encoding of ("Hello, world!") — didc encode '("Hello, world!")'
assert_eq!(
json["response_bytes"],
"4449444c0001710d48656c6c6f2c20776f726c6421"
);
assert_eq!(json["response_candid"], "(\"Hello, world!\")");
assert!(json["response_text"].is_null());
}

#[tokio::test]
async fn canister_call_query_conflicts_with_proxy() {
let ctx = TestContext::new();
Expand Down
Loading