Skip to content

Commit

Permalink
kunit: test: add support for test abort
Browse files Browse the repository at this point in the history
Add support for aborting/bailing out of test cases, which is needed for
implementing assertions.

An assertion is like an expectation, but bails out of the test case
early if the assertion is not met. The idea with assertions is that you
use them to state all the preconditions for your test. Logically
speaking, these are the premises of the test case, so if a premise isn't
true, there is no point in continuing the test case because there are no
conclusions that can be drawn without the premises. Whereas, the
expectation is the thing you are trying to prove.

Signed-off-by: Brendan Higgins <[email protected]>
Reviewed-by: Greg Kroah-Hartman <[email protected]>
Reviewed-by: Logan Gunthorpe <[email protected]>
Reviewed-by: Stephen Boyd <[email protected]>
Signed-off-by: Shuah Khan <[email protected]>
  • Loading branch information
bjh83 authored and shuahkh committed Sep 30, 2019
1 parent 33adf80 commit 5f3e062
Show file tree
Hide file tree
Showing 5 changed files with 319 additions and 16 deletions.
2 changes: 2 additions & 0 deletions include/kunit/test.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#define _KUNIT_TEST_H

#include <kunit/assert.h>
#include <kunit/try-catch.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/types.h>
Expand Down Expand Up @@ -172,6 +173,7 @@ struct kunit {

/* private: internal use only. */
const char *name; /* Read only after initialization! */
struct kunit_try_catch try_catch;
/*
* success starts as true, and may only be set to false during a
* test case; thus, it is safe to update this across multiple
Expand Down
75 changes: 75 additions & 0 deletions include/kunit/try-catch.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* An API to allow a function, that may fail, to be executed, and recover in a
* controlled manner.
*
* Copyright (C) 2019, Google LLC.
* Author: Brendan Higgins <[email protected]>
*/

#ifndef _KUNIT_TRY_CATCH_H
#define _KUNIT_TRY_CATCH_H

#include <linux/types.h>

typedef void (*kunit_try_catch_func_t)(void *);

struct completion;
struct kunit;

/**
* struct kunit_try_catch - provides a generic way to run code which might fail.
* @test: The test case that is currently being executed.
* @try_completion: Completion that the control thread waits on while test runs.
* @try_result: Contains any errno obtained while running test case.
* @try: The function, the test case, to attempt to run.
* @catch: The function called if @try bails out.
* @context: used to pass user data to the try and catch functions.
*
* kunit_try_catch provides a generic, architecture independent way to execute
* an arbitrary function of type kunit_try_catch_func_t which may bail out by
* calling kunit_try_catch_throw(). If kunit_try_catch_throw() is called, @try
* is stopped at the site of invocation and @catch is called.
*
* struct kunit_try_catch provides a generic interface for the functionality
* needed to implement kunit->abort() which in turn is needed for implementing
* assertions. Assertions allow stating a precondition for a test simplifying
* how test cases are written and presented.
*
* Assertions are like expectations, except they abort (call
* kunit_try_catch_throw()) when the specified condition is not met. This is
* useful when you look at a test case as a logical statement about some piece
* of code, where assertions are the premises for the test case, and the
* conclusion is a set of predicates, rather expectations, that must all be
* true. If your premises are violated, it does not makes sense to continue.
*/
struct kunit_try_catch {
/* private: internal use only. */
struct kunit *test;
struct completion *try_completion;
int try_result;
kunit_try_catch_func_t try;
kunit_try_catch_func_t catch;
void *context;
};

void kunit_try_catch_init(struct kunit_try_catch *try_catch,
struct kunit *test,
kunit_try_catch_func_t try,
kunit_try_catch_func_t catch);

void kunit_try_catch_run(struct kunit_try_catch *try_catch, void *context);

void __noreturn kunit_try_catch_throw(struct kunit_try_catch *try_catch);

static inline int kunit_try_catch_get_result(struct kunit_try_catch *try_catch)
{
return try_catch->try_result;
}

/*
* Exposed for testing only.
*/
void kunit_generic_try_catch_init(struct kunit_try_catch *try_catch);

#endif /* _KUNIT_TRY_CATCH_H */
3 changes: 2 additions & 1 deletion lib/kunit/Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
obj-$(CONFIG_KUNIT) += test.o \
string-stream.o \
assert.o
assert.o \
try-catch.o

obj-$(CONFIG_KUNIT_TEST) += string-stream-test.o

Expand Down
137 changes: 122 additions & 15 deletions lib/kunit/test.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
*/

#include <kunit/test.h>
#include <kunit/try-catch.h>
#include <linux/kernel.h>
#include <linux/sched/debug.h>

static void kunit_set_failure(struct kunit *test)
{
Expand Down Expand Up @@ -162,6 +164,19 @@ static void kunit_fail(struct kunit *test, struct kunit_assert *assert)
WARN_ON(string_stream_destroy(stream));
}

static void __noreturn kunit_abort(struct kunit *test)
{
kunit_try_catch_throw(&test->try_catch); /* Does not return. */

/*
* Throw could not abort from test.
*
* XXX: we should never reach this line! As kunit_try_catch_throw is
* marked __noreturn.
*/
WARN_ONCE(true, "Throw could not abort from test!\n");
}

void kunit_do_assertion(struct kunit *test,
struct kunit_assert *assert,
bool pass,
Expand All @@ -180,6 +195,9 @@ void kunit_do_assertion(struct kunit *test,
kunit_fail(test, assert);

va_end(args);

if (assert->type == KUNIT_ASSERTION)
kunit_abort(test);
}

void kunit_init_test(struct kunit *test, const char *name)
Expand All @@ -191,33 +209,122 @@ void kunit_init_test(struct kunit *test, const char *name)
}

/*
* Performs all logic to run a test case.
* Initializes and runs test case. Does not clean up or do post validations.
*/
static void kunit_run_case(struct kunit_suite *suite,
struct kunit_case *test_case)
static void kunit_run_case_internal(struct kunit *test,
struct kunit_suite *suite,
struct kunit_case *test_case)
{
struct kunit test;

kunit_init_test(&test, test_case->name);

if (suite->init) {
int ret;

ret = suite->init(&test);
ret = suite->init(test);
if (ret) {
kunit_err(&test, "failed to initialize: %d\n", ret);
kunit_set_failure(&test);
test_case->success = test.success;
kunit_err(test, "failed to initialize: %d\n", ret);
kunit_set_failure(test);
return;
}
}

test_case->run_case(&test);
test_case->run_case(test);
}

static void kunit_case_internal_cleanup(struct kunit *test)
{
kunit_cleanup(test);
}

/*
* Performs post validations and cleanup after a test case was run.
* XXX: Should ONLY BE CALLED AFTER kunit_run_case_internal!
*/
static void kunit_run_case_cleanup(struct kunit *test,
struct kunit_suite *suite)
{
if (suite->exit)
suite->exit(&test);
suite->exit(test);

kunit_case_internal_cleanup(test);
}

struct kunit_try_catch_context {
struct kunit *test;
struct kunit_suite *suite;
struct kunit_case *test_case;
};

static void kunit_try_run_case(void *data)
{
struct kunit_try_catch_context *ctx = data;
struct kunit *test = ctx->test;
struct kunit_suite *suite = ctx->suite;
struct kunit_case *test_case = ctx->test_case;

/*
* kunit_run_case_internal may encounter a fatal error; if it does,
* abort will be called, this thread will exit, and finally the parent
* thread will resume control and handle any necessary clean up.
*/
kunit_run_case_internal(test, suite, test_case);
/* This line may never be reached. */
kunit_run_case_cleanup(test, suite);
}

static void kunit_catch_run_case(void *data)
{
struct kunit_try_catch_context *ctx = data;
struct kunit *test = ctx->test;
struct kunit_suite *suite = ctx->suite;
int try_exit_code = kunit_try_catch_get_result(&test->try_catch);

if (try_exit_code) {
kunit_set_failure(test);
/*
* Test case could not finish, we have no idea what state it is
* in, so don't do clean up.
*/
if (try_exit_code == -ETIMEDOUT) {
kunit_err(test, "test case timed out\n");
/*
* Unknown internal error occurred preventing test case from
* running, so there is nothing to clean up.
*/
} else {
kunit_err(test, "internal error occurred preventing test case from running: %d\n",
try_exit_code);
}
return;
}

/*
* Test case was run, but aborted. It is the test case's business as to
* whether it failed or not, we just need to clean up.
*/
kunit_run_case_cleanup(test, suite);
}

/*
* Performs all logic to run a test case. It also catches most errors that
* occur in a test case and reports them as failures.
*/
static void kunit_run_case_catch_errors(struct kunit_suite *suite,
struct kunit_case *test_case)
{
struct kunit_try_catch_context context;
struct kunit_try_catch *try_catch;
struct kunit test;

kunit_init_test(&test, test_case->name);
try_catch = &test.try_catch;

kunit_cleanup(&test);
kunit_try_catch_init(try_catch,
&test,
kunit_try_run_case,
kunit_catch_run_case);
context.test = &test;
context.suite = suite;
context.test_case = test_case;
kunit_try_catch_run(try_catch, &context);

test_case->success = test.success;
}
Expand All @@ -230,7 +337,7 @@ int kunit_run_tests(struct kunit_suite *suite)
kunit_print_subtest_start(suite);

for (test_case = suite->test_cases; test_case->run_case; test_case++) {
kunit_run_case(suite, test_case);
kunit_run_case_catch_errors(suite, test_case);
kunit_print_test_case_ok_not_ok(test_case, test_case_count++);
}

Expand Down
Loading

0 comments on commit 5f3e062

Please sign in to comment.