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

paulmthompson / WhiskerToolbox / 16024381988

02 Jul 2025 11:53AM UTC coverage: 72.467% (+0.7%) from 71.74%
16024381988

push

github

paulmthompson
change getting times to range access in mask data

6 of 6 new or added lines in 2 files covered. (100.0%)

102 existing lines in 11 files now uncovered.

11794 of 16275 relevant lines covered (72.47%)

1101.18 hits per line

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

98.51
/src/WhiskerToolbox/DataManager/Masks/Mask_Data.test.cpp
1
#include "Masks/Mask_Data.hpp"
2
#include "DigitalTimeSeries/interval_data.hpp"
3
#include "TimeFrame.hpp"
4
#include <catch2/catch_test_macros.hpp>
5

6
#include <vector>
7
#include <algorithm>
8

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

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

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

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

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

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

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

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

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

57
        mask_data.clearAtTime(TimeFrameIndex(0));
1✔
58

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

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

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

72
        auto range = mask_data.getAllAsRange();
1✔
73

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

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

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

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

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

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

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

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

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

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

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

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

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

228
TEST_CASE("MaskData - Observer notification", "[mask][data][observer]") {
3✔
229
    MaskData mask_data;
3✔
230

231
    // Setup some test data
232
    std::vector<uint32_t> x1 = {1, 2, 3, 1};
9✔
233
    std::vector<uint32_t> y1 = {1, 1, 2, 2};
9✔
234

235
    int notification_count = 0;
3✔
236
    int observer_id = mask_data.addObserver([&notification_count]() {
3✔
237
        notification_count++;
4✔
238
    });
3✔
239

240
    SECTION("Notification on clearAtTime") {
3✔
241
        // First add a mask
242
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1, false);  // Don't notify
1✔
243
        REQUIRE(notification_count == 0);
1✔
244

245
        // Clear with notification
246
        mask_data.clearAtTime(TimeFrameIndex(0));
1✔
247
        REQUIRE(notification_count == 1);
1✔
248

249
        // Clear with notification disabled
250
        mask_data.clearAtTime(TimeFrameIndex(0), false);
1✔
251
        REQUIRE(notification_count == 1);  // Still 1, not incremented
1✔
252

253
        // Clear non-existent time (shouldn't notify)
254
        mask_data.clearAtTime(TimeFrameIndex(42));
1✔
255
        REQUIRE(notification_count == 1);  // Still 1, not incremented
1✔
256
    }
3✔
257

258
    SECTION("Notification on addAtTime") {
3✔
259
        // Add with notification
260
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1);
1✔
261
        REQUIRE(notification_count == 1);
1✔
262

263
        // Add with notification disabled
264
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1, false);
1✔
265
        REQUIRE(notification_count == 1);  // Still 1, not incremented
1✔
266

267
        // Add using point vector with notification
268
        std::vector<Point2D<uint32_t>> points = {{1, 1}, {2, 2}};
3✔
269
        mask_data.addAtTime(TimeFrameIndex(1), points);
1✔
270
        REQUIRE(notification_count == 2);
1✔
271

272
        // Add using point vector with notification disabled
273
        mask_data.addAtTime(TimeFrameIndex(1), points, false);
1✔
274
        REQUIRE(notification_count == 2);  // Still 2, not incremented
1✔
275
    }
4✔
276

277
    SECTION("Multiple operations with single notification") {
3✔
278
        // Perform multiple operations without notifying
279
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1, false);
1✔
280
        mask_data.addAtTime(TimeFrameIndex(1), x1, y1, false);
1✔
281

282
        REQUIRE(notification_count == 0);
1✔
283

284
        // Now manually notify once
285
        mask_data.notifyObservers();
1✔
286
        REQUIRE(notification_count == 1);
1✔
287
    }
3✔
288
}
6✔
289

290
TEST_CASE("MaskData - Edge cases and error handling", "[mask][data][error]") {
5✔
291
    MaskData mask_data;
5✔
292

293
    SECTION("Getting masks at non-existent time") {
5✔
294
        auto masks = mask_data.getAtTime(TimeFrameIndex(999));
1✔
295
        REQUIRE(masks.empty());
1✔
296
    }
6✔
297

298
    SECTION("Adding masks with empty point vectors") {
5✔
299
        std::vector<uint32_t> empty_x;
1✔
300
        std::vector<uint32_t> empty_y;
1✔
301

302
        // This shouldn't crash
303
        mask_data.addAtTime(TimeFrameIndex(0), empty_x, empty_y);
1✔
304

305
        auto masks = mask_data.getAtTime(TimeFrameIndex(0));
1✔
306
        REQUIRE(masks.size() == 1);
1✔
307
        REQUIRE(masks[0].empty());
1✔
308
    }
6✔
309

310
    SECTION("Clearing masks at non-existent time") {
5✔
311
        // Should not create an entry with empty vector
312
        mask_data.clearAtTime(TimeFrameIndex(42));
1✔
313

314
        auto masks = mask_data.getAtTime(TimeFrameIndex(42));
1✔
315
        REQUIRE(masks.empty());
1✔
316

317
        // Check that the time was NOT created
318
        auto range = mask_data.getAllAsRange();
1✔
319
        bool found = false;
1✔
320

321
        for (auto const& pair : range) {
1✔
UNCOV
322
            if (pair.time == TimeFrameIndex(42)) {
×
UNCOV
323
                found = true;
×
UNCOV
324
                break;
×
325
            }
326
        }
327

328
        REQUIRE_FALSE(found);
1✔
329
    }
6✔
330

331
    SECTION("Empty range with no data") {
5✔
332
        // No data added yet
333
        auto range = mask_data.getAllAsRange();
1✔
334

335
        // Count items in range
336
        size_t count = 0;
1✔
337
        for (auto const& pair : range) {
1✔
UNCOV
338
            count++;
×
339
        }
340

341
        REQUIRE(count == 0);
1✔
342
    }
5✔
343

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

348
        mask_data.addAtTime(TimeFrameIndex(5), points);
1✔
349
        mask_data.clearAtTime(TimeFrameIndex(5));
1✔
350
        mask_data.addAtTime(TimeFrameIndex(5), points);
1✔
351

352
        auto masks = mask_data.getAtTime(TimeFrameIndex(5));
1✔
353
        REQUIRE(masks.size() == 1);
1✔
354
        REQUIRE(masks[0].size() == 2);
1✔
355
    }
6✔
356
}
10✔
357

358
TEST_CASE("MaskData - Copy and Move operations", "[mask][data][copy][move]") {
17✔
359
    MaskData source_data;
17✔
360
    MaskData target_data;
17✔
361

362
    // Setup test data
363
    std::vector<uint32_t> x1 = {1, 2, 3, 1};
51✔
364
    std::vector<uint32_t> y1 = {1, 1, 2, 2};
51✔
365
    
366
    std::vector<uint32_t> x2 = {4, 5, 6, 4};
51✔
367
    std::vector<uint32_t> y2 = {3, 3, 4, 4};
51✔
368
    
369
    std::vector<Point2D<uint32_t>> points1 = {{10, 10}, {11, 10}, {11, 11}};
51✔
370
    std::vector<Point2D<uint32_t>> points2 = {{20, 20}, {21, 20}};
51✔
371

372
    source_data.addAtTime(TimeFrameIndex(10), x1, y1);
17✔
373
    source_data.addAtTime(TimeFrameIndex(10), x2, y2);  // Second mask at same time
17✔
374
    source_data.addAtTime(TimeFrameIndex(15), points1);
17✔
375
    source_data.addAtTime(TimeFrameIndex(20), points2);
17✔
376

377
    SECTION("copyTo - time range operations") {
17✔
378
        SECTION("Copy entire range") {
4✔
379
            TimeFrameInterval interval{TimeFrameIndex(10), TimeFrameIndex(20)};
1✔
380
            std::size_t copied = source_data.copyTo(target_data, interval);
1✔
381
            
382
            REQUIRE(copied == 4); // 2 masks at time 10, 1 at time 15, 1 at time 20
1✔
383
            
384
            // Verify all masks were copied
385
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
386
            REQUIRE(target_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
387
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
388
            
389
            // Verify source data is unchanged
390
            REQUIRE(source_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
391
            REQUIRE(source_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
392
            REQUIRE(source_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
393
        }
4✔
394

395
        SECTION("Copy partial range") {
4✔
396
            TimeFrameInterval interval{TimeFrameIndex(15), TimeFrameIndex(15)};
1✔
397
            std::size_t copied = source_data.copyTo(target_data, interval);
1✔
398
            
399
            REQUIRE(copied == 1); // Only 1 mask at time 15
1✔
400
            
401
            // Verify only masks in range were copied
402
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).empty());
1✔
403
            REQUIRE(target_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
404
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).empty());
1✔
405
        }
4✔
406

407
        SECTION("Copy non-existent range") {
4✔
408
            TimeFrameInterval interval{TimeFrameIndex(100), TimeFrameIndex(200)};
1✔
409
            std::size_t copied = source_data.copyTo(target_data, interval);
1✔
410
            
411
            REQUIRE(copied == 0);
1✔
412
            REQUIRE(target_data.getTimesWithData().empty());
1✔
413
        }
4✔
414

415
        SECTION("Copy with invalid range") {
4✔
416
            TimeFrameInterval interval{TimeFrameIndex(20), TimeFrameIndex(10)}; // start > end
1✔
417
            std::size_t copied = source_data.copyTo(target_data, interval);
1✔
418
            
419
            REQUIRE(copied == 0);
1✔
420
            REQUIRE(target_data.getTimesWithData().empty());
1✔
421
        }
4✔
422
    }
17✔
423

424
    SECTION("copyTo - specific times operations") {
17✔
425
        SECTION("Copy specific existing times") {
2✔
426
            std::vector<TimeFrameIndex> times = {TimeFrameIndex(10), TimeFrameIndex(20)};
3✔
427
            std::size_t copied = source_data.copyTo(target_data, times);
1✔
428
            
429
            REQUIRE(copied == 3); // 2 masks at time 10, 1 mask at time 20
1✔
430
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
431
            REQUIRE(target_data.getAtTime(TimeFrameIndex(15)).empty());
1✔
432
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
433
        }
3✔
434

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

439
            REQUIRE(copied == 3); // Only times 10 and 20 exist
1✔
440
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
441
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
442
        }
3✔
443
    }
17✔
444

445
    SECTION("moveTo - time range operations") {
17✔
446
        SECTION("Move entire range") {
3✔
447
            TimeFrameInterval interval{TimeFrameIndex(10), TimeFrameIndex(20)};
1✔
448
            std::size_t moved = source_data.moveTo(target_data, interval);
1✔
449
            
450
            REQUIRE(moved == 4); // 2 + 1 + 1 = 4 masks total
1✔
451
            
452
            // Verify all masks were moved to target
453
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
454
            REQUIRE(target_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
455
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
456
            
457
            // Verify source data is now empty
458
            REQUIRE(source_data.getTimesWithData().empty());
1✔
459
        }
3✔
460

461
        SECTION("Move partial range") {
3✔
462
            TimeFrameInterval interval{TimeFrameIndex(15), TimeFrameIndex(20)};
1✔
463
            std::size_t moved = source_data.moveTo(target_data, interval);
1✔
464
            
465
            REQUIRE(moved == 2); // 1 + 1 = 2 masks
1✔
466
            
467
            // Verify only masks in range were moved
468
            REQUIRE(target_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
469
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
470
            
471
            // Verify source still has data outside the range
472
            REQUIRE(source_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
473
            REQUIRE(source_data.getAtTime(TimeFrameIndex(15)).empty());
1✔
474
            REQUIRE(source_data.getAtTime(TimeFrameIndex(20)).empty());
1✔
475
        }
3✔
476

477
        SECTION("Move non-existent range") {
3✔
478
            TimeFrameInterval interval{TimeFrameIndex(100), TimeFrameIndex(200)};
1✔
479
            std::size_t moved = source_data.moveTo(target_data, interval);
1✔
480
            
481
            REQUIRE(moved == 0);
1✔
482
            REQUIRE(target_data.getTimesWithData().empty());
1✔
483
            
484
            // Source should be unchanged
485
            REQUIRE(source_data.getTimesWithData().size() == 3);
1✔
486
        }
3✔
487
    }
17✔
488

489
    SECTION("moveTo - specific times operations") {
17✔
490
        SECTION("Move specific existing times") {
2✔
491
            std::vector<TimeFrameIndex> times = {TimeFrameIndex(15), TimeFrameIndex(10)}; // Non-sequential order
3✔
492
            std::size_t moved = source_data.moveTo(target_data, times);
1✔
493
            
494
            REQUIRE(moved == 3); // 1 + 2 = 3 masks
1✔
495
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
496
            REQUIRE(target_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
497
            
498
            // Only time 20 should remain in source
499
            REQUIRE(source_data.getTimesWithData().size() == 1);
1✔
500
            REQUIRE(source_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
501
        }
3✔
502

503
        SECTION("Move mix of existing and non-existing times") {
2✔
504
            std::vector<TimeFrameIndex> times = {TimeFrameIndex(20), TimeFrameIndex(100), TimeFrameIndex(10), TimeFrameIndex(200)};
3✔
505
            std::size_t moved = source_data.moveTo(target_data, times);
1✔
506
            
507
            REQUIRE(moved == 3); // Only times 10 and 20 exist
1✔
508
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
509
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
510
            
511
            // Only time 15 should remain in source
512
            REQUIRE(source_data.getTimesWithData().size() == 1);
1✔
513
            REQUIRE(source_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
514
        }
3✔
515
    }
17✔
516

517
    SECTION("Copy/Move to target with existing data") {
17✔
518
        // Add some existing data to target
519
        std::vector<Point2D<uint32_t>> existing_mask = {{100, 200}};
6✔
520
        target_data.addAtTime(TimeFrameIndex(10), existing_mask);
2✔
521

522
        SECTION("Copy to existing time adds masks") {
2✔
523
            TimeFrameInterval interval{TimeFrameIndex(10), TimeFrameIndex(10)};
1✔
524
            std::size_t copied = source_data.copyTo(target_data, interval);
1✔
525
            
526
            REQUIRE(copied == 2);
1✔
527
            // Should have existing mask plus copied masks
528
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 3); // 1 existing + 2 copied
1✔
529
        }
2✔
530

531
        SECTION("Move to existing time adds masks") {
2✔
532
            TimeFrameInterval interval{TimeFrameIndex(10), TimeFrameIndex(10)};
1✔
533
            std::size_t moved = source_data.moveTo(target_data, interval);
1✔
534
            
535
            REQUIRE(moved == 2);
1✔
536
            // Should have existing mask plus moved masks
537
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 3); // 1 existing + 2 moved
1✔
538
            // Source should no longer have data at time 10
539
            REQUIRE(source_data.getAtTime(TimeFrameIndex(10)).empty());
1✔
540
        }
2✔
541
    }
19✔
542

543
    SECTION("Edge cases") {
17✔
544
        MaskData empty_source;
4✔
545

546
        SECTION("Copy from empty source") {
4✔
547
            TimeFrameInterval interval{TimeFrameIndex(0), TimeFrameIndex(100)};
1✔
548
            std::size_t copied = empty_source.copyTo(target_data, interval);
1✔
549
            REQUIRE(copied == 0);
1✔
550
            REQUIRE(target_data.getTimesWithData().empty());
1✔
551
        }
4✔
552

553
        SECTION("Move from empty source") {
4✔
554
            TimeFrameInterval interval{TimeFrameIndex(0), TimeFrameIndex(100)};
1✔
555
            std::size_t moved = empty_source.moveTo(target_data, interval);
1✔
556
            REQUIRE(moved == 0);
1✔
557
            REQUIRE(target_data.getTimesWithData().empty());
1✔
558
        }
4✔
559

560
        SECTION("Copy to self (same object)") {
4✔
561
            // This is a corner case - copying to self should return 0 and not
562
            // modify the data
563
            TimeFrameInterval interval{TimeFrameIndex(10), TimeFrameIndex(10)};
1✔
564
            std::size_t copied = source_data.copyTo(source_data, interval);
1✔
565
            REQUIRE(copied == 0);
1✔
566
            // Should now have doubled the masks at time 10
567
            REQUIRE(source_data.getAtTime(TimeFrameIndex(10)).size() == 2); // 2 original
1✔
568
        }
4✔
569

570
        SECTION("Observer notification control") {
4✔
571
            MaskData test_source;
1✔
572
            MaskData test_target;
1✔
573
            
574
            test_source.addAtTime(TimeFrameIndex(5), points1);
1✔
575
            
576
            int target_notifications = 0;
1✔
577
            test_target.addObserver([&target_notifications]() {
1✔
578
                target_notifications++;
1✔
579
            });
1✔
580
            
581
            // Copy without notification
582
            TimeFrameInterval interval{TimeFrameIndex(5), TimeFrameIndex(5)};
1✔
583
            std::size_t copied = test_source.copyTo(test_target, interval, false);
1✔
584
            REQUIRE(copied == 1);
1✔
585
            REQUIRE(target_notifications == 0);
1✔
586
            
587
            // Copy with notification
588
            copied = test_source.copyTo(test_target, interval, true);
1✔
589
            REQUIRE(copied == 1);
1✔
590
            REQUIRE(target_notifications == 1);
1✔
591
        }
5✔
592
    }
21✔
593
}
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