linux-kselftest-kunit-fixes-5.11-rc5

This KUnit update for Linux 5.11-rc5 consist of 5 fixes to kunit tool
 and documentation from Daniel Latypov and David Gow.
 -----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCgAdFiEEPZKym/RZuOCGeA/kCwJExA0NQxwFAmALbwMACgkQCwJExA0N
 QxxkRhAA4x1n5r1IhkH8jSZ15E0zDrE6eQpAJIyM5R5+ZCLfZXtgMEVK4OzYdzC1
 5z0ZdAYp/xcalbhYnKozv8TH9fe/Zkzj76DJRt45Ajo90RMG5Tnx6z3UWtsBlW+3
 mVzJPTM2nGNHuVnoVOQHnnZQMrPiCkTNm1ccjCG6u98MdPGpzRfoZydG4uCJ1UbB
 /3+j2UKoScW2oRDXUNszfiCQDACkzrXydPgr9L/+vqHgj8tGXMXaA/jZnOu++xFx
 Oy1vvWOXcTtfLfNWTZfEEKwjxIqGZq3V576JptiuAfWjadyg3XYAXk86vGpiX+g7
 cyDcOY+7AA7eL3KK+1E8bLRCL0q1GxiTHMBc3q+WK5DAUbz63Y6MT8PG2D1V1k9p
 dSEz0jL1lx5Hu6k3ABw5xLE4MzNeozI5kETsFk35GHCi7dzLi7FJEPM9vcj15kQ9
 tmN5GOHJ5bNRfy0pYcR/b67tnr6SmRRx0ion2RQNB84C2Vbx/g53FTzPKUaryKVv
 Jm0aWHqL2/ooBE2uruYCbre6502ol4lCypWd+dAq54/vJANVkgqxdM0NuLe8RjtY
 DjzMmIfRNyOvCLXcAcDHwbMVci0N5HR6YbEgdeF+ySk5hXjlL1+iR1pqevS5iUuA
 0zp0tITyTjZTDUQos1I6RF5eUqhiS7WtHBQkTfK1FyHg9kVxkns=
 =4uNi
 -----END PGP SIGNATURE-----

Merge tag 'linux-kselftest-kunit-fixes-5.11-rc5' of git://git.kernel.org/pub/scm/linux/kernel/git/shuah/linux-kselftest

Pull kunit fixes from Shuah :
 "Five fixes to the kunit tool and documentation from Daniel Latypov and
  David Gow"

* tag 'linux-kselftest-kunit-fixes-5.11-rc5' of git://git.kernel.org/pub/scm/linux/kernel/git/shuah/linux-kselftest:
  kunit: tool: move kunitconfig parsing into __init__, make it optional
  kunit: tool: fix minor typing issue with None status
  kunit: tool: surface and address more typing issues
  Documentation: kunit: include example of a parameterized test
  kunit: tool: Fix spelling of "diagnostic" in kunit_parser
This commit is contained in:
Linus Torvalds 2021-01-23 11:25:33 -08:00
commit 929b979611
6 changed files with 142 additions and 95 deletions

View File

@ -522,6 +522,63 @@ There's more boilerplate involved, but it can:
* E.g. if we wanted to also test ``sha256sum``, we could add a ``sha256``
field and reuse ``cases``.
* be converted to a "parameterized test", see below.
Parameterized Testing
~~~~~~~~~~~~~~~~~~~~~
The table-driven testing pattern is common enough that KUnit has special
support for it.
Reusing the same ``cases`` array from above, we can write the test as a
"parameterized test" with the following.
.. code-block:: c
// This is copy-pasted from above.
struct sha1_test_case {
const char *str;
const char *sha1;
};
struct sha1_test_case cases[] = {
{
.str = "hello world",
.sha1 = "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed",
},
{
.str = "hello world!",
.sha1 = "430ce34d020724ed75a196dfc2ad67c77772d169",
},
};
// Need a helper function to generate a name for each test case.
static void case_to_desc(const struct sha1_test_case *t, char *desc)
{
strcpy(desc, t->str);
}
// Creates `sha1_gen_params()` to iterate over `cases`.
KUNIT_ARRAY_PARAM(sha1, cases, case_to_desc);
// Looks no different from a normal test.
static void sha1_test(struct kunit *test)
{
// This function can just contain the body of the for-loop.
// The former `cases[i]` is accessible under test->param_value.
char out[40];
struct sha1_test_case *test_param = (struct sha1_test_case *)(test->param_value);
sha1sum(test_param->str, out);
KUNIT_EXPECT_STREQ_MSG(test, (char *)out, test_param->sha1,
"sha1sum(%s)", test_param->str);
}
// Instead of KUNIT_CASE, we use KUNIT_CASE_PARAM and pass in the
// function declared by KUNIT_ARRAY_PARAM.
static struct kunit_case sha1_test_cases[] = {
KUNIT_CASE_PARAM(sha1_test, sha1_gen_params),
{}
};
.. _kunit-on-non-uml:
KUnit on non-UML architectures

View File

@ -43,9 +43,9 @@ class KunitStatus(Enum):
BUILD_FAILURE = auto()
TEST_FAILURE = auto()
def get_kernel_root_path():
parts = sys.argv[0] if not __file__ else __file__
parts = os.path.realpath(parts).split('tools/testing/kunit')
def get_kernel_root_path() -> str:
path = sys.argv[0] if not __file__ else __file__
parts = os.path.realpath(path).split('tools/testing/kunit')
if len(parts) != 2:
sys.exit(1)
return parts[0]
@ -171,7 +171,7 @@ def run_tests(linux: kunit_kernel.LinuxSourceTree,
exec_result.elapsed_time))
return parse_result
def add_common_opts(parser):
def add_common_opts(parser) -> None:
parser.add_argument('--build_dir',
help='As in the make command, it specifies the build '
'directory.',
@ -183,13 +183,13 @@ def add_common_opts(parser):
help='Run all KUnit tests through allyesconfig',
action='store_true')
def add_build_opts(parser):
def add_build_opts(parser) -> None:
parser.add_argument('--jobs',
help='As in the make command, "Specifies the number of '
'jobs (commands) to run simultaneously."',
type=int, default=8, metavar='jobs')
def add_exec_opts(parser):
def add_exec_opts(parser) -> None:
parser.add_argument('--timeout',
help='maximum number of seconds to allow for all tests '
'to run. This does not include time taken to build the '
@ -198,7 +198,7 @@ def add_exec_opts(parser):
default=300,
metavar='timeout')
def add_parse_opts(parser):
def add_parse_opts(parser) -> None:
parser.add_argument('--raw_output', help='don\'t format output from kernel',
action='store_true')
parser.add_argument('--json',
@ -256,10 +256,7 @@ def main(argv, linux=None):
os.mkdir(cli_args.build_dir)
if not linux:
linux = kunit_kernel.LinuxSourceTree()
linux.create_kunitconfig(cli_args.build_dir)
linux.read_kunitconfig(cli_args.build_dir)
linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir)
request = KunitRequest(cli_args.raw_output,
cli_args.timeout,
@ -277,10 +274,7 @@ def main(argv, linux=None):
os.mkdir(cli_args.build_dir)
if not linux:
linux = kunit_kernel.LinuxSourceTree()
linux.create_kunitconfig(cli_args.build_dir)
linux.read_kunitconfig(cli_args.build_dir)
linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir)
request = KunitConfigRequest(cli_args.build_dir,
cli_args.make_options)
@ -292,10 +286,7 @@ def main(argv, linux=None):
sys.exit(1)
elif cli_args.subcommand == 'build':
if not linux:
linux = kunit_kernel.LinuxSourceTree()
linux.create_kunitconfig(cli_args.build_dir)
linux.read_kunitconfig(cli_args.build_dir)
linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir)
request = KunitBuildRequest(cli_args.jobs,
cli_args.build_dir,
@ -309,10 +300,7 @@ def main(argv, linux=None):
sys.exit(1)
elif cli_args.subcommand == 'exec':
if not linux:
linux = kunit_kernel.LinuxSourceTree()
linux.create_kunitconfig(cli_args.build_dir)
linux.read_kunitconfig(cli_args.build_dir)
linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir)
exec_request = KunitExecRequest(cli_args.timeout,
cli_args.build_dir,

View File

@ -8,6 +8,7 @@
import collections
import re
from typing import List, Set
CONFIG_IS_NOT_SET_PATTERN = r'^# CONFIG_(\w+) is not set$'
CONFIG_PATTERN = r'^CONFIG_(\w+)=(\S+|".*")$'
@ -30,10 +31,10 @@ class KconfigParseError(Exception):
class Kconfig(object):
"""Represents defconfig or .config specified using the Kconfig language."""
def __init__(self):
self._entries = []
def __init__(self) -> None:
self._entries = [] # type: List[KconfigEntry]
def entries(self):
def entries(self) -> Set[KconfigEntry]:
return set(self._entries)
def add_entry(self, entry: KconfigEntry) -> None:

View File

@ -13,7 +13,7 @@ import kunit_parser
from kunit_parser import TestStatus
def get_json_result(test_result, def_config, build_dir, json_path):
def get_json_result(test_result, def_config, build_dir, json_path) -> str:
sub_groups = []
# Each test suite is mapped to a KernelCI sub_group

View File

@ -11,6 +11,7 @@ import subprocess
import os
import shutil
import signal
from typing import Iterator
from contextlib import ExitStack
@ -39,7 +40,7 @@ class BuildError(Exception):
class LinuxSourceTreeOperations(object):
"""An abstraction over command line operations performed on a source tree."""
def make_mrproper(self):
def make_mrproper(self) -> None:
try:
subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT)
except OSError as e:
@ -47,7 +48,7 @@ class LinuxSourceTreeOperations(object):
except subprocess.CalledProcessError as e:
raise ConfigError(e.output.decode())
def make_olddefconfig(self, build_dir, make_options):
def make_olddefconfig(self, build_dir, make_options) -> None:
command = ['make', 'ARCH=um', 'olddefconfig']
if make_options:
command.extend(make_options)
@ -60,7 +61,7 @@ class LinuxSourceTreeOperations(object):
except subprocess.CalledProcessError as e:
raise ConfigError(e.output.decode())
def make_allyesconfig(self, build_dir, make_options):
def make_allyesconfig(self, build_dir, make_options) -> None:
kunit_parser.print_with_timestamp(
'Enabling all CONFIGs for UML...')
command = ['make', 'ARCH=um', 'allyesconfig']
@ -82,7 +83,7 @@ class LinuxSourceTreeOperations(object):
kunit_parser.print_with_timestamp(
'Starting Kernel with all configs takes a few minutes...')
def make(self, jobs, build_dir, make_options):
def make(self, jobs, build_dir, make_options) -> None:
command = ['make', 'ARCH=um', '--jobs=' + str(jobs)]
if make_options:
command.extend(make_options)
@ -100,7 +101,7 @@ class LinuxSourceTreeOperations(object):
if stderr: # likely only due to build warnings
print(stderr.decode())
def linux_bin(self, params, timeout, build_dir):
def linux_bin(self, params, timeout, build_dir) -> None:
"""Runs the Linux UML binary. Must be named 'linux'."""
linux_bin = get_file_path(build_dir, 'linux')
outfile = get_outfile_path(build_dir)
@ -110,23 +111,34 @@ class LinuxSourceTreeOperations(object):
stderr=subprocess.STDOUT)
process.wait(timeout)
def get_kconfig_path(build_dir):
def get_kconfig_path(build_dir) -> str:
return get_file_path(build_dir, KCONFIG_PATH)
def get_kunitconfig_path(build_dir):
def get_kunitconfig_path(build_dir) -> str:
return get_file_path(build_dir, KUNITCONFIG_PATH)
def get_outfile_path(build_dir):
def get_outfile_path(build_dir) -> str:
return get_file_path(build_dir, OUTFILE_PATH)
class LinuxSourceTree(object):
"""Represents a Linux kernel source tree with KUnit tests."""
def __init__(self):
self._ops = LinuxSourceTreeOperations()
def __init__(self, build_dir: str, load_config=True, defconfig=DEFAULT_KUNITCONFIG_PATH) -> None:
signal.signal(signal.SIGINT, self.signal_handler)
def clean(self):
self._ops = LinuxSourceTreeOperations()
if not load_config:
return
kunitconfig_path = get_kunitconfig_path(build_dir)
if not os.path.exists(kunitconfig_path):
shutil.copyfile(defconfig, kunitconfig_path)
self._kconfig = kunit_config.Kconfig()
self._kconfig.read_from_file(kunitconfig_path)
def clean(self) -> bool:
try:
self._ops.make_mrproper()
except ConfigError as e:
@ -134,17 +146,7 @@ class LinuxSourceTree(object):
return False
return True
def create_kunitconfig(self, build_dir, defconfig=DEFAULT_KUNITCONFIG_PATH):
kunitconfig_path = get_kunitconfig_path(build_dir)
if not os.path.exists(kunitconfig_path):
shutil.copyfile(defconfig, kunitconfig_path)
def read_kunitconfig(self, build_dir):
kunitconfig_path = get_kunitconfig_path(build_dir)
self._kconfig = kunit_config.Kconfig()
self._kconfig.read_from_file(kunitconfig_path)
def validate_config(self, build_dir):
def validate_config(self, build_dir) -> bool:
kconfig_path = get_kconfig_path(build_dir)
validated_kconfig = kunit_config.Kconfig()
validated_kconfig.read_from_file(kconfig_path)
@ -158,7 +160,7 @@ class LinuxSourceTree(object):
return False
return True
def build_config(self, build_dir, make_options):
def build_config(self, build_dir, make_options) -> bool:
kconfig_path = get_kconfig_path(build_dir)
if build_dir and not os.path.exists(build_dir):
os.mkdir(build_dir)
@ -170,7 +172,7 @@ class LinuxSourceTree(object):
return False
return self.validate_config(build_dir)
def build_reconfig(self, build_dir, make_options):
def build_reconfig(self, build_dir, make_options) -> bool:
"""Creates a new .config if it is not a subset of the .kunitconfig."""
kconfig_path = get_kconfig_path(build_dir)
if os.path.exists(kconfig_path):
@ -186,7 +188,7 @@ class LinuxSourceTree(object):
print('Generating .config ...')
return self.build_config(build_dir, make_options)
def build_um_kernel(self, alltests, jobs, build_dir, make_options):
def build_um_kernel(self, alltests, jobs, build_dir, make_options) -> bool:
try:
if alltests:
self._ops.make_allyesconfig(build_dir, make_options)
@ -197,7 +199,7 @@ class LinuxSourceTree(object):
return False
return self.validate_config(build_dir)
def run_kernel(self, args=[], build_dir='', timeout=None):
def run_kernel(self, args=[], build_dir='', timeout=None) -> Iterator[str]:
args.extend(['mem=1G', 'console=tty'])
self._ops.linux_bin(args, timeout, build_dir)
outfile = get_outfile_path(build_dir)
@ -206,6 +208,6 @@ class LinuxSourceTree(object):
for line in file:
yield line
def signal_handler(self, sig, frame):
def signal_handler(self, sig, frame) -> None:
logging.error('Build interruption occurred. Cleaning console.')
subprocess.call(['stty', 'sane'])

View File

@ -12,32 +12,32 @@ from collections import namedtuple
from datetime import datetime
from enum import Enum, auto
from functools import reduce
from typing import List, Optional, Tuple
from typing import Iterable, Iterator, List, Optional, Tuple
TestResult = namedtuple('TestResult', ['status','suites','log'])
class TestSuite(object):
def __init__(self):
self.status = None
self.name = None
self.cases = []
def __init__(self) -> None:
self.status = TestStatus.SUCCESS
self.name = ''
self.cases = [] # type: List[TestCase]
def __str__(self):
return 'TestSuite(' + self.status + ',' + self.name + ',' + str(self.cases) + ')'
def __str__(self) -> str:
return 'TestSuite(' + str(self.status) + ',' + self.name + ',' + str(self.cases) + ')'
def __repr__(self):
def __repr__(self) -> str:
return str(self)
class TestCase(object):
def __init__(self):
self.status = None
def __init__(self) -> None:
self.status = TestStatus.SUCCESS
self.name = ''
self.log = []
self.log = [] # type: List[str]
def __str__(self):
return 'TestCase(' + self.status + ',' + self.name + ',' + str(self.log) + ')'
def __str__(self) -> str:
return 'TestCase(' + str(self.status) + ',' + self.name + ',' + str(self.log) + ')'
def __repr__(self):
def __repr__(self) -> str:
return str(self)
class TestStatus(Enum):
@ -51,7 +51,7 @@ kunit_start_re = re.compile(r'TAP version [0-9]+$')
kunit_end_re = re.compile('(List of all partitions:|'
'Kernel panic - not syncing: VFS:)')
def isolate_kunit_output(kernel_output):
def isolate_kunit_output(kernel_output) -> Iterator[str]:
started = False
for line in kernel_output:
line = line.rstrip() # line always has a trailing \n
@ -64,7 +64,7 @@ def isolate_kunit_output(kernel_output):
elif started:
yield line[prefix_len:] if prefix_len > 0 else line
def raw_output(kernel_output):
def raw_output(kernel_output) -> None:
for line in kernel_output:
print(line.rstrip())
@ -72,36 +72,36 @@ DIVIDER = '=' * 60
RESET = '\033[0;0m'
def red(text):
def red(text) -> str:
return '\033[1;31m' + text + RESET
def yellow(text):
def yellow(text) -> str:
return '\033[1;33m' + text + RESET
def green(text):
def green(text) -> str:
return '\033[1;32m' + text + RESET
def print_with_timestamp(message):
def print_with_timestamp(message) -> None:
print('[%s] %s' % (datetime.now().strftime('%H:%M:%S'), message))
def format_suite_divider(message):
def format_suite_divider(message) -> str:
return '======== ' + message + ' ========'
def print_suite_divider(message):
def print_suite_divider(message) -> None:
print_with_timestamp(DIVIDER)
print_with_timestamp(format_suite_divider(message))
def print_log(log):
def print_log(log) -> None:
for m in log:
print_with_timestamp(m)
TAP_ENTRIES = re.compile(r'^(TAP|[\s]*ok|[\s]*not ok|[\s]*[0-9]+\.\.[0-9]+|[\s]*#).*$')
def consume_non_diagnositic(lines: List[str]) -> None:
def consume_non_diagnostic(lines: List[str]) -> None:
while lines and not TAP_ENTRIES.match(lines[0]):
lines.pop(0)
def save_non_diagnositic(lines: List[str], test_case: TestCase) -> None:
def save_non_diagnostic(lines: List[str], test_case: TestCase) -> None:
while lines and not TAP_ENTRIES.match(lines[0]):
test_case.log.append(lines[0])
lines.pop(0)
@ -113,7 +113,7 @@ OK_NOT_OK_SUBTEST = re.compile(r'^[\s]+(ok|not ok) [0-9]+ - (.*)$')
OK_NOT_OK_MODULE = re.compile(r'^(ok|not ok) ([0-9]+) - (.*)$')
def parse_ok_not_ok_test_case(lines: List[str], test_case: TestCase) -> bool:
save_non_diagnositic(lines, test_case)
save_non_diagnostic(lines, test_case)
if not lines:
test_case.status = TestStatus.TEST_CRASHED
return True
@ -139,7 +139,7 @@ SUBTEST_DIAGNOSTIC = re.compile(r'^[\s]+# (.*)$')
DIAGNOSTIC_CRASH_MESSAGE = re.compile(r'^[\s]+# .*?: kunit test case crashed!$')
def parse_diagnostic(lines: List[str], test_case: TestCase) -> bool:
save_non_diagnositic(lines, test_case)
save_non_diagnostic(lines, test_case)
if not lines:
return False
line = lines[0]
@ -155,7 +155,7 @@ def parse_diagnostic(lines: List[str], test_case: TestCase) -> bool:
def parse_test_case(lines: List[str]) -> Optional[TestCase]:
test_case = TestCase()
save_non_diagnositic(lines, test_case)
save_non_diagnostic(lines, test_case)
while parse_diagnostic(lines, test_case):
pass
if parse_ok_not_ok_test_case(lines, test_case):
@ -166,7 +166,7 @@ def parse_test_case(lines: List[str]) -> Optional[TestCase]:
SUBTEST_HEADER = re.compile(r'^[\s]+# Subtest: (.*)$')
def parse_subtest_header(lines: List[str]) -> Optional[str]:
consume_non_diagnositic(lines)
consume_non_diagnostic(lines)
if not lines:
return None
match = SUBTEST_HEADER.match(lines[0])
@ -179,7 +179,7 @@ def parse_subtest_header(lines: List[str]) -> Optional[str]:
SUBTEST_PLAN = re.compile(r'[\s]+[0-9]+\.\.([0-9]+)')
def parse_subtest_plan(lines: List[str]) -> Optional[int]:
consume_non_diagnositic(lines)
consume_non_diagnostic(lines)
match = SUBTEST_PLAN.match(lines[0])
if match:
lines.pop(0)
@ -202,7 +202,7 @@ def max_status(left: TestStatus, right: TestStatus) -> TestStatus:
def parse_ok_not_ok_test_suite(lines: List[str],
test_suite: TestSuite,
expected_suite_index: int) -> bool:
consume_non_diagnositic(lines)
consume_non_diagnostic(lines)
if not lines:
test_suite.status = TestStatus.TEST_CRASHED
return False
@ -224,18 +224,17 @@ def parse_ok_not_ok_test_suite(lines: List[str],
else:
return False
def bubble_up_errors(to_status, status_container_list) -> TestStatus:
status_list = map(to_status, status_container_list)
return reduce(max_status, status_list, TestStatus.SUCCESS)
def bubble_up_errors(statuses: Iterable[TestStatus]) -> TestStatus:
return reduce(max_status, statuses, TestStatus.SUCCESS)
def bubble_up_test_case_errors(test_suite: TestSuite) -> TestStatus:
max_test_case_status = bubble_up_errors(lambda x: x.status, test_suite.cases)
max_test_case_status = bubble_up_errors(x.status for x in test_suite.cases)
return max_status(max_test_case_status, test_suite.status)
def parse_test_suite(lines: List[str], expected_suite_index: int) -> Optional[TestSuite]:
if not lines:
return None
consume_non_diagnositic(lines)
consume_non_diagnostic(lines)
test_suite = TestSuite()
test_suite.status = TestStatus.SUCCESS
name = parse_subtest_header(lines)
@ -264,7 +263,7 @@ def parse_test_suite(lines: List[str], expected_suite_index: int) -> Optional[Te
TAP_HEADER = re.compile(r'^TAP version 14$')
def parse_tap_header(lines: List[str]) -> bool:
consume_non_diagnositic(lines)
consume_non_diagnostic(lines)
if TAP_HEADER.match(lines[0]):
lines.pop(0)
return True
@ -274,7 +273,7 @@ def parse_tap_header(lines: List[str]) -> bool:
TEST_PLAN = re.compile(r'[0-9]+\.\.([0-9]+)')
def parse_test_plan(lines: List[str]) -> Optional[int]:
consume_non_diagnositic(lines)
consume_non_diagnostic(lines)
match = TEST_PLAN.match(lines[0])
if match:
lines.pop(0)
@ -282,11 +281,11 @@ def parse_test_plan(lines: List[str]) -> Optional[int]:
else:
return None
def bubble_up_suite_errors(test_suite_list: List[TestSuite]) -> TestStatus:
return bubble_up_errors(lambda x: x.status, test_suite_list)
def bubble_up_suite_errors(test_suites: Iterable[TestSuite]) -> TestStatus:
return bubble_up_errors(x.status for x in test_suites)
def parse_test_result(lines: List[str]) -> TestResult:
consume_non_diagnositic(lines)
consume_non_diagnostic(lines)
if not lines or not parse_tap_header(lines):
return TestResult(TestStatus.NO_TESTS, [], lines)
expected_test_suite_num = parse_test_plan(lines)