fix: harden task recovery and DR contract handling
This commit is contained in:
@@ -9,7 +9,8 @@ flags=(-std=c++17 -O2 -Wall -Wextra -Werror -pthread -Icore/include)
|
||||
"$cxx" "${flags[@]}" -c core/src/workflow.cpp -o build/workflow.o
|
||||
"$cxx" "${flags[@]}" -c core/src/sim_driver.cpp -o build/sim_driver.o
|
||||
"$cxx" "${flags[@]}" core/examples/demo.cpp build/core.o build/workflow.o build/sim_driver.o -o build/bt_demo
|
||||
for test_name in core_test workflow_test journal_failure_test preflight_test settlement_test proof_regression_test readiness_regression_test scenario_test lifecycle_test; do
|
||||
"$cxx" "${flags[@]}" "core/tests/$test_name.cpp" build/core.o build/workflow.o -o "build/$test_name"
|
||||
for test_source in core/tests/*_test.cpp; do
|
||||
test_name="$(basename "$test_source" .cpp)"
|
||||
"$cxx" "${flags[@]}" -UNDEBUG "$test_source" build/core.o build/workflow.o -o "build/$test_name"
|
||||
"./build/$test_name"
|
||||
done
|
||||
|
||||
@@ -16,8 +16,14 @@ import trace
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CORE_TESTS = ('core_test', 'workflow_test', 'journal_failure_test', 'preflight_test',
|
||||
'settlement_test', 'proof_regression_test', 'readiness_regression_test', 'scenario_test', 'lifecycle_test')
|
||||
CORE_TESTS = tuple(path.stem for path in sorted((ROOT / 'core/tests').glob('*_test.cpp')))
|
||||
|
||||
|
||||
def executable_lines(path):
|
||||
# Python 3.14 can include artificial bytecode positions without a source
|
||||
# line; these are not executable source statements and cannot be sorted.
|
||||
return {line for line in trace._find_executable_linenos(str(path))
|
||||
if type(line) is int and line > 0}
|
||||
|
||||
|
||||
def production(path):
|
||||
@@ -35,13 +41,20 @@ def python_lines(destination):
|
||||
# are deliberately not counted as covered without an observed trace event.
|
||||
result = tracer.runfunc(unittest.TextTestRunner(verbosity=1).run, suite)
|
||||
counts = tracer.results().counts
|
||||
observed = {}
|
||||
resolved = {}
|
||||
for (filename, line), count in counts.items():
|
||||
if count <= 0:
|
||||
continue
|
||||
if filename not in resolved:
|
||||
resolved[filename] = Path(filename).resolve()
|
||||
observed.setdefault(resolved[filename], set()).add(line)
|
||||
files = []
|
||||
for path in sorted(ROOT.rglob('*.py')):
|
||||
if not production(path):
|
||||
continue
|
||||
statements = set(trace._find_executable_linenos(str(path)))
|
||||
hit = {line for (filename, line), count in counts.items()
|
||||
if Path(filename).resolve() == path and count > 0} & statements
|
||||
statements = executable_lines(path)
|
||||
hit = observed.get(path, set()) & statements
|
||||
files.append(dict(path=str(path.relative_to(ROOT)), statements=len(statements),
|
||||
covered=len(hit), missing=sorted(statements-hit)))
|
||||
data = dict(metric='Python executable source lines; main test thread only',
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure fresh-connection task admission bursts against an isolated local API.
|
||||
|
||||
The planner is deliberately idle: receipts must not wait for planning. Latency
|
||||
is reported as evidence, never asserted as a machine-independent unit-test SLA.
|
||||
"""
|
||||
import argparse
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import http.client
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0,str(ROOT/'coordinator'))
|
||||
from robot_bt_coordinator.backends import ManualBackend
|
||||
from robot_bt_coordinator.http_api import make_server
|
||||
from robot_bt_coordinator.service import Coordinator
|
||||
|
||||
|
||||
def probe(clients=10,rounds=3):
|
||||
if not 1<=clients<=128 or not 1<=rounds<=100:
|
||||
raise ValueError('clients must be 1..128 and rounds 1..100')
|
||||
with tempfile.TemporaryDirectory(prefix='bt-http-probe-') as folder:
|
||||
coordinator=Coordinator(str(Path(folder)/'tasks.db'),ManualBackend(),{'probe_robot'})
|
||||
server=make_server(coordinator,'127.0.0.1',0,'local-probe-token')
|
||||
thread=threading.Thread(target=server.serve_forever,daemon=True)
|
||||
thread.start()
|
||||
samples=[]
|
||||
try:
|
||||
for iteration in range(rounds):
|
||||
gate=threading.Barrier(clients)
|
||||
def submit(index):
|
||||
connection=http.client.HTTPConnection('127.0.0.1',server.server_port,timeout=10)
|
||||
request=dict(client_request_id=f'probe-{iteration}-{index}',robot_id='probe_robot',instruction='local admission probe',known_info={})
|
||||
gate.wait(timeout=15)
|
||||
started=time.perf_counter()
|
||||
try:
|
||||
connection.request('POST','/v1/tasks',json.dumps(request),{'Authorization':'Bearer local-probe-token','Content-Type':'application/json'})
|
||||
response=connection.getresponse()
|
||||
body=json.loads(response.read())
|
||||
return dict(elapsed_ms=(time.perf_counter()-started)*1000,http_status=response.status,task_id=body.get('task_id'),status=body.get('status'))
|
||||
finally:connection.close()
|
||||
with ThreadPoolExecutor(max_workers=clients) as pool:
|
||||
samples.extend(pool.map(submit,range(clients)))
|
||||
successful=all(s['http_status']==202 and s['status']=='QUEUED' for s in samples)
|
||||
successful=successful and len({s['task_id'] for s in samples})==clients*rounds
|
||||
durations=sorted(s['elapsed_ms'] for s in samples)
|
||||
return dict(clients=clients,rounds=rounds,requests=len(samples),all_accepted=successful,
|
||||
p50_ms=durations[math.ceil(len(durations)*.5)-1],
|
||||
p95_ms=durations[math.ceil(len(durations)*.95)-1],max_ms=max(durations),
|
||||
listen_backlog=server.request_queue_size,planner_started=bool(coordinator.backend.plans),samples=samples)
|
||||
finally:
|
||||
server.shutdown();server.server_close();thread.join();coordinator.close()
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser()
|
||||
parser.add_argument('--clients',type=int,default=10)
|
||||
parser.add_argument('--rounds',type=int,default=3)
|
||||
args=parser.parse_args()
|
||||
result=probe(args.clients,args.rounds)
|
||||
print(json.dumps(result,indent=2))
|
||||
if not result['all_accepted'] or result['planner_started']:raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -3,6 +3,7 @@ set -euo pipefail
|
||||
project_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$project_root"
|
||||
bash tools/build_portable.sh
|
||||
bash tools/test_recovery_policy.sh
|
||||
python3 -m unittest discover -s tests -p 'test_*.py' -v
|
||||
python3 ros2/bt_executor/tools/check_static.py
|
||||
python3 ros2/bt_executor/tools/test_ros_backend.py
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
project_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$project_root"
|
||||
mkdir -p build
|
||||
"${CXX:-g++}" -std=c++17 -O2 -Wall -Wextra -Werror -UNDEBUG \
|
||||
-Iros2/bt_executor/include ros2/bt_executor/tools/test_recovery_policy.cpp \
|
||||
-o build/recovery_policy_test
|
||||
./build/recovery_policy_test
|
||||
Reference in New Issue
Block a user