diff --git a/internal/frontend/lsp/debug_cross_document_test.go b/internal/frontend/lsp/debug_cross_document_test.go new file mode 100644 index 0000000000..adc95b63bc --- /dev/null +++ b/internal/frontend/lsp/debug_cross_document_test.go @@ -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) + } +} diff --git a/internal/frontend/lsp/debug_test.go b/internal/frontend/lsp/debug_test.go index 92c1165a49..8a0649000e 100644 --- a/internal/frontend/lsp/debug_test.go +++ b/internal/frontend/lsp/debug_test.go @@ -133,7 +133,7 @@ func debugServer(t *testing.T, name, src string) (*Server, uri.URI, *debugRecord rec := &debugRecorder{} s.client = rec s.notifier = rec - docURI := uri.File(name) + docURI := testDocURI(t, name) if err := s.DidOpen(context.Background(), &protocol.DidOpenTextDocumentParams{ TextDocument: protocol.TextDocumentItem{URI: docURI, LanguageID: "sysml", Version: 1, Text: src}, }); err != nil { @@ -358,7 +358,7 @@ func TestDebugProtocolShapes(t *testing.T) { // object, a session not live, a node not drawn or a signal not taken is refused // with a typed InvalidParams error. func TestDebugRefusesWhatItCannotRun(t *testing.T) { - s, docURI, _ := debugServer(t, "/w/m.sysml", debugMachine+` + s, docURI, _ := debugServer(t, "m.sysml", debugMachine+` package Other { part def Widget; view widgets : TreeView { expose Other::Widget; } @@ -435,7 +435,7 @@ package Other { // A state machine session reports active states per region, fired transitions // and queued events in render IDs; signals, the clock and breakpoints drive it. func TestDebugStateMachineSession(t *testing.T) { - s, docURI, _ := debugServer(t, "/w/m.sysml", debugMachine) + s, docURI, _ := debugServer(t, "m.sysml", debugMachine) r := render(t, s, docURI, "MachineViews::opsView") id := ids(t, r) edge := func(from, to string) string { return id[from] + "->" + id[to] } @@ -577,7 +577,7 @@ package MachineViews { // An entry transition into a nested state is taken from the start marker of the // body it is written in — the machine's — not that of the state it lands in. func TestDebugEntryTransitionIntoANestedState(t *testing.T) { - s, docURI, _ := debugServer(t, "/w/m.sysml", debugDeep) + s, docURI, _ := debugServer(t, "m.sysml", debugDeep) r := render(t, s, docURI, "MachineViews::deepView") id := ids(t, r) var machineStart string @@ -601,7 +601,7 @@ func TestDebugEntryTransitionIntoANestedState(t *testing.T) { // A continue runs what is due now and holds the clock: an event due later // leaves the machine waiting, at the same instant, until an advance reaches it. func TestDebugContinueHoldsTheClock(t *testing.T) { - s, docURI, _ := debugServer(t, "/w/m.sysml", debugMachine) + s, docURI, _ := debugServer(t, "m.sysml", debugMachine) id := ids(t, render(t, s, docURI, "MachineViews::opsView")) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, @@ -670,7 +670,7 @@ package TransientViews { // An advance pauses at a breakpoint state as it becomes active, a state left // again at the same instant included, the clock held at that instant. func TestDebugAdvancePausesOnATransientState(t *testing.T) { - s, docURI, _ := debugServer(t, "/w/t.sysml", debugTransient) + s, docURI, _ := debugServer(t, "t.sysml", debugTransient) id := ids(t, render(t, s, docURI, "TransientViews::descentView")) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, @@ -759,7 +759,7 @@ package BViews { // does: the machine's own Go rather than the first Go declared in the document, // and a signal declared in its body; a qualified name still names what it says. func TestDebugSendResolvesSignalInTargetScope(t *testing.T) { - s, docURI, _ := debugServer(t, "/w/s.sysml", debugSameNamedSignals) + s, docURI, _ := debugServer(t, "s.sysml", debugSameNamedSignals) id := ids(t, render(t, s, docURI, "BViews::machineView")) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, @@ -792,7 +792,7 @@ func TestDebugSendResolvesSignalInTargetScope(t *testing.T) { // A breakpoint on a pseudostate pauses the run once the dispatch routed through // it completes, the pseudostate reported as where it paused. func TestDebugBreakpointOnAPseudostatePauses(t *testing.T) { - s, docURI, _ := debugServer(t, "/w/j.sysml", debugJunction) + s, docURI, _ := debugServer(t, "j.sysml", debugJunction) id := ids(t, render(t, s, docURI, "RoutingViews::opsView")) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, @@ -820,7 +820,7 @@ func TestDebugBreakpointOnAPseudostatePauses(t *testing.T) { // A breakpoint on the machine's `done` pauses the run standing on it, before the // machine completes; the next step completes it. func TestDebugBreakpointOnDonePausesBeforeCompletion(t *testing.T) { - s, docURI, _ := debugServer(t, "/w/h.sysml", `package Halting { + s, docURI, _ := debugServer(t, "h.sysml", `package Halting { attribute def Halt; state def Runner { entry; then idle; @@ -872,7 +872,7 @@ package HaltingViews { // A step that lands a token on an action breakpoint pauses there, as a continue // does; the step after moves it past the breakpoint. func TestDebugStepPausesAtAnActionBreakpoint(t *testing.T) { - s, docURI, _ := debugServer(t, "/w/f.sysml", debugFlow) + s, docURI, _ := debugServer(t, "f.sysml", debugFlow) id := ids(t, render(t, s, docURI, "FlowViews::driveView")) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, View: "FlowViews::driveView", Target: "Flows::Drive", @@ -904,7 +904,7 @@ func TestDebugStepPausesAtAnActionBreakpoint(t *testing.T) { // on its fork, returning the session and the rendering's IDs. func pausedAtSplit(t *testing.T) (*Server, string, map[string]string) { t.Helper() - s, docURI, _ := debugServer(t, "/w/f.sysml", debugFlow) + s, docURI, _ := debugServer(t, "f.sysml", debugFlow) id := ids(t, render(t, s, docURI, "FlowViews::driveView")) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, View: "FlowViews::driveView", Target: "Flows::Drive", @@ -1004,7 +1004,7 @@ package ReusedViews { // A breakpoint on one drawing of a node a nested flow reuses stops the run at // that occurrence only, not at the same declaration another nested flow runs. func TestDebugBreakpointOnAReusedNodeStopsAtItsOccurrence(t *testing.T) { - s, docURI, _ := debugServer(t, "/w/r.sysml", debugReused) + s, docURI, _ := debugServer(t, "r.sysml", debugReused) r := render(t, s, docURI, "ReusedViews::twiceView") var a, b, lookInA, lookInB string for _, n := range r.Nodes { @@ -1052,7 +1052,7 @@ func TestDebugBreakpointOnAReusedNodeStopsAtItsOccurrence(t *testing.T) { // found on a breakpoint: a token still held at one already stopped at does not // stand in for the one the run has just reached. func TestDebugPausedAtIsTheBreakpointReachedNotTheFirstTokenOnOne(t *testing.T) { - s, docURI, _ := debugServer(t, "/w/f.sysml", debugFlow) + s, docURI, _ := debugServer(t, "f.sysml", debugFlow) id := ids(t, render(t, s, docURI, "FlowViews::driveView")) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, View: "FlowViews::driveView", Target: "Flows::Drive", @@ -1115,7 +1115,7 @@ package CountingViews { // already runs, so its timed effect happens once; an object exhibiting it twice // is ambiguous. func TestDebugStartAttachesToThePerformersMachine(t *testing.T) { - s, docURI, _ := debugServer(t, "/w/c.sysml", debugCounter) + s, docURI, _ := debugServer(t, "c.sysml", debugCounter) id := ids(t, render(t, s, docURI, "CountingViews::opsView")) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, @@ -1143,7 +1143,7 @@ func TestDebugStartAttachesToThePerformersMachine(t *testing.T) { // the last snapshot read from: each snapshot releases what the one before it // reported, so a long-lived session's record stays bounded while the counts go on. func TestDebugSessionReleasesTheHistoryItReports(t *testing.T) { - s, docURI, _ := debugServer(t, "/w/m.sysml", debugMachine) + s, docURI, _ := debugServer(t, "m.sysml", debugMachine) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, View: "MachineViews::opsView", Target: "Machines::Ops", Object: "Machines::Robot", @@ -1165,7 +1165,7 @@ func TestDebugSessionReleasesTheHistoryItReports(t *testing.T) { t.Errorf("after a request firing nothing taken %d, kept %d of %d; want none, none kept, %d counted", len(snap.Taken), len(machine.FiredTransitions()), machine.FiredCount(), fired+1) } - s, docURI, _ = debugServer(t, "/w/f.sysml", debugFlow) + s, docURI, _ = debugServer(t, "f.sysml", debugFlow) snap = mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, View: "FlowViews::driveView", Target: "Flows::Drive", }) @@ -1186,7 +1186,7 @@ func TestDebugSessionReleasesTheHistoryItReports(t *testing.T) { // A machine's queue shows every message pending in its context, a message no // state of the machine accepts now included: it is in flight all the same. func TestDebugMachineQueueShowsEveryPendingMessage(t *testing.T) { - s, docURI, _ := debugServer(t, "/w/m.sysml", debugMachine) + s, docURI, _ := debugServer(t, "m.sysml", debugMachine) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, View: "MachineViews::opsView", Target: "Machines::Ops", Object: "Machines::Robot", @@ -1211,7 +1211,7 @@ func TestDebugMachineQueueShowsEveryPendingMessage(t *testing.T) { // An action session reports every token (forked, nested, parked on the clock or // a signal, held at a join) with the edges it took, in the rendering's IDs. func TestDebugActionSession(t *testing.T) { - s, docURI, _ := debugServer(t, "/w/f.sysml", debugFlow) + s, docURI, _ := debugServer(t, "f.sysml", debugFlow) r := render(t, s, docURI, "FlowViews::driveView") id := ids(t, r) edge := func(from, to string) string { return id[from] + "->" + id[to] } @@ -1320,7 +1320,7 @@ func TestDebugActionSession(t *testing.T) { // An action that fails at a step reports the failure; a run that exceeds its // budget does too, as a failure rather than a hang. func TestDebugActionFailure(t *testing.T) { - s, docURI, _ := debugServer(t, "/w/f.sysml", `package Flows { + s, docURI, _ := debugServer(t, "f.sysml", `package Flows { private import ScalarValues::*; action def Spin { attribute n : Integer = 0; @@ -1372,7 +1372,7 @@ package FlowViews { // An edit leaving the behavior, performer and view as they were moves the session // to the fresh IDs, announced by debugChanged; one to the behavior ends it. func TestDebugSessionFollowsEdits(t *testing.T) { - s, docURI, rec := debugServer(t, "/w/m.sysml", debugMachine) + s, docURI, rec := debugServer(t, "m.sysml", debugMachine) ctx := context.Background() name := uriToName(docURI) r := render(t, s, docURI, "MachineViews::opsView") @@ -1441,7 +1441,7 @@ func TestDebugSessionFollowsEdits(t *testing.T) { // flight finishes at the documents it began with and the next one finds the // session already moved to the edited ones. func TestDebugEditWaitsForTheRequestInFlight(t *testing.T) { - s, docURI, rec := debugServer(t, "/w/m.sysml", debugMachine) + s, docURI, rec := debugServer(t, "m.sysml", debugMachine) name := uriToName(docURI) session := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, @@ -1477,7 +1477,7 @@ func TestDebugEditWaitsForTheRequestInFlight(t *testing.T) { // Every snapshot of a session, answered or notified, takes the next revision, so // a client can tell the newer of two that arrive out of order. func TestDebugSnapshotsAreNumbered(t *testing.T) { - s, docURI, rec := debugServer(t, "/w/m.sysml", debugMachine) + s, docURI, rec := debugServer(t, "m.sysml", debugMachine) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, View: "MachineViews::opsView", Target: "Machines::Ops", Object: "Machines::Robot", @@ -1520,7 +1520,7 @@ func TestDebugSnapshotsAreNumbered(t *testing.T) { // a machine and for an action paused in a nested flow alike. func TestDebugPauseFollowsEdits(t *testing.T) { t.Run("machine", func(t *testing.T) { - s, docURI, rec := debugServer(t, "/w/m.sysml", debugMachine) + s, docURI, rec := debugServer(t, "m.sysml", debugMachine) id := ids(t, render(t, s, docURI, "#state")) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, @@ -1559,7 +1559,7 @@ func TestDebugPauseFollowsEdits(t *testing.T) { wantStrings(t, "active after resuming", snap.ActiveStates, []string{fresh["motion"], fresh["busy"], fresh["working"], fresh["clock"], fresh["waiting"]}) }) t.Run("action", func(t *testing.T) { - s, docURI, rec := debugServer(t, "/w/f.sysml", debugFlow) + s, docURI, rec := debugServer(t, "f.sysml", debugFlow) id := ids(t, render(t, s, docURI, "#action")) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, View: "#action", Target: "Flows::Drive", @@ -1602,7 +1602,7 @@ func TestDebugPauseFollowsEdits(t *testing.T) { // session to the fresh IDs before it answers; one to the behavior — of the same // length, so every span stays put — refuses the start. func TestDebugStartSeesAnEditSinceItsReading(t *testing.T) { - s, docURI, rec := debugServer(t, "/w/m.sysml", debugMachine) + s, docURI, rec := debugServer(t, "m.sysml", debugMachine) ctx := context.Background() name := uriToName(docURI) params := &debugStartParams{ @@ -1692,7 +1692,7 @@ func TestDebugSessionEnds(t *testing.T) { } t.Run("performer edited", func(t *testing.T) { - s, docURI, rec := debugServer(t, "/w/m.sysml", debugMachine) + s, docURI, rec := debugServer(t, "m.sysml", debugMachine) session := start(t, s, docURI) s.applyDidChange(ctx, uriToName(docURI), []rawContentChange{{Text: strings.Replace(debugMachine, "exhibit state ops : Ops;", "exhibit state ops : Ops;\n\t\tattribute id : Integer;", 1)}}, 2) if reason := lastEnd(t, rec, session).Reason; !strings.Contains(reason, "Machines::Robot was edited") { @@ -1700,7 +1700,7 @@ func TestDebugSessionEnds(t *testing.T) { } }) t.Run("view removed", func(t *testing.T) { - s, docURI, rec := debugServer(t, "/w/m.sysml", debugMachine) + s, docURI, rec := debugServer(t, "m.sysml", debugMachine) session := start(t, s, docURI) s.applyDidChange(ctx, uriToName(docURI), []rawContentChange{{Text: strings.Replace(debugMachine, "view opsView : StateTransitionView { expose Machines::Ops; }", "", 1)}}, 2) if reason := lastEnd(t, rec, session).Reason; !strings.Contains(reason, "MachineViews::opsView") { @@ -1708,7 +1708,7 @@ func TestDebugSessionEnds(t *testing.T) { } }) t.Run("view rewritten but still drawing the target", func(t *testing.T) { - s, docURI, rec := debugServer(t, "/w/m.sysml", debugMachine) + s, docURI, rec := debugServer(t, "m.sysml", debugMachine) session := start(t, s, docURI) s.applyDidChange(ctx, uriToName(docURI), []rawContentChange{{Text: strings.Replace(debugMachine, "{ expose Machines::Ops; }", "{ expose Machines::Ops; expose Machines::Robot; }", 1)}}, 2) if reason := lastEnd(t, rec, session).Reason; !strings.Contains(reason, "MachineViews::opsView was edited") { @@ -1716,7 +1716,7 @@ func TestDebugSessionEnds(t *testing.T) { } }) t.Run("pseudo-view outlives edits around the target", func(t *testing.T) { - s, docURI, rec := debugServer(t, "/w/m.sysml", debugMachine) + s, docURI, rec := debugServer(t, "m.sysml", debugMachine) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, View: "#state:Machines::Ops", Target: "Machines::Ops", }) @@ -1731,7 +1731,7 @@ func TestDebugSessionEnds(t *testing.T) { } }) t.Run("document closed", func(t *testing.T) { - s, docURI, rec := debugServer(t, "/w/m.sysml", debugMachine) + s, docURI, rec := debugServer(t, "m.sysml", debugMachine) session := start(t, s, docURI) if err := s.DidClose(ctx, &protocol.DidCloseTextDocumentParams{TextDocument: protocol.TextDocumentIdentifier{URI: docURI}}); err != nil { t.Fatal(err) @@ -1762,7 +1762,7 @@ func TestDebugSessionEnds(t *testing.T) { } }) t.Run("server shut down", func(t *testing.T) { - s, docURI, rec := debugServer(t, "/w/m.sysml", debugMachine) + s, docURI, rec := debugServer(t, "m.sysml", debugMachine) first, second := start(t, s, docURI), start(t, s, docURI) if err := s.Shutdown(ctx); err != nil { t.Fatal(err) @@ -1850,7 +1850,7 @@ func TestDebugSessionEndsWhenInheritedContentChanges(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - s, docURI, rec := debugServer(t, "/w/i.sysml", debugDerived) + s, docURI, rec := debugServer(t, "i.sysml", debugDerived) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, View: tc.view, Target: tc.target, }) @@ -1865,7 +1865,7 @@ func TestDebugSessionEndsWhenInheritedContentChanges(t *testing.T) { } // An edit elsewhere in the definition's package leaves the session running. - s, docURI, rec := debugServer(t, "/w/i.sysml", debugDerived) + s, docURI, rec := debugServer(t, "i.sysml", debugDerived) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, View: "InheritedViews::derivedView", Target: "Inherited::Sub::Derived", }) @@ -1876,82 +1876,6 @@ func TestDebugSessionEndsWhenInheritedContentChanges(t *testing.T) { } } -// 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:] - open := func(t *testing.T, s *Server, name, src string) uri.URI { - t.Helper() - docURI := uri.File(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, "/w/views.sysml", views) - machinesURI := open(t, s, "/w/machines.sysml", machines) - open(t, s, "/w/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, "/w/machines.sysml", machines) - open(t, s, "/w/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(), "/w/machines.sysml, /w/spare.sysml") { - 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) - } -} - // Closing the target's or the object's document ends a session even when the // file is on disk, so the workspace goes on holding the document it declares. func TestDebugEndsWhenTargetOrObjectDocumentClosedOnDisk(t *testing.T) { @@ -2082,7 +2006,7 @@ func TestDebugSessionEndsWhenWhatItReadsChanges(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - s, docURI, rec := debugServer(t, "/w/d.sysml", debugReads) + s, docURI, rec := debugServer(t, "d.sysml", debugReads) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, View: tc.view, Target: tc.target, Object: tc.object, }) @@ -2097,7 +2021,7 @@ func TestDebugSessionEndsWhenWhatItReadsChanges(t *testing.T) { } t.Run("signal a bare trigger names", func(t *testing.T) { - s, docURI, rec := debugServer(t, "/w/m.sysml", debugMachine) + s, docURI, rec := debugServer(t, "m.sysml", debugMachine) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, View: "MachineViews::opsView", Target: "Machines::Ops", }) @@ -2109,7 +2033,7 @@ func TestDebugSessionEndsWhenWhatItReadsChanges(t *testing.T) { }) t.Run("value a send named", func(t *testing.T) { - s, docURI, rec := debugServer(t, "/w/d.sysml", debugReads) + s, docURI, rec := debugServer(t, "d.sysml", debugReads) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, View: "DepViews::opsView", Target: "Deps::Ops", }) @@ -2128,7 +2052,7 @@ func TestDebugSessionEndsWhenWhatItReadsChanges(t *testing.T) { t.Errorf("step after the end: %v, want %v", err, ErrDebugSession) } - s, docURI, rec = debugServer(t, "/w/d.sysml", debugReads) + s, docURI, rec = debugServer(t, "d.sysml", debugReads) snap = mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, View: "DepViews::opsView", Target: "Deps::Ops", }) @@ -2155,7 +2079,7 @@ func TestDebugSessionEndsWhenWhatItReadsChanges(t *testing.T) { } for _, tc := range unread { t.Run(tc.name, func(t *testing.T) { - s, docURI, rec := debugServer(t, "/w/d.sysml", debugReads) + s, docURI, rec := debugServer(t, "d.sysml", debugReads) snap := mustDebug(t, s, MethodDebugStart, &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, View: "DepViews::opsView", Target: "Deps::Ops", Object: "Deps::Bots::Robot", }) @@ -2175,7 +2099,7 @@ func TestDebugSessionEndsWhenWhatItReadsChanges(t *testing.T) { // Sessions run in runtimes of their own: two over one machine do not share // state, and rendering the view they draw on is unchanged by their running. func TestDebugSessionsAreIsolated(t *testing.T) { - s, docURI, _ := debugServer(t, "/w/m.sysml", debugMachine) + s, docURI, _ := debugServer(t, "m.sysml", debugMachine) plain := render(t, s, docURI, "MachineViews::opsView") params := &debugStartParams{ TextDocument: protocol.TextDocumentIdentifier{URI: docURI}, diff --git a/internal/frontend/lsp/handler_test.go b/internal/frontend/lsp/handler_test.go index 6f2361d314..9d389545b7 100644 --- a/internal/frontend/lsp/handler_test.go +++ b/internal/frontend/lsp/handler_test.go @@ -12,13 +12,12 @@ 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; @@ -26,6 +25,7 @@ const wireSource = "package Vehicles {\n part def Wheel;\n part w : Wheel; type wireSession struct { t *testing.T s *Server + uri string client net.Conn msgs chan map[string]any readErr chan error @@ -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), @@ -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) @@ -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 "" } @@ -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) @@ -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 } diff --git a/internal/frontend/lsp/integration_test.go b/internal/frontend/lsp/integration_test.go index 6904fbaf00..e0b99709f7 100644 --- a/internal/frontend/lsp/integration_test.go +++ b/internal/frontend/lsp/integration_test.go @@ -6,7 +6,6 @@ import ( "testing" "go.lsp.dev/protocol" - "go.lsp.dev/uri" "github.com/Open-MBEE/OpenSysML/internal/workspace/model" ) @@ -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, diff --git a/internal/frontend/lsp/modeledit_cross_document_test.go b/internal/frontend/lsp/modeledit_cross_document_test.go index 12d4951eb1..97483a9322 100644 --- a/internal/frontend/lsp/modeledit_cross_document_test.go +++ b/internal/frontend/lsp/modeledit_cross_document_test.go @@ -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 } @@ -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") @@ -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") @@ -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) @@ -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, diff --git a/internal/frontend/lsp/modeledit_test.go b/internal/frontend/lsp/modeledit_test.go index e48047d430..500ab8314a 100644 --- a/internal/frontend/lsp/modeledit_test.go +++ b/internal/frontend/lsp/modeledit_test.go @@ -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" { @@ -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) @@ -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"}) @@ -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() diff --git a/internal/frontend/lsp/opened_directory_test.go b/internal/frontend/lsp/opened_directory_test.go new file mode 100644 index 0000000000..f070b14b12 --- /dev/null +++ b/internal/frontend/lsp/opened_directory_test.go @@ -0,0 +1,108 @@ +package lsp + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "go.lsp.dev/protocol" + "go.lsp.dev/uri" + + "github.com/Open-MBEE/OpenSysML/internal/workspace/model" +) + +// A document opened under no folder has its own directory indexed and nothing +// above it: a model file in the parent directory stays out of the workspace. +func TestOpenedDocumentIndexesOnlyItsOwnDirectory(t *testing.T) { + root := t.TempDir() + stray := filepath.Join(root, "stray.sysml") + if err := os.WriteFile(stray, []byte("package Stray;\n"), 0o600); err != nil { + t.Fatal(err) + } + dir := filepath.Join(root, "proj") + if err := os.MkdirAll(dir, 0o750); err != nil { + t.Fatal(err) + } + sibling := filepath.Join(dir, "sibling.sysml") + if err := os.WriteFile(sibling, []byte("package Sibling;\n"), 0o600); err != nil { + t.Fatal(err) + } + + s := NewServer(model.NewWorkspace()) + docURI := uri.File(filepath.Join(dir, "doc.sysml")) + openDoc(t, s, docURI, "package P;\n") + + names := s.ws.DocumentNames() + if !containsName(names, sibling) { + t.Errorf("sibling %s not indexed; documents = %v", sibling, names) + } + if containsName(names, stray) { + t.Errorf("%s in the parent directory was indexed; documents = %v", stray, names) + } + if len(s.openDirs) != 1 || !s.openDirs[dir] { + t.Errorf("indexed directories = %v, want only %s", s.openDirs, dir) + } +} + +// Every helper that opens a document places it under the test's root: a stray +// model file in the system temp directory is never indexed, nor any directory outside. +func TestOpenHelpersStayUnderTheTestRoot(t *testing.T) { + stray := filepath.Join(os.TempDir(), "opensysml-lsp-stray-guard.sysml") + if err := os.WriteFile(stray, []byte("package OpenSysMLStrayGuard;\n"), 0o600); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Remove(stray) }) + testRoot := filepath.Dir(t.TempDir()) + + const src = "package P { part def Wheel; }\n" + check := func(what string, s *Server) { + t.Helper() + if names := s.ws.DocumentNames(); containsName(names, stray) { + t.Errorf("%s indexed %s outside the test root; documents = %v", what, stray, names) + } + if len(s.openDirs) == 0 { + t.Errorf("%s indexed no directory for the document it opened", what) + } + for dir := range s.openDirs { + if !underFolder(dir, testRoot) { + t.Errorf("%s indexed %s, outside the test root %s", what, dir, testRoot) + } + } + } + + w := newWireSession(t) + w.open(src) + if !strings.HasPrefix(uriToName(uri.URI(w.uri)), testRoot+string(filepath.Separator)) { + t.Errorf("wire session document %s is outside the test root %s", w.uri, testRoot) + } + check("wireSession.open", w.s) + + s := NewServer(model.NewWorkspace()) + openDoc(t, s, tempDocURI(t, "a.sysml"), src) + check("openDoc(tempDocURI)", s) + + s, _ = renderServer(t, "kit.sysml", src) + check("renderServer", s) + + s, _, _ = debugServer(t, "m.sysml", src) + check("debugServer", s) + + s = NewServer(model.NewWorkspace()) + if err := s.DidOpen(context.Background(), &protocol.DidOpenTextDocumentParams{ + TextDocument: protocol.TextDocumentItem{URI: tempDocURI(t, "b.sysml"), LanguageID: "sysml", Version: 1, Text: src}, + }); err != nil { + t.Fatalf("DidOpen err = %v", err) + } + check("DidOpen(tempDocURI)", s) +} + +func containsName(names []string, name string) bool { + for _, n := range names { + if n == name { + return true + } + } + return false +} diff --git a/internal/frontend/lsp/protocol_editor_facts_test.go b/internal/frontend/lsp/protocol_editor_facts_test.go index 10c24975bf..299d8018c3 100644 --- a/internal/frontend/lsp/protocol_editor_facts_test.go +++ b/internal/frontend/lsp/protocol_editor_facts_test.go @@ -21,7 +21,7 @@ func TestRunServesSignatureHelpCodeLensAndInlayHints(t *testing.T) { done := make(chan error, 1) go func() { done <- s.Run(context.Background(), server) }() - const file = "file:///tmp/protocol_editor_facts.sysml" + file := string(tempDocURI(t, "protocol_editor_facts.sysml")) doc := map[string]any{"uri": file} callPos := positionAfter(t, editorFactsModel, "fell = Fall(20.0, 9.") lines := uint32(len(editorFactsModel)) diff --git a/internal/frontend/lsp/protocol_features_test.go b/internal/frontend/lsp/protocol_features_test.go index 12e91ad0c7..dfdaf68b16 100644 --- a/internal/frontend/lsp/protocol_features_test.go +++ b/internal/frontend/lsp/protocol_features_test.go @@ -24,7 +24,7 @@ func TestRunServesSemanticTokensAndCodeAction(t *testing.T) { go func() { done <- s.Run(context.Background(), server) }() const src = "package P {\n part def Wheel;\n part w : Wheeel;\n}\n" - const file = "file:///tmp/protocol_features.sysml" + file := string(tempDocURI(t, "protocol_features.sysml")) doc := map[string]any{"uri": file} requests := []map[string]any{ {"jsonrpc": "2.0", "id": 1, "method": "initialize", diff --git a/internal/frontend/lsp/render_filter_test.go b/internal/frontend/lsp/render_filter_test.go index 78e22e0c15..2e55b03b90 100644 --- a/internal/frontend/lsp/render_filter_test.go +++ b/internal/frontend/lsp/render_filter_test.go @@ -15,7 +15,7 @@ import ( func TestRenderFiltersByAMetaclassDeclaredByTwoDocuments(t *testing.T) { s := initializedServer(t, map[string]any{CrossDocumentCapability: true}) open := func(name, src string) uri.URI { - docURI := uri.File(name) + docURI := tempDocURI(t, name) if err := s.DidOpen(context.Background(), &protocol.DidOpenTextDocumentParams{ TextDocument: protocol.TextDocumentItem{URI: docURI, LanguageID: "sysml", Version: 1, Text: src}, }); err != nil { @@ -24,9 +24,9 @@ func TestRenderFiltersByAMetaclassDeclaredByTwoDocuments(t *testing.T) { return docURI } const metaclassCopy = `package SysML { package Systems { metadata def PartUsage; } }` - open("/ws/lib_one.sysml", metaclassCopy) - open("/ws/lib_two.sysml", metaclassCopy) - docURI := open("/ws/interconnection.sysml", ` + open("lib_one.sysml", metaclassCopy) + open("lib_two.sysml", metaclassCopy) + docURI := open("interconnection.sysml", ` package Model { part abcd { part a { port a; } diff --git a/internal/frontend/lsp/render_test.go b/internal/frontend/lsp/render_test.go index a0afd92680..5e7a510472 100644 --- a/internal/frontend/lsp/render_test.go +++ b/internal/frontend/lsp/render_test.go @@ -139,7 +139,7 @@ func (r *recorder) all() []string { func renderServer(t *testing.T, name, src string) (*Server, uri.URI) { t.Helper() s := initializedServer(t, map[string]any{CrossDocumentCapability: true}) - docURI := uri.File(name) + docURI := testDocURI(t, name) if err := s.DidOpen(context.Background(), &protocol.DidOpenTextDocumentParams{ TextDocument: protocol.TextDocumentItem{ URI: docURI, LanguageID: "sysml", Version: 1, Text: src, @@ -1173,7 +1173,7 @@ func TestDidChangeNotifiesRenderChangedAfterDiagnostics(t *testing.T) { s.client = rec s.notifier = rec ctx := context.Background() - docURI := uri.File("kit.sysml") + docURI := tempDocURI(t, "kit.sysml") if err := s.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{ TextDocument: protocol.TextDocumentItem{ URI: docURI, LanguageID: "sysml", Version: 1, Text: "package Kit {\n\tpart def Widget;\n}\n", diff --git a/internal/frontend/lsp/run_test.go b/internal/frontend/lsp/run_test.go index 9dfaed5ce1..9a62ca9c1d 100644 --- a/internal/frontend/lsp/run_test.go +++ b/internal/frontend/lsp/run_test.go @@ -122,6 +122,7 @@ func TestRunServesEveryRequestOverOneStream(t *testing.T) { // Writes run concurrently with reads: net.Pipe is unbuffered, so the // server's diagnostics notifications must be drained as they are sent. const requests = 25 + file := string(tempDocURI(t, "run.sysml")) writeErr := make(chan error, 1) go func() { messages := []any{ @@ -133,7 +134,7 @@ func TestRunServesEveryRequestOverOneStream(t *testing.T) { map[string]any{ "jsonrpc": "2.0", "method": "textDocument/didOpen", "params": map[string]any{"textDocument": map[string]any{ - "uri": "file:///tmp/run.sysml", "languageId": "sysml", "version": 1, + "uri": file, "languageId": "sysml", "version": 1, "text": "package P {\n part def Wheel { attribute pressure; }\n part w : Wheel;\n}\n", }}, }, @@ -142,7 +143,7 @@ func TestRunServesEveryRequestOverOneStream(t *testing.T) { messages = append(messages, map[string]any{ "jsonrpc": "2.0", "id": id, "method": "textDocument/completion", "params": map[string]any{ - "textDocument": map[string]any{"uri": "file:///tmp/run.sysml"}, + "textDocument": map[string]any{"uri": file}, "position": map[string]any{"line": 2, "character": 18}, }, }) diff --git a/internal/frontend/lsp/stdlib_test.go b/internal/frontend/lsp/stdlib_test.go index 1127b70ef8..316c8f4fbf 100644 --- a/internal/frontend/lsp/stdlib_test.go +++ b/internal/frontend/lsp/stdlib_test.go @@ -416,17 +416,18 @@ func TestRunServesLibraryDocumentsOverStream(t *testing.T) { send(map[string]any{"jsonrpc": "2.0", "method": "initialized", "params": map[string]any{}}) src := "package P { attribute x : ScalarValues::Integer; }\n" + file := string(tempDocURI(t, "stream.sysml")) send(map[string]any{ "jsonrpc": "2.0", "method": "textDocument/didOpen", "params": map[string]any{"textDocument": map[string]any{ - "uri": "file:///tmp/stream.sysml", "languageId": "sysml", "version": 1, "text": src, + "uri": file, "languageId": "sysml", "version": 1, "text": src, }}, }) pos := offsetToPosition([]byte(src), strings.Index(src, "Integer")) send(map[string]any{ "jsonrpc": "2.0", "id": 2, "method": "textDocument/definition", "params": map[string]any{ - "textDocument": map[string]any{"uri": "file:///tmp/stream.sysml"}, + "textDocument": map[string]any{"uri": file}, "position": map[string]any{"line": pos.Line, "character": pos.Character}, }, }) @@ -451,7 +452,7 @@ func TestRunServesLibraryDocumentsOverStream(t *testing.T) { t.Errorf("%s returned %d bytes, want the bundled ScalarValues.kerml (%d bytes)", MethodStdlibContent, len(got), len(text)) } - send(map[string]any{"jsonrpc": "2.0", "id": 4, "method": MethodStdlibContent, "params": map[string]any{"uri": "file:///tmp/stream.sysml"}}) + send(map[string]any{"jsonrpc": "2.0", "id": 4, "method": MethodStdlibContent, "params": map[string]any{"uri": file}}) res = await(4, nil) if res["error"] == nil { t.Errorf("%s on a file URI answered %v, want an error", MethodStdlibContent, res["result"]) diff --git a/internal/frontend/lsp/sync_test.go b/internal/frontend/lsp/sync_test.go index 04500aa43a..9cf8a76868 100644 --- a/internal/frontend/lsp/sync_test.go +++ b/internal/frontend/lsp/sync_test.go @@ -15,7 +15,7 @@ import ( func TestDidOpenRegistersDocument(t *testing.T) { s := NewServer(model.NewWorkspace()) - u := uri.File("/tmp/a.sysml") + u := tempDocURI(t, "a.sysml") err := s.DidOpen(context.Background(), &protocol.DidOpenTextDocumentParams{ TextDocument: protocol.TextDocumentItem{ URI: u, @@ -60,6 +60,7 @@ func sendDidChange(t *testing.T, s *Server, u uri.URI, version int32, rawChanges func openDoc(t *testing.T, s *Server, u uri.URI, text string) { t.Helper() + requireTestOwnedDir(t, u) if err := s.DidOpen(context.Background(), &protocol.DidOpenTextDocumentParams{ TextDocument: protocol.TextDocumentItem{URI: u, Version: 1, Text: text}, }); err != nil { @@ -69,7 +70,7 @@ func openDoc(t *testing.T, s *Server, u uri.URI, text string) { func TestDidChangeFullReplace(t *testing.T) { s := NewServer(model.NewWorkspace()) - u := uri.File("/tmp/a.sysml") + u := tempDocURI(t, "a.sysml") openDoc(t, s, u, "package P;\n") // A full-document replace omits "range" entirely. sendDidChange(t, s, u, 2, []json.RawMessage{ @@ -82,7 +83,7 @@ func TestDidChangeFullReplace(t *testing.T) { func TestDidChangeIncremental(t *testing.T) { s := NewServer(model.NewWorkspace()) - u := uri.File("/tmp/a.sysml") + u := tempDocURI(t, "a.sysml") openDoc(t, s, u, "package P;\n") // Replace the single character 'P' (line 0, chars 8..9) with "Hello". sendDidChange(t, s, u, 2, []json.RawMessage{ @@ -98,7 +99,7 @@ func TestDidChangeIncremental(t *testing.T) { // front instead of wiping the buffer. func TestDidChangeIncrementalInsertAtStart(t *testing.T) { s := NewServer(model.NewWorkspace()) - u := uri.File("/tmp/a.sysml") + u := tempDocURI(t, "a.sysml") openDoc(t, s, u, "package P;\n") sendDidChange(t, s, u, 2, []json.RawMessage{ json.RawMessage(`{"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":0}},"text":"X"}`), @@ -112,7 +113,7 @@ func TestDidChangeIncrementalInsertAtStart(t *testing.T) { // start must apply all edits sequentially against the accumulating buffer. func TestDidChangeMultiChangeStartingAtOffsetZero(t *testing.T) { s := NewServer(model.NewWorkspace()) - u := uri.File("/tmp/a.sysml") + u := tempDocURI(t, "a.sysml") openDoc(t, s, u, "abc\n") sendDidChange(t, s, u, 2, []json.RawMessage{ json.RawMessage(`{"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":0}},"text":"X"}`), @@ -127,7 +128,7 @@ func TestDidChangeMultiChangeStartingAtOffsetZero(t *testing.T) { // UTF-16 columns; the byte offset must skip the 4-byte emoji correctly. func TestDidChangeIncrementalAstral(t *testing.T) { s := NewServer(model.NewWorkspace()) - u := uri.File("/tmp/a.sysml") + u := tempDocURI(t, "a.sysml") openDoc(t, s, u, "😀ab\n") // emoji = 2 UTF-16 units; 'a' at char 2, 'b' at char 3 // Replace 'a' (chars 2..3) with "Z". sendDidChange(t, s, u, 2, []json.RawMessage{ diff --git a/internal/frontend/lsp/tempdoc_test.go b/internal/frontend/lsp/tempdoc_test.go new file mode 100644 index 0000000000..76106dc0a2 --- /dev/null +++ b/internal/frontend/lsp/tempdoc_test.go @@ -0,0 +1,42 @@ +package lsp + +import ( + "os" + "path/filepath" + "testing" + + "go.lsp.dev/uri" +) + +// tempDocURI names a document in a directory of the test's own, so opening it +// under no folder indexes that directory and never a shared one such as /tmp. +func tempDocURI(t testing.TB, name string) uri.URI { + t.Helper() + return uri.File(filepath.Join(t.TempDir(), name)) +} + +// testDocURI is the URI a helper opens name under: an absolute name as given, +// a bare one in a directory of the test's own. +func testDocURI(t testing.TB, name string) uri.URI { + t.Helper() + if filepath.IsAbs(name) { + u := uri.File(name) + requireTestOwnedDir(t, u) + return u + } + return tempDocURI(t, name) +} + +// requireTestOwnedDir fails a test about to open u in a directory it does not +// own: the working directory, where a bare name lands, or the system temp directory. +func requireTestOwnedDir(t testing.TB, u uri.URI) { + t.Helper() + dir := filepath.Dir(u.Filename()) + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Getwd: %v", err) + } + if dir == wd || dir == os.TempDir() { + t.Fatalf("document %s opens in shared directory %s; use tempDocURI", u, dir) + } +} diff --git a/tests/hygiene/size_test.go b/tests/hygiene/size_test.go index f046f46be2..e03be7a372 100644 --- a/tests/hygiene/size_test.go +++ b/tests/hygiene/size_test.go @@ -35,7 +35,7 @@ var oversizeTestFiles = []oversize{ {"internal/exec/runtime/conformance_test.go", 2714, "execution-conformance driver and its per-case assertions"}, {"internal/exec/runtime/signal_test.go", 2491, "signal delivery scenarios on one shared harness"}, {"tests/migrate/behavior_more_test.go", 2465, "SysML v1 behavior migration scenarios on one shared harness"}, - {"internal/frontend/lsp/debug_test.go", 2203, "LSP debug-protocol scenarios over one wire fixture"}, + {"internal/frontend/lsp/debug_test.go", 2127, "LSP debug-protocol scenarios over one wire fixture"}, {"internal/exec/runtime/signal_injection_test.go", 2181, "signal-injection scenarios on one shared harness"}, {"internal/exec/runtime/adopt_test.go", 2163, "adoption scenarios on one shared harness"}, {"internal/doc/queryexec/execute_test.go", 2114, "query-execution scenarios on one shared harness"},