The parallel item runs branches concurrently with sync:all or sync:any policy and optional per-branch wait_for synchronization. Each branch runs in its own daemon thread and produces a clean per-item entry in the SQLite report; the live output is prefixed [<branch_name>] so concurrent branches stay readable. Supporting changes: - StdoutProxy (lib/stdout_redirect.py): thread-aware sys.stdout/stderr with per-thread capture buffers and per-branch live-output prefix. Adds writeln() for Python 3.14 unittest compatibility. - TestItemContainer: shared base extracted from Group/Cycle for the sequential children execution pattern. - TestItemSleep: interruptible loop polling _is_stopped so sync:any can cancel slow branches quickly. - TestReport: thread-safe SQLite (check_same_thread=False + lock). Also drops the unused -m/--terminal mode and its module. Validation: 11 scenarios in test/validation/items/parallel covering sync:all/any, no_fail, wait_for + timeout, conditions, multi-branch, nested parallel, parallel inside loop, real branch failure. Documentation: new parallel_test_item.rst added to the manual. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
344 lines
8.2 KiB
Plaintext
344 lines
8.2 KiB
Plaintext
# --- Test 1: both branches succeed, sync:all ---
|
|
- parallel:
|
|
name: Both branches pass
|
|
key: $(test)_PASS
|
|
sync: all
|
|
branches:
|
|
- name: Branch A
|
|
steps:
|
|
- let:
|
|
name: Set A done
|
|
values:
|
|
- branch_a_done: true
|
|
- name: Branch B
|
|
steps:
|
|
- let:
|
|
name: Set B done
|
|
values:
|
|
- branch_b_done: true
|
|
|
|
- check:
|
|
name: Both branches ran
|
|
key: $(test)_PASS
|
|
values:
|
|
- <| $(branch_a_done) == True |>
|
|
- <| $(branch_b_done) == True |>
|
|
|
|
# --- Test 2: one branch fails, sync:all + no_fail → parallel forced to PASS ---
|
|
- parallel:
|
|
name: One branch fails
|
|
key: $(test)_PASS
|
|
sync: all
|
|
no_fail: true
|
|
branches:
|
|
- name: Pass branch
|
|
steps:
|
|
- let:
|
|
name: Set pass flag
|
|
values:
|
|
- pass_branch_ran: true
|
|
- name: Fail branch
|
|
steps:
|
|
- py_func:
|
|
name: Raise exception
|
|
file: $(test_path)$(psep)parallel.py
|
|
func_name: sleep_func
|
|
param: [0]
|
|
expected_result: fail
|
|
|
|
- check:
|
|
name: Pass branch still ran
|
|
key: $(test)_PASS
|
|
values:
|
|
- <| $(pass_branch_ran) == True |>
|
|
|
|
# --- Test 3: sync:any — first branch done stops the rest ---
|
|
- let:
|
|
name: Reset slow flag
|
|
values:
|
|
- slow_done: false
|
|
|
|
- parallel:
|
|
name: sync any - first wins
|
|
key: $(test)_PASS
|
|
sync: any
|
|
branches:
|
|
- name: Fast branch
|
|
steps:
|
|
- let:
|
|
name: Fast done
|
|
values:
|
|
- fast_done: true
|
|
- name: Slow branch
|
|
steps:
|
|
- py_func:
|
|
name: Sleep 2s
|
|
file: $(test_path)$(psep)parallel.py
|
|
func_name: sleep_func
|
|
param: [2]
|
|
- let:
|
|
name: Slow done
|
|
values:
|
|
- slow_done: true
|
|
|
|
- check:
|
|
name: Fast branch ran, slow branch was stopped
|
|
key: $(test)_PASS
|
|
values:
|
|
- <| $(fast_done) == True |>
|
|
- <| $(slow_done) == False |>
|
|
|
|
# --- Test 4: wait_for — branch B waits for A to set a flag ---
|
|
- let:
|
|
name: Reset sync flag
|
|
values:
|
|
- sync_flag: ""
|
|
- waiter_ran: false
|
|
|
|
- parallel:
|
|
name: wait_for synchronization
|
|
key: $(test)_PASS
|
|
sync: all
|
|
branches:
|
|
- name: Setter branch
|
|
steps:
|
|
- py_func:
|
|
name: Sleep 0.3s then set flag
|
|
file: $(test_path)$(psep)parallel.py
|
|
func_name: sleep_func
|
|
param: [0.3]
|
|
- let:
|
|
name: Set sync flag
|
|
values:
|
|
- sync_flag: ready
|
|
- name: Waiter branch
|
|
wait_for:
|
|
condition: <| "$(sync_flag)" == "ready" |>
|
|
timeout: 10
|
|
steps:
|
|
- let:
|
|
name: Got flag
|
|
values:
|
|
- waiter_ran: true
|
|
|
|
- check:
|
|
name: Waiter branch ran after flag was set
|
|
key: $(test)_PASS
|
|
values:
|
|
- <| $(waiter_ran) == True |>
|
|
|
|
# --- Test 5: parallel is faster than sequential (timing) ---
|
|
# Two 1s sleeps in parallel → ~1s total, not ~2s sequential
|
|
- parallel:
|
|
name: Timing test
|
|
key: $(test)_PASS
|
|
sync: all
|
|
branches:
|
|
- name: Sleep A
|
|
steps:
|
|
- sleep:
|
|
name: Sleep 1s A
|
|
timeout: 1
|
|
- name: Sleep B
|
|
steps:
|
|
- sleep:
|
|
name: Sleep 1s B
|
|
timeout: 1
|
|
|
|
- let:
|
|
name: Capture parallel duration
|
|
values:
|
|
- parallel_duration: $(ts_duration_Timing test)
|
|
|
|
- check:
|
|
name: Duration < 1.8s (would be 2s if sequential)
|
|
key: $(test)_PASS
|
|
values:
|
|
- <| float("$(parallel_duration)") < 1.8 |>
|
|
|
|
# --- Test 6: more than two branches ---
|
|
- let:
|
|
name: Reset N flags
|
|
values:
|
|
- n_a: false
|
|
- n_b: false
|
|
- n_c: false
|
|
- n_d: false
|
|
|
|
- parallel:
|
|
name: Four branches
|
|
key: $(test)_PASS
|
|
sync: all
|
|
branches:
|
|
- name: NA
|
|
steps:
|
|
- let: {name: set n_a, values: [{n_a: true}]}
|
|
- name: NB
|
|
steps:
|
|
- let: {name: set n_b, values: [{n_b: true}]}
|
|
- name: NC
|
|
steps:
|
|
- let: {name: set n_c, values: [{n_c: true}]}
|
|
- name: ND
|
|
steps:
|
|
- let: {name: set n_d, values: [{n_d: true}]}
|
|
|
|
- check:
|
|
name: Four branches all set their flag
|
|
key: $(test)_PASS
|
|
values:
|
|
- <| $(n_a) == True |>
|
|
- <| $(n_b) == True |>
|
|
- <| $(n_c) == True |>
|
|
- <| $(n_d) == True |>
|
|
|
|
# --- Test 7: nested parallel ---
|
|
- let:
|
|
name: Reset nested flags
|
|
values:
|
|
- outer_x: false
|
|
- inner_x_1: false
|
|
- inner_x_2: false
|
|
|
|
- parallel:
|
|
name: Outer parallel
|
|
key: $(test)_PASS
|
|
sync: all
|
|
branches:
|
|
- name: Outer X
|
|
steps:
|
|
- let: {name: set outer_x, values: [{outer_x: true}]}
|
|
- parallel:
|
|
name: Inner parallel
|
|
sync: all
|
|
branches:
|
|
- name: Inner X1
|
|
steps:
|
|
- let: {name: set inner_x_1, values: [{inner_x_1: true}]}
|
|
- name: Inner X2
|
|
steps:
|
|
- let: {name: set inner_x_2, values: [{inner_x_2: true}]}
|
|
- name: Outer Y
|
|
steps:
|
|
- sleep:
|
|
name: brief sleep
|
|
timeout: 0
|
|
|
|
- check:
|
|
name: Nested parallel set all flags
|
|
key: $(test)_PASS
|
|
values:
|
|
- <| $(outer_x) == True |>
|
|
- <| $(inner_x_1) == True |>
|
|
- <| $(inner_x_2) == True |>
|
|
|
|
# --- Test 9: wait_for timeout ---
|
|
- let:
|
|
name: Reset waiter timeout flag
|
|
values:
|
|
- waiter_timeout_ran: false
|
|
|
|
- parallel:
|
|
name: wait_for timeout
|
|
key: $(test)_PASS
|
|
sync: all
|
|
no_fail: true
|
|
branches:
|
|
- name: Quick branch
|
|
steps:
|
|
- sleep:
|
|
name: brief sleep
|
|
timeout: 0
|
|
- name: Doomed waiter
|
|
wait_for:
|
|
condition: <| "never" == "ready" |>
|
|
timeout: 1
|
|
steps:
|
|
- let: {name: should not run, values: [{waiter_timeout_ran: true}]}
|
|
|
|
- check:
|
|
name: Doomed waiter never ran its steps
|
|
key: $(test)_PASS
|
|
values:
|
|
- <| $(waiter_timeout_ran) == False |>
|
|
|
|
# --- Test 10: sync:all with a real branch failure (parallel must FAIL) ---
|
|
- parallel:
|
|
name: One branch really fails
|
|
key: $(test)_FAIL
|
|
sync: all
|
|
branches:
|
|
- name: ok branch
|
|
steps:
|
|
- let: {name: noop, values: [{noop_var: 1}]}
|
|
- name: broken branch
|
|
steps:
|
|
- py_func:
|
|
name: Forced fail
|
|
file: $(test_path)$(psep)parallel.py
|
|
func_name: sleep_func
|
|
param: [0]
|
|
expected_result: fail
|
|
|
|
# --- Test 11: branch with unmet condition is skipped, not failing the parallel ---
|
|
- let:
|
|
name: Reset branch condition flag
|
|
values:
|
|
- cond_branch_ran: false
|
|
- other_branch_ran: false
|
|
|
|
- parallel:
|
|
name: Condition-skipped branch
|
|
key: $(test)_PASS
|
|
sync: all
|
|
branches:
|
|
- name: Skipped branch
|
|
condition: <| "always" == "false" |>
|
|
steps:
|
|
- let: {name: should not run, values: [{cond_branch_ran: true}]}
|
|
- name: Other branch
|
|
steps:
|
|
- let: {name: ran, values: [{other_branch_ran: true}]}
|
|
|
|
- check:
|
|
name: Skipped condition branch did not run
|
|
key: $(test)_PASS
|
|
values:
|
|
- <| $(cond_branch_ran) == False |>
|
|
- <| $(other_branch_ran) == True |>
|
|
|
|
# --- Test 8: parallel inside loop (re-execution) ---
|
|
- let:
|
|
name: Reset loop counters
|
|
values:
|
|
- loop_count_a: 0
|
|
- loop_count_b: 0
|
|
|
|
- loop:
|
|
name: Loop wrapping parallel
|
|
iterator: 3
|
|
steps:
|
|
- parallel:
|
|
name: Per-iteration parallel
|
|
sync: all
|
|
branches:
|
|
- name: LA
|
|
steps:
|
|
- let:
|
|
name: bump A
|
|
values:
|
|
- loop_count_a: <| int("$(loop_count_a)") + 1 |>
|
|
- name: LB
|
|
steps:
|
|
- let:
|
|
name: bump B
|
|
values:
|
|
- loop_count_b: <| int("$(loop_count_b)") + 1 |>
|
|
|
|
- check:
|
|
name: Both branches ran 3 times
|
|
key: $(test)_PASS
|
|
values:
|
|
- <| int("$(loop_count_a)") == 3 |>
|
|
- <| int("$(loop_count_b)") == 3 |>
|