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