Skip to content

Commit e5c37a9

Browse files
Add test suite to ensure examples build correctly
1 parent eae4d11 commit e5c37a9

File tree

6 files changed

+201
-19
lines changed

6 files changed

+201
-19
lines changed

Diff for: Plugins/PackageToJS/Sources/PackageToJS.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ final class DefaultPackagingSystem: PackagingSystem {
138138
}
139139
}
140140

141-
private func which(_ executable: String) throws -> URL {
141+
internal func which(_ executable: String) throws -> URL {
142142
let pathSeparator: Character
143143
#if os(Windows)
144144
pathSeparator = ";"

Diff for: Plugins/PackageToJS/Sources/PackageToJSPlugin.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,8 @@ struct PackageToJSPlugin: CommandPlugin {
160160
// not worth the overhead)
161161
var productArtifact: URL?
162162
for fileExtension in ["wasm", "xctest"] {
163-
let path = ".build/debug/\(productName).\(fileExtension)"
163+
let packageDir = context.package.directoryURL
164+
let path = packageDir.appending(path: ".build/debug/\(productName).\(fileExtension)").path
164165
if FileManager.default.fileExists(atPath: path) {
165166
productArtifact = URL(fileURLWithPath: path)
166167
break

Diff for: Plugins/PackageToJS/Tests/ExampleProjectTests.swift

-6
This file was deleted.

Diff for: Plugins/PackageToJS/Tests/ExampleTests.swift

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import Foundation
2+
import Testing
3+
4+
@testable import PackageToJS
5+
6+
extension Trait where Self == ConditionTrait {
7+
static var requireSwiftSDK: ConditionTrait {
8+
.enabled(
9+
if: ProcessInfo.processInfo.environment["SWIFT_SDK_ID"] != nil
10+
&& ProcessInfo.processInfo.environment["SWIFT_PATH"] != nil,
11+
"Requires SWIFT_SDK_ID and SWIFT_PATH environment variables"
12+
)
13+
}
14+
15+
static func requireSwiftSDK(triple: String) -> ConditionTrait {
16+
.enabled(
17+
if: ProcessInfo.processInfo.environment["SWIFT_SDK_ID"] != nil
18+
&& ProcessInfo.processInfo.environment["SWIFT_PATH"] != nil
19+
&& ProcessInfo.processInfo.environment["SWIFT_SDK_ID"]!.hasSuffix(triple),
20+
"Requires SWIFT_SDK_ID and SWIFT_PATH environment variables"
21+
)
22+
}
23+
24+
static var requireEmbeddedSwift: ConditionTrait {
25+
// Check if $SWIFT_PATH/../lib/swift/embedded/wasm32-unknown-none-wasm/ exists
26+
return .enabled(
27+
if: {
28+
guard let swiftPath = ProcessInfo.processInfo.environment["SWIFT_PATH"] else {
29+
return false
30+
}
31+
let embeddedPath = URL(fileURLWithPath: swiftPath).deletingLastPathComponent()
32+
.appending(path: "lib/swift/embedded/wasm32-unknown-none-wasm")
33+
return FileManager.default.fileExists(atPath: embeddedPath.path)
34+
}(),
35+
"Requires embedded Swift SDK under $SWIFT_PATH/../lib/swift/embedded"
36+
)
37+
}
38+
}
39+
40+
@Suite struct ExampleTests {
41+
static func getSwiftSDKID() -> String? {
42+
ProcessInfo.processInfo.environment["SWIFT_SDK_ID"]
43+
}
44+
45+
static let repoPath = URL(fileURLWithPath: #filePath)
46+
.deletingLastPathComponent()
47+
.deletingLastPathComponent()
48+
.deletingLastPathComponent()
49+
.deletingLastPathComponent()
50+
51+
static func copyRepository(to destination: URL) throws {
52+
try FileManager.default.createDirectory(
53+
atPath: destination.path, withIntermediateDirectories: true, attributes: nil)
54+
let ignore = [
55+
".git",
56+
".vscode",
57+
".build",
58+
"node_modules",
59+
]
60+
61+
let enumerator = FileManager.default.enumerator(atPath: repoPath.path)!
62+
while let file = enumerator.nextObject() as? String {
63+
let sourcePath = repoPath.appending(path: file)
64+
let destinationPath = destination.appending(path: file)
65+
if ignore.contains(where: { file.hasSuffix($0) }) {
66+
enumerator.skipDescendants()
67+
continue
68+
}
69+
// Skip directories
70+
var isDirectory: ObjCBool = false
71+
if FileManager.default.fileExists(atPath: sourcePath.path, isDirectory: &isDirectory) {
72+
if isDirectory.boolValue {
73+
continue
74+
}
75+
}
76+
77+
do {
78+
try FileManager.default.createDirectory(
79+
at: destinationPath.deletingLastPathComponent(),
80+
withIntermediateDirectories: true, attributes: nil)
81+
try FileManager.default.copyItem(at: sourcePath, to: destinationPath)
82+
} catch {
83+
print("Failed to copy \(sourcePath) to \(destinationPath): \(error)")
84+
throw error
85+
}
86+
}
87+
}
88+
89+
typealias RunSwift = (_ args: [String], _ env: [String: String]) throws -> Void
90+
91+
func withPackage(at path: String, body: (URL, _ runSwift: RunSwift) throws -> Void) throws {
92+
try withTemporaryDirectory { tempDir, retain in
93+
let destination = tempDir.appending(path: Self.repoPath.lastPathComponent)
94+
try Self.copyRepository(to: destination)
95+
try body(destination.appending(path: path)) { args, env in
96+
let process = Process()
97+
process.executableURL = URL(
98+
fileURLWithPath: "swift",
99+
relativeTo: URL(
100+
fileURLWithPath: ProcessInfo.processInfo.environment["SWIFT_PATH"]!))
101+
process.arguments = args
102+
process.currentDirectoryURL = destination.appending(path: path)
103+
process.environment = ProcessInfo.processInfo.environment.merging(env) { _, new in
104+
new
105+
}
106+
let stdoutPath = tempDir.appending(path: "stdout.txt")
107+
let stderrPath = tempDir.appending(path: "stderr.txt")
108+
_ = FileManager.default.createFile(atPath: stdoutPath.path, contents: nil)
109+
_ = FileManager.default.createFile(atPath: stderrPath.path, contents: nil)
110+
process.standardOutput = try FileHandle(forWritingTo: stdoutPath)
111+
process.standardError = try FileHandle(forWritingTo: stderrPath)
112+
113+
try process.run()
114+
process.waitUntilExit()
115+
if process.terminationStatus != 0 {
116+
retain = true
117+
}
118+
try #require(
119+
process.terminationStatus == 0,
120+
"""
121+
Swift package should build successfully, check \(destination.appending(path: path).path) for details
122+
stdout: \(stdoutPath.path)
123+
stderr: \(stderrPath.path)
124+
125+
\((try? String(contentsOf: stdoutPath, encoding: .utf8)) ?? "<<stdout is empty>>")
126+
\((try? String(contentsOf: stderrPath, encoding: .utf8)) ?? "<<stderr is empty>>")
127+
"""
128+
)
129+
}
130+
}
131+
}
132+
133+
@Test(.requireSwiftSDK)
134+
func basic() throws {
135+
let swiftSDKID = try #require(Self.getSwiftSDKID())
136+
try withPackage(at: "Examples/Basic") { packageDir, runSwift in
137+
try runSwift(["package", "--swift-sdk", swiftSDKID, "js"], [:])
138+
}
139+
}
140+
141+
@Test(.requireSwiftSDK)
142+
func testing() throws {
143+
let swiftSDKID = try #require(Self.getSwiftSDKID())
144+
try withPackage(at: "Examples/Testing") { packageDir, runSwift in
145+
try runSwift(["package", "--swift-sdk", swiftSDKID, "js", "test"], [:])
146+
try runSwift(["package", "--swift-sdk", swiftSDKID, "js", "test", "--environment", "browser"], [:])
147+
}
148+
}
149+
150+
@Test(.requireSwiftSDK(triple: "wasm32-unknown-wasip1-threads"))
151+
func multithreading() throws {
152+
let swiftSDKID = try #require(Self.getSwiftSDKID())
153+
try withPackage(at: "Examples/Multithreading") { packageDir, runSwift in
154+
try runSwift(["package", "--swift-sdk", swiftSDKID, "js"], [:])
155+
}
156+
}
157+
158+
@Test(.requireSwiftSDK(triple: "wasm32-unknown-wasip1-threads"))
159+
func offscreenCanvas() throws {
160+
let swiftSDKID = try #require(Self.getSwiftSDKID())
161+
try withPackage(at: "Examples/OffscrenCanvas") { packageDir, runSwift in
162+
try runSwift(["package", "--swift-sdk", swiftSDKID, "js"], [:])
163+
}
164+
}
165+
166+
@Test(.requireSwiftSDK(triple: "wasm32-unknown-wasip1-threads"))
167+
func actorOnWebWorker() throws {
168+
let swiftSDKID = try #require(Self.getSwiftSDKID())
169+
try withPackage(at: "Examples/ActorOnWebWorker") { packageDir, runSwift in
170+
try runSwift(["package", "--swift-sdk", swiftSDKID, "js"], [:])
171+
}
172+
}
173+
174+
@Test(.requireEmbeddedSwift) func embedded() throws {
175+
try withPackage(at: "Examples/Embedded") { packageDir, runSwift in
176+
try runSwift(
177+
["package", "--triple", "wasm32-unknown-none-wasm", "-c", "release", "js"],
178+
[
179+
"JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM": "true"
180+
]
181+
)
182+
}
183+
}
184+
}

Diff for: Plugins/PackageToJS/Tests/MiniMakeTests.swift

+7-7
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Testing
66
@Suite struct MiniMakeTests {
77
// Test basic task management functionality
88
@Test func basicTaskManagement() throws {
9-
try withTemporaryDirectory { tempDir in
9+
try withTemporaryDirectory { tempDir, _ in
1010
var make = MiniMake(printProgress: { _, _ in })
1111
let outDir = BuildPath(prefix: "OUTPUT")
1212

@@ -25,7 +25,7 @@ import Testing
2525

2626
// Test that task dependencies are handled correctly
2727
@Test func taskDependencies() throws {
28-
try withTemporaryDirectory { tempDir in
28+
try withTemporaryDirectory { tempDir, _ in
2929
var make = MiniMake(printProgress: { _, _ in })
3030
let prefix = BuildPath(prefix: "PREFIX")
3131
let scope = MiniMake.VariableScope(variables: [
@@ -59,7 +59,7 @@ import Testing
5959

6060
// Test that phony tasks are always rebuilt
6161
@Test func phonyTask() throws {
62-
try withTemporaryDirectory { tempDir in
62+
try withTemporaryDirectory { tempDir, _ in
6363
var make = MiniMake(printProgress: { _, _ in })
6464
let phonyName = "phony.txt"
6565
let outputPath = BuildPath(prefix: "OUTPUT").appending(path: phonyName)
@@ -99,7 +99,7 @@ import Testing
9999

100100
// Test that rebuilds are controlled by timestamps
101101
@Test func timestampBasedRebuild() throws {
102-
try withTemporaryDirectory { tempDir in
102+
try withTemporaryDirectory { tempDir, _ in
103103
var make = MiniMake(printProgress: { _, _ in })
104104
let prefix = BuildPath(prefix: "PREFIX")
105105
let scope = MiniMake.VariableScope(variables: [
@@ -134,7 +134,7 @@ import Testing
134134

135135
// Test that silent tasks execute without output
136136
@Test func silentTask() throws {
137-
try withTemporaryDirectory { tempDir in
137+
try withTemporaryDirectory { tempDir, _ in
138138
var messages: [(String, Int, Int, String)] = []
139139
var make = MiniMake(
140140
printProgress: { ctx, message in
@@ -167,7 +167,7 @@ import Testing
167167
// Test that error cases are handled appropriately
168168
@Test func errorWhileBuilding() throws {
169169
struct BuildError: Error {}
170-
try withTemporaryDirectory { tempDir in
170+
try withTemporaryDirectory { tempDir, _ in
171171
var make = MiniMake(printProgress: { _, _ in })
172172
let prefix = BuildPath(prefix: "PREFIX")
173173
let scope = MiniMake.VariableScope(variables: [
@@ -187,7 +187,7 @@ import Testing
187187

188188
// Test that cleanup functionality works correctly
189189
@Test func cleanup() throws {
190-
try withTemporaryDirectory { tempDir in
190+
try withTemporaryDirectory { tempDir, _ in
191191
var make = MiniMake(printProgress: { _, _ in })
192192
let prefix = BuildPath(prefix: "PREFIX")
193193
let scope = MiniMake.VariableScope(variables: [

Diff for: Plugins/PackageToJS/Tests/TemporaryDirectory.swift

+7-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ struct MakeTemporaryDirectoryError: Error {
44
let error: CInt
55
}
66

7-
internal func withTemporaryDirectory<T>(body: (URL) throws -> T) throws -> T {
7+
internal func withTemporaryDirectory<T>(body: (URL, _ retain: inout Bool) throws -> T) throws -> T {
88
// Create a temporary directory using mkdtemp
99
var template = FileManager.default.temporaryDirectory.appendingPathComponent("PackageToJSTests.XXXXXX").path
1010
return try template.withUTF8 { template in
@@ -16,9 +16,12 @@ internal func withTemporaryDirectory<T>(body: (URL) throws -> T) throws -> T {
1616
throw MakeTemporaryDirectoryError(error: errno)
1717
}
1818
let tempDir = URL(fileURLWithPath: String(cString: result))
19+
var retain = false
1920
defer {
20-
try? FileManager.default.removeItem(at: tempDir)
21+
if !retain {
22+
try? FileManager.default.removeItem(at: tempDir)
23+
}
2124
}
22-
return try body(tempDir)
25+
return try body(tempDir, &retain)
2326
}
24-
}
27+
}

0 commit comments

Comments
 (0)