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

     1  package hugolib
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"math/rand"
    10  	"os"
    11  	"path/filepath"
    12  	"regexp"
    13  	"sort"
    14  	"strings"
    15  	"sync"
    16  	"testing"
    17  
    18  	"github.com/bep/logg"
    19  
    20  	qt "github.com/frankban/quicktest"
    21  	"github.com/fsnotify/fsnotify"
    22  	"github.com/neohugo/neohugo/common/herrors"
    23  	"github.com/neohugo/neohugo/common/hexec"
    24  	"github.com/neohugo/neohugo/common/loggers"
    25  	"github.com/neohugo/neohugo/common/maps"
    26  	"github.com/neohugo/neohugo/config"
    27  	"github.com/neohugo/neohugo/config/allconfig"
    28  	"github.com/neohugo/neohugo/config/security"
    29  	"github.com/neohugo/neohugo/deps"
    30  	"github.com/neohugo/neohugo/helpers"
    31  	"github.com/neohugo/neohugo/htesting"
    32  	"github.com/neohugo/neohugo/hugofs"
    33  	"github.com/spf13/afero"
    34  	"golang.org/x/text/unicode/norm"
    35  	"golang.org/x/tools/txtar"
    36  )
    37  
    38  type TestOpt func(*IntegrationTestConfig)
    39  
    40  func TestOptRunning() TestOpt {
    41  	return func(c *IntegrationTestConfig) {
    42  		c.Running = true
    43  	}
    44  }
    45  
    46  // Enable tracing in integration tests.
    47  // THis should only be used during development and not committed to the repo.
    48  func TestOptTrace() TestOpt {
    49  	return func(c *IntegrationTestConfig) {
    50  		c.LogLevel = logg.LevelTrace
    51  	}
    52  }
    53  
    54  // TestOptDebug will enable debug logging in integration tests.
    55  func TestOptDebug() TestOpt {
    56  	return func(c *IntegrationTestConfig) {
    57  		c.LogLevel = logg.LevelDebug
    58  	}
    59  }
    60  
    61  // TestOptWarn will enable warn logging in integration tests.
    62  func TestOptWarn() TestOpt {
    63  	return func(c *IntegrationTestConfig) {
    64  		c.LogLevel = logg.LevelWarn
    65  	}
    66  }
    67  
    68  // TestOptWithNFDOnDarwin will normalize the Unicode filenames to NFD on Darwin.
    69  func TestOptWithNFDOnDarwin() TestOpt {
    70  	return func(c *IntegrationTestConfig) {
    71  		c.NFDFormOnDarwin = true
    72  	}
    73  }
    74  
    75  // TestOptWithWorkingDir allows setting any config optiona as a function al option.
    76  func TestOptWithConfig(fn func(c *IntegrationTestConfig)) TestOpt {
    77  	return func(c *IntegrationTestConfig) {
    78  		fn(c)
    79  	}
    80  }
    81  
    82  // Test is a convenience method to create a new IntegrationTestBuilder from some files and run a build.
    83  func Test(t testing.TB, files string, opts ...TestOpt) *IntegrationTestBuilder {
    84  	cfg := IntegrationTestConfig{T: t, TxtarString: files}
    85  	for _, o := range opts {
    86  		o(&cfg)
    87  	}
    88  	return NewIntegrationTestBuilder(cfg).Build()
    89  }
    90  
    91  // TestE is the same as Test, but returns an error instead of failing the test.
    92  func TestE(t testing.TB, files string, opts ...TestOpt) (*IntegrationTestBuilder, error) {
    93  	cfg := IntegrationTestConfig{T: t, TxtarString: files}
    94  	for _, o := range opts {
    95  		o(&cfg)
    96  	}
    97  	return NewIntegrationTestBuilder(cfg).BuildE()
    98  }
    99  
   100  // TestRunning is a convenience method to create a new IntegrationTestBuilder from some files with Running set to true and run a build.
   101  // Deprecated: Use Test with TestOptRunning instead.
   102  func TestRunning(t testing.TB, files string, opts ...TestOpt) *IntegrationTestBuilder {
   103  	cfg := IntegrationTestConfig{T: t, TxtarString: files, Running: true}
   104  	for _, o := range opts {
   105  		o(&cfg)
   106  	}
   107  	return NewIntegrationTestBuilder(cfg).Build()
   108  }
   109  
   110  // In most cases you should not use this function directly, but the Test or TestRunning function.
   111  func NewIntegrationTestBuilder(conf IntegrationTestConfig) *IntegrationTestBuilder {
   112  	// Code fences.
   113  	conf.TxtarString = strings.ReplaceAll(conf.TxtarString, "§§§", "```")
   114  	// Multiline strings.
   115  	conf.TxtarString = strings.ReplaceAll(conf.TxtarString, "§§", "`")
   116  
   117  	data := txtar.Parse([]byte(conf.TxtarString))
   118  
   119  	if conf.NFDFormOnDarwin {
   120  		for i, f := range data.Files {
   121  			data.Files[i].Name = norm.NFD.String(f.Name)
   122  		}
   123  	}
   124  
   125  	c, ok := conf.T.(*qt.C)
   126  	if !ok {
   127  		c = qt.New(conf.T)
   128  	}
   129  
   130  	if conf.NeedsOsFS {
   131  		if !filepath.IsAbs(conf.WorkingDir) {
   132  			tempDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-integration-test")
   133  			c.Assert(err, qt.IsNil)
   134  			conf.WorkingDir = filepath.Join(tempDir, conf.WorkingDir)
   135  			if !conf.PrintAndKeepTempDir {
   136  				c.Cleanup(clean)
   137  			} else {
   138  				fmt.Println("\nUsing WorkingDir dir:", conf.WorkingDir)
   139  			}
   140  		}
   141  	} else if conf.WorkingDir == "" {
   142  		conf.WorkingDir = helpers.FilePathSeparator
   143  	}
   144  
   145  	return &IntegrationTestBuilder{
   146  		Cfg:  conf,
   147  		C:    c,
   148  		data: data,
   149  	}
   150  }
   151  
   152  // IntegrationTestBuilder is a (partial) rewrite of sitesBuilder.
   153  // The main problem with the "old" one was that it was that the test data was often a little hidden,
   154  // so it became hard to look at a test and determine what it should do, especially coming back to the
   155  // test after a year or so.
   156  type IntegrationTestBuilder struct {
   157  	*qt.C
   158  
   159  	data *txtar.Archive
   160  
   161  	fs *hugofs.Fs
   162  	H  *HugoSites
   163  
   164  	Cfg IntegrationTestConfig
   165  
   166  	changedFiles []string
   167  	createdFiles []string
   168  	removedFiles []string
   169  	renamedFiles []string
   170  	renamedDirs  []string
   171  
   172  	buildCount   int
   173  	GCCount      int
   174  	counters     *buildCounters
   175  	logBuff      lockingBuffer
   176  	lastBuildLog string
   177  
   178  	builderInit sync.Once
   179  }
   180  
   181  type lockingBuffer struct {
   182  	sync.Mutex
   183  	bytes.Buffer
   184  }
   185  
   186  func (b *lockingBuffer) ReadFrom(r io.Reader) (n int64, err error) {
   187  	b.Lock()
   188  	n, err = b.Buffer.ReadFrom(r)
   189  	b.Unlock()
   190  	return
   191  }
   192  
   193  func (b *lockingBuffer) Write(p []byte) (n int, err error) {
   194  	b.Lock()
   195  	n, err = b.Buffer.Write(p)
   196  	b.Unlock()
   197  	return
   198  }
   199  
   200  func (s *IntegrationTestBuilder) AssertLogContains(els ...string) {
   201  	s.Helper()
   202  	for _, el := range els {
   203  		s.Assert(s.lastBuildLog, qt.Contains, el)
   204  	}
   205  }
   206  
   207  func (s *IntegrationTestBuilder) AssertLogNotContains(els ...string) {
   208  	s.Helper()
   209  	for _, el := range els {
   210  		s.Assert(s.lastBuildLog, qt.Not(qt.Contains), el)
   211  	}
   212  }
   213  
   214  func (s *IntegrationTestBuilder) AssertLogMatches(expression string) {
   215  	s.Helper()
   216  	re := regexp.MustCompile(expression)
   217  	s.Assert(re.MatchString(s.lastBuildLog), qt.IsTrue, qt.Commentf(s.lastBuildLog))
   218  }
   219  
   220  func (s *IntegrationTestBuilder) AssertBuildCountData(count int) {
   221  	s.Helper()
   222  	s.Assert(s.H.init.data.InitCount(), qt.Equals, count)
   223  }
   224  
   225  func (s *IntegrationTestBuilder) AssertBuildCountGitInfo(count int) {
   226  	s.Helper()
   227  	s.Assert(s.H.init.gitInfo.InitCount(), qt.Equals, count)
   228  }
   229  
   230  func (s *IntegrationTestBuilder) AssertBuildCountLayouts(count int) {
   231  	s.Helper()
   232  	s.Assert(s.H.init.layouts.InitCount(), qt.Equals, count)
   233  }
   234  
   235  func (s *IntegrationTestBuilder) AssertFileCount(dirname string, expected int) {
   236  	s.Helper()
   237  	fs := s.fs.WorkingDirReadOnly
   238  	count := 0
   239  	// nolint
   240  	afero.Walk(fs, dirname, func(path string, info os.FileInfo, err error) error {
   241  		if err != nil {
   242  			return err
   243  		}
   244  		if info.IsDir() {
   245  			return nil
   246  		}
   247  		count++
   248  		return nil
   249  	})
   250  	s.Assert(count, qt.Equals, expected)
   251  }
   252  
   253  func (s *IntegrationTestBuilder) AssertFileContent(filename string, matches ...string) {
   254  	s.Helper()
   255  	content := strings.TrimSpace(s.FileContent(filename))
   256  	for _, m := range matches {
   257  		cm := qt.Commentf("File: %s Match %s", filename, m)
   258  		lines := strings.Split(m, "\n")
   259  		for _, match := range lines {
   260  			match = strings.TrimSpace(match)
   261  			if match == "" || strings.HasPrefix(match, "#") {
   262  				continue
   263  			}
   264  			var negate bool
   265  			if strings.HasPrefix(match, "! ") {
   266  				negate = true
   267  				match = strings.TrimPrefix(match, "! ")
   268  			}
   269  			if negate {
   270  				s.Assert(content, qt.Not(qt.Contains), match, cm)
   271  				continue
   272  			}
   273  			s.Assert(content, qt.Contains, match, cm)
   274  		}
   275  	}
   276  }
   277  
   278  func (s *IntegrationTestBuilder) AssertFileContentExact(filename string, matches ...string) {
   279  	s.Helper()
   280  	content := s.FileContent(filename)
   281  	for _, m := range matches {
   282  		s.Assert(content, qt.Contains, m, qt.Commentf(m))
   283  	}
   284  }
   285  
   286  func (s *IntegrationTestBuilder) AssertPublishDir(matches ...string) {
   287  	s.AssertFs(s.fs.PublishDir, matches...)
   288  }
   289  
   290  func (s *IntegrationTestBuilder) AssertFs(fs afero.Fs, matches ...string) {
   291  	s.Helper()
   292  	var buff bytes.Buffer
   293  	s.Assert(s.printAndCheckFs(fs, "", &buff), qt.IsNil)
   294  	printFsLines := strings.Split(buff.String(), "\n")
   295  	sort.Strings(printFsLines)
   296  	content := strings.TrimSpace((strings.Join(printFsLines, "\n")))
   297  	for _, m := range matches {
   298  		cm := qt.Commentf("Match: %q\nIn:\n%s", m, content)
   299  		lines := strings.Split(m, "\n")
   300  		for _, match := range lines {
   301  			match = strings.TrimSpace(match)
   302  			var negate bool
   303  			if strings.HasPrefix(match, "! ") {
   304  				negate = true
   305  				match = strings.TrimPrefix(match, "! ")
   306  			}
   307  			if negate {
   308  				s.Assert(content, qt.Not(qt.Contains), match, cm)
   309  				continue
   310  			}
   311  			s.Assert(content, qt.Contains, match, cm)
   312  		}
   313  	}
   314  }
   315  
   316  func (s *IntegrationTestBuilder) printAndCheckFs(fs afero.Fs, path string, w io.Writer) error {
   317  	if fs == nil {
   318  		return nil
   319  	}
   320  
   321  	return afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error {
   322  		if err != nil {
   323  			return fmt.Errorf("error: path %q: %s", path, err)
   324  		}
   325  		path = filepath.ToSlash(path)
   326  		if path == "" {
   327  			path = "."
   328  		}
   329  		if !info.IsDir() {
   330  			f, err := fs.Open(path)
   331  			if err != nil {
   332  				return fmt.Errorf("error: path %q: %s", path, err)
   333  			}
   334  			defer f.Close()
   335  			// This will panic if the file is a directory.
   336  			var buf [1]byte
   337  			io.ReadFull(f, buf[:]) // nolint
   338  		}
   339  		fmt.Fprintln(w, path, info.IsDir())
   340  		return nil
   341  	})
   342  }
   343  
   344  func (s *IntegrationTestBuilder) AssertFileExists(filename string, b bool) {
   345  	checker := qt.IsNil
   346  	if !b {
   347  		checker = qt.IsNotNil
   348  	}
   349  	_, err := s.fs.WorkingDirReadOnly.Stat(filename)
   350  	if !herrors.IsNotExist(err) {
   351  		s.Assert(err, qt.IsNil)
   352  	}
   353  	s.Assert(err, checker)
   354  }
   355  
   356  func (s *IntegrationTestBuilder) AssertIsFileError(err error) herrors.FileError {
   357  	s.Assert(err, qt.ErrorAs, new(herrors.FileError))
   358  	return herrors.UnwrapFileError(err)
   359  }
   360  
   361  func (s *IntegrationTestBuilder) AssertRenderCountContent(count int) {
   362  	s.Helper()
   363  	s.Assert(s.counters.contentRenderCounter.Load(), qt.Equals, uint64(count))
   364  }
   365  
   366  func (s *IntegrationTestBuilder) AssertRenderCountPage(count int) {
   367  	s.Helper()
   368  	s.Assert(s.counters.pageRenderCounter.Load(), qt.Equals, uint64(count))
   369  }
   370  
   371  func (s *IntegrationTestBuilder) AssertRenderCountPageBetween(from, to int) {
   372  	s.Helper()
   373  	i := int(s.counters.pageRenderCounter.Load())
   374  	s.Assert(i >= from && i <= to, qt.IsTrue)
   375  }
   376  
   377  func (s *IntegrationTestBuilder) Build() *IntegrationTestBuilder {
   378  	s.Helper()
   379  	_, err := s.BuildE()
   380  	if s.Cfg.Verbose || err != nil {
   381  		fmt.Println(s.lastBuildLog)
   382  		if s.H != nil && err == nil {
   383  			for _, s := range s.H.Sites {
   384  				m := s.pageMap
   385  				var buff bytes.Buffer
   386  				fmt.Fprintf(&buff, "PageMap for site %q\n\n", s.Language().Lang)
   387  				m.debugPrint("", 999, &buff)
   388  				fmt.Println(buff.String())
   389  			}
   390  		}
   391  	} else if s.Cfg.LogLevel <= logg.LevelDebug {
   392  		fmt.Println(s.lastBuildLog)
   393  	}
   394  	s.Assert(err, qt.IsNil)
   395  	if s.Cfg.RunGC {
   396  		s.GCCount, err = s.H.GC()
   397  		s.Assert(err, qt.IsNil)
   398  	}
   399  
   400  	return s
   401  }
   402  
   403  func (s *IntegrationTestBuilder) LogString() string {
   404  	return s.lastBuildLog
   405  }
   406  
   407  func (s *IntegrationTestBuilder) BuildE() (*IntegrationTestBuilder, error) {
   408  	s.Helper()
   409  	if err := s.initBuilder(); err != nil {
   410  		return s, err
   411  	}
   412  
   413  	err := s.build(s.Cfg.BuildCfg)
   414  	return s, err
   415  }
   416  
   417  func (s *IntegrationTestBuilder) Init() *IntegrationTestBuilder {
   418  	if err := s.initBuilder(); err != nil {
   419  		s.Fatalf("Failed to init builder: %s", err)
   420  	}
   421  	s.lastBuildLog = s.logBuff.String()
   422  	return s
   423  }
   424  
   425  type IntegrationTestDebugConfig struct {
   426  	Out io.Writer
   427  
   428  	PrintDestinationFs bool
   429  	PrintPagemap       bool
   430  
   431  	PrefixDestinationFs string
   432  	PrefixPagemap       string
   433  }
   434  
   435  func (s *IntegrationTestBuilder) EditFileReplaceAll(filename, old, new string) *IntegrationTestBuilder {
   436  	return s.EditFileReplaceFunc(filename, func(s string) string {
   437  		return strings.ReplaceAll(s, old, new)
   438  	})
   439  }
   440  
   441  func (s *IntegrationTestBuilder) EditFileReplaceFunc(filename string, replacementFunc func(s string) string) *IntegrationTestBuilder {
   442  	absFilename := s.absFilename(filename)
   443  	b, err := afero.ReadFile(s.fs.Source, absFilename)
   444  	s.Assert(err, qt.IsNil)
   445  	s.changedFiles = append(s.changedFiles, absFilename)
   446  	oldContent := string(b)
   447  	s.writeSource(absFilename, replacementFunc(oldContent))
   448  	return s
   449  }
   450  
   451  func (s *IntegrationTestBuilder) EditFiles(filenameContent ...string) *IntegrationTestBuilder {
   452  	for i := 0; i < len(filenameContent); i += 2 {
   453  		filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1]
   454  		absFilename := s.absFilename(filename)
   455  		s.changedFiles = append(s.changedFiles, absFilename)
   456  		s.writeSource(absFilename, content)
   457  	}
   458  	return s
   459  }
   460  
   461  func (s *IntegrationTestBuilder) AddFiles(filenameContent ...string) *IntegrationTestBuilder {
   462  	for i := 0; i < len(filenameContent); i += 2 {
   463  		filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1]
   464  		absFilename := s.absFilename(filename)
   465  		s.createdFiles = append(s.createdFiles, absFilename)
   466  		s.writeSource(absFilename, content)
   467  	}
   468  	return s
   469  }
   470  
   471  func (s *IntegrationTestBuilder) RemoveFiles(filenames ...string) *IntegrationTestBuilder {
   472  	for _, filename := range filenames {
   473  		absFilename := s.absFilename(filename)
   474  		s.removedFiles = append(s.removedFiles, absFilename)
   475  		s.Assert(s.fs.Source.Remove(absFilename), qt.IsNil)
   476  
   477  	}
   478  
   479  	return s
   480  }
   481  
   482  func (s *IntegrationTestBuilder) RenameFile(old, new string) *IntegrationTestBuilder {
   483  	absOldFilename := s.absFilename(old)
   484  	absNewFilename := s.absFilename(new)
   485  	s.renamedFiles = append(s.renamedFiles, absOldFilename)
   486  	s.createdFiles = append(s.createdFiles, absNewFilename)
   487  	s.Assert(s.fs.Source.MkdirAll(filepath.Dir(absNewFilename), 0o777), qt.IsNil)
   488  	s.Assert(s.fs.Source.Rename(absOldFilename, absNewFilename), qt.IsNil)
   489  	return s
   490  }
   491  
   492  func (s *IntegrationTestBuilder) RenameDir(old, new string) *IntegrationTestBuilder {
   493  	absOldFilename := s.absFilename(old)
   494  	absNewFilename := s.absFilename(new)
   495  	s.renamedDirs = append(s.renamedDirs, absOldFilename)
   496  	s.changedFiles = append(s.changedFiles, absNewFilename)
   497  	// nolint
   498  	afero.Walk(s.fs.Source, absOldFilename, func(path string, info os.FileInfo, err error) error {
   499  		if err != nil {
   500  			return err
   501  		}
   502  		if info.IsDir() {
   503  			return nil
   504  		}
   505  		s.createdFiles = append(s.createdFiles, strings.Replace(path, absOldFilename, absNewFilename, 1))
   506  		return nil
   507  	})
   508  	s.Assert(s.fs.Source.MkdirAll(filepath.Dir(absNewFilename), 0o777), qt.IsNil)
   509  	s.Assert(s.fs.Source.Rename(absOldFilename, absNewFilename), qt.IsNil)
   510  	return s
   511  }
   512  
   513  func (s *IntegrationTestBuilder) FileContent(filename string) string {
   514  	s.Helper()
   515  	return s.readWorkingDir(s, s.fs, filepath.FromSlash(filename))
   516  }
   517  
   518  func (s *IntegrationTestBuilder) initBuilder() error {
   519  	var initErr error
   520  	s.builderInit.Do(func() {
   521  		var afs afero.Fs
   522  		if s.Cfg.NeedsOsFS {
   523  			afs = afero.NewOsFs()
   524  		} else {
   525  			afs = afero.NewMemMapFs()
   526  		}
   527  
   528  		if s.Cfg.LogLevel == 0 {
   529  			s.Cfg.LogLevel = logg.LevelError
   530  		}
   531  
   532  		isBinaryRe := regexp.MustCompile(`^(.*)(\.png|\.jpg)$`)
   533  
   534  		const dataSourceFilenamePrefix = "sourcefilename:"
   535  
   536  		for _, f := range s.data.Files {
   537  			filename := filepath.Join(s.Cfg.WorkingDir, f.Name)
   538  			data := bytes.TrimSuffix(f.Data, []byte("\n"))
   539  			datastr := strings.TrimSpace(string(data))
   540  			if strings.HasPrefix(datastr, dataSourceFilenamePrefix) {
   541  				// Read from file relative to the current dir.
   542  				var err error
   543  				wd, _ := os.Getwd()
   544  				filename := filepath.Join(wd, strings.TrimSpace(strings.TrimPrefix(datastr, dataSourceFilenamePrefix)))
   545  				data, err = os.ReadFile(filename)
   546  				s.Assert(err, qt.IsNil)
   547  			} else if isBinaryRe.MatchString(filename) {
   548  				var err error
   549  				data, err = base64.StdEncoding.DecodeString(string(data))
   550  				s.Assert(err, qt.IsNil)
   551  
   552  			}
   553  			s.Assert(afs.MkdirAll(filepath.Dir(filename), 0o777), qt.IsNil)
   554  			s.Assert(afero.WriteFile(afs, filename, data, 0o666), qt.IsNil)
   555  		}
   556  
   557  		configDir := "config"
   558  		if _, err := afs.Stat(filepath.Join(s.Cfg.WorkingDir, "config")); err != nil {
   559  			configDir = ""
   560  		}
   561  
   562  		var flags config.Provider
   563  		if s.Cfg.BaseCfg != nil {
   564  			flags = s.Cfg.BaseCfg
   565  		} else {
   566  			flags = config.New()
   567  		}
   568  
   569  		if s.Cfg.Running {
   570  			flags.Set("internal", maps.Params{
   571  				"running": s.Cfg.Running,
   572  				"watch":   s.Cfg.Running,
   573  			})
   574  		}
   575  
   576  		if s.Cfg.WorkingDir != "" {
   577  			flags.Set("workingDir", s.Cfg.WorkingDir)
   578  		}
   579  
   580  		var w io.Writer
   581  		if s.Cfg.LogLevel == logg.LevelTrace {
   582  			w = os.Stdout
   583  		} else {
   584  			w = &s.logBuff
   585  		}
   586  
   587  		logger := loggers.New(
   588  			loggers.Options{
   589  				Stdout:        w,
   590  				Stderr:        w,
   591  				Level:         s.Cfg.LogLevel,
   592  				DistinctLevel: logg.LevelWarn,
   593  			},
   594  		)
   595  
   596  		res, err := allconfig.LoadConfig(
   597  			allconfig.ConfigSourceDescriptor{
   598  				Flags:     flags,
   599  				ConfigDir: configDir,
   600  				Fs:        afs,
   601  				Logger:    logger,
   602  				Environ:   s.Cfg.Environ,
   603  			},
   604  		)
   605  		if err != nil {
   606  			initErr = err
   607  			return
   608  		}
   609  
   610  		fs := hugofs.NewFrom(afs, res.LoadingInfo.BaseConfig)
   611  
   612  		s.Assert(err, qt.IsNil)
   613  
   614  		depsCfg := deps.DepsCfg{Configs: res, Fs: fs, LogLevel: logger.Level(), LogOut: logger.Out()}
   615  		sites, err := NewHugoSites(depsCfg)
   616  		if err != nil {
   617  			initErr = err
   618  			return
   619  		}
   620  		if sites == nil {
   621  			initErr = errors.New("no sites")
   622  			return
   623  		}
   624  
   625  		s.H = sites
   626  		s.fs = fs
   627  
   628  		if s.Cfg.NeedsNpmInstall {
   629  			wd, _ := os.Getwd()
   630  			s.Assert(os.Chdir(s.Cfg.WorkingDir), qt.IsNil)
   631  			// nolint
   632  			s.C.Cleanup(func() { os.Chdir(wd) })
   633  			sc := security.DefaultConfig
   634  			sc.Exec.Allow, err = security.NewWhitelist("npm")
   635  			s.Assert(err, qt.IsNil)
   636  			ex := hexec.New(sc)
   637  			command, err := ex.New("npm", "install")
   638  			s.Assert(err, qt.IsNil)
   639  			s.Assert(command.Run(), qt.IsNil)
   640  
   641  		}
   642  	})
   643  
   644  	return initErr
   645  }
   646  
   647  func (s *IntegrationTestBuilder) absFilename(filename string) string {
   648  	filename = filepath.FromSlash(filename)
   649  	if filepath.IsAbs(filename) {
   650  		return filename
   651  	}
   652  	if s.Cfg.WorkingDir != "" && !strings.HasPrefix(filename, s.Cfg.WorkingDir) {
   653  		filename = filepath.Join(s.Cfg.WorkingDir, filename)
   654  	}
   655  	return filename
   656  }
   657  
   658  func (s *IntegrationTestBuilder) reset() {
   659  	s.changedFiles = nil
   660  	s.createdFiles = nil
   661  	s.removedFiles = nil
   662  	s.renamedFiles = nil
   663  }
   664  
   665  func (s *IntegrationTestBuilder) build(cfg BuildCfg) error {
   666  	s.Helper()
   667  	defer func() {
   668  		s.reset()
   669  		s.lastBuildLog = s.logBuff.String()
   670  		s.logBuff.Reset()
   671  	}()
   672  
   673  	changeEvents := s.changeEvents()
   674  	s.counters = &buildCounters{}
   675  	cfg.testCounters = s.counters
   676  
   677  	if s.buildCount > 0 && (len(changeEvents) == 0) {
   678  		return nil
   679  	}
   680  
   681  	s.buildCount++
   682  
   683  	err := s.H.Build(cfg, changeEvents...)
   684  	if err != nil {
   685  		return err
   686  	}
   687  
   688  	return nil
   689  }
   690  
   691  func (s *IntegrationTestBuilder) changeEvents() []fsnotify.Event {
   692  	var events []fsnotify.Event
   693  	for _, v := range s.removedFiles {
   694  		events = append(events, fsnotify.Event{
   695  			Name: v,
   696  			Op:   fsnotify.Remove,
   697  		})
   698  	}
   699  	for _, v := range s.renamedFiles {
   700  		events = append(events, fsnotify.Event{
   701  			Name: v,
   702  			Op:   fsnotify.Rename,
   703  		})
   704  	}
   705  
   706  	for _, v := range s.renamedDirs {
   707  		events = append(events, fsnotify.Event{
   708  			Name: v,
   709  			// This is what we get on MacOS.
   710  			Op: fsnotify.Remove | fsnotify.Rename,
   711  		})
   712  	}
   713  
   714  	for _, v := range s.changedFiles {
   715  		events = append(events, fsnotify.Event{
   716  			Name: v,
   717  			Op:   fsnotify.Write,
   718  		})
   719  	}
   720  	for _, v := range s.createdFiles {
   721  		events = append(events, fsnotify.Event{
   722  			Name: v,
   723  			Op:   fsnotify.Create,
   724  		})
   725  	}
   726  
   727  	// Shuffle events.
   728  	for i := range events {
   729  		j := rand.Intn(i + 1)
   730  		events[i], events[j] = events[j], events[i]
   731  	}
   732  
   733  	return events
   734  }
   735  
   736  func (s *IntegrationTestBuilder) readWorkingDir(t testing.TB, fs *hugofs.Fs, filename string) string {
   737  	t.Helper()
   738  	return s.readFileFromFs(t, fs.WorkingDirReadOnly, filename)
   739  }
   740  
   741  func (s *IntegrationTestBuilder) readFileFromFs(t testing.TB, fs afero.Fs, filename string) string {
   742  	t.Helper()
   743  	filename = filepath.Clean(filename)
   744  	b, err := afero.ReadFile(fs, filename)
   745  	if err != nil {
   746  		// Print some debug info
   747  		hadSlash := strings.HasPrefix(filename, helpers.FilePathSeparator)
   748  		start := 0
   749  		if hadSlash {
   750  			start = 1
   751  		}
   752  		end := start + 1
   753  
   754  		parts := strings.Split(filename, helpers.FilePathSeparator)
   755  		if parts[start] == "work" {
   756  			// nolint
   757  			end++
   758  		}
   759  
   760  		s.Assert(err, qt.IsNil)
   761  
   762  	}
   763  	return string(b)
   764  }
   765  
   766  func (s *IntegrationTestBuilder) writeSource(filename, content string) {
   767  	s.Helper()
   768  	s.writeToFs(s.fs.Source, filename, content)
   769  }
   770  
   771  func (s *IntegrationTestBuilder) writeToFs(fs afero.Fs, filename, content string) {
   772  	s.Helper()
   773  	if err := afero.WriteFile(fs, filepath.FromSlash(filename), []byte(content), 0o755); err != nil {
   774  		s.Fatalf("Failed to write file: %s", err)
   775  	}
   776  }
   777  
   778  type IntegrationTestConfig struct {
   779  	T testing.TB
   780  
   781  	// The files to use on txtar format, see
   782  	// https://pkg.go.dev/golang.org/x/exp/cmd/txtar
   783  	TxtarString string
   784  
   785  	// COnfig to use as the base. We will also read the config from the txtar.
   786  	BaseCfg config.Provider
   787  
   788  	// Environment variables passed to the config loader.
   789  	Environ []string
   790  
   791  	// Whether to simulate server mode.
   792  	Running bool
   793  
   794  	// Will print the log buffer after the build
   795  	Verbose bool
   796  
   797  	// The log level to use.
   798  	LogLevel logg.Level
   799  
   800  	// Whether it needs the real file system (e.g. for js.Build tests).
   801  	NeedsOsFS bool
   802  
   803  	// Whether to run GC after each build.
   804  	RunGC bool
   805  
   806  	// Do not remove the temp dir after the test.
   807  	PrintAndKeepTempDir bool
   808  
   809  	// Whether to run npm install before Build.
   810  	NeedsNpmInstall bool
   811  
   812  	// Whether to normalize the Unicode filenames to NFD on Darwin.
   813  	NFDFormOnDarwin bool
   814  
   815  	// The working dir to use. If not absolute, a temp dir will be created.
   816  	WorkingDir string
   817  
   818  	// The config to pass to Build.
   819  	BuildCfg BuildCfg
   820  }