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.
Этот коммит содержится в:
родитель
4ade3314e8
коммит
201e526078
11 изменённых файлов: 218 добавлений и 21 удалений
2
.gitignore
предоставленный
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
63
suite.go
63
suite.go
|
@ -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())
|
||||
|
|
Загрузка…
Создание таблицы
Сослаться в новой задаче