The port: ParsedResult model, LegacyResultReader, and the renderer migration (#391, Tasks 3–5b) - #443
Conversation
Verified behaviour-preserving: raw renders of all three fixtures are byte-identical before and after, against one fixture generation.
Activity types and per-activity durations leave the report; activity element ids become path-derived; the activity/failure interleave re-keys from finish to start. Diff against the 5a baseline enumerated and reviewed: every line is one of the four accepted shapes (50 class / 64 time / 204 id / 74 reposition).
📝 WalkthroughWalkthroughThe report now converts legacy ChangesParsed-result migration
Estimated code review effort: 4 (Complex) | ~60 minutes Mergeability Score: 🟡 Moderate · up to The renderer migration can misclassify JSON, text-like, or extensionless attachments as unknown links, omit fault reporting when exported payloads cannot be read, and produce ambiguous iteration labels or unstable ordering in edge cases. These are bounded but concrete output and diagnostic correctness risks, so merge should wait for fixes or explicit owner acceptance. Sequence Diagram(s)sequenceDiagram
participant Summary
participant LegacyResultReader
participant ParsedResult
participant Run
participant TestSummary
Summary->>LegacyResultReader: read xcresult bundle
LegacyResultReader->>ParsedResult: create parsed runs and test hierarchy
LegacyResultReader-->>Summary: return ParsedResult
Summary->>Run: initialize from ParsedRun
Run->>TestSummary: build summaries from testables
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Sources/XCTestHTMLReportCore/Classes/ResultReading/Legacy/ResultFile.swift (1)
128-133: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winRecord a fault when an exported payload cannot be read.
exportPayload(reference:fileName:)records.payloadExportFailedon both the export failure and the move failure.exportPayloadData(reference:)records the fault only on the export failure. IfData(contentsOf:)throws, the method logs a warning and returnsnilwithout recording anything. In.inlinerendering mode this produces content.nonefor a payload that exists, which is real degradation.Add the fault record so
--lenientand the exit-3 path see the same degradation in both rendering modes.Based on learnings: "an exported attachment that cannot be read is a genuine
.payloadExportFailedfault... The method must log the read error and record the fault so--lenientcan handle the degraded report."🐛 Proposed fix
do { return try Data(contentsOf: savedURL) } catch { - Logger.warning("Can't get content of \(savedURL)") + Logger.warning("Can't get content of \(savedURL). \(error.localizedDescription)") + faultCollector.record(.payloadExportFailed, "payload id \(reference)") return nil }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/XCTestHTMLReportCore/Classes/ResultReading/Legacy/ResultFile.swift` around lines 128 - 133, Update exportPayloadData(reference:) to record a .payloadExportFailed fault when Data(contentsOf:) throws, while preserving the existing warning log and nil return. Use the same fault-recording mechanism and reference context already used by exportPayload(reference:fileName:) for export or move failures.Source: Learnings
🧹 Nitpick comments (2)
Tests/XCTestHTMLReportTests/LegacyResultReaderTests.swift (1)
30-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a regression assertion for failure/activity ordering.
This test verifies repetition merging but not the required
start-based interleave. A regression to append failure rows, or to restorefinishordering, can still pass these assertions. In the failed iteration, assert that the failure activity occurs afterRetryable Activity.[recommend_recommended_refactor]
Based on learnings:
RetryResults/testRetryOnFailure()exercises failure ordering because its assertion occurs duringRetryable Activity.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Tests/XCTestHTMLReportTests/LegacyResultReaderTests.swift` around lines 30 - 47, Extend testRepetitionsMergeIntoOneTestCase to inspect the failed iteration’s activities and assert that the failure activity appears after “Retryable Activity” in start-based order. Use the existing RetryResults fixture and testRetryOnFailure() case, preserving the current repetition and status assertions.Source: Learnings
Sources/XCTestHTMLReportCore/Classes/ResultReading/Legacy/ResultFile.swift (1)
170-192: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueOptional cleanup only: simplify two non-functional code-quality issues.
formatEmittedOutput()returns a non-optionalString, somapis clearer thancompactMapin both extensions inResultFile.swift.- The
Iterationinitializer carries repeated rendering-context parameters; consider bundling them into aRenderingContextvalue type as a follow-up refactor.Neither suggestion changes behavior or blocks this migration.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/XCTestHTMLReportCore/Classes/ResultReading/Legacy/ResultFile.swift` around lines 170 - 192, Replace compactMap with map in the subsections transformations within ActivityLogUnitTestSection.formatEmittedOutput() and ActivityLogSection.formatEmittedOutput(), preserving the existing string formatting and joining behavior. Apply the same fix in `@Sources/XCTestHTMLReportCore/Classes/Models/Iteration.swift` around lines 29 - 38: The repeated rendering-context parameter suggestion is preserved here.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/XCTestHTMLReportCore/Classes/Models/Attachment.swift`:
- Around line 26-45: Update Attachment.init(filenameExtension:) to map text-like
extensions such as json, csv, xml, md, and plist to .text, while preserving
UTI-derived types for unsupported extensions, including public.data when the
filename is not .dat. Add regression tests covering these extension mappings and
UTI fallback behavior.
In `@Sources/XCTestHTMLReportCore/Classes/Models/Iteration.swift`:
- Line 69: Update the iteration initializer call in Test.swift to pass the
existing positional index, then change the Iteration title fallback from
iterationNumber ?? 0 to index + 1 when no backend repetition number is
available. Preserve the 1-based iterationNumber value when present and ensure
unnumbered iterations receive distinct titles.
In
`@Sources/XCTestHTMLReportCore/Classes/ResultReading/Legacy/LegacyResultReader.swift`:
- Around line 211-229: Add a "public.data" case to the UTI switch in
filenameExtension(forUTI:filename:) and map it to the appropriate data-file
extension so attachments without filename extensions are no longer resolved as
unknown.
- Around line 133-135: The combined activities/failures sort in the legacy
result reader needs a deterministic positional tiebreak for equal optional
timestamps. Update the `combined` sort closure to compare the existing
source-position field after timestamp equality, reusing the positional tiebreak
pattern already present elsewhere in `LegacyResultReader`.
In `@Tests/XCTestHTMLReportTests/FaultReportingTests.swift`:
- Around line 81-95: In the test around LegacyResultReader and exportLogs,
assert that file.exportLogsData(reference: logReference) returns non-nil
immediately after obtaining logReference and before changing bundle permissions.
Keep the existing permission change and .logExportFailed assertion so the test
verifies a successful read followed specifically by a write failure.
---
Outside diff comments:
In `@Sources/XCTestHTMLReportCore/Classes/ResultReading/Legacy/ResultFile.swift`:
- Around line 128-133: Update exportPayloadData(reference:) to record a
.payloadExportFailed fault when Data(contentsOf:) throws, while preserving the
existing warning log and nil return. Use the same fault-recording mechanism and
reference context already used by exportPayload(reference:fileName:) for export
or move failures.
---
Nitpick comments:
In `@Sources/XCTestHTMLReportCore/Classes/ResultReading/Legacy/ResultFile.swift`:
- Around line 170-192: Replace compactMap with map in the subsections
transformations within ActivityLogUnitTestSection.formatEmittedOutput() and
ActivityLogSection.formatEmittedOutput(), preserving the existing string
formatting and joining behavior.
Apply the same fix in
`@Sources/XCTestHTMLReportCore/Classes/Models/Iteration.swift` around lines 29 -
38: The repeated rendering-context parameter suggestion is preserved here.
In `@Tests/XCTestHTMLReportTests/LegacyResultReaderTests.swift`:
- Around line 30-47: Extend testRepetitionsMergeIntoOneTestCase to inspect the
failed iteration’s activities and assert that the failure activity appears after
“Retryable Activity” in start-based order. Use the existing RetryResults fixture
and testRetryOnFailure() case, preserving the current repetition and status
assertions.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 359271ec-4fe0-41ab-957f-efbbfab04c84
📒 Files selected for processing (20)
Sources/XCTestHTMLReportCore/Classes/Models/Activity.swiftSources/XCTestHTMLReportCore/Classes/Models/Attachment.swiftSources/XCTestHTMLReportCore/Classes/Models/Iteration.swiftSources/XCTestHTMLReportCore/Classes/Models/JUnitReport.swiftSources/XCTestHTMLReportCore/Classes/Models/Run.swiftSources/XCTestHTMLReportCore/Classes/Models/RunDestination.swiftSources/XCTestHTMLReportCore/Classes/Models/Summary.swiftSources/XCTestHTMLReportCore/Classes/Models/TargetDevice.swiftSources/XCTestHTMLReportCore/Classes/Models/Test.swiftSources/XCTestHTMLReportCore/Classes/Models/TestSummary.swiftSources/XCTestHTMLReportCore/Classes/Protocols/EmittableOutput.swiftSources/XCTestHTMLReportCore/Classes/Protocols/TestConforming.swiftSources/XCTestHTMLReportCore/Classes/ResultReading/Legacy/LegacyResultReader.swiftSources/XCTestHTMLReportCore/Classes/ResultReading/Legacy/ResultFile.swiftSources/XCTestHTMLReportCore/Classes/ResultReading/ParsedResult.swiftSources/XCTestHTMLReportCore/Classes/ResultReading/ResultReader.swiftTests/XCTestHTMLReportTests/FaultReportingTests.swiftTests/XCTestHTMLReportTests/LegacyResultReaderTests.swiftdocs/superpowers/plans/2026-08-10-xcresulttool-legacy-migration.mddocs/superpowers/specs/2026-08-10-xcresulttool-legacy-migration-design.md
💤 Files with no reviewable changes (1)
- Sources/XCTestHTMLReportCore/Classes/Protocols/EmittableOutput.swift
| /// The port carries a filename extension rather than a UTI (see "Deciding | ||
| /// the model before the port", answer 4), so both backends type | ||
| /// attachments from the same fact. `UTType(filenameExtension:)` would be | ||
| /// the direct route but is macOS 11+ against a 10.15 floor, so the mapping | ||
| /// is an explicit table like `mimeType`'s fallback below. | ||
| init?(filenameExtension: String) { | ||
| switch filenameExtension.lowercased() { | ||
| case "png": self = .png | ||
| case "jpg", "jpeg": self = .jpeg | ||
| case "heic": self = .heic | ||
| case "gif": self = .gif | ||
| case "mp4": self = .mp4 | ||
| case "txt": self = .text | ||
| case "log": self = .log | ||
| case "html", "htm": self = .html | ||
| case "zip": self = .zip | ||
| case "dat": self = .data | ||
| default: return nil | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: List attachment filenames and UTIs used in fixtures and sample app, to size the typing gap.
set -euo pipefail
# Sample app attachments: which UTIs and filenames are produced.
rg -n -C3 'XCTAttachment\(' XCTestHTMLReportSampleApp || true
# Reader UTI table and its callers.
rg -n -C5 'filenameExtension\(forUTI' Sources Tests || true
# Any other place that still types attachments from a UTI raw value.
rg -n -C3 'AttachmentType\(' Sources Tests || trueRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 7385
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Attachment.swift ---'
nl -ba Sources/XCTestHTMLReportCore/Classes/Models/Attachment.swift | sed -n '1,180p'
printf '%s\n' '--- LegacyResultReader.swift ---'
nl -ba Sources/XCTestHTMLReportCore/Classes/ResultReading/Legacy/LegacyResultReader.swift | sed -n '180,280p'
printf '%s\n' '--- attachment type consumers ---'
rg -n -C4 'AttachmentType|\.mimeType|HTMLTemplates\.link|attachment\.type|type:' Sources Tests
printf '%s\n' '--- UTI and filename fixtures ---'
rg -n -C3 'uniformTypeIdentifier|public\.|com\.apple\.|filename:|fileName:|payload\.json|\.csv|\.xml|\.md|\.plist|\.bin|\.dat' Tests Sources XCTestHTMLReportSampleAppRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 236
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Attachment.swift ---'
cat -n Sources/XCTestHTMLReportCore/Classes/Models/Attachment.swift | sed -n '1,180p'
printf '%s\n' '--- LegacyResultReader.swift ---'
cat -n Sources/XCTestHTMLReportCore/Classes/ResultReading/Legacy/LegacyResultReader.swift | sed -n '180,280p'
printf '%s\n' '--- attachment type consumers ---'
rg -n -C4 'AttachmentType|\.mimeType|HTMLTemplates\.link|attachment\.type|type:' Sources Tests
printf '%s\n' '--- UTI and filename fixtures ---'
rg -n -C3 'uniformTypeIdentifier|public\.|com\.apple\.|filename:|fileName:|payload\.json|\.csv|\.xml|\.md|\.plist|\.bin|\.dat' Tests Sources XCTestHTMLReportSampleAppRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 50389
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file='Sources/XCTestHTMLReportCore/Classes/Models/Attachment.swift'
reader='Sources/XCTestHTMLReportCore/Classes/ResultReading/Legacy/LegacyResultReader.swift'
printf '%s\n' '--- current commit ---'
git rev-parse HEAD
printf '%s\n' '--- change to Attachment.swift ---'
git diff HEAD^ -- "$file"
printf '%s\n' '--- previous Attachment.swift typing and rendering ---'
git show HEAD^:"$file" | grep -n -A35 -B8 'init(' | head -n 100
printf '%s\n' '--- previous LegacyResultReader attachment parsing ---'
git show HEAD^:"$reader" | grep -n -A55 -B12 'parseAttachment\|filenameExtension' | head -n 180
printf '%s\n' '--- focused attachment-related tests ---'
rg -n -C8 'AttachmentType|filenameExtension|plain-text|public\.data|json|csv|xml|plist|unknown|HTMLTemplates\.link' Tests/XCTestHTMLReportTests Sources/XCTestHTMLReportCore/ClassesRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 50390
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file='Sources/XCTestHTMLReportCore/Classes/Models/Attachment.swift'
reader='Sources/XCTestHTMLReportCore/Classes/ResultReading/Legacy/LegacyResultReader.swift'
printf '%s\n' '--- focused diff ---'
git diff --unified=20 HEAD^ HEAD -- "$file"
printf '%s\n' '--- previous Attachment.swift ---'
git show HEAD^:"$file" | sed -n '1,120p'
printf '%s\n' '--- previous LegacyResultReader.swift attachment section ---'
git show HEAD^:"$reader" | sed -n '175,250p'
printf '%s\n' '--- focused tests and model references, excluding embedded templates ---'
rg -n -C5 --glob '!**/HTMLTemplates.swift' \
'AttachmentType|filenameExtension|plain-text|public\.data|json|csv|xml|plist|unknown|HTMLTemplates\.link' \
Tests Sources/XCTestHTMLReportCore/Classes/Models Sources/XCTestHTMLReportCore/Classes/ResultReadingRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 50389
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file='Sources/XCTestHTMLReportCore/Classes/Models/Attachment.swift'
reader='Sources/XCTestHTMLReportCore/Classes/ResultReading/Legacy/LegacyResultReader.swift'
printf '%s\n' '--- repository state ---'
git status --short
git log --oneline -5 -- "$file" "$reader"
printf '%s\n' '--- historical UTI-based typing references ---'
git log --all --oneline -S 'AttachmentType(rawValue' -- "$file" "$reader"
git log --all --oneline -S 'uniformTypeIdentifier' -- "$file" "$reader"
printf '%s\n' '--- all current and reachable AttachmentType initializers ---'
git grep -n 'AttachmentType(' $(git for-each-ref --format='%(refname)' refs/heads refs/remotes 2>/dev/null) -- "$file" "$reader" 2>/dev/null || true
git grep -n 'AttachmentType(rawValue\|uniformTypeIdentifier' HEAD^ -- "$file" "$reader" 2>/dev/null || true
printf '%s\n' '--- compact focused model output ---'
cat -n "$file" | sed -n '12,46p;145,166p;260,276p'Repository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 5478
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
import subprocess
current = Path("Sources/XCTestHTMLReportCore/Classes/Models/Attachment.swift").read_text()
reader = Path(
"Sources/XCTestHTMLReportCore/Classes/ResultReading/Legacy/LegacyResultReader.swift"
).read_text()
main = subprocess.check_output(
["git", "show", "refs/heads/main:Sources/XCTestHTMLReportCore/Classes/Models/Attachment.swift"],
text=True,
)
# Extract the current filename-extension switch and evaluate representative inputs.
switch = re.search(
r"switch filenameExtension\.lowercased\(\) \{(?P<body>.*?)\n\s*default: return nil",
current,
re.S,
)
assert switch, "current filename-extension switch not found"
mapping = {}
for extensions, member in re.findall(
r'case\s+"([^"]+(?:"\s*,\s*"[^"]+)*)":\s*self\s*=\s*\.(\w+)',
switch.group("body"),
):
for ext in re.findall(r'"([^"]+)"', extensions):
mapping[ext] = member
print("current filename mapping:", mapping)
print("json ->", mapping.get("json", "unknown"))
print("csv ->", mapping.get("csv", "unknown"))
print("xml ->", mapping.get("xml", "unknown"))
print("md ->", mapping.get("md", "unknown"))
print("plist ->", mapping.get("plist", "unknown"))
print("dat ->", mapping.get("dat", "unknown"))
# Confirm the current reader returns a filename extension before consulting its UTI.
assert re.search(
r"if let filename = filename.*?return ext\.lowercased\(\).*?switch uti",
reader,
re.S,
), "reader no longer prefers filename extension"
assert mapping.get("json") is None
assert mapping.get("dat") == "data"
# Confirm the baseline used the UTI raw value and therefore preserved these types.
assert "AttachmentType(rawValue: attachment.uniformTypeIdentifier)" in main
assert 'case data = "public.data"' in main
assert 'case text = "public.plain-text"' in main
print("baseline UTI typing: public.plain-text -> text; public.data -> data")
print("current behavior: unknown filename extensions -> unknown/link template")
PY
printf '%s\n' '--- SwiftLint complexity configuration ---'
rg -n -C3 'cyclomatic_complexity|included:|excluded:' .swiftlint.yml .swiftlint.yaml Package.swift 2>/dev/null || trueRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 393
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
import subprocess
current_path = Path("Sources/XCTestHTMLReportCore/Classes/Models/Attachment.swift")
reader_path = Path(
"Sources/XCTestHTMLReportCore/Classes/ResultReading/Legacy/LegacyResultReader.swift"
)
main_path = "Sources/XCTestHTMLReportCore/Classes/Models/Attachment.swift"
current = current_path.read_text()
reader = reader_path.read_text()
main = subprocess.check_output(
["git", "show", f"refs/heads/main:{main_path}"],
text=True,
)
switch = re.search(
r"switch filenameExtension\.lowercased\(\) \{(?P<body>.*?)\n\s*default: return nil",
current,
re.S,
)
assert switch, "current filename-extension switch not found"
mapping = {}
for line in switch.group("body").splitlines():
match = re.search(r'case\s+(.+?):\s*self\s*=\s*\.(\w+)', line)
if not match:
continue
for ext in re.findall(r'"([^"]+)"', match.group(1)):
mapping[ext] = match.group(2)
print("current filename mapping:", mapping)
for ext in ("json", "csv", "xml", "md", "plist", "dat"):
print(f"{ext} ->", mapping.get(ext, "unknown"))
assert "json" not in mapping
assert "csv" not in mapping
assert "xml" not in mapping
assert "md" not in mapping
assert "plist" not in mapping
assert mapping.get("dat") == "data"
# The filename branch returns before the UTI switch.
assert re.search(
r"if let filename = filename.*?return ext\.lowercased\(\).*?switch uti",
reader,
re.S,
), "reader does not prefer filename extension"
# The baseline constructed AttachmentType from the UTI raw value.
assert "AttachmentType(rawValue: attachment.uniformTypeIdentifier)" in main
assert 'case data = "public.data"' in main
assert 'case text = "public.plain-text"' in main
print("baseline UTI typing: public.plain-text -> text; public.data -> data")
print("current behavior: unsupported filename extension -> unknown/link template")
PY
printf '%s\n' '--- SwiftLint complexity configuration ---'
rg -n -C3 'cyclomatic_complexity|included:|excluded:' .swiftlint.yml .swiftlint.yaml Package.swift 2>/dev/null || trueRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 1408
Preserve UTI-derived attachment types for unsupported filename extensions. payload.json with UTI public.plain-text now becomes .unknown and renders through HTMLTemplates.link; the previous implementation rendered it as .text. Add mappings for text-like extensions such as json, csv, xml, md, and plist, or retain a UTI-derived fallback. Preserve public.data when the filename is not .dat. Add regression tests.
🧰 Tools
🪛 GitHub Check: swift
[warning] 31-31:
Function should have complexity 10 or less; currently complexity is 11 (cyclomatic_complexity)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Sources/XCTestHTMLReportCore/Classes/Models/Attachment.swift` around lines 26
- 45, Update Attachment.init(filenameExtension:) to map text-like extensions
such as json, csv, xml, md, and plist to .text, while preserving UTI-derived
types for unsupported extensions, including public.data when the filename is not
.dat. Add regression tests covering these extension mappings and UTI fallback
behavior.
Source: Linters/SAST tools
| [ | ||
| "UUID": uuid, | ||
| "TITLE": "Iteration \(repetitionPolicy?.iteration ?? 0)", | ||
| "TITLE": "Iteration \(iterationNumber ?? 0)", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
iterationNumber ?? 0 renders every unnumbered iteration as "Iteration 0".
Line 23 documents iterationNumber as 1-based and nil when the backend reports none. The iteration template is used only when a test case has more than one iteration. If the backend reports no repetition numbers, all rows in that group render the identical title "Iteration 0", so a reader cannot tell the repetitions apart.
Pass the positional index into the initializer and use index + 1 as the fallback. Test.swift already has the index at the call site (identifierPath.appending("iteration\(index)")), so the fix belongs there rather than in a wider default.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Sources/XCTestHTMLReportCore/Classes/Models/Iteration.swift` at line 69,
Update the iteration initializer call in Test.swift to pass the existing
positional index, then change the Iteration title fallback from iterationNumber
?? 0 to index + 1 when no backend repetition number is available. Preserve the
1-based iterationNumber value when present and ensure unnumbered iterations
receive distinct titles.
| combined = (activities + failures).sorted { | ||
| ($0.start ?? .distantPast) < ($1.start ?? .distantPast) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Inspect XCResultKit's ActionTestFailureSummary and ActionTestActivitySummary time fields.
set -euo pipefail
fd -i -t f 'ActionTestFailureSummary*' . | while IFS= read -r f; do
rg -n -C 3 'timestamp|lineNumber|issueType' "$f"
done
fd -i -t f 'ActionTestActivitySummary*' . | while IFS= read -r f; do
rg -n -C 3 'start|finish' "$f"
doneRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 171
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- dependency declarations ---'
rg -n -C 3 'XCResultKit|1\.2\.1' Package.swift Package.resolved .github 2>/dev/null || true
printf '%s\n' '--- relevant reader code ---'
fd -i -t f 'LegacyResultReader.swift|Run.swift|Test.swift' . -x sh -c '
echo "--- $1"
rg -n -C 5 "combined|enumerated|sorted|distantPast|timestamp|start" "$1"
' sh {}
printf '%s\n' '--- local XCResultKit sources or lockfiles ---'
fd -i -t f 'ActionTestFailureSummary*|ActionTestActivitySummary*|Package.resolved|Cartfile.resolved' .Repository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 9835
🌐 Web query:
XCResultKit 1.2.1 ActionTestFailureSummary timestamp optional ActionTestActivitySummary start Swift source
💡 Result:
In XCResultKit, the timestamp property in ActionTestFailureSummary is defined as an optional Date type (Date?) [1][2]. Regarding ActionTestActivitySummary, this object represents a hierarchical activity within a test, and its structure includes a start property, which is also a Date? type. This property represents the start time of the activity [2]. These properties are mapped from the underlying xcresult bundle schema to friendly Swift objects to facilitate access to test result data [3][4].
Citations:
- 1: https://context7.com/davidahouse/xcresultkit/llms.txt
- 2: https://github.com/davidahouse/XCResultKit/blob/main/XCResultFormat.md
- 3: https://github.com/davidahouse/xcresultkit
- 4: https://swiftpackageregistry.com/davidahouse/XCResultKit
Add a positional tiebreak to the failure-interleaving sort.
ActionTestFailureSummary.timestamp and ActionTestActivitySummary.start are optional. Equal timestamps, including multiple .distantPast fallbacks, can produce different orders because Swift’s sorted(by:) is not stable. Add the source-position tiebreak used elsewhere in this file.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@Sources/XCTestHTMLReportCore/Classes/ResultReading/Legacy/LegacyResultReader.swift`
around lines 133 - 135, The combined activities/failures sort in the legacy
result reader needs a deterministic positional tiebreak for equal optional
timestamps. Update the `combined` sort closure to compare the existing
source-position field after timestamp equality, reusing the positional tiebreak
pattern already present elsewhere in `LegacyResultReader`.
| static func filenameExtension(forUTI uti: String?, filename: String?) -> String? { | ||
| if let filename = filename, | ||
| case let ext = (filename as NSString).pathExtension, !ext.isEmpty | ||
| { | ||
| return ext.lowercased() | ||
| } | ||
| switch uti { | ||
| case "public.png": return "png" | ||
| case "public.jpeg": return "jpeg" | ||
| case "public.heic": return "heic" | ||
| case "com.compuserve.gif": return "gif" | ||
| case "public.mpeg-4": return "mp4" | ||
| case "public.plain-text": return "txt" | ||
| case "com.apple.log": return "log" | ||
| case "public.html": return "html" | ||
| case "public.zip-archive": return "zip" | ||
| default: return nil | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Compare the reader's UTI table with AttachmentType's known types.
set -euo pipefail
fd -t f 'Attachment.swift' Sources | while IFS= read -r f; do
rg -n -C 2 'filenameExtension|mimeType|case ' "$f"
done
fd -t f 'LegacyResultReader.swift' Sources | while IFS= read -r f; do
rg -n 'public\.|com\.' "$f"
doneRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 5459
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- AttachmentType and Attachment initialization ---'
fd -t f 'Attachment.swift' Sources | while IFS= read -r f; do
cat -n "$f" | sed -n '1,180p'
done
echo '--- Legacy UTI mapping and attachment construction ---'
fd -t f 'LegacyResultReader.swift' Sources | while IFS= read -r f; do
cat -n "$f" | sed -n '180,240p'
rg -n -C 8 'filenameExtension\(forUTI|Attachment\(' "$f"
done
echo '--- Current change summary ---'
git diff --stat
git status --shortRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 11460
Add public.data to the UTI mapping.
AttachmentType recognizes public.data, but filenameExtension(forUTI:filename:) does not. Attachments without a filename extension and with this UTI resolve to .unknown.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@Sources/XCTestHTMLReportCore/Classes/ResultReading/Legacy/LegacyResultReader.swift`
around lines 211 - 229, Add a "public.data" case to the UTI switch in
filenameExtension(forUTI:filename:) and map it to the appropriate data-file
extension so attachments without filename extensions are no longer resolved as
unknown.
| let parsed = try XCTUnwrap(LegacyResultReader(file: file).read()) | ||
| let logReference = try XCTUnwrap( | ||
| parsed.runs.first?.logReference, | ||
| "Fixture is expected to carry a log reference" | ||
| ) | ||
|
|
||
| // Reading still works — only the write destination is unwritable. | ||
| try fileManager.setAttributes( | ||
| [.posixPermissions: 0o555], ofItemAtPath: bundle.path | ||
| ) | ||
|
|
||
| XCTAssertNil(file.exportLogs(reference: logReference)) | ||
| XCTAssertTrue( | ||
| collector.faults.contains { $0.kind == .logExportFailed }, | ||
| "A log that reads but cannot be written must be recorded as degradation" |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Prove that the log read succeeds before testing the write failure.
A non-nil logReference does not prove that file.getLogs(id:) succeeds. Both exportLogs failure branches return nil and record .logExportFailed. The test can therefore pass without executing the write-error path.
Assert that file.exportLogsData(reference:) is non-nil before line 88 changes permissions.
Proposed test change
let logReference = try XCTUnwrap(
parsed.runs.first?.logReference,
"Fixture is expected to carry a log reference"
)
+ XCTAssertNotNil(
+ file.exportLogsData(reference: logReference),
+ "Fixture log must be readable before testing the output write failure"
+ )
// Reading still works — only the write destination is unwritable.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let parsed = try XCTUnwrap(LegacyResultReader(file: file).read()) | |
| let logReference = try XCTUnwrap( | |
| parsed.runs.first?.logReference, | |
| "Fixture is expected to carry a log reference" | |
| ) | |
| // Reading still works — only the write destination is unwritable. | |
| try fileManager.setAttributes( | |
| [.posixPermissions: 0o555], ofItemAtPath: bundle.path | |
| ) | |
| XCTAssertNil(file.exportLogs(reference: logReference)) | |
| XCTAssertTrue( | |
| collector.faults.contains { $0.kind == .logExportFailed }, | |
| "A log that reads but cannot be written must be recorded as degradation" | |
| let parsed = try XCTUnwrap(LegacyResultReader(file: file).read()) | |
| let logReference = try XCTUnwrap( | |
| parsed.runs.first?.logReference, | |
| "Fixture is expected to carry a log reference" | |
| ) | |
| XCTAssertNotNil( | |
| file.exportLogsData(reference: logReference), | |
| "Fixture log must be readable before testing the output write failure" | |
| ) | |
| // Reading still works — only the write destination is unwritable. | |
| try fileManager.setAttributes( | |
| [.posixPermissions: 0o555], ofItemAtPath: bundle.path | |
| ) | |
| XCTAssertNil(file.exportLogs(reference: logReference)) | |
| XCTAssertTrue( | |
| collector.faults.contains { $0.kind == .logExportFailed }, | |
| "A log that reads but cannot be written must be recorded as degradation" |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Tests/XCTestHTMLReportTests/FaultReportingTests.swift` around lines 81 - 95,
In the test around LegacyResultReader and exportLogs, assert that
file.exportLogsData(reference: logReference) returns non-nil immediately after
obtaining logReference and before changing bundle permissions. Keep the existing
permission change and .logExportFailed assertion so the test verifies a
successful read followed specifically by a write failure.
…t typing (#391, Tasks 8–10) (#447) * test: pin extension-based attachment typing and map public.data on the legacy backend Task 10 of the migration plan. The AttachmentType(filenameExtension:) initializer itself landed with #443; what was missing is the cross-backend agreement pin and one table entry: LegacyResultReader's UTI fallback omitted public.data, so an extensionless public.data attachment degraded .data -> .unknown on the legacy backend only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: surface XCResultToolError diagnoses through LocalizedError Carried forward from the #441 review: consumers now exist (faults and warnings format these errors), and localizedDescription printed the generic NSError boilerplate rather than the command, exit status, and stderr. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: export attachments through the modern xcresulttool path Task 9 of the migration plan. One `export attachments` call per bundle plus the manifest.json join (activity attachment uuid <-> basename of exportedFileName) replaces the legacy per-payload export and its lock table; only the one-shot export is guarded. Run logs come from `get log --type action` and render from the structured messages, since the new format has no emittedOutput. Committed ahead of Task 8 so each commit builds: the reader's payloadStore property references this type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add ModernResultReader for the new xcresulttool format Task 8 of the migration plan. Parity rules carried exactly: statuses map into the neutral enum (Expected Failure still flattens to unknown downstream); multi-repetition status derives from the Repetition children and ignores the parent Test Case's own result (RetryResults reports Passed there for a Failed-then-Passed test); the tree renders flat, without the legacy wrapper groups. Failure text joins the two documents rather than choosing one: measured on Xcode 26.2, every assertion failure appears both as a failure-flagged activity row (positioned and timestamped, but stripped of file:line) and as a Failure Message node (file:line kept, no timestamp). Each message retitles the first unclaimed failure activity whose title is its exact suffix, in document order, nested rows included; unmatched messages (skip reasons, expected-failure notes) append. The plan's original guard kept the activity title instead, which shipped strictly less information than the spec's failureTitlePrefix entry documents — coordinator-approved correction, amended in the plan. A failed activities query records the new .missingActivities fault; fields the format structurally lacks never fault, pinned by a full modern-path render of all three fixtures asserting zero faults. The sample app gains a parameterized @test so Arguments nodes stop being fixture-unexercised; its argument sets merge into one rendered row, moving the TestResults header total from 17 to 18. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: amend plan and spec for Tasks 8-10 as executed The load-bearing correction: failure text is a join of the two documents, not a choice between them. Measured on Xcode 26.2, the activities document carries every assertion failure as a positioned, timestamped, sometimes nested failure row missing only the file:line prefix; the plan's original anti-double-count guard would have dropped the prefixed Failure Message and shipped less than the spec's failureTitlePrefix entry documents. Coordinator-approved retitle-join recorded in both documents. Mechanical notes: Task 9 commits before Task 8 (type dependency), TestRun is nested in TestActivities, the AttachmentType initializer shipped failable in 443, and regenerating fixtures after the sample-app change collapses the plan's 6-of-7 intermediate state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…elease notes (#391, Tasks 14–15) (#451) * feat!: emit our own schema from --json instead of the legacy object graph BREAKING: --json previously dumped xcresulttool's legacy object graph verbatim. That graph is Apple's internal shape and disappears with the legacy commands, so --json now emits a documented schema, identical on both backends. docs/json-schema.md is the contract, written before the encoder: field names and nesting with a complete worked example, enum spellings, one uniform null rule, duration and timestamp formats, ordering guarantees, and a semver schemaVersion policy. The encoder (JsonReport.swift) is an explicit layer rather than a synthesized Encodable on the internal model, so renaming a Parsed* property breaks compilation instead of silently renaming public output. ResultFile.exportJson() — the last exportRecursiveJson() call, kept since Task 5a — is deleted; XCResultKit is confined to ResultReading/Legacy/. JsonReportTests holds the output to the contract across both backends: recursive schema identity, values deeply equal outside the two permitted difference classes (JsonClassMask masks exactly the declared losses, with non-vacuity stats), and arguments compared per class 2 — legacy asserted == [] explicitly, modern asserted non-empty for the parameterized fixture. The value differential surfaced one reader-parity gap the HTML differential cannot see: under a retry-enabled plan, legacy stamps every summary "iteration 1" while modern reports repetition info only for real repetitions, and nothing renders a lone iteration number. The legacy reader now strips it (strippingLoneIterationNumber) — a single execution carries no repetition information on either backend. Recorded as a Task 14 execution rule in the spec. Refs #391 (Task 14). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: fold in the carried review findings from #447 and #450 - Pin the no-startTime keep-guard (#450 review): a no-start activity row carrying an attachment, a failure flag, or surviving children is kept by the symbol-annotation drop; only the contentless row is an annotation. Crafted-document test, since no fixture produces the shape. - Suffix the failure-artifact name per matrix leg in test.yml (#450 review): upload-artifact v4 refuses duplicate names, so a run where both legs fail would have lost the second upload. - ModernPayloadStore takes XCResultToolInvoking instead of the concrete client (#447 review), so export-failure faulting is provable in-suite; the new test also pins that a failed one-shot export no longer leaks its temp directory (removed in the catch — deinit never saw it). - An out-of-range repetition index no longer falls back silently to runs.first (#447 review): the iteration renders no activities and records .missingActivities, instead of borrowing repetition 1's rows. Refs #391. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: document --result-reader and the --json schema change README gains the --result-reader section, the --json break with a before/after snippet, and the known backend differences drawn from the allow-list. The 4.0 release notes are drafted at docs/release-notes/4.0.0.md in the 3.0.0 narrative style for the release to source, covering the full Task 15 checklist including the items added by rulings R1/R5/R7 and the #443/#450 reviews. The spec gains a status header (phases 1-5 landed, phase 6 deliberately deferred) and the Task 14 execution rules; the plan's Tasks 14-15 are ticked with implementation amendments recorded. Refs #391 (Task 15). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: state schema identity, not value identity, in the README The --json section claimed the file is identical across readers; the contract's own headline is schema identity, with declared value differences. Point at those and at testCase.arguments, the modern-only capability. Addresses the valid half of CodeRabbit's review on #451. Refs #391. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ixes #460) (#486) * Give every status a filter, and the log something to filter it with (fixes #460) The last of #439's redesign PRs. A1 gave the report a summary header, A2 gave the tree Xcode's outline, A3a gave each view its own surface and reserved a slot in each toolbar. This fills the slots. ## The expected-failure bucket (#460) The model has named `.expectedFailure` since #443 and A1's tally has counted it in a bucket of its own since the header landed. The filter row was the last reading of a run that still had it in no bucket at all: five functions, each naming the four row classes it shows or hides, and an expected failure carries none of them. So "Passed" left one on screen — a reader asking for the tests that passed was shown one that did not — and "All" could not put it back, because "All" only ever set `display: block` on those same four. The group pass then read the row as hidden, so a suite of nothing but expected failures vanished from the tree the moment the reader touched any filter, "All" included. **Decision: they leave `unknown` entirely, and nothing in the header moves.** That is the whole point — A1 already put them where they belong, and the coordination this needed was to make the toolbar agree rather than to move a count. `CoreTests` has carried the disagreement as a comment since A1 ("the two readings disagree until A3 rebuilds the filters"); it now asserts the partition instead. **The pills are the legend, made operable.** Both are rendered from the run's `Tally`: same buckets, same order, same drop-the-empty-ones rule. A leading "All", then one pill per outcome the run produced. That is what closes the class of defect rather than the instance — `unknown` had exactly the same gap and no issue filed against it, and it is filtered now for the same reason `expectedFailure` is: a status gets a pill *because* the run produced it. Three consequences worth stating, since each was a filter that did not finish: - **A suite goes when its last visible row does.** The pass this replaces bailed out on any group holding a sub-group, so the legacy backend's two wrapper levels — "Selected tests" and "<target>.xctest" — survived every filter. Two selected rows sat under three empty headings. Deciding deepest-first covers both cases with one rule. - **A filtered-out test takes its screenshot with it.** `.screenshot-tail` is emitted *between* rows rather than inside one (A2), so it was outside everything the filters touched. - **A childless suite is a row.** `Run.allTests` counts one as a leaf — the shape a crashed target leaves behind, which `TruncationFaultTests` is built from — so the pills count it and now filter it. ## Tests and runs Xcode's toolbar states 21 tests and 23 runs on `TestResults.xcresult`; ours knew only tests. The difference is `parameterizedAddition(value:)`, which ran once per argument set. Neither `iterations` nor `arguments` could carry that: both readers deliberately collapse argument executions into one iteration (that is what makes the two backends agree on the rows), and only the modern format names the argument values. The *count* is in both bundles — legacy has one sibling metadata entry per execution, modern one `Arguments` child — and legacy was throwing it away in `mergingArgumentExecutions`. `ParsedTestCase` gains `executionCount`, so this converges the readers rather than reading anything new, and `DifferentialTests.testExecutionCountsAgreeAcrossBackends` holds them converged, with a non-vacuity check because equality is free if nothing ever ran twice. The toolbar reads `23 executions`, not "23 runs": this report already spends the word "run" on a destination, which the picker labels `Run 1`, `Run 2`. It is `aria-live`, because a filter changes it. Each row that accounts for a difference says so — `3 arguments`, the option-A mockup's own tag — and only the count is stated, never the values, which one backend cannot see. No per-run rows in the tree: counts and filter semantics only. ## A name filter, in both views Xcode puts a Filter field at the trailing end of both toolbars. So does this, in the slot A3a reserved. On Tests it is a name-substring filter over the tree that *composes* with the pills — "Failed" and "one" together mean the failing tests whose names contain "one". On Logs it filters the log's lines. **The log moves into the page to make that possible.** It was an `<iframe src>` pointing at a `file://` sibling or a `data:` URI — a foreign origin either way — and two things followed that the report shipped: nothing in the page could filter, search or scroll it, which is why A3a's toolbar carried an inert "All Messages" label where a control belongs; and it could not see the token layer, so in dark mode the Logs view was a white slab with black text, the same class of defect as the base64 status PNGs #459 replaced. `logs-1440-dark.png` in the A3a evidence is that slab; the one here is the same view themed. A single `<pre>` rather than an element per line: a filter that rebuilds one text node is a string join, where a six-figure log would otherwise be a six-figure DOM. The `.log` export is untouched — it is the artifact #480 fixed and `DifferentialLogTests` compares — and inline mode gets *smaller*, since escaped text costs less than the same bytes base64'd into a URI. ## Substitution order, closed rather than ordered `Run` fills its two templates from two ordered lists instead of one dictionary. `HTML.html` reduces over a dictionary, so it fills placeholders in hash order and fills the ones an earlier replacement *inserted* as readily as the ones the template author wrote — the hazard the A3a review found in the picker. Two per-view templates make it closable: each list holds only what its own template needs, so a test named `[[LOG_TEXT]]` is not merely filled late but unfillable, and a log line reading `[[TEST_SUMMARIES]]` likewise. Both directions pinned in `PlaceholderOrderTests`. ## Verification - `swift test` — 193 tests, 0 failures, 3 skipped, on both legs (default/auto and `XCHR_RESULT_READER=modern`). - `DifferentialTests` green on both legs; the allow-list is untouched. - `visual` — 48 Playwright tests, 0 failures: six axe states (two new — the filtered tree and the filtered log), contrast over both themes, both fixtures and now both *views*, the filter behaviour, and 375px. - Masked duration shapes untouched. - `swiftformat --lint` clean; no new SwiftLint warnings. Refs #439. * Match suite names in the tree filter too A filter over a tree that only ever reads its leaves is one a reader finds out about the hard way: typing the name of a suite they can see emptied the pane. Matching an ancestor keeps that suite and the tests inside it, which is what the same query does in Xcode's outline. The status filter stays a leaf question. A suite's own status is folded from its children, so filtering on it would put rows of every outcome under a heading claiming one. Gated with its own precondition: the test asserts that no row carries the suite's name itself, or it would be measuring substring matching rather than ancestry. Refs #439. * Put the log's monospace stack in the token layer The one literal A3b left outside it. The sheet has had zero hardcoded font stacks since #455 gave it a token layer, and the log body had no business being the exception. Refs #439. * Carry every tail screenshot with its row, not just the nearest Review finding, verified before fixing. `TestScreenshotFlow` emits a test's *last three* screenshots (`suffix(tailCount)`), so a test with several is preceded by several — and the filter walked back exactly one sibling, leaving the outer ones on screen with no row to belong to. The synthetic fixture already contains such a row (`testFails()`, whose standard activities and failure activity each carry a screenshot), so the gate was passing on a one-tail row while the two-tail one went unchecked. It now counts every tail rather than the first, and asserts the fixture actually holds a run of more than one — mutation-tested against the one-sibling version, which it fails. Refs #439. * Keep the executions count level with the rows on screen Review finding, reproduced before fixing. Filter the real two-bundle report to `Passed` and the toolbar reads `14 executions`; jump to a failing test from the digest and a thirteenth row appears — 15 executions on screen — while the toolbar still says 14. `digestJump` cleared `display` on the row and its ancestors itself and never recomputed, so it walked around the single writer the file's own contract names ("One writer, so the rendered value and every recomputation of it are the same sentence"). The element is `aria-live`, so the notice a screen-reader reader gets when a row arrives was silence. The count is now taken off the rows rather than accumulated while the filter decides them: `countExecutionsOnScreen` sums the visible rows' `data-runs` and writes through `setCount`, and both the filter and the jump call it. Anything that changes what the pane holds can restate the figure the same way. The gate is the reviewer's scenario exactly — `Passed`, then the digest jump — asserting the toolbar equals the executions actually visible, summed from the rows, with a precondition that the jump really did reveal one. It fails on the old code (`4 executions` where 5 are showing) and holds on the new. The goldens carry only this script change. Refs #439. * Say only what the fixtures show about the combined shape Two documentation nits from the review of #486. The cross-reference named `testExecutionCountsMatchAcrossBackends`; the test is `testExecutionCountsAgreeAcrossBackends`. A stale name in a repo that otherwise keeps them exact is a reader sent looking for a test that does not exist. And the note on a test that is both parameterized and repeated asserted more than anyone here can know: "legacy counts the product, modern the larger of the two". No fixture exercises that shape, which is the whole reason it is written down — and modern's answer in particular turns on where the `Repetition` nodes sit, `R` if they nest under `Arguments` rather than `max(R, A)`. The note now says the shape is unmeasured and names the gate that would catch it, which is what the reasoning behind the field actually supports. Refs #439.
Implements migration Tasks 3, 4, 5a, and 5b for #391 (milestone 4.0) as one PR, per the plan's mandate: the safety mechanism is the byte-identical gate between 5a and its pre-refactor baseline, and splitting would create unverifiable intermediate states.
What this PR does
ParsedResult, the backend-neutral port (Classes/ResultReading/ParsedResult.swift), plus theResultReaderandPayloadProvidingprotocols. Encodes the eight settled Task 2.5 answers: nofinish, noactivityType, no UTI, neutralParsedStatus,arguments: [String]slot for Swift Testing.LegacyResultReader: XCResultKit →ParsedResult, with the repetition merge (Set<TestCase>semantics moved fromTestGroup.init) and the failure-summary interleave moved into the reader. TDD'd viaLegacyResultReaderTests(5 tests, written failing first).ParsedResultwith zero behaviour change.ObjectClass→NodeKind(same emittedtest-summary/test-summary-groupclasses),ResultFilebecomes the legacyPayloadProvidingconformer and moves toResultReading/Legacy/, the missing.logExportFailedfault is recorded on the log write-failure path (with newFaultReportingTestscoverage),getCodeCoverage()(dead code) deleted,exportJson()and the payload lock table kept.ActivityTypedeleted, per-activity durations render(),Activity.uuidminted fromIdentifierPath, and the reader's interleave re-keyed fromfinishtostart(coordinator-ruled; see amendments).JUnitReport's activity mapping reduces to the failure flag.Every
identifierPath:argument stays (#430);HTMLTemplates.swiftuntouched; status maps throughParsedStatuswith.expectedFailurestill rendering as.unknown; empty-but-decodable reads return nil soParsedResult(runs: [])never reachesSummary.init.Gate results
5a gate — byte identity against the pre-refactor baseline (one fixture generation):
All three fixtures (
TestResults,SanityResults,RetryResults), raw bytes, no normalization. Full suite at 5a: 39 tests, 2 skipped, 0 failures. Import gate:grep -rln "import XCResultKit" Sources/ | grep -v ResultReading/Legacy→ no output.5b gate — enumerated diff against the 5a capture. Every changed line machine-classified into the four mandated shapes (classifier in the PR discussion; zero unclassified lines):
activity-internal/activity-user-created/activity-skipped-test/activity-delete-attachment(1.23s)→()[0-9a-f]{32})finish→start)Shape-4 verification per the coordinator's ruling: every moved line pairs a deletion with a content-identical insertion (modulo shape-2/3 edits on the same line, multiset-verified per fixture); the non-failure activity title sequence is hash-identical before and after (md5
3793385a…/ee998611…per fixture), so only failure rows moved;SanityResults(no failures) shows zero shape-4 lines. Exercised exactly where a failure's timestamp falls inside an activity's span:RetryTests.swift:47duringRetryable Activity,FirstSuite.swift:96duringActivity with DoubleQuote…,ThirdSuite.swift:32/36/42among thetest Activityrows. Full suite at 5b: 39 tests, 2 skipped, 0 failures.Plan and spec amendments (same PR, per the snippets-are-intent rule)
ParsedActivitycarries three transitional fields (transitionalActivityType,transitionalFinish,transitionalUUID) so 5a can meet byte-identity; Task 5b deletes them. The plan's model snippet was the post-5b shape and had no slot to "carry it throughParsedActivity" as 5a's own text required.finishuntil 5b — sorting bystartin 5a provably reordersRetryResults(failure duringRetryable Activity) and would fail the byte gate. Coordinator ruling 2026-08-12: finish-key in 5a, re-key tostartin 5b with the repositioning enumerated as shape 4. Also: iteration sort gains a source-position tie-break (same total order the deletedTestCase.mergeused).ResultFile.swiftis a move-and-modify toResultReading/Legacy/(the import gate is satisfiable no other way); the XCResultKitEmittableOutputextensions move with it. Failure rows keep their historical shallower indent (old failure init defaultedpaddingto 0) — keyed onParsedActivity.isFailure.JUnitReport.swiftadded to its file list —.systemOut/.skippedactivity states lose their only producers withActivityType(their pinning testtestRetryFunctionalityJunitis already skipped for CoreTests.testRetryFunctionalityJunit expectations drift with Xcode version #378 fixture drift).startpost-5b so the allow-list gains no entry), citingRetryResults/testRetryOnFailure().Fixture note: fixtures were regenerated once mid-task (the initial set predated
testAttachScreenshot, #393/#428) and the baseline was re-captured from the pre-refactor renderer against the new generation, so all gate comparisons are within one fixture generation. A first regeneration attempt hit a cold-simulator UI-runner crash; the retry produced complete bundles (17 test rows, 3 screenshot images verified in the baseline).Part of #391. Do not merge — an independent review follows.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes