Repository navigation
feat: keep tests marked unskippable from being skipped - #40
Merged
Merged
Conversation
Coverage only sees the code a test runs, so a test whose real inputs are fixtures, resources, the network or the environment can break without any covered code changing. Test authors can now mark such tests so they always run, even when TEST_COVERAGE_ATTRIBUTION_SKIP_FILE lists them. - Swift Testing: `.unskippable` and `.skippable` traits (SkippabilityTrait), on a test or a suite. The most specific one wins, so a test or a nested suite can opt back in under an unskippable suite. The `.coverageAttribution` trait checks them before it cancels a listed test. - XCTest: class methods the observer looks up by name, so nothing links the package or XCTest: `unskippableTests`, `isUnskippable` and `skippableTests`. They work from Swift (`@objc static let`) and Objective-C, and with the observer linked from a static archive with -ObjC. - Each unskippable test that runs is listed in a new `unskippable.tsv` under $TEST_COVERAGE_ATTRIBUTION_DIR/<pid>/, with whether the skip list named it (a forced run), so the runner that writes the list can stop listing it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Test authors can mark XCTest and Swift Testing tests, or whole classes and suites, as unskippable. Those tests always run, even when
TEST_COVERAGE_ATTRIBUTION_SKIP_FILElists them.Swift Testing. Two new traits,
.unskippableand.skippable(SkippabilityTrait). They're recursive, and the most specific one wins: a test's trait overrides its suite's, and a nested suite's overrides its parent's. The.coverageAttributiontrait checks them before it callsTest.cancel.XCTest. Class methods that the observer looks up by name, so the test target links nothing and the observer still never links XCTest:
unskippableTests([String])isUnskippable(Bool)skippableTests([String])The lists win over the class, and
unskippableTestswins overskippableTests. Names take any form the skip list takes (testFoo,testFoo()or the selector). A subclass inherits these methods. A method with an unexpected return type is ignored (checked through its type encoding). From Swift, declare them with@objc static let …; from Objective-C, as+ (BOOL)isUnskippableand the like.Output for Tuist. Each unskippable test that runs is appended once per process to a new
$TEST_COVERAGE_ATTRIBUTION_DIR/<pid>/unskippable.tsv, asmodule\tsuite\tname\tlisted. Names matchrecords.bin, so an XCTest test is named by its selector.listedis1when the skip list named the test, which makes it a forced run. The README documents the format.Why
Coverage-based selection only sees the code a test runs. Some tests depend on inputs coverage can't see: fixtures, resources, the network, the environment. Such a test can break without any covered code changing, so authors need a way to keep it from ever being skipped. DataDog's dd-sdk-swift-testing has the same feature (
Tag.dd.tia.unskippable,tiaSkippable, anditr.forced_runfor a listed test that ran anyway), and this follows its semantics: the most specific marker wins.Alternatives considered
Taginstead of a trait.test.tagsmerges every level into a set, which loses which level declared a tag, and "most specific wins" needs that. A dedicated trait is also explicit about what it does. The trait relies on Swift Testing putting a suite's recursive traits before a child's own traits, outermost first (Runner.Plan._recursivelyApplyTraits), so the lastSkippabilityTraitintest.traitsis the most specific. The tests cover this, including a nested suite that opts back in..coverageAttribution(.coverageAttribution(skippable: false)). It needs a third state for "no opinion", so that a plain.coverageAttributionon a nested suite doesn't reset its parent. That's subtle, and it adds a second scope-providing trait to every marked test.-ObjC. The cost: if you forget@objcin Swift, nothing happens. The README calls this out.records.bininstead of a new file. It would only work while recording, and Swift Testing would need a newscope_endsignature. A separate TSV keepsrecords.binandkRecordVersionunchanged, matcheslaunches.tsv, and doesn't depend on counters.Impact
SkippabilityTrait,.unskippable,.skippable, and the C functiontest_coverage_attribution_report_unskippable. The scope-functions table that copies of the library share inside one process gains a third member.TuistKit.CoverageObserverOutputneeds to parseunskippable.tsv, so selection can stop listing those tests and show forced runs. That's a follow-up in tuist/tuist.CoverageAttributionTrait,suite_without_skipped_tests, the observer's output list,SkipListTests.swiftand the README. Where the two changes are the same plumbing (theoutputdirectory for the UI test fixture,skipListEnvironment, movingswiftPMCoverageReport()into an extension), I used the same text so it merges cleanly. Whichever lands second rebases. An unskippable listed test is neither removed nor cancelled, so it shouldn't appear in that report.main(0.4.0) when this started, so this PR targetsmain.Validation
Fixtures/UITests(iOS simulator,SkipListscheme), serial, parallel and shard runs, checked against the result bundle andunskippable.tsv:skippableTeststest is still skipped (and the classsetUpruns);.unskippabletest;.skippabletest, and whose.skippablenested suite, are still skipped, with an.unskippabletest inside that nested suite that runs.skipsTheListedSwiftTestingTests(SwiftPM,Fixtures/Example): an unskippable test, an unskippable nested suite and a.skippableopt-out, plusunskippable.tsvwith bothlistedvalues. The#if compiler(>=6.3)expectations are kept.needsObjCToLinkTheObserverFromAStaticArchive: a Swift class (unskippableTests) and a new Objective-C class (isUnskippableplusskippableTests) in a bundle that links the observer from a static archive. With-ObjCthe markers are honoured and reported; without it everything runs.swift test. Everything passed exceptskipsTheListedTests, which failed only on a wrong expectation of mine (XCTest names a throwing testtestFoo, not by itsAndReturnError:selector). After fixing it,skipsTheListedTestspassed for serial, parallel and shard.skipsTheListedTests(all three runs),skipsTheListedSwiftTestingTests,runsEveryTestWithoutASkipListandneedsObjCToLinkTheObserverFromAStaticArchivepassed.mise run lintis clean (SwiftFormat, SwiftLint, clang static analyzer).🤖 Generated with Claude Code