@@ -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,116 @@ 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+ let tempDir = NSTemporaryDirectory ( )
312+ let pipePath = tempDir + " swiftly_install_progress_pipe "
313+
314+ let result = mkfifo ( pipePath, 0o644 )
315+ guard result == 0 else {
316+ return // Skip test if mkfifo syscall failed
317+ }
318+
319+ defer {
320+ try ? FileManager . default. removeItem ( atPath: pipePath)
321+ }
322+
323+ var receivedMessages : [ ProgressInfo ] = [ ]
324+ let decoder = JSONDecoder ( )
325+ var installCompleted = false
326+
327+ let readerTask = Task {
328+ guard let fileHandle = FileHandle ( forReadingAtPath: pipePath) else { return }
329+ defer { fileHandle. closeFile ( ) }
330+
331+ var buffer = Data ( )
332+
333+ while !installCompleted {
334+ let data = fileHandle. availableData
335+ if data. isEmpty {
336+ try await Task . sleep ( nanoseconds: 100_000_000 )
337+ continue
338+ }
339+
340+ buffer. append ( data)
341+
342+ while let newlineRange = buffer. range ( of: " \n " . data ( using: . utf8) !) {
343+ let lineData = buffer. subdata ( in: 0 ..< newlineRange. lowerBound)
344+ buffer. removeSubrange ( 0 ..< newlineRange. upperBound)
345+
346+ if !lineData. isEmpty {
347+ if let progress = try ? decoder. decode ( ProgressInfo . self, from: lineData) {
348+ receivedMessages. append ( progress)
349+ if case . complete = progress {
350+ installCompleted = true
351+ return
352+ }
353+ }
354+ }
355+ }
356+ }
357+ }
358+
359+ let installTask = Task {
360+ try await SwiftlyTests . runCommand ( Install . self, [
361+ " install " , " 5.7.0 " ,
362+ " --post-install-file= \( fs. mktemp ( ) ) " ,
363+ " --progress-file= \( pipePath) " ,
364+ ] )
365+ }
366+
367+ await withTaskGroup ( of: Void . self) { group in
368+ group. addTask { try ? await readerTask. value }
369+ group. addTask { try ? await installTask. value }
370+ }
371+
372+ #expect( !receivedMessages. isEmpty, " Named pipe should receive progress entries " )
373+
374+ let hasCompletionEntry = receivedMessages. contains { info in
375+ if case . complete = info { return true }
376+ return false
377+ }
378+ #expect( hasCompletionEntry, " Named pipe should receive completion entry " )
379+
380+ for message in receivedMessages {
381+ switch message {
382+ case let . step( timestamp, percent, text) :
383+ #expect( timestamp. timeIntervalSince1970 > 0 )
384+ #expect( percent >= 0 && percent <= 100 )
385+ #expect( !text. isEmpty)
386+ case let . complete( success) :
387+ #expect( success == true )
388+ }
389+ }
390+ }
391+ #endif
299392}
0 commit comments