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