@@ -267,6 +267,7 @@ import Testing
267
267
/// Verify that progress information is written to the progress file when specified.
268
268
@Test ( . testHomeMockedToolchain( ) ) func installProgressFile( ) async throws {
269
269
let progressFile = fs. mktemp ( ext: " .json " )
270
+ try await fs. create ( . mode( 0o644 ) , file: progressFile)
270
271
271
272
try await SwiftlyTests . runCommand ( Install . self, [
272
273
" install " , " 5.7.0 " ,
@@ -276,24 +277,120 @@ import Testing
276
277
277
278
#expect( try await fs. exists ( atPath: progressFile) )
278
279
280
+ let decoder = JSONDecoder ( )
279
281
let progressContent = try String ( contentsOfFile: progressFile. string)
280
- let lines = progressContent. components ( separatedBy: . newlines) . filter { !$0. isEmpty }
282
+ let progressInfo = try progressContent. split ( separator: " \n " )
283
+ . filter { !$0. isEmpty }
284
+ . map { line in
285
+ try decoder. decode ( ProgressInfo . self, from: Data ( line. utf8) )
286
+ }
281
287
282
- #expect( !lines . isEmpty, " Progress file should contain progress entries " )
288
+ #expect( !progressInfo . isEmpty, " Progress file should contain progress entries " )
283
289
284
- // Verify that at least one progress entry exists
285
- let hasProgressEntry = lines. contains { line in
286
- line. contains ( " \" step \" " ) && line. contains ( " \" percent \" " ) && line. contains ( " \" timestamp \" " )
290
+ // Verify that at least one step progress entry exists
291
+ let hasStepEntry = progressInfo. contains { info in
292
+ if case . step = info { return true }
293
+ return false
287
294
}
288
- #expect( hasProgressEntry , " Progress file should contain step progress entries " )
295
+ #expect( hasStepEntry , " Progress file should contain step progress entries " )
289
296
290
297
// Verify that a completion entry exists
291
- let hasCompletionEntry = lines. contains { line in
292
- line. contains ( " \" complete \" " ) && line. contains ( " \" success \" " )
298
+ let hasCompletionEntry = progressInfo. contains { info in
299
+ if case . complete = info { return true }
300
+ return false
293
301
}
294
302
#expect( hasCompletionEntry, " Progress file should contain completion entry " )
295
303
296
304
// Clean up
297
305
try FileManager . default. removeItem ( atPath: progressFile. string)
298
306
}
307
+
308
+ #if os(Linux) || os(macOS)
309
+ @Test ( . testHomeMockedToolchain( ) )
310
+ func installProgressFileNamedPipe( ) async throws {
311
+ #if os(Linux) || os(macOS)
312
+ let tempDir = NSTemporaryDirectory ( )
313
+ let pipePath = tempDir + " swiftly_install_progress_pipe "
314
+
315
+ let result = mkfifo ( pipePath, 0o644 )
316
+ guard result == 0 else {
317
+ return // Skip test if mkfifo syscall failed
318
+ }
319
+
320
+ defer {
321
+ try ? FileManager . default. removeItem ( atPath: pipePath)
322
+ }
323
+
324
+ var receivedMessages : [ ProgressInfo ] = [ ]
325
+ let decoder = JSONDecoder ( )
326
+ var installCompleted = false
327
+
328
+ let readerTask = Task {
329
+ guard let fileHandle = FileHandle ( forReadingAtPath: pipePath) else { return }
330
+ defer { fileHandle. closeFile ( ) }
331
+
332
+ var buffer = Data ( )
333
+
334
+ while !installCompleted {
335
+ let data = fileHandle. availableData
336
+ if data. isEmpty {
337
+ try await Task . sleep ( nanoseconds: 100_000_000 )
338
+ continue
339
+ }
340
+
341
+ buffer. append ( data)
342
+
343
+ while let newlineRange = buffer. range ( of: " \n " . data ( using: . utf8) !) {
344
+ let lineData = buffer. subdata ( in: 0 ..< newlineRange. lowerBound)
345
+ buffer. removeSubrange ( 0 ..< newlineRange. upperBound)
346
+
347
+ if !lineData. isEmpty {
348
+ if let progress = try ? decoder. decode ( ProgressInfo . self, from: lineData) {
349
+ receivedMessages. append ( progress)
350
+ if case . complete = progress {
351
+ installCompleted = true
352
+ return
353
+ }
354
+ }
355
+ }
356
+ }
357
+ }
358
+ }
359
+
360
+ let installTask = Task {
361
+ try await SwiftlyTests . runCommand ( Install . self, [
362
+ " install " , " 5.7.0 " ,
363
+ " --post-install-file= \( fs. mktemp ( ) ) " ,
364
+ " --progress-file= \( pipePath) " ,
365
+ ] )
366
+ }
367
+
368
+ await withTaskGroup ( of: Void . self) { group in
369
+ group. addTask { try ? await readerTask. value }
370
+ group. addTask { try ? await installTask. value }
371
+ }
372
+
373
+ try await Task . sleep ( nanoseconds: 100_000_000 ) // 100ms
374
+
375
+ #expect( !receivedMessages. isEmpty, " Named pipe should receive progress entries " )
376
+
377
+ let hasCompletionEntry = receivedMessages. contains { info in
378
+ if case . complete = info { return true }
379
+ return false
380
+ }
381
+ #expect( hasCompletionEntry, " Named pipe should receive completion entry " )
382
+
383
+ for message in receivedMessages {
384
+ switch message {
385
+ case let . step( timestamp, percent, text) :
386
+ #expect( timestamp. timeIntervalSince1970 > 0 )
387
+ #expect( percent >= 0 && percent <= 100 )
388
+ #expect( !text. isEmpty)
389
+ case let . complete( success) :
390
+ #expect( success == true )
391
+ }
392
+ }
393
+ #endif
394
+ }
395
+ #endif
299
396
}
0 commit comments