Skip to content

connect: Rewrite test to use hspec with fail-fast. #298

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

Merged
merged 3 commits into from
Sep 18, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion exercises/connect/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ tests:
source-dirs: test
dependencies:
- connect
- HUnit
- hspec
10 changes: 4 additions & 6 deletions exercises/connect/src/Connect.hs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
module Connect (resultFor, Color(..)) where
module Connect (Mark(..), winner) where

data Color = Black
| White
deriving (Show,Eq)
data Mark = Cross | Nought deriving (Eq, Show)

resultFor :: [String] -> Maybe Color
resultFor = undefined
winner :: [String] -> Maybe Mark
winner = undefined
60 changes: 30 additions & 30 deletions exercises/connect/src/Example.hs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module Connect (resultFor, Color(..)) where
module Connect (winner, Mark(..)) where

import Control.Arrow ((***), first, second)
import Control.Monad.ST (ST, runST)
Expand All @@ -7,12 +7,12 @@ import qualified Data.Array.MArray as MA
import qualified Data.Array.ST as STA
import Prelude hiding (lines)

data Color
= White
| Black
data Mark
= Cross
| Nought
deriving (Show,Eq)

type Board = A.Array (Int,Int) (Maybe Color)
type Board = A.Array (Int,Int) (Maybe Mark)

-- Parse the lines into a board, assuming the list of lines is not empty
-- and the lines are of equal non-0 size.
Expand All @@ -23,13 +23,13 @@ parseLines lines@(firstLine:_) =
let height = length lines
width = length firstLine
in A.array ((0,0),(width - 1,height - 1)) fieldAssocs
where fieldAssocs :: [((Int,Int),Maybe Color)]
where fieldAssocs :: [((Int,Int),Maybe Mark)]
fieldAssocs =
do (l,y) <- zip lines [0 ..]
(c,x) <- zip l [0 ..]
let f = case c of
'O' -> Just White
'X' -> Just Black
'O' -> Just Nought
'X' -> Just Cross
_ -> Nothing
return ((x,y),f)

Expand All @@ -52,50 +52,50 @@ neighbours b c =
,(+ 1) *** subtract 1]
in filter (A.inRange (A.bounds b)) . map ($ c) $ dirs

isTargetEdge :: Board -> Color -> (Int,Int) -> Bool
isTargetEdge b Black (x,_) =
isTargetEdge :: Board -> Mark -> (Int,Int) -> Bool
isTargetEdge b Cross (x,_) =
let (_,(ux,_)) = A.bounds b
in x == ux
isTargetEdge b White (_,y) =
isTargetEdge b Nought (_,y) =
let (_,(_,uy)) = A.bounds b
in y == uy

startCoords :: Board -> Color -> [(Int,Int)]
startCoords b Black =
startCoords :: Board -> Mark -> [(Int,Int)]
startCoords b Cross =
let ((_,ly),(_,uy)) = A.bounds b
in [(0,y) | y <- [ly .. uy]]
startCoords b White =
startCoords b Nought =
let ((lx,_),(ux,_)) = A.bounds b
in [(x,0) | x <- [lx .. ux]]

tryConnect :: Board -> Color -> ConnArr s -> (Int,Int) -> ST s Bool
tryConnect b color ca c =
tryConnect :: Board -> Mark -> ConnArr s -> (Int,Int) -> ST s Bool
tryConnect b mark ca c =
case b A.! c of
Just fieldColor
| fieldColor == color ->
Just fieldMark
| fieldMark == mark ->
do seen <- MA.readArray ca c
if seen
then return False
else if isTargetEdge b fieldColor c
else if isTargetEdge b fieldMark c
then return True
else do MA.writeArray ca c True
or `fmap`
mapM (tryConnect b color ca)
mapM (tryConnect b mark ca)
(neighbours b c)
_ -> return False

resultFor :: [String] -> Maybe Color
resultFor lines =
winner :: [String] -> Maybe Mark
winner lines =
let board = parseLines lines
in if resultFor' board Black
then Just Black
else if resultFor' board White
then Just White
in if winner' board Cross
then Just Cross
else if winner' board Nought
then Just Nought
else Nothing
where resultFor' :: Board -> Color -> Bool
resultFor' b color =
where winner' :: Board -> Mark -> Bool
winner' b mark =
runST $
do ca <- emptyConnArr b
or `fmap`
mapM (tryConnect b color ca)
(startCoords b color)
mapM (tryConnect b mark ca)
(startCoords b mark)
198 changes: 97 additions & 101 deletions exercises/connect/test/Tests.hs
Original file line number Diff line number Diff line change
@@ -1,109 +1,105 @@
import Test.HUnit (Assertion, (@=?), runTestTT, Test(..), Counts(..))
import System.Exit (ExitCode(..), exitWith)
import Connect (resultFor, Color(Black, White))
import Prelude hiding (lines) -- don't complain about redefining lines
{-# LANGUAGE RecordWildCards #-}

exitProperly :: IO Counts -> IO ()
exitProperly m = do
counts <- m
exitWith $ if failures counts /= 0 || errors counts /= 0 then ExitFailure 1 else ExitSuccess
import Data.Foldable (for_)
import Test.Hspec (Spec, describe, it, shouldBe)
import Test.Hspec.Runner (configFastFail, defaultConfig, hspecWith)

testCase :: String -> Assertion -> Test
testCase label assertion = TestLabel label (TestCase assertion)
import Connect (Mark(Cross,Nought), winner)

-- | Remove spaces to turn a readable board into valid input for resultFor.
makeBoard :: [String] -> [String]
makeBoard = map (filter (/=' '))

test_emptyBoard :: Assertion
test_emptyBoard =
let lines = [". . . . ."
," . . . . ."
," . . . . ."
," . . . . ."
," . . . . ."
]
in Nothing @=? resultFor (makeBoard lines)

test_oneByOneBlack :: Assertion
test_oneByOneBlack =
let lines = ["X"]
in Just Black @=? resultFor (makeBoard lines)

test_oneByOneWhite :: Assertion
test_oneByOneWhite =
let lines = ["O"]
in Just White @=? resultFor (makeBoard lines)

test_convultedPath :: Assertion
test_convultedPath =
let lines = [". X X . ."
," X . X . X"
," . X . X ."
," . X X . ."
," O O O O O"
]
in Just Black @=? resultFor (makeBoard lines)

test_rectangleBlack :: Assertion
test_rectangleBlack =
let lines = [". O . ."
," O X X X"
," O X O ."
," X X O X"
," . O X ."
]
in Just Black @=? resultFor (makeBoard lines)
main :: IO ()
main = hspecWith defaultConfig {configFastFail = True} specs

test_rectangleWhite :: Assertion
test_rectangleWhite =
let lines = [". O . ."
," O X X X"
," O O O ."
," X X O X"
," . O X ."
]
in Just White @=? resultFor (makeBoard lines)
specs :: Spec
specs = describe "connect" $
describe "winner" $ for_ cases test
where

test_spiralBlack :: Assertion
test_spiralBlack =
let board = ["OXXXXXXXX"
,"OXOOOOOOO"
,"OXOXXXXXO"
,"OXOXOOOXO"
,"OXOXXXOXO"
,"OXOOOXOXO"
,"OXXXXXOXO"
,"OOOOOOOXO"
,"XXXXXXXXO"
]
in Just Black @=? resultFor board
test Case{..} = it description assertion
where
assertion = winner testBoard `shouldBe` expected
testBoard = filter (/=' ') <$> board

test_spiralNone :: Assertion
test_spiralNone =
let board = ["OXXXXXXXX"
,"OXOOOOOOO"
,"OXOXXXXXO"
,"OXOXOOOXO"
,"OXOX.XOXO"
,"OXOOOXOXO"
,"OXXXXXOXO"
,"OOOOOOOXO"
,"XXXXXXXXO"
]
in Nothing @=? resultFor board
-- Test cases adapted from `exercism/x-common` on 2016-09-16.

resultForTests :: [Test]
resultForTests =
[ testCase "empty board has no winner" test_emptyBoard
, testCase "1x1 board with black stone" test_oneByOneBlack
, testCase "1x1 board with white stone" test_oneByOneWhite
, testCase "convulted path" test_convultedPath
, testCase "rectangle, black wins" test_rectangleBlack
, testCase "rectangle, white wins" test_rectangleWhite
, testCase "spiral, black wins" test_spiralBlack
, testCase "spiral, nobody wins" test_spiralNone
]
data Case = Case { description :: String
, board :: [String]
, expected :: Maybe Mark
}

main :: IO ()
main = exitProperly (runTestTT (TestList resultForTests))
cases :: [Case]
cases = [ Case { description = "an empty board has no winner"
, board = [ ". . . . ."
, " . . . . ."
, " . . . . ."
, " . . . . ."
, " . . . . ." ]
, expected = Nothing
}
, Case { description = "X can win on a 1x1 board"
, board = [ "X" ]
, expected = Just Cross
}
, Case { description = "O can win on a 1x1 board"
, board = [ "O" ]
, expected = Just Nought
}
, Case { description = "only edges does not make a winner"
, board = [ "O O O X"
, " X . . X"
, " X . . X"
, " X O O O" ]
, expected = Nothing
}
, Case { description = "illegal diagonal does not make a winner"
, board = [ "X O . ."
, " O X X X"
, " O X O ."
, " . O X ."
, " X X O O" ]
, expected = Nothing
}
, Case { description = "nobody wins crossing adjacent angles"
, board = [ "X . . ."
, " . X O ."
, " O . X O"
, " . O . X"
, " . . O ." ]
, expected = Nothing
}
, Case { description = "X wins crossing from left to right"
, board = [ ". O . ."
, " O X X X"
, " O X O ."
, " X X O X"
, " . O X ." ]
, expected = Just Cross
}
, Case { description = "O wins crossing from top to bottom"
, board = [ ". O . ."
, " O X X X"
, " O O O ."
, " X X O X"
, " . O X ." ]
, expected = Just Nought
}
, Case { description = "X wins using a convoluted path"
, board = [ ". X X . ."
, " X . X . X"
, " . X . X ."
, " . X X . ."
, " O O O O O" ]
, expected = Just Cross
}
, Case { description = "X wins using a spiral path"
, board = [ "O X X X X X X X X"
, " O X O O O O O O O"
, " O X O X X X X X O"
, " O X O X O O O X O"
, " O X O X X X O X O"
, " O X O O O X O X O"
, " O X X X X X O X O"
, " O O O O O O O X O"
, " X X X X X X X X O" ]
, expected = Just Cross
}
]