hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

testhelpers_test.go (27976B)

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