diff --git a/README.md b/README.md new file mode 100644 index 0000000..51528a9 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ + + +# aferox + +The `aferox` packages expands on [`github.com/spf13/afero`](https://github.com/spf13/afero) by adding more `afero.Fs` implementations as well as various `afero.Fs` utility functions. + +## context + +The `context` package adds the `context.Fs` interface for filesystem implementations that accept a `context.Context` per operation. +It has a basic test suite and generally works but should be considered a 🚧 work in progress 🚧. + +The `context` package re-exports various functions and types from the standard `context` packge for convenience. +Currently the creation functions focus on adapting external `context.Fs` implementations to an `afero.Fs` to be used with the other utility functions. + +```go +var base context.Fs = mypkg.NewEffectfulFs() + +fs := context.BackgroundFs(base) + +var accessor context.AccessorFunc = func() context.Context { + return context.Background() +} + +// Equivalent to `context.BackgroundFs` +fs := context.NewFs(base, accessor) +``` + +The `context.AferoFs` interface is a union of `afero.Fs` and `context.Fs`, i.e. exposing both `fs.Create` and `fs.CreateContext`. +I'm not sure if this actually has any value but it exists. + +The `context.Discard` function adapts an `afero.Fs` to a `context.AferoFs` by ignoring the `context.Context` argument. + +```go +base := afero.NewMemMapFs() + +var fs context.AferoFs = context.Discard(base) +``` + +## docker + +The `docker` package adds a docker `afero.Fs` implementation for operating on the filesystem of a container. + +```go +client := client.NewClientWithOpts(client.FromEnv) + +fs := docker.NewFs(client, "my-container-id") +``` + +## filter + +The `filter` package adds a filtering implementation of `afero.Fs` similar to `afero.RegExpFs` at accepts a predicate instead. + +```go +base := afero.NewMemMapFs() + +fs := filter.NewFs(base, func(path string) bool { + return filepath.Ext(path) == ".go" +}) +``` + +## github + +The `github` package adds multiple implementations of `afero.Fs` for interacting with the GitHub API as if it were a filesystem. +In general it can turn a GitHub url into an `afero.Fs`. + +```go +fs := github.NewFs(github.NewClient(nil)) + +file, _ := fs.Open("https://github.com/unmango") + +// ["go", "thecluster", "pulumi-baremetal", ...] +file.Readdirnames(420) +``` + +## ignore + +The `ignore` package adds a filtering `afero.Fs` that accepts a `.gitignore` file and ignores paths matched by it. + +```go +base := afero.NewMemMapFs() + +gitignore, _ := os.Open("path/to/my/.gitignore") + +fs, _ := ignore.NewFsFromGitIgnoreReader(base, gitignore) +``` + +## testing + +The `testing` package adds helper stubs for mocking filesystems in tests. + +```go +fs := &testing.Fs{ + CreateFunc: func(name string) (afero.File, error) { + return nil, errors.New("simulated error") + } +} +``` + +## writer + +The `writer` package adds a readonly `afero.Fs` implementation that dumps all file writes to the provided `io.Writer`. +Currently paths are ignored and there are no delimeters separating files. + +```go +buf := &bytes.Buffer{} +fs := writer.NewFs(buf) + +_ = afero.WriteFile(fs, "test.txt", []byte("testing"), os.ModePerm) +_ = afero.WriteFile(fs, "other.txt", []byte("blah"), os.ModePerm) + +// "testingblah" +buf.String() +``` diff --git a/testing/gfs/afero.go b/testing/gfs/afero.go new file mode 100644 index 0000000..e0328cf --- /dev/null +++ b/testing/gfs/afero.go @@ -0,0 +1,179 @@ +package gfs + +import ( + "errors" + "fmt" + "io/fs" + "reflect" + + "github.com/onsi/gomega/types" + "github.com/spf13/afero" +) + +type containFileWithBytes struct { + path string + bytes []byte +} + +// Match implements types.GomegaMatcher. +func (c *containFileWithBytes) Match(actual interface{}) (success bool, err error) { + fs, ok := actual.(afero.Fs) + if !ok { + return false, fmt.Errorf("expected an [afero.Fs] got %s", reflect.TypeOf(actual)) + } + + return afero.FileContainsBytes(fs, c.path, c.bytes) +} + +// FailureMessage implements types.GomegaMatcher. +func (c *containFileWithBytes) FailureMessage(actual interface{}) (message string) { + fs, ok := actual.(afero.Fs) + if !ok { + return fmt.Sprintf("expected an [afero.Fs] got %s", reflect.TypeOf(actual)) + } + + data, err := afero.ReadFile(fs, c.path) + if err != nil { + return err.Error() + } + + return fmt.Sprintf( + "expected file at\n%s\n\tto contain content:\n%s\n\tbut instead had\n%s", + c.path, c.bytes, data, + ) +} + +// NegatedFailureMessage implements types.GomegaMatcher. +func (c *containFileWithBytes) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("expected file at\n%s\n\tnot to contain content:\n%s", c.path, c.bytes) +} + +func ContainFileWithBytes(path string, bytes []byte) types.GomegaMatcher { + return &containFileWithBytes{path, bytes} +} + +type containFile struct { + path string +} + +// Match implements types.GomegaMatcher. +func (c *containFile) Match(actual interface{}) (success bool, err error) { + fs, ok := actual.(afero.Fs) + if !ok { + return false, fmt.Errorf("expected an [afero.Fs] got %s", reflect.TypeOf(actual)) + } + + _, err = fs.Open(c.path) + return err == nil, nil +} + +// FailureMessage implements types.GomegaMatcher. +func (c *containFile) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("expected file to exist at %s", c.path) +} + +// NegatedFailureMessage implements types.GomegaMatcher. +func (c *containFile) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("expected %s not to exist", c.path) +} + +func ContainFile(path string) types.GomegaMatcher { + return &containFile{path} +} + +type beEquivalentToFs struct { + expected afero.Fs +} + +// Match implements types.GomegaMatcher. +func (e *beEquivalentToFs) Match(actual interface{}) (success bool, err error) { + target, ok := actual.(afero.Fs) + if !ok { + return false, fmt.Errorf("exected an [afero.Fs] but got %s", reflect.TypeOf(actual)) + } + + failures := []error{} + err = afero.Walk(e.expected, "", + func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + exists, err := afero.DirExists(target, path) + if err != nil { + return err + } + if !exists { + failures = append(failures, + fmt.Errorf("expected dir to exist at %s", path), + ) + } + + return nil + } + + exists, err := afero.Exists(target, path) + if err != nil { + return err + } + if !exists { + failures = append(failures, + fmt.Errorf("expected file to exist at %s", path), + ) + + return nil + } + + expectedBytes, err := afero.ReadFile(e.expected, path) + if err != nil { + return err + } + + matched, err := afero.FileContainsBytes(target, path, expectedBytes) + if err != nil { + return err + } + if !matched { + actualBytes, err := afero.ReadFile(target, path) + if err != nil { + return err + } + + failures = append(failures, + fmt.Errorf("expected file at %s to contain content:\n\t%s\nbut found\n\t%s", + path, string(expectedBytes), string(actualBytes), + ), + ) + } + + return nil + }, + ) + if err != nil { + return false, fmt.Errorf("walking expected filesystem: %w", err) + } + + return len(failures) == 0, errors.Join(failures...) +} + +// FailureMessage implements types.GomegaMatcher. +func (e *beEquivalentToFs) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf( + "expected fs %s to match fs %s", + actual.(afero.Fs).Name(), + e.expected.Name(), + ) +} + +// NegatedFailureMessage implements types.GomegaMatcher. +func (e *beEquivalentToFs) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf( + "expected fs %s not to match fs %s", + actual.(afero.Fs).Name(), + e.expected.Name(), + ) +} + +func BeEquivalentToFs(fs afero.Fs) types.GomegaMatcher { + return &beEquivalentToFs{fs} +}