diff --git a/include/attwoodn/expression_tree.hpp b/include/attwoodn/expression_tree.hpp index cafcb31..2e79536 100644 --- a/include/attwoodn/expression_tree.hpp +++ b/include/attwoodn/expression_tree.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include namespace attwoodn::expression_tree { @@ -53,13 +54,13 @@ namespace attwoodn::expression_tree { } } - enum class boolean_op { - AND, - OR - }; - namespace node { + enum class boolean_op { + AND, + OR + }; + template class expression_tree_op_node; @@ -83,7 +84,17 @@ namespace attwoodn::expression_tree { * False if the given object did not satisfy the expression in this node and the expressions of all * nodes under this node in the expression tree. */ - virtual bool evaluate(const Obj& obj) = 0; + virtual bool evaluate(const Obj& obj) const = 0; + + /** + * @brief Performs a deep clone of pointers to this base class to avoid object slicing. + */ + auto clone() const { + return std::unique_ptr>(clone_impl()); + } + + protected: + virtual expression_tree_node* clone_impl() const = 0; }; /** @@ -95,6 +106,11 @@ namespace attwoodn::expression_tree { public: using this_type = expression_tree_op_node; + expression_tree_op_node() = delete; + expression_tree_op_node(expression_tree_op_node&& other) = delete; + expression_tree_op_node& operator=(const expression_tree_op_node& other) = delete; + expression_tree_op_node& operator=(expression_tree_op_node&& other) = delete; + expression_tree_op_node(boolean_op bool_op) : bool_op_(bool_op) {} @@ -123,7 +139,7 @@ namespace attwoodn::expression_tree { delete l; } - bool evaluate(const Obj& obj) override { + bool evaluate(const Obj& obj) const override { if(!left_ || !right_) { throw std::runtime_error("expression_tree_op_node has a missing child node"); } @@ -151,10 +167,10 @@ namespace attwoodn::expression_tree { * and the other node that was AND'ed with this node. This node becomes the left child. The other node becomes * the right child. */ - template>::value>* = nullptr> - expression_tree_op_node* AND (OtherLeafNode* other) { - auto* op_node = new expression_tree_op_node(boolean_op::AND); + template + auto* AND (expression_tree_leaf_node* other) { + using ret = expression_tree_op_node>; + ret* op_node = new ret(boolean_op::AND); op_node->set_left(this); op_node->set_right(other); return op_node; @@ -166,10 +182,10 @@ namespace attwoodn::expression_tree { * and the other node that was OR'ed with this node. This node becomes the left child. The other node becomes * the right child. */ - template>::value>* = nullptr> - expression_tree_op_node* OR (OtherLeafNode* other) { - auto* op_node = new expression_tree_op_node(boolean_op::OR); + template + auto* OR (expression_tree_leaf_node* other) { + using ret = expression_tree_op_node>; + ret* op_node = new ret(boolean_op::OR); op_node->set_left(this); op_node->set_right(other); return op_node; @@ -181,10 +197,10 @@ namespace attwoodn::expression_tree { * and the other node that was AND'ed with this node. This node becomes the left child. The other node becomes * the right child. */ - template>::value>* = nullptr> - expression_tree_op_node* AND (OtherOpNode* other) { - auto* op_node = new expression_tree_op_node(boolean_op::AND); + template + auto* AND (expression_tree_op_node* other) { + using ret = expression_tree_op_node>; + ret* op_node = new ret(boolean_op::AND); op_node->set_left(this); op_node->set_right(other); return op_node; @@ -196,10 +212,10 @@ namespace attwoodn::expression_tree { * and the other node that was OR'ed with this node. This node becomes the left child. The other node becomes * the right child. */ - template>::value>* = nullptr> - expression_tree_op_node* OR (OtherOpNode* other) { - auto* op_node = new expression_tree_op_node(boolean_op::OR); + template + auto* OR (expression_tree_op_node* other) { + using ret = expression_tree_op_node>; + ret* op_node = new ret(boolean_op::OR); op_node->set_left(this); op_node->set_right(other); return op_node; @@ -209,6 +225,11 @@ namespace attwoodn::expression_tree { boolean_op bool_op_; LeftChild* left_ { nullptr }; RightChild* right_ { nullptr }; + + protected: + virtual expression_tree_op_node* clone_impl() const override { + return new expression_tree_op_node(*this); + } }; /** @@ -221,6 +242,13 @@ namespace attwoodn::expression_tree { public: using this_type = expression_tree_leaf_node; + expression_tree_leaf_node() = delete; + + expression_tree_leaf_node(const expression_tree_leaf_node& other) = default; + expression_tree_leaf_node(expression_tree_leaf_node&& other) = default; + expression_tree_leaf_node& operator=(const expression_tree_leaf_node& other) = default; + expression_tree_leaf_node& operator=(expression_tree_leaf_node&& other) = default; + /** * @brief Constructor that accepts a reference to a member variable of Obj */ @@ -239,7 +267,7 @@ namespace attwoodn::expression_tree { ~expression_tree_leaf_node() override {}; - bool evaluate(const Obj& obj) override { + bool evaluate(const Obj& obj) const override { if (member_func_ && member_var_) { throw std::runtime_error("expression_tree_leaf_node has both a member function reference " + std::string("and member variable reference. Only one is permitted")); @@ -273,10 +301,10 @@ namespace attwoodn::expression_tree { * and the other node that was AND'ed with this node. This node becomes the left child. The other node becomes * the right child. */ - template>::value>* = nullptr> - expression_tree_op_node* AND (OtherLeafNode* other) { - auto* op_node = new expression_tree_op_node(boolean_op::AND); + template + auto* AND (expression_tree_leaf_node* other) { + using ret = expression_tree_op_node>; + ret* op_node = new ret(boolean_op::AND); op_node->set_left(this); op_node->set_right(other); return op_node; @@ -288,10 +316,10 @@ namespace attwoodn::expression_tree { * and the other node that was OR'ed with this node. This node becomes the left child. The other node becomes * the right child. */ - template>::value>* = nullptr> - expression_tree_op_node* OR (OtherLeafNode* other) { - auto* op_node = new expression_tree_op_node(boolean_op::OR); + template + auto* OR (expression_tree_leaf_node* other) { + using ret = expression_tree_op_node>; + ret* op_node = new ret(boolean_op::OR); op_node->set_left(this); op_node->set_right(other); return op_node; @@ -303,10 +331,10 @@ namespace attwoodn::expression_tree { * and the other node that was AND'ed with this node. This node becomes the left child. The other node becomes * the right child. */ - template>::value>* = nullptr> - expression_tree_op_node* AND (OtherOpNode* other) { - auto* op_node = new expression_tree_op_node(boolean_op::AND); + template + auto* AND (expression_tree_op_node* other) { + using ret = expression_tree_op_node>; + ret* op_node = new ret(boolean_op::AND); op_node->set_left(this); op_node->set_right(other); return op_node; @@ -318,10 +346,10 @@ namespace attwoodn::expression_tree { * and the other node that was OR'ed with this node. This node becomes the left child. The other node becomes * the right child. */ - template>::value>* = nullptr> - expression_tree_op_node* OR (OtherOpNode* other) { - auto* op_node = new expression_tree_op_node(boolean_op::OR); + template + auto* OR (expression_tree_op_node* other) { + using ret = expression_tree_op_node>; + ret* op_node = new ret(boolean_op::OR); op_node->set_left(this); op_node->set_right(other); return op_node; @@ -332,6 +360,11 @@ namespace attwoodn::expression_tree { const CompValue Obj::* member_var_ = nullptr; Op logical_op_; CompValue comp_value_; + + protected: + virtual expression_tree_leaf_node* clone_impl() const override { + return new expression_tree_leaf_node(*this); + } }; } @@ -362,4 +395,85 @@ namespace attwoodn::expression_tree { node::expression_tree_leaf_node* make_expr( CompValue (Obj::* member_func)() const, Op op, CompValue comp_value ) { return new node::expression_tree_leaf_node( member_func, op, comp_value ); } -} \ No newline at end of file + + template + class expression_tree { + public: + expression_tree() = delete; + + expression_tree(node::expression_tree_node* expr) { + if(!expr) { + throw std::runtime_error("Attempted to construct an expression_tree using a null expression"); + } + expr_ = expr->clone().release(); + delete expr; + } + + expression_tree(std::unique_ptr> expr) + : expression_tree(expr.release()) {} + + expression_tree(const expression_tree& other) { + if(!other.expr_) { + throw std::runtime_error("Attempted to copy construct an expression_tree " + + std::string("from an expression_tree with a null expression")); + } + expr_ = other.expr_->clone().release(); + } + + expression_tree(expression_tree&& other) { + if(!other.expr_) { + throw std::runtime_error("Attempted to move construct an expression_tree " + + std::string("from an expression_tree with a null expression")); + } + expr_ = other.expr_; + other.expr_ = nullptr; + } + + expression_tree& operator=(const expression_tree& other) { + if(!other.expr_) { + throw std::runtime_error("Attempted copy assignment from an expression_tree with a null expression"); + } + delete expr_; + expr_ = other.expr_->clone().release(); + return *this; + } + + expression_tree& operator=(expression_tree&& other) { + if(!other.expr_) { + throw std::runtime_error("Attempted move assignment from an expression_tree with a null expression"); + } + + if(this != &other) { + delete expr_; + expr_ = other.expr_; + other.expr_ = nullptr; + } + return *this; + } + + ~expression_tree() { + delete expr_; + } + + /** + * @brief Evaluates the given object to determine if it satisfies the expressions defined in this expression tree. + * + * @returns True if the given object satisfied the expression tree conditions; + * False if the given object did not satisfy the expression tree conditions. + */ + bool evaluate(const Obj& obj) const { + if(!expr_) { + throw std::runtime_error("expression_tree has a null root expression node"); + } + + try { + return expr_->evaluate(obj); + } catch(std::exception& e) { + return false; + } + } + + private: + node::expression_tree_node* expr_ = nullptr; + }; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b692197..e5182a1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,4 +15,9 @@ if(BUILD_TESTING) target_compile_options( expression_tree_op_node_test PRIVATE -fsanitize=address ) add_test( expression_tree_op_node_test ${EXECUTABLE_OUTPUT_PATH}/expression_tree_op_node_test ) + add_executable( expression_tree_test expression_tree.cpp ) + target_link_libraries( expression_tree_test "-fsanitize=address" ) + target_compile_options( expression_tree_test PRIVATE -fsanitize=address ) + add_test( expression_tree_test ${EXECUTABLE_OUTPUT_PATH}/expression_tree_test ) + endif() \ No newline at end of file diff --git a/tests/expression_tree.cpp b/tests/expression_tree.cpp new file mode 100644 index 0000000..c6c32c3 --- /dev/null +++ b/tests/expression_tree.cpp @@ -0,0 +1,324 @@ +#include +#include "test_utils.hpp" +#include +#include + +using namespace attwoodn::expression_tree; + +void test_simple_expression_tree(); +void test_complex_expression_tree(); +void test_moved_expression_tree(); +void test_copied_expression_tree(); +void test_user_defined_operator(); + +int main(int argc, char** argv) { + test_simple_expression_tree(); + test_complex_expression_tree(); + test_moved_expression_tree(); + test_copied_expression_tree(); + test_user_defined_operator(); + + return EXIT_SUCCESS; +} + +void test_simple_expression_tree() { + auto test_procedure = [](const expression_tree& expr) { + test_fixture fixture; + fixture.some_string = "hello!"; + assert(!expr.evaluate(fixture)); + + fixture.some_string = "hello world!"; + assert(!expr.evaluate(fixture)); + + fixture.some_string = "hello,world!"; + assert(!expr.evaluate(fixture)); + + fixture.some_string = "hello, world!"; + assert(expr.evaluate(fixture)); + }; + + // test raw pointer creation of expression_tree + { + expression_tree expr { + make_expr(&test_fixture::some_string, op::equals, std::string("hello, world!")) + }; + + test_procedure(expr); + } + + // test unique_ptr creation of expression_tree + { + expression_tree expr { + std::unique_ptr>( + make_expr(&test_fixture::some_string, op::equals, std::string("hello, world!")) + ) + }; + + test_procedure(expr); + } +} + +void test_complex_expression_tree() { + + // expression: (uint > 0 AND (some_string > "a" AND some_string < "z")) OR (some_string > "0" AND some_string < "9") + expression_tree expr { + (make_expr(&test_fixture::is_some_uint_greater_than_zero, op::equals, true) + ->AND((make_expr(&test_fixture::some_string, op::greater_than, std::string("a")) + ->AND(make_expr(&test_fixture::some_string, op::less_than, std::string("z"))) + )) + ) + ->OR((make_expr(&test_fixture::some_string, op::greater_than, std::string("0")) + ->AND(make_expr(&test_fixture::some_string, op::less_than, std::string("9"))) + ) + ) + }; + + test_fixture fixture; + + fixture.some_uint = 0; + + // test left-hand side of the expression tree when some_uint is 0 + { + fixture.some_string = "a"; + assert(!expr.evaluate(fixture)); + + fixture.some_string = "aaa"; + assert(!expr.evaluate(fixture)); + + fixture.some_string = "c"; + assert(!expr.evaluate(fixture)); + + fixture.some_string = "yyyyy"; + assert(!expr.evaluate(fixture)); + + fixture.some_string = "B"; + assert(!expr.evaluate(fixture)); + + fixture.some_string = "ZZZ"; + assert(!expr.evaluate(fixture)); + + fixture.some_string = "/"; + assert(!expr.evaluate(fixture)); + + fixture.some_string = "red"; + assert(!expr.evaluate(fixture)); + + fixture.some_string = " "; + assert(!expr.evaluate(fixture)); + + fixture.some_string = "tree fiddy"; + assert(!expr.evaluate(fixture)); + } + + // test right-hand side of the expression tree when some_uint is 0 + { + fixture.some_string = "000"; + assert(expr.evaluate(fixture)); + + fixture.some_string = "00"; + assert(expr.evaluate(fixture)); + + fixture.some_string = "0"; + assert(!expr.evaluate(fixture)); + + fixture.some_string = "12345"; + assert(expr.evaluate(fixture)); + + fixture.some_string = "7"; + assert(expr.evaluate(fixture)); + + fixture.some_string = "89999"; + assert(expr.evaluate(fixture)); + + fixture.some_string = "9"; + assert(!expr.evaluate(fixture)); + } + + fixture.some_uint = 1; + + // test left-hand side of the expression tree when some_uint is 0 + { + fixture.some_string = "a"; + assert(!expr.evaluate(fixture)); + + fixture.some_string = "aaa"; + assert(expr.evaluate(fixture)); + + fixture.some_string = "c"; + assert(expr.evaluate(fixture)); + + fixture.some_string = "yyyyy"; + assert(expr.evaluate(fixture)); + + fixture.some_string = "B"; + assert(!expr.evaluate(fixture)); + + fixture.some_string = "ZZZ"; + assert(!expr.evaluate(fixture)); + + fixture.some_string = "/"; + assert(!expr.evaluate(fixture)); + + fixture.some_string = "red"; + assert(expr.evaluate(fixture)); + + fixture.some_string = " "; + assert(!expr.evaluate(fixture)); + + fixture.some_string = "tree fiddy"; + assert(expr.evaluate(fixture)); + } + + // test right-hand side of the expression tree when some_uint is 1 + { + fixture.some_string = "000"; + assert(expr.evaluate(fixture)); + + fixture.some_string = "00"; + assert(expr.evaluate(fixture)); + + fixture.some_string = "0"; + assert(!expr.evaluate(fixture)); + + fixture.some_string = "12345"; + assert(expr.evaluate(fixture)); + + fixture.some_string = "7"; + assert(expr.evaluate(fixture)); + + fixture.some_string = "89999"; + assert(expr.evaluate(fixture)); + + fixture.some_string = "9"; + assert(!expr.evaluate(fixture)); + } + + +} + +void test_moved_expression_tree() { + auto test_procedure = [](const expression_tree& expr) { + test_fixture fixture; + + fixture.some_uint = 345; + assert(expr.evaluate(fixture)); + + fixture.some_uint = 123; + assert(!expr.evaluate(fixture)); + + fixture.some_uint = 543; + assert(!expr.evaluate(fixture)); + + fixture.some_uint = 0x04; + assert(!expr.evaluate(fixture)); + + fixture.some_uint = -1; + assert(!expr.evaluate(fixture)); + + fixture.some_uint = 0; + assert(!expr.evaluate(fixture)); + + fixture.some_uint = 3; + assert(!expr.evaluate(fixture)); + }; + + expression_tree expr { + make_expr(&test_fixture::some_uint, op::equals, (uint16_t) 345) + }; + test_procedure(expr); + + expression_tree move_constructed_expr(std::move(expr)); + test_procedure(move_constructed_expr); + + expression_tree move_assigned_expr = std::move(move_constructed_expr); + test_procedure(move_assigned_expr); +} + +void test_copied_expression_tree() { + auto test_procedure = [](const expression_tree& expr) { + test_fixture fixture; + + fixture.some_uint = 345; + assert(expr.evaluate(fixture)); + + fixture.some_uint = 123; + assert(!expr.evaluate(fixture)); + + fixture.some_uint = 543; + assert(!expr.evaluate(fixture)); + + fixture.some_uint = 0x04; + assert(!expr.evaluate(fixture)); + + fixture.some_uint = -1; + assert(!expr.evaluate(fixture)); + + fixture.some_uint = 0; + assert(!expr.evaluate(fixture)); + + fixture.some_uint = 3; + assert(!expr.evaluate(fixture)); + }; + + expression_tree expr { + make_expr(&test_fixture::some_uint, op::equals, (uint16_t) 345) + }; + test_procedure(expr); + + expression_tree copy_constructed_expr(expr); + test_procedure(copy_constructed_expr); + + expression_tree copy_assigned_expr = copy_constructed_expr; + test_procedure(copy_assigned_expr); + + // ensure both of the old copied versions still work after copying + test_procedure(expr); + test_procedure(copy_constructed_expr); +} + +void test_user_defined_operator() { + auto is_small_packet_payload = [](const packet_payload& incoming, const packet_payload&) -> bool { + if(incoming.error_code == 0 && incoming.checksum_ok && incoming.payload_size() <= 10) { + return true; + } + return false; + }; + + // only accept small, non-errored data packets from Jim. + // evaluate packet contents using the user-defined lambda operator defined above + expression_tree expr { + make_expr(&data_packet::sender_name, op::equals, std::string("Jim")) + ->AND(make_expr(&data_packet::payload, is_small_packet_payload, packet_payload())) + }; + + data_packet incoming_packet; + + // Jim sends a small, non-errored data packet + incoming_packet.sender_name = "Jim"; + incoming_packet.payload.checksum_ok = true; + incoming_packet.payload.data = "hello!"; + incoming_packet.payload.error_code = 0; + assert(expr.evaluate(incoming_packet)); // passes evaluation + + // Pam sends the same packet payload + incoming_packet.sender_name = "Pam"; + assert(!expr.evaluate(incoming_packet)); // fails evaluation. No messages from Pam are accepted (sorry Pam) + + // Jim sends a packet with a bad checksum + incoming_packet.sender_name = "Jim"; + incoming_packet.payload.checksum_ok = false; + assert(!expr.evaluate(incoming_packet)); // fails evaluation. The packet was from Jim, but the checksum was bad + + // Jim sends a packet whose payload is too big + incoming_packet.payload.checksum_ok = true; + incoming_packet.payload.data = "Boy do I have a long story for you - so I was talking to Pam ..."; + assert(!expr.evaluate(incoming_packet)); // fails evaluation. The packet's payload was too big. Give me the TLDR next time, Jim + + // Jim sends a small, rude packet + incoming_packet.payload.data = "Dwight sux"; + assert(expr.evaluate(incoming_packet)); // passes evaluation. The packet's payload was the right size this time + + // Jim sends a packet has an error code + incoming_packet.payload.error_code = 404; + assert(!expr.evaluate(incoming_packet)); // fails evaluation. The packet's payload had an error code +} \ No newline at end of file diff --git a/tests/expression_tree_leaf_node.cpp b/tests/expression_tree_leaf_node.cpp index 147a1b7..8356937 100644 --- a/tests/expression_tree_leaf_node.cpp +++ b/tests/expression_tree_leaf_node.cpp @@ -1,7 +1,6 @@ #include #include "test_utils.hpp" #include -#include #include using namespace attwoodn::expression_tree; diff --git a/tests/expression_tree_op_node.cpp b/tests/expression_tree_op_node.cpp index 66323e5..3987fbb 100644 --- a/tests/expression_tree_op_node.cpp +++ b/tests/expression_tree_op_node.cpp @@ -1,8 +1,6 @@ #include #include "test_utils.hpp" #include -#include -#include #include using namespace attwoodn::expression_tree; @@ -18,10 +16,10 @@ int main(int argc, char** argv) { } void test_AND_op_node_evaluation() { - auto child_expr1 = make_expr(&test_fixture::some_string, op::equals, std::string("hello, world!")); - auto child_expr2 = make_expr(&test_fixture::some_uint, op::less_than, (uint16_t) 500); - - auto expr = make_op_node(child_expr1, boolean_op::AND, child_expr2); + auto expr = std::unique_ptr>( + make_expr(&test_fixture::some_string, op::equals, std::string("hello, world!")) + ->AND(make_expr(&test_fixture::some_uint, op::less_than, (uint16_t) 500)) + ); test_fixture fixture; fixture.some_string = "hello, world!"; @@ -69,10 +67,10 @@ void test_AND_op_node_evaluation() { } void test_OR_op_node_evaluation() { - auto child_expr1 = make_expr(&test_fixture::some_string, op::equals, std::string("hello, world!")); - auto child_expr2 = make_expr(&test_fixture::some_uint, op::less_than, (uint16_t) 500); - - auto expr = make_op_node(child_expr1, boolean_op::OR, child_expr2); + auto expr = std::unique_ptr>( + make_expr(&test_fixture::some_string, op::equals, std::string("hello, world!")) + ->OR(make_expr(&test_fixture::some_uint, op::less_than, (uint16_t) 500)) + ); test_fixture fixture; fixture.some_string = "hello, world!"; diff --git a/tests/test_utils.hpp b/tests/test_utils.hpp index 2cfe300..69ce030 100644 --- a/tests/test_utils.hpp +++ b/tests/test_utils.hpp @@ -17,13 +17,18 @@ struct test_fixture { } }; -/** - * Helper function for creating inner expression tree nodes. I'm undecided if this should be included in the public API. -*/ -template -std::unique_ptr> make_op_node(LeftChild* left, et::boolean_op op, RightChild* right) { - auto node = new et::node::expression_tree_op_node(op); - node->set_left(left); - node->set_right(right); - return std::unique_ptr>(node); -} \ No newline at end of file +struct packet_payload { + uint16_t error_code; + std::string data; + bool checksum_ok; + + uint64_t payload_size() const { + return data.size(); + } +}; + +class data_packet { + public: + std::string sender_name; + packet_payload payload; +}; \ No newline at end of file