Skip to content

Commit 8563ff7

Browse files
Merge pull request #365 from swiftwasm/yt/detect-cont-leak
PackageToJS: Fail tests when continuation leaks are detected
2 parents 3e3deff + f7ca331 commit 8563ff7

File tree

6 files changed

+91
-5
lines changed

6 files changed

+91
-5
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: 32 additions & 5 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,8 +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
121-
{
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 {
122124
try withTemporaryDirectory { tempDir, retain in
123125
let destination = tempDir.appending(path: Self.repoPath.lastPathComponent)
124126
try Self.copyRepository(to: destination)
@@ -139,11 +141,11 @@ extension Trait where Self == ConditionTrait {
139141

140142
try process.run()
141143
process.waitUntilExit()
142-
if process.terminationStatus != 0 {
144+
if !assertTerminationStatus(process.terminationStatus) {
143145
retain = true
144146
}
145147
try #require(
146-
process.terminationStatus == 0,
148+
assertTerminationStatus(process.terminationStatus),
147149
"""
148150
Swift package should build successfully, check \(destination.appending(path: path).path) for details
149151
stdout: \(stdoutPath.path)
@@ -275,4 +277,29 @@ extension Trait where Self == ConditionTrait {
275277
)
276278
}
277279
}
280+
281+
@Test(.requireSwiftSDK)
282+
func continuationLeakInTest_XCTest() throws {
283+
let swiftSDKID = try #require(Self.getSwiftSDKID())
284+
try withPackage(
285+
at: "Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest",
286+
assertTerminationStatus: { $0 != 0 }
287+
) { packageDir, _, runSwift in
288+
try runSwift(["package", "--disable-sandbox", "--swift-sdk", swiftSDKID, "js", "test"], [:])
289+
}
290+
}
291+
292+
#if compiler(>=6.1)
293+
// TODO: Remove triple restriction once swift-testing is shipped in p1-threads SDK
294+
@Test(.requireSwiftSDK(triple: "wasm32-unknown-wasi"))
295+
func continuationLeakInTest_SwiftTesting() throws {
296+
let swiftSDKID = try #require(Self.getSwiftSDKID())
297+
try withPackage(
298+
at: "Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting",
299+
assertTerminationStatus: { $0 != 0 }
300+
) { packageDir, _, runSwift in
301+
try runSwift(["package", "--disable-sandbox", "--swift-sdk", swiftSDKID, "js", "test"], [:])
302+
}
303+
}
304+
#endif
278305
}

0 commit comments

Comments
 (0)