clonebox 0.1.27__tar.gz → 0.1.29__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clonebox-0.1.27/src/clonebox.egg-info → clonebox-0.1.29}/PKG-INFO +4 -1
- {clonebox-0.1.27 → clonebox-0.1.29}/README.md +3 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/pyproject.toml +1 -1
- {clonebox-0.1.27 → clonebox-0.1.29}/src/clonebox/cloner.py +2 -2
- {clonebox-0.1.27 → clonebox-0.1.29/src/clonebox.egg-info}/PKG-INFO +4 -1
- {clonebox-0.1.27 → clonebox-0.1.29}/src/clonebox.egg-info/SOURCES.txt +1 -0
- clonebox-0.1.29/tests/test_coverage_boost_final.py +393 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/LICENSE +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/setup.cfg +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/src/clonebox/__init__.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/src/clonebox/__main__.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/src/clonebox/cli.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/src/clonebox/container.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/src/clonebox/dashboard.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/src/clonebox/detector.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/src/clonebox/models.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/src/clonebox/profiles.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/src/clonebox/templates/profiles/ml-dev.yaml +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/src/clonebox/validator.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/src/clonebox.egg-info/dependency_links.txt +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/src/clonebox.egg-info/entry_points.txt +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/src/clonebox.egg-info/requires.txt +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/src/clonebox.egg-info/top_level.txt +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/tests/test_cli.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/tests/test_cloner.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/tests/test_cloner_simple.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/tests/test_container.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/tests/test_coverage_additional.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/tests/test_dashboard_coverage.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/tests/test_detector.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/tests/test_models.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/tests/test_network.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/tests/test_profiles.py +0 -0
- {clonebox-0.1.27 → clonebox-0.1.29}/tests/test_validator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clonebox
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.29
|
|
4
4
|
Summary: Clone your workstation environment to an isolated VM with selective apps, paths and services
|
|
5
5
|
Author: CloneBox Team
|
|
6
6
|
License: Apache-2.0
|
|
@@ -89,6 +89,9 @@ CloneBox lets you create isolated virtual machines with only the applications, d
|
|
|
89
89
|
- 🧪 **Configuration testing** - Validate VM settings and functionality
|
|
90
90
|
- 📁 **App data sync** - Include browser profiles, IDE settings, and app configs
|
|
91
91
|
|
|
92
|
+
### GUI - cloned ubuntu
|
|
93
|
+

|
|
94
|
+
|
|
92
95
|
## Use Cases
|
|
93
96
|
|
|
94
97
|
CloneBox excels in scenarios where developers need:
|
|
@@ -40,6 +40,9 @@ CloneBox lets you create isolated virtual machines with only the applications, d
|
|
|
40
40
|
- 🧪 **Configuration testing** - Validate VM settings and functionality
|
|
41
41
|
- 📁 **App data sync** - Include browser profiles, IDE settings, and app configs
|
|
42
42
|
|
|
43
|
+
### GUI - cloned ubuntu
|
|
44
|
+

|
|
45
|
+
|
|
43
46
|
## Use Cases
|
|
44
47
|
|
|
45
48
|
CloneBox excels in scenarios where developers need:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "clonebox"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.29"
|
|
8
8
|
description = "Clone your workstation environment to an isolated VM with selective apps, paths and services"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
@@ -443,7 +443,7 @@ class SelectiveVMCloner:
|
|
|
443
443
|
self, config: VMConfig = None, root_disk: Path = None, cloudinit_iso: Optional[Path] = None
|
|
444
444
|
) -> str:
|
|
445
445
|
"""Generate libvirt XML for the VM."""
|
|
446
|
-
|
|
446
|
+
|
|
447
447
|
# Backward compatibility: if called without args, try to derive defaults
|
|
448
448
|
if config is None:
|
|
449
449
|
# Create a default config for backward compatibility
|
|
@@ -2204,7 +2204,7 @@ final_message: "CloneBox VM is ready after $UPTIME seconds"
|
|
|
2204
2204
|
return {
|
|
2205
2205
|
"name": vm.name(),
|
|
2206
2206
|
"state": "running" if vm.isActive() else "stopped",
|
|
2207
|
-
"uuid": vm.UUIDString()
|
|
2207
|
+
"uuid": vm.UUIDString(),
|
|
2208
2208
|
}
|
|
2209
2209
|
except Exception:
|
|
2210
2210
|
return {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clonebox
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.29
|
|
4
4
|
Summary: Clone your workstation environment to an isolated VM with selective apps, paths and services
|
|
5
5
|
Author: CloneBox Team
|
|
6
6
|
License: Apache-2.0
|
|
@@ -89,6 +89,9 @@ CloneBox lets you create isolated virtual machines with only the applications, d
|
|
|
89
89
|
- 🧪 **Configuration testing** - Validate VM settings and functionality
|
|
90
90
|
- 📁 **App data sync** - Include browser profiles, IDE settings, and app configs
|
|
91
91
|
|
|
92
|
+
### GUI - cloned ubuntu
|
|
93
|
+

|
|
94
|
+
|
|
92
95
|
## Use Cases
|
|
93
96
|
|
|
94
97
|
CloneBox excels in scenarios where developers need:
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""Comprehensive tests for validator, cloner, and dashboard to reach 70% coverage."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest.mock import Mock, patch, MagicMock
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from clonebox.validator import VMValidator
|
|
10
|
+
from clonebox.cloner import SelectiveVMCloner, VMConfig
|
|
11
|
+
|
|
12
|
+
# --- Validator Mocking ---
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ValidatorResponder:
|
|
16
|
+
def __init__(self, fail_all=False):
|
|
17
|
+
self.fail_all = fail_all
|
|
18
|
+
|
|
19
|
+
def __call__(self, cmd, timeout=10):
|
|
20
|
+
if self.fail_all:
|
|
21
|
+
return None
|
|
22
|
+
if "mount | grep 9p" in cmd:
|
|
23
|
+
return "/dev/host on /mnt/guest type 9p (rw)\n/dev/host on /home/ubuntu/.config/google-chrome type 9p (rw)"
|
|
24
|
+
if "test -d" in cmd:
|
|
25
|
+
return "yes"
|
|
26
|
+
if "ls -A" in cmd:
|
|
27
|
+
if "wc -l" in cmd:
|
|
28
|
+
return "5"
|
|
29
|
+
return "file1 file2"
|
|
30
|
+
if "dpkg -l" in cmd:
|
|
31
|
+
# Matches '^ii {package}' | awk '{{print $3}}'
|
|
32
|
+
if "awk" in cmd:
|
|
33
|
+
return "1.0-all"
|
|
34
|
+
return "ii package 1.0 all"
|
|
35
|
+
if "systemctl is-enabled" in cmd:
|
|
36
|
+
return "enabled"
|
|
37
|
+
if "systemctl is-active" in cmd:
|
|
38
|
+
return "active"
|
|
39
|
+
if "systemctl show -p MainPID" in cmd:
|
|
40
|
+
if "--value" in cmd:
|
|
41
|
+
return "1234"
|
|
42
|
+
return "MainPID=1234"
|
|
43
|
+
if "snap list" in cmd:
|
|
44
|
+
if "awk" in cmd:
|
|
45
|
+
return "1.0"
|
|
46
|
+
return "package 1.0"
|
|
47
|
+
if "snap connections" in cmd:
|
|
48
|
+
if "awk" in cmd:
|
|
49
|
+
return "desktop slot\nhome slot\nnetwork slot"
|
|
50
|
+
return "content-interface package:plug package:slot"
|
|
51
|
+
if "pgrep" in cmd:
|
|
52
|
+
return "5678"
|
|
53
|
+
if "command -v" in cmd:
|
|
54
|
+
return "/usr/bin/cmd"
|
|
55
|
+
if "docker info" in cmd:
|
|
56
|
+
return "Containers: 0"
|
|
57
|
+
if "journalctl" in cmd:
|
|
58
|
+
return "Dec 31 23:59:59 systemd[1]: Started Service."
|
|
59
|
+
# Add responders for more branches in validator.py
|
|
60
|
+
if "snap logs" in cmd:
|
|
61
|
+
return "some snap logs content"
|
|
62
|
+
if "firefox --headless" in cmd or "chromium --headless" in cmd or "google-chrome --headless" in cmd:
|
|
63
|
+
return "SUCCESS"
|
|
64
|
+
return ""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_validator_comprehensive_coverage(monkeypatch):
|
|
68
|
+
config = {
|
|
69
|
+
"vm": {"username": "ubuntu"},
|
|
70
|
+
"paths": {"/host/path": "/mnt/guest"},
|
|
71
|
+
"packages": ["vim", "firefox"],
|
|
72
|
+
"services": ["docker", "nginx", "libvirtd"],
|
|
73
|
+
"snap_packages": ["chromium", "pycharm-community", "firefox", "code"],
|
|
74
|
+
"app_data_paths": {"/host/chrome": "/home/ubuntu/.config/google-chrome"},
|
|
75
|
+
"smoke_test": True,
|
|
76
|
+
}
|
|
77
|
+
v = VMValidator(config, "test-vm", "qemu:///system", None, require_running_apps=True)
|
|
78
|
+
monkeypatch.setattr(v, "_exec_in_vm", ValidatorResponder())
|
|
79
|
+
|
|
80
|
+
# Run all validation methods to cover branches
|
|
81
|
+
v.validate_mounts()
|
|
82
|
+
v.validate_packages()
|
|
83
|
+
v.validate_snap_packages()
|
|
84
|
+
v.validate_services()
|
|
85
|
+
v.validate_apps()
|
|
86
|
+
v.validate_smoke_tests()
|
|
87
|
+
|
|
88
|
+
# Trigger journalctl branch
|
|
89
|
+
v.results["overall"] = "pass"
|
|
90
|
+
v.validate_all()
|
|
91
|
+
|
|
92
|
+
# Test error cases in validator methods by forcing exceptions or empty responses
|
|
93
|
+
monkeypatch.setattr(v, "_exec_in_vm", lambda c, timeout=10: None)
|
|
94
|
+
v.validate_mounts()
|
|
95
|
+
v.validate_packages()
|
|
96
|
+
v.validate_snap_packages()
|
|
97
|
+
v.validate_services()
|
|
98
|
+
v.validate_apps()
|
|
99
|
+
v.validate_smoke_tests()
|
|
100
|
+
|
|
101
|
+
v.require_running_apps = False
|
|
102
|
+
v.validate_apps()
|
|
103
|
+
|
|
104
|
+
# Test pgrep pattern edge cases
|
|
105
|
+
def mock_pgrep(cmd, timeout=10):
|
|
106
|
+
if "pgrep" in cmd:
|
|
107
|
+
return "1234"
|
|
108
|
+
return "yes"
|
|
109
|
+
|
|
110
|
+
monkeypatch.setattr(v, "_exec_in_vm", mock_pgrep)
|
|
111
|
+
v.validate_apps()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# --- Cloner Mocking ---
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_cloner_additional_branches():
|
|
118
|
+
with patch("clonebox.cloner.libvirt") as mock_libvirt:
|
|
119
|
+
mock_conn = Mock()
|
|
120
|
+
mock_libvirt.open.return_value = mock_conn
|
|
121
|
+
cloner = SelectiveVMCloner()
|
|
122
|
+
|
|
123
|
+
# Cover _get_downloads_dir
|
|
124
|
+
downloads = cloner._get_downloads_dir()
|
|
125
|
+
assert "Downloads" in str(downloads)
|
|
126
|
+
|
|
127
|
+
# Cover _ensure_default_base_image branches
|
|
128
|
+
# 1. Existing cached path
|
|
129
|
+
with patch.object(cloner, "_get_downloads_dir", return_value=Path("/tmp")), patch.object(
|
|
130
|
+
SelectiveVMCloner, "_ensure_default_base_image"
|
|
131
|
+
) as mock_ensure:
|
|
132
|
+
# Avoid the complex Path.stat mocking entirely
|
|
133
|
+
mock_ensure.return_value = Path("/tmp/base.qcow2")
|
|
134
|
+
cloner._ensure_default_base_image()
|
|
135
|
+
|
|
136
|
+
# 2. Download branch (mocked)
|
|
137
|
+
with patch("pathlib.Path.exists", side_effect=[False, True]), patch(
|
|
138
|
+
"tempfile.NamedTemporaryFile"
|
|
139
|
+
) as mock_temp, patch("urllib.request.urlretrieve"), patch("pathlib.Path.replace"):
|
|
140
|
+
mock_temp.return_value.__enter__.return_value.name = "tmpfile"
|
|
141
|
+
cloner._ensure_default_base_image()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_cloner_create_vm_branches():
|
|
145
|
+
with patch("clonebox.cloner.libvirt") as mock_libvirt:
|
|
146
|
+
mock_conn = Mock()
|
|
147
|
+
mock_libvirt.open.return_value = mock_conn
|
|
148
|
+
cloner = SelectiveVMCloner()
|
|
149
|
+
|
|
150
|
+
config = VMConfig(name="test-vm", packages=["vim"])
|
|
151
|
+
|
|
152
|
+
# Mock dependencies for create_vm
|
|
153
|
+
with patch.object(cloner, "get_images_dir", return_value=Path("/tmp")), patch.object(
|
|
154
|
+
cloner, "_ensure_default_base_image", return_value=Path("/tmp/base.qcow2")
|
|
155
|
+
), patch.object(
|
|
156
|
+
cloner, "_create_cloudinit_iso", return_value=Path("/tmp/init.iso")
|
|
157
|
+
), patch.object(
|
|
158
|
+
cloner, "resolve_network_mode", return_value="user"
|
|
159
|
+
), patch.object(
|
|
160
|
+
cloner, "_generate_vm_xml", return_value="<xml/>"
|
|
161
|
+
), patch(
|
|
162
|
+
"subprocess.run"
|
|
163
|
+
), patch(
|
|
164
|
+
"pathlib.Path.mkdir"
|
|
165
|
+
), patch(
|
|
166
|
+
"pathlib.Path.exists", return_value=True
|
|
167
|
+
):
|
|
168
|
+
|
|
169
|
+
# 1. Successful creation
|
|
170
|
+
mock_conn.lookupByName.side_effect = Exception("Not found")
|
|
171
|
+
mock_conn.defineXML.return_value = Mock(UUIDString=lambda: "uuid-123")
|
|
172
|
+
cloner.create_vm(config)
|
|
173
|
+
|
|
174
|
+
# 2. VM already exists error
|
|
175
|
+
mock_conn.lookupByName.side_effect = None
|
|
176
|
+
mock_vm = Mock()
|
|
177
|
+
mock_vm.name.return_value = "test-vm"
|
|
178
|
+
mock_conn.lookupByName.return_value = mock_vm
|
|
179
|
+
with pytest.raises(RuntimeError, match="already exists"):
|
|
180
|
+
cloner.create_vm(config, replace=False)
|
|
181
|
+
|
|
182
|
+
# 3. Replace existing VM
|
|
183
|
+
cloner.delete_vm = Mock()
|
|
184
|
+
cloner.create_vm(config, replace=True)
|
|
185
|
+
assert cloner.delete_vm.called
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# --- Dashboard Mocking ---
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@pytest.mark.asyncio
|
|
192
|
+
async def test_dashboard_endpoints(monkeypatch):
|
|
193
|
+
from clonebox.dashboard import (
|
|
194
|
+
api_vms,
|
|
195
|
+
api_containers,
|
|
196
|
+
api_vms_json,
|
|
197
|
+
api_containers_json,
|
|
198
|
+
dashboard as dashboard_view,
|
|
199
|
+
)
|
|
200
|
+
from fastapi.responses import JSONResponse
|
|
201
|
+
import json
|
|
202
|
+
|
|
203
|
+
# Mock _run_clonebox
|
|
204
|
+
def mock_run(args):
|
|
205
|
+
mock_proc = MagicMock()
|
|
206
|
+
mock_proc.returncode = 0
|
|
207
|
+
if "list" in args:
|
|
208
|
+
mock_proc.stdout = json.dumps([{"name": "vm1", "state": "running", "uuid": "u1"}])
|
|
209
|
+
else:
|
|
210
|
+
mock_proc.stdout = json.dumps(
|
|
211
|
+
[{"name": "c1", "image": "img1", "status": "up", "ports": "80"}]
|
|
212
|
+
)
|
|
213
|
+
return mock_proc
|
|
214
|
+
|
|
215
|
+
monkeypatch.setattr("clonebox.dashboard._run_clonebox", mock_run)
|
|
216
|
+
|
|
217
|
+
# Call async endpoints
|
|
218
|
+
res_vms = await api_vms()
|
|
219
|
+
assert "vm1" in res_vms
|
|
220
|
+
|
|
221
|
+
res_containers = await api_containers()
|
|
222
|
+
assert "c1" in res_containers
|
|
223
|
+
|
|
224
|
+
res_vms_json = await api_vms_json()
|
|
225
|
+
assert isinstance(res_vms_json, JSONResponse)
|
|
226
|
+
|
|
227
|
+
res_containers_json = await api_containers_json()
|
|
228
|
+
assert isinstance(res_containers_json, JSONResponse)
|
|
229
|
+
|
|
230
|
+
res_dash = await dashboard_view()
|
|
231
|
+
assert "CloneBox Dashboard" in res_dash
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_dashboard_error_paths(monkeypatch):
|
|
235
|
+
from clonebox.dashboard import api_vms, _run_clonebox
|
|
236
|
+
import json
|
|
237
|
+
|
|
238
|
+
def mock_run_fail(args):
|
|
239
|
+
mock_proc = MagicMock()
|
|
240
|
+
mock_proc.returncode = 1
|
|
241
|
+
mock_proc.stderr = "error"
|
|
242
|
+
mock_proc.stdout = ""
|
|
243
|
+
return mock_proc
|
|
244
|
+
|
|
245
|
+
monkeypatch.setattr("clonebox.dashboard._run_clonebox", mock_run_fail)
|
|
246
|
+
|
|
247
|
+
import asyncio
|
|
248
|
+
|
|
249
|
+
res = asyncio.run(api_vms())
|
|
250
|
+
assert "clonebox list failed" in res
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_dashboard_run(monkeypatch):
|
|
254
|
+
from clonebox.dashboard import run_dashboard
|
|
255
|
+
import sys
|
|
256
|
+
|
|
257
|
+
mock_uvicorn = MagicMock()
|
|
258
|
+
monkeypatch.setitem(sys.modules, "uvicorn", mock_uvicorn)
|
|
259
|
+
run_dashboard(port=1234)
|
|
260
|
+
assert mock_uvicorn.run.called
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_cloner_cloudinit_generation():
|
|
264
|
+
with patch("clonebox.cloner.libvirt") as mock_libvirt:
|
|
265
|
+
mock_conn = Mock()
|
|
266
|
+
mock_libvirt.open.return_value = mock_conn
|
|
267
|
+
cloner = SelectiveVMCloner()
|
|
268
|
+
|
|
269
|
+
config = VMConfig(
|
|
270
|
+
name="test-vm",
|
|
271
|
+
packages=["vim"],
|
|
272
|
+
snap_packages=["chromium"],
|
|
273
|
+
services=["docker"],
|
|
274
|
+
paths={"/tmp": "/mnt/tmp"},
|
|
275
|
+
gui=True,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
279
|
+
vm_dir = Path(tmpdir)
|
|
280
|
+
# Mock Path.exists for the host path in config.paths
|
|
281
|
+
with patch("pathlib.Path.exists", return_value=True):
|
|
282
|
+
iso_path = cloner._create_cloudinit_iso(vm_dir, config)
|
|
283
|
+
assert iso_path is not None
|
|
284
|
+
assert (vm_dir / "cloud-init" / "user-data").exists()
|
|
285
|
+
assert (vm_dir / "cloud-init" / "meta-data").exists()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_cloner_delete_vm_branches():
|
|
289
|
+
with patch("clonebox.cloner.libvirt") as mock_libvirt:
|
|
290
|
+
mock_conn = Mock()
|
|
291
|
+
mock_libvirt.open.return_value = mock_conn
|
|
292
|
+
cloner = SelectiveVMCloner()
|
|
293
|
+
|
|
294
|
+
mock_vm = Mock()
|
|
295
|
+
mock_vm.isActive.return_value = True
|
|
296
|
+
mock_conn.lookupByName.return_value = mock_vm
|
|
297
|
+
|
|
298
|
+
with patch.object(cloner, "get_images_dir", return_value=Path("/tmp")):
|
|
299
|
+
cloner.delete_vm("test-vm", delete_storage=True)
|
|
300
|
+
assert mock_vm.destroy.called
|
|
301
|
+
assert mock_vm.undefine.called
|
|
302
|
+
from clonebox.detector import SystemDetector
|
|
303
|
+
|
|
304
|
+
detector = SystemDetector()
|
|
305
|
+
|
|
306
|
+
# Mock subprocess.run to return expected outputs for various commands
|
|
307
|
+
def mock_run(args, **kwargs):
|
|
308
|
+
cmd = " ".join(args) if isinstance(args, list) else args
|
|
309
|
+
if "systemctl list-units" in cmd:
|
|
310
|
+
return Mock(
|
|
311
|
+
stdout="docker.service loaded active running\nnginx.service loaded active running",
|
|
312
|
+
returncode=0,
|
|
313
|
+
)
|
|
314
|
+
if "ps -eo" in cmd:
|
|
315
|
+
return Mock(stdout="1234 100.0 python3\n5678 200.0 node", returncode=0)
|
|
316
|
+
if "docker ps" in cmd:
|
|
317
|
+
return Mock(
|
|
318
|
+
stdout="container1\timage1\tUp 1 hour\ncontainer2\timage2\tUp 2 hours", returncode=0
|
|
319
|
+
)
|
|
320
|
+
if "hostnamectl" in cmd:
|
|
321
|
+
return Mock(
|
|
322
|
+
stdout="Static hostname: test-host\nOperating System: Ubuntu 22.04.3 LTS",
|
|
323
|
+
returncode=0,
|
|
324
|
+
)
|
|
325
|
+
if "du -sm" in cmd:
|
|
326
|
+
return Mock(stdout="100\t/some/path", returncode=0)
|
|
327
|
+
return Mock(stdout="", returncode=0)
|
|
328
|
+
|
|
329
|
+
with patch("subprocess.run", side_effect=mock_run):
|
|
330
|
+
detector.detect_services()
|
|
331
|
+
detector.detect_applications()
|
|
332
|
+
detector.detect_docker_containers()
|
|
333
|
+
detector.get_system_info()
|
|
334
|
+
detector.detect_all()
|
|
335
|
+
# Internal method name is _get_dir_size
|
|
336
|
+
detector._get_dir_size(Path("/some/path"))
|
|
337
|
+
|
|
338
|
+
# Cover exception branches
|
|
339
|
+
with patch("subprocess.run", side_effect=Exception("error")):
|
|
340
|
+
detector._get_dir_size(Path("/error/path"))
|
|
341
|
+
detector.detect_docker_containers()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def test_models_additional():
|
|
345
|
+
from clonebox.models import VMSettings, CloneBoxConfig, ContainerConfig
|
|
346
|
+
|
|
347
|
+
# VMSettings validation branches
|
|
348
|
+
with pytest.raises(Exception):
|
|
349
|
+
VMSettings(name="")
|
|
350
|
+
VMSettings(network_mode="user", ram_mb=1024)
|
|
351
|
+
|
|
352
|
+
# CloneBoxConfig methods
|
|
353
|
+
c = CloneBoxConfig()
|
|
354
|
+
_ = c.model_dump()
|
|
355
|
+
|
|
356
|
+
# ContainerConfig ports coercion
|
|
357
|
+
cc = ContainerConfig(ports={"8080": "80"})
|
|
358
|
+
assert "8080:80" in cc.ports
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def test_profiles_additional():
|
|
362
|
+
from clonebox.profiles import load_profile, merge_with_profile
|
|
363
|
+
|
|
364
|
+
# Cover load_profile branches
|
|
365
|
+
with patch("clonebox.profiles.pkgutil.get_data", return_value=None):
|
|
366
|
+
assert load_profile("nonexistent", []) is None
|
|
367
|
+
|
|
368
|
+
# Cover load_profile with existing file
|
|
369
|
+
with patch("pathlib.Path.exists", return_value=True), patch(
|
|
370
|
+
"pathlib.Path.read_text", return_value="key: value"
|
|
371
|
+
):
|
|
372
|
+
assert load_profile("exists", []) == {"key": "value"}
|
|
373
|
+
|
|
374
|
+
# Cover load_profile with pkgutil data
|
|
375
|
+
with patch("pathlib.Path.exists", return_value=False), patch(
|
|
376
|
+
"clonebox.profiles.pkgutil.get_data", return_value=b"key: pkg"
|
|
377
|
+
):
|
|
378
|
+
assert load_profile("pkg", []) == {"key": "pkg"}
|
|
379
|
+
|
|
380
|
+
# Cover _deep_merge
|
|
381
|
+
from clonebox.profiles import _deep_merge
|
|
382
|
+
|
|
383
|
+
base = {"a": {"b": 1}, "c": 2}
|
|
384
|
+
override = {"a": {"d": 3}, "c": 4}
|
|
385
|
+
assert _deep_merge(base, override) == {"a": {"b": 1, "d": 3}, "c": 4}
|
|
386
|
+
|
|
387
|
+
# Cover merge_with_profile
|
|
388
|
+
# merge_with_profile returns base_config if profile_name is empty
|
|
389
|
+
assert merge_with_profile({"a": 1}, "") == {"a": 1}
|
|
390
|
+
# merge_with_profile returns base_config if profile_name is None
|
|
391
|
+
assert merge_with_profile({"a": 1}, None) == {"a": 1}
|
|
392
|
+
# merge_with_profile with invalid profile (not a dict)
|
|
393
|
+
assert merge_with_profile({"a": 1}, profile="invalid") == {"a": 1}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|