Move error messages out of Lint.Test and into Lint.Test.ErrorMessage

This commit is contained in:
Jeroen Engels 2019-07-11 08:43:56 +02:00
parent 1d3333f489
commit b6464dbff2
4 changed files with 397 additions and 962 deletions

View File

@ -1,638 +0,0 @@
module Lint.Internal.Test exposing
( LintResult, run
, ExpectedError, expectErrors, expectNoErrors, error, atExactly
, parsingErrorMessage, messageMismatchError, wrongLocationError, didNotExpectErrors
, underMismatchError, notEnoughErrors, tooManyErrors, locationIsAmbiguousInSourceCodeError
)
{-| Module that helps you test your linting rules, using [`elm-test`](https://package.elm-lang.org/packages/elm-explorations/test/latest).
import Lint.Test exposing (LintResult)
import Test exposing (Test, describe, test)
import The.Rule.You.Want.To.Test exposing (rule)
testRule : String -> LintResult
testRule string =
Lint.Test.run rule string
-- In this example, the rule we're testing is `NoDebug`
tests : Test
tests =
describe "NoDebug"
[ test "should not report calls to normal functions" <|
\() ->
testRule """module A exposing (..)
a = foo n"""
|> Lint.Test.expectNoErrors
, test "should report Debug.log use" <|
\() ->
testRule """module A exposing (..)
a = Debug.log "some" "message\""""
|> Lint.Test.expectErrors
[ Lint.Test.error
{ message = "Forbidden use of Debug"
, under = "Debug.log"
}
]
]
# Running tests
@docs LintResult, run
# Making assertions
@docs ExpectedError, expectErrors, expectNoErrors, error, atExactly
# Error messages
@docs parsingErrorMessage, messageMismatchError, wrongLocationError, didNotExpectErrors
@docs underMismatchError, notEnoughErrors, tooManyErrors, locationIsAmbiguousInSourceCodeError
# Tips on testing
## What should you test?
TODO Add helpful tips
-}
import Array exposing (Array)
import Elm.Syntax.Range exposing (Range)
import Expect exposing (Expectation)
import Lint exposing (Severity(..), lintSource)
import Lint.Rule as Rule exposing (Error, Rule)
import List.Extra
{-| The result of running a rule on a `String` containing source code.
-}
type LintResult
= ParseFailure
| SuccessfulRun CodeInspector (List Error)
type alias CodeInspector =
{ getCodeAtLocation : Range -> Maybe String
, checkIfLocationIsAmbiguous : Error -> String -> Expectation
}
{-| An expectation for an error. Use [`error`](#error) to create one.
-}
type ExpectedError
= ExpectedError
{ message : String
, under : Under
}
type Under
= Under String
| UnderExactly String Range
type alias SourceCode =
String
{-| Run a `Rule` on a `String` containing source code. You can then use
[`expectNoErrors`](#expectNoErrors) or [`expectErrors`](#expectErrors) to assert
the errors reported by the rule.
The source code needs to be syntactically valid Elm code. If the code
can't be parsed, the test will fail regardless of the expectations you set on it.
Note that t be syntactically valid, you need at least a module declaration at the
top of the file (like `module A exposing (..)`) and one declaration (like `a = 1`).
You can't just have an expression like `1 + 2`.
-}
run : Rule -> String -> LintResult
run rule sourceCode =
case lintSource [ ( Critical, rule ) ] sourceCode of
Ok errors ->
SuccessfulRun
{ getCodeAtLocation = getCodeAtLocationInSourceCode sourceCode
, checkIfLocationIsAmbiguous = checkIfLocationIsAmbiguousInSourceCode sourceCode
}
(List.map (\( _, error_ ) -> Rule.error error_.message error_.range) errors)
Err _ ->
ParseFailure
{-| Assert that the rule reprted no errors. Note, this is equivalent to using [`expectErrors`](#expectErrors)
like `expectErrors []`.
import Lint.Test exposing (LintResult)
import Test exposing (Test, describe, test)
import The.Rule.You.Want.To.Test exposing (rule)
testRule : String -> LintResult
testRule string =
Lint.Test.run rule string
-- In this example, the rule we're testing is `NoDebug`
tests : Test
tests =
describe "NoDebug"
[ test "should not report calls to normal functions" <|
\() ->
testRule """module A exposing (..)
a = foo n"""
|> Lint.Test.expectNoErrors
]
-}
expectNoErrors : LintResult -> Expectation
expectNoErrors lintResult =
case lintResult of
ParseFailure ->
Expect.fail parsingErrorMessage
SuccessfulRun _ errors ->
Expect.true
(didNotExpectErrors errors)
(List.isEmpty errors)
didNotExpectErrors : List Error -> String
didNotExpectErrors errors =
"""I expected no errors but found:
""" ++ (List.map errorToString errors |> String.join "\n ")
{-| Assert that the rule reprted some errors, by specifying which one.
Assert which errors are reported using [`error`](#error). The test will fail if
a different number of errors than expected are reported, or if the message or the
location is incorrect.
The errors should be in the order of where they appear in the source code. An error
at the start of the source code should appear earlier in the list than
an error at the end of the source code.
import Lint.Test exposing (LintResult)
import Test exposing (Test, describe, test)
import The.Rule.You.Want.To.Test exposing (rule)
testRule : String -> LintResult
testRule string =
Lint.Test.run rule string
-- In this example, the rule we're testing is `NoDebug`
tests : Test
tests =
describe "NoDebug"
[ test "should report Debug.log use" <|
\() ->
testRule """module A exposing (..)
a = Debug.log "some" "message\""""
|> Lint.Test.expectErrors
[ Lint.Test.error
{ message = "Forbidden use of Debug"
, under = "Debug.log"
}
]
]
-}
expectErrors : List ExpectedError -> LintResult -> Expectation
expectErrors expectedErrors lintResult =
case lintResult of
ParseFailure ->
Expect.fail parsingErrorMessage
SuccessfulRun codeInspector errors ->
checkAllErrorsMatch codeInspector expectedErrors errors
{-| Create an expectation for an error.
`message` should be the message you're expecting to be shown to the user.
`under` is the part of the code where you are expecting the error to be shown to
the user. If it helps, imagine `under` to be the text under which the squiggly
lines will appear if the error appeared in an editor.
tests : Test
tests =
describe "NoDebug"
[ test "should report Debug.log use" <|
\() ->
testRule """module A exposing (..)
a = Debug.log "some" "message\""""
|> Lint.Test.expectErrors
[ Lint.Test.error
{ message = "Forbidden use of Debug"
, under = "Debug.log"
}
]
]
If there are multiple locations where the value of `under` appears, the test will
fail unless you use [`atExactly`](#atExactly) to remove any ambiguity of where the
error should be used.
-}
error : { message : String, under : String } -> ExpectedError
error input =
ExpectedError
{ message = input.message
, under = Under input.under
}
getUnder : ExpectedError -> String
getUnder (ExpectedError expectedError) =
case expectedError.under of
Under str ->
str
UnderExactly str _ ->
str
{-| Precise the exact position where the error should be shown to the user. This
is only necessary when the `under` field is ambiguous.
`atExactly` takes a record with start and end positions.
tests : Test
tests =
describe "NoDebug"
[ test "should report multiple Debug.log calls" <|
\() ->
testRule """
a = Debug.log z
b = Debug.log z
"""
|> Lint.Test.expectErrors
[ Lint.Test.error
{ message = message
, under = "Debug.log"
}
|> Lint.Test.atExactly { start = { row = 4, column = 5 }, end = { row = 4, column = 14 } }
, Lint.Test.error
{ message = message
, under = "Debug.log"
}
|> Lint.Test.atExactly { start = { row = 5, column = 5 }, end = { row = 5, column = 14 } }
]
]
Tip: By default, do not provide this field. If the test fails because there is some
ambiguity, the test error will give you a recommendation of what to use as a parameter
of `atExactly`, so you do not have to bother writing this hard to write argument.
-}
atExactly : { start : { row : Int, column : Int }, end : { row : Int, column : Int } } -> ExpectedError -> ExpectedError
atExactly range ((ExpectedError expectedError_) as expectedError) =
ExpectedError { expectedError_ | under = UnderExactly (getUnder expectedError) range }
checkAllErrorsMatch : CodeInspector -> List ExpectedError -> List Error -> Expectation
checkAllErrorsMatch codeInspector expectedErrors errors =
checkErrorsMatch codeInspector expectedErrors errors
|> List.reverse
|> (\expectations -> Expect.all expectations ())
checkErrorsMatch : CodeInspector -> List ExpectedError -> List Error -> List (() -> Expectation)
checkErrorsMatch codeInspector expectedErrors errors =
case ( expectedErrors, errors ) of
( [], [] ) ->
[ always Expect.pass ]
( expected :: restOfExpectedErrors, error_ :: restOfErrors ) ->
checkErrorMatch codeInspector expected error_ :: checkErrorsMatch codeInspector restOfExpectedErrors restOfErrors
( expected :: restOfExpectedErrors, [] ) ->
[ always <| Expect.fail <| notEnoughErrors (expected :: restOfExpectedErrors) ]
( [], error_ :: restOfErrors ) ->
[ always <| Expect.fail <| tooManyErrors (error_ :: restOfErrors) ]
checkErrorMatch : CodeInspector -> ExpectedError -> Error -> (() -> Expectation)
checkErrorMatch codeInspector ((ExpectedError expectedError_) as expectedError) error_ =
Expect.all
[ always <| Expect.true (messageMismatchError expectedError error_) (expectedError_.message == Rule.errorMessage error_)
, checkMessageAppearsUnder codeInspector error_ expectedError
]
checkMessageAppearsUnder : CodeInspector -> Error -> ExpectedError -> (() -> Expectation)
checkMessageAppearsUnder codeInspector error_ (ExpectedError expectedError) =
case codeInspector.getCodeAtLocation (Rule.errorRange error_) of
Just codeAtLocation ->
case expectedError.under of
Under under ->
Expect.all
[ always <|
Expect.true
(underMismatchError error_ { under = under, codeAtLocation = codeAtLocation })
(codeAtLocation == under)
, always <| codeInspector.checkIfLocationIsAmbiguous error_ under
]
UnderExactly under range ->
Expect.all
[ always <|
Expect.true
(underMismatchError error_ { under = under, codeAtLocation = codeAtLocation })
(codeAtLocation == under)
, always <|
Expect.true
(wrongLocationError error_ range under)
(Rule.errorRange error_ == range)
]
Nothing ->
always <| Expect.fail impossibleStateError
getCodeAtLocationInSourceCode : SourceCode -> Range -> Maybe String
getCodeAtLocationInSourceCode sourceCode =
let
lines : Array String
lines =
String.lines sourceCode
|> Array.fromList
in
\{ start, end } ->
if start.row == end.row then
Array.get (start.row - 1) lines
|> Maybe.map (String.slice (start.column - 1) (end.column - 1))
else
let
firstLine : Maybe String
firstLine =
Array.get (start.row - 1) lines
|> Maybe.map (String.dropLeft (start.column - 1))
lastLine : Maybe String
lastLine =
Array.get (end.row - 1) lines
|> Maybe.map (String.dropRight end.column)
in
[ [ firstLine ]
, Array.slice start.row (end.row - 1) lines
|> Array.toList
|> List.map Just
, [ lastLine ]
]
|> List.concat
|> List.filterMap identity
|> String.join "\n"
|> Just
formatSourceCode : String -> String
formatSourceCode string =
let
lines =
String.lines string
in
if List.length lines == 1 then
"`" ++ string ++ "`"
else
lines
|> List.map (\str -> " " ++ str)
|> String.join "\n"
|> (\str -> "```\n" ++ str ++ "\n ```")
checkIfLocationIsAmbiguousInSourceCode : SourceCode -> Error -> String -> Expectation
checkIfLocationIsAmbiguousInSourceCode sourceCode error_ under =
let
occurrencesInSourceCode : List Int
occurrencesInSourceCode =
String.indexes under sourceCode
in
Expect.true
(locationIsAmbiguousInSourceCodeError sourceCode error_ under occurrencesInSourceCode)
(List.length occurrencesInSourceCode == 1)
-- ERROR MESSAGES
parsingErrorMessage : String
parsingErrorMessage =
"""I could not parse the test source code, because it was not syntactically valid Elm code.
Maybe you forgot to add the module definition at the top, like:
`module A exposing (..)`"""
messageMismatchError : ExpectedError -> Error -> String
messageMismatchError (ExpectedError expectedError) error_ =
"""I was looking for the error with the following message:
`""" ++ expectedError.message ++ """`
but I found the following error message:
`""" ++ Rule.errorMessage error_ ++ "`"
underMismatchError : Error -> { under : String, codeAtLocation : String } -> String
underMismatchError error_ { under, codeAtLocation } =
"""I found an error with the following message:
`""" ++ Rule.errorMessage error_ ++ """`
which I was expecting, but I found it under:
""" ++ formatSourceCode codeAtLocation ++ """
when I was expecting it under:
""" ++ formatSourceCode under ++ """
Hint: Maybe you're passing the `Range` of a wrong node when calling `Rule.error`"""
wrongLocationError : Error -> Range -> String -> String
wrongLocationError error_ range under =
"""I was looking for the error with the following message:
`""" ++ Rule.errorMessage error_ ++ """`
under the following code:
""" ++ formatSourceCode under ++ """
and I found it, but the exact location you specified is not the one I found. I was expecting the error at:
""" ++ rangeAsString range ++ """
but I found it at:
""" ++ rangeAsString (Rule.errorRange error_)
listOccurrencesAsLocations : SourceCode -> String -> List Int -> String
listOccurrencesAsLocations sourceCode under occurrences =
occurrences
|> List.map
(\occurrence ->
occurrence
|> positionAsRange sourceCode under
|> rangeAsString
|> (++) " - "
)
|> String.join "\n"
positionAsRange : SourceCode -> String -> Int -> Range
positionAsRange sourceCode under position =
let
linesBeforeAndIncludingPosition : List String
linesBeforeAndIncludingPosition =
sourceCode
|> String.slice 0 position
|> String.lines
startRow : Int
startRow =
List.length linesBeforeAndIncludingPosition
startColumn : Int
startColumn =
linesBeforeAndIncludingPosition
|> List.Extra.last
|> Maybe.withDefault ""
|> String.length
|> (+) 1
linesInUnder : List String
linesInUnder =
String.lines under
endRow : Int
endRow =
startRow + List.length linesInUnder - 1
endColumn : Int
endColumn =
if startRow == endRow then
startColumn + String.length under
else
linesInUnder
|> List.Extra.last
|> Maybe.withDefault ""
|> String.length
|> (+) 1
in
{ start =
{ row = startRow
, column = startColumn
}
, end =
{ row = endRow
, column = endColumn
}
}
errorToString : Error -> String
errorToString error_ =
"- \"" ++ Rule.errorMessage error_ ++ "\" at " ++ rangeAsString (Rule.errorRange error_)
rangeAsString : Range -> String
rangeAsString { start, end } =
"{ start = { row = " ++ String.fromInt start.row ++ ", column = " ++ String.fromInt start.column ++ " }, end = { row = " ++ String.fromInt end.row ++ ", column = " ++ String.fromInt end.column ++ " } }"
notEnoughErrors : List ExpectedError -> String
notEnoughErrors missingExpectedErrors =
let
numberOfErrors : Int
numberOfErrors =
List.length missingExpectedErrors
in
"I expected to see "
++ String.fromInt numberOfErrors
++ " more "
++ pluralizeErrors numberOfErrors
++ ":\n\n"
++ (missingExpectedErrors
|> List.map expectedErrorToString
|> String.join "\n"
)
wrapInQuotes : String -> String
wrapInQuotes string =
"\"" ++ string ++ "\""
tooManyErrors : List Error -> String
tooManyErrors extraErrors =
let
numberOfErrors : Int
numberOfErrors =
List.length extraErrors
in
"I found "
++ String.fromInt numberOfErrors
++ " "
++ pluralizeErrors numberOfErrors
++ " too many:\n\n"
++ (extraErrors
|> List.map errorToString
|> String.join "\n"
)
locationIsAmbiguousInSourceCodeError : SourceCode -> Error -> String -> List Int -> String
locationIsAmbiguousInSourceCodeError sourceCode error_ under occurrencesInSourceCode =
"""Your test passes, but where the message appears is ambiguous.
You are looking for the following error message:
`""" ++ Rule.errorMessage error_ ++ """`
and expecting to see it under:
""" ++ formatSourceCode under ++ """
I found """ ++ String.fromInt (List.length occurrencesInSourceCode) ++ """ locations where that code appeared. Please use `Lint.Rule.atExactly` to make the part you were targetting unambiguous.
Tip: I found them at:
""" ++ listOccurrencesAsLocations sourceCode under occurrencesInSourceCode
impossibleStateError : String
impossibleStateError =
"Oh no! I'm in an impossible state. I found an error at a location that I could not find back. Please let me know and give me an SSCCE (http://sscce.org/) here: https://github.com/jfmengels/elm-lint/issues."
pluralizeErrors : Int -> String
pluralizeErrors n =
case n of
1 ->
"error"
_ ->
"errors"
expectedErrorToString : ExpectedError -> String
expectedErrorToString (ExpectedError expectedError) =
"- " ++ wrapInQuotes expectedError.message

View File

@ -59,7 +59,7 @@ import Elm.Syntax.Range exposing (Range)
import Expect exposing (Expectation)
import Lint exposing (Severity(..), lintSource)
import Lint.Rule as Rule exposing (Error, Rule)
import List.Extra
import Lint.Test.ErrorMessage as ErrorMessage
{-| The result of running a rule on a `String` containing source code.
@ -89,8 +89,8 @@ type Under
| UnderExactly String Range
type SourceCode
= SourceCode String
type alias SourceCode =
String
{-| Run a `Rule` on a `String` containing source code. You can then use
@ -110,8 +110,8 @@ run rule sourceCode =
case lintSource [ ( Critical, rule ) ] sourceCode of
Ok errors ->
SuccessfulRun
{ getCodeAtLocation = getCodeAtLocationInSourceCode (SourceCode sourceCode)
, checkIfLocationIsAmbiguous = checkIfLocationIsAmbiguousInSourceCode (SourceCode sourceCode)
{ getCodeAtLocation = getCodeAtLocationInSourceCode sourceCode
, checkIfLocationIsAmbiguous = checkIfLocationIsAmbiguousInSourceCode sourceCode
}
(List.map (\( _, error_ ) -> Rule.error error_.message error_.range) errors)
@ -119,7 +119,7 @@ run rule sourceCode =
ParseFailure
{-| Assert that the rule reprted no errors. Note, this is equivalent to using [`expectErrors`](#expectErrors)
{-| Assert that the rule reported no errors. Note, this is equivalent to using [`expectErrors`](#expectErrors)
like `expectErrors []`.
import Lint.Test exposing (LintResult)
@ -146,15 +146,15 @@ expectNoErrors : LintResult -> Expectation
expectNoErrors lintResult =
case lintResult of
ParseFailure ->
Expect.fail parsingErrorMessage
Expect.fail ErrorMessage.parsingFailure
SuccessfulRun _ errors ->
Expect.true
("I expected no errors but found:\n\n" ++ (List.map errorToString errors |> String.join "\n"))
(ErrorMessage.didNotExpectErrors errors)
(List.isEmpty errors)
{-| Assert that the rule reprted some errors, by specifying which one.
{-| Assert that the rule reported some errors, by specifying which one.
Assert which errors are reported using [`error`](#error). The test will fail if
a different number of errors than expected are reported, or if the message or the
@ -193,7 +193,7 @@ expectErrors : List ExpectedError -> LintResult -> Expectation
expectErrors expectedErrors lintResult =
case lintResult of
ParseFailure ->
Expect.fail parsingErrorMessage
Expect.fail ErrorMessage.parsingFailure
SuccessfulRun codeInspector errors ->
checkAllErrorsMatch codeInspector expectedErrors errors
@ -235,16 +235,6 @@ error input =
}
getUnder : ExpectedError -> String
getUnder (ExpectedError expectedError) =
case expectedError.under of
Under str ->
str
UnderExactly str _ ->
str
{-| Precise the exact position where the error should be shown to the user. This
is only necessary when the `under` field is ambiguous.
@ -283,60 +273,18 @@ atExactly range ((ExpectedError expectedError_) as expectedError) =
ExpectedError { expectedError_ | under = UnderExactly (getUnder expectedError) range }
checkAllErrorsMatch : CodeInspector -> List ExpectedError -> List Error -> Expectation
checkAllErrorsMatch codeInspector expectedErrors errors =
checkErrorsMatch codeInspector expectedErrors errors
|> List.reverse
|> (\expectations -> Expect.all expectations ())
checkErrorsMatch : CodeInspector -> List ExpectedError -> List Error -> List (() -> Expectation)
checkErrorsMatch codeInspector expectedErrors errors =
case ( expectedErrors, errors ) of
( [], [] ) ->
[ always Expect.pass ]
( expected :: restOfExpectedErrors, error_ :: restOfErrors ) ->
checkErrorMatch codeInspector expected error_ :: checkErrorsMatch codeInspector restOfExpectedErrors restOfErrors
( expected :: restOfExpectedErrors, [] ) ->
[ always <| Expect.fail <| notEnoughErrors expected restOfExpectedErrors ]
( [], error_ :: restOfErrors ) ->
[ always <| Expect.fail <| tooManyErrors error_ restOfErrors ]
checkErrorMatch : CodeInspector -> ExpectedError -> Error -> (() -> Expectation)
checkErrorMatch codeInspector ((ExpectedError expectedError_) as expectedError) error_ =
Expect.all
[ always <| Expect.true (messageMismatchError expectedError error_) (expectedError_.message == Rule.errorMessage error_)
, checkMessageAppearsUnder codeInspector error_ expectedError
]
checkMessageAppearsUnder : CodeInspector -> Error -> ExpectedError -> (() -> Expectation)
checkMessageAppearsUnder codeInspector error_ (ExpectedError expectedError) =
case codeInspector.getCodeAtLocation (Rule.errorRange error_) of
Just codeAtLocation ->
getUnder : ExpectedError -> String
getUnder (ExpectedError expectedError) =
case expectedError.under of
Under under ->
Expect.all
[ always <| Expect.true (underMismatchError error_ under codeAtLocation) (codeAtLocation == under)
, always <| codeInspector.checkIfLocationIsAmbiguous error_ under
]
Under str ->
str
UnderExactly under range ->
Expect.all
[ always <| Expect.true (underMismatchError error_ under codeAtLocation) (codeAtLocation == under)
, always <| Expect.true (wrongLocationError error_ range under) (Rule.errorRange error_ == range)
]
Nothing ->
always <| Expect.fail impossibleStateError
UnderExactly str _ ->
str
getCodeAtLocationInSourceCode : SourceCode -> Range -> Maybe String
getCodeAtLocationInSourceCode (SourceCode sourceCode) =
getCodeAtLocationInSourceCode sourceCode =
let
lines : Array String
lines =
@ -372,233 +320,91 @@ getCodeAtLocationInSourceCode (SourceCode sourceCode) =
|> Just
formatSourceCode : String -> String
formatSourceCode string =
let
lines =
String.lines string
in
if List.length lines == 1 then
"`" ++ string ++ "`"
else
lines
|> List.map (\str -> " " ++ str)
|> String.join "\n"
|> (\str -> "\n\n```\n" ++ str ++ "\n```")
checkIfLocationIsAmbiguousInSourceCode : SourceCode -> Error -> String -> Expectation
checkIfLocationIsAmbiguousInSourceCode ((SourceCode sourceCodeContent) as sourceCode) error_ under =
checkIfLocationIsAmbiguousInSourceCode sourceCode error_ under =
let
occurrencesInSourceCode : List Int
occurrencesInSourceCode =
String.indexes under sourceCodeContent
String.indexes under sourceCode
in
Expect.true
(locationIsAmbiguousInSourceCodeError sourceCode error_ under occurrencesInSourceCode)
(ErrorMessage.locationIsAmbiguousInSourceCode sourceCode error_ under occurrencesInSourceCode)
(List.length occurrencesInSourceCode == 1)
-- ERROR MESSAGES
-- RUNNING THE CHECKS
parsingErrorMessage : String
parsingErrorMessage =
"""I could not parse the test source code, because it was not syntactically valid Elm code.
Maybe you forgot to add the module definition at the top, like:
module A exposing (..)"""
checkAllErrorsMatch : CodeInspector -> List ExpectedError -> List Error -> Expectation
checkAllErrorsMatch codeInspector expectedErrors errors =
checkErrorsMatch codeInspector expectedErrors errors
|> List.reverse
|> (\expectations -> Expect.all expectations ())
messageMismatchError : ExpectedError -> Error -> String
messageMismatchError (ExpectedError expectedError) error_ =
"""I was looking for the error with the following message:
checkErrorsMatch : CodeInspector -> List ExpectedError -> List Error -> List (() -> Expectation)
checkErrorsMatch codeInspector expectedErrors errors =
case ( expectedErrors, errors ) of
( [], [] ) ->
[ always Expect.pass ]
`""" ++ expectedError.message ++ """`
( expected :: restOfExpectedErrors, error_ :: restOfErrors ) ->
checkErrorMatch codeInspector expected error_ :: checkErrorsMatch codeInspector restOfExpectedErrors restOfErrors
but I found the following error message:
( expected :: restOfExpectedErrors, [] ) ->
[ always <| Expect.fail <| ErrorMessage.expectedMoreErrors <| List.map extractExpectedErrorData (expected :: restOfExpectedErrors) ]
`""" ++ Rule.errorMessage error_ ++ "`"
( [], error_ :: restOfErrors ) ->
[ always <| Expect.fail <| ErrorMessage.tooManyErrors (error_ :: restOfErrors) ]
wrongLocationError : Error -> Range -> String -> String
wrongLocationError error_ range under =
"""I was looking for the error with the following message:
`""" ++ Rule.errorMessage error_ ++ """`
under the following code:
""" ++ formatSourceCode under ++ """
and I found it, but the exact location you specified is not the one I found. I was expecting the error at:
""" ++ rangeAsString range ++ """
but I found it at:
""" ++ rangeAsString (Rule.errorRange error_)
underMismatchError : Error -> String -> String -> String
underMismatchError error_ under codeAtLocation =
"""I found an error with the right message, but at the wrong location:
Message: `""" ++ Rule.errorMessage error_ ++ """`
I saw it under: """ ++ formatSourceCode codeAtLocation ++ """
But I expected to see it under: """ ++ formatSourceCode under
listOccurrencesAsLocations : SourceCode -> String -> List Int -> String
listOccurrencesAsLocations sourceCode under occurrences =
occurrences
|> List.map
(\occurrence ->
occurrence
|> positionAsRange sourceCode under
|> rangeAsString
|> (++) " - "
checkErrorMatch : CodeInspector -> ExpectedError -> Error -> (() -> Expectation)
checkErrorMatch codeInspector ((ExpectedError expectedError_) as expectedError) error_ =
Expect.all
[ \_ ->
(expectedError_.message == Rule.errorMessage error_)
|> Expect.true
(ErrorMessage.messageMismatch
(extractExpectedErrorData expectedError)
error_
)
|> String.join "\n"
, checkMessageAppearsUnder codeInspector error_ expectedError
]
positionAsRange : SourceCode -> String -> Int -> Range
positionAsRange (SourceCode sourceCode) under position =
let
linesBeforeAndIncludingPosition : List String
linesBeforeAndIncludingPosition =
sourceCode
|> String.slice 0 position
|> String.lines
checkMessageAppearsUnder : CodeInspector -> Error -> ExpectedError -> (() -> Expectation)
checkMessageAppearsUnder codeInspector error_ (ExpectedError expectedError) =
case codeInspector.getCodeAtLocation (Rule.errorRange error_) of
Just codeAtLocation ->
case expectedError.under of
Under under ->
Expect.all
[ always <|
Expect.true
(ErrorMessage.underMismatch error_ { under = under, codeAtLocation = codeAtLocation })
(codeAtLocation == under)
, always <| codeInspector.checkIfLocationIsAmbiguous error_ under
]
startRow : Int
startRow =
List.length linesBeforeAndIncludingPosition
UnderExactly under range ->
Expect.all
[ always <|
Expect.true
(ErrorMessage.underMismatch error_ { under = under, codeAtLocation = codeAtLocation })
(codeAtLocation == under)
, always <|
Expect.true
(ErrorMessage.wrongLocation error_ range under)
(Rule.errorRange error_ == range)
]
startColumn : Int
startColumn =
linesBeforeAndIncludingPosition
|> List.Extra.last
|> Maybe.withDefault ""
|> String.length
|> (+) 1
Nothing ->
always <| Expect.fail ErrorMessage.impossibleState
linesInUnder : List String
linesInUnder =
String.lines under
endRow : Int
endRow =
startRow + List.length linesInUnder - 1
endColumn : Int
endColumn =
if startRow == endRow then
startColumn + String.length under
else
linesInUnder
|> Debug.log "linesInUnder"
|> List.Extra.last
|> Debug.log "last"
|> Maybe.withDefault ""
|> String.length
|> (+) 1
in
{ start =
{ row = startRow
, column = startColumn
extractExpectedErrorData : ExpectedError -> ErrorMessage.ExpectedErrorData
extractExpectedErrorData ((ExpectedError expectedErrorContent) as expectedError) =
{ message = expectedErrorContent.message
, under = getUnder expectedError
}
, end =
{ row = endRow
, column = endColumn
}
}
errorToString : Error -> String
errorToString error_ =
"- \"" ++ Rule.errorMessage error_ ++ "\" at " ++ rangeAsString (Rule.errorRange error_)
rangeAsString : Range -> String
rangeAsString { start, end } =
"{ start = { row = " ++ String.fromInt start.row ++ ", column = " ++ String.fromInt start.column ++ " }, end = { row = " ++ String.fromInt end.row ++ ", column = " ++ String.fromInt end.column ++ " } }"
notEnoughErrors : ExpectedError -> List ExpectedError -> String
notEnoughErrors expected restOfExpectedErrors =
let
numberOfErrors : Int
numberOfErrors =
List.length restOfExpectedErrors + 1
in
"I expected to see "
++ String.fromInt numberOfErrors
++ " more "
++ pluralizeErrors numberOfErrors
++ ":\n\n"
++ (List.map expectedErrorToString (expected :: restOfExpectedErrors) |> String.join "\n")
wrapInQuotes : String -> String
wrapInQuotes string =
"\"" ++ string ++ "\""
tooManyErrors : Error -> List Error -> String
tooManyErrors error_ restOfErrors =
let
numberOfErrors : Int
numberOfErrors =
List.length restOfErrors + 1
in
"I found "
++ String.fromInt numberOfErrors
++ " "
++ pluralizeErrors numberOfErrors
++ " too many:\n"
++ (List.map errorToString (error_ :: restOfErrors) |> String.join "\n")
locationIsAmbiguousInSourceCodeError : SourceCode -> Error -> String -> List Int -> String
locationIsAmbiguousInSourceCodeError sourceCode error_ under occurrencesInSourceCode =
"""Your test passes, but where the message appears is ambiguous.
You are looking for the following error message:
`""" ++ Rule.errorMessage error_ ++ """`
and expecting to see it under:
""" ++ formatSourceCode under ++ """
I found """ ++ String.fromInt (List.length occurrencesInSourceCode) ++ """ locations where that code appeared. Please use `Lint.Rule.atExactly` to make the part you were targetting unambiguous.
Tip: I found them at:
""" ++ listOccurrencesAsLocations sourceCode under occurrencesInSourceCode
impossibleStateError : String
impossibleStateError =
"Oh no! I'm in an impossible state. I found an error at a location that I could not find back. Please let me know and give me an SSCCE (http://sscce.org/) here: https://github.com/jfmengels/elm-lint/issues."
pluralizeErrors : Int -> String
pluralizeErrors n =
case n of
1 ->
"error"
_ ->
"errors"
expectedErrorToString : ExpectedError -> String
expectedErrorToString (ExpectedError expectedError) =
"- " ++ wrapInQuotes expectedError.message

View File

@ -0,0 +1,270 @@
module Lint.Test.ErrorMessage exposing
( ExpectedErrorData
, parsingFailure, messageMismatch, wrongLocation, didNotExpectErrors
, underMismatch, expectedMoreErrors, tooManyErrors, locationIsAmbiguousInSourceCode
, impossibleState
)
{-| Error messages for the `Lint.Test` module.
# Error messages
@docs ExpectedErrorData
@docs parsingFailure, messageMismatch, wrongLocation, didNotExpectErrors
@docs underMismatch, expectedMoreErrors, tooManyErrors, locationIsAmbiguousInSourceCode
@docs impossibleState
-}
import Elm.Syntax.Range exposing (Range)
import Lint.Rule as Rule exposing (Error, Rule)
import List.Extra
{-| An expectation for an error. Use [`error`](#error) to create one.
-}
type alias ExpectedErrorData =
{ message : String
, under : String
}
type alias SourceCode =
String
-- ERROR MESSAGES
didNotExpectErrors : List Error -> String
didNotExpectErrors errors =
"""I expected no errors but found:
""" ++ (List.map errorToString errors |> String.join "\n ")
parsingFailure : String
parsingFailure =
"""I could not parse the test source code, because it was not syntactically valid Elm code.
Maybe you forgot to add the module definition at the top, like:
`module A exposing (..)`"""
messageMismatch : ExpectedErrorData -> Error -> String
messageMismatch expectedError error_ =
"""I was looking for the error with the following message:
`""" ++ expectedError.message ++ """`
but I found the following error message:
`""" ++ Rule.errorMessage error_ ++ "`"
underMismatch : Error -> { under : String, codeAtLocation : String } -> String
underMismatch error_ { under, codeAtLocation } =
"""I found an error with the following message:
`""" ++ Rule.errorMessage error_ ++ """`
which I was expecting, but I found it under:
""" ++ formatSourceCode codeAtLocation ++ """
when I was expecting it under:
""" ++ formatSourceCode under ++ """
Hint: Maybe you're passing the `Range` of a wrong node when calling `Rule.error`"""
wrongLocation : Error -> Range -> String -> String
wrongLocation error_ range under =
"""I was looking for the error with the following message:
`""" ++ Rule.errorMessage error_ ++ """`
under the following code:
""" ++ formatSourceCode under ++ """
and I found it, but the exact location you specified is not the one I found. I was expecting the error at:
""" ++ rangeAsString range ++ """
but I found it at:
""" ++ rangeAsString (Rule.errorRange error_)
listOccurrencesAsLocations : SourceCode -> String -> List Int -> String
listOccurrencesAsLocations sourceCode under occurrences =
occurrences
|> List.map
(\occurrence ->
occurrence
|> positionAsRange sourceCode under
|> rangeAsString
|> (++) " - "
)
|> String.join "\n"
positionAsRange : SourceCode -> String -> Int -> Range
positionAsRange sourceCode under position =
let
linesBeforeAndIncludingPosition : List String
linesBeforeAndIncludingPosition =
sourceCode
|> String.slice 0 position
|> String.lines
startRow : Int
startRow =
List.length linesBeforeAndIncludingPosition
startColumn : Int
startColumn =
linesBeforeAndIncludingPosition
|> List.Extra.last
|> Maybe.withDefault ""
|> String.length
|> (+) 1
linesInUnder : List String
linesInUnder =
String.lines under
endRow : Int
endRow =
startRow + List.length linesInUnder - 1
endColumn : Int
endColumn =
if startRow == endRow then
startColumn + String.length under
else
linesInUnder
|> List.Extra.last
|> Maybe.withDefault ""
|> String.length
|> (+) 1
in
{ start =
{ row = startRow
, column = startColumn
}
, end =
{ row = endRow
, column = endColumn
}
}
errorToString : Error -> String
errorToString error_ =
"- \"" ++ Rule.errorMessage error_ ++ "\" at " ++ rangeAsString (Rule.errorRange error_)
rangeAsString : Range -> String
rangeAsString { start, end } =
"{ start = { row = " ++ String.fromInt start.row ++ ", column = " ++ String.fromInt start.column ++ " }, end = { row = " ++ String.fromInt end.row ++ ", column = " ++ String.fromInt end.column ++ " } }"
expectedMoreErrors : List ExpectedErrorData -> String
expectedMoreErrors missingExpectedErrors =
let
numberOfErrors : Int
numberOfErrors =
List.length missingExpectedErrors
in
"I expected to see "
++ String.fromInt numberOfErrors
++ " more "
++ pluralizeErrors numberOfErrors
++ ":\n\n"
++ (missingExpectedErrors
|> List.map expectedErrorToString
|> String.join "\n"
)
wrapInQuotes : String -> String
wrapInQuotes string =
"\"" ++ string ++ "\""
tooManyErrors : List Error -> String
tooManyErrors extraErrors =
let
numberOfErrors : Int
numberOfErrors =
List.length extraErrors
in
"I found "
++ String.fromInt numberOfErrors
++ " "
++ pluralizeErrors numberOfErrors
++ " too many:\n\n"
++ (extraErrors
|> List.map errorToString
|> String.join "\n"
)
locationIsAmbiguousInSourceCode : SourceCode -> Error -> String -> List Int -> String
locationIsAmbiguousInSourceCode sourceCode error_ under occurrencesInSourceCode =
"""Your test passes, but where the message appears is ambiguous.
You are looking for the following error message:
`""" ++ Rule.errorMessage error_ ++ """`
and expecting to see it under:
""" ++ formatSourceCode under ++ """
I found """ ++ String.fromInt (List.length occurrencesInSourceCode) ++ """ locations where that code appeared. Please use `Lint.Rule.atExactly` to make the part you were targetting unambiguous.
Tip: I found them at:
""" ++ listOccurrencesAsLocations sourceCode under occurrencesInSourceCode
impossibleState : String
impossibleState =
"Oh no! I'm in an impossible state. I found an error at a location that I could not find back. Please let me know and give me an SSCCE (http://sscce.org/) here: https://github.com/jfmengels/elm-lint/issues."
pluralizeErrors : Int -> String
pluralizeErrors n =
if n == 1 then
"error"
else
"errors"
expectedErrorToString : ExpectedErrorData -> String
expectedErrorToString expectedError =
"- " ++ wrapInQuotes expectedError.message
formatSourceCode : String -> String
formatSourceCode string =
let
lines =
String.lines string
in
if List.length lines == 1 then
"`" ++ string ++ "`"
else
lines
|> List.map (\str -> " " ++ str)
|> String.join "\n"
|> (\str -> "```\n" ++ str ++ "\n ```")

View File

@ -2,29 +2,29 @@ module ErrorMessageTest exposing (all)
import Elm.Syntax.Range exposing (Range)
import Expect
import Lint.Internal.Test exposing (ExpectedError, LintResult)
import Lint.Rule as Rule exposing (Error)
import Lint.Test.ErrorMessage as ErrorMessage exposing (ExpectedErrorData)
import Test exposing (Test, describe, test)
all : Test
all =
describe "Test.ErrorMessage"
[ parsingErrorMessageTest
[ parsingFailureTest
, didNotExpectErrorsTest
, messageMismatchErrorTest
, underMismatchErrorTest
, wrongLocationErrorTest
, notEnoughErrorsTest
, messageMismatchTest
, underMismatchTest
, wrongLocationTest
, expectedMoreErrorsTest
, tooManyErrorsTest
]
parsingErrorMessageTest : Test
parsingErrorMessageTest =
test "parsingErrorMessage" <|
parsingFailureTest : Test
parsingFailureTest =
test "parsingFailure" <|
\() ->
Lint.Internal.Test.parsingErrorMessage
ErrorMessage.parsingFailure
|> Expect.equal (String.trim """
I could not parse the test source code, because it was not syntactically valid Elm code.
@ -44,7 +44,7 @@ didNotExpectErrorsTest =
, Rule.error "Some other error" dummyRange
]
in
Lint.Internal.Test.didNotExpectErrors errors
ErrorMessage.didNotExpectErrors errors
|> Expect.equal (String.trim """
I expected no errors but found:
@ -53,14 +53,13 @@ I expected no errors but found:
""")
messageMismatchErrorTest : Test
messageMismatchErrorTest =
test "messageMismatchError" <|
messageMismatchTest : Test
messageMismatchTest =
test "messageMismatch" <|
\() ->
let
expectedError : ExpectedError
expectedError : ExpectedErrorData
expectedError =
Lint.Internal.Test.error
{ message = "Forbidden use of Debug"
, under = "Debug.log"
}
@ -69,7 +68,7 @@ messageMismatchErrorTest =
error =
Rule.error "Forbidden use of Debu" dummyRange
in
Lint.Internal.Test.messageMismatchError expectedError error
ErrorMessage.messageMismatch expectedError error
|> Expect.equal (String.trim """
I was looking for the error with the following message:
@ -80,9 +79,9 @@ but I found the following error message:
`Forbidden use of Debu`""")
underMismatchErrorTest : Test
underMismatchErrorTest =
describe "underMismatchError"
underMismatchTest : Test
underMismatchTest =
describe "underMismatch"
[ test "with single-line extracts" <|
\() ->
let
@ -90,7 +89,7 @@ underMismatchErrorTest =
error =
Rule.error "Some error" dummyRange
in
Lint.Internal.Test.underMismatchError
ErrorMessage.underMismatch
error
{ under = "abcd"
, codeAtLocation = "abcd = 1"
@ -116,7 +115,7 @@ Hint: Maybe you're passing the `Range` of a wrong node when calling `Rule.error`
error =
Rule.error "Some other error" dummyRange
in
Lint.Internal.Test.underMismatchError
ErrorMessage.underMismatch
error
{ under = "abcd =\n 1\n + 2"
, codeAtLocation = "abcd =\n 1"
@ -145,9 +144,9 @@ Hint: Maybe you're passing the `Range` of a wrong node when calling `Rule.error`
]
wrongLocationErrorTest : Test
wrongLocationErrorTest =
describe "wrongLocationError"
wrongLocationTest : Test
wrongLocationTest =
describe "wrongLocation"
[ test "with single-line extracts" <|
\() ->
let
@ -157,7 +156,7 @@ wrongLocationErrorTest =
"Some error"
{ start = { row = 3, column = 1 }, end = { row = 3, column = 5 } }
in
Lint.Internal.Test.wrongLocationError
ErrorMessage.wrongLocation
error
{ start = { row = 2, column = 1 }, end = { row = 2, column = 5 } }
"abcd"
@ -187,7 +186,7 @@ but I found it at:
"Some other error"
{ start = { row = 4, column = 1 }, end = { row = 5, column = 3 } }
in
Lint.Internal.Test.wrongLocationError
ErrorMessage.wrongLocation
error
{ start = { row = 2, column = 1 }, end = { row = 3, column = 3 } }
"abcd =\n 1"
@ -214,24 +213,22 @@ but I found it at:
]
notEnoughErrorsTest : Test
notEnoughErrorsTest =
test "notEnoughErrors" <|
expectedMoreErrorsTest : Test
expectedMoreErrorsTest =
test "expectedMoreErrors" <|
\() ->
let
missingErrors : List ExpectedError
missingErrors : List ExpectedErrorData
missingErrors =
[ Lint.Internal.Test.error
{ message = "Forbidden use of Debug"
[ { message = "Forbidden use of Debug"
, under = "Debug.log"
}
, Lint.Internal.Test.error
{ message = "Forbidden use of Debug"
, { message = "Forbidden use of Debug"
, under = "Debug.log"
}
]
in
Lint.Internal.Test.notEnoughErrors missingErrors
ErrorMessage.expectedMoreErrors missingErrors
|> Expect.equal (String.trim """
I expected to see 2 more errors:
@ -253,7 +250,7 @@ tooManyErrorsTest =
{ start = { row = 2, column = 1 }, end = { row = 2, column = 5 } }
]
in
Lint.Internal.Test.tooManyErrors extraErrors
ErrorMessage.tooManyErrors extraErrors
|> Expect.equal (String.trim """
I found 1 error too many:
@ -272,7 +269,7 @@ I found 1 error too many:
{ start = { row = 3, column = 1 }, end = { row = 3, column = 5 } }
]
in
Lint.Internal.Test.tooManyErrors extraErrors
ErrorMessage.tooManyErrors extraErrors
|> Expect.equal (String.trim """
I found 2 errors too many:
@ -282,9 +279,9 @@ I found 2 errors too many:
]
locationIsAmbiguousInSourceCodeErrorTest : Test
locationIsAmbiguousInSourceCodeErrorTest =
describe "locationIsAmbiguousInSourceCodeError"
locationIsAmbiguousInSourceCodeTest : Test
locationIsAmbiguousInSourceCodeTest =
describe "locationIsAmbiguousInSourceCode"
[ test "with single-line extracts" <|
\() ->
let
@ -302,7 +299,7 @@ locationIsAmbiguousInSourceCodeErrorTest =
"Some error"
{ start = { row = 3, column = 1 }, end = { row = 3, column = 5 } }
in
Lint.Internal.Test.locationIsAmbiguousInSourceCodeError
ErrorMessage.locationIsAmbiguousInSourceCode
sourceCode
error
under
@ -341,7 +338,7 @@ Tip: I found them at:
"Some other error"
{ start = { row = 3, column = 1 }, end = { row = 4, column = 3 } }
in
Lint.Internal.Test.locationIsAmbiguousInSourceCodeError
ErrorMessage.locationIsAmbiguousInSourceCode
sourceCode
error
under