added support for Attachments (aka Embedddings) (#623)

* added support for attachments in the cucumber json and events output formats - done by sneaking the attachment into the context.Context object.
Этот коммит содержится в:
John Lonergan 2024-05-29 00:02:08 +01:00 коммит произвёл GitHub
родитель 4ade3314e8
коммит 201e526078
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
11 изменённых файлов: 218 добавлений и 21 удалений

2
.gitignore предоставленный
Просмотреть файл

@ -9,3 +9,5 @@ Gopkg.toml
.vscode .vscode
_artifacts _artifacts
vendor

Просмотреть файл

@ -11,6 +11,7 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
## [v0.14.1] ## [v0.14.1]
### Added ### Added
- Provide support for attachments / embeddings - ([623](https://github.com/cucumber/godog/pull/623) - [johnlon](https://github.com/johnlon))
- Provide testing.T-compatible interface on test context, allowing usage of assertion libraries such as testify's assert/require - ([571](https://github.com/cucumber/godog/pull/571) - [mrsheepuk](https://github.com/mrsheepuk)) - Provide testing.T-compatible interface on test context, allowing usage of assertion libraries such as testify's assert/require - ([571](https://github.com/cucumber/godog/pull/571) - [mrsheepuk](https://github.com/mrsheepuk))
- Created releasing guidelines - ([608](https://github.com/cucumber/godog/pull/608) - [glibas](https://github.com/glibas)) - Created releasing guidelines - ([608](https://github.com/cucumber/godog/pull/608) - [glibas](https://github.com/glibas))

Просмотреть файл

@ -580,3 +580,6 @@ A simple example can be [found here](/_examples/custom-formatter).
[contributing guide]: https://github.com/cucumber/godog/blob/main/CONTRIBUTING.md [contributing guide]: https://github.com/cucumber/godog/blob/main/CONTRIBUTING.md
[releasing guide]: https://github.com/cucumber/godog/blob/main/RELEASING.md [releasing guide]: https://github.com/cucumber/godog/blob/main/RELEASING.md
[community Slack]: https://cucumber.io/community#slack [community Slack]: https://cucumber.io/community#slack

Просмотреть файл

@ -12,6 +12,7 @@ package formatters
*/ */
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@ -139,6 +140,12 @@ type cukeMatch struct {
Location string `json:"location"` Location string `json:"location"`
} }
type cukeEmbedding struct {
Name string `json:"name"`
MimeType string `json:"mime_type"`
Data string `json:"data"`
}
type cukeStep struct { type cukeStep struct {
Keyword string `json:"keyword"` Keyword string `json:"keyword"`
Name string `json:"name"` Name string `json:"name"`
@ -147,6 +154,7 @@ type cukeStep struct {
Match cukeMatch `json:"match"` Match cukeMatch `json:"match"`
Result cukeResult `json:"result"` Result cukeResult `json:"result"`
DataTable []*cukeDataTableRow `json:"rows,omitempty"` DataTable []*cukeDataTableRow `json:"rows,omitempty"`
Embeddings []*cukeEmbedding `json:"embeddings,omitempty"`
} }
type cukeDataTableRow struct { type cukeDataTableRow struct {
@ -294,6 +302,21 @@ func (f *Cuke) buildCukeStep(pickle *messages.Pickle, stepResult models.PickleSt
cukeStep.Match.Location = fmt.Sprintf("%s:%d", pickle.Uri, step.Location.Line) cukeStep.Match.Location = fmt.Sprintf("%s:%d", pickle.Uri, step.Location.Line)
} }
if stepResult.Attachments != nil {
attachments := []*cukeEmbedding{}
for _, a := range stepResult.Attachments {
attachments = append(attachments, &cukeEmbedding{
Name: a.Name,
Data: base64.RawStdEncoding.EncodeToString(a.Data),
MimeType: a.MimeType,
})
}
if len(attachments) > 0 {
cukeStep.Embeddings = attachments
}
}
return cukeStep return cukeStep
} }

Просмотреть файл

@ -153,6 +153,31 @@ func (f *Events) step(pickle *messages.Pickle, pickleStep *messages.PickleStep)
if pickleStepResult.Err != nil { if pickleStepResult.Err != nil {
errMsg = pickleStepResult.Err.Error() errMsg = pickleStepResult.Err.Error()
} }
if pickleStepResult.Attachments != nil {
for _, attachment := range pickleStepResult.Attachments {
f.event(&struct {
Event string `json:"event"`
Location string `json:"location"`
Timestamp int64 `json:"timestamp"`
ContentEncoding string `json:"contentEncoding"`
FileName string `json:"fileName"`
MimeType string `json:"mimeType"`
Body string `json:"body"`
}{
"Attachment",
fmt.Sprintf("%s:%d", pickle.Uri, step.Location.Line),
utils.TimeNowFunc().UnixNano() / nanoSec,
messages.AttachmentContentEncoding_BASE64.String(),
attachment.Name,
attachment.MimeType,
string(attachment.Data),
})
}
}
f.event(&struct { f.event(&struct {
Event string `json:"event"` Event string `json:"event"`
Location string `json:"location"` Location string `json:"location"`

Просмотреть файл

@ -2,11 +2,12 @@ package formatters_test
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"testing" "testing"
@ -24,9 +25,7 @@ func Test_FmtOutput(t *testing.T) {
featureFiles, err := listFmtOutputTestsFeatureFiles() featureFiles, err := listFmtOutputTestsFeatureFiles()
require.Nil(t, err) require.Nil(t, err)
formatters := []string{"cucumber", "events", "junit", "pretty", "progress", "junit,pretty"} formatters := []string{"cucumber", "events", "junit", "pretty", "progress", "junit,pretty"}
for _, fmtName := range formatters { for _, fmtName := range formatters {
for _, featureFile := range featureFiles { for _, featureFile := range featureFiles {
testName := fmt.Sprintf("%s/%s", fmtName, featureFile) testName := fmt.Sprintf("%s/%s", fmtName, featureFile)
@ -65,6 +64,7 @@ func fmtOutputTest(fmtName, testName, featureFilePath string) func(*testing.T) {
ctx.Step(`^(?:a )?pending step$`, pendingStepDef) ctx.Step(`^(?:a )?pending step$`, pendingStepDef)
ctx.Step(`^(?:a )?passing step$`, passingStepDef) ctx.Step(`^(?:a )?passing step$`, passingStepDef)
ctx.Step(`^odd (\d+) and even (\d+) number$`, oddEvenStepDef) ctx.Step(`^odd (\d+) and even (\d+) number$`, oddEvenStepDef)
ctx.Step(`^(?:a )?a step with attachment$`, stepWithAttachment)
} }
return func(t *testing.T) { return func(t *testing.T) {
@ -74,7 +74,7 @@ func fmtOutputTest(fmtName, testName, featureFilePath string) func(*testing.T) {
t.Skipf("Couldn't find expected output file %q", expectOutputPath) t.Skipf("Couldn't find expected output file %q", expectOutputPath)
} }
expectedOutput, err := ioutil.ReadFile(expectOutputPath) expectedOutput, err := os.ReadFile(expectOutputPath)
require.NoError(t, err) require.NoError(t, err)
var buf bytes.Buffer var buf bytes.Buffer
@ -92,12 +92,23 @@ func fmtOutputTest(fmtName, testName, featureFilePath string) func(*testing.T) {
Options: &opts, Options: &opts,
}.Run() }.Run()
expected := string(expectedOutput) // normalise on unix line ending so expected vs actual works cross platform
actual := buf.String() expected := normalise(string(expectedOutput))
actual := normalise(buf.String())
assert.Equalf(t, expected, actual, "path: %s", expectOutputPath) assert.Equalf(t, expected, actual, "path: %s", expectOutputPath)
} }
} }
func normalise(s string) string {
m := regexp.MustCompile("fmt_output_test.go:[0-9]+")
normalised := m.ReplaceAllString(s, "fmt_output_test.go:XXX")
normalised = strings.Replace(normalised, "\r\n", "\n", -1)
normalised = strings.Replace(normalised, "\\r\\n", "\\n", -1)
return normalised
}
func passingStepDef() error { return nil } func passingStepDef() error { return nil }
func oddEvenStepDef(odd, even int) error { return oddOrEven(odd, even) } func oddEvenStepDef(odd, even int) error { return oddOrEven(odd, even) }
@ -115,3 +126,12 @@ func oddOrEven(odd, even int) error {
func pendingStepDef() error { return godog.ErrPending } func pendingStepDef() error { return godog.ErrPending }
func failingStepDef() error { return fmt.Errorf("step failed") } func failingStepDef() error { return fmt.Errorf("step failed") }
func stepWithAttachment(ctx context.Context) (context.Context, error) {
ctxOut := godog.Attach(ctx,
godog.Attachment{Body: []byte("TheData1"), FileName: "TheFilename1", MediaType: "text/plain"},
godog.Attachment{Body: []byte("TheData2"), FileName: "TheFilename2", MediaType: "text/plain"},
)
return ctxOut, nil
}

Просмотреть файл

@ -0,0 +1,46 @@
[
{
"uri": "formatter-tests/features/scenario_with_attachment.feature",
"id": "scenario-with-attachment",
"keyword": "Feature",
"name": "scenario with attachment",
"description": " describes\n an attachment\n feature",
"line": 1,
"elements": [
{
"id": "scenario-with-attachment;step-with-attachment",
"keyword": "Scenario",
"name": "step with attachment",
"description": "",
"line": 6,
"type": "scenario",
"steps": [
{
"keyword": "Given ",
"name": "a step with attachment",
"line": 7,
"match": {
"location": "fmt_output_test.go:119"
},
"result": {
"status": "passed",
"duration": 0
},
"embeddings": [
{
"name": "TheFilename1",
"mime_type": "text/plain",
"data": "VGhlRGF0YTE"
},
{
"name": "TheFilename2",
"mime_type": "text/plain",
"data": "VGhlRGF0YTI"
}
]
}
]
}
]
}
]

Просмотреть файл

@ -0,0 +1,10 @@
{"event":"TestRunStarted","version":"0.1.0","timestamp":-6795364578871,"suite":"events"}
{"event":"TestSource","location":"formatter-tests/features/scenario_with_attachment.feature:1","source":"Feature: scenario with attachment\n describes\n an attachment\n feature\n\n Scenario: step with attachment\n Given a step with attachment\n"}
{"event":"TestCaseStarted","location":"formatter-tests/features/scenario_with_attachment.feature:6","timestamp":-6795364578871}
{"event":"StepDefinitionFound","location":"formatter-tests/features/scenario_with_attachment.feature:7","definition_id":"fmt_output_test.go:XXX -\u003e github.com/cucumber/godog/internal/formatters_test.stepWithAttachment","arguments":[]}
{"event":"TestStepStarted","location":"formatter-tests/features/scenario_with_attachment.feature:7","timestamp":-6795364578871}
{"event":"Attachment","location":"formatter-tests/features/scenario_with_attachment.feature:7","timestamp":-6795364578871,"contentEncoding":"BASE64","fileName":"TheFilename1","mimeType":"text/plain","body":"TheData1"}
{"event":"Attachment","location":"formatter-tests/features/scenario_with_attachment.feature:7","timestamp":-6795364578871,"contentEncoding":"BASE64","fileName":"TheFilename2","mimeType":"text/plain","body":"TheData2"}
{"event":"TestStepFinished","location":"formatter-tests/features/scenario_with_attachment.feature:7","timestamp":-6795364578871,"status":"passed"}
{"event":"TestCaseFinished","location":"formatter-tests/features/scenario_with_attachment.feature:6","timestamp":-6795364578871,"status":"passed"}
{"event":"TestRunFinished","status":"passed","timestamp":-6795364578871,"snippets":"","memory":""}

Просмотреть файл

@ -0,0 +1,7 @@
Feature: scenario with attachment
describes
an attachment
feature
Scenario: step with attachment
Given a step with attachment

Просмотреть файл

@ -18,6 +18,13 @@ type PickleResult struct {
StartedAt time.Time StartedAt time.Time
} }
// PickleAttachment ...
type PickleAttachment struct {
Name string
MimeType string
Data []byte
}
// PickleStepResult ... // PickleStepResult ...
type PickleStepResult struct { type PickleStepResult struct {
Status StepResultStatus Status StepResultStatus
@ -28,6 +35,8 @@ type PickleStepResult struct {
PickleStepID string PickleStepID string
Def *StepDefinition Def *StepDefinition
Attachments []*PickleAttachment
} }
// NewStepResult ... // NewStepResult ...
@ -35,6 +44,7 @@ func NewStepResult(
status StepResultStatus, status StepResultStatus,
pickleID, pickleStepID string, pickleID, pickleStepID string,
match *StepDefinition, match *StepDefinition,
attachments []*PickleAttachment,
err error, err error,
) PickleStepResult { ) PickleStepResult {
return PickleStepResult{ return PickleStepResult{
@ -44,6 +54,7 @@ func NewStepResult(
PickleID: pickleID, PickleID: pickleID,
PickleStepID: pickleStepID, PickleStepID: pickleStepID,
Def: match, Def: match,
Attachments: attachments,
} }
} }

Просмотреть файл

@ -68,6 +68,42 @@ type suite struct {
afterScenarioHandlers []AfterScenarioHook afterScenarioHandlers []AfterScenarioHook
} }
type Attachment struct {
Body []byte
FileName string
MediaType string
}
type attachmentKey struct{}
func Attach(ctx context.Context, attachments ...Attachment) context.Context {
return context.WithValue(ctx, attachmentKey{}, attachments)
}
func Attachments(ctx context.Context) []Attachment {
v := ctx.Value(attachmentKey{})
if v == nil {
return []Attachment{}
}
return v.([]Attachment)
}
func pickleAttachments(ctx context.Context) []*models.PickleAttachment {
pickledAttachments := []*models.PickleAttachment{}
attachments := Attachments(ctx)
for _, a := range attachments {
pickledAttachments = append(pickledAttachments, &models.PickleAttachment{
Name: a.FileName,
Data: a.Body,
MimeType: a.MediaType,
})
}
return pickledAttachments
}
func (s *suite) matchStep(step *messages.PickleStep) *models.StepDefinition { func (s *suite) matchStep(step *messages.PickleStep) *models.StepDefinition {
def := s.matchStepTextAndType(step.Text, step.Type) def := s.matchStepTextAndType(step.Text, step.Type)
if def != nil && step.Argument != nil { if def != nil && step.Argument != nil {
@ -124,6 +160,9 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena
status = StepPassed status = StepPassed
} }
pickledAttachments := pickleAttachments(ctx)
ctx = Attach(ctx)
// Run after step handlers. // Run after step handlers.
rctx, err = s.runAfterStepHooks(ctx, step, status, err) rctx, err = s.runAfterStepHooks(ctx, step, status, err)
@ -140,19 +179,19 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena
switch { switch {
case err == nil: case err == nil:
sr := models.NewStepResult(models.Passed, pickle.Id, step.Id, match, nil) sr := models.NewStepResult(models.Passed, pickle.Id, step.Id, match, pickledAttachments, nil)
s.storage.MustInsertPickleStepResult(sr) s.storage.MustInsertPickleStepResult(sr)
s.fmt.Passed(pickle, step, match.GetInternalStepDefinition()) s.fmt.Passed(pickle, step, match.GetInternalStepDefinition())
case errors.Is(err, ErrPending): case errors.Is(err, ErrPending):
sr := models.NewStepResult(models.Pending, pickle.Id, step.Id, match, nil) sr := models.NewStepResult(models.Pending, pickle.Id, step.Id, match, pickledAttachments, nil)
s.storage.MustInsertPickleStepResult(sr) s.storage.MustInsertPickleStepResult(sr)
s.fmt.Pending(pickle, step, match.GetInternalStepDefinition()) s.fmt.Pending(pickle, step, match.GetInternalStepDefinition())
case errors.Is(err, ErrSkip): case errors.Is(err, ErrSkip):
sr := models.NewStepResult(models.Skipped, pickle.Id, step.Id, match, nil) sr := models.NewStepResult(models.Skipped, pickle.Id, step.Id, match, pickledAttachments, nil)
s.storage.MustInsertPickleStepResult(sr) s.storage.MustInsertPickleStepResult(sr)
s.fmt.Skipped(pickle, step, match.GetInternalStepDefinition()) s.fmt.Skipped(pickle, step, match.GetInternalStepDefinition())
default: default:
sr := models.NewStepResult(models.Failed, pickle.Id, step.Id, match, err) sr := models.NewStepResult(models.Failed, pickle.Id, step.Id, match, pickledAttachments, err)
s.storage.MustInsertPickleStepResult(sr) s.storage.MustInsertPickleStepResult(sr)
s.fmt.Failed(pickle, step, match.GetInternalStepDefinition(), err) s.fmt.Failed(pickle, step, match.GetInternalStepDefinition(), err)
} }
@ -171,7 +210,11 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena
s.fmt.Defined(pickle, step, match.GetInternalStepDefinition()) s.fmt.Defined(pickle, step, match.GetInternalStepDefinition())
if err != nil { if err != nil {
sr := models.NewStepResult(models.Failed, pickle.Id, step.Id, match, nil)
pickledAttachments := pickleAttachments(ctx)
ctx = Attach(ctx)
sr := models.NewStepResult(models.Failed, pickle.Id, step.Id, match, pickledAttachments, nil)
s.storage.MustInsertPickleStepResult(sr) s.storage.MustInsertPickleStepResult(sr)
return ctx, err return ctx, err
} }
@ -193,7 +236,10 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena
} }
} }
sr := models.NewStepResult(models.Undefined, pickle.Id, step.Id, match, nil) pickledAttachments := pickleAttachments(ctx)
ctx = Attach(ctx)
sr := models.NewStepResult(models.Undefined, pickle.Id, step.Id, match, pickledAttachments, nil)
s.storage.MustInsertPickleStepResult(sr) s.storage.MustInsertPickleStepResult(sr)
s.fmt.Undefined(pickle, step, match.GetInternalStepDefinition()) s.fmt.Undefined(pickle, step, match.GetInternalStepDefinition())
@ -201,7 +247,10 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena
} }
if scenarioErr != nil { if scenarioErr != nil {
sr := models.NewStepResult(models.Skipped, pickle.Id, step.Id, match, nil) pickledAttachments := pickleAttachments(ctx)
ctx = Attach(ctx)
sr := models.NewStepResult(models.Skipped, pickle.Id, step.Id, match, pickledAttachments, nil)
s.storage.MustInsertPickleStepResult(sr) s.storage.MustInsertPickleStepResult(sr)
s.fmt.Skipped(pickle, step, match.GetInternalStepDefinition()) s.fmt.Skipped(pickle, step, match.GetInternalStepDefinition())