1
0
Fork 0

Adding upstream version 3.10.8.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-18 09:37:23 +02:00
parent 37e9b6d587
commit 03bfe4079e
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
356 changed files with 28857 additions and 0 deletions

124
tree/f3/f3.go Normal file
View file

@ -0,0 +1,124 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"context"
"fmt"
"code.forgejo.org/f3/gof3/v3/f3"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/options"
"code.forgejo.org/f3/gof3/v3/path"
objects_helper "code.forgejo.org/f3/gof3/v3/tree/f3/objects"
"code.forgejo.org/f3/gof3/v3/tree/generic"
)
type treeF3 struct {
generic.Tree
options options.Interface
objectsHelper objects_helper.Interface
}
type setFunc func(parent path.Path, node generic.NodeInterface)
type TreeInterface interface {
generic.TreeInterface
IsContainer(kind.Kind) bool
NewFormat(kind kind.Kind) f3.Interface
CreateChild(ctx context.Context, pathString string, set setFunc)
GetObjectsHelper() objects_helper.Interface
}
func (o *treeF3) GetChildrenKind(parentKind kind.Kind) kind.Kind {
if childrenKind, ok := childrenKind[parentKind]; ok {
return childrenKind
}
panic(fmt.Errorf("unexpected kind %s", parentKind))
}
func (o *treeF3) IsContainer(kind kind.Kind) bool {
if isContainer, ok := isContainer[kind]; ok {
return isContainer
}
return false
}
func (o *treeF3) NewFormat(kind kind.Kind) f3.Interface {
return f3.New(string(kind))
}
func (o *treeF3) GetObjectsHelper() objects_helper.Interface {
return o.objectsHelper
}
func (o *treeF3) CreateChild(ctx context.Context, pathString string, set setFunc) {
p := generic.NewPathFromString(pathString)
o.Apply(ctx, p, generic.NewApplyOptions(func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
o.Trace("%s %s | %T %s", parent.String(), node.GetID(), node, node.GetKind())
child := o.Factory(ctx, o.GetChildrenKind(node.GetKind()))
child.SetParent(node)
childParentPath := parent.Append(node)
if set != nil {
set(childParentPath, child)
}
child.Upsert(ctx)
child.List(ctx)
}))
}
func newTreeF3(ctx context.Context, opts options.Interface) generic.TreeInterface {
tree := &treeF3{}
tree.Init(tree, opts)
tree.objectsHelper = objects_helper.NewObjectsHelper()
tree.SetDriver(GetForgeFactory(opts.GetName())(tree, opts))
tree.Register(kind.KindRoot, func(ctx context.Context, k kind.Kind) generic.NodeInterface {
return newFixedChildrenNode(ctx, tree, []kind.Kind{KindForge})
})
tree.Register(KindForge, func(ctx context.Context, k kind.Kind) generic.NodeInterface {
return newFixedChildrenNode(ctx, tree, []kind.Kind{KindTopics, KindUsers, KindOrganizations})
})
tree.Register(KindUser, func(ctx context.Context, k kind.Kind) generic.NodeInterface {
return newFixedChildrenNode(ctx, tree, []kind.Kind{KindProjects})
})
tree.Register(KindOrganization, func(ctx context.Context, k kind.Kind) generic.NodeInterface {
return newFixedChildrenNode(ctx, tree, []kind.Kind{KindProjects})
})
tree.Register(KindProject, func(ctx context.Context, k kind.Kind) generic.NodeInterface {
return newFixedChildrenNode(ctx, tree, []kind.Kind{KindRepositories, KindLabels, KindMilestones, KindIssues, KindPullRequests, KindReleases})
})
tree.Register(KindIssue, func(ctx context.Context, k kind.Kind) generic.NodeInterface {
return newFixedChildrenNode(ctx, tree, []kind.Kind{KindComments, KindReactions})
})
tree.Register(KindRepository, func(ctx context.Context, k kind.Kind) generic.NodeInterface {
return newRepositoryNode(ctx, tree)
})
tree.Register(KindPullRequest, func(ctx context.Context, k kind.Kind) generic.NodeInterface {
return newFixedChildrenNode(ctx, tree, []kind.Kind{KindComments, KindReactions, KindReviews})
})
tree.Register(KindReview, func(ctx context.Context, k kind.Kind) generic.NodeInterface {
return newFixedChildrenNode(ctx, tree, []kind.Kind{KindReviewComments, KindReactions})
})
tree.Register(KindComment, func(ctx context.Context, k kind.Kind) generic.NodeInterface {
return newFixedChildrenNode(ctx, tree, []kind.Kind{KindReactions})
})
tree.Register(KindRelease, func(ctx context.Context, k kind.Kind) generic.NodeInterface {
return newFixedChildrenNode(ctx, tree, []kind.Kind{KindAssets})
})
root := tree.Factory(ctx, kind.KindRoot)
tree.SetRoot(root)
return tree
}
func init() {
generic.RegisterFactory("f3", newTreeF3)
}

49
tree/f3/fixed_children.go Normal file
View file

@ -0,0 +1,49 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"context"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/tree/generic"
)
type fixedChildrenNode struct {
generic.Node
kinds []kind.Kind
}
func (o *fixedChildrenNode) GetChildren() generic.ChildrenSlice {
children := generic.NewChildrenSlice(len(o.kinds))
for _, kind := range o.kinds {
child := o.GetChild(id.NewNodeID(kind))
children = append(children, child)
}
return children
}
func (o *fixedChildrenNode) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
if page > 1 {
return generic.NewChildrenSlice(0)
}
return o.GetChildren()
}
func newFixedChildrenNode(ctx context.Context, tree generic.TreeInterface, kinds []kind.Kind) generic.NodeInterface {
node := &fixedChildrenNode{}
node.Init(node)
node.kinds = kinds
for _, kind := range kinds {
id := id.NewNodeID(kind)
child := tree.Factory(ctx, kind)
child.SetID(id)
child.SetParent(node)
node.SetChild(child)
}
return node
}

30
tree/f3/forge_factory.go Normal file
View file

@ -0,0 +1,30 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"fmt"
"strings"
"code.forgejo.org/f3/gof3/v3/tree/generic"
)
var forgeFactories = make(map[string]ForgeFactory, 10)
type ForgeFactory func(tree generic.TreeInterface, options any) generic.TreeDriverInterface
func RegisterForgeFactory(name string, factory ForgeFactory) {
name = strings.ToLower(name)
forgeFactories[name] = factory
}
func GetForgeFactory(name string) ForgeFactory {
name = strings.ToLower(name)
factory, ok := forgeFactories[name]
if !ok {
panic(fmt.Errorf("no forge registered for %s", name))
}
return factory
}

181
tree/f3/helpers.go Normal file
View file

@ -0,0 +1,181 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"context"
"fmt"
"slices"
"code.forgejo.org/f3/gof3/v3/f3"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/path"
"code.forgejo.org/f3/gof3/v3/tree/generic"
"code.forgejo.org/f3/gof3/v3/util"
)
type ForgeDriverInterface interface {
SetNative(native any)
GetNativeID() string
GetHelper() any
}
func ConvertToAny[T any](s ...T) []any {
a := make([]any, 0, len(s))
for _, e := range s {
a = append(a, e)
}
return a
}
func ConvertNativeChild(ctx context.Context, tree generic.TreeInterface, parent generic.NodeInterface, kind kind.Kind, nativeChild any) generic.NodeInterface {
child := tree.Factory(ctx, kind)
child.SetParent(parent)
childDriver := child.GetDriver().(ForgeDriverInterface)
childDriver.SetNative(nativeChild)
child.SetID(id.NewNodeID(childDriver.GetNativeID()))
return child
}
func ConvertListed(ctx context.Context, node generic.NodeInterface, nativeChildren ...any) generic.ChildrenSlice {
children := generic.NewChildrenSlice(len(nativeChildren))
tree := node.GetTree()
f3Tree := tree.(TreeInterface)
kind := f3Tree.GetChildrenKind(node.GetKind())
for _, nativeChild := range nativeChildren {
children = append(children, ConvertNativeChild(ctx, tree, node, kind, nativeChild))
}
return children
}
func GetFirstNodeKind(node generic.NodeInterface, kind ...kind.Kind) generic.NodeInterface {
if slices.Contains(kind, node.GetKind()) {
return node
}
parent := node.GetParent()
if parent == generic.NilNode {
return generic.NilNode
}
return GetFirstNodeKind(parent, kind...)
}
func GetFirstFormat[T f3.Interface](node generic.NodeInterface) T {
f := node.NewFormat()
switch f.(type) {
case T:
return node.ToFormat().(T)
}
parent := node.GetParent()
if parent == generic.NilNode {
panic(fmt.Errorf("no parent of the desired type"))
}
return GetFirstFormat[T](parent)
}
func GetProject(node generic.NodeInterface) generic.NodeInterface {
return GetFirstNodeKind(node, KindProject)
}
func GetProjectID(node generic.NodeInterface) int64 {
return util.ParseInt(GetProject(node).GetID().String())
}
func GetProjectName(node generic.NodeInterface) string {
return GetProject(node).ToFormat().(*f3.Project).Name
}
func GetOwner(node generic.NodeInterface) generic.NodeInterface {
return GetFirstNodeKind(node, KindUser, KindOrganization)
}
func GetOwnerID(node generic.NodeInterface) int64 {
return GetOwner(node).GetID().Int64()
}
func GetReactionable(node generic.NodeInterface) generic.NodeInterface {
return GetFirstNodeKind(node, KindComment, KindIssue, KindPullRequest)
}
func GetReactionableID(node generic.NodeInterface) int64 {
return GetReactionable(node).GetID().Int64()
}
func GetCommentable(node generic.NodeInterface) generic.NodeInterface {
return GetFirstNodeKind(node, KindIssue, KindPullRequest)
}
func GetCommentableID(node generic.NodeInterface) int64 {
return GetCommentable(node).GetID().Int64()
}
func GetComment(node generic.NodeInterface) generic.NodeInterface {
return GetFirstNodeKind(node, KindComment, KindReviewComment)
}
func GetCommentID(node generic.NodeInterface) int64 {
return GetComment(node).GetID().Int64()
}
func GetRelease(node generic.NodeInterface) generic.NodeInterface {
return GetFirstNodeKind(node, KindRelease)
}
func GetReleaseID(node generic.NodeInterface) int64 {
return GetRelease(node).GetID().Int64()
}
func GetPullRequest(node generic.NodeInterface) generic.NodeInterface {
return GetFirstNodeKind(node, KindPullRequest)
}
func GetPullRequestID(node generic.NodeInterface) int64 {
return GetPullRequest(node).GetID().Int64()
}
func GetReview(node generic.NodeInterface) generic.NodeInterface {
return GetFirstNodeKind(node, KindReview)
}
func GetReviewID(node generic.NodeInterface) int64 {
return GetReview(node).GetID().Int64()
}
func GetReviewComment(node generic.NodeInterface) generic.NodeInterface {
return GetFirstNodeKind(node, KindReviewComment)
}
func GetReviewCommentID(node generic.NodeInterface) int64 {
return GetReviewComment(node).GetID().Int64()
}
func GetOwnerName(node generic.NodeInterface) string {
owner := GetOwner(node)
if owner == generic.NilNode {
panic(fmt.Errorf("no user or organization parent for %s", node))
}
switch f := owner.ToFormat().(type) {
case *f3.User:
return f.UserName
case *f3.Organization:
return f.Name
default:
panic(fmt.Errorf("unexpected type %T", owner.ToFormat()))
}
}
func GetUsernameFromID(ctx context.Context, tree TreeInterface, id int64) string {
var name string
getName := func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
name = node.ToFormat().(*f3.User).UserName
}
p := NewUserPath(id)
if !tree.ApplyAndGet(ctx, p, generic.NewApplyOptions(getName)) {
panic(fmt.Errorf("%s not found", p))
}
return name
}

102
tree/f3/kind.go Normal file
View file

@ -0,0 +1,102 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"code.forgejo.org/f3/gof3/v3/f3"
"code.forgejo.org/f3/gof3/v3/kind"
)
const (
KindAsset = f3.ResourceAsset
KindAssets = f3.ResourceAssets
KindComment = f3.ResourceComment
KindComments = f3.ResourceComments
KindForge = f3.ResourceForge
KindIssue = f3.ResourceIssue
KindIssues = f3.ResourceIssues
KindLabel = f3.ResourceLabel
KindLabels = f3.ResourceLabels
KindMilestone = f3.ResourceMilestone
KindMilestones = f3.ResourceMilestones
KindOrganization = f3.ResourceOrganization
KindOrganizations = f3.ResourceOrganizations
KindProject = f3.ResourceProject
KindProjects = f3.ResourceProjects
KindPullRequest = f3.ResourcePullRequest
KindPullRequests = f3.ResourcePullRequests
KindReaction = f3.ResourceReaction
KindReactions = f3.ResourceReactions
KindRelease = f3.ResourceRelease
KindReleases = f3.ResourceReleases
KindRepository = f3.ResourceRepository
KindRepositories = f3.ResourceRepositories
KindReview = f3.ResourceReview
KindReviews = f3.ResourceReviews
KindReviewComment = f3.ResourceReviewComment
KindReviewComments = f3.ResourceReviewComments
KindTopic = f3.ResourceTopic
KindTopics = f3.ResourceTopics
KindUser = f3.ResourceUser
KindUsers = f3.ResourceUsers
)
var isContainer = map[kind.Kind]bool{
kind.KindRoot: true,
KindAssets: true,
KindComments: true,
KindIssues: true,
KindLabels: true,
KindMilestones: true,
KindOrganizations: true,
KindProjects: true,
KindPullRequests: true,
KindReactions: true,
KindReleases: true,
KindRepositories: true,
KindReviews: true,
KindReviewComments: true,
KindTopics: true,
KindUsers: true,
}
var childrenKind = map[kind.Kind]kind.Kind{
kind.KindRoot: KindForge,
KindAssets: KindAsset,
KindComments: KindComment,
KindIssues: KindIssue,
KindLabels: KindLabel,
KindMilestones: KindMilestone,
KindOrganizations: KindOrganization,
KindProjects: KindProject,
KindPullRequests: KindPullRequest,
KindReactions: KindReaction,
KindReleases: KindRelease,
KindRepositories: KindRepository,
KindReviews: KindReview,
KindReviewComments: KindReviewComment,
KindTopics: KindTopic,
KindUsers: KindUser,
}
var containerChildFixedID = map[kind.Kind]bool{
kind.KindRoot: true,
KindAssets: false,
KindComments: false,
KindForge: true,
KindIssues: false,
KindLabels: false,
KindMilestones: false,
KindOrganizations: false,
KindProjects: false,
KindPullRequests: false,
KindReactions: false,
KindReleases: false,
KindRepositories: false,
KindReviewComments: false,
KindReviews: false,
KindTopics: false,
KindUsers: false,
}

27
tree/f3/label.go Normal file
View file

@ -0,0 +1,27 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"fmt"
"code.forgejo.org/f3/gof3/v3/f3"
)
func NewLabelPathString[T any](projectPath string, id T) string {
return fmt.Sprintf("%s/labels/%v", projectPath, id)
}
func NewLabelReference[T any](projectPath string, id T) *f3.Reference {
return f3.NewReference(NewLabelPathString(projectPath, id))
}
func NewIssueLabelReference[T any](id T) *f3.Reference {
return f3.NewReference(NewLabelPathString("../..", id))
}
func NewPullRequestLabelReference[T any](id T) *f3.Reference {
return f3.NewReference(NewLabelPathString("../..", id))
}

23
tree/f3/milestone.go Normal file
View file

@ -0,0 +1,23 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"fmt"
"code.forgejo.org/f3/gof3/v3/f3"
)
func NewMilestonePathString[T any](projectPath string, id T) string {
return fmt.Sprintf("%s/milestones/%v", projectPath, id)
}
func NewMilestoneReference[T any](projectPath string, id T) *f3.Reference {
return f3.NewReference(NewMilestonePathString(projectPath, id))
}
func NewIssueMilestoneReference[T any](id T) *f3.Reference {
return f3.NewReference(NewMilestonePathString("../..", id))
}

View file

@ -0,0 +1,99 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package objects
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
)
type Interface interface {
Save(rc io.ReadCloser) (string, string)
}
type helper struct {
dir *string
}
func (o *helper) getDir() string {
if o.dir == nil {
dir, err := os.MkdirTemp("", "objectHelper")
if err != nil {
panic(err)
}
runtime.SetFinalizer(o, func(o *helper) {
err := os.RemoveAll(dir)
if err != nil {
panic(err)
}
})
o.dir = &dir
}
return *o.dir
}
func (o *helper) getPath(sha string) string {
return filepath.Join(o.getDir(), sha[0:2], sha[2:4], sha)
}
func (o *helper) Save(rc io.ReadCloser) (string, string) {
tempFile, err := os.CreateTemp("", "object")
if err != nil {
panic(err)
}
tempRemoved := false
defer func() {
if !tempRemoved {
_ = os.Remove(tempFile.Name())
}
}()
// reader
defer rc.Close()
// writer to file
f, err := os.OpenFile(tempFile.Name(), os.O_CREATE|os.O_RDWR, 0o644)
if err != nil {
panic(err)
}
defer f.Close()
// writer to sha256
h := sha256.New()
// copy reader to file & sha256
w := io.MultiWriter(f, h)
if _, err := io.Copy(w, rc); err != nil {
panic(fmt.Errorf("while copying object: %w", err))
}
// finalize writer to sha256
sha := hex.EncodeToString(h.Sum(nil))
// finalize writer to file
if err := tempFile.Close(); err != nil {
panic(err)
}
path := o.getPath(sha)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
panic(err)
}
if err := os.Rename(tempFile.Name(), path); err != nil {
panic(err)
}
tempRemoved = true
return sha, path
}
func NewObjectsHelper() Interface {
return &helper{}
}

68
tree/f3/objects/sha.go Normal file
View file

@ -0,0 +1,68 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package objects
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"hash"
"io"
"net/http"
options_http "code.forgejo.org/f3/gof3/v3/options/http"
)
type SHASetter interface {
SetSHA(sha string)
}
func FuncReadURLAndSetSHA(newHTTPClient options_http.NewMigrationHTTPClientFun, url string, sha SHASetter) func() io.ReadCloser {
return func() io.ReadCloser {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
panic(err)
}
httpClient := newHTTPClient()
resp, err := httpClient.Do(req)
if err != nil {
panic(fmt.Errorf("while downloading %s %w", url, err))
}
return FuncReadAndSetSHA(resp.Body, sha)()
}
}
type readAndSetSHA struct {
reader io.Reader
closer io.Closer
sha SHASetter
hasher hash.Hash
}
func (o *readAndSetSHA) Read(b []byte) (n int, err error) {
return o.reader.Read(b)
}
func (o *readAndSetSHA) Close() error {
o.sha.SetSHA(hex.EncodeToString(o.hasher.Sum(nil)))
return o.closer.Close()
}
func newReadAndSetSHA(reader io.ReadCloser, sha SHASetter) *readAndSetSHA {
hasher := sha256.New()
return &readAndSetSHA{
reader: io.TeeReader(reader, hasher),
closer: reader,
sha: sha,
hasher: hasher,
}
}
func FuncReadAndSetSHA(reader io.ReadCloser, sha SHASetter) func() io.ReadCloser {
return func() io.ReadCloser {
return newReadAndSetSHA(reader, sha)
}
}

View file

@ -0,0 +1,35 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package objects
import (
"io"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
type sha string
func (o *sha) SetSHA(s string) {
*o = sha(s)
}
func (o *sha) GetSHA() string {
return string(*o)
}
func Test_FuncReadAndSetSHA(t *testing.T) {
content := "CONTENT"
r := strings.NewReader(content)
s := new(sha)
f := FuncReadAndSetSHA(io.NopCloser(r), s)()
c, err := io.ReadAll(f)
require.NoError(t, err)
require.NoError(t, f.Close())
require.Equal(t, "65f23e22a9bfedda96929b3cfcb8b6d2fdd34a2e877ddb81f45d79ab05710e12", s.GetSHA())
require.Equal(t, content, string(c))
}

11
tree/f3/organizations.go Normal file
View file

@ -0,0 +1,11 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"code.forgejo.org/f3/gof3/v3/tree/generic"
)
var OrganizationsPath = generic.NewPathFromString("/forge/organizations")

188
tree/f3/path.go Normal file
View file

@ -0,0 +1,188 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"fmt"
"slices"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/kind"
generic_path "code.forgejo.org/f3/gof3/v3/path"
"code.forgejo.org/f3/gof3/v3/tree/generic"
)
type Path interface {
NodeIDs() []id.NodeID
OwnerAndProjectID() (owner, project int64)
AppendID(id string) Path
Ignore() Path
generic_path.Path
Root() Path
Forge() Path
SetForge() Path
Assets() Path
SetAssets() Path
Comments() Path
SetComments() Path
Issues() Path
SetIssues() Path
Labels() Path
SetLabels() Path
Milestones() Path
SetMilestones() Path
Owners() Path
SetOwners(owners kind.Kind) Path
Organizations() Path
SetOrganizations() Path
Projects() Path
SetProjects() Path
PullRequests() Path
SetPullRequests() Path
Reactions() Path
SetReactions() Path
Releases() Path
SetReleases() Path
Repositories() Path
SetRepositories() Path
Reviews() Path
SetReviews() Path
ReviewComments() Path
SetReviewComments() Path
Topics() Path
SetTopics() Path
Users() Path
SetUsers() Path
}
type f3path struct {
generic_path.Implementation
}
func (o f3path) popKind(k ...kind.Kind) Path {
firstKind := kind.Kind(o.First().(generic.NodeInterface).GetID().String())
if !slices.Contains(k, firstKind) {
panic(fmt.Errorf("%s expected one of %s got %s", o, k, firstKind))
}
return ToPath(o.RemoveFirst())
}
func (o f3path) NodeIDs() []id.NodeID {
nodeIDs := make([]id.NodeID, 0, o.Length())
collectID := false
for _, node := range o.Root().All() {
if collectID {
nodeIDs = append(nodeIDs, node.(generic.NodeInterface).GetID())
collectID = false
continue
}
kind := kind.Kind(node.(generic.NodeInterface).GetID().String())
fixedID, ok := containerChildFixedID[kind]
if !ok {
panic(fmt.Errorf("%s unexpected kind %s", o.All(), kind))
}
if !fixedID {
collectID = true
}
}
return nodeIDs
}
func (o f3path) OwnerAndProjectID() (owner, project int64) {
nodeIDs := o.NodeIDs()
return nodeIDs[0].Int64(), nodeIDs[1].Int64()
}
func (o f3path) Root() Path { return o.popKind(kind.Kind("")) }
func (o f3path) Forge() Path { return o.popKind(KindForge) }
func (o f3path) SetForge() Path { return o.appendKind(KindForge) }
func (o f3path) Assets() Path { return o.popKind(KindAssets) }
func (o f3path) SetAssets() Path { return o.appendKind(KindAssets) }
func (o f3path) Comments() Path { return o.popKind(KindComments) }
func (o f3path) SetComments() Path { return o.appendKind(KindComments) }
func (o f3path) Issues() Path { return o.popKind(KindIssues) }
func (o f3path) SetIssues() Path { return o.appendKind(KindIssues) }
func (o f3path) Labels() Path { return o.popKind(KindLabels) }
func (o f3path) SetLabels() Path { return o.appendKind(KindLabels) }
func (o f3path) Milestones() Path { return o.popKind(KindMilestones) }
func (o f3path) SetMilestones() Path { return o.appendKind(KindMilestones) }
func (o f3path) Organizations() Path { return o.popKind(KindOrganizations) }
func (o f3path) SetOrganizations() Path { return o.appendKind(KindOrganizations) }
func (o f3path) Projects() Path { return o.popKind(KindProjects) }
func (o f3path) SetProjects() Path { return o.appendKind(KindProjects) }
func (o f3path) PullRequests() Path { return o.popKind(KindPullRequests) }
func (o f3path) SetPullRequests() Path { return o.appendKind(KindPullRequests) }
func (o f3path) Reactions() Path { return o.popKind(KindReactions) }
func (o f3path) SetReactions() Path { return o.appendKind(KindReactions) }
func (o f3path) Releases() Path { return o.popKind(KindReleases) }
func (o f3path) SetReleases() Path { return o.appendKind(KindReleases) }
func (o f3path) Repositories() Path { return o.popKind(KindRepositories) }
func (o f3path) SetRepositories() Path { return o.appendKind(KindRepositories) }
func (o f3path) Reviews() Path { return o.popKind(KindReviews) }
func (o f3path) SetReviews() Path { return o.appendKind(KindReviews) }
func (o f3path) ReviewComments() Path { return o.popKind(KindReviewComments) }
func (o f3path) SetReviewComments() Path { return o.appendKind(KindReviewComments) }
func (o f3path) Topics() Path { return o.popKind(KindTopics) }
func (o f3path) SetTopics() Path { return o.appendKind(KindTopics) }
func (o f3path) Users() Path { return o.popKind(KindUsers) }
func (o f3path) SetUsers() Path { return o.appendKind(KindUsers) }
func (o f3path) Owners() Path { return o.popKind(KindUsers, KindOrganizations) }
func (o f3path) SetOwners(owners kind.Kind) Path { return o.appendKind(owners) }
func (o f3path) AppendID(id string) Path {
return ToPath(o.Append(generic.NewNodeFromID(id)))
}
func (o f3path) appendKind(kind kind.Kind) Path {
return o.AppendID(string(kind))
}
func (o f3path) Ignore() Path {
return ToPath(o.RemoveFirst())
}
func ToPath(other generic_path.Path) Path {
return f3path{other.(generic_path.Implementation)}
}
func NewPathFromString(pathString string) Path {
return ToPath(generic.NewPathFromString(pathString))
}

181
tree/f3/path_test.go Normal file
View file

@ -0,0 +1,181 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"testing"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/tree/generic"
"code.forgejo.org/f3/gof3/v3/util"
"github.com/stretchr/testify/assert"
)
func TestF3Path(t *testing.T) {
p := NewPathFromString("/")
p = p.SetForge()
{
p.Root().Forge()
}
nodeIDs := make([]id.NodeID, 0, 10)
{
p := p.SetOwners(KindUsers)
i := "1"
ownerID := util.ParseInt(i)
nodeIDs := append(nodeIDs, id.NewNodeID(i))
p = p.AppendID(i)
assert.EqualValues(t, id.NewNodeID(i), p.Root().Forge().Users().First().(generic.NodeInterface).GetID())
assert.EqualValues(t, nodeIDs, p.NodeIDs())
{
p := p.SetProjects()
i := "2"
projectID := util.ParseInt(i)
nodeIDs := append(nodeIDs, id.NewNodeID(i))
p = p.AppendID(i)
assert.EqualValues(t, id.NewNodeID(i), p.Root().Forge().Users().Ignore().Projects().First().(generic.NodeInterface).GetID())
assert.EqualValues(t, nodeIDs, p.NodeIDs())
owner, project := p.OwnerAndProjectID()
assert.EqualValues(t, ownerID, owner)
assert.EqualValues(t, projectID, project)
}
}
{
p := p.SetAssets()
i := "something"
nodeIDs := append(nodeIDs, id.NewNodeID(i))
p = p.AppendID(i)
assert.EqualValues(t, id.NewNodeID(i), p.Root().Forge().Assets().First().(generic.NodeInterface).GetID())
assert.EqualValues(t, nodeIDs, p.NodeIDs())
}
{
p := p.SetComments()
i := "something"
nodeIDs := append(nodeIDs, id.NewNodeID(i))
p = p.AppendID(i)
assert.EqualValues(t, id.NewNodeID(i), p.Root().Forge().Comments().First().(generic.NodeInterface).GetID())
assert.EqualValues(t, nodeIDs, p.NodeIDs())
}
{
p := p.SetIssues()
i := "something"
nodeIDs := append(nodeIDs, id.NewNodeID(i))
p = p.AppendID(i)
assert.EqualValues(t, id.NewNodeID(i), p.Root().Forge().Issues().First().(generic.NodeInterface).GetID())
assert.EqualValues(t, nodeIDs, p.NodeIDs())
}
{
p := p.SetLabels()
i := "something"
nodeIDs := append(nodeIDs, id.NewNodeID(i))
p = p.AppendID(i)
assert.EqualValues(t, id.NewNodeID(i), p.Root().Forge().Labels().First().(generic.NodeInterface).GetID())
assert.EqualValues(t, nodeIDs, p.NodeIDs())
}
{
p := p.SetMilestones()
i := "something"
nodeIDs := append(nodeIDs, id.NewNodeID(i))
p = p.AppendID(i)
assert.EqualValues(t, id.NewNodeID(i), p.Root().Forge().Milestones().First().(generic.NodeInterface).GetID())
assert.EqualValues(t, nodeIDs, p.NodeIDs())
}
{
p := p.SetOrganizations()
i := "something"
nodeIDs := append(nodeIDs, id.NewNodeID(i))
p = p.AppendID(i)
assert.EqualValues(t, id.NewNodeID(i), p.Root().Forge().Organizations().First().(generic.NodeInterface).GetID())
assert.EqualValues(t, nodeIDs, p.NodeIDs())
}
{
p := p.SetProjects()
i := "something"
nodeIDs := append(nodeIDs, id.NewNodeID(i))
p = p.AppendID(i)
assert.EqualValues(t, id.NewNodeID(i), p.Root().Forge().Projects().First().(generic.NodeInterface).GetID())
assert.EqualValues(t, nodeIDs, p.NodeIDs())
}
{
p := p.SetPullRequests()
i := "something"
nodeIDs := append(nodeIDs, id.NewNodeID(i))
p = p.AppendID(i)
assert.EqualValues(t, id.NewNodeID(i), p.Root().Forge().PullRequests().First().(generic.NodeInterface).GetID())
assert.EqualValues(t, nodeIDs, p.NodeIDs())
}
{
p := p.SetReactions()
i := "something"
nodeIDs := append(nodeIDs, id.NewNodeID(i))
p = p.AppendID(i)
assert.EqualValues(t, id.NewNodeID(i), p.Root().Forge().Reactions().First().(generic.NodeInterface).GetID())
assert.EqualValues(t, nodeIDs, p.NodeIDs())
}
{
p := p.SetReleases()
i := "something"
nodeIDs := append(nodeIDs, id.NewNodeID(i))
p = p.AppendID(i)
assert.EqualValues(t, id.NewNodeID(i), p.Root().Forge().Releases().First().(generic.NodeInterface).GetID())
assert.EqualValues(t, nodeIDs, p.NodeIDs())
}
{
p := p.SetRepositories()
i := "something"
nodeIDs := append(nodeIDs, id.NewNodeID(i))
p = p.AppendID(i)
assert.EqualValues(t, id.NewNodeID(i), p.Root().Forge().Repositories().First().(generic.NodeInterface).GetID())
assert.EqualValues(t, nodeIDs, p.NodeIDs())
}
{
p := p.SetReviews()
i := "something"
nodeIDs := append(nodeIDs, id.NewNodeID(i))
p = p.AppendID(i)
assert.EqualValues(t, id.NewNodeID(i), p.Root().Forge().Reviews().First().(generic.NodeInterface).GetID())
assert.EqualValues(t, nodeIDs, p.NodeIDs())
}
{
p := p.SetReviewComments()
i := "something"
nodeIDs := append(nodeIDs, id.NewNodeID(i))
p = p.AppendID(i)
assert.EqualValues(t, id.NewNodeID(i), p.Root().Forge().ReviewComments().First().(generic.NodeInterface).GetID())
assert.EqualValues(t, nodeIDs, p.NodeIDs())
}
{
p := p.SetTopics()
i := "something"
nodeIDs := append(nodeIDs, id.NewNodeID(i))
p = p.AppendID(i)
assert.EqualValues(t, id.NewNodeID(i), p.Root().Forge().Topics().First().(generic.NodeInterface).GetID())
assert.EqualValues(t, nodeIDs, p.NodeIDs())
}
{
p := p.SetUsers()
i := "something"
nodeIDs := append(nodeIDs, id.NewNodeID(i))
p = p.AppendID(i)
assert.EqualValues(t, id.NewNodeID(i), p.Root().Forge().Users().First().(generic.NodeInterface).GetID())
assert.EqualValues(t, nodeIDs, p.NodeIDs())
}
}

29
tree/f3/project.go Normal file
View file

@ -0,0 +1,29 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"context"
"fmt"
"code.forgejo.org/f3/gof3/v3/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
)
func NewProjectPathString[U, P any](owners string, user U, project P) string {
return fmt.Sprintf("/forge/%s/%v/projects/%v", owners, user, project)
}
func NewProjectReference[U, P any](owners string, user U, project P) *f3.Reference {
return f3.NewReference(NewProjectPathString(owners, user, project))
}
func ResolveProjectReference(ctx context.Context, tree generic.TreeInterface, r *f3.Reference) (string, string) {
project := tree.Find(generic.NewPathFromString(r.Get()))
if project == generic.NilNode {
panic(fmt.Errorf("%s not found", r.Get()))
}
return GetOwnerName(project), GetProjectName(project)
}

47
tree/f3/pullrequest.go Normal file
View file

@ -0,0 +1,47 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"context"
"code.forgejo.org/f3/gof3/v3/tree/generic"
)
type PullRequestDriverInterface interface {
GetPullRequestHead() string
GetPullRequestRef() string
GetPullRequestPushRefs() []string
}
type PullRequestNodeDriverProxyInterface interface {
PullRequestDriverInterface
}
type PullRequestNodeInterface interface {
generic.NodeInterface
PullRequestNodeDriverProxyInterface
}
type pullRequestNode struct {
generic.Node
}
func (o *pullRequestNode) GetPullRequestHead() string {
return o.GetDriver().(PullRequestDriverInterface).GetPullRequestHead()
}
func (o *pullRequestNode) GetPullRequestRef() string {
return o.GetDriver().(PullRequestDriverInterface).GetPullRequestRef()
}
func (o *pullRequestNode) GetPullRequestPushRefs() []string {
return o.GetDriver().(PullRequestDriverInterface).GetPullRequestPushRefs()
}
func newPullRequestNode(ctx context.Context, tree generic.TreeInterface) generic.NodeInterface {
node := &pullRequestNode{}
return node.Init(node)
}

66
tree/f3/repository.go Normal file
View file

@ -0,0 +1,66 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"context"
"fmt"
"code.forgejo.org/f3/gof3/v3/f3"
"code.forgejo.org/f3/gof3/v3/path"
"code.forgejo.org/f3/gof3/v3/tree/generic"
)
type RepositoryDriverInterface interface {
GetRepositoryURL() string
GetRepositoryPushURL() string
GetRepositoryInternalRefs() []string
}
type RepositoryNodeDriverProxyInterface interface {
RepositoryDriverInterface
}
type RepositoryNodeInterface interface {
generic.NodeInterface
RepositoryNodeDriverProxyInterface
}
type repositoryNode struct {
generic.Node
}
func (o *repositoryNode) GetRepositoryURL() string {
return o.GetDriver().(RepositoryDriverInterface).GetRepositoryURL()
}
func (o *repositoryNode) GetRepositoryPushURL() string {
return o.GetDriver().(RepositoryDriverInterface).GetRepositoryPushURL()
}
func (o *repositoryNode) GetRepositoryInternalRefs() []string {
return o.GetDriver().(RepositoryDriverInterface).GetRepositoryInternalRefs()
}
func newRepositoryNode(ctx context.Context, tree generic.TreeInterface) generic.NodeInterface {
node := &repositoryNode{}
return node.Init(node)
}
func NewRepositoryPath[T, U any](owners string, owner T, project U) path.Path {
return generic.NewPathFromString(NewRepositoryPathString(owners, owner, project))
}
func NewRepositoryPathString[T, U any](owners string, owner T, project U) string {
return fmt.Sprintf("%s/%v/projects/%v/repositories/vcs", owners, owner, project)
}
func NewRepositoryReference[T, U any](owners string, owner T, project U) *f3.Reference {
return f3.NewReference(NewRepositoryPathString(owners, owner, project))
}
func NewPullRequestSameRepositoryReference() *f3.Reference {
return f3.NewReference("../../repositories/vcs")
}

23
tree/f3/topic.go Normal file
View file

@ -0,0 +1,23 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"fmt"
"code.forgejo.org/f3/gof3/v3/f3"
)
func NewTopicPath[T any](id T) Path {
return NewPathFromString(NewTopicPathString(id))
}
func NewTopicPathString[T any](id T) string {
return fmt.Sprintf("/forge/topics/%v", id)
}
func NewTopicReference[T any](id T) *f3.Reference {
return f3.NewReference(NewTopicPathString(id))
}

11
tree/f3/topics.go Normal file
View file

@ -0,0 +1,11 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"code.forgejo.org/f3/gof3/v3/tree/generic"
)
var TopicsPath = generic.NewPathFromString("/forge/topics")

23
tree/f3/user.go Normal file
View file

@ -0,0 +1,23 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"fmt"
"code.forgejo.org/f3/gof3/v3/f3"
)
func NewUserPath[T any](id T) Path {
return NewPathFromString(NewUserPathString(id))
}
func NewUserPathString[T any](id T) string {
return fmt.Sprintf("/forge/users/%v", id)
}
func NewUserReference[T any](id T) *f3.Reference {
return f3.NewReference(NewUserPathString(id))
}

11
tree/f3/users.go Normal file
View file

@ -0,0 +1,11 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"code.forgejo.org/f3/gof3/v3/tree/generic"
)
var UsersPath = generic.NewPathFromString("/forge/users")

72
tree/generic/compare.go Normal file
View file

@ -0,0 +1,72 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/path"
)
func NodeCompare(ctx context.Context, a, b NodeInterface) bool {
a.Trace("a '%s' | b '%s'", a.GetCurrentPath().ReadableString(), b.GetCurrentPath().ReadableString())
if a.GetKind() != b.GetKind() {
a.Trace("kind is different a = %s | b = %s", a.GetKind(), b.GetKind())
return false
}
if diff := a.GetTree().Diff(a, b); diff != "" {
a.Trace("difference %s", diff)
return false
}
aChildren := a.GetNodeChildren()
bChildren := b.GetNodeChildren()
aLen := len(aChildren)
bLen := len(bChildren)
if aLen != bLen {
a.Trace("children count is different a = %d | b = %d", aLen, bLen)
return false
}
for aID, aChild := range aChildren {
bID := aChild.GetID()
mappedID := aChild.GetMappedID()
if mappedID != id.NilID {
bID = mappedID
}
bChild, ok := bChildren[bID]
if !ok {
a.Trace("there is no child in 'b' with id %s matching the child in 'a' with id %s", bID, aID)
return false
}
if !NodeCompare(ctx, aChild, bChild) {
return false
}
}
return true
}
func TreeCompare(ctx context.Context, aTree TreeInterface, aPath path.Path, bTree TreeInterface, bPath path.Path) bool {
aTree.Trace("'%s' => '%s'", aPath.ReadableString(), bPath.ReadableString())
a := aTree.Find(aPath)
if a == NilNode {
aTree.Trace("a does not have %s", aPath.ReadableString())
return false
}
b := bTree.Find(bPath)
if b == NilNode {
aTree.Trace("b does not have %s", bPath.ReadableString())
return false
}
return NodeCompare(ctx, a, b)
}

View file

@ -0,0 +1,226 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"testing"
"code.forgejo.org/f3/gof3/v3/f3"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/logger"
"code.forgejo.org/f3/gof3/v3/path"
"github.com/stretchr/testify/assert"
)
type compareFormat struct {
f3.Common
V int
}
func (o *compareFormat) Clone() f3.Interface {
clone := &compareFormat{}
*clone = *o
return clone
}
type compareNodeDriver struct {
NullDriver
v int
ID string
}
func (o *compareNodeDriver) NewFormat() f3.Interface {
return &compareFormat{
Common: f3.NewCommon(o.ID),
}
}
func (o *compareNodeDriver) ToFormat() f3.Interface {
f := o.NewFormat().(*compareFormat)
f.V = o.v
return f
}
func (o *compareNodeDriver) FromFormat(f f3.Interface) {
fc := f.(*compareFormat)
o.v = fc.V
o.ID = fc.GetID()
}
func newCompareNodeDriver() NodeDriverInterface {
return &compareNodeDriver{}
}
type compareTreeDriver struct {
NullTreeDriver
}
func newCompareTreeDriver() TreeDriverInterface {
return &compareTreeDriver{}
}
func (o *compareTreeDriver) Factory(ctx context.Context, kind kind.Kind) NodeDriverInterface {
d := newCompareNodeDriver()
d.SetTreeDriver(o)
return d
}
func newCompareTree() TreeInterface {
tree := &testTree{}
tree.Init(tree, newTestOptions())
tree.Trace("init done")
tree.SetDriver(newCompareTreeDriver())
tree.Register(kindCompareNode, func(ctx context.Context, kind kind.Kind) NodeInterface {
node := &compareNode{}
return node.Init(node)
})
return tree
}
var kindCompareNode = kind.Kind("compare")
type compareNode struct {
Node
}
func TestTreeCompare(t *testing.T) {
tree := newTestTree()
log := logger.NewCaptureLogger()
log.SetLevel(logger.Trace)
tree.SetLogger(log)
root := tree.Factory(context.Background(), kindTestNodeLevelOne)
tree.SetRoot(root)
assert.True(t, TreeCompare(context.Background(), tree, path.NewPathFromString(NewElementNode, ""), tree, path.NewPathFromString(NewElementNode, "")))
log.Reset()
assert.False(t, TreeCompare(context.Background(), tree, path.NewPathFromString(NewElementNode, "/notfound"), tree, path.NewPathFromString(NewElementNode, "")))
assert.Contains(t, log.String(), "a does not have /notfound")
log.Reset()
assert.False(t, TreeCompare(context.Background(), tree, path.NewPathFromString(NewElementNode, "/"), tree, path.NewPathFromString(NewElementNode, "/notfound")))
assert.Contains(t, log.String(), "b does not have /notfound")
}
func TestNodeCompare(t *testing.T) {
log := logger.NewCaptureLogger()
log.SetLevel(logger.Trace)
treeA := newCompareTree()
treeA.SetLogger(log)
treeB := newCompareTree()
treeB.SetLogger(log)
nodeA := treeA.Factory(context.Background(), kindCompareNode)
nodeA.SetID(id.NewNodeID("root"))
assert.True(t, NodeCompare(context.Background(), nodeA, nodeA))
t.Run("different kind", func(t *testing.T) {
log.Reset()
other := treeB.Factory(context.Background(), kindCompareNode)
other.SetKind("other")
assert.False(t, NodeCompare(context.Background(), nodeA, other))
assert.Contains(t, log.String(), "kind is different")
})
t.Run("difference", func(t *testing.T) {
log.Reset()
other := treeB.Factory(context.Background(), kindCompareNode)
other.FromFormat(&compareFormat{V: 123456})
assert.False(t, NodeCompare(context.Background(), nodeA, other))
assert.Contains(t, log.String(), "difference")
assert.Contains(t, log.String(), "123456")
})
t.Run("children count", func(t *testing.T) {
log.Reset()
other := treeB.Factory(context.Background(), kindCompareNode)
other.SetChild(treeB.Factory(context.Background(), kindCompareNode))
assert.False(t, NodeCompare(context.Background(), nodeA, other))
assert.Contains(t, log.String(), "children count")
})
nodeAA := treeA.Factory(context.Background(), kindCompareNode)
nodeAA.SetID(id.NewNodeID("levelone"))
nodeA.SetChild(nodeAA)
nodeB := treeB.Factory(context.Background(), kindCompareNode)
nodeB.SetID(id.NewNodeID("root"))
t.Run("children are the same", func(t *testing.T) {
nodeBB := treeB.Factory(context.Background(), kindCompareNode)
nodeBB.SetID(id.NewNodeID("levelone"))
nodeB.SetChild(nodeBB)
assert.True(t, NodeCompare(context.Background(), nodeA, nodeB))
nodeB.DeleteChild(nodeBB.GetID())
})
t.Run("children have different IDs", func(t *testing.T) {
log.Reset()
nodeBB := treeB.Factory(context.Background(), kindCompareNode)
nodeBB.SetID(id.NewNodeID("SOMETHINGELSE"))
nodeB.SetChild(nodeBB)
assert.False(t, NodeCompare(context.Background(), nodeA, nodeB))
assert.Contains(t, log.String(), "id levelone matching the child")
nodeB.DeleteChild(nodeBB.GetID())
})
t.Run("children have different content", func(t *testing.T) {
log.Reset()
nodeBB := treeB.Factory(context.Background(), kindCompareNode)
nodeBB.FromFormat(&compareFormat{V: 12345678})
nodeBB.SetID(id.NewNodeID("levelone"))
nodeB.SetChild(nodeBB)
assert.False(t, NodeCompare(context.Background(), nodeA, nodeB))
assert.Contains(t, log.String(), "difference")
assert.Contains(t, log.String(), "12345678")
nodeB.DeleteChild(nodeBB.GetID())
})
t.Run("children are the same because of their mapped ID", func(t *testing.T) {
log.Reset()
nodeBB := treeB.Factory(context.Background(), kindCompareNode)
nodeBB.SetID(id.NewNodeID("REMAPPEDID"))
nodeB.SetChild(nodeBB)
nodeAA.SetMappedID(id.NewNodeID("REMAPPEDID"))
assert.True(t, NodeCompare(context.Background(), nodeA, nodeB))
nodeB.DeleteChild(nodeBB.GetID())
nodeAA.SetMappedID(id.NilID)
})
t.Run("children are different because of their mapped ID", func(t *testing.T) {
log.Reset()
nodeBB := treeB.Factory(context.Background(), kindCompareNode)
nodeBB.SetID(id.NewNodeID("levelone"))
nodeB.SetChild(nodeBB)
nodeAA.SetMappedID(id.NewNodeID("REMAPPEDID"))
assert.False(t, NodeCompare(context.Background(), nodeA, nodeB))
assert.Contains(t, log.String(), "id REMAPPEDID matching the child")
nodeB.DeleteChild(nodeBB.GetID())
nodeAA.SetMappedID(id.NilID)
})
}

107
tree/generic/driver_node.go Normal file
View file

@ -0,0 +1,107 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"errors"
"fmt"
"code.forgejo.org/f3/gof3/v3/f3"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/logger"
)
type NodeDriverInterface interface {
logger.MessageInterface
MapIDInterface
IsNull() bool
GetNode() NodeInterface
SetNode(NodeInterface)
SetTreeDriver(treeDriver TreeDriverInterface)
GetTreeDriver() TreeDriverInterface
ListPage(context.Context, int) ChildrenSlice
GetIDFromName(context.Context, string) id.NodeID
Equals(context.Context, NodeInterface) bool
Get(context.Context) bool
Put(context.Context) id.NodeID
Patch(context.Context)
Delete(context.Context)
NewFormat() f3.Interface
FromFormat(f3.Interface)
ToFormat() f3.Interface
LookupMappedID(id.NodeID) id.NodeID
String() string
}
type NullDriver struct {
logger.Logger
node NodeInterface
mapped id.NodeID
treeDriver TreeDriverInterface
}
func NewNullDriver() NodeDriverInterface {
return &NullDriver{}
}
func (o *NullDriver) IsNull() bool { return true }
func (o *NullDriver) SetNode(node NodeInterface) { o.node = node }
func (o *NullDriver) GetNode() NodeInterface { return o.node }
func (o *NullDriver) GetMappedID() id.NodeID {
if o.mapped == nil {
return id.NilID
}
return o.mapped
}
func (o *NullDriver) SetMappedID(mapped id.NodeID) { o.mapped = mapped }
func (o *NullDriver) SetTreeDriver(treeDriver TreeDriverInterface) {
o.treeDriver = treeDriver
if treeDriver != nil {
o.SetLogger(treeDriver)
}
}
func (o *NullDriver) GetTreeDriver() TreeDriverInterface { return o.treeDriver }
func (o *NullDriver) ListPage(context.Context, int) ChildrenSlice {
panic(fmt.Errorf("ListPage %s", o.GetNode().GetKind()))
}
func (o *NullDriver) GetIDFromName(ctx context.Context, name string) id.NodeID {
panic(errors.New(name))
}
func (o *NullDriver) Equals(context.Context, NodeInterface) bool {
panic(fmt.Errorf("Equals %s", o.GetNode().GetKind()))
}
func (o *NullDriver) Get(context.Context) bool { panic(fmt.Errorf("Get %s", o.GetNode().GetKind())) }
func (o *NullDriver) Put(context.Context) id.NodeID {
panic(fmt.Errorf("Put %s", o.GetNode().GetKind()))
}
func (o *NullDriver) Patch(context.Context) {
panic(fmt.Errorf("Patch %s", o.GetNode().GetKind()))
}
func (o *NullDriver) Delete(context.Context) { panic(fmt.Errorf("Delete %s", o.GetNode().GetKind())) }
func (o *NullDriver) NewFormat() f3.Interface {
panic(fmt.Errorf("NewFormat %s", o.GetNode().GetKind()))
}
func (o *NullDriver) FromFormat(f3.Interface) {
panic(fmt.Errorf("FromFormat %s", o.GetNode().GetKind()))
}
func (o *NullDriver) ToFormat() f3.Interface { panic(fmt.Errorf("ToFormat %s", o.GetNode().GetKind())) }
func (o *NullDriver) LookupMappedID(id.NodeID) id.NodeID { return id.NilID }
func (o *NullDriver) String() string { return "" }

View file

@ -0,0 +1,78 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/logger"
"github.com/google/go-cmp/cmp"
)
type TreeDriverInterface interface {
logger.Interface
GetTree() TreeInterface
SetTree(TreeInterface)
GetPageSize() int
SetPageSize(int)
AllocateID() bool
Init()
Diff(a, b NodeDriverInterface) string
Factory(ctx context.Context, kind kind.Kind) NodeDriverInterface
}
var DefaultPageSize = int(25)
type NullTreeDriver struct {
logger.Logger
tree TreeInterface
pageSize int
}
func (o *NullTreeDriver) Init() {
o.pageSize = DefaultPageSize
}
func NewNullTreeDriver() TreeDriverInterface {
d := &NullTreeDriver{}
d.Init()
return d
}
func (o *NullTreeDriver) SetTree(tree TreeInterface) {
o.tree = tree
if tree != nil {
o.SetLogger(tree)
}
}
func (o *NullTreeDriver) GetTree() TreeInterface { return o.tree }
func (o *NullTreeDriver) GetPageSize() int { return o.pageSize }
func (o *NullTreeDriver) SetPageSize(pageSize int) { o.pageSize = pageSize }
func (o *NullTreeDriver) AllocateID() bool { return true }
func (o *NullTreeDriver) Diff(a, b NodeDriverInterface) string {
aFormat := a.ToFormat()
bFormat := b.ToFormat()
return cmp.Diff(aFormat, bFormat)
}
func (o *NullTreeDriver) Factory(ctx context.Context, kind kind.Kind) NodeDriverInterface {
d := NewNullDriver()
d.SetTreeDriver(o)
return d
}

31
tree/generic/factory.go Normal file
View file

@ -0,0 +1,31 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"fmt"
"strings"
"code.forgejo.org/f3/gof3/v3/options"
)
var treeFactories = make(map[string]TreeFactory, 10)
type TreeFactory func(ctx context.Context, opts options.Interface) TreeInterface
func RegisterFactory(name string, factory TreeFactory) {
name = strings.ToLower(name)
treeFactories[name] = factory
}
func GetFactory(name string) TreeFactory {
name = strings.ToLower(name)
factory, ok := treeFactories[name]
if !ok {
panic(fmt.Errorf("no factory registered for %s", name))
}
return factory
}

View file

@ -0,0 +1,101 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"code.forgejo.org/f3/gof3/v3/f3"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/logger"
"code.forgejo.org/f3/gof3/v3/path"
)
type NodeAccessorsInterface interface {
SetIsNil(bool)
GetIsNil() bool
SetIsSync(bool)
GetIsSync() bool
GetParent() NodeInterface
SetParent(NodeInterface)
GetKind() kind.Kind
SetKind(kind.Kind)
GetID() id.NodeID
SetID(id.NodeID)
GetTree() TreeInterface
SetTree(TreeInterface)
GetNodeChildren() NodeChildren
GetChildren() ChildrenSlice
SetChildren(NodeChildren)
GetDriver() NodeDriverInterface
SetDriver(NodeDriverInterface)
}
type NodeTreeInterface interface {
GetChild(id.NodeID) NodeInterface
GetIDFromName(context.Context, string) id.NodeID
SetChild(NodeInterface) NodeInterface
DeleteChild(id.NodeID) NodeInterface
CreateChild(context.Context) NodeInterface
MustFind(path.Path) NodeInterface
Find(path.Path) NodeInterface
FindAndGet(context.Context, path.Path) NodeInterface
GetCurrentPath() path.Path
Walk(ctx context.Context, parent path.Path, options *WalkOptions)
WalkAndGet(ctx context.Context, parent path.Path, options *WalkOptions)
Apply(ctx context.Context, parent, path path.Path, options *ApplyOptions) bool
ApplyAndGet(ctx context.Context, path path.Path, options *ApplyOptions) bool
List(context.Context) ChildrenSlice
}
type NodeDriverProxyInterface interface {
MapIDInterface
ListPage(context.Context, int) ChildrenSlice
GetIDFromName(context.Context, string) id.NodeID
Equals(context.Context, NodeInterface) bool
Get(context.Context) NodeInterface
Upsert(context.Context) NodeInterface
Delete(context.Context) NodeInterface
NewFormat() f3.Interface
FromFormat(f3.Interface) NodeInterface
ToFormat() f3.Interface
LookupMappedID(id.NodeID) id.NodeID
}
type MapIDInterface interface {
GetMappedID() id.NodeID
SetMappedID(id.NodeID)
}
type NodeInterface interface {
logger.MessageInterface
NodeAccessorsInterface
NodeTreeInterface
NodeDriverProxyInterface
path.PathElement
Init(NodeInterface) NodeInterface
GetSelf() NodeInterface
SetSelf(NodeInterface)
String() string
}

View file

@ -0,0 +1,58 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/logger"
"code.forgejo.org/f3/gof3/v3/options"
"code.forgejo.org/f3/gof3/v3/path"
)
type TreeInterface interface {
logger.Interface
Init(TreeInterface, options.Interface) TreeInterface
GetOptions() options.Interface
SetOptions(options.Interface)
GetSelf() TreeInterface
SetSelf(TreeInterface)
SetRoot(NodeInterface)
GetRoot() NodeInterface
SetLogger(logger.Interface)
GetLogger() logger.Interface
GetDriver() TreeDriverInterface
SetDriver(TreeDriverInterface)
GetChildrenKind(kind.Kind) kind.Kind
GetPageSize() int
AllocateID() bool
Clear(context.Context)
Diff(a, b NodeInterface) string
MustFind(path.Path) NodeInterface
Find(path.Path) NodeInterface
FindAndGet(context.Context, path.Path) NodeInterface
Exists(context.Context, path.Path) bool
Walk(context.Context, *WalkOptions)
WalkAndGet(context.Context, *WalkOptions)
Apply(context.Context, path.Path, *ApplyOptions) bool
ApplyAndGet(context.Context, path.Path, *ApplyOptions) bool
Register(kind kind.Kind, factory FactoryFun)
Factory(ctx context.Context, kind kind.Kind) NodeInterface
}

139
tree/generic/main_test.go Normal file
View file

@ -0,0 +1,139 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"code.forgejo.org/f3/gof3/v3/f3"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/logger"
"code.forgejo.org/f3/gof3/v3/options"
options_logger "code.forgejo.org/f3/gof3/v3/options/logger"
)
type Storage map[string]int
type testTree struct {
Tree
}
type testOptions struct {
options.Options
options_logger.OptionsLogger
}
func newTestOptions() options.Interface {
opts := &testOptions{}
opts.SetName("test")
l := logger.NewLogger()
l.SetLevel(logger.Trace)
opts.SetLogger(l)
return opts
}
type noopNodeDriver struct {
NullDriver
}
func (o *noopNodeDriver) ListPage(context.Context, int) ChildrenSlice {
return ChildrenSlice{}
}
func (o *noopNodeDriver) GetIDFromName(ctx context.Context, name string) id.NodeID {
return id.NilID
}
func (o *noopNodeDriver) Equals(context.Context, NodeInterface) bool {
return false
}
func (o *noopNodeDriver) Put(context.Context) id.NodeID {
return o.GetNode().GetID()
}
func (o *noopNodeDriver) Patch(context.Context) {
}
func (o *noopNodeDriver) NewFormat() f3.Interface {
f := f3.NewCommon(id.NilID.String())
return &f
}
func (o *noopNodeDriver) ToFormat() f3.Interface {
f := f3.NewCommon(o.GetNode().GetID().String())
return &f
}
func (o *noopNodeDriver) FromFormat(f f3.Interface) {
o.GetNode().SetID(id.NewNodeID(f.GetID()))
}
func newTestNodeDriver() NodeDriverInterface {
return &noopNodeDriver{}
}
type testTreeDriver struct {
NullTreeDriver
}
func newTestTreeDriver() TreeDriverInterface {
return &testTreeDriver{}
}
func (o *testTreeDriver) Factory(ctx context.Context, kind kind.Kind) NodeDriverInterface {
d := newTestNodeDriver()
d.SetTreeDriver(o)
return d
}
func newTestTree() TreeInterface {
tree := &testTree{}
tree.Init(tree, newTestOptions())
tree.Trace("init done")
tree.SetDriver(newTestTreeDriver())
tree.Register(kindTestNodeLevelOne, func(ctx context.Context, kind kind.Kind) NodeInterface {
node := &testNodeLevelOne{}
return node.Init(node)
})
tree.Register(kindTestNodeLevelTwo, func(ctx context.Context, kind kind.Kind) NodeInterface {
node := &testNodeLevelTwo{}
return node.Init(node)
})
return tree
}
type testNodeInterface interface {
GetV() int
}
type testNode struct {
Node
v int
}
func (o *testNode) GetV() int {
return o.v
}
func (o *testNode) Equals(ctx context.Context, other NodeInterface) bool {
if !o.Node.Equals(ctx, other) {
return false
}
return o.GetV() == other.(testNodeInterface).GetV()
}
var kindTestNodeLevelOne = kind.Kind("levelone")
type testNodeLevelOne struct {
testNode
}
var kindTestNodeLevelTwo = kind.Kind("leveltwo")
type testNodeLevelTwo struct {
testNode
}

52
tree/generic/mirror.go Normal file
View file

@ -0,0 +1,52 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"code.forgejo.org/f3/gof3/v3/path"
)
type MirrorOptions struct {
noremap bool
}
func NewMirrorOptions() *MirrorOptions {
return &MirrorOptions{}
}
func (o *MirrorOptions) SetNoRemap(noremap bool) *MirrorOptions {
o.noremap = noremap
return o
}
func NodeMirror(ctx context.Context, origin, destination NodeInterface, options *MirrorOptions) {
origin.Trace("origin '%s' | destination '%s'", origin.GetCurrentPath().ReadableString(), destination.GetCurrentPath().ReadableString())
origin.Trace("start unify references")
for _, reference := range NodeCollectReferences(ctx, origin) {
origin.Trace("unify reference %s", reference)
TreeUnifyPath(ctx, origin.GetTree(), reference, destination.GetTree(), NewUnifyOptions(destination.GetTree()))
}
origin.Trace("end unify references")
originParents := origin.GetParent().GetCurrentPath()
destinationParents := destination.GetParent().GetCurrentPath()
NodeUnify(ctx, origin, originParents, destination, destinationParents, NewUnifyOptions(destination.GetTree()))
}
func TreeMirror(ctx context.Context, originTree, destinationTree TreeInterface, path path.Path, options *MirrorOptions) {
originTree.Trace("'%s'", path.ReadableString())
TreeUnifyPath(ctx, originTree, path, destinationTree, NewUnifyOptions(destinationTree))
TreeParallelApply(ctx, originTree, path, destinationTree, NewParallelApplyOptions(func(ctx context.Context, origin, destination NodeInterface) {
NodeMirror(ctx, origin, destination, NewMirrorOptions())
}))
}
func TreePartialMirror(ctx context.Context, originTree TreeInterface, originPath path.Path, destinationTree TreeInterface, destinationPath path.Path, options *MirrorOptions) {
originTree.Trace("'%s => %s'", originPath, destinationPath)
originNode := originTree.MustFind(originPath)
destinationNode := destinationTree.MustFind(destinationPath)
NodeMirror(ctx, originNode, destinationNode, NewMirrorOptions())
}

414
tree/generic/node.go Normal file
View file

@ -0,0 +1,414 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"fmt"
"sort"
"code.forgejo.org/f3/gof3/v3/f3"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/logger"
"code.forgejo.org/f3/gof3/v3/path"
"code.forgejo.org/f3/gof3/v3/util"
)
type ChildrenSlice []NodeInterface
func NewChildrenSlice(len int) ChildrenSlice {
return make([]NodeInterface, 0, len)
}
func (o ChildrenSlice) Len() int {
return len(o)
}
func (o ChildrenSlice) Swap(i, j int) {
o[i], o[j] = o[j], o[i]
}
func (o ChildrenSlice) Less(i, j int) bool {
return o[i].GetID().String() < o[j].GetID().String()
}
type NodeChildren map[id.NodeID]NodeInterface
func NewNodeChildren() NodeChildren {
return make(NodeChildren)
}
var (
NilNode = &Node{isNil: true}
NilParent = NilNode
)
type Node struct {
logger.Logger
tree TreeInterface
isNil bool
sync bool
kind kind.Kind
id id.NodeID
driver NodeDriverInterface
self NodeInterface
parent NodeInterface
children NodeChildren
}
func NewElementNode() path.PathElement {
return NewNode().(path.PathElement)
}
func NewNode() NodeInterface {
node := &Node{}
return node.Init(node)
}
func NewNodeFromID[T any](i T) NodeInterface {
node := NewNode()
node.SetID(id.NewNodeID(i))
return node
}
func (o *Node) Init(self NodeInterface) NodeInterface {
o.SetTree(nil)
o.SetIsNil(true)
o.SetKind(kind.KindNil)
o.SetID(id.NilID)
o.SetSelf(self)
o.SetParent(NilNode)
o.children = NewNodeChildren()
return self
}
func (o *Node) GetIsNil() bool { return o == nil || o.isNil }
func (o *Node) SetIsNil(isNil bool) { o.isNil = isNil }
func (o *Node) GetIsSync() bool { return o.sync }
func (o *Node) SetIsSync(sync bool) { o.sync = sync }
func (o *Node) GetSelf() NodeInterface { return o.self }
func (o *Node) SetSelf(self NodeInterface) { o.self = self }
func (o *Node) GetParent() NodeInterface { return o.parent }
func (o *Node) SetParent(parent NodeInterface) { o.parent = parent }
func (o *Node) GetKind() kind.Kind { return o.kind }
func (o *Node) SetKind(kind kind.Kind) { o.kind = kind }
func (o *Node) GetID() id.NodeID {
if o.id == nil {
return id.NilID
}
return o.id
}
func (o *Node) SetID(id id.NodeID) { o.id = id }
func (o *Node) GetMappedID() id.NodeID {
mappedID := o.GetDriver().GetMappedID()
if mappedID == nil {
mappedID = id.NilID
}
return mappedID
}
func (o *Node) SetMappedID(mapped id.NodeID) {
o.GetDriver().SetMappedID(mapped)
}
func (o *Node) GetTree() TreeInterface { return o.tree }
func (o *Node) SetTree(tree TreeInterface) {
o.tree = tree
if tree != nil {
o.SetLogger(tree.GetLogger())
}
}
func (o *Node) GetDriver() NodeDriverInterface { return o.driver }
func (o *Node) SetDriver(driver NodeDriverInterface) {
driver.SetNode(o)
o.driver = driver
}
func (o *Node) GetNodeChildren() NodeChildren {
return o.children
}
func (o *Node) GetChildren() ChildrenSlice {
children := NewChildrenSlice(len(o.children))
for _, child := range o.children {
children = append(children, child)
}
sort.Sort(children)
return children
}
func (o *Node) SetChildren(children NodeChildren) { o.children = children }
func (o *Node) String() string {
driver := o.GetDriver().String()
if driver != "" {
driver = "=" + driver
}
return o.GetID().String() + driver
}
func (o *Node) Equals(ctx context.Context, other NodeInterface) bool {
return o.GetKind() == other.GetKind() && o.GetID() == other.GetID()
}
func (o *Node) GetCurrentPath() path.Path {
var p path.Path
if o.GetIsNil() {
p = path.NewPath()
} else {
p = o.GetParent().GetCurrentPath().Append(o)
}
return p
}
func (o *Node) ListPage(ctx context.Context, page int) ChildrenSlice {
return o.GetDriver().ListPage(ctx, page)
}
func (o *Node) List(ctx context.Context) ChildrenSlice {
o.Trace("%s", o.GetKind())
children := NewNodeChildren()
self := o.GetSelf()
for page := 1; ; page++ {
util.MaybeTerminate(ctx)
childrenPage := self.ListPage(ctx, page)
for _, child := range childrenPage {
children[child.GetID()] = child
}
if len(childrenPage) < o.GetTree().GetPageSize() {
break
}
}
o.children = children
return o.GetChildren()
}
func (o *Node) GetChild(id id.NodeID) NodeInterface {
child, ok := o.children[id]
if !ok {
return NilNode
}
return child
}
func (o *Node) SetChild(child NodeInterface) NodeInterface {
o.children[child.GetID()] = child
return child
}
func (o *Node) DeleteChild(id id.NodeID) NodeInterface {
if child, ok := o.children[id]; ok {
delete(o.children, id)
return child
}
return NilNode
}
func (o *Node) CreateChild(ctx context.Context) NodeInterface {
tree := o.GetTree()
child := tree.Factory(ctx, tree.GetChildrenKind(o.GetKind()))
child.SetParent(o)
return child
}
func (o *Node) GetIDFromName(ctx context.Context, name string) id.NodeID {
return o.GetDriver().GetIDFromName(ctx, name)
}
func (o *Node) Get(ctx context.Context) NodeInterface {
if o.GetDriver().Get(ctx) {
o.SetIsSync(true)
}
return o
}
func (o *Node) Upsert(ctx context.Context) NodeInterface {
if o.GetID() != id.NilID {
o.GetDriver().Patch(ctx)
} else {
o.SetID(o.GetDriver().Put(ctx))
}
if o.GetParent() != NilNode {
o.GetParent().SetChild(o)
}
return o
}
func (o *Node) Delete(ctx context.Context) NodeInterface {
o.GetDriver().Delete(ctx)
return o.GetParent().DeleteChild(o.GetID())
}
func (o *Node) NewFormat() f3.Interface {
return o.GetDriver().NewFormat()
}
func (o *Node) FromFormat(f f3.Interface) NodeInterface {
o.SetID(id.NewNodeID(f.GetID()))
o.GetDriver().FromFormat(f)
return o
}
func (o *Node) ToFormat() f3.Interface {
if o == nil || o.GetDriver() == nil {
return nil
}
return o.GetDriver().ToFormat()
}
func (o *Node) LookupMappedID(id id.NodeID) id.NodeID { return o.GetDriver().LookupMappedID(id) }
func (o *Node) ApplyAndGet(ctx context.Context, p path.Path, options *ApplyOptions) bool {
applyWrapInGet := func(options *ApplyOptions) ApplyFunc {
return func(ctx context.Context, parent, p path.Path, node NodeInterface) {
if options.fun != nil && (p.Empty() || options.where == ApplyEachNode) {
options.fun(ctx, parent, p, node)
}
if p.Empty() {
return
}
childID := p.First().(NodeInterface).GetID()
// is it a known child?
child := node.GetChild(childID)
// if not, maybe it is a known child referenced by name
if child.GetIsNil() {
childIDFromName := node.GetIDFromName(ctx, childID.String())
if childIDFromName != id.NilID {
childID = childIDFromName
child = node.GetChild(childID)
p.First().(NodeInterface).SetID(childID)
}
}
// not a known child by ID or by Name: make one
if child.GetIsNil() {
child = node.CreateChild(ctx)
child.SetID(childID)
if child.Get(ctx).GetIsSync() {
// only set the child if the driver is able to get it, otherwise
// it means that although the ID is known the child itself cannot be
// obtained and is assumed to be not found
node.SetChild(child)
}
}
}
}
return o.Apply(ctx, path.NewPath(), p, NewApplyOptions(applyWrapInGet(options)).SetWhere(ApplyEachNode))
}
func (o *Node) WalkAndGet(ctx context.Context, parent path.Path, options *WalkOptions) {
walkWrapInGet := func(fun WalkFunc) WalkFunc {
return func(ctx context.Context, p path.Path, node NodeInterface) {
node.Get(ctx)
node.List(ctx)
if fun != nil {
fun(ctx, p, node)
}
}
}
o.Walk(ctx, parent, NewWalkOptions(walkWrapInGet(options.fun)))
}
func (o *Node) Walk(ctx context.Context, parent path.Path, options *WalkOptions) {
util.MaybeTerminate(ctx)
options.fun(ctx, parent, o.GetSelf())
parent = parent.Append(o.GetSelf().(path.PathElement))
for _, child := range o.GetSelf().GetChildren() {
child.Walk(ctx, parent, options)
}
}
func (o *Node) FindAndGet(ctx context.Context, p path.Path) NodeInterface {
var r NodeInterface
r = NilNode
set := func(ctx context.Context, parent, p path.Path, node NodeInterface) {
r = node
}
o.ApplyAndGet(ctx, p, NewApplyOptions(set))
return r
}
func (o *Node) MustFind(p path.Path) NodeInterface {
found := o.Find(p)
if found.GetIsNil() {
panic(fmt.Errorf("%s not found", p))
}
return found
}
func (o *Node) Find(p path.Path) NodeInterface {
if p.Empty() {
return o
}
child := o.GetChild(p.First().(NodeInterface).GetID())
if child.GetIsNil() {
o.Info("'%s' not found", p.String())
return NilNode
}
return child.Find(p.RemoveFirst())
}
func (o *Node) Apply(ctx context.Context, parentPath, p path.Path, options *ApplyOptions) bool {
o.Trace("parent '%s', node '%s', path '%s'", parentPath.ReadableString(), o.String(), p.ReadableString())
util.MaybeTerminate(ctx)
if p.Empty() {
options.fun(ctx, parentPath, p, o.GetSelf())
return true
}
i := p.First().(NodeInterface).GetID().String()
if i == "." {
return o.Apply(ctx, parentPath, p.RemoveFirst(), options)
}
if i == ".." {
parent, parentPath := parentPath.Pop()
return parent.(NodeInterface).Apply(ctx, parentPath, p.RemoveFirst(), options)
}
if options.where == ApplyEachNode {
options.fun(ctx, parentPath, p, o.GetSelf())
}
child := o.GetChild(p.First().(NodeInterface).GetID())
if child.GetIsNil() && options.search == ApplySearchByName {
if childID := o.GetIDFromName(ctx, p.First().(NodeInterface).GetID().String()); childID != id.NilID {
child = o.GetChild(childID)
}
}
if child.GetIsNil() {
return false
}
parentPath = parentPath.Append(o.GetSelf().(path.PathElement))
return child.Apply(ctx, parentPath, p.RemoveFirst(), options)
}
type ErrorNodeNotFound error
func NewError[T error](message string, args ...any) T {
e := fmt.Errorf(message, args...)
return e.(T)
}

296
tree/generic/node_test.go Normal file
View file

@ -0,0 +1,296 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"fmt"
"testing"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/kind"
"github.com/stretchr/testify/assert"
)
func TestNodeInit(t *testing.T) {
node := NewNode()
assert.NotNil(t, node)
}
func TestNewNodeFromID(t *testing.T) {
{
i := "nodeID"
node := NewNodeFromID(i)
assert.Equal(t, id.NewNodeID(i), node.GetID())
}
{
i := 123
node := NewNodeFromID(i)
assert.Equal(t, id.NewNodeID(fmt.Sprintf("%d", i)), node.GetID())
}
}
func TestNodeIsNil(t *testing.T) {
node := NewNode()
assert.True(t, node.GetIsNil())
node.SetIsNil(false)
assert.False(t, node.GetIsNil())
node.SetIsNil(true)
assert.True(t, node.GetIsNil())
var nilNode *Node
assert.True(t, nilNode.GetIsNil())
}
func TestNodeEquals(t *testing.T) {
ctx := context.Background()
tree := newTestTree()
one := tree.Factory(ctx, kindTestNodeLevelOne)
one.(*testNodeLevelOne).v = 1
assert.True(t, one.Equals(ctx, one))
two := tree.Factory(ctx, kindTestNodeLevelOne)
two.(*testNodeLevelOne).v = 2
assert.False(t, one.Equals(ctx, two))
}
func TestNodeParent(t *testing.T) {
node := NewNode()
assert.True(t, node.GetParent().GetIsNil())
parent := NewNode()
node.SetParent(parent)
assert.True(t, parent == node.GetParent())
}
func TestNodeKind(t *testing.T) {
node := NewNode()
assert.EqualValues(t, kind.KindNil, node.GetKind())
kind := kind.Kind("something")
node.SetKind(kind)
assert.True(t, kind == node.GetKind())
}
func TestNodeMappedID(t *testing.T) {
node := NewNode()
node.SetDriver(NewNullDriver())
assert.EqualValues(t, id.NilID, node.GetMappedID())
mapped := id.NewNodeID("mapped")
node.SetMappedID(mapped)
assert.True(t, mapped == node.GetMappedID())
}
func TestNodeNewFormat(t *testing.T) {
node := NewNode()
node.SetDriver(NewNullDriver())
assert.Panics(t, func() { node.NewFormat() })
}
func TestChildren(t *testing.T) {
parent := NewNode()
assert.Empty(t, parent.GetChildren())
id1 := id.NewNodeID("one")
child1 := NewNode()
child1.SetID(id1)
parent.SetChild(child1)
id2 := id.NewNodeID("two")
child2 := NewNode()
child2.SetID(id2)
parent.SetChild(child2)
children := parent.GetChildren()
assert.Len(t, children, 2)
assert.Equal(t, children[0].GetID(), id1)
assert.Equal(t, children[1].GetID(), id2)
nodeChildren := parent.GetNodeChildren()
assert.Len(t, nodeChildren, 2)
delete(nodeChildren, id1)
parent.SetChildren(nodeChildren)
nodeChildren = parent.GetNodeChildren()
assert.Len(t, nodeChildren, 1)
}
func TestNodeID(t *testing.T) {
node := NewNode()
assert.EqualValues(t, id.NilID, node.GetID())
i := id.NewNodeID("1234")
node.SetID(i)
assert.True(t, i == node.GetID())
}
func TestNodeTree(t *testing.T) {
node := NewNode()
assert.Nil(t, node.GetTree())
tree := NewTree(newTestOptions())
node.SetTree(tree)
assert.True(t, tree == node.GetTree())
}
type driverChildren struct {
NullDriver
}
func (o *driverChildren) ListPage(cxt context.Context, page int) ChildrenSlice {
node := o.GetNode()
tree := node.GetTree()
count := tree.GetPageSize()
if page > 1 {
count = 1
}
offset := (page - 1) * tree.GetPageSize()
children := NewChildrenSlice(0)
for i := 0; i < count; i++ {
child := tree.GetSelf().Factory(cxt, kind.KindNil)
child.SetID(id.NewNodeID(i + offset))
children = append(children, child)
}
return children
}
type driverTreeChildren struct {
NullTreeDriver
}
func (o *driverTreeChildren) Factory(ctx context.Context, kind kind.Kind) NodeDriverInterface {
return &driverChildren{}
}
type treeChildren struct {
Tree
}
func newTreeChildren() TreeInterface {
tree := &treeChildren{}
tree.Init(tree, newTestOptions())
treeDriver := &driverTreeChildren{}
treeDriver.Init()
tree.SetDriver(treeDriver)
tree.Register(kind.KindNil, func(ctx context.Context, kind kind.Kind) NodeInterface {
node := NewNode()
node.SetIsNil(false)
return node
})
return tree
}
func TestNodeListPage(t *testing.T) {
tree := newTreeChildren()
ctx := context.Background()
node := tree.Factory(ctx, kind.KindNil)
children := node.ListPage(context.Background(), int(1))
assert.NotNil(t, children)
assert.EqualValues(t, tree.GetPageSize(), len(children))
children = node.ListPage(context.Background(), int(2))
assert.NotNil(t, children)
assert.EqualValues(t, 1, len(children))
}
func TestNodeList(t *testing.T) {
tree := newTreeChildren()
ctx := context.Background()
node := tree.Factory(ctx, kind.KindNil)
children := node.List(ctx)
assert.EqualValues(t, tree.GetPageSize()+1, len(children))
idThree := id.NewNodeID("3")
childThree := node.GetChild(idThree)
assert.False(t, childThree.GetIsNil())
assert.EqualValues(t, idThree, childThree.GetID())
node.DeleteChild(idThree)
childThree = node.GetChild(idThree)
assert.True(t, childThree.GetIsNil())
}
type nodeGetIDFromName struct {
Node
name string
}
func (o *nodeGetIDFromName) GetIDFromName(ctx context.Context, name string) id.NodeID {
for _, child := range o.GetChildren() {
if child.(*nodeGetIDFromName).name == name {
return child.GetID()
}
}
return id.NilID
}
func newNodeGetIDFromName(i, name string) NodeInterface {
node := &nodeGetIDFromName{}
node.Init(node)
node.SetIsNil(false)
node.SetID(id.NewNodeID(i))
node.name = name
return node
}
func TestNodeGetIDFromName(t *testing.T) {
i := "1243"
name := "NAME"
node := newNodeGetIDFromName(i, name)
ctx := context.Background()
parent := newNodeGetIDFromName("parent", "PARENT")
parent.SetChild(node)
r := parent.GetIDFromName(ctx, "OTHERNAME")
assert.EqualValues(t, id.NilID, r)
r = parent.GetIDFromName(ctx, name)
assert.True(t, r == id.NewNodeID(i))
}
func TestNodePath(t *testing.T) {
{
path := NilNode.GetCurrentPath()
assert.True(t, path.Empty())
pathString := path.PathString()
assert.True(t, pathString.Empty())
assert.EqualValues(t, "", pathString.Join())
}
root := NewNode()
root.SetIsNil(false)
root.SetKind(kind.KindRoot)
level1 := NewNode()
level1.SetIsNil(false)
level1.SetParent(root)
id1 := id.NewNodeID("1")
level1.SetID(id1)
level2 := NewNode()
level2.SetIsNil(false)
level2.SetParent(level1)
id2 := id.NewNodeID("2")
level2.SetID(id2)
{
p := level2.GetCurrentPath()
assert.False(t, p.Empty())
if assert.EqualValues(t, 3, p.Length()) {
assert.True(t, root == p.First(), p.First().(NodeInterface).GetID())
rest := p.RemoveFirst()
assert.True(t, level1 == rest.First(), p.First().(NodeInterface).GetID())
rest = rest.RemoveFirst()
assert.True(t, level2 == rest.First(), p.First().(NodeInterface).GetID())
}
pathString := p.PathString()
assert.False(t, pathString.Empty())
assert.EqualValues(t, 3, len(pathString.Elements()))
assert.EqualValues(t, "/1/2", pathString.Join())
}
}

13
tree/generic/path.go Normal file
View file

@ -0,0 +1,13 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"code.forgejo.org/f3/gof3/v3/path"
)
func NewPathFromString(pathString string) path.Path {
return path.NewPathFromString(NewElementNode, pathString)
}

View file

@ -0,0 +1,92 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"strings"
"code.forgejo.org/f3/gof3/v3/f3"
"code.forgejo.org/f3/gof3/v3/path"
"code.forgejo.org/f3/gof3/v3/util"
)
type ErrorRemapReferencesRelative error
func RemapReferences(ctx context.Context, node NodeInterface, f f3.Interface) {
for _, reference := range f.GetReferences() {
toPath := path.NewPath()
collectTo := func(ctx context.Context, parent, p path.Path, node NodeInterface) {
element := NewNode()
mappedID := node.GetMappedID()
if mappedID.String() == "" {
node.Trace("mapped ID for %s is not defined", p.ReadableString())
}
element.SetID(mappedID)
toPath = toPath.Append(element.(path.PathElement))
}
from := reference.Get()
isRelative := !strings.HasPrefix(from, "/")
if isRelative && !strings.HasPrefix(from, "..") {
panic(NewError[ErrorRemapReferencesRelative]("relative references that do not start with .. are not supported '%s'", from))
}
current := node.GetCurrentPath().String()
fromPath := path.PathAbsolute(NewElementNode, current, from)
node.GetTree().Apply(ctx, fromPath, NewApplyOptions(collectTo).SetWhere(ApplyEachNode))
to := toPath.String()
node.Trace("from '%s' to '%s'", fromPath.ReadableString(), toPath.ReadableString())
if isRelative {
currentMapped := node.GetParent().GetCurrentPath().PathMappedString().Join()
// because the mapped ID of the current node has not been allocated yet
// and it does not matter as long as it is replaced with ..
// it will not work at all if a relative reference does not start with ..
currentMapped += "/PLACEHODLER"
to = path.PathRelativeString(currentMapped, to)
}
node.Trace("convert reference %s => %s", reference.Get(), to)
reference.Set(to)
}
}
func NodeCollectReferences(ctx context.Context, node NodeInterface) []path.Path {
pathToReferences := make(map[string]path.Path, 5)
tree := node.GetTree()
collect := func(ctx context.Context, parent path.Path, node NodeInterface) {
util.MaybeTerminate(ctx)
f := node.GetSelf().ToFormat()
for _, reference := range f.GetReferences() {
absoluteReference := path.PathAbsoluteString(node.GetCurrentPath().String(), reference.Get())
if _, ok := pathToReferences[absoluteReference]; ok {
continue
}
tree.ApplyAndGet(ctx, path.NewPathFromString(NewElementNode, absoluteReference), NewApplyOptions(func(ctx context.Context, parent, path path.Path, node NodeInterface) {
pathToReferences[absoluteReference] = node.GetCurrentPath()
}))
}
}
node.Walk(ctx, path.NewPath(node.(path.PathElement)), NewWalkOptions(collect))
references := make([]path.Path, 0, len(pathToReferences))
for _, reference := range pathToReferences {
tree.Debug("collect %s", reference)
references = append(references, reference)
}
return references
}
func TreeCollectReferences(ctx context.Context, tree TreeInterface, p path.Path) []path.Path {
var references []path.Path
tree.Apply(ctx, p, NewApplyOptions(func(ctx context.Context, parent, path path.Path, node NodeInterface) {
references = NodeCollectReferences(ctx, node)
}))
return references
}

View file

@ -0,0 +1,166 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"testing"
"code.forgejo.org/f3/gof3/v3/f3"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/path"
"github.com/stretchr/testify/assert"
)
type referenceFormat struct {
f3.Common
R *f3.Reference
}
func (o *referenceFormat) Clone() f3.Interface {
clone := &referenceFormat{}
*clone = *o
return clone
}
func (o *referenceFormat) GetReferences() f3.References {
references := o.Common.GetReferences()
if o.R != nil {
references = append(references, o.R)
}
return references
}
type referencesNodeDriver struct {
NullDriver
f referenceFormat
}
func (o *referencesNodeDriver) GetIDFromName(ctx context.Context, name string) id.NodeID {
return id.NilID
}
func (o *referencesNodeDriver) Get(context.Context) bool {
return true
}
func (o *referencesNodeDriver) NewFormat() f3.Interface {
return &referenceFormat{}
}
func (o *referencesNodeDriver) ToFormat() f3.Interface {
return &o.f
}
func (o *referencesNodeDriver) FromFormat(f f3.Interface) {
o.f = *f.(*referenceFormat)
}
func newReferencesNodeDriver() NodeDriverInterface {
return &referencesNodeDriver{}
}
type testTreeReferencesDriver struct {
NullTreeDriver
}
func newTestTreeReferencesDriver() TreeDriverInterface {
return &testTreeReferencesDriver{}
}
func (o *testTreeReferencesDriver) Factory(ctx context.Context, kind kind.Kind) NodeDriverInterface {
d := newReferencesNodeDriver()
d.SetTreeDriver(o)
return d
}
var kindTestNodeReferences = kind.Kind("references")
type testNodeReferences struct {
testNode
}
func newTestTreeReferences() TreeInterface {
tree := &testTree{}
tree.Init(tree, newTestOptions())
tree.SetDriver(newTestTreeReferencesDriver())
tree.Register(kindTestNodeReferences, func(ctx context.Context, kind kind.Kind) NodeInterface {
node := &testNodeReferences{}
return node.Init(node)
})
return tree
}
func TestTreeCollectReferences(t *testing.T) {
tree := newTestTreeReferences()
root := tree.Factory(context.Background(), kindTestNodeReferences)
tree.SetRoot(root)
one := tree.Factory(context.Background(), kindTestNodeReferences)
one.FromFormat(&referenceFormat{R: f3.NewReference("/somewhere")})
one.SetID(id.NewNodeID("one"))
root.SetChild(one)
// the second node that has the same reference is here to ensure
// they are deduplicated
two := tree.Factory(context.Background(), kindTestNodeReferences)
two.FromFormat(&referenceFormat{R: f3.NewReference("/somewhere")})
two.SetID(id.NewNodeID("two"))
root.SetChild(two)
references := TreeCollectReferences(context.Background(), tree, path.NewPathFromString(NewElementNode, "/"))
assert.Len(t, references, 1)
assert.EqualValues(t, "/somewhere", references[0].String())
}
func TestRemapReferences(t *testing.T) {
tree := newTestTreeReferences()
root := tree.Factory(context.Background(), kindTestNodeReferences)
tree.SetRoot(root)
one := tree.Factory(context.Background(), kindTestNodeReferences)
one.SetID(id.NewNodeID("one"))
one.SetMappedID(id.NewNodeID("remappedone"))
one.SetParent(root)
root.SetChild(one)
two := tree.Factory(context.Background(), kindTestNodeReferences)
two.FromFormat(&referenceFormat{R: f3.NewReference("/one")})
two.SetID(id.NewNodeID("two"))
two.SetMappedID(id.NewNodeID("two"))
two.SetParent(root)
root.SetChild(two)
{
f := two.ToFormat()
RemapReferences(context.Background(), two, f)
r := f.GetReferences()
if assert.Len(t, r, 1) {
assert.Equal(t, "/remappedone", r[0].Get())
}
}
three := tree.Factory(context.Background(), kindTestNodeReferences)
three.FromFormat(&referenceFormat{R: f3.NewReference("../../one")})
three.SetID(id.NewNodeID("three"))
three.SetMappedID(id.NewNodeID("three"))
three.SetParent(two)
two.SetChild(three)
{
f := three.ToFormat()
RemapReferences(context.Background(), three, f)
r := f.GetReferences()
if assert.Len(t, r, 1) {
assert.Equal(t, "../../remappedone", r[0].Get())
}
}
assert.Panics(t, func() { RemapReferences(context.Background(), three, &referenceFormat{R: f3.NewReference("./one")}) })
}

134
tree/generic/tree.go Normal file
View file

@ -0,0 +1,134 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/logger"
"code.forgejo.org/f3/gof3/v3/options"
"code.forgejo.org/f3/gof3/v3/path"
)
type FactoryFun func(ctx context.Context, kind kind.Kind) NodeInterface
type kindMap map[kind.Kind]FactoryFun
type Tree struct {
logger.Logger
opts options.Interface
self TreeInterface
driver TreeDriverInterface
root NodeInterface
kind kindMap
}
func NewTree(opts options.Interface) TreeInterface {
tree := &Tree{}
return tree.Init(tree, opts)
}
func (o *Tree) Init(self TreeInterface, opts options.Interface) TreeInterface {
o.self = self
o.kind = make(kindMap)
o.SetDriver(NewNullTreeDriver())
o.SetOptions(opts)
o.SetLogger(opts.(options.LoggerInterface).GetLogger())
return o
}
func (o *Tree) GetOptions() options.Interface { return o.opts }
func (o *Tree) SetOptions(opts options.Interface) { o.opts = opts }
func (o *Tree) GetSelf() TreeInterface { return o.self }
func (o *Tree) SetSelf(self TreeInterface) { o.self = self }
func (o *Tree) GetRoot() NodeInterface { return o.root }
func (o *Tree) SetRoot(root NodeInterface) { o.root = root }
func (o *Tree) GetDriver() TreeDriverInterface { return o.driver }
func (o *Tree) SetDriver(driver TreeDriverInterface) {
driver.SetTree(o.GetSelf())
o.driver = driver
}
func (o *Tree) GetChildrenKind(parentKind kind.Kind) kind.Kind {
return kind.KindNil
}
func (o *Tree) GetPageSize() int { return o.GetDriver().GetPageSize() }
func (o *Tree) AllocateID() bool { return o.GetDriver().AllocateID() }
func (o *Tree) Walk(ctx context.Context, options *WalkOptions) {
o.GetRoot().Walk(ctx, path.NewPath(), options)
}
func (o *Tree) WalkAndGet(ctx context.Context, options *WalkOptions) {
o.GetRoot().WalkAndGet(ctx, path.NewPath(), options)
}
func (o *Tree) Clear(ctx context.Context) {
rootKind := o.GetRoot().GetKind()
o.SetRoot(o.Factory(ctx, rootKind))
}
func (o *Tree) Exists(ctx context.Context, path path.Path) bool {
return o.Find(path) != NilNode
}
func (o *Tree) MustFind(path path.Path) NodeInterface {
return o.GetRoot().MustFind(path.RemoveFirst())
}
func (o *Tree) Find(path path.Path) NodeInterface {
return o.GetRoot().Find(path.RemoveFirst())
}
func (o *Tree) FindAndGet(ctx context.Context, path path.Path) NodeInterface {
return o.GetRoot().FindAndGet(ctx, path.RemoveFirst())
}
func (o *Tree) Diff(a, b NodeInterface) string {
return o.GetDriver().Diff(a.GetDriver(), b.GetDriver())
}
func (o *Tree) Apply(ctx context.Context, p path.Path, options *ApplyOptions) bool {
if p.Empty() {
return true
}
return o.GetRoot().Apply(ctx, path.NewPath(), p.RemoveFirst(), options)
}
func (o *Tree) ApplyAndGet(ctx context.Context, path path.Path, options *ApplyOptions) bool {
if path.Empty() {
return true
}
return o.GetRoot().ApplyAndGet(ctx, path.RemoveFirst(), options)
}
func (o *Tree) Factory(ctx context.Context, kind kind.Kind) NodeInterface {
var node NodeInterface
if factory, ok := o.kind[kind]; ok {
node = factory(ctx, kind)
} else {
node = NewNode()
}
node.SetIsNil(false)
node.SetKind(kind)
node.SetTree(o.GetSelf())
if o.GetDriver() != nil {
nodeDriver := o.GetDriver().Factory(ctx, kind)
nodeDriver.SetTreeDriver(o.GetDriver())
node.SetDriver(nodeDriver)
}
return node
}
func (o *Tree) Register(kind kind.Kind, factory FactoryFun) {
o.kind[kind] = factory
}

90
tree/generic/tree_test.go Normal file
View file

@ -0,0 +1,90 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"testing"
"code.forgejo.org/f3/gof3/v3/path"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTreeInit(t *testing.T) {
tree := NewTree(newTestOptions())
assert.NotNil(t, tree)
assert.True(t, tree == tree.GetSelf())
tree.SetSelf(tree)
assert.True(t, tree == tree.GetSelf())
other := NewTree(newTestOptions())
assert.False(t, other == tree.GetSelf())
assert.False(t, other.GetSelf() == tree)
assert.False(t, other.GetSelf() == tree.GetSelf())
}
func TestTreeRoot(t *testing.T) {
tree := NewTree(newTestOptions())
root := NewNode()
tree.SetRoot(root)
assert.True(t, root == tree.GetRoot())
}
func TestTreePageSize(t *testing.T) {
tree := NewTree(newTestOptions())
assert.EqualValues(t, DefaultPageSize, tree.GetPageSize())
pageSize := int(100)
tree.GetDriver().SetPageSize(pageSize)
assert.EqualValues(t, pageSize, tree.GetPageSize())
}
func TestTreeAllocateID(t *testing.T) {
tree := NewTree(newTestOptions())
assert.True(t, tree.AllocateID())
}
func TestTreeFactoryDerived(t *testing.T) {
tree := newTestTree()
root := tree.Factory(context.Background(), kindTestNodeLevelOne)
tree.SetRoot(root)
assert.True(t, root == tree.GetRoot())
assert.True(t, kindTestNodeLevelOne == root.GetKind())
assert.True(t, tree.GetSelf() == root.GetTree().GetSelf())
leveltwo := tree.Factory(context.Background(), kindTestNodeLevelTwo)
leveltwo.SetParent(root)
assert.True(t, root == leveltwo.GetParent())
assert.True(t, kindTestNodeLevelTwo == leveltwo.GetKind())
assert.True(t, tree.GetSelf() == leveltwo.GetTree().GetSelf())
}
func TestTreeApply(t *testing.T) {
tree := newTestTree()
root := tree.Factory(context.Background(), kindTestNodeLevelOne)
tree.SetRoot(root)
assert.True(t, tree.Apply(context.Background(), path.NewPath(), nil))
assert.True(t, tree.Apply(context.Background(), path.NewPathFromString(NewElementNode, "/"),
NewApplyOptions(func(ctx context.Context, parent, path path.Path, node NodeInterface) {
require.Equal(t, root, node)
})),
)
}
func TestTreeApplyAndGet(t *testing.T) {
tree := newTestTree()
root := tree.Factory(context.Background(), kindTestNodeLevelOne)
tree.SetRoot(root)
assert.True(t, tree.ApplyAndGet(context.Background(), path.NewPath(), nil))
assert.True(t, tree.ApplyAndGet(context.Background(), path.NewPathFromString(NewElementNode, "/"),
NewApplyOptions(func(ctx context.Context, parent, path path.Path, node NodeInterface) {
require.Equal(t, root, node)
})),
)
}

320
tree/generic/unify.go Normal file
View file

@ -0,0 +1,320 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/path"
"code.forgejo.org/f3/gof3/v3/util"
)
type (
UnifyUpsertFunc func(ctx context.Context, origin NodeInterface, originParent path.Path, destination NodeInterface, destinationParent path.Path)
UnifyDeleteFunc func(ctx context.Context, destination NodeInterface, destinationParent path.Path)
)
type UnifyOptions struct {
destinationTree TreeInterface
upsert UnifyUpsertFunc
delete UnifyDeleteFunc
noremap bool
}
func NewUnifyOptions(destinationTree TreeInterface) *UnifyOptions {
return &UnifyOptions{
destinationTree: destinationTree,
}
}
func (o *UnifyOptions) SetUpsert(upsert UnifyUpsertFunc) *UnifyOptions {
o.upsert = upsert
return o
}
func (o *UnifyOptions) SetDelete(delete UnifyDeleteFunc) *UnifyOptions {
o.delete = delete
return o
}
func (o *UnifyOptions) SetNoRemap(noremap bool) *UnifyOptions {
o.noremap = noremap
return o
}
func TreeUnifyPath(ctx context.Context, origin TreeInterface, p path.Path, destination TreeInterface, options *UnifyOptions) {
if p.Empty() || p.First().(NodeInterface).GetID().String() == "." {
return
}
originRoot := origin.GetRoot()
destinationRoot := destination.GetRoot()
if destinationRoot == nil {
destinationRoot = destination.Factory(ctx, originRoot.GetKind())
destination.SetRoot(destinationRoot)
}
p = p.RemoveFirst()
if p.Empty() {
return
}
originNode := originRoot.GetChild(p.First().(NodeInterface).GetID()).GetSelf()
NodeUnifyPath(ctx, originNode, path.NewPath(originRoot.(path.PathElement)), p.RemoveFirst(), path.NewPath(destinationRoot.(path.PathElement)), options)
}
func TreeUnify(ctx context.Context, origin, destination TreeInterface, options *UnifyOptions) {
origin.Trace("")
originRoot := origin.GetRoot()
if originRoot == nil {
destination.SetRoot(nil)
return
}
destinationRoot := destination.GetRoot()
if destinationRoot == nil {
destinationRoot = destination.Factory(ctx, originRoot.GetKind())
destination.SetRoot(destinationRoot)
NodeCopy(ctx, originRoot, destinationRoot, originRoot.GetID(), options)
}
NodeUnify(ctx, originRoot, path.NewPath(), destinationRoot, path.NewPath(), options)
}
func NodeCopy(ctx context.Context, origin, destination NodeInterface, destinationID id.NodeID, options *UnifyOptions) {
f := origin.GetSelf().ToFormat()
if options.noremap {
origin.Trace("noremap")
} else {
RemapReferences(ctx, origin, f)
}
f.SetID(destinationID.String())
destination.GetSelf().FromFormat(f)
destination.Upsert(ctx)
}
func NodeUnify(ctx context.Context, origin NodeInterface, originPath path.Path, destination NodeInterface, destinationPath path.Path, options *UnifyOptions) {
origin.Trace("origin '%s' | destination '%s'", origin.GetCurrentPath().ReadableString(), destination.GetCurrentPath().ReadableString())
util.MaybeTerminate(ctx)
originPath = originPath.Append(origin.(path.PathElement))
destinationPath = destinationPath.Append(destination.(path.PathElement))
originChildren := origin.GetSelf().GetChildren()
existing := make(map[id.NodeID]any, len(originChildren))
for _, originChild := range originChildren {
destinationID := GetMappedID(ctx, originChild, destination, options)
destinationChild := destination.GetChild(destinationID)
createDestinationChild := destinationChild == NilNode
if createDestinationChild {
destinationChild = options.destinationTree.Factory(ctx, originChild.GetKind())
destinationChild.SetParent(destination)
}
NodeCopy(ctx, originChild, destinationChild, destinationID, options)
if options.upsert != nil {
options.upsert(ctx, originChild.GetSelf(), originPath, destinationChild.GetSelf(), destinationPath)
}
if createDestinationChild {
destination.SetChild(destinationChild)
}
SetMappedID(ctx, originChild, destinationChild, options)
existing[destinationChild.GetID()] = true
NodeUnify(ctx, originChild, originPath, destinationChild, destinationPath, options)
}
destinationChildren := destination.GetSelf().GetChildren()
for _, destinationChild := range destinationChildren {
destinationID := destinationChild.GetID()
if _, ok := existing[destinationID]; !ok {
destinationChild.GetSelf().Delete(ctx)
if options.delete != nil {
options.delete(ctx, destinationChild.GetSelf(), destinationPath)
}
destination.DeleteChild(destinationID)
}
}
}
func SetMappedID(ctx context.Context, origin, destination NodeInterface, options *UnifyOptions) {
if options.noremap {
return
}
origin.SetMappedID(destination.GetID())
}
func GetMappedID(ctx context.Context, origin, destinationParent NodeInterface, options *UnifyOptions) id.NodeID {
if options.noremap {
return origin.GetID()
}
if i := origin.GetMappedID(); i != id.NilID {
return i
}
return destinationParent.LookupMappedID(origin.GetID())
}
func NodeUnifyOne(ctx context.Context, origin NodeInterface, originPath, path, destinationPath path.Path, options *UnifyOptions) NodeInterface {
destinationParent := destinationPath.Last().(NodeInterface)
destinationID := GetMappedID(ctx, origin, destinationParent, options)
origin.Trace("'%s' '%s' '%s' %v", originPath.ReadableString(), path.ReadableString(), destinationPath.ReadableString(), destinationID)
destination := destinationParent.GetChild(destinationID)
createDestination := destination == NilNode
if createDestination {
destination = options.destinationTree.Factory(ctx, origin.GetKind())
destination.SetParent(destinationParent)
}
NodeCopy(ctx, origin, destination, destinationID, options)
if options.upsert != nil {
options.upsert(ctx, origin.GetSelf(), originPath, destination.GetSelf(), destinationPath)
}
if createDestination {
destinationParent.SetChild(destination)
}
origin.SetMappedID(destination.GetID())
return destination
}
func NodeUnifyPath(ctx context.Context, origin NodeInterface, originPath, path, destinationPath path.Path, options *UnifyOptions) {
origin.Trace("origin '%s' '%s' | destination '%s' | path '%s'", originPath.ReadableString(), origin.GetID(), destinationPath.ReadableString(), path.ReadableString())
util.MaybeTerminate(ctx)
if path.Empty() {
NodeUnifyOne(ctx, origin, originPath, path, destinationPath, options)
return
}
id := path.First().(NodeInterface).GetID().String()
if id == "." {
NodeUnifyOne(ctx, origin, originPath, path, destinationPath, options)
return
}
if id == ".." {
parent, originPath := originPath.Pop()
_, destinationPath := destinationPath.Pop()
NodeUnifyPath(ctx, parent.(NodeInterface), originPath, path.RemoveFirst(), destinationPath, options)
return
}
destination := NodeUnifyOne(ctx, origin, originPath, path, destinationPath, options)
originPath = originPath.Append(origin.GetSelf())
destinationPath = destinationPath.Append(destination.GetSelf())
child := origin.GetSelf().GetChild(path.First().(NodeInterface).GetID())
if child == NilNode {
panic(NewError[ErrorNodeNotFound]("%s has no child with id %s", originPath.String(), path.First().(NodeInterface).GetID()))
}
NodeUnifyPath(ctx, child, originPath, path.RemoveFirst(), destinationPath, options)
}
type ParallelApplyFunc func(ctx context.Context, origin, destination NodeInterface)
type ParallelApplyOptions struct {
fun ParallelApplyFunc
where ApplyWhere
noremap bool
}
func NewParallelApplyOptions(fun ParallelApplyFunc) *ParallelApplyOptions {
return &ParallelApplyOptions{
fun: fun,
}
}
func (o *ParallelApplyOptions) SetWhere(where ApplyWhere) *ParallelApplyOptions {
o.where = where
return o
}
func (o *ParallelApplyOptions) SetNoRemap(noremap bool) *ParallelApplyOptions {
o.noremap = noremap
return o
}
func TreePathRemap(ctx context.Context, origin TreeInterface, p path.Path, destination TreeInterface) path.Path {
remappedPath := path.NewPath()
remap := func(ctx context.Context, origin, destination NodeInterface) {
remappedPath = destination.GetCurrentPath()
}
TreeParallelApply(ctx, origin, p, destination, NewParallelApplyOptions(remap))
return remappedPath
}
func TreeParallelApply(ctx context.Context, origin TreeInterface, path path.Path, destination TreeInterface, options *ParallelApplyOptions) bool {
if path.Empty() {
return true
}
return NodeParallelApply(ctx, origin.GetRoot(), path.RemoveFirst(), destination.GetRoot(), options)
}
func NodeParallelApply(ctx context.Context, origin NodeInterface, path path.Path, destination NodeInterface, options *ParallelApplyOptions) bool {
origin.Trace("origin '%s' | destination '%s' | path '%s'", origin.GetCurrentPath().ReadableString(), destination.GetCurrentPath().ReadableString(), path.ReadableString())
util.MaybeTerminate(ctx)
if path.Empty() {
options.fun(ctx, origin, destination)
return true
}
i := path.First().(NodeInterface).GetID().String()
if i == "." {
return NodeParallelApply(ctx, origin, path.RemoveFirst(), destination, options)
}
if i == ".." {
return NodeParallelApply(ctx, origin.GetParent(), path.RemoveFirst(), destination.GetParent(), options)
}
if options.where == ApplyEachNode {
options.fun(ctx, origin, destination)
}
originChild := origin.GetSelf().GetChild(path.First().(NodeInterface).GetID())
if originChild == NilNode {
origin.Trace("no child %s", path.First().(NodeInterface).GetID())
return false
}
var mappedID id.NodeID
if options.noremap {
mappedID = originChild.GetID()
} else {
mappedID = originChild.GetMappedID()
if mappedID == id.NilID {
origin.Trace("%s no mapped", originChild.GetID())
return false
}
}
destinationChild := destination.GetChild(mappedID)
if destinationChild == NilNode {
panic(NewError[ErrorNodeNotFound]("%s has no child with id %s", destination.String(), originChild.GetMappedID()))
}
return NodeParallelApply(ctx, originChild, path.RemoveFirst(), destinationChild, options)
}

View file

@ -0,0 +1,57 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTreeUnify(t *testing.T) {
ctx := context.Background()
for _, testCase := range []struct {
name string
originRoot bool
destinationRoot bool
}{
{
name: "Origin",
originRoot: true,
destinationRoot: false,
},
{
name: "Destination",
originRoot: false,
destinationRoot: true,
},
{
name: "None",
originRoot: false,
destinationRoot: false,
},
} {
t.Run(testCase.name, func(t *testing.T) {
origin := newTestTree()
if testCase.originRoot {
origin.SetRoot(origin.Factory(ctx, kindTestNodeLevelOne))
}
destination := newTestTree()
if testCase.destinationRoot {
destination.SetRoot(destination.Factory(ctx, kindTestNodeLevelTwo))
}
TreeUnify(ctx, origin, destination, NewUnifyOptions(destination))
if testCase.originRoot {
destinationRoot := destination.GetRoot()
assert.NotNil(t, destinationRoot)
assert.EqualValues(t, kindTestNodeLevelOne, destinationRoot.GetKind())
} else {
assert.EqualValues(t, nil, destination.GetRoot())
}
})
}
}

View file

@ -0,0 +1,63 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"code.forgejo.org/f3/gof3/v3/path"
)
type WalkFunc func(context.Context, path.Path, NodeInterface)
type WalkOptions struct {
fun WalkFunc
}
func NewWalkOptions(fun WalkFunc) *WalkOptions {
return &WalkOptions{
fun: fun,
}
}
type ApplyFunc func(ctx context.Context, parentPath, path path.Path, node NodeInterface)
type ApplyWhere bool
const (
ApplyEachNode ApplyWhere = true
ApplyLastNode ApplyWhere = false
)
type ApplySearch bool
const (
ApplySearchByName ApplySearch = true
ApplySearchByID ApplySearch = false
)
type ApplyOptions struct {
fun ApplyFunc
where ApplyWhere
search ApplySearch
}
func NewApplyOptions(fun ApplyFunc) *ApplyOptions {
return &ApplyOptions{
fun: fun,
where: ApplyLastNode,
search: ApplySearchByID,
}
}
func (o *ApplyOptions) SetWhere(where ApplyWhere) *ApplyOptions {
o.where = where
return o
}
func (o *ApplyOptions) SetSearch(search ApplySearch) *ApplyOptions {
o.search = search
return o
}

350
tree/memory/memory.go Normal file
View file

@ -0,0 +1,350 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package memory
import (
"context"
"fmt"
"code.forgejo.org/f3/gof3/v3/f3"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/logger"
"code.forgejo.org/f3/gof3/v3/options"
options_logger "code.forgejo.org/f3/gof3/v3/options/logger"
"code.forgejo.org/f3/gof3/v3/path"
"code.forgejo.org/f3/gof3/v3/tree/generic"
)
type IDAllocatorInterface interface {
allocateID(id string) string
isNull() bool
}
type idAllocatorGenerator struct {
prefix string
lastID rune
}
func NewIDAllocatorGenerator(prefix string) IDAllocatorInterface {
return &idAllocatorGenerator{
prefix: prefix,
lastID: 'A',
}
}
func (o *idAllocatorGenerator) allocateID(string) string {
r := fmt.Sprintf("%s-%c", o.prefix, o.lastID)
o.lastID++
return r
}
func (o *idAllocatorGenerator) isNull() bool { return false }
type idAllocatorNull struct{}
func (o *idAllocatorNull) allocateID(id string) string { return id }
func (o *idAllocatorNull) isNull() bool { return true }
func NewIDAllocatorNull() IDAllocatorInterface {
return &idAllocatorNull{}
}
type memoryOptions struct {
options.Options
options_logger.OptionsLogger
IDAllocator IDAllocatorInterface
}
func NewOptions(idAllocator IDAllocatorInterface) options.Interface {
opts := &memoryOptions{}
opts.IDAllocator = idAllocator
opts.SetName("memory")
l := logger.NewLogger()
l.SetLevel(logger.Trace)
opts.SetLogger(l)
return opts
}
type memoryStorage struct {
idAllocator IDAllocatorInterface
root *memoryStorageNode
}
func newmemoryStorage(opts options.Interface) *memoryStorage {
return &memoryStorage{
idAllocator: opts.(*memoryOptions).IDAllocator,
}
}
func (o *memoryStorage) newStorageNode(id string) *memoryStorageNode {
return &memoryStorageNode{
storage: o,
f: NewFormat(o.idAllocator.allocateID(id)),
children: make(map[string]*memoryStorageNode),
}
}
func (o *memoryStorage) Find(path path.PathString) *memoryStorageNode {
p := path.Elements()
fmt.Printf("memoryStorage Find %s\n", path.Join())
current := o.root
for {
fmt.Printf(" memoryStorage Find lookup '%s'\n", current.f.GetID())
if current.f.GetID() != p[0] {
panic("")
}
p = p[1:]
if len(p) == 0 {
return current
}
if next, ok := current.children[p[0]]; ok {
current = next
} else {
return nil
}
}
}
type memoryStorageNode struct {
storage *memoryStorage
f *FormatMemory
children map[string]*memoryStorageNode
}
type treeDriver struct {
generic.NullTreeDriver
storage *memoryStorage
allocateID bool
}
func newTreeDriver(opts options.Interface) *treeDriver {
tree := &treeDriver{
storage: newmemoryStorage(opts),
allocateID: !opts.(*memoryOptions).IDAllocator.isNull(),
}
tree.Init()
return tree
}
func (o *treeDriver) AllocateID() bool { return o.allocateID }
func (o *treeDriver) Factory(ctx context.Context, kind kind.Kind) generic.NodeDriverInterface {
return &Driver{}
}
type Driver struct {
generic.NullDriver
f *FormatMemory
}
type FormatMemory struct {
f3.Common
Ref *f3.Reference
Content string
}
func NewFormat(id string) *FormatMemory {
f := &FormatMemory{}
f.Ref = f3.NewReference("")
f.Content = "????"
f.SetID(id)
return f
}
func (o *FormatMemory) GetReferences() f3.References {
references := o.Common.GetReferences()
if o.Ref.Get() != "" {
references = append(references, o.Ref)
}
return references
}
func (o *Driver) NewFormat() f3.Interface {
return &FormatMemory{}
}
func (o *Driver) ToFormat() f3.Interface {
if o == nil || o.f == nil {
return o.NewFormat()
}
return &FormatMemory{
Common: o.f.Common,
Ref: o.f.Ref,
Content: o.f.Content,
}
}
func (o *Driver) FromFormat(f f3.Interface) {
m := f.(*FormatMemory)
o.f = &FormatMemory{
Common: m.Common,
Ref: m.Ref,
Content: m.Content,
}
}
func (o *Driver) Equals(ctx context.Context, other generic.NodeInterface) bool {
switch i := other.GetDriver().(type) {
case *Driver:
return o.f.Content == i.f.Content
default:
return false
}
}
func (o *Driver) String() string {
if o.f == nil {
return "<nil>"
}
return o.f.Content
}
func (o *Driver) Get(ctx context.Context) bool {
node := o.GetNode()
o.Trace("id '%s' '%s'", node.GetID(), node.GetCurrentPath().ReadableString())
storage := o.getStorage(node, node.GetCurrentPath())
if storage != nil {
o.f = storage.f
}
node.List(ctx)
return true
}
func (o *Driver) Put(ctx context.Context) id.NodeID {
return o.upsert(ctx)
}
func (o *Driver) Patch(ctx context.Context) {
o.upsert(ctx)
}
func (o *Driver) upsert(ctx context.Context) id.NodeID {
node := o.GetNode()
path := node.GetParent().GetCurrentPath()
storageParent := o.getStorage(node, path)
i := node.GetID()
o.Trace("node id '%s'", i)
if existing, ok := storageParent.children[i.String()]; ok {
o.Trace("update %s content=%s ref=%s", node.GetCurrentPath().ReadableString(), o.f.Content, o.f.Ref)
existing.f = o.f
} else {
storageChild := storageParent.storage.newStorageNode(i.String())
storageID := storageChild.f.GetID()
storageParent.children[storageID] = storageChild
i = id.NewNodeID(storageID)
if o.f == nil {
o.f = storageChild.f
} else {
o.f.SetID(storageChild.f.GetID())
}
o.Trace("create '%s' '%s'", node.GetCurrentPath().ReadableString(), i)
}
return i
}
func (o *Driver) Delete(ctx context.Context) {
node := o.GetNode()
storage := o.getStorage(node, node.GetParent().GetCurrentPath())
if storage != nil && storage.children != nil {
id := node.GetID().String()
delete(storage.children, id)
}
}
func (o *Driver) getStorage(node generic.NodeInterface, path path.Path) *memoryStorageNode {
storage := node.GetTree().GetDriver().(*treeDriver).storage
return storage.Find(path.PathString())
}
func (o *Driver) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
node := o.GetNode()
o.Trace("'%s'", node.GetCurrentPath().ReadableString())
storage := o.getStorage(node, node.GetCurrentPath())
children := generic.NewChildrenSlice(0)
if storage != nil {
for i := range storage.children {
node := node.CreateChild(context.Background())
childID := id.NewNodeID(i)
node.SetID(childID)
children = append(children, node)
}
}
return children
}
func (o *Driver) GetIDFromName(ctx context.Context, content string) id.NodeID {
node := o.GetNode()
o.Trace("'%s'", node.GetCurrentPath().ReadableString())
storage := o.getStorage(node, node.GetCurrentPath())
if storage != nil {
for i, child := range storage.children {
if child.f.Content == content {
o.Trace("found '%s'", i)
return id.NewNodeID(i)
}
}
}
return id.NilID
}
type treeMemory struct {
generic.Tree
}
func newTreeMemory(ctx context.Context, opts options.Interface) generic.TreeInterface {
t := &treeMemory{}
t.Init(t, opts)
t.GetLogger().SetLevel(logger.Trace)
t.Register(kind.KindNil, func(ctx context.Context, kind kind.Kind) generic.NodeInterface {
return generic.NewNode()
})
treeDriver := newTreeDriver(opts)
t.SetDriver(treeDriver)
f := NewFormat("")
f.Content = "ROOT"
storage := treeDriver.storage
storage.root = &memoryStorageNode{
storage: storage,
f: f,
children: make(map[string]*memoryStorageNode),
}
root := t.Factory(ctx, kind.KindRoot)
root.FromFormat(f)
t.SetRoot(root)
return t
}
func SetContent(node generic.NodeInterface, content string) {
f := node.GetDriver().ToFormat().(*FormatMemory)
f.Content = content
node.FromFormat(f)
}
func GetContent(node generic.NodeInterface) string {
return node.GetDriver().ToFormat().(*FormatMemory).Content
}
func SetRef(node generic.NodeInterface, ref string) {
f := node.GetDriver().ToFormat().(*FormatMemory)
f.Ref = f3.NewReference(ref)
node.FromFormat(f)
}
func GetRef(node generic.NodeInterface) string {
return node.GetDriver().ToFormat().(*FormatMemory).Ref.Get()
}
func init() {
generic.RegisterFactory("memory", newTreeMemory)
}

397
tree/tests/f3/creator.go Normal file
View file

@ -0,0 +1,397 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"context"
"crypto/sha256"
"fmt"
"io"
"strings"
"time"
"code.forgejo.org/f3/gof3/v3/f3"
helpers_repository "code.forgejo.org/f3/gof3/v3/forges/helpers/repository"
tests_repository "code.forgejo.org/f3/gof3/v3/forges/helpers/tests/repository"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/logger"
"code.forgejo.org/f3/gof3/v3/path"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
"github.com/stretchr/testify/require"
)
var RootUser *f3.User = &f3.User{
UserName: "root",
}
type Creator struct {
logger logger.Interface
t TestingT
d string
name string
serial int
}
func now() time.Time {
return time.Now().Truncate(time.Second)
}
func tick(now *time.Time) time.Time {
*now = now.Add(1 * time.Minute)
return *now
}
func NewCreator(t TestingT, name string, logger logger.Interface) *Creator {
return &Creator{
t: t,
d: t.TempDir(),
logger: logger,
name: name,
}
}
func (f *Creator) randomString(prefix string) string {
f.serial++
return fmt.Sprintf("%s%s%015d", prefix, f.name, f.serial)
}
func (f *Creator) GetDirectory() string {
return f.d
}
func (f *Creator) Generate(k kind.Kind, parent path.Path) f3.Interface {
switch k {
case f3_tree.KindForge:
return f.GenerateForge()
case f3_tree.KindUser:
return f.GenerateUser()
case f3_tree.KindOrganization:
return f.GenerateOrganization()
case f3_tree.KindProject:
return f.GenerateProject()
case f3_tree.KindIssue:
return f.GenerateIssue(parent)
case f3_tree.KindMilestone:
return f.GenerateMilestone()
case f3_tree.KindTopic:
return f.GenerateTopic()
case f3_tree.KindReaction:
return f.GenerateReaction(parent)
case f3_tree.KindLabel:
return f.GenerateLabel()
case f3_tree.KindComment:
return f.GenerateComment(parent)
case f3_tree.KindRepository:
return f.GenerateRepository(f3.RepositoryNameDefault)
case f3_tree.KindRelease:
return f.GenerateRelease(parent)
case f3_tree.KindAsset:
return f.GenerateAsset(parent)
case f3_tree.KindPullRequest:
return f.GeneratePullRequest(parent)
case f3_tree.KindReview:
return f.GenerateReview(parent)
case f3_tree.KindReviewComment:
return f.GenerateReviewComment(parent)
default:
panic(fmt.Errorf("not implemented %s", k))
}
}
func (f *Creator) GenerateForge() *f3.Forge {
return &f3.Forge{
Common: f3.NewCommon("forge"),
}
}
func (f *Creator) GenerateUser() *f3.User {
username := f.randomString("user")
return &f3.User{
Name: username + " Doe",
UserName: username,
Email: username + "@example.com",
Password: "Wrobyak4",
}
}
func (f *Creator) GenerateOrganization() *f3.Organization {
orgname := f.randomString("org")
return &f3.Organization{
FullName: orgname + " Lambda",
Name: orgname,
}
}
func (f *Creator) GenerateProject() *f3.Project {
projectname := f.randomString("project")
return &f3.Project{
Name: projectname,
IsPrivate: false,
IsMirror: false,
Description: "project description",
DefaultBranch: "master",
}
}
func (f *Creator) GenerateForkedProject(parent path.Path, forked string) *f3.Project {
project := f.GenerateProject()
project.Forked = f3.NewReference(forked)
return project
}
func (f *Creator) GenerateRepository(name string) *f3.Repository {
repository := &f3.Repository{
Name: name,
}
p := f.t.TempDir()
helper := tests_repository.NewTestHelper(f.t, p, nil)
helper.CreateRepositoryContent("").PushMirror()
repository.FetchFunc = func(ctx context.Context, destination string, internalRefs []string) {
f.logger.Debug("%s %s", p, destination)
helpers_repository.GitMirror(context.Background(), nil, p, destination, internalRefs)
}
return repository
}
func (f *Creator) GenerateReaction(parent path.Path) *f3.Reaction {
user := f3_tree.GetFirstFormat[*f3.User](parent.Last().(generic.NodeInterface))
return &f3.Reaction{
UserID: f3_tree.NewUserReference(user.GetID()),
Content: "heart",
}
}
func (f *Creator) GenerateMilestone() *f3.Milestone {
now := now()
created := tick(&now)
updated := tick(&now)
deadline := tick(&now)
return &f3.Milestone{
Title: "milestone1",
Description: "milestone1 description",
Deadline: &deadline,
Created: created,
Updated: &updated,
Closed: nil,
State: f3.MilestoneStateOpen,
}
}
func (f *Creator) GeneratePullRequest(parent path.Path) *f3.PullRequest {
user := f3_tree.GetFirstFormat[*f3.User](parent.Last().(generic.NodeInterface))
projectNode := f3_tree.GetFirstNodeKind(parent.Last().(generic.NodeInterface), f3_tree.KindProject)
repositoryNode := projectNode.Find(generic.NewPathFromString("repositories/vcs"))
repositoryNode.Get(context.Background())
repositoryHelper := tests_repository.NewTestHelper(f.t, "", repositoryNode)
mainRef := "master"
mainSha := repositoryHelper.GetRepositorySha(mainRef)
featureRef := "feature"
repositoryHelper.InternalBranchRepositoryFeature(featureRef, "feature content")
featureSha := repositoryHelper.GetRepositorySha(featureRef)
f.logger.Debug("master %s at main %s feature %s", repositoryHelper.GetBare(), mainSha, featureSha)
repositoryHelper.PushMirror()
now := now()
prCreated := tick(&now)
prUpdated := tick(&now)
return &f3.PullRequest{
PosterID: f3_tree.NewUserReference(user.GetID()),
Title: "pr title",
Content: "pr content",
State: f3.PullRequestStateOpen,
IsLocked: false,
Created: prCreated,
Updated: prUpdated,
Closed: nil,
Merged: false,
MergedTime: nil,
MergeCommitSHA: "",
Head: f3.PullRequestBranch{
Ref: featureRef,
SHA: featureSha,
Repository: f3_tree.NewPullRequestSameRepositoryReference(),
},
Base: f3.PullRequestBranch{
Ref: mainRef,
SHA: mainSha,
Repository: f3_tree.NewPullRequestSameRepositoryReference(),
},
}
}
func (f *Creator) GenerateLabel() *f3.Label {
name := f.randomString("label")
return &f3.Label{
Name: name,
Color: "ffffff",
Description: name + " description",
}
}
func (f *Creator) GenerateTopic() *f3.Topic {
return &f3.Topic{
Name: "topic1",
}
}
func (f *Creator) GenerateIssue(parent path.Path) *f3.Issue {
user := f3_tree.GetFirstFormat[*f3.User](parent.Last().(generic.NodeInterface))
projectNode := f3_tree.GetFirstNodeKind(parent.Last().(generic.NodeInterface), f3_tree.KindProject)
labelsNode := projectNode.Find(generic.NewPathFromString("labels"))
require.NotEqualValues(f.t, generic.NilNode, labelsNode)
labels := labelsNode.GetChildren()
require.NotEmpty(f.t, labels)
firstLabel := labels[0]
milestonesNode := projectNode.Find(generic.NewPathFromString("milestones"))
require.NotEqualValues(f.t, generic.NilNode, milestonesNode)
milestones := milestonesNode.GetChildren()
require.NotEmpty(f.t, milestones)
firstMilestone := milestones[0]
now := now()
updated := tick(&now)
closed := tick(&now)
created := tick(&now)
return &f3.Issue{
PosterID: f3_tree.NewUserReference(user.GetID()),
Assignees: []*f3.Reference{f3_tree.NewUserReference(user.GetID())},
Labels: []*f3.Reference{f3_tree.NewIssueLabelReference(firstLabel.GetID())},
Milestone: f3_tree.NewIssueMilestoneReference(firstMilestone.GetID()),
Title: "title",
Content: "content",
State: f3.IssueStateOpen,
IsLocked: false,
Created: created,
Updated: updated,
Closed: &closed,
}
}
func (f *Creator) GenerateComment(parent path.Path) *f3.Comment {
user := f3_tree.GetFirstFormat[*f3.User](parent.Last().(generic.NodeInterface))
now := now()
commentCreated := tick(&now)
commentUpdated := tick(&now)
posterID := f3_tree.NewUserReference(user.GetID())
return &f3.Comment{
PosterID: posterID,
Created: commentCreated,
Updated: commentUpdated,
Content: "comment content",
}
}
func (f *Creator) GenerateRelease(parent path.Path) *f3.Release {
user := f3_tree.GetFirstFormat[*f3.User](parent.Last().(generic.NodeInterface))
project := f3_tree.GetFirstNodeKind(parent.Last().(generic.NodeInterface), f3_tree.KindProject)
repository := project.Find(generic.NewPathFromString("repositories/vcs"))
repository.Get(context.Background())
repositoryHelper := tests_repository.NewTestHelper(f.t, "", repository)
now := now()
releaseCreated := tick(&now)
tag := "releasetagv12"
repositoryHelper.CreateRepositoryTag(tag, "master")
sha := repositoryHelper.GetRepositorySha("master")
repositoryHelper.PushMirror()
return &f3.Release{
TagName: tag,
TargetCommitish: sha,
Name: "v12 name",
Body: "v12 body",
Draft: false,
Prerelease: false,
PublisherID: f3_tree.NewUserReference(user.GetID()),
Created: releaseCreated,
}
}
func (f *Creator) GenerateAsset(parent path.Path) *f3.ReleaseAsset {
name := "assetname"
content := "assetcontent"
downloadURL := "downloadURL"
now := now()
assetCreated := tick(&now)
size := len(content)
downloadCount := int64(10)
sha256 := fmt.Sprintf("%x", sha256.Sum256([]byte(content)))
return &f3.ReleaseAsset{
Name: name,
Size: int64(size),
DownloadCount: downloadCount,
Created: assetCreated,
SHA256: sha256,
DownloadURL: downloadURL,
DownloadFunc: func() io.ReadCloser {
rc := io.NopCloser(strings.NewReader(content))
return rc
},
}
}
func (f *Creator) GenerateReview(parent path.Path) *f3.Review {
user := f3_tree.GetFirstFormat[*f3.User](parent.Last().(generic.NodeInterface))
projectNode := f3_tree.GetFirstNodeKind(parent.Last().(generic.NodeInterface), f3_tree.KindProject)
repositoryNode := projectNode.Find(generic.NewPathFromString("repositories/vcs"))
repositoryNode.Get(context.Background())
repositoryHelper := tests_repository.NewTestHelper(f.t, "", repositoryNode)
now := now()
reviewCreated := tick(&now)
featureSha := repositoryHelper.GetRepositorySha("feature")
return &f3.Review{
ReviewerID: f3_tree.NewUserReference(user.GetID()),
Official: true,
CommitID: featureSha,
Content: "the review content",
CreatedAt: reviewCreated,
State: f3.ReviewStateCommented,
}
}
func (f *Creator) GenerateReviewComment(parent path.Path) *f3.ReviewComment {
user := f3_tree.GetFirstFormat[*f3.User](parent.Last().(generic.NodeInterface))
projectNode := f3_tree.GetFirstNodeKind(parent.Last().(generic.NodeInterface), f3_tree.KindProject)
repositoryNode := projectNode.Find(generic.NewPathFromString("repositories/vcs"))
repositoryNode.Get(context.Background())
repositoryHelper := tests_repository.NewTestHelper(f.t, "", repositoryNode)
now := now()
commentCreated := tick(&now)
commentUpdated := tick(&now)
featureSha := repositoryHelper.GetRepositorySha("feature")
return &f3.ReviewComment{
Content: "comment content",
TreePath: "README.md",
DiffHunk: "@@ -108,7 +108,6 @@",
Line: 1,
CommitID: featureSha,
PosterID: f3_tree.NewUserReference(user.GetID()),
CreatedAt: commentCreated,
UpdatedAt: commentUpdated,
}
}

260
tree/tests/f3/f3_test.go Normal file
View file

@ -0,0 +1,260 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"context"
"crypto/sha256"
"fmt"
"io"
"path/filepath"
"sort"
"strings"
"testing"
"code.forgejo.org/f3/gof3/v3/f3"
filesystem_options "code.forgejo.org/f3/gof3/v3/forges/filesystem/options"
tests_repository "code.forgejo.org/f3/gof3/v3/forges/helpers/tests/repository"
"code.forgejo.org/f3/gof3/v3/options"
"code.forgejo.org/f3/gof3/v3/path"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
tests_forge "code.forgejo.org/f3/gof3/v3/tree/tests/f3/forge"
"github.com/stretchr/testify/assert"
)
func TestF3Mirror(t *testing.T) {
ctx := context.Background()
for _, factory := range tests_forge.GetFactories() {
testForge := factory()
t.Run(testForge.GetName(), func(t *testing.T) {
// testCase.options will t.Skip if the forge instance is not up
forgeWriteOptions := testForge.NewOptions(t)
forgeReadOptions := testForge.NewOptions(t)
forgeReadOptions.(options.URLInterface).SetURL(forgeWriteOptions.(options.URLInterface).GetURL())
fixtureTree := generic.GetFactory("f3")(ctx, tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t))
fixtureTree.Trace("======= build fixture")
TreeBuildPartial(t, "F3Mirror"+testForge.GetName(), testForge.GetKindExceptions(), forgeWriteOptions, fixtureTree)
fixtureTree.Trace("======= mirror fixture to forge")
forgeWriteTree := generic.GetFactory("f3")(ctx, forgeWriteOptions)
generic.TreeMirror(ctx, fixtureTree, forgeWriteTree, generic.NewPathFromString(""), generic.NewMirrorOptions())
paths := []string{""}
if testForge.GetName() != filesystem_options.Name {
paths = []string{"/forge/users/10111", "/forge/users/20222"}
}
pathPairs := make([][2]path.Path, 0, 5)
for _, p := range paths {
p := generic.NewPathFromString(p)
pathPairs = append(pathPairs, [2]path.Path{p, generic.TreePathRemap(ctx, fixtureTree, p, forgeWriteTree)})
}
fixtureTree.Trace("======= read from forge")
forgeReadTree := generic.GetFactory("f3")(ctx, forgeReadOptions)
forgeReadTree.WalkAndGet(ctx, generic.NewWalkOptions(nil))
fixtureTree.Trace("======= mirror forge to filesystem")
verificationTree := generic.GetFactory("f3")(ctx, tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t))
for _, pathPair := range pathPairs {
generic.TreeMirror(ctx, forgeReadTree, verificationTree, pathPair[1], generic.NewMirrorOptions())
}
fixtureTree.Trace("======= compare fixture with forge mirrored to filesystem")
for _, pathPair := range pathPairs {
assert.True(t, generic.TreeCompare(ctx, fixtureTree, pathPair[0], verificationTree, pathPair[1]))
}
TreeDelete(t, testForge.GetNonTestUsers(), forgeWriteOptions, forgeWriteTree)
})
}
}
func TestF3Tree(t *testing.T) {
ctx := context.Background()
verify := func(tree generic.TreeInterface, expected []string) {
collected := make([]string, 0, 10)
collect := func(ctx context.Context, path path.Path, node generic.NodeInterface) {
path = path.Append(node)
collected = append(collected, path.String())
}
tree.WalkAndGet(ctx, generic.NewWalkOptions(collect))
sort.Strings(expected)
sort.Strings(collected)
assert.EqualValues(t, expected, collected)
}
for _, testCase := range []struct {
name string
build func(tree generic.TreeInterface)
operations func(tree generic.TreeInterface)
expected []string
}{
{
name: "full tree",
build: func(tree generic.TreeInterface) { TreeBuild(t, "F3", nil, tree) },
operations: func(tree generic.TreeInterface) {},
expected: []string{
"",
"/forge",
"/forge/organizations",
"/forge/organizations/3330001",
"/forge/organizations/3330001/projects",
"/forge/topics",
"/forge/topics/14411441",
"/forge/users",
"/forge/users/10111",
"/forge/users/10111/projects",
"/forge/users/10111/projects/74823",
"/forge/users/10111/projects/74823/issues",
"/forge/users/10111/projects/74823/issues/1234567",
"/forge/users/10111/projects/74823/issues/1234567/comments",
"/forge/users/10111/projects/74823/issues/1234567/comments/1111999",
"/forge/users/10111/projects/74823/issues/1234567/comments/1111999/reactions",
"/forge/users/10111/projects/74823/issues/1234567/reactions",
"/forge/users/10111/projects/74823/issues/1234567/reactions/1212",
"/forge/users/10111/projects/74823/labels",
"/forge/users/10111/projects/74823/labels/7777",
"/forge/users/10111/projects/74823/labels/99999",
"/forge/users/10111/projects/74823/milestones",
"/forge/users/10111/projects/74823/milestones/7888",
"/forge/users/10111/projects/74823/pull_requests",
"/forge/users/10111/projects/74823/pull_requests/2222",
"/forge/users/10111/projects/74823/pull_requests/2222/comments",
"/forge/users/10111/projects/74823/pull_requests/2222/reactions",
"/forge/users/10111/projects/74823/pull_requests/2222/reviews",
"/forge/users/10111/projects/74823/pull_requests/2222/reviews/4593",
"/forge/users/10111/projects/74823/pull_requests/2222/reviews/4593/reactions",
"/forge/users/10111/projects/74823/pull_requests/2222/reviews/4593/reviewcomments",
"/forge/users/10111/projects/74823/pull_requests/2222/reviews/4593/reviewcomments/9876543",
"/forge/users/10111/projects/74823/releases",
"/forge/users/10111/projects/74823/releases/123",
"/forge/users/10111/projects/74823/releases/123/assets",
"/forge/users/10111/projects/74823/releases/123/assets/585858",
"/forge/users/10111/projects/74823/repositories",
"/forge/users/10111/projects/74823/repositories/vcs",
"/forge/users/20222",
"/forge/users/20222/projects",
"/forge/users/20222/projects/99099",
"/forge/users/20222/projects/99099/issues",
"/forge/users/20222/projects/99099/labels",
"/forge/users/20222/projects/99099/milestones",
"/forge/users/20222/projects/99099/pull_requests",
"/forge/users/20222/projects/99099/releases",
"/forge/users/20222/projects/99099/repositories",
"/forge/users/20222/projects/99099/repositories/vcs",
},
},
} {
t.Run(testCase.name, func(t *testing.T) {
tree := generic.GetFactory("f3")(ctx, tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t))
tree.Trace("======= BUILD")
testCase.build(tree)
tree.Trace("======= OPERATIONS")
testCase.operations(tree)
tree.Trace("======= VERIFY")
verify(tree, testCase.expected)
tree.Clear(ctx)
tree.Trace("======= VERIFY STEP 2")
verify(tree, testCase.expected)
tree.Trace("======= COMPARE")
otherTree := generic.GetFactory("f3")(ctx, tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t))
otherTree.GetOptions().(options.URLInterface).SetURL(tree.GetOptions().(options.URLInterface).GetURL())
otherTree.WalkAndGet(ctx, generic.NewWalkOptions(nil))
assert.True(t, generic.TreeCompare(ctx, tree, generic.NewPathFromString(""), otherTree, generic.NewPathFromString("")))
})
}
}
func TestF3Repository(t *testing.T) {
ctx := context.Background()
verify := func(tree generic.TreeInterface, name string) {
repositoryPath := generic.NewPathFromString(filepath.Join("/forge/users/10111/projects/74823/repositories", name))
found := false
tree.Apply(ctx, repositoryPath, generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
node.Get(ctx)
node.List(ctx)
if node.GetKind() != f3_tree.KindRepository {
return
}
helper := tests_repository.NewTestHelper(t, "", node)
helper.AssertRepositoryFileExists("README.md")
if name == f3.RepositoryNameDefault {
helper.AssertRepositoryTagExists("releasetagv12")
helper.AssertRepositoryBranchExists("feature")
}
found = true
}).SetWhere(generic.ApplyEachNode))
assert.True(t, found)
}
for _, name := range []string{f3.RepositoryNameDefault} {
t.Run(name, func(t *testing.T) {
tree := generic.GetFactory("f3")(ctx, tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t))
tree.Trace("======= BUILD")
TreeBuild(t, "F3", nil, tree)
tree.Trace("======= VERIFY")
verify(tree, name)
tree.Clear(ctx)
tree.Trace("======= VERIFY STEP 2")
verify(tree, name)
})
}
}
func TestF3Asset(t *testing.T) {
ctx := context.Background()
content := "OTHER CONTENT"
expectedSHA256 := fmt.Sprintf("%x", sha256.Sum256([]byte(content)))
opts := tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t)
{
tree := generic.GetFactory("f3")(ctx, opts)
TreeBuild(t, "F3", nil, tree)
assetPath := generic.NewPathFromString("/forge/users/10111/projects/74823/releases/123/assets/585858")
asset := tree.Find(assetPath)
assert.False(t, asset.GetIsNil())
assetFormat := asset.ToFormat().(*f3.ReleaseAsset)
assetFormat.DownloadFunc = func() io.ReadCloser {
rc := io.NopCloser(strings.NewReader(content))
return rc
}
asset.FromFormat(assetFormat)
asset.Upsert(ctx)
assetFormat = asset.ToFormat().(*f3.ReleaseAsset)
assert.EqualValues(t, expectedSHA256, assetFormat.SHA256)
}
{
tree := generic.GetFactory("f3")(ctx, opts)
tree.WalkAndGet(ctx, generic.NewWalkOptions(nil))
assetPath := generic.NewPathFromString("/forge/users/10111/projects/74823/releases/123/assets/585858")
asset := tree.Find(assetPath)
assert.False(t, asset.GetIsNil())
assetFormat := asset.ToFormat().(*f3.ReleaseAsset)
assert.EqualValues(t, expectedSHA256, assetFormat.SHA256)
}
}

View file

@ -0,0 +1,105 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"context"
"path/filepath"
"testing"
filesystem_options "code.forgejo.org/f3/gof3/v3/forges/filesystem/options"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/options"
"code.forgejo.org/f3/gof3/v3/path"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
tests_forge "code.forgejo.org/f3/gof3/v3/tree/tests/f3/forge"
"github.com/stretchr/testify/assert"
)
func TestF3FilesystemMappedID(t *testing.T) {
ctx := context.Background()
//
// aTree only has /forge/user/10111
//
aTree := generic.GetFactory("f3")(ctx, tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t))
aDir := aTree.GetOptions().(options.URLInterface).GetURL()
creator := NewCreator(t, "F3", aTree.GetLogger())
aF3Tree := aTree.(f3_tree.TreeInterface)
userID := "10111"
aF3Tree.CreateChild(ctx, "/", func(parent path.Path, forge generic.NodeInterface) {
forge.FromFormat(creator.GenerateForge())
})
aF3Tree.CreateChild(ctx, "/forge/users", func(parent path.Path, user generic.NodeInterface) {
user.FromFormat(GeneratorSetID(creator.GenerateUser(), userID))
})
//
// bTree mirrors aTree exactly
//
rootPath := generic.NewPathFromString("")
bTree := generic.GetFactory("f3")(ctx, tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t))
generic.TreeMirror(ctx, aTree, bTree, rootPath, generic.NewMirrorOptions())
assert.True(t, generic.TreeCompare(ctx, aTree, rootPath, bTree, rootPath))
//
// aTree maps user id 10111 to 10111.mapped
//
userPath := generic.NewPathFromString(filepath.Join("/forge/users", userID))
userMappedID := id.NewNodeID("10111.mapped")
assert.True(t, aTree.Apply(ctx, userPath, generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
node.SetMappedID(userMappedID)
node.Upsert(ctx)
})))
aTree = generic.GetFactory("f3")(ctx, tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t))
aTree.GetOptions().(options.URLInterface).SetURL(aDir)
aTree.WalkAndGet(ctx, generic.NewWalkOptions(nil))
assert.NotEqualValues(t, generic.NilNode, aTree.Find(userPath))
//
// cTree mirrors aTree with user id 10111 remapped to 10111.mapped
//
cTree := generic.GetFactory("f3")(ctx, tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t))
cDir := cTree.GetOptions().(options.URLInterface).GetURL()
generic.TreeMirror(ctx, aTree, cTree, rootPath, generic.NewMirrorOptions())
userMappedPath := generic.NewPathFromString(filepath.Join("/forge/users", userMappedID.String()))
assert.NotEqualValues(t, generic.NilNode, cTree.Find(userMappedPath))
assert.EqualValues(t, generic.NilNode, cTree.Find(userPath))
//
// reset cTree and read from the filesystem
//
cTree = generic.GetFactory("f3")(ctx, tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t))
cTree.GetOptions().(options.URLInterface).SetURL(cDir)
cTree.WalkAndGet(ctx, generic.NewWalkOptions(nil))
assert.NotEqualValues(t, generic.NilNode, cTree.Find(userMappedPath))
assert.EqualValues(t, generic.NilNode, cTree.Find(userPath))
//
// delete aTree user
//
deleted := aTree.Find(userPath).Delete(ctx)
assert.EqualValues(t, userMappedID, deleted.GetMappedID())
assert.EqualValues(t, generic.NilNode, cTree.Find(userPath))
}

224
tree/tests/f3/fixture.go Normal file
View file

@ -0,0 +1,224 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"context"
"path/filepath"
"slices"
"sort"
"testing"
"code.forgejo.org/f3/gof3/v3/f3"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/options"
"code.forgejo.org/f3/gof3/v3/path"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
)
var KindToFixturePath = map[kind.Kind]string{
f3_tree.KindTopics: "/forge/topics",
f3_tree.KindTopic: "/forge/topics/14411441",
f3_tree.KindUsers: "/forge/users",
f3_tree.KindUser: "/forge/users/10111",
f3_tree.KindProjects: "/forge/users/10111/projects",
f3_tree.KindProject: "/forge/users/10111/projects/74823",
f3_tree.KindLabels: "/forge/users/10111/projects/74823/labels",
f3_tree.KindLabel: "/forge/users/10111/projects/74823/labels/7777",
f3_tree.KindIssues: "/forge/users/10111/projects/74823/issues",
f3_tree.KindIssue: "/forge/users/10111/projects/74823/issues/1234567",
f3_tree.KindPullRequests: "/forge/users/10111/projects/74823/pull_requests",
f3_tree.KindPullRequest: "/forge/users/10111/projects/74823/pull_requests/2222",
f3_tree.KindReviews: "/forge/users/10111/projects/74823/pull_requests/2222/reviews",
f3_tree.KindReview: "/forge/users/10111/projects/74823/pull_requests/2222/reviews/4593",
f3_tree.KindReviewComments: "/forge/users/10111/projects/74823/pull_requests/2222/reviews/4593/reviewcomments",
f3_tree.KindReviewComment: "/forge/users/10111/projects/74823/pull_requests/2222/reviews/4593/reviewcomments/9876543",
f3_tree.KindMilestones: "/forge/users/10111/projects/74823/milestones",
f3_tree.KindMilestone: "/forge/users/10111/projects/74823/milestones/7888",
f3_tree.KindReactions: "/forge/users/10111/projects/74823/issues/1234567/reactions",
f3_tree.KindReaction: "/forge/users/10111/projects/74823/issues/1234567/reactions/1212",
f3_tree.KindComments: "/forge/users/10111/projects/74823/issues/1234567/comments",
f3_tree.KindComment: "/forge/users/10111/projects/74823/issues/1234567/comments/1111999",
f3_tree.KindRepositories: "/forge/users/10111/projects/74823/repositories",
f3_tree.KindRepository: "/forge/users/10111/projects/74823/repositories/vcs",
f3_tree.KindReleases: "/forge/users/10111/projects/74823/releases",
f3_tree.KindRelease: "/forge/users/10111/projects/74823/releases/123",
f3_tree.KindAssets: "/forge/users/10111/projects/74823/releases/123/assets",
f3_tree.KindAsset: "/forge/users/10111/projects/74823/releases/123/assets/585858",
f3_tree.KindOrganizations: "/forge/organizations",
f3_tree.KindOrganization: "/forge/organizations/3330001",
}
var KindWithFixturePath = SetKindWithFixturePath()
func SetKindWithFixturePath() []kind.Kind {
l := make([]kind.Kind, 0, len(KindToFixturePath))
for kind := range KindToFixturePath {
l = append(l, kind)
}
sort.Slice(l, func(i, j int) bool { return string(l[i]) < string(l[j]) })
return l
}
func TreeBuild(t *testing.T, name string, opts options.Interface, tree generic.TreeInterface) {
TreeBuildPartial(t, name, []kind.Kind{}, opts, tree)
}
func TreeBuildPartial(t *testing.T, name string, exceptions []kind.Kind, opts options.Interface, tree generic.TreeInterface) {
ctx := context.Background()
creator := NewCreator(t, name, tree.GetLogger())
f3Tree := tree.(f3_tree.TreeInterface)
url := "<unknown>"
if urlInterface, ok := opts.(options.URLInterface); ok {
url = urlInterface.GetURL()
}
f3Tree.CreateChild(ctx, "/", func(parent path.Path, forge generic.NodeInterface) {
f := creator.GenerateForge()
f.URL = url
forge.FromFormat(f)
})
if slices.Contains(exceptions, f3_tree.KindUsers) {
return
}
f3Tree.CreateChild(ctx, KindToFixturePath[f3_tree.KindUsers], func(parent path.Path, user generic.NodeInterface) {
user.FromFormat(GeneratorSetID(creator.GenerateUser(), "10111"))
})
if slices.Contains(exceptions, f3_tree.KindProjects) {
return
}
f3Tree.CreateChild(ctx, KindToFixturePath[f3_tree.KindProjects], func(parent path.Path, project generic.NodeInterface) {
project.FromFormat(GeneratorSetID(creator.GenerateProject(), "74823"))
})
if slices.Contains(exceptions, f3_tree.KindRepositories) {
return
}
f3Tree.CreateChild(ctx, KindToFixturePath[f3_tree.KindRepositories], func(parent path.Path, repository generic.NodeInterface) {
repository.FromFormat(GeneratorSetID(creator.GenerateRepository(f3.RepositoryNameDefault), f3.RepositoryNameDefault))
})
if !slices.Contains(exceptions, f3_tree.KindReleases) {
f3Tree.CreateChild(ctx, KindToFixturePath[f3_tree.KindReleases], func(parent path.Path, release generic.NodeInterface) {
release.FromFormat(GeneratorSetID(creator.GenerateRelease(parent), "123"))
})
if !slices.Contains(exceptions, f3_tree.KindAssets) {
f3Tree.CreateChild(ctx, KindToFixturePath[f3_tree.KindAssets], func(parent path.Path, asset generic.NodeInterface) {
asset.FromFormat(GeneratorSetID(creator.GenerateAsset(parent), "585858"))
})
}
}
reviewerID := "20222"
{
userID := "20222"
f3Tree.CreateChild(ctx, "/forge/users", func(parent path.Path, user generic.NodeInterface) {
user.FromFormat(GeneratorSetID(creator.GenerateUser(), userID))
})
projectID := "99099"
f3Tree.CreateChild(ctx, filepath.Join("/forge/users", userID, "projects"), func(parent path.Path, user generic.NodeInterface) {
user.FromFormat(GeneratorSetID(creator.GenerateForkedProject(parent, KindToFixturePath[f3_tree.KindProject]), projectID))
})
f3Tree.CreateChild(ctx, filepath.Join("/forge/users", userID, "projects", projectID, "repositories"), func(parent path.Path, repository generic.NodeInterface) {
repository.FromFormat(GeneratorSetID(creator.GenerateRepository(f3.RepositoryNameDefault), f3.RepositoryNameDefault))
})
}
if !slices.Contains(exceptions, f3_tree.KindPullRequests) {
f3Tree.CreateChild(ctx, KindToFixturePath[f3_tree.KindPullRequests], func(parent path.Path, pullRequest generic.NodeInterface) {
pullRequest.FromFormat(GeneratorSetID(creator.GeneratePullRequest(parent), "2222"))
})
if !slices.Contains(exceptions, f3_tree.KindReviews) {
f3Tree.CreateChild(ctx, KindToFixturePath[f3_tree.KindReviews], func(parent path.Path, review generic.NodeInterface) {
reviewFormat := creator.GenerateReview(parent)
GeneratorSetID(reviewFormat, "4593")
reviewFormat.ReviewerID = f3_tree.NewUserReference(reviewerID)
review.FromFormat(reviewFormat)
})
if !slices.Contains(exceptions, f3_tree.KindReviewComments) {
f3Tree.CreateChild(ctx, KindToFixturePath[f3_tree.KindReviewComments], func(parent path.Path, reviewComment generic.NodeInterface) {
reviewCommentFormat := creator.GenerateReviewComment(parent)
GeneratorSetID(reviewCommentFormat, "9876543")
reviewCommentFormat.PosterID = f3_tree.NewUserReference(reviewerID)
reviewComment.FromFormat(reviewCommentFormat)
})
}
}
}
if !slices.Contains(exceptions, f3_tree.KindLabels) {
f3Tree.CreateChild(ctx, KindToFixturePath[f3_tree.KindLabels], func(parent path.Path, label generic.NodeInterface) {
label.FromFormat(GeneratorSetID(creator.GenerateLabel(), "99999"))
})
f3Tree.CreateChild(ctx, KindToFixturePath[f3_tree.KindLabels], func(parent path.Path, label generic.NodeInterface) {
label.FromFormat(GeneratorSetID(creator.GenerateLabel(), "7777"))
})
}
if !slices.Contains(exceptions, f3_tree.KindMilestones) {
f3Tree.CreateChild(ctx, KindToFixturePath[f3_tree.KindMilestones], func(parent path.Path, milestone generic.NodeInterface) {
milestone.FromFormat(GeneratorSetID(creator.GenerateMilestone(), "7888"))
})
}
if !slices.Contains(exceptions, f3_tree.KindIssues) {
f3Tree.CreateChild(ctx, KindToFixturePath[f3_tree.KindIssues], func(parent path.Path, issue generic.NodeInterface) {
issue.FromFormat(GeneratorSetID(creator.GenerateIssue(parent), "1234567"))
})
if !slices.Contains(exceptions, f3_tree.KindComments) {
f3Tree.CreateChild(ctx, KindToFixturePath[f3_tree.KindComments], func(parent path.Path, comment generic.NodeInterface) {
comment.FromFormat(GeneratorSetID(creator.GenerateComment(parent), "1111999"))
})
if !slices.Contains(exceptions, f3_tree.KindReactions) {
f3Tree.CreateChild(ctx, KindToFixturePath[f3_tree.KindReactions], func(parent path.Path, reaction generic.NodeInterface) {
reaction.FromFormat(GeneratorSetID(creator.GenerateReaction(parent), "1212"))
})
}
}
}
if !slices.Contains(exceptions, f3_tree.KindOrganizations) {
f3Tree.CreateChild(ctx, KindToFixturePath[f3_tree.KindOrganizations], func(parent path.Path, organization generic.NodeInterface) {
organization.FromFormat(GeneratorSetID(creator.GenerateOrganization(), "3330001"))
})
}
if !slices.Contains(exceptions, f3_tree.KindTopics) {
f3Tree.CreateChild(ctx, KindToFixturePath[f3_tree.KindTopics], func(parent path.Path, topic generic.NodeInterface) {
topic.FromFormat(GeneratorSetID(creator.GenerateTopic(), "14411441"))
})
}
}
func TreeDelete(t *testing.T, nonTestUsers []string, options options.Interface, tree generic.TreeInterface) {
ctx := context.Background()
for _, owners := range []path.Path{f3_tree.OrganizationsPath, f3_tree.UsersPath} {
for _, owner := range tree.Find(owners).List(ctx) {
if user, ok := owner.ToFormat().(*f3.User); ok {
if slices.Contains(nonTestUsers, user.UserName) {
continue
}
}
for _, project := range owner.Find(generic.NewPathFromString(f3_tree.KindProjects)).List(ctx) {
project.Delete(ctx)
}
owner.Delete(ctx)
}
}
}

View file

@ -0,0 +1,30 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package forge
import (
"os"
"code.forgejo.org/f3/gof3/v3/kind"
)
const (
ComplianceNameForkedPullRequest = "forked_pull_request"
)
type Base struct {
name string
}
func (o *Base) GetName() string { return o.name }
func (o *Base) SetName(name string) { o.name = name }
func (o *Base) GetNonTestUsers() []string { return []string{} }
func (o *Base) GetKindExceptions() []kind.Kind { return nil }
func (o *Base) GetNameExceptions() []string { return nil }
func (o *Base) DeleteAfterCompliance() bool {
return os.Getenv("GOF3_TEST_COMPLIANCE_CLEANUP") != "false"
}

View file

@ -0,0 +1,35 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package forge
import (
"fmt"
"strings"
)
type (
Factory func() Interface
Factories map[string]Factory
)
var factories = make(Factories, 10)
func GetFactories() Factories {
return factories
}
func RegisterFactory(name string, factory Factory) {
name = strings.ToLower(name)
factories[name] = factory
}
func GetFactory(name string) Factory {
name = strings.ToLower(name)
factory, ok := factories[name]
if !ok {
panic(fmt.Errorf("no factory registered for %s", name))
}
return factory
}

View file

@ -0,0 +1,26 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package forge
import (
"testing"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/options"
)
type Interface interface {
GetName() string
SetName(string)
DeleteAfterCompliance() bool
GetKindExceptions() []kind.Kind
GetNameExceptions() []string
GetNonTestUsers() []string
NewOptions(t *testing.T) options.Interface
}

View file

@ -0,0 +1,254 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"context"
"slices"
"testing"
"time"
"code.forgejo.org/f3/gof3/v3/f3"
filesystem_options "code.forgejo.org/f3/gof3/v3/forges/filesystem/options"
tests_repository "code.forgejo.org/f3/gof3/v3/forges/helpers/tests/repository"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/path"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
tests_forge "code.forgejo.org/f3/gof3/v3/tree/tests/f3/forge"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type remapper func(path string) path.Path
func ForgeCompliance(t *testing.T, name string) {
testForge := tests_forge.GetFactory(name)()
ctx := context.Background()
forgeOptions := testForge.NewOptions(t)
forgeTree := generic.GetFactory("f3")(ctx, forgeOptions)
forgeTree.Trace("======= build fixture")
fixtureTree := generic.GetFactory("f3")(ctx, tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t))
TreeBuildPartial(t, name, testForge.GetKindExceptions(), forgeOptions, fixtureTree)
forgeTree.Trace("======= mirror fixture to forge")
generic.TreeMirror(ctx, fixtureTree, forgeTree, generic.NewPathFromString(""), generic.NewMirrorOptions())
forgeTree.Trace("======= run compliance tests")
remap := func(p string) path.Path {
return generic.TreePathRemap(ctx, fixtureTree, generic.NewPathFromString(p), forgeTree)
}
kindToForgePath := make(map[kind.Kind]string, len(KindToFixturePath))
for kind, path := range KindToFixturePath {
remappedPath := remap(path)
if remappedPath.Empty() {
forgeTree.Trace("%s was not mirrored, ignored", path)
continue
}
kindToForgePath[kind] = remappedPath.String()
}
ComplianceKindTests(t, name, forgeTree.(f3_tree.TreeInterface), kindToForgePath, testForge.GetKindExceptions())
ComplianceNameTests(t, name, forgeTree.(f3_tree.TreeInterface), remap, kindToForgePath, testForge.GetNameExceptions())
if testForge.DeleteAfterCompliance() {
TreeDelete(t, testForge.GetNonTestUsers(), forgeOptions, forgeTree)
}
}
func ComplianceNameTests(t *testing.T, name string, tree f3_tree.TreeInterface, remap remapper, kindToForgePath map[kind.Kind]string, exceptions []string) {
t.Helper()
ctx := context.Background()
if !slices.Contains(exceptions, tests_forge.ComplianceNameForkedPullRequest) {
t.Run(tests_forge.ComplianceNameForkedPullRequest, func(t *testing.T) {
kind := kind.Kind(f3_tree.KindPullRequests)
p := kindToForgePath[kind]
parent := tree.Find(generic.NewPathFromString(p))
require.NotEqualValues(t, generic.NilNode, parent, p)
child := tree.Factory(ctx, tree.GetChildrenKind(kind))
childFormat := ComplianceForkedPullRequest(t, tree, remap, child.NewFormat().(*f3.PullRequest), parent.GetCurrentPath())
child.FromFormat(childFormat)
tree.Trace("'Upsert' the new forked pull request in the parent and store it in the forge")
child.SetParent(parent)
child.Upsert(ctx)
tree.Trace("'Delete' child forked pull request")
child.Delete(ctx)
})
}
}
func ComplianceForkedPullRequest(t *testing.T, tree f3_tree.TreeInterface, remap remapper, pullRequest *f3.PullRequest, parent path.Path) *f3.PullRequest {
user := f3_tree.GetFirstFormat[*f3.User](parent.Last().(generic.NodeInterface))
projectNode := f3_tree.GetFirstNodeKind(parent.Last().(generic.NodeInterface), f3_tree.KindProject)
repositoryNode := projectNode.Find(generic.NewPathFromString("repositories/vcs"))
repositoryNode.Get(context.Background())
repositoryHelper := tests_repository.NewTestHelper(t, "", repositoryNode)
mainRef := "master"
mainSha := repositoryHelper.GetRepositorySha(mainRef)
tree.Trace("create a feature branch in the /forge/users/20222/projects/99099 fork")
forkedRepositoryPath := remap("/forge/users/20222/projects/99099/repositories/vcs")
forkedRepositoryNode := tree.Find(forkedRepositoryPath)
require.NotEqual(t, forkedRepositoryNode, generic.NilNode)
forkedRepositoryHelper := tests_repository.NewTestHelper(t, "", forkedRepositoryNode)
featureRef := "generatedforkfeature"
forkedRepositoryHelper.InternalBranchRepositoryFeature(featureRef, featureRef+" content")
featureSha := forkedRepositoryHelper.GetRepositorySha(featureRef)
forkedRepositoryHelper.PushMirror()
now := now()
prCreated := tick(&now)
prUpdated := tick(&now)
pullRequest.PosterID = f3_tree.NewUserReference(user.GetID())
pullRequest.Title = featureRef + " pr title"
pullRequest.Content = featureRef + " pr content"
pullRequest.State = f3.PullRequestStateOpen
pullRequest.IsLocked = false
pullRequest.Created = prCreated
pullRequest.Updated = prUpdated
pullRequest.Closed = nil
pullRequest.Merged = false
pullRequest.MergedTime = nil
pullRequest.MergeCommitSHA = ""
pullRequest.Head = f3.PullRequestBranch{
Ref: featureRef,
SHA: featureSha,
Repository: f3.NewReference(forkedRepositoryPath.String()),
}
pullRequest.Base = f3.PullRequestBranch{
Ref: mainRef,
SHA: mainSha,
Repository: f3.NewReference("../../repository/vcs"),
}
return pullRequest
}
func ComplianceKindTests(t *testing.T, name string, tree f3_tree.TreeInterface, kindToForgePath map[kind.Kind]string, exceptions []kind.Kind) {
t.Helper()
exceptions = append(exceptions, f3_tree.KindRepositories)
for _, kind := range KindWithFixturePath {
path := kindToForgePath[kind]
if !tree.IsContainer(kind) {
continue
}
if slices.Contains(exceptions, kind) {
continue
}
t.Run(string(kind), func(t *testing.T) {
Compliance(t, name, tree, path, kind, GeneratorSetRandom, GeneratorModify)
})
}
}
func Compliance(t *testing.T, name string, tree f3_tree.TreeInterface, p string, kind kind.Kind, generator GeneratorFunc, modificator ModificatorFunc) {
t.Helper()
ctx := context.Background()
tree.Trace("%s", p)
parent := tree.Find(generic.NewPathFromString(p))
require.NotEqualValues(t, generic.NilNode, parent, p)
tree.Trace("create a new child in memory")
child := tree.Factory(ctx, tree.GetChildrenKind(kind))
childFormat := generator(t, name, child.NewFormat(), parent.GetCurrentPath())
child.FromFormat(childFormat)
if i := child.GetID(); i != id.NilID {
tree.Trace("about to insert child %s", i)
assert.EqualValues(t, generic.NilNode, parent.GetChild(child.GetID()))
} else {
tree.Trace("about to insert child with nil ID")
}
if child.GetDriver().IsNull() {
t.Skip("no driver, skipping")
}
tree.Trace("'Upsert' the new child in the parent and store it in the forge")
child.SetParent(parent)
child.Upsert(ctx)
tree.Trace("done inserting child '%s'", child.GetID())
before := child.ToFormat()
require.EqualValues(t, before.GetID(), child.GetID().String())
tree.Trace("'Get' the child '%s' from the forge", child.GetID())
child.Get(ctx)
after := child.ToFormat()
tree.Trace("check the F3 representations Upsert & Get to/from the forge are equivalent")
require.True(t, cmp.Equal(before, after), cmp.Diff(before, after))
tree.Trace("check the F3 representation FromFormat/ToFormat are identical")
{
saved := after
a := childFormat.Clone()
if tree.AllocateID() {
a.SetID("123456")
}
child.FromFormat(a)
b := child.ToFormat()
require.True(t, cmp.Equal(a, b), cmp.Diff(a, b))
child.FromFormat(saved)
}
if childFormat.GetName() != childFormat.GetID() {
tree.Trace("'GetIDFromName' %s %s", kind, childFormat.GetName())
id := parent.GetIDFromName(ctx, childFormat.GetName())
assert.EqualValues(t, child.GetID(), id)
}
for i, modified := range modificator(t, after, parent.GetCurrentPath()) {
tree.Trace("%d: %s 'Upsert' a modified child %v", i, kind, modified)
child.FromFormat(modified)
child.Upsert(ctx)
tree.Trace("%d: 'Get' the modified child '%s' from the forge", i, child.GetID())
child.Get(ctx)
after = child.ToFormat()
tree.Trace("%d: check the F3 representations Upsert & Get to/from the forge of the modified child are equivalent", i)
require.True(t, cmp.Equal(modified, after), cmp.Diff(modified, after))
}
nodeChildren := parent.GetNodeChildren()
tree.Trace("'ListPage' and only 'Get' known %d children of %s", len(nodeChildren), parent.GetKind())
if len(nodeChildren) > 0 {
parent.List(ctx)
for _, child := range parent.GetChildren() {
if _, ok := nodeChildren[child.GetID()]; ok {
tree.Trace("'WalkAndGet' %s child %s %s", parent.GetCurrentPath().ReadableString(), child.GetKind(), child.GetID())
child.WalkAndGet(ctx, parent.GetCurrentPath(), generic.NewWalkOptions(nil))
}
}
}
tree.Trace("'Delete' child '%s' from the forge", child.GetID())
child.Delete(ctx)
assert.EqualValues(t, generic.NilNode, parent.GetChild(child.GetID()))
assert.True(t, child.GetIsSync())
loop := 100
for i := 0; i < loop; i++ {
child.SetIsSync(false)
child.Get(ctx)
if !child.GetIsSync() {
break
}
tree.Trace("waiting for asynchronous child deletion (%d/%d)", i, loop)
time.Sleep(5 * time.Second)
}
assert.False(t, child.GetIsSync())
tree.Trace("%s did something %s", kind, child)
}

View file

@ -0,0 +1,19 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"testing"
tests_forge "code.forgejo.org/f3/gof3/v3/tree/tests/f3/forge"
)
func TestF3Forge(t *testing.T) {
for name := range tests_forge.GetFactories() {
t.Run(name, func(t *testing.T) {
ForgeCompliance(t, name)
})
}
}

474
tree/tests/f3/generator.go Normal file
View file

@ -0,0 +1,474 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"context"
"crypto/sha256"
"fmt"
"io"
"strings"
"testing"
"time"
"code.forgejo.org/f3/gof3/v3/f3"
tests_repository "code.forgejo.org/f3/gof3/v3/forges/helpers/tests/repository"
"code.forgejo.org/f3/gof3/v3/path"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
)
func GeneratorSetRandomID(id string, f f3.Interface, parent path.Path) f3.Interface {
if parent.First().(generic.NodeInterface).GetTree().AllocateID() {
return f
}
return GeneratorSetID(f, id)
}
func GeneratorSetID(f f3.Interface, id string) f3.Interface {
f.SetID(id)
return f
}
type ModificatorFunc func(t *testing.T, f f3.Interface, parent path.Path) []f3.Interface
func GeneratorModify(t *testing.T, f f3.Interface, parent path.Path) []f3.Interface {
switch v := f.(type) {
case *f3.User:
return GeneratorModifyUser(v)
case *f3.Organization:
return GeneratorModifyOrganization(v)
case *f3.Project:
return GeneratorModifyProject(v, parent)
case *f3.Issue:
return GeneratorModifyIssue(v, parent)
case *f3.Milestone:
return GeneratorModifyMilestone(v, parent)
case *f3.Topic:
return GeneratorModifyTopic(v, parent)
case *f3.Reaction:
// a reaction cannot be modified, it can only be created and deleted
return []f3.Interface{}
case *f3.Label:
return GeneratorModifyLabel(v, parent)
case *f3.Comment:
return GeneratorModifyComment(v, parent)
case *f3.Release:
return GeneratorModifyRelease(t, v, parent)
case *f3.ReleaseAsset:
return GeneratorModifyReleaseAsset(v, parent)
case *f3.PullRequest:
return GeneratorModifyPullRequest(t, v, parent)
case *f3.Review:
// a review cannot be modified, it can only be created and deleted
return []f3.Interface{}
case *f3.ReviewComment:
return GeneratorModifyReviewComment(t, v, parent)
default:
panic(fmt.Errorf("not implemented %T", f))
}
}
type GeneratorFunc func(t *testing.T, name string, f f3.Interface, parent path.Path) f3.Interface
func GeneratorSetRandom(t *testing.T, name string, f f3.Interface, parent path.Path) f3.Interface {
GeneratorSetRandomID(name, f, parent)
switch v := f.(type) {
case *f3.User:
return GeneratorSetRandomUser(v)
case *f3.Organization:
return GeneratorSetRandomOrganization(v)
case *f3.Project:
return GeneratorSetRandomProject(v, parent)
case *f3.Issue:
return GeneratorSetRandomIssue(v, parent)
case *f3.Milestone:
return GeneratorSetRandomMilestone(v, parent)
case *f3.Topic:
return GeneratorSetRandomTopic(v, parent)
case *f3.Reaction:
return GeneratorSetRandomReaction(v, parent)
case *f3.Label:
return GeneratorSetRandomLabel(v, parent)
case *f3.Comment:
return GeneratorSetRandomComment(v, parent)
case *f3.Release:
return GeneratorSetRandomRelease(t, v, parent)
case *f3.ReleaseAsset:
return GeneratorSetRandomReleaseAsset(v, parent)
case *f3.PullRequest:
return GeneratorSetRandomPullRequest(t, v, parent)
case *f3.Review:
return GeneratorSetReview(t, v, parent)
case *f3.ReviewComment:
return GeneratorSetReviewComment(t, v, parent)
default:
panic(fmt.Errorf("not implemented %T", f))
}
}
func GeneratorSetRandomUser(user *f3.User) *f3.User {
username := fmt.Sprintf("generateduser%s", user.GetID())
user.Name = username + " Doe"
user.UserName = username
user.Email = username + "@example.com"
user.Password = "Wrobyak4"
return user
}
func GeneratorModifyUser(user *f3.User) []f3.Interface {
return []f3.Interface{user.Clone()}
}
func GeneratorSetRandomOrganization(organization *f3.Organization) *f3.Organization {
organizationname := fmt.Sprintf("generatedorg%s", organization.GetID())
organization.FullName = organizationname + " Lambda"
organization.Name = organizationname
return organization
}
func GeneratorModifyOrganization(organization *f3.Organization) []f3.Interface {
organization0 := organization.Clone().(*f3.Organization)
organization0.FullName = "modified " + organization.FullName
return []f3.Interface{organization0}
}
func GeneratorSetRandomProject(project *f3.Project, parent path.Path) *f3.Project {
projectname := fmt.Sprintf("project%s", project.GetID())
project.Name = projectname
project.IsPrivate = false
project.IsMirror = false
project.Description = "project description"
project.DefaultBranch = "main"
return project
}
func GeneratorModifyProject(project *f3.Project, parent path.Path) []f3.Interface {
project0 := project.Clone().(*f3.Project)
project0.Description = "modified " + project.Description
return []f3.Interface{project0}
}
func GeneratorSetRandomIssue(issue *f3.Issue, parent path.Path) *f3.Issue {
user := f3_tree.GetFirstFormat[*f3.User](parent.Last().(generic.NodeInterface))
projectNode := f3_tree.GetFirstNodeKind(parent.Last().(generic.NodeInterface), f3_tree.KindProject)
labelsNode := projectNode.Find(generic.NewPathFromString("labels"))
labels := labelsNode.GetChildren()
firstLabel := labels[0]
now := now()
updated := tick(&now)
closed := tick(&now)
created := tick(&now)
userRef := f3_tree.NewUserReference(user.GetID())
labelRef := f3_tree.NewIssueLabelReference(firstLabel.GetID())
milestonesNode := projectNode.Find(generic.NewPathFromString("milestones"))
milestones := milestonesNode.GetChildren()
firstMilestone := milestones[0]
issue.PosterID = userRef
issue.Assignees = []*f3.Reference{userRef}
issue.Labels = []*f3.Reference{labelRef}
issue.Milestone = f3_tree.NewIssueMilestoneReference(firstMilestone.GetID())
issue.Title = "title"
issue.Content = "content"
issue.State = f3.IssueStateOpen
issue.IsLocked = false
issue.Created = created
issue.Updated = updated
issue.Closed = &closed
return issue
}
func GeneratorModifyIssue(issue *f3.Issue, parent path.Path) []f3.Interface {
assignees := issue.Assignees
milestone := issue.Milestone
labels := issue.Labels
issue0 := issue.Clone().(*f3.Issue)
issue0.Title = "modified " + issue.Title
issue0.Content = "modified " + issue.Content
issueClosed := issue0.Clone().(*f3.Issue)
issueClosed.Assignees = []*f3.Reference{}
issueClosed.Milestone = &f3.Reference{}
issueClosed.Labels = []*f3.Reference{}
issueClosed.State = f3.IssueStateClosed
issueClosed.IsLocked = true
issueOpen := issue0.Clone().(*f3.Issue)
issueOpen.Assignees = assignees
issueOpen.Milestone = milestone
issueOpen.Labels = labels
issueOpen.State = f3.IssueStateOpen
issueClosed.IsLocked = false
return []f3.Interface{
issue0,
issueClosed,
issueOpen,
}
}
func GeneratorSetRandomMilestone(milestone *f3.Milestone, parent path.Path) *f3.Milestone {
now := now()
created := tick(&now)
updated := tick(&now)
deadline := tick(&now)
title := fmt.Sprintf("milestone%s", milestone.GetID())
milestone.Title = title
milestone.Description = title + " description"
milestone.Deadline = &deadline
milestone.Created = created
milestone.Updated = &updated
milestone.Closed = nil
milestone.State = f3.MilestoneStateOpen
return milestone
}
func GeneratorModifyMilestone(milestone *f3.Milestone, parent path.Path) []f3.Interface {
milestone0 := milestone.Clone().(*f3.Milestone)
milestone0.Title = "modified " + milestone.Title
milestoneClosed := milestone0.Clone().(*f3.Milestone)
milestoneClosed.State = f3.MilestoneStateClosed
deadline := time.Now().Truncate(time.Second).Add(5 * time.Minute)
milestoneClosed.Deadline = &deadline
milestoneOpen := milestone0.Clone().(*f3.Milestone)
milestoneOpen.State = f3.MilestoneStateOpen
return []f3.Interface{
milestone0,
milestoneClosed,
milestoneOpen,
}
}
func GeneratorSetRandomTopic(topic *f3.Topic, parent path.Path) *f3.Topic {
topic.Name = fmt.Sprintf("topic%s", topic.GetID())
return topic
}
func GeneratorModifyTopic(topic *f3.Topic, parent path.Path) []f3.Interface {
topic0 := topic.Clone().(*f3.Topic)
return []f3.Interface{topic0}
}
func GeneratorSetRandomReaction(reaction *f3.Reaction, parent path.Path) *f3.Reaction {
user := f3_tree.GetFirstFormat[*f3.User](parent.Last().(generic.NodeInterface))
reaction.UserID = f3_tree.NewUserReference(user.GetID())
reaction.Content = "laugh"
return reaction
}
func GeneratorSetRandomLabel(label *f3.Label, parent path.Path) *f3.Label {
name := fmt.Sprintf("label%s", label.GetID())
label.Name = name
label.Description = name + " description"
label.Color = "ffffff"
return label
}
func GeneratorModifyLabel(label *f3.Label, parent path.Path) []f3.Interface {
label0 := label.Clone().(*f3.Label)
label0.Name = "modified" + label.Name
label0.Color = "f0f0f0"
label0.Description = "modified " + label.Description
return []f3.Interface{label0}
}
func GeneratorSetRandomComment(comment *f3.Comment, parent path.Path) *f3.Comment {
user := f3_tree.GetFirstFormat[*f3.User](parent.Last().(generic.NodeInterface))
now := now()
commentCreated := tick(&now)
commentUpdated := tick(&now)
comment.PosterID = f3_tree.NewUserReference(user.GetID())
comment.Created = commentCreated
comment.Updated = commentUpdated
comment.Content = "comment content"
return comment
}
func GeneratorModifyComment(comment *f3.Comment, parent path.Path) []f3.Interface {
comment0 := comment.Clone().(*f3.Comment)
comment0.Content = "modified" + comment.Content
return []f3.Interface{comment0}
}
func GeneratorSetRandomRelease(t *testing.T, release *f3.Release, parent path.Path) *f3.Release {
user := f3_tree.GetFirstFormat[*f3.User](parent.Last().(generic.NodeInterface))
project := f3_tree.GetFirstNodeKind(parent.Last().(generic.NodeInterface), f3_tree.KindProject)
repository := project.Find(generic.NewPathFromString("repositories/vcs"))
repository.Get(context.Background())
repositoryHelper := tests_repository.NewTestHelper(t, "", repository)
now := now()
releaseCreated := tick(&now)
tag := fmt.Sprintf("release%s", release.GetID())
repositoryHelper.CreateRepositoryTag(tag, "master")
sha := repositoryHelper.GetRepositorySha("master")
fmt.Printf("GeneratorSetRandomRelease %s %s\n", repository.GetCurrentPath(), repository.GetID())
repositoryHelper.PushMirror()
release.TagName = tag
release.TargetCommitish = sha
release.Name = tag + " name"
release.Body = tag + " body"
release.Draft = false
release.Prerelease = false
release.PublisherID = f3_tree.NewUserReference(user.GetID())
release.Created = releaseCreated
return release
}
func GeneratorModifyRelease(t *testing.T, release *f3.Release, parent path.Path) []f3.Interface {
release0 := release.Clone().(*f3.Release)
release0.Body = "modified " + release.Body
return []f3.Interface{release0}
}
func GeneratorSetRandomReleaseAsset(asset *f3.ReleaseAsset, parent path.Path) *f3.ReleaseAsset {
name := fmt.Sprintf("assetname%s", asset.GetID())
content := fmt.Sprintf("assetcontent%s", asset.GetID())
downloadURL := "downloadURL"
now := now()
assetCreated := tick(&now)
size := len(content)
downloadCount := int64(10)
sha256 := fmt.Sprintf("%x", sha256.Sum256([]byte(content)))
asset.Name = name
asset.Size = int64(size)
asset.DownloadCount = downloadCount
asset.Created = assetCreated
asset.SHA256 = sha256
asset.DownloadURL = downloadURL
asset.DownloadFunc = func() io.ReadCloser {
rc := io.NopCloser(strings.NewReader(content))
return rc
}
return asset
}
func GeneratorModifyReleaseAsset(asset *f3.ReleaseAsset, parent path.Path) []f3.Interface {
asset0 := asset.Clone().(*f3.ReleaseAsset)
asset0.Name = "modified" + asset.Name
return []f3.Interface{asset0}
}
func GeneratorSetRandomPullRequest(t *testing.T, pullRequest *f3.PullRequest, parent path.Path) *f3.PullRequest {
user := f3_tree.GetFirstFormat[*f3.User](parent.Last().(generic.NodeInterface))
projectNode := f3_tree.GetFirstNodeKind(parent.Last().(generic.NodeInterface), f3_tree.KindProject)
repositoryNode := projectNode.Find(generic.NewPathFromString("repositories/vcs"))
repositoryNode.Get(context.Background())
repositoryHelper := tests_repository.NewTestHelper(t, "", repositoryNode)
mainRef := "master"
mainSha := repositoryHelper.GetRepositorySha(mainRef)
featureRef := "generatedfeature"
repositoryHelper.InternalBranchRepositoryFeature(featureRef, featureRef+" content")
featureSha := repositoryHelper.GetRepositorySha(featureRef)
fmt.Printf("createPullRequest: master %s at main %s feature %s\n", repositoryHelper.GetBare(), mainSha, featureSha)
repositoryHelper.PushMirror()
now := now()
prCreated := tick(&now)
prUpdated := tick(&now)
pullRequest.PosterID = f3_tree.NewUserReference(user.GetID())
pullRequest.Title = featureRef + " pr title"
pullRequest.Content = featureRef + " pr content"
pullRequest.State = f3.PullRequestStateOpen
pullRequest.IsLocked = false
pullRequest.Created = prCreated
pullRequest.Updated = prUpdated
pullRequest.Closed = nil
pullRequest.Merged = false
pullRequest.MergedTime = nil
pullRequest.MergeCommitSHA = ""
pullRequest.Head = f3.PullRequestBranch{
Ref: featureRef,
SHA: featureSha,
Repository: f3.NewReference("../../repository/vcs"),
}
pullRequest.Base = f3.PullRequestBranch{
Ref: mainRef,
SHA: mainSha,
Repository: f3.NewReference("../../repository/vcs"),
}
return pullRequest
}
func GeneratorModifyPullRequest(t *testing.T, pullRequest *f3.PullRequest, parent path.Path) []f3.Interface {
pullRequest0 := pullRequest.Clone().(*f3.PullRequest)
pullRequest0.Title = "modified " + pullRequest.Title
return []f3.Interface{pullRequest0}
}
func GeneratorSetReview(t *testing.T, review *f3.Review, parent path.Path) *f3.Review {
user := f3_tree.GetFirstFormat[*f3.User](parent.Last().(generic.NodeInterface))
projectNode := f3_tree.GetFirstNodeKind(parent.Last().(generic.NodeInterface), f3_tree.KindProject)
repositoryNode := projectNode.Find(generic.NewPathFromString("repositories/vcs"))
repositoryNode.Get(context.Background())
repositoryHelper := tests_repository.NewTestHelper(t, "", repositoryNode)
now := now()
reviewCreated := tick(&now)
featureSha := repositoryHelper.GetRepositorySha("feature")
review.ReviewerID = f3_tree.NewUserReference(user.GetID())
review.Official = true
review.CommitID = featureSha
review.Content = "the review content"
review.CreatedAt = reviewCreated
review.State = f3.ReviewStateCommented
return review
}
func GeneratorSetReviewComment(t *testing.T, comment *f3.ReviewComment, parent path.Path) *f3.ReviewComment {
user := f3_tree.GetFirstFormat[*f3.User](parent.Last().(generic.NodeInterface))
projectNode := f3_tree.GetFirstNodeKind(parent.Last().(generic.NodeInterface), f3_tree.KindProject)
repositoryNode := projectNode.Find(generic.NewPathFromString("repositories/vcs"))
repositoryNode.Get(context.Background())
repositoryHelper := tests_repository.NewTestHelper(t, "", repositoryNode)
now := now()
commentCreated := tick(&now)
commentUpdated := tick(&now)
featureSha := repositoryHelper.GetRepositorySha("feature")
comment.Content = "comment content"
comment.TreePath = "README.md"
comment.DiffHunk = "@@ -108,7 +108,6 @@"
comment.Line = 1
comment.CommitID = featureSha
comment.PosterID = f3_tree.NewUserReference(user.GetID())
comment.CreatedAt = commentCreated
comment.UpdatedAt = commentUpdated
return comment
}
func GeneratorModifyReviewComment(t *testing.T, comment *f3.ReviewComment, parent path.Path) []f3.Interface {
comment0 := comment.Clone().(*f3.ReviewComment)
comment0.Content = "modified " + comment.Content
return []f3.Interface{comment0}
}

View file

@ -0,0 +1,125 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"context"
"testing"
filesystem_options "code.forgejo.org/f3/gof3/v3/forges/filesystem/options"
forgejo_options "code.forgejo.org/f3/gof3/v3/forges/forgejo/options"
helpers_repository "code.forgejo.org/f3/gof3/v3/forges/helpers/repository"
tests_repository "code.forgejo.org/f3/gof3/v3/forges/helpers/tests/repository"
"code.forgejo.org/f3/gof3/v3/logger"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
tests_forge "code.forgejo.org/f3/gof3/v3/tree/tests/f3/forge"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNodeRepositoryDriverMirrorNoop(t *testing.T) {
ctx := context.Background()
url := t.TempDir()
repositoryHelper := tests_repository.NewTestHelper(t, url, nil)
repositoryHelper.CreateRepositoryContent("").PushMirror()
log := logger.NewCaptureLogger()
log.SetLevel(logger.Trace)
log.Reset()
helpers_repository.GitMirror(ctx, log, url, url, []string{})
assert.Contains(t, log.String(), "do nothing")
log.Reset()
helpers_repository.GitMirrorRef(ctx, log, url, "fakeref", url, "fakeref")
assert.Contains(t, log.String(), "do nothing")
}
func TestNodeRepositoryDriverMirrorForgejo(t *testing.T) {
ctx := context.Background()
log := logger.NewCaptureLogger()
log.SetLevel(logger.Trace)
testForgejo := tests_forge.GetFactory(forgejo_options.Name)()
opts := testForgejo.NewOptions(t)
forgeTree := generic.GetFactory("f3")(ctx, opts)
forgeTree.Trace("======= build fixture")
fixtureTree := generic.GetFactory("f3")(ctx, tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t))
TreeBuildPartial(t, "F3", testForgejo.GetKindExceptions(), opts, fixtureTree)
forgeTree.Trace("======= mirror fixture to forge")
generic.TreeMirror(ctx, fixtureTree, forgeTree, generic.NewPathFromString(""), generic.NewMirrorOptions())
forgeTree.Trace("======= NodeRepositoryDriverMirror from a forgejo project to a directory")
repositoryPath := generic.TreePathRemap(ctx, fixtureTree, generic.NewPathFromString("/forge/users/10111/projects/74823/repositories/vcs"), forgeTree)
require.False(t, repositoryPath.Empty())
repository := forgeTree.Find(repositoryPath)
repositoryURL := repository.GetDriver().(f3_tree.RepositoryNodeDriverProxyInterface).GetRepositoryURL()
internalRefs := repository.GetDriver().(f3_tree.RepositoryNodeDriverProxyInterface).GetRepositoryInternalRefs()
log.Reset()
destination := t.TempDir()
tests_repository.NewTestHelper(t, destination, nil)
helpers_repository.GitMirror(ctx, log, repositoryURL, destination, internalRefs)
require.Contains(t, log.String(), "fetch fetchMirror")
forgeTree.Trace("======= NodeRepositoryDriverMirror from one forgejo project to another forgejo project")
otherRepositoryPath := generic.TreePathRemap(ctx, fixtureTree, generic.NewPathFromString("/forge/users/20222/projects/99099/repositories/vcs"), forgeTree)
require.False(t, otherRepositoryPath.Empty())
otherRepository := forgeTree.Find(otherRepositoryPath)
otherRepositoryURL := otherRepository.GetDriver().(f3_tree.RepositoryNodeDriverProxyInterface).GetRepositoryURL()
otherRepositoryPushURL := otherRepository.GetDriver().(f3_tree.RepositoryNodeDriverProxyInterface).GetRepositoryPushURL()
otherInternalRefs := otherRepository.GetDriver().(f3_tree.RepositoryNodeDriverProxyInterface).GetRepositoryInternalRefs()
log.Reset()
helpers_repository.GitMirror(ctx, log, repositoryURL, otherRepositoryPushURL, otherInternalRefs)
require.Contains(t, log.String(), "+refs/")
forgeTree.Trace("======= NodeRepositoryDriverMirror from a directory to a forgejo project")
log.Reset()
repositoryHelper := tests_repository.NewTestHelper(t, destination, nil)
content := "SOMETHING DIFFERENT"
repositoryHelper.CreateRepositoryContent(content).PushMirror()
helpers_repository.GitMirror(ctx, log, destination, otherRepositoryPushURL, otherInternalRefs)
require.Contains(t, log.String(), "+refs/")
verificationDir := t.TempDir()
repositoryHelper = tests_repository.NewTestHelper(t, verificationDir, nil)
helpers_repository.GitMirror(ctx, log, otherRepositoryURL, verificationDir, []string{})
repositoryHelper.AssertReadmeContains(content)
forgeTree.Trace("======= NodeRepositoryDriverMirrorRef from a forgejo project to a directory")
masterRef := "refs/heads/master"
forgejoRef := "refs/forgejo/test"
directoryRef := "refs/directory/test"
log.Reset()
helpers_repository.GitMirrorRef(ctx, log, repositoryURL, masterRef, destination, directoryRef)
require.Contains(t, log.String(), "new branch")
directorySha := helpers_repository.GitGetSha(ctx, log, destination, directoryRef)
forgeTree.Trace("======= NodeRepositoryDriverMirrorRef from one forgejo project to another forgejo project")
log.Reset()
otherForgejoRef := "refs/otherforgejo/test"
otherDirectoryRef := "refs/otherdirectory/test"
helpers_repository.GitMirrorRef(ctx, log, repositoryURL, masterRef, otherRepositoryPushURL, otherForgejoRef)
helpers_repository.GitMirrorRef(ctx, log, otherRepositoryURL, otherForgejoRef, destination, otherDirectoryRef)
assert.EqualValues(t, directorySha, helpers_repository.GitGetSha(ctx, log, destination, otherDirectoryRef))
forgeTree.Trace("======= NodeRepositoryDriverMirrorRef from a directory to a forgejo project")
log.Reset()
helpers_repository.GitMirrorRef(ctx, log, verificationDir, masterRef, otherRepositoryPushURL, forgejoRef)
masterSha := helpers_repository.GitGetSha(ctx, log, verificationDir, masterRef)
helpers_repository.GitMirrorRef(ctx, log, otherRepositoryURL, forgejoRef, verificationDir, directoryRef)
assert.EqualValues(t, masterSha, helpers_repository.GitGetSha(ctx, log, verificationDir, directoryRef))
}

19
tree/tests/f3/init.go Normal file
View file

@ -0,0 +1,19 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
// register filesystem test factory
_ "code.forgejo.org/f3/gof3/v3/forges/filesystem"
_ "code.forgejo.org/f3/gof3/v3/forges/filesystem/tests"
// register forgejo test factory
_ "code.forgejo.org/f3/gof3/v3/forges/forgejo"
_ "code.forgejo.org/f3/gof3/v3/forges/forgejo/tests"
// register gitlab test factory
_ "code.forgejo.org/f3/gof3/v3/forges/gitlab"
_ "code.forgejo.org/f3/gof3/v3/forges/gitlab/tests"
)

View file

@ -0,0 +1,16 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"github.com/stretchr/testify/assert"
)
type TestingT interface {
assert.TestingT
TempDir() string
Skip(args ...any)
FailNow()
}

View file

@ -0,0 +1,9 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
_ "code.forgejo.org/f3/gof3/v3/forges"
)

View file

@ -0,0 +1,71 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"context"
"testing"
filesystem_options "code.forgejo.org/f3/gof3/v3/forges/filesystem/options"
"code.forgejo.org/f3/gof3/v3/options"
"code.forgejo.org/f3/gof3/v3/path"
"code.forgejo.org/f3/gof3/v3/tree/generic"
tests_forge "code.forgejo.org/f3/gof3/v3/tree/tests/f3/forge"
"github.com/stretchr/testify/assert"
)
func TestF3PullRequest(t *testing.T) {
ctx := context.Background()
for _, factory := range tests_forge.GetFactories() {
testForge := factory()
t.Run(testForge.GetName(), func(t *testing.T) {
// testCase.options will t.Skip if the forge instance is not up
forgeWriteOptions := testForge.NewOptions(t)
forgeReadOptions := testForge.NewOptions(t)
forgeReadOptions.(options.URLInterface).SetURL(forgeWriteOptions.(options.URLInterface).GetURL())
fixtureTree := generic.GetFactory("f3")(ctx, tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t))
fixtureTree.Trace("======= build fixture")
TreeBuildPartial(t, "F3PullRequest"+testForge.GetName(), testForge.GetKindExceptions(), forgeWriteOptions, fixtureTree)
// craft a PR condition depending on testCase
fixtureTree.Trace("======= mirror fixture to forge")
forgeWriteTree := generic.GetFactory("f3")(ctx, forgeWriteOptions)
generic.TreeMirror(ctx, fixtureTree, forgeWriteTree, generic.NewPathFromString(""), generic.NewMirrorOptions())
paths := []string{"/forge/users/10111/projects/74823/repositories", "/forge/users/10111/projects/74823/pull_requests"}
pathPairs := make([][2]path.Path, 0, 5)
for _, p := range paths {
p := generic.NewPathFromString(p)
pathPairs = append(pathPairs, [2]path.Path{p, generic.TreePathRemap(ctx, fixtureTree, p, forgeWriteTree)})
}
fixtureTree.Trace("======= read from forge")
forgeReadTree := generic.GetFactory("f3")(ctx, forgeReadOptions)
forgeReadTree.WalkAndGet(ctx, generic.NewWalkOptions(nil))
fixtureTree.Trace("======= mirror forge to filesystem")
verificationTree := generic.GetFactory("f3")(ctx, tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t))
for _, pathPair := range pathPairs {
generic.TreeMirror(ctx, forgeReadTree, verificationTree, pathPair[1], generic.NewMirrorOptions())
}
fixtureTree.Trace("======= compare fixture with forge mirrored to filesystem")
for _, pathPair := range pathPairs {
fixtureTree.Trace("======= compare %s with %s", pathPair[0], pathPair[1])
assert.True(t, generic.TreeCompare(ctx, fixtureTree, pathPair[0], verificationTree, pathPair[1]))
}
TreeDelete(t, testForge.GetNonTestUsers(), forgeWriteOptions, forgeWriteTree)
})
}
}

View file

@ -0,0 +1,74 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"testing"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/tree/generic"
"code.forgejo.org/f3/gof3/v3/tree/memory"
"github.com/stretchr/testify/assert"
)
func TestCompare(t *testing.T) {
ctx := context.Background()
aTree := NewMemoryTree(ctx, "O")
testTreeBuild(t, aTree, 2)
bTree := NewMemoryTree(ctx, "O")
testTreeBuild(t, bTree, 2)
assert.True(t, generic.TreeCompare(ctx, aTree, generic.NewPathFromString(""), bTree, generic.NewPathFromString("")))
{
toDelete := generic.NewPathFromString("/O-A/O-B")
aTree.Find(toDelete).Delete(ctx)
assert.False(t, generic.TreeCompare(ctx, aTree, generic.NewPathFromString(""), bTree, generic.NewPathFromString("")))
bTree.Find(toDelete).Delete(ctx)
assert.True(t, generic.TreeCompare(ctx, aTree, generic.NewPathFromString(""), bTree, generic.NewPathFromString("")))
}
{
content := "OTHER CONTENT"
toModify := generic.NewPathFromString("/O-A/O-F")
aNode := aTree.Find(toModify)
memory.SetContent(aNode, content)
aNode.Upsert(ctx)
assert.False(t, generic.TreeCompare(ctx, aTree, generic.NewPathFromString(""), bTree, generic.NewPathFromString("")))
bNode := bTree.Find(toModify)
memory.SetContent(bNode, content)
bNode.Upsert(ctx)
assert.True(t, generic.TreeCompare(ctx, aTree, generic.NewPathFromString(""), bTree, generic.NewPathFromString("")))
}
{
toModify := generic.NewPathFromString("/O-A/O-F")
aTree.Find(toModify).SetKind(kind.Kind("???"))
assert.False(t, generic.TreeCompare(ctx, aTree, generic.NewPathFromString(""), bTree, generic.NewPathFromString("")))
bTree.Find(toModify).SetKind(kind.Kind("???"))
assert.True(t, generic.TreeCompare(ctx, aTree, generic.NewPathFromString(""), bTree, generic.NewPathFromString("")))
}
{
pathToMap := generic.NewPathFromString("/O-A/O-J/O-M")
mappedID := id.NewNodeID("MAPPED")
aTree.Find(pathToMap).SetMappedID(mappedID)
assert.False(t, generic.TreeCompare(ctx, aTree, generic.NewPathFromString(""), bTree, generic.NewPathFromString("")))
bNode := bTree.Find(pathToMap).Delete(ctx)
bNode.SetID(mappedID)
parentPathToMap := generic.NewPathFromString("/O-A/O-J")
bTree.Find(parentPathToMap).SetChild(bNode)
assert.True(t, generic.TreeCompare(ctx, aTree, generic.NewPathFromString(""), bTree, generic.NewPathFromString("")))
}
}

View file

@ -0,0 +1,193 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"sort"
"testing"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/path"
"code.forgejo.org/f3/gof3/v3/tree/generic"
"code.forgejo.org/f3/gof3/v3/tree/memory"
"github.com/stretchr/testify/assert"
)
func NewMemoryTree(ctx context.Context, name string) generic.TreeInterface {
return generic.GetFactory("memory")(ctx, memory.NewOptions(memory.NewIDAllocatorGenerator(name)))
}
func TestMemoryTreeIDAllocator(t *testing.T) {
ctx := context.Background()
name := "T"
id := "THEID"
for _, testCase := range []struct {
idAllocator memory.IDAllocatorInterface
setID bool
expectedID string
}{
{
idAllocator: memory.NewIDAllocatorGenerator(name),
setID: false,
expectedID: name + "-A",
},
{
idAllocator: memory.NewIDAllocatorNull(),
setID: true,
expectedID: id,
},
} {
t.Run(testCase.expectedID, func(t *testing.T) {
tree := generic.GetFactory("memory")(ctx, memory.NewOptions(testCase.idAllocator))
node := tree.GetRoot().CreateChild(ctx)
f := memory.NewFormat("")
if testCase.setID {
f.SetID(id)
}
node.FromFormat(f)
node.Upsert(ctx)
assert.EqualValues(t, testCase.expectedID, node.GetID())
})
}
}
func testTreeBuild(t *testing.T, tree generic.TreeInterface, maxDepth int) {
ctx := context.Background()
insert := func(tree generic.TreeInterface, parent generic.NodeInterface) generic.NodeInterface {
node := parent.CreateChild(ctx)
node.Upsert(ctx)
memory.SetContent(node, "content "+node.GetID().String())
node.Upsert(ctx)
return node
}
var populate func(depth int, tree generic.TreeInterface, parent generic.NodeInterface)
populate = func(depth int, tree generic.TreeInterface, parent generic.NodeInterface) {
if depth >= maxDepth {
return
}
depth++
for i := 1; i <= 3; i++ {
node := insert(tree, parent)
populate(depth, tree, node)
}
}
populate(0, tree, insert(tree, tree.GetRoot()))
}
func TestMemoryTreeBuild(t *testing.T) {
ctx := context.Background()
verify := func(tree generic.TreeInterface, expected []string) {
collected := make([]string, 0, 10)
collect := func(ctx context.Context, p path.Path, node generic.NodeInterface) {
if node.GetKind() == kind.KindRoot {
return
}
p = p.Append(node)
collected = append(collected, p.String()+":"+memory.GetContent(node))
}
tree.WalkAndGet(ctx, generic.NewWalkOptions(collect))
sort.Strings(expected)
sort.Strings(collected)
assert.EqualValues(t, expected, collected)
}
for _, testCase := range []struct {
name string
build func(tree generic.TreeInterface)
operations func(tree generic.TreeInterface)
expected []string
}{
{
name: "full tree",
build: func(tree generic.TreeInterface) { testTreeBuild(t, tree, 2) },
operations: func(tree generic.TreeInterface) {},
expected: []string{"/T-A/T-B/T-C:content T-C", "/T-A/T-B/T-D:content T-D", "/T-A/T-B/T-E:content T-E", "/T-A/T-B:content T-B", "/T-A/T-F/T-G:content T-G", "/T-A/T-F/T-H:content T-H", "/T-A/T-F/T-I:content T-I", "/T-A/T-F:content T-F", "/T-A/T-J/T-K:content T-K", "/T-A/T-J/T-L:content T-L", "/T-A/T-J/T-M:content T-M", "/T-A/T-J:content T-J", "/T-A:content T-A"},
},
{
name: "scenario 1",
build: func(tree generic.TreeInterface) { testTreeBuild(t, tree, 2) },
operations: func(tree generic.TreeInterface) {
root := tree.GetRoot()
id0 := id.NewNodeID("T-A")
root.List(ctx)
zero := root.GetChild(id0)
assert.False(t, generic.NilNode == zero)
assert.True(t, zero == zero.Get(ctx))
id1 := id.NewNodeID("T-B")
zero.List(ctx)
one := zero.GetChild(id1)
assert.False(t, generic.NilNode == one)
one.Get(ctx)
memory.SetContent(one, "other one")
one.Upsert(ctx)
id2 := id.NewNodeID("T-F")
two := zero.GetChild(id2)
two.Delete(ctx)
two.Delete(ctx)
assert.True(t, generic.NilNode == zero.GetChild(id2))
},
expected: []string{"/T-A/T-B/T-C:content T-C", "/T-A/T-B/T-D:content T-D", "/T-A/T-B/T-E:content T-E", "/T-A/T-B:other one", "/T-A/T-J/T-K:content T-K", "/T-A/T-J/T-L:content T-L", "/T-A/T-J/T-M:content T-M", "/T-A/T-J:content T-J", "/T-A:content T-A"},
},
{
name: "scenario 2",
build: func(tree generic.TreeInterface) { testTreeBuild(t, tree, 0) },
operations: func(tree generic.TreeInterface) {
root := tree.GetRoot()
id0 := id.NewNodeID("T-A")
root.List(ctx)
zero := root.GetChild(id0)
assert.False(t, generic.NilNode == zero)
zero.Get(ctx)
one := zero.CreateChild(ctx)
one.Upsert(ctx)
memory.SetContent(one, "ONE")
one.Upsert(ctx)
two := one.CreateChild(ctx)
two.Upsert(ctx)
memory.SetContent(two, "SOMETHING")
two.Upsert(ctx)
memory.SetContent(two, "ONE/TWO")
two.Upsert(ctx)
one.DeleteChild(two.GetID())
two.Get(ctx)
three := two.CreateChild(ctx)
three.Upsert(ctx)
memory.SetContent(three, "ONE/THREE")
three.Upsert(ctx)
three.Delete(ctx)
},
expected: []string{"/T-A/T-B/T-C:ONE/TWO", "/T-A/T-B:ONE", "/T-A:content T-A"},
},
} {
t.Run(testCase.name, func(t *testing.T) {
tree := NewMemoryTree(ctx, "T")
tree.Trace("========== BUILD")
testCase.build(tree)
tree.Trace("========== OPERATIONS")
testCase.operations(tree)
verify(tree, testCase.expected)
tree.Trace("========== VERIFY RELOAD")
tree.Clear(ctx)
verify(tree, testCase.expected)
})
}
}

View file

@ -0,0 +1,98 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"testing"
"code.forgejo.org/f3/gof3/v3/path"
"code.forgejo.org/f3/gof3/v3/tree/generic"
"code.forgejo.org/f3/gof3/v3/tree/memory"
"github.com/stretchr/testify/assert"
)
type testReference struct {
originPath string
originReference string
destinationPath string
destinationReference string
}
func TestMirror(t *testing.T) {
ctx := context.Background()
for _, testCase := range []struct {
start string
references []testReference
expected []string
}{
{
start: "/",
expected: []string{":ROOT:", "/D-A:content O-A:", "/D-A/D-B:content O-B:", "/D-A/D-B/D-C:content O-C:", "/D-A/D-B/D-D:content O-D:", "/D-A/D-B/D-E:content O-E:", "/D-A/D-F:content O-F:", "/D-A/D-F/D-G:content O-G:", "/D-A/D-F/D-H:content O-H:", "/D-A/D-F/D-I:content O-I:", "/D-A/D-J:content O-J:", "/D-A/D-J/D-K:content O-K:", "/D-A/D-J/D-L:content O-L:", "/D-A/D-J/D-M:content O-M:"},
},
{
start: "/O-A/O-B",
references: []testReference{
{
originPath: "/O-A/O-B/O-C",
originReference: "/O-A/O-F",
destinationPath: "/D-A/D-B/D-D",
destinationReference: "/D-A/D-C",
},
},
expected: []string{":ROOT:", "/D-A:content O-A:", "/D-A/D-B:content O-B:", "/D-A/D-B/D-D:content O-C:/D-A/D-C", "/D-A/D-B/D-E:content O-D:", "/D-A/D-B/D-F:content O-E:", "/D-A/D-C:content O-F:"},
},
{
start: "/O-A/O-F",
references: []testReference{
{
originPath: "/O-A/O-F/O-G",
originReference: "../../O-J",
destinationPath: "/D-A/D-B/D-D",
destinationReference: "../../D-C",
},
},
expected: []string{":ROOT:", "/D-A:content O-A:", "/D-A/D-B:content O-F:", "/D-A/D-B/D-D:content O-G:../../D-C", "/D-A/D-B/D-E:content O-H:", "/D-A/D-B/D-F:content O-I:", "/D-A/D-C:content O-J:"},
},
} {
t.Run(" "+testCase.start, func(t *testing.T) {
originTree := NewMemoryTree(ctx, "O")
log := originTree.GetLogger()
log.Trace("=========== build")
testTreeBuild(t, originTree, 2)
for _, c := range testCase.references {
log.Trace("=========== inject reference %s", c.originReference)
assert.True(t, originTree.Apply(ctx, generic.NewPathFromString(c.originPath), generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
memory.SetRef(node, c.originReference)
})))
}
log.Trace("=========== mirror")
destinationTree := NewMemoryTree(ctx, "D")
generic.TreeMirror(ctx, originTree, destinationTree, generic.NewPathFromString(testCase.start), generic.NewMirrorOptions())
log.Trace("=========== verify")
collected := make([]string, 0, 10)
collect := func(ctx context.Context, parent path.Path, node generic.NodeInterface) {
collected = append(collected, node.GetCurrentPath().String()+":"+memory.GetContent(node)+":"+memory.GetRef(node))
}
destinationTree.Walk(ctx, generic.NewWalkOptions(collect))
assert.EqualValues(t, testCase.expected, collected)
for _, c := range testCase.references {
log.Trace("=========== look for reference %s", c.destinationReference)
var called bool
assert.True(t, destinationTree.Apply(ctx, generic.NewPathFromString(c.destinationPath), generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
assert.EqualValues(t, c.destinationReference, memory.GetRef(node))
called = true
})))
assert.True(t, called)
}
})
}
}

View file

@ -0,0 +1,341 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"fmt"
"sort"
"testing"
"code.forgejo.org/f3/gof3/v3/path"
"code.forgejo.org/f3/gof3/v3/tree/generic"
"code.forgejo.org/f3/gof3/v3/tree/memory"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBothApplyWalk(t *testing.T) {
ctx := context.Background()
tree := NewMemoryTree(ctx, "T")
for _, testCase := range []struct {
path string
expected []string
}{
{
path: "/T-A",
expected: []string{"/T-A", "/T-A/T-B", "/T-A/T-B/T-C", "/T-A/T-B/T-D", "/T-A/T-B/T-E", "/T-A/T-F", "/T-A/T-F/T-G", "/T-A/T-F/T-H", "/T-A/T-F/T-I", "/T-A/T-J", "/T-A/T-J/T-K", "/T-A/T-J/T-L", "/T-A/T-J/T-M"},
},
{
path: "/T-A/T-B",
expected: []string{"/T-A/T-B", "/T-A/T-B/T-C", "/T-A/T-B/T-D", "/T-A/T-B/T-E"},
},
{
path: "/T-A/T-B/T-C",
expected: []string{"/T-A/T-B/T-C"},
},
} {
testTreeBuild(t, tree, 2)
collected := make([]string, 0, 10)
p := generic.NewPathFromString(testCase.path)
walk := func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
collect := func(ctx context.Context, p path.Path, node generic.NodeInterface) {
p = p.Append(node)
collected = append(collected, p.String())
}
node.Walk(ctx, parent, generic.NewWalkOptions(collect))
}
assert.True(t, tree.Apply(ctx, p, generic.NewApplyOptions(walk)))
sort.Strings(testCase.expected)
sort.Strings(collected)
assert.EqualValues(t, testCase.expected, collected)
}
}
func TestWalk(t *testing.T) {
ctx := context.Background()
tree := NewMemoryTree(ctx, "T")
expected := []string{"", "/T-A", "/T-A/T-B", "/T-A/T-B/T-C", "/T-A/T-B/T-D", "/T-A/T-B/T-E", "/T-A/T-F", "/T-A/T-F/T-G", "/T-A/T-F/T-H", "/T-A/T-F/T-I", "/T-A/T-J", "/T-A/T-J/T-K", "/T-A/T-J/T-L", "/T-A/T-J/T-M"}
testTreeBuild(t, tree, 2)
collected := make([]string, 0, 10)
collect := func(ctx context.Context, p path.Path, node generic.NodeInterface) {
p = p.Append(node)
collected = append(collected, p.String())
}
tree.Walk(ctx, generic.NewWalkOptions(collect))
sort.Strings(expected)
sort.Strings(collected)
assert.EqualValues(t, expected, collected)
}
func TestWalkAndGet(t *testing.T) {
ctx := context.Background()
tree := NewMemoryTree(ctx, "T")
testTreeBuild(t, tree, 1)
tree.Clear(ctx)
collected := make([]string, 0, 10)
collect := func(ctx context.Context, p path.Path, node generic.NodeInterface) {
p = p.Append(node)
collected = append(collected, p.String())
}
tree.Walk(ctx, generic.NewWalkOptions(collect))
assert.EqualValues(t, []string{""}, collected)
collected = make([]string, 0, 10)
tree.WalkAndGet(ctx, generic.NewWalkOptions(collect))
expected := []string{"", "/T-A", "/T-A/T-B", "/T-A/T-C", "/T-A/T-D"}
sort.Strings(expected)
sort.Strings(collected)
assert.EqualValues(t, expected, collected)
}
func TestApplyVisitID(t *testing.T) {
ctx := context.Background()
tree := NewMemoryTree(ctx, "T")
testTreeBuild(t, tree, 2)
for _, testPath := range []string{"/T-A", "/T-A/T-B", "/T-A/T-B/T-C"} {
collected := make([]string, 0, 10)
p := generic.NewPathFromString(testPath)
collect := func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
parent = parent.Append(node)
assert.False(t, node.GetIsNil(), node.String())
assert.EqualValues(t, parent.Length(), node.GetCurrentPath().Length())
expected := parent.PathString().Join()
actual := node.GetCurrentPath().String()
assert.EqualValues(t, actual, expected)
collected = append(collected, parent.String())
}
assert.True(t, tree.Apply(ctx, p, generic.NewApplyOptions(collect)))
if assert.EqualValues(t, 1, len(collected)) {
assert.EqualValues(t, testPath, collected[0])
}
}
p := generic.NewPathFromString("/1/2/3/4")
called := false
assert.False(t, tree.Apply(ctx, p, generic.NewApplyOptions(func(context.Context, path.Path, path.Path, generic.NodeInterface) { called = true })))
assert.False(t, called)
}
func TestApplyVisitByName(t *testing.T) {
ctx := context.Background()
tree := NewMemoryTree(ctx, "T")
testTreeBuild(t, tree, 2)
for _, testCase := range []struct {
path string
expected string
}{
{
path: "/T-A/content T-B/T-C",
expected: "/T-A/T-B/T-C",
},
{
path: "/T-A/content T-B/content T-C",
expected: "/T-A/T-B/T-C",
},
{
path: "/content T-A/content T-B/content T-C",
expected: "/T-A/T-B/T-C",
},
} {
collected := make([]string, 0, 10)
p := generic.NewPathFromString(testCase.path)
collect := func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
parent = parent.Append(node)
assert.False(t, node.GetIsNil(), node.String())
assert.EqualValues(t, parent.Length(), node.GetCurrentPath().Length())
expected := parent.PathString().Join()
actual := node.GetCurrentPath().String()
assert.EqualValues(t, actual, expected)
collected = append(collected, parent.String())
}
assert.True(t, tree.Apply(ctx, p, generic.NewApplyOptions(collect).SetSearch(generic.ApplySearchByName)))
if assert.EqualValues(t, 1, len(collected)) {
assert.EqualValues(t, testCase.expected, collected[0])
}
}
}
func TestApplyAndGet(t *testing.T) {
ctx := context.Background()
for _, testCase := range []struct {
path string
expected []string
}{
{
path: "/T-A",
expected: []string{"", "/T-A"},
},
{
path: "/T-A/T-B",
expected: []string{"", "/T-A", "/T-A/T-B"},
},
{
path: "/T-A/T-B/T-C",
expected: []string{"", "/T-A", "/T-A/T-B", "/T-A/T-B/T-C"},
},
} {
tree := NewMemoryTree(ctx, "T")
testTreeBuild(t, tree, 2)
tree.Clear(ctx)
p := generic.NewPathFromString(testCase.path)
var collected []string
collect := func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
parent = parent.Append(node)
collected = append(collected, parent.String())
}
{
collected = make([]string, 0, 10)
require.False(t, tree.Apply(ctx, p, generic.NewApplyOptions(collect)))
}
{
collected = make([]string, 0, 10)
require.True(t, tree.ApplyAndGet(ctx, p, generic.NewApplyOptions(collect)))
require.EqualValues(t, 1, len(collected))
assert.EqualValues(t, testCase.path, collected[0])
}
{
collected = make([]string, 0, 10)
require.True(t, tree.ApplyAndGet(ctx, p, generic.NewApplyOptions(collect).SetWhere(generic.ApplyEachNode)))
sort.Strings(testCase.expected)
sort.Strings(collected)
assert.EqualValues(t, testCase.expected, collected)
}
}
}
func TestApplyVisitRelative(t *testing.T) {
ctx := context.Background()
tree := NewMemoryTree(ctx, "T")
// The "destination" is clean and there is no need to test a/../b which becomes
// b etc. Only when the first element of the path is either an id or ..
for _, testCase := range []struct {
start string
destination string
expected string
}{
{
start: "/T-A",
destination: "T-B",
expected: "/T-A/T-B",
},
{
start: "/T-A/T-B",
destination: ".",
expected: "/T-A/T-B",
},
{
start: "/T-A/T-B",
destination: "T-C",
expected: "/T-A/T-B/T-C",
},
{
start: "/T-A/T-B",
destination: "..",
expected: "/T-A",
},
{
start: "/T-A/T-B",
destination: "../T-F/T-G",
expected: "/T-A/T-F/T-G",
},
{
start: "/T-A/T-B/T-C",
destination: "../../T-F",
expected: "/T-A/T-F",
},
} {
t.Run(" "+testCase.start+" => "+testCase.destination, func(t *testing.T) {
testTreeBuild(t, tree, 2)
collected := make([]string, 0, 10)
start := generic.NewPathFromString(testCase.start)
collect := func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
parent = parent.Append(node)
assert.False(t, node.GetIsNil(), node.String())
assert.EqualValues(t, parent.Length(), node.GetCurrentPath().Length())
expected := parent.PathString().Join()
actual := node.GetCurrentPath().String()
assert.EqualValues(t, actual, expected)
collected = append(collected, parent.String())
}
cd := func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
destination := generic.NewPathFromString(testCase.destination)
fmt.Println("start ", node.GetCurrentPath().String())
assert.True(t, node.Apply(ctx, parent, destination, generic.NewApplyOptions(collect)))
}
assert.True(t, tree.Apply(ctx, start, generic.NewApplyOptions(cd)))
if assert.EqualValues(t, 1, len(collected)) {
assert.EqualValues(t, testCase.expected, collected[0])
}
})
p := generic.NewPathFromString("/1/2/3/4")
called := false
assert.False(t, tree.Apply(ctx, p, generic.NewApplyOptions(func(context.Context, path.Path, path.Path, generic.NodeInterface) { called = true })))
assert.False(t, called)
}
}
func TestApplyUpsert(t *testing.T) {
ctx := context.Background()
tree := NewMemoryTree(ctx, "T")
testTreeBuild(t, tree, 2)
expected := generic.NewPathFromString("/T-A/T-B/T-N")
assert.False(t, tree.Exists(ctx, expected))
assert.True(t, tree.Apply(ctx, generic.NewPathFromString("/T-A/T-B"), generic.NewApplyOptions(func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
new := node.CreateChild(ctx)
new.Upsert(ctx)
memory.SetContent(new, "content "+new.GetID().String())
new.Upsert(ctx)
})))
assert.True(t, tree.Exists(ctx, expected))
}
func TestApplyDelete(t *testing.T) {
ctx := context.Background()
tree := NewMemoryTree(ctx, "T")
testTreeBuild(t, tree, 2)
toDelete := generic.NewPathFromString("/T-A/T-B")
assert.True(t, tree.Exists(ctx, toDelete))
assert.True(t, tree.Apply(ctx, toDelete, generic.NewApplyOptions(func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
node.Delete(ctx)
})))
assert.False(t, tree.Exists(ctx, toDelete))
}

View file

@ -0,0 +1,74 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"sort"
"testing"
"code.forgejo.org/f3/gof3/v3/path"
"code.forgejo.org/f3/gof3/v3/tree/generic"
"code.forgejo.org/f3/gof3/v3/tree/memory"
"github.com/stretchr/testify/assert"
)
func TestTreeCollectReferences(t *testing.T) {
ctx := context.Background()
for _, testCase := range []struct {
path string
expected []string
}{
{
path: "/O-A/O-B",
expected: []string{"/O-A/O-B/O-C"},
},
{
path: "/O-A/O-D",
expected: []string{},
},
{
path: "/O-A/O-J",
expected: []string{"/O-A/O-F", "/O-A/O-J/O-K"},
},
{
path: "/O-A/O-J/O-L",
expected: []string{"/O-A/O-J/O-K"},
},
{
path: "/O-A/O-J/O-M",
expected: []string{"/O-A/O-F"},
},
{
path: "/O-A",
expected: []string{"/O-A/O-B/O-C", "/O-A/O-F", "/O-A/O-F/O-H", "/O-A/O-J/O-K"},
},
} {
t.Run(testCase.path, func(t *testing.T) {
tree := NewMemoryTree(ctx, "O")
testTreeBuild(t, tree, 2)
setReference := func(p, reference string) {
assert.True(t, tree.Apply(ctx, generic.NewPathFromString(p), generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
memory.SetRef(node, reference)
})))
}
setReference("/O-A/O-B", "/O-A/O-B/O-C")
setReference("/O-A/O-F/O-G", "/O-A/O-F/O-H")
setReference("/O-A/O-J/O-M", "../../O-F")
setReference("/O-A/O-J/O-L", "../O-K")
actual := make([]string, 0, 10)
for _, reference := range generic.TreeCollectReferences(ctx, tree, generic.NewPathFromString(testCase.path)) {
actual = append(actual, reference.String())
}
sort.Strings(actual)
assert.EqualValues(t, testCase.expected, actual)
})
}
}

View file

@ -0,0 +1,626 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package generic
import (
"context"
"fmt"
"path/filepath"
"sort"
"testing"
"code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/path"
"code.forgejo.org/f3/gof3/v3/tree/generic"
"code.forgejo.org/f3/gof3/v3/tree/memory"
"github.com/stretchr/testify/assert"
)
func TestUnifyPathSimpleRemap(t *testing.T) {
ctx := context.Background()
for _, testCase := range []struct {
path string
expected []string
}{
{
path: "",
expected: []string{},
},
{
path: "/O-A",
expected: []string{"/O-A:O-A=content O-A => /D-A:D-A=content O-A"},
},
{
path: "/O-A/O-B",
expected: []string{"/O-A:O-A=content O-A => /D-A:D-A=content O-A", "/O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B"},
},
{
path: "/O-A/O-B/O-C",
expected: []string{"/O-A:O-A=content O-A => /D-A:D-A=content O-A", "/O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "/O-A/O-B/O-C:O-C=content O-C => /D-A/D-B/D-C:D-C=content O-C"},
},
} {
t.Run(" "+testCase.path, func(t *testing.T) {
originTree := NewMemoryTree(ctx, "O")
testTreeBuild(t, originTree, 2)
destinationTree := NewMemoryTree(ctx, "D")
collected := make([]string, 0, 10)
p := generic.NewPathFromString(testCase.path)
upsert := func(ctx context.Context, origin generic.NodeInterface, originPath path.Path, destination generic.NodeInterface, destinationPath path.Path) {
fmt.Printf("origin %v destination %v\n", origin, destination)
originPath = originPath.Append(origin)
destinationPath = destinationPath.Append(destination)
collected = append(collected, originPath.String()+":"+origin.String()+" => "+destinationPath.String()+":"+destination.String())
}
generic.TreeUnifyPath(ctx, originTree, p, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert))
assert.EqualValues(t, testCase.expected, collected)
collected = make([]string, 0, 10)
generic.TreeUnifyPath(ctx, originTree, p, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert))
assert.EqualValues(t, testCase.expected, collected)
})
}
}
func TestUnifyPathSimpleNoRemap(t *testing.T) {
ctx := context.Background()
for _, testCase := range []struct {
noremap bool
path string
expected []string
}{
{
noremap: false,
path: "/O-A/O-B",
expected: []string{"/O-A:O-A=content O-A => /D-A:D-A=content O-A", "/O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B"},
},
{
noremap: true,
path: "/O-A/O-B",
expected: []string{"/O-A:O-A=content O-A => /O-A:O-A=content O-A", "/O-A/O-B:O-B=content O-B => /O-A/O-B:O-B=content O-B"},
},
} {
t.Run(fmt.Sprintf("noremap=%v,path=%s", testCase.noremap, testCase.path), func(t *testing.T) {
originName := "O"
originTree := NewMemoryTree(ctx, originName)
testTreeBuild(t, originTree, 2)
var destinationName string
if testCase.noremap {
destinationName = originName
} else {
destinationName = "D"
}
destinationTree := NewMemoryTree(ctx, destinationName)
collected := make([]string, 0, 10)
p := generic.NewPathFromString(testCase.path)
upsert := func(ctx context.Context, origin generic.NodeInterface, originPath path.Path, destination generic.NodeInterface, destinationPath path.Path) {
fmt.Printf("origin %v destination %v\n", origin, destination)
originPath = originPath.Append(origin)
destinationPath = destinationPath.Append(destination)
collected = append(collected, originPath.String()+":"+origin.String()+" => "+destinationPath.String()+":"+destination.String())
}
generic.TreeUnifyPath(ctx, originTree, p, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert).SetNoRemap(testCase.noremap))
assert.EqualValues(t, testCase.expected, collected)
collected = make([]string, 0, 10)
generic.TreeUnifyPath(ctx, originTree, p, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert).SetNoRemap(testCase.noremap))
assert.EqualValues(t, testCase.expected, collected)
})
}
}
func TestUnifyPathRelative(t *testing.T) {
ctx := context.Background()
for _, testCase := range []struct {
start string
destination string
expected []string
}{
{
start: "/O-A/O-B",
destination: "O-C",
expected: []string{"cd: /O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "unify: /O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "unify: /O-A/O-B/O-C:O-C=content O-C => /D-A/D-B/D-C:D-C=content O-C"},
},
{
start: "/O-A/O-B",
destination: ".",
expected: []string{"cd: /O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "unify: /O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B"},
},
{
start: "/O-A/O-B",
destination: "../O-F/O-G",
expected: []string{"cd: /O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "unify: /O-A:O-A=content O-A => /D-A:D-A=content O-A", "unify: /O-A/O-F:O-F=content O-F => /D-A/D-C:D-C=content O-F", "unify: /O-A/O-F/O-G:O-G=content O-G => /D-A/D-C/D-D:D-D=content O-G"},
},
{
start: "/O-A/O-B/O-C",
destination: "../O-E",
expected: []string{"cd: /O-A/O-B/O-C:O-C=content O-C => /D-A/D-B/D-C:D-C=content O-C", "unify: /O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "unify: /O-A/O-B/O-E:O-E=content O-E => /D-A/D-B/D-D:D-D=content O-E"},
},
} {
t.Run(" "+testCase.start+" => "+testCase.destination, func(t *testing.T) {
originTree := NewMemoryTree(ctx, "O")
testTreeBuild(t, originTree, 2)
destinationTree := NewMemoryTree(ctx, "D")
var collected []string
start := generic.NewPathFromString(testCase.start)
collect := func(prefix string, origin, destination generic.NodeInterface) {
originPath := origin.GetCurrentPath().String()
destinationPath := destination.GetCurrentPath().String()
collected = append(collected, prefix+originPath+":"+origin.GetSelf().String()+" => "+destinationPath+":"+destination.GetSelf().String())
}
//
// Unify testCase.start
//
upsert := func(ctx context.Context, origin generic.NodeInterface, originParent path.Path, destination generic.NodeInterface, destinationParent path.Path) {
collect("unify: ", origin, destination)
}
generic.TreeUnifyPath(ctx, originTree, start, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert))
//
// Beginning from testCase.start, unify testCase.destination
//
cd := func(ctx context.Context, origin, destination generic.NodeInterface) {
collect("cd: ", origin, destination)
path := generic.NewPathFromString(testCase.destination)
originParent := origin.GetParent().GetCurrentPath()
destinationParent := destination.GetParent().GetCurrentPath()
generic.NodeUnifyPath(ctx, origin.GetSelf(), originParent, path, destinationParent, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert))
}
//
//
//
collected = make([]string, 0, 10)
generic.TreeParallelApply(ctx, originTree, start, destinationTree, generic.NewParallelApplyOptions(cd))
assert.EqualValues(t, testCase.expected, collected)
//
// Do it twice to verify it is idempotent
//
collected = make([]string, 0, 10)
generic.TreeParallelApply(ctx, originTree, start, destinationTree, generic.NewParallelApplyOptions(cd))
assert.EqualValues(t, testCase.expected, collected)
})
}
}
func TestUnifyPathScenario(t *testing.T) {
ctx := context.Background()
//
// build and populate a tree
//
originTree := NewMemoryTree(ctx, "O")
testTreeBuild(t, originTree, 2)
//
// build an empty tree
//
destinationTree := NewMemoryTree(ctx, "D")
//
// accumulate the call results for verification
//
var collected []string
upsert := func(ctx context.Context, origin generic.NodeInterface, originPath path.Path, destination generic.NodeInterface, destinationPath path.Path) {
originPath = originPath.Append(origin)
destinationPath = destinationPath.Append(destination)
what := originPath.String() + ":" + origin.String() + " => " + destinationPath.String() + ":" + destination.String()
fmt.Printf("unify: %T => %T | %s\n", origin, destination, what)
collected = append(collected, what)
}
assertTree := func(tree generic.TreeInterface, expected []string) {
collected := make([]string, 0, 10)
tree.Walk(ctx, generic.NewWalkOptions(func(ctx context.Context, path path.Path, node generic.NodeInterface) {
if node.GetKind() == kind.KindRoot {
return
}
path = path.Append(node)
collected = append(collected, path.String()+":"+node.String())
}))
sort.Strings(collected)
assert.EqualValues(t, expected, collected)
}
//
// unify the originTree with the destinationTree on the specified path
//
fullPath := generic.NewPathFromString("/O-A/O-B/O-C")
collected = make([]string, 0, 10)
generic.TreeUnifyPath(ctx, originTree, fullPath, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert))
sort.Strings(collected)
assert.EqualValues(t, []string{"/O-A/O-B/O-C:O-C=content O-C => /D-A/D-B/D-C:D-C=content O-C", "/O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "/O-A:O-A=content O-A => /D-A:D-A=content O-A"}, collected)
assertTree(destinationTree, []string{"/D-A/D-B/D-C:D-C=content O-C", "/D-A/D-B:D-B=content O-B", "/D-A:D-A=content O-A"})
//
// Add a node unrelated to the unification path
//
var unrelatedOriginPath path.Path
{
originTree.Apply(ctx, generic.NewPathFromString("/O-A/O-B"), generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
assert.EqualValues(t, parent.Length()+1, node.GetCurrentPath().Length())
unrelated := node.CreateChild(ctx)
unrelated.Upsert(ctx)
memory.SetContent(unrelated, "content "+unrelated.GetID().String())
unrelated.Upsert(ctx)
unrelatedOriginPath = unrelated.GetCurrentPath()
assert.EqualValues(t, "/O-A/O-B/O-N", (unrelatedOriginPath.PathString().Join()))
}))
}
//
// Replace the content of the last node
//
lastContent := "LAST"
{
lastPath := generic.NewPathFromString("/O-A/O-B/O-C")
originTree.Apply(ctx, lastPath, generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
memory.SetContent(node, lastContent)
}))
collected = make([]string, 0, 10)
generic.TreeUnifyPath(ctx, originTree, lastPath, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert))
sort.Strings(collected)
assert.EqualValues(t, []string{"/O-A/O-B/O-C:O-C=LAST => /D-A/D-B/D-C:D-C=LAST", "/O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "/O-A:O-A=content O-A => /D-A:D-A=content O-A"}, collected)
assertTree(destinationTree, []string{"/D-A/D-B/D-C:D-C=LAST", "/D-A/D-B:D-B=content O-B", "/D-A:D-A=content O-A"})
}
//
// Replace the content of the first node
//
firstContent := "FIRST"
{
firstPath := generic.NewPathFromString("/O-A")
originTree.Apply(ctx, firstPath, generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
memory.SetContent(node, firstContent)
}))
collected = make([]string, 0, 10)
generic.TreeUnifyPath(ctx, originTree, firstPath, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert))
sort.Strings(collected)
assert.EqualValues(t, []string{"/O-A:O-A=" + firstContent + " => /D-A:D-A=" + firstContent}, collected)
assertTree(destinationTree, []string{"/D-A/D-B/D-C:D-C=LAST", "/D-A/D-B:D-B=content O-B", "/D-A:D-A=FIRST"})
}
//
// Replace the content of the second node
//
secondContent := "SECOND"
{
secondPath := generic.NewPathFromString("/O-A/O-B")
originTree.Apply(ctx, secondPath, generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
memory.SetContent(node, secondContent)
}))
collected = make([]string, 0, 10)
generic.TreeUnifyPath(ctx, originTree, secondPath, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert))
assert.EqualValues(t, []string{"/O-A:O-A=" + firstContent + " => /D-A:D-A=" + firstContent, "/O-A/O-B:O-B=" + secondContent + " => /D-A/D-B:D-B=" + secondContent}, collected)
sort.Strings(collected)
assertTree(destinationTree, []string{"/D-A/D-B/D-C:D-C=LAST", "/D-A/D-B:D-B=SECOND", "/D-A:D-A=FIRST"})
}
//
// verify the node unrelated to the unification is still there
//
{
var found bool
originTree.Apply(ctx, unrelatedOriginPath, generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
found = true
}))
assert.True(t, found)
}
}
func TestUnifyMirror(t *testing.T) {
ctx := context.Background()
//
// build and populate a tree
//
originTree := NewMemoryTree(ctx, "O")
log := originTree.GetLogger()
testTreeBuild(t, originTree, 2)
//
// build an empty tree
//
destinationTree := NewMemoryTree(ctx, "D")
upsert := func(ctx context.Context, origin generic.NodeInterface, originPath path.Path, destination generic.NodeInterface, destinationPath path.Path) {
assert.NotNil(t, origin.GetDriver().(*memory.Driver))
assert.NotNil(t, destination.GetDriver().(*memory.Driver))
originPath = originPath.Append(origin)
destinationPath = destinationPath.Append(destination)
what := fmt.Sprintf("%s:%s => %s:%s", originPath, origin.GetSelf(), destinationPath, destination.GetSelf())
log.Trace("mirror upsert: %T => %T | %s", origin, destination, what)
}
delete := func(ctx context.Context, destination generic.NodeInterface, destinationPath path.Path) {
assert.NotNil(t, destination.GetDriver().(*memory.Driver))
destinationPath = destinationPath.Append(destination)
log.Trace("mirror delete: %T | %s:%s", destination, destinationPath, destination)
}
var sameTree func(origin, destination generic.NodeInterface) bool
sameTree = func(origin, destination generic.NodeInterface) bool {
what := origin.GetCurrentPath().String() + ":" + origin.GetSelf().String() + " => " + destination.GetCurrentPath().String() + ":" + destination.GetSelf().String()
log.Trace("sameTree: %T => %T | %s", origin.GetSelf(), destination.GetSelf(), what)
if origin.GetMappedID() != destination.GetID() {
log.Trace("sameTree: different: %s != %s", origin.GetMappedID(), destination.GetID())
return false
}
originChildren := origin.GetChildren()
destinationChildren := destination.GetChildren()
if len(originChildren) != len(destinationChildren) {
log.Trace("sameTree: different: length %v != %v", len(originChildren), len(destinationChildren))
return false
}
for _, originChild := range originChildren {
destinationChild := destination.GetChild(originChild.GetMappedID())
if destinationChild == generic.NilNode {
log.Trace("sameTree: different: %s not found", originChild.GetMappedID())
return false
}
if !sameTree(originChild, destinationChild) {
return false
}
}
return true
}
//
// unify the originTree with the destinationTree
//
assert.False(t, sameTree(originTree.GetRoot(), destinationTree.GetRoot()))
generic.TreeUnify(ctx, originTree, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert).SetDelete(delete))
assert.True(t, sameTree(originTree.GetRoot(), destinationTree.GetRoot()))
generic.TreeUnify(ctx, originTree, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert).SetDelete(delete))
assert.True(t, sameTree(originTree.GetRoot(), destinationTree.GetRoot()))
{
addNode := func(tree generic.TreeInterface, pathString string) {
tree.Apply(ctx, generic.NewPathFromString(pathString), generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
new := node.CreateChild(ctx)
new.Upsert(ctx)
memory.SetContent(new, "content "+new.GetID().String())
new.Upsert(ctx)
log.Trace("add: %s", parent.ReadableString())
log.Trace("add: %s", node.GetCurrentPath().ReadableString())
log.Trace("add: %s", new.GetCurrentPath().ReadableString())
}))
}
for _, testCase := range []struct {
existingPath string
newPath string
}{
{
existingPath: "/O-A/O-B",
newPath: "/D-A/D-B/D-N",
},
{
existingPath: "/O-A",
newPath: "/D-A/D-O",
},
{
existingPath: "/O-A/O-J/O-K",
newPath: "/D-A/D-J/D-K/D-P",
},
} {
t.Run("add"+testCase.newPath, func(t *testing.T) {
destinationPath := generic.NewPathFromString(testCase.newPath)
addNode(originTree, testCase.existingPath)
assert.False(t, sameTree(originTree.GetRoot(), destinationTree.GetRoot()))
assert.False(t, destinationTree.Exists(ctx, destinationPath), destinationPath.String())
generic.TreeUnify(ctx, originTree, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert).SetDelete(delete))
assert.True(t, destinationTree.Exists(ctx, destinationPath), destinationPath.String())
assert.True(t, sameTree(originTree.GetRoot(), destinationTree.GetRoot()))
})
}
}
{
deleteNode := func(tree generic.TreeInterface, toDelete path.Path) {
tree.Apply(ctx, toDelete, generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
node.Delete(ctx)
}))
}
for _, testCase := range []struct {
originPath string
destinationPath string
}{
{
originPath: "/O-A/O-F",
destinationPath: "/D-A/D-F",
},
} {
t.Run("delete"+testCase.originPath, func(t *testing.T) {
originPath := generic.NewPathFromString(testCase.originPath)
destinationPath := generic.NewPathFromString(testCase.destinationPath)
assert.True(t, originTree.Exists(ctx, originPath))
assert.True(t, destinationTree.Exists(ctx, destinationPath), destinationPath.String())
deleteNode(originTree, originPath)
assert.False(t, originTree.Exists(ctx, originPath))
generic.TreeUnify(ctx, originTree, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert).SetDelete(delete))
assert.False(t, destinationTree.Exists(ctx, destinationPath), destinationPath.String())
})
}
}
}
func TestNodeParallelApplyFound(t *testing.T) {
ctx := context.Background()
for _, testCase := range []struct {
start string
destination string
expected []string
expectedRemapped string
}{
{
start: "/O-A/O-B",
destination: "O-C",
expected: []string{"/O-A/O-B => /D-A/D-B", "/O-A/O-B/O-C => /D-A/D-B/D-C"},
expectedRemapped: "/D-A/D-B/D-C",
},
{
start: "/O-A/O-B/O-D",
destination: ".",
expected: []string{"/O-A/O-B/O-D => /D-A/D-B/D-D"},
expectedRemapped: "/D-A/D-B/D-D",
},
{
start: ".",
destination: ".",
expected: []string{" => "},
expectedRemapped: "",
},
{
start: "/O-A/O-B/O-C",
destination: "../O-D",
expected: []string{"/O-A/O-B => /D-A/D-B", "/O-A/O-B/O-D => /D-A/D-B/D-D"},
expectedRemapped: "/D-A/D-B/D-D",
},
{
start: "/",
destination: ".",
expected: []string{" => "},
expectedRemapped: "",
},
} {
t.Run(" "+testCase.start+" => "+testCase.destination, func(t *testing.T) {
originTree := NewMemoryTree(ctx, "O")
testTreeBuild(t, originTree, 2)
destinationTree := NewMemoryTree(ctx, "D")
//
// Mirror two trees
//
generic.TreeMirror(ctx, originTree, destinationTree, generic.NewPathFromString("/"), generic.NewMirrorOptions())
//
// collect all nodes traversed by the apply function along testCase.destination
//
collected := make([]string, 0, 10)
collect := func(ctx context.Context, origin, destination generic.NodeInterface) {
collected = append(collected, fmt.Sprintf("%s => %s", origin.GetCurrentPath().String(), destination.GetCurrentPath().String()))
}
//
// get to testCase.start and from there run the apply function to reach testCase.destination
//
nodeApply := func(ctx context.Context, origin, destination generic.NodeInterface) {
assert.True(t, generic.NodeParallelApply(ctx, origin, generic.NewPathFromString(testCase.destination), destination, generic.NewParallelApplyOptions(collect).SetWhere(generic.ApplyEachNode)))
}
assert.True(t, generic.TreeParallelApply(ctx, originTree, generic.NewPathFromString(testCase.start), destinationTree, generic.NewParallelApplyOptions(nodeApply)))
assert.EqualValues(t, testCase.expected, collected)
//
// test TreePathRemap
//
remappedPath := generic.TreePathRemap(ctx, originTree, generic.NewPathFromString(filepath.Join(testCase.start, testCase.destination)), destinationTree)
assert.EqualValues(t, testCase.expectedRemapped, remappedPath.String())
})
}
}
func TestNodeParallelApplyNoRemap(t *testing.T) {
ctx := context.Background()
for _, testCase := range []struct {
noremap bool
start string
expected []string
expectedRemapped string
}{
{
noremap: false,
start: "/O-A/O-B",
expected: []string{" => ", "/O-A => /D-A", "/O-A/O-B => /D-A/D-B"},
expectedRemapped: "/D-A/D-B",
},
{
noremap: true,
start: "/O-A/O-B",
expected: []string{" => ", "/O-A => /O-A", "/O-A/O-B => /O-A/O-B"},
expectedRemapped: "/O-A/O-B",
},
} {
t.Run(fmt.Sprintf("noremap=%v,start=%s", testCase.noremap, testCase.start), func(t *testing.T) {
originName := "O"
originTree := NewMemoryTree(ctx, originName)
testTreeBuild(t, originTree, 2)
var destinationName string
if testCase.noremap {
destinationName = originName
} else {
destinationName = "D"
}
destinationTree := NewMemoryTree(ctx, destinationName)
//
// Mirror two trees
//
generic.TreeMirror(ctx, originTree, destinationTree, generic.NewPathFromString("/"), generic.NewMirrorOptions())
//
// collect all nodes traversed by the apply function along testCase.destination
//
collected := make([]string, 0, 10)
collect := func(ctx context.Context, origin, destination generic.NodeInterface) {
collected = append(collected, fmt.Sprintf("%s => %s", origin.GetCurrentPath(), destination.GetCurrentPath()))
}
//
// get to testCase.start and from there run the apply function to reach testCase.destination
//
assert.True(t, generic.TreeParallelApply(ctx, originTree, generic.NewPathFromString(testCase.start), destinationTree, generic.NewParallelApplyOptions(collect).SetWhere(generic.ApplyEachNode).SetNoRemap(testCase.noremap)))
assert.EqualValues(t, testCase.expected, collected)
//
// test TreePathRemap
//
remappedPath := generic.TreePathRemap(ctx, originTree, generic.NewPathFromString(testCase.start), destinationTree)
assert.EqualValues(t, testCase.expectedRemapped, remappedPath.String())
})
}
}
func TestNodeParallelApplyNotFound(t *testing.T) {
ctx := context.Background()
for _, testCase := range []struct {
start string
destination string
}{
{
start: "/O-A",
destination: "O-B/???",
},
{
start: "/O-A/O-B",
destination: "???",
},
{
start: "/O-A/O-B",
destination: "../???",
},
} {
t.Run(" "+testCase.start+" => "+testCase.destination, func(t *testing.T) {
originTree := NewMemoryTree(ctx, "O")
testTreeBuild(t, originTree, 2)
destinationTree := NewMemoryTree(ctx, "D")
//
// Mirror two trees
//
generic.TreeMirror(ctx, originTree, destinationTree, generic.NewPathFromString("/"), generic.NewMirrorOptions())
//
// get to testCase.start and from there run the apply function to reach testCase.destination
//
var called bool
nodeApply := func(ctx context.Context, origin, destination generic.NodeInterface) {
called = true
found := generic.NodeParallelApply(ctx, origin, generic.NewPathFromString(testCase.destination), destination, generic.NewParallelApplyOptions(nil))
assert.False(t, found)
}
assert.True(t, generic.TreeParallelApply(ctx, originTree, generic.NewPathFromString(testCase.start), destinationTree, generic.NewParallelApplyOptions(nodeApply)))
assert.True(t, called)
})
}
}