Skip to content

Align CTRF report writer overwrite behavior and add public IArtifactNamingService#9780

Closed
Evangelink wants to merge 1 commit into
mainfrom
dev/amauryleve/artifact-naming-service
Closed

Align CTRF report writer overwrite behavior and add public IArtifactNamingService#9780
Evangelink wants to merge 1 commit into
mainfrom
dev/amauryleve/artifact-naming-service

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Two related changes to MTP report file naming.

1. Align CTRF report writer to overwrite + warn (matches TRX/HTML/JUnit)

CtrfReportEngine was the only report extension that, for a default-generated file name, used FileMode.CreateNew plus a _1/_2 disambiguating-suffix retry loop instead of overwriting. This change aligns it with the shared rule already used by the TRX, HTML and JUnit report extensions: always overwrite (FileMode.Create) and emit a warning when the file already existed, giving users a single, predictable rule.

  • CtrfReportEngine.FileWriter.cs: replaced WriteWithRetryAsync (and the SplitCtrfExtension helper) with a small WriteAsync that mirrors HtmlReportEngine.
  • CtrfReportEngine.cs: GenerateReportCoreAsync now calls WriteAsync and no longer threads the fileNameExplicitlyProvided flag.
  • CtrfReportEngineTests.cs: default-name test now expects FileMode.Create; GenerateReportAsync_AppendsDisambiguatingSuffix_When_DefaultFileExists was replaced by GenerateReportAsync_OverwritesAndWarns_When_DefaultFileExists, and the IOException test was renamed to ..._When_WriteFails — both mirroring the HtmlReportEngineTests equivalents. (27/27 passing.)

The CtrfReportFileExistsAndWillBeOverwritten resource already existed, so no resx/xlf changes were needed.

2. New public, injectable IArtifactNamingService in MTP core

Exposes file-name templating as a public, injectable core service so it can be consumed outside the existing linked-source ([Embedded] helper) pattern.

  • IArtifactNamingService (public) — single member ResolveFileName(string template); expands the standard placeholders ({pname}, {pid}, {asm}, {tfm}, {arch}, {time}) and sanitizes the leaf file name while preserving any directory portion.
  • ArtifactNamingService (internal) — wraps the existing ArtifactNamingHelper templating with process/name/time resolution from ITestApplicationModuleInfo/IEnvironment/IClock.
  • ArtifactFileNameSanitizer (internal) — a core-internal copy of the invalid-char/reserved-name sanitization from the extension-side ReportFileNameSanitizer (which has shipped InternalAPI entries and is intentionally left in place; the two are marked for future consolidation).
  • Registered as a common service in TestHostBuilder.CommonServices.cs and resolvable via a new public ServiceProviderExtensions.GetArtifactNamingService(...) accessor.
  • Public/internal API tracking files updated; new ArtifactNamingServiceTests (4/4 passing) cover placeholder expansion, sanitization, directory preservation, and unknown-placeholder passthrough.

Verification

Built Microsoft.Testing.Platform.UnitTests and Microsoft.Testing.Extensions.UnitTests and ran the targeted filters:

  • ArtifactNamingService — 4/4 passing
  • CtrfReportEngineTests — 27/27 passing

⚠️ Heads-up: pre-existing, unrelated baseline break

There is a pre-existing repo-wide build break introduced by #9774 that is not addressed here and needs a separate fix: IPC/Serializers/BaseSerializer.cs gained two protected static methods (ReadFields, WriteListPayload) but no matching InternalAPI.Unshipped.txt entries were added, so analyzer RS0051 fails as an error in every project that links that source (Microsoft.Testing.Platform and the HangDump / MSBuild / Retry / TrxReport extensions). A transient local workaround was applied only to verify this PR and then reverted, so it is intentionally not part of this change.

…amingService

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
Member Author

Split into two independent PRs per maintainer request: #9782 (Align CTRF report writer overwrite behavior with TRX/HTML/JUnit) and #9783 (Add public injectable IArtifactNamingService to Microsoft.Testing.Platform). Closing this combined PR in favor of those two.

@Evangelink Evangelink closed this Jul 9, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR makes two related changes to Microsoft.Testing.Platform (MTP) artifact/report file naming:

  1. Aligns the CTRF report writer to the shared "overwrite + warn" rule. CtrfReportEngine was the only report extension that, for default-generated file names, used FileMode.CreateNew plus a _1/_2 disambiguating-suffix retry loop. The change replaces that with a small WriteAsync that always overwrites (FileMode.Create) and warns when the file pre-existed — mirroring the HtmlReportEngine/TrxReportEngine/JUnitReportEngine behavior (verified all three already share this exact rule via ReportEngineBase).
  2. Adds a new public, injectable IArtifactNamingService in MTP core. It exposes file-name templating (placeholder expansion + leaf sanitization) as a common service, backed by an internal ArtifactNamingService (wrapping the existing [Embedded] ArtifactNamingHelper) and a core-internal ArtifactFileNameSanitizer (an intentional, documented copy of the extension-side ReportFileNameSanitizer).

Changes:

  • Replace CTRF's WriteWithRetryAsync/SplitCtrfExtension with an overwrite-and-warn WriteAsync, and drop the now-unused fileNameExplicitlyProvided threading.
  • Introduce IArtifactNamingService (public) + ArtifactNamingService/ArtifactFileNameSanitizer (internal), register it as a common service, and add a ServiceProviderExtensions.GetArtifactNamingService accessor.
  • Update Public/Internal API tracking files and add unit tests for both changes.
Show a summary per file
File Description
src/Platform/Microsoft.Testing.Extensions.CtrfReport/CtrfReportEngine.FileWriter.cs Replaces retry/suffix loop with overwrite-and-warn WriteAsync, matching sibling report engines.
src/Platform/Microsoft.Testing.Extensions.CtrfReport/CtrfReportEngine.cs GenerateReportCoreAsync calls WriteAsync and discards the WasExplicit flag.
src/Platform/Microsoft.Testing.Platform/Services/IArtifactNamingService.cs New public interface with documented ResolveFileName(template) and placeholder list.
src/Platform/Microsoft.Testing.Platform/Services/ArtifactNamingService.cs Internal implementation wrapping ArtifactNamingHelper + leaf sanitization/directory preservation.
src/Platform/Microsoft.Testing.Platform/Services/ArtifactFileNameSanitizer.cs Core-internal copy of ReportFileNameSanitizer, documented as pending consolidation.
src/Platform/Microsoft.Testing.Platform/Services/ServiceProviderExtensions.cs Adds public GetArtifactNamingService accessor.
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.CommonServices.cs Registers ArtifactNamingService as a common service.
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt Declares the new public interface + extension accessor.
src/Platform/Microsoft.Testing.Platform/InternalAPI/InternalAPI.Unshipped.txt Declares the new internal types/members.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Services/ArtifactNamingServiceTests.cs New tests for placeholder expansion, sanitization, directory preservation, unknown placeholders.
test/UnitTests/Microsoft.Testing.Extensions.UnitTests/CtrfReportEngineTests.cs Updates default-name tests to expect FileMode.Create overwrite-and-warn semantics.

Notes from verification (no comments stored):

  • The service resolves correctly through the public accessor: ServiceProvider.GetServicesInternal uses IsInstanceOfType, so registering the concrete ArtifactNamingService satisfies GetRequiredService<IArtifactNamingService>(), and the type is not in InternalOnlyExtensions.
  • The new public interface uses a plain method (no init accessors) and is declared in PublicAPI.Unshipped.txt; InternalAPI entries for the new internal types/members are complete.
  • ArtifactNamingService.ResolveFileName matches the established ReportFileNameHelper.ResolveAndSanitize behavior (sanitize leaf, preserve directory), and ArtifactFileNameSanitizer is a byte-for-byte copy of ReportFileNameSanitizer aside from namespace — duplication is explicitly documented as intentional pending consolidation.
  • The CTRF change leaves no dangling references to the removed helpers, and the base fields it stopped using directly remain in use elsewhere.

Review details

  • Files reviewed: 11/11 changed files
  • Comments generated: 0
  • Review effort level: Medium

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🔍 Build Failure Analysis

Summary — Two new protected static methods on BaseSerializer (introduced in base commit a946ef7) are missing from InternalAPI.Unshipped.txt, triggering RS0051 across all 3 TFMs.

Root cause: Missing internal API declarations for BaseSerializer.ReadFields and BaseSerializer.WriteListPayload<T> (RS0051 × 6)

The base branch commit a946ef7 ("Stabilize extension UIDs and add naming governance") added two protected static methods to BaseSerializer.cs:

  1. ReadFields(Stream stream, Func<ushort, int, bool> tryReadField) — line 359
  2. WriteListPayload<T>(Stream stream, ushort fieldId, T[]? list, Action<Stream, T> writeItem) — line 380

These were not declared in InternalAPI/InternalAPI.Unshipped.txt. Since BaseSerializer is tracked via internal API files (not public), the fix is to add both entries there.

Fix — Append to src/Platform/Microsoft.Testing.Platform/InternalAPI/InternalAPI.Unshipped.txt:

static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.ReadFields(System.IO.Stream! stream, System.Func<ushort, int, bool>! tryReadField) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.WriteListPayload<T>(System.IO.Stream! stream, ushort fieldId, T[]? list, System.Action<System.IO.Stream!, T>! writeItem) -> void

The CrashDump and CtrfReport project failures are cascading — they depend on Microsoft.Testing.Platform which failed to compile.


Build overview
Metric Value
Status ❌ FAILED
Duration 164.1 s
Projects 49
Errors 7 (6 unique RS0051 + 1 "Build failed")
Warnings 0

Failed projects:

Project Duration Reason
Microsoft.Testing.Platform.csproj 46.4 s RS0051 errors (root cause)
Microsoft.Testing.Extensions.CrashDump.csproj 79.3 s Cascading (depends on MTP)
Microsoft.Testing.Extensions.CtrfReport.csproj 78.2 s Cascading (depends on MTP)
All MSBuild errors (7)
Code Project File:Line Message
RS0051 Microsoft.Testing.Platform BaseSerializer.cs:359 ReadFields(...) not part of declared API
RS0051 Microsoft.Testing.Platform BaseSerializer.cs:380 WriteListPayload<T>(...) not part of declared API
RS0051 Microsoft.Testing.Platform BaseSerializer.cs:359 (duplicate — net9.0 TFM)
RS0051 Microsoft.Testing.Platform BaseSerializer.cs:380 (duplicate — net9.0 TFM)
RS0051 Microsoft.Testing.Platform BaseSerializer.cs:359 (duplicate — netstandard2.0 TFM)
RS0051 Microsoft.Testing.Platform BaseSerializer.cs:380 (duplicate — netstandard2.0 TFM)
Build.proj Build failed.

🤖 Generated by the Build Failure Analysis workflow · commit 7591119

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · 168 AIC · ⌖ 6.91 AIC · ⊞ 7.3K · [◷]( · )

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · 168 AIC · ⌖ 6.91 AIC · ⊞ 7.3K ·

Microsoft.Testing.Platform.Services.ArtifactNamingService
Microsoft.Testing.Platform.Services.ArtifactNamingService.ArtifactNamingService(Microsoft.Testing.Platform.Services.ITestApplicationModuleInfo! testApplicationModuleInfo, Microsoft.Testing.Platform.Helpers.IEnvironment! environment, Microsoft.Testing.Platform.Helpers.IClock! clock) -> void
Microsoft.Testing.Platform.Services.ArtifactNamingService.ResolveFileName(string! template) -> string!
static Microsoft.Testing.Platform.Services.ArtifactFileNameSanitizer.ReplaceInvalidFileNameChars(string! fileName) -> string!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The base branch (a946ef7) introduced BaseSerializer.ReadFields and BaseSerializer.WriteListPayload<T> as protected static methods but they were never declared here, causing RS0051 errors across all 3 TFMs.

Suggested change
static Microsoft.Testing.Platform.Services.ArtifactFileNameSanitizer.ReplaceInvalidFileNameChars(string! fileName) -> string!
static Microsoft.Testing.Platform.Services.ArtifactFileNameSanitizer.ReplaceInvalidFileNameChars(string! fileName) -> string!
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.ReadFields(System.IO.Stream! stream, System.Func<ushort, int, bool>! tryReadField) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.WriteListPayload<T>(System.IO.Stream! stream, ushort fieldId, T[]? list, System.Action<System.IO.Stream!, T>! writeItem) -> void

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.

2 participants