543 lines
18 KiB
GDScript
543 lines
18 KiB
GDScript
extends SceneTree
|
|
|
|
const PLAYER_CASH := &"PLAYER_CASH"
|
|
const AUTHORITY_FINE_EXPENSE := &"AUTHORITY_FINE_EXPENSE"
|
|
|
|
var _checks := 0
|
|
var _failures := 0
|
|
var _clock: Variant
|
|
var _state: Variant
|
|
var _runtime: Variant
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
print("S5: integrated Authority runtime cycle")
|
|
_test_incident_visibility_and_daily_evaluation()
|
|
_test_resolution_replay_unreachable_exit_and_long_horizon()
|
|
_cleanup()
|
|
if _failures == 0:
|
|
print("PASS: %d checks" % _checks)
|
|
else:
|
|
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
|
quit(_failures)
|
|
|
|
|
|
func _reset_runtime(seed: int, hour := 6, minute := 0) -> void:
|
|
_cleanup()
|
|
_clock = load("res://autoload/game_clock.gd").new()
|
|
_state = load("res://autoload/game_state.gd").new()
|
|
_runtime = load("res://autoload/simulation_runtime.gd").new()
|
|
_clock.reset(1, hour, minute)
|
|
_state.reset()
|
|
var report: Dictionary = _runtime.initialize(_clock, _state, seed)
|
|
_clock.set_paused(true)
|
|
_expect(report[&"valid"], "runtime initializes with the Authority aggregate")
|
|
_expect(
|
|
_runtime.authority_service.current_minute == _clock.absolute_minute,
|
|
"Authority time begins aligned with SimulationKernel"
|
|
)
|
|
_expect(
|
|
_runtime.economy_service.has_account(AUTHORITY_FINE_EXPENSE),
|
|
"Authority fine expense account exists at initialization"
|
|
)
|
|
|
|
|
|
func _cleanup() -> void:
|
|
if _runtime != null:
|
|
_runtime.free()
|
|
if _clock != null:
|
|
_clock.free()
|
|
if _state != null:
|
|
_state.free()
|
|
_runtime = null
|
|
_clock = null
|
|
_state = null
|
|
|
|
|
|
func _test_incident_visibility_and_daily_evaluation() -> void:
|
|
_reset_runtime(5_501, 5, 59)
|
|
var reported := _submit(
|
|
&"report_authority_incident",
|
|
{
|
|
&"incident_id": &"incident.runtime.shared",
|
|
&"incident_type": &"illegal_facility",
|
|
&"detector_id": &"detector.noise",
|
|
&"options": {
|
|
&"severity": 80,
|
|
&"heat_delta": 100,
|
|
&"facility_id": &"facility.hidden",
|
|
},
|
|
},
|
|
&"s5.incident.first"
|
|
)
|
|
var duplicate := _submit(
|
|
&"report_authority_incident",
|
|
{
|
|
&"incident_id": &"incident.runtime.shared",
|
|
&"incident_type": &"conflicting_payload",
|
|
&"detector_id": &"detector.witness",
|
|
&"options": {&"severity": 100, &"heat_delta": 100},
|
|
},
|
|
&"s5.incident.second-detector"
|
|
)
|
|
var incident: AuthorityIncident = _runtime.authority_service.get_incident(&"incident.runtime.shared")
|
|
_expect(reported.is_success(), "incident report executes through Runtime Command")
|
|
_expect(
|
|
duplicate.is_success() and duplicate.is_replay,
|
|
"incident_id deduplicates reports from independent detectors"
|
|
)
|
|
_expect(
|
|
_runtime.authority_service.get_authority_state().heat == 100,
|
|
"duplicate detector cannot apply Heat twice"
|
|
)
|
|
_expect(
|
|
incident.detector_ids == [&"detector.noise", &"detector.witness"]
|
|
and incident.incident_type == &"illegal_facility",
|
|
"dedup retains sorted detector audit data and the first canonical payload"
|
|
)
|
|
_expect(
|
|
_count_domain_events(&"INCIDENT_CREATED", incident.incident_id) == 1
|
|
and _count_domain_events(&"INCIDENT_DETECTOR_ADDED", incident.incident_id) == 1,
|
|
"Authority signals project once into DomainEventStream"
|
|
)
|
|
|
|
var wall := Vector3i(1, 0, WorldService.LAYER_FRONT)
|
|
var evidence_cell := Vector3i(2, 0, WorldService.LAYER_FRONT)
|
|
_expect(_runtime.world_service.set_walkable(wall, false), "LOS fixture wall is installed")
|
|
_expect(_runtime.rebuild_path_graph_from_world(), "Path graph rebuilds after wall installation")
|
|
var registered := _submit(
|
|
&"register_authority_evidence",
|
|
{
|
|
&"evidence_id": &"evidence.runtime.wall",
|
|
&"source": &"visual_trace",
|
|
&"severity": 35,
|
|
&"options": {
|
|
&"incident_id": incident.incident_id,
|
|
&"facility_id": &"facility.hidden",
|
|
&"location_cell": evidence_cell,
|
|
&"expires_at": 2_000,
|
|
},
|
|
},
|
|
&"s5.evidence.register"
|
|
)
|
|
var blocked := _submit(
|
|
&"discover_authority_evidence",
|
|
{
|
|
&"evidence_id": &"evidence.runtime.wall",
|
|
&"inspector_id": &"inspector.los",
|
|
&"inspector_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
|
|
&"context": {},
|
|
},
|
|
&"s5.evidence.blocked"
|
|
)
|
|
_expect(registered.is_success(), "latent evidence registers through Runtime Command")
|
|
_expect(
|
|
not blocked.is_success() and blocked.details.get(&"reason", &"") == &"LINE_OF_SIGHT_BLOCKED",
|
|
"WorldService wall blocks automatic evidence discovery"
|
|
)
|
|
_expect(
|
|
_runtime.authority_service.get_authority_state().evidence_points == 0,
|
|
"wall-blocked evidence contributes no Authority score"
|
|
)
|
|
_expect(_runtime.world_service.set_walkable(wall, true), "LOS fixture wall is removed")
|
|
_expect(_runtime.rebuild_path_graph_from_world(), "Path graph rebuilds after opening sight line")
|
|
var discovered := _submit(
|
|
&"discover_authority_evidence",
|
|
{
|
|
&"evidence_id": &"evidence.runtime.wall",
|
|
&"inspector_id": &"inspector.los",
|
|
&"inspector_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
|
|
&"context": {&"illegal_confirmed": true},
|
|
},
|
|
&"s5.evidence.visible"
|
|
)
|
|
_expect(
|
|
discovered.is_success() and discovered.details[&"discovery_method"] == &"LINE_OF_SIGHT",
|
|
"evidence becomes visible only after the World callback confirms sight"
|
|
)
|
|
_expect(
|
|
_runtime.authority_service.get_authority_state().evidence_points == 35,
|
|
"visible evidence contributes its deterministic Incident score"
|
|
)
|
|
|
|
var authority_rng: DeterministicRng = _runtime.get_rng_stream(&"authority")
|
|
var expected_rng := DeterministicRng.from_root_seed(0, &"authority") as DeterministicRng
|
|
_expect(expected_rng.restore(authority_rng.snapshot()), "Authority RNG snapshot clones for prediction")
|
|
var expected_roll := expected_rng.randi_range(0, 99)
|
|
var before_six: Dictionary = _runtime.create_snapshot()
|
|
_clock.advance_one_minute()
|
|
var evaluation := _evaluation_for_day(0)
|
|
_expect(
|
|
_clock.absolute_minute == 360 and _runtime.authority_service.current_minute == 360,
|
|
"INCIDENTS_AND_EVENTS advances Authority time at 06:00"
|
|
)
|
|
_expect(
|
|
int(evaluation.get(&"roll", -1)) == expected_roll
|
|
and bool(evaluation.get(&"selected", false)) == (expected_roll < int(evaluation.get(&"chance", 0))),
|
|
"06:00 inspection decision consumes only the deterministic Authority RNG stream"
|
|
)
|
|
_expect(
|
|
int(evaluation.get(&"chance", -1)) == 95
|
|
and not StringName(evaluation.get(&"inspection_id", &"")).is_empty(),
|
|
"Heat and active-case terms deterministically schedule the selected inspection"
|
|
)
|
|
_expect(
|
|
_runtime.get_rng_stream(&"authority").get_counter() == expected_rng.get_counter(),
|
|
"06:00 evaluation advances the Authority RNG counter exactly once"
|
|
)
|
|
_expect(
|
|
_count_domain_events(&"AUTHORITY_INSPECTION_EVALUATED") == 1
|
|
and _count_domain_events(&"INSPECTION_SCHEDULED") == 1,
|
|
"automatic inspection selection emits its Domain Events once"
|
|
)
|
|
var first_six_snapshot: Dictionary = _runtime.create_snapshot()
|
|
_expect(_runtime.restore_snapshot(before_six), "pre-06:00 Runtime snapshot restores atomically")
|
|
_clock.advance_one_minute()
|
|
_expect(
|
|
_runtime.create_snapshot() == first_six_snapshot,
|
|
"restored 06:00 Authority selection reproduces the full Runtime snapshot"
|
|
)
|
|
|
|
var stable_before_corruption: Dictionary = _runtime.create_snapshot()
|
|
var corrupt := stable_before_corruption.duplicate(true)
|
|
corrupt[&"systems"][&"runtime"][&"authority"][&"current_minute"] = 359
|
|
_expect(
|
|
not _runtime.restore_snapshot(corrupt),
|
|
"restore rejects an Authority/SimulationKernel minute mismatch"
|
|
)
|
|
_expect(
|
|
_runtime.create_snapshot() == stable_before_corruption,
|
|
"rejected Authority restore leaves the live Runtime snapshot unchanged"
|
|
)
|
|
_expect(
|
|
_runtime.authority_service.validate_invariants()[&"ok"],
|
|
"integrated incident, evidence and inspection selection preserve Authority invariants"
|
|
)
|
|
|
|
|
|
func _test_resolution_replay_unreachable_exit_and_long_horizon() -> void:
|
|
_reset_runtime(5_502)
|
|
_expect(_submit(
|
|
&"report_authority_incident",
|
|
{
|
|
&"incident_id": &"incident.runtime.fine",
|
|
&"incident_type": &"contraband",
|
|
&"detector_id": &"detector.audit",
|
|
&"options": {&"severity": 80, &"heat_delta": 20},
|
|
},
|
|
&"s5.fine.incident"
|
|
).is_success(), "fine fixture Incident registers")
|
|
_expect(_submit(
|
|
&"register_authority_evidence",
|
|
{
|
|
&"evidence_id": &"evidence.runtime.fine",
|
|
&"source": &"physical_record",
|
|
&"severity": 60,
|
|
&"options": {
|
|
&"incident_id": &"incident.runtime.fine",
|
|
&"facility_id": &"facility.illegal",
|
|
&"location_cell": Vector3i(2, 0, WorldService.LAYER_FRONT),
|
|
},
|
|
},
|
|
&"s5.fine.evidence"
|
|
).is_success(), "fine fixture Evidence registers")
|
|
_expect(_submit(
|
|
&"schedule_authority_inspection",
|
|
{&"options": {
|
|
&"inspection_id": &"inspection.runtime.fine",
|
|
&"inspector_id": &"inspector.runtime.fine",
|
|
&"entrance_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
|
|
}},
|
|
&"s5.fine.schedule"
|
|
).is_success(), "inspection schedules through Runtime Command")
|
|
_expect(_submit(
|
|
&"start_authority_inspection",
|
|
{&"inspection_id": &"inspection.runtime.fine"},
|
|
&"s5.fine.start"
|
|
).is_success(), "inspection starts through Runtime Command")
|
|
_expect(_submit(
|
|
&"arrive_authority_inspection",
|
|
{&"inspection_id": &"inspection.runtime.fine"},
|
|
&"s5.fine.arrive"
|
|
).is_success(), "inspector checks in through Runtime Command")
|
|
_expect(_submit(
|
|
&"plan_authority_inspection",
|
|
{
|
|
&"inspection_id": &"inspection.runtime.fine",
|
|
&"candidates": [{
|
|
&"waypoint_id": &"waypoint.illegal.room",
|
|
&"cell": Vector3i(2, 0, WorldService.LAYER_FRONT),
|
|
&"suspect_score": 100,
|
|
&"evidence_potential": 100,
|
|
&"travel_minutes": 2,
|
|
&"inspection_minutes": 3,
|
|
}],
|
|
&"time_budget_minutes": 30,
|
|
},
|
|
&"s5.fine.plan"
|
|
).is_success(), "deterministic inspection patrol is planned")
|
|
var moved := _submit(
|
|
&"advance_authority_inspection",
|
|
{&"inspection_id": &"inspection.runtime.fine", &"context": {}},
|
|
&"s5.fine.advance"
|
|
)
|
|
_expect(
|
|
moved.is_success() and moved.details.get(&"reachable", false),
|
|
"PathService callback advances the inspector to one reachable waypoint"
|
|
)
|
|
var mid_inspection: Dictionary = _runtime.create_snapshot()
|
|
var mid_digest := SimulationDigest.compute(mid_inspection)
|
|
var first_final := _run_resolution_timeline()
|
|
_expect(first_final.get(&"ok", false), "inspection resolution timeline completes")
|
|
var first_final_snapshot: Dictionary = _runtime.create_snapshot()
|
|
var first_final_digest := SimulationDigest.compute(first_final_snapshot)
|
|
_expect(_runtime.restore_snapshot(mid_inspection), "mid-inspection Runtime snapshot restores")
|
|
_expect(
|
|
SimulationDigest.compute(_runtime.create_snapshot()) == mid_digest,
|
|
"mid-inspection restore reproduces its exact digest"
|
|
)
|
|
var replay_final := _run_resolution_timeline()
|
|
_expect(replay_final.get(&"ok", false), "restored inspection timeline completes again")
|
|
_expect(
|
|
SimulationDigest.compute(_runtime.create_snapshot()) == first_final_digest
|
|
and _runtime.create_snapshot() == first_final_snapshot,
|
|
"mid-inspection replay reproduces Authority, ledger and Domain Events exactly"
|
|
)
|
|
|
|
_expect(_run_unreachable_inspection(), "unreachable patrol target follows the safe exit path")
|
|
_expect(
|
|
_runtime.authority_service.validate_invariants()[&"ok"]
|
|
and _runtime.economy_service.validate_ledger()[&"ok"],
|
|
"resolved and unreachable inspections preserve Authority and ledger invariants"
|
|
)
|
|
var horizon_snapshot: Dictionary = _runtime.create_snapshot()
|
|
_clock.advance_minutes(1_440)
|
|
var first_horizon_digest: String = _runtime.latest_digest
|
|
var first_horizon_snapshot: Dictionary = _runtime.create_snapshot()
|
|
_expect(
|
|
_runtime.authority_service.current_minute == _clock.absolute_minute,
|
|
"Authority stays aligned through a 24-hour simulation horizon"
|
|
)
|
|
_expect(_runtime.restore_snapshot(horizon_snapshot), "24-hour Authority replay origin restores")
|
|
_clock.advance_minutes(1_440)
|
|
_expect(
|
|
_runtime.latest_digest == first_horizon_digest
|
|
and _runtime.create_snapshot() == first_horizon_snapshot,
|
|
"24-hour Runtime replay reproduces daily Heat and inspection evaluation exactly"
|
|
)
|
|
_expect(
|
|
_runtime.authority_service.validate_invariants()[&"ok"]
|
|
and _runtime.economy_service.validate_ledger()[&"ok"],
|
|
"long replay ends with valid Authority and Economy aggregates"
|
|
)
|
|
|
|
|
|
func _run_resolution_timeline() -> Dictionary:
|
|
var inspected := _submit(
|
|
&"inspect_authority_waypoint",
|
|
{
|
|
&"inspection_id": &"inspection.runtime.fine",
|
|
&"evidence_ids": [&"evidence.runtime.fine"],
|
|
&"context": {&"illegal_confirmed": true},
|
|
},
|
|
&"s5.fine.inspect"
|
|
)
|
|
var reviewed := _submit(
|
|
&"advance_authority_inspection",
|
|
{&"inspection_id": &"inspection.runtime.fine", &"context": {}},
|
|
&"s5.fine.review"
|
|
)
|
|
var authority_before_failure: Dictionary = _runtime.authority_service.create_snapshot()
|
|
var economy_before_failure: Dictionary = _runtime.economy_service.create_snapshot()
|
|
var rejected := _submit(
|
|
&"resolve_authority_inspection",
|
|
{
|
|
&"inspection_id": &"inspection.runtime.fine",
|
|
&"resolution_id": &"resolution.runtime.unfunded",
|
|
&"options": {&"fine_per_point": 100},
|
|
},
|
|
&"s5.fine.resolve.unfunded"
|
|
)
|
|
var rejection_atomic: bool = not rejected.is_success() \
|
|
and _runtime.authority_service.create_snapshot() == authority_before_failure \
|
|
and _runtime.economy_service.create_snapshot() == economy_before_failure
|
|
var resolved := _submit(
|
|
&"resolve_authority_inspection",
|
|
{
|
|
&"inspection_id": &"inspection.runtime.fine",
|
|
&"resolution_id": &"resolution.runtime.once",
|
|
&"options": {&"fine_per_point": 10},
|
|
},
|
|
&"s5.fine.resolve.once"
|
|
)
|
|
var duplicate := _submit(
|
|
&"resolve_authority_inspection",
|
|
{
|
|
&"inspection_id": &"inspection.runtime.fine",
|
|
&"resolution_id": &"resolution.runtime.once",
|
|
&"options": {&"fine_per_point": 999},
|
|
},
|
|
&"s5.fine.resolve.retry"
|
|
)
|
|
var departed := _submit(
|
|
&"depart_authority_inspection",
|
|
{&"inspection_id": &"inspection.runtime.fine"},
|
|
&"s5.fine.depart"
|
|
)
|
|
var fine_entries: Array = _runtime.economy_service.get_entries().filter(
|
|
func(entry: LedgerEntry) -> bool: return entry.category == &"AUTHORITY_FINE"
|
|
)
|
|
var incident: AuthorityIncident = _runtime.authority_service.get_incident(&"incident.runtime.fine")
|
|
var inspection: AuthorityInspection = _runtime.authority_service.get_inspection(&"inspection.runtime.fine")
|
|
_expect(inspected.is_success(), "reached waypoint discovers only the supplied Evidence IDs")
|
|
_expect(
|
|
reviewed.is_success()
|
|
and reviewed.details.get(&"new_state", &"") == &"REVIEWING"
|
|
and inspection != null,
|
|
"completed patrol enters REVIEWING before resolution"
|
|
)
|
|
_expect(rejection_atomic, "unfunded Authority fine leaves Authority and Economy snapshots unchanged")
|
|
_expect(
|
|
resolved.is_success() and resolved.details.get(&"fine_amount", -1) == 600
|
|
and resolved.details.get(&"sanction", &"") == &"TEMPORARY_CLOSURE",
|
|
"evidence score projects the deterministic fine and sanction"
|
|
)
|
|
_expect(
|
|
duplicate.is_success() and duplicate.is_replay
|
|
and duplicate.details.get(&"fine_amount", -1) == 600,
|
|
"duplicate resolution_id replays the canonical outcome"
|
|
)
|
|
_expect(
|
|
fine_entries.size() == 1
|
|
and _runtime.economy_service.get_balance(AUTHORITY_FINE_EXPENSE) == 600
|
|
and _runtime.economy_service.get_balance(PLAYER_CASH) == 4_400
|
|
and _state.get_resource(&"gold") == 4_400,
|
|
"fine posts exactly once to the ledger and synchronized Gold mirror"
|
|
)
|
|
_expect(
|
|
incident != null and incident.state == AuthorityIncident.State.RESOLVED
|
|
and departed.is_success() and inspection.state == AuthorityInspection.State.CLOSED,
|
|
"successful resolution closes its Incident and inspection lifecycle"
|
|
)
|
|
_expect(
|
|
_count_domain_events(&"INSPECTION_RESOLVED", &"inspection.runtime.fine") == 1,
|
|
"resolution replay emits no duplicate INSPECTION_RESOLVED event"
|
|
)
|
|
return {
|
|
&"ok": inspected.is_success()
|
|
and reviewed.is_success()
|
|
and rejection_atomic
|
|
and resolved.is_success()
|
|
and duplicate.is_success()
|
|
and departed.is_success(),
|
|
}
|
|
|
|
|
|
func _run_unreachable_inspection() -> bool:
|
|
var inspection_id := &"inspection.runtime.blocked"
|
|
var target := Vector3i(0, 1, WorldService.LAYER_FRONT)
|
|
var scheduled := _submit(
|
|
&"schedule_authority_inspection",
|
|
{&"options": {
|
|
&"inspection_id": inspection_id,
|
|
&"inspector_id": &"inspector.runtime.blocked",
|
|
&"entrance_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
|
|
}},
|
|
&"s5.blocked.schedule"
|
|
)
|
|
var started := _submit(
|
|
&"start_authority_inspection",
|
|
{&"inspection_id": inspection_id},
|
|
&"s5.blocked.start"
|
|
)
|
|
var arrived := _submit(
|
|
&"arrive_authority_inspection",
|
|
{&"inspection_id": inspection_id},
|
|
&"s5.blocked.arrive"
|
|
)
|
|
var planned := _submit(
|
|
&"plan_authority_inspection",
|
|
{
|
|
&"inspection_id": inspection_id,
|
|
&"candidates": [{
|
|
&"waypoint_id": &"waypoint.sealed",
|
|
&"cell": target,
|
|
&"suspect_score": 90,
|
|
&"evidence_potential": 90,
|
|
&"travel_minutes": 5,
|
|
&"inspection_minutes": 5,
|
|
}],
|
|
},
|
|
&"s5.blocked.plan"
|
|
)
|
|
var blocked := _submit(
|
|
&"advance_authority_inspection",
|
|
{&"inspection_id": inspection_id, &"context": {}},
|
|
&"s5.blocked.advance"
|
|
)
|
|
var inspection: AuthorityInspection = _runtime.authority_service.get_inspection(inspection_id)
|
|
var safe_departure: bool = blocked.is_success() \
|
|
and not bool(blocked.details.get(&"reachable", true)) \
|
|
and inspection != null \
|
|
and inspection.state == AuthorityInspection.State.DEPARTING \
|
|
and inspection.exit_reason == &"PATH_UNREACHABLE" \
|
|
and inspection.visited_waypoint_ids.is_empty() \
|
|
and inspection.inaccessible_waypoint_ids == [&"waypoint.sealed"] \
|
|
and not inspection.result_reason.is_empty() \
|
|
and _count_domain_events(&"INSPECTION_PATH_BLOCKED", inspection_id) == 1
|
|
var departed := _submit(
|
|
&"depart_authority_inspection",
|
|
{&"inspection_id": inspection_id},
|
|
&"s5.blocked.depart"
|
|
)
|
|
return scheduled.is_success() \
|
|
and started.is_success() \
|
|
and arrived.is_success() \
|
|
and planned.is_success() \
|
|
and safe_departure \
|
|
and departed.is_success() \
|
|
and inspection.state == AuthorityInspection.State.CLOSED
|
|
|
|
|
|
func _submit(command_type: StringName, payload: Dictionary, idempotency_key: StringName) -> CommandResult:
|
|
return _runtime.submit_command(
|
|
command_type,
|
|
payload,
|
|
&"authority_controller",
|
|
&"",
|
|
80,
|
|
-1,
|
|
idempotency_key
|
|
)
|
|
|
|
|
|
func _evaluation_for_day(day_index: int) -> Dictionary:
|
|
var snapshot: Dictionary = _runtime.authority_service.create_snapshot()
|
|
for value: Variant in snapshot[&"evaluated_inspection_days"]:
|
|
if value is Dictionary and int(value.get(&"day_index", -1)) == day_index:
|
|
return (value[&"result"] as Dictionary).duplicate(true)
|
|
return {}
|
|
|
|
|
|
func _count_domain_events(event_type: StringName, entity_id: StringName = &"") -> int:
|
|
var count := 0
|
|
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
|
|
if event.event_type != event_type:
|
|
continue
|
|
if entity_id.is_empty() or entity_id in event.entity_ids:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _expect(condition: bool, label: String) -> void:
|
|
_checks += 1
|
|
if condition:
|
|
print(" ✓ %s" % label)
|
|
return
|
|
_failures += 1
|
|
push_error(" ✗ %s" % label)
|