package lsp_test import ( "bufio" "flag" "os" "os/exec" "strings" "testing" "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/json" "github.com/microsoft/typescript-go/internal/jsonrpc" "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/lsp" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/testutil/lsptestutil" "github.com/microsoft/typescript-go/internal/vfs/osvfs" ) var ( replay = flag.String("replay", "", "Path to replay file") testDir = flag.String("testDir", "", "Path to project directory") simple = flag.Bool("simple", false, "Replay only file opening and closing, plus the final request") superSimple = flag.Bool("superSimple", false, "Replay only the final file opening and the final request") ) type initialArguments struct { RootDirUriPlaceholder string `json:"rootDirUriPlaceholder"` RootDirPlaceholder string `json:"rootDirPlaceholder"` } type rawMessage struct { Kind string `json:"kind"` Method string `json:"method"` Params json.Value `json:"params"` } func TestReplay(t *testing.T) { t.Parallel() if replay == nil || *replay == "" { t.Skip("no replay file specified") } if testDir == nil || *testDir == "" { t.Fatal("testDir must be specified") } testDirUri := lsconv.FileNameToDocumentURI(*testDir) fs := bundled.WrapFS(osvfs.FS()) defaultLibraryPath := bundled.LibPath() typingsLocation := osvfs.GetGlobalTypingsCacheLocation() serverOpts := lsp.ServerOptions{ Err: os.Stderr, Cwd: core.Must(os.Getwd()), FS: fs, DefaultLibraryPath: defaultLibraryPath, TypingsLocation: typingsLocation, NpmInstall: func(cwd string, args []string) ([]byte, error) { cmd := exec.Command("npm", args...) cmd.Dir = cwd return cmd.Output() }, } client, closeClient := lsptestutil.NewLSPClient(t, serverOpts, nil) defer func() { err := closeClient() if err != nil { t.Errorf("goroutine error: %v", err) } }() f, err := os.Open(*replay) if err != nil { t.Fatalf("failed to read replay file: %v", err) } defer f.Close() scanner := bufio.NewScanner(f) scanner.Buffer(make([]byte, 0, 64*1024), 10*1024*1024) if !scanner.Scan() { t.Fatalf("replay file is empty") } rootDirPlaceholder := "@PROJECT_ROOT@" rootDirUriPlaceholder := "@PROJECT_ROOT_URI@" firstLine := scanner.Bytes() var initObj initialArguments err = json.Unmarshal(firstLine, &initObj) if err != nil { t.Fatalf("failed to parse initial arguments: %v", err) } if initObj.RootDirPlaceholder != "" { rootDirPlaceholder = initObj.RootDirPlaceholder } if initObj.RootDirUriPlaceholder != "" { rootDirUriPlaceholder = initObj.RootDirUriPlaceholder } rootDirReplacer := strings.NewReplacer( rootDirPlaceholder, *testDir, rootDirUriPlaceholder, string(testDirUri), ) var messages []*rawMessage for scanner.Scan() { line := scanner.Text() line = rootDirReplacer.Replace(line) var rawMsg rawMessage err := json.Unmarshal([]byte(line), &rawMsg) if err != nil { t.Fatalf("failed to parse message: %v", err) } messages = append(messages, &rawMsg) } if err := scanner.Err(); err != nil { t.Fatalf("error scanning replay file: %v", err) } if simple != nil && *simple { // Include only initialization, file opening/changing/closing, and shutdown messages, plus the final request. var newMessages []*rawMessage var i int for i = 0; i < len(messages) && isInitializationMessage(messages[i]); i++ { newMessages = append(newMessages, messages[i]) } var j int for j = len(messages) - 1; j >= 0 && isExitMessage(messages[j]); j-- { } for k := i; k <= j; k++ { msg := messages[k] if msg.Method == "textDocument/didOpen" || msg.Method == "textDocument/didChange" || msg.Method == "textDocument/didClose" { newMessages = append(newMessages, msg) } } for k := max(i, j); k < len(messages); k++ { newMessages = append(newMessages, messages[k]) } messages = newMessages } else if superSimple != nil && *superSimple { // Include only initialization, shutdown, the last file open and the final request. // We assume here the final request will be for the file that was opened last. var newMessages []*rawMessage var i int for i = 0; i < len(messages) && isInitializationMessage(messages[i]); i++ { newMessages = append(newMessages, messages[i]) } var j int for j = len(messages) - 1; j >= 0 && isExitMessage(messages[j]); j-- { } var openIdx int for openIdx = j; openIdx >= i; openIdx-- { msg := messages[openIdx] if msg.Method == "textDocument/didOpen" { newMessages = append(newMessages, msg) break } } for k := max(openIdx+1, j); k < len(messages); k++ { newMessages = append(newMessages, messages[k]) } messages = newMessages } for _, rawMsg := range messages { var kind jsonrpc.MessageKind var reqID *jsonrpc.ID switch rawMsg.Kind { case "request": kind = jsonrpc.MessageKindRequest reqID = lsproto.NewID(lsproto.IntegerOrString{Integer: new(client.NextID())}) case "notification": kind = jsonrpc.MessageKindNotification default: t.Fatalf("unknown message kind: %s", rawMsg.Kind) } var rpcMsg struct { JSONRPC string `json:"jsonrpc"` ID *jsonrpc.ID `json:"id"` Method string `json:"method"` Params json.Value `json:"params"` } rpcMsg.JSONRPC = "2.0" rpcMsg.ID = reqID rpcMsg.Method = rawMsg.Method rpcMsg.Params = rawMsg.Params rpcData, err := json.Marshal(rpcMsg) if err != nil { t.Fatalf("failed to marshal rpc message: %v", err) } var msg lsproto.Message err = json.Unmarshal(rpcData, &msg) if err != nil { t.Fatalf("failed to unmarshal rpc message into lsproto.Message: %v", err) } switch kind { case jsonrpc.MessageKindRequest: response, ok := client.SendRequestWorker(t, msg.AsRequest(), reqID) if !ok { t.Fatalf("failed to send request for method %s", rawMsg.Method) } if response.Error != nil { t.Fatalf("server returned error for method %s params %s:\n%v", rawMsg.Method, rawMsg.Params, response.Error) } case jsonrpc.MessageKindNotification: client.WriteMsg(t, &msg) default: t.Fatalf("unknown message kind: %s", rawMsg.Kind) } } } func isInitializationMessage(msg *rawMessage) bool { return msg.Method == "initialize" || msg.Method == "initialized" } func isExitMessage(msg *rawMessage) bool { return msg.Method == "exit" || msg.Method == "shutdown" }