Using Swift Testing with CMake

Hey all,

I am using CMake with my Swift -> Zephyr project and have some code I'd like to test using Swift Testing. It's code that is pure Swift and does not contain any hardware or Zephyr-specific dependencies. My idea was to put this code into a separate module from the "Firmware" module I have, and in this separate module be able to import that into a "Tests" module to then write some tests for my code:

import Testing

@testable import FirmwareCore

struct MyTests {
   @Test func testFeatureA() {
     ....
   }
}

So my question is- is this possible to do with CMake? How would I then run these tests?

2 Likes

There’s some documentation about this in the swift-testing repo. See CMake.md.

4 Likes

Cool! I need to get better at looking at the repo first before asking questions...

However, I'm curious if the Testing module that is included with Swift 6.1 can be used directly instead of having to include the swift-testing package. I'll give it a try either way!

Answered my own question by trying it. Here's what I ended up with in my CMakeLists.txt:

cmake_minimum_required(VERSION 3.29)
project(SampleTests Swift)

add_executable(SampleTests Test.swift)
set_target_properties(SampleTests PROPERTIES
    SUFFIX .swift-testing
)
target_link_libraries(SampleTests PRIVATE Testing)
target_compile_options(SampleTests PRIVATE -parse-as-library)

This does need to be separate from the Zephyr CMakeLists.txt since that one is set to compile for armv7em-none-none-eabi. The Testing module is not available for embedded, of course.

My next step is to create my core library and include that to start testing the code. But, this is a good start.

1 Like

That reminds me—we should update that file to describe the correct entry point to use rather than the SwiftPM one.

1 Like

This is not the one?

@main struct Runner {
    static func main() async {
        await Testing.__swiftPMEntryPoint() as Never
    }
}

It works for me, but what would be the correct entry point to use?

Note the callout immediately under that sample code:

:warning: Warning

The entry point is expected to change to an entry point designed for other build systems prior to the initial stable release of Swift Testing.

We've implemented that, we just haven't updated this particular documentation. Assuming you've built Swift Testing from source and can import SPI, you'll do something like this instead:

@_spi(ForToolsIntegrationOnly) import Testing
import YourCStandardLibraryNameHere /* Darwin, Glibc, etc. */

let entryPoint = Testing.ABI.v0.entryPoint

@main struct Runner {
  static func main() async throws {
    let result = try await entryPoint(nil) { _ in }
    exit(result ? EXIT_SUCCESS : EXIT_FAILURE)
  }
}

If you do not have access to SPI symbols, you can load a reference to this function from the C function swt_abiv0_getEntryPoint(). This approach currently requires a tiny bit more ceremony:

import Testing
import YourCStandardLibraryNameHere /* Darwin, Glibc, etc. */

@_extern(c, "swt_abiv0_getEntryPoint")
func swt_abiv0_getEntryPoint() -> UnsafeRawPointer

let entryPoint = unsafeBitCast(
  swt_abiv0_getEntryPoint(),
  to: (@convention(thin) @Sendable (
    UnsafeRawBufferPointer?,
    @escaping @Sendable (UnsafeRawBufferPointer) -> Void
  ) async throws -> Bool).self
)

/* struct Runner same as before */

Note that @_extern(c) is an experimental language feature; if you can't use it, you can use dlsym() or equivalent instead to dynamically look up swt_abiv0_getEntryPoint(), or if you have a C module, declare it extern there.

Wow, seems kind of hacky compare to the solution from the CMake guide. Why not just provide an official entry point that can be used for CMake instead of requiring Swift Testing to be built from source or using an experimental language feature? :sweat_smile:

Either way my goal is to not have to compile Swift Testing from source but to use the Testing module in Swift 6.1. I did try to use the 2nd solution with -enable-experimental-feature Extern but as I said...it seems kind of hacky to me...

import Testing

#if canImport(Glibc)
    import Glibc
#elseif canImport(Darwin)
    import Darwin
#endif

@_extern(c, "swt_abiv0_getEntryPoint")
func swt_abiv0_getEntryPoint() -> UnsafeRawPointer

let entryPoint = unsafeBitCast(
    swt_abiv0_getEntryPoint(),
    to: (@convention(thin) @Sendable (
        UnsafeRawBufferPointer?,
        @escaping @Sendable (UnsafeRawBufferPointer) -> Void
    ) async throws -> Bool).self
)

Regardless, hope this can help anyone else who is looking on how to use Testing with CMake, and I'll use it for my own project as well :blush:

Well yes, it absolutely is hacky. :grin: We don't officially support using Swift Testing via CMake (except as part of the Swift toolchain build) in the first place, so you're already off the beaten path here. The expectation is that most developers will use a Swift package or an Xcode project.

That doesn't mean we could never support other workflows, but we don't today, which means we don't offer API in support of said workflows. The compromise is to provide the aforementioned ABI-stable entry point function for workflows that aren't SwiftPM- or Xcode-based and to promise that that function will remain exported and accessible for the long term.

1 Like

That's fair. Well, at least there are some ways now to use it with CMake, even if it's not officially supported!

1 Like