Skip to content

Commit 33d7ce0

Browse files
Add LSP hover tests (#4254)
1 parent 5fb6a08 commit 33d7ce0

File tree

4 files changed

+397
-0
lines changed

4 files changed

+397
-0
lines changed

private/buf/buflsp/buflsp_test.go

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// Copyright 2020-2025 Buf Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package buflsp_test
16+
17+
import (
18+
"context"
19+
"net"
20+
"net/http"
21+
"os"
22+
"path/filepath"
23+
"testing"
24+
25+
"buf.build/go/app"
26+
"buf.build/go/app/appext"
27+
"github.com/bufbuild/buf/private/buf/bufctl"
28+
"github.com/bufbuild/buf/private/buf/buflsp"
29+
"github.com/bufbuild/buf/private/buf/bufwkt/bufwktstore"
30+
"github.com/bufbuild/buf/private/bufpkg/bufmodule"
31+
"github.com/bufbuild/buf/private/bufpkg/bufparse"
32+
"github.com/bufbuild/buf/private/bufpkg/bufplugin"
33+
"github.com/bufbuild/buf/private/bufpkg/bufpolicy"
34+
"github.com/bufbuild/buf/private/pkg/git"
35+
"github.com/bufbuild/buf/private/pkg/httpauth"
36+
"github.com/bufbuild/buf/private/pkg/slogtestext"
37+
"github.com/bufbuild/buf/private/pkg/storage/storageos"
38+
"github.com/bufbuild/buf/private/pkg/wasm"
39+
"github.com/bufbuild/protocompile/experimental/incremental"
40+
"github.com/stretchr/testify/assert"
41+
"github.com/stretchr/testify/require"
42+
"go.lsp.dev/jsonrpc2"
43+
"go.lsp.dev/protocol"
44+
"go.lsp.dev/uri"
45+
)
46+
47+
// nopModuleKeyProvider is a no-op implementation of ModuleKeyProvider for testing
48+
type nopModuleKeyProvider struct{}
49+
50+
func (nopModuleKeyProvider) GetModuleKeysForModuleRefs(context.Context, []bufparse.Ref, bufmodule.DigestType) ([]bufmodule.ModuleKey, error) {
51+
return nil, os.ErrNotExist
52+
}
53+
54+
// setupLSPServer creates and initializes an LSP server for testing.
55+
// Returns the client JSON-RPC connection and the test file URI.
56+
func setupLSPServer(
57+
t *testing.T,
58+
testProtoPath string,
59+
) (jsonrpc2.Conn, protocol.URI) {
60+
t.Helper()
61+
62+
ctx := t.Context()
63+
64+
logger := slogtestext.NewLogger(t)
65+
66+
appContainer, err := app.NewContainerForOS()
67+
require.NoError(t, err)
68+
69+
nameContainer, err := appext.NewNameContainer(appContainer, "buf-test")
70+
require.NoError(t, err)
71+
appextContainer := appext.NewContainer(nameContainer, logger)
72+
73+
graphProvider := bufmodule.NopGraphProvider
74+
moduleDataProvider := bufmodule.NopModuleDataProvider
75+
commitProvider := bufmodule.NopCommitProvider
76+
pluginKeyProvider := bufplugin.NopPluginKeyProvider
77+
pluginDataProvider := bufplugin.NopPluginDataProvider
78+
policyKeyProvider := bufpolicy.NopPolicyKeyProvider
79+
policyDataProvider := bufpolicy.NopPolicyDataProvider
80+
81+
tmpDir := t.TempDir()
82+
storageBucket, err := storageos.NewProvider().NewReadWriteBucket(tmpDir)
83+
require.NoError(t, err)
84+
85+
wktStore := bufwktstore.NewStore(logger, storageBucket)
86+
87+
controller, err := bufctl.NewController(
88+
logger,
89+
appContainer,
90+
graphProvider,
91+
nopModuleKeyProvider{},
92+
moduleDataProvider,
93+
commitProvider,
94+
pluginKeyProvider,
95+
pluginDataProvider,
96+
policyKeyProvider,
97+
policyDataProvider,
98+
wktStore,
99+
&http.Client{},
100+
httpauth.NewNopAuthenticator(),
101+
git.ClonerOptions{},
102+
)
103+
require.NoError(t, err)
104+
105+
wktBucket, err := wktStore.GetBucket(ctx)
106+
require.NoError(t, err)
107+
108+
wasmRuntime, err := wasm.NewRuntime(ctx)
109+
require.NoError(t, err)
110+
t.Cleanup(func() {
111+
require.NoError(t, wasmRuntime.Close(ctx))
112+
})
113+
114+
queryExecutor := incremental.New()
115+
116+
serverConn, clientConn := net.Pipe()
117+
t.Cleanup(func() {
118+
require.NoError(t, serverConn.Close())
119+
require.NoError(t, clientConn.Close())
120+
})
121+
122+
stream := jsonrpc2.NewStream(serverConn)
123+
124+
go func() {
125+
conn, err := buflsp.Serve(
126+
ctx,
127+
wktBucket,
128+
appextContainer,
129+
controller,
130+
wasmRuntime,
131+
stream,
132+
queryExecutor,
133+
)
134+
if err != nil {
135+
t.Errorf("Failed to start server: %v", err)
136+
return
137+
}
138+
t.Cleanup(func() {
139+
require.NoError(t, conn.Close())
140+
})
141+
<-ctx.Done()
142+
}()
143+
144+
clientStream := jsonrpc2.NewStream(clientConn)
145+
clientJSONConn := jsonrpc2.NewConn(clientStream)
146+
clientJSONConn.Go(ctx, jsonrpc2.AsyncHandler(func(_ context.Context, reply jsonrpc2.Replier, _ jsonrpc2.Request) error {
147+
return reply(ctx, nil, nil)
148+
}))
149+
t.Cleanup(func() {
150+
require.NoError(t, clientJSONConn.Close())
151+
})
152+
153+
testWorkspaceDir := filepath.Dir(testProtoPath)
154+
testURI := uri.New(testProtoPath)
155+
var initResult protocol.InitializeResult
156+
_, initErr := clientJSONConn.Call(ctx, protocol.MethodInitialize, &protocol.InitializeParams{
157+
RootURI: uri.New(testWorkspaceDir),
158+
Capabilities: protocol.ClientCapabilities{
159+
TextDocument: &protocol.TextDocumentClientCapabilities{},
160+
},
161+
}, &initResult)
162+
require.NoError(t, initErr)
163+
assert.True(t, initResult.Capabilities.HoverProvider != nil)
164+
165+
err = clientJSONConn.Notify(ctx, protocol.MethodInitialized, &protocol.InitializedParams{})
166+
require.NoError(t, err)
167+
168+
testProtoContent, err := os.ReadFile(testProtoPath)
169+
require.NoError(t, err)
170+
171+
err = clientJSONConn.Notify(ctx, protocol.MethodTextDocumentDidOpen, &protocol.DidOpenTextDocumentParams{
172+
TextDocument: protocol.TextDocumentItem{
173+
URI: testURI,
174+
LanguageID: "protobuf",
175+
Version: 1,
176+
Text: string(testProtoContent),
177+
},
178+
})
179+
require.NoError(t, err)
180+
181+
return clientJSONConn, testURI
182+
}

private/buf/buflsp/hover_test.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Copyright 2020-2025 Buf Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package buflsp_test
16+
17+
import (
18+
"path/filepath"
19+
"testing"
20+
21+
"github.com/stretchr/testify/assert"
22+
"github.com/stretchr/testify/require"
23+
"go.lsp.dev/protocol"
24+
)
25+
26+
func TestHover(t *testing.T) {
27+
t.Parallel()
28+
29+
// if runtime.GOOS == "windows" {
30+
// t.Skip("Skipping on Windows")
31+
// }
32+
33+
ctx := t.Context()
34+
35+
testProtoPath, err := filepath.Abs("testdata/hover/test.proto")
36+
require.NoError(t, err)
37+
38+
clientJSONConn, testURI := setupLSPServer(t, testProtoPath)
39+
40+
tests := []struct {
41+
name string
42+
line uint32
43+
character uint32
44+
expectedContains string
45+
expectNoHover bool
46+
}{
47+
{
48+
name: "hover_on_user_message",
49+
line: 7, // Line with "message User {"
50+
character: 8, // On the word "User"
51+
expectedContains: "User represents a user in the system",
52+
},
53+
{
54+
name: "hover_on_id_field",
55+
line: 9, // Line with "string id = 1;"
56+
character: 10, // On the word "id"
57+
expectedContains: "The unique identifier for the user",
58+
},
59+
{
60+
name: "hover_on_status_enum",
61+
line: 19, // Line with "enum Status {"
62+
character: 5, // On the word "Status"
63+
expectedContains: "Status represents the current state of a user",
64+
},
65+
{
66+
name: "hover_on_status_active",
67+
line: 24, // Line with "STATUS_ACTIVE = 1;"
68+
character: 2, // On "STATUS_ACTIVE"
69+
expectedContains: "The user is active",
70+
},
71+
{
72+
name: "hover_on_user_service",
73+
line: 31, // Line with "service UserService {"
74+
character: 8, // On "UserService"
75+
expectedContains: "UserService provides operations for managing users",
76+
},
77+
{
78+
name: "hover_on_get_user_rpc",
79+
line: 33, // Line with "rpc GetUser"
80+
character: 6, // On "GetUser"
81+
expectedContains: "GetUser retrieves a user by their ID",
82+
},
83+
{
84+
name: "hover_on_status_type_reference",
85+
line: 15, // Line with "Status status = 3;"
86+
character: 2, // On "Status" type
87+
expectedContains: "Status represents the current state of a user",
88+
},
89+
{
90+
name: "hover_on_user_type_reference",
91+
line: 45, // Line with "User user = 1;"
92+
character: 2, // On "User" type
93+
expectedContains: "User represents a user in the system",
94+
},
95+
{
96+
name: "hover_on_rpc_request_type",
97+
line: 33, // Line with "rpc GetUser(GetUserRequest)"
98+
character: 14, // On "GetUserRequest"
99+
expectedContains: "GetUserRequest is the request message for GetUser",
100+
},
101+
{
102+
name: "hover_on_rpc_response_type",
103+
line: 33, // Line with "returns (GetUserResponse)"
104+
character: 39, // On "GetUserResponse"
105+
expectedContains: "GetUserResponse is the response message for GetUser",
106+
},
107+
{
108+
name: "hover_on_syntax_keyword",
109+
line: 0, // Line with "syntax = "proto3";"
110+
character: 0, // On "syntax"
111+
expectNoHover: true,
112+
},
113+
{
114+
name: "hover_on_proto3_string",
115+
line: 0, // Line with "syntax = "proto3";"
116+
character: 10, // On "proto3"
117+
expectNoHover: true,
118+
},
119+
{
120+
name: "hover_on_package_keyword",
121+
line: 2, // Line with "package example.v1;"
122+
character: 0, // On "package"
123+
expectNoHover: true,
124+
},
125+
{
126+
name: "hover_on_package_name",
127+
line: 2, // Line with "package example.v1;"
128+
character: 8, // On "example"
129+
expectNoHover: true,
130+
},
131+
}
132+
133+
for _, tt := range tests {
134+
t.Run(tt.name, func(t *testing.T) {
135+
t.Parallel()
136+
var hover *protocol.Hover
137+
_, hoverErr := clientJSONConn.Call(ctx, protocol.MethodTextDocumentHover, protocol.HoverParams{
138+
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
139+
TextDocument: protocol.TextDocumentIdentifier{
140+
URI: testURI,
141+
},
142+
Position: protocol.Position{
143+
Line: tt.line,
144+
Character: tt.character,
145+
},
146+
},
147+
}, &hover)
148+
require.NoError(t, hoverErr)
149+
150+
if tt.expectNoHover {
151+
assert.Nil(t, hover, "expected no hover information")
152+
} else if tt.expectedContains != "" {
153+
require.NotNil(t, hover, "expected hover to be non-nil")
154+
assert.Equal(t, protocol.Markdown, hover.Contents.Kind)
155+
assert.Contains(t, hover.Contents.Value, tt.expectedContains)
156+
}
157+
})
158+
}
159+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
version: v2
2+
modules:
3+
- path: .
4+
lint:
5+
use:
6+
- STANDARD
7+
breaking:
8+
use:
9+
- FILE

0 commit comments

Comments
 (0)