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
_artifacts
vendor

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

@ -11,6 +11,7 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
## [v0.14.1]
### 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))
- 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
[releasing guide]: https://github.com/cucumber/godog/blob/main/RELEASING.md
[community Slack]: https://cucumber.io/community#slack

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

@ -12,6 +12,7 @@ package formatters
*/
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
@ -139,6 +140,12 @@ type cukeMatch struct {
Location string `json:"location"`
}
type cukeEmbedding struct {
Name string `json:"name"`
MimeType string `json:"mime_type"`
Data string `json:"data"`
}
type cukeStep struct {
Keyword string `json:"keyword"`
Name string `json:"name"`
@ -147,6 +154,7 @@ type cukeStep struct {
Match cukeMatch `json:"match"`
Result cukeResult `json:"result"`
DataTable []*cukeDataTableRow `json:"rows,omitempty"`
Embeddings []*cukeEmbedding `json:"embeddings,omitempty"`
}
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)
}
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
}

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

@ -153,6 +153,31 @@ func (f *Events) step(pickle *messages.Pickle, pickleStep *messages.PickleStep)
if pickleStepResult.Err != nil {
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 {
Event string `json:"event"`
Location string `json:"location"`

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

@ -2,11 +2,12 @@ package formatters_test
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"testing"
@ -24,9 +25,7 @@ func Test_FmtOutput(t *testing.T) {
featureFiles, err := listFmtOutputTestsFeatureFiles()
require.Nil(t, err)
formatters := []string{"cucumber", "events", "junit", "pretty", "progress", "junit,pretty"}
for _, fmtName := range formatters {
for _, featureFile := range featureFiles {
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 )?passing step$`, passingStepDef)
ctx.Step(`^odd (\d+) and even (\d+) number$`, oddEvenStepDef)
ctx.Step(`^(?:a )?a step with attachment$`, stepWithAttachment)
}
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)
}
expectedOutput, err := ioutil.ReadFile(expectOutputPath)
expectedOutput, err := os.ReadFile(expectOutputPath)
require.NoError(t, err)
var buf bytes.Buffer
@ -92,12 +92,23 @@ func fmtOutputTest(fmtName, testName, featureFilePath string) func(*testing.T) {
Options: &opts,
}.Run()
expected := string(expectedOutput)
actual := buf.String()
// normalise on unix line ending so expected vs actual works cross platform
expected := normalise(string(expectedOutput))
actual := normalise(buf.String())
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 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 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
}
// PickleAttachment ...
type PickleAttachment struct {
Name string
MimeType string
Data []byte
}
// PickleStepResult ...
type PickleStepResult struct {
Status StepResultStatus
@ -28,6 +35,8 @@ type PickleStepResult struct {
PickleStepID string
Def *StepDefinition
Attachments []*PickleAttachment
}
// NewStepResult ...
@ -35,6 +44,7 @@ func NewStepResult(
status StepResultStatus,
pickleID, pickleStepID string,
match *StepDefinition,
attachments []*PickleAttachment,
err error,
) PickleStepResult {
return PickleStepResult{
@ -44,6 +54,7 @@ func NewStepResult(
PickleID: pickleID,
PickleStepID: pickleStepID,
Def: match,
Attachments: attachments,
}
}

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

@ -68,6 +68,42 @@ type suite struct {
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 {
def := s.matchStepTextAndType(step.Text, step.Type)
if def != nil && step.Argument != nil {
@ -124,6 +160,9 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena
status = StepPassed
}
pickledAttachments := pickleAttachments(ctx)
ctx = Attach(ctx)
// Run after step handlers.
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 {
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.fmt.Passed(pickle, step, match.GetInternalStepDefinition())
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.fmt.Pending(pickle, step, match.GetInternalStepDefinition())
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.fmt.Skipped(pickle, step, match.GetInternalStepDefinition())
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.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())
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)
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.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 {
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.fmt.Skipped(pickle, step, match.GetInternalStepDefinition())