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

paulmthompson / WhiskerToolbox / 17920603410

22 Sep 2025 03:39PM UTC coverage: 71.97% (-0.05%) from 72.02%
17920603410

push

github

paulmthompson
all tests pass

277 of 288 new or added lines in 8 files covered. (96.18%)

520 existing lines in 35 files now uncovered.

40275 of 55961 relevant lines covered (71.97%)

1225.8 hits per line

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

99.22
/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
    lineData->setIdentityContext("TestLines", dm.getEntityRegistry());
3✔
56
    lineData->rebuildAllEntityIds();
1✔
57

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

62
    // Create DataManagerExtension
63
    DataManagerExtension dme(dm);
1✔
64

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

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

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

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

86
    auto table = builder.build();
1✔
87

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

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

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

134

135
TEST_CASE("DM - TV - LineSamplingMultiComputer can be created via registry", "[LineSamplingMultiComputer][Registry]") {
1✔
136
    DataManager dm;
1✔
137

138
    std::vector<int> timeValues = {0, 1};
3✔
139
    auto tf = std::make_shared<TimeFrame>(timeValues);
1✔
140

141
    auto lineData = std::make_shared<LineData>();
1✔
142
    lineData->setTimeFrame(tf);
1✔
143
    std::vector<float> xs = {0.0f, 10.0f};
3✔
144
    std::vector<float> ys = {0.0f, 0.0f};
3✔
145
    lineData->addAtTime(TimeFrameIndex(0), xs, ys, false);
1✔
146
    lineData->addAtTime(TimeFrameIndex(1), xs, ys, false);
1✔
147

148
    lineData->setIdentityContext("RegLines", dm.getEntityRegistry());
3✔
149
    lineData->rebuildAllEntityIds();
1✔
150

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

153
    // Create DataSourceVariant via registry adapter to ensure consistent type usage
154
    ComputerRegistry registry;
1✔
155
    auto adapted = registry.createAdapter(
1✔
156
        "Line Data",
157
        std::static_pointer_cast<void>(lineData),
2✔
158
        tf,
159
        std::string{"RegLines"},
4✔
160
            {});
6✔
161
    // Diagnostics
162
    {
163
        auto adapter_names = registry.getAllAdapterNames();
1✔
164
        std::cout << "Registered adapters (" << adapter_names.size() << ")" << std::endl;
1✔
165
        for (auto const & n: adapter_names) {
4✔
166
            std::cout << "  Adapter: " << n << std::endl;
3✔
167
        }
168
        std::cout << "Adapted variant index: " << adapted.index() << std::endl;
1✔
169
    }
1✔
170
    // Fallback to direct adapter if registry adapter not found (should not happen after registration)
171
    DataSourceVariant variant = adapted.index() != std::variant_npos ? adapted
1✔
172
                               : DataSourceVariant{std::static_pointer_cast<ILineSource>(lineAdapter)};
1✔
173

174
    // More diagnostics: list available computers
175
    {
176
        auto comps = registry.getAvailableComputers(RowSelectorType::Timestamp, variant);
1✔
177
        std::cout << "Available computers for Timestamp + variant(" << variant.index() << ") = " << comps.size() << std::endl;
1✔
178
        for (auto const & ci: comps) {
2✔
179
            std::cout << "  Computer: " << ci.name
1✔
180
                      << ", isMultiOutput=" << (ci.isMultiOutput ? "true" : "false")
1✔
181
                      << ", requiredSourceType=" << ci.requiredSourceType.name() << std::endl;
1✔
182
        }
183
        auto info = registry.findComputerInfo("Line Sample XY");
3✔
184
        if (info) {
1✔
185
            std::cout << "Found computer info for 'Line Sample XY' with requiredSourceType=" << info->requiredSourceType.name()
186
                      << ", rowSelector=" << static_cast<int>(info->requiredRowSelector)
1✔
187
                      << ", isMultiOutput=" << (info->isMultiOutput ? "true" : "false") << std::endl;
1✔
188
        } else {
UNCOV
189
            std::cout << "Did not find computer info for 'Line Sample XY'" << std::endl;
×
190
        }
191
    }
1✔
192

193
    // Create via registry
194
    std::map<std::string, std::string> params{{"segments", "2"}};
5✔
195
    auto multi = registry.createTypedMultiComputer<double>("Line Sample XY", variant, params);
3✔
196
    REQUIRE(multi != nullptr);
1✔
197

198
    // Build with builder
199
    auto dme_ptr = std::make_shared<DataManagerExtension>(dm);
1✔
200
    std::vector<TimeFrameIndex> timestamps = {TimeFrameIndex(0), TimeFrameIndex(1)};
3✔
201
    auto rowSelector = std::make_unique<TimestampSelector>(timestamps, tf);
1✔
202

203
    TableViewBuilder builder(dme_ptr);
1✔
204
    builder.setRowSelector(std::move(rowSelector));
1✔
205
    builder.addColumns<double>("Line", std::move(multi));
3✔
206
    auto table = builder.build();
1✔
207

208
    auto names = table.getColumnNames();
1✔
209
    REQUIRE(names.size() == 6);
1✔
210
}
3✔
211

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

215
    // Timeframe with 5 timestamps
216
    std::vector<int> timeValues = {0, 1, 2, 3, 4};
3✔
217
    auto tf = std::make_shared<TimeFrame>(timeValues);
1✔
218

219
    // LineData with varying number of lines per timestamp
220
    auto lineData = std::make_shared<LineData>();
1✔
221
    lineData->setTimeFrame(tf);
1✔
222

223
    // t=0: no lines (should be dropped)
224
    // t=1: one horizontal line from x=0..10
225
    {
226
        std::vector<float> xs = {0.0f, 10.0f};
3✔
227
        std::vector<float> ys = {0.0f, 0.0f};
3✔
228
        lineData->addAtTime(TimeFrameIndex(1), xs, ys, false);
1✔
229
    }
1✔
230
    // t=2: two lines; l0 horizontal (x 0..10), l1 vertical (y 0..10)
231
    {
232
        std::vector<float> xs = {0.0f, 10.0f};
3✔
233
        std::vector<float> ys = {0.0f, 0.0f};
3✔
234
        lineData->addAtTime(TimeFrameIndex(2), xs, ys, false);
1✔
235
        std::vector<float> xs2 = {5.0f, 5.0f};
3✔
236
        std::vector<float> ys2 = {0.0f, 10.0f};
3✔
237
        lineData->addAtTime(TimeFrameIndex(2), xs2, ys2, false);
1✔
238
    }
1✔
239
    // t=3: no lines (should be dropped)
240
    // t=4: one vertical line (y 0..10 at x=2)
241
    {
242
        std::vector<float> xs = {2.0f, 2.0f};
3✔
243
        std::vector<float> ys = {0.0f, 10.0f};
3✔
244
        lineData->addAtTime(TimeFrameIndex(4), xs, ys, false);
1✔
245
    }
1✔
246

247
    lineData->setIdentityContext("ExpLines", dm.getEntityRegistry());
3✔
248
    lineData->rebuildAllEntityIds();
1✔
249

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

254
    // Timestamps include empty ones; expansion should drop t=0 and t=3
255
    std::vector<TimeFrameIndex> timestamps = {
1✔
256
            TimeFrameIndex(0), TimeFrameIndex(1), TimeFrameIndex(2), TimeFrameIndex(3), TimeFrameIndex(4)};
3✔
257
    auto rowSelector = std::make_unique<TimestampSelector>(timestamps, tf);
1✔
258

259
    // Build table
260
    auto dme_ptr = std::make_shared<DataManagerExtension>(dm);
1✔
261
    TableViewBuilder builder(dme_ptr);
1✔
262
    builder.setRowSelector(std::move(rowSelector));
1✔
263

264
    auto multi = std::make_unique<LineSamplingMultiComputer>(
4✔
265
        std::static_pointer_cast<ILineSource>(lineAdapter),
2✔
266
        std::string{"ExpLines"},
2✔
267
        tf,
268
            2// positions 0.0, 0.5, 1.0
1✔
269
    );
3✔
270
    builder.addColumns<double>("Line", std::move(multi));
3✔
271

272
    auto table = builder.build();
1✔
273

274
    // With expansion: expected rows = t1:1 + t2:2 + t4:1 = 4 rows
275
    REQUIRE(table.getRowCount() == 4);
1✔
276

277
    // Column names same structure
278
    auto names = table.getColumnNames();
1✔
279
    REQUIRE(names.size() == 6);
1✔
280

281
    // Validate per-entity sampling ordering as inserted:
282
    // Row 0 -> t=1, the single horizontal line: x@0.5 = 5, y@0.5 = 0
283
    // Row 1 -> t=2, entity 0 (horizontal): x@0.5 = 5, y@0.5 = 0
284
    // Row 2 -> t=2, entity 1 (vertical):   x@0.5 = 5, y@0.5 = 5
285
    // Row 3 -> t=4, the single vertical line at x=2: x@0.5 = 2, y@0.5 = 5
286
    auto const & xsMid = table.getColumnValues<double>("Line.x@0.500");
3✔
287
    auto const & ysMid = table.getColumnValues<double>("Line.y@0.500");
3✔
288
    REQUIRE(xsMid.size() == 4);
1✔
289
    REQUIRE(ysMid.size() == 4);
1✔
290

291
    REQUIRE(xsMid[0] == Catch::Approx(5.0));
1✔
292
    REQUIRE(ysMid[0] == Catch::Approx(0.0));
1✔
293

294
    REQUIRE(xsMid[1] == Catch::Approx(5.0));
1✔
295
    REQUIRE(ysMid[1] == Catch::Approx(0.0));
1✔
296

297
    REQUIRE(xsMid[2] == Catch::Approx(5.0));
1✔
298
    REQUIRE(ysMid[2] == Catch::Approx(5.0));
1✔
299

300
    REQUIRE(xsMid[3] == Catch::Approx(2.0));
1✔
301
    REQUIRE(ysMid[3] == Catch::Approx(5.0));
1✔
302
}
2✔
303

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

307
    std::vector<int> timeValues = {0, 1, 2, 3};
3✔
308
    auto tf = std::make_shared<TimeFrame>(timeValues);
1✔
309

310
    dm.setTime(TimeKey("test_time"), tf);
1✔
311

312
    // LineData: only at t=1
313
    auto lineData = std::make_shared<LineData>();
1✔
314
    lineData->setTimeFrame(tf);
1✔
315
    {
316
        std::vector<float> xs = {0.0f, 10.0f};
3✔
317
        std::vector<float> ys = {1.0f, 1.0f};
3✔
318
        lineData->addAtTime(TimeFrameIndex(1), xs, ys, false);
1✔
319
    }
1✔
320

321
    lineData->setIdentityContext("MixedLines", dm.getEntityRegistry());
3✔
322
    lineData->rebuildAllEntityIds();
1✔
323

324
    dm.setData<LineData>("MixedLines", lineData, TimeKey("test_time"));
3✔
325

326
    // Analog data present at all timestamps: values 0,10,20,30
327
    std::vector<float> analogVals = {0.f, 10.f, 20.f, 30.f};
3✔
328
    std::vector<TimeFrameIndex> analogTimes = {TimeFrameIndex(0), TimeFrameIndex(1), TimeFrameIndex(2), TimeFrameIndex(3)};
3✔
329
    auto analogData = std::make_shared<AnalogTimeSeries>(analogVals, analogTimes);
1✔
330
    dm.setData<AnalogTimeSeries>("AnalogA", analogData, TimeKey("test_time"));
3✔
331

332
    // Build selector across all timestamps
333
    std::vector<TimeFrameIndex> timestamps = {TimeFrameIndex(0), TimeFrameIndex(1), TimeFrameIndex(2), TimeFrameIndex(3)};
3✔
334
    auto rowSelector = std::make_unique<TimestampSelector>(timestamps, tf);
1✔
335

336
    auto dme_ptr = std::make_shared<DataManagerExtension>(dm);
1✔
337
    TableViewBuilder builder(dme_ptr);
1✔
338
    builder.setRowSelector(std::move(rowSelector));
1✔
339

340
    // Multi-line columns (expanding)
341
    auto lineAdapter = std::make_shared<LineDataAdapter>(lineData, tf, std::string{"MixedLines"});
3✔
342
    auto multi = std::make_unique<LineSamplingMultiComputer>(
4✔
343
        std::static_pointer_cast<ILineSource>(lineAdapter),
2✔
344
        std::string{"MixedLines"},
2✔
345
        tf,
346
            2);
4✔
347
    builder.addColumns<double>("Line", std::move(multi));
3✔
348

349
    // Analog timestamp value column
350
    // Use registry to create the computer; simpler path: direct TimestampValueComputer
351
    // but we need an IAnalogSource from DataManagerExtension, resolved by name "AnalogA"
352
    class SimpleTimestampValueComputer : public IColumnComputer<double> {
353
    public:
354
        explicit SimpleTimestampValueComputer(std::shared_ptr<IAnalogSource> src)
1✔
355
            : src_(std::move(src)) {}
1✔
356
        [[nodiscard]] std::pair<std::vector<double>, ColumnEntityIds> compute(ExecutionPlan const & plan) const override {
1✔
357
            std::vector<TimeFrameIndex> idx;
1✔
358
            if (plan.hasIndices()) {
1✔
359
                idx = plan.getIndices();
1✔
360
            } else {
361
                // Build from rows (expanded)
UNCOV
362
                for (auto const & r: plan.getRows()) idx.push_back(r.timeIndex);
×
363
            }
364
            std::vector<double> out(idx.size(), 0.0);
3✔
365
            // naive: use AnalogDataAdapter semantics: value == index*10
366
            for (size_t i = 0; i < idx.size(); ++i) out[i] = static_cast<double>(idx[i].getValue() * 10);
5✔
367
            return {out, std::monostate{}};
3✔
368
        }
1✔
369
        [[nodiscard]] auto getSourceDependency() const -> std::string override { return src_ ? src_->getName() : std::string{"AnalogA"}; }
6✔
370

371
    private:
372
        std::shared_ptr<IAnalogSource> src_;
373
    };
374

375
    auto analogSrc = dme_ptr->getAnalogSource("AnalogA");
3✔
376
    REQUIRE(analogSrc != nullptr);
1✔
377
    auto analogComp = std::make_unique<SimpleTimestampValueComputer>(analogSrc);
1✔
378
    builder.addColumn<double>("Analog", std::move(analogComp));
3✔
379

380
    auto table = builder.build();
1✔
381

382
    // Expect expanded rows keep all timestamps due to coexisting analog column: t=0,1,2,3 -> 4 rows
383
    // Line columns will have zero for t=0,2,3 where no line exists; analog column has 0,10,20,30
384
    REQUIRE(table.getRowCount() == 4);
1✔
385
    auto const & xsMid = table.getColumnValues<double>("Line.x@0.500");
3✔
386
    auto const & ysMid = table.getColumnValues<double>("Line.y@0.500");
3✔
387
    auto const & analog = table.getColumnValues<double>("Analog");
3✔
388
    REQUIRE(xsMid.size() == 4);
1✔
389
    REQUIRE(ysMid.size() == 4);
1✔
390
    REQUIRE(analog.size() == 4);
1✔
391

392
    // At t=1 (row 1), line exists; others should be zeros for line columns
393
    REQUIRE(xsMid[0] == Catch::Approx(0.0));
1✔
394
    REQUIRE(ysMid[0] == Catch::Approx(0.0));
1✔
395
    REQUIRE(xsMid[1] == Catch::Approx(5.0));
1✔
396
    REQUIRE(ysMid[1] == Catch::Approx(1.0));
1✔
397
    REQUIRE(xsMid[2] == Catch::Approx(0.0));
1✔
398
    REQUIRE(ysMid[2] == Catch::Approx(0.0));
1✔
399
    REQUIRE(xsMid[3] == Catch::Approx(0.0));
1✔
400
    REQUIRE(ysMid[3] == Catch::Approx(0.0));
1✔
401

402
    REQUIRE(analog[0] == Catch::Approx(0.0));
1✔
403
    REQUIRE(analog[1] == Catch::Approx(10.0));
1✔
404
    REQUIRE(analog[2] == Catch::Approx(20.0));
1✔
405
    REQUIRE(analog[3] == Catch::Approx(30.0));
1✔
406
}
2✔
407

408
/**
409
 * @brief Base test fixture for LineSamplingMultiComputer with realistic line data
410
 * 
411
 * This fixture provides a DataManager populated with:
412
 * - TimeFrames with different granularities
413
 * - Line data representing whisker traces or geometric features
414
 * - Multiple lines per timestamp for testing entity expansion
415
 * - Cross-timeframe scenarios for testing timeframe conversion
416
 */
417
class LineSamplingTestFixture {
418
protected:
419
    LineSamplingTestFixture() {
8✔
420
        // Initialize the DataManager
421
        m_data_manager = std::make_unique<DataManager>();
8✔
422

423
        // Populate with test data
424
        populateWithLineTestData();
8✔
425
    }
8✔
426

427
    ~LineSamplingTestFixture() = default;
8✔
428

429
    /**
430
     * @brief Get the DataManager instance
431
     */
432
    DataManager & getDataManager() { return *m_data_manager; }
16✔
433
    DataManager const & getDataManager() const { return *m_data_manager; }
434
    DataManager * getDataManagerPtr() { return m_data_manager.get(); }
435

436
private:
437
    std::unique_ptr<DataManager> m_data_manager;
438

439
    /**
440
     * @brief Populate the DataManager with line test data
441
     */
442
    void populateWithLineTestData() {
8✔
443
        createTimeFrames();
8✔
444
        createWhiskerTraces();
8✔
445
        createGeometricShapes();
8✔
446
    }
8✔
447

448
    /**
449
     * @brief Create TimeFrame objects for different data streams
450
     */
451
    void createTimeFrames() {
8✔
452
        // Create "whisker_time" timeframe: 0 to 100 (101 points) - whisker tracking at high frequency
453
        std::vector<int> whisker_time_values(101);
24✔
454
        std::iota(whisker_time_values.begin(), whisker_time_values.end(), 0);
8✔
455
        auto whisker_time_frame = std::make_shared<TimeFrame>(whisker_time_values);
8✔
456
        m_data_manager->setTime(TimeKey("whisker_time"), whisker_time_frame, true);
8✔
457

458
        // Create "shape_time" timeframe: 0, 10, 20, 30, ..., 100 (11 points) - geometric shapes at lower frequency
459
        std::vector<int> shape_time_values;
8✔
460
        shape_time_values.reserve(11);
8✔
461
        for (int i = 0; i <= 10; ++i) {
96✔
462
            shape_time_values.push_back(i * 10);
88✔
463
        }
464
        auto shape_time_frame = std::make_shared<TimeFrame>(shape_time_values);
8✔
465
        m_data_manager->setTime(TimeKey("shape_time"), shape_time_frame, true);
8✔
466
    }
16✔
467

468
    /**
469
     * @brief Create whisker trace data (complex curved lines)
470
     */
471
    void createWhiskerTraces() {
8✔
472
        // Create whisker traces with varying curvature and length
473
        auto whisker_lines = std::make_shared<LineData>();
8✔
474

475
        // Create curved whisker traces at different time points
476
        for (int t = 10; t <= 90; t += 20) {
48✔
477
            // Primary whisker - curved arc
478
            std::vector<float> xs, ys;
40✔
479
            for (int i = 0; i <= 20; ++i) {
880✔
480
                float s = static_cast<float>(i) / 20.0f;
840✔
481
                float x = s * 100.0f;
840✔
482
                float y = 20.0f * std::sin(s * 3.14159f / 2.0f) * (1.0f + 0.1f * static_cast<float>(t) / 100.0f);
840✔
483
                xs.push_back(x);
840✔
484
                ys.push_back(y);
840✔
485
            }
486
            whisker_lines->addAtTime(TimeFrameIndex(t), xs, ys, false);
40✔
487

488
            // Secondary whisker - smaller arc below
489
            if (t >= 30) {
40✔
490
                std::vector<float> xs2, ys2;
32✔
491
                for (int i = 0; i <= 15; ++i) {
544✔
492
                    float s = static_cast<float>(i) / 15.0f;
512✔
493
                    float x = s * 75.0f;
512✔
494
                    float y = -10.0f - 15.0f * std::sin(s * 3.14159f / 3.0f);
512✔
495
                    xs2.push_back(x);
512✔
496
                    ys2.push_back(y);
512✔
497
                }
498
                whisker_lines->addAtTime(TimeFrameIndex(t), xs2, ys2, false);
32✔
499
            }
32✔
500
        }
40✔
501

502
        whisker_lines->setIdentityContext("WhiskerTraces", m_data_manager->getEntityRegistry());
24✔
503
        whisker_lines->rebuildAllEntityIds();
8✔
504

505
        m_data_manager->setData<LineData>("WhiskerTraces", whisker_lines, TimeKey("whisker_time"));
24✔
506
    }
16✔
507

508
    /**
509
     * @brief Create geometric shape data (simple geometric lines)
510
     */
511
    void createGeometricShapes() {
8✔
512
        // Create geometric shapes with different patterns
513
        auto shape_lines = std::make_shared<LineData>();
8✔
514

515
        // Square at t=0
516
        {
517
            std::vector<float> xs = {0.0f, 10.0f, 10.0f, 0.0f, 0.0f};
24✔
518
            std::vector<float> ys = {0.0f, 0.0f, 10.0f, 10.0f, 0.0f};
24✔
519
            shape_lines->addAtTime(TimeFrameIndex(0), xs, ys, false);
8✔
520
        }
8✔
521

522
        // Triangle at t=20
523
        {
524
            std::vector<float> xs = {5.0f, 10.0f, 0.0f, 5.0f};
24✔
525
            std::vector<float> ys = {0.0f, 10.0f, 10.0f, 0.0f};
24✔
526
            shape_lines->addAtTime(TimeFrameIndex(2), xs, ys, false);
8✔
527
        }
8✔
528

529
        // Circle (octagon approximation) at t=40
530
        {
531
            std::vector<float> xs, ys;
8✔
532
            for (int i = 0; i <= 8; ++i) {
80✔
533
                float angle = static_cast<float>(i) * 2.0f * 3.14159f / 8.0f;
72✔
534
                xs.push_back(5.0f + 5.0f * std::cos(angle));
72✔
535
                ys.push_back(5.0f + 5.0f * std::sin(angle));
72✔
536
            }
537
            shape_lines->addAtTime(TimeFrameIndex(4), xs, ys, false);
8✔
538
        }
8✔
539

540
        // Multiple shapes at different times - star at t=60, circle at t=80
541
        {
542
            // Star shape at t=60
543
            std::vector<float> xs1, ys1;
8✔
544
            for (int i = 0; i <= 10; ++i) {
96✔
545
                float angle = static_cast<float>(i) * 2.0f * 3.14159f / 10.0f;
88✔
546
                float radius = (i % 2 == 0) ? 8.0f : 4.0f;
88✔
547
                xs1.push_back(15.0f + radius * std::cos(angle));
88✔
548
                ys1.push_back(15.0f + radius * std::sin(angle));
88✔
549
            }
550
            shape_lines->addAtTime(TimeFrameIndex(6), xs1, ys1, false);
8✔
551

552
            // Small circle at t=80
553
            std::vector<float> xs2, ys2;
8✔
554
            for (int i = 0; i <= 6; ++i) {
64✔
555
                float angle = static_cast<float>(i) * 2.0f * 3.14159f / 6.0f;
56✔
556
                xs2.push_back(25.0f + 3.0f * std::cos(angle));
56✔
557
                ys2.push_back(25.0f + 3.0f * std::sin(angle));
56✔
558
            }
559
            shape_lines->addAtTime(TimeFrameIndex(8), xs2, ys2, false);
8✔
560
        }
8✔
561

562
        shape_lines->setIdentityContext("GeometricShapes", m_data_manager->getEntityRegistry());
24✔
563
        shape_lines->rebuildAllEntityIds();
8✔
564

565
        m_data_manager->setData<LineData>("GeometricShapes", shape_lines, TimeKey("shape_time"));
24✔
566
    }
16✔
567
};
568

569
/**
570
 * @brief Test fixture combining LineSamplingTestFixture with TableRegistry and TablePipeline
571
 * 
572
 * This fixture provides everything needed to test JSON-based table pipeline execution:
573
 * - DataManager with line test data (from LineSamplingTestFixture)
574
 * - TableRegistry for managing table configurations
575
 * - TablePipeline for executing JSON configurations
576
 */
577
class LineSamplingTableRegistryTestFixture : public LineSamplingTestFixture {
578
protected:
579
    LineSamplingTableRegistryTestFixture()
6✔
580
        : LineSamplingTestFixture() {
6✔
581
        // Use the DataManager's existing TableRegistry instead of creating a new one
582
        m_table_registry_ptr = getDataManager().getTableRegistry();
6✔
583

584
        // Initialize TablePipeline with the existing TableRegistry
585
        m_table_pipeline = std::make_unique<TablePipeline>(m_table_registry_ptr, &getDataManager());
6✔
586
    }
6✔
587

588
    ~LineSamplingTableRegistryTestFixture() = default;
6✔
589

590
    /**
591
     * @brief Get the TableRegistry instance
592
     * @return Reference to the TableRegistry
593
     */
594
    TableRegistry & getTableRegistry() { return *m_table_registry_ptr; }
6✔
595

596
    /**
597
     * @brief Get the TableRegistry instance (const version)
598
     * @return Const reference to the TableRegistry
599
     */
600
    TableRegistry const & getTableRegistry() const { return *m_table_registry_ptr; }
601

602
    /**
603
     * @brief Get a pointer to the TableRegistry
604
     * @return Raw pointer to the TableRegistry
605
     */
606
    TableRegistry * getTableRegistryPtr() { return m_table_registry_ptr; }
607

608
    /**
609
     * @brief Get the TablePipeline instance
610
     * @return Reference to the TablePipeline
611
     */
612
    TablePipeline & getTablePipeline() { return *m_table_pipeline; }
3✔
613

614
    /**
615
     * @brief Get the TablePipeline instance (const version)
616
     * @return Const reference to the TablePipeline
617
     */
618
    TablePipeline const & getTablePipeline() const { return *m_table_pipeline; }
619

620
    /**
621
     * @brief Get a pointer to the TablePipeline
622
     * @return Raw pointer to the TablePipeline
623
     */
624
    TablePipeline * getTablePipelinePtr() { return m_table_pipeline.get(); }
625

626
    /**
627
     * @brief Get the DataManagerExtension instance
628
     */
629
    std::shared_ptr<DataManagerExtension> getDataManagerExtension() {
630
        if (!m_data_manager_extension) {
631
            m_data_manager_extension = std::make_shared<DataManagerExtension>(getDataManager());
632
        }
633
        return m_data_manager_extension;
634
    }
635

636
private:
637
    TableRegistry * m_table_registry_ptr;// Points to DataManager's TableRegistry
638
    std::unique_ptr<TablePipeline> m_table_pipeline;
639
    std::shared_ptr<DataManagerExtension> m_data_manager_extension;// Lazy-initialized
640
};
641

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

644
    SECTION("Test with whisker trace data from fixture") {
2✔
645
        auto & dm = getDataManager();
1✔
646
        auto dme = std::make_shared<DataManagerExtension>(dm);
1✔
647

648
        // Get the line source from the DataManager
649
        auto whisker_source = dme->getLineSource("WhiskerTraces");
3✔
650

651
        REQUIRE(whisker_source != nullptr);
1✔
652

653
        // Create row selector from timestamps where whisker data exists
654
        auto whisker_time_frame = dm.getTime(TimeKey("whisker_time"));
1✔
655
        std::vector<TimeFrameIndex> timestamps = {
1✔
656
                TimeFrameIndex(10), TimeFrameIndex(30), TimeFrameIndex(50), TimeFrameIndex(70), TimeFrameIndex(90)};
3✔
657

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

660
        // Create TableView builder
661
        TableViewBuilder builder(dme);
1✔
662
        builder.setRowSelector(std::move(row_selector));
1✔
663

664
        // Add LineSamplingMultiComputer with different segment counts
665
        auto multi_3seg = std::make_unique<LineSamplingMultiComputer>(
1✔
666
                whisker_source, "WhiskerTraces", whisker_time_frame, 3);
1✔
667
        builder.addColumns<double>("Whisker", std::move(multi_3seg));
3✔
668

669
                // Build the table
670
        TableView table = builder.build();
1✔
671
        
672
        // Verify table structure - 4 segments = 8 columns (x,y per position: 0.0, 0.333, 0.667, 1.0)
673
        // Expected rows: t=10(1) + t=30(2) + t=50(2) + t=70(2) + t=90(2) = 9 rows due to entity expansion
674
        REQUIRE(table.getRowCount() == 9);
1✔
675
        REQUIRE(table.getColumnCount() == 8);
1✔
676

677
        auto column_names = table.getColumnNames();
1✔
678
        REQUIRE(column_names.size() == 8);
1✔
679

680
        // Verify expected column names
681
        REQUIRE(table.hasColumn("Whisker.x@0.000"));
3✔
682
        REQUIRE(table.hasColumn("Whisker.y@0.000"));
3✔
683
        REQUIRE(table.hasColumn("Whisker.x@0.333"));
3✔
684
        REQUIRE(table.hasColumn("Whisker.y@0.333"));
3✔
685
        REQUIRE(table.hasColumn("Whisker.x@0.667"));
3✔
686
        REQUIRE(table.hasColumn("Whisker.y@0.667"));
3✔
687
        REQUIRE(table.hasColumn("Whisker.x@1.000"));
3✔
688
        REQUIRE(table.hasColumn("Whisker.y@1.000"));
3✔
689

690
                // Get sample data to verify reasonable values
691
        auto x_start = table.getColumnValues<double>("Whisker.x@0.000");
3✔
692
        auto y_start = table.getColumnValues<double>("Whisker.y@0.000");
3✔
693
        auto x_end = table.getColumnValues<double>("Whisker.x@1.000");
3✔
694
        auto y_end = table.getColumnValues<double>("Whisker.y@1.000");
3✔
695
        
696
        REQUIRE(x_start.size() == 9);
1✔
697
        REQUIRE(y_start.size() == 9);
1✔
698
        REQUIRE(x_end.size() == 9);
1✔
699
        REQUIRE(y_end.size() == 9);
1✔
700
        
701
        // Verify that whisker curves start at x=0 and end at x=100 (for primary whiskers)
702
        // or x=0 and x=75 (for secondary whiskers)
703
        for (size_t i = 0; i < 9; ++i) {
10✔
704
            REQUIRE(x_start[i] == Catch::Approx(0.0));
9✔
705
            // Primary whiskers end at x=100, secondary at x=75
706
            REQUIRE((x_end[i] == Catch::Approx(100.0) || x_end[i] == Catch::Approx(75.0)));
9✔
707
            // Y values should be reasonable for curved whiskers
708
            REQUIRE(std::abs(y_start[i]) >= 0.0);
9✔
709
        }
710
    }
3✔
711

712
    SECTION("Test with geometric shape data and multiple entities") {
2✔
713
        auto & dm = getDataManager();
1✔
714
        auto dme = std::make_shared<DataManagerExtension>(dm);
1✔
715

716
        // Get the shape source
717
        auto shape_source = dme->getLineSource("GeometricShapes");
3✔
718
        REQUIRE(shape_source != nullptr);
1✔
719

720
        // Create row selector for shape timestamps
721
        auto shape_time_frame = dm.getTime(TimeKey("shape_time"));
1✔
722
        std::vector<TimeFrameIndex> timestamps = {
1✔
723
                TimeFrameIndex(0), TimeFrameIndex(2), TimeFrameIndex(4), TimeFrameIndex(6)};
3✔
724

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

727
        TableViewBuilder builder(dme);
1✔
728
        builder.setRowSelector(std::move(row_selector));
1✔
729

730
        // Add LineSamplingMultiComputer with 1 segment (start and end points only)
731
        auto multi_1seg = std::make_unique<LineSamplingMultiComputer>(
1✔
732
                shape_source, "GeometricShapes", shape_time_frame, 1);
1✔
733
        builder.addColumns<double>("Shape", std::move(multi_1seg));
3✔
734

735
        TableView table = builder.build();
1✔
736

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

742
        auto x_start = table.getColumnValues<double>("Shape.x@0.000");
3✔
743
        auto y_start = table.getColumnValues<double>("Shape.y@0.000");
3✔
744
        auto x_end = table.getColumnValues<double>("Shape.x@1.000");
3✔
745
        auto y_end = table.getColumnValues<double>("Shape.y@1.000");
3✔
746

747
        REQUIRE(x_start.size() == 4);
1✔
748

749
        // Verify square (t=0): starts at (0,0), ends at (0,0) - closed shape
750
        REQUIRE(x_start[0] == Catch::Approx(0.0));
1✔
751
        REQUIRE(y_start[0] == Catch::Approx(0.0));
1✔
752
        REQUIRE(x_end[0] == Catch::Approx(0.0));
1✔
753
        REQUIRE(y_end[0] == Catch::Approx(0.0));
1✔
754

755
        // Verify triangle (t=2): starts at (5,0), ends at (5,0) - closed shape
756
        REQUIRE(x_start[1] == Catch::Approx(5.0));
1✔
757
        REQUIRE(y_start[1] == Catch::Approx(0.0));
1✔
758
        REQUIRE(x_end[1] == Catch::Approx(5.0));
1✔
759
        REQUIRE(y_end[1] == Catch::Approx(0.0));
1✔
760
    }
3✔
761

762
    /*
763
    SECTION("Test cross-timeframe sampling") {
764
        auto & dm = getDataManager();
765
        auto dme = std::make_shared<DataManagerExtension>(dm);
766

767
        // Get sources from different timeframes
768
        auto whisker_source = dme->getLineSource("WhiskerTraces");// whisker_time frame
769
        auto shape_source = dme->getLineSource("GeometricShapes");// shape_time frame
770

771
        REQUIRE(whisker_source != nullptr);
772
        REQUIRE(shape_source != nullptr);
773

774
        // Verify they have different timeframes
775
        auto whisker_tf = whisker_source->getTimeFrame();
776
        auto shape_tf = shape_source->getTimeFrame();
777
        REQUIRE(whisker_tf != shape_tf);
778
        REQUIRE(whisker_tf->getTotalFrameCount() == 101);// whisker_time: 0-100
779
        REQUIRE(shape_tf->getTotalFrameCount() == 11);   // shape_time: 0,10,20,...,100
780

781
        // Create a test with whisker timeframe but sampling shape data
782
        std::vector<TimeFrameIndex> test_timestamps = {
783
                TimeFrameIndex(20), TimeFrameIndex(40)// These exist in shape_time as indices 2,4
784
        };
785

786
        auto row_selector = std::make_unique<TimestampSelector>(test_timestamps, whisker_tf);
787

788
        TableViewBuilder builder(dme);
789
        builder.setRowSelector(std::move(row_selector));
790

791
        // Sample shape data using whisker timeframe
792
        auto multi = std::make_unique<LineSamplingMultiComputer>(
793
                shape_source, "GeometricShapes", shape_tf, 2);
794
        builder.addColumns<double>("CrossFrame", std::move(multi));
795

796
        TableView table = builder.build();
797

798
        REQUIRE(table.getRowCount() == 2);
799
        REQUIRE(table.getColumnCount() == 6);// 3 positions * 2 coordinates
800

801
        auto x_mid = table.getColumnValues<double>("CrossFrame.x@0.500");
802
        auto y_mid = table.getColumnValues<double>("CrossFrame.y@0.500");
803

804
        REQUIRE(x_mid.size() == 2);
805
        REQUIRE(y_mid.size() == 2);
806

807
        // Should have sampled triangle at t=20 and circle at t=40
808
        // Values depend on specific timeframe conversion implementation
809
        REQUIRE(x_mid[0] >= 0.0);// Triangle midpoint
810
        REQUIRE(y_mid[0] >= 0.0);
811
        REQUIRE(x_mid[1] >= 0.0);// Circle midpoint
812
        REQUIRE(y_mid[1] >= 0.0);
813

814
        std::cout << "Cross-timeframe test - Triangle midpoint: (" << x_mid[0]
815
                  << ", " << y_mid[0] << "), Circle midpoint: (" << x_mid[1]
816
                  << ", " << y_mid[1] << ")" << std::endl;
817
    }
818
    */
819
}
2✔
820

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

823
    SECTION("Verify LineSamplingMultiComputer is registered in ComputerRegistry") {
3✔
824
        auto & registry = getTableRegistry().getComputerRegistry();
1✔
825

826
        // Check that LineSamplingMultiComputer is registered
827
        auto line_sample_info = registry.findComputerInfo("Line Sample XY");
3✔
828

829
        REQUIRE(line_sample_info != nullptr);
1✔
830

831
        // Verify computer info details
832
        REQUIRE(line_sample_info->name == "Line Sample XY");
1✔
833
        REQUIRE(line_sample_info->outputType == typeid(double));
1✔
834
        REQUIRE(line_sample_info->outputTypeName == "double");
1✔
835
        REQUIRE(line_sample_info->requiredRowSelector == RowSelectorType::Timestamp);
1✔
836
        REQUIRE(line_sample_info->requiredSourceType == typeid(std::shared_ptr<ILineSource>));
1✔
837
        REQUIRE(line_sample_info->isMultiOutput == true);
1✔
838

839
        // Verify parameter information
840
        REQUIRE(line_sample_info->hasParameters() == true);
1✔
841
        REQUIRE(line_sample_info->parameterDescriptors.size() == 1);
1✔
842
        REQUIRE(line_sample_info->parameterDescriptors[0]->getName() == "segments");
1✔
843
        REQUIRE(line_sample_info->parameterDescriptors[0]->getUIHint() == "number");
1✔
844
    }
3✔
845

846
    SECTION("Create LineSamplingMultiComputer via ComputerRegistry") {
3✔
847
        auto & dm = getDataManager();
1✔
848
        auto dme = std::make_shared<DataManagerExtension>(dm);
1✔
849
        auto & registry = getTableRegistry().getComputerRegistry();
1✔
850

851
        // Get whisker source for testing
852
        auto whisker_source = dme->getLineSource("WhiskerTraces");
3✔
853
        REQUIRE(whisker_source != nullptr);
1✔
854

855
        // Create computer via registry with different segment parameters
856
        std::map<std::string, std::string> params_2seg{{"segments", "2"}};
5✔
857
        std::map<std::string, std::string> params_5seg{{"segments", "5"}};
5✔
858

859
        auto computer_2seg = registry.createTypedMultiComputer<double>(
4✔
860
                "Line Sample XY", whisker_source, params_2seg);
3✔
861
        auto computer_5seg = registry.createTypedMultiComputer<double>(
4✔
862
                "Line Sample XY", whisker_source, params_5seg);
3✔
863

864
        REQUIRE(computer_2seg != nullptr);
1✔
865
        REQUIRE(computer_5seg != nullptr);
1✔
866

867
        // Test that the created computers work correctly
868
        auto whisker_time_frame = dm.getTime(TimeKey("whisker_time"));
1✔
869

870
        // Create a simple test
871
        std::vector<TimeFrameIndex> test_timestamps = {TimeFrameIndex(30)};
3✔
872
        auto row_selector_2seg = std::make_unique<TimestampSelector>(test_timestamps, whisker_time_frame);
1✔
873
        auto row_selector_5seg = std::make_unique<TimestampSelector>(test_timestamps, whisker_time_frame);
1✔
874

875
        // Test 2-segment computer
876
        {
877
            TableViewBuilder builder(dme);
1✔
878
            builder.setRowSelector(std::move(row_selector_2seg));
1✔
879
            builder.addColumns("Registry2Seg", std::move(computer_2seg));
3✔
880

881
            auto table = builder.build();
1✔
882
            REQUIRE(table.getRowCount() == 2);
1✔
883
            REQUIRE(table.getColumnCount() == 6);// 3 positions * 2 coordinates
1✔
884

885
            auto column_names = table.getColumnNames();
1✔
886
            REQUIRE(table.hasColumn("Registry2Seg.x@0.000"));
3✔
887
            REQUIRE(table.hasColumn("Registry2Seg.y@0.000"));
3✔
888
            REQUIRE(table.hasColumn("Registry2Seg.x@0.500"));
3✔
889
            REQUIRE(table.hasColumn("Registry2Seg.y@0.500"));
3✔
890
            REQUIRE(table.hasColumn("Registry2Seg.x@1.000"));
3✔
891
            REQUIRE(table.hasColumn("Registry2Seg.y@1.000"));
3✔
892
        }
1✔
893

894
        // Test 5-segment computer
895
        {
896
            TableViewBuilder builder(dme);
1✔
897
            builder.setRowSelector(std::move(row_selector_5seg));
1✔
898
            builder.addColumns("Registry5Seg", std::move(computer_5seg));
3✔
899

900
            auto table = builder.build();
1✔
901
            REQUIRE(table.getRowCount() == 2);
1✔
902
            REQUIRE(table.getColumnCount() == 12);// 6 positions * 2 coordinates
1✔
903

904
            auto column_names = table.getColumnNames();
1✔
905
            REQUIRE(table.hasColumn("Registry5Seg.x@0.000"));
3✔
906
            REQUIRE(table.hasColumn("Registry5Seg.y@0.000"));
3✔
907
            REQUIRE(table.hasColumn("Registry5Seg.x@0.200"));
3✔
908
            REQUIRE(table.hasColumn("Registry5Seg.y@0.200"));
3✔
909
            REQUIRE(table.hasColumn("Registry5Seg.x@1.000"));
3✔
910
            REQUIRE(table.hasColumn("Registry5Seg.y@1.000"));
3✔
911
        }
1✔
912
    }
4✔
913

914
    SECTION("Compare registry-created vs direct-created computers") {
3✔
915
        auto & dm = getDataManager();
1✔
916
        auto dme = std::make_shared<DataManagerExtension>(dm);
1✔
917
        auto & registry = getTableRegistry().getComputerRegistry();
1✔
918

919
        auto whisker_source = dme->getLineSource("WhiskerTraces");
3✔
920
        REQUIRE(whisker_source != nullptr);
1✔
921

922
        // Create computer via registry
923
        std::map<std::string, std::string> params{{"segments", "3"}};
5✔
924
        auto registry_computer = registry.createTypedMultiComputer<double>(
4✔
925
                "Line Sample XY", whisker_source, params);
3✔
926

927
        // Create computer directly
928
        auto whisker_time_frame = dm.getTime(TimeKey("whisker_time"));
1✔
929
        auto direct_computer = std::make_unique<LineSamplingMultiComputer>(
1✔
930
                whisker_source, "WhiskerTraces", whisker_time_frame, 3);
1✔
931

932
        REQUIRE(registry_computer != nullptr);
1✔
933
        REQUIRE(direct_computer != nullptr);
1✔
934

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

938
        // Registry computer test
939
        auto registry_output_names = registry_computer->getOutputNames();
1✔
940

941
        // Direct computer test
942
        auto direct_output_names = direct_computer->getOutputNames();
1✔
943

944
        // Output names should be identical
945
        REQUIRE(registry_output_names.size() == direct_output_names.size());
1✔
946
        REQUIRE(registry_output_names.size() == 8);// 4 positions * 2 coordinates
1✔
947

948
        for (size_t i = 0; i < registry_output_names.size(); ++i) {
9✔
949
            REQUIRE(registry_output_names[i] == direct_output_names[i]);
8✔
950
        }
951

952
        std::cout << "Comparison test - Both computers produce " << registry_output_names.size()
1✔
953
                  << " identical output names" << std::endl;
1✔
954
    }
4✔
955
}
6✔
956

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

959
    SECTION("Test basic line sampling via JSON pipeline") {
3✔
960
        // JSON configuration for testing LineSamplingMultiComputer through TablePipeline
961
        char const * json_config = R"({
1✔
962
            "metadata": {
963
                "name": "Line Sampling Test",
964
                "description": "Test JSON execution of LineSamplingMultiComputer",
965
                "version": "1.0"
966
            },
967
            "tables": [
968
                {
969
                    "table_id": "line_sampling_test",
970
                    "name": "Line Sampling Test Table",
971
                    "description": "Test table using LineSamplingMultiComputer",
972
                    "row_selector": {
973
                        "type": "timestamp",
974
                        "timestamps": [10, 30, 50, 70, 90],
975
                        "timeframe": "whisker_time"
976
                    },
977
                    "columns": [
978
                        {
979
                            "name": "WhiskerSampling",
980
                            "description": "Sample whisker trace at 3 equally spaced positions",
981
                            "data_source": "WhiskerTraces",
982
                            "computer": "Line Sample XY",
983
                            "parameters": {
984
                                "segments": "2"
985
                            }
986
                        }
987
                    ]
988
                }
989
            ]
990
        })";
991

992
        auto & pipeline = getTablePipeline();
1✔
993

994
        // Parse the JSON configuration
995
        nlohmann::json json_obj = nlohmann::json::parse(json_config);
1✔
996

997
        // Load configuration into pipeline
998
        bool load_success = pipeline.loadFromJson(json_obj);
1✔
999
        REQUIRE(load_success);
1✔
1000

1001
        // Verify configuration was loaded correctly
1002
        auto table_configs = pipeline.getTableConfigurations();
1✔
1003
        REQUIRE(table_configs.size() == 1);
1✔
1004

1005
        auto const & config = table_configs[0];
1✔
1006
        REQUIRE(config.table_id == "line_sampling_test");
1✔
1007
        REQUIRE(config.name == "Line Sampling Test Table");
1✔
1008
        REQUIRE(config.columns.size() == 1);
1✔
1009

1010
        // Verify column configuration
1011
        auto const & column = config.columns[0];
1✔
1012
        REQUIRE(column["name"] == "WhiskerSampling");
1✔
1013
        REQUIRE(column["computer"] == "Line Sample XY");
1✔
1014
        REQUIRE(column["data_source"] == "WhiskerTraces");
1✔
1015
        REQUIRE(column["parameters"]["segments"] == "2");
1✔
1016

1017
        // Verify row selector configuration
1018
        REQUIRE(config.row_selector["type"] == "timestamp");
1✔
1019
        auto timestamps = config.row_selector["timestamps"];
1✔
1020
        REQUIRE(timestamps.size() == 5);
1✔
1021
        REQUIRE(timestamps[0] == 10);
1✔
1022
        REQUIRE(timestamps[4] == 90);
1✔
1023

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

1026
        // Execute the pipeline
1027
        auto pipeline_result = pipeline.execute([](int table_index, std::string const & table_name, int table_progress, int overall_progress) {
2✔
1028
            std::cout << "Building table " << table_index << " (" << table_name << "): "
3✔
1029
                      << table_progress << "% (Overall: " << overall_progress << "%)" << std::endl;
3✔
1030
        });
2✔
1031

1032
        if (pipeline_result.success) {
1✔
1033
            std::cout << "Pipeline executed successfully!" << std::endl;
1✔
1034
            std::cout << "Tables completed: " << pipeline_result.tables_completed << "/" << pipeline_result.total_tables << std::endl;
1✔
1035
            std::cout << "Execution time: " << pipeline_result.total_execution_time_ms << " ms" << std::endl;
1✔
1036

1037
            // Verify the built table exists
1038
            auto & registry = getTableRegistry();
1✔
1039
            REQUIRE(registry.hasTable("line_sampling_test"));
3✔
1040

1041
            // Get the built table and verify its structure
1042
            auto built_table = registry.getBuiltTable("line_sampling_test");
3✔
1043
            REQUIRE(built_table != nullptr);
1✔
1044

1045
            // Check that the table has the expected columns
1046
            auto column_names = built_table->getColumnNames();
1✔
1047
            std::cout << "Built table has " << column_names.size() << " columns" << std::endl;
1✔
1048
            for (auto const & name: column_names) {
7✔
1049
                std::cout << "  Column: " << name << std::endl;
6✔
1050
            }
1051

1052
            REQUIRE(column_names.size() == 6);// 3 positions * 2 coordinates
1✔
1053
            REQUIRE(built_table->hasColumn("WhiskerSampling.x@0.000"));
3✔
1054
            REQUIRE(built_table->hasColumn("WhiskerSampling.y@0.000"));
3✔
1055
            REQUIRE(built_table->hasColumn("WhiskerSampling.x@0.500"));
3✔
1056
            REQUIRE(built_table->hasColumn("WhiskerSampling.y@0.500"));
3✔
1057
            REQUIRE(built_table->hasColumn("WhiskerSampling.x@1.000"));
3✔
1058
            REQUIRE(built_table->hasColumn("WhiskerSampling.y@1.000"));
3✔
1059

1060
                        // 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
1061
            REQUIRE(built_table->getRowCount() == 9);
1✔
1062

1063
            // Get and verify the computed values
1064
            auto x_start = built_table->getColumnValues<double>("WhiskerSampling.x@0.000");
3✔
1065
            auto y_start = built_table->getColumnValues<double>("WhiskerSampling.y@0.000");
3✔
1066
            auto x_mid = built_table->getColumnValues<double>("WhiskerSampling.x@0.500");
3✔
1067
            auto y_mid = built_table->getColumnValues<double>("WhiskerSampling.y@0.500");
3✔
1068
            auto x_end = built_table->getColumnValues<double>("WhiskerSampling.x@1.000");
3✔
1069
            auto y_end = built_table->getColumnValues<double>("WhiskerSampling.y@1.000");
3✔
1070

1071
            REQUIRE(x_start.size() == 9);
1✔
1072
            REQUIRE(y_start.size() == 9);
1✔
1073
            REQUIRE(x_mid.size() == 9);
1✔
1074
            REQUIRE(y_mid.size() == 9);
1✔
1075
            REQUIRE(x_end.size() == 9);
1✔
1076
            REQUIRE(y_end.size() == 9);
1✔
1077

1078
            for (size_t i = 0; i < 9; ++i) {
10✔
1079
                // Whisker traces should start at x=0
1080
                REQUIRE(x_start[i] == Catch::Approx(0.0));
9✔
1081
                // Primary whiskers end at x=100, secondary at x=75
1082
                REQUIRE_THAT(x_end[i], 
9✔
1083
                    Catch::Matchers::WithinAbs(100.0, 1.0) 
1084
                    || Catch::Matchers::WithinAbs(75.0, 1.0));
1085
                // Middle should be around x=50 for primary, x=37.5 for secondary
1086
                REQUIRE_THAT(x_mid[i], 
9✔
1087
                    Catch::Matchers::WithinAbs(50.0, 1.0) 
1088
                    || Catch::Matchers::WithinAbs(37.5, 1.0));
1089
                // Y values should be reasonable for curved whiskers
1090
                REQUIRE(std::abs(y_start[i]) >= 0.0);
9✔
1091
                REQUIRE(std::abs(y_mid[i]) >= 0.0);
9✔
1092
                
1093
                std::cout << "Row " << i << ": Start=(" << x_start[i] << "," << y_start[i] 
9✔
1094
                          << "), Mid=(" << x_mid[i] << "," << y_mid[i] 
9✔
1095
                          << "), End=(" << x_end[i] << "," << y_end[i] << ")" << std::endl;
9✔
1096
            }
1097

1098
        } else {
1✔
UNCOV
1099
            FAIL("Pipeline execution failed: " + pipeline_result.error_message);
×
1100
        }
1101
    }
4✔
1102

1103
    SECTION("Test different segment counts via JSON") {
3✔
1104
        char const * json_config = R"JSON({
1✔
1105
            "metadata": {
1106
                "name": "Line Sampling Segment Test",
1107
                "description": "Test different segment counts for LineSamplingMultiComputer"
1108
            },
1109
            "tables": [
1110
                {
1111
                    "table_id": "line_sampling_segments_test",
1112
                    "name": "Line Sampling Segments Test Table",
1113
                    "description": "Test table with different segment counts",
1114
                    "row_selector": {
1115
                        "type": "timestamp", 
1116
                        "timestamps": [20, 40]
1117
                    },
1118
                    "columns": [
1119
                        {
1120
                            "name": "Shape1Seg",
1121
                            "description": "Sample geometric shapes with 1 segment (start/end only)",
1122
                            "data_source": "GeometricShapes",
1123
                            "computer": "Line Sample XY",
1124
                            "parameters": {
1125
                                "segments": "1"
1126
                            }
1127
                        },
1128
                        {
1129
                            "name": "Shape4Seg",
1130
                            "description": "Sample geometric shapes with 4 segments (5 positions)",
1131
                            "data_source": "GeometricShapes",
1132
                            "computer": "Line Sample XY",
1133
                            "parameters": {
1134
                                "segments": "4"
1135
                            }
1136
                        }
1137
                    ]
1138
                }
1139
            ]
1140
        })JSON";
1141

1142
        auto & pipeline = getTablePipeline();
1✔
1143
        nlohmann::json json_obj = nlohmann::json::parse(json_config);
1✔
1144

1145
        bool load_success = pipeline.loadFromJson(json_obj);
1✔
1146
        REQUIRE(load_success);
1✔
1147

1148
        auto table_configs = pipeline.getTableConfigurations();
1✔
1149
        REQUIRE(table_configs.size() == 1);
1✔
1150

1151
        auto const & config = table_configs[0];
1✔
1152
        REQUIRE(config.columns.size() == 2);
1✔
1153
        REQUIRE(config.columns[0]["parameters"]["segments"] == "1");
1✔
1154
        REQUIRE(config.columns[1]["parameters"]["segments"] == "4");
1✔
1155

1156
        std::cout << "Segment count JSON configuration parsed successfully" << std::endl;
1✔
1157

1158
        auto pipeline_result = pipeline.execute();
1✔
1159

1160
        if (pipeline_result.success) {
1✔
1161
            std::cout << "✓ Segment count pipeline executed successfully!" << std::endl;
1✔
1162

1163
            auto & registry = getTableRegistry();
1✔
1164
            auto built_table = registry.getBuiltTable("line_sampling_segments_test");
3✔
1165
            REQUIRE(built_table != nullptr);
1✔
1166

1167
            REQUIRE(built_table->getRowCount() == 2);    // 2 timestamps
1✔
1168
            REQUIRE(built_table->getColumnCount() == 14);// 1seg(4 cols) + 4seg(10 cols) = 14 total
1✔
1169

1170
            // Verify 1-segment columns (2 positions * 2 coordinates = 4 columns)
1171
            REQUIRE(built_table->hasColumn("Shape1Seg.x@0.000"));
3✔
1172
            REQUIRE(built_table->hasColumn("Shape1Seg.y@0.000"));
3✔
1173
            REQUIRE(built_table->hasColumn("Shape1Seg.x@1.000"));
3✔
1174
            REQUIRE(built_table->hasColumn("Shape1Seg.y@1.000"));
3✔
1175

1176
            // Verify 4-segment columns (5 positions * 2 coordinates = 10 columns)
1177
            REQUIRE(built_table->hasColumn("Shape4Seg.x@0.000"));
3✔
1178
            REQUIRE(built_table->hasColumn("Shape4Seg.y@0.000"));
3✔
1179
            REQUIRE(built_table->hasColumn("Shape4Seg.x@0.250"));
3✔
1180
            REQUIRE(built_table->hasColumn("Shape4Seg.y@0.250"));
3✔
1181
            REQUIRE(built_table->hasColumn("Shape4Seg.x@0.500"));
3✔
1182
            REQUIRE(built_table->hasColumn("Shape4Seg.y@0.500"));
3✔
1183
            REQUIRE(built_table->hasColumn("Shape4Seg.x@0.750"));
3✔
1184
            REQUIRE(built_table->hasColumn("Shape4Seg.y@0.750"));
3✔
1185
            REQUIRE(built_table->hasColumn("Shape4Seg.x@1.000"));
3✔
1186
            REQUIRE(built_table->hasColumn("Shape4Seg.y@1.000"));
3✔
1187

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

1190
        } else {
1✔
UNCOV
1191
            FAIL("Segment count pipeline execution failed: " + pipeline_result.error_message);
×
1192
        }
1193
    }
4✔
1194

1195
    SECTION("Test multiple line data sources via JSON") {
3✔
1196
        char const * json_config = R"JSON({
1✔
1197
            "metadata": {
1198
                "name": "Multi-Source Line Sampling Test",
1199
                "description": "Test multiple line data sources in same table"
1200
            },
1201
            "tables": [
1202
                {
1203
                    "table_id": "multi_source_line_test",
1204
                    "name": "Multi-Source Line Test Table",
1205
                    "description": "Test table with multiple line data sources",
1206
                    "row_selector": {
1207
                        "type": "timestamp",
1208
                        "timestamps": [30, 60]
1209
                    },
1210
                    "columns": [
1211
                        {
1212
                            "name": "WhiskerPoints",
1213
                            "description": "Sample whisker traces at key points",
1214
                            "data_source": "WhiskerTraces",
1215
                            "computer": "Line Sample XY",
1216
                            "parameters": {
1217
                                "segments": "3"
1218
                            }
1219
                        },
1220
                        {
1221
                            "name": "ShapePoints",
1222
                            "description": "Sample geometric shapes at key points",
1223
                            "data_source": "GeometricShapes",
1224
                            "computer": "Line Sample XY",
1225
                            "parameters": {
1226
                                "segments": "3"
1227
                            }
1228
                        }
1229
                    ]
1230
                }
1231
            ]
1232
        })JSON";
1233

1234
        auto & pipeline = getTablePipeline();
1✔
1235
        nlohmann::json json_obj = nlohmann::json::parse(json_config);
1✔
1236

1237
        bool load_success = pipeline.loadFromJson(json_obj);
1✔
1238
        REQUIRE(load_success);
1✔
1239

1240
        auto pipeline_result = pipeline.execute();
1✔
1241

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

1245
            auto & registry = getTableRegistry();
1✔
1246
            auto built_table = registry.getBuiltTable("multi_source_line_test");
3✔
1247
            REQUIRE(built_table != nullptr);
1✔
1248

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

1255
            // Verify whisker columns exist
1256
            REQUIRE(built_table->hasColumn("WhiskerPoints.x@0.000"));
3✔
1257
            REQUIRE(built_table->hasColumn("WhiskerPoints.y@0.333"));
3✔
1258
            REQUIRE(built_table->hasColumn("WhiskerPoints.x@1.000"));
3✔
1259

1260
            // Verify shape columns exist
1261
            REQUIRE(built_table->hasColumn("ShapePoints.x@0.000"));
3✔
1262
            REQUIRE(built_table->hasColumn("ShapePoints.y@0.333"));
3✔
1263
            REQUIRE(built_table->hasColumn("ShapePoints.x@1.000"));
3✔
1264

1265
            std::cout << "✓ Multi-source line sampling completed with "
1✔
1266
                      << built_table->getColumnCount() << " columns" << std::endl;
1✔
1267

1268
        } else {
1✔
UNCOV
1269
            FAIL("Multi-source pipeline execution failed: " + pipeline_result.error_message);
×
1270
        }
1271
    }
4✔
1272
}
3✔
1273

1274
// =============== EntityGroupManager Integration Tests ===============
1275

1276
/**
1277
 * @brief Test fixture for LineSamplingMultiComputer integration with EntityGroupManager
1278
 * 
1279
 * This fixture creates a complete test environment with:
1280
 * - DataManager with EntityGroupManager
1281
 * - LineData with known test lines at multiple time frames
1282
 * - TimeFrame setup for consistent temporal handling
1283
 */
1284
class LineSamplingEntityIntegrationFixture {
1285
public:
1286
    void SetUp() {
1✔
1287
        // Create DataManager
1288
        data_manager = std::make_unique<DataManager>();
1✔
1289
        
1290
        // Create TimeFrame with specific time points
1291
        std::vector<int> time_values;
1✔
1292
        // Fill 0 to 30
1293
        for (int i = 0; i <= 30; ++i) {
32✔
1294
            time_values.push_back(i);
31✔
1295
        }
1296
        time_frame = std::make_shared<TimeFrame>(time_values);
1✔
1297
        data_manager->setTime(TimeKey("test_time"), time_frame);
1✔
1298
        
1299
        // Create LineData with test lines
1300
        line_data = std::make_shared<LineData>();
1✔
1301
        line_data->setTimeFrame(time_frame);
1✔
1302

1303
        setupTestLines();
1✔
1304

1305
        line_data->setIdentityContext("test_lines", data_manager->getEntityRegistry());
3✔
1306
        line_data->rebuildAllEntityIds();
1✔
1307
        
1308
        // Register LineData with DataManager for entity expansion to work
1309
        data_manager->setData<LineData>("test_lines", line_data, TimeKey("test_time"));
3✔
1310
    }
2✔
1311
    
1312
private:
1313
    void setupTestLines() {
1✔
1314
        // Time 10: Add 2 lines
1315
        {
1316
            std::vector<float> xs1 = {0.0f, 10.0f, 20.0f};
3✔
1317
            std::vector<float> ys1 = {0.0f, 5.0f, 10.0f};
3✔
1318
            line_data->addAtTime(TimeFrameIndex(10), xs1, ys1, false);
1✔
1319
            
1320
            std::vector<float> xs2 = {5.0f, 15.0f};
3✔
1321
            std::vector<float> ys2 = {2.0f, 8.0f};
3✔
1322
            line_data->addAtTime(TimeFrameIndex(10), xs2, ys2, false);
1✔
1323
        }
1✔
1324
        
1325
        // Time 20: Add 2 lines
1326
        {
1327
            std::vector<float> xs1 = {1.0f, 11.0f, 21.0f};
3✔
1328
            std::vector<float> ys1 = {1.0f, 6.0f, 11.0f};
3✔
1329
            line_data->addAtTime(TimeFrameIndex(20), xs1, ys1, false);
1✔
1330
            
1331
            std::vector<float> xs2 = {6.0f, 16.0f};
3✔
1332
            std::vector<float> ys2 = {3.0f, 9.0f};
3✔
1333
            line_data->addAtTime(TimeFrameIndex(20), xs2, ys2, false);
1✔
1334
        }
1✔
1335
        
1336
        // Time 30: Add 1 line
1337
        {
1338
            std::vector<float> xs1 = {2.0f, 12.0f, 22.0f, 32.0f};
3✔
1339
            std::vector<float> ys1 = {2.0f, 7.0f, 12.0f, 17.0f};
3✔
1340
            line_data->addAtTime(TimeFrameIndex(30), xs1, ys1, false);
1✔
1341
        }
1✔
1342
    }
1✔
1343
    
1344
public:
1345
    std::unique_ptr<DataManager> data_manager;
1346
    std::shared_ptr<LineData> line_data;
1347
    std::shared_ptr<TimeFrame> time_frame;
1348
};
1349

1350
TEST_CASE_METHOD(LineSamplingEntityIntegrationFixture,
1✔
1351
                 "DM - TV - LineSamplingMultiComputer EntityID Round-trip Integration",
1352
                 "[LineSamplingMultiComputer][EntityGroupManager][integration]") {
1353
    
1354
    SetUp();
1✔
1355
    
1356
    SECTION("TableView creation and EntityID extraction with LineSamplingMultiComputer") {
1✔
1357
        // Get required components
1358
        auto* group_manager = data_manager->getEntityGroupManager();
1✔
1359
        REQUIRE(group_manager != nullptr);
1✔
1360
        
1361
        // Create DataManagerExtension for TableView integration
1362
        auto dme = std::make_shared<DataManagerExtension>(*data_manager);
1✔
1363
        
1364
        // Create LineDataAdapter from our test data
1365
        auto line_adapter = std::make_shared<LineDataAdapter>(line_data, time_frame, "test_lines");
1✔
1366
        
1367
        // Create LineSamplingMultiComputer with 2 segments (3 sample points: 0.0, 0.5, 1.0)
1368
        auto multi_computer = std::make_unique<LineSamplingMultiComputer>(
1✔
1369
            std::static_pointer_cast<ILineSource>(line_adapter),
2✔
1370
            "test_lines",
1371
            time_frame,
1✔
1372
            2  // 2 segments = 3 sample points
1✔
1373
        );
2✔
1374
        
1375
        // Create row selector for our time frames
1376
        std::vector<TimeFrameIndex> timestamps = {TimeFrameIndex(10), TimeFrameIndex(20), TimeFrameIndex(30)};
3✔
1377
        auto row_selector = std::make_unique<TimestampSelector>(timestamps, time_frame);
1✔
1378
        
1379
        // Build TableView using TableViewBuilder
1380
        TableViewBuilder builder(dme);
1✔
1381
        builder.setRowSelector(std::move(row_selector));
1✔
1382
        builder.addColumns<double>("Line", std::move(multi_computer));
3✔
1383
        
1384
        auto table = builder.build();
1✔
1385

1386
        auto line_data_from_table_x = table.getColumnValues<double>("Line.x@0.000");
3✔
1387
        auto line_data_from_table_y = table.getColumnValues<double>("Line.y@0.000");
3✔
1388
        auto line_data_from_table_x_mid = table.getColumnValues<double>("Line.x@0.500");
3✔
1389
        auto line_data_from_table_y_mid = table.getColumnValues<double>("Line.y@0.500");
3✔
1390
        auto line_data_from_table_x_end = table.getColumnValues<double>("Line.x@1.000");
3✔
1391
        auto line_data_from_table_y_end = table.getColumnValues<double>("Line.y@1.000");
3✔
1392
        
1393
        // Verify table structure matches expected entity expansion
1394
        REQUIRE(table.getRowCount() == 5);  // t10:2 + t20:2 + t30:1 = 5 rows (entity expansion)
1✔
1395
        REQUIRE(table.getColumnCount() == 6);  // 3 sample points * 2 coordinates = 6 columns
1✔
1396
        
1397
        // Verify column names are correct
1398
        REQUIRE(table.hasColumn("Line.x@0.000"));
3✔
1399
        REQUIRE(table.hasColumn("Line.y@0.000"));
3✔
1400
        REQUIRE(table.hasColumn("Line.x@0.500"));
3✔
1401
        REQUIRE(table.hasColumn("Line.y@0.500"));
3✔
1402
        REQUIRE(table.hasColumn("Line.x@1.000"));
3✔
1403
        REQUIRE(table.hasColumn("Line.y@1.000"));
3✔
1404
        
1405
        // Verify EntityID information is available for LineSamplingMultiComputer columns
1406
        /*
1407
        REQUIRE(table.hasColumnEntityIds("Line.x@0.000"));
1408
        REQUIRE(table.hasColumnEntityIds("Line.y@0.000"));
1409
        REQUIRE(table.hasColumnEntityIds("Line.x@0.500"));
1410
        REQUIRE(table.hasColumnEntityIds("Line.y@0.500"));
1411
        REQUIRE(table.hasColumnEntityIds("Line.x@1.000"));
1412
        REQUIRE(table.hasColumnEntityIds("Line.y@1.000"));
1413
        */
1414
        
1415
        // Get EntityIDs from one of the columns (all LineSamplingMultiComputer columns should share the same EntityIDs)
1416
        auto column_entity_ids_variant = table.getColumnEntityIds("Line.x@0.000");
3✔
1417
        auto column_entity_ids = std::get<std::vector<EntityId>>(column_entity_ids_variant);
1✔
1418
        REQUIRE(column_entity_ids.size() == 5); // Should match row count
1✔
1419
        
1420
        // Verify all EntityIDs are valid (non-zero)
1421
        for (EntityId id : column_entity_ids) {
6✔
1422
            REQUIRE(id != 0);
5✔
1423
            INFO("Column EntityID: " << id);
5✔
1424
        }
5✔
1425
        
1426
        // Verify that all LineSamplingMultiComputer columns have the same EntityIDs
1427
        auto y_start_entity_ids_variant = table.getColumnEntityIds("Line.y@0.000");
3✔
1428
        auto x_mid_entity_ids_variant = table.getColumnEntityIds("Line.x@0.500");
3✔
1429
        auto y_mid_entity_ids_variant = table.getColumnEntityIds("Line.y@0.500");
3✔
1430
        auto x_end_entity_ids_variant = table.getColumnEntityIds("Line.x@1.000");
3✔
1431
        auto y_end_entity_ids_variant = table.getColumnEntityIds("Line.y@1.000");
3✔
1432
        
1433
        // Extract EntityIDs from variants (LineSamplingMultiComputer uses Simple structure)
1434
        REQUIRE(std::holds_alternative<std::vector<EntityId>>(y_start_entity_ids_variant));
1✔
1435
        REQUIRE(std::holds_alternative<std::vector<EntityId>>(x_mid_entity_ids_variant));
1✔
1436
        REQUIRE(std::holds_alternative<std::vector<EntityId>>(y_mid_entity_ids_variant));
1✔
1437
        REQUIRE(std::holds_alternative<std::vector<EntityId>>(x_end_entity_ids_variant));
1✔
1438
        REQUIRE(std::holds_alternative<std::vector<EntityId>>(y_end_entity_ids_variant));
1✔
1439
        
1440
        auto y_start_entity_ids = std::get<std::vector<EntityId>>(y_start_entity_ids_variant);
1✔
1441
        auto x_mid_entity_ids = std::get<std::vector<EntityId>>(x_mid_entity_ids_variant);
1✔
1442
        auto y_mid_entity_ids = std::get<std::vector<EntityId>>(y_mid_entity_ids_variant);
1✔
1443
        auto x_end_entity_ids = std::get<std::vector<EntityId>>(x_end_entity_ids_variant);
1✔
1444
        auto y_end_entity_ids = std::get<std::vector<EntityId>>(y_end_entity_ids_variant);
1✔
1445
        
1446
        // Compare extracted EntityID vectors
1447
        REQUIRE(y_start_entity_ids == column_entity_ids);
1✔
1448
        REQUIRE(x_mid_entity_ids == column_entity_ids);
1✔
1449
        REQUIRE(y_mid_entity_ids == column_entity_ids);
1✔
1450
        REQUIRE(x_end_entity_ids == column_entity_ids);
1✔
1451
        REQUIRE(y_end_entity_ids == column_entity_ids);
1✔
1452
        
1453
        // Get sample data from table columns
1454
        auto x_start = table.getColumnValues<double>("Line.x@0.000");
3✔
1455
        auto y_start = table.getColumnValues<double>("Line.y@0.000");
3✔
1456
        auto x_mid = table.getColumnValues<double>("Line.x@0.500");
3✔
1457
        auto y_mid = table.getColumnValues<double>("Line.y@0.500");
3✔
1458
        auto x_end = table.getColumnValues<double>("Line.x@1.000");
3✔
1459
        auto y_end = table.getColumnValues<double>("Line.y@1.000");
3✔
1460
        
1461
        REQUIRE(x_start.size() == 5);
1✔
1462
        REQUIRE(y_start.size() == 5);
1✔
1463
        REQUIRE(x_mid.size() == 5);
1✔
1464
        REQUIRE(y_mid.size() == 5);
1✔
1465
        REQUIRE(x_end.size() == 5);
1✔
1466
        REQUIRE(y_end.size() == 5);
1✔
1467
        
1468
        // Select specific rows for our group (e.g., rows 1, 2, and 4)
1469
        std::vector<size_t> selected_row_indices = {1, 2, 4};
3✔
1470
        std::vector<EntityId> selected_entity_ids;
1✔
1471
        
1472
        for (size_t row_idx : selected_row_indices) {
4✔
1473
            REQUIRE(row_idx < column_entity_ids.size());
3✔
1474
            selected_entity_ids.push_back(column_entity_ids[row_idx]);
3✔
1475
        }
1476
        
1477
        REQUIRE(selected_entity_ids.size() == 3);
1✔
1478
        
1479
        // Verify all selected EntityIDs are valid
1480
        for (EntityId id : selected_entity_ids) {
4✔
1481
            REQUIRE(id != 0);
3✔
1482
            INFO("Selected EntityID: " << id);
3✔
1483
        }
3✔
1484
        
1485
        // Create a group in EntityGroupManager with these EntityIDs
1486
        GroupId test_group = group_manager->createGroup("LineSampling Selection", "Entities from selected table rows");
5✔
1487
        size_t added = group_manager->addEntitiesToGroup(test_group, selected_entity_ids);
1✔
1488
        REQUIRE(added == selected_entity_ids.size());
1✔
1489
        
1490
        // Verify the group was created correctly
1491
        REQUIRE(group_manager->hasGroup(test_group));
1✔
1492
        REQUIRE(group_manager->getGroupSize(test_group) == selected_entity_ids.size());
1✔
1493
        
1494
        auto group_entities = group_manager->getEntitiesInGroup(test_group);
1✔
1495
        REQUIRE(group_entities.size() == selected_entity_ids.size());
1✔
1496
        
1497
        // Now query LineData using the grouped EntityIDs to get the original line data
1498
        auto lines_from_group = line_data->getLinesByEntityIds(group_entities);
1✔
1499
        REQUIRE(lines_from_group.size() == selected_entity_ids.size());
1✔
1500
        
1501
        // Verify that the lines we get back match the data in the corresponding table rows
1502
        // We'll compare the start and end points from LineSamplingMultiComputer with actual line data
1503
        
1504
        for (size_t i = 0; i < lines_from_group.size(); ++i) {
4✔
1505
            EntityId entity_id = lines_from_group[i].first;
3✔
1506
            Line2D const& original_line = lines_from_group[i].second;
3✔
1507
            
1508
            // Find which row this EntityID corresponds to in our selected rows
1509
            size_t table_row_index = 0;
3✔
1510
            bool found = false;
3✔
1511
            for (size_t j = 0; j < selected_row_indices.size(); ++j) {
6✔
1512
                if (entity_id == selected_entity_ids[j]) {
6✔
1513
                    table_row_index = selected_row_indices[j];
3✔
1514
                    found = true;
3✔
1515
                    break;
3✔
1516
                }
1517
            }
1518
            
1519
            if (!found) {
3✔
UNCOV
1520
                FAIL("Unexpected EntityID in group: " << entity_id);
×
1521
            }
1522
            
1523
            // Get the sampled points from the table for this row
1524
            float table_x_start = static_cast<float>(x_start[table_row_index]);
3✔
1525
            float table_y_start = static_cast<float>(y_start[table_row_index]);
3✔
1526
            float table_x_end = static_cast<float>(x_end[table_row_index]);
3✔
1527
            float table_y_end = static_cast<float>(y_end[table_row_index]);
3✔
1528
            
1529
            // Get actual start and end points from the original line
1530
            REQUIRE(original_line.size() >= 2);
3✔
1531
            Point2D<float> actual_start = original_line.front();
3✔
1532
            Point2D<float> actual_end = original_line.back();
3✔
1533
            
1534
            // Verify that the table data matches the original line data
1535
            INFO("Checking EntityID " << entity_id << " at table row " << table_row_index);
3✔
1536
            INFO("Table start: (" << table_x_start << ", " << table_y_start << ")");
3✔
1537
            INFO("Actual start: (" << actual_start.x << ", " << actual_start.y << ")");
3✔
1538
            INFO("Table end: (" << table_x_end << ", " << table_y_end << ")");
3✔
1539
            INFO("Actual end: (" << actual_end.x << ", " << actual_end.y << ")");
3✔
1540
            
1541
            REQUIRE(table_x_start == Catch::Approx(actual_start.x).epsilon(0.001f));
3✔
1542
            REQUIRE(table_y_start == Catch::Approx(actual_start.y).epsilon(0.001f));
3✔
1543
            REQUIRE(table_x_end == Catch::Approx(actual_end.x).epsilon(0.001f));
3✔
1544
            REQUIRE(table_y_end == Catch::Approx(actual_end.y).epsilon(0.001f));
3✔
1545
        }
3✔
1546
        
1547
        INFO("Successfully verified round-trip: LineData -> LineSamplingMultiComputer -> TableView -> EntityGroupManager -> LineData");
1✔
1548
    }
2✔
1549
}
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