diff --git a/os_testr/subunit2html.py b/os_testr/subunit2html.py
index 096a91b..a710b9f 100755
--- a/os_testr/subunit2html.py
+++ b/os_testr/subunit2html.py
@@ -617,8 +617,12 @@ class HtmlOutput(testtools.TestResult):
# unittest does not seems to run in any particular order.
# Here at least we want to group them together by class.
rmap = {}
- classes = []
+ # Differentiate between classes that have test failures so we can sort
+ # them at the top of the html page for easier troubleshooting
+ failclasses = []
+ passclasses = []
for n, t, o, e in result_list:
+ classes = failclasses if n == 1 or n == 2 else passclasses
if hasattr(t, '_tests'):
for inner_test in t._tests:
self._add_cls(rmap, classes, inner_test,
@@ -626,7 +630,9 @@ class HtmlOutput(testtools.TestResult):
else:
self._add_cls(rmap, classes, t, (n, t, o, e))
classort = lambda s: str(s)
- sortedclasses = sorted(classes, key=classort)
+ sortedfailclasses = sorted(failclasses, key=classort)
+ sortedpassclasses = sorted(passclasses, key=classort)
+ sortedclasses = sortedfailclasses + sortedpassclasses
r = [(cls, rmap[str(cls)]) for cls in sortedclasses]
return r
diff --git a/os_testr/tests/test_subunit2html.py b/os_testr/tests/test_subunit2html.py
index f3196a5..994eed6 100644
--- a/os_testr/tests/test_subunit2html.py
+++ b/os_testr/tests/test_subunit2html.py
@@ -10,6 +10,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import sys
+
from ddt import data
from ddt import ddt
from subunit import RemotedTestCase
@@ -29,3 +31,46 @@ class TestSubunit2html(base.TestCase):
cls_ = []
obj_._add_cls({}, cls_, test_, ())
self.assertEqual("example.path.to.test", cls_[0].name)
+
+ @data(RemotedTestCase, PlaceHolder)
+ def test_result_sorting(self, test_cls):
+ tests = []
+ for i in range(9):
+ tests.append(test_cls('example.path.to.test%d.method' % i))
+ # addFailure, addError, and addSkip need the real exc_info
+ try:
+ raise Exception('fake')
+ except Exception:
+ err = sys.exc_info()
+ obj = subunit2html.HtmlOutput()
+ obj.addSuccess(tests[3])
+ obj.addSuccess(tests[1])
+ # example.path.to.test2 has a failure
+ obj.addFailure(tests[2], err)
+ obj.addSkip(tests[0], err)
+ obj.addSuccess(tests[8])
+ # example.path.to.test5 has a failure (error)
+ obj.addError(tests[5], err)
+ # example.path.to.test4 has a failure
+ obj.addFailure(tests[4], err)
+ obj.addSuccess(tests[7])
+ # example.path.to.test6 has a failure
+ obj.addFailure(tests[6], err)
+ sorted_result = obj._sortResult(obj.result)
+ # _sortResult returns a list of results of format:
+ # [(class, [test_result_tuple, ...]), ...]
+ # sorted by str(class)
+ #
+ # Classes with failures (2, 4, 5, and 6) should be sorted separately
+ # at the top. The rest of the classes should be in sorted order after.
+ expected_class_order = ['example.path.to.test2',
+ 'example.path.to.test4',
+ 'example.path.to.test5',
+ 'example.path.to.test6',
+ 'example.path.to.test0',
+ 'example.path.to.test1',
+ 'example.path.to.test3',
+ 'example.path.to.test7',
+ 'example.path.to.test8']
+ for i, r in enumerate(sorted_result):
+ self.assertEqual(expected_class_order[i], str(r[0]))