diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 57649250..b3ce5fdf 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -261,6 +261,21 @@ or by setting the :code:`render_collapsed` in a configuration file (pytest.ini, **NOTE:** Setting :code:`render_collapsed` will, unlike the query parameter, affect all statuses. + +Keep original test cases run order +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, all rows in the **Results** table will be sorted by category. Failed test cases will be placed at the top of the **Results** table. + +This behavior can be customized by setting the :code:`keep_original_order` in a configuration file (pytest.ini, setup.cfg, etc). + +.. code-block:: ini + + [pytest] + keep_original_order = True + +**NOTE:** Setting :code:`keep_original_order` will turn off a possibility of changing order by table headers. + Controlling Test Result Visibility Via Query Params ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/pytest_html/html_report.py b/src/pytest_html/html_report.py index 66e10f07..09e660eb 100644 --- a/src/pytest_html/html_report.py +++ b/src/pytest_html/html_report.py @@ -36,15 +36,20 @@ def __init__(self, logfile, config): def _appendrow(self, outcome, report): result = TestResult(outcome, report, self.logfile, self.config) if result.row_table is not None: - index = bisect.bisect_right(self.results, result) - self.results.insert(index, result) tbody = html.tbody( result.row_table, class_="{} results-table-row".format(result.outcome.lower()), ) if result.row_extra is not None: tbody.append(result.row_extra) - self.test_logs.insert(index, tbody) + + if self.config.getini("keep_original_order"): + self.results.append(result) + self.test_logs.append(tbody) + else: + index = bisect.bisect_right(self.results, result) + self.results.insert(index, result) + self.test_logs.insert(index, tbody) def append_passed(self, report): if report.when == "call": @@ -145,11 +150,21 @@ def _generate_report(self, session): if i < len(outcomes): summary.append(", ") + sortable_class_attrib = "sortable" + initial_sort_class_attrib = "initial-sort" + if self.config.getini("keep_original_order"): + sortable_class_attrib = "" + initial_sort_class_attrib = "" + cells = [ - html.th("Result", class_="sortable result initial-sort", col="result"), - html.th("Test", class_="sortable", col="name"), - html.th("Duration", class_="sortable", col="duration"), - html.th("Links", class_="sortable links", col="links"), + html.th( + "Result", + class_=f"{sortable_class_attrib} result {initial_sort_class_attrib}", + col="result", + ), + html.th("Test", class_=f"{sortable_class_attrib}", col="name"), + html.th("Duration", class_=f"{sortable_class_attrib}", col="duration"), + html.th("Links", class_=f"{sortable_class_attrib} links", col="links"), ] session.config.hook.pytest_html_results_table_header(cells=cells) diff --git a/src/pytest_html/plugin.py b/src/pytest_html/plugin.py index 0034da19..80c74488 100644 --- a/src/pytest_html/plugin.py +++ b/src/pytest_html/plugin.py @@ -59,6 +59,13 @@ def pytest_addoption(parser): help="A list of regexes corresponding to environment " "table variables whose values should be redacted from the report", ) + parser.addini( + "keep_original_order", + type="bool", + default=False, + help="Keep original order of test cases run in report. " + "That option turns off a possibility of order rows by table headers.", + ) def pytest_configure(config): diff --git a/testing/test_pytest_html.py b/testing/test_pytest_html.py index b13dd4c6..1157d888 100644 --- a/testing/test_pytest_html.py +++ b/testing/test_pytest_html.py @@ -1095,6 +1095,123 @@ def test_pass(): assert len(re.findall(collapsed_html, html)) == expected_count assert_results(html, tests=2, passed=1, failed=1) + @pytest.mark.parametrize( + "keep_original_order, expected_table_header_tag", + [ + ( + False, + [ + {"html_tag": '', "expected_count": 4}, + {"html_tag": '', "expected_count": 1}, + ], + ), + ( + True, + [ + {"html_tag": '', "expected_count": 0}, + {"html_tag": '', "expected_count": 0}, + ], + ), + ], + ids=( + "keep_original_order option is not set", + "keep_original_order option is set", + ), + ) + def test_keep_original_order_option_remove_any_sort_class_from_headers( + self, testdir, keep_original_order, expected_table_header_tag + ): + testdir.makeini( + f""" + [pytest] + keep_original_order = {keep_original_order} + """ + ) + testdir.makepyfile( + """ + def test_pass1(): + assert True + + def test_fail1(): + assert False + + def test_pass2(): + assert True + + def test_fail2(): + assert False + """ + ) + result, html = run(testdir) + assert result.ret == 1 + for expect_element in expected_table_header_tag: + assert ( + len(re.findall(expect_element["html_tag"], html)) + == expect_element["expected_count"] + ) + assert_results(html, tests=4, passed=2, failed=2) + + @pytest.mark.parametrize( + "keep_original_order, expected_order", + [ + ( + False, + [ + "test_fail1", + "test_fail2", + "test_pass1", + "test_pass2", + ], + ), + ( + True, + [ + "test_pass1", + "test_fail1", + "test_pass2", + "test_fail2", + ], + ), + ], + ids=( + "keep_original_order option is not set", + "keep_original_order option is set", + ), + ) + def test_keep_original_order_option_hold_test_run_order( + self, testdir, keep_original_order, expected_order + ): + testdir.makeini( + f""" + [pytest] + keep_original_order = {keep_original_order} + """ + ) + testdir.makepyfile( + """ + def test_pass1(): + assert True + + def test_fail1(): + assert False + + def test_pass2(): + assert True + + def test_fail2(): + assert False + """ + ) + result, html = run(testdir) + assert result.ret == 1 + result_report_test_order = re.findall('.*', html) + assert len(result_report_test_order) == len(expected_order) + for expected_test_name, result_test_name in zip( + expected_order, result_report_test_order + ): + assert expected_test_name in result_test_name + assert_results(html, tests=4, passed=2, failed=2) + def test_setup_and_teardown_in_html(self, testdir): testdir.makepyfile( """