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