github.com/neohugo/neohugo@v0.123.8/hugolib/doctree/nodeshiftree_test.go (about)

     1  // Copyright 2024 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package doctree_test
    15  
    16  import (
    17  	"context"
    18  	"fmt"
    19  	"math/rand"
    20  	"path"
    21  	"strings"
    22  	"testing"
    23  
    24  	qt "github.com/frankban/quicktest"
    25  	"github.com/google/go-cmp/cmp"
    26  	"github.com/neohugo/neohugo/common/para"
    27  	"github.com/neohugo/neohugo/hugolib/doctree"
    28  )
    29  
    30  var eq = qt.CmpEquals(
    31  	cmp.Comparer(func(n1, n2 *testValue) bool {
    32  		if n1 == n2 {
    33  			return true
    34  		}
    35  
    36  		return n1.ID == n2.ID && n1.Lang == n2.Lang
    37  	}),
    38  )
    39  
    40  func TestTree(t *testing.T) {
    41  	c := qt.New(t)
    42  
    43  	zeroZero := doctree.New(
    44  		doctree.Config[*testValue]{
    45  			Shifter: &testShifter{},
    46  		},
    47  	)
    48  
    49  	a := &testValue{ID: "/a"}
    50  	zeroZero.InsertIntoValuesDimension("/a", a)
    51  	ab := &testValue{ID: "/a/b"}
    52  	zeroZero.InsertIntoValuesDimension("/a/b", ab)
    53  
    54  	c.Assert(zeroZero.Get("/a"), eq, &testValue{ID: "/a", Lang: 0})
    55  	s, v := zeroZero.LongestPrefix("/a/b/c", true, nil)
    56  	c.Assert(v, eq, ab)
    57  	c.Assert(s, eq, "/a/b")
    58  
    59  	// Change language.
    60  	oneZero := zeroZero.Increment(0)
    61  	c.Assert(zeroZero.Get("/a"), eq, &testValue{ID: "/a", Lang: 0})
    62  	c.Assert(oneZero.Get("/a"), eq, &testValue{ID: "/a", Lang: 1})
    63  }
    64  
    65  func TestTreeData(t *testing.T) {
    66  	c := qt.New(t)
    67  
    68  	tree := doctree.New(
    69  		doctree.Config[*testValue]{
    70  			Shifter: &testShifter{},
    71  		},
    72  	)
    73  
    74  	tree.InsertIntoValuesDimension("", &testValue{ID: "HOME"})
    75  	tree.InsertIntoValuesDimension("/a", &testValue{ID: "/a"})
    76  	tree.InsertIntoValuesDimension("/a/b", &testValue{ID: "/a/b"})
    77  	tree.InsertIntoValuesDimension("/b", &testValue{ID: "/b"})
    78  	tree.InsertIntoValuesDimension("/b/c", &testValue{ID: "/b/c"})
    79  	tree.InsertIntoValuesDimension("/b/c/d", &testValue{ID: "/b/c/d"})
    80  
    81  	var values []string
    82  
    83  	ctx := &doctree.WalkContext[*testValue]{}
    84  
    85  	w := &doctree.NodeShiftTreeWalker[*testValue]{
    86  		Tree:        tree,
    87  		WalkContext: ctx,
    88  		Handle: func(s string, t *testValue, match doctree.DimensionFlag) (bool, error) {
    89  			ctx.Data().Insert(s, map[string]any{
    90  				"id": t.ID,
    91  			})
    92  
    93  			if s != "" {
    94  				p, v := ctx.Data().LongestPrefix(path.Dir(s))
    95  				values = append(values, fmt.Sprintf("%s:%s:%v", s, p, v))
    96  			}
    97  			return false, nil
    98  		},
    99  	}
   100  
   101  	w.Walk(context.Background()) // nolint
   102  
   103  	c.Assert(strings.Join(values, "|"), qt.Equals, "/a::map[id:HOME]|/a/b:/a:map[id:/a]|/b::map[id:HOME]|/b/c:/b:map[id:/b]|/b/c/d:/b/c:map[id:/b/c]")
   104  }
   105  
   106  func TestTreeEvents(t *testing.T) {
   107  	c := qt.New(t)
   108  
   109  	tree := doctree.New(
   110  		doctree.Config[*testValue]{
   111  			Shifter: &testShifter{echo: true},
   112  		},
   113  	)
   114  
   115  	tree.InsertIntoValuesDimension("/a", &testValue{ID: "/a", Weight: 2, IsBranch: true})
   116  	tree.InsertIntoValuesDimension("/a/p1", &testValue{ID: "/a/p1", Weight: 5})
   117  	tree.InsertIntoValuesDimension("/a/p", &testValue{ID: "/a/p2", Weight: 6})
   118  	tree.InsertIntoValuesDimension("/a/s1", &testValue{ID: "/a/s1", Weight: 5, IsBranch: true})
   119  	tree.InsertIntoValuesDimension("/a/s1/p1", &testValue{ID: "/a/s1/p1", Weight: 8})
   120  	tree.InsertIntoValuesDimension("/a/s1/p1", &testValue{ID: "/a/s1/p2", Weight: 9})
   121  	tree.InsertIntoValuesDimension("/a/s1/s2", &testValue{ID: "/a/s1/s2", Weight: 6, IsBranch: true})
   122  	tree.InsertIntoValuesDimension("/a/s1/s2/p1", &testValue{ID: "/a/s1/s2/p1", Weight: 8})
   123  	tree.InsertIntoValuesDimension("/a/s1/s2/p2", &testValue{ID: "/a/s1/s2/p2", Weight: 7})
   124  
   125  	w := &doctree.NodeShiftTreeWalker[*testValue]{
   126  		Tree:        tree,
   127  		WalkContext: &doctree.WalkContext[*testValue]{},
   128  	}
   129  
   130  	w.Handle = func(s string, t *testValue, match doctree.DimensionFlag) (bool, error) {
   131  		if t.IsBranch {
   132  			w.WalkContext.AddEventListener("weight", s, func(e *doctree.Event[*testValue]) {
   133  				if e.Source.Weight > t.Weight {
   134  					t.Weight = e.Source.Weight
   135  					w.WalkContext.SendEvent(&doctree.Event[*testValue]{Source: t, Path: s, Name: "weight"})
   136  				}
   137  				// Reduces the amount of events bubbling up the tree. If the weight for this branch has
   138  				// increased, that will be announced in its own event.
   139  				e.StopPropagation()
   140  			})
   141  		} else {
   142  			w.WalkContext.SendEvent(&doctree.Event[*testValue]{Source: t, Path: s, Name: "weight"})
   143  		}
   144  
   145  		return false, nil
   146  	}
   147  
   148  	c.Assert(w.Walk(context.Background()), qt.IsNil)
   149  	c.Assert(w.WalkContext.HandleEventsAndHooks(), qt.IsNil)
   150  
   151  	c.Assert(tree.Get("/a").Weight, eq, 9)
   152  	c.Assert(tree.Get("/a/s1").Weight, eq, 9)
   153  	c.Assert(tree.Get("/a/p").Weight, eq, 6)
   154  	c.Assert(tree.Get("/a/s1/s2").Weight, eq, 8)
   155  	c.Assert(tree.Get("/a/s1/s2/p2").Weight, eq, 7)
   156  }
   157  
   158  func TestTreeInsert(t *testing.T) {
   159  	c := qt.New(t)
   160  
   161  	tree := doctree.New(
   162  		doctree.Config[*testValue]{
   163  			Shifter: &testShifter{},
   164  		},
   165  	)
   166  
   167  	a := &testValue{ID: "/a"}
   168  	tree.InsertIntoValuesDimension("/a", a)
   169  	ab := &testValue{ID: "/a/b"}
   170  	tree.InsertIntoValuesDimension("/a/b", ab)
   171  
   172  	c.Assert(tree.Get("/a"), eq, &testValue{ID: "/a", Lang: 0})
   173  	c.Assert(tree.Get("/notfound"), qt.IsNil)
   174  
   175  	ab2 := &testValue{ID: "/a/b", Lang: 0}
   176  	v, ok := tree.InsertIntoValuesDimension("/a/b", ab2)
   177  	c.Assert(ok, qt.IsTrue)
   178  	c.Assert(v, qt.DeepEquals, ab2)
   179  
   180  	tree1 := tree.Increment(0)
   181  	c.Assert(tree1.Get("/a/b"), qt.DeepEquals, &testValue{ID: "/a/b", Lang: 1})
   182  }
   183  
   184  func TestTreePara(t *testing.T) {
   185  	c := qt.New(t)
   186  
   187  	p := para.New(4)
   188  	r, _ := p.Start(context.Background())
   189  
   190  	tree := doctree.New(
   191  		doctree.Config[*testValue]{
   192  			Shifter: &testShifter{},
   193  		},
   194  	)
   195  
   196  	for i := 0; i < 8; i++ {
   197  		i := i
   198  		r.Run(func() error {
   199  			a := &testValue{ID: "/a"}
   200  			lock := tree.Lock(true)
   201  			defer lock()
   202  			tree.InsertIntoValuesDimension("/a", a)
   203  			ab := &testValue{ID: "/a/b"}
   204  			tree.InsertIntoValuesDimension("/a/b", ab)
   205  
   206  			key := fmt.Sprintf("/a/b/c/%d", i)
   207  			val := &testValue{ID: key}
   208  			tree.InsertIntoValuesDimension(key, val)
   209  			c.Assert(tree.Get(key), eq, val)
   210  			// s, _ := tree.LongestPrefix(key, nil)
   211  			// c.Assert(s, eq, "/a/b")
   212  
   213  			return nil
   214  		})
   215  	}
   216  
   217  	c.Assert(r.Wait(), qt.IsNil)
   218  }
   219  
   220  func TestValidateKey(t *testing.T) {
   221  	c := qt.New(t)
   222  
   223  	c.Assert(doctree.ValidateKey(""), qt.IsNil)
   224  	c.Assert(doctree.ValidateKey("/a/b/c"), qt.IsNil)
   225  	c.Assert(doctree.ValidateKey("/"), qt.IsNotNil)
   226  	c.Assert(doctree.ValidateKey("a"), qt.IsNotNil)
   227  	c.Assert(doctree.ValidateKey("abc"), qt.IsNotNil)
   228  	c.Assert(doctree.ValidateKey("/abc/"), qt.IsNotNil)
   229  }
   230  
   231  type testShifter struct {
   232  	echo bool
   233  }
   234  
   235  func (s *testShifter) ForEeachInDimension(n *testValue, d int, f func(n *testValue) bool) {
   236  	if d != doctree.DimensionLanguage.Index() {
   237  		panic("not implemented")
   238  	}
   239  	f(n)
   240  }
   241  
   242  func (s *testShifter) Insert(old, new *testValue) *testValue {
   243  	return new
   244  }
   245  
   246  func (s *testShifter) InsertInto(old, new *testValue, dimension doctree.Dimension) *testValue {
   247  	return new
   248  }
   249  
   250  func (s *testShifter) Delete(n *testValue, dimension doctree.Dimension) (bool, bool) {
   251  	return true, true
   252  }
   253  
   254  func (s *testShifter) Shift(n *testValue, dimension doctree.Dimension, exact bool) (*testValue, bool, doctree.DimensionFlag) {
   255  	if s.echo {
   256  		return n, true, doctree.DimensionLanguage
   257  	}
   258  	if n.NoCopy {
   259  		if n.Lang == dimension[0] {
   260  			return n, true, doctree.DimensionLanguage
   261  		}
   262  		return nil, false, doctree.DimensionLanguage
   263  	}
   264  	c := *n
   265  	c.Lang = dimension[0]
   266  	return &c, true, doctree.DimensionLanguage
   267  }
   268  
   269  func (s *testShifter) All(n *testValue) []*testValue {
   270  	return []*testValue{n}
   271  }
   272  
   273  type testValue struct {
   274  	ID   string
   275  	Lang int
   276  
   277  	Weight   int
   278  	IsBranch bool
   279  
   280  	NoCopy bool
   281  }
   282  
   283  func BenchmarkTreeInsert(b *testing.B) {
   284  	runBench := func(b *testing.B, numElements int) {
   285  		for i := 0; i < b.N; i++ {
   286  			tree := doctree.New(
   287  				doctree.Config[*testValue]{
   288  					Shifter: &testShifter{},
   289  				},
   290  			)
   291  
   292  			for i := 0; i < numElements; i++ {
   293  				lang := rand.Intn(2)
   294  				tree.InsertIntoValuesDimension(fmt.Sprintf("/%d", i), &testValue{ID: fmt.Sprintf("/%d", i), Lang: lang, Weight: i, NoCopy: true})
   295  			}
   296  		}
   297  	}
   298  
   299  	b.Run("1000", func(b *testing.B) {
   300  		runBench(b, 1000)
   301  	})
   302  
   303  	b.Run("10000", func(b *testing.B) {
   304  		runBench(b, 10000)
   305  	})
   306  
   307  	b.Run("100000", func(b *testing.B) {
   308  		runBench(b, 100000)
   309  	})
   310  
   311  	b.Run("300000", func(b *testing.B) {
   312  		runBench(b, 300000)
   313  	})
   314  }
   315  
   316  func BenchmarkWalk(b *testing.B) {
   317  	const numElements = 1000
   318  
   319  	createTree := func() *doctree.NodeShiftTree[*testValue] {
   320  		tree := doctree.New(
   321  			doctree.Config[*testValue]{
   322  				Shifter: &testShifter{},
   323  			},
   324  		)
   325  
   326  		for i := 0; i < numElements; i++ {
   327  			lang := rand.Intn(2)
   328  			tree.InsertIntoValuesDimension(fmt.Sprintf("/%d", i), &testValue{ID: fmt.Sprintf("/%d", i), Lang: lang, Weight: i, NoCopy: true})
   329  		}
   330  
   331  		return tree
   332  	}
   333  
   334  	handle := func(s string, t *testValue, match doctree.DimensionFlag) (bool, error) {
   335  		return false, nil
   336  	}
   337  
   338  	for _, numElements := range []int{1000, 10000, 100000} {
   339  
   340  		b.Run(fmt.Sprintf("Walk one dimension %d", numElements), func(b *testing.B) {
   341  			tree := createTree()
   342  			b.ResetTimer()
   343  			for i := 0; i < b.N; i++ {
   344  				w := &doctree.NodeShiftTreeWalker[*testValue]{
   345  					Tree:   tree,
   346  					Handle: handle,
   347  				}
   348  				if err := w.Walk(context.Background()); err != nil {
   349  					b.Fatal(err)
   350  				}
   351  			}
   352  		})
   353  
   354  		b.Run(fmt.Sprintf("Walk all dimensions %d", numElements), func(b *testing.B) {
   355  			base := createTree()
   356  			b.ResetTimer()
   357  			for i := 0; i < b.N; i++ {
   358  				for d1 := 0; d1 < 1; d1++ {
   359  					for d2 := 0; d2 < 2; d2++ {
   360  						tree := base.Shape(d1, d2)
   361  						w := &doctree.NodeShiftTreeWalker[*testValue]{
   362  							Tree:   tree,
   363  							Handle: handle,
   364  						}
   365  						if err := w.Walk(context.Background()); err != nil {
   366  							b.Fatal(err)
   367  						}
   368  					}
   369  				}
   370  			}
   371  		})
   372  
   373  	}
   374  }