aboutsummaryrefslogtreecommitdiffstats
path: root/host/tests
diff options
context:
space:
mode:
authorMartin Braun <martin.braun@ettus.com>2019-04-24 18:23:31 -0700
committerMartin Braun <martin.braun@ettus.com>2019-11-26 11:49:14 -0800
commitc97bdc6c94c98753215a90cf499af4bdf06db8e2 (patch)
tree67f623ae84acb045d145bd22036df60a1724b789 /host/tests
parentf0371292a43c3e4e3c68d8631c57d64ab10faf4c (diff)
downloaduhd-c97bdc6c94c98753215a90cf499af4bdf06db8e2.tar.gz
uhd-c97bdc6c94c98753215a90cf499af4bdf06db8e2.tar.bz2
uhd-c97bdc6c94c98753215a90cf499af4bdf06db8e2.zip
rfnoc: Add property propagation, Boost.Graph storage
- Adds a detail::graph_t class, which handles the propagation - Adds methods to node_t to aid with propagation - Adds unit tests - Adds dynamic property forwarding: Nodes are now able to forward properties they don't know about by providing a forwarding policy. A good example is the FIFO block which simply forwards most properties verbatim. - node: Temporarily disabling consistency check at init
Diffstat (limited to 'host/tests')
-rw-r--r--host/tests/CMakeLists.txt26
-rw-r--r--host/tests/rfnoc_detailgraph_test.cpp215
-rw-r--r--host/tests/rfnoc_graph_mock_nodes.hpp233
-rw-r--r--host/tests/rfnoc_node_test.cpp58
-rw-r--r--host/tests/rfnoc_propprop_test.cpp366
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();
+}