Commit 107148bd authored by Stephen Rothwell's avatar Stephen Rothwell
Browse files

Merge remote-tracking branch 'kunit-next/kunit'

parents 8d2d41d2 7af29141
......@@ -13,6 +13,7 @@ KUnit - Unit Testing for the Linux Kernel
api/index
style
faq
tips
What is KUnit?
==============
......@@ -88,6 +89,7 @@ How do I use it?
================
* :doc:`start` - for new users of KUnit
* :doc:`tips` - for short examples of best practices
* :doc:`usage` - for a more detailed explanation of KUnit features
* :doc:`api/index` - for the list of KUnit APIs used for testing
* :doc:`kunit-tool` - for more information on the kunit_tool helper script
......
......@@ -196,8 +196,9 @@ Now add the following to ``drivers/misc/Kconfig``:
.. code-block:: kconfig
config MISC_EXAMPLE_TEST
bool "Test for my example"
tristate "Test for my example" if !KUNIT_ALL_TESTS
depends on MISC_EXAMPLE && KUNIT=y
default KUNIT_ALL_TESTS
and the following to ``drivers/misc/Makefile``:
......@@ -233,5 +234,7 @@ Congrats! You just wrote your first KUnit test!
Next Steps
==========
* Check out the :doc:`usage` page for a more
* Check out the :doc:`tips` page for tips on
writing idiomatic KUnit tests.
* Optional: see the :doc:`usage` page for a more
in-depth explanation of KUnit.
.. SPDX-License-Identifier: GPL-2.0
============================
Tips For Writing KUnit Tests
============================
Exiting early on failed expectations
------------------------------------
``KUNIT_EXPECT_EQ`` and friends will mark the test as failed and continue
execution. In some cases, it's unsafe to continue and you can use the
``KUNIT_ASSERT`` variant to exit on failure.
.. code-block:: c
void example_test_user_alloc_function(struct kunit *test)
{
void *object = alloc_some_object_for_me();
/* Make sure we got a valid pointer back. */
KUNIT_ASSERT_NOT_ERR_OR_NULL(test, object);
do_something_with_object(object);
}
Allocating memory
-----------------
Where you would use ``kzalloc``, you should prefer ``kunit_kzalloc`` instead.
KUnit will ensure the memory is freed once the test completes.
This is particularly useful since it lets you use the ``KUNIT_ASSERT_EQ``
macros to exit early from a test without having to worry about remembering to
call ``kfree``.
Example:
.. code-block:: c
void example_test_allocation(struct kunit *test)
{
char *buffer = kunit_kzalloc(test, 16, GFP_KERNEL);
/* Ensure allocation succeeded. */
KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buffer);
KUNIT_ASSERT_STREQ(test, buffer, "");
}
Testing static functions
------------------------
If you don't want to expose functions or variables just for testing, one option
is to conditionally ``#include`` the test file at the end of your .c file, e.g.
.. code-block:: c
/* In my_file.c */
static int do_interesting_thing();
#ifdef CONFIG_MY_KUNIT_TEST
#include "my_kunit_test.c"
#endif
Injecting test-only code
------------------------
Similarly to the above, it can be useful to add test-specific logic.
.. code-block:: c
/* In my_file.h */
#ifdef CONFIG_MY_KUNIT_TEST
/* Defined in my_kunit_test.c */
void test_only_hook(void);
#else
void test_only_hook(void) { }
#endif
TODO(dlatypov@google.com): add an example of using ``current->kunit_test`` in
such a hook when it's not only updated for ``CONFIG_KASAN=y``.
Customizing error messages
--------------------------
Each of the ``KUNIT_EXPECT`` and ``KUNIT_ASSERT`` macros have a ``_MSG`` variant.
These take a format string and arguments to provide additional context to the automatically generated error messages.
.. code-block:: c
char some_str[41];
generate_sha1_hex_string(some_str);
/* Before. Not easy to tell why the test failed. */
KUNIT_EXPECT_EQ(test, strlen(some_str), 40);
/* After. Now we see the offending string. */
KUNIT_EXPECT_EQ_MSG(test, strlen(some_str), 40, "some_str='%s'", some_str);
Alternatively, one can take full control over the error message by using ``KUNIT_FAIL()``, e.g.
.. code-block:: c
/* Before */
KUNIT_EXPECT_EQ(test, some_setup_function(), 0);
/* After: full control over the failure message. */
if (some_setup_function())
KUNIT_FAIL(test, "Failed to setup thing for testing");
Next Steps
==========
* Optional: see the :doc:`usage` page for a more
in-depth explanation of KUnit.
......@@ -4,6 +4,7 @@
menuconfig KUNIT
tristate "KUnit - Enable support for unit tests"
select GLOB if KUNIT=y
help
Enables support for kernel unit tests (KUnit), a lightweight unit
testing and mocking framework for the Linux kernel. These tests are
......
......@@ -85,6 +85,29 @@ void kunit_ptr_not_err_assert_format(const struct kunit_assert *assert,
}
EXPORT_SYMBOL_GPL(kunit_ptr_not_err_assert_format);
/* Checks if `text` is a literal representing `value`, e.g. "5" and 5 */
static bool is_literal(struct kunit *test, const char *text, long long value,
gfp_t gfp)
{
char *buffer;
int len;
bool ret;
len = snprintf(NULL, 0, "%lld", value);
if (strlen(text) != len)
return false;
buffer = kunit_kmalloc(test, len+1, gfp);
if (!buffer)
return false;
snprintf(buffer, len+1, "%lld", value);
ret = strncmp(buffer, text, len) == 0;
kunit_kfree(test, buffer);
return ret;
}
void kunit_binary_assert_format(const struct kunit_assert *assert,
struct string_stream *stream)
{
......@@ -97,12 +120,16 @@ void kunit_binary_assert_format(const struct kunit_assert *assert,
binary_assert->left_text,
binary_assert->operation,
binary_assert->right_text);
string_stream_add(stream, KUNIT_SUBSUBTEST_INDENT "%s == %lld\n",
binary_assert->left_text,
binary_assert->left_value);
string_stream_add(stream, KUNIT_SUBSUBTEST_INDENT "%s == %lld",
binary_assert->right_text,
binary_assert->right_value);
if (!is_literal(stream->test, binary_assert->left_text,
binary_assert->left_value, stream->gfp))
string_stream_add(stream, KUNIT_SUBSUBTEST_INDENT "%s == %lld\n",
binary_assert->left_text,
binary_assert->left_value);
if (!is_literal(stream->test, binary_assert->right_text,
binary_assert->right_value, stream->gfp))
string_stream_add(stream, KUNIT_SUBSUBTEST_INDENT "%s == %lld",
binary_assert->right_text,
binary_assert->right_value);
kunit_assert_print_msg(assert, stream);
}
EXPORT_SYMBOL_GPL(kunit_binary_assert_format);
......
// SPDX-License-Identifier: GPL-2.0
#include <kunit/test.h>
#include <linux/glob.h>
#include <linux/moduleparam.h>
/*
* These symbols point to the .kunit_test_suites section and are defined in
......@@ -11,14 +13,81 @@ extern struct kunit_suite * const * const __kunit_suites_end[];
#if IS_BUILTIN(CONFIG_KUNIT)
static void kunit_print_tap_header(void)
static char *filter_glob;
module_param(filter_glob, charp, 0);
MODULE_PARM_DESC(filter_glob,
"Filter which KUnit test suites run at boot-time, e.g. list*");
static struct kunit_suite * const *
kunit_filter_subsuite(struct kunit_suite * const * const subsuite)
{
int i, n = 0;
struct kunit_suite **filtered;
n = 0;
for (i = 0; subsuite[i] != NULL; ++i) {
if (glob_match(filter_glob, subsuite[i]->name))
++n;
}
if (n == 0)
return NULL;
filtered = kmalloc_array(n + 1, sizeof(*filtered), GFP_KERNEL);
if (!filtered)
return NULL;
n = 0;
for (i = 0; subsuite[i] != NULL; ++i) {
if (glob_match(filter_glob, subsuite[i]->name))
filtered[n++] = subsuite[i];
}
filtered[n] = NULL;
return filtered;
}
struct suite_set {
struct kunit_suite * const * const *start;
struct kunit_suite * const * const *end;
};
static struct suite_set kunit_filter_suites(void)
{
int i;
struct kunit_suite * const **copy, * const *filtered_subsuite;
struct suite_set filtered;
const size_t max = __kunit_suites_end - __kunit_suites_start;
if (!filter_glob) {
filtered.start = __kunit_suites_start;
filtered.end = __kunit_suites_end;
return filtered;
}
copy = kmalloc_array(max, sizeof(*filtered.start), GFP_KERNEL);
filtered.start = copy;
if (!copy) { /* won't be able to run anything, return an empty set */
filtered.end = copy;
return filtered;
}
for (i = 0; i < max; ++i) {
filtered_subsuite = kunit_filter_subsuite(__kunit_suites_start[i]);
if (filtered_subsuite)
*copy++ = filtered_subsuite;
}
filtered.end = copy;
return filtered;
}
static void kunit_print_tap_header(struct suite_set *suite_set)
{
struct kunit_suite * const * const *suites, * const *subsuite;
int num_of_suites = 0;
for (suites = __kunit_suites_start;
suites < __kunit_suites_end;
suites++)
for (suites = suite_set->start; suites < suite_set->end; suites++)
for (subsuite = *suites; *subsuite != NULL; subsuite++)
num_of_suites++;
......@@ -30,12 +99,18 @@ int kunit_run_all_tests(void)
{
struct kunit_suite * const * const *suites;
kunit_print_tap_header();
struct suite_set suite_set = kunit_filter_suites();
kunit_print_tap_header(&suite_set);
for (suites = suite_set.start; suites < suite_set.end; suites++)
__kunit_test_suites_init(*suites);
for (suites = __kunit_suites_start;
suites < __kunit_suites_end;
suites++)
__kunit_test_suites_init(*suites);
if (filter_glob) { /* a copy was made of each array */
for (suites = suite_set.start; suites < suite_set.end; suites++)
kfree(*suites);
kfree(suite_set.start);
}
return 0;
}
......
......@@ -28,12 +28,12 @@ KunitBuildRequest = namedtuple('KunitBuildRequest',
['jobs', 'build_dir', 'alltests',
'make_options'])
KunitExecRequest = namedtuple('KunitExecRequest',
['timeout', 'build_dir', 'alltests'])
['timeout', 'build_dir', 'alltests', 'filter_glob'])
KunitParseRequest = namedtuple('KunitParseRequest',
['raw_output', 'input_data', 'build_dir', 'json'])
KunitRequest = namedtuple('KunitRequest', ['raw_output','timeout', 'jobs',
'build_dir', 'alltests', 'json',
'make_options'])
'build_dir', 'alltests', 'filter_glob',
'json', 'make_options'])
KernelDirectoryPath = sys.argv[0].split('tools/testing/kunit/')[0]
......@@ -93,6 +93,7 @@ def exec_tests(linux: kunit_kernel.LinuxSourceTree,
test_start = time.time()
result = linux.run_kernel(
timeout=None if request.alltests else request.timeout,
filter_glob=request.filter_glob,
build_dir=request.build_dir)
test_end = time.time()
......@@ -149,7 +150,7 @@ def run_tests(linux: kunit_kernel.LinuxSourceTree,
return build_result
exec_request = KunitExecRequest(request.timeout, request.build_dir,
request.alltests)
request.alltests, request.filter_glob)
exec_result = exec_tests(linux, exec_request)
if exec_result.status != KunitStatus.SUCCESS:
return exec_result
......@@ -182,6 +183,9 @@ def add_common_opts(parser) -> None:
parser.add_argument('--alltests',
help='Run all KUnit tests through allyesconfig',
action='store_true')
parser.add_argument('--kunitconfig',
help='Path to Kconfig fragment that enables KUnit tests',
metavar='kunitconfig')
def add_build_opts(parser) -> None:
parser.add_argument('--jobs',
......@@ -197,6 +201,14 @@ def add_exec_opts(parser) -> None:
type=int,
default=300,
metavar='timeout')
parser.add_argument('filter_glob',
help='maximum number of seconds to allow for all tests '
'to run. This does not include time taken to build the '
'tests.',
type=str,
nargs='?',
default='',
metavar='filter_glob')
def add_parse_opts(parser) -> None:
parser.add_argument('--raw_output', help='don\'t format output from kernel',
......@@ -256,13 +268,14 @@ def main(argv, linux=None):
os.mkdir(cli_args.build_dir)
if not linux:
linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir)
linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir, kunitconfig_path=cli_args.kunitconfig)
request = KunitRequest(cli_args.raw_output,
cli_args.timeout,
cli_args.jobs,
cli_args.build_dir,
cli_args.alltests,
cli_args.filter_glob,
cli_args.json,
cli_args.make_options)
result = run_tests(linux, request)
......@@ -274,7 +287,7 @@ def main(argv, linux=None):
os.mkdir(cli_args.build_dir)
if not linux:
linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir)
linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir, kunitconfig_path=cli_args.kunitconfig)
request = KunitConfigRequest(cli_args.build_dir,
cli_args.make_options)
......@@ -286,7 +299,7 @@ def main(argv, linux=None):
sys.exit(1)
elif cli_args.subcommand == 'build':
if not linux:
linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir)
linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir, kunitconfig_path=cli_args.kunitconfig)
request = KunitBuildRequest(cli_args.jobs,
cli_args.build_dir,
......@@ -304,7 +317,8 @@ def main(argv, linux=None):
exec_request = KunitExecRequest(cli_args.timeout,
cli_args.build_dir,
cli_args.alltests)
cli_args.alltests,
cli_args.filter_glob)
exec_result = exec_tests(linux, exec_request)
parse_request = KunitParseRequest(cli_args.raw_output,
exec_result.result,
......
......@@ -41,15 +41,14 @@ class Kconfig(object):
self._entries.append(entry)
def is_subset_of(self, other: 'Kconfig') -> bool:
other_dict = {e.name: e.value for e in other.entries()}
for a in self.entries():
found = False
for b in other.entries():
if a.name != b.name:
b = other_dict.get(a.name)
if b is None:
if a.value == 'n':
continue
if a.value != b.value:
return False
found = True
if a.value != 'n' and found == False:
return False
elif a.value != b:
return False
return True
......
......@@ -123,7 +123,7 @@ def get_outfile_path(build_dir) -> str:
class LinuxSourceTree(object):
"""Represents a Linux kernel source tree with KUnit tests."""
def __init__(self, build_dir: str, load_config=True, defconfig=DEFAULT_KUNITCONFIG_PATH) -> None:
def __init__(self, build_dir: str, load_config=True, kunitconfig_path='') -> None:
signal.signal(signal.SIGINT, self.signal_handler)
self._ops = LinuxSourceTreeOperations()
......@@ -131,9 +131,13 @@ class LinuxSourceTree(object):
if not load_config:
return
kunitconfig_path = get_kunitconfig_path(build_dir)
if not os.path.exists(kunitconfig_path):
shutil.copyfile(defconfig, kunitconfig_path)
if kunitconfig_path:
if not os.path.exists(kunitconfig_path):
raise ConfigError(f'Specified kunitconfig ({kunitconfig_path}) does not exist')
else:
kunitconfig_path = get_kunitconfig_path(build_dir)
if not os.path.exists(kunitconfig_path):
shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, kunitconfig_path)
self._kconfig = kunit_config.Kconfig()
self._kconfig.read_from_file(kunitconfig_path)
......@@ -199,8 +203,12 @@ class LinuxSourceTree(object):
return False
return self.validate_config(build_dir)
def run_kernel(self, args=[], build_dir='', timeout=None) -> Iterator[str]:
def run_kernel(self, args=None, build_dir='', filter_glob='', timeout=None) -> Iterator[str]:
if not args:
args = []
args.extend(['mem=1G', 'console=tty'])
if filter_glob:
args.append('kunit.filter_glob='+filter_glob)
self._ops.linux_bin(args, timeout, build_dir)
outfile = get_outfile_path(build_dir)
subprocess.call(['stty', 'sane'])
......
<
......@@ -12,6 +12,7 @@ from unittest import mock
import tempfile, shutil # Handling test_tmpdir
import json
import signal
import os
import kunit_config
......@@ -21,16 +22,18 @@ import kunit_json
import kunit
test_tmpdir = ''
abs_test_data_dir = ''
def setUpModule():
global test_tmpdir
global test_tmpdir, abs_test_data_dir
test_tmpdir = tempfile.mkdtemp()
abs_test_data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'test_data'))
def tearDownModule():
shutil.rmtree(test_tmpdir)
def get_absolute_path(path):
return os.path.join(os.path.dirname(__file__), path)
def test_data_path(path):
return os.path.join(abs_test_data_dir, path)
class KconfigTest(unittest.TestCase):
......@@ -46,8 +49,7 @@ class KconfigTest(unittest.TestCase):
def test_read_from_file(self):
kconfig = kunit_config.Kconfig()
kconfig_path = get_absolute_path(
'test_data/test_read_from_file.kconfig')
kconfig_path = test_data_path('test_read_from_file.kconfig')
kconfig.read_from_file(kconfig_path)
......@@ -98,21 +100,18 @@ class KUnitParserTest(unittest.TestCase):
str(needle) + '" not found in "' + str(haystack) + '"!')
def test_output_isolated_correctly(self):
log_path = get_absolute_path(
'test_data/test_output_isolated_correctly.log')
file = open(log_path)
result = kunit_parser.isolate_kunit_output(file.readlines())
log_path = test_data_path('test_output_isolated_correctly.log')
with open(log_path) as file:
result = kunit_parser.isolate_kunit_output(file.readlines())
self.assertContains('TAP version 14', result)
self.assertContains(' # Subtest: example', result)
self.assertContains(' 1..2', result)
self.assertContains(' ok 1 - example_simple_test', result)
self.assertContains(' ok 2 - example_mock_test', result)
self.assertContains('ok 1 - example', result)
file.close()
def test_output_with_prefix_isolated_correctly(self):
log_path = get_absolute_path(
'test_data/test_pound_sign.log')
log_path = test_data_path('test_pound_sign.log')
with open(log_path) as file:
result = kunit_parser.isolate_kunit_output(file.readlines())
self.assertContains('TAP version 14', result)
......@@ -141,61 +140,51 @@ class KUnitParserTest(unittest.TestCase):
self.assertContains('ok 3 - string-stream-test', result)
def test_parse_successful_test_log(self):
all_passed_log = get_absolute_path(
'test_data/test_is_test_passed-all_passed.log')
file = open(all_passed_log)
result = kunit_parser.parse_run_tests(file.readlines())
all_passed_log = test_data_path('test_is_test_passed-all_passed.log')
with open(all_passed_log) as file:
result = kunit_parser.parse_run_tests(file.readlines())
self.assertEqual(
kunit_parser.TestStatus.SUCCESS,
result.status)
file.close()
def test_parse_failed_test_log(self):
failed_log = get_absolute_path(
'test_data/test_is_test_passed-failure.log')
file = open(failed_log)
result = kunit_parser.parse_run_tests(file.readlines())
failed_log = test_data_path('test_is_test_passed-failure.log')
with open(failed_log) as file:
result = kunit_parser.parse_run_tests(file.readlines())
self.assertEqual(
kunit_parser.TestStatus.FAILURE,
result.status)
file.close()
def test_no_tests(self):
empty_log = get_absolute_path(
'test_data/test_is_test_passed-no_tests_run.log')
file = open(empty_log)
result = kunit_parser.parse_run_tests(
kunit_parser.isolate_kunit_output(file.readlines()))
empty_log = test_data_path('test_is_test_passed-no_tests_run.log')
with open(empty_log) as file:
result = kunit_parser.parse_run_tests(
kunit_parser.isolate_kunit_output(file.readlines()))
self.assertEqual(0, len(result.suites))
self.assertEqual(
kunit_parser.TestStatus.NO_TESTS,
result.status)
file.close()
def test_no_kunit_output(self):
crash_log = get_absolute_path(
'test_data/test_insufficient_memory.log')
file = open(crash_log)
crash_log = test_data_path('test_insufficient_memory.log')
print_mock = mock.patch('builtins.print').start()
result = kunit_parser.parse_run_tests(
kunit_parser.isolate_kunit_output(file.readlines()))
with open(crash_log) as file:
result = kunit_parser.parse_run_tests(
kunit_parser.isolate_kunit_output(file.readlines()))
print_mock.assert_any_call(StrContains('no tests run!'))
print_mock.stop()