Repository: python-tap/tappy

Issue:
Sub tests are not included
If the unittests include [subtests](https://docs.python.org/3/library/unittest.html#distinguishing-test-iterations-using-subtests) the results of these subtests are not picked up.

Also, if the test class contains only failing subtests, the tap file may not be generated at all.


Comments:

Patch:
diff --git a/AUTHORS b/AUTHORS
index 566404e..06d46c5 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -5,6 +5,7 @@ Contributors
 
 * Andrew McNamara
 * Chris Clarke
+* Erik Cederstrand
 * Marc Abramowitz
 * Mark E. Hamilton
 * Matt Layman
diff --git a/docs/releases.rst b/docs/releases.rst
index b04ab19..ee84c7e 100644
--- a/docs/releases.rst
+++ b/docs/releases.rst
@@ -5,6 +5,7 @@ Version 3.0, To Be Released
 ---------------------------
 
 * Drop support for Python 2 (it is end-of-life).
+* Add support for subtests.
 
 Version 2.6.2, Released October 20, 2019
 ----------------------------------------
diff --git a/tap/runner.py b/tap/runner.py
index 94134e1..10e8510 100644
--- a/tap/runner.py
+++ b/tap/runner.py
@@ -17,6 +17,18 @@ class TAPTestResult(TextTestResult):
     def __init__(self, stream, descriptions, verbosity):
         super(TAPTestResult, self).__init__(stream, descriptions, verbosity)
 
+    def addSubTest(self, test, subtest, err):
+        super(TAPTestResult, self).addSubTest(test, subtest, err)
+        if err is not None:
+            diagnostics = formatter.format_exception(err)
+            self.tracker.add_not_ok(
+                self._cls_name(test),
+                self._description(subtest),
+                diagnostics=diagnostics,
+            )
+        else:
+            self.tracker.add_ok(self._cls_name(test), self._description(subtest))
+
     def stopTestRun(self):
         """Once the test run is complete, generate each of the TAP files."""
         super(TAPTestResult, self).stopTestRun()


Test patch:
diff --git a/tap/tests/test_result.py b/tap/tests/test_result.py
index 06fb7e1..b5c1c9a 100644
--- a/tap/tests/test_result.py
+++ b/tap/tests/test_result.py
@@ -1,7 +1,9 @@
 # Copyright (c) 2019, Matt Layman and contributors
 
+import contextlib
 import os
 import unittest
+import unittest.case
 
 from tap.i18n import _
 from tap.runner import TAPTestResult
@@ -13,6 +15,14 @@ class FakeTestCase(unittest.TestCase):
     def runTest(self):
         pass
 
+    @contextlib.contextmanager
+    def subTest(self, *args, **kwargs):
+        try:
+            self._subtest = unittest.case._SubTest(self, object(), {})
+            yield
+        finally:
+            self._subtest = None
+
     def __call__(self, result):
         pass
 
@@ -70,3 +80,25 @@ class TestTAPTestResult(TestCase):
         self.assertEqual(
             line.directive.text, "TODO {}".format(_("(unexpected success)"))
         )
+
+    def test_adds_subtest_success(self):
+        """Test that the runner handles subtest success results."""
+        result = self._make_one()
+        test = FakeTestCase()
+        with test.subTest():
+            result.addSubTest(test, test._subtest, None)
+        line = result.tracker._test_cases["FakeTestCase"][0]
+        self.assertTrue(line.ok)
+
+    def test_adds_subtest_failure(self):
+        """Test that the runner handles subtest failure results."""
+        result = self._make_one()
+        # Python 3 does some extra testing in unittest on exceptions so fake
+        # the cause as if it were raised.
+        ex = Exception()
+        ex.__cause__ = None
+        test = FakeTestCase()
+        with test.subTest():
+            result.addSubTest(test, test._subtest, (ex.__class__, ex, None))
+        line = result.tracker._test_cases["FakeTestCase"][0]
+        self.assertFalse(line.ok)