diff --git a/internal/langserver/handlers/cancel_request_test.go b/internal/langserver/handlers/cancel_request_test.go new file mode 100644 index 000000000..618215f07 --- /dev/null +++ b/internal/langserver/handlers/cancel_request_test.go @@ -0,0 +1,77 @@ +package handlers + +import ( + "context" + "fmt" + "log" + "sync" + "testing" + "time" + + "github.com/creachadair/jrpc2" + "github.com/creachadair/jrpc2/handler" + "github.com/hashicorp/terraform-ls/internal/langserver" +) + +func TestLangServer_cancelRequest(t *testing.T) { + tmpDir := TempDir(t) + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + AdditionalHandlers: map[string]handler.Func{ + "$/sleepTicker": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { + ticker := time.NewTicker(100 * time.Millisecond) + + ctx, cancelFunc := context.WithTimeout(ctx, 1*time.Second) + t.Cleanup(cancelFunc) + + var wg sync.WaitGroup + wg.Add(1) + go func(ctx context.Context) { + defer wg.Done() + for { + select { + case <-ctx.Done(): + ticker.Stop() + return + case <-ticker.C: + log.Printf("tick at %s", time.Now()) + } + } + }(ctx) + wg.Wait() + + return nil, ctx.Err() + }, + }, + })) + stop := ls.Start(t) + defer stop() + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": {}, + "rootUri": %q, + "processId": 12345 + }`, tmpDir.URI())}) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + ls.CallAndExpectError(t, &langserver.CallRequest{ + Method: "$/sleepTicker", + ReqParams: `{}`, + }, context.Canceled) + }() + time.Sleep(100 * time.Millisecond) + ls.Call(t, &langserver.CallRequest{ + Method: "$/cancelRequest", + ReqParams: fmt.Sprintf(`{"id": %d}`, 2), + }) + wg.Wait() +} diff --git a/internal/langserver/handlers/service.go b/internal/langserver/handlers/service.go index 84d7d898b..9f39ab337 100644 --- a/internal/langserver/handlers/service.go +++ b/internal/langserver/handlers/service.go @@ -42,6 +42,8 @@ type service struct { newWalker module.WalkerFactory tfDiscoFunc discovery.DiscoveryFunc tfExecFactory exec.ExecutorFactory + + additionalHandlers map[string]rpch.Func } var discardLogs = log.New(ioutil.Discard, "", 0) @@ -344,6 +346,13 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { }, } + // For use in tests, e.g. to test request cancellation + if len(svc.additionalHandlers) > 0 { + for methodName, handlerFunc := range svc.additionalHandlers { + m[methodName] = handlerFunc + } + } + return convertMap(m), nil } diff --git a/internal/langserver/handlers/session_mock_test.go b/internal/langserver/handlers/session_mock_test.go index 706e1d910..4d717ade0 100644 --- a/internal/langserver/handlers/session_mock_test.go +++ b/internal/langserver/handlers/session_mock_test.go @@ -8,6 +8,7 @@ import ( "sync" "testing" + "github.com/creachadair/jrpc2/handler" "github.com/hashicorp/terraform-ls/internal/filesystem" "github.com/hashicorp/terraform-ls/internal/langserver/session" "github.com/hashicorp/terraform-ls/internal/terraform/discovery" @@ -16,8 +17,9 @@ import ( ) type MockSessionInput struct { - Filesystem filesystem.Filesystem - TerraformCalls *exec.TerraformMockCalls + Filesystem filesystem.Filesystem + TerraformCalls *exec.TerraformMockCalls + AdditionalHandlers map[string]handler.Func } type mockSession struct { @@ -40,10 +42,13 @@ func (ms *mockSession) new(srvCtx context.Context) session.Session { } var fs filesystem.Filesystem - if ms.mockInput != nil && ms.mockInput.Filesystem != nil { - fs = ms.mockInput.Filesystem - } else { - fs = filesystem.NewFilesystem() + fs = filesystem.NewFilesystem() + var handlers map[string]handler.Func + if ms.mockInput != nil { + if ms.mockInput.Filesystem != nil { + fs = ms.mockInput.Filesystem + } + handlers = ms.mockInput.AdditionalHandlers } var tfCalls *exec.TerraformMockCalls @@ -56,16 +61,17 @@ func (ms *mockSession) new(srvCtx context.Context) session.Session { } svc := &service{ - logger: testLogger(), - srvCtx: srvCtx, - sessCtx: sessCtx, - stopSession: ms.stop, - fs: fs, - newModuleManager: module.NewModuleManagerMock(input), - newWatcher: module.MockWatcher(), - newWalker: module.SyncWalker, - tfDiscoFunc: d.LookPath, - tfExecFactory: exec.NewMockExecutor(tfCalls), + logger: testLogger(), + srvCtx: srvCtx, + sessCtx: sessCtx, + stopSession: ms.stop, + fs: fs, + newModuleManager: module.NewModuleManagerMock(input), + newWatcher: module.MockWatcher(), + newWalker: module.SyncWalker, + tfDiscoFunc: d.LookPath, + tfExecFactory: exec.NewMockExecutor(tfCalls), + additionalHandlers: handlers, } return svc