diff --git a/qase-python-commons/changelog.md b/qase-python-commons/changelog.md index 74107cf4..6137ca70 100644 --- a/qase-python-commons/changelog.md +++ b/qase-python-commons/changelog.md @@ -1,3 +1,9 @@ +# qase-python-commons@3.5.4 + +## What's new + +- Added support for filtering test results by status. + # qase-python-commons@3.5.3 ## What's new diff --git a/qase-python-commons/docs/STATUS_FILTER.md b/qase-python-commons/docs/STATUS_FILTER.md new file mode 100644 index 00000000..d3740da8 --- /dev/null +++ b/qase-python-commons/docs/STATUS_FILTER.md @@ -0,0 +1,154 @@ +# Status Filtering for Test Results + +This document describes the status filtering functionality that allows you to exclude test results with specific statuses from being sent to Qase TestOps. + +## Overview + +The status filter feature enables you to configure which test result statuses should be excluded from reporting. This is useful when you want to: + +- Skip reporting of passed tests to reduce noise +- Exclude certain statuses from specific test runs +- Focus on specific types of test results (e.g., only failures and errors) + +## Configuration + +### Configuration File + +You can configure status filtering in your `qase.config.json` file: + +```json +{ + "testops": { + "statusFilter": ["passed", "skipped"] + } +} +``` + +### Environment Variables + +You can also use environment variables: + +```bash +export QASE_TESTOPS_STATUS_FILTER="passed,skipped" +``` + +### Command Line Options + +For frameworks that support CLI options: + +```bash +# Pytest +pytest --qase-testops-status-filter="passed,skipped" + +# Tavern +pytest --qase-testops-status-filter="passed,skipped" +``` + +## Supported Statuses + +The following statuses can be used in the filter: + +- `passed` - Test passed successfully +- `failed` - Test failed +- `skipped` - Test was skipped +- `blocked` - Test was blocked +- `untested` - Test was not executed + +## Examples + +### Filter Out Passed Tests + +```json +{ + "testops": { + "statusFilter": ["passed"] + } +} +``` + +This configuration will exclude all passed tests from being sent to Qase TestOps. + +### Filter Out Multiple Statuses + +```json +{ + "testops": { + "statusFilter": ["passed", "skipped"] + } +} +``` + +This configuration will exclude both passed and skipped tests. + +### No Filtering + +```json +{ + "testops": { + "statusFilter": [] + } +} +``` + +Or simply omit the `statusFilter` field to disable filtering. + +## Behavior + +- **Filtering Logic**: Results with statuses listed in `statusFilter` are excluded from sending +- **Batch Processing**: Filtering is applied when results are sent in batches +- **Logging**: Filtered results are logged at debug level, and the count of filtered results is logged at info level +- **Performance**: Filtering occurs before sending, so no network requests are made for filtered results + +## Use Cases + +### CI/CD Pipelines + +In continuous integration, you might want to only report failures and errors: + +```json +{ + "testops": { + "statusFilter": ["passed", "skipped"] + } +} +``` + +### Development Testing + +During development, you might want to see all results: + +```json +{ + "testops": { + "statusFilter": [] + } +} +``` + +### Production Monitoring + +In production, you might want to focus on critical issues: + +```json +{ + "testops": { + "statusFilter": ["passed", "skipped", "blocked"] + } +} +``` + +## Framework Support + +Status filtering is supported across all Qase Python frameworks: + +- **qase-pytest**: Via config file, environment variables, and CLI options +- **qase-behave**: Via config file, environment variables, and behave userdata +- **qase-robotframework**: Via config file and environment variables +- **qase-tavern**: Via config file, environment variables, and CLI options + +## Notes + +- Filtering is applied at the result level, not at the step level +- Results with `None` status are not filtered (they are always sent) +- The filter is case-sensitive and must match the exact status strings +- Empty or invalid filter configurations are treated as no filtering diff --git a/qase-python-commons/pyproject.toml b/qase-python-commons/pyproject.toml index 9239281f..6d2bc310 100644 --- a/qase-python-commons/pyproject.toml +++ b/qase-python-commons/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "qase-python-commons" -version = "3.5.3" +version = "3.5.4" description = "A library for Qase TestOps and Qase Report" readme = "README.md" authors = [{name = "Qase Team", email = "support@qase.io"}] diff --git a/qase-python-commons/src/qase/commons/config.py b/qase-python-commons/src/qase/commons/config.py index 01a06f88..50bec06d 100644 --- a/qase-python-commons/src/qase/commons/config.py +++ b/qase-python-commons/src/qase/commons/config.py @@ -142,6 +142,14 @@ def __load_file_config(self): self.config.testops.configurations.set_create_if_not_exists( configurations.get("createIfNotExists")) + if testops.get("statusFilter"): + status_filter = testops.get("statusFilter") + if isinstance(status_filter, list): + self.config.testops.set_status_filter(status_filter) + elif isinstance(status_filter, str): + # Parse comma-separated string + self.config.testops.set_status_filter([s.strip() for s in status_filter.split(',')]) + if config.get("report"): report = config.get("report") @@ -262,6 +270,10 @@ def __load_env_config(self): if key == 'QASE_TESTOPS_CONFIGURATIONS_CREATE_IF_NOT_EXISTS': self.config.testops.configurations.set_create_if_not_exists(value) + if key == 'QASE_TESTOPS_STATUS_FILTER': + # Parse comma-separated string + self.config.testops.set_status_filter([s.strip() for s in value.split(',')]) + if key == 'QASE_REPORT_DRIVER': self.config.report.set_driver(value) diff --git a/qase-python-commons/src/qase/commons/models/config/testops.py b/qase-python-commons/src/qase/commons/models/config/testops.py index 56afc628..c9252931 100644 --- a/qase-python-commons/src/qase/commons/models/config/testops.py +++ b/qase-python-commons/src/qase/commons/models/config/testops.py @@ -48,6 +48,7 @@ class TestopsConfig(BaseModel): plan: PlanConfig = None batch: BatchConfig = None configurations: ConfigurationsConfig = None + status_filter: List[str] = None def __init__(self): self.api = ApiConfig() @@ -56,9 +57,13 @@ def __init__(self): self.plan = PlanConfig() self.configurations = ConfigurationsConfig() self.defect = False + self.status_filter = [] def set_project(self, project: str): self.project = project def set_defect(self, defect): self.defect = QaseUtils.parse_bool(defect) + + def set_status_filter(self, status_filter: List[str]): + self.status_filter = status_filter diff --git a/qase-python-commons/src/qase/commons/reporters/testops.py b/qase-python-commons/src/qase/commons/reporters/testops.py index c5e1de85..48ab86c3 100644 --- a/qase-python-commons/src/qase/commons/reporters/testops.py +++ b/qase-python-commons/src/qase/commons/reporters/testops.py @@ -86,15 +86,34 @@ def _send_results_threaded(self, results): def _send_results(self) -> None: if self.results: - # Acquire semaphore before starting the send operation - self.send_semaphore.acquire() - self.count_running_threads += 1 + # Filter results by status if status_filter is configured results_to_send = self.results.copy() + + if self.config.testops.status_filter and len(self.config.testops.status_filter) > 0: + filtered_results = [] + for result in results_to_send: + result_status = result.get_status() + if result_status and result_status not in self.config.testops.status_filter: + filtered_results.append(result) + else: + self.logger.log_debug(f"Filtering out result '{result.title}' with status '{result_status}'") + + results_to_send = filtered_results + self.logger.log_debug(f"Filtered {len(self.results) - len(results_to_send)} results by status filter") + + if results_to_send: + # Acquire semaphore before starting the send operation + self.send_semaphore.acquire() + self.count_running_threads += 1 + + # Start a new thread for sending results + send_thread = threading.Thread(target=self._send_results_threaded, args=(results_to_send,)) + send_thread.start() + else: + self.logger.log("No results to send after filtering", "info") + + # Clear results regardless of filtering self.results = [] - - # Start a new thread for sending results - send_thread = threading.Thread(target=self._send_results_threaded, args=(results_to_send,)) - send_thread.start() else: self.logger.log("No results to send", "info")