diff options
author | Sami Hiltunen <shiltunen@gitlab.com> | 2023-04-06 19:56:27 +0300 |
---|---|---|
committer | Sami Hiltunen <shiltunen@gitlab.com> | 2023-04-17 17:25:32 +0300 |
commit | f5e29f898471886311f8f5e44f67e0fa48546998 (patch) | |
tree | 1d8048239886c76b1364b677798c94948727b1d5 | |
parent | b7492443beb518d87a30ad0bd49a7df92e17c152 (diff) |
Support parsing content in RequireDirectoryState
RequireDirectoryState allows for asserting that a directory matches
the expected state. Right now it only supports matching file content
against expected bytes. This is inconvenient as it can be difficult
to make sense of binary file formats on failure and say what exactly
is wrong without looking at the file itself. Further, some files may
be physically different but semantically the same. An example of such
a file would be Git's pack files, where the physical format may change
for example depending on the delta compression but the actual contained
objects are the same.
This commit adds support for parsing the file's content with a parser
passed in the assertion. The parser takes the file's contents and returns
a parsed object of any type. The parsed type is ultimately asserted for
equality with the expected content. With this in place, we can for example
assert the content of pack files rather than than the physical file bytes.
Missing tests are added for the function.
-rw-r--r-- | internal/testhelper/directory.go | 24 | ||||
-rw-r--r-- | internal/testhelper/directory_test.go | 174 |
2 files changed, 193 insertions, 5 deletions
diff --git a/internal/testhelper/directory.go b/internal/testhelper/directory.go index f4c8c6110..c56e66038 100644 --- a/internal/testhelper/directory.go +++ b/internal/testhelper/directory.go @@ -19,7 +19,11 @@ type DirectoryEntry struct { // Mode is the file mode of the entry. Mode fs.FileMode // Content contains the file content if this is a regular file. - Content []byte + Content any + // ParseContent is a function that receives the file's actual content and + // returns it parsed into the expected form. The returned value is ultimately + // asserted for equality with the Content. + ParseContent func(tb testing.TB, content []byte) any } // DirectoryState models the contents of a directory. The key is relative of the entry in @@ -56,8 +60,13 @@ func RequireDirectoryState(tb testing.TB, rootDirectory, relativeDirectory strin } if entry.Type().IsRegular() { - actualEntry.Content, err = os.ReadFile(path) + content, err := os.ReadFile(path) require.NoError(tb, err) + + actualEntry.Content = content + if expectedEntry, ok := expected[trimmedPath]; ok && expectedEntry.ParseContent != nil { + actualEntry.Content = expectedEntry.ParseContent(tb, content) + } } actual[trimmedPath] = actualEntry @@ -65,11 +74,16 @@ func RequireDirectoryState(tb testing.TB, rootDirectory, relativeDirectory strin return nil })) - if expected == nil { - expected = DirectoryState{} + // Functions are never equal unless they are nil, see https://pkg.go.dev/reflect#DeepEqual. + // So to check of equality we set the ParseContent functions to nil. + // We use a copy so we don't unexpectedly modify the original. + expectedCopy := make(DirectoryState, len(expected)) + for key, value := range expected { + value.ParseContent = nil + expectedCopy[key] = value } - require.Equal(tb, expected, actual) + require.Equal(tb, expectedCopy, actual) } // RequireTarState asserts that the provided tarball contents matches the expected state. diff --git a/internal/testhelper/directory_test.go b/internal/testhelper/directory_test.go new file mode 100644 index 000000000..81022cb32 --- /dev/null +++ b/internal/testhelper/directory_test.go @@ -0,0 +1,174 @@ +package testhelper + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/v15/internal/helper/perm" +) + +type tbRecorder struct { + // Embed a nil TB as we'd rather panic if some calls that were + // made were not captured by the recorder. + testing.TB + tb testing.TB + + errorMessage string + helper bool + failNow bool +} + +func (r *tbRecorder) Name() string { + return r.tb.Name() +} + +func (r *tbRecorder) Errorf(format string, args ...any) { + r.errorMessage = fmt.Sprintf(format, args...) +} + +func (r *tbRecorder) Helper() { + r.helper = true +} + +func (r *tbRecorder) FailNow() { + r.failNow = true +} + +func TestRequireDirectoryState(t *testing.T) { + umask := perm.GetUmask() + + t.Parallel() + + rootDir := t.TempDir() + + relativePath := "assertion-root" + + require.NoError(t, + os.MkdirAll( + filepath.Join(rootDir, relativePath, "dir-a"), + fs.ModePerm, + ), + ) + require.NoError(t, + os.MkdirAll( + filepath.Join(rootDir, relativePath, "dir-b"), + perm.PrivateDir, + ), + ) + require.NoError(t, + os.WriteFile( + filepath.Join(rootDir, relativePath, "dir-a", "unparsed-file"), + []byte("raw content"), + fs.ModePerm, + ), + ) + require.NoError(t, + os.WriteFile( + filepath.Join(rootDir, relativePath, "parsed-file"), + []byte("raw content"), + perm.PrivateFile, + ), + ) + + for _, tc := range []struct { + desc string + modifyAssertion func(DirectoryState) + expectedErrorMessage string + }{ + { + desc: "correct assertion", + modifyAssertion: func(DirectoryState) {}, + }, + { + desc: "unexpected directory", + modifyAssertion: func(state DirectoryState) { + delete(state, "/assertion-root") + }, + expectedErrorMessage: `+ (string) (len=15) "/assertion-root": (testhelper.DirectoryEntry)`, + }, + { + desc: "unexpected file", + modifyAssertion: func(state DirectoryState) { + delete(state, "/assertion-root/dir-a/unparsed-file") + }, + expectedErrorMessage: `+ (string) (len=35) "/assertion-root/dir-a/unparsed-file": (testhelper.DirectoryEntry)`, + }, + { + desc: "wrong mode", + modifyAssertion: func(state DirectoryState) { + modified := state["/assertion-root/dir-b"] + modified.Mode = fs.ModePerm + state["/assertion-root/dir-b"] = modified + }, + expectedErrorMessage: `- Mode: (fs.FileMode)`, + }, + { + desc: "wrong unparsed content", + modifyAssertion: func(state DirectoryState) { + modified := state["/assertion-root/dir-a/unparsed-file"] + modified.Content = "incorrect content" + state["/assertion-root/dir-a/unparsed-file"] = modified + }, + expectedErrorMessage: `- Content: (string) (len=17) "incorrect content", + + Content: ([]uint8) (len=11) { + + 00000000 72 61 77 20 63 6f 6e 74 65 6e 74 |raw content| + + }`, + }, + { + desc: "wrong parsed content", + modifyAssertion: func(state DirectoryState) { + modified := state["/assertion-root/parsed-file"] + modified.Content = "incorrect content" + state["/assertion-root/parsed-file"] = modified + }, + expectedErrorMessage: `- Content: (string) (len=17) "incorrect content", + + Content: (string) (len=14) "parsed content"`, + }, + { + desc: "missing entry", + modifyAssertion: func(state DirectoryState) { + state["/does/not/exist/on/disk"] = DirectoryEntry{} + }, + expectedErrorMessage: `- (string) (len=23) "/does/not/exist/on/disk": (testhelper.DirectoryEntry)`, + }, + } { + tc := tc + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + expectedState := DirectoryState{ + "/assertion-root": {Mode: umask.Mask(fs.ModeDir | fs.ModePerm)}, + "/assertion-root/parsed-file": { + Mode: umask.Mask(perm.PrivateFile), + Content: "parsed content", + ParseContent: func(tb testing.TB, content []byte) any { + return "parsed content" + }, + }, + "/assertion-root/dir-a": {Mode: umask.Mask(fs.ModeDir | fs.ModePerm)}, + "/assertion-root/dir-a/unparsed-file": {Mode: umask.Mask(fs.ModePerm), Content: []byte("raw content")}, + "/assertion-root/dir-b": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, + } + + tc.modifyAssertion(expectedState) + + recordedTB := &tbRecorder{tb: t} + RequireDirectoryState(recordedTB, rootDir, relativePath, expectedState) + if tc.expectedErrorMessage != "" { + require.Contains(t, recordedTB.errorMessage, tc.expectedErrorMessage) + require.True(t, recordedTB.failNow) + } else { + require.Empty(t, recordedTB.errorMessage) + require.False(t, recordedTB.failNow) + } + require.True(t, recordedTB.helper) + require.NotNil(t, + expectedState["/assertion-root/parsed-file"].ParseContent, + "ParseContent should still be set on the original expected state", + ) + }) + } +} |