Project
Loading...
Searching...
No Matches
test_FrameworkDataFlowToO2Control.cxx
Go to the documentation of this file.
1// Copyright 2019-2020 CERN and copyright holders of ALICE O2.
2// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3// All rights not expressly granted are reserved.
4//
5// This software is distributed under the terms of the GNU General Public
6// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7//
8// In applying this license CERN does not waive the privileges and immunities
9// granted to it by virtue of its status as an Intergovernmental Organization
10// or submit itself to any jurisdiction.
11
12#include "Mocking.h"
13#include <catch_amalgamated.hpp>
14#include "../src/O2ControlHelpers.h"
15#include "../src/DeviceSpecHelpers.h"
16#include "../src/SimpleResourceManager.h"
17#include "../src/ComputingResourceHelpers.h"
23
24#include <sstream>
25
26using namespace o2::framework;
27
28namespace
29{
31{
32 return {{.name = "A", //
33 .outputs = Outputs{OutputSpec{"TST", "A1"}, OutputSpec{"TST", "A2"}}, // A1 will be consumed twice, A2 is dangling
34 .algorithm = AlgorithmSpec{}, //
35 .options = {ConfigParamSpec{"channel-config", VariantType::String, // raw input channel
36 "name=into_dpl,type=pull,method=connect,address=ipc:///tmp/pipe-into-dpl,transport=shmem,rateLogging=10,rcvBufSize=789",
37 {"Out-of-band channel config"}}}},
38 {.name = "B", // producer, no inputs
39 .outputs = Outputs{OutputSpec{"TST", "B1"}},
40 .metadata = {{ecs::cpuKillThreshold, "3.0"}}},
41 {.name = "C", // first consumer of A1, consumer of B1
42 .inputs = {InputSpec{"y", "TST", "A1"}, InputSpec{"y", "TST", "B1"}},
43 .labels = {{"expendable"}},
45 {.name = "D", // second consumer of A1
46 .inputs = Inputs{InputSpec{"x", "TST", "A1"}},
47 .options = {ConfigParamSpec{"a-param", VariantType::Int, 1, {"A parameter which should not be escaped"}},
48 ConfigParamSpec{"b-param", VariantType::String, "", {"a parameter which will be escaped"}},
49 ConfigParamSpec{"c-param", VariantType::String, "foo;bar", {"another parameter which will be escaped"}},
50 ConfigParamSpec{"d-param", VariantType::String, R"(["foo","bar"])", {"a parameter with double quotes"}},
51 ConfigParamSpec{"channel-config", VariantType::String, // raw output channel
52 "name=outta_dpl,type=push,method=bind,address=ipc:///tmp/pipe-outta-dpl,transport=shmem,rateLogging=10",
53 {"Out-of-band channel config"}}},
54 .labels = {{"resilient"}}}};
55}
56
57char* strdiffchr(const char* s1, const char* s2)
58{
59 while (*s1 && *s1 == *s2) {
60 s1++;
61 s2++;
62 }
63 return (*s1 == *s2) ? nullptr : (char*)s1;
64}
65
66} // namespace
67const auto expectedWorkflow = R"EXPECTED(name: testwf
68vars:
69 dpl_command: >-
70 o2-exe --abdf -defg 'asdf fdsa' | o2-exe-2 -b --zxcv "asdf zxcv"
71defaults:
72 monitoring_dpl_url: "no-op://"
73 user: "flp"
74 fmq_rate_logging: 0
75 shm_segment_size: 10000000000
76 shm_throw_bad_alloc: false
77 session_id: default
78 resources_monitoring: 15
79roles:
80 - name: "A"
81 connect:
82 - name: into_dpl
83 type: pull
84 transport: shmem
85 target: "::into_dpl-{{ it }}"
86 rateLogging: "{{ fmq_rate_logging }}"
87 rcvBufSize: 789
88 task:
89 load: testwf-A
90 critical: true
91 - name: "B"
92 connect:
93 task:
94 load: testwf-B
95 critical: true
96 - name: "C"
97 connect:
98 - name: from_A_to_C
99 type: pull
100 transport: shmem
101 target: "{{ Parent().Path }}.A:from_A_to_C"
102 rateLogging: "{{ fmq_rate_logging }}"
103 sndBufSize: 1
104 rcvBufSize: 1
105 - name: from_B_to_C
106 type: pull
107 transport: shmem
108 target: "{{ Parent().Path }}.B:from_B_to_C"
109 rateLogging: "{{ fmq_rate_logging }}"
110 sndBufSize: 1
111 rcvBufSize: 1
112 task:
113 load: testwf-C
114 critical: false
115 - name: "D"
116 connect:
117 - name: from_C_to_D
118 type: pull
119 transport: shmem
120 target: "{{ Parent().Path }}.C:from_C_to_D"
121 rateLogging: "{{ fmq_rate_logging }}"
122 sndBufSize: 1
123 rcvBufSize: 1
124 bind:
125 - name: outta_dpl
126 type: push
127 transport: shmem
128 addressing: ipc
129 rateLogging: "{{ fmq_rate_logging }}"
130 global: "outta_dpl-{{ it }}"
131 task:
132 load: testwf-D
133 critical: true
134)EXPECTED";
135
136const std::vector expectedTasks{
137 R"EXPECTED(name: A
138defaults:
139 log_task_stdout: none
140 log_task_stderr: none
141 exit_transition_timeout: 15
142 data_processing_timeout: 10
143 _module_cmdline: >-
144 source /etc/profile.d/modules.sh && MODULEPATH={{ modulepath }} module load O2 QualityControl Control-OCCPlugin &&
145 {{ dpl_command }} | bcsadc/foo
146 _plain_cmdline: >-
147 source /etc/profile.d/o2.sh && {{ len(extra_env_vars)>0 ? 'export ' + extra_env_vars + ' &&' : '' }} {{ dpl_command }} | bcsadc/foo
148control:
149 mode: "fairmq"
150wants:
151 cpu: 0.01
152 memory: 1
153bind:
154 - name: from_A_to_C
155 type: push
156 transport: shmem
157 addressing: ipc
158 rateLogging: "{{ fmq_rate_logging }}"
159 sndBufSize: 1
160 rcvBufSize: 1
161command:
162 shell: true
163 stdout: "{{ log_task_stdout }}"
164 stderr: "{{ log_task_stderr }}"
165 env:
166 - O2_DETECTOR={{ detector }}
167 - O2_PARTITION={{ environment_id }}
168 - HOME=/tmp
169 user: "{{ user }}"
170 value: "{{ len(modulepath)>0 ? _module_cmdline : _plain_cmdline }}"
171 arguments:
172 - "-b"
173 - "--exit-transition-timeout"
174 - "'{{ exit_transition_timeout }}'"
175 - "--data-processing-timeout"
176 - "'{{ data_processing_timeout }}'"
177 - "--monitoring-backend"
178 - "'{{ monitoring_dpl_url }}'"
179 - "--session"
180 - "'{{ session_id }}'"
181 - "--infologger-severity"
182 - "'{{ infologger_severity }}'"
183 - "--infologger-mode"
184 - "'{{ infologger_mode }}'"
185 - "--driver-client-backend"
186 - "'stdout://'"
187 - "--shm-segment-size"
188 - "'{{ shm_segment_size }}'"
189 - "--shm-throw-bad-alloc"
190 - "'{{ shm_throw_bad_alloc }}'"
191 - "--resources-monitoring"
192 - "'{{ resources_monitoring }}'"
193 - "--id"
194 - "'A'"
195 - "--shm-monitor"
196 - "'false'"
197 - "--log-color"
198 - "'false'"
199 - "--no-batch"
200 - "--bad-alloc-attempt-interval"
201 - "'50'"
202 - "--bad-alloc-max-attempts"
203 - "'1'"
204 - "--channel-prefix"
205 - "''"
206 - "--early-forward-policy"
207 - "'never'"
208 - "--io-threads"
209 - "'1'"
210 - "--jobs"
211 - "'1'"
212 - "--severity"
213 - "'info'"
214 - "--shm-allocation"
215 - "'rbtree_best_fit'"
216 - "--shm-metadata-msg-size"
217 - "'0'"
218 - "--shm-mlock-segment"
219 - "'false'"
220 - "--shm-mlock-segment-on-creation"
221 - "'false'"
222 - "--shm-no-cleanup"
223 - "'false'"
224 - "--shm-segment-id"
225 - "'0'"
226 - "--shm-zero-segment"
227 - "'false'"
228 - "--signposts"
229 - "''"
230 - "--stacktrace-on-signal"
231 - "'simple'"
232 - "--timeframes-rate-limit"
233 - "'0'"
234)EXPECTED",
235 R"EXPECTED(name: B
236defaults:
237 log_task_stdout: none
238 log_task_stderr: none
239 exit_transition_timeout: 15
240 data_processing_timeout: 10
241 _module_cmdline: >-
242 source /etc/profile.d/modules.sh && MODULEPATH={{ modulepath }} module load O2 QualityControl Control-OCCPlugin &&
243 {{ dpl_command }} | foo
244 _plain_cmdline: >-
245 source /etc/profile.d/o2.sh && {{ len(extra_env_vars)>0 ? 'export ' + extra_env_vars + ' &&' : '' }} {{ dpl_command }} | foo
246control:
247 mode: "fairmq"
248wants:
249 cpu: 0.01
250 memory: 1
251limits:
252 cpu: 3.0
253bind:
254 - name: from_B_to_C
255 type: push
256 transport: shmem
257 addressing: ipc
258 rateLogging: "{{ fmq_rate_logging }}"
259 sndBufSize: 1
260 rcvBufSize: 1
261command:
262 shell: true
263 stdout: "{{ log_task_stdout }}"
264 stderr: "{{ log_task_stderr }}"
265 env:
266 - O2_DETECTOR={{ detector }}
267 - O2_PARTITION={{ environment_id }}
268 - HOME=/tmp
269 user: "{{ user }}"
270 value: "{{ len(modulepath)>0 ? _module_cmdline : _plain_cmdline }}"
271 arguments:
272 - "-b"
273 - "--exit-transition-timeout"
274 - "'{{ exit_transition_timeout }}'"
275 - "--data-processing-timeout"
276 - "'{{ data_processing_timeout }}'"
277 - "--monitoring-backend"
278 - "'{{ monitoring_dpl_url }}'"
279 - "--session"
280 - "'{{ session_id }}'"
281 - "--infologger-severity"
282 - "'{{ infologger_severity }}'"
283 - "--infologger-mode"
284 - "'{{ infologger_mode }}'"
285 - "--driver-client-backend"
286 - "'stdout://'"
287 - "--shm-segment-size"
288 - "'{{ shm_segment_size }}'"
289 - "--shm-throw-bad-alloc"
290 - "'{{ shm_throw_bad_alloc }}'"
291 - "--resources-monitoring"
292 - "'{{ resources_monitoring }}'"
293 - "--id"
294 - "'B'"
295 - "--shm-monitor"
296 - "'false'"
297 - "--log-color"
298 - "'false'"
299 - "--no-batch"
300 - "--bad-alloc-attempt-interval"
301 - "'50'"
302 - "--bad-alloc-max-attempts"
303 - "'1'"
304 - "--channel-prefix"
305 - "''"
306 - "--early-forward-policy"
307 - "'never'"
308 - "--io-threads"
309 - "'1'"
310 - "--jobs"
311 - "'1'"
312 - "--severity"
313 - "'info'"
314 - "--shm-allocation"
315 - "'rbtree_best_fit'"
316 - "--shm-metadata-msg-size"
317 - "'0'"
318 - "--shm-mlock-segment"
319 - "'false'"
320 - "--shm-mlock-segment-on-creation"
321 - "'false'"
322 - "--shm-no-cleanup"
323 - "'false'"
324 - "--shm-segment-id"
325 - "'0'"
326 - "--shm-zero-segment"
327 - "'false'"
328 - "--signposts"
329 - "''"
330 - "--stacktrace-on-signal"
331 - "'simple'"
332 - "--timeframes-rate-limit"
333 - "'0'"
334)EXPECTED",
335 R"EXPECTED(name: C
336defaults:
337 log_task_stdout: none
338 log_task_stderr: none
339 exit_transition_timeout: 15
340 data_processing_timeout: 10
341 _module_cmdline: >-
342 source /etc/profile.d/modules.sh && MODULEPATH={{ modulepath }} module load O2 QualityControl Control-OCCPlugin &&
343 {{ dpl_command }} | foo
344 _plain_cmdline: >-
345 source /etc/profile.d/o2.sh && {{ len(extra_env_vars)>0 ? 'export ' + extra_env_vars + ' &&' : '' }} {{ dpl_command }} | foo
346control:
347 mode: "fairmq"
348wants:
349 cpu: 0.01
350 memory: 1
351limits:
352 memory: 5000
353bind:
354 - name: from_C_to_D
355 type: push
356 transport: shmem
357 addressing: ipc
358 rateLogging: "{{ fmq_rate_logging }}"
359 sndBufSize: 1
360 rcvBufSize: 1
361command:
362 shell: true
363 stdout: "{{ log_task_stdout }}"
364 stderr: "{{ log_task_stderr }}"
365 env:
366 - O2_DETECTOR={{ detector }}
367 - O2_PARTITION={{ environment_id }}
368 - HOME=/tmp
369 user: "{{ user }}"
370 value: "{{ len(modulepath)>0 ? _module_cmdline : _plain_cmdline }}"
371 arguments:
372 - "-b"
373 - "--exit-transition-timeout"
374 - "'{{ exit_transition_timeout }}'"
375 - "--data-processing-timeout"
376 - "'{{ data_processing_timeout }}'"
377 - "--monitoring-backend"
378 - "'{{ monitoring_dpl_url }}'"
379 - "--session"
380 - "'{{ session_id }}'"
381 - "--infologger-severity"
382 - "'{{ infologger_severity }}'"
383 - "--infologger-mode"
384 - "'{{ infologger_mode }}'"
385 - "--driver-client-backend"
386 - "'stdout://'"
387 - "--shm-segment-size"
388 - "'{{ shm_segment_size }}'"
389 - "--shm-throw-bad-alloc"
390 - "'{{ shm_throw_bad_alloc }}'"
391 - "--resources-monitoring"
392 - "'{{ resources_monitoring }}'"
393 - "--id"
394 - "'C'"
395 - "--shm-monitor"
396 - "'false'"
397 - "--log-color"
398 - "'false'"
399 - "--no-batch"
400 - "--bad-alloc-attempt-interval"
401 - "'50'"
402 - "--bad-alloc-max-attempts"
403 - "'1'"
404 - "--channel-prefix"
405 - "''"
406 - "--early-forward-policy"
407 - "'never'"
408 - "--io-threads"
409 - "'1'"
410 - "--jobs"
411 - "'1'"
412 - "--severity"
413 - "'info'"
414 - "--shm-allocation"
415 - "'rbtree_best_fit'"
416 - "--shm-metadata-msg-size"
417 - "'0'"
418 - "--shm-mlock-segment"
419 - "'false'"
420 - "--shm-mlock-segment-on-creation"
421 - "'false'"
422 - "--shm-no-cleanup"
423 - "'false'"
424 - "--shm-segment-id"
425 - "'0'"
426 - "--shm-zero-segment"
427 - "'false'"
428 - "--signposts"
429 - "''"
430 - "--stacktrace-on-signal"
431 - "'simple'"
432 - "--timeframes-rate-limit"
433 - "'0'"
434)EXPECTED",
435 R"EXPECTED(name: D
436defaults:
437 log_task_stdout: none
438 log_task_stderr: none
439 exit_transition_timeout: 15
440 data_processing_timeout: 10
441 _module_cmdline: >-
442 source /etc/profile.d/modules.sh && MODULEPATH={{ modulepath }} module load O2 QualityControl Control-OCCPlugin &&
443 {{ dpl_command }} | foo
444 _plain_cmdline: >-
445 source /etc/profile.d/o2.sh && {{ len(extra_env_vars)>0 ? 'export ' + extra_env_vars + ' &&' : '' }} {{ dpl_command }} | foo
446control:
447 mode: "fairmq"
448wants:
449 cpu: 0.01
450 memory: 1
451bind:
452 - name: outta_dpl
453 type: push
454 transport: shmem
455 addressing: ipc
456 rateLogging: "{{ fmq_rate_logging }}"
457 global: "outta_dpl-{{ it }}"
458command:
459 shell: true
460 stdout: "{{ log_task_stdout }}"
461 stderr: "{{ log_task_stderr }}"
462 env:
463 - O2_DETECTOR={{ detector }}
464 - O2_PARTITION={{ environment_id }}
465 - HOME=/tmp
466 user: "{{ user }}"
467 value: "{{ len(modulepath)>0 ? _module_cmdline : _plain_cmdline }}"
468 arguments:
469 - "-b"
470 - "--exit-transition-timeout"
471 - "'{{ exit_transition_timeout }}'"
472 - "--data-processing-timeout"
473 - "'{{ data_processing_timeout }}'"
474 - "--monitoring-backend"
475 - "'{{ monitoring_dpl_url }}'"
476 - "--session"
477 - "'{{ session_id }}'"
478 - "--infologger-severity"
479 - "'{{ infologger_severity }}'"
480 - "--infologger-mode"
481 - "'{{ infologger_mode }}'"
482 - "--driver-client-backend"
483 - "'stdout://'"
484 - "--shm-segment-size"
485 - "'{{ shm_segment_size }}'"
486 - "--shm-throw-bad-alloc"
487 - "'{{ shm_throw_bad_alloc }}'"
488 - "--resources-monitoring"
489 - "'{{ resources_monitoring }}'"
490 - "--id"
491 - "'D'"
492 - "--shm-monitor"
493 - "'false'"
494 - "--log-color"
495 - "'false'"
496 - "--no-batch"
497 - "--bad-alloc-attempt-interval"
498 - "'50'"
499 - "--bad-alloc-max-attempts"
500 - "'1'"
501 - "--channel-prefix"
502 - "''"
503 - "--early-forward-policy"
504 - "'never'"
505 - "--io-threads"
506 - "'1'"
507 - "--jobs"
508 - "'1'"
509 - "--severity"
510 - "'info'"
511 - "--shm-allocation"
512 - "'rbtree_best_fit'"
513 - "--shm-metadata-msg-size"
514 - "'0'"
515 - "--shm-mlock-segment"
516 - "'false'"
517 - "--shm-mlock-segment-on-creation"
518 - "'false'"
519 - "--shm-no-cleanup"
520 - "'false'"
521 - "--shm-segment-id"
522 - "'0'"
523 - "--shm-zero-segment"
524 - "'false'"
525 - "--signposts"
526 - "''"
527 - "--stacktrace-on-signal"
528 - "'simple'"
529 - "--timeframes-rate-limit"
530 - "'0'"
531 - "--a-param"
532 - "'1'"
533 - "--b-param"
534 - "''"
535 - "--c-param"
536 - "'foo;bar'"
537 - "--d-param"
538 - "'[\"foo\",\"bar\"]'"
539)EXPECTED"};
540
541TEST_CASE("TestO2ControlDump")
542{
543 auto workflow = defineDataProcessing();
544 std::ostringstream ss{""};
545 auto configContext = makeEmptyConfigContext();
546 auto channelPolicies = makeTrivialChannelPolicies(*configContext);
547 std::vector<DeviceSpec> devices;
548 std::vector<ComputingResource> resources{ComputingResourceHelpers::getLocalhostResource()};
549 SimpleResourceManager rm(resources);
550 auto completionPolicies = CompletionPolicy::createDefaultPolicies();
551 auto callbacksPolicies = CallbacksPolicy::createDefaultPolicies();
552 DeviceSpecHelpers::dataProcessorSpecs2DeviceSpecs(workflow, channelPolicies, completionPolicies, callbacksPolicies, devices, rm, "workflow-id", *configContext, true);
553 std::vector<DeviceControl> controls;
554 std::vector<DeviceExecution> executions;
555 controls.resize(devices.size());
556 executions.resize(devices.size());
557 CommandInfo commandInfo{R"(o2-exe --abdf -defg 'asdf fdsa' | o2-exe-2 -b --zxcv "asdf zxcv")"};
558
559 std::vector<ConfigParamSpec> workflowOptions = {
560 ConfigParamSpec{"jobs", VariantType::Int, 1, {"number of producer jobs"}}};
561
562 std::vector<DataProcessorInfo> dataProcessorInfos = {
563 {
564 {.name = "A", .executable = "bcsadc/foo", .workflowOptions = workflowOptions},
565 {.name = "B", .executable = "foo", .workflowOptions = workflowOptions},
566 {.name = "C", .executable = "foo", .workflowOptions = workflowOptions},
567 {.name = "D", .executable = "foo", .workflowOptions = workflowOptions},
568 }};
569
570 DriverConfig driverConfig{
571 .batch = false,
572 };
573 DeviceSpecHelpers::prepareArguments(false, false, false, 8080,
574 driverConfig,
575 dataProcessorInfos,
576 devices, executions, controls, {},
577 "workflow-id");
578
579 dumpWorkflow(ss, devices, executions, commandInfo, "testwf", "");
580
581 REQUIRE(strdiffchr(ss.str().data(), expectedWorkflow) == strdiffchr(expectedWorkflow, ss.str().data()));
582 REQUIRE(ss.str() == expectedWorkflow);
583
584 REQUIRE(devices.size() == executions.size());
585 REQUIRE(devices.size() == expectedTasks.size());
586 for (size_t di = 0; di < devices.size(); ++di) {
587 auto& spec = devices[di];
588 auto& expected = expectedTasks[di];
589
590 SECTION("Device " + std::string(spec.name))
591 {
592 ss.str({});
593 ss.clear();
594 dumpTask(ss, devices[di], executions[di], devices[di].name, "");
595 REQUIRE(strdiffchr(ss.str().data(), expected) == strdiffchr(expected, ss.str().data()));
596 REQUIRE(ss.str() == expected);
597 }
598 }
599}
WorkflowSpec defineDataProcessing(ConfigContext const &configcontext)
std::unique_ptr< ConfigContext > makeEmptyConfigContext()
GLuint GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat s1
Definition glcorearb.h:5034
GLuint const GLchar * name
Definition glcorearb.h:781
const decltype(DataProcessorMetadata::key) privateMemoryKillThresholdMB
const decltype(DataProcessorMetadata::key) cpuKillThreshold
Defining PrimaryVertex explicitly as messageable.
Definition TFIDInfo.h:20
void dumpTask(std::ostream &dumpOut, const DeviceSpec &spec, const DeviceExecution &execution, std::string taskName, std::string indLevel)
Dumps only one task.
TEST_CASE("test_prepareArguments")
std::vector< DataProcessorSpec > WorkflowSpec
void dumpWorkflow(std::ostream &dumpOut, const std::vector< DeviceSpec > &specs, const std::vector< DeviceExecution > &executions, const CommandInfo &commandInfo, std::string workflowName, std::string indLevel)
Dumps only the workflow file.
std::vector< InputSpec > Inputs
std::vector< OutputSpec > Outputs
static std::vector< CallbacksPolicy > createDefaultPolicies()
static std::vector< CompletionPolicy > createDefaultPolicies()
Helper to create the default configuration.
static void prepareArguments(bool defaultQuiet, bool defaultStopped, bool intereactive, unsigned short driverPort, DriverConfig const &driverConfig, std::vector< DataProcessorInfo > const &processorInfos, std::vector< DeviceSpec > const &deviceSpecs, std::vector< DeviceExecution > &deviceExecutions, std::vector< DeviceControl > &deviceControls, std::vector< ConfigParamSpec > const &detectedOptions, std::string const &uniqueWorkflowId)
static void dataProcessorSpecs2DeviceSpecs(const WorkflowSpec &workflow, std::vector< ChannelConfigurationPolicy > const &channelPolicies, std::vector< CompletionPolicy > const &completionPolicies, std::vector< DispatchPolicy > const &dispatchPolicies, std::vector< ResourcePolicy > const &resourcePolicies, std::vector< CallbacksPolicy > const &callbacksPolicies, std::vector< SendingPolicy > const &sendingPolicy, std::vector< ForwardingPolicy > const &forwardingPolicies, std::vector< DeviceSpec > &devices, ResourceManager &resourceManager, std::string const &uniqueWorkflowId, ConfigContext const &configContext, bool optimizeTopology=false, unsigned short resourcesMonitoringInterval=0, std::string const &channelPrefix="", OverrideServiceSpecs const &overrideServices={})
bool batch
Whether the driver was started in batch mode or not.
std::vector< ConfigParamSpec > metadata
A set of configurables which can be used to customise the InputSpec.
Definition InputSpec.h:76
std::vector< ConfigParamSpec > metadata
A set of configurables which can be used to customise the InputSpec.
Definition OutputSpec.h:87
std::map< std::string, ID > expected
const auto expectedWorkflow
const std::vector expectedTasks