Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cf0d5c44e | ||
|
|
88a2aa7953 | ||
|
|
098ed80990 |
No files matched your search
@@ -1041,20 +1041,37 @@ is `git describe --tags --always --dirty`, which `script/version` runs for the
|
|||||||
A `docker build .` of a clone, with no build arguments, runs the same
|
A `docker build .` of a clone, with no build arguments, runs the same
|
||||||
`git describe` (without `--dirty`) on the `.git` in its build context, so it
|
`git describe` (without `--dirty`) on the `.git` in its build context, so it
|
||||||
stamps the same value for a clean commit; the build fails if the context carries
|
stamps the same value for a clean commit; the build fails if the context carries
|
||||||
`.git` and no version, commit or commit date comes out. A binary built without
|
`.git` and no version, commit or commit date comes out. `make` outside a git
|
||||||
git metadata reports `dev`, or `unknown` when `script/docker` or
|
checkout and `docker build .` without `.git` both stamp `dev`. `script/docker`
|
||||||
`script/cibuild` built it outside a git checkout.
|
and `script/cibuild` stamp `unknown` when they run outside a git checkout.
|
||||||
|
|
||||||
|
A binary built by plain `go install` or `go build` carries no stamped version.
|
||||||
|
It reports the version Go recorded in it instead:
|
||||||
|
|
||||||
|
- a `go install` of the tag `v1.0.0` → `v1.0.0`.
|
||||||
|
- a `go build` of an untagged commit → a Go pseudo-version, such as
|
||||||
|
`v1.0.1-0.20261110231226-0123456789ab`.
|
||||||
|
- a build where Go recorded no version → `dev`.
|
||||||
|
|
||||||
`goreleaser` stamps a release binary with the tag minus its leading `v`, so the
|
`goreleaser` stamps a release binary with the tag minus its leading `v`, so the
|
||||||
tag `v1.0.0` produces `vaultik 1.0.0`, matching the archive name
|
tag `v1.0.0` produces `vaultik 1.0.0`, matching the archive name
|
||||||
`vaultik_1.0.0_linux_amd64.tar.gz`. `goreleaser --snapshot` stamps
|
`vaultik_1.0.0_linux_amd64.tar.gz`. `goreleaser --snapshot` stamps
|
||||||
`dev-<12 chars of the commit sha>` rather than inventing the next patch number.
|
`dev-<12 chars of the commit sha>` rather than inventing the next patch number.
|
||||||
`vaultik version` calls a build a development build when its version is `dev`,
|
|
||||||
`unknown`, `dev-<sha>`, the short commit sha or `<tag>-<N>-g<short sha>`, with
|
`vaultik version` calls a build a development build when its version is one of
|
||||||
or without `-dirty`; only a plain tag, such as `v1.0.0` or `1.0.0`, is a
|
these:
|
||||||
release. If `script/version` cannot be run at all, `make` stops with an error
|
|
||||||
instead of building an unversioned binary, and a binary that somehow carries an
|
- `dev` or `unknown`.
|
||||||
empty version string still reports itself as a development build.
|
- `dev-<sha>`.
|
||||||
|
- the short commit sha.
|
||||||
|
- `<tag>-<N>-g<short sha>`.
|
||||||
|
- a Go pseudo-version.
|
||||||
|
- any version with a `-dirty` or `+dirty` suffix.
|
||||||
|
|
||||||
|
Only a plain tag, such as `v1.0.0` or `1.0.0`, is a release. If `script/version`
|
||||||
|
cannot be run at all, `make` stops with an error instead of building an
|
||||||
|
unversioned binary, and a binary that somehow carries an empty version string
|
||||||
|
still reports itself as a development build.
|
||||||
|
|
||||||
### cutting a release
|
### cutting a release
|
||||||
|
|
||||||
|
|||||||
+3
-1205
File diff suppressed because it is too large.
Load diff
@@ -24,6 +24,7 @@ require (
|
|||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
go.uber.org/dig v1.19.0
|
go.uber.org/dig v1.19.0
|
||||||
go.uber.org/fx v1.24.0
|
go.uber.org/fx v1.24.0
|
||||||
|
golang.org/x/mod v0.29.0
|
||||||
golang.org/x/sync v0.18.0
|
golang.org/x/sync v0.18.0
|
||||||
golang.org/x/sys v0.38.0
|
golang.org/x/sys v0.38.0
|
||||||
golang.org/x/term v0.37.0
|
golang.org/x/term v0.37.0
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const shortCommitLen = 12
|
|||||||
const exitCodeInterrupted = 130
|
const exitCodeInterrupted = 130
|
||||||
|
|
||||||
// Entry is the main entry point for the CLI application.
|
// Entry is the main entry point for the CLI application.
|
||||||
|
// It first calls globals.UseBuildInfo, which fills in the version,
|
||||||
|
// commit and commit date that the linker flags did not set.
|
||||||
// It prints the startup banner to stderr (unless a banner-suppressing
|
// It prints the startup banner to stderr (unless a banner-suppressing
|
||||||
// flag is present in os.Args — see bannerSuppressedInArgs), executes the
|
// flag is present in os.Args — see bannerSuppressedInArgs), executes the
|
||||||
// root cobra command, and routes any returned error through the
|
// root cobra command, and routes any returned error through the
|
||||||
@@ -33,6 +35,7 @@ const exitCodeInterrupted = 130
|
|||||||
// deferred profile writers run before the process ends. See run in
|
// deferred profile writers run before the process ends. See run in
|
||||||
// cmd/vaultik/main.go.
|
// cmd/vaultik/main.go.
|
||||||
func Entry() int {
|
func Entry() int {
|
||||||
|
globals.UseBuildInfo()
|
||||||
emitStartupBanner(os.Args[1:], os.Stderr)
|
emitStartupBanner(os.Args[1:], os.Stderr)
|
||||||
|
|
||||||
rootCmd := NewRootCommand()
|
rootCmd := NewRootCommand()
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
// Package globals holds application-wide metadata (name, version,
|
// Package globals holds application-wide metadata (name, version,
|
||||||
// commit) that is populated at build time via linker flags.
|
// commit) that is populated at build time via linker flags, or from the
|
||||||
|
// build info Go records in the binary when the linker flags set none.
|
||||||
package globals
|
package globals
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/mod/module"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Appname is the application name, populated from main().
|
// Appname is the application name, populated from main().
|
||||||
var Appname = "vaultik" //nolint:gochecknoglobals // set via -ldflags at build time
|
var Appname = "vaultik" //nolint:gochecknoglobals // set via -ldflags at build time
|
||||||
|
|
||||||
// DevVersion is the version a binary reports when it was built without
|
// DevVersion is the version a binary reports when it was built without
|
||||||
// git metadata: script/version emits it outside a git checkout, and an
|
// git metadata: script/version emits it outside a git checkout, and a
|
||||||
// unstamped `go build` keeps it. goreleaser's snapshot template stamps
|
// binary keeps it when neither its linker flags nor its Go build info
|
||||||
|
// name a version. goreleaser's snapshot template stamps
|
||||||
// it followed by "-" and the commit it was built from. It is
|
// it followed by "-" and the commit it was built from. It is
|
||||||
// deliberately not a number.
|
// deliberately not a number.
|
||||||
const DevVersion = "dev"
|
const DevVersion = "dev"
|
||||||
@@ -32,6 +37,54 @@ var Commit = Unknown //nolint:gochecknoglobals // set via -ldflags at build time
|
|||||||
// CommitDate is the ISO-8601 date of the commit, populated from main().
|
// CommitDate is the ISO-8601 date of the commit, populated from main().
|
||||||
var CommitDate = Unknown //nolint:gochecknoglobals // set via -ldflags at build time
|
var CommitDate = Unknown //nolint:gochecknoglobals // set via -ldflags at build time
|
||||||
|
|
||||||
|
// develVersion is the main module version Go records when it cannot
|
||||||
|
// tell which version it built.
|
||||||
|
const develVersion = "(devel)"
|
||||||
|
|
||||||
|
// UseBuildInfo applies FromBuildInfo to Version, Commit and CommitDate,
|
||||||
|
// with the build info of the running binary.
|
||||||
|
func UseBuildInfo() {
|
||||||
|
info, ok := debug.ReadBuildInfo()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Version, Commit, CommitDate = FromBuildInfo(
|
||||||
|
info, Version, Commit, CommitDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromBuildInfo returns version, commit and commitDate. Each one that
|
||||||
|
// still holds its default ("dev" or empty for the version, "unknown"
|
||||||
|
// for the others) is replaced by the value Go recorded in info. A
|
||||||
|
// `go install` of a tag sets no linker flags, but Go records the tag
|
||||||
|
// as the main module's version. Go records the commit and its time
|
||||||
|
// only when it builds from a git checkout. The time is cut to a date,
|
||||||
|
// as the Makefile stamps it.
|
||||||
|
func FromBuildInfo(
|
||||||
|
info *debug.BuildInfo, version, commit, commitDate string,
|
||||||
|
) (string, string, string) {
|
||||||
|
if (version == "" || version == DevVersion) &&
|
||||||
|
info.Main.Version != "" && info.Main.Version != develVersion {
|
||||||
|
version = info.Main.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range info.Settings {
|
||||||
|
switch s.Key {
|
||||||
|
case "vcs.revision":
|
||||||
|
if commit == Unknown {
|
||||||
|
commit = s.Value
|
||||||
|
}
|
||||||
|
case "vcs.time":
|
||||||
|
t, err := time.Parse(time.RFC3339, s.Value)
|
||||||
|
if commitDate == Unknown && err == nil {
|
||||||
|
commitDate = t.UTC().Format(time.DateOnly)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return version, commit, commitDate
|
||||||
|
}
|
||||||
|
|
||||||
// Author identifies the upstream author of vaultik.
|
// Author identifies the upstream author of vaultik.
|
||||||
const Author = "Jeffrey Paul <sneak@sneak.berlin>"
|
const Author = "Jeffrey Paul <sneak@sneak.berlin>"
|
||||||
|
|
||||||
@@ -68,8 +121,10 @@ func New() (*Globals, error) {
|
|||||||
// a release. "dev" and goreleaser's snapshot "dev-<sha>" count, and so
|
// a release. "dev" and goreleaser's snapshot "dev-<sha>" count, and so
|
||||||
// does what `git describe --tags --always --dirty` gives a make or
|
// does what `git describe --tags --always --dirty` gives a make or
|
||||||
// docker build of an untagged commit: the bare short commit, or
|
// docker build of an untagged commit: the bare short commit, or
|
||||||
// tag-N-gHASH on a commit after a tag. Any version ending in "-dirty"
|
// tag-N-gHASH on a commit after a tag. A Go pseudo-version counts: Go
|
||||||
// counts, a modified checkout of a tag ("v1.0.0-dirty") included.
|
// records one for a build of an untagged commit without linker flags.
|
||||||
|
// Any version ending in "-dirty" or "+dirty" counts, a modified
|
||||||
|
// checkout of a tag ("v1.0.0-dirty", "v1.0.0+dirty") included.
|
||||||
// A plain tag such as "v1.0.0" or "1.0.0" is a release.
|
// A plain tag such as "v1.0.0" or "1.0.0" is a release.
|
||||||
//
|
//
|
||||||
// The empty string counts too. Nothing that knows its version reports
|
// The empty string counts too. Nothing that knows its version reports
|
||||||
@@ -82,7 +137,8 @@ func New() (*Globals, error) {
|
|||||||
func IsDevVersion(v string) bool {
|
func IsDevVersion(v string) bool {
|
||||||
if v == "" || v == Unknown || v == DevVersion ||
|
if v == "" || v == Unknown || v == DevVersion ||
|
||||||
strings.HasPrefix(v, DevVersion+"-") ||
|
strings.HasPrefix(v, DevVersion+"-") ||
|
||||||
strings.HasSuffix(v, "-dirty") {
|
strings.HasSuffix(v, "-dirty") || strings.HasSuffix(v, "+dirty") ||
|
||||||
|
module.IsPseudoVersion(v) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package globals_test
|
package globals_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"runtime/debug"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sneak.berlin/go/vaultik/internal/globals"
|
"sneak.berlin/go/vaultik/internal/globals"
|
||||||
@@ -59,12 +60,20 @@ func TestIsDevVersion(t *testing.T) {
|
|||||||
{"1.0.0-rc.1-12-g877eb2f", true},
|
{"1.0.0-rc.1-12-g877eb2f", true},
|
||||||
// A tagged commit with uncommitted changes is not that tag.
|
// A tagged commit with uncommitted changes is not that tag.
|
||||||
{"v1.0.0-dirty", true},
|
{"v1.0.0-dirty", true},
|
||||||
|
// What Go records for a build without linker flags: a
|
||||||
|
// pseudo-version for an untagged commit, "+dirty" for a
|
||||||
|
// modified checkout.
|
||||||
|
{"v1.0.0-rc3.0.20261111001226-abcdefabcdef", true},
|
||||||
|
{"v0.0.0-20261111001226-abcdefabcdef", true},
|
||||||
|
{"v1.0.1-0.20261111001226-abcdefabcdef", true},
|
||||||
|
{"v1.0.0+dirty", true},
|
||||||
// What a tagged build produces (goreleaser's .Version strips
|
// What a tagged build produces (goreleaser's .Version strips
|
||||||
// the leading "v"; script/version keeps it).
|
// the leading "v"; script/version keeps it).
|
||||||
{"1.0.0", false},
|
{"1.0.0", false},
|
||||||
{"0.1.0", false},
|
{"0.1.0", false},
|
||||||
{"1.0.0-rc.1", false},
|
{"1.0.0-rc.1", false},
|
||||||
{"v1.0.0", false},
|
{"v1.0.0", false},
|
||||||
|
{"v1.0.0-rc3", false},
|
||||||
// A release must not be mistaken for a dev build just because
|
// A release must not be mistaken for a dev build just because
|
||||||
// the string happens to contain "dev".
|
// the string happens to contain "dev".
|
||||||
{"1.0.0-dev", false},
|
{"1.0.0-dev", false},
|
||||||
@@ -86,10 +95,9 @@ func TestIsDevVersion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDefaultVersionIsDev pins the linker-flag contract: an unstamped
|
// TestDefaultVersionIsDev pins the default: a binary whose linker flags
|
||||||
// binary (no -ldflags at all, which is what `go build ./...` and `go
|
// and build info name no version must report itself as a development
|
||||||
// install` produce) must report itself as a development build rather
|
// build rather than as some default release number.
|
||||||
// than as some default release number.
|
|
||||||
func TestDefaultVersionIsDev(t *testing.T) {
|
func TestDefaultVersionIsDev(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -98,3 +106,104 @@ func TestDefaultVersionIsDev(t *testing.T) {
|
|||||||
globals.DevVersion)
|
globals.DevVersion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildInfo returns build info with the given main module version and,
|
||||||
|
// when revision is not empty, the commit settings Go records for a
|
||||||
|
// build from a git checkout.
|
||||||
|
func buildInfo(version, revision string) *debug.BuildInfo {
|
||||||
|
info := &debug.BuildInfo{
|
||||||
|
Main: debug.Module{Path: "sneak.berlin/go/vaultik", Version: version},
|
||||||
|
}
|
||||||
|
|
||||||
|
if revision != "" {
|
||||||
|
info.Settings = []debug.BuildSetting{
|
||||||
|
{Key: "vcs.revision", Value: revision},
|
||||||
|
{Key: "vcs.time", Value: "2026-11-10T23:12:26Z"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFromBuildInfo covers a binary from `go install` of a tag, which
|
||||||
|
// carries no linker flags: it must report the tag. Values the linker
|
||||||
|
// flags set are kept.
|
||||||
|
func TestFromBuildInfo(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
tag = "v1.0.0-rc3"
|
||||||
|
rev = "0123456789abcdef0123456789abcdef01234567"
|
||||||
|
)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
info *debug.BuildInfo
|
||||||
|
version, commit, date string
|
||||||
|
wantVersion, wantCommit string
|
||||||
|
wantDate string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "go install of a tag",
|
||||||
|
info: buildInfo(tag, ""),
|
||||||
|
version: globals.DevVersion,
|
||||||
|
commit: globals.Unknown,
|
||||||
|
date: globals.Unknown,
|
||||||
|
wantVersion: tag,
|
||||||
|
wantCommit: globals.Unknown,
|
||||||
|
wantDate: globals.Unknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty version",
|
||||||
|
info: buildInfo(tag, ""),
|
||||||
|
version: "",
|
||||||
|
commit: globals.Unknown,
|
||||||
|
date: globals.Unknown,
|
||||||
|
wantVersion: tag,
|
||||||
|
wantCommit: globals.Unknown,
|
||||||
|
wantDate: globals.Unknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "go build in a git checkout",
|
||||||
|
info: buildInfo(tag, rev),
|
||||||
|
version: globals.DevVersion,
|
||||||
|
commit: globals.Unknown,
|
||||||
|
date: globals.Unknown,
|
||||||
|
wantVersion: tag,
|
||||||
|
wantCommit: rev,
|
||||||
|
wantDate: "2026-11-10",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Go could not tell the version",
|
||||||
|
info: buildInfo("(devel)", ""),
|
||||||
|
version: globals.DevVersion,
|
||||||
|
commit: globals.Unknown,
|
||||||
|
date: globals.Unknown,
|
||||||
|
wantVersion: globals.DevVersion,
|
||||||
|
wantCommit: globals.Unknown,
|
||||||
|
wantDate: globals.Unknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "linker flags set",
|
||||||
|
info: buildInfo(tag, rev),
|
||||||
|
version: "v1.0.0-rc2-4-gabcdef1",
|
||||||
|
commit: "abcdef1",
|
||||||
|
date: "2026-11-01",
|
||||||
|
wantVersion: "v1.0.0-rc2-4-gabcdef1",
|
||||||
|
wantCommit: "abcdef1",
|
||||||
|
wantDate: "2026-11-01",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
version, commit, date := globals.FromBuildInfo(
|
||||||
|
tc.info, tc.version, tc.commit, tc.date)
|
||||||
|
|
||||||
|
if version != tc.wantVersion || commit != tc.wantCommit ||
|
||||||
|
date != tc.wantDate {
|
||||||
|
t.Errorf("%s: got %q, %q, %q; want %q, %q, %q", tc.name,
|
||||||
|
version, commit, date,
|
||||||
|
tc.wantVersion, tc.wantCommit, tc.wantDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
@@ -24,19 +25,28 @@ import (
|
|||||||
// that is not plugged in, cannot be listed. It is not an empty store, so
|
// that is not plugged in, cannot be listed. It is not an empty store, so
|
||||||
// no command may conclude from it that the local snapshots are gone.
|
// no command may conclude from it that the local snapshots are gone.
|
||||||
|
|
||||||
// newFileDestinationVaultik returns a Vaultik that backs up the
|
// newFileDestinationVaultik writes the fault source tree and returns a
|
||||||
// snapshot named "first" to a file:// destination at storeDir.
|
// Vaultik that backs it up to a file:// destination at storeDir.
|
||||||
func newFileDestinationVaultik(
|
func newFileDestinationVaultik(
|
||||||
ctx context.Context, t *testing.T, storeDir string,
|
ctx context.Context, t *testing.T, storeDir string,
|
||||||
) (*vaultik.Vaultik, *database.Repositories) {
|
) (*vaultik.Vaultik, *database.Repositories) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
osFs := afero.NewOsFs()
|
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
dataDir := filepath.Join(tempDir, "src")
|
dataDir := filepath.Join(tempDir, "src")
|
||||||
dbPath := filepath.Join(tempDir, "index.sqlite")
|
writeFaultSourceTree(t, afero.NewOsFs(), dataDir)
|
||||||
|
|
||||||
writeFaultSourceTree(t, osFs, dataDir)
|
return openFileDestinationVaultik(ctx, t, storeDir, dataDir,
|
||||||
|
filepath.Join(tempDir, "index.sqlite"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// openFileDestinationVaultik returns a Vaultik that backs up dataDir to a
|
||||||
|
// file:// destination at storeDir, with its local index at dbPath. The
|
||||||
|
// index is closed when the test ends.
|
||||||
|
func openFileDestinationVaultik(
|
||||||
|
ctx context.Context, t *testing.T, storeDir, dataDir, dbPath string,
|
||||||
|
) (*vaultik.Vaultik, *database.Repositories) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
store, err := storage.NewFileStorer(storeDir)
|
store, err := storage.NewFileStorer(storeDir)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -48,22 +58,84 @@ func newFileDestinationVaultik(
|
|||||||
repos := database.NewRepositories(db)
|
repos := database.NewRepositories(db)
|
||||||
cfg := changedFileConfig(dataDir, dbPath)
|
cfg := changedFileConfig(dataDir, dbPath)
|
||||||
|
|
||||||
return newBackupVaultik(ctx, cfg, store, repos, db, osFs), repos
|
return newBackupVaultik(ctx, cfg, store, repos, db, afero.NewOsFs()), repos
|
||||||
|
}
|
||||||
|
|
||||||
|
// firstBackup is one backup of the snapshot named "first" to a file://
|
||||||
|
// destination. dir holds its source tree in src, its store in store and
|
||||||
|
// its closed local index in index/index.sqlite. The first test that
|
||||||
|
// needs it makes it, and TestMain removes it. Every test that copies it
|
||||||
|
// backs up the same source tree, so no test may change that tree.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // made once and shared by the tests that copy it
|
||||||
|
var firstBackup struct {
|
||||||
|
once sync.Once
|
||||||
|
dir string
|
||||||
|
ready bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMain removes firstBackup once every test has run.
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
code := m.Run()
|
||||||
|
|
||||||
|
_ = os.RemoveAll(firstBackup.dir)
|
||||||
|
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeFirstBackup writes the fault source tree and backs it up as the
|
||||||
|
// snapshot named "first", all in a new directory that it records in
|
||||||
|
// firstBackup.
|
||||||
|
func makeFirstBackup(ctx context.Context, t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
dir, err := os.MkdirTemp("", "vaultik-first-backup-")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
firstBackup.dir = dir
|
||||||
|
storeDir := filepath.Join(dir, "store")
|
||||||
|
dataDir := filepath.Join(dir, "src")
|
||||||
|
indexDir := filepath.Join(dir, "index")
|
||||||
|
|
||||||
|
require.NoError(t, os.Mkdir(storeDir, 0o750))
|
||||||
|
require.NoError(t, os.Mkdir(indexDir, 0o750))
|
||||||
|
writeFaultSourceTree(t, afero.NewOsFs(), dataDir)
|
||||||
|
|
||||||
|
v, _ := openFileDestinationVaultik(ctx, t, storeDir, dataDir,
|
||||||
|
filepath.Join(indexDir, "index.sqlite"))
|
||||||
|
require.NoError(t, backUp(v, "first"))
|
||||||
|
require.NoError(t, v.DB.Close())
|
||||||
|
|
||||||
|
firstBackup.ready = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// backUpToFileDestination creates the directory storeDir, as a user must
|
// backUpToFileDestination creates the directory storeDir, as a user must
|
||||||
// before the first backup, and backs up the snapshot named "first" to a
|
// before the first backup, and returns a Vaultik whose file://
|
||||||
// file:// destination there. Everything the returned Vaultik prints
|
// destination there holds a backup of the snapshot named "first". The
|
||||||
// after the backup goes to the returned buffer.
|
// store and the local index are copies of firstBackup's, which is faster
|
||||||
|
// than backing up in every test. The returned Vaultik backs up a source
|
||||||
|
// tree shared with every other caller, so no test may change that tree.
|
||||||
|
// Everything the returned Vaultik prints after the backup goes to the
|
||||||
|
// returned buffer.
|
||||||
func backUpToFileDestination(
|
func backUpToFileDestination(
|
||||||
ctx context.Context, t *testing.T, storeDir string,
|
ctx context.Context, t *testing.T, storeDir string,
|
||||||
) (*vaultik.Vaultik, *database.Repositories, *bytes.Buffer) {
|
) (*vaultik.Vaultik, *database.Repositories, *bytes.Buffer) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
require.NoError(t, os.MkdirAll(storeDir, 0o750))
|
firstBackup.once.Do(func() { makeFirstBackup(ctx, t) })
|
||||||
|
require.True(t, firstBackup.ready, "another test failed to make firstBackup")
|
||||||
|
|
||||||
v, repos := newFileDestinationVaultik(ctx, t, storeDir)
|
require.NoError(t, os.MkdirAll(storeDir, 0o750))
|
||||||
require.NoError(t, backUp(v, "first"))
|
require.NoError(t, os.CopyFS(storeDir,
|
||||||
|
os.DirFS(filepath.Join(firstBackup.dir, "store"))))
|
||||||
|
|
||||||
|
indexDir := t.TempDir()
|
||||||
|
require.NoError(t, os.CopyFS(indexDir,
|
||||||
|
os.DirFS(filepath.Join(firstBackup.dir, "index"))))
|
||||||
|
|
||||||
|
v, repos := openFileDestinationVaultik(ctx, t, storeDir,
|
||||||
|
filepath.Join(firstBackup.dir, "src"),
|
||||||
|
filepath.Join(indexDir, "index.sqlite"))
|
||||||
|
|
||||||
out := &bytes.Buffer{}
|
out := &bytes.Buffer{}
|
||||||
v.Stdout = out
|
v.Stdout = out
|
||||||
|
|||||||
@@ -46,11 +46,11 @@ import (
|
|||||||
// slice out a few KB — currently the dominant cost in restore.
|
// slice out a few KB — currently the dominant cost in restore.
|
||||||
//
|
//
|
||||||
// The test deliberately constructs an adversarial scenario: three
|
// The test deliberately constructs an adversarial scenario: three
|
||||||
// blobs A/B/C of ~6 MB each, nine files distributed across them, and
|
// blobs A/B/C of five 64 KiB chunks each, nine files distributed across
|
||||||
// path-ordered names that interleave the blobs (a1, b1, c1, a2, b2,
|
// them, and path-ordered names that interleave the blobs (a1, b1, c1,
|
||||||
// c2, …) so naive path-order processing would touch every blob before
|
// a2, b2, c2, …) so naive path-order processing would touch every blob
|
||||||
// finishing any of them.
|
// before finishing any of them.
|
||||||
// localitySource is one 1 MiB fixture file used by the locality test.
|
// localitySource is one 64 KiB fixture file used by the locality test.
|
||||||
type localitySource struct {
|
type localitySource struct {
|
||||||
path string
|
path string
|
||||||
data []byte
|
data []byte
|
||||||
@@ -65,10 +65,10 @@ type localityCopy struct {
|
|||||||
|
|
||||||
// localitySourceBytes is the size of each source file of the locality
|
// localitySourceBytes is the size of each source file of the locality
|
||||||
// fixture.
|
// fixture.
|
||||||
const localitySourceBytes = 1024 * 1024
|
const localitySourceBytes = 64 * 1024
|
||||||
|
|
||||||
// buildLocalityFixture writes the adversarial source layout described
|
// buildLocalityFixture writes the adversarial source layout described
|
||||||
// in TestRestoreLocalityAndReadAt: 15 sources of 1 MiB each (which the
|
// in TestRestoreLocalityAndReadAt: 15 sources of 64 KiB each (which the
|
||||||
// backup packs into 3 blobs of 5 chunks) plus 9 interleaved-name copies
|
// backup packs into 3 blobs of 5 chunks) plus 9 interleaved-name copies
|
||||||
// (3 per blob group).
|
// (3 per blob group).
|
||||||
func buildLocalityFixture(
|
func buildLocalityFixture(
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ import (
|
|||||||
// TestRestoreSweeperEvictsBlobs exercises the reference-counted blob
|
// TestRestoreSweeperEvictsBlobs exercises the reference-counted blob
|
||||||
// disk cache eviction during restore.
|
// disk cache eviction during restore.
|
||||||
//
|
//
|
||||||
// The scenario: 30 unique 1 MB random files plus 10 duplicates of those
|
// The scenario: 30 unique 128 KiB random files plus 10 duplicates of
|
||||||
// (40 files total, 30 MB of unique content) get backed up with a 10 MB
|
// those (40 files total, 3.75 MiB of unique content) get backed up with
|
||||||
// blob_size_limit. After backup the snapshot's encrypted blobs are
|
// a 1 MiB blob_size_limit. After backup the snapshot's encrypted blobs
|
||||||
// restored through Vaultik.Restore, and per-key Get counts on the
|
// are restored through Vaultik.Restore, and per-key Get counts on the
|
||||||
// storage layer are recorded. Each blob in the snapshot MUST be
|
// storage layer are recorded. Each blob in the snapshot MUST be
|
||||||
// downloaded exactly once — re-downloads would mean the sweeper either
|
// downloaded exactly once — re-downloads would mean the sweeper either
|
||||||
// evicted a blob that was still needed (LRU regression) or that the
|
// evicted a blob that was still needed (LRU regression) or that the
|
||||||
@@ -41,7 +41,7 @@ import (
|
|||||||
// original AND every duplicate referencing its chunks have been
|
// original AND every duplicate referencing its chunks have been
|
||||||
// restored.
|
// restored.
|
||||||
// buildSweeperFixture writes the source layout for the sweeper test: 30
|
// buildSweeperFixture writes the source layout for the sweeper test: 30
|
||||||
// unique 1 MB random files plus 10 duplicates of a random subset. The
|
// unique 128 KiB random files plus 10 duplicates of a random subset. The
|
||||||
// PRNG seed is fixed so failures are reproducible; the entropy is what
|
// PRNG seed is fixed so failures are reproducible; the entropy is what
|
||||||
// matters here — the FastCDC chunker needs realistic-looking data to
|
// matters here — the FastCDC chunker needs realistic-looking data to
|
||||||
// pick chunk boundaries naturally. Returns the expected content by
|
// pick chunk boundaries naturally. Returns the expected content by
|
||||||
@@ -51,7 +51,7 @@ func buildSweeperFixture(
|
|||||||
) map[string][]byte {
|
) map[string][]byte {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
const fileSize = 1 * 1024 * 1024
|
const fileSize = 128 * 1024
|
||||||
|
|
||||||
rng := rand.New(rand.NewSource(42)) //nolint:gosec // G404: fixture data only
|
rng := rand.New(rand.NewSource(42)) //nolint:gosec // G404: fixture data only
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ func runSweeperBackup(
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, uniqueFiles+duplicateFiles, scanResult.FilesScanned)
|
require.Equal(t, uniqueFiles+duplicateFiles, scanResult.FilesScanned)
|
||||||
require.Greater(t, scanResult.BlobsCreated, 1,
|
require.Greater(t, scanResult.BlobsCreated, 1,
|
||||||
"30 MB of unique data at 10 MB blob size should yield multiple blobs")
|
"3.75 MiB of unique data at 1 MiB blob size should yield multiple blobs")
|
||||||
|
|
||||||
require.NoError(t, sm.CompleteSnapshot(ctx, snapshotID))
|
require.NoError(t, sm.CompleteSnapshot(ctx, snapshotID))
|
||||||
require.NoError(t, sm.ExportSnapshotMetadata(ctx, dbPath, snapshotID))
|
require.NoError(t, sm.ExportSnapshotMetadata(ctx, dbPath, snapshotID))
|
||||||
@@ -199,8 +199,8 @@ func TestRestoreSweeperEvictsBlobs(t *testing.T) {
|
|||||||
|
|
||||||
expected := buildSweeperFixture(t, fs, dataDir, uniqueFiles, duplicateFiles)
|
expected := buildSweeperFixture(t, fs, dataDir, uniqueFiles, duplicateFiles)
|
||||||
|
|
||||||
chunkSize := int64(64 * 1024)
|
chunkSize := int64(16 * 1024)
|
||||||
maxBlobSize := int64(10 * 1024 * 1024)
|
maxBlobSize := int64(1024 * 1024)
|
||||||
|
|
||||||
storer, err := storage.NewFileStorer(storeDir)
|
storer, err := storage.NewFileStorer(storeDir)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -187,6 +187,17 @@ func (e *listEnv) addRemoteRawTimestamp(
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
remoteKey := snapshot.RemoteSnapshotKey(snapshotID)
|
remoteKey := snapshot.RemoteSnapshotKey(snapshotID)
|
||||||
|
e.putRemoteManifest(t, remoteKey,
|
||||||
|
encodeRemoteManifest(t, remoteKey, timestamp))
|
||||||
|
|
||||||
|
return remoteKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeRemoteManifest returns the encoded manifest of a one-blob
|
||||||
|
// snapshot whose remote key is remoteKey.
|
||||||
|
func encodeRemoteManifest(t *testing.T, remoteKey, timestamp string) []byte {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
manifest := &snapshot.Manifest{
|
manifest := &snapshot.Manifest{
|
||||||
// Note: the hashed key, never the human ID. That is precisely
|
// Note: the hashed key, never the human ID. That is precisely
|
||||||
// why a remote-only snapshot cannot be named.
|
// why a remote-only snapshot cannot be named.
|
||||||
@@ -202,11 +213,17 @@ func (e *listEnv) addRemoteRawTimestamp(
|
|||||||
data, err := snapshot.EncodeManifest(manifest, 3)
|
data, err := snapshot.EncodeManifest(manifest, 3)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = e.store.Put(context.Background(),
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// putRemoteManifest stores an encoded manifest on the destination store
|
||||||
|
// under remoteKey.
|
||||||
|
func (e *listEnv) putRemoteManifest(t *testing.T, remoteKey string, data []byte) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
err := e.store.Put(context.Background(),
|
||||||
"metadata/"+remoteKey+"/manifest.json.zst", bytes.NewReader(data))
|
"metadata/"+remoteKey+"/manifest.json.zst", bytes.NewReader(data))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return remoteKey
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fixtures shared across the listing tests.
|
// Fixtures shared across the listing tests.
|
||||||
@@ -791,9 +808,17 @@ func TestListSnapshots_JSONReportsTruncation(t *testing.T) {
|
|||||||
|
|
||||||
timestamp := time.Date(2026, 3, 2, 11, 22, 33, 0, time.UTC)
|
timestamp := time.Date(2026, 3, 2, 11, 22, 33, 0, time.UTC)
|
||||||
|
|
||||||
// One past the cap, so exactly one snapshot is omitted.
|
// One past the cap, so exactly one snapshot is omitted. The listing
|
||||||
|
// names a snapshot by the key its manifest is stored under, never by
|
||||||
|
// the manifest's own ID. So all of them share one encoded manifest:
|
||||||
|
// encoding 1001 took seconds.
|
||||||
|
data := encodeRemoteManifest(t,
|
||||||
|
snapshot.RemoteSnapshotKey("otherhost_bulk"),
|
||||||
|
timestamp.Format(time.RFC3339))
|
||||||
|
|
||||||
for i := range maxRemoteOnlyRowsForTest + 1 {
|
for i := range maxRemoteOnlyRowsForTest + 1 {
|
||||||
env.addRemote(t, fmt.Sprintf("otherhost_bulk_%04d", i), timestamp)
|
env.putRemoteManifest(t, snapshot.RemoteSnapshotKey(
|
||||||
|
fmt.Sprintf("otherhost_bulk_%04d", i)), data)
|
||||||
}
|
}
|
||||||
|
|
||||||
stderr := captureProcessStderr(t, func() {
|
stderr := captureProcessStderr(t, func() {
|
||||||
|
|||||||
+2
-2
@@ -11,8 +11,8 @@
|
|||||||
# when no tag is reachable, each with a "-dirty" suffix when tracked
|
# when no tag is reachable, each with a "-dirty" suffix when tracked
|
||||||
# files have uncommitted changes. Untracked files are ignored: a stray
|
# files have uncommitted changes. Untracked files are ignored: a stray
|
||||||
# scratch file does not change what was compiled. Outside a git checkout
|
# scratch file does not change what was compiled. Outside a git checkout
|
||||||
# (release tarball, `go install`), or in one with no commits, it is
|
# (such as an unpacked release tarball), or in one with no commits, it
|
||||||
# "dev".
|
# is "dev".
|
||||||
#
|
#
|
||||||
# Nothing here ever invents a version number.
|
# Nothing here ever invents a version number.
|
||||||
set -eu
|
set -eu
|
||||||
|
|||||||
Reference in new issue
Block a user