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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- An option `WithMaxAttachmentSize` to limit a size of large attachments by trimming them.

## [1.0.3] - 2026-05-26

### Fixed
Expand Down
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ func (Suite) TestRun(t T) {

## Attachments

### Deduplication

Allure plugin features an efficient hashsum-based attachment deduplication mechanism.

It will automatically keep track of written attachments so that a single attachment added, say, 100 times, will result
Expand All @@ -152,12 +154,52 @@ But it can be enabled with `WithDeduplicateAttachments` option, if you need it.

```go
func init() {
testo.Option(
testo.Options(
allure.WithDeduplicateAttachments(true),
)
}
```

### Size limit

Large attachments can be automatically trimmed to ensure that your allure report
won't grow more than needed.

This feature is disabled by default, but you can enable it with `WithMaxAttachmentSize` option:

```go
// WithMaxAttachmentSize specifies a limit for the size of
// each attachment as a number of bytes.
//
// If greater than zero, attachments are automatically trimmed of their suffix
// if their size exceeds this limit.
//
// Trimmed attachments are always of type [TextPlain] with suffix
// message added stating that an attachment exceeds a size limit.
//
// WithMaxAttachmentSize(1000) // 1 KB
func WithMaxAttachmentSize(bytes int64) testoplugin.Option
```

Example:

```go
func init() {
testo.Options(
allure.WithMaxAttachmentSize(1000),
)
}
```

With this option set, large attachment will look like this:

```txt
some large attachment with
endless text and...

...size exceeds 1000 bytes limit
```

## Options

This plugin provides several options for configuring default behavior.
Expand Down
95 changes: 85 additions & 10 deletions allure.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"math"
"os"
"path"
Expand Down Expand Up @@ -37,7 +38,7 @@ const (

// TODO(metafates): use tools.go pattern or go tool command when this plugin is moved into separate repo.

//go:generate ifacemaker -f $GOFILE -o interface.go -s PluginAllure -i Interface -p $GOPACKAGE -e Plugin -y "Interface defines allure plugin interface.\nUseful for writing helpers which require allure methods but can't rely on concrete type." -x -e panicked -e status -e asResult -e parameters -e links -e attachments -e allRawAttachments -e title -e asStep -e timeBoundaries -e steps -e containers -e beforeEach -e afterEach -e hooks -e addMessage -e addTrace -e overrides -e results -e resultsGroupParametrized -e afterAll -e writeResults -e writeContainers -e writeAttachments -e writeAttachment -e writeProperties -e writeCategories -e labels -e attachmentPath -e baseName -e testCaseID -e historyID -e resultsFlattenParametrized -e statusDetails -e suiteName -e plugin -e beforeAll -e cleanup -e writeReport -e plan -e applyOptions -e fullName -e createOutputDir -e asContainer -e beforeEachSub -e afterEachSub -e propagatedStatusDetails -e hookDescendants -e descendants -e testChildren -e hasTestNeighbors -e subtest -e parentSuiteName
//go:generate ifacemaker -f $GOFILE -o interface.go -s PluginAllure -i Interface -p $GOPACKAGE -e Plugin -y "Interface defines allure plugin interface.\nUseful for writing helpers which require allure methods but can't rely on concrete type." -x -e panicked -e status -e asResult -e parameters -e links -e attachments -e allRawAttachments -e title -e asStep -e timeBoundaries -e steps -e containers -e beforeEach -e afterEach -e hooks -e addMessage -e addTrace -e overrides -e results -e resultsGroupParametrized -e afterAll -e writeResults -e writeContainers -e writeAttachments -e writeAttachment -e writeProperties -e writeCategories -e labels -e attachmentPath -e baseName -e testCaseID -e historyID -e resultsFlattenParametrized -e statusDetails -e suiteName -e plugin -e beforeAll -e cleanup -e writeReport -e plan -e applyOptions -e fullName -e createOutputDir -e asContainer -e beforeEachSub -e afterEachSub -e propagatedStatusDetails -e hookDescendants -e descendants -e testChildren -e hasTestNeighbors -e subtest -e attach -e parentSuiteName

var _ Interface = (*PluginAllure)(nil)

Expand Down Expand Up @@ -111,6 +112,8 @@ type PluginAllure struct {

queuedSetups syncutil.MutexGuarded[[]*PluginAllure]
queuedTearDowns syncutil.MutexGuarded[[]*PluginAllure]

maxAttachmentSize int64
}

// Plugin implements [testoplugin.Plugin].
Expand Down Expand Up @@ -330,6 +333,12 @@ func (a *PluginAllure) Known() {

// Attach an attachment.
//
// If option [WithMaxAttachmentSize] is specified, passed
// attachment is automatically trimmed of its suffix.
//
// Trimmed attachments are always of type [TextPlain] with suffix
// message added stating that an attachment exceeds a size limit.
//
// See [Bytes] and [File] to create an attachment.
//
// t.Attach("login page", allure.Bytes([]byte(...)))
Expand All @@ -341,6 +350,57 @@ func (a *PluginAllure) Attach(name string, at Attachment) {
return
}

if a.maxAttachmentSize <= 0 {
a.attach(name, at)

return
}

if size, ok := at.SizeHint(); ok && size <= a.maxAttachmentSize {
a.attach(name, at)

return
}

// fast path (most common).
if b, ok := at.(AttachmentBytes); ok {
trimmed := trimmedAttachment(
b.Data,
b.Type(),
a.maxAttachmentSize,
)

a.attach(name, trimmed)

return
}

r, err := at.Open()
if err != nil {
a.attach(name, at)

return
}

defer func() { _ = r.Close() }()

// add one extra byte so that [trimmedAttachment] trims it,
// yet we don't load more data in memory than needed.
data, err := io.ReadAll(io.LimitReader(r, a.maxAttachmentSize+1))
if err != nil {
a.attach(name, at)

return
}

trimmed := trimmedAttachment(data, at.Type(), a.maxAttachmentSize)

a.attach(name, trimmed)
}

func (a *PluginAllure) attach(name string, at Attachment) {
a.Helper()

if err := mkdir(a.outputDir); err != nil {
a.Logf("allure: failed to create output dir: %v", err)

Expand Down Expand Up @@ -1202,17 +1262,12 @@ func (a *PluginAllure) overrides() testoplugin.Overrides {

Parallel: func(f testoplugin.FuncParallel) testoplugin.FuncParallel {
return func() {
// If other plugin calls Parallel before each with TryFirst priority
// there exists a chance that timeTest.Start would equal to zero,
// making beforeParallel a huge duration and breaking other timings.
//
// So in that case, if start is zero we should update it here.
if a.timeTest.Start.IsZero() {
a.timeTest.Start = time.Now()
// if start is zero it means we are inside a BeforeEach hook of other plugin.
// in that case, real test has not started yet, so we shouldn't compute beforeParallel timing.
if !a.timeTest.Start.IsZero() {
a.beforeParallel = time.Since(a.timeTest.Start)
}

a.beforeParallel = time.Since(a.timeTest.Start)

f()

a.timeTest.Start = time.Now()
Expand Down Expand Up @@ -1571,3 +1626,23 @@ func (a *PluginAllure) propagatedStatusDetails(descendants []*PluginAllure) Stat
Trace: strings.Join(traces, "\n\n\n"),
}
}

func trimmedAttachment(
data []byte,
mediaType MediaType,
limit int64,
) AttachmentBytes {
if len(data) <= int(limit) {
return Bytes(data).As(mediaType)
}

// we can't use format like "want %d, got %d" because len(data)
// isn't always a "full" attachment.

suffix := fmt.Sprintf("...\n\n...size exceeds %d bytes limit", limit)

data = data[:limit]
data = append(data, suffix...)

return Bytes(data).As(TextPlain)
}
6 changes: 6 additions & 0 deletions interface.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,25 @@ func WithLinks(links ...Link) testoplugin.Option {
}
}

// WithMaxAttachmentSize specifies a limit for the size of
// each attachment as a number of bytes.
//
// If greater than zero, attachments are automatically trimmed of their suffix
// if their size exceeds this limit.
//
// Trimmed attachments are always of type [TextPlain] with suffix
// message added stating that an attachment exceeds a size limit.
//
// WithMaxAttachmentSize(1000) // 1 KB
func WithMaxAttachmentSize(bytes int64) testoplugin.Option {
return testoplugin.Option{
Value: option(func(a *PluginAllure) {
a.maxAttachmentSize = bytes
}),
Propagate: true,
}
}

func asStep() testoplugin.Option {
return testoplugin.Option{
Value: option(func(a *PluginAllure) {
Expand Down
Loading