refactor(pipeline): 출력 포트 경계를 적용한다

runner 출력 경계 분리 마일스톤에서 pipeline이 CLI 구현체에 직접 의존하지 않도록 explicit ExecutionContext.output 경계를 사용한다.

현재 iop 미구현 legacy smoke는 사용자 승인으로 완료 범위에서 제외하고 task archive와 완료 로그를 남긴다.
This commit is contained in:
toki 2026-06-07 17:52:25 +09:00
parent 365995900e
commit 04adcaf93a
16 changed files with 662 additions and 372 deletions

View file

@ -30,6 +30,10 @@ pipeline/core 실행 로직이 CLI 출력 구현에 직접 의존하지 않도
- `apps/runner/lib/oto/pipeline/pipeline_exe.dart``pipeline_foreach.dart`는 CLI 출력 계층을 직접 import한다.
- `apps/runner/lib/oto/core/data_composer.dart`는 CLI color/printer 형태를 core 동작으로 노출한다.
- 표준선: domain 실행 로직은 출력 port를 호출하고, CLI/UI/server는 port adapter를 제공한다.
- 현재 작업 현황:
- `01_output_port_contract`는 PASS 완료되어 `[port]` Task가 완료 상태다.
- `02+01_pipeline_output_port` active plan/review가 다음 실행 대상이며 `[pipeline]` Task를 완료한다.
- `03+01,02_core_cli_adapter` active plan/review는 `02+01_pipeline_output_port` 완료 후 `[core]`, `[cli-adapter]` Task를 완료한다.
## 기능

View file

@ -0,0 +1,60 @@
# User Review Resolved - m-runner-output-boundary/02+01_pipeline_output_port
## 요청 일시
2026-06-07
## 상태
RESOLVED_PASS
## 사유
- 유형: environment-blocked
- 현재 리뷰 회차: 1
- 최종 판정: PASS (user-review resolved)
- 요약: pipeline output port 구현과 focused regression은 통과했다. 전체 `cd apps/runner && dart test`의 legacy iop Edge smoke 실패는 현재 iop에 해당 구현이 없어 이 pipeline subtask 완료 범위에서 제외하기로 사용자가 승인했다.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_local_G06_0.log` | `code_review_local_G06_0.log` | FAIL | 전체 `dart test``test/oto_iop_connection_smoke_test.dart`의 external iop Edge config 실패로 통과하지 못했다. |
| `USER_REVIEW.md` | user decision | PASS/RESOLVED | 사용자가 현재 iop 미구현으로 인한 legacy iop smoke 실패는 이 pipeline subtask 범위에서 제외하는 것이 맞다고 승인했다. |
## 해소 결정
- 결정: legacy iop smoke는 현재 iop에 구현이 없어 이 pipeline output port subtask의 완료 범위에서 제외한다.
- 최종 완료 근거: `cd apps/runner && dart analyze` 통과, `cd apps/runner && dart test test/oto_core_test.dart` 통과, pipeline output이 explicit `ExecutionContext.output`으로 전달되는 regression test 추가 및 통과.
- 미해결 외부 항목: `test/oto_iop_connection_smoke_test.dart`는 별도 agent/iop 범위에서 다룬다.
## 차단 근거
- 문제: `plan_local_G06_0.log`는 최종 검증으로 `cd apps/runner && dart test` 통과를 요구했지만, 전체 runner test가 legacy iop smoke 실패로 종료된다.
- 현재 archive plan: `plan_local_G06_0.log`
- 현재 archive review: `code_review_local_G06_0.log`
- 검증 명령: `cd apps/runner && dart test`; focused 재현 `cd apps/runner && dart test test/oto_iop_connection_smoke_test.dart -p vm --plain-name 'OTO Dart proto-socket client registers with iop Edge and goes online'`
- 실제 출력: 전체 suite는 `00:15 +216 -1: Some tests failed.`로 종료했다. focused 재현은 `iop Edge exited before listening (code 1): Error: load config: nodes[0] alias="oto-smoke": invalid agent_kind "oto-agent" (allowed: "generic-node")`로 실패했다.
- 차단 판단 근거: `agent-test/local/agent-smoke.md`는 legacy iop smoke를 독립 OTO Control Plane agent-smoke 필수 검증에서 제외한다고 명시하지만, 이 plan은 pipeline-smoke 최종 검증으로 전체 runner test를 요구했다. 사용자가 legacy iop smoke 실패를 이 pipeline subtask 완료에서 어떻게 취급할지 결정해야 한다.
## 사용자 결정 필요
- [ ] 자동 follow-up plan/review를 계속 진행한다.
- [ ] 계획을 재작성한다.
- [ ] 테스트 환경, secret, 외부 서비스, SDK, 장비 조건을 준비한 뒤 재시도한다.
- [x] 작업 범위를 줄이거나 보류/폐기한다.
## 재개 조건
- 다음 중 하나가 필요하다: 호환되는 iop checkout을 `IOP_REPO_ROOT`로 제공해 전체 `cd apps/runner && dart test`가 통과한다; 별도 agent/iop 범위 작업으로 legacy smoke fixture를 현재 iop 계약에 맞춘다; 또는 사용자가 이 pipeline subtask는 `dart analyze``test/oto_core_test.dart` 통과 및 legacy iop smoke 실패 기록으로 완료 가능하다고 승인한다.
## 다음 실행 힌트
- 재시도 명령: `cd apps/runner && dart test`
- focused 차단 확인: `cd apps/runner && dart test test/oto_iop_connection_smoke_test.dart -p vm --plain-name 'OTO Dart proto-socket client registers with iop Edge and goes online'`
- 작업 경로: `agent-task/m-runner-output-boundary/02+01_pipeline_output_port/`
## 종료 규칙
- 사용자가 이 stop state를 완료/PASS로 해소하면 `USER_REVIEW.md`를 해소 상태로 갱신하고, `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
- 새 구현이 필요하면 `plan` 스킬이 `USER_REVIEW.md``user_review_N.log`로 아카이브한 뒤 새 `PLAN-*-G??.md` / `CODE_REVIEW-*-G??.md`를 작성한다.

View file

@ -0,0 +1,133 @@
<!-- task=m-runner-output-boundary/02+01_pipeline_output_port plan=1 tag=REFACTOR -->
# Code Review Reference - REFACTOR
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> Do not ask the user directly; record blockers in `사용자 리뷰 요청`.
> Finalization is review-agent-only.
## 개요
date=2026-06-07
task=m-runner-output-boundary/02+01_pipeline_output_port, plan=1, tag=REFACTOR
## Roadmap Targets
- Milestone: `agent-roadmap/phase/independent-control-plane/milestones/runner-output-boundary.md`
- Task ids:
- `pipeline`: pipeline domain이 CLI 구현체 대신 output port를 사용하도록 분리한다. 검증: runner analyze와 pipeline 관련 테스트가 통과한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다.
1. predecessor complete.log 확인 여부를 검증한다.
2. 판정을 append한다.
3. PASS이면 plan/review log화, complete.log 작성, archive 이동을 수행한다.
4. roadmap 수정은 런타임 책임이다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REFACTOR-1] Pipeline Output Port Rollout | [x] |
## 구현 체크리스트
- [x] `01_output_port_contract` complete.log를 확인한다.
- [x] `pipeline_exe.dart`, `pipeline_exe_handle.dart`, `pipeline_foreach.dart`에서 CLI import와 CLI direct call을 제거한다.
- [x] pipeline 출력은 `runtimeContext.output.line` 또는 `runtimeContext.output.buildStep`을 사용한다.
- [x] explicit context output regression test를 추가한다.
- [x] `cd apps/runner && dart analyze`, `cd apps/runner && dart test test/oto_core_test.dart`, `cd apps/runner && dart test`를 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] active review/plan 파일을 `.log`로 아카이브한다.
- [x] PASS이면 `complete.log` 작성 후 active task 디렉터리를 archive로 이동한다.
- [x] PASS split 작업이면 parent 유지/정리 상태를 확인한다.
- [x] PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다.
- [x] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되어 `USER_REVIEW.md`를 resolved 상태로 갱신하고 `complete.log`를 작성한다.
## 계획 대비 변경 사항
- `printCondition` 인자를 positional이 아닌 named parameter `{ExecutionContext? context}`로 추가했다. 기존 5개 호출부를 모두 `context: runtimeContext`로 갱신하여 누락 없이 처리했다.
- `pipeline_exe_handle.dart`에서 `Application` import를 제거하려 했으나 `CommandState` enum이 `application.dart`에 정의되어 있어 유지했다. `cli.dart` import만 제거했다.
## 주요 설계 결정
- `printCondition` static 메서드에 `ExecutionContext? context` named optional 파라미터를 추가하고, `(context ?? Application.instance.context).output.line(...)` 패턴으로 fallback 유지. `Application.log`는 로그 파일 경계로 보존했다.
- `buildStep` 호출 시 명시적 `style` 인자 없이 기본값(`RunnerOutputStyle.success`) 사용. `printBuildStep(Color.green)` 의미와 일치한다.
- `PipelineForeach` 완료 출력: `CLI.println(color: Color.magentaStrong)` → `runtimeContext.output.line(style: RunnerOutputStyle.progress)` 매핑.
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- pipeline 도메인에서 `package:oto/cli/cli.dart` import가 사라졌는지 확인한다.
- `Application.instance.printBuildStep` 직접 호출이 pipeline에 남지 않았는지 확인한다.
- explicit context output 테스트가 singleton Application output에 기대지 않는지 확인한다.
## 검증 결과
### REFACTOR-1 중간 검증
```bash
$ cd apps/runner && dart analyze
Analyzing runner...
No issues found!
$ cd apps/runner && dart test test/oto_core_test.dart
00:00 +31: All tests passed!
```
### 최종 검증
```bash
$ cd apps/runner && dart analyze
Analyzing runner...
No issues found!
$ cd apps/runner && dart test
00:15 +216 -1: Some tests failed.
```
> 실패한 1건은 `test/oto_iop_connection_smoke_test.dart: OTO Dart proto-socket client registers with iop Edge and goes online`이다. 재실행한 focused 명령도 `iop Edge exited before listening (code 1): Error: load config: nodes[0] alias="oto-smoke": invalid agent_kind "oto-agent" (allowed: "generic-node")`로 실패했다. pipeline 변경과 직접 연결된 `oto_core_test.dart` 31개는 통과했다.
## 코드리뷰 결과
_리뷰 에이전트가 append한다._
### 2026-06-07 리뷰 판정
- 종합 판정: FAIL
- 차원별 평가:
- Correctness: Pass
- Completeness: Fail
- Test coverage: Pass
- API contract: Pass
- Code quality: Pass
- Plan deviation: Warn
- Verification trust: Fail
- 발견된 문제:
- Required: `agent-task/m-runner-output-boundary/02+01_pipeline_output_port/plan_local_G06_0.log:154`의 최종 검증 계약은 `cd apps/runner && dart test` 통과를 요구하지만, 실제 재현 결과 전체 runner test가 `test/oto_iop_connection_smoke_test.dart` 1건 실패로 종료된다. 실패 근거는 [oto_iop_connection_smoke_test.dart](/config/workspace/oto/apps/runner/test/oto_iop_connection_smoke_test.dart:276)의 legacy iop Edge config가 현재 외부 iop checkout에서 허용하지 않는 `agent_kind: "oto-agent"`를 사용해 `allowed: "generic-node"` 오류가 나는 것이다. 이 pipeline subtask 안에서 임의로 agent/iop smoke 계약을 변경하거나 legacy smoke 제외를 확정할 수 없으므로, 사용자 결정 또는 호환되는 `IOP_REPO_ROOT` 환경이 필요하다.
- Nit: 리뷰 중 `dart format`으로 변경 대상 Dart 파일의 formatting drift를 정리했다. 동작 변경은 없다.
- 다음 단계:
- USER_REVIEW: active plan/review를 로그로 아카이브하고, 전체 `dart test` 실패를 사용자 판단/외부 환경 선결 조건으로 남기는 `USER_REVIEW.md`를 작성한다.

View file

@ -0,0 +1,44 @@
# Complete - m-runner-output-boundary/02+01_pipeline_output_port
## 완료 일시
2026-06-07
## 요약
pipeline domain의 CLI direct output dependency를 explicit `ExecutionContext.output` port로 전환했고, 1회 리뷰에서 legacy iop smoke 범위 판단으로 USER_REVIEW를 거친 뒤 PASS로 종료했다.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_local_G06_0.log` | `code_review_local_G06_0.log` | FAIL | 전체 `dart test`가 legacy iop smoke 1건으로 실패해 사용자 범위 판단이 필요했다. |
| `USER_REVIEW.md` | user decision | PASS/RESOLVED | 현재 iop 미구현으로 인한 legacy iop smoke 실패는 이 pipeline subtask 완료 범위에서 제외한다고 사용자가 승인했다. |
## 구현/정리 내용
- pipeline condition/build-step/foreach progress 출력이 `runtimeContext.output.line` 또는 `runtimeContext.output.buildStep`을 사용하도록 전환했다.
- `apps/runner/lib/oto/pipeline/**`에서 `package:oto/cli/cli.dart` import와 `Application.instance.printBuildStep` direct call을 제거했다.
- explicit context output port regression test를 `apps/runner/test/oto_core_test.dart`에 추가했다.
- 리뷰 중 변경 대상 Dart 파일에 `dart format`을 적용해 formatting drift를 정리했다.
## 최종 검증
- `cd apps/runner && dart analyze` - PASS; `No issues found!`
- `cd apps/runner && dart test test/oto_core_test.dart` - PASS; `+31: All tests passed!`
- `cd apps/runner && dart test` - USER-RESOLVED EXCLUDED; `test/oto_iop_connection_smoke_test.dart`가 `invalid agent_kind "oto-agent" (allowed: "generic-node")`로 실패하지만, 현재 iop 미구현으로 이 pipeline subtask 완료 범위에서 제외한다고 사용자가 승인했다.
## Roadmap Completion
- Milestone: `agent-roadmap/phase/independent-control-plane/milestones/runner-output-boundary.md`
- Completed task ids:
- `pipeline`: PASS; evidence=`agent-task/archive/2026/06/m-runner-output-boundary/02+01_pipeline_output_port/plan_local_G06_0.log`, `agent-task/archive/2026/06/m-runner-output-boundary/02+01_pipeline_output_port/code_review_local_G06_0.log`, `agent-task/archive/2026/06/m-runner-output-boundary/02+01_pipeline_output_port/USER_REVIEW.md`; verification=`cd apps/runner && dart analyze`, `cd apps/runner && dart test test/oto_core_test.dart`
- Not completed task ids: 없음
## 잔여 Nit
- 없음
## 후속 작업
- legacy iop smoke는 현재 iop 구현 상태와 맞지 않으므로 별도 agent/iop 범위에서 정리한다.

View file

@ -15,8 +15,8 @@
## Roadmap Dependency
- predecessor: `agent-task/m-runner-output-boundary/01_output_port_contract/complete.log`
- 상태: 작성 시점에는 미충족. 구현 시작 전 active 또는 archive complete.log를 확인한다.
- predecessor: `agent-task/archive/2026/06/m-runner-output-boundary/01_output_port_contract/complete.log`
- 상태: 충족. `01_output_port_contract`가 PASS 완료되어 milestone `[port]` Task에 반영되었으므로 이 subtask는 실행 가능하다.
## 분석 결과

View file

@ -1,108 +0,0 @@
<!-- task=m-runner-output-boundary/02+01_pipeline_output_port plan=1 tag=REFACTOR -->
# Code Review Reference - REFACTOR
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> Do not ask the user directly; record blockers in `사용자 리뷰 요청`.
> Finalization is review-agent-only.
## 개요
date=2026-06-07
task=m-runner-output-boundary/02+01_pipeline_output_port, plan=1, tag=REFACTOR
## Roadmap Targets
- Milestone: `agent-roadmap/phase/independent-control-plane/milestones/runner-output-boundary.md`
- Task ids:
- `pipeline`: pipeline domain이 CLI 구현체 대신 output port를 사용하도록 분리한다. 검증: runner analyze와 pipeline 관련 테스트가 통과한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다.
1. predecessor complete.log 확인 여부를 검증한다.
2. 판정을 append한다.
3. PASS이면 plan/review log화, complete.log 작성, archive 이동을 수행한다.
4. roadmap 수정은 런타임 책임이다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REFACTOR-1] Pipeline Output Port Rollout | [ ] |
## 구현 체크리스트
- [ ] `01_output_port_contract` complete.log를 확인한다.
- [ ] `pipeline_exe.dart`, `pipeline_exe_handle.dart`, `pipeline_foreach.dart`에서 CLI import와 CLI direct call을 제거한다.
- [ ] pipeline 출력은 `runtimeContext.output.line` 또는 `runtimeContext.output.buildStep`을 사용한다.
- [ ] explicit context output regression test를 추가한다.
- [ ] `cd apps/runner && dart analyze`, `cd apps/runner && dart test test/oto_core_test.dart`, `cd apps/runner && dart test`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] active review/plan 파일을 `.log`로 아카이브한다.
- [ ] PASS이면 `complete.log` 작성 후 active task 디렉터리를 archive로 이동한다.
- [ ] PASS split 작업이면 parent 유지/정리 상태를 확인한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- pipeline 도메인에서 `package:oto/cli/cli.dart` import가 사라졌는지 확인한다.
- `Application.instance.printBuildStep` 직접 호출이 pipeline에 남지 않았는지 확인한다.
- explicit context output 테스트가 singleton Application output에 기대지 않는지 확인한다.
## 검증 결과
### REFACTOR-1 중간 검증
```bash
$ cd apps/runner && dart analyze
(output)
$ cd apps/runner && dart test test/oto_core_test.dart
(output)
```
### 최종 검증
```bash
$ cd apps/runner && dart analyze
(output)
$ cd apps/runner && dart test
(output)
```
## 코드리뷰 결과
_리뷰 에이전트가 append한다._

View file

@ -16,9 +16,9 @@
## Roadmap Dependency
- predecessor: `agent-task/m-runner-output-boundary/01_output_port_contract/complete.log`
- predecessor: `agent-task/archive/2026/06/m-runner-output-boundary/01_output_port_contract/complete.log`
- predecessor: `agent-task/m-runner-output-boundary/02+01_pipeline_output_port/complete.log`
- 상태: 작성 시점에는 미충족. 구현 시작 전 active 또는 archive complete.log를 확인한다.
- 상태: 부분 충족. `01_output_port_contract`는 PASS 완료되었고, `02+01_pipeline_output_port` 완료 후 이 subtask를 실행한다.
## 분석 결과

View file

@ -60,16 +60,20 @@ class PipelineContain extends PipelineExecutor {
}
//set true task
var validateResult =
PipelineExecutor.validateTasks(data.on_true, context: context);
var validateResult = PipelineExecutor.validateTasks(
data.on_true,
context: context,
);
if (!validateResult.enable) {
return validateResult;
}
_pipelineTrue = validateResult.pipeline;
//set false task
validateResult =
PipelineExecutor.validateTasks(data.on_false, context: context);
validateResult = PipelineExecutor.validateTasks(
data.on_false,
context: context,
);
if (!validateResult.enable) {
return validateResult;
}
@ -82,21 +86,30 @@ class PipelineContain extends PipelineExecutor {
@override
Future execute() async {
var result = ConditionCheckResult();
var value = TagSystem.replaceSingleTagValue(data.target, context: runtimeContext);
var value = TagSystem.replaceSingleTagValue(
data.target,
context: runtimeContext,
);
value ??= data.target;
result.conditionValue = 'Contain in "$value".';
var listRaw = data.list;
if(listRaw is String) {
var str = TagSystem.replaceTagValue(listRaw, context: runtimeContext) as String;
if (listRaw is String) {
var str =
TagSystem.replaceTagValue(listRaw, context: runtimeContext) as String;
result.result = str.contains(value);
} else {
var list = listRaw as List;
result.result = list.contains(value);
}
await PipelineExecutor.printCondition('Contain', 'Contain in "${data.target}"', getPostfix(result.result), result);
await PipelineExecutor.printCondition(
'Contain',
'Contain in "${data.target}"',
getPostfix(result.result),
result,
context: runtimeContext,
);
if (result.result) {
await _pipelineTrue?.execute();
} else {

View file

@ -1,8 +1,8 @@
import 'package:dart_framework/utils/system_util.dart';
import 'package:oto/cli/cli.dart';
import 'package:oto/oto/application.dart';
import 'package:oto/oto/commands/command.dart';
import 'package:oto/oto/core/execution_context.dart';
import 'package:oto/oto/core/output_port.dart';
import 'package:oto/oto/data/command_data.dart';
import 'package:oto/oto/pipeline/pipeline.dart';
import 'package:oto/oto/pipeline/pipeline_condition.dart';
@ -29,8 +29,10 @@ abstract class PipelineExecutor {
PipelineValidateResult initialize(Map<String, dynamic> set);
Future execute();
static PipelineValidateResult validateTasks(List<dynamic>? list,
{ExecutionContext? context}) {
static PipelineValidateResult validateTasks(
List<dynamic>? list, {
ExecutionContext? context,
}) {
var result = PipelineValidateResult();
if (list != null) {
return Pipeline.pipelineInitialize(list, context: context);
@ -38,11 +40,20 @@ abstract class PipelineExecutor {
return result;
}
static Future printCondition(String syntax, String condition, String postfix,
ConditionCheckResult result) async {
await CLI.println('[Pipeline-$syntax]', color: Color.magentaStrong);
static Future printCondition(
String syntax,
String condition,
String postfix,
ConditionCheckResult result, {
ExecutionContext? context,
}) async {
await (context ?? Application.instance.context).output.line(
'[Pipeline-$syntax]',
style: RunnerOutputStyle.progress,
);
Application.log(
' - Condition: $condition\n - Values: ${result.conditionValue}\n - Result: ${result.result} ===> $postfix');
' - Condition: $condition\n - Values: ${result.conditionValue}\n - Result: ${result.result} ===> $postfix',
);
return simpleFuture;
}
@ -90,9 +101,9 @@ class PipelineExe extends PipelineExecutor {
Future<DataCommand> get commandData async {
var commandData = runtimeContext.dataCommandMap[commandID]!;
await Application.instance.printBuildStep(
'${printPrefix}Phase Start: ${commandData.name} ($commandID)',
Color.green);
await runtimeContext.output.buildStep(
'${printPrefix}Phase Start: ${commandData.name} ($commandID)',
);
return dataFutrue(commandData);
}

View file

@ -1,5 +1,4 @@
import 'package:dart_framework/utils/system_util.dart';
import 'package:oto/cli/cli.dart';
import 'package:oto/oto/application.dart';
import 'package:oto/oto/commands/command.dart';
import 'package:oto/oto/data/command_data.dart';
@ -89,16 +88,20 @@ class PipelineExeHandle extends PipelineExecutor {
handleData = ExeHandleData();
//set success task
var validateResult =
PipelineExecutor.validateTasks(data.on_success, context: context);
var validateResult = PipelineExecutor.validateTasks(
data.on_success,
context: context,
);
if (!validateResult.enable) {
return validateResult;
}
handleData.pipelineSuccess = validateResult.pipeline;
//set fail task
validateResult =
PipelineExecutor.validateTasks(data.on_fail, context: context);
validateResult = PipelineExecutor.validateTasks(
data.on_fail,
context: context,
);
if (!validateResult.enable) {
return validateResult;
}
@ -111,8 +114,9 @@ class PipelineExeHandle extends PipelineExecutor {
Future<DataCommand> get commandData async {
var commandID = data.id;
var commandData = runtimeContext.dataCommandMap[commandID]!;
await Application.instance.printBuildStep(
'Phase Start: ${commandData.name} ($commandID)', Color.green);
await runtimeContext.output.buildStep(
'Phase Start: ${commandData.name} ($commandID)',
);
return dataFutrue(commandData);
}

View file

@ -1,6 +1,6 @@
import 'package:dart_framework/utils/system_util.dart';
import 'package:oto/cli/cli.dart';
import 'package:oto/oto/application.dart';
import 'package:oto/oto/core/output_port.dart';
import 'package:oto/oto/core/tag_system.dart';
import 'package:oto/oto/data/pipeline_data.dart';
import 'package:oto/oto/pipeline/pipeline.dart';
@ -71,8 +71,10 @@ class PipelineForeach extends PipelineExecutor {
}
//set do task
var validateResult =
PipelineExecutor.validateTasks(data.on_do, context: context);
var validateResult = PipelineExecutor.validateTasks(
data.on_do,
context: context,
);
if (!validateResult.enable) {
return validateResult;
}
@ -83,37 +85,49 @@ class PipelineForeach extends PipelineExecutor {
@override
Future execute() async {
var iterator = TagSystem.replaceTagValue(data.iterator!, context: runtimeContext);
var iterator = TagSystem.replaceTagValue(
data.iterator!,
context: runtimeContext,
);
var iteratorType = '';
if(iterator != null)
{
if(iterator is List)
{
if (iterator != null) {
if (iterator is List) {
iteratorType = 'List';
var elementTag = data.setValue;
for(var item in iterator)
{
await TagSystem.setPropertyValue(elementTag, item, context: runtimeContext);
for (var item in iterator) {
await TagSystem.setPropertyValue(
elementTag,
item,
context: runtimeContext,
);
await _pipelineOnDo?.execute();
}
}
if(iterator is Map)
{
if (iterator is Map) {
iteratorType = 'Map';
var keyTag = data.setKey;
var elementTag = data.setValue;
for(var pair in iterator.entries)
{
if(keyTag != null)
{
await TagSystem.setPropertyValue(keyTag, pair.key, context: runtimeContext);
for (var pair in iterator.entries) {
if (keyTag != null) {
await TagSystem.setPropertyValue(
keyTag,
pair.key,
context: runtimeContext,
);
}
await TagSystem.setPropertyValue(elementTag, pair.value, context: runtimeContext);
await TagSystem.setPropertyValue(
elementTag,
pair.value,
context: runtimeContext,
);
await _pipelineOnDo?.execute();
}
}
await CLI.println('[Pipeline-Foreach ($iteratorType)]', color: Color.magentaStrong);
await runtimeContext.output.line(
'[Pipeline-Foreach ($iteratorType)]',
style: RunnerOutputStyle.progress,
);
Application.log(' - Foreach complete. Loop out');
}
return simpleFuture;

View file

@ -64,16 +64,20 @@ class PipelineIf extends PipelineCondition {
}
//set true task
var validateResult =
PipelineExecutor.validateTasks(data.on_true, context: context);
var validateResult = PipelineExecutor.validateTasks(
data.on_true,
context: context,
);
if (!validateResult.enable) {
return validateResult;
}
_pipelineTrue = validateResult.pipeline;
//set false task
validateResult =
PipelineExecutor.validateTasks(data.on_false, context: context);
validateResult = PipelineExecutor.validateTasks(
data.on_false,
context: context,
);
if (!validateResult.enable) {
return validateResult;
}
@ -87,7 +91,12 @@ class PipelineIf extends PipelineCondition {
var condition = data.condition!;
var result = checkCondition(condition);
await PipelineExecutor.printCondition(
'IF', condition, getPostfix(result.result), result);
'IF',
condition,
getPostfix(result.result),
result,
context: runtimeContext,
);
if (result.result) {
await _pipelineTrue?.execute();
} else {

View file

@ -70,7 +70,10 @@ class PipelineSwitch extends PipelineCondition {
}
for (var item in data.cases) {
var validateResult = PipelineExecutor.validateTasks(item.tasks, context: context);
var validateResult = PipelineExecutor.validateTasks(
item.tasks,
context: context,
);
if (!validateResult.enable) {
return validateResult;
}
@ -90,10 +93,18 @@ class PipelineSwitch extends PipelineCondition {
var result = checkCondition(addCondition);
if (result.result) {
executed = true;
var validateResult =
PipelineExecutor.validateTasks(item.tasks, context: context);
var validateResult = PipelineExecutor.validateTasks(
item.tasks,
context: context,
);
_pipeline = validateResult.pipeline;
await PipelineExecutor.printCondition('Switch', data.condition!, 'Start task', result);
await PipelineExecutor.printCondition(
'Switch',
data.condition!,
'Start task',
result,
context: runtimeContext,
);
await _pipeline?.execute();
break;
}
@ -103,7 +114,12 @@ class PipelineSwitch extends PipelineCondition {
result.conditionValue = 'null';
result.result = false;
await PipelineExecutor.printCondition(
'Switch', data.condition!, 'not exist Same condition', result);
'Switch',
data.condition!,
'not exist Same condition',
result,
context: runtimeContext,
);
}
return simpleFuture;
}

View file

@ -41,13 +41,23 @@ class PipelineWaitUntil extends PipelineCondition {
var condition = data.condition!;
var result = checkCondition(condition);
await PipelineExecutor.printCondition(
'Wait-Until', condition, 'Start wait.', result);
'Wait-Until',
condition,
'Start wait.',
result,
context: runtimeContext,
);
while (result.result) {
await Future.delayed(const Duration(milliseconds: 200));
result = checkCondition(data.condition!);
}
await PipelineExecutor.printCondition(
'Wait-Until', condition, 'Loop escape.', result);
'Wait-Until',
condition,
'Loop escape.',
result,
context: runtimeContext,
);
return simpleFuture;
}
}

View file

@ -52,8 +52,10 @@ class PipelineWhile extends PipelineCondition {
}
//set do task
var validateResult =
PipelineExecutor.validateTasks(data.on_do, context: context);
var validateResult = PipelineExecutor.validateTasks(
data.on_do,
context: context,
);
if (!validateResult.enable) {
return validateResult;
}
@ -68,12 +70,22 @@ class PipelineWhile extends PipelineCondition {
var result = checkCondition(condition);
while (result.result) {
await PipelineExecutor.printCondition(
'While', condition, 'Execute: on-do', result);
'While',
condition,
'Execute: on-do',
result,
context: runtimeContext,
);
await _pipelineOnDo?.execute();
result = checkCondition(data.condition!);
}
await PipelineExecutor.printCondition(
'While', condition, 'Loop escaped.', result);
'While',
condition,
'Loop escaped.',
result,
context: runtimeContext,
);
return simpleFuture;
}
}

View file

@ -59,7 +59,7 @@ pipeline:
test('pipeline validation rejects missing command id', () {
final result = Pipeline.pipelineInitialize([
{'exe': 'doesNotExist'}
{'exe': 'doesNotExist'},
]);
expect(result.enable, isFalse);
expect(result.message, contains('doesNotExist'));
@ -70,7 +70,9 @@ pipeline:
// and asserts that flow-control uses the explicit context exclusively.
ExecutionContext makeCtx(
Map<String, dynamic> props, List<String> commandIds) {
Map<String, dynamic> props,
List<String> commandIds,
) {
registerAllCommands();
final ctx = ExecutionContext();
ctx.property.addAll(props);
@ -84,98 +86,105 @@ pipeline:
return ctx;
}
test('PipelineIf evaluates condition from explicit context not singleton',
() async {
// Singleton: env = 'dev' on-false; explicit: env = 'prod' on-true
Application.instance.property['env'] = 'dev';
final ctx = makeCtx({'env': 'prod'}, ['onTrue', 'onFalse']);
test(
'PipelineIf evaluates condition from explicit context not singleton',
() async {
// Singleton: env = 'dev' on-false; explicit: env = 'prod' on-true
Application.instance.property['env'] = 'dev';
final ctx = makeCtx({'env': 'prod'}, ['onTrue', 'onFalse']);
final result = Pipeline.pipelineInitialize([
{
'if': {
'condition-string': '<!property.env> == prod',
'on-true': [
{'exe': 'onTrue'}
],
'on-false': [
{'exe': 'onFalse'}
],
}
}
], context: ctx);
final result = Pipeline.pipelineInitialize([
{
'if': {
'condition-string': '<!property.env> == prod',
'on-true': [
{'exe': 'onTrue'},
],
'on-false': [
{'exe': 'onFalse'},
],
},
},
], context: ctx);
expect(result.enable, isTrue, reason: result.message);
await result.pipeline!.execute();
expect(result.enable, isTrue, reason: result.message);
await result.pipeline!.execute();
expect(ctx.commandStates['onTrue'], CommandState.complete);
expect(ctx.commandStates['onFalse'], CommandState.ready);
});
expect(ctx.commandStates['onTrue'], CommandState.complete);
expect(ctx.commandStates['onFalse'], CommandState.ready);
},
);
test('PipelineForeach iterates list from explicit context not singleton',
() async {
// Singleton: items = [] (empty); explicit: items = [10, 20, 30]
Application.instance.property['items'] = <dynamic>[];
final ctx = makeCtx({
'items': [10, 20, 30]
}, [
'print'
]);
test(
'PipelineForeach iterates list from explicit context not singleton',
() async {
// Singleton: items = [] (empty); explicit: items = [10, 20, 30]
Application.instance.property['items'] = <dynamic>[];
final ctx = makeCtx(
{
'items': [10, 20, 30],
},
['print'],
);
final result = Pipeline.pipelineInitialize([
{
'foreach': {
'iterator': '<!property.items>',
'setValue': '<@property.current>',
'on-do': [
{'exe': 'print'}
],
}
}
], context: ctx);
final result = Pipeline.pipelineInitialize([
{
'foreach': {
'iterator': '<!property.items>',
'setValue': '<@property.current>',
'on-do': [
{'exe': 'print'},
],
},
},
], context: ctx);
expect(result.enable, isTrue, reason: result.message);
await result.pipeline!.execute();
expect(result.enable, isTrue, reason: result.message);
await result.pipeline!.execute();
// Loop ran 3 times using explicit context; last assigned value = 30
expect(ctx.property['current'], 30);
// Singleton must not have been modified
expect(Application.instance.property.containsKey('current'), isFalse);
});
// Loop ran 3 times using explicit context; last assigned value = 30
expect(ctx.property['current'], 30);
// Singleton must not have been modified
expect(Application.instance.property.containsKey('current'), isFalse);
},
);
test('PipelineContain evaluates target from explicit context not singleton',
() async {
// Singleton: target = 'xyz' (not in list); explicit: target = 'foo' (in list)
Application.instance.property['target'] = 'xyz';
final ctx = makeCtx({'target': 'foo'}, ['onTrue', 'onFalse']);
test(
'PipelineContain evaluates target from explicit context not singleton',
() async {
// Singleton: target = 'xyz' (not in list); explicit: target = 'foo' (in list)
Application.instance.property['target'] = 'xyz';
final ctx = makeCtx({'target': 'foo'}, ['onTrue', 'onFalse']);
final result = Pipeline.pipelineInitialize([
{
'contain': {
'target': '<!property.target>',
'list': ['foo', 'bar'],
'on-true': [
{'exe': 'onTrue'}
],
'on-false': [
{'exe': 'onFalse'}
],
}
}
], context: ctx);
final result = Pipeline.pipelineInitialize([
{
'contain': {
'target': '<!property.target>',
'list': ['foo', 'bar'],
'on-true': [
{'exe': 'onTrue'},
],
'on-false': [
{'exe': 'onFalse'},
],
},
},
], context: ctx);
expect(result.enable, isTrue, reason: result.message);
await result.pipeline!.execute();
expect(result.enable, isTrue, reason: result.message);
await result.pipeline!.execute();
expect(ctx.commandStates['onTrue'], CommandState.complete);
expect(ctx.commandStates['onFalse'], CommandState.ready);
});
expect(ctx.commandStates['onTrue'], CommandState.complete);
expect(ctx.commandStates['onFalse'], CommandState.ready);
},
);
// REVIEW_VALIDATE-2: malformed nested pipeline regression tests
test('pipeline validation rejects if with no condition field', () {
final result = Pipeline.pipelineInitialize([
{
'if': {'on-true': []}
}
'if': {'on-true': []},
},
]);
expect(result.enable, isFalse);
expect(result.message, isNotNull);
@ -189,10 +198,10 @@ pipeline:
'iterator': 123,
'setValue': '<@property.item>',
'on-do': [
{'exe': 'someCmd'}
{'exe': 'someCmd'},
],
}
}
},
},
]);
expect(result.enable, isFalse);
expect(result.message, isNotNull);
@ -203,11 +212,8 @@ pipeline:
test('pipeline validation rejects switch with non-list cases', () {
final result = Pipeline.pipelineInitialize([
{
'switch': {
'condition-string': 'test',
'cases': 'not-a-list',
}
}
'switch': {'condition-string': 'test', 'cases': 'not-a-list'},
},
]);
expect(result.enable, isFalse);
expect(result.message, isNotNull);
@ -221,10 +227,10 @@ pipeline:
'if': {
'condition-string': '<!property.env>',
'on-true': [
{'exe': 'hello'}
{'exe': 'hello'},
],
}
}
},
},
]);
expect(result.enable, isFalse);
expect(result.message, contains('supported operator'));
@ -237,10 +243,10 @@ pipeline:
'while': {
'condition-bool': '<!property.running>',
'on-do': [
{'exe': 'hello'}
{'exe': 'hello'},
],
}
}
},
},
]);
expect(result.enable, isFalse);
expect(result.message, contains('supported operator'));
@ -249,7 +255,7 @@ pipeline:
test('pipeline validation rejects wait-until condition without operator', () {
final result = Pipeline.pipelineInitialize([
{'wait-until-string': '<!state.buildStep>'}
{'wait-until-string': '<!state.buildStep>'},
]);
expect(result.enable, isFalse);
expect(result.message, contains('supported operator'));
@ -264,10 +270,10 @@ pipeline:
'id': 'hello',
'on-success': 'not-a-list',
'on-fail': [
{'exe': 'hello'}
{'exe': 'hello'},
],
}
}
},
},
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
@ -281,11 +287,11 @@ pipeline:
'exe-handle': {
'id': 'hello',
'on-success': [
{'exe': 'hello'}
{'exe': 'hello'},
],
'on-fail': 'not-a-list',
}
}
},
},
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
@ -296,11 +302,8 @@ pipeline:
test('pipeline validation rejects if with non-list on-true', () {
final result = Pipeline.pipelineInitialize([
{
'if': {
'condition-string': 'a == b',
'on-true': 'not-a-list',
}
}
'if': {'condition-string': 'a == b', 'on-true': 'not-a-list'},
},
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
@ -314,11 +317,11 @@ pipeline:
'if': {
'condition-string': 'a == b',
'on-true': [
{'exe': 'hello'}
{'exe': 'hello'},
],
'on-false': 'not-a-list',
}
}
},
},
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
@ -329,11 +332,8 @@ pipeline:
test('pipeline validation rejects while with non-list on-do', () {
final result = Pipeline.pipelineInitialize([
{
'while': {
'condition-bool': 'true',
'on-do': 'not-a-list',
}
}
'while': {'condition-bool': 'true', 'on-do': 'not-a-list'},
},
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
@ -348,8 +348,8 @@ pipeline:
'iterator': '<!property.items>',
'setValue': '<@property.item>',
'on-do': 'not-a-list',
}
}
},
},
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
@ -363,8 +363,8 @@ pipeline:
'switch': {
'condition-string': 'prod',
'cases': ['not-a-map'],
}
}
},
},
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
@ -378,10 +378,10 @@ pipeline:
'switch': {
'condition-string': 'prod',
'cases': [
{'value': 'prod'}
{'value': 'prod'},
],
}
}
},
},
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
@ -395,10 +395,10 @@ pipeline:
'switch': {
'condition-string': 'prod',
'cases': [
{'value': 'prod', 'tasks': 'not-a-list'}
{'value': 'prod', 'tasks': 'not-a-list'},
],
}
}
},
},
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
@ -409,7 +409,7 @@ pipeline:
// REVIEW_REVIEW_VALIDATE-2: contain node guard regression tests
test('pipeline validation rejects contain with non-map value', () {
final result = Pipeline.pipelineInitialize([
{'contain': 123}
{'contain': 123},
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
@ -422,10 +422,10 @@ pipeline:
'contain': {
'list': ['a', 'b'],
'on-true': [
{'exe': 'x'}
{'exe': 'x'},
],
}
}
},
},
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
@ -440,10 +440,10 @@ pipeline:
'target': 123,
'list': ['a', 'b'],
'on-true': [
{'exe': 'x'}
{'exe': 'x'},
],
}
}
},
},
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
@ -457,10 +457,10 @@ pipeline:
'contain': {
'target': 'foo',
'on-true': [
{'exe': 'x'}
{'exe': 'x'},
],
}
}
},
},
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
@ -469,67 +469,70 @@ pipeline:
});
test(
'PipelineSwitch validate uses explicit context command map not singleton',
() {
// Singleton dataCommandMap is empty; only explicit context has the command.
registerAllCommands();
final ctx = ExecutionContext();
final cmd = DataCommand()
..command = CommandType.Print
..id = 'deployProd'
..param = {'message': 'prod'};
ctx.dataCommandMap['deployProd'] = cmd;
'PipelineSwitch validate uses explicit context command map not singleton',
() {
// Singleton dataCommandMap is empty; only explicit context has the command.
registerAllCommands();
final ctx = ExecutionContext();
final cmd = DataCommand()
..command = CommandType.Print
..id = 'deployProd'
..param = {'message': 'prod'};
ctx.dataCommandMap['deployProd'] = cmd;
final result = Pipeline.pipelineInitialize([
{
'switch': {
'condition-string': 'prod',
'cases': [
{
'value': 'prod',
'tasks': [
{'exe': 'deployProd'}
],
}
],
}
}
], context: ctx);
final result = Pipeline.pipelineInitialize([
{
'switch': {
'condition-string': 'prod',
'cases': [
{
'value': 'prod',
'tasks': [
{'exe': 'deployProd'},
],
},
],
},
},
], context: ctx);
// Should succeed: deployProd exists in explicit context.
// Would fail if singleton (empty) context were used.
expect(result.enable, isTrue, reason: result.message);
});
// Should succeed: deployProd exists in explicit context.
// Would fail if singleton (empty) context were used.
expect(result.enable, isTrue, reason: result.message);
},
);
test('pipeline execution records started and completed step events',
() async {
final ctx = makeCtx({}, ['hello']);
final result = Pipeline.pipelineInitialize([
{'exe': 'hello'}
], context: ctx);
test(
'pipeline execution records started and completed step events',
() async {
final ctx = makeCtx({}, ['hello']);
final result = Pipeline.pipelineInitialize([
{'exe': 'hello'},
], context: ctx);
expect(result.enable, isTrue);
final pipeline = result.pipeline!;
await pipeline.execute();
expect(result.enable, isTrue);
final pipeline = result.pipeline!;
await pipeline.execute();
expect(ctx.stepEvents, hasLength(2));
final started = ctx.stepEvents[0];
final completed = ctx.stepEvents[1];
expect(ctx.stepEvents, hasLength(2));
final started = ctx.stepEvents[0];
final completed = ctx.stepEvents[1];
expect(started.event, 'started');
expect(started.stepId, 0);
expect(started.workflowIndex, 0);
expect(started.stepType, 'exe');
expect(started.commandId, 'hello');
expect(started.commandType, 'Print');
expect(started.event, 'started');
expect(started.stepId, 0);
expect(started.workflowIndex, 0);
expect(started.stepType, 'exe');
expect(started.commandId, 'hello');
expect(started.commandType, 'Print');
expect(completed.event, 'completed');
expect(completed.stepId, 0);
expect(completed.workflowIndex, 0);
expect(completed.stepType, 'exe');
expect(completed.commandId, 'hello');
expect(completed.commandType, 'Print');
});
expect(completed.event, 'completed');
expect(completed.stepId, 0);
expect(completed.workflowIndex, 0);
expect(completed.stepType, 'exe');
expect(completed.commandId, 'hello');
expect(completed.commandType, 'Print');
},
);
test('pipeline execution records failed step event before rethrow', () async {
final ctx = ExecutionContext();
@ -541,8 +544,13 @@ pipeline:
await expectLater(
pipeline.execute(),
throwsA(isA<Exception>().having(
(e) => e.toString(), 'message', contains('Test error message'))),
throwsA(
isA<Exception>().having(
(e) => e.toString(),
'message',
contains('Test error message'),
),
),
);
expect(ctx.stepEvents, hasLength(2));
@ -564,16 +572,70 @@ pipeline:
expect(ctx.output, isA<NoopRunnerOutputPort>());
await expectLater(ctx.output.line('hello'), completes);
await expectLater(
ctx.output.block('block', style: RunnerOutputStyle.success), completes);
ctx.output.block('block', style: RunnerOutputStyle.success),
completes,
);
await expectLater(
ctx.output.buildStep('step', style: RunnerOutputStyle.accent),
completes);
ctx.output.buildStep('step', style: RunnerOutputStyle.accent),
completes,
);
final recording = _RecordingOutputPort();
ctx.output = recording;
await ctx.output.line('msg', style: RunnerOutputStyle.warning);
expect(recording.lines, contains('msg'));
});
test(
'pipeline emits progress through explicit context output port',
() async {
final ctx = makeCtx({}, ['hello']);
final recording = _RecordingOutputPort();
ctx.output = recording;
final result = Pipeline.pipelineInitialize([
{'exe': 'hello'},
], context: ctx);
expect(result.enable, isTrue, reason: result.message);
await result.pipeline!.execute();
expect(recording.steps, anyElement(contains('hello')));
expect(Application.instance.context.output, isA<NoopRunnerOutputPort>());
},
);
test(
'PipelineForeach emits completion through explicit context output port',
() async {
final ctx = makeCtx(
{
'items': [1, 2],
},
['print'],
);
final recording = _RecordingOutputPort();
ctx.output = recording;
final result = Pipeline.pipelineInitialize([
{
'foreach': {
'iterator': '<!property.items>',
'setValue': '<@property.current>',
'on-do': [
{'exe': 'print'},
],
},
},
], context: ctx);
expect(result.enable, isTrue, reason: result.message);
await result.pipeline!.execute();
expect(recording.lines, anyElement(contains('Foreach')));
expect(Application.instance.context.output, isA<NoopRunnerOutputPort>());
},
);
}
class ThrowingExecutor extends PipelineExecutor {
@ -591,20 +653,26 @@ class _RecordingOutputPort implements RunnerOutputPort {
final List<String> steps = [];
@override
Future<void> line(String message,
{RunnerOutputStyle style = RunnerOutputStyle.normal}) async {
Future<void> line(
String message, {
RunnerOutputStyle style = RunnerOutputStyle.normal,
}) async {
lines.add(message);
}
@override
Future<void> block(String message,
{RunnerOutputStyle style = RunnerOutputStyle.normal}) async {
Future<void> block(
String message, {
RunnerOutputStyle style = RunnerOutputStyle.normal,
}) async {
blocks.add(message);
}
@override
Future<void> buildStep(String name,
{RunnerOutputStyle style = RunnerOutputStyle.success}) async {
Future<void> buildStep(
String name, {
RunnerOutputStyle style = RunnerOutputStyle.success,
}) async {
steps.add(name);
}
}