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.
Files changed (49) hide show
  1. chipwright/__init__.py +11 -0
  2. chipwright/adapt.py +57 -0
  3. chipwright/build.py +329 -0
  4. chipwright/cache.py +65 -0
  5. chipwright/cli.py +242 -0
  6. chipwright/config.py +106 -0
  7. chipwright/data/behavior_prompts.yaml +108 -0
  8. chipwright/data/index.yaml +327 -0
  9. chipwright/device.py +143 -0
  10. chipwright/device_adb.py +121 -0
  11. chipwright/env.py +229 -0
  12. chipwright/errors.py +41 -0
  13. chipwright/llm_reference.py +114 -0
  14. chipwright/modalities/__init__.py +3 -0
  15. chipwright/modalities/asr_encoder.py +90 -0
  16. chipwright/modalities/cv.py +34 -0
  17. chipwright/modalities/llm.py +300 -0
  18. chipwright/recipe.py +205 -0
  19. chipwright/registry.py +65 -0
  20. chipwright/resolver.py +197 -0
  21. chipwright/run.py +173 -0
  22. chipwright/sdk.py +140 -0
  23. chipwright/tags.py +56 -0
  24. chipwright/task_eval.py +190 -0
  25. chipwright/tests/conftest.py +12 -0
  26. chipwright/tests/test_adapt.py +61 -0
  27. chipwright/tests/test_build.py +279 -0
  28. chipwright/tests/test_cache.py +147 -0
  29. chipwright/tests/test_device_adb.py +140 -0
  30. chipwright/tests/test_env.py +73 -0
  31. chipwright/tests/test_env_adb.py +120 -0
  32. chipwright/tests/test_llm_modality.py +259 -0
  33. chipwright/tests/test_packaging.py +147 -0
  34. chipwright/tests/test_precision_support.py +128 -0
  35. chipwright/tests/test_registry.py +107 -0
  36. chipwright/tests/test_resolver.py +93 -0
  37. chipwright/tests/test_run_llm.py +47 -0
  38. chipwright/tests/test_tags.py +95 -0
  39. chipwright/tests/test_task_eval.py +164 -0
  40. chipwright/tests/test_verify.py +92 -0
  41. chipwright/tests/test_verify_llm.py +244 -0
  42. chipwright/verify.py +24 -0
  43. chipwright/verify_llm.py +203 -0
  44. chipwright-0.2.0.dist-info/METADATA +268 -0
  45. chipwright-0.2.0.dist-info/RECORD +49 -0
  46. chipwright-0.2.0.dist-info/WHEEL +5 -0
  47. chipwright-0.2.0.dist-info/entry_points.txt +2 -0
  48. chipwright-0.2.0.dist-info/licenses/LICENSE +21 -0
  49. chipwright-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,259 @@
1
+ """LLM modality pure helpers — chat-template wrap, profile-JSON parse, generation parse, mock run.
2
+
3
+ No device and no onnxruntime: the device is a tiny fake exposing push_file/shell/pull/exists.
4
+ Runnable as `python3 test_llm_modality.py`.
5
+ """
6
+ import json
7
+ import os
8
+ import sys
9
+
10
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
11
+
12
+ from chipwright.modalities.llm import (
13
+ apply_chat_template, parse_profile, parse_generation, run,
14
+ retokenize, load_bundle_tokenizer, greedy_sampler, apply_greedy_to_config,
15
+ )
16
+
17
+
18
+ # --- chat template --------------------------------------------------------------------------------
19
+
20
+ def test_chatml_wrap_has_im_markers_and_open_assistant_turn():
21
+ out = apply_chat_template("What is the capital of France?", template="chatml")
22
+ assert "<|im_start|>user\nWhat is the capital of France?<|im_end|>" in out
23
+ assert out.endswith("<|im_start|>assistant\n") # open turn so generation continues
24
+
25
+
26
+ def test_chatml_wrap_includes_system_when_given():
27
+ out = apply_chat_template("hi", template="chatml", system="You are terse.")
28
+ assert "<|im_start|>system\nYou are terse.<|im_end|>" in out
29
+
30
+
31
+ def test_raw_template_is_passthrough():
32
+ assert apply_chat_template("just text", template="raw") == "just text"
33
+
34
+
35
+ def test_unknown_template_is_refused_by_name():
36
+ try:
37
+ apply_chat_template("x", template="llama-chat")
38
+ except ValueError as e:
39
+ assert "llama-chat" in str(e)
40
+ else:
41
+ assert False, "expected ValueError for unknown template"
42
+
43
+
44
+ # --- profile JSON parse ---------------------------------------------------------------------------
45
+
46
+ def test_parse_profile_maps_latency_fields():
47
+ prof = json.dumps({
48
+ "token_generation_rate": 33.0,
49
+ "prompt_processing_rate": 292.0,
50
+ "time_to_first_token_ms": 55.0,
51
+ "init_time": 0.88,
52
+ })
53
+ lat = parse_profile(prof)
54
+ assert lat["decode_toks_per_sec"] == 33.0
55
+ assert lat["prefill_toks_per_sec"] == 292.0
56
+ assert lat["time_to_first_token_ms"] == 55.0
57
+ assert lat["model_load_s"] == 0.88
58
+
59
+
60
+ def test_parse_profile_handles_nested_and_missing_fields():
61
+ prof = {"profile": {"decode": {"rate": 30.0}}} # nested; others absent
62
+ lat = parse_profile(prof)
63
+ assert lat["decode_toks_per_sec"] == 30.0
64
+ assert lat["prefill_toks_per_sec"] is None # absent -> shown as None, not hidden
65
+
66
+
67
+ # --- generation parse -----------------------------------------------------------------------------
68
+
69
+ def test_parse_generation_extracts_text_and_token_ids():
70
+ stdout = "loading...\n[BEGIN]Paris.[END]\ntoken_ids: [40, 1138, 13]\ndone"
71
+ g = parse_generation(stdout)
72
+ assert g["text"] == "Paris."
73
+ assert g["tokens"] == [40, 1138, 13]
74
+
75
+
76
+ def test_parse_generation_missing_tokens_is_none():
77
+ g = parse_generation("[BEGIN]hello[END]")
78
+ assert g["text"] == "hello"
79
+ assert g["tokens"] is None
80
+
81
+
82
+ def test_parse_generation_without_end_marker_reads_to_eof():
83
+ # w4a16 degenerates and never emits EOS; the board kills the run on `timeout` before [END],
84
+ # so the text must be read up to EOF, not dropped as empty.
85
+ g = parse_generation("noise\n[BEGIN]: Paris is the capital of France and it keeps going")
86
+ assert g["text"] == "Paris is the capital of France and it keeps going"
87
+
88
+
89
+ # --- retokenize (text -> token ids; genie emits no ids on QAIRT 2.51) -----------------------------
90
+
91
+ class _Enc:
92
+ def __init__(self, ids): self.ids = ids
93
+
94
+
95
+ class FakeTokenizer:
96
+ """Duck-typed stand-in for a HuggingFace Tokenizer: encode(text, add_special_tokens).ids."""
97
+ def __init__(self, mapping=None): self.mapping = mapping or {}; self.calls = []
98
+ def encode(self, text, add_special_tokens=False):
99
+ self.calls.append((text, add_special_tokens))
100
+ return _Enc(self.mapping.get(text, [len(text)]))
101
+
102
+
103
+ def test_retokenize_uses_injected_tokenizer_with_special_tokens_off():
104
+ tok = FakeTokenizer({"Paris.": [40, 1138, 13]})
105
+ ids = retokenize("Paris.", tokenizer=tok)
106
+ assert ids == [40, 1138, 13]
107
+ assert tok.calls == [("Paris.", False)] # special tokens OFF — we match answer tokens
108
+
109
+
110
+ def test_retokenize_with_real_tokenizers_library_roundtrips():
111
+ # Build a tiny real tokenizer.json via the `tokenizers` library and prove the file path loads.
112
+ from tokenizers import Tokenizer
113
+ from tokenizers.models import WordLevel
114
+ from tokenizers.pre_tokenizers import Whitespace
115
+ import tempfile
116
+ vocab = {"paris": 0, "london": 1, "is": 2, "the": 3, "capital": 4, "[UNK]": 5}
117
+ t = Tokenizer(WordLevel(vocab, unk_token="[UNK]"))
118
+ t.pre_tokenizer = Whitespace()
119
+ p = os.path.join(tempfile.mkdtemp(), "tokenizer.json")
120
+ t.save(p)
121
+ ids = retokenize("paris is the capital", p)
122
+ assert ids == [0, 2, 3, 4]
123
+
124
+
125
+ def test_load_bundle_tokenizer_missing_file_fails_by_name():
126
+ try:
127
+ load_bundle_tokenizer("/no/such/tokenizer.json")
128
+ except FileNotFoundError as e:
129
+ assert "tokenizer" in str(e).lower()
130
+ else:
131
+ assert False, "expected FileNotFoundError for a missing bundle tokenizer"
132
+
133
+
134
+ def test_load_bundle_tokenizer_fails_by_name_without_library(monkeypatch=None):
135
+ # If `tokenizers` cannot be imported, the modality must fail BY NAME, not silently return no tokens.
136
+ import builtins
137
+ real_import = builtins.__import__
138
+
139
+ def blocked(name, *a, **k):
140
+ if name == "tokenizers" or name.startswith("tokenizers."):
141
+ raise ImportError("blocked for test")
142
+ return real_import(name, *a, **k)
143
+
144
+ builtins.__import__ = blocked
145
+ try:
146
+ load_bundle_tokenizer("/tmp/whatever.json")
147
+ assert False, "expected RuntimeError naming the tokenizers library"
148
+ except RuntimeError as e:
149
+ assert "tokenizers" in str(e)
150
+ finally:
151
+ builtins.__import__ = real_import
152
+
153
+
154
+ # --- greedy sampler: top-k:1, NOT sampler.greedy (genie silently ignores greedy) ------------------
155
+
156
+ def test_greedy_sampler_sets_topk_1_not_relying_on_greedy_flag():
157
+ s = greedy_sampler()
158
+ assert s["top-k"] == 1 # the control genie HONORS
159
+ assert s["temp"] == 0.0
160
+ assert s["top-p"] == 1.0
161
+
162
+
163
+ def test_apply_greedy_to_config_forces_topk1_without_mutating_input():
164
+ cfg = {"dialog": {"sampler": {"version": 1, "greedy": True, "temp": 0.8, "top-k": 40}}}
165
+ out = apply_greedy_to_config(cfg)
166
+ assert out["dialog"]["sampler"]["top-k"] == 1
167
+ assert out["dialog"]["sampler"]["temp"] == 0.0
168
+ # input is not mutated (pure) — the on-disk bundle config is never clobbered
169
+ assert cfg["dialog"]["sampler"]["top-k"] == 40
170
+
171
+
172
+ # --- mock-device run ------------------------------------------------------------------------------
173
+
174
+ class _Proc:
175
+ def __init__(self, stdout): self.returncode = 0; self.stdout = stdout
176
+
177
+
178
+ class FakeDevice:
179
+ """Minimal device fake over the (push_file/shell/pull/exists) interface."""
180
+ def __init__(self, stdout, profile=None):
181
+ self._stdout = stdout
182
+ self._profile = profile
183
+ self.pushed = []
184
+ self.shelled = []
185
+ self._present = set()
186
+ def push_file(self, local, remote):
187
+ self.pushed.append((os.path.basename(local), remote))
188
+ self._present.add(remote)
189
+ def shell(self, cmd):
190
+ self.shelled.append(cmd)
191
+ return _Proc(self._stdout)
192
+ def exists(self, remote):
193
+ return remote.endswith("profile.json") and self._profile is not None or remote in self._present
194
+ def pull(self, remote, local):
195
+ with open(local, "w") as f:
196
+ f.write(self._profile or "{}")
197
+
198
+
199
+ def test_run_pushes_bundle_file_by_file_and_parses(tmp_path=None):
200
+ import tempfile
201
+ d = tempfile.mkdtemp()
202
+ # genie_config.json must be valid JSON so the greedy patch (top-k:1) can read it.
203
+ with open(os.path.join(d, "genie_config.json"), "w") as f:
204
+ json.dump({"dialog": {"sampler": {"version": 1, "greedy": True, "temp": 0.8}}}, f)
205
+ for name in ("tokenizer.json", "context_0.bin"):
206
+ with open(os.path.join(d, name), "w") as f:
207
+ f.write("x")
208
+ stdout = "loading...\n[BEGIN]Paris.[END]" # genie emits TEXT, no token ids on QAIRT 2.51
209
+ profile = json.dumps({"token_generation_rate": 33.0, "prompt_processing_rate": 292.0})
210
+ dev = FakeDevice(stdout, profile=profile)
211
+ meta = {"template": "chatml", "genie_config": "genie_config.json",
212
+ "bundle_files": ["genie_config.json", "tokenizer.json", "context_0.bin"]}
213
+ tok = FakeTokenizer({"Paris.": [40, 1138]}) # inject: no real tokenizer.json needed
214
+
215
+ out = run(dev, d, meta, "capital of France?", tokenizer=tok, profile=True) # exercise the profile-on path
216
+
217
+ pushed = [remote for _, remote in dev.pushed]
218
+ # bundle pushed one file at a time (FR-L02: never a single multi-GB directory push)
219
+ assert sum(1 for p in pushed if p.endswith(("genie_config.json", "tokenizer.json", "context_0.bin"))) == 3
220
+ # prompt went over as a FILE (--prompt_file), not inline on the shell command line
221
+ assert any(p.endswith("prompt.txt") for p in pushed)
222
+ with open(os.path.join(d, "prompt.txt")) as f:
223
+ assert "im_start" in f.read() # chat template applied, written to the prompt file
224
+ cmd = " ".join(dev.shelled)
225
+ assert "--prompt_file" in cmd
226
+ assert "--print-token-ids" not in cmd and "--max-length" not in cmd # Bug 1: non-existent flags gone
227
+ # greedy run drives genie at a derived top-k:1 config (Bug 2), not sampler.greedy
228
+ assert any(p.endswith("genie_config.greedy.json") for p in pushed)
229
+ assert "-c genie_config.greedy.json" in cmd
230
+ with open(os.path.join(d, "genie_config.greedy.json")) as f:
231
+ assert json.load(f)["dialog"]["sampler"]["top-k"] == 1
232
+ # text parsed, then RETOKENIZED into ids for the gate
233
+ assert out["text"] == "Paris."
234
+ assert out["tokens"] == [40, 1138]
235
+ assert out["latency"]["decode_toks_per_sec"] == 33.0
236
+
237
+
238
+ def test_run_omits_profile_by_default():
239
+ """`--profile` defaults OFF: on a real SM8850/v81 board it broke genie 2.51's output and
240
+ false-FAILed a known-good artifact (token_match 0.0). Default run must not pass the flag."""
241
+ import tempfile
242
+ d = tempfile.mkdtemp()
243
+ for name in ("tokenizer.json", "genie_config.json"):
244
+ with open(os.path.join(d, name), "w") as f:
245
+ f.write("{}" if name.endswith(".json") else "x")
246
+ dev = FakeDevice("[BEGIN]Paris.[END]")
247
+ meta = {"template": "chatml", "genie_config": "genie_config.json",
248
+ "bundle_files": ["genie_config.json", "tokenizer.json"]}
249
+ out = run(dev, d, meta, "capital of France?", tokenizer=FakeTokenizer({"Paris.": [40]}))
250
+ cmd = " ".join(dev.shelled)
251
+ assert "--profile" not in cmd # default off (silicon finding)
252
+ assert out["latency"] == {} # no profile pulled/parsed when off
253
+
254
+
255
+ if __name__ == "__main__":
256
+ fns = [f for n, f in sorted(globals().items()) if n.startswith("test_")]
257
+ for f in fns:
258
+ f(); print("ok", f.__name__)
259
+ print(f"\n{len(fns)} passed")
@@ -0,0 +1,147 @@
1
+ """Packaging guards — the public registry must be portable (runnable by a stranger).
2
+
3
+ The #1 release blocker was author-local absolute paths baked into the bundled registry, which make
4
+ `chipwright run` fail for everyone but the author. These tests fail the build if that regresses.
5
+ """
6
+ import os
7
+ import sys
8
+
9
+ import pytest
10
+ import yaml
11
+
12
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
13
+
14
+ PKG = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
15
+ PUBLIC_INDEX = os.path.join(PKG, "data", "index.yaml") # bundled public index (ships in the wheel)
16
+
17
+
18
+ def test_public_index_has_no_absolute_paths():
19
+ """The bundled public registry must not carry author-local absolute paths."""
20
+ text = open(PUBLIC_INDEX).read()
21
+ offenders = [ln for ln in text.splitlines() if "/Users/" in ln or "/home/" in ln]
22
+ assert not offenders, "author-local absolute paths leaked into the public registry:\n" + "\n".join(offenders)
23
+
24
+
25
+ def test_public_index_parses_and_keeps_records():
26
+ """The public index still parses and every verified record survived the portability strip."""
27
+ doc = yaml.safe_load(open(PUBLIC_INDEX))
28
+ assert doc.get("registry_schema") == 1
29
+ models = doc.get("models", [])
30
+ assert models, "public registry has no models"
31
+ for m in models:
32
+ for v in m.get("variants", []):
33
+ # a variant must not carry a local path; it is runnable via url, or build-only, or a record.
34
+ assert "local" not in v or not str(v.get("local", "")).startswith(("/Users", "/home")), m["model"]
35
+
36
+
37
+ def test_verified_record_on_field_not_yaml_coerced():
38
+ """The verification record's device field is `on:` — which YAML 1.1 coerces to boolean `true` if
39
+ unquoted. Guard against a `true:` key sneaking back in: a record MUST name its device under "on"."""
40
+ doc = yaml.safe_load(open(PUBLIC_INDEX))
41
+ for m in doc.get("models", []):
42
+ for v in m.get("variants", []):
43
+ ver = v.get("verified")
44
+ if isinstance(ver, dict):
45
+ assert True not in ver, (
46
+ f"{m['model']}: a verified-record key coerced to boolean true (quote `on:` as \"on\")")
47
+ if any(k in ver for k in ("date", "loads")): # a real record → it names its device
48
+ assert "on" in ver, f"{m['model']}: verified record missing the `on:` device field"
49
+
50
+
51
+ def test_every_variant_is_resolvable_or_buildable():
52
+ """Each variant either hosts an artifact (url+sha256), or is build-only, or carries a record —
53
+ never a dangling local path that only the author can reach."""
54
+ doc = yaml.safe_load(open(PUBLIC_INDEX))
55
+ for m in doc.get("models", []):
56
+ for v in m.get("variants", []):
57
+ has_hosted = bool(v.get("url"))
58
+ has_record = bool(v.get("verified"))
59
+ has_recipe = bool(v.get("recipe") or m.get("recipe") or v.get("source") or m.get("source"))
60
+ assert has_hosted or has_record or has_recipe, (
61
+ f"{m['model']} variant is neither hosted, recorded, nor buildable")
62
+
63
+
64
+ def test_default_index_is_bundled_inside_the_package():
65
+ """The default index must live inside the `chipwright` package so it ships in the wheel and
66
+ resolves after a clean `pip install` — not at a repo-relative path that vanishes once installed."""
67
+ from chipwright.registry import DEFAULT_INDEX
68
+ assert os.path.exists(DEFAULT_INDEX), DEFAULT_INDEX
69
+ assert os.path.abspath(DEFAULT_INDEX).startswith(PKG + os.sep), (
70
+ f"DEFAULT_INDEX {DEFAULT_INDEX} is outside the package dir {PKG}; it won't ship in the wheel")
71
+
72
+
73
+ def test_missing_sshpass_refuses_by_name(monkeypatch):
74
+ """`sshpass` is an undeclared-by-pip system dep; a missing binary must refuse by name with the
75
+ install hint, not raise a cryptic FileNotFoundError deep in subprocess (spec 003 FR-R05)."""
76
+ from chipwright import device
77
+ monkeypatch.setattr(device.shutil, "which", lambda _: None)
78
+ with pytest.raises(RuntimeError, match="sshpass"):
79
+ device.RemoteQNN("1.2.3.4", "root", "pw", 22)
80
+
81
+
82
+ def _variant(**kw):
83
+ from chipwright.resolver import Variant
84
+ base = dict(model="x", version="1", arch="v68", quant="w8a16", shape="s",
85
+ sdk_tested="2.37.1", sdk_compatible=">=2.34,<2.40")
86
+ base.update(kw)
87
+ return Variant(**base)
88
+
89
+
90
+ def test_locate_ref_local_path_expands(monkeypatch):
91
+ """A local run reference is just expanduser'd — unchanged behaviour."""
92
+ from chipwright import run as runmod
93
+ got = runmod._locate_ref({"cpu_onnx": "~/x/model.onnx"}, "cpu_onnx")
94
+ assert got == os.path.expanduser("~/x/model.onnx")
95
+ assert runmod._locate_ref({}, "meta") is None
96
+
97
+
98
+ def test_locate_ref_url_fetches_by_sha(monkeypatch):
99
+ """A hosted (url) reference is fetched into the cache by its companion sha, keeping its filename."""
100
+ from chipwright import run as runmod, cache
101
+ calls = {}
102
+ monkeypatch.setattr(cache, "fetch_named",
103
+ lambda url, sha, name, sidecars: calls.update(url=url, sha=sha, name=name, side=sidecars) or "/c/model.onnx")
104
+ r = {"cpu_onnx": "https://h/f/model.onnx", "cpu_onnx_sha256": "abc",
105
+ "cpu_onnx_sidecars": [{"url": "https://h/f/model.data", "sha256": "def", "name": "model.data"}]}
106
+ got = runmod._locate_ref(r, "cpu_onnx")
107
+ assert got == "/c/model.onnx"
108
+ assert calls["url"].endswith("model.onnx") and calls["sha"] == "abc" and calls["name"] == "model.onnx"
109
+ assert calls["side"] == [("https://h/f/model.data", "def", "model.data")]
110
+
111
+
112
+ def test_locate_ref_url_without_sha_refuses_by_name():
113
+ """A url reference with no sha is refused — never fetch a reference the gate can't verify."""
114
+ from chipwright import run as runmod
115
+ with pytest.raises(RuntimeError, match="unverified reference|_sha256"):
116
+ runmod._locate_ref({"input": "https://h/f/input.npy"}, "input")
117
+
118
+
119
+ def test_run_refuses_when_no_artifact(monkeypatch):
120
+ """Refusal path 1 (spec 003 FR-R02 / SC-R03): a record-only variant with no hosted url and no
121
+ local path must refuse by name — pointing at `build` and CW_REGISTRY — not a raw traceback."""
122
+ from chipwright import run as runmod
123
+ v = _variant(run={"modality": "asr_encoder"}) # no url, no local
124
+ with pytest.raises(RuntimeError, match="build .* chipwright build|CW_REGISTRY"):
125
+ runmod.run_variant(v)
126
+
127
+
128
+ def test_run_refuses_when_no_cpu_reference(monkeypatch, tmp_path):
129
+ """Refusal path 2 (spec 003 FR-R03 / SC-R03): an artifact present but no CPU reference in the
130
+ active registry must refuse — the verification gate can't run, and a pass is never reported."""
131
+ from chipwright import run as runmod
132
+ art = tmp_path / "a.bin"
133
+ art.write_bytes(b"x")
134
+ v = _variant(local=str(art), run={"modality": "asr_encoder", "board_name": "fx"}) # no cpu_onnx
135
+
136
+ class _FakeDev:
137
+ def __init__(self, *a, **k):
138
+ pass
139
+
140
+ def ensure_ctx(self, _art, _name):
141
+ return "/board/ctx"
142
+
143
+ monkeypatch.setattr(runmod, "_board_env",
144
+ lambda: dict(host="h", user="root", pw="p", port=22))
145
+ monkeypatch.setattr(runmod, "RemoteQNN", _FakeDev)
146
+ with pytest.raises(RuntimeError, match="CPU reference|verification gate"):
147
+ runmod.run_variant(v)
@@ -0,0 +1,128 @@
1
+ """Precision-support matrix (FR-L06) + arch v79/v81 as first-class hard axes (FR-L05).
2
+
3
+ Pure, no board: the recipe parse/validate and the resolver refusal are both plain functions. Also
4
+ runnable as `python3 test_precision_support.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 TargetKey
12
+ from chipwright.recipe import from_dict
13
+ from chipwright.resolver import Outcome, Variant, precision_refusal, resolve
14
+
15
+
16
+ # --- recipe schema: precision_support parses + validates -----------------------------------------
17
+ _BASE = {"name": "qwen2.5-1.5b", "version": "1.0.0",
18
+ "source": {"format": "onnx", "sha256": "abc"},
19
+ "target": {"arch": "v81", "quant": "w4a16", "shape": "ctx4096"}}
20
+
21
+
22
+ def _recipe(**extra):
23
+ doc = dict(_BASE)
24
+ doc.update(extra)
25
+ return from_dict(doc)
26
+
27
+
28
+ def test_recipe_parses_precision_matrix():
29
+ r = _recipe(precision_support={
30
+ "w4a16": "supported",
31
+ "w8a16": "blocked:kv-cache-encoding",
32
+ "fp16": "unsupported:genie-engine"})
33
+ assert r.precision_status("w4a16") == "supported"
34
+ assert r.precision_status("w8a16") == "blocked:kv-cache-encoding"
35
+ assert r.supported_precisions == ["w4a16"]
36
+
37
+
38
+ def test_recipe_without_matrix_is_empty_not_error():
39
+ r = _recipe()
40
+ assert r.precision_support == {} and r.supported_precisions == []
41
+
42
+
43
+ def test_recipe_rejects_unknown_status_kind():
44
+ for bad in ({"w8a16": "maybe"}, {"w8a16": "nope:x"}):
45
+ try:
46
+ _recipe(precision_support=bad)
47
+ except ValueError:
48
+ continue
49
+ raise AssertionError(f"expected ValueError for {bad!r}")
50
+
51
+
52
+ def test_recipe_rejects_blocked_without_reason():
53
+ for bad in ({"w8a16": "blocked"}, {"w8a16": "unsupported:"}, {"w8a16": "blocked: "}):
54
+ try:
55
+ _recipe(precision_support=bad)
56
+ except ValueError:
57
+ continue
58
+ raise AssertionError(f"a blocked/unsupported status must NAME a reason: {bad!r}")
59
+
60
+
61
+ # --- the pure refusal helper ---------------------------------------------------------------------
62
+ def test_precision_refusal_helper():
63
+ support = {"w4a16": "supported", "w8a16": "blocked:kv-cache-encoding"}
64
+ assert precision_refusal("w4a16", support) is None # supported -> no refusal
65
+ assert precision_refusal("int4", support) is None # not declared -> not refused here
66
+ assert precision_refusal("w8a16", support) == "blocked:kv-cache-encoding"
67
+ assert precision_refusal("w8a16", None) is None # no matrix -> nothing to refuse
68
+
69
+
70
+ # --- resolver refuses a non-supported precision and NAMES the reason -----------------------------
71
+ _SUPPORT = {"w4a16": "supported", "w8a16": "blocked:kv-cache-encoding", "fp16": "unsupported:genie-engine"}
72
+
73
+
74
+ def _llm_variant(arch="v81", quant="w4a16", **kw):
75
+ base = dict(model="qwen2.5-1.5b", version="1.0.0", arch=arch, quant=quant, shape="ctx4096",
76
+ sdk_tested="2.38.0", sdk_compatible=">=2.34,<2.40", precision_support=_SUPPORT)
77
+ base.update(kw)
78
+ return Variant(**base)
79
+
80
+
81
+ def test_resolve_refuses_blocked_precision_by_name():
82
+ t = TargetKey(htp_arch="v81", qairt="2.38.0")
83
+ d = resolve(t, [_llm_variant(url="u", sha256="s")], want_quant="w8a16")
84
+ assert d.outcome is Outcome.FAIL and d.axis == "precision"
85
+ assert "kv-cache-encoding" in d.reason
86
+
87
+
88
+ def test_resolve_refuses_unsupported_precision_by_name():
89
+ t = TargetKey(htp_arch="v81", qairt="2.38.0")
90
+ d = resolve(t, [_llm_variant(url="u", sha256="s")], want_quant="fp16")
91
+ assert d.outcome is Outcome.FAIL and d.axis == "precision"
92
+ assert "genie-engine" in d.reason
93
+
94
+
95
+ def test_resolve_allows_supported_precision():
96
+ t = TargetKey(htp_arch="v81", qairt="2.38.0")
97
+ d = resolve(t, [_llm_variant(url="u", sha256="s")], want_quant="w4a16")
98
+ assert d.ok and d.outcome is Outcome.USE
99
+
100
+
101
+ def test_resolve_takes_explicit_matrix_over_variants():
102
+ # a caller may pass the matrix directly (e.g. straight from the recipe) — it wins
103
+ t = TargetKey(htp_arch="v81", qairt="2.38.0")
104
+ v = _llm_variant(url="u", sha256="s", precision_support=None)
105
+ d = resolve(t, [v], want_quant="w8a16", precision_support=_SUPPORT)
106
+ assert d.outcome is Outcome.FAIL and d.axis == "precision"
107
+
108
+
109
+ # --- arch v79 and v81 are first-class hard axes (not special-cased anywhere) ----------------------
110
+ def test_arch_v79_and_v81_resolve_as_hard_axis():
111
+ for arch in ("v79", "v81"):
112
+ t = TargetKey(htp_arch=arch, qairt="2.38.0")
113
+ d = resolve(t, [_llm_variant(arch=arch, url="u", sha256="s")], want_quant="w4a16")
114
+ assert d.ok, f"{arch} should USE its own artifact"
115
+
116
+
117
+ def test_arch_mismatch_v79_vs_v81_fails_on_arch_axis():
118
+ t = TargetKey(htp_arch="v79", qairt="2.38.0") # board is v79
119
+ d = resolve(t, [_llm_variant(arch="v81", url="u", sha256="s")], want_quant="w4a16")
120
+ assert d.outcome is Outcome.FAIL and d.axis == "htp_arch"
121
+ assert "v79" in d.reason
122
+
123
+
124
+ if __name__ == "__main__":
125
+ fns = [f for n, f in sorted(globals().items()) if n.startswith("test_")]
126
+ for f in fns:
127
+ f(); print("ok", f.__name__)
128
+ print(f"\n{len(fns)} passed")
@@ -0,0 +1,107 @@
1
+ """Registry — load the bundled public index (chipwright/data/index.yaml, read-only) and check Variant parsing.
2
+
3
+ Uses the real seed index so the test fails if the shipped index stops parsing. Also runnable as
4
+ `python3 test_registry.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.registry import DEFAULT_INDEX, Registry
12
+
13
+
14
+ def test_bundled_index_exists_and_loads():
15
+ assert os.path.exists(DEFAULT_INDEX), DEFAULT_INDEX
16
+ reg = Registry()
17
+ assert reg.schema == 1
18
+ models = reg.models()
19
+ for expected in ("laya-decision", "whisper-small-en", "zipformer-enin", "qwen2.5-1.5b-instruct"):
20
+ assert expected in models, expected
21
+
22
+
23
+ def test_each_model_yields_variants():
24
+ reg = Registry()
25
+ for model in reg.models():
26
+ vs = reg.variants(model)
27
+ assert vs, f"{model} has no variants"
28
+ for v in vs:
29
+ assert v.model == model
30
+ assert v.arch and v.quant and v.shape
31
+
32
+
33
+ def test_hosted_variant_has_artifact():
34
+ reg = Registry()
35
+ laya = reg.variants("laya-decision")[0]
36
+ assert laya.url is not None
37
+ assert laya.sha256 is not None
38
+ assert laya.has_artifact is True
39
+
40
+
41
+ def test_recipe_only_variant_has_recipe_not_artifact():
42
+ reg = Registry()
43
+ whisper = reg.variants("whisper-small-en")[0]
44
+ assert whisper.url is None and whisper.local is None
45
+ assert whisper.has_artifact is False
46
+ assert whisper.has_recipe is True
47
+ assert whisper.recipe.endswith(".yaml")
48
+
49
+
50
+ # The public registry is portable (no author-local paths — see spec 003), so the `local` and
51
+ # run-asset fields are exercised against a fixture registry, not the shipped index. This tests the
52
+ # PARSER capability without coupling it to machine-specific content.
53
+ _FIXTURE = """registry_schema: 1
54
+ models:
55
+ - model: fixture-model
56
+ variants:
57
+ - arch: v68
58
+ quant: w8a16
59
+ shape: win39
60
+ sdk_tested: "2.37.1"
61
+ sdk_compatible: ">=2.34,<2.40"
62
+ ext: qnnctx
63
+ local: /tmp/fixture_artifact.bin
64
+ run: {modality: asr_encoder, board_name: fx, meta: /tmp/m.json, cpu_onnx: /tmp/e.onnx, input: /tmp/i.npy, threshold: 0.95}
65
+ """
66
+
67
+
68
+ def _fixture_registry(tmp_path):
69
+ p = tmp_path / "fixture_index.yaml"
70
+ p.write_text(_FIXTURE)
71
+ return Registry(index=str(p))
72
+
73
+
74
+ def test_local_field_parses_as_on_disk_artifact(tmp_path):
75
+ v = _fixture_registry(tmp_path).variants("fixture-model")[0]
76
+ # a `local:` path counts as a present artifact even with no url
77
+ assert v.url is None
78
+ assert v.local is not None and v.local.endswith(".bin")
79
+ assert v.has_artifact is True
80
+
81
+
82
+ def test_run_field_parses_into_dict(tmp_path):
83
+ v = _fixture_registry(tmp_path).variants("fixture-model")[0]
84
+ assert isinstance(v.run, dict)
85
+ assert v.run["modality"] == "asr_encoder"
86
+ assert v.run["threshold"] == 0.95
87
+ for key in ("board_name", "meta", "cpu_onnx", "input"):
88
+ assert key in v.run
89
+
90
+
91
+ def test_verified_record_parses():
92
+ reg = Registry()
93
+ laya = reg.variants("laya-decision")[0]
94
+ assert isinstance(laya.verified, dict)
95
+ assert laya.verified["loads"] is True
96
+
97
+
98
+ def test_unknown_model_returns_empty_list():
99
+ reg = Registry()
100
+ assert reg.variants("does-not-exist") == []
101
+
102
+
103
+ if __name__ == "__main__":
104
+ fns = [f for n, f in sorted(globals().items()) if n.startswith("test_")]
105
+ for f in fns:
106
+ f(); print("ok", f.__name__)
107
+ print(f"\n{len(fns)} passed")