github.com/neohugo/neohugo@v0.123.8/hugolib/testhelpers_test.go (about)

     1  package hugolib
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"image/jpeg"
     8  	"io"
     9  	"io/fs"
    10  	"math/rand"
    11  	"os"
    12  	"path/filepath"
    13  	"regexp"
    14  	"strings"
    15  	"testing"
    16  	"text/template"
    17  	"time"
    18  
    19  	"github.com/neohugo/neohugo/config/allconfig"
    20  	"github.com/neohugo/neohugo/config/security"
    21  	"github.com/neohugo/neohugo/htesting"
    22  
    23  	"github.com/neohugo/neohugo/output"
    24  
    25  	"github.com/google/go-cmp/cmp"
    26  	"github.com/neohugo/neohugo/parser/metadecoders"
    27  
    28  	"github.com/neohugo/neohugo/parser"
    29  
    30  	"github.com/fsnotify/fsnotify"
    31  	"github.com/neohugo/neohugo/common/hexec"
    32  	"github.com/neohugo/neohugo/common/loggers"
    33  	"github.com/neohugo/neohugo/common/maps"
    34  	"github.com/neohugo/neohugo/config"
    35  	"github.com/neohugo/neohugo/deps"
    36  	"github.com/neohugo/neohugo/resources/page"
    37  	"github.com/sanity-io/litter"
    38  	"github.com/spf13/afero"
    39  	"github.com/spf13/cast"
    40  
    41  	"github.com/neohugo/neohugo/helpers"
    42  
    43  	"github.com/neohugo/neohugo/resources/resource"
    44  
    45  	qt "github.com/frankban/quicktest"
    46  	"github.com/neohugo/neohugo/hugofs"
    47  )
    48  
    49  var (
    50  	deepEqualsPages         = qt.CmpEquals(cmp.Comparer(func(p1, p2 *pageState) bool { return p1 == p2 }))
    51  	deepEqualsOutputFormats = qt.CmpEquals(cmp.Comparer(func(o1, o2 output.Format) bool {
    52  		return o1.Name == o2.Name && o1.MediaType.Type == o2.MediaType.Type
    53  	}))
    54  )
    55  
    56  type sitesBuilder struct {
    57  	Cfg     config.Provider
    58  	Configs *allconfig.Configs
    59  
    60  	environ []string
    61  
    62  	Fs      *hugofs.Fs
    63  	T       testing.TB
    64  	depsCfg deps.DepsCfg
    65  
    66  	*qt.C
    67  
    68  	logger loggers.Logger
    69  	rnd    *rand.Rand
    70  	dumper litter.Options
    71  
    72  	// Used to test partial rebuilds.
    73  	changedFiles []string
    74  	removedFiles []string
    75  
    76  	// Aka the Hugo server mode.
    77  	running bool
    78  
    79  	H *HugoSites
    80  
    81  	theme string
    82  
    83  	// Default toml
    84  	configFormat  string
    85  	configFileSet bool
    86  	configSet     bool
    87  
    88  	// Default is empty.
    89  	// TODO(bep) revisit this and consider always setting it to something.
    90  	// Consider this in relation to using the BaseFs.PublishFs to all publishing.
    91  	workingDir string
    92  
    93  	addNothing bool
    94  	// Base data/content
    95  	contentFilePairs  []filenameContent
    96  	templateFilePairs []filenameContent
    97  	i18nFilePairs     []filenameContent
    98  	dataFilePairs     []filenameContent
    99  
   100  	// Additional data/content.
   101  	// As in "use the base, but add these on top".
   102  	contentFilePairsAdded  []filenameContent
   103  	templateFilePairsAdded []filenameContent
   104  	i18nFilePairsAdded     []filenameContent
   105  	dataFilePairsAdded     []filenameContent
   106  }
   107  
   108  type filenameContent struct {
   109  	filename string
   110  	content  string
   111  }
   112  
   113  func newTestSitesBuilder(t testing.TB) *sitesBuilder {
   114  	v := config.New()
   115  	v.Set("publishDir", "public")
   116  	v.Set("disableLiveReload", true)
   117  	fs := hugofs.NewFromOld(afero.NewMemMapFs(), 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  
   131  func 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.Configs.LoadingInfo.BaseConfig.WorkingDir
   142  
   143  	b.WithWorkingDir(workingDir)
   144  
   145  	//nolint
   146  	return b
   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 any, 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  
   189  	if err := templ.Execute(&b, data); err != nil {
   190  		s.Fatalf("Template Execute failed: %s", err)
   191  	}
   192  	return s.WithConfigFile(format, b.String())
   193  }
   194  
   195  func (s *sitesBuilder) WithViper(v config.Provider) *sitesBuilder {
   196  	s.T.Helper()
   197  	if s.configFileSet {
   198  		s.T.Fatal("WithViper: use Viper or config.toml, not both")
   199  	}
   200  	defer func() {
   201  		s.configSet = true
   202  	}()
   203  
   204  	// Write to a config file to make sure the tests follow the same code path.
   205  	var buff bytes.Buffer
   206  	m := v.Get("").(maps.Params)
   207  	s.Assert(parser.InterfaceToConfig(m, metadecoders.TOML, &buff), qt.IsNil)
   208  	return s.WithConfigFile("toml", buff.String())
   209  }
   210  
   211  func (s *sitesBuilder) WithConfigFile(format, conf string) *sitesBuilder {
   212  	s.T.Helper()
   213  	if s.configSet {
   214  		s.T.Fatal("WithConfigFile: use config.Config or config.toml, not both")
   215  	}
   216  	s.configFileSet = true
   217  	filename := s.absFilename("config." + format)
   218  	writeSource(s.T, s.Fs, filename, conf)
   219  	s.configFormat = format
   220  	return s
   221  }
   222  
   223  func (s *sitesBuilder) WithThemeConfigFile(format, conf string) *sitesBuilder {
   224  	s.T.Helper()
   225  	if s.theme == "" {
   226  		s.theme = "test-theme"
   227  	}
   228  	filename := filepath.Join("themes", s.theme, "config."+format)
   229  	writeSource(s.T, s.Fs, s.absFilename(filename), conf)
   230  	return s
   231  }
   232  
   233  func (s *sitesBuilder) WithSourceFile(filenameContent ...string) *sitesBuilder {
   234  	s.T.Helper()
   235  	for i := 0; i < len(filenameContent); i += 2 {
   236  		writeSource(s.T, s.Fs, s.absFilename(filenameContent[i]), filenameContent[i+1])
   237  	}
   238  	return s
   239  }
   240  
   241  func (s *sitesBuilder) absFilename(filename string) string {
   242  	filename = filepath.FromSlash(filename)
   243  	if filepath.IsAbs(filename) {
   244  		return filename
   245  	}
   246  	if s.workingDir != "" && !strings.HasPrefix(filename, s.workingDir) {
   247  		filename = filepath.Join(s.workingDir, filename)
   248  	}
   249  	return filename
   250  }
   251  
   252  const commonConfigSections = `
   253  
   254  [services]
   255  [services.disqus]
   256  shortname = "disqus_shortname"
   257  [services.googleAnalytics]
   258  id = "UA-ga_id"
   259  
   260  [privacy]
   261  [privacy.disqus]
   262  disable = false
   263  [privacy.googleAnalytics]
   264  respectDoNotTrack = true
   265  anonymizeIP = true
   266  [privacy.instagram]
   267  simple = true
   268  [privacy.twitter]
   269  enableDNT = true
   270  [privacy.vimeo]
   271  disable = false
   272  [privacy.youtube]
   273  disable = false
   274  privacyEnhanced = true
   275  
   276  `
   277  
   278  func (s *sitesBuilder) WithSimpleConfigFile() *sitesBuilder {
   279  	s.T.Helper()
   280  	return s.WithSimpleConfigFileAndBaseURL("http://example.com/")
   281  }
   282  
   283  func (s *sitesBuilder) WithSimpleConfigFileAndBaseURL(baseURL string) *sitesBuilder {
   284  	s.T.Helper()
   285  	return s.WithSimpleConfigFileAndSettings(map[string]any{"baseURL": baseURL})
   286  }
   287  
   288  func (s *sitesBuilder) WithSimpleConfigFileAndSettings(settings any) *sitesBuilder {
   289  	s.T.Helper()
   290  	var buf bytes.Buffer
   291  	//nolint
   292  	parser.InterfaceToConfig(settings, metadecoders.TOML, &buf)
   293  	config := buf.String() + commonConfigSections
   294  	return s.WithConfigFile("toml", config)
   295  }
   296  
   297  func (s *sitesBuilder) WithDefaultMultiSiteConfig() *sitesBuilder {
   298  	defaultMultiSiteConfig := `
   299  baseURL = "http://example.com/blog"
   300  
   301  paginate = 1
   302  disablePathToLower = true
   303  defaultContentLanguage = "en"
   304  defaultContentLanguageInSubdir = true
   305  
   306  [permalinks]
   307  other = "/somewhere/else/:filename"
   308  
   309  [Taxonomies]
   310  tag = "tags"
   311  
   312  [Languages]
   313  [Languages.en]
   314  weight = 10
   315  title = "In English"
   316  languageName = "English"
   317  [[Languages.en.menu.main]]
   318  url    = "/"
   319  name   = "Home"
   320  weight = 0
   321  
   322  [Languages.fr]
   323  weight = 20
   324  title = "Le Français"
   325  languageName = "Français"
   326  [Languages.fr.Taxonomies]
   327  plaque = "plaques"
   328  
   329  [Languages.nn]
   330  weight = 30
   331  title = "På nynorsk"
   332  languageName = "Nynorsk"
   333  paginatePath = "side"
   334  [Languages.nn.Taxonomies]
   335  lag = "lag"
   336  [[Languages.nn.menu.main]]
   337  url    = "/"
   338  name   = "Heim"
   339  weight = 1
   340  
   341  [Languages.nb]
   342  weight = 40
   343  title = "På bokmål"
   344  languageName = "Bokmål"
   345  paginatePath = "side"
   346  [Languages.nb.Taxonomies]
   347  lag = "lag"
   348  ` + commonConfigSections
   349  
   350  	return s.WithConfigFile("toml", defaultMultiSiteConfig)
   351  }
   352  
   353  func (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  
   368  func (s *sitesBuilder) createFilenameContent(pairs []string) []filenameContent {
   369  	var slice []filenameContent
   370  	s.appendFilenameContent(&slice, pairs...)
   371  	return slice
   372  }
   373  
   374  func (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  
   387  func (s *sitesBuilder) WithContent(filenameContent ...string) *sitesBuilder {
   388  	s.appendFilenameContent(&s.contentFilePairs, filenameContent...)
   389  	return s
   390  }
   391  
   392  func (s *sitesBuilder) WithContentAdded(filenameContent ...string) *sitesBuilder {
   393  	s.appendFilenameContent(&s.contentFilePairsAdded, filenameContent...)
   394  	return s
   395  }
   396  
   397  func (s *sitesBuilder) WithTemplates(filenameContent ...string) *sitesBuilder {
   398  	s.appendFilenameContent(&s.templateFilePairs, filenameContent...)
   399  	return s
   400  }
   401  
   402  func (s *sitesBuilder) WithTemplatesAdded(filenameContent ...string) *sitesBuilder {
   403  	s.appendFilenameContent(&s.templateFilePairsAdded, filenameContent...)
   404  	return s
   405  }
   406  
   407  func (s *sitesBuilder) WithData(filenameContent ...string) *sitesBuilder {
   408  	s.appendFilenameContent(&s.dataFilePairs, filenameContent...)
   409  	return s
   410  }
   411  
   412  func (s *sitesBuilder) WithDataAdded(filenameContent ...string) *sitesBuilder {
   413  	s.appendFilenameContent(&s.dataFilePairsAdded, filenameContent...)
   414  	return s
   415  }
   416  
   417  func (s *sitesBuilder) WithI18n(filenameContent ...string) *sitesBuilder {
   418  	s.appendFilenameContent(&s.i18nFilePairs, filenameContent...)
   419  	return s
   420  }
   421  
   422  func (s *sitesBuilder) WithI18nAdded(filenameContent ...string) *sitesBuilder {
   423  	s.appendFilenameContent(&s.i18nFilePairsAdded, filenameContent...)
   424  	return s
   425  }
   426  
   427  func (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  
   438  func (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  
   447  func (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  
   471  func (s *sitesBuilder) CreateSites() *sitesBuilder {
   472  	if err := s.CreateSitesE(); err != nil {
   473  		s.Fatalf("Failed to create sites: %s", err)
   474  	}
   475  
   476  	s.Assert(s.Fs.PublishDir, qt.IsNotNil)
   477  	s.Assert(s.Fs.WorkingDirReadOnly, qt.IsNotNil)
   478  
   479  	return s
   480  }
   481  
   482  func (s *sitesBuilder) LoadConfig() error {
   483  	if !s.configFileSet {
   484  		s.WithSimpleConfigFile()
   485  	}
   486  
   487  	flags := config.New()
   488  	flags.Set("internal", map[string]any{
   489  		"running": s.running,
   490  		"watch":   s.running,
   491  	})
   492  
   493  	if s.workingDir != "" {
   494  		flags.Set("workingDir", s.workingDir)
   495  	}
   496  
   497  	res, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{
   498  		Fs:       s.Fs.Source,
   499  		Logger:   s.logger,
   500  		Flags:    flags,
   501  		Environ:  s.environ,
   502  		Filename: "config." + s.configFormat,
   503  	})
   504  	if err != nil {
   505  		return err
   506  	}
   507  
   508  	s.Cfg = res.LoadingInfo.Cfg
   509  	s.Configs = res
   510  
   511  	return nil
   512  }
   513  
   514  func (s *sitesBuilder) CreateSitesE() error {
   515  	if !s.addNothing {
   516  		if _, ok := s.Fs.Source.(*afero.OsFs); ok {
   517  			for _, dir := range []string{
   518  				"content/sect",
   519  				"layouts/_default",
   520  				"layouts/_default/_markup",
   521  				"layouts/partials",
   522  				"layouts/shortcodes",
   523  				"data",
   524  				"i18n",
   525  			} {
   526  				if err := os.MkdirAll(filepath.Join(s.workingDir, dir), 0o777); err != nil {
   527  					return fmt.Errorf("failed to create %q: %w", dir, err)
   528  				}
   529  			}
   530  		}
   531  
   532  		s.addDefaults()
   533  		s.writeFilePairs("content", s.contentFilePairsAdded)
   534  		s.writeFilePairs("layouts", s.templateFilePairsAdded)
   535  		s.writeFilePairs("data", s.dataFilePairsAdded)
   536  		s.writeFilePairs("i18n", s.i18nFilePairsAdded)
   537  
   538  		s.writeFilePairs("i18n", s.i18nFilePairs)
   539  		s.writeFilePairs("data", s.dataFilePairs)
   540  		s.writeFilePairs("content", s.contentFilePairs)
   541  		s.writeFilePairs("layouts", s.templateFilePairs)
   542  
   543  	}
   544  
   545  	if err := s.LoadConfig(); err != nil {
   546  		return fmt.Errorf("failed to load config: %w", err)
   547  	}
   548  
   549  	s.Fs.PublishDir = hugofs.NewCreateCountingFs(s.Fs.PublishDir)
   550  
   551  	depsCfg := s.depsCfg
   552  	depsCfg.Fs = s.Fs
   553  	if depsCfg.Configs.IsZero() {
   554  		depsCfg.Configs = s.Configs
   555  	}
   556  	depsCfg.TestLogger = s.logger
   557  
   558  	sites, err := NewHugoSites(depsCfg)
   559  	if err != nil {
   560  		return fmt.Errorf("failed to create sites: %w", err)
   561  	}
   562  	s.H = sites
   563  
   564  	return nil
   565  }
   566  
   567  func (s *sitesBuilder) BuildE(cfg BuildCfg) error {
   568  	if s.H == nil {
   569  		s.CreateSites()
   570  	}
   571  
   572  	return s.H.Build(cfg)
   573  }
   574  
   575  func (s *sitesBuilder) Build(cfg BuildCfg) *sitesBuilder {
   576  	s.T.Helper()
   577  	return s.build(cfg, false)
   578  }
   579  
   580  func (s *sitesBuilder) BuildFail(cfg BuildCfg) *sitesBuilder {
   581  	s.T.Helper()
   582  	return s.build(cfg, true)
   583  }
   584  
   585  func (s *sitesBuilder) changeEvents() []fsnotify.Event {
   586  	var events []fsnotify.Event
   587  
   588  	for _, v := range s.changedFiles {
   589  		events = append(events, fsnotify.Event{
   590  			Name: v,
   591  			Op:   fsnotify.Write,
   592  		})
   593  	}
   594  	for _, v := range s.removedFiles {
   595  		events = append(events, fsnotify.Event{
   596  			Name: v,
   597  			Op:   fsnotify.Remove,
   598  		})
   599  	}
   600  
   601  	return events
   602  }
   603  
   604  func (s *sitesBuilder) build(cfg BuildCfg, shouldFail bool) *sitesBuilder {
   605  	s.Helper()
   606  	defer func() {
   607  		s.changedFiles = nil
   608  	}()
   609  
   610  	if s.H == nil {
   611  		s.CreateSites()
   612  	}
   613  
   614  	err := s.H.Build(cfg, s.changeEvents()...)
   615  
   616  	if err == nil {
   617  		logErrorCount := s.H.NumLogErrors()
   618  		if logErrorCount > 0 {
   619  			err = fmt.Errorf("logged %d errors", logErrorCount)
   620  		}
   621  	}
   622  	if err != nil && !shouldFail {
   623  		s.Fatalf("Build failed: %s", err)
   624  	} else if err == nil && shouldFail {
   625  		s.Fatalf("Expected error")
   626  	}
   627  
   628  	return s
   629  }
   630  
   631  func (s *sitesBuilder) addDefaults() {
   632  	var (
   633  		contentTemplate = `---
   634  title: doc1
   635  weight: 1
   636  tags:
   637   - tag1
   638  date: "2018-02-28"
   639  ---
   640  # doc1
   641  *some "content"*
   642  {{< shortcode >}}
   643  {{< lingo >}}
   644  `
   645  
   646  		defaultContent = []string{
   647  			"content/sect/doc1.en.md", contentTemplate,
   648  			"content/sect/doc1.fr.md", contentTemplate,
   649  			"content/sect/doc1.nb.md", contentTemplate,
   650  			"content/sect/doc1.nn.md", contentTemplate,
   651  		}
   652  
   653  		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 }}"
   654  
   655  		defaultTemplates = []string{
   656  			"_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 }}",
   657  			"_default/list.html", "List Page " + listTemplateCommon,
   658  			"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  }}|String Resource Permalink: {{ ( \"Hugo Pipes\" | resources.FromString \"text/pipes.txt\").Permalink  }}",
   659  			"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  }}|String Resource Permalink: {{ ( \"Hugo Pipes\" | resources.FromString \"text/pipes.txt\").Permalink  }}",
   660  			"_default/terms.html", "Taxonomy Term Page " + listTemplateCommon,
   661  			"_default/taxonomy.html", "Taxonomy List Page " + listTemplateCommon,
   662  			// Shortcodes
   663  			"shortcodes/shortcode.html", "Shortcode: {{ i18n \"hello\" }}",
   664  			// A shortcode in multiple languages
   665  			"shortcodes/lingo.html", "LingoDefault",
   666  			"shortcodes/lingo.fr.html", "LingoFrench",
   667  			// Special templates
   668  			"404.html", "404|{{ .Lang }}|{{ .Title }}",
   669  			"robots.txt", "robots|{{ .Lang }}|{{ .Title }}",
   670  		}
   671  
   672  		defaultI18n = []string{
   673  			"en.yaml", `
   674  hello:
   675    other: "Hello"
   676  `,
   677  			"fr.yaml", `
   678  hello:
   679    other: "Bonjour"
   680  `,
   681  		}
   682  
   683  		defaultData = []string{
   684  			"hugo.toml", "slogan = \"Hugo Rocks!\"",
   685  		}
   686  	)
   687  
   688  	if len(s.contentFilePairs) == 0 {
   689  		s.writeFilePairs("content", s.createFilenameContent(defaultContent))
   690  	}
   691  
   692  	if len(s.templateFilePairs) == 0 {
   693  		s.writeFilePairs("layouts", s.createFilenameContent(defaultTemplates))
   694  	}
   695  	if len(s.dataFilePairs) == 0 {
   696  		s.writeFilePairs("data", s.createFilenameContent(defaultData))
   697  	}
   698  	if len(s.i18nFilePairs) == 0 {
   699  		s.writeFilePairs("i18n", s.createFilenameContent(defaultI18n))
   700  	}
   701  }
   702  
   703  func (s *sitesBuilder) Fatalf(format string, args ...any) {
   704  	s.T.Helper()
   705  	s.T.Fatalf(format, args...)
   706  }
   707  
   708  func (s *sitesBuilder) AssertFileContentFn(filename string, f func(s string) bool) {
   709  	s.T.Helper()
   710  	content := s.FileContent(filename)
   711  	if !f(content) {
   712  		s.Fatalf("Assert failed for %q in content\n%s", filename, content)
   713  	}
   714  }
   715  
   716  // Helper to migrate tests to new format.
   717  func (s *sitesBuilder) DumpTxtar() string {
   718  	var sb strings.Builder
   719  
   720  	skipRe := regexp.MustCompile(`^(public|resources|package-lock.json|go.sum)`)
   721  
   722  	// nolint
   723  	afero.Walk(s.Fs.Source, s.workingDir, func(path string, info fs.FileInfo, err error) error {
   724  		if err != nil {
   725  			return err
   726  		}
   727  		rel := strings.TrimPrefix(path, s.workingDir+"/")
   728  		if skipRe.MatchString(rel) {
   729  			if info.IsDir() {
   730  				return filepath.SkipDir
   731  			}
   732  			return nil
   733  		}
   734  		if info == nil || info.IsDir() {
   735  			return nil
   736  		}
   737  		// nolint
   738  		sb.WriteString(fmt.Sprintf("-- %s --\n", rel))
   739  		b, err := afero.ReadFile(s.Fs.Source, path)
   740  		s.Assert(err, qt.IsNil)
   741  		sb.WriteString(strings.TrimSpace(string(b)))
   742  		sb.WriteString("\n")
   743  		return nil
   744  	})
   745  
   746  	return sb.String()
   747  }
   748  
   749  func (s *sitesBuilder) AssertHome(matches ...string) {
   750  	s.AssertFileContent("public/index.html", matches...)
   751  }
   752  
   753  func (s *sitesBuilder) AssertFileContent(filename string, matches ...string) {
   754  	s.T.Helper()
   755  	content := s.FileContent(filename)
   756  	for _, m := range matches {
   757  		lines := strings.Split(m, "\n")
   758  		for _, match := range lines {
   759  			match = strings.TrimSpace(match)
   760  			if match == "" {
   761  				continue
   762  			}
   763  			if !strings.Contains(content, match) {
   764  				s.Assert(content, qt.Contains, match, qt.Commentf(match+" not in: \n"+content))
   765  			}
   766  		}
   767  	}
   768  }
   769  
   770  func (s *sitesBuilder) AssertFileDoesNotExist(filename string) {
   771  	if s.CheckExists(filename) {
   772  		s.Fatalf("File %q exists but must not exist.", filename)
   773  	}
   774  }
   775  
   776  func (s *sitesBuilder) AssertImage(width, height int, filename string) {
   777  	f, err := s.Fs.WorkingDirReadOnly.Open(filename)
   778  	s.Assert(err, qt.IsNil)
   779  	defer f.Close()
   780  	cfg, err := jpeg.DecodeConfig(f)
   781  	s.Assert(err, qt.IsNil)
   782  	s.Assert(cfg.Width, qt.Equals, width)
   783  	s.Assert(cfg.Height, qt.Equals, height)
   784  }
   785  
   786  func (s *sitesBuilder) AssertNoDuplicateWrites() {
   787  	s.Helper()
   788  	hugofs.WalkFilesystems(s.Fs.PublishDir, func(fs afero.Fs) bool {
   789  		if dfs, ok := fs.(hugofs.DuplicatesReporter); ok {
   790  			s.Assert(dfs.ReportDuplicates(), qt.Equals, "")
   791  		}
   792  		return false
   793  	})
   794  }
   795  
   796  func (s *sitesBuilder) FileContent(filename string) string {
   797  	s.Helper()
   798  	filename = filepath.FromSlash(filename)
   799  	return readWorkingDir(s.T, s.Fs, filename)
   800  }
   801  
   802  func (s *sitesBuilder) AssertObject(expected string, object any) {
   803  	s.T.Helper()
   804  	got := s.dumper.Sdump(object)
   805  	expected = strings.TrimSpace(expected)
   806  
   807  	if expected != got {
   808  		fmt.Println(got)
   809  		diff := htesting.DiffStrings(expected, got)
   810  		s.Fatalf("diff:\n%s\nexpected\n%s\ngot\n%s", diff, expected, got)
   811  	}
   812  }
   813  
   814  func (s *sitesBuilder) AssertFileContentRe(filename string, matches ...string) {
   815  	content := readWorkingDir(s.T, s.Fs, filename)
   816  	for _, match := range matches {
   817  		r := regexp.MustCompile("(?s)" + match)
   818  		if !r.MatchString(content) {
   819  			s.Fatalf("No match for %q in content for %s\n%q", match, filename, content)
   820  		}
   821  	}
   822  }
   823  
   824  func (s *sitesBuilder) CheckExists(filename string) bool {
   825  	return workingDirExists(s.Fs, filepath.Clean(filename))
   826  }
   827  
   828  func (s *sitesBuilder) GetPage(ref string) page.Page {
   829  	p, err := s.H.Sites[0].getPage(nil, ref)
   830  	s.Assert(err, qt.IsNil)
   831  	return p
   832  }
   833  
   834  func (s *sitesBuilder) GetPageRel(p page.Page, ref string) page.Page {
   835  	p, err := s.H.Sites[0].getPage(p, ref)
   836  	s.Assert(err, qt.IsNil)
   837  	return p
   838  }
   839  
   840  func (s *sitesBuilder) NpmInstall() hexec.Runner {
   841  	sc := security.DefaultConfig
   842  	var err error
   843  	sc.Exec.Allow, err = security.NewWhitelist("npm")
   844  	s.Assert(err, qt.IsNil)
   845  	ex := hexec.New(sc)
   846  	command, err := ex.New("npm", "install")
   847  	s.Assert(err, qt.IsNil)
   848  	return command
   849  }
   850  
   851  func newTestHelperFromProvider(cfg config.Provider, fs *hugofs.Fs, t testing.TB) (testHelper, *allconfig.Configs) {
   852  	res, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{
   853  		Flags: cfg,
   854  		Fs:    fs.Source,
   855  	})
   856  	if err != nil {
   857  		t.Fatal(err)
   858  	}
   859  	return newTestHelper(res.Base, fs, t), res
   860  }
   861  
   862  func newTestHelper(cfg *allconfig.Config, fs *hugofs.Fs, t testing.TB) testHelper {
   863  	return testHelper{
   864  		Cfg: cfg,
   865  		Fs:  fs,
   866  		C:   qt.New(t),
   867  	}
   868  }
   869  
   870  type testHelper struct {
   871  	Cfg *allconfig.Config
   872  	Fs  *hugofs.Fs
   873  	*qt.C
   874  }
   875  
   876  func (th testHelper) assertFileContent(filename string, matches ...string) {
   877  	th.Helper()
   878  	filename = th.replaceDefaultContentLanguageValue(filename)
   879  	content := readWorkingDir(th, th.Fs, filename)
   880  	for _, match := range matches {
   881  		match = th.replaceDefaultContentLanguageValue(match)
   882  		th.Assert(strings.Contains(content, match), qt.Equals, true, qt.Commentf(match+" not in: \n"+content))
   883  	}
   884  }
   885  
   886  func (th testHelper) assertFileNotExist(filename string) {
   887  	exists, err := helpers.Exists(filename, th.Fs.PublishDir)
   888  	th.Assert(err, qt.IsNil)
   889  	th.Assert(exists, qt.Equals, false)
   890  }
   891  
   892  func (th testHelper) replaceDefaultContentLanguageValue(value string) string {
   893  	defaultInSubDir := th.Cfg.DefaultContentLanguageInSubdir
   894  	replace := th.Cfg.DefaultContentLanguage + "/"
   895  
   896  	if !defaultInSubDir {
   897  		value = strings.Replace(value, replace, "", 1)
   898  	}
   899  	return value
   900  }
   901  
   902  func loadTestConfigFromProvider(cfg config.Provider) (*allconfig.Configs, error) {
   903  	workingDir := cfg.GetString("workingDir")
   904  	fs := afero.NewMemMapFs()
   905  	if workingDir != "" {
   906  		fs.MkdirAll(workingDir, 0o755) // nolint
   907  	}
   908  	res, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{Flags: cfg, Fs: fs})
   909  	return res, err
   910  }
   911  
   912  func newTestCfg(withConfig ...func(cfg config.Provider) error) (config.Provider, *hugofs.Fs) {
   913  	mm := afero.NewMemMapFs()
   914  	cfg := config.New()
   915  	cfg.Set("defaultContentLanguageInSubdir", false)
   916  	cfg.Set("publishDir", "public")
   917  
   918  	fs := hugofs.NewFromOld(hugofs.NewBaseFileDecorator(mm), cfg)
   919  
   920  	return cfg, fs
   921  }
   922  
   923  func newTestSitesFromConfig(t testing.TB, afs afero.Fs, tomlConfig string, layoutPathContentPairs ...string) (testHelper, *HugoSites) {
   924  	if len(layoutPathContentPairs)%2 != 0 {
   925  		t.Fatalf("Layouts must be provided in pairs")
   926  	}
   927  
   928  	c := qt.New(t)
   929  
   930  	writeToFs(t, afs, filepath.Join("content", ".gitkeep"), "")
   931  	writeToFs(t, afs, "config.toml", tomlConfig)
   932  
   933  	cfg, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{Fs: afs})
   934  	c.Assert(err, qt.IsNil)
   935  
   936  	fs := hugofs.NewFrom(afs, cfg.LoadingInfo.BaseConfig)
   937  	th := newTestHelper(cfg.Base, fs, t)
   938  
   939  	for i := 0; i < len(layoutPathContentPairs); i += 2 {
   940  		writeSource(t, fs, layoutPathContentPairs[i], layoutPathContentPairs[i+1])
   941  	}
   942  
   943  	h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Configs: cfg})
   944  
   945  	c.Assert(err, qt.IsNil)
   946  
   947  	return th, h
   948  }
   949  
   950  // TODO(bep) replace these with the builder
   951  func buildSingleSite(t testing.TB, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site {
   952  	t.Helper()
   953  	return buildSingleSiteExpected(t, false, false, depsCfg, buildCfg)
   954  }
   955  
   956  func buildSingleSiteExpected(t testing.TB, expectSiteInitError, expectBuildError bool, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site {
   957  	t.Helper()
   958  	b := newTestSitesBuilderFromDepsCfg(t, depsCfg).WithNothingAdded()
   959  
   960  	err := b.CreateSitesE()
   961  
   962  	if expectSiteInitError {
   963  		b.Assert(err, qt.Not(qt.IsNil))
   964  		return nil
   965  	} else {
   966  		b.Assert(err, qt.IsNil)
   967  	}
   968  
   969  	h := b.H
   970  
   971  	b.Assert(len(h.Sites), qt.Equals, 1)
   972  
   973  	if expectBuildError {
   974  		b.Assert(h.Build(buildCfg), qt.Not(qt.IsNil))
   975  		return nil
   976  
   977  	}
   978  
   979  	b.Assert(h.Build(buildCfg), qt.IsNil)
   980  
   981  	return h.Sites[0]
   982  }
   983  
   984  func writeSourcesToSource(t *testing.T, base string, fs *hugofs.Fs, sources ...[2]string) {
   985  	for _, src := range sources {
   986  		writeSource(t, fs, filepath.Join(base, src[0]), src[1])
   987  	}
   988  }
   989  
   990  func getPage(in page.Page, ref string) page.Page {
   991  	p, err := in.GetPage(ref)
   992  	if err != nil {
   993  		panic(err)
   994  	}
   995  	return p
   996  }
   997  
   998  func content(c resource.ContentProvider) string {
   999  	cc, err := c.Content(context.Background())
  1000  	if err != nil {
  1001  		panic(err)
  1002  	}
  1003  
  1004  	ccs, err := cast.ToStringE(cc)
  1005  	if err != nil {
  1006  		panic(err)
  1007  	}
  1008  	return ccs
  1009  }