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