From 5d7e83594e8aa4f8b465165c05938116279f493d Mon Sep 17 00:00:00 2001 From: Philippe Proulx Date: Mon, 13 Apr 2020 21:38:22 -0400 Subject: [PATCH] tests: add libbabeltrace2 pre/postcondition testing infrastructure This patch adds a basic libbabeltrace2 pre/postcondition testing infrastructure to the project. When a libbabeltrace2 public function precondition or postcondition is not satisfied, the function logs a FATAL-level message which indicates what's wrong and then aborts. Slow path functions check such conditions unconditionally, while fast path functions check them only in developer mode. Testing that a libbabeltrace2 function catches an unsatisfied pre/postcondition and reports it is not straightforward: the library aborts the program, so we can't use the usual approach because the test program itself would abort. The solution brought by this patch is the following process in `tests/lib/conds`: 1. The `conds-triggers.c` program contains all the pre/postcondition failure triggering instructions to test, one condition per function. Each triggering function triggers a single pre/postcondition check. It is expected that the function actually aborts. main() calls ppc_main(), an internal utility, with its command-line arguments and an array of condition trigger descriptors. Each condition trigger descriptor has: * A condition type: precondition or postcondition. * The ID of the condition to trigger. * A name suffix. * A condition triggering function. Each condition trigger descriptor has a unique name: its condition ID and an optional name suffix. As of this patch, the PPC_TRIGGER_*_RUN_IN_COMP_CLS_INIT() macros create condition trigger descriptors of which the function runs within a component class initialization function (accepts a `bt_self_component *` parameter). This is often needed as many libbabeltrace2 functions are only accessible through a self component (all the trace IR API, for example). ppc_main() handles two command-line subcommands: `list`: Prints a JSON array of objects which represent the triggering descriptors, for example (output for this patch): [ { "cond-id": "pre:field-class-integer-set-field-value-range:valid-n", "name": "pre:field-class-integer-set-field-value-range:valid-n-0" }, { "cond-id": "pre:field-class-integer-set-field-value-range:valid-n", "name": "pre:field-class-integer-set-field-value-range:valid-n-gt-64" }, { "cond-id": "pre:field-class-integer-set-field-value-range:not-null:field-class", "name": "pre:field-class-integer-set-field-value-range:not-null:field-class" } ] `run INDEX`: Runs the condition triggering function for the descriptor at index `INDEX` in the `list` array. It is expected that this command aborts. 2. `test_conds` is a Bash script which only does this: reldir=lib/conds export BT_TESTS_LIB_CONDS_TRIGGER_BIN="$BT_TESTS_BUILDDIR/$reldir/conds-triggers" if [ "$BT_OS_TYPE" = "mingw" ]; then BT_TESTS_LIB_CONDS_TRIGGER_BIN="$BT_TESTS_LIB_CONDS_TRIGGER_BIN.exe" fi run_python_bt2_test "$BT_TESTS_SRCDIR/$reldir" test.py In other words, it runs the Python TAP test runner to discover and run tests in `test.py`. This script is part of the `make check` test set. 3. In `test.py`, a function: a) Runs `conds-triggers list` and decodes the output to get the list of available condition trigger descriptors. This step also validates the regular expressions and checks that there are no duplicate descriptor names. b) For each condition trigger descriptor, creates and adds a test to its single test case class which: I. Executes `conds-triggers run INDEX` as a subprocess, where `INDEX` is the descriptor's index. II. Reads the complete process's standard error. III. Asserts that the process aborts (`SIGABRT` signal). This seems to be only possible on a POSIX system with the `subprocess.Popen` API. IV. Asserts that the standard error (II) contains the descriptor's condition ID. All this is only enabled if, at configuration time: * Python 3 is available. * The Babeltrace 2 developer mode is enabled (`BABELTRACE_DEV_MODE=1`). This patch's `conds-triggers.c` includes two precondition triggering functions to confirm that everything works as expected: trigger_fc_int_set_field_value_range_n_0(): When calling bt_field_class_integer_set_field_value_range(), the parameter `N` cannot be greater than 64. trigger_fc_int_set_field_value_range_n_gt_64(): When calling bt_field_class_integer_set_field_value_range(), the parameter `N` cannot be 0. trigger_fc_int_set_field_value_range_null(): When calling bt_field_class_integer_set_field_value_range(), the field class cannot be `NULL`. Signed-off-by: Philippe Proulx Change-Id: I70713d690f7dbfeac5804e6cfcec989242823611 Reviewed-on: https://review.lttng.org/c/babeltrace/+/3401 --- .gitignore | 1 + configure.ac | 2 + tests/Makefile.am | 4 + tests/lib/Makefile.am | 6 + tests/lib/conds/Makefile.am | 13 +++ tests/lib/conds/conds-triggers.c | 66 +++++++++++ tests/lib/conds/test.py | 148 +++++++++++++++++++++++++ tests/lib/conds/test_conds | 23 ++++ tests/lib/conds/utils.c | 182 +++++++++++++++++++++++++++++++ tests/lib/conds/utils.h | 82 ++++++++++++++ 10 files changed, 527 insertions(+) create mode 100644 tests/lib/conds/Makefile.am create mode 100644 tests/lib/conds/conds-triggers.c create mode 100644 tests/lib/conds/test.py create mode 100755 tests/lib/conds/test_conds create mode 100644 tests/lib/conds/utils.c create mode 100644 tests/lib/conds/utils.h diff --git a/.gitignore b/.gitignore index a9a11a57..709ac071 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ /tests/lib/test_trace_ir_ref /tests/lib/test_simple_sink /tests/lib/test_remove_destruction_listener_in_destruction_listener +/tests/lib/conds/conds-triggers /tests/param-validation/test_param_validation /tests/plugins/flt.lttng-utils.debug-info/test_bin_info /tests/plugins/flt.lttng-utils.debug-info/test_dwarf diff --git a/configure.ac b/configure.ac index e52538bd..91d82300 100644 --- a/configure.ac +++ b/configure.ac @@ -330,6 +330,7 @@ AC_ARG_VAR([BABELTRACE_DEV_MODE], [Set to 1 to enable the Babeltrace developer m AS_IF([test "x$BABELTRACE_DEV_MODE" = x1], [ AC_DEFINE([BT_DEV_MODE], 1, [Babeltrace developer mode]) ], [BABELTRACE_DEV_MODE=0]) +AM_CONDITIONAL([DEV_MODE], [test "x$BABELTRACE_DEV_MODE" = x1]) # BABELTRACE_DEBUG_MODE: AC_ARG_VAR([BABELTRACE_DEBUG_MODE], [Set to 1 to enable the Babeltrace debug mode (enables internal assertions for Babeltrace maintainers)]) @@ -816,6 +817,7 @@ AC_CONFIG_FILES([ tests/ctf-writer/Makefile tests/lib/Makefile tests/lib/test-plugin-plugins/Makefile + tests/lib/conds/Makefile tests/Makefile tests/param-validation/Makefile tests/plugins/Makefile diff --git a/tests/Makefile.am b/tests/Makefile.am index c545f9a7..bd7ae03d 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -141,6 +141,10 @@ endif if HAVE_PYTHON TESTS_PLUGINS += plugins/src.ctf.lttng-live/test_live + +if DEV_MODE +TESTS_LIB += lib/conds/test_conds +endif endif TESTS_PYTHON_PLUGIN_PROVIDER = diff --git a/tests/lib/Makefile.am b/tests/lib/Makefile.am index ff7ab172..c1b68d7f 100644 --- a/tests/lib/Makefile.am +++ b/tests/lib/Makefile.am @@ -54,3 +54,9 @@ SUBDIRS += test-plugin-plugins endif dist_check_SCRIPTS = test_plugin + +if HAVE_PYTHON +if DEV_MODE +SUBDIRS += conds +endif +endif diff --git a/tests/lib/conds/Makefile.am b/tests/lib/conds/Makefile.am new file mode 100644 index 00000000..7b0b6d3d --- /dev/null +++ b/tests/lib/conds/Makefile.am @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: MIT + +AM_CPPFLAGS += -I$(top_srcdir)/tests/utils + +conds_triggers_SOURCES = conds-triggers.c utils.c utils.h +conds_triggers_LDADD = \ + $(top_builddir)/src/common/libbabeltrace2-common.la \ + $(top_builddir)/src/logging/libbabeltrace2-logging.la \ + $(top_builddir)/src/lib/libbabeltrace2.la + +noinst_PROGRAMS = conds-triggers + +dist_check_SCRIPTS = test_conds test.py diff --git a/tests/lib/conds/conds-triggers.c b/tests/lib/conds/conds-triggers.c new file mode 100644 index 00000000..810af972 --- /dev/null +++ b/tests/lib/conds/conds-triggers.c @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: GPL-2.0-only + * + * Copyright (C) 2020 Philippe Proulx + */ + +#include + +#include "common/assert.h" +#include "utils.h" + +static +bt_field_class *get_uint_fc(bt_self_component *self_comp) +{ + bt_trace_class *tc = bt_trace_class_create(self_comp); + bt_field_class *fc; + + BT_ASSERT(tc); + fc = bt_field_class_integer_unsigned_create(tc); + BT_ASSERT(fc); + return fc; +} + +static +void trigger_fc_int_set_field_value_range_n_0(bt_self_component *self_comp) +{ + bt_field_class_integer_set_field_value_range(get_uint_fc(self_comp), 0); +} + +static +void trigger_fc_int_set_field_value_range_n_gt_64(bt_self_component *self_comp) +{ + bt_field_class_integer_set_field_value_range(get_uint_fc(self_comp), + 65); +} + +static +void trigger_fc_int_set_field_value_range_null(bt_self_component *self_comp) +{ + bt_field_class_integer_set_field_value_range(NULL, 23); +} + +static +const struct cond_trigger triggers[] = { + COND_TRIGGER_PRE_RUN_IN_COMP_CLS_INIT( + "pre:field-class-integer-set-field-value-range:valid-n", + "0", + trigger_fc_int_set_field_value_range_n_0 + ), + COND_TRIGGER_PRE_RUN_IN_COMP_CLS_INIT( + "pre:field-class-integer-set-field-value-range:valid-n", + "gt-64", + trigger_fc_int_set_field_value_range_n_gt_64 + ), + COND_TRIGGER_PRE_RUN_IN_COMP_CLS_INIT( + "pre:field-class-integer-set-field-value-range:not-null:field-class", + NULL, + trigger_fc_int_set_field_value_range_null + ), +}; + +int main(int argc, const char *argv[]) +{ + cond_main(argc, argv, triggers, sizeof(triggers) / sizeof(*triggers)); + return 0; +} diff --git a/tests/lib/conds/test.py b/tests/lib/conds/test.py new file mode 100644 index 00000000..0f39d2ac --- /dev/null +++ b/tests/lib/conds/test.py @@ -0,0 +1,148 @@ +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2020 Philippe Proulx + +import unittest +import subprocess +import functools +import signal +import os +import os.path +import re +import json + + +# the `conds-triggers` program's full path +_CONDS_TRIGGERS_PATH = os.environ['BT_TESTS_LIB_CONDS_TRIGGER_BIN'] + + +# test methods are added by _create_tests() +class LibPrePostCondsTestCase(unittest.TestCase): + pass + + +# a condition trigger descriptor (base) +class _CondTriggerDescriptor: + def __init__(self, index, trigger_name, cond_id): + self._index = index + self._trigger_name = trigger_name + self._cond_id = cond_id + + @property + def index(self): + return self._index + + @property + def trigger_name(self): + return self._trigger_name + + @property + def cond_id(self): + return self._cond_id + + +# precondition trigger descriptor +class _PreCondTriggerDescriptor(_CondTriggerDescriptor): + @property + def type_str(self): + return 'pre' + + +# postcondition trigger descriptor +class _PostCondTriggerDescriptor(_CondTriggerDescriptor): + @property + def type_str(self): + return 'post' + + +# test method template for `LibPrePostCondsTestCase` +def _test(self, descriptor): + # Execute: + # + # $ conds-triggers run + # + # where `` is the descriptor's index. + with subprocess.Popen( + [_CONDS_TRIGGERS_PATH, 'run', str(descriptor.index)], + stderr=subprocess.PIPE, + universal_newlines=True, + ) as proc: + # wait for termination and get standard output/error data + timeout = 5 + + try: + # wait for program end and get standard error pipe's contents + _, stderr = proc.communicate(timeout=timeout) + except subprocess.TimeoutExpired: + self.fail('Process hanged for {} seconds'.format(timeout)) + return + + # assert that program aborted (only available on POSIX) + if os.name == 'posix': + self.assertEqual(proc.returncode, -int(signal.SIGABRT)) + + # assert that the standard error text contains the condition ID + text = 'Condition ID: `{}`.'.format(descriptor.cond_id) + self.assertIn(text, stderr) + + +# Condition trigger descriptors from the JSON array returned by +# +# $ conds-triggers list +def _cond_trigger_descriptors_from_json(json_descr_array): + descriptors = [] + descriptor_names = set() + + for index, json_descr in enumerate(json_descr_array): + # sanity check: check for duplicate + trigger_name = json_descr['name'] + + if trigger_name in descriptor_names: + raise ValueError( + 'Duplicate condition trigger name `{}`'.format(trigger_name) + ) + + # condition ID + cond_id = json_descr['cond-id'] + + if cond_id.startswith('pre'): + cond_type = _PreCondTriggerDescriptor + elif cond_id.startswith('post'): + cond_type = _PostCondTriggerDescriptor + else: + raise ValueError('Invalid condition ID `{}`'.format(cond_id)) + + descriptors.append(cond_type(index, trigger_name, cond_id)) + descriptor_names.add(trigger_name) + + return descriptors + + +# creates the individual tests of `LibPrePostCondsTestCase` +def _create_tests(): + # Execute `conds-triggers list` to get a JSON array of condition + # trigger descriptors. + json_descr_array = json.loads( + subprocess.check_output([_CONDS_TRIGGERS_PATH, 'list'], universal_newlines=True) + ) + + # get condition trigger descriptor objects from JSON + descriptors = _cond_trigger_descriptors_from_json(json_descr_array) + + # create test methods + for descriptor in descriptors: + # test method name + test_meth_name = 'test_{}'.format( + re.sub(r'[^a-zA-Z0-9_]', '_', descriptor.trigger_name) + ) + + # test method + meth = functools.partialmethod(_test, descriptor) + setattr(LibPrePostCondsTestCase, test_meth_name, meth) + + +_create_tests() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/lib/conds/test_conds b/tests/lib/conds/test_conds new file mode 100755 index 00000000..48b4fe75 --- /dev/null +++ b/tests/lib/conds/test_conds @@ -0,0 +1,23 @@ +#!/bin/bash +# +# SPDX-License-Identifier: GPL-2.0-only +# +# Copyright (C) 2020 Philippe Proulx + +if [ "x${BT_TESTS_SRCDIR:-}" != "x" ]; then + UTILSSH="$BT_TESTS_SRCDIR/utils/utils.sh" +else + UTILSSH="$(dirname "$0")/../../utils/utils.sh" +fi + +# shellcheck source=../../utils/utils.sh +source "$UTILSSH" + +reldir=lib/conds +export BT_TESTS_LIB_CONDS_TRIGGER_BIN="$BT_TESTS_BUILDDIR/$reldir/conds-triggers" + +if [ "$BT_OS_TYPE" = "mingw" ]; then + BT_TESTS_LIB_CONDS_TRIGGER_BIN="$BT_TESTS_LIB_CONDS_TRIGGER_BIN.exe" +fi + +run_python_bt2_test "$BT_TESTS_SRCDIR/$reldir" test.py diff --git a/tests/lib/conds/utils.c b/tests/lib/conds/utils.c new file mode 100644 index 00000000..8fb5bc8f --- /dev/null +++ b/tests/lib/conds/utils.c @@ -0,0 +1,182 @@ +/* + * SPDX-License-Identifier: GPL-2.0-only + * + * Copyright (C) 2020 Philippe Proulx + */ + +#include +#include +#include +#include +#include +#include + +#include "common/assert.h" +#include "utils.h" + +typedef void (* run_in_comp_cls_init_func)( + bt_self_component *self_comp, void *user_data); + +struct comp_cls_init_method_data { + run_in_comp_cls_init_func func; + void *user_data; +}; + +static +bt_component_class_initialize_method_status comp_cls_init( + bt_self_component_source *self_comp, + bt_self_component_source_configuration *conf, + const bt_value *params, void *init_method_data) +{ + struct comp_cls_init_method_data *data = init_method_data; + + /* Call user function which is expected to abort */ + data->func(bt_self_component_source_as_self_component(self_comp), + data->user_data); + + /* Never reached! */ + return BT_COMPONENT_CLASS_INITIALIZE_METHOD_STATUS_ERROR; +} + +static +bt_message_iterator_class_next_method_status msg_iter_cls_next( + bt_self_message_iterator *self_msg_iter, + bt_message_array_const msgs, uint64_t capacity, + uint64_t *count) +{ + /* Not used */ + return BT_MESSAGE_ITERATOR_CLASS_NEXT_METHOD_STATUS_ERROR; +} + +static +void run_in_comp_cls_init(run_in_comp_cls_init_func func, + void *user_data) +{ + bt_message_iterator_class *msg_iter_cls; + bt_component_class_source *comp_cls; + bt_component_class_set_method_status set_method_status; + bt_graph *graph; + struct comp_cls_init_method_data init_method_data = { + .func = func, + .user_data = user_data, + }; + + /* Create component class */ + msg_iter_cls = bt_message_iterator_class_create(msg_iter_cls_next); + BT_ASSERT(msg_iter_cls); + comp_cls = bt_component_class_source_create("yo", msg_iter_cls); + BT_ASSERT(comp_cls); + set_method_status = bt_component_class_source_set_initialize_method( + comp_cls, comp_cls_init); + BT_ASSERT(set_method_status == BT_COMPONENT_CLASS_SET_METHOD_STATUS_OK); + + /* Create graph */ + graph = bt_graph_create(0); + BT_ASSERT(graph); + + /* + * Add source component: this calls the initialization method, + * calling `func`. + */ + (void) bt_graph_add_source_component_with_initialize_method_data(graph, + comp_cls, "whatever", NULL, &init_method_data, + BT_LOGGING_LEVEL_NONE, NULL); + + /* + * This point is not expected to be reached as func() is + * expected to abort. + */ +} + +static +void run_in_comp_cls_init_defer(bt_self_component *self_comp, + void *user_data) +{ + cond_trigger_run_in_comp_cls_init_func user_func = user_data; + + user_func(self_comp); +} + +static +void run_trigger(const struct cond_trigger *trigger) +{ + switch (trigger->func_type) { + case COND_TRIGGER_FUNC_TYPE_BASIC: + trigger->func.basic(); + break; + case COND_TRIGGER_FUNC_TYPE_RUN_IN_COMP_CLS_INIT: + run_in_comp_cls_init(run_in_comp_cls_init_defer, + trigger->func.run_in_comp_cls_init); + break; + default: + abort(); + } +} + +static +void escape_json_string(const char *str, GString *escaped_str) +{ + g_string_assign(escaped_str, ""); + + for (const char *ch = str; *ch; ch++) { + if (*ch == '\\' || *ch == '"') { + g_string_append_c(escaped_str, '\\'); + } + + g_string_append_c(escaped_str, *ch); + } +} + +static +void list_triggers(const struct cond_trigger triggers[], size_t trigger_count) +{ + GString *escaped_str = g_string_new(NULL); + size_t i; + + BT_ASSERT(escaped_str); + printf("["); + + for (i = 0; i < trigger_count; i++) { + const struct cond_trigger *trigger = &triggers[i]; + + /* Condition ID */ + escape_json_string(trigger->cond_id, escaped_str); + printf("{\"cond-id\":\"%s\",", escaped_str->str); + + /* Name starts with condition ID */ + printf("\"name\":\"%s", escaped_str->str); + + if (trigger->suffix) { + escape_json_string(trigger->suffix, escaped_str); + printf("-%s", escaped_str->str); + } + + printf("\"}"); + + if (i < trigger_count - 1) { + /* Comma between objects */ + printf(","); + } + } + + printf("]"); + g_string_free(escaped_str, TRUE); + fflush(stdout); +} + +void cond_main(int argc, const char *argv[], + const struct cond_trigger triggers[], size_t trigger_count) +{ + BT_ASSERT(argc >= 2); + + if (strcmp(argv[1], "list") == 0) { + list_triggers(triggers, trigger_count); + } else if (strcmp(argv[1], "run") == 0) { + int index; + + BT_ASSERT(argc >= 3); + index = atoi(argv[2]); + BT_ASSERT(index >= 0 && index < trigger_count); + run_trigger(&triggers[index]); + } +} diff --git a/tests/lib/conds/utils.h b/tests/lib/conds/utils.h new file mode 100644 index 00000000..1a77bde5 --- /dev/null +++ b/tests/lib/conds/utils.h @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: GPL-2.0-only + * + * Copyright (C) 2020 Philippe Proulx + */ + +#ifndef TESTS_LIB_CONDS_UTILS_H +#define TESTS_LIB_CONDS_UTILS_H + +enum cond_trigger_func_type { + COND_TRIGGER_FUNC_TYPE_BASIC, + COND_TRIGGER_FUNC_TYPE_RUN_IN_COMP_CLS_INIT, +}; + +enum cond_trigger_type { + COND_TRIGGER_TYPE_PRE, + COND_TRIGGER_TYPE_POST, +}; + +typedef void (* cond_trigger_basic_func)(void); +typedef void (* cond_trigger_run_in_comp_cls_init_func)(bt_self_component *); + +struct cond_trigger { + enum cond_trigger_type type; + enum cond_trigger_func_type func_type; + const char *cond_id; + const char *suffix; + union { + cond_trigger_basic_func basic; + cond_trigger_run_in_comp_cls_init_func run_in_comp_cls_init; + } func; +}; + +#define COND_TRIGGER_PRE_BASIC(_cond_id, _suffix, _func) \ + { \ + .type = COND_TRIGGER_TYPE_PRE, \ + .func_type = COND_TRIGGER_FUNC_TYPE_BASIC, \ + .cond_id = _cond_id, \ + .suffix = _suffix, \ + .func = { \ + .basic = _func, \ + } \ + } + +#define COND_TRIGGER_POST_BASIC(_cond_id, _suffix, _func) \ + { \ + .type = COND_TRIGGER_TYPE_POST, \ + .func_type = COND_TRIGGER_FUNC_TYPE_BASIC, \ + .cond_id = _cond_id, \ + .suffix = _suffix, \ + .func = { \ + .basic = _func, \ + } \ + } + +#define COND_TRIGGER_PRE_RUN_IN_COMP_CLS_INIT(_cond_id, _suffix, _func) \ + { \ + .type = COND_TRIGGER_TYPE_PRE, \ + .func_type = COND_TRIGGER_FUNC_TYPE_RUN_IN_COMP_CLS_INIT, \ + .cond_id = _cond_id, \ + .suffix = _suffix, \ + .func = { \ + .run_in_comp_cls_init = _func, \ + } \ + } + +#define COND_TRIGGER_POST_RUN_IN_COMP_CLS_INIT(_cond_id, _suffix, _func) \ + { \ + .type = COND_TRIGGER_TYPE_POST, \ + .func_type = COND_TRIGGER_FUNC_TYPE_RUN_IN_COMP_CLS_INIT, \ + .cond_id = _cond_id, \ + .suffix = _suffix, \ + .func = { \ + .run_in_comp_cls_init = _func, \ + } \ + } + +void cond_main(int argc, const char *argv[], + const struct cond_trigger triggers[], + size_t trigger_count); + +#endif /* TESTS_LIB_CONDS_UTILS_H */ -- 2.34.1