diff --git a/CHANGELOG.md b/CHANGELOG.md index 835a2a5..bdc39a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 3b34e6d..e139630 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/allure.go b/allure.go index 74fbedd..81b023a 100644 --- a/allure.go +++ b/allure.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "math" "os" "path" @@ -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) @@ -111,6 +112,8 @@ type PluginAllure struct { queuedSetups syncutil.MutexGuarded[[]*PluginAllure] queuedTearDowns syncutil.MutexGuarded[[]*PluginAllure] + + maxAttachmentSize int64 } // Plugin implements [testoplugin.Plugin]. @@ -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(...))) @@ -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) @@ -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() @@ -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) +} diff --git a/interface.go b/interface.go index ed374d1..51d5833 100644 --- a/interface.go +++ b/interface.go @@ -116,6 +116,12 @@ type Interface interface { 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(...))) diff --git a/options.go b/options.go index 7e6cbe2..1de023b 100644 --- a/options.go +++ b/options.go @@ -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) {