Skip to content

feat: complete option behavior runtime and e2e coverage in one pass#41

Merged
overtrue merged 4 commits intomainfrom
overtrue/find-exec-print-e2e-phase4
Mar 9, 2026
Merged

feat: complete option behavior runtime and e2e coverage in one pass#41
overtrue merged 4 commits intomainfrom
overtrue/find-exec-print-e2e-phase4

Conversation

@overtrue
Copy link
Contributor

@overtrue overtrue commented Mar 8, 2026

Summary

This PR is a one-pass completion for the remaining command-option behavior gaps, with runtime fixes and e2e coverage added together.

Core Fixes

  • Implemented missing runtime behavior for find options (from the previous commit in this PR):
    • --print now outputs full remote path (alias/bucket/key)
    • --exec now executes per match with {} substitution
    • --exec --json is now explicitly rejected with usage error
  • Implemented real runtime behavior for mirror --parallel (previously effectively no-op):
    • copy/remove operations now run with bounded concurrency
    • added input validation: --parallel must be > 0

Test Completion (e2e)

Added and validated behavior-level integration tests for option semantics (not only help contract):

  • Existing in this PR branch (previous commit):
    • test_find_print_outputs_full_remote_path
    • test_find_exec_runs_command_with_full_remote_path
    • test_find_exec_rejects_json_output
  • Newly added in this commit:
    • test_find_maxdepth_excludes_deeper_matches
    • test_mirror_remove_with_parallel_synchronizes_destination
    • test_mirror_parallel_zero_returns_usage_error
    • test_tree_option_combination_filters_expected_nodes
    • test_diff_diff_only_excludes_same_entries
    • test_share_rejects_expiration_over_seven_days

Total option behavior tests in option_behavior_operations: 15.

CI / Smoke updates

Updated integration.yml smoke list to include additional fast option-regression guards:

  • option_behavior_operations::test_find_exec_rejects_json_output
  • option_behavior_operations::test_mirror_parallel_zero_returns_usage_error

Validation

All required checks passed locally:

  • cargo fmt --all --check
  • cargo clippy --workspace -- -D warnings
  • cargo test --workspace

Real RustFS backend validation (TEST_S3_ENDPOINT=http://localhost:9000):

  • cargo test --package rustfs-cli --test integration --features integration option_behavior_operations:: -- --test-threads=1
    • result: 15 passed, 0 failed
  • smoke sequence replay with updated list: all passed

Scope

  • No protected files modified
  • No schema/config breaking changes
  • Focused on option behavior correctness + e2e completion + smoke hardening

Copilot AI review requested due to automatic review settings March 8, 2026 20:25
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR closes a CLI help/runtime behavior gap for rc find by wiring up the previously no-op --print and --exec options, and adds integration coverage to prevent regressions.

Changes:

  • Implement find --print to render matches using full remote paths (alias/bucket/key).
  • Implement find --exec to run a user-provided command per match (with {} substituted by the full remote path) and reject --exec combined with --json.
  • Add unit + integration tests for the new behaviors and extend the integration workflow smoke list.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
crates/cli/src/commands/find.rs Adds --exec execution, --print full-path rendering, JSON incompatibility check, plus a helper for full remote paths.
crates/cli/tests/integration.rs Adds 3 e2e integration tests covering --print, --exec, and --exec --json rejection.
.github/workflows/integration.yml Adds the new find --print integration test to the smoke test list.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +143 to +147
for m in &matches {
let object_path = full_object_path(&alias_name, &bucket, &m.key);
let command_text = exec_template.replace("{}", &object_path);
let output = match run_shell_command(&command_text) {
Ok(output) => output,
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--exec builds a shell command string by substituting the (potentially untrusted) object key into exec_template and then runs it via sh -c / cmd /C. Because the substituted path is not escaped/quoted, object keys containing shell metacharacters (e.g. ;, &&, backticks, quotes, newlines, $()) can lead to unintended command execution. Prefer executing without a shell by parsing --exec into a program + args (e.g. Vec<String>) and replacing {} at the argv level, or shell-escape the substituted path (with clear docs about limitations).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d00f741. --exec no longer routes through sh -c / cmd /C. It now parses the template with shlex, replaces {} at argv level, and executes via Command::new(program).args(args), so object keys are not interpreted by a shell.

Comment on lines +164 to +166
formatter.error(&format!(
"Command failed for {}: {}",
object_path, command_text
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the executed command exits non-zero, the error message doesn’t include the exit status (code/signal), which makes failures harder to debug—especially since command_text may be long and stderr may be empty. Consider including output.status (and/or output.status.code()) in the error, and optionally distinguishing between spawn failures vs. non-zero exit.

Suggested change
formatter.error(&format!(
"Command failed for {}: {}",
object_path, command_text
let status_desc = format!("{}", output.status);
let status_code = output.status.code().unwrap_or(-1);
formatter.error(&format!(
"Command failed for {} (status {} [{}]): {}",
object_path, status_desc, status_code, command_text

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d00f741. Non-zero exits now include process status in the error message: Command failed for <path> (status <status>): <rendered command>.

}

fn full_object_path(alias: &str, bucket: &str, key: &str) -> String {
format!("{alias}/{bucket}/{key}")
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

full_object_path() duplicates logic that already exists in rc_core::RemotePath::to_full_path() (and the existing helper handles empty keys without producing a trailing slash). Using RemotePath::new(alias, bucket, key).to_full_path() here would reduce duplication and keep remote path formatting consistent across the CLI.

Suggested change
format!("{alias}/{bucket}/{key}")
RemotePath::new(alias, bucket, key).to_full_path()

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d00f741. full_object_path() now delegates to RemotePath::new(...).to_full_path() to keep path formatting consistent and avoid duplication.

Comment on lines 173 to 189
let display_matches: Vec<MatchInfo> = matches
.iter()
.map(|m| MatchInfo {
key: if args.print {
full_object_path(&alias_name, &bucket, &m.key)
} else {
m.key.clone()
},
size_bytes: m.size_bytes,
size_human: m.size_human.clone(),
last_modified: m.last_modified.clone(),
})
.collect();

// Calculate totals
let total_count = matches.len();
let total_size: i64 = matches.iter().filter_map(|m| m.size_bytes).sum();
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

display_matches is always rebuilt by cloning every MatchInfo field (including key, size_human, last_modified) even when --print is not set. For large listings this adds avoidable allocations. Consider reusing/mutating the existing matches vec in-place (e.g., only rewrite key when --print is set) so the common path doesn’t clone every entry.

Suggested change
let display_matches: Vec<MatchInfo> = matches
.iter()
.map(|m| MatchInfo {
key: if args.print {
full_object_path(&alias_name, &bucket, &m.key)
} else {
m.key.clone()
},
size_bytes: m.size_bytes,
size_human: m.size_human.clone(),
last_modified: m.last_modified.clone(),
})
.collect();
// Calculate totals
let total_count = matches.len();
let total_size: i64 = matches.iter().filter_map(|m| m.size_bytes).sum();
let display_matches: Vec<MatchInfo> = if args.print {
matches
.into_iter()
.map(|mut m| {
let full_key = full_object_path(&alias_name, &bucket, &m.key);
m.key = full_key;
m
})
.collect()
} else {
matches
};
// Calculate totals
let total_count = display_matches.len();
let total_size: i64 = display_matches
.iter()
.filter_map(|m| m.size_bytes)
.sum();

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d00f741. display_matches now reuses the existing vector and rewrites keys in-place only when --print is enabled, avoiding per-entry cloning in the common path.

@overtrue overtrue changed the title fix: wire find --print/--exec behavior and add e2e coverage feat: complete option behavior runtime and e2e coverage in one pass Mar 8, 2026
@overtrue overtrue merged commit bb166ec into main Mar 9, 2026
15 checks passed
@overtrue overtrue deleted the overtrue/find-exec-print-e2e-phase4 branch March 9, 2026 01:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants