package installer import ( "os" "path/filepath" "strings" "testing" "zsvo/pkg/packager" "zsvo/pkg/recipe" "zsvo/pkg/types" ) func buildTestPackage(t *testing.T, workDir string, r *recipe.Recipe, files map[string][]byte) string { t.Helper() stagingDir := r.GetStagingDir(workDir) for relPath, content := range files { path := filepath.Join(stagingDir, relPath) if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("MkdirAll(%s) error = %v", path, err) } if err := os.WriteFile(path, content, 0o755); err != nil { t.Fatalf("WriteFile(%s) error = %v", path, err) } } p := packager.NewPackager(workDir) if err := p.Package(r); err != nil { t.Fatalf("Package(%s) error = %v", r.GetPackageName(), err) } return p.GetPackageFile(r) } func TestInstallPackageCopiesFilesAndSymlinks(t *testing.T) { t.Parallel() workDir := t.TempDir() rootDir := t.TempDir() r := &recipe.Recipe{Name: "demo", Version: "1.0.0", Build: []string{"true"}, Install: []string{"true"}} stagingDir := r.GetStagingDir(workDir) if err := os.MkdirAll(filepath.Join(stagingDir, "usr", "bin"), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } if err := os.WriteFile(filepath.Join(stagingDir, "usr", "bin", "demo"), []byte("demo\n"), 0o755); err != nil { t.Fatalf("WriteFile() error = %v", err) } if err := os.Symlink("demo", filepath.Join(stagingDir, "usr", "bin", "demo-link")); err != nil { t.Fatalf("Symlink() error = %v", err) } p := packager.NewPackager(workDir) if err := p.Package(r); err != nil { t.Fatalf("Package() error = %v", err) } ins := NewInstaller(rootDir) if err := ins.Install(p.GetPackageFile(r)); err != nil { t.Fatalf("Install() error = %v", err) } if _, err := os.Stat(filepath.Join(rootDir, "usr", "bin", "demo")); err != nil { t.Fatalf("installed file missing: %v", err) } if target, err := os.Readlink(filepath.Join(rootDir, "usr", "bin", "demo-link")); err != nil { t.Fatalf("Readlink() error = %v", err) } else if target != "demo" { t.Fatalf("unexpected symlink target: %s", target) } } func TestInstallManyOrdersByDependencies(t *testing.T) { t.Parallel() workDir := t.TempDir() rootDir := t.TempDir() ins := NewInstaller(rootDir) libPkg := buildTestPackage(t, workDir, &recipe.Recipe{ Name: "libfoo", Version: "1.0.0", Build: []string{"true"}, Install: []string{"true"}, }, map[string][]byte{"usr/lib/libfoo.so": []byte("lib\n")}) appPkg := buildTestPackage(t, workDir, &recipe.Recipe{ Name: "app", Version: "1.0.0", Build: []string{"true"}, Install: []string{"true"}, Deps: []string{"libfoo"}, }, map[string][]byte{"usr/bin/app": []byte("app\n")}) // app передан первым, installer должен поставить libfoo раньше. if err := ins.InstallMany([]string{appPkg, libPkg}); err != nil { t.Fatalf("InstallMany() error = %v", err) } if !ins.IsInstalled("libfoo") || !ins.IsInstalled("app") { t.Fatalf("expected libfoo and app to be installed") } } func TestInstallManyRollsBackOnFailure(t *testing.T) { t.Parallel() workDir := t.TempDir() rootDir := t.TempDir() ins := NewInstaller(rootDir) libPkg := buildTestPackage(t, workDir, &recipe.Recipe{ Name: "libfoo", Version: "1.0.0", Build: []string{"true"}, Install: []string{"true"}, }, map[string][]byte{"usr/lib/libfoo.so": []byte("lib\n")}) appPkg := buildTestPackage(t, workDir, &recipe.Recipe{ Name: "app", Version: "1.0.0", Build: []string{"true"}, Install: []string{"true"}, Deps: []string{"libfoo"}, }, map[string][]byte{"usr/bin/app": []byte("app\n")}) brokenPkg := buildTestPackage(t, workDir, &recipe.Recipe{ Name: "broken", Version: "1.0.0", Build: []string{"true"}, Install: []string{"true"}, Deps: []string{"missing"}, }, map[string][]byte{"usr/bin/broken": []byte("broken\n")}) err := ins.InstallMany([]string{appPkg, libPkg, brokenPkg}) if err == nil { t.Fatalf("expected InstallMany() to fail") } if ins.IsInstalled("libfoo") || ins.IsInstalled("app") || ins.IsInstalled("broken") { t.Fatalf("expected transaction rollback") } } func TestInstallRespectsRootFlag(t *testing.T) { t.Parallel() workDir := t.TempDir() rootDir := t.TempDir() ins := NewInstaller(rootDir) pkgFile := buildTestPackage(t, workDir, &recipe.Recipe{ Name: "rooted", Version: "1.0.0", Build: []string{"true"}, Install: []string{"true"}, }, map[string][]byte{"usr/bin/rooted": []byte("ok\n")}) if err := ins.Install(pkgFile); err != nil { t.Fatalf("Install() error = %v", err) } if _, err := os.Stat(filepath.Join(rootDir, "usr", "bin", "rooted")); err != nil { t.Fatalf("expected file in custom root: %v", err) } } func TestRemoveManyWithoutCascadeFails(t *testing.T) { t.Parallel() rootDir := t.TempDir() ins := NewInstaller(rootDir) lib := &types.PkgInfo{Name: "libfoo", Version: "1.0.0", Files: []string{"usr/lib/libfoo.so"}} app := &types.PkgInfo{Name: "app", Version: "1.0.0", Dependencies: []string{"libfoo"}, Files: []string{"usr/bin/app"}} if err := ins.registerPackage(lib); err != nil { t.Fatalf("registerPackage(lib) error = %v", err) } if err := ins.registerPackage(app); err != nil { t.Fatalf("registerPackage(app) error = %v", err) } _, err := ins.RemoveMany([]string{"libfoo"}, RemoveOptions{}) if err == nil { t.Fatalf("expected RemoveMany() to fail") } if !strings.Contains(err.Error(), "required by") { t.Fatalf("unexpected error: %v", err) } } func TestRemoveManyCascade(t *testing.T) { t.Parallel() rootDir := t.TempDir() ins := NewInstaller(rootDir) lib := &types.PkgInfo{Name: "libfoo", Version: "1.0.0", Files: []string{"usr/lib/libfoo.so"}} app := &types.PkgInfo{Name: "app", Version: "1.0.0", Dependencies: []string{"libfoo"}, Files: []string{"usr/bin/app"}} if err := ins.registerPackage(lib); err != nil { t.Fatalf("registerPackage(lib) error = %v", err) } if err := ins.registerPackage(app); err != nil { t.Fatalf("registerPackage(app) error = %v", err) } removed, err := ins.RemoveMany([]string{"libfoo"}, RemoveOptions{Cascade: true}) if err != nil { t.Fatalf("RemoveMany(cascade) error = %v", err) } if len(removed) != 2 { t.Fatalf("expected two removed packages, got %d", len(removed)) } if ins.IsInstalled("libfoo") || ins.IsInstalled("app") { t.Fatalf("expected packages to be removed") } } func TestListOrphans(t *testing.T) { t.Parallel() rootDir := t.TempDir() ins := NewInstaller(rootDir) lib := &types.PkgInfo{Name: "libfoo", Version: "1.0.0"} app := &types.PkgInfo{Name: "app", Version: "1.0.0", Dependencies: []string{"libfoo"}} tool := &types.PkgInfo{Name: "tool", Version: "1.0.0"} if err := ins.registerPackage(lib); err != nil { t.Fatalf("registerPackage(lib) error = %v", err) } if err := ins.registerPackage(app); err != nil { t.Fatalf("registerPackage(app) error = %v", err) } if err := ins.registerPackage(tool); err != nil { t.Fatalf("registerPackage(tool) error = %v", err) } orphans, err := ins.ListOrphans() if err != nil { t.Fatalf("ListOrphans() error = %v", err) } set := make(map[string]struct{}, len(orphans)) for _, name := range orphans { set[name] = struct{}{} } if _, ok := set["app"]; !ok { t.Fatalf("expected app to be orphan") } if _, ok := set["tool"]; !ok { t.Fatalf("expected tool to be orphan") } if _, ok := set["libfoo"]; ok { t.Fatalf("did not expect libfoo to be orphan") } }