github.com/april1989/origin-go-tools@v0.0.32/internal/lsp/cmd/test/cmdtest.go (about)

     1  // Copyright 2019 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package cmdtest contains the test suite for the command line behavior of gopls.
     6  package cmdtest
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"fmt"
    12  	"io"
    13  	"os"
    14  	"path/filepath"
    15  	"strconv"
    16  	"strings"
    17  	"sync"
    18  	"testing"
    19  
    20  	"github.com/april1989/origin-go-tools/go/packages/packagestest"
    21  	"github.com/april1989/origin-go-tools/internal/jsonrpc2/servertest"
    22  	"github.com/april1989/origin-go-tools/internal/lsp/cache"
    23  	"github.com/april1989/origin-go-tools/internal/lsp/cmd"
    24  	"github.com/april1989/origin-go-tools/internal/lsp/debug"
    25  	"github.com/april1989/origin-go-tools/internal/lsp/lsprpc"
    26  	"github.com/april1989/origin-go-tools/internal/lsp/protocol"
    27  	"github.com/april1989/origin-go-tools/internal/lsp/source"
    28  	"github.com/april1989/origin-go-tools/internal/lsp/tests"
    29  	"github.com/april1989/origin-go-tools/internal/span"
    30  	"github.com/april1989/origin-go-tools/internal/tool"
    31  )
    32  
    33  type runner struct {
    34  	exporter    packagestest.Exporter
    35  	data        *tests.Data
    36  	ctx         context.Context
    37  	options     func(*source.Options)
    38  	normalizers []normalizer
    39  	remote      string
    40  }
    41  
    42  type normalizer struct {
    43  	path     string
    44  	slashed  string
    45  	escaped  string
    46  	fragment string
    47  }
    48  
    49  func TestCommandLine(testdata string, options func(*source.Options)) func(*testing.T, packagestest.Exporter) {
    50  	return func(t *testing.T, exporter packagestest.Exporter) {
    51  		if stat, err := os.Stat(testdata); err != nil || !stat.IsDir() {
    52  			t.Skip("testdata directory not present")
    53  		}
    54  		ctx := tests.Context(t)
    55  		ts := NewTestServer(ctx, options)
    56  		data := tests.Load(t, exporter, testdata)
    57  		for _, datum := range data {
    58  			defer datum.Exported.Cleanup()
    59  			t.Run(tests.FormatFolderName(datum.Folder), func(t *testing.T) {
    60  				t.Helper()
    61  				tests.Run(t, NewRunner(exporter, datum, ctx, ts.Addr, options), datum)
    62  			})
    63  		}
    64  		cmd.CloseTestConnections(ctx)
    65  	}
    66  }
    67  
    68  func NewTestServer(ctx context.Context, options func(*source.Options)) *servertest.TCPServer {
    69  	ctx = debug.WithInstance(ctx, "", "")
    70  	cache := cache.New(ctx, options)
    71  	ss := lsprpc.NewStreamServer(cache, false)
    72  	return servertest.NewTCPServer(ctx, ss, nil)
    73  }
    74  
    75  func NewRunner(exporter packagestest.Exporter, data *tests.Data, ctx context.Context, remote string, options func(*source.Options)) *runner {
    76  	r := &runner{
    77  		exporter:    exporter,
    78  		data:        data,
    79  		ctx:         ctx,
    80  		options:     options,
    81  		normalizers: make([]normalizer, 0, len(data.Exported.Modules)),
    82  		remote:      remote,
    83  	}
    84  	// build the path normalizing patterns
    85  	for _, m := range data.Exported.Modules {
    86  		for fragment := range m.Files {
    87  			n := normalizer{
    88  				path:     data.Exported.File(m.Name, fragment),
    89  				fragment: fragment,
    90  			}
    91  			if n.slashed = filepath.ToSlash(n.path); n.slashed == n.path {
    92  				n.slashed = ""
    93  			}
    94  			quoted := strconv.Quote(n.path)
    95  			if n.escaped = quoted[1 : len(quoted)-1]; n.escaped == n.path {
    96  				n.escaped = ""
    97  			}
    98  			r.normalizers = append(r.normalizers, n)
    99  		}
   100  	}
   101  	return r
   102  }
   103  
   104  func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) {
   105  	//TODO: add command line completions tests when it works
   106  }
   107  
   108  func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
   109  	//TODO: add command line completions tests when it works
   110  }
   111  
   112  func (r *runner) CompletionSnippet(t *testing.T, src span.Span, expected tests.CompletionSnippet, placeholders bool, items tests.CompletionItems) {
   113  	//TODO: add command line completions tests when it works
   114  }
   115  
   116  func (r *runner) UnimportedCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
   117  	//TODO: add command line completions tests when it works
   118  }
   119  
   120  func (r *runner) DeepCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
   121  	//TODO: add command line completions tests when it works
   122  }
   123  
   124  func (r *runner) FuzzyCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
   125  	//TODO: add command line completions tests when it works
   126  }
   127  
   128  func (r *runner) CaseSensitiveCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
   129  	//TODO: add command line completions tests when it works
   130  }
   131  
   132  func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
   133  	//TODO: add command line completions tests when it works
   134  }
   135  
   136  func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span) {
   137  	//TODO: function extraction not supported on command line
   138  }
   139  
   140  func (r *runner) runGoplsCmd(t testing.TB, args ...string) (string, string) {
   141  	rStdout, wStdout, err := os.Pipe()
   142  	if err != nil {
   143  		t.Fatal(err)
   144  	}
   145  	oldStdout := os.Stdout
   146  	rStderr, wStderr, err := os.Pipe()
   147  	if err != nil {
   148  		t.Fatal(err)
   149  	}
   150  	oldStderr := os.Stderr
   151  	stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
   152  	var wg sync.WaitGroup
   153  	wg.Add(2)
   154  	go func() {
   155  		io.Copy(stdout, rStdout)
   156  		wg.Done()
   157  	}()
   158  	go func() {
   159  		io.Copy(stderr, rStderr)
   160  		wg.Done()
   161  	}()
   162  	os.Stdout, os.Stderr = wStdout, wStderr
   163  	app := cmd.New("gopls-test", r.data.Config.Dir, r.data.Exported.Config.Env, r.options)
   164  	remote := r.remote
   165  	err = tool.Run(tests.Context(t),
   166  		app,
   167  		append([]string{fmt.Sprintf("-remote=internal@%s", remote)}, args...))
   168  	if err != nil {
   169  		fmt.Fprint(os.Stderr, err)
   170  	}
   171  	wStdout.Close()
   172  	wStderr.Close()
   173  	wg.Wait()
   174  	os.Stdout, os.Stderr = oldStdout, oldStderr
   175  	rStdout.Close()
   176  	rStderr.Close()
   177  	return stdout.String(), stderr.String()
   178  }
   179  
   180  // NormalizeGoplsCmd runs the gopls command and normalizes its output.
   181  func (r *runner) NormalizeGoplsCmd(t testing.TB, args ...string) (string, string) {
   182  	stdout, stderr := r.runGoplsCmd(t, args...)
   183  	return r.Normalize(stdout), r.Normalize(stderr)
   184  }
   185  
   186  // NormalizePrefix normalizes a single path at the front of the input string.
   187  func (r *runner) NormalizePrefix(s string) string {
   188  	for _, n := range r.normalizers {
   189  		if t := strings.TrimPrefix(s, n.path); t != s {
   190  			return n.fragment + t
   191  		}
   192  		if t := strings.TrimPrefix(s, n.slashed); t != s {
   193  			return n.fragment + t
   194  		}
   195  		if t := strings.TrimPrefix(s, n.escaped); t != s {
   196  			return n.fragment + t
   197  		}
   198  	}
   199  	return s
   200  }
   201  
   202  // Normalize replaces all paths present in s with just the fragment portion
   203  // this is used to make golden files not depend on the temporary paths of the files
   204  func (r *runner) Normalize(s string) string {
   205  	type entry struct {
   206  		path     string
   207  		index    int
   208  		fragment string
   209  	}
   210  	match := make([]entry, 0, len(r.normalizers))
   211  	// collect the initial state of all the matchers
   212  	for _, n := range r.normalizers {
   213  		index := strings.Index(s, n.path)
   214  		if index >= 0 {
   215  			match = append(match, entry{n.path, index, n.fragment})
   216  		}
   217  		if n.slashed != "" {
   218  			index := strings.Index(s, n.slashed)
   219  			if index >= 0 {
   220  				match = append(match, entry{n.slashed, index, n.fragment})
   221  			}
   222  		}
   223  		if n.escaped != "" {
   224  			index := strings.Index(s, n.escaped)
   225  			if index >= 0 {
   226  				match = append(match, entry{n.escaped, index, n.fragment})
   227  			}
   228  		}
   229  	}
   230  	// result should be the same or shorter than the input
   231  	buf := bytes.NewBuffer(make([]byte, 0, len(s)))
   232  	last := 0
   233  	for {
   234  		// find the nearest path match to the start of the buffer
   235  		next := -1
   236  		nearest := len(s)
   237  		for i, c := range match {
   238  			if c.index >= 0 && nearest > c.index {
   239  				nearest = c.index
   240  				next = i
   241  			}
   242  		}
   243  		// if there are no matches, we copy the rest of the string and are done
   244  		if next < 0 {
   245  			buf.WriteString(s[last:])
   246  			return buf.String()
   247  		}
   248  		// we have a match
   249  		n := &match[next]
   250  		// copy up to the start of the match
   251  		buf.WriteString(s[last:n.index])
   252  		// skip over the filename
   253  		last = n.index + len(n.path)
   254  		// add in the fragment instead
   255  		buf.WriteString(n.fragment)
   256  		// see what the next match for this path is
   257  		n.index = strings.Index(s[last:], n.path)
   258  		if n.index >= 0 {
   259  			n.index += last
   260  		}
   261  	}
   262  }