@@ -18,6 +18,7 @@ import ContainerClient
1818import Foundation
1919import Testing
2020
21+ @Suite ( . serialized)
2122class TestCLIVolumes : CLITest {
2223
2324 func doVolumeCreate( name: String ) throws {
@@ -323,4 +324,132 @@ class TestCLIVolumes: CLITest {
323324
324325 #expect( status2 == 0 , " second container should succeed " )
325326 }
327+
328+ @Test func testVolumePruneNoVolumes( ) throws {
329+ // Prune with no volumes should succeed with 0 reclaimed
330+ let ( output, error, status) = try run ( arguments: [ " volume " , " prune " ] )
331+ if status != 0 {
332+ throw CLIError . executionFailed ( " volume prune failed: \( error) " )
333+ }
334+
335+ #expect( output. contains ( " 0 B " ) || output. contains ( " No volumes to prune " ) , " should show no space reclaimed or no volumes message " )
336+ }
337+
338+ @Test func testVolumePruneUnusedVolumes( ) throws {
339+ let testName = getTestName ( )
340+ let volumeName1 = " \( testName) _vol1 "
341+ let volumeName2 = " \( testName) _vol2 "
342+
343+ // Clean up any existing resources from previous runs
344+ doVolumeDeleteIfExists ( name: volumeName1)
345+ doVolumeDeleteIfExists ( name: volumeName2)
346+
347+ defer {
348+ doVolumeDeleteIfExists ( name: volumeName1)
349+ doVolumeDeleteIfExists ( name: volumeName2)
350+ }
351+
352+ try doVolumeCreate ( name: volumeName1)
353+ try doVolumeCreate ( name: volumeName2)
354+ let ( listBefore, _, statusBefore) = try run ( arguments: [ " volume " , " list " , " --quiet " ] )
355+ #expect( statusBefore == 0 )
356+ #expect( listBefore. contains ( volumeName1) )
357+ #expect( listBefore. contains ( volumeName2) )
358+
359+ // Prune should remove both
360+ let ( output, error, status) = try run ( arguments: [ " volume " , " prune " ] )
361+ if status != 0 {
362+ throw CLIError . executionFailed ( " volume prune failed: \( error) " )
363+ }
364+
365+ #expect( output. contains ( volumeName1) || !output. contains ( " No volumes to prune " ) , " should prune volume1 " )
366+ #expect( output. contains ( volumeName2) || !output. contains ( " No volumes to prune " ) , " should prune volume2 " )
367+ #expect( output. contains ( " Reclaimed " ) , " should show reclaimed space " )
368+
369+ // Verify volumes are gone
370+ let ( listAfter, _, statusAfter) = try run ( arguments: [ " volume " , " list " , " --quiet " ] )
371+ #expect( statusAfter == 0 )
372+ #expect( !listAfter. contains ( volumeName1) , " volume1 should be pruned " )
373+ #expect( !listAfter. contains ( volumeName2) , " volume2 should be pruned " )
374+ }
375+
376+ @Test func testVolumePruneSkipsVolumeInUse( ) throws {
377+ let testName = getTestName ( )
378+ let volumeInUse = " \( testName) _inuse "
379+ let volumeUnused = " \( testName) _unused "
380+ let containerName = " \( testName) _c1 "
381+
382+ // Clean up any existing resources from previous runs
383+ doVolumeDeleteIfExists ( name: volumeInUse)
384+ doVolumeDeleteIfExists ( name: volumeUnused)
385+ doRemoveIfExists ( name: containerName, force: true )
386+
387+ defer {
388+ try ? doStop ( name: containerName)
389+ doRemoveIfExists ( name: containerName, force: true )
390+ doVolumeDeleteIfExists ( name: volumeInUse)
391+ doVolumeDeleteIfExists ( name: volumeUnused)
392+ }
393+
394+ try doVolumeCreate ( name: volumeInUse)
395+ try doVolumeCreate ( name: volumeUnused)
396+ try doLongRun ( name: containerName, args: [ " -v " , " \( volumeInUse) :/data " ] )
397+ try waitForContainerRunning ( containerName)
398+
399+ // Prune should only remove the unused volume
400+ let ( _, error, status) = try run ( arguments: [ " volume " , " prune " ] )
401+ if status != 0 {
402+ throw CLIError . executionFailed ( " volume prune failed: \( error) " )
403+ }
404+
405+ // Verify in-use volume still exists
406+ let ( listAfter, _, statusAfter) = try run ( arguments: [ " volume " , " list " , " --quiet " ] )
407+ #expect( statusAfter == 0 )
408+ #expect( listAfter. contains ( volumeInUse) , " volume in use should NOT be pruned " )
409+ #expect( !listAfter. contains ( volumeUnused) , " unused volume should be pruned " )
410+
411+ try doStop ( name: containerName)
412+ doRemoveIfExists ( name: containerName, force: true )
413+ doVolumeDeleteIfExists ( name: volumeInUse)
414+ }
415+
416+ @Test func testVolumePruneSkipsVolumeAttachedToStoppedContainer( ) async throws {
417+ let testName = getTestName ( )
418+ let volumeName = " \( testName) _vol "
419+ let containerName = " \( testName) _c1 "
420+
421+ // Clean up any existing resources from previous runs
422+ doVolumeDeleteIfExists ( name: volumeName)
423+ doRemoveIfExists ( name: containerName, force: true )
424+
425+ defer {
426+ doRemoveIfExists ( name: containerName, force: true )
427+ doVolumeDeleteIfExists ( name: volumeName)
428+ }
429+
430+ try doVolumeCreate ( name: volumeName)
431+ try doCreate ( name: containerName, image: alpine, volumes: [ " \( volumeName) :/data " ] )
432+ try await Task . sleep ( for: . seconds( 1 ) )
433+
434+ // Prune should NOT remove the volume (container exists, even if stopped)
435+ let ( _, error, status) = try run ( arguments: [ " volume " , " prune " ] )
436+ if status != 0 {
437+ throw CLIError . executionFailed ( " volume prune failed: \( error) " )
438+ }
439+
440+ let ( listAfter, _, statusAfter) = try run ( arguments: [ " volume " , " list " , " --quiet " ] )
441+ #expect( statusAfter == 0 )
442+ #expect( listAfter. contains ( volumeName) , " volume attached to stopped container should NOT be pruned " )
443+
444+ doRemoveIfExists ( name: containerName, force: true )
445+ let ( _, error2, status2) = try run ( arguments: [ " volume " , " prune " ] )
446+ if status2 != 0 {
447+ throw CLIError . executionFailed ( " volume prune failed: \( error2) " )
448+ }
449+
450+ // Verify volume is gone
451+ let ( listFinal, _, statusFinal) = try run ( arguments: [ " volume " , " list " , " --quiet " ] )
452+ #expect( statusFinal == 0 )
453+ #expect( !listFinal. contains ( volumeName) , " volume should be pruned after container is deleted " )
454+ }
326455}
0 commit comments