358 lines
12 KiB
Swift
358 lines
12 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025-2026 Apple Inc. and the Containerization 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 ContainerizationOCI
|
|
import Foundation
|
|
import Testing
|
|
|
|
@testable import Containerization
|
|
|
|
struct MountTests {
|
|
|
|
@Test func mountShareCreatesVirtiofsMount() {
|
|
let mount = Mount.share(
|
|
source: "/host/shared",
|
|
destination: "/guest/shared",
|
|
options: ["rw", "noatime"],
|
|
runtimeOptions: ["tag=shared"]
|
|
)
|
|
|
|
#expect(mount.type == "virtiofs")
|
|
#expect(mount.source == "/host/shared")
|
|
#expect(mount.destination == "/guest/shared")
|
|
#expect(mount.options == ["rw", "noatime"])
|
|
|
|
if case .virtiofs(let opts) = mount.runtimeOptions {
|
|
#expect(opts == ["tag=shared"])
|
|
} else {
|
|
#expect(Bool(false), "Expected virtiofs runtime options")
|
|
}
|
|
}
|
|
|
|
@Test func sortMountsByDestinationDepthPreventsParentShadowing() {
|
|
let mounts: [ContainerizationOCI.Mount] = [
|
|
.init(destination: "/tmp/foo/bar"),
|
|
.init(destination: "/tmp"),
|
|
.init(destination: "/var/log/app"),
|
|
.init(destination: "/var"),
|
|
]
|
|
|
|
let sorted = sortMountsByDestinationDepth(mounts)
|
|
|
|
#expect(
|
|
sorted.map(\.destination) == [
|
|
"/tmp",
|
|
"/var",
|
|
"/tmp/foo/bar",
|
|
"/var/log/app",
|
|
])
|
|
}
|
|
|
|
@Test func sortMountsByDestinationDepthPreservesOrderForEqualDepth() {
|
|
let mounts: [ContainerizationOCI.Mount] = [
|
|
.init(destination: "/b"),
|
|
.init(destination: "/a"),
|
|
.init(destination: "/c"),
|
|
]
|
|
|
|
let sorted = sortMountsByDestinationDepth(mounts)
|
|
|
|
// All same depth, order should be preserved (stable sort).
|
|
#expect(sorted.map(\.destination) == ["/b", "/a", "/c"])
|
|
}
|
|
|
|
@Test func sortMountsByDestinationDepthHandlesTrailingAndDoubleSlashes() {
|
|
let mounts: [ContainerizationOCI.Mount] = [
|
|
.init(destination: "/a//b/c"),
|
|
.init(destination: "/a/"),
|
|
]
|
|
|
|
let sorted = cleanAndSortMounts(mounts)
|
|
|
|
// Paths are cleaned: "/a/" -> "/a", "/a//b/c" -> "/a/b/c"
|
|
#expect(sorted.map(\.destination) == ["/a", "/a/b/c"])
|
|
}
|
|
|
|
@Test func sortMountsByDestinationDepthCleansDotAndDotDot() {
|
|
let mounts: [ContainerizationOCI.Mount] = [
|
|
.init(destination: "/tmp/../foo"),
|
|
.init(destination: "/tmp/./bar/baz"),
|
|
.init(destination: "/"),
|
|
]
|
|
|
|
let sorted = cleanAndSortMounts(mounts)
|
|
|
|
// "/tmp/../foo" -> "/foo", "/tmp/./bar/baz" -> "/tmp/bar/baz"
|
|
#expect(sorted.map(\.destination) == ["/", "/foo", "/tmp/bar/baz"])
|
|
}
|
|
}
|
|
|
|
@Suite("AttachedFilesystem runtimeOptions dispatch")
|
|
struct AttachedFilesystemTests {
|
|
|
|
@Test func virtioblkMountAllocatesBlockDevice() throws {
|
|
let mount = Mount.block(
|
|
format: "ext4",
|
|
source: "/path/to/disk.img",
|
|
destination: "/data"
|
|
)
|
|
let allocator = Character.blockDeviceTagAllocator()
|
|
let attached = try AttachedFilesystem(mount: mount, allocator: allocator)
|
|
|
|
#expect(attached.source == "/dev/vda")
|
|
#expect(attached.type == "ext4")
|
|
#expect(attached.destination == "/data")
|
|
}
|
|
|
|
@Test func nbdMountAllocatesBlockDevice() throws {
|
|
let mount = Mount.block(
|
|
format: "ext4",
|
|
source: "nbd://localhost:10809",
|
|
destination: "/data"
|
|
)
|
|
let allocator = Character.blockDeviceTagAllocator()
|
|
let attached = try AttachedFilesystem(mount: mount, allocator: allocator)
|
|
|
|
#expect(attached.source == "/dev/vda")
|
|
#expect(attached.type == "ext4")
|
|
#expect(attached.destination == "/data")
|
|
}
|
|
|
|
@Test func nbdMountWithNonExt4FormatAllocatesBlockDevice() throws {
|
|
let mount = Mount.block(
|
|
format: "xfs",
|
|
source: "nbd://localhost:10809",
|
|
destination: "/data"
|
|
)
|
|
let allocator = Character.blockDeviceTagAllocator()
|
|
let attached = try AttachedFilesystem(mount: mount, allocator: allocator)
|
|
|
|
#expect(attached.source == "/dev/vda")
|
|
#expect(attached.type == "xfs")
|
|
}
|
|
|
|
@Test func multipleBlockDevicesAllocateSequentially() throws {
|
|
let allocator = Character.blockDeviceTagAllocator()
|
|
|
|
let m1 = Mount.block(format: "ext4", source: "/disk1.img", destination: "/a")
|
|
let m2 = Mount.block(format: "ext4", source: "nbd://host:10809", destination: "/b")
|
|
let m3 = Mount.block(format: "ext4", source: "/disk2.img", destination: "/c")
|
|
|
|
let a1 = try AttachedFilesystem(mount: m1, allocator: allocator)
|
|
let a2 = try AttachedFilesystem(mount: m2, allocator: allocator)
|
|
let a3 = try AttachedFilesystem(mount: m3, allocator: allocator)
|
|
|
|
#expect(a1.source == "/dev/vda")
|
|
#expect(a2.source == "/dev/vdb")
|
|
#expect(a3.source == "/dev/vdc")
|
|
}
|
|
|
|
@Test func anyMountUsesSourceDirectly() throws {
|
|
let mount = Mount.any(
|
|
type: "tmpfs",
|
|
source: "tmpfs",
|
|
destination: "/tmp"
|
|
)
|
|
let allocator = Character.blockDeviceTagAllocator()
|
|
let attached = try AttachedFilesystem(mount: mount, allocator: allocator)
|
|
|
|
#expect(attached.source == "tmpfs")
|
|
}
|
|
}
|
|
|
|
@Suite("PodVolume and shared mount types")
|
|
struct PodVolumeTests {
|
|
|
|
@Test func podVolumeNBDSourceCreation() {
|
|
let volume = LinuxPod.PodVolume(
|
|
name: "shared-data",
|
|
source: .nbd(url: URL(string: "nbd://localhost:10809")!),
|
|
format: "ext4"
|
|
)
|
|
|
|
#expect(volume.name == "shared-data")
|
|
#expect(volume.format == "ext4")
|
|
if case .nbd(let url, let timeout, let readOnly) = volume.source {
|
|
#expect(url.absoluteString == "nbd://localhost:10809")
|
|
#expect(timeout == nil)
|
|
#expect(readOnly == false)
|
|
} else {
|
|
Issue.record("Expected .nbd source")
|
|
}
|
|
}
|
|
|
|
@Test func podVolumeNBDSourceWithOptions() {
|
|
let volume = LinuxPod.PodVolume(
|
|
name: "data",
|
|
source: .nbd(url: URL(string: "nbd://host:10809")!, timeout: 30, readOnly: true),
|
|
format: "xfs"
|
|
)
|
|
|
|
if case .nbd(_, let timeout, let readOnly) = volume.source {
|
|
#expect(timeout == 30)
|
|
#expect(readOnly == true)
|
|
} else {
|
|
Issue.record("Expected .nbd source")
|
|
}
|
|
}
|
|
|
|
@Test func podVolumeToMountConvertsCorrectly() {
|
|
let volume = LinuxPod.PodVolume(
|
|
name: "my-vol",
|
|
source: .nbd(url: URL(string: "nbd://host:10809/export")!),
|
|
format: "ext4"
|
|
)
|
|
|
|
let mount = volume.toMount()
|
|
|
|
#expect(mount.source == "nbd://host:10809/export")
|
|
#expect(mount.destination == "/run/volumes/my-vol")
|
|
#expect(mount.type == "ext4")
|
|
#expect(mount.isBlock)
|
|
}
|
|
|
|
@Test func podVolumeToMountWithReadOnlySetsOptions() {
|
|
let volume = LinuxPod.PodVolume(
|
|
name: "ro-vol",
|
|
source: .nbd(url: URL(string: "nbd://host:10809")!, readOnly: true),
|
|
format: "ext4"
|
|
)
|
|
|
|
let mount = volume.toMount()
|
|
|
|
#expect(mount.options.contains("ro"))
|
|
#expect(mount.isBlock)
|
|
}
|
|
|
|
@Test func podVolumeToMountWithTimeoutSetsRuntimeOption() {
|
|
let volume = LinuxPod.PodVolume(
|
|
name: "data",
|
|
source: .nbd(url: URL(string: "nbd://host:10809")!, timeout: 60),
|
|
format: "ext4"
|
|
)
|
|
|
|
let mount = volume.toMount()
|
|
|
|
if case .virtioblk(let opts) = mount.runtimeOptions {
|
|
#expect(opts.contains("vzTimeout=60.0"))
|
|
} else {
|
|
Issue.record("Expected virtioblk runtime options")
|
|
}
|
|
}
|
|
|
|
@Test func podVolumeToMountUsesMountType() {
|
|
let volume = LinuxPod.PodVolume(
|
|
name: "data",
|
|
source: .nbd(url: URL(string: "nbd://host:10809")!),
|
|
format: "xfs"
|
|
)
|
|
|
|
let mount = volume.toMount()
|
|
|
|
#expect(mount.type == "xfs")
|
|
}
|
|
|
|
@Test func podVolumeDiskImageSourceCreation() {
|
|
let volume = LinuxPod.PodVolume(
|
|
name: "disk-data",
|
|
source: .diskImage(path: URL(fileURLWithPath: "/tmp/disk.ext4")),
|
|
format: "ext4"
|
|
)
|
|
|
|
#expect(volume.name == "disk-data")
|
|
#expect(volume.format == "ext4")
|
|
if case .diskImage(let path, let readOnly) = volume.source {
|
|
#expect(path.path == "/tmp/disk.ext4")
|
|
#expect(readOnly == false)
|
|
} else {
|
|
Issue.record("Expected .diskImage source")
|
|
}
|
|
}
|
|
|
|
@Test func podVolumeDiskImageToMountConvertsCorrectly() {
|
|
let volume = LinuxPod.PodVolume(
|
|
name: "my-disk",
|
|
source: .diskImage(path: URL(fileURLWithPath: "/tmp/my-disk.ext4")),
|
|
format: "ext4"
|
|
)
|
|
|
|
let mount = volume.toMount()
|
|
|
|
// The mount source must be the raw filesystem path, not a file:// URL.
|
|
#expect(mount.source == "/tmp/my-disk.ext4")
|
|
#expect(mount.destination == "/run/volumes/my-disk")
|
|
#expect(mount.type == "ext4")
|
|
#expect(mount.isBlock)
|
|
}
|
|
|
|
@Test func podVolumeDiskImageReadOnlySetsOptions() {
|
|
let volume = LinuxPod.PodVolume(
|
|
name: "ro-disk",
|
|
source: .diskImage(path: URL(fileURLWithPath: "/tmp/ro-disk.ext4"), readOnly: true),
|
|
format: "ext4"
|
|
)
|
|
|
|
let mount = volume.toMount()
|
|
|
|
#expect(mount.options.contains("ro"))
|
|
#expect(mount.isBlock)
|
|
}
|
|
|
|
@Test func sharedMountCreation() {
|
|
let mount = Mount.sharedMount(
|
|
name: "shared-data",
|
|
destination: "/data",
|
|
options: ["ro"]
|
|
)
|
|
|
|
#expect(mount.source == "shared-data")
|
|
#expect(mount.destination == "/data")
|
|
#expect(mount.options == ["ro"])
|
|
#expect(mount.type == "none")
|
|
if case .shared = mount.runtimeOptions {
|
|
// correct
|
|
} else {
|
|
Issue.record("Expected .shared runtime options")
|
|
}
|
|
}
|
|
|
|
@Test func sharedMountDefaultOptions() {
|
|
let mount = Mount.sharedMount(name: "data", destination: "/mnt")
|
|
|
|
#expect(mount.options.isEmpty)
|
|
}
|
|
|
|
@Test func sharedMountIsNotBlock() {
|
|
let mount = Mount.sharedMount(name: "data", destination: "/mnt")
|
|
|
|
#expect(!mount.isBlock)
|
|
}
|
|
|
|
@Test func sharedMountDoesNotAllocateBlockDevice() throws {
|
|
let allocator = Character.blockDeviceTagAllocator()
|
|
|
|
// Shared mount should not consume a device letter.
|
|
let shared = Mount.sharedMount(name: "vol", destination: "/data")
|
|
let attached = try AttachedFilesystem(mount: shared, allocator: allocator)
|
|
#expect(attached.source == "vol")
|
|
|
|
// Next block device should still get vda.
|
|
let block = Mount.block(format: "ext4", source: "/disk.img", destination: "/mnt")
|
|
let blockAttached = try AttachedFilesystem(mount: block, allocator: allocator)
|
|
#expect(blockAttached.source == "/dev/vda")
|
|
}
|
|
}
|