diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 74833f5..e620e4c 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -9,7 +9,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.3 - name: Run clang-format-lint uses: DoozyX/clang-format-lint-action@v0.11 with: @@ -34,6 +34,6 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.3 - name: Check build of Dockerfile is successful run: docker build -t sg-bridge:check-build -f build/Dockerfile . diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..2a82c84 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,20 @@ +name: Unit tests suite +env: + TEST_IMAGE: quay.io/centos/centos:stream9 + PROJECT_ROOT: /src/github.com/infrawatch/sg-bridge + OPSTOOLS_REPO: https://git.centos.org/rpms/centos-release-opstools/raw/c9s-sig-opstools/f/SOURCES/CentOS-OpsTools.repo + +on: [push, pull_request] + +jobs: + unit-tests: + name: Unit tests + runs-on: ubuntu-20.04 + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Run sg-core unit test suite + run: | + docker run --name=testsuite -uroot --network host -e OPSTOOLS_REPO \ + --volume ${{ github.workspace }}:$PROJECT_ROOT:z --workdir $PROJECT_ROOT \ + $TEST_IMAGE bash $PROJECT_ROOT/ci/run_tests.sh \ No newline at end of file diff --git a/Makefile b/Makefile index 9ea424f..18b8448 100644 --- a/Makefile +++ b/Makefile @@ -1,24 +1,32 @@ - MAJOR?=0 MINOR?=1 VERSION=$(MAJOR).$(MINOR) BIN := bridge +TEST_EXEC = tests -SRCS = $(wildcard *.c) +# Specify the source files, excluding the test and the main source file +MAIN_SRC = bridge.c +SRCS = $(filter-out tests.c $(MAIN_SRC), $(wildcard *.c)) +TEST_SRC = tests.c OBJDIR := obj +TEST_OBJDIR := test_obj DEPDIR := $(OBJDIR)/.deps # object files, auto generated from source files OBJS := $(patsubst %,$(OBJDIR)/%.o,$(basename $(SRCS))) +TEST_OBJS := $(patsubst %,$(TEST_OBJDIR)/%.o,$(basename $(SRCS))) +TEST_OBJS += $(TEST_OBJDIR)/$(basename $(TEST_SRC)).o + # dependency files, auto generated from source files DEPS := $(patsubst %,$(DEPDIR)/%.d,$(basename $(SRCS))) # compilers (at least gcc and clang) don't create the subdirectories automatically $(shell mkdir -p $(dir $(OBJS)) >/dev/null) +$(shell mkdir -p $(dir $(TEST_OBJS)) >/dev/null) $(shell mkdir -p $(dir $(DEPS)) >/dev/null) CC=gcc @@ -48,7 +56,7 @@ debug: all .PHONY: clean clean: - rm -fr $(OBJDIR) $(DEPDIR) + rm -fr $(OBJDIR) $(TEST_OBJDIR) $(DEPDIR) .PHONY: clean-image clean-image: version-check @@ -61,21 +69,34 @@ image: version-check @buildah bud -t ${HUB_NAMESPACE}/${BRIDGE_IMAGE_NAME}:latest -f build/Dockerfile . @echo 'Done.' -$(BIN): $(OBJS) +$(BIN): $(OBJS) $(OBJDIR)/$(basename $(MAIN_SRC)).o $(CC) -o $@ $^ $(LDFLAGS) $(CFLAGS) $(LDLIBS) -$(OBJDIR)/%.o: %.c -$(OBJDIR)/%.o: %.c $(DEPDIR)/%.d +$(OBJDIR)/%.o: %.c %.h +$(OBJDIR)/%.o: %.c $(DEPDIR)/%.d %.h $(PRECOMPILE) $(COMPILE.c) $< $(POSTCOMPILE) -$(OBJDIR)/%.o : %.c $(DEPDIR)/%.d | $(DEPDIR) +$(OBJDIR)/%.o : %.c $(DEPDIR)/%.d %.h | $(DEPDIR) $(COMPILE.c) $(OUTPUT_OPTION) $< .PRECIOUS: $(DEPDIR)/%.d $(DEPDIR)/%.d: ; +# Build unit test executable +$(TEST_EXEC): $(TEST_OBJS) + $(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS) + +# Compile unit test file +$(TEST_OBJDIR)/%.o: %.c + @mkdir -p $(TEST_OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +# Run unit tests +test: $(TEST_EXEC) + @$(TEST_EXEC) + ################################# # Utilities ################################# @@ -83,11 +104,11 @@ $(DEPDIR)/%.d: ; .PHONY: version-check version-check: @echo "+ $@" - ifdef VERSION + ifdef VERSION @echo "VERSION is ${VERSION}" - else + else @echo "VERSION is not set!" @false; - endif + endif -include $(DEPS) diff --git a/bridge.c b/bridge.c index bb81263..80e9442 100644 --- a/bridge.c +++ b/bridge.c @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -136,44 +135,6 @@ static void usage(char *program) { } } -static int match_regex(char *regmatch, char *matches[], int n_matches, - const char *to_match) { - /* "M" contains the matches found. */ - regmatch_t m[n_matches]; - regex_t regex; - - if (regcomp(®ex, regmatch, REG_EXTENDED)) { - fprintf(stderr, "Could not compile regex: %s\n", regmatch); - - return -1; - } - - int nomatch = regexec(®ex, to_match, n_matches, m, 0); - if (nomatch == REG_NOMATCH) { - return 0; - } - - int match_count = 0; - for (int i = 0; i < n_matches; i++) { - if (m[i].rm_so == -1) { - continue; - } - match_count++; - - int match_len = m[i].rm_eo - m[i].rm_so; - - matches[i] = malloc(match_len + 1); // make room for '\0' - - int k = 0; - for (int j = m[i].rm_so; j < m[i].rm_eo; j++) { - matches[i][k++] = to_match[j]; - } - matches[i][k] = '\0'; - } - - return match_count; -} - int main(int argc, char **argv) { app_data_t app = {0}; char cid_buf[100]; @@ -275,16 +236,21 @@ int main(int argc, char **argv) { match_regex(AMQP_URL_REGEX, matches, 10, app.amqp_con.url); if (matches[3] != NULL) { - app.amqp_con.user = strdup(matches[2]); + app.amqp_con.user = strdup(matches[3]); } if (matches[5] != NULL) { - app.amqp_con.password = strdup(matches[4]); + app.amqp_con.password = strdup(matches[5]); } if (matches[6] == NULL || matches[9] == NULL) { fprintf(stderr, "Invalid AMQP URL: %s", app.amqp_con.url); exit(1); } - app.amqp_con.host = strdup(matches[6]); + if (strchr(matches[6], '[') != NULL && strchr(matches[6], ']') != NULL) { + app.amqp_con.host = strndup(matches[6] + 1, strlen(matches[6]) - 2); + } else { + app.amqp_con.host = strdup(matches[6]); + } + app.amqp_con.address = strdup(matches[9]); if (matches[8] != NULL) { app.amqp_con.port = strdup(matches[8]); diff --git a/bridge.h b/bridge.h index 04077cb..42a567d 100644 --- a/bridge.h +++ b/bridge.h @@ -26,7 +26,9 @@ #define DEFAULT_AMQP_BLOCK "false" #define AMQP_URL_REGEX \ - "^(amqps*)://(([a-z]+)(:([a-z]+))*@)*([a-zA-Z_0-9.-]+)(:([0-9]+))*(.+)$" + "^(amqps*)://" \ + "(([a-z]+)(:([a-z]+))*@)*([a-zA-Z_0-9.-]+|\\[[:a-fA-F0-9]+\\])(:([0-9]+))" \ + "?(/[^[:space:]]*)?$" typedef struct { char *user; diff --git a/build/repos/opstools.repo b/build/repos/opstools.repo index aee2188..2d95a92 100644 --- a/build/repos/opstools.repo +++ b/build/repos/opstools.repo @@ -11,8 +11,8 @@ enabled=0 [centos-opstools] name=CentOS-OpsTools - collectd -mirrorlist=http://mirrorlist.centos.org/?arch=$basearch&release=$releasever-stream&repo=opstools-collectd-5 -#baseurl=http://mirror.centos.org/$contentdir/$releasever-stream/opstools/$basearch/collectd-5/ +#mirrorlist=http://mirrorlist.centos.org/?arch=$basearch&release=$releasever-stream&repo=opstools-collectd-5 +baseurl=http://vault.centos.org/$releasever-stream/opstools/$basearch/collectd-5/ gpgcheck=0 enabled=1 skip_if_unavailable=1 diff --git a/ci/run_tests.sh b/ci/run_tests.sh new file mode 100644 index 0000000..baeeab8 --- /dev/null +++ b/ci/run_tests.sh @@ -0,0 +1,11 @@ +#!/bin/env bash +# purpose: runt unit test suite + +set -ex + +# enable required repo(s) +dnf install -y centos-release-opstools tree +dnf install make gcc qpid-proton-c-devel annobin-annocheck gcc-plugin-annobin rpm-build -y + +make tests +./tests diff --git a/minunit.h b/minunit.h new file mode 100644 index 0000000..c6f5b4d --- /dev/null +++ b/minunit.h @@ -0,0 +1,18 @@ +/* + * taken from https://jera.com/techinfo/jtns/jtn002 + */ + +/* file: minunit.h */ +#define mu_assert(message, test) \ + do { \ + if (!(test)) \ + return message; \ + } while (0) +#define mu_run_test(test) \ + do { \ + char *message = test(); \ + tests_run++; \ + if (message) \ + return message; \ + } while (0) +extern int tests_run; \ No newline at end of file diff --git a/tests.c b/tests.c new file mode 100644 index 0000000..16e01c9 --- /dev/null +++ b/tests.c @@ -0,0 +1,168 @@ +/* + * Copyright 2024 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#include "bridge.h" +#include "minunit.h" +#include "utils.h" +#include + +/***************** test suite *****************/ + +static char *test_match_amqp_url_hostname() { + char *matches[10]; + memset(matches, 0, sizeof(matches)); + match_regex(AMQP_URL_REGEX, matches, 10, + "amqp://scooby:doo@some.k8s.svc:5666/foo/bar"); + + mu_assert("Failed to parse full amqp_url in form of hostname: user", + !strcmp(matches[3], "scooby")); + mu_assert("Failed to parse full amqp_url in form of hostname: password", + !strcmp(matches[5], "doo")); + mu_assert("Failed to parse full amqp_url in form of hostname: host", + !strcmp(matches[6], "some.k8s.svc")); + mu_assert("Failed to parse full amqp_url in form of hostname: port", + !strcmp(matches[8], "5666")); + mu_assert("Failed to parse full amqp_url in form of hostname: address", + !strcmp(matches[9], "/foo/bar")); + + char *matches2[10]; + memset(matches2, 0, sizeof(matches2)); + match_regex(AMQP_URL_REGEX, matches2, 10, "amqps://other.k8s.svc:5666/baz"); + + mu_assert( + "Failed to parse amqp_url w/o user info in form of hostname: user", + matches2[3] == NULL); + mu_assert( + "Failed to parse amqp_url w/o user info in form of hostname: password", + matches2[5] == NULL); + mu_assert( + "Failed to parse amqp_url w/o user info in form of hostname: host", + !strcmp(matches2[6], "other.k8s.svc")); + mu_assert( + "Failed to parse amqp_url w/o user info in form of hostname: port", + !strcmp(matches2[8], "5666")); + mu_assert( + "Failed to parse amqp_url w/o user info in form of hostname: address", + !strcmp(matches2[9], "/baz")); + + return 0; +} + +static char *test_match_amqp_url_ipv4() { + char *matches[10]; + memset(matches, 0, sizeof(matches)); + match_regex(AMQP_URL_REGEX, matches, 10, + "amqp://scooby:doo@127.0.0.1:5666/foo/bar"); + + mu_assert("Failed to parse amqp_url in form of IPv4: user invalid: %s", + !strcmp(matches[3], "scooby")); + mu_assert("Failed to parse amqp_url in form of IPv4: password", + !strcmp(matches[5], "doo")); + mu_assert("Failed to parse amqp_url in form of IPv4:i host", + !strcmp(matches[6], "127.0.0.1")); + mu_assert("Failed to parse amqp_url in form of IPv4: port", + !strcmp(matches[8], "5666")); + mu_assert("Failed to parse amqp_url in form of IPv4: address", + !strcmp(matches[9], "/foo/bar")); + + char *matches2[10]; + memset(matches2, 0, sizeof(matches2)); + match_regex(AMQP_URL_REGEX, matches2, 10, "amqps://127.0.0.1:5666/baz"); + + mu_assert("Failed to parse amqp_url w/o user info in form of IPv4: user", + matches2[3] == NULL); + mu_assert( + "Failed to parse amqp_url w/o user info in form of IPv4: password", + matches2[5] == NULL); + mu_assert("Failed to parse amqp_url w/o user info in form of IPv4: host", + !strcmp(matches2[6], "127.0.0.1")); + mu_assert("Failed to parse amqp_url w/o user info in form of IPv4: port", + !strcmp(matches2[8], "5666")); + mu_assert("Failed to parse amqp_url w/o user info in form of IPv4: address", + !strcmp(matches2[9], "/baz")); + return 0; +} + +static char *test_match_amqp_url_ipv6() { + char *matches[10]; + memset(matches, 0, sizeof(matches)); + match_regex(AMQP_URL_REGEX, matches, 10, + "amqp://scooby:doo@[fe80::abcd:fcff:fe07:9999]:5666/foo/bar"); + + mu_assert("Failed to parse amqp_url in form of IPv6: user", + !strcmp(matches[3], "scooby")); + mu_assert("Failed to parse amqp_url in form of IPv6: password", + !strcmp(matches[5], "doo")); + mu_assert("Failed to parse amqp_url in form of IPv6: host", + !strcmp(matches[6], "[fe80::abcd:fcff:fe07:9999]")); + mu_assert("Failed to parse amqp_url in form of IPv6: port", + !strcmp(matches[8], "5666")); + mu_assert("Failed to parse amqp_url in form of IPv6: address", + !strcmp(matches[9], "/foo/bar")); + + char *matches2[10]; + memset(matches2, 0, sizeof(matches2)); + match_regex(AMQP_URL_REGEX, matches2, 10, + "amqps://[fe80::abcd:fcff:fe07:9999]:5666/bas"); + + mu_assert("Failed to parse amqp_url w/o user info in form of IPv6: user", + matches2[3] == NULL); + mu_assert( + "Failed to parse amqp_url w/o user info in form of IPv6: password", + matches2[5] == NULL); + mu_assert("Failed to parse amqp_url w/o user info in form of IPv6: host", + !strcmp(matches2[6], "[fe80::abcd:fcff:fe07:9999]")); + mu_assert("Failed to parse amqp_url w/o user info in form of IPv6: port", + !strcmp(matches2[8], "5666")); + mu_assert("Failed to parse amqp_url w/o user info in form of IPv6: address", + !strcmp(matches2[9], "/bas")); + return 0; +} + +static char *test_match_amqp_url_fail() { + char *matches[10]; + memset(matches, 0, sizeof(matches)); + match_regex(AMQP_URL_REGEX, matches, 10, + "amqp://scooby:doo@[XXX.666.000.123/64]:5666/foo/bar"); + + mu_assert("Failed to fail parsing invalid amqp_url", + matches[6] == NULL && matches[9] == NULL); + return 0; +} + +/******************* runner *******************/ + +int tests_run = 0; + +static char *all_tests() { + mu_run_test(test_match_amqp_url_hostname); + mu_run_test(test_match_amqp_url_ipv4); + mu_run_test(test_match_amqp_url_ipv6); + mu_run_test(test_match_amqp_url_fail); + return 0; +} + +int main(int argc, char **argv) { + char *result = all_tests(); + if (result != 0) { + printf("%s\n", result); + } else { + printf("ALL TESTS PASSED\n"); + } + printf("Tests run: %d\n", tests_run); + + return result != 0; +} diff --git a/utils.c b/utils.c index a88c460..eef52dd 100644 --- a/utils.c +++ b/utils.c @@ -1,5 +1,7 @@ #include "utils.h" +#include #include +#include void time_diff(struct timespec t1, struct timespec t2, struct timespec *diff) { if (t2.tv_nsec < t1.tv_nsec) { @@ -28,3 +30,41 @@ char *time_snprintf(char *buf, size_t n, struct timespec t1) { return buf; } + +int match_regex(char *regmatch, char *matches[], int n_matches, + const char *to_match) { + /* "M" contains the matches found. */ + regmatch_t m[n_matches]; + regex_t regex; + + if (regcomp(®ex, regmatch, REG_EXTENDED)) { + fprintf(stderr, "Could not compile regex: %s\n", regmatch); + + return -1; + } + + int nomatch = regexec(®ex, to_match, n_matches, m, 0); + if (nomatch == REG_NOMATCH) { + return 0; + } + + int match_count = 0; + for (int i = 0; i < n_matches; i++) { + if (m[i].rm_so == -1) { + continue; + } + match_count++; + + int match_len = m[i].rm_eo - m[i].rm_so; + + matches[i] = malloc(match_len + 1); // make room for '\0' + + int k = 0; + for (int j = m[i].rm_so; j < m[i].rm_eo; j++) { + matches[i][k++] = to_match[j]; + } + matches[i][k] = '\0'; + } + + return match_count; +} diff --git a/utils.h b/utils.h index 8317067..eab73f2 100644 --- a/utils.h +++ b/utils.h @@ -6,4 +6,7 @@ void time_diff(struct timespec t1, struct timespec t2, struct timespec *diff); char *time_snprintf(char *buf, size_t n, struct timespec t1); +int match_regex(char *regmatch, char *matches[], int n_matches, + const char *to_match); + #endif