From cb929bb22d137262fe674a22652626e05678b8af Mon Sep 17 00:00:00 2001 From: nojaf Date: Tue, 19 Oct 2021 08:56:55 +0200 Subject: [PATCH] Add daemon unit tests. --- .../LSPFantomasServiceTypes.fs | 5 +- .../DaemonTests.fs | 580 ++++++++---------- src/Fantomas.CoreGlobalTool/Daemon.fs | 25 +- 3 files changed, 267 insertions(+), 343 deletions(-) diff --git a/src/Fantomas.Client/LSPFantomasServiceTypes.fs b/src/Fantomas.Client/LSPFantomasServiceTypes.fs index 57a72c037e..e1692f3941 100644 --- a/src/Fantomas.Client/LSPFantomasServiceTypes.fs +++ b/src/Fantomas.Client/LSPFantomasServiceTypes.fs @@ -1,6 +1,5 @@ module Fantomas.Client.LSPFantomasServiceTypes -open System open StreamJsonRpc open Fantomas.Client.Contracts @@ -18,7 +17,7 @@ type FantomasResponseCode = [] type FormatSelectionResponse = | Formatted of filename: string * formattedContent: string - | Error of filename: string * formattingError: Exception + | Error of filename: string * formattingError: string member this.AsFormatResponse() = match this with @@ -29,7 +28,7 @@ type FormatSelectionResponse = | FormatSelectionResponse.Error (name, ex) -> { Code = int FantomasResponseCode.Error FilePath = name - Content = Some(ex.Message) } + Content = Some ex } [] type FormatDocumentResponse = diff --git a/src/Fantomas.CoreGlobalTool.Tests/DaemonTests.fs b/src/Fantomas.CoreGlobalTool.Tests/DaemonTests.fs index 73490cd6a6..49ad4be099 100644 --- a/src/Fantomas.CoreGlobalTool.Tests/DaemonTests.fs +++ b/src/Fantomas.CoreGlobalTool.Tests/DaemonTests.fs @@ -8,7 +8,6 @@ open FsUnit open Fantomas.CoreGlobalTool.Tests.TestHelpers open Fantomas open Fantomas.Client.Contracts -open Fantomas.Client.LSPFantomasService open Nerdbank.Streams open StreamJsonRpc @@ -16,86 +15,7 @@ let private assertFormatted (actual: string) (expected: string) : unit = String.normalizeNewLine actual |> should equal (String.normalizeNewLine expected) -let mutable service: FantomasService = Unchecked.defaultof - -[] -let ``create service`` () = service <- new LSPFantomasService() - -[] -let ``dispose service`` () = service.Dispose() - -// [] -let ``compare the version with the public api`` () = - async { - let! { Content = version } = - service.VersionAsync(@"C:\Users\fverdonck\Temp\meh\SomeFile.fs") - |> Async.AwaitTask - - version - |> Option.defaultValue "???" - |> should equal (CodeFormatter.GetVersion()) - } - -// [] -let ``cached version`` () = - async { - let! _ = - service.VersionAsync(@"C:\Users\fverdonck\Temp\meh\SomeFile.fs") - |> Async.AwaitTask - - let! { Content = version } = - service.VersionAsync(@"C:\Users\fverdonck\Temp\meh\SomeFile.fs") - |> Async.AwaitTask - - version - |> Option.defaultValue "???" - |> should equal (CodeFormatter.GetVersion()) - } - -// [] -let ``relative path should not be accepted`` () = - async { - let! { Code = code } = - service.ConfigurationAsync @"..\src\Fantomas\CodePrinter.fs" - |> Async.AwaitTask - - code - |> should equal (int FantomasResponseCode.FilePathIsNotAbsolute) - } - -// [] -let ``fantomas tool file`` () = - async { - let path = - @"c:\Users\fverdonck\Projects\fantomas-tools\src\server\ASTViewer\Decoders.fs" - - let source = System.IO.File.ReadAllText path - - let! { Code = code } = - service.FormatDocumentAsync - { FilePath = path - SourceCode = source - Config = None } - |> Async.AwaitTask - - code - |> should equal (int FantomasResponseCode.ToolNotFound) - } - -// [] -let ``config as json`` () = - async { - let! { Content = json } = - service.ConfigurationAsync @"C:\Users\fverdonck\Temp\meh\SomeFile.fs" - |> Async.AwaitTask - - match json with - | Some json -> json.StartsWith("{") |> should equal true - | None -> Assert.Fail "expected json config" - } - -// [] -let ``version request`` () = +let private runWithDaemon (fn: JsonRpc -> Async) = async { let struct (serverStream, clientStream) = FullDuplexStream.CreatePair() @@ -104,288 +24,290 @@ let ``version request`` () = let client = new JsonRpc(clientStream, clientStream) client.StartListening() - - let! version = - client.InvokeAsync(Methods.Version) - |> Async.AwaitTask - - version - |> should equal (CodeFormatter.GetVersion()) - + do! fn client client.Dispose() (daemon :> IDisposable).Dispose() } -// [] -let ``should respect editorconfig`` () = - async { - let struct (serverStream, clientStream) = FullDuplexStream.CreatePair() - - let _daemon = - new FantomasDaemon(serverStream, serverStream) - - let client = new JsonRpc(clientStream, clientStream) - client.StartListening() - - let path = - @"c:\Users\fverdonck\Projects\fantomas-tools\src\server\ASTViewer\Decoders.fs" +[] +let ``version request`` () = + runWithDaemon + (fun client -> + async { + let! version = + client.InvokeAsync(Methods.Version) + |> Async.AwaitTask + + version + |> should equal (CodeFormatter.GetVersion()) + }) + +[] +let ``config request`` () = + runWithDaemon + (fun client -> + async { + let! config = + client.InvokeAsync(Methods.Configuration) + |> Async.AwaitTask + + FormatConfig.FormatConfig.Default + |> Fantomas.Extras.EditorConfig.configToEditorConfig + |> fun s -> s.Split('\n') + |> Seq.map (fun line -> line.Split('=').[0]) + |> Seq.iter (fun setting -> Assert.True(config.Contains(setting))) + }) + +[] +let ``format implementation file`` () = + runWithDaemon + (fun client -> + async { + let sourceCode = "module Foobar" + use codeFile = new TemporaryFileCodeSample(sourceCode) + + let request = + { SourceCode = sourceCode + FilePath = codeFile.Filename + Config = None } - let source = System.IO.File.ReadAllText path + let! response = + client.InvokeAsync(Methods.FormatDocument, request) + |> Async.AwaitTask - let req = - { FilePath = path - SourceCode = source - Config = None } + match response with + | FormatDocumentResponse.Formatted (_, formatted) -> + assertFormatted + formatted + "module Foobar +" + | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" + }) + +[] +let ``format implementation file, unchanged`` () = + runWithDaemon + (fun client -> + async { + let sourceCode = "module Foobar\n" + use codeFile = new TemporaryFileCodeSample(sourceCode) + + let request = + { SourceCode = sourceCode + FilePath = codeFile.Filename + Config = Some(readOnlyDict [ "end_of_line", "lf" ]) } + + let! response = + client.InvokeAsync(Methods.FormatDocument, request) + |> Async.AwaitTask + + match response with + | FormatDocumentResponse.Unchanged _ -> Assert.Pass() + | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" + }) + +[] +let ``format implementation file, error`` () = + runWithDaemon + (fun client -> + async { + let sourceCode = "let foo =" + use codeFile = new TemporaryFileCodeSample(sourceCode) + + let request = + { SourceCode = sourceCode + FilePath = codeFile.Filename + Config = None } - let! res = - client.InvokeAsync(Methods.FormatDocument, req) - |> Async.AwaitTask + let! response = + client.InvokeAsync(Methods.FormatDocument, request) + |> Async.AwaitTask + + match response with + | FormatDocumentResponse.Error _ -> Assert.Pass() + | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" + }) + +[] +let ``format implementation file, ignored file`` () = + runWithDaemon + (fun client -> + async { + let sourceCode = "let foo = 4" + use codeFile = new TemporaryFileCodeSample(sourceCode) + use _ignoreFixture = new FantomasIgnoreFile("*.fs") + + let request = + { SourceCode = sourceCode + FilePath = codeFile.Filename + Config = None } - res.Code |> should equal 1 - } + let! response = + client.InvokeAsync(Methods.FormatDocument, request) + |> Async.AwaitTask -// [] -let ``format implementation file`` () = - async { - let sourceCode = "module Foobar" - use codeFile = new TemporaryFileCodeSample(sourceCode) - - let request = - { SourceCode = sourceCode - FilePath = codeFile.Filename - Config = None } - - let! response = - service.FormatDocumentAsync(request) - |> Async.AwaitTask - - match response with - | { Code = 1; Content = Some formatted } -> - assertFormatted - formatted - "module Foobar -" - | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" - } + match response with + | FormatDocumentResponse.IgnoredFile _ -> Assert.Pass() + | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" + }) -// [] +[] let ``format signature file`` () = - async { - let sourceCode = "module Foobar\n\nval meh : int" + runWithDaemon + (fun client -> + async { + let sourceCode = "module Foobar\n\nval meh : int" - use codeFile = - new TemporaryFileCodeSample(sourceCode, extension = "fsi") + use codeFile = + new TemporaryFileCodeSample(sourceCode, extension = "fsi") - let request = - { SourceCode = sourceCode - FilePath = codeFile.Filename - Config = None } + let request = + { SourceCode = sourceCode + FilePath = codeFile.Filename + Config = None } - let! response = - service.FormatDocumentAsync(request) - |> Async.AwaitTask + let! response = + client.InvokeAsync(Methods.FormatDocument, request) + |> Async.AwaitTask - match response with - | { Code = 1; Content = Some formatted } -> - assertFormatted - formatted - "module Foobar + match response with + | FormatDocumentResponse.Formatted (_, formatted) -> + assertFormatted + formatted + "module Foobar val meh : int " - | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" - } + | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" + }) -// [] +[] let ``format document respecting .editorconfig file`` () = - async { - let sourceCode = "module Foo\n\nlet a = //\n 4" - use codeFile = new TemporaryFileCodeSample(sourceCode) - - use _config = - new ConfigurationFile("[*.fs]\nindent_size=2") - - let request = - { SourceCode = sourceCode - FilePath = codeFile.Filename - Config = None } + runWithDaemon + (fun client -> + async { + let sourceCode = "module Foo\n\nlet a = //\n 4" + use codeFile = new TemporaryFileCodeSample(sourceCode) + + use _config = + new ConfigurationFile("[*.fs]\nindent_size=2") + + let request = + { SourceCode = sourceCode + FilePath = codeFile.Filename + Config = None } - let! response = - service.FormatDocumentAsync(request) - |> Async.AwaitTask + let! response = + client.InvokeAsync(Methods.FormatDocument, request) + |> Async.AwaitTask - match response with - | { Code = 1; Content = Some formatted } -> - assertFormatted - formatted - "module Foo + match response with + | FormatDocumentResponse.Formatted (_, formatted) -> + assertFormatted + formatted + "module Foo let a = // 4 " - | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" - } + | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" + }) -// [] +[] let ``custom configuration has precedence over .editorconfig file`` () = - async { - let sourceCode = "module Foo\n\nlet a = //\n 4" - use codeFile = new TemporaryFileCodeSample(sourceCode) - - use _config = - new ConfigurationFile("[*.fs]\nindent_size=2") - - let request = - { SourceCode = sourceCode - FilePath = codeFile.Filename - Config = Some(readOnlyDict [ "indent_size", "4" ]) } - - let! response = - service.FormatDocumentAsync(request) - |> Async.AwaitTask - - match response with - | { Code = 1; Content = Some formatted } -> - assertFormatted - formatted - "module Foo + runWithDaemon + (fun client -> + async { + let sourceCode = "module Foo\n\nlet a = //\n 4" + use codeFile = new TemporaryFileCodeSample(sourceCode) + + use _config = + new ConfigurationFile("[*.fs]\nindent_size=2") + + let request = + { SourceCode = sourceCode + FilePath = codeFile.Filename + Config = Some(readOnlyDict [ "indent_size", "4" ]) } + + let! response = + client.InvokeAsync(Methods.FormatDocument, request) + |> Async.AwaitTask + + match response with + | FormatDocumentResponse.Formatted (_, formatted) -> + assertFormatted + formatted + "module Foo let a = // 4 " - | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" - } - -// [] -let ``already formatted file returns unchanged`` () = - async { - let sourceCode = "let a = x\n" - - use codeFile = - new TemporaryFileCodeSample(sourceCode, extension = "fsx") - - let request = - { SourceCode = sourceCode - FilePath = codeFile.Filename - Config = Some(readOnlyDict [ "end_of_line", "lf" ]) } - - let! response = - service.FormatDocumentAsync(request) - |> Async.AwaitTask - - match response with - | { Code = 2; FilePath = fileName } -> fileName |> should equal codeFile.Filename - | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" - } - -// [] -let ``ignored file returns ignored`` () = - async { - let sourceCode = "let a = x\n" - - use codeFile = - new TemporaryFileCodeSample(sourceCode, extension = "fsx") - - use _ignoreFile = new FantomasIgnoreFile("*.fsx") + | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" + }) - let request = - { SourceCode = sourceCode - FilePath = codeFile.Filename - Config = None } - - let! response = - service.FormatDocumentAsync(request) - |> Async.AwaitTask - - match response with - | { Code = 4; FilePath = fileName } -> fileName |> should equal codeFile.Filename - | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" - } - -// [] -let ``format invalid code`` () = - async { - let sourceCode = "module Foobar\n\nlet ziggy =" - use codeFile = new TemporaryFileCodeSample(sourceCode) - - let request = - { SourceCode = sourceCode - FilePath = codeFile.Filename - Config = None } - - let! response = - service.FormatDocumentAsync(request) - |> Async.AwaitTask - - match response with - | { Code = 3 - Content = Some error - FilePath = fileName } -> - fileName |> should equal codeFile.Filename - StringAssert.StartsWith("Parsing failed with errors:", error) - | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" - } - -// [] +[] let ``format selection`` () = - async { - let sourceCode = - """module Foo + runWithDaemon + (fun client -> + async { + let sourceCode = + """module Foo let x = 4 let y = 5 -""" - - use _codeFile = new TemporaryFileCodeSample(sourceCode) - - let request: FormatSelectionRequest = - let range = FormatSelectionRange(3, 0, 3, 16) - - { SourceCode = sourceCode - FilePath = "tmp.fsx" // codeFile.Filename - Config = None - Range = range } - - let! response = - service.FormatSelectionAsync(request) - |> Async.AwaitTask - - match response with - | { Code = 1 - Content = Some formatted - FilePath = fileName } -> - fileName |> should equal "tmp.fsx" - assertFormatted formatted "let x = 4\n" - | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" - } - -(* -// [] -let ``find fantomas tool from working directory`` () = - async { - let filePath = - @"C:\Users\nojaf\Projects\fantomas\src\Fantomas.Client\LSPFantomasService.fs" - - let originalCode = System.IO.File.ReadAllText(filePath) - - let workingDir = @"C:\Users\nojaf\Projects\fantomas" - - use client = - let x = createForWorkingDirectory workingDir - - match x with - | Ok service -> service - | Error error -> failwithf "butter: %s" error - - let! formattedResponse = - (client :> FantomasService) - .FormatDocumentAsync( - { SourceCode = originalCode - FilePath = filePath - Config = None } - ) - - let formattedCode = formattedResponse - () - } -*) + """ + + use codeFile = new TemporaryFileCodeSample(sourceCode) + + let request: FormatSelectionRequest = + let range = FormatSelectionRange(3, 0, 3, 16) + + { SourceCode = sourceCode + FilePath = codeFile.Filename + Config = None + Range = range } + + let! response = + client.InvokeAsync(Methods.FormatSelection, request) + |> Async.AwaitTask + + match response with + | FormatSelectionResponse.Formatted (fileName, formatted) -> + fileName |> should equal codeFile.Filename + assertFormatted formatted "let x = 4\n" + | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" + }) + +// I don't know if formatting selection for signature files has ever worked. +// There is no way of getting valid AST of the substring as far as I know. +[] +let ``format selection, fsi`` () = + runWithDaemon + (fun client -> + async { + let sourceCode = + """module Foo + +val x : int +val y : string + """ + + use codeFile = + new TemporaryFileCodeSample(sourceCode, extension = "fsi") + + let request: FormatSelectionRequest = + let range = FormatSelectionRange(3, 0, 3, 18) + + { SourceCode = sourceCode + FilePath = codeFile.Filename + Config = None + Range = range } + + let! response = + client.InvokeAsync(Methods.FormatSelection, request) + |> Async.AwaitTask + + match response with + | FormatSelectionResponse.Error _ -> Assert.Pass() + | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" + }) diff --git a/src/Fantomas.CoreGlobalTool/Daemon.fs b/src/Fantomas.CoreGlobalTool/Daemon.fs index b46ec00b3d..07cc30870e 100644 --- a/src/Fantomas.CoreGlobalTool/Daemon.fs +++ b/src/Fantomas.CoreGlobalTool/Daemon.fs @@ -89,17 +89,20 @@ type FantomasDaemon(sender: Stream, reader: Stream) as this = let r = request.Range mkRange request.FilePath (mkPos r.StartLine r.StartColumn) (mkPos r.EndLine r.EndColumn) - let! formatted = - CodeFormatter.FormatSelectionAsync( - request.FilePath, // TODO: does this really work with FSI?? - range, - SourceString request.SourceCode, - config, - CodeFormatterImpl.createParsingOptionsFromFile request.FilePath, // Use safe name ?? - CodeFormatterImpl.sharedChecker.Value - ) - - return FormatSelectionResponse.Formatted(request.FilePath, formatted) + try + let! formatted = + CodeFormatter.FormatSelectionAsync( + request.FilePath, + range, + SourceString request.SourceCode, + config, + CodeFormatterImpl.createParsingOptionsFromFile request.FilePath, + CodeFormatterImpl.sharedChecker.Value + ) + + return FormatSelectionResponse.Formatted(request.FilePath, formatted) + with + | ex -> return FormatSelectionResponse.Error(request.FilePath, ex.Message) } |> Async.StartAsTask