• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

paulmthompson / WhiskerToolbox / 17846711083

19 Sep 2025 02:28AM UTC coverage: 72.02% (+0.08%) from 71.942%
17846711083

push

github

paulmthompson
event in interval computer works with entity ids

259 of 280 new or added lines in 6 files covered. (92.5%)

268 existing lines in 17 files now uncovered.

40247 of 55883 relevant lines covered (72.02%)

1227.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.25
/src/DataManager/utils/TableView/computers/LineSamplingMultiComputer.test.cpp
1
#include <catch2/catch_approx.hpp>
2
#include <catch2/catch_test_macros.hpp>
3
#include <catch2/matchers/catch_matchers_floating_point.hpp>
4

5
#include "AnalogTimeSeries/Analog_Time_Series.hpp"
6
#include "DataManager.hpp"
7
#include "DataManagerTypes.hpp"
8
#include "Lines/Line_Data.hpp"
9
#include "Entity/EntityGroupManager.hpp"
10
#include "Entity/EntityRegistry.hpp"
11
#include "Entity/EntityTypes.hpp"
12
#include "CoreGeometry/lines.hpp"
13
#include "CoreGeometry/points.hpp"
14
#include "TimeFrame/TimeFrame.hpp"
15
#include "TimeFrame/StrongTimeTypes.hpp"
16
#include "utils/TableView/ComputerRegistry.hpp"
17
#include "utils/TableView/TableRegistry.hpp"
18
#include "utils/TableView/adapters/DataManagerExtension.h"
19
#include "utils/TableView/adapters/LineDataAdapter.h"
20
#include "utils/TableView/computers/LineSamplingMultiComputer.h"
21
#include "utils/TableView/core/TableView.h"
22
#include "utils/TableView/core/TableViewBuilder.h"
23
#include "utils/TableView/interfaces/ILineSource.h"
24
#include "utils/TableView/interfaces/IRowSelector.h"
25
#include "utils/TableView/pipeline/TablePipeline.hpp"
26

27
#include <cstdint>
28
#include <iostream>
29
#include <memory>
30
#include <nlohmann/json.hpp>
31
#include <numeric>
32
#include <vector>
33

34
TEST_CASE("DM - TV - LineSamplingMultiComputer basic integration", "[LineSamplingMultiComputer]") {
1✔
35
    // Build a simple DataManager and inject LineData
36
    DataManager dm;
1✔
37

38
    // Create a TimeFrame with 3 timestamps
39
    std::vector<int> timeValues = {0, 1, 2};
3✔
40
    auto tf = std::make_shared<TimeFrame>(timeValues);
1✔
41

42
    // Create LineData and add one simple line at each timestamp
43
    auto lineData = std::make_shared<LineData>();
1✔
44
    lineData->setTimeFrame(tf);
1✔
45

46
    // simple polyline: (0,0) -> (10,0)
47
    {
48
        std::vector<float> xs = {0.0f, 10.0f};
3✔
49
        std::vector<float> ys = {0.0f, 0.0f};
3✔
50
        lineData->addAtTime(TimeFrameIndex(0), xs, ys, false);
1✔
51
        lineData->addAtTime(TimeFrameIndex(1), xs, ys, false);
1✔
52
        lineData->addAtTime(TimeFrameIndex(2), xs, ys, false);
1✔
53
    }
1✔
54

55
    // Install into DataManager under a key (emulate registry storage)
56
    // The DataManager API in this project typically uses typed storage;
57
    // if there is no direct setter, we can directly adapt via LineDataAdapter below.
58

59
    // Create DataManagerExtension
60
    DataManagerExtension dme(dm);
1✔
61

62
    // Create a TableView with Timestamp rows [0,1,2]
63
    std::vector<TimeFrameIndex> timestamps = {TimeFrameIndex(0), TimeFrameIndex(1), TimeFrameIndex(2)};
3✔
64
    auto rowSelector = std::make_unique<TimestampSelector>(timestamps, tf);
1✔
65

66
    // Build LineDataAdapter directly (bypassing DataManager registry) and wrap as ILineSource
67
    auto lineAdapter = std::make_shared<LineDataAdapter>(lineData, tf, std::string{"TestLines"});
3✔
68

69
    // Directly construct the multi-output computer (interface-level test)
70
    int segments = 2;// positions: 0.0, 0.5, 1.0 => 6 outputs (x,y per position)
1✔
71
    auto multi = std::make_unique<LineSamplingMultiComputer>(
1✔
72
        std::static_pointer_cast<ILineSource>(lineAdapter),
2✔
73
        std::string{"TestLines"},
4✔
74
        tf,
75
            segments);
3✔
76

77
    // Build the table with addColumns
78
    auto dme_ptr = std::make_shared<DataManagerExtension>(dme);
1✔
79
    TableViewBuilder builder(dme_ptr);
1✔
80
    builder.setRowSelector(std::move(rowSelector));
1✔
81
    builder.addColumns<double>("Line", std::move(multi));
3✔
82

83
    auto table = builder.build();
1✔
84

85
    // Expect 6 columns: x@0.000, y@0.000, x@0.500, y@0.500, x@1.000, y@1.000
86
    auto names = table.getColumnNames();
1✔
87
    REQUIRE(names.size() == 6);
1✔
88

89
    // Validate simple geometry on the straight line: y is 0 everywhere; x progresses 0,5,10
90
    // x@0.000
91
    {
92
        auto const & xs0 = table.getColumnValues<double>("Line.x@0.000");
3✔
93
        REQUIRE(xs0.size() == 3);
1✔
94
        REQUIRE(xs0[0] == Catch::Approx(0.0));
1✔
95
        REQUIRE(xs0[1] == Catch::Approx(0.0));
1✔
96
        REQUIRE(xs0[2] == Catch::Approx(0.0));
1✔
97
    }
98
    // x@0.500
99
    {
100
        auto const & xsMid = table.getColumnValues<double>("Line.x@0.500");
3✔
101
        REQUIRE(xsMid.size() == 3);
1✔
102
        REQUIRE(xsMid[0] == Catch::Approx(5.0));
1✔
103
        REQUIRE(xsMid[1] == Catch::Approx(5.0));
1✔
104
        REQUIRE(xsMid[2] == Catch::Approx(5.0));
1✔
105
    }
106
    // x@1.000
107
    {
108
        auto const & xs1 = table.getColumnValues<double>("Line.x@1.000");
3✔
109
        REQUIRE(xs1.size() == 3);
1✔
110
        REQUIRE(xs1[0] == Catch::Approx(10.0));
1✔
111
        REQUIRE(xs1[1] == Catch::Approx(10.0));
1✔
112
        REQUIRE(xs1[2] == Catch::Approx(10.0));
1✔
113
    }
114

115
    // y columns should be zeros
116
    {
117
        auto const & ys0 = table.getColumnValues<double>("Line.y@0.000");
3✔
118
        auto const & ysMid = table.getColumnValues<double>("Line.y@0.500");
3✔
119
        auto const & ys1 = table.getColumnValues<double>("Line.y@1.000");
3✔
120
        REQUIRE(ys0.size() == 3);
1✔
121
        REQUIRE(ysMid.size() == 3);
1✔
122
        REQUIRE(ys1.size() == 3);
1✔
123
        for (size_t i = 0; i < 3; ++i) {
4✔
124
            REQUIRE(ys0[i] == Catch::Approx(0.0));
3✔
125
            REQUIRE(ysMid[i] == Catch::Approx(0.0));
3✔
126
            REQUIRE(ys1[i] == Catch::Approx(0.0));
3✔
127
        }
128
    }
129
}
2✔
130

131
TEST_CASE("DM - TV - LineSamplingMultiComputer handles missing lines as zeros", "[LineSamplingMultiComputer]") {
1✔
132
    DataManager dm;
1✔
133

134
    std::vector<int> timeValues = {0, 1, 2};
3✔
135
    auto tf = std::make_shared<TimeFrame>(timeValues);
1✔
136

137
    auto lineData = std::make_shared<LineData>();
1✔
138
    lineData->setTimeFrame(tf);
1✔
139

140
    // Add a line at t=0 and t=2 only; t=1 has no lines
141
    std::vector<float> xs = {0.0f, 10.0f};
3✔
142
    std::vector<float> ys = {0.0f, 0.0f};
3✔
143
    lineData->addAtTime(TimeFrameIndex(0), xs, ys, false);
1✔
144
    lineData->addAtTime(TimeFrameIndex(2), xs, ys, false);
1✔
145

146
    auto lineAdapter = std::make_shared<LineDataAdapter>(lineData, tf, std::string{"TestLinesMissing"});
3✔
147

148
    std::vector<TimeFrameIndex> timestamps = {TimeFrameIndex(0), TimeFrameIndex(1), TimeFrameIndex(2)};
3✔
149
    auto rowSelector = std::make_unique<TimestampSelector>(timestamps, tf);
1✔
150

151
    auto dme_ptr = std::make_shared<DataManagerExtension>(dm);
1✔
152
    TableViewBuilder builder(dme_ptr);
1✔
153
    builder.setRowSelector(std::move(rowSelector));
1✔
154

155
    // Build multi-computer directly
156
    auto multi = std::make_unique<LineSamplingMultiComputer>(
1✔
157
        std::static_pointer_cast<ILineSource>(lineAdapter),
2✔
158
        std::string{"TestLinesMissing"},
4✔
159
        tf,
160
            2);
4✔
161
    builder.addColumns<double>("Line", std::move(multi));
3✔
162

163
    auto table = builder.build();
1✔
164

165
    // At t=1 (middle row), expect zeros
166
    auto const & xs0 = table.getColumnValues<double>("Line.x@0.000");
3✔
167
    auto const & ys0 = table.getColumnValues<double>("Line.y@0.000");
3✔
168
    auto const & xsMid = table.getColumnValues<double>("Line.x@0.500");
3✔
169
    auto const & ysMid = table.getColumnValues<double>("Line.y@0.500");
3✔
170
    auto const & xs1 = table.getColumnValues<double>("Line.x@1.000");
3✔
171
    auto const & ys1 = table.getColumnValues<double>("Line.y@1.000");
3✔
172

173
    REQUIRE(xs0.size() == 3);
1✔
174
    REQUIRE(ys0.size() == 3);
1✔
175
    REQUIRE(xsMid.size() == 3);
1✔
176
    REQUIRE(ysMid.size() == 3);
1✔
177
    REQUIRE(xs1.size() == 3);
1✔
178
    REQUIRE(ys1.size() == 3);
1✔
179

180
    REQUIRE(xs0[1] == Catch::Approx(0.0));
1✔
181
    REQUIRE(ys0[1] == Catch::Approx(0.0));
1✔
182
    REQUIRE(xsMid[1] == Catch::Approx(0.0));
1✔
183
    REQUIRE(ysMid[1] == Catch::Approx(0.0));
1✔
184
    REQUIRE(xs1[1] == Catch::Approx(0.0));
1✔
185
    REQUIRE(ys1[1] == Catch::Approx(0.0));
1✔
186
}
2✔
187

188
TEST_CASE("DM - TV - LineSamplingMultiComputer can be created via registry", "[LineSamplingMultiComputer][Registry]") {
1✔
189
    DataManager dm;
1✔
190

191
    std::vector<int> timeValues = {0, 1};
3✔
192
    auto tf = std::make_shared<TimeFrame>(timeValues);
1✔
193

194
    auto lineData = std::make_shared<LineData>();
1✔
195
    lineData->setTimeFrame(tf);
1✔
196
    std::vector<float> xs = {0.0f, 10.0f};
3✔
197
    std::vector<float> ys = {0.0f, 0.0f};
3✔
198
    lineData->addAtTime(TimeFrameIndex(0), xs, ys, false);
1✔
199
    lineData->addAtTime(TimeFrameIndex(1), xs, ys, false);
1✔
200

201
    auto lineAdapter = std::make_shared<LineDataAdapter>(lineData, tf, std::string{"RegLines"});
3✔
202

203
    // Create DataSourceVariant via registry adapter to ensure consistent type usage
204
    ComputerRegistry registry;
1✔
205
    auto adapted = registry.createAdapter(
1✔
206
        "Line Data",
207
        std::static_pointer_cast<void>(lineData),
2✔
208
        tf,
209
        std::string{"RegLines"},
4✔
210
            {});
6✔
211
    // Diagnostics
212
    {
213
        auto adapter_names = registry.getAllAdapterNames();
1✔
214
        std::cout << "Registered adapters (" << adapter_names.size() << ")" << std::endl;
1✔
215
        for (auto const & n: adapter_names) {
4✔
216
            std::cout << "  Adapter: " << n << std::endl;
3✔
217
        }
218
        std::cout << "Adapted variant index: " << adapted.index() << std::endl;
1✔
219
    }
1✔
220
    // Fallback to direct adapter if registry adapter not found (should not happen after registration)
221
    DataSourceVariant variant = adapted.index() != std::variant_npos ? adapted
1✔
222
                               : DataSourceVariant{std::static_pointer_cast<ILineSource>(lineAdapter)};
1✔
223

224
    // More diagnostics: list available computers
225
    {
226
        auto comps = registry.getAvailableComputers(RowSelectorType::Timestamp, variant);
1✔
227
        std::cout << "Available computers for Timestamp + variant(" << variant.index() << ") = " << comps.size() << std::endl;
1✔
228
        for (auto const & ci: comps) {
2✔
229
            std::cout << "  Computer: " << ci.name
1✔
230
                      << ", isMultiOutput=" << (ci.isMultiOutput ? "true" : "false")
1✔
231
                      << ", requiredSourceType=" << ci.requiredSourceType.name() << std::endl;
1✔
232
        }
233
        auto info = registry.findComputerInfo("Line Sample XY");
3✔
234
        if (info) {
1✔
235
            std::cout << "Found computer info for 'Line Sample XY' with requiredSourceType=" << info->requiredSourceType.name()
236
                      << ", rowSelector=" << static_cast<int>(info->requiredRowSelector)
1✔
237
                      << ", isMultiOutput=" << (info->isMultiOutput ? "true" : "false") << std::endl;
1✔
238
        } else {
UNCOV
239
            std::cout << "Did not find computer info for 'Line Sample XY'" << std::endl;
×
240
        }
241
    }
1✔
242

243
    // Create via registry
244
    std::map<std::string, std::string> params{{"segments", "2"}};
5✔
245
    auto multi = registry.createTypedMultiComputer<double>("Line Sample XY", variant, params);
3✔
246
    REQUIRE(multi != nullptr);
1✔
247

248
    // Build with builder
249
    auto dme_ptr = std::make_shared<DataManagerExtension>(dm);
1✔
250
    std::vector<TimeFrameIndex> timestamps = {TimeFrameIndex(0), TimeFrameIndex(1)};
3✔
251
    auto rowSelector = std::make_unique<TimestampSelector>(timestamps, tf);
1✔
252

253
    TableViewBuilder builder(dme_ptr);
1✔
254
    builder.setRowSelector(std::move(rowSelector));
1✔
255
    builder.addColumns<double>("Line", std::move(multi));
3✔
256
    auto table = builder.build();
1✔
257

258
    auto names = table.getColumnNames();
1✔
259
    REQUIRE(names.size() == 6);
1✔
260
}
3✔
261

262
TEST_CASE("DM - TV - LineSamplingMultiComputer with per-line row expansion drops empty timestamps and samples per entity", "[LineSamplingMultiComputer][Expansion]") {
1✔
263
    DataManager dm;
1✔
264

265
    // Timeframe with 5 timestamps
266
    std::vector<int> timeValues = {0, 1, 2, 3, 4};
3✔
267
    auto tf = std::make_shared<TimeFrame>(timeValues);
1✔
268

269
    // LineData with varying number of lines per timestamp
270
    auto lineData = std::make_shared<LineData>();
1✔
271
    lineData->setTimeFrame(tf);
1✔
272

273
    // t=0: no lines (should be dropped)
274
    // t=1: one horizontal line from x=0..10
275
    {
276
        std::vector<float> xs = {0.0f, 10.0f};
3✔
277
        std::vector<float> ys = {0.0f, 0.0f};
3✔
278
        lineData->addAtTime(TimeFrameIndex(1), xs, ys, false);
1✔
279
    }
1✔
280
    // t=2: two lines; l0 horizontal (x 0..10), l1 vertical (y 0..10)
281
    {
282
        std::vector<float> xs = {0.0f, 10.0f};
3✔
283
        std::vector<float> ys = {0.0f, 0.0f};
3✔
284
        lineData->addAtTime(TimeFrameIndex(2), xs, ys, false);
1✔
285
        std::vector<float> xs2 = {5.0f, 5.0f};
3✔
286
        std::vector<float> ys2 = {0.0f, 10.0f};
3✔
287
        lineData->addAtTime(TimeFrameIndex(2), xs2, ys2, false);
1✔
288
    }
1✔
289
    // t=3: no lines (should be dropped)
290
    // t=4: one vertical line (y 0..10 at x=2)
291
    {
292
        std::vector<float> xs = {2.0f, 2.0f};
3✔
293
        std::vector<float> ys = {0.0f, 10.0f};
3✔
294
        lineData->addAtTime(TimeFrameIndex(4), xs, ys, false);
1✔
295
    }
1✔
296

297
    auto lineAdapter = std::make_shared<LineDataAdapter>(lineData, tf, std::string{"ExpLines"});
3✔
298
    // Register into DataManager so TableView expansion can resolve the line source by name
299
    dm.setData<LineData>("ExpLines", lineData, TimeKey("time"));
3✔
300

301
    // Timestamps include empty ones; expansion should drop t=0 and t=3
302
    std::vector<TimeFrameIndex> timestamps = {
1✔
303
            TimeFrameIndex(0), TimeFrameIndex(1), TimeFrameIndex(2), TimeFrameIndex(3), TimeFrameIndex(4)};
3✔
304
    auto rowSelector = std::make_unique<TimestampSelector>(timestamps, tf);
1✔
305

306
    // Build table
307
    auto dme_ptr = std::make_shared<DataManagerExtension>(dm);
1✔
308
    TableViewBuilder builder(dme_ptr);
1✔
309
    builder.setRowSelector(std::move(rowSelector));
1✔
310

311
    auto multi = std::make_unique<LineSamplingMultiComputer>(
4✔
312
        std::static_pointer_cast<ILineSource>(lineAdapter),
2✔
313
        std::string{"ExpLines"},
2✔
314
        tf,
315
            2// positions 0.0, 0.5, 1.0
1✔
316
    );
3✔
317
    builder.addColumns<double>("Line", std::move(multi));
3✔
318

319
    auto table = builder.build();
1✔
320

321
    // With expansion: expected rows = t1:1 + t2:2 + t4:1 = 4 rows
322
    REQUIRE(table.getRowCount() == 4);
1✔
323

324
    // Column names same structure
325
    auto names = table.getColumnNames();
1✔
326
    REQUIRE(names.size() == 6);
1✔
327

328
    // Validate per-entity sampling ordering as inserted:
329
    // Row 0 -> t=1, the single horizontal line: x@0.5 = 5, y@0.5 = 0
330
    // Row 1 -> t=2, entity 0 (horizontal): x@0.5 = 5, y@0.5 = 0
331
    // Row 2 -> t=2, entity 1 (vertical):   x@0.5 = 5, y@0.5 = 5
332
    // Row 3 -> t=4, the single vertical line at x=2: x@0.5 = 2, y@0.5 = 5
333
    auto const & xsMid = table.getColumnValues<double>("Line.x@0.500");
3✔
334
    auto const & ysMid = table.getColumnValues<double>("Line.y@0.500");
3✔
335
    REQUIRE(xsMid.size() == 4);
1✔
336
    REQUIRE(ysMid.size() == 4);
1✔
337

338
    REQUIRE(xsMid[0] == Catch::Approx(5.0));
1✔
339
    REQUIRE(ysMid[0] == Catch::Approx(0.0));
1✔
340

341
    REQUIRE(xsMid[1] == Catch::Approx(5.0));
1✔
342
    REQUIRE(ysMid[1] == Catch::Approx(0.0));
1✔
343

344
    REQUIRE(xsMid[2] == Catch::Approx(5.0));
1✔
345
    REQUIRE(ysMid[2] == Catch::Approx(5.0));
1✔
346

347
    REQUIRE(xsMid[3] == Catch::Approx(2.0));
1✔
348
    REQUIRE(ysMid[3] == Catch::Approx(5.0));
1✔
349
}
2✔
350

351
TEST_CASE("DM - TV - LineSamplingMultiComputer expansion with coexisting analog column retains empty-line timestamps for analog", "[LineSamplingMultiComputer][Expansion][AnalogBroadcast]") {
1✔
352
    DataManager dm;
1✔
353

354
    std::vector<int> timeValues = {0, 1, 2, 3};
3✔
355
    auto tf = std::make_shared<TimeFrame>(timeValues);
1✔
356

357
    // LineData: only at t=1
358
    auto lineData = std::make_shared<LineData>();
1✔
359
    lineData->setTimeFrame(tf);
1✔
360
    {
361
        std::vector<float> xs = {0.0f, 10.0f};
3✔
362
        std::vector<float> ys = {1.0f, 1.0f};
3✔
363
        lineData->addAtTime(TimeFrameIndex(1), xs, ys, false);
1✔
364
    }
1✔
365
    dm.setData<LineData>("MixedLines", lineData, TimeKey("time"));
3✔
366

367
    // Analog data present at all timestamps: values 0,10,20,30
368
    std::vector<float> analogVals = {0.f, 10.f, 20.f, 30.f};
3✔
369
    std::vector<TimeFrameIndex> analogTimes = {TimeFrameIndex(0), TimeFrameIndex(1), TimeFrameIndex(2), TimeFrameIndex(3)};
3✔
370
    auto analogData = std::make_shared<AnalogTimeSeries>(analogVals, analogTimes);
1✔
371
    dm.setData<AnalogTimeSeries>("AnalogA", analogData, TimeKey("time"));
3✔
372

373
    // Build selector across all timestamps
374
    std::vector<TimeFrameIndex> timestamps = {TimeFrameIndex(0), TimeFrameIndex(1), TimeFrameIndex(2), TimeFrameIndex(3)};
3✔
375
    auto rowSelector = std::make_unique<TimestampSelector>(timestamps, tf);
1✔
376

377
    auto dme_ptr = std::make_shared<DataManagerExtension>(dm);
1✔
378
    TableViewBuilder builder(dme_ptr);
1✔
379
    builder.setRowSelector(std::move(rowSelector));
1✔
380

381
    // Multi-line columns (expanding)
382
    auto lineAdapter = std::make_shared<LineDataAdapter>(lineData, tf, std::string{"MixedLines"});
3✔
383
    auto multi = std::make_unique<LineSamplingMultiComputer>(
4✔
384
        std::static_pointer_cast<ILineSource>(lineAdapter),
2✔
385
        std::string{"MixedLines"},
2✔
386
        tf,
387
            2);
4✔
388
    builder.addColumns<double>("Line", std::move(multi));
3✔
389

390
    // Analog timestamp value column
391
    // Use registry to create the computer; simpler path: direct TimestampValueComputer
392
    // but we need an IAnalogSource from DataManagerExtension, resolved by name "AnalogA"
393
    class SimpleTimestampValueComputer : public IColumnComputer<double> {
394
    public:
395
        explicit SimpleTimestampValueComputer(std::shared_ptr<IAnalogSource> src)
1✔
396
            : src_(std::move(src)) {}
1✔
397
        [[nodiscard]] auto compute(ExecutionPlan const & plan) const -> std::vector<double> override {
1✔
398
            std::vector<TimeFrameIndex> idx;
1✔
399
            if (plan.hasIndices()) {
1✔
400
                idx = plan.getIndices();
1✔
401
            } else {
402
                // Build from rows (expanded)
UNCOV
403
                for (auto const & r: plan.getRows()) idx.push_back(r.timeIndex);
×
404
            }
405
            std::vector<double> out(idx.size(), 0.0);
3✔
406
            // naive: use AnalogDataAdapter semantics: value == index*10
407
            for (size_t i = 0; i < idx.size(); ++i) out[i] = static_cast<double>(idx[i].getValue() * 10);
5✔
408
            return out;
2✔
409
        }
1✔
410
        [[nodiscard]] auto getSourceDependency() const -> std::string override { return src_ ? src_->getName() : std::string{"AnalogA"}; }
6✔
411

412
    private:
413
        std::shared_ptr<IAnalogSource> src_;
414
    };
415

416
    auto analogSrc = dme_ptr->getAnalogSource("AnalogA");
3✔
417
    REQUIRE(analogSrc != nullptr);
1✔
418
    auto analogComp = std::make_unique<SimpleTimestampValueComputer>(analogSrc);
1✔
419
    builder.addColumn<double>("Analog", std::move(analogComp));
3✔
420

421
    auto table = builder.build();
1✔
422

423
    // Expect expanded rows keep all timestamps due to coexisting analog column: t=0,1,2,3 -> 4 rows
424
    // Line columns will have zero for t=0,2,3 where no line exists; analog column has 0,10,20,30
425
    REQUIRE(table.getRowCount() == 4);
1✔
426
    auto const & xsMid = table.getColumnValues<double>("Line.x@0.500");
3✔
427
    auto const & ysMid = table.getColumnValues<double>("Line.y@0.500");
3✔
428
    auto const & analog = table.getColumnValues<double>("Analog");
3✔
429
    REQUIRE(xsMid.size() == 4);
1✔
430
    REQUIRE(ysMid.size() == 4);
1✔
431
    REQUIRE(analog.size() == 4);
1✔
432

433
    // At t=1 (row 1), line exists; others should be zeros for line columns
434
    REQUIRE(xsMid[0] == Catch::Approx(0.0));
1✔
435
    REQUIRE(ysMid[0] == Catch::Approx(0.0));
1✔
436
    REQUIRE(xsMid[1] == Catch::Approx(5.0));
1✔
437
    REQUIRE(ysMid[1] == Catch::Approx(1.0));
1✔
438
    REQUIRE(xsMid[2] == Catch::Approx(0.0));
1✔
439
    REQUIRE(ysMid[2] == Catch::Approx(0.0));
1✔
440
    REQUIRE(xsMid[3] == Catch::Approx(0.0));
1✔
441
    REQUIRE(ysMid[3] == Catch::Approx(0.0));
1✔
442

443
    REQUIRE(analog[0] == Catch::Approx(0.0));
1✔
444
    REQUIRE(analog[1] == Catch::Approx(10.0));
1✔
445
    REQUIRE(analog[2] == Catch::Approx(20.0));
1✔
446
    REQUIRE(analog[3] == Catch::Approx(30.0));
1✔
447
}
2✔
448

449
/**
450
 * @brief Base test fixture for LineSamplingMultiComputer with realistic line data
451
 * 
452
 * This fixture provides a DataManager populated with:
453
 * - TimeFrames with different granularities
454
 * - Line data representing whisker traces or geometric features
455
 * - Multiple lines per timestamp for testing entity expansion
456
 * - Cross-timeframe scenarios for testing timeframe conversion
457
 */
458
class LineSamplingTestFixture {
459
protected:
460
    LineSamplingTestFixture() {
8✔
461
        // Initialize the DataManager
462
        m_data_manager = std::make_unique<DataManager>();
8✔
463

464
        // Populate with test data
465
        populateWithLineTestData();
8✔
466
    }
8✔
467

468
    ~LineSamplingTestFixture() = default;
8✔
469

470
    /**
471
     * @brief Get the DataManager instance
472
     */
473
    DataManager & getDataManager() { return *m_data_manager; }
16✔
474
    DataManager const & getDataManager() const { return *m_data_manager; }
475
    DataManager * getDataManagerPtr() { return m_data_manager.get(); }
476

477
private:
478
    std::unique_ptr<DataManager> m_data_manager;
479

480
    /**
481
     * @brief Populate the DataManager with line test data
482
     */
483
    void populateWithLineTestData() {
8✔
484
        createTimeFrames();
8✔
485
        createWhiskerTraces();
8✔
486
        createGeometricShapes();
8✔
487
    }
8✔
488

489
    /**
490
     * @brief Create TimeFrame objects for different data streams
491
     */
492
    void createTimeFrames() {
8✔
493
        // Create "whisker_time" timeframe: 0 to 100 (101 points) - whisker tracking at high frequency
494
        std::vector<int> whisker_time_values(101);
24✔
495
        std::iota(whisker_time_values.begin(), whisker_time_values.end(), 0);
8✔
496
        auto whisker_time_frame = std::make_shared<TimeFrame>(whisker_time_values);
8✔
497
        m_data_manager->setTime(TimeKey("whisker_time"), whisker_time_frame, true);
8✔
498

499
        // Create "shape_time" timeframe: 0, 10, 20, 30, ..., 100 (11 points) - geometric shapes at lower frequency
500
        std::vector<int> shape_time_values;
8✔
501
        shape_time_values.reserve(11);
8✔
502
        for (int i = 0; i <= 10; ++i) {
96✔
503
            shape_time_values.push_back(i * 10);
88✔
504
        }
505
        auto shape_time_frame = std::make_shared<TimeFrame>(shape_time_values);
8✔
506
        m_data_manager->setTime(TimeKey("shape_time"), shape_time_frame, true);
8✔
507
    }
16✔
508

509
    /**
510
     * @brief Create whisker trace data (complex curved lines)
511
     */
512
    void createWhiskerTraces() {
8✔
513
        // Create whisker traces with varying curvature and length
514
        auto whisker_lines = std::make_shared<LineData>();
8✔
515

516
        // Create curved whisker traces at different time points
517
        for (int t = 10; t <= 90; t += 20) {
48✔
518
            // Primary whisker - curved arc
519
            std::vector<float> xs, ys;
40✔
520
            for (int i = 0; i <= 20; ++i) {
880✔
521
                float s = static_cast<float>(i) / 20.0f;
840✔
522
                float x = s * 100.0f;
840✔
523
                float y = 20.0f * std::sin(s * 3.14159f / 2.0f) * (1.0f + 0.1f * static_cast<float>(t) / 100.0f);
840✔
524
                xs.push_back(x);
840✔
525
                ys.push_back(y);
840✔
526
            }
527
            whisker_lines->addAtTime(TimeFrameIndex(t), xs, ys, false);
40✔
528

529
            // Secondary whisker - smaller arc below
530
            if (t >= 30) {
40✔
531
                std::vector<float> xs2, ys2;
32✔
532
                for (int i = 0; i <= 15; ++i) {
544✔
533
                    float s = static_cast<float>(i) / 15.0f;
512✔
534
                    float x = s * 75.0f;
512✔
535
                    float y = -10.0f - 15.0f * std::sin(s * 3.14159f / 3.0f);
512✔
536
                    xs2.push_back(x);
512✔
537
                    ys2.push_back(y);
512✔
538
                }
539
                whisker_lines->addAtTime(TimeFrameIndex(t), xs2, ys2, false);
32✔
540
            }
32✔
541
        }
40✔
542

543
        m_data_manager->setData<LineData>("WhiskerTraces", whisker_lines, TimeKey("whisker_time"));
24✔
544
    }
16✔
545

546
    /**
547
     * @brief Create geometric shape data (simple geometric lines)
548
     */
549
    void createGeometricShapes() {
8✔
550
        // Create geometric shapes with different patterns
551
        auto shape_lines = std::make_shared<LineData>();
8✔
552

553
        // Square at t=0
554
        {
555
            std::vector<float> xs = {0.0f, 10.0f, 10.0f, 0.0f, 0.0f};
24✔
556
            std::vector<float> ys = {0.0f, 0.0f, 10.0f, 10.0f, 0.0f};
24✔
557
            shape_lines->addAtTime(TimeFrameIndex(0), xs, ys, false);
8✔
558
        }
8✔
559

560
        // Triangle at t=20
561
        {
562
            std::vector<float> xs = {5.0f, 10.0f, 0.0f, 5.0f};
24✔
563
            std::vector<float> ys = {0.0f, 10.0f, 10.0f, 0.0f};
24✔
564
            shape_lines->addAtTime(TimeFrameIndex(2), xs, ys, false);
8✔
565
        }
8✔
566

567
        // Circle (octagon approximation) at t=40
568
        {
569
            std::vector<float> xs, ys;
8✔
570
            for (int i = 0; i <= 8; ++i) {
80✔
571
                float angle = static_cast<float>(i) * 2.0f * 3.14159f / 8.0f;
72✔
572
                xs.push_back(5.0f + 5.0f * std::cos(angle));
72✔
573
                ys.push_back(5.0f + 5.0f * std::sin(angle));
72✔
574
            }
575
            shape_lines->addAtTime(TimeFrameIndex(4), xs, ys, false);
8✔
576
        }
8✔
577

578
        // Multiple shapes at different times - star at t=60, circle at t=80
579
        {
580
            // Star shape at t=60
581
            std::vector<float> xs1, ys1;
8✔
582
            for (int i = 0; i <= 10; ++i) {
96✔
583
                float angle = static_cast<float>(i) * 2.0f * 3.14159f / 10.0f;
88✔
584
                float radius = (i % 2 == 0) ? 8.0f : 4.0f;
88✔
585
                xs1.push_back(15.0f + radius * std::cos(angle));
88✔
586
                ys1.push_back(15.0f + radius * std::sin(angle));
88✔
587
            }
588
            shape_lines->addAtTime(TimeFrameIndex(6), xs1, ys1, false);
8✔
589

590
            // Small circle at t=80
591
            std::vector<float> xs2, ys2;
8✔
592
            for (int i = 0; i <= 6; ++i) {
64✔
593
                float angle = static_cast<float>(i) * 2.0f * 3.14159f / 6.0f;
56✔
594
                xs2.push_back(25.0f + 3.0f * std::cos(angle));
56✔
595
                ys2.push_back(25.0f + 3.0f * std::sin(angle));
56✔
596
            }
597
            shape_lines->addAtTime(TimeFrameIndex(8), xs2, ys2, false);
8✔
598
        }
8✔
599

600
        m_data_manager->setData<LineData>("GeometricShapes", shape_lines, TimeKey("shape_time"));
24✔
601
    }
16✔
602
};
603

604
/**
605
 * @brief Test fixture combining LineSamplingTestFixture with TableRegistry and TablePipeline
606
 * 
607
 * This fixture provides everything needed to test JSON-based table pipeline execution:
608
 * - DataManager with line test data (from LineSamplingTestFixture)
609
 * - TableRegistry for managing table configurations
610
 * - TablePipeline for executing JSON configurations
611
 */
612
class LineSamplingTableRegistryTestFixture : public LineSamplingTestFixture {
613
protected:
614
    LineSamplingTableRegistryTestFixture()
6✔
615
        : LineSamplingTestFixture() {
6✔
616
        // Use the DataManager's existing TableRegistry instead of creating a new one
617
        m_table_registry_ptr = getDataManager().getTableRegistry();
6✔
618

619
        // Initialize TablePipeline with the existing TableRegistry
620
        m_table_pipeline = std::make_unique<TablePipeline>(m_table_registry_ptr, &getDataManager());
6✔
621
    }
6✔
622

623
    ~LineSamplingTableRegistryTestFixture() = default;
6✔
624

625
    /**
626
     * @brief Get the TableRegistry instance
627
     * @return Reference to the TableRegistry
628
     */
629
    TableRegistry & getTableRegistry() { return *m_table_registry_ptr; }
6✔
630

631
    /**
632
     * @brief Get the TableRegistry instance (const version)
633
     * @return Const reference to the TableRegistry
634
     */
635
    TableRegistry const & getTableRegistry() const { return *m_table_registry_ptr; }
636

637
    /**
638
     * @brief Get a pointer to the TableRegistry
639
     * @return Raw pointer to the TableRegistry
640
     */
641
    TableRegistry * getTableRegistryPtr() { return m_table_registry_ptr; }
642

643
    /**
644
     * @brief Get the TablePipeline instance
645
     * @return Reference to the TablePipeline
646
     */
647
    TablePipeline & getTablePipeline() { return *m_table_pipeline; }
3✔
648

649
    /**
650
     * @brief Get the TablePipeline instance (const version)
651
     * @return Const reference to the TablePipeline
652
     */
653
    TablePipeline const & getTablePipeline() const { return *m_table_pipeline; }
654

655
    /**
656
     * @brief Get a pointer to the TablePipeline
657
     * @return Raw pointer to the TablePipeline
658
     */
659
    TablePipeline * getTablePipelinePtr() { return m_table_pipeline.get(); }
660

661
    /**
662
     * @brief Get the DataManagerExtension instance
663
     */
664
    std::shared_ptr<DataManagerExtension> getDataManagerExtension() {
665
        if (!m_data_manager_extension) {
666
            m_data_manager_extension = std::make_shared<DataManagerExtension>(getDataManager());
667
        }
668
        return m_data_manager_extension;
669
    }
670

671
private:
672
    TableRegistry * m_table_registry_ptr;// Points to DataManager's TableRegistry
673
    std::unique_ptr<TablePipeline> m_table_pipeline;
674
    std::shared_ptr<DataManagerExtension> m_data_manager_extension;// Lazy-initialized
675
};
676

677
TEST_CASE_METHOD(LineSamplingTestFixture, "DM - TV - LineSamplingMultiComputer with DataManager fixture", "[LineSamplingMultiComputer][DataManager][Fixture]") {
2✔
678

679
    SECTION("Test with whisker trace data from fixture") {
2✔
680
        auto & dm = getDataManager();
1✔
681
        auto dme = std::make_shared<DataManagerExtension>(dm);
1✔
682

683
        // Get the line source from the DataManager
684
        auto whisker_source = dme->getLineSource("WhiskerTraces");
3✔
685

686
        REQUIRE(whisker_source != nullptr);
1✔
687

688
        // Create row selector from timestamps where whisker data exists
689
        auto whisker_time_frame = dm.getTime(TimeKey("whisker_time"));
1✔
690
        std::vector<TimeFrameIndex> timestamps = {
1✔
691
                TimeFrameIndex(10), TimeFrameIndex(30), TimeFrameIndex(50), TimeFrameIndex(70), TimeFrameIndex(90)};
3✔
692

693
        auto row_selector = std::make_unique<TimestampSelector>(timestamps, whisker_time_frame);
1✔
694

695
        // Create TableView builder
696
        TableViewBuilder builder(dme);
1✔
697
        builder.setRowSelector(std::move(row_selector));
1✔
698

699
        // Add LineSamplingMultiComputer with different segment counts
700
        auto multi_3seg = std::make_unique<LineSamplingMultiComputer>(
1✔
701
                whisker_source, "WhiskerTraces", whisker_time_frame, 3);
1✔
702
        builder.addColumns<double>("Whisker", std::move(multi_3seg));
3✔
703

704
                // Build the table
705
        TableView table = builder.build();
1✔
706
        
707
        // Verify table structure - 4 segments = 8 columns (x,y per position: 0.0, 0.333, 0.667, 1.0)
708
        // Expected rows: t=10(1) + t=30(2) + t=50(2) + t=70(2) + t=90(2) = 9 rows due to entity expansion
709
        REQUIRE(table.getRowCount() == 9);
1✔
710
        REQUIRE(table.getColumnCount() == 8);
1✔
711

712
        auto column_names = table.getColumnNames();
1✔
713
        REQUIRE(column_names.size() == 8);
1✔
714

715
        // Verify expected column names
716
        REQUIRE(table.hasColumn("Whisker.x@0.000"));
3✔
717
        REQUIRE(table.hasColumn("Whisker.y@0.000"));
3✔
718
        REQUIRE(table.hasColumn("Whisker.x@0.333"));
3✔
719
        REQUIRE(table.hasColumn("Whisker.y@0.333"));
3✔
720
        REQUIRE(table.hasColumn("Whisker.x@0.667"));
3✔
721
        REQUIRE(table.hasColumn("Whisker.y@0.667"));
3✔
722
        REQUIRE(table.hasColumn("Whisker.x@1.000"));
3✔
723
        REQUIRE(table.hasColumn("Whisker.y@1.000"));
3✔
724

725
                // Get sample data to verify reasonable values
726
        auto x_start = table.getColumnValues<double>("Whisker.x@0.000");
3✔
727
        auto y_start = table.getColumnValues<double>("Whisker.y@0.000");
3✔
728
        auto x_end = table.getColumnValues<double>("Whisker.x@1.000");
3✔
729
        auto y_end = table.getColumnValues<double>("Whisker.y@1.000");
3✔
730
        
731
        REQUIRE(x_start.size() == 9);
1✔
732
        REQUIRE(y_start.size() == 9);
1✔
733
        REQUIRE(x_end.size() == 9);
1✔
734
        REQUIRE(y_end.size() == 9);
1✔
735
        
736
        // Verify that whisker curves start at x=0 and end at x=100 (for primary whiskers)
737
        // or x=0 and x=75 (for secondary whiskers)
738
        for (size_t i = 0; i < 9; ++i) {
10✔
739
            REQUIRE(x_start[i] == Catch::Approx(0.0));
9✔
740
            // Primary whiskers end at x=100, secondary at x=75
741
            REQUIRE((x_end[i] == Catch::Approx(100.0) || x_end[i] == Catch::Approx(75.0)));
9✔
742
            // Y values should be reasonable for curved whiskers
743
            REQUIRE(std::abs(y_start[i]) >= 0.0);
9✔
744
        }
745
    }
3✔
746

747
    SECTION("Test with geometric shape data and multiple entities") {
2✔
748
        auto & dm = getDataManager();
1✔
749
        auto dme = std::make_shared<DataManagerExtension>(dm);
1✔
750

751
        // Get the shape source
752
        auto shape_source = dme->getLineSource("GeometricShapes");
3✔
753
        REQUIRE(shape_source != nullptr);
1✔
754

755
        // Create row selector for shape timestamps
756
        auto shape_time_frame = dm.getTime(TimeKey("shape_time"));
1✔
757
        std::vector<TimeFrameIndex> timestamps = {
1✔
758
                TimeFrameIndex(0), TimeFrameIndex(2), TimeFrameIndex(4), TimeFrameIndex(6)};
3✔
759

760
        auto row_selector = std::make_unique<TimestampSelector>(timestamps, shape_time_frame);
1✔
761

762
        TableViewBuilder builder(dme);
1✔
763
        builder.setRowSelector(std::move(row_selector));
1✔
764

765
        // Add LineSamplingMultiComputer with 1 segment (start and end points only)
766
        auto multi_1seg = std::make_unique<LineSamplingMultiComputer>(
1✔
767
                shape_source, "GeometricShapes", shape_time_frame, 1);
1✔
768
        builder.addColumns<double>("Shape", std::move(multi_1seg));
3✔
769

770
        TableView table = builder.build();
1✔
771

772
        // Should have 4 rows: square(1) + triangle(1) + circle(1) + star(1) = 4 rows
773
        // Note: The circle was moved to TimeFrameIndex(8) to avoid multiple entities at same timestamp
774
        REQUIRE(table.getRowCount() == 4);
1✔
775
        REQUIRE(table.getColumnCount() == 4);// 2 positions * 2 coordinates = 4 columns
1✔
776

777
        auto x_start = table.getColumnValues<double>("Shape.x@0.000");
3✔
778
        auto y_start = table.getColumnValues<double>("Shape.y@0.000");
3✔
779
        auto x_end = table.getColumnValues<double>("Shape.x@1.000");
3✔
780
        auto y_end = table.getColumnValues<double>("Shape.y@1.000");
3✔
781

782
        REQUIRE(x_start.size() == 4);
1✔
783

784
        // Verify square (t=0): starts at (0,0), ends at (0,0) - closed shape
785
        REQUIRE(x_start[0] == Catch::Approx(0.0));
1✔
786
        REQUIRE(y_start[0] == Catch::Approx(0.0));
1✔
787
        REQUIRE(x_end[0] == Catch::Approx(0.0));
1✔
788
        REQUIRE(y_end[0] == Catch::Approx(0.0));
1✔
789

790
        // Verify triangle (t=2): starts at (5,0), ends at (5,0) - closed shape
791
        REQUIRE(x_start[1] == Catch::Approx(5.0));
1✔
792
        REQUIRE(y_start[1] == Catch::Approx(0.0));
1✔
793
        REQUIRE(x_end[1] == Catch::Approx(5.0));
1✔
794
        REQUIRE(y_end[1] == Catch::Approx(0.0));
1✔
795
    }
3✔
796

797
    /*
798
    SECTION("Test cross-timeframe sampling") {
799
        auto & dm = getDataManager();
800
        auto dme = std::make_shared<DataManagerExtension>(dm);
801

802
        // Get sources from different timeframes
803
        auto whisker_source = dme->getLineSource("WhiskerTraces");// whisker_time frame
804
        auto shape_source = dme->getLineSource("GeometricShapes");// shape_time frame
805

806
        REQUIRE(whisker_source != nullptr);
807
        REQUIRE(shape_source != nullptr);
808

809
        // Verify they have different timeframes
810
        auto whisker_tf = whisker_source->getTimeFrame();
811
        auto shape_tf = shape_source->getTimeFrame();
812
        REQUIRE(whisker_tf != shape_tf);
813
        REQUIRE(whisker_tf->getTotalFrameCount() == 101);// whisker_time: 0-100
814
        REQUIRE(shape_tf->getTotalFrameCount() == 11);   // shape_time: 0,10,20,...,100
815

816
        // Create a test with whisker timeframe but sampling shape data
817
        std::vector<TimeFrameIndex> test_timestamps = {
818
                TimeFrameIndex(20), TimeFrameIndex(40)// These exist in shape_time as indices 2,4
819
        };
820

821
        auto row_selector = std::make_unique<TimestampSelector>(test_timestamps, whisker_tf);
822

823
        TableViewBuilder builder(dme);
824
        builder.setRowSelector(std::move(row_selector));
825

826
        // Sample shape data using whisker timeframe
827
        auto multi = std::make_unique<LineSamplingMultiComputer>(
828
                shape_source, "GeometricShapes", shape_tf, 2);
829
        builder.addColumns<double>("CrossFrame", std::move(multi));
830

831
        TableView table = builder.build();
832

833
        REQUIRE(table.getRowCount() == 2);
834
        REQUIRE(table.getColumnCount() == 6);// 3 positions * 2 coordinates
835

836
        auto x_mid = table.getColumnValues<double>("CrossFrame.x@0.500");
837
        auto y_mid = table.getColumnValues<double>("CrossFrame.y@0.500");
838

839
        REQUIRE(x_mid.size() == 2);
840
        REQUIRE(y_mid.size() == 2);
841

842
        // Should have sampled triangle at t=20 and circle at t=40
843
        // Values depend on specific timeframe conversion implementation
844
        REQUIRE(x_mid[0] >= 0.0);// Triangle midpoint
845
        REQUIRE(y_mid[0] >= 0.0);
846
        REQUIRE(x_mid[1] >= 0.0);// Circle midpoint
847
        REQUIRE(y_mid[1] >= 0.0);
848

849
        std::cout << "Cross-timeframe test - Triangle midpoint: (" << x_mid[0]
850
                  << ", " << y_mid[0] << "), Circle midpoint: (" << x_mid[1]
851
                  << ", " << y_mid[1] << ")" << std::endl;
852
    }
853
    */
854
}
2✔
855

856
TEST_CASE_METHOD(LineSamplingTableRegistryTestFixture, "DM - TV - LineSamplingMultiComputer via ComputerRegistry", "[LineSamplingMultiComputer][Registry]") {
3✔
857

858
    SECTION("Verify LineSamplingMultiComputer is registered in ComputerRegistry") {
3✔
859
        auto & registry = getTableRegistry().getComputerRegistry();
1✔
860

861
        // Check that LineSamplingMultiComputer is registered
862
        auto line_sample_info = registry.findComputerInfo("Line Sample XY");
3✔
863

864
        REQUIRE(line_sample_info != nullptr);
1✔
865

866
        // Verify computer info details
867
        REQUIRE(line_sample_info->name == "Line Sample XY");
1✔
868
        REQUIRE(line_sample_info->outputType == typeid(double));
1✔
869
        REQUIRE(line_sample_info->outputTypeName == "double");
1✔
870
        REQUIRE(line_sample_info->requiredRowSelector == RowSelectorType::Timestamp);
1✔
871
        REQUIRE(line_sample_info->requiredSourceType == typeid(std::shared_ptr<ILineSource>));
1✔
872
        REQUIRE(line_sample_info->isMultiOutput == true);
1✔
873

874
        // Verify parameter information
875
        REQUIRE(line_sample_info->hasParameters() == true);
1✔
876
        REQUIRE(line_sample_info->parameterDescriptors.size() == 1);
1✔
877
        REQUIRE(line_sample_info->parameterDescriptors[0]->getName() == "segments");
1✔
878
        REQUIRE(line_sample_info->parameterDescriptors[0]->getUIHint() == "number");
1✔
879
    }
3✔
880

881
    SECTION("Create LineSamplingMultiComputer via ComputerRegistry") {
3✔
882
        auto & dm = getDataManager();
1✔
883
        auto dme = std::make_shared<DataManagerExtension>(dm);
1✔
884
        auto & registry = getTableRegistry().getComputerRegistry();
1✔
885

886
        // Get whisker source for testing
887
        auto whisker_source = dme->getLineSource("WhiskerTraces");
3✔
888
        REQUIRE(whisker_source != nullptr);
1✔
889

890
        // Create computer via registry with different segment parameters
891
        std::map<std::string, std::string> params_2seg{{"segments", "2"}};
5✔
892
        std::map<std::string, std::string> params_5seg{{"segments", "5"}};
5✔
893

894
        auto computer_2seg = registry.createTypedMultiComputer<double>(
4✔
895
                "Line Sample XY", whisker_source, params_2seg);
3✔
896
        auto computer_5seg = registry.createTypedMultiComputer<double>(
4✔
897
                "Line Sample XY", whisker_source, params_5seg);
3✔
898

899
        REQUIRE(computer_2seg != nullptr);
1✔
900
        REQUIRE(computer_5seg != nullptr);
1✔
901

902
        // Test that the created computers work correctly
903
        auto whisker_time_frame = dm.getTime(TimeKey("whisker_time"));
1✔
904

905
        // Create a simple test
906
        std::vector<TimeFrameIndex> test_timestamps = {TimeFrameIndex(30)};
3✔
907
        auto row_selector_2seg = std::make_unique<TimestampSelector>(test_timestamps, whisker_time_frame);
1✔
908
        auto row_selector_5seg = std::make_unique<TimestampSelector>(test_timestamps, whisker_time_frame);
1✔
909

910
        // Test 2-segment computer
911
        {
912
            TableViewBuilder builder(dme);
1✔
913
            builder.setRowSelector(std::move(row_selector_2seg));
1✔
914
            builder.addColumns("Registry2Seg", std::move(computer_2seg));
3✔
915

916
            auto table = builder.build();
1✔
917
            REQUIRE(table.getRowCount() == 2);
1✔
918
            REQUIRE(table.getColumnCount() == 6);// 3 positions * 2 coordinates
1✔
919

920
            auto column_names = table.getColumnNames();
1✔
921
            REQUIRE(table.hasColumn("Registry2Seg.x@0.000"));
3✔
922
            REQUIRE(table.hasColumn("Registry2Seg.y@0.000"));
3✔
923
            REQUIRE(table.hasColumn("Registry2Seg.x@0.500"));
3✔
924
            REQUIRE(table.hasColumn("Registry2Seg.y@0.500"));
3✔
925
            REQUIRE(table.hasColumn("Registry2Seg.x@1.000"));
3✔
926
            REQUIRE(table.hasColumn("Registry2Seg.y@1.000"));
3✔
927
        }
1✔
928

929
        // Test 5-segment computer
930
        {
931
            TableViewBuilder builder(dme);
1✔
932
            builder.setRowSelector(std::move(row_selector_5seg));
1✔
933
            builder.addColumns("Registry5Seg", std::move(computer_5seg));
3✔
934

935
            auto table = builder.build();
1✔
936
            REQUIRE(table.getRowCount() == 2);
1✔
937
            REQUIRE(table.getColumnCount() == 12);// 6 positions * 2 coordinates
1✔
938

939
            auto column_names = table.getColumnNames();
1✔
940
            REQUIRE(table.hasColumn("Registry5Seg.x@0.000"));
3✔
941
            REQUIRE(table.hasColumn("Registry5Seg.y@0.000"));
3✔
942
            REQUIRE(table.hasColumn("Registry5Seg.x@0.200"));
3✔
943
            REQUIRE(table.hasColumn("Registry5Seg.y@0.200"));
3✔
944
            REQUIRE(table.hasColumn("Registry5Seg.x@1.000"));
3✔
945
            REQUIRE(table.hasColumn("Registry5Seg.y@1.000"));
3✔
946
        }
1✔
947
    }
4✔
948

949
    SECTION("Compare registry-created vs direct-created computers") {
3✔
950
        auto & dm = getDataManager();
1✔
951
        auto dme = std::make_shared<DataManagerExtension>(dm);
1✔
952
        auto & registry = getTableRegistry().getComputerRegistry();
1✔
953

954
        auto whisker_source = dme->getLineSource("WhiskerTraces");
3✔
955
        REQUIRE(whisker_source != nullptr);
1✔
956

957
        // Create computer via registry
958
        std::map<std::string, std::string> params{{"segments", "3"}};
5✔
959
        auto registry_computer = registry.createTypedMultiComputer<double>(
4✔
960
                "Line Sample XY", whisker_source, params);
3✔
961

962
        // Create computer directly
963
        auto whisker_time_frame = dm.getTime(TimeKey("whisker_time"));
1✔
964
        auto direct_computer = std::make_unique<LineSamplingMultiComputer>(
1✔
965
                whisker_source, "WhiskerTraces", whisker_time_frame, 3);
1✔
966

967
        REQUIRE(registry_computer != nullptr);
1✔
968
        REQUIRE(direct_computer != nullptr);
1✔
969

970
        // Test both computers with the same data
971
        std::vector<TimeFrameIndex> test_timestamps = {TimeFrameIndex(50)};
3✔
972

973
        // Registry computer test
974
        auto registry_output_names = registry_computer->getOutputNames();
1✔
975

976
        // Direct computer test
977
        auto direct_output_names = direct_computer->getOutputNames();
1✔
978

979
        // Output names should be identical
980
        REQUIRE(registry_output_names.size() == direct_output_names.size());
1✔
981
        REQUIRE(registry_output_names.size() == 8);// 4 positions * 2 coordinates
1✔
982

983
        for (size_t i = 0; i < registry_output_names.size(); ++i) {
9✔
984
            REQUIRE(registry_output_names[i] == direct_output_names[i]);
8✔
985
        }
986

987
        std::cout << "Comparison test - Both computers produce " << registry_output_names.size()
1✔
988
                  << " identical output names" << std::endl;
1✔
989
    }
4✔
990
}
6✔
991

992
TEST_CASE_METHOD(LineSamplingTableRegistryTestFixture, "DM - TV - LineSamplingMultiComputer via JSON TablePipeline", "[LineSamplingMultiComputer][JSON][Pipeline]") {
3✔
993

994
    SECTION("Test basic line sampling via JSON pipeline") {
3✔
995
        // JSON configuration for testing LineSamplingMultiComputer through TablePipeline
996
        char const * json_config = R"({
1✔
997
            "metadata": {
998
                "name": "Line Sampling Test",
999
                "description": "Test JSON execution of LineSamplingMultiComputer",
1000
                "version": "1.0"
1001
            },
1002
            "tables": [
1003
                {
1004
                    "table_id": "line_sampling_test",
1005
                    "name": "Line Sampling Test Table",
1006
                    "description": "Test table using LineSamplingMultiComputer",
1007
                    "row_selector": {
1008
                        "type": "timestamp",
1009
                        "timestamps": [10, 30, 50, 70, 90]
1010
                    },
1011
                    "columns": [
1012
                        {
1013
                            "name": "WhiskerSampling",
1014
                            "description": "Sample whisker trace at 3 equally spaced positions",
1015
                            "data_source": "WhiskerTraces",
1016
                            "computer": "Line Sample XY",
1017
                            "parameters": {
1018
                                "segments": "2"
1019
                            }
1020
                        }
1021
                    ]
1022
                }
1023
            ]
1024
        })";
1025

1026
        auto & pipeline = getTablePipeline();
1✔
1027

1028
        // Parse the JSON configuration
1029
        nlohmann::json json_obj = nlohmann::json::parse(json_config);
1✔
1030

1031
        // Load configuration into pipeline
1032
        bool load_success = pipeline.loadFromJson(json_obj);
1✔
1033
        REQUIRE(load_success);
1✔
1034

1035
        // Verify configuration was loaded correctly
1036
        auto table_configs = pipeline.getTableConfigurations();
1✔
1037
        REQUIRE(table_configs.size() == 1);
1✔
1038

1039
        auto const & config = table_configs[0];
1✔
1040
        REQUIRE(config.table_id == "line_sampling_test");
1✔
1041
        REQUIRE(config.name == "Line Sampling Test Table");
1✔
1042
        REQUIRE(config.columns.size() == 1);
1✔
1043

1044
        // Verify column configuration
1045
        auto const & column = config.columns[0];
1✔
1046
        REQUIRE(column["name"] == "WhiskerSampling");
1✔
1047
        REQUIRE(column["computer"] == "Line Sample XY");
1✔
1048
        REQUIRE(column["data_source"] == "WhiskerTraces");
1✔
1049
        REQUIRE(column["parameters"]["segments"] == "2");
1✔
1050

1051
        // Verify row selector configuration
1052
        REQUIRE(config.row_selector["type"] == "timestamp");
1✔
1053
        auto timestamps = config.row_selector["timestamps"];
1✔
1054
        REQUIRE(timestamps.size() == 5);
1✔
1055
        REQUIRE(timestamps[0] == 10);
1✔
1056
        REQUIRE(timestamps[4] == 90);
1✔
1057

1058
        std::cout << "JSON pipeline configuration loaded and parsed successfully" << std::endl;
1✔
1059

1060
        // Execute the pipeline
1061
        auto pipeline_result = pipeline.execute([](int table_index, std::string const & table_name, int table_progress, int overall_progress) {
2✔
1062
            std::cout << "Building table " << table_index << " (" << table_name << "): "
3✔
1063
                      << table_progress << "% (Overall: " << overall_progress << "%)" << std::endl;
3✔
1064
        });
2✔
1065

1066
        if (pipeline_result.success) {
1✔
1067
            std::cout << "Pipeline executed successfully!" << std::endl;
1✔
1068
            std::cout << "Tables completed: " << pipeline_result.tables_completed << "/" << pipeline_result.total_tables << std::endl;
1✔
1069
            std::cout << "Execution time: " << pipeline_result.total_execution_time_ms << " ms" << std::endl;
1✔
1070

1071
            // Verify the built table exists
1072
            auto & registry = getTableRegistry();
1✔
1073
            REQUIRE(registry.hasTable("line_sampling_test"));
3✔
1074

1075
            // Get the built table and verify its structure
1076
            auto built_table = registry.getBuiltTable("line_sampling_test");
3✔
1077
            REQUIRE(built_table != nullptr);
1✔
1078

1079
            // Check that the table has the expected columns
1080
            auto column_names = built_table->getColumnNames();
1✔
1081
            std::cout << "Built table has " << column_names.size() << " columns" << std::endl;
1✔
1082
            for (auto const & name: column_names) {
7✔
1083
                std::cout << "  Column: " << name << std::endl;
6✔
1084
            }
1085

1086
            REQUIRE(column_names.size() == 6);// 3 positions * 2 coordinates
1✔
1087
            REQUIRE(built_table->hasColumn("WhiskerSampling.x@0.000"));
3✔
1088
            REQUIRE(built_table->hasColumn("WhiskerSampling.y@0.000"));
3✔
1089
            REQUIRE(built_table->hasColumn("WhiskerSampling.x@0.500"));
3✔
1090
            REQUIRE(built_table->hasColumn("WhiskerSampling.y@0.500"));
3✔
1091
            REQUIRE(built_table->hasColumn("WhiskerSampling.x@1.000"));
3✔
1092
            REQUIRE(built_table->hasColumn("WhiskerSampling.y@1.000"));
3✔
1093

1094
                        // Verify table has 9 rows due to entity expansion: t=10(1) + t=30(2) + t=50(2) + t=70(2) + t=90(2) = 9
1095
            REQUIRE(built_table->getRowCount() == 9);
1✔
1096

1097
            // Get and verify the computed values
1098
            auto x_start = built_table->getColumnValues<double>("WhiskerSampling.x@0.000");
3✔
1099
            auto y_start = built_table->getColumnValues<double>("WhiskerSampling.y@0.000");
3✔
1100
            auto x_mid = built_table->getColumnValues<double>("WhiskerSampling.x@0.500");
3✔
1101
            auto y_mid = built_table->getColumnValues<double>("WhiskerSampling.y@0.500");
3✔
1102
            auto x_end = built_table->getColumnValues<double>("WhiskerSampling.x@1.000");
3✔
1103
            auto y_end = built_table->getColumnValues<double>("WhiskerSampling.y@1.000");
3✔
1104

1105
            REQUIRE(x_start.size() == 9);
1✔
1106
            REQUIRE(y_start.size() == 9);
1✔
1107
            REQUIRE(x_mid.size() == 9);
1✔
1108
            REQUIRE(y_mid.size() == 9);
1✔
1109
            REQUIRE(x_end.size() == 9);
1✔
1110
            REQUIRE(y_end.size() == 9);
1✔
1111

1112
            for (size_t i = 0; i < 9; ++i) {
10✔
1113
                // Whisker traces should start at x=0
1114
                REQUIRE(x_start[i] == Catch::Approx(0.0));
9✔
1115
                // Primary whiskers end at x=100, secondary at x=75
1116
                REQUIRE_THAT(x_end[i], 
9✔
1117
                    Catch::Matchers::WithinAbs(100.0, 1.0) 
1118
                    || Catch::Matchers::WithinAbs(75.0, 1.0));
1119
                // Middle should be around x=50 for primary, x=37.5 for secondary
1120
                REQUIRE_THAT(x_mid[i], 
9✔
1121
                    Catch::Matchers::WithinAbs(50.0, 1.0) 
1122
                    || Catch::Matchers::WithinAbs(37.5, 1.0));
1123
                // Y values should be reasonable for curved whiskers
1124
                REQUIRE(std::abs(y_start[i]) >= 0.0);
9✔
1125
                REQUIRE(std::abs(y_mid[i]) >= 0.0);
9✔
1126
                
1127
                std::cout << "Row " << i << ": Start=(" << x_start[i] << "," << y_start[i] 
9✔
1128
                          << "), Mid=(" << x_mid[i] << "," << y_mid[i] 
9✔
1129
                          << "), End=(" << x_end[i] << "," << y_end[i] << ")" << std::endl;
9✔
1130
            }
1131

1132
        } else {
1✔
UNCOV
1133
            FAIL("Pipeline execution failed: " + pipeline_result.error_message);
×
1134
        }
1135
    }
4✔
1136

1137
    SECTION("Test different segment counts via JSON") {
3✔
1138
        char const * json_config = R"JSON({
1✔
1139
            "metadata": {
1140
                "name": "Line Sampling Segment Test",
1141
                "description": "Test different segment counts for LineSamplingMultiComputer"
1142
            },
1143
            "tables": [
1144
                {
1145
                    "table_id": "line_sampling_segments_test",
1146
                    "name": "Line Sampling Segments Test Table",
1147
                    "description": "Test table with different segment counts",
1148
                    "row_selector": {
1149
                        "type": "timestamp", 
1150
                        "timestamps": [20, 40]
1151
                    },
1152
                    "columns": [
1153
                        {
1154
                            "name": "Shape1Seg",
1155
                            "description": "Sample geometric shapes with 1 segment (start/end only)",
1156
                            "data_source": "GeometricShapes",
1157
                            "computer": "Line Sample XY",
1158
                            "parameters": {
1159
                                "segments": "1"
1160
                            }
1161
                        },
1162
                        {
1163
                            "name": "Shape4Seg",
1164
                            "description": "Sample geometric shapes with 4 segments (5 positions)",
1165
                            "data_source": "GeometricShapes",
1166
                            "computer": "Line Sample XY",
1167
                            "parameters": {
1168
                                "segments": "4"
1169
                            }
1170
                        }
1171
                    ]
1172
                }
1173
            ]
1174
        })JSON";
1175

1176
        auto & pipeline = getTablePipeline();
1✔
1177
        nlohmann::json json_obj = nlohmann::json::parse(json_config);
1✔
1178

1179
        bool load_success = pipeline.loadFromJson(json_obj);
1✔
1180
        REQUIRE(load_success);
1✔
1181

1182
        auto table_configs = pipeline.getTableConfigurations();
1✔
1183
        REQUIRE(table_configs.size() == 1);
1✔
1184

1185
        auto const & config = table_configs[0];
1✔
1186
        REQUIRE(config.columns.size() == 2);
1✔
1187
        REQUIRE(config.columns[0]["parameters"]["segments"] == "1");
1✔
1188
        REQUIRE(config.columns[1]["parameters"]["segments"] == "4");
1✔
1189

1190
        std::cout << "Segment count JSON configuration parsed successfully" << std::endl;
1✔
1191

1192
        auto pipeline_result = pipeline.execute();
1✔
1193

1194
        if (pipeline_result.success) {
1✔
1195
            std::cout << "✓ Segment count pipeline executed successfully!" << std::endl;
1✔
1196

1197
            auto & registry = getTableRegistry();
1✔
1198
            auto built_table = registry.getBuiltTable("line_sampling_segments_test");
3✔
1199
            REQUIRE(built_table != nullptr);
1✔
1200

1201
            REQUIRE(built_table->getRowCount() == 2);    // 2 timestamps
1✔
1202
            REQUIRE(built_table->getColumnCount() == 14);// 1seg(4 cols) + 4seg(10 cols) = 14 total
1✔
1203

1204
            // Verify 1-segment columns (2 positions * 2 coordinates = 4 columns)
1205
            REQUIRE(built_table->hasColumn("Shape1Seg.x@0.000"));
3✔
1206
            REQUIRE(built_table->hasColumn("Shape1Seg.y@0.000"));
3✔
1207
            REQUIRE(built_table->hasColumn("Shape1Seg.x@1.000"));
3✔
1208
            REQUIRE(built_table->hasColumn("Shape1Seg.y@1.000"));
3✔
1209

1210
            // Verify 4-segment columns (5 positions * 2 coordinates = 10 columns)
1211
            REQUIRE(built_table->hasColumn("Shape4Seg.x@0.000"));
3✔
1212
            REQUIRE(built_table->hasColumn("Shape4Seg.y@0.000"));
3✔
1213
            REQUIRE(built_table->hasColumn("Shape4Seg.x@0.250"));
3✔
1214
            REQUIRE(built_table->hasColumn("Shape4Seg.y@0.250"));
3✔
1215
            REQUIRE(built_table->hasColumn("Shape4Seg.x@0.500"));
3✔
1216
            REQUIRE(built_table->hasColumn("Shape4Seg.y@0.500"));
3✔
1217
            REQUIRE(built_table->hasColumn("Shape4Seg.x@0.750"));
3✔
1218
            REQUIRE(built_table->hasColumn("Shape4Seg.y@0.750"));
3✔
1219
            REQUIRE(built_table->hasColumn("Shape4Seg.x@1.000"));
3✔
1220
            REQUIRE(built_table->hasColumn("Shape4Seg.y@1.000"));
3✔
1221

1222
            std::cout << "✓ All expected columns present for different segment counts" << std::endl;
1✔
1223

1224
        } else {
1✔
UNCOV
1225
            FAIL("Segment count pipeline execution failed: " + pipeline_result.error_message);
×
1226
        }
1227
    }
4✔
1228

1229
    SECTION("Test multiple line data sources via JSON") {
3✔
1230
        char const * json_config = R"JSON({
1✔
1231
            "metadata": {
1232
                "name": "Multi-Source Line Sampling Test",
1233
                "description": "Test multiple line data sources in same table"
1234
            },
1235
            "tables": [
1236
                {
1237
                    "table_id": "multi_source_line_test",
1238
                    "name": "Multi-Source Line Test Table",
1239
                    "description": "Test table with multiple line data sources",
1240
                    "row_selector": {
1241
                        "type": "timestamp",
1242
                        "timestamps": [30, 60]
1243
                    },
1244
                    "columns": [
1245
                        {
1246
                            "name": "WhiskerPoints",
1247
                            "description": "Sample whisker traces at key points",
1248
                            "data_source": "WhiskerTraces",
1249
                            "computer": "Line Sample XY",
1250
                            "parameters": {
1251
                                "segments": "3"
1252
                            }
1253
                        },
1254
                        {
1255
                            "name": "ShapePoints",
1256
                            "description": "Sample geometric shapes at key points",
1257
                            "data_source": "GeometricShapes",
1258
                            "computer": "Line Sample XY",
1259
                            "parameters": {
1260
                                "segments": "3"
1261
                            }
1262
                        }
1263
                    ]
1264
                }
1265
            ]
1266
        })JSON";
1267

1268
        auto & pipeline = getTablePipeline();
1✔
1269
        nlohmann::json json_obj = nlohmann::json::parse(json_config);
1✔
1270

1271
        bool load_success = pipeline.loadFromJson(json_obj);
1✔
1272
        REQUIRE(load_success);
1✔
1273

1274
        auto pipeline_result = pipeline.execute();
1✔
1275

1276
        if (pipeline_result.success) {
1✔
1277
            std::cout << "✓ Multi-source pipeline executed successfully!" << std::endl;
1✔
1278

1279
            auto & registry = getTableRegistry();
1✔
1280
            auto built_table = registry.getBuiltTable("multi_source_line_test");
3✔
1281
            REQUIRE(built_table != nullptr);
1✔
1282

1283
            // Should have 3 rows due to entity expansion: t=30(2 whiskers from WhiskerTraces) + t=60(1 whisker from WhiskerTraces) = 3 rows
1284
            // Note: Only WhiskerTraces has multiple entities, GeometricShapes has single entities at each timestamp
1285
            REQUIRE(built_table->getRowCount() >= 2);  // At least 2 rows, may be more due to expansion
1✔
1286
            // Should have 16 columns: 2 sources * 4 positions * 2 coordinates
1287
            REQUIRE(built_table->getColumnCount() == 16);
1✔
1288

1289
            // Verify whisker columns exist
1290
            REQUIRE(built_table->hasColumn("WhiskerPoints.x@0.000"));
3✔
1291
            REQUIRE(built_table->hasColumn("WhiskerPoints.y@0.333"));
3✔
1292
            REQUIRE(built_table->hasColumn("WhiskerPoints.x@1.000"));
3✔
1293

1294
            // Verify shape columns exist
1295
            REQUIRE(built_table->hasColumn("ShapePoints.x@0.000"));
3✔
1296
            REQUIRE(built_table->hasColumn("ShapePoints.y@0.333"));
3✔
1297
            REQUIRE(built_table->hasColumn("ShapePoints.x@1.000"));
3✔
1298

1299
            std::cout << "✓ Multi-source line sampling completed with "
1✔
1300
                      << built_table->getColumnCount() << " columns" << std::endl;
1✔
1301

1302
        } else {
1✔
UNCOV
1303
            FAIL("Multi-source pipeline execution failed: " + pipeline_result.error_message);
×
1304
        }
1305
    }
4✔
1306
}
3✔
1307

1308
// =============== EntityGroupManager Integration Tests ===============
1309

1310
/**
1311
 * @brief Test fixture for LineSamplingMultiComputer integration with EntityGroupManager
1312
 * 
1313
 * This fixture creates a complete test environment with:
1314
 * - DataManager with EntityGroupManager
1315
 * - LineData with known test lines at multiple time frames
1316
 * - TimeFrame setup for consistent temporal handling
1317
 */
1318
class LineSamplingEntityIntegrationFixture {
1319
public:
1320
    void SetUp() {
1✔
1321
        // Create DataManager
1322
        data_manager = std::make_unique<DataManager>();
1✔
1323
        
1324
        // Create TimeFrame with specific time points
1325
        std::vector<int> time_values = {10, 20, 30};
3✔
1326
        time_frame = std::make_shared<TimeFrame>(time_values);
1✔
1327
        
1328
        // Create LineData with test lines
1329
        line_data = std::make_shared<LineData>();
1✔
1330
        line_data->setTimeFrame(time_frame);
1✔
1331
        
1332
        // Set up identity context for EntityID generation
1333
        line_data->setIdentityContext("test_lines", data_manager->getEntityRegistry());
3✔
1334
        
1335

1336
        setupTestLines();
1✔
1337
        
1338
        // Register LineData with DataManager for entity expansion to work
1339
        data_manager->setData<LineData>("test_lines", line_data, TimeKey("time"));
3✔
1340
    }
2✔
1341
    
1342
private:
1343
    void setupTestLines() {
1✔
1344
        // Time 10: Add 2 lines
1345
        {
1346
            std::vector<float> xs1 = {0.0f, 10.0f, 20.0f};
3✔
1347
            std::vector<float> ys1 = {0.0f, 5.0f, 10.0f};
3✔
1348
            line_data->addAtTime(TimeFrameIndex(10), xs1, ys1, false);
1✔
1349
            
1350
            std::vector<float> xs2 = {5.0f, 15.0f};
3✔
1351
            std::vector<float> ys2 = {2.0f, 8.0f};
3✔
1352
            line_data->addAtTime(TimeFrameIndex(10), xs2, ys2, false);
1✔
1353
        }
1✔
1354
        
1355
        // Time 20: Add 2 lines
1356
        {
1357
            std::vector<float> xs1 = {1.0f, 11.0f, 21.0f};
3✔
1358
            std::vector<float> ys1 = {1.0f, 6.0f, 11.0f};
3✔
1359
            line_data->addAtTime(TimeFrameIndex(20), xs1, ys1, false);
1✔
1360
            
1361
            std::vector<float> xs2 = {6.0f, 16.0f};
3✔
1362
            std::vector<float> ys2 = {3.0f, 9.0f};
3✔
1363
            line_data->addAtTime(TimeFrameIndex(20), xs2, ys2, false);
1✔
1364
        }
1✔
1365
        
1366
        // Time 30: Add 1 line
1367
        {
1368
            std::vector<float> xs1 = {2.0f, 12.0f, 22.0f, 32.0f};
3✔
1369
            std::vector<float> ys1 = {2.0f, 7.0f, 12.0f, 17.0f};
3✔
1370
            line_data->addAtTime(TimeFrameIndex(30), xs1, ys1, false);
1✔
1371
        }
1✔
1372
        
1373
        // Rebuild entity IDs to ensure they're generated
1374
        line_data->rebuildAllEntityIds();
1✔
1375
    }
1✔
1376
    
1377
public:
1378
    std::unique_ptr<DataManager> data_manager;
1379
    std::shared_ptr<LineData> line_data;
1380
    std::shared_ptr<TimeFrame> time_frame;
1381
};
1382

1383
TEST_CASE_METHOD(LineSamplingEntityIntegrationFixture,
1✔
1384
                 "DM - TV - LineSamplingMultiComputer EntityID Round-trip Integration",
1385
                 "[LineSamplingMultiComputer][EntityGroupManager][integration]") {
1386
    
1387
    SetUp();
1✔
1388
    
1389
    SECTION("TableView creation and EntityID extraction with LineSamplingMultiComputer") {
1✔
1390
        // Get required components
1391
        auto* group_manager = data_manager->getEntityGroupManager();
1✔
1392
        REQUIRE(group_manager != nullptr);
1✔
1393
        
1394
        // Create DataManagerExtension for TableView integration
1395
        auto dme = std::make_shared<DataManagerExtension>(*data_manager);
1✔
1396
        
1397
        // Create LineDataAdapter from our test data
1398
        auto line_adapter = std::make_shared<LineDataAdapter>(line_data, time_frame, "test_lines");
1✔
1399
        
1400
        // Create LineSamplingMultiComputer with 2 segments (3 sample points: 0.0, 0.5, 1.0)
1401
        auto multi_computer = std::make_unique<LineSamplingMultiComputer>(
1✔
1402
            std::static_pointer_cast<ILineSource>(line_adapter),
2✔
1403
            "test_lines",
1404
            time_frame,
1✔
1405
            2  // 2 segments = 3 sample points
1✔
1406
        );
2✔
1407
        
1408
        // Create row selector for our time frames
1409
        std::vector<TimeFrameIndex> timestamps = {TimeFrameIndex(10), TimeFrameIndex(20), TimeFrameIndex(30)};
3✔
1410
        auto row_selector = std::make_unique<TimestampSelector>(timestamps, time_frame);
1✔
1411
        
1412
        // Build TableView using TableViewBuilder
1413
        TableViewBuilder builder(dme);
1✔
1414
        builder.setRowSelector(std::move(row_selector));
1✔
1415
        builder.addColumns<double>("Line", std::move(multi_computer));
3✔
1416
        
1417
        auto table = builder.build();
1✔
1418
        
1419
        // Verify table structure matches expected entity expansion
1420
        REQUIRE(table.getRowCount() == 5);  // t10:2 + t20:2 + t30:1 = 5 rows (entity expansion)
1✔
1421
        REQUIRE(table.getColumnCount() == 6);  // 3 sample points * 2 coordinates = 6 columns
1✔
1422
        
1423
        // Verify column names are correct
1424
        REQUIRE(table.hasColumn("Line.x@0.000"));
3✔
1425
        REQUIRE(table.hasColumn("Line.y@0.000"));
3✔
1426
        REQUIRE(table.hasColumn("Line.x@0.500"));
3✔
1427
        REQUIRE(table.hasColumn("Line.y@0.500"));
3✔
1428
        REQUIRE(table.hasColumn("Line.x@1.000"));
3✔
1429
        REQUIRE(table.hasColumn("Line.y@1.000"));
3✔
1430
        
1431
        // Verify EntityID information is available for LineSamplingMultiComputer columns
1432
        REQUIRE(table.hasColumnEntityIds("Line.x@0.000"));
3✔
1433
        REQUIRE(table.hasColumnEntityIds("Line.y@0.000"));
3✔
1434
        REQUIRE(table.hasColumnEntityIds("Line.x@0.500"));
3✔
1435
        REQUIRE(table.hasColumnEntityIds("Line.y@0.500"));
3✔
1436
        REQUIRE(table.hasColumnEntityIds("Line.x@1.000"));
3✔
1437
        REQUIRE(table.hasColumnEntityIds("Line.y@1.000"));
3✔
1438
        
1439
        // Get EntityIDs from one of the columns (all LineSamplingMultiComputer columns should share the same EntityIDs)
1440
        auto column_entity_ids_variant = table.getColumnEntityIds("Line.x@0.000");
3✔
1441
        auto column_entity_ids = std::get<std::vector<EntityId>>(column_entity_ids_variant);
1✔
1442
        REQUIRE(column_entity_ids.size() == 5); // Should match row count
1✔
1443
        
1444
        // Verify all EntityIDs are valid (non-zero)
1445
        for (EntityId id : column_entity_ids) {
6✔
1446
            REQUIRE(id != 0);
5✔
1447
            INFO("Column EntityID: " << id);
5✔
1448
        }
5✔
1449
        
1450
        // Verify that all LineSamplingMultiComputer columns have the same EntityIDs
1451
        auto y_start_entity_ids_variant = table.getColumnEntityIds("Line.y@0.000");
3✔
1452
        auto x_mid_entity_ids_variant = table.getColumnEntityIds("Line.x@0.500");
3✔
1453
        auto y_mid_entity_ids_variant = table.getColumnEntityIds("Line.y@0.500");
3✔
1454
        auto x_end_entity_ids_variant = table.getColumnEntityIds("Line.x@1.000");
3✔
1455
        auto y_end_entity_ids_variant = table.getColumnEntityIds("Line.y@1.000");
3✔
1456
        
1457
        // Extract EntityIDs from variants (LineSamplingMultiComputer uses Simple structure)
1458
        REQUIRE(std::holds_alternative<std::vector<EntityId>>(y_start_entity_ids_variant));
1✔
1459
        REQUIRE(std::holds_alternative<std::vector<EntityId>>(x_mid_entity_ids_variant));
1✔
1460
        REQUIRE(std::holds_alternative<std::vector<EntityId>>(y_mid_entity_ids_variant));
1✔
1461
        REQUIRE(std::holds_alternative<std::vector<EntityId>>(x_end_entity_ids_variant));
1✔
1462
        REQUIRE(std::holds_alternative<std::vector<EntityId>>(y_end_entity_ids_variant));
1✔
1463
        
1464
        auto y_start_entity_ids = std::get<std::vector<EntityId>>(y_start_entity_ids_variant);
1✔
1465
        auto x_mid_entity_ids = std::get<std::vector<EntityId>>(x_mid_entity_ids_variant);
1✔
1466
        auto y_mid_entity_ids = std::get<std::vector<EntityId>>(y_mid_entity_ids_variant);
1✔
1467
        auto x_end_entity_ids = std::get<std::vector<EntityId>>(x_end_entity_ids_variant);
1✔
1468
        auto y_end_entity_ids = std::get<std::vector<EntityId>>(y_end_entity_ids_variant);
1✔
1469
        
1470
        // Compare extracted EntityID vectors
1471
        REQUIRE(y_start_entity_ids == column_entity_ids);
1✔
1472
        REQUIRE(x_mid_entity_ids == column_entity_ids);
1✔
1473
        REQUIRE(y_mid_entity_ids == column_entity_ids);
1✔
1474
        REQUIRE(x_end_entity_ids == column_entity_ids);
1✔
1475
        REQUIRE(y_end_entity_ids == column_entity_ids);
1✔
1476
        
1477
        // Get sample data from table columns
1478
        auto x_start = table.getColumnValues<double>("Line.x@0.000");
3✔
1479
        auto y_start = table.getColumnValues<double>("Line.y@0.000");
3✔
1480
        auto x_mid = table.getColumnValues<double>("Line.x@0.500");
3✔
1481
        auto y_mid = table.getColumnValues<double>("Line.y@0.500");
3✔
1482
        auto x_end = table.getColumnValues<double>("Line.x@1.000");
3✔
1483
        auto y_end = table.getColumnValues<double>("Line.y@1.000");
3✔
1484
        
1485
        REQUIRE(x_start.size() == 5);
1✔
1486
        REQUIRE(y_start.size() == 5);
1✔
1487
        REQUIRE(x_mid.size() == 5);
1✔
1488
        REQUIRE(y_mid.size() == 5);
1✔
1489
        REQUIRE(x_end.size() == 5);
1✔
1490
        REQUIRE(y_end.size() == 5);
1✔
1491
        
1492
        // Select specific rows for our group (e.g., rows 1, 2, and 4)
1493
        std::vector<size_t> selected_row_indices = {1, 2, 4};
3✔
1494
        std::vector<EntityId> selected_entity_ids;
1✔
1495
        
1496
        for (size_t row_idx : selected_row_indices) {
4✔
1497
            REQUIRE(row_idx < column_entity_ids.size());
3✔
1498
            selected_entity_ids.push_back(column_entity_ids[row_idx]);
3✔
1499
        }
1500
        
1501
        REQUIRE(selected_entity_ids.size() == 3);
1✔
1502
        
1503
        // Verify all selected EntityIDs are valid
1504
        for (EntityId id : selected_entity_ids) {
4✔
1505
            REQUIRE(id != 0);
3✔
1506
            INFO("Selected EntityID: " << id);
3✔
1507
        }
3✔
1508
        
1509
        // Create a group in EntityGroupManager with these EntityIDs
1510
        GroupId test_group = group_manager->createGroup("LineSampling Selection", "Entities from selected table rows");
5✔
1511
        size_t added = group_manager->addEntitiesToGroup(test_group, selected_entity_ids);
1✔
1512
        REQUIRE(added == selected_entity_ids.size());
1✔
1513
        
1514
        // Verify the group was created correctly
1515
        REQUIRE(group_manager->hasGroup(test_group));
1✔
1516
        REQUIRE(group_manager->getGroupSize(test_group) == selected_entity_ids.size());
1✔
1517
        
1518
        auto group_entities = group_manager->getEntitiesInGroup(test_group);
1✔
1519
        REQUIRE(group_entities.size() == selected_entity_ids.size());
1✔
1520
        
1521
        // Now query LineData using the grouped EntityIDs to get the original line data
1522
        auto lines_from_group = line_data->getLinesByEntityIds(group_entities);
1✔
1523
        REQUIRE(lines_from_group.size() == selected_entity_ids.size());
1✔
1524
        
1525
        // Verify that the lines we get back match the data in the corresponding table rows
1526
        // We'll compare the start and end points from LineSamplingMultiComputer with actual line data
1527
        
1528
        for (size_t i = 0; i < lines_from_group.size(); ++i) {
4✔
1529
            EntityId entity_id = lines_from_group[i].first;
3✔
1530
            Line2D const& original_line = lines_from_group[i].second;
3✔
1531
            
1532
            // Find which row this EntityID corresponds to in our selected rows
1533
            size_t table_row_index = 0;
3✔
1534
            bool found = false;
3✔
1535
            for (size_t j = 0; j < selected_row_indices.size(); ++j) {
6✔
1536
                if (entity_id == selected_entity_ids[j]) {
6✔
1537
                    table_row_index = selected_row_indices[j];
3✔
1538
                    found = true;
3✔
1539
                    break;
3✔
1540
                }
1541
            }
1542
            
1543
            if (!found) {
3✔
UNCOV
1544
                FAIL("Unexpected EntityID in group: " << entity_id);
×
1545
            }
1546
            
1547
            // Get the sampled points from the table for this row
1548
            float table_x_start = static_cast<float>(x_start[table_row_index]);
3✔
1549
            float table_y_start = static_cast<float>(y_start[table_row_index]);
3✔
1550
            float table_x_end = static_cast<float>(x_end[table_row_index]);
3✔
1551
            float table_y_end = static_cast<float>(y_end[table_row_index]);
3✔
1552
            
1553
            // Get actual start and end points from the original line
1554
            REQUIRE(original_line.size() >= 2);
3✔
1555
            Point2D<float> actual_start = original_line.front();
3✔
1556
            Point2D<float> actual_end = original_line.back();
3✔
1557
            
1558
            // Verify that the table data matches the original line data
1559
            INFO("Checking EntityID " << entity_id << " at table row " << table_row_index);
3✔
1560
            INFO("Table start: (" << table_x_start << ", " << table_y_start << ")");
3✔
1561
            INFO("Actual start: (" << actual_start.x << ", " << actual_start.y << ")");
3✔
1562
            INFO("Table end: (" << table_x_end << ", " << table_y_end << ")");
3✔
1563
            INFO("Actual end: (" << actual_end.x << ", " << actual_end.y << ")");
3✔
1564
            
1565
            REQUIRE(table_x_start == Catch::Approx(actual_start.x).epsilon(0.001f));
3✔
1566
            REQUIRE(table_y_start == Catch::Approx(actual_start.y).epsilon(0.001f));
3✔
1567
            REQUIRE(table_x_end == Catch::Approx(actual_end.x).epsilon(0.001f));
3✔
1568
            REQUIRE(table_y_end == Catch::Approx(actual_end.y).epsilon(0.001f));
3✔
1569
        }
3✔
1570
        
1571
        INFO("Successfully verified round-trip: LineData -> LineSamplingMultiComputer -> TableView -> EntityGroupManager -> LineData");
1✔
1572
    }
2✔
1573
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc