aboutsummaryrefslogtreecommitdiffstats
path: root/host/tests/rfnoc_propprop_test.cpp
blob: c4693418dcaaff8d9f986d5e67957bc8292df196 (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
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
//
// Copyright 2019 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: GPL-3.0-or-later
//

#include "rfnoc_graph_mock_nodes.hpp"
#include <uhd/rfnoc/mock_block.hpp>
#include <uhd/rfnoc/noc_block_base.hpp>
#include <uhd/rfnoc/rfnoc_types.hpp>
#include <uhd/utils/log.hpp>
#include <uhdlib/rfnoc/clock_iface.hpp>
#include <uhdlib/rfnoc/graph.hpp>
#include <uhdlib/rfnoc/node_accessor.hpp>
#include <uhdlib/rfnoc/prop_accessor.hpp>
#include <boost/test/unit_test.hpp>
#include <iostream>
#include <algorithm>

/*! Mock invalid node
 *
 * This block has an output prop that is always twice the input prop. This block
 * is invalid because the defaults don't work.
 */
class mock_invalid_node1_t : public node_t
{
public:
    mock_invalid_node1_t()
    {
        register_property(&_in);
        register_property(&_out);

        add_property_resolver({&_in}, {&_out}, [this]() { _out = _in * 2; });
        add_property_resolver({&_out}, {&_in}, [this]() { _in = _out / 2; });
    }

    std::string get_unique_id() const override
    {
        return "MOCK_INVALID_NODE1";
    }

    size_t get_num_input_ports() const override
    {
        return 1;
    }

    size_t get_num_output_ports() const override
    {
        return 1;
    }

private:
    property_t<double> _in{"in", 1.0, {res_source_info::INPUT_EDGE}};
    // This has an invalid default value: It would have to be 2.0 for the block
    // to be able to initialize
    property_t<double> _out{"out", 1.0 /* SIC */, {res_source_info::OUTPUT_EDGE}};
};

/*! Mock invalid node
 *
 * This block will write conflicting values to the output at resolution time.
 */
class mock_invalid_node2_t : public node_t
{
public:
    mock_invalid_node2_t()
    {
        register_property(&_in);
        register_property(&_out);

        add_property_resolver({&_in}, {&_out}, [this]() {
            UHD_LOG_INFO("MOCK2", "Calling resolver 1/2 for _out");
            _out = _in * 2.0;
        });
        // If this->factor != 2.0, then this resolver will contradict the
        // previous one:
        add_property_resolver({&_in}, {&_out}, [this]() {
            UHD_LOG_INFO("MOCK2", "Calling resolver 2/2 for _out");
            _out = _in * this->factor;
        });
        add_property_resolver({&_out}, {&_in}, [this]() {
            UHD_LOG_INFO("MOCK2", "Calling resolver for _in");
            _in = _out / 2.0;
        });
    }

    void mark_in_dirty()
    {
        prop_accessor_t prop_accessor{};
        auto access_lock = prop_accessor.get_scoped_prop_access(_in, property_base_t::RW);
        double old_val   = _in.get();
        _in.set(old_val * 2.0);
        _in.set(old_val);
    }

    size_t get_num_input_ports() const override
    {
        return 1;
    }

    size_t get_num_output_ports() const override
    {
        return 1;
    }

    std::string get_unique_id() const override
    {
        return "MOCK_INVALID_NODE2";
    }

    // When we change this, we break resolver #2.
    double factor = 2.0;

private:
    property_t<double> _in{"in", 1.0, {res_source_info::INPUT_EDGE}};
    property_t<double> _out{"out", 2.0, {res_source_info::OUTPUT_EDGE}};
};

/*! Mock node, circular prop deps
 */
class mock_circular_prop_node_t : public node_t
{
public:
    mock_circular_prop_node_t()
    {
        register_property(&_x1);
        register_property(&_x2);
        register_property(&_x4);

        add_property_resolver({&_x1}, {&_x2}, [this]() {
            RFNOC_LOG_INFO("Calling resolver for _x1");
            _x2 = 2.0 * _x1.get();
        });
        add_property_resolver({&_x2}, {&_x4}, [this]() {
            RFNOC_LOG_INFO("Calling resolver for _x2");
            _x4 = 2.0 * _x2.get();
        });
        add_property_resolver({&_x4}, {&_x1}, [this]() {
            RFNOC_LOG_INFO("Calling resolver for _x4");
            _x1 = _x4.get() / 4.0;
        });
    }

    size_t get_num_input_ports() const override
    {
        return 1;
    }

    size_t get_num_output_ports() const override
    {
        return 1;
    }

    std::string get_unique_id() const override
    {
        return "MOCK_CIRCULAR_PROPS";
    }

    property_t<double> _x1{"x1", 1.0, {res_source_info::USER}};
    property_t<double> _x2{"x2", 2.0, {res_source_info::USER}};
    property_t<double> _x4{"x4", 4.0, {res_source_info::USER}};
};

/*! A mock RFNoC block
 *
 * A mock RFNoC block deriving from noc_block_base, strictly for the purpose
 * of testing multiple calls to set_mtu_forwarding_policy() (which is a
 * protected member of noc_block_base, sigh)
 */
class mock_noc_block_t : public noc_block_base
{
public:
    mock_noc_block_t(noc_block_base::make_args_ptr make_args) :
        noc_block_base(std::move(make_args))
    {
    }

    void set_mtu_forwarding_policy(node_t::forwarding_policy_t policy)
    {
        noc_block_base::set_mtu_forwarding_policy(policy);
    }
};


// Do some sanity checks on the mock just so we don't get surprised later
BOOST_AUTO_TEST_CASE(test_mock)
{
    BOOST_CHECK_EQUAL(1, mock_ddc_node_t::coerce_decim(1));
    BOOST_CHECK_EQUAL(2, mock_ddc_node_t::coerce_decim(2));
    BOOST_CHECK_EQUAL(512, mock_ddc_node_t::coerce_decim(1212));
    BOOST_CHECK_EQUAL(512, mock_ddc_node_t::coerce_decim(513));
    BOOST_CHECK_EQUAL(2, mock_ddc_node_t::coerce_decim(3));

    mock_ddc_node_t mock{};
    BOOST_CHECK(mock._decim.is_dirty());
    BOOST_CHECK(mock._samp_rate_out.is_dirty());
    BOOST_CHECK(mock._samp_rate_in.is_dirty());
    BOOST_CHECK_EQUAL(mock._decim.get(), DEFAULT_DECIM);
    BOOST_CHECK_EQUAL(mock._samp_rate_out.get(), DEFAULT_RATE);
    BOOST_CHECK_EQUAL(mock._samp_rate_in.get(), DEFAULT_RATE);
}

BOOST_AUTO_TEST_CASE(test_init_and_resolve)
{
    mock_ddc_node_t mock_ddc{};
    mock_radio_node_t mock_radio(0);
    node_accessor_t node_accessor{};

    node_accessor.init_props(&mock_ddc);
    node_accessor.init_props(&mock_radio);

    BOOST_CHECK(!mock_ddc._decim.is_dirty());
    BOOST_CHECK(!mock_ddc._samp_rate_out.is_dirty());
    BOOST_CHECK(!mock_ddc._samp_rate_in.is_dirty());
    BOOST_CHECK_EQUAL(mock_ddc._decim.get(), DEFAULT_DECIM);
    BOOST_CHECK_EQUAL(mock_ddc._samp_rate_out.get(), DEFAULT_RATE);
    BOOST_CHECK_EQUAL(mock_ddc._samp_rate_in.get(), DEFAULT_RATE);

    BOOST_CHECK_EQUAL(mock_ddc.get_property<int>("decim", 0), DEFAULT_DECIM);

    mock_ddc.set_property("decim", 2, 0);
    BOOST_CHECK(!mock_ddc._decim.is_dirty());
    node_accessor.resolve_props(&mock_ddc);

    BOOST_CHECK_EQUAL(mock_ddc.get_property<int>("decim", 0), 2);
    BOOST_CHECK_EQUAL(mock_ddc._samp_rate_in.get(), DEFAULT_RATE);
    BOOST_CHECK_EQUAL(mock_ddc._samp_rate_in.get() / 2, mock_ddc._samp_rate_out.get());
}

BOOST_AUTO_TEST_CASE(test_failures)
{
    node_accessor_t node_accessor{};

    UHD_LOG_INFO("TEST", "We expect an ERROR log message next:");
    mock_invalid_node1_t mock1{};
    // BOOST_REQUIRE_THROW(
    // node_accessor.init_props(&mock1),
    // uhd::runtime_error);

    mock_invalid_node2_t mock2{};
    node_accessor.init_props(&mock2);
    mock2.factor = 1.0;
    mock2.mark_in_dirty();
    BOOST_REQUIRE_THROW(node_accessor.resolve_props(&mock2), uhd::resolve_error);
}

// Redeclare this here, since it's only defined outside of UHD_API
noc_block_base::make_args_t::~make_args_t() = default;

BOOST_AUTO_TEST_CASE(test_mtu_forwarding_policy_restrictions)
{
    // Most of this is just dummy stuff required to correctly instantiate a
    // noc_block_base-derived block and is inconsequential to the test itself
    mock_block_container mbc;
    mbc.reg_iface = std::make_shared<mock_reg_iface_t>();
    mbc.tree      = uhd::property_tree::make();
    mbc.make_args                   = std::make_unique<noc_block_base::make_args_t>();
    mbc.make_args->noc_id           = 0x01020304;
    mbc.make_args->block_id         = block_id_t("0/Dummy#0");
    mbc.make_args->num_input_ports  = 2;
    mbc.make_args->num_output_ports = 2;
    mbc.make_args->mtu              = 8000;
    mbc.make_args->chdr_w           = uhd::rfnoc::CHDR_W_64;
    mbc.make_args->reg_iface        = mbc.reg_iface;
    mbc.make_args->tree             = mbc.tree;
    mbc.make_args->tb_clk_iface =
        std::make_shared<clock_iface>("dummy");
    mbc.make_args->ctrlport_clk_iface =
        std::make_shared<clock_iface>("dummy");
    mbc.make_args->mb_control = nullptr;

    // Construct the dummy RFNoC block
    mock_noc_block_t mock_block(std::move(mbc.make_args));

    // Set the MTU forwarding policy once; this should work
    mock_block.set_mtu_forwarding_policy(noc_block_base::forwarding_policy_t::ONE_TO_ONE);

    // And the seocnd time should generate an exception
    BOOST_REQUIRE_THROW(mock_block.set_mtu_forwarding_policy(noc_block_base::forwarding_policy_t::ONE_TO_FAN), uhd::runtime_error);
}

BOOST_AUTO_TEST_CASE(test_graph_resolve_ddc_radio)
{
    node_accessor_t node_accessor{};
    uhd::rfnoc::detail::graph_t graph{};
    // Define some mock nodes:
    mock_ddc_node_t mock_ddc{};
    // Source radio
    mock_radio_node_t mock_rx_radio(0);
    // Sink radio
    mock_radio_node_t mock_tx_radio(1);

    // These init calls would normally be done by the framework
    node_accessor.init_props(&mock_ddc);
    node_accessor.init_props(&mock_tx_radio);
    node_accessor.init_props(&mock_rx_radio);

    // In this simple graph, all connections are identical from an edge info
    // perspective, so we're lazy and share an edge_info object:
    uhd::rfnoc::detail::graph_t::graph_edge_t edge_info;
    edge_info.src_port                    = 0;
    edge_info.dst_port                    = 0;
    edge_info.property_propagation_active = true;
    edge_info.edge = uhd::rfnoc::detail::graph_t::graph_edge_t::DYNAMIC;

    // Now create the graph and commit:
    graph.connect(&mock_rx_radio, &mock_ddc, edge_info);
    graph.connect(&mock_ddc, &mock_tx_radio, edge_info);
    graph.commit();
    BOOST_CHECK_EQUAL(mock_ddc._decim.get(), 1);

    mock_tx_radio.set_property<double>("master_clock_rate", 100e6, 0);
    BOOST_CHECK_EQUAL(mock_ddc._decim.get(), 2);

    UHD_LOG_INFO("TEST", "Now tempting DDC to invalid prop value...");
    mock_ddc.set_property<int>("decim", 42, 0);
    // It will bounce back:
    BOOST_CHECK_EQUAL(mock_ddc._decim.get(), 2);

    graph.release();
    mock_tx_radio.set_property<double>("master_clock_rate", 200e6, 0);
    // Won't change yet:
    BOOST_CHECK_EQUAL(mock_ddc._decim.get(), 2);
    graph.commit();
    BOOST_CHECK_EQUAL(mock_ddc._decim.get(), 1);
}


BOOST_AUTO_TEST_CASE(test_graph_catch_invalid_graph)
{
    node_accessor_t node_accessor{};
    uhd::rfnoc::detail::graph_t graph{};
    // Define some mock nodes:
    // Source radio
    mock_radio_node_t mock_rx_radio(0);
    // Sink radio
    mock_radio_node_t mock_tx_radio(1);

    // These init calls would normally be done by the framework
    node_accessor.init_props(&mock_tx_radio);
    node_accessor.init_props(&mock_rx_radio);
    mock_tx_radio.set_property<double>("master_clock_rate", 100e6, 0);

    // In this simple graph, all connections are identical from an edge info
    // perspective, so we're lazy and share an edge_info object:
    uhd::rfnoc::detail::graph_t::graph_edge_t edge_info;
    edge_info.src_port                    = 0;
    edge_info.dst_port                    = 0;
    edge_info.property_propagation_active = true;
    edge_info.edge = uhd::rfnoc::detail::graph_t::graph_edge_t::DYNAMIC;

    // Now create the graph and commit:
    graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info);
    BOOST_REQUIRE_THROW(graph.commit(), uhd::resolve_error);
    UHD_LOG_INFO("TEST", "^^^ Expected an error message.");
}

BOOST_AUTO_TEST_CASE(test_graph_ro_prop)
{
    node_accessor_t node_accessor{};
    uhd::rfnoc::detail::graph_t graph{};
    // Define some mock nodes:
    // Source radio
    mock_radio_node_t mock_rx_radio(0);
    // Sink radio
    mock_radio_node_t mock_tx_radio(1);

    // These init calls would normally be done by the framework
    node_accessor.init_props(&mock_tx_radio);
    node_accessor.init_props(&mock_rx_radio);
    BOOST_CHECK_EQUAL(mock_tx_radio.rssi_resolver_count, 1);
    BOOST_CHECK_EQUAL(mock_rx_radio.rssi_resolver_count, 1);

    // In this simple graph, all connections are identical from an edge info
    // perspective, so we're lazy and share an edge_info object:
    uhd::rfnoc::detail::graph_t::graph_edge_t edge_info;
    edge_info.src_port                    = 0;
    edge_info.dst_port                    = 0;
    edge_info.property_propagation_active = true;
    edge_info.edge = uhd::rfnoc::detail::graph_t::graph_edge_t::DYNAMIC;

    // Now create the graph and commit:
    graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info);
    graph.commit();

    const size_t rx_rssi_resolver_count = mock_rx_radio.rssi_resolver_count;
    UHD_LOG_INFO("TEST", "Now testing mock RSSI resolver/get prop");
    UHD_LOG_DEBUG("TEST", "RX RSSI: " << mock_rx_radio.get_property<double>("rssi"));
    // The next value must match the value in graph.cpp. We have one additional
    // backward- and forward resolution.
    BOOST_CHECK_EQUAL(rx_rssi_resolver_count + 2, mock_rx_radio.rssi_resolver_count);
}

BOOST_AUTO_TEST_CASE(test_graph_double_connect)
{
    node_accessor_t node_accessor{};
    using uhd::rfnoc::detail::graph_t;
    graph_t graph{};
    using edge_t = graph_t::graph_edge_t;
    // Define some mock nodes:
    mock_radio_node_t mock_rx_radio0(0);
    mock_radio_node_t mock_rx_radio1(1);
    mock_radio_node_t mock_tx_radio0(2);
    mock_radio_node_t mock_tx_radio1(3);

    // These init calls would normally be done by the framework
    node_accessor.init_props(&mock_tx_radio0);
    node_accessor.init_props(&mock_tx_radio1);
    node_accessor.init_props(&mock_rx_radio0);
    node_accessor.init_props(&mock_rx_radio1);

    graph.connect(&mock_rx_radio0, &mock_tx_radio0, {0, 0, edge_t::DYNAMIC, true});
    // Twice is also OK:
    UHD_LOG_INFO("TEST", "Testing double-connect with same edges");
    graph.connect(&mock_rx_radio0, &mock_tx_radio0, {0, 0, edge_t::DYNAMIC, true});
    UHD_LOG_INFO("TEST", "Testing double-connect with same edges, different attributes");
    BOOST_REQUIRE_THROW(
        graph.connect(&mock_rx_radio0, &mock_tx_radio0, {0, 0, edge_t::DYNAMIC, false}),
        uhd::rfnoc_error);
    BOOST_REQUIRE_THROW(
        graph.connect(&mock_rx_radio0, &mock_tx_radio0, {0, 0, edge_t::STATIC, false}),
        uhd::rfnoc_error);
    UHD_LOG_INFO("TEST", "Testing double-connect output port, new dest node");
    BOOST_REQUIRE_THROW(
        graph.connect(&mock_rx_radio0, &mock_tx_radio1, {0, 0, edge_t::DYNAMIC, true}),
        uhd::rfnoc_error);
    UHD_LOG_INFO("TEST", "Testing double-connect input port, new source node");
    BOOST_REQUIRE_THROW(
        graph.connect(&mock_rx_radio1, &mock_tx_radio0, {0, 0, edge_t::DYNAMIC, true}),
        uhd::rfnoc_error);
    // Add another valid connection
    graph.connect(&mock_rx_radio1, &mock_tx_radio1, {0, 0, edge_t::DYNAMIC, true});
    UHD_LOG_INFO("TEST", "Testing double-connect output port, existing dest node");
    BOOST_REQUIRE_THROW(
        graph.connect(&mock_rx_radio0, &mock_tx_radio1, {0, 0, edge_t::DYNAMIC, true}),
        uhd::rfnoc_error);
    UHD_LOG_INFO("TEST", "Testing double-connect input port, existing source node");
    BOOST_REQUIRE_THROW(
        graph.connect(&mock_rx_radio1, &mock_tx_radio0, {0, 0, edge_t::DYNAMIC, true}),
        uhd::rfnoc_error);
}

BOOST_AUTO_TEST_CASE(test_graph_crisscross_fifo)
{
    node_accessor_t node_accessor{};
    uhd::rfnoc::detail::graph_t graph{};
    // Define some mock nodes:
    // Source radios
    mock_radio_node_t mock_rx_radio0(0); // -> 2
    mock_radio_node_t mock_rx_radio1(1); // -> 3
    // Sink radios
    mock_radio_node_t mock_tx_radio0(2);
    mock_radio_node_t mock_tx_radio1(3);
    // FIFO
    mock_fifo_t mock_fifo(2);

    // These init calls would normally be done by the framework
    node_accessor.init_props(&mock_rx_radio0);
    node_accessor.init_props(&mock_rx_radio1);
    node_accessor.init_props(&mock_tx_radio0);
    node_accessor.init_props(&mock_tx_radio1);
    node_accessor.init_props(&mock_fifo);

    mock_rx_radio0.set_property<double>("master_clock_rate", 200e6, 0);
    mock_rx_radio1.set_property<double>("master_clock_rate", 100e6, 0);
    mock_tx_radio0.set_property<double>("master_clock_rate", 100e6, 0);
    mock_tx_radio1.set_property<double>("master_clock_rate", 200e6, 0);

    using graph_edge_t = uhd::rfnoc::detail::graph_t::graph_edge_t;

    // Now create the graph and commit:
    graph.connect(&mock_rx_radio0, &mock_fifo, {0, 0, graph_edge_t::DYNAMIC, true});
    graph.connect(&mock_rx_radio1, &mock_fifo, {0, 1, graph_edge_t::DYNAMIC, true});
    // Notice how we swap the TX radios
    graph.connect(&mock_fifo, &mock_tx_radio0, {1, 0, graph_edge_t::DYNAMIC, true});
    graph.connect(&mock_fifo, &mock_tx_radio1, {0, 0, graph_edge_t::DYNAMIC, true});
    UHD_LOG_INFO("TEST", "Now testing criss-cross prop resolution");
    graph.commit();
}

BOOST_AUTO_TEST_CASE(test_circular_deps)
{
    node_accessor_t node_accessor{};
    // Define some mock nodes:
    // Source radios
    mock_circular_prop_node_t mock_circular_prop_node{};

    // These init calls would normally be done by the framework
    node_accessor.init_props(&mock_circular_prop_node);

    mock_circular_prop_node.set_property<double>("x1", 5.0, 0);
    BOOST_CHECK_EQUAL(mock_circular_prop_node.get_property<double>("x4"), 4 * 5.0);
}

BOOST_AUTO_TEST_CASE(test_propagation_map)
{
    // Set up the graph
    node_accessor_t node_accessor{};
    uhd::rfnoc::detail::graph_t graph{};

    constexpr size_t NUM_INPUTS  = 8;
    constexpr size_t NUM_OUTPUTS = 8;

    node_t::forwarding_map_t fwd_map = {
        // input edges 0-3 --> output edges 3-0
        {{res_source_info::INPUT_EDGE, 0}, {{res_source_info::OUTPUT_EDGE, 3}}},
        {{res_source_info::INPUT_EDGE, 1}, {{res_source_info::OUTPUT_EDGE, 2}}},
        {{res_source_info::INPUT_EDGE, 2}, {{res_source_info::OUTPUT_EDGE, 1}}},
        {{res_source_info::INPUT_EDGE, 3}, {{res_source_info::OUTPUT_EDGE, 0}}},
        // input edge 4 --> output edges 4 and 5
        {{res_source_info::INPUT_EDGE, 4},
            {{res_source_info::OUTPUT_EDGE, 4}, {res_source_info::OUTPUT_EDGE, 5}}},
        // input edge 5 --> output edges 6 and 7
        {{res_source_info::INPUT_EDGE, 5},
            {{res_source_info::OUTPUT_EDGE, 6}, {res_source_info::OUTPUT_EDGE, 7}}},
        // input edge 6 no destination (i.e. drop)
        {{res_source_info::INPUT_EDGE, 6}, {}}
        // input edge 7 not in map (i.e. drop)
    };

    mock_edge_node_t input{0, NUM_INPUTS, "MOCK_EDGE_NODE<input>"};
    mock_routing_node_t middle{NUM_INPUTS, NUM_OUTPUTS};
    mock_edge_node_t output{NUM_OUTPUTS, 0, "MOCK_EDGE_NODE<output>"};

    middle.set_prop_forwarding_map(fwd_map);

    // These init calls would normally be done by the framework
    node_accessor.init_props(&input);
    node_accessor.init_props(&middle);
    node_accessor.init_props(&output);

    // Prime the output edge properties on the input block
    for (size_t i = 0; i < NUM_INPUTS; i++) {
        input.set_edge_property<int>("prop", 100 + i, {res_source_info::OUTPUT_EDGE, i});
    }

    using graph_edge_t = uhd::rfnoc::detail::graph_t::graph_edge_t;

    // Connect the nodes in the graph
    for (size_t i = 0; i < NUM_INPUTS; i++) {
        graph.connect(&input, &middle, {i, i, graph_edge_t::DYNAMIC, true});
    }
    for (size_t i = 0; i < NUM_OUTPUTS; i++) {
        graph.connect(&middle, &output, {i, i, graph_edge_t::DYNAMIC, true});
    }
    UHD_LOG_INFO("TEST", "Now testing map-driven property propagation");
    graph.commit();

    // Verify that the properties were propagated per the table
    BOOST_CHECK_EQUAL(
        output.get_edge_property<int>("prop", {res_source_info::INPUT_EDGE, 0}), 103);
    BOOST_CHECK_EQUAL(
        output.get_edge_property<int>("prop", {res_source_info::INPUT_EDGE, 1}), 102);
    BOOST_CHECK_EQUAL(
        output.get_edge_property<int>("prop", {res_source_info::INPUT_EDGE, 2}), 101);
    BOOST_CHECK_EQUAL(
        output.get_edge_property<int>("prop", {res_source_info::INPUT_EDGE, 3}), 100);
    BOOST_CHECK_EQUAL(
        output.get_edge_property<int>("prop", {res_source_info::INPUT_EDGE, 4}), 104);
    BOOST_CHECK_EQUAL(
        output.get_edge_property<int>("prop", {res_source_info::INPUT_EDGE, 5}), 104);
    BOOST_CHECK_EQUAL(
        output.get_edge_property<int>("prop", {res_source_info::INPUT_EDGE, 6}), 105);
    BOOST_CHECK_EQUAL(
        output.get_edge_property<int>("prop", {res_source_info::INPUT_EDGE, 7}), 105);
}

BOOST_AUTO_TEST_CASE(test_propagation_map_exception_invalid_destination)
{
    // Set up the graph
    node_accessor_t node_accessor{};
    uhd::rfnoc::detail::graph_t graph{};

    // Create a map that will generate an exception at propagation time due
    // to the mapping pointing to a non-existent port
    node_t::forwarding_map_t no_port_fwd_map = {
        // input edge 0 --> output edge 1 (output port does not exist)
        {{res_source_info::INPUT_EDGE, 0}, {{res_source_info::OUTPUT_EDGE, 1}}}};

    mock_edge_node_t generator{0, 1, "MOCK_EDGE_NODE<generator>"};
    mock_routing_node_t router{1, 1};
    mock_edge_node_t receiver{1, 0, "MOCK_EDGE_NODE<receiver>"};

    router.set_prop_forwarding_map(no_port_fwd_map);

    // These init calls would normally be done by the framework
    node_accessor.init_props(&generator);
    node_accessor.init_props(&router);
    node_accessor.init_props(&receiver);

    generator.set_edge_property<int>("prop", 100, {res_source_info::OUTPUT_EDGE, 0});

    using graph_edge_t = uhd::rfnoc::detail::graph_t::graph_edge_t;

    // Connect the nodes in the graph
    graph.connect(&generator, &router, {0, 0, graph_edge_t::DYNAMIC, true});
    graph.connect(&router, &receiver, {0, 0, graph_edge_t::DYNAMIC, true});

    UHD_LOG_INFO("TEST",
        "Now testing map-driven property propagation with invalid map (no destination "
        "port)");
    BOOST_REQUIRE_THROW(graph.commit(), uhd::rfnoc_error);
}

BOOST_AUTO_TEST_CASE(test_graph_node_deletion_resolver_fix)
{
    node_accessor_t node_accessor{};
    uhd::rfnoc::detail::graph_t graph{};

    // First node: Source radio
    mock_radio_node_t mock_rx_radio(0);
    // Second node: DDC
    mock_ddc_node_t mock_ddc{};

    // These init calls would normally be done by the framework
    node_accessor.init_props(&mock_rx_radio);
    node_accessor.init_props(&mock_ddc);

    // The DDC node at this point is not and has never been part of a
    // graph, but setting the decimation property should still invoke
    // that resolver
    constexpr int new_interp_ratio = 100;
    mock_ddc.set_property<int>("decim", new_interp_ratio, 0);
    BOOST_CHECK_EQUAL(
        mock_ddc._samp_rate_in.get() / new_interp_ratio, mock_ddc._samp_rate_out.get());

    // Connect the radio to the DDC in a simple graph
    uhd::rfnoc::detail::graph_t::graph_edge_t edge_info;
    edge_info.src_port                    = 0;
    edge_info.dst_port                    = 0;
    edge_info.property_propagation_active = true;
    edge_info.edge = uhd::rfnoc::detail::graph_t::graph_edge_t::DYNAMIC;

    graph.connect(&mock_rx_radio, &mock_ddc, edge_info);
    BOOST_CHECK_EQUAL(mock_ddc._decim.get(), new_interp_ratio);

    // Set the radio MCR; after graph committal, that should propagate
    // to the DDC input rate
    const int ddc_input_rate = mock_ddc._samp_rate_in.get();
    mock_rx_radio.set_property<double>("master_clock_rate", 100e6, 0);
    BOOST_CHECK_EQUAL(mock_ddc._samp_rate_in.get(), ddc_input_rate);
    graph.commit();
    BOOST_CHECK_EQUAL(mock_ddc._samp_rate_in.get(), 100e6);

    // Now disconect the DDC from the radio--as there are no incoming or
    // outgoing edges on either node afterwards, the nodes should be removed
    // from the graph altogether and thus their resolver callbacks should
    // also be reset to the default behavior
    graph.disconnect(&mock_rx_radio, &mock_ddc, edge_info);

    // Setting a new decimation ratio even after the DDC node has been
    // removed from the graph should work as expected
    constexpr int disconnected_interp_ratio = 20;
    mock_ddc.set_property<int>("decim", disconnected_interp_ratio, 0);
    BOOST_CHECK_EQUAL(mock_ddc._samp_rate_in.get() / disconnected_interp_ratio,
        mock_ddc._samp_rate_out.get());
}


BOOST_AUTO_TEST_CASE(test_graph_node_loop_resolve)
{
    // Mock the radio with only the AIS property. It will keep AIS at a multiple
    // of 4.
    class mock_radio_ais_node_t : public node_t
    {
    public:
        mock_radio_ais_node_t()
        {
            register_property(&_ais_in);
            register_property(&_ais_out);

            add_property_resolver({&_ais_in}, {&_ais_in}, [this]() {
                _ais_in = std::max<size_t>(4, (_ais_in.get() / 4) * 4);
            });
            add_property_resolver({&_ais_out}, {&_ais_out}, [this]() {
                _ais_out = std::max<size_t>(4, (_ais_out.get() / 4) * 4);
            });
        }

        std::string get_unique_id() const override
        {
            return "MOCK_RADIO_AIS_NODE";
        }

        size_t get_num_input_ports() const override
        {
            return 1;
        }

        size_t get_num_output_ports() const override
        {
            return 1;
        }

    private:
        property_t<size_t> _ais_in{
            PROP_KEY_ATOMIC_ITEM_SIZE, 4, {res_source_info::INPUT_EDGE}};
        property_t<size_t> _ais_out{
            PROP_KEY_ATOMIC_ITEM_SIZE, 8, {res_source_info::OUTPUT_EDGE}};
    };

    // Mock the replay with only the AIS property. It will keep AIS at exactly 8.
    class mock_replay_ais_node_t : public node_t
    {
    public:
        mock_replay_ais_node_t()
        {
            register_property(&_ais_in);
            register_property(&_ais_out);

            add_property_resolver({&_ais_in}, {&_ais_in}, [this]() {
                _ais_in = 8;
            });
            add_property_resolver({&_ais_out}, {&_ais_out}, [this]() {
                _ais_out = 8;
            });
        }

        std::string get_unique_id() const override
        {
            return "MOCK_REPLAY_AIS_NODE";
        }

        size_t get_num_input_ports() const override
        {
            return 1;
        }

        size_t get_num_output_ports() const override
        {
            return 1;
        }

    private:
        property_t<size_t> _ais_in{
            PROP_KEY_ATOMIC_ITEM_SIZE, 4, {res_source_info::INPUT_EDGE}};
        property_t<size_t> _ais_out{
            PROP_KEY_ATOMIC_ITEM_SIZE, 8, {res_source_info::OUTPUT_EDGE}};
    };

    // Now let's define things
    node_accessor_t node_accessor{};
    uhd::rfnoc::detail::graph_t graph{};

    // Create mock blocks
    mock_radio_ais_node_t mock_radio_ais_node{};
    mock_replay_ais_node_t mock_replay_ais_node{};

    // These init calls would normally be done by the framework
    node_accessor.init_props(&mock_radio_ais_node);
    node_accessor.init_props(&mock_replay_ais_node);

    // Connect the radio to the replay in a simple graph
    uhd::rfnoc::detail::graph_t::graph_edge_t edge_info;
    edge_info.src_port                    = 0;
    edge_info.dst_port                    = 0;
    edge_info.property_propagation_active = true;
    edge_info.edge = uhd::rfnoc::detail::graph_t::graph_edge_t::DYNAMIC;

    graph.connect(&mock_radio_ais_node, &mock_replay_ais_node, edge_info);
    // Declare back-edge
    edge_info.property_propagation_active = false;
    graph.connect(&mock_replay_ais_node, &mock_radio_ais_node, edge_info);
    UHD_LOG_INFO("TEST", "Committing replay/radio loop graph");
    BOOST_CHECK_NO_THROW(graph.commit(););
}