Skip to content

Commit 738b02d

Browse files
Merge pull request #298 from swiftwasm/katei/testing-plugin
Setup unit test infrastructure for PackageToJS
2 parents 9d1b014 + ae2cc40 commit 738b02d

22 files changed

+2324
-329
lines changed

.github/workflows/compatibility.yml

-20
This file was deleted.

.github/workflows/test.yml

+5-18
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ jobs:
3838
id: setup-swiftwasm
3939
with:
4040
target: ${{ matrix.entry.target }}
41-
- name: Configure Swift SDK
42-
run: echo "SWIFT_SDK_ID=${{ steps.setup-swiftwasm.outputs.swift-sdk-id }}" >> $GITHUB_ENV
41+
- name: Configure environment variables
42+
run: |
43+
echo "SWIFT_SDK_ID=${{ steps.setup-swiftwasm.outputs.swift-sdk-id }}" >> $GITHUB_ENV
44+
echo "SWIFT_PATH=$(dirname $(which swiftc))" >> $GITHUB_ENV
4345
- run: make bootstrap
4446
- run: make unittest
4547
# Skip unit tests with uwasi because its proc_exit throws
@@ -49,6 +51,7 @@ jobs:
4951
run: |
5052
make regenerate_swiftpm_resources
5153
git diff --exit-code Sources/JavaScriptKit/Runtime
54+
- run: swift test --package-path ./Plugins/PackageToJS
5255

5356
native-build:
5457
# Check native build to make it easy to develop applications by Xcode
@@ -64,19 +67,3 @@ jobs:
6467
- run: swift build
6568
env:
6669
DEVELOPER_DIR: /Applications/${{ matrix.xcode }}.app/Contents/Developer/
67-
68-
embedded-build:
69-
name: Build for embedded target
70-
runs-on: ubuntu-22.04
71-
strategy:
72-
matrix:
73-
entry:
74-
- os: ubuntu-22.04
75-
toolchain:
76-
download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a-ubuntu22.04.tar.gz
77-
steps:
78-
- uses: actions/checkout@v4
79-
- uses: ./.github/actions/install-swift
80-
with:
81-
download-url: ${{ matrix.entry.toolchain.download-url }}
82-
- run: ./Examples/Embedded/build.sh

Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ SWIFT_BUILD_FLAGS := --swift-sdk $(SWIFT_SDK_ID)
66
.PHONY: bootstrap
77
bootstrap:
88
npm ci
9+
npx playwright install
910

1011
.PHONY: build
1112
build:

Plugins/PackageToJS/Package.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ let package = Package(
77
platforms: [.macOS(.v13)],
88
targets: [
99
.target(name: "PackageToJS"),
10-
.testTarget(name: "PackageToJSTests", dependencies: ["PackageToJS"]),
10+
.testTarget(
11+
name: "PackageToJSTests",
12+
dependencies: ["PackageToJS"],
13+
exclude: ["__Snapshots__"]
14+
),
1115
]
1216
)

Plugins/PackageToJS/Sources/MiniMake.swift

+115-40
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ struct MiniMake {
1414

1515
/// Information about a task enough to capture build
1616
/// graph changes
17-
struct TaskInfo: Codable {
17+
struct TaskInfo: Encodable {
1818
/// Input tasks not yet built
1919
let wants: [TaskKey]
20-
/// Set of files that must be built before this task
21-
let inputs: [String]
22-
/// Output task name
23-
let output: String
20+
/// Set of file paths that must be built before this task
21+
let inputs: [BuildPath]
22+
/// Output file path
23+
let output: BuildPath
2424
/// Attributes of the task
2525
let attributes: [TaskAttribute]
2626
/// Salt for the task, used to differentiate between otherwise identical tasks
@@ -30,41 +30,69 @@ struct MiniMake {
3030
/// A task to build
3131
struct Task {
3232
let info: TaskInfo
33-
/// Input tasks not yet built
33+
/// Input tasks (files and phony tasks) not yet built
3434
let wants: Set<TaskKey>
3535
/// Attributes of the task
3636
let attributes: Set<TaskAttribute>
37-
/// Display name of the task
38-
let displayName: String
3937
/// Key of the task
4038
let key: TaskKey
4139
/// Build operation
42-
let build: (Task) throws -> Void
40+
let build: (_ task: Task, _ scope: VariableScope) throws -> Void
4341
/// Whether the task is done
4442
var isDone: Bool
4543

46-
var inputs: [String] { self.info.inputs }
47-
var output: String { self.info.output }
44+
var inputs: [BuildPath] { self.info.inputs }
45+
var output: BuildPath { self.info.output }
4846
}
4947

5048
/// A task key
51-
struct TaskKey: Codable, Hashable, Comparable, CustomStringConvertible {
49+
struct TaskKey: Encodable, Hashable, Comparable, CustomStringConvertible {
5250
let id: String
5351
var description: String { self.id }
5452

5553
fileprivate init(id: String) {
5654
self.id = id
5755
}
5856

57+
func encode(to encoder: any Encoder) throws {
58+
var container = encoder.singleValueContainer()
59+
try container.encode(self.id)
60+
}
61+
5962
static func < (lhs: TaskKey, rhs: TaskKey) -> Bool { lhs.id < rhs.id }
6063
}
6164

65+
struct VariableScope {
66+
let variables: [String: String]
67+
68+
func resolve(path: BuildPath) -> URL {
69+
var components = [String]()
70+
for component in path.components {
71+
switch component {
72+
case .prefix(let variable):
73+
guard let value = variables[variable] else {
74+
fatalError("Build path variable \"\(variable)\" not defined!")
75+
}
76+
components.append(value)
77+
case .constant(let path):
78+
components.append(path)
79+
}
80+
}
81+
guard let first = components.first else {
82+
fatalError("Build path is empty")
83+
}
84+
var url = URL(fileURLWithPath: first)
85+
for component in components.dropFirst() {
86+
url = url.appending(path: component)
87+
}
88+
return url
89+
}
90+
}
91+
6292
/// All tasks in the build system
6393
private var tasks: [TaskKey: Task]
6494
/// Whether to explain why tasks are built
6595
private var shouldExplain: Bool
66-
/// Current working directory at the time the build started
67-
private let buildCwd: String
6896
/// Prints progress of the build
6997
private var printProgress: ProgressPrinter.PrintProgress
7098

@@ -74,20 +102,16 @@ struct MiniMake {
74102
) {
75103
self.tasks = [:]
76104
self.shouldExplain = explain
77-
self.buildCwd = FileManager.default.currentDirectoryPath
78105
self.printProgress = printProgress
79106
}
80107

81108
/// Adds a task to the build system
82109
mutating func addTask(
83-
inputFiles: [String] = [], inputTasks: [TaskKey] = [], output: String,
110+
inputFiles: [BuildPath] = [], inputTasks: [TaskKey] = [], output: BuildPath,
84111
attributes: [TaskAttribute] = [], salt: (any Encodable)? = nil,
85-
build: @escaping (Task) throws -> Void
112+
build: @escaping (_ task: Task, _ scope: VariableScope) throws -> Void
86113
) -> TaskKey {
87-
let displayName =
88-
output.hasPrefix(self.buildCwd)
89-
? String(output.dropFirst(self.buildCwd.count + 1)) : output
90-
let taskKey = TaskKey(id: output)
114+
let taskKey = TaskKey(id: output.description)
91115
let saltData = try! salt.map {
92116
let encoder = JSONEncoder()
93117
encoder.outputFormatting = .sortedKeys
@@ -99,17 +123,20 @@ struct MiniMake {
99123
)
100124
self.tasks[taskKey] = Task(
101125
info: info, wants: Set(inputTasks), attributes: Set(attributes),
102-
displayName: displayName, key: taskKey, build: build, isDone: false)
126+
key: taskKey, build: build, isDone: false)
103127
return taskKey
104128
}
105129

106130
/// Computes a stable fingerprint of the build graph
107131
///
108132
/// This fingerprint must be stable across builds and must change
109133
/// if the build graph changes in any way.
110-
func computeFingerprint(root: TaskKey) throws -> Data {
134+
func computeFingerprint(root: TaskKey, prettyPrint: Bool = false) throws -> Data {
111135
let encoder = JSONEncoder()
112136
encoder.outputFormatting = .sortedKeys
137+
if prettyPrint {
138+
encoder.outputFormatting.insert(.prettyPrinted)
139+
}
113140
let tasks = self.tasks.sorted { $0.key < $1.key }.map { $0.value.info }
114141
return try encoder.encode(tasks)
115142
}
@@ -126,7 +153,13 @@ struct MiniMake {
126153

127154
/// Prints progress of the build
128155
struct ProgressPrinter {
129-
typealias PrintProgress = (_ subject: Task, _ total: Int, _ built: Int, _ message: String) -> Void
156+
struct Context {
157+
let subject: Task
158+
let total: Int
159+
let built: Int
160+
let scope: VariableScope
161+
}
162+
typealias PrintProgress = (_ context: Context, _ message: String) -> Void
130163

131164
/// Total number of tasks to build
132165
let total: Int
@@ -145,17 +178,17 @@ struct MiniMake {
145178
private static var yellow: String { "\u{001B}[33m" }
146179
private static var reset: String { "\u{001B}[0m" }
147180

148-
mutating func started(_ task: Task) {
149-
self.print(task, "\(Self.green)building\(Self.reset)")
181+
mutating func started(_ task: Task, scope: VariableScope) {
182+
self.print(task, scope, "\(Self.green)building\(Self.reset)")
150183
}
151184

152-
mutating func skipped(_ task: Task) {
153-
self.print(task, "\(Self.yellow)skipped\(Self.reset)")
185+
mutating func skipped(_ task: Task, scope: VariableScope) {
186+
self.print(task, scope, "\(Self.yellow)skipped\(Self.reset)")
154187
}
155188

156-
private mutating func print(_ task: Task, _ message: @autoclosure () -> String) {
189+
private mutating func print(_ task: Task, _ scope: VariableScope, _ message: @autoclosure () -> String) {
157190
guard !task.attributes.contains(.silent) else { return }
158-
self.printProgress(task, self.total, self.built, message())
191+
self.printProgress(Context(subject: task, total: self.total, built: self.built, scope: scope), message())
159192
self.built += 1
160193
}
161194
}
@@ -176,32 +209,32 @@ struct MiniMake {
176209
}
177210

178211
/// Cleans all outputs of all tasks
179-
func cleanEverything() {
212+
func cleanEverything(scope: VariableScope) {
180213
for task in self.tasks.values {
181-
try? FileManager.default.removeItem(atPath: task.output)
214+
try? FileManager.default.removeItem(at: scope.resolve(path: task.output))
182215
}
183216
}
184217

185218
/// Starts building
186-
func build(output: TaskKey) throws {
219+
func build(output: TaskKey, scope: VariableScope) throws {
187220
/// Returns true if any of the task's inputs have a modification date later than the task's output
188221
func shouldBuild(task: Task) -> Bool {
189222
if task.attributes.contains(.phony) {
190223
return true
191224
}
192-
let outputURL = URL(fileURLWithPath: task.output)
193-
if !FileManager.default.fileExists(atPath: task.output) {
225+
let outputURL = scope.resolve(path: task.output)
226+
if !FileManager.default.fileExists(atPath: outputURL.path) {
194227
explain("Task \(task.output) should be built because it doesn't exist")
195228
return true
196229
}
197230
let outputMtime = try? outputURL.resourceValues(forKeys: [.contentModificationDateKey])
198231
.contentModificationDate
199232
return task.inputs.contains { input in
200-
let inputURL = URL(fileURLWithPath: input)
233+
let inputURL = scope.resolve(path: input)
201234
// Ignore directory modification times
202235
var isDirectory: ObjCBool = false
203236
let fileExists = FileManager.default.fileExists(
204-
atPath: input, isDirectory: &isDirectory)
237+
atPath: inputURL.path, isDirectory: &isDirectory)
205238
if fileExists && isDirectory.boolValue {
206239
return false
207240
}
@@ -238,14 +271,56 @@ struct MiniMake {
238271
}
239272

240273
if shouldBuild(task: task) {
241-
progressPrinter.started(task)
242-
try task.build(task)
274+
progressPrinter.started(task, scope: scope)
275+
try task.build(task, scope)
243276
} else {
244-
progressPrinter.skipped(task)
277+
progressPrinter.skipped(task, scope: scope)
245278
}
246279
task.isDone = true
247280
tasks[taskKey] = task
248281
}
249282
try runTask(taskKey: output)
250283
}
251284
}
285+
286+
struct BuildPath: Encodable, Hashable, CustomStringConvertible {
287+
enum Component: Hashable, CustomStringConvertible {
288+
case prefix(variable: String)
289+
case constant(String)
290+
291+
var description: String {
292+
switch self {
293+
case .prefix(let variable): return "$\(variable)"
294+
case .constant(let path): return path
295+
}
296+
}
297+
}
298+
fileprivate let components: [Component]
299+
300+
var description: String { self.components.map(\.description).joined(separator: "/") }
301+
302+
init(phony: String) {
303+
self.components = [.constant(phony)]
304+
}
305+
306+
init(prefix: String, _ tail: String...) {
307+
self.components = [.prefix(variable: prefix)] + tail.map(Component.constant)
308+
}
309+
310+
init(absolute: String) {
311+
self.components = [.constant(absolute)]
312+
}
313+
314+
private init(components: [Component]) {
315+
self.components = components
316+
}
317+
318+
func appending(path: String) -> BuildPath {
319+
return BuildPath(components: self.components + [.constant(path)])
320+
}
321+
322+
func encode(to encoder: any Encoder) throws {
323+
var container = encoder.singleValueContainer()
324+
try container.encode(self.description)
325+
}
326+
}

0 commit comments

Comments
 (0)