Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions internal/frontend/lsp/debug_cross_document_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package lsp

import (
"context"
"errors"
"path/filepath"
"strings"
"testing"

"go.lsp.dev/jsonrpc2"
"go.lsp.dev/protocol"
"go.lsp.dev/uri"
)

// A view debugs the behavior it exposes from another document, performed by an
// object from a third; edits there move or end the session, duplicates are refused.
func TestDebugTargetDeclaredInAnotherDocument(t *testing.T) {
ctx := context.Background()
split := strings.Index(debugMachine, "package MachineViews")
machines, views := debugMachine[:split], debugMachine[split:]
dir := t.TempDir()
open := func(t *testing.T, s *Server, name, src string) uri.URI {
t.Helper()
docURI := uri.File(filepath.Join(dir, name))
if err := s.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
TextDocument: protocol.TextDocumentItem{URI: docURI, LanguageID: "sysml", Version: 1, Text: src},
}); err != nil {
t.Fatalf("DidOpen %s: %v", name, err)
}
return docURI
}
s, viewsURI, rec := debugServer(t, "views.sysml", views)
machinesURI := open(t, s, "machines.sysml", machines)
open(t, s, "robots.sysml", "package Robots { part def Rover { exhibit state ops : Machines::Ops; } }\n")
r := render(t, s, viewsURI, "MachineViews::opsView")
id := ids(t, r)

snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{
TextDocument: protocol.TextDocumentIdentifier{URI: viewsURI},
View: "MachineViews::opsView", Target: "Machines::Ops", Object: "Robots::Rover",
})
if snap.Target != "Machines::Ops" || snap.Object != "Robots::Rover" || snap.Root != r.Nodes[0].ID {
t.Fatalf("start snapshot = %s", describe(snap))
}
wantState(t, snap, debugWaiting)
wantStrings(t, "initial active states", snap.ActiveStates, []string{id["motion"], id["idle"], id["clock"], id["waiting"]})

// An edit to the machine's document that leaves the machine as declared
// keeps the session running; one rewriting the machine ends it.
s.applyDidChange(ctx, uriToName(machinesURI), []rawContentChange{{Text: strings.Replace(machines, "attribute def Halt;", "attribute def Halt;\n\tattribute def Stop;", 1)}}, 2)
for _, moved := range rec.debugChanged() {
if moved.Session != snap.Session || moved.State == debugEnded {
t.Fatalf("debugChanged after an unrelated edit to the machine's document = %s", describe(moved))
}
wantStrings(t, "active states after an unrelated edit", moved.ActiveStates, snap.ActiveStates)
}
s.applyDidChange(ctx, uriToName(machinesURI), []rawContentChange{{Text: strings.Replace(machines, "state elapsed;", "state elapsed;\n\t\tstate cooling;", 1)}}, 3)
changed := rec.debugChanged()
last := changed[len(changed)-1]
if last.Session != snap.Session || last.State != debugEnded || !strings.Contains(last.Reason, "Machines::Ops was edited") {
t.Fatalf("debugChanged after the machine was edited = %s", describe(last))
}

// Closing the machine's document ends a session on it too.
snap = mustDebug(t, s, MethodDebugStart, &debugStartParams{
TextDocument: protocol.TextDocumentIdentifier{URI: viewsURI}, View: "MachineViews::opsView", Target: "Machines::Ops",
})
if err := s.DidClose(ctx, &protocol.DidCloseTextDocumentParams{TextDocument: protocol.TextDocumentIdentifier{URI: machinesURI}}); err != nil {
t.Fatalf("DidClose: %v", err)
}
changed = rec.debugChanged()
last = changed[len(changed)-1]
if last.Session != snap.Session || last.State != debugEnded || !strings.Contains(last.Reason, "machines.sysml was closed") {
t.Fatalf("debugChanged after closing the machine's document = %s", describe(last))
}

// The same name declared by two documents names neither.
open(t, s, "machines.sysml", machines)
spareURI := open(t, s, "spare.sysml", machines)
_, err := s.DebugStart(&debugStartParams{
TextDocument: protocol.TextDocumentIdentifier{URI: viewsURI}, View: "MachineViews::opsView", Target: "Machines::Ops",
})
if !errors.Is(err, ErrDebugTarget) || !strings.Contains(err.Error(), machinesURI.Filename()+", "+spareURI.Filename()) {
t.Fatalf("start of an ambiguous target: err = %v, want %v naming both documents", err, ErrDebugTarget)
}
var wire *jsonrpc2.Error
if !errors.As(err, &wire) || wire.Code != jsonrpc2.InvalidParams {
t.Errorf("err = %v, want an InvalidParams reply", err)
}
}
154 changes: 39 additions & 115 deletions internal/frontend/lsp/debug_test.go

Large diffs are not rendered by default.

33 changes: 17 additions & 16 deletions internal/frontend/lsp/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@ import (
"time"

"github.com/Open-MBEE/OpenSysML/internal/workspace/model"
"go.lsp.dev/uri"
)

// These tests drive textDocument/didChange with the framed JSON an editor sends,
// where an omitted range (full replace) and a {0,0}-{0,0} range (insert) differ.

const wireURI = "file:///tmp/wire.sysml"

const wireSource = "package Vehicles {\n part def Wheel;\n part w : Wheel;\n}\n"

// wireSession is a served Server reached only through its framed stream;
// a reader goroutine hands messages to the test, which picks them by id or method.
type wireSession struct {
t *testing.T
s *Server
uri string
client net.Conn
msgs chan map[string]any
readErr chan error
Expand Down Expand Up @@ -55,6 +55,7 @@ func newWireSession(t *testing.T) *wireSession {
w := &wireSession{
t: t,
s: s,
uri: string(tempDocURI(t, "wire.sysml")),
client: client,
msgs: make(chan map[string]any, 256),
readErr: make(chan error, 1),
Expand Down Expand Up @@ -155,42 +156,42 @@ func (w *wireSession) next(what string, want func(map[string]any) bool) map[stri
}
}

// open initializes the session and opens wireURI with text, consuming its diagnostics.
// open initializes the session and opens w.uri with text, consuming its diagnostics.
func (w *wireSession) open(text string) {
w.t.Helper()
w.call("initialize", `{"capabilities":{}}`)
w.notify("initialized", `{}`)
w.notify("textDocument/didOpen", fmt.Sprintf(
`{"textDocument":{"uri":%q,"languageId":"sysml","version":1,"text":%s}}`, wireURI, jsonString(text)))
if diags := w.awaitDiagnostics(wireURI); len(diags) != 0 {
`{"textDocument":{"uri":%q,"languageId":"sysml","version":1,"text":%s}}`, w.uri, jsonString(text)))
if diags := w.awaitDiagnostics(w.uri); len(diags) != 0 {
w.t.Fatalf("didOpen published %v, want a clean document to start from", diags)
}
}

// didChange sends the given JSON change objects verbatim for wireURI and
// didChange sends the given JSON change objects verbatim for w.uri and
// returns the diagnostics published for the result.
func (w *wireSession) didChange(version int, changes ...string) []any {
w.t.Helper()
w.notify("textDocument/didChange", fmt.Sprintf(
`{"textDocument":{"uri":%q,"version":%d},"contentChanges":[%s]}`,
wireURI, version, strings.Join(changes, ",")))
return w.awaitDiagnostics(wireURI)
w.uri, version, strings.Join(changes, ",")))
return w.awaitDiagnostics(w.uri)
}

// content is the text the workspace holds for wireURI.
// content is the text the workspace holds for w.uri.
func (w *wireSession) content() string {
w.t.Helper()
doc := w.s.ws.Document(uriToName(wireURI))
doc := w.s.ws.Document(uriToName(uri.URI(w.uri)))
if doc == nil {
w.t.Fatalf("%s is not in the workspace", wireURI)
w.t.Fatalf("%s is not in the workspace", w.uri)
}
return string(doc.Content)
}

// symbolNames returns the top-level documentSymbol names of wireURI.
// symbolNames returns the top-level documentSymbol names of w.uri.
func (w *wireSession) symbolNames() []string {
w.t.Helper()
result := w.call("textDocument/documentSymbol", fmt.Sprintf(`{"textDocument":{"uri":%q}}`, wireURI))
result := w.call("textDocument/documentSymbol", fmt.Sprintf(`{"textDocument":{"uri":%q}}`, w.uri))
symbols, ok := result.([]any)
if !ok {
w.t.Fatalf("documentSymbol result = %v, want an array", result)
Expand All @@ -208,7 +209,7 @@ func (w *wireSession) symbolNames() []string {
func (w *wireSession) hoverText(line, character int) string {
w.t.Helper()
result := w.call("textDocument/hover", fmt.Sprintf(
`{"textDocument":{"uri":%q},"position":{"line":%d,"character":%d}}`, wireURI, line, character))
`{"textDocument":{"uri":%q},"position":{"line":%d,"character":%d}}`, w.uri, line, character))
if result == nil {
return ""
}
Expand Down Expand Up @@ -319,7 +320,7 @@ func TestWireDidChangeMixedBatchAppliesInOrder(t *testing.T) {
if got := w.content(); got != want {
t.Errorf("content = %q, want %q", got, want)
}
result := w.call("textDocument/documentSymbol", fmt.Sprintf(`{"textDocument":{"uri":%q}}`, wireURI))
result := w.call("textDocument/documentSymbol", fmt.Sprintf(`{"textDocument":{"uri":%q}}`, w.uri))
symbols, _ := result.([]any)
if len(symbols) != 1 {
t.Fatalf("documentSymbol = %v, want the one package Final", result)
Expand Down Expand Up @@ -384,7 +385,7 @@ func TestWireRangeFormattingAnswersEditsOnSelectedLines(t *testing.T) {
request := func(startLine, endLine int) []any {
result := w.call("textDocument/rangeFormatting", fmt.Sprintf(
`{"textDocument":{"uri":%q},"range":{"start":{"line":%d,"character":0},"end":{"line":%d,"character":0}},"options":{"tabSize":4,"insertSpaces":true}}`,
wireURI, startLine, endLine))
w.uri, startLine, endLine))
if result == nil {
return nil
}
Expand Down
4 changes: 1 addition & 3 deletions internal/frontend/lsp/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"testing"

"go.lsp.dev/protocol"
"go.lsp.dev/uri"

"github.com/Open-MBEE/OpenSysML/internal/workspace/model"
)
Expand All @@ -29,9 +28,8 @@ func TestLSPEndToEnd(t *testing.T) {
}

// Open a document.
name := "e2e.sysml"
src := "package P { namespace N; }\nimport P::N;\n"
docURI := uri.File(name)
docURI := tempDocURI(t, "e2e.sysml")
if err := s.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
TextDocument: protocol.TextDocumentItem{
URI: docURI,
Expand Down
10 changes: 5 additions & 5 deletions internal/frontend/lsp/modeledit_cross_document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const (
func engineWorkspace(t *testing.T) (s *Server, viewsURI, partsURI uri.URI) {
t.Helper()
s, viewsURI = renderServer(t, "views.sysml", engineViews)
partsURI = uri.File("parts.sysml")
partsURI = tempDocURI(t, "parts.sysml")
openDoc(t, s, partsURI, engineParts)
return s, viewsURI, partsURI
}
Expand Down Expand Up @@ -93,7 +93,7 @@ func TestRenderLocatesNodesInTheDocumentDeclaringThem(t *testing.T) {
func TestRenderMarksWhatEachDocumentsNodesAdmit(t *testing.T) {
const views = "package EngineViews {\n\tprivate import Views::*;\n\tprivate import StandardViewDefinitions::*;\n\n\tpart def Mount;\n\tview engineView : InterconnectionView {\n\t\texpose Mount;\n\t\texpose Machinery::Engine;\n\t\texpose Parts::Part;\n\t}\n}\n"
s, viewsURI := renderServer(t, "views.sysml", views)
partsURI := uri.File("parts.sysml")
partsURI := tempDocURI(t, "parts.sysml")
openDoc(t, s, partsURI, engineParts)
drawn := render(t, s, viewsURI, "EngineViews::engineView")

Expand Down Expand Up @@ -137,7 +137,7 @@ func TestRenderMarksWhatEachDocumentsNodesAdmit(t *testing.T) {
func TestRenderNamesOnlyTheRequestedDocumentsDeclarationsToAClientWithoutTheCapability(t *testing.T) {
const views = "package EngineViews {\n\tprivate import Views::*;\n\tprivate import StandardViewDefinitions::*;\n\n\tpart def Mount;\n\tview engineView : InterconnectionView {\n\t\texpose Mount;\n\t\texpose Machinery::Engine;\n\t}\n}\n"
s := initializedServer(t, nil)
viewsURI, partsURI := uri.File("views.sysml"), uri.File("parts.sysml")
viewsURI, partsURI := tempDocURI(t, "views.sysml"), tempDocURI(t, "parts.sysml")
openDoc(t, s, viewsURI, views)
openDoc(t, s, partsURI, engineParts)
drawn := render(t, s, viewsURI, "EngineViews::engineView")
Expand Down Expand Up @@ -296,7 +296,7 @@ func TestApplyModelEditPlacesInlineIntoTheDeclaringDocument(t *testing.T) {
const motor = "package Motors {\n\tstate def Motor {\n\t\tstate off;\n\t\tstate on;\n\t\ttransition first off then on;\n\t}\n}\n"
const fleet = "package Fleet {\n\tstate motor : Motors::Motor;\n}\n"
s, fleetURI := renderServer(t, "fleet.sysml", fleet)
motorURI := uri.File("motor.sysml")
motorURI := tempDocURI(t, "motor.sysml")
openDoc(t, s, motorURI, motor)
tellChanged(t, s, motorURI, motor, 2)

Expand Down Expand Up @@ -413,7 +413,7 @@ func TestApplyModelEditLayoutIntoDiskAndLibraryDocuments(t *testing.T) {
func TestApplyModelEditLayoutRefusesWhenAnotherDocumentBecomesInvalid(t *testing.T) {
shadowed := strings.Replace(engineViews, "\tview engineView", "\tpart def DiagramLayout;\n\tview engineView", 1)
s, partsURI := renderServer(t, "parts.sysml", engineParts)
viewsURI := uri.File("views.sysml")
viewsURI := tempDocURI(t, "views.sysml")
openDoc(t, s, viewsURI, shadowed)

out := applyModelEdit(t, s, partsURI, 1,
Expand Down
8 changes: 4 additions & 4 deletions internal/frontend/lsp/modeledit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ func TestApplyModelEditRefusesImpossibleMoves(t *testing.T) {
t.Errorf("clashing move = %+v, want member-name-taken", out)
}
s, docURI = renderServer(t, "vehicle.sysml", editModel)
fleetURI := uri.File("fleet.sysml")
fleetURI := tempDocURI(t, "fleet.sysml")
openDoc(t, s, fleetURI, "package Fleet {\n part truck : Vehicle::Car;\n}\n")
out = applyModelEdit(t, s, docURI, 1, ModelEditOperation{Kind: EditMove, Target: "Vehicle::Car", Owner: "Vehicle::Tank"})
if len(out.Refused) != 1 || out.Refused[0].Failure != "referenced-elsewhere" {
Expand Down Expand Up @@ -553,7 +553,7 @@ const fleetModel = "package Fleet {\n part truck : Vehicle::Car;\n part va
// the version the server holds for it.
func TestApplyModelEditRenameRespellsAnotherDocument(t *testing.T) {
s, docURI := renderServer(t, "vehicle.sysml", editModel)
fleetURI := uri.File("fleet.sysml")
fleetURI := tempDocURI(t, "fleet.sysml")
openDoc(t, s, fleetURI, fleetModel)
op := ModelEditOperation{Kind: EditRename, Target: "Vehicle::Car", NewName: "Auto"}
out := applyModelEdit(t, s, docURI, 1, op)
Expand Down Expand Up @@ -581,7 +581,7 @@ func TestApplyModelEditRenameRespellsAnotherDocument(t *testing.T) {
// without cascade it is refused, naming them by document.
func TestApplyModelEditDeleteCascadesIntoAnotherDocument(t *testing.T) {
s, docURI := renderServer(t, "vehicle.sysml", editModel)
fleetURI := uri.File("fleet.sysml")
fleetURI := tempDocURI(t, "fleet.sysml")
openDoc(t, s, fleetURI, fleetModel)

out := applyModelEdit(t, s, docURI, 1, ModelEditOperation{Kind: EditDelete, Target: "Vehicle::Car"})
Expand Down Expand Up @@ -622,7 +622,7 @@ func TestApplyModelEditDeleteCascadesIntoAnotherDocument(t *testing.T) {
// document read from disk, which no client versions, carries none.
func TestApplyModelEditVersionsOtherDocumentsAsHeld(t *testing.T) {
s, docURI := renderServer(t, "vehicle.sysml", editModel)
fleetURI := uri.File("fleet.sysml")
fleetURI := tempDocURI(t, "fleet.sysml")
openDoc(t, s, fleetURI, fleetModel)
sendDidChange(t, s, fleetURI, 3, []json.RawMessage{json.RawMessage(`{"text":"package Fleet {\n part van : Vehicle::Car;\n}\n"}`)})
depotName := uri.File("depot.sysml").Filename()
Expand Down
Loading
Loading