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
126 changes: 126 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The project started as a fork of testify, but over time it got its own runner an
+ [Test with attachments](#test-with-attachment)
+ [Run few parallel suites](#run-few-parallel-suites)
+ [Setup hooks](#setup-hooks)
+ [Access test status in hooks](#access-test-status-in-hooks)
+ [XSkip](#xskip)
+ [:rocket: Parametrized tests](#parametrized-test)
+ [Setup test](#setup-test)
Expand All @@ -38,6 +39,61 @@ Providing a separate package allows you to customize your work with allure.<br>

### What's new?

**New Feature: Access Test Status in AfterEach Hook**

#### GetCurrentTestResult method

Now you can access test execution result in `AfterEach` hook:<br>
- `t.GetCurrentTestResult()` - returns current test result with status and details (read-only copy)<br>

This allows you to perform conditional actions based on test status (Passed, Failed, Broken, Skipped).<br>

**Basic Example:**

```go
func (s *MySuite) AfterEach(t provider.T) {
result, ok := t.GetCurrentTestResult()
if ok && result.Status == allure.Failed {
// Save screenshot only for failed tests
screenshot := takeScreenshot()
t.WithNewAttachment("failure.png", allure.ImagePng, screenshot)

// Check error message
if strings.Contains(result.StatusDetails.Message, "Setup failed") {
t.Log("Test didn't run - BeforeEach hook failed, skipping cleanup")
}
}
}
```

**How it works with BeforeEach failures:**

When `BeforeEach` fails, the test doesn't run, but you can still detect the failure in `AfterEach`:

```go
func (s *MySuite) BeforeEach(t provider.T) {
// This will fail
t.Require().True(false, "Setup failed")
}

func (s *MySuite) AfterEach(t provider.T) {
result, ok := t.GetCurrentTestResult()
if ok && result.Status == allure.Failed &&
strings.Contains(result.StatusDetails.Message, "Setup failed") {
t.Log("BeforeEach failed - test was not executed")
}
}
```

**Important Notes:**

:information_source: This feature **does not change** hook or test behavior - it only provides read-only access to test status.<br>
:information_source: `GetCurrentTestResult()` returns `(*allure.CurrentResult, bool)` - a read-only copy with `Status` and `StatusDetails` fields.<br>
:information_source: Available **only in `AfterEach` hook** - returns `(nil, false)` in other contexts (BeforeEach, BeforeAll, AfterAll, test body).<br>
:information_source: For **parametrized tests** (using `t.Run()`): AfterEach is called once for the parent test, not for each subtest.<br>
:information_source: For **nested tests**: AfterEach sees the parent test status, which may be Passed even if subtests failed.<br>


**Release v0.6.17**

#### WithTestSetup/WithTestTeardown methods
Expand Down Expand Up @@ -668,6 +724,76 @@ Output to Allure:

![](.resources/example_befores_afters.png)

### [Access test status in hooks](examples/suite_demo/test_status_in_hooks_test.go)

You can access test execution result in `AfterEach` hook to perform conditional actions based on test status.

Test code:

```go
package suite_demo

import (
"strings"
"testing"

"github.com/ozontech/allure-go/pkg/allure"
"github.com/ozontech/allure-go/pkg/framework/provider"
"github.com/ozontech/allure-go/pkg/framework/suite"
)

type StatusInHooksSuite struct {
suite.Suite
}

func (s *StatusInHooksSuite) AfterEach(t provider.T) {
// Get the test result from execution context (read-only copy)
result, ok := t.GetCurrentTestResult()

if ok {
switch result.Status {
case allure.Failed:
// Check if BeforeEach failed
if strings.Contains(result.StatusDetails.Message, "Setup failed") {
t.Log("BeforeEach hook failed, skipping cleanup")
return
}

// Handle test failure
t.Log("Test failed")
// You can save screenshot, logs, etc.
// screenshot := takeScreenshot()
// t.WithNewAttachment("failure.png", allure.ImagePng, screenshot)

case allure.Passed:
t.Log("Test passed")
// Cleanup test data for successful tests

case allure.Broken:
t.Log("Test broken")
}
}
}

func (s *StatusInHooksSuite) TestExample(t provider.T) {
t.Title("Example test")
t.Require().Equal(1, 1)
}

func TestStatusInHooks(t *testing.T) {
suite.RunSuite(t, new(StatusInHooksSuite))
}
```

Key points:
- `t.GetCurrentTestResult()` returns `(*allure.CurrentResult, bool)` with test status and details in `AfterEach`
- `allure.CurrentResult` is a read-only copy containing `Status` and `StatusDetails` fields
- Available statuses: `allure.Passed`, `allure.Failed`, `allure.Broken`, `allure.Skipped`
- Returns `(nil, false)` in contexts other than `AfterEach` (e.g., in test body, `BeforeEach`, or `AfterAll`)
- Check `result.StatusDetails.Message` or `result.StatusDetails.Trace` to analyze failure reasons
- Does not change hook or test behavior - only provides read-only access to test status


### [XSkip](examples/suite_demo/fails_test.go)

Test code:
Expand Down
13 changes: 13 additions & 0 deletions pkg/allure/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ type Result struct {
ToPrint bool `json:"-"` // If false - the report will not be saved to a file
}

// CurrentResult Use for getting current test result in AfterEach hook
type CurrentResult struct {
Status Status `json:"status,omitempty"` // Status of the test execution
StatusDetails StatusDetail `json:"statusDetails,omitempty"` // Details about the test (for example, errors during test execution will be recorded here)
}

// NewResult Constructor Builds a new `allure.Result`. Sets the default values for the structure.
// ================================================
// |Field Value| Default |
Expand Down Expand Up @@ -342,3 +348,10 @@ func getMD5Hash(text string) string {
hash := md5.Sum([]byte(text))
return hex.EncodeToString(hash[:])
}

func (result *CurrentResult) GetStatusMessage() string {
return result.StatusDetails.Message
}
func (result *CurrentResult) GetStatusTrace() string {
return result.StatusDetails.Trace
}
11 changes: 11 additions & 0 deletions pkg/framework/core/allure_manager/ctx/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
type hooksCtx struct {
name string
container *allure.Container
result *allure.Result // Result of the test (for AfterEach hook)
}

// NewAfterAllCtx returns after all context
Expand All @@ -23,6 +24,11 @@ func NewAfterEachCtx(container *allure.Container) provider.ExecutionContext {
return &hooksCtx{container: container, name: constants.AfterEachContextName}
}

// NewAfterEachCtxWithResult returns after each context with test result
func NewAfterEachCtxWithResult(container *allure.Container, result *allure.Result) provider.ExecutionContext {
return &hooksCtx{container: container, result: result, name: constants.AfterEachContextName}
}

// NewBeforeAllCtx returns before all context
func NewBeforeAllCtx(container *allure.Container) provider.ExecutionContext {
return &hooksCtx{container: container, name: constants.BeforeAllContextName}
Expand All @@ -49,6 +55,11 @@ func (ctx *hooksCtx) GetName() string {
return ctx.name
}

// GetTestResult returns test result if available (for AfterEach hook)
func (ctx *hooksCtx) GetTestResult() *allure.Result {
return ctx.result
}

// AddAttachments adds attachment to the execution context
func (ctx *hooksCtx) AddAttachments(attachments ...*allure.Attachment) {
if len(attachments) == 0 {
Expand Down
123 changes: 123 additions & 0 deletions pkg/framework/core/allure_manager/ctx/hooks_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ctx

import (
"sync"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -95,3 +96,125 @@ func TestHooksCtx_AddAttachment(t *testing.T) {
require.Len(t, afterEach.container.Afters[0].Attachments, 1)
require.Equal(t, attach, afterEach.container.Afters[0].Attachments[0])
}

// Tests for GetTestResult functionality
func TestNewAfterEachCtxWithResult(t *testing.T) {
container := allure.NewContainer()
result := allure.NewResult("TestName", "TestFullName")
result.Status = allure.Passed

ctx := NewAfterEachCtxWithResult(container, result)
require.NotNil(t, ctx)

// Verify we can get the result back
retrievedResult := ctx.GetTestResult()
require.NotNil(t, retrievedResult)
require.Equal(t, result, retrievedResult)
require.Equal(t, allure.Passed, retrievedResult.Status)
require.Equal(t, "TestName", retrievedResult.Name)
}

func TestHooksCtx_GetTestResult_AfterEach(t *testing.T) {
container := allure.NewContainer()
testResult := allure.NewResult("TestName", "TestFullName")
testResult.Status = allure.Failed
testResult.SetStatusMessage("Test failed")

ctx := NewAfterEachCtxWithResult(container, testResult)

result := ctx.GetTestResult()
require.NotNil(t, result)
require.Equal(t, allure.Failed, result.Status)
require.Equal(t, "TestName", result.Name)
require.Equal(t, "Test failed", result.GetStatusMessage())
}

func TestHooksCtx_GetTestResult_NilForOtherContexts(t *testing.T) {
container := allure.NewContainer()

// BeforeEach should return nil
beforeEachCtx := NewBeforeEachCtx(container)
require.Nil(t, beforeEachCtx.GetTestResult())

// BeforeAll should return nil
beforeAllCtx := NewBeforeAllCtx(container)
require.Nil(t, beforeAllCtx.GetTestResult())

// AfterEach without result should return nil
afterEachCtx := NewAfterEachCtx(container)
require.Nil(t, afterEachCtx.GetTestResult())
}

// Tests for parametrized test scenarios
func TestHooksCtx_GetTestResult_ParametrizedTestScenario(t *testing.T) {
// Simulate a parametrized test where the parent test is Passed
// even though subtests may have failed
container := allure.NewContainer()
parentResult := allure.NewResult("TestParametrized", "FullTestParametrized")
parentResult.Status = allure.Passed // Parent can be Passed even if subtests fail

ctx := NewAfterEachCtxWithResult(container, parentResult)

result := ctx.GetTestResult()
require.NotNil(t, result)
require.Equal(t, allure.Passed, result.Status)
require.Equal(t, "TestParametrized", result.Name)
}

// Test for nested test scenario
func TestHooksCtx_GetTestResult_NestedTestScenario(t *testing.T) {
// Simulate nested tests where parent test status may differ from subtests
container := allure.NewContainer()

// Parent test can be Passed even if nested subtests failed
parentResult := allure.NewResult("TestNested", "FullTestNested")
parentResult.Status = allure.Passed

ctx := NewAfterEachCtxWithResult(container, parentResult)

result := ctx.GetTestResult()
require.NotNil(t, result)
require.Equal(t, allure.Passed, result.Status)
}

// Test for BeforeEach failure scenario
func TestHooksCtx_GetTestResult_BeforeEachFailure(t *testing.T) {
container := allure.NewContainer()

// When BeforeEach fails, test doesn't run but result is created with Failed status
result := allure.NewResult("TestWillNotRun", "FullTestWillNotRun")
result.Status = allure.Failed
result.SetStatusMessage("TestWillNotRun/BeforeEach setup was failed")

ctx := NewAfterEachCtxWithResult(container, result)

retrievedResult := ctx.GetTestResult()
require.NotNil(t, retrievedResult)
require.Equal(t, allure.Failed, retrievedResult.Status)
require.Contains(t, retrievedResult.GetStatusMessage(), "setup was failed")
}

// Test concurrent access safety
func TestHooksCtx_GetTestResult_ConcurrentAccess(t *testing.T) {
container := allure.NewContainer()
result := allure.NewResult("TestConcurrent", "FullTestConcurrent")
result.Status = allure.Passed

ctx := NewAfterEachCtxWithResult(container, result)

// Multiple goroutines reading the result
const numGoroutines = 10
var wg sync.WaitGroup
wg.Add(numGoroutines)

for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
r := ctx.GetTestResult()
require.NotNil(t, r)
require.Equal(t, allure.Passed, r.Status)
}()
}

wg.Wait()
}
4 changes: 4 additions & 0 deletions pkg/framework/core/allure_manager/ctx/test_ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ func (ctx *testCtx) GetName() string {
return ctx.name
}

func (ctx *testCtx) GetTestResult() *allure.Result {
return ctx.result
}

func (ctx *testCtx) AddAttachments(attachments ...*allure.Attachment) {
ctx.result.Attachments = append(ctx.result.Attachments, attachments...)
}
Loading