From b778d93134c4c631f4b759d2824772e43055796a Mon Sep 17 00:00:00 2001 From: Stanislav Fedii Date: Wed, 22 Jan 2020 06:20:43 -0500 Subject: [PATCH] Variadic option arguments for Test and Step functions (#22) * WIP version of function slices. * WIP for option slices. * WIP for option slices. * Working version of variadic function arguments for Test and Step functions. * Re-arranged functions and interfaces to remove `test` and `steps` packages. * Changed some non-essential types to private. * Addressed review comments: renamed allure.Body to allure.Action for more clarity and added allure.Parameters for more convenience. --- attachment.go | 12 +- helper.go => ctx.go | 0 error.go | 2 +- example/example_attachment_test.go | 24 ++-- example/example_error_test.go | 35 +++-- example/example_functions.go | 14 +- example/example_new_test.go | 19 +++ example/example_panic_handling_test.go | 7 +- example/example_panic_in_step_test.go | 14 +- example/example_parameters_test.go | 108 ++++++++-------- example/example_skip_test.go | 14 ++ example/example_test.go | 70 +++++----- example/example_unallure_test.go | 14 ++ init.go | 12 +- interfaces.go | 28 ++++ label.go | 2 +- misc.go | 125 ++++++++++++++++++ options.go | 73 +++++++++++ parameter.go | 62 +-------- result.go | 170 ++++++------------------- step.go | 142 ++++++++++++--------- test.go | 25 ++-- 22 files changed, 553 insertions(+), 419 deletions(-) rename helper.go => ctx.go (100%) create mode 100644 example/example_new_test.go create mode 100644 example/example_skip_test.go create mode 100644 example/example_unallure_test.go create mode 100644 interfaces.go create mode 100644 misc.go create mode 100644 options.go diff --git a/attachment.go b/attachment.go index 0cb00f1..938c425 100644 --- a/attachment.go +++ b/attachment.go @@ -25,23 +25,23 @@ func AddAttachment(name string, mimeType MimeType, content []byte) error { return errors.Wrap(err, "Failed to create an attachment file") } if node, ok := ctxMgr.GetValue(nodeKey); ok { - node.(hasAttachments).AddAttachment(*attachment) + node.(hasAttachments).addAttachment(*attachment) } return nil } func (a *attachment) writeAttachmentFile() error { - resultsPathEnv := os.Getenv(resultsPathEnvKey) + resultsPathEnv := os.Getenv(ResultsPathEnvKey) if resultsPathEnv == "" { - log.Printf("%s environment variable cannot be empty\n", resultsPathEnvKey) + log.Printf("%s environment variable cannot be empty\n", ResultsPathEnvKey) } - if resultsPath == "" { - resultsPath = fmt.Sprintf("%s/allure-results", resultsPathEnv) + if ResultsPath == "" { + ResultsPath = fmt.Sprintf("%s/allure-results", resultsPathEnv) } a.Source = fmt.Sprintf("%s-attachment", a.uuid) - err := ioutil.WriteFile(fmt.Sprintf("%s/%s-attachment", resultsPath, a.uuid), a.content, 0777) + err := ioutil.WriteFile(fmt.Sprintf("%s/%s-attachment", ResultsPath, a.uuid), a.content, 0777) if err != nil { return errors.Wrap(err, "Failed to write in file") } diff --git a/helper.go b/ctx.go similarity index 100% rename from helper.go rename to ctx.go diff --git a/error.go b/error.go index 3eb888c..ad67d9e 100644 --- a/error.go +++ b/error.go @@ -46,7 +46,7 @@ func allureError(err error, status string, now bool) { manipulateOnObjectFromCtx( nodeKey, func(node interface{}) { - node.(hasStatus).SetStatus(status) + node.(hasStatus).setStatus(status) }) manipulateOnObjectFromCtx( testInstanceKey, diff --git a/example/example_attachment_test.go b/example/example_attachment_test.go index 032d263..799c9d1 100644 --- a/example/example_attachment_test.go +++ b/example/example_attachment_test.go @@ -8,37 +8,37 @@ import ( ) func TestTextAttachmentToStep(t *testing.T) { - allure.Test(t, "Testing a text attachment", func() { - allure.Step("adding a text attachment", func() { + allure.Test(t, allure.Description("Testing a text attachment"), allure.Action(func() { + allure.Step(allure.Description("adding a text attachment"), allure.Action(func() { _ = allure.AddAttachment("text!", allure.TextPlain, []byte("Some text!")) - }) - }) + })) + })) } func TestImageAttachmentToStep(t *testing.T) { - allure.Test(t, "testing an image attachment", func() { - allure.Step("adding an image attachment", func() { + allure.Test(t, allure.Description("testing an image attachment"), allure.Action(func() { + allure.Step(allure.Description("adding an image attachment"), allure.Action(func() { dat, err := ioutil.ReadFile("../Coryphaena_hippurus.png") if err != nil { log.Println(err) } _ = allure.AddAttachment("mahi-mahi", allure.ImagePng, dat) - }) - }) + })) + })) } func TestTextAttachment(t *testing.T) { - allure.Test(t, "Testing a text attachment", func() { + allure.Test(t, allure.Description("Testing a text attachment"), allure.Action(func() { _ = allure.AddAttachment("text!", allure.TextPlain, []byte("Some text!")) - }) + })) } func TestImageAttachment(t *testing.T) { - allure.Test(t, "testing an image attachment", func() { + allure.Test(t, allure.Description("testing an image attachment"), allure.Action(func() { dat, err := ioutil.ReadFile("../Coryphaena_hippurus.png") if err != nil { log.Println(err) } _ = allure.AddAttachment("mahi-mahi", allure.ImagePng, dat) - }) + })) } diff --git a/example/example_error_test.go b/example/example_error_test.go index 41f0b58..879eabe 100644 --- a/example/example_error_test.go +++ b/example/example_error_test.go @@ -7,35 +7,34 @@ import ( ) func TestFailAllure(t *testing.T) { - allure.Test(t, "Test with Allure error in it", func() { - allure.Step("Step 1", func() { - }) - allure.Step("Step 2", func() { + allure.Test(t, allure.Description("Test with Allure error in it"), allure.Action(func() { + allure.Step(allure.Description("Step 1"), allure.Action(func() { + })) + allure.Step(allure.Description("Step 2"), allure.Action(func() { allure.Fail(errors.New("Error message")) - }) - }) + })) + })) } func TestFailNowAllure(t *testing.T) { - allure.Test(t, "Test with Allure error in it", func() { + allure.Test(t, allure.Description("Test with Allure error in it"), allure.Action(func() { allure.FailNow(errors.New("A more serious error")) - allure.Step("Step you're not supposed to see", func() {}) - }) + allure.Step(allure.Description("Step you're not supposed to see"), allure.Action(func() {})) + })) } func TestBreakAllure(t *testing.T) { - allure.Test(t, "Test with Allure error in it", func() { - allure.Step("Step 1", func() { - }) - allure.Step("Step 2", func() { + allure.Test(t, allure.Description("Test with Allure error in it"), allure.Action(func() { + allure.Step(allure.Description("Step 1"), allure.Action(func() {})) + allure.Step(allure.Description("Step 2"), allure.Action(func() { allure.Break(errors.New("Error message")) - }) - }) + })) + })) } func TestBreakNowAllure(t *testing.T) { - allure.Test(t, "Test with Allure error in it", func() { + allure.Test(t, allure.Description("Test with Allure error in it"), allure.Action(func() { allure.BreakNow(errors.New("A more serious error")) - allure.Step("Step you're not supposed to see", func() {}) - }) + allure.Step(allure.Description("Step you're not supposed to see"), allure.Action(func() {})) + })) } diff --git a/example/example_functions.go b/example/example_functions.go index f02bd42..10926ef 100644 --- a/example/example_functions.go +++ b/example/example_functions.go @@ -1,17 +1,15 @@ package example -import "github.com/dailymotion/allure-go" +import ( + "github.com/dailymotion/allure-go" +) func doSomething() { - allure.Step("Something", func() { + allure.Step(allure.Description("Something"), allure.Action(func() { doSomethingNested() - }) + })) } func doSomethingNested() { - allure.Step("Because we can!", func() {}) -} - -func addSomeParameters(parameters map[string]interface{}) { - allure.StepWithParameter("Step with parameters", parameters, func() {}) + allure.Step(allure.Description("Because we can!"), allure.Action(func() {})) } diff --git a/example/example_new_test.go b/example/example_new_test.go new file mode 100644 index 0000000..131b260 --- /dev/null +++ b/example/example_new_test.go @@ -0,0 +1,19 @@ +package example + +import ( + "github.com/dailymotion/allure-go" + "testing" +) + +func TestNewTest(t *testing.T) { + allure.Test( + t, + allure.Description("New Test Description"), + allure.Action(func() { + allure.Step( + allure.Description("Step description"), + allure.Action(func() { + + })) + })) +} diff --git a/example/example_panic_handling_test.go b/example/example_panic_handling_test.go index e9ae21d..036a3e7 100644 --- a/example/example_panic_handling_test.go +++ b/example/example_panic_handling_test.go @@ -6,7 +6,8 @@ import ( ) func TestPanicBasic(t *testing.T) { - allure.Test(t, "Panic handling test", func() { - panic("throwing a panic") - }) + allure.Test(t, allure.Description("Panic handling test"), + allure.Action(func() { + panic("throwing a panic") + })) } diff --git a/example/example_panic_in_step_test.go b/example/example_panic_in_step_test.go index 4d3c05b..c1dd9d3 100644 --- a/example/example_panic_in_step_test.go +++ b/example/example_panic_in_step_test.go @@ -6,9 +6,13 @@ import ( ) func TestPanicInStep(t *testing.T) { - allure.Test(t, "Panic handling test", func() { - allure.Step("step that will panic", func() { - panic("throwing a panic") - }) - }) + allure.Test(t, + allure.Description("Panic handling test"), + allure.Action(func() { + allure.Step( + allure.Description("step that will panic"), + allure.Action(func() { + panic("throwing a panic") + })) + })) } diff --git a/example/example_parameters_test.go b/example/example_parameters_test.go index 93e534c..c3e1d93 100644 --- a/example/example_parameters_test.go +++ b/example/example_parameters_test.go @@ -27,75 +27,69 @@ var parameters = map[string]interface{}{ func TestAllureParameterized(t *testing.T) { for i := 0; i < 5; i++ { t.Run("", func(t *testing.T) { - allure.TestWithParameters(t, - "Test with parameters", - map[string]interface{}{ - "counter": i, - }, - allure.TestLabels{}, - func() { - - }) + allure.Test(t, + allure.Description("Test with parameters"), + allure.Parameter("counter", i), + allure.Action(func() {})) }) } } func TestAllureParametersExample(t *testing.T) { - allure.TestWithParameters(t, - "This is a test to show allure implementation with a passing test", - parameters, - allure.TestLabels{}, - func() { - allure.Step(fmt.Sprintf("Number: %d", parameters["int"]), func() {}) - allure.Step(fmt.Sprintf("String: %s", parameters["string"]), func() {}) - allure.Step(fmt.Sprintf("Interface: %+v", parameters["struct"]), func() {}) - }) + allure.Test(t, + allure.Parameters(parameters), + allure.Description("This is a test to show allure implementation with a passing test"), + allure.Action(func() { + allure.Step(allure.Description(fmt.Sprintf("Number: %d", parameters["int"])), allure.Action(func() {})) + allure.Step(allure.Description(fmt.Sprintf("String: %s", parameters["string"])), allure.Action(func() {})) + allure.Step(allure.Description(fmt.Sprintf("Interface: %+v", parameters["struct"])), allure.Action(func() {})) + })) } func TestAllureStepWithParameters(t *testing.T) { - allure.Test(t, "Test with steps that have parameters", func() { - for i := 0; i < 5; i++ { - allure.StepWithParameter("Step with parameters", map[string]interface{}{"counter": i}, func() {}) - } - allure.SkipStepWithParameter("Step with parameters", "Skip this step with parameters", map[string]interface{}{"counter": 6}, func() {}) - }) + allure.Test(t, + allure.Description("Test with steps that have parameters"), + allure.Action(func() { + for i := 0; i < 5; i++ { + allure.Step( + allure.Description("Step with parameters"), + allure.Parameter("counter", i), + allure.Action(func() {})) + } + allure.SkipStep(allure.Description("Step with parameters"), allure.Reason("Skip this step with parameters"), allure.Parameter("counter", 5), allure.Action(func() {})) + })) } func TestAllureParameterTypes(t *testing.T) { - allure.TestWithParameters(t, - "Test parameter types", - map[string]interface{}{ - "uintptr": uintptr(10), - "uint": uint(10), - "uint8": uint8(10), - "uint16": uint16(10), - "uint32": uint32(10), - "uint64": uint64(10), - "int": int(10), - "int8": int8(10), - "int16": int16(10), - "int32": int32(10), - "int64": int64(10), - "float32": float32(10.5), - "float64": float64(10.5), - "complex64": complex(float32(2), float32(-2)), - "complex128": complex(float64(2), float64(-2)), - }, - allure.TestLabels{}, - func() {}) + allure.Test(t, + allure.Description("Test parameter types"), + allure.Parameter("uintptr", uintptr(10)), + allure.Parameter("uint", uint(10)), + allure.Parameter("uint8", uint8(10)), + allure.Parameter("uint16", uint16(10)), + allure.Parameter("uint32", uint32(10)), + allure.Parameter("uint64", uint64(10)), + allure.Parameter("int", int(10)), + allure.Parameter("int8", int8(10)), + allure.Parameter("int16", int16(10)), + allure.Parameter("int32", int32(10)), + allure.Parameter("int64", int64(10)), + allure.Parameter("float32", float32(10.5)), + allure.Parameter("float64", float64(10.5)), + allure.Parameter("complex64", complex(float32(2), float32(-2))), + allure.Parameter("complex128", complex(float64(2), float64(-2))), + allure.Action(func() {})) } func TestAllureWithLabels(t *testing.T) { - allure.TestWithParameters(t, "Test with labels", - nil, - allure.TestLabels{ - Epic: "Epic Epic of Epicness", - Lead: "Duke Nukem", - Owner: "Rex Powercolt", - Severity: severity.Critical, - Story: []string{"story1", "story2"}, - Feature: []string{"feature1", "feature2"}, - }, func() { - - }) + allure.Test(t, allure.Description("Test with labels"), + allure.Epic("Epic Epic of Epicness"), + allure.Lead("Duke Nukem"), + allure.Owner("Rex Powercolt"), + allure.Severity(severity.Critical), + allure.Story("story1"), + allure.Story("story2"), + allure.Feature("feature1"), + allure.Feature("feature2"), + allure.Action(func() {})) } diff --git a/example/example_skip_test.go b/example/example_skip_test.go new file mode 100644 index 0000000..00d554f --- /dev/null +++ b/example/example_skip_test.go @@ -0,0 +1,14 @@ +package example + +import ( + "github.com/dailymotion/allure-go" + "testing" +) + +func TestSkip(t *testing.T) { + allure.Test(t, + allure.Description("Skip test"), + allure.Action(func() { + allure.SkipStep(allure.Description("Skip"), allure.Action(func() {}), allure.Reason("reason")) + })) +} diff --git a/example/example_test.go b/example/example_test.go index 7af7b5c..00f228f 100644 --- a/example/example_test.go +++ b/example/example_test.go @@ -2,56 +2,58 @@ package example import ( "github.com/dailymotion/allure-go" - "testing" ) func TestPassedExample(t *testing.T) { - allure.Test(t, "This is a test to show allure implementation with a passing test", func() { - s := "Hello world" - if len(s) == 0 { - t.Errorf("Expected 'hello world' string, but got %s ", s) - } - }) + allure.Test(t, + allure.Description("This is a test to show allure implementation with a passing test"), + allure.Action(func() { + s := "Hello world" + if len(s) == 0 { + t.Errorf("Expected 'hello world' string, but got %s ", s) + } + })) } func TestWithIntricateSubsteps(t *testing.T) { - allure.Test(t, "Test", func() { - allure.Step("Step 1", func() { - doSomething() - allure.Step("Sub-step 1.1", func() { - t.Errorf("Failure") - }) - allure.Step("Sub-step 1.2", func() {}) - allure.SkipStep("Sub-step 1.3", "Skip this step because of defect to be fixed", func() {}) - }) - allure.Step("Step 2", func() { - allure.Step("Sub-step 2.1", func() { - allure.Step("Step 2.1.1", func() { - allure.Step("Sub-step 2.1.1.1", func() { - t.Errorf("Failure") - }) - allure.Step("Sub-step 2.1.1.2", func() { - t.Error("Failed like this") - }) - }) - }) - allure.Step("Sub-step 2.2", func() {}) - }) - }) + allure.Test(t, allure.Description("Test"), + allure.Action(func() { + allure.Step(allure.Description("Step 1"), allure.Action(func() { + doSomething() + allure.Step(allure.Description("Sub-step 1.1"), allure.Action(func() { + t.Errorf("Failure") + })) + allure.Step(allure.Description("Sub-step 1.2"), allure.Action(func() {})) + allure.SkipStep(allure.Description("Sub-step 1.3"), allure.Reason("Skip this step because of defect to be fixed"), allure.Action(func() {})) + })) + allure.Step(allure.Description("Step 2"), allure.Action(func() { + allure.Step(allure.Description("Sub-step 2.1"), allure.Action(func() { + allure.Step(allure.Description("Step 2.1.1"), allure.Action(func() { + allure.Step(allure.Description("Sub-step 2.1.1.1"), allure.Action(func() { + t.Errorf("Failure") + })) + allure.Step(allure.Description("Sub-step 2.1.1.2"), allure.Action(func() { + t.Error("Failed like this") + })) + })) + })) + allure.Step(allure.Description("Sub-step 2.2"), allure.Action(func() {})) + })) + })) } func TestFailedExample(t *testing.T) { - allure.Test(t, "This is a test to show allure implementation with a failing test", func() { + allure.Test(t, allure.Description("This is a test to show allure implementation with a failing test"), allure.Action(func() { s := "Hello world" if len(s) != 0 { t.Errorf("Expected empty string, but got %s ", s) } - }) + })) } func TestSkippedExample(t *testing.T) { - allure.Test(t, "This is a test to show allure implementation with a skipped test", func() { + allure.Test(t, allure.Description("This is a test to show allure implementation with a skipped test"), allure.Action(func() { t.Skip("Skipping this test") - }) + })) } diff --git a/example/example_unallure_test.go b/example/example_unallure_test.go new file mode 100644 index 0000000..e1e108f --- /dev/null +++ b/example/example_unallure_test.go @@ -0,0 +1,14 @@ +package example + +import ( + "fmt" + "testing" +) + +func TestPanicking(t *testing.T) { + panic("") +} + +func TestOkay(t *testing.T) { + fmt.Println("okay") +} diff --git a/init.go b/init.go index 6549783..18ca93b 100644 --- a/init.go +++ b/init.go @@ -8,15 +8,15 @@ import ( var ( ctxMgr *gls.ContextManager wsd string - resultsPath string - createFolderOnce sync.Once - copyEnvFileOnce sync.Once + ResultsPath string + CreateFolderOnce sync.Once + CopyEnvFileOnce sync.Once ) const ( - resultsPathEnvKey = "ALLURE_RESULTS_PATH" - wsPathEnvKey = "ALLURE_WORKSPACE_PATH" - envFileKey = "ALLURE_ENVIRONMENT_FILE_PATH" + ResultsPathEnvKey = "ALLURE_RESULTS_PATH" + WsPathEnvKey = "ALLURE_WORKSPACE_PATH" + EnvFileKey = "ALLURE_ENVIRONMENT_FILE_PATH" nodeKey = "current_step_container" testResultKey = "test_result_object" testInstanceKey = "test_instance" diff --git a/interfaces.go b/interfaces.go new file mode 100644 index 0000000..8e73482 --- /dev/null +++ b/interfaces.go @@ -0,0 +1,28 @@ +package allure + +type hasOptions interface { + addLabel(key string, value string) + addDescription(description string) + addParameter(name string, value interface{}) + addParameters(parameters map[string]interface{}) + addName(name string) + addAction(action func()) + addReason(reason string) +} + +// This interface provides functions required to manipulate children step records, used in the result object and +// step object for recursive handling +type hasSteps interface { + getSteps() []stepObject + addStep(step stepObject) +} + +type hasStatus interface { + setStatus(status string) + getStatus() string +} + +type hasAttachments interface { + getAttachments() []attachment + addAttachment(attachment attachment) +} diff --git a/label.go b/label.go index 58a7645..c60f913 100644 --- a/label.go +++ b/label.go @@ -1,6 +1,6 @@ package allure -type Label struct { +type label struct { Name string `json:"name,omitempty"` Value string `json:"value,omitempty"` } diff --git a/misc.go b/misc.go new file mode 100644 index 0000000..753f1da --- /dev/null +++ b/misc.go @@ -0,0 +1,125 @@ +package allure + +import ( + "fmt" + "io" + "log" + "os" + "strconv" + "strings" + "time" +) + +func parseParameter(name string, value interface{}) parameter { + result := parameter{ + Name: name, + } + + switch value.(type) { + case []byte: + result.Value = string(value.([]byte)) + case uintptr: + result.Value = strconv.Itoa(int(value.(uintptr))) + case float32: + result.Value = strconv.FormatFloat(float64(value.(float32)), 'f', -1, 64) + case float64: + result.Value = strconv.FormatFloat(value.(float64), 'f', -1, 64) + case complex64: + result.Value = fmt.Sprintf("%f i%f", real(value.(complex64)), imag(value.(complex64))) + case complex128: + result.Value = fmt.Sprintf("%f i%f", real(value.(complex128)), imag(value.(complex128))) + case uint: + result.Value = strconv.FormatUint(uint64(value.(uint)), 10) + case uint8: + result.Value = strconv.FormatUint(uint64(value.(uint8)), 10) + case uint16: + result.Value = strconv.FormatUint(uint64(value.(uint16)), 10) + case uint32: + result.Value = strconv.FormatUint(uint64(value.(uint32)), 10) + case uint64: + result.Value = strconv.FormatUint(value.(uint64), 10) + case int: + result.Value = strconv.FormatInt(int64(value.(int)), 10) + case int8: + result.Value = strconv.FormatInt(int64(value.(int8)), 10) + case int16: + result.Value = strconv.FormatInt(int64(value.(int16)), 10) + case int32: + result.Value = strconv.FormatInt(int64(value.(int32)), 10) + case int64: + result.Value = strconv.FormatInt(value.(int64), 10) + case bool: + result.Value = strconv.FormatBool(value.(bool)) + case string: + result.Value = value.(string) + default: + result.Value = fmt.Sprintf("%+value", value) + } + + return result +} + +func getTimestampMs() int64 { + return time.Now().UnixNano() / int64(time.Millisecond) +} + +func createFolderIfNotExists() { + resultsPathEnv := os.Getenv(ResultsPathEnvKey) + if resultsPathEnv == "" { + log.Printf("environment variable %s cannot be empty\n", ResultsPathEnvKey) + } + ResultsPath = fmt.Sprintf("%s/allure-results", resultsPathEnv) + + if _, err := os.Stat(ResultsPath); os.IsNotExist(err) { + err = os.Mkdir(ResultsPath, 0777) + if err != nil { + log.Println(err, "Failed to create allure-results folder") + } + } +} + +func copyEnvFileIfExists() { + if envFilePath := os.Getenv(EnvFileKey); envFilePath != "" { + envFilesStrings := strings.Split(envFilePath, "/") + if ResultsPath != "" { + if _, err := copy(envFilePath, ResultsPath+"/"+envFilesStrings[len(envFilesStrings)-1]); err != nil { + log.Println("Could not copy the environment file", err) + } + } + + } +} + +func copy(src, dst string) (int64, error) { + sourceFileStat, err := os.Stat(src) + if err != nil { + return 0, err + } + + if !sourceFileStat.Mode().IsRegular() { + return 0, fmt.Errorf("%s is not a regular file", src) + } + + source, err := os.Open(src) + if err != nil { + return 0, err + } + defer func() { + if err = source.Close(); err != nil { + log.Printf("Could not close the stream for the environment file, %f\n", err) + } + }() + + destination, err := os.Create(dst) + if err != nil { + return 0, err + } + defer func() { + if err = destination.Close(); err != nil { + log.Printf("Could not close the stream for the destination of the environment file, %f\n", err) + } + }() + + nBytes, err := io.Copy(destination, source) + return nBytes, err +} diff --git a/options.go b/options.go new file mode 100644 index 0000000..a7fe8a0 --- /dev/null +++ b/options.go @@ -0,0 +1,73 @@ +package allure + +import ( + "github.com/dailymotion/allure-go/severity" +) + +type Option func(r hasOptions) + +func Lead(lead string) Option { + return func(r hasOptions) { + r.addLabel("lead", lead) + } +} + +func Owner(owner string) Option { + return func(r hasOptions) { + r.addLabel("owner", owner) + } +} + +func Epic(epic string) Option { + return func(r hasOptions) { + r.addLabel("epic", epic) + } +} + +func Severity(severity severity.Severity) Option { + return func(r hasOptions) { + r.addLabel("severity", string(severity)) + } +} + +func Story(story string) Option { + return func(r hasOptions) { + r.addLabel("story", story) + } +} + +func Feature(feature string) Option { + return func(r hasOptions) { + r.addLabel("feature", feature) + } +} + +func Parameter(name string, value interface{}) Option { + return func(r hasOptions) { + r.addParameter(name, value) + } +} + +func Parameters(parameters map[string]interface{}) Option { + return func(r hasOptions) { + r.addParameters(parameters) + } +} + +func Description(description string) Option { + return func(r hasOptions) { + r.addDescription(description) + } +} + +func Action(action func()) Option { + return func(r hasOptions) { + r.addAction(action) + } +} + +func Reason(reason string) Option { + return func(r hasOptions) { + r.addReason(reason) + } +} diff --git a/parameter.go b/parameter.go index f674782..5c91dbb 100644 --- a/parameter.go +++ b/parameter.go @@ -1,66 +1,6 @@ package allure -import ( - "fmt" - "strconv" -) - -type Parameter struct { +type parameter struct { Name string `json:"name,omitempty"` Value string `json:"value,omitempty"` } - -func convertMapToParameters(parameters map[string]interface{}) []Parameter { - result := make([]Parameter, 0) - - for k, v := range parameters { - currentParameter := Parameter{ - Name: k, - } - - switch v.(type) { - case []byte: - currentParameter.Value = string(v.([]byte)) - case uintptr: - currentParameter.Value = strconv.Itoa(int(v.(uintptr))) - case float32: - currentParameter.Value = strconv.FormatFloat(float64(v.(float32)), 'f', -1, 64) - case float64: - currentParameter.Value = strconv.FormatFloat(v.(float64), 'f', -1, 64) - case complex64: - currentParameter.Value = fmt.Sprintf("%f i%f", real(v.(complex64)), imag(v.(complex64))) - case complex128: - currentParameter.Value = fmt.Sprintf("%f i%f", real(v.(complex128)), imag(v.(complex128))) - case uint: - currentParameter.Value = strconv.FormatUint(uint64(v.(uint)), 10) - case uint8: - currentParameter.Value = strconv.FormatUint(uint64(v.(uint8)), 10) - case uint16: - currentParameter.Value = strconv.FormatUint(uint64(v.(uint16)), 10) - case uint32: - currentParameter.Value = strconv.FormatUint(uint64(v.(uint32)), 10) - case uint64: - currentParameter.Value = strconv.FormatUint(v.(uint64), 10) - case int: - currentParameter.Value = strconv.FormatInt(int64(v.(int)), 10) - case int8: - currentParameter.Value = strconv.FormatInt(int64(v.(int8)), 10) - case int16: - currentParameter.Value = strconv.FormatInt(int64(v.(int16)), 10) - case int32: - currentParameter.Value = strconv.FormatInt(int64(v.(int32)), 10) - case int64: - currentParameter.Value = strconv.FormatInt(v.(int64), 10) - case bool: - currentParameter.Value = strconv.FormatBool(v.(bool)) - case string: - currentParameter.Value = v.(string) - default: - currentParameter.Value = fmt.Sprintf("%+v", v) - } - - result = append(result, currentParameter) - } - - return result -} diff --git a/result.go b/result.go index 6212ec8..ca5caef 100644 --- a/result.go +++ b/result.go @@ -3,16 +3,12 @@ package allure import ( "encoding/json" "fmt" - "io" + "github.com/pkg/errors" "io/ioutil" - "log" "os" "runtime" "strings" "testing" - "time" - - "github.com/pkg/errors" ) //result is the top level report object for a test @@ -25,72 +21,73 @@ type result struct { Stage string `json:"stage,omitempty"` Steps []stepObject `json:"steps,omitempty"` Attachments []attachment `json:"attachments,omitempty"` - Parameters []Parameter `json:"parameters,omitempty"` + Parameters []parameter `json:"parameters,omitempty"` Start int64 `json:"start,omitempty"` Stop int64 `json:"stop,omitempty"` Children []string `json:"children,omitempty"` - Befores []Before `json:"befores,omitempty"` FullName string `json:"fullName,omitempty"` - Labels []Label `json:"labels,omitempty"` + Labels []label `json:"labels,omitempty"` + Test func() `json:"-"` } -type FailureMode string -//Before defines a step -type Before struct { - Name string `json:"name,omitempty"` - Status string `json:"status,omitempty"` - StatusDetails *statusDetails `json:"statusDetails,omitempty"` - Stage string `json:"stage,omitempty"` - Description string `json:"description,omitempty"` - Start int64 `json:"start,omitempty"` - Stop int64 `json:"stop,omitempty"` - Steps []stepObject `json:"steps,omitempty"` - Attachments []attachment `json:"attachments,omitempty"` +func (r *result) addReason(reason string) { + testStatusDetails := r.StatusDetails + if testStatusDetails == nil { + testStatusDetails = &statusDetails{} + } + r.StatusDetails.Message = reason } -// This interface provides functions required to manipulate children step records, used in the result object and -// step object for recursive handling -type hasSteps interface { - GetSteps() []stepObject - AddStep(step stepObject) +func (r *result) addDescription(description string) { + r.Description = description } -type hasAttachments interface { - GetAttachments() []attachment - AddAttachment(attachment attachment) +func (r *result) addParameter(name string, value interface{}) { + r.Parameters = append(r.Parameters, parseParameter(name, value)) } -type hasStatus interface { - SetStatus(status string) - GetStatus() string +func (r *result) addParameters(parameters map[string]interface{}) { + for key, value := range parameters { + r.Parameters = append(r.Parameters, parseParameter(key, value)) + } } -func (r *result) GetAttachments() []attachment { +func (r *result) addName(name string) { + r.Name = name +} + +func (r *result) addAction(action func()) { + r.Test = action +} + +type FailureMode string + +func (r *result) getAttachments() []attachment { return r.Attachments } -func (r *result) AddAttachment(attachment attachment) { +func (r *result) addAttachment(attachment attachment) { r.Attachments = append(r.Attachments, attachment) } -func (r *result) GetSteps() []stepObject { +func (r *result) getSteps() []stepObject { return r.Steps } -func (r *result) AddStep(step stepObject) { +func (r *result) addStep(step stepObject) { r.Steps = append(r.Steps, step) } -func (r *result) SetStatus(status string) { +func (r *result) setStatus(status string) { r.Status = status } -func (r *result) GetStatus() string { +func (r *result) getStatus() string { return r.Status } -func (r *result) setLabels(t *testing.T, labels TestLabels) { - wsd := os.Getenv(wsPathEnvKey) +func (r *result) setDefaultLabels(t *testing.T) { + wsd := os.Getenv(WsPathEnvKey) programCounters := make([]uintptr, 10) callersCount := runtime.Callers(0, programCounters) @@ -106,28 +103,6 @@ func (r *result) setLabels(t *testing.T, labels TestLabels) { r.addLabel("package", testPackage) r.addLabel("testClass", testPackage) r.addLabel("testMethod", t.Name()) - if labels.Owner != "" { - r.addLabel("owner", labels.Owner) - } - if labels.Lead != "" { - r.addLabel("lead", labels.Lead) - } - if labels.Epic != "" { - r.addLabel("epic", labels.Epic) - } - if labels.Severity != "" { - r.addLabel("severity", string(labels.Severity)) - } - if labels.Story != nil && len(labels.Story) > 0 { - for _, v := range labels.Story { - r.addLabel("story", v) - } - } - if labels.Feature != nil && len(labels.Feature) > 0 { - for _, v := range labels.Feature { - r.addLabel("feature", v) - } - } if hostname, err := os.Hostname(); err == nil { r.addLabel("host", hostname) } @@ -144,21 +119,21 @@ func (r *result) setLabels(t *testing.T, labels TestLabels) { } func (r *result) addLabel(name string, value string) { - r.Labels = append(r.Labels, Label{ + r.Labels = append(r.Labels, label{ Name: name, Value: value, }) } func (r *result) writeResultsFile() error { - createFolderOnce.Do(createFolderIfNotExists) - copyEnvFileOnce.Do(copyEnvFileIfExists) + CreateFolderOnce.Do(createFolderIfNotExists) + CopyEnvFileOnce.Do(copyEnvFileIfExists) j, err := json.Marshal(r) if err != nil { return errors.Wrap(err, "Failed to marshall result into JSON") } - err = ioutil.WriteFile(fmt.Sprintf("%s/%s-result.json", resultsPath, r.UUID), j, 0777) + err = ioutil.WriteFile(fmt.Sprintf("%s/%s-result.json", ResultsPath, r.UUID), j, 0777) if err != nil { return errors.Wrap(err, "Failed to write in file") } @@ -171,68 +146,3 @@ func newResult() *result { Start: getTimestampMs(), } } - -func getTimestampMs() int64 { - return time.Now().UnixNano() / int64(time.Millisecond) -} - -func createFolderIfNotExists() { - resultsPathEnv := os.Getenv(resultsPathEnvKey) - if resultsPathEnv == "" { - log.Printf("environment variable %s cannot be empty\n", resultsPathEnvKey) - } - resultsPath = fmt.Sprintf("%s/allure-results", resultsPathEnv) - - if _, err := os.Stat(resultsPath); os.IsNotExist(err) { - err = os.Mkdir(resultsPath, 0777) - if err != nil { - log.Println(err, "Failed to create allure-results folder") - } - } -} - -func copyEnvFileIfExists() { - if envFilePath := os.Getenv(envFileKey); envFilePath != "" { - envFilesStrings := strings.Split(envFilePath, "/") - if resultsPath != "" { - if _, err := copy(envFilePath, resultsPath+"/"+envFilesStrings[len(envFilesStrings)-1]); err != nil { - log.Println("Could not copy the environment file", err) - } - } - - } -} - -func copy(src, dst string) (int64, error) { - sourceFileStat, err := os.Stat(src) - if err != nil { - return 0, err - } - - if !sourceFileStat.Mode().IsRegular() { - return 0, fmt.Errorf("%s is not a regular file", src) - } - - source, err := os.Open(src) - if err != nil { - return 0, err - } - defer func() { - if err = source.Close(); err != nil { - log.Printf("Could not close the stream for the environment file, %f\n", err) - } - }() - - destination, err := os.Create(dst) - if err != nil { - return 0, err - } - defer func() { - if err = destination.Close(); err != nil { - log.Printf("Could not close the stream for the destination of the environment file, %f\n", err) - } - }() - - nBytes, err := io.Copy(destination, source) - return nBytes, err -} diff --git a/step.go b/step.go index 0f723fe..c90d173 100644 --- a/step.go +++ b/step.go @@ -9,64 +9,106 @@ import ( ) type stepObject struct { - Name string `json:"name,omitempty"` - Status string `json:"status,omitempty"` - StatusDetail statusDetails `json:"statusDetails,omitempty"` - Stage string `json:"stage"` - ChildrenSteps []stepObject `json:"steps"` - Attachments []attachment `json:"attachments"` - Parameters []Parameter `json:"parameters"` - Start int64 `json:"start"` - Stop int64 `json:"stop"` -} - -func (s *stepObject) GetSteps() []stepObject { + Name string `json:"name,omitempty"` + Status string `json:"status,omitempty"` + StatusDetails *statusDetails `json:"statusDetails,omitempty"` + Stage string `json:"stage"` + ChildrenSteps []stepObject `json:"steps"` + Attachments []attachment `json:"attachments"` + Parameters []parameter `json:"parameters"` + Start int64 `json:"start"` + Stop int64 `json:"stop"` + Action func() `json:"-"` +} + +func (s *stepObject) addReason(reason string) { + testStatusDetails := s.StatusDetails + if testStatusDetails == nil { + s.StatusDetails = &statusDetails{} + } + s.StatusDetails.Message = reason +} + +func (s *stepObject) addLabel(key string, value string) { + // Step doesn't have labels +} + +func (s *stepObject) addDescription(description string) { + s.Name = description +} + +func (s *stepObject) addParameter(name string, value interface{}) { + s.Parameters = append(s.Parameters, parseParameter(name, value)) +} + +func (s *stepObject) addParameters(parameters map[string]interface{}) { + for key, value := range parameters { + s.Parameters = append(s.Parameters, parseParameter(key, value)) + } +} + +func (s *stepObject) addName(name string) { + s.Name = name +} + +func (s *stepObject) addAction(action func()) { + s.Action = action +} + +func (s *stepObject) getSteps() []stepObject { return s.ChildrenSteps } -func (s *stepObject) AddStep(step stepObject) { +func (s *stepObject) addStep(step stepObject) { s.ChildrenSteps = append(s.ChildrenSteps, step) } -func (s *stepObject) GetAttachments() []attachment { +func (s *stepObject) getAttachments() []attachment { return s.Attachments } -func (s *stepObject) AddAttachment(attachment attachment) { +func (s *stepObject) addAttachment(attachment attachment) { s.Attachments = append(s.Attachments, attachment) } -func (s *stepObject) SetStatus(status string) { +func (s *stepObject) setStatus(status string) { s.Status = status } -func (s *stepObject) GetStatus() string { +func (s *stepObject) getStatus() string { return s.Status } -// Step is meant to be wrapped around actions -func Step(description string, action func()) { - StepWithParameter(description, nil, action) -} - // SkipStep doesn't execute the action and marks the step as skipped in report // Reason won't appear in report until https://github.com/allure-framework/allure2/issues/774 is fixed -func SkipStep(description, reason string, action func()) { - SkipStepWithParameter(description, reason, nil, action) +func SkipStep(stepOptions ...Option) { + stepObject := newStep() + stepObject.Start = getTimestampMs() + for _, option := range stepOptions { + option(stepObject) + } + stepObject.Status = "skipped" + stepObject.Stage = "finished" + stepObject.Stop = getTimestampMs() + if currentStepObj, ok := ctxMgr.GetValue(nodeKey); ok { + currentStep := currentStepObj.(hasSteps) + currentStep.addStep(*stepObject) + } else { + log.Fatalln("could not retrieve current allure node") + } } -// StepWithParameter is meant to be wrapped around actions with the purpose of logging the parameters -func StepWithParameter(description string, parameters map[string]interface{}, action func()) { - step := newStep() - step.Name = description - step.Start = getTimestampMs() - if parameters == nil || len(parameters) > 0 { - step.Parameters = convertMapToParameters(parameters) +// Step is meant to be wrapped around actions +func Step(stepOptions ...Option) { + stepObject := newStep() + stepObject.Start = getTimestampMs() + for _, option := range stepOptions { + option(stepObject) } defer func() { panicObject := recover() - step.Stop = getTimestampMs() + stepObject.Stop = getTimestampMs() manipulateOnObjectFromCtx( testInstanceKey, func(testInstance interface{}) { @@ -75,18 +117,18 @@ func StepWithParameter(description string, parameters map[string]interface{}, ac } if testInstance.(*testing.T).Failed() || panicObject != nil { - if step.Status == "" { - step.Status = "failed" + if stepObject.Status == "" { + stepObject.Status = "failed" } } }) - step.Stage = "finished" - if step.Status == "" { - step.Status = "passed" + stepObject.Stage = "finished" + if stepObject.Status == "" { + stepObject.Status = "passed" } manipulateOnObjectFromCtx(nodeKey, func(currentStepObj interface{}) { currentStep := currentStepObj.(hasSteps) - currentStep.AddStep(*step) + currentStep.addStep(*stepObject) }) if panicObject != nil { @@ -94,33 +136,13 @@ func StepWithParameter(description string, parameters map[string]interface{}, ac } }() - ctxMgr.SetValues(gls.Values{nodeKey: step}, action) -} - -// SkipStepWithParameter doesn't execute the action and marks the step as skipped in report -// Reason won't appear in report until https://github.com/allure-framework/allure2/issues/774 is fixed -func SkipStepWithParameter(description, reason string, parameters map[string]interface{}, action func()) { - step := newStep() - step.Start = getTimestampMs() - step.Name = description - if parameters == nil || len(parameters) > 0 { - step.Parameters = convertMapToParameters(parameters) - } - step.Status = "skipped" - step.StatusDetail.Message = reason - if currentStepObj, ok := ctxMgr.GetValue(nodeKey); ok { - currentStep := currentStepObj.(hasSteps) - currentStep.AddStep(*step) - } else { - log.Fatalln("could not retrieve current allure node") - } - step.Stop = getTimestampMs() + ctxMgr.SetValues(gls.Values{nodeKey: stepObject}, stepObject.Action) } func newStep() *stepObject { return &stepObject{ Attachments: make([]attachment, 0), ChildrenSteps: make([]stepObject, 0), - Parameters: make([]Parameter, 0), + Parameters: make([]parameter, 0), } } diff --git a/test.go b/test.go index 4392111..851e7d3 100644 --- a/test.go +++ b/test.go @@ -11,7 +11,7 @@ import ( "testing" ) -type TestLabels struct { +type testLabels struct { Epic string Lead string Owner string @@ -27,19 +27,19 @@ type TestLabels struct { Language string } -// TestWithParameters executes a test and adds parameters to the Allure result object -func TestWithParameters(t *testing.T, description string, parameters map[string]interface{}, labels TestLabels, testFunc func()) { +//Test execute the test and creates an Allure result used by Allure reports +func Test(t *testing.T, testOptions ...Option) { var r *result r = newResult() r.UUID = generateUUID() r.Start = getTimestampMs() r.Name = t.Name() r.FullName = strings.Join(camelcase.Split(t.Name()), " ") - r.Description = description - r.setLabels(t, labels) + r.Description = t.Name() + r.setDefaultLabels(t) r.Steps = make([]stepObject, 0) - if parameters == nil || len(parameters) > 0 { - r.Parameters = convertMapToParameters(parameters) + for _, option := range testOptions { + option(r) } defer func() { @@ -62,19 +62,10 @@ func TestWithParameters(t *testing.T, description string, parameters map[string] if err != nil { log.Println("Failed to write content of result to json file", err) } - - if panicObject != nil { - panic(panicObject) - } }() ctxMgr.SetValues(gls.Values{ testResultKey: r, nodeKey: r, testInstanceKey: t, - }, testFunc) -} - -//Test execute the test and creates an Allure result used by Allure reports -func Test(t *testing.T, description string, testFunc func()) { - TestWithParameters(t, description, nil, TestLabels{}, testFunc) + }, r.Test) }