protocol-validation-suite-framework-apis 26.7.1__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.
- Protocol_Validation_Suite_APIs/CODE_REVIEW_REPORT.md +616 -0
- Protocol_Validation_Suite_APIs/__init__.py +9 -0
- Protocol_Validation_Suite_APIs/_client.py +596 -0
- Protocol_Validation_Suite_APIs/_decorators.py +43 -0
- Protocol_Validation_Suite_APIs/_utils.py +66 -0
- Protocol_Validation_Suite_APIs/models.py +103 -0
- Protocol_Validation_Suite_APIs/paths.json +4 -0
- Protocol_Validation_Suite_APIs/protocol_validation_suite_apis.py +167 -0
- Protocol_Validation_Suite_APIs/stubs/GRPC_pb2.py +104 -0
- Protocol_Validation_Suite_APIs/stubs/GRPC_pb2_grpc.py +951 -0
- Protocol_Validation_Suite_APIs/stubs/__init__.py +0 -0
- Protocol_Validation_Suite_APIs/stubs/protos/gRPC.proto +246 -0
- protocol_validation_suite_framework_apis-26.7.1.dist-info/METADATA +116 -0
- protocol_validation_suite_framework_apis-26.7.1.dist-info/RECORD +16 -0
- protocol_validation_suite_framework_apis-26.7.1.dist-info/WHEEL +4 -0
- protocol_validation_suite_framework_apis-26.7.1.dist-info/licenses/LICENSE +53 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
# Code Review Report — Protocol_Validation_Suite_APIs
|
|
2
|
+
|
|
3
|
+
**Reviewer disposition:** by-the-book, strict.
|
|
4
|
+
**Scope:** all hand-written Python in `Protocol_Validation_Suite_APIs/`.
|
|
5
|
+
**Excluded:** auto-generated `stubs/GRPC_pb2.py` and `stubs/GRPC_pb2_grpc.py` (generated, "DO NOT EDIT"); docstring/inline-comment prose (except where a docstring asserts a wrong *type* or a contract the code contradicts).
|
|
6
|
+
|
|
7
|
+
**Files reviewed:**
|
|
8
|
+
- `__init__.py`
|
|
9
|
+
- `models.py`
|
|
10
|
+
- `protocol_validation_suite_apis.py`
|
|
11
|
+
- `_client.py`
|
|
12
|
+
- `_decorators.py`
|
|
13
|
+
- `_utils.py`
|
|
14
|
+
|
|
15
|
+
### Summary
|
|
16
|
+
|
|
17
|
+
| Severity | Count | IDs |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| 🔴 Critical | 5 | C1–C5 |
|
|
20
|
+
| 🟠 High | 8 | C6–C13 |
|
|
21
|
+
| 🟡 Medium | 17 | S1–S17 |
|
|
22
|
+
|
|
23
|
+
**Most urgent:** C1 and C2 are outright functional bugs that hit real callers. C6 is the systemic issue — the catch-all decorator masks C1, C7, C13 and every other failure as a silent `None`. Fix C6's swallowing behavior first, because it currently hides the rest.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 🔴 Critical — Functional defects
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
### C1. `Selected_Step_Numbers` type mismatch — `list[int]` sent to a `repeated string` field
|
|
32
|
+
|
|
33
|
+
**Location:** `_client.py:394` (definition of type at `models.py:51`)
|
|
34
|
+
|
|
35
|
+
**What is wrong:**
|
|
36
|
+
The proto defines `Compile_Details.Selected_Step_Numbers` as **`repeated string`**. `compile_sequence` passes `compile_sequence_details.selected_step_numbers` straight through, and `CompileSequenceRequest.selected_step_numbers` is typed `list[int]`.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# _client.py:391-397
|
|
40
|
+
request = GRPC_pb2.Compile_Details(
|
|
41
|
+
Sequence_Name=compile_sequence_details.sequence_name,
|
|
42
|
+
Variable_Name=compile_sequence_details.variable_name,
|
|
43
|
+
Selected_Step_Numbers=compile_sequence_details.selected_step_numbers, # list[int] -> repeated string
|
|
44
|
+
Skip_Migrate=compile_sequence_details.skip_migrate,
|
|
45
|
+
Run_Sequence_Manager=compile_sequence_details.run_sequence_manager,
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Why it is critical:**
|
|
50
|
+
protobuf raises `TypeError` when serializing `int` into a `string` field. Any caller passing a non-empty `selected_step_numbers` list gets a hard failure. Because of the catch-all decorator (C6), that failure is swallowed and the caller silently receives `None` instead of a compile result — the feature simply does not work and gives no signal that it broke.
|
|
51
|
+
|
|
52
|
+
**Suggested fix:**
|
|
53
|
+
Convert to strings before building the message:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
Selected_Step_Numbers=[str(step) for step in compile_sequence_details.selected_step_numbers],
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
### C2. `run_sequence` returns a protobuf message but is typed and documented as `str`
|
|
62
|
+
|
|
63
|
+
**Location:** `_client.py:503-504`
|
|
64
|
+
|
|
65
|
+
**What is wrong:**
|
|
66
|
+
`Get_Report_Details` returns a `Report_Details` message whose payload is the `Report_Folder_Path` field. The code returns the whole message object, while the signature says `-> str` and the docstring promises "Absolute path to the generated report directory."
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# _client.py:503-504
|
|
70
|
+
report_path = self.stub.Get_Report_Details(GRPC_pb2.Trigger(Trigger=True))
|
|
71
|
+
return report_path # <-- returns a Report_Details message, not a str
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Why it is critical:**
|
|
75
|
+
The documented and annotated return type is violated. Callers that do `os.path.exists(result)`, string concatenation, or logging on the return value get a protobuf object and either crash or behave incorrectly. This breaks the primary success-path contract of the method.
|
|
76
|
+
|
|
77
|
+
**Suggested fix:**
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
report_details = self.stub.Get_Report_Details(GRPC_pb2.Trigger(Trigger=True))
|
|
81
|
+
return report_details.Report_Folder_Path
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### C3. Inconsistent return contract in `run_sequence` — error path returns a human sentence
|
|
87
|
+
|
|
88
|
+
**Location:** `_client.py:499` and `_client.py:506`
|
|
89
|
+
|
|
90
|
+
**What is wrong:**
|
|
91
|
+
The same function returns three structurally different things:
|
|
92
|
+
- success → a report path
|
|
93
|
+
- runtime errors → `"Sequence run encountered errors: ..."` (line 499)
|
|
94
|
+
- compile failure → `"Sequence compilation failed, cannot run sequence ..."` (line 506)
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# _client.py:499
|
|
98
|
+
return "Sequence run encountered errors: " + ";\n ".join(parts)
|
|
99
|
+
...
|
|
100
|
+
# _client.py:506
|
|
101
|
+
return f"Sequence compilation failed, cannot run sequence {run_sequence_details.sequence_name} - {response.Message}."
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Why it is critical:**
|
|
105
|
+
A caller cannot programmatically distinguish success from failure without string-sniffing the return value. Automation code built on this API cannot reliably branch on outcome, which defeats the purpose of an automation SDK.
|
|
106
|
+
|
|
107
|
+
**Suggested fix:**
|
|
108
|
+
Return a structured result instead of overloaded prose, e.g. a dataclass:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
@dataclass
|
|
112
|
+
class RunSequenceResult:
|
|
113
|
+
success: bool
|
|
114
|
+
report_path: str | None = None
|
|
115
|
+
errors: list[tuple[int, str]] | None = None
|
|
116
|
+
message: str | None = None
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
…and populate `success`/`report_path`/`errors`/`message` on each path so callers branch on fields, not strings.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
### C4. Dead branch: `step is None` can never be true
|
|
124
|
+
|
|
125
|
+
**Location:** `_client.py:489-490`
|
|
126
|
+
|
|
127
|
+
**What is wrong:**
|
|
128
|
+
`errors` only ever receives tuples where `step_number_int is not None` (guarded at `_client.py:465`). So in the reporting loop, `step` is never `None`.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
# _client.py:465-466 -- guard ensures step is never None when added
|
|
132
|
+
if step_number_int is not None:
|
|
133
|
+
errors.add((step_number_int, info))
|
|
134
|
+
...
|
|
135
|
+
# _client.py:489-490 -- therefore unreachable
|
|
136
|
+
if step is None:
|
|
137
|
+
step_label = "Step Unknown"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Why it is critical (correctness):**
|
|
141
|
+
Unreachable code signals a logic gap: either the guard at line 465 is wrong (some errors with unknown step should be captured but are being dropped), or the `None` branch is stale. Either way the intent and the code disagree, which is exactly the kind of latent defect a strict review must flag.
|
|
142
|
+
|
|
143
|
+
**Suggested fix:**
|
|
144
|
+
Decide the intent. If unknown-step errors should be reported, remove the `is not None` guard at line 465 and keep the `None` branch. If not, remove the dead `if step is None` branch entirely.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### C5. Comment/logic contradiction on end-of-sequence detection
|
|
149
|
+
|
|
150
|
+
**Location:** `_client.py:468-472` vs `_client.py:491-492`
|
|
151
|
+
|
|
152
|
+
**What is wrong:**
|
|
153
|
+
The loop comment says `step_number_int == 0` is "Completion of Sequence" and `-1` is "Error before the start of sequence." But the label logic only special-cases `-1` as `"End of Sequence"` and formats `0` as `"Step 0"`.
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# _client.py:468-472
|
|
157
|
+
# -- Completion of Sequence (step_number_int == 0)
|
|
158
|
+
# -- Error before the start of sequence (step_number_int == -1)
|
|
159
|
+
if step_number_int in [0, -1]:
|
|
160
|
+
break
|
|
161
|
+
...
|
|
162
|
+
# _client.py:491-494
|
|
163
|
+
elif step == -1:
|
|
164
|
+
step_label = "End of Sequence"
|
|
165
|
+
else:
|
|
166
|
+
step_label = f"Step {step}" # step == 0 falls here -> "Step 0"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Why it is critical:**
|
|
170
|
+
The two pieces of logic disagree about what `0` and `-1` mean. `-1` is documented as "error before start" but labeled "End of Sequence"; `0` is documented as "completion" but rendered as an ordinary "Step 0". Error reports emitted to users will be mislabeled, and the mismatch strongly suggests the sentinel handling is wrong.
|
|
171
|
+
|
|
172
|
+
**Suggested fix:**
|
|
173
|
+
Make labels match the documented semantics, e.g.:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
if step == -1:
|
|
177
|
+
step_label = "Before Sequence Start"
|
|
178
|
+
elif step == 0:
|
|
179
|
+
step_label = "End of Sequence"
|
|
180
|
+
else:
|
|
181
|
+
step_label = f"Step {step}"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
(Confirm the true sentinel meaning with the server contract before finalizing.)
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 🟠 High — Robustness / correctness
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### C6. Blanket `@handle_exceptions` swallows everything and returns `None`
|
|
193
|
+
|
|
194
|
+
**Location:** `_decorators.py:34-41`
|
|
195
|
+
|
|
196
|
+
**What is wrong:**
|
|
197
|
+
Every public method is wrapped so that **any** exception is printed and converted to `None`.
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
# _decorators.py:35-41
|
|
201
|
+
def wrapper(*args, **kwargs):
|
|
202
|
+
try:
|
|
203
|
+
return func(*args, **kwargs)
|
|
204
|
+
except Exception as e:
|
|
205
|
+
print(f"[ERROR] Exception occurred in '{func_name}': {e}")
|
|
206
|
+
traceback.print_exc()
|
|
207
|
+
return None
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Why it is critical:**
|
|
211
|
+
- Functions documented `-> bool` / `-> str` / `-> dict` can silently return `None`, violating their type contract on every error path.
|
|
212
|
+
- It masks real bugs (C1, C7, C13) — failures become silent `None`s with no exception to catch.
|
|
213
|
+
- `launch_server` documents `Raises: FileNotFoundError` (`_client.py:71`), `_wait_until_server_is_ready` documents `Raises: TimeoutError`, and `get_paths` documents `Raises: FileNotFoundError, JSONDecodeError` — but the decorator guarantees none of these can ever propagate. Docstring and behavior are in direct conflict.
|
|
214
|
+
- `print` for error reporting is inappropriate for a library; use `logging`.
|
|
215
|
+
|
|
216
|
+
**Suggested fix:**
|
|
217
|
+
- Do not blanket-swallow. Catch specific, expected exceptions where recovery is meaningful and let the rest propagate.
|
|
218
|
+
- Replace `print` with the `logging` module.
|
|
219
|
+
- If a "graceful" mode is genuinely required, make it opt-in and return a typed result (never `None` masquerading as `bool`/`str`).
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
### C7. `close_server` crashes / no-ops if the server was never launched
|
|
224
|
+
|
|
225
|
+
**Location:** `_client.py:205`
|
|
226
|
+
|
|
227
|
+
**What is wrong:**
|
|
228
|
+
`self.stub` is `None` until `_init_grpc_channel` runs. `close_server` dereferences it unconditionally.
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
# _client.py:204-205
|
|
232
|
+
trigger = Trigger(Trigger=False)
|
|
233
|
+
response = self.stub.Close_Server(trigger) # AttributeError if stub is None
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Why it is critical:**
|
|
237
|
+
If `launch_server` failed or was never called, this raises `AttributeError`, which C6 converts to `None`. The caller believes shutdown happened when it did not — resource/process cleanup is skipped silently.
|
|
238
|
+
|
|
239
|
+
**Suggested fix:**
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
if self.stub is not None:
|
|
243
|
+
self.stub.Close_Server(Trigger(Trigger=False))
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
…and still run the process/thread teardown below it regardless.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
### C8. `_wait_until_server_is_ready` leaks a channel and does not reuse the real one
|
|
251
|
+
|
|
252
|
+
**Location:** `_client.py:172-174`
|
|
253
|
+
|
|
254
|
+
**What is wrong:**
|
|
255
|
+
A fresh channel is created only for the readiness probe and never closed; `_init_grpc_channel` then creates a second channel.
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
# _client.py:172-174
|
|
259
|
+
grpc.channel_ready_future(
|
|
260
|
+
grpc.insecure_channel(self.__grpc_target_host) # created, never closed
|
|
261
|
+
).result(timeout=self.__grpc_server_timeout)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Why it is critical (resource leak):**
|
|
265
|
+
Every launch leaks one gRPC channel. Two channels exist for the same target. Over repeated launch cycles this accumulates.
|
|
266
|
+
|
|
267
|
+
**Suggested fix:**
|
|
268
|
+
Create the channel once, store it, probe readiness on it, and reuse it in `_init_grpc_channel`:
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
self.channel = grpc.insecure_channel(self.__grpc_target_host)
|
|
272
|
+
grpc.channel_ready_future(self.channel).result(timeout=self.__grpc_server_timeout)
|
|
273
|
+
# _init_grpc_channel then just binds the stub to self.channel
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
### C9. Error-watcher exception "signalling" is effectively useless + thread race
|
|
279
|
+
|
|
280
|
+
**Location:** `_client.py:138-152` (and read side at `_client.py:177`)
|
|
281
|
+
|
|
282
|
+
**What is wrong:**
|
|
283
|
+
Fatal stderr lines are appended to `_server_launch_exceptions`, but this list is only ever read inside `_wait_until_server_is_ready` on a `FutureTimeoutError`.
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
# _client.py:177-178
|
|
287
|
+
if len(self._server_launch_exceptions) > 0:
|
|
288
|
+
raise self._server_launch_exceptions[0]
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Why it is critical:**
|
|
292
|
+
- If the server becomes reachable but the watcher later captures a fatal error, nothing surfaces it — the error is collected and ignored.
|
|
293
|
+
- The list is written on a daemon thread and read on the main thread with no lock → data race.
|
|
294
|
+
|
|
295
|
+
**Suggested fix:**
|
|
296
|
+
- Guard `_server_launch_exceptions` with a `threading.Lock` (or use a `queue.Queue`).
|
|
297
|
+
- Check for captured fatal errors after RPC calls, or fail fast when the watcher records one, rather than only on the readiness timeout.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
### C10. Substring error-classification is fragile
|
|
302
|
+
|
|
303
|
+
**Location:** `_client.py:124-133`
|
|
304
|
+
|
|
305
|
+
**What is wrong:**
|
|
306
|
+
Free-form stderr is lowercased and matched against substrings like `"unavailable"` and `"connection refused"` to decide "non-fatal."
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
# _client.py:124-133
|
|
310
|
+
conn_err_markers = ["failed to connect to all addresses", "connectex",
|
|
311
|
+
"connection refused", "10061", "unavailable"]
|
|
312
|
+
low = stripped_line.lower()
|
|
313
|
+
if any(m in low for m in conn_err_markers):
|
|
314
|
+
continue
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Why it is critical:**
|
|
318
|
+
Any legitimate fatal error whose message merely *contains* one of these words (e.g. a real failure mentioning "service unavailable") is misclassified as non-fatal and dropped, hiding genuine startup failures.
|
|
319
|
+
|
|
320
|
+
**Suggested fix:**
|
|
321
|
+
Classify on structured signals (gRPC status codes / typed exceptions) rather than substring matching of human-readable text. If substring matching is unavoidable, anchor it tightly and document the exact source strings.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
### C11. Fragile magic-string parsing of the setup payload
|
|
326
|
+
|
|
327
|
+
**Location:** `_client.py:305-308`
|
|
328
|
+
|
|
329
|
+
**What is wrong:**
|
|
330
|
+
`setup_interposer_board` strips a literal `"Data: "` prefix then `json.loads`.
|
|
331
|
+
|
|
332
|
+
```python
|
|
333
|
+
# _client.py:305-308
|
|
334
|
+
if raw_data.startswith("Data: "):
|
|
335
|
+
raw_data = raw_data[len("Data: ") :]
|
|
336
|
+
data = json.loads(raw_data)
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**Why it is critical:**
|
|
340
|
+
If the server omits/renames the prefix or returns non-JSON on failure, `json.loads` throws, which C6 swallows → `None`. The undocumented magic-string protocol makes the parse brittle and the failure invisible.
|
|
341
|
+
|
|
342
|
+
**Suggested fix:**
|
|
343
|
+
Wrap the parse defensively and handle malformed payloads explicitly:
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
try:
|
|
347
|
+
data = json.loads(raw_data)
|
|
348
|
+
except json.JSONDecodeError:
|
|
349
|
+
return "Setup InterposerBoard Failed: unrecognized server response."
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Better: agree a structured response field with the server rather than a prefixed string.
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
### C12. `get_paths` opens config without an explicit encoding
|
|
357
|
+
|
|
358
|
+
**Location:** `_utils.py:58`
|
|
359
|
+
|
|
360
|
+
**What is wrong:**
|
|
361
|
+
|
|
362
|
+
```python
|
|
363
|
+
# _utils.py:58
|
|
364
|
+
with open(json_path, "r") as f: # no encoding
|
|
365
|
+
data = json.load(f)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**Why it is critical (portability):**
|
|
369
|
+
Uses the platform default encoding, which differs across OSes/locales and can corrupt non-ASCII paths. `_client.py:45` correctly uses `encoding="utf-8"`; this one is inconsistent and non-portable.
|
|
370
|
+
|
|
371
|
+
**Suggested fix:**
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
with open(json_path, "r", encoding="utf-8") as f:
|
|
375
|
+
data = json.load(f)
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
### C13. `Utils.get_paths` documents `Raises` but is decorated to never raise → constructor `TypeError`
|
|
381
|
+
|
|
382
|
+
**Location:** `_utils.py:47-49` (fallout at `_client.py:44-45`)
|
|
383
|
+
|
|
384
|
+
**What is wrong:**
|
|
385
|
+
The docstring promises `FileNotFoundError` / `JSONDecodeError`, but `@handle_exceptions` converts both to `None`. That `None` then flows into the constructor:
|
|
386
|
+
|
|
387
|
+
```python
|
|
388
|
+
# _client.py:44-45
|
|
389
|
+
self.__paths = Utils.get_paths() # may be None (C6)
|
|
390
|
+
with open(self.__paths["ApplicationSettingsPath"], ...) # None[...] -> TypeError
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Why it is critical:**
|
|
394
|
+
On any config problem, `get_paths` returns `None` instead of raising, and `self.__paths["..."]` raises `TypeError: 'NoneType' object is not subscriptable`. The constructor has no defined failure mode, and the actual error (missing/invalid config) is obscured behind an unrelated `TypeError`.
|
|
395
|
+
|
|
396
|
+
**Suggested fix:**
|
|
397
|
+
Remove `@handle_exceptions` from `get_paths` (let config errors propagate as documented), or validate the result in `__init__`:
|
|
398
|
+
|
|
399
|
+
```python
|
|
400
|
+
self.__paths = Utils.get_paths()
|
|
401
|
+
if not self.__paths:
|
|
402
|
+
raise RuntimeError("Failed to load paths.json configuration.")
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## 🟡 Medium — Coding standards / consistency
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
### S1. Inconsistent parameter naming between facade and client
|
|
412
|
+
|
|
413
|
+
**Location:** `protocol_validation_suite_apis.py:66` vs `_client.py:265`, `_client.py:381`, `_client.py:402`
|
|
414
|
+
|
|
415
|
+
The public facade uses `request`, while the client uses `setup_interposer_board_request` / `compile_sequence_details` / `run_sequence_details`.
|
|
416
|
+
|
|
417
|
+
**Why it matters:** Inconsistent naming for the same concept across the call chain hurts readability and maintainability.
|
|
418
|
+
|
|
419
|
+
**Suggested fix:** Pick one convention (e.g. `request`) and apply it in both layers.
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
### S2. Missing / inconsistent return type hints
|
|
424
|
+
|
|
425
|
+
**Location:** `protocol_validation_suite_apis.py` (`launch_server`, `set_profile`, `setup_interposer_board`, `compile_sequence`, `run_sequence`, `i3s_monitor_setup`); `_client.py:381` (`compile_sequence` documented `tuple[bool, str]` but unannotated); `set_profile` unannotated while `load_project` is annotated.
|
|
426
|
+
|
|
427
|
+
**Why it matters:** Documented return types without annotations defeat static analysis and IDE support; the codebase is uneven.
|
|
428
|
+
|
|
429
|
+
**Suggested fix:** Add explicit return annotations matching the docstrings (e.g. `-> tuple[bool, str]`, `-> bool`, `-> str`).
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
### S3. `__init__.py` public surface is incomplete
|
|
434
|
+
|
|
435
|
+
**Location:** `__init__.py:2-8`
|
|
436
|
+
|
|
437
|
+
`SetupInterposerBoardRequest` and `ResetInterposerBoardRequest` are required arguments of public methods but are **not** exported. `ProtocolDebugResponse` is also unexported.
|
|
438
|
+
|
|
439
|
+
**Why it matters:** Users must import from `.models` (a private-looking module) to call public methods — the advertised API is not self-contained.
|
|
440
|
+
|
|
441
|
+
**Suggested fix:** Add the request models to the imports and `__all__`:
|
|
442
|
+
|
|
443
|
+
```python
|
|
444
|
+
from .models import (
|
|
445
|
+
CompileSequenceRequest, RunSequenceRequest, I3SMonitorSetupRequest,
|
|
446
|
+
SetupInterposerBoardRequest, ResetInterposerBoardRequest,
|
|
447
|
+
)
|
|
448
|
+
__all__ = [
|
|
449
|
+
"ProtocolValidationSuiteAPIs",
|
|
450
|
+
"CompileSequenceRequest", "RunSequenceRequest", "I3SMonitorSetupRequest",
|
|
451
|
+
"SetupInterposerBoardRequest", "ResetInterposerBoardRequest",
|
|
452
|
+
]
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
### S4. Dead / unreachable capability: `_call_protocol_debug` and `ProtocolDebugResponse`
|
|
458
|
+
|
|
459
|
+
**Location:** `_client.py:536`, `models.py:89`
|
|
460
|
+
|
|
461
|
+
`_call_protocol_debug` is private and has **no** facade method in `ProtocolValidationSuiteAPIs`, so it is unreachable through the public API. `ProtocolDebugResponse` exists only to serve it.
|
|
462
|
+
|
|
463
|
+
**Why it matters:** Either incomplete work or dead code; both should be resolved.
|
|
464
|
+
|
|
465
|
+
**Suggested fix:** Expose a public `protocol_debug(...)` method on the facade if intended, or remove `_call_protocol_debug` and `ProtocolDebugResponse` if not.
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
### S5. Local variable shadowing / reassigning `request` to a different message type
|
|
470
|
+
|
|
471
|
+
**Location:** `_client.py:282-300`, `_client.py:415-425`
|
|
472
|
+
|
|
473
|
+
`request` is built as one message type, used, then rebound to a different message type in the same scope (`Setup_Configurations` → `Request`; `Compile_Details` → `Run_Details`).
|
|
474
|
+
|
|
475
|
+
**Why it matters:** Reusing one name for two distinct types hurts readability and defeats type inference.
|
|
476
|
+
|
|
477
|
+
**Suggested fix:** Use distinct names, e.g. `setup_request` / `channel_request`, and `compile_request` / `run_request`.
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
### S6. Unused instance attribute `self._error_stream`
|
|
482
|
+
|
|
483
|
+
**Location:** `_client.py:79`
|
|
484
|
+
|
|
485
|
+
```python
|
|
486
|
+
self._error_stream = subprocess.PIPE # assigned, never read
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
`Popen` uses the literal `subprocess.PIPE` on the next line.
|
|
490
|
+
|
|
491
|
+
**Suggested fix:** Remove the attribute.
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
### S7. `response` assigned but unused; channel-update success ignored
|
|
496
|
+
|
|
497
|
+
**Location:** `_client.py:205`, `_client.py:300`, `_client.py:352`
|
|
498
|
+
|
|
499
|
+
`Close_Server` and both `Update_*_Channels` calls capture a return value that is never inspected. Notably the channel-update `Success` is ignored, so a failed channel update still reports `"Successful."`.
|
|
500
|
+
|
|
501
|
+
**Suggested fix:** Either check the returned `.Success` / `.Response_Data` and reflect it in the result, or drop the unused assignment.
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
### S8. `launch_server` returns `bool` but `close_server` returns `None` — asymmetric
|
|
506
|
+
|
|
507
|
+
**Location:** `protocol_validation_suite_apis.py:28-38`
|
|
508
|
+
|
|
509
|
+
`launch_server` forwards the client's bool (with no annotation); `close_server` drops the return.
|
|
510
|
+
|
|
511
|
+
**Suggested fix:** Make the pair symmetric — return a status from `close_server` too, and annotate both.
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
### S9. Imports not grouped per PEP 8 / split model import
|
|
516
|
+
|
|
517
|
+
**Location:** `_client.py:7-8` (third-party `grpc` interleaved with local `.stubs`); `protocol_validation_suite_apis.py:4-10` (`.models` imported in two statements); `_utils.py:1-3` (`os, json, re` not alphabetical).
|
|
518
|
+
|
|
519
|
+
**Suggested fix:** Group imports as stdlib / third-party / local with blank lines between groups; consolidate the split `.models` import.
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
### S10. Inline/local imports instead of top-of-module
|
|
524
|
+
|
|
525
|
+
**Location:** `_client.py:200-202`
|
|
526
|
+
|
|
527
|
+
`Trigger` is imported inside `close_server` "to avoid circular imports," but `GRPC_pb2` is already imported at module top and used as `GRPC_pb2.Trigger` elsewhere (e.g. `_client.py:439`). The justification is false. Similarly `Debug_Configurations` is imported at top (`_client.py:20`) while every other message is namespaced via `GRPC_pb2.` — mixed styles.
|
|
528
|
+
|
|
529
|
+
**Suggested fix:** Remove the local import and use `GRPC_pb2.Trigger`; standardize on the `GRPC_pb2.` namespace everywhere (drop the direct `Debug_Configurations` import).
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
### S11. `i3s_monitor_setup` unreachable `else` branch / duplicated body
|
|
534
|
+
|
|
535
|
+
**Location:** `_client.py:588-590`
|
|
536
|
+
|
|
537
|
+
`configurations` is typed `dict[str, Any] | str`; the `dict` and `str` branches are exhaustive. The "best-effort fallback" `else` is only reachable on a type-contract violation and duplicates the `dict` branch anyway.
|
|
538
|
+
|
|
539
|
+
**Suggested fix:** Reduce to the two valid branches (`isinstance(..., str)` → passthrough; else `json.dumps`), or validate input and raise on unsupported types.
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
### S12. `handle_exceptions` name resolution is brittle and redundant
|
|
544
|
+
|
|
545
|
+
**Location:** `_decorators.py:30-32`
|
|
546
|
+
|
|
547
|
+
```python
|
|
548
|
+
func_name = getattr(func, "__name__", None) or getattr(func, "__func__", None).__name__
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
If `func` has neither `__name__` nor `__func__`, the second `getattr` returns `None` and `.__name__` raises `AttributeError` at decoration time. Also, `functools.wraps` already provides the name.
|
|
552
|
+
|
|
553
|
+
**Suggested fix:** Use `func.__name__` after `functools.wraps`, or read the name inside `wrapper` via `wrapper.__name__`; drop the fragile fallback chain.
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
### S13. `set_test_condition` redundant assignment
|
|
558
|
+
|
|
559
|
+
**Location:** `_client.py:373`
|
|
560
|
+
|
|
561
|
+
```python
|
|
562
|
+
file_path = test_file_path # pointless alias
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**Suggested fix:** Use `test_file_path` directly and remove the alias.
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
### S14. `set_test_condition` always returns `True`
|
|
570
|
+
|
|
571
|
+
**Location:** `_client.py:378`
|
|
572
|
+
|
|
573
|
+
Returns hard-coded `True`; the only non-`True` outcome is an exception → `None` (via C6). The documented `bool` is really `True | None`.
|
|
574
|
+
|
|
575
|
+
**Suggested fix:** Either let it raise on failure (return type becomes meaningful only if there is a real `False` path), or document that success is `True` and failure raises.
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
### S15. Facade docstring for `reset_interposer_board` names the wrong type
|
|
580
|
+
|
|
581
|
+
**Location:** `protocol_validation_suite_apis.py:85`
|
|
582
|
+
|
|
583
|
+
Arg documented as `SetupInterposerBoardRequest` but the parameter is `ResetInterposerBoardRequest`.
|
|
584
|
+
|
|
585
|
+
**Suggested fix:** Correct the documented type to `ResetInterposerBoardRequest`.
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
### S16. `mass_compile_sequence` param name mismatch with docstring
|
|
590
|
+
|
|
591
|
+
**Location:** `protocol_validation_suite_apis.py:141-147`
|
|
592
|
+
|
|
593
|
+
Parameter is `requests`; the docstring `Args` calls it `request`.
|
|
594
|
+
|
|
595
|
+
**Suggested fix:** Rename the docstring arg to `requests`.
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
### S17. Blank-line convention inside `models.py`
|
|
600
|
+
|
|
601
|
+
**Location:** `models.py:88-89`
|
|
602
|
+
|
|
603
|
+
Only one blank line between `I3SMonitorSetupRequest` and `ProtocolDebugResponse`; every other top-level class uses two (PEP 8).
|
|
604
|
+
|
|
605
|
+
**Suggested fix:** Add the second blank line before `ProtocolDebugResponse`.
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## Recommended fix order
|
|
610
|
+
|
|
611
|
+
1. **C6** — stop the blanket swallow; it is hiding every other bug.
|
|
612
|
+
2. **C1** — fix the `list[int]` → `repeated string` serialization crash.
|
|
613
|
+
3. **C2** — return `report_details.Report_Folder_Path` (honor the `str` contract).
|
|
614
|
+
4. **C3** — give `run_sequence` a structured, machine-checkable result.
|
|
615
|
+
5. **C7, C13** — harden the launch/close/config lifecycle against `None`/uninitialized state.
|
|
616
|
+
6. Remaining High (C8–C12), then Medium (S1–S17).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .protocol_validation_suite_apis import ProtocolValidationSuiteAPIs
|
|
2
|
+
from .models import CompileSequenceRequest, RunSequenceRequest, I3SMonitorSetupRequest
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"ProtocolValidationSuiteAPIs",
|
|
6
|
+
"CompileSequenceRequest",
|
|
7
|
+
"RunSequenceRequest",
|
|
8
|
+
"I3SMonitorSetupRequest",
|
|
9
|
+
]
|