package testsuite import ( "errors" "fmt" "io/fs" "path/filepath" "strings" ) var ( // ErrFSBadLength is returned by [FS.Compare] for a directory with an // unexpected amount of dents. ErrFSBadLength = errors.New("bad dir length") // ErrFSBadData is returned by [FS.Compare] for a file with unexpected // contents. ErrFSBadData = errors.New("data differs") // ErrFSBadMode is returned by [FS.Compare] for an entry with unexpected // mode. ErrFSBadMode = errors.New("mode differs") // ErrFSInvalidEnt is returned by [FS.Compare] if an invalid [FS] is visited. ErrFSInvalidEnt = errors.New("invalid entry condition") ) // FS represents part of a filesystem hierarchy. type FS struct { // Expected mode of corresponding entry. Mode fs.FileMode `json:"mode"` // Expected directory contents. The directory is not descended if Dir is nil. Dir map[string]*FS `json:"dir"` // Expected file contents. The file is not read if Data is nil. Data *string `json:"data"` } // dprintf calls printf if it is non-nil. func dprintf(printf func(format string, a ...any), format string, a ...any) { if printf == nil { return } printf(format, a...) } // printDir prints a failed [FS.Compare] directory. func printDir( printf func(format string, a ...any), prefix string, dir []fs.DirEntry, ) { names := make([]string, len(dir)) for i, ent := range dir { name := ent.Name() if ent.IsDir() { name += "/" } names[i] = fmt.Sprintf("%q", name) } dprintf(printf, "[FAIL] d %s: %s", prefix, strings.Join(names, " ")) } // Compare compares the contents of prefix against the hierarchy described by s. func (s *FS) Compare( printf func(format string, a ...any), prefix string, e fs.FS, ) error { if s.Data != nil { if s.Dir != nil { panic("invalid state") } panic("invalid compare call") } if s.Dir == nil { dprintf(printf, "[ OK ] s %s", prefix) return nil } var dir []fs.DirEntry if d, err := fs.ReadDir(e, prefix); err != nil { return err } else if len(d) != len(s.Dir) { printDir(printf, prefix, d) return ErrFSBadLength } else { dir = d } for _, got := range dir { name := got.Name() if want, ok := s.Dir[name]; !ok { printDir(printf, prefix, dir) return fs.ErrNotExist } else if want.Dir != nil && !got.IsDir() { printDir(printf, prefix, dir) return ErrFSInvalidEnt } else { name = filepath.Join(prefix, name) if fi, err := got.Info(); err != nil { return err } else if fi.Mode() != want.Mode { dprintf(printf, "[FAIL] m %s: %#o, want %#o", name, uint32(fi.Mode()), uint32(want.Mode)) return ErrFSBadMode } if want.Data != nil { if want.Dir != nil { panic("invalid state") } if v, err := fs.ReadFile(e, name); err != nil { return err } else if string(v) != *want.Data { dprintf(printf, "[FAIL] f %s\n\t got: %s\n\twant: %s", name, v, *want.Data, ) return ErrFSBadData } dprintf(printf, "[ OK ] f %s", name) } else if err := want.Compare(printf, name, e); err != nil { return err } } } dprintf(printf, "[ OK ] d %s", prefix) return nil }