chipwright 0.2.0__py3-none-any.whl
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.
- chipwright/__init__.py +11 -0
- chipwright/adapt.py +57 -0
- chipwright/build.py +329 -0
- chipwright/cache.py +65 -0
- chipwright/cli.py +242 -0
- chipwright/config.py +106 -0
- chipwright/data/behavior_prompts.yaml +108 -0
- chipwright/data/index.yaml +327 -0
- chipwright/device.py +143 -0
- chipwright/device_adb.py +121 -0
- chipwright/env.py +229 -0
- chipwright/errors.py +41 -0
- chipwright/llm_reference.py +114 -0
- chipwright/modalities/__init__.py +3 -0
- chipwright/modalities/asr_encoder.py +90 -0
- chipwright/modalities/cv.py +34 -0
- chipwright/modalities/llm.py +300 -0
- chipwright/recipe.py +205 -0
- chipwright/registry.py +65 -0
- chipwright/resolver.py +197 -0
- chipwright/run.py +173 -0
- chipwright/sdk.py +140 -0
- chipwright/tags.py +56 -0
- chipwright/task_eval.py +190 -0
- chipwright/tests/conftest.py +12 -0
- chipwright/tests/test_adapt.py +61 -0
- chipwright/tests/test_build.py +279 -0
- chipwright/tests/test_cache.py +147 -0
- chipwright/tests/test_device_adb.py +140 -0
- chipwright/tests/test_env.py +73 -0
- chipwright/tests/test_env_adb.py +120 -0
- chipwright/tests/test_llm_modality.py +259 -0
- chipwright/tests/test_packaging.py +147 -0
- chipwright/tests/test_precision_support.py +128 -0
- chipwright/tests/test_registry.py +107 -0
- chipwright/tests/test_resolver.py +93 -0
- chipwright/tests/test_run_llm.py +47 -0
- chipwright/tests/test_tags.py +95 -0
- chipwright/tests/test_task_eval.py +164 -0
- chipwright/tests/test_verify.py +92 -0
- chipwright/tests/test_verify_llm.py +244 -0
- chipwright/verify.py +24 -0
- chipwright/verify_llm.py +203 -0
- chipwright-0.2.0.dist-info/METADATA +268 -0
- chipwright-0.2.0.dist-info/RECORD +49 -0
- chipwright-0.2.0.dist-info/WHEEL +5 -0
- chipwright-0.2.0.dist-info/entry_points.txt +2 -0
- chipwright-0.2.0.dist-info/licenses/LICENSE +21 -0
- chipwright-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Content-addressed cache — sharded paths, hash of a file, membership, and fetch verification.
|
|
2
|
+
|
|
3
|
+
No network: the one fetch test replaces urllib.request.urlretrieve with a local writer. CACHE_DIR is
|
|
4
|
+
redirected to a temp dir so the real ~/.cache/chipwright is never touched. Runnable as
|
|
5
|
+
`python3 test_cache.py`.
|
|
6
|
+
"""
|
|
7
|
+
import hashlib
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import urllib.request
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
14
|
+
|
|
15
|
+
from chipwright import cache
|
|
16
|
+
|
|
17
|
+
# a valid-looking sha256 (64 hex chars) that nothing in the cache will match
|
|
18
|
+
_RANDOM_SHA = "0" * 63 + "1"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_path_for_is_content_addressed_and_sharded():
|
|
22
|
+
sha = "ab" + "c" * 62
|
|
23
|
+
p = cache.path_for(sha)
|
|
24
|
+
# sharded by the first two chars, then the full digest as the filename
|
|
25
|
+
assert os.path.basename(p) == sha
|
|
26
|
+
assert os.path.basename(os.path.dirname(p)) == "ab"
|
|
27
|
+
assert os.path.dirname(os.path.dirname(p)).endswith(os.path.join("blobs"))
|
|
28
|
+
# same input -> same path (deterministic, no randomness)
|
|
29
|
+
assert cache.path_for(sha) == p
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_sha256_matches_hashlib():
|
|
33
|
+
payload = b"chipwright content-addressed blob \x00\x01\x02"
|
|
34
|
+
with tempfile.NamedTemporaryFile(delete=False) as f:
|
|
35
|
+
f.write(payload)
|
|
36
|
+
tmp = f.name
|
|
37
|
+
try:
|
|
38
|
+
assert cache._sha256(tmp) == hashlib.sha256(payload).hexdigest()
|
|
39
|
+
finally:
|
|
40
|
+
os.remove(tmp)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_have_false_for_random_hash():
|
|
44
|
+
with _temp_cache():
|
|
45
|
+
assert cache.have(_RANDOM_SHA) is False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_fetch_with_wrong_sha_raises_and_leaves_no_file():
|
|
49
|
+
payload = b"the actual downloaded bytes"
|
|
50
|
+
wrong_sha = "f" * 64 # deliberately not the sha of `payload`
|
|
51
|
+
assert hashlib.sha256(payload).hexdigest() != wrong_sha
|
|
52
|
+
|
|
53
|
+
def _fake_urlretrieve(url, filename):
|
|
54
|
+
with open(filename, "wb") as fh:
|
|
55
|
+
fh.write(payload)
|
|
56
|
+
return filename, None
|
|
57
|
+
|
|
58
|
+
with _temp_cache():
|
|
59
|
+
orig = urllib.request.urlretrieve
|
|
60
|
+
urllib.request.urlretrieve = _fake_urlretrieve
|
|
61
|
+
try:
|
|
62
|
+
raised = False
|
|
63
|
+
try:
|
|
64
|
+
cache.fetch("http://example.invalid/blob", wrong_sha)
|
|
65
|
+
except RuntimeError as e:
|
|
66
|
+
raised = True
|
|
67
|
+
assert "mismatch" in str(e)
|
|
68
|
+
assert raised, "expected RuntimeError on sha mismatch"
|
|
69
|
+
dst = cache.path_for(wrong_sha)
|
|
70
|
+
# neither the final blob nor the .part temp survives a failed verify
|
|
71
|
+
assert not os.path.exists(dst)
|
|
72
|
+
assert not os.path.exists(dst + ".part")
|
|
73
|
+
finally:
|
|
74
|
+
urllib.request.urlretrieve = orig
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_fetch_with_correct_sha_stores_and_is_have():
|
|
78
|
+
payload = b"a correctly-hashed blob"
|
|
79
|
+
good_sha = hashlib.sha256(payload).hexdigest()
|
|
80
|
+
|
|
81
|
+
def _fake_urlretrieve(url, filename):
|
|
82
|
+
with open(filename, "wb") as fh:
|
|
83
|
+
fh.write(payload)
|
|
84
|
+
return filename, None
|
|
85
|
+
|
|
86
|
+
with _temp_cache():
|
|
87
|
+
orig = urllib.request.urlretrieve
|
|
88
|
+
urllib.request.urlretrieve = _fake_urlretrieve
|
|
89
|
+
try:
|
|
90
|
+
path = cache.fetch("http://example.invalid/blob", good_sha)
|
|
91
|
+
assert path == cache.path_for(good_sha)
|
|
92
|
+
assert os.path.exists(path)
|
|
93
|
+
assert cache.have(good_sha) is True
|
|
94
|
+
# a second fetch is a cache hit (no re-download needed)
|
|
95
|
+
assert cache.fetch("http://example.invalid/blob", good_sha) == path
|
|
96
|
+
finally:
|
|
97
|
+
urllib.request.urlretrieve = orig
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_fetch_named_keeps_filename_and_fetches_sidecar():
|
|
101
|
+
"""A run reference fetches with its ORIGINAL filename (so an ONNX's `.data` sidecar resolves), and
|
|
102
|
+
declared sidecars land in the same dir."""
|
|
103
|
+
onnx = b"onnx graph bytes (external-data)"
|
|
104
|
+
data = b"the external weight data"
|
|
105
|
+
onnx_sha = hashlib.sha256(onnx).hexdigest()
|
|
106
|
+
data_sha = hashlib.sha256(data).hexdigest()
|
|
107
|
+
payloads = {"http://x/m.onnx": onnx, "http://x/m.data": data}
|
|
108
|
+
|
|
109
|
+
def _fake(url, filename):
|
|
110
|
+
with open(filename, "wb") as fh:
|
|
111
|
+
fh.write(payloads[url])
|
|
112
|
+
return filename, None
|
|
113
|
+
|
|
114
|
+
with _temp_cache():
|
|
115
|
+
orig = urllib.request.urlretrieve
|
|
116
|
+
urllib.request.urlretrieve = _fake
|
|
117
|
+
try:
|
|
118
|
+
p = cache.fetch_named("http://x/m.onnx", onnx_sha, "m.onnx",
|
|
119
|
+
sidecars=[("http://x/m.data", data_sha, "m.data")])
|
|
120
|
+
assert os.path.basename(p) == "m.onnx" # keeps its name, not the hash
|
|
121
|
+
assert os.path.exists(os.path.join(os.path.dirname(p), "m.data")) # sidecar beside it
|
|
122
|
+
assert cache._sha256(p) == onnx_sha
|
|
123
|
+
finally:
|
|
124
|
+
urllib.request.urlretrieve = orig
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class _temp_cache:
|
|
128
|
+
"""Context manager: point cache.CACHE_DIR at a fresh temp dir, restore on exit."""
|
|
129
|
+
|
|
130
|
+
def __enter__(self):
|
|
131
|
+
self._orig = cache.CACHE_DIR
|
|
132
|
+
self._dir = tempfile.mkdtemp(prefix="cw-cache-test-")
|
|
133
|
+
cache.CACHE_DIR = self._dir
|
|
134
|
+
return self._dir
|
|
135
|
+
|
|
136
|
+
def __exit__(self, *exc):
|
|
137
|
+
cache.CACHE_DIR = self._orig
|
|
138
|
+
import shutil
|
|
139
|
+
shutil.rmtree(self._dir, ignore_errors=True)
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
fns = [f for n, f in sorted(globals().items()) if n.startswith("test_")]
|
|
145
|
+
for f in fns:
|
|
146
|
+
f(); print("ok", f.__name__)
|
|
147
|
+
print(f"\n{len(fns)} passed")
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""AdbDevice — the adb transport, with `adb` MOCKED (shutil.which + subprocess.run swapped).
|
|
2
|
+
|
|
3
|
+
No board and no `adb` binary needed: the suite runs green on a laptop with nothing installed. Also
|
|
4
|
+
runnable as `python3 test_device_adb.py`. Mirrors test_build.py's mock discipline (swap the module's
|
|
5
|
+
`shutil.which` / `subprocess.run`, restore in finally).
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
13
|
+
|
|
14
|
+
from chipwright import device_adb
|
|
15
|
+
from chipwright.device_adb import AdbDevice
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _fake_run(returncode=0, stdout="", stderr=""):
|
|
19
|
+
"""A subprocess.run replacement that records argv and returns a canned CompletedProcess."""
|
|
20
|
+
calls = []
|
|
21
|
+
|
|
22
|
+
def run(argv, capture_output=True, text=True, timeout=None):
|
|
23
|
+
calls.append(argv)
|
|
24
|
+
return subprocess.CompletedProcess(argv, returncode, stdout, stderr)
|
|
25
|
+
|
|
26
|
+
return run, calls
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _patch(which=lambda n, path=None: "/usr/bin/adb", run=None):
|
|
30
|
+
"""Swap device_adb's shutil.which / subprocess.run / time.sleep; return a restore() callable."""
|
|
31
|
+
orig = (device_adb.shutil.which, device_adb.subprocess.run, device_adb.time.sleep)
|
|
32
|
+
device_adb.shutil.which = which
|
|
33
|
+
if run is not None:
|
|
34
|
+
device_adb.subprocess.run = run
|
|
35
|
+
device_adb.time.sleep = lambda *a, **k: None # never sleep on a retry in tests
|
|
36
|
+
|
|
37
|
+
def restore():
|
|
38
|
+
device_adb.shutil.which, device_adb.subprocess.run, device_adb.time.sleep = orig
|
|
39
|
+
|
|
40
|
+
return restore
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_require_adb_refuses_by_name_when_missing():
|
|
44
|
+
restore = _patch(which=lambda n, path=None: None)
|
|
45
|
+
try:
|
|
46
|
+
AdbDevice()
|
|
47
|
+
except RuntimeError as e:
|
|
48
|
+
assert "adb" in str(e) and "platform-tools" in str(e)
|
|
49
|
+
else:
|
|
50
|
+
raise AssertionError("AdbDevice() must refuse when `adb` is absent")
|
|
51
|
+
finally:
|
|
52
|
+
restore()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_shell_returns_completed_process():
|
|
56
|
+
run, calls = _fake_run(returncode=0, stdout="hello\n")
|
|
57
|
+
restore = _patch(run=run)
|
|
58
|
+
try:
|
|
59
|
+
dev = AdbDevice(serial="ABC123")
|
|
60
|
+
r = dev.shell("echo hello")
|
|
61
|
+
assert r.returncode == 0 and r.stdout == "hello\n"
|
|
62
|
+
# the serial is pinned into the argv and `shell` is the verb
|
|
63
|
+
assert calls[-1][:4] == ["adb", "-s", "ABC123", "shell"]
|
|
64
|
+
finally:
|
|
65
|
+
restore()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_push_file_one_file_pushes():
|
|
69
|
+
run, calls = _fake_run(returncode=0)
|
|
70
|
+
restore = _patch(run=run)
|
|
71
|
+
try:
|
|
72
|
+
with tempfile.NamedTemporaryFile("w", suffix=".bin", delete=False) as f:
|
|
73
|
+
f.write("x")
|
|
74
|
+
local = f.name
|
|
75
|
+
dev = AdbDevice()
|
|
76
|
+
remote = dev.push_file(local, "/data/local/tmp/chipwright/a.bin")
|
|
77
|
+
assert remote == "/data/local/tmp/chipwright/a.bin"
|
|
78
|
+
# exactly one `adb push` happened (the mkdir of the parent is a `shell`, not a push)
|
|
79
|
+
pushes = [c for c in calls if "push" in c]
|
|
80
|
+
assert len(pushes) == 1 and local in pushes[0]
|
|
81
|
+
os.unlink(local)
|
|
82
|
+
finally:
|
|
83
|
+
restore()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_push_file_refuses_a_directory():
|
|
87
|
+
run, _ = _fake_run(returncode=0)
|
|
88
|
+
restore = _patch(run=run)
|
|
89
|
+
try:
|
|
90
|
+
dev = AdbDevice()
|
|
91
|
+
with tempfile.TemporaryDirectory() as d:
|
|
92
|
+
try:
|
|
93
|
+
dev.push_file(d, "/data/local/tmp/chipwright/dir")
|
|
94
|
+
except ValueError as e:
|
|
95
|
+
assert "directory" in str(e) and "wedge" in str(e)
|
|
96
|
+
else:
|
|
97
|
+
raise AssertionError("push_file must refuse a directory (it wedges the adb daemon)")
|
|
98
|
+
finally:
|
|
99
|
+
restore()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_push_file_raises_on_adb_failure():
|
|
103
|
+
run, _ = _fake_run(returncode=1, stderr="no space left")
|
|
104
|
+
restore = _patch(run=run)
|
|
105
|
+
try:
|
|
106
|
+
with tempfile.NamedTemporaryFile("w", delete=False) as f:
|
|
107
|
+
f.write("x")
|
|
108
|
+
local = f.name
|
|
109
|
+
dev = AdbDevice()
|
|
110
|
+
try:
|
|
111
|
+
dev.push_file(local, "/data/local/tmp/x.bin")
|
|
112
|
+
except RuntimeError as e:
|
|
113
|
+
assert "adb push failed" in str(e) and "no space left" in str(e)
|
|
114
|
+
else:
|
|
115
|
+
raise AssertionError("a non-zero adb push must raise, naming the failure")
|
|
116
|
+
os.unlink(local)
|
|
117
|
+
finally:
|
|
118
|
+
restore()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_exists_parses_ok_marker():
|
|
122
|
+
run_true, _ = _fake_run(returncode=0, stdout="OK\n")
|
|
123
|
+
restore = _patch(run=run_true)
|
|
124
|
+
try:
|
|
125
|
+
assert AdbDevice().exists("/data/local/tmp/present") is True
|
|
126
|
+
finally:
|
|
127
|
+
restore()
|
|
128
|
+
run_false, _ = _fake_run(returncode=0, stdout="") # command ran, no OK => absent
|
|
129
|
+
restore = _patch(run=run_false)
|
|
130
|
+
try:
|
|
131
|
+
assert AdbDevice().exists("/data/local/tmp/absent") is False
|
|
132
|
+
finally:
|
|
133
|
+
restore()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
fns = [f for n, f in sorted(globals().items()) if n.startswith("test_")]
|
|
138
|
+
for f in fns:
|
|
139
|
+
f(); print("ok", f.__name__)
|
|
140
|
+
print(f"\n{len(fns)} passed")
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""TargetKey — the pure parts of the board descriptor (no probe(), no board).
|
|
2
|
+
|
|
3
|
+
Covers manual() spec parsing, str() formatting, and the SOC_ID lookup table. probe() is deliberately
|
|
4
|
+
NOT exercised here; it needs a board over SSH/ADB. Also runnable as `python3 test_env.py`.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
10
|
+
|
|
11
|
+
from chipwright.env import SOC_ID, TargetKey
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_manual_parses_arch_and_qairt():
|
|
15
|
+
t = TargetKey.manual("htpv68,qnn2.37")
|
|
16
|
+
assert t.htp_arch == "v68"
|
|
17
|
+
assert t.qairt == "2.37"
|
|
18
|
+
# a hand-typed target is offline by construction
|
|
19
|
+
assert t.reachable is False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_manual_tolerates_whitespace_and_order():
|
|
23
|
+
t = TargetKey.manual(" qnn2.38.0 , htpv73 ")
|
|
24
|
+
assert t.htp_arch == "v73"
|
|
25
|
+
assert t.qairt == "2.38.0"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_manual_arch_only_leaves_qairt_none():
|
|
29
|
+
t = TargetKey.manual("htpv66")
|
|
30
|
+
assert t.htp_arch == "v66"
|
|
31
|
+
assert t.qairt is None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_manual_without_arch_raises():
|
|
35
|
+
for bad in ["qnn2.37", "", "soc498"]:
|
|
36
|
+
try:
|
|
37
|
+
TargetKey.manual(bad)
|
|
38
|
+
except ValueError:
|
|
39
|
+
continue
|
|
40
|
+
raise AssertionError(f"expected ValueError for {bad!r}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_str_formats_arch_sdk_and_soc():
|
|
44
|
+
# arch only
|
|
45
|
+
assert str(TargetKey(htp_arch="v68")) == "htpv68"
|
|
46
|
+
# arch + qairt
|
|
47
|
+
assert str(TargetKey(htp_arch="v68", qairt="2.37.1")) == "htpv68, qnn2.37.1"
|
|
48
|
+
# arch + qairt + soc name
|
|
49
|
+
t = TargetKey(htp_arch="v68", qairt="2.37.1", soc_name="QCS6490 / RB3 Gen2")
|
|
50
|
+
assert str(t) == "htpv68, qnn2.37.1 [QCS6490 / RB3 Gen2]"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_soc_id_map_resolves_498_to_qcs6490_v68():
|
|
54
|
+
name, arch = SOC_ID[498]
|
|
55
|
+
assert "QCS6490" in name
|
|
56
|
+
assert arch == "v68"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_soc_id_map_covers_known_boards():
|
|
60
|
+
# the lookup returns a (name, arch) pair for each known SoC id
|
|
61
|
+
for soc_id in (498, 35, 30, 57, 69):
|
|
62
|
+
name, arch = SOC_ID[soc_id]
|
|
63
|
+
assert isinstance(name, str) and name
|
|
64
|
+
assert arch.startswith("v")
|
|
65
|
+
# an unknown id yields the caller's default via .get()
|
|
66
|
+
assert SOC_ID.get(9999, (None, "unknown")) == (None, "unknown")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
fns = [f for n, f in sorted(globals().items()) if n.startswith("test_")]
|
|
71
|
+
for f in fns:
|
|
72
|
+
f(); print("ok", f.__name__)
|
|
73
|
+
print(f"\n{len(fns)} passed")
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""env.py — the adb probe parse, the SM8750->v79 SoC mapping, and skel-load detection.
|
|
2
|
+
|
|
3
|
+
All offline: `probe_adb` is exercised with `subprocess.run` MOCKED (no board, no `adb`); the skel
|
|
4
|
+
detector takes a plain `shell` callable so it needs nothing at all. Also runnable as
|
|
5
|
+
`python3 test_env_adb.py`.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
12
|
+
|
|
13
|
+
from chipwright import env
|
|
14
|
+
from chipwright.env import SOC_ID, SkelCheck, detect_skel_load, probe_adb
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# --- SoC table: SM8750 -> v79 (FR-L05, the one arch that was still missing) ----------------------
|
|
18
|
+
def test_soc_id_maps_sm8750_to_v79():
|
|
19
|
+
name, arch = SOC_ID[618]
|
|
20
|
+
assert "SM8750" in name
|
|
21
|
+
assert arch == "v79"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_soc_table_now_covers_v73_v79_v81():
|
|
25
|
+
archs = {arch for _name, arch in SOC_ID.values()}
|
|
26
|
+
for a in ("v73", "v79", "v81"):
|
|
27
|
+
assert a in archs, f"{a} missing from SOC_ID"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# --- probe_adb: parse the on-device echo block into a TargetKey ----------------------------------
|
|
31
|
+
def _fake_adb(stdout, returncode=0):
|
|
32
|
+
def run(argv, capture_output=True, text=True, timeout=None):
|
|
33
|
+
return subprocess.CompletedProcess(argv, returncode, stdout, "")
|
|
34
|
+
return run
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _patch_run(run):
|
|
38
|
+
orig = env.subprocess.run
|
|
39
|
+
env.subprocess.run = run
|
|
40
|
+
return lambda: setattr(env.subprocess, "run", orig)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_probe_adb_parses_sm8750_board():
|
|
44
|
+
out = ("SOC=618\n"
|
|
45
|
+
"NETRUN=/usr/bin/qnn-net-run\n"
|
|
46
|
+
"LIBS=libQnnHtpV79.so libQnnHtpV79Stub.so\n"
|
|
47
|
+
"QAIRT=2.38.0\n"
|
|
48
|
+
"RAM=16219136\n"
|
|
49
|
+
"TEMP=41200\n")
|
|
50
|
+
restore = _patch_run(_fake_adb(out))
|
|
51
|
+
try:
|
|
52
|
+
t = probe_adb(serial="PHONE1")
|
|
53
|
+
assert t.reachable is True
|
|
54
|
+
assert t.soc_id == 618
|
|
55
|
+
assert t.htp_arch == "v79"
|
|
56
|
+
assert "SM8750" in t.soc_name
|
|
57
|
+
assert t.qairt == "2.38.0"
|
|
58
|
+
assert t.ram_kb == 16219136
|
|
59
|
+
assert t.thermal_c == 41.2
|
|
60
|
+
finally:
|
|
61
|
+
restore()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_probe_adb_arch_falls_back_to_lib_name_for_unknown_soc():
|
|
65
|
+
# an soc_id not in the table, but a single HTP lib on the board -> arch recovered from the lib name
|
|
66
|
+
out = "SOC=99999\nLIBS=libQnnHtpV79.so\nQAIRT=2.38.0\nRAM=8000000\nTEMP=\n"
|
|
67
|
+
restore = _patch_run(_fake_adb(out))
|
|
68
|
+
try:
|
|
69
|
+
t = probe_adb()
|
|
70
|
+
assert t.htp_arch == "v79"
|
|
71
|
+
assert t.soc_name is None # unknown soc id -> no marketing name
|
|
72
|
+
finally:
|
|
73
|
+
restore()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_probe_adb_unreachable_is_offline_not_error():
|
|
77
|
+
def boom(argv, capture_output=True, text=True, timeout=None):
|
|
78
|
+
raise FileNotFoundError("adb not found")
|
|
79
|
+
restore = _patch_run(boom)
|
|
80
|
+
try:
|
|
81
|
+
t = probe_adb()
|
|
82
|
+
assert t.reachable is False and t.htp_arch == "unknown"
|
|
83
|
+
finally:
|
|
84
|
+
restore()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# --- skel-load detection: a locked image -> device-capability FAIL, not a hang -------------------
|
|
88
|
+
def _shell(stdout="", stderr="", returncode=0):
|
|
89
|
+
def shell(cmd, timeout=60):
|
|
90
|
+
return subprocess.CompletedProcess(cmd, returncode, stdout, stderr)
|
|
91
|
+
return shell
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_detect_skel_load_flags_error_4000():
|
|
95
|
+
sh = _shell(stderr="GraphPrepare: Failed to load skel, error 4000\n")
|
|
96
|
+
r = detect_skel_load(sh)
|
|
97
|
+
assert isinstance(r, SkelCheck)
|
|
98
|
+
assert r.ok is False and bool(r) is False
|
|
99
|
+
assert r.code == 4000
|
|
100
|
+
assert "skel" in r.reason and "device-capability" in r.reason
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_detect_skel_load_passes_on_clean_init():
|
|
104
|
+
sh = _shell(stdout="qnn-net-run version 2.38.0\nbackend initialized\n")
|
|
105
|
+
r = detect_skel_load(sh)
|
|
106
|
+
assert r.ok is True and bool(r) is True and r.code is None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_detect_skel_load_reports_unreachable_shell():
|
|
110
|
+
def shell(cmd, timeout=60):
|
|
111
|
+
raise RuntimeError("device offline")
|
|
112
|
+
r = detect_skel_load(shell)
|
|
113
|
+
assert r.ok is False and "could not run" in r.reason
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
fns = [f for n, f in sorted(globals().items()) if n.startswith("test_")]
|
|
118
|
+
for f in fns:
|
|
119
|
+
f(); print("ok", f.__name__)
|
|
120
|
+
print(f"\n{len(fns)} passed")
|