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