Template
feat(detect): implement check registry probe specs
This commit is contained in:
@@ -0,0 +1,482 @@
|
||||
/* LINK: ../../src/detect/checks.c ../../src/detect/check_registry.c ../../src/kdl/schema.c ../../src/kdl/parser.c ../../src/kdl/lexer.c ../../src/kdl/value.c ../../src/error.c ../../src/span.c */
|
||||
|
||||
#ifndef _POSIX_C_SOURCE
|
||||
#define _POSIX_C_SOURCE 200809L /* popen/pclose, sys/wait.h */
|
||||
#endif
|
||||
|
||||
/*
|
||||
* tests/unit/test_checks.c
|
||||
*
|
||||
* Unit tests for the check registry implementation (todo 11): for each of
|
||||
* the 8 check kinds, src/detect/checks.c builds the exact PROBE SPEC
|
||||
* (mode + C snippet + command argv + extra args) that todo 12 turns into
|
||||
* configure-time compile/link/run/command invocations. This module builds
|
||||
* specs ONLY - it never executes a probe and never emits shell text.
|
||||
*
|
||||
* The magic LINK comment on line 1 is REQUIRED by tests/run.sh: it lists
|
||||
* the extra .c sources compiled into this test binary (paths relative to
|
||||
* tests/unit/). checks.c needs check_registry.c (the shape table) + value.c
|
||||
* (arg extraction); parser.c + lexer.c build documents end-to-end from
|
||||
* source text; error.c + span.c via error.c's st_span_print; schema.c
|
||||
* linked per the task's file list.
|
||||
*/
|
||||
#include "munit.h"
|
||||
|
||||
#include "detect/check_registry.h"
|
||||
#include "detect/checks.h"
|
||||
#include "error.h"
|
||||
#include "kdl/ast.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
/* ---- helpers ---------------------------------------------------------- */
|
||||
|
||||
/* Parse a single-node document and return its one node (parse success is
|
||||
* asserted). The caller frees the returned document. */
|
||||
static struct st_kdl_node *
|
||||
parse_one_node(const char *src, struct st_kdl_document **doc_out)
|
||||
{
|
||||
struct st_error *err = NULL;
|
||||
|
||||
*doc_out = st_kdl_parse(src, "t.kdl", &err);
|
||||
munit_assert_null(err);
|
||||
munit_assert_not_null(*doc_out);
|
||||
return (*doc_out)->nodes;
|
||||
}
|
||||
|
||||
/* Parse `src` as a single check node, resolve its kind, and build its
|
||||
* probe spec into `*out`. Asserts parse + kind resolution + build success
|
||||
* and frees the document before returning (the probe is fully heap-owned
|
||||
* and independent of the document). Returns the resolved kind. */
|
||||
static enum st_check_kind
|
||||
build_from(const char *src, struct st_check_probe *out)
|
||||
{
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc = NULL;
|
||||
struct st_kdl_node *node;
|
||||
enum st_check_kind kind;
|
||||
|
||||
node = parse_one_node(src, &doc);
|
||||
kind = st_check_kind_from_node(node, &err);
|
||||
munit_assert_null(err);
|
||||
munit_assert_int(kind, !=, ST_CHECK_KIND_COUNT);
|
||||
munit_assert_null(st_check_probe_build(kind, node, out));
|
||||
st_kdl_document_free(doc);
|
||||
return kind;
|
||||
}
|
||||
|
||||
/* Compile `src` through a real cc on stdin (compile-only), with an
|
||||
* optional leading flag ("" for none, or e.g. "-fno-builtin "). Returns
|
||||
* the compiler's exit status, or -1 on any setup failure. The cc command
|
||||
* is a fixed literal; `src` reaches cc via stdin, never via the shell. */
|
||||
static int
|
||||
cc_compile_stdin(const char *src, const char *flag)
|
||||
{
|
||||
char cmd[256];
|
||||
FILE *p;
|
||||
int n;
|
||||
int rc;
|
||||
size_t len = strlen(src);
|
||||
|
||||
n = snprintf(cmd, sizeof cmd,
|
||||
"cc -std=c23 -Wall -Wextra -Wpedantic %s-c -x c - -o /dev/null",
|
||||
flag != NULL ? flag : "");
|
||||
if (n < 0 || (size_t)n >= sizeof cmd) {
|
||||
return -1;
|
||||
}
|
||||
p = popen(cmd, "w");
|
||||
if (p == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (fwrite(src, 1, len, p) != len) {
|
||||
(void)pclose(p);
|
||||
return -1;
|
||||
}
|
||||
rc = pclose(p);
|
||||
if (rc == -1 || !WIFEXITED(rc)) {
|
||||
return -1;
|
||||
}
|
||||
return WEXITSTATUS(rc);
|
||||
}
|
||||
|
||||
/* ---- tests ------------------------------------------------------------ */
|
||||
|
||||
/* (a) header "pthread.h" -> COMPILE probe whose source includes the
|
||||
* header with the angle-bracket form. */
|
||||
static MunitResult
|
||||
test_header(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
enum st_check_kind kind = build_from("header \"pthread.h\"", &p);
|
||||
|
||||
munit_assert_int(kind, ==, ST_CHECK_HEADER);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_COMPILE);
|
||||
munit_assert_not_null(p.c_source);
|
||||
munit_assert_true(strstr(p.c_source, "#include <pthread.h>") != NULL);
|
||||
munit_assert_null(p.command);
|
||||
munit_assert_null(p.extra_args);
|
||||
munit_assert_null(p.version_constraint);
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (b) function "strdup" -> LINK probe whose source declares and calls the
|
||||
* symbol; -fno-builtin so GCC's builtin machinery can't reject/mis-type
|
||||
* the declaration. */
|
||||
static MunitResult
|
||||
test_function(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
enum st_check_kind kind = build_from("function \"strdup\"", &p);
|
||||
|
||||
munit_assert_int(kind, ==, ST_CHECK_FUNCTION);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_LINK);
|
||||
munit_assert_not_null(p.c_source);
|
||||
munit_assert_true(strstr(p.c_source, "extern void strdup(void)") != NULL);
|
||||
munit_assert_true(strstr(p.c_source, "strdup();") != NULL);
|
||||
munit_assert_not_null(p.extra_args);
|
||||
munit_assert_string_equal(p.extra_args[0], "-fno-builtin");
|
||||
munit_assert_null(p.extra_args[1]);
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (c) library "pthread" -> LINK probe with extra_arg "-lpthread". */
|
||||
static MunitResult
|
||||
test_library(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
enum st_check_kind kind = build_from("library \"pthread\"", &p);
|
||||
|
||||
munit_assert_int(kind, ==, ST_CHECK_LIBRARY);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_LINK);
|
||||
munit_assert_not_null(p.c_source);
|
||||
munit_assert_not_null(p.extra_args);
|
||||
munit_assert_string_equal(p.extra_args[0], "-lpthread");
|
||||
munit_assert_null(p.extra_args[1]);
|
||||
munit_assert_null(p.version_constraint);
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (d) type "size_t" -> COMPILE probe using sizeof(size_t) in a static
|
||||
* assertion. */
|
||||
static MunitResult
|
||||
test_type(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
enum st_check_kind kind = build_from("type \"size_t\"", &p);
|
||||
|
||||
munit_assert_int(kind, ==, ST_CHECK_TYPE);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_COMPILE);
|
||||
munit_assert_not_null(p.c_source);
|
||||
munit_assert_true(strstr(p.c_source, "sizeof(size_t)") != NULL);
|
||||
munit_assert_true(strstr(p.c_source, "_Static_assert") != NULL);
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (e) sizeof "long" -> RUN-capable probe: a _Static_assert sentinel plus a
|
||||
* printf run path. */
|
||||
static MunitResult
|
||||
test_sizeof(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
enum st_check_kind kind = build_from("sizeof \"long\"", &p);
|
||||
|
||||
munit_assert_int(kind, ==, ST_CHECK_SIZEOF);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_RUN);
|
||||
munit_assert_not_null(p.c_source);
|
||||
munit_assert_true(strstr(p.c_source, "sizeof(long)") != NULL);
|
||||
munit_assert_true(strstr(p.c_source, "_Static_assert") != NULL);
|
||||
munit_assert_true(strstr(p.c_source, "printf") != NULL);
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (f) program "pkg-config" -> COMMAND argv {command, -v, pkg-config}. */
|
||||
static MunitResult
|
||||
test_program(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
enum st_check_kind kind = build_from("program \"pkg-config\"", &p);
|
||||
|
||||
munit_assert_int(kind, ==, ST_CHECK_PROGRAM);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_COMMAND);
|
||||
munit_assert_null(p.c_source);
|
||||
munit_assert_not_null(p.command);
|
||||
munit_assert_string_equal(p.command[0], "command");
|
||||
munit_assert_string_equal(p.command[1], "-v");
|
||||
munit_assert_string_equal(p.command[2], "pkg-config");
|
||||
munit_assert_null(p.command[3]);
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (g) compiler_flag "-fsanitize=address" -> COMPILE probe with the flag as
|
||||
* an extra_arg. */
|
||||
static MunitResult
|
||||
test_compiler_flag(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
enum st_check_kind kind = build_from("compiler_flag \"-fsanitize=address\"",
|
||||
&p);
|
||||
|
||||
munit_assert_int(kind, ==, ST_CHECK_COMPILER_FLAG);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_COMPILE);
|
||||
munit_assert_not_null(p.c_source);
|
||||
munit_assert_not_null(p.extra_args);
|
||||
munit_assert_string_equal(p.extra_args[0], "-fsanitize=address");
|
||||
munit_assert_null(p.extra_args[1]);
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (h) pkg_config "openssl" -> COMMAND argv {pkg-config, --cflags, --libs,
|
||||
* openssl}. */
|
||||
static MunitResult
|
||||
test_pkg_config(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
enum st_check_kind kind = build_from("pkg_config \"openssl\"", &p);
|
||||
|
||||
munit_assert_int(kind, ==, ST_CHECK_PKG_CONFIG);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_COMMAND);
|
||||
munit_assert_null(p.c_source);
|
||||
munit_assert_not_null(p.command);
|
||||
munit_assert_string_equal(p.command[0], "pkg-config");
|
||||
munit_assert_string_equal(p.command[1], "--cflags");
|
||||
munit_assert_string_equal(p.command[2], "--libs");
|
||||
munit_assert_string_equal(p.command[3], "openssl");
|
||||
munit_assert_null(p.command[4]);
|
||||
munit_assert_null(p.version_constraint);
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* The optional `version` constraint: library records it (and still links
|
||||
* with plain -l); pkg_config maps >= to --atleast-version=. */
|
||||
static MunitResult
|
||||
test_version_constraints(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe lib = { 0 };
|
||||
struct st_check_probe pk = { 0 };
|
||||
struct st_check_probe pkeq = { 0 };
|
||||
|
||||
(void)build_from("library \"pthread\" version \">=2.0\"", &lib);
|
||||
munit_assert_int(lib.mode, ==, ST_PROBE_LINK);
|
||||
munit_assert_string_equal(lib.extra_args[0], "-lpthread");
|
||||
munit_assert_string_equal(lib.version_constraint, ">=2.0");
|
||||
|
||||
(void)build_from("pkg_config \"openssl\" version \">=1.1\"", &pk);
|
||||
munit_assert_int(pk.mode, ==, ST_PROBE_COMMAND);
|
||||
munit_assert_string_equal(pk.command[0], "pkg-config");
|
||||
munit_assert_string_equal(pk.command[1], "--atleast-version=1.1");
|
||||
munit_assert_string_equal(pk.command[2], "--cflags");
|
||||
munit_assert_string_equal(pk.command[3], "--libs");
|
||||
munit_assert_string_equal(pk.command[4], "openssl");
|
||||
munit_assert_null(pk.command[5]);
|
||||
munit_assert_string_equal(pk.version_constraint, ">=1.1");
|
||||
|
||||
(void)build_from("pkg_config \"openssl\" version \"=1.2\"", &pkeq);
|
||||
munit_assert_string_equal(pkeq.command[1], "--exact-version=1.2");
|
||||
|
||||
st_check_probe_free(&lib);
|
||||
st_check_probe_free(&pk);
|
||||
st_check_probe_free(&pkeq);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* A malformed version constraint (no operator) is rejected with a schema
|
||||
* error naming the check. */
|
||||
static MunitResult
|
||||
test_malformed_constraint(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc = NULL;
|
||||
struct st_kdl_node *node;
|
||||
struct st_check_probe p = { 0 };
|
||||
|
||||
node = parse_one_node("library \"l\" version \"7.0\"", &doc);
|
||||
err = st_check_probe_build(ST_CHECK_LIBRARY, node, &p);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
|
||||
munit_assert_true(strstr(st_error_message(err), "malformed version "
|
||||
"constraint") != NULL);
|
||||
st_error_free(err);
|
||||
st_kdl_document_free(doc);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (j) unknown kind / NULL node / missing argument all error cleanly. */
|
||||
static MunitResult
|
||||
test_bad_input(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_error *err = NULL;
|
||||
struct st_check_probe p = { 0 };
|
||||
struct st_kdl_document *doc = NULL;
|
||||
struct st_kdl_node *node;
|
||||
|
||||
err = st_check_probe_build(ST_CHECK_KIND_COUNT, NULL, &p);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
|
||||
st_error_free(err);
|
||||
|
||||
err = st_check_probe_build(ST_CHECK_HEADER, NULL, &p);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
|
||||
st_error_free(err);
|
||||
|
||||
node = parse_one_node("header", &doc);
|
||||
err = st_check_probe_build(ST_CHECK_HEADER, node, &p);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
|
||||
munit_assert_true(strstr(st_error_message(err), "requires an argument")
|
||||
!= NULL);
|
||||
st_error_free(err);
|
||||
st_kdl_document_free(doc);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (i) COMPILE-PROOF: three generated snippets actually compile under
|
||||
* `cc -std=c23 -Wall -Wextra -Wpedantic -c -x c -` (exit 0); the function
|
||||
* snippet additionally compiles clean with its own -fno-builtin arg. */
|
||||
static MunitResult
|
||||
test_compile_proof(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe hdr = { 0 };
|
||||
struct st_check_probe lib = { 0 };
|
||||
struct st_check_probe sz = { 0 };
|
||||
struct st_check_probe fn = { 0 };
|
||||
|
||||
(void)build_from("header \"pthread.h\"", &hdr);
|
||||
(void)build_from("library \"pthread\"", &lib);
|
||||
(void)build_from("sizeof \"long\"", &sz);
|
||||
(void)build_from("function \"strdup\"", &fn);
|
||||
|
||||
munit_assert_int(cc_compile_stdin(hdr.c_source, ""), ==, 0);
|
||||
munit_assert_int(cc_compile_stdin(lib.c_source, ""), ==, 0);
|
||||
munit_assert_int(cc_compile_stdin(sz.c_source, ""), ==, 0);
|
||||
munit_assert_int(cc_compile_stdin(fn.c_source, "-fno-builtin "), ==, 0);
|
||||
|
||||
st_check_probe_free(&hdr);
|
||||
st_check_probe_free(&lib);
|
||||
st_check_probe_free(&sz);
|
||||
st_check_probe_free(&fn);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* Adversarial: a header name with metacharacters yields a single-line
|
||||
* quoted include (no injection, valid C); a non-identifier function name
|
||||
* errors; a COMMAND probe carries its program name as ONE argv element. */
|
||||
static MunitResult
|
||||
test_adversarial(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc = NULL;
|
||||
size_t len;
|
||||
|
||||
(void)build_from("header \"\\\"; rm -rf /\"", &p);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_COMPILE);
|
||||
munit_assert_true(strncmp(p.c_source, "#include \"", 10) == 0);
|
||||
munit_assert_true(strstr(p.c_source, "rm -rf /") != NULL);
|
||||
munit_assert_null(strchr(p.c_source, '<'));
|
||||
len = strlen(p.c_source);
|
||||
munit_assert_true(len >= 1);
|
||||
munit_assert_true(p.c_source[len - 1] == '\n');
|
||||
munit_assert_true(strchr(p.c_source, '\n') == &p.c_source[len - 1]);
|
||||
st_check_probe_free(&p);
|
||||
|
||||
doc = st_kdl_parse("function \"foo bar\"", "t.kdl", &err);
|
||||
munit_assert_not_null(doc);
|
||||
munit_assert_null(err);
|
||||
err = st_check_probe_build(ST_CHECK_FUNCTION, doc->nodes, &p);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
|
||||
munit_assert_true(strstr(st_error_message(err), "not a valid C "
|
||||
"identifier") != NULL);
|
||||
st_error_free(err);
|
||||
st_kdl_document_free(doc);
|
||||
|
||||
doc = NULL;
|
||||
err = NULL;
|
||||
doc = st_kdl_parse("program \"pkg-config; rm -rf /\"", "t.kdl", &err);
|
||||
munit_assert_not_null(doc);
|
||||
munit_assert_null(err);
|
||||
munit_assert_null(st_check_probe_build(ST_CHECK_PROGRAM, doc->nodes, &p));
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_COMMAND);
|
||||
munit_assert_string_equal(p.command[2], "pkg-config; rm -rf /");
|
||||
munit_assert_null(p.command[3]);
|
||||
st_check_probe_free(&p);
|
||||
st_kdl_document_free(doc);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
static MunitTest tests[] = {
|
||||
{ "/checks/header", test_header, NULL, NULL, MUNIT_TEST_OPTION_NONE,
|
||||
NULL },
|
||||
{ "/checks/function", test_function, NULL, NULL, MUNIT_TEST_OPTION_NONE,
|
||||
NULL },
|
||||
{ "/checks/library", test_library, NULL, NULL, MUNIT_TEST_OPTION_NONE,
|
||||
NULL },
|
||||
{ "/checks/type", test_type, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/checks/sizeof", test_sizeof, NULL, NULL, MUNIT_TEST_OPTION_NONE,
|
||||
NULL },
|
||||
{ "/checks/program", test_program, NULL, NULL, MUNIT_TEST_OPTION_NONE,
|
||||
NULL },
|
||||
{ "/checks/compiler-flag", test_compiler_flag, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/checks/pkg-config", test_pkg_config, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/checks/version-constraints", test_version_constraints, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/checks/malformed-constraint", test_malformed_constraint, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/checks/bad-input", test_bad_input, NULL, NULL, MUNIT_TEST_OPTION_NONE,
|
||||
NULL },
|
||||
{ "/checks/compile-proof", test_compile_proof, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/checks/adversarial", test_adversarial, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||
};
|
||||
|
||||
static const MunitSuite suite = {
|
||||
"/checks", tests, NULL, 1, MUNIT_SUITE_OPTION_NONE,
|
||||
};
|
||||
|
||||
int
|
||||
main(int argc, char *argv[MUNIT_ARRAY_PARAM(argc + 1)])
|
||||
{
|
||||
return munit_suite_main(&suite, NULL, argc, argv);
|
||||
}
|
||||
Reference in New Issue
Block a user