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 }