Files
felhom-controller/controller/internal/appexport/estimate_volsize_test.go
T
admin 73efb091d9
gates / gates (push) Successful in 9s
R-203: the app and its backup look in the same directory — one resolver, every caller
appbackup's path helpers take a NAMESPACE ROOT. Five call sites passed a bare DRIVE path.
On an enrolled drive the two coincide, so nothing showed; on the system-data fallback they
differ by exactly the felhom-data segment, and the app then bound a directory the off-site
capture set never looked at -- while the run reported ok. Measured live on demo-hp: the app
wrote to /mnt/sys_drive/userdata/media/books, the capture set looked for
/mnt/sys_drive/felhom-data/userdata/media/books.

THE RULE NOW HAS ONE EXPRESSION. appbackup.NamespaceRootFor / IsEnrolledDrive encode the
drive-kind comparison; backup.Manager.namespaceRoot and stacks.Manager.inGuest delegate to
it. There were already TWO copies and they differed -- the backup package's compared without
filepath.Clean, the stacks package's with it, so a trailing slash from config would have
flipped the mode in one and not the other.

Sites routed through it:
  - stacks/deploy.go withPathVars -> ${USERDATA_PATH}   (the live defect)
  - appexport/fabplan.go + export.go                     (via a new provider method)
  - web/handlers.go FileBrowser mounts                   (latent: the system drive is
    deliberately never a registered StoragePath, so this is the identity today)

ComputeFabBuckets now receives the namespace root, which is what ComputeCaptureSet has always
received -- so the export's classified paths and the backup's capture set describe the same
directories by construction instead of by coincidence.

Tests are table-driven over BOTH drive kinds, because this survived by being invisible on the
kind that already worked. Red-proofs observed: restoring the bare-path call fails the
system-drive row with the two paths differing by /felhom-data; inverting the drive-kind
comparison fails every enrolled row.
2026-08-04 18:17:05 +02:00

113 lines
4.0 KiB
Go

package appexport
import (
"errors"
"io"
"log"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
)
// hddProvider is an rtProvider that reports an HDD-backed stack (estimate scenario H + the
// v0.130.0 additive-export tests in export_additive_test.go).
type hddProvider struct {
*rtProvider
mounts []string
hddPath string
binds []appbackup.ClassifiedBind
hasBinds bool
}
func (p *hddProvider) GetStackNeedsHDD(string) bool { return true }
func (p *hddProvider) GetStackHDDMounts(string) []string { return p.mounts }
func (p *hddProvider) GetStackHDDPath(string) string { return p.hddPath }
func (p *hddProvider) GetImportRoot() string { return "" } // R-75: no import binds in this fixture
// R-203: these fixtures use ENROLLED drive paths, where the namespace root IS the drive path.
// Delegating keeps that identity explicit rather than hardcoding it.
func (p *hddProvider) GetStackNamespaceRoot(name string) string { return p.GetStackHDDPath(name) }
func (p *hddProvider) GetStackClassifiedBinds(string) ([]appbackup.ClassifiedBind, bool) {
return p.binds, p.hasBinds
}
func newEstimator(t *testing.T, provider ExportStackProvider) *Exporter {
t.Helper()
return NewExporter(provider, log.New(io.Discard, "", 0), "test")
}
// Scenario F (the F-A fix): a volume-only app with a >1 GiB volume reports the REAL size via the
// container-view sizer — not 0/"3.6 KB". This is the F-A red-proof anchor (revert EstimateExport to
// dockerVolumeSize → reads 0).
func TestEstimate_VolumeSize_RealNotZero(t *testing.T) {
const twoGiB = int64(2) << 30
orig := volumeSizer
volumeSizer = func(vol string) (int64, error) { return twoGiB, nil }
defer func() { volumeSizer = orig }()
e := newEstimator(t, &rtProvider{stackDir: t.TempDir(), volumes: []string{"app_data"}})
est, err := e.EstimateExport("app", t.TempDir())
if err != nil {
t.Fatal(err)
}
if est.SizeUnknown {
t.Fatalf("size must be known when the sizer succeeds")
}
if est.DataSizeBytes != twoGiB {
t.Fatalf("DataSizeBytes = %d, want %d (WRONG would be 0 — the F-A bug)", est.DataSizeBytes, twoGiB)
}
if !strings.Contains(est.DataSizeHuman, "GB") {
t.Fatalf("DataSizeHuman = %q, want GB-scale (WRONG would be \"3.6 KB\")", est.DataSizeHuman)
}
}
// Scenario G: a failed volume read must never render as "fits". Size is marked unknown, the human
// string says so, and FitsOnDest is forced false.
func TestEstimate_VolumeSize_FailureNeverFits(t *testing.T) {
orig := volumeSizer
volumeSizer = func(vol string) (int64, error) { return 0, errors.New("docker: no such image") }
defer func() { volumeSizer = orig }()
e := newEstimator(t, &rtProvider{stackDir: t.TempDir(), volumes: []string{"app_data"}})
est, err := e.EstimateExport("app", t.TempDir())
if err != nil {
t.Fatal(err)
}
if !est.SizeUnknown {
t.Fatalf("a failed volume read must set SizeUnknown")
}
if est.FitsOnDest {
t.Fatalf("an unknown size must NEVER render as fits_on_dest:true")
}
if est.DataSizeHuman != "ismeretlen méret" {
t.Fatalf("DataSizeHuman = %q, want \"ismeretlen méret\"", est.DataSizeHuman)
}
if est.DataSizeBytes != 0 {
t.Fatalf("no successful read → DataSizeBytes should be 0, got %d", est.DataSizeBytes)
}
}
// Scenario H (regression): an HDD-backed stack must NOT touch the new volume sizer — the HDD branch
// (duBytes on the mounted /mnt path) is unchanged. Platform-independent: assert the seam is not
// invoked and SizeUnknown stays false.
func TestEstimate_HDDPath_DoesNotUseVolumeSizer(t *testing.T) {
called := false
orig := volumeSizer
volumeSizer = func(vol string) (int64, error) { called = true; return 0, nil }
defer func() { volumeSizer = orig }()
p := &hddProvider{rtProvider: &rtProvider{stackDir: t.TempDir()}, mounts: []string{t.TempDir()}}
e := newEstimator(t, p)
est, err := e.EstimateExport("app", t.TempDir())
if err != nil {
t.Fatal(err)
}
if called {
t.Fatalf("HDD-backed stack must not call the docker volume sizer")
}
if est.SizeUnknown {
t.Fatalf("HDD branch must not set SizeUnknown")
}
}