Skip to content

Commit 3633509

Browse files
PackageToJS: Fail tests when continuation leaks are detected
1 parent 42ead29 commit 3633509

File tree

6 files changed

+82
-4
lines changed

6 files changed

+82
-4
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// swift-tools-version: 6.0
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "Check",
6+
dependencies: [.package(name: "JavaScriptKit", path: "../../../../../")],
7+
targets: [
8+
.testTarget(
9+
name: "CheckTests",
10+
dependencies: [
11+
"JavaScriptKit",
12+
.product(name: "JavaScriptEventLoopTestSupport", package: "JavaScriptKit"),
13+
],
14+
path: "Tests"
15+
)
16+
]
17+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Testing
2+
3+
@Test func never() async throws {
4+
let _: Void = await withUnsafeContinuation { _ in }
5+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// swift-tools-version: 6.0
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "Check",
6+
dependencies: [.package(name: "JavaScriptKit", path: "../../../../../")],
7+
targets: [
8+
.testTarget(
9+
name: "CheckTests",
10+
dependencies: [
11+
"JavaScriptKit",
12+
.product(name: "JavaScriptEventLoopTestSupport", package: "JavaScriptKit"),
13+
],
14+
path: "Tests"
15+
)
16+
]
17+
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import XCTest
2+
3+
final class CheckTests: XCTestCase {
4+
func testNever() async throws {
5+
let _: Void = await withUnsafeContinuation { _ in }
6+
}
7+
}

Plugins/PackageToJS/Templates/bin/test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,19 @@ const harnesses = {
6868
options = prelude.setupOptions(options, { isMainThread: true })
6969
}
7070
}
71+
process.on("beforeExit", () => {
72+
// NOTE: "beforeExit" is fired when the process exits gracefully without calling `process.exit`
73+
// Either XCTest or swift-testing should always call `process.exit` through `proc_exit` even
74+
// if the test succeeds. So exiting gracefully means something went wrong (e.g. withUnsafeContinuation is leaked)
75+
// Therefore, we exit with code 1 to indicate that the test execution failed.
76+
console.error(`
77+
78+
=================================================================================================
79+
Detected that the test execution ended without a termination signal from the testing framework.
80+
Hint: This typically means that a continuation leak occurred.
81+
=================================================================================================`)
82+
process.exit(1)
83+
})
7184
await instantiate(options)
7285
} catch (e) {
7386
if (e instanceof WebAssembly.CompileError) {

Plugins/PackageToJS/Tests/ExampleTests.swift

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ extension Trait where Self == ConditionTrait {
8888
atPath: destinationPath.path,
8989
withDestinationPath: linkDestination
9090
)
91-
enumerator.skipDescendants()
9291
continue
9392
}
9493

@@ -117,7 +116,11 @@ extension Trait where Self == ConditionTrait {
117116
typealias RunProcess = (_ executableURL: URL, _ args: [String], _ env: [String: String]) throws -> Void
118117
typealias RunSwift = (_ args: [String], _ env: [String: String]) throws -> Void
119118

120-
func withPackage(at path: String, body: (URL, _ runProcess: RunProcess, _ runSwift: RunSwift) throws -> Void) throws
119+
func withPackage(
120+
at path: String,
121+
assertTerminationStatus: (Int32) -> Bool = { $0 == 0 },
122+
body: @escaping (URL, _ runProcess: RunProcess, _ runSwift: RunSwift) throws -> Void
123+
) throws
121124
{
122125
try withTemporaryDirectory { tempDir, retain in
123126
let destination = tempDir.appending(path: Self.repoPath.lastPathComponent)
@@ -139,11 +142,11 @@ extension Trait where Self == ConditionTrait {
139142

140143
try process.run()
141144
process.waitUntilExit()
142-
if process.terminationStatus != 0 {
145+
if !assertTerminationStatus(process.terminationStatus) {
143146
retain = true
144147
}
145148
try #require(
146-
process.terminationStatus == 0,
149+
assertTerminationStatus(process.terminationStatus),
147150
"""
148151
Swift package should build successfully, check \(destination.appending(path: path).path) for details
149152
stdout: \(stdoutPath.path)
@@ -275,4 +278,20 @@ extension Trait where Self == ConditionTrait {
275278
)
276279
}
277280
}
281+
282+
@Test(.requireSwiftSDK)
283+
func continuationLeakInTest_XCTest() throws {
284+
let swiftSDKID = try #require(Self.getSwiftSDKID())
285+
try withPackage(at: "Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest", assertTerminationStatus: { $0 != 0 }) { packageDir, _, runSwift in
286+
try runSwift(["package", "--disable-sandbox", "--swift-sdk", swiftSDKID, "js", "test"], [:])
287+
}
288+
}
289+
290+
@Test(.requireSwiftSDK)
291+
func continuationLeakInTest_SwiftTesting() throws {
292+
let swiftSDKID = try #require(Self.getSwiftSDKID())
293+
try withPackage(at: "Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting", assertTerminationStatus: { $0 == 0 }) { packageDir, _, runSwift in
294+
try runSwift(["package", "--disable-sandbox", "--swift-sdk", swiftSDKID, "js", "test"], [:])
295+
}
296+
}
278297
}

0 commit comments

Comments
 (0)