Replace Report.ExpandClassTimeline with [ClassTimeline] attribute#5875
Replace Report.ExpandClassTimeline with [ClassTimeline] attribute#5875
Conversation
) The global setting from #5847 was too coarse for repos that mix BDD [DependsOn] chains with classes of independent tests. This swaps it for a per-class opt-in: [ClassTimeline(TimelineMode.FullExecution)] public class OrderProcessingFlow { ... } Class-level wins over assembly-level via IScopedAttribute. Attribute writes a custom property at discovery; the JS reads it from the group's first test — no new reporter code path, no global setting.
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| Security | 1 critical |
🟢 Metrics 0 complexity
Metric Results Complexity 0
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Code Review — Replace Report.ExpandClassTimeline with [ClassTimeline] attribute
Summary
Clean, well-motivated migration. Per-class opt-in is the right design for this use case — a global toggle is genuinely wrong for mixed codebases (BDD chains + independent test classes side-by-side). The IScopedAttribute pattern is the established TUnit convention for this kind of attribute-priority question. Overall this is good work; a few things worth calling out below.
What's good
- Design alignment — Using
IScopedAttributewithScopeType => typeof(ClassTimelineAttribute)mirrors[NotInParallel]exactly. Class-wins-over-assembly scoping comes for free from the runtime with no bespoke logic. - Zero data-pipeline change — The attribute pushes data via
DiscoveredTestContext.AddPropertyat discovery time; the existingTestExtensions.cs → ReportTestResult.CustomPropertiesflow carries it through untouched. No new coupling between the core attribute and the reporter. - Contract test is good —
GenerateHtml_RoundTrips_ClassTimeline_CustomProperty_OnTestpins the C#/JS string contract ("tunit.report.timeline"/"FullExecution") at a level that will catch regressions if either side changes. - JS defensive coding —
isClassTimelineFullExecutioncorrectly handlesgroup.testsbeing falsy/empty before calling.some(...), so no runtime exception on an empty group.
Issues / Suggestions
1. [ClassTimeline(TimelineMode.Collapsed)] writes a custom property that has no visible effect
// ClassTimelineAttribute.cs
public ValueTask OnTestDiscovered(DiscoveredTestContext context)
{
context.AddProperty(ClassTimelinePropertyKey, Mode.ToString()); // always writes
return default;
}When Mode == Collapsed, the JS check (value === 'FullExecution') returns false, so the property is written but ignored. This is not a bug, but it is wasteful and potentially confusing: a user who explicitly writes [ClassTimeline(TimelineMode.Collapsed)] gets no change in behaviour over having no attribute at all.
Suggested fix: guard the write, or document why the no-op write is intentional:
public ValueTask OnTestDiscovered(DiscoveredTestContext context)
{
if (Mode != TimelineMode.Collapsed)
context.AddProperty(ClassTimelinePropertyKey, Mode.ToString());
return default;
}Alternatively, remove Collapsed from the public API entirely — or give it a concrete opt-out semantic so the class-level value can override an assembly-level FullExecution. That second interpretation is arguably more useful: [ClassTimeline(TimelineMode.Collapsed)] on a class inside an assembly tagged [assembly: ClassTimeline(TimelineMode.FullExecution)] would explicitly collapse just that class. The current JS only checks for FullExecution, so Collapsed already works as an override in practice, but the docs for Collapsed say "Same as the implicit default" rather than "Explicit opt-out when assembly is FullExecution" — the intent should be clearer.
2. No test covering assembly-level usage or class-wins-over-assembly scoping
The PR adds [AttributeUsage(Class | Assembly)] and mentions IScopedAttribute handles precedence, but neither ClassTimelineAttribute_Exposes_Mode_And_ScopeType nor any other new test exercises the assembly-level path or proves that a class-level attribute overrides an assembly-level one.
The runtime machinery for IScopedAttribute is covered elsewhere, but a simple test asserting the expected scoping behaviour would make the contract explicit and protect against a future regression where someone changes ScopeType:
[Test]
public void ClassTimeline_ClassLevelWins_OverAssemblyLevel()
{
// IScopedAttribute contract: same ScopeType → class-level instance wins
var asm = new ClassTimelineAttribute(TimelineMode.FullExecution);
var klass = new ClassTimelineAttribute(TimelineMode.Collapsed);
asm.ScopeType.ShouldBe(klass.ScopeType); // same type → runtime de-dupes by scope
}This is minor, but the PR description explicitly calls out this scoping as a key design decision.
3. ClassTimelinePropertyKey is internal but the roundtrip test accesses it directly
// HtmlReporterTests.cs
new ReportKeyValue { Key = ClassTimelineAttribute.ClassTimelinePropertyKey, ... }This works today only because InternalsVisibleTo (or a shared test assembly) grants access. If the visibility is narrowed, the test breaks silently. The string literal "tunit.report.timeline" is also hardcoded in embedded.ShouldContain(...) two lines later — so the constant isn't being used consistently within the same test. Either use the constant throughout, or accept the literal everywhere and drop the internal const. Mixed usage is the worst of both.
Dual-mode check (CLAUDE.md requirement)
ITestDiscoveryEventReceiver.OnTestDiscovered is part of the core interfaces invoked by both the source-gen and reflection paths. No source-generator changes are needed here since the attribute carries its own discovery logic. This appears to satisfy the dual-mode requirement.
Verdict
Approve with the above noted. Issue #1 (the Collapsed no-op semantic) is the most worth addressing before merge because it affects public API clarity. Issues #2 and #3 are minor and can be addressed in a follow-up if preferred.
Code Review — Replace
|
Closes #5871.
Summary
TUnitSettings.Default.Report.ExpandClassTimeline(added in Show multi-step test spans in class timeline, align report ordering with execution, and correlate linked OTel activities #5847) with a per-class opt-in attribute[ClassTimeline(TimelineMode.FullExecution)].IScopedAttribute(same pattern as[NotInParallel]).tunit.report.timelinecustom property at discovery; existingTestExtensions.csplumbing already flows that intoReportTestResult.CustomProperties. The JS reads it fromgroup.tests[0].customPropertiesto gate the timeline branch.Why per-class beats a global flag
Most projects mix BDD
[DependsOn]chains (where the multi-step view is gold) with independent-test classes (where it just adds noise). The single global toggle from #5847 forced an all-or-nothing trade. Per-class lets BDD classes opt in without polluting the rest of the report.API surface change
Removed:
TUnit.Core.Settings.ReportSettingsTUnit.Core.Settings.TUnitSettings.ReportAdded:
TUnit.Core.ClassTimelineAttribute(TimelineMode)—[AttributeUsage(Class | Assembly)]TUnit.Core.Enums.TimelineMode { Collapsed = 0, FullExecution = 1 }ReportSettingsshipped in #5847 with no downstream consumers, so this is a clean swap rather than a breaking change in practice. PublicAPI snapshots updated for net4.7/net8/net9/net10.Test plan
TUnit.Engine.Tests/HtmlReporterTests(13/13 pass) — added two cases: attribute exposesMode/ScopeType;tunit.report.timeline=FullExecutionround-trips into the embedded report JSON viaReportTestResult.CustomProperties.TUnit.UnitTests/TUnitSettingsTests(6/6 pass) — reverted the_savedExpandClassTimelinesnapshot/restore additions from Show multi-step test spans in class timeline, align report ordering with execution, and correlate linked OTel activities #5847.TUnit.PublicAPICore net10 verified (snapshots updated for all four TFMs).TUnit.slnxbuild clean (0 errors, pre-existing warnings only).[ClassTimeline(TimelineMode.FullExecution)]on a 3-test BDD chain in a sample project, run with the HTML reporter, confirm the class timeline shows test-case spans + child activities (and a sibling class without the attribute still shows only suite/init/dispose). Recommend the reviewer eyeballs this on real output before merge.