github.com/fighterlyt/hugo@v0.47.1/hugolib/testhelpers_test.go (about)

     1  package hugolib
     2  
     3  import (
     4  	"io/ioutil"
     5  	"path/filepath"
     6  	"runtime"
     7  	"testing"
     8  
     9  	"bytes"
    10  	"fmt"
    11  	"regexp"
    12  	"strings"
    13  	"text/template"
    14  
    15  	"github.com/gohugoio/hugo/langs"
    16  	"github.com/sanity-io/litter"
    17  	jww "github.com/spf13/jwalterweatherman"
    18  
    19  	"github.com/gohugoio/hugo/config"
    20  	"github.com/gohugoio/hugo/deps"
    21  	"github.com/spf13/afero"
    22  
    23  	"github.com/gohugoio/hugo/helpers"
    24  	"github.com/gohugoio/hugo/tpl"
    25  	"github.com/spf13/viper"
    26  
    27  	"os"
    28  
    29  	"github.com/gohugoio/hugo/hugofs"
    30  	"github.com/stretchr/testify/assert"
    31  	"github.com/stretchr/testify/require"
    32  )
    33  
    34  const ()
    35  
    36  type sitesBuilder struct {
    37  	Cfg config.Provider
    38  	Fs  *hugofs.Fs
    39  	T   testing.TB
    40  
    41  	logger *jww.Notepad
    42  
    43  	dumper litter.Options
    44  
    45  	// Aka the Hugo server mode.
    46  	running bool
    47  
    48  	H *HugoSites
    49  
    50  	theme string
    51  
    52  	// Default toml
    53  	configFormat string
    54  
    55  	// Default is empty.
    56  	// TODO(bep) revisit this and consider always setting it to something.
    57  	// Consider this in relation to using the BaseFs.PublishFs to all publishing.
    58  	workingDir string
    59  
    60  	// Base data/content
    61  	contentFilePairs  []string
    62  	templateFilePairs []string
    63  	i18nFilePairs     []string
    64  	dataFilePairs     []string
    65  
    66  	// Additional data/content.
    67  	// As in "use the base, but add these on top".
    68  	contentFilePairsAdded  []string
    69  	templateFilePairsAdded []string
    70  	i18nFilePairsAdded     []string
    71  	dataFilePairsAdded     []string
    72  }
    73  
    74  func newTestSitesBuilder(t testing.TB) *sitesBuilder {
    75  	v := viper.New()
    76  	fs := hugofs.NewMem(v)
    77  
    78  	litterOptions := litter.Options{
    79  		HidePrivateFields: true,
    80  		StripPackageNames: true,
    81  		Separator:         " ",
    82  	}
    83  
    84  	return &sitesBuilder{T: t, Fs: fs, configFormat: "toml", dumper: litterOptions}
    85  }
    86  
    87  func createTempDir(prefix string) (string, func(), error) {
    88  	workDir, err := ioutil.TempDir("", prefix)
    89  	if err != nil {
    90  		return "", nil, err
    91  	}
    92  
    93  	if runtime.GOOS == "darwin" && !strings.HasPrefix(workDir, "/private") {
    94  		// To get the entry folder in line with the rest. This its a little bit
    95  		// mysterious, but so be it.
    96  		workDir = "/private" + workDir
    97  	}
    98  	return workDir, func() { os.RemoveAll(workDir) }, nil
    99  }
   100  
   101  func (s *sitesBuilder) Running() *sitesBuilder {
   102  	s.running = true
   103  	return s
   104  }
   105  
   106  func (s *sitesBuilder) WithLogger(logger *jww.Notepad) *sitesBuilder {
   107  	s.logger = logger
   108  	return s
   109  }
   110  
   111  func (s *sitesBuilder) WithWorkingDir(dir string) *sitesBuilder {
   112  	s.workingDir = dir
   113  	return s
   114  }
   115  
   116  func (s *sitesBuilder) WithConfigTemplate(data interface{}, format, configTemplate string) *sitesBuilder {
   117  	if format == "" {
   118  		format = "toml"
   119  	}
   120  
   121  	templ, err := template.New("test").Parse(configTemplate)
   122  	if err != nil {
   123  		s.Fatalf("Template parse failed: %s", err)
   124  	}
   125  	var b bytes.Buffer
   126  	templ.Execute(&b, data)
   127  	return s.WithConfigFile(format, b.String())
   128  }
   129  
   130  func (s *sitesBuilder) WithViper(v *viper.Viper) *sitesBuilder {
   131  	loadDefaultSettingsFor(v)
   132  	s.Cfg = v
   133  
   134  	return s
   135  }
   136  
   137  func (s *sitesBuilder) WithConfigFile(format, conf string) *sitesBuilder {
   138  	writeSource(s.T, s.Fs, "config."+format, conf)
   139  	s.configFormat = format
   140  	return s
   141  }
   142  
   143  func (s *sitesBuilder) WithThemeConfigFile(format, conf string) *sitesBuilder {
   144  	if s.theme == "" {
   145  		s.theme = "test-theme"
   146  	}
   147  	filename := filepath.Join("themes", s.theme, "config."+format)
   148  	writeSource(s.T, s.Fs, filename, conf)
   149  	return s
   150  }
   151  
   152  func (s *sitesBuilder) WithSourceFile(filename, content string) *sitesBuilder {
   153  	writeSource(s.T, s.Fs, filepath.FromSlash(filename), content)
   154  	return s
   155  }
   156  
   157  const commonConfigSections = `
   158  
   159  [services]
   160  [services.disqus]
   161  shortname = "disqus_shortname"
   162  [services.googleAnalytics]
   163  id = "ga_id"
   164  
   165  [privacy]
   166  [privacy.disqus]
   167  disable = false
   168  [privacy.googleAnalytics]
   169  respectDoNotTrack = true
   170  anonymizeIP = true
   171  [privacy.instagram]
   172  simple = true
   173  [privacy.twitter]
   174  enableDNT = true
   175  [privacy.vimeo]
   176  disable = false
   177  [privacy.youtube]
   178  disable = false
   179  privacyEnhanced = true
   180  
   181  `
   182  
   183  func (s *sitesBuilder) WithSimpleConfigFile() *sitesBuilder {
   184  	var config = `
   185  baseURL = "http://example.com/"
   186  
   187  ` + commonConfigSections
   188  	return s.WithConfigFile("toml", config)
   189  }
   190  
   191  func (s *sitesBuilder) WithDefaultMultiSiteConfig() *sitesBuilder {
   192  	var defaultMultiSiteConfig = `
   193  baseURL = "http://example.com/blog"
   194  
   195  paginate = 1
   196  disablePathToLower = true
   197  defaultContentLanguage = "en"
   198  defaultContentLanguageInSubdir = true
   199  
   200  [permalinks]
   201  other = "/somewhere/else/:filename"
   202  
   203  [blackfriday]
   204  angledQuotes = true
   205  
   206  [Taxonomies]
   207  tag = "tags"
   208  
   209  [Languages]
   210  [Languages.en]
   211  weight = 10
   212  title = "In English"
   213  languageName = "English"
   214  [Languages.en.blackfriday]
   215  angledQuotes = false
   216  [[Languages.en.menu.main]]
   217  url    = "/"
   218  name   = "Home"
   219  weight = 0
   220  
   221  [Languages.fr]
   222  weight = 20
   223  title = "Le Français"
   224  languageName = "Français"
   225  [Languages.fr.Taxonomies]
   226  plaque = "plaques"
   227  
   228  [Languages.nn]
   229  weight = 30
   230  title = "På nynorsk"
   231  languageName = "Nynorsk"
   232  paginatePath = "side"
   233  [Languages.nn.Taxonomies]
   234  lag = "lag"
   235  [[Languages.nn.menu.main]]
   236  url    = "/"
   237  name   = "Heim"
   238  weight = 1
   239  
   240  [Languages.nb]
   241  weight = 40
   242  title = "På bokmål"
   243  languageName = "Bokmål"
   244  paginatePath = "side"
   245  [Languages.nb.Taxonomies]
   246  lag = "lag"
   247  ` + commonConfigSections
   248  
   249  	return s.WithConfigFile("toml", defaultMultiSiteConfig)
   250  
   251  }
   252  
   253  func (s *sitesBuilder) WithContent(filenameContent ...string) *sitesBuilder {
   254  	s.contentFilePairs = append(s.contentFilePairs, filenameContent...)
   255  	return s
   256  }
   257  
   258  func (s *sitesBuilder) WithContentAdded(filenameContent ...string) *sitesBuilder {
   259  	s.contentFilePairsAdded = append(s.contentFilePairsAdded, filenameContent...)
   260  	return s
   261  }
   262  
   263  func (s *sitesBuilder) WithTemplates(filenameContent ...string) *sitesBuilder {
   264  	s.templateFilePairs = append(s.templateFilePairs, filenameContent...)
   265  	return s
   266  }
   267  
   268  func (s *sitesBuilder) WithTemplatesAdded(filenameContent ...string) *sitesBuilder {
   269  	s.templateFilePairsAdded = append(s.templateFilePairsAdded, filenameContent...)
   270  	return s
   271  }
   272  
   273  func (s *sitesBuilder) WithData(filenameContent ...string) *sitesBuilder {
   274  	s.dataFilePairs = append(s.dataFilePairs, filenameContent...)
   275  	return s
   276  }
   277  
   278  func (s *sitesBuilder) WithDataAdded(filenameContent ...string) *sitesBuilder {
   279  	s.dataFilePairsAdded = append(s.dataFilePairsAdded, filenameContent...)
   280  	return s
   281  }
   282  
   283  func (s *sitesBuilder) WithI18n(filenameContent ...string) *sitesBuilder {
   284  	s.i18nFilePairs = append(s.i18nFilePairs, filenameContent...)
   285  	return s
   286  }
   287  
   288  func (s *sitesBuilder) WithI18nAdded(filenameContent ...string) *sitesBuilder {
   289  	s.i18nFilePairsAdded = append(s.i18nFilePairsAdded, filenameContent...)
   290  	return s
   291  }
   292  
   293  func (s *sitesBuilder) writeFilePairs(folder string, filenameContent []string) *sitesBuilder {
   294  	if len(filenameContent)%2 != 0 {
   295  		s.Fatalf("expect filenameContent for %q in pairs (%d)", folder, len(filenameContent))
   296  	}
   297  	for i := 0; i < len(filenameContent); i += 2 {
   298  		filename, content := filenameContent[i], filenameContent[i+1]
   299  		target := folder
   300  		// TODO(bep) clean  up this magic.
   301  		if strings.HasPrefix(filename, folder) {
   302  			target = ""
   303  		}
   304  
   305  		if s.workingDir != "" {
   306  			target = filepath.Join(s.workingDir, target)
   307  		}
   308  
   309  		writeSource(s.T, s.Fs, filepath.Join(target, filename), content)
   310  	}
   311  	return s
   312  }
   313  
   314  func (s *sitesBuilder) CreateSites() *sitesBuilder {
   315  	s.addDefaults()
   316  	s.writeFilePairs("content", s.contentFilePairs)
   317  	s.writeFilePairs("content", s.contentFilePairsAdded)
   318  	s.writeFilePairs("layouts", s.templateFilePairs)
   319  	s.writeFilePairs("layouts", s.templateFilePairsAdded)
   320  	s.writeFilePairs("data", s.dataFilePairs)
   321  	s.writeFilePairs("data", s.dataFilePairsAdded)
   322  	s.writeFilePairs("i18n", s.i18nFilePairs)
   323  	s.writeFilePairs("i18n", s.i18nFilePairsAdded)
   324  
   325  	if s.Cfg == nil {
   326  		cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: s.Fs.Source, Filename: "config." + s.configFormat})
   327  		if err != nil {
   328  			s.Fatalf("Failed to load config: %s", err)
   329  		}
   330  		// TODO(bep)
   331  		/*		expectedConfigs := 1
   332  				if s.theme != "" {
   333  					expectedConfigs = 2
   334  				}
   335  				require.Equal(s.T, expectedConfigs, len(configFiles), fmt.Sprintf("Configs: %v", configFiles))
   336  		*/
   337  		s.Cfg = cfg
   338  	}
   339  
   340  	sites, err := NewHugoSites(deps.DepsCfg{Fs: s.Fs, Cfg: s.Cfg, Logger: s.logger, Running: s.running})
   341  	if err != nil {
   342  		s.Fatalf("Failed to create sites: %s", err)
   343  	}
   344  	s.H = sites
   345  
   346  	return s
   347  }
   348  
   349  func (s *sitesBuilder) Build(cfg BuildCfg) *sitesBuilder {
   350  	return s.build(cfg, false)
   351  }
   352  
   353  func (s *sitesBuilder) BuildFail(cfg BuildCfg) *sitesBuilder {
   354  	return s.build(cfg, true)
   355  }
   356  
   357  func (s *sitesBuilder) build(cfg BuildCfg, shouldFail bool) *sitesBuilder {
   358  	if s.H == nil {
   359  		s.CreateSites()
   360  	}
   361  
   362  	err := s.H.Build(cfg)
   363  	if err == nil {
   364  		logErrorCount := s.H.NumLogErrors()
   365  		if logErrorCount > 0 {
   366  			err = fmt.Errorf("logged %d errors", logErrorCount)
   367  		}
   368  	}
   369  	if err != nil && !shouldFail {
   370  		s.Fatalf("Build failed: %s", err)
   371  	} else if err == nil && shouldFail {
   372  		s.Fatalf("Expected error")
   373  	}
   374  
   375  	return s
   376  }
   377  
   378  func (s *sitesBuilder) addDefaults() {
   379  
   380  	var (
   381  		contentTemplate = `---
   382  title: doc1
   383  weight: 1
   384  tags:
   385   - tag1
   386  date: "2018-02-28"
   387  ---
   388  # doc1
   389  *some "content"*
   390  
   391  {{< shortcode >}}
   392  
   393  {{< lingo >}}
   394  `
   395  
   396  		defaultContent = []string{
   397  			"content/sect/doc1.en.md", contentTemplate,
   398  			"content/sect/doc1.fr.md", contentTemplate,
   399  			"content/sect/doc1.nb.md", contentTemplate,
   400  			"content/sect/doc1.nn.md", contentTemplate,
   401  		}
   402  
   403  		defaultTemplates = []string{
   404  			"_default/single.html", "Single: {{ .Title }}|{{ i18n \"hello\" }}|{{.Lang}}|{{ .Content }}",
   405  			"_default/list.html", "{{ $p := .Paginator }}List Page {{ $p.PageNumber }}: {{ .Title }}|{{ i18n \"hello\" }}|{{ .Permalink }}|Pager: {{ template \"_internal/pagination.html\" . }}",
   406  			"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  }}",
   407  			"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  }}",
   408  
   409  			// Shortcodes
   410  			"shortcodes/shortcode.html", "Shortcode: {{ i18n \"hello\" }}",
   411  			// A shortcode in multiple languages
   412  			"shortcodes/lingo.html", "LingoDefault",
   413  			"shortcodes/lingo.fr.html", "LingoFrench",
   414  		}
   415  
   416  		defaultI18n = []string{
   417  			"en.yaml", `
   418  hello:
   419    other: "Hello"
   420  `,
   421  			"fr.yaml", `
   422  hello:
   423    other: "Bonjour"
   424  `,
   425  		}
   426  
   427  		defaultData = []string{
   428  			"hugo.toml", "slogan = \"Hugo Rocks!\"",
   429  		}
   430  	)
   431  
   432  	if len(s.contentFilePairs) == 0 {
   433  		s.writeFilePairs("content", defaultContent)
   434  	}
   435  	if len(s.templateFilePairs) == 0 {
   436  		s.writeFilePairs("layouts", defaultTemplates)
   437  	}
   438  	if len(s.dataFilePairs) == 0 {
   439  		s.writeFilePairs("data", defaultData)
   440  	}
   441  	if len(s.i18nFilePairs) == 0 {
   442  		s.writeFilePairs("i18n", defaultI18n)
   443  	}
   444  }
   445  
   446  func (s *sitesBuilder) Fatalf(format string, args ...interface{}) {
   447  	Fatalf(s.T, format, args...)
   448  }
   449  
   450  func Fatalf(t testing.TB, format string, args ...interface{}) {
   451  	trace := strings.Join(assert.CallerInfo(), "\n\r\t\t\t")
   452  	format = format + "\n%s"
   453  	args = append(args, trace)
   454  	t.Fatalf(format, args...)
   455  }
   456  
   457  func (s *sitesBuilder) AssertFileContent(filename string, matches ...string) {
   458  	content := readDestination(s.T, s.Fs, filename)
   459  	for _, match := range matches {
   460  		if !strings.Contains(content, match) {
   461  			s.Fatalf("No match for %q in content for %s\n%s\n%q", match, filename, content, content)
   462  		}
   463  	}
   464  }
   465  
   466  func (s *sitesBuilder) AssertObject(expected string, object interface{}) {
   467  	got := s.dumper.Sdump(object)
   468  	expected = strings.TrimSpace(expected)
   469  
   470  	if expected != got {
   471  		fmt.Println(got)
   472  		diff := helpers.DiffStrings(expected, got)
   473  		s.Fatalf("diff:\n%s\nexpected\n%s\ngot\n%s", diff, expected, got)
   474  	}
   475  }
   476  
   477  func (s *sitesBuilder) AssertFileContentRe(filename string, matches ...string) {
   478  	content := readDestination(s.T, s.Fs, filename)
   479  	for _, match := range matches {
   480  		r := regexp.MustCompile(match)
   481  		if !r.MatchString(content) {
   482  			s.Fatalf("No match for %q in content for %s\n%q", match, filename, content)
   483  		}
   484  	}
   485  }
   486  
   487  func (s *sitesBuilder) CheckExists(filename string) bool {
   488  	return destinationExists(s.Fs, filepath.Clean(filename))
   489  }
   490  
   491  type testHelper struct {
   492  	Cfg config.Provider
   493  	Fs  *hugofs.Fs
   494  	T   testing.TB
   495  }
   496  
   497  func (th testHelper) assertFileContent(filename string, matches ...string) {
   498  	filename = th.replaceDefaultContentLanguageValue(filename)
   499  	content := readDestination(th.T, th.Fs, filename)
   500  	for _, match := range matches {
   501  		match = th.replaceDefaultContentLanguageValue(match)
   502  		require.True(th.T, strings.Contains(content, match), fmt.Sprintf("File no match for\n%q in\n%q:\n%s", strings.Replace(match, "%", "%%", -1), filename, strings.Replace(content, "%", "%%", -1)))
   503  	}
   504  }
   505  
   506  func (th testHelper) assertFileContentRegexp(filename string, matches ...string) {
   507  	filename = th.replaceDefaultContentLanguageValue(filename)
   508  	content := readDestination(th.T, th.Fs, filename)
   509  	for _, match := range matches {
   510  		match = th.replaceDefaultContentLanguageValue(match)
   511  		r := regexp.MustCompile(match)
   512  		require.True(th.T, r.MatchString(content), fmt.Sprintf("File no match for\n%q in\n%q:\n%s", strings.Replace(match, "%", "%%", -1), filename, strings.Replace(content, "%", "%%", -1)))
   513  	}
   514  }
   515  
   516  func (th testHelper) assertFileNotExist(filename string) {
   517  	exists, err := helpers.Exists(filename, th.Fs.Destination)
   518  	require.NoError(th.T, err)
   519  	require.False(th.T, exists)
   520  }
   521  
   522  func (th testHelper) replaceDefaultContentLanguageValue(value string) string {
   523  	defaultInSubDir := th.Cfg.GetBool("defaultContentLanguageInSubDir")
   524  	replace := th.Cfg.GetString("defaultContentLanguage") + "/"
   525  
   526  	if !defaultInSubDir {
   527  		value = strings.Replace(value, replace, "", 1)
   528  
   529  	}
   530  	return value
   531  }
   532  
   533  func newTestPathSpec(fs *hugofs.Fs, v *viper.Viper) *helpers.PathSpec {
   534  	l := langs.NewDefaultLanguage(v)
   535  	ps, _ := helpers.NewPathSpec(fs, l)
   536  	return ps
   537  }
   538  
   539  func newTestDefaultPathSpec(t *testing.T) *helpers.PathSpec {
   540  	v := viper.New()
   541  	// Easier to reason about in tests.
   542  	v.Set("disablePathToLower", true)
   543  	v.Set("contentDir", "content")
   544  	v.Set("dataDir", "data")
   545  	v.Set("i18nDir", "i18n")
   546  	v.Set("layoutDir", "layouts")
   547  	v.Set("archetypeDir", "archetypes")
   548  	v.Set("assetDir", "assets")
   549  	v.Set("resourceDir", "resources")
   550  	v.Set("publishDir", "public")
   551  	fs := hugofs.NewDefault(v)
   552  	ps, err := helpers.NewPathSpec(fs, v)
   553  	if err != nil {
   554  		t.Fatal(err)
   555  	}
   556  	return ps
   557  }
   558  
   559  func newTestCfg() (*viper.Viper, *hugofs.Fs) {
   560  
   561  	v := viper.New()
   562  	fs := hugofs.NewMem(v)
   563  
   564  	v.SetFs(fs.Source)
   565  
   566  	loadDefaultSettingsFor(v)
   567  
   568  	// Default is false, but true is easier to use as default in tests
   569  	v.Set("defaultContentLanguageInSubdir", true)
   570  
   571  	return v, fs
   572  
   573  }
   574  
   575  // newTestSite creates a new site in the  English language with in-memory Fs.
   576  // The site will have a template system loaded and ready to use.
   577  // Note: This is only used in single site tests.
   578  func newTestSite(t testing.TB, configKeyValues ...interface{}) *Site {
   579  
   580  	cfg, fs := newTestCfg()
   581  
   582  	for i := 0; i < len(configKeyValues); i += 2 {
   583  		cfg.Set(configKeyValues[i].(string), configKeyValues[i+1])
   584  	}
   585  
   586  	d := deps.DepsCfg{Fs: fs, Cfg: cfg}
   587  
   588  	s, err := NewSiteForCfg(d)
   589  
   590  	if err != nil {
   591  		Fatalf(t, "Failed to create Site: %s", err)
   592  	}
   593  	return s
   594  }
   595  
   596  func newTestSitesFromConfig(t testing.TB, afs afero.Fs, tomlConfig string, layoutPathContentPairs ...string) (testHelper, *HugoSites) {
   597  	if len(layoutPathContentPairs)%2 != 0 {
   598  		Fatalf(t, "Layouts must be provided in pairs")
   599  	}
   600  
   601  	writeToFs(t, afs, "config.toml", tomlConfig)
   602  
   603  	cfg, err := LoadConfigDefault(afs)
   604  	require.NoError(t, err)
   605  
   606  	fs := hugofs.NewFrom(afs, cfg)
   607  	th := testHelper{cfg, fs, t}
   608  
   609  	for i := 0; i < len(layoutPathContentPairs); i += 2 {
   610  		writeSource(t, fs, layoutPathContentPairs[i], layoutPathContentPairs[i+1])
   611  	}
   612  
   613  	h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg})
   614  
   615  	require.NoError(t, err)
   616  
   617  	return th, h
   618  }
   619  
   620  func newTestSitesFromConfigWithDefaultTemplates(t testing.TB, tomlConfig string) (testHelper, *HugoSites) {
   621  	return newTestSitesFromConfig(t, afero.NewMemMapFs(), tomlConfig,
   622  		"layouts/_default/single.html", "Single|{{ .Title }}|{{ .Content }}",
   623  		"layouts/_default/list.html", "List|{{ .Title }}|{{ .Content }}",
   624  		"layouts/_default/terms.html", "Terms List|{{ .Title }}|{{ .Content }}",
   625  	)
   626  }
   627  
   628  func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tpl.TemplateHandler) error {
   629  
   630  	return func(templ tpl.TemplateHandler) error {
   631  		for i := 0; i < len(additionalTemplates); i += 2 {
   632  			err := templ.AddTemplate(additionalTemplates[i], additionalTemplates[i+1])
   633  			if err != nil {
   634  				return err
   635  			}
   636  		}
   637  		return nil
   638  	}
   639  }
   640  
   641  func buildSingleSite(t testing.TB, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site {
   642  	return buildSingleSiteExpected(t, false, depsCfg, buildCfg)
   643  }
   644  
   645  func buildSingleSiteExpected(t testing.TB, expectBuildError bool, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site {
   646  	h, err := NewHugoSites(depsCfg)
   647  
   648  	require.NoError(t, err)
   649  	require.Len(t, h.Sites, 1)
   650  
   651  	if expectBuildError {
   652  		require.Error(t, h.Build(buildCfg))
   653  		return nil
   654  
   655  	}
   656  
   657  	require.NoError(t, h.Build(buildCfg))
   658  
   659  	return h.Sites[0]
   660  }
   661  
   662  func writeSourcesToSource(t *testing.T, base string, fs *hugofs.Fs, sources ...[2]string) {
   663  	for _, src := range sources {
   664  		writeSource(t, fs, filepath.Join(base, src[0]), src[1])
   665  	}
   666  }
   667  
   668  func dumpPages(pages ...*Page) {
   669  	for i, p := range pages {
   670  		fmt.Printf("%d: Kind: %s Title: %-10s RelPermalink: %-10s Path: %-10s sections: %s Len Sections(): %d\n",
   671  			i+1,
   672  			p.Kind, p.title, p.RelPermalink(), p.Path(), p.sections, len(p.Sections()))
   673  	}
   674  }
   675  
   676  func isCI() bool {
   677  	return os.Getenv("CI") != ""
   678  }