package hugolib import ( "bytes" "fmt" "image/jpeg" "io" "io/fs" "math/rand" "os" "path/filepath" "regexp" "runtime" "sort" "strconv" "strings" "testing" "text/template" "time" "unicode/utf8" "github.com/gohugoio/hugo/config/security" "github.com/gohugoio/hugo/htesting" "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/parser/metadecoders" "github.com/google/go-cmp/cmp" "github.com/gohugoio/hugo/parser" "github.com/fsnotify/fsnotify" "github.com/gohugoio/hugo/common/hexec" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/resources/page" "github.com/sanity-io/litter" "github.com/spf13/afero" "github.com/spf13/cast" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/resources/resource" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/hugofs" ) var ( deepEqualsPages = qt.CmpEquals(cmp.Comparer(func(p1, p2 *pageState) bool { return p1 == p2 })) deepEqualsOutputFormats = qt.CmpEquals(cmp.Comparer(func(o1, o2 output.Format) bool { return o1.Name == o2.Name && o1.MediaType.Type() == o2.MediaType.Type() })) ) type sitesBuilder struct { Cfg config.Provider environ []string Fs *hugofs.Fs T testing.TB depsCfg deps.DepsCfg *qt.C logger loggers.Logger rnd *rand.Rand dumper litter.Options // Used to test partial rebuilds. changedFiles []string removedFiles []string // Aka the Hugo server mode. running bool H *HugoSites theme string // Default toml configFormat string configFileSet bool configSet bool // Default is empty. // TODO(bep) revisit this and consider always setting it to something. // Consider this in relation to using the BaseFs.PublishFs to all publishing. workingDir string addNothing bool // Base data/content contentFilePairs []filenameContent templateFilePairs []filenameContent i18nFilePairs []filenameContent dataFilePairs []filenameContent // Additional data/content. // As in "use the base, but add these on top". contentFilePairsAdded []filenameContent templateFilePairsAdded []filenameContent i18nFilePairsAdded []filenameContent dataFilePairsAdded []filenameContent } type filenameContent struct { filename string content string } func newTestSitesBuilder(t testing.TB) *sitesBuilder { v := config.NewWithTestDefaults() fs := hugofs.NewMem(v) litterOptions := litter.Options{ HidePrivateFields: true, StripPackageNames: true, Separator: " ", } return &sitesBuilder{ T: t, C: qt.New(t), Fs: fs, configFormat: "toml", dumper: litterOptions, rnd: rand.New(rand.NewSource(time.Now().Unix())), } } func newTestSitesBuilderFromDepsCfg(t testing.TB, d deps.DepsCfg) *sitesBuilder { c := qt.New(t) litterOptions := litter.Options{ HidePrivateFields: true, StripPackageNames: true, Separator: " ", } b := &sitesBuilder{T: t, C: c, depsCfg: d, Fs: d.Fs, dumper: litterOptions, rnd: rand.New(rand.NewSource(time.Now().Unix()))} workingDir := d.Cfg.GetString("workingDir") b.WithWorkingDir(workingDir) return b.WithViper(d.Cfg.(config.Provider)) } func (s *sitesBuilder) Running() *sitesBuilder { s.running = true return s } func (s *sitesBuilder) WithNothingAdded() *sitesBuilder { s.addNothing = true return s } func (s *sitesBuilder) WithLogger(logger loggers.Logger) *sitesBuilder { s.logger = logger return s } func (s *sitesBuilder) WithWorkingDir(dir string) *sitesBuilder { s.workingDir = filepath.FromSlash(dir) return s } func (s *sitesBuilder) WithEnviron(env ...string) *sitesBuilder { for i := 0; i < len(env); i += 2 { s.environ = append(s.environ, fmt.Sprintf("%s=%s", env[i], env[i+1])) } return s } func (s *sitesBuilder) WithConfigTemplate(data any, format, configTemplate string) *sitesBuilder { s.T.Helper() if format == "" { format = "toml" } templ, err := template.New("test").Parse(configTemplate) if err != nil { s.Fatalf("Template parse failed: %s", err) } var b bytes.Buffer templ.Execute(&b, data) return s.WithConfigFile(format, b.String()) } func (s *sitesBuilder) WithViper(v config.Provider) *sitesBuilder { s.T.Helper() if s.configFileSet { s.T.Fatal("WithViper: use Viper or config.toml, not both") } defer func() { s.configSet = true }() // Write to a config file to make sure the tests follow the same code path. var buff bytes.Buffer m := v.Get("").(maps.Params) s.Assert(parser.InterfaceToConfig(m, metadecoders.TOML, &buff), qt.IsNil) return s.WithConfigFile("toml", buff.String()) } func (s *sitesBuilder) WithConfigFile(format, conf string) *sitesBuilder { s.T.Helper() if s.configSet { s.T.Fatal("WithConfigFile: use config.Config or config.toml, not both") } s.configFileSet = true filename := s.absFilename("config." + format) writeSource(s.T, s.Fs, filename, conf) s.configFormat = format return s } func (s *sitesBuilder) WithThemeConfigFile(format, conf string) *sitesBuilder { s.T.Helper() if s.theme == "" { s.theme = "test-theme" } filename := filepath.Join("themes", s.theme, "config."+format) writeSource(s.T, s.Fs, s.absFilename(filename), conf) return s } func (s *sitesBuilder) WithSourceFile(filenameContent ...string) *sitesBuilder { s.T.Helper() for i := 0; i < len(filenameContent); i += 2 { writeSource(s.T, s.Fs, s.absFilename(filenameContent[i]), filenameContent[i+1]) } return s } func (s *sitesBuilder) absFilename(filename string) string { filename = filepath.FromSlash(filename) if filepath.IsAbs(filename) { return filename } if s.workingDir != "" && !strings.HasPrefix(filename, s.workingDir) { filename = filepath.Join(s.workingDir, filename) } return filename } const commonConfigSections = ` [services] [services.disqus] shortname = "disqus_shortname" [services.googleAnalytics] id = "UA-ga_id" [privacy] [privacy.disqus] disable = false [privacy.googleAnalytics] respectDoNotTrack = true anonymizeIP = true [privacy.instagram] simple = true [privacy.twitter] enableDNT = true [privacy.vimeo] disable = false [privacy.youtube] disable = false privacyEnhanced = true ` func (s *sitesBuilder) WithSimpleConfigFile() *sitesBuilder { s.T.Helper() return s.WithSimpleConfigFileAndBaseURL("http://example.com/") } func (s *sitesBuilder) WithSimpleConfigFileAndBaseURL(baseURL string) *sitesBuilder { s.T.Helper() return s.WithSimpleConfigFileAndSettings(map[string]any{"baseURL": baseURL}) } func (s *sitesBuilder) WithSimpleConfigFileAndSettings(settings any) *sitesBuilder { s.T.Helper() var buf bytes.Buffer parser.InterfaceToConfig(settings, metadecoders.TOML, &buf) config := buf.String() + commonConfigSections return s.WithConfigFile("toml", config) } func (s *sitesBuilder) WithDefaultMultiSiteConfig() *sitesBuilder { defaultMultiSiteConfig := ` baseURL = "http://example.com/blog" paginate = 1 disablePathToLower = true defaultContentLanguage = "en" defaultContentLanguageInSubdir = true [permalinks] other = "/somewhere/else/:filename" [Taxonomies] tag = "tags" [Languages] [Languages.en] weight = 10 title = "In English" languageName = "English" [[Languages.en.menu.main]] url = "/" name = "Home" weight = 0 [Languages.fr] weight = 20 title = "Le Français" languageName = "Français" [Languages.fr.Taxonomies] plaque = "plaques" [Languages.nn] weight = 30 title = "På nynorsk" languageName = "Nynorsk" paginatePath = "side" [Languages.nn.Taxonomies] lag = "lag" [[Languages.nn.menu.main]] url = "/" name = "Heim" weight = 1 [Languages.nb] weight = 40 title = "På bokmål" languageName = "Bokmål" paginatePath = "side" [Languages.nb.Taxonomies] lag = "lag" ` + commonConfigSections return s.WithConfigFile("toml", defaultMultiSiteConfig) } func (s *sitesBuilder) WithSunset(in string) { // Write a real image into one of the bundle above. src, err := os.Open(filepath.FromSlash("testdata/sunset.jpg")) s.Assert(err, qt.IsNil) out, err := s.Fs.Source.Create(filepath.FromSlash(filepath.Join(s.workingDir, in))) s.Assert(err, qt.IsNil) _, err = io.Copy(out, src) s.Assert(err, qt.IsNil) out.Close() src.Close() } func (s *sitesBuilder) createFilenameContent(pairs []string) []filenameContent { var slice []filenameContent s.appendFilenameContent(&slice, pairs...) return slice } func (s *sitesBuilder) appendFilenameContent(slice *[]filenameContent, pairs ...string) { if len(pairs)%2 != 0 { panic("file content mismatch") } for i := 0; i < len(pairs); i += 2 { c := filenameContent{ filename: pairs[i], content: pairs[i+1], } *slice = append(*slice, c) } } func (s *sitesBuilder) WithContent(filenameContent ...string) *sitesBuilder { s.appendFilenameContent(&s.contentFilePairs, filenameContent...) return s } func (s *sitesBuilder) WithContentAdded(filenameContent ...string) *sitesBuilder { s.appendFilenameContent(&s.contentFilePairsAdded, filenameContent...) return s } func (s *sitesBuilder) WithTemplates(filenameContent ...string) *sitesBuilder { s.appendFilenameContent(&s.templateFilePairs, filenameContent...) return s } func (s *sitesBuilder) WithTemplatesAdded(filenameContent ...string) *sitesBuilder { s.appendFilenameContent(&s.templateFilePairsAdded, filenameContent...) return s } func (s *sitesBuilder) WithData(filenameContent ...string) *sitesBuilder { s.appendFilenameContent(&s.dataFilePairs, filenameContent...) return s } func (s *sitesBuilder) WithDataAdded(filenameContent ...string) *sitesBuilder { s.appendFilenameContent(&s.dataFilePairsAdded, filenameContent...) return s } func (s *sitesBuilder) WithI18n(filenameContent ...string) *sitesBuilder { s.appendFilenameContent(&s.i18nFilePairs, filenameContent...) return s } func (s *sitesBuilder) WithI18nAdded(filenameContent ...string) *sitesBuilder { s.appendFilenameContent(&s.i18nFilePairsAdded, filenameContent...) return s } func (s *sitesBuilder) EditFiles(filenameContent ...string) *sitesBuilder { for i := 0; i < len(filenameContent); i += 2 { filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1] absFilename := s.absFilename(filename) s.changedFiles = append(s.changedFiles, absFilename) writeSource(s.T, s.Fs, absFilename, content) } return s } func (s *sitesBuilder) RemoveFiles(filenames ...string) *sitesBuilder { for _, filename := range filenames { absFilename := s.absFilename(filename) s.removedFiles = append(s.removedFiles, absFilename) s.Assert(s.Fs.Source.Remove(absFilename), qt.IsNil) } return s } func (s *sitesBuilder) writeFilePairs(folder string, files []filenameContent) *sitesBuilder { // We have had some "filesystem ordering" bugs that we have not discovered in // our tests running with the in memory filesystem. // That file system is backed by a map so not sure how this helps, but some // randomness in tests doesn't hurt. // TODO(bep) this turns out to be more confusing than helpful. // s.rnd.Shuffle(len(files), func(i, j int) { files[i], files[j] = files[j], files[i] }) for _, fc := range files { target := folder // TODO(bep) clean up this magic. if strings.HasPrefix(fc.filename, folder) { target = "" } if s.workingDir != "" { target = filepath.Join(s.workingDir, target) } writeSource(s.T, s.Fs, filepath.Join(target, fc.filename), fc.content) } return s } func (s *sitesBuilder) CreateSites() *sitesBuilder { if err := s.CreateSitesE(); err != nil { s.Fatalf("Failed to create sites: %s", err) } s.Assert(s.Fs.PublishDir, qt.IsNotNil) s.Assert(s.Fs.WorkingDirReadOnly, qt.IsNotNil) return s } func (s *sitesBuilder) LoadConfig() error { if !s.configFileSet { s.WithSimpleConfigFile() } cfg, _, err := LoadConfig(ConfigSourceDescriptor{ WorkingDir: s.workingDir, Fs: s.Fs.Source, Logger: s.logger, Environ: s.environ, Filename: "config." + s.configFormat, }, func(cfg config.Provider) error { return nil }) if err != nil { return err } s.Cfg = cfg return nil } func (s *sitesBuilder) CreateSitesE() error { if !s.addNothing { if _, ok := s.Fs.Source.(*afero.OsFs); ok { for _, dir := range []string{ "content/sect", "layouts/_default", "layouts/_default/_markup", "layouts/partials", "layouts/shortcodes", "data", "i18n", } { if err := os.MkdirAll(filepath.Join(s.workingDir, dir), 0777); err != nil { return fmt.Errorf("failed to create %q: %w", dir, err) } } } s.addDefaults() s.writeFilePairs("content", s.contentFilePairsAdded) s.writeFilePairs("layouts", s.templateFilePairsAdded) s.writeFilePairs("data", s.dataFilePairsAdded) s.writeFilePairs("i18n", s.i18nFilePairsAdded) s.writeFilePairs("i18n", s.i18nFilePairs) s.writeFilePairs("data", s.dataFilePairs) s.writeFilePairs("content", s.contentFilePairs) s.writeFilePairs("layouts", s.templateFilePairs) } if err := s.LoadConfig(); err != nil { return fmt.Errorf("failed to load config: %w", err) } s.Fs.PublishDir = hugofs.NewCreateCountingFs(s.Fs.PublishDir) depsCfg := s.depsCfg depsCfg.Fs = s.Fs depsCfg.Cfg = s.Cfg depsCfg.Logger = s.logger depsCfg.Running = s.running sites, err := NewHugoSites(depsCfg) if err != nil { return fmt.Errorf("failed to create sites: %w", err) } s.H = sites return nil } func (s *sitesBuilder) BuildE(cfg BuildCfg) error { if s.H == nil { s.CreateSites() } return s.H.Build(cfg) } func (s *sitesBuilder) Build(cfg BuildCfg) *sitesBuilder { s.T.Helper() return s.build(cfg, false) } func (s *sitesBuilder) BuildFail(cfg BuildCfg) *sitesBuilder { s.T.Helper() return s.build(cfg, true) } func (s *sitesBuilder) changeEvents() []fsnotify.Event { var events []fsnotify.Event for _, v := range s.changedFiles { events = append(events, fsnotify.Event{ Name: v, Op: fsnotify.Write, }) } for _, v := range s.removedFiles { events = append(events, fsnotify.Event{ Name: v, Op: fsnotify.Remove, }) } return events } func (s *sitesBuilder) build(cfg BuildCfg, shouldFail bool) *sitesBuilder { s.Helper() defer func() { s.changedFiles = nil }() if s.H == nil { s.CreateSites() } err := s.H.Build(cfg, s.changeEvents()...) if err == nil { logErrorCount := s.H.NumLogErrors() if logErrorCount > 0 { err = fmt.Errorf("logged %d errors", logErrorCount) } } if err != nil && !shouldFail { s.Fatalf("Build failed: %s", err) } else if err == nil && shouldFail { s.Fatalf("Expected error") } return s } func (s *sitesBuilder) addDefaults() { var ( contentTemplate = `--- title: doc1 weight: 1 tags: - tag1 date: "2018-02-28" --- # doc1 *some "content"* {{< shortcode >}} {{< lingo >}} ` defaultContent = []string{ "content/sect/doc1.en.md", contentTemplate, "content/sect/doc1.fr.md", contentTemplate, "content/sect/doc1.nb.md", contentTemplate, "content/sect/doc1.nn.md", contentTemplate, } 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 }}" defaultTemplates = []string{ "_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 }}", "_default/list.html", "List Page " + listTemplateCommon, "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 }}", "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 }}", "_default/terms.html", "Taxonomy Term Page " + listTemplateCommon, "_default/taxonomy.html", "Taxonomy List Page " + listTemplateCommon, // Shortcodes "shortcodes/shortcode.html", "Shortcode: {{ i18n \"hello\" }}", // A shortcode in multiple languages "shortcodes/lingo.html", "LingoDefault", "shortcodes/lingo.fr.html", "LingoFrench", // Special templates "404.html", "404|{{ .Lang }}|{{ .Title }}", "robots.txt", "robots|{{ .Lang }}|{{ .Title }}", } defaultI18n = []string{ "en.yaml", ` hello: other: "Hello" `, "fr.yaml", ` hello: other: "Bonjour" `, } defaultData = []string{ "hugo.toml", "slogan = \"Hugo Rocks!\"", } ) if len(s.contentFilePairs) == 0 { s.writeFilePairs("content", s.createFilenameContent(defaultContent)) } if len(s.templateFilePairs) == 0 { s.writeFilePairs("layouts", s.createFilenameContent(defaultTemplates)) } if len(s.dataFilePairs) == 0 { s.writeFilePairs("data", s.createFilenameContent(defaultData)) } if len(s.i18nFilePairs) == 0 { s.writeFilePairs("i18n", s.createFilenameContent(defaultI18n)) } } func (s *sitesBuilder) Fatalf(format string, args ...any) { s.T.Helper() s.T.Fatalf(format, args...) } func (s *sitesBuilder) AssertFileContentFn(filename string, f func(s string) bool) { s.T.Helper() content := s.FileContent(filename) if !f(content) { s.Fatalf("Assert failed for %q in content\n%s", filename, content) } } // Helper to migrate tests to new format. func (s *sitesBuilder) DumpTxtar() string { var sb strings.Builder skipRe := regexp.MustCompile(`^(public|resources|package-lock.json|go.sum)`) afero.Walk(s.Fs.Source, s.workingDir, func(path string, info fs.FileInfo, err error) error { rel := strings.TrimPrefix(path, s.workingDir+"/") if skipRe.MatchString(rel) { if info.IsDir() { return filepath.SkipDir } return nil } if info == nil || info.IsDir() { return nil } sb.WriteString(fmt.Sprintf("-- %s --\n", rel)) b, err := afero.ReadFile(s.Fs.Source, path) s.Assert(err, qt.IsNil) sb.WriteString(strings.TrimSpace(string(b))) sb.WriteString("\n") return nil }) return sb.String() } func (s *sitesBuilder) AssertHome(matches ...string) { s.AssertFileContent("public/index.html", matches...) } func (s *sitesBuilder) AssertFileContent(filename string, matches ...string) { s.T.Helper() content := s.FileContent(filename) for _, m := range matches { lines := strings.Split(m, "\n") for _, match := range lines { match = strings.TrimSpace(match) if match == "" { continue } if !strings.Contains(content, match) { s.Fatalf("No match for %q in content for %s\n%s\n%q", match, filename, content, content) } } } } func (s *sitesBuilder) AssertFileDoesNotExist(filename string) { if s.CheckExists(filename) { s.Fatalf("File %q exists but must not exist.", filename) } } func (s *sitesBuilder) AssertImage(width, height int, filename string) { f, err := s.Fs.WorkingDirReadOnly.Open(filename) s.Assert(err, qt.IsNil) defer f.Close() cfg, err := jpeg.DecodeConfig(f) s.Assert(err, qt.IsNil) s.Assert(cfg.Width, qt.Equals, width) s.Assert(cfg.Height, qt.Equals, height) } func (s *sitesBuilder) AssertNoDuplicateWrites() { s.Helper() d := s.Fs.PublishDir.(hugofs.DuplicatesReporter) s.Assert(d.ReportDuplicates(), qt.Equals, "") } func (s *sitesBuilder) FileContent(filename string) string { s.Helper() filename = filepath.FromSlash(filename) return readWorkingDir(s.T, s.Fs, filename) } func (s *sitesBuilder) AssertObject(expected string, object any) { s.T.Helper() got := s.dumper.Sdump(object) expected = strings.TrimSpace(expected) if expected != got { fmt.Println(got) diff := htesting.DiffStrings(expected, got) s.Fatalf("diff:\n%s\nexpected\n%s\ngot\n%s", diff, expected, got) } } func (s *sitesBuilder) AssertFileContentRe(filename string, matches ...string) { content := readWorkingDir(s.T, s.Fs, filename) for _, match := range matches { r := regexp.MustCompile("(?s)" + match) if !r.MatchString(content) { s.Fatalf("No match for %q in content for %s\n%q", match, filename, content) } } } func (s *sitesBuilder) CheckExists(filename string) bool { return workingDirExists(s.Fs, filepath.Clean(filename)) } func (s *sitesBuilder) GetPage(ref string) page.Page { p, err := s.H.Sites[0].getPageNew(nil, ref) s.Assert(err, qt.IsNil) return p } func (s *sitesBuilder) GetPageRel(p page.Page, ref string) page.Page { p, err := s.H.Sites[0].getPageNew(p, ref) s.Assert(err, qt.IsNil) return p } func (s *sitesBuilder) NpmInstall() hexec.Runner { sc := security.DefaultConfig sc.Exec.Allow = security.NewWhitelist("npm") ex := hexec.New(sc) command, err := ex.New("npm", "install") s.Assert(err, qt.IsNil) return command } func newTestHelper(cfg config.Provider, fs *hugofs.Fs, t testing.TB) testHelper { return testHelper{ Cfg: cfg, Fs: fs, C: qt.New(t), } } type testHelper struct { Cfg config.Provider Fs *hugofs.Fs *qt.C } func (th testHelper) assertFileContent(filename string, matches ...string) { th.Helper() filename = th.replaceDefaultContentLanguageValue(filename) content := readWorkingDir(th, th.Fs, filename) for _, match := range matches { match = th.replaceDefaultContentLanguageValue(match) th.Assert(strings.Contains(content, match), qt.Equals, true, qt.Commentf(match+" not in: \n"+content)) } } func (th testHelper) assertFileContentRegexp(filename string, matches ...string) { filename = th.replaceDefaultContentLanguageValue(filename) content := readWorkingDir(th, th.Fs, filename) for _, match := range matches { match = th.replaceDefaultContentLanguageValue(match) r := regexp.MustCompile(match) matches := r.MatchString(content) if !matches { fmt.Println("Expected to match regexp:\n"+match+"\nGot:\n", content) } th.Assert(matches, qt.Equals, true) } } func (th testHelper) assertFileNotExist(filename string) { exists, err := helpers.Exists(filename, th.Fs.PublishDir) th.Assert(err, qt.IsNil) th.Assert(exists, qt.Equals, false) } func (th testHelper) replaceDefaultContentLanguageValue(value string) string { defaultInSubDir := th.Cfg.GetBool("defaultContentLanguageInSubDir") replace := th.Cfg.GetString("defaultContentLanguage") + "/" if !defaultInSubDir { value = strings.Replace(value, replace, "", 1) } return value } func loadTestConfig(fs afero.Fs, withConfig ...func(cfg config.Provider) error) (config.Provider, error) { v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs}, withConfig...) return v, err } func newTestCfgBasic() (config.Provider, *hugofs.Fs) { mm := afero.NewMemMapFs() v := config.NewWithTestDefaults() v.Set("defaultContentLanguageInSubdir", true) fs := hugofs.NewFrom(hugofs.NewBaseFileDecorator(mm), v) return v, fs } func newTestCfg(withConfig ...func(cfg config.Provider) error) (config.Provider, *hugofs.Fs) { mm := afero.NewMemMapFs() v, err := loadTestConfig(mm, func(cfg config.Provider) error { // Default is false, but true is easier to use as default in tests cfg.Set("defaultContentLanguageInSubdir", true) for _, w := range withConfig { w(cfg) } return nil }) if err != nil && err != ErrNoConfigFile { panic(err) } fs := hugofs.NewFrom(hugofs.NewBaseFileDecorator(mm), v) return v, fs } func newTestSitesFromConfig(t testing.TB, afs afero.Fs, tomlConfig string, layoutPathContentPairs ...string) (testHelper, *HugoSites) { if len(layoutPathContentPairs)%2 != 0 { t.Fatalf("Layouts must be provided in pairs") } c := qt.New(t) writeToFs(t, afs, filepath.Join("content", ".gitkeep"), "") writeToFs(t, afs, "config.toml", tomlConfig) cfg, err := LoadConfigDefault(afs) c.Assert(err, qt.IsNil) fs := hugofs.NewFrom(afs, cfg) th := newTestHelper(cfg, fs, t) for i := 0; i < len(layoutPathContentPairs); i += 2 { writeSource(t, fs, layoutPathContentPairs[i], layoutPathContentPairs[i+1]) } h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) c.Assert(err, qt.IsNil) return th, h } func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tpl.TemplateManager) error { return func(templ tpl.TemplateManager) error { for i := 0; i < len(additionalTemplates); i += 2 { err := templ.AddTemplate(additionalTemplates[i], additionalTemplates[i+1]) if err != nil { return err } } return nil } } // TODO(bep) replace these with the builder func buildSingleSite(t testing.TB, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site { t.Helper() return buildSingleSiteExpected(t, false, false, depsCfg, buildCfg) } func buildSingleSiteExpected(t testing.TB, expectSiteInitError, expectBuildError bool, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site { t.Helper() b := newTestSitesBuilderFromDepsCfg(t, depsCfg).WithNothingAdded() err := b.CreateSitesE() if expectSiteInitError { b.Assert(err, qt.Not(qt.IsNil)) return nil } else { b.Assert(err, qt.IsNil) } h := b.H b.Assert(len(h.Sites), qt.Equals, 1) if expectBuildError { b.Assert(h.Build(buildCfg), qt.Not(qt.IsNil)) return nil } b.Assert(h.Build(buildCfg), qt.IsNil) return h.Sites[0] } func writeSourcesToSource(t *testing.T, base string, fs *hugofs.Fs, sources ...[2]string) { for _, src := range sources { writeSource(t, fs, filepath.Join(base, src[0]), src[1]) } } func getPage(in page.Page, ref string) page.Page { p, err := in.GetPage(ref) if err != nil { panic(err) } return p } func content(c resource.ContentProvider) string { cc, err := c.Content() if err != nil { panic(err) } ccs, err := cast.ToStringE(cc) if err != nil { panic(err) } return ccs } func pagesToString(pages ...page.Page) string { var paths []string for _, p := range pages { paths = append(paths, p.Pathc()) } sort.Strings(paths) return strings.Join(paths, "|") } func dumpPagesLinks(pages ...page.Page) { var links []string for _, p := range pages { links = append(links, p.RelPermalink()) } sort.Strings(links) for _, link := range links { fmt.Println(link) } } func dumpPages(pages ...page.Page) { fmt.Println("---------") for _, p := range pages { fmt.Printf("Kind: %s Title: %-10s RelPermalink: %-10s Path: %-10s sections: %s Lang: %s\n", p.Kind(), p.Title(), p.RelPermalink(), p.Pathc(), p.SectionsPath(), p.Lang()) } } func dumpSPages(pages ...*pageState) { for i, p := range pages { fmt.Printf("%d: Kind: %s Title: %-10s RelPermalink: %-10s Path: %-10s sections: %s\n", i+1, p.Kind(), p.Title(), p.RelPermalink(), p.Pathc(), p.SectionsPath()) } } func printStringIndexes(s string) { lines := strings.Split(s, "\n") i := 0 for _, line := range lines { for _, r := range line { fmt.Printf("%-3s", strconv.Itoa(i)) i += utf8.RuneLen(r) } i++ fmt.Println() for _, r := range line { fmt.Printf("%-3s", string(r)) } fmt.Println() } } // See https://github.com/golang/go/issues/19280 // Not in use. var parallelEnabled = true func parallel(t *testing.T) { if parallelEnabled { t.Parallel() } } func skipSymlink(t *testing.T) { if runtime.GOOS == "windows" && os.Getenv("CI") == "" { t.Skip("skip symlink test on local Windows (needs admin)") } } func captureStderr(f func() error) (string, error) { old := os.Stderr r, w, _ := os.Pipe() os.Stderr = w err := f() w.Close() os.Stderr = old var buf bytes.Buffer io.Copy(&buf, r) return buf.String(), err } func captureStdout(f func() error) (string, error) { old := os.Stdout r, w, _ := os.Pipe() os.Stdout = w err := f() w.Close() os.Stdout = old var buf bytes.Buffer io.Copy(&buf, r) return buf.String(), err }