summaryrefslogtreecommitdiffstats
path: root/hugolib/testhelpers_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'hugolib/testhelpers_test.go')
-rw-r--r--hugolib/testhelpers_test.go1072
1 files changed, 1072 insertions, 0 deletions
diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go
new file mode 100644
index 000000000..eace35c97
--- /dev/null
+++ b/hugolib/testhelpers_test.go
@@ -0,0 +1,1072 @@
+package hugolib
+
+import (
+ "image/jpeg"
+ "io"
+ "math/rand"
+ "path/filepath"
+ "runtime"
+ "sort"
+ "strconv"
+ "testing"
+ "time"
+ "unicode/utf8"
+
+ "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/pkg/errors"
+
+ "bytes"
+ "fmt"
+ "regexp"
+ "strings"
+ "text/template"
+
+ "github.com/fsnotify/fsnotify"
+ "github.com/gohugoio/hugo/common/herrors"
+ "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/spf13/viper"
+
+ "os"
+
+ "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
+ viperSet 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 := viper.New()
+ 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.(*viper.Viper))
+
+}
+
+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 interface{}, 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 *viper.Viper) *sitesBuilder {
+ s.T.Helper()
+ if s.configFileSet {
+ s.T.Fatal("WithViper: use Viper or config.toml, not both")
+ }
+ defer func() {
+ s.viperSet = true
+ }()
+
+ // Write to a config file to make sure the tests follow the same code path.
+ var buff bytes.Buffer
+ m := v.AllSettings()
+ 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.viperSet {
+ s.T.Fatal("WithConfigFile: use Viper 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 = "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]interface{}{"baseURL": baseURL})
+}
+
+func (s *sitesBuilder) WithSimpleConfigFileAndSettings(settings interface{}) *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 {
+ var defaultMultiSiteConfig = `
+baseURL = "http://example.com/blog"
+
+paginate = 1
+disablePathToLower = true
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = true
+
+[permalinks]
+other = "/somewhere/else/:filename"
+
+[blackfriday]
+angledQuotes = true
+
+[Taxonomies]
+tag = "tags"
+
+[Languages]
+[Languages.en]
+weight = 10
+title = "In English"
+languageName = "English"
+[Languages.en.blackfriday]
+angledQuotes = false
+[[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 {
+ herrors.PrintStackTraceFromErr(err)
+ s.Fatalf("Failed to create sites: %s", err)
+ }
+
+ 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 errors.Wrapf(err, "failed to create %q", dir)
+ }
+ }
+ }
+
+ 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 errors.Wrap(err, "failed to load config")
+ }
+
+ s.Fs.Destination = hugofs.NewCreateCountingFs(s.Fs.Destination)
+
+ 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 errors.Wrap(err, "failed to create sites")
+ }
+ 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 {
+ herrors.PrintStackTraceFromErr(err)
+ 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 ...interface{}) {
+ 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)
+ }
+}
+
+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) AssertImage(width, height int, filename string) {
+ filename = filepath.Join(s.workingDir, filename)
+ f, err := s.Fs.Destination.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.Destination.(hugofs.DuplicatesReporter)
+ s.Assert(d.ReportDuplicates(), qt.Equals, "")
+}
+
+func (s *sitesBuilder) FileContent(filename string) string {
+ s.T.Helper()
+ filename = filepath.FromSlash(filename)
+ if !strings.HasPrefix(filename, s.workingDir) {
+ filename = filepath.Join(s.workingDir, filename)
+ }
+ return readDestination(s.T, s.Fs, filename)
+}
+
+func (s *sitesBuilder) AssertObject(expected string, object interface{}) {
+ 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 := readDestination(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 destinationExists(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 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 := readDestination(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 := readDestination(th, th.Fs, filename)
+ for _, match := range matches {
+ match = th.replaceDefaultContentLanguageValue(match)
+ r := regexp.MustCompile(match)
+ matches := r.MatchString(content)
+ if !matches {
+ fmt.Println(match+":\n", content)
+ }
+ th.Assert(matches, qt.Equals, true)
+ }
+}
+
+func (th testHelper) assertFileNotExist(filename string) {
+ exists, err := helpers.Exists(filename, th.Fs.Destination)
+ 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) (*viper.Viper, error) {
+ v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs}, withConfig...)
+ return v, err
+}
+
+func newTestCfgBasic() (*viper.Viper, *hugofs.Fs) {
+ mm := afero.NewMemMapFs()
+ v := viper.New()
+ v.Set("defaultContentLanguageInSubdir", true)
+
+ fs := hugofs.NewFrom(hugofs.NewBaseFileDecorator(mm), v)
+
+ return v, fs
+
+}
+
+func newTestCfg(withConfig ...func(cfg config.Provider) error) (*viper.Viper, *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, expectSiteInitEror, expectBuildError bool, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site {
+ t.Helper()
+ b := newTestSitesBuilderFromDepsCfg(t, depsCfg).WithNothingAdded()
+
+ err := b.CreateSitesE()
+
+ if expectSiteInitEror {
+ 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.Path())
+ }
+ sort.Strings(paths)
+ return strings.Join(paths, "|")
+}
+
+func dumpPages(pages ...page.Page) {
+ fmt.Println("---------")
+ for _, p := range pages {
+ var meta interface{}
+ if p.File() != nil && p.File().FileInfo() != nil {
+ meta = p.File().FileInfo().Meta()
+ }
+ fmt.Printf("Kind: %s Title: %-10s RelPermalink: %-10s Path: %-10s sections: %s Lang: %s Meta: %v\n",
+ p.Kind(), p.Title(), p.RelPermalink(), p.Path(), p.SectionsPath(), p.Lang(), meta)
+ }
+}
+
+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.Path(), 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()
+
+ }
+}
+
+func isCI() bool {
+ return (os.Getenv("CI") != "" || os.Getenv("CI_LOCAL") != "") && os.Getenv("CIRCLE_BRANCH") == ""
+}
+
+// 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
+}