Compare commits
3 Commits
3abd6580fe
...
20260503
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c0821d157 | |||
| d6aa23c293 | |||
| 81dfc91dd6 |
1599
script/SDG2042X_V0.2.6.py
Normal file
BIN
script/Test_pulse.bin
Normal file
BIN
script/archive/V0.0/SDG2042X_GUI_ICON.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
script/archive/V0.1/SDG2042X_GUI_ICON.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
31
script/archive/V0.1/SDG2042x.dat
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# SDG2042X presets
|
||||||
|
SLOT1_NAME=
|
||||||
|
SLOT1_BSWV=
|
||||||
|
SLOT1_OUTP=
|
||||||
|
SLOT2_NAME=
|
||||||
|
SLOT2_BSWV=
|
||||||
|
SLOT2_OUTP=
|
||||||
|
SLOT3_NAME=
|
||||||
|
SLOT3_BSWV=
|
||||||
|
SLOT3_OUTP=
|
||||||
|
SLOT4_NAME=
|
||||||
|
SLOT4_BSWV=
|
||||||
|
SLOT4_OUTP=
|
||||||
|
SLOT5_NAME=
|
||||||
|
SLOT5_BSWV=
|
||||||
|
SLOT5_OUTP=
|
||||||
|
SLOT6_NAME=
|
||||||
|
SLOT6_BSWV=
|
||||||
|
SLOT6_OUTP=
|
||||||
|
SLOT7_NAME=
|
||||||
|
SLOT7_BSWV=
|
||||||
|
SLOT7_OUTP=
|
||||||
|
SLOT8_NAME=
|
||||||
|
SLOT8_BSWV=
|
||||||
|
SLOT8_OUTP=
|
||||||
|
SLOT9_NAME=
|
||||||
|
SLOT9_BSWV=
|
||||||
|
SLOT9_OUTP=
|
||||||
|
SLOT10_NAME=
|
||||||
|
SLOT10_BSWV=
|
||||||
|
SLOT10_OUTP=
|
||||||
BIN
script/archive/V0.2.1/SDG2042X_GUI_ICON.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
1480
script/archive/V0.2.1/SDG2042X_V0.2.1.py
Normal file
31
script/archive/V0.2.1/SDG2042x.dat
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# SDG2042X presets
|
||||||
|
SLOT1_NAME=
|
||||||
|
SLOT1_BSWV=
|
||||||
|
SLOT1_OUTP=
|
||||||
|
SLOT2_NAME=
|
||||||
|
SLOT2_BSWV=
|
||||||
|
SLOT2_OUTP=
|
||||||
|
SLOT3_NAME=
|
||||||
|
SLOT3_BSWV=
|
||||||
|
SLOT3_OUTP=
|
||||||
|
SLOT4_NAME=
|
||||||
|
SLOT4_BSWV=
|
||||||
|
SLOT4_OUTP=
|
||||||
|
SLOT5_NAME=
|
||||||
|
SLOT5_BSWV=
|
||||||
|
SLOT5_OUTP=
|
||||||
|
SLOT6_NAME=
|
||||||
|
SLOT6_BSWV=
|
||||||
|
SLOT6_OUTP=
|
||||||
|
SLOT7_NAME=
|
||||||
|
SLOT7_BSWV=
|
||||||
|
SLOT7_OUTP=
|
||||||
|
SLOT8_NAME=
|
||||||
|
SLOT8_BSWV=
|
||||||
|
SLOT8_OUTP=
|
||||||
|
SLOT9_NAME=
|
||||||
|
SLOT9_BSWV=
|
||||||
|
SLOT9_OUTP=
|
||||||
|
SLOT10_NAME=
|
||||||
|
SLOT10_BSWV=
|
||||||
|
SLOT10_OUTP=
|
||||||
BIN
script/archive/V0.2.1/Test_pulse.bin
Normal file
BIN
script/archive/V0.2.2/SDG2042X_GUI_ICON.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
1518
script/archive/V0.2.2/SDG2042X_V0.2.2.py
Normal file
31
script/archive/V0.2.2/SDG2042x.dat
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# SDG2042X presets
|
||||||
|
SLOT1_NAME=
|
||||||
|
SLOT1_BSWV=
|
||||||
|
SLOT1_OUTP=
|
||||||
|
SLOT2_NAME=
|
||||||
|
SLOT2_BSWV=
|
||||||
|
SLOT2_OUTP=
|
||||||
|
SLOT3_NAME=
|
||||||
|
SLOT3_BSWV=
|
||||||
|
SLOT3_OUTP=
|
||||||
|
SLOT4_NAME=
|
||||||
|
SLOT4_BSWV=
|
||||||
|
SLOT4_OUTP=
|
||||||
|
SLOT5_NAME=
|
||||||
|
SLOT5_BSWV=
|
||||||
|
SLOT5_OUTP=
|
||||||
|
SLOT6_NAME=
|
||||||
|
SLOT6_BSWV=
|
||||||
|
SLOT6_OUTP=
|
||||||
|
SLOT7_NAME=
|
||||||
|
SLOT7_BSWV=
|
||||||
|
SLOT7_OUTP=
|
||||||
|
SLOT8_NAME=
|
||||||
|
SLOT8_BSWV=
|
||||||
|
SLOT8_OUTP=
|
||||||
|
SLOT9_NAME=
|
||||||
|
SLOT9_BSWV=
|
||||||
|
SLOT9_OUTP=
|
||||||
|
SLOT10_NAME=
|
||||||
|
SLOT10_BSWV=
|
||||||
|
SLOT10_OUTP=
|
||||||
66
script/archive/V0.2.2/TEST_PROCEDURE_v0.2.2_Issue_10.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# Test Procedure — v0.2.2 Issue #10 Sweep fallback timing
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Verify that sweep fallback no longer depends on an immediate `SYST:ERR?` after the first `SWWV` write.
|
||||||
|
|
||||||
|
The new logic should do this sequence:
|
||||||
|
|
||||||
|
1. `*CLS`
|
||||||
|
2. send first sweep command
|
||||||
|
3. wait on `*OPC?`
|
||||||
|
4. then read `SYST:ERR?`
|
||||||
|
5. only if needed, retry with `SPAC`
|
||||||
|
|
||||||
|
## Test files
|
||||||
|
- `SDG2042X_V0.2.2.py`
|
||||||
|
- `test_v0.2.2_Issue_10_sweep_fallback.py`
|
||||||
|
|
||||||
|
## Preconditions
|
||||||
|
- SDG reachable via LAN on port `5025`
|
||||||
|
- No other program connected to the instrument
|
||||||
|
- Output load/test setup safe for a short sweep test
|
||||||
|
|
||||||
|
## Command
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 test_v0.2.2_Issue_10_sweep_fallback.py --ip 192.168.178.50 --channel C1
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional custom values:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 test_v0.2.2_Issue_10_sweep_fallback.py \
|
||||||
|
--ip 192.168.178.50 \
|
||||||
|
--channel C1 \
|
||||||
|
--start 1000 \
|
||||||
|
--stop 5000 \
|
||||||
|
--time 1.0 \
|
||||||
|
--type LIN
|
||||||
|
```
|
||||||
|
|
||||||
|
## What the script checks
|
||||||
|
1. Socket connection and `*IDN?`
|
||||||
|
2. Safe baseline setup (`BSWV` to sine)
|
||||||
|
3. Direct one-form check for:
|
||||||
|
- `WAV,...`
|
||||||
|
- `SPAC,...`
|
||||||
|
4. Production-like bug #10 helper
|
||||||
|
5. Stale-error immunity:
|
||||||
|
- inject old error
|
||||||
|
- run helper again
|
||||||
|
- helper must still succeed because it starts with `*CLS`
|
||||||
|
|
||||||
|
## Pass criteria
|
||||||
|
- `critical tests passed: True`
|
||||||
|
- exit code `0`
|
||||||
|
- no final `[ERR]` style failures from the script
|
||||||
|
|
||||||
|
## Interpretation
|
||||||
|
- If only one of the direct forms works, that is acceptable.
|
||||||
|
The important point is that the production-like helper succeeds.
|
||||||
|
- If the stale-error test fails, the error-clear / sync sequence is still not robust enough.
|
||||||
|
- If both direct forms fail, then bug #10 may be masked by a larger sweep syntax compatibility issue.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
This test script intentionally mirrors the current GUI sweep token style (`WAV`, `STAR`) so it checks the real regression path for Issue #10, not a separate protocol cleanup.
|
||||||
BIN
script/archive/V0.2.2/Test_pulse.bin
Normal file
306
script/archive/V0.2.2/test_v0.2.2_Issue_10_sweep_fallback.py
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Live instrument test for SDG GUI bug #10.
|
||||||
|
|
||||||
|
Bug #10
|
||||||
|
-------
|
||||||
|
Sweep fallback from WAV to SPAC was timing-fragile because the code queried
|
||||||
|
SYST:ERR? immediately after the first SWWV write. This script tests the new
|
||||||
|
synchronised approach on a real instrument via raw socket SCPI.
|
||||||
|
|
||||||
|
What it checks
|
||||||
|
--------------
|
||||||
|
1. Instrument identity and basic socket comms.
|
||||||
|
2. Which direct sweep form the instrument accepts:
|
||||||
|
- WAV,<LIN|LOG>
|
||||||
|
- SPAC,<LIN|LOG>
|
||||||
|
3. The production-style fallback logic:
|
||||||
|
*CLS -> SWWV write -> *OPC? -> SYST:ERR? -> optional SPAC fallback
|
||||||
|
4. Stale-error immunity:
|
||||||
|
an old queued error should not poison the next sweep apply because the
|
||||||
|
helper clears the error state before the real attempt.
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
- This mirrors the current GUI sweep token set (WAV / STAR) on purpose,
|
||||||
|
because the target is bug #10 regression testing of the GUI path.
|
||||||
|
- The programming guide documents START and SWMD for sweep. This script does
|
||||||
|
not try to resolve that wider compatibility question.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
DEFAULT_PORT = 5025
|
||||||
|
SOCKET_TIMEOUT = 4.0
|
||||||
|
|
||||||
|
|
||||||
|
class SDGLan:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.sock: Optional[socket.socket] = None
|
||||||
|
|
||||||
|
def connect(self, host: str, port: int = DEFAULT_PORT) -> None:
|
||||||
|
self.close()
|
||||||
|
self.sock = socket.create_connection((host, port), timeout=SOCKET_TIMEOUT)
|
||||||
|
self.sock.settimeout(SOCKET_TIMEOUT)
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
try:
|
||||||
|
if self.sock is not None:
|
||||||
|
self.sock.close()
|
||||||
|
finally:
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
def write(self, cmd: str) -> None:
|
||||||
|
if self.sock is None:
|
||||||
|
raise RuntimeError("Not connected")
|
||||||
|
if not cmd.endswith("\n"):
|
||||||
|
cmd += "\n"
|
||||||
|
self.sock.sendall(cmd.encode("ascii"))
|
||||||
|
|
||||||
|
def drain(self) -> bytes:
|
||||||
|
if self.sock is None:
|
||||||
|
raise RuntimeError("Not connected")
|
||||||
|
data = bytearray()
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
chunk = self.sock.recv(65536)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
data.extend(chunk)
|
||||||
|
except socket.timeout:
|
||||||
|
break
|
||||||
|
return bytes(data)
|
||||||
|
|
||||||
|
def _recv_line(self) -> str:
|
||||||
|
if self.sock is None:
|
||||||
|
raise RuntimeError("Not connected")
|
||||||
|
buf = bytearray()
|
||||||
|
while True:
|
||||||
|
chunk = self.sock.recv(4096)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
buf.extend(chunk)
|
||||||
|
if b"\n" in buf:
|
||||||
|
break
|
||||||
|
return buf.decode("ascii", errors="ignore")
|
||||||
|
|
||||||
|
def query_retry(self, cmd: str, retries: int = 2) -> str:
|
||||||
|
last_err: Optional[Exception] = None
|
||||||
|
for _ in range(retries + 1):
|
||||||
|
try:
|
||||||
|
self.drain()
|
||||||
|
self.write(cmd)
|
||||||
|
return self._recv_line().strip()
|
||||||
|
except socket.timeout as exc:
|
||||||
|
last_err = exc
|
||||||
|
try:
|
||||||
|
self.write("*CLS")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if last_err is not None:
|
||||||
|
raise last_err
|
||||||
|
raise RuntimeError("query_retry failed without exception")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ApplyResult:
|
||||||
|
ok: bool
|
||||||
|
cmd: str
|
||||||
|
opc: str
|
||||||
|
err: str
|
||||||
|
used_fallback: bool = False
|
||||||
|
fallback_cmd: str = ""
|
||||||
|
fallback_opc: str = ""
|
||||||
|
fallback_err: str = ""
|
||||||
|
note: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def print_section(title: str) -> None:
|
||||||
|
print("\n" + "=" * 78)
|
||||||
|
print(title)
|
||||||
|
print("=" * 78)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_apply(io: SDGLan, cmd: str) -> ApplyResult:
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
io.write("*CLS")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
io.write(cmd)
|
||||||
|
opc = io.query_retry("*OPC?")
|
||||||
|
err = io.query_retry("SYST:ERR?")
|
||||||
|
return ApplyResult(ok=err.startswith("0"), cmd=cmd, opc=opc, err=err)
|
||||||
|
except Exception as exc:
|
||||||
|
return ApplyResult(ok=False, cmd=cmd, opc="", err=str(exc), note="exception during sync_apply")
|
||||||
|
|
||||||
|
|
||||||
|
def apply_with_bug10_logic(io: SDGLan, channel: str, parts: List[str]) -> ApplyResult:
|
||||||
|
cmd = f"{channel}:SWWV " + ",".join(parts)
|
||||||
|
first = sync_apply(io, cmd)
|
||||||
|
if first.ok:
|
||||||
|
first.note = "primary form accepted"
|
||||||
|
return first
|
||||||
|
|
||||||
|
parts2 = [p.replace("WAV,", "SPAC,") for p in parts]
|
||||||
|
if parts2 == parts:
|
||||||
|
first.note = "primary form failed and no fallback token found"
|
||||||
|
return first
|
||||||
|
|
||||||
|
cmd2 = f"{channel}:SWWV " + ",".join(parts2)
|
||||||
|
second = sync_apply(io, cmd2)
|
||||||
|
return ApplyResult(
|
||||||
|
ok=second.ok,
|
||||||
|
cmd=first.cmd,
|
||||||
|
opc=first.opc,
|
||||||
|
err=first.err,
|
||||||
|
used_fallback=True,
|
||||||
|
fallback_cmd=cmd2,
|
||||||
|
fallback_opc=second.opc,
|
||||||
|
fallback_err=second.err,
|
||||||
|
note="fallback accepted" if second.ok else "both forms failed",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_gui_style_parts(start_hz: int, stop_hz: int, sweep_type: str = "LIN", time_s: float = 1.0,
|
||||||
|
direction: str = "UP", trigger_source: str = "INT") -> List[str]:
|
||||||
|
return [
|
||||||
|
"STATE,ON",
|
||||||
|
f"WAV,{sweep_type}",
|
||||||
|
f"STAR,{start_hz}",
|
||||||
|
f"STOP,{stop_hz}",
|
||||||
|
f"TIME,{time_s}",
|
||||||
|
f"DIR,{direction}",
|
||||||
|
f"TRSR,{trigger_source}",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def set_safe_basic_wave(io: SDGLan, channel: str) -> None:
|
||||||
|
io.write(f"{channel}:BSWV WVTP,SINE,FRQ,1000,AMP,2,OFST,0")
|
||||||
|
io.query_retry("*OPC?")
|
||||||
|
_ = io.query_retry("SYST:ERR?")
|
||||||
|
|
||||||
|
|
||||||
|
def read_back(io: SDGLan, channel: str) -> str:
|
||||||
|
try:
|
||||||
|
return io.query_retry(f"{channel}:SWWV?")
|
||||||
|
except Exception as exc:
|
||||||
|
return f"<readback failed: {exc}>"
|
||||||
|
|
||||||
|
|
||||||
|
def inject_stale_error(io: SDGLan, channel: str) -> None:
|
||||||
|
# Force a known parser error, wait until the instrument has processed it,
|
||||||
|
# but intentionally do not read SYST:ERR? afterwards. The next helper call
|
||||||
|
# should clear it with *CLS before doing the real sweep write.
|
||||||
|
io.write(f"{channel}:SWWV BADTOKEN,1")
|
||||||
|
try:
|
||||||
|
io.query_retry("*OPC?")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup(io: SDGLan, channel: str) -> None:
|
||||||
|
try:
|
||||||
|
sync_apply(io, f"{channel}:SWWV STATE,OFF")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Live regression test for SDG GUI bug #10 sweep fallback")
|
||||||
|
parser.add_argument("--ip", required=True, help="Instrument IP address")
|
||||||
|
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"Instrument port (default: {DEFAULT_PORT})")
|
||||||
|
parser.add_argument("--channel", default="C1", choices=["C1", "C2"], help="Target channel")
|
||||||
|
parser.add_argument("--start", type=int, default=1000, help="Sweep start frequency in Hz")
|
||||||
|
parser.add_argument("--stop", type=int, default=5000, help="Sweep stop frequency in Hz")
|
||||||
|
parser.add_argument("--time", type=float, default=1.0, help="Sweep time in seconds")
|
||||||
|
parser.add_argument("--type", default="LIN", choices=["LIN", "LOG"], help="Sweep type token used by GUI")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
io = SDGLan()
|
||||||
|
try:
|
||||||
|
print_section("CONNECT")
|
||||||
|
io.connect(args.ip, args.port)
|
||||||
|
print(f"Connected to {args.ip}:{args.port}")
|
||||||
|
print(f"*IDN? -> {io.query_retry('*IDN?')}")
|
||||||
|
|
||||||
|
print_section("PREPARE")
|
||||||
|
set_safe_basic_wave(io, args.channel)
|
||||||
|
print(f"{args.channel}: basic waveform set to safe SINE baseline")
|
||||||
|
|
||||||
|
parts = build_gui_style_parts(
|
||||||
|
start_hz=args.start,
|
||||||
|
stop_hz=args.stop,
|
||||||
|
sweep_type=args.type,
|
||||||
|
time_s=args.time,
|
||||||
|
direction="UP",
|
||||||
|
trigger_source="INT",
|
||||||
|
)
|
||||||
|
parts_spac = [p.replace("WAV,", "SPAC,") for p in parts]
|
||||||
|
|
||||||
|
print_section("INFORMATIONAL DIRECT FORM CHECKS")
|
||||||
|
direct_wav = sync_apply(io, f"{args.channel}:SWWV " + ",".join(parts))
|
||||||
|
print(f"WAV form ok : {direct_wav.ok}")
|
||||||
|
print(f"WAV form cmd : {direct_wav.cmd}")
|
||||||
|
print(f"WAV form *OPC? : {direct_wav.opc}")
|
||||||
|
print(f"WAV form SYST:ERR?: {direct_wav.err}")
|
||||||
|
|
||||||
|
direct_spac = sync_apply(io, f"{args.channel}:SWWV " + ",".join(parts_spac))
|
||||||
|
print(f"SPAC form ok : {direct_spac.ok}")
|
||||||
|
print(f"SPAC form cmd : {direct_spac.cmd}")
|
||||||
|
print(f"SPAC form *OPC? : {direct_spac.opc}")
|
||||||
|
print(f"SPAC form SYST:ERR?: {direct_spac.err}")
|
||||||
|
|
||||||
|
print_section("TEST 1 - PRODUCTION-LIKE BUG #10 HELPER")
|
||||||
|
helper = apply_with_bug10_logic(io, args.channel, parts)
|
||||||
|
print(f"helper ok : {helper.ok}")
|
||||||
|
print(f"helper note : {helper.note}")
|
||||||
|
print(f"primary cmd : {helper.cmd}")
|
||||||
|
print(f"primary *OPC? : {helper.opc}")
|
||||||
|
print(f"primary SYST:ERR?: {helper.err}")
|
||||||
|
if helper.used_fallback:
|
||||||
|
print(f"fallback used : True")
|
||||||
|
print(f"fallback cmd : {helper.fallback_cmd}")
|
||||||
|
print(f"fallback *OPC? : {helper.fallback_opc}")
|
||||||
|
print(f"fallback SYST:ERR?: {helper.fallback_err}")
|
||||||
|
else:
|
||||||
|
print("fallback used : False")
|
||||||
|
print(f"readback : {read_back(io, args.channel)}")
|
||||||
|
|
||||||
|
print_section("TEST 2 - STALE ERROR IMMUNITY")
|
||||||
|
inject_stale_error(io, args.channel)
|
||||||
|
stale = apply_with_bug10_logic(io, args.channel, parts)
|
||||||
|
print(f"stale-test ok : {stale.ok}")
|
||||||
|
print(f"stale-test note : {stale.note}")
|
||||||
|
print(f"primary cmd : {stale.cmd}")
|
||||||
|
print(f"primary *OPC? : {stale.opc}")
|
||||||
|
print(f"primary SYST:ERR?: {stale.err}")
|
||||||
|
if stale.used_fallback:
|
||||||
|
print("fallback used : True")
|
||||||
|
print(f"fallback cmd : {stale.fallback_cmd}")
|
||||||
|
print(f"fallback *OPC? : {stale.fallback_opc}")
|
||||||
|
print(f"fallback SYST:ERR?: {stale.fallback_err}")
|
||||||
|
else:
|
||||||
|
print("fallback used : False")
|
||||||
|
print(f"readback : {read_back(io, args.channel)}")
|
||||||
|
|
||||||
|
print_section("RESULT")
|
||||||
|
critical_ok = helper.ok and stale.ok
|
||||||
|
print(f"critical tests passed: {critical_ok}")
|
||||||
|
print("Informational note: direct WAV/SPAC one-form checks may differ by model/firmware.")
|
||||||
|
return 0 if critical_ok else 1
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
cleanup(io, args.channel)
|
||||||
|
finally:
|
||||||
|
io.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
|
||||||
|
==============================================================================
|
||||||
|
CONNECT
|
||||||
|
==============================================================================
|
||||||
|
Connected to 192.168.178.220:5025
|
||||||
|
*IDN? -> Siglent Technologies,SDG2042X,SDG2XFBC8R0225,2.01.01.38
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
PREPARE
|
||||||
|
==============================================================================
|
||||||
|
C1: basic waveform set to safe SINE baseline
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
INFORMATIONAL DIRECT FORM CHECKS
|
||||||
|
==============================================================================
|
||||||
|
WAV form ok : True
|
||||||
|
WAV form cmd : C1:SWWV STATE,ON,WAV,LIN,STAR,1000,STOP,5000,TIME,1.0,DIR,UP,TRSR,INT
|
||||||
|
WAV form *OPC? : 1
|
||||||
|
WAV form SYST:ERR?: 0,"No error"
|
||||||
|
SPAC form ok : True
|
||||||
|
SPAC form cmd : C1:SWWV STATE,ON,SPAC,LIN,STAR,1000,STOP,5000,TIME,1.0,DIR,UP,TRSR,INT
|
||||||
|
SPAC form *OPC? : 1
|
||||||
|
SPAC form SYST:ERR?: 0,"No error"
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
TEST 1 - PRODUCTION-LIKE BUG #10 HELPER
|
||||||
|
==============================================================================
|
||||||
|
helper ok : True
|
||||||
|
helper note : primary form accepted
|
||||||
|
primary cmd : C1:SWWV STATE,ON,WAV,LIN,STAR,1000,STOP,5000,TIME,1.0,DIR,UP,TRSR,INT
|
||||||
|
primary *OPC? : 1
|
||||||
|
primary SYST:ERR?: 0,"No error"
|
||||||
|
fallback used : False
|
||||||
|
readback : C1:SWWV STATE,ON,TIME,1S,STOP,5000HZ,START,1e-06HZ,TRSR,INT,TRMD,OFF,SWMD,LINE,DIR,UP,SYM,1.20072702051015e-47,MARK_STATE,OFF,MARK_FREQ,0HZ,IDLE_FREQ,START_FREQ,CARR,WVTP,SINE,FRQ,2500.000001HZ,AMP,2V,AMPVRMS,0.707Vrms,AMPDBM,0dBm,OFST,0V,PHSE,0
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
TEST 2 - STALE ERROR IMMUNITY
|
||||||
|
==============================================================================
|
||||||
|
stale-test ok : True
|
||||||
|
stale-test note : primary form accepted
|
||||||
|
primary cmd : C1:SWWV STATE,ON,WAV,LIN,STAR,1000,STOP,5000,TIME,1.0,DIR,UP,TRSR,INT
|
||||||
|
primary *OPC? : 1
|
||||||
|
primary SYST:ERR?: 0,"No error"
|
||||||
|
fallback used : False
|
||||||
|
readback : C1:SWWV STATE,ON,TIME,1S,STOP,5000HZ,START,1e-06HZ,TRSR,INT,TRMD,OFF,SWMD,LINE,DIR,UP,SYM,1.20072702051015e-47,MARK_STATE,OFF,MARK_FREQ,0HZ,IDLE_FREQ,START_FREQ,CARR,WVTP,SINE,FRQ,2500.000001HZ,AMP,2V,AMPVRMS,0.707Vrms,AMPDBM,0dBm,OFST,0V,PHSE,0
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
RESULT
|
||||||
|
==============================================================================
|
||||||
|
critical tests passed: True
|
||||||
|
Informational note: direct WAV/SPAC one-form checks may differ by model/firmware.
|
||||||
BIN
script/archive/V0.2.3/SDG2042X_GUI_ICON.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
1546
script/archive/V0.2.3/SDG2042X_V0.2.3.py
Normal file
31
script/archive/V0.2.3/SDG2042x.dat
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# SDG2042X presets
|
||||||
|
SLOT1_NAME=
|
||||||
|
SLOT1_BSWV=
|
||||||
|
SLOT1_OUTP=
|
||||||
|
SLOT2_NAME=
|
||||||
|
SLOT2_BSWV=
|
||||||
|
SLOT2_OUTP=
|
||||||
|
SLOT3_NAME=
|
||||||
|
SLOT3_BSWV=C1:BSWV WVTP,SQUARE,FRQ,1000HZ,PERI,0.001S,AMP,2V,AMPVRMS,1Vrms,AMPDBM,0dBm,MAX_OUTPUT_AMP,20V,OFST,0V,HLEV,1V,LLEV,-1V,PHSE,0,DUTY,50
|
||||||
|
SLOT3_OUTP=ON
|
||||||
|
SLOT4_NAME=
|
||||||
|
SLOT4_BSWV=
|
||||||
|
SLOT4_OUTP=
|
||||||
|
SLOT5_NAME=
|
||||||
|
SLOT5_BSWV=
|
||||||
|
SLOT5_OUTP=
|
||||||
|
SLOT6_NAME=
|
||||||
|
SLOT6_BSWV=
|
||||||
|
SLOT6_OUTP=
|
||||||
|
SLOT7_NAME=
|
||||||
|
SLOT7_BSWV=
|
||||||
|
SLOT7_OUTP=
|
||||||
|
SLOT8_NAME=
|
||||||
|
SLOT8_BSWV=
|
||||||
|
SLOT8_OUTP=
|
||||||
|
SLOT9_NAME=
|
||||||
|
SLOT9_BSWV=
|
||||||
|
SLOT9_OUTP=
|
||||||
|
SLOT10_NAME=
|
||||||
|
SLOT10_BSWV=
|
||||||
|
SLOT10_OUTP=
|
||||||
BIN
script/archive/V0.2.3/Test_pulse.bin
Normal file
BIN
script/archive/V0.2.4/SDG2042X_GUI_ICON.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
1568
script/archive/V0.2.4/SDG2042X_V0.2.4.py
Normal file
30
script/archive/V0.2.4/SDG2042x.dat
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# SDG2042X presets
|
||||||
|
SLOT1_NAME=
|
||||||
|
SLOT1_BSWV=
|
||||||
|
SLOT1_OUTP=
|
||||||
|
SLOT2_NAME=
|
||||||
|
SLOT2_BSWV=
|
||||||
|
SLOT2_OUTP=
|
||||||
|
SLOT3_NAME=
|
||||||
|
SLOT3_BSWV=
|
||||||
|
SLOT4_NAME=
|
||||||
|
SLOT4_BSWV=
|
||||||
|
SLOT4_OUTP=
|
||||||
|
SLOT5_NAME=
|
||||||
|
SLOT5_BSWV=
|
||||||
|
SLOT5_OUTP=
|
||||||
|
SLOT6_NAME=
|
||||||
|
SLOT6_BSWV=
|
||||||
|
SLOT6_OUTP=
|
||||||
|
SLOT7_NAME=
|
||||||
|
SLOT7_BSWV=
|
||||||
|
SLOT7_OUTP=
|
||||||
|
SLOT8_NAME=
|
||||||
|
SLOT8_BSWV=
|
||||||
|
SLOT8_OUTP=
|
||||||
|
SLOT9_NAME=
|
||||||
|
SLOT9_BSWV=
|
||||||
|
SLOT9_OUTP=
|
||||||
|
SLOT10_NAME=
|
||||||
|
SLOT10_BSWV=
|
||||||
|
SLOT10_OUTP=
|
||||||
BIN
script/archive/V0.2.4/Test_pulse.bin
Normal file
31
script/archive/V0.2.4/preset_A.dat
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# SDG2042X presets
|
||||||
|
SLOT1_NAME=FILE_A
|
||||||
|
SLOT1_BSWV=C1:BSWV WVTP,SINE,FRQ,1000,AMP,1,OFST,0
|
||||||
|
SLOT1_OUTP=OFF
|
||||||
|
SLOT2_NAME=
|
||||||
|
SLOT2_BSWV=
|
||||||
|
SLOT2_OUTP=
|
||||||
|
SLOT3_NAME=
|
||||||
|
SLOT3_BSWV=
|
||||||
|
SLOT3_OUTP=
|
||||||
|
SLOT4_NAME=
|
||||||
|
SLOT4_BSWV=
|
||||||
|
SLOT4_OUTP=
|
||||||
|
SLOT5_NAME=
|
||||||
|
SLOT5_BSWV=
|
||||||
|
SLOT5_OUTP=
|
||||||
|
SLOT6_NAME=
|
||||||
|
SLOT6_BSWV=
|
||||||
|
SLOT6_OUTP=
|
||||||
|
SLOT7_NAME=
|
||||||
|
SLOT7_BSWV=
|
||||||
|
SLOT7_OUTP=
|
||||||
|
SLOT8_NAME=
|
||||||
|
SLOT8_BSWV=
|
||||||
|
SLOT8_OUTP=
|
||||||
|
SLOT9_NAME=
|
||||||
|
SLOT9_BSWV=
|
||||||
|
SLOT9_OUTP=
|
||||||
|
SLOT10_NAME=
|
||||||
|
SLOT10_BSWV=
|
||||||
|
SLOT10_OUTP=
|
||||||
31
script/archive/V0.2.4/preset_B.dat
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# SDG2042X presets
|
||||||
|
SLOT1_NAME=
|
||||||
|
SLOT1_BSWV=C1:BSWV WVTP,SQUARE,FRQ,1000HZ,PERI,0.001S,AMP,2V,AMPVRMS,1Vrms,AMPDBM,13.0103dBm,MAX_OUTPUT_AMP,20V,OFST,0V,HLEV,1V,LLEV,-1V,PHSE,0,DUTY,50
|
||||||
|
SLOT1_OUTP=ON
|
||||||
|
SLOT2_NAME=
|
||||||
|
SLOT2_BSWV=
|
||||||
|
SLOT2_OUTP=
|
||||||
|
SLOT3_NAME=
|
||||||
|
SLOT3_BSWV=
|
||||||
|
SLOT3_OUTP=
|
||||||
|
SLOT4_NAME=
|
||||||
|
SLOT4_BSWV=
|
||||||
|
SLOT4_OUTP=
|
||||||
|
SLOT5_NAME=
|
||||||
|
SLOT5_BSWV=
|
||||||
|
SLOT5_OUTP=
|
||||||
|
SLOT6_NAME=
|
||||||
|
SLOT6_BSWV=
|
||||||
|
SLOT6_OUTP=
|
||||||
|
SLOT7_NAME=
|
||||||
|
SLOT7_BSWV=
|
||||||
|
SLOT7_OUTP=
|
||||||
|
SLOT8_NAME=
|
||||||
|
SLOT8_BSWV=
|
||||||
|
SLOT8_OUTP=
|
||||||
|
SLOT9_NAME=
|
||||||
|
SLOT9_BSWV=
|
||||||
|
SLOT9_OUTP=
|
||||||
|
SLOT10_NAME=
|
||||||
|
SLOT10_BSWV=
|
||||||
|
SLOT10_OUTP=
|
||||||
BIN
script/archive/V0.2.5/SDG2042X_GUI_ICON.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
1569
script/archive/V0.2.5/SDG2042X_V0.2.5.py
Normal file
31
script/archive/V0.2.5/SDG2042x.dat
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# SDG2042X presets
|
||||||
|
SLOT1_NAME=
|
||||||
|
SLOT1_BSWV=
|
||||||
|
SLOT1_OUTP=
|
||||||
|
SLOT2_NAME=
|
||||||
|
SLOT2_BSWV=
|
||||||
|
SLOT2_OUTP=
|
||||||
|
SLOT3_NAME=
|
||||||
|
SLOT3_BSWV=
|
||||||
|
SLOT3_OUTP=
|
||||||
|
SLOT4_NAME=
|
||||||
|
SLOT4_BSWV=
|
||||||
|
SLOT4_OUTP=
|
||||||
|
SLOT5_NAME=
|
||||||
|
SLOT5_BSWV=
|
||||||
|
SLOT5_OUTP=
|
||||||
|
SLOT6_NAME=
|
||||||
|
SLOT6_BSWV=
|
||||||
|
SLOT6_OUTP=
|
||||||
|
SLOT7_NAME=
|
||||||
|
SLOT7_BSWV=
|
||||||
|
SLOT7_OUTP=
|
||||||
|
SLOT8_NAME=
|
||||||
|
SLOT8_BSWV=
|
||||||
|
SLOT8_OUTP=
|
||||||
|
SLOT9_NAME=
|
||||||
|
SLOT9_BSWV=
|
||||||
|
SLOT9_OUTP=
|
||||||
|
SLOT10_NAME=
|
||||||
|
SLOT10_BSWV=
|
||||||
|
SLOT10_OUTP=
|
||||||
BIN
script/archive/V0.2.5/Test_pulse.bin
Normal file
BIN
script/archive/V0.2/SDG2042X_GUI_ICON.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
1361
script/archive/V0.2/SDG2042X_V0.2.py
Normal file
31
script/archive/V0.2/SDG2042x.dat
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# SDG2042X presets
|
||||||
|
SLOT1_NAME=
|
||||||
|
SLOT1_BSWV=
|
||||||
|
SLOT1_OUTP=
|
||||||
|
SLOT2_NAME=
|
||||||
|
SLOT2_BSWV=
|
||||||
|
SLOT2_OUTP=
|
||||||
|
SLOT3_NAME=
|
||||||
|
SLOT3_BSWV=
|
||||||
|
SLOT3_OUTP=
|
||||||
|
SLOT4_NAME=
|
||||||
|
SLOT4_BSWV=
|
||||||
|
SLOT4_OUTP=
|
||||||
|
SLOT5_NAME=
|
||||||
|
SLOT5_BSWV=
|
||||||
|
SLOT5_OUTP=
|
||||||
|
SLOT6_NAME=
|
||||||
|
SLOT6_BSWV=
|
||||||
|
SLOT6_OUTP=
|
||||||
|
SLOT7_NAME=
|
||||||
|
SLOT7_BSWV=
|
||||||
|
SLOT7_OUTP=
|
||||||
|
SLOT8_NAME=
|
||||||
|
SLOT8_BSWV=
|
||||||
|
SLOT8_OUTP=
|
||||||
|
SLOT9_NAME=
|
||||||
|
SLOT9_BSWV=
|
||||||
|
SLOT9_OUTP=
|
||||||
|
SLOT10_NAME=
|
||||||
|
SLOT10_BSWV=
|
||||||
|
SLOT10_OUTP=
|
||||||
1361
script/archive/V0.2/test_v02_Issue_1-2-6-12/SDG2042X_V0.2.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# SDG2042X v0.2 — Test Procedure
|
||||||
|
**Milestone:** v0.2.0 - first bugfix round
|
||||||
|
**Issues:** #1 · #2 · #6 · #12
|
||||||
|
**Date:** 2026-04-18
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Required
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `SDG2042X_V0.2.py` | Script under test |
|
||||||
|
| `test_v0.2_all.py` | Automated test suite |
|
||||||
|
|
||||||
|
Both files must be in the **same directory**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 1 — Automated Tests (no instrument required)
|
||||||
|
|
||||||
|
Covers: **#1** `_recv_line()` · **#6** screenshot path · **#12** `human_to_eng()`
|
||||||
|
Covers structurally: **#2** ARB thread (AST check)
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/scripts
|
||||||
|
python3 test_v0.2_all.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Single issue:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 test_v0.2_all.py --issue 12
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected output
|
||||||
|
|
||||||
|
```
|
||||||
|
Issue #1 PASS (5/5)
|
||||||
|
Issue #12 PASS (20/20)
|
||||||
|
Issue #2 PASS (8/8)
|
||||||
|
Issue #6 PASS (2/2)
|
||||||
|
|
||||||
|
Total: 35/35 passed
|
||||||
|
✓ All tests passed — safe to close all four issues
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit code `0` = pass, `1` = fail. Suitable for CI.
|
||||||
|
|
||||||
|
### Close criteria
|
||||||
|
|
||||||
|
All 35 TCs green → close **#1, #6, #12** immediately.
|
||||||
|
**#2** additionally requires the hardware test below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 2 — Hardware Test (instrument required)
|
||||||
|
|
||||||
|
Covers: **#2** ARB download GUI responsiveness
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
- SDG2042X powered and reachable on LAN
|
||||||
|
- At least one USER waveform stored on the instrument
|
||||||
|
*(if none: upload any `.bin` via ARB Manager → Upload first)*
|
||||||
|
|
||||||
|
### Procedure
|
||||||
|
|
||||||
|
| Step | Action | Expected |
|
||||||
|
|------|--------|---------|
|
||||||
|
| 1 | Start GUI: `python3 SDG2042X_V0.2.py --ip <addr>` | Window opens |
|
||||||
|
| 2 | Click **Connect** | Log: `Connected to <addr>:5025` |
|
||||||
|
| 3 | Tab **ARB Manager** → click **Refresh** | USER list populated |
|
||||||
|
| 4 | Select a USER waveform → click **Download USER → file** | Log: `ARB download started…` |
|
||||||
|
| 5 | **While download runs**: click IDN?, change waveform combo, click SYST:ERR? | UI responds immediately — no freeze |
|
||||||
|
| 6 | Wait for completion | Log: `Downloaded -> <path> (N bytes)` |
|
||||||
|
| 7 | Verify saved file: `ls -lh <saved_file>` | Non-zero size |
|
||||||
|
|
||||||
|
### Close criteria
|
||||||
|
|
||||||
|
Steps 5 and 6 both pass → close **#2**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary Checklist
|
||||||
|
|
||||||
|
```
|
||||||
|
[ ] python3 test_v0.2_all.py → 35/35
|
||||||
|
[ ] #1 close
|
||||||
|
[ ] #6 close
|
||||||
|
[ ] #12 close
|
||||||
|
[ ] Hardware test step 5: GUI responsive during ARB download
|
||||||
|
[ ] Hardware test step 6: file saved, non-zero size
|
||||||
|
[ ] #2 close
|
||||||
|
[ ] Milestone v0.2.0 → mark closed
|
||||||
|
```
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
[36;1mSDG2042X v0.2 — regression test suite[0m
|
||||||
|
[2mScript under test: /home/tgohle/TGONet_Virtual/tgo-files/git/ToGo-Lab/0003-SDG2042X-PyQt-GUI-for-Linux/script/test_v02_Issue_1-2-6-12/./SDG2042X_V0.2.py[0m
|
||||||
|
|
||||||
|
[36;1m── Issue #12 — human_to_eng() MHz regression ──[0m
|
||||||
|
[[32;1m OK [0m] BUG-REG: 1.5 MHz → 1500000 (was 0.0000015)
|
||||||
|
[[32;1m OK [0m] BUG-REG: 30 MHz → 30000000
|
||||||
|
[[32;1m OK [0m] BUG-REG: 100 MHz → 100000000
|
||||||
|
[[32;1m OK [0m] 1 kHz
|
||||||
|
[[32;1m OK [0m] 10 kHz
|
||||||
|
[[32;1m OK [0m] 100 Hz
|
||||||
|
[[32;1m OK [0m] 1 GHz
|
||||||
|
[[32;1m OK [0m] lowercase hz
|
||||||
|
[[32;1m OK [0m] uppercase KHZ
|
||||||
|
[[32;1m OK [0m] 500 ms
|
||||||
|
[[32;1m OK [0m] 1 us
|
||||||
|
[[32;1m OK [0m] 250 ns
|
||||||
|
[[32;1m OK [0m] 2.5 V
|
||||||
|
[[32;1m OK [0m] 500 mV
|
||||||
|
[[32;1m OK [0m] 1.0 Vpp
|
||||||
|
[[32;1m OK [0m] bare integer
|
||||||
|
[[32;1m OK [0m] bare float
|
||||||
|
[[32;1m OK [0m] scientific notation
|
||||||
|
[[32;1m OK [0m] non-numeric fallback
|
||||||
|
[[32;1m OK [0m] empty string
|
||||||
|
|
||||||
|
[36;1m── Issue #1 — _recv_line() handles partial TCP receive ──[0m
|
||||||
|
[[32;1m OK [0m] TC-1a single chunk
|
||||||
|
[[32;1m OK [0m] TC-1b newline split into own chunk
|
||||||
|
[[32;1m OK [0m] TC-1c byte-by-byte
|
||||||
|
[[32;1m OK [0m] TC-1d split at midpoint
|
||||||
|
[[32;1m OK [0m] TC-1e empty response returns empty string
|
||||||
|
|
||||||
|
[36;1m── Issue #6 — on_screenshot() uses in-memory cfg, not disk config ──[0m
|
||||||
|
[[32;1m OK [0m] TC-6a: worker receives self.cfg path, not load_config() path
|
||||||
|
[2m disk cfg = /tmp/tmpy6m9nh8m/dir_disk[0m
|
||||||
|
[2m self.cfg = /tmp/tmpy6m9nh8m/dir_memory[0m
|
||||||
|
[2m received = /tmp/tmpy6m9nh8m/dir_memory[0m
|
||||||
|
[[32;1m OK [0m] TC-6b: worker path differs from stale disk path
|
||||||
|
|
||||||
|
[36;1m── Issue #2 — ARB download moved to QThread (structural) ──[0m
|
||||||
|
[[32;1m OK [0m] TC-2a: class ARBDownloadWorker exists
|
||||||
|
[[32;1m OK [0m] TC-2b: ARBDownloadWorker inherits QThread
|
||||||
|
[[32;1m OK [0m] TC-2c: ARBDownloadWorker.run() defined
|
||||||
|
[[32;1m OK [0m] TC-2d: recv() call inside ARBDownloadWorker.run()
|
||||||
|
[[32;1m OK [0m] TC-2e: on_arb_download() contains no blocking while loop
|
||||||
|
[[32;1m OK [0m] TC-2f: on_arb_download() calls worker.start()
|
||||||
|
[[32;1m OK [0m] TC-2g: _on_arb_download_done() handler defined
|
||||||
|
[[32;1m OK [0m] TC-2h: done signal connected to handler
|
||||||
|
|
||||||
|
[2m NOTE: live GUI responsiveness during download requires[0m
|
||||||
|
[2m a connected SDG2042X — run manually per test strategy.[0m
|
||||||
|
|
||||||
|
[36;1m══════════════════════════════════════════════[0m
|
||||||
|
[36;1m SUMMARY[0m
|
||||||
|
[36;1m══════════════════════════════════════════════[0m
|
||||||
|
Issue #1 [32;1mPASS (5/5)[0m
|
||||||
|
Issue #12 [32;1mPASS (20/20)[0m
|
||||||
|
Issue #2 [32;1mPASS (8/8)[0m
|
||||||
|
Issue #6 [32;1mPASS (2/2)[0m
|
||||||
|
|
||||||
|
Total: 35/35 passed
|
||||||
|
[32;1m ✓ All tests passed — safe to close all four issues[0m
|
||||||
|
|
||||||
|
Gitea close checklist:
|
||||||
|
#1 [32;1m✓ close[0m
|
||||||
|
#2 [32;1m✓ close[0m
|
||||||
|
#6 [32;1m✓ close[0m
|
||||||
|
#12 [32;1m✓ close[0m
|
||||||
|
|
||||||
@ -0,0 +1,476 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
test_v0.2_all.py -- SDG2042X v0.2 regression test suite
|
||||||
|
Tests: Issue #1, #2, #6, #12 (milestone: v0.2.0 - first bugfix round finished)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 test_v0.2_all.py # run all tests
|
||||||
|
python3 test_v0.2_all.py --issue 12 # run single issue
|
||||||
|
|
||||||
|
Place this file next to SDG2042X_V0.2.py and run from that directory.
|
||||||
|
No instrument required. All tests are offline / mocked.
|
||||||
|
|
||||||
|
Author: ToGo-Lab test suite — generated 2026-04-18
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import types
|
||||||
|
|
||||||
|
SCRIPT_NAME = "SDG2042X_V0.2.py"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# ANSI colours (disabled on Windows)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
USE_COLOUR = sys.platform != "win32"
|
||||||
|
def _c(code, s): return f"\033[{code}m{s}\033[0m" if USE_COLOUR else s
|
||||||
|
OK = lambda s: _c("32;1", s)
|
||||||
|
FAIL = lambda s: _c("31;1", s)
|
||||||
|
HEAD = lambda s: _c("36;1", s)
|
||||||
|
DIM = lambda s: _c("2", s)
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Result collector
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
results: dict[str, list[tuple[bool, str]]] = {}
|
||||||
|
|
||||||
|
def record(issue: str, ok: bool, label: str, detail: str = ""):
|
||||||
|
results.setdefault(issue, []).append((ok, label))
|
||||||
|
mark = OK(" OK ") if ok else FAIL(" FAIL ")
|
||||||
|
print(f" [{mark}] {label}")
|
||||||
|
if detail:
|
||||||
|
for line in detail.strip().splitlines():
|
||||||
|
print(DIM(f" {line}"))
|
||||||
|
|
||||||
|
def section(title: str):
|
||||||
|
print()
|
||||||
|
print(HEAD(f"── {title} ──"))
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Locate the script under test
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
def find_script() -> str:
|
||||||
|
candidate = os.path.join(os.path.dirname(__file__), SCRIPT_NAME)
|
||||||
|
if not os.path.exists(candidate):
|
||||||
|
sys.exit(FAIL(f"ERROR: {SCRIPT_NAME} not found next to this test file.\n"
|
||||||
|
f"Expected: {candidate}"))
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
SCRIPT_PATH = find_script()
|
||||||
|
SRC = open(SCRIPT_PATH, encoding="utf-8").read()
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Minimal PyQt5 mock so the module can be imported without a display
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
def _install_pyqt5_mock():
|
||||||
|
def _noop(*a, **k): return None
|
||||||
|
def _cls(name): return type(name, (), {
|
||||||
|
"__init__": _noop, "__call__": _noop,
|
||||||
|
"setEnabled": _noop, "addItems": _noop, "setText": _noop,
|
||||||
|
"text": lambda s: "", "currentText": lambda s: "",
|
||||||
|
"appendPlainText": _noop, "setReadOnly": _noop,
|
||||||
|
"setMinimumHeight": _noop, "setPlaceholderText": _noop,
|
||||||
|
"setWindowTitle": _noop, "resize": _noop, "show": _noop,
|
||||||
|
"setRange": _noop, "setValue": _noop, "value": lambda s: 1,
|
||||||
|
"addWidget": _noop, "setLayout": _noop, "addStretch": _noop,
|
||||||
|
"setColumnStretch": _noop, "setHorizontalHeaderLabels": _noop,
|
||||||
|
"horizontalHeader": lambda s: type("h",(),{"setStretchLastSection":_noop})(),
|
||||||
|
"verticalHeader": lambda s: type("v",(),{"setVisible":_noop})(),
|
||||||
|
"setItem": _noop, "item": lambda s,r,c: type("i",(),{"setFlags":_noop,"setText":_noop,"text":lambda ss:""})(),
|
||||||
|
"setFlags": _noop, "clicked": type("sig",(),{"connect":_noop})(),
|
||||||
|
"currentTextChanged": type("sig",(),{"connect":_noop})(),
|
||||||
|
"returnPressed": type("sig",(),{"connect":_noop})(),
|
||||||
|
"isRunning": lambda s: False, "start": _noop,
|
||||||
|
"done": type("sig",(),{"connect":_noop, "emit":_noop})(),
|
||||||
|
"isNull": lambda s: True,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Build mock modules
|
||||||
|
for mod in ["PyQt5","PyQt5.QtWidgets","PyQt5.QtCore","PyQt5.QtGui"]:
|
||||||
|
if mod not in sys.modules:
|
||||||
|
sys.modules[mod] = types.ModuleType(mod)
|
||||||
|
|
||||||
|
qw = sys.modules["PyQt5.QtWidgets"]
|
||||||
|
qc = sys.modules["PyQt5.QtCore"]
|
||||||
|
qg = sys.modules["PyQt5.QtGui"]
|
||||||
|
|
||||||
|
for name in ["QWidget","QLabel","QLineEdit","QComboBox","QPushButton",
|
||||||
|
"QPlainTextEdit","QTabWidget","QGridLayout","QHBoxLayout",
|
||||||
|
"QVBoxLayout","QApplication","QSpinBox","QCheckBox",
|
||||||
|
"QListWidget","QTableWidget","QTableWidgetItem",
|
||||||
|
"QFileDialog","QSizePolicy"]:
|
||||||
|
setattr(qw, name, _cls(name))
|
||||||
|
|
||||||
|
# QThread: base for workers
|
||||||
|
setattr(qc, "QThread", _cls("QThread"))
|
||||||
|
setattr(qc, "Qt", type("Qt", (), {"WaitCursor": 0, "ItemIsEnabled": 0}))
|
||||||
|
|
||||||
|
class _Signal:
|
||||||
|
def __init__(self, *types): pass
|
||||||
|
def connect(self, fn): pass
|
||||||
|
def emit(self, *a): pass
|
||||||
|
setattr(qc, "pyqtSignal", _Signal)
|
||||||
|
|
||||||
|
setattr(qg, "QImage", _cls("QImage"))
|
||||||
|
setattr(qg, "QPixmap", _cls("QPixmap"))
|
||||||
|
|
||||||
|
_install_pyqt5_mock()
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Load module under test (SDG2042X_V0.2.py)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
spec = importlib.util.spec_from_file_location("sdg", SCRIPT_PATH)
|
||||||
|
sdg = importlib.util.module_from_spec(spec)
|
||||||
|
try:
|
||||||
|
spec.loader.exec_module(sdg)
|
||||||
|
except Exception as e:
|
||||||
|
sys.exit(FAIL(f"ERROR: failed to import {SCRIPT_NAME}: {e}"))
|
||||||
|
|
||||||
|
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
# ISSUE #12 — human_to_eng() MHz regression
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
def test_issue_12():
|
||||||
|
section("Issue #12 — human_to_eng() MHz regression")
|
||||||
|
|
||||||
|
fn = sdg.human_to_eng
|
||||||
|
|
||||||
|
cases = [
|
||||||
|
# (input, expected, description)
|
||||||
|
# ── THE BUG: these were silently wrong before v0.2 ──
|
||||||
|
("1.5 MHz", "1500000", "BUG-REG: 1.5 MHz → 1500000 (was 0.0000015)"),
|
||||||
|
("30 MHz", "30000000", "BUG-REG: 30 MHz → 30000000"),
|
||||||
|
("100 MHz", "100000000", "BUG-REG: 100 MHz → 100000000"),
|
||||||
|
# ── Frequency ──
|
||||||
|
("1 kHz", "1000", "1 kHz"),
|
||||||
|
("10 kHz", "10000", "10 kHz"),
|
||||||
|
("100 Hz", "100", "100 Hz"),
|
||||||
|
("1 GHz", "1000000000", "1 GHz"),
|
||||||
|
("1 hz", "1", "lowercase hz"),
|
||||||
|
("1 KHZ", "1000", "uppercase KHZ"),
|
||||||
|
# ── Time ──
|
||||||
|
("500 ms", "0.5", "500 ms"),
|
||||||
|
("1 us", "1e-06", "1 us"),
|
||||||
|
("250 ns", repr(250e-9), "250 ns"),
|
||||||
|
# ── Voltage ──
|
||||||
|
("2.5 V", "2.5", "2.5 V"),
|
||||||
|
("500 mV", "0.5", "500 mV"),
|
||||||
|
("1.0 Vpp", "1", "1.0 Vpp"),
|
||||||
|
# ── Bare numbers (duty, phase, offset) ──
|
||||||
|
("50", "50", "bare integer"),
|
||||||
|
("0.001", "0.001", "bare float"),
|
||||||
|
("1e-6", "1e-06", "scientific notation"),
|
||||||
|
# ── Fallback ──
|
||||||
|
("SINE", "SINE", "non-numeric fallback"),
|
||||||
|
("", "", "empty string"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def _eq(a, b):
|
||||||
|
"""String equality first; fall back to float comparison for IEEE 754 noise."""
|
||||||
|
if a == b:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
return abs(float(a) - float(b)) <= 1e-12 * max(abs(float(b)), 1e-30)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
for inp, expected, desc in cases:
|
||||||
|
result = fn(inp)
|
||||||
|
ok = _eq(result, expected)
|
||||||
|
record("12", ok, desc,
|
||||||
|
f"input={inp!r} expected≈{expected!r} got={result!r}" if not ok else "")
|
||||||
|
|
||||||
|
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
# ISSUE #1 — _recv_line() partial TCP receive
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
def _fragmented_server(host, port, response: bytes, chunk_sizes: list, ready: threading.Event):
|
||||||
|
"""Mini TCP server: sends response in deliberate fragments."""
|
||||||
|
srv = socket.create_server((host, port))
|
||||||
|
srv.settimeout(5)
|
||||||
|
ready.set()
|
||||||
|
try:
|
||||||
|
conn, _ = srv.accept()
|
||||||
|
offset = 0
|
||||||
|
for size in chunk_sizes:
|
||||||
|
conn.sendall(response[offset:offset + size])
|
||||||
|
offset += size
|
||||||
|
time.sleep(0.012)
|
||||||
|
if offset < len(response):
|
||||||
|
conn.sendall(response[offset:])
|
||||||
|
conn.close()
|
||||||
|
finally:
|
||||||
|
srv.close()
|
||||||
|
|
||||||
|
def _next_port(base=15025):
|
||||||
|
"""Find a free port starting from base."""
|
||||||
|
for p in range(base, base + 100):
|
||||||
|
try:
|
||||||
|
s = socket.create_server(("127.0.0.1", p)); s.close(); return p
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
raise RuntimeError("No free port found")
|
||||||
|
|
||||||
|
def test_issue_1():
|
||||||
|
section("Issue #1 — _recv_line() handles partial TCP receive")
|
||||||
|
|
||||||
|
def run_tc(label, response: bytes, chunk_sizes: list):
|
||||||
|
port = _next_port()
|
||||||
|
ready = threading.Event()
|
||||||
|
t = threading.Thread(
|
||||||
|
target=_fragmented_server,
|
||||||
|
args=("127.0.0.1", port, response, chunk_sizes, ready),
|
||||||
|
daemon=True)
|
||||||
|
t.start()
|
||||||
|
ready.wait(timeout=2)
|
||||||
|
|
||||||
|
io = sdg.SDGLan()
|
||||||
|
try:
|
||||||
|
io.connect("127.0.0.1", port)
|
||||||
|
result = io._recv_line()
|
||||||
|
finally:
|
||||||
|
io.close()
|
||||||
|
t.join(timeout=3)
|
||||||
|
|
||||||
|
expected = response.decode("ascii").strip()
|
||||||
|
ok = result.strip() == expected
|
||||||
|
record("1", ok, label,
|
||||||
|
f"expected={expected!r} got={result.strip()!r}" if not ok else "")
|
||||||
|
|
||||||
|
# TC-1a: single chunk (baseline)
|
||||||
|
run_tc("TC-1a single chunk",
|
||||||
|
b"C1:BSWV WVTP,SINE,FRQ,1000\n", [])
|
||||||
|
|
||||||
|
# TC-1b: \n arrives in a separate recv() — the original bug
|
||||||
|
msg = b"C1:BSWV WVTP,SINE,FRQ,1000\n"
|
||||||
|
run_tc("TC-1b newline split into own chunk",
|
||||||
|
msg, [len(msg) - 1])
|
||||||
|
|
||||||
|
# TC-1c: byte-by-byte fragmentation
|
||||||
|
msg = b"*IDN? Siglent,SDG2042X\n"
|
||||||
|
run_tc("TC-1c byte-by-byte",
|
||||||
|
msg, [1] * (len(msg) - 1))
|
||||||
|
|
||||||
|
# TC-1d: two unequal halves
|
||||||
|
msg = b"C1:OUTP ON\n"
|
||||||
|
run_tc("TC-1d split at midpoint",
|
||||||
|
msg, [len(msg) // 2])
|
||||||
|
|
||||||
|
# TC-1e: empty response (server closes immediately — must not hang)
|
||||||
|
port = _next_port()
|
||||||
|
ready = threading.Event()
|
||||||
|
def empty_server():
|
||||||
|
srv = socket.create_server(("127.0.0.1", port)); srv.settimeout(5)
|
||||||
|
ready.set()
|
||||||
|
conn, _ = srv.accept(); conn.close(); srv.close()
|
||||||
|
t = threading.Thread(target=empty_server, daemon=True); t.start()
|
||||||
|
ready.wait(timeout=2)
|
||||||
|
io = sdg.SDGLan()
|
||||||
|
try:
|
||||||
|
io.connect("127.0.0.1", port)
|
||||||
|
result = io._recv_line()
|
||||||
|
finally:
|
||||||
|
io.close()
|
||||||
|
t.join(timeout=3)
|
||||||
|
record("1", result == "", "TC-1e empty response returns empty string",
|
||||||
|
f"got={result!r}" if result != "" else "")
|
||||||
|
|
||||||
|
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
# ISSUE #6 — Screenshot path uses stale disk config
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
def test_issue_6():
|
||||||
|
section("Issue #6 — on_screenshot() uses in-memory cfg, not disk config")
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
dir_disk = os.path.join(tmp, "dir_disk"); os.makedirs(dir_disk)
|
||||||
|
dir_memory = os.path.join(tmp, "dir_memory"); os.makedirs(dir_memory)
|
||||||
|
|
||||||
|
# Write a config file pointing at dir_disk
|
||||||
|
cfg_path = os.path.join(tmp, "SDG2042x.config")
|
||||||
|
with open(cfg_path, "w") as f:
|
||||||
|
f.write(f"SCREENSHOT_DIR={dir_disk}\nPRESET_DIR={tmp}\n")
|
||||||
|
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
# Patch load_config to return disk dir (simulates on-disk state)
|
||||||
|
_orig_load = sdg.load_config
|
||||||
|
sdg.load_config = lambda p: {"PRESET_DIR": tmp, "SCREENSHOT_DIR": dir_disk}
|
||||||
|
|
||||||
|
# Patch ScreenshotWorker: capture outdir, don't actually run
|
||||||
|
class FakeWorker:
|
||||||
|
def __init__(self, io, outdir, parent=None):
|
||||||
|
captured["outdir"] = outdir
|
||||||
|
def isRunning(self): return False
|
||||||
|
def start(self): pass
|
||||||
|
done = type("sig", (), {"connect": lambda s, f: None})()
|
||||||
|
sdg.ScreenshotWorker = FakeWorker
|
||||||
|
|
||||||
|
# Minimal stand-in for Main instance with in-memory cfg = dir_memory
|
||||||
|
class FakeMain:
|
||||||
|
cfg = {"SCREENSHOT_DIR": dir_memory, "PRESET_DIR": tmp}
|
||||||
|
i = type("io", (), {"sock": object()})()
|
||||||
|
_scr_worker = None
|
||||||
|
log = type("log", (), {"appendPlainText": lambda s, t: None})()
|
||||||
|
def logln(self, s): pass
|
||||||
|
def setCursor(self, c): pass
|
||||||
|
def unsetCursor(self): pass
|
||||||
|
def _on_screenshot_done(self, *a): pass
|
||||||
|
btn_scr = type("btn", (), {
|
||||||
|
"setEnabled": lambda s, v: None,
|
||||||
|
"isRunning": lambda s: False})()
|
||||||
|
|
||||||
|
obj = FakeMain()
|
||||||
|
sdg.Main.on_screenshot(obj)
|
||||||
|
|
||||||
|
sdg.load_config = _orig_load
|
||||||
|
|
||||||
|
used = captured.get("outdir", "NOT_CAPTURED")
|
||||||
|
|
||||||
|
# TC-6a: worker must receive the in-memory dir, not the disk dir
|
||||||
|
record("6",
|
||||||
|
used == dir_memory,
|
||||||
|
"TC-6a: worker receives self.cfg path, not load_config() path",
|
||||||
|
f"disk cfg = {dir_disk}\n"
|
||||||
|
f"self.cfg = {dir_memory}\n"
|
||||||
|
f"received = {used}")
|
||||||
|
|
||||||
|
# TC-6b: inverse sanity — must NOT be the disk path
|
||||||
|
record("6",
|
||||||
|
used != dir_disk,
|
||||||
|
"TC-6b: worker path differs from stale disk path",
|
||||||
|
f"got {used!r}, must not be {dir_disk!r}" if used == dir_disk else "")
|
||||||
|
|
||||||
|
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
# ISSUE #2 — ARB download blocks GUI thread (structural AST check)
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
def test_issue_2():
|
||||||
|
section("Issue #2 — ARB download moved to QThread (structural)")
|
||||||
|
|
||||||
|
tree = ast.parse(SRC)
|
||||||
|
fn_names = {n.name for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)}
|
||||||
|
|
||||||
|
# TC-2a: ARBDownloadWorker class exists
|
||||||
|
worker_nodes = [n for n in ast.walk(tree)
|
||||||
|
if isinstance(n, ast.ClassDef) and n.name == "ARBDownloadWorker"]
|
||||||
|
record("2", bool(worker_nodes), "TC-2a: class ARBDownloadWorker exists")
|
||||||
|
|
||||||
|
if worker_nodes:
|
||||||
|
worker = worker_nodes[0]
|
||||||
|
bases = [getattr(b, "attr", getattr(b, "id", "")) for b in worker.bases]
|
||||||
|
# TC-2b: inherits QThread
|
||||||
|
record("2", "QThread" in bases,
|
||||||
|
"TC-2b: ARBDownloadWorker inherits QThread",
|
||||||
|
f"bases found: {bases}" if "QThread" not in bases else "")
|
||||||
|
|
||||||
|
# TC-2c: has a run() method
|
||||||
|
run_methods = [n for n in ast.walk(worker)
|
||||||
|
if isinstance(n, ast.FunctionDef) and n.name == "run"]
|
||||||
|
record("2", bool(run_methods), "TC-2c: ARBDownloadWorker.run() defined")
|
||||||
|
|
||||||
|
if run_methods:
|
||||||
|
run_src = ast.get_source_segment(SRC, run_methods[0]) or ""
|
||||||
|
# TC-2d: recv is inside the worker's run(), i.e. the work is off-thread
|
||||||
|
record("2", "recv" in run_src,
|
||||||
|
"TC-2d: recv() call inside ARBDownloadWorker.run()",
|
||||||
|
"No recv() found in run() body" if "recv" not in run_src else "")
|
||||||
|
|
||||||
|
# TC-2e: on_arb_download in Main has NO while loop (blocking recv removed)
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.FunctionDef) and node.name == "on_arb_download":
|
||||||
|
has_while = any(isinstance(n, ast.While) for n in ast.walk(node))
|
||||||
|
record("2", not has_while,
|
||||||
|
"TC-2e: on_arb_download() contains no blocking while loop",
|
||||||
|
"while loop still present — BUG NOT FIXED" if has_while else "")
|
||||||
|
|
||||||
|
# TC-2f: calls .start() to launch the thread
|
||||||
|
call_attrs = [getattr(n.func, "attr", "")
|
||||||
|
for n in ast.walk(node) if isinstance(n, ast.Call)]
|
||||||
|
record("2", "start" in call_attrs,
|
||||||
|
"TC-2f: on_arb_download() calls worker.start()",
|
||||||
|
f"call attrs found: {call_attrs}" if "start" not in call_attrs else "")
|
||||||
|
|
||||||
|
# TC-2g: result handler exists
|
||||||
|
record("2", "_on_arb_download_done" in fn_names,
|
||||||
|
"TC-2g: _on_arb_download_done() handler defined")
|
||||||
|
|
||||||
|
# TC-2h: done signal connected to handler
|
||||||
|
record("2", "_on_arb_download_done" in SRC and "done.connect" in SRC,
|
||||||
|
"TC-2h: done signal connected to handler")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(DIM(" NOTE: live GUI responsiveness during download requires"))
|
||||||
|
print(DIM(" a connected SDG2042X — run manually per test strategy."))
|
||||||
|
|
||||||
|
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Summary
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
def print_summary():
|
||||||
|
print()
|
||||||
|
print(HEAD("══════════════════════════════════════════════"))
|
||||||
|
print(HEAD(" SUMMARY"))
|
||||||
|
print(HEAD("══════════════════════════════════════════════"))
|
||||||
|
|
||||||
|
total_ok = total_fail = 0
|
||||||
|
issue_verdict = {}
|
||||||
|
|
||||||
|
for issue, tcs in sorted(results.items()):
|
||||||
|
ok_count = sum(1 for ok, _ in tcs if ok)
|
||||||
|
fail_count = sum(1 for ok, _ in tcs if not ok)
|
||||||
|
total_ok += ok_count
|
||||||
|
total_fail += fail_count
|
||||||
|
verdict = "PASS" if fail_count == 0 else "FAIL"
|
||||||
|
issue_verdict[issue] = verdict
|
||||||
|
v_str = OK(f"PASS ({ok_count}/{len(tcs)})") if verdict == "PASS" \
|
||||||
|
else FAIL(f"FAIL ({fail_count} failed)")
|
||||||
|
print(f" Issue #{issue:2s} {v_str}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
overall = total_fail == 0
|
||||||
|
total = total_ok + total_fail
|
||||||
|
print(f" Total: {total_ok}/{total} passed")
|
||||||
|
if overall:
|
||||||
|
print(OK(" ✓ All tests passed — safe to close all four issues"))
|
||||||
|
else:
|
||||||
|
print(FAIL(f" ✗ {total_fail} test(s) failed — do NOT close affected issues"))
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" Gitea close checklist:")
|
||||||
|
for iss in ["1", "2", "6", "12"]:
|
||||||
|
v = issue_verdict.get(iss, "NOT RUN")
|
||||||
|
box = OK("✓ close") if v == "PASS" else FAIL("✗ keep open")
|
||||||
|
print(f" #{iss:2s} {box}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
return overall
|
||||||
|
|
||||||
|
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Entry point
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
if __name__ == "__main__":
|
||||||
|
target = None
|
||||||
|
if "--issue" in sys.argv:
|
||||||
|
idx = sys.argv.index("--issue")
|
||||||
|
if idx + 1 < len(sys.argv):
|
||||||
|
target = sys.argv[idx + 1]
|
||||||
|
|
||||||
|
print(HEAD(f"SDG2042X v0.2 — regression test suite"))
|
||||||
|
print(DIM(f"Script under test: {SCRIPT_PATH}"))
|
||||||
|
|
||||||
|
if target in (None, "12"): test_issue_12()
|
||||||
|
if target in (None, "1"): test_issue_1()
|
||||||
|
if target in (None, "6"): test_issue_6()
|
||||||
|
if target in (None, "2"): test_issue_2()
|
||||||
|
|
||||||
|
ok = print_summary()
|
||||||
|
sys.exit(0 if ok else 1)
|
||||||