diff --git a/llama_stack/providers/tests/conftest.py b/llama_stack/providers/tests/conftest.py index 66c185cbf6..bd59812e0d 100644 --- a/llama_stack/providers/tests/conftest.py +++ b/llama_stack/providers/tests/conftest.py @@ -63,7 +63,8 @@ def pytest_configure(config): key, value = env_var.split("=", 1) os.environ[key] = value - config.pluginmanager.register(Report(config)) + if config.getoption("--config") is not None: + config.pluginmanager.register(Report(config)) def pytest_addoption(parser): diff --git a/llama_stack/providers/tests/report.py b/llama_stack/providers/tests/report.py index 6cc1734dba..bc1c132633 100644 --- a/llama_stack/providers/tests/report.py +++ b/llama_stack/providers/tests/report.py @@ -6,6 +6,7 @@ from collections import defaultdict +from pathlib import Path import pytest from llama_models.datatypes import CoreModelId @@ -66,83 +67,66 @@ class Report: def __init__(self, _config): self.report_data = defaultdict(dict) self.test_data = dict() + self.inference_tests = defaultdict(dict) @pytest.hookimpl(tryfirst=True) def pytest_runtest_logreport(self, report): # This hook is called in several phases, including setup, call and teardown # The test is considered failed / error if any of the outcomes is not "Passed" outcome = _process_outcome(report) + data = { + "outcome": report.outcome, + "longrepr": report.longrepr, + "name": report.nodeid, + } if report.nodeid not in self.test_data: - self.test_data[report.nodeid] = outcome + self.test_data[report.nodeid] = data elif self.test_data[report.nodeid] != outcome and outcome != "Passed": - self.test_data[report.nodeid] = outcome - - def pytest_html_results_summary(self, prefix, summary, postfix): - prefix.append("

Inference Providers:

") - for provider in self.report_data.keys(): - prefix.extend( - [ - f"

{ provider }

", - "") + test_nodeids = self.inference_tests[provider][api] + report.append(f"\n - /{api}. Test coverage:") + report.extend(self._generate_test_result_short(test_nodeids)) - @pytest.hookimpl(tryfirst=True) - def pytest_runtest_makereport(self, item, call): - if call.when != "setup": - return - # generate the mapping from provider, api/functionality to test nodeid - provider = item.callspec.params.get("inference_stack") - if provider is not None: - api, functionality = self._process_function_name(item.name.split("[")[0]) - - api_test_funcs = self.report_data[provider].get(api, set()) - functionality_test_funcs = self.report_data[provider].get( - functionality, set() - ) - api_test_funcs.add(item.nodeid) - functionality_test_funcs.add(item.nodeid) - self.report_data[provider][api] = api_test_funcs - self.report_data[provider][functionality] = functionality_test_funcs + report.append("\n - **Functionality:**") + for functionality in FUNCTIONALITIES: + test_nodeids = self.inference_tests[provider][functionality] + report.append(f"\n - {functionality}. Test coverage:") + report.extend(self._generate_test_result_short(test_nodeids)) + + output_file = Path("pytest_report.md") + output_file.write_text("\n".join(report)) + print(f"\n Report generated: {output_file.absolute()}") + + @pytest.hookimpl(trylast=True) + def pytest_collection_modifyitems(self, session, config, items): + for item in items: + inference = item.callspec.params.get("inference_stack") + if "inference" in item.nodeid: + api, functionality = self._process_function_name(item.nodeid) + api_tests = self.inference_tests[inference].get(api, set()) + functionality_tests = self.inference_tests[inference].get( + functionality, set() + ) + api_tests.add(item.nodeid) + functionality_tests.add(item.nodeid) + self.inference_tests[inference][api] = api_tests + self.inference_tests[inference][functionality] = functionality_tests def _process_function_name(self, function_name): api, functionality = None, None @@ -160,3 +144,25 @@ def _print_result_icon(self, result): else: # result == "Failed" or result == "Error": return "❌" + + def get_function_name(self, nodeid): + """Extract function name from nodeid. + + Examples: + - 'tests/test_math.py::test_addition' -> 'test_addition' + - 'tests/test_math.py::TestClass::test_method' -> 'TestClass.test_method' + """ + parts = nodeid.split("::") + if len(parts) == 2: # Simple function + return parts[1] + elif len(parts) == 3: # Class method + return f"{parts[1]}.{parts[2]}" + return nodeid # Fallback to full nodeid if pattern doesn't match + + def _generate_test_result_short(self, test_nodeids): + report = [] + for nodeid in test_nodeids: + name = self.get_function_name(self.test_data[nodeid]["name"]) + result = self.test_data[nodeid]["outcome"] + report.append(f" - {name}. Result: {result}") + return report