godog/internal/formatters/fmt_ast.go
Softonik ccc4a42649 AST: из Base скопирован/поправлен генератор функций:
с прямой последовательностью и добавлением шагов в тестовые файлы
2025-06-06 16:10:49 +03:00

680 строки
19 КиБ
Go

package formatters
import (
"bytes"
"fmt"
"io"
"os"
"sort"
"strconv"
"strings"
"text/template"
"unicode"
"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"
"git.golang1.ru/softonik/godog/pkg/formatters/ast"
)
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) Snippets() string {
undefinedStepResults := f.Storage.MustGetPickleStepResultsByStatus(undefined)
if len(undefinedStepResults) == 0 {
return ""
}
var index int
var snips []undefinedSnippet
// build snippets
for _, u := range undefinedStepResults {
pickleStep := f.Storage.MustGetPickleStep(u.PickleStepID)
steps := []string{pickleStep.Text}
arg := pickleStep.Argument
if u.Def != nil {
steps = u.Def.Undefined
arg = nil
}
for _, step := range steps {
expr := snippetExprCleanup.ReplaceAllString(step, "\\$1")
expr = snippetNumbers.ReplaceAllString(expr, "(\\d+)")
expr = snippetExprQuoted.ReplaceAllString(expr, "$1\"([^\"]*)\"$2")
expr = "^" + strings.TrimSpace(expr) + "$"
name := snippetNumbers.ReplaceAllString(step, " ")
name = snippetExprQuoted.ReplaceAllString(name, " ")
name = strings.TrimSpace(snippetMethodName.ReplaceAllString(name, ""))
var words []string
for i, w := range strings.Split(name, " ") {
switch {
case i != 0:
w = strings.Title(w)
case len(w) > 0:
r := []rune(w)
w = string(unicode.ToLower(r[0])) + string(r[1:])
}
words = append(words, w)
}
name = strings.Join(words, "")
if len(name) == 0 {
index++
name = fmt.Sprintf("StepDefinitioninition%d", index)
}
var found bool
for _, snip := range snips {
if snip.Expr == expr {
found = true
break
}
}
if !found {
s := undefinedSnippet{Method: name, Expr: expr, argument: arg}
snips = append(snips, s)
ast.ДобавитьШаг("`"+expr+"`", name, s.Args())
}
}
}
var buf bytes.Buffer
if err := AST_undefinedSnippetsTpl.Execute(&buf, snips); err != nil {
panic(err)
}
// there may be trailing spaces
return strings.Replace(buf.String(), " \n", "\n", -1)
}
var AST_undefinedSnippetsTpl = template.Must(template.New("snippets").Funcs(snippetHelperFuncs).Parse(`
func InitializeScenario(ctx *godog.ScenarioContext) {}
// ---
{{ range . }}func {{ .Method }}({{ .Args }}) {
}
{{end}}{{ range . }}
ctx.Step({{ backticked .Expr }}, {{ .Method }}){{end}}
`))
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)
}