Skip to content

Add --enable-code-coverage #299

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions Examples/Testing/.gitignore

This file was deleted.

4 changes: 0 additions & 4 deletions Examples/Testing/Package.swift
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "Counter",
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "Counter",
targets: ["Counter"]),
],
dependencies: [.package(name: "JavaScriptKit", path: "../../")],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "Counter",
dependencies: [
Expand Down
33 changes: 33 additions & 0 deletions Examples/Testing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Testing example

This example demonstrates how to write and run tests for Swift code compiled to WebAssembly using JavaScriptKit.

## Running Tests

To run the tests, use the following command:

```console
swift package --disable-sandbox --swift-sdk wasm32-unknown-wasi js test
```

## Code Coverage

To generate and view code coverage reports:

1. Run tests with code coverage enabled:

```console
swift package --disable-sandbox --swift-sdk wasm32-unknown-wasi js test --enable-code-coverage
```

2. Generate HTML coverage report:

```console
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
```

3. Serve and view the coverage report:

```console
npx serve .build/coverage/html
```
10 changes: 5 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ unittest:
@echo Running unit tests
swift package --swift-sdk "$(SWIFT_SDK_ID)" \
--disable-sandbox \
-Xlinker --stack-first \
-Xlinker --global-base=524288 \
-Xlinker -z \
-Xlinker stack-size=524288 \
js test --prelude ./Tests/prelude.mjs
-Xlinker --stack-first \
-Xlinker --global-base=524288 \
-Xlinker -z \
-Xlinker stack-size=524288 \
js test --prelude ./Tests/prelude.mjs

.PHONY: benchmark_setup
benchmark_setup:
Expand Down
116 changes: 101 additions & 15 deletions Plugins/PackageToJS/Sources/PackageToJS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ struct PackageToJS {
var outputPath: String?
/// Name of the package (default: lowercased Package.swift name)
var packageName: String?
/// Whether to explain the build plan
/// Whether to explain the build plan (default: false)
var explain: Bool = false
/// Whether to use CDN for dependency packages
/// Whether to use CDN for dependency packages (default: false)
var useCDN: Bool = false
/// Whether to enable code coverage collection (default: false)
var enableCodeCoverage: Bool = false
}

struct BuildOptions {
Expand Down Expand Up @@ -51,7 +53,69 @@ struct PackageToJS {
return (buildConfiguration, triple)
}

static func runTest(testRunner: URL, currentDirectoryURL: URL, extraArguments: [String]) throws {
static func runTest(testRunner: URL, currentDirectoryURL: URL, outputDir: URL, testOptions: TestOptions) throws {
var testJsArguments: [String] = []
var testLibraryArguments: [String] = []
if testOptions.listTests {
testLibraryArguments += ["--list-tests"]
}
if let prelude = testOptions.prelude {
let preludeURL = URL(fileURLWithPath: prelude, relativeTo: URL(fileURLWithPath: FileManager.default.currentDirectoryPath))
testJsArguments += ["--prelude", preludeURL.path]
}
if let environment = testOptions.environment {
testJsArguments += ["--environment", environment]
}
if testOptions.inspect {
testJsArguments += ["--inspect"]
}

let xctestCoverageFile = outputDir.appending(path: "XCTest.profraw")
do {
var extraArguments = testJsArguments
if testOptions.packageOptions.enableCodeCoverage {
extraArguments += ["--coverage-file", xctestCoverageFile.path]
}
extraArguments += ["--"]
extraArguments += testLibraryArguments
extraArguments += testOptions.filter

try PackageToJS.runSingleTestingLibrary(
testRunner: testRunner, currentDirectoryURL: currentDirectoryURL,
extraArguments: extraArguments
)
}
let swiftTestingCoverageFile = outputDir.appending(path: "SwiftTesting.profraw")
do {
var extraArguments = testJsArguments
if testOptions.packageOptions.enableCodeCoverage {
extraArguments += ["--coverage-file", swiftTestingCoverageFile.path]
}
extraArguments += ["--", "--testing-library", "swift-testing"]
extraArguments += testLibraryArguments
extraArguments += testOptions.filter.flatMap { ["--filter", $0] }

try PackageToJS.runSingleTestingLibrary(
testRunner: testRunner, currentDirectoryURL: currentDirectoryURL,
extraArguments: extraArguments
)
}

if testOptions.packageOptions.enableCodeCoverage {
let profrawFiles = [xctestCoverageFile, swiftTestingCoverageFile].filter { FileManager.default.fileExists(atPath: $0.path) }
do {
try PackageToJS.postProcessCoverageFiles(outputDir: outputDir, profrawFiles: profrawFiles)
} catch {
print("Warning: Failed to merge coverage files: \(error)")
}
}
}

static func runSingleTestingLibrary(
testRunner: URL,
currentDirectoryURL: URL,
extraArguments: [String]
) throws {
let node = try which("node")
let arguments = ["--experimental-wasi-unstable-preview1", testRunner.path] + extraArguments
print("Running test...")
Expand All @@ -70,6 +134,18 @@ struct PackageToJS {
throw PackageToJSError("Test failed with status \(task.terminationStatus)")
}
}

static func postProcessCoverageFiles(outputDir: URL, profrawFiles: [URL]) throws {
let mergedCoverageFile = outputDir.appending(path: "default.profdata")
do {
// Merge the coverage files by llvm-profdata
let arguments = ["merge", "-sparse", "-output", mergedCoverageFile.path] + profrawFiles.map { $0.path }
let llvmProfdata = try which("llvm-profdata")
logCommandExecution(llvmProfdata.path, arguments)
try runCommand(llvmProfdata, arguments)
print("Saved profile data to \(mergedCoverageFile.path)")
}
}
}

struct PackageToJSError: Swift.Error, CustomStringConvertible {
Expand Down Expand Up @@ -140,21 +216,19 @@ final class DefaultPackagingSystem: PackagingSystem {
}
try runCommand(wasmOpt, arguments + ["-o", output, input])
}

private func runCommand(_ command: URL, _ arguments: [String]) throws {
let task = Process()
task.executableURL = command
task.arguments = arguments
task.currentDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
try task.run()
task.waitUntilExit()
guard task.terminationStatus == 0 else {
throw PackageToJSError("Command failed with status \(task.terminationStatus)")
}
}
}

internal func which(_ executable: String) throws -> URL {
do {
// Check overriding environment variable
let envVariable = executable.uppercased().replacingOccurrences(of: "-", with: "_") + "_PATH"
if let path = ProcessInfo.processInfo.environment[envVariable] {
let url = URL(fileURLWithPath: path).appendingPathComponent(executable)
if FileManager.default.isExecutableFile(atPath: url.path) {
return url
}
}
}
let pathSeparator: Character
#if os(Windows)
pathSeparator = ";"
Expand All @@ -171,6 +245,18 @@ internal func which(_ executable: String) throws -> URL {
throw PackageToJSError("Executable \(executable) not found in PATH")
}

private func runCommand(_ command: URL, _ arguments: [String]) throws {
let task = Process()
task.executableURL = command
task.arguments = arguments
task.currentDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
try task.run()
task.waitUntilExit()
guard task.terminationStatus == 0 else {
throw PackageToJSError("Command failed with status \(task.terminationStatus)")
}
}

/// Plans the build for packaging.
struct PackagingPlanner {
/// The options for packaging
Expand Down
71 changes: 34 additions & 37 deletions Plugins/PackageToJS/Sources/PackageToJSPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ struct PackageToJSPlugin: CommandPlugin {
// Build products
let productName = try buildOptions.product ?? deriveDefaultProduct(package: context.package)
let build = try buildWasm(
productName: productName, context: context)
productName: productName, context: context,
enableCodeCoverage: buildOptions.packageOptions.enableCodeCoverage
)
guard build.succeeded else {
reportBuildFailure(build, arguments)
exit(1)
Expand Down Expand Up @@ -145,7 +147,9 @@ struct PackageToJSPlugin: CommandPlugin {

let productName = "\(context.package.displayName)PackageTests"
let build = try buildWasm(
productName: productName, context: context)
productName: productName, context: context,
enableCodeCoverage: testOptions.packageOptions.enableCodeCoverage
)
guard build.succeeded else {
reportBuildFailure(build, arguments)
exit(1)
Expand Down Expand Up @@ -198,36 +202,18 @@ struct PackageToJSPlugin: CommandPlugin {
try make.build(output: rootTask, scope: scope)
print("Packaging tests finished")

let testRunner = scope.resolve(path: binDir.appending(path: "test.js"))
if !testOptions.buildOnly {
var testJsArguments: [String] = []
var testFrameworkArguments: [String] = []
if testOptions.listTests {
testFrameworkArguments += ["--list-tests"]
}
if let prelude = testOptions.prelude {
let preludeURL = URL(fileURLWithPath: prelude, relativeTo: URL(fileURLWithPath: FileManager.default.currentDirectoryPath))
testJsArguments += ["--prelude", preludeURL.path]
}
if let environment = testOptions.environment {
testJsArguments += ["--environment", environment]
}
if testOptions.inspect {
testJsArguments += ["--inspect"]
}
try PackageToJS.runTest(
testRunner: testRunner, currentDirectoryURL: context.pluginWorkDirectoryURL,
extraArguments: testJsArguments + ["--"] + testFrameworkArguments + testOptions.filter
)
let testRunner = scope.resolve(path: binDir.appending(path: "test.js"))
try PackageToJS.runTest(
testRunner: testRunner, currentDirectoryURL: context.pluginWorkDirectoryURL,
extraArguments: testJsArguments + ["--", "--testing-library", "swift-testing"] + testFrameworkArguments
+ testOptions.filter.flatMap { ["--filter", $0] }
testRunner: testRunner,
currentDirectoryURL: context.pluginWorkDirectoryURL,
outputDir: outputDir,
testOptions: testOptions
)
}
}

private func buildWasm(productName: String, context: PluginContext) throws
private func buildWasm(productName: String, context: PluginContext, enableCodeCoverage: Bool) throws
-> PackageManager.BuildResult
{
var parameters = PackageManager.BuildParameters(
Expand All @@ -248,6 +234,12 @@ struct PackageToJSPlugin: CommandPlugin {
parameters.otherLinkerFlags = [
"--export-if-defined=__main_argc_argv"
]

// Enable code coverage options if requested
if enableCodeCoverage {
parameters.otherSwiftcFlags += ["-profile-coverage-mapping", "-profile-generate"]
parameters.otherCFlags += ["-fprofile-instr-generate", "-fcoverage-mapping"]
}
}
return try self.packageManager.build(.product(productName), parameters: parameters)
}
Expand Down Expand Up @@ -292,8 +284,9 @@ extension PackageToJS.PackageOptions {
let packageName = extractor.extractOption(named: "package-name").last
let explain = extractor.extractFlag(named: "explain")
let useCDN = extractor.extractFlag(named: "use-cdn")
let enableCodeCoverage = extractor.extractFlag(named: "enable-code-coverage")
return PackageToJS.PackageOptions(
outputPath: outputPath, packageName: packageName, explain: explain != 0, useCDN: useCDN != 0
outputPath: outputPath, packageName: packageName, explain: explain != 0, useCDN: useCDN != 0, enableCodeCoverage: enableCodeCoverage != 0
)
}
}
Expand All @@ -314,12 +307,14 @@ extension PackageToJS.BuildOptions {
USAGE: swift package --swift-sdk <swift-sdk> [SwiftPM options] PackageToJS [options] [subcommand]

OPTIONS:
--product <product> Product to build (default: executable target if there's only one)
--output <path> Path to the output directory (default: .build/plugins/PackageToJS/outputs/Package)
--package-name <name> Name of the package (default: lowercased Package.swift name)
--explain Whether to explain the build plan
--split-debug Whether to split debug information into a separate .wasm.debug file (default: false)
--no-optimize Whether to disable wasm-opt optimization (default: false)
--product <product> Product to build (default: executable target if there's only one)
--output <path> Path to the output directory (default: .build/plugins/PackageToJS/outputs/Package)
--package-name <name> Name of the package (default: lowercased Package.swift name)
--explain Whether to explain the build plan (default: false)
--split-debug Whether to split debug information into a separate .wasm.debug file (default: false)
--no-optimize Whether to disable wasm-opt optimization (default: false)
--use-cdn Whether to use CDN for dependency packages (default: false)
--enable-code-coverage Whether to enable code coverage collection (default: false)

SUBCOMMANDS:
test Builds and runs tests
Expand Down Expand Up @@ -365,10 +360,12 @@ extension PackageToJS.TestOptions {
USAGE: swift package --swift-sdk <swift-sdk> [SwiftPM options] PackageToJS test [options]

OPTIONS:
--build-only Whether to build only (default: false)
--prelude <path> Path to the prelude script
--environment <name> The environment to use for the tests
--inspect Whether to run tests in the browser with inspector enabled
--build-only Whether to build only (default: false)
--prelude <path> Path to the prelude script
--environment <name> The environment to use for the tests
--inspect Whether to run tests in the browser with inspector enabled
--use-cdn Whether to use CDN for dependency packages (default: false)
--enable-code-coverage Whether to enable code coverage collection (default: false)

EXAMPLES:
$ swift package --swift-sdk wasm32-unknown-wasi plugin js test
Expand Down
Loading