Skip to content

Logs tab renders an 86-byte husk on the default (legacy) reader #480

Description

@tylervick

Summary

On the default reader path the Logs tab renders an 86-byte husk. The tab loads, the iframe gets HTTP 200, and the page shows four section titles and nothing else. Rendering the same bundle with --result-reader modern produces 4,853 bytes of real log.

auto resolves to legacy for as long as xcresulttool still accepts --legacy, so this is what every user gets today.

Evidence

Same TestResults.xcresult, same binary, only --result-reader differs:

reader exported log lines
default (auto → legacy) 86 bytes 10
modern 4,853 bytes 83

The entire legacy payload:

Test MainScheme

Launch actions

Launch SampleAppUITests


Launch SampleAppUnitTests

Every line is a section title. No device information, no install actions, no launch lines, no per-test rows. The modern payload for the same bundle carries Platform / Device Identifier / Operating System Version / Model / Target Architecture, Install Actions, launch lines, and a -------- ... -------- section per test target — content that lines up with what Xcode's own Log view shows under All Messages for the same bundle.

Recon artifacts: orca-artifacts/xcode-vs-a1/log-evidence/ (both payloads), a1-logs-1440-light.png vs a1-logs-modern-1440-light.png.

Root cause

The legacy log document is a polymorphic tree. Its nodes are ActivityLogSection, ActivityLogUnitTestSection and ActivityLogCommandInvocationSection, and the log text lives in each node's messages[]. Content is lost at two independent points:

1. Decode-time type filter (primary). XCResultFile.getLogs(id:) returns ActivityLogSection, whose children XCResultKit decodes as

subsections = xcArray(element: "subsections", from: json).ofType(ActivityLogSection.self)

and ofType keeps only elements whose _type._name is exactly "ActivityLogSection". Every subtyped child is discarded before any of our code runs. On the fixture that deletes both Test target … branches at the root and every Install Actions / Launch … leaf.

2. A formatter written for the wrong shape (secondary). ActivityLogSection.formatEmittedOutput() in Legacy/ResultFile.swift emits title plus its subsections and nothing else:

extension ActivityLogSection: EmittableOutput {
    func formatEmittedOutput() -> String {
        "\(title)\n\n" + subsections
            .compactMap { $0.formatEmittedOutput() }
            .joined(separator: "\n")
    }
}

ActivityLogSection has no emittedOutput at all — that property is declared only on the ActivityLogUnitTestSection subtype — so the content the surviving nodes do carry, messages[], is never read. The sibling ActivityLogUnitTestSection: EmittableOutput conformance that concatenates emittedOutput is unreachable on this path: getLogs is typed to the base class and subsections is [ActivityLogSection], so Swift statically dispatches the base implementation at every level.

Four titles survive both filters. That is the 86-byte husk, exactly.

It is not the mirror image of the modern gap

The migration's verified facts note that modern publishes emittedOutput but never populates it, carrying content in messages[] instead. Dumping both documents for the same bundle shows they describe a node-for-node identical tree with identical messages[]. Legacy additionally populates emittedOutput on its unit-test nodes (9.6 KB, nested cumulatively — a parent repeats every descendant's text), but that is the per-row console popover in Xcode, not the Log view; Xcode's own All Messages view renders titles and messages, which is precisely what the modern reader already emits. So messages[] is the log on both sides, and reading it makes the two exports byte-identical rather than merely similar.

Why nothing caught it

DifferentialTests compares the rendered index.html and attachment filenames and bytes. The exported log payload is neither, so a 56× content difference between the backends passes green. The iframe src is a digest of the run rather than of the content, so it is byte-identical on both sides and the divergence cannot even reach the HTML comparison.

Why it is worse than a plain bug

It fails in the way that is hardest to notice — the tab loads, the iframe is populated, the response is 200, nothing warns — and it will silently self-repair the day Apple removes --legacy and auto flips to modern, taking the evidence with it.

Fix

Read messages[] on the legacy path through a decoder that does not throw away subtyped nodes, and format both backends through one shared formatter so they cannot drift again. Extend DifferentialTests to compare exported log content across backends, the way it already compares attachment bytes.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions