Add Azure DevOps result-level attachments and run-level coverage upload - #8782

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/azdo-result-attachments
Jun 3, 2026
Merged

Add Azure DevOps result-level attachments and run-level coverage upload#8782
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/azdo-result-attachments

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Part of #5951 — direction 3b (Result-level attachments).

Now that the live-publishing groundwork (#8295, #8297, #8298) is merged, this PR auto-attaches per-test artifacts and code coverage to the Azure DevOps Tests tab while the run is in progress.

What this does

For each failed/error/timeout/cancelled test result that the publisher streams to AzDO, after the batch is accepted and the per-result IDs come back, the publisher uploads (best effort):

  • Every FileArtifactProperty on the TestNode (dumps, screenshots, log files…) as a GeneralAttachment to /Runs/{runId}/Results/{resultId}/attachments.
  • StandardOutputProperty as stdout.log (ConsoleLog type), truncated at 256 KB inline with a ...[truncated] marker.
  • StandardErrorProperty as stderr.log (GeneralAttachment type), same truncation.

Passing tests get no attachments by design (privacy/bandwidth).

For the run itself, the publisher now consumes SessionFileArtifact and uploads *.coverage, *.cobertura.xml, and *.opencover.xml to /Runs/{runId}/attachments with the CodeCoverage attachment type. Each process uploads what it sees on its own message bus — no owner-only directory scan, so no duplicates.

Key design decisions (refined via rubber-duck pass)

  1. No retry of attachments via the publishing queue.PublishTestResultsAsync now returns Task<IReadOnlyList<int>?>. null means "AzDO accepted the batch but the response can't be parsed back to per-result IDs" — callers skip attachments and must not retry (to avoid double-publishing results). Only HTTP transport failures throw and feed the existing 3-attempt retry stack.
  2. ID-to-result matching is validated. The parser checks response[i].automatedTestName == submitted[i].AutomatedTestName before trusting the IDs. Mismatch ⇒ returns null.
  3. 16 MB per-file cap, enforced client-side in TryBuildAttachmentRequest. Larger files are silently skipped (matches AzDO's own limits and avoids base64 OOM).
  4. 5-minute attachment HttpClient timeout (vs the default 100s) to accommodate large dump payloads.
  5. OfType().FirstOrDefault() for stdout/stderr (not SingleOrDefault — duplicate properties shouldn't crash the publisher).

Test coverage

Added 9 new unit tests on top of the existing 22:

  • ConsumeAsync_UploadsAttachmentsForFailedTests — file + stdout + stderr go to the right endpoints with the right types.
  • ConsumeAsync_DoesNotUploadAttachmentsForPassedTests — passed outcome ⇒ zero attachment calls.
  • ConsumeAsync_AttachmentUploadFailureLogsWarningAndDoesNotRetryPublish — upload exception logs AzureDevOpsLivePublishingResultAttachmentFailed and publishes the result only once.
  • ConsumeAsync_PublishReturnsNullSkipsAttachmentsAndDoesNotRetry — ID-parse failure path skips attachments + warns.
  • ConsumeAsync_SkipsOversizedFileAttachment — publisher forwards both files; the client's size cap is responsible for filtering.
  • ConsumeAsync_TruncatesLargeStdoutInline — verifies the 256 KB cap and ...[truncated] marker.
  • OnTestSessionFinishingAsync_UploadsCoverageSessionFileArtifacts*.cobertura.xml / *.opencover.xml / *.coverage all uploaded as CodeCoverage.
  • OnTestSessionFinishingAsync_SkipsNonCoverageSessionFileArtifacts — random *.log ignored.
  • OnTestSessionFinishingAsync_CoverageUploadFailureLogsWarning — warns with AzureDevOpsLivePublishingRunAttachmentFailed.

All 31 AzureDevOpsLivePublishingTests and all 92 ClassName~AzureDevOps tests pass on net9.0.

Files changed

  • AzureDevOpsLivePublishingModels.cs — constants, AzureDevOpsAttachmentTypes, wrapper + attachment record with factories.
  • IAzureDevOpsTestResultsClient.cs — new return type + two new upload methods.
  • AzureDevOpsTestResultsClient.cs — response parsing, attachment URI builders, TryBuildAttachmentRequest with size cap, attachment payload DTO.
  • AzureDevOpsTestResultsPublisher.csSessionFileArtifact consumption, attachment building, FlushPendingResultsAsync ID capture + per-result upload, OnTestSessionFinishingAsync coverage drain.
  • AzureDevOpsResources.resx + 13 xlf files — 3 new warning strings.
  • PACKAGE.md — live publishing + automatic attachments listed under features.
  • AzureDevOpsLivePublishingTests.cs — updated existing fake client + 9 new tests.

Out of scope (still open in #5951)

  • Direction 4 (publish --report-trx TRX as an artifact for completeness when live publishing is on).
  • Direction 5 (telemetry / metrics emission).
  • A user-facing opt-out flag for per-test attachments (deferred to first-feedback).

Part of #5951 (direction 3b — auto-attach per-test artifacts and code
coverage to live-published AzDO test runs).
Publisher (AzureDevOpsTestResultsPublisher):
- Consumes SessionFileArtifact so each process uploads its own coverage
through the message bus (no owner-only directory scan, no duplicates).
- For failed/error/timeout/cancelled outcomes, collects FileArtifactProperty
files plus StandardOutputProperty/StandardErrorProperty inline content as
result-level attachments. stdout/stderr capped at 256 KB UTF-8 inline with
a "...[truncated]" marker. Passing tests get no attachments.
- After a successful PublishTestResultsAsync call, captures returned result
IDs and uploads each attachment to /Runs/{runId}/Results/{id}/attachments.
Upload failures are logged as warnings and never push the result back into
the retry queue.
- After the final flush, drains queued *.coverage / *.cobertura.xml /
*.opencover.xml SessionFileArtifacts as CodeCoverage run-level attachments.
Client (AzureDevOpsTestResultsClient):
- PublishTestResultsAsync now returns Task<IReadOnlyList<int>?>. Null means
"AzDO accepted the batch but the response couldn't be parsed or didn't
match by position+automatedTestName" — callers must NOT retry. Only HTTP
transport failures throw (preserving existing retry behavior).
- New UploadTestResultAttachmentAsync / UploadTestRunAttachmentAsync POST
base64-encoded payloads to /attachments endpoints (api 7.1). Files
>16 MB are skipped client-side via TryBuildAttachmentRequest. Attachment
uploads use a 5-minute HttpClient timeout to accommodate large payloads.
Models:
- New AzureDevOpsLivePublishingConstants.MaxAttachmentSizeBytes (16 MB) and
MaxInlineAttachmentBytes (256 KB).
- New AzureDevOpsAttachmentTypes (GeneralAttachment / ConsoleLog /
CodeCoverage), AzureDevOpsTestCaseResultWithAttachments wrapper, and
AzureDevOpsTestResultAttachment with FromFile / FromString factories.
Tests:
- 9 new tests cover: attachment upload for failed tests, no-attachments for
passing tests, attachment failure logs warning + no retry, publish-null
skips attachments + no retry, oversized file forwarded for client-side
filtering, stdout truncation, coverage SessionFileArtifact upload,
non-coverage files skipped, coverage upload failure logs warning.
- Existing fake client updated for the new IReadOnlyList<int>? return type
and to record attachment calls; existing tests adapted to the wrapper.
PACKAGE.md updated to describe live publishing + automatic attachments.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 3, 2026 00:34

CopilotAI 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 extends the Azure DevOps live test-results publisher in Microsoft.Testing.Extensions.AzureDevOpsReport to upload result-level attachments (file artifacts + stdout/stderr) for failing tests and run-level code coverage attachments (*.coverage, *.cobertura.xml, *.opencover.xml) during the run lifecycle. It also updates the REST client contract to return server-assigned result IDs so attachments can be posted to the correct result rows.

Changes:

  • Change live result publishing to return per-result IDs and use them to upload per-result attachments (best-effort, no re-publish on ID-parse failure).
  • Add run-level attachment upload for coverage session artifacts.
  • Update unit tests, PACKAGE.md feature list, and localized resources for new warnings.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsLivePublishingTests.csUpdates fake client contract (publish returns IDs) and adds unit tests for result/run attachment behaviors.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/AzureDevOpsResources.resxAdds new warning strings for attachment upload failures and ID-parse failure.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.cs.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.de.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.es.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.fr.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.it.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ja.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ko.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pl.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pt-BR.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ru.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.tr.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hans.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hant.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PACKAGE.mdDocuments live publishing + automatic attachments as package features.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/IAzureDevOpsTestResultsClient.csUpdates publish API to return IDs (or null) and adds upload methods for result/run attachments.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsPublisher.csBuilds/queues attachments, uploads per-result attachments after publish, and uploads run-level coverage attachments at session finish.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsClient.csImplements publish response parsing for IDs and adds REST calls + payload building for attachments (size cap, timeouts).
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsLivePublishingModels.csAdds attachment constants/types and attachment DTOs used by publisher/client.

Copilot's findings

  • Files reviewed: 20/20 changed files
  • Comments generated: 6

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Review Summary — PR #8782

The overall design is sound: clear separation between result-level and run-level attachments, the null-return contract on PublishTestResultsAsync correctly prevents double-publishing on ID-parse failure, and the concurrency model (_flushSemaphore guarding _retryResults, ConcurrentQueue for the other collections) is correct. The xlf files were regenerated by the build tool (all new entries have state="new" with source-text placeholders), so that's fine per repo guidelines.

DimensionFindingSeverity
Performance & AllocationsTruncateInline: per-character char[1] heap allocation via [ch] collection expressionMAJOR
Algorithmic CorrectnessTruncateInline: truncation at a high surrogate produces an invalid UTF-16 stringMODERATE
Algorithmic CorrectnessTryBuildAttachmentRequest: TOCTOU between fileInfo.Length check and ReadAllBytesMODERATE
Test CompletenessTruncateInline boundary cases not tested (exact-limit, multi-byte chars, surrogate straddling budget)MODERATE
All other dimensions (17)✅ Clean

No blocking issues found. The three moderate findings should be addressed before merge; the performance finding in particular is straightforward to fix.

Generated by Expert Code Review (on open) for issue #8782 · sonnet46 4.2M

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8782 · sonnet46 6.1M

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

This comment has been minimized.

CopilotAI review requested due to automatic review settings June 3, 2026 15:39

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

Copilot's findings

  • Files reviewed: 20/20 changed files
  • Comments generated: 3

Comment threadsrc/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PACKAGE.md Outdated
@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build fails in AzureDevOpsLivePublishingTests.cs due to two analyzer violations in newly-added test code: one IDE0053 (lambda should use expression body) and five MSTEST0037 (use Assert.Contains instead of Assert.IsTrue + .Any()).

i️ /tmp/binlog-data/ was not populated by the pre-agent steps; analysis was performed directly from /tmp/build-output.log and the workspace source.


Root cause 1: IDE0053 — Block-body lambda where expression body is required

The new ConsumeAsync_SkipsOversizedFileAttachment test assigns UploadTestResultAttachmentAsyncFunc using a block-body lambda whose sole statement is return Task.CompletedTask;. The project enforces IDE0053 as an error, so this must be an expression-body lambda.

Affected files / errors

Proposed fix — collapse the block body (the inline comments can be removed or moved to a separate comment above):

- client.UploadTestResultAttachmentAsyncFunc = (_, _, _, attachment, _) =>- {- // The publisher still queues the oversized attachment; the client side TryBuildAttachmentRequest- // drops it. In this fake we just record the call regardless — the contract is exercised end-to-end- // when running against the real client. For the unit test we only assert what the publisher sends.- return Task.CompletedTask;- };+ // The publisher still queues the oversized attachment; the client side TryBuildAttachmentRequest+ // drops it. In this fake we just record the call regardless — the contract is exercised end-to-end+ // when running against the real client. For the unit test we only assert what the publisher sends.+ client.UploadTestResultAttachmentAsyncFunc = (_, _, _, attachment, _) => Task.CompletedTask;

Root cause 2: MSTEST0037Assert.IsTrue(...Any(...)) should be Assert.Contains

Five assertions in two new tests use Assert.IsTrue(collection.Any(predicate)) instead of the Assert.Contains(collection, predicate) overload that produces clearer failure messages and satisfies the MSTEST0037 analyzer rule.

Affected files / errors

LineCollectionPredicate
537UploadTestResultAttachmentCallsFileName == "small.txt"
538UploadTestResultAttachmentCallsFileName == "big.bin"
621UploadTestRunAttachmentCallsFileName == "results.cobertura.xml"
622UploadTestRunAttachmentCallsFileName == "results.opencover.xml"
623UploadTestRunAttachmentCallsFileName == "results.coverage"

Proposed fix (same pattern for all five):

- Assert.IsTrue(client.UploadTestResultAttachmentCalls.Any(c => c.Attachment.FileName == "small.txt"));- Assert.IsTrue(client.UploadTestResultAttachmentCalls.Any(c => c.Attachment.FileName == "big.bin"));+ Assert.Contains(client.UploadTestResultAttachmentCalls, c => c.Attachment.FileName == "small.txt");+ Assert.Contains(client.UploadTestResultAttachmentCalls, c => c.Attachment.FileName == "big.bin");

All MSBuild errors (6 unique, each repeated for net8.0 and net9.0)
CodeFile:LineMessage
IDE0053AzureDevOpsLivePublishingTests.cs:518Use expression body for lambda expression
MSTEST0037AzureDevOpsLivePublishingTests.cs:537Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:538Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:621Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:622Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:623Use Assert.Contains instead of Assert.IsTrue

🤖 Generated by the Build Failure Analysis workflow using build output log · commit 1f62231

Generated by Build Failure Analysis for issue #8782 · sonnet46 8.3M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8782 · sonnet46 8.3M

- AzureDevOpsLivePublishingTests.cs: convert block-body lambda to expression body and replace Assert.IsTrue(...Any(...)) with Assert.Contains(predicate, collection) (IDE0053 + MSTEST0037 build errors)
- PACKAGE.md: fix CLI option name (--publish-azdo-test-results, not --report-azdo-publish-results)
- AzureDevOpsTestResultsPublisher.cs: read FileInfo.FullName inside the IO/security/PathTooLong best-effort try/catch in TryCreateRunAttachment
- AzureDevOpsTestResultsPublisher.cs: fix misleading OperationCanceledException comment in UploadPendingRunAttachmentsAsync — code rethrows, no re-queue
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) merged commit e5487e9 into mainJun 3, 2026
52 of 58 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/azdo-result-attachments branch June 3, 2026 22:48
Sign up for freeto 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

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add Azure DevOps result-level attachments and run-level coverage upload - #8782

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/azdo-result-attachments
Jun 3, 2026
Merged

Add Azure DevOps result-level attachments and run-level coverage upload#8782
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/azdo-result-attachments

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Part of #5951 — direction 3b (Result-level attachments).

Now that the live-publishing groundwork (#8295, #8297, #8298) is merged, this PR auto-attaches per-test artifacts and code coverage to the Azure DevOps Tests tab while the run is in progress.

What this does

For each failed/error/timeout/cancelled test result that the publisher streams to AzDO, after the batch is accepted and the per-result IDs come back, the publisher uploads (best effort):

  • Every FileArtifactProperty on the TestNode (dumps, screenshots, log files…) as a GeneralAttachment to /Runs/{runId}/Results/{resultId}/attachments.
  • StandardOutputProperty as stdout.log (ConsoleLog type), truncated at 256 KB inline with a ...[truncated] marker.
  • StandardErrorProperty as stderr.log (GeneralAttachment type), same truncation.

Passing tests get no attachments by design (privacy/bandwidth).

For the run itself, the publisher now consumes SessionFileArtifact and uploads *.coverage, *.cobertura.xml, and *.opencover.xml to /Runs/{runId}/attachments with the CodeCoverage attachment type. Each process uploads what it sees on its own message bus — no owner-only directory scan, so no duplicates.

Key design decisions (refined via rubber-duck pass)

  1. No retry of attachments via the publishing queue.PublishTestResultsAsync now returns Task<IReadOnlyList<int>?>. null means "AzDO accepted the batch but the response can't be parsed back to per-result IDs" — callers skip attachments and must not retry (to avoid double-publishing results). Only HTTP transport failures throw and feed the existing 3-attempt retry stack.
  2. ID-to-result matching is validated. The parser checks response[i].automatedTestName == submitted[i].AutomatedTestName before trusting the IDs. Mismatch ⇒ returns null.
  3. 16 MB per-file cap, enforced client-side in TryBuildAttachmentRequest. Larger files are silently skipped (matches AzDO's own limits and avoids base64 OOM).
  4. 5-minute attachment HttpClient timeout (vs the default 100s) to accommodate large dump payloads.
  5. OfType().FirstOrDefault() for stdout/stderr (not SingleOrDefault — duplicate properties shouldn't crash the publisher).

Test coverage

Added 9 new unit tests on top of the existing 22:

  • ConsumeAsync_UploadsAttachmentsForFailedTests — file + stdout + stderr go to the right endpoints with the right types.
  • ConsumeAsync_DoesNotUploadAttachmentsForPassedTests — passed outcome ⇒ zero attachment calls.
  • ConsumeAsync_AttachmentUploadFailureLogsWarningAndDoesNotRetryPublish — upload exception logs AzureDevOpsLivePublishingResultAttachmentFailed and publishes the result only once.
  • ConsumeAsync_PublishReturnsNullSkipsAttachmentsAndDoesNotRetry — ID-parse failure path skips attachments + warns.
  • ConsumeAsync_SkipsOversizedFileAttachment — publisher forwards both files; the client's size cap is responsible for filtering.
  • ConsumeAsync_TruncatesLargeStdoutInline — verifies the 256 KB cap and ...[truncated] marker.
  • OnTestSessionFinishingAsync_UploadsCoverageSessionFileArtifacts*.cobertura.xml / *.opencover.xml / *.coverage all uploaded as CodeCoverage.
  • OnTestSessionFinishingAsync_SkipsNonCoverageSessionFileArtifacts — random *.log ignored.
  • OnTestSessionFinishingAsync_CoverageUploadFailureLogsWarning — warns with AzureDevOpsLivePublishingRunAttachmentFailed.

All 31 AzureDevOpsLivePublishingTests and all 92 ClassName~AzureDevOps tests pass on net9.0.

Files changed

  • AzureDevOpsLivePublishingModels.cs — constants, AzureDevOpsAttachmentTypes, wrapper + attachment record with factories.
  • IAzureDevOpsTestResultsClient.cs — new return type + two new upload methods.
  • AzureDevOpsTestResultsClient.cs — response parsing, attachment URI builders, TryBuildAttachmentRequest with size cap, attachment payload DTO.
  • AzureDevOpsTestResultsPublisher.csSessionFileArtifact consumption, attachment building, FlushPendingResultsAsync ID capture + per-result upload, OnTestSessionFinishingAsync coverage drain.
  • AzureDevOpsResources.resx + 13 xlf files — 3 new warning strings.
  • PACKAGE.md — live publishing + automatic attachments listed under features.
  • AzureDevOpsLivePublishingTests.cs — updated existing fake client + 9 new tests.

Out of scope (still open in #5951)

  • Direction 4 (publish --report-trx TRX as an artifact for completeness when live publishing is on).
  • Direction 5 (telemetry / metrics emission).
  • A user-facing opt-out flag for per-test attachments (deferred to first-feedback).

Part of #5951 (direction 3b — auto-attach per-test artifacts and code
coverage to live-published AzDO test runs).
Publisher (AzureDevOpsTestResultsPublisher):
- Consumes SessionFileArtifact so each process uploads its own coverage
through the message bus (no owner-only directory scan, no duplicates).
- For failed/error/timeout/cancelled outcomes, collects FileArtifactProperty
files plus StandardOutputProperty/StandardErrorProperty inline content as
result-level attachments. stdout/stderr capped at 256 KB UTF-8 inline with
a "...[truncated]" marker. Passing tests get no attachments.
- After a successful PublishTestResultsAsync call, captures returned result
IDs and uploads each attachment to /Runs/{runId}/Results/{id}/attachments.
Upload failures are logged as warnings and never push the result back into
the retry queue.
- After the final flush, drains queued *.coverage / *.cobertura.xml /
*.opencover.xml SessionFileArtifacts as CodeCoverage run-level attachments.
Client (AzureDevOpsTestResultsClient):
- PublishTestResultsAsync now returns Task<IReadOnlyList<int>?>. Null means
"AzDO accepted the batch but the response couldn't be parsed or didn't
match by position+automatedTestName" — callers must NOT retry. Only HTTP
transport failures throw (preserving existing retry behavior).
- New UploadTestResultAttachmentAsync / UploadTestRunAttachmentAsync POST
base64-encoded payloads to /attachments endpoints (api 7.1). Files
>16 MB are skipped client-side via TryBuildAttachmentRequest. Attachment
uploads use a 5-minute HttpClient timeout to accommodate large payloads.
Models:
- New AzureDevOpsLivePublishingConstants.MaxAttachmentSizeBytes (16 MB) and
MaxInlineAttachmentBytes (256 KB).
- New AzureDevOpsAttachmentTypes (GeneralAttachment / ConsoleLog /
CodeCoverage), AzureDevOpsTestCaseResultWithAttachments wrapper, and
AzureDevOpsTestResultAttachment with FromFile / FromString factories.
Tests:
- 9 new tests cover: attachment upload for failed tests, no-attachments for
passing tests, attachment failure logs warning + no retry, publish-null
skips attachments + no retry, oversized file forwarded for client-side
filtering, stdout truncation, coverage SessionFileArtifact upload,
non-coverage files skipped, coverage upload failure logs warning.
- Existing fake client updated for the new IReadOnlyList<int>? return type
and to record attachment calls; existing tests adapted to the wrapper.
PACKAGE.md updated to describe live publishing + automatic attachments.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 3, 2026 00:34

CopilotAI 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 extends the Azure DevOps live test-results publisher in Microsoft.Testing.Extensions.AzureDevOpsReport to upload result-level attachments (file artifacts + stdout/stderr) for failing tests and run-level code coverage attachments (*.coverage, *.cobertura.xml, *.opencover.xml) during the run lifecycle. It also updates the REST client contract to return server-assigned result IDs so attachments can be posted to the correct result rows.

Changes:

  • Change live result publishing to return per-result IDs and use them to upload per-result attachments (best-effort, no re-publish on ID-parse failure).
  • Add run-level attachment upload for coverage session artifacts.
  • Update unit tests, PACKAGE.md feature list, and localized resources for new warnings.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsLivePublishingTests.csUpdates fake client contract (publish returns IDs) and adds unit tests for result/run attachment behaviors.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/AzureDevOpsResources.resxAdds new warning strings for attachment upload failures and ID-parse failure.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.cs.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.de.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.es.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.fr.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.it.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ja.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ko.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pl.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pt-BR.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ru.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.tr.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hans.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hant.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PACKAGE.mdDocuments live publishing + automatic attachments as package features.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/IAzureDevOpsTestResultsClient.csUpdates publish API to return IDs (or null) and adds upload methods for result/run attachments.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsPublisher.csBuilds/queues attachments, uploads per-result attachments after publish, and uploads run-level coverage attachments at session finish.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsClient.csImplements publish response parsing for IDs and adds REST calls + payload building for attachments (size cap, timeouts).
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsLivePublishingModels.csAdds attachment constants/types and attachment DTOs used by publisher/client.

Copilot's findings

  • Files reviewed: 20/20 changed files
  • Comments generated: 6

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Review Summary — PR #8782

The overall design is sound: clear separation between result-level and run-level attachments, the null-return contract on PublishTestResultsAsync correctly prevents double-publishing on ID-parse failure, and the concurrency model (_flushSemaphore guarding _retryResults, ConcurrentQueue for the other collections) is correct. The xlf files were regenerated by the build tool (all new entries have state="new" with source-text placeholders), so that's fine per repo guidelines.

DimensionFindingSeverity
Performance & AllocationsTruncateInline: per-character char[1] heap allocation via [ch] collection expressionMAJOR
Algorithmic CorrectnessTruncateInline: truncation at a high surrogate produces an invalid UTF-16 stringMODERATE
Algorithmic CorrectnessTryBuildAttachmentRequest: TOCTOU between fileInfo.Length check and ReadAllBytesMODERATE
Test CompletenessTruncateInline boundary cases not tested (exact-limit, multi-byte chars, surrogate straddling budget)MODERATE
All other dimensions (17)✅ Clean

No blocking issues found. The three moderate findings should be addressed before merge; the performance finding in particular is straightforward to fix.

Generated by Expert Code Review (on open) for issue #8782 · sonnet46 4.2M

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8782 · sonnet46 6.1M

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

This comment has been minimized.

CopilotAI review requested due to automatic review settings June 3, 2026 15:39

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

Copilot's findings

  • Files reviewed: 20/20 changed files
  • Comments generated: 3

Comment threadsrc/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PACKAGE.md Outdated
@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build fails in AzureDevOpsLivePublishingTests.cs due to two analyzer violations in newly-added test code: one IDE0053 (lambda should use expression body) and five MSTEST0037 (use Assert.Contains instead of Assert.IsTrue + .Any()).

i️ /tmp/binlog-data/ was not populated by the pre-agent steps; analysis was performed directly from /tmp/build-output.log and the workspace source.


Root cause 1: IDE0053 — Block-body lambda where expression body is required

The new ConsumeAsync_SkipsOversizedFileAttachment test assigns UploadTestResultAttachmentAsyncFunc using a block-body lambda whose sole statement is return Task.CompletedTask;. The project enforces IDE0053 as an error, so this must be an expression-body lambda.

Affected files / errors

Proposed fix — collapse the block body (the inline comments can be removed or moved to a separate comment above):

- client.UploadTestResultAttachmentAsyncFunc = (_, _, _, attachment, _) =>- {- // The publisher still queues the oversized attachment; the client side TryBuildAttachmentRequest- // drops it. In this fake we just record the call regardless — the contract is exercised end-to-end- // when running against the real client. For the unit test we only assert what the publisher sends.- return Task.CompletedTask;- };+ // The publisher still queues the oversized attachment; the client side TryBuildAttachmentRequest+ // drops it. In this fake we just record the call regardless — the contract is exercised end-to-end+ // when running against the real client. For the unit test we only assert what the publisher sends.+ client.UploadTestResultAttachmentAsyncFunc = (_, _, _, attachment, _) => Task.CompletedTask;

Root cause 2: MSTEST0037Assert.IsTrue(...Any(...)) should be Assert.Contains

Five assertions in two new tests use Assert.IsTrue(collection.Any(predicate)) instead of the Assert.Contains(collection, predicate) overload that produces clearer failure messages and satisfies the MSTEST0037 analyzer rule.

Affected files / errors

LineCollectionPredicate
537UploadTestResultAttachmentCallsFileName == "small.txt"
538UploadTestResultAttachmentCallsFileName == "big.bin"
621UploadTestRunAttachmentCallsFileName == "results.cobertura.xml"
622UploadTestRunAttachmentCallsFileName == "results.opencover.xml"
623UploadTestRunAttachmentCallsFileName == "results.coverage"

Proposed fix (same pattern for all five):

- Assert.IsTrue(client.UploadTestResultAttachmentCalls.Any(c => c.Attachment.FileName == "small.txt"));- Assert.IsTrue(client.UploadTestResultAttachmentCalls.Any(c => c.Attachment.FileName == "big.bin"));+ Assert.Contains(client.UploadTestResultAttachmentCalls, c => c.Attachment.FileName == "small.txt");+ Assert.Contains(client.UploadTestResultAttachmentCalls, c => c.Attachment.FileName == "big.bin");

All MSBuild errors (6 unique, each repeated for net8.0 and net9.0)
CodeFile:LineMessage
IDE0053AzureDevOpsLivePublishingTests.cs:518Use expression body for lambda expression
MSTEST0037AzureDevOpsLivePublishingTests.cs:537Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:538Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:621Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:622Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:623Use Assert.Contains instead of Assert.IsTrue

🤖 Generated by the Build Failure Analysis workflow using build output log · commit 1f62231

Generated by Build Failure Analysis for issue #8782 · sonnet46 8.3M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8782 · sonnet46 8.3M

- AzureDevOpsLivePublishingTests.cs: convert block-body lambda to expression body and replace Assert.IsTrue(...Any(...)) with Assert.Contains(predicate, collection) (IDE0053 + MSTEST0037 build errors)
- PACKAGE.md: fix CLI option name (--publish-azdo-test-results, not --report-azdo-publish-results)
- AzureDevOpsTestResultsPublisher.cs: read FileInfo.FullName inside the IO/security/PathTooLong best-effort try/catch in TryCreateRunAttachment
- AzureDevOpsTestResultsPublisher.cs: fix misleading OperationCanceledException comment in UploadPendingRunAttachmentsAsync — code rethrows, no re-queue
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) merged commit e5487e9 into mainJun 3, 2026
52 of 58 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/azdo-result-attachments branch June 3, 2026 22:48
Sign up for freeto 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

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Azure DevOps result-level attachments and run-level coverage upload - #8782

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/azdo-result-attachments
Jun 3, 2026
Merged

Add Azure DevOps result-level attachments and run-level coverage upload#8782
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/azdo-result-attachments

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Part of #5951 — direction 3b (Result-level attachments).

Now that the live-publishing groundwork (#8295, #8297, #8298) is merged, this PR auto-attaches per-test artifacts and code coverage to the Azure DevOps Tests tab while the run is in progress.

What this does

For each failed/error/timeout/cancelled test result that the publisher streams to AzDO, after the batch is accepted and the per-result IDs come back, the publisher uploads (best effort):

  • Every FileArtifactProperty on the TestNode (dumps, screenshots, log files…) as a GeneralAttachment to /Runs/{runId}/Results/{resultId}/attachments.
  • StandardOutputProperty as stdout.log (ConsoleLog type), truncated at 256 KB inline with a ...[truncated] marker.
  • StandardErrorProperty as stderr.log (GeneralAttachment type), same truncation.

Passing tests get no attachments by design (privacy/bandwidth).

For the run itself, the publisher now consumes SessionFileArtifact and uploads *.coverage, *.cobertura.xml, and *.opencover.xml to /Runs/{runId}/attachments with the CodeCoverage attachment type. Each process uploads what it sees on its own message bus — no owner-only directory scan, so no duplicates.

Key design decisions (refined via rubber-duck pass)

  1. No retry of attachments via the publishing queue.PublishTestResultsAsync now returns Task<IReadOnlyList<int>?>. null means "AzDO accepted the batch but the response can't be parsed back to per-result IDs" — callers skip attachments and must not retry (to avoid double-publishing results). Only HTTP transport failures throw and feed the existing 3-attempt retry stack.
  2. ID-to-result matching is validated. The parser checks response[i].automatedTestName == submitted[i].AutomatedTestName before trusting the IDs. Mismatch ⇒ returns null.
  3. 16 MB per-file cap, enforced client-side in TryBuildAttachmentRequest. Larger files are silently skipped (matches AzDO's own limits and avoids base64 OOM).
  4. 5-minute attachment HttpClient timeout (vs the default 100s) to accommodate large dump payloads.
  5. OfType().FirstOrDefault() for stdout/stderr (not SingleOrDefault — duplicate properties shouldn't crash the publisher).

Test coverage

Added 9 new unit tests on top of the existing 22:

  • ConsumeAsync_UploadsAttachmentsForFailedTests — file + stdout + stderr go to the right endpoints with the right types.
  • ConsumeAsync_DoesNotUploadAttachmentsForPassedTests — passed outcome ⇒ zero attachment calls.
  • ConsumeAsync_AttachmentUploadFailureLogsWarningAndDoesNotRetryPublish — upload exception logs AzureDevOpsLivePublishingResultAttachmentFailed and publishes the result only once.
  • ConsumeAsync_PublishReturnsNullSkipsAttachmentsAndDoesNotRetry — ID-parse failure path skips attachments + warns.
  • ConsumeAsync_SkipsOversizedFileAttachment — publisher forwards both files; the client's size cap is responsible for filtering.
  • ConsumeAsync_TruncatesLargeStdoutInline — verifies the 256 KB cap and ...[truncated] marker.
  • OnTestSessionFinishingAsync_UploadsCoverageSessionFileArtifacts*.cobertura.xml / *.opencover.xml / *.coverage all uploaded as CodeCoverage.
  • OnTestSessionFinishingAsync_SkipsNonCoverageSessionFileArtifacts — random *.log ignored.
  • OnTestSessionFinishingAsync_CoverageUploadFailureLogsWarning — warns with AzureDevOpsLivePublishingRunAttachmentFailed.

All 31 AzureDevOpsLivePublishingTests and all 92 ClassName~AzureDevOps tests pass on net9.0.

Files changed

  • AzureDevOpsLivePublishingModels.cs — constants, AzureDevOpsAttachmentTypes, wrapper + attachment record with factories.
  • IAzureDevOpsTestResultsClient.cs — new return type + two new upload methods.
  • AzureDevOpsTestResultsClient.cs — response parsing, attachment URI builders, TryBuildAttachmentRequest with size cap, attachment payload DTO.
  • AzureDevOpsTestResultsPublisher.csSessionFileArtifact consumption, attachment building, FlushPendingResultsAsync ID capture + per-result upload, OnTestSessionFinishingAsync coverage drain.
  • AzureDevOpsResources.resx + 13 xlf files — 3 new warning strings.
  • PACKAGE.md — live publishing + automatic attachments listed under features.
  • AzureDevOpsLivePublishingTests.cs — updated existing fake client + 9 new tests.

Out of scope (still open in #5951)

  • Direction 4 (publish --report-trx TRX as an artifact for completeness when live publishing is on).
  • Direction 5 (telemetry / metrics emission).
  • A user-facing opt-out flag for per-test attachments (deferred to first-feedback).

Part of #5951 (direction 3b — auto-attach per-test artifacts and code
coverage to live-published AzDO test runs).
Publisher (AzureDevOpsTestResultsPublisher):
- Consumes SessionFileArtifact so each process uploads its own coverage
through the message bus (no owner-only directory scan, no duplicates).
- For failed/error/timeout/cancelled outcomes, collects FileArtifactProperty
files plus StandardOutputProperty/StandardErrorProperty inline content as
result-level attachments. stdout/stderr capped at 256 KB UTF-8 inline with
a "...[truncated]" marker. Passing tests get no attachments.
- After a successful PublishTestResultsAsync call, captures returned result
IDs and uploads each attachment to /Runs/{runId}/Results/{id}/attachments.
Upload failures are logged as warnings and never push the result back into
the retry queue.
- After the final flush, drains queued *.coverage / *.cobertura.xml /
*.opencover.xml SessionFileArtifacts as CodeCoverage run-level attachments.
Client (AzureDevOpsTestResultsClient):
- PublishTestResultsAsync now returns Task<IReadOnlyList<int>?>. Null means
"AzDO accepted the batch but the response couldn't be parsed or didn't
match by position+automatedTestName" — callers must NOT retry. Only HTTP
transport failures throw (preserving existing retry behavior).
- New UploadTestResultAttachmentAsync / UploadTestRunAttachmentAsync POST
base64-encoded payloads to /attachments endpoints (api 7.1). Files
>16 MB are skipped client-side via TryBuildAttachmentRequest. Attachment
uploads use a 5-minute HttpClient timeout to accommodate large payloads.
Models:
- New AzureDevOpsLivePublishingConstants.MaxAttachmentSizeBytes (16 MB) and
MaxInlineAttachmentBytes (256 KB).
- New AzureDevOpsAttachmentTypes (GeneralAttachment / ConsoleLog /
CodeCoverage), AzureDevOpsTestCaseResultWithAttachments wrapper, and
AzureDevOpsTestResultAttachment with FromFile / FromString factories.
Tests:
- 9 new tests cover: attachment upload for failed tests, no-attachments for
passing tests, attachment failure logs warning + no retry, publish-null
skips attachments + no retry, oversized file forwarded for client-side
filtering, stdout truncation, coverage SessionFileArtifact upload,
non-coverage files skipped, coverage upload failure logs warning.
- Existing fake client updated for the new IReadOnlyList<int>? return type
and to record attachment calls; existing tests adapted to the wrapper.
PACKAGE.md updated to describe live publishing + automatic attachments.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 3, 2026 00:34

CopilotAI 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 extends the Azure DevOps live test-results publisher in Microsoft.Testing.Extensions.AzureDevOpsReport to upload result-level attachments (file artifacts + stdout/stderr) for failing tests and run-level code coverage attachments (*.coverage, *.cobertura.xml, *.opencover.xml) during the run lifecycle. It also updates the REST client contract to return server-assigned result IDs so attachments can be posted to the correct result rows.

Changes:

  • Change live result publishing to return per-result IDs and use them to upload per-result attachments (best-effort, no re-publish on ID-parse failure).
  • Add run-level attachment upload for coverage session artifacts.
  • Update unit tests, PACKAGE.md feature list, and localized resources for new warnings.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsLivePublishingTests.csUpdates fake client contract (publish returns IDs) and adds unit tests for result/run attachment behaviors.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/AzureDevOpsResources.resxAdds new warning strings for attachment upload failures and ID-parse failure.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.cs.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.de.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.es.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.fr.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.it.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ja.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ko.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pl.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pt-BR.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ru.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.tr.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hans.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hant.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PACKAGE.mdDocuments live publishing + automatic attachments as package features.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/IAzureDevOpsTestResultsClient.csUpdates publish API to return IDs (or null) and adds upload methods for result/run attachments.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsPublisher.csBuilds/queues attachments, uploads per-result attachments after publish, and uploads run-level coverage attachments at session finish.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsClient.csImplements publish response parsing for IDs and adds REST calls + payload building for attachments (size cap, timeouts).
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsLivePublishingModels.csAdds attachment constants/types and attachment DTOs used by publisher/client.

Copilot's findings

  • Files reviewed: 20/20 changed files
  • Comments generated: 6

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Review Summary — PR #8782

The overall design is sound: clear separation between result-level and run-level attachments, the null-return contract on PublishTestResultsAsync correctly prevents double-publishing on ID-parse failure, and the concurrency model (_flushSemaphore guarding _retryResults, ConcurrentQueue for the other collections) is correct. The xlf files were regenerated by the build tool (all new entries have state="new" with source-text placeholders), so that's fine per repo guidelines.

DimensionFindingSeverity
Performance & AllocationsTruncateInline: per-character char[1] heap allocation via [ch] collection expressionMAJOR
Algorithmic CorrectnessTruncateInline: truncation at a high surrogate produces an invalid UTF-16 stringMODERATE
Algorithmic CorrectnessTryBuildAttachmentRequest: TOCTOU between fileInfo.Length check and ReadAllBytesMODERATE
Test CompletenessTruncateInline boundary cases not tested (exact-limit, multi-byte chars, surrogate straddling budget)MODERATE
All other dimensions (17)✅ Clean

No blocking issues found. The three moderate findings should be addressed before merge; the performance finding in particular is straightforward to fix.

Generated by Expert Code Review (on open) for issue #8782 · sonnet46 4.2M

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8782 · sonnet46 6.1M

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

This comment has been minimized.

CopilotAI review requested due to automatic review settings June 3, 2026 15:39

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

Copilot's findings

  • Files reviewed: 20/20 changed files
  • Comments generated: 3

Comment threadsrc/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PACKAGE.md Outdated
@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build fails in AzureDevOpsLivePublishingTests.cs due to two analyzer violations in newly-added test code: one IDE0053 (lambda should use expression body) and five MSTEST0037 (use Assert.Contains instead of Assert.IsTrue + .Any()).

i️ /tmp/binlog-data/ was not populated by the pre-agent steps; analysis was performed directly from /tmp/build-output.log and the workspace source.


Root cause 1: IDE0053 — Block-body lambda where expression body is required

The new ConsumeAsync_SkipsOversizedFileAttachment test assigns UploadTestResultAttachmentAsyncFunc using a block-body lambda whose sole statement is return Task.CompletedTask;. The project enforces IDE0053 as an error, so this must be an expression-body lambda.

Affected files / errors

Proposed fix — collapse the block body (the inline comments can be removed or moved to a separate comment above):

- client.UploadTestResultAttachmentAsyncFunc = (_, _, _, attachment, _) =>- {- // The publisher still queues the oversized attachment; the client side TryBuildAttachmentRequest- // drops it. In this fake we just record the call regardless — the contract is exercised end-to-end- // when running against the real client. For the unit test we only assert what the publisher sends.- return Task.CompletedTask;- };+ // The publisher still queues the oversized attachment; the client side TryBuildAttachmentRequest+ // drops it. In this fake we just record the call regardless — the contract is exercised end-to-end+ // when running against the real client. For the unit test we only assert what the publisher sends.+ client.UploadTestResultAttachmentAsyncFunc = (_, _, _, attachment, _) => Task.CompletedTask;

Root cause 2: MSTEST0037Assert.IsTrue(...Any(...)) should be Assert.Contains

Five assertions in two new tests use Assert.IsTrue(collection.Any(predicate)) instead of the Assert.Contains(collection, predicate) overload that produces clearer failure messages and satisfies the MSTEST0037 analyzer rule.

Affected files / errors

LineCollectionPredicate
537UploadTestResultAttachmentCallsFileName == "small.txt"
538UploadTestResultAttachmentCallsFileName == "big.bin"
621UploadTestRunAttachmentCallsFileName == "results.cobertura.xml"
622UploadTestRunAttachmentCallsFileName == "results.opencover.xml"
623UploadTestRunAttachmentCallsFileName == "results.coverage"

Proposed fix (same pattern for all five):

- Assert.IsTrue(client.UploadTestResultAttachmentCalls.Any(c => c.Attachment.FileName == "small.txt"));- Assert.IsTrue(client.UploadTestResultAttachmentCalls.Any(c => c.Attachment.FileName == "big.bin"));+ Assert.Contains(client.UploadTestResultAttachmentCalls, c => c.Attachment.FileName == "small.txt");+ Assert.Contains(client.UploadTestResultAttachmentCalls, c => c.Attachment.FileName == "big.bin");

All MSBuild errors (6 unique, each repeated for net8.0 and net9.0)
CodeFile:LineMessage
IDE0053AzureDevOpsLivePublishingTests.cs:518Use expression body for lambda expression
MSTEST0037AzureDevOpsLivePublishingTests.cs:537Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:538Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:621Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:622Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:623Use Assert.Contains instead of Assert.IsTrue

🤖 Generated by the Build Failure Analysis workflow using build output log · commit 1f62231

Generated by Build Failure Analysis for issue #8782 · sonnet46 8.3M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8782 · sonnet46 8.3M

- AzureDevOpsLivePublishingTests.cs: convert block-body lambda to expression body and replace Assert.IsTrue(...Any(...)) with Assert.Contains(predicate, collection) (IDE0053 + MSTEST0037 build errors)
- PACKAGE.md: fix CLI option name (--publish-azdo-test-results, not --report-azdo-publish-results)
- AzureDevOpsTestResultsPublisher.cs: read FileInfo.FullName inside the IO/security/PathTooLong best-effort try/catch in TryCreateRunAttachment
- AzureDevOpsTestResultsPublisher.cs: fix misleading OperationCanceledException comment in UploadPendingRunAttachmentsAsync — code rethrows, no re-queue
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) merged commit e5487e9 into mainJun 3, 2026
52 of 58 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/azdo-result-attachments branch June 3, 2026 22:48
Sign up for freeto 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

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Azure DevOps result-level attachments and run-level coverage upload - #8782

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/azdo-result-attachments
Jun 3, 2026
Merged

Add Azure DevOps result-level attachments and run-level coverage upload#8782
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/azdo-result-attachments

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Part of #5951 — direction 3b (Result-level attachments).

Now that the live-publishing groundwork (#8295, #8297, #8298) is merged, this PR auto-attaches per-test artifacts and code coverage to the Azure DevOps Tests tab while the run is in progress.

What this does

For each failed/error/timeout/cancelled test result that the publisher streams to AzDO, after the batch is accepted and the per-result IDs come back, the publisher uploads (best effort):

  • Every FileArtifactProperty on the TestNode (dumps, screenshots, log files…) as a GeneralAttachment to /Runs/{runId}/Results/{resultId}/attachments.
  • StandardOutputProperty as stdout.log (ConsoleLog type), truncated at 256 KB inline with a ...[truncated] marker.
  • StandardErrorProperty as stderr.log (GeneralAttachment type), same truncation.

Passing tests get no attachments by design (privacy/bandwidth).

For the run itself, the publisher now consumes SessionFileArtifact and uploads *.coverage, *.cobertura.xml, and *.opencover.xml to /Runs/{runId}/attachments with the CodeCoverage attachment type. Each process uploads what it sees on its own message bus — no owner-only directory scan, so no duplicates.

Key design decisions (refined via rubber-duck pass)

  1. No retry of attachments via the publishing queue.PublishTestResultsAsync now returns Task<IReadOnlyList<int>?>. null means "AzDO accepted the batch but the response can't be parsed back to per-result IDs" — callers skip attachments and must not retry (to avoid double-publishing results). Only HTTP transport failures throw and feed the existing 3-attempt retry stack.
  2. ID-to-result matching is validated. The parser checks response[i].automatedTestName == submitted[i].AutomatedTestName before trusting the IDs. Mismatch ⇒ returns null.
  3. 16 MB per-file cap, enforced client-side in TryBuildAttachmentRequest. Larger files are silently skipped (matches AzDO's own limits and avoids base64 OOM).
  4. 5-minute attachment HttpClient timeout (vs the default 100s) to accommodate large dump payloads.
  5. OfType().FirstOrDefault() for stdout/stderr (not SingleOrDefault — duplicate properties shouldn't crash the publisher).

Test coverage

Added 9 new unit tests on top of the existing 22:

  • ConsumeAsync_UploadsAttachmentsForFailedTests — file + stdout + stderr go to the right endpoints with the right types.
  • ConsumeAsync_DoesNotUploadAttachmentsForPassedTests — passed outcome ⇒ zero attachment calls.
  • ConsumeAsync_AttachmentUploadFailureLogsWarningAndDoesNotRetryPublish — upload exception logs AzureDevOpsLivePublishingResultAttachmentFailed and publishes the result only once.
  • ConsumeAsync_PublishReturnsNullSkipsAttachmentsAndDoesNotRetry — ID-parse failure path skips attachments + warns.
  • ConsumeAsync_SkipsOversizedFileAttachment — publisher forwards both files; the client's size cap is responsible for filtering.
  • ConsumeAsync_TruncatesLargeStdoutInline — verifies the 256 KB cap and ...[truncated] marker.
  • OnTestSessionFinishingAsync_UploadsCoverageSessionFileArtifacts*.cobertura.xml / *.opencover.xml / *.coverage all uploaded as CodeCoverage.
  • OnTestSessionFinishingAsync_SkipsNonCoverageSessionFileArtifacts — random *.log ignored.
  • OnTestSessionFinishingAsync_CoverageUploadFailureLogsWarning — warns with AzureDevOpsLivePublishingRunAttachmentFailed.

All 31 AzureDevOpsLivePublishingTests and all 92 ClassName~AzureDevOps tests pass on net9.0.

Files changed

  • AzureDevOpsLivePublishingModels.cs — constants, AzureDevOpsAttachmentTypes, wrapper + attachment record with factories.
  • IAzureDevOpsTestResultsClient.cs — new return type + two new upload methods.
  • AzureDevOpsTestResultsClient.cs — response parsing, attachment URI builders, TryBuildAttachmentRequest with size cap, attachment payload DTO.
  • AzureDevOpsTestResultsPublisher.csSessionFileArtifact consumption, attachment building, FlushPendingResultsAsync ID capture + per-result upload, OnTestSessionFinishingAsync coverage drain.
  • AzureDevOpsResources.resx + 13 xlf files — 3 new warning strings.
  • PACKAGE.md — live publishing + automatic attachments listed under features.
  • AzureDevOpsLivePublishingTests.cs — updated existing fake client + 9 new tests.

Out of scope (still open in #5951)

  • Direction 4 (publish --report-trx TRX as an artifact for completeness when live publishing is on).
  • Direction 5 (telemetry / metrics emission).
  • A user-facing opt-out flag for per-test attachments (deferred to first-feedback).

Part of #5951 (direction 3b — auto-attach per-test artifacts and code
coverage to live-published AzDO test runs).
Publisher (AzureDevOpsTestResultsPublisher):
- Consumes SessionFileArtifact so each process uploads its own coverage
through the message bus (no owner-only directory scan, no duplicates).
- For failed/error/timeout/cancelled outcomes, collects FileArtifactProperty
files plus StandardOutputProperty/StandardErrorProperty inline content as
result-level attachments. stdout/stderr capped at 256 KB UTF-8 inline with
a "...[truncated]" marker. Passing tests get no attachments.
- After a successful PublishTestResultsAsync call, captures returned result
IDs and uploads each attachment to /Runs/{runId}/Results/{id}/attachments.
Upload failures are logged as warnings and never push the result back into
the retry queue.
- After the final flush, drains queued *.coverage / *.cobertura.xml /
*.opencover.xml SessionFileArtifacts as CodeCoverage run-level attachments.
Client (AzureDevOpsTestResultsClient):
- PublishTestResultsAsync now returns Task<IReadOnlyList<int>?>. Null means
"AzDO accepted the batch but the response couldn't be parsed or didn't
match by position+automatedTestName" — callers must NOT retry. Only HTTP
transport failures throw (preserving existing retry behavior).
- New UploadTestResultAttachmentAsync / UploadTestRunAttachmentAsync POST
base64-encoded payloads to /attachments endpoints (api 7.1). Files
>16 MB are skipped client-side via TryBuildAttachmentRequest. Attachment
uploads use a 5-minute HttpClient timeout to accommodate large payloads.
Models:
- New AzureDevOpsLivePublishingConstants.MaxAttachmentSizeBytes (16 MB) and
MaxInlineAttachmentBytes (256 KB).
- New AzureDevOpsAttachmentTypes (GeneralAttachment / ConsoleLog /
CodeCoverage), AzureDevOpsTestCaseResultWithAttachments wrapper, and
AzureDevOpsTestResultAttachment with FromFile / FromString factories.
Tests:
- 9 new tests cover: attachment upload for failed tests, no-attachments for
passing tests, attachment failure logs warning + no retry, publish-null
skips attachments + no retry, oversized file forwarded for client-side
filtering, stdout truncation, coverage SessionFileArtifact upload,
non-coverage files skipped, coverage upload failure logs warning.
- Existing fake client updated for the new IReadOnlyList<int>? return type
and to record attachment calls; existing tests adapted to the wrapper.
PACKAGE.md updated to describe live publishing + automatic attachments.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 3, 2026 00:34

CopilotAI 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 extends the Azure DevOps live test-results publisher in Microsoft.Testing.Extensions.AzureDevOpsReport to upload result-level attachments (file artifacts + stdout/stderr) for failing tests and run-level code coverage attachments (*.coverage, *.cobertura.xml, *.opencover.xml) during the run lifecycle. It also updates the REST client contract to return server-assigned result IDs so attachments can be posted to the correct result rows.

Changes:

  • Change live result publishing to return per-result IDs and use them to upload per-result attachments (best-effort, no re-publish on ID-parse failure).
  • Add run-level attachment upload for coverage session artifacts.
  • Update unit tests, PACKAGE.md feature list, and localized resources for new warnings.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsLivePublishingTests.csUpdates fake client contract (publish returns IDs) and adds unit tests for result/run attachment behaviors.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/AzureDevOpsResources.resxAdds new warning strings for attachment upload failures and ID-parse failure.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.cs.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.de.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.es.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.fr.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.it.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ja.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ko.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pl.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pt-BR.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ru.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.tr.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hans.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hant.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PACKAGE.mdDocuments live publishing + automatic attachments as package features.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/IAzureDevOpsTestResultsClient.csUpdates publish API to return IDs (or null) and adds upload methods for result/run attachments.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsPublisher.csBuilds/queues attachments, uploads per-result attachments after publish, and uploads run-level coverage attachments at session finish.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsClient.csImplements publish response parsing for IDs and adds REST calls + payload building for attachments (size cap, timeouts).
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsLivePublishingModels.csAdds attachment constants/types and attachment DTOs used by publisher/client.

Copilot's findings

  • Files reviewed: 20/20 changed files
  • Comments generated: 6

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Review Summary — PR #8782

The overall design is sound: clear separation between result-level and run-level attachments, the null-return contract on PublishTestResultsAsync correctly prevents double-publishing on ID-parse failure, and the concurrency model (_flushSemaphore guarding _retryResults, ConcurrentQueue for the other collections) is correct. The xlf files were regenerated by the build tool (all new entries have state="new" with source-text placeholders), so that's fine per repo guidelines.

DimensionFindingSeverity
Performance & AllocationsTruncateInline: per-character char[1] heap allocation via [ch] collection expressionMAJOR
Algorithmic CorrectnessTruncateInline: truncation at a high surrogate produces an invalid UTF-16 stringMODERATE
Algorithmic CorrectnessTryBuildAttachmentRequest: TOCTOU between fileInfo.Length check and ReadAllBytesMODERATE
Test CompletenessTruncateInline boundary cases not tested (exact-limit, multi-byte chars, surrogate straddling budget)MODERATE
All other dimensions (17)✅ Clean

No blocking issues found. The three moderate findings should be addressed before merge; the performance finding in particular is straightforward to fix.

Generated by Expert Code Review (on open) for issue #8782 · sonnet46 4.2M

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8782 · sonnet46 6.1M

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

This comment has been minimized.

CopilotAI review requested due to automatic review settings June 3, 2026 15:39

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

Copilot's findings

  • Files reviewed: 20/20 changed files
  • Comments generated: 3

Comment threadsrc/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PACKAGE.md Outdated
@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build fails in AzureDevOpsLivePublishingTests.cs due to two analyzer violations in newly-added test code: one IDE0053 (lambda should use expression body) and five MSTEST0037 (use Assert.Contains instead of Assert.IsTrue + .Any()).

i️ /tmp/binlog-data/ was not populated by the pre-agent steps; analysis was performed directly from /tmp/build-output.log and the workspace source.


Root cause 1: IDE0053 — Block-body lambda where expression body is required

The new ConsumeAsync_SkipsOversizedFileAttachment test assigns UploadTestResultAttachmentAsyncFunc using a block-body lambda whose sole statement is return Task.CompletedTask;. The project enforces IDE0053 as an error, so this must be an expression-body lambda.

Affected files / errors

Proposed fix — collapse the block body (the inline comments can be removed or moved to a separate comment above):

- client.UploadTestResultAttachmentAsyncFunc = (_, _, _, attachment, _) =>- {- // The publisher still queues the oversized attachment; the client side TryBuildAttachmentRequest- // drops it. In this fake we just record the call regardless — the contract is exercised end-to-end- // when running against the real client. For the unit test we only assert what the publisher sends.- return Task.CompletedTask;- };+ // The publisher still queues the oversized attachment; the client side TryBuildAttachmentRequest+ // drops it. In this fake we just record the call regardless — the contract is exercised end-to-end+ // when running against the real client. For the unit test we only assert what the publisher sends.+ client.UploadTestResultAttachmentAsyncFunc = (_, _, _, attachment, _) => Task.CompletedTask;

Root cause 2: MSTEST0037Assert.IsTrue(...Any(...)) should be Assert.Contains

Five assertions in two new tests use Assert.IsTrue(collection.Any(predicate)) instead of the Assert.Contains(collection, predicate) overload that produces clearer failure messages and satisfies the MSTEST0037 analyzer rule.

Affected files / errors

LineCollectionPredicate
537UploadTestResultAttachmentCallsFileName == "small.txt"
538UploadTestResultAttachmentCallsFileName == "big.bin"
621UploadTestRunAttachmentCallsFileName == "results.cobertura.xml"
622UploadTestRunAttachmentCallsFileName == "results.opencover.xml"
623UploadTestRunAttachmentCallsFileName == "results.coverage"

Proposed fix (same pattern for all five):

- Assert.IsTrue(client.UploadTestResultAttachmentCalls.Any(c => c.Attachment.FileName == "small.txt"));- Assert.IsTrue(client.UploadTestResultAttachmentCalls.Any(c => c.Attachment.FileName == "big.bin"));+ Assert.Contains(client.UploadTestResultAttachmentCalls, c => c.Attachment.FileName == "small.txt");+ Assert.Contains(client.UploadTestResultAttachmentCalls, c => c.Attachment.FileName == "big.bin");

All MSBuild errors (6 unique, each repeated for net8.0 and net9.0)
CodeFile:LineMessage
IDE0053AzureDevOpsLivePublishingTests.cs:518Use expression body for lambda expression
MSTEST0037AzureDevOpsLivePublishingTests.cs:537Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:538Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:621Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:622Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:623Use Assert.Contains instead of Assert.IsTrue

🤖 Generated by the Build Failure Analysis workflow using build output log · commit 1f62231

Generated by Build Failure Analysis for issue #8782 · sonnet46 8.3M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8782 · sonnet46 8.3M

- AzureDevOpsLivePublishingTests.cs: convert block-body lambda to expression body and replace Assert.IsTrue(...Any(...)) with Assert.Contains(predicate, collection) (IDE0053 + MSTEST0037 build errors)
- PACKAGE.md: fix CLI option name (--publish-azdo-test-results, not --report-azdo-publish-results)
- AzureDevOpsTestResultsPublisher.cs: read FileInfo.FullName inside the IO/security/PathTooLong best-effort try/catch in TryCreateRunAttachment
- AzureDevOpsTestResultsPublisher.cs: fix misleading OperationCanceledException comment in UploadPendingRunAttachmentsAsync — code rethrows, no re-queue
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) merged commit e5487e9 into mainJun 3, 2026
52 of 58 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/azdo-result-attachments branch June 3, 2026 22:48
Sign up for freeto 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

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add Azure DevOps result-level attachments and run-level coverage upload - #8782

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/azdo-result-attachments
Jun 3, 2026
Merged

Add Azure DevOps result-level attachments and run-level coverage upload#8782
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/azdo-result-attachments

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Part of #5951 — direction 3b (Result-level attachments).

Now that the live-publishing groundwork (#8295, #8297, #8298) is merged, this PR auto-attaches per-test artifacts and code coverage to the Azure DevOps Tests tab while the run is in progress.

What this does

For each failed/error/timeout/cancelled test result that the publisher streams to AzDO, after the batch is accepted and the per-result IDs come back, the publisher uploads (best effort):

  • Every FileArtifactProperty on the TestNode (dumps, screenshots, log files…) as a GeneralAttachment to /Runs/{runId}/Results/{resultId}/attachments.
  • StandardOutputProperty as stdout.log (ConsoleLog type), truncated at 256 KB inline with a ...[truncated] marker.
  • StandardErrorProperty as stderr.log (GeneralAttachment type), same truncation.

Passing tests get no attachments by design (privacy/bandwidth).

For the run itself, the publisher now consumes SessionFileArtifact and uploads *.coverage, *.cobertura.xml, and *.opencover.xml to /Runs/{runId}/attachments with the CodeCoverage attachment type. Each process uploads what it sees on its own message bus — no owner-only directory scan, so no duplicates.

Key design decisions (refined via rubber-duck pass)

  1. No retry of attachments via the publishing queue.PublishTestResultsAsync now returns Task<IReadOnlyList<int>?>. null means "AzDO accepted the batch but the response can't be parsed back to per-result IDs" — callers skip attachments and must not retry (to avoid double-publishing results). Only HTTP transport failures throw and feed the existing 3-attempt retry stack.
  2. ID-to-result matching is validated. The parser checks response[i].automatedTestName == submitted[i].AutomatedTestName before trusting the IDs. Mismatch ⇒ returns null.
  3. 16 MB per-file cap, enforced client-side in TryBuildAttachmentRequest. Larger files are silently skipped (matches AzDO's own limits and avoids base64 OOM).
  4. 5-minute attachment HttpClient timeout (vs the default 100s) to accommodate large dump payloads.
  5. OfType().FirstOrDefault() for stdout/stderr (not SingleOrDefault — duplicate properties shouldn't crash the publisher).

Test coverage

Added 9 new unit tests on top of the existing 22:

  • ConsumeAsync_UploadsAttachmentsForFailedTests — file + stdout + stderr go to the right endpoints with the right types.
  • ConsumeAsync_DoesNotUploadAttachmentsForPassedTests — passed outcome ⇒ zero attachment calls.
  • ConsumeAsync_AttachmentUploadFailureLogsWarningAndDoesNotRetryPublish — upload exception logs AzureDevOpsLivePublishingResultAttachmentFailed and publishes the result only once.
  • ConsumeAsync_PublishReturnsNullSkipsAttachmentsAndDoesNotRetry — ID-parse failure path skips attachments + warns.
  • ConsumeAsync_SkipsOversizedFileAttachment — publisher forwards both files; the client's size cap is responsible for filtering.
  • ConsumeAsync_TruncatesLargeStdoutInline — verifies the 256 KB cap and ...[truncated] marker.
  • OnTestSessionFinishingAsync_UploadsCoverageSessionFileArtifacts*.cobertura.xml / *.opencover.xml / *.coverage all uploaded as CodeCoverage.
  • OnTestSessionFinishingAsync_SkipsNonCoverageSessionFileArtifacts — random *.log ignored.
  • OnTestSessionFinishingAsync_CoverageUploadFailureLogsWarning — warns with AzureDevOpsLivePublishingRunAttachmentFailed.

All 31 AzureDevOpsLivePublishingTests and all 92 ClassName~AzureDevOps tests pass on net9.0.

Files changed

  • AzureDevOpsLivePublishingModels.cs — constants, AzureDevOpsAttachmentTypes, wrapper + attachment record with factories.
  • IAzureDevOpsTestResultsClient.cs — new return type + two new upload methods.
  • AzureDevOpsTestResultsClient.cs — response parsing, attachment URI builders, TryBuildAttachmentRequest with size cap, attachment payload DTO.
  • AzureDevOpsTestResultsPublisher.csSessionFileArtifact consumption, attachment building, FlushPendingResultsAsync ID capture + per-result upload, OnTestSessionFinishingAsync coverage drain.
  • AzureDevOpsResources.resx + 13 xlf files — 3 new warning strings.
  • PACKAGE.md — live publishing + automatic attachments listed under features.
  • AzureDevOpsLivePublishingTests.cs — updated existing fake client + 9 new tests.

Out of scope (still open in #5951)

  • Direction 4 (publish --report-trx TRX as an artifact for completeness when live publishing is on).
  • Direction 5 (telemetry / metrics emission).
  • A user-facing opt-out flag for per-test attachments (deferred to first-feedback).

Part of #5951 (direction 3b — auto-attach per-test artifacts and code
coverage to live-published AzDO test runs).
Publisher (AzureDevOpsTestResultsPublisher):
- Consumes SessionFileArtifact so each process uploads its own coverage
through the message bus (no owner-only directory scan, no duplicates).
- For failed/error/timeout/cancelled outcomes, collects FileArtifactProperty
files plus StandardOutputProperty/StandardErrorProperty inline content as
result-level attachments. stdout/stderr capped at 256 KB UTF-8 inline with
a "...[truncated]" marker. Passing tests get no attachments.
- After a successful PublishTestResultsAsync call, captures returned result
IDs and uploads each attachment to /Runs/{runId}/Results/{id}/attachments.
Upload failures are logged as warnings and never push the result back into
the retry queue.
- After the final flush, drains queued *.coverage / *.cobertura.xml /
*.opencover.xml SessionFileArtifacts as CodeCoverage run-level attachments.
Client (AzureDevOpsTestResultsClient):
- PublishTestResultsAsync now returns Task<IReadOnlyList<int>?>. Null means
"AzDO accepted the batch but the response couldn't be parsed or didn't
match by position+automatedTestName" — callers must NOT retry. Only HTTP
transport failures throw (preserving existing retry behavior).
- New UploadTestResultAttachmentAsync / UploadTestRunAttachmentAsync POST
base64-encoded payloads to /attachments endpoints (api 7.1). Files
>16 MB are skipped client-side via TryBuildAttachmentRequest. Attachment
uploads use a 5-minute HttpClient timeout to accommodate large payloads.
Models:
- New AzureDevOpsLivePublishingConstants.MaxAttachmentSizeBytes (16 MB) and
MaxInlineAttachmentBytes (256 KB).
- New AzureDevOpsAttachmentTypes (GeneralAttachment / ConsoleLog /
CodeCoverage), AzureDevOpsTestCaseResultWithAttachments wrapper, and
AzureDevOpsTestResultAttachment with FromFile / FromString factories.
Tests:
- 9 new tests cover: attachment upload for failed tests, no-attachments for
passing tests, attachment failure logs warning + no retry, publish-null
skips attachments + no retry, oversized file forwarded for client-side
filtering, stdout truncation, coverage SessionFileArtifact upload,
non-coverage files skipped, coverage upload failure logs warning.
- Existing fake client updated for the new IReadOnlyList<int>? return type
and to record attachment calls; existing tests adapted to the wrapper.
PACKAGE.md updated to describe live publishing + automatic attachments.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 3, 2026 00:34

CopilotAI 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 extends the Azure DevOps live test-results publisher in Microsoft.Testing.Extensions.AzureDevOpsReport to upload result-level attachments (file artifacts + stdout/stderr) for failing tests and run-level code coverage attachments (*.coverage, *.cobertura.xml, *.opencover.xml) during the run lifecycle. It also updates the REST client contract to return server-assigned result IDs so attachments can be posted to the correct result rows.

Changes:

  • Change live result publishing to return per-result IDs and use them to upload per-result attachments (best-effort, no re-publish on ID-parse failure).
  • Add run-level attachment upload for coverage session artifacts.
  • Update unit tests, PACKAGE.md feature list, and localized resources for new warnings.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsLivePublishingTests.csUpdates fake client contract (publish returns IDs) and adds unit tests for result/run attachment behaviors.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/AzureDevOpsResources.resxAdds new warning strings for attachment upload failures and ID-parse failure.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.cs.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.de.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.es.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.fr.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.it.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ja.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ko.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pl.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pt-BR.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ru.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.tr.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hans.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hant.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PACKAGE.mdDocuments live publishing + automatic attachments as package features.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/IAzureDevOpsTestResultsClient.csUpdates publish API to return IDs (or null) and adds upload methods for result/run attachments.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsPublisher.csBuilds/queues attachments, uploads per-result attachments after publish, and uploads run-level coverage attachments at session finish.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsClient.csImplements publish response parsing for IDs and adds REST calls + payload building for attachments (size cap, timeouts).
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsLivePublishingModels.csAdds attachment constants/types and attachment DTOs used by publisher/client.

Copilot's findings

  • Files reviewed: 20/20 changed files
  • Comments generated: 6

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Review Summary — PR #8782

The overall design is sound: clear separation between result-level and run-level attachments, the null-return contract on PublishTestResultsAsync correctly prevents double-publishing on ID-parse failure, and the concurrency model (_flushSemaphore guarding _retryResults, ConcurrentQueue for the other collections) is correct. The xlf files were regenerated by the build tool (all new entries have state="new" with source-text placeholders), so that's fine per repo guidelines.

DimensionFindingSeverity
Performance & AllocationsTruncateInline: per-character char[1] heap allocation via [ch] collection expressionMAJOR
Algorithmic CorrectnessTruncateInline: truncation at a high surrogate produces an invalid UTF-16 stringMODERATE
Algorithmic CorrectnessTryBuildAttachmentRequest: TOCTOU between fileInfo.Length check and ReadAllBytesMODERATE
Test CompletenessTruncateInline boundary cases not tested (exact-limit, multi-byte chars, surrogate straddling budget)MODERATE
All other dimensions (17)✅ Clean

No blocking issues found. The three moderate findings should be addressed before merge; the performance finding in particular is straightforward to fix.

Generated by Expert Code Review (on open) for issue #8782 · sonnet46 4.2M

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8782 · sonnet46 6.1M

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

This comment has been minimized.

CopilotAI review requested due to automatic review settings June 3, 2026 15:39

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

Copilot's findings

  • Files reviewed: 20/20 changed files
  • Comments generated: 3

Comment threadsrc/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PACKAGE.md Outdated
@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build fails in AzureDevOpsLivePublishingTests.cs due to two analyzer violations in newly-added test code: one IDE0053 (lambda should use expression body) and five MSTEST0037 (use Assert.Contains instead of Assert.IsTrue + .Any()).

i️ /tmp/binlog-data/ was not populated by the pre-agent steps; analysis was performed directly from /tmp/build-output.log and the workspace source.


Root cause 1: IDE0053 — Block-body lambda where expression body is required

The new ConsumeAsync_SkipsOversizedFileAttachment test assigns UploadTestResultAttachmentAsyncFunc using a block-body lambda whose sole statement is return Task.CompletedTask;. The project enforces IDE0053 as an error, so this must be an expression-body lambda.

Affected files / errors

Proposed fix — collapse the block body (the inline comments can be removed or moved to a separate comment above):

- client.UploadTestResultAttachmentAsyncFunc = (_, _, _, attachment, _) =>- {- // The publisher still queues the oversized attachment; the client side TryBuildAttachmentRequest- // drops it. In this fake we just record the call regardless — the contract is exercised end-to-end- // when running against the real client. For the unit test we only assert what the publisher sends.- return Task.CompletedTask;- };+ // The publisher still queues the oversized attachment; the client side TryBuildAttachmentRequest+ // drops it. In this fake we just record the call regardless — the contract is exercised end-to-end+ // when running against the real client. For the unit test we only assert what the publisher sends.+ client.UploadTestResultAttachmentAsyncFunc = (_, _, _, attachment, _) => Task.CompletedTask;

Root cause 2: MSTEST0037Assert.IsTrue(...Any(...)) should be Assert.Contains

Five assertions in two new tests use Assert.IsTrue(collection.Any(predicate)) instead of the Assert.Contains(collection, predicate) overload that produces clearer failure messages and satisfies the MSTEST0037 analyzer rule.

Affected files / errors

LineCollectionPredicate
537UploadTestResultAttachmentCallsFileName == "small.txt"
538UploadTestResultAttachmentCallsFileName == "big.bin"
621UploadTestRunAttachmentCallsFileName == "results.cobertura.xml"
622UploadTestRunAttachmentCallsFileName == "results.opencover.xml"
623UploadTestRunAttachmentCallsFileName == "results.coverage"

Proposed fix (same pattern for all five):

- Assert.IsTrue(client.UploadTestResultAttachmentCalls.Any(c => c.Attachment.FileName == "small.txt"));- Assert.IsTrue(client.UploadTestResultAttachmentCalls.Any(c => c.Attachment.FileName == "big.bin"));+ Assert.Contains(client.UploadTestResultAttachmentCalls, c => c.Attachment.FileName == "small.txt");+ Assert.Contains(client.UploadTestResultAttachmentCalls, c => c.Attachment.FileName == "big.bin");

All MSBuild errors (6 unique, each repeated for net8.0 and net9.0)
CodeFile:LineMessage
IDE0053AzureDevOpsLivePublishingTests.cs:518Use expression body for lambda expression
MSTEST0037AzureDevOpsLivePublishingTests.cs:537Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:538Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:621Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:622Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:623Use Assert.Contains instead of Assert.IsTrue

🤖 Generated by the Build Failure Analysis workflow using build output log · commit 1f62231

Generated by Build Failure Analysis for issue #8782 · sonnet46 8.3M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8782 · sonnet46 8.3M

- AzureDevOpsLivePublishingTests.cs: convert block-body lambda to expression body and replace Assert.IsTrue(...Any(...)) with Assert.Contains(predicate, collection) (IDE0053 + MSTEST0037 build errors)
- PACKAGE.md: fix CLI option name (--publish-azdo-test-results, not --report-azdo-publish-results)
- AzureDevOpsTestResultsPublisher.cs: read FileInfo.FullName inside the IO/security/PathTooLong best-effort try/catch in TryCreateRunAttachment
- AzureDevOpsTestResultsPublisher.cs: fix misleading OperationCanceledException comment in UploadPendingRunAttachmentsAsync — code rethrows, no re-queue
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) merged commit e5487e9 into mainJun 3, 2026
52 of 58 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/azdo-result-attachments branch June 3, 2026 22:48
Sign up for freeto 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

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Azure DevOps result-level attachments and run-level coverage upload - #8782

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/azdo-result-attachments
Jun 3, 2026
Merged

Add Azure DevOps result-level attachments and run-level coverage upload#8782
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/azdo-result-attachments

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Part of #5951 — direction 3b (Result-level attachments).

Now that the live-publishing groundwork (#8295, #8297, #8298) is merged, this PR auto-attaches per-test artifacts and code coverage to the Azure DevOps Tests tab while the run is in progress.

What this does

For each failed/error/timeout/cancelled test result that the publisher streams to AzDO, after the batch is accepted and the per-result IDs come back, the publisher uploads (best effort):

  • Every FileArtifactProperty on the TestNode (dumps, screenshots, log files…) as a GeneralAttachment to /Runs/{runId}/Results/{resultId}/attachments.
  • StandardOutputProperty as stdout.log (ConsoleLog type), truncated at 256 KB inline with a ...[truncated] marker.
  • StandardErrorProperty as stderr.log (GeneralAttachment type), same truncation.

Passing tests get no attachments by design (privacy/bandwidth).

For the run itself, the publisher now consumes SessionFileArtifact and uploads *.coverage, *.cobertura.xml, and *.opencover.xml to /Runs/{runId}/attachments with the CodeCoverage attachment type. Each process uploads what it sees on its own message bus — no owner-only directory scan, so no duplicates.

Key design decisions (refined via rubber-duck pass)

  1. No retry of attachments via the publishing queue.PublishTestResultsAsync now returns Task<IReadOnlyList<int>?>. null means "AzDO accepted the batch but the response can't be parsed back to per-result IDs" — callers skip attachments and must not retry (to avoid double-publishing results). Only HTTP transport failures throw and feed the existing 3-attempt retry stack.
  2. ID-to-result matching is validated. The parser checks response[i].automatedTestName == submitted[i].AutomatedTestName before trusting the IDs. Mismatch ⇒ returns null.
  3. 16 MB per-file cap, enforced client-side in TryBuildAttachmentRequest. Larger files are silently skipped (matches AzDO's own limits and avoids base64 OOM).
  4. 5-minute attachment HttpClient timeout (vs the default 100s) to accommodate large dump payloads.
  5. OfType().FirstOrDefault() for stdout/stderr (not SingleOrDefault — duplicate properties shouldn't crash the publisher).

Test coverage

Added 9 new unit tests on top of the existing 22:

  • ConsumeAsync_UploadsAttachmentsForFailedTests — file + stdout + stderr go to the right endpoints with the right types.
  • ConsumeAsync_DoesNotUploadAttachmentsForPassedTests — passed outcome ⇒ zero attachment calls.
  • ConsumeAsync_AttachmentUploadFailureLogsWarningAndDoesNotRetryPublish — upload exception logs AzureDevOpsLivePublishingResultAttachmentFailed and publishes the result only once.
  • ConsumeAsync_PublishReturnsNullSkipsAttachmentsAndDoesNotRetry — ID-parse failure path skips attachments + warns.
  • ConsumeAsync_SkipsOversizedFileAttachment — publisher forwards both files; the client's size cap is responsible for filtering.
  • ConsumeAsync_TruncatesLargeStdoutInline — verifies the 256 KB cap and ...[truncated] marker.
  • OnTestSessionFinishingAsync_UploadsCoverageSessionFileArtifacts*.cobertura.xml / *.opencover.xml / *.coverage all uploaded as CodeCoverage.
  • OnTestSessionFinishingAsync_SkipsNonCoverageSessionFileArtifacts — random *.log ignored.
  • OnTestSessionFinishingAsync_CoverageUploadFailureLogsWarning — warns with AzureDevOpsLivePublishingRunAttachmentFailed.

All 31 AzureDevOpsLivePublishingTests and all 92 ClassName~AzureDevOps tests pass on net9.0.

Files changed

  • AzureDevOpsLivePublishingModels.cs — constants, AzureDevOpsAttachmentTypes, wrapper + attachment record with factories.
  • IAzureDevOpsTestResultsClient.cs — new return type + two new upload methods.
  • AzureDevOpsTestResultsClient.cs — response parsing, attachment URI builders, TryBuildAttachmentRequest with size cap, attachment payload DTO.
  • AzureDevOpsTestResultsPublisher.csSessionFileArtifact consumption, attachment building, FlushPendingResultsAsync ID capture + per-result upload, OnTestSessionFinishingAsync coverage drain.
  • AzureDevOpsResources.resx + 13 xlf files — 3 new warning strings.
  • PACKAGE.md — live publishing + automatic attachments listed under features.
  • AzureDevOpsLivePublishingTests.cs — updated existing fake client + 9 new tests.

Out of scope (still open in #5951)

  • Direction 4 (publish --report-trx TRX as an artifact for completeness when live publishing is on).
  • Direction 5 (telemetry / metrics emission).
  • A user-facing opt-out flag for per-test attachments (deferred to first-feedback).

Part of #5951 (direction 3b — auto-attach per-test artifacts and code
coverage to live-published AzDO test runs).
Publisher (AzureDevOpsTestResultsPublisher):
- Consumes SessionFileArtifact so each process uploads its own coverage
through the message bus (no owner-only directory scan, no duplicates).
- For failed/error/timeout/cancelled outcomes, collects FileArtifactProperty
files plus StandardOutputProperty/StandardErrorProperty inline content as
result-level attachments. stdout/stderr capped at 256 KB UTF-8 inline with
a "...[truncated]" marker. Passing tests get no attachments.
- After a successful PublishTestResultsAsync call, captures returned result
IDs and uploads each attachment to /Runs/{runId}/Results/{id}/attachments.
Upload failures are logged as warnings and never push the result back into
the retry queue.
- After the final flush, drains queued *.coverage / *.cobertura.xml /
*.opencover.xml SessionFileArtifacts as CodeCoverage run-level attachments.
Client (AzureDevOpsTestResultsClient):
- PublishTestResultsAsync now returns Task<IReadOnlyList<int>?>. Null means
"AzDO accepted the batch but the response couldn't be parsed or didn't
match by position+automatedTestName" — callers must NOT retry. Only HTTP
transport failures throw (preserving existing retry behavior).
- New UploadTestResultAttachmentAsync / UploadTestRunAttachmentAsync POST
base64-encoded payloads to /attachments endpoints (api 7.1). Files
>16 MB are skipped client-side via TryBuildAttachmentRequest. Attachment
uploads use a 5-minute HttpClient timeout to accommodate large payloads.
Models:
- New AzureDevOpsLivePublishingConstants.MaxAttachmentSizeBytes (16 MB) and
MaxInlineAttachmentBytes (256 KB).
- New AzureDevOpsAttachmentTypes (GeneralAttachment / ConsoleLog /
CodeCoverage), AzureDevOpsTestCaseResultWithAttachments wrapper, and
AzureDevOpsTestResultAttachment with FromFile / FromString factories.
Tests:
- 9 new tests cover: attachment upload for failed tests, no-attachments for
passing tests, attachment failure logs warning + no retry, publish-null
skips attachments + no retry, oversized file forwarded for client-side
filtering, stdout truncation, coverage SessionFileArtifact upload,
non-coverage files skipped, coverage upload failure logs warning.
- Existing fake client updated for the new IReadOnlyList<int>? return type
and to record attachment calls; existing tests adapted to the wrapper.
PACKAGE.md updated to describe live publishing + automatic attachments.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 3, 2026 00:34

CopilotAI 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 extends the Azure DevOps live test-results publisher in Microsoft.Testing.Extensions.AzureDevOpsReport to upload result-level attachments (file artifacts + stdout/stderr) for failing tests and run-level code coverage attachments (*.coverage, *.cobertura.xml, *.opencover.xml) during the run lifecycle. It also updates the REST client contract to return server-assigned result IDs so attachments can be posted to the correct result rows.

Changes:

  • Change live result publishing to return per-result IDs and use them to upload per-result attachments (best-effort, no re-publish on ID-parse failure).
  • Add run-level attachment upload for coverage session artifacts.
  • Update unit tests, PACKAGE.md feature list, and localized resources for new warnings.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsLivePublishingTests.csUpdates fake client contract (publish returns IDs) and adds unit tests for result/run attachment behaviors.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/AzureDevOpsResources.resxAdds new warning strings for attachment upload failures and ID-parse failure.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.cs.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.de.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.es.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.fr.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.it.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ja.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ko.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pl.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pt-BR.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ru.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.tr.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hans.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hant.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PACKAGE.mdDocuments live publishing + automatic attachments as package features.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/IAzureDevOpsTestResultsClient.csUpdates publish API to return IDs (or null) and adds upload methods for result/run attachments.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsPublisher.csBuilds/queues attachments, uploads per-result attachments after publish, and uploads run-level coverage attachments at session finish.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsClient.csImplements publish response parsing for IDs and adds REST calls + payload building for attachments (size cap, timeouts).
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsLivePublishingModels.csAdds attachment constants/types and attachment DTOs used by publisher/client.

Copilot's findings

  • Files reviewed: 20/20 changed files
  • Comments generated: 6

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Review Summary — PR #8782

The overall design is sound: clear separation between result-level and run-level attachments, the null-return contract on PublishTestResultsAsync correctly prevents double-publishing on ID-parse failure, and the concurrency model (_flushSemaphore guarding _retryResults, ConcurrentQueue for the other collections) is correct. The xlf files were regenerated by the build tool (all new entries have state="new" with source-text placeholders), so that's fine per repo guidelines.

DimensionFindingSeverity
Performance & AllocationsTruncateInline: per-character char[1] heap allocation via [ch] collection expressionMAJOR
Algorithmic CorrectnessTruncateInline: truncation at a high surrogate produces an invalid UTF-16 stringMODERATE
Algorithmic CorrectnessTryBuildAttachmentRequest: TOCTOU between fileInfo.Length check and ReadAllBytesMODERATE
Test CompletenessTruncateInline boundary cases not tested (exact-limit, multi-byte chars, surrogate straddling budget)MODERATE
All other dimensions (17)✅ Clean

No blocking issues found. The three moderate findings should be addressed before merge; the performance finding in particular is straightforward to fix.

Generated by Expert Code Review (on open) for issue #8782 · sonnet46 4.2M

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8782 · sonnet46 6.1M

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

This comment has been minimized.

CopilotAI review requested due to automatic review settings June 3, 2026 15:39

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

Copilot's findings

  • Files reviewed: 20/20 changed files
  • Comments generated: 3

Comment threadsrc/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PACKAGE.md Outdated
@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build fails in AzureDevOpsLivePublishingTests.cs due to two analyzer violations in newly-added test code: one IDE0053 (lambda should use expression body) and five MSTEST0037 (use Assert.Contains instead of Assert.IsTrue + .Any()).

i️ /tmp/binlog-data/ was not populated by the pre-agent steps; analysis was performed directly from /tmp/build-output.log and the workspace source.


Root cause 1: IDE0053 — Block-body lambda where expression body is required

The new ConsumeAsync_SkipsOversizedFileAttachment test assigns UploadTestResultAttachmentAsyncFunc using a block-body lambda whose sole statement is return Task.CompletedTask;. The project enforces IDE0053 as an error, so this must be an expression-body lambda.

Affected files / errors

Proposed fix — collapse the block body (the inline comments can be removed or moved to a separate comment above):

- client.UploadTestResultAttachmentAsyncFunc = (_, _, _, attachment, _) =>- {- // The publisher still queues the oversized attachment; the client side TryBuildAttachmentRequest- // drops it. In this fake we just record the call regardless — the contract is exercised end-to-end- // when running against the real client. For the unit test we only assert what the publisher sends.- return Task.CompletedTask;- };+ // The publisher still queues the oversized attachment; the client side TryBuildAttachmentRequest+ // drops it. In this fake we just record the call regardless — the contract is exercised end-to-end+ // when running against the real client. For the unit test we only assert what the publisher sends.+ client.UploadTestResultAttachmentAsyncFunc = (_, _, _, attachment, _) => Task.CompletedTask;

Root cause 2: MSTEST0037Assert.IsTrue(...Any(...)) should be Assert.Contains

Five assertions in two new tests use Assert.IsTrue(collection.Any(predicate)) instead of the Assert.Contains(collection, predicate) overload that produces clearer failure messages and satisfies the MSTEST0037 analyzer rule.

Affected files / errors

LineCollectionPredicate
537UploadTestResultAttachmentCallsFileName == "small.txt"
538UploadTestResultAttachmentCallsFileName == "big.bin"
621UploadTestRunAttachmentCallsFileName == "results.cobertura.xml"
622UploadTestRunAttachmentCallsFileName == "results.opencover.xml"
623UploadTestRunAttachmentCallsFileName == "results.coverage"

Proposed fix (same pattern for all five):

- Assert.IsTrue(client.UploadTestResultAttachmentCalls.Any(c => c.Attachment.FileName == "small.txt"));- Assert.IsTrue(client.UploadTestResultAttachmentCalls.Any(c => c.Attachment.FileName == "big.bin"));+ Assert.Contains(client.UploadTestResultAttachmentCalls, c => c.Attachment.FileName == "small.txt");+ Assert.Contains(client.UploadTestResultAttachmentCalls, c => c.Attachment.FileName == "big.bin");

All MSBuild errors (6 unique, each repeated for net8.0 and net9.0)
CodeFile:LineMessage
IDE0053AzureDevOpsLivePublishingTests.cs:518Use expression body for lambda expression
MSTEST0037AzureDevOpsLivePublishingTests.cs:537Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:538Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:621Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:622Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:623Use Assert.Contains instead of Assert.IsTrue

🤖 Generated by the Build Failure Analysis workflow using build output log · commit 1f62231

Generated by Build Failure Analysis for issue #8782 · sonnet46 8.3M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8782 · sonnet46 8.3M

- AzureDevOpsLivePublishingTests.cs: convert block-body lambda to expression body and replace Assert.IsTrue(...Any(...)) with Assert.Contains(predicate, collection) (IDE0053 + MSTEST0037 build errors)
- PACKAGE.md: fix CLI option name (--publish-azdo-test-results, not --report-azdo-publish-results)
- AzureDevOpsTestResultsPublisher.cs: read FileInfo.FullName inside the IO/security/PathTooLong best-effort try/catch in TryCreateRunAttachment
- AzureDevOpsTestResultsPublisher.cs: fix misleading OperationCanceledException comment in UploadPendingRunAttachmentsAsync — code rethrows, no re-queue
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) merged commit e5487e9 into mainJun 3, 2026
52 of 58 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/azdo-result-attachments branch June 3, 2026 22:48
Sign up for freeto 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

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Azure DevOps result-level attachments and run-level coverage upload - #8782

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/azdo-result-attachments
Jun 3, 2026
Merged

Add Azure DevOps result-level attachments and run-level coverage upload#8782
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/azdo-result-attachments

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Part of #5951 — direction 3b (Result-level attachments).

Now that the live-publishing groundwork (#8295, #8297, #8298) is merged, this PR auto-attaches per-test artifacts and code coverage to the Azure DevOps Tests tab while the run is in progress.

What this does

For each failed/error/timeout/cancelled test result that the publisher streams to AzDO, after the batch is accepted and the per-result IDs come back, the publisher uploads (best effort):

  • Every FileArtifactProperty on the TestNode (dumps, screenshots, log files…) as a GeneralAttachment to /Runs/{runId}/Results/{resultId}/attachments.
  • StandardOutputProperty as stdout.log (ConsoleLog type), truncated at 256 KB inline with a ...[truncated] marker.
  • StandardErrorProperty as stderr.log (GeneralAttachment type), same truncation.

Passing tests get no attachments by design (privacy/bandwidth).

For the run itself, the publisher now consumes SessionFileArtifact and uploads *.coverage, *.cobertura.xml, and *.opencover.xml to /Runs/{runId}/attachments with the CodeCoverage attachment type. Each process uploads what it sees on its own message bus — no owner-only directory scan, so no duplicates.

Key design decisions (refined via rubber-duck pass)

  1. No retry of attachments via the publishing queue.PublishTestResultsAsync now returns Task<IReadOnlyList<int>?>. null means "AzDO accepted the batch but the response can't be parsed back to per-result IDs" — callers skip attachments and must not retry (to avoid double-publishing results). Only HTTP transport failures throw and feed the existing 3-attempt retry stack.
  2. ID-to-result matching is validated. The parser checks response[i].automatedTestName == submitted[i].AutomatedTestName before trusting the IDs. Mismatch ⇒ returns null.
  3. 16 MB per-file cap, enforced client-side in TryBuildAttachmentRequest. Larger files are silently skipped (matches AzDO's own limits and avoids base64 OOM).
  4. 5-minute attachment HttpClient timeout (vs the default 100s) to accommodate large dump payloads.
  5. OfType().FirstOrDefault() for stdout/stderr (not SingleOrDefault — duplicate properties shouldn't crash the publisher).

Test coverage

Added 9 new unit tests on top of the existing 22:

  • ConsumeAsync_UploadsAttachmentsForFailedTests — file + stdout + stderr go to the right endpoints with the right types.
  • ConsumeAsync_DoesNotUploadAttachmentsForPassedTests — passed outcome ⇒ zero attachment calls.
  • ConsumeAsync_AttachmentUploadFailureLogsWarningAndDoesNotRetryPublish — upload exception logs AzureDevOpsLivePublishingResultAttachmentFailed and publishes the result only once.
  • ConsumeAsync_PublishReturnsNullSkipsAttachmentsAndDoesNotRetry — ID-parse failure path skips attachments + warns.
  • ConsumeAsync_SkipsOversizedFileAttachment — publisher forwards both files; the client's size cap is responsible for filtering.
  • ConsumeAsync_TruncatesLargeStdoutInline — verifies the 256 KB cap and ...[truncated] marker.
  • OnTestSessionFinishingAsync_UploadsCoverageSessionFileArtifacts*.cobertura.xml / *.opencover.xml / *.coverage all uploaded as CodeCoverage.
  • OnTestSessionFinishingAsync_SkipsNonCoverageSessionFileArtifacts — random *.log ignored.
  • OnTestSessionFinishingAsync_CoverageUploadFailureLogsWarning — warns with AzureDevOpsLivePublishingRunAttachmentFailed.

All 31 AzureDevOpsLivePublishingTests and all 92 ClassName~AzureDevOps tests pass on net9.0.

Files changed

  • AzureDevOpsLivePublishingModels.cs — constants, AzureDevOpsAttachmentTypes, wrapper + attachment record with factories.
  • IAzureDevOpsTestResultsClient.cs — new return type + two new upload methods.
  • AzureDevOpsTestResultsClient.cs — response parsing, attachment URI builders, TryBuildAttachmentRequest with size cap, attachment payload DTO.
  • AzureDevOpsTestResultsPublisher.csSessionFileArtifact consumption, attachment building, FlushPendingResultsAsync ID capture + per-result upload, OnTestSessionFinishingAsync coverage drain.
  • AzureDevOpsResources.resx + 13 xlf files — 3 new warning strings.
  • PACKAGE.md — live publishing + automatic attachments listed under features.
  • AzureDevOpsLivePublishingTests.cs — updated existing fake client + 9 new tests.

Out of scope (still open in #5951)

  • Direction 4 (publish --report-trx TRX as an artifact for completeness when live publishing is on).
  • Direction 5 (telemetry / metrics emission).
  • A user-facing opt-out flag for per-test attachments (deferred to first-feedback).

Part of #5951 (direction 3b — auto-attach per-test artifacts and code
coverage to live-published AzDO test runs).
Publisher (AzureDevOpsTestResultsPublisher):
- Consumes SessionFileArtifact so each process uploads its own coverage
through the message bus (no owner-only directory scan, no duplicates).
- For failed/error/timeout/cancelled outcomes, collects FileArtifactProperty
files plus StandardOutputProperty/StandardErrorProperty inline content as
result-level attachments. stdout/stderr capped at 256 KB UTF-8 inline with
a "...[truncated]" marker. Passing tests get no attachments.
- After a successful PublishTestResultsAsync call, captures returned result
IDs and uploads each attachment to /Runs/{runId}/Results/{id}/attachments.
Upload failures are logged as warnings and never push the result back into
the retry queue.
- After the final flush, drains queued *.coverage / *.cobertura.xml /
*.opencover.xml SessionFileArtifacts as CodeCoverage run-level attachments.
Client (AzureDevOpsTestResultsClient):
- PublishTestResultsAsync now returns Task<IReadOnlyList<int>?>. Null means
"AzDO accepted the batch but the response couldn't be parsed or didn't
match by position+automatedTestName" — callers must NOT retry. Only HTTP
transport failures throw (preserving existing retry behavior).
- New UploadTestResultAttachmentAsync / UploadTestRunAttachmentAsync POST
base64-encoded payloads to /attachments endpoints (api 7.1). Files
>16 MB are skipped client-side via TryBuildAttachmentRequest. Attachment
uploads use a 5-minute HttpClient timeout to accommodate large payloads.
Models:
- New AzureDevOpsLivePublishingConstants.MaxAttachmentSizeBytes (16 MB) and
MaxInlineAttachmentBytes (256 KB).
- New AzureDevOpsAttachmentTypes (GeneralAttachment / ConsoleLog /
CodeCoverage), AzureDevOpsTestCaseResultWithAttachments wrapper, and
AzureDevOpsTestResultAttachment with FromFile / FromString factories.
Tests:
- 9 new tests cover: attachment upload for failed tests, no-attachments for
passing tests, attachment failure logs warning + no retry, publish-null
skips attachments + no retry, oversized file forwarded for client-side
filtering, stdout truncation, coverage SessionFileArtifact upload,
non-coverage files skipped, coverage upload failure logs warning.
- Existing fake client updated for the new IReadOnlyList<int>? return type
and to record attachment calls; existing tests adapted to the wrapper.
PACKAGE.md updated to describe live publishing + automatic attachments.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 3, 2026 00:34

CopilotAI 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 extends the Azure DevOps live test-results publisher in Microsoft.Testing.Extensions.AzureDevOpsReport to upload result-level attachments (file artifacts + stdout/stderr) for failing tests and run-level code coverage attachments (*.coverage, *.cobertura.xml, *.opencover.xml) during the run lifecycle. It also updates the REST client contract to return server-assigned result IDs so attachments can be posted to the correct result rows.

Changes:

  • Change live result publishing to return per-result IDs and use them to upload per-result attachments (best-effort, no re-publish on ID-parse failure).
  • Add run-level attachment upload for coverage session artifacts.
  • Update unit tests, PACKAGE.md feature list, and localized resources for new warnings.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsLivePublishingTests.csUpdates fake client contract (publish returns IDs) and adds unit tests for result/run attachment behaviors.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/AzureDevOpsResources.resxAdds new warning strings for attachment upload failures and ID-parse failure.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.cs.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.de.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.es.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.fr.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.it.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ja.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ko.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pl.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pt-BR.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ru.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.tr.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hans.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hant.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PACKAGE.mdDocuments live publishing + automatic attachments as package features.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/IAzureDevOpsTestResultsClient.csUpdates publish API to return IDs (or null) and adds upload methods for result/run attachments.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsPublisher.csBuilds/queues attachments, uploads per-result attachments after publish, and uploads run-level coverage attachments at session finish.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsClient.csImplements publish response parsing for IDs and adds REST calls + payload building for attachments (size cap, timeouts).
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsLivePublishingModels.csAdds attachment constants/types and attachment DTOs used by publisher/client.

Copilot's findings

  • Files reviewed: 20/20 changed files
  • Comments generated: 6

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Review Summary — PR #8782

The overall design is sound: clear separation between result-level and run-level attachments, the null-return contract on PublishTestResultsAsync correctly prevents double-publishing on ID-parse failure, and the concurrency model (_flushSemaphore guarding _retryResults, ConcurrentQueue for the other collections) is correct. The xlf files were regenerated by the build tool (all new entries have state="new" with source-text placeholders), so that's fine per repo guidelines.

DimensionFindingSeverity
Performance & AllocationsTruncateInline: per-character char[1] heap allocation via [ch] collection expressionMAJOR
Algorithmic CorrectnessTruncateInline: truncation at a high surrogate produces an invalid UTF-16 stringMODERATE
Algorithmic CorrectnessTryBuildAttachmentRequest: TOCTOU between fileInfo.Length check and ReadAllBytesMODERATE
Test CompletenessTruncateInline boundary cases not tested (exact-limit, multi-byte chars, surrogate straddling budget)MODERATE
All other dimensions (17)✅ Clean

No blocking issues found. The three moderate findings should be addressed before merge; the performance finding in particular is straightforward to fix.

Generated by Expert Code Review (on open) for issue #8782 · sonnet46 4.2M

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8782 · sonnet46 6.1M

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

This comment has been minimized.

CopilotAI review requested due to automatic review settings June 3, 2026 15:39

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

Copilot's findings

  • Files reviewed: 20/20 changed files
  • Comments generated: 3

Comment threadsrc/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PACKAGE.md Outdated
@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build fails in AzureDevOpsLivePublishingTests.cs due to two analyzer violations in newly-added test code: one IDE0053 (lambda should use expression body) and five MSTEST0037 (use Assert.Contains instead of Assert.IsTrue + .Any()).

i️ /tmp/binlog-data/ was not populated by the pre-agent steps; analysis was performed directly from /tmp/build-output.log and the workspace source.


Root cause 1: IDE0053 — Block-body lambda where expression body is required

The new ConsumeAsync_SkipsOversizedFileAttachment test assigns UploadTestResultAttachmentAsyncFunc using a block-body lambda whose sole statement is return Task.CompletedTask;. The project enforces IDE0053 as an error, so this must be an expression-body lambda.

Affected files / errors

Proposed fix — collapse the block body (the inline comments can be removed or moved to a separate comment above):

- client.UploadTestResultAttachmentAsyncFunc = (_, _, _, attachment, _) =>- {- // The publisher still queues the oversized attachment; the client side TryBuildAttachmentRequest- // drops it. In this fake we just record the call regardless — the contract is exercised end-to-end- // when running against the real client. For the unit test we only assert what the publisher sends.- return Task.CompletedTask;- };+ // The publisher still queues the oversized attachment; the client side TryBuildAttachmentRequest+ // drops it. In this fake we just record the call regardless — the contract is exercised end-to-end+ // when running against the real client. For the unit test we only assert what the publisher sends.+ client.UploadTestResultAttachmentAsyncFunc = (_, _, _, attachment, _) => Task.CompletedTask;

Root cause 2: MSTEST0037Assert.IsTrue(...Any(...)) should be Assert.Contains

Five assertions in two new tests use Assert.IsTrue(collection.Any(predicate)) instead of the Assert.Contains(collection, predicate) overload that produces clearer failure messages and satisfies the MSTEST0037 analyzer rule.

Affected files / errors

LineCollectionPredicate
537UploadTestResultAttachmentCallsFileName == "small.txt"
538UploadTestResultAttachmentCallsFileName == "big.bin"
621UploadTestRunAttachmentCallsFileName == "results.cobertura.xml"
622UploadTestRunAttachmentCallsFileName == "results.opencover.xml"
623UploadTestRunAttachmentCallsFileName == "results.coverage"

Proposed fix (same pattern for all five):

- Assert.IsTrue(client.UploadTestResultAttachmentCalls.Any(c => c.Attachment.FileName == "small.txt"));- Assert.IsTrue(client.UploadTestResultAttachmentCalls.Any(c => c.Attachment.FileName == "big.bin"));+ Assert.Contains(client.UploadTestResultAttachmentCalls, c => c.Attachment.FileName == "small.txt");+ Assert.Contains(client.UploadTestResultAttachmentCalls, c => c.Attachment.FileName == "big.bin");

All MSBuild errors (6 unique, each repeated for net8.0 and net9.0)
CodeFile:LineMessage
IDE0053AzureDevOpsLivePublishingTests.cs:518Use expression body for lambda expression
MSTEST0037AzureDevOpsLivePublishingTests.cs:537Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:538Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:621Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:622Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:623Use Assert.Contains instead of Assert.IsTrue

🤖 Generated by the Build Failure Analysis workflow using build output log · commit 1f62231

Generated by Build Failure Analysis for issue #8782 · sonnet46 8.3M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8782 · sonnet46 8.3M

- AzureDevOpsLivePublishingTests.cs: convert block-body lambda to expression body and replace Assert.IsTrue(...Any(...)) with Assert.Contains(predicate, collection) (IDE0053 + MSTEST0037 build errors)
- PACKAGE.md: fix CLI option name (--publish-azdo-test-results, not --report-azdo-publish-results)
- AzureDevOpsTestResultsPublisher.cs: read FileInfo.FullName inside the IO/security/PathTooLong best-effort try/catch in TryCreateRunAttachment
- AzureDevOpsTestResultsPublisher.cs: fix misleading OperationCanceledException comment in UploadPendingRunAttachmentsAsync — code rethrows, no re-queue
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) merged commit e5487e9 into mainJun 3, 2026
52 of 58 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/azdo-result-attachments branch June 3, 2026 22:48
Sign up for freeto 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

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add Azure DevOps result-level attachments and run-level coverage upload - #8782

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/azdo-result-attachments
Jun 3, 2026
Merged

Add Azure DevOps result-level attachments and run-level coverage upload#8782
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
dev/amauryleve/azdo-result-attachments

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Part of #5951 — direction 3b (Result-level attachments).

Now that the live-publishing groundwork (#8295, #8297, #8298) is merged, this PR auto-attaches per-test artifacts and code coverage to the Azure DevOps Tests tab while the run is in progress.

What this does

For each failed/error/timeout/cancelled test result that the publisher streams to AzDO, after the batch is accepted and the per-result IDs come back, the publisher uploads (best effort):

  • Every FileArtifactProperty on the TestNode (dumps, screenshots, log files…) as a GeneralAttachment to /Runs/{runId}/Results/{resultId}/attachments.
  • StandardOutputProperty as stdout.log (ConsoleLog type), truncated at 256 KB inline with a ...[truncated] marker.
  • StandardErrorProperty as stderr.log (GeneralAttachment type), same truncation.

Passing tests get no attachments by design (privacy/bandwidth).

For the run itself, the publisher now consumes SessionFileArtifact and uploads *.coverage, *.cobertura.xml, and *.opencover.xml to /Runs/{runId}/attachments with the CodeCoverage attachment type. Each process uploads what it sees on its own message bus — no owner-only directory scan, so no duplicates.

Key design decisions (refined via rubber-duck pass)

  1. No retry of attachments via the publishing queue.PublishTestResultsAsync now returns Task<IReadOnlyList<int>?>. null means "AzDO accepted the batch but the response can't be parsed back to per-result IDs" — callers skip attachments and must not retry (to avoid double-publishing results). Only HTTP transport failures throw and feed the existing 3-attempt retry stack.
  2. ID-to-result matching is validated. The parser checks response[i].automatedTestName == submitted[i].AutomatedTestName before trusting the IDs. Mismatch ⇒ returns null.
  3. 16 MB per-file cap, enforced client-side in TryBuildAttachmentRequest. Larger files are silently skipped (matches AzDO's own limits and avoids base64 OOM).
  4. 5-minute attachment HttpClient timeout (vs the default 100s) to accommodate large dump payloads.
  5. OfType().FirstOrDefault() for stdout/stderr (not SingleOrDefault — duplicate properties shouldn't crash the publisher).

Test coverage

Added 9 new unit tests on top of the existing 22:

  • ConsumeAsync_UploadsAttachmentsForFailedTests — file + stdout + stderr go to the right endpoints with the right types.
  • ConsumeAsync_DoesNotUploadAttachmentsForPassedTests — passed outcome ⇒ zero attachment calls.
  • ConsumeAsync_AttachmentUploadFailureLogsWarningAndDoesNotRetryPublish — upload exception logs AzureDevOpsLivePublishingResultAttachmentFailed and publishes the result only once.
  • ConsumeAsync_PublishReturnsNullSkipsAttachmentsAndDoesNotRetry — ID-parse failure path skips attachments + warns.
  • ConsumeAsync_SkipsOversizedFileAttachment — publisher forwards both files; the client's size cap is responsible for filtering.
  • ConsumeAsync_TruncatesLargeStdoutInline — verifies the 256 KB cap and ...[truncated] marker.
  • OnTestSessionFinishingAsync_UploadsCoverageSessionFileArtifacts*.cobertura.xml / *.opencover.xml / *.coverage all uploaded as CodeCoverage.
  • OnTestSessionFinishingAsync_SkipsNonCoverageSessionFileArtifacts — random *.log ignored.
  • OnTestSessionFinishingAsync_CoverageUploadFailureLogsWarning — warns with AzureDevOpsLivePublishingRunAttachmentFailed.

All 31 AzureDevOpsLivePublishingTests and all 92 ClassName~AzureDevOps tests pass on net9.0.

Files changed

  • AzureDevOpsLivePublishingModels.cs — constants, AzureDevOpsAttachmentTypes, wrapper + attachment record with factories.
  • IAzureDevOpsTestResultsClient.cs — new return type + two new upload methods.
  • AzureDevOpsTestResultsClient.cs — response parsing, attachment URI builders, TryBuildAttachmentRequest with size cap, attachment payload DTO.
  • AzureDevOpsTestResultsPublisher.csSessionFileArtifact consumption, attachment building, FlushPendingResultsAsync ID capture + per-result upload, OnTestSessionFinishingAsync coverage drain.
  • AzureDevOpsResources.resx + 13 xlf files — 3 new warning strings.
  • PACKAGE.md — live publishing + automatic attachments listed under features.
  • AzureDevOpsLivePublishingTests.cs — updated existing fake client + 9 new tests.

Out of scope (still open in #5951)

  • Direction 4 (publish --report-trx TRX as an artifact for completeness when live publishing is on).
  • Direction 5 (telemetry / metrics emission).
  • A user-facing opt-out flag for per-test attachments (deferred to first-feedback).

Part of #5951 (direction 3b — auto-attach per-test artifacts and code
coverage to live-published AzDO test runs).
Publisher (AzureDevOpsTestResultsPublisher):
- Consumes SessionFileArtifact so each process uploads its own coverage
through the message bus (no owner-only directory scan, no duplicates).
- For failed/error/timeout/cancelled outcomes, collects FileArtifactProperty
files plus StandardOutputProperty/StandardErrorProperty inline content as
result-level attachments. stdout/stderr capped at 256 KB UTF-8 inline with
a "...[truncated]" marker. Passing tests get no attachments.
- After a successful PublishTestResultsAsync call, captures returned result
IDs and uploads each attachment to /Runs/{runId}/Results/{id}/attachments.
Upload failures are logged as warnings and never push the result back into
the retry queue.
- After the final flush, drains queued *.coverage / *.cobertura.xml /
*.opencover.xml SessionFileArtifacts as CodeCoverage run-level attachments.
Client (AzureDevOpsTestResultsClient):
- PublishTestResultsAsync now returns Task<IReadOnlyList<int>?>. Null means
"AzDO accepted the batch but the response couldn't be parsed or didn't
match by position+automatedTestName" — callers must NOT retry. Only HTTP
transport failures throw (preserving existing retry behavior).
- New UploadTestResultAttachmentAsync / UploadTestRunAttachmentAsync POST
base64-encoded payloads to /attachments endpoints (api 7.1). Files
>16 MB are skipped client-side via TryBuildAttachmentRequest. Attachment
uploads use a 5-minute HttpClient timeout to accommodate large payloads.
Models:
- New AzureDevOpsLivePublishingConstants.MaxAttachmentSizeBytes (16 MB) and
MaxInlineAttachmentBytes (256 KB).
- New AzureDevOpsAttachmentTypes (GeneralAttachment / ConsoleLog /
CodeCoverage), AzureDevOpsTestCaseResultWithAttachments wrapper, and
AzureDevOpsTestResultAttachment with FromFile / FromString factories.
Tests:
- 9 new tests cover: attachment upload for failed tests, no-attachments for
passing tests, attachment failure logs warning + no retry, publish-null
skips attachments + no retry, oversized file forwarded for client-side
filtering, stdout truncation, coverage SessionFileArtifact upload,
non-coverage files skipped, coverage upload failure logs warning.
- Existing fake client updated for the new IReadOnlyList<int>? return type
and to record attachment calls; existing tests adapted to the wrapper.
PACKAGE.md updated to describe live publishing + automatic attachments.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 3, 2026 00:34

CopilotAI 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 extends the Azure DevOps live test-results publisher in Microsoft.Testing.Extensions.AzureDevOpsReport to upload result-level attachments (file artifacts + stdout/stderr) for failing tests and run-level code coverage attachments (*.coverage, *.cobertura.xml, *.opencover.xml) during the run lifecycle. It also updates the REST client contract to return server-assigned result IDs so attachments can be posted to the correct result rows.

Changes:

  • Change live result publishing to return per-result IDs and use them to upload per-result attachments (best-effort, no re-publish on ID-parse failure).
  • Add run-level attachment upload for coverage session artifacts.
  • Update unit tests, PACKAGE.md feature list, and localized resources for new warnings.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsLivePublishingTests.csUpdates fake client contract (publish returns IDs) and adds unit tests for result/run attachment behaviors.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/AzureDevOpsResources.resxAdds new warning strings for attachment upload failures and ID-parse failure.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.cs.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.de.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.es.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.fr.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.it.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ja.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ko.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pl.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pt-BR.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ru.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.tr.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hans.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hant.xlfRegenerates localization entries for the new resource strings.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PACKAGE.mdDocuments live publishing + automatic attachments as package features.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/IAzureDevOpsTestResultsClient.csUpdates publish API to return IDs (or null) and adds upload methods for result/run attachments.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsPublisher.csBuilds/queues attachments, uploads per-result attachments after publish, and uploads run-level coverage attachments at session finish.
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsClient.csImplements publish response parsing for IDs and adds REST calls + payload building for attachments (size cap, timeouts).
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsLivePublishingModels.csAdds attachment constants/types and attachment DTOs used by publisher/client.

Copilot's findings

  • Files reviewed: 20/20 changed files
  • Comments generated: 6

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Review Summary — PR #8782

The overall design is sound: clear separation between result-level and run-level attachments, the null-return contract on PublishTestResultsAsync correctly prevents double-publishing on ID-parse failure, and the concurrency model (_flushSemaphore guarding _retryResults, ConcurrentQueue for the other collections) is correct. The xlf files were regenerated by the build tool (all new entries have state="new" with source-text placeholders), so that's fine per repo guidelines.

DimensionFindingSeverity
Performance & AllocationsTruncateInline: per-character char[1] heap allocation via [ch] collection expressionMAJOR
Algorithmic CorrectnessTruncateInline: truncation at a high surrogate produces an invalid UTF-16 stringMODERATE
Algorithmic CorrectnessTryBuildAttachmentRequest: TOCTOU between fileInfo.Length check and ReadAllBytesMODERATE
Test CompletenessTruncateInline boundary cases not tested (exact-limit, multi-byte chars, surrogate straddling budget)MODERATE
All other dimensions (17)✅ Clean

No blocking issues found. The three moderate findings should be addressed before merge; the performance finding in particular is straightforward to fix.

Generated by Expert Code Review (on open) for issue #8782 · sonnet46 4.2M

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8782 · sonnet46 6.1M

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

This comment has been minimized.

CopilotAI review requested due to automatic review settings June 3, 2026 15:39

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

Copilot's findings

  • Files reviewed: 20/20 changed files
  • Comments generated: 3

Comment threadsrc/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PACKAGE.md Outdated
@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

This comment has been minimized.

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build fails in AzureDevOpsLivePublishingTests.cs due to two analyzer violations in newly-added test code: one IDE0053 (lambda should use expression body) and five MSTEST0037 (use Assert.Contains instead of Assert.IsTrue + .Any()).

i️ /tmp/binlog-data/ was not populated by the pre-agent steps; analysis was performed directly from /tmp/build-output.log and the workspace source.


Root cause 1: IDE0053 — Block-body lambda where expression body is required

The new ConsumeAsync_SkipsOversizedFileAttachment test assigns UploadTestResultAttachmentAsyncFunc using a block-body lambda whose sole statement is return Task.CompletedTask;. The project enforces IDE0053 as an error, so this must be an expression-body lambda.

Affected files / errors

Proposed fix — collapse the block body (the inline comments can be removed or moved to a separate comment above):

- client.UploadTestResultAttachmentAsyncFunc = (_, _, _, attachment, _) =>- {- // The publisher still queues the oversized attachment; the client side TryBuildAttachmentRequest- // drops it. In this fake we just record the call regardless — the contract is exercised end-to-end- // when running against the real client. For the unit test we only assert what the publisher sends.- return Task.CompletedTask;- };+ // The publisher still queues the oversized attachment; the client side TryBuildAttachmentRequest+ // drops it. In this fake we just record the call regardless — the contract is exercised end-to-end+ // when running against the real client. For the unit test we only assert what the publisher sends.+ client.UploadTestResultAttachmentAsyncFunc = (_, _, _, attachment, _) => Task.CompletedTask;

Root cause 2: MSTEST0037Assert.IsTrue(...Any(...)) should be Assert.Contains

Five assertions in two new tests use Assert.IsTrue(collection.Any(predicate)) instead of the Assert.Contains(collection, predicate) overload that produces clearer failure messages and satisfies the MSTEST0037 analyzer rule.

Affected files / errors

LineCollectionPredicate
537UploadTestResultAttachmentCallsFileName == "small.txt"
538UploadTestResultAttachmentCallsFileName == "big.bin"
621UploadTestRunAttachmentCallsFileName == "results.cobertura.xml"
622UploadTestRunAttachmentCallsFileName == "results.opencover.xml"
623UploadTestRunAttachmentCallsFileName == "results.coverage"

Proposed fix (same pattern for all five):

- Assert.IsTrue(client.UploadTestResultAttachmentCalls.Any(c => c.Attachment.FileName == "small.txt"));- Assert.IsTrue(client.UploadTestResultAttachmentCalls.Any(c => c.Attachment.FileName == "big.bin"));+ Assert.Contains(client.UploadTestResultAttachmentCalls, c => c.Attachment.FileName == "small.txt");+ Assert.Contains(client.UploadTestResultAttachmentCalls, c => c.Attachment.FileName == "big.bin");

All MSBuild errors (6 unique, each repeated for net8.0 and net9.0)
CodeFile:LineMessage
IDE0053AzureDevOpsLivePublishingTests.cs:518Use expression body for lambda expression
MSTEST0037AzureDevOpsLivePublishingTests.cs:537Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:538Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:621Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:622Use Assert.Contains instead of Assert.IsTrue
MSTEST0037AzureDevOpsLivePublishingTests.cs:623Use Assert.Contains instead of Assert.IsTrue

🤖 Generated by the Build Failure Analysis workflow using build output log · commit 1f62231

Generated by Build Failure Analysis for issue #8782 · sonnet46 8.3M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8782 · sonnet46 8.3M

- AzureDevOpsLivePublishingTests.cs: convert block-body lambda to expression body and replace Assert.IsTrue(...Any(...)) with Assert.Contains(predicate, collection) (IDE0053 + MSTEST0037 build errors)
- PACKAGE.md: fix CLI option name (--publish-azdo-test-results, not --report-azdo-publish-results)
- AzureDevOpsTestResultsPublisher.cs: read FileInfo.FullName inside the IO/security/PathTooLong best-effort try/catch in TryCreateRunAttachment
- AzureDevOpsTestResultsPublisher.cs: fix misleading OperationCanceledException comment in UploadPendingRunAttachmentsAsync — code rethrows, no re-queue
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) merged commit e5487e9 into mainJun 3, 2026
52 of 58 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/azdo-result-attachments branch June 3, 2026 22:48
Sign up for freeto 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

@Evangelink