Skip to content

Commit 4b673a7

Browse files
Add test suite for MiniMake
1 parent a202c21 commit 4b673a7

File tree

5 files changed

+261
-15
lines changed

5 files changed

+261
-15
lines changed

Plugins/PackageToJS/Sources/MiniMake.swift

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,17 @@ struct MiniMake {
6161
private var shouldExplain: Bool
6262
/// Current working directory at the time the build started
6363
private let buildCwd: String
64+
/// Prints progress of the build
65+
private var printProgress: ProgressPrinter.PrintProgress
6466

65-
init(explain: Bool = false) {
67+
init(
68+
explain: Bool = false,
69+
printProgress: @escaping ProgressPrinter.PrintProgress
70+
) {
6671
self.tasks = [:]
6772
self.shouldExplain = explain
6873
self.buildCwd = FileManager.default.currentDirectoryPath
74+
self.printProgress = printProgress
6975
}
7076

7177
/// Adds a task to the build system
@@ -108,14 +114,19 @@ struct MiniMake {
108114

109115
/// Prints progress of the build
110116
struct ProgressPrinter {
117+
typealias PrintProgress = (_ subject: Task, _ total: Int, _ built: Int, _ message: String) -> Void
118+
111119
/// Total number of tasks to build
112120
let total: Int
113121
/// Number of tasks built so far
114122
var built: Int
123+
/// Prints progress of the build
124+
var printProgress: PrintProgress
115125

116-
init(total: Int) {
126+
init(total: Int, printProgress: @escaping PrintProgress) {
117127
self.total = total
118128
self.built = 0
129+
self.printProgress = printProgress
119130
}
120131

121132
private static var green: String { "\u{001B}[32m" }
@@ -132,7 +143,7 @@ struct MiniMake {
132143

133144
private mutating func print(_ task: Task, _ message: @autoclosure () -> String) {
134145
guard !task.attributes.contains(.silent) else { return }
135-
Swift.print("[\(self.built + 1)/\(self.total)] \(task.displayName): \(message())")
146+
self.printProgress(task, self.total, self.built, message())
136147
self.built += 1
137148
}
138149
}
@@ -160,7 +171,7 @@ struct MiniMake {
160171
}
161172

162173
/// Starts building
163-
mutating func build(output: TaskKey) throws {
174+
func build(output: TaskKey) throws {
164175
/// Returns true if any of the task's inputs have a modification date later than the task's output
165176
func shouldBuild(task: Task) -> Bool {
166177
if task.attributes.contains(.phony) {
@@ -196,10 +207,14 @@ struct MiniMake {
196207
}
197208
}
198209
var progressPrinter = ProgressPrinter(
199-
total: self.computeTotalTasksForDisplay(task: self.tasks[output]!))
210+
total: self.computeTotalTasksForDisplay(task: self.tasks[output]!),
211+
printProgress: self.printProgress
212+
)
213+
// Make a copy of the tasks so we can mutate the state
214+
var tasks = self.tasks
200215

201216
func runTask(taskKey: TaskKey) throws {
202-
guard var task = self.tasks[taskKey] else {
217+
guard var task = tasks[taskKey] else {
203218
violated("Task \(taskKey) not found")
204219
return
205220
}
@@ -217,7 +232,7 @@ struct MiniMake {
217232
progressPrinter.skipped(task)
218233
}
219234
task.isDone = true
220-
self.tasks[taskKey] = task
235+
tasks[taskKey] = task
221236
}
222237
try runTask(taskKey: output)
223238
}

Plugins/PackageToJS/Sources/PackageToJS.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,10 @@ struct PackageToJS: CommandPlugin {
205205
else {
206206
throw PackageToJSError("Failed to find JavaScriptKit in dependencies!?")
207207
}
208-
var make = MiniMake(explain: buildOptions.options.explain)
208+
var make = MiniMake(
209+
explain: buildOptions.options.explain,
210+
printProgress: self.printProgress
211+
)
209212
let planner = PackagingPlanner(
210213
options: buildOptions.options, context: context, selfPackage: selfPackage,
211214
outputDir: outputDir)
@@ -272,7 +275,10 @@ struct PackageToJS: CommandPlugin {
272275
else {
273276
throw PackageToJSError("Failed to find JavaScriptKit in dependencies!?")
274277
}
275-
var make = MiniMake(explain: testOptions.options.explain)
278+
var make = MiniMake(
279+
explain: testOptions.options.explain,
280+
printProgress: self.printProgress
281+
)
276282
let planner = PackagingPlanner(
277283
options: testOptions.options, context: context, selfPackage: selfPackage,
278284
outputDir: outputDir)
@@ -362,6 +368,10 @@ struct PackageToJS: CommandPlugin {
362368
}
363369
try? currentBuildFingerprint?.write(to: buildFingerprint)
364370
}
371+
372+
private func printProgress(task: MiniMake.Task, total: Int, built: Int, message: String) {
373+
print("[\(built + 1)/\(total)] \(task.displayName): \(message)")
374+
}
365375
}
366376

367377
/// Derive default product from the package
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import Foundation
2+
import Testing
3+
4+
@testable import PackageToJS
5+
6+
@Suite struct MiniMakeTests {
7+
// Test basic task management functionality
8+
@Test func testBasicTaskManagement() throws {
9+
try withTemporaryDirectory { tempDir in
10+
var make = MiniMake(printProgress: { _, _, _, _ in })
11+
let outputPath = tempDir.appendingPathComponent("output.txt").path
12+
13+
let task = make.addTask(output: outputPath) { task in
14+
try "Hello".write(toFile: task.output, atomically: true, encoding: .utf8)
15+
}
16+
17+
try make.build(output: task)
18+
let content = try String(contentsOfFile: outputPath, encoding: .utf8)
19+
#expect(content == "Hello")
20+
}
21+
}
22+
23+
// Test that task dependencies are handled correctly
24+
@Test func testTaskDependencies() throws {
25+
try withTemporaryDirectory { tempDir in
26+
var make = MiniMake(printProgress: { _, _, _, _ in })
27+
let input = tempDir.appendingPathComponent("input.txt").path
28+
let intermediate = tempDir.appendingPathComponent("intermediate.txt").path
29+
let output = tempDir.appendingPathComponent("output.txt").path
30+
31+
try "Input".write(toFile: input, atomically: true, encoding: .utf8)
32+
33+
let intermediateTask = make.addTask(inputFiles: [input], output: intermediate) { task in
34+
let content = try String(contentsOfFile: task.inputs[0], encoding: .utf8)
35+
try (content + " processed").write(
36+
toFile: task.output, atomically: true, encoding: .utf8)
37+
}
38+
39+
let finalTask = make.addTask(
40+
inputFiles: [intermediate], inputTasks: [intermediateTask], output: output
41+
) { task in
42+
let content = try String(contentsOfFile: task.inputs[0], encoding: .utf8)
43+
try (content + " final").write(
44+
toFile: task.output, atomically: true, encoding: .utf8)
45+
}
46+
47+
try make.build(output: finalTask)
48+
let content = try String(contentsOfFile: output, encoding: .utf8)
49+
#expect(content == "Input processed final")
50+
}
51+
}
52+
53+
// Test that phony tasks are always rebuilt
54+
@Test func testPhonyTask() throws {
55+
try withTemporaryDirectory { tempDir in
56+
var make = MiniMake(printProgress: { _, _, _, _ in })
57+
let outputPath = tempDir.appendingPathComponent("phony.txt").path
58+
try "Hello".write(toFile: outputPath, atomically: true, encoding: .utf8)
59+
var buildCount = 0
60+
61+
let task = make.addTask(output: outputPath, attributes: [.phony]) { task in
62+
buildCount += 1
63+
try String(buildCount).write(toFile: task.output, atomically: true, encoding: .utf8)
64+
}
65+
66+
try make.build(output: task)
67+
try make.build(output: task)
68+
69+
#expect(buildCount == 2, "Phony task should always rebuild")
70+
}
71+
}
72+
73+
// Test that the same build graph produces stable fingerprints
74+
@Test func testFingerprintStability() throws {
75+
var make1 = MiniMake(printProgress: { _, _, _, _ in })
76+
var make2 = MiniMake(printProgress: { _, _, _, _ in })
77+
78+
let output1 = "output1.txt"
79+
80+
let task1 = make1.addTask(output: output1) { _ in }
81+
let task2 = make2.addTask(output: output1) { _ in }
82+
83+
let fingerprint1 = try make1.computeFingerprint(root: task1)
84+
let fingerprint2 = try make2.computeFingerprint(root: task2)
85+
86+
#expect(fingerprint1 == fingerprint2, "Same build graph should have same fingerprint")
87+
}
88+
89+
// Test that rebuilds are controlled by timestamps
90+
@Test func testTimestampBasedRebuild() throws {
91+
try withTemporaryDirectory { tempDir in
92+
var make = MiniMake(printProgress: { _, _, _, _ in })
93+
let input = tempDir.appendingPathComponent("input.txt").path
94+
let output = tempDir.appendingPathComponent("output.txt").path
95+
var buildCount = 0
96+
97+
try "Initial".write(toFile: input, atomically: true, encoding: .utf8)
98+
99+
let task = make.addTask(inputFiles: [input], output: output) { task in
100+
buildCount += 1
101+
let content = try String(contentsOfFile: task.inputs[0], encoding: .utf8)
102+
try content.write(toFile: task.output, atomically: true, encoding: .utf8)
103+
}
104+
105+
// First build
106+
try make.build(output: task)
107+
#expect(buildCount == 1, "First build should occur")
108+
109+
// Second build without changes
110+
try make.build(output: task)
111+
#expect(buildCount == 1, "No rebuild should occur if input is not modified")
112+
113+
// Modify input and rebuild
114+
try "Modified".write(toFile: input, atomically: true, encoding: .utf8)
115+
try make.build(output: task)
116+
#expect(buildCount == 2, "Should rebuild when input is modified")
117+
}
118+
}
119+
120+
// Test that silent tasks execute without output
121+
@Test func testSilentTask() throws {
122+
try withTemporaryDirectory { tempDir in
123+
var messages: [(String, Int, Int, String)] = []
124+
var make = MiniMake(
125+
printProgress: { task, total, built, message in
126+
messages.append((URL(fileURLWithPath: task.output).lastPathComponent, total, built, message))
127+
}
128+
)
129+
let silentOutputPath = tempDir.appendingPathComponent("silent.txt").path
130+
let silentTask = make.addTask(output: silentOutputPath, attributes: [.silent]) { task in
131+
try "Silent".write(toFile: task.output, atomically: true, encoding: .utf8)
132+
}
133+
let finalOutputPath = tempDir.appendingPathComponent("output.txt").path
134+
let task = make.addTask(
135+
inputTasks: [silentTask], output: finalOutputPath
136+
) { task in
137+
try "Hello".write(toFile: task.output, atomically: true, encoding: .utf8)
138+
}
139+
140+
try make.build(output: task)
141+
#expect(FileManager.default.fileExists(atPath: silentOutputPath), "Silent task should still create output file")
142+
#expect(FileManager.default.fileExists(atPath: finalOutputPath), "Final task should create output file")
143+
try #require(messages.count == 1, "Should print progress for the final task")
144+
#expect(messages[0] == ("output.txt", 1, 0, "\u{1B}[32mbuilding\u{1B}[0m"))
145+
}
146+
}
147+
148+
// Test that error cases are handled appropriately
149+
@Test func testErrorWhileBuilding() throws {
150+
struct BuildError: Error {}
151+
try withTemporaryDirectory { tempDir in
152+
var make = MiniMake(printProgress: { _, _, _, _ in })
153+
let output = tempDir.appendingPathComponent("error.txt").path
154+
155+
let task = make.addTask(output: output) { task in
156+
throw BuildError()
157+
}
158+
159+
#expect(throws: BuildError.self) {
160+
try make.build(output: task)
161+
}
162+
}
163+
}
164+
165+
// Test that cleanup functionality works correctly
166+
@Test func testCleanup() throws {
167+
try withTemporaryDirectory { tempDir in
168+
var make = MiniMake(printProgress: { _, _, _, _ in })
169+
let outputs = [
170+
tempDir.appendingPathComponent("clean1.txt").path,
171+
tempDir.appendingPathComponent("clean2.txt").path,
172+
]
173+
174+
// Create tasks and build them
175+
let tasks = outputs.map { output in
176+
make.addTask(output: output) { task in
177+
try "Content".write(toFile: task.output, atomically: true, encoding: .utf8)
178+
}
179+
}
180+
181+
for task in tasks {
182+
try make.build(output: task)
183+
}
184+
185+
// Verify files exist
186+
for output in outputs {
187+
#expect(
188+
FileManager.default.fileExists(atPath: output),
189+
"Output file should exist before cleanup")
190+
}
191+
192+
// Clean everything
193+
make.cleanEverything()
194+
195+
// Verify files are removed
196+
for output in outputs {
197+
#expect(
198+
!FileManager.default.fileExists(atPath: output),
199+
"Output file should not exist after cleanup")
200+
}
201+
}
202+
}
203+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Foundation
2+
3+
struct MakeTemporaryDirectoryError: Error {
4+
let error: CInt
5+
}
6+
7+
internal func withTemporaryDirectory<T>(body: (URL) throws -> T) throws -> T {
8+
// Create a temporary directory using mkdtemp
9+
var template = FileManager.default.temporaryDirectory.appendingPathComponent("PackageToJSTests.XXXXXX").path
10+
return try template.withUTF8 { template in
11+
let copy = UnsafeMutableBufferPointer<CChar>.allocate(capacity: template.count + 1)
12+
template.copyBytes(to: copy)
13+
copy[template.count] = 0
14+
15+
guard let result = mkdtemp(copy.baseAddress!) else {
16+
throw MakeTemporaryDirectoryError(error: errno)
17+
}
18+
let tempDir = URL(fileURLWithPath: String(cString: result))
19+
defer {
20+
try? FileManager.default.removeItem(at: tempDir)
21+
}
22+
return try body(tempDir)
23+
}
24+
}

Plugins/PackageToJS/Tests/TestsTests.swift

Lines changed: 0 additions & 6 deletions
This file was deleted.

0 commit comments

Comments
 (0)