refactor(ddc): extract monitor-proximity matching into a pure testable function - #27
Conversation
didriksg
left a comment
There was a problem hiding this comment.
Really solid extraction. I traced the matcher against the old inline logic and the semantics are preserved exactly, and I could reproduce the mutation-kill behavior described in the PR body. Four items below: the identity-struct dedup and the em-dash sweep are the only hard asks, the other two are suggestions to take or push back on.
| /// `ProductAttributes` (`LegacyManufacturerID` / `ProductID` / `SerialNumber`) | ||
| /// which line up with `CGDisplayVendorNumber` / `CGDisplayModelNumber` / | ||
| /// `CGDisplaySerialNumber` for the same physical display. | ||
| struct Identity: Equatable { |
There was a problem hiding this comment.
DDCServiceMatcher.Identity is a field-for-field copy of the private DisplayIdentity in DDCService.swift, and the call site now does a manual 1:1 conversion between them. Since the only reason for the copy is that DisplayIdentity is private, I'd delete DisplayIdentity and use DDCServiceMatcher.Identity everywhere in DDCService (including displayIdentity(from:)). Two identical structs will drift eventually, and the conversion loop disappears too.
| /// Service→display assignments in ascending service-index order. | ||
| let assignments: [Assignment] | ||
| /// Service indices still unclaimed **after** the Strategy 2 fallback. | ||
| let unmatchedServiceIndices: [Int] |
There was a problem hiding this comment.
unmatchedServiceIndices and the claimedByFallback bookkeeping exist only for the tests; the runtime reads just assignments and ambiguous. The doc note already calls this out as the one departure, which is exactly why it's easy to move: the tests can derive unmatched indices as services.indices minus assignments.map(\.serviceIndex), letting Result shrink to the two fields production consumes. If the field is meant for a future UI use, a note at the call site would make that intent visible. Suggestion, not blocking.
|
|
||
| // Invert displayID→serviceIndex into ascending service-index order. Each | ||
| // service index appears as a value at most once, so the inversion is sound. | ||
| let assignments = serviceByDisplayID |
There was a problem hiding this comment.
match builds a displayID-keyed dictionary, inverts it into a sorted [Assignment], and buildAVServiceMapByProximity immediately inverts it back into a displayID-keyed map. If Result carried [CGDirectDisplayID: Int] directly, the Assignment struct, the sort, and the inversion-soundness comment all go away, and the tests stay one-line asserts since dictionaries are Equatable. Taken together with the suggestion above, match could return (byDisplayID: [CGDirectDisplayID: Int], ambiguous: Bool) and both files get shorter. Suggestion only: if you prefer the explicit service-order reading in the tests, that is a fair trade.
| /// | ||
| /// Inputs mirror exactly what the runtime method feeds it: | ||
| /// - `services` are the working DDC channels in **IORegistry traversal order** | ||
| /// (order-sensitive — do not sort). |
There was a problem hiding this comment.
Nit: house style here avoids em dashes in comments, and there are around 9 across the two new files (e.g. this line, and DDCServiceMatcherTests.swift line 8). Commas, colons, or parentheses instead; one mechanical sweep.
…cher.Result Addresses review on didriksg#27: reuse DDCServiceMatcher.Identity in DDCService instead of a private field-for-field copy, return the displayID-keyed mapping directly instead of inverting it into a sorted [Assignment], drop the test-only unmatchedServiceIndices bookkeeping, and sweep em dashes from the two new files. No behaviour change: verified against the previous implementation over a 62-case differential corpus.
|
Thanks for the trace, that is a much more useful review than a rubber stamp. Pushed 1. Duplicated identity struct (hard ask). 2 + 3. The Test coverage did not shrink. Still 12 tests, before and after. Assertions on let claimed = Set(result.byDisplayID.values)
return services.indices.filter { !claimed.contains($0) }which is identical by construction, since an index appears in 4. Em dashes (hard ask). Swept, Equivalence proof. Since this round changes the return shape, I ran an exhaustive
|
|
Pushed
Semantics unchanged: the 12 mutation-kill tests stay green asserting the same pairings (the order-sensitive cases — sorted-leftover and display-iteration-order — still kill their mutations under dictionary equality), |
|
Thanks @didriksg — pushed a rework (commit
|
didriksg
left a comment
There was a problem hiding this comment.
Rework verified: all four review items addressed. Independently checked the matcher against the original inline algorithm on 50k fuzzed cases (identical maps and ambiguity flags), and the full test suite passes locally on a simulated merge with 1.3.3-dev (15/15). Thanks for the thorough mutation-tested contribution!
What & why
The DDC AVService identity-matching core (rewritten in #13 to fix wrong-display brightness on Apple Silicon) is currently inlined inside
buildAVServiceMapByProximity(), interleaved with liveIOAVServiceReadI2Cprobes andIORegistryEntryCreateCFPropertycalls. Its decision logic — vendor+product+serial exact match → vendor+product fallback → sorted-traversal-order fallback → ambiguity flag — has no unit tests and cannot be exercised without a real external monitor.This PR extracts that decision core into a pure function (
DDCServiceMatcher.matchinCrisp/Models/DDCServiceMatcher.swift) and ships a headlessXCTestCasethat pins its documented behavior. Runtime DDC pairing is unchanged:buildAVServiceMapByProximity()now calls the matcher and reconstructs the samemap+mappingWarning. 1:1 refactor for testability of the area most recently rewritten in #13.How tested
make test(xcodegen +xcodebuild test -scheme Crisp -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO SWIFT_VERSION=5 SWIFT_STRICT_CONCURRENCY=minimal): all tests pass. 15 test cases (3 inDisplayModeGeometryTests+ 12 inDDCServiceMatcherTests). Prints** TEST SUCCEEDED **.CGDisplayVendorNumber/CGDisplayModelNumber/CGDisplaySerialNumbervalues and the sameDisplayIdentity(vendor/product/serial) the inline code consumed, and the call site reconstructsmapviamap[a.displayID] = ordered[a.serviceIndex]— the exact inverse of the originalmap[matched] = ordered[i](a bijection over the assigned set, since each service claims at most one display). ThemappingWarningliteral is byte-identical. The IORegistry walk,DCPAVServiceProxydetection, theIOAVServiceReadI2Cprobe,displayIdentity(from:),findAVService, and the cache are untouched (one diff hunk, matching block only). The one documented, test-only departure: the returnedunmatchedServiceIndicesreflects post-fallback reality rather than the original's discarded Strategy-1 scratch array — the runtime reads onlyresult.assignmentsandresult.ambiguous, so behavior is unaffected.testByModelFallbackPicksCorrectDisplayNotFallbackOrder), M2 (drop used-display guard →testIdenticalMonitorsShareUsedDisplayGuard), M3 (flip ambiguity> 1to> 0→testAmbiguousFlagRequiresMoreThanOneLeftover), plus bonus M4 (sortdisplaysbefore the scan →testIdenticalRealSerialMonitorsPreserveDisplayIterationOrder) and drop-exact/only-byModel (testExactSerialMatchPrefersCorrectSerialOverByModel).Checklist
./dev.sh, or./scripts/release.sh v0.0.0-cifor the full release build)