Добавлен формат AST
Этот коммит содержится в:
родитель
0c030e0a6c
коммит
78bab577c5
2 изменённых файлов: 488 добавлений и 0 удалений
480
internal/formatters/fmt_ast.go
Обычный файл
480
internal/formatters/fmt_ast.go
Обычный файл
|
@ -0,0 +1,480 @@
|
|||
package formatters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
messages "github.com/cucumber/messages/go/v21"
|
||||
|
||||
"git.golang1.ru/softonik/godog/colors"
|
||||
"git.golang1.ru/softonik/godog/formatters"
|
||||
)
|
||||
|
||||
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.Fprintln(f.out, s(f.indent*3)+red("Error: ")+redb(fmt.Sprintf("%+v", fail.Err))+"\n")
|
||||
}
|
||||
}
|
||||
|
||||
f.Base.Summary()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
8
run.go
8
run.go
|
@ -344,6 +344,14 @@ func (ts TestSuite) Run() int {
|
|||
return exitOptionError
|
||||
}
|
||||
}
|
||||
|
||||
if ts.Options.Format == "" {
|
||||
ts.Options.Format = "ast"
|
||||
}
|
||||
if ts.Options.Format == "ast" {
|
||||
ifmt.ASTRegister()
|
||||
}
|
||||
|
||||
if ts.Options.FS == nil {
|
||||
ts.Options.FS = storage.FS{}
|
||||
}
|
||||
|
|
Загрузка…
Создание таблицы
Сослаться в новой задаче