smartcli-toolkit 0.1.0__tar.gz → 0.1.1__tar.gz
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.
- {smartcli_toolkit-0.1.0 → smartcli_toolkit-0.1.1}/PKG-INFO +22 -2
- {smartcli_toolkit-0.1.0 → smartcli_toolkit-0.1.1}/README.md +1 -1
- {smartcli_toolkit-0.1.0 → smartcli_toolkit-0.1.1}/pyproject.toml +43 -1
- {smartcli_toolkit-0.1.0 → smartcli_toolkit-0.1.1}/smartcli_core/__init__.py +1 -1
- {smartcli_toolkit-0.1.0 → smartcli_toolkit-0.1.1}/smartcli_toolkit.egg-info/PKG-INFO +22 -2
- {smartcli_toolkit-0.1.0 → smartcli_toolkit-0.1.1}/smartcli_toolkit.egg-info/SOURCES.txt +2 -1
- smartcli_toolkit-0.1.1/tests/test_readiness.py +301 -0
- {smartcli_toolkit-0.1.0 → smartcli_toolkit-0.1.1}/LICENSE +0 -0
- {smartcli_toolkit-0.1.0 → smartcli_toolkit-0.1.1}/setup.cfg +0 -0
- {smartcli_toolkit-0.1.0 → smartcli_toolkit-0.1.1}/smartcli_core/pty_backend.py +0 -0
- {smartcli_toolkit-0.1.0 → smartcli_toolkit-0.1.1}/smartcli_core/readiness.py +0 -0
- {smartcli_toolkit-0.1.0 → smartcli_toolkit-0.1.1}/smartcli_core/screen_model.py +0 -0
- {smartcli_toolkit-0.1.0 → smartcli_toolkit-0.1.1}/smartcli_core/session.py +0 -0
- {smartcli_toolkit-0.1.0 → smartcli_toolkit-0.1.1}/smartcli_core/snapshot.py +0 -0
- {smartcli_toolkit-0.1.0 → smartcli_toolkit-0.1.1}/smartcli_toolkit.egg-info/dependency_links.txt +0 -0
- {smartcli_toolkit-0.1.0 → smartcli_toolkit-0.1.1}/smartcli_toolkit.egg-info/requires.txt +0 -0
- {smartcli_toolkit-0.1.0 → smartcli_toolkit-0.1.1}/smartcli_toolkit.egg-info/top_level.txt +0 -0
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: smartcli-toolkit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Pluggable-PTY + pyte screen model + semantic snapshot / readiness core for driving interactive terminal programs (SmartCLI shared core).
|
|
5
|
+
Author: dwgx
|
|
5
6
|
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dwgx/SmartCLI
|
|
8
|
+
Project-URL: Repository, https://github.com/dwgx/SmartCLI
|
|
9
|
+
Project-URL: Changelog, https://github.com/dwgx/SmartCLI/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/dwgx/SmartCLI/issues
|
|
11
|
+
Project-URL: PyPI, https://pypi.org/project/smartcli-toolkit/
|
|
12
|
+
Keywords: terminal,tui,pty,ansi,cli,ascii-art,terminal-effects,claude-skill,pyte,conpty
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Terminals
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
25
|
+
Classifier: Environment :: Console
|
|
6
26
|
Requires-Python: >=3.9
|
|
7
27
|
Description-Content-Type: text/markdown
|
|
8
28
|
License-File: LICENSE
|
|
@@ -66,7 +86,7 @@ works today from a clean clone.
|
|
|
66
86
|
**Primary — reproduce the full dev environment** (recommended):
|
|
67
87
|
|
|
68
88
|
```bash
|
|
69
|
-
git clone
|
|
89
|
+
git clone https://github.com/dwgx/SmartCLI SmartCLI
|
|
70
90
|
cd SmartCLI
|
|
71
91
|
python -m pip install -r requirements.txt
|
|
72
92
|
```
|
|
@@ -44,7 +44,7 @@ works today from a clean clone.
|
|
|
44
44
|
**Primary — reproduce the full dev environment** (recommended):
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
git clone
|
|
47
|
+
git clone https://github.com/dwgx/SmartCLI SmartCLI
|
|
48
48
|
cd SmartCLI
|
|
49
49
|
python -m pip install -r requirements.txt
|
|
50
50
|
```
|
|
@@ -30,7 +30,7 @@ build-backend = "setuptools.build_meta"
|
|
|
30
30
|
|
|
31
31
|
[project]
|
|
32
32
|
name = "smartcli-toolkit"
|
|
33
|
-
version = "0.1.
|
|
33
|
+
version = "0.1.1"
|
|
34
34
|
description = "Pluggable-PTY + pyte screen model + semantic snapshot / readiness core for driving interactive terminal programs (SmartCLI shared core)."
|
|
35
35
|
readme = "README.md"
|
|
36
36
|
license = "MIT"
|
|
@@ -40,6 +40,41 @@ license-files = ["LICENSE"]
|
|
|
40
40
|
# No match/case and no runtime `X | Y` types are used, so nothing forces higher.
|
|
41
41
|
requires-python = ">=3.9"
|
|
42
42
|
|
|
43
|
+
authors = [{name = "dwgx"}]
|
|
44
|
+
|
|
45
|
+
keywords = [
|
|
46
|
+
"terminal",
|
|
47
|
+
"tui",
|
|
48
|
+
"pty",
|
|
49
|
+
"ansi",
|
|
50
|
+
"cli",
|
|
51
|
+
"ascii-art",
|
|
52
|
+
"terminal-effects",
|
|
53
|
+
"claude-skill",
|
|
54
|
+
"pyte",
|
|
55
|
+
"conpty",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
# Real PyPI trove classifiers. NOTE: no `License ::` classifier is listed on
|
|
59
|
+
# purpose — with the PEP 639 SPDX `license = "MIT"` field above, setuptools
|
|
60
|
+
# (77+) treats a license trove classifier as redundant/conflicting and errors
|
|
61
|
+
# out. The SPDX field is the single source of truth for licensing.
|
|
62
|
+
classifiers = [
|
|
63
|
+
"Development Status :: 4 - Beta",
|
|
64
|
+
"Intended Audience :: Developers",
|
|
65
|
+
"Operating System :: OS Independent",
|
|
66
|
+
"Programming Language :: Python :: 3",
|
|
67
|
+
"Programming Language :: Python :: 3.9",
|
|
68
|
+
"Programming Language :: Python :: 3.10",
|
|
69
|
+
"Programming Language :: Python :: 3.11",
|
|
70
|
+
"Programming Language :: Python :: 3.12",
|
|
71
|
+
"Programming Language :: Python :: 3.13",
|
|
72
|
+
"Programming Language :: Python :: 3.14",
|
|
73
|
+
"Topic :: Terminals",
|
|
74
|
+
"Topic :: Software Development :: Libraries",
|
|
75
|
+
"Environment :: Console",
|
|
76
|
+
]
|
|
77
|
+
|
|
43
78
|
# REQUIRED runtime deps (mirror requirements.txt). pyte is a hard top-level
|
|
44
79
|
# import; pywinpty is imported only on the Windows PTY backend path, hence the
|
|
45
80
|
# environment marker. POSIX uses the stdlib `pty` backend (no extra package).
|
|
@@ -48,6 +83,13 @@ dependencies = [
|
|
|
48
83
|
"pywinpty>=2.0; platform_system == 'Windows'",
|
|
49
84
|
]
|
|
50
85
|
|
|
86
|
+
[project.urls]
|
|
87
|
+
Homepage = "https://github.com/dwgx/SmartCLI"
|
|
88
|
+
Repository = "https://github.com/dwgx/SmartCLI"
|
|
89
|
+
Changelog = "https://github.com/dwgx/SmartCLI/blob/main/CHANGELOG.md"
|
|
90
|
+
Issues = "https://github.com/dwgx/SmartCLI/issues"
|
|
91
|
+
PyPI = "https://pypi.org/project/smartcli-toolkit/"
|
|
92
|
+
|
|
51
93
|
[project.optional-dependencies]
|
|
52
94
|
# All guarded imports with pure-stdlib fallbacks — none required to run.
|
|
53
95
|
art = ["pyfiglet>=1.0.0"] # real FIGlet fonts (else built-in block font)
|
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: smartcli-toolkit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Pluggable-PTY + pyte screen model + semantic snapshot / readiness core for driving interactive terminal programs (SmartCLI shared core).
|
|
5
|
+
Author: dwgx
|
|
5
6
|
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dwgx/SmartCLI
|
|
8
|
+
Project-URL: Repository, https://github.com/dwgx/SmartCLI
|
|
9
|
+
Project-URL: Changelog, https://github.com/dwgx/SmartCLI/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/dwgx/SmartCLI/issues
|
|
11
|
+
Project-URL: PyPI, https://pypi.org/project/smartcli-toolkit/
|
|
12
|
+
Keywords: terminal,tui,pty,ansi,cli,ascii-art,terminal-effects,claude-skill,pyte,conpty
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Terminals
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
25
|
+
Classifier: Environment :: Console
|
|
6
26
|
Requires-Python: >=3.9
|
|
7
27
|
Description-Content-Type: text/markdown
|
|
8
28
|
License-File: LICENSE
|
|
@@ -66,7 +86,7 @@ works today from a clean clone.
|
|
|
66
86
|
**Primary — reproduce the full dev environment** (recommended):
|
|
67
87
|
|
|
68
88
|
```bash
|
|
69
|
-
git clone
|
|
89
|
+
git clone https://github.com/dwgx/SmartCLI SmartCLI
|
|
70
90
|
cd SmartCLI
|
|
71
91
|
python -m pip install -r requirements.txt
|
|
72
92
|
```
|
|
@@ -11,4 +11,5 @@ smartcli_toolkit.egg-info/PKG-INFO
|
|
|
11
11
|
smartcli_toolkit.egg-info/SOURCES.txt
|
|
12
12
|
smartcli_toolkit.egg-info/dependency_links.txt
|
|
13
13
|
smartcli_toolkit.egg-info/requires.txt
|
|
14
|
-
smartcli_toolkit.egg-info/top_level.txt
|
|
14
|
+
smartcli_toolkit.egg-info/top_level.txt
|
|
15
|
+
tests/test_readiness.py
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""test_readiness.py — deterministic unit tests for smartcli_core.readiness.
|
|
3
|
+
|
|
4
|
+
These cover the failure/edge paths that historically broke and are otherwise only
|
|
5
|
+
reachable through flaky real-PTY timing:
|
|
6
|
+
* TIMEOUT — a screen that never settles / a marker that never matches.
|
|
7
|
+
* STABLE — the content hash repeats long enough to declare stability.
|
|
8
|
+
* MARKER — an expected regex is found (beats stability).
|
|
9
|
+
* late-flush — data arrives after the grace sleep → the wait resumes instead of
|
|
10
|
+
declaring done on a half-drawn screen (the ConPTY quiet-gap bug).
|
|
11
|
+
* min_wait — stability/marker is NOT declared before the minimum elapsed time.
|
|
12
|
+
|
|
13
|
+
Determinism: we monkeypatch ``readiness.time.monotonic`` / ``readiness.time.sleep``
|
|
14
|
+
with a virtual clock so ``sleep(x)`` advances virtual time by ``x`` and timeouts
|
|
15
|
+
fire instantly with no real waiting. This patches only the clock BOUNDARY — the
|
|
16
|
+
real decision logic in readiness.py runs unchanged (HARD-LESSONS rule 5: a test
|
|
17
|
+
double for the clock/IO edge, never a patch that hides a bug).
|
|
18
|
+
|
|
19
|
+
Run: python tests/test_readiness.py (exit 0 = pass, 1 = fail)
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
# Portable: make the repo root importable regardless of cwd.
|
|
28
|
+
_ROOT = Path(__file__).resolve().parents[1]
|
|
29
|
+
if str(_ROOT) not in sys.path:
|
|
30
|
+
sys.path.insert(0, str(_ROOT))
|
|
31
|
+
|
|
32
|
+
from smartcli_core import readiness # noqa: E402
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class VirtualClock:
|
|
36
|
+
"""A fake monotonic clock: sleep() advances virtual time; no real waiting."""
|
|
37
|
+
|
|
38
|
+
def __init__(self) -> None:
|
|
39
|
+
self.t = 0.0
|
|
40
|
+
|
|
41
|
+
def monotonic(self) -> float:
|
|
42
|
+
return self.t
|
|
43
|
+
|
|
44
|
+
def sleep(self, seconds: float) -> None:
|
|
45
|
+
# Advance virtual time; also nudge forward on zero-sleep to guarantee
|
|
46
|
+
# progress so no loop can spin forever in a test.
|
|
47
|
+
self.t += seconds if seconds > 0 else 0.001
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _install_clock():
|
|
51
|
+
clk = VirtualClock()
|
|
52
|
+
readiness.time.monotonic = clk.monotonic # type: ignore[assignment]
|
|
53
|
+
readiness.time.sleep = clk.sleep # type: ignore[assignment]
|
|
54
|
+
return clk
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# scripted-callable helpers ------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def seq_reader(batches):
|
|
60
|
+
"""read_fn that returns each batch in turn, then b'' forever."""
|
|
61
|
+
it = iter(batches)
|
|
62
|
+
|
|
63
|
+
def _read():
|
|
64
|
+
return next(it, b"")
|
|
65
|
+
|
|
66
|
+
return _read
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def seq_hashes(values):
|
|
70
|
+
"""hash_fn that returns each value in turn, then repeats the last forever."""
|
|
71
|
+
state = {"i": 0, "vals": list(values)}
|
|
72
|
+
|
|
73
|
+
def _hash():
|
|
74
|
+
i = state["i"]
|
|
75
|
+
vals = state["vals"]
|
|
76
|
+
v = vals[i] if i < len(vals) else vals[-1]
|
|
77
|
+
if i < len(vals) - 1:
|
|
78
|
+
state["i"] = i + 1
|
|
79
|
+
return v
|
|
80
|
+
|
|
81
|
+
return _hash
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def seq_text(values):
|
|
85
|
+
"""text_fn that returns each value in turn, then repeats the last forever."""
|
|
86
|
+
state = {"i": 0, "vals": list(values)}
|
|
87
|
+
|
|
88
|
+
def _text():
|
|
89
|
+
i = state["i"]
|
|
90
|
+
vals = state["vals"]
|
|
91
|
+
v = vals[i] if i < len(vals) else vals[-1]
|
|
92
|
+
if i < len(vals) - 1:
|
|
93
|
+
state["i"] = i + 1
|
|
94
|
+
return v
|
|
95
|
+
|
|
96
|
+
return _text
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
_fails = []
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def check(cond, name, detail=""):
|
|
103
|
+
tag = "[PASS]" if cond else "[FAIL]"
|
|
104
|
+
print(f"{tag} {name} {detail}")
|
|
105
|
+
if not cond:
|
|
106
|
+
_fails.append(name)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_stable_reached():
|
|
110
|
+
"""Hash unchanged + no new bytes for quiet_ms → wait_until_stable True, fast."""
|
|
111
|
+
_install_clock()
|
|
112
|
+
ok = readiness.wait_until_stable(
|
|
113
|
+
read_fn=seq_reader([]), # no bytes ever
|
|
114
|
+
get_screen_hash_fn=seq_hashes([42]), # constant hash
|
|
115
|
+
quiet_ms=200, poll_ms=30, max_wait_ms=8000, grace_ms=0, min_wait_ms=0,
|
|
116
|
+
)
|
|
117
|
+
check(ok is True, "wait_until_stable: constant screen settles", f"ret={ok}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_stable_timeout():
|
|
121
|
+
"""Hash never stops changing → returns False (TIMEOUT) within budget, no hang."""
|
|
122
|
+
clk = _install_clock()
|
|
123
|
+
# hash flips forever: 0,1,0,1,... → never quiet
|
|
124
|
+
flip = {"n": 0}
|
|
125
|
+
|
|
126
|
+
def churn_hash():
|
|
127
|
+
flip["n"] ^= 1
|
|
128
|
+
return flip["n"]
|
|
129
|
+
|
|
130
|
+
ok = readiness.wait_until_stable(
|
|
131
|
+
read_fn=seq_reader([]),
|
|
132
|
+
get_screen_hash_fn=churn_hash,
|
|
133
|
+
quiet_ms=200, poll_ms=30, max_wait_ms=1000, grace_ms=0, min_wait_ms=0,
|
|
134
|
+
)
|
|
135
|
+
check(ok is False, "wait_until_stable: churning screen hits TIMEOUT", f"ret={ok}")
|
|
136
|
+
check(clk.t >= 1.0, "wait_until_stable: respected max_wait ceiling", f"virt_t={clk.t:.2f}s")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_late_flush_resume():
|
|
140
|
+
"""A late flush after grace resumes the wait instead of declaring done early.
|
|
141
|
+
|
|
142
|
+
We inject exactly one late batch on the post-grace `tail = read_fn()` drain.
|
|
143
|
+
readiness must consume it, reset stability, and only return True once the
|
|
144
|
+
screen re-settles — never return on the half-drawn (pre-flush) screen.
|
|
145
|
+
"""
|
|
146
|
+
_install_clock()
|
|
147
|
+
reads = {"n": 0}
|
|
148
|
+
flush_at = {"call": None} # records which read_fn call delivered the flush
|
|
149
|
+
|
|
150
|
+
def read_fn():
|
|
151
|
+
reads["n"] += 1
|
|
152
|
+
# Deliver the late flush the first time we've gone quiet long enough that
|
|
153
|
+
# a grace-drain read happens (n>=4 in this cadence), exactly once.
|
|
154
|
+
if flush_at["call"] is None and reads["n"] >= 4:
|
|
155
|
+
flush_at["call"] = reads["n"]
|
|
156
|
+
return b"late output"
|
|
157
|
+
return b""
|
|
158
|
+
|
|
159
|
+
def hash_fn():
|
|
160
|
+
# screen changes once the flush has been delivered, then settles at 2
|
|
161
|
+
return 2 if flush_at["call"] is not None else 1
|
|
162
|
+
|
|
163
|
+
ok = readiness.wait_until_stable(
|
|
164
|
+
read_fn=read_fn,
|
|
165
|
+
get_screen_hash_fn=hash_fn,
|
|
166
|
+
quiet_ms=60, poll_ms=30, max_wait_ms=5000, grace_ms=40, min_wait_ms=0,
|
|
167
|
+
)
|
|
168
|
+
# Main contract: it re-settled to True AFTER a late flush was consumed.
|
|
169
|
+
check(ok is True, "wait_until_stable: resumes after late flush then settles", f"ret={ok}")
|
|
170
|
+
# A late flush was actually delivered and consumed, and reads continued
|
|
171
|
+
# past that point (so it did NOT return on the stale pre-flush screen).
|
|
172
|
+
check(flush_at["call"] is not None, "wait_until_stable: late flush was delivered", f"flush_at={flush_at['call']}")
|
|
173
|
+
check(reads["n"] > flush_at["call"], "wait_until_stable: kept reading past the late flush",
|
|
174
|
+
f"reads={reads['n']} flush_at={flush_at['call']}")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_min_wait_guard():
|
|
178
|
+
"""Stability must NOT be declared before min_wait_ms even if screen is quiet."""
|
|
179
|
+
clk = _install_clock()
|
|
180
|
+
ok = readiness.wait_until_stable(
|
|
181
|
+
read_fn=seq_reader([]),
|
|
182
|
+
get_screen_hash_fn=seq_hashes([7]), # instantly "quiet"
|
|
183
|
+
quiet_ms=50, poll_ms=30, max_wait_ms=8000, grace_ms=0, min_wait_ms=500,
|
|
184
|
+
)
|
|
185
|
+
check(ok is True, "wait_until_stable: settles after min_wait", f"ret={ok}")
|
|
186
|
+
check(clk.t >= 0.5, "wait_until_stable: honored min_wait_ms floor", f"virt_t={clk.t:.3f}s (>=0.5)")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_regex_match():
|
|
190
|
+
"""wait_for_regex finds the marker and returns (True, snapshot)."""
|
|
191
|
+
_install_clock()
|
|
192
|
+
matched, snap = readiness.wait_for_regex(
|
|
193
|
+
read_fn=seq_reader([b">>> "]),
|
|
194
|
+
get_text_fn=seq_text(["loading...", "loading...", ">>> ready"]),
|
|
195
|
+
get_snapshot_fn=lambda: "SNAP",
|
|
196
|
+
pattern=r">>> ", timeout_ms=5000, poll_ms=30, min_wait_ms=0,
|
|
197
|
+
)
|
|
198
|
+
check(matched is True, "wait_for_regex: finds marker", f"matched={matched}")
|
|
199
|
+
check(snap == "SNAP", "wait_for_regex: returns snapshot on match", f"snap={snap}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_regex_timeout_returns_snapshot():
|
|
203
|
+
"""wait_for_regex on a never-matching pattern returns (False, snapshot)."""
|
|
204
|
+
clk = _install_clock()
|
|
205
|
+
matched, snap = readiness.wait_for_regex(
|
|
206
|
+
read_fn=seq_reader([]),
|
|
207
|
+
get_text_fn=seq_text(["nothing here"]),
|
|
208
|
+
get_snapshot_fn=lambda: "LAST",
|
|
209
|
+
pattern=r"WILL_NEVER_APPEAR", timeout_ms=800, poll_ms=30, min_wait_ms=0,
|
|
210
|
+
)
|
|
211
|
+
check(matched is False, "wait_for_regex: TIMEOUT when no match", f"matched={matched}")
|
|
212
|
+
check(snap == "LAST", "wait_for_regex: still returns last snapshot on timeout", f"snap={snap}")
|
|
213
|
+
check(clk.t >= 0.8, "wait_for_regex: respected timeout ceiling", f"virt_t={clk.t:.2f}s")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_regex_min_wait():
|
|
217
|
+
"""A match present from t=0 must be ignored until min_wait_ms elapses."""
|
|
218
|
+
clk = _install_clock()
|
|
219
|
+
matched, _ = readiness.wait_for_regex(
|
|
220
|
+
read_fn=seq_reader([]),
|
|
221
|
+
get_text_fn=seq_text([">>> "]), # matches immediately
|
|
222
|
+
get_snapshot_fn=lambda: None,
|
|
223
|
+
pattern=r">>> ", timeout_ms=5000, poll_ms=30, min_wait_ms=300,
|
|
224
|
+
)
|
|
225
|
+
check(matched is True, "wait_for_regex: eventually matches past min_wait", f"matched={matched}")
|
|
226
|
+
check(clk.t >= 0.3, "wait_for_regex: did not match before min_wait", f"virt_t={clk.t:.3f}s (>=0.3)")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_wait_ready_marker_beats_stability():
|
|
230
|
+
"""wait_ready returns MARKER when the regex hits (even amid churn)."""
|
|
231
|
+
_install_clock()
|
|
232
|
+
# screen keeps changing (never stable) but marker appears on 3rd text sample
|
|
233
|
+
reason, snap = readiness.wait_ready(
|
|
234
|
+
read_fn=seq_reader([b"a", b"b", b"c"]),
|
|
235
|
+
get_screen_hash_fn=seq_hashes([1, 2, 3, 4, 5]), # churning
|
|
236
|
+
get_text_fn=seq_text(["...", "...", "DONE!"]),
|
|
237
|
+
get_snapshot_fn=lambda: "S",
|
|
238
|
+
marker=r"DONE!", quiet_ms=200, poll_ms=30, max_wait_ms=5000, min_wait_ms=0,
|
|
239
|
+
)
|
|
240
|
+
check(reason == "MARKER", "wait_ready: marker beats a churning screen", f"reason={reason}")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_wait_ready_stable():
|
|
244
|
+
"""wait_ready returns STABLE when no marker and screen settles."""
|
|
245
|
+
_install_clock()
|
|
246
|
+
reason, _ = readiness.wait_ready(
|
|
247
|
+
read_fn=seq_reader([]),
|
|
248
|
+
get_screen_hash_fn=seq_hashes([9]),
|
|
249
|
+
get_text_fn=seq_text(["idle"]),
|
|
250
|
+
get_snapshot_fn=lambda: "S",
|
|
251
|
+
marker=None, quiet_ms=100, poll_ms=30, max_wait_ms=5000, min_wait_ms=0, grace_ms=0,
|
|
252
|
+
)
|
|
253
|
+
check(reason == "STABLE", "wait_ready: settles to STABLE with no marker", f"reason={reason}")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_wait_ready_timeout():
|
|
257
|
+
"""wait_ready returns TIMEOUT when marker never matches and screen churns."""
|
|
258
|
+
clk = _install_clock()
|
|
259
|
+
flip = {"n": 0}
|
|
260
|
+
|
|
261
|
+
def churn_hash():
|
|
262
|
+
flip["n"] ^= 1
|
|
263
|
+
return flip["n"]
|
|
264
|
+
|
|
265
|
+
reason, snap = readiness.wait_ready(
|
|
266
|
+
read_fn=seq_reader([b"x", b"y", b"z"] * 100), # bytes keep coming
|
|
267
|
+
get_screen_hash_fn=churn_hash,
|
|
268
|
+
get_text_fn=seq_text(["busy"]),
|
|
269
|
+
get_snapshot_fn=lambda: "LASTSNAP",
|
|
270
|
+
marker=r"NOPE", quiet_ms=200, poll_ms=30, max_wait_ms=1000, min_wait_ms=0,
|
|
271
|
+
)
|
|
272
|
+
check(reason == "TIMEOUT", "wait_ready: TIMEOUT when nothing satisfies", f"reason={reason}")
|
|
273
|
+
check(snap == "LASTSNAP", "wait_ready: returns last snapshot on timeout", f"snap={snap}")
|
|
274
|
+
check(clk.t >= 1.0, "wait_ready: respected max_wait ceiling", f"virt_t={clk.t:.2f}s")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def main() -> int:
|
|
278
|
+
import time as _real_time
|
|
279
|
+
t0 = _real_time.perf_counter()
|
|
280
|
+
print("=" * 60)
|
|
281
|
+
print("readiness deterministic unit tests (virtual clock, no real waits)")
|
|
282
|
+
print("=" * 60)
|
|
283
|
+
for fn in (
|
|
284
|
+
test_stable_reached, test_stable_timeout, test_late_flush_resume,
|
|
285
|
+
test_min_wait_guard, test_regex_match, test_regex_timeout_returns_snapshot,
|
|
286
|
+
test_regex_min_wait, test_wait_ready_marker_beats_stability,
|
|
287
|
+
test_wait_ready_stable, test_wait_ready_timeout,
|
|
288
|
+
):
|
|
289
|
+
fn()
|
|
290
|
+
wall = _real_time.perf_counter() - t0
|
|
291
|
+
print("-" * 60)
|
|
292
|
+
if _fails:
|
|
293
|
+
print(f"FAIL: {len(_fails)} check(s) failed: {_fails}")
|
|
294
|
+
return 1
|
|
295
|
+
print(f"PASS: all readiness edge paths covered in {wall*1000:.0f} ms wall (deterministic)")
|
|
296
|
+
return 0
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
if __name__ == "__main__":
|
|
300
|
+
sys.exit(main())
|
|
301
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{smartcli_toolkit-0.1.0 → smartcli_toolkit-0.1.1}/smartcli_toolkit.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|