Skip to content

Remove duplicated #if NETCOREAPP / #else primitives in BaseSerializer#9776

Merged
Evangelink merged 3 commits into
mainfrom
evangelink-baseserializer-dedup-primitives
Jul 9, 2026
Merged

Remove duplicated #if NETCOREAPP / #else primitives in BaseSerializer#9776
Evangelink merged 3 commits into
mainfrom
evangelink-baseserializer-dedup-primitives

Conversation

@Evangelink

@Evangelink Evangelink commented Jul 9, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #9765.

BaseSerializer.cs previously implemented every read/write primitive twice — once under #if NETCOREAPP (Span<byte> / ArrayPool<byte> / ReadExactly) and once under #else (heap byte[] + stream.Read). The two branches were structurally identical, so adding or fixing a primitive meant editing both.

This collapses the 12 duplicated primitives into a single byte[]-based implementation. The only remaining conditional is a small private ReadExactly helper:

  • On NETCOREAPP it delegates to the framework Stream.ReadExactly.
  • Otherwise it loops until the requested byte count is read, throwing EndOfStreamException on premature EOF.

Why byte[] instead of Span everywhere

Microsoft.Testing.Platform intentionally does not reference System.Memory (dropped in #4652), and this file is shared as source into several other projects. Span/ArrayPool are therefore unavailable on the netstandard2.0 target, so the unified path stays byte[]-based.

Bug fix included

The old #else branch used single stream.Read(...) calls that ignored short reads, which could silently return fewer bytes than requested and corrupt data on the .NET Framework / netstandard2.0 path. Routing all reads through ReadExactly fixes this.

Tests

Added BaseSerializerPartialReadTests, which deserialize through a stream that returns one byte per Read (forcing every length prefix / field id / string payload through the ReadExactly loop) and through a truncated stream (asserting EndOfStreamException). These run on net8.0/net9.0 (framework Stream.ReadExactly path) and net462 (the hand-written loop).

InternalAPI baseline changes (to unblock CI — not part of the refactor)

⚠️ These InternalAPI.Unshipped.txt edits are not part of the BaseSerializer refactor. They declare internal symbols that recently landed on main (via #9752 InternalAPI tracking + #9774 DotnetTest serializer dedup) but were left undeclared, so RS0051 currently fails on main and is inherited by every PR. They are included here only to keep this PR's CI green:

  • BaseSerializer.ReadFields / WriteListPayload — declared in Platform, Extensions.HangDump, Extensions.MSBuild, Extensions.Retry, Extensions.TrxReport (each compiles BaseSerializer.cs as shared source).
  • PlatformServicesConfigurationAdapter — declared in MSTest.TestAdapter. This one is unrelated to this change and was pulled in via the merge with main; happy to split it (and the other baseline updates) into a dedicated PR if preferred.

Validation

  • Full solution build passes with 0 code errors across net8.0, net9.0, and netstandard2.0 (all previously-failing RS0051 errors resolved).
  • No behavioral public/protected member changes to BaseSerializer (bodies only); the API-baseline files above are updated purely to declare pre-existing-on-main symbols.

Copilot AI review requested due to automatic review settings July 9, 2026 09:28

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

Refactors BaseSerializer in Microsoft.Testing.Platform’s IPC serializers to remove duplicated #if NETCOREAPP / #else primitive read/write implementations, consolidating onto a single byte[]-based path and centralizing exact-read behavior to avoid short-read corruption on non-NETCOREAPP targets.

Changes:

  • Removed duplicated NETCOREAPP vs non-NETCOREAPP implementations for serializer primitives, keeping one byte[] implementation.
  • Introduced a private ReadExactly helper that uses Stream.ReadExactly on NETCOREAPP and a looped read on other TFMs.
  • Fixed historical short-read behavior by routing primitive reads through ReadExactly.
Show a summary per file
File Description
src/Platform/Microsoft.Testing.Platform/IPC/Serializers/BaseSerializer.cs Collapses duplicated primitive serialization logic and centralizes exact-read behavior to avoid short-read corruption.

Review details

  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Low

@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.

Note

🤖 Automated review by GitHub Copilot. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

✅ 22/22 dimensions clean — no findings.

Notes on the review:

  • Algorithmic Correctness: ReadExactly loop is correct — properly handles count = 0, short reads, and EOF.
  • IPC Wire Compatibility: Wire format unchanged — same length-prefix encoding for strings, same byte ordering for primitives.
  • Cross-TFM: Stream.ReadExactly(byte[], int, int) correctly guarded behind #if NETCOREAPP (available .NET 7+); fallback loop uses standard Stream.Read.
  • Performance: The removal of stackalloc/ArrayPool on NETCOREAPP in favor of byte[] is a deliberate trade-off for maintainability (no System.Memory reference, shared source). For fixed-size primitives (4–8 bytes) the GC cost is negligible; for string buffers this is bounded by IPC message size.
  • Bug fix: Short-read correction on non-NETCOREAPP is sound — EndOfStreamException matches the framework's Stream.ReadExactly behavior.
  • Public API: No surface changes — internal abstract class with protected static members only.

@github-actions

This comment has been minimized.

@Evangelink Evangelink added the state/needs-review Awaiting review from the team. label Jul 9, 2026
Copilot AI review requested due to automatic review settings July 9, 2026 10:19

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.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Medium

@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🔍 Build Failure Analysis

Summary — The build fails with 6 RS0051 (undeclared API) errors because PlatformServicesConfigurationAdapter and its members are not listed in the InternalAPI/InternalAPI.Unshipped.txt file. This is a merge conflict with main, not caused by the PR's changes.

Root cause: Missing internal API declaration after main introduced InternalAPI tracking

A recent commit to main (merged between b81ed0d and 1e92d898) added InternalAPI/InternalAPI.Shipped.txt and InternalAPI/InternalAPI.Unshipped.txt as AdditionalFiles in MSTest.TestAdapter.csproj. These files instruct the Public API Analyzer to track all internal symbols visible through InternalsVisibleTo.

The existing internal sealed class PlatformServicesConfigurationAdapter (and its two members) were not added to either InternalAPI file when tracking was introduced. When this PR is merge-queued against the current main, the analyzer fires RS0051 for the 3 undeclared symbols (× 2 TFMs = 6 errors).

Affected file / errors

Code TFM File:Line Symbol
RS0051 net8.0, net9.0 PlatformServicesConfigurationAdapter.cs:12 PlatformServicesConfigurationAdapter (class)
RS0051 net8.0, net9.0 PlatformServicesConfigurationAdapter.cs:16 .ctor(IConfiguration!)
RS0051 net8.0, net9.0 PlatformServicesConfigurationAdapter.cs:19 this[string!].get

Proposed fix

After rebasing on latest main, add the following 3 lines to src/Adapter/MSTest.TestAdapter/InternalAPI/InternalAPI.Unshipped.txt:

 #nullable enable
+Microsoft.VisualStudio.TestTools.UnitTesting.PlatformServicesConfigurationAdapter
+Microsoft.VisualStudio.TestTools.UnitTesting.PlatformServicesConfigurationAdapter.PlatformServicesConfigurationAdapter(Microsoft.Testing.Platform.Configurations.IConfiguration! configuration) -> void
+Microsoft.VisualStudio.TestTools.UnitTesting.PlatformServicesConfigurationAdapter.this[string! key].get -> string?

Note: This fix targets a file that doesn't exist on the PR branch yet — it was introduced on main after this PR was created. Rebasing the branch onto current main will pull in the InternalAPI tracking infrastructure, at which point the 3 lines above can be appended to InternalAPI.Unshipped.txt.


Build overview
Field Value
Result FAILED
Duration 241.1 s
MSBuild 18.8.0-preview-26302-115
Solution NonWindowsTests.slnf
Failed project MSTest.TestAdapter.csproj (net8.0 + net9.0)
Error count 7 (6 unique RS0051 + 1 "Build failed")
Warnings 0
All MSBuild errors (7)
# Code Project File:Line Message (truncated)
1 RS0051 MSTest.TestAdapter (net8.0) PlatformServicesConfigurationAdapter.cs:12 Symbol '...PlatformServicesConfigurationAdapter' is not part of the declared API
2 RS0051 MSTest.TestAdapter (net8.0) PlatformServicesConfigurationAdapter.cs:16 Symbol '...PlatformServicesConfigurationAdapter(...)' is not part of the declared API
3 RS0051 MSTest.TestAdapter (net8.0) PlatformServicesConfigurationAdapter.cs:19 Symbol '...this[string! key].get' is not part of the declared API
4 RS0051 MSTest.TestAdapter (net9.0) PlatformServicesConfigurationAdapter.cs:12 Symbol '...PlatformServicesConfigurationAdapter' is not part of the declared API
5 RS0051 MSTest.TestAdapter (net9.0) PlatformServicesConfigurationAdapter.cs:16 Symbol '...PlatformServicesConfigurationAdapter(...)' is not part of the declared API
6 RS0051 MSTest.TestAdapter (net9.0) PlatformServicesConfigurationAdapter.cs:19 Symbol '...this[string! key].get' is not part of the declared API
7 Build.proj Build failed.
Why this isn't caused by this PR's changes

This PR modifies only:

  • src/Platform/Microsoft.Testing.Platform/IPC/Serializers/BaseSerializer.cs (refactor)
  • test/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/BaseSerializerPartialReadTests.cs (new test)

Neither file is in the MSTest.TestAdapter project. The failure is triggered by new InternalAPI tracking infrastructure on main that this PR hasn't picked up yet.


🤖 Generated by the Build Failure Analysis workflow using (a href="(dev.azure.com/redacted) · commit 4c23558

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

Evangelink and others added 3 commits July 9, 2026 13:07
…rimitives

Collapse the 12 dual-implemented read/write primitives into a single
byte[]-based implementation whose only conditional piece is a private
ReadExactly helper, which also fixes the historical short-read bug on the
non-NETCOREAPP path where a single Stream.Read could return fewer bytes
than requested and silently corrupt data.

Fixes #9765

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Existing serializer round-trip tests only use MemoryStream, which never
returns short reads, so the new centralized ReadExactly behavior was
untested. Add tests that deserialize through a one-byte-per-read stream
(round-trips correctly) and a truncated stream (throws EndOfStreamException).
These exercise the framework Stream.ReadExactly path on NETCOREAPP and the
hand-written read-until-complete loop on net462.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…esConfigurationAdapter in InternalAPI

These internal symbols landed on main (ReadFields/WriteListPayload via the
DotnetTest serializer dedup, and PlatformServicesConfigurationAdapter) after
InternalAPI tracking was enabled, but were never added to the declared API,
so RS0051 fires across every project that compiles BaseSerializer.cs as
shared source (Platform, HangDump, MSBuild, Retry, TrxReport) plus
MSTest.TestAdapter. Declaring them in the corresponding InternalAPI.Unshipped
files unblocks the build.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 9, 2026 11:25
@Evangelink
Evangelink force-pushed the evangelink-baseserializer-dedup-primitives branch from 4c23558 to 0724c73 Compare July 9, 2026 11:25
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #9776

GradeTestNotes
A (90–100) new BaseSerializerPartialReadTests.
Deserialize_
WhenStreamIsTruncated_
ThrowsEndOfStreamException
Clear AAA; exact-type exception assertion plus inner-exception type check are solid — consider asserting InnerException is not null first for a cleaner failure message.
A (90–100) new BaseSerializerPartialReadTests.
Deserialize_
WhenStreamReturnsOneBytePerRead_
RoundTripsCorrectly
Three field-equality assertions confirm the partial-read round-trip reconstructs all serialized values correctly. No issues found.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · 64.9 AIC · ⌖ 7.29 AIC · ⊞ 9.5K · [◷]( · )

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.

Review details

  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • Review effort level: Medium

@Evangelink
Evangelink merged commit c42b18c into main Jul 9, 2026
47 checks passed
@Evangelink
Evangelink deleted the evangelink-baseserializer-dedup-primitives branch July 9, 2026 16:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-review Awaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[duplicate-code] Duplicate Code: BaseSerializer.cs Dual #if NETCOREAPP / #else Primitive Read/Write Implementations

3 participants