Skip to content

Add structured rtk dotnet support (build/test/restore) with binlog/TRX parsing, robust argument forwarding, and locale-stable fallback behavior#172

Open
danielmarbach wants to merge 11 commits intortk-ai:masterfrom
danielmarbach:feat-dotnet

Conversation

@danielmarbach
Copy link

@danielmarbach danielmarbach commented Feb 17, 2026

  • Add first-class .NET support to RTK: rtk dotnet build, rtk dotnet test, rtk dotnet restore (+ passthrough for other subcommands).
  • Implement structured parsing with new modules:
    • src/binlog.rs for MSBuild binlog parsing
    • src/dotnet_trx.rs for TRX parsing + failed test extraction
    • src/dotnet_cmd.rs for orchestration, formatting, forwarding, and fallback logic
  • Make structured artifacts authoritative (binlog/TRX first, text parsing only as fallback).
  • Improve robustness by setting DOTNET_CLI_UI_LANGUAGE=en-US for spawned dotnet invocations (stable fallback parsing across locales).
  • Integrate command routing/docs/fixtures (src/main.rs, README.md, tests/fixtures/dotnet/*, Cargo.toml/Cargo.lock).

Why

  • Raw dotnet output is verbose and token-expensive in LLM workflows.
  • .NET/MSBuild output can be localized; text-only parsing is brittle.
  • Structured-first parsing gives more reliable summaries while preserving exit codes and user args.
    Benefits
  • Significant token reduction on real .NET workflows.
  • Cleaner, focused build/test/restore summaries.
  • Better cross-locale reliability without sacrificing fallback resilience.
  • Safer argument handling and improved test diagnostics extraction.

Real-world impact (NServiceBus)

Solution-level run against NServiceBus.slnx:

  • rtk dotnet test ...: 86.4% savings (~3.1K tokens)
  • rtk dotnet build ...: ~93% savings (~523 tokens)
  • rtk dotnet restore ...: ~0% (already minimal output)

More outputs

02-17 13:23 ▲ rtk dotnet test           -82% (186)
02-17 13:23 ▲ rtk dotnet build          -80% (163)
02-17 13:17 ▲ rtk dotnet build          -91% (863)
02-17 13:17 ▲ rtk dotnet restore        -97% (1.2K)
02-17 13:10 ▲ rtk dotnet test NServi... -86% (3.1K)
02-17 13:00 ▲ rtk dotnet build NServ... -93% (523)
02-17 13:00 • rtk dotnet restore NSe... -0% (0)
02-17 12:54 ■ rtk dotnet test --no-b... -38% (303)
02-17 12:54 ▲ rtk dotnet build --no-... -76% (130)
02-17 12:54 • rtk dotnet restore        -0% (0)

@danielmarbach
Copy link
Author

@DavidFowler this might be something of interest to you

It probably still misses a few things but might be a good start

@F0rty-Tw0
Copy link

Will be testing this during the week. Thanks for enormous work.

Copy link

@F0rty-Tw0 F0rty-Tw0 left a comment

Choose a reason for hiding this comment

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

Overall very good job :)

find_recent_trx_in_dir(Path::new("./TestResults"))
}

fn find_recent_trx_in_dir(dir: &Path) -> Option<PathBuf> {

Choose a reason for hiding this comment

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

a failing dotnet test can produce a ./TestResults/<GUID>/test.trx and it looks like we don't check for that in here.


lazy_static! {
// Note: (?s) enables DOTALL mode so . matches newlines
static ref TRX_COUNTERS_RE: Regex = Regex::new(

Choose a reason for hiding this comment

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

here and in line 12 regex takes in consideration the attribute order, but the xml report attribute order is not guaranteed, parser can silently miss counters/failed tests and under-report failures

.any(|arg| matches!(arg.to_ascii_lowercase().as_str(), "-nologo" | "/nologo"))
}

fn has_logger_arg(args: &[String]) -> bool {

Choose a reason for hiding this comment

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

interesting one, and i think it can wrongly return true when an argument like --results-directory logs/logger-output is passed, or a test filter containing logger.

@pszymkowiak
Copy link
Collaborator

This is an outstanding PR — probably the most thorough community contribution we've received. The binary binlog parser, multi-layer fallback strategy (binlog → text → TRX), and 42 tests
show serious engineering. Really impressive work, thank you!

A few things to address before merge:

  1. Missing hook rewrite (required)

Without this, Claude Code will type dotnet build and it won't go through the RTK filter. Every command we support has a corresponding rewrite in .claude/hooks/rtk-rewrite.sh. Please add:

--- .NET ---

elif echo "$MATCH_CMD" | grep -qE '^dotnet[[:space:]]+(build|test|restore)([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^dotnet/rtk dotnet/')"

See the cargo/pytest/ruff sections in the hook file for examples.

  1. Binlog parse crash on build (bug)

In dotnet_cmd.rs, run_dotnet_with_binlog() for "build" does:
binlog::parse_build(&binlog_path)?
If the binlog is corrupted, this crashes instead of falling back to text. The "test" path already handles this correctly with .unwrap_or_default(). Please use the same pattern for build
and restore.

  1. Regex compiled at runtime in scrub_sensitive_env_vars()

This function calls Regex::new() in a loop for every env var on every invocation. Should be lazy_static! to avoid recompilation. Not a blocker but matters for performance.

  1. Binlog temp files not cleaned up

build_binlog_path() creates files in /tmp/rtk_dotnet_* but never removes them. Could accumulate over time. Consider cleaning up after parsing, similar to how you handle TRX cleanup.

None of these are major — #1 is the only blocker. Great work!

@danielmarbach
Copy link
Author

Thank you for the kinds words

It will take a while until I can address those because I have a few prio things I need to get through first but I should be able to get back to this next week

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.

3 participants

Comments