Skip to content

Commit 0bad002

Browse files
authored
Add a document about porting to new platforms. (#711)
This PR adds a new Porting.md file including information for developers who want to port Swift Testing to new platforms. The document uses Classic Mac OS as a strawman/example platform. That was fun. Shoutout to @belkadan. It also adds a `SWT_NO_TIMESPEC` conditional because I realized that there are platforms that don't have C11's `struct timespec`. (Because Classic Mac OS should obviously have C types standardized 2011.) Formatted version [here](https://github.com/swiftlang/swift-testing/blob/jgrynspan/porting-doc/Documentation/Porting.md). ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent aa427e7 commit 0bad002

File tree

4 files changed

+268
-3
lines changed

4 files changed

+268
-3
lines changed

Documentation/Porting.md

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
# Porting to new platforms
2+
3+
<!--
4+
This source file is part of the Swift.org open source project
5+
6+
Copyright (c) 2024 Apple Inc. and the Swift project authors
7+
Licensed under Apache License v2.0 with Runtime Library Exception
8+
9+
See https://swift.org/LICENSE.txt for license information
10+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
11+
-->
12+
13+
When the Swift toolchain is ported to a new platform, it is necessary to port
14+
Swift Testing as well. This document contains various information, trivia, and
15+
deep wisdoms about porting Swift Testing.
16+
17+
> [!NOTE]
18+
> This document uses Classic Mac OS ("Classic") as an example target platform.
19+
> In this hypothetical scenario, we assume that the Swift compiler identifies
20+
> Classic with `os(Classic)` and that the C++ compiler identifies it with
21+
> `defined(macintosh)`. Other platforms would be identified differently.
22+
23+
## Getting started
24+
25+
Before you start the porting process, make sure you are very familiar with Swift
26+
and C++ as well as the C standard library and platform SDK for your target
27+
platform.
28+
29+
Your first task when porting Swift Testing is ensuring that it builds.
30+
31+
We've made an effort to ensure that as much of our code as possible is
32+
platform-agnostic. When building the toolchain for a new platform, you will
33+
hopefully find that Swift Testing builds out-of-the-box with few, if any,
34+
errors.
35+
36+
> [!NOTE]
37+
> Swift Testing relies on the Swift [standard library](https://github.com/swiftlang/swift),
38+
> Swift macros (including the [swift-syntax](https://github.com/swiftlang/swift-syntax) package),
39+
> and [Foundation](https://github.com/apple/swift-foundation). These components
40+
> must build and (minimally) function before you will be able to successfully
41+
> build Swift Testing regardless of which platform you are porting to.
42+
43+
### Swift or C++?
44+
45+
Generally, prefer to implement changes in Swift rather than C++ where possible.
46+
Swift Testing is a Swift package and our goal is to keep as much of it written
47+
in Swift as we can. Generally speaking, you should not need to write much code
48+
using C++.
49+
50+
## Resolving "platform-specific implementation missing" warnings
51+
52+
The package will _not_ build without warnings which you (or we) will need
53+
to resolve. These warnings take the form:
54+
55+
> ⚠️ WARNING: Platform-specific implementation missing: ...
56+
57+
These warnings may be emitted by our internal C++ module (`_TestingInternals`)
58+
or by our library module (`Testing`). Both indicate areas of our code that needs
59+
platform-specific attention.
60+
61+
Most platform dependencies can be resolved through the use of platform-specific
62+
API. For example, Swift Testing uses the C11 standard [`timespec`](https://en.cppreference.com/w/c/chrono/timespec)
63+
type to accurately track the durations of test runs. If you are porting Swift
64+
Testing to Classic, you will run into trouble getting the UTC time needed by
65+
`Test.Clock`, but you could use the platform-specific [`GetDateTime()`](https://developer.apple.com/library/archive/documentation/mac/pdf/Operating_System_Utilities/DT_And_M_Utilities.pdf)
66+
function to get the current system time.
67+
68+
### Including system headers
69+
70+
Before we can call `GetDateTime()` from Swift, we need the Swift compiler to be
71+
able to see it. Swift Testing includes an internal clang module,
72+
`_TestingInternals`, that includes any system-provided C headers that we use as
73+
well as a small amount of C++ glue code (for code that cannot currently be
74+
implemented directly in Swift.) `GetDateTime()` is declared in `DateTimeUtils.h`
75+
on Classic, so we would add that header to `Includes.h` in the internal target:
76+
77+
```diff
78+
--- a/Sources/_TestingInternals/include/Includes.h
79+
+++ b/Sources/_TestingInternals/include/Includes.h
80+
81+
+#if defined(macintosh)
82+
+#include <DateTimeUtils.h>
83+
+#endif
84+
```
85+
86+
We intentionally don't import platform-specific C standard library modules
87+
(`Darwin`, `Glibc`, `WinSDK`, etc.) in Swift because they often include overlay
88+
code written in Swift and adding those modules as dependencies would make it
89+
more difficult to test that Swift code using Swift Testing.
90+
91+
### Changes in Swift
92+
93+
Once the header is included, we can call `GetDateTime()` from `Clock.swift`:
94+
95+
```diff
96+
--- a/Sources/Testing/Events/Clock.swift
97+
+++ b/Sources/Testing/Events/Clock.swift
98+
99+
fileprivate(set) var wall: TimeValue = {
100+
#if !SWT_NO_TIMESPEC
101+
// ...
102+
+#elseif os(Classic)
103+
+ var seconds = CUnsignedLong(0)
104+
+ GetDateTime(&seconds)
105+
+ seconds -= 2_082_844_800 // seconds between epochs
106+
+ return TimeValue((seconds: Int64(seconds), attoseconds: 0))
107+
#else
108+
#warning("Platform-specific implementation missing: UTC time unavailable (no timespec)")
109+
#endif
110+
}
111+
```
112+
113+
## Runtime test discovery
114+
115+
When porting to a new platform, you may need to provide a new implementation for
116+
`enumerateTypeMetadataSections()` in `Discovery.cpp`. Test discovery is
117+
dependent on Swift metadata discovery which is an inherently platform-specific
118+
operation.
119+
120+
_Most_ platforms will be able to reuse the implementation used by Linux and
121+
Windows that calls an internal Swift runtime function to enumerate available
122+
metadata. If you are porting Swift Testing to Classic, this function won't be
123+
available, so you'll need to write a custom implementation instead. Assuming
124+
that the Swift compiler emits section information into the resource fork on
125+
Classic, you could use the [Resource Manager](https://developer.apple.com/library/archive/documentation/mac/pdf/MoreMacintoshToolbox.pdf)
126+
to load that information:
127+
128+
```diff
129+
--- a/Sources/_TestingInternals/Discovery.cpp
130+
+++ b/Sources/_TestingInternals/Discovery.cpp
131+
132+
// ...
133+
+#elif defined(macintosh)
134+
+template <typename SectionEnumerator>
135+
+static void enumerateTypeMetadataSections(const SectionEnumerator& body) {
136+
+ ResFileRefNum refNum;
137+
+ if (noErr == GetTopResourceFile(&refNum)) {
138+
+ ResFileRefNum oldRefNum = refNum;
139+
+ do {
140+
+ UseResFile(refNum);
141+
+ Handle handle = Get1NamedResource('swft', "\p__swift5_types");
142+
+ if (handle && *handle) {
143+
+ size_t size = GetHandleSize(handle);
144+
+ body(*handle, size);
145+
+ }
146+
+ } while (noErr == GetNextResourceFile(refNum, &refNum));
147+
+ UseResFile(oldRefNum);
148+
+ }
149+
+}
150+
#else
151+
#warning Platform-specific implementation missing: Runtime test discovery unavailable
152+
template <typename SectionEnumerator>
153+
static void enumerateTypeMetadataSections(const SectionEnumerator& body) {}
154+
#endif
155+
```
156+
157+
## C++ stub implementations
158+
159+
Some symbols defined in C and C++ headers, especially "complex" macros, cannot
160+
be represented in Swift. The `_TestingInternals` module includes a header file,
161+
`Stubs.h`, where you can define thin wrappers around these symbols that are
162+
visible to Swift. For example, to use timers on Classic, you'll need to call
163+
`NewTimerUPP()` to define the timer's callback, but that symbol is sometimes
164+
declared as a macro and cannot be called from Swift. You can add a stub function
165+
to `Stubs.h`:
166+
167+
```diff
168+
--- a/Sources/_TestingInternals/include/Stubs.h
169+
+++ b/Sources/_TestingInternals/include/Stubs.h
170+
171+
+#if defined(macintosh)
172+
+static TimerUPP swt_NewTimerUPP(TimerProcPtr userRoutine) {
173+
+ return NewTimerUPP(userRoutine);
174+
+}
175+
+#endif
176+
```
177+
178+
Stub functions should generally be `static` to allow for inlining and when
179+
possible should be named to match the symbols they wrap.
180+
181+
## Unavailable features
182+
183+
You may find that some feature of C++, Swift, or Swift Testing cannot be ported
184+
to your target platform. For example, Swift Testing's `FileHandle` type includes
185+
an `isTTY` property to determine if a file handle refers to a pseudoterminal,
186+
but Classic did not implement pseudoterminals at the file system layer, so
187+
`isTTY` cannot be meaningfully implemented.
188+
189+
For most situations like this one, you can guard the affected code with a
190+
platform conditional and provide a stub implementation:
191+
192+
```diff
193+
--- a/Sources/Testing/Support/FileHandle.swift
194+
+++ b/Sources/Testing/Support/FileHandle.swift
195+
196+
var isTTY: Bool {
197+
#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(Android) || os(WASI)
198+
// ...
199+
+#elseif os(Classic)
200+
+ return false
201+
#else
202+
#warning("Platform-specific implementation missing: cannot tell if a file is a TTY")
203+
return false
204+
#endif
205+
}
206+
```
207+
208+
If another function in Swift Testing asks if a file is a TTY, it will then
209+
always get a result of `false` (which is always the correct result on Classic.)
210+
No further changes are needed in this case.
211+
212+
If your target platform is missing some feature that is used pervasively
213+
throughout Swift Testing, this approach may be insufficient. Please reach out to
214+
us in the Swift forums for advice.
215+
216+
## Adding new dependencies
217+
218+
Avoid adding new Swift package or toolchain library dependencies. Swift Testing
219+
needs to support running tests for all Swift targets except, for the moment, the
220+
Swift standard library itself. Adding a dependency on another Swift component
221+
means that that component may be unable to link to Swift Testing. If you find
222+
yourself needing to link to a Swift component, please reach out to us in the
223+
Swift forums for advice.
224+
225+
> [!WARNING]
226+
> Swift Testing has some dependencies on Foundation, specifically to support our
227+
> JSON event stream. Do not add new uses of Foundation without talking to us
228+
> first. If you _do_ add any new uses of Foundation (including any related
229+
> modules such as CoreFoundation or FoundationEssentials), they _must_ be
230+
> imported using the `private` keyword.
231+
232+
It is acceptable to add dependencies on C or C++ modules that are included by
233+
default in the new target platform. For example, Classic always includes the
234+
[Memory Manager](https://developer.apple.com/library/archive/documentation/mac/pdf/Memory/Memory_Preface.pdf),
235+
so there is no problem using it. On the other hand, [WorldScript](https://developer.apple.com/library/archive/documentation/mac/pdf/Text.pdf)
236+
is an optional component, so the Classic port of Swift Testing must be able to
237+
function when it is not installed.
238+
239+
If you need Swift Testing to link to additional libraries at build time, be sure
240+
to update both the [package manifest](https://github.com/swiftlang/swift-testing/blob/main/Package.swift)
241+
and the library target's [CMake script](https://github.com/swiftlang/swift-testing/blob/main/Sources/Testing/CMakeLists.txt)
242+
to include the necessary linker flags.
243+
244+
## Adding CI jobs for the new platform
245+
246+
The Swift project maintains a set of CI jobs that target various platforms. To
247+
add CI jobs for Swift Testing or the Swift toolchain, please contain the CI
248+
maintainers on the Swift forums.
249+
250+
If you wish to host your own CI jobs, let us know: we'd be happy to run them as
251+
part of Swift Testing's regular development cycle.

Documentation/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ principles and goals.
3636
support status of various platforms, and more.
3737
- [Contributing](https://github.com/swiftlang/swift-testing/blob/main/CONTRIBUTING.md)
3838
provides guidance for developing and making project contributions.
39+
- [Porting](https://github.com/swiftlang/swift-testing/blob/main/Documentation/Porting.md)
40+
includes advice and instructions for developers who are porting Swift Testing
41+
to a new platform.
3942
- [Style Guide](https://github.com/swiftlang/swift-testing/blob/main/Documentation/StyleGuide.md)
4043
describes this project's guidelines for code and documentation style.
4144
- [SPI groups in Swift Testing](https://github.com/swiftlang/swift-testing/blob/main/Documentation/SPI.md)

Sources/Testing/Events/Clock.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ extension Test {
2222
public struct Instant: Sendable {
2323
/// The suspending-clock time corresponding to this instant.
2424
fileprivate(set) var suspending: TimeValue = {
25-
#if SWT_TARGET_OS_APPLE
25+
#if !SWT_NO_TIMESPEC && SWT_TARGET_OS_APPLE
2626
// The testing library's availability on Apple platforms is earlier than
2727
// that of the Swift Clock API, so we don't use `SuspendingClock`
2828
// directly on them and instead derive a value from platform-specific
@@ -40,6 +40,7 @@ extension Test {
4040
#if !SWT_NO_UTC_CLOCK
4141
/// The wall-clock time corresponding to this instant.
4242
fileprivate(set) var wall: TimeValue = {
43+
#if !SWT_NO_TIMESPEC
4344
var wall = timespec()
4445
#if os(Android)
4546
// Android headers recommend `clock_gettime` over `timespec_get` which
@@ -49,6 +50,10 @@ extension Test {
4950
timespec_get(&wall, TIME_UTC)
5051
#endif
5152
return TimeValue(wall)
53+
#else
54+
#warning("Platform-specific implementation missing: UTC time unavailable (no timespec)")
55+
return TimeValue((0, 0))
56+
#endif
5257
}()
5358
#endif
5459

@@ -133,7 +138,9 @@ extension Test.Clock {
133138
/// can use ``sleep(for:tolerance:)`` or ``sleep(until:tolerance:)`` instead.
134139
@available(_clockAPI, *)
135140
static func sleep(for duration: Duration) async throws {
136-
#if SWT_NO_UNSTRUCTURED_TASKS
141+
#if !SWT_NO_UNSTRUCTURED_TASKS
142+
return try await SuspendingClock().sleep(for: duration)
143+
#elseif !SWT_NO_TIMESPEC
137144
let timeValue = TimeValue(duration)
138145
var ts = timespec(timeValue)
139146
var tsRemaining = ts
@@ -142,7 +149,7 @@ extension Test.Clock {
142149
ts = tsRemaining
143150
}
144151
#else
145-
return try await SuspendingClock().sleep(for: duration)
152+
#warning("Platform-specific implementation missing: task sleep unavailable")
146153
#endif
147154
}
148155
}

Sources/Testing/Events/TimeValue.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ struct TimeValue: Sendable {
4141
(seconds, attoseconds) = components
4242
}
4343

44+
#if !SWT_NO_TIMESPEC
4445
init(_ timespec: timespec) {
4546
self.init((Int64(timespec.tv_sec), Int64(timespec.tv_nsec) * 1_000_000_000))
4647
}
48+
#endif
4749

4850
@available(_clockAPI, *)
4951
init(_ duration: Duration) {
@@ -112,11 +114,13 @@ extension SuspendingClock.Instant {
112114
}
113115
}
114116

117+
#if !SWT_NO_TIMESPEC
115118
extension timespec {
116119
init(_ timeValue: TimeValue) {
117120
self.init(tv_sec: .init(timeValue.seconds), tv_nsec: .init(timeValue.attoseconds / 1_000_000_000))
118121
}
119122
}
123+
#endif
120124

121125
extension FloatingPoint {
122126
/// Initialize this floating-point value with the total number of seconds

0 commit comments

Comments
 (0)