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

paulmthompson / WhiskerToolbox / 15939366926

28 Jun 2025 01:59AM UTC coverage: 71.74% (+0.1%) from 71.62%
15939366926

push

github

paulmthompson
convert mask data over to strong typed time

208 of 218 new or added lines in 16 files covered. (95.41%)

42 existing lines in 3 files now uncovered.

11340 of 15807 relevant lines covered (71.74%)

1133.35 hits per line

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

98.64
/src/WhiskerToolbox/DataManager/Masks/Mask_Data.test.cpp
1
#include "Masks/Mask_Data.hpp"
2
#include <catch2/catch_test_macros.hpp>
3

4
#include <vector>
5
#include <algorithm>
6

7
TEST_CASE("MaskData - Core functionality", "[mask][data][core]") {
4✔
8
    MaskData mask_data;
4✔
9

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

14
    std::vector<uint32_t> x2 = {4, 5, 6, 4};
12✔
15
    std::vector<uint32_t> y2 = {3, 3, 4, 4};
12✔
16

17
    std::vector<Point2D<uint32_t>> points = {
4✔
18
            {10, 10},
19
            {11, 10},
20
            {11, 11},
21
            {10, 11}
22
    };
12✔
23

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

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

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

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

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

55
        mask_data.clearAtTime(TimeFrameIndex(0));
1✔
56

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

60
        REQUIRE(masks_at_0.empty());
1✔
61
        REQUIRE(masks_at_10.size() == 1);
1✔
62
    }
5✔
63

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

70
        auto range = mask_data.getAllAsRange();
1✔
71

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

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

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

90
    SECTION("Setting and getting image size") {
4✔
91
        ImageSize size{640, 480};
1✔
92
        mask_data.setImageSize(size);
1✔
93

94
        auto retrieved_size = mask_data.getImageSize();
1✔
95
        REQUIRE(retrieved_size.width == 640);
1✔
96
        REQUIRE(retrieved_size.height == 480);
1✔
97
    }
4✔
98
}
8✔
99

100
TEST_CASE("MaskData - Observer notification", "[mask][data][observer]") {
3✔
101
    MaskData mask_data;
3✔
102

103
    // Setup some test data
104
    std::vector<uint32_t> x1 = {1, 2, 3, 1};
9✔
105
    std::vector<uint32_t> y1 = {1, 1, 2, 2};
9✔
106

107
    int notification_count = 0;
3✔
108
    int observer_id = mask_data.addObserver([&notification_count]() {
3✔
109
        notification_count++;
4✔
110
    });
3✔
111

112
    SECTION("Notification on clearAtTime") {
3✔
113
        // First add a mask
114
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1, false);  // Don't notify
1✔
115
        REQUIRE(notification_count == 0);
1✔
116

117
        // Clear with notification
118
        mask_data.clearAtTime(TimeFrameIndex(0));
1✔
119
        REQUIRE(notification_count == 1);
1✔
120

121
        // Clear with notification disabled
122
        mask_data.clearAtTime(TimeFrameIndex(0), false);
1✔
123
        REQUIRE(notification_count == 1);  // Still 1, not incremented
1✔
124

125
        // Clear non-existent time (shouldn't notify)
126
        mask_data.clearAtTime(TimeFrameIndex(42));
1✔
127
        REQUIRE(notification_count == 1);  // Still 1, not incremented
1✔
128
    }
3✔
129

130
    SECTION("Notification on addAtTime") {
3✔
131
        // Add with notification
132
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1);
1✔
133
        REQUIRE(notification_count == 1);
1✔
134

135
        // Add with notification disabled
136
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1, false);
1✔
137
        REQUIRE(notification_count == 1);  // Still 1, not incremented
1✔
138

139
        // Add using point vector with notification
140
        std::vector<Point2D<uint32_t>> points = {{1, 1}, {2, 2}};
3✔
141
        mask_data.addAtTime(TimeFrameIndex(1), points);
1✔
142
        REQUIRE(notification_count == 2);
1✔
143

144
        // Add using point vector with notification disabled
145
        mask_data.addAtTime(TimeFrameIndex(1), points, false);
1✔
146
        REQUIRE(notification_count == 2);  // Still 2, not incremented
1✔
147
    }
4✔
148

149
    SECTION("Multiple operations with single notification") {
3✔
150
        // Perform multiple operations without notifying
151
        mask_data.addAtTime(TimeFrameIndex(0), x1, y1, false);
1✔
152
        mask_data.addAtTime(TimeFrameIndex(1), x1, y1, false);
1✔
153

154
        REQUIRE(notification_count == 0);
1✔
155

156
        // Now manually notify once
157
        mask_data.notifyObservers();
1✔
158
        REQUIRE(notification_count == 1);
1✔
159
    }
3✔
160
}
6✔
161

162
TEST_CASE("MaskData - Edge cases and error handling", "[mask][data][error]") {
5✔
163
    MaskData mask_data;
5✔
164

165
    SECTION("Getting masks at non-existent time") {
5✔
166
        auto masks = mask_data.getAtTime(TimeFrameIndex(999));
1✔
167
        REQUIRE(masks.empty());
1✔
168
    }
6✔
169

170
    SECTION("Adding masks with empty point vectors") {
5✔
171
        std::vector<uint32_t> empty_x;
1✔
172
        std::vector<uint32_t> empty_y;
1✔
173

174
        // This shouldn't crash
175
        mask_data.addAtTime(TimeFrameIndex(0), empty_x, empty_y);
1✔
176

177
        auto masks = mask_data.getAtTime(TimeFrameIndex(0));
1✔
178
        REQUIRE(masks.size() == 1);
1✔
179
        REQUIRE(masks[0].empty());
1✔
180
    }
6✔
181

182
    SECTION("Clearing masks at non-existent time") {
5✔
183
        // Should not create an entry with empty vector
184
        mask_data.clearAtTime(TimeFrameIndex(42));
1✔
185

186
        auto masks = mask_data.getAtTime(TimeFrameIndex(42));
1✔
187
        REQUIRE(masks.empty());
1✔
188

189
        // Check that the time was NOT created
190
        auto range = mask_data.getAllAsRange();
1✔
191
        bool found = false;
1✔
192

193
        for (auto const& pair : range) {
1✔
NEW
194
            if (pair.time == TimeFrameIndex(42)) {
×
195
                found = true;
×
196
                break;
×
197
            }
198
        }
199

200
        REQUIRE_FALSE(found);
1✔
201
    }
6✔
202

203
    SECTION("Empty range with no data") {
5✔
204
        // No data added yet
205
        auto range = mask_data.getAllAsRange();
1✔
206

207
        // Count items in range
208
        size_t count = 0;
1✔
209
        for (auto const& pair : range) {
1✔
210
            count++;
×
211
        }
212

213
        REQUIRE(count == 0);
1✔
214
    }
5✔
215

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

220
        mask_data.addAtTime(TimeFrameIndex(5), points);
1✔
221
        mask_data.clearAtTime(TimeFrameIndex(5));
1✔
222
        mask_data.addAtTime(TimeFrameIndex(5), points);
1✔
223

224
        auto masks = mask_data.getAtTime(TimeFrameIndex(5));
1✔
225
        REQUIRE(masks.size() == 1);
1✔
226
        REQUIRE(masks[0].size() == 2);
1✔
227
    }
6✔
228
}
10✔
229

230
TEST_CASE("MaskData - Copy and Move operations", "[mask][data][copy][move]") {
17✔
231
    MaskData source_data;
17✔
232
    MaskData target_data;
17✔
233

234
    // Setup test data
235
    std::vector<uint32_t> x1 = {1, 2, 3, 1};
51✔
236
    std::vector<uint32_t> y1 = {1, 1, 2, 2};
51✔
237
    
238
    std::vector<uint32_t> x2 = {4, 5, 6, 4};
51✔
239
    std::vector<uint32_t> y2 = {3, 3, 4, 4};
51✔
240
    
241
    std::vector<Point2D<uint32_t>> points1 = {{10, 10}, {11, 10}, {11, 11}};
51✔
242
    std::vector<Point2D<uint32_t>> points2 = {{20, 20}, {21, 20}};
51✔
243

244
    source_data.addAtTime(TimeFrameIndex(10), x1, y1);
17✔
245
    source_data.addAtTime(TimeFrameIndex(10), x2, y2);  // Second mask at same time
17✔
246
    source_data.addAtTime(TimeFrameIndex(15), points1);
17✔
247
    source_data.addAtTime(TimeFrameIndex(20), points2);
17✔
248

249
    SECTION("copyTo - time range operations") {
17✔
250
        SECTION("Copy entire range") {
4✔
251
            std::size_t copied = source_data.copyTo(target_data, TimeFrameIndex(10), TimeFrameIndex(20));
1✔
252
            
253
            REQUIRE(copied == 4); // 2 masks at time 10, 1 at time 15, 1 at time 20
1✔
254
            
255
            // Verify all masks were copied
256
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
257
            REQUIRE(target_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
258
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
259
            
260
            // Verify source data is unchanged
261
            REQUIRE(source_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
262
            REQUIRE(source_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
263
            REQUIRE(source_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
264
        }
4✔
265

266
        SECTION("Copy partial range") {
4✔
267
            std::size_t copied = source_data.copyTo(target_data, TimeFrameIndex(15), TimeFrameIndex(15));
1✔
268
            
269
            REQUIRE(copied == 1); // Only 1 mask at time 15
1✔
270
            
271
            // Verify only masks in range were copied
272
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).empty());
1✔
273
            REQUIRE(target_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
274
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).empty());
1✔
275
        }
4✔
276

277
        SECTION("Copy non-existent range") {
4✔
278
            std::size_t copied = source_data.copyTo(target_data, TimeFrameIndex(100), TimeFrameIndex(200));
1✔
279
            
280
            REQUIRE(copied == 0);
1✔
281
            REQUIRE(target_data.getTimesWithData().empty());
1✔
282
        }
4✔
283

284
        SECTION("Copy with invalid range") {
4✔
285
            std::size_t copied = source_data.copyTo(target_data, TimeFrameIndex(20), TimeFrameIndex(10)); // start > end
1✔
286
            
287
            REQUIRE(copied == 0);
1✔
288
            REQUIRE(target_data.getTimesWithData().empty());
1✔
289
        }
4✔
290
    }
17✔
291

292
    SECTION("copyTo - specific times operations") {
17✔
293
        SECTION("Copy specific existing times") {
2✔
294
            std::vector<TimeFrameIndex> times = {TimeFrameIndex(10), TimeFrameIndex(20)};
3✔
295
            std::size_t copied = source_data.copyTo(target_data, times);
1✔
296
            
297
            REQUIRE(copied == 3); // 2 masks at time 10, 1 mask at time 20
1✔
298
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
299
            REQUIRE(target_data.getAtTime(TimeFrameIndex(15)).empty());
1✔
300
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
301
        }
3✔
302

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

307
            REQUIRE(copied == 3); // Only times 10 and 20 exist
1✔
308
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
309
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
310
        }
3✔
311
    }
17✔
312

313
    SECTION("moveTo - time range operations") {
17✔
314
        SECTION("Move entire range") {
3✔
315
            std::size_t moved = source_data.moveTo(target_data, TimeFrameIndex(10), TimeFrameIndex(20));
1✔
316
            
317
            REQUIRE(moved == 4); // 2 + 1 + 1 = 4 masks total
1✔
318
            
319
            // Verify all masks were moved to target
320
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
321
            REQUIRE(target_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
322
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
323
            
324
            // Verify source data is now empty
325
            REQUIRE(source_data.getTimesWithData().empty());
1✔
326
        }
3✔
327

328
        SECTION("Move partial range") {
3✔
329
            std::size_t moved = source_data.moveTo(target_data, TimeFrameIndex(15), TimeFrameIndex(20));
1✔
330
            
331
            REQUIRE(moved == 2); // 1 + 1 = 2 masks
1✔
332
            
333
            // Verify only masks in range were moved
334
            REQUIRE(target_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
335
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
336
            
337
            // Verify source still has data outside the range
338
            REQUIRE(source_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
339
            REQUIRE(source_data.getAtTime(TimeFrameIndex(15)).empty());
1✔
340
            REQUIRE(source_data.getAtTime(TimeFrameIndex(20)).empty());
1✔
341
        }
3✔
342

343
        SECTION("Move non-existent range") {
3✔
344
            std::size_t moved = source_data.moveTo(target_data, TimeFrameIndex(100), TimeFrameIndex(200));
1✔
345
            
346
            REQUIRE(moved == 0);
1✔
347
            REQUIRE(target_data.getTimesWithData().empty());
1✔
348
            
349
            // Source should be unchanged
350
            REQUIRE(source_data.getTimesWithData().size() == 3);
1✔
351
        }
3✔
352
    }
17✔
353

354
    SECTION("moveTo - specific times operations") {
17✔
355
        SECTION("Move specific existing times") {
2✔
356
            std::vector<TimeFrameIndex> times = {TimeFrameIndex(15), TimeFrameIndex(10)}; // Non-sequential order
3✔
357
            std::size_t moved = source_data.moveTo(target_data, times);
1✔
358
            
359
            REQUIRE(moved == 3); // 1 + 2 = 3 masks
1✔
360
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
361
            REQUIRE(target_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
362
            
363
            // Only time 20 should remain in source
364
            REQUIRE(source_data.getTimesWithData().size() == 1);
1✔
365
            REQUIRE(source_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
366
        }
3✔
367

368
        SECTION("Move mix of existing and non-existing times") {
2✔
369
            std::vector<TimeFrameIndex> times = {TimeFrameIndex(20), TimeFrameIndex(100), TimeFrameIndex(10), TimeFrameIndex(200)};
3✔
370
            std::size_t moved = source_data.moveTo(target_data, times);
1✔
371
            
372
            REQUIRE(moved == 3); // Only times 10 and 20 exist
1✔
373
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 2);
1✔
374
            REQUIRE(target_data.getAtTime(TimeFrameIndex(20)).size() == 1);
1✔
375
            
376
            // Only time 15 should remain in source
377
            REQUIRE(source_data.getTimesWithData().size() == 1);
1✔
378
            REQUIRE(source_data.getAtTime(TimeFrameIndex(15)).size() == 1);
1✔
379
        }
3✔
380
    }
17✔
381

382
    SECTION("Copy/Move to target with existing data") {
17✔
383
        // Add some existing data to target
384
        std::vector<Point2D<uint32_t>> existing_mask = {{100, 200}};
6✔
385
        target_data.addAtTime(TimeFrameIndex(10), existing_mask);
2✔
386

387
        SECTION("Copy to existing time adds masks") {
2✔
388
            std::size_t copied = source_data.copyTo(target_data, TimeFrameIndex(10), TimeFrameIndex(10));
1✔
389
            
390
            REQUIRE(copied == 2);
1✔
391
            // Should have existing mask plus copied masks
392
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 3); // 1 existing + 2 copied
1✔
393
        }
2✔
394

395
        SECTION("Move to existing time adds masks") {
2✔
396
            std::size_t moved = source_data.moveTo(target_data, TimeFrameIndex(10), TimeFrameIndex(10));
1✔
397
            
398
            REQUIRE(moved == 2);
1✔
399
            // Should have existing mask plus moved masks
400
            REQUIRE(target_data.getAtTime(TimeFrameIndex(10)).size() == 3); // 1 existing + 2 moved
1✔
401
            // Source should no longer have data at time 10
402
            REQUIRE(source_data.getAtTime(TimeFrameIndex(10)).empty());
1✔
403
        }
2✔
404
    }
19✔
405

406
    SECTION("Edge cases") {
17✔
407
        MaskData empty_source;
4✔
408

409
        SECTION("Copy from empty source") {
4✔
410
            std::size_t copied = empty_source.copyTo(target_data, TimeFrameIndex(0), TimeFrameIndex(100));
1✔
411
            REQUIRE(copied == 0);
1✔
412
            REQUIRE(target_data.getTimesWithData().empty());
1✔
413
        }
4✔
414

415
        SECTION("Move from empty source") {
4✔
416
            std::size_t moved = empty_source.moveTo(target_data, TimeFrameIndex(0), TimeFrameIndex(100));
1✔
417
            REQUIRE(moved == 0);
1✔
418
            REQUIRE(target_data.getTimesWithData().empty());
1✔
419
        }
4✔
420

421
        SECTION("Copy to self (same object)") {
4✔
422
            // This is a corner case - copying to self should return 0 and not
423
            // modify the data
424
            std::size_t copied = source_data.copyTo(source_data, TimeFrameIndex(10), TimeFrameIndex(10));
1✔
425
            REQUIRE(copied == 0);
1✔
426
            // Should now have doubled the masks at time 10
427
            REQUIRE(source_data.getAtTime(TimeFrameIndex(10)).size() == 2); // 2 original
1✔
428
        }
4✔
429

430
        SECTION("Observer notification control") {
4✔
431
            MaskData test_source;
1✔
432
            MaskData test_target;
1✔
433
            
434
            test_source.addAtTime(TimeFrameIndex(5), points1);
1✔
435
            
436
            int target_notifications = 0;
1✔
437
            test_target.addObserver([&target_notifications]() {
1✔
438
                target_notifications++;
1✔
439
            });
1✔
440
            
441
            // Copy without notification
442
            std::size_t copied = test_source.copyTo(test_target, TimeFrameIndex(5), TimeFrameIndex(5), false);
1✔
443
            REQUIRE(copied == 1);
1✔
444
            REQUIRE(target_notifications == 0);
1✔
445
            
446
            // Copy with notification
447
            copied = test_source.copyTo(test_target, TimeFrameIndex(5), TimeFrameIndex(5), true);
1✔
448
            REQUIRE(copied == 1);
1✔
449
            REQUIRE(target_notifications == 1);
1✔
450
        }
5✔
451
    }
21✔
452
}
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