Browse Source

package mountree

ZRY 2 months ago
parent
commit
b0d888c10c

+ 8 - 0
go.work

@@ -0,0 +1,8 @@
+go 1.21.6
+
+use (
+	amntfs
+	iofswrap
+	mountree
+	testing/mountree_test
+)

+ 3 - 0
mountree/README.md

@@ -0,0 +1,3 @@
+# mountree
+
+A mount point tree implementation for mountable filesystem.

+ 36 - 0
mountree/error.go

@@ -0,0 +1,36 @@
+package mountree
+
+type ErrNo uint8
+
+const (
+	ErrMountPointAlreadyExists          ErrNo = iota
+	ErrMountPointNotExists              ErrNo = iota
+	ErrNoAvailableMountPointForThisPath ErrNo = iota
+)
+
+func (e ErrNo) Error() string {
+	switch e {
+	case ErrMountPointAlreadyExists:
+		return "mount point already exists"
+	case ErrMountPointNotExists:
+		return "mount point not exists"
+	case ErrNoAvailableMountPointForThisPath:
+		return "no available mount point for this path"
+	default:
+		return "unknown error"
+	}
+}
+
+var _ error = (*ErrNo)(nil)
+
+func CheckErrorType(err error, errNo ErrNo) bool {
+	if err == nil {
+		return false
+	}
+	switch err.(type) {
+	case ErrNo:
+		return err.(ErrNo) == errNo
+	default:
+		return false
+	}
+}

+ 3 - 0
mountree/go.mod

@@ -0,0 +1,3 @@
+go 1.21
+
+module git.swzry.com/ProjectNagae/FsUtils/mountree

+ 164 - 0
mountree/mnt_tree.go

@@ -0,0 +1,164 @@
+package mountree
+
+import (
+	"strings"
+	"sync"
+)
+
+type TreeNode[T PayloadType] struct {
+	Path     []string
+	Children map[string]*TreeNode[T]
+	Parent   *TreeNode[T]
+	Payload  *T
+}
+
+type Tree[T PayloadType] struct {
+	root   *TreeNode[T]
+	rwlock sync.RWMutex
+}
+
+func NewTree[T PayloadType]() *Tree[T] {
+	tree := &Tree[T]{
+		root: &TreeNode[T]{
+			Path:     []string{},
+			Children: make(map[string]*TreeNode[T]),
+			Parent:   nil,
+			Payload:  nil,
+		},
+		rwlock: sync.RWMutex{},
+	}
+	return tree
+}
+
+func (t *Tree[T]) search(
+	pathSeq []string,
+	found func(fnode *TreeNode[T], remainPath []string, pnode *TreeNode[T]) error,
+	readonly bool,
+) error {
+	fspnode := t.root
+	if readonly {
+		defer t.rwlock.RUnlock()
+		t.rwlock.RLock()
+	} else {
+		defer t.rwlock.Unlock()
+		t.rwlock.Lock()
+	}
+	total := len(pathSeq)
+	if total <= 0 {
+		return found(t.root, []string{}, fspnode)
+	}
+	current := t.root
+	for i, v := range pathSeq {
+		if current.Children == nil || len(current.Children) == 0 {
+			return found(current, pathSeq[i:], fspnode)
+		}
+		if child, ok := current.Children[v]; ok {
+			current = child
+			if current.Payload != nil {
+				fspnode = current
+			}
+		} else {
+			return found(current, pathSeq[i:], fspnode)
+		}
+	}
+	return found(current, []string{}, fspnode)
+}
+
+func (t *Tree[T]) Mount(pathSeq []string, payload T, forceRemount bool) error {
+	return t.search(
+		pathSeq,
+		func(fnode *TreeNode[T], remainPath []string, pnode *TreeNode[T]) error {
+			if len(remainPath) == 0 {
+				if fnode.Payload != nil && !forceRemount {
+					return ErrMountPointAlreadyExists
+				}
+				fnode.Payload = &payload
+				return nil
+			}
+			currentPath := fnode.Path
+			currentNode := fnode
+			for _, v := range remainPath {
+				currentPath = append(currentPath, v)
+				newNode := &TreeNode[T]{
+					Path:     currentPath,
+					Children: make(map[string]*TreeNode[T]),
+					Parent:   currentNode,
+					Payload:  nil,
+				}
+				currentNode.Children[v] = newNode
+				currentNode = newNode
+			}
+			currentNode.Payload = &payload
+			return nil
+		},
+		false,
+	)
+}
+
+func (t *Tree[T]) Umount(pathSeq []string) error {
+	if len(pathSeq) == 0 {
+		t.root.Payload = nil
+		return nil
+	}
+	return t.search(
+		pathSeq,
+		func(fnode *TreeNode[T], remainPath []string, pnode *TreeNode[T]) error {
+			if len(remainPath) != 0 {
+				return ErrMountPointNotExists
+			}
+			if len(fnode.Children) > 0 {
+				fnode.Payload = nil
+				return nil
+			} else {
+				pnode := fnode.Parent
+				delete(pnode.Children, fnode.Path[len(fnode.Path)-1])
+				return nil
+			}
+		},
+		false,
+	)
+}
+
+func (t *Tree[T]) GetPayload(pathSeq []string) (payload *T, RemainPath []string, rerr error) {
+	payload = nil
+	RemainPath = pathSeq
+	rerr = t.search(
+		pathSeq,
+		func(fnode *TreeNode[T], remainPath []string, pnode *TreeNode[T]) error {
+			if fnode.Payload == nil {
+				if pnode.Payload == nil {
+					return ErrNoAvailableMountPointForThisPath
+				} else {
+					payload = pnode.Payload
+					xpl := len(pnode.Path)
+					RemainPath = pathSeq[xpl:]
+					return nil
+				}
+			} else {
+				RemainPath = remainPath
+				payload = fnode.Payload
+				return nil
+			}
+		},
+		true,
+	)
+	return
+}
+
+func (t *Tree[T]) ListAllMount(prt func(path string, payload T), sep string) {
+	stack := []*TreeNode[T]{t.root}
+
+	for len(stack) > 0 {
+		node := stack[len(stack)-1]
+		stack = stack[:len(stack)-1]
+
+		if node.Payload != nil {
+			prtPath := sep + strings.Join(node.Path, sep)
+			prt(prtPath, *node.Payload)
+		}
+
+		for _, child := range node.Children {
+			stack = append(stack, child)
+		}
+	}
+}

+ 6 - 0
mountree/payload_intf.go

@@ -0,0 +1,6 @@
+package mountree
+
+type PayloadType interface {
+	Name() string
+	Description() string
+}

+ 54 - 0
mountree/unix_path_wrap.go

@@ -0,0 +1,54 @@
+package mountree
+
+import (
+	libpath "path"
+	"strings"
+)
+
+type SimpleUnixLikePathTree[T PayloadType] struct {
+	baseTree *Tree[T]
+}
+
+func NewSimpleUnixLikePathTree[T PayloadType]() *SimpleUnixLikePathTree[T] {
+	return &SimpleUnixLikePathTree[T]{
+		baseTree: NewTree[T](),
+	}
+}
+
+func (t *SimpleUnixLikePathTree[T]) splitPath(path string) []string {
+	if path == "" {
+		return []string{}
+	}
+	sp := strings.Split(libpath.Clean(path), "/")
+	filtered := make([]string, 0, len(sp))
+	for _, str := range sp {
+		if str != "" {
+			filtered = append(filtered, str)
+		}
+	}
+	return filtered
+}
+
+func (t *SimpleUnixLikePathTree[T]) Mount(path string, payload T, forceRemount bool) error {
+	pseq := t.splitPath(path)
+	return t.baseTree.Mount(pseq, payload, forceRemount)
+}
+
+func (t *SimpleUnixLikePathTree[T]) Umount(path string) error {
+	pseq := t.splitPath(path)
+	return t.baseTree.Umount(pseq)
+}
+
+func (t *SimpleUnixLikePathTree[T]) GetPayload(path string) (*T, string, error) {
+	pseq := t.splitPath(path)
+	payload, rpseq, err := t.baseTree.GetPayload(pseq)
+	if err != nil {
+		return nil, path, err
+	}
+	rp := strings.Join(rpseq, "/")
+	return payload, rp, nil
+}
+
+func (t *SimpleUnixLikePathTree[T]) ListAllMount(prt func(path string, payload T)) {
+	t.baseTree.ListAllMount(prt, "/")
+}

+ 3 - 0
testing/mountree_test/go.mod

@@ -0,0 +1,3 @@
+go 1.21
+
+module git.swzry.com/ProjectNagae/FsUtils/testing/mountree_test

+ 380 - 0
testing/mountree_test/unix_mountree_test.go

@@ -0,0 +1,380 @@
+package mountree_test
+
+import (
+	"git.swzry.com/ProjectNagae/FsUtils/mountree"
+	"testing"
+)
+
+type Mount struct {
+	FileSystemName string
+	MountPoint     string
+}
+
+type TestCase struct {
+	Path           string
+	FileSystemName string
+	RemainPath     string
+}
+
+type Payload struct {
+	FsName string
+}
+
+func (p *Payload) Name() string {
+	return p.FsName
+}
+
+func (p *Payload) Description() string {
+	return p.FsName
+}
+
+var _ mountree.PayloadType = (*Payload)(nil)
+
+var FS *mountree.SimpleUnixLikePathTree[*Payload]
+
+func TestAll(t *testing.T) {
+	t.Log("==== Phase1 ====")
+	mounts1 := []Mount{
+		{"fs1", "/"},
+		{"fs2", "/foo"},
+		{"fs3", "/foo/bar"},
+		{"fs4", "/foo/baz"},
+		{"fs5", "/foo/baz/qux/tac/mud"},
+		{"fs6", "/foo/baz/qux/fox"},
+	}
+	for _, mount := range mounts1 {
+		err := FS.Mount(mount.MountPoint, &Payload{FsName: mount.FileSystemName}, false)
+		if err != nil {
+			t.Fatalf("failed to mount %v: %v", mount.MountPoint, err)
+		} else {
+			t.Logf("mounted %s at %s.", mount.FileSystemName, mount.MountPoint)
+		}
+	}
+	t.Log("-- Mount Test OK --")
+	t.Log("Mount Table:")
+	FS.ListAllMount(func(path string, payload *Payload) {
+		t.Logf("\t%s\t\t%s", payload.Name(), path)
+	})
+	t.Log("-- List Mount Table OK --")
+	tc1 := []TestCase{
+		{"/", "fs1", ""},
+		{"///", "fs1", ""},
+		{"", "fs1", ""},
+		{"/foobar", "fs1", "foobar"},
+		{"/reimu", "fs1", "reimu"},
+		{"/foobar/satori", "fs1", "foobar/satori"},
+		{"/reimu/cirno", "fs1", "reimu/cirno"},
+		{"/foo", "fs2", ""},
+		{"/foo/cirno", "fs2", "cirno"},
+		{"/foo/cirno/satori", "fs2", "cirno/satori"},
+		{"/foo/cirno/satori_komeiji", "fs2", "cirno/satori_komeiji"},
+		{"/foo/bar", "fs3", ""},
+		{"/foo/bar/////", "fs3", ""},
+		{"/foo/bar/", "fs3", ""},
+		{"/foo/bar/koishi", "fs3", "koishi"},
+		{"/foo/baz", "fs4", ""},
+		{"/foo/baz/qux114514", "fs4", "qux114514"},
+		{"/foo/baz/qux114514.fox", "fs4", "qux114514.fox"},
+		{"/foo/baz/qux/tac/mud", "fs5", ""},
+		{"/foo/baz/qux/tac/mud/can", "fs5", "can"},
+		{"/foo/baz/qux/fox", "fs6", ""},
+		{"/foo/baz/qux/fox/can/tell/you", "fs6", "can/tell/you"},
+	}
+	for _, tc := range tc1 {
+		payload, remain, err := FS.GetPayload(tc.Path)
+		if err != nil {
+			t.Fatalf("failed to get payload for %s: %v", tc, err)
+		}
+		if (*payload).Name() != tc.FileSystemName {
+			t.Fatalf("at '%s': payload mismatch: want %#v, got %#v", tc.Path, tc.FileSystemName, *payload)
+		}
+		if remain != tc.RemainPath {
+			t.Fatalf("at '%s': remain path mismatch: want %#v, got %#v", tc.Path, tc.RemainPath, remain)
+		}
+		t.Logf("at '%s': payload: %s, remain: %s", tc.Path, *payload, remain)
+	}
+	t.Log("-- GetPayload Test OK --")
+	t.Log("==== Phase2 ====")
+	umounts1 := []string{
+		"/foo/baz/qux/fox",
+		"/foo/bar",
+	}
+	for _, mount := range umounts1 {
+		err := FS.Umount(mount)
+		if err != nil {
+			t.Fatalf("failed to umount %v: %v", mount, err)
+		} else {
+			t.Logf("umounted %s.", mount)
+		}
+	}
+	t.Log("-- Umount Test OK --")
+	t.Log("Mount Table:")
+	FS.ListAllMount(func(path string, payload *Payload) {
+		t.Logf("\t%s\t\t%s", payload.Name(), path)
+	})
+	t.Log("-- List Mount Table OK --")
+	tc2 := []TestCase{
+		{"/", "fs1", ""},
+		{"///", "fs1", ""},
+		{"", "fs1", ""},
+		{"/foobar", "fs1", "foobar"},
+		{"/reimu", "fs1", "reimu"},
+		{"/foobar/satori", "fs1", "foobar/satori"},
+		{"/reimu/cirno", "fs1", "reimu/cirno"},
+		{"/foo", "fs2", ""},
+		{"/foo/cirno", "fs2", "cirno"},
+		{"/foo/cirno/satori", "fs2", "cirno/satori"},
+		{"/foo/cirno/satori_komeiji", "fs2", "cirno/satori_komeiji"},
+		{"/foo/bar", "fs2", "bar"},
+		{"/foo/bar/////", "fs2", "bar"},
+		{"/foo/bar/", "fs2", "bar"},
+		{"/foo/bar/koishi", "fs2", "bar/koishi"},
+		{"/foo/baz", "fs4", ""},
+		{"/foo/baz/qux114514", "fs4", "qux114514"},
+		{"/foo/baz/qux114514.fox", "fs4", "qux114514.fox"},
+		{"/foo/baz/qux/tac/mud", "fs5", ""},
+		{"/foo/baz/qux/tac/mud/can", "fs5", "can"},
+		{"/foo/baz/qux/fox", "fs4", "qux/fox"},
+		{"/foo/baz/qux/fox/can/tell/you", "fs4", "qux/fox/can/tell/you"},
+	}
+	for _, tc := range tc2 {
+		payload, remain, err := FS.GetPayload(tc.Path)
+		if err != nil {
+			t.Fatalf("failed to get payload for %s: %v", tc.Path, err.Error())
+		}
+		if (*payload).Name() != tc.FileSystemName {
+			t.Fatalf("at '%s': payload mismatch: want %#v, got %#v", tc.Path, tc.FileSystemName, *payload)
+		}
+		if remain != tc.RemainPath {
+			t.Fatalf("at '%s': remain path mismatch: want %#v, got %#v", tc.Path, tc.RemainPath, remain)
+		}
+		t.Logf("at '%s': payload: %s, remain: %s", tc.Path, (*payload).Name(), remain)
+	}
+	t.Log("-- GetPayload Test OK --")
+	t.Log("==== Phase3 ====")
+	t.Log("mount an already mounted path '/' without force")
+	err := FS.Mount("/", &Payload{FsName: "errfs1"}, false)
+	if err != nil {
+		if mountree.CheckErrorType(err, mountree.ErrMountPointAlreadyExists) {
+			t.Log("Ok")
+		} else {
+			t.Fatalf(
+				"unexpected error type: want '%s', got '%s'",
+				mountree.ErrMountPointAlreadyExists.Error(),
+				err.Error(),
+			)
+		}
+	} else {
+		t.Fatal("test failed: this action should cause error")
+	}
+	t.Log("mount an already mounted path '/foo' without force")
+	err = FS.Mount("/", &Payload{FsName: "errfs2"}, false)
+	if err != nil {
+		if mountree.CheckErrorType(err, mountree.ErrMountPointAlreadyExists) {
+			t.Log("Ok")
+		} else {
+			t.Fatalf(
+				"unexpected error type: want '%s', got '%s'",
+				mountree.ErrMountPointAlreadyExists.Error(),
+				err.Error(),
+			)
+		}
+	} else {
+		t.Fatal("test failed: this action should cause error")
+	}
+	t.Log("mount an already mounted path '/' with force")
+	err = FS.Mount("/", &Payload{FsName: "fs7"}, true)
+	if err != nil {
+		t.Fatal("force remount '/' failed: ", err.Error())
+	} else {
+		t.Log("Ok")
+	}
+	t.Log("Mount Table:")
+	FS.ListAllMount(func(path string, payload *Payload) {
+		t.Logf("\t%s\t\t%s", payload.Name(), path)
+	})
+	t.Log("-- List Mount Table OK --")
+	tc3 := []TestCase{
+		{"/", "fs7", ""},
+		{"///", "fs7", ""},
+		{"", "fs7", ""},
+		{"/foobar", "fs7", "foobar"},
+		{"/foo/baz/qux/tac/mud", "fs5", ""},
+		{"/foo/baz/qux/tac/mud/can", "fs5", "can"},
+	}
+	for _, tc := range tc3 {
+		payload, remain, err := FS.GetPayload(tc.Path)
+		if err != nil {
+			t.Fatalf("failed to get payload for %s: %v", tc.Path, err.Error())
+		}
+		if (*payload).Name() != tc.FileSystemName {
+			t.Fatalf("at '%s': payload mismatch: want %#v, got %#v", tc.Path, tc.FileSystemName, *payload)
+		}
+		if remain != tc.RemainPath {
+			t.Fatalf("at '%s': remain path mismatch: want %#v, got %#v", tc.Path, tc.RemainPath, remain)
+		}
+		t.Logf("at '%s': payload: %s, remain: %s", tc.Path, (*payload).Name(), remain)
+	}
+	t.Log("-- GetPayload Test OK --")
+	t.Log("==== Phase4 ====")
+	t.Log("umount mount point that not exists: '/foo/cirno'")
+	err = FS.Umount("/foo/cirno")
+	if err != nil {
+		if mountree.CheckErrorType(err, mountree.ErrMountPointNotExists) {
+			t.Log("Ok")
+		} else {
+			t.Fatalf(
+				"unexpected error type: want '%s', got '%s'",
+				mountree.ErrMountPointNotExists.Error(),
+				err.Error(),
+			)
+		}
+	} else {
+		t.Fatal("test failed: this action should cause error")
+	}
+	t.Log("umount '/'")
+	err = FS.Umount("/")
+	if err != nil {
+		t.Fatal("umount '/' failed: ", err.Error())
+	} else {
+		t.Log("Ok")
+	}
+	t.Log("Mount Table:")
+	FS.ListAllMount(func(path string, payload *Payload) {
+		t.Logf("\t%s\t\t%s", payload.Name(), path)
+	})
+	t.Log("-- List Mount Table OK --")
+	t.Log("test for '/'")
+	_, _, err = FS.GetPayload("/")
+	if err != nil {
+		if mountree.CheckErrorType(err, mountree.ErrNoAvailableMountPointForThisPath) {
+			t.Log("Ok")
+		} else {
+			t.Fatalf(
+				"unexpected error type: want '%s', got '%s'",
+				mountree.ErrNoAvailableMountPointForThisPath.Error(),
+				err.Error(),
+			)
+		}
+	} else {
+		t.Fatal("test failed: this action should cause error")
+	}
+	t.Log("test for '/cirno/koishi'")
+	_, _, err = FS.GetPayload("/cirno/koishi")
+	if err != nil {
+		if mountree.CheckErrorType(err, mountree.ErrNoAvailableMountPointForThisPath) {
+			t.Log("Ok")
+		} else {
+			t.Fatalf(
+				"unexpected error type: want '%s', got '%s'",
+				mountree.ErrNoAvailableMountPointForThisPath.Error(),
+				err.Error(),
+			)
+		}
+	} else {
+		t.Fatal("test failed: this action should cause error")
+	}
+	t.Log("==== Phase5 ====")
+	mounts2 := []Mount{
+		{"fs8", "/"},
+		{"fs9", "/foo/bar"},
+		{"fs10", "/foo/baz/qux/fox"},
+		{"fs11", "/foo/baz/lut"},
+		{"fs12", "/foo/baz/cuk"},
+		{"fs13", "/foo/baz/zek"},
+		{"fs14", "/foo/baz/zek/cirno"},
+	}
+	for _, mount := range mounts2 {
+		err := FS.Mount(mount.MountPoint, &Payload{FsName: mount.FileSystemName}, false)
+		if err != nil {
+			t.Fatalf("failed to mount %v: %v", mount.MountPoint, err)
+		} else {
+			t.Logf("mounted %s at %s.", mount.FileSystemName, mount.MountPoint)
+		}
+	}
+	t.Log("-- Mount Test OK --")
+	t.Log("Mount Table:")
+	FS.ListAllMount(func(path string, payload *Payload) {
+		t.Logf("\t%s\t\t%s", payload.Name(), path)
+	})
+	t.Log("-- List Mount Table OK --")
+	tc4 := []TestCase{
+		{"/", "fs8", ""},
+		{"///", "fs8", ""},
+		{"", "fs8", ""},
+		{"/foo/bar/810", "fs9", "810"},
+		{"/foo/baz/qux/tac/mud", "fs5", ""},
+		{"/foo/baz/qux/tac/mud/can", "fs5", "can"},
+		{"/foo/baz/qux/fox/cat/dog", "fs10", "cat/dog"},
+		{"/foo/baz/lut/fox/cat/dog", "fs11", "fox/cat/dog"},
+		{"/foo/baz/cuk/cat/dog/fox", "fs12", "cat/dog/fox"},
+		{"/foo/baz/zek", "fs13", ""},
+		{"/foo/baz/zek/cirno", "fs14", ""},
+		{"/foo/baz/zek/cirno/satori", "fs14", "satori"},
+		{"/foo/baz", "fs4", ""},
+		{"/foo/baz/zun", "fs4", "zun"},
+	}
+	for _, tc := range tc4 {
+		payload, remain, err := FS.GetPayload(tc.Path)
+		if err != nil {
+			t.Fatalf("failed to get payload for %s: %v", tc.Path, err.Error())
+		}
+		if (*payload).Name() != tc.FileSystemName {
+			t.Fatalf("at '%s': payload mismatch: want %#v, got %#v", tc.Path, tc.FileSystemName, *payload)
+		}
+		if remain != tc.RemainPath {
+			t.Fatalf("at '%s': remain path mismatch: want %#v, got %#v", tc.Path, tc.RemainPath, remain)
+		}
+		t.Logf("at '%s': payload: %s, remain: %s", tc.Path, (*payload).Name(), remain)
+	}
+	t.Log("-- GetPayload Test OK --")
+	t.Log("==== Phase5 ====")
+
+	t.Log("umount '/foo/baz'")
+	err = FS.Umount("/foo/baz")
+	if err != nil {
+		t.Fatal("umount '/' failed: ", err.Error())
+	} else {
+		t.Log("Ok")
+	}
+	t.Log("Mount Table:")
+	FS.ListAllMount(func(path string, payload *Payload) {
+		t.Logf("\t%s\t\t%s", payload.Name(), path)
+	})
+	t.Log("-- List Mount Table OK --")
+	tc5 := []TestCase{
+		{"/", "fs8", ""},
+		{"///", "fs8", ""},
+		{"", "fs8", ""},
+		{"/foo/bar/810", "fs9", "810"},
+		{"/foo/baz/qux/tac/mud", "fs5", ""},
+		{"/foo/baz/qux/tac/mud/can", "fs5", "can"},
+		{"/foo/baz/qux/fox/cat/dog", "fs10", "cat/dog"},
+		{"/foo/baz/lut/fox/cat/dog", "fs11", "fox/cat/dog"},
+		{"/foo/baz/cuk/cat/dog/fox", "fs12", "cat/dog/fox"},
+		{"/foo/baz/zek", "fs13", ""},
+		{"/foo/baz/zek/cirno", "fs14", ""},
+		{"/foo/baz/zek/cirno/satori", "fs14", "satori"},
+		{"/foo/baz", "fs2", "baz"},
+		{"/foo/baz/zun", "fs2", "baz/zun"},
+	}
+	for _, tc := range tc5 {
+		payload, remain, err := FS.GetPayload(tc.Path)
+		if err != nil {
+			t.Fatalf("failed to get payload for %s: %v", tc.Path, err.Error())
+		}
+		if (*payload).Name() != tc.FileSystemName {
+			t.Fatalf("at '%s': payload mismatch: want %#v, got %#v", tc.Path, tc.FileSystemName, *payload)
+		}
+		if remain != tc.RemainPath {
+			t.Fatalf("at '%s': remain path mismatch: want %#v, got %#v", tc.Path, tc.RemainPath, remain)
+		}
+		t.Logf("at '%s': payload: %s, remain: %s", tc.Path, (*payload).Name(), remain)
+	}
+	t.Log("-- GetPayload Test OK --")
+}
+
+func TestMain(m *testing.M) {
+	FS = mountree.NewSimpleUnixLikePathTree[*Payload]()
+	m.Run()
+}