mirror of
https://github.com/cirruslabs/tart.git
synced 2026-10-17 19:36:10 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
456ebc1c7b | ||
|
|
14cbc727ad | ||
|
|
03e6d9345d | ||
|
|
c09cbefdd0 | ||
|
|
3896728eb9 | ||
|
|
de993fbf6d | ||
|
|
f08ddf3855 | ||
|
|
8524d93741 |
+23
-3
@@ -1,13 +1,30 @@
|
||||
task:
|
||||
name: Test on Ventura
|
||||
alias: test
|
||||
persistent_worker:
|
||||
labels:
|
||||
name: Mac-Mini-M1
|
||||
build_script: swift test
|
||||
test_script: swift test
|
||||
name: scaleway-m1
|
||||
test_script:
|
||||
- swift test
|
||||
integration_test_script:
|
||||
# Build Tart
|
||||
- swift build
|
||||
- codesign --sign - --entitlements Resources/tart.entitlements --force .build/debug/tart
|
||||
- export PATH=$(pwd)/.build/arm64-apple-macosx/debug:$PATH
|
||||
# Run integration tests
|
||||
- cd integration-tests
|
||||
- HOMEBREW_NO_AUTO_UPDATE=1 brew install virtualenv
|
||||
- virtualenv venv
|
||||
- source venv/bin/activate
|
||||
- pip install -r requirements.txt
|
||||
- pytest --verbose --junit-xml=pytest-junit.xml
|
||||
pytest_junit_result_artifacts:
|
||||
path: "integration-tests/pytest-junit.xml"
|
||||
format: junit
|
||||
|
||||
task:
|
||||
name: Build
|
||||
alias: build
|
||||
only_if: $CIRRUS_TAG == ''
|
||||
macos_instance:
|
||||
image: ghcr.io/cirruslabs/macos-ventura-xcode:latest
|
||||
@@ -19,6 +36,9 @@ task:
|
||||
task:
|
||||
name: Release
|
||||
only_if: $CIRRUS_TAG != ''
|
||||
depends_on:
|
||||
- test
|
||||
- build
|
||||
macos_instance:
|
||||
image: ghcr.io/cirruslabs/macos-ventura-xcode:latest
|
||||
env:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
* @edigaryev @fkorotkov
|
||||
@@ -22,6 +22,22 @@ Built by CI engineers for your automation needs. Here are some highlights of Tar
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Many more companies are using Tart in their internal setups. Here are a few of them:
|
||||
|
||||
<p align="center">
|
||||
<a href="https://ahrefs.com/" target=_blank>
|
||||
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/ahrefs.png" height="65"/>
|
||||
</a>
|
||||
<a href="https://suran.com/" target=_blank>
|
||||
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/Suran.png" height="65"/>
|
||||
</a>
|
||||
<a href="https://symflower.com/" target=_blank>
|
||||
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/Symflower.png" height="65"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
**Note:** If your company or project is using Tart please consider [adding yourself to the list above](/Resources/Users/HowToAddYourself.md).
|
||||
|
||||
## Usage
|
||||
|
||||
Try running a Tart VM on your Apple Silicon device running macOS Monterey or later (will download a 25 GB image):
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4d1f58f6460217434ae133c6d6e444320eb705c5dcf9efebbb215b117ae452c1
|
||||
size 589
|
||||
@@ -0,0 +1,4 @@
|
||||
If you'd like to highlight your use of Tart, please create a `456px` by `130px` logo and create a PR
|
||||
that adds it to `README.md` in alphabetical order. Don't forget to include a small description of your usage pattern.
|
||||
|
||||
You can refer to `Background.png` as a base for your logo.
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:630c4975b9eead9e4f62e2a5375509dc0ee6cab2940ec3ffc2ae16b08d325964
|
||||
size 6765
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5e48fbe90f6cc84e100fc286a5349c681cacb22358811181046d35ea298309de
|
||||
size 16379
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1c7f7f9bf0fc0c9807f3a415fe6bf773c8a416b351c5aa08de46149a13de7387
|
||||
size 5297
|
||||
@@ -48,6 +48,20 @@ struct Run: AsyncParsableCommand {
|
||||
""", valueName: "path[:ro]"))
|
||||
var disk: [String] = []
|
||||
|
||||
@Option(name: [.customLong("rosetta")], help: ArgumentHelp(
|
||||
"Attaches a Rosetta share to the guest Linux VM with a specific tag (e.g. --rosetta=\"rosetta\")",
|
||||
discussion: """
|
||||
Requires host to be macOS 13.0 (Ventura) with Rosetta installed. The latter can be done
|
||||
by running "softwareupdate --install-rosetta" (without quotes) in the Terminal.app.
|
||||
|
||||
Note that you also have to configure Rosetta in the guest Linux VM by following the
|
||||
steps from "Mount the Shared Directory and Register Rosetta" section here:
|
||||
https://developer.apple.com/documentation/virtualization/running_intel_binaries_in_linux_vms_with_rosetta#3978496
|
||||
""",
|
||||
valueName: "tag"
|
||||
))
|
||||
var rosettaTag: String?
|
||||
|
||||
@Option(help: ArgumentHelp("""
|
||||
Additional directory shares with an optional read-only specifier\n(e.g. --dir=\"build:~/src/build\" --dir=\"sources:~/src/sources:ro\")
|
||||
""", discussion: """
|
||||
@@ -90,11 +104,24 @@ struct Run: AsyncParsableCommand {
|
||||
@MainActor
|
||||
func run() async throws {
|
||||
let vmDir = try VMStorageLocal().open(name)
|
||||
|
||||
let additionalDiskAttachments = try additionalDiskAttachments()
|
||||
|
||||
// Error out if the disk is locked by the host (e.g. it was mounted in Finder),
|
||||
// see https://github.com/cirruslabs/tart/issues/323 for more details.
|
||||
for additionalDiskAttachment in additionalDiskAttachments {
|
||||
if try !FileLock(lockURL: additionalDiskAttachment.url).trylock() {
|
||||
print("disk \(additionalDiskAttachment.url.path) seems to be already in use, "
|
||||
+ "unmount it first in Finder")
|
||||
Foundation.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
vm = try VM(
|
||||
vmDir: vmDir,
|
||||
network: userSpecifiedNetwork(vmDir: vmDir) ?? NetworkShared(),
|
||||
additionalDiskAttachments: additionalDiskAttachments(),
|
||||
directoryShares: directoryShares()
|
||||
additionalDiskAttachments: additionalDiskAttachments,
|
||||
directorySharingDevices: directoryShares() + rosettaDirectoryShare()
|
||||
)
|
||||
|
||||
let vncImpl: VNC? = try {
|
||||
@@ -229,8 +256,22 @@ struct Run: AsyncParsableCommand {
|
||||
return result
|
||||
}
|
||||
|
||||
func directoryShares() throws -> [DirectoryShare] {
|
||||
var result: [DirectoryShare] = []
|
||||
func directoryShares() throws -> [VZDirectorySharingDeviceConfiguration] {
|
||||
if dir.isEmpty {
|
||||
return []
|
||||
}
|
||||
|
||||
guard #available(macOS 13, *) else {
|
||||
throw UnsupportedOSError("directory sharing", "is")
|
||||
}
|
||||
|
||||
struct DirectoryShare {
|
||||
let name: String
|
||||
let path: URL
|
||||
let readOnly: Bool
|
||||
}
|
||||
|
||||
var directoryShares: [DirectoryShare] = []
|
||||
|
||||
for rawDir in dir {
|
||||
let splits = rawDir.split(maxSplits: 2) { $0 == ":" }
|
||||
@@ -251,10 +292,46 @@ struct Run: AsyncParsableCommand {
|
||||
|
||||
let (name, path) = (String(splits[0]), String(splits[1]))
|
||||
|
||||
result.append(DirectoryShare(name: name, path: URL(fileURLWithPath: NSString(string: path).expandingTildeInPath), readOnly: readOnly))
|
||||
directoryShares.append(DirectoryShare(
|
||||
name: name,
|
||||
path: URL(fileURLWithPath: NSString(string: path).expandingTildeInPath),
|
||||
readOnly: readOnly)
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
var directories: [String : VZSharedDirectory] = Dictionary()
|
||||
directoryShares.forEach { directories[$0.name] = VZSharedDirectory(url: $0.path, readOnly: $0.readOnly) }
|
||||
|
||||
let automountTag = VZVirtioFileSystemDeviceConfiguration.macOSGuestAutomountTag
|
||||
let sharingDevice = VZVirtioFileSystemDeviceConfiguration(tag: automountTag)
|
||||
sharingDevice.share = VZMultipleDirectoryShare(directories: directories)
|
||||
|
||||
return [sharingDevice]
|
||||
}
|
||||
|
||||
private func rosettaDirectoryShare() throws -> [VZDirectorySharingDeviceConfiguration] {
|
||||
guard let rosettaTag = rosettaTag else {
|
||||
return []
|
||||
}
|
||||
|
||||
guard #available(macOS 13, *) else {
|
||||
throw UnsupportedOSError("Rosetta directory share", "is")
|
||||
}
|
||||
|
||||
switch VZLinuxRosettaDirectoryShare.availability {
|
||||
case .notInstalled:
|
||||
throw UnsupportedOSError("Rosetta directory share", "is", "that have Rosetta installed")
|
||||
case .notSupported:
|
||||
throw UnsupportedOSError("Rosetta directory share", "is", "running Apple silicon")
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
try VZVirtioFileSystemDeviceConfiguration.validateTag(rosettaTag)
|
||||
let device = VZVirtioFileSystemDeviceConfiguration(tag: rosettaTag)
|
||||
device.share = try VZLinuxRosettaDirectoryShare()
|
||||
|
||||
return [device]
|
||||
}
|
||||
|
||||
private func runUI() {
|
||||
|
||||
@@ -11,7 +11,7 @@ struct Set: AsyncParsableCommand {
|
||||
var cpu: UInt16?
|
||||
|
||||
@Option(help: "VM memory size in megabytes")
|
||||
var memory: UInt16?
|
||||
var memory: UInt64?
|
||||
|
||||
@Option(help: "VM display resolution in a format of <width>x<height>. For example, 1200x800")
|
||||
var display: VMDisplayConfig?
|
||||
@@ -29,7 +29,7 @@ struct Set: AsyncParsableCommand {
|
||||
}
|
||||
|
||||
if let memory = memory {
|
||||
try vmConfig.setMemory(memorySize: UInt64(memory) * 1024 * 1024)
|
||||
try vmConfig.setMemory(memorySize: memory * 1024 * 1024)
|
||||
}
|
||||
|
||||
if let display = display {
|
||||
|
||||
+19
-28
@@ -14,8 +14,8 @@ struct DownloadFailed: Error {
|
||||
struct UnsupportedOSError: Error, CustomStringConvertible {
|
||||
let description: String
|
||||
|
||||
init(_ what: String, _ plural: String) {
|
||||
description = "error: \(what) \(plural) only supported on hosts running macOS 13.0 (Ventura) or newer"
|
||||
init(_ what: String, _ plural: String, _ requires: String = "running macOS 13.0 (Ventura) or newer") {
|
||||
description = "error: \(what) \(plural) only supported on hosts \(requires)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
init(vmDir: VMDirectory,
|
||||
network: Network = NetworkShared(),
|
||||
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [],
|
||||
directoryShares: [DirectoryShare] = []
|
||||
directorySharingDevices: [VZDirectorySharingDeviceConfiguration] = []
|
||||
) throws {
|
||||
name = vmDir.name
|
||||
config = try VMConfig.init(fromURL: vmDir.configURL)
|
||||
@@ -54,7 +54,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL,
|
||||
nvramURL: vmDir.nvramURL, vmConfig: config,
|
||||
network: network, additionalDiskAttachments: additionalDiskAttachments,
|
||||
directoryShares: directoryShares)
|
||||
directorySharingDevices: directorySharingDevices
|
||||
)
|
||||
virtualMachine = VZVirtualMachine(configuration: configuration)
|
||||
|
||||
super.init()
|
||||
@@ -63,7 +64,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
|
||||
static func retrieveIPSW(remoteURL: URL) async throws -> URL {
|
||||
// Check if we already have this IPSW in cache
|
||||
let (channel, response) = try await Fetcher.fetch(URLRequest(url: remoteURL))
|
||||
let (channel, response) = try await Fetcher.fetch(URLRequest(url: remoteURL), viaFile: true)
|
||||
|
||||
if let hash = response.value(forHTTPHeaderField: "x-amz-meta-digest-sha256") {
|
||||
let ipswLocation = try IPSWCache().locationFor(fileName: "sha256:\(hash).ipsw")
|
||||
@@ -130,7 +131,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
ipswURL: URL,
|
||||
diskSizeGB: UInt16,
|
||||
network: Network = NetworkShared(),
|
||||
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = []
|
||||
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [],
|
||||
directorySharingDevices: [VZDirectorySharingDeviceConfiguration] = []
|
||||
) async throws {
|
||||
var ipswURL = ipswURL
|
||||
|
||||
@@ -138,6 +140,11 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
ipswURL = try await VM.retrieveIPSW(remoteURL: ipswURL)
|
||||
}
|
||||
|
||||
// We create a temporary TART_HOME directory in tests, which has its "cache" folder symlinked
|
||||
// to the users Tart cache directory (~/.tart/cache). However, the Virtualization.Framework
|
||||
// cannot deal with paths that contain symlinks, so expand them here first.
|
||||
ipswURL.resolveSymlinksInPath()
|
||||
|
||||
// Load the restore image and try to get the requirements
|
||||
// that match both the image and our platform
|
||||
let image = try await withCheckedThrowingContinuation { continuation in
|
||||
@@ -172,7 +179,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, nvramURL: vmDir.nvramURL,
|
||||
vmConfig: config, network: network,
|
||||
additionalDiskAttachments: additionalDiskAttachments,
|
||||
directoryShares: [])
|
||||
directorySharingDevices: directorySharingDevices
|
||||
)
|
||||
virtualMachine = VZVirtualMachine(configuration: configuration)
|
||||
|
||||
super.init()
|
||||
@@ -248,7 +256,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
vmConfig: VMConfig,
|
||||
network: Network = NetworkShared(),
|
||||
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment],
|
||||
directoryShares: [DirectoryShare]
|
||||
directorySharingDevices: [VZDirectorySharingDeviceConfiguration]
|
||||
) throws -> VZVirtualMachineConfiguration {
|
||||
let configuration = VZVirtualMachineConfiguration()
|
||||
|
||||
@@ -292,19 +300,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
// Entropy
|
||||
configuration.entropyDevices = [VZVirtioEntropyDeviceConfiguration()]
|
||||
|
||||
// Directory share
|
||||
if #available(macOS 13, *) {
|
||||
var directories: [String : VZSharedDirectory] = Dictionary()
|
||||
directoryShares.forEach { directories[$0.name] = VZSharedDirectory(url: $0.path, readOnly: $0.readOnly) }
|
||||
|
||||
let automountTag = VZVirtioFileSystemDeviceConfiguration.macOSGuestAutomountTag
|
||||
let sharingDevice = VZVirtioFileSystemDeviceConfiguration(tag: automountTag)
|
||||
sharingDevice.share = VZMultipleDirectoryShare(directories: directories)
|
||||
|
||||
configuration.directorySharingDevices = [sharingDevice]
|
||||
} else if !directoryShares.isEmpty {
|
||||
throw UnsupportedOSError("directory sharing", "is")
|
||||
}
|
||||
// Directory sharing devices
|
||||
configuration.directorySharingDevices = directorySharingDevices
|
||||
|
||||
try configuration.validate()
|
||||
|
||||
@@ -317,7 +314,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
}
|
||||
|
||||
func virtualMachine(_ virtualMachine: VZVirtualMachine, didStopWithError error: Error) {
|
||||
print("guest has stopped the virtual machine due to error")
|
||||
print("guest has stopped the virtual machine due to error: \(error)")
|
||||
sema.signal()
|
||||
}
|
||||
|
||||
@@ -326,9 +323,3 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
sema.signal()
|
||||
}
|
||||
}
|
||||
|
||||
struct DirectoryShare {
|
||||
let name: String
|
||||
let path: URL
|
||||
let readOnly: Bool
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import pytest
|
||||
|
||||
from tart import Tart
|
||||
from docker_registry import DockerRegistry
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def tart():
|
||||
with Tart() as tart:
|
||||
yield tart
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def docker_registry():
|
||||
with DockerRegistry() as docker_registry:
|
||||
yield docker_registry
|
||||
@@ -0,0 +1,20 @@
|
||||
import requests
|
||||
|
||||
from testcontainers.core.waiting_utils import wait_container_is_ready
|
||||
from testcontainers.general import DockerContainer
|
||||
|
||||
|
||||
class DockerRegistry(DockerContainer):
|
||||
_default_exposed_port = 5000
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("registry:2")
|
||||
self.with_exposed_ports(self._default_exposed_port)
|
||||
|
||||
@wait_container_is_ready(requests.exceptions.ConnectionError)
|
||||
def remote_name(self, for_vm: str):
|
||||
exposed_port = self.get_exposed_port(self._default_exposed_port)
|
||||
|
||||
requests.get(f"http://127.0.0.1:{exposed_port}/v2/")
|
||||
|
||||
return f"127.0.0.1:{exposed_port}/tart/{for_vm}:latest"
|
||||
@@ -0,0 +1,5 @@
|
||||
pytest
|
||||
testcontainers
|
||||
requests
|
||||
bitmath
|
||||
pytest-dependency
|
||||
@@ -0,0 +1,33 @@
|
||||
import tempfile
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
||||
class Tart:
|
||||
def __init__(self):
|
||||
self.tmp_dir = tempfile.TemporaryDirectory(dir=os.environ.get("CIRRUS_WORKING_DIR"))
|
||||
|
||||
# Link to the users IPSW cache to make things faster
|
||||
src = os.path.join(os.path.expanduser("~"), ".tart", "cache", "IPSWs")
|
||||
dst = os.path.join(self.tmp_dir.name, "cache", "IPSWs")
|
||||
os.makedirs(os.path.join(self.tmp_dir.name, "cache"))
|
||||
os.symlink(src, dst)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.tmp_dir.cleanup()
|
||||
|
||||
def home(self) -> str:
|
||||
return self.tmp_dir.name
|
||||
|
||||
def run(self, args):
|
||||
env = os.environ.copy()
|
||||
env.update({"TART_HOME": self.tmp_dir.name})
|
||||
|
||||
completed_process = subprocess.run(["tart"] + args, env=env, capture_output=True)
|
||||
|
||||
completed_process.check_returncode()
|
||||
|
||||
return completed_process.stdout.decode("utf-8"), completed_process.stderr.decode("utf-8")
|
||||
@@ -0,0 +1,10 @@
|
||||
def test_clone(tart):
|
||||
# Create a Linux VM (because we can create it really fast)
|
||||
tart.run(["create", "--linux", "debian"])
|
||||
|
||||
# Clone the VM
|
||||
tart.run(["clone", "debian", "ubuntu"])
|
||||
|
||||
# Ensure that we have now 2 VMs
|
||||
stdout, _, = tart.run(["list", "--quiet"])
|
||||
assert stdout == "debian\nubuntu\n"
|
||||
@@ -0,0 +1,16 @@
|
||||
def test_create_macos(tart):
|
||||
# Create a macOS VM
|
||||
tart.run(["create", "--from-ipsw", "latest", "macos-vm"])
|
||||
|
||||
# Ensure that the VM was created
|
||||
stdout, _ = tart.run(["list", "--quiet"])
|
||||
assert stdout == "macos-vm\n"
|
||||
|
||||
|
||||
def test_create_linux(tart):
|
||||
# Create a Linux VM
|
||||
tart.run(["create", "--linux", "linux-vm"])
|
||||
|
||||
# Ensure that the VM was created
|
||||
stdout, _ = tart.run(["list", "--quiet"])
|
||||
assert stdout == "linux-vm\n"
|
||||
@@ -0,0 +1,14 @@
|
||||
def test_delete(tart):
|
||||
# Create a Linux VM (because we can create it really fast)
|
||||
tart.run(["create", "--linux", "debian"])
|
||||
|
||||
# Ensure that the VM exists
|
||||
stdout, _, = tart.run(["list", "--quiet"])
|
||||
assert stdout == "debian\n"
|
||||
|
||||
# Delete the VM
|
||||
tart.run(["delete", "debian"])
|
||||
|
||||
# Ensure that the VM was removed
|
||||
stdout, _, = tart.run(["list", "--quiet"])
|
||||
assert stdout == ""
|
||||
@@ -0,0 +1,55 @@
|
||||
import os
|
||||
import tempfile
|
||||
import timeit
|
||||
import uuid
|
||||
|
||||
import bitmath
|
||||
import pytest
|
||||
|
||||
amount_to_transfer = bitmath.GB(1)
|
||||
minimal_speed_per_second = bitmath.Mb(100)
|
||||
|
||||
|
||||
class TestOCI:
|
||||
@pytest.mark.dependency()
|
||||
def test_push_speed(self, tart, vm_with_random_disk, docker_registry):
|
||||
start = timeit.default_timer()
|
||||
tart.run(["push", "--insecure", vm_with_random_disk, docker_registry.remote_name(vm_with_random_disk)])
|
||||
stop = timeit.default_timer()
|
||||
|
||||
actual_speed_per_second = self._calculate_speed_per_second(amount_to_transfer, stop - start)
|
||||
assert actual_speed_per_second > minimal_speed_per_second
|
||||
|
||||
@pytest.mark.dependency(depends=["TestOCI::test_push_speed"])
|
||||
def test_pull_speed(self, tart, vm_with_random_disk, docker_registry):
|
||||
start = timeit.default_timer()
|
||||
tart.run(["pull", "--insecure", docker_registry.remote_name(vm_with_random_disk)])
|
||||
stop = timeit.default_timer()
|
||||
|
||||
actual_speed_per_second = self._calculate_speed_per_second(amount_to_transfer, stop - start)
|
||||
assert actual_speed_per_second > minimal_speed_per_second
|
||||
|
||||
@staticmethod
|
||||
def _calculate_speed_per_second(amount_transferred, time_taken):
|
||||
return (amount_transferred / time_taken).best_prefix(bitmath.SI)
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def vm_with_random_disk(tart):
|
||||
vm_name = str(uuid.uuid4())
|
||||
|
||||
# Create a VM (Linux for speed's sake)
|
||||
tart.run(["create", "--linux", vm_name])
|
||||
|
||||
# Populate VM's disk with "amount_to_transfer" of random bytes
|
||||
# to effectively disable Tart's OCI blob compression
|
||||
disk_path = os.path.join(tart.home(), "vms", vm_name, "disk.img")
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False) as tf:
|
||||
tf.write(os.urandom(amount_to_transfer.bytes))
|
||||
tf.close()
|
||||
os.rename(tf.name, disk_path)
|
||||
|
||||
yield vm_name
|
||||
|
||||
tart.run(["delete", vm_name])
|
||||
@@ -0,0 +1,10 @@
|
||||
def test_rename(tart):
|
||||
# Create a Linux VM (because we can create it really fast)
|
||||
tart.run(["create", "--linux", "debian"])
|
||||
|
||||
# Rename that VM
|
||||
tart.run(["rename", "debian", "ubuntu"])
|
||||
|
||||
# Ensure that the VM is now named "ubuntu"
|
||||
stdout, _, = tart.run(["list", "--quiet"])
|
||||
assert stdout == "ubuntu\n"
|
||||
Reference in New Issue
Block a user