Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test runner Rewrite operation does not handle triple-quoted strings correctly #272

Open
RamanBut-Husaim opened this issue Feb 27, 2025 · 6 comments

Comments

@RamanBut-Husaim
Copy link

Source

https://forum.exercism.org/t/restapi-and-system-text-json-issue/15946

Description

The issue has been initially identified as part of the work on rest-api exercise for F# language. All tests are green locally, however exercism platform test runner considers all tests as failed with the following error:

System.Text.Json.JsonException : '\' is an invalid start of a property name. Expected a '"'. Path: $ | LineNumber: 0 | BytePositionInLine: 1.
---- System.Text.Json.JsonReaderException : '\' is an invalid start of a property name. Expected a '"'. LineNumber: 0 | BytePositionInLine: 1.

The local debug showed that the issue is produced by the Rewrite.fs file. After the rewrite, the output of the test code has every " inside the string escaped with additional \, that makes the json inside the triple-quoted string invalid.

Here is the full error output of the test in JetBrains Rider:

System.Text.Json.JsonException: '\' is an invalid start of a property name. Expected a '"'. Path: $ | LineNumber: 0 | BytePositionInLine: 1.

System.Text.Json.JsonException
'\' is an invalid start of a property name. Expected a '"'. Path: $ | LineNumber: 0 | BytePositionInLine: 1.
   at System.Text.Json.ThrowHelper.ReThrowWithPath(ReadStack& state, JsonReaderException ex)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 utf8Json, JsonTypeInfo`1 jsonTypeInfo, Nullable`1 actualByteCount)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 json, JsonTypeInfo`1 jsonTypeInfo)
   at RestApi.RestApi..ctor(String database) in /Users/Rakkatakka/Exercism/fsharp/rest-api/RestApi.fs:line 37
   at RestApiTests.Add user() in /Users/Rakkatakka/Exercism/fsharp/rest-api/RestApiTests.fs:line 24
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

System.Text.Json.JsonReaderException
'\' is an invalid start of a property name. Expected a '"'. LineNumber: 0 | BytePositionInLine: 1.
   at System.Text.Json.ThrowHelper.ThrowJsonReaderException(Utf8JsonReader& json, ExceptionResource resource, Byte nextByte, ReadOnlySpan`1 bytes)
   at System.Text.Json.Utf8JsonReader.ReadSingleSegment()
   at System.Text.Json.Utf8JsonReader.Read()
   at System.Text.Json.Serialization.Converters.ObjectWithParameterizedConstructorConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value, Boolean& isPopulatedValue)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)

The entire content of the malformed file is:

module RestApiTests

open FsUnit.Xunit
open Xunit
open RestApi

[<Fact>]
let ``No users`` () =
    let database = """{\\"users\\":[]}"""
    let url = "/users"
    let expected = """{\\"users\\":[]}"""
    let api = RestApi(database)
    api.Get url |> should equal expected

[<Fact>]
let ``Add user`` () =
    let database = """{\\"users\\":[]}"""
    let payload = """{\\"user\\":\\"Adam\\"}"""
    let url = "/add"

    let expected =
        """{\\"name\\":\\"Adam\\",\\"owes\\":{},\\"owed_by\\":{},\\"balance\\":0.0}"""

    let api = RestApi(database)
    api.Post(url, payload) |> should equal expected

[<Fact>]
let ``Get single user`` () =
    let database =
        """{\\"users\\":[{\\"name\\":\\"Adam\\",\\"owes\\":{},\\"owed_by\\":{},\\"balance\\":0.0},{\\"name\\":\\"Bob\\",\\"owes\\":{},\\"owed_by\\":{},\\"balance\\":0.0}]}"""

    let payload = """{\\"users\\":[\\"Bob\\"]}"""
    let url = "/users"

    let expected =
        """{\\"users\\":[{\\"name\\":\\"Bob\\",\\"owes\\":{},\\"owed_by\\":{},\\"balance\\":0.0}]}"""

    let api = RestApi(database)
    api.Get(url, payload) |> should equal expected

[<Fact>]
let ``Both users have 0 balance`` () =
    let database =
        """{\\"users\\":[{\\"name\\":\\"Adam\\",\\"owes\\":{},\\"owed_by\\":{},\\"balance\\":0.0},{\\"name\\":\\"Bob\\",\\"owes\\":{},\\"owed_by\\":{},\\"balance\\":0.0}]}"""

    let payload =
        """{\\"lender\\":\\"Adam\\",\\"borrower\\":\\"Bob\\",\\"amount\\":3.0}"""

    let url = "/iou"

    let expected =
        """{\\"users\\":[{\\"name\\":\\"Adam\\",\\"owes\\":{},\\"owed_by\\":{\\"Bob\\":3.0},\\"balance\\":3.0},{\\"name\\":\\"Bob\\",\\"owes\\":{\\"Adam\\":3.0},\\"owed_by\\":{},\\"balance\\":-3.0}]}"""

    let api = RestApi(database)
    api.Post(url, payload) |> should equal expected

[<Fact>]
let ``Borrower has negative balance`` () =
    let database =
        """{\\"users\\":[{\\"name\\":\\"Adam\\",\\"owes\\":{},\\"owed_by\\":{},\\"balance\\":0.0},{\\"name\\":\\"Bob\\",\\"owes\\":{\\"Chuck\\":3.0},\\"owed_by\\":{},\\"balance\\":-3.0},{\\"name\\":\\"Chuck\\",\\"owes\\":{},\\"owed_by\\":{\\"Bob\\":3.0},\\"balance\\":3.0}]}"""

    let payload =
        """{\\"lender\\":\\"Adam\\",\\"borrower\\":\\"Bob\\",\\"amount\\":3.0}"""

    let url = "/iou"

    let expected =
        """{\\"users\\":[{\\"name\\":\\"Adam\\",\\"owes\\":{},\\"owed_by\\":{\\"Bob\\":3.0},\\"balance\\":3.0},{\\"name\\":\\"Bob\\",\\"owes\\":{\\"Adam\\":3.0,\\"Chuck\\":3.0},\\"owed_by\\":{},\\"balance\\":-6.0}]}"""

    let api = RestApi(database)
    api.Post(url, payload) |> should equal expected

[<Fact>]
let ``Lender has negative balance`` () =
    let database =
        """{\\"users\\":[{\\"name\\":\\"Adam\\",\\"owes\\":{},\\"owed_by\\":{},\\"balance\\":0.0},{\\"name\\":\\"Bob\\",\\"owes\\":{\\"Chuck\\":3.0},\\"owed_by\\":{},\\"balance\\":-3.0},{\\"name\\":\\"Chuck\\",\\"owes\\":{},\\"owed_by\\":{\\"Bob\\":3.0},\\"balance\\":3.0}]}"""

    let payload =
        """{\\"lender\\":\\"Bob\\",\\"borrower\\":\\"Adam\\",\\"amount\\":3.0}"""

    let url = "/iou"

    let expected =
        """{\\"users\\":[{\\"name\\":\\"Adam\\",\\"owes\\":{\\"Bob\\":3.0},\\"owed_by\\":{},\\"balance\\":-3.0},{\\"name\\":\\"Bob\\",\\"owes\\":{\\"Chuck\\":3.0},\\"owed_by\\":{\\"Adam\\":3.0},\\"balance\\":0.0}]}"""

    let api = RestApi(database)
    api.Post(url, payload) |> should equal expected

[<Fact>]
let ``Lender owes borrower`` () =
    let database =
        """{\\"users\\":[{\\"name\\":\\"Adam\\",\\"owes\\":{\\"Bob\\":3.0},\\"owed_by\\":{},\\"balance\\":-3.0},{\\"name\\":\\"Bob\\",\\"owes\\":{},\\"owed_by\\":{\\"Adam\\":3.0},\\"balance\\":3.0}]}"""

    let payload =
        """{\\"lender\\":\\"Adam\\",\\"borrower\\":\\"Bob\\",\\"amount\\":2.0}"""

    let url = "/iou"

    let expected =
        """{\\"users\\":[{\\"name\\":\\"Adam\\",\\"owes\\":{\\"Bob\\":1.0},\\"owed_by\\":{},\\"balance\\":-1.0},{\\"name\\":\\"Bob\\",\\"owes\\":{},\\"owed_by\\":{\\"Adam\\":1.0},\\"balance\\":1.0}]}"""

    let api = RestApi(database)
    api.Post(url, payload) |> should equal expected

[<Fact>]
let ``Lender owes borrower less than new loan`` () =
    let database =
        """{\\"users\\":[{\\"name\\":\\"Adam\\",\\"owes\\":{\\"Bob\\":3.0},\\"owed_by\\":{},\\"balance\\":-3.0},{\\"name\\":\\"Bob\\",\\"owes\\":{},\\"owed_by\\":{\\"Adam\\":3.0},\\"balance\\":3.0}]}"""

    let payload =
        """{\\"lender\\":\\"Adam\\",\\"borrower\\":\\"Bob\\",\\"amount\\":4.0}"""

    let url = "/iou"

    let expected =
        """{\\"users\\":[{\\"name\\":\\"Adam\\",\\"owes\\":{},\\"owed_by\\":{\\"Bob\\":1.0},\\"balance\\":1.0},{\\"name\\":\\"Bob\\",\\"owes\\":{\\"Adam\\":1.0},\\"owed_by\\":{},\\"balance\\":-1.0}]}"""

    let api = RestApi(database)
    api.Post(url, payload) |> should equal expected

[<Fact>]
let ``Lender owes borrower same as new loan`` () =
    let database =
        """{\\"users\\":[{\\"name\\":\\"Adam\\",\\"owes\\":{\\"Bob\\":3.0},\\"owed_by\\":{},\\"balance\\":-3.0},{\\"name\\":\\"Bob\\",\\"owes\\":{},\\"owed_by\\":{\\"Adam\\":3.0},\\"balance\\":3.0}]}"""

    let payload =
        """{\\"lender\\":\\"Adam\\",\\"borrower\\":\\"Bob\\",\\"amount\\":3.0}"""

    let url = "/iou"

    let expected =
        """{\\"users\\":[{\\"name\\":\\"Adam\\",\\"owes\\":{},\\"owed_by\\":{},\\"balance\\":0.0},{\\"name\\":\\"Bob\\",\\"owes\\":{},\\"owed_by\\":{},\\"balance\\":0.0}]}"""

    let api = RestApi(database)
    api.Post(url, payload) |> should equal expected

Quick Fix

The replacement of triple-quoted strings """ with a single quote ones " in the test file helps to address the issue. Here is the example of a test file that successfully passes the rewrite:

module RestApiTests

open FsUnit.Xunit
open Xunit
open RestApi

[<Fact>]
let ``No users`` () =
    let database = "{\"users\":[]}"
    let url = "/users"
    let expected = "{\"users\":[]}"
    let api = RestApi(database)
    api.Get url |> should equal expected

[<Fact(Skip = "Remove this Skip property to run this test")>]
let ``Add user`` () =
    let database = "{\"users\":[]}"
    let payload = "{\"user\":\"Adam\"}"
    let url = "/add"
    let expected = "{\"name\":\"Adam\",\"owes\":{},\"owed_by\":{},\"balance\":0.0}"
    let api = RestApi(database)
    api.Post(url, payload) |> should equal expected

[<Fact(Skip = "Remove this Skip property to run this test")>]
let ``Get single user`` () =
    let database =
        "{\"users\":[{\"name\":\"Adam\",\"owes\":{},\"owed_by\":{},\"balance\":0.0},{\"name\":\"Bob\",\"owes\":{},\"owed_by\":{},\"balance\":0.0}]}"

    let payload = "{\"users\":[\"Bob\"]}"
    let url = "/users"

    let expected =
        "{\"users\":[{\"name\":\"Bob\",\"owes\":{},\"owed_by\":{},\"balance\":0.0}]}"

    let api = RestApi(database)
    api.Get(url, payload) |> should equal expected

[<Fact(Skip = "Remove this Skip property to run this test")>]
let ``Both users have 0 balance`` () =
    let database =
        "{\"users\":[{\"name\":\"Adam\",\"owes\":{},\"owed_by\":{},\"balance\":0.0},{\"name\":\"Bob\",\"owes\":{},\"owed_by\":{},\"balance\":0.0}]}"

    let payload = "{\"lender\":\"Adam\",\"borrower\":\"Bob\",\"amount\":3.0}"
    let url = "/iou"

    let expected =
        "{\"users\":[{\"name\":\"Adam\",\"owes\":{},\"owed_by\":{\"Bob\":3.0},\"balance\":3.0},{\"name\":\"Bob\",\"owes\":{\"Adam\":3.0},\"owed_by\":{},\"balance\":-3.0}]}"

    let api = RestApi(database)
    api.Post(url, payload) |> should equal expected

[<Fact(Skip = "Remove this Skip property to run this test")>]
let ``Borrower has negative balance`` () =
    let database =
        "{\"users\":[{\"name\":\"Adam\",\"owes\":{},\"owed_by\":{},\"balance\":0.0},{\"name\":\"Bob\",\"owes\":{\"Chuck\":3.0},\"owed_by\":{},\"balance\":-3.0},{\"name\":\"Chuck\",\"owes\":{},\"owed_by\":{\"Bob\":3.0},\"balance\":3.0}]}"

    let payload = "{\"lender\":\"Adam\",\"borrower\":\"Bob\",\"amount\":3.0}"
    let url = "/iou"

    let expected =
        "{\"users\":[{\"name\":\"Adam\",\"owes\":{},\"owed_by\":{\"Bob\":3.0},\"balance\":3.0},{\"name\":\"Bob\",\"owes\":{\"Adam\":3.0,\"Chuck\":3.0},\"owed_by\":{},\"balance\":-6.0}]}"

    let api = RestApi(database)
    api.Post(url, payload) |> should equal expected

[<Fact(Skip = "Remove this Skip property to run this test")>]
let ``Lender has negative balance`` () =
    let database =
        "{\"users\":[{\"name\":\"Adam\",\"owes\":{},\"owed_by\":{},\"balance\":0.0},{\"name\":\"Bob\",\"owes\":{\"Chuck\":3.0},\"owed_by\":{},\"balance\":-3.0},{\"name\":\"Chuck\",\"owes\":{},\"owed_by\":{\"Bob\":3.0},\"balance\":3.0}]}"

    let payload = "{\"lender\":\"Bob\",\"borrower\":\"Adam\",\"amount\":3.0}"
    let url = "/iou"

    let expected =
        "{\"users\":[{\"name\":\"Adam\",\"owes\":{\"Bob\":3.0},\"owed_by\":{},\"balance\":-3.0},{\"name\":\"Bob\",\"owes\":{\"Chuck\":3.0},\"owed_by\":{\"Adam\":3.0},\"balance\":0.0}]}"

    let api = RestApi(database)
    api.Post(url, payload) |> should equal expected

[<Fact(Skip = "Remove this Skip property to run this test")>]
let ``Lender owes borrower`` () =
    let database =
        "{\"users\":[{\"name\":\"Adam\",\"owes\":{\"Bob\":3.0},\"owed_by\":{},\"balance\":-3.0},{\"name\":\"Bob\",\"owes\":{},\"owed_by\":{\"Adam\":3.0},\"balance\":3.0}]}"

    let payload = "{\"lender\":\"Adam\",\"borrower\":\"Bob\",\"amount\":2.0}"
    let url = "/iou"

    let expected =
        "{\"users\":[{\"name\":\"Adam\",\"owes\":{\"Bob\":1.0},\"owed_by\":{},\"balance\":-1.0},{\"name\":\"Bob\",\"owes\":{},\"owed_by\":{\"Adam\":1.0},\"balance\":1.0}]}"

    let api = RestApi(database)
    api.Post(url, payload) |> should equal expected

[<Fact(Skip = "Remove this Skip property to run this test")>]
let ``Lender owes borrower less than new loan`` () =
    let database =
        "{\"users\":[{\"name\":\"Adam\",\"owes\":{\"Bob\":3.0},\"owed_by\":{},\"balance\":-3.0},{\"name\":\"Bob\",\"owes\":{},\"owed_by\":{\"Adam\":3.0},\"balance\":3.0}]}"

    let payload = "{\"lender\":\"Adam\",\"borrower\":\"Bob\",\"amount\":4.0}"
    let url = "/iou"

    let expected =
        "{\"users\":[{\"name\":\"Adam\",\"owes\":{},\"owed_by\":{\"Bob\":1.0},\"balance\":1.0},{\"name\":\"Bob\",\"owes\":{\"Adam\":1.0},\"owed_by\":{},\"balance\":-1.0}]}"

    let api = RestApi(database)
    api.Post(url, payload) |> should equal expected

[<Fact(Skip = "Remove this Skip property to run this test")>]
let ``Lender owes borrower same as new loan`` () =
    let database =
        "{\"users\":[{\"name\":\"Adam\",\"owes\":{\"Bob\":3.0},\"owed_by\":{},\"balance\":-3.0},{\"name\":\"Bob\",\"owes\":{},\"owed_by\":{\"Adam\":3.0},\"balance\":3.0}]}"

    let payload = "{\"lender\":\"Adam\",\"borrower\":\"Bob\",\"amount\":3.0}"
    let url = "/iou"

    let expected =
        "{\"users\":[{\"name\":\"Adam\",\"owes\":{},\"owed_by\":{},\"balance\":0.0},{\"name\":\"Bob\",\"owes\":{},\"owed_by\":{},\"balance\":0.0}]}"

    let api = RestApi(database)
    api.Post(url, payload) |> should equal expected
Copy link

Hello. Thanks for opening an issue on Exercism 🙂

At Exercism we use our Community Forum, not GitHub issues, as the primary place for discussion. That allows maintainers and contributors from across Exercism's ecosystem to discuss your problems/ideas/suggestions without them having to subscribe to hundreds of repositories.

This issue will be automatically closed. Please use this link to copy your GitHub Issue into a new topic on the forum, where we look forward to chatting with you!

If you're interested in learning more about this auto-responder, please read this blog post.

@ErikSchierboom
Copy link
Member

@RamanBut-Husaim great find. Would you be willing to submit a PR to fix this?

@RamanBut-Husaim
Copy link
Author

@ErikSchierboom I spent some time trying to understand what might be the issue here with no success, though, as I'm not so fluent with F#.

If there are no time restrictions to address the issue, assuming that I submitted a quick fix PR - exercism/fsharp#1308, I can continue the investigation in my spare time.

Thank you.

@ErikSchierboom
Copy link
Member

@RamanBut-Husaim It could also be that upgrading the Fantomas library fixes this

@RamanBut-Husaim
Copy link
Author

RamanBut-Husaim commented Mar 2, 2025

Hello @ErikSchierboom ,
I've just tried to update Fantomas to the latest version in this branch - abb91f8. It does not fix the issue.

The incorrect escape happens as part of CodeFormatter.FormatASTAsync inside Fantomas (code ref).

So it appears that the issue lives inside the Fantomas library itself.

Thanks.

@ErikSchierboom
Copy link
Member

Thanks for checking!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants