Enforce a single result per subtest for pytest results

Previously, if we got a test failure and an error during teardown, we'd end up with
multiple results for the same test. This just picks the final result for the test.

Differential Revision: https://phabricator.services.mozilla.com/D151007

bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1778083
gecko-commit: 228851107e0f9f76f38d2a9558cb377ce50ec52e
gecko-reviewers: webdriver-reviewers, jdescottes
diff --git a/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py b/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py
index 2da0813..f22d948 100644
--- a/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py
+++ b/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py
@@ -16,6 +16,7 @@
 import os
 import shutil
 import tempfile
+from collections import OrderedDict
 
 
 pytest = None
@@ -79,7 +80,8 @@
     finally:
         os.environ = old_environ
 
-    return (harness.outcome, subtests.results)
+    subtests_results = [(key,) + value for (key, value) in subtests.results.items()]
+    return (harness.outcome, subtests_results)
 
 
 class HarnessResultRecorder:
@@ -100,7 +102,7 @@
 
 class SubtestResultRecorder:
     def __init__(self):
-        self.results = []
+        self.results = OrderedDict()
 
     def pytest_runtest_logreport(self, report):
         if report.passed and report.when == "call":
@@ -144,8 +146,15 @@
     def record(self, test, status, message=None, stack=None):
         if stack is not None:
             stack = str(stack)
-        new_result = (test.split("::")[-1], status, message, stack)
-        self.results.append(new_result)
+        # Ensure we get a single result per subtest; pytest will sometimes
+        # call pytest_runtest_logreport more than once per test e.g. if
+        # it fails and then there's an error during teardown.
+        subtest_id = test.split("::")[-1]
+        if subtest_id in self.results and status == "PASS":
+            # This shouldn't happen, but never overwrite an existing result with PASS
+            return
+        new_result = (status, message, stack)
+        self.results[subtest_id] = new_result
 
 
 class TemporaryDirectory: