Add unit tests suite using cmocka library
authorKirill Isakov <bootctl@gmail.com>
Wed, 23 Mar 2022 11:41:31 +0000 (17:41 +0600)
committerKirill Isakov <bootctl@gmail.com>
Wed, 23 Mar 2022 11:41:31 +0000 (17:41 +0600)
src/include/meson.build
src/meson.build
test/meson.build
test/unit/meson.build [new file with mode: 0644]
test/unit/test_net.c [new file with mode: 0644]
test/unit/test_splay_tree.c [new file with mode: 0644]
test/unit/test_subnet.c [new file with mode: 0644]
test/unit/unittest.h [new file with mode: 0644]

index 55da167..f6f4e04 100644 (file)
@@ -1,6 +1,6 @@
 configure_file(output: 'config.h', configuration: cdata)
 
-src_lib_tinc += vcs_tag(
+src_lib_common += vcs_tag(
   command: './git_tag.sh',
   fallback: 'unknown',
   input: '../version_git.h.in',
index c9f5d59..3eb4bc4 100644 (file)
@@ -79,7 +79,7 @@ check_types = [
 subdir('ed25519')
 subdir('chacha-poly1305')
 
-src_lib_tinc = [
+src_lib_common = [
   'conf.c',
   'dropin.c',
   'keys.c',
@@ -101,7 +101,6 @@ src_tinc = [
   'ifconfig.c',
   'info.c',
   'invitation.c',
-  'tincctl.c',
   'top.c',
 ]
 
@@ -135,7 +134,6 @@ src_tincd = [
   'raw_socket_device.c',
   'route.c',
   'subnet.c',
-  'tincd.c',
 ]
 
 cc_flags_tincd = cc_flags
@@ -198,7 +196,7 @@ foreach type : check_types
 endforeach
 
 if not cdata.has('HAVE_GETOPT_H') or not cc.has_function('getopt_long', prefix: have_prefix, args: cc_defs)
-  src_lib_tinc += ['getopt.c', 'getopt1.c']
+  src_lib_common += ['getopt.c', 'getopt1.c']
 endif
 
 if not opt_miniupnpc.disabled()
@@ -326,13 +324,36 @@ lib_crypto = static_library(
   build_by_default: false,
 )
 
-deps_lib_tinc = [deps_common, dep_crypto]
+deps_lib_common = [deps_common, dep_crypto]
+deps_tinc += deps_lib_common
+deps_tincd += deps_lib_common
+
+lib_common = static_library(
+  'common',
+  sources: src_lib_common,
+  dependencies: deps_lib_common,
+  link_with: [lib_ed25519, lib_chacha_poly, lib_crypto],
+  implicit_include_directories: false,
+  include_directories: inc_conf,
+  build_by_default: false,
+)
 
 lib_tinc = static_library(
   'tinc',
-  sources: src_lib_tinc,
-  dependencies: deps_lib_tinc,
-  link_with: [lib_ed25519, lib_chacha_poly, lib_crypto],
+  sources: src_tinc,
+  dependencies: deps_tinc,
+  link_with: lib_common,
+  implicit_include_directories: false,
+  include_directories: inc_conf,
+  build_by_default: false,
+)
+
+lib_tincd = static_library(
+  'tincd',
+  sources: src_tincd,
+  dependencies: deps_tincd,
+  link_with: lib_common,
+  c_args: cc_flags_tincd,
   implicit_include_directories: false,
   include_directories: inc_conf,
   build_by_default: false,
@@ -340,8 +361,8 @@ lib_tinc = static_library(
 
 exe_tinc = executable(
   'tinc',
-  sources: src_tinc,
-  dependencies: [deps_lib_tinc, deps_tinc],
+  sources: 'tincctl.c',
+  dependencies: deps_tinc,
   link_with: lib_tinc,
   implicit_include_directories: false,
   include_directories: inc_conf,
@@ -351,9 +372,9 @@ exe_tinc = executable(
 
 exe_tincd = executable(
   'tincd',
-  sources: src_tincd,
-  dependencies: [deps_lib_tinc, deps_tincd],
-  link_with: lib_tinc,
+  sources: 'tincd.c',
+  dependencies: deps_tincd,
+  link_with: lib_tincd,
   c_args: cc_flags_tincd,
   implicit_include_directories: false,
   include_directories: inc_conf,
@@ -364,8 +385,8 @@ exe_tincd = executable(
 exe_sptps_test = executable(
   'sptps_test',
   sources: 'sptps_test.c',
-  dependencies: deps_lib_tinc,
-  link_with: lib_tinc,
+  dependencies: deps_lib_common,
+  link_with: lib_common,
   implicit_include_directories: false,
   include_directories: inc_conf,
   build_by_default: false,
@@ -374,8 +395,8 @@ exe_sptps_test = executable(
 exe_sptps_keypair = executable(
   'sptps_keypair',
   sources: 'sptps_keypair.c',
-  dependencies: deps_lib_tinc,
-  link_with: lib_tinc,
+  dependencies: deps_lib_common,
+  link_with: lib_common,
   implicit_include_directories: false,
   include_directories: inc_conf,
   build_by_default: false,
@@ -387,8 +408,8 @@ if os_name == 'linux'
   exe_sptps_speed = executable(
     'sptps_speed',
     sources: 'sptps_speed.c',
-    dependencies: [deps_lib_tinc, dep_rt],
-    link_with: lib_tinc,
+    dependencies: [deps_lib_common, dep_rt],
+    link_with: lib_common,
     implicit_include_directories: false,
     include_directories: inc_conf,
     build_by_default: false,
index db719a2..48e6689 100644 (file)
@@ -1,2 +1,3 @@
 subdir('integration')
+subdir('unit')
 
diff --git a/test/unit/meson.build b/test/unit/meson.build
new file mode 100644 (file)
index 0000000..120d527
--- /dev/null
@@ -0,0 +1,66 @@
+dep_cmocka = dependency('cmocka', required: opt_tests)
+if not dep_cmocka.found()
+  subdir_done()
+endif
+
+can_wrap = cc.has_link_argument('-Wl,--wrap=func')
+if not can_wrap
+  message('linker has no support for function wrapping, mocked tests will not run')
+endif
+
+link_tinc = { 'lib': lib_tinc, 'dep': deps_tinc }
+link_tincd = { 'lib': lib_tincd, 'dep': deps_tincd }
+
+# Test definition format:
+#
+# 'free-form test name': {
+#   'code': 'test1.c',      // or ['test1.c', 'test1_util.c']
+#   'mock': ['foo', 'bar'], // list of functions to mock (default: empty)
+#   'link': link_tinc,      // which binary to link with (default: tincd)
+# }
+
+tests = {
+  'net': {
+    'code': 'test_net.c',
+    'mock': ['execute_script', 'environment_init', 'environment_exit'],
+  },
+  'subnet': {
+    'code': 'test_subnet.c',
+  },
+  'splay_tree': {
+    'code': 'test_splay_tree.c',
+    'link': link_tinc,
+  },
+}
+
+env = ['CMOCKA_MESSAGE_OUTPUT=TAP']
+
+foreach test, data : tests
+  args = ld_flags
+
+  if can_wrap
+    mocks = data.get('mock', [])
+    if mocks.length() > 0
+      args += ',--wrap='.join(['-Wl'] + mocks)
+    endif
+  endif
+
+  libs = data.get('link', link_tincd)
+
+  exe = executable(test,
+                   sources: data['code'],
+                   link_args: args,
+                   dependencies: [libs['dep'], dep_cmocka],
+                   link_with: libs['lib'],
+                   implicit_include_directories: false,
+                   include_directories: inc_conf,
+                   build_by_default: false)
+
+  test(test,
+       exe,
+       suite: 'unit',
+       timeout: 60,
+       protocol: 'tap',
+       env: env)
+endforeach
+
diff --git a/test/unit/test_net.c b/test/unit/test_net.c
new file mode 100644 (file)
index 0000000..5901fc7
--- /dev/null
@@ -0,0 +1,58 @@
+#include "unittest.h"
+#include "../../src/net.h"
+#include "../../src/script.h"
+
+static environment_t *device_env = NULL;
+
+// silence -Wmissing-prototypes
+void __wrap_environment_init(environment_t *env);
+void __wrap_environment_exit(environment_t *env);
+bool __wrap_execute_script(const char *name, environment_t *env);
+
+void __wrap_environment_init(environment_t *env) {
+       assert_non_null(env);
+       assert_null(device_env);
+       device_env = env;
+}
+
+void __wrap_environment_exit(environment_t *env) {
+       assert_ptr_equal(device_env, env);
+       device_env = NULL;
+}
+
+bool __wrap_execute_script(const char *name, environment_t *env) {
+       (void)env;
+
+       check_expected_ptr(name);
+
+       // Used instead of mock_type(bool) to silence clang warning
+       return mock() ? true : false;
+}
+
+static void run_device_enable_disable(void (*device_func)(void),
+                                      const char *script) {
+       expect_string(__wrap_execute_script, name, script);
+       will_return(__wrap_execute_script, true);
+
+       device_func();
+}
+
+static void test_device_enable_calls_tinc_up(void **state) {
+       (void)state;
+
+       run_device_enable_disable(&device_enable, "tinc-up");
+}
+
+static void test_device_disable_calls_tinc_down(void **state) {
+       (void)state;
+
+       run_device_enable_disable(&device_disable, "tinc-down");
+}
+
+int main(void) {
+       const struct CMUnitTest tests[] = {
+               cmocka_unit_test(test_device_enable_calls_tinc_up),
+               cmocka_unit_test(test_device_disable_calls_tinc_down),
+       };
+       return cmocka_run_group_tests(tests, NULL, NULL);
+}
diff --git a/test/unit/test_splay_tree.c b/test/unit/test_splay_tree.c
new file mode 100644 (file)
index 0000000..dfeaaba
--- /dev/null
@@ -0,0 +1,270 @@
+#include "unittest.h"
+#include "../../src/splay_tree.h"
+
+typedef struct node_t {
+       int id;
+} node_t;
+
+// We cannot use test_malloc / test_free here, because the library seems to be
+// checking for leaks right after running each test, before doing teardown,
+// which results in a bunch of spurious test failures. We rely on teardown to
+// clean up after us. Valgrind and ASAN show no leaks.
+static node_t *create_node(int id) {
+       node_t *node = malloc(sizeof(node_t));
+       node->id = id;
+       return node;
+}
+
+static void free_node(node_t *node) {
+       free(node);
+}
+
+static int node_compare(const node_t *lhs, const node_t *rhs) {
+       return lhs->id - rhs->id;
+}
+
+static int test_setup(void **state) {
+       splay_tree_t *tree = splay_alloc_tree((splay_compare_t) node_compare, (splay_action_t) free_node);
+
+       if(!tree) {
+               return -1;
+       }
+
+       *state = tree;
+       return 0;
+}
+
+static int test_teardown(void **state) {
+       splay_delete_tree(*state);
+       return 0;
+}
+
+static void test_tree_allocation_deletion(void **state) {
+       (void)state;
+
+       splay_tree_t *tree = splay_alloc_tree((splay_compare_t) node_compare,
+                                             (splay_action_t) free_node);
+       assert_non_null(tree);
+
+       node_t *one = create_node(1);
+       assert_non_null(splay_insert(tree, one));
+
+       node_t *two = create_node(2);
+       assert_non_null(splay_insert(tree, two));
+
+       // AddressSanitizer will notify us if there's a leak
+       splay_delete_tree(tree);
+}
+
+static int multiply_tree_node_calls = 0;
+
+static void increment_id_tree_node(node_t *node) {
+       ++node->id;
+       ++multiply_tree_node_calls;
+}
+
+static int multiply_splay_node_calls = 0;
+
+static void multiply_id_splay_node(splay_node_t *node) {
+       node_t *t = node->data;
+       t->id *= 2;
+       ++multiply_splay_node_calls;
+}
+
+static void test_splay_foreach(void **state) {
+       splay_tree_t *tree = *state;
+
+       node_t *one = create_node(1);
+       splay_node_t *node_one = splay_insert(tree, one);
+       assert_ptr_equal(one, node_one->data);
+
+       node_t *two = create_node(5);
+       splay_node_t *node_two = splay_insert(tree, two);
+       assert_ptr_equal(two, node_two->data);
+
+       splay_foreach(tree, (splay_action_t) increment_id_tree_node);
+       assert_int_equal(2, one->id);
+       assert_int_equal(6, two->id);
+
+       splay_foreach_node(tree, (splay_action_t) multiply_id_splay_node);
+       assert_int_equal(4, one->id);
+       assert_int_equal(12, two->id);
+
+       assert_int_equal(2, multiply_tree_node_calls);
+       assert_int_equal(2, multiply_splay_node_calls);
+}
+
+static void test_splay_each(void **state) {
+       splay_tree_t *tree = *state;
+
+       node_t *one = create_node(1);
+       node_t *two = create_node(2);
+
+       splay_insert(tree, one);
+       splay_insert(tree, two);
+
+       // splay_each should iterate over all nodes
+       for splay_each(node_t, n, tree) {
+               n->id = -n->id;
+       }
+
+       assert_int_equal(-1, one->id);
+       assert_int_equal(-2, two->id);
+
+       // splay_each should allow removal of the current node
+       for splay_each(node_t, n, tree) {
+               splay_delete(tree, n);
+       }
+}
+
+static void test_splay_basic_ops(void **state) {
+       splay_tree_t *tree = *state;
+       node_t *node = create_node(1);
+
+       // Should not find anything if the tree is empty
+       node_t *found_one = splay_search(tree, node);
+       assert_null(found_one);
+
+       // Insertion should return a non-NULL node with `data` pointing to our `tree_node`
+       splay_node_t *node_one = splay_insert(tree, node);
+       assert_ptr_equal(node, node_one->data);
+
+       // Should find after insertion
+       found_one = splay_search(tree, node);
+       assert_ptr_equal(node, found_one);
+}
+
+static void test_splay_insert_before_after(void **state) {
+       splay_tree_t *tree = *state;
+
+       node_t *one = create_node(1);
+       splay_node_t *node_one = splay_insert(tree, one);
+       assert_non_null(node_one);
+
+       // splay_insert_before should set up `prev` and `next` pointers
+       splay_node_t *node_two = splay_alloc_node();
+       assert_non_null(node_two);
+       node_two->data = create_node(2);
+
+       splay_insert_after(tree, node_one, node_two);
+       assert_null(node_one->prev);
+       assert_ptr_equal(node_one->next, node_two);
+       assert_ptr_equal(node_two->prev, node_one);
+       assert_null(node_two->next);
+
+       splay_node_t *node_thr = splay_alloc_node();
+       assert_non_null(node_thr);
+       node_thr->data = create_node(3);
+
+       splay_insert_after(tree, node_two, node_thr);
+       assert_null(node_one->prev);
+       assert_ptr_equal(node_one->next, node_two);
+       assert_ptr_equal(node_two->prev, node_one);
+       assert_ptr_equal(node_two->next, node_thr);
+       assert_ptr_equal(node_thr->prev, node_two);
+       assert_null(node_thr->next);
+}
+
+static void test_search_node(void **state) {
+       splay_tree_t *tree = *state;
+
+       node_t *one = create_node(1);
+       node_t *two = create_node(2);
+
+       splay_node_t *one_node = splay_search_node(tree, one);
+       assert_null(one_node);
+
+       one_node = splay_insert(tree, one);
+       assert_ptr_equal(one_node, splay_search_node(tree, one));
+
+       splay_node_t *two_node = splay_search_node(tree, two);
+       assert_null(two_node);
+
+       two_node = splay_insert(tree, two);
+       assert_ptr_equal(one_node, splay_search_node(tree, one));
+       assert_ptr_equal(two_node, splay_search_node(tree, two));
+
+       node_t *copy_one = create_node(1);
+       node_t *copy_two = create_node(2);
+
+       splay_delete(tree, one);
+       assert_null(splay_search_node(tree, copy_one));
+       assert_ptr_equal(two_node, splay_search_node(tree, two));
+
+       splay_delete(tree, two);
+       assert_null(splay_search_node(tree, copy_one));
+       assert_null(splay_search_node(tree, copy_two));
+
+       free_node(copy_one);
+       free_node(copy_two);
+}
+
+static void test_unlink(void **state) {
+       splay_tree_t *tree = *state;
+       node_t *one = create_node(1);
+
+       splay_node_t *node_one = splay_insert(tree, one);
+
+       // Unlink should return the unlinked node
+       splay_node_t *unlinked_one = splay_unlink(tree, one);
+       assert_ptr_equal(one, unlinked_one->data);
+
+       // Unlinking the same node should return NULL
+       assert_null(splay_unlink(tree, one));
+
+       // Inserting it back should return the same node
+       unlinked_one = splay_insert_node(tree, unlinked_one);
+       assert_ptr_equal(node_one, unlinked_one);
+}
+
+static void test_unlink_node(void **state) {
+       splay_tree_t *tree = *state;
+       node_t *one = create_node(1);
+
+       splay_node_t *node_one = splay_insert(tree, one);
+       assert_ptr_equal(one, node_one->data);
+       assert_ptr_equal(one, splay_search(tree, one));
+       assert_ptr_equal(node_one, splay_search_node(tree, one));
+
+       splay_unlink_node(tree, node_one);
+       assert_null(splay_search(tree, one));
+       assert_null(splay_search_node(tree, one));
+
+       splay_free_node(tree, node_one);
+}
+
+static void test_delete_node(void **state) {
+       splay_tree_t *tree = *state;
+       node_t *one = create_node(1);
+
+       splay_node_t *node_one = splay_insert(tree, one);
+       assert_ptr_equal(one, node_one->data);
+       assert_ptr_equal(one, splay_search(tree, one));
+       assert_ptr_equal(node_one, splay_search_node(tree, one));
+
+       node_t *copy = create_node(1);
+       assert_ptr_equal(one, splay_search(tree, copy));
+
+       splay_delete_node(tree, node_one);
+       assert_null(splay_search(tree, copy));
+
+       free_node(copy);
+}
+
+#define test_with_state(test_func) \
+       cmocka_unit_test_setup_teardown((test_func), test_setup, test_teardown)
+
+int main(void) {
+       const struct CMUnitTest tests[] = {
+               cmocka_unit_test(test_tree_allocation_deletion),
+               test_with_state(test_splay_basic_ops),
+               test_with_state(test_splay_insert_before_after),
+               test_with_state(test_splay_foreach),
+               test_with_state(test_splay_each),
+               test_with_state(test_search_node),
+               test_with_state(test_unlink),
+               test_with_state(test_unlink_node),
+               test_with_state(test_delete_node),
+       };
+       return cmocka_run_group_tests(tests, NULL, NULL);
+}
diff --git a/test/unit/test_subnet.c b/test/unit/test_subnet.c
new file mode 100644 (file)
index 0000000..a362164
--- /dev/null
@@ -0,0 +1,551 @@
+#include "unittest.h"
+#include "../../src/subnet.h"
+
+typedef struct net_str_testcase {
+       const char *text;
+       subnet_t data;
+} net_str_testcase;
+
+static void test_subnet_compare_different_types(void **state) {
+       (void)state;
+
+       const subnet_t ipv4 = {.type = SUBNET_IPV4};
+       const subnet_t ipv6 = {.type = SUBNET_IPV6};
+       const subnet_t mac = {.type = SUBNET_MAC};
+
+       assert_int_not_equal(0, subnet_compare(&ipv4, &ipv6));
+       assert_int_not_equal(0, subnet_compare(&ipv4, &mac));
+       assert_int_not_equal(0, subnet_compare(&ipv6, &mac));
+}
+
+static const mac_t mac1 = {{0x00, 0x01, 0x02, 0x03, 0x04, 0x05}};
+static const mac_t mac2 = {{0x42, 0x01, 0x02, 0x03, 0x04, 0x05}};
+
+static const subnet_ipv4_t ipv4_1 = {.address = {{0x01, 0x02, 0x03, 0x04}}, .prefixlength = 24};
+static const subnet_ipv4_t ipv4_1_pref = {.address = {{0x01, 0x02, 0x03, 0x04}}, .prefixlength = 16};
+static const subnet_ipv4_t ipv4_2 = {.address = {{0x11, 0x22, 0x33, 0x44}}, .prefixlength = 16};
+
+static const subnet_ipv6_t ipv6_1 = {
+       .address = {{0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04}},
+       .prefixlength = 24
+};
+
+static const subnet_ipv6_t ipv6_1_pref = {
+       .address = {{0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04}},
+       .prefixlength = 16
+};
+
+static const subnet_ipv6_t ipv6_2 = {
+       .address = {{0x11, 0x22, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04}},
+       .prefixlength = 24
+};
+
+static void test_maskcmp(void **state) {
+       (void)state;
+
+       const ipv4_t a = {{1, 2, 3, 4}};
+       const ipv4_t b = {{1, 2, 3, 0xff}};
+
+       for(int mask = 0; mask <= 24; ++mask) {
+               assert_int_equal(0, maskcmp(&a, &b, mask));
+       }
+
+       for(int mask = 25; mask <= 32; ++mask) {
+               assert_true(maskcmp(&a, &b, mask) != 0);
+       }
+}
+
+static void test_mask(void **state) {
+       (void)state;
+
+       ipv4_t dst = {{0xff, 0xff, 0xff, 0xff}};
+       mask(&dst, 23, sizeof(dst));
+
+       const ipv4_t ref = {{0xff, 0xff, 0xfe, 0x00}};
+       assert_memory_equal(&ref, &dst, sizeof(dst));
+}
+
+static void test_maskcpy(void **state) {
+       (void)state;
+
+       const ipv4_t src = {{0xff, 0xff, 0xff, 0xff}};
+       const ipv4_t ref = {{0xff, 0xff, 0xfe, 0x00}};
+       ipv4_t dst;
+
+       maskcpy(&dst, &src, 23, sizeof(src));
+
+       assert_memory_equal(&ref, &dst, sizeof(dst));
+}
+
+static void test_subnet_compare_mac_eq(void **state) {
+       (void)state;
+
+       node_t owner = {.name = strdup("foobar")};
+       const subnet_t a = {.type = SUBNET_MAC, .net.mac.address = mac1, .weight = 42, .owner = &owner};
+       const subnet_t b = {.type = SUBNET_MAC, .net.mac.address = mac1, .weight = 42, .owner = &owner};
+
+       assert_int_equal(0, subnet_compare(&a, &a));
+       assert_int_equal(0, subnet_compare(&a, &b));
+       assert_int_equal(0, subnet_compare(&b, &a));
+
+       free(owner.name);
+}
+
+static void test_subnet_compare_mac_neq_address(void **state) {
+       (void)state;
+
+       node_t owner = {.name = strdup("foobar")};
+       const subnet_t a = {.type = SUBNET_MAC, .net.mac.address = mac1, .weight = 10, .owner = &owner};
+       const subnet_t b = {.type = SUBNET_MAC, .net.mac.address = mac2, .weight = 10, .owner = &owner};
+
+       assert_true(subnet_compare(&a, &b) < 0);
+       assert_true(subnet_compare(&b, &a) > 0);
+
+       free(owner.name);
+}
+
+static void test_subnet_compare_mac_weight(void **state) {
+       (void)state;
+
+       node_t owner = {.name = strdup("foobar")};
+       const subnet_t a = {.type = SUBNET_MAC, .net.mac.address = mac1, .weight = 42, .owner = &owner};
+       const subnet_t b = {.type = SUBNET_MAC, .net.mac.address = mac1, .weight = 42, .owner = &owner};
+       const subnet_t c = {.type = SUBNET_MAC, .net.mac.address = mac1, .weight = 10, .owner = &owner};
+
+       assert_int_equal(0, subnet_compare(&a, &a));
+       assert_int_equal(0, subnet_compare(&a, &b));
+       assert_int_equal(0, subnet_compare(&b, &a));
+
+       assert_true(subnet_compare(&a, &c) > 0);
+       assert_true(subnet_compare(&c, &a) < 0);
+
+       free(owner.name);
+}
+
+static void test_subnet_compare_mac_owners(void **state) {
+       (void)state;
+
+       node_t foo = {.name = strdup("foo")};
+       node_t bar = {.name = strdup("bar")};
+
+       const subnet_t a = {.type = SUBNET_MAC, .net.mac.address = mac1, .weight = 42, .owner = &foo};
+       const subnet_t b = {.type = SUBNET_MAC, .net.mac.address = mac1, .weight = 42, .owner = &bar};
+
+       assert_int_equal(0, subnet_compare(&a, &a));
+       assert_int_equal(0, subnet_compare(&b, &b));
+
+       assert_true(subnet_compare(&a, &b) > 0);
+       assert_true(subnet_compare(&b, &a) < 0);
+
+       free(foo.name);
+       free(bar.name);
+}
+
+
+static void test_subnet_compare_ipv4_eq(void **state) {
+       (void)state;
+
+       const subnet_t a = {.type = SUBNET_IPV4, .net.ipv4 = ipv4_1};
+       const subnet_t b = {.type = SUBNET_IPV4, .net.ipv4 = ipv4_1};
+
+       assert_int_equal(0, subnet_compare(&a, &b));
+       assert_int_equal(0, subnet_compare(&b, &a));
+}
+
+static void test_subnet_compare_ipv4_neq(void **state) {
+       (void)state;
+
+       const subnet_t a = {.type = SUBNET_IPV4, .net.ipv4 = ipv4_1};
+       const subnet_t b = {.type = SUBNET_IPV4, .net.ipv4 = ipv4_1_pref};
+       const subnet_t c = {.type = SUBNET_IPV4, .net.ipv4 = ipv4_2};
+
+       assert_true(subnet_compare(&a, &b) < 0);
+       assert_true(subnet_compare(&b, &a) > 0);
+
+       assert_true(subnet_compare(&a, &c) < 0);
+       assert_true(subnet_compare(&b, &c) < 0);
+}
+
+static void test_subnet_compare_ipv4_weight(void **state) {
+       (void)state;
+
+       const subnet_t a = {.type = SUBNET_IPV4, .net.ipv4 = ipv4_1, .weight = 1};
+       const subnet_t b = {.type = SUBNET_IPV4, .net.ipv4 = ipv4_1, .weight = 2};
+
+       assert_true(subnet_compare(&a, &b) < 0);
+}
+
+static void test_subnet_compare_ipv4_owners(void **state) {
+       (void)state;
+
+       node_t foo = {.name = strdup("foo")};
+       node_t bar = {.name = strdup("bar")};
+
+       const subnet_t a = {.type = SUBNET_IPV4, .net.ipv4 = ipv4_1, .owner = &foo};
+       const subnet_t b = {.type = SUBNET_IPV4, .net.ipv4 = ipv4_1, .owner = &foo};
+       const subnet_t c = {.type = SUBNET_IPV4, .net.ipv4 = ipv4_1, .owner = &bar};
+
+       assert_int_equal(0, subnet_compare(&a, &b));
+       assert_true(subnet_compare(&a, &c) > 0);
+
+       free(foo.name);
+       free(bar.name);
+}
+
+static void test_subnet_compare_ipv6_eq(void **state) {
+       (void)state;
+
+       const subnet_t a = {.type = SUBNET_IPV6, .net.ipv6 = ipv6_1};
+       const subnet_t b = {.type = SUBNET_IPV6, .net.ipv6 = ipv6_1};
+
+       assert_int_equal(0, subnet_compare(&a, &b));
+       assert_int_equal(0, subnet_compare(&b, &a));
+}
+
+static void test_subnet_compare_ipv6_neq(void **state) {
+       (void)state;
+
+       const subnet_t a = {.type = SUBNET_IPV6, .net.ipv6 = ipv6_1};
+       const subnet_t b = {.type = SUBNET_IPV6, .net.ipv6 = ipv6_1_pref};
+       const subnet_t c = {.type = SUBNET_IPV6, .net.ipv6 = ipv6_2};
+
+       assert_true(subnet_compare(&a, &b) < 0);
+       assert_true(subnet_compare(&b, &a) > 0);
+
+       assert_true(subnet_compare(&a, &c) < 0);
+       assert_true(subnet_compare(&b, &c) > 0);
+}
+
+static void test_subnet_compare_ipv6_weight(void **state) {
+       (void)state;
+
+       const subnet_t a = {.type = SUBNET_IPV6, .net.ipv6 = ipv6_1, .weight = 1};
+       const subnet_t b = {.type = SUBNET_IPV6, .net.ipv6 = ipv6_1, .weight = 2};
+
+       assert_true(subnet_compare(&a, &b) < 0);
+}
+
+static void test_subnet_compare_ipv6_owners(void **state) {
+       (void)state;
+
+       node_t foo = {.name = strdup("foo")};
+       node_t bar = {.name = strdup("bar")};
+
+       const subnet_t a = {.type = SUBNET_IPV6, .net.ipv6 = ipv6_1, .owner = &foo};
+       const subnet_t b = {.type = SUBNET_IPV6, .net.ipv6 = ipv6_1, .owner = &foo};
+       const subnet_t c = {.type = SUBNET_IPV6, .net.ipv6 = ipv6_1, .owner = &bar};
+
+       assert_int_equal(0, subnet_compare(&a, &b));
+       assert_true(subnet_compare(&a, &c) > 0);
+
+       free(foo.name);
+       free(bar.name);
+}
+
+static void test_str2net_valid(void **state) {
+       (void)state;
+
+       const net_str_testcase testcases[] = {
+               {
+                       .text = "1.2.3.0/24#42",
+                       .data = {
+                               .type = SUBNET_IPV4,
+                               .weight = 42,
+                               .net = {
+                                       .ipv4 = {
+                                               .address = {
+                                                       .x = {1, 2, 3, 0}
+                                               },
+                                               .prefixlength = 24,
+                                       },
+                               },
+                       },
+               },
+               {
+                       .text = "04fb:7deb:78db:1950:2d21:258d:40b6:f0d7/128#999",
+                       .data = {
+                               .type = SUBNET_IPV6,
+                               .weight = 999,
+                               .net = {
+                                       .ipv6 = {
+                                               .address = {
+                                                       .x = {
+                                                               htons(0x04fb), htons(0x7deb), htons(0x78db), htons(0x1950),
+                                                               htons(0x2d21), htons(0x258d), htons(0x40b6), htons(0xf0d7),
+                                                       }
+                                               },
+                                               .prefixlength = 128,
+                                       },
+                               },
+                       },
+               },
+               {
+                       .text = "fe80::16dd:a9ff:fe7e:b4c2/64",
+                       .data = {
+                               .type = SUBNET_IPV6,
+                               .weight = 10,
+                               .net = {
+                                       .ipv6 = {
+                                               .address = {
+                                                       .x = {
+                                                               htons(0xfe80), htons(0x0000), htons(0x0000), htons(0x0000),
+                                                               htons(0x16dd), htons(0xa9ff), htons(0xfe7e), htons(0xb4c2),
+                                                       }
+                                               },
+                                               .prefixlength = 64,
+                                       },
+                               },
+                       },
+               },
+               {
+                       .text = "57:04:13:01:f9:26#60",
+                       .data = {
+                               .type = SUBNET_MAC,
+                               .weight = 60,
+                               .net = {
+                                       .mac = {
+                                               .address = {
+                                                       .x = {0x57, 0x04, 0x13, 0x01, 0xf9, 0x26},
+                                               },
+                                       },
+                               },
+                       },
+               },
+       };
+
+       for(size_t i = 0; i < sizeof(testcases) / sizeof(*testcases); ++i) {
+               const char *text = testcases[i].text;
+               const subnet_t *ref = &testcases[i].data;
+
+               subnet_t sub = {0};
+               bool ok = str2net(&sub, text);
+
+               // Split into separate assertions for more clear failures
+               assert_true(ok);
+               assert_int_equal(ref->type, sub.type);
+               assert_int_equal(ref->weight, sub.weight);
+
+               switch(ref->type) {
+               case SUBNET_MAC:
+                       assert_memory_equal(&ref->net.mac.address, &sub.net.mac.address, sizeof(mac_t));
+                       break;
+
+               case SUBNET_IPV4:
+                       assert_int_equal(ref->net.ipv4.prefixlength, sub.net.ipv4.prefixlength);
+                       assert_memory_equal(&ref->net.ipv4.address, &sub.net.ipv4.address, sizeof(ipv4_t));
+                       break;
+
+               case SUBNET_IPV6:
+                       assert_int_equal(ref->net.ipv6.prefixlength, sub.net.ipv6.prefixlength);
+                       assert_memory_equal(&ref->net.ipv6.address, &sub.net.ipv6.address, sizeof(ipv6_t));
+                       break;
+
+               default:
+                       fail_msg("unknown subnet type %d", ref->type);
+               }
+       }
+}
+
+static void test_str2net_invalid(void **state) {
+       (void)state;
+
+       subnet_t sub = {0};
+
+       const char *test_cases[] = {
+               // Overflow
+               "1.2.256.0",
+
+               // Invalid mask
+               "1.2.3.0/",
+               "1.2.3.0/42",
+               "1.2.3.0/MASK",
+               "fe80::/129",
+               "fe80::/MASK",
+               "cb:0c:1b:60:ed:7a/1",
+
+               // Invalid weight
+               "1.2.3.4#WEIGHT",
+               "1.2.0.0/16#WEIGHT",
+               "1.2.0.0/16#",
+               "feff::/16#",
+               "feff::/16#w",
+
+               NULL,
+       };
+
+       for(const char **str = test_cases; *str; ++str) {
+               bool ok = str2net(&sub, *str);
+               assert_false(ok);
+       }
+}
+
+static void test_net2str_valid(void **state) {
+       (void)state;
+
+       const net_str_testcase testcases[] = {
+               {
+                       .text = "12:fe:ff:3a:28:90#42",
+                       .data = {
+                               .type = SUBNET_MAC,
+                               .weight = 42,
+                               .net = {
+                                       .mac = {
+                                               .address = {
+                                                       .x = {0x12, 0xfe, 0xff, 0x3a, 0x28, 0x90}
+                                               },
+                                       },
+                               },
+                       },
+               },
+               {
+                       .text = "1.2.3.4",
+                       .data = {
+                               .type = SUBNET_IPV4,
+                               .weight = 10,
+                               .net = {
+                                       .ipv4 = {
+                                               .address = {
+                                                       .x = {1, 2, 3, 4}
+                                               },
+                                               .prefixlength = 32,
+                                       },
+                               },
+                       },
+               },
+               {
+                       .text = "181.35.16.0/27#1",
+                       .data = {
+                               .type = SUBNET_IPV4,
+                               .weight = 1,
+                               .net = {
+                                       .ipv4 = {
+                                               .address = {
+                                                       .x = {181, 35, 16, 0}
+                                               },
+                                               .prefixlength = 27,
+                                       },
+                               },
+                       },
+               },
+               {
+                       .text = "5fbf:5cfe:0:fdd2:fd76::/96#900",
+                       .data = {
+                               .type = SUBNET_IPV6,
+                               .weight = 900,
+                               .net = {
+                                       .ipv6 = {
+                                               .address = {
+                                                       .x = {
+                                                               htons(0x5fbf), htons(0x5cfe), htons(0x0000), htons(0xfdd2),
+                                                               htons(0xfd76), htons(0x0000), htons(0x0000), htons(0x0000),
+                                                       },
+                                               },
+                                               .prefixlength = 96,
+                                       },
+                               },
+                       },
+               },
+       };
+
+       for(size_t i = 0; i < sizeof(testcases) / sizeof(*testcases); ++i) {
+               const char *text = testcases[i].text;
+               const subnet_t *ref = &testcases[i].data;
+
+               char buf[256];
+               bool ok = net2str(buf, sizeof(buf), ref);
+
+               assert_true(ok);
+               assert_string_equal(text, buf);
+       }
+}
+
+static void test_net2str_invalid(void **state) {
+       (void)state;
+
+       const subnet_t sub = {0};
+       char buf[256];
+       assert_false(net2str(NULL, sizeof(buf), &sub));
+       assert_false(net2str(buf, sizeof(buf), NULL));
+}
+
+static void test_maskcheck_valid_ipv4(void **state) {
+       (void)state;
+
+       const ipv4_t a = {{10, 0, 0, 0}};
+       const ipv4_t b = {{192, 168, 0, 0}};
+       const ipv4_t c = {{192, 168, 24, 0}};
+
+       assert_true(maskcheck(&a, 8, sizeof(a)));
+       assert_true(maskcheck(&b, 16, sizeof(b)));
+       assert_true(maskcheck(&c, 24, sizeof(c)));
+}
+
+static void test_maskcheck_valid_ipv6(void **state) {
+       (void)state;
+
+       const ipv6_t a = {{10, 0, 0, 0, 0, 0, 0, 0}};
+       assert_true(maskcheck(&a, 8, sizeof(a)));
+
+       const ipv6_t b = {{10, 20, 0, 0, 0, 0, 0, 0}};
+       assert_true(maskcheck(&b, 32, sizeof(b)));
+
+       const ipv6_t c = {{192, 168, 24, 0, 0, 0, 0, 0}};
+       assert_true(maskcheck(&c, 48, sizeof(c)));
+}
+
+static void test_maskcheck_invalid_ipv4(void **state) {
+       (void)state;
+
+       const ipv4_t a = {{10, 20, 0, 0}};
+       const ipv4_t b = {{10, 20, 30, 0}};
+
+       assert_false(maskcheck(&a, 8, sizeof(a)));
+       assert_false(maskcheck(&b, 16, sizeof(b)));
+}
+
+static void test_maskcheck_invalid_ipv6(void **state) {
+       (void)state;
+
+       const ipv6_t a = {{1, 2, 3, 4, 5, 6, 7, 0xAABB}};
+
+       for(int mask = 0; mask < 128; mask += 8) {
+               assert_false(maskcheck(&a, mask, sizeof(a)));
+       }
+}
+
+int main(void) {
+       const struct CMUnitTest tests[] = {
+               cmocka_unit_test(test_maskcmp),
+               cmocka_unit_test(test_mask),
+               cmocka_unit_test(test_maskcpy),
+
+               cmocka_unit_test(test_subnet_compare_different_types),
+
+               cmocka_unit_test(test_subnet_compare_mac_eq),
+               cmocka_unit_test(test_subnet_compare_mac_neq_address),
+               cmocka_unit_test(test_subnet_compare_mac_weight),
+               cmocka_unit_test(test_subnet_compare_mac_owners),
+
+               cmocka_unit_test(test_subnet_compare_ipv4_eq),
+               cmocka_unit_test(test_subnet_compare_ipv4_neq),
+               cmocka_unit_test(test_subnet_compare_ipv4_weight),
+               cmocka_unit_test(test_subnet_compare_ipv4_owners),
+
+               cmocka_unit_test(test_subnet_compare_ipv6_eq),
+               cmocka_unit_test(test_subnet_compare_ipv6_neq),
+               cmocka_unit_test(test_subnet_compare_ipv6_weight),
+               cmocka_unit_test(test_subnet_compare_ipv6_owners),
+
+               cmocka_unit_test(test_str2net_valid),
+               cmocka_unit_test(test_str2net_invalid),
+
+               cmocka_unit_test(test_net2str_valid),
+               cmocka_unit_test(test_net2str_invalid),
+
+               cmocka_unit_test(test_maskcheck_valid_ipv4),
+               cmocka_unit_test(test_maskcheck_valid_ipv6),
+               cmocka_unit_test(test_maskcheck_invalid_ipv4),
+               cmocka_unit_test(test_maskcheck_invalid_ipv6),
+       };
+       return cmocka_run_group_tests(tests, NULL, NULL);
+}
diff --git a/test/unit/unittest.h b/test/unit/unittest.h
new file mode 100644 (file)
index 0000000..e96cb04
--- /dev/null
@@ -0,0 +1,11 @@
+#ifndef TINC_UNITTEST_H
+#define TINC_UNITTEST_H
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <stdlib.h>
+#include <cmocka.h>
+#include "../../src/system.h"
+
+#endif // TINC_UNITTEST_H