diff --git a/.vscode/settings.json b/.vscode/settings.json index e23a1f2..74db50d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "astrolib", "Berthe", "bodyclose", + "chardata", "Clonable", "cmds", "Cobrass", @@ -51,6 +52,7 @@ "linters", "lorax", "mattn", + "musico", "nakedret", "nolint", "nolintlint", diff --git a/internal/helpers/directory-tree-builder.go b/internal/helpers/directory-tree-builder.go new file mode 100644 index 0000000..1c93c91 --- /dev/null +++ b/internal/helpers/directory-tree-builder.go @@ -0,0 +1,359 @@ +package helpers + +import ( + "bytes" + "encoding/xml" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "testing/fstest" + + "github.com/snivilised/extendio/collections" + "github.com/snivilised/traverse/internal/lo" + + "github.com/snivilised/extendio/xfs/utils" +) + +const ( + offset = 2 + tabSize = 2 + doWrite = true +) + +func Musico(portion string, verbose bool) (fsys fstest.MapFS, root string) { + fsys = fstest.MapFS{ + ".": &fstest.MapFile{ + Mode: os.ModeDir, + }, + } + + return fsys, Provision(NewMemWriteProvider(fsys, os.ReadFile, portion), verbose) +} + +func Provision(provider *IOProvider, verbose bool) (root string) { + repo := Repo(filepath.Join("..", "..", "test", "data", "MUSICO")) + utils.Must(ensure(repo, provider, verbose)) + + if verbose { + fmt.Printf("\n🤖 re-generated tree at '%v'\n", repo) + } + + return repo +} + +// ensure +func ensure(root string, provider *IOProvider, verbose bool) error { + repo := Repo(filepath.Join("..", "..")) + index := Path(repo, "test/data/musico-index.xml") + parent, _ := utils.SplitParent(root) + builder := directoryTreeBuilder{ + root: TrimRoot(root), + stack: collections.NewStackWith([]string{parent}), + index: index, + doWrite: doWrite, + provider: provider, + verbose: verbose, + show: func(path string, exists existsEntry) { + if !verbose { + return + } + + status := lo.Ternary(exists(path), "✅", "❌") + + fmt.Printf("---> %v path: '%v'\n", status, path) + }, + } + + return builder.walk() +} + +func TrimRoot(root string) string { + // omit leading '/', because test-fs stupidly doesn't like it, + // so we have to jump through hoops + if strings.HasPrefix(root, string(filepath.Separator)) { + return root[1:] + } + + pattern := `^[a-zA-Z]:[\\/]*` + re := regexp.MustCompile(pattern) + + return re.ReplaceAllString(root, "") +} + +// NewMemWriteProvider +func NewMemWriteProvider(store fstest.MapFS, indexReader readFile, portion string) *IOProvider { + filter := lo.Ternary(portion != "", + matcher(func(path string) bool { + return strings.Contains(path, portion) + }), + matcher(func(string) bool { + return true + }), + ) + + // PS: to check the existence of a path in an fs in production + // code, use fs.Stat(fsys, path) instead of os.Stat/os.Lstat + + filePresent := existsEntry(func(path string) bool { + entry, ok := store[path] + return ok && !entry.Mode.IsDir() + }) + + folderPresent := existsEntry(func(path string) bool { + entry, ok := store[path] + return ok && entry.Mode.IsDir() + }) + + return &IOProvider{ + filter: filter, + file: fileHandler{ + in: indexReader, + out: writeFile(func(name string, data []byte, mode os.FileMode, show display) error { + if name == "" { + return nil + } + + if filter(name) { + trimmed := TrimRoot(name) + store[trimmed] = &fstest.MapFile{ + Data: data, + Mode: mode, + } + show(trimmed, filePresent) + } + + return nil + }), + exists: filePresent, + }, + folder: folderHandler{ + out: writeFolder(func(path string, mode os.FileMode, show display, isRoot bool) error { + if path == "" { + return nil + } + + if isRoot || filter(path) { + trimmed := TrimRoot(path) + store[trimmed] = &fstest.MapFile{ + Mode: mode | os.ModeDir, + } + show(trimmed, folderPresent) + } + + return nil + }), + exists: folderPresent, + }, + } +} + +type ( + entryExists interface { + exists(path string) bool + } + + existsEntry func(path string) bool + + display func(path string, exists existsEntry) + + fileReader interface { + read(name string) ([]byte, error) + } + + readFile func(name string) ([]byte, error) + + fileWriter interface { + write(name string, data []byte, perm os.FileMode, show display) error + } + + writeFile func(name string, data []byte, perm os.FileMode, show display) error + + folderWriter interface { + write(path string, perm os.FileMode, show display, isRoot bool) error + } + + writeFolder func(path string, perm os.FileMode, show display, isRoot bool) error + + filter interface { + match(portion string) bool + } + + matcher func(portion string) bool + + fileHandler struct { + in fileReader + out fileWriter + exists entryExists + } + + folderHandler struct { + out folderWriter + exists existsEntry + } + + IOProvider struct { + filter filter + file fileHandler + folder folderHandler + } + + Tree struct { + XMLName xml.Name `xml:"tree"` + Root Directory `xml:"directory"` + } + + Directory struct { + XMLName xml.Name `xml:"directory"` + Name string `xml:"name,attr"` + Files []File `xml:"file"` + Directories []Directory `xml:"directory"` + } + + File struct { + XMLName xml.Name `xml:"file"` + Name string `xml:"name,attr"` + Text string `xml:",chardata"` + } +) + +func (fn readFile) read(name string) ([]byte, error) { + return fn(name) +} + +func (fn writeFile) write(name string, data []byte, perm os.FileMode, show display) error { + return fn(name, data, perm, show) +} + +func (fn existsEntry) exists(path string) bool { + return fn(path) +} + +func (fn writeFolder) write(path string, perm os.FileMode, show display, isRoot bool) error { + return fn(path, perm, show, isRoot) +} + +func (fn matcher) match(portion string) bool { + return fn(portion) +} + +// directoryTreeBuilder +type directoryTreeBuilder struct { + root string + full string + stack *collections.Stack[string] + index string + doWrite bool + depth int + padding string + provider *IOProvider + verbose bool + show display +} + +func (r *directoryTreeBuilder) read() (*Directory, error) { + data, err := r.provider.file.in.read(r.index) + + if err != nil { + return nil, err + } + + var tree Tree + + if ue := xml.Unmarshal(data, &tree); ue != nil { + return nil, ue + } + + return &tree.Root, nil +} + +func (r *directoryTreeBuilder) status(path string) string { + return lo.Ternary(utils.Exists(path), "✅", "❌") +} + +func (r *directoryTreeBuilder) pad() string { + return string(bytes.Repeat([]byte{' '}, (r.depth+offset)*tabSize)) +} + +func (r *directoryTreeBuilder) refill() string { + segments := r.stack.Content() + return filepath.Join(segments...) +} + +func (r *directoryTreeBuilder) inc(name string) { + r.stack.Push(name) + r.full = r.refill() + + r.depth++ + r.padding = r.pad() +} + +func (r *directoryTreeBuilder) dec() { + _, _ = r.stack.Pop() + r.full = r.refill() + + r.depth-- + r.padding = r.pad() +} + +func (r *directoryTreeBuilder) showL(path, indicator, name string) { + if r.verbose { + status := r.status(path) + fmt.Printf("%v(depth: '%v') (%v) %v: -> '%v' (🔥 %v)\n", + r.padding, r.depth, status, indicator, name, r.full, + ) + } +} + +func (r *directoryTreeBuilder) walk() error { + top, err := r.read() + + if err != nil { + return err + } + + r.full = r.root + + return r.dir(*top, true) +} + +func (r *directoryTreeBuilder) dir(dir Directory, isRoot bool) error { //nolint:gocritic // performance is not a concern + r.inc(dir.Name) + + if r.doWrite { + if err := r.provider.folder.out.write( + r.full, + os.ModePerm, + r.show, + isRoot, + ); err != nil { + return err + } + } + + for _, directory := range dir.Directories { + if err := r.dir(directory, false); err != nil { + return err + } + } + + for _, file := range dir.Files { + full := Path(r.full, file.Name) + + if r.doWrite { + if err := r.provider.file.out.write( + full, + []byte(file.Text), + os.ModePerm, + r.show, + ); err != nil { + return err + } + } + } + + r.dec() + + return nil +} diff --git a/internal/kernel/navigator-universal_test.go b/internal/kernel/navigator-universal_test.go index 394e520..9ca4e2e 100644 --- a/internal/kernel/navigator-universal_test.go +++ b/internal/kernel/navigator-universal_test.go @@ -11,18 +11,27 @@ import ( . "github.com/onsi/gomega" //nolint:revive // ok tv "github.com/snivilised/traverse" + "github.com/snivilised/traverse/internal/helpers" "github.com/snivilised/traverse/internal/services" ) -var _ = Describe("NavigatorUniversal", func() { - var memFS fstest.MapFS +var _ = Describe("NavigatorUniversal", Ordered, func() { + var ( + memFS fstest.MapFS + root string + ) + + BeforeAll(func() { + const ( + verbose = true + ) + var portion = filepath.Join("MUSICO", "bass") + memFS, root = helpers.Musico(portion, verbose) + Expect(root).NotTo(BeEmpty()) + }) BeforeEach(func() { services.Reset() - memFS = fstest.MapFS{ - filepath.Join(RootPath, "foo.txt"): {}, - RootPath: {}, - } }) Context("nav", func() { @@ -35,7 +44,7 @@ var _ = Describe("NavigatorUniversal", func() { _, err := tv.Walk().Configure().Extent(tv.Prime( &tv.Using{ - Root: RootPath, + Root: root, Subscription: tv.SubscribeUniversal, Handler: func(_ *tv.Node) error { return nil @@ -45,7 +54,12 @@ var _ = Describe("NavigatorUniversal", func() { }, }, - tv.WithHookQueryStatus(memFS.Stat), + tv.WithHookQueryStatus(func(path string) (fs.FileInfo, error) { + return memFS.Stat(helpers.TrimRoot(path)) + }), + tv.WithHookReadDirectory(func(_ fs.FS, dirname string) ([]fs.DirEntry, error) { + return memFS.ReadDir(helpers.TrimRoot(dirname)) + }), ), ).Navigate(ctx) diff --git a/test/data/musico-index.xml b/test/data/musico-index.xml new file mode 100644 index 0000000..443ceb4 --- /dev/null +++ b/test/data/musico-index.xml @@ -0,0 +1,1019 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 177 + 656 + +