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

paulmthompson / WhiskerToolbox / 18685379784

21 Oct 2025 01:25PM UTC coverage: 72.522% (+0.1%) from 72.391%
18685379784

push

github

paulmthompson
fix failing tests

18 of 40 new or added lines in 1 file covered. (45.0%)

1765 existing lines in 32 files now uncovered.

53998 of 74457 relevant lines covered (72.52%)

46177.73 hits per line

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

98.07
/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✔
144
                count++;
×
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✔
164
                count++;
×
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::unordered_set<EntityId> ids_set_10c(entity_ids_10.begin(), entity_ids_10.end());
3✔
257
        std::size_t copied = source_data->copyByEntityIds(*target_data, ids_set_10c);
1✔
258
        REQUIRE(copied == 2);
1✔
259
        REQUIRE(target_data->getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
260
        auto target_entity_ids = target_data->getAllEntityIds();
1✔
261
        REQUIRE(target_entity_ids.size() == 2);
1✔
262
        REQUIRE(target_entity_ids != entity_ids_10);
1✔
263
    }
7✔
264

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

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

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

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

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

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

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

295
        source_data->addAtTime(TimeFrameIndex(10), x1, y1);
1✔
296
        std::vector<EntityId> fake_ids = {99999, 88888};
3✔
297
        std::unordered_set<EntityId> ids_set_fakec(fake_ids.begin(), fake_ids.end());
3✔
298
        std::size_t copied = source_data->copyByEntityIds(*target_data, ids_set_fakec);
1✔
299
        REQUIRE(copied == 0);
1✔
300
        REQUIRE(target_data->getTimesWithData().empty());
1✔
301
    }
7✔
302

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

307
        auto source_data = data_manager->getData<MaskData>("source_data");
3✔
308
        auto target_data = data_manager->getData<MaskData>("target_data");
3✔
309

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

315
        std::unordered_set<EntityId> const ids_set_10(ids_10.begin(), ids_10.end());
3✔
316
        std::size_t moved = source_data->moveByEntityIds(*target_data, ids_set_10);
1✔
317
        REQUIRE(moved == 2);
1✔
318
        REQUIRE(source_data->getAtTime(TimeFrameIndex(10)).size() == 0);
1✔
319
        REQUIRE(target_data->getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
320
        auto target_entity_ids = target_data->getAllEntityIds();
1✔
321
        REQUIRE(target_entity_ids == ids_10);
1✔
322
    }
7✔
323

324
    SECTION("Move masks by EntityID - mixed times") {
6✔
325
        data_manager->setData<MaskData>("source_data", TimeKey("test_time"));
3✔
326
        data_manager->setData<MaskData>("target_data", TimeKey("test_time"));
3✔
327

328
        auto source_data = data_manager->getData<MaskData>("source_data");
3✔
329
        auto target_data = data_manager->getData<MaskData>("target_data");
3✔
330

331
        source_data->addAtTime(TimeFrameIndex(10), x1, y1);
1✔
332
        source_data->addAtTime(TimeFrameIndex(20), points);
1✔
333

334
        auto ids_10 = source_data->getEntityIdsAtTime(TimeFrameIndex(10));
1✔
335
        auto ids_20 = source_data->getEntityIdsAtTime(TimeFrameIndex(20));
1✔
336
        std::vector<EntityId> mixed = {ids_10[0], ids_20[0]};
3✔
337
        std::unordered_set<EntityId> const ids_set_mixed(mixed.begin(), mixed.end());
3✔
338
        std::size_t moved = source_data->moveByEntityIds(*target_data, ids_set_mixed);
1✔
339
        REQUIRE(moved == 2);
1✔
340
        REQUIRE(source_data->getAtTime(TimeFrameIndex(10)).size() == 0);
1✔
341
        REQUIRE(source_data->getAtTime(TimeFrameIndex(20)).size() == 0);
1✔
342
        REQUIRE(target_data->getAtTime(TimeFrameIndex(10)).size() == 1);
1✔
343
        REQUIRE(target_data->getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
344
    }
7✔
345

346
    SECTION("Move masks by EntityID - non-existent EntityIDs") {
6✔
347
        data_manager->setData<MaskData>("source_data", TimeKey("test_time"));
3✔
348
        data_manager->setData<MaskData>("target_data", TimeKey("test_time"));
3✔
349

350
        auto source_data = data_manager->getData<MaskData>("source_data");
3✔
351
        auto target_data = data_manager->getData<MaskData>("target_data");
3✔
352

353
        source_data->addAtTime(TimeFrameIndex(10), x1, y1);
1✔
354
        std::vector<EntityId> fake_ids = {99999, 88888};
3✔
355
        std::unordered_set<EntityId> const ids_set_fake(fake_ids.begin(), fake_ids.end());
3✔
356
        std::size_t moved = source_data->moveByEntityIds(*target_data, ids_set_fake);
1✔
357
        REQUIRE(moved == 0);
1✔
358
        REQUIRE(target_data->getTimesWithData().empty());
1✔
359
        REQUIRE(source_data->getAtTime(TimeFrameIndex(10)).size() == 1);
1✔
360
    }
7✔
361
}
12✔
362

363
TEST_CASE("MaskData - Observer notification", "[mask][data][observer]") {
3✔
364
    MaskData mask_data;
3✔
365

366
    // Setup some test data
367
    std::vector<uint32_t> x1 = {1, 2, 3, 1};
9✔
368
    std::vector<uint32_t> y1 = {1, 1, 2, 2};
9✔
369

370
    int notification_count = 0;
3✔
371
    int observer_id = mask_data.addObserver([&notification_count]() {
3✔
372
        notification_count++;
4✔
373
    });
3✔
374

375
    SECTION("Notification on clearAtTime") {
3✔
376
        // First add a mask
377
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1, false);  // Don't notify
1✔
378
        REQUIRE(notification_count == 0);
1✔
379

380
        // Clear with notification
381
        REQUIRE(mask_data.clearAtTime(TimeFrameIndex(0)));
1✔
382
        REQUIRE(notification_count == 1);
1✔
383

384
        // Clear with notification disabled (should return false - nothing to clear)
385
        REQUIRE_FALSE(mask_data.clearAtTime(TimeFrameIndex(0), false));
1✔
386
        REQUIRE(notification_count == 1);  // Still 1, not incremented
1✔
387

388
        // Clear non-existent time (shouldn't notify)
389
        REQUIRE_FALSE(mask_data.clearAtTime(TimeFrameIndex(42)));
1✔
390
        REQUIRE(notification_count == 1);  // Still 1, not incremented
1✔
391
    }
3✔
392

393
    SECTION("Notification on addAtTime") {
3✔
394
        // Add with notification
395
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1);
1✔
396
        REQUIRE(notification_count == 1);
1✔
397

398
        // Add with notification disabled
399
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1, false);
1✔
400
        REQUIRE(notification_count == 1);  // Still 1, not incremented
1✔
401

402
        // Add using point vector with notification
403
        std::vector<Point2D<uint32_t>> points = {{1, 1}, {2, 2}};
3✔
404
        mask_data.addAtTime(TimeFrameIndex(1), points);
1✔
405
        REQUIRE(notification_count == 2);
1✔
406

407
        // Add using point vector with notification disabled
408
        mask_data.addAtTime(TimeFrameIndex(1), points, false);
1✔
409
        REQUIRE(notification_count == 2);  // Still 2, not incremented
1✔
410
    }
4✔
411

412
    SECTION("Multiple operations with single notification") {
3✔
413
        // Perform multiple operations without notifying
414
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1, false);
1✔
415
        mask_data.addAtTime(TimeFrameIndex(1), x1, y1, false);
1✔
416

417
        REQUIRE(notification_count == 0);
1✔
418

419
        // Now manually notify once
420
        mask_data.notifyObservers();
1✔
421
        REQUIRE(notification_count == 1);
1✔
422
    }
3✔
423
}
6✔
424

425
TEST_CASE("MaskData - Edge cases and error handling", "[mask][data][error]") {
5✔
426
    MaskData mask_data;
5✔
427

428
    SECTION("Getting masks at non-existent time") {
5✔
429
        auto masks = mask_data.getAtTime(TimeFrameIndex(999));
1✔
430
        REQUIRE(masks.empty());
1✔
431
    }
6✔
432

433
    SECTION("Adding masks with empty point vectors") {
5✔
434
        std::vector<uint32_t> empty_x;
1✔
435
        std::vector<uint32_t> empty_y;
1✔
436

437
        // This shouldn't crash
438
        mask_data.addAtTime(TimeFrameIndex(0), empty_x, empty_y);
1✔
439

440
        auto masks = mask_data.getAtTime(TimeFrameIndex(0));
1✔
441
        REQUIRE(masks.size() == 1);
1✔
442
        REQUIRE(masks[0].empty());
1✔
443
    }
6✔
444

445
    SECTION("Clearing masks at non-existent time") {
5✔
446
        // Should not create an entry with empty vector
447
        REQUIRE_FALSE(mask_data.clearAtTime(TimeFrameIndex(42)));
1✔
448

449
        auto masks = mask_data.getAtTime(TimeFrameIndex(42));
1✔
450
        REQUIRE(masks.empty());
1✔
451

452
        // Check that the time was NOT created
453
        auto range = mask_data.getAllAsRange();
1✔
454
        bool found = false;
1✔
455

456
        for (auto const& pair : range) {
1✔
UNCOV
457
            if (pair.time == TimeFrameIndex(42)) {
×
UNCOV
458
                found = true;
×
UNCOV
459
                break;
×
460
            }
UNCOV
461
        }
×
462

463
        REQUIRE_FALSE(found);
1✔
464
    }
6✔
465

466
    SECTION("Empty range with no data") {
5✔
467
        // No data added yet
468
        auto range = mask_data.getAllAsRange();
1✔
469

470
        // Count items in range
471
        size_t count = 0;
1✔
472
        for (auto const& pair : range) {
1✔
UNCOV
473
            count++;
×
UNCOV
474
        }
×
475

476
        REQUIRE(count == 0);
1✔
477
    }
5✔
478

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

483
        mask_data.addAtTime(TimeFrameIndex(5), points);
1✔
484
        REQUIRE(mask_data.clearAtTime(TimeFrameIndex(5)));
1✔
485
        mask_data.addAtTime(TimeFrameIndex(5), points);
1✔
486

487
        auto masks = mask_data.getAtTime(TimeFrameIndex(5));
1✔
488
        REQUIRE(masks.size() == 1);
1✔
489
        REQUIRE(masks[0].size() == 2);
1✔
490
    }
6✔
491
}
10✔
492

493
TEST_CASE("MaskData - Copy and Move operations", "[mask][data][copy][move]") {
17✔
494
    MaskData source_data;
17✔
495
    MaskData target_data;
17✔
496

497
    // Setup test data
498
    std::vector<uint32_t> x1 = {1, 2, 3, 1};
51✔
499
    std::vector<uint32_t> y1 = {1, 1, 2, 2};
51✔
500
    
501
    std::vector<uint32_t> x2 = {4, 5, 6, 4};
51✔
502
    std::vector<uint32_t> y2 = {3, 3, 4, 4};
51✔
503
    
504
    std::vector<Point2D<uint32_t>> points1 = {{10, 10}, {11, 10}, {11, 11}};
51✔
505
    std::vector<Point2D<uint32_t>> points2 = {{20, 20}, {21, 20}};
51✔
506

507
    source_data.addAtTime(TimeFrameIndex(10), x1, y1);
17✔
508
    source_data.addAtTime(TimeFrameIndex(10), x2, y2);  // Second mask at same time
17✔
509
    source_data.addAtTime(TimeFrameIndex(15), points1);
17✔
510
    source_data.addAtTime(TimeFrameIndex(20), points2);
17✔
511

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

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

542
        SECTION("Copy non-existent range") {
4✔
543
            TimeFrameInterval interval{TimeFrameIndex(100), TimeFrameIndex(200)};
1✔
544
            std::size_t copied = source_data.copyTo(target_data, interval);
1✔
545
            
546
            REQUIRE(copied == 0);
1✔
547
            REQUIRE(target_data.getTimesWithData().empty());
1✔
548
        }
4✔
549

550
        SECTION("Copy with invalid range") {
4✔
551
            TimeFrameInterval interval{TimeFrameIndex(20), TimeFrameIndex(10)}; // start > end
1✔
552
            std::size_t copied = source_data.copyTo(target_data, interval);
1✔
553
            
554
            REQUIRE(copied == 0);
1✔
555
            REQUIRE(target_data.getTimesWithData().empty());
1✔
556
        }
4✔
557
    }
17✔
558

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

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

574
            REQUIRE(copied == 3); // Only times 10 and 20 exist
1✔
575
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
576
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
577
        }
3✔
578
    }
17✔
579

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

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

612
        SECTION("Move non-existent range") {
3✔
613
            TimeFrameInterval interval{TimeFrameIndex(100), TimeFrameIndex(200)};
1✔
614
            std::size_t moved = source_data.moveTo(target_data, interval);
1✔
615
            
616
            REQUIRE(moved == 0);
1✔
617
            REQUIRE(target_data.getTimesWithData().empty());
1✔
618
            
619
            // Source should be unchanged
620
            REQUIRE(source_data.getTimesWithData().size() == 3);
1✔
621
        }
3✔
622
    }
17✔
623

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

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

652
    SECTION("Copy/Move to target with existing data") {
17✔
653
        // Add some existing data to target
654
        std::vector<Point2D<uint32_t>> existing_mask = {{100, 200}};
6✔
655
        target_data.addAtTime(TimeFrameIndex(10), existing_mask);
2✔
656

657
        SECTION("Copy to existing time adds masks") {
2✔
658
            TimeFrameInterval interval{TimeFrameIndex(10), TimeFrameIndex(10)};
1✔
659
            std::size_t copied = source_data.copyTo(target_data, interval);
1✔
660
            
661
            REQUIRE(copied == 2);
1✔
662
            // Should have existing mask plus copied masks
663
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 3); // 1 existing + 2 copied
1✔
664
        }
2✔
665

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

678
    SECTION("Edge cases") {
17✔
679
        MaskData empty_source;
4✔
680

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

688
        SECTION("Move from empty source") {
4✔
689
            TimeFrameInterval interval{TimeFrameIndex(0), TimeFrameIndex(100)};
1✔
690
            std::size_t moved = empty_source.moveTo(target_data, interval);
1✔
691
            REQUIRE(moved == 0);
1✔
692
            REQUIRE(target_data.getTimesWithData().empty());
1✔
693
        }
4✔
694

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

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