Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update readme and copy over gfs #5

Merged
merged 2 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<!-- markdownlint-disable-file MD010 -->

# 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()
```
179 changes: 179 additions & 0 deletions testing/gfs/afero.go
Original file line number Diff line number Diff line change
@@ -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}
}
Loading