Skip to content
Closed
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ Full rationale: [ADR 0002 — Package naming convention](docs/adr/0002-package-n
The wiki's **[Module Catalog](https://github.com/tamp-build/tamp/wiki/Module-Catalog)** is the canonical reference for every published `Tamp.*` package — what it wraps, latest version, and source repo. Categories include:

- **.NET toolchain** — `Tamp.NetCli.V8/9/10`, `Tamp.EFCore.V8/9/10`, `Tamp.Coverlet.V6`, `Tamp.ReportGenerator.V5`, `Tamp.DotNetCoverage.V18`, `Tamp.GitVersion.V6`
See [test-host crash diagnostics](docs/dotnet-test-diagnostics.md) for typed VSTest crash-dump options.
- **Containers + cluster ops** — `Tamp.Docker.V27`, `Tamp.Helm.V3`, `Tamp.Kubectl`, `Tamp.Sccache`, `Tamp.AdjacentContainer(.Local/.Provisioning)`, `Tamp.Testcontainers.V4`
- **JavaScript / TypeScript** — `Tamp.Yarn.V4`, `Tamp.Npm.V10`, `Tamp.Turbo.V2`, `Tamp.Vite.V5`, `Tamp.Playwright.V1`, `Tamp.GraphQLCodegen.V5`, `Tamp.Eslint.V9`
- **Rust + desktop ship chain** — `Tamp.Cargo`, `Tamp.Tauri.V2`, `Tamp.Msix`, `Tamp.MicrosoftStoreCli`
Expand Down
28 changes: 28 additions & 0 deletions docs/dotnet-test-diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Diagnosing test-host crashes

The `Tamp.NetCli.V8`, `V9`, and `V10` test settings expose VSTest crash diagnostics
alongside the existing hang diagnostics:

```csharp
using Tamp.NetCli.V10;

var plan = DotNet.Test(s => s
.SetProject("Tests.csproj")
.SetBlameCrash(true)
.SetBlameCrashDumpType(DotNetCrashDumpType.Mini));
```

This emits `test Tests.csproj --blame-crash --blame-crash-dump-type mini`.
Choose `DotNetCrashDumpType.Full` for a full dump. The nullable dump-type setting
is validated when assigned, including direct property assignments; undefined
enum values are rejected rather than emitted as numeric CLI arguments.

Both options are omitted by default. `SetBlameCrash(false)` omits the explicit
crash flag, and `SetBlameCrashDumpType(null)` omits the dump-type flag. As in the
underlying CLI, specifying a dump type still implies crash diagnostics even
without the explicit crash flag. Crash and hang settings can be combined.

These flags target **VSTest**, not Microsoft.Testing.Platform. Dump availability
still depends on the runtime, operating system, and crash type. See Microsoft's
[dotnet test VSTest reference](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-test-vstest#options)
for the underlying behavior and platform requirements.
35 changes: 35 additions & 0 deletions src/Tamp.NetCli.V10/DotNetTestSettings.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
namespace Tamp.NetCli.V10;

/// <summary>Crash dump sizes accepted by VSTest's <c>--blame-crash-dump-type</c>.</summary>
public enum DotNetCrashDumpType
{
/// <summary>Collect a small crash dump.</summary>
Mini,
/// <summary>Collect a full crash dump.</summary>
Full,
}

public sealed class DotNetTestSettings : DotNetSettingsBase
{
public Configuration? Configuration { get; set; }
Expand All @@ -12,6 +21,22 @@ public sealed class DotNetTestSettings : DotNetSettingsBase
public string? Settings { get; set; }
public string? Runtime { get; set; }
public string? Framework { get; set; }
/// <summary>Collect crash diagnostics when the VSTest test host exits unexpectedly.</summary>
public bool BlameCrash { get; set; }

private DotNetCrashDumpType? _blameCrashDumpType;
/// <summary>Crash dump size. Null uses the CLI default; specifying a size implies crash diagnostics.</summary>
public DotNetCrashDumpType? BlameCrashDumpType
{
get => _blameCrashDumpType;
set
{
if (value is { } type && !Enum.IsDefined(type))
throw new ArgumentOutOfRangeException(nameof(value), value, "Unsupported crash dump type.");
_blameCrashDumpType = value;
}
}

public bool BlameHang { get; set; }
public TimeSpan? BlameHangTimeout { get; set; }
public Dictionary<string, string> Properties { get; } = new();
Expand All @@ -36,6 +61,10 @@ public sealed class DotNetTestSettings : DotNetSettingsBase
public DotNetTestSettings SetSettings(string? path) { Settings = path; return this; }
public DotNetTestSettings SetRuntime(string? runtime) { Runtime = runtime; return this; }
public DotNetTestSettings SetFramework(string? tfm) { Framework = tfm; return this; }
/// <summary>Enable or disable VSTest crash diagnostics.</summary>
public DotNetTestSettings SetBlameCrash(bool v) { BlameCrash = v; return this; }
/// <summary>Set the crash dump size, or null to omit the dump-type flag.</summary>
public DotNetTestSettings SetBlameCrashDumpType(DotNetCrashDumpType? v) { BlameCrashDumpType = v; return this; }
public DotNetTestSettings SetBlameHang(bool v) { BlameHang = v; return this; }
public DotNetTestSettings SetBlameHangTimeout(TimeSpan? t) { BlameHangTimeout = t; return this; }
public DotNetTestSettings SetProperty(string name, string value) { Properties[name] = value; return this; }
Expand All @@ -57,6 +86,12 @@ protected override IEnumerable<string> BuildVerbArguments()
if (!string.IsNullOrEmpty(Settings)) { yield return "--settings"; yield return Settings!; }
if (!string.IsNullOrEmpty(Runtime)) { yield return "--runtime"; yield return Runtime!; }
if (!string.IsNullOrEmpty(Framework)) { yield return "--framework"; yield return Framework!; }
if (BlameCrash) yield return "--blame-crash";
if (BlameCrashDumpType is { } dumpType)
{
yield return "--blame-crash-dump-type";
yield return dumpType.ToString().ToLowerInvariant();
}
if (BlameHang) yield return "--blame-hang";
if (BlameHangTimeout is { } t) { yield return "--blame-hang-timeout"; yield return $"{(int)t.TotalMilliseconds}ms"; }
foreach (var (k, v) in Properties)
Expand Down
35 changes: 35 additions & 0 deletions src/Tamp.NetCli.V8/DotNetTestSettings.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
namespace Tamp.NetCli.V8;

/// <summary>Crash dump sizes accepted by VSTest's <c>--blame-crash-dump-type</c>.</summary>
public enum DotNetCrashDumpType
{
/// <summary>Collect a small crash dump.</summary>
Mini,
/// <summary>Collect a full crash dump.</summary>
Full,
}

public sealed class DotNetTestSettings : DotNetSettingsBase
{
public Configuration? Configuration { get; set; }
Expand All @@ -12,6 +21,22 @@ public sealed class DotNetTestSettings : DotNetSettingsBase
public string? Settings { get; set; }
public string? Runtime { get; set; }
public string? Framework { get; set; }
/// <summary>Collect crash diagnostics when the VSTest test host exits unexpectedly.</summary>
public bool BlameCrash { get; set; }

private DotNetCrashDumpType? _blameCrashDumpType;
/// <summary>Crash dump size. Null uses the CLI default; specifying a size implies crash diagnostics.</summary>
public DotNetCrashDumpType? BlameCrashDumpType
{
get => _blameCrashDumpType;
set
{
if (value is { } type && !Enum.IsDefined(type))
throw new ArgumentOutOfRangeException(nameof(value), value, "Unsupported crash dump type.");
_blameCrashDumpType = value;
}
}

public bool BlameHang { get; set; }
public TimeSpan? BlameHangTimeout { get; set; }
public Dictionary<string, string> Properties { get; } = new();
Expand All @@ -36,6 +61,10 @@ public sealed class DotNetTestSettings : DotNetSettingsBase
public DotNetTestSettings SetSettings(string? path) { Settings = path; return this; }
public DotNetTestSettings SetRuntime(string? runtime) { Runtime = runtime; return this; }
public DotNetTestSettings SetFramework(string? tfm) { Framework = tfm; return this; }
/// <summary>Enable or disable VSTest crash diagnostics.</summary>
public DotNetTestSettings SetBlameCrash(bool v) { BlameCrash = v; return this; }
/// <summary>Set the crash dump size, or null to omit the dump-type flag.</summary>
public DotNetTestSettings SetBlameCrashDumpType(DotNetCrashDumpType? v) { BlameCrashDumpType = v; return this; }
public DotNetTestSettings SetBlameHang(bool v) { BlameHang = v; return this; }
public DotNetTestSettings SetBlameHangTimeout(TimeSpan? t) { BlameHangTimeout = t; return this; }
public DotNetTestSettings SetProperty(string name, string value) { Properties[name] = value; return this; }
Expand All @@ -57,6 +86,12 @@ protected override IEnumerable<string> BuildVerbArguments()
if (!string.IsNullOrEmpty(Settings)) { yield return "--settings"; yield return Settings!; }
if (!string.IsNullOrEmpty(Runtime)) { yield return "--runtime"; yield return Runtime!; }
if (!string.IsNullOrEmpty(Framework)) { yield return "--framework"; yield return Framework!; }
if (BlameCrash) yield return "--blame-crash";
if (BlameCrashDumpType is { } dumpType)
{
yield return "--blame-crash-dump-type";
yield return dumpType.ToString().ToLowerInvariant();
}
if (BlameHang) yield return "--blame-hang";
if (BlameHangTimeout is { } t) { yield return "--blame-hang-timeout"; yield return $"{(int)t.TotalMilliseconds}ms"; }
foreach (var (k, v) in Properties)
Expand Down
35 changes: 35 additions & 0 deletions src/Tamp.NetCli.V9/DotNetTestSettings.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
namespace Tamp.NetCli.V9;

/// <summary>Crash dump sizes accepted by VSTest's <c>--blame-crash-dump-type</c>.</summary>
public enum DotNetCrashDumpType
{
/// <summary>Collect a small crash dump.</summary>
Mini,
/// <summary>Collect a full crash dump.</summary>
Full,
}

public sealed class DotNetTestSettings : DotNetSettingsBase
{
public Configuration? Configuration { get; set; }
Expand All @@ -12,6 +21,22 @@ public sealed class DotNetTestSettings : DotNetSettingsBase
public string? Settings { get; set; }
public string? Runtime { get; set; }
public string? Framework { get; set; }
/// <summary>Collect crash diagnostics when the VSTest test host exits unexpectedly.</summary>
public bool BlameCrash { get; set; }

private DotNetCrashDumpType? _blameCrashDumpType;
/// <summary>Crash dump size. Null uses the CLI default; specifying a size implies crash diagnostics.</summary>
public DotNetCrashDumpType? BlameCrashDumpType
{
get => _blameCrashDumpType;
set
{
if (value is { } type && !Enum.IsDefined(type))
throw new ArgumentOutOfRangeException(nameof(value), value, "Unsupported crash dump type.");
_blameCrashDumpType = value;
}
}

public bool BlameHang { get; set; }
public TimeSpan? BlameHangTimeout { get; set; }
public Dictionary<string, string> Properties { get; } = new();
Expand All @@ -36,6 +61,10 @@ public sealed class DotNetTestSettings : DotNetSettingsBase
public DotNetTestSettings SetSettings(string? path) { Settings = path; return this; }
public DotNetTestSettings SetRuntime(string? runtime) { Runtime = runtime; return this; }
public DotNetTestSettings SetFramework(string? tfm) { Framework = tfm; return this; }
/// <summary>Enable or disable VSTest crash diagnostics.</summary>
public DotNetTestSettings SetBlameCrash(bool v) { BlameCrash = v; return this; }
/// <summary>Set the crash dump size, or null to omit the dump-type flag.</summary>
public DotNetTestSettings SetBlameCrashDumpType(DotNetCrashDumpType? v) { BlameCrashDumpType = v; return this; }
public DotNetTestSettings SetBlameHang(bool v) { BlameHang = v; return this; }
public DotNetTestSettings SetBlameHangTimeout(TimeSpan? t) { BlameHangTimeout = t; return this; }
public DotNetTestSettings SetProperty(string name, string value) { Properties[name] = value; return this; }
Expand All @@ -57,6 +86,12 @@ protected override IEnumerable<string> BuildVerbArguments()
if (!string.IsNullOrEmpty(Settings)) { yield return "--settings"; yield return Settings!; }
if (!string.IsNullOrEmpty(Runtime)) { yield return "--runtime"; yield return Runtime!; }
if (!string.IsNullOrEmpty(Framework)) { yield return "--framework"; yield return Framework!; }
if (BlameCrash) yield return "--blame-crash";
if (BlameCrashDumpType is { } dumpType)
{
yield return "--blame-crash-dump-type";
yield return dumpType.ToString().ToLowerInvariant();
}
if (BlameHang) yield return "--blame-hang";
if (BlameHangTimeout is { } t) { yield return "--blame-hang-timeout"; yield return $"{(int)t.TotalMilliseconds}ms"; }
foreach (var (k, v) in Properties)
Expand Down
57 changes: 57 additions & 0 deletions tests/Tamp.NetCli.V10.Tests/DotNetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,63 @@ public void Test_BlameHang_With_Timeout_Emits_Milliseconds()
Assert.Equal("45000ms", args[IndexOf(args, "--blame-hang-timeout") + 1]);
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void Test_BlameCrash_Flag_Does_Not_Require_A_Dump_Type(bool enabled)
{
var args = DotNet.Test(s => s.SetBlameCrash(enabled)).Arguments;
Assert.Equal(enabled, args.Contains("--blame-crash"));
Assert.DoesNotContain("--blame-crash-dump-type", args);
}

[Theory]
[InlineData(DotNetCrashDumpType.Mini, "mini")]
[InlineData(DotNetCrashDumpType.Full, "full")]
public void Test_BlameCrash_Emits_Requested_Dump_Alongside_Hang_Diagnostics(
DotNetCrashDumpType dumpType, string token)
{
var plan = DotNet.Test(s => s.SetBlameCrash(true).SetBlameCrashDumpType(dumpType)
.SetBlameHang(true).SetBlameHangTimeout(TimeSpan.FromSeconds(45)));
var args = plan.Arguments;
Assert.Contains("--blame-crash", args);
Assert.Equal(token, args[IndexOf(args, "--blame-crash-dump-type") + 1]);
Assert.Contains("--blame-hang", args);
Assert.Equal("45000ms", args[IndexOf(args, "--blame-hang-timeout") + 1]);
}

[Fact]
public void Test_BlameCrash_Defaults_And_Reset_Omit_Crash_Flags()
{
Assert.DoesNotContain("--blame-crash", DotNet.Test().Arguments);
Assert.DoesNotContain("--blame-crash-dump-type", DotNet.Test().Arguments);
var plan = DotNet.Test(s => s.SetBlameCrash(true).SetBlameCrashDumpType(DotNetCrashDumpType.Mini)
.SetBlameCrash(false).SetBlameCrashDumpType(null));
Assert.DoesNotContain("--blame-crash", plan.Arguments);
Assert.DoesNotContain("--blame-crash-dump-type", plan.Arguments);
}

[Fact]
public void Test_Crash_Dump_Type_Can_Be_Used_Without_Explicit_Blame_Flag()
{
var settings = new DotNetTestSettings { BlameCrashDumpType = DotNetCrashDumpType.Mini };
var args = settings.ToCommandPlan().Arguments;
Assert.Equal("mini", args[IndexOf(args, "--blame-crash-dump-type") + 1]);
Assert.DoesNotContain("--blame-crash", args); // VSTest implies it from dump type.
}

[Theory]
[InlineData(-1)]
[InlineData(2)]
[InlineData(int.MaxValue)]
public void Test_Invalid_Crash_Dump_Type_Is_Rejected_Before_It_Becomes_State(int value)
{
var settings = new DotNetTestSettings { BlameCrashDumpType = DotNetCrashDumpType.Full };
Assert.Throws<ArgumentOutOfRangeException>(() => settings.BlameCrashDumpType = (DotNetCrashDumpType)value);
Assert.Throws<ArgumentOutOfRangeException>(() => settings.SetBlameCrashDumpType((DotNetCrashDumpType)value));
Assert.Equal(DotNetCrashDumpType.Full, settings.BlameCrashDumpType);
}

// ---- Pack ----

[Fact]
Expand Down
57 changes: 57 additions & 0 deletions tests/Tamp.NetCli.V8.Tests/DotNetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,63 @@ public void Test_BlameHang_With_Timeout_Emits_Milliseconds()
Assert.Equal("45000ms", args[IndexOf(args, "--blame-hang-timeout") + 1]);
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void Test_BlameCrash_Flag_Does_Not_Require_A_Dump_Type(bool enabled)
{
var args = DotNet.Test(s => s.SetBlameCrash(enabled)).Arguments;
Assert.Equal(enabled, args.Contains("--blame-crash"));
Assert.DoesNotContain("--blame-crash-dump-type", args);
}

[Theory]
[InlineData(DotNetCrashDumpType.Mini, "mini")]
[InlineData(DotNetCrashDumpType.Full, "full")]
public void Test_BlameCrash_Emits_Requested_Dump_Alongside_Hang_Diagnostics(
DotNetCrashDumpType dumpType, string token)
{
var plan = DotNet.Test(s => s.SetBlameCrash(true).SetBlameCrashDumpType(dumpType)
.SetBlameHang(true).SetBlameHangTimeout(TimeSpan.FromSeconds(45)));
var args = plan.Arguments;
Assert.Contains("--blame-crash", args);
Assert.Equal(token, args[IndexOf(args, "--blame-crash-dump-type") + 1]);
Assert.Contains("--blame-hang", args);
Assert.Equal("45000ms", args[IndexOf(args, "--blame-hang-timeout") + 1]);
}

[Fact]
public void Test_BlameCrash_Defaults_And_Reset_Omit_Crash_Flags()
{
Assert.DoesNotContain("--blame-crash", DotNet.Test().Arguments);
Assert.DoesNotContain("--blame-crash-dump-type", DotNet.Test().Arguments);
var plan = DotNet.Test(s => s.SetBlameCrash(true).SetBlameCrashDumpType(DotNetCrashDumpType.Mini)
.SetBlameCrash(false).SetBlameCrashDumpType(null));
Assert.DoesNotContain("--blame-crash", plan.Arguments);
Assert.DoesNotContain("--blame-crash-dump-type", plan.Arguments);
}

[Fact]
public void Test_Crash_Dump_Type_Can_Be_Used_Without_Explicit_Blame_Flag()
{
var settings = new DotNetTestSettings { BlameCrashDumpType = DotNetCrashDumpType.Mini };
var args = settings.ToCommandPlan().Arguments;
Assert.Equal("mini", args[IndexOf(args, "--blame-crash-dump-type") + 1]);
Assert.DoesNotContain("--blame-crash", args); // VSTest implies it from dump type.
}

[Theory]
[InlineData(-1)]
[InlineData(2)]
[InlineData(int.MaxValue)]
public void Test_Invalid_Crash_Dump_Type_Is_Rejected_Before_It_Becomes_State(int value)
{
var settings = new DotNetTestSettings { BlameCrashDumpType = DotNetCrashDumpType.Full };
Assert.Throws<ArgumentOutOfRangeException>(() => settings.BlameCrashDumpType = (DotNetCrashDumpType)value);
Assert.Throws<ArgumentOutOfRangeException>(() => settings.SetBlameCrashDumpType((DotNetCrashDumpType)value));
Assert.Equal(DotNetCrashDumpType.Full, settings.BlameCrashDumpType);
}

// ---- Pack ----

[Fact]
Expand Down
Loading