diff --git a/README.md b/README.md index 78e1efe..aba0155 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ Expectation | Type constraints | Description `is.StringContaining` | `string` | Expects the given value to be a string containing a given substring `is.StringHavingPrefix` | `string` | Expects the given value to be a string having a given prefix `is.StringHavingSuffix` | `string` | Expects the given value to be a string having a given suffix +`is.EqualToStringByLines` | `string` | Similar to EqualTo used on two strings but reports differences on a line-by-line basis ### Deep equality diff --git a/cmd/expect-migrate/README.md b/cmd/expect-migrate/README.md new file mode 100644 index 0000000..7e90c14 --- /dev/null +++ b/cmd/expect-migrate/README.md @@ -0,0 +1,58 @@ +# expect-migrate + +`expect-migrate` is a small command line tool to upgrade your go test source files +using `github.com/halimath/expect-go@v0.1.0` to `github.com/halimath/expect@latest`. The tool rewrites +go source files that in a lot of cases directly compile and run. In some cases minor work by the developer +is needed. + +# Installation + +```shell +go install github.com/halimath/expect/cmd/expect-migrate@main +``` + +# Usage + +Given an input file `some_test.go` + +```go +package some + +import ( + "testing" + . "github.com/halimath/expect-go" +) + +func TestSomething(t *testing.T) { + var err error + s := produceValue() + + EnsureThat(t, err).Is(NoError()) + ExpectThat(t, s).Is(Equal("foo")) +} +``` + +applying + +```shell +expect-migrate some_test.go +``` + +will rewrite the file to + +```go +package some + +import ( + "testing" + "github.com/halimath/expect" + "github.com/halimath/expect/is" +) + +func TestSomething(t *testing.T) { + var err error + s := produceValue() + expect.That(t, expect.FailNow(is.NoError(err))) + expect.That(t, is.EqualTo(s, "foo")) +} +``` diff --git a/cmd/expect-migrate/main.go b/cmd/expect-migrate/main.go new file mode 100644 index 0000000..cfdc804 --- /dev/null +++ b/cmd/expect-migrate/main.go @@ -0,0 +1,371 @@ +// Package main contains a cli application that rewrites go source code containing test code using +// github.com/halimath/expect-go with a dot import and matchers from v0.1.0 to the same test code using +// github.com/halimath/expect v0.3.0 with regular imports. It handles most of the matchers (for some a +// manual work is needed), handels ExpectThat and EnsureThat but does not wrap multiple expectations into +// a single call to expect.That. Thus, this tool can reduce the amount of manual work needed when upgrading +// expect to v0.3.0 but some remainings are still needed in order to have readable, maintainable test code. +// +// Usage: +// - either: cat source_test.go | expect1to3 > source_test.go +// - or: expect1to3 source_test.go +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "io" + "os" + "strconv" +) + +func main() { + var err error + + if len(os.Args) == 1 { + err = migrate("stdin.go", os.Stdin, os.Stdout) + } else { + err = migrateFile(os.Args[1]) + } + + if err != nil { + fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err) + os.Exit(1) + } +} + +func migrateFile(filename string) error { + renamedFilename := filename + "~" + if err := os.Rename(filename, renamedFilename); err != nil { + return fmt.Errorf("failed to rename input file: %v", err) + } + + in, err := os.Open(renamedFilename) + if err != nil { + return fmt.Errorf("failed to open input file: %v", err) + } + defer in.Close() + + out, err := os.Create(filename) + if err != nil { + return fmt.Errorf("failed to open output file: %v", err) + } + defer out.Close() + + if err := migrate(filename, in, out); err != nil { + return err + } + + return nil +} + +func migrate(filename string, src io.Reader, out io.Writer) error { + // Parse the source file + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, filename, src, 0) + if err != nil { + return fmt.Errorf("failed to parse source file: %v", err) + } + + // Rewrite the AST to update existing calls to ExpectThat as well as + // change the existing import. + ast.Walk(&expectationRewriteVisitor{}, file) + + // Rewrite existing import statement + ast.Inspect(file, rewriteImports) + + // Insert another import for expect/is + addImportToIsPackage(file) + + // Render the updated AST back to go source code. + if err := printer.Fprint(out, fset, file); err != nil { + return fmt.Errorf("failed to write source code: %v", err) + } + + return nil +} + +var matcherNameTranslationTable = map[string]string{ + "Equal": "EqualTo", + "DeepEqual": "DeepEqualTo", +} + +func translateMatcherFuncName(n string) string { + // TODO: What about the matcher Len, Nil, NotNil? + + if translated, ok := matcherNameTranslationTable[n]; ok { + return translated + } + + return n +} + +var chainingMethodNames = []string{ + "Is", + "Has", + "And", + "Matches", +} + +func addImportToIsPackage(file *ast.File) { + var isImportFound bool + for _, imp := range file.Imports { + if imp.Path.Value == strconv.Quote("github.com/halimath/expect/is") { + isImportFound = true + break + } + } + + if isImportFound { + return + } + + for _, decl := range file.Decls { + if genDecl, ok := decl.(*ast.GenDecl); ok && genDecl.Tok == token.IMPORT { + importSpec := &ast.ImportSpec{Path: &ast.BasicLit{Value: strconv.Quote("github.com/halimath/expect/is")}} + genDecl.Specs = append(genDecl.Specs, importSpec) + break + } + } + +} + +func rewriteImports(node ast.Node) bool { + if imp, ok := node.(*ast.ImportSpec); ok { + if imp.Path.Value == "\"github.com/halimath/expect-go\"" { + imp.Path.Value = "\"github.com/halimath/expect\"" + imp.Name = nil + } + + return false + } + + return true +} + +type blockFrame struct { + block *ast.BlockStmt + lastExpectation *ast.CallExpr + currentStmtIndex int + stmtsToRemove []int +} + +type expectationRewriteVisitor struct { + blocks Stack[*blockFrame] +} + +func (v *expectationRewriteVisitor) removeMarkedStatments(currentBlock *blockFrame) { + stmts := make([]ast.Stmt, 0, len(currentBlock.block.List)) + + for i := range currentBlock.block.List { + if !contains(currentBlock.stmtsToRemove, i) { + stmts = append(stmts, currentBlock.block.List[i]) + } + } + + currentBlock.block.List = stmts +} + +func (v *expectationRewriteVisitor) Visit(node ast.Node) ast.Visitor { + if node == nil { + return nil + } + + currentBlock, _ := v.blocks.Peek() + + if b, ok := node.(*ast.BlockStmt); ok { + v.blocks.Push(&blockFrame{ + block: b, + currentStmtIndex: -1, + }) + + for _, n := range b.List { + ast.Walk(v, n) + } + + blockStatement, _ := v.blocks.Pop() + v.removeMarkedStatments(blockStatement) + + return nil + } + + if _, ok := node.(ast.Stmt); ok { + if currentBlock != nil { + currentBlock.currentStmtIndex++ + } + } + + if _, ok := node.(*ast.ExprStmt); ok { + return v + } + + call, ok := node.(*ast.CallExpr) + if !ok { + if currentBlock != nil { + currentBlock.lastExpectation = nil + } + return v + } + + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + if currentBlock != nil { + currentBlock.lastExpectation = nil + } + + return v + } + + if !contains(chainingMethodNames, sel.Sel.Name) { + if currentBlock != nil { + currentBlock.lastExpectation = nil + } + + return v + } + + if len(call.Args) != 1 { + if currentBlock != nil { + currentBlock.lastExpectation = nil + } + + return v + } + + matcher, ok := call.Args[0].(*ast.CallExpr) + if !ok { + if currentBlock != nil { + currentBlock.lastExpectation = nil + } + + return v + } + + expectThatCall, ok := sel.X.(*ast.CallExpr) + if !ok { + if currentBlock != nil { + currentBlock.lastExpectation = nil + } + + return v + } + + expectThat, ok := expectThatCall.Fun.(*ast.Ident) + if !ok { + if currentBlock != nil { + currentBlock.lastExpectation = nil + } + + return v + } + + if expectThat.Name != "ExpectThat" && expectThat.Name != "EnsureThat" { + if currentBlock != nil { + currentBlock.lastExpectation = nil + } + + return v + } + + failNow := expectThat.Name == "EnsureThat" + + if len(expectThatCall.Args) != 2 { + if currentBlock != nil { + currentBlock.lastExpectation = nil + } + + return nil + } + + got := expectThatCall.Args[1] + + newMatcherArgs := []ast.Expr{got} + newMatcherArgs = append(newMatcherArgs, matcher.Args...) + matcher.Args = newMatcherArgs + + var matcherFun ast.Expr = &ast.SelectorExpr{ + X: &ast.Ident{ + Name: "is", + }, + Sel: &ast.Ident{ + Name: translateMatcherFuncName(matcher.Fun.(*ast.Ident).Name), + }, + } + + matcher.Fun = matcherFun + + if failNow { + failNowCall := &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{Name: "expect"}, + Sel: &ast.Ident{Name: "FailNow"}, + }, + Args: []ast.Expr{matcher}, + } + matcher = failNowCall + } + + if currentBlock.lastExpectation != nil { + // The previous expression was a call to expect.That + // Append the expectation that call + currentBlock.lastExpectation.Args = append(currentBlock.lastExpectation.Args, matcher) + + // Mark the statement for removal + currentBlock.stmtsToRemove = append(currentBlock.stmtsToRemove, currentBlock.currentStmtIndex) + } else { + call.Fun = &ast.SelectorExpr{ + X: &ast.Ident{ + Name: "expect", + }, + Sel: &ast.Ident{ + Name: "That", + }, + } + + call.Args = expectThatCall.Args[:1] + + call.Args = append(call.Args, matcher) + currentBlock.lastExpectation = call + } + + return nil +} + +type Stack[T any] []T + +func (s *Stack[T]) Push(v T) { + *s = append(*s, v) +} + +func (s *Stack[T]) Pop() (v T, ok bool) { + if len(*s) == 0 { + return + } + + v = (*s)[len(*s)-1] + *s = (*s)[:len(*s)-1] + ok = true + return +} + +func (s *Stack[T]) Peek() (v T, ok bool) { + if len(*s) == 0 { + return + } + + v = (*s)[len(*s)-1] + ok = true + return +} + +func contains[S ~[]E, E comparable](s S, v E) bool { + for i := range s { + if s[i] == v { + return true + } + } + + return false +} diff --git a/cmd/expect-migrate/main_test.go b/cmd/expect-migrate/main_test.go new file mode 100644 index 0000000..b0801c7 --- /dev/null +++ b/cmd/expect-migrate/main_test.go @@ -0,0 +1,235 @@ +package main + +import ( + "os" + "strings" + "testing" + + "github.com/halimath/expect" + "github.com/halimath/expect/is" +) + +// func TestDebug(t *testing.T) { +// in := `package test +// func TestMemfs_OpenFile(t *testing.T) { +// ExpectThat(t, err).Is(NoError()) +// ExpectThat(t, err).Is(NoError()) +// ExpectThat(t, err).Is(NoError()) + +// var s string +// } +// ` + +// // in := ` +// // package test + +// // import ( +// // "testing" +// // . "github.com/halimath/expect-go" +// // ) + +// // func TestSomething(t *testing.T) { +// // var err error +// // s := produceValue() +// // EnsureThat(t, err).Is(NoError()) + +// // } +// // ` + +// var sb strings.Builder +// err := migrate("test.go", strings.NewReader(in), &sb) +// expect.That(t, is.NoError(err)) +// t.Log(sb.String()) +// } + +func TestMigrate_rewriteOldCode(t *testing.T) { + type testCase struct { + label, src, want string + } + + tests := []testCase{ + { + label: "should rewrite old test code", + src: ` +package test + +import ( + "testing" + . "github.com/halimath/expect-go" +) + +func TestSomething(t *testing.T) { + var err error + s := produceValue() + + EnsureThat(t, err).Is(NoError()) + ExpectThat(t, s).Is(Equal("foo")) +} + `, + want: strings.TrimSpace(` +package test + +import ( + "testing" + "github.com/halimath/expect" + "github.com/halimath/expect/is" +) + +func TestSomething(t *testing.T) { + var err error + s := produceValue() + expect.That(t, expect.FailNow(is.NoError(err)), is.EqualTo(s, "foo")) + +} + `), + }, + { + label: "should rewrite old test code with inline got value", + src: ` +package test + +import ( + "testing" + . "github.com/halimath/expect-go" +) + +func TestSomething(t *testing.T) { + EnsureThat(t, l).Is(Equal(len("hello, world"))) + EnsureThat(t, f.Close()).Is(NoError()) +} + `, + want: strings.TrimSpace(` +package test + +import ( + "testing" + "github.com/halimath/expect" + "github.com/halimath/expect/is" +) + +func TestSomething(t *testing.T) { + expect.That(t, expect.FailNow(is.EqualTo(l, len("hello, world"))), expect.FailNow(is.NoError(f.Close()))) + +} + `), + }, + { + label: "should not rewrite new test code", + src: ` +package test + +import ( + "testing" + "github.com/halimath/expect" + "github.com/halimath/expect/is" +) + +func TestSomething(t *testing.T) { + var err error + s := produceValue() + expect.That(t, expect.FailNow(is.NoError(err))) + expect.That(t, is.EqualTo(s, "foo")) +} + `, + + want: strings.TrimSpace(` +package test + +import ( + "testing" + "github.com/halimath/expect" + "github.com/halimath/expect/is" +) + +func TestSomething(t *testing.T) { + var err error + s := produceValue() + expect.That(t, expect.FailNow(is.NoError(err))) + expect.That(t, is.EqualTo(s, "foo")) +} + `), + }, + // ------------------------- + { + label: "should rewrite code from Run functions with multiple expectations", + src: ` +package test + +import ( + "testing" + . "github.com/halimath/expect-go" + . "github.com/halimath/fixture" +) + +func TestMemfs_OpenFile(t *testing.T) { + With(t, new(memfsFixture)). + Run("success", func(t *testing.T, f *memfsFixture) { + file, err := f.fs.OpenFile("open_file", fsx.O_RDWR|fsx.O_CREATE, 0644) + EnsureThat(t, err).Is(NoError()) + + l, err := file.Write([]byte("hello, world")) + EnsureThat(t, err).Is(NoError()) + EnsureThat(t, l).Is(Equal(len("hello, world"))) + + EnsureThat(t, file.Close()).Is(NoError()) + + got, err := fs.ReadFile(f.fs, "open_file") + EnsureThat(t, err).Is(NoError()) + ExpectThat(t, string(got)).Is(Equal("hello, world")) + }) +} + `, + want: `package test + +import ( + "testing" + "github.com/halimath/expect" + . "github.com/halimath/fixture" + "github.com/halimath/expect/is" +) + +func TestMemfs_OpenFile(t *testing.T) { + With(t, new(memfsFixture)). + Run("success", func(t *testing.T, f *memfsFixture) { + file, err := f.fs.OpenFile("open_file", fsx.O_RDWR|fsx.O_CREATE, 0644) + expect.That(t, expect.FailNow(is.NoError(err))) + + l, err := file.Write([]byte("hello, world")) + expect.That(t, expect.FailNow(is.NoError(err)), expect.FailNow(is.EqualTo(l, len("hello, world"))), expect.FailNow(is.NoError(file.Close()))) + + got, err := fs.ReadFile(f.fs, "open_file") + expect.That(t, expect.FailNow(is.NoError(err)), is.EqualTo(string(got), "hello, world")) + + }) +}`, + }, + } + + for _, test := range tests { + var sb strings.Builder + err := migrate("unit_test.go", strings.NewReader(test.src), &sb) + got := strings.TrimSpace(sb.String()) + + expect.WithMessage(t, test.label).That( + is.NoError(err), + is.EqualToStringByLines(got, test.want), + ) + } +} + +func TestMigrate_realWorldTestcase(t *testing.T) { + defer func() { + os.Rename("./testdata/in_test.go~", "./testdata/in_test.go") + }() + + err := migrateFile("./testdata/in_test.go") + expect.That(t, expect.FailNow(is.NoError(err))) + + want, err := os.ReadFile("./testdata/want_test.go") + expect.That(t, expect.FailNow(is.NoError(err))) + + got, err := os.ReadFile("./testdata/in_test.go") + expect.That(t, expect.FailNow(is.NoError(err))) + + expect.That(t, is.EqualToStringByLines(string(got), string(want))) +} diff --git a/cmd/expect-migrate/testdata/in_test.go b/cmd/expect-migrate/testdata/in_test.go new file mode 100644 index 0000000..704f5c3 --- /dev/null +++ b/cmd/expect-migrate/testdata/in_test.go @@ -0,0 +1,406 @@ +package memfs + +import ( + "io/fs" + "reflect" + "testing" + "time" + + . "github.com/halimath/expect-go" + . "github.com/halimath/fixture" + "github.com/halimath/fsx" +) + +type memfsFixture struct { + fs fsx.LinkFS +} + +func (f *memfsFixture) BeforeEach(t *testing.T) error { + f.fs = New() + return nil +} + +func TestMemfs_Mkdir(t *testing.T) { + With(t, new(memfsFixture)). + Run("success", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, f.fs.Mkdir("mkdir", 0777)).Is(NoError()) + EnsureThat(t, f.fs.Mkdir("mkdir/child", 0777)).Is(NoError()) + }). + Run("noParent", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, f.fs.Mkdir("mkdir/child", 0777)).Is(Error(fs.ErrNotExist)) + }). + Run("parentNotADirectory", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, fsx.WriteFile(f.fs, "not_a_directory", []byte("hello, world"), 0666)).Is(NoError()) + EnsureThat(t, f.fs.Mkdir("not_a_directory/child", 0777)).Is(Error(fs.ErrInvalid)) + }) +} + +func TestMemfs_OpenFile(t *testing.T) { + With(t, new(memfsFixture)). + Run("success", func(t *testing.T, f *memfsFixture) { + file, err := f.fs.OpenFile("open_file", fsx.O_RDWR|fsx.O_CREATE, 0644) + EnsureThat(t, err).Is(NoError()) + + l, err := file.Write([]byte("hello, world")) + EnsureThat(t, err).Is(NoError()) + EnsureThat(t, l).Is(Equal(len("hello, world"))) + + EnsureThat(t, file.Close()).Is(NoError()) + + got, err := fs.ReadFile(f.fs, "open_file") + EnsureThat(t, err).Is(NoError()) + ExpectThat(t, string(got)).Is(Equal("hello, world")) + }). + Run("notExist", func(t *testing.T, f *memfsFixture) { + _, err := f.fs.OpenFile("not_found", fsx.O_RDONLY, 0644) + EnsureThat(t, err).Is(Error(fs.ErrNotExist)) + }). + Run("parentNotExist", func(t *testing.T, f *memfsFixture) { + _, err := f.fs.OpenFile("parent_not_found/not_found", fsx.O_RDONLY, 0644) + EnsureThat(t, err).Is(Error(fs.ErrNotExist)) + }). + Run("parentNotADirectory", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, fsx.WriteFile(f.fs, "not_a_directory", []byte("hello, world"), 0666)).Is(NoError()) + + _, err := f.fs.OpenFile("not_a_directory/file", fsx.O_CREATE, 0644) + EnsureThat(t, err).Is(Error(fs.ErrInvalid)) + }). + Run("parentNotWritable", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, f.fs.Mkdir("dir", 0400)).Is(NoError()) + + _, err := f.fs.OpenFile("dir/file", fsx.O_WRONLY|fsx.O_CREATE, 0400) + ExpectThat(t, err).Is(Error(fs.ErrPermission)) + }). + Run("fileNotWritable", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, fsx.WriteFile(f.fs, "file", []byte("hello, world"), 0600)).Is(NoError()) + EnsureThat(t, fsx.Chmod(f.fs, "file", 0400)).Is(NoError()) + + _, err := f.fs.OpenFile("file", fsx.O_WRONLY, 0400) + ExpectThat(t, err).Is(Error(fs.ErrPermission)) + }) +} + +func TestMemfs_Open(t *testing.T) { + With(t, new(memfsFixture)). + Run("notExist", func(t *testing.T, f *memfsFixture) { + _, err := f.fs.Open("not_found") + EnsureThat(t, err).Is(Error(fs.ErrNotExist)) + }). + Run("success", func(t *testing.T, f *memfsFixture) { + file, err := f.fs.OpenFile("open", fsx.O_RDWR|fsx.O_CREATE, 0644) + EnsureThat(t, err).Is(NoError()) + + l, err := file.Write([]byte("hello, world")) + EnsureThat(t, err).Is(NoError()) + EnsureThat(t, l).Is(Equal(len("hello, world"))) + + EnsureThat(t, file.Close()).Is(NoError()) + + rf, err := f.fs.Open("open") + EnsureThat(t, err).Is(NoError()) + + buf := make([]byte, len("hello, world")) + l, err = rf.Read(buf) + EnsureThat(t, err).Is(NoError()) + ExpectThat(t, l).Is(Equal(len("hello, world"))) + ExpectThat(t, string(buf)).Is(Equal("hello, world")) + + EnsureThat(t, rf.Close()).Is(NoError()) + }). + Run("dir", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, f.fs.Mkdir("dir", 0777)).Is(NoError()) + EnsureThat(t, f.fs.Mkdir("dir/sub_dir", 0777)).Is(NoError()) + EnsureThat(t, fsx.WriteFile(f.fs, "dir/sub_file", []byte("hello, world"), 0666)).Is(NoError()) + + rd, err := f.fs.Open("dir") + EnsureThat(t, err).Is(NoError()) + + info, err := rd.Stat() + EnsureThat(t, err).Is(NoError()) + + ExpectThat(t, info.IsDir()).Is(Equal(true)) + + readDirFile, ok := rd.(fs.ReadDirFile) + EnsureThat(t, ok).Is(Equal(true)) + + entries, err := readDirFile.ReadDir(-1) + EnsureThat(t, err).Is(NoError()) + + ExpectThat(t, entries).Is(DeepEqual([]fs.DirEntry{ + &dirEntry{ + name: "sub_dir", + info: &fileInfo{ + path: "dir/sub_dir", + size: 0, + mode: fs.ModeDir | 0777, + }, + }, + &dirEntry{ + name: "sub_file", + info: &fileInfo{ + path: "dir/sub_file", + size: 12, + mode: 0666, + }, + }, + }, + ExcludeTypes{reflect.TypeOf(time.Now())}, + )) + + EnsureThat(t, rd.Close()).Is(NoError()) + }) +} + +func TestMemfs_Remove(t *testing.T) { + With(t, new(memfsFixture)). + Run("emptyDir", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, f.fs.Mkdir("dir", 0777)).Is(NoError()) + + EnsureThat(t, f.fs.Remove("dir")).Is(NoError()) + + _, err := fs.Stat(f.fs, "dir") + ExpectThat(t, err).Is(Error(fs.ErrNotExist)) + }). + Run("file", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, fsx.WriteFile(f.fs, "file", []byte("hello, world"), 0644)).Is(NoError()) + + EnsureThat(t, f.fs.Remove("file")).Is(NoError()) + + _, err := fs.Stat(f.fs, "file") + ExpectThat(t, err).Is(Error(fs.ErrNotExist)) + }). + Run("not_exist", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, f.fs.Remove("not_exist")).Is(NoError()) + }). + Run("parent_not_exist", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, f.fs.Remove("not_exist/sub")).Is(Error(fs.ErrNotExist)) + }). + Run("parent_not_a_directory", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, fsx.WriteFile(f.fs, "file", []byte("hello, world"), 0644)).Is(NoError()) + + EnsureThat(t, f.fs.Remove("file/sub")).Is(Error(fs.ErrInvalid)) + }) +} + +func TestMemfs_SameFile(t *testing.T) { + With(t, new(memfsFixture)). + Run("same", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, fsx.WriteFile(f.fs, "f1", []byte("hello, world"), 0644)).Is(NoError()) + + fi1, err := fs.Stat(f.fs, "f1") + EnsureThat(t, err).Is(NoError()) + + fi2, err := fs.Stat(f.fs, "f1") + EnsureThat(t, err).Is(NoError()) + + ExpectThat(t, f.fs.SameFile(fi1, fi2)).Is(Equal(true)) + }). + Run("different", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, fsx.WriteFile(f.fs, "f1", []byte("hello, world"), 0644)).Is(NoError()) + EnsureThat(t, fsx.WriteFile(f.fs, "f2", []byte("hello, world"), 0644)).Is(NoError()) + + fi1, err := fs.Stat(f.fs, "f1") + EnsureThat(t, err).Is(NoError()) + + fi2, err := fs.Stat(f.fs, "f2") + EnsureThat(t, err).Is(NoError()) + + ExpectThat(t, f.fs.SameFile(fi1, fi2)).Is(Equal(false)) + }) +} + +func TestMemfs_Rename(t *testing.T) { + With(t, new(memfsFixture)). + Run("old_parent_not_exist", func(t *testing.T, f *memfsFixture) { + ExpectThat(t, f.fs.Rename("not_exists/file", "file")).Is(Error(fs.ErrNotExist)) + }). + Run("new_parent_not_exist", func(t *testing.T, f *memfsFixture) { + ExpectThat(t, f.fs.Rename("file", "not_exists/file")).Is(Error(fs.ErrNotExist)) + }). + Run("d´directories", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, f.fs.Mkdir("from", 0777)).Is(NoError()) + EnsureThat(t, fsx.WriteFile(f.fs, "from/file", []byte("hello, world"), 0644)).Is(NoError()) + + EnsureThat(t, f.fs.Rename("from", "to")).Is(NoError()) + + _, err := fs.Stat(f.fs, "from/file") + ExpectThat(t, err).Is(Error(fs.ErrNotExist)) + + _, err = fs.Stat(f.fs, "to/file") + ExpectThat(t, err).Is(NoError()) + }). + Run("file", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, fsx.WriteFile(f.fs, "file", []byte("hello, world"), 0644)).Is(NoError()) + + EnsureThat(t, f.fs.Rename("file", "to")).Is(NoError()) + + _, err := fs.Stat(f.fs, "file") + ExpectThat(t, err).Is(Error(fs.ErrNotExist)) + + _, err = fs.Stat(f.fs, "to") + ExpectThat(t, err).Is(NoError()) + }). + Run("file_inside_same_dir", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, f.fs.Mkdir("dir", 0777)).Is(NoError()) + EnsureThat(t, fsx.WriteFile(f.fs, "dir/from", []byte("hello, world"), 0644)).Is(NoError()) + + EnsureThat(t, f.fs.Rename("dir/from", "dir/to")).Is(NoError()) + + _, err := fs.Stat(f.fs, "dir/from") + ExpectThat(t, err).Is(Error(fs.ErrNotExist)) + + _, err = fs.Stat(f.fs, "dir/to") + ExpectThat(t, err).Is(NoError()) + }). + Run("file_between_different_dirs", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, f.fs.Mkdir("from", 0777)).Is(NoError()) + EnsureThat(t, f.fs.Mkdir("to", 0777)).Is(NoError()) + + EnsureThat(t, fsx.WriteFile(f.fs, "from/file", []byte("hello, world"), 0644)).Is(NoError()) + + EnsureThat(t, f.fs.Rename("from/file", "to/file")).Is(NoError()) + + _, err := fs.Stat(f.fs, "from/file") + ExpectThat(t, err).Is(Error(fs.ErrNotExist)) + + _, err = fs.Stat(f.fs, "to/file") + ExpectThat(t, err).Is(NoError()) + }). + Run("invalid_source", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, fsx.WriteFile(f.fs, "from", []byte("hello, world"), 0644)).Is(NoError()) + + ExpectThat(t, f.fs.Rename("from/file", "file")).Is(Error(fs.ErrInvalid)) + }). + Run("invalid_target", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, fsx.WriteFile(f.fs, "from", []byte("hello, world"), 0644)).Is(NoError()) + EnsureThat(t, fsx.WriteFile(f.fs, "to", []byte("hello, world"), 0644)).Is(NoError()) + + ExpectThat(t, f.fs.Rename("from", "to/file")).Is(Error(fs.ErrInvalid)) + }). + Run("source_not_found", func(t *testing.T, f *memfsFixture) { + ExpectThat(t, f.fs.Rename("from", "to")).Is(Error(fs.ErrNotExist)) + }) +} + +func TestMemfs_Chmod(t *testing.T) { + With(t, new(memfsFixture)). + Run("dir", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, f.fs.Mkdir("dir", 0777)).Is(NoError()) + + file, err := f.fs.OpenFile("dir", fsx.O_WRONLY, 0) + EnsureThat(t, err).Is(NoError()) + + EnsureThat(t, file.Chmod(0700)).Is(NoError()) + + EnsureThat(t, file.Close()).Is(NoError()) + + info, err := fs.Stat(f.fs, "dir") + EnsureThat(t, err).Is(NoError()) + + ExpectThat(t, info.Mode()).Is(Equal(fs.ModeDir | 0700)) + }). + Run("file", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, fsx.WriteFile(f.fs, "file", []byte("hello, world"), 0666)).Is(NoError()) + + file, err := f.fs.OpenFile("file", fsx.O_WRONLY, 0) + EnsureThat(t, err).Is(NoError()) + + EnsureThat(t, file.Chmod(0600)).Is(NoError()) + + EnsureThat(t, file.Close()).Is(NoError()) + + info, err := fs.Stat(f.fs, "file") + EnsureThat(t, err).Is(NoError()) + + ExpectThat(t, info.Mode()).Is(Equal(fs.FileMode(0600))) + }) +} + +func TestMemfs_RemoveAll(t *testing.T) { + With(t, new(memfsFixture)). + Run("success", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, f.fs.Mkdir("remove_all", 0777)).Is(NoError()) + EnsureThat(t, fsx.WriteFile(f.fs, "remove_all/file", []byte("hello, world"), 0644)).Is(NoError()) + + EnsureThat(t, fsx.RemoveAll(f.fs, "remove_all")).Is(NoError()) + + _, err := fs.Stat(f.fs, "remove_all/file") + ExpectThat(t, err).Is(Error(fs.ErrNotExist)) + + _, err = fs.Stat(f.fs, "remove_all") + ExpectThat(t, err).Is(Error(fs.ErrNotExist)) + }) +} + +func TestMemfs_ReadFile(t *testing.T) { + With(t, new(memfsFixture)). + Run("not_exist", func(t *testing.T, f *memfsFixture) { + _, err := fs.ReadFile(f.fs, "not_exist") + ExpectThat(t, err).Is(Error(fs.ErrNotExist)) + }). + Run("no_file", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, f.fs.Mkdir("dir", 0777)).Is(NoError()) + + _, err := fs.ReadFile(f.fs, "dir") + ExpectThat(t, err).Is(Error(ErrIsDirectory)) + }). + Run("success", func(t *testing.T, f *memfsFixture) { + EnsureThat(t, fsx.WriteFile(f.fs, "f", []byte("test"), 0644)).Is(NoError()) + + data, err := fs.ReadFile(f.fs, "f") + ExpectThat(t, err).Is(NoError()) + ExpectThat(t, data).Is(DeepEqual([]byte("test"))) + }) +} + +func TestMemfs_Symlink(t *testing.T) { + With(t, new(memfsFixture)). + Run("success", func(t *testing.T, f *memfsFixture) { + err := fsx.WriteFile(f.fs, "f", []byte("hello world"), 0666) + EnsureThat(t, err).Is(NoError()) + + EnsureThat(t, f.fs.Symlink("f", "l")).Is(NoError()) + + got, err := fs.ReadFile(f.fs, "l") + ExpectThat(t, err).Is(NoError()) + ExpectThat(t, string(got)).Is(Equal("hello world")) + }) +} + +func TestMemfs_Link(t *testing.T) { + With(t, new(memfsFixture)). + Run("file", func(t *testing.T, f *memfsFixture) { + err := fsx.WriteFile(f.fs, "f", []byte("hello world"), 0666) + EnsureThat(t, err).Is(NoError()) + + EnsureThat(t, f.fs.Link("f", "l")).Is(NoError()) + + got, err := fs.ReadFile(f.fs, "l") + ExpectThat(t, err).Is(NoError()) + ExpectThat(t, string(got)).Is(Equal("hello world")) + }). + Run("dir", func(t *testing.T, f *memfsFixture) { + err := fsx.MkdirAll(f.fs, "dir/child", 0777) + EnsureThat(t, err).Is(NoError()) + + EnsureThat(t, f.fs.Link("dir", "l")).Is(NoError()) + + got, err := fs.ReadDir(f.fs, "l") + ExpectThat(t, err).Is(NoError()) + ExpectThat(t, got).Has(Len(1)) + ExpectThat(t, got[0].Name()).Is(Equal("child")) + }) +} + +func TestMemfs_Readlink(t *testing.T) { + With(t, new(memfsFixture)). + Run("symlink", func(t *testing.T, f *memfsFixture) { + err := fsx.WriteFile(f.fs, "f", []byte("hello world"), 0666) + EnsureThat(t, err).Is(NoError()) + EnsureThat(t, f.fs.Symlink("f", "l")).Is(NoError()) + + got, err := f.fs.Readlink("l") + ExpectThat(t, err).Is(NoError()) + ExpectThat(t, got).Is(Equal("f")) + }) +} diff --git a/cmd/expect-migrate/testdata/want_test.go b/cmd/expect-migrate/testdata/want_test.go new file mode 100644 index 0000000..b7afef6 --- /dev/null +++ b/cmd/expect-migrate/testdata/want_test.go @@ -0,0 +1,345 @@ +package memfs + +import ( + "io/fs" + "reflect" + "testing" + "time" + + "github.com/halimath/expect" + . "github.com/halimath/fixture" + "github.com/halimath/fsx" + "github.com/halimath/expect/is" +) + +type memfsFixture struct { + fs fsx.LinkFS +} + +func (f *memfsFixture) BeforeEach(t *testing.T) error { + f.fs = New() + return nil +} + +func TestMemfs_Mkdir(t *testing.T) { + With(t, new(memfsFixture)). + Run("success", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(f.fs.Mkdir("mkdir", 0777))), expect.FailNow(is.NoError(f.fs.Mkdir("mkdir/child", 0777)))) + + }). + Run("noParent", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.Error(f.fs.Mkdir("mkdir/child", 0777), fs.ErrNotExist))) + }). + Run("parentNotADirectory", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "not_a_directory", []byte("hello, world"), 0666))), expect.FailNow(is.Error(f.fs.Mkdir("not_a_directory/child", 0777), fs.ErrInvalid))) + + }) +} + +func TestMemfs_OpenFile(t *testing.T) { + With(t, new(memfsFixture)). + Run("success", func(t *testing.T, f *memfsFixture) { + file, err := f.fs.OpenFile("open_file", fsx.O_RDWR|fsx.O_CREATE, 0644) + expect.That(t, expect.FailNow(is.NoError(err))) + + l, err := file.Write([]byte("hello, world")) + expect.That(t, expect.FailNow(is.NoError(err)), expect.FailNow(is.EqualTo(l, len("hello, world"))), expect.FailNow(is.NoError(file.Close()))) + + got, err := fs.ReadFile(f.fs, "open_file") + expect.That(t, expect.FailNow(is.NoError(err)), is.EqualTo(string(got), "hello, world")) + + }). + Run("notExist", func(t *testing.T, f *memfsFixture) { + _, err := f.fs.OpenFile("not_found", fsx.O_RDONLY, 0644) + expect.That(t, expect.FailNow(is.Error(err, fs.ErrNotExist))) + }). + Run("parentNotExist", func(t *testing.T, f *memfsFixture) { + _, err := f.fs.OpenFile("parent_not_found/not_found", fsx.O_RDONLY, 0644) + expect.That(t, expect.FailNow(is.Error(err, fs.ErrNotExist))) + }). + Run("parentNotADirectory", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "not_a_directory", []byte("hello, world"), 0666)))) + + _, err := f.fs.OpenFile("not_a_directory/file", fsx.O_CREATE, 0644) + expect.That(t, expect.FailNow(is.Error(err, fs.ErrInvalid))) + }). + Run("parentNotWritable", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(f.fs.Mkdir("dir", 0400)))) + + _, err := f.fs.OpenFile("dir/file", fsx.O_WRONLY|fsx.O_CREATE, 0400) + expect.That(t, is.Error(err, fs.ErrPermission)) + }). + Run("fileNotWritable", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "file", []byte("hello, world"), 0600))), expect.FailNow(is.NoError(fsx.Chmod(f.fs, "file", 0400)))) + + _, err := f.fs.OpenFile("file", fsx.O_WRONLY, 0400) + expect.That(t, is.Error(err, fs.ErrPermission)) + }) +} + +func TestMemfs_Open(t *testing.T) { + With(t, new(memfsFixture)). + Run("notExist", func(t *testing.T, f *memfsFixture) { + _, err := f.fs.Open("not_found") + expect.That(t, expect.FailNow(is.Error(err, fs.ErrNotExist))) + }). + Run("success", func(t *testing.T, f *memfsFixture) { + file, err := f.fs.OpenFile("open", fsx.O_RDWR|fsx.O_CREATE, 0644) + expect.That(t, expect.FailNow(is.NoError(err))) + + l, err := file.Write([]byte("hello, world")) + expect.That(t, expect.FailNow(is.NoError(err)), expect.FailNow(is.EqualTo(l, len("hello, world"))), expect.FailNow(is.NoError(file.Close()))) + + rf, err := f.fs.Open("open") + expect.That(t, expect.FailNow(is.NoError(err))) + + buf := make([]byte, len("hello, world")) + l, err = rf.Read(buf) + expect.That(t, expect.FailNow(is.NoError(err)), is.EqualTo(l, len("hello, world")), is.EqualTo(string(buf), "hello, world"), expect.FailNow(is.NoError(rf.Close()))) + + }). + Run("dir", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(f.fs.Mkdir("dir", 0777))), expect.FailNow(is.NoError(f.fs.Mkdir("dir/sub_dir", 0777))), expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "dir/sub_file", []byte("hello, world"), 0666)))) + + rd, err := f.fs.Open("dir") + expect.That(t, expect.FailNow(is.NoError(err))) + + info, err := rd.Stat() + expect.That(t, expect.FailNow(is.NoError(err)), is.EqualTo(info.IsDir(), true)) + + readDirFile, ok := rd.(fs.ReadDirFile) + expect.That(t, expect.FailNow(is.EqualTo(ok, true))) + + entries, err := readDirFile.ReadDir(-1) + expect.That(t, expect.FailNow(is.NoError(err)), is.DeepEqualTo(entries, []fs.DirEntry{ + &dirEntry{ + name: "sub_dir", + info: &fileInfo{ + path: "dir/sub_dir", + size: 0, + mode: fs.ModeDir | 0777, + }, + }, + &dirEntry{ + name: "sub_file", + info: &fileInfo{ + path: "dir/sub_file", + size: 12, + mode: 0666, + }, + }, + }, + ExcludeTypes{reflect.TypeOf(time.Now())}, + ), expect.FailNow(is.NoError(rd.Close()))) + + }) +} + +func TestMemfs_Remove(t *testing.T) { + With(t, new(memfsFixture)). + Run("emptyDir", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(f.fs.Mkdir("dir", 0777))), expect.FailNow(is.NoError(f.fs.Remove("dir")))) + + _, err := fs.Stat(f.fs, "dir") + expect.That(t, is.Error(err, fs.ErrNotExist)) + }). + Run("file", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "file", []byte("hello, world"), 0644))), expect.FailNow(is.NoError(f.fs.Remove("file")))) + + _, err := fs.Stat(f.fs, "file") + expect.That(t, is.Error(err, fs.ErrNotExist)) + }). + Run("not_exist", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(f.fs.Remove("not_exist")))) + }). + Run("parent_not_exist", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.Error(f.fs.Remove("not_exist/sub"), fs.ErrNotExist))) + }). + Run("parent_not_a_directory", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "file", []byte("hello, world"), 0644))), expect.FailNow(is.Error(f.fs.Remove("file/sub"), fs.ErrInvalid))) + + }) +} + +func TestMemfs_SameFile(t *testing.T) { + With(t, new(memfsFixture)). + Run("same", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "f1", []byte("hello, world"), 0644)))) + + fi1, err := fs.Stat(f.fs, "f1") + expect.That(t, expect.FailNow(is.NoError(err))) + + fi2, err := fs.Stat(f.fs, "f1") + expect.That(t, expect.FailNow(is.NoError(err)), is.EqualTo(f.fs.SameFile(fi1, fi2), true)) + + }). + Run("different", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "f1", []byte("hello, world"), 0644))), expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "f2", []byte("hello, world"), 0644)))) + + fi1, err := fs.Stat(f.fs, "f1") + expect.That(t, expect.FailNow(is.NoError(err))) + + fi2, err := fs.Stat(f.fs, "f2") + expect.That(t, expect.FailNow(is.NoError(err)), is.EqualTo(f.fs.SameFile(fi1, fi2), false)) + + }) +} + +func TestMemfs_Rename(t *testing.T) { + With(t, new(memfsFixture)). + Run("old_parent_not_exist", func(t *testing.T, f *memfsFixture) { + expect.That(t, is.Error(f.fs.Rename("not_exists/file", "file"), fs.ErrNotExist)) + }). + Run("new_parent_not_exist", func(t *testing.T, f *memfsFixture) { + expect.That(t, is.Error(f.fs.Rename("file", "not_exists/file"), fs.ErrNotExist)) + }). + Run("d´directories", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(f.fs.Mkdir("from", 0777))), expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "from/file", []byte("hello, world"), 0644))), expect.FailNow(is.NoError(f.fs.Rename("from", "to")))) + + _, err := fs.Stat(f.fs, "from/file") + expect.That(t, is.Error(err, fs.ErrNotExist)) + + _, err = fs.Stat(f.fs, "to/file") + expect.That(t, is.NoError(err)) + }). + Run("file", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "file", []byte("hello, world"), 0644))), expect.FailNow(is.NoError(f.fs.Rename("file", "to")))) + + _, err := fs.Stat(f.fs, "file") + expect.That(t, is.Error(err, fs.ErrNotExist)) + + _, err = fs.Stat(f.fs, "to") + expect.That(t, is.NoError(err)) + }). + Run("file_inside_same_dir", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(f.fs.Mkdir("dir", 0777))), expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "dir/from", []byte("hello, world"), 0644))), expect.FailNow(is.NoError(f.fs.Rename("dir/from", "dir/to")))) + + _, err := fs.Stat(f.fs, "dir/from") + expect.That(t, is.Error(err, fs.ErrNotExist)) + + _, err = fs.Stat(f.fs, "dir/to") + expect.That(t, is.NoError(err)) + }). + Run("file_between_different_dirs", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(f.fs.Mkdir("from", 0777))), expect.FailNow(is.NoError(f.fs.Mkdir("to", 0777))), expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "from/file", []byte("hello, world"), 0644))), expect.FailNow(is.NoError(f.fs.Rename("from/file", "to/file")))) + + _, err := fs.Stat(f.fs, "from/file") + expect.That(t, is.Error(err, fs.ErrNotExist)) + + _, err = fs.Stat(f.fs, "to/file") + expect.That(t, is.NoError(err)) + }). + Run("invalid_source", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "from", []byte("hello, world"), 0644))), is.Error(f.fs.Rename("from/file", "file"), fs.ErrInvalid)) + + }). + Run("invalid_target", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "from", []byte("hello, world"), 0644))), expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "to", []byte("hello, world"), 0644))), is.Error(f.fs.Rename("from", "to/file"), fs.ErrInvalid)) + + }). + Run("source_not_found", func(t *testing.T, f *memfsFixture) { + expect.That(t, is.Error(f.fs.Rename("from", "to"), fs.ErrNotExist)) + }) +} + +func TestMemfs_Chmod(t *testing.T) { + With(t, new(memfsFixture)). + Run("dir", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(f.fs.Mkdir("dir", 0777)))) + + file, err := f.fs.OpenFile("dir", fsx.O_WRONLY, 0) + expect.That(t, expect.FailNow(is.NoError(err)), expect.FailNow(is.NoError(file.Chmod(0700))), expect.FailNow(is.NoError(file.Close()))) + + info, err := fs.Stat(f.fs, "dir") + expect.That(t, expect.FailNow(is.NoError(err)), is.EqualTo(info.Mode(), fs.ModeDir|0700)) + + }). + Run("file", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "file", []byte("hello, world"), 0666)))) + + file, err := f.fs.OpenFile("file", fsx.O_WRONLY, 0) + expect.That(t, expect.FailNow(is.NoError(err)), expect.FailNow(is.NoError(file.Chmod(0600))), expect.FailNow(is.NoError(file.Close()))) + + info, err := fs.Stat(f.fs, "file") + expect.That(t, expect.FailNow(is.NoError(err)), is.EqualTo(info.Mode(), fs.FileMode(0600))) + + }) +} + +func TestMemfs_RemoveAll(t *testing.T) { + With(t, new(memfsFixture)). + Run("success", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(f.fs.Mkdir("remove_all", 0777))), expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "remove_all/file", []byte("hello, world"), 0644))), expect.FailNow(is.NoError(fsx.RemoveAll(f.fs, "remove_all")))) + + _, err := fs.Stat(f.fs, "remove_all/file") + expect.That(t, is.Error(err, fs.ErrNotExist)) + + _, err = fs.Stat(f.fs, "remove_all") + expect.That(t, is.Error(err, fs.ErrNotExist)) + }) +} + +func TestMemfs_ReadFile(t *testing.T) { + With(t, new(memfsFixture)). + Run("not_exist", func(t *testing.T, f *memfsFixture) { + _, err := fs.ReadFile(f.fs, "not_exist") + expect.That(t, is.Error(err, fs.ErrNotExist)) + }). + Run("no_file", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(f.fs.Mkdir("dir", 0777)))) + + _, err := fs.ReadFile(f.fs, "dir") + expect.That(t, is.Error(err, ErrIsDirectory)) + }). + Run("success", func(t *testing.T, f *memfsFixture) { + expect.That(t, expect.FailNow(is.NoError(fsx.WriteFile(f.fs, "f", []byte("test"), 0644)))) + + data, err := fs.ReadFile(f.fs, "f") + expect.That(t, is.NoError(err), is.DeepEqualTo(data, []byte("test"))) + + }) +} + +func TestMemfs_Symlink(t *testing.T) { + With(t, new(memfsFixture)). + Run("success", func(t *testing.T, f *memfsFixture) { + err := fsx.WriteFile(f.fs, "f", []byte("hello world"), 0666) + expect.That(t, expect.FailNow(is.NoError(err)), expect.FailNow(is.NoError(f.fs.Symlink("f", "l")))) + + got, err := fs.ReadFile(f.fs, "l") + expect.That(t, is.NoError(err), is.EqualTo(string(got), "hello world")) + + }) +} + +func TestMemfs_Link(t *testing.T) { + With(t, new(memfsFixture)). + Run("file", func(t *testing.T, f *memfsFixture) { + err := fsx.WriteFile(f.fs, "f", []byte("hello world"), 0666) + expect.That(t, expect.FailNow(is.NoError(err)), expect.FailNow(is.NoError(f.fs.Link("f", "l")))) + + got, err := fs.ReadFile(f.fs, "l") + expect.That(t, is.NoError(err), is.EqualTo(string(got), "hello world")) + + }). + Run("dir", func(t *testing.T, f *memfsFixture) { + err := fsx.MkdirAll(f.fs, "dir/child", 0777) + expect.That(t, expect.FailNow(is.NoError(err)), expect.FailNow(is.NoError(f.fs.Link("dir", "l")))) + + got, err := fs.ReadDir(f.fs, "l") + expect.That(t, is.NoError(err), is.Len(got, 1), is.EqualTo(got[0].Name(), "child")) + + }) +} + +func TestMemfs_Readlink(t *testing.T) { + With(t, new(memfsFixture)). + Run("symlink", func(t *testing.T, f *memfsFixture) { + err := fsx.WriteFile(f.fs, "f", []byte("hello world"), 0666) + expect.That(t, expect.FailNow(is.NoError(err)), expect.FailNow(is.NoError(f.fs.Symlink("f", "l")))) + + got, err := f.fs.Readlink("l") + expect.That(t, is.NoError(err), is.EqualTo(got, "f")) + + }) +} diff --git a/is/string.go b/is/string.go index f389e85..619376f 100644 --- a/is/string.go +++ b/is/string.go @@ -48,3 +48,52 @@ func StringWithSuffix(got, want string) expect.Expectation { } }) } + +// EqualToStringByLines compares got and want line by line and reports different +// lines one at a time. This makes it easiert to understand failed expectations +// when comparing large strings. +func EqualToStringByLines(got, want string) expect.Expectation { + return expect.ExpectFunc(func(t expect.TB) { + t.Helper() + + gotLines := strings.Split(got, "\n") + wantLines := strings.Split(want, "\n") + + lenghtsDiffer := len(gotLines) != len(wantLines) + + if lenghtsDiffer { + t.Errorf("expected string to have %d lines but got %d", len(wantLines), len(gotLines)) + } + + limit := min(len(gotLines), len(wantLines)) + + for i := 0; i < limit; i++ { + if wantLines[i] != gotLines[i] { + t.Errorf("at line %d: wanted\n%q\nbut got\n%q", i, wantLines[i], gotLines[i]) + if lenghtsDiffer { + return + } + } + } + + if len(gotLines) > limit { + for i, line := range gotLines[limit:] { + t.Errorf("line %d: wanted no line but got\n%q", i+limit, line) + } + } + + if len(wantLines) > limit { + for i, line := range wantLines[limit:] { + t.Errorf("line %d: wanted\n%q\nbut got no line", i+limit, line) + } + } + + }) +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/is/string_test.go b/is/string_test.go index cb71767..6b79554 100644 --- a/is/string_test.go +++ b/is/string_test.go @@ -70,3 +70,28 @@ func TestStringWithSuffix(t *testing.T) { t.Errorf("not expected: %#v", tb) } } + +func TestEqualToStringByLines(t *testing.T) { + var tb testhelper.TB + + EqualToStringByLines("foo\nbar", "foo\nbar").Expect(&tb) + EqualToStringByLines("foo\nbar", "foobar").Expect(&tb) + EqualToStringByLines("foo\nbar", "foo").Expect(&tb) + EqualToStringByLines("foo", "foo\nbar").Expect(&tb) + EqualToStringByLines("foo\nbar", "foo\nspam").Expect(&tb) + + if !reflect.DeepEqual(tb, testhelper.TB{ + ErrFlag: true, + + Logs: []string{ + "expected string to have 1 lines but got 2", + "at line 0: wanted\n\"foobar\"\nbut got\n\"foo\"", + "expected string to have 1 lines but got 2", + "line 1: wanted no line but got\n\"bar\"", + "expected string to have 2 lines but got 1", + "line 1: wanted\n\"bar\"\nbut got no line", + "at line 1: wanted\n\"spam\"\nbut got\n\"bar\""}, + }) { + t.Errorf("not expected: %#v", tb) + } +}