Skip to content

Commit

Permalink
Add Tests
Browse files Browse the repository at this point in the history
Note:
There's a function to compare the `open` location retrieved from code
completion (`textDocument/completion`) with the `open` location from
Quick Fix "open Namespace" (`textDocument/CodeAction`).
That's currently disable because these two methods `open` in different
locations:
* Code Completion: Nearest position (closest module/namespace)
* Code Quick: Top Level (most outer scope in file)

This might be useful for testing if/when these different behaviors are
unified (#789)
  • Loading branch information
Booksbaum authored and baronfel committed Jun 13, 2021
1 parent 7162168 commit b013245
Show file tree
Hide file tree
Showing 16 changed files with 607 additions and 0 deletions.
198 changes: 198 additions & 0 deletions test/FsAutoComplete.Tests.Lsp/CompletionTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,201 @@ let autocompleteTest state =
testList "Autocomplete within script files" (makeAutocompleteTestList scriptServer)
]
)

let autoOpenTests state =
let dirPath = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "CompletionAutoOpenTests")
let serverFor (scriptPath: string) = async {
// Auto Open requires unopened things in completions -> External
let config = { defaultConfigDto with ExternalAutocomplete = Some true; ResolveNamespaces = Some true }

let dirPath = Path.GetDirectoryName scriptPath
let scriptName = Path.GetFileName scriptPath
let! (server, events) = serverInitialize dirPath config state
do! waitForWorkspaceFinishedParsing events

let tdop: DidOpenTextDocumentParams = { TextDocument = loadDocument scriptPath }
do! server.TextDocumentDidOpen tdop
do!
waitForParseResultsForFile scriptName events
|> AsyncResult.bimap (fun _ -> failtest "Should have had errors") id
|> Async.Ignore

return (server, scriptPath)
}
let calcOpenPos (edit: TextEdit) =
let text = edit.NewText
let pos = edit.Range.Start
let indentation = pos.Character + (text.Length - text.TrimStart().Length)
{ Line = pos.Line; Character = indentation}
let getQuickFix (server: FSharpLspServer, path: string) (word: string, ns: string) (cursor: Position) = async {
let p = {
CodeActionParams.TextDocument = { Uri = Path.FilePathToUri path }
Range = { Start = cursor; End = cursor}
Context = { Diagnostics = [|
{
Range = { Start = cursor; End = cursor }
Severity = Some DiagnosticSeverity.Error
// Message required for QuickFix to fire ("is not defined")
Message = $"The value or constructor '{word}' is not defined."
Code = Some "39"
Source = "F# Compiler"
RelatedInformation = None
Tags = None
}
|] }
}
let (|ContainsOpenAction|_|) (codeActions: CodeAction []) =
codeActions
|> Array.tryFind (fun ca -> ca.Kind = Some "quickfix" && ca.Title.StartsWith "open ")
match! server.TextDocumentCodeAction p with
| Error e -> return failtestf "Quick fix Request failed: %A" e
| Ok None -> return failtest "Quick fix Request none"
| Ok (Some (TextDocumentCodeActionResult.CodeActions (ContainsOpenAction quickfix))) ->
let ns = quickfix.Title.Substring ("open ".Length)
let edit = quickfix.Edit.DocumentChanges.Value.[0].Edits.[0]
let openPos = calcOpenPos edit
return (edit, ns, openPos)
| Ok _ -> return failtest $"Quick fix on `{word}` doesn't contain open action"
}
let test (compareWithQuickFix: bool) (name: string option) (server: Async<FSharpLspServer * string>) (word: string, ns: string) (cursor: Position) (expectedOpen: Position) =
let name = name |> Option.defaultWith (fun _ -> sprintf "completion on `Regex` at (%i, %i) should `open System.Text.RegularExpressions` at (%i, %i) (0-based)" (cursor.Line) (cursor.Character) (expectedOpen.Line) (expectedOpen.Character))
testCaseAsync name <| async {
let! server, path = server

let p : CompletionParams = { TextDocument = { Uri = Path.FilePathToUri path}
// Line AND Column are ZERO-based!
Position = cursor
Context = None }
match! server.TextDocumentCompletion p with
| Error e -> failtestf "Request failed: %A" e
| Ok None -> failtest "Request none"
| Ok (Some res) ->
Expect.isFalse res.IsIncomplete "Result is incomplete"
let ci = res.Items |> Array.find (fun c -> c.Label = word)

// now get details: `completionItem/resolve` (previous request was `textDocument/completion` -> List of all completions, but without details)
match! server.CompletionItemResolve ci with
| Error e -> failtestf "Request failed: %A" e
| Ok ci ->
Expect.equal ci.Label $"{word} (open {ns})" $"Should be unopened {word}"
let edit = ci.AdditionalTextEdits.Value |> Array.head
let text = edit.NewText
Expect.equal (text.Trim()) $"open {ns}" $"Edit should be `open {ns}`"
let openPos = calcOpenPos edit
Expect.equal openPos.Line expectedOpen.Line "Should be on correct line"
Expect.equal openPos.Character expectedOpen.Character "Should have correct indentation"
Expect.stringEnds text "\n" "Should end with New Line"

if compareWithQuickFix then
// must be same as open quick fix (`ResolveNamespace`)
// NOTE: currently code completion and quick fix open in different locations:
// * Code Completion: nearest position
// * Quick Fix: Top Level
let! (_, _, qfOpenPos) = getQuickFix (server, path) (word, ns) cursor
Expect.equal qfOpenPos openPos "Auto-Open and Open Quick Fix should open at same location"
}

/// In passed file: Cursor positions are marked with comments (multi-line comments: `(*...*)`)
/// Cursor:
/// * before start of open comment (before the leading `(`)
/// * Completion is executed here -> expected to be `Regex`
/// In comment:
/// * Expected position of generated `open ...`. Column marks the the position of the leading `o` of `open` (-> Column is indentation)
///
/// Format of position: `Line,Column`
/// * Note: Line & Column are 0-based!
/// Format of numbers (Line & Column):
/// * absolute number: `3`
/// * relative number: `+1`, `-2`
/// * relative to cursor position
/// * relative to indentation of current line: `|-2` -> current indentation - 2 spaces
/// * only makes sense for Column
/// Example:
/// ```fsharp
/// 03: //...
/// 04: let foo = Regex(*-1,|-2*)
// 05: //...
/// ```
/// * Expected open:
/// * Line: `-1` relative to Line `04` -> Line `03`
/// * Column: `|-2`: indentation - 2 spaces
/// * Current indentation is 4 spaces
/// * -> Expected indentation is 2 spaces -> Column 2
/// * -> Position of open: (3,2)
/// * NOTE: 0-based, but display in editor is 1-based (Line 4, Column 3 in editor!)
let readData path =
let regex = System.Text.RegularExpressions.Regex("\(\*(?<data>.*)\*\)")
let parseData (line, column) (lineStr: string) (data: string) =
match data.Split(',') with
| [| l; c |] ->
let calcN (current: int) (n: string) =
let n = n.Trim()
match n.[0] with
| '|' ->
//relative to indentation of current line
let ind = lineStr.Length - lineStr.TrimStart().Length
match n.Substring(1).Trim() with
| "" -> ind
| n -> ind + int n
| '+' | '-' ->
// relative to current position
current + int n
| _ ->
// absolute
int n

let (l, c) = (calcN line l, calcN column c)
{ Line = l; Character = c }
| _ -> failwithf "Invalid data in line (%i,%i) '%s'" line column lineStr

let extractData (lineNumber: int) (line: string) =
let m = regex.Match line
if not m.Success then
None
else
let data = m.Groups.["data"]
let (l,c) = (lineNumber, m.Index)
let openPos = parseData (l,c) line data.Value
let cursorPos = { Line = l; Character = c }

(cursorPos, openPos)
|> Some

System.IO.File.ReadAllLines path
|> Seq.mapi (fun i l -> (i,l))
|> Seq.filter (fun (_, l) -> l.Contains "(*")
|> Seq.choose (fun (i, l) -> extractData i l)
|> Seq.toList

let testScript name scriptName =
testList name [
let scriptPath = Path.Combine(dirPath, scriptName)
let server = serverFor scriptPath
let tests =
readData scriptPath
|> List.map (fun (cursor, expectedOpen) -> test false None server ("Regex", "System.Text.RegularExpressions") cursor expectedOpen)
yield! tests

testCaseAsync "cleanup" (async {
let! server, _ = server
do! server.Shutdown()
})
]

testList "Completion.AutoOpen" [
// NOTE: Positions are ZERO-based!: { Line = 3; Character = 9 } -> Line 4, Column 10 in editor display
testScript "with root module with new line" "ModuleWithNewLine.fsx"
testScript "with root module" "Module.fsx"
testScript "with root module with open" "ModuleWithOpen.fsx"
testScript "with root module with open and new line" "ModuleWithOpenAndNewLine.fsx"
testScript "with namespace with new line" "NamespaceWithNewLine.fsx"
testScript "with namespace" "Namespace.fsx"
testScript "with namespace with open" "NamespaceWithOpen.fsx"
testScript "with namespace with open and new line" "NamespaceWithOpenAndNewLine.fsx"
testScript "with implicit top level module with new line" "ImplicitTopLevelModuleWithNewLine.fsx"
testScript "with implicit top level module" "ImplicitTopLevelModule.fsx"
testScript "with implicit top level module with open" "ImplicitTopLevelModuleWithOpen.fsx"
testScript "with implicit top level module with open and new line" "ImplicitTopLevelModuleWithOpenAndNewLine.fsx"
testScript "with implicit top level module with open and new lines" "ImplicitTopLevelModuleWithOpenAndNewLines.fsx"
testScript "with root module with comments and new line before open" "ModuleDocsAndNewLineBeforeOpen.fsx"
]
1 change: 1 addition & 0 deletions test/FsAutoComplete.Tests.Lsp/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ let tests =
codeLensTest state
documentSymbolTest state
Completion.autocompleteTest state
Completion.autoOpenTests state
Rename.tests state
foldingTests state
tooltipTests state
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module RootModle =
let foo () =
Regex(*-1,|-2*)

type T = {
Value: Regex(*1,2*)
}
with
member _.Foo () =
Regex(*1,2*)

module Nested1 =
let foo =
Regex(*-1,|-2*)

type T = {
Value: Regex(*-4,|-2*)
}
with
member _.Foo () =
Regex(*-8,|-4*)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module RootModle =

let foo () =
Regex(*-2,|-2*)

type T = {
Value: Regex(*1,2*)
}
with
member _.Foo () =
Regex(*1,2*)

module Nested1 =

let foo =
Regex(*-2,|-2*)

type T = {
Value: Regex(*-5,|-2*)
}
with
member _.Foo () =
Regex(*-9,|-4*)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module RootModle =
open System
open System.Collections.Generic
let foo () =
Regex(*-1,|-2*)

type T = {
Value: Regex(*-4,2*)
}
with
member _.Foo () =
Regex(*-8,2*)

module Nested1 =
open System
open System.Collections.Generic
let foo =
Regex(*-1,|-2*)

type T = {
Value: Regex(*-4,|-2*)
}
with
member _.Foo () =
Regex(*-8,|-4*)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module RootModle =
open System
open System.Collections.Generic

let foo () =
Regex(*-2,|-2*)

type T = {
Value: Regex(*-5,2*)
}
with
member _.Foo () =
Regex(*-9,2*)

module Nested1 =

let foo =
Regex(*-2,|-2*)

type T = {
Value: Regex(*-5,|-2*)
}
with
member _.Foo () =
Regex(*-9,|-4*)
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module RootModle =
open System
open System.Collections.Generic




let foo () =
Regex(*3,|-2*)

type T = {
Value: Regex(*3,2*)
}
with
member _.Foo () =
Regex(*3,2*)

module Nested1 =
open System
open System.Collections.Generic



let foo =
Regex(*-4,|-2*)

type T = {
Value: Regex(*-7,|-2*)
}
with
member _.Foo () =
Regex(*-11,|-4*)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module OpenNamespace.RootModule
let foo: Regex(*-0,0*) =
Regex(*-1,0*)

type T = {
Value: Regex(*1,0*)
}
with
member _.Foo () =
Regex(*1,0*)

module Nested1 =
let foo =
Regex(*-1,|-2*)

type T = {
Value: Regex(*-4,|-2*)
}
with
member _.Foo () =
Regex(*-8,|-4*)
Loading

0 comments on commit b013245

Please sign in to comment.