dsh-labscript 0.0.0-stage → 0.2.0

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.
@@ -0,0 +1,239 @@
1
+ """Self-contained tests for the python bridge core (mock backend only).
2
+
3
+ Run: python python/tests/run_tests.py
4
+ No pytest, no labscript, no hardware: numpy and h5py are enough.
5
+ """
6
+ import json
7
+ import os
8
+ import shutil
9
+ import sys
10
+ import tempfile
11
+
12
+ HERE = os.path.dirname(os.path.abspath(__file__))
13
+ sys.path.insert(0, os.path.dirname(HERE))
14
+ EXAMPLES = os.path.join(os.path.dirname(os.path.dirname(HERE)), "examples")
15
+
16
+ import bridge
17
+
18
+
19
+ def make_config(tmpdir):
20
+ return {
21
+ "backend": "mock-labscript",
22
+ "mock": {"stateDir": os.path.join(tmpdir, "mock")},
23
+ "panelHistoryDir": os.path.join(tmpdir, "history"),
24
+ "calibrationLog": os.path.join(tmpdir, "calibration.jsonl"),
25
+ "engage": {"timeoutS": 10, "pollS": 0.05, "settleS": 0.05},
26
+ "postprocess": {"entry": "postprocess_demo:analyze", "pythonPaths": [EXAMPLES]},
27
+ }
28
+
29
+
30
+ def protocol_raw():
31
+ return {
32
+ "name": "demo_scan",
33
+ "labscript": "/tmp/mock_sequence.py",
34
+ "set": {"amplitude": 0.5},
35
+ "scan": {"duration_test_us": "np.linspace(0, 2, 3)"},
36
+ "reps": 2,
37
+ "postprocess": "demo.peaks",
38
+ "finish": {
39
+ "restore": True,
40
+ "panel": {"duration_test_us": "fit.center"},
41
+ "history": {"demo.peaks.center_us": "fit.center"},
42
+ },
43
+ }
44
+
45
+
46
+ def test_scan_expressions():
47
+ assert bridge.eval_scan_expr("np.arange(0, 1, 0.25)").size == 4
48
+ assert bridge.eval_scan_expr("np.linspace(0, 2, 5)").size == 5
49
+ assert bridge.eval_scan_expr([0, 1, 2]).size == 3
50
+ assert bridge.eval_scan_expr(3).size == 1
51
+ for bad in ["os.system('x')", "__import__('os')", "np.ones(3)" ]:
52
+ try:
53
+ bridge.eval_scan_expr(bad)
54
+ except bridge.BridgeError as exc:
55
+ assert exc.code == "BAD_SCAN_EXPR", exc.message
56
+ else:
57
+ raise AssertionError("expected rejection for " + bad)
58
+
59
+
60
+ def test_mock_status_and_set():
61
+ tmp = tempfile.mkdtemp(prefix="dsh-labscript-py-")
62
+ try:
63
+ cfg = make_config(tmp)
64
+ status = bridge.cmd_status(cfg, {})
65
+ assert status["n_shots"] == 1
66
+ out = bridge.cmd_set(cfg, {"values": {"amplitude": 0.7}, "reps": 2, "note": "test"})
67
+ assert out["n_shots"] == 2
68
+ assert out["missing"] == []
69
+ assert out["snapshot"]["name"].endswith(".json")
70
+ assert out["readback"]["amplitude"] == 0.7
71
+ raw = bridge.cmd_status(cfg, {"raw": True})
72
+ assert raw["raw_globals"]["amplitude"] == "0.7"
73
+ restored = bridge.cmd_restore(cfg, {"snapshot": "latest"})
74
+ assert restored["restored_from"]["file"]
75
+ assert bridge.cmd_status(cfg, {})["n_shots"] == 1
76
+ finally:
77
+ shutil.rmtree(tmp, ignore_errors=True)
78
+
79
+
80
+ def test_protocol_end_to_end():
81
+ tmp = tempfile.mkdtemp(prefix="dsh-labscript-protocol-")
82
+ try:
83
+ cfg = make_config(tmp)
84
+ planned = bridge.cmd_protocol_plan(cfg, {"protocol": protocol_raw()})
85
+ assert planned["plan"]["expected_n_shots"] == 6
86
+ started = bridge.cmd_protocol_start(cfg, {"protocol": protocol_raw(), "note": "cal demo"})
87
+ assert started["n_shots_at_submit"] == 6
88
+ run_folder = started["engage"]["run_folder"]
89
+ waited = bridge.wait_for_run(cfg, run_folder, 6, 10)
90
+ assert waited["complete"] is True, waited
91
+ assert waited["n_h5"] == 6
92
+ final = bridge.cmd_protocol_finalize(cfg, {
93
+ "protocol": protocol_raw(),
94
+ "run_folder": run_folder,
95
+ "snapshot": started["snapshot"],
96
+ })
97
+ assert final["postprocess"]["fit"]["center"] == 2.5
98
+ assert final["restore"]["restored"] is True
99
+ panel = bridge.cmd_status(cfg, {})["globals"]
100
+ assert panel["duration_test_us"] == 2.5
101
+ assert panel["amplitude"] == 0.5
102
+ with open(cfg["calibrationLog"], "r", encoding="utf-8") as handle:
103
+ rows = [json.loads(line) for line in handle if line.strip()]
104
+ assert len(rows) == 1
105
+ assert rows[0]["key"] == "demo.peaks.center_us"
106
+ assert rows[0]["value"] == 2.5
107
+ events = os.path.join(cfg["panelHistoryDir"], "events.jsonl")
108
+ assert os.path.isfile(events)
109
+ assert len(open(events, encoding="utf-8").readlines()) >= 2
110
+ finally:
111
+ shutil.rmtree(tmp, ignore_errors=True)
112
+
113
+
114
+ def test_hard_limits():
115
+ tmp = tempfile.mkdtemp(prefix="dsh-labscript-limits-")
116
+ try:
117
+ cfg = make_config(tmp)
118
+ limits_path = os.path.join(tmp, "limits.json")
119
+ with open(limits_path, "w", encoding="utf-8") as handle:
120
+ json.dump({"limits": {"amplitude": {"max": 0.4}}, "run_limits": {"max_total_shots": 4}}, handle)
121
+ cfg["hardLimitsFile"] = limits_path
122
+ raw = protocol_raw()
123
+ try:
124
+ bridge.cmd_protocol_plan(cfg, {"protocol": raw})
125
+ except bridge.BridgeError as exc:
126
+ assert exc.code == "HARD_LIMIT_EXCEEDED", exc.message
127
+ findings = exc.detail["findings"]
128
+ assert any(item["kind"] == "over_max" for item in findings)
129
+ else:
130
+ raise AssertionError("expected hard limit rejection")
131
+ raw["set"] = {}
132
+ raw["scan"] = {"duration_test_us": "np.linspace(0, 2, 3)"}
133
+ raw["reps"] = 3
134
+ try:
135
+ bridge.cmd_protocol_plan(cfg, {"protocol": raw})
136
+ except bridge.BridgeError as exc:
137
+ findings = exc.detail["findings"]
138
+ assert any(item["kind"] == "over_max_total_shots" for item in findings)
139
+ else:
140
+ raise AssertionError("expected shot limit rejection")
141
+ finally:
142
+ shutil.rmtree(tmp, ignore_errors=True)
143
+
144
+
145
+ def test_array_values_through_set():
146
+ tmp = tempfile.mkdtemp(prefix="dsh-labscript-array-")
147
+ try:
148
+ cfg = make_config(tmp)
149
+ out = bridge.cmd_set(cfg, {"values": {"duration_test_us": [0.0, 0.5, 1.0]}, "note": "array set"})
150
+ assert out["n_shots"] == 3, out
151
+ raw = out["raw_readback"]["duration_test_us"]
152
+ assert raw.startswith("np.linspace(") or raw.startswith("np.array(["), raw
153
+ raw = bridge.cmd_status(cfg, {"raw": True})
154
+ assert raw["globals"] is not None
155
+ finally:
156
+ shutil.rmtree(tmp, ignore_errors=True)
157
+
158
+
159
+ def test_data_reading():
160
+ tmp = tempfile.mkdtemp(prefix="dsh-labscript-data-")
161
+ try:
162
+ cfg = make_config(tmp)
163
+ started = bridge.cmd_protocol_start(cfg, {"protocol": protocol_raw()})
164
+ run_folder = started["engage"]["run_folder"]
165
+ data = bridge.cmd_data(cfg, {"run_folder": run_folder, "scan": "duration_test_us"})
166
+ assert data["n_h5"] == 6
167
+ assert data["scan_table"]["n_points"] == 6
168
+ assert data["globals"]["amplitude"] == 0.5
169
+ keys = data["scan_table"]["rows"][0].keys()
170
+ assert any(str(key).startswith("demo.") for key in keys), list(keys)
171
+ finally:
172
+ shutil.rmtree(tmp, ignore_errors=True)
173
+
174
+
175
+ def test_selective_restore():
176
+ tmp = tempfile.mkdtemp(prefix="dsh-labscript-selective-")
177
+ try:
178
+ cfg = make_config(tmp)
179
+ bridge.cmd_set(cfg, {"values": {"amplitude": 0.61}})
180
+ second = bridge.cmd_set(cfg, {"values": {"do_acquire": False}})
181
+ bridge.cmd_set(cfg, {"values": {"amplitude": 0.66}})
182
+ restored = bridge.cmd_restore(cfg, {"snapshot": second["snapshot"]["name"]})
183
+ panel = bridge.cmd_status(cfg, {})["globals"]
184
+ assert restored["selective"] is True, restored
185
+ assert panel["amplitude"] == 0.66, panel
186
+ assert panel["do_acquire"] is True, panel
187
+ finally:
188
+ shutil.rmtree(tmp, ignore_errors=True)
189
+
190
+
191
+ def test_wait_diagnosis_compiled_not_executed():
192
+ import h5py
193
+ tmp = tempfile.mkdtemp(prefix="dsh-labscript-diag-")
194
+ try:
195
+ cfg = make_config(tmp)
196
+ cfg["engage"] = {"timeoutS": 5, "pollS": 0.05, "settleS": 0.1, "stallS": 0.3}
197
+ run_dir = os.path.join(tmp, "compiled_run")
198
+ os.makedirs(run_dir, exist_ok=True)
199
+ with h5py.File(os.path.join(run_dir, "shot_0000.h5"), "w") as handle:
200
+ group = handle.create_group("globals")
201
+ group.attrs["dummy_iterator"] = 0
202
+ result = bridge.wait_for_run(cfg, run_dir, 1, 4)
203
+ assert result["complete"] is False, result
204
+ assert result["diagnosis"] == "compiled_not_executed", result
205
+ assert result["executed"] is False, result
206
+ finally:
207
+ shutil.rmtree(tmp, ignore_errors=True)
208
+
209
+
210
+ def test_doctor_mock():
211
+ tmp = tempfile.mkdtemp(prefix="dsh-labscript-doctor-")
212
+ try:
213
+ cfg = make_config(tmp)
214
+ out = bridge.cmd_doctor(cfg, {})
215
+ names = dict((check["name"], check["status"]) for check in out["checks"])
216
+ assert names.get("python") == "ok", names
217
+ assert names.get("numpy") == "ok", names
218
+ assert names.get("h5py") == "ok", names
219
+ assert names.get("backend") == "ok", names
220
+ finally:
221
+ shutil.rmtree(tmp, ignore_errors=True)
222
+
223
+
224
+ def main():
225
+ tests = [value for name, value in sorted(globals().items()) if name.startswith("test_")]
226
+ failures = 0
227
+ for test in tests:
228
+ try:
229
+ test()
230
+ print("ok " + test.__name__)
231
+ except Exception as exc: # noqa: BLE001
232
+ failures += 1
233
+ print("FAIL " + test.__name__ + ": " + repr(exc))
234
+ print(str(len(tests)) + " tests, " + str(failures) + " failures")
235
+ return 1 if failures else 0
236
+
237
+
238
+ if __name__ == "__main__":
239
+ sys.exit(main())