python-aidot-cameras 0.5.8__tar.gz → 0.5.10__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.
- {python_aidot_cameras-0.5.8/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.5.10}/PKG-INFO +1 -1
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/pyproject.toml +1 -1
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/src/aidot/device_client.py +118 -3
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10/src/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/src/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
- python_aidot_cameras-0.5.10/tests/test_sdes_idle_release.py +143 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/LICENSE +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/README.md +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/setup.cfg +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/src/aidot/__init__.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/src/aidot/aes_utils.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/src/aidot/client.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/src/aidot/const.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/src/aidot/credentials.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/src/aidot/discover.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/src/aidot/exceptions.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/src/aidot/g711.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/src/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/src/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/src/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/tests/test_token_refresh.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.10
|
|
4
4
|
Summary: Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)
|
|
5
5
|
Author-email: cbrightly <chris.brightly@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.10"
|
|
8
8
|
description = "Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -2059,8 +2059,11 @@ def _save_sprop(devid: str, sprop: str) -> None:
|
|
|
2059
2059
|
with open(tmp, "w") as fh:
|
|
2060
2060
|
fh.write(sprop)
|
|
2061
2061
|
os.replace(tmp, _sprop_cache_path(devid))
|
|
2062
|
-
except OSError:
|
|
2063
|
-
|
|
2062
|
+
except OSError as exc:
|
|
2063
|
+
# Surface (don't swallow): if the cache dir isn't writable the whole
|
|
2064
|
+
# sprop feature is silently inert. AIDOT_SPROP_DIR can redirect it.
|
|
2065
|
+
_LOGGER.warning("sprop cache write failed (%s): %s — set AIDOT_SPROP_DIR "
|
|
2066
|
+
"to a writable path", _SPROP_DIR, exc)
|
|
2064
2067
|
|
|
2065
2068
|
|
|
2066
2069
|
def _inject_sprop(sdp: str, devid: str) -> str:
|
|
@@ -2080,6 +2083,53 @@ def _inject_sprop(sdp: str, devid: str) -> str:
|
|
|
2080
2083
|
return "\r\n".join(out)
|
|
2081
2084
|
|
|
2082
2085
|
|
|
2086
|
+
def _sdes_serve_port(url: "Optional[str]") -> "Optional[int]":
|
|
2087
|
+
"""Extract the TCP port from an SDES serve URL (``http://127.0.0.1:PORT/x.ts``).
|
|
2088
|
+
|
|
2089
|
+
Returns None if the URL is missing/malformed. Pure (unit-testable)."""
|
|
2090
|
+
if not url:
|
|
2091
|
+
return None
|
|
2092
|
+
try:
|
|
2093
|
+
return int(url.rsplit(":", 1)[1].split("/", 1)[0])
|
|
2094
|
+
except (ValueError, IndexError):
|
|
2095
|
+
return None
|
|
2096
|
+
|
|
2097
|
+
|
|
2098
|
+
def _tcp_table_has_established_on_port(table_text: str, port: int) -> bool:
|
|
2099
|
+
"""True if any row in a /proc/net/tcp[6] dump is ESTABLISHED on local ``port``.
|
|
2100
|
+
|
|
2101
|
+
/proc/net/tcp columns: ``sl local_address rem_address st ...`` where
|
|
2102
|
+
local_address is ``HEXIP:HEXPORT`` (port is upper-hex, %04X) and ``st`` is the
|
|
2103
|
+
connection state (``01`` == ESTABLISHED, ``0A`` == LISTEN). A LISTEN socket
|
|
2104
|
+
(ffmpeg ``-listen 1`` waiting) is NOT a consumer; only an ESTABLISHED peer is.
|
|
2105
|
+
Pure function so the no-viewer policy is unit-testable without a live host."""
|
|
2106
|
+
target = f"{port:04X}"
|
|
2107
|
+
for line in table_text.splitlines()[1:]: # skip header row
|
|
2108
|
+
cols = line.split()
|
|
2109
|
+
if len(cols) < 4:
|
|
2110
|
+
continue
|
|
2111
|
+
local, st = cols[1], cols[3]
|
|
2112
|
+
if ":" not in local:
|
|
2113
|
+
continue
|
|
2114
|
+
if local.rsplit(":", 1)[1].upper() == target and st == "01":
|
|
2115
|
+
return True
|
|
2116
|
+
return False
|
|
2117
|
+
|
|
2118
|
+
|
|
2119
|
+
def _idle_release_due(present, last_consumer: float, now: float,
|
|
2120
|
+
idle_secs: float) -> bool:
|
|
2121
|
+
"""Whether a viewerless SDES keepalive should release.
|
|
2122
|
+
|
|
2123
|
+
``present``: True (a TCP consumer is connected to the serve port), False
|
|
2124
|
+
(none), or None (the table was unreadable - unknown). Release ONLY when
|
|
2125
|
+
we're certain there's no consumer (False) AND the idle window has elapsed;
|
|
2126
|
+
True keeps it alive and None never releases (fail-safe on non-Linux). Pure
|
|
2127
|
+
so the policy is unit-testable without a live loop."""
|
|
2128
|
+
if present is not False:
|
|
2129
|
+
return False
|
|
2130
|
+
return (now - last_consumer) > idle_secs
|
|
2131
|
+
|
|
2132
|
+
|
|
2083
2133
|
def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
|
|
2084
2134
|
"""Mux tapped video (H.264 copy) + audio (PCMA->AAC) to ``out_fileobj`` as
|
|
2085
2135
|
RTP-timestamped MPEG-TS. Runs in a worker thread; the serve's ffmpeg reads
|
|
@@ -4276,6 +4326,24 @@ class DeviceClient(object):
|
|
|
4276
4326
|
await session.stop()
|
|
4277
4327
|
return True
|
|
4278
4328
|
|
|
4329
|
+
def _sdes_serve_consumer_present(self, port: int) -> "Optional[bool]":
|
|
4330
|
+
"""True if a TCP client is connected to the SDES serve ``port`` (a viewer
|
|
4331
|
+
is pulling), False if none, None if the TCP table can't be read (non-Linux
|
|
4332
|
+
/ sandboxed host). The caller MUST treat None as 'unknown' and NOT release
|
|
4333
|
+
— failing safe to the current always-reconnect behaviour."""
|
|
4334
|
+
read_any = False
|
|
4335
|
+
found = False
|
|
4336
|
+
for _path in ("/proc/net/tcp", "/proc/net/tcp6"):
|
|
4337
|
+
try:
|
|
4338
|
+
with open(_path, "r") as _fh:
|
|
4339
|
+
_txt = _fh.read()
|
|
4340
|
+
except OSError:
|
|
4341
|
+
continue
|
|
4342
|
+
read_any = True
|
|
4343
|
+
if _tcp_table_has_established_on_port(_txt, port):
|
|
4344
|
+
found = True
|
|
4345
|
+
return found if read_any else None
|
|
4346
|
+
|
|
4279
4347
|
async def _sdes_keepalive_loop(self) -> None:
|
|
4280
4348
|
"""Background task: keep SDES stream alive; push to go2rtc via RTSP."""
|
|
4281
4349
|
_MIN_DELAY = 10.0
|
|
@@ -4312,6 +4380,20 @@ class DeviceClient(object):
|
|
|
4312
4380
|
_started_at = time.monotonic()
|
|
4313
4381
|
_done = asyncio.ensure_future(session.wait_done())
|
|
4314
4382
|
_stalled = False
|
|
4383
|
+
_idle_release = False
|
|
4384
|
+
# No-viewer release. Unlike the DTLS serve (which idle-releases when
|
|
4385
|
+
# its mux pipe goes stale), an SDES keepalive otherwise reconnects
|
|
4386
|
+
# FOREVER even with zero HA consumers — a battery-draining orphan that
|
|
4387
|
+
# also holds a stream slot and TURN-relay bandwidth. SDES has no mux
|
|
4388
|
+
# pipe to watch, so detect "nobody is pulling" via an ESTABLISHED TCP
|
|
4389
|
+
# connection on the -listen serve port and release after the same idle
|
|
4390
|
+
# window as DTLS; the next view re-runs camera.stream_source(). Fail
|
|
4391
|
+
# safe: unknown (non-Linux /proc) never releases. Escape hatch:
|
|
4392
|
+
# AIDOT_SDES_IDLE_RELEASE=0.
|
|
4393
|
+
_idle_on = os.environ.get("AIDOT_SDES_IDLE_RELEASE", "1") != "0"
|
|
4394
|
+
_idle_secs = float(os.environ.get("AIDOT_STREAM_IDLE_S", "120"))
|
|
4395
|
+
_serve_port = _sdes_serve_port(self._keepalive_rtsp_url)
|
|
4396
|
+
_last_consumer = _started_at # grace: count idle from session open
|
|
4315
4397
|
try:
|
|
4316
4398
|
while True:
|
|
4317
4399
|
_fin, _ = await asyncio.wait({_done}, timeout=5.0)
|
|
@@ -4324,6 +4406,15 @@ class DeviceClient(object):
|
|
|
4324
4406
|
):
|
|
4325
4407
|
_stalled = True
|
|
4326
4408
|
break
|
|
4409
|
+
if _idle_on and _serve_port is not None:
|
|
4410
|
+
_present = self._sdes_serve_consumer_present(_serve_port)
|
|
4411
|
+
if _present: # True → a viewer is pulling; stay alive
|
|
4412
|
+
_last_consumer = time.monotonic()
|
|
4413
|
+
elif _idle_release_due(_present, _last_consumer,
|
|
4414
|
+
time.monotonic(), _idle_secs):
|
|
4415
|
+
_idle_release = True
|
|
4416
|
+
break
|
|
4417
|
+
# _present is None (unreadable table) → don't release
|
|
4327
4418
|
except asyncio.CancelledError:
|
|
4328
4419
|
_done.cancel()
|
|
4329
4420
|
self._stream_session = None
|
|
@@ -4332,6 +4423,25 @@ class DeviceClient(object):
|
|
|
4332
4423
|
finally:
|
|
4333
4424
|
self._stream_session = None
|
|
4334
4425
|
|
|
4426
|
+
if _idle_release:
|
|
4427
|
+
# No HA consumer for the idle window: go dormant instead of
|
|
4428
|
+
# reconnecting forever. camera.stream_source() restarts us (and
|
|
4429
|
+
# the camera.py stale-stream watchdog evicts HA's cached stream
|
|
4430
|
+
# once stream_rtsp_url is None) when someone opens the view again.
|
|
4431
|
+
_done.cancel()
|
|
4432
|
+
try:
|
|
4433
|
+
await session.stop()
|
|
4434
|
+
except Exception:
|
|
4435
|
+
pass
|
|
4436
|
+
self._streaming_active = False
|
|
4437
|
+
self._keepalive_rtsp_url = None
|
|
4438
|
+
self._serve_ready.clear()
|
|
4439
|
+
_LOGGER.debug(
|
|
4440
|
+
"SDES serve: %s idle (no viewer) - released until next view",
|
|
4441
|
+
self.device_id,
|
|
4442
|
+
)
|
|
4443
|
+
return
|
|
4444
|
+
|
|
4335
4445
|
if _stalled:
|
|
4336
4446
|
_LOGGER.info(
|
|
4337
4447
|
"SDES %s: no media in watchdog window - restarting stream",
|
|
@@ -12098,8 +12208,13 @@ class DeviceClient(object):
|
|
|
12098
12208
|
"a=rtcp-mux\r\n"
|
|
12099
12209
|
)
|
|
12100
12210
|
try:
|
|
12211
|
+
# Re-apply the cached sprop-parameter-sets here too:
|
|
12212
|
+
# this serve-restart rewrite is the SDP the keepalive
|
|
12213
|
+
# ffmpeg actually reads on every watchdog cycle, so
|
|
12214
|
+
# without this the out-of-band SPS is lost on restart
|
|
12215
|
+
# (the failure persisted live even for cached cameras).
|
|
12101
12216
|
with open(sdp_path, "w") as _f2:
|
|
12102
|
-
_f2.write(_new_sdp)
|
|
12217
|
+
_f2.write(_inject_sprop(_new_sdp, self.device_id))
|
|
12103
12218
|
except Exception as _sdp_exc2:
|
|
12104
12219
|
_LOGGER.warning("could not rewrite SDP for restart: %s", _sdp_exc2)
|
|
12105
12220
|
proc = subprocess.Popen(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.10
|
|
4
4
|
Summary: Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)
|
|
5
5
|
Author-email: cbrightly <chris.brightly@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Unit tests for the SDES no-viewer idle-release policy helpers.
|
|
2
|
+
|
|
3
|
+
An SDES keepalive otherwise reconnects forever even with zero HA consumers
|
|
4
|
+
(a battery-draining orphan). The loop releases when no TCP client is connected
|
|
5
|
+
to the -listen serve port for the idle window. These pure helpers decide that
|
|
6
|
+
policy; no camera/network/host needed.
|
|
7
|
+
|
|
8
|
+
- _sdes_serve_port: pull the TCP port out of the serve URL.
|
|
9
|
+
- _tcp_table_has_established_on_port: is a consumer ESTABLISHED on that port?
|
|
10
|
+
(A LISTEN socket - ffmpeg waiting - must NOT count as a consumer.)
|
|
11
|
+
"""
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
16
|
+
|
|
17
|
+
from aidot.device_client import (
|
|
18
|
+
_sdes_serve_port,
|
|
19
|
+
_tcp_table_has_established_on_port,
|
|
20
|
+
_idle_release_due,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_PORT = 18981 # 0x4A25
|
|
24
|
+
_HEX = f"{_PORT:04X}" # "4A25"
|
|
25
|
+
|
|
26
|
+
_HEADER = (
|
|
27
|
+
" sl local_address rem_address st tx_queue rx_queue tr tm->when "
|
|
28
|
+
"retrnsmt uid timeout inode"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _row(local_port_hex, st, remote="0100007F:9C40"):
|
|
33
|
+
return (
|
|
34
|
+
f" 0: 0100007F:{local_port_hex} {remote} {st} "
|
|
35
|
+
"00000000:00000000 00:00000000 00000000 0 0 99999 1"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---- _sdes_serve_port ------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
def test_serve_port_parsed():
|
|
42
|
+
assert _sdes_serve_port("http://127.0.0.1:18981/abc.ts") == 18981
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_serve_port_none_url():
|
|
46
|
+
assert _sdes_serve_port(None) is None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_serve_port_malformed():
|
|
50
|
+
assert _sdes_serve_port("not-a-url") is None
|
|
51
|
+
assert _sdes_serve_port("http://127.0.0.1:notaport/x.ts") is None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---- _tcp_table_has_established_on_port ------------------------------------
|
|
55
|
+
|
|
56
|
+
def test_established_consumer_present():
|
|
57
|
+
table = _HEADER + "\n" + _row(_HEX, "01") # 01 == ESTABLISHED
|
|
58
|
+
assert _tcp_table_has_established_on_port(table, _PORT) is True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_listen_only_is_not_a_consumer():
|
|
62
|
+
# ffmpeg -listen 1 waiting (0A == LISTEN) is not a viewer pulling.
|
|
63
|
+
table = _HEADER + "\n" + _row(_HEX, "0A")
|
|
64
|
+
assert _tcp_table_has_established_on_port(table, _PORT) is False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_established_on_other_port_ignored():
|
|
68
|
+
table = _HEADER + "\n" + _row("0050", "01") # ESTABLISHED on :80, not ours
|
|
69
|
+
assert _tcp_table_has_established_on_port(table, _PORT) is False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_empty_or_header_only_table():
|
|
73
|
+
assert _tcp_table_has_established_on_port("", _PORT) is False
|
|
74
|
+
assert _tcp_table_has_established_on_port(_HEADER, _PORT) is False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_tcp6_long_local_address_parses():
|
|
78
|
+
# tcp6 local_address is a 32-hex-char IPv6 + :PORT; port parse must still work.
|
|
79
|
+
table = (
|
|
80
|
+
_HEADER + "\n"
|
|
81
|
+
f" 0: 00000000000000000000000001000000:{_HEX} "
|
|
82
|
+
"00000000000000000000000000000000:0000 01 "
|
|
83
|
+
"00000000:00000000 00:00000000 00000000 0 0 12345 1"
|
|
84
|
+
)
|
|
85
|
+
assert _tcp_table_has_established_on_port(table, _PORT) is True
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_mixed_rows_finds_the_established_one():
|
|
89
|
+
table = "\n".join([
|
|
90
|
+
_HEADER,
|
|
91
|
+
_row(_HEX, "0A"), # our port, LISTEN (not a consumer)
|
|
92
|
+
_row("0050", "01"), # other port, ESTABLISHED
|
|
93
|
+
_row(_HEX, "01"), # our port, ESTABLISHED <-- the viewer
|
|
94
|
+
])
|
|
95
|
+
assert _tcp_table_has_established_on_port(table, _PORT) is True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---- _idle_release_due (the release decision) ------------------------------
|
|
99
|
+
|
|
100
|
+
_IDLE = 120.0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_consumer_present_never_releases():
|
|
104
|
+
# present=True → a viewer is pulling, even long past the idle window
|
|
105
|
+
assert _idle_release_due(True, last_consumer=0.0, now=10_000.0,
|
|
106
|
+
idle_secs=_IDLE) is False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_unknown_table_never_releases():
|
|
110
|
+
# present=None → /proc unreadable (non-Linux) → fail-safe, never release
|
|
111
|
+
assert _idle_release_due(None, last_consumer=0.0, now=10_000.0,
|
|
112
|
+
idle_secs=_IDLE) is False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_no_consumer_within_window_holds():
|
|
116
|
+
# no consumer but still inside the idle window → keep trying
|
|
117
|
+
assert _idle_release_due(False, last_consumer=1000.0, now=1090.0,
|
|
118
|
+
idle_secs=_IDLE) is False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_no_consumer_past_window_releases():
|
|
122
|
+
# no consumer for longer than the idle window → release the orphan
|
|
123
|
+
assert _idle_release_due(False, last_consumer=1000.0, now=1121.0,
|
|
124
|
+
idle_secs=_IDLE) is True
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_release_boundary_is_strictly_greater():
|
|
128
|
+
assert _idle_release_due(False, 1000.0, 1120.0, _IDLE) is False # == window
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
import traceback
|
|
133
|
+
_fail = 0
|
|
134
|
+
for _k, _v in sorted(globals().items()):
|
|
135
|
+
if _k.startswith("test_"):
|
|
136
|
+
try:
|
|
137
|
+
_v()
|
|
138
|
+
print(f"PASS {_k}")
|
|
139
|
+
except Exception:
|
|
140
|
+
_fail += 1
|
|
141
|
+
print(f"FAIL {_k}")
|
|
142
|
+
traceback.print_exc()
|
|
143
|
+
raise SystemExit(1 if _fail else 0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.5.8 → python_aidot_cameras-0.5.10}/tests/test_highport_nomination.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|