Closed atcol closed 8 years ago
This is just a remark on the easiest case, but classify
is a pure pipe as it stands. These can easily be tested via the the corresponding list function
pure_pipe_test :: Pipe a b Identity () -> [a] -> [b]
pure_pipe_test p ls = P.toList $ each ls >-> p
I haven't used hspec, but used similar relationships when writing quickcheck tests. So here I guess we can write:
shouldMatchPipe :: (Show a, Eq a) => Pipe a b Identity () -> [a] -> [b] -> Expectation
shouldMatchPipe p as bs = shouldMatchList (pure_pipe_test p as) bs
That's probably not the best name, but anyway, then I can write
spec = hspec $ do
describe "classify" $ do
it "determines friends" $ do
shouldMatchPipe classify [alex] [(alex, Friend)]
shouldMatchPipe classify [bob] [(bob, Foe)]
it "determines undecided" $ do
shouldMatchPipe classify [no1] [(no1, Undecided)]
and see
>>> spec
classify
determines friends
determines undecided
Finished in 0.0004 seconds
2 examples, 0 failures
Note that 'classify' is an ultrafinite pipe that only allows one item to pass.
I will try this. Thanks so much @michaelt !
How do I go from Pipe a b IO ()
to Pipe a b Identity ()
?
Oh you can't - I was just thinking of the 'pure' case, where the pipe works for any monad, and thus for Identity or IO. Let me look at the SO question.
@atc- The simple solution is to just generalize the base monad like this:
classify :: Monad m => Pipe Person (Person, Classification) m ()
This is also the type that the compiler would infer.
That will then type-check as both Pipe Person (Person, Classification) Identity ()
and Pipe Person (Person, Classification) IO ()
, so you can use it in both a pure and impure pipeline
Hi Gabriel,
Perhaps I'm mistaken, but it seems to me that adding that contraint works when there are no actual IO
actions in the pipe, but say classify
did this:
classify :: Monad m => Pipe Person (Person, Classification) m ()
classify = do
p@(Person name _) <- await
case name of
"Alex" -> do
lift $ writeFile "Friends" "Alex"
yield (p, Friend)
"Bob" -> yield (p, Foe)
_ -> yield (p, Undecided)
then we fail to compile:
Couldn't match type `m' with `IO'
`m' is a rigid type variable bound by
the type signature for
classify :: Monad m => Pipe Person (Person, Classification) m ()
at test\WorkerSpec.hs:14:13
Expected type: Proxy () Person () (Person, Classification) m ()
Actual type: Proxy () Person () (Person, Classification) IO ()
Relevant bindings include
classify :: Pipe Person (Person, Classification) m ()
(bound at test\WorkerSpec.hs:15:1)
In a stmt of a 'do' block: lift $ writeFile "Friends" "Alex"
Have I misunderstood?
The pipes I've written will perform IO -- e.g. DB calls or logging -- so my overall API is in the IO monad.
Yes, if the pipe you want to test does IO
then you will definitely need another approach.
Here's one way to test if a Producer
matches a given list:
import Pipes
import qualified Pipes.Prelude
equals :: Eq a => [a] -> Producer a IO () -> IO Bool
equals xs p = do
e <- next p
case e of
Left () -> case xs of
[] -> return True
_ -> return False
Right (a, p') -> case xs of
y:ys | a == y -> equals ys p'
_ -> return False
Thanks, but ultimately the issue I have when using this with hspec
is that having defined Pipe
s (not just Producer
s) I need to run an effect and retrieve some final result, which, I appreciate, deviates from the design goals of Pipe
s, e.g.:
v <- runEffect $ p1 >-> p2 >->p3
v `shouldBe` someValue -- evaluates to a Test.Hspec.Core.Result
Obviously the above is not how Pipes works.
I strongly suspect that I have designed my program poorly or misused this library. I have written my Pipe
s to take input via await
, do something -- write to PostgreSQL, or log something -- and yield some output. There are no Producer
s of single values, if you see what I mean (bar the beginning of the Pipe
) -- every Pipe
has to act on something and pass on its result, and it does this entirely through functions that just return a Pipe
who await
and yield
themselves. I do not "compose" pipes from single Consumer
s and Producer
s. Perhaps if I'd done it that way (indeed thus changing the Producer
s here to functions of 1 parameter that yield, thus giving a Producer
) then things might be more testable.
Here's some simple-minded examples of the use of toListM
to test impure producers and pipes.
import Control.Monad
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Test.Hspec
import Pipes
import qualified Pipes.Prelude as P
import qualified Pipes.Text as PT
import Pipes.Prelude.Text
import Pipes.Safe
import qualified System.IO as IO
import System.Directory
a = describe "HUnit properties:" $ do
it "yields lines of text" $ do
xs <- fmap T.lines $ T.readFile "pipes_hunit.hs"
ys <- runSafeT $ P.toListM $ readFileLn "pipes_hunit.hs"
ys `shouldBe` xs
it "yields lines of text" $ do
let ints = map (T.pack . show) [1..100::Int]
T.writeFile "tmp.txt" $ T.unlines ints
ys <- runSafeT $ P.toListM $ readFileLn "tmp.txt"
removeFile "tmp.txt"
ys `shouldBe` ints
it "filters out even values" $ do
let is = [1 ..100]:: [Int]
evens = filter even is
piped <- P.toListM $ each is >-> P.filter even
piped `shouldBe` evens
it "logs inputs" $ do
ls <- IO.withFile "tmp_numbers.txt" IO.WriteMode $ \h -> do
P.toListM $ each [1..10::Int] >-> logInputs h
ls' <- fmap (map read . lines) (readFile "tmp_numbers.txt") :: IO [Int]
ls `shouldBe` ls'
removeFile "tmp_numbers.txt"
logInputs h = forever $ do
n <- await
liftIO $ IO.hPutStrLn h (show n)
yield n
-- P.chain (IO.hPutStrLn h . show)
Amazing! That's exactly what I want. Funnily enough, I was looking at toListM
yesterday when studying the types and had planned to try and use it for this purpose.
Thank you both for your help.
I originally asked this question about testing my code that uses Pipes with HSpec, and it's not conclusive how I might do so. I wondered if this is something you've considered for Pipes and whether it's possible at all?
For example, suppose I have this domain:
and this Pipe:
I'd like to be able to test it a bit like this:
Thanks!