From d28e7cbea3e1bc3fbba890506c20560998bd5ca0 Mon Sep 17 00:00:00 2001 From: Chris Koch Date: Thu, 8 Feb 2024 04:19:36 +0000 Subject: [PATCH] More u-root build tests Signed-off-by: Chris Koch --- uroot/initramfs/files.go | 30 +++- uroot/initramfs/test/ramfs.go | 15 ++ uroot/uroot.go | 32 ++-- uroot/uroot_test.go | 265 +++++++++++++++++++++++++++++++--- 4 files changed, 303 insertions(+), 39 deletions(-) diff --git a/uroot/initramfs/files.go b/uroot/initramfs/files.go index da8de05..7aecfe2 100644 --- a/uroot/initramfs/files.go +++ b/uroot/initramfs/files.go @@ -73,7 +73,7 @@ func (af *Files) addFile(src string, dest string, follow bool) error { // a record or file, we want to include its children anyway. sInfo, err := os.Lstat(src) if err != nil { - return fmt.Errorf("adding %q to archive failed because Lstat failed: %v", src, err) + return fmt.Errorf("adding %q to archive failed because Lstat failed: %w", src, err) } // Recursively add children. @@ -92,7 +92,11 @@ func (af *Files) addFile(src string, dest string, follow bool) error { } if record, ok := af.Records[dest]; ok { - return fmt.Errorf("record for %q already exists in archive: %v", dest, record) + return &os.PathError{ + Op: "add to archive", + Path: dest, + Err: fmt.Errorf("%w: is %v", os.ErrExist, record), + } } if srcFile, ok := af.Files[dest]; ok { @@ -100,7 +104,11 @@ func (af *Files) addFile(src string, dest string, follow bool) error { if src == srcFile { return nil } - return fmt.Errorf("record for %q already exists in archive (is %q)", dest, src) + return &os.PathError{ + Op: "add to archive", + Path: dest, + Err: fmt.Errorf("%w: backed by %q", os.ErrExist, src), + } } af.Files[dest] = src @@ -137,13 +145,21 @@ func (af *Files) AddRecord(r cpio.Record) error { } if src, ok := af.Files[r.Name]; ok { - return fmt.Errorf("record for %q already exists in archive: file %q", r.Name, src) + return &os.PathError{ + Op: "add to archive", + Path: r.Name, + Err: fmt.Errorf("%w: backed by %q", os.ErrExist, src), + } } - if rr, ok := af.Records[r.Name]; ok { - if rr.Info == r.Info { + if record, ok := af.Records[r.Name]; ok { + if record.Info == r.Info { return nil } - return fmt.Errorf("record for %q already exists in archive: %v", r.Name, rr) + return &os.PathError{ + Op: "add to archive", + Path: r.Name, + Err: fmt.Errorf("%w: is %v", os.ErrExist, record), + } } af.Records[r.Name] = r diff --git a/uroot/initramfs/test/ramfs.go b/uroot/initramfs/test/ramfs.go index a998df5..0869716 100644 --- a/uroot/initramfs/test/ramfs.go +++ b/uroot/initramfs/test/ramfs.go @@ -43,6 +43,21 @@ func (hf HasFile) Validate(a *cpio.Archive) error { return fmt.Errorf("archive does not contain %s, but should", hf.Path) } +type HasDir struct { + Path string +} + +func (h HasDir) Validate(a *cpio.Archive) error { + r, ok := a.Get(h.Path) + if !ok { + return fmt.Errorf("archive does not contain %s, but should", h.Path) + } + if r.Mode&cpio.S_IFDIR == 0 { + return fmt.Errorf("file %v should be directory, but isn't", h.Path) + } + return nil +} + type HasContent struct { Path string Content string diff --git a/uroot/uroot.go b/uroot/uroot.go index 6f0768c..e772243 100644 --- a/uroot/uroot.go +++ b/uroot/uroot.go @@ -10,6 +10,7 @@ package uroot import ( "debug/elf" + "errors" "fmt" "os" "path" @@ -233,7 +234,7 @@ type Opts struct { // CreateInitramfs creates an initramfs built to opts' specifications. func CreateInitramfs(logger ulog.Logger, opts Opts) error { if _, err := os.Stat(opts.TempDir); os.IsNotExist(err) { - return fmt.Errorf("temp dir %q must exist: %v", opts.TempDir, err) + return fmt.Errorf("temp dir %q must exist: %w", opts.TempDir, err) } if opts.OutputFile == nil { return fmt.Errorf("must give output file") @@ -254,7 +255,7 @@ func CreateInitramfs(logger ulog.Logger, opts Opts) error { for index, cmds := range opts.Commands { paths, err := findpkg.ResolveGlobs(logger, env, lookupEnv, cmds.Packages) if err != nil { - return err + return fmt.Errorf("%w: %w", errResolvePackage, err) } opts.Commands[index].Packages = paths } @@ -294,21 +295,21 @@ func CreateInitramfs(logger ulog.Logger, opts Opts) error { return err } if err := opts.addSymlinkTo(logger, archive, opts.UinitCmd, "bin/uinit"); err != nil { - return fmt.Errorf("%v: specify -uinitcmd=\"\" to ignore this error and build without a uinit", err) + return fmt.Errorf("%w: %w", err, errUinitSymlink) } if len(opts.UinitArgs) > 0 { if err := archive.AddRecord(cpio.StaticFile("etc/uinit.flags", fileflag.ArgvToFile(opts.UinitArgs), 0o444)); err != nil { - return fmt.Errorf("%v: could not add uinit arguments from UinitArgs (-uinitcmd) to initramfs", err) + return fmt.Errorf("%w: %w", err, errUinitArgs) } } if err := opts.addSymlinkTo(logger, archive, opts.InitCmd, "init"); err != nil { - return fmt.Errorf("%v: specify -initcmd=\"\" to ignore this error and build without an init (or, did you specify a list, and are you missing github.com/u-root/u-root/cmds/core/init?)", err) + return fmt.Errorf("%w: %w", err, errInitSymlink) } if err := opts.addSymlinkTo(logger, archive, opts.DefaultShell, "bin/sh"); err != nil { - return fmt.Errorf("%v: specify -defaultsh=\"\" to ignore this error and build without a shell", err) + return fmt.Errorf("%w: %w", err, errDefaultshSymlink) } if err := opts.addSymlinkTo(logger, archive, opts.DefaultShell, "bin/defaultsh"); err != nil { - return fmt.Errorf("%v: specify -defaultsh=\"\" to ignore this error and build without a shell", err) + return fmt.Errorf("%w: %w", err, errDefaultshSymlink) } // Finally, write the archive. @@ -318,6 +319,15 @@ func CreateInitramfs(logger ulog.Logger, opts Opts) error { return nil } +var ( + errResolvePackage = errors.New("failed to resolve package paths") + errInitSymlink = errors.New("specify -initcmd=\"\" to ignore this error and build without an init (or, did you specify a list, and are you missing github.com/u-root/u-root/cmds/core/init?)") + errUinitSymlink = errors.New("specify -uinitcmd=\"\" to ignore this error and build without a uinit") + errDefaultshSymlink = errors.New("specify -defaultsh=\"\" to ignore this error and build without a shell") + errSymlink = errors.New("could not create symlink") + errUinitArgs = errors.New("could not add uinit arguments") +) + func (o *Opts) addSymlinkTo(logger ulog.Logger, archive *initramfs.Opts, command string, source string) error { if len(command) == 0 { return nil @@ -326,7 +336,7 @@ func (o *Opts) addSymlinkTo(logger ulog.Logger, archive *initramfs.Opts, command target, err := resolveCommandOrPath(command, o.Commands) if err != nil { if o.Commands != nil { - return fmt.Errorf("could not create symlink from %q to %q: %v", source, command, err) + return fmt.Errorf("%w from %q to %q: %w", errSymlink, source, command, err) } logger.Printf("Could not create symlink from %q to %q: %v", source, command, err) return nil @@ -343,7 +353,7 @@ func (o *Opts) addSymlinkTo(logger ulog.Logger, archive *initramfs.Opts, command } if err := archive.AddRecord(cpio.Symlink(source, relTarget)); err != nil { - return fmt.Errorf("failed to add symlink %s -> %s to initramfs: %v", source, relTarget, err) + return fmt.Errorf("failed to add symlink %s -> %s to initramfs: %w", source, relTarget, err) } return nil } @@ -405,10 +415,10 @@ func ParseExtraFiles(logger ulog.Logger, archive *initramfs.Files, extraFiles [] } src, err := filepath.Abs(src) if err != nil { - return fmt.Errorf("couldn't find absolute path for %q: %v", src, err) + return fmt.Errorf("couldn't find absolute path for %q: %w", src, err) } if err := archive.AddFile(src, dst); err != nil { - return fmt.Errorf("couldn't add %q to archive: %v", file, err) + return fmt.Errorf("couldn't add %q to archive: %w", file, err) } if lddDeps { diff --git a/uroot/uroot_test.go b/uroot/uroot_test.go index e099763..f5a568b 100644 --- a/uroot/uroot_test.go +++ b/uroot/uroot_test.go @@ -5,6 +5,7 @@ package uroot import ( + "errors" "fmt" "os" "path/filepath" @@ -35,28 +36,33 @@ func TestCreateInitramfs(t *testing.T) { } tmp777 := filepath.Join(dir, "tmp777") - if err := os.MkdirAll(tmp777, 0o777); err != nil { - t.Error(err) - } + _ = os.MkdirAll(tmp777, 0o777) + tmp400 := filepath.Join(dir, "tmp400") + _ = os.MkdirAll(tmp400, 0o400) + + somefile := filepath.Join(dir, "somefile") + somefile2 := filepath.Join(dir, "somefile2") + _ = os.WriteFile(somefile, []byte("foobar"), 0o777) + _ = os.WriteFile(somefile2, []byte("spongebob"), 0o777) + + cwd, _ := os.Getwd() l := ulogtest.Logger{TB: t} for i, tt := range []struct { name string opts Opts - want string + errs []error validators []itest.ArchiveValidator }{ { - name: "BB archive with ls and init", + name: "BB archive", opts: Opts{ - Env: golang.Default(golang.DisableCGO()), - TempDir: dir, - ExtraFiles: nil, - UseExistingInit: false, - InitCmd: "init", - DefaultShell: "ls", - UrootSource: urootpath, + Env: golang.Default(golang.DisableCGO()), + TempDir: dir, + InitCmd: "init", + DefaultShell: "ls", + UrootSource: urootpath, Commands: []Commands{ { Builder: builder.BusyBox, @@ -67,7 +73,6 @@ func TestCreateInitramfs(t *testing.T) { }, }, }, - want: "", validators: []itest.ArchiveValidator{ itest.HasFile{Path: "bbin/bb"}, itest.HasRecord{R: cpio.Symlink("bbin/init", "bb")}, @@ -82,7 +87,7 @@ func TestCreateInitramfs(t *testing.T) { InitCmd: "init", DefaultShell: "", }, - want: "temp dir \"\" must exist: stat : no such file or directory", + errs: []error{os.ErrNotExist}, validators: []itest.ArchiveValidator{ itest.IsEmpty{}, }, @@ -92,11 +97,187 @@ func TestCreateInitramfs(t *testing.T) { opts: Opts{ TempDir: dir, }, - want: "", validators: []itest.ArchiveValidator{ itest.MissingFile{Path: "bbin/bb"}, }, }, + { + name: "files", + opts: Opts{ + TempDir: dir, + ExtraFiles: []string{ + somefile + ":etc/somefile", + somefile2 + ":etc/somefile2", + somefile, + // Empty is ignored. + "", + "uroot_test.go", + filepath.Join(cwd, "uroot_test.go"), + // Parent directory is created. + somefile + ":somedir/somefile", + }, + }, + validators: []itest.ArchiveValidator{ + itest.MissingFile{Path: "bbin/bb"}, + itest.HasContent{Path: "etc/somefile", Content: "foobar"}, + itest.HasContent{Path: somefile, Content: "foobar"}, + itest.HasContent{Path: "etc/somefile2", Content: "spongebob"}, + // TODO: This behavior is weird. + itest.HasFile{Path: "uroot_test.go"}, + itest.HasFile{Path: filepath.Join(cwd, "uroot_test.go")}, + itest.HasDir{Path: "somedir"}, + itest.HasContent{Path: "somedir/somefile", Content: "foobar"}, + }, + }, + { + name: "files conflict", + opts: Opts{ + TempDir: dir, + ExtraFiles: []string{ + somefile + ":etc/somefile", + somefile2 + ":etc/somefile", + }, + }, + errs: []error{os.ErrExist}, + validators: []itest.ArchiveValidator{ + itest.IsEmpty{}, + }, + }, + { + name: "file does not exist", + opts: Opts{ + TempDir: dir, + ExtraFiles: []string{ + filepath.Join(dir, "doesnotexist") + ":etc/somefile", + }, + }, + errs: []error{os.ErrNotExist}, + validators: []itest.ArchiveValidator{ + itest.IsEmpty{}, + }, + }, + /* TODO: case is broken. + { + name: "files invalid syntax 1", + opts: Opts{ + TempDir: dir, + ExtraFiles: []string{ + ":etc/somefile", + }, + }, + //errs: []error{os.ErrExist}, + validators: []itest.ArchiveValidator{ + itest.IsEmpty{}, + }, + }, + */ + /* TODO: case is broken. + { + name: "files invalid syntax 2", + opts: Opts{ + TempDir: dir, + ExtraFiles: []string{ + somefile + ":", + }, + }, + //errs: []error{os.ErrExist}, + validators: []itest.ArchiveValidator{ + itest.IsEmpty{}, + }, + }, + */ + // TODO: files are directories. + { + name: "file conflicts with init", + opts: Opts{ + TempDir: dir, + InitCmd: "/bin/systemd", + ExtraFiles: []string{ + somefile + ":init", + }, + }, + errs: []error{os.ErrExist, errInitSymlink}, + validators: []itest.ArchiveValidator{ + itest.IsEmpty{}, + }, + }, + { + name: "file conflicts with uinit flags", + opts: Opts{ + TempDir: dir, + UinitArgs: []string{"-foo", "-bar"}, + ExtraFiles: []string{ + somefile + ":etc/uinit.flags", + }, + }, + errs: []error{os.ErrExist, errUinitArgs}, + validators: []itest.ArchiveValidator{ + itest.IsEmpty{}, + }, + }, + { + name: "file conflicts with uinit", + opts: Opts{ + TempDir: dir, + UinitCmd: "/bin/systemd", + ExtraFiles: []string{ + somefile + ":bin/uinit", + }, + }, + errs: []error{os.ErrExist, errUinitSymlink}, + validators: []itest.ArchiveValidator{ + itest.IsEmpty{}, + }, + }, + { + name: "file conflicts with sh", + opts: Opts{ + TempDir: dir, + DefaultShell: "/bin/systemd", + ExtraFiles: []string{ + somefile + ":bin/sh", + }, + }, + errs: []error{os.ErrExist, errDefaultshSymlink}, + validators: []itest.ArchiveValidator{ + itest.IsEmpty{}, + }, + }, + { + name: "file conflicts with defaultsh", + opts: Opts{ + TempDir: dir, + DefaultShell: "/bin/systemd", + ExtraFiles: []string{ + somefile + ":bin/defaultsh", + }, + }, + errs: []error{os.ErrExist, errDefaultshSymlink}, + validators: []itest.ArchiveValidator{ + itest.IsEmpty{}, + }, + }, + { + name: "file does not conflict if default files not specified", + opts: Opts{ + TempDir: dir, + // No DefaultShell, Init, or UinitCmd. + ExtraFiles: []string{ + somefile + ":bin/defaultsh", + somefile + ":bin/sh", + somefile + ":bin/uinit", + somefile + ":etc/uinit.flags", + somefile + ":init", + }, + }, + validators: []itest.ArchiveValidator{ + itest.HasContent{Path: "bin/defaultsh", Content: "foobar"}, + itest.HasContent{Path: "bin/sh", Content: "foobar"}, + itest.HasContent{Path: "bin/uinit", Content: "foobar"}, + itest.HasContent{Path: "etc/uinit.flags", Content: "foobar"}, + itest.HasContent{Path: "init", Content: "foobar"}, + }, + }, { name: "init specified, but not in commands", opts: Opts{ @@ -114,18 +295,30 @@ func TestCreateInitramfs(t *testing.T) { }, }, }, - want: "could not create symlink from \"init\" to \"foobar\": command or path \"foobar\" not included in u-root build: specify -initcmd=\"\" to ignore this error and build without an init (or, did you specify a list, and are you missing github.com/u-root/u-root/cmds/core/init?)", + errs: []error{errSymlink, errInitSymlink}, + validators: []itest.ArchiveValidator{ + itest.IsEmpty{}, + }, + }, + /* TODO: case broken. + { + name: "init not resolvable", + opts: Opts{ + TempDir: dir, + InitCmd: "init", + }, + errs: []error{errSymlink, errInitSymlink}, validators: []itest.ArchiveValidator{ itest.IsEmpty{}, }, }, + */ { name: "init symlinked to absolute path", opts: Opts{ TempDir: dir, InitCmd: "/bin/systemd", }, - want: "", validators: []itest.ArchiveValidator{ itest.HasRecord{R: cpio.Symlink("init", "bin/systemd")}, }, @@ -157,7 +350,6 @@ func TestCreateInitramfs(t *testing.T) { }, }, }, - want: "", validators: []itest.ArchiveValidator{ itest.HasRecord{R: cpio.Symlink("init", "bbin/init")}, @@ -173,13 +365,44 @@ func TestCreateInitramfs(t *testing.T) { itest.HasFile{Path: "bin/dd"}, }, }, + { + name: "glob fail", + opts: Opts{ + Env: golang.Default(golang.DisableCGO()), + TempDir: dir, + UrootSource: urootpath, + Commands: BinaryCmds("github.com/u-root/u-root/cmds/notexist/*"), + }, + errs: []error{errResolvePackage}, + validators: []itest.ArchiveValidator{ + itest.IsEmpty{}, + }, + }, + { + name: "tmp not writable", + opts: Opts{ + Env: golang.Default(golang.DisableCGO()), + TempDir: tmp400, + UrootSource: urootpath, + Commands: BinaryCmds("github.com/u-root/u-root/cmds/core/..."), + }, + errs: []error{os.ErrPermission}, + validators: []itest.ArchiveValidator{ + itest.IsEmpty{}, + }, + }, } { t.Run(fmt.Sprintf("Test %d [%s]", i, tt.name), func(t *testing.T) { archive := inMemArchive{cpio.InMemArchive()} tt.opts.OutputFile = archive - // Compare error type or error string. - if err := CreateInitramfs(l, tt.opts); (err != nil && err.Error() != tt.want) || (len(tt.want) > 0 && err == nil) { - t.Errorf("CreateInitramfs(%v) = %v, want %v", tt.opts, err, tt.want) + err := CreateInitramfs(l, tt.opts) + for _, want := range tt.errs { + if !errors.Is(err, want) { + t.Errorf("CreateInitramfs = %v, want %v", err, want) + } + } + if err != nil && len(tt.errs) == 0 { + t.Errorf("CreateInitramfs = %v, want %v", err, nil) } for _, v := range tt.validators {