示例代码场景规划(Tool / ToolGroup · v1.9)
规格 ID:TC019 — 稳定标识(页面改名/迁移不变),全站对照见设计规格索引。
实现边界:本仓可编译示例只有 examples 的
headless_run(examples/headless_run.cpp+examples/headless_camera.{hpp,cpp};贴近 S07 的无 UI 入口,不是 S07 全文)。测试侧有两个 ctest 可执行:smoke_headers(分发器tests/smoke_headers.cpp,各组在tests/smoke_*.cpp,共享tests/smoke_common.hpp)与orchestration_contracts(分发器tests/orchestration_contracts.cpp,各节在tests/contracts_*.cpp,共享tests/contracts_common.hpp);另有 C# 侧bindings/csharp/Vyntek.Controls.Tests。它们覆盖下表「本仓对应代码」列的骨架,不构成勾选——勾选仍空。 关联:toolgroup · test-contracts · host-integration · image-display
示例代码场景规划(Tool / ToolGroup · v1.9)
目标设计与示意骨架,非可编译工程。总体边界见设计概述;编排权威见 toolgroup(v1.9);成像/显示契约片段见 示例指南。
约束:Tool / ToolGroup;命名空间
vyntek::toolgroup/vyntek::imaging;ToolId;DiagnosticCode。编排与成像核心 = C++;ImageDisplay 仅 C#。API 名与 toolgroup v1.9 / cpp-headers / csharp-binding 一致。fake 替身见 toolgroup §7.6;句柄交接见 §3.4;Diagnostic 见 §7.4.6;.avtg见 §6.6。
本文是成体系场景目录:每个场景给出目的、前置、步骤骨架与可测断言,供实现期单测 / headless 验收对照。详细契约片段仍落在 示例指南 与 toolgroup §9,避免把契约文撑爆。
关联:示例指南 · toolgroup §9 / §10.1 · test-contracts(R4 可勾选契约;R6 含 S09;TC-OUT-* / S10;TC-ACQ-* / TC-SESS-* / S11;TC-MEM-* / S12;TC-DISP-* / S13) · host-integration(R7 · S07 同核;两端都停;TC-CTL-05 交叉) · cpp-headers · sketch 页(R5 伪树·示意实现对照,非工程) · csharp-binding · glossary(术语终检) · output-inspection(T10) · camera-acquisition · session-lifecycle · memory-management · image-display(仅 C#) · validation-and-roadmap
场景目录
| ID | 标题 | 主路径 | 文档回链 |
|---|---|---|---|
| S01 | 单工具 Run | 注册 → Create/AddTool → RunSession.Run → Outputs | toolgroup §7.4 / §9.4;cpp-headers |
| S02 | 多工具 ToolGroup 顺序链 | 列表序 Link + Validate + 自上而下 Run | toolgroup §4.1 / §4.2 / §5 / §9.1 |
| S03 | 采集→对齐→检测→输出观察(fake) | fake_acq → fake_pm_align → passthrough/constant;观察 LastRun | toolgroup §7.6.3 / §9.1;output-inspection |
| S04 | .avtg Load + Validate 失败 | UnknownTypeId / MissingRequiredProperty | toolgroup §6.5–6.6 / §7.4.5–7.4.6 |
| S05 | 句柄交接与交显示 | Complete→ReadOnly、链接赋值、H1–H10;显示段 仅 C# | toolgroup §3.4;test-contracts §6;examples;csharp-binding;image-display |
| S06 | Cancel / abort / Busy | 协作取消、abort_on_tool_failure、同 session 重入拒绝 | toolgroup §4.3 / §7.4.3 / §7.4.6;session-lifecycle |
| S07 | headless 批跑 vs 带显示宿主 | 无 UI 循环 Run vs 绑定层 + ImageDisplay | toolgroup §7.2–7.3;host-integration(R7);csharp-binding;examples T12 |
| S08 | Tool 作者:fake 注册 + 最小自定义 Tool | fake::register_fake_tools + ITool 最小契约 | toolgroup §7.6;cpp-headers |
| S09a | 嵌套 ToolGroup 合法 + Encapsulation | 内层终端对外;私有属性 → Encapsulation | toolgroup §9.3 / §7.4.6 / §10.1 |
| S09b | SourceDisabled + InvalidBinding | 源禁用拉值失败;端点/方向非法主断言 | toolgroup §4.3 / §7.4.6 / §10.1 |
| S09c | IoFailure + 停相机 ≠ 自动 Cancel | 截断/不可读;停止矩阵独立步骤 | toolgroup §7.4.3 / §7.4.6;session-lifecycle |
| S09d | §9.2 两组终端串联(可选) | Locate→Defect 终端链;极薄正路径 | toolgroup §9.2 / §4.1 |
| S10 | 输出观察 T10 事件闭集(fake) | SnapshotReplaced / EmptySnapshot / Unavailable / Evicted;失败保留;Subscribe+replay;查看者 vs Evict;KeepLatest vs ReleaseAfterRouting;观察永不驱动 Run | output-inspection(T10);test-contracts TC-OUT-*;TC-RUN-05 |
| S11 | 采集接入 + 会话停流(fake · 阶段 D) | ImportNative vs 回调 Copy;TryPostOwned;满池/入口预算;归还泵与 ReturnCompleted;Latest 预览;回调不堵归还;停流结束条件;外保留≠假归还;超时续清;ReturnFailed/断线可观察;两端都停须显式 Cancel+停采集 | camera-acquisition;session-lifecycle;test-contracts TC-ACQ-* / TC-SESS-*;TC-CTL-04 |
| S12 | 存储 / pin / 归还(fake · 阶段 B) | Rent/TakeOwnership/ImportNative 工厂接管;最后引用→Returning;禁等自身 ReturnCompleted;pin 阻写/归还;写 pin 阻 Complete;授予/取消与 Share/Dispose 原子;未 Complete→Invalid;首次根范围/ROI 中止整实体;Invalid 恢复;releaseAsync 一次与归还隔离/ReturnFailed | memory-management;test-contracts TC-MEM-*;image-model(根范围薄指) |
| S13 | 显示集成(仅 C# · 阶段 F) | 登记≠授予;Share 不接管;四回执;失败保留;四格式;ROI;卸载;预算;显示只观察 | image-display;TC-DISP-*;≠ T15 R10 |
场景 ↔ 本仓代码对照
每个场景给出本仓已有的对应代码与缺口。骨架代码仍是规格示意;下表才是「与实际代码对得上」的入口。均不构成勾选(test-contracts 勾选全空)。
| 场景 | 本仓对应代码 | 缺口 |
|---|---|---|
| S01 单工具 Run | tests/contracts_run.cpp(TC-RUN-01/03);入口 tests/orchestration_contracts.cpp | 无独立示例工程;fake_constant 在 vyntek/toolgroup/fake_tools.hpp |
| S02 顺序链 | tests/contracts_link.cpp(TC-LNK-01…08)、tests/contracts_run.cpp(TC-RUN-02/03) | 无 .avtg 配方样例 |
| S03 fake 流水线观察 | tests/contracts_run.cpp(TC-RUN-03/05)用 fake_acq / fake_pm_align / fake_passthrough | 观察订阅不挂 RunSession(见 S10 缺口) |
| S04 Load / Validate 失败 | tests/contracts_serialize.cpp(TC-SER-02/03/04)、tests/contracts_link.cpp(TC-LNK-07) | 无落盘 fixtures/*.avtg;夹具在测试内内联构建 |
| S05 句柄交接 | tests/contracts_handoff.cpp(TC-MEM-11 / TC-RUN-02)、tests/smoke_memory_*.cpp(TC-MEM-*) | 显示段仅 C# 骨架(bindings/csharp),无真实渲染 |
| S06 Cancel / abort / Busy | tests/contracts_control.cpp(TC-CTL-01…05 / TC-RUN-04) | fake 清单无内置 Busy 替身(重入探针在测试内定义);无示例工程 |
| S07 headless 批跑 vs 显示宿主 | examples/headless_run.cpp + examples/headless_camera.{hpp,cpp}(可编译可运行;退出码表见示例指南) | 只跑单次 Run、无 .avtg 批跑;显示宿主无工程 |
| S08 Tool 作者 | vyntek/toolgroup/fake_tools.hpp(fake::* 八个替身 + fake::register_fake_tools)、ToolRegistry::RegisterBuiltinTools(src/toolgroup/registry.cpp,注册 passthrough) | 无「最小自定义 Tool」教程工程 |
| S09a 嵌套封装 | tests/contracts_link.cpp(TC-LNK-04)、include/vyntek/toolgroup/nested_group_tool.hpp | — |
| S09b SourceDisabled / InvalidBinding | tests/contracts_link.cpp(TC-LNK-05/06)、tests/contracts_control.cpp | — |
| S09c IoFailure / 停相机≠自动 Cancel | tests/contracts_serialize.cpp(TC-SER-07)、tests/smoke_acquisition_stop.cpp(TC-SESS-05)、examples/headless_camera.cpp(退出码 7/8 即该断言) | — |
| S09d 两组终端串联 | tests/contracts_run.cpp(TC-RUN-07) | 无 Locate→Defect 双组样例 |
| S10 输出观察 | tests/smoke_output_inspector.cpp(TC-OUT-01…08)、tests/smoke_observation_gate.cpp(TC-OUT-09 / TC-RUN-05) | 观察面是独立 ImageOutputInspector(toolgroup/output_inspector.hpp);未挂在 RunSession 上(本文 session.SubscribeOutputs 等仍为规格 API) |
| S11 采集接入 + 停流 | tests/smoke_acquisition_stop.cpp(TC-ACQ-01/02/07 + TC-SESS-01…05)、tests/smoke_memory_handoff.cpp(TC-ACQ-03…06)、include/vyntek/imaging/fake_camera_adapter.hpp | 供应商 SDK 归还泵压力未验;examples/headless_camera.cpp 只演示显式停流 |
| S12 存储 / pin / 归还 | tests/smoke_memory_pool.cpp / smoke_memory_ownership.cpp / smoke_memory_grants.cpp / smoke_memory_handoff.cpp(TC-MEM-01…13) | 真实池压力未验 |
| S13 显示集成 | bindings/csharp/Vyntek.Controls.Tests/(dotnet test,骨架) | 无真实渲染 / GPU 预算 / context 重建;TC-DISP 未勾 |
S01 · 单工具 Run
目的
验证最小闭环:Registry 创建单一 Tool → 放入 ToolGroup → RunSession::Run → 读 Outputs / 工具属性;断言成功路径与 ToolId 可定位。
前置
ToolRegistry已fake::register_fake_tools(registry)(或至少注册fake_constant)。- 无
.avtg;内存建组即可。 - 无图像句柄要求(标量输出即可)。
步骤骨架(C++)
// 与本仓 tests/contracts_run.cpp(TC-RUN-01)同一形状;此处只列公开头
#include <vyntek/toolgroup/toolgroup.hpp>
#include <vyntek/toolgroup/registry.hpp>
#include <vyntek/toolgroup/fake_tools.hpp>
#include <vyntek/toolgroup/run_session.hpp>
using namespace vyntek::toolgroup;
ToolRegistry registry;
fake::register_fake_tools(registry); // 测试替身;生产配方勿链(TC-REG-06)
auto group = ToolGroup::Create("S01_Single");
Tool* c = group->AddTool(registry.Create("fake_constant", "Const")); // AddTool 接管所有权,返回非拥有观察指针
// fake_constant 必填配置属性 Value(Int32);端口 Output(Int32) / FlagOut(Bool)
c->SetProperty("Value", Value::Int32(42));
group->AddOutput("Out", TypeDesc{Type::Int32, {}});
group->LinkToOutput(c, "Output", "Out"); // 工具端口 → 流 Outputs 终端
DiagnosticList diags;
if (!group->Validate(diags)) { /* 期望无 Severity::Error */ return; }
RunSession session(group); // 构造吃 std::shared_ptr<ToolGroup>
RunResult r = session.Run();
// 断言见下
int out = 0;
bool got = session.GetOutput("Out", out); // GetOutput(name, T&) → bool(终端缺失/未设置/类型不符 → false)
期望断言
| 项 | 期望 |
|---|---|
Validate | 无 Severity::Error |
r.ok() | true(注意:是方法不是字段;工具仅报 NotImplemented 时 Run 仍可 Succeeded) |
| Outputs | got == true 且 out == 42;或 session.GetOutputValue("Out").As<Int32>() |
ToolId | c->id() 非空且组内稳定;失败时可写入 Diagnostic.tool_id |
| 诊断 | 成功路径 diagnostics 可空或仅 Info |
| 契约映射 | TC-RUN-01 / TC-REG-04 / TC-LNK-08 |
对应文档章节
toolgroup §3.1 / §7.4.1 / §7.4.3 / §7.4.4 / §9.4;cpp-headers(公开头);§7.6.3(fake_constant)
S02 · 多工具 ToolGroup 顺序链
目的
验证列表序权威调度:多工具 AddTool → Link(源须在目标之上)→ Validate → 一次 Run 自上而下执行;链接不是边就绪调度。
前置
fake::register_fake_tools:fake_acq、fake_passthrough、fake_constant(或等价)。- 可选:故意建「源在目标之下」的坏链,另测
OrderViolation(本场景主路径为合法链)。
步骤骨架(C++)
auto group = ToolGroup::Create("S02_Chain");
auto* acq = group->AddTool(registry.Create("fake_acq", "Acq"));
auto* pass = group->AddTool(registry.Create("fake_passthrough", "Pass"));
auto* flag = group->AddTool(registry.Create("fake_constant", "Flag"));
group->AddOutput("Image", TypeDesc{Type::ImageGray8});
group->AddOutput("Flag", TypeDesc{Type::Int32});
group->Link(acq, "OutputImage", pass, "InputImage");
group->LinkToOutput(pass, "OutputImage", "Image");
group->LinkToOutput(flag, "Output", "Flag");
// 坏链对照(勿留在成功配方):group->Link(pass, "OutputImage", acq, "InputImage");
// → Validate 期望 DiagnosticCode::OrderViolation
std::vector<Diagnostic> diags;
assert(group->Validate(diags));
RunSession session(group); // 构造吃 std::shared_ptr<ToolGroup>
RunResult r = session.Run();
期望断言
| 项 | 期望 |
|---|---|
| 执行序 | Acq → Pass → Flag(按 tools[]);无「边就绪即跑」 |
r.ok() | true |
| H6 | Outputs.Image 为 Access=ReadOnly;对输入 OpenWrite 立即拒绝 |
| 坏链 | Validate 失败且含 OrderViolation;不留下半条边 |
TypeMismatch | 图像链到 Int32 终端 → Error |
| 禁用源 | 源 disabled 时下游 Run 拉值 → DiagnosticCode::SourceDisabled(MVP 严格;主场景见 S09b) |
| 契约映射 | 至少覆盖 test-contracts TC-RUN-02 / TC-LNK-01 / TC-LNK-03 |
对应文档章节
toolgroup §4.1 / §4.2 / §5.4 / §7.4.2 / §9.1;§3.4 H6
S03 · 采集→对齐→检测→输出观察(fake)
目的
用 MVP fake 替身复现 §9.1 流水线心智(无需真实相机/PMAlign):fake_acq → fake_pm_align → passthrough/constant;Run 后只读观察 Outputs / LastRun(观察不驱动 Run)。
前置
fake::register_fake_tools:fake_acq、fake_pm_align、fake_passthrough或fake_constant。- 无真实 SDK;可选输出观察订阅(见 output-inspection,T10)。
步骤骨架(C++)
auto group = ToolGroup::Create("S03_InspectFake");
auto* acq = group->AddTool(registry.Create("fake_acq", "Acq")); // 端口 OutputImage (ImageGray8)
auto* align = group->AddTool(registry.Create("fake_pm_align", "Align")); // InputImage / OutputImage / Pose (Pose2d)
auto* pass = group->AddTool(registry.Create("fake_passthrough", "OutImg"));
auto* cnt = group->AddTool(registry.Create("fake_constant", "Count")); // 必填 Value
cnt->SetProperty("Value", Value::Int32(0));
group->AddOutput("PoseX", TypeDesc{Type::Float64, {}}); // 示意:从 Pose 最小字段拆出
group->AddOutput("DefectCount", TypeDesc{Type::Int32, {}});
group->AddOutput("Preview", TypeDesc{Type::ImageGray8, {}});
group->Link(acq, "OutputImage", align, "InputImage");
group->Link(align, "OutputImage", pass, "InputImage"); // passthrough 只 ShareImage,不拷像素
group->LinkToOutput(pass, "OutputImage", "Preview");
group->LinkToOutput(cnt, "Output", "DefectCount");
// Pose → 终端:按 §5.1 端点路径(示意)
DiagnosticList diags;
assert(group->Validate(diags));
RunSession session(group); // 构造吃 std::shared_ptr<ToolGroup>
RunResult r = session.Run();
// 输出观察:只读 LastRun / Outputs;观察回调不得反过来触发 session.Run(TC-OUT-09)
const Value pv = session.GetOutputValue("Preview");
const vyntek::imaging::PixelImageHandle* preview = pv.image(); // Value 直接持句柄;空/类型不符 → nullptr
// 可选:ImageOutputInspector 订阅 EmptySnapshot / Unavailable(T10)— 见 output-inspection / S10
期望断言
| 项 | 期望 |
|---|---|
r.ok() | true |
| H3/H4/H6 | preview != nullptr 且 preview->access() == BufferAccess::ReadOnly;同 BufferId 语义(passthrough Share) |
| Pose / Count | Outputs 可读;类型符合 TypeDesc |
| 观察 | 订阅变更不触发第二次 Run;失败保留策略见 output-inspection |
| 无设备 | 全程无相机 SDK 调用 |
| 契约映射 | TC-RUN-03 / TC-H6 / TC-REG-01 / TC-RUN-05(薄);细项 S10 / TC-OUT-* |
对应文档章节
toolgroup §7.6.3 / §9.1 / §3.4;output-inspection(T10);examples(成像读写片段)
S04 · .avtg Load + Validate 失败
目的
钉住持久化失败分轨:未知 type_id → Load UnknownTypeId(严格失败,无占位成功);已知 type 缺必填配置属性 → Validate MissingRequiredProperty(禁止静默默认糊弄)。对照 §6.5 / §6.6 反样例。
前置
- 合法正样例
.avtg可作对照(toolgroup §6.6.2)。 - Registry 仅含 fake/builtin;不含
type_id_not_registered。 - 另备一份「已知
fake_constant但缺必填Value」的配方或 Load 后删属性。
步骤骨架(C++)
#include <vyntek/toolgroup/serialize.hpp>
// 与本仓 tests/contracts_serialize.cpp(TC-SER-02/03/04)同一形状。
// ToolGroup::Load 与 LoadAvtg 都返回 LoadResult{ shared_ptr<ToolGroup> group; DiagnosticList diagnostics; }
// --- A) 未知 type_id ---
LoadResult bad = LoadAvtg("fixtures/unknown_type.avtg", registry);
// fixtures 内 tools[].type_id = "type_id_not_registered"
assert(bad.group == nullptr); // 严格失败,无占位成功
assert(HasCode(bad.diagnostics, DiagnosticCode::UnknownTypeId)); // Severity::Error
// --- B) 已知 type、缺必填属性 ---
LoadResult loaded = LoadAvtg("fixtures/missing_required.avtg", registry);
assert(loaded.group != nullptr); // Load 成功(type 已知;component_version 不拦,T8)
DiagnosticList diags;
bool ok = loaded.group->Validate(diags);
assert(!ok);
assert(std::any_of(diags.begin(), diags.end(), [](const Diagnostic& d) {
return d.code == DiagnosticCode::MissingRequiredProperty
&& d.severity == Severity::Error;
}));
// 不得出现「静默填默认后 Validate 成功」
反样例要点(文档级,与 §6.6.4 对齐):
| 失败类 | 触发 | 码 |
|---|---|---|
| 未知工具 | tools[].type_id 不在 Registry | UnknownTypeId(Load) |
| 缺必填 | 已知 type,缺配置属性 | MissingRequiredProperty(Validate) |
| schema 过高 | schema_version > 加载器 | SchemaTooNew(Load;本场景可作附带用例) |
期望断言
| 项 | 期望 |
|---|---|
| A | Load 失败;≥1 条 UnknownTypeId Error;无半初始化成功流 |
| B | Load 成功;Validate 失败;MissingRequiredProperty;与 UnknownTypeId 分轨 |
| 附带 | 过高 schema → SchemaTooNew;component_version 差异不单独拦 Load |
| 契约映射 | TC-SER-02 / TC-SER-03 / TC-LNK-07 / TC-SER-04(见 test-contracts) |
对应文档章节
toolgroup §6.5 / §6.6 / §7.4.5 / §7.4.6 / §10.1(Load 复载);validation-and-roadmap(T8)
S05 · 句柄交接与交显示
目的
覆盖 §3.4 主路径:工具内 Complete / CompleteAndOpenRead + ShareImage → 链接拉值写入下游(H6)→ headless 持 ReadOnly 句柄;上屏仅经绑定层 + C# ImageDisplay(禁止 C++ UI)。
前置
- fake:
fake_acq+fake_clone_write(或最小自定义写工具)+fake_passthrough。 - 显示宿主:PInvoke 绑定可用;C# 侧有 ImageDisplay 实例(见 csharp-binding / image-display)。
步骤骨架(C++ · 编排与成像)
#include <vyntek/imaging/handle.hpp>
#include <vyntek/imaging/factory.hpp>
#include <vyntek/toolgroup/toolgroup.hpp>
#include <vyntek/toolgroup/run_session.hpp>
// 流:Acq → CloneThenWrite → Passthrough → Output 终端
auto group = ToolGroup::Create("S05_Handle");
auto* acq = group->AddTool(registry.Create("fake_acq", "Acq"));
auto* wr = group->AddTool(registry.Create("fake_clone_write", "Write"));
auto* pass = group->AddTool(registry.Create("fake_passthrough", "Share"));
group->AddOutput("DisplayImage", TypeDesc{Type::ImageGray8});
group->Link(acq, "OutputImage", wr, "InputImage");
group->Link(wr, "OutputImage", pass, "InputImage");
group->LinkToOutput(pass, "OutputImage", "DisplayImage");
RunSession session(group); // 构造吃 std::shared_ptr<ToolGroup>
RunResult r = session.Run();
const vyntek::imaging::PixelImageHandle* handle = session.GetOutputValue("DisplayImage").image();
assert(handle != nullptr);
assert(handle->access() == vyntek::imaging::BufferAccess::ReadOnly); // H6:链给下游的是只读句柄
// headless:到此为止;句柄经绑定交给 UI 进程/线程(示意)
步骤骨架(C# · 仅显示段)
// 示意、非可编译;仅 ImageDisplay;像素来自 C++ 句柄经 PInvoke 绑定
// 对齐 csharp-binding 与本站 image-display 规格页;控件 API 见 bindings/csharp/Vyntek.Controls/ImageDisplay.cs
using var bound = ImagingBindings.AdoptReadOnly(nativeHandle); // opaque + Dispose
var update = imageDisplay.SetImage(bound); // SetImage(IPixelImage?, InputRetention?) → ImageDisplayUpdate
// 取样 / Depth 选项等见本站 image-display 页;不得在此启动 ToolGroup.Run / Cancel 相机期望断言
| 项 | 期望 |
|---|---|
| H1/H2 | Complete 后再写失败;CompleteAndOpenRead 得 ReadOnly;未 Complete 不得链给下游 |
| H3/H4 | ShareImage / ReadOnly Share → OpenWrite 拒绝;多句柄独立关闭 |
| H6 | 链接赋值后目标输入 ReadOnly;列表序不变 |
| H7/H8 | 已交下游不可改;Clone 新 BufferId 可写后再发布 |
| 显示 | C# SetImage 成功;控件不承担编排 Cancel / 停相机(停止矩阵 §7.4.3) |
| 语言 | 无 C++ UI / Avalonia include |
| H9 | ReadOnly ROI OpenWrite 拒绝;不得把根退化为可写 |
| H10 | 失败/未 Complete 输出不可假成功交给下游(与 S06 abort 联测) |
| 契约映射 | test-contracts TC-H1…TC-H9(主路径);显示段 TC-CTL-05 |
对应文档章节
toolgroup §3.4(H1–H10)/ §7.4.3;test-contracts §6;示例指南(T12);csharp-binding;image-display;memory-management / image-model
S06 · Cancel / abort / Busy
目的
验证执行控制三件套:(1)协作式 RequestCancel → Cancelled,后续工具不启动;(2)abort_on_tool_failure=true + 失败工具 → Aborted,后续不 Run(H10);(3)同 RunSession 重入 → Busy(MVP 拒绝,不排队)。
前置
- fake:
fake_cancel_check(可set_spin拉长协作窗口、set_cancel_hook在 Run 内触发取消)、fake_fail_once/fake_fail_always。fake 清单见vyntek/toolgroup/fake_tools.hpp(八个替身;无fake_busy_probe,Busy 路径用嵌套 Re-entrancy 或第二线程构造)。 abort_on_tool_failure默认 true(ToolGroup头注;TC-CTL-02断言此默认)。
步骤骨架(C++)
// --- Cancel:RunStatus::Cancelled(对应 tests/contracts_control.cpp TC-CTL-01)---
auto group_c = ToolGroup::Create("S06_Cancel");
auto* chk = group_c->AddTool(registry.Create("fake_cancel_check", "chk"));
auto* cst = group_c->AddTool(registry.Create("fake_constant", "cst"));
cst->SetProperty("Value", Value::Int32(1));
group_c->AddOutput("Out", TypeDesc{Type::Int32, {}});
group_c->LinkToOutput(cst, "Output", "Out");
RunSession s1(group_c);
// 取消在 Run 在途到达:单测用 fake_cancel_check::set_cancel_hook 在 Run 内触发;
// 真宿主则在另一线程调 s1.RequestCancel()(协作式,T9)
auto* cancel_check = dynamic_cast<fake::FakeCancelCheck*>(chk);
cancel_check->set_cancel_hook([&s1] { s1.RequestCancel(); });
RunResult rc = s1.Run();
// 期望:rc.status == RunStatus::Cancelled;diagnostics 含 DiagnosticCode::Cancelled;
// cancel_check->saw_cancel();后续 cst 未 Run(GetOutputValue("Out").empty())
// --- abort_on_tool_failure:RunStatus::Failed + DiagnosticCode::Aborted(TC-CTL-02)---
auto group_a = ToolGroup::Create("S06_Abort");
auto* bad = group_a->AddTool(registry.Create("fake_fail_always", "bad"));
auto* cst2 = group_a->AddTool(registry.Create("fake_constant", "cst"));
cst2->SetProperty("Value", Value::Int32(3));
group_a->AddOutput("Out", TypeDesc{Type::Int32, {}});
group_a->LinkToOutput(cst2, "Output", "Out");
// abort_on_tool_failure() 默认 true(§4.3;TC-CTL-02 断言此默认)
RunSession s_a(group_a);
RunResult ra = s_a.Run();
// 期望:ra.status == RunStatus::Failed(不是独立的 Aborted 状态);diagnostics 含 Aborted;
// ra.failed_tool_id == "bad";cst 未 Run(H10:不假成功交给下游)
// --- Busy:RunStatus::Busy(TC-CTL-03:同 session 重入拒绝,不排队)---
auto group_b = ToolGroup::Create("S06_Busy");
auto* probe = group_b->AddTool(std::make_unique<ReentrantProbeTool>("probe"));
// 单测用自定义探针在工具 Run 内部再调 session.Run();真宿主则是第二线程同时 Run
RunSession s2(group_b);
// 线程1:auto r1 = s2.Run(); → r1.ok(),外层 Run 不受损
// 线程2:RunResult rb = s2.Run(); → rb.status == RunStatus::Busy
期望断言
| 项 | 期望 |
|---|---|
| Cancel | RunStatus::Cancelled;当前工具合作返回后不再启动后续;rc.ok() == false |
| Abort | RunStatus::Failed + DiagnosticCode::Aborted;failed_tool_id 指向失败工具;后续 LastRun 未执行;H10 |
| Busy | 同 session 第二次 Run → Busy Error;首次 Run 不受损 |
| 停止矩阵 | Cancel ≠ 停相机;停相机 ≠ 自动 Cancel(§7.4.3;独立步骤见 S09c) |
| 契约映射 | TC-CTL-01 / TC-CTL-02 / TC-CTL-03 / TC-H10 |
对应文档章节
toolgroup §4.3 / §7.4.3 / §7.4.6 / §3.4 H10 / §10.1;session-lifecycle;validation-and-roadmap(T9)
S07 · headless 批跑 vs 带显示宿主
目的
对照两种宿主形态:纯编排(循环 Run、断言 Outputs、无 UI)与 显示宿主(同 C++ 核心 + PInvoke + 仅 C# ImageDisplay)。证明不存在第二执行引擎,也不写 C++ UI。
前置
- 同一
.avtg或内存流(建议 S03 fake 流水线)。 - headless:测试进程 / CLI runner。
- 显示:C# 应用引用绑定包。
步骤骨架(C++ · headless 批跑)
本仓已有可运行样例:examples/headless_run.cpp + examples/headless_camera.{hpp,cpp}(退出码表见示例指南)。下面是「.avtg 批跑」的目标形态:
LoadResult loaded = ToolGroup::Load("recipes/inspect_fake.avtg", registry);
assert(loaded.group != nullptr); // 未知 type / SchemaTooNew 等见 S04
RunSession session(loaded.group); // 构造吃 shared_ptr;session 不独占 group 所有权(§7.4.5)
for (int i = 0; i < N; ++i) {
// 可选:注入帧 / 依赖 fake_acq 自产
RunResult r = session.Run();
assert(r.ok());
int n = 0;
session.GetOutput("DefectCount", n); // 终端缺失/未设置/类型不符 → false
// 记录 n;无 SetImage / 无窗口
}步骤骨架(C# · 显示宿主,编排仍可经绑定调 C++)
// 示意薄 PInvoke 入口(对齐 csharp-binding);编排仍在 C++,非第二套 ToolGroup
ToolGroupNative.RunOnce(sessionHandle); // opaque session handle;不在托管侧重写执行器
using var img = ToolGroupNative.GetOutputImage(sessionHandle, "Preview");
imageDisplay.SetImage(img);
// Cancel / 停相机按停止矩阵分派到编排/采集 API,不进 ImageDisplay
期望断言
| 项 | 期望 |
|---|---|
| headless | N 次 Run 稳定;无 UI 依赖;Diagnostic 可收集到日志;本仓已可运行的是 examples/headless_run(单次 Run,无 .avtg 批跑) |
| 显示宿主 | 同一配方结果一致;SetImage 只读快照;控件卸载不 Abort 编排 |
| 边界 | 无第二套 ToolGroup;无 C++ ImageDisplay |
| 会话 | 批跑结束按 session-lifecycle 排空/释放;观察引用不阻塞归还契约 |
| 契约映射 | TC-RUN-04 / TC-SER-01 / TC-CTL-05 |
对应文档章节
toolgroup §7.2 / §7.3 / §9.4;host-integration(R7 同核 / 停止分派);csharp-binding;示例指南 T12;session-lifecycle;image-display;test-contracts TC-RUN-04 / TC-SER-01 / TC-CTL-05
S08 · Tool 作者:fake 注册 + 最小自定义 Tool
目的
落实 §7.6:测试包 fake::register_fake_tools;作者实现最小自定义 Tool(type_id 由基类构造固定、属性袋、Run、失败/取消、成像纪律);与 Create/Load 共用同一 Registry;重复 type_id 拒绝。
前置
- 公开头:
toolgroup/registry.hpp/toolgroup/tool.hpp(基类是Tool,不是ITool);成像公开句柄头。 - 自定义工具不要默认链进生产
ToolRegistry::RegisterBuiltinTools(src/toolgroup/registry.cpp)。
步骤骨架(C++)
#include <vyntek/toolgroup/registry.hpp>
#include <vyntek/toolgroup/tool.hpp>
#include <vyntek/imaging/handle.hpp>
// 最小自定义 Tool(对齐公开头 Tool:type_id / id / name 由基类构造固定)
class ScaleGrayTool final : public Tool {
public:
explicit ScaleGrayTool(ToolId id) : Tool(std::move(id), "demo_scale_gray", "ScaleGray") {
AddInput("InputImage", TypeDesc{Type::ImageGray8, {}}); // 端口同时是运行时值槽
AddOutput("OutputImage", TypeDesc{Type::ImageGray8, {}});
DeclareProperty("Scale", TypeDesc{Type::Float32, {}}, PropertyFlags{true, true, true, true});
}
DiagnosticList Run(RunContext& ctx) override {
DiagnosticList diags;
const Value in = ReadPort("InputImage"); // 链接拉值由执行器写入端口(§4.2)
const vyntek::imaging::PixelImageHandle* src = in.image();
if (src != nullptr) {
// Clone → OpenWrite → 缩放填充 → Complete → ShareImage → WriteOutput("OutputImage", …)
}
if (ctx.cancel_requested) {
// 协作退出(T9 / TC-CTL-01);失败/取消不得交出未 Complete 句柄(H10)
}
last_run_ = LastRunStatus::Succeeded; // 失败时置 Failed 并返回 ToolFailure 诊断
return diags;
}
// 其余按 §7.6.1:id/name/enabled/props/Get/Set 由基类提供
};
void register_demo_tools(ToolRegistry& r) {
r.Register("demo_scale_gray", [](ToolId id) {
return std::make_unique<ScaleGrayTool>(std::move(id));
});
// 重复 Register 同 type_id → 返回 false,不覆盖已有工厂(TC-REG-02)
}
// 测试入口
ToolRegistry registry;
fake::register_fake_tools(registry); // §7.6.3 清单
register_demo_tools(registry);
auto t = registry.Create("demo_scale_gray", "S");
assert(t != nullptr);
auto missing = registry.Create("no_such_type", "X");
assert(missing == nullptr); // 调用方映射为 DiagnosticCode::UnknownTypeId(TC-REG-03)
期望断言
| 项 | 期望 |
|---|---|
| 注册 | fake + demo 可 Create;未知 type_id → Create 返回 nullptr,调用方映射 UnknownTypeId |
| 共用 | .avtg 含 demo_scale_gray 时 Load 走同一工厂;禁止反序列化旁路 |
| 成像纪律 | 输入只读;输出稳定发布;H1/H2/H8 可单测 |
| 失败 | 未 Complete 输出不可被下游拉成成功 ReadOnly(H10) |
| 重复注册 | 同 type_id 第二次 Register 失败(生产默认) |
| 契约映射 | TC-REG-01…TC-REG-06;成像 TC-H2 / TC-H8 / TC-H10 |
对应文档章节
toolgroup §7.6 / §7.4.1 / §3.4;cpp-headers;示例指南(工具读写像素片段)
S09a · 嵌套 ToolGroup 合法 + Encapsulation
目的
补齐 §9.3 / §10.1「嵌套仅经终端对外」:内层先组好再 AddTool 进外层;外层只链内层 Outputs/Inputs 终端;企图绑内层私有工具属性 → Encapsulation。证明嵌套 Run = 子组完整顺序表,对外仍是一个 ITool::Run。
前置
fake::register_fake_tools:fake_acq、fake_passthrough、fake_constant(或等价)。- 无
.avtg亦可;内存建内层/外层即可(.avtg嵌套 schema 见 §6.6,本场景主路径为内存)。
步骤骨架(C++)
// 示意、非可编译;对齐 toolgroup §9.3 / §9.4 嵌套示意 / §7.4.6 Encapsulation
auto inner = ToolGroup::Create("InnerLocate");
auto* acq = inner->AddTool(registry.Create("fake_acq", "Acq"));
auto* pass = inner->AddTool(registry.Create("fake_passthrough", "Share"));
inner->AddOutput("FixturedImage", TypeDesc{Type::ImageGray8, {}});
inner->Link(acq, "OutputImage", pass, "InputImage");
inner->LinkToOutput(pass, "OutputImage", "FixturedImage");
DiagnosticList inner_diags;
assert(inner->Validate(inner_diags));
auto outer = ToolGroup::Create("OuterMeasure");
// 嵌套不是把 ToolGroup 直接塞进 AddTool;要包一层 NestedGroupTool(toolgroup §3.3 / nested_group_tool.hpp)
auto nested = std::make_unique<NestedGroupTool>("locate", inner, "InnerLocate");
nested->SyncTerminalsFromInner(); // 把内层 Inputs/Outputs 镜像成本工具端口
Tool* locate = outer->AddTool(std::move(nested)); // 内层流仍由 shared_ptr 共享,非独占
auto* flag = outer->AddTool(registry.Create("fake_constant", "WidthFlag"));
outer->AddOutput("Preview", TypeDesc{Type::ImageGray8, {}});
outer->AddOutput("Flag", TypeDesc{Type::Int32, {}});
// 合法:外层只链内层终端(镜像端口)
outer->LinkToOutput(locate, "FixturedImage", "Preview");
// 端点写法等价:outer->Link(LinkEndpoint{locate,"FixturedImage"}, LinkEndpoint{"group","Preview"});
flag->SetProperty("Value", Value::Int32(1));
outer->LinkToOutput(flag, "Output", "Flag");
DiagnosticList diags;
assert(outer->Validate(diags));
RunSession s(outer);
RunResult r = s.Run();
// 期望:r.ok();Preview / Flag 可读;内层私有 Acq 属性对外不可见
// --- Encapsulation 负例(勿留在成功配方)---
// outer->Link(…, /* 指向 */ "InnerLocate.Acq.OutputImage", …);
// 或 Validate 时发现外层 link 写内层私有路径
// → DiagnosticCode::Encapsulation(Severity::Error);不留下半条边
期望断言
| 项 | 期望 |
|---|---|
| 合法嵌套 | Validate 无 Error;r.ok() == true;外层 Outputs 可读 |
| 封装 | 外层仅经内层终端链接;内层私有工具属性不可被外层 Link |
| Encapsulation | 绑内层私有路径 → DiagnosticCode::Encapsulation;无残链 |
| 嵌套 Run | 子组按自身列表序完整执行;对外一次 Tool::Run(NestedGroupTool::Run 转发内层) |
| 契约映射 | TC-RUN-06 / TC-LNK-04 / TC-REG-05 |
对应文档章节
toolgroup §4.3(嵌套 Run)/ §9.3 / §9.4 嵌套示意 / §6.6 / §7.4.6 / §10.1(终端封装)
S09b · SourceDisabled + InvalidBinding
目的
将已进 §10.1 / Diagnostic 但 S01–S08 未独立成文的两项升格为清晰主断言:(1)源工具 disabled → 下游 Run 拉值 SourceDisabled(MVP 严格);(2)端点缺失 / 方向非法 → InvalidBinding(从 S04 附带升格,不与 UnknownTypeId / MissingRequiredProperty 混轨)。
前置
- fake:
fake_acq+fake_passthrough(或fake_constant)。 - 可内存建组;InvalidBinding 亦可经残缺
.avtglinks 触发(§6.6.4)。
步骤骨架(C++)
// --- A) SourceDisabled ---
auto group_d = ToolGroup::Create("S09b_Disabled");
auto* acq = group_d->AddTool(registry.Create("fake_acq", "Acq"));
auto* pass = group_d->AddTool(registry.Create("fake_passthrough", "Pass"));
group_d->AddOutput("Image", TypeDesc{Type::ImageGray8, {}});
group_d->Link(acq, "OutputImage", pass, "InputImage");
group_d->LinkToOutput(pass, "OutputImage", "Image");
group_d->SetDisabled("Acq", true); // 流级 disabled 集合(§4.3);工具级 enabled 开关语义相同
DiagnosticList ddiags;
assert(group_d->Validate(ddiags)); // 结构合法;禁用本身不必然 Validate 失败
RunSession sd(group_d);
RunResult rd = sd.Run();
// 期望:rd.ok() == false;diagnostics 含 DiagnosticCode::SourceDisabled;
// Pass 不得假成功交出 Image;下游不得读到「假装有效」的帧(TC-LNK-06)
// --- B) InvalidBinding(主断言)---
auto group_b = ToolGroup::Create("S09b_BadBind");
auto* c = group_b->AddTool(registry.Create("fake_constant", "C"));
group_b->AddOutput("Out", TypeDesc{Type::Int32, {}});
// 端点缺失:属性名不存在 / 方向非法(输出←输出、输入→输入)/ 残缺 link
// Link 单边检查:失败即返回 LinkResult{ok=false},不留下半条边(§7.4.2)
LinkResult bad = group_b->LinkToOutput(c, "NoSuchProp", "Out");
assert(!bad.ok);
// 或:Load("fixtures/invalid_binding.avtg") 后 Validate(AppendRawLink 不做边验证)
DiagnosticList bdiags;
bool ok = group_b->Validate(bdiags);
assert(!ok);
assert(std::any_of(bdiags.begin(), bdiags.end(), [](const Diagnostic& d) {
return d.code == DiagnosticCode::InvalidBinding
&& d.severity == Severity::Error;
}));
// 不留下半条边;与 TypeMismatch / OrderViolation / Encapsulation 分码
期望断言
| 项 | 期望 |
|---|---|
| SourceDisabled | Run 拉值失败;码 SourceDisabled;下游无假成功 Outputs |
| 禁用目标 | 以其为目标的链接本轮不写入(§4.3);本场景主断言在「源禁用」 |
| InvalidBinding | 端点缺失/方向非法/残链 → InvalidBinding Error;无残链 |
| 分轨 | 不与 UnknownTypeId(Load)/ MissingRequiredProperty(Validate 缺配置)/ TypeMismatch / Encapsulation 混用同一码 |
| 契约映射 | TC-LNK-06 / TC-LNK-05 |
对应文档章节
toolgroup §4.3 / §7.4.2 / §7.4.6 / §6.6.4 / §10.1(禁用源);S02 负例指针升格至此
S09c · IoFailure + 停相机 ≠ 自动 Cancel
目的
(1)文件不可读 / 截断 → Load/Save IoFailure(补齐 Diagnostic 表有码、场景正文缺席)。(2)停止矩阵独立步骤:宿主停相机 不自动 RequestCancel 在途 Run(可并入本场景;与 S06 Cancel/Busy 分测,避免只写在断言附注里)。
前置
- 夹具:截断/权限错误的
.avtg路径;或 Save 到不可写路径(示意)。 - 停相机:采集入口可用 fake/桩(无真实相机 SDK 证据要求);Run 在途可用
fake_cancel_check(可set_spin)或自定义探针工具。
步骤骨架(C++)
// --- A) IoFailure(对应 tests/contracts_serialize.cpp TC-SER-07)---
LoadResult truncated = LoadAvtg("fixtures/truncated.avtg", registry);
assert(truncated.group == nullptr);
// truncated.diagnostics 含 DiagnosticCode::IoFailure(Severity::Error)
// 对照:未知 type → UnknownTypeId;过高 schema → SchemaTooNew;本项为 IO/截断
// SaveResult s = SaveAvtg(*group, "/root/no_write/out.avtg"); // 不可写路径
// 期望:s.ok == false;s.diagnostics 含 IoFailure
// --- B) 停相机 ≠ 自动 Cancel(停止矩阵独立步骤;对应 tests/smoke_acquisition_stop.cpp)---
auto group = ToolGroup::Create("S09c_StopCam");
auto* slow = group->AddTool(registry.Create("fake_cancel_check", "Slow"));
// … 合法最小链 …
RunSession session(group); // 构造吃 std::shared_ptr<ToolGroup>
vyntek::imaging::AcquisitionSession camera;
camera.Arm("cam"); camera.Start();
// 线程1:RunResult r = session.Run(); // 在途
// 宿主:camera.RequestStop(); // 采集层;见 camera-acquisition / session-lifecycle
// 期望:session **未**被自动 RequestCancel;r 可跑至工具边界正常结束(或宿主再显式 Cancel)
// 诊断:不得仅因停相机而出现 Cancelled(除非宿主显式既停采集又 RequestCancel,TC-SESS-05)
期望断言
| 项 | 期望 |
|---|---|
| IoFailure | 截断/不可读 Load 或不可写 Save → IoFailure;无半初始化成功流 |
| 分轨 | IoFailure ≠ SchemaTooNew ≠ UnknownTypeId |
| 停相机 | 不自动 RequestCancel;在途 Run 继续至边界或自行 Cancel |
| 显式双停 | 宿主若要两端都停,须显式停采集 + RequestCancel(文档断言,非隐式) |
| 与显示 | 显示关闭仍不停编排(TC-CTL-05;本场景主钉停相机条) |
| 契约映射 | TC-SER-07 / TC-CTL-04 |
对应文档章节
toolgroup §7.4.3 停止矩阵 / §7.4.6 / §10.1(T9);session-lifecycle;camera-acquisition(薄);S06 附注升格
S09d · §9.2 两组终端串联(可选)
目的
可选补齐 §9.2 心智:外层 Job/组内两个子 ToolGroup(Locate → Defect)仅经 终端 串联;列表序先 Locate 再 Defect;非嵌套私有属性、非边就绪调度。与 S09a(真嵌套封装)对照:本场景是「两组并列 + 终端链接」。
前置
- fake 足以冒充 Locate/Defect 对外终端(如内部分别用
fake_acq+passthrough /fake_constant)。 - 可全部内存组装;不要求编辑 UI。
步骤骨架(C++)
// 示意:两子组先各自 Validate,再经 NestedGroupTool 包装放进外层列表并链终端(§9.2)
auto locate = ToolGroup::Create("Locate");
// … AddTool fake_acq → passthrough;AddOutput FixturedImage / Pose(示意)…
DiagnosticList ldiags;
assert(locate->Validate(ldiags));
auto defect = ToolGroup::Create("Defect");
defect->AddInput("Image", TypeDesc{Type::ImageGray8, {}});
// … 内部 fake 检测;AddOutput OkNg / DefectCount …
DiagnosticList ddiags;
assert(defect->Validate(ddiags));
auto job = ToolGroup::Create("Job");
// ToolGroup 不能直接 AddTool;每个子组包一层 NestedGroupTool(镜像其终端为端口)
auto loc_wrap = std::make_unique<NestedGroupTool>("loc", locate, "Locate");
loc_wrap->SyncTerminalsFromInner();
auto* loc = job->AddTool(std::move(loc_wrap));
auto def_wrap = std::make_unique<NestedGroupTool>("def", defect, "Defect");
def_wrap->SyncTerminalsFromInner();
auto* def = job->AddTool(std::move(def_wrap));
job->Link(loc, "FixturedImage", def, "Image"); // 便捷重载:工具端口 → 工具端口
// 可选:Pose 终端同理
job->AddOutput("DefectCount", TypeDesc{Type::Int32, {}});
job->LinkToOutput(def, "DefectCount", "DefectCount");
DiagnosticList jdiags;
assert(job->Validate(jdiags));
RunSession s(job);
RunResult r = s.Run();
// 期望:执行序 Locate → Defect;r.ok();终端赋值生效;无环边
期望断言
| 项 | 期望 |
|---|---|
| 序 | 外层列表:Locate 在 Defect 之上;先 Locate.Run 再 Defect.Run |
| 终端 | 只链子组 Inputs/Outputs;不绑子组内部私有工具属性(否则走 S09a Encapsulation) |
r.ok() | true;Outputs 符合链接 |
| 非边就绪调度 | 链接按列表序赋值,不按边就绪触发 |
| 契约映射 | TC-RUN-07 / TC-LNK-01 / TC-REG-05 |
对应文档章节
toolgroup §9.2 / §4.1 / §10.1;与 S09a(§9.3 嵌套)对照
S10 · 输出观察 T10 事件闭集(fake)
目的
将 output-inspection T10 已定集合事件与观察不变量收成可跑骨架(fake tools 即可):SnapshotReplaced / EmptySnapshot(禁静默)/ Unavailable / Evicted;失败保留上次成功;Subscribe + replay;查看者寿命 vs Evict;KeepLatest vs ReleaseAfterRouting;观察永不驱动 Run(对齐 TC-RUN-05 / TC-OUT-09)。不改事件闭集与 toolgroup v1.9 语义。
前置
- 观察面是独立的
ImageOutputInspector(toolgroup/output_inspector.hpp;IImageOutputInspector虚接口)——不是挂在RunSession上。本场景用假图(ImageBuffer::Rent(n))发布,无需真图或相机。 - 本仓已有可跑骨架:
tests/smoke_output_inspector.cpp(TC-OUT-01…08)+tests/smoke_observation_gate.cpp(TC-OUT-09/TC-RUN-05:观察不驱动 Run)。
步骤骨架(C++)
// 与本仓 tests/smoke_output_inspector.cpp 同一形状;对齐 output-inspection T10
using namespace vyntek::toolgroup;
using namespace vyntek::imaging;
ImageOutputInspector inspector; // 默认 KeepLatest
std::vector<OutputCollectionEvent> events;
auto sub = inspector.SubscribeCollectionEvents(
[&](const OutputCollectionEvent& ev) { events.push_back(ev); },
/*replay_latest=*/false); // true = 先重放当前保留集(含 EmptySnapshot)
ImageOutputItem item;
item.port_name = "out";
item.image_index = 0;
item.buffer = ImageBuffer::Rent(32);
const BufferId first_id = item.buffer.buffer_id();
// --- A: KeepLatest ---
std::vector<ImageOutputItem> items;
items.push_back(std::move(item));
inspector.PublishSuccess("run1", "toolA", "inv1", std::move(items));
// 非空成功 → SnapshotReplaced;当前集合可打开
ImageOutputKey key; ImageBuffer opened;
inspector.TryOpenLatestOutput("toolA", "out", 0, key, opened); // opened.buffer_id() == first_id
// 空成功 → 必须 EmptySnapshot(禁静默)
inspector.PublishSuccess("run2", "toolA", "inv2", {});
// 失败不替换上次成功快照(TC-OUT-05)
inspector.PublishFailure("run3", "toolA", "inv3"); // 保留仍在;不发 SnapshotReplaced
// 保留/拷贝失败 → Unavailable(不得把旧集合冒充本轮新结果)
inspector.NoteUnavailable("run4", "toolA", "inv4");
// Evict + 查看者寿命:已打开的 Share 仍有效;新 Open 对新身份不可用(TC-OUT-07)
ImageBuffer held = opened; // 模拟已开查看者
inspector.Evict("toolA");
// held 仍可用至自行关闭;TryOpenLatestOutput 对新身份返回 false
sub.Unsubscribe(); // 只释放订阅,不清空已保留的最近输出(KeepLatest)
// --- B: ReleaseAfterRouting ---
inspector.SetRetentionPolicy(OutputRetentionPolicy::ReleaseAfterRouting);
// 发布后即不保留;再 Subscribe 无法补取已回收历史
期望断言
| 项 | 期望 |
|---|---|
| SnapshotReplaced | 非空成功 → 发该事件;当前集合身份可打开 |
| EmptySnapshot | 空成功 → 必须发事件;禁止静默;不得把上次非空标为当前 |
| Unavailable | 保留失败 → 该事件;不冒充本轮新结果 |
| Evicted | 淘汰 → 该事件;其后 TryOpen 不可用 |
| 失败保留 | 工具失败不替换上次成功快照(TC-OUT-05 / TC-RUN-05) |
| Subscribe+replay | replay_latest 重放当前保留(含空);之后不漏更新 |
| 查看者 vs Evict | 已打开查看者不被强制关;新 Open 不可用 |
| KeepLatest vs ReleaseAfterRouting | 前者可重放;后者不冒充可恢复已回收历史 |
| 永不驱动 Run | 观察回调/查询不隐式 Run(TC-OUT-09) |
| 契约映射 | TC-OUT-01…TC-OUT-09;交叉 TC-RUN-05 |
对应文档章节
output-inspection(T10);test-contracts §7 TC-OUT-*;toolgroup §7.4.3 / §10.1 非编排驱动;S03 薄指针
S11 · 采集接入 + 会话停流(fake · 阶段 D)
目的
将 camera-acquisition / session-lifecycle 已定散文收成可跑骨架(fake SDK / 假入口即可):持帧 ImportNative vs 回调内 Copy;TryPostOwned 接管/拒绝/抛错;满池与入口预算;归还泵与 ReturnCompleted;Latest 预览;回调不堵 SDK 归还;停流状态机结束条件;外保留 ≠ 假称已归还;超时未排空但底层续清;ReturnFailed / 断线可观察;两端都停须显式 Cancel + 停采集。不改 Diagnostic / T9 停止矩阵语义与 toolgroup v1.9。
前置
- 本仓已有可跑骨架:
tests/smoke_acquisition_stop.cpp(TC-ACQ-01/02/07+TC-SESS-01…05)与tests/smoke_memory_handoff.cpp(TC-ACQ-03…06)。 - fake 采集适配:
include/vyntek/imaging/fake_camera_adapter.hpp的FakeCameraAdapter(Mode::HoldFrameImportvsMode::CallbackCopy;evidence()提供 import/copy/sdk_release 计数作证据)。假池容量与入口预算可注入满载(ImageBuffer::CanRent/TryRent+MemoryPool*)。 - 假归还泵:
AcquisitionSession::SetOutstandingReturnsForTest/SetReturnFailedCountForTest可注入;断线用NoteCameraDisconnected()。 - 供应商 SDK 驱动非必需。
步骤骨架(C++)
// 与本仓 tests/smoke_acquisition_stop.cpp / smoke_memory_handoff.cpp 同一形状
using namespace vyntek::imaging;
AcquisitionSession session;
session.Arm("fake-cam");
session.Start(); // state() == Running
FakeCameraAdapter adapter;
// --- A: 持帧 ImportNative(TC-ACQ-01)---
// PublishFrame(HoldFrameImport) = ImportNative(ReadOnly) + TryPostOwned;
// SDK release 恰好一次,且只在最后句柄关闭时发生(sdk_release_count())
adapter.PrepareSlot(64, 0xAB);
adapter.PublishFrame(session, FakeCameraAdapter::Mode::HoldFrameImport);
assert(adapter.import_native_count() == 1 && adapter.slot_held());
// --- B: 回调内 Copy(TC-ACQ-02)---
// 先查入口/池预算(CanRent)→ TryRent → Copy-into → TryPostOwned;满则不拷直接拒绝;
// SDK 槽位立即归还(slot_returned()),回调内不得等归还(AssertCallbackNonBlocking)
adapter.PublishFrameFromCallback(session, FakeCameraAdapter::Mode::CallbackCopy);
assert(adapter.copy_count() == 1 && adapter.slot_returned());
// --- C: Latest 预览(TC-ACQ-06)---
for (int i = 0; i < 10; ++i) { adapter.PublishFrame(session, FakeCameraAdapter::Mode::HoldFrameImport); }
auto latest = session.Latest(); // 有所有权预览槽:只留最新;替换即关上一预览
auto front = session.PeekIngressFront(); // FIFO 队首;与 Latest 分槽,禁混淆
assert(session.ingress_size() <= 8); // kIngressCapacity = 8;满 → InvalidOperation
// --- D: 停流结束条件(TC-SESS-01)---
session.RequestStop(); // → Quiescing → … → Stopped
StopSummary summary;
session.WaitStop(StopTimeout{1000}, &summary);
assert(session.state() == AcquisitionState::Stopped);
// 结束联合条件:无新回调、队列空、会话拥有引用已关、SDK 归还完成(stop_progress)
// --- E: 外保留 ≠ 假称已归还(TC-SESS-02)---
auto exported = latest; // 会话外仍持有(外部保留)
// stop_progress().external_retention 为 true;claims_device_returned() 不得为 true
// --- F: 超时续清(TC-SESS-03)---
StopSummary to;
session.WaitStop(StopTimeout{50}, &to); // 超时返回未排空;cleanup_continues,不假装 Stopped
// --- G: ReturnFailed / 断线可观察(TC-SESS-04)---
session.SetReturnFailedCountForTest(1); // 或 session.NoteCameraDisconnected()
// stop_faults() 可观察;不自动再 Release;泵继续其他项;停止结果汇总
// --- H: 两端都停(TC-SESS-05)---
// 宿主要相机与 Run 一起停:必须显式,两端互不代停(M3-07 / TC-SESS-05)
session.RequestStop();
run_session.RequestCancel(); // 显式 Cancel;RunSession 不会替你停采集
// 禁止假设一次 SDK.Stop = 三层全停
期望断言
| 项 | 期望 |
|---|---|
| ImportNative | 持帧路径稳定 ReadOnly;Share 不重复 AddRef;一次 Release(TC-ACQ-01) |
| 回调 Copy | 临时指针路径必须 Copy;回调后不持原地址(TC-ACQ-02) |
| TryPostOwned | 成功接管 / 拒绝关闭 / 抛错未接管(TC-ACQ-03) |
| 满池预算 | 先预算再拷;满则拒绝可观察(TC-ACQ-04) |
| 归还泵 | ReturnCompleted 仅 SDK 确认后;失败不吞、不自动重试(TC-ACQ-05) |
| Latest | 有界最新槽;替换即关;无无限带图 Post(TC-ACQ-06) |
| 回调不堵 | 回调不等 UI/算法/同线程归还(TC-ACQ-07) |
| 停流结束 | 状态机到 Stopped 的联合条件满足(TC-SESS-01) |
| 外保留 | 不得假称已归还(TC-SESS-02) |
| 超时续清 | 超时返回未排空;底层继续(TC-SESS-03) |
| 故障可观察 | ReturnFailed/断线汇总;泵不卡死(TC-SESS-04) |
| 两端都停 | 显式 Cancel + 停采集(TC-SESS-05) |
| 契约映射 | TC-ACQ-01…07;TC-SESS-01…05;交叉 TC-CTL-04 |
对应文档章节
camera-acquisition;session-lifecycle;test-contracts §8–§9;host-integration §4「两端都停」;toolgroup §7.4.3(T9,不改语义);S09c 薄指针
S12 · 存储 / pin / 归还(fake · 阶段 B)
目的
将 memory-management 已定散文收成可跑骨架(fake buffer 即可):工厂接管点;最后引用与 Returning;禁止持最后引用等自身 ReturnCompleted;pin 延寿与写 pin 阻 Complete;读 pin 永不提升为可写;OpenRead/OpenWrite 取消与 Share/Dispose 原子裁定;未 Complete→Invalid;首次根范围与 ROI 中止整实体;Invalid 仅无期望版本完整恢复;releaseAsync 恰好一次、一项卡住不堵其他、ReturnFailed 不自动重试不回池。不改 Diagnostic / T9 停止矩阵与 toolgroup v1.9。
前置
- 本仓已有可跑骨架:
tests/smoke_memory_pool.cpp/smoke_memory_ownership.cpp/smoke_memory_grants.cpp/smoke_memory_handoff.cpp(覆盖TC-MEM-01…13)。 - fake
MemoryPool/ fake nativerelease回调;可注入抛错、延迟归还、ReturnFailed(SetReturnFailedCountForTest)。 - 可构造多 handle、排队读/写申请、读/写 pin。
- 类型化首次根范围断言可薄指 image-model;本场景主断言在存储协调层。
步骤骨架(C++)
// 与本仓 tests/smoke_memory_pool.cpp / smoke_memory_ownership.cpp /
// smoke_memory_grants.cpp / smoke_memory_handoff.cpp 同一形状
using namespace vyntek::imaging;
// --- A: 工厂接管 ---
auto rented = ImageBuffer::Rent(4096); // TC-MEM-01 成功接管(失败则空,不半建实体)
auto rented2 = ImageBuffer::TryRent(4096); // 复制前预算路径(M4-03)
assert(ImageBuffer::CanRent(4096)); // 非预留查询,总在 TryRent 之前
// TakeOwnership 抛错路径:owner 仍由调用方释放(TC-MEM-02)
// ImportNative 抛错路径:不调 release(TC-MEM-03)
// --- B: 最后引用 ---
auto h1 = rented.Share();
auto h2 = rented.Share();
h1 = ImageBuffer{}; // 关掉一个 Share;仍 Active(TC-MEM-04)
// 关闭全部 handle + 访问 + 排队申请后才 Returning
// 禁止:持最后引用边 AwaitReturnCompleted(TC-MEM-05,否则卡死)
// --- C: pin(pin 在 View/像素句柄层,不在 ImageBuffer 上)---
Gray8Handle gh{PixelImage::RentGray8(ImageInfo(32, 16))};
auto rview = gh.OpenRead(); // OpenResult<ReadOnlyViewBase>
ImageBufferPin pin = rview->Pin(); // 读 pin:父 reader close 后仍阻写/归还(TC-MEM-06)
assert(pin && pin.access() == BufferAccess::ReadOnly);
rview->close(); // pin 仍阻写/归还;Access 永 ReadOnly
pin = ImageBufferPin{}; // 释放读 pin
auto wview = gh.OpenWrite(); // OpenResult<WritableViewBase>
ImageBufferPin wpin = wview->Pin(); // 写 pin
// wview->Complete() 在写 pin 未放时 → 抛 std::runtime_error(TC-MEM-07)
wpin = ImageBufferPin{}; // 释放写 pin
wview->Complete(); // 现在可 Complete
// --- D: 授予/取消与 Share/Dispose ---
// OpenRead/OpenWrite + CancelToken;发起 handle Dispose 不撤已入队申请(TC-MEM-08)
// Share 与 Dispose 竞争:成功子对象独立有效(TC-MEM-09)
// --- E: 初始化 / Invalid ---
{
Gray8Handle h{PixelImage::RentGray8(ImageInfo(32, 16))};
auto w = h.OpenWrite();
w->close(); // 未 Complete → Invalid(TC-MEM-10)
auto bad = h.OpenRead();
assert(!bad && bad.status == Status::Invalid);
}
// 类型化首次仅写 ROI → 拒绝;ROI 中止 → 整实体 Invalid(TC-MEM-11)
// Invalid 后带 expectedVersion 的 OpenWrite 拒绝;无期望版本完整写可恢复(TC-MEM-12)
// --- F: 归还隔离 ---
// 两实体并行归还;其一 releaseAsync 卡住/失败
// expect:恰好一次 release;不串行堵另一项;ReturnFailed 不自动重试、不回池(TC-MEM-13)
期望断言
| 项 | 期望 |
|---|---|
| Rent | 成功接管;失败归还内部;ReadOnly+Uninitialized 拒绝(TC-MEM-01) |
| TakeOwnership | 成功才接管;抛错不释放调用方 owner(TC-MEM-02) |
| ImportNative | 成功才接管;抛错不调 release(TC-MEM-03) |
| 最后引用 | 全 handle+访问+排队结束才 Returning(TC-MEM-04) |
| 等待环 | 禁持最后引用等自身 ReturnCompleted(TC-MEM-05) |
| 读 pin | 父 reader close 后仍阻写/归还;Access 永 ReadOnly(TC-MEM-06) |
| 写 pin | 未放则 Complete 拒绝(抛 std::runtime_error);父先 Dispose→Invalid 但独占至末 pin(TC-MEM-07) |
| 取消/排队 | cancel 可撤等待;发起 handle Dispose 不撤已入队(TC-MEM-08) |
| Share/Dispose | 原子裁定;成功子对象独立有效(TC-MEM-09) |
| 未 Complete | close/Dispose→Invalid;不重发旧代次(TC-MEM-10) |
| 根范围/ROI | 首次须根范围;ROI 中止整实体 Invalid(TC-MEM-11) |
| 恢复 | 无期望版本完整写可恢复;带 expected 拒绝 Invalid(TC-MEM-12) |
| 归还隔离 | release 一次;卡住不堵其他;ReturnFailed 不重试不回池(TC-MEM-13) |
| 契约映射 | TC-MEM-01…13 |
对应文档章节
memory-management;image-model(首次根范围 / padding 薄指);test-contracts §10;S05 句柄交接薄指针(H1–H10 不替代本表)
S13 · 显示集成(仅 C# · 阶段 F)
目的
将 image-display 已定显示契约收成可勾选路径:登记≠授予、Share 不接管、Retain/Snapshot/Detach、四回执互不等价、失败保留旧图、四格式取样、ROI/负步长、卸载/重挂/context、预算失败不丢旧图、显示只观察。仅 C# 显示宿主;像素经绑定自 C++ vyntek::imaging。勿写成 C++ UI;≠ T15 开放风险 R10(C++ UI·已关闭)。
前置
- 绑定层可将 C++
PixelImageHandle投影为IPixelImage(及 Gray8/Rgb24/Bgr24/Depth16);控件实现在bindings/csharp/Vyntek.Controls/ImageDisplay.cs。 - 本仓已有可跑骨架:
bindings/csharp/Vyntek.Controls.Tests/(40 项:38[Fact]+ 1[Theory]×2;dotnet test)。 - UI 线程可用;C#
ImageDisplay实例已创建(见 csharp-binding / 示例指南 显示段)。 - fake / 测试图像可提供四格式、ROI、负步长与可写/只读两态(无需真机相机)。
步骤骨架(C# · 仅显示段)
// 与本仓 bindings/csharp/Vyntek.Controls/ImageDisplay.cs 同一形状(对齐 image-display / TC-DISP-*)
// 像素已由绑定层从 C++ 句柄投影为 IPixelImage
var update = viewer.SetImage(image); // TC-DISP-01:登记+Share;未承诺已 OpenRead
// 调用方随后可关闭自有 handle;控件不接管(TC-DISP-02)
var retain = viewer.SetImage(rwImage, InputRetention.RetainInput); // 默认即 RetainInput
// 原地写须等读者退出(TC-DISP-03)
var snap = viewer.SetImage(rwImage, InputRetention.Snapshot);
await snap.Completion; await snap.InputReleased; // 释放后原图可写;屏幕可仍旧图(TC-DISP-04)
var detach = await retain.DetachInputAsync(); // Released 或 CpuBudgetExceeded 不毁当前(TC-DISP-05)
// Completion / InputReleased / ImageRendered / ReturnCompleted 分别断言,互不等价(TC-DISP-06)
// ImageRendered 由宿主 NotifyImageRendered(updateId) 上报;控件不冒充(TC-DISP-06)
// 注入 VersionMismatch / UnsupportedImage → Failed,旧画面保留(TC-DISP-07)
viewer.SetImage(gray); viewer.TryGetGray8(x, y, out var g); // 仅相符格式成功(TC-DISP-08)
viewer.SetImage(rgb); viewer.TryGetRgb24(x, y, out var r2); // 无隐式转换
viewer.SetImage(bgr); viewer.TryGetBgr24(x, y, out var b);
viewer.SetImage(depth); viewer.TryGetDepth16(x, y, out var d); // 含 out_is_valid
viewer.TryGetGray8AtView(viewX, viewY, out var p); // ROI/负步长→逻辑坐标(TC-DISP-09)
viewer.Unload(); // 卸载 → 取消候选;保留 Source(TC-DISP-10)
var again = viewer.Remount(); // 重挂载仍有效;Source = 新请求
viewer.CpuBudgetLimitBytes = 4 << 20; // 预算不足 → Failed + 保留旧图(TC-DISP-11)
// Dispose / ClearImageAsync → 不停 Run、不停相机(TC-DISP-12 / TC-CTL-05)
期望断言
| 项 | 期望 |
|---|---|
| 登记≠授予 | SetImage 返回即已 Share;Installed/OpenRead 在 Completion 路径(TC-DISP-01) |
| Share 不接管 | 控件不 Dispose 调用方 handle;多控件同图合法(TC-DISP-02) |
| RetainInput | 持稳定读;原地写等待退出;属性不改当前存储(TC-DISP-03) |
| Snapshot | InputReleased 后不阻原图写;Source 不置空(TC-DISP-04) |
| Detach | Released 保持来源身份;预算不足不毁当前(TC-DISP-05) |
| 四回执 | Completion ≠ InputReleased ≠ ImageRendered ≠ ReturnCompleted(TC-DISP-06) |
| 失败保留 | Failed/VersionMismatch 保留旧画面;无信息混叠(TC-DISP-07) |
| 四格式 | TryGet* 无隐式转换;Depth 无效码/米制规则正确(TC-DISP-08) |
| ROI/负步长 | TryGet*AtView 逻辑像素取样;禁止 viewPos 当缓冲偏移(TC-DISP-09) |
| 卸载/重挂 | 取消候选;重挂=新请求;context 可恢复(TC-DISP-10) |
| 预算 | 失败不丢旧图;释放本次引用;无自动重试队列(TC-DISP-11) |
| 只观察 | 卸图/Dispose 不停编排/不停相机(TC-DISP-12 / TC-CTL-05) |
| 契约映射 | TC-DISP-01…12;交叉 TC-CTL-05 |
| 边界 | 仅 C#;无 C++ UI;≠ T15 R10;不扩 T9 |
对应文档章节
image-display;示例指南 显示段;test-contracts §11;host-integration TC-CTL-05;S05/S07 显示段薄指针(不替代本表)
使用说明(给实现 / 验收)
- 示意非可编译:骨架钉住 API 形状与断言;落地时以公开头与单测为准。树形碎片对照见 sketch 页(R5,非验收正文)。
- 主路径覆盖:S01–S08 为八条主路径;S09a–d(R6) 补齐嵌套/Encapsulation、SourceDisabled、InvalidBinding、IoFailure、停相机≠自动 Cancel,以及可选 §9.2 两组串联;S10 对齐 T10 输出观察可勾选;S11 对齐阶段 D 采集/会话可勾选;S12 对齐阶段 B 存储/pin/归还可勾选;S13 对齐阶段 F 显示集成可勾选(仅 C#;≠ T15 R10)——不改 v1.9 语义与 Diagnostic 码表 / T9 停止矩阵 / T10 事件闭集。
- 验收勾选:全文契约见 test-contracts(R4 + R6 及后续增补映射,
TC-*/TC-OUT-*/TC-ACQ-*/TC-SESS-*/TC-MEM-*/TC-DISP-*);与 toolgroup §10.1 / T13 生命周期对照(索引已对齐 OUT/ACQ/SESS/MEM/DISP + S10–S13);场景骨架映射见契约表 §12;薄指针见 validation-and-roadmap。不以本文代替契约表勾选正文。 - 禁止:第二执行引擎;C++ UI;把 ImageDisplay 示例改成 C++;不写覆盖率 KPI;不扩观察 UI / 脚本 / IPC / ABI。
文档状态:场景规划草案,对齐 toolgroup v1.9 / R4 契约表 / R6 S09a–d / R7 S07↔host-integration / 导读索引 / S10↔TC-OUT- / S11↔TC-ACQ-·TC-SESS- / S12↔TC-MEM-* / S13↔TC-DISP-(显示仅 C#;≠ T15 R10)/ §10.1 T13 索引对齐。实施以代码与契约表 / §10.1 勾选为准。
实现现状
| 场景 | 本仓 |
|---|---|
| S07 无 UI 入口 | examples/headless_run.cpp 只走到 passthrough Run + RequestCancel |
| S10–S13 | 规格与薄摘在本站;Controls.Tests / smoke 是外形,未验收 |
| S01–S06 / S08 / S09 | 无成套示例工程 |