diff options
Diffstat (limited to 'host/tests')
| -rw-r--r-- | host/tests/CMakeLists.txt | 26 | ||||
| -rw-r--r-- | host/tests/rfnoc_detailgraph_test.cpp | 215 | ||||
| -rw-r--r-- | host/tests/rfnoc_graph_mock_nodes.hpp | 233 | ||||
| -rw-r--r-- | host/tests/rfnoc_node_test.cpp | 58 | ||||
| -rw-r--r-- | host/tests/rfnoc_propprop_test.cpp | 366 | 
5 files changed, 886 insertions, 12 deletions
diff --git a/host/tests/CMakeLists.txt b/host/tests/CMakeLists.txt index eab27833b..551b57bb9 100644 --- a/host/tests/CMakeLists.txt +++ b/host/tests/CMakeLists.txt @@ -208,6 +208,32 @@ UHD_ADD_NONAPI_TEST(      ${CMAKE_SOURCE_DIR}/lib/utils/pathslib.cpp  ) +add_executable(rfnoc_propprop_test +    rfnoc_propprop_test.cpp +    ${CMAKE_SOURCE_DIR}/lib/rfnoc/graph.cpp +) +target_link_libraries(rfnoc_propprop_test uhd ${Boost_LIBRARIES}) +UHD_ADD_TEST(rfnoc_propprop_test rfnoc_propprop_test) +UHD_INSTALL(TARGETS +    rfnoc_propprop_test +    RUNTIME +    DESTINATION ${PKG_LIB_DIR}/tests +    COMPONENT tests +) + +add_executable(rfnoc_detailgraph_test +    rfnoc_detailgraph_test.cpp +    ${CMAKE_SOURCE_DIR}/lib/rfnoc/graph.cpp +) +target_link_libraries(rfnoc_detailgraph_test uhd ${Boost_LIBRARIES}) +UHD_ADD_TEST(rfnoc_detailgraph_test rfnoc_detailgraph_test) +UHD_INSTALL(TARGETS +    rfnoc_detailgraph_test +    RUNTIME +    DESTINATION ${PKG_LIB_DIR}/tests +    COMPONENT tests +) +  ########################################################################  # demo of a loadable module  ######################################################################## diff --git a/host/tests/rfnoc_detailgraph_test.cpp b/host/tests/rfnoc_detailgraph_test.cpp new file mode 100644 index 000000000..6273430e6 --- /dev/null +++ b/host/tests/rfnoc_detailgraph_test.cpp @@ -0,0 +1,215 @@ +// +// Copyright 2019 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#include <uhd/rfnoc/node.hpp> +#include <uhd/utils/log.hpp> +#include <uhdlib/rfnoc/node_accessor.hpp> +#include <uhdlib/rfnoc/prop_accessor.hpp> +#include <uhdlib/rfnoc/graph.hpp> +#include <boost/test/unit_test.hpp> +#include <iostream> + +#include "rfnoc_graph_mock_nodes.hpp" + +using uhd::rfnoc::detail::graph_t; +using namespace uhd::rfnoc; + +namespace uhd { namespace rfnoc { namespace detail { + +/*! Helper class to access internals of detail::graph + * + * This is basically a cheat code to get around the 'private' part of graph_t. + */ +class graph_accessor_t +{ +public: +    using vertex_descriptor = graph_t::rfnoc_graph_t::vertex_descriptor; + +    graph_accessor_t(graph_t* graph_ptr) : _graph_ptr(graph_ptr) +    { /* nop */ +    } + +    graph_t::rfnoc_graph_t& get_graph() +    { +        return _graph_ptr->_graph; +    } + +    template <typename VertexIterator> +    graph_t::node_ref_t get_node_ref_from_iterator(VertexIterator it) +    { +        return boost::get(graph_t::vertex_property_t(), get_graph(), *it); +    } + +    auto find_neighbour(vertex_descriptor origin, res_source_info port_info) +    { +        return _graph_ptr->_find_neighbour(origin, port_info); +    } + +    auto find_dirty_nodes() +    { +        return _graph_ptr->_find_dirty_nodes(); +    } + +    auto get_topo_sorted_nodes() +    { +        return _graph_ptr->_vertices_to_nodes(_graph_ptr->_get_topo_sorted_nodes()); +    } +private: +    graph_t* _graph_ptr; +}; + +}}}; + +BOOST_AUTO_TEST_CASE(test_graph) +{ +    graph_t graph{}; +    uhd::rfnoc::detail::graph_accessor_t graph_accessor(&graph); +    node_accessor_t node_accessor{}; + +    auto& bgl_graph = graph_accessor.get_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_rx_radio); +    node_accessor.init_props(&mock_tx_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: +    graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info); + +    // A whole bunch of low-level checks first: +    BOOST_CHECK_EQUAL(boost::num_vertices(bgl_graph), 2); +    auto vertex_iterators = boost::vertices(bgl_graph); +    auto vertex_iterator  = vertex_iterators.first; +    auto rx_descriptor    = *vertex_iterator; +    graph_t::node_ref_t node_ref = +        graph_accessor.get_node_ref_from_iterator(vertex_iterator++); +    BOOST_CHECK_EQUAL(node_ref->get_unique_id(), mock_rx_radio.get_unique_id()); +    auto tx_descriptor = *vertex_iterator; +    node_ref           = graph_accessor.get_node_ref_from_iterator(vertex_iterator++); +    BOOST_CHECK_EQUAL(node_ref->get_unique_id(), mock_tx_radio.get_unique_id()); +    BOOST_CHECK(vertex_iterator == vertex_iterators.second); + +    auto rx_neighbour_info = +        graph_accessor.find_neighbour(rx_descriptor, {res_source_info::OUTPUT_EDGE, 0}); +    BOOST_REQUIRE(rx_neighbour_info.first); +    BOOST_CHECK_EQUAL( +        rx_neighbour_info.first->get_unique_id(), mock_tx_radio.get_unique_id()); +    BOOST_CHECK(std::tie(rx_neighbour_info.second.src_port, +                    rx_neighbour_info.second.dst_port, +                    rx_neighbour_info.second.property_propagation_active) +                == std::tie(edge_info.src_port, +                       edge_info.dst_port, +                       edge_info.property_propagation_active)); + +    auto tx_neighbour_info = +        graph_accessor.find_neighbour(tx_descriptor, {res_source_info::INPUT_EDGE, 0}); +    BOOST_REQUIRE(tx_neighbour_info.first); +    BOOST_CHECK_EQUAL( +        tx_neighbour_info.first->get_unique_id(), mock_rx_radio.get_unique_id()); +    BOOST_CHECK(std::tie(tx_neighbour_info.second.src_port, +                    tx_neighbour_info.second.dst_port, +                    tx_neighbour_info.second.property_propagation_active) +                == std::tie(edge_info.src_port, +                       edge_info.dst_port, +                       edge_info.property_propagation_active)); + +    auto rx_upstream_neighbour_info = +        graph_accessor.find_neighbour(rx_descriptor, {res_source_info::INPUT_EDGE, 0}); +    BOOST_CHECK(rx_upstream_neighbour_info.first == nullptr); +    auto tx_downstream_neighbour_info = +        graph_accessor.find_neighbour(tx_descriptor, {res_source_info::OUTPUT_EDGE, 0}); +    BOOST_CHECK(tx_downstream_neighbour_info.first == nullptr); +    auto rx_wrongport_neighbour_info = +        graph_accessor.find_neighbour(rx_descriptor, {res_source_info::OUTPUT_EDGE, 1}); +    BOOST_CHECK(rx_wrongport_neighbour_info.first == nullptr); +    auto tx_wrongport_neighbour_info = +        graph_accessor.find_neighbour(tx_descriptor, {res_source_info::INPUT_EDGE, 1}); +    BOOST_CHECK(tx_wrongport_neighbour_info.first == nullptr); + +    // Check there are no dirty nodes (init_props() will clean them all) +    BOOST_CHECK_EQUAL(graph_accessor.find_dirty_nodes().empty(), true); + +    auto topo_sorted_nodes = graph_accessor.get_topo_sorted_nodes(); +    BOOST_CHECK_EQUAL(topo_sorted_nodes.size(), 2); +    BOOST_CHECK_EQUAL( +        topo_sorted_nodes.at(0)->get_unique_id(), mock_rx_radio.get_unique_id()); + +    // Now initialize the graph (will force a call to resolve_all_properties()) +    graph.initialize(); + +    // This will be ignored +    graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info); +    BOOST_CHECK_EQUAL(boost::num_vertices(bgl_graph), 2); + +    // Now attempt illegal connections (they must all fail) +    edge_info.src_port = 1; +    edge_info.dst_port = 0; +    BOOST_REQUIRE_THROW( +        graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info), uhd::rfnoc_error); +    edge_info.src_port = 0; +    edge_info.dst_port = 1; +    BOOST_REQUIRE_THROW( +        graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info), uhd::rfnoc_error); +    edge_info.src_port                    = 0; +    edge_info.dst_port                    = 0; +    edge_info.property_propagation_active = false; +    BOOST_REQUIRE_THROW( +        graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info), uhd::rfnoc_error); +} + +BOOST_AUTO_TEST_CASE(test_graph_unresolvable) +{ +    graph_t graph{}; +    node_accessor_t node_accessor{}; + +    // 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_rx_radio); +    node_accessor.init_props(&mock_tx_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( +        0, 0, graph_t::graph_edge_t::DYNAMIC, true); + +    // Now create the graph and commit: +    graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info); +    graph.initialize(); + +    // Now set a property that will cause the graph to fail to resolve: +    BOOST_REQUIRE_THROW(mock_tx_radio.set_property<double>("master_clock_rate", 100e6, 0), +        uhd::resolve_error); + +    // Now we add a back-edge +    edge_info.src_port                    = 0; +    edge_info.dst_port                    = 0; +    edge_info.property_propagation_active = false; +    graph.connect(&mock_tx_radio, &mock_rx_radio, edge_info); +    UHD_LOG_INFO("TEST", "Testing back edge error path"); +    mock_tx_radio.disable_samp_out_resolver = true; +    // The set_property would be valid if we hadn't futzed with the back-edge +    BOOST_REQUIRE_THROW(mock_tx_radio.set_property<double>("master_clock_rate", 200e6, 0), +        uhd::resolve_error); +    UHD_LOG_INFO("TEST", "^^^ Expected ERROR here."); +} diff --git a/host/tests/rfnoc_graph_mock_nodes.hpp b/host/tests/rfnoc_graph_mock_nodes.hpp new file mode 100644 index 000000000..85e667ebd --- /dev/null +++ b/host/tests/rfnoc_graph_mock_nodes.hpp @@ -0,0 +1,233 @@ +// +// Copyright 2019 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#ifndef INCLUDED_LIBUHD_TESTS_MOCK_NODES_HPP +#define INCLUDED_LIBUHD_TESTS_MOCK_NODES_HPP + +#include <uhd/rfnoc/node.hpp> + +using namespace uhd::rfnoc; + +constexpr int MAX_DECIM       = 512; +constexpr double DEFAULT_RATE = 1e9; +constexpr int DEFAULT_DECIM   = 1; + +/*! Mock Radio node + * + * - "Full Duplex" + * - Has two master clock rates: 100e6 and 200e6 + * - RSSI is a read-only prop that always needs updating + */ +class mock_radio_node_t : public node_t +{ +public: +    mock_radio_node_t(const size_t radio_idx) : _radio_idx(radio_idx) +    { +        register_property(&_samp_rate_in); +        register_property(&_samp_rate_out); +        register_property(&_master_clock_rate); +        register_property(&_rssi); + +        // Resolver for the input rate: We don't actually try and be clever, we +        // always reset the rate back to the TX rate. +        add_property_resolver({&_samp_rate_in}, +            {&_samp_rate_in}, +            [& samp_rate_in        = _samp_rate_in, +                &master_clock_rate = _master_clock_rate, +                this]() { +                UHD_LOG_INFO(get_unique_id(), " Calling resolver for `samp_rate_in'..."); +                samp_rate_in = master_clock_rate.get(); +            }); +        add_property_resolver({&_samp_rate_out}, +            {&_samp_rate_out}, +            [this]() { +                UHD_LOG_INFO(get_unique_id(), " Calling resolver for `samp_rate_out'..."); +                if (this->disable_samp_out_resolver) { +                    _samp_rate_out = this->force_samp_out_value; +                    UHD_LOG_DEBUG(get_unique_id(), +                        "Forcing samp_rate_out to " << _samp_rate_out.get()); +                    return; +                } +                this->_samp_rate_out = this->_master_clock_rate.get(); +            }); +        add_property_resolver({&_master_clock_rate}, +            {&_master_clock_rate, &_samp_rate_in, &_samp_rate_out}, +            [& samp_rate_out       = _samp_rate_out, +                &samp_rate_in      = _samp_rate_in, +                &master_clock_rate = _master_clock_rate, +                this]() { +                UHD_LOG_INFO(get_unique_id(), " Calling resolver for `master_clock_rate'..."); +                if (_master_clock_rate.get() > 150e6) { +                    _master_clock_rate = 200e6; +                } else { +                    _master_clock_rate = 100e6; +                } +                _samp_rate_in = _master_clock_rate.get(); +                if (!this->disable_samp_out_resolver) { +                    _samp_rate_out = _master_clock_rate.get(); +                } else { +                    _samp_rate_out = this->force_samp_out_value; +                    UHD_LOG_DEBUG(get_unique_id(), +                        "Forcing samp_rate_out to " << _samp_rate_out.get()); +                } +            }); +        // By depending on ALWAYS_DIRTY, this property is always updated: +        add_property_resolver({&ALWAYS_DIRTY}, +            {&_rssi}, +            [this]() { +                UHD_LOG_INFO(get_unique_id(), " Calling resolver for `rssi'..."); +                rssi_resolver_count++; +                _rssi = static_cast<double>(rssi_resolver_count); +            }); +    } + +    std::string get_unique_id() const { return "MOCK_RADIO" + std::to_string(_radio_idx); } + +    size_t get_num_input_ports() const +    { +        return 1; +    } + +    size_t get_num_output_ports() const +    { +        return 1; +    } + +    // Some public attributes that help debugging +    size_t rssi_resolver_count = 0; +    bool disable_samp_out_resolver = false; +    double force_samp_out_value = 23e6; + +private: +    const size_t _radio_idx; + +    property_t<double> _samp_rate_in{ +        "samp_rate", 200e6, {res_source_info::INPUT_EDGE}}; +    property_t<double> _samp_rate_out{ +        "samp_rate", 200e6, {res_source_info::OUTPUT_EDGE}}; +    property_t<double> _master_clock_rate{ +        "master_clock_rate", 200e6, {res_source_info::USER}}; +    property_t<double> _rssi{"rssi", 0, {res_source_info::USER}}; +}; + +/*! Mock DDC node + * + * - Single channel + * - Does simple coercion of decimation + * - Keeps output and input rates consistent with decimation + */ +class mock_ddc_node_t : public node_t +{ +public: +    mock_ddc_node_t() +    { +        register_property(&_samp_rate_in); +        register_property(&_samp_rate_out); +        register_property(&_decim); + +        // Resolver for _decim: This gets executed when the user directly +        // modifies _decim. The desired behaviour is to coerce it first, then +        // keep the input rate constant, and re-calculate the output rate. +        add_property_resolver({&_decim}, +            {&_decim, &_samp_rate_out}, +            [& decim           = _decim, +                &samp_rate_out = _samp_rate_out, +                &samp_rate_in  = _samp_rate_in]() { +                UHD_LOG_INFO("MOCK DDC", "Calling resolver for `decim'..."); +                decim         = coerce_decim(decim.get()); +                samp_rate_out = samp_rate_in.get() / decim.get(); +            }); +        // Resolver for the input rate: We try and match decim so that the output +        // rate is not modified. If decim needs to be coerced, only then the +        // output rate is modified. +        add_property_resolver({&_samp_rate_in}, +            {&_decim, &_samp_rate_out}, +            [& decim           = _decim, +                &samp_rate_out = _samp_rate_out, +                &samp_rate_in  = _samp_rate_in]() { +                UHD_LOG_INFO("MOCK DDC", "Calling resolver for `samp_rate_in'..."); +                decim = coerce_decim(int(samp_rate_in.get() / samp_rate_out.get())); +                samp_rate_out = samp_rate_in.get() / decim.get(); +            }); +        // Resolver for the output rate: Like the previous one, but flipped. +        add_property_resolver({&_samp_rate_out}, +            {&_decim, &_samp_rate_in}, +            [& decim           = _decim, +                &samp_rate_out = _samp_rate_out, +                &samp_rate_in  = _samp_rate_in]() { +                UHD_LOG_INFO("MOCK DDC", "Calling resolver for `samp_rate_out'..."); +                decim = coerce_decim(int(samp_rate_in.get() / samp_rate_out.get())); +                samp_rate_in = samp_rate_out.get() * decim.get(); +            }); +    } + +    std::string get_unique_id() const { return "MOCK_DDC"; } + +    size_t get_num_input_ports() const +    { +        return 1; +    } + +    size_t get_num_output_ports() const +    { +        return 1; +    } + +    // Simplified coercer: Let's pretend like we can hit all even rates or 1 +    // for all rates <= MAX_DECIM +    static int coerce_decim(const int requested_decim) +    { +        if (requested_decim <= 1) { +            return 1; +        } +        return std::min(requested_decim - (requested_decim % 2), MAX_DECIM); +    } + + +    // We make the properties global so we can inspect them, but that's not what +    // your supposed to do. However, we do keep the underscore notation, since that's +    // what they be called if they were in the class like they're supposed to. +    property_t<double> _samp_rate_in{ +        "samp_rate", DEFAULT_RATE, {res_source_info::INPUT_EDGE}}; +    property_t<double> _samp_rate_out{ +        "samp_rate", DEFAULT_RATE, {res_source_info::OUTPUT_EDGE}}; +    property_t<int> _decim{"decim", DEFAULT_DECIM, {res_source_info::USER}}; + +private: +    // This is where you normally put the properties +}; + + +/*! FIFO + * + * Not much here -- we use it to test dynamic prop forwarding. + */ +class mock_fifo_t : public node_t +{ +public: +    mock_fifo_t(const size_t num_ports) : _num_ports(num_ports) +    { +        set_prop_forwarding_policy(forwarding_policy_t::ONE_TO_ONE); +    } + +    std::string get_unique_id() const { return "MOCK_FIFO"; } + +    size_t get_num_input_ports() const +    { +        return _num_ports; +    } + +    size_t get_num_output_ports() const +    { +        return _num_ports; +    } + + +private: +    const size_t _num_ports; +}; + +#endif /* INCLUDED_LIBUHD_TESTS_MOCK_NODES_HPP */ diff --git a/host/tests/rfnoc_node_test.cpp b/host/tests/rfnoc_node_test.cpp index 046f8ab33..15597de16 100644 --- a/host/tests/rfnoc_node_test.cpp +++ b/host/tests/rfnoc_node_test.cpp @@ -5,6 +5,7 @@  //  #include <uhd/rfnoc/node.hpp> +#include <uhdlib/rfnoc/node_accessor.hpp>  #include <boost/test/unit_test.hpp>  #include <iostream> @@ -16,13 +17,17 @@ public:      test_node_t(size_t num_inputs, size_t num_outputs)          : _num_input_ports(num_inputs), _num_output_ports(num_outputs)      { -        register_property(&_double_prop_user); +        register_property(&_double_prop_user, [this]() { +            std::cout << "Calling clean callback for user prop" << std::endl; +            this->user_prop_cb_called = true; +        });          register_property(&_double_prop_in);          register_property(&_double_prop_out);          // A property with a simple 1:1 dependency -        add_property_resolver( -            {&_double_prop_user}, {&_double_prop_out}, []() { std::cout << "foo" << std::endl; }); +        add_property_resolver({&_double_prop_user}, {&_double_prop_out}, []() { +            std::cout << "Executing user prop resolver" << std::endl; +        });      }      //! Register a property for the second time, with the goal of triggering an @@ -33,35 +38,46 @@ public:      }      //! Register an identical property for the first time, with the goal of -    //triggering an exception +    // triggering an exception      void double_register_input()      { -        property_t<double> double_prop_in{"double_prop", 0.0, {res_source_info::INPUT_EDGE, 0}}; +        property_t<double> double_prop_in{ +            "double_prop", 0.0, {res_source_info::INPUT_EDGE, 0}};          register_property(&double_prop_in);      }      //! This should throw an error because the property in the output isn't      // registered -    void add_unregistered_resolver_in() { +    void add_unregistered_resolver_in() +    {          property_t<double> temp{"temp", 0.0, {res_source_info::INPUT_EDGE, 5}}; -        add_property_resolver( -            {&temp}, {}, []() { std::cout << "foo" << std::endl; }); +        add_property_resolver({&temp}, {}, []() { std::cout << "foo" << std::endl; });      }      //! This should throw an error because the property in the output isn't      // registered -    void add_unregistered_resolver_out() { +    void add_unregistered_resolver_out() +    {          property_t<double> temp{"temp", 0.0, {res_source_info::INPUT_EDGE, 5}};          add_property_resolver(              {&_double_prop_user}, {&temp}, []() { std::cout << "foo" << std::endl; });      } -    size_t get_num_input_ports() const { return _num_input_ports; } -    size_t get_num_output_ports() const { return _num_output_ports; } +    size_t get_num_input_ports() const +    { +        return _num_input_ports; +    } +    size_t get_num_output_ports() const +    { +        return _num_output_ports; +    } + +    bool user_prop_cb_called = false;  private:      property_t<double> _double_prop_user{"double_prop", 0.0, {res_source_info::USER}}; -    property_t<double> _double_prop_in{"double_prop", 0.0, {res_source_info::INPUT_EDGE, 0}}; +    property_t<double> _double_prop_in{ +        "double_prop", 0.0, {res_source_info::INPUT_EDGE, 0}};      property_t<double> _double_prop_out{          "double_prop", 0.0, {res_source_info::OUTPUT_EDGE, 1}}; @@ -106,3 +122,21 @@ BOOST_AUTO_TEST_CASE(test_node_prop_access)      BOOST_CHECK_EQUAL(TN1.get_property<double>("double_prop"), 4.2);  } +BOOST_AUTO_TEST_CASE(test_node_accessor) +{ +    test_node_t TN1(2, 3); +    node_accessor_t node_accessor{}; + +    auto user_props = node_accessor.filter_props(&TN1, [](property_base_t* prop) { +        return (prop->get_src_info().type == res_source_info::USER); +    }); + +    BOOST_CHECK_EQUAL(user_props.size(), 1); +    BOOST_CHECK_EQUAL((*user_props.begin())->get_id(), "double_prop"); +    BOOST_CHECK((*user_props.begin())->get_src_info().type == res_source_info::USER); + +    BOOST_CHECK(!TN1.user_prop_cb_called); +    node_accessor.init_props(&TN1); +    BOOST_CHECK(TN1.user_prop_cb_called); + +} diff --git a/host/tests/rfnoc_propprop_test.cpp b/host/tests/rfnoc_propprop_test.cpp new file mode 100644 index 000000000..1b9b94b05 --- /dev/null +++ b/host/tests/rfnoc_propprop_test.cpp @@ -0,0 +1,366 @@ +// +// Copyright 2019 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#include "rfnoc_graph_mock_nodes.hpp" +#include <uhd/utils/log.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> + +/*! 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 +    { +        return "MOCK_INVALID_NODE1"; +    } + +    size_t get_num_input_ports() const +    { +        return 1; +    } + +    size_t get_num_output_ports() const +    { +        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 +    { +        return 1; +    } + +    size_t get_num_output_ports() const +    { +        return 1; +    } + +    std::string get_unique_id() const +    { +        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}}; +}; + +// 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); +} + +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.initialize(); +    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); +} + + +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.initialize(), 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.initialize(); + +    const size_t rx_rssi_resolver_count = mock_rx_radio.rssi_resolver_count; +    UHD_LOG_DEBUG("TEST", "RX RSSI: " << mock_rx_radio.get_property<double>("rssi")); +    // The next value must match the value in graph.cpp +    constexpr size_t MAX_NUM_ITERATIONS = 2; +    BOOST_CHECK_EQUAL( +        rx_rssi_resolver_count + MAX_NUM_ITERATIONS, 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.initialize(); +}  | 
