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