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

paulmthompson / WhiskerToolbox / 18389801194

09 Oct 2025 09:35PM UTC coverage: 71.943% (+0.1%) from 71.826%
18389801194

push

github

paulmthompson
add correlation matrix to filtering interface

207 of 337 new or added lines in 5 files covered. (61.42%)

867 existing lines in 31 files now uncovered.

49964 of 69449 relevant lines covered (71.94%)

1103.53 hits per line

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

98.05
/src/DataManager/Masks/Mask_Data.test.cpp
1
#include "Masks/Mask_Data.hpp"
2
#include "TimeFrame/interval_data.hpp"
3
#include "TimeFrame/TimeFrame.hpp"
4
#include "Entity/EntityRegistry.hpp"
5
#include "DataManager.hpp"
6
#include <catch2/catch_test_macros.hpp>
7

8
#include <vector>
9
#include <algorithm>
10

11
TEST_CASE("MaskData - Core functionality", "[mask][data][core]") {
11✔
12
    MaskData mask_data;
11✔
13

14
    // Setup some test data
15
    std::vector<uint32_t> x1 = {1, 2, 3, 1};
33✔
16
    std::vector<uint32_t> y1 = {1, 1, 2, 2};
33✔
17

18
    std::vector<uint32_t> x2 = {4, 5, 6, 4};
33✔
19
    std::vector<uint32_t> y2 = {3, 3, 4, 4};
33✔
20

21
    std::vector<Point2D<uint32_t>> points = {
11✔
22
            {10, 10},
23
            {11, 10},
24
            {11, 11},
25
            {10, 11}
26
    };
33✔
27

28
    SECTION("Adding masks at time") {
11✔
29
        // Add first mask at time 0
30
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1);
1✔
31

32
        auto masks_at_0 = mask_data.getAtTime(TimeFrameIndex(0));
1✔
33
        REQUIRE(masks_at_0.size() == 1);
1✔
34
        REQUIRE(masks_at_0[0].size() == 4);
1✔
35
        REQUIRE(masks_at_0[0][0].x == 1);
1✔
36
        REQUIRE(masks_at_0[0][0].y == 1);
1✔
37

38
        // Add second mask at time 0
39
        mask_data.addAtTime(TimeFrameIndex(0), x2, y2);
1✔
40
        masks_at_0 = mask_data.getAtTime(TimeFrameIndex(0));
1✔
41
        REQUIRE(masks_at_0.size() == 2);
1✔
42
        REQUIRE(masks_at_0[1].size() == 4);
1✔
43
        REQUIRE(masks_at_0[1][0].x == 4);
1✔
44

45
        // Add mask at new time 10
46
        mask_data.addAtTime(TimeFrameIndex(10), points);
1✔
47
        auto masks_at_10 = mask_data.getAtTime(TimeFrameIndex(10));
1✔
48
        REQUIRE(masks_at_10.size() == 1);
1✔
49
        REQUIRE(masks_at_10[0].size() == 4);
1✔
50
        REQUIRE(masks_at_10[0][0].x == 10);
1✔
51
    }
12✔
52

53
    SECTION("Clearing masks at time") {
11✔
54
        // Add masks and then clear them
55
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1);
1✔
56
        mask_data.addAtTime(TimeFrameIndex(0), x2, y2);
1✔
57
        mask_data.addAtTime(TimeFrameIndex(10), points);
1✔
58

59
        static_cast<void>(mask_data.clearAtTime(TimeFrameIndex(0)));
1✔
60

61
        auto masks_at_0 = mask_data.getAtTime(TimeFrameIndex(0));
1✔
62
        auto masks_at_10 = mask_data.getAtTime(TimeFrameIndex(10));
1✔
63

64
        REQUIRE(masks_at_0.empty());
1✔
65
        REQUIRE(masks_at_10.size() == 1);
1✔
66
    }
12✔
67

68
    SECTION("Getting masks as range") {
11✔
69
        // Add multiple masks at different times
70
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1);
1✔
71
        mask_data.addAtTime(TimeFrameIndex(0), x2, y2);
1✔
72
        mask_data.addAtTime(TimeFrameIndex(10), points);
1✔
73

74
        auto range = mask_data.getAllAsRange();
1✔
75

76
        // Count items in range
77
        size_t count = 0;
1✔
78
        TimeFrameIndex first_time = TimeFrameIndex(-1);
1✔
79
        size_t first_size = 0;
1✔
80

81
        for (auto const& pair : range) {
3✔
82
            if (count == 0) {
2✔
83
                first_time = pair.time;
1✔
84
                first_size = pair.masks.size();
1✔
85
            }
86
            count++;
2✔
87
        }
2✔
88

89
        REQUIRE(count == 2);  // 2 different times: 0 and 10
1✔
90
        REQUIRE(first_time == TimeFrameIndex(0));
1✔
91
        REQUIRE(first_size == 2);  // 2 masks at time 0
1✔
92
    }   
11✔
93

94
    SECTION("Setting and getting image size") {
11✔
95
        ImageSize size{640, 480};
1✔
96
        mask_data.setImageSize(size);
1✔
97

98
        auto retrieved_size = mask_data.getImageSize();
1✔
99
        REQUIRE(retrieved_size.width == 640);
1✔
100
        REQUIRE(retrieved_size.height == 480);
1✔
101
    }
11✔
102

103
    SECTION("GetMasksInRange functionality") {
11✔
104
        // Setup data at multiple time points
105
        mask_data.addAtTime(TimeFrameIndex(5), x1, y1);       // 1 mask
7✔
106
        mask_data.addAtTime(TimeFrameIndex(10), x1, y1);      // 1 mask  
7✔
107
        mask_data.addAtTime(TimeFrameIndex(10), x2, y2);      // 2nd mask at same time
7✔
108
        mask_data.addAtTime(TimeFrameIndex(15), points);      // 1 mask
7✔
109
        mask_data.addAtTime(TimeFrameIndex(20), points);      // 1 mask
7✔
110
        mask_data.addAtTime(TimeFrameIndex(25), x1, y1);      // 1 mask
7✔
111

112
        SECTION("Range includes some data") {
7✔
113
            TimeFrameInterval interval{TimeFrameIndex(10), TimeFrameIndex(20)};
1✔
114
            size_t count = 0;
1✔
115
            for (const auto& pair : mask_data.GetMasksInRange(interval)) {
4✔
116
                if (count == 0) {
3✔
117
                    REQUIRE(pair.time.getValue() == 10);
1✔
118
                    REQUIRE(pair.masks.size() == 2);  // 2 masks at time 10
1✔
119
                } else if (count == 1) {
2✔
120
                    REQUIRE(pair.time.getValue() == 15);
1✔
121
                    REQUIRE(pair.masks.size() == 1);
1✔
122
                } else if (count == 2) {
1✔
123
                    REQUIRE(pair.time.getValue() == 20);
1✔
124
                    REQUIRE(pair.masks.size() == 1);
1✔
125
                }
126
                count++;
3✔
127
            }
3✔
128
            REQUIRE(count == 3); // Should include times 10, 15, 20
1✔
129
        }
7✔
130

131
        SECTION("Range includes all data") {
7✔
132
            TimeFrameInterval interval{TimeFrameIndex(0), TimeFrameIndex(30)};
1✔
133
            size_t count = 0;
1✔
134
            for (const auto& pair : mask_data.GetMasksInRange(interval)) {
6✔
135
                count++;
5✔
136
            }
5✔
137
            REQUIRE(count == 5); // Should include all 5 time points
1✔
138
        }
7✔
139

140
        SECTION("Range includes no data") {
7✔
141
            TimeFrameInterval interval{TimeFrameIndex(100), TimeFrameIndex(200)};
1✔
142
            size_t count = 0;
1✔
143
            for (const auto& pair : mask_data.GetMasksInRange(interval)) {
1✔
UNCOV
144
                count++;
×
UNCOV
145
            }
×
146
            REQUIRE(count == 0); // Should be empty
1✔
147
        }
7✔
148

149
        SECTION("Range with single time point") {
7✔
150
            TimeFrameInterval interval{TimeFrameIndex(15), TimeFrameIndex(15)};
1✔
151
            size_t count = 0;
1✔
152
            for (const auto& pair : mask_data.GetMasksInRange(interval)) {
2✔
153
                REQUIRE(pair.time.getValue() == 15);
1✔
154
                REQUIRE(pair.masks.size() == 1);
1✔
155
                count++;
1✔
156
            }
1✔
157
            REQUIRE(count == 1); // Should include only time 15
1✔
158
        }
7✔
159

160
        SECTION("Range with start > end") {
7✔
161
            TimeFrameInterval interval{TimeFrameIndex(20), TimeFrameIndex(10)};
1✔
162
            size_t count = 0;
1✔
163
            for (const auto& pair : mask_data.GetMasksInRange(interval)) {
1✔
UNCOV
164
                count++;
×
UNCOV
165
            }
×
166
            REQUIRE(count == 0); // Should be empty when start > end
1✔
167
        }
7✔
168

169
        SECTION("Range with timeframe conversion - same timeframes") {
7✔
170
            // Test with same source and target timeframes
171
            std::vector<int> times = {5, 10, 15, 20, 25};
3✔
172
            auto timeframe = std::make_shared<TimeFrame>(times);
1✔
173
            
174
            TimeFrameInterval interval{TimeFrameIndex(10), TimeFrameIndex(20)};
1✔
175
            size_t count = 0;
1✔
176
            for (const auto& pair : mask_data.GetMasksInRange(interval, timeframe, timeframe)) {
4✔
177
                if (count == 0) {
3✔
178
                    REQUIRE(pair.time.getValue() == 10);
1✔
179
                    REQUIRE(pair.masks.size() == 2);
1✔
180
                } else if (count == 1) {
2✔
181
                    REQUIRE(pair.time.getValue() == 15);
1✔
182
                    REQUIRE(pair.masks.size() == 1);
1✔
183
                } else if (count == 2) {
1✔
184
                    REQUIRE(pair.time.getValue() == 20);
1✔
185
                    REQUIRE(pair.masks.size() == 1);
1✔
186
                }
187
                count++;
3✔
188
            }
3✔
189
            REQUIRE(count == 3); // Should include times 10, 15, 20
1✔
190
        }
8✔
191

192
        SECTION("Range with timeframe conversion - different timeframes") {
7✔
193
            // Create a separate mask data instance for timeframe conversion test
194
            MaskData timeframe_test_data;
1✔
195
            
196
            // Create source timeframe (video frames)
197
            std::vector<int> video_times = {0, 10, 20, 30, 40};  
3✔
198
            auto video_timeframe = std::make_shared<TimeFrame>(video_times);
1✔
199
            
200
            // Create target timeframe (data sampling)
201
            std::vector<int> data_times = {0, 5, 10, 15, 20, 25, 30, 35, 40}; 
3✔
202
            auto data_timeframe = std::make_shared<TimeFrame>(data_times);
1✔
203
            
204
            // Add data at target timeframe indices
205
            timeframe_test_data.addAtTime(TimeFrameIndex(2), x1, y1);  // At data timeframe index 2 (time=10)
1✔
206
            timeframe_test_data.addAtTime(TimeFrameIndex(3), points);  // At data timeframe index 3 (time=15)
1✔
207
            timeframe_test_data.addAtTime(TimeFrameIndex(4), x2, y2);  // At data timeframe index 4 (time=20)
1✔
208
            
209
            // Query video frames 1-2 (times 10-20) which should map to data indices 2-4 (times 10-20)
210
            TimeFrameInterval video_interval{TimeFrameIndex(1), TimeFrameIndex(2)};
1✔
211
            size_t count = 0;
1✔
212
            for (const auto& pair : timeframe_test_data.GetMasksInRange(video_interval, video_timeframe, data_timeframe)) {
4✔
213
                if (count == 0) {
3✔
214
                    REQUIRE(pair.time.getValue() == 2);
1✔
215
                    REQUIRE(pair.masks.size() == 1);
1✔
216
                } else if (count == 1) {
2✔
217
                    REQUIRE(pair.time.getValue() == 3);
1✔
218
                    REQUIRE(pair.masks.size() == 1);
1✔
219
                } else if (count == 2) {
1✔
220
                    REQUIRE(pair.time.getValue() == 4);
1✔
221
                    REQUIRE(pair.masks.size() == 1);
1✔
222
                }
223
                count++;
3✔
224
            }
3✔
225
            REQUIRE(count == 3); // Should include converted times 2, 3, 4
1✔
226
        }
8✔
227
    }
11✔
228
}
22✔
229

230
TEST_CASE("MaskData - Copy and Move by EntityID", "[mask][data][entity][copy][move][by_id]") {
6✔
231
    // Setup test data vectors
232
    std::vector<uint32_t> x1 = {1, 2, 3, 1};
18✔
233
    std::vector<uint32_t> y1 = {1, 1, 2, 2};
18✔
234
    std::vector<uint32_t> x2 = {4, 5, 6, 4};
18✔
235
    std::vector<uint32_t> y2 = {3, 3, 4, 4};
18✔
236
    std::vector<Point2D<uint32_t>> points = {{10, 10}, {11, 10}, {11, 11}};
18✔
237

238
    auto data_manager = std::make_unique<DataManager>();
6✔
239
    auto time_frame = std::make_shared<TimeFrame>(std::vector<int>{0, 10, 20, 30});
18✔
240
    data_manager->setTime(TimeKey("test_time"), time_frame);
6✔
241

242
    SECTION("Copy masks by EntityID - basic functionality") {
6✔
243
        data_manager->setData<MaskData>("source_data", TimeKey("test_time"));
3✔
244
        data_manager->setData<MaskData>("target_data", TimeKey("test_time"));
3✔
245

246
        auto source_data = data_manager->getData<MaskData>("source_data");
3✔
247
        auto target_data = data_manager->getData<MaskData>("target_data");
3✔
248

249
        source_data->addAtTime(TimeFrameIndex(10), x1, y1);
1✔
250
        source_data->addAtTime(TimeFrameIndex(10), x2, y2);
1✔
251
        source_data->addAtTime(TimeFrameIndex(20), points);
1✔
252

253
        auto entity_ids_10 = source_data->getEntityIdsAtTime(TimeFrameIndex(10));
1✔
254
        REQUIRE(entity_ids_10.size() == 2);
1✔
255

256
        std::size_t copied = source_data->copyMasksByEntityIds(*target_data, entity_ids_10);
1✔
257
        REQUIRE(copied == 2);
1✔
258
        REQUIRE(target_data->getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
259
        auto target_entity_ids = target_data->getAllEntityIds();
1✔
260
        REQUIRE(target_entity_ids.size() == 2);
1✔
261
        REQUIRE(target_entity_ids != entity_ids_10);
1✔
262
    }
7✔
263

264
    SECTION("Copy masks by EntityID - mixed times") {
6✔
265
        data_manager->setData<MaskData>("source_data", TimeKey("test_time"));
3✔
266
        data_manager->setData<MaskData>("target_data", TimeKey("test_time"));
3✔
267

268
        auto source_data = data_manager->getData<MaskData>("source_data");
3✔
269
        auto target_data = data_manager->getData<MaskData>("target_data");
3✔
270

271
        source_data->addAtTime(TimeFrameIndex(10), x1, y1);
1✔
272
        source_data->addAtTime(TimeFrameIndex(20), points);
1✔
273

274
        auto ids_10 = source_data->getEntityIdsAtTime(TimeFrameIndex(10));
1✔
275
        auto ids_20 = source_data->getEntityIdsAtTime(TimeFrameIndex(20));
1✔
276
        REQUIRE(ids_10.size() == 1);
1✔
277
        REQUIRE(ids_20.size() == 1);
1✔
278

279
        std::vector<EntityId> mixed = {ids_10[0], ids_20[0]};
3✔
280
        std::size_t copied = source_data->copyMasksByEntityIds(*target_data, mixed);
1✔
281
        REQUIRE(copied == 2);
1✔
282
        REQUIRE(target_data->getAtTime(TimeFrameIndex(10)).size() == 1);
1✔
283
        REQUIRE(target_data->getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
284
    }
7✔
285

286
    SECTION("Copy masks by EntityID - non-existent EntityIDs") {
6✔
287
        data_manager->setData<MaskData>("source_data", TimeKey("test_time"));
3✔
288
        data_manager->setData<MaskData>("target_data", TimeKey("test_time"));
3✔
289

290
        auto source_data = data_manager->getData<MaskData>("source_data");
3✔
291
        auto target_data = data_manager->getData<MaskData>("target_data");
3✔
292

293
        source_data->addAtTime(TimeFrameIndex(10), x1, y1);
1✔
294
        std::vector<EntityId> fake_ids = {99999, 88888};
3✔
295
        std::size_t copied = source_data->copyMasksByEntityIds(*target_data, fake_ids);
1✔
296
        REQUIRE(copied == 0);
1✔
297
        REQUIRE(target_data->getTimesWithData().empty());
1✔
298
    }
7✔
299

300
    SECTION("Move masks by EntityID - basic functionality") {
6✔
301
        data_manager->setData<MaskData>("source_data", TimeKey("test_time"));
3✔
302
        data_manager->setData<MaskData>("target_data", TimeKey("test_time"));
3✔
303

304
        auto source_data = data_manager->getData<MaskData>("source_data");
3✔
305
        auto target_data = data_manager->getData<MaskData>("target_data");
3✔
306

307
        source_data->addAtTime(TimeFrameIndex(10), x1, y1);
1✔
308
        source_data->addAtTime(TimeFrameIndex(10), x2, y2);
1✔
309
        auto ids_10 = source_data->getEntityIdsAtTime(TimeFrameIndex(10));
1✔
310
        REQUIRE(ids_10.size() == 2);
1✔
311

312
        std::size_t moved = source_data->moveMasksByEntityIds(*target_data, ids_10);
1✔
313
        REQUIRE(moved == 2);
1✔
314
        REQUIRE(source_data->getAtTime(TimeFrameIndex(10)).size() == 0);
1✔
315
        REQUIRE(target_data->getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
316
        auto target_entity_ids = target_data->getAllEntityIds();
1✔
317
        REQUIRE(target_entity_ids == ids_10);
1✔
318
    }
7✔
319

320
    SECTION("Move masks by EntityID - mixed times") {
6✔
321
        data_manager->setData<MaskData>("source_data", TimeKey("test_time"));
3✔
322
        data_manager->setData<MaskData>("target_data", TimeKey("test_time"));
3✔
323

324
        auto source_data = data_manager->getData<MaskData>("source_data");
3✔
325
        auto target_data = data_manager->getData<MaskData>("target_data");
3✔
326

327
        source_data->addAtTime(TimeFrameIndex(10), x1, y1);
1✔
328
        source_data->addAtTime(TimeFrameIndex(20), points);
1✔
329

330
        auto ids_10 = source_data->getEntityIdsAtTime(TimeFrameIndex(10));
1✔
331
        auto ids_20 = source_data->getEntityIdsAtTime(TimeFrameIndex(20));
1✔
332
        std::vector<EntityId> mixed = {ids_10[0], ids_20[0]};
3✔
333
        std::size_t moved = source_data->moveMasksByEntityIds(*target_data, mixed);
1✔
334
        REQUIRE(moved == 2);
1✔
335
        REQUIRE(source_data->getAtTime(TimeFrameIndex(10)).size() == 0);
1✔
336
        REQUIRE(source_data->getAtTime(TimeFrameIndex(20)).size() == 0);
1✔
337
        REQUIRE(target_data->getAtTime(TimeFrameIndex(10)).size() == 1);
1✔
338
        REQUIRE(target_data->getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
339
    }
7✔
340

341
    SECTION("Move masks by EntityID - non-existent EntityIDs") {
6✔
342
        data_manager->setData<MaskData>("source_data", TimeKey("test_time"));
3✔
343
        data_manager->setData<MaskData>("target_data", TimeKey("test_time"));
3✔
344

345
        auto source_data = data_manager->getData<MaskData>("source_data");
3✔
346
        auto target_data = data_manager->getData<MaskData>("target_data");
3✔
347

348
        source_data->addAtTime(TimeFrameIndex(10), x1, y1);
1✔
349
        std::vector<EntityId> fake_ids = {99999, 88888};
3✔
350
        std::size_t moved = source_data->moveMasksByEntityIds(*target_data, fake_ids);
1✔
351
        REQUIRE(moved == 0);
1✔
352
        REQUIRE(target_data->getTimesWithData().empty());
1✔
353
        REQUIRE(source_data->getAtTime(TimeFrameIndex(10)).size() == 1);
1✔
354
    }
7✔
355
}
12✔
356

357
TEST_CASE("MaskData - Observer notification", "[mask][data][observer]") {
3✔
358
    MaskData mask_data;
3✔
359

360
    // Setup some test data
361
    std::vector<uint32_t> x1 = {1, 2, 3, 1};
9✔
362
    std::vector<uint32_t> y1 = {1, 1, 2, 2};
9✔
363

364
    int notification_count = 0;
3✔
365
    int observer_id = mask_data.addObserver([&notification_count]() {
3✔
366
        notification_count++;
4✔
367
    });
3✔
368

369
    SECTION("Notification on clearAtTime") {
3✔
370
        // First add a mask
371
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1, false);  // Don't notify
1✔
372
        REQUIRE(notification_count == 0);
1✔
373

374
        // Clear with notification
375
        REQUIRE(mask_data.clearAtTime(TimeFrameIndex(0)));
1✔
376
        REQUIRE(notification_count == 1);
1✔
377

378
        // Clear with notification disabled (should return false - nothing to clear)
379
        REQUIRE_FALSE(mask_data.clearAtTime(TimeFrameIndex(0), false));
1✔
380
        REQUIRE(notification_count == 1);  // Still 1, not incremented
1✔
381

382
        // Clear non-existent time (shouldn't notify)
383
        REQUIRE_FALSE(mask_data.clearAtTime(TimeFrameIndex(42)));
1✔
384
        REQUIRE(notification_count == 1);  // Still 1, not incremented
1✔
385
    }
3✔
386

387
    SECTION("Notification on addAtTime") {
3✔
388
        // Add with notification
389
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1);
1✔
390
        REQUIRE(notification_count == 1);
1✔
391

392
        // Add with notification disabled
393
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1, false);
1✔
394
        REQUIRE(notification_count == 1);  // Still 1, not incremented
1✔
395

396
        // Add using point vector with notification
397
        std::vector<Point2D<uint32_t>> points = {{1, 1}, {2, 2}};
3✔
398
        mask_data.addAtTime(TimeFrameIndex(1), points);
1✔
399
        REQUIRE(notification_count == 2);
1✔
400

401
        // Add using point vector with notification disabled
402
        mask_data.addAtTime(TimeFrameIndex(1), points, false);
1✔
403
        REQUIRE(notification_count == 2);  // Still 2, not incremented
1✔
404
    }
4✔
405

406
    SECTION("Multiple operations with single notification") {
3✔
407
        // Perform multiple operations without notifying
408
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1, false);
1✔
409
        mask_data.addAtTime(TimeFrameIndex(1), x1, y1, false);
1✔
410

411
        REQUIRE(notification_count == 0);
1✔
412

413
        // Now manually notify once
414
        mask_data.notifyObservers();
1✔
415
        REQUIRE(notification_count == 1);
1✔
416
    }
3✔
417
}
6✔
418

419
TEST_CASE("MaskData - Edge cases and error handling", "[mask][data][error]") {
5✔
420
    MaskData mask_data;
5✔
421

422
    SECTION("Getting masks at non-existent time") {
5✔
423
        auto masks = mask_data.getAtTime(TimeFrameIndex(999));
1✔
424
        REQUIRE(masks.empty());
1✔
425
    }
6✔
426

427
    SECTION("Adding masks with empty point vectors") {
5✔
428
        std::vector<uint32_t> empty_x;
1✔
429
        std::vector<uint32_t> empty_y;
1✔
430

431
        // This shouldn't crash
432
        mask_data.addAtTime(TimeFrameIndex(0), empty_x, empty_y);
1✔
433

434
        auto masks = mask_data.getAtTime(TimeFrameIndex(0));
1✔
435
        REQUIRE(masks.size() == 1);
1✔
436
        REQUIRE(masks[0].empty());
1✔
437
    }
6✔
438

439
    SECTION("Clearing masks at non-existent time") {
5✔
440
        // Should not create an entry with empty vector
441
        REQUIRE_FALSE(mask_data.clearAtTime(TimeFrameIndex(42)));
1✔
442

443
        auto masks = mask_data.getAtTime(TimeFrameIndex(42));
1✔
444
        REQUIRE(masks.empty());
1✔
445

446
        // Check that the time was NOT created
447
        auto range = mask_data.getAllAsRange();
1✔
448
        bool found = false;
1✔
449

450
        for (auto const& pair : range) {
1✔
UNCOV
451
            if (pair.time == TimeFrameIndex(42)) {
×
UNCOV
452
                found = true;
×
UNCOV
453
                break;
×
454
            }
UNCOV
455
        }
×
456

457
        REQUIRE_FALSE(found);
1✔
458
    }
6✔
459

460
    SECTION("Empty range with no data") {
5✔
461
        // No data added yet
462
        auto range = mask_data.getAllAsRange();
1✔
463

464
        // Count items in range
465
        size_t count = 0;
1✔
466
        for (auto const& pair : range) {
1✔
UNCOV
467
            count++;
×
UNCOV
468
        }
×
469

470
        REQUIRE(count == 0);
1✔
471
    }
5✔
472

473
    SECTION("Multiple operations sequence") {
5✔
474
        // Add, clear, add again to test internal state consistency
475
        std::vector<Point2D<uint32_t>> points = {{1, 1}, {2, 2}};
3✔
476

477
        mask_data.addAtTime(TimeFrameIndex(5), points);
1✔
478
        REQUIRE(mask_data.clearAtTime(TimeFrameIndex(5)));
1✔
479
        mask_data.addAtTime(TimeFrameIndex(5), points);
1✔
480

481
        auto masks = mask_data.getAtTime(TimeFrameIndex(5));
1✔
482
        REQUIRE(masks.size() == 1);
1✔
483
        REQUIRE(masks[0].size() == 2);
1✔
484
    }
6✔
485
}
10✔
486

487
TEST_CASE("MaskData - Copy and Move operations", "[mask][data][copy][move]") {
17✔
488
    MaskData source_data;
17✔
489
    MaskData target_data;
17✔
490

491
    // Setup test data
492
    std::vector<uint32_t> x1 = {1, 2, 3, 1};
51✔
493
    std::vector<uint32_t> y1 = {1, 1, 2, 2};
51✔
494
    
495
    std::vector<uint32_t> x2 = {4, 5, 6, 4};
51✔
496
    std::vector<uint32_t> y2 = {3, 3, 4, 4};
51✔
497
    
498
    std::vector<Point2D<uint32_t>> points1 = {{10, 10}, {11, 10}, {11, 11}};
51✔
499
    std::vector<Point2D<uint32_t>> points2 = {{20, 20}, {21, 20}};
51✔
500

501
    source_data.addAtTime(TimeFrameIndex(10), x1, y1);
17✔
502
    source_data.addAtTime(TimeFrameIndex(10), x2, y2);  // Second mask at same time
17✔
503
    source_data.addAtTime(TimeFrameIndex(15), points1);
17✔
504
    source_data.addAtTime(TimeFrameIndex(20), points2);
17✔
505

506
    SECTION("copyTo - time range operations") {
17✔
507
        SECTION("Copy entire range") {
4✔
508
            TimeFrameInterval interval{TimeFrameIndex(10), TimeFrameIndex(20)};
1✔
509
            std::size_t copied = source_data.copyTo(target_data, interval);
1✔
510
            
511
            REQUIRE(copied == 4); // 2 masks at time 10, 1 at time 15, 1 at time 20
1✔
512
            
513
            // Verify all masks were copied
514
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
515
            REQUIRE(target_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
516
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
517
            
518
            // Verify source data is unchanged
519
            REQUIRE(source_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
520
            REQUIRE(source_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
521
            REQUIRE(source_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
522
        }
4✔
523

524
        SECTION("Copy partial range") {
4✔
525
            TimeFrameInterval interval{TimeFrameIndex(15), TimeFrameIndex(15)};
1✔
526
            std::size_t copied = source_data.copyTo(target_data, interval);
1✔
527
            
528
            REQUIRE(copied == 1); // Only 1 mask at time 15
1✔
529
            
530
            // Verify only masks in range were copied
531
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).empty());
1✔
532
            REQUIRE(target_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
533
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).empty());
1✔
534
        }
4✔
535

536
        SECTION("Copy non-existent range") {
4✔
537
            TimeFrameInterval interval{TimeFrameIndex(100), TimeFrameIndex(200)};
1✔
538
            std::size_t copied = source_data.copyTo(target_data, interval);
1✔
539
            
540
            REQUIRE(copied == 0);
1✔
541
            REQUIRE(target_data.getTimesWithData().empty());
1✔
542
        }
4✔
543

544
        SECTION("Copy with invalid range") {
4✔
545
            TimeFrameInterval interval{TimeFrameIndex(20), TimeFrameIndex(10)}; // start > end
1✔
546
            std::size_t copied = source_data.copyTo(target_data, interval);
1✔
547
            
548
            REQUIRE(copied == 0);
1✔
549
            REQUIRE(target_data.getTimesWithData().empty());
1✔
550
        }
4✔
551
    }
17✔
552

553
    SECTION("copyTo - specific times operations") {
17✔
554
        SECTION("Copy specific existing times") {
2✔
555
            std::vector<TimeFrameIndex> times = {TimeFrameIndex(10), TimeFrameIndex(20)};
3✔
556
            std::size_t copied = source_data.copyTo(target_data, times);
1✔
557
            
558
            REQUIRE(copied == 3); // 2 masks at time 10, 1 mask at time 20
1✔
559
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
560
            REQUIRE(target_data.getAtTime(TimeFrameIndex(15)).empty());
1✔
561
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
562
        }
3✔
563

564
        SECTION("Copy mix of existing and non-existing times") {
2✔
565
            std::vector<TimeFrameIndex> times = {TimeFrameIndex(10), TimeFrameIndex(100), TimeFrameIndex(20), TimeFrameIndex(200)};
3✔
566
            std::size_t copied = source_data.copyTo(target_data, times);
1✔
567

568
            REQUIRE(copied == 3); // Only times 10 and 20 exist
1✔
569
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
570
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
571
        }
3✔
572
    }
17✔
573

574
    SECTION("moveTo - time range operations") {
17✔
575
        SECTION("Move entire range") {
3✔
576
            TimeFrameInterval interval{TimeFrameIndex(10), TimeFrameIndex(20)};
1✔
577
            std::size_t moved = source_data.moveTo(target_data, interval);
1✔
578
            
579
            REQUIRE(moved == 4); // 2 + 1 + 1 = 4 masks total
1✔
580
            
581
            // Verify all masks were moved to target
582
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
583
            REQUIRE(target_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
584
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
585
            
586
            // Verify source data is now empty
587
            REQUIRE(source_data.getTimesWithData().empty());
1✔
588
        }
3✔
589

590
        SECTION("Move partial range") {
3✔
591
            TimeFrameInterval interval{TimeFrameIndex(15), TimeFrameIndex(20)};
1✔
592
            std::size_t moved = source_data.moveTo(target_data, interval);
1✔
593
            
594
            REQUIRE(moved == 2); // 1 + 1 = 2 masks
1✔
595
            
596
            // Verify only masks in range were moved
597
            REQUIRE(target_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
598
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
599
            
600
            // Verify source still has data outside the range
601
            REQUIRE(source_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
602
            REQUIRE(source_data.getAtTime(TimeFrameIndex(15)).empty());
1✔
603
            REQUIRE(source_data.getAtTime(TimeFrameIndex(20)).empty());
1✔
604
        }
3✔
605

606
        SECTION("Move non-existent range") {
3✔
607
            TimeFrameInterval interval{TimeFrameIndex(100), TimeFrameIndex(200)};
1✔
608
            std::size_t moved = source_data.moveTo(target_data, interval);
1✔
609
            
610
            REQUIRE(moved == 0);
1✔
611
            REQUIRE(target_data.getTimesWithData().empty());
1✔
612
            
613
            // Source should be unchanged
614
            REQUIRE(source_data.getTimesWithData().size() == 3);
1✔
615
        }
3✔
616
    }
17✔
617

618
    SECTION("moveTo - specific times operations") {
17✔
619
        SECTION("Move specific existing times") {
2✔
620
            std::vector<TimeFrameIndex> times = {TimeFrameIndex(15), TimeFrameIndex(10)}; // Non-sequential order
3✔
621
            std::size_t moved = source_data.moveTo(target_data, times);
1✔
622
            
623
            REQUIRE(moved == 3); // 1 + 2 = 3 masks
1✔
624
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
625
            REQUIRE(target_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
626
            
627
            // Only time 20 should remain in source
628
            REQUIRE(source_data.getTimesWithData().size() == 1);
1✔
629
            REQUIRE(source_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
630
        }
3✔
631

632
        SECTION("Move mix of existing and non-existing times") {
2✔
633
            std::vector<TimeFrameIndex> times = {TimeFrameIndex(20), TimeFrameIndex(100), TimeFrameIndex(10), TimeFrameIndex(200)};
3✔
634
            std::size_t moved = source_data.moveTo(target_data, times);
1✔
635
            
636
            REQUIRE(moved == 3); // Only times 10 and 20 exist
1✔
637
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
638
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
639
            
640
            // Only time 15 should remain in source
641
            REQUIRE(source_data.getTimesWithData().size() == 1);
1✔
642
            REQUIRE(source_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
643
        }
3✔
644
    }
17✔
645

646
    SECTION("Copy/Move to target with existing data") {
17✔
647
        // Add some existing data to target
648
        std::vector<Point2D<uint32_t>> existing_mask = {{100, 200}};
6✔
649
        target_data.addAtTime(TimeFrameIndex(10), existing_mask);
2✔
650

651
        SECTION("Copy to existing time adds masks") {
2✔
652
            TimeFrameInterval interval{TimeFrameIndex(10), TimeFrameIndex(10)};
1✔
653
            std::size_t copied = source_data.copyTo(target_data, interval);
1✔
654
            
655
            REQUIRE(copied == 2);
1✔
656
            // Should have existing mask plus copied masks
657
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 3); // 1 existing + 2 copied
1✔
658
        }
2✔
659

660
        SECTION("Move to existing time adds masks") {
2✔
661
            TimeFrameInterval interval{TimeFrameIndex(10), TimeFrameIndex(10)};
1✔
662
            std::size_t moved = source_data.moveTo(target_data, interval);
1✔
663
            
664
            REQUIRE(moved == 2);
1✔
665
            // Should have existing mask plus moved masks
666
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 3); // 1 existing + 2 moved
1✔
667
            // Source should no longer have data at time 10
668
            REQUIRE(source_data.getAtTime(TimeFrameIndex(10)).empty());
1✔
669
        }
2✔
670
    }
19✔
671

672
    SECTION("Edge cases") {
17✔
673
        MaskData empty_source;
4✔
674

675
        SECTION("Copy from empty source") {
4✔
676
            TimeFrameInterval interval{TimeFrameIndex(0), TimeFrameIndex(100)};
1✔
677
            std::size_t copied = empty_source.copyTo(target_data, interval);
1✔
678
            REQUIRE(copied == 0);
1✔
679
            REQUIRE(target_data.getTimesWithData().empty());
1✔
680
        }
4✔
681

682
        SECTION("Move from empty source") {
4✔
683
            TimeFrameInterval interval{TimeFrameIndex(0), TimeFrameIndex(100)};
1✔
684
            std::size_t moved = empty_source.moveTo(target_data, interval);
1✔
685
            REQUIRE(moved == 0);
1✔
686
            REQUIRE(target_data.getTimesWithData().empty());
1✔
687
        }
4✔
688

689
        SECTION("Copy to self (same object)") {
4✔
690
            // This is a corner case - copying to self should return 0 and not
691
            // modify the data
692
            TimeFrameInterval interval{TimeFrameIndex(10), TimeFrameIndex(10)};
1✔
693
            std::size_t copied = source_data.copyTo(source_data, interval);
1✔
694
            REQUIRE(copied == 0);
1✔
695
            // Should now have doubled the masks at time 10
696
            REQUIRE(source_data.getAtTime(TimeFrameIndex(10)).size() == 2); // 2 original
1✔
697
        }
4✔
698

699
        SECTION("Observer notification control") {
4✔
700
            MaskData test_source;
1✔
701
            MaskData test_target;
1✔
702
            
703
            test_source.addAtTime(TimeFrameIndex(5), points1);
1✔
704
            
705
            int target_notifications = 0;
1✔
706
            test_target.addObserver([&target_notifications]() {
1✔
707
                target_notifications++;
1✔
708
            });
1✔
709
            
710
            // Copy without notification
711
            TimeFrameInterval interval{TimeFrameIndex(5), TimeFrameIndex(5)};
1✔
712
            std::size_t copied = test_source.copyTo(test_target, interval, false);
1✔
713
            REQUIRE(copied == 1);
1✔
714
            REQUIRE(target_notifications == 0);
1✔
715
            
716
            // Copy with notification
717
            copied = test_source.copyTo(test_target, interval, true);
1✔
718
            REQUIRE(copied == 1);
1✔
719
            REQUIRE(target_notifications == 1);
1✔
720
        }
5✔
721
    }
21✔
722
}
34✔
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