Skip to content

Commit 558770b

Browse files
Add --enable-code-coverage
1 parent 96d73a2 commit 558770b

File tree

11 files changed

+269
-72
lines changed

11 files changed

+269
-72
lines changed

Examples/Testing/.gitignore

-8
This file was deleted.

Examples/Testing/Package.swift

-4
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
11
// swift-tools-version: 6.0
2-
// The swift-tools-version declares the minimum version of Swift required to build this package.
32

43
import PackageDescription
54

65
let package = Package(
76
name: "Counter",
87
products: [
9-
// Products define the executables and libraries a package produces, making them visible to other packages.
108
.library(
119
name: "Counter",
1210
targets: ["Counter"]),
1311
],
1412
dependencies: [.package(name: "JavaScriptKit", path: "../../")],
1513
targets: [
16-
// Targets are the basic building blocks of a package, defining a module or a test suite.
17-
// Targets can depend on other targets in this package and products from dependencies.
1814
.target(
1915
name: "Counter",
2016
dependencies: [

Examples/Testing/README.md

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Testing example
2+
3+
This example demonstrates how to write and run tests for Swift code compiled to WebAssembly using JavaScriptKit.
4+
5+
## Running Tests
6+
7+
To run the tests, use the following command:
8+
9+
```console
10+
swift package --disable-sandbox --swift-sdk wasm32-unknown-wasi js test
11+
```
12+
13+
## Code Coverage
14+
15+
To generate and view code coverage reports:
16+
17+
1. Run tests with code coverage enabled:
18+
19+
```console
20+
swift package --disable-sandbox --swift-sdk wasm32-unknown-wasi js test --enable-code-coverage
21+
```
22+
23+
2. Generate HTML coverage report:
24+
25+
```console
26+
llvm-cov show -instr-profile=.build/plugins/PackageToJS/outputs/PackageTests/default.profdata --format=html .build/plugins/PackageToJS/outputs/PackageTests/main.wasm -o .build/coverage/html Sources
27+
```
28+
29+
3. Serve and view the coverage report:
30+
31+
```console
32+
npx serve .build/coverage/html
33+
```

Makefile

+5-5
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ unittest:
1818
@echo Running unit tests
1919
swift package --swift-sdk "$(SWIFT_SDK_ID)" \
2020
--disable-sandbox \
21-
-Xlinker --stack-first \
22-
-Xlinker --global-base=524288 \
23-
-Xlinker -z \
24-
-Xlinker stack-size=524288 \
25-
js test --prelude ./Tests/prelude.mjs
21+
-Xlinker --stack-first \
22+
-Xlinker --global-base=524288 \
23+
-Xlinker -z \
24+
-Xlinker stack-size=524288 \
25+
js test --prelude ./Tests/prelude.mjs
2626

2727
.PHONY: benchmark_setup
2828
benchmark_setup:

Plugins/PackageToJS/Sources/PackageToJS.swift

+101-15
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ struct PackageToJS {
66
var outputPath: String?
77
/// Name of the package (default: lowercased Package.swift name)
88
var packageName: String?
9-
/// Whether to explain the build plan
9+
/// Whether to explain the build plan (default: false)
1010
var explain: Bool = false
11-
/// Whether to use CDN for dependency packages
11+
/// Whether to use CDN for dependency packages (default: false)
1212
var useCDN: Bool = false
13+
/// Whether to enable code coverage collection (default: false)
14+
var enableCodeCoverage: Bool = false
1315
}
1416

1517
struct BuildOptions {
@@ -51,7 +53,69 @@ struct PackageToJS {
5153
return (buildConfiguration, triple)
5254
}
5355

54-
static func runTest(testRunner: URL, currentDirectoryURL: URL, extraArguments: [String]) throws {
56+
static func runTest(testRunner: URL, currentDirectoryURL: URL, outputDir: URL, testOptions: TestOptions) throws {
57+
var testJsArguments: [String] = []
58+
var testLibraryArguments: [String] = []
59+
if testOptions.listTests {
60+
testLibraryArguments += ["--list-tests"]
61+
}
62+
if let prelude = testOptions.prelude {
63+
let preludeURL = URL(fileURLWithPath: prelude, relativeTo: URL(fileURLWithPath: FileManager.default.currentDirectoryPath))
64+
testJsArguments += ["--prelude", preludeURL.path]
65+
}
66+
if let environment = testOptions.environment {
67+
testJsArguments += ["--environment", environment]
68+
}
69+
if testOptions.inspect {
70+
testJsArguments += ["--inspect"]
71+
}
72+
73+
let xctestCoverageFile = outputDir.appending(path: "XCTest.profraw")
74+
do {
75+
var extraArguments = testJsArguments
76+
if testOptions.packageOptions.enableCodeCoverage {
77+
extraArguments += ["--coverage-file", xctestCoverageFile.path]
78+
}
79+
extraArguments += ["--"]
80+
extraArguments += testLibraryArguments
81+
extraArguments += testOptions.filter
82+
83+
try PackageToJS.runSingleTestingLibrary(
84+
testRunner: testRunner, currentDirectoryURL: currentDirectoryURL,
85+
extraArguments: extraArguments
86+
)
87+
}
88+
let swiftTestingCoverageFile = outputDir.appending(path: "SwiftTesting.profraw")
89+
do {
90+
var extraArguments = testJsArguments
91+
if testOptions.packageOptions.enableCodeCoverage {
92+
extraArguments += ["--coverage-file", swiftTestingCoverageFile.path]
93+
}
94+
extraArguments += ["--", "--testing-library", "swift-testing"]
95+
extraArguments += testLibraryArguments
96+
extraArguments += testOptions.filter.flatMap { ["--filter", $0] }
97+
98+
try PackageToJS.runSingleTestingLibrary(
99+
testRunner: testRunner, currentDirectoryURL: currentDirectoryURL,
100+
extraArguments: extraArguments
101+
)
102+
}
103+
104+
if testOptions.packageOptions.enableCodeCoverage {
105+
let profrawFiles = [xctestCoverageFile, swiftTestingCoverageFile].filter { FileManager.default.fileExists(atPath: $0.path) }
106+
do {
107+
try PackageToJS.postProcessCoverageFiles(outputDir: outputDir, profrawFiles: profrawFiles)
108+
} catch {
109+
print("Warning: Failed to merge coverage files: \(error)")
110+
}
111+
}
112+
}
113+
114+
static func runSingleTestingLibrary(
115+
testRunner: URL,
116+
currentDirectoryURL: URL,
117+
extraArguments: [String]
118+
) throws {
55119
let node = try which("node")
56120
let arguments = ["--experimental-wasi-unstable-preview1", testRunner.path] + extraArguments
57121
print("Running test...")
@@ -70,6 +134,18 @@ struct PackageToJS {
70134
throw PackageToJSError("Test failed with status \(task.terminationStatus)")
71135
}
72136
}
137+
138+
static func postProcessCoverageFiles(outputDir: URL, profrawFiles: [URL]) throws {
139+
let mergedCoverageFile = outputDir.appending(path: "default.profdata")
140+
do {
141+
// Merge the coverage files by llvm-profdata
142+
let arguments = ["merge", "-sparse", "-output", mergedCoverageFile.path] + profrawFiles.map { $0.path }
143+
let llvmProfdata = try which("llvm-profdata")
144+
logCommandExecution(llvmProfdata.path, arguments)
145+
try runCommand(llvmProfdata, arguments)
146+
print("Saved profile data to \(mergedCoverageFile.path)")
147+
}
148+
}
73149
}
74150

75151
struct PackageToJSError: Swift.Error, CustomStringConvertible {
@@ -140,21 +216,19 @@ final class DefaultPackagingSystem: PackagingSystem {
140216
}
141217
try runCommand(wasmOpt, arguments + ["-o", output, input])
142218
}
143-
144-
private func runCommand(_ command: URL, _ arguments: [String]) throws {
145-
let task = Process()
146-
task.executableURL = command
147-
task.arguments = arguments
148-
task.currentDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
149-
try task.run()
150-
task.waitUntilExit()
151-
guard task.terminationStatus == 0 else {
152-
throw PackageToJSError("Command failed with status \(task.terminationStatus)")
153-
}
154-
}
155219
}
156220

157221
internal func which(_ executable: String) throws -> URL {
222+
do {
223+
// Check overriding environment variable
224+
let envVariable = executable.uppercased().replacingOccurrences(of: "-", with: "_") + "_PATH"
225+
if let path = ProcessInfo.processInfo.environment[envVariable] {
226+
let url = URL(fileURLWithPath: path).appendingPathComponent(executable)
227+
if FileManager.default.isExecutableFile(atPath: url.path) {
228+
return url
229+
}
230+
}
231+
}
158232
let pathSeparator: Character
159233
#if os(Windows)
160234
pathSeparator = ";"
@@ -171,6 +245,18 @@ internal func which(_ executable: String) throws -> URL {
171245
throw PackageToJSError("Executable \(executable) not found in PATH")
172246
}
173247

248+
private func runCommand(_ command: URL, _ arguments: [String]) throws {
249+
let task = Process()
250+
task.executableURL = command
251+
task.arguments = arguments
252+
task.currentDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
253+
try task.run()
254+
task.waitUntilExit()
255+
guard task.terminationStatus == 0 else {
256+
throw PackageToJSError("Command failed with status \(task.terminationStatus)")
257+
}
258+
}
259+
174260
/// Plans the build for packaging.
175261
struct PackagingPlanner {
176262
/// The options for packaging

Plugins/PackageToJS/Sources/PackageToJSPlugin.swift

+34-37
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ struct PackageToJSPlugin: CommandPlugin {
9393
// Build products
9494
let productName = try buildOptions.product ?? deriveDefaultProduct(package: context.package)
9595
let build = try buildWasm(
96-
productName: productName, context: context)
96+
productName: productName, context: context,
97+
enableCodeCoverage: buildOptions.packageOptions.enableCodeCoverage
98+
)
9799
guard build.succeeded else {
98100
reportBuildFailure(build, arguments)
99101
exit(1)
@@ -145,7 +147,9 @@ struct PackageToJSPlugin: CommandPlugin {
145147

146148
let productName = "\(context.package.displayName)PackageTests"
147149
let build = try buildWasm(
148-
productName: productName, context: context)
150+
productName: productName, context: context,
151+
enableCodeCoverage: testOptions.packageOptions.enableCodeCoverage
152+
)
149153
guard build.succeeded else {
150154
reportBuildFailure(build, arguments)
151155
exit(1)
@@ -198,36 +202,18 @@ struct PackageToJSPlugin: CommandPlugin {
198202
try make.build(output: rootTask, scope: scope)
199203
print("Packaging tests finished")
200204

201-
let testRunner = scope.resolve(path: binDir.appending(path: "test.js"))
202205
if !testOptions.buildOnly {
203-
var testJsArguments: [String] = []
204-
var testFrameworkArguments: [String] = []
205-
if testOptions.listTests {
206-
testFrameworkArguments += ["--list-tests"]
207-
}
208-
if let prelude = testOptions.prelude {
209-
let preludeURL = URL(fileURLWithPath: prelude, relativeTo: URL(fileURLWithPath: FileManager.default.currentDirectoryPath))
210-
testJsArguments += ["--prelude", preludeURL.path]
211-
}
212-
if let environment = testOptions.environment {
213-
testJsArguments += ["--environment", environment]
214-
}
215-
if testOptions.inspect {
216-
testJsArguments += ["--inspect"]
217-
}
218-
try PackageToJS.runTest(
219-
testRunner: testRunner, currentDirectoryURL: context.pluginWorkDirectoryURL,
220-
extraArguments: testJsArguments + ["--"] + testFrameworkArguments + testOptions.filter
221-
)
206+
let testRunner = scope.resolve(path: binDir.appending(path: "test.js"))
222207
try PackageToJS.runTest(
223-
testRunner: testRunner, currentDirectoryURL: context.pluginWorkDirectoryURL,
224-
extraArguments: testJsArguments + ["--", "--testing-library", "swift-testing"] + testFrameworkArguments
225-
+ testOptions.filter.flatMap { ["--filter", $0] }
208+
testRunner: testRunner,
209+
currentDirectoryURL: context.pluginWorkDirectoryURL,
210+
outputDir: outputDir,
211+
testOptions: testOptions
226212
)
227213
}
228214
}
229215

230-
private func buildWasm(productName: String, context: PluginContext) throws
216+
private func buildWasm(productName: String, context: PluginContext, enableCodeCoverage: Bool) throws
231217
-> PackageManager.BuildResult
232218
{
233219
var parameters = PackageManager.BuildParameters(
@@ -248,6 +234,12 @@ struct PackageToJSPlugin: CommandPlugin {
248234
parameters.otherLinkerFlags = [
249235
"--export-if-defined=__main_argc_argv"
250236
]
237+
238+
// Enable code coverage options if requested
239+
if enableCodeCoverage {
240+
parameters.otherSwiftcFlags += ["-profile-coverage-mapping", "-profile-generate"]
241+
parameters.otherCFlags += ["-fprofile-instr-generate", "-fcoverage-mapping"]
242+
}
251243
}
252244
return try self.packageManager.build(.product(productName), parameters: parameters)
253245
}
@@ -292,8 +284,9 @@ extension PackageToJS.PackageOptions {
292284
let packageName = extractor.extractOption(named: "package-name").last
293285
let explain = extractor.extractFlag(named: "explain")
294286
let useCDN = extractor.extractFlag(named: "use-cdn")
287+
let enableCodeCoverage = extractor.extractFlag(named: "enable-code-coverage")
295288
return PackageToJS.PackageOptions(
296-
outputPath: outputPath, packageName: packageName, explain: explain != 0, useCDN: useCDN != 0
289+
outputPath: outputPath, packageName: packageName, explain: explain != 0, useCDN: useCDN != 0, enableCodeCoverage: enableCodeCoverage != 0
297290
)
298291
}
299292
}
@@ -314,12 +307,14 @@ extension PackageToJS.BuildOptions {
314307
USAGE: swift package --swift-sdk <swift-sdk> [SwiftPM options] PackageToJS [options] [subcommand]
315308
316309
OPTIONS:
317-
--product <product> Product to build (default: executable target if there's only one)
318-
--output <path> Path to the output directory (default: .build/plugins/PackageToJS/outputs/Package)
319-
--package-name <name> Name of the package (default: lowercased Package.swift name)
320-
--explain Whether to explain the build plan
321-
--split-debug Whether to split debug information into a separate .wasm.debug file (default: false)
322-
--no-optimize Whether to disable wasm-opt optimization (default: false)
310+
--product <product> Product to build (default: executable target if there's only one)
311+
--output <path> Path to the output directory (default: .build/plugins/PackageToJS/outputs/Package)
312+
--package-name <name> Name of the package (default: lowercased Package.swift name)
313+
--explain Whether to explain the build plan (default: false)
314+
--split-debug Whether to split debug information into a separate .wasm.debug file (default: false)
315+
--no-optimize Whether to disable wasm-opt optimization (default: false)
316+
--use-cdn Whether to use CDN for dependency packages (default: false)
317+
--enable-code-coverage Whether to enable code coverage collection (default: false)
323318
324319
SUBCOMMANDS:
325320
test Builds and runs tests
@@ -365,10 +360,12 @@ extension PackageToJS.TestOptions {
365360
USAGE: swift package --swift-sdk <swift-sdk> [SwiftPM options] PackageToJS test [options]
366361
367362
OPTIONS:
368-
--build-only Whether to build only (default: false)
369-
--prelude <path> Path to the prelude script
370-
--environment <name> The environment to use for the tests
371-
--inspect Whether to run tests in the browser with inspector enabled
363+
--build-only Whether to build only (default: false)
364+
--prelude <path> Path to the prelude script
365+
--environment <name> The environment to use for the tests
366+
--inspect Whether to run tests in the browser with inspector enabled
367+
--use-cdn Whether to use CDN for dependency packages (default: false)
368+
--enable-code-coverage Whether to enable code coverage collection (default: false)
372369
373370
EXAMPLES:
374371
$ swift package --swift-sdk wasm32-unknown-wasi plugin js test

0 commit comments

Comments
 (0)