package formatters import ( "fmt" "io" "os" "sort" "strconv" "strings" "unicode/utf8" messages "github.com/cucumber/messages/go/v21" "git.golang1.ru/softonik/godog/colors" "git.golang1.ru/softonik/godog/formatters" "git.golang1.ru/softonik/godog/internal/models" "git.golang1.ru/softonik/godog/internal/utils" ) func ASTRegister() { formatters.Format("ast", "Prints every feature with runtime statuses + updates ast.", ASTFormatterFunc) } // ASTFormatterFunc implements the FormatterFunc for the AST formatter func ASTFormatterFunc(suite string, out io.Writer) formatters.Formatter { return &AST{Base: NewBase(suite, out)} } // AST is a formatter for readable output. type AST struct { *Base firstFeature *bool } // TestRunStarted is triggered on test start. func (f *AST) TestRunStarted() { f.Base.TestRunStarted() f.Lock.Lock() defer f.Lock.Unlock() firstFeature := true f.firstFeature = &firstFeature } // Feature receives gherkin document. func (f *AST) Feature(gd *messages.GherkinDocument, p string, c []byte) { f.Lock.Lock() if !*f.firstFeature { fmt.Fprintln(f.out, "") } *f.firstFeature = false f.Lock.Unlock() f.Base.Feature(gd, p, c) f.Lock.Lock() defer f.Lock.Unlock() f.printFeature(gd.Feature) } // Pickle takes a gherkin node for formatting. func (f *AST) Pickle(pickle *messages.Pickle) { f.Base.Pickle(pickle) f.Lock.Lock() defer f.Lock.Unlock() if len(pickle.Steps) == 0 { f.printUndefinedPickle(pickle) return } } // Passed captures passed step. func (f *AST) Passed(pickle *messages.Pickle, step *messages.PickleStep, match *formatters.StepDefinition) { f.Base.Passed(pickle, step, match) f.Lock.Lock() defer f.Lock.Unlock() f.printStep(pickle, step) } // Skipped captures skipped step. func (f *AST) Skipped(pickle *messages.Pickle, step *messages.PickleStep, match *formatters.StepDefinition) { f.Base.Skipped(pickle, step, match) f.Lock.Lock() defer f.Lock.Unlock() f.printStep(pickle, step) } // Undefined captures undefined step. func (f *AST) Undefined(pickle *messages.Pickle, step *messages.PickleStep, match *formatters.StepDefinition) { f.Base.Undefined(pickle, step, match) f.Lock.Lock() defer f.Lock.Unlock() f.printStep(pickle, step) } // Failed captures failed step. func (f *AST) Failed(pickle *messages.Pickle, step *messages.PickleStep, match *formatters.StepDefinition, err error) { f.Base.Failed(pickle, step, match, err) f.Lock.Lock() defer f.Lock.Unlock() f.printStep(pickle, step) } // Failed captures failed step. func (f *AST) Ambiguous(pickle *messages.Pickle, step *messages.PickleStep, match *formatters.StepDefinition, err error) { f.Base.Ambiguous(pickle, step, match, err) f.Lock.Lock() defer f.Lock.Unlock() f.printStep(pickle, step) } // Pending captures pending step. func (f *AST) Pending(pickle *messages.Pickle, step *messages.PickleStep, match *formatters.StepDefinition) { f.Base.Pending(pickle, step, match) f.Lock.Lock() defer f.Lock.Unlock() f.printStep(pickle, step) } func (f *AST) printFeature(feature *messages.Feature) { fmt.Fprintln(f.out, keywordAndName(feature.Keyword, feature.Name)) if strings.TrimSpace(feature.Description) != "" { for _, line := range strings.Split(feature.Description, "\n") { fmt.Fprintln(f.out, s(f.indent)+strings.TrimSpace(line)) } } } func (f *AST) scenarioLengths(pickle *messages.Pickle) (scenarioHeaderLength int, maxLength int) { feature := f.Storage.MustGetFeature(pickle.Uri) astScenario := feature.FindScenario(pickle.AstNodeIds[0]) astBackground := feature.FindBackground(pickle.AstNodeIds[0]) scenarioHeaderLength = f.lengthPickle(astScenario.Keyword, astScenario.Name) maxLength = f.longestStep(astScenario.Steps, scenarioHeaderLength) if astBackground != nil { maxLength = f.longestStep(astBackground.Steps, maxLength) } return scenarioHeaderLength, maxLength } func (f *AST) printScenarioHeader(pickle *messages.Pickle, astScenario *messages.Scenario, spaceFilling int) { feature := f.Storage.MustGetFeature(pickle.Uri) text := s(f.indent) + keywordAndName(astScenario.Keyword, astScenario.Name) text += s(spaceFilling) + line(feature.Uri, astScenario.Location) fmt.Fprintln(f.out, "\n"+text) } func (f *AST) printUndefinedPickle(pickle *messages.Pickle) { feature := f.Storage.MustGetFeature(pickle.Uri) astScenario := feature.FindScenario(pickle.AstNodeIds[0]) astBackground := feature.FindBackground(pickle.AstNodeIds[0]) scenarioHeaderLength, maxLength := f.scenarioLengths(pickle) if astBackground != nil { fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(astBackground.Keyword, astBackground.Name)) for _, step := range astBackground.Steps { text := s(f.indent*2) + cyan(strings.TrimSpace(step.Keyword)) + " " + cyan(step.Text) fmt.Fprintln(f.out, text) } } // do not print scenario headers and examples multiple times if len(astScenario.Examples) > 0 { exampleTable, exampleRow := feature.FindExample(pickle.AstNodeIds[1]) firstExampleRow := exampleTable.TableBody[0].Id == exampleRow.Id firstExamplesTable := astScenario.Examples[0].Location.Line == exampleTable.Location.Line if !(firstExamplesTable && firstExampleRow) { return } } f.printScenarioHeader(pickle, astScenario, maxLength-scenarioHeaderLength) for _, examples := range astScenario.Examples { max := longestExampleRow(examples, cyan, cyan) fmt.Fprintln(f.out, "") fmt.Fprintln(f.out, s(f.indent*2)+keywordAndName(examples.Keyword, examples.Name)) f.printTableHeader(examples.TableHeader, max) for _, row := range examples.TableBody { f.printTableRow(row, max, cyan) } } } // Summary renders summary information. func (f *AST) Summary() { failedStepResults := f.Storage.MustGetPickleStepResultsByStatus(failed) if len(failedStepResults) > 0 { fmt.Fprintln(f.out, "\n--- "+red("Failed steps:")+"\n") sort.Sort(sortPickleStepResultsByPickleStepID(failedStepResults)) for _, fail := range failedStepResults { pickle := f.Storage.MustGetPickle(fail.PickleID) pickleStep := f.Storage.MustGetPickleStep(fail.PickleStepID) feature := f.Storage.MustGetFeature(pickle.Uri) astScenario := feature.FindScenario(pickle.AstNodeIds[0]) scenarioDesc := fmt.Sprintf("%s: %s", astScenario.Keyword, pickle.Name) astStep := feature.FindStep(pickleStep.AstNodeIds[0]) stepDesc := strings.TrimSpace(astStep.Keyword) + " " + pickleStep.Text fmt.Fprintln(f.out, s(f.indent)+red(scenarioDesc)+line(feature.Uri, astScenario.Location)) fmt.Fprintln(f.out, s(f.indent*2)+red(stepDesc)+line(feature.Uri, astStep.Location)) fmt.Fprint(f.out, s(f.indent*3)+redb(fmt.Sprintf("%+v", fail.Err))) } } f.SummaryBottom() } func (f *AST) SummaryBottom() { var totalSc, passedSc, undefinedSc int var totalSt, passedSt, failedSt, skippedSt, pendingSt, undefinedSt, ambiguousSt int pickleResults := f.Storage.MustGetPickleResults() for _, pr := range pickleResults { var prStatus models.StepResultStatus totalSc++ pickleStepResults := f.Storage.MustGetPickleStepResultsByPickleID(pr.PickleID) if len(pickleStepResults) == 0 { prStatus = undefined } for _, sr := range pickleStepResults { totalSt++ switch sr.Status { case passed: passedSt++ case failed: prStatus = failed failedSt++ case ambiguous: prStatus = ambiguous ambiguousSt++ case skipped: skippedSt++ case undefined: prStatus = undefined undefinedSt++ case pending: prStatus = pending pendingSt++ } } if prStatus == passed { passedSc++ } else if prStatus == undefined { undefinedSc++ } } var steps, parts, scenarios []string if passedSt > 0 { steps = append(steps, green(fmt.Sprintf("%d passed", passedSt))) } if failedSt > 0 { parts = append(parts, red(fmt.Sprintf("%d failed", failedSt))) steps = append(steps, red(fmt.Sprintf("%d failed", failedSt))) } if pendingSt > 0 { parts = append(parts, yellow(fmt.Sprintf("%d pending", pendingSt))) steps = append(steps, yellow(fmt.Sprintf("%d pending", pendingSt))) } if ambiguousSt > 0 { parts = append(parts, yellow(fmt.Sprintf("%d ambiguous", ambiguousSt))) steps = append(steps, yellow(fmt.Sprintf("%d ambiguous", ambiguousSt))) } if undefinedSt > 0 { parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefinedSc))) steps = append(steps, yellow(fmt.Sprintf("%d undefined", undefinedSt))) } else if undefinedSc > 0 { // there may be some scenarios without steps parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefinedSc))) } if skippedSt > 0 { steps = append(steps, cyan(fmt.Sprintf("%d skipped", skippedSt))) } if passedSc > 0 { scenarios = append(scenarios, green(fmt.Sprintf("%d passed", passedSc))) } scenarios = append(scenarios, parts...) testRunStartedAt := f.Storage.MustGetTestRunStarted().StartedAt elapsed := utils.TimeNowFunc().Sub(testRunStartedAt) fmt.Fprintln(f.out, "") if totalSc == 0 { fmt.Fprintln(f.out, "No scenarios") } else { fmt.Fprintf(f.out, "%d scenarios (%s), ", totalSc, strings.Join(scenarios, ", ")) } if totalSt == 0 { fmt.Fprintln(f.out, "No steps") } else { fmt.Fprintf(f.out, "%d steps (%s)\n", totalSt, strings.Join(steps, ", ")) } elapsedString := elapsed.String() if elapsed.Nanoseconds() == 0 { // go 1.5 and 1.6 prints 0 instead of 0s, if duration is zero. elapsedString = "0s" } fmt.Fprintln(f.out, elapsedString) // prints used randomization seed seed, err := strconv.ParseInt(os.Getenv("GODOG_SEED"), 10, 64) if err == nil && seed != 0 { fmt.Fprintln(f.out, "") fmt.Fprintln(f.out, "Randomized with seed:", colors.Yellow(seed)) } if text := f.Snippets(); text != "" { fmt.Fprintln(f.out, "") fmt.Fprintln(f.out, yellow("You can implement step definitions for undefined steps with these snippets:")) fmt.Fprintln(f.out, yellow(text)) } } func (f *AST) printOutlineExample(pickle *messages.Pickle, step *messages.PickleStep, backgroundSteps int) { var errorMsg string var clr = green feature := f.Storage.MustGetFeature(pickle.Uri) astScenario := feature.FindScenario(pickle.AstNodeIds[0]) scenarioHeaderLength, maxLength := f.scenarioLengths(pickle) exampleTable, exampleRow := feature.FindExample(pickle.AstNodeIds[1]) printExampleHeader := exampleTable.TableBody[0].Id == exampleRow.Id firstExamplesTable := astScenario.Examples[0].Location.Line == exampleTable.Location.Line pickleStepResults := f.Storage.MustGetPickleStepResultsByPickleIDUntilStep(pickle.Id, step.Id) firstExecutedScenarioStep := len(pickleStepResults) == backgroundSteps+1 if firstExamplesTable && printExampleHeader && firstExecutedScenarioStep { f.printScenarioHeader(pickle, astScenario, maxLength-scenarioHeaderLength) } if len(exampleTable.TableBody) == 0 { // do not print empty examples return } lastStep := len(pickleStepResults) == len(pickle.Steps) if !lastStep { // do not print examples unless all steps has finished return } for _, result := range pickleStepResults { // determine example row status switch { case result.Status == failed: errorMsg = result.Err.Error() clr = result.Status.Color() case result.Status == ambiguous: errorMsg = result.Err.Error() clr = result.Status.Color() case result.Status == undefined || result.Status == pending: clr = result.Status.Color() case result.Status == skipped && clr == nil: clr = cyan } if firstExamplesTable && printExampleHeader { // in first example, we need to print steps pickleStep := f.Storage.MustGetPickleStep(result.PickleStepID) astStep := feature.FindStep(pickleStep.AstNodeIds[0]) var text = "" if result.Def != nil { if m := outlinePlaceholderRegexp.FindAllStringIndex(astStep.Text, -1); len(m) > 0 { var pos int for i := 0; i < len(m); i++ { pair := m[i] text += cyan(astStep.Text[pos:pair[0]]) text += cyanb(astStep.Text[pair[0]:pair[1]]) pos = pair[1] } text += cyan(astStep.Text[pos:len(astStep.Text)]) } else { text = cyan(astStep.Text) } _, maxLength := f.scenarioLengths(pickle) stepLength := f.lengthPickleStep(astStep.Keyword, astStep.Text) text += s(maxLength - stepLength) text += " " + blackb("# "+DefinitionID(result.Def)) } // print the step outline fmt.Fprintln(f.out, s(f.indent*2)+cyan(strings.TrimSpace(astStep.Keyword))+" "+text) if pickleStep.Argument != nil { if table := pickleStep.Argument.DataTable; table != nil { f.printTable(table, cyan) } if docString := astStep.DocString; docString != nil { f.printDocString(docString) } } } } max := longestExampleRow(exampleTable, clr, cyan) // an example table header if printExampleHeader { fmt.Fprintln(f.out, "") fmt.Fprintln(f.out, s(f.indent*2)+keywordAndName(exampleTable.Keyword, exampleTable.Name)) f.printTableHeader(exampleTable.TableHeader, max) } f.printTableRow(exampleRow, max, clr) if errorMsg != "" { fmt.Fprintln(f.out, s(f.indent*4)+redb(errorMsg)) } } func (f *AST) printTableRow(row *messages.TableRow, max []int, clr colors.ColorFunc) { cells := make([]string, len(row.Cells)) for i, cell := range row.Cells { val := clr(cell.Value) ln := utf8.RuneCountInString(val) cells[i] = val + s(max[i]-ln) } fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cells, " | ")+" |") } func (f *AST) printTableHeader(row *messages.TableRow, max []int) { f.printTableRow(row, max, cyan) } func (f *AST) printStep(pickle *messages.Pickle, pickleStep *messages.PickleStep) { feature := f.Storage.MustGetFeature(pickle.Uri) astBackground := feature.FindBackground(pickle.AstNodeIds[0]) astScenario := feature.FindScenario(pickle.AstNodeIds[0]) astRule := feature.FindRule(pickle.AstNodeIds[0]) astStep := feature.FindStep(pickleStep.AstNodeIds[0]) var astBackgroundStep bool var firstExecutedBackgroundStep bool var backgroundSteps int if astBackground != nil { backgroundSteps = len(astBackground.Steps) for idx, step := range astBackground.Steps { if step.Id == pickleStep.AstNodeIds[0] { astBackgroundStep = true firstExecutedBackgroundStep = idx == 0 break } } } firstPickle := isFirstPickleAndNoRule(feature, pickle, astRule) || isFirstScenarioInRule(astRule, astScenario) if astBackgroundStep && !firstPickle { return } if astBackgroundStep && firstExecutedBackgroundStep { fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(astBackground.Keyword, astBackground.Name)) } if !astBackgroundStep && len(astScenario.Examples) > 0 { f.printOutlineExample(pickle, pickleStep, backgroundSteps) return } scenarioHeaderLength, maxLength := f.scenarioLengths(pickle) stepLength := f.lengthPickleStep(astStep.Keyword, pickleStep.Text) firstExecutedScenarioStep := astScenario.Steps[0].Id == pickleStep.AstNodeIds[0] if !astBackgroundStep && firstExecutedScenarioStep { f.printScenarioHeader(pickle, astScenario, maxLength-scenarioHeaderLength) } pickleStepResult := f.Storage.MustGetPickleStepResult(pickleStep.Id) text := s(f.indent*2) + pickleStepResult.Status.Color()(strings.TrimSpace(astStep.Keyword)) + " " + pickleStepResult.Status.Color()(pickleStep.Text) if pickleStepResult.Def != nil { text += s(maxLength - stepLength + 1) text += blackb("# " + DefinitionID(pickleStepResult.Def)) } fmt.Fprintln(f.out, text) if pickleStep.Argument != nil { if table := pickleStep.Argument.DataTable; table != nil { f.printTable(table, cyan) } if docString := astStep.DocString; docString != nil { f.printDocString(docString) } } if pickleStepResult.Err != nil { fmt.Fprintln(f.out, s(f.indent*2)+redb(fmt.Sprintf("%+v", pickleStepResult.Err))) } if pickleStepResult.Status == pending { fmt.Fprintln(f.out, s(f.indent*3)+yellow("TODO: write pending definition")) } } func (f *AST) printDocString(docString *messages.DocString) { var ct string if len(docString.MediaType) > 0 { ct = " " + cyan(docString.MediaType) } fmt.Fprintln(f.out, s(f.indent*3)+cyan(docString.Delimiter)+ct) for _, ln := range strings.Split(docString.Content, "\n") { fmt.Fprintln(f.out, s(f.indent*3)+cyan(ln)) } fmt.Fprintln(f.out, s(f.indent*3)+cyan(docString.Delimiter)) } // print table with aligned table cells // @TODO: need to make example header cells bold func (f *AST) printTable(t *messages.PickleTable, c colors.ColorFunc) { maxColLengths := maxColLengths(t, c) var cols = make([]string, len(t.Rows[0].Cells)) for _, row := range t.Rows { for i, cell := range row.Cells { val := c(cell.Value) colLength := utf8.RuneCountInString(val) cols[i] = val + s(maxColLengths[i]-colLength) } fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cols, " | ")+" |") } } func (f *AST) longestStep(steps []*messages.Step, pickleLength int) int { max := pickleLength for _, step := range steps { length := f.lengthPickleStep(step.Keyword, step.Text) if length > max { max = length } } return max } func (f *AST) lengthPickleStep(keyword, text string) int { return f.indent*2 + utf8.RuneCountInString(strings.TrimSpace(keyword)+" "+text) } func (f *AST) lengthPickle(keyword, name string) int { return f.indent + utf8.RuneCountInString(strings.TrimSpace(keyword)+": "+name) }