|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/json" |
| 6 | + "errors" |
| 7 | + "fmt" |
| 8 | + "io" |
| 9 | + "os" |
| 10 | + "path/filepath" |
| 11 | + "strings" |
| 12 | + "sync/atomic" |
| 13 | + "testing" |
| 14 | + |
| 15 | + "github.com/rogpeppe/go-internal/testscript" |
| 16 | + "go.lsp.dev/jsonrpc2" |
| 17 | + |
| 18 | + lspenv "github.com/gnolang/gnopls/internal/env" |
| 19 | + "github.com/gnolang/gnopls/internal/lsp" |
| 20 | + "github.com/gnolang/gnopls/internal/version" |
| 21 | +) |
| 22 | + |
| 23 | +type buffer struct { |
| 24 | + *io.PipeWriter |
| 25 | + *io.PipeReader |
| 26 | +} |
| 27 | + |
| 28 | +func (b buffer) Close() error { |
| 29 | + b.PipeReader.Close() |
| 30 | + b.PipeWriter.Close() |
| 31 | + return nil |
| 32 | +} |
| 33 | + |
| 34 | +// TestScripts runs a gnopls server instance and uses the buffer type to |
| 35 | +// communicate with it. Then it executes all the txtar files found in the |
| 36 | +// testdata directory. |
| 37 | +func TestScripts(t *testing.T) { |
| 38 | + testscript.Run(t, testscript.Params{ |
| 39 | + UpdateScripts: os.Getenv("TXTAR_UPDATE") != "", |
| 40 | + Setup: func(env *testscript.Env) error { |
| 41 | + var ( |
| 42 | + clientRead, serverWrite = io.Pipe() |
| 43 | + serverRead, clientWrite = io.Pipe() |
| 44 | + serverBuf = buffer{ |
| 45 | + PipeWriter: serverWrite, |
| 46 | + PipeReader: serverRead, |
| 47 | + } |
| 48 | + clientBuf = buffer{ |
| 49 | + PipeWriter: clientWrite, |
| 50 | + PipeReader: clientRead, |
| 51 | + } |
| 52 | + serverConn = jsonrpc2.NewConn(jsonrpc2.NewStream(serverBuf)) |
| 53 | + procEnv = &lspenv.Env{ |
| 54 | + GNOROOT: os.Getenv("GNOROOT"), |
| 55 | + GNOHOME: lspenv.GnoHome(), |
| 56 | + } |
| 57 | + serverHandler = jsonrpc2.HandlerServer(lsp.BuildServerHandler(serverConn, procEnv)) |
| 58 | + clientConn = jsonrpc2.NewConn(jsonrpc2.NewStream(clientBuf)) |
| 59 | + ) |
| 60 | + env.Values["conn"] = clientConn |
| 61 | + |
| 62 | + // Start LSP server |
| 63 | + ctx := context.Background() |
| 64 | + go func() { |
| 65 | + if err := serverHandler.ServeStream(ctx, serverConn); !errors.Is(err, io.ErrClosedPipe) { |
| 66 | + env.T().Fatal("Server error", err) |
| 67 | + } |
| 68 | + }() |
| 69 | + // Listen to server notifications |
| 70 | + // All servers notifications are expected to be find in the txtar working |
| 71 | + // directory, using the following format: |
| 72 | + // $WORK/output/notify{i}.json |
| 73 | + // where {i} is a counter starting at 1. |
| 74 | + var notifyNum atomic.Uint32 |
| 75 | + clientConn.Go(ctx, func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error { |
| 76 | + filename := fmt.Sprintf("notify%d.json", notifyNum.Add(1)) |
| 77 | + return writeJSON(env, filename, req) |
| 78 | + }) |
| 79 | + |
| 80 | + // Stop LSP server at the end of test |
| 81 | + env.Defer(func() { |
| 82 | + clientConn.Close() |
| 83 | + serverConn.Close() |
| 84 | + <-clientConn.Done() |
| 85 | + <-serverConn.Done() |
| 86 | + }) |
| 87 | + |
| 88 | + // GNOPLS_WD is used in txtar files to know the location of the gnopls |
| 89 | + // working dir. |
| 90 | + env.Setenv("GNOPLS_WD", filepath.Join(lspenv.GnoHome(), "gnopls", "tmp")) |
| 91 | + // GNOPLS_VERSION is used in txtar files to know the server version. |
| 92 | + // This required because it changes over git status (can be "local" or |
| 93 | + // "v0.1.0-local"...) |
| 94 | + env.Setenv("GNOPLS_VERSION", version.GetVersion(ctx)) |
| 95 | + |
| 96 | + return nil |
| 97 | + }, |
| 98 | + Dir: "testdata", |
| 99 | + Cmds: map[string]func(*testscript.TestScript, bool, []string){ |
| 100 | + // "lsp" is a txtar command that sends a lsp command to the server with |
| 101 | + // the following arguments: |
| 102 | + // - the method name |
| 103 | + // - the path to the file that contains the method parameters |
| 104 | + // |
| 105 | + // The server's response is encoded into the $WORK/output directory, with |
| 106 | + // filename equals to the parameter filename. |
| 107 | + // For example, the response of the following command: |
| 108 | + // lsp initialized input/initialized.json |
| 109 | + // will be available in $WORK/output/initialized.json |
| 110 | + "lsp": func(ts *testscript.TestScript, neg bool, args []string) { //nolint:unparam |
| 111 | + if len(args) != 2 { |
| 112 | + ts.Fatalf("usage: lsp <method> <param_file>") |
| 113 | + } |
| 114 | + var ( |
| 115 | + method = args[0] |
| 116 | + paramsFile = args[1] |
| 117 | + ) |
| 118 | + call(ts, method, paramsFile) |
| 119 | + }, |
| 120 | + }, |
| 121 | + }) |
| 122 | +} |
| 123 | + |
| 124 | +// call decodes paramFile and send it to the server using method. |
| 125 | +func call(ts *testscript.TestScript, method string, paramFile string) { |
| 126 | + paramStr := ts.ReadFile(paramFile) |
| 127 | + // Replace $WORK with real path |
| 128 | + paramStr = os.Expand(paramStr, func(key string) string { |
| 129 | + if strings.HasPrefix(key, "FILE_") { |
| 130 | + // ${FILE_filename} is a convenient directive in txtar files used to be |
| 131 | + // replaced by the content of the file "filename". |
| 132 | + // Since the prefix is "FILE_", key[5:] holds the filename. |
| 133 | + fileContent := ts.ReadFile(key[5:]) |
| 134 | + // Escape fileContent for JSON format |
| 135 | + bz, err := json.Marshal(fileContent) |
| 136 | + if err != nil { |
| 137 | + ts.Fatalf("encode key %s %q: %v", key, fileContent, err) |
| 138 | + } |
| 139 | + return string(bz[1 : len(bz)-1]) // remove quote wrapping |
| 140 | + } |
| 141 | + return ts.Getenv(key) |
| 142 | + }) |
| 143 | + var params any |
| 144 | + if err := json.Unmarshal([]byte(paramStr), ¶ms); err != nil { |
| 145 | + ts.Fatalf("decode param file %s: %v", paramFile, err) |
| 146 | + } |
| 147 | + var ( |
| 148 | + conn = ts.Value("conn").(jsonrpc2.Conn) //nolint:errcheck |
| 149 | + response any |
| 150 | + ) |
| 151 | + _, err := conn.Call(context.Background(), method, params, &response) |
| 152 | + if err != nil { |
| 153 | + response = map[string]any{"error": err} |
| 154 | + } |
| 155 | + if err := writeJSON(ts, filepath.Base(paramFile), response); err != nil { |
| 156 | + ts.Fatalf("writeJSON: %v", err) |
| 157 | + } |
| 158 | +} |
| 159 | + |
| 160 | +// writeJSON writes x to $WORK/output/filename |
| 161 | +func writeJSON(ts interface{ Getenv(string) string }, filename string, x any) error { |
| 162 | + workDir := ts.Getenv("WORK") |
| 163 | + filename = filepath.Join(workDir, "output", filename) |
| 164 | + err := os.MkdirAll(filepath.Dir(filename), os.ModePerm) |
| 165 | + if err != nil { |
| 166 | + return err |
| 167 | + } |
| 168 | + bz, err := json.MarshalIndent(x, "", " ") |
| 169 | + if err != nil { |
| 170 | + return err |
| 171 | + } |
| 172 | + bz = append(bz, '\n') // txtar files always have a final newline |
| 173 | + return os.WriteFile(filename, bz, os.ModePerm) |
| 174 | +} |
0 commit comments