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