@@ -267,6 +267,7 @@ import Testing
267267 /// Verify that progress information is written to the progress file when specified.
268268 @Test ( . testHomeMockedToolchain( ) ) func installProgressFile( ) async throws {
269269 let progressFile = fs. mktemp ( ext: " .json " )
270+ try await fs. create ( . mode( 0o644 ) , file: progressFile)
270271
271272 try await SwiftlyTests . runCommand ( Install . self, [
272273 " install " , " 5.7.0 " ,
@@ -276,24 +277,120 @@ import Testing
276277
277278 #expect( try await fs. exists ( atPath: progressFile) )
278279
280+ let decoder = JSONDecoder ( )
279281 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+ }
281287
282- #expect( !lines . isEmpty, " Progress file should contain progress entries " )
288+ #expect( !progressInfo . isEmpty, " Progress file should contain progress entries " )
283289
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
287294 }
288- #expect( hasProgressEntry , " Progress file should contain step progress entries " )
295+ #expect( hasStepEntry , " Progress file should contain step progress entries " )
289296
290297 // 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
293301 }
294302 #expect( hasCompletionEntry, " Progress file should contain completion entry " )
295303
296304 // Clean up
297305 try FileManager . default. removeItem ( atPath: progressFile. string)
298306 }
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
299396}
0 commit comments