check / check (push) Waiting to run
internal/log now logs through log/slog. After Init, records go to a small slog.Handler that prints the same lines as before; before Init they go to slog.Default(). The helpers and their call sites are unchanged. With NO_COLOR set on a terminal, log lines are no longer colored. pterm is dropped: it only printed progress lines, which fmt now writes directly, outside slog. apex/log, pterm and their indirect dependencies leave go.mod; golang.org/x/term becomes direct. simplelog is not imported: importing it replaces slog's default handler in every program that imports package mfer. The logger stays process-global, since injecting it would change package mfer's API. Progress and log lines now share the logger's lock, so the check progress test runs check ten times to catch a missing wait. Model: opus-5-5
1159 lines
34 KiB
Go
1159 lines
34 KiB
Go
//nolint:testpackage // white-box tests exercise unexported internals
|
|
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
urfcli "github.com/urfave/cli/v2"
|
|
"sneak.berlin/go/mfer/internal/log"
|
|
"sneak.berlin/go/mfer/mfer"
|
|
)
|
|
|
|
const (
|
|
testApp = "mfer"
|
|
testDir = "/testdir"
|
|
testFile1 = "/testdir/file1.txt"
|
|
testMF = "/testdir/test.mf"
|
|
testOutput = "/output.mf"
|
|
testManifest = "/manifest.mf"
|
|
testFlagBase = "--base"
|
|
testFlagNoExtra = "--no-extra-files"
|
|
testFlagVersion = "--version"
|
|
testFlagVerbose = "--verbose"
|
|
)
|
|
|
|
var errSimulatedWrite = errors.New("simulated write failure")
|
|
|
|
// runMu serializes CLI runs: RunWithOptions wires the process-global
|
|
// logger to the run's I/O streams, so parallel runs would cross-wire
|
|
// captured output between tests.
|
|
//
|
|
//nolint:gochecknoglobals // guards process-global logger state in tests
|
|
var runMu sync.Mutex
|
|
|
|
// runCLI invokes RunWithOptions while holding runMu so parallel tests
|
|
// capture their own output, and returns its exit code.
|
|
func runCLI(opts *RunOptions) int {
|
|
exitCode, _ := runCLIWithLevel(opts)
|
|
|
|
return exitCode
|
|
}
|
|
|
|
// runCLIWithLevel is runCLI that also returns the log level the run left
|
|
// set, read while runMu still keeps other runs from changing it. Each run
|
|
// starts at the default level, as a new process does. Before releasing the
|
|
// lock it points the process-global logger at io.Discard: other tests log
|
|
// outside the lock (manifest loads, scans), and those lines must not land in
|
|
// this run's buffers once it has returned and its test is reading them.
|
|
func runCLIWithLevel(opts *RunOptions) (int, log.Level) {
|
|
runMu.Lock()
|
|
defer runMu.Unlock()
|
|
|
|
log.SetLevel(log.InfoLevel)
|
|
|
|
exitCode := RunWithOptions(opts)
|
|
level := log.GetLevel()
|
|
|
|
log.SetOutput(io.Discard, io.Discard)
|
|
log.Init()
|
|
|
|
return exitCode, level
|
|
}
|
|
|
|
func TestMain(m *testing.M) {
|
|
// Prevent urfave/cli from calling os.Exit during tests
|
|
urfcli.OsExiter = func(_ int) {}
|
|
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestBuild(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := &CLIApp{}
|
|
assert.NotNil(t, m)
|
|
}
|
|
|
|
func testOpts(args []string, fs afero.Fs) *RunOptions {
|
|
return &RunOptions{
|
|
Appname: testApp,
|
|
Version: "1.0.0",
|
|
Gitrev: "abc123",
|
|
Args: args,
|
|
Stdin: &bytes.Buffer{},
|
|
Stdout: &bytes.Buffer{},
|
|
Stderr: &bytes.Buffer{},
|
|
Fs: fs,
|
|
}
|
|
}
|
|
|
|
func testStdout(t *testing.T, opts *RunOptions) string {
|
|
t.Helper()
|
|
|
|
buf, ok := opts.Stdout.(*bytes.Buffer)
|
|
require.True(t, ok)
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
func testStderr(t *testing.T, opts *RunOptions) string {
|
|
t.Helper()
|
|
|
|
buf, ok := opts.Stderr.(*bytes.Buffer)
|
|
require.True(t, ok)
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
func writeTestFile(t *testing.T, fs afero.Fs, path, content string) {
|
|
t.Helper()
|
|
|
|
require.NoError(t, afero.WriteFile(fs, path, []byte(content), 0o644))
|
|
}
|
|
|
|
func TestVersionCommand(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
opts := testOpts([]string{testApp, cmdVersion}, fs)
|
|
|
|
exitCode := runCLI(opts)
|
|
|
|
assert.Equal(t, 0, exitCode)
|
|
|
|
stdout := testStdout(t, opts)
|
|
assert.Contains(t, stdout, mfer.Version)
|
|
assert.Contains(t, stdout, "abc123")
|
|
}
|
|
|
|
// TestVFlagCollision covers the -v/--verbose vs --version flag interaction
|
|
// (issue #64). Verbose owns -v; version answers to --version and -V. None of
|
|
// these invocations may produce a parser error, and the two ways of asking
|
|
// for the version must print the same thing.
|
|
func TestVFlagCollision(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Invocations that must print the version and exit 0.
|
|
versionCases := map[string][]string{
|
|
"long version flag": {testApp, testFlagVersion},
|
|
"short version flag": {testApp, "-V"},
|
|
"verbose then version": {testApp, "-v", testFlagVersion},
|
|
"long verbose and version": {testApp, "--verbose", testFlagVersion},
|
|
}
|
|
|
|
for name, args := range versionCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
opts := testOpts(args, afero.NewMemMapFs())
|
|
exitCode := runCLI(opts)
|
|
|
|
assert.Equal(t, 0, exitCode, "stderr: %s", testStderr(t, opts))
|
|
assert.Contains(t, testStdout(t, opts), mfer.Version)
|
|
assert.NotContains(t, testStderr(t, opts), "two forms of the same flag")
|
|
})
|
|
}
|
|
|
|
// Invocations that must enable verbose and exit 0 without a parser error.
|
|
verboseCases := map[string][]string{
|
|
"short verbose flag": {testApp, "-v"},
|
|
"long verbose flag": {testApp, "--verbose"},
|
|
}
|
|
|
|
for name, args := range verboseCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
opts := testOpts(args, afero.NewMemMapFs())
|
|
exitCode := runCLI(opts)
|
|
|
|
assert.Equal(t, 0, exitCode, "stderr: %s", testStderr(t, opts))
|
|
assert.Contains(t, testStdout(t, opts), cmdGenerate,
|
|
"root should show help listing subcommands")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestVersionFlagAndCommandMatch asserts that "mfer --version" and
|
|
// "mfer version" produce identical output (issue #64).
|
|
func TestVersionFlagAndCommandMatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
flagOpts := testOpts([]string{testApp, testFlagVersion}, afero.NewMemMapFs())
|
|
require.Equal(t, 0, runCLI(flagOpts))
|
|
|
|
cmdOpts := testOpts([]string{testApp, cmdVersion}, afero.NewMemMapFs())
|
|
require.Equal(t, 0, runCLI(cmdOpts))
|
|
|
|
assert.Equal(t, testStdout(t, flagOpts), testStdout(t, cmdOpts))
|
|
}
|
|
|
|
// TestRootVerbosityFlags asserts that -q and -v given before any subcommand
|
|
// name take effect: in the root itself, and in the fetch and export
|
|
// subcommands (issue #64). It does not run in parallel: other tests log
|
|
// outside runMu (manifest loads, scans), and a line logged while one of these
|
|
// runs is in progress lands in its output.
|
|
//
|
|
//nolint:paralleltest // see above
|
|
func TestRootVerbosityFlags(t *testing.T) {
|
|
t.Run("quiet with no subcommand hides the banner", func(t *testing.T) {
|
|
loud := testOpts([]string{testApp}, afero.NewMemMapFs())
|
|
require.Equal(t, 0, runCLI(loud))
|
|
assert.Contains(t, testStdout(t, loud), banner)
|
|
|
|
quiet := testOpts([]string{testApp, "-q"}, afero.NewMemMapFs())
|
|
require.Equal(t, 0, runCLI(quiet))
|
|
assert.Contains(t, testStdout(t, quiet), cmdGenerate)
|
|
assert.NotContains(t, testStdout(t, quiet), banner)
|
|
})
|
|
|
|
t.Run("quiet before fetch hides the banner", func(t *testing.T) {
|
|
opts := testOpts([]string{testApp, "-q", cmdFetch}, afero.NewMemMapFs())
|
|
|
|
assert.Equal(t, 1, runCLI(opts))
|
|
assert.Contains(t, testStderr(t, opts), errURLRequired.Error())
|
|
assert.NotContains(t, testStdout(t, opts), banner)
|
|
})
|
|
|
|
t.Run("verbose twice before fetch enables debug logging", func(t *testing.T) {
|
|
opts := testOpts([]string{testApp, "-v", "-v", cmdFetch}, afero.NewMemMapFs())
|
|
|
|
assert.Equal(t, 1, runCLI(opts))
|
|
assert.Contains(t, testStderr(t, opts), "fetchManifestOperation()")
|
|
})
|
|
|
|
t.Run("quiet before export hides the manifest summary", func(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
manifest := buildTestManifest(t, map[string][]byte{"a.txt": []byte("a")})
|
|
require.NoError(t, afero.WriteFile(fs, testManifest, manifest, 0o644))
|
|
|
|
loud := testOpts([]string{testApp, cmdExport, testManifest}, fs)
|
|
require.Equal(t, 0, runCLI(loud))
|
|
assert.Contains(t, testStderr(t, loud), "loaded manifest")
|
|
|
|
quiet := testOpts([]string{testApp, "-q", cmdExport, testManifest}, fs)
|
|
require.Equal(t, 0, runCLI(quiet))
|
|
assert.NotContains(t, testStderr(t, quiet), "loaded manifest")
|
|
})
|
|
}
|
|
|
|
// commandsTakingVerbose returns the command lines -v can follow: the root and
|
|
// the generate, check, freshen and fetch subcommands.
|
|
func commandsTakingVerbose() [][]string {
|
|
return [][]string{
|
|
{testApp},
|
|
{testApp, cmdGenerate},
|
|
{testApp, cmdCheck},
|
|
{testApp, cmdFreshen},
|
|
{testApp, cmdFetch},
|
|
}
|
|
}
|
|
|
|
// TestVerboseCount asserts that one -v or --verbose gives verbose output and
|
|
// two -v give debug output (issue #125). urfave/cli before v2.25.5 counted a
|
|
// flag given by its alias twice, so one -v gave debug output.
|
|
func TestVerboseCount(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
flags []string
|
|
want log.Level
|
|
}{
|
|
{[]string{"-v"}, log.VerboseLevel},
|
|
{[]string{testFlagVerbose}, log.VerboseLevel},
|
|
{[]string{"-v", "-v"}, log.DebugLevel},
|
|
}
|
|
|
|
for _, command := range commandsTakingVerbose() {
|
|
for _, tc := range cases {
|
|
args := slices.Concat(command, tc.flags)
|
|
|
|
t.Run(strings.Join(args, " "), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, level := runCLIWithLevel(testOpts(args, afero.NewMemMapFs()))
|
|
assert.Equal(t, tc.want, level)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCombinedShortVerboseRefused asserts that -vv is refused (issue #125):
|
|
// single-letter flags do not combine, so the -v help text says -v -v.
|
|
func TestCombinedShortVerboseRefused(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, command := range commandsTakingVerbose() {
|
|
args := slices.Concat(command, []string{"-vv"})
|
|
|
|
t.Run(strings.Join(args, " "), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
opts := testOpts(args, afero.NewMemMapFs())
|
|
exitCode, level := runCLIWithLevel(opts)
|
|
|
|
assert.Equal(t, 1, exitCode)
|
|
assert.Contains(t, testStderr(t, opts), "flag provided but not defined: -vv")
|
|
assert.Equal(t, log.InfoLevel, level)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestShortAndLongVerboseRefused asserts that -v and --verbose given together
|
|
// are refused (issue #125): urfave/cli v2 refuses a flag given under two of its
|
|
// names, and one flag with an alias keeps help and parsing simple.
|
|
func TestShortAndLongVerboseRefused(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, command := range commandsTakingVerbose() {
|
|
args := slices.Concat(command, []string{"-v", testFlagVerbose})
|
|
|
|
t.Run(strings.Join(args, " "), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
opts := testOpts(args, afero.NewMemMapFs())
|
|
exitCode, level := runCLIWithLevel(opts)
|
|
|
|
assert.Equal(t, 1, exitCode)
|
|
assert.Contains(t, testStderr(t, opts), "Cannot use two forms of the same flag")
|
|
assert.Equal(t, log.InfoLevel, level)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHelpCommand(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
opts := testOpts([]string{testApp, "--help"}, fs)
|
|
|
|
exitCode := runCLI(opts)
|
|
|
|
assert.Equal(t, 0, exitCode)
|
|
|
|
stdout := testStdout(t, opts)
|
|
assert.Contains(t, stdout, cmdGenerate)
|
|
assert.Contains(t, stdout, cmdCheck)
|
|
assert.Contains(t, stdout, cmdFetch)
|
|
}
|
|
|
|
func TestGenerateCommand(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create test files in memory filesystem
|
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
writeTestFile(t, fs, testFile1, "hello world")
|
|
writeTestFile(t, fs, "/testdir/file2.txt", "test content")
|
|
|
|
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testMF, testDir}, fs)
|
|
|
|
exitCode := runCLI(opts)
|
|
|
|
assert.Equal(t, 0, exitCode, "stderr: %s", testStderr(t, opts))
|
|
|
|
// Verify manifest was created
|
|
exists, err := afero.Exists(fs, testMF)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
}
|
|
|
|
func TestGenerateAndCheckCommand(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create test files with subdirectory
|
|
require.NoError(t, fs.MkdirAll("/testdir/subdir", 0o755))
|
|
writeTestFile(t, fs, testFile1, "hello world")
|
|
writeTestFile(t, fs, "/testdir/subdir/file2.txt", "test content")
|
|
|
|
// Generate manifest
|
|
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testMF, testDir}, fs)
|
|
exitCode := runCLI(opts)
|
|
require.Equal(t, 0, exitCode, "generate failed: %s", testStderr(t, opts))
|
|
|
|
// Check manifest
|
|
opts = testOpts([]string{testApp, cmdCheck, "-q", testFlagBase, testDir, testMF}, fs)
|
|
exitCode = runCLI(opts)
|
|
assert.Equal(t, 0, exitCode, "check failed: %s", testStderr(t, opts))
|
|
}
|
|
|
|
// sharedWriter appends to a buffer shared with other sharedWriters, so
|
|
// output written to stdout and stderr is kept in the order it was written.
|
|
// Each write first waits for delay.
|
|
type sharedWriter struct {
|
|
mu *sync.Mutex
|
|
buf *bytes.Buffer
|
|
delay time.Duration
|
|
}
|
|
|
|
func (w sharedWriter) Write(p []byte) (int, error) {
|
|
time.Sleep(w.delay)
|
|
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
return w.buf.Write(p)
|
|
}
|
|
|
|
// TestCheckClearsProgressBeforeSummary asserts that check --progress writes
|
|
// its last progress line and clears it before it logs the summary. Progress
|
|
// writes are slowed down, so a progress goroutine that check did not wait for
|
|
// would write after the summary, or after the run has returned.
|
|
//
|
|
// Progress lines and log lines take the logger's lock, and when the progress
|
|
// goroutine gets it first the clear lands before the summary even without the
|
|
// wait. Which goroutine gets it first changes from run to run, so check runs
|
|
// ten times.
|
|
func TestCheckClearsProgressBeforeSummary(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
writeTestFile(t, fs, testFile1, "hello world")
|
|
|
|
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testMF, testDir}, fs)
|
|
require.Equal(t, 0, runCLI(opts), "generate failed: %s", testStderr(t, opts))
|
|
|
|
for range 10 {
|
|
var (
|
|
mu sync.Mutex
|
|
output bytes.Buffer
|
|
)
|
|
|
|
opts = testOpts([]string{
|
|
testApp, cmdCheck, "--progress", testFlagBase, testDir, testMF,
|
|
}, fs)
|
|
opts.Stdout = sharedWriter{mu: &mu, buf: &output, delay: 100 * time.Millisecond}
|
|
opts.Stderr = sharedWriter{mu: &mu, buf: &output}
|
|
require.Equal(t, 0, runCLI(opts))
|
|
|
|
mu.Lock()
|
|
got := output.String()
|
|
mu.Unlock()
|
|
|
|
lastProgress := strings.Index(got, "Checking: 1/1 files")
|
|
progressDone := strings.Index(got, "\r\033[K")
|
|
summary := strings.Index(got, "checked 1 files")
|
|
|
|
require.NotEqual(t, -1, lastProgress, "no last progress line in %q", got)
|
|
require.NotEqual(t, -1, progressDone, "progress line never cleared in %q", got)
|
|
require.NotEqual(t, -1, summary, "no summary in %q", got)
|
|
require.Less(t, lastProgress, progressDone,
|
|
"progress cleared before its last line in %q", got)
|
|
require.Less(t, progressDone, summary,
|
|
"summary logged before progress was cleared in %q", got)
|
|
}
|
|
}
|
|
|
|
func TestCheckCommandWithMissingFile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create test file
|
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
writeTestFile(t, fs, testFile1, "hello world")
|
|
|
|
// Generate manifest
|
|
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testMF, testDir}, fs)
|
|
exitCode := runCLI(opts)
|
|
require.Equal(t, 0, exitCode, "generate failed: %s", testStderr(t, opts))
|
|
|
|
// Delete the file
|
|
require.NoError(t, fs.Remove(testFile1))
|
|
|
|
// Check manifest - should fail
|
|
opts = testOpts([]string{testApp, cmdCheck, "-q", testFlagBase, testDir, testMF}, fs)
|
|
exitCode = runCLI(opts)
|
|
assert.Equal(t, 1, exitCode, "check should have failed for missing file")
|
|
}
|
|
|
|
func runCheckAfterRewrite(t *testing.T, rewritten, msg string) {
|
|
t.Helper()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create test file
|
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
writeTestFile(t, fs, testFile1, "hello world")
|
|
|
|
// Generate manifest
|
|
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testMF, testDir}, fs)
|
|
exitCode := runCLI(opts)
|
|
require.Equal(t, 0, exitCode, "generate failed: %s", testStderr(t, opts))
|
|
|
|
// Rewrite the file, then check the manifest - it must fail
|
|
writeTestFile(t, fs, testFile1, rewritten)
|
|
|
|
opts = testOpts([]string{testApp, cmdCheck, "-q", testFlagBase, testDir, testMF}, fs)
|
|
exitCode = runCLI(opts)
|
|
assert.Equal(t, 1, exitCode, msg)
|
|
}
|
|
|
|
func TestCheckCommandWithCorruptedFile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Corrupt the file (change content but keep same size)
|
|
runCheckAfterRewrite(t, "HELLO WORLD",
|
|
"check should have failed for corrupted file")
|
|
}
|
|
|
|
func TestCheckCommandWithSizeMismatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Change file size
|
|
runCheckAfterRewrite(t, "different size content here",
|
|
"check should have failed for size mismatch")
|
|
}
|
|
|
|
func TestBannerOutput(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create test file
|
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
writeTestFile(t, fs, testFile1, "hello")
|
|
|
|
// Run without -q to see banner
|
|
opts := testOpts([]string{testApp, cmdGenerate, "-o", testMF, testDir}, fs)
|
|
exitCode := runCLI(opts)
|
|
assert.Equal(t, 0, exitCode)
|
|
|
|
// Banner ASCII art should be in stdout
|
|
stdout := testStdout(t, opts)
|
|
assert.Contains(t, stdout, "___")
|
|
assert.Contains(t, stdout, "\\")
|
|
}
|
|
|
|
func TestUnknownCommand(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
opts := testOpts([]string{testApp, "unknown"}, fs)
|
|
|
|
exitCode := runCLI(opts)
|
|
assert.Equal(t, 1, exitCode)
|
|
}
|
|
|
|
func TestGenerateExcludesDotfilesByDefault(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create test files including dotfiles
|
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
writeTestFile(t, fs, testFile1, "hello")
|
|
writeTestFile(t, fs, "/testdir/.hidden", "secret")
|
|
|
|
// Generate manifest without --include-dotfiles (default excludes dotfiles)
|
|
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testMF, testDir}, fs)
|
|
exitCode := runCLI(opts)
|
|
require.Equal(t, 0, exitCode)
|
|
|
|
// Check that manifest exists
|
|
exists, _ := afero.Exists(fs, testMF)
|
|
assert.True(t, exists)
|
|
|
|
// Verify manifest only has 1 file (the non-dotfile)
|
|
manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{
|
|
Path: testMF,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Len(t, manifest.Files(), 1)
|
|
assert.Equal(t, "file1.txt", manifest.Files()[0].GetPath())
|
|
}
|
|
|
|
func TestGenerateWithIncludeDotfiles(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create test files including dotfiles
|
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
writeTestFile(t, fs, testFile1, "hello")
|
|
writeTestFile(t, fs, "/testdir/.hidden", "secret")
|
|
|
|
// Generate manifest with --include-dotfiles
|
|
opts := testOpts([]string{
|
|
testApp, cmdGenerate, "-q", "--include-dotfiles", "-o", testMF, testDir,
|
|
}, fs)
|
|
exitCode := runCLI(opts)
|
|
require.Equal(t, 0, exitCode)
|
|
|
|
// Verify manifest has 2 files (including dotfile)
|
|
manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{
|
|
Path: testMF,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Len(t, manifest.Files(), 2)
|
|
}
|
|
|
|
func TestMultipleInputPaths(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create test files in multiple directories
|
|
require.NoError(t, fs.MkdirAll("/dir1", 0o755))
|
|
require.NoError(t, fs.MkdirAll("/dir2", 0o755))
|
|
writeTestFile(t, fs, "/dir1/file1.txt", "content1")
|
|
writeTestFile(t, fs, "/dir2/file2.txt", "content2")
|
|
|
|
// Generate manifest from multiple paths
|
|
opts := testOpts([]string{
|
|
testApp, cmdGenerate, "-q", "-o", testOutput, "/dir1", "/dir2",
|
|
}, fs)
|
|
exitCode := runCLI(opts)
|
|
assert.Equal(t, 0, exitCode, "stderr: %s", testStderr(t, opts))
|
|
|
|
exists, _ := afero.Exists(fs, testOutput)
|
|
assert.True(t, exists)
|
|
}
|
|
|
|
func TestNoExtraFilesPass(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create test files
|
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
writeTestFile(t, fs, testFile1, "hello")
|
|
writeTestFile(t, fs, "/testdir/file2.txt", "world")
|
|
|
|
// Generate manifest
|
|
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testManifest, testDir}, fs)
|
|
exitCode := runCLI(opts)
|
|
require.Equal(t, 0, exitCode)
|
|
|
|
// Check with --no-extra-files (should pass - no extra files)
|
|
opts = testOpts([]string{
|
|
testApp, cmdCheck, "-q", testFlagNoExtra, testFlagBase, testDir, testManifest,
|
|
}, fs)
|
|
exitCode = runCLI(opts)
|
|
assert.Equal(t, 0, exitCode)
|
|
}
|
|
|
|
func TestNoExtraFilesFail(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create test files
|
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
writeTestFile(t, fs, testFile1, "hello")
|
|
|
|
// Generate manifest
|
|
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testManifest, testDir}, fs)
|
|
exitCode := runCLI(opts)
|
|
require.Equal(t, 0, exitCode)
|
|
|
|
// Add an extra file after manifest generation
|
|
writeTestFile(t, fs, "/testdir/extra.txt", "extra")
|
|
|
|
// Check with --no-extra-files (should fail - extra file exists)
|
|
opts = testOpts([]string{
|
|
testApp, cmdCheck, "-q", testFlagNoExtra, testFlagBase, testDir, testManifest,
|
|
}, fs)
|
|
exitCode = runCLI(opts)
|
|
assert.Equal(t, 1, exitCode, "check should fail when extra files exist")
|
|
}
|
|
|
|
func TestNoExtraFilesWithSubdirectory(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create test files with subdirectory
|
|
require.NoError(t, fs.MkdirAll("/testdir/subdir", 0o755))
|
|
writeTestFile(t, fs, testFile1, "hello")
|
|
writeTestFile(t, fs, "/testdir/subdir/file2.txt", "world")
|
|
|
|
// Generate manifest
|
|
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testManifest, testDir}, fs)
|
|
exitCode := runCLI(opts)
|
|
require.Equal(t, 0, exitCode)
|
|
|
|
// Add extra file in subdirectory
|
|
writeTestFile(t, fs, "/testdir/subdir/extra.txt", "extra")
|
|
|
|
// Check with --no-extra-files (should fail)
|
|
opts = testOpts([]string{
|
|
testApp, cmdCheck, "-q", testFlagNoExtra, testFlagBase, testDir, testManifest,
|
|
}, fs)
|
|
exitCode = runCLI(opts)
|
|
assert.Equal(t, 1, exitCode,
|
|
"check should fail when extra files exist in subdirectory")
|
|
}
|
|
|
|
func TestCheckWithoutNoExtraFilesIgnoresExtra(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create test file
|
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
writeTestFile(t, fs, testFile1, "hello")
|
|
|
|
// Generate manifest
|
|
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testManifest, testDir}, fs)
|
|
exitCode := runCLI(opts)
|
|
require.Equal(t, 0, exitCode)
|
|
|
|
// Add extra file
|
|
writeTestFile(t, fs, "/testdir/extra.txt", "extra")
|
|
|
|
// Check WITHOUT --no-extra-files (should pass - extra files ignored)
|
|
opts = testOpts([]string{
|
|
testApp, cmdCheck, "-q", testFlagBase, testDir, testManifest,
|
|
}, fs)
|
|
exitCode = runCLI(opts)
|
|
assert.Equal(t, 0, exitCode,
|
|
"check without --no-extra-files should ignore extra files")
|
|
}
|
|
|
|
func TestGenerateAtomicWriteNoTempFileOnSuccess(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create test file
|
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
writeTestFile(t, fs, testFile1, "hello")
|
|
|
|
// Generate manifest
|
|
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testOutput, testDir}, fs)
|
|
exitCode := runCLI(opts)
|
|
require.Equal(t, 0, exitCode)
|
|
|
|
// Verify output file exists
|
|
exists, err := afero.Exists(fs, testOutput)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists, "output file should exist")
|
|
|
|
// Verify temp file does NOT exist
|
|
tmpExists, err := afero.Exists(fs, manifestTempPath(testOutput))
|
|
require.NoError(t, err)
|
|
assert.False(t, tmpExists,
|
|
"temp file should not exist after successful generation")
|
|
}
|
|
|
|
func TestGenerateAtomicWriteOverwriteWithForce(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create test file
|
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
writeTestFile(t, fs, testFile1, "hello")
|
|
|
|
// Create existing manifest with different content
|
|
writeTestFile(t, fs, testOutput, "old content")
|
|
|
|
// Generate manifest with --force
|
|
opts := testOpts([]string{
|
|
testApp, cmdGenerate, "-q", "-f", "-o", testOutput, testDir,
|
|
}, fs)
|
|
exitCode := runCLI(opts)
|
|
require.Equal(t, 0, exitCode)
|
|
|
|
// Verify output file exists and was overwritten
|
|
content, err := afero.ReadFile(fs, testOutput)
|
|
require.NoError(t, err)
|
|
assert.NotEqual(t, "old content", string(content),
|
|
"manifest should be overwritten")
|
|
|
|
// Verify temp file does NOT exist
|
|
tmpExists, err := afero.Exists(fs, manifestTempPath(testOutput))
|
|
require.NoError(t, err)
|
|
assert.False(t, tmpExists,
|
|
"temp file should not exist after successful generation")
|
|
}
|
|
|
|
func TestGenerateFailsWithoutForceWhenOutputExists(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create test file
|
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
writeTestFile(t, fs, testFile1, "hello")
|
|
|
|
// Create existing manifest
|
|
writeTestFile(t, fs, testOutput, "existing")
|
|
|
|
// Generate manifest WITHOUT --force (should fail)
|
|
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testOutput, testDir}, fs)
|
|
exitCode := runCLI(opts)
|
|
assert.Equal(t, 1, exitCode, "should fail when output exists without --force")
|
|
|
|
// Verify original content is preserved
|
|
content, err := afero.ReadFile(fs, testOutput)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "existing", string(content), "original file should be preserved")
|
|
}
|
|
|
|
// manifestPaths returns the file paths listed by the manifest at path.
|
|
func manifestPaths(t *testing.T, fs afero.Fs, path string) []string {
|
|
t.Helper()
|
|
|
|
manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{
|
|
Path: path,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
paths := make([]string, 0, len(manifest.Files()))
|
|
for _, f := range manifest.Files() {
|
|
paths = append(paths, f.GetPath())
|
|
}
|
|
|
|
return paths
|
|
}
|
|
|
|
// TestGenerateLeavesOutputOutOfListing overwrites an output file inside the
|
|
// scanned tree with --force: the old file is not listed, even when the tree
|
|
// is named through a symlink, while a file named index.mf in a subdirectory
|
|
// still is. The output file is recognized by file identity, which needs
|
|
// the real filesystem.
|
|
func TestGenerateLeavesOutputOutOfListing(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Paths are relative to a temp dir holding data/tree and link, a
|
|
// symlink to data.
|
|
for name, tc := range map[string]struct{ input, output string }{
|
|
"default name": {"data/tree", "data/tree/index.mf"},
|
|
"other name in a subdirectory": {"data/tree", "data/tree/sub/listing.mf"},
|
|
"tree named through a symlink": {"link/tree", "data/tree/index.mf"},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
root := t.TempDir()
|
|
tree := filepath.Join(root, "data", "tree")
|
|
output := filepath.Join(root, tc.output)
|
|
|
|
fs := afero.NewOsFs()
|
|
require.NoError(t, fs.MkdirAll(filepath.Join(tree, "sub"), 0o750))
|
|
require.NoError(t,
|
|
os.Symlink(filepath.Join(root, "data"), filepath.Join(root, "link")))
|
|
writeTestFile(t, fs, filepath.Join(tree, testFileTxt), "hello")
|
|
writeTestFile(t, fs, filepath.Join(tree, "sub", "index.mf"), "an ordinary file")
|
|
writeTestFile(t, fs, output, "previous manifest")
|
|
|
|
opts := testOpts([]string{
|
|
testApp, cmdGenerate, "-q", "--force", "-o", output, filepath.Join(root, tc.input),
|
|
}, fs)
|
|
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
|
|
|
assert.ElementsMatch(t, []string{testFileTxt, "sub/index.mf"},
|
|
manifestPaths(t, fs, output))
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGenerateDefaultOutputLeftOutOfListing runs gen with --force and no
|
|
// other arguments, so it scans the current directory and writes the
|
|
// relative path index.mf: the index.mf already there is not listed.
|
|
//
|
|
//nolint:paralleltest // changes the process-global working directory
|
|
func TestGenerateDefaultOutputLeftOutOfListing(t *testing.T) {
|
|
chdirTemp(t)
|
|
|
|
fs := afero.NewOsFs()
|
|
writeTestFile(t, fs, testFileTxt, "hello")
|
|
writeTestFile(t, fs, "index.mf", "previous manifest")
|
|
|
|
opts := testOpts([]string{testApp, cmdGenerate, "-q", "--force"}, fs)
|
|
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
|
|
|
assert.Equal(t, []string{testFileTxt}, manifestPaths(t, fs, "index.mf"))
|
|
}
|
|
|
|
// TestGenerateLeavesLeftoverTempFileOutOfListing runs gen where an
|
|
// interrupted run left its temp file beside the output: the leftover is
|
|
// not listed, and gen does not fail when it overwrites it.
|
|
func TestGenerateLeavesLeftoverTempFileOutOfListing(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
root := t.TempDir()
|
|
output := filepath.Join(root, "index.mf")
|
|
|
|
fs := afero.NewOsFs()
|
|
writeTestFile(t, fs, filepath.Join(root, testFileTxt), "hello")
|
|
writeTestFile(t, fs, manifestTempPath(output), "part of a manifest")
|
|
|
|
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", output, root}, fs)
|
|
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
|
|
|
assert.Equal(t, []string{testFileTxt}, manifestPaths(t, fs, output))
|
|
}
|
|
|
|
func TestGenerateAtomicWriteUsesTemp(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// This test verifies that generate uses a temp file by checking
|
|
// that the output file doesn't exist until generation completes.
|
|
// We do this by generating to a path and verifying the temp file
|
|
// pattern is used (output.mf.tmp -> output.mf)
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create test file
|
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
writeTestFile(t, fs, testFile1, "hello")
|
|
|
|
// Generate manifest
|
|
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testOutput, testDir}, fs)
|
|
exitCode := runCLI(opts)
|
|
require.Equal(t, 0, exitCode)
|
|
|
|
// Both output file should exist and temp should not
|
|
exists, _ := afero.Exists(fs, testOutput)
|
|
assert.True(t, exists, "output file should exist")
|
|
|
|
tmpExists, _ := afero.Exists(fs, manifestTempPath(testOutput))
|
|
assert.False(t, tmpExists, "temp file should be cleaned up")
|
|
|
|
// Verify manifest is valid (not empty)
|
|
content, err := afero.ReadFile(fs, testOutput)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, content, "manifest should not be empty")
|
|
}
|
|
|
|
// failingWriterFs wraps a filesystem and makes writes fail after N bytes
|
|
type failingWriterFs struct {
|
|
afero.Fs
|
|
|
|
failAfter int64
|
|
written int64
|
|
}
|
|
|
|
type failingFile struct {
|
|
afero.File
|
|
|
|
fs *failingWriterFs
|
|
}
|
|
|
|
func (f *failingFile) Write(p []byte) (int, error) {
|
|
f.fs.written += int64(len(p))
|
|
if f.fs.written > f.fs.failAfter {
|
|
return 0, errSimulatedWrite
|
|
}
|
|
|
|
return f.File.Write(p)
|
|
}
|
|
|
|
//nolint:ireturn // Create must return afero.File to satisfy afero.Fs.
|
|
func (fs *failingWriterFs) Create(name string) (afero.File, error) {
|
|
f, err := fs.Fs.Create(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &failingFile{File: f, fs: fs}, nil
|
|
}
|
|
|
|
func TestGenerateAtomicWriteCleansUpOnError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
baseFs := afero.NewMemMapFs()
|
|
|
|
// Create test files - need enough content to trigger the write failure
|
|
require.NoError(t, baseFs.MkdirAll(testDir, 0o755))
|
|
writeTestFile(t, baseFs, testFile1, "hello world this is a test file")
|
|
|
|
// Wrap with failing writer that fails after writing some bytes
|
|
fs := &failingWriterFs{Fs: baseFs, failAfter: 10}
|
|
|
|
// Generate manifest - should fail during write
|
|
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testOutput, testDir}, fs)
|
|
exitCode := runCLI(opts)
|
|
assert.Equal(t, 1, exitCode, "should fail due to write error")
|
|
|
|
// With atomic writes: output.mf should NOT exist (temp was cleaned up)
|
|
// With non-atomic writes: output.mf WOULD exist (partial/empty)
|
|
exists, _ := afero.Exists(baseFs, testOutput)
|
|
assert.False(t, exists,
|
|
"output file should not exist after failed generation (atomic write)")
|
|
|
|
// Temp file should also not exist
|
|
tmpExists, _ := afero.Exists(baseFs, manifestTempPath(testOutput))
|
|
assert.False(t, tmpExists,
|
|
"temp file should be cleaned up after failed generation")
|
|
}
|
|
|
|
func TestGenerateValidatesInputPaths(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
seedValidDir := func(t *testing.T, fs afero.Fs) {
|
|
t.Helper()
|
|
|
|
require.NoError(t, fs.MkdirAll("/validdir", 0o755))
|
|
writeTestFile(t, fs, "/validdir/file.txt", "content")
|
|
}
|
|
|
|
t.Run("nonexistent path fails fast", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
seedValidDir(t, fs)
|
|
|
|
opts := testOpts([]string{
|
|
testApp, cmdGenerate, "-q", "-o", testOutput, "/nonexistent",
|
|
}, fs)
|
|
exitCode := runCLI(opts)
|
|
assert.Equal(t, 1, exitCode)
|
|
|
|
stderr := testStderr(t, opts)
|
|
assert.Contains(t, stderr, "path does not exist")
|
|
assert.Contains(t, stderr, "/nonexistent")
|
|
})
|
|
|
|
t.Run("mix of valid and invalid paths fails fast", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
seedValidDir(t, fs)
|
|
|
|
opts := testOpts([]string{
|
|
testApp, cmdGenerate, "-q", "-o", testOutput,
|
|
"/validdir", "/alsononexistent",
|
|
}, fs)
|
|
exitCode := runCLI(opts)
|
|
assert.Equal(t, 1, exitCode)
|
|
|
|
stderr := testStderr(t, opts)
|
|
assert.Contains(t, stderr, "path does not exist")
|
|
assert.Contains(t, stderr, "/alsononexistent")
|
|
|
|
// Output file should not have been created
|
|
exists, _ := afero.Exists(fs, testOutput)
|
|
assert.False(t, exists,
|
|
"output file should not exist when path validation fails")
|
|
})
|
|
|
|
t.Run("valid paths succeed", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
seedValidDir(t, fs)
|
|
|
|
opts := testOpts([]string{
|
|
testApp, cmdGenerate, "-q", "-o", testOutput, "/validdir",
|
|
}, fs)
|
|
exitCode := runCLI(opts)
|
|
assert.Equal(t, 0, exitCode)
|
|
})
|
|
}
|
|
|
|
func TestCheckDetectsManifestCorruption(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
rng := rand.New(rand.NewSource(42)) //nolint:gosec // deterministic test data
|
|
|
|
// Create many small files with random names so the manifest has many
|
|
// entries and random single-byte flips land at varied offsets. Each
|
|
// manifest entry is roughly 50-60 bytes. Kept modest so the suite stays
|
|
// within its wall-clock budget under -race.
|
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
|
|
numFiles := 1500
|
|
for range numFiles {
|
|
// Generate random filename
|
|
filename := fmt.Sprintf("/testdir/%08x%08x%08x.dat",
|
|
rng.Uint32(), rng.Uint32(), rng.Uint32())
|
|
// Small random content
|
|
content := make([]byte, 16+rng.Intn(48))
|
|
_, _ = rng.Read(content)
|
|
require.NoError(t, afero.WriteFile(fs, filename, content, 0o644))
|
|
}
|
|
|
|
// Generate manifest outside of testdir
|
|
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testManifest, testDir}, fs)
|
|
exitCode := runCLI(opts)
|
|
require.Equal(t, 0, exitCode, "generate should succeed")
|
|
|
|
// Read the valid manifest and verify it has real size.
|
|
validManifest, err := afero.ReadFile(fs, testManifest)
|
|
require.NoError(t, err)
|
|
require.GreaterOrEqual(t, len(validManifest), 64*1024,
|
|
"manifest should be at least 64KB, got %d bytes", len(validManifest))
|
|
t.Logf("manifest size: %d bytes (%d files)", len(validManifest), numFiles)
|
|
|
|
// First corruption: truncate the manifest
|
|
require.NoError(t, afero.WriteFile(fs, testManifest,
|
|
validManifest[:len(validManifest)/2], 0o644))
|
|
|
|
// Check should fail with truncated manifest
|
|
opts = testOpts([]string{
|
|
testApp, cmdCheck, "-q", testFlagBase, testDir, testManifest,
|
|
}, fs)
|
|
exitCode = runCLI(opts)
|
|
assert.Equal(t, 1, exitCode, "check should fail with truncated manifest")
|
|
|
|
// Verify check passes with valid manifest
|
|
require.NoError(t, afero.WriteFile(fs, testManifest, validManifest, 0o644))
|
|
|
|
opts = testOpts([]string{
|
|
testApp, cmdCheck, "-q", testFlagBase, testDir, testManifest,
|
|
}, fs)
|
|
exitCode = runCLI(opts)
|
|
require.Equal(t, 0, exitCode, "check should pass with valid manifest")
|
|
|
|
// Now do 100 random corruption iterations
|
|
for i := range 100 {
|
|
// Corrupt: write a random byte at a random offset
|
|
corrupted := make([]byte, len(validManifest))
|
|
copy(corrupted, validManifest)
|
|
|
|
offset := rng.Intn(len(corrupted))
|
|
originalByte := corrupted[offset]
|
|
|
|
// Make sure we actually change the byte
|
|
buf := make([]byte, 1)
|
|
|
|
newByte := originalByte
|
|
for newByte == originalByte {
|
|
_, _ = rng.Read(buf)
|
|
newByte = buf[0]
|
|
}
|
|
|
|
corrupted[offset] = newByte
|
|
|
|
require.NoError(t, afero.WriteFile(fs, testManifest, corrupted, 0o644))
|
|
|
|
// Check should fail with corrupted manifest
|
|
opts = testOpts([]string{
|
|
testApp, cmdCheck, "-q", testFlagBase, testDir, testManifest,
|
|
}, fs)
|
|
exitCode = runCLI(opts)
|
|
assert.Equal(t, 1, exitCode,
|
|
"iteration %d: check should fail with corrupted manifest "+
|
|
"(offset %d, 0x%02x -> 0x%02x)",
|
|
i, offset, originalByte, newByte)
|
|
|
|
// Restore valid manifest for next iteration
|
|
require.NoError(t, afero.WriteFile(fs, testManifest, validManifest, 0o644))
|
|
}
|
|
}
|