github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/hugolib/testhelpers_test.go (about)

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