vendor tsgo
This commit is contained in:
90
tools/tsgo/internal/execute/tsctests/fs.go
Normal file
90
tools/tsgo/internal/execute/tsctests/fs.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package tsctests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/execute/incremental"
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/fsbaselineutil"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/harnessutil"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
)
|
||||
|
||||
type testFs struct {
|
||||
vfs.FS
|
||||
defaultLibs *collections.SyncSet[string]
|
||||
writtenFiles collections.SyncSet[string]
|
||||
}
|
||||
|
||||
func (f *testFs) removeIgnoreLibPath(path string) {
|
||||
if f.defaultLibs != nil && f.defaultLibs.Has(path) {
|
||||
f.defaultLibs.Delete(path)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadFile reads the file specified by path and returns the content.
|
||||
// If the file fails to be read, ok will be false.
|
||||
func (f *testFs) ReadFile(path string) (contents string, ok bool) {
|
||||
f.removeIgnoreLibPath(path)
|
||||
return f.readFileHandlingBuildInfo(path)
|
||||
}
|
||||
|
||||
func (f *testFs) readFileHandlingBuildInfo(path string) (contents string, ok bool) {
|
||||
contents, ok = f.FS.ReadFile(path)
|
||||
if ok && tspath.FileExtensionIs(path, tspath.ExtensionTsBuildInfo) {
|
||||
// read buildinfo and modify version
|
||||
var buildInfo incremental.BuildInfo
|
||||
err := json.Unmarshal([]byte(contents), &buildInfo)
|
||||
if err == nil && buildInfo.Version == harnessutil.FakeTSVersion {
|
||||
buildInfo.Version = core.Version()
|
||||
newContents, err := json.Marshal(&buildInfo)
|
||||
if err != nil {
|
||||
panic("testFs.ReadFile: failed to marshal build info after fixing version: " + err.Error())
|
||||
}
|
||||
contents = string(newContents)
|
||||
}
|
||||
}
|
||||
return contents, ok
|
||||
}
|
||||
|
||||
func (f *testFs) WriteFile(path string, data string) error {
|
||||
f.removeIgnoreLibPath(path)
|
||||
f.writtenFiles.Add(path)
|
||||
return f.writeFileHandlingBuildInfo(path, data)
|
||||
}
|
||||
|
||||
func (f *testFs) writeFileHandlingBuildInfo(path string, data string) error {
|
||||
if tspath.FileExtensionIs(path, tspath.ExtensionTsBuildInfo) {
|
||||
var buildInfo incremental.BuildInfo
|
||||
if err := json.Unmarshal([]byte(data), &buildInfo); err == nil {
|
||||
if buildInfo.Version == core.Version() {
|
||||
// Change it to harnessutil.FakeTSVersion
|
||||
buildInfo.Version = harnessutil.FakeTSVersion
|
||||
newData, err := json.Marshal(&buildInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("testFs.WriteFile: failed to marshal build info after fixing version: %w", err)
|
||||
}
|
||||
data = string(newData)
|
||||
}
|
||||
// Write readable build info version
|
||||
if err := f.WriteFile(
|
||||
path+".readable.baseline.txt",
|
||||
toReadableBuildInfo(&buildInfo, fsbaselineutil.SanitizeInternalSymbolName(data)),
|
||||
); err != nil {
|
||||
return fmt.Errorf("testFs.WriteFile: failed to write readable build info: %w", err)
|
||||
}
|
||||
} else {
|
||||
panic("testFs.WriteFile: failed to unmarshal build info: - use underlying FS's write method if this is intended use for testcase" + err.Error())
|
||||
}
|
||||
}
|
||||
return f.FS.WriteFile(path, data)
|
||||
}
|
||||
|
||||
// Removes `path` and all its contents. Will return the first error it encounters.
|
||||
func (f *testFs) Remove(path string) error {
|
||||
f.removeIgnoreLibPath(path)
|
||||
return f.FS.Remove(path)
|
||||
}
|
||||
216
tools/tsgo/internal/execute/tsctests/mock_watch_backend.go
Normal file
216
tools/tsgo/internal/execute/tsctests/mock_watch_backend.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package tsctests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/execute/watchmanager"
|
||||
"github.com/microsoft/typescript-go/internal/fswatch"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/fsbaselineutil"
|
||||
)
|
||||
|
||||
// MockWatchBackend implements watchmanager.WatchBackend for testing. It
|
||||
// records all WatchDirectory calls so tests can verify that
|
||||
// the correct watches are registered. Events can be delivered through
|
||||
// SendEvents, which routes them only through watches whose paths
|
||||
// match, enforcing that tests fail if the wrong watches are set up.
|
||||
type MockWatchBackend struct {
|
||||
mu sync.Mutex
|
||||
Dirs map[string]*MockWatch
|
||||
DirectoryExists func(string) bool // if set, WatchDirectory fails for non-existent dirs
|
||||
}
|
||||
|
||||
var _ watchmanager.WatchBackend = (*MockWatchBackend)(nil)
|
||||
|
||||
// NewMockWatchBackend creates a ready-to-use mock backend.
|
||||
func NewMockWatchBackend() *MockWatchBackend {
|
||||
return &MockWatchBackend{
|
||||
Dirs: make(map[string]*MockWatch),
|
||||
}
|
||||
}
|
||||
|
||||
// HasWatches reports whether any watches have been registered.
|
||||
func (m *MockWatchBackend) HasWatches() bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return len(m.Dirs) > 0
|
||||
}
|
||||
|
||||
// MockWatch records a single registered watch.
|
||||
type MockWatch struct {
|
||||
Path string
|
||||
Callback fswatch.WatchCallback
|
||||
Recursive bool
|
||||
Ignore func(string) bool
|
||||
Closed bool
|
||||
}
|
||||
|
||||
func (w *MockWatch) Close() error {
|
||||
w.Closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockWatchBackend) WatchDirectory(dir string, fn fswatch.WatchCallback, recursive bool, ignore func(string) bool) (io.Closer, error) {
|
||||
closers, err := m.WatchDirectories([]watchmanager.WatchDirectoryRequest{{
|
||||
Dir: dir,
|
||||
Callback: fn,
|
||||
Recursive: recursive,
|
||||
Ignore: ignore,
|
||||
}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return closers[0], nil
|
||||
}
|
||||
|
||||
func (m *MockWatchBackend) WatchDirectories(requests []watchmanager.WatchDirectoryRequest) ([]io.Closer, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for _, request := range requests {
|
||||
if m.DirectoryExists != nil && !m.DirectoryExists(request.Dir) {
|
||||
return nil, fmt.Errorf("directory does not exist: %s", request.Dir)
|
||||
}
|
||||
}
|
||||
closers := make([]io.Closer, len(requests))
|
||||
for i, request := range requests {
|
||||
w := &MockWatch{Path: request.Dir, Callback: request.Callback, Recursive: request.Recursive, Ignore: request.Ignore}
|
||||
m.Dirs[request.Dir] = w
|
||||
closers[i] = w
|
||||
}
|
||||
return closers, nil
|
||||
}
|
||||
|
||||
// SendEvents routes events through the registered watch callbacks
|
||||
// that match each event's path. Directory watches match if the event
|
||||
// path is a child (or recursive descendant) of the watched directory.
|
||||
// Events that match no watch are silently dropped — this is by design
|
||||
// so that tests fail when the production code doesn't register the
|
||||
// needed watches.
|
||||
func (m *MockWatchBackend) SendEvents(events []fswatch.Event) {
|
||||
// Snapshot callbacks under the lock, then invoke outside the lock
|
||||
// to avoid deadlock if the callback re-enters the mock.
|
||||
m.mu.Lock()
|
||||
type target struct {
|
||||
cb fswatch.WatchCallback
|
||||
events []fswatch.Event
|
||||
}
|
||||
targets := make(map[*MockWatch]*target)
|
||||
|
||||
for _, e := range events {
|
||||
// Check directory watches.
|
||||
for _, w := range m.Dirs {
|
||||
if w.Closed {
|
||||
continue
|
||||
}
|
||||
if w.Ignore != nil && w.Ignore(e.Path) {
|
||||
continue
|
||||
}
|
||||
if !pathIsUnder(e.Path, w.Path, w.Recursive) {
|
||||
continue
|
||||
}
|
||||
if t, ok := targets[w]; ok {
|
||||
t.events = append(t.events, e)
|
||||
} else {
|
||||
targets[w] = &target{cb: w.Callback, events: []fswatch.Event{e}}
|
||||
}
|
||||
}
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
for _, t := range targets {
|
||||
t.cb(t.events, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// SendChangedPaths converts a list of file changes into fswatch
|
||||
// events with appropriate event kinds and routes them through
|
||||
// registered watches via SendEvents. For new/modified files, it also
|
||||
// emits update events for their parent directories, simulating how
|
||||
// real filesystem watchers report directory events.
|
||||
func (m *MockWatchBackend) SendChangedPaths(changes []fsbaselineutil.FileChange) {
|
||||
events := make([]fswatch.Event, 0, len(changes)*2)
|
||||
seenDirs := make(map[string]struct{})
|
||||
for _, c := range changes {
|
||||
kind := fswatch.EventUpdate
|
||||
if c.Deleted {
|
||||
kind = fswatch.EventDelete
|
||||
}
|
||||
events = append(events, fswatch.Event{Kind: kind, Path: c.Path})
|
||||
// Emit update events for parent directories of changed files.
|
||||
// Real filesystem watchers deliver events to non-recursive watches
|
||||
// when a child directory is created, which the mock must replicate.
|
||||
dir := path.Dir(c.Path)
|
||||
for dir != "" && dir != "/" && dir != "." {
|
||||
if _, seen := seenDirs[dir]; seen {
|
||||
break
|
||||
}
|
||||
seenDirs[dir] = struct{}{}
|
||||
events = append(events, fswatch.Event{Kind: fswatch.EventUpdate, Path: dir})
|
||||
parent := path.Dir(dir)
|
||||
if parent == dir {
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
m.SendEvents(events)
|
||||
}
|
||||
|
||||
// pathIsUnder reports whether eventPath is inside dir. If recursive is
|
||||
// false, only direct children match.
|
||||
func pathIsUnder(eventPath, dir string, recursive bool) bool {
|
||||
if !strings.HasPrefix(eventPath, dir) {
|
||||
return false
|
||||
}
|
||||
rest := eventPath[len(dir):]
|
||||
if len(rest) == 0 {
|
||||
return false // exact match = the dir itself, not a child
|
||||
}
|
||||
if rest[0] != '/' {
|
||||
return false // e.g. dir="/foo", path="/foobar"
|
||||
}
|
||||
if !recursive {
|
||||
// Direct child only: no further '/' after the separator.
|
||||
return !strings.Contains(rest[1:], "/")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// WatchState returns a deterministic, human-readable summary of all
|
||||
// active watches. This is intended to be included in test baselines
|
||||
// so that watch registration correctness is verified via snapshot diffs.
|
||||
func (m *MockWatchBackend) WatchState() string {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("Watch Registrations::\n")
|
||||
|
||||
// Directory watches, sorted by path.
|
||||
var dirs []string
|
||||
for dir, w := range m.Dirs {
|
||||
if !w.Closed {
|
||||
dirs = append(dirs, dir)
|
||||
}
|
||||
}
|
||||
sort.Strings(dirs)
|
||||
|
||||
b.WriteString("Directory watches::\n")
|
||||
if len(dirs) == 0 {
|
||||
b.WriteString(" (none)\n")
|
||||
}
|
||||
for _, d := range dirs {
|
||||
w := m.Dirs[d]
|
||||
if w.Recursive {
|
||||
fmt.Fprintf(&b, " %s (recursive)\n", d)
|
||||
} else {
|
||||
fmt.Fprintf(&b, " %s\n", d)
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
427
tools/tsgo/internal/execute/tsctests/readablebuildinfo.go
Normal file
427
tools/tsgo/internal/execute/tsctests/readablebuildinfo.go
Normal file
@@ -0,0 +1,427 @@
|
||||
package tsctests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/diagnostics"
|
||||
"github.com/microsoft/typescript-go/internal/execute/incremental"
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
)
|
||||
|
||||
type readableBuildInfo struct {
|
||||
buildInfo *incremental.BuildInfo
|
||||
Version string `json:"version,omitzero"`
|
||||
|
||||
// Common between incremental and tsc -b buildinfo for non incremental programs
|
||||
Errors bool `json:"errors,omitzero"`
|
||||
CheckPending bool `json:"checkPending,omitzero"`
|
||||
Root []*readableBuildInfoRoot `json:"root,omitzero"`
|
||||
PackageJsons []string `json:"packageJsons,omitzero"`
|
||||
MissingPackageJsons []string `json:"missingPackageJsons,omitzero"`
|
||||
|
||||
// IncrementalProgram info
|
||||
FileNames []string `json:"fileNames,omitzero"`
|
||||
FileInfos []*readableBuildInfoFileInfo `json:"fileInfos,omitzero"`
|
||||
FileIdsList [][]string `json:"fileIdsList,omitzero"`
|
||||
Options *collections.OrderedMap[string, any] `json:"options,omitzero"`
|
||||
ReferencedMap *collections.OrderedMap[string, []string] `json:"referencedMap,omitzero"`
|
||||
SemanticDiagnosticsPerFile []*readableBuildInfoSemanticDiagnostic `json:"semanticDiagnosticsPerFile,omitzero"`
|
||||
EmitDiagnosticsPerFile []*readableBuildInfoDiagnosticsOfFile `json:"emitDiagnosticsPerFile,omitzero"`
|
||||
ChangeFileSet []string `json:"changeFileSet,omitzero"` // List of changed files in the program, not the whole set of files
|
||||
AffectedFilesPendingEmit []*readableBuildInfoFilePendingEmit `json:"affectedFilesPendingEmit,omitzero"`
|
||||
LatestChangedDtsFile string `json:"latestChangedDtsFile,omitzero"` // Because this is only output file in the program, we dont need fileId to deduplicate name
|
||||
EmitSignatures []*readableBuildInfoEmitSignature `json:"emitSignatures,omitzero"`
|
||||
ResolvedRoot []*readableBuildInfoResolvedRoot `json:"resolvedRoot,omitzero"`
|
||||
Size int `json:"size,omitzero"` // Size of the build info file
|
||||
|
||||
// NonIncrementalProgram info
|
||||
SemanticErrors bool `json:"semanticErrors,omitzero"`
|
||||
}
|
||||
|
||||
type readableBuildInfoRoot struct {
|
||||
Files []string `json:"files,omitzero"`
|
||||
Original *incremental.BuildInfoRoot `json:"original,omitzero"`
|
||||
}
|
||||
|
||||
type readableBuildInfoFileInfo struct {
|
||||
FileName string `json:"fileName,omitzero"`
|
||||
Version string `json:"version,omitzero"`
|
||||
Signature string `json:"signature,omitzero"`
|
||||
AffectsGlobalScope bool `json:"affectsGlobalScope,omitzero"`
|
||||
ImpliedNodeFormat string `json:"impliedNodeFormat,omitzero"`
|
||||
Original *incremental.BuildInfoFileInfo `json:"original,omitzero"` // Original file path, if available
|
||||
}
|
||||
|
||||
type readableBuildInfoDiagnostic struct {
|
||||
// incrementalBuildInfoFileId if it is for a File thats other than its stored for
|
||||
File string `json:"file,omitzero"`
|
||||
NoFile bool `json:"noFile,omitzero"`
|
||||
Pos int `json:"pos,omitzero"`
|
||||
End int `json:"end,omitzero"`
|
||||
Code int32 `json:"code,omitzero"`
|
||||
Category diagnostics.Category `json:"category,omitzero"`
|
||||
MessageKey diagnostics.Key `json:"messageKey,omitzero"`
|
||||
MessageArgs []string `json:"messageArgs,omitzero"`
|
||||
MessageChain []*readableBuildInfoDiagnostic `json:"messageChain,omitzero"`
|
||||
RelatedInformation []*readableBuildInfoDiagnostic `json:"relatedInformation,omitzero"`
|
||||
ReportsUnnecessary bool `json:"reportsUnnecessary,omitzero"`
|
||||
ReportsDeprecated bool `json:"reportsDeprecated,omitzero"`
|
||||
SkippedOnNoEmit bool `json:"skippedOnNoEmit,omitzero"`
|
||||
}
|
||||
|
||||
type readableBuildInfoDiagnosticsOfFile struct {
|
||||
file string
|
||||
diagnostics []*readableBuildInfoDiagnostic
|
||||
}
|
||||
|
||||
func (r *readableBuildInfoDiagnosticsOfFile) MarshalJSON() ([]byte, error) {
|
||||
fileIdAndDiagnostics := make([]any, 0, 2)
|
||||
fileIdAndDiagnostics = append(fileIdAndDiagnostics, r.file)
|
||||
fileIdAndDiagnostics = append(fileIdAndDiagnostics, r.diagnostics)
|
||||
return json.Marshal(fileIdAndDiagnostics)
|
||||
}
|
||||
|
||||
func (r *readableBuildInfoDiagnosticsOfFile) UnmarshalJSON(data []byte) error {
|
||||
var fileIdAndDiagnostics []any
|
||||
if err := json.Unmarshal(data, &fileIdAndDiagnostics); err != nil {
|
||||
return fmt.Errorf("invalid readableBuildInfoDiagnosticsOfFile: %s", data)
|
||||
}
|
||||
if len(fileIdAndDiagnostics) != 2 {
|
||||
return fmt.Errorf("invalid readableBuildInfoDiagnosticsOfFile: expected 2 elements, got %d", len(fileIdAndDiagnostics))
|
||||
}
|
||||
file, ok := fileIdAndDiagnostics[0].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid fileId in readableBuildInfoDiagnosticsOfFile: expected string, got %T", fileIdAndDiagnostics[0])
|
||||
}
|
||||
if diagnostics, ok := fileIdAndDiagnostics[1].([]*readableBuildInfoDiagnostic); !ok {
|
||||
return fmt.Errorf("invalid diagnostics in readableBuildInfoDiagnosticsOfFile: expected []*readableBuildInfoDiagnostic, got %T", fileIdAndDiagnostics[1])
|
||||
} else {
|
||||
*r = readableBuildInfoDiagnosticsOfFile{
|
||||
file: file,
|
||||
diagnostics: diagnostics,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type readableBuildInfoSemanticDiagnostic struct {
|
||||
file string // File is not in changedSet and still doesnt have cached diagnostics
|
||||
diagnostics *readableBuildInfoDiagnosticsOfFile // Diagnostics for file
|
||||
}
|
||||
|
||||
func (r *readableBuildInfoSemanticDiagnostic) MarshalJSON() ([]byte, error) {
|
||||
if r.file != "" {
|
||||
return json.Marshal(r.file)
|
||||
}
|
||||
return json.Marshal(r.diagnostics)
|
||||
}
|
||||
|
||||
func (r *readableBuildInfoSemanticDiagnostic) UnmarshalJSON(data []byte) error {
|
||||
var file string
|
||||
if err := json.Unmarshal(data, &file); err != nil {
|
||||
var diagnostics readableBuildInfoDiagnosticsOfFile
|
||||
if err := json.Unmarshal(data, &diagnostics); err != nil {
|
||||
return fmt.Errorf("invalid readableBuildInfoSemanticDiagnostic: %s", data)
|
||||
}
|
||||
*r = readableBuildInfoSemanticDiagnostic{
|
||||
diagnostics: &diagnostics,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
*r = readableBuildInfoSemanticDiagnostic{
|
||||
file: file,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type readableBuildInfoFilePendingEmit struct {
|
||||
file string
|
||||
emitKind string
|
||||
original *incremental.BuildInfoFilePendingEmit
|
||||
}
|
||||
|
||||
func (b *readableBuildInfoFilePendingEmit) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal([]any{b.file, b.emitKind, b.original})
|
||||
}
|
||||
|
||||
func (b *readableBuildInfoFilePendingEmit) UnmarshalJSON(data []byte) error {
|
||||
var fileIdAndEmitKind []any
|
||||
if err := json.Unmarshal(data, &fileIdAndEmitKind); err != nil {
|
||||
return fmt.Errorf("invalid readableBuildInfoFilePendingEmit: %s", data)
|
||||
}
|
||||
if len(fileIdAndEmitKind) != 3 {
|
||||
return fmt.Errorf("invalid readableBuildInfoFilePendingEmit: expected 3 elements, got %d", len(fileIdAndEmitKind))
|
||||
}
|
||||
file, ok := fileIdAndEmitKind[0].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid fileId in readableBuildInfoFilePendingEmit: expected string, got %T", fileIdAndEmitKind[0])
|
||||
}
|
||||
var emitKind string
|
||||
emitKind, ok = fileIdAndEmitKind[1].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid emitKind in readableBuildInfoFilePendingEmit: expected string, got %T", fileIdAndEmitKind[1])
|
||||
}
|
||||
var original *incremental.BuildInfoFilePendingEmit
|
||||
original, ok = fileIdAndEmitKind[2].(*incremental.BuildInfoFilePendingEmit)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid original in readableBuildInfoFilePendingEmit: expected *incremental.BuildInfoFilePendingEmit, got %T", fileIdAndEmitKind[2])
|
||||
}
|
||||
*b = readableBuildInfoFilePendingEmit{
|
||||
file: file,
|
||||
emitKind: emitKind,
|
||||
original: original,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type readableBuildInfoEmitSignature struct {
|
||||
File string `json:"file,omitzero"`
|
||||
Signature string `json:"signature,omitzero"`
|
||||
DiffersOnlyInDtsMap bool `json:"differsOnlyInDtsMap,omitzero"`
|
||||
DiffersInOptions bool `json:"differsInOptions,omitzero"`
|
||||
Original *incremental.BuildInfoEmitSignature `json:"original,omitzero"`
|
||||
}
|
||||
|
||||
type readableBuildInfoResolvedRoot struct {
|
||||
Resolved string
|
||||
Root string
|
||||
}
|
||||
|
||||
func (b *readableBuildInfoResolvedRoot) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal([2]string{b.Resolved, b.Root})
|
||||
}
|
||||
|
||||
func (b *readableBuildInfoResolvedRoot) UnmarshalJSON(data []byte) error {
|
||||
var resolvedAndRoot [2]string
|
||||
if err := json.Unmarshal(data, &resolvedAndRoot); err != nil {
|
||||
return fmt.Errorf("invalid BuildInfoResolvedRoot: %s", data)
|
||||
}
|
||||
*b = readableBuildInfoResolvedRoot{
|
||||
Resolved: resolvedAndRoot[0],
|
||||
Root: resolvedAndRoot[1],
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func toReadableBuildInfo(buildInfo *incremental.BuildInfo, buildInfoText string) string {
|
||||
readable := readableBuildInfo{
|
||||
buildInfo: buildInfo,
|
||||
Version: buildInfo.Version,
|
||||
Errors: buildInfo.Errors,
|
||||
CheckPending: buildInfo.CheckPending,
|
||||
FileNames: buildInfo.FileNames,
|
||||
Options: buildInfo.Options,
|
||||
LatestChangedDtsFile: buildInfo.LatestChangedDtsFile,
|
||||
SemanticErrors: buildInfo.SemanticErrors,
|
||||
PackageJsons: buildInfo.PackageJsons,
|
||||
MissingPackageJsons: buildInfo.MissingPackageJsons,
|
||||
Size: len(buildInfoText),
|
||||
}
|
||||
readable.setFileInfos()
|
||||
readable.setRoot()
|
||||
readable.setFileIdsList()
|
||||
readable.setReferencedMap()
|
||||
readable.setChangeFileSet()
|
||||
readable.setSemanticDiagnostics()
|
||||
readable.setEmitDiagnostics()
|
||||
readable.setAffectedFilesPendingEmit()
|
||||
readable.setEmitSignatures()
|
||||
readable.setResolvedRoot()
|
||||
contents, err := json.MarshalIndent(&readable, "", " ")
|
||||
if err != nil {
|
||||
panic("readableBuildInfo: failed to marshal readable build info: " + err.Error())
|
||||
}
|
||||
return string(contents)
|
||||
}
|
||||
|
||||
func (r *readableBuildInfo) toFilePath(fileId incremental.BuildInfoFileId) string {
|
||||
return r.buildInfo.FileNames[fileId-1]
|
||||
}
|
||||
|
||||
func (r *readableBuildInfo) toFilePathSet(fileIdListId incremental.BuildInfoFileIdListId) []string {
|
||||
return r.FileIdsList[fileIdListId-1]
|
||||
}
|
||||
|
||||
func (r *readableBuildInfo) toReadableBuildInfoDiagnostic(diagnostics []*incremental.BuildInfoDiagnostic) []*readableBuildInfoDiagnostic {
|
||||
return core.Map(diagnostics, func(d *incremental.BuildInfoDiagnostic) *readableBuildInfoDiagnostic {
|
||||
var file string
|
||||
if d.File != 0 {
|
||||
file = r.toFilePath(d.File)
|
||||
}
|
||||
return &readableBuildInfoDiagnostic{
|
||||
File: file,
|
||||
NoFile: d.NoFile,
|
||||
Pos: d.Pos,
|
||||
End: d.End,
|
||||
Code: d.Code,
|
||||
Category: d.Category,
|
||||
MessageKey: d.MessageKey,
|
||||
MessageArgs: d.MessageArgs,
|
||||
MessageChain: r.toReadableBuildInfoDiagnostic(d.MessageChain),
|
||||
RelatedInformation: r.toReadableBuildInfoDiagnostic(d.RelatedInformation),
|
||||
ReportsUnnecessary: d.ReportsUnnecessary,
|
||||
ReportsDeprecated: d.ReportsDeprecated,
|
||||
SkippedOnNoEmit: d.SkippedOnNoEmit,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (r *readableBuildInfo) toReadableBuildInfoDiagnosticsOfFile(diagnostics *incremental.BuildInfoDiagnosticsOfFile) *readableBuildInfoDiagnosticsOfFile {
|
||||
return &readableBuildInfoDiagnosticsOfFile{
|
||||
file: r.toFilePath(diagnostics.FileId),
|
||||
diagnostics: r.toReadableBuildInfoDiagnostic(diagnostics.Diagnostics),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *readableBuildInfo) setFileInfos() {
|
||||
r.FileInfos = core.MapIndex(r.buildInfo.FileInfos, func(original *incremental.BuildInfoFileInfo, index int) *readableBuildInfoFileInfo {
|
||||
fileInfo := original.GetFileInfo()
|
||||
// Dont set original for string encoding
|
||||
if original.HasSignature() {
|
||||
original = nil
|
||||
}
|
||||
return &readableBuildInfoFileInfo{
|
||||
FileName: r.toFilePath(incremental.BuildInfoFileId(index + 1)),
|
||||
Version: fileInfo.Version(),
|
||||
Signature: fileInfo.Signature(),
|
||||
AffectsGlobalScope: fileInfo.AffectsGlobalScope(),
|
||||
ImpliedNodeFormat: fileInfo.ImpliedNodeFormat().String(),
|
||||
Original: original,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (r *readableBuildInfo) setRoot() {
|
||||
r.Root = core.Map(r.buildInfo.Root, func(original *incremental.BuildInfoRoot) *readableBuildInfoRoot {
|
||||
var files []string
|
||||
if original.NonIncremental != "" {
|
||||
files = []string{original.NonIncremental}
|
||||
} else if original.End == 0 {
|
||||
files = []string{r.toFilePath(original.Start)}
|
||||
} else {
|
||||
files = make([]string, 0, original.End-original.Start+1)
|
||||
for i := original.Start; i <= original.End; i++ {
|
||||
files = append(files, r.toFilePath(i))
|
||||
}
|
||||
}
|
||||
return &readableBuildInfoRoot{
|
||||
Files: files,
|
||||
Original: original,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (r *readableBuildInfo) setFileIdsList() {
|
||||
r.FileIdsList = core.Map(r.buildInfo.FileIdsList, func(ids []incremental.BuildInfoFileId) []string {
|
||||
return core.Map(ids, r.toFilePath)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *readableBuildInfo) setReferencedMap() {
|
||||
if r.buildInfo.ReferencedMap != nil {
|
||||
r.ReferencedMap = &collections.OrderedMap[string, []string]{}
|
||||
for _, entry := range r.buildInfo.ReferencedMap {
|
||||
r.ReferencedMap.Set(r.toFilePath(entry.FileId), r.toFilePathSet(entry.FileIdListId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *readableBuildInfo) setChangeFileSet() {
|
||||
r.ChangeFileSet = core.Map(r.buildInfo.ChangeFileSet, r.toFilePath)
|
||||
}
|
||||
|
||||
func (r *readableBuildInfo) setSemanticDiagnostics() {
|
||||
r.SemanticDiagnosticsPerFile = core.Map(r.buildInfo.SemanticDiagnosticsPerFile, func(diagnostics *incremental.BuildInfoSemanticDiagnostic) *readableBuildInfoSemanticDiagnostic {
|
||||
if diagnostics.FileId != 0 {
|
||||
return &readableBuildInfoSemanticDiagnostic{
|
||||
file: r.toFilePath(diagnostics.FileId),
|
||||
}
|
||||
}
|
||||
return &readableBuildInfoSemanticDiagnostic{
|
||||
diagnostics: r.toReadableBuildInfoDiagnosticsOfFile(diagnostics.Diagnostics),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (r *readableBuildInfo) setEmitDiagnostics() {
|
||||
r.EmitDiagnosticsPerFile = core.Map(r.buildInfo.EmitDiagnosticsPerFile, r.toReadableBuildInfoDiagnosticsOfFile)
|
||||
}
|
||||
|
||||
func (r *readableBuildInfo) setAffectedFilesPendingEmit() {
|
||||
if r.buildInfo.AffectedFilesPendingEmit == nil {
|
||||
return
|
||||
}
|
||||
fullEmitKind := incremental.GetFileEmitKind(r.buildInfo.GetCompilerOptions(""))
|
||||
r.AffectedFilesPendingEmit = core.Map(r.buildInfo.AffectedFilesPendingEmit, func(pendingEmit *incremental.BuildInfoFilePendingEmit) *readableBuildInfoFilePendingEmit {
|
||||
emitKind := core.IfElse(pendingEmit.EmitKind == 0, fullEmitKind, pendingEmit.EmitKind)
|
||||
return &readableBuildInfoFilePendingEmit{
|
||||
file: r.toFilePath(pendingEmit.FileId),
|
||||
emitKind: toReadableFileEmitKind(emitKind),
|
||||
original: pendingEmit,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func toReadableFileEmitKind(fileEmitKind incremental.FileEmitKind) string {
|
||||
var builder strings.Builder
|
||||
addFlags := func(flags string) {
|
||||
if builder.Len() == 0 {
|
||||
builder.WriteString(flags)
|
||||
} else {
|
||||
builder.WriteString("|")
|
||||
builder.WriteString(flags)
|
||||
}
|
||||
}
|
||||
if fileEmitKind != 0 {
|
||||
if (fileEmitKind & incremental.FileEmitKindJs) != 0 {
|
||||
addFlags("Js")
|
||||
}
|
||||
if (fileEmitKind & incremental.FileEmitKindJsMap) != 0 {
|
||||
addFlags("JsMap")
|
||||
}
|
||||
if (fileEmitKind & incremental.FileEmitKindJsInlineMap) != 0 {
|
||||
addFlags("JsInlineMap")
|
||||
}
|
||||
if (fileEmitKind & incremental.FileEmitKindDts) == incremental.FileEmitKindDts {
|
||||
addFlags("Dts")
|
||||
} else {
|
||||
if (fileEmitKind & incremental.FileEmitKindDtsEmit) != 0 {
|
||||
addFlags("DtsEmit")
|
||||
}
|
||||
if (fileEmitKind & incremental.FileEmitKindDtsErrors) != 0 {
|
||||
addFlags("DtsErrors")
|
||||
}
|
||||
}
|
||||
if (fileEmitKind & incremental.FileEmitKindDtsMap) != 0 {
|
||||
addFlags("DtsMap")
|
||||
}
|
||||
}
|
||||
if builder.Len() != 0 {
|
||||
return builder.String()
|
||||
}
|
||||
return "None"
|
||||
}
|
||||
|
||||
func (r *readableBuildInfo) setEmitSignatures() {
|
||||
r.EmitSignatures = core.Map(r.buildInfo.EmitSignatures, func(signature *incremental.BuildInfoEmitSignature) *readableBuildInfoEmitSignature {
|
||||
return &readableBuildInfoEmitSignature{
|
||||
File: r.toFilePath(signature.FileId),
|
||||
Signature: signature.Signature,
|
||||
DiffersOnlyInDtsMap: signature.DiffersOnlyInDtsMap,
|
||||
DiffersInOptions: signature.DiffersInOptions,
|
||||
Original: signature,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (r *readableBuildInfo) setResolvedRoot() {
|
||||
r.ResolvedRoot = core.Map(r.buildInfo.ResolvedRoot, func(original *incremental.BuildInfoResolvedRoot) *readableBuildInfoResolvedRoot {
|
||||
return &readableBuildInfoResolvedRoot{
|
||||
Resolved: r.toFilePath(original.Resolved),
|
||||
Root: r.toFilePath(original.Root),
|
||||
}
|
||||
})
|
||||
}
|
||||
202
tools/tsgo/internal/execute/tsctests/runner.go
Normal file
202
tools/tsgo/internal/execute/tsctests/runner.go
Normal file
@@ -0,0 +1,202 @@
|
||||
package tsctests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/execute"
|
||||
"github.com/microsoft/typescript-go/internal/execute/tsc"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/baseline"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
type tscEdit struct {
|
||||
caption string
|
||||
commandLineArgs []string
|
||||
edit func(*TestSys)
|
||||
expectedDiff string
|
||||
}
|
||||
|
||||
var noChange = &tscEdit{
|
||||
caption: "no change",
|
||||
}
|
||||
|
||||
var noChangeOnlyEdit = []*tscEdit{
|
||||
noChange,
|
||||
}
|
||||
|
||||
type tscInput struct {
|
||||
subScenario string
|
||||
commandLineArgs []string
|
||||
files FileMap
|
||||
cwd string
|
||||
edits []*tscEdit
|
||||
env map[string]string
|
||||
ignoreCase bool
|
||||
windowsStyleRoot string
|
||||
}
|
||||
|
||||
func (test *tscInput) executeCommand(sys *TestSys, baselineBuilder *strings.Builder, commandLineArgs []string) tsc.CommandLineResult {
|
||||
fmt.Fprint(baselineBuilder, "tsgo ", strings.Join(commandLineArgs, " "), "\n")
|
||||
result := execute.CommandLine(context.Background(), sys, commandLineArgs, sys)
|
||||
switch result.Status {
|
||||
case tsc.ExitStatusSuccess:
|
||||
baselineBuilder.WriteString("ExitStatus:: Success")
|
||||
case tsc.ExitStatusDiagnosticsPresent_OutputsSkipped:
|
||||
baselineBuilder.WriteString("ExitStatus:: DiagnosticsPresent_OutputsSkipped")
|
||||
case tsc.ExitStatusDiagnosticsPresent_OutputsGenerated:
|
||||
baselineBuilder.WriteString("ExitStatus:: DiagnosticsPresent_OutputsGenerated")
|
||||
case tsc.ExitStatusInvalidProject_OutputsSkipped:
|
||||
baselineBuilder.WriteString("ExitStatus:: InvalidProject_OutputsSkipped")
|
||||
case tsc.ExitStatusProjectReferenceCycle_OutputsSkipped:
|
||||
baselineBuilder.WriteString("ExitStatus:: ProjectReferenceCycle_OutputsSkipped")
|
||||
case tsc.ExitStatusNotImplemented:
|
||||
baselineBuilder.WriteString("ExitStatus:: NotImplemented")
|
||||
default:
|
||||
panic(fmt.Sprintf("UnknownExitStatus %d", result.Status))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (test *tscInput) run(t *testing.T, scenario string) {
|
||||
t.Helper()
|
||||
t.Run(test.getBaselineSubFolder()+"/"+test.subScenario, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// initial test tsc compile
|
||||
baselineBuilder := &strings.Builder{}
|
||||
sys := newTestSys(test, false)
|
||||
fmt.Fprint(
|
||||
baselineBuilder,
|
||||
"currentDirectory::",
|
||||
sys.GetCurrentDirectory(),
|
||||
"\nuseCaseSensitiveFileNames::",
|
||||
sys.FS().UseCaseSensitiveFileNames(),
|
||||
"\nInput::\n",
|
||||
)
|
||||
sys.baselineFSwithDiff(baselineBuilder)
|
||||
result := test.executeCommand(sys, baselineBuilder, test.commandLineArgs)
|
||||
sys.serializeState(baselineBuilder)
|
||||
if result.Watcher != nil && sys.mockWatchBackend.HasWatches() {
|
||||
baselineBuilder.WriteString(sys.mockWatchBackend.WatchState())
|
||||
}
|
||||
var unexpectedDiff strings.Builder
|
||||
unexpectedDiff.WriteString(sys.baselinePrograms(baselineBuilder, "Initial build"))
|
||||
|
||||
for index, do := range test.edits {
|
||||
sys.clearOutput()
|
||||
wg := core.NewWorkGroup(false)
|
||||
var nonIncrementalSys *TestSys
|
||||
commandLineArgs := core.IfElse(do.commandLineArgs == nil, test.commandLineArgs, do.commandLineArgs)
|
||||
wg.Queue(func() {
|
||||
baselineBuilder.WriteString(fmt.Sprintf("\n\nEdit [%d]:: %s\n", index, do.caption))
|
||||
if do.edit != nil {
|
||||
do.edit(sys)
|
||||
}
|
||||
changedPaths := sys.fsDiffer.ChangedPaths()
|
||||
sys.baselineFSwithDiff(baselineBuilder)
|
||||
|
||||
if result.Watcher == nil {
|
||||
test.executeCommand(sys, baselineBuilder, commandLineArgs)
|
||||
} else {
|
||||
sys.mockWatchBackend.SendChangedPaths(changedPaths)
|
||||
result.Watcher.DoCycle()
|
||||
}
|
||||
sys.serializeState(baselineBuilder)
|
||||
if result.Watcher != nil && sys.mockWatchBackend.HasWatches() {
|
||||
baselineBuilder.WriteString(sys.mockWatchBackend.WatchState())
|
||||
}
|
||||
unexpectedDiff.WriteString(sys.baselinePrograms(baselineBuilder, fmt.Sprintf("Edit [%d]:: %s\n", index, do.caption)))
|
||||
})
|
||||
wg.Queue(func() {
|
||||
// Compute build with all the edits
|
||||
nonIncrementalSys = newTestSys(test, true)
|
||||
for i := range index + 1 {
|
||||
if test.edits[i].edit != nil {
|
||||
test.edits[i].edit(nonIncrementalSys)
|
||||
}
|
||||
}
|
||||
execute.CommandLine(context.Background(), nonIncrementalSys, commandLineArgs, nonIncrementalSys)
|
||||
})
|
||||
wg.RunAndWait()
|
||||
|
||||
diff := getDiffForIncremental(sys, nonIncrementalSys)
|
||||
if diff != "" {
|
||||
baselineBuilder.WriteString(fmt.Sprintf("\n\nDiff:: %s\n", core.IfElse(do.expectedDiff == "", "!!! Unexpected diff, please review and either fix or write explanation as expectedDiff !!!", do.expectedDiff)))
|
||||
baselineBuilder.WriteString(diff)
|
||||
if do.expectedDiff == "" {
|
||||
unexpectedDiff.WriteString(fmt.Sprintf("Edit [%d]:: %s\n!!! Unexpected diff, please review and either fix or write explanation as expectedDiff !!!\n%s\n", index, do.caption, diff))
|
||||
}
|
||||
} else if do.expectedDiff != "" {
|
||||
baselineBuilder.WriteString(fmt.Sprintf("\n\nDiff:: %s !!! Diff not found but explanation present, please review and remove the explanation !!!\n", do.expectedDiff))
|
||||
unexpectedDiff.WriteString(fmt.Sprintf("Edit [%d]:: %s\n!!! Diff not found but explanation present, please review and remove the explanation !!!\n", index, do.caption))
|
||||
}
|
||||
}
|
||||
baseline.Run(t, strings.ReplaceAll(test.subScenario, " ", "-")+".js", baselineBuilder.String(), baseline.Options{Subfolder: filepath.Join(test.getBaselineSubFolder(), scenario)})
|
||||
if unexpectedDiff.String() != "" {
|
||||
t.Errorf("Test %s has unexpected diff %s with incremental build, please review the baseline file", test.subScenario, unexpectedDiff.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func getDiffForIncremental(incrementalSys *TestSys, nonIncrementalSys *TestSys) string {
|
||||
var diffBuilder strings.Builder
|
||||
|
||||
nonIncrementalOutputs := nonIncrementalSys.fs.writtenFiles.ToSlice()
|
||||
slices.Sort(nonIncrementalOutputs)
|
||||
for _, nonIncrementalOutput := range nonIncrementalOutputs {
|
||||
if tspath.FileExtensionIs(nonIncrementalOutput, tspath.ExtensionTsBuildInfo) ||
|
||||
strings.HasSuffix(nonIncrementalOutput, ".readable.baseline.txt") {
|
||||
// Just check existence
|
||||
if !incrementalSys.fsFromFileMap().FileExists(nonIncrementalOutput) {
|
||||
diffBuilder.WriteString(baseline.DiffText("nonIncremental "+nonIncrementalOutput, "incremental "+nonIncrementalOutput, "Exists", ""))
|
||||
diffBuilder.WriteString("\n")
|
||||
}
|
||||
} else {
|
||||
nonIncrementalText, ok := nonIncrementalSys.fsFromFileMap().ReadFile(nonIncrementalOutput)
|
||||
if !ok {
|
||||
panic("Written file not found " + nonIncrementalOutput)
|
||||
}
|
||||
incrementalText, ok := incrementalSys.fsFromFileMap().ReadFile(nonIncrementalOutput)
|
||||
if !ok || incrementalText != nonIncrementalText {
|
||||
diffBuilder.WriteString(baseline.DiffText("nonIncremental "+nonIncrementalOutput, "incremental "+nonIncrementalOutput, nonIncrementalText, incrementalText))
|
||||
diffBuilder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
incrementalOutput := incrementalSys.getOutput(true)
|
||||
nonIncrementalOutput := nonIncrementalSys.getOutput(true)
|
||||
if incrementalOutput != nonIncrementalOutput {
|
||||
diffBuilder.WriteString(baseline.DiffText("nonIncremental.output.txt", "incremental.output.txt", nonIncrementalOutput, incrementalOutput))
|
||||
}
|
||||
return diffBuilder.String()
|
||||
}
|
||||
|
||||
func (test *tscInput) getBaselineSubFolder() string {
|
||||
commandName := "tsc"
|
||||
if slices.ContainsFunc(test.commandLineArgs, func(arg string) bool {
|
||||
switch arg {
|
||||
case "-b", "--b", "-build", "--build":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}) {
|
||||
commandName = "tsbuild"
|
||||
}
|
||||
w := ""
|
||||
if slices.ContainsFunc(test.commandLineArgs, func(arg string) bool {
|
||||
switch arg {
|
||||
case "-w", "--w", "-watch", "--watch":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}) {
|
||||
w = "Watch"
|
||||
}
|
||||
return commandName + w
|
||||
}
|
||||
219
tools/tsgo/internal/execute/tsctests/showconfig_test.go
Normal file
219
tools/tsgo/internal/execute/tsctests/showconfig_test.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package tsctests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/testutil/stringtestutil"
|
||||
)
|
||||
|
||||
func TestShowConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []*tscInput{
|
||||
{
|
||||
subScenario: "Default initialized TSConfig",
|
||||
commandLineArgs: []string{"--showConfig"},
|
||||
},
|
||||
{
|
||||
subScenario: "Show TSConfig with files options",
|
||||
commandLineArgs: []string{"--showConfig", "file0.ts", "file1.ts", "file2.ts"},
|
||||
},
|
||||
{
|
||||
subScenario: "Show TSConfig with boolean value compiler options",
|
||||
commandLineArgs: []string{"--showConfig", "--noUnusedLocals"},
|
||||
},
|
||||
{
|
||||
subScenario: "Show TSConfig with enum value compiler options",
|
||||
commandLineArgs: []string{"--showConfig", "--target", "es5", "--jsx", "react"},
|
||||
},
|
||||
{
|
||||
subScenario: "Show TSConfig with list compiler options",
|
||||
commandLineArgs: []string{"--showConfig", "--types", "jquery,mocha"},
|
||||
},
|
||||
{
|
||||
subScenario: "Show TSConfig with list compiler options with enum value",
|
||||
commandLineArgs: []string{"--showConfig", "--lib", "es5,es2015.core"},
|
||||
},
|
||||
{
|
||||
subScenario: "Show TSConfig with incorrect compiler option",
|
||||
commandLineArgs: []string{"--showConfig", "--someNonExistOption"},
|
||||
},
|
||||
{
|
||||
subScenario: "Show TSConfig with incorrect compiler option value",
|
||||
commandLineArgs: []string{"--showConfig", "--lib", "nonExistLib,es5,es2015.promise"},
|
||||
},
|
||||
{
|
||||
subScenario: "Show TSConfig with advanced options",
|
||||
commandLineArgs: []string{"--showConfig", "--declaration", "--declarationDir", "lib", "--skipLibCheck", "--noErrorTruncation"},
|
||||
},
|
||||
{
|
||||
subScenario: "Show TSConfig with compileOnSave and more",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/src/index.ts": `export const a = 1;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"strict": true
|
||||
},
|
||||
"compileOnSave": true,
|
||||
"exclude": [
|
||||
"dist"
|
||||
],
|
||||
"files": [],
|
||||
"include": [
|
||||
"src/*"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "./test" }
|
||||
]
|
||||
}`),
|
||||
},
|
||||
commandLineArgs: []string{"-p", "tsconfig.json", "--showConfig"},
|
||||
},
|
||||
{
|
||||
subScenario: "Show TSConfig with paths and more",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/src/index.ts": `export const a = 1;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"outDir": "./lib",
|
||||
"esModuleInterop": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"target": "ES2017",
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@root/*": ["./*"],
|
||||
"@configs/*": ["src/configs/*"],
|
||||
"@common/*": ["src/common/*"],
|
||||
"*": [
|
||||
"node_modules/*",
|
||||
"src/types/*"
|
||||
]
|
||||
},
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
]
|
||||
}`),
|
||||
},
|
||||
commandLineArgs: []string{"-p", "tsconfig.json", "--showConfig"},
|
||||
},
|
||||
{
|
||||
subScenario: "Show TSConfig with include filtering files",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/src/main.ts": `export const a = 1;`,
|
||||
"/home/src/workspaces/project/src/util.ts": `export const b = 2;`,
|
||||
"/home/src/workspaces/project/extra.ts": `export const c = 3;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}`),
|
||||
},
|
||||
commandLineArgs: []string{"-p", "tsconfig.json", "--showConfig"},
|
||||
},
|
||||
{
|
||||
subScenario: "Show TSConfig with references",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/src/index.ts": `export const a = 1;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"strict": true
|
||||
},
|
||||
"references": [
|
||||
{ "path": "./packages/a" },
|
||||
{ "path": "./packages/b" }
|
||||
]
|
||||
}`),
|
||||
},
|
||||
commandLineArgs: []string{"-p", "tsconfig.json", "--showConfig"},
|
||||
},
|
||||
{
|
||||
subScenario: "Show TSConfig with exclude",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/src/index.ts": `export const a = 1;`,
|
||||
"/home/src/workspaces/project/test/test1.ts": `import { a } from "../src";`,
|
||||
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
},
|
||||
"exclude": [
|
||||
"test"
|
||||
]
|
||||
}`),
|
||||
},
|
||||
commandLineArgs: []string{"-p", "tsconfig.json", "--showConfig"},
|
||||
},
|
||||
{
|
||||
subScenario: "Show TSConfig with files and include",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/src/main.ts": `export const a = 1;`,
|
||||
"/home/src/workspaces/project/extra.ts": `export const c = 3;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
},
|
||||
"files": [
|
||||
"extra.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}`),
|
||||
},
|
||||
commandLineArgs: []string{"-p", "tsconfig.json", "--showConfig"},
|
||||
},
|
||||
{
|
||||
subScenario: "Show TSConfig with transitively implied options",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/src/index.ts": `export const a = 1;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "nodenext"
|
||||
}
|
||||
}`),
|
||||
},
|
||||
commandLineArgs: []string{"-p", "tsconfig.json", "--showConfig"},
|
||||
},
|
||||
{
|
||||
subScenario: "Show TSConfig with exclude and outDir",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/src/index.ts": `export const a = 1;`,
|
||||
"/home/src/workspaces/project/src/bin/tool.ts": `export const b = 2;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"outDir": "./build"
|
||||
},
|
||||
"exclude": [
|
||||
"build"
|
||||
]
|
||||
}`),
|
||||
},
|
||||
commandLineArgs: []string{"-p", "tsconfig.json", "--showConfig"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test.run(t, "showConfig")
|
||||
}
|
||||
}
|
||||
592
tools/tsgo/internal/execute/tsctests/sys.go
Normal file
592
tools/tsgo/internal/execute/tsctests/sys.go
Normal file
@@ -0,0 +1,592 @@
|
||||
package tsctests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/diagnostics"
|
||||
"github.com/microsoft/typescript-go/internal/execute"
|
||||
"github.com/microsoft/typescript-go/internal/execute/incremental"
|
||||
"github.com/microsoft/typescript-go/internal/execute/tsc"
|
||||
"github.com/microsoft/typescript-go/internal/execute/watchmanager"
|
||||
"github.com/microsoft/typescript-go/internal/locale"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/fsbaselineutil"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/harnessutil"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/stringtestutil"
|
||||
"github.com/microsoft/typescript-go/internal/tsoptions"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/iovfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type FileMap map[string]any
|
||||
|
||||
var tscLibPath = "/home/src/tslibs/TS/Lib"
|
||||
|
||||
var tscDefaultLibContent = stringtestutil.Dedent(`
|
||||
/// <reference no-default-lib="true"/>
|
||||
interface Boolean {}
|
||||
interface Function {}
|
||||
interface CallableFunction {}
|
||||
interface NewableFunction {}
|
||||
interface IArguments {}
|
||||
interface Number { toExponential: any; }
|
||||
interface Object {}
|
||||
interface RegExp {}
|
||||
interface String { charAt: any; }
|
||||
interface Array<T> { length: number; [n: number]: T; }
|
||||
interface ReadonlyArray<T> {}
|
||||
interface SymbolConstructor {
|
||||
(desc?: string | number): symbol;
|
||||
for(name: string): symbol;
|
||||
readonly toStringTag: symbol;
|
||||
}
|
||||
declare var Symbol: SymbolConstructor;
|
||||
interface Symbol {
|
||||
readonly [Symbol.toStringTag]: string;
|
||||
}
|
||||
declare const console: { log(msg: any): void; };
|
||||
`)
|
||||
|
||||
func getTestLibPathFor(libName string) string {
|
||||
var libFile string
|
||||
if value, ok := tsoptions.LibMap.Get(libName); ok {
|
||||
libFile = value.(string)
|
||||
} else {
|
||||
libFile = "lib." + libName + ".d.ts"
|
||||
}
|
||||
return tscLibPath + "/" + libFile
|
||||
}
|
||||
|
||||
type TestClock struct {
|
||||
start time.Time
|
||||
now time.Time
|
||||
nowMu sync.Mutex
|
||||
}
|
||||
|
||||
func (t *TestClock) Now() time.Time {
|
||||
t.nowMu.Lock()
|
||||
defer t.nowMu.Unlock()
|
||||
if t.now.IsZero() {
|
||||
t.now = t.start
|
||||
}
|
||||
t.now = t.now.Add(1 * time.Second) // Simulate some time passing
|
||||
return t.now
|
||||
}
|
||||
|
||||
func (t *TestClock) SinceStart() time.Duration {
|
||||
return t.Now().Sub(t.start)
|
||||
}
|
||||
|
||||
func NewTscSystem(files FileMap, useCaseSensitiveFileNames bool, cwd string) *TestSys {
|
||||
clock := &TestClock{start: time.Now()}
|
||||
return &TestSys{
|
||||
fs: &testFs{
|
||||
FS: vfstest.FromMapWithClock(files, useCaseSensitiveFileNames, clock),
|
||||
},
|
||||
cwd: cwd,
|
||||
clock: clock,
|
||||
}
|
||||
}
|
||||
|
||||
func GetFileMapWithBuild(files FileMap, commandLineArgs []string) FileMap {
|
||||
sys := newTestSys(&tscInput{
|
||||
files: maps.Clone(files),
|
||||
}, false)
|
||||
execute.CommandLine(context.Background(), sys, commandLineArgs, sys)
|
||||
sys.fs.writtenFiles.Range(func(key string) bool {
|
||||
if text, ok := sys.fsFromFileMap().ReadFile(key); ok {
|
||||
files[key] = text
|
||||
}
|
||||
return true
|
||||
})
|
||||
return files
|
||||
}
|
||||
|
||||
func newTestSys(tscInput *tscInput, forIncrementalCorrectness bool) *TestSys {
|
||||
cwd := tscInput.cwd
|
||||
if cwd == "" {
|
||||
cwd = "/home/src/workspaces/project"
|
||||
}
|
||||
libPath := tscLibPath
|
||||
if tscInput.windowsStyleRoot != "" {
|
||||
libPath = tscInput.windowsStyleRoot + libPath[1:]
|
||||
}
|
||||
currentWrite := &strings.Builder{}
|
||||
sys := NewTscSystem(tscInput.files, !tscInput.ignoreCase, cwd)
|
||||
sys.defaultLibraryPath = libPath
|
||||
sys.currentWrite = currentWrite
|
||||
sys.tracer = harnessutil.NewTracerForBaselining(tspath.ComparePathsOptions{
|
||||
UseCaseSensitiveFileNames: !tscInput.ignoreCase,
|
||||
CurrentDirectory: cwd,
|
||||
}, currentWrite)
|
||||
sys.env = tscInput.env
|
||||
sys.forIncrementalCorrectness = forIncrementalCorrectness
|
||||
sys.mockWatchBackend = NewMockWatchBackend()
|
||||
sys.mockWatchBackend.DirectoryExists = sys.fs.FS.DirectoryExists
|
||||
sys.fsDiffer = &fsbaselineutil.FSDiffer{
|
||||
FS: sys.fs.FS.(iovfs.FsWithSys),
|
||||
DefaultLibs: func() *collections.SyncSet[string] { return sys.fs.defaultLibs },
|
||||
WrittenFiles: &sys.fs.writtenFiles,
|
||||
}
|
||||
|
||||
// Ensure the default library file is present
|
||||
sys.ensureLibPathExists("lib.d.ts")
|
||||
for _, libFile := range tsoptions.TargetToLibMap() {
|
||||
sys.ensureLibPathExists(libFile)
|
||||
}
|
||||
for libFile := range tsoptions.LibFilesSet.Keys() {
|
||||
sys.ensureLibPathExists(libFile)
|
||||
}
|
||||
return sys
|
||||
}
|
||||
|
||||
type TestSys struct {
|
||||
currentWrite *strings.Builder
|
||||
programBaselines strings.Builder
|
||||
programIncludeBaselines strings.Builder
|
||||
tracer *harnessutil.TracerForBaselining
|
||||
fsDiffer *fsbaselineutil.FSDiffer
|
||||
forIncrementalCorrectness bool
|
||||
mockWatchBackend *MockWatchBackend
|
||||
|
||||
fs *testFs
|
||||
defaultLibraryPath string
|
||||
cwd string
|
||||
env map[string]string
|
||||
clock *TestClock
|
||||
}
|
||||
|
||||
var (
|
||||
_ tsc.System = (*TestSys)(nil)
|
||||
_ tsc.CommandLineTesting = (*TestSys)(nil)
|
||||
)
|
||||
|
||||
func (s *TestSys) Now() time.Time {
|
||||
return s.clock.Now()
|
||||
}
|
||||
|
||||
func (s *TestSys) SinceStart() time.Duration {
|
||||
return s.clock.SinceStart()
|
||||
}
|
||||
|
||||
func (s *TestSys) FS() vfs.FS {
|
||||
return s.fs
|
||||
}
|
||||
|
||||
func (s *TestSys) fsFromFileMap() iovfs.FsWithSys {
|
||||
return s.fsDiffer.FS
|
||||
}
|
||||
|
||||
func (s *TestSys) mapFs() *vfstest.MapFS {
|
||||
return s.fsDiffer.MapFs()
|
||||
}
|
||||
|
||||
func (s *TestSys) ensureLibPathExists(path string) {
|
||||
path = s.defaultLibraryPath + "/" + path
|
||||
if _, ok := s.fsFromFileMap().ReadFile(path); !ok {
|
||||
if s.fs.defaultLibs == nil {
|
||||
s.fs.defaultLibs = &collections.SyncSet[string]{}
|
||||
}
|
||||
s.fs.defaultLibs.Add(path)
|
||||
err := s.fsFromFileMap().WriteFile(path, tscDefaultLibContent)
|
||||
if err != nil {
|
||||
panic("Failed to write default library file: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestSys) DefaultLibraryPath() string {
|
||||
return s.defaultLibraryPath
|
||||
}
|
||||
|
||||
func (s *TestSys) GetCurrentDirectory() string {
|
||||
return s.cwd
|
||||
}
|
||||
|
||||
func (s *TestSys) Writer() io.Writer {
|
||||
return s.currentWrite
|
||||
}
|
||||
|
||||
func (s *TestSys) WriteOutputIsTTY() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *TestSys) GetWidthOfTerminal() int {
|
||||
if widthStr := s.GetEnvironmentVariable("TS_TEST_TERMINAL_WIDTH"); widthStr != "" {
|
||||
return core.Must(strconv.Atoi(widthStr))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *TestSys) GetEnvironmentVariable(name string) string {
|
||||
return s.env[name]
|
||||
}
|
||||
|
||||
func (s *TestSys) OnEmittedFiles(result *compiler.EmitResult, mTimesCache *collections.SyncMap[tspath.Path, time.Time]) {
|
||||
if result != nil {
|
||||
for _, file := range result.EmittedFiles {
|
||||
modTime := s.mapFs().GetModTime(file)
|
||||
if serializedDiff := s.fsDiffer.SerializedDiff(); serializedDiff != nil {
|
||||
if diff, ok := serializedDiff.Snap[file]; ok && diff.MTime.Equal(modTime) {
|
||||
// Even though written, timestamp was reverted
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the timestamp for emitted files is in the order
|
||||
now := s.Now()
|
||||
if err := s.fsFromFileMap().Chtimes(file, time.Time{}, now); err != nil {
|
||||
panic("Failed to change time for emitted file: " + file + ": " + err.Error())
|
||||
}
|
||||
// Update the mTime cache in --b mode to store the updated timestamp so tests will behave deteministically when finding newest output
|
||||
if mTimesCache != nil {
|
||||
path := tspath.ToPath(file, s.GetCurrentDirectory(), s.FS().UseCaseSensitiveFileNames())
|
||||
if _, found := mTimesCache.Load(path); found {
|
||||
mTimesCache.Store(path, now)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestSys) OnListFilesStart(w io.Writer) {
|
||||
fmt.Fprintln(w, listFileStart)
|
||||
}
|
||||
|
||||
func (s *TestSys) OnListFilesEnd(w io.Writer) {
|
||||
fmt.Fprintln(w, listFileEnd)
|
||||
}
|
||||
|
||||
func (s *TestSys) OnStatisticsStart(w io.Writer) {
|
||||
fmt.Fprintln(w, statisticsStart)
|
||||
}
|
||||
|
||||
func (s *TestSys) OnStatisticsEnd(w io.Writer) {
|
||||
fmt.Fprintln(w, statisticsEnd)
|
||||
}
|
||||
|
||||
func (s *TestSys) OnBuildStatusReportStart(w io.Writer) {
|
||||
fmt.Fprintln(w, buildStatusReportStart)
|
||||
}
|
||||
|
||||
func (s *TestSys) OnBuildStatusReportEnd(w io.Writer) {
|
||||
fmt.Fprintln(w, buildStatusReportEnd)
|
||||
}
|
||||
|
||||
func (s *TestSys) OnWatchStatusReportStart() {
|
||||
fmt.Fprintln(s.Writer(), watchStatusReportStart)
|
||||
}
|
||||
|
||||
func (s *TestSys) OnWatchStatusReportEnd() {
|
||||
fmt.Fprintln(s.Writer(), watchStatusReportEnd)
|
||||
}
|
||||
|
||||
func (s *TestSys) GetTrace(w io.Writer, locale locale.Locale) func(msg *diagnostics.Message, args ...any) {
|
||||
return func(msg *diagnostics.Message, args ...any) {
|
||||
fmt.Fprintln(w, traceStart)
|
||||
defer fmt.Fprintln(w, traceEnd)
|
||||
// With tsc -b building projects in parallel we cannot serialize the package.json lookup trace
|
||||
// so trace as if it wasnt cached
|
||||
str := msg.Localize(locale, args...)
|
||||
s.tracer.TraceWithWriter(w, str, w == s.Writer())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestSys) writeHeaderToBaseline(builder *strings.Builder, program *incremental.Program) {
|
||||
if builder.Len() != 0 {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
if configFilePath := program.Options().ConfigFilePath; configFilePath != "" {
|
||||
builder.WriteString(tspath.GetRelativePathFromDirectory(s.cwd, configFilePath, tspath.ComparePathsOptions{
|
||||
UseCaseSensitiveFileNames: s.FS().UseCaseSensitiveFileNames(),
|
||||
CurrentDirectory: s.GetCurrentDirectory(),
|
||||
}))
|
||||
builder.WriteString("::\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestSys) WatchBackend() watchmanager.WatchBackend {
|
||||
return s.mockWatchBackend
|
||||
}
|
||||
|
||||
func (s *TestSys) OnProgram(program *incremental.Program) {
|
||||
s.writeHeaderToBaseline(&s.programBaselines, program)
|
||||
|
||||
testingData := program.GetTestingData()
|
||||
s.programBaselines.WriteString("SemanticDiagnostics::\n")
|
||||
for _, file := range program.GetProgram().GetSourceFiles() {
|
||||
if diagnostics, ok := testingData.SemanticDiagnosticsPerFile.Load(file.Path()); ok {
|
||||
if oldDiagnostics, ok := testingData.OldProgramSemanticDiagnosticsPerFile.Load(file.Path()); !ok || oldDiagnostics != diagnostics {
|
||||
s.programBaselines.WriteString("*refresh* ")
|
||||
s.programBaselines.WriteString(file.FileName())
|
||||
s.programBaselines.WriteString("\n")
|
||||
}
|
||||
} else {
|
||||
s.programBaselines.WriteString("*not cached* ")
|
||||
s.programBaselines.WriteString(file.FileName())
|
||||
s.programBaselines.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Write signature updates
|
||||
s.programBaselines.WriteString("Signatures::\n")
|
||||
for _, file := range program.GetProgram().GetSourceFiles() {
|
||||
if kind, ok := testingData.UpdatedSignatureKinds[file.Path()]; ok {
|
||||
switch kind {
|
||||
case incremental.SignatureUpdateKindComputedDts:
|
||||
s.programBaselines.WriteString("(computed .d.ts) ")
|
||||
s.programBaselines.WriteString(file.FileName())
|
||||
s.programBaselines.WriteString("\n")
|
||||
case incremental.SignatureUpdateKindStoredAtEmit:
|
||||
s.programBaselines.WriteString("(stored at emit) ")
|
||||
s.programBaselines.WriteString(file.FileName())
|
||||
s.programBaselines.WriteString("\n")
|
||||
case incremental.SignatureUpdateKindUsedVersion:
|
||||
s.programBaselines.WriteString("(used version) ")
|
||||
s.programBaselines.WriteString(file.FileName())
|
||||
s.programBaselines.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var filesWithoutIncludeReason []string
|
||||
var fileNotInProgramWithIncludeReason []string
|
||||
includeReasons := program.GetProgram().GetIncludeReasons()
|
||||
for _, file := range program.GetProgram().GetSourceFiles() {
|
||||
if _, ok := includeReasons[file.Path()]; !ok {
|
||||
filesWithoutIncludeReason = append(filesWithoutIncludeReason, string(file.Path()))
|
||||
}
|
||||
}
|
||||
for path := range includeReasons {
|
||||
if program.GetProgram().GetSourceFileByPath(path) == nil && !program.GetProgram().IsMissingPath(path) {
|
||||
fileNotInProgramWithIncludeReason = append(fileNotInProgramWithIncludeReason, string(path))
|
||||
}
|
||||
}
|
||||
if len(filesWithoutIncludeReason) > 0 || len(fileNotInProgramWithIncludeReason) > 0 {
|
||||
s.writeHeaderToBaseline(&s.programIncludeBaselines, program)
|
||||
s.programIncludeBaselines.WriteString("!!! Expected all files to have include reasons\nfilesWithoutIncludeReason::\n")
|
||||
for _, file := range filesWithoutIncludeReason {
|
||||
s.programIncludeBaselines.WriteString(" ")
|
||||
s.programIncludeBaselines.WriteString(file)
|
||||
s.programIncludeBaselines.WriteString("\n")
|
||||
}
|
||||
s.programIncludeBaselines.WriteString("filesNotInProgramWithIncludeReason::\n")
|
||||
for _, file := range fileNotInProgramWithIncludeReason {
|
||||
s.programIncludeBaselines.WriteString(" ")
|
||||
s.programIncludeBaselines.WriteString(file)
|
||||
s.programIncludeBaselines.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestSys) baselinePrograms(baseline *strings.Builder, header string) string {
|
||||
baseline.WriteString(s.programBaselines.String())
|
||||
s.programBaselines.Reset()
|
||||
var result string
|
||||
if s.programIncludeBaselines.Len() > 0 {
|
||||
result += fmt.Sprintf("\n\n%s\n!!! Include reasons expectations don't match pls review!!!\n", header)
|
||||
result += s.programIncludeBaselines.String()
|
||||
s.programIncludeBaselines.Reset()
|
||||
baseline.WriteString(result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *TestSys) serializeState(baseline *strings.Builder) {
|
||||
s.baselineOutput(baseline)
|
||||
s.baselineFSwithDiff(baseline)
|
||||
// todo watch
|
||||
// this.serializeWatches(baseline);
|
||||
// this.timeoutCallbacks.serialize(baseline);
|
||||
// this.immediateCallbacks.serialize(baseline);
|
||||
// this.pendingInstalls.serialize(baseline);
|
||||
// this.service?.baseline();
|
||||
}
|
||||
|
||||
var (
|
||||
fakeTimeStamp = "HH:MM:SS AM"
|
||||
fakeDuration = "d.ddds"
|
||||
|
||||
buildStartingAt = "build starting at "
|
||||
buildFinishedIn = "build finished in "
|
||||
listFileStart = "!!! List files start"
|
||||
listFileEnd = "!!! List files end"
|
||||
statisticsStart = "!!! Statistics start"
|
||||
statisticsEnd = "!!! Statistics end"
|
||||
buildStatusReportStart = "!!! Build Status Report Start"
|
||||
buildStatusReportEnd = "!!! Build Status Report End"
|
||||
watchStatusReportStart = "!!! Watch Status Report Start"
|
||||
watchStatusReportEnd = "!!! Watch Status Report End"
|
||||
traceStart = "!!! Trace start"
|
||||
traceEnd = "!!! Trace end"
|
||||
)
|
||||
|
||||
func (s *TestSys) baselineOutput(baseline io.Writer) {
|
||||
fmt.Fprint(baseline, "\nOutput::\n")
|
||||
output := s.getOutput(false)
|
||||
fmt.Fprint(baseline, output)
|
||||
}
|
||||
|
||||
type outputSanitizer struct {
|
||||
forComparing bool
|
||||
lines []string
|
||||
index int
|
||||
outputLines []string
|
||||
}
|
||||
|
||||
var (
|
||||
englishVersion = diagnostics.Version_0.Localize(locale.Default, core.Version())
|
||||
fakeEnglishVersion = diagnostics.Version_0.Localize(locale.Default, harnessutil.FakeTSVersion)
|
||||
czech = locale.Locale(language.MustParse("cs"))
|
||||
czechVersion = diagnostics.Version_0.Localize(czech, core.Version())
|
||||
fakeCzechVersion = diagnostics.Version_0.Localize(czech, harnessutil.FakeTSVersion)
|
||||
)
|
||||
|
||||
func (o *outputSanitizer) addOutputLine(s string) {
|
||||
s = strings.ReplaceAll(s, fmt.Sprintf("'%s'", core.Version()), fmt.Sprintf("'%s'", harnessutil.FakeTSVersion))
|
||||
s = strings.ReplaceAll(s, englishVersion, fakeEnglishVersion)
|
||||
s = strings.ReplaceAll(s, czechVersion, fakeCzechVersion)
|
||||
s = fsbaselineutil.SanitizeInternalSymbolName(s)
|
||||
o.outputLines = append(o.outputLines, s)
|
||||
}
|
||||
|
||||
func (o *outputSanitizer) sanitizeBuildStatusTimeStamp() string {
|
||||
statusLine := o.lines[o.index]
|
||||
hhSeparator := strings.IndexRune(statusLine, ':')
|
||||
if hhSeparator < 2 {
|
||||
panic("Expected timestamp")
|
||||
}
|
||||
return statusLine[:hhSeparator-2] + fakeTimeStamp + statusLine[hhSeparator+len(fakeTimeStamp)-2:]
|
||||
}
|
||||
|
||||
func (o *outputSanitizer) transformLines() string {
|
||||
for ; o.index < len(o.lines); o.index++ {
|
||||
line := o.lines[o.index]
|
||||
if strings.HasPrefix(line, buildStartingAt) {
|
||||
if !o.forComparing {
|
||||
o.addOutputLine(buildStartingAt + fakeTimeStamp)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, buildFinishedIn) {
|
||||
if !o.forComparing {
|
||||
o.addOutputLine(buildFinishedIn + fakeDuration)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !o.addOrSkipLinesForComparing(listFileStart, listFileEnd, false, nil) &&
|
||||
!o.addOrSkipLinesForComparing(statisticsStart, statisticsEnd, true, nil) &&
|
||||
!o.addOrSkipLinesForComparing(traceStart, traceEnd, false, nil) &&
|
||||
!o.addOrSkipLinesForComparing(buildStatusReportStart, buildStatusReportEnd, false, o.sanitizeBuildStatusTimeStamp) &&
|
||||
!o.addOrSkipLinesForComparing(watchStatusReportStart, watchStatusReportEnd, false, o.sanitizeBuildStatusTimeStamp) {
|
||||
o.addOutputLine(line)
|
||||
}
|
||||
}
|
||||
return strings.Join(o.outputLines, "\n")
|
||||
}
|
||||
|
||||
func (o *outputSanitizer) addOrSkipLinesForComparing(
|
||||
lineStart string,
|
||||
lineEnd string,
|
||||
skipEvenIfNotComparing bool,
|
||||
sanitizeFirstLine func() string,
|
||||
) bool {
|
||||
if o.lines[o.index] != lineStart {
|
||||
return false
|
||||
}
|
||||
o.index++
|
||||
isFirstLine := true
|
||||
for ; o.index < len(o.lines); o.index++ {
|
||||
if o.lines[o.index] == lineEnd {
|
||||
return true
|
||||
}
|
||||
if !o.forComparing && !skipEvenIfNotComparing {
|
||||
line := o.lines[o.index]
|
||||
if isFirstLine && sanitizeFirstLine != nil {
|
||||
line = sanitizeFirstLine()
|
||||
isFirstLine = false
|
||||
}
|
||||
o.addOutputLine(line)
|
||||
}
|
||||
}
|
||||
panic("Expected lineEnd" + lineEnd + " not found after " + lineStart)
|
||||
}
|
||||
|
||||
func (s *TestSys) getOutput(forComparing bool) string {
|
||||
lines := strings.Split(s.currentWrite.String(), "\n")
|
||||
transformer := &outputSanitizer{
|
||||
forComparing: forComparing,
|
||||
lines: lines,
|
||||
outputLines: make([]string, 0, len(lines)),
|
||||
}
|
||||
return transformer.transformLines()
|
||||
}
|
||||
|
||||
func (s *TestSys) clearOutput() {
|
||||
s.currentWrite.Reset()
|
||||
s.tracer.Reset()
|
||||
}
|
||||
|
||||
func (s *TestSys) baselineFSwithDiff(baseline io.Writer) {
|
||||
s.fsDiffer.BaselineFSwithDiff(baseline)
|
||||
}
|
||||
|
||||
func (s *TestSys) writeFileNoError(path string, content string) {
|
||||
if err := s.fsFromFileMap().WriteFile(path, content); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestSys) removeNoError(path string) {
|
||||
if err := s.fsFromFileMap().Remove(path); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestSys) readFileNoError(path string) string {
|
||||
content, ok := s.fsFromFileMap().ReadFile(path)
|
||||
if !ok {
|
||||
panic("File not found: " + path)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func (s *TestSys) renameFileNoError(oldPath string, newPath string) {
|
||||
s.writeFileNoError(newPath, s.readFileNoError(oldPath))
|
||||
s.removeNoError(oldPath)
|
||||
}
|
||||
|
||||
func (s *TestSys) replaceFileText(path string, oldText string, newText string) {
|
||||
content := s.readFileNoError(path)
|
||||
content = strings.Replace(content, oldText, newText, 1)
|
||||
s.writeFileNoError(path, content)
|
||||
}
|
||||
|
||||
func (s *TestSys) replaceFileTextAll(path string, oldText string, newText string) {
|
||||
content := s.readFileNoError(path)
|
||||
content = strings.ReplaceAll(content, oldText, newText)
|
||||
s.writeFileNoError(path, content)
|
||||
}
|
||||
|
||||
func (s *TestSys) appendFile(path string, text string) {
|
||||
content := s.readFileNoError(path)
|
||||
s.writeFileNoError(path, content+text)
|
||||
}
|
||||
|
||||
func (s *TestSys) prependFile(path string, text string) {
|
||||
content := s.readFileNoError(path)
|
||||
s.writeFileNoError(path, text+content)
|
||||
}
|
||||
14
tools/tsgo/internal/execute/tsctests/testmain_test.go
Normal file
14
tools/tsgo/internal/execute/tsctests/testmain_test.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package tsctests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/baseline"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
core.ApplyDebugStackLimit()
|
||||
defer baseline.Track()()
|
||||
m.Run()
|
||||
}
|
||||
4720
tools/tsgo/internal/execute/tsctests/tsc_test.go
Normal file
4720
tools/tsgo/internal/execute/tsctests/tsc_test.go
Normal file
File diff suppressed because it is too large
Load Diff
4541
tools/tsgo/internal/execute/tsctests/tscbuild_test.go
Normal file
4541
tools/tsgo/internal/execute/tsctests/tscbuild_test.go
Normal file
File diff suppressed because it is too large
Load Diff
688
tools/tsgo/internal/execute/tsctests/tscwatch_test.go
Normal file
688
tools/tsgo/internal/execute/tsctests/tscwatch_test.go
Normal file
@@ -0,0 +1,688 @@
|
||||
package tsctests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
|
||||
)
|
||||
|
||||
func TestWatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
bunDependencyTest := func() *tscInput {
|
||||
files := FileMap{}
|
||||
var index strings.Builder
|
||||
var fileNames strings.Builder
|
||||
fileNames.WriteString(`"index.ts"`)
|
||||
for i := range 12 {
|
||||
name := fmt.Sprintf("pkg%d", i)
|
||||
value := fmt.Sprintf("value%d", i)
|
||||
index.WriteString(fmt.Sprintf(`import { %[1]s } from "./node_modules/.bun/%[2]s/index"; %[1]s;`, value, name))
|
||||
index.WriteString("\n")
|
||||
files["/home/src/workspaces/project/node_modules/.bun/"+name+"/index.ts"] = fmt.Sprintf("export const %s = %d;", value, i)
|
||||
fileNames.WriteString(fmt.Sprintf(`, "node_modules/.bun/%s/index.ts"`, name))
|
||||
}
|
||||
files["/home/src/workspaces/project/index.ts"] = index.String()
|
||||
files["/home/src/workspaces/project/tsconfig.json"] = fmt.Sprintf(`{
|
||||
"compilerOptions": {},
|
||||
"files": [%s]
|
||||
}`, fileNames.String())
|
||||
return &tscInput{
|
||||
subScenario: "watch handles many bun dependency files",
|
||||
files: files,
|
||||
commandLineArgs: []string{"--watch"},
|
||||
}
|
||||
}
|
||||
testCases := []*tscInput{
|
||||
{
|
||||
subScenario: "watch with no tsconfig",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": "",
|
||||
},
|
||||
commandLineArgs: []string{"index.ts", "--watch"},
|
||||
},
|
||||
{
|
||||
subScenario: "watch with tsconfig and incremental",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": "",
|
||||
"/home/src/workspaces/project/tsconfig.json": "{}",
|
||||
},
|
||||
commandLineArgs: []string{"--watch", "--incremental"},
|
||||
},
|
||||
bunDependencyTest(),
|
||||
{
|
||||
subScenario: "watch skips build when no files change",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `const x: number = 1;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": "{}",
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
noChange,
|
||||
},
|
||||
},
|
||||
{
|
||||
subScenario: "watch rebuilds when file is modified",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `const x: number = 1;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": "{}",
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("modify file", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/index.ts", `const x: number = 2;`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
subScenario: "watch rebuilds when source file is deleted",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/a.ts": `import { b } from "./b";`,
|
||||
"/home/src/workspaces/project/b.ts": `export const b = 1;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": "{}",
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
{
|
||||
caption: "delete imported file",
|
||||
edit: func(sys *TestSys) {
|
||||
sys.removeNoError("/home/src/workspaces/project/b.ts")
|
||||
},
|
||||
expectedDiff: "incremental resolves to .js output from prior build (TS7016) while clean build cannot find module at all (TS2307)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
subScenario: "watch detects new file resolving failed import",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/a.ts": `import { b } from "./b";`,
|
||||
"/home/src/workspaces/project/tsconfig.json": "{}",
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("create missing file", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/b.ts", `export const b = 1;`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
// Directory-level change detection via imports
|
||||
{
|
||||
subScenario: "watch detects imported file added in new directory",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `import { util } from "./lib/util";`,
|
||||
"/home/src/workspaces/project/tsconfig.json": "{}",
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("create directory and imported file", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/lib/util.ts", `export const util = "hello";`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
subScenario: "watch detects imported directory removed",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `import { util } from "./lib/util";`,
|
||||
"/home/src/workspaces/project/lib/util.ts": `export const util = "hello";`,
|
||||
"/home/src/workspaces/project/tsconfig.json": "{}",
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
{
|
||||
caption: "remove directory with imported file",
|
||||
edit: func(sys *TestSys) {
|
||||
sys.removeNoError("/home/src/workspaces/project/lib/util.ts")
|
||||
},
|
||||
expectedDiff: "incremental resolves to .js output from prior build (TS7016) while clean build cannot find module at all (TS2307)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
subScenario: "watch detects import path restructured",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `import { util } from "./lib/util";`,
|
||||
"/home/src/workspaces/project/lib/util.ts": `export const util = "v1";`,
|
||||
"/home/src/workspaces/project/tsconfig.json": "{}",
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("move file to new path and update import", func(sys *TestSys) {
|
||||
sys.removeNoError("/home/src/workspaces/project/lib/util.ts")
|
||||
sys.writeFileNoError("/home/src/workspaces/project/src/util.ts", `export const util = "v2";`)
|
||||
sys.writeFileNoError("/home/src/workspaces/project/index.ts", `import { util } from "./src/util";`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
// tsconfig include/exclude change detection
|
||||
{
|
||||
subScenario: "watch rebuilds when tsconfig include pattern adds file",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `const x = 1;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{
|
||||
"compilerOptions": {},
|
||||
"include": ["*.ts"]
|
||||
}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("widen include pattern to add src dir", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/src/extra.ts", `export const extra = 2;`)
|
||||
sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", `{
|
||||
"compilerOptions": {},
|
||||
"include": ["*.ts", "src/**/*.ts"]
|
||||
}`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
subScenario: "watch rebuilds when tsconfig is modified to change strict",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `const x = null; const y: string = x;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("enable strict mode", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", `{"compilerOptions": {"strict": true}}`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
// Path resolution: tsconfig include pointing to non-existent directory
|
||||
{
|
||||
subScenario: "watch detects file added to previously non-existent include path",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `const x = 1;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{
|
||||
"compilerOptions": {},
|
||||
"include": ["index.ts", "src/**/*.ts"]
|
||||
}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("create src dir with ts file matching include", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/src/helper.ts", `export const helper = "added";`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
subScenario: "watch detects new file in existing include directory",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/src/a.ts": `export const a = 1;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{
|
||||
"compilerOptions": {},
|
||||
"include": ["src/**/*.ts"]
|
||||
}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("add new file to existing src directory", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/src/b.ts", `export const b = 2;`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
// Wildcard include: nested subdirectory detection
|
||||
{
|
||||
subScenario: "watch detects file added in new nested subdirectory",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/src/a.ts": `export const a = 1;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{
|
||||
"compilerOptions": {},
|
||||
"include": ["src/**/*.ts"]
|
||||
}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("create nested dir with ts file", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/src/deep/nested/util.ts", `export const util = "nested";`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
subScenario: "watch detects file added in multiple new subdirectories simultaneously",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/src/a.ts": `export const a = 1;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{
|
||||
"compilerOptions": {},
|
||||
"include": ["src/**/*.ts"]
|
||||
}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("create multiple new subdirs with files", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/src/models/user.ts", `export interface User { name: string; }`)
|
||||
sys.writeFileNoError("/home/src/workspaces/project/src/utils/format.ts", `export function format(s: string): string { return s.trim(); }`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
subScenario: "watch detects nested subdirectory removed and recreated",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/src/lib/helper.ts": `export const helper = "v1";`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{
|
||||
"compilerOptions": {},
|
||||
"include": ["src/**/*.ts"]
|
||||
}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
{
|
||||
caption: "remove nested dir",
|
||||
expectedDiff: "incremental has prior state and does not report no-inputs error",
|
||||
edit: func(sys *TestSys) {
|
||||
sys.removeNoError("/home/src/workspaces/project/src/lib/helper.ts")
|
||||
},
|
||||
},
|
||||
newTscEdit("recreate nested dir with new content", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/src/lib/helper.ts", `export const helper = "v2";`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
// Path resolution: import from non-existent node_modules package
|
||||
{
|
||||
subScenario: "watch detects node modules package added",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `import { lib } from "mylib";`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("install package in node_modules", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/package.json", `{"name": "mylib", "main": "index.js", "types": "index.d.ts"}`)
|
||||
sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/index.js", `exports.lib = "hello";`)
|
||||
sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/index.d.ts", `export declare const lib: string;`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
// Path resolution: node_modules package removed
|
||||
{
|
||||
subScenario: "watch detects node modules package removed",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `import { lib } from "mylib";`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{}`,
|
||||
"/home/src/workspaces/project/node_modules/mylib/package.json": `{"name": "mylib", "main": "index.js", "types": "index.d.ts"}`,
|
||||
"/home/src/workspaces/project/node_modules/mylib/index.js": `exports.lib = "hello";`,
|
||||
"/home/src/workspaces/project/node_modules/mylib/index.d.ts": `export declare const lib: string;`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
{
|
||||
caption: "remove node_modules package",
|
||||
edit: func(sys *TestSys) {
|
||||
sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/index.d.ts")
|
||||
sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/index.js")
|
||||
sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/package.json")
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// Path resolution: node_modules removed then reinstalled (npm ci after rm -rf)
|
||||
{
|
||||
subScenario: "watch detects node modules reinstalled after deletion",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `import { lib } from "mylib";`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{}`,
|
||||
"/home/src/workspaces/project/node_modules/mylib/package.json": `{"name": "mylib", "main": "index.js", "types": "index.d.ts"}`,
|
||||
"/home/src/workspaces/project/node_modules/mylib/index.js": `exports.lib = "hello";`,
|
||||
"/home/src/workspaces/project/node_modules/mylib/index.d.ts": `export declare const lib: string;`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
{
|
||||
caption: "delete node_modules entirely",
|
||||
edit: func(sys *TestSys) {
|
||||
sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/index.d.ts")
|
||||
sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/index.js")
|
||||
sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/package.json")
|
||||
},
|
||||
},
|
||||
newTscEdit("reinstall node_modules", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/package.json", `{"name": "mylib", "main": "index.js", "types": "index.d.ts"}`)
|
||||
sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/index.js", `exports.lib = "hello";`)
|
||||
sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/index.d.ts", `export declare const lib: string;`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
// Config file lifecycle
|
||||
{
|
||||
subScenario: "watch handles tsconfig deleted",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `const x = 1;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
{
|
||||
caption: "delete tsconfig",
|
||||
expectedDiff: "incremental reports config read error while clean build without tsconfig prints usage help",
|
||||
edit: func(sys *TestSys) {
|
||||
sys.removeNoError("/home/src/workspaces/project/tsconfig.json")
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
subScenario: "watch handles tsconfig with extends base modified",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `const x = null; const y: string = x;`,
|
||||
"/home/src/workspaces/project/base.json": `{
|
||||
"compilerOptions": { "strict": false }
|
||||
}`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{
|
||||
"extends": "./base.json"
|
||||
}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("modify base config to enable strict", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/base.json", `{
|
||||
"compilerOptions": { "strict": true }
|
||||
}`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
subScenario: "watch rebuilds when tsconfig is touched but content unchanged",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `const x = 1;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("touch tsconfig without changing content", func(sys *TestSys) {
|
||||
content := sys.readFileNoError("/home/src/workspaces/project/tsconfig.json")
|
||||
sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", content)
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
subScenario: "watch with tsconfig files list entry deleted",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/a.ts": `export const a = 1;`,
|
||||
"/home/src/workspaces/project/b.ts": `export const b = 2;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{
|
||||
"compilerOptions": {},
|
||||
"files": ["a.ts", "b.ts"]
|
||||
}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("delete file listed in files array", func(sys *TestSys) {
|
||||
sys.removeNoError("/home/src/workspaces/project/b.ts")
|
||||
}),
|
||||
},
|
||||
},
|
||||
// Module resolution & dependencies
|
||||
{
|
||||
subScenario: "watch detects module going missing then coming back",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `import { util } from "./util";`,
|
||||
"/home/src/workspaces/project/util.ts": `export const util = "v1";`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
{
|
||||
caption: "delete util module",
|
||||
edit: func(sys *TestSys) {
|
||||
sys.removeNoError("/home/src/workspaces/project/util.ts")
|
||||
},
|
||||
expectedDiff: "incremental resolves to .js output from prior build while clean build cannot find module",
|
||||
},
|
||||
newTscEdit("recreate util module with new content", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/util.ts", `export const util = "v2";`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
subScenario: "watch detects scoped package installed",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `import { lib } from "@scope/mylib";`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("install scoped package", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/node_modules/@scope/mylib/package.json", `{"name": "@scope/mylib", "types": "index.d.ts"}`)
|
||||
sys.writeFileNoError("/home/src/workspaces/project/node_modules/@scope/mylib/index.d.ts", `export declare const lib: string;`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
subScenario: "watch detects package json types field edited",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `import { lib } from "mylib";`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{}`,
|
||||
"/home/src/workspaces/project/node_modules/mylib/package.json": `{"name": "mylib", "types": "old.d.ts"}`,
|
||||
"/home/src/workspaces/project/node_modules/mylib/old.d.ts": `export declare const lib: number;`,
|
||||
"/home/src/workspaces/project/node_modules/mylib/new.d.ts": `export declare const lib: string;`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("change package.json types field", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/package.json", `{"name": "mylib", "types": "new.d.ts"}`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
subScenario: "watch detects at-types package installed later",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `import * as lib from "untyped-lib";`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{}`,
|
||||
"/home/src/workspaces/project/node_modules/untyped-lib/index.js": `module.exports = {};`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("install @types for the library", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/node_modules/@types/untyped-lib/index.d.ts", `declare module "untyped-lib" { export const value: string; }`)
|
||||
sys.writeFileNoError("/home/src/workspaces/project/node_modules/@types/untyped-lib/package.json", `{"name": "@types/untyped-lib", "types": "index.d.ts"}`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
// File operations
|
||||
{
|
||||
subScenario: "watch detects file renamed and renamed back",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `import { helper } from "./helper";`,
|
||||
"/home/src/workspaces/project/helper.ts": `export const helper = 1;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
{
|
||||
caption: "rename helper to helper2",
|
||||
edit: func(sys *TestSys) {
|
||||
sys.renameFileNoError("/home/src/workspaces/project/helper.ts", "/home/src/workspaces/project/helper2.ts")
|
||||
},
|
||||
expectedDiff: "incremental resolves to .js output from prior build while clean build cannot find module",
|
||||
},
|
||||
newTscEdit("rename back to helper", func(sys *TestSys) {
|
||||
sys.renameFileNoError("/home/src/workspaces/project/helper2.ts", "/home/src/workspaces/project/helper.ts")
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
subScenario: "watch detects file deleted and new file added simultaneously",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/a.ts": `import { b } from "./b";`,
|
||||
"/home/src/workspaces/project/b.ts": `export const b = 1;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("delete b.ts and create c.ts with updated import", func(sys *TestSys) {
|
||||
sys.removeNoError("/home/src/workspaces/project/b.ts")
|
||||
sys.writeFileNoError("/home/src/workspaces/project/c.ts", `export const c = 2;`)
|
||||
sys.writeFileNoError("/home/src/workspaces/project/a.ts", `import { c } from "./c";`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
subScenario: "watch handles file rapidly recreated",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `import { val } from "./data";`,
|
||||
"/home/src/workspaces/project/data.ts": `export const val = "original";`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("delete and immediately recreate with new content", func(sys *TestSys) {
|
||||
sys.removeNoError("/home/src/workspaces/project/data.ts")
|
||||
sys.writeFileNoError("/home/src/workspaces/project/data.ts", `export const val = "recreated";`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
// Symlinks — only node_modules symlinks are resolved via Realpath,
|
||||
// matching the TypeScript compiler's behavior (see program.ts:2119).
|
||||
{
|
||||
subScenario: "watch detects change in symlinked node_modules file",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `import { shared } from "shared";`,
|
||||
"/home/src/workspaces/shared/index.ts": `export const shared = "v1";`,
|
||||
"/home/src/workspaces/project/node_modules/shared/index.ts": vfstest.Symlink("/home/src/workspaces/shared/index.ts"),
|
||||
"/home/src/workspaces/project/tsconfig.json": `{}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("modify symlink target", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/shared/index.ts", `export const shared = "v2";`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
// Ancestor fallback stability — when a tsconfig include references a
|
||||
// directory that doesn't exist
|
||||
{
|
||||
subScenario: "watch stability with ancestor directory fallback",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `const x: number = 1;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{ "include": ["*.ts", "missing/**/*"] }`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("trivial file change", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/index.ts", `const x: number = 2;`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
// Ancestor fallback: creating deeply nested directories that didn't
|
||||
// exist at initial build time should trigger a rebuild and re-watch.
|
||||
{
|
||||
subScenario: "watch detects file added in deeply nested non-existent include path",
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/index.ts": `const x: number = 1;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{ "include": ["*.ts", "deep/nested/dir/**/*"] }`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("create deeply nested file matching include", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/deep/nested/dir/added.ts", `export const added = 1;`)
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test.run(t, "commandLineWatch")
|
||||
}
|
||||
}
|
||||
|
||||
func listToTsconfig(base string, tsconfigOpts ...string) (string, string) {
|
||||
optionString := strings.Join(tsconfigOpts, ",\n ")
|
||||
tsconfigText := `{
|
||||
"compilerOptions": {
|
||||
`
|
||||
after := " "
|
||||
if base != "" {
|
||||
tsconfigText += " " + base
|
||||
after = ",\n "
|
||||
}
|
||||
if len(tsconfigOpts) != 0 {
|
||||
tsconfigText += after + optionString
|
||||
}
|
||||
tsconfigText += `
|
||||
}
|
||||
}`
|
||||
return tsconfigText, optionString
|
||||
}
|
||||
|
||||
func toTsconfig(base string, compilerOpts string) string {
|
||||
tsconfigText, _ := listToTsconfig(base, compilerOpts)
|
||||
return tsconfigText
|
||||
}
|
||||
|
||||
func noEmitWatchTestInput(
|
||||
subScenario string,
|
||||
commandLineArgs []string,
|
||||
aText string,
|
||||
tsconfigOptions []string,
|
||||
) *tscInput {
|
||||
noEmitOpt := `"noEmit": true`
|
||||
tsconfigText, optionString := listToTsconfig(noEmitOpt, tsconfigOptions...)
|
||||
return &tscInput{
|
||||
subScenario: subScenario,
|
||||
commandLineArgs: commandLineArgs,
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/a.ts": aText,
|
||||
"/home/src/workspaces/project/tsconfig.json": tsconfigText,
|
||||
},
|
||||
edits: []*tscEdit{
|
||||
newTscEdit("fix error", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/a.ts", `const a = "hello";`)
|
||||
}),
|
||||
newTscEdit("emit after fixing error", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", toTsconfig("", optionString))
|
||||
}),
|
||||
newTscEdit("no emit run after fixing error", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", toTsconfig(noEmitOpt, optionString))
|
||||
}),
|
||||
newTscEdit("introduce error", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/a.ts", aText)
|
||||
}),
|
||||
newTscEdit("emit when error", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", toTsconfig("", optionString))
|
||||
}),
|
||||
newTscEdit("no emit run when error", func(sys *TestSys) {
|
||||
sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", toTsconfig(noEmitOpt, optionString))
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newTscEdit(name string, edit func(sys *TestSys)) *tscEdit {
|
||||
return &tscEdit{caption: name, edit: edit}
|
||||
}
|
||||
|
||||
func TestTscNoEmitWatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []*tscInput{
|
||||
noEmitWatchTestInput(
|
||||
"syntax errors",
|
||||
[]string{"-w"},
|
||||
`const a = "hello`,
|
||||
nil,
|
||||
),
|
||||
noEmitWatchTestInput(
|
||||
"semantic errors",
|
||||
[]string{"-w"},
|
||||
`const a: number = "hello"`,
|
||||
nil,
|
||||
),
|
||||
noEmitWatchTestInput(
|
||||
"dts errors without dts enabled",
|
||||
[]string{"-w"},
|
||||
`const a = class { private p = 10; };`,
|
||||
nil,
|
||||
),
|
||||
noEmitWatchTestInput(
|
||||
"dts errors",
|
||||
[]string{"-w"},
|
||||
`const a = class { private p = 10; };`,
|
||||
[]string{`"declaration": true`},
|
||||
),
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test.run(t, "noEmit")
|
||||
}
|
||||
}
|
||||
292
tools/tsgo/internal/execute/tsctests/watcher_race_test.go
Normal file
292
tools/tsgo/internal/execute/tsctests/watcher_race_test.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package tsctests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/execute"
|
||||
"github.com/microsoft/typescript-go/internal/execute/tsc"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
// createTestWatcher sets up a minimal project with a tsconfig and
|
||||
// returns a Watcher ready for concurrent testing, plus the TestSys
|
||||
// for file manipulation.
|
||||
func createTestWatcher(t *testing.T) (*execute.Watcher, *TestSys) {
|
||||
t.Helper()
|
||||
input := &tscInput{
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/a.ts": `const a: number = 1;`,
|
||||
"/home/src/workspaces/project/b.ts": `import { a } from "./a"; export const b = a;`,
|
||||
"/home/src/workspaces/project/tsconfig.json": `{}`,
|
||||
},
|
||||
commandLineArgs: []string{"--watch"},
|
||||
}
|
||||
sys := newTestSys(input, false)
|
||||
result := execute.CommandLine(context.Background(), sys, []string{"--watch"}, sys)
|
||||
if result.Watcher == nil {
|
||||
t.Fatal("expected Watcher to be non-nil in watch mode")
|
||||
}
|
||||
w, ok := result.Watcher.(*execute.Watcher)
|
||||
if !ok {
|
||||
t.Fatalf("expected *execute.Watcher, got %T", result.Watcher)
|
||||
}
|
||||
return w, sys
|
||||
}
|
||||
|
||||
// TestWatcherConcurrentDoCycle calls DoCycle from multiple goroutines
|
||||
// while modifying source files, exposing data races on Watcher fields
|
||||
// such as configModified, program, config, and the underlying
|
||||
// FileWatcher state. Run with -race to detect.
|
||||
func TestWatcherConcurrentDoCycle(t *testing.T) {
|
||||
t.Parallel()
|
||||
w, sys := createTestWatcher(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := range 8 {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
for j := range 10 {
|
||||
_ = sys.fsFromFileMap().WriteFile(
|
||||
"/home/src/workspaces/project/a.ts",
|
||||
fmt.Sprintf("const a: number = %d;", i*10+j),
|
||||
)
|
||||
w.DoCycle()
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// TestWatcherDoCycleWithConcurrentStateReads calls DoCycle from
|
||||
// multiple goroutines, some modifying files and some not, to test
|
||||
// concurrent access to all Watcher and FileWatcher state.
|
||||
func TestWatcherDoCycleWithConcurrentStateReads(t *testing.T) {
|
||||
t.Parallel()
|
||||
w, sys := createTestWatcher(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// DoCycle goroutines
|
||||
for i := range 4 {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
for j := range 15 {
|
||||
_ = sys.fsFromFileMap().WriteFile(
|
||||
"/home/src/workspaces/project/a.ts",
|
||||
fmt.Sprintf("const a: number = %d;", i*15+j),
|
||||
)
|
||||
w.DoCycle()
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// State reader goroutines
|
||||
for range 8 {
|
||||
wg.Go(func() {
|
||||
for range 50 {
|
||||
w.DoCycle()
|
||||
w.DoCycle()
|
||||
w.DoCycle()
|
||||
w.DoCycle()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// TestWatcherConcurrentFileChangesAndDoCycle creates, modifies, and
|
||||
// deletes files from multiple goroutines while DoCycle runs, testing
|
||||
// races between FS mutations and watch state updates.
|
||||
func TestWatcherConcurrentFileChangesAndDoCycle(t *testing.T) {
|
||||
t.Parallel()
|
||||
w, sys := createTestWatcher(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// File creators
|
||||
for i := range 4 {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
for j := range 20 {
|
||||
path := fmt.Sprintf("/home/src/workspaces/project/gen_%d_%d.ts", i, j)
|
||||
_ = sys.fsFromFileMap().WriteFile(path, fmt.Sprintf("export const x%d_%d = %d;", i, j, j))
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// File deleters
|
||||
wg.Go(func() {
|
||||
for j := range 20 {
|
||||
_ = sys.fsFromFileMap().Remove(
|
||||
fmt.Sprintf("/home/src/workspaces/project/gen_0_%d.ts", j),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// DoCycle callers
|
||||
for range 4 {
|
||||
wg.Go(func() {
|
||||
for range 10 {
|
||||
w.DoCycle()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// TestWatcherRapidConfigChanges modifies tsconfig.json rapidly from
|
||||
// multiple goroutines while DoCycle runs, testing races on
|
||||
// config-related fields (configModified, configHasErrors,
|
||||
// configFilePaths, config, extendedConfigCache).
|
||||
func TestWatcherRapidConfigChanges(t *testing.T) {
|
||||
t.Parallel()
|
||||
w, sys := createTestWatcher(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
configs := []string{
|
||||
`{}`,
|
||||
`{"compilerOptions": {"strict": true}}`,
|
||||
`{"compilerOptions": {"target": "ES2020"}}`,
|
||||
`{"compilerOptions": {"noEmit": true}}`,
|
||||
}
|
||||
|
||||
// Config modifiers + DoCycle
|
||||
for i := range 3 {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
for j := range 10 {
|
||||
_ = sys.fsFromFileMap().WriteFile(
|
||||
"/home/src/workspaces/project/tsconfig.json",
|
||||
configs[(i+j)%len(configs)],
|
||||
)
|
||||
w.DoCycle()
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Concurrent source file modifications
|
||||
for i := range 2 {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
for j := range 15 {
|
||||
_ = sys.fsFromFileMap().WriteFile(
|
||||
"/home/src/workspaces/project/a.ts",
|
||||
fmt.Sprintf("const a: number = %d;", i*15+j),
|
||||
)
|
||||
w.DoCycle()
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// State readers
|
||||
for range 4 {
|
||||
wg.Go(func() {
|
||||
for range 30 {
|
||||
w.DoCycle()
|
||||
w.DoCycle()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// TestWatcherConcurrentDoCycleNoChanges calls DoCycle from many
|
||||
// goroutines when no files have changed, testing the early-return
|
||||
// path where WatchState is read and HasChanges is called.
|
||||
func TestWatcherConcurrentDoCycleNoChanges(t *testing.T) {
|
||||
t.Parallel()
|
||||
w, _ := createTestWatcher(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for range 16 {
|
||||
wg.Go(func() {
|
||||
for range 50 {
|
||||
w.DoCycle()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// TestWatcherAlternatingModifyAndDoCycle alternates between modifying
|
||||
// a file and calling DoCycle from different goroutines, creating a
|
||||
// realistic scenario where the file watcher detects changes mid-cycle.
|
||||
func TestWatcherAlternatingModifyAndDoCycle(t *testing.T) {
|
||||
t.Parallel()
|
||||
w, sys := createTestWatcher(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Writer goroutine: continuously modifies files
|
||||
wg.Go(func() {
|
||||
for j := range 100 {
|
||||
_ = sys.fsFromFileMap().WriteFile(
|
||||
"/home/src/workspaces/project/a.ts",
|
||||
fmt.Sprintf("const a: number = %d;", j),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Multiple DoCycle goroutines
|
||||
for range 4 {
|
||||
wg.Go(func() {
|
||||
for range 25 {
|
||||
w.DoCycle()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// State reader goroutines
|
||||
for range 4 {
|
||||
wg.Go(func() {
|
||||
for range 100 {
|
||||
w.DoCycle()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestBuildWatchStopsWhenContextIsCancelled(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sys := newTestSys(&tscInput{
|
||||
files: FileMap{
|
||||
"/home/src/workspaces/project/tsconfig.json": `{"compilerOptions":{"composite":true},"files":["index.ts"]}`,
|
||||
"/home/src/workspaces/project/index.ts": `export const x = 1;`,
|
||||
},
|
||||
}, false)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
resultCh := make(chan tsc.CommandLineResult, 1)
|
||||
go func() {
|
||||
resultCh <- execute.CommandLine(ctx, sys, []string{"--build", "--watch", "--watchInterval", "60000"}, sys)
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
assert.Equal(t, result.Status, tsc.ExitStatusSuccess)
|
||||
assert.Assert(t, result.Watcher != nil)
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("build watch did not stop after context cancellation")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user