The freshen tests built a fixture and never ran freshen. They now run gen and freshen through the command entry point on a temp dir: an unchanged tree leaves the manifest's entries as they were, and a modified file (also one edited without changing its size), a new or a deleted file gives a manifest that lists exactly the tree, with sizes, hashes and mtimes from disk, on which check passes. New fetch tests run the command against an httptest server: a nested tree lands complete, a hash mismatch exits non-zero and leaves no file, and a partly filled destination gets every listed file downloaded again and replaced, while a file the manifest does not list is left alone. Model: opus-5-5
This commit was merged in pull request #145.
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -235,6 +236,33 @@ func manifestOf(t *testing.T, files map[string][]byte) []byte {
|
||||
return scanToManifest(t, sourceFs)
|
||||
}
|
||||
|
||||
// filesUnder returns the content of every file under dir, by its path
|
||||
// relative to dir.
|
||||
func filesUnder(t *testing.T, dir string) map[string][]byte {
|
||||
t.Helper()
|
||||
|
||||
files := map[string][]byte{}
|
||||
|
||||
err := filepath.WalkDir(dir, func(path string, entry os.DirEntry, err error) error {
|
||||
if err != nil || entry.IsDir() {
|
||||
return err
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(dir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path) //nolint:gosec // test-controlled path
|
||||
files[filepath.ToSlash(rel)] = content
|
||||
|
||||
return err
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// testClient returns the client tests download with. Its retries wait
|
||||
// milliseconds rather than seconds.
|
||||
func testClient() retryingClient {
|
||||
@@ -848,6 +876,108 @@ func TestFetchEscapedPaths(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestFetchTree runs fetch on a tree with nested directories. Every file
|
||||
// the manifest lists must land under its own path with its own content,
|
||||
// and nothing else may be left in the destination.
|
||||
//
|
||||
//nolint:paralleltest // changes the process-global working directory
|
||||
func TestFetchTree(t *testing.T) {
|
||||
files := map[string][]byte{
|
||||
"top.txt": []byte("at the top"),
|
||||
"sub/one.txt": []byte("one level down"),
|
||||
"sub/deeper/two.txt": []byte("two levels down"),
|
||||
"other/deep/est.txt": []byte("in a second directory"),
|
||||
}
|
||||
|
||||
server := httptest.NewServer(fetchTestHandler(manifestOf(t, files), files))
|
||||
defer server.Close()
|
||||
|
||||
dest := chdirTemp(t)
|
||||
|
||||
opts := testOpts([]string{testApp, cmdFetch, "-q", server.URL}, afero.NewOsFs())
|
||||
require.Equal(t, 0, runCLI(opts), testStderr(t, opts))
|
||||
|
||||
assert.Equal(t, files, filesUnder(t, dest))
|
||||
}
|
||||
|
||||
// TestFetchFailsOnHashMismatch runs fetch against a server that serves a
|
||||
// file with the size the manifest lists but different content. fetch must
|
||||
// exit non-zero and leave no file in the destination.
|
||||
//
|
||||
//nolint:paralleltest // changes the process-global working directory
|
||||
func TestFetchFailsOnHashMismatch(t *testing.T) {
|
||||
listed := map[string][]byte{testDirFile: []byte("original")}
|
||||
served := map[string][]byte{testDirFile: []byte("tampered")}
|
||||
|
||||
server := httptest.NewServer(fetchTestHandler(manifestOf(t, listed), served))
|
||||
defer server.Close()
|
||||
|
||||
dest := chdirTemp(t)
|
||||
|
||||
opts := testOpts([]string{testApp, cmdFetch, "-q", server.URL}, afero.NewOsFs())
|
||||
assert.Equal(t, 1, runCLI(opts))
|
||||
assert.Empty(t, filesUnder(t, dest))
|
||||
}
|
||||
|
||||
// TestFetchIntoPartlyFilledDestination runs fetch where an interrupted
|
||||
// fetch of an older version of the tree left one file current, one file
|
||||
// out of date and one half written to its temp file, beside a file the
|
||||
// manifest does not list. fetch downloads every file the manifest lists,
|
||||
// those already present included, and replaces what is there; the file
|
||||
// the manifest does not list is left alone.
|
||||
//
|
||||
//nolint:paralleltest // changes the process-global working directory
|
||||
func TestFetchIntoPartlyFilledDestination(t *testing.T) {
|
||||
files := map[string][]byte{
|
||||
"current.txt": []byte("already fetched"),
|
||||
"sub/changed.txt": []byte("new version"),
|
||||
"sub/partial.txt": []byte("cut off partway"),
|
||||
}
|
||||
unlisted := []byte("not in the manifest")
|
||||
|
||||
tree := fetchTestHandler(manifestOf(t, files), files)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
requested []string
|
||||
)
|
||||
|
||||
server := httptest.NewServer(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
|
||||
requested = append(requested, r.URL.Path)
|
||||
|
||||
mu.Unlock()
|
||||
|
||||
tree.ServeHTTP(w, r)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
dest := chdirTemp(t)
|
||||
|
||||
require.NoError(t, os.MkdirAll("sub", 0o750))
|
||||
require.NoError(t, os.WriteFile("current.txt", files["current.txt"], 0o600))
|
||||
require.NoError(t, os.WriteFile("sub/changed.txt", []byte("old version"), 0o600))
|
||||
require.NoError(t,
|
||||
os.WriteFile(tempPathFor("sub/partial.txt"), []byte("cut off"), 0o600))
|
||||
require.NoError(t, os.WriteFile("unlisted.txt", unlisted, 0o600))
|
||||
|
||||
opts := testOpts([]string{testApp, cmdFetch, "-q", server.URL}, afero.NewOsFs())
|
||||
require.Equal(t, 0, runCLI(opts), testStderr(t, opts))
|
||||
|
||||
want := maps.Clone(files)
|
||||
want["unlisted.txt"] = unlisted
|
||||
assert.Equal(t, want, filesUnder(t, dest))
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
assert.ElementsMatch(t, []string{
|
||||
"/" + defaultManifestName, "/current.txt", "/sub/changed.txt", "/sub/partial.txt",
|
||||
}, requested)
|
||||
}
|
||||
|
||||
// TestFetchTimeoutFlag runs fetch with --timeout against a server that
|
||||
// never answers. Without the flag's limit the request would wait forever;
|
||||
// once fetch gives up on it, the server cancels fetch's context so that
|
||||
|
||||
Reference in New Issue
Block a user