stalecontext/forgejo-cli-plus
2
4
Fork
You've already forked forgejo-cli-plus
1

test: add command stubber for subprocess testing (gh-cli style) #39

Open
opened 2026年03月31日 00:25:06 +02:00 by stalecontext · 0 comments

Summary

Several commands shell out to external programs (git, browser, gpg) and can't be tested with wiremock alone. The GitHub CLI solves this with a command stubber that intercepts all subprocess calls via regex pattern matching.

What gh-cli does

Their implementation lives in `internal/run/stub.go`:

  1. A `CommandStubber` replaces the real command runner during tests
  2. Register expected commands as regex patterns with exit codes and output:
    ```go
    cs.Register(`git fetch origin +refs/heads/feature:refs/remotes/origin/feature`, 0, "")
    cs.Register(`git checkout -b feature --track origin/feature`, 0, "")
    ```
  3. Auto-adds git auth patterns for fetch/pull/push/clone/remote-add/submodule
  4. Verifies all registered stubs were matched during test cleanup (fails on unused stubs)
  5. Supports callbacks to inspect raw arguments
  6. Normalizes `git.exe` to `git` for cross-platform consistency

What this would look like in Rust

A trait-based abstraction over `std::process::Command`:

```rust
#[cfg_attr(test, mockall::automock)]
pub trait CommandRunner {
fn run(&self, program: &str, args: &[&str]) -> eyre::Resultstd::process::Output;
}
```

Or a simpler approach: a global function pointer / env-var-based stub that intercepts command execution in test builds.

Commands this would unlock testing for

  • `repo clone` / `wiki clone` -- git clone subprocess
  • `pr checkout` -- git fetch + git checkout
  • `pr create --agit` -- git push
  • `user key upload` / `user gpg upload` / `user gpg verify` -- gpg subprocess
  • All browse commands -- `open::that_detached` subprocess

Trade-offs

  • Pro: Would enable 100% command coverage without real git repos or browsers
  • Pro: Verifies exact git commands being constructed (argument order, flags, etc.)
  • Con: Significant refactoring -- every callsite using `std::process::Command` or `tokio::process::Command` needs to go through the abstraction
  • Con: Adds complexity to production code paths for testability

Alternative (current approach)

Tier 1/2 testing covers most gaps without refactoring:

  • Real temp git repos for clone/checkout tests
  • URL extraction for browse commands
  • EDITOR env var for editor tests
  • HTTP-layer mocking for file upload/download

The command stubber is the long-term ideal but not blocking for the current test suite.

## Summary Several commands shell out to external programs (git, browser, gpg) and can't be tested with wiremock alone. The GitHub CLI solves this with a command stubber that intercepts all subprocess calls via regex pattern matching. ## What gh-cli does Their implementation lives in \`internal/run/stub.go\`: 1. A \`CommandStubber\` replaces the real command runner during tests 2. Register expected commands as regex patterns with exit codes and output: \`\`\`go cs.Register(\`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature\`, 0, "") cs.Register(\`git checkout -b feature --track origin/feature\`, 0, "") \`\`\` 3. Auto-adds git auth patterns for fetch/pull/push/clone/remote-add/submodule 4. Verifies all registered stubs were matched during test cleanup (fails on unused stubs) 5. Supports callbacks to inspect raw arguments 6. Normalizes \`git.exe\` to \`git\` for cross-platform consistency ## What this would look like in Rust A trait-based abstraction over \`std::process::Command\`: \`\`\`rust #[cfg_attr(test, mockall::automock)] pub trait CommandRunner { fn run(&self, program: &str, args: &[&str]) -> eyre::Result<std::process::Output>; } \`\`\` Or a simpler approach: a global function pointer / env-var-based stub that intercepts command execution in test builds. ## Commands this would unlock testing for - \`repo clone\` / \`wiki clone\` -- git clone subprocess - \`pr checkout\` -- git fetch + git checkout - \`pr create --agit\` -- git push - \`user key upload\` / \`user gpg upload\` / \`user gpg verify\` -- gpg subprocess - All browse commands -- \`open::that_detached\` subprocess ## Trade-offs - **Pro**: Would enable 100% command coverage without real git repos or browsers - **Pro**: Verifies exact git commands being constructed (argument order, flags, etc.) - **Con**: Significant refactoring -- every callsite using \`std::process::Command\` or \`tokio::process::Command\` needs to go through the abstraction - **Con**: Adds complexity to production code paths for testability ## Alternative (current approach) Tier 1/2 testing covers most gaps without refactoring: - Real temp git repos for clone/checkout tests - URL extraction for browse commands - EDITOR env var for editor tests - HTTP-layer mocking for file upload/download The command stubber is the long-term ideal but not blocking for the current test suite.
Sign in to join this conversation.
No Branch/Tag specified
main
feature/issue-batch-2026-07
upstream-sync/2026-07-02
feat/repo-create-org
projects
v2.0.0
upstream-reviewed/2026-07-02
v1.1.0
upstream-reviewed/2026-03-31
v1.0.0
upstream-reviewed/2026-03-16
v0.4.0
v0.3.0
v0.2.0
v0.1.1
v0.1.0
v0.0.4
v0.0.3
v0.0.2
v0.0.1
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
stalecontext/forgejo-cli-plus#39
Reference in a new issue
stalecontext/forgejo-cli-plus
No description provided.
Delete branch "%!s()"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?