godog/internal/parser/parser.go
Tighearnán Carroll 6ce2b8696b
Use fs.FS abstraction for filesystem (#550)
* compiles

* mock fs in tests

* fix parser tests

* fix run.go

* rename FeatureFS to FS

* fix docs typos

* remove debug log

* add os.DirFS("./") to default options

* reword docstring

* add fs wrapper

* updated readme and changelog

* added note

* fix changelog

* remove ./ gating from defaults

* add new storage.FS tests

* increase coverage of parser.parsePath

* increase coverage of TestSuite.RetrieveFeatures

* remove another os.Stat

---------

Co-authored-by: Tighearnán Carroll <tighearnan.carroll@gamil.com>
2023-03-27 21:52:51 +02:00

235 строки
5,2 КиБ
Go

package parser
import (
"bytes"
"fmt"
"io"
"io/fs"
"os"
"regexp"
"strconv"
"strings"
gherkin "github.com/cucumber/gherkin/go/v26"
messages "github.com/cucumber/messages/go/v21"
"github.com/cucumber/godog/internal/flags"
"github.com/cucumber/godog/internal/models"
"github.com/cucumber/godog/internal/tags"
)
var pathLineRe = regexp.MustCompile(`:([\d]+)$`)
// ExtractFeaturePathLine ...
func ExtractFeaturePathLine(p string) (string, int) {
line := -1
retPath := p
if m := pathLineRe.FindStringSubmatch(p); len(m) > 0 {
if i, err := strconv.Atoi(m[1]); err == nil {
line = i
retPath = p[:strings.LastIndexByte(p, ':')]
}
}
return retPath, line
}
func parseFeatureFile(fsys fs.FS, path string, newIDFunc func() string) (*models.Feature, error) {
reader, err := fsys.Open(path)
if err != nil {
return nil, err
}
defer reader.Close()
var buf bytes.Buffer
gherkinDocument, err := gherkin.ParseGherkinDocument(io.TeeReader(reader, &buf), newIDFunc)
if err != nil {
return nil, fmt.Errorf("%s - %v", path, err)
}
gherkinDocument.Uri = path
pickles := gherkin.Pickles(*gherkinDocument, path, newIDFunc)
f := models.Feature{GherkinDocument: gherkinDocument, Pickles: pickles, Content: buf.Bytes()}
return &f, nil
}
func parseBytes(path string, feature []byte, newIDFunc func() string) (*models.Feature, error) {
reader := bytes.NewReader(feature)
var buf bytes.Buffer
gherkinDocument, err := gherkin.ParseGherkinDocument(io.TeeReader(reader, &buf), newIDFunc)
if err != nil {
return nil, fmt.Errorf("%s - %v", path, err)
}
gherkinDocument.Uri = path
pickles := gherkin.Pickles(*gherkinDocument, path, newIDFunc)
f := models.Feature{GherkinDocument: gherkinDocument, Pickles: pickles, Content: buf.Bytes()}
return &f, nil
}
func parseFeatureDir(fsys fs.FS, dir string, newIDFunc func() string) ([]*models.Feature, error) {
var features []*models.Feature
return features, fs.WalkDir(fsys, dir, func(p string, f fs.DirEntry, err error) error {
if err != nil {
return err
}
if f.IsDir() {
return nil
}
if !strings.HasSuffix(p, ".feature") {
return nil
}
feat, err := parseFeatureFile(fsys, p, newIDFunc)
if err != nil {
return err
}
features = append(features, feat)
return nil
})
}
func parsePath(fsys fs.FS, path string, newIDFunc func() string) ([]*models.Feature, error) {
var features []*models.Feature
path, line := ExtractFeaturePathLine(path)
fi, err := func() (fs.FileInfo, error) {
file, err := fsys.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
return file.Stat()
}()
if err != nil {
return features, err
}
if fi.IsDir() {
return parseFeatureDir(fsys, path, newIDFunc)
}
ft, err := parseFeatureFile(fsys, path, newIDFunc)
if err != nil {
return features, err
}
// filter scenario by line number
var pickles []*messages.Pickle
if line != -1 {
ft.Uri += ":" + strconv.Itoa(line)
}
for _, pickle := range ft.Pickles {
sc := ft.FindScenario(pickle.AstNodeIds[0])
if line == -1 || int64(line) == sc.Location.Line {
if line != -1 {
pickle.Uri += ":" + strconv.Itoa(line)
}
pickles = append(pickles, pickle)
}
}
ft.Pickles = pickles
return append(features, ft), nil
}
// ParseFeatures ...
func ParseFeatures(fsys fs.FS, filter string, paths []string) ([]*models.Feature, error) {
var order int
featureIdxs := make(map[string]int)
uniqueFeatureURI := make(map[string]*models.Feature)
newIDFunc := (&messages.Incrementing{}).NewId
for _, path := range paths {
feats, err := parsePath(fsys, path, newIDFunc)
switch {
case os.IsNotExist(err):
return nil, fmt.Errorf(`feature path "%s" is not available`, path)
case os.IsPermission(err):
return nil, fmt.Errorf(`feature path "%s" is not accessible`, path)
case err != nil:
return nil, err
}
for _, ft := range feats {
if _, duplicate := uniqueFeatureURI[ft.Uri]; duplicate {
continue
}
uniqueFeatureURI[ft.Uri] = ft
featureIdxs[ft.Uri] = order
order++
}
}
var features = make([]*models.Feature, len(uniqueFeatureURI))
for uri, feature := range uniqueFeatureURI {
idx := featureIdxs[uri]
features[idx] = feature
}
features = filterFeatures(filter, features)
return features, nil
}
type FeatureContent = flags.Feature
func ParseFromBytes(filter string, featuresInputs []FeatureContent) ([]*models.Feature, error) {
var order int
featureIdxs := make(map[string]int)
uniqueFeatureURI := make(map[string]*models.Feature)
newIDFunc := (&messages.Incrementing{}).NewId
for _, f := range featuresInputs {
ft, err := parseBytes(f.Name, f.Contents, newIDFunc)
if err != nil {
return nil, err
}
if _, duplicate := uniqueFeatureURI[ft.Uri]; duplicate {
continue
}
uniqueFeatureURI[ft.Uri] = ft
featureIdxs[ft.Uri] = order
order++
}
var features = make([]*models.Feature, len(uniqueFeatureURI))
for uri, feature := range uniqueFeatureURI {
idx := featureIdxs[uri]
features[idx] = feature
}
features = filterFeatures(filter, features)
return features, nil
}
func filterFeatures(filter string, features []*models.Feature) (result []*models.Feature) {
for _, ft := range features {
ft.Pickles = tags.ApplyTagFilter(filter, ft.Pickles)
if ft.Feature != nil && len(ft.Pickles) > 0 {
result = append(result, ft)
}
}
return
}