Files
container/Tests/CLITests/Subcommands/System/TestCLISystemDF.swift
T
Raj 7358102c68 Fix system df to count content blobs and deduplicate shared storage (#1555)
- Closes #1526 and #1527.

## Type of Change
- [x] Bug fix
- [ ] New feature  
- [ ] Breaking change
- [ ] Documentation update

## Motivation and Context
This PR fixes `system df` to report actual on-disk allocated bytes
(content blobs + snapshots) instead of summing per-image snapshot sizes.
Orphaned blobs are now included as reclaimable, and storage shared
across tags is no longer double counted. Also consolidates three
identical `calculateDirectorySize` implementations into a shared
`FileManager.allocatedSize(of:)` extension.

## Testing
- [x] Tested locally
- [x] Added/updated tests
- [ ] Added/updated docs
2026-06-01 15:44:43 -07:00

101 lines
4.1 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Foundation
import Testing
@Suite(.serialSuites, .serialized)
final class TestCLISystemDF: CLITest {
private struct DiskUsageStats: Decodable {
let images: ResourceUsage
}
private struct ResourceUsage: Decodable {
let active: Int
let reclaimable: UInt64
let sizeInBytes: UInt64
let total: Int
}
// Issue #1526: reported image size must include content blobs, not just unpacked snapshots.
@Test func imageDiskUsageIsPopulatedAfterPull() throws {
try withCleanImageStore {
try doPull(imageName: alpine)
let stats = try systemDiskUsage()
#expect(stats.images.total >= 1)
#expect(stats.images.active == 0)
#expect(stats.images.sizeInBytes > 0)
#expect(stats.images.reclaimable == stats.images.sizeInBytes)
}
}
// Issue #1527: tagging the same image must not double-count its storage.
@Test func tagsDoNotDoubleCountImageStorage() throws {
try withCleanImageStore {
try doPull(imageName: alpine)
let before = try systemDiskUsage()
try doImageTag(image: alpine, newName: "local/system-df-alpine:tag-one")
try doImageTag(image: alpine, newName: "local/system-df-alpine:tag-two")
let after = try systemDiskUsage()
#expect(after.images.total == before.images.total + 2)
#expect(after.images.sizeInBytes == before.images.sizeInBytes)
#expect(after.images.reclaimable == before.images.reclaimable)
}
}
// Issue #1527: removing one of several tags must not free shared storage.
// Assumes no background GC runs between operations; blobs stay until all references are removed.
@Test func deletingOneOfMultipleTagsPreservesSharedStorage() throws {
try withCleanImageStore {
let baseline = try systemDiskUsage()
try doPull(imageName: alpine)
try doImageTag(image: alpine, newName: "local/system-df-alpine:delete-probe")
let beforeDelete = try systemDiskUsage()
try doRemoveImages(images: ["local/system-df-alpine:delete-probe"])
let afterAliasDelete = try systemDiskUsage()
#expect(afterAliasDelete.images.total == beforeDelete.images.total - 1)
#expect(afterAliasDelete.images.sizeInBytes == beforeDelete.images.sizeInBytes)
#expect(afterAliasDelete.images.reclaimable == beforeDelete.images.reclaimable)
_ = try? run(arguments: ["image", "rm", "--all"])
let afterFullClean = try systemDiskUsage()
#expect(afterFullClean.images.total <= baseline.images.total)
#expect(afterFullClean.images.sizeInBytes <= baseline.images.sizeInBytes)
}
}
private func withCleanImageStore(_ body: () throws -> Void) throws {
_ = try? run(arguments: ["image", "rm", "--all"])
defer {
_ = try? run(arguments: ["image", "rm", "--all"])
}
try body()
}
private func systemDiskUsage() throws -> DiskUsageStats {
let (data, _, error, status) = try run(arguments: ["system", "df", "--format", "json"])
guard status == 0 else {
throw CLIError.executionFailed("system df failed: \(error)")
}
return try JSONDecoder().decode(DiskUsageStats.self, from: data)
}
}