Skip to content

Commit 50db4b0

Browse files
authored
Add basic spec tests (#84)
* Add basic spec tests * remove unused import * Don't export chainl' and chainr' * fix endBy and sepEndBy semantics to align with docs * update changelog * add anychar to benchmark
1 parent 449b87d commit 50db4b0

File tree

7 files changed

+183
-22
lines changed

7 files changed

+183
-22
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Breaking changes:
99
New features:
1010

1111
Bugfixes:
12+
- Do not export `chainl'` and `chainr'` helper functions (#84 by @chtenb)
13+
- Fix semantics of endBy and sepEndBy parser combinators (#84 by @chtenb)
1214
- Issue #69: Fix regex parser to always wrap pattern inside `^(..)` (#80 by @chtenb)
1315

1416
Other improvements:

bench/Main.purs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
-- | This benchmark suite is intended to guide changes to this package so that
66
-- | we can compare the benchmarks of different commits.
77

8-
98
module Bench.Main where
109

1110
import Prelude
@@ -20,14 +19,17 @@ import Text.Parsing.StringParser (Parser, runParser)
2019
import Text.Parsing.StringParser.CodePoints as StringParser.CodePoints
2120
import Text.Parsing.StringParser.CodeUnits as StringParser.CodeUnits
2221

23-
string23 :: String
24-
string23 = "23"
25-
26-
string23_2 :: String
27-
string23_2 = fold $ replicate 2 string23
22+
string23_100 :: String
23+
string23_100 = fold $ replicate 100 "23"
2824

2925
string23_10000 :: String
30-
string23_10000 = fold $ replicate 10000 string23
26+
string23_10000 = fold $ replicate 100 string23_100
27+
28+
parse23AnyCharPoints :: Parser (List Char)
29+
parse23AnyCharPoints = manyRec StringParser.CodePoints.anyChar
30+
31+
parse23AnyCharUnits :: Parser (List Char)
32+
parse23AnyCharUnits = manyRec StringParser.CodeUnits.anyChar
3133

3234
parse23DigitPoints :: Parser (List Char)
3335
parse23DigitPoints = manyRec StringParser.CodePoints.anyDigit
@@ -45,14 +47,17 @@ parse23RegexPoints :: Parser (List String)
4547
parse23RegexPoints = manyRec $ StringParser.CodePoints.regex """\d\d"""
4648

4749
parse23RegexUnits :: Parser (List String)
48-
parse23RegexUnits = manyRec $ StringParser.CodeUnits.string """\d\d"""
50+
parse23RegexUnits = manyRec $ StringParser.CodeUnits.regex """\d\d"""
4951

5052
main :: Effect Unit
5153
main = do
52-
-- log $ show $ runParser string23_2 parse23
53-
-- log $ show $ Regex.match pattern23 string23_2
54-
-- log $ show $ runParser stringSkidoo_2 parseSkidoo
55-
-- log $ show $ Regex.match patternSkidoo stringSkidoo_2
54+
log "StringParser.runParser parse23AnyCharPoints"
55+
benchWith 20
56+
$ \_ -> runParser parse23AnyCharPoints string23_10000
57+
log "StringParser.runParser parse23AnyCharUnits"
58+
benchWith 200
59+
$ \_ -> runParser parse23AnyCharUnits string23_10000
60+
5661
log "StringParser.runParser parse23DigitPoints"
5762
benchWith 20
5863
$ \_ -> runParser parse23DigitPoints string23_10000

spago.dhall

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
, "psci-support"
1818
, "strings"
1919
, "tailrec"
20+
, "transformers"
2021
, "unfoldable"
2122
]
2223
, packages = ./packages.dhall

src/Text/Parsing/StringParser/Combinators.purs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@ module Text.Parsing.StringParser.Combinators
1818
, chainr
1919
, chainl
2020
, chainl1
21-
, chainl1'
2221
, chainr1
23-
, chainr1'
2422
, choice
2523
, manyTill
2624
, many1Till
@@ -91,7 +89,7 @@ sepBy1 p sep = do
9189

9290
-- | Parse zero or more separated values, optionally ending with a separator.
9391
sepEndBy :: forall a sep. Parser a -> Parser sep -> Parser (List a)
94-
sepEndBy p sep = map NEL.toList (sepEndBy1 p sep) <|> pure Nil
92+
sepEndBy p sep = (sepEndBy1 p sep <#> NEL.toList) <|> (sep $> Nil) <|> pure Nil
9593

9694
-- | Parse one or more separated values, optionally ending with a separator.
9795
sepEndBy1 :: forall a sep. Parser a -> Parser sep -> Parser (NonEmptyList a)
@@ -103,14 +101,14 @@ sepEndBy1 p sep = do
103101
pure (cons' a as)
104102
) <|> pure (NEL.singleton a)
105103

104+
-- | Parse zero or more separated values, ending with a separator.
105+
endBy :: forall a sep. Parser a -> Parser sep -> Parser (List a)
106+
endBy p sep = (endBy1 p sep <#> NEL.toList) <|> (sep $> Nil)
107+
106108
-- | Parse one or more separated values, ending with a separator.
107109
endBy1 :: forall a sep. Parser a -> Parser sep -> Parser (NonEmptyList a)
108110
endBy1 p sep = many1 $ p <* sep
109111

110-
-- | Parse zero or more separated values, ending with a separator.
111-
endBy :: forall a sep. Parser a -> Parser sep -> Parser (List a)
112-
endBy p sep = many $ p <* sep
113-
114112
-- | Parse zero or more values separated by a right-associative operator.
115113
chainr :: forall a. Parser a -> Parser (a -> a -> a) -> a -> Parser a
116114
chainr p f a = chainr1 p f <|> pure a
@@ -125,7 +123,6 @@ chainl1 p f = do
125123
a <- p
126124
chainl1' p f a
127125

128-
-- | Parse one or more values separated by a left-associative operator.
129126
chainl1' :: forall a. Parser a -> Parser (a -> a -> a) -> a -> Parser a
130127
chainl1' p f a =
131128
( do
@@ -140,7 +137,6 @@ chainr1 p f = do
140137
a <- p
141138
chainr1' p f a
142139

143-
-- | Parse one or more values separated by a right-associative operator.
144140
chainr1' :: forall a. Parser a -> Parser (a -> a -> a) -> a -> Parser a
145141
chainr1' p f a =
146142
( do

test/BasicSpecs.purs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
module Test.BasicSpecs where
2+
3+
import Prelude hiding (between)
4+
5+
import Test.Utils (AnyParser(..), mkAnyParser)
6+
import Control.Alt ((<|>))
7+
import Control.Monad.Writer (Writer, execWriter, tell)
8+
import Data.Either (isRight)
9+
import Data.List (List)
10+
import Data.List as List
11+
import Data.Traversable (traverse)
12+
import Effect (Effect)
13+
import Effect.Class.Console (log)
14+
import Test.Assert (assert')
15+
import Text.Parsing.StringParser (Parser, runParser, try)
16+
import Text.Parsing.StringParser.CodePoints (anyChar, anyDigit, anyLetter, char, eof, skipSpaces, string)
17+
import Text.Parsing.StringParser.Combinators (between, chainl, chainl1, endBy, endBy1, lookAhead, many, many1, manyTill, sepBy, sepBy1, sepEndBy, sepEndBy1)
18+
19+
type TestInputs = { successes :: Array String, failures :: Array String }
20+
type TestCase = { name :: String, parser :: AnyParser, inputs :: TestInputs }
21+
22+
testCases :: Array TestCase
23+
testCases =
24+
[ { name: "anyChar"
25+
, parser: mkAnyParser anyChar
26+
-- TODO: test "🙂" which should fail
27+
-- this is an open upstream issue https://github.com/purescript/purescript-strings/issues/153
28+
, inputs: { successes: [ "a", "%" ], failures: [ "" ] }
29+
}
30+
, { name: "anyLetter"
31+
, parser: mkAnyParser anyLetter
32+
, inputs: { successes: [ "a" ], failures: [ "9" ] }
33+
}
34+
, { name: "skipSpaces"
35+
, parser: mkAnyParser $ skipSpaces *> anyChar
36+
, inputs: { successes: [ " 9", "9" ], failures: [ "9 " ] }
37+
}
38+
, { name: "map"
39+
, parser: mkAnyParser $ anyChar <#> const 3
40+
, inputs: { successes: [ "9" ], failures: [ "" ] }
41+
}
42+
, { name: "applicative"
43+
, parser: mkAnyParser $ (anyLetter <#> (\c -> (\c2 -> [ c, c2 ]))) <*> anyDigit
44+
, inputs: { successes: [ "a9" ], failures: [ "", "-", "a", "9" ] }
45+
}
46+
, { name: "alt"
47+
, parser: mkAnyParser $ anyLetter <|> anyDigit
48+
, inputs: { successes: [ "x", "9" ], failures: [ "", "-", "aa" ] }
49+
}
50+
, { name: "bind"
51+
, parser: mkAnyParser $ anyLetter >>= \letter -> char letter
52+
, inputs: { successes: [ "xx" ], failures: [ "", "-", "a", "aaa" ] }
53+
}
54+
, { name: "try"
55+
, parser: mkAnyParser $ try (anyLetter *> anyDigit) <|> char 'a'
56+
, inputs: { successes: [ "b9", "a6", "a" ], failures: [ "", "b", "-", "6" ] }
57+
}
58+
, { name: "lookAhead"
59+
, parser: mkAnyParser $ lookAhead (char 'a') *> anyLetter
60+
, inputs: { successes: [ "a" ], failures: [ "", "b" ] }
61+
}
62+
, { name: "many"
63+
, parser: mkAnyParser $ many (char 'a')
64+
, inputs: { successes: [ "", "a", "aaaa" ], failures: [ "b" ] }
65+
}
66+
, { name: "many1"
67+
, parser: mkAnyParser $ many1 (char 'a')
68+
, inputs: { successes: [ "a", "aaaa" ], failures: [ "", "b" ] }
69+
}
70+
, { name: "between"
71+
, parser: mkAnyParser $ between (char 'a') (char 'b') (char 'x')
72+
, inputs: { successes: [ "axb" ], failures: [ "", "x", "a", "b", "ab" ] }
73+
}
74+
, { name: "sepBy"
75+
, parser: mkAnyParser $ sepBy anyLetter (char ';')
76+
, inputs: { successes: [ "", "a", "a;b", "a;b;c" ], failures: [ ";", ";a", "a;", "ab", "a;ab" ] }
77+
}
78+
, { name: "sepBy1"
79+
, parser: mkAnyParser $ sepBy1 anyLetter (char ';')
80+
, inputs: { successes: [ "a", "a;b", "a;b;c" ], failures: [ "", ";", ";a", "a;", "ab", "a;ab" ] }
81+
}
82+
, { name: "sepEndBy"
83+
, parser: mkAnyParser $ sepEndBy anyLetter (char ';')
84+
, inputs: { successes: [ "", ";", "a", "a;b", "a;b;c", "a;" ], failures: [ ";a", "ab", "a;ab" ] }
85+
}
86+
, { name: "sepEndBy1"
87+
, parser: mkAnyParser $ sepEndBy1 anyLetter (char ';')
88+
, inputs: { successes: [ "a", "a;b", "a;b;c", "a;" ], failures: [ "", ";", ";a", "ab", "a;ab" ] }
89+
}
90+
, { name: "endBy"
91+
, parser: mkAnyParser $ endBy anyLetter (char ';')
92+
, inputs: { successes: [ ";", "a;", "a;b;", "a;b;c;" ], failures: [ "", "a", ";a", "ab", "a;b", "a;b;c" ] }
93+
}
94+
, { name: "endBy1"
95+
, parser: mkAnyParser $ endBy1 anyLetter (char ';')
96+
, inputs: { successes: [ "a;", "a;b;", "a;b;c;" ], failures: [ "", ";", "a", ";a", "ab", "a;b", "a;b;c" ] }
97+
}
98+
, { name: "manyTill"
99+
, parser: mkAnyParser $ manyTill anyLetter (char ';')
100+
, inputs: { successes: [ ";", "a;", "abc;" ], failures: [ "", "a", ";a", "ab", "a;b", "a;b;c" ] }
101+
}
102+
, { name: "manyTill overlapping"
103+
, parser: mkAnyParser $ manyTill anyLetter (char 'z')
104+
, inputs: { successes: [ "z", "az", "abcz" ], failures: [ "", "a", "za", "ab", "azb", "azbzc" ] }
105+
}
106+
, { name: "chainl"
107+
, parser: mkAnyParser $ chainl (string "x") (char '+' $> (<>)) ""
108+
, inputs: { successes: [ "", "x", "x+x+x+x" ], failures: [ "+", "+x", "x+", "x+x+", "xx", "xx+" ] }
109+
}
110+
, { name: "chainl1"
111+
, parser: mkAnyParser $ chainl1 (string "x") (char '+' $> (<>))
112+
, inputs: { successes: [ "x", "x+x+x+x" ], failures: [ "", "+", "+x", "x+", "x+x+", "xx", "xx+" ] }
113+
}
114+
]
115+
116+
type TestResult = Writer (List String) Unit
117+
118+
reportError :: String -> TestResult
119+
reportError = tell <<< List.singleton
120+
121+
runTestCases :: Effect Unit
122+
runTestCases = do
123+
let errors = execWriter $ traverse evalTestCase testCases
124+
when (List.length errors > 0) do
125+
_ <- traverse log errors
126+
assert' "Errors found" false
127+
128+
evalTestCase :: TestCase -> TestResult
129+
evalTestCase tc = do
130+
_ <- traverse assertSuccess tc.inputs.successes
131+
_ <- traverse assertFailure tc.inputs.failures
132+
pure unit
133+
where
134+
assertSuccess input =
135+
when (not (evalAnyParser tc.parser input)) do
136+
reportError ("Expected " <> tc.name <> " to succeed on '" <> input <> "' but it failed")
137+
assertFailure input =
138+
when (evalAnyParser tc.parser input) do
139+
reportError ("Expected " <> tc.name <> " to fail on '" <> input <> "' but it succeeded")
140+
141+
evalAnyParser :: AnyParser -> String -> Boolean
142+
evalAnyParser (AnyParser anyP) input = anyP canFullyParse input
143+
144+
canFullyParse :: forall a. Parser a -> String -> Boolean
145+
canFullyParse p input = isRight $ runParser (p *> eof) input

test/Main.purs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ import Effect (Effect)
66
import Effect.Console (log)
77
import Test.CodePoints (testCodePoints)
88
import Test.CodeUnits (testCodeUnits)
9+
import Test.BasicSpecs (runTestCases)
910

1011
main :: Effect Unit
1112
main = do
12-
log "Testing CodePoint parsing\n"
13+
log "Running basic spec test cases\n"
14+
runTestCases
15+
16+
log "\n\nTesting CodePoint parsing\n"
1317
testCodePoints
1418

1519
log "\n\nTesting CodeUnit parsing\n"

test/Utils.purs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module Test.Utils where
2+
3+
import Text.Parsing.StringParser (Parser)
4+
5+
newtype AnyParser = AnyParser (forall r. (forall a. Parser a -> r) -> r)
6+
7+
mkAnyParser :: forall a. Parser a -> AnyParser
8+
mkAnyParser p = AnyParser \f -> f p

0 commit comments

Comments
 (0)