Skip to content

Commit c2e67cb

Browse files
authored
feat: implement TestBuildContext for capturing build-time output (#3849)
* feat: implement TestBuildContext for capturing build-time output and enhance output retrieval in TestContext * feat: enhance TestBuildContext with new methods and override output retrieval in TestContext * feat(tests): add attributes to TestBuildContextOutputCaptureTests for parallel execution and expected state * fix(tests): correct attribute usage in TestBuildContextOutputCaptureTests
1 parent 3af0305 commit c2e67cb

10 files changed

Lines changed: 355 additions & 15 deletions

TUnit.Core/Context.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ protected Context? Parent
1414

1515
public static Context Current =>
1616
TestContext.Current as Context
17+
?? TestBuildContext.Current as Context
1718
?? ClassHookContext.Current as Context
1819
?? AssemblyHookContext.Current as Context
1920
?? TestSessionContext.Current as Context
@@ -67,13 +68,13 @@ public void AddAsyncLocalValues()
6768
#endif
6869
}
6970

70-
public string GetStandardOutput()
71+
public virtual string GetStandardOutput()
7172
{
7273
if (_outputBuilder.Length == 0)
7374
{
7475
return string.Empty;
7576
}
76-
77+
7778
_outputLock.EnterReadLock();
7879

7980
try
@@ -86,7 +87,7 @@ public string GetStandardOutput()
8687
}
8788
}
8889

89-
public string GetErrorOutput()
90+
public virtual string GetErrorOutput()
9091
{
9192
if (_errorOutputBuilder.Length == 0)
9293
{
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
namespace TUnit.Core;
2+
3+
/// <summary>
4+
/// Context for capturing output during test building and data source initialization.
5+
/// This context is active during the test building phase, before TestContext is created.
6+
/// Output captured here is transferred to the TestContext when it's created.
7+
/// </summary>
8+
public sealed class TestBuildContext : Context, IDisposable
9+
{
10+
private static readonly AsyncLocal<TestBuildContext?> _current = new();
11+
12+
public static new TestBuildContext? Current
13+
{
14+
get => _current.Value;
15+
internal set => _current.Value = value;
16+
}
17+
18+
public TestBuildContext() : base(null)
19+
{
20+
}
21+
22+
/// <summary>
23+
/// Gets the captured standard output during test building.
24+
/// </summary>
25+
public string GetCapturedOutput() => GetStandardOutput();
26+
27+
/// <summary>
28+
/// Gets the captured error output during test building.
29+
/// </summary>
30+
public string GetCapturedErrorOutput() => GetErrorOutput();
31+
32+
internal override void SetAsyncLocalContext()
33+
{
34+
Current = this;
35+
}
36+
37+
/// <summary>
38+
/// Clears the current TestBuildContext.
39+
/// </summary>
40+
public new void Dispose()
41+
{
42+
Current = null;
43+
base.Dispose();
44+
}
45+
}

TUnit.Core/TestContext.Output.cs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ void ITestOutput.AttachArtifact(Artifact artifact)
3232
}
3333

3434
string ITestOutput.GetStandardOutput() => GetOutput();
35-
string ITestOutput.GetErrorOutput() => GetErrorOutput();
35+
string ITestOutput.GetErrorOutput() => GetOutputError();
3636

3737
void ITestOutput.WriteLine(string message)
3838
{
@@ -46,7 +46,47 @@ void ITestOutput.WriteError(string message)
4646
_errorWriter.WriteLine(message);
4747
}
4848

49-
internal string GetOutput() => _outputWriter?.ToString() ?? string.Empty;
49+
/// <summary>
50+
/// Gets the combined build-time and execution-time standard output.
51+
/// </summary>
52+
public override string GetStandardOutput()
53+
{
54+
return GetOutput();
55+
}
56+
57+
/// <summary>
58+
/// Gets the combined build-time and execution-time error output.
59+
/// </summary>
60+
public override string GetErrorOutput()
61+
{
62+
return GetOutputError();
63+
}
64+
65+
internal string GetOutput()
66+
{
67+
var buildOutput = _buildTimeOutput ?? string.Empty;
68+
var baseOutput = base.GetStandardOutput(); // Get output from base class (Context)
69+
var writerOutput = _outputWriter?.ToString() ?? string.Empty;
5070

51-
internal new string GetErrorOutput() => _errorWriter?.ToString() ?? string.Empty;
71+
// Combine all three sources: build-time, base class output, and writer output
72+
var parts = new[] { buildOutput, baseOutput, writerOutput }
73+
.Where(s => !string.IsNullOrEmpty(s))
74+
.ToArray();
75+
76+
return parts.Length == 0 ? string.Empty : string.Join(Environment.NewLine, parts);
77+
}
78+
79+
internal string GetOutputError()
80+
{
81+
var buildErrorOutput = _buildTimeErrorOutput ?? string.Empty;
82+
var baseErrorOutput = base.GetErrorOutput(); // Get error output from base class (Context)
83+
var writerErrorOutput = _errorWriter?.ToString() ?? string.Empty;
84+
85+
// Combine all three sources: build-time error, base class error output, and writer error output
86+
var parts = new[] { buildErrorOutput, baseErrorOutput, writerErrorOutput }
87+
.Where(s => !string.IsNullOrEmpty(s))
88+
.ToArray();
89+
90+
return parts.Length == 0 ? string.Empty : string.Join(Environment.NewLine, parts);
91+
}
5292
}

TUnit.Core/TestContext.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ public TestContext(string testName, IServiceProvider serviceProvider, ClassHookC
5555

5656
private StringWriter? _errorWriter;
5757

58+
private string? _buildTimeOutput;
59+
private string? _buildTimeErrorOutput;
60+
5861
public static new TestContext? Current
5962
{
6063
get => TestContexts.Value;
@@ -152,5 +155,13 @@ internal override void SetAsyncLocalContext()
152155
internal ConcurrentDictionary<int, HashSet<object>> TrackedObjects =>
153156
_trackedObjects ??= new();
154157

155-
158+
/// <summary>
159+
/// Sets the output captured during test building phase.
160+
/// This output is prepended to the test's execution output.
161+
/// </summary>
162+
internal void SetBuildTimeOutput(string? output, string? errorOutput)
163+
{
164+
_buildTimeOutput = output;
165+
_buildTimeErrorOutput = errorOutput;
166+
}
156167
}

TUnit.Engine/Building/TestBuilder.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ public async Task<IEnumerable<AbstractExecutableTest>> BuildTestsFromMetadataAsy
136136

137137
try
138138
{
139+
// Create a context for capturing output during test building
140+
using var buildContext = new TestBuildContext();
141+
TestBuildContext.Current = buildContext;
142+
139143
// Handle GenericTestMetadata with ConcreteInstantiations
140144
if (metadata is GenericTestMetadata { ConcreteInstantiations.Count: > 0 } genericMetadata)
141145
{
@@ -508,6 +512,18 @@ public async Task<IEnumerable<AbstractExecutableTest>> BuildTestsFromMetadataAsy
508512
tests.Add(test);
509513
}
510514
}
515+
516+
// Transfer captured build-time output to all test contexts
517+
var capturedOutput = buildContext.GetCapturedOutput();
518+
var capturedErrorOutput = buildContext.GetCapturedErrorOutput();
519+
520+
if (!string.IsNullOrEmpty(capturedOutput) || !string.IsNullOrEmpty(capturedErrorOutput))
521+
{
522+
foreach (var test in tests)
523+
{
524+
test.Context.SetBuildTimeOutput(capturedOutput, capturedErrorOutput);
525+
}
526+
}
511527
}
512528
catch (Exception ex)
513529
{

TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,8 +399,8 @@ namespace
399399
public void AddAsyncLocalValues() { }
400400
public void Dispose() { }
401401
public . GetDefaultLogger() { }
402-
public string GetErrorOutput() { }
403-
public string GetStandardOutput() { }
402+
public virtual string GetErrorOutput() { }
403+
public virtual string GetStandardOutput() { }
404404
public void RestoreExecutionContext() { }
405405
}
406406
public class ContextProvider : .
@@ -1241,6 +1241,14 @@ namespace
12411241
{
12421242
public TestAttribute([.] string file = "", [.] int line = 0) { }
12431243
}
1244+
public sealed class TestBuildContext : .Context,
1245+
{
1246+
public TestBuildContext() { }
1247+
public new static .TestBuildContext? Current { get; }
1248+
public new void Dispose() { }
1249+
public string GetCapturedErrorOutput() { }
1250+
public string GetCapturedOutput() { }
1251+
}
12441252
public class TestBuilderContext : <.TestBuilderContext>
12451253
{
12461254
public TestBuilderContext() { }
@@ -1291,6 +1299,8 @@ namespace
12911299
public static string? OutputDirectory { get; }
12921300
public static .<string, .<string>> Parameters { get; }
12931301
public static string WorkingDirectory { get; set; }
1302+
public override string GetErrorOutput() { }
1303+
public override string GetStandardOutput() { }
12941304
public static .TestContext? GetById(string id) { }
12951305
}
12961306
public class TestContextEvents : .

TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,8 +399,8 @@ namespace
399399
public void AddAsyncLocalValues() { }
400400
public void Dispose() { }
401401
public . GetDefaultLogger() { }
402-
public string GetErrorOutput() { }
403-
public string GetStandardOutput() { }
402+
public virtual string GetErrorOutput() { }
403+
public virtual string GetStandardOutput() { }
404404
public void RestoreExecutionContext() { }
405405
}
406406
public class ContextProvider : .
@@ -1241,6 +1241,14 @@ namespace
12411241
{
12421242
public TestAttribute([.] string file = "", [.] int line = 0) { }
12431243
}
1244+
public sealed class TestBuildContext : .Context,
1245+
{
1246+
public TestBuildContext() { }
1247+
public new static .TestBuildContext? Current { get; }
1248+
public new void Dispose() { }
1249+
public string GetCapturedErrorOutput() { }
1250+
public string GetCapturedOutput() { }
1251+
}
12441252
public class TestBuilderContext : <.TestBuilderContext>
12451253
{
12461254
public TestBuilderContext() { }
@@ -1291,6 +1299,8 @@ namespace
12911299
public static string? OutputDirectory { get; }
12921300
public static .<string, .<string>> Parameters { get; }
12931301
public static string WorkingDirectory { get; set; }
1302+
public override string GetErrorOutput() { }
1303+
public override string GetStandardOutput() { }
12941304
public static .TestContext? GetById(string id) { }
12951305
}
12961306
public class TestContextEvents : .

TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,8 +399,8 @@ namespace
399399
public void AddAsyncLocalValues() { }
400400
public void Dispose() { }
401401
public . GetDefaultLogger() { }
402-
public string GetErrorOutput() { }
403-
public string GetStandardOutput() { }
402+
public virtual string GetErrorOutput() { }
403+
public virtual string GetStandardOutput() { }
404404
public void RestoreExecutionContext() { }
405405
}
406406
public class ContextProvider : .
@@ -1241,6 +1241,14 @@ namespace
12411241
{
12421242
public TestAttribute([.] string file = "", [.] int line = 0) { }
12431243
}
1244+
public sealed class TestBuildContext : .Context,
1245+
{
1246+
public TestBuildContext() { }
1247+
public new static .TestBuildContext? Current { get; }
1248+
public new void Dispose() { }
1249+
public string GetCapturedErrorOutput() { }
1250+
public string GetCapturedOutput() { }
1251+
}
12441252
public class TestBuilderContext : <.TestBuilderContext>
12451253
{
12461254
public TestBuilderContext() { }
@@ -1291,6 +1299,8 @@ namespace
12911299
public static string? OutputDirectory { get; }
12921300
public static .<string, .<string>> Parameters { get; }
12931301
public static string WorkingDirectory { get; set; }
1302+
public override string GetErrorOutput() { }
1303+
public override string GetStandardOutput() { }
12941304
public static .TestContext? GetById(string id) { }
12951305
}
12961306
public class TestContextEvents : .

TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,8 @@ namespace
379379
public void AddAsyncLocalValues() { }
380380
public void Dispose() { }
381381
public . GetDefaultLogger() { }
382-
public string GetErrorOutput() { }
383-
public string GetStandardOutput() { }
382+
public virtual string GetErrorOutput() { }
383+
public virtual string GetStandardOutput() { }
384384
public void RestoreExecutionContext() { }
385385
}
386386
public class ContextProvider : .
@@ -1196,6 +1196,14 @@ namespace
11961196
{
11971197
public TestAttribute([.] string file = "", [.] int line = 0) { }
11981198
}
1199+
public sealed class TestBuildContext : .Context,
1200+
{
1201+
public TestBuildContext() { }
1202+
public new static .TestBuildContext? Current { get; }
1203+
public new void Dispose() { }
1204+
public string GetCapturedErrorOutput() { }
1205+
public string GetCapturedOutput() { }
1206+
}
11991207
public class TestBuilderContext : <.TestBuilderContext>
12001208
{
12011209
public TestBuilderContext() { }
@@ -1246,6 +1254,8 @@ namespace
12461254
public static string? OutputDirectory { get; }
12471255
public static .<string, .<string>> Parameters { get; }
12481256
public static string WorkingDirectory { get; set; }
1257+
public override string GetErrorOutput() { }
1258+
public override string GetStandardOutput() { }
12491259
public static .TestContext? GetById(string id) { }
12501260
}
12511261
public class TestContextEvents : .

0 commit comments

Comments
 (0)