Files
2025-09-17 11:56:05 -04:00

345 lines
9.6 KiB
Go

package bundler_tests
// Bundling test results are stored in snapshot files, located in the
// "snapshots" directory. This allows test results to be updated easily without
// manually rewriting all of the expected values. To update the tests run
// "UPDATE_SNAPSHOTS=1 make test" and commit the updated values. Make sure to
// inspect the diff to ensure the expected values are valid.
import (
"fmt"
"io/ioutil"
"os"
"path"
"sort"
"strings"
"sync"
"testing"
"github.com/evanw/esbuild/internal/bundler"
"github.com/evanw/esbuild/internal/cache"
"github.com/evanw/esbuild/internal/compat"
"github.com/evanw/esbuild/internal/config"
"github.com/evanw/esbuild/internal/fs"
"github.com/evanw/esbuild/internal/linker"
"github.com/evanw/esbuild/internal/logger"
"github.com/evanw/esbuild/internal/test"
)
func es(version int) compat.JSFeature {
return compat.UnsupportedJSFeatures(map[compat.Engine]compat.Semver{
compat.ES: {Parts: []int{version}},
})
}
func assertLog(t *testing.T, msgs []logger.Msg, expected string) {
t.Helper()
var text strings.Builder
for _, msg := range msgs {
text.WriteString(msg.String(logger.OutputOptions{}, logger.TerminalInfo{}))
}
test.AssertEqualWithDiff(t, text.String(), expected)
}
func hasErrors(msgs []logger.Msg) bool {
for _, msg := range msgs {
if msg.Kind == logger.Error {
return true
}
}
return false
}
type bundled struct {
files map[string]string
entryPaths []string
entryPathsAdvanced []bundler.EntryPoint
expectedScanLog string
expectedCompileLog string
options config.Options
debugLogs bool
absWorkingDir string
}
type suite struct {
expectedSnapshots map[string]string
generatedSnapshots map[string]string
name string
path string
mutex sync.Mutex
}
func (s *suite) expectBundled(t *testing.T, args bundled) {
t.Helper()
s.__expectBundledImpl(t, args, fs.MockUnix)
// Handle conversion to Windows-style paths
{
files := make(map[string]string)
for k, v := range args.files {
files[unix2win(k)] = v
}
args.files = files
args.entryPaths = append([]string{}, args.entryPaths...)
for i, entry := range args.entryPaths {
args.entryPaths[i] = unix2win(entry)
}
args.absWorkingDir = unix2win(args.absWorkingDir)
args.options.InjectPaths = append([]string{}, args.options.InjectPaths...)
for i, absPath := range args.options.InjectPaths {
args.options.InjectPaths[i] = unix2win(absPath)
}
aliases := make(map[string]string)
for k, v := range args.options.PackageAliases {
if strings.HasPrefix(v, "/") {
v = unix2win(v)
}
aliases[k] = v
}
args.options.PackageAliases = aliases
replace := make(map[string]bool)
for k, v := range args.options.ExternalSettings.PostResolve.Exact {
replace[unix2win(k)] = v
}
args.options.ExternalSettings.PostResolve.Exact = replace
args.options.AbsOutputFile = unix2win(args.options.AbsOutputFile)
args.options.AbsOutputBase = unix2win(args.options.AbsOutputBase)
args.options.AbsOutputDir = unix2win(args.options.AbsOutputDir)
args.options.TSConfigPath = unix2win(args.options.TSConfigPath)
}
s.__expectBundledImpl(t, args, fs.MockWindows)
}
func (s *suite) expectBundledUnix(t *testing.T, args bundled) {
t.Helper()
s.__expectBundledImpl(t, args, fs.MockUnix)
}
func (s *suite) expectBundledWindows(t *testing.T, args bundled) {
t.Helper()
s.__expectBundledImpl(t, args, fs.MockWindows)
}
// Don't call this directly. Call the helpers above instead.
func (s *suite) __expectBundledImpl(t *testing.T, args bundled, fsKind fs.MockKind) {
t.Helper()
testName := t.Name()
subName := "Unix"
if fsKind == fs.MockWindows {
subName = "Windows"
}
t.Run(subName, func(t *testing.T) {
t.Helper()
// Prepare the options
if args.options.ExtensionOrder == nil {
args.options.ExtensionOrder = []string{".tsx", ".ts", ".jsx", ".js", ".css", ".json"}
}
if args.options.AbsOutputFile != "" {
if fsKind == fs.MockWindows {
args.options.AbsOutputDir = unix2win(path.Dir(win2unix(args.options.AbsOutputFile)))
} else {
args.options.AbsOutputDir = path.Dir(args.options.AbsOutputFile)
}
}
if args.options.Mode == config.ModeBundle || (args.options.Mode == config.ModeConvertFormat && args.options.OutputFormat == config.FormatIIFE) {
// Apply this default to all tests since it was not configurable when the tests were written
args.options.TreeShaking = true
}
if args.options.Mode == config.ModeBundle && args.options.OutputFormat == config.FormatPreserve {
// The format can't be "preserve" while bundling
args.options.OutputFormat = config.FormatESModule
}
logKind := logger.DeferLogNoVerboseOrDebug
if args.debugLogs {
logKind = logger.DeferLogAll
}
entryPoints := make([]bundler.EntryPoint, 0, len(args.entryPaths)+len(args.entryPathsAdvanced))
for _, path := range args.entryPaths {
entryPoints = append(entryPoints, bundler.EntryPoint{InputPath: path})
}
entryPoints = append(entryPoints, args.entryPathsAdvanced...)
if args.absWorkingDir == "" {
if fsKind == fs.MockWindows {
args.absWorkingDir = "C:\\"
} else {
args.absWorkingDir = "/"
}
}
if args.options.AbsOutputDir == "" {
args.options.AbsOutputDir = args.absWorkingDir // Match the behavior of the API in this case
}
// Run the bundler
log := logger.NewDeferLog(logKind, nil)
caches := cache.MakeCacheSet()
mockFS := fs.MockFS(args.files, fsKind, args.absWorkingDir)
args.options.OmitRuntimeForTests = true
bundle := bundler.ScanBundle(config.BuildCall, log, mockFS, caches, entryPoints, args.options, nil)
msgs := log.Done()
assertLog(t, msgs, args.expectedScanLog)
// Stop now if there were any errors during the scan
if hasErrors(msgs) {
return
}
log = logger.NewDeferLog(logKind, nil)
results, metafileJSON := bundle.Compile(log, nil, nil, linker.Link)
msgs = log.Done()
assertLog(t, msgs, args.expectedCompileLog)
// Stop now if there were any errors during the compile
if hasErrors(msgs) {
return
}
// Don't include source maps in results since they are just noise. Source
// map validity is tested separately in a test that uses Mozilla's source
// map parsing library.
generated := ""
for _, result := range results {
if generated != "" {
generated += "\n"
}
if fsKind == fs.MockWindows {
result.AbsPath = win2unix(result.AbsPath)
}
generated += fmt.Sprintf("---------- %s ----------\n%s", result.AbsPath, string(result.Contents))
}
if metafileJSON != "" {
generated += fmt.Sprintf("---------- metafile.json ----------\n%s", metafileJSON)
}
s.compareSnapshot(t, testName, generated)
})
}
const snapshotsDir = "snapshots"
const snapshotSplitter = "\n================================================================================\n"
var globalTestMutex sync.Mutex
var globalSuites map[*suite]bool
var globalUpdateSnapshots bool
func (s *suite) compareSnapshot(t *testing.T, testName string, generated string) {
t.Helper()
// Initialize the test suite during the first test
s.mutex.Lock()
defer s.mutex.Unlock()
if s.path == "" {
s.path = snapshotsDir + "/snapshots_" + s.name + ".txt"
s.generatedSnapshots = make(map[string]string)
s.expectedSnapshots = make(map[string]string)
if contents, err := ioutil.ReadFile(s.path); err == nil {
// Replacing CRLF with LF is necessary to fix tests in GitHub actions,
// which for some reason check out the source code in CLRF mode
for _, part := range strings.Split(strings.ReplaceAll(string(contents), "\r\n", "\n"), snapshotSplitter) {
if newline := strings.IndexByte(part, '\n'); newline != -1 {
key := part[:newline]
value := part[newline+1:]
s.expectedSnapshots[key] = value
} else {
s.expectedSnapshots[part] = ""
}
}
}
globalTestMutex.Lock()
defer globalTestMutex.Unlock()
if globalSuites == nil {
globalSuites = make(map[*suite]bool)
}
globalSuites[s] = true
_, globalUpdateSnapshots = os.LookupEnv("UPDATE_SNAPSHOTS")
}
// Check against the stored snapshot if present
s.generatedSnapshots[testName] = generated
if !globalUpdateSnapshots {
if expected, ok := s.expectedSnapshots[testName]; ok {
test.AssertEqualWithDiff(t, generated, expected)
} else {
t.Fatalf("No snapshot saved for %s\n%s%s%s",
testName,
logger.TerminalColors.Green,
generated,
logger.TerminalColors.Reset,
)
}
}
}
func (s *suite) updateSnapshots() {
os.Mkdir(snapshotsDir, 0755)
keys := make([]string, 0, len(s.generatedSnapshots))
for key := range s.generatedSnapshots {
keys = append(keys, key)
}
sort.Strings(keys)
var contents strings.Builder
for i, key := range keys {
if i > 0 {
contents.WriteString(snapshotSplitter)
}
contents.WriteString(fmt.Sprintf("%s\n%s", key, s.generatedSnapshots[key]))
}
if err := ioutil.WriteFile(s.path, []byte(contents.String()), 0644); err != nil {
panic(err)
}
}
func (s *suite) validateSnapshots() bool {
isValid := true
for key := range s.expectedSnapshots {
if _, ok := s.generatedSnapshots[key]; !ok {
if isValid {
fmt.Printf("%s\n", s.path)
}
fmt.Printf(" No test found for snapshot %s\n", key)
isValid = false
}
}
return isValid
}
func TestMain(m *testing.M) {
code := m.Run()
if globalSuites != nil {
if globalUpdateSnapshots {
for s := range globalSuites {
s.updateSnapshots()
}
} else {
for s := range globalSuites {
if !s.validateSnapshots() {
code = 1
}
}
}
}
os.Exit(code)
}
func win2unix(p string) string {
if strings.HasPrefix(p, "C:\\") {
p = p[2:]
}
p = strings.ReplaceAll(p, "\\", "/")
return p
}
func unix2win(p string) string {
p = strings.ReplaceAll(p, "/", "\\")
if strings.HasPrefix(p, "\\") {
p = "C:" + p
}
return p
}