summaryrefslogtreecommitdiffstats
path: root/src/libixion/dirty_cell_tracker_test.cpp
blob: 3a035fc60d5b47ec901001afce43fb8d382d59bd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include "test_global.hpp" // This must be the first header to be included.
#include <ixion/dirty_cell_tracker.hpp>
#include <cassert>
#include <iostream>
#include <unordered_map>

using namespace ixion;
using namespace std;

using ranks_type = std::unordered_map<abs_range_t, size_t, abs_range_t::hash>;

ranks_type create_ranks(const std::vector<abs_range_t>& sorted)
{
    ranks_type ranks;
    size_t rank = 0;
    for (const abs_range_t& r : sorted)
        ranks.insert({r, rank++});

    return ranks;
}

void test_empty_query()
{
    IXION_TEST_FUNC_SCOPE;

    dirty_cell_tracker tracker;

    // Empty query.
    abs_range_set_t mod_cells;
    abs_range_set_t res = tracker.query_dirty_cells(mod_cells);
    assert(res.empty());

    // A "modified" cell is outside existing sheet range. Make sure we don't
    // crash here.
    mod_cells.emplace(1, 1, 1);
    res = tracker.query_dirty_cells(mod_cells);
    assert(res.empty());

    auto sorted = tracker.query_and_sort_dirty_cells(mod_cells);
    assert(sorted.empty());

    // Dirty cells are like newly entered formula cells that need to be
    // calculated regardless.
    abs_range_set_t dirty_cells;
    dirty_cells.emplace(0, 0, 0); // A1
    dirty_cells.emplace(0, 1, 0); // A2
    dirty_cells.emplace(0, 0, 1); // B1
    dirty_cells.emplace(0, 1, 1); // B2
    sorted = tracker.query_and_sort_dirty_cells(mod_cells, &dirty_cells);
    assert(sorted.size() == 4);
}

void test_cell_to_cell()
{
    IXION_TEST_FUNC_SCOPE;

    dirty_cell_tracker tracker;

    // A2 to listen to A1.
    abs_address_t A1(0, 0, 0);
    abs_address_t A2(0, 1, 0);
    tracker.add(A2, A1);

    // A1 is modified.  A2 should be updated.
    abs_range_set_t mod_cells;
    mod_cells.insert(A1);
    abs_range_set_t res = tracker.query_dirty_cells(mod_cells);
    assert(res.size() == 1);
    abs_range_t cell = *res.cbegin();
    assert(cell.first == A2);

    // A3 to listen to A2.
    abs_address_t A3(0, 2, 0);
    tracker.add(A3, A2);

    // A1 is modified.  Both A2 and A3 should be updated.
    res = tracker.query_dirty_cells(mod_cells);
    assert(res.size() == 2);

    assert(res.count(A2) > 0);
    assert(res.count(A3) > 0);

    // A2 should be ranked lower than A3.
    auto sorted = tracker.query_and_sort_dirty_cells(mod_cells);
    auto ranks = create_ranks(sorted);
    assert(ranks[A2] < ranks[A3]);

    // A2 no longer tracks A1, and because of this, a modification of A1
    // should not trigger any updates.
    tracker.remove(A2, A1);
    res = tracker.query_dirty_cells(mod_cells);
    assert(res.empty());

    // Add A2->A1 once again, to re-establish the previous chain.
    tracker.add(A2, A1);
    res = tracker.query_dirty_cells(mod_cells);
    assert(res.size() == 2);

    assert(res.count(A2) > 0);
    assert(res.count(A3) > 0);
}

void test_cell_to_range()
{
    IXION_TEST_FUNC_SCOPE;

    dirty_cell_tracker tracker;

    // B2 listens to C1:D4.
    abs_address_t B2(0, 1, 1);
    abs_range_t C1_D4(0, 0, 2, 4, 2);
    tracker.add(B2, C1_D4);

    // D3 gets modified.  B2 should be updated.
    abs_range_set_t mod_cells;
    mod_cells.emplace(0, 2, 3);
    abs_range_set_t res = tracker.query_dirty_cells(mod_cells);
    assert(res.size() == 1);
    assert(res.count(B2) > 0);

    // E10 listens to A1:B4 which includes B2.
    abs_address_t E10(0, 9, 4);
    abs_range_t A1_B4(0, 0, 0, 4, 2);
    tracker.add(E10, A1_B4);

    // D3 gets modified again. This time both B2 and E10 need updating.
    res = tracker.query_dirty_cells(mod_cells);
    assert(res.size() == 2);
    assert(res.count(B2) > 0);
    assert(res.count(E10) > 0);

    // B2 no longer listens to C1:D4.
    tracker.remove(B2, C1_D4);

    // D3 gets modified again, but no cells need updating.
    res = tracker.query_dirty_cells(mod_cells);
    assert(res.empty());

    // B3 gets modified.  E10 should be updated.
    mod_cells.clear();
    abs_address_t B3(0, 2, 1);
    mod_cells.insert(B3);
    res = tracker.query_dirty_cells(mod_cells);
    assert(res.size() == 1);
    assert(res.count(E10) > 0);
}

void test_volatile_cells()
{
    IXION_TEST_FUNC_SCOPE;

    dirty_cell_tracker tracker;

    // We use sheet 2 in this test.
    abs_address_t A1(1, 0, 0);
    abs_address_t B2(1, 1, 1);
    abs_address_t C3(1, 2, 2);

    tracker.add_volatile(A1);

    // No cells have been modified.
    abs_range_set_t mod_cells;
    abs_range_set_t res = tracker.query_dirty_cells(mod_cells);

    assert(res.size() == 1);
    assert(res.count(A1) > 0);

    tracker.add_volatile(B2);
    res = tracker.query_dirty_cells(mod_cells);
    assert(res.size() == 2);
    assert(res.count(A1) > 0);
    assert(res.count(B2) > 0);

    tracker.add_volatile(C3);
    res = tracker.query_dirty_cells(mod_cells);
    assert(res.size() == 3);
    assert(res.count(A1) > 0);
    assert(res.count(B2) > 0);
    assert(res.count(C3) > 0);

    // Start removing the volatile cells one by one.
    tracker.remove_volatile(B2);
    res = tracker.query_dirty_cells(mod_cells);
    assert(res.size() == 2);
    assert(res.count(A1) > 0);
    assert(res.count(C3) > 0);

    tracker.remove_volatile(C3);
    res = tracker.query_dirty_cells(mod_cells);
    assert(res.size() == 1);
    assert(res.count(A1) > 0);

    tracker.remove_volatile(A1);
    res = tracker.query_dirty_cells(mod_cells);
    assert(res.empty());
}

void test_volatile_cells_2()
{
    IXION_TEST_FUNC_SCOPE;

    dirty_cell_tracker tracker;

    abs_address_t A1(1, 0, 0);
    abs_address_t B2(1, 1, 1);
    abs_address_t C3(1, 2, 2);

    // B2 tracks A1 and C3 tracks B2.
    tracker.add(B2, A1);
    tracker.add(C3, B2);

    // No cells have been modified.
    abs_range_set_t mod_cells;
    abs_range_set_t res = tracker.query_dirty_cells(mod_cells);
    assert(res.empty());

    // Set A1 as volatile cell.  All of A2, B2 and C3 should be dirty.
    tracker.add_volatile(A1);
    res = tracker.query_dirty_cells(mod_cells);
    assert(res.size() == 3);

    auto sorted = tracker.query_and_sort_dirty_cells(mod_cells);
    assert(sorted.size() == 3);
    auto ranks = create_ranks(sorted);
    assert(ranks[A1] < ranks[B2]);
    assert(ranks[B2] < ranks[C3]);

    // Remove A1 as volatile cell. Now no cells should be dirty.
    tracker.remove_volatile(A1);
    sorted = tracker.query_and_sort_dirty_cells(mod_cells);
    assert(sorted.empty());

    // Now, declare A1 as a dirty cell.
    abs_range_set_t dirty_cells;
    dirty_cells.insert(A1);
    sorted = tracker.query_and_sort_dirty_cells(mod_cells, &dirty_cells);
    assert(sorted.size() == 3);
}

void test_multi_sheets()
{
    IXION_TEST_FUNC_SCOPE;

    dirty_cell_tracker tracker;

    // B2 on sheet 2 tracks A10 on sheet 1.
    abs_address_t s2_B2(1, 1, 1);
    abs_address_t s1_A10(0, 9, 0);
    tracker.add(s2_B2, s1_A10);

    // A10 on sheet 1 gets modified.
    abs_range_set_t res = tracker.query_dirty_cells(s1_A10);
    assert(res.size() == 1);
    assert(res.count(s2_B2) > 0);

    // A10 on sheet 2 gets modified. This should trigger no updates.
    abs_address_t s2_A10(1, 9, 0);
    res = tracker.query_dirty_cells(s2_A10);
    assert(res.empty());
}

void test_recursive_tracking()
{
    IXION_TEST_FUNC_SCOPE;

    dirty_cell_tracker tracker;

    abs_address_t A1(0, 0, 0), B1(0, 0, 1);

    // A1 and B1 track each other.
    tracker.add(A1, B1);
    tracker.add(B1, A1);

    abs_range_set_t res = tracker.query_dirty_cells(A1);
    assert(res.size() == 2);
    assert(res.count(A1) > 0);
    assert(res.count(B1) > 0);
}

void test_listen_to_cell_in_range()
{
    IXION_TEST_FUNC_SCOPE;

    dirty_cell_tracker tracker;

    abs_address_t A2(0, 1, 0), E7(0, 6, 4), G11(0, 10, 6);
    abs_range_t C5_E7(0, 4, 2, 3, 3);
    abs_range_t A1_A3(0, 0, 0, 3, 1);
    abs_range_t C1_E1(0, 0, 2, 1, 3);
    abs_range_t G5_H7(0, 4, 6, 3, 2);
    abs_range_t E7_G9(0, 6, 4, 3, 3);

    tracker.add(G11, E7);
    tracker.add(C5_E7, A1_A3);
    tracker.add(C5_E7, C1_E1);

    cout << "--" << endl;
    cout << tracker.to_string() << endl;

    abs_range_set_t res = tracker.query_dirty_cells(A2);
    assert(res.size() == 2);
    assert(res.count(C5_E7) > 0);
    assert(res.count(G11) > 0);

    tracker.add(G5_H7, A1_A3);
    tracker.remove(G11, E7);
    tracker.add(G11, E7_G9);

    cout << "--" << endl;
    cout << tracker.to_string() << endl;

    res = tracker.query_dirty_cells(A2);
    assert(res.size() == 3);
    assert(res.count(C5_E7) > 0);
    assert(res.count(G5_H7) > 0);
    assert(res.count(G11) > 0);

    // Test topological sort results, and make sure they are ranked correctly.
    std::vector<abs_range_t> sorted = tracker.query_and_sort_dirty_cells(A2);

    auto ranks = create_ranks(sorted);
    assert(ranks[C5_E7] < ranks[G11]);
    assert(ranks[G5_H7] < ranks[G11]);
}

void test_listen_to_3d_range()
{
    IXION_TEST_FUNC_SCOPE;

    dirty_cell_tracker tracker;

    abs_address_t E7(0, 6, 4);
    abs_range_t C1_E5_sheets_0_2(0, 0, 2, 5, 3);
    C1_E5_sheets_0_2.last.sheet += 2;
    tracker.add(E7, C1_E5_sheets_0_2);

    cout << "--" << endl;
    cout << tracker.to_string() << endl;

    abs_address_t D3(0, 2, 3);
    for (sheet_t s = 0; s <= 4; ++s)
    {
        D3.sheet = s;
        auto cells = tracker.query_dirty_cells(D3);
        if (s <= 2)
        {
            assert(cells.size() == 1);
            assert(E7 == *cells.begin());
        }
        else
            assert(cells.empty());
    }

    for (sheet_t s = 0; s <= 4; ++s)
    {
        abs_range_t E5_F6(0, 4, 4, 2, 2);
        E5_F6.first.sheet = E5_F6.last.sheet = s;
        auto cells = tracker.query_dirty_cells(E5_F6);
        if (s <= 2)
        {
            assert(cells.size() == 1);
            assert(E7 == *cells.begin());
        }
        else
            assert(cells.empty());
    }

    // Remove the multi-sheet listener, and make sure the tracker is empty.
    tracker.remove(E7, C1_E5_sheets_0_2);
    assert(tracker.empty());
}

int main()
{
    test_empty_query();
    test_cell_to_cell();
    test_cell_to_range();
    test_volatile_cells();
    test_volatile_cells_2();
    test_multi_sheets();
    test_recursive_tracking();
    test_listen_to_cell_in_range();
    test_listen_to_3d_range();

    return EXIT_SUCCESS;
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */