实现行为树执行器、任务协调和技能接口
This commit is contained in:
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
project_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$project_root"
|
||||
mkdir -p build
|
||||
cxx="${CXX:-g++}"
|
||||
flags=(-std=c++17 -O2 -Wall -Wextra -Werror -pthread -Icore/include)
|
||||
"$cxx" "${flags[@]}" -c core/src/core.cpp -o build/core.o
|
||||
"$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"
|
||||
"./build/$test_name"
|
||||
done
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reproducible standard-library Python line and GCC core branch coverage.
|
||||
|
||||
No ROS emulation is labeled a middleware integration test. GCC branch counts
|
||||
include compiler-generated exception/short-circuit branches; the scenario
|
||||
campaign is a separate functional metric. Run from any working directory.
|
||||
"""
|
||||
import argparse
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
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')
|
||||
|
||||
|
||||
def production(path):
|
||||
relative = Path(path).resolve().relative_to(ROOT)
|
||||
return relative.parts[0] in ('coordinator', 'robobrain', 'navigation_gateway', 'ros2') and not any(
|
||||
p in ('tests', 'test', 'tools', '__pycache__') for p in relative.parts) and relative.name not in ('setup.py',)
|
||||
|
||||
|
||||
def python_lines(destination):
|
||||
os.chdir(ROOT)
|
||||
sys.path[:0] = [str(ROOT), str(ROOT / 'tests')]
|
||||
suite = unittest.defaultTestLoader.discover(str(ROOT / 'tests'), pattern='test_*.py')
|
||||
tracer = trace.Trace(count=True, trace=False, ignoredirs=[sys.prefix, sys.exec_prefix])
|
||||
# Trace.runfunc covers the executing test thread. Background thread lines
|
||||
# are deliberately not counted as covered without an observed trace event.
|
||||
result = tracer.runfunc(unittest.TextTestRunner(verbosity=1).run, suite)
|
||||
counts = tracer.results().counts
|
||||
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
|
||||
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',
|
||||
tests_run=result.testsRun, failures=len(result.failures), errors=len(result.errors),
|
||||
statements=sum(f['statements'] for f in files), covered=sum(f['covered'] for f in files), files=files)
|
||||
(destination / 'python_lines.json').write_text(json.dumps(data, indent=2)+'\n')
|
||||
if not result.wasSuccessful():
|
||||
raise RuntimeError('Python suite failed under instrumentation')
|
||||
return data
|
||||
|
||||
|
||||
def cpp_branches(destination):
|
||||
build = destination / 'gcc'; build.mkdir(exist_ok=True)
|
||||
# Each execution is a fresh measurement, not an accumulation of old runs.
|
||||
for p in build.glob('*.gcda'): p.unlink()
|
||||
compiler = os.environ.get('CXX', 'g++')
|
||||
flags = ['-std=c++17', '-O0', '-g', '--coverage', '-Wall', '-Wextra', '-Werror',
|
||||
'-pthread', '-UNDEBUG', '-I'+str(ROOT / 'core/include')]
|
||||
objects = []
|
||||
for source in ('core', 'workflow'):
|
||||
obj = build / (source+'.o')
|
||||
subprocess.run([compiler, *flags, '-c', str(ROOT / 'core/src' / (source+'.cpp')), '-o', str(obj)], check=True)
|
||||
objects.append(str(obj))
|
||||
runs = []
|
||||
for name in CORE_TESTS:
|
||||
exe = build / name
|
||||
subprocess.run([compiler, *flags, str(ROOT / 'core/tests' / (name+'.cpp')), *objects, '-o', str(exe)], check=True)
|
||||
completed = subprocess.run([str(exe)], check=True, capture_output=True, text=True)
|
||||
runs.append(dict(test=name, exit_code=completed.returncode, stdout=completed.stdout))
|
||||
for obj in objects:
|
||||
subprocess.run(['gcov', '-j', '-b', '-c', obj], cwd=build, check=True, stdout=subprocess.DEVNULL)
|
||||
files = []
|
||||
for archive in build.glob('*.gcov.json.gz'):
|
||||
with gzip.open(archive, 'rt') as stream: data=json.load(stream)
|
||||
for source in data['files']:
|
||||
path=Path(source['file']).resolve()
|
||||
if path.parent != ROOT / 'core/src': continue
|
||||
lines=source['lines'];branches=[b for line in lines for b in line.get('branches', [])]
|
||||
files.append(dict(path=str(path.relative_to(ROOT)), statements=len(lines),
|
||||
covered=sum(line['count']>0 for line in lines),
|
||||
branches=len(branches), branches_taken=sum(b['count']>0 for b in branches),
|
||||
missing_lines=[line['line_number'] for line in lines if not line['count']],
|
||||
lines_with_untaken_branches=[line['line_number'] for line in lines if any(not b['count'] for b in line.get('branches', []))]))
|
||||
data=dict(metric='GCC source branches including exception and short-circuit arcs', files=files, runs=runs,
|
||||
statements=sum(f['statements'] for f in files), covered=sum(f['covered'] for f in files),
|
||||
branches=sum(f['branches'] for f in files), branches_taken=sum(f['branches_taken'] for f in files))
|
||||
(destination / 'cpp_branches.json').write_text(json.dumps(data, indent=2)+'\n')
|
||||
return data
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser();parser.add_argument('--output', default=str(ROOT / 'build/coverage'))
|
||||
args=parser.parse_args();out=Path(args.output).resolve();out.mkdir(parents=True, exist_ok=True)
|
||||
py=python_lines(out);cpp=cpp_branches(out)
|
||||
summary=dict(python={k:v for k,v in py.items() if k!='files'},
|
||||
cpp={k:v for k,v in cpp.items() if k not in ('files','runs')},
|
||||
ros_wire_execution=False, hardware_execution=False)
|
||||
(out / 'summary.json').write_text(json.dumps(summary, indent=2)+'\n')
|
||||
print(json.dumps(summary, indent=2))
|
||||
|
||||
|
||||
if __name__=='__main__': main()
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Real HTTP -> coordinator -> portable C++ StageRunner -> durable delivery test."""
|
||||
import http.client
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0,str(ROOT/'coordinator'))
|
||||
from robot_bt_coordinator.backends import DemoBackend
|
||||
from robot_bt_coordinator.http_api import make_server
|
||||
from robot_bt_coordinator.replay import replay_events
|
||||
from robot_bt_coordinator.service import Coordinator,demo_site
|
||||
|
||||
|
||||
def main():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
backend=DemoBackend(ROOT/'build/bt_demo',Path(tmp)/'sim')
|
||||
coordinator=Coordinator(str(Path(tmp)/'tasks.db'),backend,{'robot_01'},demo_site())
|
||||
server=make_server(coordinator,'127.0.0.1',0,'smoke-local-only')
|
||||
thread=threading.Thread(target=server.serve_forever,daemon=True);thread.start()
|
||||
def request(method,path,body=None):
|
||||
c=http.client.HTTPConnection('127.0.0.1',server.server_port,timeout=5)
|
||||
c.request(method,path,json.dumps(body) if body is not None else None,{'Authorization':'Bearer smoke-local-only','Content-Type':'application/json'})
|
||||
r=c.getresponse();data=json.loads(r.read());c.close()
|
||||
assert r.status<300,(r.status,data)
|
||||
return data
|
||||
try:
|
||||
payload=json.loads((ROOT/'config/demo_request.json').read_text())
|
||||
start=time.monotonic();receipt=request('POST','/v1/tasks',payload);ack_ms=(time.monotonic()-start)*1000
|
||||
tid=receipt['task_id']
|
||||
for _ in range(200):
|
||||
coordinator.tick();view=request('GET','/v1/tasks/'+tid)
|
||||
if view['status'] in ('SUCCEEDED','FAILED','INTERVENTION_REQUIRED'):break
|
||||
time.sleep(.01)
|
||||
assert view['status']=='SUCCEEDED',view
|
||||
assert view['completed_quantity']==1,view
|
||||
duplicate=request('POST','/v1/tasks',payload)
|
||||
assert duplicate['task_id']==tid and duplicate['deduplicated'] is True
|
||||
events=request('GET',f'/v1/tasks/{tid}/events?limit=500')['events']
|
||||
replay=replay_events(events)
|
||||
assert replay['status']=='SUCCEEDED' and replay['completed_quantity']==1
|
||||
print(json.dumps({'http_e2e':'passed','task_id':tid,'completed_quantity':1,'deduplicated':True,'offline_replay':'passed','event_count':len(events),'single_receipt_ms':round(ack_ms,2),'robot_motion':'simulated only'},indent=2))
|
||||
finally:
|
||||
server.shutdown();server.server_close();thread.join();coordinator.close()
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
"""HTTP -> RoboBrain domain service fixture -> sequential C++ routes -> replay."""
|
||||
import json,sys,tempfile,time,threading,urllib.request
|
||||
from pathlib import Path
|
||||
ROOT=Path(__file__).resolve().parents[1];sys.path[:0]=[str(ROOT/'coordinator'),str(ROOT/'robobrain')]
|
||||
from robot_robobrain.demo_backend import BrainDemoBackend
|
||||
from robot_bt_coordinator.service import Coordinator
|
||||
from robot_bt_coordinator.http_api import make_server
|
||||
from robot_bt_coordinator.replay import replay_events
|
||||
|
||||
def run(route):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
site=json.loads((ROOT/'config'/('sim_site_'+route.lower()+'.json')).read_text())
|
||||
backend=BrainDemoBackend(ROOT/'build/bt_demo',tmp+'/sim',site)
|
||||
c=Coordinator(tmp+'/tasks.db',backend,['robot_01'],site)
|
||||
server=make_server(c,'127.0.0.1',0,'simulation-token');thread=threading.Thread(target=server.serve_forever,daemon=True);thread.start()
|
||||
base='http://127.0.0.1:'+str(server.server_port)
|
||||
def api(path,body=None):
|
||||
req=urllib.request.Request(base+path,data=None if body is None else json.dumps(body).encode(),headers={'Authorization':'Bearer simulation-token','Content-Type':'application/json'})
|
||||
with urllib.request.urlopen(req,timeout=3) as r:return json.load(r)
|
||||
try:
|
||||
request=json.loads((ROOT/'config/demo_multi_request.json').read_text());t=api('/v1/tasks',request);tid=t['task_id'];deadline=time.monotonic()+20
|
||||
while time.monotonic()<deadline:
|
||||
c.tick();t=api('/v1/tasks/'+tid)
|
||||
if t['status'] in ('SUCCEEDED','FAILED','INTERVENTION_REQUIRED'):break
|
||||
time.sleep(.01)
|
||||
assert t['status']=='SUCCEEDED',t
|
||||
assert t['completed_quantity']==3 and len(backend.executions)==3
|
||||
assert len({x['run_id'] for x in backend.executions})==3
|
||||
assert [x['context']['target_id'] for x in backend.executions]==['water','water','doll']
|
||||
assert api('/v1/tasks',request)['deduplicated']
|
||||
audit=replay_events(c.events(tid,limit=500));assert audit['completed_quantity']==3
|
||||
assert len(list((Path(tmp)/'sim/planning_records').glob('*.json')))==1
|
||||
assert api('/v1/capabilities')['quantity_max']==20
|
||||
return {'route':route,'status':t['status'],'delivered':3,'independent_runs':3,'replay':audit['status'],'model':'fixture','motion':'simulated'}
|
||||
finally:server.shutdown();thread.join();server.server_close();c.close()
|
||||
if __name__=='__main__':print(json.dumps([run(r) for r in ('OBJECT_TABLE','SHELF_CELL')],indent=2))
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
project_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$project_root"
|
||||
: "${ROBOT_BT_API_TOKEN:?Set ROBOT_BT_API_TOKEN to a chosen local API token}"
|
||||
if [[ ! -x build/bt_demo ]]; then bash tools/build_portable.sh; fi
|
||||
export PYTHONPATH="$project_root/coordinator${PYTHONPATH:+:$PYTHONPATH}"
|
||||
exec python3 -m robot_bt_coordinator.cli --backend mock --site config/sim_site.json "$@"
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
project_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$project_root"
|
||||
: "${ROBOT_BT_API_TOKEN:?Set a local API token}"
|
||||
if [[ ! -x build/bt_demo ]]; then bash tools/build_portable.sh; fi
|
||||
export PYTHONPATH="$project_root/coordinator:$project_root/robobrain${PYTHONPATH:+:$PYTHONPATH}"
|
||||
exec python3 -m robot_bt_coordinator.cli --backend brain-mock --site config/sim_site_object_table.json "$@"
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
project_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$project_root"
|
||||
bash tools/build_portable.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
|
||||
python3 tools/http_smoke.py
|
||||
|
||||
python3 tools/robobrain_smoke.py
|
||||
Reference in New Issue
Block a user