2 Commits

Author SHA1 Message Date
4e07a1a657 wip(assertion): incomplete refactor of tests and assertion helpers
Tests have started using github.com/jimeh/go-mocktesting which allows
testing unhappy paths where t.Fatal() and related functions are called.
2021-12-28 02:24:00 +00:00
3f967b571f feat(assert): add marshaling assertion helpers for JSON, YAML and XML 2021-11-06 20:28:03 +00:00
136 changed files with 3634 additions and 3031 deletions

View File

@@ -7,15 +7,11 @@ jobs:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.18"
cache: false
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v4
uses: golangci/golangci-lint-action@v2
with:
version: v1.56
version: v1.43
env:
VERBOSE: "true"
@@ -23,10 +19,16 @@ jobs:
name: Tidy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: "1.18"
go-version: 1.15
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Check if mods are tidy
run: make check-tidy
@@ -34,12 +36,18 @@ jobs:
name: Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: "1.18"
go-version: 1.15
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Publish coverage
uses: paambaati/codeclimate-action@v5.0.0
uses: paambaati/codeclimate-action@v2.7.4
env:
VERBOSE: "true"
GOMAXPROCS: 4
@@ -60,16 +68,20 @@ jobs:
- macos-latest
- windows-latest
go_version:
- "1.18"
- "1.19"
- "1.20"
- "1.21"
- "1.22"
- "1.15"
- "1.16"
- "1.17"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: "1.22"
go-version: ${{ matrix.go_version }}
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run tests
run: go test -v -count=1 -race ./...

View File

@@ -25,6 +25,7 @@ linters:
- asciicheck
- bodyclose
- deadcode
- depguard
- durationcheck
- errcheck
- errorlint
@@ -68,6 +69,9 @@ linters:
- whitespace
issues:
exclude:
- Using the variable on range scope `tt` in function literal
- Using the variable on range scope `tc` in function literal
exclude-rules:
- path: "_test\\.go"
linters:

View File

@@ -34,19 +34,23 @@ SHELL := env \
# Tools
#
TOOLS += $(TOOLDIR)/gobin
$(TOOLDIR)/gobin:
GO111MODULE=off go get -u github.com/myitcv/gobin
# external tool
define tool # 1: binary-name, 2: go-import-path
TOOLS += $(TOOLDIR)/$(1)
$(TOOLDIR)/$(1): Makefile
GOBIN="$(CURDIR)/$(TOOLDIR)" go install "$(2)"
$(TOOLDIR)/$(1): $(TOOLDIR)/gobin Makefile
gobin $(V) "$(2)"
endef
$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc@latest))
$(eval $(call tool,gofumpt,mvdan.cc/gofumpt@latest))
$(eval $(call tool,goimports,golang.org/x/tools/cmd/goimports@latest))
$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.56))
$(eval $(call tool,gomod,github.com/Helcaraxan/gomod@latest))
$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc))
$(eval $(call tool,gofumpt,mvdan.cc/gofumpt))
$(eval $(call tool,goimports,golang.org/x/tools/cmd/goimports))
$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.43))
$(eval $(call tool,gomod,github.com/Helcaraxan/gomod))
.PHONY: tools
tools: $(TOOLS)

69
assert.go Normal file
View File

@@ -0,0 +1,69 @@
package golden
var defaultAsserter = NewAsserter()
// AssertJSONMarshaling asserts that the given "v" value JSON marshals to an
// expected value fetched from a golden file on disk, and then verifies that the
// marshaled result produces a value that is equal to "v" when unmarshaled.
//
// Used for objects that do NOT change when they are marshaled and unmarshaled.
func AssertJSONMarshaling(t TestingT, v interface{}) {
t.Helper()
defaultAsserter.JSONMarshaling(t, v)
}
// AssertJSONMarshalingP asserts that the given "v" value JSON marshals to an
// expected value fetched from a golden file on disk, and then verifies that the
// marshaled result produces a value that is equal to "want" when unmarshaled.
//
// Used for objects that change when they are marshaled and unmarshaled.
func AssertJSONMarshalingP(t TestingT, v, want interface{}) {
t.Helper()
defaultAsserter.JSONMarshalingP(t, v, want)
}
// AssertXMLMarshaling asserts that the given "v" value XML marshals to an
// expected value fetched from a golden file on disk, and then verifies that the
// marshaled result produces a value that is equal to "v" when unmarshaled.
//
// Used for objects that do NOT change when they are marshaled and unmarshaled.
func AssertXMLMarshaling(t TestingT, v interface{}) {
t.Helper()
defaultAsserter.XMLMarshaling(t, v)
}
// AssertXMLMarshalingP asserts that the given "v" value XML marshals to an
// expected value fetched from a golden file on disk, and then verifies that the
// marshaled result produces a value that is equal to "want" when unmarshaled.
//
// Used for objects that change when they are marshaled and unmarshaled.
func AssertXMLMarshalingP(t TestingT, v, want interface{}) {
t.Helper()
defaultAsserter.XMLMarshalingP(t, v, want)
}
// AssertYAMLMarshaling asserts that the given "v" value YAML marshals to an
// expected value fetched from a golden file on disk, and then verifies that the
// marshaled result produces a value that is equal to "v" when unmarshaled.
//
// Used for objects that do NOT change when they are marshaled and unmarshaled.
func AssertYAMLMarshaling(t TestingT, v interface{}) {
t.Helper()
defaultAsserter.YAMLMarshaling(t, v)
}
// AssertYAMLMarshalingP asserts that the given "v" value YAML marshals to an
// expected value fetched from a golden file on disk, and then verifies that the
// marshaled result produces a value that is equal to "want" when unmarshaled.
//
// Used for objects that change when they are marshaled and unmarshaled.
func AssertYAMLMarshalingP(t TestingT, v, want interface{}) {
t.Helper()
defaultAsserter.YAMLMarshalingP(t, v, want)
}

68
assert_example_test.go Normal file
View File

@@ -0,0 +1,68 @@
package golden_test
import (
"testing"
"github.com/jimeh/go-golden"
)
type MyBook struct {
FooBar string `json:"foo_bar,omitempty" yaml:"fooBar,omitempty" xml:"Foo_Bar,omitempty"`
Bar string `json:"-" yaml:"-" xml:"-"`
baz string
}
// TestExampleMyBookMarshaling reads/writes the following golden files:
//
// testdata/TestExampleMyBookMarshaling/marshaled_json.golden
// testdata/TestExampleMyBookMarshaling/marshaled_xml.golden
// testdata/TestExampleMyBookMarshaling/marshaled_yaml.golden
//
func TestExampleMyBookMarshaling(t *testing.T) {
obj := &MyBook{FooBar: "Hello World!"}
golden.AssertJSONMarshaling(t, obj)
golden.AssertYAMLMarshaling(t, obj)
golden.AssertXMLMarshaling(t, obj)
}
// TestExampleMyBookMarshalingP reads/writes the following golden files:
//
// testdata/TestExampleMyBookMarshalingP/marshaled_json.golden
// testdata/TestExampleMyBookMarshalingP/marshaled_xml.golden
// testdata/TestExampleMyBookMarshalingP/marshaled_yaml.golden
//
func TestExampleMyBookMarshalingP(t *testing.T) {
obj := &MyBook{FooBar: "Hello World!", Bar: "Oops", baz: "nope!"}
want := &MyBook{FooBar: "Hello World!"}
golden.AssertJSONMarshalingP(t, obj, want)
golden.AssertYAMLMarshalingP(t, obj, want)
golden.AssertXMLMarshalingP(t, obj, want)
}
// TestExampleMyBookMarshalingTabular reads/writes the following golden files:
//
// testdata/TestExampleMyBookMarshalingTabular/empty/marshaled_json.golden
// testdata/TestExampleMyBookMarshalingTabular/empty/marshaled_xml.golden
// testdata/TestExampleMyBookMarshalingTabular/empty/marshaled_yaml.golden
// testdata/TestExampleMyBookMarshalingTabular/full/marshaled_json.golden
// testdata/TestExampleMyBookMarshalingTabular/full/marshaled_xml.golden
// testdata/TestExampleMyBookMarshalingTabular/full/marshaled_yaml.golden
//
func TestExampleMyBookMarshalingTabular(t *testing.T) {
tests := []struct {
name string
obj *MyBook
}{
{name: "empty", obj: &MyBook{}},
{name: "full", obj: &MyBook{FooBar: "Hello World!"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
golden.AssertJSONMarshaling(t, tt.obj)
golden.AssertYAMLMarshaling(t, tt.obj)
golden.AssertXMLMarshaling(t, tt.obj)
})
}
}

559
assert_test.go Normal file
View File

@@ -0,0 +1,559 @@
package golden
import (
"bytes"
"encoding/xml"
"fmt"
"regexp"
"testing"
"time"
"github.com/jimeh/go-golden/marshal"
"github.com/jimeh/go-golden/unmarshal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
//
// Helpers
//
type author struct {
FirstName string `json:"first_name" yaml:"first_name" xml:"first_name"`
LastName string `json:"last_name" yaml:"last_name" xml:"last_name"`
}
type book struct {
ID string `json:"id" yaml:"id" xml:"id"`
Title string `json:"title" yaml:"title" xml:"title"`
Author *author `json:"author,omitempty" yaml:"author,omitempty" xml:"author,omitempty"`
Year int `json:"year,omitempty" yaml:"year,omitempty" xml:"year,omitempty"`
}
type article struct {
ID string `json:"id" yaml:"id" xml:"id"`
Title string `json:"title" yaml:"title" xml:"title"`
Author *author `json:"author" yaml:"author" xml:"author"`
Date *time.Time `json:"date,omitempty" yaml:"date,omitempty" xml:"date,omitempty"`
Rank int `json:"-" yaml:"-" xml:"-"`
order int
}
// comic is used for testing custom marshal/unmarshal functions on a type.
type comic struct {
ID string
Name string
Issue string
Ignored string
}
type xmlComic struct {
ID string `xml:"id,attr"`
Name string `xml:",chardata"`
Issue string `xml:"issue,attr"`
}
func (s *comic) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`{"%s":"%s=%s"}`, s.ID, s.Name, s.Issue)), nil
}
func (s *comic) UnmarshalJSON(data []byte) error {
m := regexp.MustCompile(`^{\s*"(.*?)":\s*"(.*?)=(.*)"\s*}$`)
matches := m.FindSubmatch(bytes.TrimSpace(data))
if matches == nil {
return nil
}
s.ID = string(matches[1])
s.Name = string(matches[2])
s.Issue = string(matches[3])
return nil
}
func (s *comic) MarshalYAML() (interface{}, error) {
return map[string]map[string]string{s.ID: {s.Name: s.Issue}}, nil
}
func (s *comic) UnmarshalYAML(value *yaml.Node) error {
// Horribly hacky code, but it works and specifically only needs to extract
// these specific three values.
if len(value.Content) == 2 {
s.ID = value.Content[0].Value
if len(value.Content[1].Content) == 2 {
s.Name = value.Content[1].Content[0].Value
s.Issue = value.Content[1].Content[1].Value
}
}
return nil
}
func (s *comic) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(
&xmlComic{ID: s.ID, Name: s.Name, Issue: s.Issue},
start,
)
}
func (s *comic) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
x := &xmlComic{}
_ = d.DecodeElement(x, &start)
v := comic{ID: x.ID, Name: x.Name, Issue: x.Issue}
*s = v
return nil
}
func boolPtr(b bool) *bool {
return &b
}
func intPtr(i int) *int {
return &i
}
func stringPtr(s string) *string {
return &s
}
//
// Test cases
//
var marhalingTestCases = []struct {
name string
v interface{}
}{
{
name: "true bool pointer",
v: boolPtr(true),
},
{
name: "false bool pointer",
v: boolPtr(false),
},
{
name: "int pointer",
v: intPtr(42),
},
{
name: "string pointer",
v: stringPtr("hello world"),
},
{
name: "empty struct",
v: &book{},
},
{
name: "partial struct",
v: &book{
ID: "cfda163c-d5c1-44a2-909b-5d2ce3a31979",
Title: "The Traveler",
},
},
{
name: "full struct",
v: &book{
ID: "cfda163c-d5c1-44a2-909b-5d2ce3a31979",
Title: "The Traveler",
Author: &author{
FirstName: "John",
LastName: "Twelve Hawks",
},
Year: 2005,
},
},
{
name: "custom marshaling",
v: &comic{
ID: "2fd5af35-b85e-4f03-8eba-524be28d7a5b",
Name: "Hello World!",
Issue: "Forty Two",
},
},
}
var articleDate = time.Date(
2021, time.October, 27, 23, 30, 34, 0, time.FixedZone("", 1*60*60),
).UTC()
var marshalingPTestCases = []struct {
name string
v interface{}
want interface{}
}{
{
name: "true bool pointer",
v: boolPtr(true),
want: boolPtr(true),
},
{
name: "false bool pointer",
v: boolPtr(false),
want: boolPtr(false),
},
{
name: "int pointer",
v: intPtr(42),
want: intPtr(42),
},
{
name: "string pointer",
v: stringPtr("hello world"),
want: stringPtr("hello world"),
},
{
name: "empty struct",
v: &article{},
want: &article{},
},
{
name: "partial struct",
v: &book{
ID: "10eec54d-e30a-4428-be18-01095d889126",
Title: "Time Travel",
},
want: &book{
ID: "10eec54d-e30a-4428-be18-01095d889126",
Title: "Time Travel",
},
},
{
name: "full struct",
v: &article{
ID: "10eec54d-e30a-4428-be18-01095d889126",
Title: "Time Travel",
Author: &author{
FirstName: "Doc",
LastName: "Brown",
},
Date: &articleDate,
Rank: 8,
order: 16,
},
want: &article{
ID: "10eec54d-e30a-4428-be18-01095d889126",
Title: "Time Travel",
Author: &author{
FirstName: "Doc",
LastName: "Brown",
},
Date: &articleDate,
},
},
{
name: "custom marshaling",
v: &comic{
ID: "2fd5af35-b85e-4f03-8eba-524be28d7a5b",
Name: "Hello World!",
Issue: "Forty Two",
Ignored: "don't pay attention to this :)",
},
want: &comic{
ID: "2fd5af35-b85e-4f03-8eba-524be28d7a5b",
Name: "Hello World!",
Issue: "Forty Two",
},
},
}
//
// Tests
//
func TestWithGolden(t *testing.T) {
type args struct {
golden Golden
}
tests := []struct {
name string
args args
}{
{
name: "nil",
args: args{golden: nil},
},
{
name: "non-nil",
args: args{golden: New(WithSuffix(".my-custom-golden"))},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := New(WithSuffix(".original-golden"))
o := &asserterOptions{golden: original}
fn := WithGolden(tt.args.golden)
fn.apply(o)
if tt.args.golden == nil {
assert.Equal(t, original, o.golden)
} else {
assert.Equal(t, tt.args.golden, o.golden)
}
})
}
}
func TestNormalizedLineBreaks(t *testing.T) {
type args struct {
value bool
}
tests := []struct {
name string
args args
}{
{
name: "true",
args: args{value: true},
},
{
name: "false",
args: args{value: false},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := &asserterOptions{normalizeLineBreaks: !tt.args.value}
fn := WithNormalizedLineBreaks(tt.args.value)
fn.apply(o)
assert.Equal(t, tt.args.value, o.normalizeLineBreaks)
})
}
}
func TestNewAssert(t *testing.T) {
otherGolden := New(WithSuffix(".other-golden"))
type args struct {
options []AsserterOption
}
tests := []struct {
name string
args args
want *asserter
}{
{
name: "no options",
args: args{options: []AsserterOption{}},
want: &asserter{
json: NewMarshalingAsserter(
defaultGolden, "JSON",
marshal.JSON, unmarshal.JSON,
true,
),
xml: NewMarshalingAsserter(
defaultGolden, "XML",
marshal.XML, unmarshal.XML,
true,
),
yaml: NewMarshalingAsserter(
defaultGolden, "YAML",
marshal.YAML, unmarshal.YAML,
true,
),
},
},
{
name: "WithGlobal option",
args: args{
options: []AsserterOption{
WithGolden(otherGolden),
},
},
want: &asserter{
json: NewMarshalingAsserter(
otherGolden, "JSON",
marshal.JSON, unmarshal.JSON,
true,
),
xml: NewMarshalingAsserter(
otherGolden, "XML",
marshal.XML, unmarshal.XML,
true,
),
yaml: NewMarshalingAsserter(
otherGolden, "YAML",
marshal.YAML, unmarshal.YAML,
true,
),
},
},
{
name: "WithNormalizedLineBreaks option",
args: args{
options: []AsserterOption{
WithNormalizedLineBreaks(false),
},
},
want: &asserter{
json: NewMarshalingAsserter(
defaultGolden, "JSON",
marshal.JSON, unmarshal.JSON,
false,
),
xml: NewMarshalingAsserter(
defaultGolden, "XML",
marshal.XML, unmarshal.XML,
false,
),
yaml: NewMarshalingAsserter(
defaultGolden, "YAML",
marshal.YAML, unmarshal.YAML,
false,
),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := NewAsserter(tt.args.options...)
assert.Implements(t, (*Asserter)(nil), a)
got, ok := a.(*asserter)
require.True(
t, ok, "failed to type assert return value to a *asserter",
)
assert.Equal(t, tt.want.json.Golden, got.json.Golden)
assert.Equal(t, tt.want.json.Format, got.json.Format)
assert.Equal(t,
tt.want.json.NormalizeLineBreaks, got.json.NormalizeLineBreaks,
)
assert.Equal(t, tt.want.xml.Golden, got.xml.Golden)
assert.Equal(t, tt.want.xml.Format, got.xml.Format)
assert.Equal(t,
tt.want.xml.NormalizeLineBreaks, got.xml.NormalizeLineBreaks,
)
assert.Equal(t, tt.want.yaml.Golden, got.yaml.Golden)
assert.Equal(t, tt.want.yaml.Format, got.yaml.Format)
assert.Equal(t,
tt.want.yaml.NormalizeLineBreaks, got.yaml.NormalizeLineBreaks,
)
})
}
}
func Test_asserter(t *testing.T) {
a := &asserter{}
assert.Implements(t, (*Asserter)(nil), a)
}
func TestAssertJSONMarshaling(t *testing.T) {
for _, tt := range marhalingTestCases {
t.Run(tt.name, func(t *testing.T) {
AssertJSONMarshaling(t, tt.v)
})
}
}
func TestAssertJSONMarshalingP(t *testing.T) {
for _, tt := range marshalingPTestCases {
t.Run(tt.name, func(t *testing.T) {
AssertJSONMarshalingP(t, tt.v, tt.want)
})
}
}
func TestAssertXMLMarshaling(t *testing.T) {
for _, tt := range marhalingTestCases {
t.Run(tt.name, func(t *testing.T) {
AssertXMLMarshaling(t, tt.v)
})
}
}
func TestAssertXMLMarshalingP(t *testing.T) {
for _, tt := range marshalingPTestCases {
t.Run(tt.name, func(t *testing.T) {
AssertXMLMarshalingP(t, tt.v, tt.want)
})
}
}
func TestAssertYAMLMarshaling(t *testing.T) {
for _, tt := range marhalingTestCases {
t.Run(tt.name, func(t *testing.T) {
AssertYAMLMarshaling(t, tt.v)
})
}
}
func TestAssertYAMLMarshalingP(t *testing.T) {
for _, tt := range marshalingPTestCases {
t.Run(tt.name, func(t *testing.T) {
AssertYAMLMarshalingP(t, tt.v, tt.want)
})
}
}
func TestAssert_JSONMarshaling(t *testing.T) {
for _, tt := range marhalingTestCases {
t.Run(tt.name, func(t *testing.T) {
assert := NewAsserter()
assert.JSONMarshaling(t, tt.v)
})
}
}
func TestAssert_JSONMarshalingP(t *testing.T) {
for _, tt := range marshalingPTestCases {
t.Run(tt.name, func(t *testing.T) {
assert := NewAsserter()
assert.JSONMarshalingP(t, tt.v, tt.want)
})
}
}
func TestAssert_XMLMarshaling(t *testing.T) {
for _, tt := range marhalingTestCases {
t.Run(tt.name, func(t *testing.T) {
assert := NewAsserter()
assert.XMLMarshaling(t, tt.v)
})
}
}
func TestAssert_XMLMarshalingP(t *testing.T) {
for _, tt := range marshalingPTestCases {
t.Run(tt.name, func(t *testing.T) {
assert := NewAsserter()
assert.XMLMarshalingP(t, tt.v, tt.want)
})
}
}
func TestAssert_YAMLMarshaling(t *testing.T) {
for _, tt := range marhalingTestCases {
t.Run(tt.name, func(t *testing.T) {
assert := NewAsserter()
assert.YAMLMarshaling(t, tt.v)
})
}
}
func TestAssert_YAMLMarshalingP(t *testing.T) {
for _, tt := range marshalingPTestCases {
t.Run(tt.name, func(t *testing.T) {
assert := NewAsserter()
assert.YAMLMarshalingP(t, tt.v, tt.want)
})
}
}

178
asserter.go Normal file
View File

@@ -0,0 +1,178 @@
package golden
import (
"github.com/jimeh/go-golden/marshal"
"github.com/jimeh/go-golden/unmarshal"
)
// Asserter exposes a series of JSON, YAML, and XML marshaling assertion
// helpers.
type Asserter interface {
// JSONMarshaling asserts that the given "v" value JSON marshals to an
// expected value fetched from a golden file on disk, and then verifies that
// the marshaled result produces a value that is equal to "v" when
// unmarshaled.
//
// Used for objects that do NOT change when they are marshaled and
// unmarshaled.
JSONMarshaling(t TestingT, v interface{})
// JSONMarshalingP asserts that the given "v" value JSON marshals to an
// expected value fetched from a golden file on disk, and then verifies that
// the marshaled result produces a value that is equal to "want" when
// unmarshaled.
//
// Used for objects that change when they are marshaled and unmarshaled.
JSONMarshalingP(t TestingT, v interface{}, want interface{})
// XMLMarshaling asserts that the given "v" value XML marshals to an
// expected value fetched from a golden file on disk, and then verifies that
// the marshaled result produces a value that is equal to "v" when
// unmarshaled.
//
// Used for objects that do NOT change when they are marshaled and
// unmarshaled.
XMLMarshaling(t TestingT, v interface{})
// XMLMarshalingP asserts that the given "v" value XML marshals to an
// expected value fetched from a golden file on disk, and then verifies that
// the marshaled result produces a value that is equal to "want" when
// unmarshaled.
//
// Used for objects that change when they are marshaled and unmarshaled.
XMLMarshalingP(t TestingT, v interface{}, want interface{})
// YAMLMarshaling asserts that the given "v" value YAML marshals to an
// expected value fetched from a golden file on disk, and then verifies that
// the marshaled result produces a value that is equal to "v" when
// unmarshaled.
//
// Used for objects that do NOT change when they are marshaled and
// unmarshaled.
YAMLMarshaling(t TestingT, v interface{})
// YAMLMarshalingP asserts that the given "v" value YAML marshals to an
// expected value fetched from a golden file on disk, and then verifies that
// the marshaled result produces a value that is equal to "want" when
// unmarshaled.
//
// Used for objects that change when they are marshaled and unmarshaled.
YAMLMarshalingP(t TestingT, v interface{}, want interface{})
}
// NewAsserter returns a new Asserter which exposes a number of marshaling
// assertion helpers for JSON, YAML and XML.
//
// The default encoders all specify indentation of two spaces, essentially
// enforcing pretty formatting for JSON and XML.
//
// The default decoders for JSON and YAML prohibit unknown fields which are not
// present on the provided struct.
func NewAsserter(options ...AsserterOption) Asserter {
o := &asserterOptions{
golden: defaultGolden,
normalizeLineBreaks: true,
}
for _, opt := range options {
opt.apply(o)
}
return &asserter{
json: NewMarshalingAsserter(
o.golden, "JSON",
marshal.JSON, unmarshal.JSON,
o.normalizeLineBreaks,
),
xml: NewMarshalingAsserter(
o.golden, "XML",
marshal.XML, unmarshal.XML,
o.normalizeLineBreaks,
),
yaml: NewMarshalingAsserter(
o.golden, "YAML",
marshal.YAML, unmarshal.YAML,
o.normalizeLineBreaks,
),
}
}
type asserterOptions struct {
golden Golden
normalizeLineBreaks bool
}
type AsserterOption interface {
apply(*asserterOptions)
}
type asserterOptionFunc func(*asserterOptions)
func (fn asserterOptionFunc) apply(c *asserterOptions) {
fn(c)
}
// WithGolden allows setting a custom *Golden instance when calling NewAssert().
func WithGolden(golden Golden) AsserterOption {
return asserterOptionFunc(func(a *asserterOptions) {
if golden != nil {
a.golden = golden
}
})
}
// WithNormalizedLineBreaks allows turning off line-break normalization which
// replaces Windows' CRLF (\r\n) and Mac Classic CR (\r) line breaks with Unix's
// LF (\n) line breaks.
func WithNormalizedLineBreaks(value bool) AsserterOption {
return asserterOptionFunc(func(a *asserterOptions) {
a.normalizeLineBreaks = value
})
}
// asserter implements the Assert interface.
type asserter struct {
json *MarshalingAsserter
xml *MarshalingAsserter
yaml *MarshalingAsserter
}
func (s *asserter) JSONMarshaling(t TestingT, v interface{}) {
t.Helper()
s.json.Marshaling(t, v)
}
func (s *asserter) JSONMarshalingP(
t TestingT,
v interface{},
want interface{},
) {
t.Helper()
s.json.MarshalingP(t, v, want)
}
func (s *asserter) XMLMarshaling(t TestingT, v interface{}) {
t.Helper()
s.xml.Marshaling(t, v)
}
func (s *asserter) XMLMarshalingP(t TestingT, v, want interface{}) {
t.Helper()
s.xml.MarshalingP(t, v, want)
}
func (s *asserter) YAMLMarshaling(t TestingT, v interface{}) {
t.Helper()
s.yaml.Marshaling(t, v)
}
func (s *asserter) YAMLMarshalingP(t TestingT, v, want interface{}) {
t.Helper()
s.yaml.MarshalingP(t, v, want)
}

46
fs.go
View File

@@ -1,46 +0,0 @@
package golden
import "os"
type FS interface {
// MkdirAll creates a directory named path, along with any necessary
// parents, and returns nil, or else returns an error. The permission bits
// perm (before umask) are used for all directories that MkdirAll creates.
MkdirAll(path string, perm os.FileMode) error
// ReadFile reads the named file and returns the contents. A successful call
// returns err == nil, not err == EOF. Because ReadFile reads the whole
// file, it does not treat an EOF from Read as an error to be reported.
ReadFile(filename string) ([]byte, error)
// WriteFile writes data to a file named by filename. If the file does not
// exist, WriteFile creates it with permissions perm; otherwise WriteFile
// truncates it before writing, without changing permissions.
WriteFile(name string, data []byte, perm os.FileMode) error
}
type fsImpl struct{}
var _ FS = fsImpl{}
// NewFS returns a new FS instance which operates against the host file system
// via calls to functions in the os package.
func NewFS() FS {
return fsImpl{}
}
// DefaultFS is the default FS instance used by all top-level package functions,
// including the Default Golden instance, and also the New function.
var DefaultFS = NewFS()
func (fsImpl) MkdirAll(path string, perm os.FileMode) error {
return os.MkdirAll(path, perm)
}
func (fsImpl) ReadFile(filename string) ([]byte, error) {
return os.ReadFile(filename)
}
func (fsImpl) WriteFile(filename string, data []byte, perm os.FileMode) error {
return os.WriteFile(filename, data, perm)
}

View File

@@ -1,131 +0,0 @@
package golden
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMkdirAll(t *testing.T) {
tempDir := t.TempDir()
tests := []struct {
name string
path string
perm os.FileMode
wantErr bool
}{
{"create new dir", "newdir", 0o755, false},
{"create nested dirs", "nested/dir/structure", 0o755, false},
{"invalid path", string([]byte{0, 0}), 0o755, true},
}
fs := NewFS()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path := filepath.Join(tempDir, tt.path)
err := fs.MkdirAll(path, tt.perm)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
_, err := os.Stat(path)
assert.NoError(t, err)
}
})
}
}
func TestReadFile(t *testing.T) {
tempDir := t.TempDir()
sampleFilePath := filepath.Join(tempDir, "sample.txt")
sampleContent := []byte("Hello, world!")
err := os.WriteFile(sampleFilePath, sampleContent, 0o600)
require.NoError(t, err)
tests := []struct {
name string
filename string
want []byte
wantErr bool
}{
{"read existing file", sampleFilePath, sampleContent, false},
{"file does not exist", "nonexistent.txt", nil, true},
}
fs := NewFS()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := fs.ReadFile(tt.filename)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, string(tt.want), string(got))
}
})
}
}
func TestWriteFile(t *testing.T) {
tempDir := t.TempDir()
tests := []struct {
name string
filename string
data []byte
perm os.FileMode
wantErr bool
}{
{
"write to new file",
"newfile.txt",
[]byte("new content"),
0o644,
false,
},
{
"overwrite existing file",
"existing.txt",
[]byte("overwritten content"),
0o644,
false,
},
{
"invalid filename",
string([]byte{0, 0}),
[]byte("invalid filename"),
0o644,
true,
},
{
"non-existent directory",
"nonexistentdir/newfile.txt",
[]byte("this will fail"),
0o644,
true,
},
}
fs := NewFS()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
filePath := filepath.Join(tempDir, tt.filename)
err := fs.WriteFile(filePath, tt.data, tt.perm)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
content, err := os.ReadFile(filePath)
assert.NoError(t, err)
assert.Equal(t, tt.data, content)
}
})
}
}

13
go.mod
View File

@@ -1,14 +1,11 @@
module github.com/jimeh/go-golden
go 1.18
go 1.15
require (
github.com/jimeh/envctl v0.1.0
github.com/stretchr/testify v1.9.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
github.com/jimeh/go-mocktesting v0.1.0
github.com/spf13/afero v1.6.0
github.com/stretchr/testify v1.7.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)

29
go.sum
View File

@@ -1,16 +1,33 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jimeh/envctl v0.1.0 h1:KTv3D+pi5M4/PgFVE/W8ssWqiZP3pDJ8Cga50L+1avo=
github.com/jimeh/envctl v0.1.0/go.mod h1:aM27ffBbO1yUBKUzgJGCUorS4z+wyh+qhQe1ruxXZZo=
github.com/jimeh/go-mocktesting v0.1.0 h1:y0tLABo3V4i9io7m6TiXdXbU3IVMjtPvWkr+A0+aLTM=
github.com/jimeh/go-mocktesting v0.1.0/go.mod h1:xnekQ6yP/ull2ewkOp1CbgH7Dym7nbKa/t96XWrIiH8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

173
gold.go
View File

@@ -1,173 +0,0 @@
package golden
import (
"os"
"path/filepath"
"strings"
"github.com/jimeh/go-golden/sanitize"
)
// gold is the underlying struct that implements the Golden interface.
type gold struct {
// dirMode determines the file system permissions of any folders created to
// hold golden files.
dirMode os.FileMode
// fileMode determines the file system permissions of any created or updated
// golden files written to disk.
fileMode os.FileMode
// suffix determines the filename suffix for all golden files. Typically
// this should be ".golden", but can be changed here if needed.
suffix string
// dirname is the name of the top-level directory at the root of the package
// which holds all golden files. Typically this should be "testdata", but
// can be changed here if needed.
dirname string
// updateFunc is used to determine if golden files should be updated or
// not. Its boolean return value is returned by Update().
updateFunc UpdateFunc
// fs is used for all file system operations. This enables providing custom
// afero.fs instances which can be useful for testing purposes.
fs FS
// logOnWrite determines if a message is logged with t.Logf when a golden
// file is written to with either of the set methods.
logOnWrite bool
}
// Ensure golden satisfies Golden interface.
var _ Golden = &gold{}
func (g *gold) Do(t TestingT, data []byte) []byte {
t.Helper()
if g.Update() {
g.Set(t, data)
}
return g.Get(t)
}
func (g *gold) DoP(t TestingT, name string, data []byte) []byte {
t.Helper()
if g.Update() {
g.SetP(t, name, data)
}
return g.GetP(t, name)
}
func (g *gold) File(t TestingT) string {
t.Helper()
return g.file(t, "")
}
func (g *gold) FileP(t TestingT, name string) string {
t.Helper()
if name == "" {
t.Fatalf("golden: name cannot be empty")
}
return g.file(t, name)
}
func (g *gold) file(t TestingT, name string) string {
t.Helper()
if t.Name() == "" {
t.Fatalf("golden: could not determine filename for TestingT instance")
}
base := []string{g.dirname, filepath.FromSlash(t.Name())}
if name != "" {
base = append(base, name)
}
f := filepath.Clean(filepath.Join(base...) + g.suffix)
dirty := strings.Split(f, string(os.PathSeparator))
clean := make([]string, 0, len(dirty))
for _, s := range dirty {
clean = append(clean, sanitize.Filename(s))
}
return strings.Join(clean, string(os.PathSeparator))
}
func (g *gold) Get(t TestingT) []byte {
t.Helper()
return g.get(t, "")
}
func (g *gold) GetP(t TestingT, name string) []byte {
t.Helper()
if name == "" {
t.Fatalf("golden: name cannot be empty")
}
return g.get(t, name)
}
func (g *gold) get(t TestingT, name string) []byte {
t.Helper()
f := g.file(t, name)
b, err := g.fs.ReadFile(f)
if err != nil {
t.Fatalf("golden: %s", err.Error())
}
return b
}
func (g *gold) Set(t TestingT, data []byte) {
t.Helper()
g.set(t, "", data)
}
func (g *gold) SetP(t TestingT, name string, data []byte) {
t.Helper()
if name == "" {
t.Fatalf("golden: name cannot be empty")
}
g.set(t, name, data)
}
func (g *gold) set(t TestingT, name string, data []byte) {
t.Helper()
f := g.file(t, name)
dir := filepath.Dir(f)
if g.logOnWrite {
t.Logf("golden: writing golden file: %s", f)
}
err := g.fs.MkdirAll(dir, g.dirMode)
if err != nil {
t.Fatalf("golden: failed to create directory: %s", err.Error())
}
err = g.fs.WriteFile(f, data, g.fileMode)
if err != nil {
t.Fatalf("golden: filed to write file: %s", err.Error())
}
}
func (g *gold) Update() bool {
return g.updateFunc()
}

View File

@@ -1,528 +0,0 @@
package golden
// func Test_gold_File(t *testing.T) {
// type fields struct {
// suffix *string
// dirname *string
// }
// tests := []struct {
// name string
// testName string
// fields fields
// want string
// wantFatals []string
// }{
// {
// name: "top-level",
// testName: "TestFooBar",
// want: filepath.Join("testdata", "TestFooBar.golden"),
// },
// {
// name: "sub-test",
// testName: "TestFooBar/it_is_here",
// want: filepath.Join(
// "testdata", "TestFooBar", "it_is_here.golden",
// ),
// },
// {
// name: "blank test name",
// testName: "",
// wantFatals: []string{
// "golden: could not determine filename for TestingT instance",
// },
// },
// {
// name: "custom dirname",
// testName: "TestFozBar",
// fields: fields{
// dirname: stringPtr("goldenfiles"),
// },
// want: filepath.Join("goldenfiles", "TestFozBar.golden"),
// },
// {
// name: "custom suffix",
// testName: "TestFozBaz",
// fields: fields{
// suffix: stringPtr(".goldfile"),
// },
// want: filepath.Join("testdata", "TestFozBaz.goldfile"),
// },
// {
// name: "custom dirname and suffix",
// testName: "TestFozBar",
// fields: fields{
// dirname: stringPtr("goldenfiles"),
// suffix: stringPtr(".goldfile"),
// },
// want: filepath.Join("goldenfiles", "TestFozBar.goldfile"),
// },
// {
// name: "invalid chars in test name",
// testName: `TestFooBar/foo?<>:*|"bar`,
// want: filepath.Join(
// "testdata", "TestFooBar", "foo_______bar.golden",
// ),
// },
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// if tt.fields.suffix == nil {
// tt.fields.suffix = stringPtr(".golden")
// }
// if tt.fields.dirname == nil {
// tt.fields.dirname = stringPtr("testdata")
// }
// g := &gold{
// suffix: *tt.fields.suffix,
// dirname: *tt.fields.dirname,
// }
// ft := &fakeTestingT{name: tt.testName}
// var got string
// testInGoroutine(t, func() {
// got = g.File(ft)
// })
// assert.Equal(t, tt.want, got)
// assert.Equal(t, tt.wantFatals, ft.fatals)
// })
// }
// }
// func Test_gold_FileP(t *testing.T) {
// type fields struct {
// suffix *string
// dirname *string
// }
// tests := []struct {
// name string
// testName string
// goldenName string
// fields fields
// want string
// wantFatals []string
// }{
// {
// name: "top-level",
// testName: "TestFooBar",
// goldenName: "yaml",
// want: filepath.Join("testdata", "TestFooBar", "yaml.golden"),
// },
// {
// name: "sub-test",
// testName: "TestFooBar/it_is_here",
// goldenName: "json",
// want: filepath.Join(
// "testdata", "TestFooBar", "it_is_here", "json.golden",
// ),
// },
// {
// name: "blank test name",
// testName: "",
// goldenName: "json",
// wantFatals: []string{
// "golden: could not determine filename for TestintT instance",
// },
// },
// {
// name: "custom dirname",
// testName: "TestFozBar",
// goldenName: "xml",
// fields: fields{
// dirname: stringPtr("goldenfiles"),
// },
// want: filepath.Join("goldenfiles", "TestFozBar", "xml.golden"),
// },
// {
// name: "custom suffix",
// testName: "TestFozBaz",
// goldenName: "toml",
// fields: fields{
// suffix: stringPtr(".goldfile"),
// },
// want: filepath.Join("testdata", "TestFozBaz", "toml.goldfile"),
// },
// {
// name: "custom dirname and suffix",
// testName: "TestFozBar",
// goldenName: "json",
// fields: fields{
// dirname: stringPtr("goldenfiles"),
// suffix: stringPtr(".goldfile"),
// },
// want: filepath.Join("goldenfiles", "TestFozBar", "json.goldfile"),
// },
// {
// name: "invalid chars in test name",
// testName: `TestFooBar/foo?<>:*|"bar`,
// goldenName: "yml",
// want: filepath.Join(
// "testdata", "TestFooBar", "foo_______bar", "yml.golden",
// ),
// },
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// if tt.fields.suffix == nil {
// tt.fields.suffix = stringPtr(".golden")
// }
// if tt.fields.dirname == nil {
// tt.fields.dirname = stringPtr("testdata")
// }
// g := &gold{
// suffix: *tt.fields.suffix,
// dirname: *tt.fields.dirname,
// }
// ft := &fakeTestingT{name: tt.testName}
// var got string
// testInGoroutine(t, func() {
// got = g.FileP(ft, tt.goldenName)
// })
// assert.Equal(t, tt.want, got)
// assert.Equal(t, tt.wantFatals, ft.fatals)
// })
// }
// }
// func Test_gold_Get(t *testing.T) {
// type fields struct {
// suffix *string
// dirname *string
// }
// tests := []struct {
// name string
// testName string
// fields fields
// files map[string][]byte
// want []byte
// wantAborted bool
// wantFailCount int
// wantTestOutput []string
// }{
// {
// name: "file exists",
// testName: "TestFooBar",
// files: map[string][]byte{
// filepath.Join("testdata", "TestFooBar.golden"): []byte(
// "foo: bar\nhello: world",
// ),
// },
// want: []byte("foo: bar\nhello: world"),
// },
// {
// name: "file is missing",
// testName: "TestFooBar",
// files: map[string][]byte{},
// wantAborted: true,
// wantFailCount: 1,
// wantTestOutput: []string{
// "golden: open " + filepath.Join(
// "testdata", "TestFooBar.golden",
// ) + ": file does not exist\n",
// },
// },
// {
// name: "sub-test file exists",
// testName: "TestFooBar/it_is_here",
// files: map[string][]byte{
// filepath.Join(
// "testdata", "TestFooBar", "it_is_here.golden",
// ): []byte("this is really here ^_^\n"),
// },
// want: []byte("this is really here ^_^\n"),
// },
// {
// name: "sub-test file is missing",
// testName: "TestFooBar/not_really_here",
// files: map[string][]byte{},
// wantAborted: true,
// wantFailCount: 1,
// wantTestOutput: []string{
// "golden: open " + filepath.Join(
// "testdata", "TestFooBar", "not_really_here.golden",
// ) + ": file does not exist\n",
// },
// },
// {
// name: "blank test name",
// testName: "",
// wantAborted: true,
// wantFailCount: 1,
// wantTestOutput: []string{
// "golden: could not determine filename for given " +
// "*mocktesting.T instance\n",
// },
// },
// {
// name: "custom dirname",
// testName: "TestFozBar",
// fields: fields{
// dirname: stringPtr("goldenfiles"),
// },
// files: map[string][]byte{
// filepath.Join("goldenfiles", "TestFozBar.golden"): []byte(
// "foo: bar\nhello: world",
// ),
// },
// want: []byte("foo: bar\nhello: world"),
// },
// {
// name: "custom suffix",
// testName: "TestFozBaz",
// fields: fields{
// suffix: stringPtr(".goldfile"),
// },
// files: map[string][]byte{
// filepath.Join("testdata", "TestFozBaz.goldfile"): []byte(
// "foo: bar\nhello: world",
// ),
// },
// want: []byte("foo: bar\nhello: world"),
// },
// {
// name: "custom dirname and suffix",
// testName: "TestFozBar",
// fields: fields{
// dirname: stringPtr("goldenfiles"),
// suffix: stringPtr(".goldfile"),
// },
// files: map[string][]byte{
// filepath.Join("goldenfiles", "TestFozBar.goldfile"): []byte(
// "foo: bar\nhello: world",
// ),
// },
// want: []byte("foo: bar\nhello: world"),
// },
// {
// name: "invalid chars in test name",
// testName: `TestFooBar/foo?<>:*|"bar`,
// files: map[string][]byte{
// filepath.Join(
// "testdata", "TestFooBar", "foo_______bar.golden",
// ): []byte("foo: bar\nhello: world"),
// },
// want: []byte("foo: bar\nhello: world"),
// },
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// fs := NewFS() // TODO: Replace with in-memory stub FS.
// for f, b := range tt.files {
// _ = fs.WriteFile(f, b, 0o644)
// }
// if tt.fields.suffix == nil {
// tt.fields.suffix = stringPtr(".golden")
// }
// if tt.fields.dirname == nil {
// tt.fields.dirname = stringPtr("testdata")
// }
// g := &gold{
// suffix: *tt.fields.suffix,
// dirname: *tt.fields.dirname,
// fs: fs,
// }
// mt := mocktesting.NewT(tt.testName)
// var got []byte
// mocktesting.Go(func() {
// got = g.Get(mt)
// })
// assert.Equal(t, tt.want, got)
// assert.Equal(t, tt.wantAborted, mt.Aborted(), "aborted")
// assert.Equal(t,
// tt.wantFailCount, mt.FailedCount(), "failed count",
// )
// assert.Equal(t, tt.wantTestOutput, mt.Output(), "test output")
// })
// }
// }
// func Test_gold_GetP(t *testing.T) {
// type args struct {
// name string
// }
// type fields struct {
// suffix *string
// dirname *string
// }
// tests := []struct {
// name string
// testName string
// args args
// fields fields
// files map[string][]byte
// want []byte
// wantAborted bool
// wantFailCount int
// wantTestOutput []string
// }{
// {
// name: "file exists",
// testName: "TestFooBar",
// args: args{name: "yaml"},
// files: map[string][]byte{
// filepath.Join("testdata", "TestFooBar", "yaml.golden"): []byte(
// "foo: bar\nhello: world",
// ),
// },
// want: []byte("foo: bar\nhello: world"),
// },
// {
// name: "file is missing",
// testName: "TestFooBar",
// args: args{name: "yaml"},
// files: map[string][]byte{},
// wantAborted: true,
// wantFailCount: 1,
// wantTestOutput: []string{
// "golden: open " + filepath.Join(
// "testdata", "TestFooBar", "yaml.golden",
// ) + ": file does not exist\n",
// },
// },
// {
// name: "sub-test file exists",
// testName: "TestFooBar/it_is_here",
// args: args{name: "plain"},
// files: map[string][]byte{
// filepath.Join(
// "testdata", "TestFooBar", "it_is_here", "plain.golden",
// ): []byte("this is really here ^_^\n"),
// },
// want: []byte("this is really here ^_^\n"),
// },
// {
// name: "sub-test file is missing",
// testName: "TestFooBar/not_really_here",
// args: args{name: "plain"},
// files: map[string][]byte{},
// wantAborted: true,
// wantFailCount: 1,
// wantTestOutput: []string{
// "golden: open " + filepath.Join(
// "testdata", "TestFooBar", "not_really_here", "plain.golden",
// ) + ": file does not exist\n",
// },
// },
// {
// name: "blank test name",
// testName: "",
// args: args{name: "plain"},
// wantAborted: true,
// wantFailCount: 1,
// wantTestOutput: []string{
// "golden: could not determine filename for given " +
// "*mocktesting.T instance\n",
// },
// },
// {
// name: "blank name",
// testName: "TestFooBar",
// args: args{name: ""},
// wantAborted: true,
// wantFailCount: 1,
// wantTestOutput: []string{
// "golden: name cannot be empty\n",
// },
// },
// {
// name: "custom dirname",
// testName: "TestFozBar",
// args: args{name: "yaml"},
// fields: fields{
// dirname: stringPtr("goldenfiles"),
// },
// files: map[string][]byte{
// filepath.Join(
// "goldenfiles", "TestFozBar", "yaml.golden",
// ): []byte("foo: bar\nhello: world"),
// },
// want: []byte("foo: bar\nhello: world"),
// },
// {
// name: "custom suffix",
// testName: "TestFozBaz",
// args: args{name: "yaml"},
// fields: fields{
// suffix: stringPtr(".goldfile"),
// },
// files: map[string][]byte{
// filepath.Join(
// "testdata", "TestFozBaz", "yaml.goldfile",
// ): []byte("foo: bar\nhello: world"),
// },
// want: []byte("foo: bar\nhello: world"),
// },
// {
// name: "custom dirname and suffix",
// testName: "TestFozBar",
// args: args{name: "yaml"},
// fields: fields{
// dirname: stringPtr("goldenfiles"),
// suffix: stringPtr(".goldfile"),
// },
// files: map[string][]byte{
// filepath.Join(
// "goldenfiles", "TestFozBar", "yaml.goldfile",
// ): []byte("foo: bar\nhello: world"),
// },
// want: []byte("foo: bar\nhello: world"),
// },
// {
// name: "invalid chars in test name",
// testName: `TestFooBar/foo?<>:*|"bar`,
// args: args{name: "trash"},
// files: map[string][]byte{
// filepath.Join(
// "testdata", "TestFooBar", "foo_______bar", "trash.golden",
// ): []byte("foo: bar\nhello: world"),
// },
// want: []byte("foo: bar\nhello: world"),
// },
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// fs := NewFS() // TODO: Replace with in-memory stub FS
// for f, b := range tt.files {
// _ = fs.WriteFile(f, b, 0o644)
// }
// if tt.fields.suffix == nil {
// tt.fields.suffix = stringPtr(".golden")
// }
// if tt.fields.dirname == nil {
// tt.fields.dirname = stringPtr("testdata")
// }
// g := &gold{
// suffix: *tt.fields.suffix,
// dirname: *tt.fields.dirname,
// fs: fs,
// }
// mt := mocktesting.NewT(tt.testName)
// var got []byte
// mocktesting.Go(func() {
// got = g.GetP(mt, tt.args.name)
// })
// assert.Equal(t, tt.want, got)
// assert.Equal(t, tt.wantAborted, mt.Aborted(), "aborted")
// assert.Equal(t,
// tt.wantFailCount, mt.FailedCount(), "failed count",
// )
// assert.Equal(t, tt.wantTestOutput, mt.Output(), "test output")
// })
// }
// }

625
golden.go
View File

@@ -1,154 +1,275 @@
// Package golden is yet another package for working with *.golden test files,
// with a focus on simplicity through it's default behavior.
// with a focus on simplicity through it's default behavior, and marshaling
// assertion helpers to validate the JSON, YAML, and XML formats of structs.
//
// Golden file names are based on the name of the test function and any subtest
// names by calling t.Name(). File names are sanitized to ensure they are
// names by calling t.Name(). File names are sanitized to ensure they're
// compatible with Linux, macOS and Windows systems regardless of what
// characters might be in a subtest's name.
//
// # Usage
// Assertion Helpers
//
// Typical usage should look something like this:
// There are marshaling assertion helpers for JSON, YAML, and XML. Each helper
// operates in two stages:
//
// func TestExampleMyStruct(t *testing.T) {
// got, err := json.Marshal(&MyStruct{Foo: "Bar"})
// require.NoError(t, err)
// 1. Marshal the provided object to a byte slice and read the corresponding
// golden file from disk followed by verifying both are identical.
//
// want := golden.Do(t, got)
// 2. Unmarshal the content of the golden file, verifying that the result is
// identical to the original given object.
//
// assert.Equal(t, want, got)
// }
// Typical usage of a assertion helper would look something like this in a
// tabular test:
//
// The above example will attempt to read/write to:
// type MyStruct struct {
// FooBar string `json:"foo_bar" yaml:"fooBar" xml:"Foo_Bar"`
// }
//
// testdata/TestExampleMyStruct.golden
// func TestMyStructMarshaling(t *testing.T) {
// tests := []struct {
// name string
// obj *MyStruct
// }{
// {name: "empty", obj: &MyStruct{}},
// {name: "full", obj: &MyStruct{FooBar: "Hello World!"}},
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// golden.AssertJSONMarshaling(t, tt.obj)
// golden.AssertYAMLMarshaling(t, tt.obj)
// golden.AssertXMLMarshaling(t, tt.obj)
// })
// }
// }
//
// The call to golden.Do() is equivalent to:
// The above example will read from the following golden files:
//
// if golden.Update() {
// golden.Set(t, got)
// }
// want := golden.Get(t)
// testdata/TestMyStructMarshaling/empty/marshaled_json.golden
// testdata/TestMyStructMarshaling/empty/marshaled_xml.golden
// testdata/TestMyStructMarshaling/empty/marshaled_yaml.golden
// testdata/TestMyStructMarshaling/full/marshaled_json.golden
// testdata/TestMyStructMarshaling/full/marshaled_xml.golden
// testdata/TestMyStructMarshaling/full/marshaled_yaml.golden
//
// If a corresponding golden file cannot be found on disk, the test will fail.
// To create/update golden files, simply set the GOLDEN_UPDATE environment
// variable to one of "1", "y", "t", "yes", "on", or "true" when running the
// tests.
//
// Golden files should be committed to source control, as it allow tests to fail
// when the marshaling results for an object changes.
//
// Golden Usage
//
// If you need manual control over golden file reading and writing, this is a
// typical example:
//
// func TestExampleMyStruct(t *testing.T) {
// got, err := json.Marshal(&MyStruct{Foo: "Bar"})
// require.NoError(t, err)
//
// if golden.Update() {
// golden.Set(t, got)
// }
// want := golden.Get(t)
//
// assert.Equal(t, want, got)
// }
//
// The above example will read/write to:
//
// testdata/TestExampleMyStruct.golden
//
// To update the golden file (have golden.Update() return true), simply set the
// GOLDEN_UPDATE environment variable to one of "1", "y", "t", "yes", "on", or
// "true" when running tests.
//
// # Sub-Tests
// Sub-Tests
//
// As the golden filename is based on t.Name(), it works with sub-tests too,
// ensuring each sub-test gets it's own golden file. For example:
//
// func TestExampleMyStructTabular(t *testing.T) {
// tests := []struct {
// name string
// obj *MyStruct
// }{
// {name: "empty struct", obj: &MyStruct{}},
// {name: "full struct", obj: &MyStruct{Foo: "Bar"}},
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// got, err := json.Marshal(tt.obj)
// require.NoError(t, err)
// func TestExampleMyStructTabular(t *testing.T) {
// tests := []struct {
// name string
// obj *MyStruct
// }{
// {name: "empty struct", obj: &MyStruct{}},
// {name: "full struct", obj: &MyStruct{Foo: "Bar"}},
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// got, err := json.Marshal(tt.obj)
// require.NoError(t, err)
//
// want := golden.Do(t, got)
// if golden.Update() {
// golden.Set(t, got)
// }
// want := golden.Get(t)
//
// assert.Equal(t, want, got)
// })
// }
// }
// assert.Equal(t, want, got)
// })
// }
// }
//
// The above example will read/write to:
//
// testdata/TestExampleMyStructTabular/empty_struct.golden
// testdata/TestExampleMyStructTabular/full_struct.golden
// testdata/TestExampleMyStructTabular/empty_struct.golden
// testdata/TestExampleMyStructTabular/full_struct.golden
//
// # Multiple Golden Files in a Single Test
// Multiple Golden Files in a Single Test
//
// The "P" suffixed methods, GetP(), SetP(), DoP(), and FileP(), all take a name
// The "P" suffixed methods, GetP(), SetP(), and FileP(), all take a name
// argument which allows using specific golden files within a given *testing.T
// instance.
//
// func TestExampleMyStructP(t *testing.T) {
// gotJSON, _ := json.Marshal(&MyStruct{Foo: "Bar"})
// gotXML, _ := xml.Marshal(&MyStruct{Foo: "Bar"})
// func TestExampleMyStructP(t *testing.T) {
// gotJSON, _ := json.Marshal(&MyStruct{Foo: "Bar"})
// gotXML, _ := xml.Marshal(&MyStruct{Foo: "Bar"})
//
// wantJSON := golden.DoP(t, "json", gotJSON)
// wantXML := golden.DoP(t, "xml", gotXML)
// if golden.Update() {
// golden.SetP(t, "json", gotJSON)
// golden.SetP(t, "xml", gotXML)
// }
//
// assert.Equal(t, wantJSON, gotJSON)
// assert.Equal(t, wantXML, gotXML)
// }
// assert.Equal(t, golden.GetP(t, "json"), gotJSON)
// assert.Equal(t, golden.GetP(t, "xml"), gotXML)
// }
//
// The above example will read/write to:
//
// testdata/TestExampleMyStructP/json.golden
// testdata/TestExampleMyStructP/xml.golden
// testdata/TestExampleMyStructP/json.golden
// testdata/TestExampleMyStructP/xml.golden
//
// This works with tabular tests too of course:
//
// func TestExampleMyStructTabularP(t *testing.T) {
// tests := []struct {
// name string
// obj *MyStruct
// }{
// {name: "empty struct", obj: &MyStruct{}},
// {name: "full struct", obj: &MyStruct{Foo: "Bar"}},
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// gotJSON, _ := json.Marshal(tt.obj)
// gotXML, _ := xml.Marshal(tt.obj)
// func TestExampleMyStructTabularP(t *testing.T) {
// tests := []struct {
// name string
// obj *MyStruct
// }{
// {name: "empty struct", obj: &MyStruct{}},
// {name: "full struct", obj: &MyStruct{Foo: "Bar"}},
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// gotJSON, _ := json.Marshal(tt.obj)
// gotXML, _ := xml.Marshal(tt.obj)
//
// wantJSON := golden.DoP(t, "json", gotJSON)
// wantXML := golden.DoP(t, "xml", gotXML)
// if golden.Update() {
// golden.SetP(t, "json", gotJSON)
// golden.SetP(t, "xml", gotXML)
// }
//
// assert.Equal(t, wantJSON, gotJSON)
// assert.Equal(t, wantXML, gotXML)
// })
// }
// }
// assert.Equal(t, golden.GetP(t, "json"), gotJSON)
// assert.Equal(t, golden.GetP(t, "xml"), gotXML)
// })
// }
// }
//
// The above example will read/write to:
//
// testdata/TestExampleMyStructTabularP/empty_struct/json.golden
// testdata/TestExampleMyStructTabularP/empty_struct/xml.golden
// testdata/TestExampleMyStructTabularP/full_struct/json.golden
// testdata/TestExampleMyStructTabularP/full_struct/xml.golden
// testdata/TestExampleMyStructTabularP/empty_struct/json.golden
// testdata/TestExampleMyStructTabularP/empty_struct/xml.golden
// testdata/TestExampleMyStructTabularP/full_struct/json.golden
// testdata/TestExampleMyStructTabularP/full_struct/xml.golden
//
package golden
import "os"
import (
"os"
"path/filepath"
"strings"
"testing"
var (
// DefaultGolden is the default Golden instance used by all top-level
// package functions.
DefaultGolden = New()
// DefaultDirMode is the default file system permissions used for any
// created directories to hold golden files.
DefaultDirMode = os.FileMode(0o755)
// DefaultFileMode is the default file system permissions used for any
// created or updated golden files written to disk.
DefaultFileMode = os.FileMode(0o644)
// DefaultSuffix is the default filename suffix used for all golden files.
DefaultSuffix = ".golden"
// DefaultDirname is the default name of the top-level directory used to
// hold golden files.
DefaultDirname = "testdata"
// DefaultUpdateFunc is the default function used to determine if golden
// files should be updated or not. It is called by Update().
DefaultUpdateFunc = EnvUpdateFunc
// DefaultLogOnWrite is the default value for logOnWrite on all Golden
// instances.
DefaultLogOnWrite = true
"github.com/jimeh/go-golden/sanitize"
"github.com/spf13/afero"
)
// TestingT is a interface describing a sub-set of methods of *testing.T which
// golden uses.
type TestingT interface {
Error(args ...interface{})
Errorf(format string, args ...interface{})
FailNow()
Fatal(args ...interface{})
Fatalf(format string, args ...interface{})
Helper()
Log(args ...interface{})
Logf(format string, args ...interface{})
Name() string
}
var defaultGolden = New()
// File returns the filename of the golden file for the given *testing.T
// instance as determined by t.Name().
func File(t *testing.T) string {
t.Helper()
return defaultGolden.File(t)
}
// Get returns the content of the golden file for the given *testing.T instance
// as determined by t.Name(). If no golden file can be found/read, it will fail
// the test by calling t.Fatal().
func Get(t *testing.T) []byte {
t.Helper()
return defaultGolden.Get(t)
}
// Set writes given data to the golden file for the given *testing.T instance as
// determined by t.Name(). If writing fails it will fail the test by calling
// t.Fatal() with error details.
func Set(t *testing.T, data []byte) {
t.Helper()
defaultGolden.Set(t, data)
}
// FileP returns the filename of the specifically named golden file for the
// given *testing.T instance as determined by t.Name().
func FileP(t *testing.T, name string) string {
t.Helper()
return defaultGolden.FileP(t, name)
}
// GetP returns the content of the specifically named golden file belonging
// to the given *testing.T instance as determined by t.Name(). If no golden file
// can be found/read, it will fail the test with t.Fatal().
//
// This is very similar to Get(), but it allows multiple different golden files
// to be used within the same one *testing.T instance.
func GetP(t *testing.T, name string) []byte {
t.Helper()
return defaultGolden.GetP(t, name)
}
// SetP writes given data of the specifically named golden file belonging to
// the given *testing.T instance as determined by t.Name(). If writing fails it
// will fail the test with t.Fatal() detailing the error.
//
// This is very similar to Set(), but it allows multiple different golden files
// to be used within the same one *testing.T instance.
func SetP(t *testing.T, name string, data []byte) {
t.Helper()
defaultGolden.SetP(t, name, data)
}
// Update returns true when golden is set to update golden files. Should be used
// to determine if golden.Set() or golden.SetP() should be called or not.
//
// Default behavior uses EnvUpdateFunc() to check if the "GOLDEN_UPDATE"
// environment variable is set to a truthy value. To customize create a custom
// *Golden instance with New() and set a new UpdateFunc value.
func Update() bool {
return defaultGolden.Update()
}
// Golden handles all interactions with golden files. The top-level package
// functions proxy through to a default global Golden instance.
type Golden interface {
@@ -166,11 +287,6 @@ type Golden interface {
// test by calling t.Fatal() with error details.
Set(t TestingT, data []byte)
// Do is a convenience function for calling Update(), Set(), and Get() in a
// single call. If Update() returns true, data will be written to the golden
// file using Set(), before reading it back with Get().
Do(t TestingT, data []byte) []byte
// FileP returns the filename of the specifically named golden file for the
// given TestingT instance as determined by t.Name().
FileP(t TestingT, name string) string
@@ -191,11 +307,6 @@ type Golden interface {
// files to be used within the same one TestingT instance.
SetP(t TestingT, name string, data []byte)
// DoP is a convenience function for calling Update(), SetP(), and GetP() in
// a single call. If Update() returns true, data will be written to the
// golden file using SetP(), before reading it back with GetP().
DoP(t TestingT, name string, data []byte) []byte
// Update returns true when golden is set to update golden files. Should be
// used to determine if golden.Set() or golden.SetP() should be called or
// not.
@@ -209,14 +320,14 @@ type Golden interface {
// New returns a new Golden instance. Used to create custom Golden instances.
// See the the various Option functions for details of what can be customized.
func New(options ...Option) Golden {
g := &gold{
dirMode: DefaultDirMode,
fileMode: DefaultFileMode,
suffix: DefaultSuffix,
dirname: DefaultDirname,
updateFunc: DefaultUpdateFunc,
fs: DefaultFS,
logOnWrite: DefaultLogOnWrite,
g := &golden{
dirMode: 0o755,
fileMode: 0o644,
suffix: ".golden",
dirname: "testdata",
updateFunc: EnvUpdateFunc,
fs: afero.NewOsFs(),
logOnWrite: true,
}
for _, opt := range options {
@@ -226,88 +337,224 @@ func New(options ...Option) Golden {
return g
}
// Do is a convenience function for calling Update(), Set(), and Get() in a
// single call. If Update() returns true, data will be written to the golden
// file using Set(), before reading it back with Get().
func Do(t TestingT, data []byte) []byte {
t.Helper()
return DefaultGolden.Do(t, data)
type Option interface {
apply(*golden)
}
// DoP is a convenience function for calling Update(), SetP(), and GetP() in a
// single call. If Update() returns true, data will be written to the golden
// file using SetP(), before reading it back with GetP().
func DoP(t TestingT, name string, data []byte) []byte {
t.Helper()
type optionFunc func(*golden)
return DefaultGolden.DoP(t, name, data)
func (fn optionFunc) apply(g *golden) {
fn(g)
}
// File returns the filename of the golden file for the given *testing.T
// instance as determined by t.Name().
func File(t TestingT) string {
t.Helper()
return DefaultGolden.File(t)
}
// FileP returns the filename of the specifically named golden file for the
// given *testing.T instance as determined by t.Name().
func FileP(t TestingT, name string) string {
t.Helper()
return DefaultGolden.FileP(t, name)
}
// Get returns the content of the golden file for the given *testing.T instance
// as determined by t.Name(). If no golden file can be found/read, it will fail
// the test by calling t.Fatal().
func Get(t TestingT) []byte {
t.Helper()
return DefaultGolden.Get(t)
}
// GetP returns the content of the specifically named golden file belonging
// to the given *testing.T instance as determined by t.Name(). If no golden file
// can be found/read, it will fail the test with t.Fatal().
// WithDirMode sets the file system permissions used for any folders created to
// hold golden files.
//
// This is very similar to Get(), but it allows multiple different golden files
// to be used within the same one *testing.T instance.
func GetP(t TestingT, name string) []byte {
t.Helper()
return DefaultGolden.GetP(t, name)
// When this option is not provided, the default value is 0o755.
func WithDirMode(mode os.FileMode) Option {
return optionFunc(func(g *golden) {
g.dirMode = mode
})
}
// Set writes given data to the golden file for the given *testing.T instance as
// determined by t.Name(). If writing fails it will fail the test by calling
// t.Fatal() with error details.
func Set(t TestingT, data []byte) {
t.Helper()
DefaultGolden.Set(t, data)
}
// SetP writes given data of the specifically named golden file belonging to
// the given *testing.T instance as determined by t.Name(). If writing fails it
// will fail the test with t.Fatal() detailing the error.
// WithFileMode sets the file system permissions used for any created or updated
// golden files written to.
//
// This is very similar to Set(), but it allows multiple different golden files
// to be used within the same one *testing.T instance.
func SetP(t TestingT, name string, data []byte) {
// When this option is not provided, the default value is 0o644.
func WithFileMode(mode os.FileMode) Option {
return optionFunc(func(g *golden) {
g.fileMode = mode
})
}
// WithSuffix sets the filename suffix used for all golden files.
//
// When this option is not provided, the default value is ".golden".
func WithSuffix(suffix string) Option {
return optionFunc(func(g *golden) {
g.suffix = suffix
})
}
// WithDirname sets the name of the top-level directory used to hold golden
// files.
//
// When this option is not provided, the default value is "testdata".
func WithDirname(name string) Option {
return optionFunc(func(g *golden) {
g.dirname = name
})
}
// WithUpdateFunc sets the function used to determine if golden files should be
// updated or not. Essentially the provided UpdateFunc is called by Update().
//
// When this option is not provided, the default value is EnvUpdateFunc.
func WithUpdateFunc(fn UpdateFunc) Option {
return optionFunc(func(g *golden) {
g.updateFunc = fn
})
}
// WithFs sets s afero.Fs instance which is used to read/write all golden files.
//
// When this option is not provided, the default value is afero.NewOsFs().
func WithFs(fs afero.Fs) Option {
return optionFunc(func(g *golden) {
g.fs = fs
})
}
// WithSilentWrites silences the "golden: writing [...]" log messages whenever
// set functions write a golden file to disk.
func WithSilentWrites() Option {
return optionFunc(func(g *golden) {
g.logOnWrite = false
})
}
// golden is the underlying struct that implements the Golden interface.
type golden struct {
// dirMode determines the file system permissions of any folders created to
// hold golden files.
dirMode os.FileMode
// fileMode determines the file system permissions of any created or updated
// golden files written to disk.
fileMode os.FileMode
// suffix determines the filename suffix for all golden files. Typically
// this should be ".golden", but can be changed here if needed.
suffix string
// dirname is the name of the top-level directory at the root of the package
// which holds all golden files. Typically this should be "testdata", but
// can be changed here if needed.
dirname string
// updateFunc is used to determine if golden files should be updated or
// not. Its boolean return value is returned by Update().
updateFunc UpdateFunc
// fs is used for all file system operations. This enables providing custom
// afero.fs instances which can be useful for testing purposes.
fs afero.Fs
// logOnWrite determines if a message is logged with t.Logf when a golden
// file is written to with either of the set methods.
logOnWrite bool
}
// Ensure golden satisfies Golden interface.
var _ Golden = &golden{}
func (s *golden) File(t TestingT) string {
t.Helper()
DefaultGolden.SetP(t, name, data)
return s.file(t, "")
}
// Update returns true when golden is set to update golden files. Should be used
// to determine if golden.Set() or golden.SetP() should be called or not.
//
// Default behavior uses EnvUpdateFunc() to check if the "GOLDEN_UPDATE"
// environment variable is set to a truthy value. To customize create a custom
// *Golden instance with New() and set a new UpdateFunc value.
func Update() bool {
return DefaultGolden.Update()
func (s *golden) Get(t TestingT) []byte {
t.Helper()
return s.get(t, "")
}
func (s *golden) Set(t TestingT, data []byte) {
t.Helper()
s.set(t, "", data)
}
func (s *golden) FileP(t TestingT, name string) string {
t.Helper()
if name == "" {
t.Fatalf("golden: test name cannot be empty")
}
return s.file(t, name)
}
func (s *golden) GetP(t TestingT, name string) []byte {
t.Helper()
if name == "" {
t.Fatal("golden: name cannot be empty")
}
return s.get(t, name)
}
func (s *golden) SetP(t TestingT, name string, data []byte) {
t.Helper()
if name == "" {
t.Fatal("golden: name cannot be empty")
}
s.set(t, name, data)
}
func (s *golden) file(t TestingT, name string) string {
t.Helper()
if t.Name() == "" {
t.Fatalf(
"golden: could not determine filename for given %T instance", t,
)
}
base := []string{s.dirname, filepath.FromSlash(t.Name())}
if name != "" {
base = append(base, name)
}
f := filepath.Clean(filepath.Join(base...) + s.suffix)
dirty := strings.Split(f, string(os.PathSeparator))
clean := make([]string, 0, len(dirty))
for _, s := range dirty {
clean = append(clean, sanitize.Filename(s))
}
return strings.Join(clean, string(os.PathSeparator))
}
func (s *golden) get(t TestingT, name string) []byte {
t.Helper()
f := s.file(t, name)
b, err := afero.ReadFile(s.fs, f)
if err != nil {
t.Fatalf("golden: %s", err.Error())
}
return b
}
func (s *golden) set(t TestingT, name string, data []byte) {
t.Helper()
f := s.file(t, name)
dir := filepath.Dir(f)
if s.logOnWrite {
t.Logf("golden: writing golden file: %s", f)
}
err := s.fs.MkdirAll(dir, s.dirMode)
if err != nil {
t.Fatalf("golden: failed to create directory: %s", err.Error())
}
err = afero.WriteFile(s.fs, f, data, s.fileMode)
if err != nil {
t.Fatalf("golden: filed to write file: %s", err.Error())
}
}
func (s *golden) Update() bool {
return s.updateFunc()
}

View File

@@ -16,7 +16,8 @@ type MyStruct struct {
// TestExampleMyStruct reads/writes the following golden file:
//
// testdata/TestExampleMyStruct.golden
// testdata/TestExampleMyStruct.golden
//
func TestExampleMyStruct(t *testing.T) {
got, err := json.Marshal(&MyStruct{Foo: "Bar"})
require.NoError(t, err)
@@ -31,8 +32,9 @@ func TestExampleMyStruct(t *testing.T) {
// TestExampleMyStructTabular reads/writes the following golden files:
//
// testdata/TestExampleMyStructTabular/empty_struct.golden
// testdata/TestExampleMyStructTabular/full_struct.golden
// testdata/TestExampleMyStructTabular/empty_struct.golden
// testdata/TestExampleMyStructTabular/full_struct.golden
//
func TestExampleMyStructTabular(t *testing.T) {
tests := []struct {
name string
@@ -58,8 +60,9 @@ func TestExampleMyStructTabular(t *testing.T) {
// TestExampleMyStructP reads/writes the following golden file:
//
// testdata/TestExampleMyStructP/json.golden
// testdata/TestExampleMyStructP/xml.golden
// testdata/TestExampleMyStructP/json.golden
// testdata/TestExampleMyStructP/xml.golden
//
func TestExampleMyStructP(t *testing.T) {
gotJSON, _ := json.Marshal(&MyStruct{Foo: "Bar"})
gotXML, _ := xml.Marshal(&MyStruct{Foo: "Bar"})
@@ -75,10 +78,11 @@ func TestExampleMyStructP(t *testing.T) {
// TestExampleMyStructTabularP reads/writes the following golden file:
//
// testdata/TestExampleMyStructTabularP/empty_struct/json.golden
// testdata/TestExampleMyStructTabularP/empty_struct/xml.golden
// testdata/TestExampleMyStructTabularP/full_struct/json.golden
// testdata/TestExampleMyStructTabularP/full_struct/xml.golden
// testdata/TestExampleMyStructTabularP/empty_struct/json.golden
// testdata/TestExampleMyStructTabularP/empty_struct/xml.golden
// testdata/TestExampleMyStructTabularP/full_struct/json.golden
// testdata/TestExampleMyStructTabularP/full_struct/xml.golden
//
func TestExampleMyStructTabularP(t *testing.T) {
tests := []struct {
name string

File diff suppressed because it is too large Load Diff

45
marshal/marshal.go Normal file
View File

@@ -0,0 +1,45 @@
package marshal
import (
"bytes"
"encoding/json"
"encoding/xml"
"gopkg.in/yaml.v3"
)
// JSON returns the JSON encoding of v. Returned JSON is intended by two spaces
// (pretty formatted), and is not HTML escaped.
func JSON(v interface{}) ([]byte, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetIndent("", " ")
enc.SetEscapeHTML(false)
err := enc.Encode(v)
return buf.Bytes(), err
}
// XML returns the XML encoding of v. Returned XML is intended by two spaces
// (pretty formatted).
func XML(v interface{}) ([]byte, error) {
var buf bytes.Buffer
enc := xml.NewEncoder(&buf)
enc.Indent("", " ")
err := enc.Encode(v)
return buf.Bytes(), err
}
// YAML returns the YAML encoding of v. Returned YAML is intended by two spaces.
func YAML(v interface{}) ([]byte, error) {
var buf bytes.Buffer
enc := yaml.NewEncoder(&buf)
enc.SetIndent(2)
err := enc.Encode(v)
return buf.Bytes(), err
}

318
marshal/marshal_test.go Normal file
View File

@@ -0,0 +1,318 @@
package marshal_test
import (
"testing"
"github.com/jimeh/go-golden/marshal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type book struct {
Title string `json:"title" yaml:"title" xml:"title"`
Author string `json:"author,omitempty" yaml:"author,omitempty" xml:"author,omitempty"`
Price int `json:"price" yaml:"price" xml:"price"`
}
type shoe struct {
Make string `json:"make" yaml:"make" xml:"make"`
Model string `json:"model,omitempty" yaml:"model,omitempty" xml:"model,omitempty"`
Size int `json:"size" yaml:"size" xml:"size"`
}
func TestJSON(t *testing.T) {
type args struct {
v interface{}
}
tests := []struct {
name string
args args
want []byte
wantErr string
wantErrIs error
}{
{
name: "nil",
args: args{v: nil},
want: []byte("null\n"),
},
{
name: "empty struct (1)",
args: args{v: &book{}},
want: []byte(`{
"title": "",
"price": 0
}
`,
),
},
{
name: "empty struct (2)",
args: args{v: &shoe{}},
want: []byte(`{
"make": "",
"size": 0
}
`,
),
},
{
name: "full struct (1)",
args: args{
v: &book{
Title: "a",
Author: "b",
Price: 499,
},
},
want: []byte(`{
"title": "a",
"author": "b",
"price": 499
}
`,
),
},
{
name: "empty struct (2)",
args: args{
v: &shoe{
Make: "a",
Model: "b",
Size: 42,
},
},
want: []byte(`{
"make": "a",
"model": "b",
"size": 42
}
`,
),
},
{
name: "channel",
args: args{
v: make(chan int),
},
wantErr: "json: unsupported type: chan int",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := marshal.JSON(tt.args.v)
if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr)
}
if tt.wantErrIs != nil {
assert.ErrorIs(t, err, tt.wantErrIs)
}
if tt.wantErr == "" && tt.wantErrIs == nil {
require.NoError(t, err)
}
assert.Equal(t, string(tt.want), string(got))
})
}
}
func TestYAML(t *testing.T) {
type args struct {
v interface{}
}
tests := []struct {
name string
args args
want []byte
wantErr string
wantErrIs error
wantPanic interface{}
}{
{
name: "nil",
args: args{v: nil},
want: []byte("null\n"),
},
{
name: "empty struct (1)",
args: args{v: &book{}},
want: []byte(`title: ""
price: 0
`,
),
},
{
name: "empty struct (2)",
args: args{v: &shoe{}},
want: []byte(`make: ""
size: 0
`,
),
},
{
name: "full struct (1)",
args: args{
v: &book{
Title: "a",
Author: "b",
Price: 499,
},
},
want: []byte(`title: a
author: b
price: 499
`,
),
},
{
name: "empty struct (2)",
args: args{
v: &shoe{
Make: "a",
Model: "b",
Size: 42,
},
},
want: []byte(`make: a
model: b
size: 42
`,
),
},
{
name: "channel",
args: args{
v: make(chan int),
},
wantPanic: "cannot marshal type: chan int",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := func() (got []byte, err error, p interface{}) {
defer func() { p = recover() }()
got, err = marshal.YAML(tt.args.v)
return
}
got, err, p := f()
if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr)
}
if tt.wantErrIs != nil {
assert.ErrorIs(t, err, tt.wantErrIs)
}
if tt.wantPanic != nil {
assert.Equal(t, tt.wantPanic, p)
}
if tt.wantErr == "" && tt.wantErrIs == nil {
require.NoError(t, err)
}
assert.Equal(t, string(tt.want), string(got))
})
}
}
func TestXML(t *testing.T) {
type args struct {
v interface{}
}
tests := []struct {
name string
args args
want []byte
wantErr string
wantErrIs error
}{
{
name: "nil",
args: args{v: nil},
want: []byte(""),
},
{
name: "empty struct (1)",
args: args{v: &book{}},
want: []byte(`<book>
<title></title>
<price>0</price>
</book>`,
),
},
{
name: "empty struct (2)",
args: args{v: &shoe{}},
want: []byte(`<shoe>
<make></make>
<size>0</size>
</shoe>`,
),
},
{
name: "full struct (1)",
args: args{
v: &book{
Title: "a",
Author: "b",
Price: 499,
},
},
want: []byte(`<book>
<title>a</title>
<author>b</author>
<price>499</price>
</book>`,
),
},
{
name: "empty struct (2)",
args: args{
v: &shoe{
Make: "a",
Model: "b",
Size: 42,
},
},
want: []byte(`<shoe>
<make>a</make>
<model>b</model>
<size>42</size>
</shoe>`,
),
},
{
name: "channel",
args: args{
v: make(chan int),
},
wantErr: "xml: unsupported type: chan int",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := marshal.XML(tt.args.v)
if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr)
}
if tt.wantErrIs != nil {
assert.ErrorIs(t, err, tt.wantErrIs)
}
if tt.wantErr == "" && tt.wantErrIs == nil {
require.NoError(t, err)
}
assert.Equal(t, string(tt.want), string(got))
})
}
}

133
marshaling_asserter.go Normal file
View File

@@ -0,0 +1,133 @@
package golden
import (
"reflect"
"strings"
"github.com/jimeh/go-golden/sanitize"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type (
MarshalFunc func(interface{}) ([]byte, error)
UnmarshalFunc func([]byte, interface{}) error
)
// MarshalingAsserter allows building marshaling asserters by providing
// functions which marshal and unmarshal objects.
type MarshalingAsserter struct {
// Golden is the *Golden instance used to read/write golden files.
Golden Golden
// Name of the format the MarshalAsserter handles.
Format string
// GoldName is the name of the golden file used when marshaling. This is by
// default set based on Format using NewMarshalAsserter. For example if
// Format is set to "JSON", GoldName will be set to "marshaled_json" by
// default.
GoldName string
// MarshalFunc is the function used to marshal given objects.
MarshalFunc MarshalFunc
// UnmarshalFunc is the function used to unmarshal given objects.
UnmarshalFunc UnmarshalFunc
// NormalizeLineBreaks determines if Windows' CRLF (\r\n) and MacOS Classic
// CR (\r) line breaks are replaced with Unix's LF (\n) line breaks. This
// ensures marshaling assertions work across different platforms.
NormalizeLineBreaks bool
}
// New returns a new MarshalingAsserter.
func NewMarshalingAsserter(
golden Golden,
format string,
marshalFunc MarshalFunc,
unmarshalFunc UnmarshalFunc,
normalizeLineBreaks bool,
) *MarshalingAsserter {
goldName := "marshaled_" + strings.ToLower(sanitize.Filename(format))
return &MarshalingAsserter{
Golden: golden,
Format: format,
GoldName: goldName,
MarshalFunc: marshalFunc,
UnmarshalFunc: unmarshalFunc,
NormalizeLineBreaks: normalizeLineBreaks,
}
}
// Marshaling asserts that the given "v" value marshals via the provided encoder
// to an expected value fetched from a golden file on disk, and then verifies
// that the marshaled result produces a value that is equal to "v" when
// unmarshaled.
//
// Used for objects that do NOT change when they are marshaled and unmarshaled.
func (s *MarshalingAsserter) Marshaling(t TestingT, v interface{}) {
t.Helper()
s.MarshalingP(t, v, v)
}
// MarshalingP asserts that the given "v" value marshals via the provided
// encoder to an expected value fetched from a golden file on disk, and then
// verifies that the marshaled result produces a value that is equal to "want"
// when unmarshaled.
//
// Used for objects that change when they are marshaled and unmarshaled.
func (s *MarshalingAsserter) MarshalingP(
t TestingT,
v interface{},
want interface{},
) {
t.Helper()
if reflect.ValueOf(want).Kind() != reflect.Ptr {
require.FailNowf(t,
"golden: only pointer types can be asserted",
"%T is not a pointer type", want,
)
}
marshaled, err := s.MarshalFunc(v)
require.NoErrorf(t,
err, "golden: failed to %s marshal %T: %+v", s.Format, v, v,
)
if s.NormalizeLineBreaks {
marshaled = sanitize.LineBreaks(marshaled)
}
if s.Golden.Update() {
s.Golden.SetP(t, s.GoldName, marshaled)
}
gold := s.Golden.GetP(t, s.GoldName)
if s.NormalizeLineBreaks {
gold = sanitize.LineBreaks(gold)
}
switch strings.ToLower(s.Format) {
case "json":
assert.JSONEq(t, string(gold), string(marshaled))
case "yaml", "yml":
assert.YAMLEq(t, string(gold), string(marshaled))
default:
assert.Equal(t, string(gold), string(marshaled))
}
got := reflect.New(reflect.TypeOf(want).Elem()).Interface()
err = s.UnmarshalFunc(gold, got)
f := s.Golden.FileP(t, s.GoldName)
require.NoErrorf(t, err,
"golden: failed to %s unmarshal %T from %s", s.Format, got, f,
)
assert.Equalf(t, want, got,
"golden: unmarshaling from golden file does not match "+
"expected object; golden file: %s", f,
)
}

View File

@@ -0,0 +1,98 @@
package golden
import (
"testing"
"github.com/jimeh/go-golden/marshal"
"github.com/jimeh/go-golden/unmarshal"
"github.com/stretchr/testify/assert"
)
func TestNewMarshalingAsserter(t *testing.T) {
type args struct {
golden Golden
format string
marshalFunc MarshalFunc
unmarshalFunc UnmarshalFunc
normalizeLineBreaks bool
}
tests := []struct {
name string
args args
want *MarshalingAsserter
}{
{
name: "json",
args: args{
nil,
"JSON",
marshal.JSON,
unmarshal.JSON,
true,
},
want: &MarshalingAsserter{
Golden: nil,
Format: "JSON",
GoldName: "marshaled_json",
MarshalFunc: marshal.JSON,
UnmarshalFunc: unmarshal.JSON,
NormalizeLineBreaks: true,
},
},
{
name: "xml",
args: args{
nil,
"XML",
marshal.XML,
unmarshal.XML,
true,
},
want: &MarshalingAsserter{
Golden: nil,
Format: "XML",
GoldName: "marshaled_xml",
MarshalFunc: marshal.XML,
UnmarshalFunc: unmarshal.XML,
NormalizeLineBreaks: true,
},
},
{
name: "yaml",
args: args{
nil,
"YAML",
marshal.YAML,
unmarshal.YAML,
true,
},
want: &MarshalingAsserter{
Golden: nil,
Format: "YAML",
GoldName: "marshaled_yaml",
MarshalFunc: marshal.YAML,
UnmarshalFunc: unmarshal.YAML,
NormalizeLineBreaks: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewMarshalingAsserter(
tt.args.golden,
tt.args.format,
tt.args.marshalFunc,
tt.args.unmarshalFunc,
tt.args.normalizeLineBreaks,
)
assert.Equal(t, tt.want.Golden, got.Golden)
assert.Equal(t, tt.want.Format, got.Format)
assert.Equal(t, tt.want.GoldName, got.GoldName)
assert.Equal(t,
tt.want.NormalizeLineBreaks,
got.NormalizeLineBreaks,
)
})
}
}

View File

@@ -1,80 +0,0 @@
package golden
import "os"
type Option interface {
apply(*gold)
}
type optionFunc func(*gold)
func (fn optionFunc) apply(g *gold) {
fn(g)
}
// WithDirMode sets the file system permissions used for any folders created to
// hold golden files.
//
// When this option is not provided, the default value is 0o755.
func WithDirMode(mode os.FileMode) Option {
return optionFunc(func(g *gold) {
g.dirMode = mode
})
}
// WithFileMode sets the file system permissions used for any created or updated
// golden files written to.
//
// When this option is not provided, the default value is 0o644.
func WithFileMode(mode os.FileMode) Option {
return optionFunc(func(g *gold) {
g.fileMode = mode
})
}
// WithSuffix sets the filename suffix used for all golden files.
//
// When this option is not provided, the default value is ".golden".
func WithSuffix(suffix string) Option {
return optionFunc(func(g *gold) {
g.suffix = suffix
})
}
// WithDirname sets the name of the top-level directory used to hold golden
// files.
//
// When this option is not provided, the default value is "testdata".
func WithDirname(name string) Option {
return optionFunc(func(g *gold) {
g.dirname = name
})
}
// WithUpdateFunc sets the function used to determine if golden files should be
// updated or not. Essentially the provided UpdateFunc is called by Update().
//
// When this option is not provided, the default value is EnvUpdateFunc.
func WithUpdateFunc(fn UpdateFunc) Option {
return optionFunc(func(g *gold) {
g.updateFunc = fn
})
}
// WithFS sets the afero.Fs instance which is used for all file system
// operations to read/write golden files.
//
// When this option is not provided, the default value is afero.NewOsFs().
func WithFS(fs FS) Option {
return optionFunc(func(g *gold) {
g.fs = fs
})
}
// WithSilentWrites silences the "golden: writing [...]" log messages whenever
// set functions write a golden file to disk.
func WithSilentWrites() Option {
return optionFunc(func(g *gold) {
g.logOnWrite = false
})
}

View File

@@ -1,275 +0,0 @@
package testfs
import (
"errors"
"os"
"path"
"strings"
)
type Node struct {
data []byte
perm os.FileMode
isDir bool
}
type FS struct {
Pwd string
Nodes map[string]*Node
}
func New() *FS {
return &FS{
Pwd: "/root",
Nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/root": {perm: 0o700, isDir: true},
},
}
}
func (fs *FS) MkdirAll(name string, perm os.FileMode) error {
if !path.IsAbs(name) && name != "" {
name = path.Join(fs.Pwd, name)
}
dirs := []string{name}
for d := path.Dir(name); d != "/"; d = path.Dir(d) {
dirs = append(dirs, d)
}
dirs = append(dirs, "/")
for i := len(dirs) - 1; i >= 0; i-- {
dir := dirs[i]
parent := path.Dir(dir)
if info, ok := fs.Nodes[dir]; ok {
if !info.isDir {
return &os.PathError{
Op: "mkdir",
Path: dir,
Err: errors.New("not a directory"),
}
}
continue
}
parentInfo, ok := fs.Nodes[parent]
if !ok {
return &os.PathError{
Op: "mkdir",
Path: parent,
Err: errors.New("no such file or directory"),
}
}
if !parentInfo.isDir {
return &os.PathError{
Op: "mkdir",
Path: parent,
Err: errors.New("not a directory"),
}
}
// Ensure all parent directories have execute permissions, and direct
// parent also has write permission.
if parentInfo.perm&0o100 == 0 || i == 1 && parentInfo.perm&0o200 == 0 {
return &os.PathError{
Op: "mkdir",
Path: dir,
Err: errors.New("permission denied"),
}
}
fs.Nodes[dir] = &Node{perm: perm, isDir: true}
}
return nil
}
func (fs *FS) ReadFile(name string) ([]byte, error) {
if !path.IsAbs(name) && name != "" {
name = path.Join(fs.Pwd, name)
}
_, err := fs.checkParents(name, false)
if err != nil {
return nil, err
}
info, ok := fs.Nodes[name]
if !ok {
return nil, &os.PathError{
Op: "open",
Path: name,
Err: errors.New("no such file or directory"),
}
}
if info.isDir {
return nil, &os.PathError{
Op: "open",
Path: name,
Err: errors.New("is a directory"),
}
}
if info.perm&0o400 == 0 {
return nil, &os.PathError{
Op: "open",
Path: name,
Err: errors.New("permission denied"),
}
}
return info.data, nil
}
func (fs *FS) WriteFile(name string, data []byte, perm os.FileMode) error {
if !path.IsAbs(name) && name != "" {
name = path.Join(fs.Pwd, name)
}
parent, err := fs.checkParents(name, true)
if err != nil {
return err
}
info, ok := fs.Nodes[name]
if ok {
if info.isDir {
return &os.PathError{
Op: "open",
Path: name,
Err: errors.New("is a directory"),
}
}
}
// Return error if file exists and has no write permission, or if the file
// does not exist and the direct parent has no write permission.
if ok && info.perm&0o200 == 0 || !ok && parent.perm&0o200 == 0 {
return &os.PathError{
Op: "open",
Path: name,
Err: errors.New("permission denied"),
}
}
fs.Nodes[name] = &Node{data: data, perm: perm}
return nil
}
func (fs *FS) Remove(name string) error {
if !path.IsAbs(name) && name != "" {
name = path.Join(fs.Pwd, name)
}
parent, err := fs.checkParents(name, false)
if err != nil {
return err
}
if parent != nil && parent.perm&0o200 == 0 {
return &os.PathError{
Op: "remove",
Path: name,
Err: errors.New("permission denied"),
}
}
info, ok := fs.Nodes[name]
if !ok {
return &os.PathError{
Op: "remove",
Path: name,
Err: errors.New("no such file or directory"),
}
}
if info.perm&0o200 == 0 {
return &os.PathError{
Op: "remove",
Path: name,
Err: errors.New("permission denied"),
}
}
if info.isDir {
for p := range fs.Nodes {
if strings.HasPrefix(p, name) && p != name {
return &os.PathError{
Op: "remove",
Path: name,
Err: errors.New("directory not empty"),
}
}
}
}
delete(fs.Nodes, name)
return nil
}
func (fs *FS) Exists(name string) bool {
if !path.IsAbs(name) && name != "" {
name = path.Join(fs.Pwd, name)
}
_, ok := fs.Nodes[name]
return ok
}
func (fs *FS) FileMode(name string) (os.FileMode, error) {
if !path.IsAbs(name) && name != "" {
name = path.Join(fs.Pwd, name)
}
if info, ok := fs.Nodes[name]; ok {
return info.perm, nil
}
return 0, &os.PathError{
Op: "open",
Path: name,
Err: os.ErrNotExist,
}
}
func (fs *FS) checkParents(absPath string, noExistError bool) (*Node, error) {
var parents []string
for d := path.Dir(absPath); d != "/"; d = path.Dir(d) {
parents = append(parents, d)
}
parents = append(parents, "/")
var directParent *Node
for i := 0; i < len(parents); i++ {
dir := parents[i]
info, ok := fs.Nodes[dir]
if !ok && noExistError {
return nil, &os.PathError{
Op: "open",
Path: dir,
Err: errors.New("no such file or directory"),
}
}
if info != nil && !info.isDir {
return nil, &os.PathError{
Op: "open",
Path: dir,
Err: errors.New("not a directory"),
}
}
// Ensure all parent directories have execute permissions.
if info != nil && info.perm&0o100 == 0 {
return nil, &os.PathError{
Op: "open",
Path: dir,
Err: errors.New("permission denied"),
}
}
if i == 0 {
directParent = info
}
}
return directParent, nil
}

View File

@@ -1,670 +0,0 @@
package testfs
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFSMkdirAll(t *testing.T) {
type args struct {
path string
perm os.FileMode
}
tests := []struct {
name string
args args
nodes map[string]*Node
want map[string]*Node
wantErr bool
}{
{
name: "create relative new dir",
args: args{path: "newdir", perm: 0o755},
want: map[string]*Node{
"/root/newdir": {perm: 0o755, isDir: true},
},
},
{
name: "create absolute new dir",
args: args{path: "/opt/newdir", perm: 0o755},
want: map[string]*Node{
"/opt": {perm: 0o755, isDir: true},
"/opt/newdir": {perm: 0o755, isDir: true},
},
},
{
name: "create relative nested dirs",
args: args{path: "nested/dir/structure", perm: 0o755},
want: map[string]*Node{
"/root/nested": {perm: 0o755, isDir: true},
"/root/nested/dir": {perm: 0o755, isDir: true},
"/root/nested/dir/structure": {perm: 0o755, isDir: true},
},
},
{
name: "create absolute nested dirs",
args: args{path: "/opt/nested/dir/structure", perm: 0o755},
want: map[string]*Node{
"/opt": {perm: 0o755, isDir: true},
"/opt/nested": {perm: 0o755, isDir: true},
"/opt/nested/dir": {perm: 0o755, isDir: true},
"/opt/nested/dir/structure": {perm: 0o755, isDir: true},
},
},
{
name: "create relative nested dirs with other perms",
args: args{path: "nested/dir/structure", perm: 0o750},
want: map[string]*Node{
"/root/nested": {perm: 0o750, isDir: true},
"/root/nested/dir": {perm: 0o750, isDir: true},
"/root/nested/dir/structure": {perm: 0o750, isDir: true},
},
},
{
name: "create absolute nested dirs with other perms",
args: args{path: "/opt/nested/dir/structure", perm: 0o750},
want: map[string]*Node{
"/opt": {perm: 0o750, isDir: true},
"/opt/nested": {perm: 0o750, isDir: true},
"/opt/nested/dir": {perm: 0o750, isDir: true},
"/opt/nested/dir/structure": {perm: 0o750, isDir: true},
},
},
{
name: "create relative nested dirs with existing dirs",
args: args{path: "nested/dir/structure", perm: 0o755},
want: map[string]*Node{
"/root/nested": {perm: 0o755, isDir: true},
"/root/nested/dir": {perm: 0o755, isDir: true},
"/root/nested/dir/structure": {perm: 0o755, isDir: true},
},
},
{
name: "create absolute nested dirs with existing dirs",
args: args{path: "/root/nested/dir/structure", perm: 0o755},
want: map[string]*Node{
"/root/nested": {perm: 0o755, isDir: true},
"/root/nested/dir": {perm: 0o755, isDir: true},
"/root/nested/dir/structure": {perm: 0o755, isDir: true},
},
},
{
name: "create relative under file",
args: args{path: "file/newdir", perm: 0o755},
nodes: map[string]*Node{
"/root/file": {perm: 0o644},
},
wantErr: true,
},
{
name: "create absolute under file",
args: args{path: "/root/file/newdir", perm: 0o755},
nodes: map[string]*Node{
"/root/file": {perm: 0o644},
},
wantErr: true,
},
{
name: "create relative directory without execute permission",
args: args{path: "dir/newdir", perm: 0o755},
nodes: map[string]*Node{
"/root": {perm: 0o644},
},
wantErr: true,
},
{
name: "create absolute directory without execute permission",
args: args{path: "/root/dir/newdir", perm: 0o755},
nodes: map[string]*Node{
"/root": {perm: 0o644},
},
wantErr: true,
},
{
name: "create relative directory without write permission",
args: args{path: "dir/newdir", perm: 0o755},
nodes: map[string]*Node{
"/root": {perm: 0o444},
},
wantErr: true,
},
{
name: "create absolute directory without write permission",
args: args{path: "/root/dir/newdir", perm: 0o755},
nodes: map[string]*Node{
"/root": {perm: 0o444},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fs := &FS{
Pwd: "/root",
Nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/root": {perm: 0o700, isDir: true},
},
}
for fp, info := range tt.nodes {
fs.Nodes[fp] = info
}
err := fs.MkdirAll(tt.args.path, tt.args.perm)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
for fp, info := range tt.want {
got := fs.Nodes[fp]
assert.Equal(t, info, got, "path: %s", fp)
}
}
})
}
}
func TestFSReadFile(t *testing.T) {
type args struct {
name string
}
tests := []struct {
name string
args args
nodes map[string]*Node
want []byte
wantErr bool
}{
{
name: "relative read existing file",
args: args{name: "file.txt"},
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/root": {perm: 0o755, isDir: true},
"/root/file.txt": {data: []byte("file content"), perm: 0o644},
},
want: []byte("file content"),
},
{
name: "absolute read existing file",
args: args{name: "/opt/file.txt"},
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/opt": {perm: 0o755, isDir: true},
"/opt/file.txt": {data: []byte("file content"), perm: 0o644},
},
want: []byte("file content"),
},
{
name: "relative file does not exist",
args: args{name: "nonexistent.txt"},
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/root": {perm: 0o755, isDir: true},
},
wantErr: true,
},
{
name: "absolute file does not exist",
args: args{name: "/opt/nonexistent.txt"},
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/opt": {perm: 0o755, isDir: true},
},
wantErr: true,
},
{
name: "relative file is a directory",
args: args{name: "dir"},
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/root": {perm: 0o755, isDir: true},
"/root/dir": {perm: 0o755, isDir: true},
},
wantErr: true,
},
{
name: "absolute file is a directory",
args: args{name: "/opt/dir"},
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/opt": {perm: 0o755, isDir: true},
"/opt/dir": {perm: 0o755, isDir: true},
},
wantErr: true,
},
{
name: "relative file permission denied",
args: args{name: "file.txt"},
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/root": {perm: 0o755, isDir: true},
"/root/file.txt": {data: []byte("file content"), perm: 0o200},
},
wantErr: true,
},
{
name: "relative no directory read permission",
args: args{name: "file.txt"},
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/root": {perm: 0o355, isDir: true},
"/root/file.txt": {data: []byte("file content"), perm: 0o644},
},
want: []byte("file content"),
},
{
name: "relative no directory execute permission",
args: args{name: "file.txt"},
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/root": {perm: 0o655, isDir: true},
"/root/file.txt": {data: []byte("file content"), perm: 0o200},
},
wantErr: true,
},
{
name: "relative no grandparent directory execute permission",
args: args{name: "foo/file.txt"},
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/root": {perm: 0o655, isDir: true},
"/root/foo": {perm: 0o755, isDir: true},
"/root/foo/file.txt": {data: []byte("hello"), perm: 0o200},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fs := &FS{
Pwd: "/root",
Nodes: tt.nodes,
}
got, err := fs.ReadFile(tt.args.name)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
}
})
}
}
func TestFSWriteFile(t *testing.T) {
type args struct {
name string
data []byte
perm os.FileMode
}
tests := []struct {
name string
args args
nodes map[string]*Node
wantPath string
wantErr bool
}{
{
name: "relative write to new file",
args: args{
name: "newfile.txt",
data: []byte("new content"),
perm: 0o644,
},
wantPath: "/tmp/newfile.txt",
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/tmp": {perm: 0o755, isDir: true},
},
},
{
name: "absolute write to new file",
args: args{
name: "/opt/newfile.txt",
data: []byte("new content"),
perm: 0o644,
},
wantPath: "/opt/newfile.txt",
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/opt": {perm: 0o755, isDir: true},
},
},
{
name: "relative overwrite existing file",
args: args{
name: "existing.txt",
data: []byte("overwritten"),
perm: 0o644,
},
wantPath: "/tmp/existing.txt",
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/tmp": {perm: 0o755, isDir: true},
"/tmp/existing": {data: []byte("existing"), perm: 0o644},
},
},
{
name: "absolute overwrite existing file",
args: args{
name: "/opt/existing.txt",
data: []byte("overwritten"),
perm: 0o644,
},
wantPath: "/opt/existing.txt",
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/opt": {perm: 0o755, isDir: true},
"/opt/existing": {data: []byte("existing"), perm: 0o644},
},
},
{
name: "relative overwrite file permissions denied",
args: args{
name: "existing.txt",
data: []byte("overwritten"),
perm: 0o644,
},
wantPath: "/tmp/existing.txt",
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/tmp": {perm: 0o755, isDir: true},
"/tmp/existing.txt": {data: []byte("existing"), perm: 0o400},
},
wantErr: true,
},
{
name: "absolute overwrite file permissions denied",
args: args{
name: "/opt/existing.txt",
data: []byte("overwritten"),
perm: 0o644,
},
wantPath: "/opt/existing.txt",
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/opt": {perm: 0o755, isDir: true},
"/opt/existing.txt": {data: []byte("existing"), perm: 0o400},
},
wantErr: true,
},
{
name: "relative overwrite directory",
args: args{
name: "dir",
data: []byte("overwritten"),
perm: 0o644,
},
wantPath: "/tmp/dir",
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/tmp": {perm: 0o755, isDir: true},
"/tmp/dir": {perm: 0o644, isDir: true},
},
wantErr: true,
},
{
name: "absolute overwrite directory",
args: args{
name: "/opt/dir",
data: []byte("overwritten"),
perm: 0o644,
},
wantPath: "/opt/dir",
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/opt": {perm: 0o755, isDir: true},
"/opt/dir": {perm: 0o644, isDir: true},
},
wantErr: true,
},
{
name: "relative write to non-existent directory",
args: args{
name: "nonexistentdir/newfile.txt",
data: []byte("this will fail"),
perm: 0o644,
},
wantPath: "/tmp/nonexistentdir/newfile.txt",
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/tmp": {perm: 0o755, isDir: true},
},
wantErr: true,
},
{
name: "absolute write to non-existent directory",
args: args{
name: "/opt/nonexistentdir/newfile.txt",
data: []byte("this will fail"),
perm: 0o644,
},
wantPath: "/opt/nonexistentdir/newfile.txt",
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/opt": {perm: 0o755, isDir: true},
},
wantErr: true,
},
{
name: "relative write parent directory is a file",
args: args{
name: "file/newfile.txt",
data: []byte("this will fail"),
perm: 0o644,
},
wantPath: "/tmp/file/newfile.txt",
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/tmp": {perm: 0o755, isDir: true},
"/tmp/file": {data: []byte("file content"), perm: 0o644},
},
wantErr: true,
},
{
name: "relative no parent directory write permission denied",
args: args{
name: "dir/newfile.txt",
data: []byte("this will fail"),
perm: 0o644,
},
wantPath: "/tmp/dir/newfile.txt",
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/tmp": {perm: 0o755, isDir: true},
"/tmp/dir": {perm: 0o500, isDir: true},
},
wantErr: true,
},
{
name: "relative no parent directory execute permission denied",
args: args{
name: "dir/newfile.txt",
data: []byte("this will fail"),
perm: 0o644,
},
wantPath: "/tmp/dir/newfile.txt",
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/tmp": {perm: 0o755, isDir: true},
"/tmp/dir": {perm: 0o600, isDir: true},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fs := &FS{
Pwd: "/tmp",
Nodes: tt.nodes,
}
err := fs.WriteFile(tt.args.name, tt.args.data, tt.args.perm)
if tt.wantErr {
assert.Error(t, err)
if _, ok := tt.nodes[tt.wantPath]; ok {
assert.Equal(t,
tt.nodes[tt.wantPath],
fs.Nodes[tt.wantPath],
)
} else {
assert.NotContains(t, fs.Nodes, tt.wantPath)
}
} else {
assert.NoError(t, err)
got := fs.Nodes[tt.wantPath]
assert.Equal(t, tt.args.data, got.data)
assert.Equal(t, tt.args.perm, got.perm)
assert.Equal(t, false, got.isDir)
}
})
}
}
func TestFSRemove(t *testing.T) {
type args struct {
name string
}
tests := []struct {
name string
args args
nodes map[string]*Node
wantErr bool
}{
{
name: "relative remove existing file",
args: args{name: "file.txt"},
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/root": {perm: 0o755, isDir: true},
"/root/file.txt": {data: []byte("file content"), perm: 0o644},
},
},
{
name: "absolute remove existing file",
args: args{name: "/opt/file.txt"},
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/opt": {perm: 0o755, isDir: true},
"/opt/file.txt": {data: []byte("file content"), perm: 0o644},
},
},
{
name: "relative file does not exist",
args: args{name: "nonexistent.txt"},
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/root": {perm: 0o755, isDir: true},
},
wantErr: true,
},
{
name: "absolute file does not exist",
args: args{name: "/opt/nonexistent.txt"},
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/opt": {perm: 0o755, isDir: true},
},
wantErr: true,
},
{
name: "relative file is a directory",
args: args{name: "dir"},
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/root": {perm: 0o755, isDir: true},
"/root/dir": {perm: 0o755, isDir: true},
},
},
{
name: "absolute file is a directory",
args: args{name: "/opt/dir"},
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/opt": {perm: 0o755, isDir: true},
"/opt/dir": {perm: 0o755, isDir: true},
},
},
{
name: "relative file permission denied",
args: args{name: "file.txt"},
nodes: map[string]*Node{
"/root": {perm: 0o755, isDir: true},
"/root/file.txt": {data: []byte("file content"), perm: 0o400},
},
wantErr: true,
},
{
name: "absolute file permission denied",
args: args{name: "/opt/file.txt"},
nodes: map[string]*Node{
"/": {perm: 0o755, isDir: true},
"/opt": {perm: 0o755, isDir: true},
"/opt/file.txt": {data: []byte("file content"), perm: 0o400},
},
wantErr: true,
},
{
name: "relative no directory write permission",
args: args{name: "file.txt"},
nodes: map[string]*Node{
"/root": {perm: 0o555, isDir: true},
"/root/file.txt": {data: []byte("file content"), perm: 0o644},
},
wantErr: true,
},
{
name: "relative no directory execute permission",
args: args{name: "file.txt"},
nodes: map[string]*Node{
"/root": {perm: 0o655, isDir: true},
"/root/file.txt": {data: []byte("file content"), perm: 0o644},
},
wantErr: true,
},
{
name: "relative no grandparent directory execute permission",
args: args{name: "file.txt"},
nodes: map[string]*Node{
"/root": {perm: 0o655, isDir: true},
"/root/dir": {perm: 0o755, isDir: true},
"/root/dir/file.txt": {
data: []byte("file content"), perm: 0o644,
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fs := &FS{
Pwd: "/root",
Nodes: tt.nodes,
}
err := fs.Remove(tt.args.name)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,3 @@
{
"2fd5af35-b85e-4f03-8eba-524be28d7a5b": "Hello World!=Forty Two"
}

View File

@@ -0,0 +1,4 @@
{
"id": "",
"title": ""
}

View File

@@ -0,0 +1 @@
false

View File

@@ -0,0 +1,9 @@
{
"id": "cfda163c-d5c1-44a2-909b-5d2ce3a31979",
"title": "The Traveler",
"author": {
"first_name": "John",
"last_name": "Twelve Hawks"
},
"year": 2005
}

View File

@@ -0,0 +1 @@
42

View File

@@ -0,0 +1,4 @@
{
"id": "cfda163c-d5c1-44a2-909b-5d2ce3a31979",
"title": "The Traveler"
}

View File

@@ -0,0 +1 @@
"hello world"

View File

@@ -0,0 +1 @@
true

View File

@@ -0,0 +1,3 @@
{
"2fd5af35-b85e-4f03-8eba-524be28d7a5b": "Hello World!=Forty Two"
}

View File

@@ -0,0 +1,5 @@
{
"id": "",
"title": "",
"author": null
}

View File

@@ -0,0 +1 @@
false

View File

@@ -0,0 +1,9 @@
{
"id": "10eec54d-e30a-4428-be18-01095d889126",
"title": "Time Travel",
"author": {
"first_name": "Doc",
"last_name": "Brown"
},
"date": "2021-10-27T22:30:34Z"
}

View File

@@ -0,0 +1 @@
42

View File

@@ -0,0 +1,4 @@
{
"id": "10eec54d-e30a-4428-be18-01095d889126",
"title": "Time Travel"
}

View File

@@ -0,0 +1 @@
"hello world"

View File

@@ -0,0 +1 @@
true

View File

@@ -0,0 +1 @@
<comic id="2fd5af35-b85e-4f03-8eba-524be28d7a5b" issue="Forty Two">Hello World!</comic>

View File

@@ -0,0 +1,4 @@
<book>
<id></id>
<title></title>
</book>

View File

@@ -0,0 +1 @@
<bool>false</bool>

View File

@@ -0,0 +1,9 @@
<book>
<id>cfda163c-d5c1-44a2-909b-5d2ce3a31979</id>
<title>The Traveler</title>
<author>
<first_name>John</first_name>
<last_name>Twelve Hawks</last_name>
</author>
<year>2005</year>
</book>

View File

@@ -0,0 +1 @@
<int>42</int>

View File

@@ -0,0 +1,4 @@
<book>
<id>cfda163c-d5c1-44a2-909b-5d2ce3a31979</id>
<title>The Traveler</title>
</book>

View File

@@ -0,0 +1 @@
<string>hello world</string>

View File

@@ -0,0 +1 @@
<bool>true</bool>

View File

@@ -0,0 +1 @@
<comic id="2fd5af35-b85e-4f03-8eba-524be28d7a5b" issue="Forty Two">Hello World!</comic>

View File

@@ -0,0 +1,4 @@
<article>
<id></id>
<title></title>
</article>

View File

@@ -0,0 +1 @@
<bool>false</bool>

View File

@@ -0,0 +1,9 @@
<article>
<id>10eec54d-e30a-4428-be18-01095d889126</id>
<title>Time Travel</title>
<author>
<first_name>Doc</first_name>
<last_name>Brown</last_name>
</author>
<date>2021-10-27T22:30:34Z</date>
</article>

View File

@@ -0,0 +1 @@
<int>42</int>

View File

@@ -0,0 +1,4 @@
<book>
<id>10eec54d-e30a-4428-be18-01095d889126</id>
<title>Time Travel</title>
</book>

View File

@@ -0,0 +1 @@
<string>hello world</string>

View File

@@ -0,0 +1 @@
<bool>true</bool>

View File

@@ -0,0 +1,2 @@
2fd5af35-b85e-4f03-8eba-524be28d7a5b:
Hello World!: Forty Two

View File

@@ -0,0 +1,2 @@
id: ""
title: ""

View File

@@ -0,0 +1 @@
false

View File

@@ -0,0 +1,6 @@
id: cfda163c-d5c1-44a2-909b-5d2ce3a31979
title: The Traveler
author:
first_name: John
last_name: Twelve Hawks
year: 2005

View File

@@ -0,0 +1 @@
42

View File

@@ -0,0 +1,2 @@
id: cfda163c-d5c1-44a2-909b-5d2ce3a31979
title: The Traveler

View File

@@ -0,0 +1 @@
hello world

View File

@@ -0,0 +1 @@
true

View File

@@ -0,0 +1,2 @@
2fd5af35-b85e-4f03-8eba-524be28d7a5b:
Hello World!: Forty Two

View File

@@ -0,0 +1,3 @@
id: ""
title: ""
author: null

View File

@@ -0,0 +1 @@
false

View File

@@ -0,0 +1,6 @@
id: 10eec54d-e30a-4428-be18-01095d889126
title: Time Travel
author:
first_name: Doc
last_name: Brown
date: 2021-10-27T22:30:34Z

View File

@@ -0,0 +1 @@
42

View File

@@ -0,0 +1,2 @@
id: 10eec54d-e30a-4428-be18-01095d889126
title: Time Travel

View File

@@ -0,0 +1 @@
hello world

View File

@@ -0,0 +1 @@
true

View File

@@ -0,0 +1,3 @@
{
"2fd5af35-b85e-4f03-8eba-524be28d7a5b": "Hello World!=Forty Two"
}

View File

@@ -0,0 +1,4 @@
{
"id": "",
"title": ""
}

View File

@@ -0,0 +1 @@
false

View File

@@ -0,0 +1,9 @@
{
"id": "cfda163c-d5c1-44a2-909b-5d2ce3a31979",
"title": "The Traveler",
"author": {
"first_name": "John",
"last_name": "Twelve Hawks"
},
"year": 2005
}

View File

@@ -0,0 +1 @@
42

View File

@@ -0,0 +1,4 @@
{
"id": "cfda163c-d5c1-44a2-909b-5d2ce3a31979",
"title": "The Traveler"
}

View File

@@ -0,0 +1 @@
"hello world"

View File

@@ -0,0 +1 @@
true

View File

@@ -0,0 +1,3 @@
{
"2fd5af35-b85e-4f03-8eba-524be28d7a5b": "Hello World!=Forty Two"
}

View File

@@ -0,0 +1,5 @@
{
"id": "",
"title": "",
"author": null
}

View File

@@ -0,0 +1 @@
false

View File

@@ -0,0 +1,9 @@
{
"id": "10eec54d-e30a-4428-be18-01095d889126",
"title": "Time Travel",
"author": {
"first_name": "Doc",
"last_name": "Brown"
},
"date": "2021-10-27T22:30:34Z"
}

View File

@@ -0,0 +1 @@
42

View File

@@ -0,0 +1,4 @@
{
"id": "10eec54d-e30a-4428-be18-01095d889126",
"title": "Time Travel"
}

View File

@@ -0,0 +1 @@
"hello world"

View File

@@ -0,0 +1 @@
true

View File

@@ -0,0 +1 @@
<comic id="2fd5af35-b85e-4f03-8eba-524be28d7a5b" issue="Forty Two">Hello World!</comic>

View File

@@ -0,0 +1,4 @@
<book>
<id></id>
<title></title>
</book>

View File

@@ -0,0 +1 @@
<bool>false</bool>

View File

@@ -0,0 +1,9 @@
<book>
<id>cfda163c-d5c1-44a2-909b-5d2ce3a31979</id>
<title>The Traveler</title>
<author>
<first_name>John</first_name>
<last_name>Twelve Hawks</last_name>
</author>
<year>2005</year>
</book>

View File

@@ -0,0 +1 @@
<int>42</int>

View File

@@ -0,0 +1,4 @@
<book>
<id>cfda163c-d5c1-44a2-909b-5d2ce3a31979</id>
<title>The Traveler</title>
</book>

View File

@@ -0,0 +1 @@
<string>hello world</string>

View File

@@ -0,0 +1 @@
<bool>true</bool>

View File

@@ -0,0 +1 @@
<comic id="2fd5af35-b85e-4f03-8eba-524be28d7a5b" issue="Forty Two">Hello World!</comic>

View File

@@ -0,0 +1,4 @@
<article>
<id></id>
<title></title>
</article>

View File

@@ -0,0 +1 @@
<bool>false</bool>

View File

@@ -0,0 +1,9 @@
<article>
<id>10eec54d-e30a-4428-be18-01095d889126</id>
<title>Time Travel</title>
<author>
<first_name>Doc</first_name>
<last_name>Brown</last_name>
</author>
<date>2021-10-27T22:30:34Z</date>
</article>

View File

@@ -0,0 +1 @@
<int>42</int>

Some files were not shown because too many files have changed in this diff Show More