From 37cabf6e8594a1c280685ff5a7214e692c6376a6 Mon Sep 17 00:00:00 2001 From: Dmitrii Gridnev Date: Tue, 13 Jan 2026 17:07:53 +0300 Subject: [PATCH] feat: enhance attachment functionality in Qase Behave - Updated the attachment methods to allow for step-specific attachments using `qase.attach_to_step()`. - Improved the QaseFormatter to manage current step attachments more effectively. - Added documentation for attaching files and content to both test cases and specific steps, enhancing usability and clarity for users. These changes improve the flexibility and functionality of the attachment system in Qase Behave, allowing for better tracking of test step-related data. --- .../tests/features/steps/attachment_steps.py | 2 +- qase-behave/changelog.md | 6 + qase-behave/docs/usage.md | 95 ++++++++++++++++ qase-behave/pyproject.toml | 2 +- qase-behave/src/qase/behave/formatter.py | 7 ++ qase-behave/src/qase/behave/qase_global.py | 105 +++++++++++++++--- 6 files changed, 202 insertions(+), 15 deletions(-) diff --git a/examples/behave/tests/features/steps/attachment_steps.py b/examples/behave/tests/features/steps/attachment_steps.py index e137079c..f31acea5 100644 --- a/examples/behave/tests/features/steps/attachment_steps.py +++ b/examples/behave/tests/features/steps/attachment_steps.py @@ -22,7 +22,7 @@ def step_impl(context): @when('I attach content as text') def step_impl(context): # Attach text content directly - qase.attach(content="This is some text content", file_name="text_content.txt") + qase.attach_to_step(content="This is some text content", file_name="text_content.txt") @when('I attach JSON data') diff --git a/qase-behave/changelog.md b/qase-behave/changelog.md index 74223f4f..60b0a90a 100644 --- a/qase-behave/changelog.md +++ b/qase-behave/changelog.md @@ -1,3 +1,9 @@ +# qase-behave 2.0.6 + +## What's new + +- Supported attachments in step level. + # qase-behave 2.0.5 ## What's new diff --git a/qase-behave/docs/usage.md b/qase-behave/docs/usage.md index a3f8edc7..4c254ed8 100644 --- a/qase-behave/docs/usage.md +++ b/qase-behave/docs/usage.md @@ -94,3 +94,98 @@ Feature: Example tests When I run it Then it should pass ``` + +--- + +## Adding Attachments to Tests + +Qase Behave supports attaching files and content to test results. You can attach files or content either to the test case (scenario level) or to a specific test step. + +### Attach to Test Case + +Use `qase.attach()` to attach files or content to the test case. This is useful for screenshots, logs, or data files that are relevant to the entire test scenario. + +### Example: + +```gherkin +Feature: Example tests + + @qase.id:1 + Scenario: Example test with attachments + Given I have a test with a file + When I attach a screenshot + Then the attachment should be in the test case +``` + +```python +from behave import * +from qase.behave import qase + +@given('I have a test with a file') +def step_impl(context): + # Attach an existing file to the test case + qase.attach(file_path="/path/to/your/file.txt") + +@when('I attach a screenshot') +def step_impl(context): + # Attach binary data (e.g., screenshot) to the test case + screenshot_data = b"binary_screenshot_data" + qase.attach( + content=screenshot_data, + file_name="screenshot.png", + mime_type="image/png" + ) +``` + +### Attach to Test Step + +Use `qase.attach_to_step()` to attach files or content directly to a specific test step. This is useful when you want to associate attachments with a particular step execution. + +### Example: + +```gherkin +Feature: Example tests + + @qase.id:1 + Scenario: Example test with step attachments + Given I have a test + When I attach a screenshot to this step + Then the attachment should be in the step +``` + +```python +from behave import * +from qase.behave import qase + +@when('I attach a screenshot to this step') +def step_impl(context): + # Attach binary data to the current step + screenshot_data = b"binary_screenshot_data" + qase.attach_to_step( + content=screenshot_data, + file_name="step_screenshot.png", + mime_type="image/png" + ) + + # Attach text content to the current step + qase.attach_to_step( + content="Step execution log", + file_name="step_log.txt" + ) +``` + +### Method Parameters + +Both `qase.attach()` and `qase.attach_to_step()` accept the same parameters: + +- `file_path`: Path to the file to attach (mutually exclusive with `content`) +- `content`: Content to attach as string or bytes (mutually exclusive with `file_path`) +- `file_name`: Name for the attachment (auto-detected from `file_path` if not provided) +- `mime_type`: MIME type of the attachment (auto-detected if not provided) + +**Notes:** + +- Either `file_path` or `content` must be provided, but not both +- If `file_name` is not provided, it will be derived from `file_path` or default to "attachment.txt" +- If `mime_type` is not provided, it will be auto-detected from the file extension or default to "text/plain" +- Attachments are automatically included in the test result when the scenario completes diff --git a/qase-behave/pyproject.toml b/qase-behave/pyproject.toml index 3ebf4fd0..d8b81b94 100644 --- a/qase-behave/pyproject.toml +++ b/qase-behave/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "qase-behave" -version = "2.0.5" +version = "2.0.6" description = "Qase Behave Plugin for Qase TestOps and Qase Report" readme = "README.md" keywords = ["qase", "behave", "plugin", "testops", "report", "qase reporting", "test observability"] diff --git a/qase-behave/src/qase/behave/formatter.py b/qase-behave/src/qase/behave/formatter.py index e67b3ead..c449f9ca 100644 --- a/qase-behave/src/qase/behave/formatter.py +++ b/qase-behave/src/qase/behave/formatter.py @@ -41,10 +41,15 @@ def scenario(self, scenario: Scenario): self.__current_scenario = parse_scenario(scenario) # Update global qase object with current scenario qase._set_current_scenario(self.__current_scenario) + # Clear current step when starting new scenario + qase._set_current_step(None) pass def result(self, result: Step): step = parse_step(result) + # Set current step to allow future attachments and apply pending ones + qase._set_current_step(step) + if step.execution.status != 'passed': # Check if it's an assertion error or other error is_assertion_error = False @@ -64,6 +69,8 @@ def result(self, result: Step): if result.error_message: self.__current_scenario.execution.stacktrace = result.error_message self.__current_scenario.steps.append(step) + # Clear current step after adding to scenario + qase._set_current_step(None) pass def eof(self): diff --git a/qase-behave/src/qase/behave/qase_global.py b/qase-behave/src/qase/behave/qase_global.py index e3611dfb..5c6c1e37 100644 --- a/qase-behave/src/qase/behave/qase_global.py +++ b/qase-behave/src/qase/behave/qase_global.py @@ -1,7 +1,7 @@ import os import mimetypes import logging -from typing import Optional, Union +from typing import Optional, Union, Dict from qase.commons.models import Attachment logger = logging.getLogger(__name__) @@ -15,29 +15,51 @@ class QaseGlobal: def __init__(self): self._current_scenario = None + self._current_step = None + self._pending_step_attachments: Dict[str, list] = {} def _set_current_scenario(self, scenario): """Set the current scenario for attachment tracking""" self._current_scenario = scenario + # Clear pending attachments when scenario changes + self._pending_step_attachments.clear() - def attach(self, - file_path: Optional[str] = None, - content: Optional[Union[str, bytes]] = None, - file_name: Optional[str] = None, - mime_type: Optional[str] = None) -> None: + def _set_current_step(self, step): + """Set the current step for attachment tracking""" + self._current_step = step + # If step is set, apply any pending attachments + if step is not None: + step_id = step.id + if step_id in self._pending_step_attachments: + for attachment in self._pending_step_attachments[step_id]: + step.add_attachment(attachment) + del self._pending_step_attachments[step_id] + # Also apply attachments stored for the next step + if '_next_step' in self._pending_step_attachments: + for attachment in self._pending_step_attachments['_next_step']: + step.add_attachment(attachment) + del self._pending_step_attachments['_next_step'] + + def _create_attachment(self, + file_path: Optional[str] = None, + content: Optional[Union[str, bytes]] = None, + file_name: Optional[str] = None, + mime_type: Optional[str] = None) -> Attachment: """ - Attach a file or content to the current test scenario. + Create an Attachment object from the provided parameters. Args: file_path: Path to the file to attach content: Content to attach (string or bytes) file_name: Name for the attachment (if not provided, will be derived from file_path) mime_type: MIME type of the attachment (if not provided, will be auto-detected) + + Returns: + Attachment object + + Raises: + ValueError: If both file_path and content are provided, or if neither is provided """ - - if self._current_scenario is None: - raise RuntimeError("No active scenario. Cannot attach file.") - if file_path and content: raise ValueError("Either file_path or content must be provided, not both.") @@ -67,17 +89,37 @@ def attach(self, # Create attachment if file_path: - attachment = Attachment( + return Attachment( file_name=file_name, mime_type=mime_type, file_path=file_path ) else: - attachment = Attachment( + return Attachment( file_name=file_name, mime_type=mime_type, content=content ) + + def attach(self, + file_path: Optional[str] = None, + content: Optional[Union[str, bytes]] = None, + file_name: Optional[str] = None, + mime_type: Optional[str] = None) -> None: + """ + Attach a file or content to the current test scenario. + + Args: + file_path: Path to the file to attach + content: Content to attach (string or bytes) + file_name: Name for the attachment (if not provided, will be derived from file_path) + mime_type: MIME type of the attachment (if not provided, will be auto-detected) + """ + + if self._current_scenario is None: + raise RuntimeError("No active scenario. Cannot attach file.") + + attachment = self._create_attachment(file_path, content, file_name, mime_type) # Add attachment to current scenario if not hasattr(self._current_scenario, 'attachments'): @@ -85,6 +127,43 @@ def attach(self, self._current_scenario.attachments.append(attachment) + def attach_to_step(self, + file_path: Optional[str] = None, + content: Optional[Union[str, bytes]] = None, + file_name: Optional[str] = None, + mime_type: Optional[str] = None) -> None: + """ + Attach a file or content to the current test step. + If the step is not yet available, the attachment will be stored + and applied when the step is created. + + Args: + file_path: Path to the file to attach + content: Content to attach (string or bytes) + file_name: Name for the attachment (if not provided, will be derived from file_path) + mime_type: MIME type of the attachment (if not provided, will be auto-detected) + + Raises: + RuntimeError: If no active scenario is available + """ + + if self._current_scenario is None: + raise RuntimeError("No active scenario. Cannot attach file to step.") + + attachment = self._create_attachment(file_path, content, file_name, mime_type) + + # Add attachment to current step if available, otherwise store for later + if self._current_step is not None: + self._current_step.add_attachment(attachment) + else: + # Store attachment to be applied when step is created + # Use a temporary key that will be matched when step is set + # We'll use the step's line number or name as identifier + # For now, use a special key that will be applied to the next step + if '_next_step' not in self._pending_step_attachments: + self._pending_step_attachments['_next_step'] = [] + self._pending_step_attachments['_next_step'].append(attachment) + def comment(self, message: str) -> None: """