diff mbox series

[1/3] kunit: Support skipped tests

Message ID 20210526081112.3652290-1-davidgow@google.com
State Superseded
Headers show
Series [1/3] kunit: Support skipped tests | expand

Commit Message

David Gow May 26, 2021, 8:11 a.m. UTC
The kunit_mark_skipped() macro marks the current test as "skipped", with
the provided reason. The kunit_skip() macro will mark the test as
skipped, and abort the test.

The TAP specification supports this "SKIP directive" as a comment after
the "ok" / "not ok" for a test. See the "Directives" section of the TAP
spec for details:
https://testanything.org/tap-specification.html#directives

The 'success' field for KUnit tests is replaced with a kunit_status
enum, which can be SUCCESS, FAILURE, or SKIPPED, combined with a
'status_comment' containing information on why a test was skipped.

A new 'kunit_status' test suite is added to test this.

Signed-off-by: David Gow <davidgow@google.com>
---
This change depends on the assertion typechecking fix here:
https://lore.kernel.org/linux-kselftest/20210513193204.816681-1-davidgow@google.com/
Only the first two patches in the series are required.

This is the long-awaited follow-up to the skip tests RFC:
https://lore.kernel.org/linux-kselftest/20200513042956.109987-1-davidgow@google.com/

There are quite a few changes since that version, principally:
- A kunit_status enum is now used, with SKIPPED a distinct state
- The kunit_mark_skipped() and kunit_skip() macros now take printf-style
  format strings.
- There is now a kunit_status test suite providing basic tests of this
  functionality.
- The kunit_tool changes have been split into a separate commit.
- The example skipped tests have been expanded an moved to their own
  suite, which is not enabled by KUNIT_ALL_TESTS.
- A number of other fixes and changes here and there.

Cheers,
-- David

 include/kunit/test.h   | 68 ++++++++++++++++++++++++++++++++++++++----
 lib/kunit/kunit-test.c | 42 +++++++++++++++++++++++++-
 lib/kunit/test.c       | 51 ++++++++++++++++++-------------
 3 files changed, 134 insertions(+), 27 deletions(-)

Comments

Marco Elver May 26, 2021, 9:03 a.m. UTC | #1
On Wed, May 26, 2021 at 01:11AM -0700, David Gow wrote:
> The kunit_mark_skipped() macro marks the current test as "skipped", with
> the provided reason. The kunit_skip() macro will mark the test as
> skipped, and abort the test.
> 
> The TAP specification supports this "SKIP directive" as a comment after
> the "ok" / "not ok" for a test. See the "Directives" section of the TAP
> spec for details:
> https://testanything.org/tap-specification.html#directives
> 
> The 'success' field for KUnit tests is replaced with a kunit_status
> enum, which can be SUCCESS, FAILURE, or SKIPPED, combined with a
> 'status_comment' containing information on why a test was skipped.
> 
> A new 'kunit_status' test suite is added to test this.
> 
> Signed-off-by: David Gow <davidgow@google.com>
[...]
>  include/kunit/test.h   | 68 ++++++++++++++++++++++++++++++++++++++----
>  lib/kunit/kunit-test.c | 42 +++++++++++++++++++++++++-
>  lib/kunit/test.c       | 51 ++++++++++++++++++-------------
>  3 files changed, 134 insertions(+), 27 deletions(-)

Very nice, thank you.

	Tested-by: Marco Elver <elver@google.com>

, with the below changes to test_kasan.c. If you would like an immediate
user of kunit_skip(), please feel free to add the below patch to your
series.

Thanks,
-- Marco

------ >8 ------

From: Marco Elver <elver@google.com>
Date: Wed, 26 May 2021 10:43:12 +0200
Subject: [PATCH] kasan: test: make use of kunit_skip()

Make use of the recently added kunit_skip() to skip tests, as it permits
TAP parsers to recognize if a test was deliberately skipped.

Signed-off-by: Marco Elver <elver@google.com>
---
 lib/test_kasan.c | 12 ++++--------
 1 file changed, 4 insertions(+), 8 deletions(-)

diff --git a/lib/test_kasan.c b/lib/test_kasan.c
index cacbbbdef768..0a2029d14c91 100644
--- a/lib/test_kasan.c
+++ b/lib/test_kasan.c
@@ -111,17 +111,13 @@ static void kasan_test_exit(struct kunit *test)
 } while (0)
 
 #define KASAN_TEST_NEEDS_CONFIG_ON(test, config) do {			\
-	if (!IS_ENABLED(config)) {					\
-		kunit_info((test), "skipping, " #config " required");	\
-		return;							\
-	}								\
+	if (!IS_ENABLED(config))					\
+		kunit_skip((test), "Test requires " #config "=y");	\
 } while (0)
 
 #define KASAN_TEST_NEEDS_CONFIG_OFF(test, config) do {			\
-	if (IS_ENABLED(config)) {					\
-		kunit_info((test), "skipping, " #config " enabled");	\
-		return;							\
-	}								\
+	if (IS_ENABLED(config))						\
+		kunit_skip((test), "Test requires " #config "=n");	\
 } while (0)
 
 static void kmalloc_oob_right(struct kunit *test)
kernel test robot May 26, 2021, 12:03 p.m. UTC | #2
Hi David,

I love your patch! Yet something to improve:

[auto build test ERROR on linus/master]
[also build test ERROR on v5.13-rc3 next-20210526]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch]

url:    https://github.com/0day-ci/linux/commits/David-Gow/kunit-Support-skipped-tests/20210526-161324
base:   https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git ad9f25d338605d26acedcaf3ba5fab5ca26f1c10
config: x86_64-randconfig-r025-20210526 (attached as .config)
compiler: clang version 13.0.0 (https://github.com/llvm/llvm-project 99155e913e9bad5f7f8a247f8bb3a3ff3da74af1)
reproduce (this is a W=1 build):
        wget https://raw.githubusercontent.com/intel/lkp-tests/master/sbin/make.cross -O ~/bin/make.cross
        chmod +x ~/bin/make.cross
        # install x86_64 cross compiling tool for clang build
        # apt-get install binutils-x86-64-linux-gnu
        # https://github.com/0day-ci/linux/commit/83c919857a4ca319ed69d6feaf3d5b5325dbdc29
        git remote add linux-review https://github.com/0day-ci/linux
        git fetch --no-tags linux-review David-Gow/kunit-Support-skipped-tests/20210526-161324
        git checkout 83c919857a4ca319ed69d6feaf3d5b5325dbdc29
        # save the attached .config to linux build tree
        COMPILER_INSTALL_PATH=$HOME/0day COMPILER=clang make.cross ARCH=x86_64 

If you fix the issue, kindly add following tag as appropriate
Reported-by: kernel test robot <lkp@intel.com>

All errors (new ones prefixed by >>):

   lib/kunit/kunit-test.c:458:2: warning: comparison of distinct pointer types ('typeof (__left) *' (aka 'enum kunit_status *') and 'typeof (__right) *' (aka 'int *')) [-Wcompare-distinct-pointer-types]
           KUNIT_EXPECT_EQ(test, fake.status, KUNIT_SUCCESS);
           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   include/kunit/test.h:1320:2: note: expanded from macro 'KUNIT_EXPECT_EQ'
           KUNIT_BINARY_EQ_ASSERTION(test, KUNIT_EXPECTATION, left, right)
           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   include/kunit/test.h:957:2: note: expanded from macro 'KUNIT_BINARY_EQ_ASSERTION'
           KUNIT_BINARY_EQ_MSG_ASSERTION(test,                                    \
           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   include/kunit/test.h:947:2: note: expanded from macro 'KUNIT_BINARY_EQ_MSG_ASSERTION'
           KUNIT_BASE_EQ_MSG_ASSERTION(test,                                      \
           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   include/kunit/test.h:858:2: note: expanded from macro 'KUNIT_BASE_EQ_MSG_ASSERTION'
           KUNIT_BASE_BINARY_ASSERTION(test,                                      \
           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   include/kunit/test.h:834:9: note: expanded from macro 'KUNIT_BASE_BINARY_ASSERTION'
           ((void)__typecheck(__left, __right));                                  \
                  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
   include/linux/minmax.h:20:28: note: expanded from macro '__typecheck'
           (!!(sizeof((typeof(x) *)1 == (typeof(y) *)1)))
                      ~~~~~~~~~~~~~~ ^  ~~~~~~~~~~~~~~
>> lib/kunit/kunit-test.c:459:2: error: array initializer must be an initializer list or string literal
           KUNIT_EXPECT_STREQ(test, fake.status_comment, "");
           ^
   include/kunit/test.h:1502:2: note: expanded from macro 'KUNIT_EXPECT_STREQ'
           KUNIT_BINARY_STR_EQ_ASSERTION(test, KUNIT_EXPECTATION, left, right)
           ^
   include/kunit/test.h:1218:2: note: expanded from macro 'KUNIT_BINARY_STR_EQ_ASSERTION'
           KUNIT_BINARY_STR_EQ_MSG_ASSERTION(test,                                \
           ^
   include/kunit/test.h:1211:2: note: expanded from macro 'KUNIT_BINARY_STR_EQ_MSG_ASSERTION'
           KUNIT_BINARY_STR_ASSERTION(test,                                       \
           ^
   include/kunit/test.h:1188:15: note: expanded from macro 'KUNIT_BINARY_STR_ASSERTION'
           typeof(left) __left = (left);                                          \
                        ^
   lib/kunit/kunit-test.c:466:2: error: array initializer must be an initializer list or string literal
           KUNIT_EXPECT_STREQ(test, fake.status_comment, "Accepts format string: YES");
           ^
   include/kunit/test.h:1502:2: note: expanded from macro 'KUNIT_EXPECT_STREQ'
           KUNIT_BINARY_STR_EQ_ASSERTION(test, KUNIT_EXPECTATION, left, right)
           ^
   include/kunit/test.h:1218:2: note: expanded from macro 'KUNIT_BINARY_STR_EQ_ASSERTION'
           KUNIT_BINARY_STR_EQ_MSG_ASSERTION(test,                                \
           ^
   include/kunit/test.h:1211:2: note: expanded from macro 'KUNIT_BINARY_STR_EQ_MSG_ASSERTION'
           KUNIT_BINARY_STR_ASSERTION(test,                                       \
           ^
   include/kunit/test.h:1188:15: note: expanded from macro 'KUNIT_BINARY_STR_ASSERTION'
           typeof(left) __left = (left);                                          \
                        ^
   1 warning and 2 errors generated.


vim +459 lib/kunit/kunit-test.c

   450	
   451	static void kunit_status_mark_skipped_test(struct kunit *test)
   452	{
   453		struct kunit fake;
   454	
   455		kunit_init_test(&fake, "fake test", NULL);
   456	
   457		/* Before: Should be SUCCESS with no comment. */
   458		KUNIT_EXPECT_EQ(test, fake.status, KUNIT_SUCCESS);
 > 459		KUNIT_EXPECT_STREQ(test, fake.status_comment, "");
   460	
   461		/* Mark the test as skipped. */
   462		kunit_mark_skipped(&fake, "Accepts format string: %s", "YES");
   463	
   464		/* After: Should be SKIPPED with our comment. */
   465		KUNIT_EXPECT_EQ(test, fake.status, (enum kunit_status)KUNIT_SKIPPED);
   466		KUNIT_EXPECT_STREQ(test, fake.status_comment, "Accepts format string: YES");
   467	}
   468	

---
0-DAY CI Kernel Test Service, Intel Corporation
https://lists.01.org/hyperkitty/list/kbuild-all@lists.01.org
Daniel Latypov May 26, 2021, 7:10 p.m. UTC | #3
On Wed, May 26, 2021 at 1:11 AM 'David Gow' via KUnit Development
<kunit-dev@googlegroups.com> wrote:
>
> Add support for the SKIP directive to kunit_tool's TAP parser.
>
> Skipped tests now show up as such in the printed summary. The number of
> skipped tests is counted, and if all tests in a suite are skipped, the
> suite is also marked as skipped. Otherwise, skipped tests do affect the
> suite result.
>
> Example output:
> [00:22:34] ======== [SKIPPED] example_skip ========
> [00:22:34] [SKIPPED] example_skip_test # SKIP this test should be skipped
> [00:22:34] [SKIPPED] example_mark_skipped_test # SKIP this test should be skipped
> [00:22:34] ============================================================
> [00:22:34] Testing complete. 2 tests run. 0 failed. 0 crashed. 2 skipped.
>
> Signed-off-by: David Gow <davidgow@google.com>
> ---
>  tools/testing/kunit/kunit_parser.py    | 47 +++++++++++++++++++-------
>  tools/testing/kunit/kunit_tool_test.py | 22 ++++++++++++

This seems to be missing the added test files.

>  2 files changed, 57 insertions(+), 12 deletions(-)
>
> diff --git a/tools/testing/kunit/kunit_parser.py b/tools/testing/kunit/kunit_parser.py
> index e8bcc139702e..6b5dd26b479d 100644
> --- a/tools/testing/kunit/kunit_parser.py
> +++ b/tools/testing/kunit/kunit_parser.py
> @@ -43,6 +43,7 @@ class TestCase(object):
>  class TestStatus(Enum):
>         SUCCESS = auto()
>         FAILURE = auto()
> +       SKIPPED = auto()
>         TEST_CRASHED = auto()
>         NO_TESTS = auto()
>         FAILURE_TO_PARSE_TESTS = auto()
> @@ -108,6 +109,8 @@ def save_non_diagnostic(lines: List[str], test_case: TestCase) -> None:
>
>  OkNotOkResult = namedtuple('OkNotOkResult', ['is_ok','description', 'text'])
>
> +OK_NOT_OK_SKIP = re.compile(r'^[\s]*(ok|not ok) [0-9]+ - (.*) # SKIP(.*)$')
> +
>  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]+) - (.*)$')
> @@ -125,6 +128,10 @@ def parse_ok_not_ok_test_case(lines: List[str], test_case: TestCase) -> bool:
>         if match:
>                 test_case.log.append(lines.pop(0))
>                 test_case.name = match.group(2)
> +               skip_match = OK_NOT_OK_SKIP.match(line)
> +               if skip_match:
> +                       test_case.status = TestStatus.SKIPPED
> +                       return True
>                 if test_case.status == TestStatus.TEST_CRASHED:
>                         return True
>                 if match.group(1) == 'ok':
> @@ -188,16 +195,16 @@ def parse_subtest_plan(lines: List[str]) -> Optional[int]:
>                 return None
>
>  def max_status(left: TestStatus, right: TestStatus) -> TestStatus:
> -       if left == TestStatus.TEST_CRASHED or right == TestStatus.TEST_CRASHED:
> +       if left == right:
> +               return left
> +       elif left == TestStatus.TEST_CRASHED or right == TestStatus.TEST_CRASHED:
>                 return TestStatus.TEST_CRASHED
>         elif left == TestStatus.FAILURE or right == TestStatus.FAILURE:
>                 return TestStatus.FAILURE
> -       elif left != TestStatus.SUCCESS:
> -               return left
> -       elif right != TestStatus.SUCCESS:
> +       elif left == TestStatus.SKIPPED:
>                 return right
>         else:
> -               return TestStatus.SUCCESS
> +               return left
>
>  def parse_ok_not_ok_test_suite(lines: List[str],
>                                test_suite: TestSuite,
> @@ -214,6 +221,9 @@ def parse_ok_not_ok_test_suite(lines: List[str],
>                         test_suite.status = TestStatus.SUCCESS
>                 else:
>                         test_suite.status = TestStatus.FAILURE
> +               skip_match = OK_NOT_OK_SKIP.match(line)
> +               if skip_match:
> +                       test_suite.status = TestStatus.SKIPPED
>                 suite_index = int(match.group(2))
>                 if suite_index != expected_suite_index:
>                         print_with_timestamp(
> @@ -224,8 +234,8 @@ def parse_ok_not_ok_test_suite(lines: List[str],
>         else:
>                 return False
>
> -def bubble_up_errors(statuses: Iterable[TestStatus]) -> TestStatus:
> -       return reduce(max_status, statuses, TestStatus.SUCCESS)
> +def bubble_up_errors(status_list: Iterable[TestStatus]) -> TestStatus:
> +       return reduce(max_status, status_list, TestStatus.SKIPPED)
>
>  def bubble_up_test_case_errors(test_suite: TestSuite) -> TestStatus:
>         max_test_case_status = bubble_up_errors(x.status for x in test_suite.cases)
> @@ -315,9 +325,12 @@ def print_and_count_results(test_result: TestResult) -> Tuple[int, int, int]:

Btw, this type annotation is out of date.
But I think an ever growing Tuple is too cumbersome, how about this?

diff --git a/tools/testing/kunit/kunit_parser.py
b/tools/testing/kunit/kunit_parser.py
index 6b5dd26b479d..055ee1e4d19d 100644
--- a/tools/testing/kunit/kunit_parser.py
+++ b/tools/testing/kunit/kunit_parser.py
@@ -6,6 +6,7 @@
 # Author: Felix Guo <felixguoxiuping@gmail.com>
 # Author: Brendan Higgins <brendanhiggins@google.com>

+from dataclasses import dataclass
 import re

 from collections import namedtuple
@@ -321,11 +322,19 @@ def parse_test_result(lines: List[str]) -> TestResult:
        else:
                return TestResult(TestStatus.NO_TESTS, [], lines)

-def print_and_count_results(test_result: TestResult) -> Tuple[int, int, int]:
-       total_tests = 0
-       failed_tests = 0
-       crashed_tests = 0
-       skipped_tests = 0
+#  Note: This would require Python 3.7. We currently only required
3.6 (enum.auto). We can do it by hand to avoid that, if we want.
+@dataclass
+class TestCounts:
+       passed: int = 0
+       failed: int = 0
+       skipped: int = 0
+       crashed: int = 0
+
+       def total(self) -> int:
+               return self.passed + self.failed + self.skipped + self.crashed
+
+def print_and_count_results(test_result: TestResult) -> TestCounts:
+       counts = TestCounts()
        for test_suite in test_result.suites:
                if test_suite.status == TestStatus.SUCCESS:
                        print_suite_divider(green('[PASSED] ') +
test_suite.name)
@@ -336,39 +345,33 @@ def print_and_count_results(test_result:
TestResult) -> Tuple[int, int, int]:
                else:
                        print_suite_divider(red('[FAILED] ') + test_suite.name)
                for test_case in test_suite.cases:
-                       total_tests += 1
                        if test_case.status == TestStatus.SUCCESS:
+                               counts.passed += 1
                                print_with_timestamp(green('[PASSED]
') + test_case.name)
                        elif test_case.status == TestStatus.SKIPPED:
-                               skipped_tests += 1
+                               counts.skipped += 1
                                print_with_timestamp(yellow('[SKIPPED]
') + test_case.name)
                        elif test_case.status == TestStatus.TEST_CRASHED:
-                               crashed_tests += 1
+                               counts.crashed += 1
                                print_with_timestamp(red('[CRASHED] '
+ test_case.name))
                                print_log(map(yellow, test_case.log))
                                print_with_timestamp('')
                        else:
-                               failed_tests += 1
+                               counts.failed += 1
                                print_with_timestamp(red('[FAILED] ')
+ test_case.name)
                                print_log(map(yellow, test_case.log))
                                print_with_timestamp('')
-       return total_tests, failed_tests, crashed_tests, skipped_tests
+       return counts

 def parse_run_tests(kernel_output) -> TestResult:
-       total_tests = 0
-       failed_tests = 0
-       crashed_tests = 0
-       skipped_tests = 0
+       counts = TestCounts()
        test_result =
parse_test_result(list(isolate_kunit_output(kernel_output)))
        if test_result.status == TestStatus.NO_TESTS:
                print(red('[ERROR] ') + yellow('no tests run!'))
        elif test_result.status == TestStatus.FAILURE_TO_PARSE_TESTS:
                print(red('[ERROR] ') + yellow('could not parse test results!'))
        else:
-               (total_tests,
-                failed_tests,
-                crashed_tests,
-                skipped_tests) = print_and_count_results(test_result)
+               counts = print_and_count_results(test_result)
        print_with_timestamp(DIVIDER)
        if test_result.status == TestStatus.SUCCESS:
                fmt = green
@@ -378,5 +381,5 @@ def parse_run_tests(kernel_output) -> TestResult:
                fmt =red
        print_with_timestamp(
                fmt('Testing complete. %d tests run. %d failed. %d
crashed. %d skipped.' %
-                   (total_tests, failed_tests, crashed_tests, skipped_tests)))
+                   (counts.total(), counts.failed, counts.crashed,
counts.skipped)))
        return test_result

>         total_tests = 0
>         failed_tests = 0
>         crashed_tests = 0
> +       skipped_tests = 0
>         for test_suite in test_result.suites:
>                 if test_suite.status == TestStatus.SUCCESS:
>                         print_suite_divider(green('[PASSED] ') + test_suite.name)
> +               elif test_suite.status == TestStatus.SKIPPED:
> +                       print_suite_divider(yellow('[SKIPPED] ') + test_suite.name)
>                 elif test_suite.status == TestStatus.TEST_CRASHED:
>                         print_suite_divider(red('[CRASHED] ' + test_suite.name))
>                 else:
> @@ -326,6 +339,9 @@ def print_and_count_results(test_result: TestResult) -> Tuple[int, int, int]:
>                         total_tests += 1
>                         if test_case.status == TestStatus.SUCCESS:
>                                 print_with_timestamp(green('[PASSED] ') + test_case.name)
> +                       elif test_case.status == TestStatus.SKIPPED:
> +                               skipped_tests += 1
> +                               print_with_timestamp(yellow('[SKIPPED] ') + test_case.name)
>                         elif test_case.status == TestStatus.TEST_CRASHED:
>                                 crashed_tests += 1
>                                 print_with_timestamp(red('[CRASHED] ' + test_case.name))
> @@ -336,12 +352,13 @@ def print_and_count_results(test_result: TestResult) -> Tuple[int, int, int]:
>                                 print_with_timestamp(red('[FAILED] ') + test_case.name)
>                                 print_log(map(yellow, test_case.log))
>                                 print_with_timestamp('')
> -       return total_tests, failed_tests, crashed_tests
> +       return total_tests, failed_tests, crashed_tests, skipped_tests
>
>  def parse_run_tests(kernel_output) -> TestResult:
>         total_tests = 0
>         failed_tests = 0
>         crashed_tests = 0
> +       skipped_tests = 0
>         test_result = parse_test_result(list(isolate_kunit_output(kernel_output)))
>         if test_result.status == TestStatus.NO_TESTS:
>                 print(red('[ERROR] ') + yellow('no tests run!'))
> @@ -350,10 +367,16 @@ def parse_run_tests(kernel_output) -> TestResult:
>         else:
>                 (total_tests,
>                  failed_tests,
> -                crashed_tests) = print_and_count_results(test_result)
> +                crashed_tests,
> +                skipped_tests) = print_and_count_results(test_result)
>         print_with_timestamp(DIVIDER)
> -       fmt = green if test_result.status == TestStatus.SUCCESS else red
> +       if test_result.status == TestStatus.SUCCESS:
> +               fmt = green
> +       elif test_result.status == TestStatus.SKIPPED:
> +               fmt = yellow
> +       else:
> +               fmt =red
>         print_with_timestamp(
> -               fmt('Testing complete. %d tests run. %d failed. %d crashed.' %
> -                   (total_tests, failed_tests, crashed_tests)))
> +               fmt('Testing complete. %d tests run. %d failed. %d crashed. %d skipped.' %
> +                   (total_tests, failed_tests, crashed_tests, skipped_tests)))
>         return test_result
> diff --git a/tools/testing/kunit/kunit_tool_test.py b/tools/testing/kunit/kunit_tool_test.py
> index 2e809dd956a7..a51e70cafcc1 100755
> --- a/tools/testing/kunit/kunit_tool_test.py
> +++ b/tools/testing/kunit/kunit_tool_test.py
> @@ -183,6 +183,28 @@ class KUnitParserTest(unittest.TestCase):
>                         kunit_parser.TestStatus.TEST_CRASHED,
>                         result.status)
>
> +       def test_skipped_test(self):
> +               skipped_log = test_data_path('test_skip_tests.log')
> +               file = open(skipped_log)
> +               result = kunit_parser.parse_run_tests(file.readlines())
> +
> +               # A skipped test does not fail the whole suite.
> +               self.assertEqual(
> +                       kunit_parser.TestStatus.SUCCESS,
> +                       result.status)
> +               file.close()
> +
> +       def test_skipped_all_tests(self):
> +               skipped_log = test_data_path('test_skip_all_tests.log')
> +               file = open(skipped_log)
> +               result = kunit_parser.parse_run_tests(file.readlines())
> +
> +               self.assertEqual(
> +                       kunit_parser.TestStatus.SKIPPED,
> +                       result.status)
> +               file.close()
> +
> +
>         def test_ignores_prefix_printk_time(self):
>                 prefix_log = test_data_path('test_config_printk_time.log')
>                 with open(prefix_log) as file:
> --
> 2.31.1.818.g46aad6cb9e-goog
>
> --
> You received this message because you are subscribed to the Google Groups "KUnit Development" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to kunit-dev+unsubscribe@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/kunit-dev/20210526081112.3652290-2-davidgow%40google.com.
David Gow May 27, 2021, 8:21 a.m. UTC | #4
On Thu, May 27, 2021 at 4:49 AM Daniel Latypov <dlatypov@google.com> wrote:
>

> On Wed, May 26, 2021 at 1:11 AM 'David Gow' via KUnit Development

> <kunit-dev@googlegroups.com> wrote:

> >

> > The kunit_mark_skipped() macro marks the current test as "skipped", with

> > the provided reason. The kunit_skip() macro will mark the test as

> > skipped, and abort the test.

> >

> > The TAP specification supports this "SKIP directive" as a comment after

> > the "ok" / "not ok" for a test. See the "Directives" section of the TAP

> > spec for details:

> > https://testanything.org/tap-specification.html#directives

> >

> > The 'success' field for KUnit tests is replaced with a kunit_status

> > enum, which can be SUCCESS, FAILURE, or SKIPPED, combined with a

> > 'status_comment' containing information on why a test was skipped.

> >

> > A new 'kunit_status' test suite is added to test this.

> >

> > Signed-off-by: David Gow <davidgow@google.com>

>

> Reviewed-by: Daniel Latypov <dlatypov@google.com>

>

> This is pretty exciting to see.

> Some minor nits below.

>

>


Thanks: I'll take these suggestions on board for v2.

> > ---

> > This change depends on the assertion typechecking fix here:

> > https://lore.kernel.org/linux-kselftest/20210513193204.816681-1-davidgow@google.com/

> > Only the first two patches in the series are required.

> >

> > This is the long-awaited follow-up to the skip tests RFC:

> > https://lore.kernel.org/linux-kselftest/20200513042956.109987-1-davidgow@google.com/

> >

> > There are quite a few changes since that version, principally:

> > - A kunit_status enum is now used, with SKIPPED a distinct state

> > - The kunit_mark_skipped() and kunit_skip() macros now take printf-style

> >   format strings.

> > - There is now a kunit_status test suite providing basic tests of this

> >   functionality.

> > - The kunit_tool changes have been split into a separate commit.

> > - The example skipped tests have been expanded an moved to their own

> >   suite, which is not enabled by KUNIT_ALL_TESTS.

> > - A number of other fixes and changes here and there.

> >

> > Cheers,

> > -- David

> >

> >  include/kunit/test.h   | 68 ++++++++++++++++++++++++++++++++++++++----

> >  lib/kunit/kunit-test.c | 42 +++++++++++++++++++++++++-

> >  lib/kunit/test.c       | 51 ++++++++++++++++++-------------

> >  3 files changed, 134 insertions(+), 27 deletions(-)

> >

> > diff --git a/include/kunit/test.h b/include/kunit/test.h

> > index b68c61348121..40b536da027e 100644

> > --- a/include/kunit/test.h

> > +++ b/include/kunit/test.h

> > @@ -105,6 +105,18 @@ struct kunit;

> >  #define KUNIT_SUBTEST_INDENT           "    "

> >  #define KUNIT_SUBSUBTEST_INDENT                "        "

> >

> > +/**

> > + * enum kunit_status - Type of result for a test or test suite

> > + * @KUNIT_SUCCESS: Denotes the test suite has not failed nor been skipped

> > + * @KUNIT_FAILURE: Denotes the test has failed.

> > + * @KUNIT_SKIPPED: Denotes the test has been skipped.

> > + */

> > +enum kunit_status {

> > +       KUNIT_SUCCESS,

> > +       KUNIT_FAILURE,

> > +       KUNIT_SKIPPED,

> > +};

> > +

> >  /**

> >   * struct kunit_case - represents an individual test case.

> >   *

> > @@ -148,13 +160,20 @@ struct kunit_case {

> >         const void* (*generate_params)(const void *prev, char *desc);

> >

> >         /* private: internal use only. */

> > -       bool success;

> > +       enum kunit_status status;

> >         char *log;

> >  };

> >

> > -static inline char *kunit_status_to_string(bool status)

> > +static inline char *kunit_status_to_string(enum kunit_status status)

>

> nit: I personally would think this maps SKIPPED => "SKIPPED", etc.

> (If I didn't know all that logic lived in kunit tool).

>

> I don't have any replacement names to suggest that I'm fully happy

> with, however.

> kunit_status_to_tap_str(), kunit_status_to_ok_notok(), eh.

>


Yeah: I kept the existing names for these functions, which which
worked well when it was just a bool.

The TAP spec seems to just call this "ok/not ok", and given we already
have kunit_print_okay_not_ok(), kunit_status_to_ok_not_ok() seems the
best of those options.

> >  {

> > -       return status ? "ok" : "not ok";

> > +       switch (status) {

> > +       case KUNIT_SKIPPED:

> > +       case KUNIT_SUCCESS:

> > +               return "ok";

> > +       case KUNIT_FAILURE:

> > +               return "not ok";

> > +       }

> > +       return "invalid";

> >  }

> >

> >  /**

> > @@ -212,6 +231,7 @@ struct kunit_suite {

> >         struct kunit_case *test_cases;

> >

> >         /* private: internal use only */

> > +       char status_comment[256];

> >         struct dentry *debugfs;

> >         char *log;

> >  };

> > @@ -245,19 +265,21 @@ struct kunit {

> >          * be read after the test case finishes once all threads associated

> >          * with the test case have terminated.

> >          */

> > -       bool success; /* Read only after test_case finishes! */

> >         spinlock_t lock; /* Guards all mutable test state. */

> > +       enum kunit_status status; /* Read only after test_case finishes! */

> >         /*

> >          * Because resources is a list that may be updated multiple times (with

> >          * new resources) from any thread associated with a test case, we must

> >          * protect it with some type of lock.

> >          */

> >         struct list_head resources; /* Protected by lock. */

> > +

> > +       char status_comment[256];

> >  };

> >

> >  static inline void kunit_set_failure(struct kunit *test)

> >  {

> > -       WRITE_ONCE(test->success, false);

> > +       WRITE_ONCE(test->status, KUNIT_FAILURE);

> >  }

> >

> >  void kunit_init_test(struct kunit *test, const char *name, char *log);

> > @@ -348,7 +370,7 @@ static inline int kunit_run_all_tests(void)

> >  #define kunit_suite_for_each_test_case(suite, test_case)               \

> >         for (test_case = suite->test_cases; test_case->run_case; test_case++)

> >

> > -bool kunit_suite_has_succeeded(struct kunit_suite *suite);

> > +enum kunit_status kunit_suite_has_succeeded(struct kunit_suite *suite);

> >

> >  /*

> >   * Like kunit_alloc_resource() below, but returns the struct kunit_resource

> > @@ -612,6 +634,40 @@ void kunit_cleanup(struct kunit *test);

> >

> >  void kunit_log_append(char *log, const char *fmt, ...);

> >

> > +/**

> > + * kunit_mark_skipped() - Marks @test_or_suite as skipped

> > + *

> > + * @test_or_suite: The test context object.

> > + * @fmt:  A printk() style format string.

> > + *

> > + * Marks the test as skipped. @fmt is given output as the test status

> > + * comment, typically the reason the test was skipped.

> > + *

> > + * Test execution continues after kunit_mark_skipped() is called.

> > + */

> > +#define kunit_mark_skipped(test_or_suite, fmt, ...)                    \

> > +       do {                                                            \

> > +               WRITE_ONCE((test_or_suite)->status, KUNIT_SKIPPED);     \

> > +               scnprintf((test_or_suite)->status_comment, 256, fmt, ##__VA_ARGS__); \

> > +       } while (0)

> > +

> > +/**

> > + * kunit_skip() - Marks @test_or_suite as skipped

> > + *

> > + * @test_or_suite: The test context object.

> > + * @fmt:  A printk() style format string.

> > + *

> > + * Skips the test. @fmt is given output as the test status

> > + * comment, typically the reason the test was skipped.

> > + *

> > + * Test execution is halted after kunit_skip() is called.

> > + */

> > +#define kunit_skip(test_or_suite, fmt, ...)                            \

> > +       do {                                                            \

> > +               kunit_mark_skipped((test_or_suite), fmt, ##__VA_ARGS__);\

> > +               kunit_try_catch_throw(&((test_or_suite)->try_catch));   \

> > +       } while (0)

> > +

> >  /*

> >   * printk and log to per-test or per-suite log buffer.  Logging only done

> >   * if CONFIG_KUNIT_DEBUGFS is 'y'; if it is 'n', no log is allocated/used.

> > diff --git a/lib/kunit/kunit-test.c b/lib/kunit/kunit-test.c

> > index 69f902440a0e..d69efcbed624 100644

> > --- a/lib/kunit/kunit-test.c

> > +++ b/lib/kunit/kunit-test.c

> > @@ -437,7 +437,47 @@ static void kunit_log_test(struct kunit *test)

> >  #endif

> >  }

> >

> > +static void kunit_status_set_failure_test(struct kunit *test)

> > +{

> > +       struct kunit fake;

> > +

> > +       kunit_init_test(&fake, "fake test", NULL);

> > +

> > +       KUNIT_EXPECT_EQ(test, fake.status, (enum kunit_status)KUNIT_SUCCESS);

> > +       kunit_set_failure(&fake);

> > +       KUNIT_EXPECT_EQ(test, fake.status, (enum kunit_status)KUNIT_FAILURE);

> > +}

> > +

> > +static void kunit_status_mark_skipped_test(struct kunit *test)

> > +{

> > +       struct kunit fake;

> > +

> > +       kunit_init_test(&fake, "fake test", NULL);

> > +

> > +       /* Before: Should be SUCCESS with no comment. */

> > +       KUNIT_EXPECT_EQ(test, fake.status, KUNIT_SUCCESS);

> > +       KUNIT_EXPECT_STREQ(test, fake.status_comment, "");

> > +

> > +       /* Mark the test as skipped. */

> > +       kunit_mark_skipped(&fake, "Accepts format string: %s", "YES");

> > +

> > +       /* After: Should be SKIPPED with our comment. */

> > +       KUNIT_EXPECT_EQ(test, fake.status, (enum kunit_status)KUNIT_SKIPPED);

> > +       KUNIT_EXPECT_STREQ(test, fake.status_comment, "Accepts format string: YES");

> > +}

> > +

> > +static struct kunit_case kunit_status_test_cases[] = {

> > +       KUNIT_CASE(kunit_status_set_failure_test),

> > +       KUNIT_CASE(kunit_status_mark_skipped_test),

> > +       {}

> > +};

> > +

> > +static struct kunit_suite kunit_status_test_suite = {

> > +       .name = "kunit_status",

> > +       .test_cases = kunit_status_test_cases,

> > +};

> > +

> >  kunit_test_suites(&kunit_try_catch_test_suite, &kunit_resource_test_suite,

> > -                 &kunit_log_test_suite);

> > +                 &kunit_log_test_suite, &kunit_status_test_suite);

> >

> >  MODULE_LICENSE("GPL v2");

> > diff --git a/lib/kunit/test.c b/lib/kunit/test.c

> > index 2f6cc0123232..0ee07705d2b0 100644

> > --- a/lib/kunit/test.c

> > +++ b/lib/kunit/test.c

> > @@ -98,12 +98,14 @@ static void kunit_print_subtest_start(struct kunit_suite *suite)

> >

> >  static void kunit_print_ok_not_ok(void *test_or_suite,

> >                                   bool is_test,

> > -                                 bool is_ok,

> > +                                 enum kunit_status status,

> >                                   size_t test_number,

> > -                                 const char *description)

> > +                                 const char *description,

> > +                                 const char *directive)

> >  {

> >         struct kunit_suite *suite = is_test ? NULL : test_or_suite;

> >         struct kunit *test = is_test ? test_or_suite : NULL;

> > +       const char *directive_header = (status == KUNIT_SKIPPED) ? " # SKIP " : "";

> >

> >         /*

> >          * We do not log the test suite results as doing so would

> > @@ -114,25 +116,31 @@ static void kunit_print_ok_not_ok(void *test_or_suite,

> >          * representation.

> >          */

> >         if (suite)

> > -               pr_info("%s %zd - %s\n",

> > -                       kunit_status_to_string(is_ok),

> > -                       test_number, description);

> > +               pr_info("%s %zd - %s%s%s\n",

> > +                       kunit_status_to_string(status),

> > +                       test_number, description,

> > +                       directive_header, directive ? directive : "");

> >         else

> > -               kunit_log(KERN_INFO, test, KUNIT_SUBTEST_INDENT "%s %zd - %s",

>

> Hmm, why not kunit_info(test, ...)?

>


No reason: it was kunit_log() originally, and I didn't change it. I
can replace this for v2 if you prefer.


> > -                         kunit_status_to_string(is_ok),

> > -                         test_number, description);

> > +               kunit_log(KERN_INFO, test,

> > +                         KUNIT_SUBTEST_INDENT "%s %zd - %s%s%s",

> > +                         kunit_status_to_string(status),

> > +                         test_number, description,

> > +                         directive_header, directive ? directive : "");

> >  }

> >

> > -bool kunit_suite_has_succeeded(struct kunit_suite *suite)

> > +enum kunit_status kunit_suite_has_succeeded(struct kunit_suite *suite)

> >  {

> >         const struct kunit_case *test_case;

> > +       enum kunit_status status = KUNIT_SKIPPED;

> >

> >         kunit_suite_for_each_test_case(suite, test_case) {

> > -               if (!test_case->success)

> > -                       return false;

> > +               if (test_case->status == KUNIT_FAILURE)

> > +                       return KUNIT_FAILURE;

> > +               else if (test_case->status == KUNIT_SUCCESS)

> > +                       status = KUNIT_SUCCESS;

> >         }

> >

> > -       return true;

> > +       return status;

> >  }

> >  EXPORT_SYMBOL_GPL(kunit_suite_has_succeeded);

> >

> > @@ -143,7 +151,8 @@ static void kunit_print_subtest_end(struct kunit_suite *suite)

> >         kunit_print_ok_not_ok((void *)suite, false,

> >                               kunit_suite_has_succeeded(suite),

> >                               kunit_suite_counter++,

> > -                             suite->name);

> > +                             suite->name,

> > +                             suite->status_comment);

> >  }

> >

> >  unsigned int kunit_test_case_num(struct kunit_suite *suite,

> > @@ -252,7 +261,8 @@ void kunit_init_test(struct kunit *test, const char *name, char *log)

> >         test->log = log;

> >         if (test->log)

> >                 test->log[0] = '\0';

> > -       test->success = true;

> > +       test->status = KUNIT_SUCCESS;

> > +       test->status_comment[0] = '\0';

> >  }

> >  EXPORT_SYMBOL_GPL(kunit_init_test);

> >

> > @@ -376,7 +386,8 @@ static void kunit_run_case_catch_errors(struct kunit_suite *suite,

> >         context.test_case = test_case;

> >         kunit_try_catch_run(try_catch, &context);

> >

> > -       test_case->success = test->success;

> > +       test_case->status = test->status;

> > +

> >  }

> >

> >  int kunit_run_tests(struct kunit_suite *suite)

> > @@ -388,7 +399,6 @@ int kunit_run_tests(struct kunit_suite *suite)

> >

> >         kunit_suite_for_each_test_case(suite, test_case) {

> >                 struct kunit test = { .param_value = NULL, .param_index = 0 };

> > -               bool test_success = true;

> >

> >                 if (test_case->generate_params) {

> >                         /* Get initial param. */

> > @@ -398,7 +408,6 @@ int kunit_run_tests(struct kunit_suite *suite)

> >

> >                 do {

> >                         kunit_run_case_catch_errors(suite, test_case, &test);

> > -                       test_success &= test_case->success;

> >

> >                         if (test_case->generate_params) {

> >                                 if (param_desc[0] == '\0') {

> > @@ -410,7 +419,7 @@ int kunit_run_tests(struct kunit_suite *suite)

> >                                           KUNIT_SUBTEST_INDENT

> >                                           "# %s: %s %d - %s",

> >                                           test_case->name,

> > -                                         kunit_status_to_string(test.success),

> > +                                         kunit_status_to_string(test.status),

> >                                           test.param_index + 1, param_desc);

> >

> >                                 /* Get next param. */

> > @@ -420,9 +429,10 @@ int kunit_run_tests(struct kunit_suite *suite)

> >                         }

> >                 } while (test.param_value);

> >

> > -               kunit_print_ok_not_ok(&test, true, test_success,

> > +               kunit_print_ok_not_ok(&test, true, test_case->status,

> >                                       kunit_test_case_num(suite, test_case),

> > -                                     test_case->name);

> > +                                     test_case->name,

> > +                                     test.status_comment);

> >         }

> >

> >         kunit_print_subtest_end(suite);

> > @@ -434,6 +444,7 @@ EXPORT_SYMBOL_GPL(kunit_run_tests);

> >  static void kunit_init_suite(struct kunit_suite *suite)

> >  {

> >         kunit_debugfs_create_suite(suite);

> > +       suite->status_comment[0] = '\0';

> >  }

> >

> >  int __kunit_test_suites_init(struct kunit_suite * const * const suites)

> > --

> > 2.31.1.818.g46aad6cb9e-goog

> >

> > --

> > You received this message because you are subscribed to the Google Groups "KUnit Development" group.

> > To unsubscribe from this group and stop receiving emails from it, send an email to kunit-dev+unsubscribe@googlegroups.com.

> > To view this discussion on the web visit https://groups.google.com/d/msgid/kunit-dev/20210526081112.3652290-1-davidgow%40google.com.
David Gow May 27, 2021, 8:21 a.m. UTC | #5
On Wed, May 26, 2021 at 5:03 PM Marco Elver <elver@google.com> wrote:
>

> On Wed, May 26, 2021 at 01:11AM -0700, David Gow wrote:

> > The kunit_mark_skipped() macro marks the current test as "skipped", with

> > the provided reason. The kunit_skip() macro will mark the test as

> > skipped, and abort the test.

> >

> > The TAP specification supports this "SKIP directive" as a comment after

> > the "ok" / "not ok" for a test. See the "Directives" section of the TAP

> > spec for details:

> > https://testanything.org/tap-specification.html#directives

> >

> > The 'success' field for KUnit tests is replaced with a kunit_status

> > enum, which can be SUCCESS, FAILURE, or SKIPPED, combined with a

> > 'status_comment' containing information on why a test was skipped.

> >

> > A new 'kunit_status' test suite is added to test this.

> >

> > Signed-off-by: David Gow <davidgow@google.com>

> [...]

> >  include/kunit/test.h   | 68 ++++++++++++++++++++++++++++++++++++++----

> >  lib/kunit/kunit-test.c | 42 +++++++++++++++++++++++++-

> >  lib/kunit/test.c       | 51 ++++++++++++++++++-------------

> >  3 files changed, 134 insertions(+), 27 deletions(-)

>

> Very nice, thank you.

>

>         Tested-by: Marco Elver <elver@google.com>

>

> , with the below changes to test_kasan.c. If you would like an immediate

> user of kunit_skip(), please feel free to add the below patch to your

> series.

>

> Thanks,

> -- Marco

>


Thanks! I'll add this to the next version.

Cheers,
-- David

> ------ >8 ------

>

> From: Marco Elver <elver@google.com>

> Date: Wed, 26 May 2021 10:43:12 +0200

> Subject: [PATCH] kasan: test: make use of kunit_skip()

>

> Make use of the recently added kunit_skip() to skip tests, as it permits

> TAP parsers to recognize if a test was deliberately skipped.

>

> Signed-off-by: Marco Elver <elver@google.com>

> ---

>  lib/test_kasan.c | 12 ++++--------

>  1 file changed, 4 insertions(+), 8 deletions(-)

>

> diff --git a/lib/test_kasan.c b/lib/test_kasan.c

> index cacbbbdef768..0a2029d14c91 100644

> --- a/lib/test_kasan.c

> +++ b/lib/test_kasan.c

> @@ -111,17 +111,13 @@ static void kasan_test_exit(struct kunit *test)

>  } while (0)

>

>  #define KASAN_TEST_NEEDS_CONFIG_ON(test, config) do {                  \

> -       if (!IS_ENABLED(config)) {                                      \

> -               kunit_info((test), "skipping, " #config " required");   \

> -               return;                                                 \

> -       }                                                               \

> +       if (!IS_ENABLED(config))                                        \

> +               kunit_skip((test), "Test requires " #config "=y");      \

>  } while (0)

>

>  #define KASAN_TEST_NEEDS_CONFIG_OFF(test, config) do {                 \

> -       if (IS_ENABLED(config)) {                                       \

> -               kunit_info((test), "skipping, " #config " enabled");    \

> -               return;                                                 \

> -       }                                                               \

> +       if (IS_ENABLED(config))                                         \

> +               kunit_skip((test), "Test requires " #config "=n");      \

>  } while (0)

>

>  static void kmalloc_oob_right(struct kunit *test)

> --

> 2.31.1.818.g46aad6cb9e-goog

>
David Gow May 27, 2021, 8:22 a.m. UTC | #6
On Thu, May 27, 2021 at 3:10 AM Daniel Latypov <dlatypov@google.com> wrote:
>

> On Wed, May 26, 2021 at 1:11 AM 'David Gow' via KUnit Development

> <kunit-dev@googlegroups.com> wrote:

> >

> > Add support for the SKIP directive to kunit_tool's TAP parser.

> >

> > Skipped tests now show up as such in the printed summary. The number of

> > skipped tests is counted, and if all tests in a suite are skipped, the

> > suite is also marked as skipped. Otherwise, skipped tests do affect the

> > suite result.

> >

> > Example output:

> > [00:22:34] ======== [SKIPPED] example_skip ========

> > [00:22:34] [SKIPPED] example_skip_test # SKIP this test should be skipped

> > [00:22:34] [SKIPPED] example_mark_skipped_test # SKIP this test should be skipped

> > [00:22:34] ============================================================

> > [00:22:34] Testing complete. 2 tests run. 0 failed. 0 crashed. 2 skipped.

> >

> > Signed-off-by: David Gow <davidgow@google.com>

> > ---

> >  tools/testing/kunit/kunit_parser.py    | 47 +++++++++++++++++++-------

> >  tools/testing/kunit/kunit_tool_test.py | 22 ++++++++++++

>

> This seems to be missing the added test files.

>


Whoops, yes: I'll add these back in v2.

> >  2 files changed, 57 insertions(+), 12 deletions(-)

> >

> > diff --git a/tools/testing/kunit/kunit_parser.py b/tools/testing/kunit/kunit_parser.py

> > index e8bcc139702e..6b5dd26b479d 100644

> > --- a/tools/testing/kunit/kunit_parser.py

> > +++ b/tools/testing/kunit/kunit_parser.py

> > @@ -43,6 +43,7 @@ class TestCase(object):

> >  class TestStatus(Enum):

> >         SUCCESS = auto()

> >         FAILURE = auto()

> > +       SKIPPED = auto()

> >         TEST_CRASHED = auto()

> >         NO_TESTS = auto()

> >         FAILURE_TO_PARSE_TESTS = auto()

> > @@ -108,6 +109,8 @@ def save_non_diagnostic(lines: List[str], test_case: TestCase) -> None:

> >

> >  OkNotOkResult = namedtuple('OkNotOkResult', ['is_ok','description', 'text'])

> >

> > +OK_NOT_OK_SKIP = re.compile(r'^[\s]*(ok|not ok) [0-9]+ - (.*) # SKIP(.*)$')

> > +

> >  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]+) - (.*)$')

> > @@ -125,6 +128,10 @@ def parse_ok_not_ok_test_case(lines: List[str], test_case: TestCase) -> bool:

> >         if match:

> >                 test_case.log.append(lines.pop(0))

> >                 test_case.name = match.group(2)

> > +               skip_match = OK_NOT_OK_SKIP.match(line)

> > +               if skip_match:

> > +                       test_case.status = TestStatus.SKIPPED

> > +                       return True

> >                 if test_case.status == TestStatus.TEST_CRASHED:

> >                         return True

> >                 if match.group(1) == 'ok':

> > @@ -188,16 +195,16 @@ def parse_subtest_plan(lines: List[str]) -> Optional[int]:

> >                 return None

> >

> >  def max_status(left: TestStatus, right: TestStatus) -> TestStatus:

> > -       if left == TestStatus.TEST_CRASHED or right == TestStatus.TEST_CRASHED:

> > +       if left == right:

> > +               return left

> > +       elif left == TestStatus.TEST_CRASHED or right == TestStatus.TEST_CRASHED:

> >                 return TestStatus.TEST_CRASHED

> >         elif left == TestStatus.FAILURE or right == TestStatus.FAILURE:

> >                 return TestStatus.FAILURE

> > -       elif left != TestStatus.SUCCESS:

> > -               return left

> > -       elif right != TestStatus.SUCCESS:

> > +       elif left == TestStatus.SKIPPED:

> >                 return right

> >         else:

> > -               return TestStatus.SUCCESS

> > +               return left

> >

> >  def parse_ok_not_ok_test_suite(lines: List[str],

> >                                test_suite: TestSuite,

> > @@ -214,6 +221,9 @@ def parse_ok_not_ok_test_suite(lines: List[str],

> >                         test_suite.status = TestStatus.SUCCESS

> >                 else:

> >                         test_suite.status = TestStatus.FAILURE

> > +               skip_match = OK_NOT_OK_SKIP.match(line)

> > +               if skip_match:

> > +                       test_suite.status = TestStatus.SKIPPED

> >                 suite_index = int(match.group(2))

> >                 if suite_index != expected_suite_index:

> >                         print_with_timestamp(

> > @@ -224,8 +234,8 @@ def parse_ok_not_ok_test_suite(lines: List[str],

> >         else:

> >                 return False

> >

> > -def bubble_up_errors(statuses: Iterable[TestStatus]) -> TestStatus:

> > -       return reduce(max_status, statuses, TestStatus.SUCCESS)

> > +def bubble_up_errors(status_list: Iterable[TestStatus]) -> TestStatus:

> > +       return reduce(max_status, status_list, TestStatus.SKIPPED)

> >

> >  def bubble_up_test_case_errors(test_suite: TestSuite) -> TestStatus:

> >         max_test_case_status = bubble_up_errors(x.status for x in test_suite.cases)

> > @@ -315,9 +325,12 @@ def print_and_count_results(test_result: TestResult) -> Tuple[int, int, int]:

>

> Btw, this type annotation is out of date.


Oops: will fix and/or replace with the below.

> But I think an ever growing Tuple is too cumbersome, how about this?

>


Yeah, this does seem cleaner: I'll put this or something like it in v2.

> diff --git a/tools/testing/kunit/kunit_parser.py

> b/tools/testing/kunit/kunit_parser.py

> index 6b5dd26b479d..055ee1e4d19d 100644

> --- a/tools/testing/kunit/kunit_parser.py

> +++ b/tools/testing/kunit/kunit_parser.py

> @@ -6,6 +6,7 @@

>  # Author: Felix Guo <felixguoxiuping@gmail.com>

>  # Author: Brendan Higgins <brendanhiggins@google.com>

>

> +from dataclasses import dataclass

>  import re

>

>  from collections import namedtuple

> @@ -321,11 +322,19 @@ def parse_test_result(lines: List[str]) -> TestResult:

>         else:

>                 return TestResult(TestStatus.NO_TESTS, [], lines)

>

> -def print_and_count_results(test_result: TestResult) -> Tuple[int, int, int]:

> -       total_tests = 0

> -       failed_tests = 0

> -       crashed_tests = 0

> -       skipped_tests = 0

> +#  Note: This would require Python 3.7. We currently only required

> 3.6 (enum.auto). We can do it by hand to avoid that, if we want.


Hmm... I'm generally loath to increase the version requirement for
something this simple, so might look into doing a version of this
without the dataclass.


> +@dataclass

> +class TestCounts:

> +       passed: int = 0

> +       failed: int = 0

> +       skipped: int = 0

> +       crashed: int = 0

> +

> +       def total(self) -> int:

> +               return self.passed + self.failed + self.skipped + self.crashed

> +

> +def print_and_count_results(test_result: TestResult) -> TestCounts:

> +       counts = TestCounts()

>         for test_suite in test_result.suites:

>                 if test_suite.status == TestStatus.SUCCESS:

>                         print_suite_divider(green('[PASSED] ') +

> test_suite.name)

> @@ -336,39 +345,33 @@ def print_and_count_results(test_result:

> TestResult) -> Tuple[int, int, int]:

>                 else:

>                         print_suite_divider(red('[FAILED] ') + test_suite.name)

>                 for test_case in test_suite.cases:

> -                       total_tests += 1

>                         if test_case.status == TestStatus.SUCCESS:

> +                               counts.passed += 1

>                                 print_with_timestamp(green('[PASSED]

> ') + test_case.name)

>                         elif test_case.status == TestStatus.SKIPPED:

> -                               skipped_tests += 1

> +                               counts.skipped += 1

>                                 print_with_timestamp(yellow('[SKIPPED]

> ') + test_case.name)

>                         elif test_case.status == TestStatus.TEST_CRASHED:

> -                               crashed_tests += 1

> +                               counts.crashed += 1

>                                 print_with_timestamp(red('[CRASHED] '

> + test_case.name))

>                                 print_log(map(yellow, test_case.log))

>                                 print_with_timestamp('')

>                         else:

> -                               failed_tests += 1

> +                               counts.failed += 1

>                                 print_with_timestamp(red('[FAILED] ')

> + test_case.name)

>                                 print_log(map(yellow, test_case.log))

>                                 print_with_timestamp('')

> -       return total_tests, failed_tests, crashed_tests, skipped_tests

> +       return counts

>

>  def parse_run_tests(kernel_output) -> TestResult:

> -       total_tests = 0

> -       failed_tests = 0

> -       crashed_tests = 0

> -       skipped_tests = 0

> +       counts = TestCounts()

>         test_result =

> parse_test_result(list(isolate_kunit_output(kernel_output)))

>         if test_result.status == TestStatus.NO_TESTS:

>                 print(red('[ERROR] ') + yellow('no tests run!'))

>         elif test_result.status == TestStatus.FAILURE_TO_PARSE_TESTS:

>                 print(red('[ERROR] ') + yellow('could not parse test results!'))

>         else:

> -               (total_tests,

> -                failed_tests,

> -                crashed_tests,

> -                skipped_tests) = print_and_count_results(test_result)

> +               counts = print_and_count_results(test_result)

>         print_with_timestamp(DIVIDER)

>         if test_result.status == TestStatus.SUCCESS:

>                 fmt = green

> @@ -378,5 +381,5 @@ def parse_run_tests(kernel_output) -> TestResult:

>                 fmt =red

>         print_with_timestamp(

>                 fmt('Testing complete. %d tests run. %d failed. %d

> crashed. %d skipped.' %

> -                   (total_tests, failed_tests, crashed_tests, skipped_tests)))

> +                   (counts.total(), counts.failed, counts.crashed,

> counts.skipped)))

>         return test_result

>

> >         total_tests = 0

> >         failed_tests = 0

> >         crashed_tests = 0

> > +       skipped_tests = 0

> >         for test_suite in test_result.suites:

> >                 if test_suite.status == TestStatus.SUCCESS:

> >                         print_suite_divider(green('[PASSED] ') + test_suite.name)

> > +               elif test_suite.status == TestStatus.SKIPPED:

> > +                       print_suite_divider(yellow('[SKIPPED] ') + test_suite.name)

> >                 elif test_suite.status == TestStatus.TEST_CRASHED:

> >                         print_suite_divider(red('[CRASHED] ' + test_suite.name))

> >                 else:

> > @@ -326,6 +339,9 @@ def print_and_count_results(test_result: TestResult) -> Tuple[int, int, int]:

> >                         total_tests += 1

> >                         if test_case.status == TestStatus.SUCCESS:

> >                                 print_with_timestamp(green('[PASSED] ') + test_case.name)

> > +                       elif test_case.status == TestStatus.SKIPPED:

> > +                               skipped_tests += 1

> > +                               print_with_timestamp(yellow('[SKIPPED] ') + test_case.name)

> >                         elif test_case.status == TestStatus.TEST_CRASHED:

> >                                 crashed_tests += 1

> >                                 print_with_timestamp(red('[CRASHED] ' + test_case.name))

> > @@ -336,12 +352,13 @@ def print_and_count_results(test_result: TestResult) -> Tuple[int, int, int]:

> >                                 print_with_timestamp(red('[FAILED] ') + test_case.name)

> >                                 print_log(map(yellow, test_case.log))

> >                                 print_with_timestamp('')

> > -       return total_tests, failed_tests, crashed_tests

> > +       return total_tests, failed_tests, crashed_tests, skipped_tests

> >

> >  def parse_run_tests(kernel_output) -> TestResult:

> >         total_tests = 0

> >         failed_tests = 0

> >         crashed_tests = 0

> > +       skipped_tests = 0

> >         test_result = parse_test_result(list(isolate_kunit_output(kernel_output)))

> >         if test_result.status == TestStatus.NO_TESTS:

> >                 print(red('[ERROR] ') + yellow('no tests run!'))

> > @@ -350,10 +367,16 @@ def parse_run_tests(kernel_output) -> TestResult:

> >         else:

> >                 (total_tests,

> >                  failed_tests,

> > -                crashed_tests) = print_and_count_results(test_result)

> > +                crashed_tests,

> > +                skipped_tests) = print_and_count_results(test_result)

> >         print_with_timestamp(DIVIDER)

> > -       fmt = green if test_result.status == TestStatus.SUCCESS else red

> > +       if test_result.status == TestStatus.SUCCESS:

> > +               fmt = green

> > +       elif test_result.status == TestStatus.SKIPPED:

> > +               fmt = yellow

> > +       else:

> > +               fmt =red

> >         print_with_timestamp(

> > -               fmt('Testing complete. %d tests run. %d failed. %d crashed.' %

> > -                   (total_tests, failed_tests, crashed_tests)))

> > +               fmt('Testing complete. %d tests run. %d failed. %d crashed. %d skipped.' %

> > +                   (total_tests, failed_tests, crashed_tests, skipped_tests)))

> >         return test_result

> > diff --git a/tools/testing/kunit/kunit_tool_test.py b/tools/testing/kunit/kunit_tool_test.py

> > index 2e809dd956a7..a51e70cafcc1 100755

> > --- a/tools/testing/kunit/kunit_tool_test.py

> > +++ b/tools/testing/kunit/kunit_tool_test.py

> > @@ -183,6 +183,28 @@ class KUnitParserTest(unittest.TestCase):

> >                         kunit_parser.TestStatus.TEST_CRASHED,

> >                         result.status)

> >

> > +       def test_skipped_test(self):

> > +               skipped_log = test_data_path('test_skip_tests.log')

> > +               file = open(skipped_log)

> > +               result = kunit_parser.parse_run_tests(file.readlines())

> > +

> > +               # A skipped test does not fail the whole suite.

> > +               self.assertEqual(

> > +                       kunit_parser.TestStatus.SUCCESS,

> > +                       result.status)

> > +               file.close()

> > +

> > +       def test_skipped_all_tests(self):

> > +               skipped_log = test_data_path('test_skip_all_tests.log')

> > +               file = open(skipped_log)

> > +               result = kunit_parser.parse_run_tests(file.readlines())

> > +

> > +               self.assertEqual(

> > +                       kunit_parser.TestStatus.SKIPPED,

> > +                       result.status)

> > +               file.close()

> > +

> > +

> >         def test_ignores_prefix_printk_time(self):

> >                 prefix_log = test_data_path('test_config_printk_time.log')

> >                 with open(prefix_log) as file:

> > --

> > 2.31.1.818.g46aad6cb9e-goog

> >

> > --

> > You received this message because you are subscribed to the Google Groups "KUnit Development" group.

> > To unsubscribe from this group and stop receiving emails from it, send an email to kunit-dev+unsubscribe@googlegroups.com.

> > To view this discussion on the web visit https://groups.google.com/d/msgid/kunit-dev/20210526081112.3652290-2-davidgow%40google.com.
Daniel Latypov May 27, 2021, 7:11 p.m. UTC | #7
On Thu, May 27, 2021 at 1:22 AM David Gow <davidgow@google.com> wrote:
>

> On Thu, May 27, 2021 at 3:10 AM Daniel Latypov <dlatypov@google.com> wrote:

> >

> > On Wed, May 26, 2021 at 1:11 AM 'David Gow' via KUnit Development

> > <kunit-dev@googlegroups.com> wrote:

> > >

> > > Add support for the SKIP directive to kunit_tool's TAP parser.

> > >

> > > Skipped tests now show up as such in the printed summary. The number of

> > > skipped tests is counted, and if all tests in a suite are skipped, the

> > > suite is also marked as skipped. Otherwise, skipped tests do affect the

> > > suite result.

> > >

> > > Example output:

> > > [00:22:34] ======== [SKIPPED] example_skip ========

> > > [00:22:34] [SKIPPED] example_skip_test # SKIP this test should be skipped

> > > [00:22:34] [SKIPPED] example_mark_skipped_test # SKIP this test should be skipped

> > > [00:22:34] ============================================================

> > > [00:22:34] Testing complete. 2 tests run. 0 failed. 0 crashed. 2 skipped.

> > >

> > > Signed-off-by: David Gow <davidgow@google.com>

> > > ---

> > >  tools/testing/kunit/kunit_parser.py    | 47 +++++++++++++++++++-------

> > >  tools/testing/kunit/kunit_tool_test.py | 22 ++++++++++++

> >

> > This seems to be missing the added test files.

> >

>

> Whoops, yes: I'll add these back in v2.

>

> > >  2 files changed, 57 insertions(+), 12 deletions(-)

> > >

> > > diff --git a/tools/testing/kunit/kunit_parser.py b/tools/testing/kunit/kunit_parser.py

> > > index e8bcc139702e..6b5dd26b479d 100644

> > > --- a/tools/testing/kunit/kunit_parser.py

> > > +++ b/tools/testing/kunit/kunit_parser.py

> > > @@ -43,6 +43,7 @@ class TestCase(object):

> > >  class TestStatus(Enum):

> > >         SUCCESS = auto()

> > >         FAILURE = auto()

> > > +       SKIPPED = auto()

> > >         TEST_CRASHED = auto()

> > >         NO_TESTS = auto()

> > >         FAILURE_TO_PARSE_TESTS = auto()

> > > @@ -108,6 +109,8 @@ def save_non_diagnostic(lines: List[str], test_case: TestCase) -> None:

> > >

> > >  OkNotOkResult = namedtuple('OkNotOkResult', ['is_ok','description', 'text'])

> > >

> > > +OK_NOT_OK_SKIP = re.compile(r'^[\s]*(ok|not ok) [0-9]+ - (.*) # SKIP(.*)$')

> > > +

> > >  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]+) - (.*)$')

> > > @@ -125,6 +128,10 @@ def parse_ok_not_ok_test_case(lines: List[str], test_case: TestCase) -> bool:

> > >         if match:

> > >                 test_case.log.append(lines.pop(0))

> > >                 test_case.name = match.group(2)

> > > +               skip_match = OK_NOT_OK_SKIP.match(line)

> > > +               if skip_match:

> > > +                       test_case.status = TestStatus.SKIPPED

> > > +                       return True

> > >                 if test_case.status == TestStatus.TEST_CRASHED:

> > >                         return True

> > >                 if match.group(1) == 'ok':

> > > @@ -188,16 +195,16 @@ def parse_subtest_plan(lines: List[str]) -> Optional[int]:

> > >                 return None

> > >

> > >  def max_status(left: TestStatus, right: TestStatus) -> TestStatus:

> > > -       if left == TestStatus.TEST_CRASHED or right == TestStatus.TEST_CRASHED:

> > > +       if left == right:

> > > +               return left

> > > +       elif left == TestStatus.TEST_CRASHED or right == TestStatus.TEST_CRASHED:

> > >                 return TestStatus.TEST_CRASHED

> > >         elif left == TestStatus.FAILURE or right == TestStatus.FAILURE:

> > >                 return TestStatus.FAILURE

> > > -       elif left != TestStatus.SUCCESS:

> > > -               return left

> > > -       elif right != TestStatus.SUCCESS:

> > > +       elif left == TestStatus.SKIPPED:

> > >                 return right

> > >         else:

> > > -               return TestStatus.SUCCESS

> > > +               return left

> > >

> > >  def parse_ok_not_ok_test_suite(lines: List[str],

> > >                                test_suite: TestSuite,

> > > @@ -214,6 +221,9 @@ def parse_ok_not_ok_test_suite(lines: List[str],

> > >                         test_suite.status = TestStatus.SUCCESS

> > >                 else:

> > >                         test_suite.status = TestStatus.FAILURE

> > > +               skip_match = OK_NOT_OK_SKIP.match(line)

> > > +               if skip_match:

> > > +                       test_suite.status = TestStatus.SKIPPED

> > >                 suite_index = int(match.group(2))

> > >                 if suite_index != expected_suite_index:

> > >                         print_with_timestamp(

> > > @@ -224,8 +234,8 @@ def parse_ok_not_ok_test_suite(lines: List[str],

> > >         else:

> > >                 return False

> > >

> > > -def bubble_up_errors(statuses: Iterable[TestStatus]) -> TestStatus:

> > > -       return reduce(max_status, statuses, TestStatus.SUCCESS)

> > > +def bubble_up_errors(status_list: Iterable[TestStatus]) -> TestStatus:

> > > +       return reduce(max_status, status_list, TestStatus.SKIPPED)

> > >

> > >  def bubble_up_test_case_errors(test_suite: TestSuite) -> TestStatus:

> > >         max_test_case_status = bubble_up_errors(x.status for x in test_suite.cases)

> > > @@ -315,9 +325,12 @@ def print_and_count_results(test_result: TestResult) -> Tuple[int, int, int]:

> >

> > Btw, this type annotation is out of date.

>

> Oops: will fix and/or replace with the below.

>

> > But I think an ever growing Tuple is too cumbersome, how about this?

> >

>

> Yeah, this does seem cleaner: I'll put this or something like it in v2.

>

> > diff --git a/tools/testing/kunit/kunit_parser.py

> > b/tools/testing/kunit/kunit_parser.py

> > index 6b5dd26b479d..055ee1e4d19d 100644

> > --- a/tools/testing/kunit/kunit_parser.py

> > +++ b/tools/testing/kunit/kunit_parser.py

> > @@ -6,6 +6,7 @@

> >  # Author: Felix Guo <felixguoxiuping@gmail.com>

> >  # Author: Brendan Higgins <brendanhiggins@google.com>

> >

> > +from dataclasses import dataclass

> >  import re

> >

> >  from collections import namedtuple

> > @@ -321,11 +322,19 @@ def parse_test_result(lines: List[str]) -> TestResult:

> >         else:

> >                 return TestResult(TestStatus.NO_TESTS, [], lines)

> >

> > -def print_and_count_results(test_result: TestResult) -> Tuple[int, int, int]:

> > -       total_tests = 0

> > -       failed_tests = 0

> > -       crashed_tests = 0

> > -       skipped_tests = 0

> > +#  Note: This would require Python 3.7. We currently only required

> > 3.6 (enum.auto). We can do it by hand to avoid that, if we want.

>

> Hmm... I'm generally loath to increase the version requirement for

> something this simple, so might look into doing a version of this

> without the dataclass.


I think the same argument applies to enum.auto when we can just
manually assign values :P

But yes, I'd suggest not using it.
You'd just need to manually write the __init__() in that case (you
can't use namedtuple since we need to modify the fields, but also I
prefer having type annotations on my fields).

I only used @dataclass to make my example easier to write since I'm lazy.

>

>

> > +@dataclass

> > +class TestCounts:

> > +       passed: int = 0

> > +       failed: int = 0

> > +       skipped: int = 0

> > +       crashed: int = 0

> > +

> > +       def total(self) -> int:

> > +               return self.passed + self.failed + self.skipped + self.crashed

> > +

> > +def print_and_count_results(test_result: TestResult) -> TestCounts:

> > +       counts = TestCounts()

> >         for test_suite in test_result.suites:

> >                 if test_suite.status == TestStatus.SUCCESS:

> >                         print_suite_divider(green('[PASSED] ') +

> > test_suite.name)

> > @@ -336,39 +345,33 @@ def print_and_count_results(test_result:

> > TestResult) -> Tuple[int, int, int]:

> >                 else:

> >                         print_suite_divider(red('[FAILED] ') + test_suite.name)

> >                 for test_case in test_suite.cases:

> > -                       total_tests += 1

> >                         if test_case.status == TestStatus.SUCCESS:

> > +                               counts.passed += 1

> >                                 print_with_timestamp(green('[PASSED]

> > ') + test_case.name)

> >                         elif test_case.status == TestStatus.SKIPPED:

> > -                               skipped_tests += 1

> > +                               counts.skipped += 1

> >                                 print_with_timestamp(yellow('[SKIPPED]

> > ') + test_case.name)

> >                         elif test_case.status == TestStatus.TEST_CRASHED:

> > -                               crashed_tests += 1

> > +                               counts.crashed += 1

> >                                 print_with_timestamp(red('[CRASHED] '

> > + test_case.name))

> >                                 print_log(map(yellow, test_case.log))

> >                                 print_with_timestamp('')

> >                         else:

> > -                               failed_tests += 1

> > +                               counts.failed += 1

> >                                 print_with_timestamp(red('[FAILED] ')

> > + test_case.name)

> >                                 print_log(map(yellow, test_case.log))

> >                                 print_with_timestamp('')

> > -       return total_tests, failed_tests, crashed_tests, skipped_tests

> > +       return counts

> >

> >  def parse_run_tests(kernel_output) -> TestResult:

> > -       total_tests = 0

> > -       failed_tests = 0

> > -       crashed_tests = 0

> > -       skipped_tests = 0

> > +       counts = TestCounts()

> >         test_result =

> > parse_test_result(list(isolate_kunit_output(kernel_output)))

> >         if test_result.status == TestStatus.NO_TESTS:

> >                 print(red('[ERROR] ') + yellow('no tests run!'))

> >         elif test_result.status == TestStatus.FAILURE_TO_PARSE_TESTS:

> >                 print(red('[ERROR] ') + yellow('could not parse test results!'))

> >         else:

> > -               (total_tests,

> > -                failed_tests,

> > -                crashed_tests,

> > -                skipped_tests) = print_and_count_results(test_result)

> > +               counts = print_and_count_results(test_result)

> >         print_with_timestamp(DIVIDER)

> >         if test_result.status == TestStatus.SUCCESS:

> >                 fmt = green

> > @@ -378,5 +381,5 @@ def parse_run_tests(kernel_output) -> TestResult:

> >                 fmt =red

> >         print_with_timestamp(

> >                 fmt('Testing complete. %d tests run. %d failed. %d

> > crashed. %d skipped.' %

> > -                   (total_tests, failed_tests, crashed_tests, skipped_tests)))

> > +                   (counts.total(), counts.failed, counts.crashed,

> > counts.skipped)))

> >         return test_result

> >

> > >         total_tests = 0

> > >         failed_tests = 0

> > >         crashed_tests = 0

> > > +       skipped_tests = 0

> > >         for test_suite in test_result.suites:

> > >                 if test_suite.status == TestStatus.SUCCESS:

> > >                         print_suite_divider(green('[PASSED] ') + test_suite.name)

> > > +               elif test_suite.status == TestStatus.SKIPPED:

> > > +                       print_suite_divider(yellow('[SKIPPED] ') + test_suite.name)

> > >                 elif test_suite.status == TestStatus.TEST_CRASHED:

> > >                         print_suite_divider(red('[CRASHED] ' + test_suite.name))

> > >                 else:

> > > @@ -326,6 +339,9 @@ def print_and_count_results(test_result: TestResult) -> Tuple[int, int, int]:

> > >                         total_tests += 1

> > >                         if test_case.status == TestStatus.SUCCESS:

> > >                                 print_with_timestamp(green('[PASSED] ') + test_case.name)

> > > +                       elif test_case.status == TestStatus.SKIPPED:

> > > +                               skipped_tests += 1

> > > +                               print_with_timestamp(yellow('[SKIPPED] ') + test_case.name)

> > >                         elif test_case.status == TestStatus.TEST_CRASHED:

> > >                                 crashed_tests += 1

> > >                                 print_with_timestamp(red('[CRASHED] ' + test_case.name))

> > > @@ -336,12 +352,13 @@ def print_and_count_results(test_result: TestResult) -> Tuple[int, int, int]:

> > >                                 print_with_timestamp(red('[FAILED] ') + test_case.name)

> > >                                 print_log(map(yellow, test_case.log))

> > >                                 print_with_timestamp('')

> > > -       return total_tests, failed_tests, crashed_tests

> > > +       return total_tests, failed_tests, crashed_tests, skipped_tests

> > >

> > >  def parse_run_tests(kernel_output) -> TestResult:

> > >         total_tests = 0

> > >         failed_tests = 0

> > >         crashed_tests = 0

> > > +       skipped_tests = 0

> > >         test_result = parse_test_result(list(isolate_kunit_output(kernel_output)))

> > >         if test_result.status == TestStatus.NO_TESTS:

> > >                 print(red('[ERROR] ') + yellow('no tests run!'))

> > > @@ -350,10 +367,16 @@ def parse_run_tests(kernel_output) -> TestResult:

> > >         else:

> > >                 (total_tests,

> > >                  failed_tests,

> > > -                crashed_tests) = print_and_count_results(test_result)

> > > +                crashed_tests,

> > > +                skipped_tests) = print_and_count_results(test_result)

> > >         print_with_timestamp(DIVIDER)

> > > -       fmt = green if test_result.status == TestStatus.SUCCESS else red

> > > +       if test_result.status == TestStatus.SUCCESS:

> > > +               fmt = green

> > > +       elif test_result.status == TestStatus.SKIPPED:

> > > +               fmt = yellow

> > > +       else:

> > > +               fmt =red

> > >         print_with_timestamp(

> > > -               fmt('Testing complete. %d tests run. %d failed. %d crashed.' %

> > > -                   (total_tests, failed_tests, crashed_tests)))

> > > +               fmt('Testing complete. %d tests run. %d failed. %d crashed. %d skipped.' %

> > > +                   (total_tests, failed_tests, crashed_tests, skipped_tests)))

> > >         return test_result

> > > diff --git a/tools/testing/kunit/kunit_tool_test.py b/tools/testing/kunit/kunit_tool_test.py

> > > index 2e809dd956a7..a51e70cafcc1 100755

> > > --- a/tools/testing/kunit/kunit_tool_test.py

> > > +++ b/tools/testing/kunit/kunit_tool_test.py

> > > @@ -183,6 +183,28 @@ class KUnitParserTest(unittest.TestCase):

> > >                         kunit_parser.TestStatus.TEST_CRASHED,

> > >                         result.status)

> > >

> > > +       def test_skipped_test(self):

> > > +               skipped_log = test_data_path('test_skip_tests.log')

> > > +               file = open(skipped_log)

> > > +               result = kunit_parser.parse_run_tests(file.readlines())

> > > +

> > > +               # A skipped test does not fail the whole suite.

> > > +               self.assertEqual(

> > > +                       kunit_parser.TestStatus.SUCCESS,

> > > +                       result.status)

> > > +               file.close()

> > > +

> > > +       def test_skipped_all_tests(self):

> > > +               skipped_log = test_data_path('test_skip_all_tests.log')

> > > +               file = open(skipped_log)

> > > +               result = kunit_parser.parse_run_tests(file.readlines())

> > > +

> > > +               self.assertEqual(

> > > +                       kunit_parser.TestStatus.SKIPPED,

> > > +                       result.status)

> > > +               file.close()

> > > +

> > > +

> > >         def test_ignores_prefix_printk_time(self):

> > >                 prefix_log = test_data_path('test_config_printk_time.log')

> > >                 with open(prefix_log) as file:

> > > --

> > > 2.31.1.818.g46aad6cb9e-goog

> > >

> > > --

> > > You received this message because you are subscribed to the Google Groups "KUnit Development" group.

> > > To unsubscribe from this group and stop receiving emails from it, send an email to kunit-dev+unsubscribe@googlegroups.com.

> > > To view this discussion on the web visit https://groups.google.com/d/msgid/kunit-dev/20210526081112.3652290-2-davidgow%40google.com.
diff mbox series

Patch

diff --git a/include/kunit/test.h b/include/kunit/test.h
index b68c61348121..40b536da027e 100644
--- a/include/kunit/test.h
+++ b/include/kunit/test.h
@@ -105,6 +105,18 @@  struct kunit;
 #define KUNIT_SUBTEST_INDENT		"    "
 #define KUNIT_SUBSUBTEST_INDENT		"        "
 
+/**
+ * enum kunit_status - Type of result for a test or test suite
+ * @KUNIT_SUCCESS: Denotes the test suite has not failed nor been skipped
+ * @KUNIT_FAILURE: Denotes the test has failed.
+ * @KUNIT_SKIPPED: Denotes the test has been skipped.
+ */
+enum kunit_status {
+	KUNIT_SUCCESS,
+	KUNIT_FAILURE,
+	KUNIT_SKIPPED,
+};
+
 /**
  * struct kunit_case - represents an individual test case.
  *
@@ -148,13 +160,20 @@  struct kunit_case {
 	const void* (*generate_params)(const void *prev, char *desc);
 
 	/* private: internal use only. */
-	bool success;
+	enum kunit_status status;
 	char *log;
 };
 
-static inline char *kunit_status_to_string(bool status)
+static inline char *kunit_status_to_string(enum kunit_status status)
 {
-	return status ? "ok" : "not ok";
+	switch (status) {
+	case KUNIT_SKIPPED:
+	case KUNIT_SUCCESS:
+		return "ok";
+	case KUNIT_FAILURE:
+		return "not ok";
+	}
+	return "invalid";
 }
 
 /**
@@ -212,6 +231,7 @@  struct kunit_suite {
 	struct kunit_case *test_cases;
 
 	/* private: internal use only */
+	char status_comment[256];
 	struct dentry *debugfs;
 	char *log;
 };
@@ -245,19 +265,21 @@  struct kunit {
 	 * be read after the test case finishes once all threads associated
 	 * with the test case have terminated.
 	 */
-	bool success; /* Read only after test_case finishes! */
 	spinlock_t lock; /* Guards all mutable test state. */
+	enum kunit_status status; /* Read only after test_case finishes! */
 	/*
 	 * Because resources is a list that may be updated multiple times (with
 	 * new resources) from any thread associated with a test case, we must
 	 * protect it with some type of lock.
 	 */
 	struct list_head resources; /* Protected by lock. */
+
+	char status_comment[256];
 };
 
 static inline void kunit_set_failure(struct kunit *test)
 {
-	WRITE_ONCE(test->success, false);
+	WRITE_ONCE(test->status, KUNIT_FAILURE);
 }
 
 void kunit_init_test(struct kunit *test, const char *name, char *log);
@@ -348,7 +370,7 @@  static inline int kunit_run_all_tests(void)
 #define kunit_suite_for_each_test_case(suite, test_case)		\
 	for (test_case = suite->test_cases; test_case->run_case; test_case++)
 
-bool kunit_suite_has_succeeded(struct kunit_suite *suite);
+enum kunit_status kunit_suite_has_succeeded(struct kunit_suite *suite);
 
 /*
  * Like kunit_alloc_resource() below, but returns the struct kunit_resource
@@ -612,6 +634,40 @@  void kunit_cleanup(struct kunit *test);
 
 void kunit_log_append(char *log, const char *fmt, ...);
 
+/**
+ * kunit_mark_skipped() - Marks @test_or_suite as skipped
+ *
+ * @test_or_suite: The test context object.
+ * @fmt:  A printk() style format string.
+ *
+ * Marks the test as skipped. @fmt is given output as the test status
+ * comment, typically the reason the test was skipped.
+ *
+ * Test execution continues after kunit_mark_skipped() is called.
+ */
+#define kunit_mark_skipped(test_or_suite, fmt, ...)			\
+	do {								\
+		WRITE_ONCE((test_or_suite)->status, KUNIT_SKIPPED);	\
+		scnprintf((test_or_suite)->status_comment, 256, fmt, ##__VA_ARGS__); \
+	} while (0)
+
+/**
+ * kunit_skip() - Marks @test_or_suite as skipped
+ *
+ * @test_or_suite: The test context object.
+ * @fmt:  A printk() style format string.
+ *
+ * Skips the test. @fmt is given output as the test status
+ * comment, typically the reason the test was skipped.
+ *
+ * Test execution is halted after kunit_skip() is called.
+ */
+#define kunit_skip(test_or_suite, fmt, ...)				\
+	do {								\
+		kunit_mark_skipped((test_or_suite), fmt, ##__VA_ARGS__);\
+		kunit_try_catch_throw(&((test_or_suite)->try_catch));	\
+	} while (0)
+
 /*
  * printk and log to per-test or per-suite log buffer.  Logging only done
  * if CONFIG_KUNIT_DEBUGFS is 'y'; if it is 'n', no log is allocated/used.
diff --git a/lib/kunit/kunit-test.c b/lib/kunit/kunit-test.c
index 69f902440a0e..d69efcbed624 100644
--- a/lib/kunit/kunit-test.c
+++ b/lib/kunit/kunit-test.c
@@ -437,7 +437,47 @@  static void kunit_log_test(struct kunit *test)
 #endif
 }
 
+static void kunit_status_set_failure_test(struct kunit *test)
+{
+	struct kunit fake;
+
+	kunit_init_test(&fake, "fake test", NULL);
+
+	KUNIT_EXPECT_EQ(test, fake.status, (enum kunit_status)KUNIT_SUCCESS);
+	kunit_set_failure(&fake);
+	KUNIT_EXPECT_EQ(test, fake.status, (enum kunit_status)KUNIT_FAILURE);
+}
+
+static void kunit_status_mark_skipped_test(struct kunit *test)
+{
+	struct kunit fake;
+
+	kunit_init_test(&fake, "fake test", NULL);
+
+	/* Before: Should be SUCCESS with no comment. */
+	KUNIT_EXPECT_EQ(test, fake.status, KUNIT_SUCCESS);
+	KUNIT_EXPECT_STREQ(test, fake.status_comment, "");
+
+	/* Mark the test as skipped. */
+	kunit_mark_skipped(&fake, "Accepts format string: %s", "YES");
+
+	/* After: Should be SKIPPED with our comment. */
+	KUNIT_EXPECT_EQ(test, fake.status, (enum kunit_status)KUNIT_SKIPPED);
+	KUNIT_EXPECT_STREQ(test, fake.status_comment, "Accepts format string: YES");
+}
+
+static struct kunit_case kunit_status_test_cases[] = {
+	KUNIT_CASE(kunit_status_set_failure_test),
+	KUNIT_CASE(kunit_status_mark_skipped_test),
+	{}
+};
+
+static struct kunit_suite kunit_status_test_suite = {
+	.name = "kunit_status",
+	.test_cases = kunit_status_test_cases,
+};
+
 kunit_test_suites(&kunit_try_catch_test_suite, &kunit_resource_test_suite,
-		  &kunit_log_test_suite);
+		  &kunit_log_test_suite, &kunit_status_test_suite);
 
 MODULE_LICENSE("GPL v2");
diff --git a/lib/kunit/test.c b/lib/kunit/test.c
index 2f6cc0123232..0ee07705d2b0 100644
--- a/lib/kunit/test.c
+++ b/lib/kunit/test.c
@@ -98,12 +98,14 @@  static void kunit_print_subtest_start(struct kunit_suite *suite)
 
 static void kunit_print_ok_not_ok(void *test_or_suite,
 				  bool is_test,
-				  bool is_ok,
+				  enum kunit_status status,
 				  size_t test_number,
-				  const char *description)
+				  const char *description,
+				  const char *directive)
 {
 	struct kunit_suite *suite = is_test ? NULL : test_or_suite;
 	struct kunit *test = is_test ? test_or_suite : NULL;
+	const char *directive_header = (status == KUNIT_SKIPPED) ? " # SKIP " : "";
 
 	/*
 	 * We do not log the test suite results as doing so would
@@ -114,25 +116,31 @@  static void kunit_print_ok_not_ok(void *test_or_suite,
 	 * representation.
 	 */
 	if (suite)
-		pr_info("%s %zd - %s\n",
-			kunit_status_to_string(is_ok),
-			test_number, description);
+		pr_info("%s %zd - %s%s%s\n",
+			kunit_status_to_string(status),
+			test_number, description,
+			directive_header, directive ? directive : "");
 	else
-		kunit_log(KERN_INFO, test, KUNIT_SUBTEST_INDENT "%s %zd - %s",
-			  kunit_status_to_string(is_ok),
-			  test_number, description);
+		kunit_log(KERN_INFO, test,
+			  KUNIT_SUBTEST_INDENT "%s %zd - %s%s%s",
+			  kunit_status_to_string(status),
+			  test_number, description,
+			  directive_header, directive ? directive : "");
 }
 
-bool kunit_suite_has_succeeded(struct kunit_suite *suite)
+enum kunit_status kunit_suite_has_succeeded(struct kunit_suite *suite)
 {
 	const struct kunit_case *test_case;
+	enum kunit_status status = KUNIT_SKIPPED;
 
 	kunit_suite_for_each_test_case(suite, test_case) {
-		if (!test_case->success)
-			return false;
+		if (test_case->status == KUNIT_FAILURE)
+			return KUNIT_FAILURE;
+		else if (test_case->status == KUNIT_SUCCESS)
+			status = KUNIT_SUCCESS;
 	}
 
-	return true;
+	return status;
 }
 EXPORT_SYMBOL_GPL(kunit_suite_has_succeeded);
 
@@ -143,7 +151,8 @@  static void kunit_print_subtest_end(struct kunit_suite *suite)
 	kunit_print_ok_not_ok((void *)suite, false,
 			      kunit_suite_has_succeeded(suite),
 			      kunit_suite_counter++,
-			      suite->name);
+			      suite->name,
+			      suite->status_comment);
 }
 
 unsigned int kunit_test_case_num(struct kunit_suite *suite,
@@ -252,7 +261,8 @@  void kunit_init_test(struct kunit *test, const char *name, char *log)
 	test->log = log;
 	if (test->log)
 		test->log[0] = '\0';
-	test->success = true;
+	test->status = KUNIT_SUCCESS;
+	test->status_comment[0] = '\0';
 }
 EXPORT_SYMBOL_GPL(kunit_init_test);
 
@@ -376,7 +386,8 @@  static void kunit_run_case_catch_errors(struct kunit_suite *suite,
 	context.test_case = test_case;
 	kunit_try_catch_run(try_catch, &context);
 
-	test_case->success = test->success;
+	test_case->status = test->status;
+
 }
 
 int kunit_run_tests(struct kunit_suite *suite)
@@ -388,7 +399,6 @@  int kunit_run_tests(struct kunit_suite *suite)
 
 	kunit_suite_for_each_test_case(suite, test_case) {
 		struct kunit test = { .param_value = NULL, .param_index = 0 };
-		bool test_success = true;
 
 		if (test_case->generate_params) {
 			/* Get initial param. */
@@ -398,7 +408,6 @@  int kunit_run_tests(struct kunit_suite *suite)
 
 		do {
 			kunit_run_case_catch_errors(suite, test_case, &test);
-			test_success &= test_case->success;
 
 			if (test_case->generate_params) {
 				if (param_desc[0] == '\0') {
@@ -410,7 +419,7 @@  int kunit_run_tests(struct kunit_suite *suite)
 					  KUNIT_SUBTEST_INDENT
 					  "# %s: %s %d - %s",
 					  test_case->name,
-					  kunit_status_to_string(test.success),
+					  kunit_status_to_string(test.status),
 					  test.param_index + 1, param_desc);
 
 				/* Get next param. */
@@ -420,9 +429,10 @@  int kunit_run_tests(struct kunit_suite *suite)
 			}
 		} while (test.param_value);
 
-		kunit_print_ok_not_ok(&test, true, test_success,
+		kunit_print_ok_not_ok(&test, true, test_case->status,
 				      kunit_test_case_num(suite, test_case),
-				      test_case->name);
+				      test_case->name,
+				      test.status_comment);
 	}
 
 	kunit_print_subtest_end(suite);
@@ -434,6 +444,7 @@  EXPORT_SYMBOL_GPL(kunit_run_tests);
 static void kunit_init_suite(struct kunit_suite *suite)
 {
 	kunit_debugfs_create_suite(suite);
+	suite->status_comment[0] = '\0';
 }
 
 int __kunit_test_suites_init(struct kunit_suite * const * const suites)