- invalidate_by_adr 新增 pending 分组:ADR 直接关联的 TODO 任务也标记 INVALIDATED - apply_full_replace 保留 DISPATCHED 状态,不再保留 INVALIDATED(全量替换时重置) - 新增 2 项测试:apply_full_replace 状态保留 + 真实 git revert - 测试从 11→13 项,全部通过 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
492 lines
19 KiB
Python
492 lines
19 KiB
Python
#!/usr/bin/env python3
|
||
"""P1-21 ADR 变更级联失效 功能测试"""
|
||
|
||
import sys
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, str(Path(__file__).parent / "lib"))
|
||
|
||
|
||
def test_task_node_adr_refs():
|
||
"""测试 TaskNode.adr_refs 字段"""
|
||
from air_runtime.task_graph import TaskNode
|
||
|
||
node = TaskNode(id="T-001", adr_refs=["ADR-0005", "ADR-0012"])
|
||
assert node.adr_refs == ["ADR-0005", "ADR-0012"]
|
||
|
||
# 默认为空列表
|
||
node2 = TaskNode(id="T-002")
|
||
assert node2.adr_refs == []
|
||
|
||
print("✓ TaskNode.adr_refs 测试通过")
|
||
return True
|
||
|
||
|
||
def test_invalidated_status():
|
||
"""测试 INVALIDATED 状态"""
|
||
from air_runtime.task_graph import TaskNode, TaskGraph
|
||
|
||
node = TaskNode(id="T-001", status="DONE", task="用 ffmpeg 实现解码器",
|
||
adr_refs=["ADR-0005"])
|
||
|
||
# 直接设置 INVALIDATED
|
||
node.status = "INVALIDATED"
|
||
assert node.status == "INVALIDATED"
|
||
|
||
# _update_node 允许 INVALIDATED 覆盖 DONE
|
||
graph = TaskGraph()
|
||
graph.add_node(TaskNode(id="T-001", status="DONE", task="旧描述", adr_refs=["ADR-0005"]))
|
||
graph._update_node(TaskNode(id="T-001", status="INVALIDATED", task="新描述", adr_refs=["ADR-0005"]))
|
||
assert graph.nodes["T-001"].status == "INVALIDATED"
|
||
|
||
# _update_node 仍然保护 DONE 不被普通修改覆盖
|
||
graph2 = TaskGraph()
|
||
graph2.add_node(TaskNode(id="T-002", status="DONE", task="旧描述"))
|
||
graph2._update_node(TaskNode(id="T-002", status="TODO", task="新描述"))
|
||
assert graph2.nodes["T-002"].status == "DONE" # 被保护,不允许降级为 TODO
|
||
|
||
print("✓ INVALIDATED 状态测试通过")
|
||
return True
|
||
|
||
|
||
def test_tasks_by_adr():
|
||
"""测试 ADR→任务溯源链查询"""
|
||
from air_runtime.task_graph import TaskGraph, TaskNode
|
||
|
||
graph = TaskGraph()
|
||
graph.add_node(TaskNode(id="T-001", task="ffmpeg 解码器", adr_refs=["ADR-0005"]))
|
||
graph.add_node(TaskNode(id="T-002", task="ffmpeg 编码器", adr_refs=["ADR-0005", "ADR-0010"]))
|
||
graph.add_node(TaskNode(id="T-003", task="UI 界面", adr_refs=["ADR-0008"]))
|
||
graph.add_node(TaskNode(id="T-004", task="日志模块", adr_refs=[]))
|
||
|
||
# ADR-0005 关联 T-001 和 T-002
|
||
result = graph.tasks_by_adr("ADR-0005")
|
||
assert len(result) == 2
|
||
assert {n.id for n in result} == {"T-001", "T-002"}
|
||
|
||
# ADR-0010 只关联 T-002
|
||
result2 = graph.tasks_by_adr("ADR-0010")
|
||
assert len(result2) == 1
|
||
assert result2[0].id == "T-002"
|
||
|
||
# 不存在的 ADR
|
||
result3 = graph.tasks_by_adr("ADR-9999")
|
||
assert len(result3) == 0
|
||
|
||
print("✓ ADR→任务溯源链查询测试通过")
|
||
return True
|
||
|
||
|
||
def test_invalidate_by_adr():
|
||
"""测试 ADR 变更级联失效"""
|
||
from air_runtime.task_graph import TaskGraph, TaskNode, Edge, PlanDelta
|
||
|
||
graph = TaskGraph()
|
||
# 已完成的 ffmpeg 任务
|
||
graph.add_node(TaskNode(id="T-001", status="DONE", task="ffmpeg 解码器", adr_refs=["ADR-0005"]))
|
||
graph.add_node(TaskNode(id="T-002", status="DONE", task="ffmpeg 编码器", adr_refs=["ADR-0005"]))
|
||
# 进行中的 ffmpeg 任务
|
||
graph.add_node(TaskNode(id="T-003", status="DISPATCHED", task="ffmpeg 流媒体", adr_refs=["ADR-0005"]))
|
||
# ADR 直接关联的 TODO 任务
|
||
graph.add_node(TaskNode(id="T-003b", status="TODO", task="ffmpeg 滤镜", adr_refs=["ADR-0005"]))
|
||
# 下游任务(依赖上面的 ffmpeg 模块)
|
||
graph.add_node(TaskNode(id="T-004", status="TODO", task="集成测试"))
|
||
graph.add_node(TaskNode(id="T-005", status="TODO", task="部署"))
|
||
# 无关任务
|
||
graph.add_node(TaskNode(id="T-006", status="TODO", task="UI 优化", adr_refs=["ADR-0008"]))
|
||
|
||
graph.add_edge(Edge(source="T-001", target="T-004"))
|
||
graph.add_edge(Edge(source="T-002", target="T-004"))
|
||
graph.add_edge(Edge(source="T-004", target="T-005"))
|
||
|
||
delta = PlanDelta()
|
||
report = graph.invalidate_by_adr("ADR-0005", delta)
|
||
|
||
# 已完成: T-001, T-002
|
||
assert report.invalidated_completed == 2
|
||
# 进行中: T-003
|
||
assert report.terminated_in_progress == 1
|
||
# 下游: T-004, T-005 (T-003b 是直接关联的 TODO,不属于 cascaded_downstream)
|
||
# 注意:T-003b 直接关联 ADR-0005,作为 pending 标记为 INVALIDATED
|
||
# 但它不属于 cascaded_downstream(级联的下游任务)
|
||
# cascaded_downstream 仅计算 _find_downstream 返回的非 ADR 直接关联任务
|
||
assert report.cascaded_downstream == 2
|
||
# 总失效 = completed(2) + in_progress(1) + pending(1) + cascaded(2) = 6
|
||
total_invalidated = len(report.invalidated_task_ids)
|
||
assert total_invalidated == 6
|
||
|
||
# 验证状态已变为 INVALIDATED
|
||
assert graph.nodes["T-001"].status == "INVALIDATED"
|
||
assert graph.nodes["T-002"].status == "INVALIDATED"
|
||
assert graph.nodes["T-003"].status == "INVALIDATED"
|
||
assert graph.nodes["T-003b"].status == "INVALIDATED"
|
||
assert graph.nodes["T-004"].status == "INVALIDATED"
|
||
assert graph.nodes["T-005"].status == "INVALIDATED"
|
||
|
||
# 无关任务不受影响
|
||
assert graph.nodes["T-006"].status == "TODO"
|
||
|
||
# 调度已冻结
|
||
assert graph.dispatch_frozen == True
|
||
|
||
# ready_tasks 为空(冻结状态)
|
||
assert graph.ready_tasks() == []
|
||
|
||
print(f"✓ ADR 变更级联失效测试通过 (完成={report.invalidated_completed}, "
|
||
f"进行中={report.terminated_in_progress}, 下游={report.cascaded_downstream})")
|
||
return True
|
||
|
||
|
||
def test_dispatch_freeze_unfreeze():
|
||
"""测试调度冻结和解冻"""
|
||
from air_runtime.task_graph import TaskGraph, TaskNode
|
||
|
||
graph = TaskGraph()
|
||
graph.add_node(TaskNode(id="T-001", status="TODO", task="任务1"))
|
||
|
||
# 正常状态
|
||
assert graph.ready_tasks() == ["T-001"]
|
||
|
||
# 冻结
|
||
graph.dispatch_frozen = True
|
||
assert graph.ready_tasks() == []
|
||
|
||
# 解冻
|
||
graph.unfreeze_dispatch()
|
||
assert graph.dispatch_frozen == False
|
||
assert graph.ready_tasks() == ["T-001"]
|
||
|
||
print("✓ 调度冻结/解冻测试通过")
|
||
return True
|
||
|
||
|
||
def test_cascade_report():
|
||
"""测试 CascadeReport 数据结构"""
|
||
from air_runtime.task_graph import CascadeReport
|
||
|
||
report = CascadeReport(
|
||
invalidated_completed=2,
|
||
terminated_in_progress=1,
|
||
cascaded_downstream=3,
|
||
rollback_ref="airplan/adr-invalidate-2026-06-11",
|
||
invalidated_task_ids=["T-001", "T-002", "T-003", "T-004", "T-005", "T-006"],
|
||
)
|
||
|
||
assert report.invalidated_completed == 2
|
||
assert report.terminated_in_progress == 1
|
||
assert report.cascaded_downstream == 3
|
||
assert report.rollback_ref.startswith("airplan/")
|
||
assert len(report.invalidated_task_ids) == 6
|
||
|
||
print("✓ CascadeReport 测试通过")
|
||
return True
|
||
|
||
|
||
def test_plan_delta_rollback_ref():
|
||
"""测试 PlanDelta.rollback_ref"""
|
||
from air_runtime.task_graph import PlanDelta
|
||
|
||
delta = PlanDelta(rollback_ref="airplan/adr-invalidate-2026-06-11")
|
||
assert delta.rollback_ref == "airplan/adr-invalidate-2026-06-11"
|
||
|
||
# 默认为空字符串
|
||
delta2 = PlanDelta()
|
||
assert delta2.rollback_ref == ""
|
||
|
||
print("✓ PlanDelta.rollback_ref 测试通过")
|
||
return True
|
||
|
||
|
||
def test_rvr_invalidated_cleanup_check():
|
||
"""测试 AirRvr INVALIDATED 清理检查"""
|
||
from air_runtime.review_runtime import ReviewRuntime
|
||
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
project_root = Path(tmpdir)
|
||
rvr = ReviewRuntime(project_root)
|
||
|
||
# 无残留
|
||
result = rvr.check_invalidated_cleanup(["T-001", "T-002"])
|
||
assert result["cleaned"] == True
|
||
assert result["residualCount"] == 0
|
||
|
||
# 创建残留的 result 文件
|
||
result_dir = project_root / "AirPlan" / "state" / "airdo" / "tasks" / "T-001"
|
||
result_dir.mkdir(parents=True, exist_ok=True)
|
||
(result_dir / "result.json").write_text('{"taskId": "T-001", "status": "done"}')
|
||
|
||
result2 = rvr.check_invalidated_cleanup(["T-001"])
|
||
assert result2["cleaned"] == False
|
||
assert result2["residualCount"] == 1
|
||
assert result2["residualDetails"][0]["taskId"] == "T-001"
|
||
|
||
print("✓ AirRvr INVALIDATED 清理检查测试通过")
|
||
return True
|
||
|
||
|
||
def test_eng_handle_adr_invalidation():
|
||
"""测试 Eng ADR 失效处理入口"""
|
||
from air_runtime.modes.eng_mode import handle_adr_invalidation
|
||
import json
|
||
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
project_root = Path(tmpdir)
|
||
|
||
# 初始化引擎
|
||
from air_runtime.modes.eng_mode import enter_engine
|
||
enter_engine(project_root)
|
||
|
||
# 创建 task-graph.json(含 ADR 引用)
|
||
arc_dir = project_root / "AirPlan" / "state" / "airarc" / "reviews"
|
||
arc_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
from air_runtime.task_graph import TaskGraph, TaskNode, Edge
|
||
from air_runtime.modes.arc_mode import _export_task_graph_json
|
||
|
||
graph = TaskGraph()
|
||
graph.add_node(TaskNode(id="T-001", status="DONE", task="ffmpeg 解码器", adr_refs=["ADR-0005"]))
|
||
graph.add_node(TaskNode(id="T-002", status="TODO", task="集成测试"))
|
||
graph.add_edge(Edge(source="T-001", target="T-002"))
|
||
_export_task_graph_json(graph, arc_dir / "task-graph.json")
|
||
|
||
# 执行 ADR 失效
|
||
result = handle_adr_invalidation(project_root, "ADR-0005")
|
||
|
||
assert "cascadeReport" in result
|
||
assert result["cascadeReport"]["invalidatedCompleted"] == 1
|
||
assert result["cascadeReport"]["cascadedDownstream"] == 1
|
||
assert result["nextStep"] == "arc-replan-then-unfreeze"
|
||
|
||
# 验证 task-graph.json 已更新
|
||
updated = TaskGraph.load(arc_dir / "task-graph.json")
|
||
assert updated.nodes["T-001"].status == "INVALIDATED"
|
||
assert updated.dispatch_frozen == True
|
||
|
||
print("✓ Eng ADR 失效处理测试通过")
|
||
return True
|
||
|
||
|
||
def test_eng_unfreeze_after_replan():
|
||
"""测试 Eng 解冻调度"""
|
||
from air_runtime.modes.eng_mode import handle_adr_invalidation, unfreeze_after_replan
|
||
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
project_root = Path(tmpdir)
|
||
|
||
from air_runtime.modes.eng_mode import enter_engine
|
||
enter_engine(project_root)
|
||
|
||
arc_dir = project_root / "AirPlan" / "state" / "airarc" / "reviews"
|
||
arc_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
from air_runtime.task_graph import TaskGraph, TaskNode
|
||
from air_runtime.modes.arc_mode import _export_task_graph_json
|
||
|
||
graph = TaskGraph()
|
||
graph.add_node(TaskNode(id="T-001", status="DONE", task="ffmpeg 解码器", adr_refs=["ADR-0005"]))
|
||
graph.dispatch_frozen = True
|
||
_export_task_graph_json(graph, arc_dir / "task-graph.json")
|
||
|
||
# 解冻
|
||
result = unfreeze_after_replan(project_root)
|
||
|
||
assert result["frozen"] == False
|
||
|
||
# 验证 task-graph.json 已解冻
|
||
updated = TaskGraph.load(arc_dir / "task-graph.json")
|
||
assert updated.dispatch_frozen == False
|
||
|
||
print("✓ Eng 解冻调度测试通过")
|
||
return True
|
||
|
||
|
||
def test_ffmpeg_to_gstreamer_scenario():
|
||
"""完整场景测试:ffmpeg → gstreamer 架构变更"""
|
||
from air_runtime.task_graph import TaskGraph, TaskNode, Edge, PlanDelta
|
||
|
||
# 初始状态:ffmpeg 架构
|
||
graph = TaskGraph()
|
||
graph.add_node(TaskNode(id="T-001", status="DONE", task="用 ffmpeg 实现视频解码器",
|
||
files_dirs="src/decoder.cpp", adr_refs=["ADR-0005"]))
|
||
graph.add_node(TaskNode(id="T-002", status="DONE", task="用 ffmpeg 实现音频解码器",
|
||
files_dirs="src/audio_decoder.cpp", adr_refs=["ADR-0005"]))
|
||
graph.add_node(TaskNode(id="T-003", status="DONE", task="ffmpeg 流媒体传输",
|
||
files_dirs="src/streaming.cpp", adr_refs=["ADR-0005", "ADR-0006"]))
|
||
graph.add_node(TaskNode(id="T-004", status="TODO", task="解码器集成测试",
|
||
files_dirs="tests/decoder_test.cpp"))
|
||
graph.add_node(TaskNode(id="T-005", status="TODO", task="流媒体集成测试",
|
||
files_dirs="tests/streaming_test.cpp"))
|
||
graph.add_node(TaskNode(id="T-006", status="TODO", task="部署播放器",
|
||
files_dirs="deploy/"))
|
||
# 无关任务
|
||
graph.add_node(TaskNode(id="T-007", status="DONE", task="用户界面",
|
||
files_dirs="src/ui/", adr_refs=["ADR-0008"]))
|
||
graph.add_node(TaskNode(id="T-008", status="TODO", task="UI 测试",
|
||
files_dirs="tests/ui_test.cpp"))
|
||
|
||
graph.add_edge(Edge(source="T-001", target="T-004"))
|
||
graph.add_edge(Edge(source="T-002", target="T-004"))
|
||
graph.add_edge(Edge(source="T-003", target="T-005"))
|
||
graph.add_edge(Edge(source="T-004", target="T-006"))
|
||
graph.add_edge(Edge(source="T-005", target="T-006"))
|
||
graph.add_edge(Edge(source="T-007", target="T-008"))
|
||
|
||
# ADR-0005 变更:ffmpeg → gstreamer
|
||
delta = PlanDelta(rollback_ref="airplan/adr-invalidate-snapshot-001")
|
||
report = graph.invalidate_by_adr("ADR-0005", delta)
|
||
|
||
# T-001, T-002, T-003 完成 → INVALIDATED
|
||
assert report.invalidated_completed == 3
|
||
# T-004, T-005, T-006 下游 → INVALIDATED
|
||
assert report.cascaded_downstream == 3
|
||
total_invalidated = len(report.invalidated_task_ids)
|
||
assert total_invalidated == 6 # T-001~T-006 全部失效
|
||
|
||
# 无关任务不受影响
|
||
assert graph.nodes["T-007"].status == "DONE"
|
||
assert graph.nodes["T-008"].status == "TODO"
|
||
|
||
# 调度冻结
|
||
assert graph.dispatch_frozen == True
|
||
assert graph.ready_tasks() == []
|
||
|
||
# 模拟 Arc 重新生成 gstreamer 任务后解冻
|
||
graph.add_node(TaskNode(id="T-101", status="TODO", task="用 gstreamer 实现视频解码器",
|
||
files_dirs="src/decoder.cpp", adr_refs=["ADR-0005"]))
|
||
graph.add_node(TaskNode(id="T-102", status="TODO", task="用 gstreamer 实现音频解码器",
|
||
files_dirs="src/audio_decoder.cpp", adr_refs=["ADR-0005"]))
|
||
graph.unfreeze_dispatch()
|
||
|
||
assert graph.dispatch_frozen == False
|
||
# 新任务可以派发
|
||
ready = [tid for tid, n in graph.nodes.items() if n.status == "TODO" and n.in_degree == 0]
|
||
assert "T-101" in ready
|
||
assert "T-102" in ready
|
||
# T-008 有入度(依赖 T-007),不在 ready 中
|
||
assert "T-008" not in ready
|
||
|
||
print(f"✓ ffmpeg→gstreamer 完整场景测试通过 (失效={total_invalidated}, 新任务可派发)")
|
||
return True
|
||
|
||
|
||
def test_apply_full_replace_preserves_dispatched():
|
||
"""测试 apply_full_replace 保留 DISPATCHED 状态(不含 INVALIDATED)"""
|
||
from air_runtime.task_graph import TaskGraph, TaskNode, Edge
|
||
|
||
old_graph = TaskGraph()
|
||
old_graph.add_node(TaskNode(id="T-001", status="DONE", task="已完成"))
|
||
old_graph.add_node(TaskNode(id="T-002", status="DISPATCHED", task="进行中"))
|
||
old_graph.add_node(TaskNode(id="T-003", status="INVALIDATED", task="已失效"))
|
||
|
||
new_nodes = [
|
||
TaskNode(id="T-001", status="TODO", task="已完成"),
|
||
TaskNode(id="T-002", status="TODO", task="进行中"),
|
||
TaskNode(id="T-003", status="TODO", task="已失效"),
|
||
]
|
||
|
||
old_graph.apply_full_replace(new_nodes, [])
|
||
|
||
# DONE 和 DISPATCHED 被保留
|
||
assert old_graph.nodes["T-001"].status == "DONE"
|
||
assert old_graph.nodes["T-002"].status == "DISPATCHED"
|
||
# INVALIDATED 不被保留(全量替换时重置为 TODO)
|
||
assert old_graph.nodes["T-003"].status == "TODO"
|
||
|
||
print("✓ apply_full_replace 保留 DONE/DISPATCHED 测试通过")
|
||
return True
|
||
|
||
|
||
def test_git_revert_with_real_repo():
|
||
"""测试真实 git 仓库环境下的 revert"""
|
||
import subprocess
|
||
from air_runtime.modes.eng_mode import handle_adr_invalidation
|
||
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
project_root = Path(tmpdir)
|
||
|
||
# 初始化 git 仓库
|
||
subprocess.run(["git", "init"], cwd=project_root, capture_output=True)
|
||
subprocess.run(["git", "config", "user.email", "test@test.com"], cwd=project_root, capture_output=True)
|
||
subprocess.run(["git", "config", "user.name", "Test"], cwd=project_root, capture_output=True)
|
||
|
||
# 创建文件并提交(模拟 T-001 的代码)
|
||
(project_root / "src").mkdir()
|
||
(project_root / "src" / "decoder.cpp").write_text("// ffmpeg decoder")
|
||
subprocess.run(["git", "add", "."], cwd=project_root, capture_output=True)
|
||
subprocess.run(["git", "commit", "-m", "feat: T-001 ffmpeg decoder"], cwd=project_root, capture_output=True)
|
||
|
||
# 初始化引擎和 task-graph
|
||
from air_runtime.modes.eng_mode import enter_engine
|
||
enter_engine(project_root)
|
||
|
||
arc_dir = project_root / "AirPlan" / "state" / "airarc" / "reviews"
|
||
arc_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
from air_runtime.task_graph import TaskGraph, TaskNode
|
||
from air_runtime.modes.arc_mode import _export_task_graph_json
|
||
|
||
graph = TaskGraph()
|
||
graph.add_node(TaskNode(id="T-001", status="DONE", task="ffmpeg 解码器", adr_refs=["ADR-0005"]))
|
||
_export_task_graph_json(graph, arc_dir / "task-graph.json")
|
||
|
||
# 执行 ADR 失效
|
||
result = handle_adr_invalidation(project_root, "ADR-0005")
|
||
|
||
# 验证 revert 结果
|
||
assert "revertResults" in result
|
||
# 应该找到一个包含 "T-001" 的 commit 并尝试 revert
|
||
revert = result["revertResults"][0]
|
||
assert revert["taskId"] == "T-001"
|
||
# commit 应该被找到
|
||
assert revert["commit"] is not None
|
||
|
||
print("✓ 真实 git 环境下 revert 测试通过")
|
||
return True
|
||
|
||
|
||
def main():
|
||
print("=" * 50)
|
||
print("P1-21 ADR 变更级联失效 功能测试")
|
||
print("=" * 50)
|
||
|
||
tests = [
|
||
("TaskNode.adr_refs", test_task_node_adr_refs),
|
||
("INVALIDATED 状态", test_invalidated_status),
|
||
("ADR→任务溯源链", test_tasks_by_adr),
|
||
("级联失效", test_invalidate_by_adr),
|
||
("调度冻结/解冻", test_dispatch_freeze_unfreeze),
|
||
("CascadeReport", test_cascade_report),
|
||
("PlanDelta.rollback_ref", test_plan_delta_rollback_ref),
|
||
("AirRvr 清理检查", test_rvr_invalidated_cleanup_check),
|
||
("Eng ADR 失效处理", test_eng_handle_adr_invalidation),
|
||
("Eng 解冻调度", test_eng_unfreeze_after_replan),
|
||
("apply_full_replace 保留状态", test_apply_full_replace_preserves_dispatched),
|
||
("真实 git revert", test_git_revert_with_real_repo),
|
||
("ffmpeg→gstreamer 场景", test_ffmpeg_to_gstreamer_scenario),
|
||
]
|
||
|
||
passed = 0
|
||
failed = 0
|
||
|
||
for name, test_fn in tests:
|
||
try:
|
||
test_fn()
|
||
passed += 1
|
||
except Exception as e:
|
||
print(f"✗ {name} 失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
failed += 1
|
||
|
||
print("=" * 50)
|
||
print(f"测试结果: {passed} 通过, {failed} 失败")
|
||
print("=" * 50)
|
||
|
||
return failed == 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
success = main()
|
||
sys.exit(0 if success else 1)
|