github.com/jbramsden/hugo@v0.47.1/hugolib/testhelpers_test.go (about) 1 package hugolib 2 3 import ( 4 "io/ioutil" 5 "path/filepath" 6 "runtime" 7 "testing" 8 9 "bytes" 10 "fmt" 11 "regexp" 12 "strings" 13 "text/template" 14 15 "github.com/gohugoio/hugo/langs" 16 "github.com/sanity-io/litter" 17 jww "github.com/spf13/jwalterweatherman" 18 19 "github.com/gohugoio/hugo/config" 20 "github.com/gohugoio/hugo/deps" 21 "github.com/spf13/afero" 22 23 "github.com/gohugoio/hugo/helpers" 24 "github.com/gohugoio/hugo/tpl" 25 "github.com/spf13/viper" 26 27 "os" 28 29 "github.com/gohugoio/hugo/hugofs" 30 "github.com/stretchr/testify/assert" 31 "github.com/stretchr/testify/require" 32 ) 33 34 const () 35 36 type sitesBuilder struct { 37 Cfg config.Provider 38 Fs *hugofs.Fs 39 T testing.TB 40 41 logger *jww.Notepad 42 43 dumper litter.Options 44 45 // Aka the Hugo server mode. 46 running bool 47 48 H *HugoSites 49 50 theme string 51 52 // Default toml 53 configFormat string 54 55 // Default is empty. 56 // TODO(bep) revisit this and consider always setting it to something. 57 // Consider this in relation to using the BaseFs.PublishFs to all publishing. 58 workingDir string 59 60 // Base data/content 61 contentFilePairs []string 62 templateFilePairs []string 63 i18nFilePairs []string 64 dataFilePairs []string 65 66 // Additional data/content. 67 // As in "use the base, but add these on top". 68 contentFilePairsAdded []string 69 templateFilePairsAdded []string 70 i18nFilePairsAdded []string 71 dataFilePairsAdded []string 72 } 73 74 func newTestSitesBuilder(t testing.TB) *sitesBuilder { 75 v := viper.New() 76 fs := hugofs.NewMem(v) 77 78 litterOptions := litter.Options{ 79 HidePrivateFields: true, 80 StripPackageNames: true, 81 Separator: " ", 82 } 83 84 return &sitesBuilder{T: t, Fs: fs, configFormat: "toml", dumper: litterOptions} 85 } 86 87 func createTempDir(prefix string) (string, func(), error) { 88 workDir, err := ioutil.TempDir("", prefix) 89 if err != nil { 90 return "", nil, err 91 } 92 93 if runtime.GOOS == "darwin" && !strings.HasPrefix(workDir, "/private") { 94 // To get the entry folder in line with the rest. This its a little bit 95 // mysterious, but so be it. 96 workDir = "/private" + workDir 97 } 98 return workDir, func() { os.RemoveAll(workDir) }, nil 99 } 100 101 func (s *sitesBuilder) Running() *sitesBuilder { 102 s.running = true 103 return s 104 } 105 106 func (s *sitesBuilder) WithLogger(logger *jww.Notepad) *sitesBuilder { 107 s.logger = logger 108 return s 109 } 110 111 func (s *sitesBuilder) WithWorkingDir(dir string) *sitesBuilder { 112 s.workingDir = dir 113 return s 114 } 115 116 func (s *sitesBuilder) WithConfigTemplate(data interface{}, format, configTemplate string) *sitesBuilder { 117 if format == "" { 118 format = "toml" 119 } 120 121 templ, err := template.New("test").Parse(configTemplate) 122 if err != nil { 123 s.Fatalf("Template parse failed: %s", err) 124 } 125 var b bytes.Buffer 126 templ.Execute(&b, data) 127 return s.WithConfigFile(format, b.String()) 128 } 129 130 func (s *sitesBuilder) WithViper(v *viper.Viper) *sitesBuilder { 131 loadDefaultSettingsFor(v) 132 s.Cfg = v 133 134 return s 135 } 136 137 func (s *sitesBuilder) WithConfigFile(format, conf string) *sitesBuilder { 138 writeSource(s.T, s.Fs, "config."+format, conf) 139 s.configFormat = format 140 return s 141 } 142 143 func (s *sitesBuilder) WithThemeConfigFile(format, conf string) *sitesBuilder { 144 if s.theme == "" { 145 s.theme = "test-theme" 146 } 147 filename := filepath.Join("themes", s.theme, "config."+format) 148 writeSource(s.T, s.Fs, filename, conf) 149 return s 150 } 151 152 func (s *sitesBuilder) WithSourceFile(filename, content string) *sitesBuilder { 153 writeSource(s.T, s.Fs, filepath.FromSlash(filename), content) 154 return s 155 } 156 157 const commonConfigSections = ` 158 159 [services] 160 [services.disqus] 161 shortname = "disqus_shortname" 162 [services.googleAnalytics] 163 id = "ga_id" 164 165 [privacy] 166 [privacy.disqus] 167 disable = false 168 [privacy.googleAnalytics] 169 respectDoNotTrack = true 170 anonymizeIP = true 171 [privacy.instagram] 172 simple = true 173 [privacy.twitter] 174 enableDNT = true 175 [privacy.vimeo] 176 disable = false 177 [privacy.youtube] 178 disable = false 179 privacyEnhanced = true 180 181 ` 182 183 func (s *sitesBuilder) WithSimpleConfigFile() *sitesBuilder { 184 var config = ` 185 baseURL = "http://example.com/" 186 187 ` + commonConfigSections 188 return s.WithConfigFile("toml", config) 189 } 190 191 func (s *sitesBuilder) WithDefaultMultiSiteConfig() *sitesBuilder { 192 var defaultMultiSiteConfig = ` 193 baseURL = "http://example.com/blog" 194 195 paginate = 1 196 disablePathToLower = true 197 defaultContentLanguage = "en" 198 defaultContentLanguageInSubdir = true 199 200 [permalinks] 201 other = "/somewhere/else/:filename" 202 203 [blackfriday] 204 angledQuotes = true 205 206 [Taxonomies] 207 tag = "tags" 208 209 [Languages] 210 [Languages.en] 211 weight = 10 212 title = "In English" 213 languageName = "English" 214 [Languages.en.blackfriday] 215 angledQuotes = false 216 [[Languages.en.menu.main]] 217 url = "/" 218 name = "Home" 219 weight = 0 220 221 [Languages.fr] 222 weight = 20 223 title = "Le Français" 224 languageName = "Français" 225 [Languages.fr.Taxonomies] 226 plaque = "plaques" 227 228 [Languages.nn] 229 weight = 30 230 title = "På nynorsk" 231 languageName = "Nynorsk" 232 paginatePath = "side" 233 [Languages.nn.Taxonomies] 234 lag = "lag" 235 [[Languages.nn.menu.main]] 236 url = "/" 237 name = "Heim" 238 weight = 1 239 240 [Languages.nb] 241 weight = 40 242 title = "På bokmål" 243 languageName = "Bokmål" 244 paginatePath = "side" 245 [Languages.nb.Taxonomies] 246 lag = "lag" 247 ` + commonConfigSections 248 249 return s.WithConfigFile("toml", defaultMultiSiteConfig) 250 251 } 252 253 func (s *sitesBuilder) WithContent(filenameContent ...string) *sitesBuilder { 254 s.contentFilePairs = append(s.contentFilePairs, filenameContent...) 255 return s 256 } 257 258 func (s *sitesBuilder) WithContentAdded(filenameContent ...string) *sitesBuilder { 259 s.contentFilePairsAdded = append(s.contentFilePairsAdded, filenameContent...) 260 return s 261 } 262 263 func (s *sitesBuilder) WithTemplates(filenameContent ...string) *sitesBuilder { 264 s.templateFilePairs = append(s.templateFilePairs, filenameContent...) 265 return s 266 } 267 268 func (s *sitesBuilder) WithTemplatesAdded(filenameContent ...string) *sitesBuilder { 269 s.templateFilePairsAdded = append(s.templateFilePairsAdded, filenameContent...) 270 return s 271 } 272 273 func (s *sitesBuilder) WithData(filenameContent ...string) *sitesBuilder { 274 s.dataFilePairs = append(s.dataFilePairs, filenameContent...) 275 return s 276 } 277 278 func (s *sitesBuilder) WithDataAdded(filenameContent ...string) *sitesBuilder { 279 s.dataFilePairsAdded = append(s.dataFilePairsAdded, filenameContent...) 280 return s 281 } 282 283 func (s *sitesBuilder) WithI18n(filenameContent ...string) *sitesBuilder { 284 s.i18nFilePairs = append(s.i18nFilePairs, filenameContent...) 285 return s 286 } 287 288 func (s *sitesBuilder) WithI18nAdded(filenameContent ...string) *sitesBuilder { 289 s.i18nFilePairsAdded = append(s.i18nFilePairsAdded, filenameContent...) 290 return s 291 } 292 293 func (s *sitesBuilder) writeFilePairs(folder string, filenameContent []string) *sitesBuilder { 294 if len(filenameContent)%2 != 0 { 295 s.Fatalf("expect filenameContent for %q in pairs (%d)", folder, len(filenameContent)) 296 } 297 for i := 0; i < len(filenameContent); i += 2 { 298 filename, content := filenameContent[i], filenameContent[i+1] 299 target := folder 300 // TODO(bep) clean up this magic. 301 if strings.HasPrefix(filename, folder) { 302 target = "" 303 } 304 305 if s.workingDir != "" { 306 target = filepath.Join(s.workingDir, target) 307 } 308 309 writeSource(s.T, s.Fs, filepath.Join(target, filename), content) 310 } 311 return s 312 } 313 314 func (s *sitesBuilder) CreateSites() *sitesBuilder { 315 s.addDefaults() 316 s.writeFilePairs("content", s.contentFilePairs) 317 s.writeFilePairs("content", s.contentFilePairsAdded) 318 s.writeFilePairs("layouts", s.templateFilePairs) 319 s.writeFilePairs("layouts", s.templateFilePairsAdded) 320 s.writeFilePairs("data", s.dataFilePairs) 321 s.writeFilePairs("data", s.dataFilePairsAdded) 322 s.writeFilePairs("i18n", s.i18nFilePairs) 323 s.writeFilePairs("i18n", s.i18nFilePairsAdded) 324 325 if s.Cfg == nil { 326 cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: s.Fs.Source, Filename: "config." + s.configFormat}) 327 if err != nil { 328 s.Fatalf("Failed to load config: %s", err) 329 } 330 // TODO(bep) 331 /* expectedConfigs := 1 332 if s.theme != "" { 333 expectedConfigs = 2 334 } 335 require.Equal(s.T, expectedConfigs, len(configFiles), fmt.Sprintf("Configs: %v", configFiles)) 336 */ 337 s.Cfg = cfg 338 } 339 340 sites, err := NewHugoSites(deps.DepsCfg{Fs: s.Fs, Cfg: s.Cfg, Logger: s.logger, Running: s.running}) 341 if err != nil { 342 s.Fatalf("Failed to create sites: %s", err) 343 } 344 s.H = sites 345 346 return s 347 } 348 349 func (s *sitesBuilder) Build(cfg BuildCfg) *sitesBuilder { 350 return s.build(cfg, false) 351 } 352 353 func (s *sitesBuilder) BuildFail(cfg BuildCfg) *sitesBuilder { 354 return s.build(cfg, true) 355 } 356 357 func (s *sitesBuilder) build(cfg BuildCfg, shouldFail bool) *sitesBuilder { 358 if s.H == nil { 359 s.CreateSites() 360 } 361 362 err := s.H.Build(cfg) 363 if err == nil { 364 logErrorCount := s.H.NumLogErrors() 365 if logErrorCount > 0 { 366 err = fmt.Errorf("logged %d errors", logErrorCount) 367 } 368 } 369 if err != nil && !shouldFail { 370 s.Fatalf("Build failed: %s", err) 371 } else if err == nil && shouldFail { 372 s.Fatalf("Expected error") 373 } 374 375 return s 376 } 377 378 func (s *sitesBuilder) addDefaults() { 379 380 var ( 381 contentTemplate = `--- 382 title: doc1 383 weight: 1 384 tags: 385 - tag1 386 date: "2018-02-28" 387 --- 388 # doc1 389 *some "content"* 390 391 {{< shortcode >}} 392 393 {{< lingo >}} 394 ` 395 396 defaultContent = []string{ 397 "content/sect/doc1.en.md", contentTemplate, 398 "content/sect/doc1.fr.md", contentTemplate, 399 "content/sect/doc1.nb.md", contentTemplate, 400 "content/sect/doc1.nn.md", contentTemplate, 401 } 402 403 defaultTemplates = []string{ 404 "_default/single.html", "Single: {{ .Title }}|{{ i18n \"hello\" }}|{{.Lang}}|{{ .Content }}", 405 "_default/list.html", "{{ $p := .Paginator }}List Page {{ $p.PageNumber }}: {{ .Title }}|{{ i18n \"hello\" }}|{{ .Permalink }}|Pager: {{ template \"_internal/pagination.html\" . }}", 406 "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 }}", 407 "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 }}", 408 409 // Shortcodes 410 "shortcodes/shortcode.html", "Shortcode: {{ i18n \"hello\" }}", 411 // A shortcode in multiple languages 412 "shortcodes/lingo.html", "LingoDefault", 413 "shortcodes/lingo.fr.html", "LingoFrench", 414 } 415 416 defaultI18n = []string{ 417 "en.yaml", ` 418 hello: 419 other: "Hello" 420 `, 421 "fr.yaml", ` 422 hello: 423 other: "Bonjour" 424 `, 425 } 426 427 defaultData = []string{ 428 "hugo.toml", "slogan = \"Hugo Rocks!\"", 429 } 430 ) 431 432 if len(s.contentFilePairs) == 0 { 433 s.writeFilePairs("content", defaultContent) 434 } 435 if len(s.templateFilePairs) == 0 { 436 s.writeFilePairs("layouts", defaultTemplates) 437 } 438 if len(s.dataFilePairs) == 0 { 439 s.writeFilePairs("data", defaultData) 440 } 441 if len(s.i18nFilePairs) == 0 { 442 s.writeFilePairs("i18n", defaultI18n) 443 } 444 } 445 446 func (s *sitesBuilder) Fatalf(format string, args ...interface{}) { 447 Fatalf(s.T, format, args...) 448 } 449 450 func Fatalf(t testing.TB, format string, args ...interface{}) { 451 trace := strings.Join(assert.CallerInfo(), "\n\r\t\t\t") 452 format = format + "\n%s" 453 args = append(args, trace) 454 t.Fatalf(format, args...) 455 } 456 457 func (s *sitesBuilder) AssertFileContent(filename string, matches ...string) { 458 content := readDestination(s.T, s.Fs, filename) 459 for _, match := range matches { 460 if !strings.Contains(content, match) { 461 s.Fatalf("No match for %q in content for %s\n%s\n%q", match, filename, content, content) 462 } 463 } 464 } 465 466 func (s *sitesBuilder) AssertObject(expected string, object interface{}) { 467 got := s.dumper.Sdump(object) 468 expected = strings.TrimSpace(expected) 469 470 if expected != got { 471 fmt.Println(got) 472 diff := helpers.DiffStrings(expected, got) 473 s.Fatalf("diff:\n%s\nexpected\n%s\ngot\n%s", diff, expected, got) 474 } 475 } 476 477 func (s *sitesBuilder) AssertFileContentRe(filename string, matches ...string) { 478 content := readDestination(s.T, s.Fs, filename) 479 for _, match := range matches { 480 r := regexp.MustCompile(match) 481 if !r.MatchString(content) { 482 s.Fatalf("No match for %q in content for %s\n%q", match, filename, content) 483 } 484 } 485 } 486 487 func (s *sitesBuilder) CheckExists(filename string) bool { 488 return destinationExists(s.Fs, filepath.Clean(filename)) 489 } 490 491 type testHelper struct { 492 Cfg config.Provider 493 Fs *hugofs.Fs 494 T testing.TB 495 } 496 497 func (th testHelper) assertFileContent(filename string, matches ...string) { 498 filename = th.replaceDefaultContentLanguageValue(filename) 499 content := readDestination(th.T, th.Fs, filename) 500 for _, match := range matches { 501 match = th.replaceDefaultContentLanguageValue(match) 502 require.True(th.T, strings.Contains(content, match), fmt.Sprintf("File no match for\n%q in\n%q:\n%s", strings.Replace(match, "%", "%%", -1), filename, strings.Replace(content, "%", "%%", -1))) 503 } 504 } 505 506 func (th testHelper) assertFileContentRegexp(filename string, matches ...string) { 507 filename = th.replaceDefaultContentLanguageValue(filename) 508 content := readDestination(th.T, th.Fs, filename) 509 for _, match := range matches { 510 match = th.replaceDefaultContentLanguageValue(match) 511 r := regexp.MustCompile(match) 512 require.True(th.T, r.MatchString(content), fmt.Sprintf("File no match for\n%q in\n%q:\n%s", strings.Replace(match, "%", "%%", -1), filename, strings.Replace(content, "%", "%%", -1))) 513 } 514 } 515 516 func (th testHelper) assertFileNotExist(filename string) { 517 exists, err := helpers.Exists(filename, th.Fs.Destination) 518 require.NoError(th.T, err) 519 require.False(th.T, exists) 520 } 521 522 func (th testHelper) replaceDefaultContentLanguageValue(value string) string { 523 defaultInSubDir := th.Cfg.GetBool("defaultContentLanguageInSubDir") 524 replace := th.Cfg.GetString("defaultContentLanguage") + "/" 525 526 if !defaultInSubDir { 527 value = strings.Replace(value, replace, "", 1) 528 529 } 530 return value 531 } 532 533 func newTestPathSpec(fs *hugofs.Fs, v *viper.Viper) *helpers.PathSpec { 534 l := langs.NewDefaultLanguage(v) 535 ps, _ := helpers.NewPathSpec(fs, l) 536 return ps 537 } 538 539 func newTestDefaultPathSpec(t *testing.T) *helpers.PathSpec { 540 v := viper.New() 541 // Easier to reason about in tests. 542 v.Set("disablePathToLower", true) 543 v.Set("contentDir", "content") 544 v.Set("dataDir", "data") 545 v.Set("i18nDir", "i18n") 546 v.Set("layoutDir", "layouts") 547 v.Set("archetypeDir", "archetypes") 548 v.Set("assetDir", "assets") 549 v.Set("resourceDir", "resources") 550 v.Set("publishDir", "public") 551 fs := hugofs.NewDefault(v) 552 ps, err := helpers.NewPathSpec(fs, v) 553 if err != nil { 554 t.Fatal(err) 555 } 556 return ps 557 } 558 559 func newTestCfg() (*viper.Viper, *hugofs.Fs) { 560 561 v := viper.New() 562 fs := hugofs.NewMem(v) 563 564 v.SetFs(fs.Source) 565 566 loadDefaultSettingsFor(v) 567 568 // Default is false, but true is easier to use as default in tests 569 v.Set("defaultContentLanguageInSubdir", true) 570 571 return v, fs 572 573 } 574 575 // newTestSite creates a new site in the English language with in-memory Fs. 576 // The site will have a template system loaded and ready to use. 577 // Note: This is only used in single site tests. 578 func newTestSite(t testing.TB, configKeyValues ...interface{}) *Site { 579 580 cfg, fs := newTestCfg() 581 582 for i := 0; i < len(configKeyValues); i += 2 { 583 cfg.Set(configKeyValues[i].(string), configKeyValues[i+1]) 584 } 585 586 d := deps.DepsCfg{Fs: fs, Cfg: cfg} 587 588 s, err := NewSiteForCfg(d) 589 590 if err != nil { 591 Fatalf(t, "Failed to create Site: %s", err) 592 } 593 return s 594 } 595 596 func newTestSitesFromConfig(t testing.TB, afs afero.Fs, tomlConfig string, layoutPathContentPairs ...string) (testHelper, *HugoSites) { 597 if len(layoutPathContentPairs)%2 != 0 { 598 Fatalf(t, "Layouts must be provided in pairs") 599 } 600 601 writeToFs(t, afs, "config.toml", tomlConfig) 602 603 cfg, err := LoadConfigDefault(afs) 604 require.NoError(t, err) 605 606 fs := hugofs.NewFrom(afs, cfg) 607 th := testHelper{cfg, fs, t} 608 609 for i := 0; i < len(layoutPathContentPairs); i += 2 { 610 writeSource(t, fs, layoutPathContentPairs[i], layoutPathContentPairs[i+1]) 611 } 612 613 h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) 614 615 require.NoError(t, err) 616 617 return th, h 618 } 619 620 func newTestSitesFromConfigWithDefaultTemplates(t testing.TB, tomlConfig string) (testHelper, *HugoSites) { 621 return newTestSitesFromConfig(t, afero.NewMemMapFs(), tomlConfig, 622 "layouts/_default/single.html", "Single|{{ .Title }}|{{ .Content }}", 623 "layouts/_default/list.html", "List|{{ .Title }}|{{ .Content }}", 624 "layouts/_default/terms.html", "Terms List|{{ .Title }}|{{ .Content }}", 625 ) 626 } 627 628 func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tpl.TemplateHandler) error { 629 630 return func(templ tpl.TemplateHandler) error { 631 for i := 0; i < len(additionalTemplates); i += 2 { 632 err := templ.AddTemplate(additionalTemplates[i], additionalTemplates[i+1]) 633 if err != nil { 634 return err 635 } 636 } 637 return nil 638 } 639 } 640 641 func buildSingleSite(t testing.TB, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site { 642 return buildSingleSiteExpected(t, false, depsCfg, buildCfg) 643 } 644 645 func buildSingleSiteExpected(t testing.TB, expectBuildError bool, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site { 646 h, err := NewHugoSites(depsCfg) 647 648 require.NoError(t, err) 649 require.Len(t, h.Sites, 1) 650 651 if expectBuildError { 652 require.Error(t, h.Build(buildCfg)) 653 return nil 654 655 } 656 657 require.NoError(t, h.Build(buildCfg)) 658 659 return h.Sites[0] 660 } 661 662 func writeSourcesToSource(t *testing.T, base string, fs *hugofs.Fs, sources ...[2]string) { 663 for _, src := range sources { 664 writeSource(t, fs, filepath.Join(base, src[0]), src[1]) 665 } 666 } 667 668 func dumpPages(pages ...*Page) { 669 for i, p := range pages { 670 fmt.Printf("%d: Kind: %s Title: %-10s RelPermalink: %-10s Path: %-10s sections: %s Len Sections(): %d\n", 671 i+1, 672 p.Kind, p.title, p.RelPermalink(), p.Path(), p.sections, len(p.Sections())) 673 } 674 } 675 676 func isCI() bool { 677 return os.Getenv("CI") != "" 678 }