github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/hugolib/integrationtest_builder.go (about) 1 package hugolib 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "strings" 10 "sync" 11 "testing" 12 13 jww "github.com/spf13/jwalterweatherman" 14 15 qt "github.com/frankban/quicktest" 16 "github.com/fsnotify/fsnotify" 17 "github.com/gohugoio/hugo/common/herrors" 18 "github.com/gohugoio/hugo/common/hexec" 19 "github.com/gohugoio/hugo/common/loggers" 20 "github.com/gohugoio/hugo/config" 21 "github.com/gohugoio/hugo/config/security" 22 "github.com/gohugoio/hugo/deps" 23 "github.com/gohugoio/hugo/helpers" 24 "github.com/gohugoio/hugo/htesting" 25 "github.com/gohugoio/hugo/hugofs" 26 "github.com/spf13/afero" 27 "golang.org/x/tools/txtar" 28 ) 29 30 func NewIntegrationTestBuilder(conf IntegrationTestConfig) *IntegrationTestBuilder { 31 data := txtar.Parse([]byte(conf.TxtarString)) 32 33 c, ok := conf.T.(*qt.C) 34 if !ok { 35 c = qt.New(conf.T) 36 } 37 38 if conf.NeedsOsFS { 39 doClean := true 40 tempDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-integration-test") 41 c.Assert(err, qt.IsNil) 42 conf.WorkingDir = filepath.Join(tempDir, conf.WorkingDir) 43 if doClean { 44 c.Cleanup(clean) 45 } 46 } 47 48 return &IntegrationTestBuilder{ 49 Cfg: conf, 50 C: c, 51 data: data, 52 } 53 } 54 55 // IntegrationTestBuilder is a (partial) rewrite of sitesBuilder. 56 // The main problem with the "old" one was that it was that the test data was often a little hidden, 57 // so it became hard to look at a test and determine what it should do, especially coming back to the 58 // test after a year or so. 59 type IntegrationTestBuilder struct { 60 *qt.C 61 62 data *txtar.Archive 63 64 fs *hugofs.Fs 65 H *HugoSites 66 67 Cfg IntegrationTestConfig 68 69 changedFiles []string 70 createdFiles []string 71 removedFiles []string 72 renamedFiles []string 73 74 buildCount int 75 counters *testCounters 76 logBuff lockingBuffer 77 78 builderInit sync.Once 79 } 80 81 type lockingBuffer struct { 82 sync.Mutex 83 bytes.Buffer 84 } 85 86 func (b *lockingBuffer) Write(p []byte) (n int, err error) { 87 b.Lock() 88 n, err = b.Buffer.Write(p) 89 b.Unlock() 90 return 91 } 92 93 func (s *IntegrationTestBuilder) AssertLogContains(text string) { 94 s.Helper() 95 s.Assert(s.logBuff.String(), qt.Contains, text) 96 } 97 98 func (s *IntegrationTestBuilder) AssertBuildCountData(count int) { 99 s.Helper() 100 s.Assert(s.H.init.data.InitCount(), qt.Equals, count) 101 } 102 103 func (s *IntegrationTestBuilder) AssertBuildCountGitInfo(count int) { 104 s.Helper() 105 s.Assert(s.H.init.gitInfo.InitCount(), qt.Equals, count) 106 } 107 108 func (s *IntegrationTestBuilder) AssertBuildCountLayouts(count int) { 109 s.Helper() 110 s.Assert(s.H.init.layouts.InitCount(), qt.Equals, count) 111 } 112 113 func (s *IntegrationTestBuilder) AssertBuildCountTranslations(count int) { 114 s.Helper() 115 s.Assert(s.H.init.translations.InitCount(), qt.Equals, count) 116 } 117 118 func (s *IntegrationTestBuilder) AssertFileContent(filename string, matches ...string) { 119 s.Helper() 120 content := strings.TrimSpace(s.FileContent(filename)) 121 for _, m := range matches { 122 lines := strings.Split(m, "\n") 123 for _, match := range lines { 124 match = strings.TrimSpace(match) 125 if match == "" || strings.HasPrefix(match, "#") { 126 continue 127 } 128 s.Assert(content, qt.Contains, match, qt.Commentf(m)) 129 } 130 } 131 } 132 133 func (s *IntegrationTestBuilder) AssertDestinationExists(filename string, b bool) { 134 checker := qt.IsTrue 135 if !b { 136 checker = qt.IsFalse 137 } 138 s.Assert(s.destinationExists(filepath.Clean(filename)), checker) 139 } 140 141 func (s *IntegrationTestBuilder) destinationExists(filename string) bool { 142 b, err := helpers.Exists(filename, s.fs.Destination) 143 if err != nil { 144 panic(err) 145 } 146 return b 147 } 148 149 func (s *IntegrationTestBuilder) AssertIsFileError(err error) { 150 var ferr *herrors.ErrorWithFileContext 151 s.Assert(err, qt.ErrorAs, &ferr) 152 } 153 154 func (s *IntegrationTestBuilder) AssertRenderCountContent(count int) { 155 s.Helper() 156 s.Assert(s.counters.contentRenderCounter, qt.Equals, uint64(count)) 157 } 158 159 func (s *IntegrationTestBuilder) AssertRenderCountPage(count int) { 160 s.Helper() 161 s.Assert(s.counters.pageRenderCounter, qt.Equals, uint64(count)) 162 } 163 164 func (s *IntegrationTestBuilder) Build() *IntegrationTestBuilder { 165 s.Helper() 166 _, err := s.BuildE() 167 if s.Cfg.Verbose || err != nil { 168 fmt.Println(s.logBuff.String()) 169 } 170 s.Assert(err, qt.IsNil) 171 return s 172 } 173 174 func (s *IntegrationTestBuilder) BuildE() (*IntegrationTestBuilder, error) { 175 s.Helper() 176 s.initBuilder() 177 err := s.build(BuildCfg{}) 178 return s, err 179 } 180 181 type IntegrationTestDebugConfig struct { 182 Out io.Writer 183 184 PrintDestinationFs bool 185 PrintPagemap bool 186 187 PrefixDestinationFs string 188 PrefixPagemap string 189 } 190 191 func (s *IntegrationTestBuilder) EditFileReplace(filename string, replacementFunc func(s string) string) *IntegrationTestBuilder { 192 absFilename := s.absFilename(filename) 193 b, err := afero.ReadFile(s.fs.Source, absFilename) 194 s.Assert(err, qt.IsNil) 195 s.changedFiles = append(s.changedFiles, absFilename) 196 oldContent := string(b) 197 s.writeSource(absFilename, replacementFunc(oldContent)) 198 return s 199 } 200 201 func (s *IntegrationTestBuilder) EditFiles(filenameContent ...string) *IntegrationTestBuilder { 202 for i := 0; i < len(filenameContent); i += 2 { 203 filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1] 204 absFilename := s.absFilename(filename) 205 s.changedFiles = append(s.changedFiles, absFilename) 206 s.writeSource(absFilename, content) 207 } 208 return s 209 } 210 211 func (s *IntegrationTestBuilder) AddFiles(filenameContent ...string) *IntegrationTestBuilder { 212 for i := 0; i < len(filenameContent); i += 2 { 213 filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1] 214 absFilename := s.absFilename(filename) 215 s.createdFiles = append(s.createdFiles, absFilename) 216 s.writeSource(absFilename, content) 217 } 218 return s 219 } 220 221 func (s *IntegrationTestBuilder) RemoveFiles(filenames ...string) *IntegrationTestBuilder { 222 for _, filename := range filenames { 223 absFilename := s.absFilename(filename) 224 s.removedFiles = append(s.removedFiles, absFilename) 225 s.Assert(s.fs.Source.Remove(absFilename), qt.IsNil) 226 227 } 228 229 return s 230 } 231 232 func (s *IntegrationTestBuilder) RenameFile(old, new string) *IntegrationTestBuilder { 233 absOldFilename := s.absFilename(old) 234 absNewFilename := s.absFilename(new) 235 s.renamedFiles = append(s.renamedFiles, absOldFilename) 236 s.createdFiles = append(s.createdFiles, absNewFilename) 237 s.Assert(s.fs.Source.Rename(absOldFilename, absNewFilename), qt.IsNil) 238 return s 239 } 240 241 func (s *IntegrationTestBuilder) FileContent(filename string) string { 242 s.Helper() 243 filename = filepath.FromSlash(filename) 244 if !strings.HasPrefix(filename, s.Cfg.WorkingDir) { 245 filename = filepath.Join(s.Cfg.WorkingDir, filename) 246 } 247 return s.readDestination(s, s.fs, filename) 248 } 249 250 func (s *IntegrationTestBuilder) initBuilder() { 251 s.builderInit.Do(func() { 252 var afs afero.Fs 253 if s.Cfg.NeedsOsFS { 254 afs = afero.NewOsFs() 255 } else { 256 afs = afero.NewMemMapFs() 257 } 258 259 if s.Cfg.LogLevel == 0 { 260 s.Cfg.LogLevel = jww.LevelWarn 261 } 262 263 logger := loggers.NewBasicLoggerForWriter(s.Cfg.LogLevel, &s.logBuff) 264 265 fs := hugofs.NewFrom(afs, config.New()) 266 267 for _, f := range s.data.Files { 268 filename := filepath.Join(s.Cfg.WorkingDir, f.Name) 269 s.Assert(afs.MkdirAll(filepath.Dir(filename), 0777), qt.IsNil) 270 s.Assert(afero.WriteFile(afs, filename, bytes.TrimSuffix(f.Data, []byte("\n")), 0666), qt.IsNil) 271 } 272 273 cfg, _, err := LoadConfig( 274 ConfigSourceDescriptor{ 275 WorkingDir: s.Cfg.WorkingDir, 276 Fs: afs, 277 Logger: logger, 278 Environ: []string{}, 279 Filename: "config.toml", 280 }, 281 func(cfg config.Provider) error { 282 return nil 283 }, 284 ) 285 286 s.Assert(err, qt.IsNil) 287 288 cfg.Set("workingDir", s.Cfg.WorkingDir) 289 290 depsCfg := deps.DepsCfg{Cfg: cfg, Fs: fs, Running: s.Cfg.Running, Logger: logger} 291 sites, err := NewHugoSites(depsCfg) 292 s.Assert(err, qt.IsNil) 293 294 s.H = sites 295 s.fs = fs 296 297 if s.Cfg.NeedsNpmInstall { 298 wd, _ := os.Getwd() 299 s.Assert(os.Chdir(s.Cfg.WorkingDir), qt.IsNil) 300 s.C.Cleanup(func() { os.Chdir(wd) }) 301 sc := security.DefaultConfig 302 sc.Exec.Allow = security.NewWhitelist("npm") 303 ex := hexec.New(sc) 304 command, err := ex.New("npm", "install") 305 s.Assert(err, qt.IsNil) 306 s.Assert(command.Run(), qt.IsNil) 307 308 } 309 }) 310 } 311 312 func (s *IntegrationTestBuilder) absFilename(filename string) string { 313 filename = filepath.FromSlash(filename) 314 if filepath.IsAbs(filename) { 315 return filename 316 } 317 if s.Cfg.WorkingDir != "" && !strings.HasPrefix(filename, s.Cfg.WorkingDir) { 318 filename = filepath.Join(s.Cfg.WorkingDir, filename) 319 } 320 return filename 321 } 322 323 func (s *IntegrationTestBuilder) build(cfg BuildCfg) error { 324 s.Helper() 325 defer func() { 326 s.changedFiles = nil 327 s.createdFiles = nil 328 s.removedFiles = nil 329 s.renamedFiles = nil 330 }() 331 332 changeEvents := s.changeEvents() 333 s.logBuff.Reset() 334 s.counters = &testCounters{} 335 cfg.testCounters = s.counters 336 337 if s.buildCount > 0 && (len(changeEvents) == 0) { 338 return nil 339 } 340 341 s.buildCount++ 342 343 err := s.H.Build(cfg, changeEvents...) 344 if err != nil { 345 return err 346 } 347 logErrorCount := s.H.NumLogErrors() 348 if logErrorCount > 0 { 349 return fmt.Errorf("logged %d error(s): %s", logErrorCount, s.logBuff.String()) 350 } 351 352 return nil 353 } 354 355 func (s *IntegrationTestBuilder) changeEvents() []fsnotify.Event { 356 var events []fsnotify.Event 357 for _, v := range s.removedFiles { 358 events = append(events, fsnotify.Event{ 359 Name: v, 360 Op: fsnotify.Remove, 361 }) 362 } 363 for _, v := range s.renamedFiles { 364 events = append(events, fsnotify.Event{ 365 Name: v, 366 Op: fsnotify.Rename, 367 }) 368 } 369 for _, v := range s.changedFiles { 370 events = append(events, fsnotify.Event{ 371 Name: v, 372 Op: fsnotify.Write, 373 }) 374 } 375 for _, v := range s.createdFiles { 376 events = append(events, fsnotify.Event{ 377 Name: v, 378 Op: fsnotify.Create, 379 }) 380 } 381 382 return events 383 } 384 385 func (s *IntegrationTestBuilder) readDestination(t testing.TB, fs *hugofs.Fs, filename string) string { 386 t.Helper() 387 return s.readFileFromFs(t, fs.Destination, filename) 388 } 389 390 func (s *IntegrationTestBuilder) readFileFromFs(t testing.TB, fs afero.Fs, filename string) string { 391 t.Helper() 392 filename = filepath.Clean(filename) 393 b, err := afero.ReadFile(fs, filename) 394 if err != nil { 395 // Print some debug info 396 hadSlash := strings.HasPrefix(filename, helpers.FilePathSeparator) 397 start := 0 398 if hadSlash { 399 start = 1 400 } 401 end := start + 1 402 403 parts := strings.Split(filename, helpers.FilePathSeparator) 404 if parts[start] == "work" { 405 end++ 406 } 407 408 s.Assert(err, qt.IsNil) 409 410 } 411 return string(b) 412 } 413 414 func (s *IntegrationTestBuilder) writeSource(filename, content string) { 415 s.Helper() 416 s.writeToFs(s.fs.Source, filename, content) 417 } 418 419 func (s *IntegrationTestBuilder) writeToFs(fs afero.Fs, filename, content string) { 420 s.Helper() 421 if err := afero.WriteFile(fs, filepath.FromSlash(filename), []byte(content), 0755); err != nil { 422 s.Fatalf("Failed to write file: %s", err) 423 } 424 } 425 426 type IntegrationTestConfig struct { 427 T testing.TB 428 429 // The files to use on txtar format, see 430 // https://pkg.go.dev/golang.org/x/exp/cmd/txtar 431 TxtarString string 432 433 // Whether to simulate server mode. 434 Running bool 435 436 // Will print the log buffer after the build 437 Verbose bool 438 439 LogLevel jww.Threshold 440 441 // Whether it needs the real file system (e.g. for js.Build tests). 442 NeedsOsFS bool 443 444 // Whether to run npm install before Build. 445 NeedsNpmInstall bool 446 447 WorkingDir string 448 }