github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/hugolib/testhelpers_test.go (about)

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