Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/behave/tests/features/steps/attachment_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
6 changes: 6 additions & 0 deletions qase-behave/changelog.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
95 changes: 95 additions & 0 deletions qase-behave/docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion qase-behave/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
7 changes: 7 additions & 0 deletions qase-behave/src/qase/behave/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
105 changes: 92 additions & 13 deletions qase-behave/src/qase/behave/qase_global.py
Original file line number Diff line number Diff line change
@@ -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__)
Expand All @@ -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.")

Expand Down Expand Up @@ -67,24 +89,81 @@ 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'):
self._current_scenario.attachments = []

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:
"""
Expand Down