How to test package traits?

Playing around with the new package traits feature, and it's almost great, but I'm running into one problem: I can't find a way to unit test non-default traits. Ideally, I'd like to do something like:

// swift-tools-version:6.1

import PackageDescription

let package = Package(
    name: "MyPackage",
    products: [
        .library(
            name: "MyPackage",
            targets: ["MyTarget"]
        ),
    ],
    traits: [
        "SomeTrait"
    ],
    targets: [
        .target(
            name: "MyTarget"
        ),
        .testTarget(
            name: "NoTraitTests",
            dependencies: ["MyTarget"],
            traits: []
        ),
        .testTarget(
            name: "WithSomeTraitTests",
            dependencies: ["MyTarget"],
            traits: ["SomeTrait"]
        )
    ]
)

What I'd like for this to do would be to compile the project twice; once with the trait enabled and once without, and run the unit tests in both configurations, to make sure both configurations work properly.

I do know about swift test --traits SomeTrait, but this seems to be all-in one way or another; I don't see a way to test both with one command. Furthermore, this option doesn't seem to be available at all when editing a package in Xcode's UI; I can't find any way to add arguments to the swift test command it uses under the hood, and somewhat strangely, whether it enables the trait or not appears to be somewhat random. It's usually off, but sometimes it just compiles with the trait enabled, and I'm not sure why.

Are there any tricks that I'm missing?

1 Like

Thanks for the feedback.

This was something that we called out as a future direction. I think it would be great to have a swift build/test --all-trait-combinations that uses all possible permutations. Right now you are correct that you have to manually spell out the different swift build/test invocations.

Since Xcode is out of scope of the Swift project and an Apple provided tool, do you mind filing a feedback to such a feature?

@FranzBusch I don't think --all-trait-combinations is a good idea. If you have 3 traits which are exclusive with each other then you need to be able to express that in the Package file. But that's not the main problem. The main problem is that if you have 10+ traits, each controlling a tiny unrelated portion of the behavior, do you really want to test 1000+ combinations? Probably not.

I think the best solution would be to be able to define multiple sets of traits for each test target and if you don't provide any flag that affects traits then the tests would be run multiple times, once with each set of traits. If you provide a flag that affects traits then all the tests will be run with just that set of traits. So if you have the 3-way exclusive traits and 10 other traits controlling unrelated parts, you could have just 3 set of traits and it would cover all the testing needs.

For the test targets that don't specify trait sets, they would be run with the default set of traits (or the traits provided by the command line flags).

With only one set of traits per test target (like in the OP), running the tests with a specific set of traits doesn't really have a good strategy. Either tests will fail or tests will be skipped. (Assuming multiple traits that are controlling unrelated parts of the code and each have a test target for the turned on and one for the turned off state, with all other trait turned off)

1 Like

Also if we have traits that don't change existing behavior, only add new APIs (like importing a 3rd party framework and providing convenience inits/protocol conformances/etc.), then I can see an "only run this test target if this trait is turned on" parameter (in addition to providing one or multiple sets of traits to use when running tests without specifying traits), so that tests about those additional APIs can live in their separate test target without needing #if trait around everything.

edit: I don't know if it was clear based on my comments but I think the main goal should be to have swift test test everything that the author of the package wants to test (minus multiple OSs), so that if someone wants to make a small change, they don't need to look for the correct test command in documentation/scripts/etc.

Hey! This seems like an interesting discussion and I wanted to see if there was an opportunity for me to contribute/help in working towards a solution.

Although the initial traits document does list --all-trait-combinations as a possible future direction, I agree with @Cyberbeni that it can get very intensive very quickly. It may still be a useful flag to have in some scenarios, but I’m unsure whether it effectively solves the problem being discussed here.

There is a a related issue #9523, which explores allowing a test target to request a non-default trait. That could address one non-default configuration at a time, but it doesn't appear to cover the broader requirement of building and testing several trait configurations in one invocation.

Perhaps the solution here is adding a configuration parameter to the Package in the Package.swift file that can declare the configurations/combinations of traits that will be used by this package? it could declare configurations for no traits, selected traits, default traits, or all traits.

Test targets could then indicate which of those configurations they should participate in. Running the test suite would require SwiftPM to build and test each selected configuration independently…

This way, we avoid running all 2^n combinations of traits, and only focus in on ones that are truly required by our package.

Still, I suppose some design questions remain on how we would go about this:

  • Does it make sense for plain swift test run every test trait configuration declared by the package author?
  • Or, should this require an explicit option like swift test --all-configurations for instance?
  • If the user passes a --traits argument, should that override the declared test configurations and produce a single test run?

I’d very much be interested in helping investigate or implement this, but I wanted to first ask whether this general direction makes sense? I’m assuming this would have to go through the evolution process in this case because of the scope (please correct me if I am wrong), but would love to hear and discuss if you think there are any other implementations that may help us work our way towards this solution.

Looking forward to discussing more!

2 Likes

Hey!

Wanted to follow up and refine what I suggested earlier. After some discussions with @FranzBusch and @kukushechkin, and actually working on this in code, I realized there is probably a better solution here. It's along similar lines to everything suggested in this thread earlier, but I think this new solution is much more robust and am looking forward to hearing about what you all think.

This is how I'd imagine things can be defined here:

// swift-tools-version: 6.4

import PackageDescription

let package = Package(
    name: "MyPackage",
    products: [
        .library(
            name: "MyPackage",
            targets: ["MyTarget"]
        ),
    ],
    traits: [
        "SomeTrait",
        "AnotherTrait",
    ],
    targets: [
        .target(
            name: "MyTarget"
        ),
        .testTarget(
            name: "NoTraitTests",
            dependencies: ["MyTarget"],
            traitConfigurations: [.disableAllTraits]
        ),
        // A target can declare several combinations and runs under each of them.
        .testTarget(
            name: "WithSomeTraitTests",
            dependencies: ["MyTarget"],
            traitConfigurations: [
                .enabledTraits(["SomeTrait"]),
                .enabledTraits(["SomeTrait", "AnotherTrait"]),
            ]
        ),
        // Targets declaring the same combination share one build.
        .testTarget(
            name: "WithBothTraitsTests",
            dependencies: ["MyTarget"],
            traitConfigurations: [.enabledTraits(["AnotherTrait", "SomeTrait"])]
        ),
    ]
)

Running swift test --enable-trait-configurations would then do the following:

Running tests with all traits disabled
Test Suite 'NoTraitTests' passed ...

Running tests with traits: SomeTrait
Test Suite 'WithSomeTraitTests' passed ...

Running tests with traits: AnotherTrait, SomeTrait
Test Suite 'WithSomeTraitTests' passed ...
Test Suite 'WithBothTraitsTests' passed ...

This will run multiple builds, one for each unique trait combination specified across all test targets' traitConfigurations. For each combination's build, it'll find all the test targets that are relevant and will run those. The last two targets in the example declare the same combination, so they share one build, even though in a separate order. Passing in multiple configurations is also supported as shown above, which you can see in action above: WithSomeTraitTests runs twice under different configurations.

I initially considered an array of sets that would represent all applicable combinations, but instead landed on mirroring SwiftPM's existing TraitConfiguration. It lets you express .enableAllTraits without enlisting every trait manually, and lets you type .default instead of re-writing the default traits every time.

This makes it fully opt-in, and doesn't break any existing code or expectations that come with swift test, or anything else for that matter--which I now realize I wrongly suggested earlier.

Another build optimization, per @FranzBusch's suggestion, has been made. Instead of all configurations being in the same build directory, each one now builds into its own sub-folder. In my testing, a second run of a small example package dropped from 27 seconds to 4.

I've kept this to happen only for this flag in specific to avoid messing with other expectations elsewhere, but is definitely something that could be expanded beyond this feature in the future I suppose.

Curious to hear from others about what they think of this shape, and whether anything here feels off before I take it further! :)

5 Likes

This is fantastic! I like the idea of re-using the TraitConfiguration for test targets, as you said there are some ergonomic keywords that can help to configure these test settings without making it too verbose.

I think there is also some benefit to considering the CLI cases wherein a user could specify a list of enabled traits (or, perhaps --disable-default-traits, etc.) for a particular test target as well. That way folks can still invoke the desired test suite where there perhaps isn't yet a test target mirroring it with the trait configurations.

Quick clarifying question on your suggestion here -- in this scenario, would running swift test --enable-trait-configurations be the command + opt-in flag that would then pick up on the test target's traitConfigurations: ?

Thank you!

Curious about what you suggested: do you think something like the existing swift test --filter SomeTestTarget --traits SomeTrait or swift test --test-product NoTraitTestTarget --disable-default-traits and permutations of those might work for that use case? Or if there is something else you had in mind that I'm missing?

To your last question, yes, that's right. That was indeed the intention for the opt-in flag. I also considered --enable-test-trait-combinations and a few others along those lines. Do you have any suggestions for a more ergonomic name? Perhaps the name of the traitConfigurations parameter can also be changed to match the new flag, if needed.

Yes, something similar to declaring --traits <...> for the test command but with a bit more flexibility.

Say I had a test target that had no declared trait configurations in the Package.swift, however I'd like to be able to run the test suite with some traits before constructing another test target:

    .testTarget(
        name: "MyTestSuite",
        dependencies: \["MyTarget"\]
    ),

For whatever reason I don't have another .testTarget that declares a trait configuration, but let's say I'm curious about invoking these tests with a few trait configurations to potentially debug where some tests may be going awry:

swift test --filter MyTestSuite --trait-combinations Trait1 Trait2

This would essentially mimic the use case of having several trait configurations in a single test target, where this would test against enabling:

  • Trait1
  • Trait2
  • Trait1 and Trait2

Which is similar to the --all-trait-combinations idea but with a specified list of traits. I haven't thought too deeply about the impact of this, but thought to throw the idea out there for consideration :slight_smile:

As for the naming of the opt-in flag, I think something like --enable-trait-configurations or even --enable-test-traits could work. I'm not too strongly opinionated there.

1 Like

Oh! I get it now, thanks a lot for the detailed explanation. Very interesting, almost like an "all combinations of", I suppose? I like the idea of scoping the all combinations to a certain number of traits to avoid test combinations from growing exponentially out of hand.

I was wondering how we could complement this CLI option for our --enable-test-traits flag (I do like this name for its succinctness, haha). Do you think making a custom type that adds a .allCombinationsOf([String]) to the existing options offered by TraitConfiguration might be useful? This would only be a part of the manifest-facing enum and will act as a generator that later simplifies into plain TestConfigurations when the manifest is being parsed. I suppose it is slightly odd semantically, but offers a powerful tool that can reduce further manual enumeration in some cases.

1 Like

I definitely encourage playing around with adding possible options to a TraitConfiguration, though I would just caution against the fact that the purpose of this enum today is to be used as an internal model for SwiftPM to consume and is translated from user-provided CLI arguments ([String] -> TraitConfiguration) :slightly_smiling_face:

It might be useful to create a type within the PackageDescription itself that can translate into the PackageModel.TraitConfiguration for SwiftPM to consume, since TraitConfiguration itself is not accessible in a Package.swift today.

Hi, yes, that is how I tried to implement it already in my POC which I based my previous discussion on--even without the addition of .allCombinationsOf!

It is a type in the PackageDescription which is serialized and then converted during manifest loading into PackageModel.TraitConfiguration.

The intention was to keep the original internal enum unmodified; the copy in the PackageDescription can get the .allCombinationsOf([String]) as a generator that translates into a multiple .enabledTraits() for the PackageModel during manifest loading.

I think this .allCombinationsOf case might be a nice addition that may allow us to write more maintainable, cleaner, and clearer code that skips the issues that may arise if we were to instead manually list out every possible combination, and describes the intention clearly. I'll experiment with implementing this shortly to see this in action. Until then, what do you think?

I just made the pitch live: [Pitch] Declaring trait configurations for test targets for easier testing

Thank you! Excited to hear what more people have to say about the same!

1 Like