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 }