github.com/neohugo/neohugo@v0.123.8/hugolib/testhelpers_test.go (about) 1 package hugolib 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "image/jpeg" 8 "io" 9 "io/fs" 10 "math/rand" 11 "os" 12 "path/filepath" 13 "regexp" 14 "strings" 15 "testing" 16 "text/template" 17 "time" 18 19 "github.com/neohugo/neohugo/config/allconfig" 20 "github.com/neohugo/neohugo/config/security" 21 "github.com/neohugo/neohugo/htesting" 22 23 "github.com/neohugo/neohugo/output" 24 25 "github.com/google/go-cmp/cmp" 26 "github.com/neohugo/neohugo/parser/metadecoders" 27 28 "github.com/neohugo/neohugo/parser" 29 30 "github.com/fsnotify/fsnotify" 31 "github.com/neohugo/neohugo/common/hexec" 32 "github.com/neohugo/neohugo/common/loggers" 33 "github.com/neohugo/neohugo/common/maps" 34 "github.com/neohugo/neohugo/config" 35 "github.com/neohugo/neohugo/deps" 36 "github.com/neohugo/neohugo/resources/page" 37 "github.com/sanity-io/litter" 38 "github.com/spf13/afero" 39 "github.com/spf13/cast" 40 41 "github.com/neohugo/neohugo/helpers" 42 43 "github.com/neohugo/neohugo/resources/resource" 44 45 qt "github.com/frankban/quicktest" 46 "github.com/neohugo/neohugo/hugofs" 47 ) 48 49 var ( 50 deepEqualsPages = qt.CmpEquals(cmp.Comparer(func(p1, p2 *pageState) bool { return p1 == p2 })) 51 deepEqualsOutputFormats = qt.CmpEquals(cmp.Comparer(func(o1, o2 output.Format) bool { 52 return o1.Name == o2.Name && o1.MediaType.Type == o2.MediaType.Type 53 })) 54 ) 55 56 type sitesBuilder struct { 57 Cfg config.Provider 58 Configs *allconfig.Configs 59 60 environ []string 61 62 Fs *hugofs.Fs 63 T testing.TB 64 depsCfg deps.DepsCfg 65 66 *qt.C 67 68 logger loggers.Logger 69 rnd *rand.Rand 70 dumper litter.Options 71 72 // Used to test partial rebuilds. 73 changedFiles []string 74 removedFiles []string 75 76 // Aka the Hugo server mode. 77 running bool 78 79 H *HugoSites 80 81 theme string 82 83 // Default toml 84 configFormat string 85 configFileSet bool 86 configSet bool 87 88 // Default is empty. 89 // TODO(bep) revisit this and consider always setting it to something. 90 // Consider this in relation to using the BaseFs.PublishFs to all publishing. 91 workingDir string 92 93 addNothing bool 94 // Base data/content 95 contentFilePairs []filenameContent 96 templateFilePairs []filenameContent 97 i18nFilePairs []filenameContent 98 dataFilePairs []filenameContent 99 100 // Additional data/content. 101 // As in "use the base, but add these on top". 102 contentFilePairsAdded []filenameContent 103 templateFilePairsAdded []filenameContent 104 i18nFilePairsAdded []filenameContent 105 dataFilePairsAdded []filenameContent 106 } 107 108 type filenameContent struct { 109 filename string 110 content string 111 } 112 113 func newTestSitesBuilder(t testing.TB) *sitesBuilder { 114 v := config.New() 115 v.Set("publishDir", "public") 116 v.Set("disableLiveReload", true) 117 fs := hugofs.NewFromOld(afero.NewMemMapFs(), v) 118 119 litterOptions := litter.Options{ 120 HidePrivateFields: true, 121 StripPackageNames: true, 122 Separator: " ", 123 } 124 125 return &sitesBuilder{ 126 T: t, C: qt.New(t), Fs: fs, configFormat: "toml", 127 dumper: litterOptions, rnd: rand.New(rand.NewSource(time.Now().Unix())), 128 } 129 } 130 131 func newTestSitesBuilderFromDepsCfg(t testing.TB, d deps.DepsCfg) *sitesBuilder { 132 c := qt.New(t) 133 134 litterOptions := litter.Options{ 135 HidePrivateFields: true, 136 StripPackageNames: true, 137 Separator: " ", 138 } 139 140 b := &sitesBuilder{T: t, C: c, depsCfg: d, Fs: d.Fs, dumper: litterOptions, rnd: rand.New(rand.NewSource(time.Now().Unix()))} 141 workingDir := d.Configs.LoadingInfo.BaseConfig.WorkingDir 142 143 b.WithWorkingDir(workingDir) 144 145 //nolint 146 return b 147 } 148 149 func (s *sitesBuilder) Running() *sitesBuilder { 150 s.running = true 151 return s 152 } 153 154 func (s *sitesBuilder) WithNothingAdded() *sitesBuilder { 155 s.addNothing = true 156 return s 157 } 158 159 func (s *sitesBuilder) WithLogger(logger loggers.Logger) *sitesBuilder { 160 s.logger = logger 161 return s 162 } 163 164 func (s *sitesBuilder) WithWorkingDir(dir string) *sitesBuilder { 165 s.workingDir = filepath.FromSlash(dir) 166 return s 167 } 168 169 func (s *sitesBuilder) WithEnviron(env ...string) *sitesBuilder { 170 for i := 0; i < len(env); i += 2 { 171 s.environ = append(s.environ, fmt.Sprintf("%s=%s", env[i], env[i+1])) 172 } 173 return s 174 } 175 176 func (s *sitesBuilder) WithConfigTemplate(data any, format, configTemplate string) *sitesBuilder { 177 s.T.Helper() 178 179 if format == "" { 180 format = "toml" 181 } 182 183 templ, err := template.New("test").Parse(configTemplate) 184 if err != nil { 185 s.Fatalf("Template parse failed: %s", err) 186 } 187 var b bytes.Buffer 188 189 if err := templ.Execute(&b, data); err != nil { 190 s.Fatalf("Template Execute failed: %s", err) 191 } 192 return s.WithConfigFile(format, b.String()) 193 } 194 195 func (s *sitesBuilder) WithViper(v config.Provider) *sitesBuilder { 196 s.T.Helper() 197 if s.configFileSet { 198 s.T.Fatal("WithViper: use Viper or config.toml, not both") 199 } 200 defer func() { 201 s.configSet = true 202 }() 203 204 // Write to a config file to make sure the tests follow the same code path. 205 var buff bytes.Buffer 206 m := v.Get("").(maps.Params) 207 s.Assert(parser.InterfaceToConfig(m, metadecoders.TOML, &buff), qt.IsNil) 208 return s.WithConfigFile("toml", buff.String()) 209 } 210 211 func (s *sitesBuilder) WithConfigFile(format, conf string) *sitesBuilder { 212 s.T.Helper() 213 if s.configSet { 214 s.T.Fatal("WithConfigFile: use config.Config or config.toml, not both") 215 } 216 s.configFileSet = true 217 filename := s.absFilename("config." + format) 218 writeSource(s.T, s.Fs, filename, conf) 219 s.configFormat = format 220 return s 221 } 222 223 func (s *sitesBuilder) WithThemeConfigFile(format, conf string) *sitesBuilder { 224 s.T.Helper() 225 if s.theme == "" { 226 s.theme = "test-theme" 227 } 228 filename := filepath.Join("themes", s.theme, "config."+format) 229 writeSource(s.T, s.Fs, s.absFilename(filename), conf) 230 return s 231 } 232 233 func (s *sitesBuilder) WithSourceFile(filenameContent ...string) *sitesBuilder { 234 s.T.Helper() 235 for i := 0; i < len(filenameContent); i += 2 { 236 writeSource(s.T, s.Fs, s.absFilename(filenameContent[i]), filenameContent[i+1]) 237 } 238 return s 239 } 240 241 func (s *sitesBuilder) absFilename(filename string) string { 242 filename = filepath.FromSlash(filename) 243 if filepath.IsAbs(filename) { 244 return filename 245 } 246 if s.workingDir != "" && !strings.HasPrefix(filename, s.workingDir) { 247 filename = filepath.Join(s.workingDir, filename) 248 } 249 return filename 250 } 251 252 const commonConfigSections = ` 253 254 [services] 255 [services.disqus] 256 shortname = "disqus_shortname" 257 [services.googleAnalytics] 258 id = "UA-ga_id" 259 260 [privacy] 261 [privacy.disqus] 262 disable = false 263 [privacy.googleAnalytics] 264 respectDoNotTrack = true 265 anonymizeIP = true 266 [privacy.instagram] 267 simple = true 268 [privacy.twitter] 269 enableDNT = true 270 [privacy.vimeo] 271 disable = false 272 [privacy.youtube] 273 disable = false 274 privacyEnhanced = true 275 276 ` 277 278 func (s *sitesBuilder) WithSimpleConfigFile() *sitesBuilder { 279 s.T.Helper() 280 return s.WithSimpleConfigFileAndBaseURL("http://example.com/") 281 } 282 283 func (s *sitesBuilder) WithSimpleConfigFileAndBaseURL(baseURL string) *sitesBuilder { 284 s.T.Helper() 285 return s.WithSimpleConfigFileAndSettings(map[string]any{"baseURL": baseURL}) 286 } 287 288 func (s *sitesBuilder) WithSimpleConfigFileAndSettings(settings any) *sitesBuilder { 289 s.T.Helper() 290 var buf bytes.Buffer 291 //nolint 292 parser.InterfaceToConfig(settings, metadecoders.TOML, &buf) 293 config := buf.String() + commonConfigSections 294 return s.WithConfigFile("toml", config) 295 } 296 297 func (s *sitesBuilder) WithDefaultMultiSiteConfig() *sitesBuilder { 298 defaultMultiSiteConfig := ` 299 baseURL = "http://example.com/blog" 300 301 paginate = 1 302 disablePathToLower = true 303 defaultContentLanguage = "en" 304 defaultContentLanguageInSubdir = true 305 306 [permalinks] 307 other = "/somewhere/else/:filename" 308 309 [Taxonomies] 310 tag = "tags" 311 312 [Languages] 313 [Languages.en] 314 weight = 10 315 title = "In English" 316 languageName = "English" 317 [[Languages.en.menu.main]] 318 url = "/" 319 name = "Home" 320 weight = 0 321 322 [Languages.fr] 323 weight = 20 324 title = "Le Français" 325 languageName = "Français" 326 [Languages.fr.Taxonomies] 327 plaque = "plaques" 328 329 [Languages.nn] 330 weight = 30 331 title = "På nynorsk" 332 languageName = "Nynorsk" 333 paginatePath = "side" 334 [Languages.nn.Taxonomies] 335 lag = "lag" 336 [[Languages.nn.menu.main]] 337 url = "/" 338 name = "Heim" 339 weight = 1 340 341 [Languages.nb] 342 weight = 40 343 title = "På bokmål" 344 languageName = "Bokmål" 345 paginatePath = "side" 346 [Languages.nb.Taxonomies] 347 lag = "lag" 348 ` + commonConfigSections 349 350 return s.WithConfigFile("toml", defaultMultiSiteConfig) 351 } 352 353 func (s *sitesBuilder) WithSunset(in string) { 354 // Write a real image into one of the bundle above. 355 src, err := os.Open(filepath.FromSlash("testdata/sunset.jpg")) 356 s.Assert(err, qt.IsNil) 357 358 out, err := s.Fs.Source.Create(filepath.FromSlash(filepath.Join(s.workingDir, in))) 359 s.Assert(err, qt.IsNil) 360 361 _, err = io.Copy(out, src) 362 s.Assert(err, qt.IsNil) 363 364 out.Close() 365 src.Close() 366 } 367 368 func (s *sitesBuilder) createFilenameContent(pairs []string) []filenameContent { 369 var slice []filenameContent 370 s.appendFilenameContent(&slice, pairs...) 371 return slice 372 } 373 374 func (s *sitesBuilder) appendFilenameContent(slice *[]filenameContent, pairs ...string) { 375 if len(pairs)%2 != 0 { 376 panic("file content mismatch") 377 } 378 for i := 0; i < len(pairs); i += 2 { 379 c := filenameContent{ 380 filename: pairs[i], 381 content: pairs[i+1], 382 } 383 *slice = append(*slice, c) 384 } 385 } 386 387 func (s *sitesBuilder) WithContent(filenameContent ...string) *sitesBuilder { 388 s.appendFilenameContent(&s.contentFilePairs, filenameContent...) 389 return s 390 } 391 392 func (s *sitesBuilder) WithContentAdded(filenameContent ...string) *sitesBuilder { 393 s.appendFilenameContent(&s.contentFilePairsAdded, filenameContent...) 394 return s 395 } 396 397 func (s *sitesBuilder) WithTemplates(filenameContent ...string) *sitesBuilder { 398 s.appendFilenameContent(&s.templateFilePairs, filenameContent...) 399 return s 400 } 401 402 func (s *sitesBuilder) WithTemplatesAdded(filenameContent ...string) *sitesBuilder { 403 s.appendFilenameContent(&s.templateFilePairsAdded, filenameContent...) 404 return s 405 } 406 407 func (s *sitesBuilder) WithData(filenameContent ...string) *sitesBuilder { 408 s.appendFilenameContent(&s.dataFilePairs, filenameContent...) 409 return s 410 } 411 412 func (s *sitesBuilder) WithDataAdded(filenameContent ...string) *sitesBuilder { 413 s.appendFilenameContent(&s.dataFilePairsAdded, filenameContent...) 414 return s 415 } 416 417 func (s *sitesBuilder) WithI18n(filenameContent ...string) *sitesBuilder { 418 s.appendFilenameContent(&s.i18nFilePairs, filenameContent...) 419 return s 420 } 421 422 func (s *sitesBuilder) WithI18nAdded(filenameContent ...string) *sitesBuilder { 423 s.appendFilenameContent(&s.i18nFilePairsAdded, filenameContent...) 424 return s 425 } 426 427 func (s *sitesBuilder) EditFiles(filenameContent ...string) *sitesBuilder { 428 for i := 0; i < len(filenameContent); i += 2 { 429 filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1] 430 absFilename := s.absFilename(filename) 431 s.changedFiles = append(s.changedFiles, absFilename) 432 writeSource(s.T, s.Fs, absFilename, content) 433 434 } 435 return s 436 } 437 438 func (s *sitesBuilder) RemoveFiles(filenames ...string) *sitesBuilder { 439 for _, filename := range filenames { 440 absFilename := s.absFilename(filename) 441 s.removedFiles = append(s.removedFiles, absFilename) 442 s.Assert(s.Fs.Source.Remove(absFilename), qt.IsNil) 443 } 444 return s 445 } 446 447 func (s *sitesBuilder) writeFilePairs(folder string, files []filenameContent) *sitesBuilder { 448 // We have had some "filesystem ordering" bugs that we have not discovered in 449 // our tests running with the in memory filesystem. 450 // That file system is backed by a map so not sure how this helps, but some 451 // randomness in tests doesn't hurt. 452 // TODO(bep) this turns out to be more confusing than helpful. 453 // s.rnd.Shuffle(len(files), func(i, j int) { files[i], files[j] = files[j], files[i] }) 454 455 for _, fc := range files { 456 target := folder 457 // TODO(bep) clean up this magic. 458 if strings.HasPrefix(fc.filename, folder) { 459 target = "" 460 } 461 462 if s.workingDir != "" { 463 target = filepath.Join(s.workingDir, target) 464 } 465 466 writeSource(s.T, s.Fs, filepath.Join(target, fc.filename), fc.content) 467 } 468 return s 469 } 470 471 func (s *sitesBuilder) CreateSites() *sitesBuilder { 472 if err := s.CreateSitesE(); err != nil { 473 s.Fatalf("Failed to create sites: %s", err) 474 } 475 476 s.Assert(s.Fs.PublishDir, qt.IsNotNil) 477 s.Assert(s.Fs.WorkingDirReadOnly, qt.IsNotNil) 478 479 return s 480 } 481 482 func (s *sitesBuilder) LoadConfig() error { 483 if !s.configFileSet { 484 s.WithSimpleConfigFile() 485 } 486 487 flags := config.New() 488 flags.Set("internal", map[string]any{ 489 "running": s.running, 490 "watch": s.running, 491 }) 492 493 if s.workingDir != "" { 494 flags.Set("workingDir", s.workingDir) 495 } 496 497 res, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{ 498 Fs: s.Fs.Source, 499 Logger: s.logger, 500 Flags: flags, 501 Environ: s.environ, 502 Filename: "config." + s.configFormat, 503 }) 504 if err != nil { 505 return err 506 } 507 508 s.Cfg = res.LoadingInfo.Cfg 509 s.Configs = res 510 511 return nil 512 } 513 514 func (s *sitesBuilder) CreateSitesE() error { 515 if !s.addNothing { 516 if _, ok := s.Fs.Source.(*afero.OsFs); ok { 517 for _, dir := range []string{ 518 "content/sect", 519 "layouts/_default", 520 "layouts/_default/_markup", 521 "layouts/partials", 522 "layouts/shortcodes", 523 "data", 524 "i18n", 525 } { 526 if err := os.MkdirAll(filepath.Join(s.workingDir, dir), 0o777); err != nil { 527 return fmt.Errorf("failed to create %q: %w", dir, err) 528 } 529 } 530 } 531 532 s.addDefaults() 533 s.writeFilePairs("content", s.contentFilePairsAdded) 534 s.writeFilePairs("layouts", s.templateFilePairsAdded) 535 s.writeFilePairs("data", s.dataFilePairsAdded) 536 s.writeFilePairs("i18n", s.i18nFilePairsAdded) 537 538 s.writeFilePairs("i18n", s.i18nFilePairs) 539 s.writeFilePairs("data", s.dataFilePairs) 540 s.writeFilePairs("content", s.contentFilePairs) 541 s.writeFilePairs("layouts", s.templateFilePairs) 542 543 } 544 545 if err := s.LoadConfig(); err != nil { 546 return fmt.Errorf("failed to load config: %w", err) 547 } 548 549 s.Fs.PublishDir = hugofs.NewCreateCountingFs(s.Fs.PublishDir) 550 551 depsCfg := s.depsCfg 552 depsCfg.Fs = s.Fs 553 if depsCfg.Configs.IsZero() { 554 depsCfg.Configs = s.Configs 555 } 556 depsCfg.TestLogger = s.logger 557 558 sites, err := NewHugoSites(depsCfg) 559 if err != nil { 560 return fmt.Errorf("failed to create sites: %w", err) 561 } 562 s.H = sites 563 564 return nil 565 } 566 567 func (s *sitesBuilder) BuildE(cfg BuildCfg) error { 568 if s.H == nil { 569 s.CreateSites() 570 } 571 572 return s.H.Build(cfg) 573 } 574 575 func (s *sitesBuilder) Build(cfg BuildCfg) *sitesBuilder { 576 s.T.Helper() 577 return s.build(cfg, false) 578 } 579 580 func (s *sitesBuilder) BuildFail(cfg BuildCfg) *sitesBuilder { 581 s.T.Helper() 582 return s.build(cfg, true) 583 } 584 585 func (s *sitesBuilder) changeEvents() []fsnotify.Event { 586 var events []fsnotify.Event 587 588 for _, v := range s.changedFiles { 589 events = append(events, fsnotify.Event{ 590 Name: v, 591 Op: fsnotify.Write, 592 }) 593 } 594 for _, v := range s.removedFiles { 595 events = append(events, fsnotify.Event{ 596 Name: v, 597 Op: fsnotify.Remove, 598 }) 599 } 600 601 return events 602 } 603 604 func (s *sitesBuilder) build(cfg BuildCfg, shouldFail bool) *sitesBuilder { 605 s.Helper() 606 defer func() { 607 s.changedFiles = nil 608 }() 609 610 if s.H == nil { 611 s.CreateSites() 612 } 613 614 err := s.H.Build(cfg, s.changeEvents()...) 615 616 if err == nil { 617 logErrorCount := s.H.NumLogErrors() 618 if logErrorCount > 0 { 619 err = fmt.Errorf("logged %d errors", logErrorCount) 620 } 621 } 622 if err != nil && !shouldFail { 623 s.Fatalf("Build failed: %s", err) 624 } else if err == nil && shouldFail { 625 s.Fatalf("Expected error") 626 } 627 628 return s 629 } 630 631 func (s *sitesBuilder) addDefaults() { 632 var ( 633 contentTemplate = `--- 634 title: doc1 635 weight: 1 636 tags: 637 - tag1 638 date: "2018-02-28" 639 --- 640 # doc1 641 *some "content"* 642 {{< shortcode >}} 643 {{< lingo >}} 644 ` 645 646 defaultContent = []string{ 647 "content/sect/doc1.en.md", contentTemplate, 648 "content/sect/doc1.fr.md", contentTemplate, 649 "content/sect/doc1.nb.md", contentTemplate, 650 "content/sect/doc1.nn.md", contentTemplate, 651 } 652 653 listTemplateCommon = "{{ $p := .Paginator }}{{ $p.PageNumber }}|{{ .Title }}|{{ i18n \"hello\" }}|{{ .Permalink }}|Pager: {{ template \"_internal/pagination.html\" . }}|Kind: {{ .Kind }}|Content: {{ .Content }}|Len Pages: {{ len .Pages }}|Len RegularPages: {{ len .RegularPages }}| HasParent: {{ if .Parent }}YES{{ else }}NO{{ end }}" 654 655 defaultTemplates = []string{ 656 "_default/single.html", "Single: {{ .Title }}|{{ i18n \"hello\" }}|{{.Language.Lang}}|RelPermalink: {{ .RelPermalink }}|Permalink: {{ .Permalink }}|{{ .Content }}|Resources: {{ range .Resources }}{{ .MediaType }}: {{ .RelPermalink}} -- {{ end }}|Summary: {{ .Summary }}|Truncated: {{ .Truncated }}|Parent: {{ .Parent.Title }}", 657 "_default/list.html", "List Page " + listTemplateCommon, 658 "index.html", "{{ $p := .Paginator }}Default Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}|String Resource: {{ ( \"Hugo Pipes\" | resources.FromString \"text/pipes.txt\").RelPermalink }}|String Resource Permalink: {{ ( \"Hugo Pipes\" | resources.FromString \"text/pipes.txt\").Permalink }}", 659 "index.fr.html", "{{ $p := .Paginator }}French Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}|String Resource: {{ ( \"Hugo Pipes\" | resources.FromString \"text/pipes.txt\").RelPermalink }}|String Resource Permalink: {{ ( \"Hugo Pipes\" | resources.FromString \"text/pipes.txt\").Permalink }}", 660 "_default/terms.html", "Taxonomy Term Page " + listTemplateCommon, 661 "_default/taxonomy.html", "Taxonomy List Page " + listTemplateCommon, 662 // Shortcodes 663 "shortcodes/shortcode.html", "Shortcode: {{ i18n \"hello\" }}", 664 // A shortcode in multiple languages 665 "shortcodes/lingo.html", "LingoDefault", 666 "shortcodes/lingo.fr.html", "LingoFrench", 667 // Special templates 668 "404.html", "404|{{ .Lang }}|{{ .Title }}", 669 "robots.txt", "robots|{{ .Lang }}|{{ .Title }}", 670 } 671 672 defaultI18n = []string{ 673 "en.yaml", ` 674 hello: 675 other: "Hello" 676 `, 677 "fr.yaml", ` 678 hello: 679 other: "Bonjour" 680 `, 681 } 682 683 defaultData = []string{ 684 "hugo.toml", "slogan = \"Hugo Rocks!\"", 685 } 686 ) 687 688 if len(s.contentFilePairs) == 0 { 689 s.writeFilePairs("content", s.createFilenameContent(defaultContent)) 690 } 691 692 if len(s.templateFilePairs) == 0 { 693 s.writeFilePairs("layouts", s.createFilenameContent(defaultTemplates)) 694 } 695 if len(s.dataFilePairs) == 0 { 696 s.writeFilePairs("data", s.createFilenameContent(defaultData)) 697 } 698 if len(s.i18nFilePairs) == 0 { 699 s.writeFilePairs("i18n", s.createFilenameContent(defaultI18n)) 700 } 701 } 702 703 func (s *sitesBuilder) Fatalf(format string, args ...any) { 704 s.T.Helper() 705 s.T.Fatalf(format, args...) 706 } 707 708 func (s *sitesBuilder) AssertFileContentFn(filename string, f func(s string) bool) { 709 s.T.Helper() 710 content := s.FileContent(filename) 711 if !f(content) { 712 s.Fatalf("Assert failed for %q in content\n%s", filename, content) 713 } 714 } 715 716 // Helper to migrate tests to new format. 717 func (s *sitesBuilder) DumpTxtar() string { 718 var sb strings.Builder 719 720 skipRe := regexp.MustCompile(`^(public|resources|package-lock.json|go.sum)`) 721 722 // nolint 723 afero.Walk(s.Fs.Source, s.workingDir, func(path string, info fs.FileInfo, err error) error { 724 if err != nil { 725 return err 726 } 727 rel := strings.TrimPrefix(path, s.workingDir+"/") 728 if skipRe.MatchString(rel) { 729 if info.IsDir() { 730 return filepath.SkipDir 731 } 732 return nil 733 } 734 if info == nil || info.IsDir() { 735 return nil 736 } 737 // nolint 738 sb.WriteString(fmt.Sprintf("-- %s --\n", rel)) 739 b, err := afero.ReadFile(s.Fs.Source, path) 740 s.Assert(err, qt.IsNil) 741 sb.WriteString(strings.TrimSpace(string(b))) 742 sb.WriteString("\n") 743 return nil 744 }) 745 746 return sb.String() 747 } 748 749 func (s *sitesBuilder) AssertHome(matches ...string) { 750 s.AssertFileContent("public/index.html", matches...) 751 } 752 753 func (s *sitesBuilder) AssertFileContent(filename string, matches ...string) { 754 s.T.Helper() 755 content := s.FileContent(filename) 756 for _, m := range matches { 757 lines := strings.Split(m, "\n") 758 for _, match := range lines { 759 match = strings.TrimSpace(match) 760 if match == "" { 761 continue 762 } 763 if !strings.Contains(content, match) { 764 s.Assert(content, qt.Contains, match, qt.Commentf(match+" not in: \n"+content)) 765 } 766 } 767 } 768 } 769 770 func (s *sitesBuilder) AssertFileDoesNotExist(filename string) { 771 if s.CheckExists(filename) { 772 s.Fatalf("File %q exists but must not exist.", filename) 773 } 774 } 775 776 func (s *sitesBuilder) AssertImage(width, height int, filename string) { 777 f, err := s.Fs.WorkingDirReadOnly.Open(filename) 778 s.Assert(err, qt.IsNil) 779 defer f.Close() 780 cfg, err := jpeg.DecodeConfig(f) 781 s.Assert(err, qt.IsNil) 782 s.Assert(cfg.Width, qt.Equals, width) 783 s.Assert(cfg.Height, qt.Equals, height) 784 } 785 786 func (s *sitesBuilder) AssertNoDuplicateWrites() { 787 s.Helper() 788 hugofs.WalkFilesystems(s.Fs.PublishDir, func(fs afero.Fs) bool { 789 if dfs, ok := fs.(hugofs.DuplicatesReporter); ok { 790 s.Assert(dfs.ReportDuplicates(), qt.Equals, "") 791 } 792 return false 793 }) 794 } 795 796 func (s *sitesBuilder) FileContent(filename string) string { 797 s.Helper() 798 filename = filepath.FromSlash(filename) 799 return readWorkingDir(s.T, s.Fs, filename) 800 } 801 802 func (s *sitesBuilder) AssertObject(expected string, object any) { 803 s.T.Helper() 804 got := s.dumper.Sdump(object) 805 expected = strings.TrimSpace(expected) 806 807 if expected != got { 808 fmt.Println(got) 809 diff := htesting.DiffStrings(expected, got) 810 s.Fatalf("diff:\n%s\nexpected\n%s\ngot\n%s", diff, expected, got) 811 } 812 } 813 814 func (s *sitesBuilder) AssertFileContentRe(filename string, matches ...string) { 815 content := readWorkingDir(s.T, s.Fs, filename) 816 for _, match := range matches { 817 r := regexp.MustCompile("(?s)" + match) 818 if !r.MatchString(content) { 819 s.Fatalf("No match for %q in content for %s\n%q", match, filename, content) 820 } 821 } 822 } 823 824 func (s *sitesBuilder) CheckExists(filename string) bool { 825 return workingDirExists(s.Fs, filepath.Clean(filename)) 826 } 827 828 func (s *sitesBuilder) GetPage(ref string) page.Page { 829 p, err := s.H.Sites[0].getPage(nil, ref) 830 s.Assert(err, qt.IsNil) 831 return p 832 } 833 834 func (s *sitesBuilder) GetPageRel(p page.Page, ref string) page.Page { 835 p, err := s.H.Sites[0].getPage(p, ref) 836 s.Assert(err, qt.IsNil) 837 return p 838 } 839 840 func (s *sitesBuilder) NpmInstall() hexec.Runner { 841 sc := security.DefaultConfig 842 var err error 843 sc.Exec.Allow, err = security.NewWhitelist("npm") 844 s.Assert(err, qt.IsNil) 845 ex := hexec.New(sc) 846 command, err := ex.New("npm", "install") 847 s.Assert(err, qt.IsNil) 848 return command 849 } 850 851 func newTestHelperFromProvider(cfg config.Provider, fs *hugofs.Fs, t testing.TB) (testHelper, *allconfig.Configs) { 852 res, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{ 853 Flags: cfg, 854 Fs: fs.Source, 855 }) 856 if err != nil { 857 t.Fatal(err) 858 } 859 return newTestHelper(res.Base, fs, t), res 860 } 861 862 func newTestHelper(cfg *allconfig.Config, fs *hugofs.Fs, t testing.TB) testHelper { 863 return testHelper{ 864 Cfg: cfg, 865 Fs: fs, 866 C: qt.New(t), 867 } 868 } 869 870 type testHelper struct { 871 Cfg *allconfig.Config 872 Fs *hugofs.Fs 873 *qt.C 874 } 875 876 func (th testHelper) assertFileContent(filename string, matches ...string) { 877 th.Helper() 878 filename = th.replaceDefaultContentLanguageValue(filename) 879 content := readWorkingDir(th, th.Fs, filename) 880 for _, match := range matches { 881 match = th.replaceDefaultContentLanguageValue(match) 882 th.Assert(strings.Contains(content, match), qt.Equals, true, qt.Commentf(match+" not in: \n"+content)) 883 } 884 } 885 886 func (th testHelper) assertFileNotExist(filename string) { 887 exists, err := helpers.Exists(filename, th.Fs.PublishDir) 888 th.Assert(err, qt.IsNil) 889 th.Assert(exists, qt.Equals, false) 890 } 891 892 func (th testHelper) replaceDefaultContentLanguageValue(value string) string { 893 defaultInSubDir := th.Cfg.DefaultContentLanguageInSubdir 894 replace := th.Cfg.DefaultContentLanguage + "/" 895 896 if !defaultInSubDir { 897 value = strings.Replace(value, replace, "", 1) 898 } 899 return value 900 } 901 902 func loadTestConfigFromProvider(cfg config.Provider) (*allconfig.Configs, error) { 903 workingDir := cfg.GetString("workingDir") 904 fs := afero.NewMemMapFs() 905 if workingDir != "" { 906 fs.MkdirAll(workingDir, 0o755) // nolint 907 } 908 res, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{Flags: cfg, Fs: fs}) 909 return res, err 910 } 911 912 func newTestCfg(withConfig ...func(cfg config.Provider) error) (config.Provider, *hugofs.Fs) { 913 mm := afero.NewMemMapFs() 914 cfg := config.New() 915 cfg.Set("defaultContentLanguageInSubdir", false) 916 cfg.Set("publishDir", "public") 917 918 fs := hugofs.NewFromOld(hugofs.NewBaseFileDecorator(mm), cfg) 919 920 return cfg, fs 921 } 922 923 func newTestSitesFromConfig(t testing.TB, afs afero.Fs, tomlConfig string, layoutPathContentPairs ...string) (testHelper, *HugoSites) { 924 if len(layoutPathContentPairs)%2 != 0 { 925 t.Fatalf("Layouts must be provided in pairs") 926 } 927 928 c := qt.New(t) 929 930 writeToFs(t, afs, filepath.Join("content", ".gitkeep"), "") 931 writeToFs(t, afs, "config.toml", tomlConfig) 932 933 cfg, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{Fs: afs}) 934 c.Assert(err, qt.IsNil) 935 936 fs := hugofs.NewFrom(afs, cfg.LoadingInfo.BaseConfig) 937 th := newTestHelper(cfg.Base, fs, t) 938 939 for i := 0; i < len(layoutPathContentPairs); i += 2 { 940 writeSource(t, fs, layoutPathContentPairs[i], layoutPathContentPairs[i+1]) 941 } 942 943 h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Configs: cfg}) 944 945 c.Assert(err, qt.IsNil) 946 947 return th, h 948 } 949 950 // TODO(bep) replace these with the builder 951 func buildSingleSite(t testing.TB, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site { 952 t.Helper() 953 return buildSingleSiteExpected(t, false, false, depsCfg, buildCfg) 954 } 955 956 func buildSingleSiteExpected(t testing.TB, expectSiteInitError, expectBuildError bool, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site { 957 t.Helper() 958 b := newTestSitesBuilderFromDepsCfg(t, depsCfg).WithNothingAdded() 959 960 err := b.CreateSitesE() 961 962 if expectSiteInitError { 963 b.Assert(err, qt.Not(qt.IsNil)) 964 return nil 965 } else { 966 b.Assert(err, qt.IsNil) 967 } 968 969 h := b.H 970 971 b.Assert(len(h.Sites), qt.Equals, 1) 972 973 if expectBuildError { 974 b.Assert(h.Build(buildCfg), qt.Not(qt.IsNil)) 975 return nil 976 977 } 978 979 b.Assert(h.Build(buildCfg), qt.IsNil) 980 981 return h.Sites[0] 982 } 983 984 func writeSourcesToSource(t *testing.T, base string, fs *hugofs.Fs, sources ...[2]string) { 985 for _, src := range sources { 986 writeSource(t, fs, filepath.Join(base, src[0]), src[1]) 987 } 988 } 989 990 func getPage(in page.Page, ref string) page.Page { 991 p, err := in.GetPage(ref) 992 if err != nil { 993 panic(err) 994 } 995 return p 996 } 997 998 func content(c resource.ContentProvider) string { 999 cc, err := c.Content(context.Background()) 1000 if err != nil { 1001 panic(err) 1002 } 1003 1004 ccs, err := cast.ToStringE(cc) 1005 if err != nil { 1006 panic(err) 1007 } 1008 return ccs 1009 }