Skip to content

Commit

Permalink
Use fs.FS abstraction for filesystem (#550)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
tigh-latte and Tighearnán Carroll committed Mar 27, 2023
1 parent 3bd9e9c commit 6ce2b86
Show file tree
Hide file tree
Showing 11 changed files with 331 additions and 59 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -8,6 +8,7 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt

## Unreleased
### Added
- Support for reading feature files from an `fs.FS` ([550](https://github.com/cucumber/godog/pull/550) - [tigh-latte](https://github.com/tigh-latte))
- Added keyword functions. ([509](https://github.com/cucumber/godog/pull/509) - [otrava7](https://github.com/otrava7))
- prefer go test to use of godog cli in README ([548](https://github.com/cucumber/godog/pull/548) - [danielhelfand](https://github.com/danielhelfand)

Expand Down
26 changes: 26 additions & 0 deletions README.md
Expand Up @@ -521,6 +521,32 @@ func (a *asserter) Errorf(format string, args ...interface{}) {
}
```

### Embeds

If you're looking to compile your test binary in advance of running, you can compile the feature files into the binary via `go:embed`:

```go

//go:embed features/*
var features embed.FS

var opts = godog.Options{
Paths: []string{"features"},
FS: features,
}
```

Now, the test binary can be compiled with all feature files embedded, and can be ran independently from the feature files:

```sh
> go test -c ./test/integration/integration_test.go
> mv integration.test /some/random/dir
> cd /some/random/dir
> ./integration.test
```

**NOTE:** `godog.Options.FS` is as `fs.FS`, so custom filesystem loaders can be used.

## CLI Mode

**NOTE:** The [`godog` CLI has been deprecated](https://github.com/cucumber/godog/discussions/478). It is recommended to use `go test` instead.
Expand Down
1 change: 0 additions & 1 deletion go.sum
Expand Up @@ -46,7 +46,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
Expand Down
5 changes: 5 additions & 0 deletions internal/flags/options.go
Expand Up @@ -3,6 +3,7 @@ package flags
import (
"context"
"io"
"io/fs"
"testing"
)

Expand Down Expand Up @@ -70,6 +71,10 @@ type Options struct {
// in a map entry
FeatureContents []Feature

// FS allows passing in an `fs.FS` to read features from, such as an `embed.FS`
// or os.DirFS(string).
FS fs.FS

// ShowHelp enables suite to show CLI flags usage help and exit.
ShowHelp bool
}
Expand Down
36 changes: 22 additions & 14 deletions internal/parser/parser.go
Expand Up @@ -4,14 +4,14 @@ import (
"bytes"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"

"github.com/cucumber/gherkin/go/v26"
"github.com/cucumber/messages/go/v21"
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"
Expand All @@ -33,8 +33,8 @@ func ExtractFeaturePathLine(p string) (string, int) {
return retPath, line
}

func parseFeatureFile(path string, newIDFunc func() string) (*models.Feature, error) {
reader, err := os.Open(path)
func parseFeatureFile(fsys fs.FS, path string, newIDFunc func() string) (*models.Feature, error) {
reader, err := fsys.Open(path)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -70,9 +70,9 @@ func parseBytes(path string, feature []byte, newIDFunc func() string) (*models.F
return &f, nil
}

func parseFeatureDir(dir string, newIDFunc func() string) ([]*models.Feature, error) {
func parseFeatureDir(fsys fs.FS, dir string, newIDFunc func() string) ([]*models.Feature, error) {
var features []*models.Feature
return features, filepath.Walk(dir, func(p string, f os.FileInfo, err error) error {
return features, fs.WalkDir(fsys, dir, func(p string, f fs.DirEntry, err error) error {
if err != nil {
return err
}
Expand All @@ -85,7 +85,7 @@ func parseFeatureDir(dir string, newIDFunc func() string) ([]*models.Feature, er
return nil
}

feat, err := parseFeatureFile(p, newIDFunc)
feat, err := parseFeatureFile(fsys, p, newIDFunc)
if err != nil {
return err
}
Expand All @@ -95,21 +95,29 @@ func parseFeatureDir(dir string, newIDFunc func() string) ([]*models.Feature, er
})
}

func parsePath(path string, newIDFunc func() string) ([]*models.Feature, error) {
func parsePath(fsys fs.FS, path string, newIDFunc func() string) ([]*models.Feature, error) {
var features []*models.Feature

path, line := ExtractFeaturePathLine(path)

fi, err := os.Stat(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(path, newIDFunc)
return parseFeatureDir(fsys, path, newIDFunc)
}

ft, err := parseFeatureFile(path, newIDFunc)
ft, err := parseFeatureFile(fsys, path, newIDFunc)
if err != nil {
return features, err
}
Expand Down Expand Up @@ -138,14 +146,14 @@ func parsePath(path string, newIDFunc func() string) ([]*models.Feature, error)
}

// ParseFeatures ...
func ParseFeatures(filter string, paths []string) ([]*models.Feature, error) {
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(path, newIDFunc)
feats, err := parsePath(fsys, path, newIDFunc)

switch {
case os.IsNotExist(err):
Expand Down
111 changes: 77 additions & 34 deletions internal/parser/parser_test.go
@@ -1,10 +1,11 @@
package parser_test

import (
"io/ioutil"
"os"
"errors"
"io/fs"
"path/filepath"
"testing"
"testing/fstest"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -71,16 +72,15 @@ Feature: eat godogs
When I eat 5
Then there should be 7 remaining`

baseDir := filepath.Join(os.TempDir(), t.Name(), "godogs")
errA := os.MkdirAll(baseDir+"/a", 0755)
defer os.RemoveAll(baseDir)

require.Nil(t, errA)

err := ioutil.WriteFile(filepath.Join(baseDir, featureFileName), []byte(eatGodogContents), 0644)
require.Nil(t, err)
baseDir := "base"
fsys := fstest.MapFS{
filepath.Join(baseDir, featureFileName): {
Data: []byte(eatGodogContents),
Mode: fs.FileMode(0644),
},
}

featureFromFile, err := parser.ParseFeatures("", []string{baseDir})
featureFromFile, err := parser.ParseFeatures(fsys, "", []string{baseDir})
require.NoError(t, err)
require.Len(t, featureFromFile, 1)

Expand All @@ -96,8 +96,9 @@ Feature: eat godogs
}

func Test_ParseFeatures_FromMultiplePaths(t *testing.T) {
const featureFileName = "godogs.feature"
const featureFileContents = `Feature: eat godogs
const (
defaultFeatureFile = "godogs.feature"
defaultFeatureContents = `Feature: eat godogs
In order to be happy
As a hungry gopher
I need to be able to eat godogs
Expand All @@ -106,32 +107,74 @@ func Test_ParseFeatures_FromMultiplePaths(t *testing.T) {
Given there are 12 godogs
When I eat 5
Then there should be 7 remaining`
)

tests := map[string]struct {
fsys fs.FS
paths []string

expFeatures int
expError error
}{
"feature directories can be parsed": {
paths: []string{"base/a", "base/b"},
fsys: fstest.MapFS{
filepath.Join("base/a", defaultFeatureFile): {
Data: []byte(defaultFeatureContents),
},
filepath.Join("base/b", defaultFeatureFile): {
Data: []byte(defaultFeatureContents),
},
},
expFeatures: 2,
},
"path not found errors": {
fsys: fstest.MapFS{},
paths: []string{"base/a", "base/b"},
expError: errors.New(`feature path "base/a" is not available`),
},
"feature files can be parsed": {
paths: []string{
filepath.Join("base/a/", defaultFeatureFile),
filepath.Join("base/b/", defaultFeatureFile),
},
fsys: fstest.MapFS{
filepath.Join("base/a", defaultFeatureFile): {
Data: []byte(defaultFeatureContents),
},
filepath.Join("base/b", defaultFeatureFile): {
Data: []byte(defaultFeatureContents),
},
},
expFeatures: 2,
},
}

baseDir := filepath.Join(os.TempDir(), t.Name(), "godogs")
errA := os.MkdirAll(baseDir+"/a", 0755)
errB := os.MkdirAll(baseDir+"/b", 0755)
defer os.RemoveAll(baseDir)
for name, test := range tests {
test := test
t.Run(name, func(t *testing.T) {
t.Parallel()

require.Nil(t, errA)
require.Nil(t, errB)
features, err := parser.ParseFeatures(test.fsys, "", test.paths)
if test.expError != nil {
require.Error(t, err)
require.EqualError(t, err, test.expError.Error())
return
}

err := ioutil.WriteFile(filepath.Join(baseDir+"/a", featureFileName), []byte(featureFileContents), 0644)
require.Nil(t, err)
err = ioutil.WriteFile(filepath.Join(baseDir+"/b", featureFileName), []byte(featureFileContents), 0644)
require.Nil(t, err)
assert.Nil(t, err)
assert.Len(t, features, test.expFeatures)

features, err := parser.ParseFeatures("", []string{baseDir + "/a", baseDir + "/b"})
assert.Nil(t, err)
assert.Len(t, features, 2)
pickleIDs := map[string]bool{}
for _, f := range features {
for _, p := range f.Pickles {
if pickleIDs[p.Id] {
assert.Failf(t, "found duplicate pickle ID", "Pickle ID %s was already used", p.Id)
}

pickleIDs := map[string]bool{}
for _, f := range features {
for _, p := range f.Pickles {
if pickleIDs[p.Id] {
assert.Failf(t, "found duplicate pickle ID", "Pickle ID %s was already used", p.Id)
pickleIDs[p.Id] = true
}
}

pickleIDs[p.Id] = true
}
})
}
}
21 changes: 21 additions & 0 deletions internal/storage/fs.go
@@ -0,0 +1,21 @@
package storage

import (
"io/fs"
"os"
)

// FS is a wrapper that falls back to `os`.
type FS struct {
FS fs.FS
}

// Open a file in the provided `fs.FS`. If none provided,
// open via `os.Open`
func (f FS) Open(name string) (fs.File, error) {
if f.FS == nil {
return os.Open(name)
}

return f.FS.Open(name)
}

0 comments on commit 6ce2b86

Please sign in to comment.