Commit bf4eebf8 authored by Linus Torvalds's avatar Linus Torvalds

Merge tag 'linux-kselftest-kunit-5.17-rc1' of...

Merge tag 'linux-kselftest-kunit-5.17-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/shuah/linux-kselftest

Pull KUnit updates from Shuah Khan:
 "This consists of several fixes and enhancements. A few highlights:

   - Option --kconfig_add option allows easily tweaking kunitconfigs

   - make build subcommand can reconfigure if needed

   - doesn't error on tests without test plans

   - doesn't crash if no parameters are generated

   - defaults --jobs to # of cups

   - reports test parameter results as (K)TAP subtests"

* tag 'linux-kselftest-kunit-5.17-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/shuah/linux-kselftest:
  kunit: tool: Default --jobs to number of CPUs
  kunit: tool: fix newly introduced typechecker errors
  kunit: tool: make `build` subcommand also reconfigure if needed
  kunit: tool: delete kunit_parser.TestResult type
  kunit: tool: use dataclass instead of collections.namedtuple
  kunit: tool: suggest using decode_stacktrace.sh on kernel crash
  kunit: tool: reconfigure when the used kunitconfig changes
  kunit: tool: revamp message for invalid kunitconfig
  kunit: tool: add --kconfig_add to allow easily tweaking kunitconfigs
  kunit: tool: move Kconfig read_from_file/parse_from_string to package-level
  kunit: tool: print parsed test results fully incrementally
  kunit: Report test parameter results as (K)TAP subtests
  kunit: Don't crash if no parameters are generated
  kunit: tool: Report an error if any test has no subtests
  kunit: tool: Do not error on tests without test plans
  kunit: add run_checks.py script to validate kunit changes
  Documentation: kunit: remove claims that kunit is a mocking framework
  kunit: tool: fix --json output for skipped tests
parents 4369b3ce ad659ccb
...@@ -12,5 +12,4 @@ following sections: ...@@ -12,5 +12,4 @@ following sections:
Documentation/dev-tools/kunit/api/test.rst Documentation/dev-tools/kunit/api/test.rst
- documents all of the standard testing API excluding mocking - documents all of the standard testing API
or mocking related features.
...@@ -4,8 +4,7 @@ ...@@ -4,8 +4,7 @@
Test API Test API
======== ========
This file documents all of the standard testing API excluding mocking or mocking This file documents all of the standard testing API.
related features.
.. kernel-doc:: include/kunit/test.h .. kernel-doc:: include/kunit/test.h
:internal: :internal:
...@@ -19,7 +19,7 @@ KUnit - Unit Testing for the Linux Kernel ...@@ -19,7 +19,7 @@ KUnit - Unit Testing for the Linux Kernel
What is KUnit? What is KUnit?
============== ==============
KUnit is a lightweight unit testing and mocking framework for the Linux kernel. KUnit is a lightweight unit testing framework for the Linux kernel.
KUnit is heavily inspired by JUnit, Python's unittest.mock, and KUnit is heavily inspired by JUnit, Python's unittest.mock, and
Googletest/Googlemock for C++. KUnit provides facilities for defining unit test Googletest/Googlemock for C++. KUnit provides facilities for defining unit test
......
...@@ -50,10 +50,10 @@ It'll warn you if you haven't included the dependencies of the options you're ...@@ -50,10 +50,10 @@ It'll warn you if you haven't included the dependencies of the options you're
using. using.
.. note:: .. note::
Note that removing something from the ``.kunitconfig`` will not trigger a If you change the ``.kunitconfig``, kunit.py will trigger a rebuild of the
rebuild of the ``.config`` file: the configuration is only updated if the ``.config`` file. But you can edit the ``.config`` file directly or with
``.kunitconfig`` is not a subset of ``.config``. This means that you can use tools like ``make menuconfig O=.kunit``. As long as its a superset of
other tools (such as make menuconfig) to adjust other config options. ``.kunitconfig``, kunit.py won't overwrite your changes.
Running the tests (KUnit Wrapper) Running the tests (KUnit Wrapper)
......
...@@ -504,25 +504,28 @@ int kunit_run_tests(struct kunit_suite *suite) ...@@ -504,25 +504,28 @@ int kunit_run_tests(struct kunit_suite *suite)
struct kunit_result_stats param_stats = { 0 }; struct kunit_result_stats param_stats = { 0 };
test_case->status = KUNIT_SKIPPED; test_case->status = KUNIT_SKIPPED;
if (test_case->generate_params) { if (!test_case->generate_params) {
/* Non-parameterised test. */
kunit_run_case_catch_errors(suite, test_case, &test);
kunit_update_stats(&param_stats, test.status);
} else {
/* Get initial param. */ /* Get initial param. */
param_desc[0] = '\0'; param_desc[0] = '\0';
test.param_value = test_case->generate_params(NULL, param_desc); test.param_value = test_case->generate_params(NULL, param_desc);
} kunit_log(KERN_INFO, &test, KUNIT_SUBTEST_INDENT KUNIT_SUBTEST_INDENT
"# Subtest: %s", test_case->name);
do { while (test.param_value) {
kunit_run_case_catch_errors(suite, test_case, &test); kunit_run_case_catch_errors(suite, test_case, &test);
if (test_case->generate_params) {
if (param_desc[0] == '\0') { if (param_desc[0] == '\0') {
snprintf(param_desc, sizeof(param_desc), snprintf(param_desc, sizeof(param_desc),
"param-%d", test.param_index); "param-%d", test.param_index);
} }
kunit_log(KERN_INFO, &test, kunit_log(KERN_INFO, &test,
KUNIT_SUBTEST_INDENT KUNIT_SUBTEST_INDENT KUNIT_SUBTEST_INDENT
"# %s: %s %d - %s", "%s %d - %s",
test_case->name,
kunit_status_to_ok_not_ok(test.status), kunit_status_to_ok_not_ok(test.status),
test.param_index + 1, param_desc); test.param_index + 1, param_desc);
...@@ -530,11 +533,11 @@ int kunit_run_tests(struct kunit_suite *suite) ...@@ -530,11 +533,11 @@ int kunit_run_tests(struct kunit_suite *suite)
param_desc[0] = '\0'; param_desc[0] = '\0';
test.param_value = test_case->generate_params(test.param_value, param_desc); test.param_value = test_case->generate_params(test.param_value, param_desc);
test.param_index++; test.param_index++;
}
kunit_update_stats(&param_stats, test.status); kunit_update_stats(&param_stats, test.status);
}
}
} while (test.param_value);
kunit_print_test_stats(&test, param_stats); kunit_print_test_stats(&test, param_stats);
......
This diff is collapsed.
...@@ -62,33 +62,34 @@ class Kconfig(object): ...@@ -62,33 +62,34 @@ class Kconfig(object):
for entry in self.entries(): for entry in self.entries():
f.write(str(entry) + '\n') f.write(str(entry) + '\n')
def parse_from_string(self, blob: str) -> None: def parse_file(path: str) -> Kconfig:
"""Parses a string containing KconfigEntrys and populates this Kconfig.""" with open(path, 'r') as f:
self._entries = [] return parse_from_string(f.read())
is_not_set_matcher = re.compile(CONFIG_IS_NOT_SET_PATTERN)
config_matcher = re.compile(CONFIG_PATTERN) def parse_from_string(blob: str) -> Kconfig:
for line in blob.split('\n'): """Parses a string containing Kconfig entries."""
line = line.strip() kconfig = Kconfig()
if not line: is_not_set_matcher = re.compile(CONFIG_IS_NOT_SET_PATTERN)
continue config_matcher = re.compile(CONFIG_PATTERN)
for line in blob.split('\n'):
match = config_matcher.match(line) line = line.strip()
if match: if not line:
entry = KconfigEntry(match.group(1), match.group(2)) continue
self.add_entry(entry)
continue match = config_matcher.match(line)
if match:
empty_match = is_not_set_matcher.match(line) entry = KconfigEntry(match.group(1), match.group(2))
if empty_match: kconfig.add_entry(entry)
entry = KconfigEntry(empty_match.group(1), 'n') continue
self.add_entry(entry)
continue empty_match = is_not_set_matcher.match(line)
if empty_match:
if line[0] == '#': entry = KconfigEntry(empty_match.group(1), 'n')
continue kconfig.add_entry(entry)
else: continue
raise KconfigParseError('Failed to parse: ' + line)
if line[0] == '#':
def read_from_file(self, path: str) -> None: continue
with open(path, 'r') as f: else:
self.parse_from_string(f.read()) raise KconfigParseError('Failed to parse: ' + line)
return kconfig
...@@ -11,7 +11,7 @@ import os ...@@ -11,7 +11,7 @@ import os
import kunit_parser import kunit_parser
from kunit_parser import Test, TestResult, TestStatus from kunit_parser import Test, TestStatus
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
JsonObj = Dict[str, Any] JsonObj = Dict[str, Any]
...@@ -30,6 +30,8 @@ def _get_group_json(test: Test, def_config: str, ...@@ -30,6 +30,8 @@ def _get_group_json(test: Test, def_config: str,
test_case = {"name": subtest.name, "status": "FAIL"} test_case = {"name": subtest.name, "status": "FAIL"}
if subtest.status == TestStatus.SUCCESS: if subtest.status == TestStatus.SUCCESS:
test_case["status"] = "PASS" test_case["status"] = "PASS"
elif subtest.status == TestStatus.SKIPPED:
test_case["status"] = "SKIP"
elif subtest.status == TestStatus.TEST_CRASHED: elif subtest.status == TestStatus.TEST_CRASHED:
test_case["status"] = "ERROR" test_case["status"] = "ERROR"
test_cases.append(test_case) test_cases.append(test_case)
...@@ -48,9 +50,9 @@ def _get_group_json(test: Test, def_config: str, ...@@ -48,9 +50,9 @@ def _get_group_json(test: Test, def_config: str,
} }
return test_group return test_group
def get_json_result(test_result: TestResult, def_config: str, def get_json_result(test: Test, def_config: str,
build_dir: Optional[str], json_path: str) -> str: build_dir: Optional[str], json_path: str) -> str:
test_group = _get_group_json(test_result.test, def_config, build_dir) test_group = _get_group_json(test, def_config, build_dir)
test_group["name"] = "KUnit Test Group" test_group["name"] = "KUnit Test Group"
json_obj = json.dumps(test_group, indent=4) json_obj = json.dumps(test_group, indent=4)
if json_path != 'stdout': if json_path != 'stdout':
......
...@@ -21,6 +21,7 @@ import qemu_config ...@@ -21,6 +21,7 @@ import qemu_config
KCONFIG_PATH = '.config' KCONFIG_PATH = '.config'
KUNITCONFIG_PATH = '.kunitconfig' KUNITCONFIG_PATH = '.kunitconfig'
OLD_KUNITCONFIG_PATH = 'last_used_kunitconfig'
DEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config' DEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config'
BROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config' BROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config'
OUTFILE_PATH = 'test.log' OUTFILE_PATH = 'test.log'
...@@ -116,8 +117,7 @@ class LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations): ...@@ -116,8 +117,7 @@ class LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations):
self._extra_qemu_params = qemu_arch_params.extra_qemu_params self._extra_qemu_params = qemu_arch_params.extra_qemu_params
def make_arch_qemuconfig(self, base_kunitconfig: kunit_config.Kconfig) -> None: def make_arch_qemuconfig(self, base_kunitconfig: kunit_config.Kconfig) -> None:
kconfig = kunit_config.Kconfig() kconfig = kunit_config.parse_from_string(self._kconfig)
kconfig.parse_from_string(self._kconfig)
base_kunitconfig.merge_in_entries(kconfig) base_kunitconfig.merge_in_entries(kconfig)
def start(self, params: List[str], build_dir: str) -> subprocess.Popen: def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
...@@ -180,6 +180,9 @@ def get_kconfig_path(build_dir) -> str: ...@@ -180,6 +180,9 @@ def get_kconfig_path(build_dir) -> str:
def get_kunitconfig_path(build_dir) -> str: def get_kunitconfig_path(build_dir) -> str:
return get_file_path(build_dir, KUNITCONFIG_PATH) return get_file_path(build_dir, KUNITCONFIG_PATH)
def get_old_kunitconfig_path(build_dir) -> str:
return get_file_path(build_dir, OLD_KUNITCONFIG_PATH)
def get_outfile_path(build_dir) -> str: def get_outfile_path(build_dir) -> str:
return get_file_path(build_dir, OUTFILE_PATH) return get_file_path(build_dir, OUTFILE_PATH)
...@@ -206,6 +209,7 @@ def get_source_tree_ops_from_qemu_config(config_path: str, ...@@ -206,6 +209,7 @@ def get_source_tree_ops_from_qemu_config(config_path: str,
# exists as a file. # exists as a file.
module_path = '.' + os.path.join(os.path.basename(QEMU_CONFIGS_DIR), os.path.basename(config_path)) module_path = '.' + os.path.join(os.path.basename(QEMU_CONFIGS_DIR), os.path.basename(config_path))
spec = importlib.util.spec_from_file_location(module_path, config_path) spec = importlib.util.spec_from_file_location(module_path, config_path)
assert spec is not None
config = importlib.util.module_from_spec(spec) config = importlib.util.module_from_spec(spec)
# See https://github.com/python/typeshed/pull/2626 for context. # See https://github.com/python/typeshed/pull/2626 for context.
assert isinstance(spec.loader, importlib.abc.Loader) assert isinstance(spec.loader, importlib.abc.Loader)
...@@ -225,6 +229,7 @@ class LinuxSourceTree(object): ...@@ -225,6 +229,7 @@ class LinuxSourceTree(object):
build_dir: str, build_dir: str,
load_config=True, load_config=True,
kunitconfig_path='', kunitconfig_path='',
kconfig_add: Optional[List[str]]=None,
arch=None, arch=None,
cross_compile=None, cross_compile=None,
qemu_config_path=None) -> None: qemu_config_path=None) -> None:
...@@ -249,8 +254,11 @@ class LinuxSourceTree(object): ...@@ -249,8 +254,11 @@ class LinuxSourceTree(object):
if not os.path.exists(kunitconfig_path): if not os.path.exists(kunitconfig_path):
shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, kunitconfig_path) shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, kunitconfig_path)
self._kconfig = kunit_config.Kconfig() self._kconfig = kunit_config.parse_file(kunitconfig_path)
self._kconfig.read_from_file(kunitconfig_path) if kconfig_add:
kconfig = kunit_config.parse_from_string('\n'.join(kconfig_add))
self._kconfig.merge_in_entries(kconfig)
def clean(self) -> bool: def clean(self) -> bool:
try: try:
...@@ -262,17 +270,18 @@ class LinuxSourceTree(object): ...@@ -262,17 +270,18 @@ class LinuxSourceTree(object):
def validate_config(self, build_dir) -> bool: def validate_config(self, build_dir) -> bool:
kconfig_path = get_kconfig_path(build_dir) kconfig_path = get_kconfig_path(build_dir)
validated_kconfig = kunit_config.Kconfig() validated_kconfig = kunit_config.parse_file(kconfig_path)
validated_kconfig.read_from_file(kconfig_path) if self._kconfig.is_subset_of(validated_kconfig):
if not self._kconfig.is_subset_of(validated_kconfig): return True
invalid = self._kconfig.entries() - validated_kconfig.entries() invalid = self._kconfig.entries() - validated_kconfig.entries()
message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \ message = 'Not all Kconfig options selected in kunitconfig were in the generated .config.\n' \
'but not in .config: %s' % ( 'This is probably due to unsatisfied dependencies.\n' \
', '.join([str(e) for e in invalid]) 'Missing: ' + ', '.join([str(e) for e in invalid])
) if self._arch == 'um':
logging.error(message) message += '\nNote: many Kconfig options aren\'t available on UML. You can try running ' \
return False 'on a different architecture with something like "--arch=x86_64".'
return True logging.error(message)
return False
def build_config(self, build_dir, make_options) -> bool: def build_config(self, build_dir, make_options) -> bool:
kconfig_path = get_kconfig_path(build_dir) kconfig_path = get_kconfig_path(build_dir)
...@@ -285,25 +294,38 @@ class LinuxSourceTree(object): ...@@ -285,25 +294,38 @@ class LinuxSourceTree(object):
except ConfigError as e: except ConfigError as e:
logging.error(e) logging.error(e)
return False return False
return self.validate_config(build_dir) if not self.validate_config(build_dir):
return False
old_path = get_old_kunitconfig_path(build_dir)
if os.path.exists(old_path):
os.remove(old_path) # write_to_file appends to the file
self._kconfig.write_to_file(old_path)
return True
def _kunitconfig_changed(self, build_dir: str) -> bool:
old_path = get_old_kunitconfig_path(build_dir)
if not os.path.exists(old_path):
return True
old_kconfig = kunit_config.parse_file(old_path)
return old_kconfig.entries() != self._kconfig.entries()
def build_reconfig(self, build_dir, make_options) -> bool: def build_reconfig(self, build_dir, make_options) -> bool:
"""Creates a new .config if it is not a subset of the .kunitconfig.""" """Creates a new .config if it is not a subset of the .kunitconfig."""
kconfig_path = get_kconfig_path(build_dir) kconfig_path = get_kconfig_path(build_dir)
if os.path.exists(kconfig_path): if not os.path.exists(kconfig_path):
existing_kconfig = kunit_config.Kconfig()
existing_kconfig.read_from_file(kconfig_path)
self._ops.make_arch_qemuconfig(self._kconfig)
if not self._kconfig.is_subset_of(existing_kconfig):
print('Regenerating .config ...')
os.remove(kconfig_path)
return self.build_config(build_dir, make_options)
else:
return True
else:
print('Generating .config ...') print('Generating .config ...')
return self.build_config(build_dir, make_options) return self.build_config(build_dir, make_options)
existing_kconfig = kunit_config.parse_file(kconfig_path)
self._ops.make_arch_qemuconfig(self._kconfig)
if self._kconfig.is_subset_of(existing_kconfig) and not self._kunitconfig_changed(build_dir):
return True
print('Regenerating .config ...')
os.remove(kconfig_path)
return self.build_config(build_dir, make_options)
def build_kernel(self, alltests, jobs, build_dir, make_options) -> bool: def build_kernel(self, alltests, jobs, build_dir, make_options) -> bool:
try: try:
if alltests: if alltests:
......
...@@ -12,14 +12,11 @@ ...@@ -12,14 +12,11 @@
from __future__ import annotations from __future__ import annotations
import re import re
from collections import namedtuple import datetime
from datetime import datetime
from enum import Enum, auto from enum import Enum, auto
from functools import reduce from functools import reduce
from typing import Iterable, Iterator, List, Optional, Tuple from typing import Iterable, Iterator, List, Optional, Tuple
TestResult = namedtuple('TestResult', ['status','test','log'])
class Test(object): class Test(object):
""" """
A class to represent a test parsed from KTAP results. All KTAP A class to represent a test parsed from KTAP results. All KTAP
...@@ -168,42 +165,51 @@ class TestCounts: ...@@ -168,42 +165,51 @@ class TestCounts:
class LineStream: class LineStream:
""" """
A class to represent the lines of kernel output. A class to represent the lines of kernel output.
Provides a peek()/pop() interface over an iterator of Provides a lazy peek()/pop() interface over an iterator of
(line#, text). (line#, text).
""" """
_lines: Iterator[Tuple[int, str]] _lines: Iterator[Tuple[int, str]]
_next: Tuple[int, str] _next: Tuple[int, str]
_need_next: bool
_done: bool _done: bool
def __init__(self, lines: Iterator[Tuple[int, str]]): def __init__(self, lines: Iterator[Tuple[int, str]]):
"""Creates a new LineStream that wraps the given iterator.""" """Creates a new LineStream that wraps the given iterator."""
self._lines = lines self._lines = lines
self._done = False self._done = False
self._need_next = True
self._next = (0, '') self._next = (0, '')
self._get_next()
def _get_next(self) -> None: def _get_next(self) -> None:
"""Advances the LineSteam to the next line.""" """Advances the LineSteam to the next line, if necessary."""
if not self._need_next:
return
try: try:
self._next = next(self._lines) self._next = next(self._lines)
except StopIteration: except StopIteration:
self._done = True self._done = True
finally:
self._need_next = False
def peek(self) -> str: def peek(self) -> str:
"""Returns the current line, without advancing the LineStream. """Returns the current line, without advancing the LineStream.
""" """
self._get_next()
return self._next[1] return self._next[1]
def pop(self) -> str: def pop(self) -> str:
"""Returns the current line and advances the LineStream to """Returns the current line and advances the LineStream to
the next line. the next line.
""" """
n = self._next s = self.peek()
self._get_next() if self._done:
return n[1] raise ValueError(f'LineStream: going past EOF, last line was {s}')
self._need_next = True
return s
def __bool__(self) -> bool: def __bool__(self) -> bool:
"""Returns True if stream has more lines.""" """Returns True if stream has more lines."""
self._get_next()
return not self._done return not self._done
# Only used by kunit_tool_test.py. # Only used by kunit_tool_test.py.
...@@ -216,6 +222,7 @@ class LineStream: ...@@ -216,6 +222,7 @@ class LineStream:
def line_number(self) -> int: def line_number(self) -> int:
"""Returns the line number of the current line.""" """Returns the line number of the current line."""
self._get_next()
return self._next[0] return self._next[0]
# Parsing helper methods: # Parsing helper methods:
...@@ -340,8 +347,8 @@ def parse_test_plan(lines: LineStream, test: Test) -> bool: ...@@ -340,8 +347,8 @@ def parse_test_plan(lines: LineStream, test: Test) -> bool:
""" """
Parses test plan line and stores the expected number of subtests in Parses test plan line and stores the expected number of subtests in
test object. Reports an error if expected count is 0. test object. Reports an error if expected count is 0.
Returns False and reports missing test plan error if fails to parse Returns False and sets expected_count to None if there is no valid test
test plan. plan.
Accepted format: Accepted format:
- '1..[number of subtests]' - '1..[number of subtests]'
...@@ -356,14 +363,10 @@ def parse_test_plan(lines: LineStream, test: Test) -> bool: ...@@ -356,14 +363,10 @@ def parse_test_plan(lines: LineStream, test: Test) -> bool:
match = TEST_PLAN.match(lines.peek()) match = TEST_PLAN.match(lines.peek())
if not match: if not match:
test.expected_count = None test.expected_count = None
test.add_error('missing plan line!')
return False return False
test.log.append(lines.pop()) test.log.append(lines.pop())
expected_count = int(match.group(1)) expected_count = int(match.group(1))
test.expected_count = expected_count test.expected_count = expected_count
if expected_count == 0:
test.status = TestStatus.NO_TESTS
test.add_error('0 tests run!')
return True return True
TEST_RESULT = re.compile(r'^(ok|not ok) ([0-9]+) (- )?([^#]*)( # .*)?$') TEST_RESULT = re.compile(r'^(ok|not ok) ([0-9]+) (- )?([^#]*)( # .*)?$')
...@@ -514,7 +517,7 @@ ANSI_LEN = len(red('')) ...@@ -514,7 +517,7 @@ ANSI_LEN = len(red(''))
def print_with_timestamp(message: str) -> None: def print_with_timestamp(message: str) -> None:
"""Prints message with timestamp at beginning.""" """Prints message with timestamp at beginning."""
print('[%s] %s' % (datetime.now().strftime('%H:%M:%S'), message)) print('[%s] %s' % (datetime.datetime.now().strftime('%H:%M:%S'), message))
def format_test_divider(message: str, len_message: int) -> str: def format_test_divider(message: str, len_message: int) -> str:
""" """
...@@ -590,6 +593,8 @@ def format_test_result(test: Test) -> str: ...@@ -590,6 +593,8 @@ def format_test_result(test: Test) -> str:
return (green('[PASSED] ') + test.name) return (green('[PASSED] ') + test.name)
elif test.status == TestStatus.SKIPPED: elif test.status == TestStatus.SKIPPED:
return (yellow('[SKIPPED] ') + test.name) return (yellow('[SKIPPED] ') + test.name)
elif test.status == TestStatus.NO_TESTS:
return (yellow('[NO TESTS RUN] ') + test.name)
elif test.status == TestStatus.TEST_CRASHED: elif test.status == TestStatus.TEST_CRASHED:
print_log(test.log) print_log(test.log)
return (red('[CRASHED] ') + test.name) return (red('[CRASHED] ') + test.name)
...@@ -732,6 +737,7 @@ def parse_test(lines: LineStream, expected_num: int, log: List[str]) -> Test: ...@@ -732,6 +737,7 @@ def parse_test(lines: LineStream, expected_num: int, log: List[str]) -> Test:
# test plan # test plan
test.name = "main" test.name = "main"
parse_test_plan(lines, test) parse_test_plan(lines, test)
parent_test = True
else: else:
# If KTAP/TAP header is not found, test must be subtest # If KTAP/TAP header is not found, test must be subtest
# header or test result line so parse attempt to parser # header or test result line so parse attempt to parser
...@@ -745,7 +751,7 @@ def parse_test(lines: LineStream, expected_num: int, log: List[str]) -> Test: ...@@ -745,7 +751,7 @@ def parse_test(lines: LineStream, expected_num: int, log: List[str]) -> Test:
expected_count = test.expected_count expected_count = test.expected_count
subtests = [] subtests = []
test_num = 1 test_num = 1
while expected_count is None or test_num <= expected_count: while parent_test and (expected_count is None or test_num <= expected_count):
# Loop to parse any subtests. # Loop to parse any subtests.
# Break after parsing expected number of tests or # Break after parsing expected number of tests or
# if expected number of tests is unknown break when test # if expected number of tests is unknown break when test
...@@ -780,9 +786,15 @@ def parse_test(lines: LineStream, expected_num: int, log: List[str]) -> Test: ...@@ -780,9 +786,15 @@ def parse_test(lines: LineStream, expected_num: int, log: List[str]) -> Test:
parse_test_result(lines, test, expected_num) parse_test_result(lines, test, expected_num)
else: else:
test.add_error('missing subtest result line!') test.add_error('missing subtest result line!')
# Check for there being no tests
if parent_test and len(subtests) == 0:
test.status = TestStatus.NO_TESTS
test.add_error('0 tests run!')
# Add statuses to TestCounts attribute in Test object # Add statuses to TestCounts attribute in Test object
bubble_up_test_results(test) bubble_up_test_results(test)
if parent_test: if parent_test and not main:
# If test has subtests and is not the main test object, print # If test has subtests and is not the main test object, print
# footer. # footer.
print_test_footer(test) print_test_footer(test)
...@@ -790,7 +802,7 @@ def parse_test(lines: LineStream, expected_num: int, log: List[str]) -> Test: ...@@ -790,7 +802,7 @@ def parse_test(lines: LineStream, expected_num: int, log: List[str]) -> Test:
print_test_result(test) print_test_result(test)
return test return test
def parse_run_tests(kernel_output: Iterable[str]) -> TestResult: def parse_run_tests(kernel_output: Iterable[str]) -> Test:
""" """
Using kernel output, extract KTAP lines, parse the lines for test Using kernel output, extract KTAP lines, parse the lines for test
results and print condensed test results and summary line . results and print condensed test results and summary line .
...@@ -799,8 +811,7 @@ def parse_run_tests(kernel_output: Iterable[str]) -> TestResult: ...@@ -799,8 +811,7 @@ def parse_run_tests(kernel_output: Iterable[str]) -> TestResult:
kernel_output - Iterable object contains lines of kernel output kernel_output - Iterable object contains lines of kernel output
Return: Return:
TestResult - Tuple containg status of main test object, main test Test - the main test object with all subtests.
object with all subtests, and log of all KTAP lines.
""" """
print_with_timestamp(DIVIDER) print_with_timestamp(DIVIDER)
lines = extract_tap_lines(kernel_output) lines = extract_tap_lines(kernel_output)
...@@ -814,4 +825,4 @@ def parse_run_tests(kernel_output: Iterable[str]) -> TestResult: ...@@ -814,4 +825,4 @@ def parse_run_tests(kernel_output: Iterable[str]) -> TestResult:
test.status = test.counts.get_status() test.status = test.counts.get_status()
print_with_timestamp(DIVIDER) print_with_timestamp(DIVIDER)
print_summary_line(test) print_summary_line(test)
return TestResult(test.status, test, lines) return test
This diff is collapsed.
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
#
# This file runs some basic checks to verify kunit works.
# It is only of interest if you're making changes to KUnit itself.
#
# Copyright (C) 2021, Google LLC.
# Author: Daniel Latypov <dlatypov@google.com.com>
from concurrent import futures
import datetime
import os
import shutil
import subprocess
import sys
import textwrap
from typing import Dict, List, Sequence, Tuple
ABS_TOOL_PATH = os.path.abspath(os.path.dirname(__file__))
TIMEOUT = datetime.timedelta(minutes=5).total_seconds()
commands: Dict[str, Sequence[str]] = {
'kunit_tool_test.py': ['./kunit_tool_test.py'],
'kunit smoke test': ['./kunit.py', 'run', '--kunitconfig=lib/kunit', '--build_dir=kunit_run_checks'],
'pytype': ['/bin/sh', '-c', 'pytype *.py'],
'mypy': ['/bin/sh', '-c', 'mypy *.py'],
}
# The user might not have mypy or pytype installed, skip them if so.
# Note: you can install both via `$ pip install mypy pytype`
necessary_deps : Dict[str, str] = {
'pytype': 'pytype',
'mypy': 'mypy',
}
def main(argv: Sequence[str]) -> None:
if argv:
raise RuntimeError('This script takes no arguments')
future_to_name: Dict[futures.Future, str] = {}
executor = futures.ThreadPoolExecutor(max_workers=len(commands))
for name, argv in commands.items():
if name in necessary_deps and shutil.which(necessary_deps[name]) is None:
print(f'{name}: SKIPPED, {necessary_deps[name]} not in $PATH')
continue
f = executor.submit(run_cmd, argv)
future_to_name[f] = name
has_failures = False
print(f'Waiting on {len(future_to_name)} checks ({", ".join(future_to_name.values())})...')
for f in futures.as_completed(future_to_name.keys()):
name = future_to_name[f]
ex = f.exception()
if not ex:
print(f'{name}: PASSED')
continue
has_failures = True
if isinstance(ex, subprocess.TimeoutExpired):
print(f'{name}: TIMED OUT')
elif isinstance(ex, subprocess.CalledProcessError):
print(f'{name}: FAILED')
else:
print('{name}: unexpected exception: {ex}')
continue
output = ex.output
if output:
print(textwrap.indent(output.decode(), '> '))
executor.shutdown()
if has_failures:
sys.exit(1)
def run_cmd(argv: Sequence[str]):
subprocess.check_output(argv, stderr=subprocess.STDOUT, cwd=ABS_TOOL_PATH, timeout=TIMEOUT)
if __name__ == '__main__':
main(sys.argv[1:])
TAP version 14
1..1
# Subtest: suite
1..1
# Subtest: case
ok 1 - case # SKIP
ok 1 - suite
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment