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

Add Koan about Stdin clock #2

Merged
merged 6 commits into from
Jan 9, 2024
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
10 changes: 9 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
## Basic

* stdin clock
* char count per line
* line count & total char count
* exception for eof & final summary
* cat a file in the tool
* secondly progress report
* using the current time
* behaviours: reusability across clocks
* infer clock interval from component, or vice versa? if not possible, add type signature?
Expand Down Expand Up @@ -56,9 +61,12 @@

## Terminal

* snake clone

## wai or servant

Goal might be a Haskell menti clone
* One possible goal might be a Haskell menti clone
* Another possible goal: Caching file server with interaction via StdinClock (invalidate cache, report current status, start/stop live or summary logging, quit)

## Cassava

Expand Down
54 changes: 45 additions & 9 deletions generic/test-io/TestIO.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,70 @@ module TestIO where
-- base
import Control.Concurrent
import Control.Monad
import GHC.IO.Handle (hDuplicate, hDuplicateTo)
import System.Exit
import System.IO
import System.IO (Handle, IOMode (..), hFlush, stderr, stdin, withFile)
import Prelude hiding (lines, putStrLn, unlines, writeFile)

-- text
import Data.Text
import Data.Text.IO

-- silently
import System.IO.Silently

-- temporary
import System.IO.Temp

-- | Integration test a main function
testForSeconds ::
-- | How many seconds the test should run
Int ->
-- | The standard input to supply
Maybe [String] ->
-- | The main function to test
IO () ->
-- | The property to test on the produced output ([] = test passes)
([String] -> [String]) ->
([Text] -> [Text]) ->
IO ()
testForSeconds nSeconds input mainFunction testFunction = do
testForSeconds nSeconds mainFunction testFunction = testForSecondsErrHandle nSeconds mainFunction testFunction stderr

-- Like testForSeconds, but with custom stderr handle
testForSecondsErrHandle :: Int -> IO () -> ([Text] -> [Text]) -> Handle -> IO ()
testForSecondsErrHandle nSeconds mainFunction testFunction stderrOld = do
putStrLn "---------------------------"
void $ forkIO mainFunction
output <- capture_ $ do
hPutStr stderr "Testing"
forM_ [(1 :: Int) .. 20] $ const $ hPutStr stderr "." >> threadDelay (50000 * nSeconds) >> hFlush stderr
void $ forkIO mainFunction
hPutStr stderrOld "Testing"
forM_ [(1 :: Int) .. 20] $ const $ hPutStr stderrOld "." >> threadDelay (50000 * nSeconds) >> hFlush stderrOld
putStrLn "\n---------------------------\n"
case testFunction $ lines output of
case testFunction $ lines $ pack output of
[] -> putStrLn "Well done!"
errors -> do
putStrLn "Oh no!"
forM_ errors putStrLn
putStrLn ""
exitFailure

-- | Integration test a main function, providing input
testForSecondsInput ::
-- | How many seconds the test should run
Int ->
-- | The standard input to supply
[Text] ->
-- | The main function to test
IO () ->
-- | The property to test on the produced output ([] = test passes)
([Text] -> [Text]) ->
IO ()
testForSecondsInput nSeconds input mainFunction testFunction = do
inputFileName <- emptySystemTempFile "input.txt"
writeFile inputFileName $ unlines input
withFile inputFileName ReadMode $ \stdinFile -> do
hDuplicateTo stdinFile stdin
stderrOld <- hDuplicate stderr
withSystemTempFile "stderr.txt" $ \_path stderrFile -> do
hDuplicateTo stderrFile stderr -- silence stderr to avoid "hGetLine: end of file" message
testForSecondsErrHandle nSeconds mainFunction testFunction stderrOld

-- | Like 'show', but for 'Text'
tshow :: (Show a) => a -> Text
tshow = pack . show
2 changes: 1 addition & 1 deletion koans/basic/1-hello-rhine/Test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Koan qualified (main)
import TestIO

main :: IO ()
main = testForSeconds 2 Nothing Koan.main $ \output ->
main = testForSeconds 2 Koan.main $ \output ->
case (length output, length (filter (== "Hello Rhine!") output)) of
(_, 2) -> []
(_, 1) -> ["Your program seems to be running a bit slow."]
Expand Down
2 changes: 1 addition & 1 deletion koans/basic/2-fix-the-bug/Test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Koan qualified (main)
import TestIO

main :: IO ()
main = testForSeconds 2 Nothing Koan.main $ \output ->
main = testForSeconds 2 Koan.main $ \output ->
case length (filter (== "Hello Rhine!") output) of
2 -> []
1 -> ["Your program seems to be running a bit slow."]
Expand Down
4 changes: 2 additions & 2 deletions koans/basic/3-faster/Test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Koan qualified (main)
import TestIO

main :: IO ()
main = testForSeconds 2 Nothing Koan.main $ \output ->
main = testForSeconds 2 Koan.main $ \output ->
case length (filter (== "Hello Rhine!") output) of
n
| 19 <= n && n <= 21 -> []
Expand All @@ -28,4 +28,4 @@ main = testForSeconds 2 Nothing Koan.main $ \output ->
, avgLengthMsg n
]
where
avgLengthMsg n = "The average length between two ticks was " ++ show (round (2000 / fromIntegral n :: Double) :: Int) ++ " milliseconds."
avgLengthMsg n = "The average length between two ticks was " <> tshow (round (2000 / fromIntegral n :: Double) :: Int) <> " milliseconds."
2 changes: 1 addition & 1 deletion koans/basic/4-compose/Test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Koan qualified (main)
import TestIO

main :: IO ()
main = testForSeconds 2 Nothing Koan.main $ \output ->
main = testForSeconds 2 Koan.main $ \output ->
case length (filter (== "Hello Rhine!") output) of
2 -> []
1 -> ["Your program seems to be running a bit slow."]
Expand Down
2 changes: 1 addition & 1 deletion koans/basic/5-compose-more/Test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Koan qualified (main)
import TestIO

main :: IO ()
main = testForSeconds 2 Nothing Koan.main $ \output ->
main = testForSeconds 2 Koan.main $ \output ->
let errorsWithExcl = case length (filter (== "Hello Rhine!") output) of
2 -> []
1 -> ["Your program seems to be running a bit slow."]
Expand Down
2 changes: 1 addition & 1 deletion koans/basic/6-compose-signal-functions-and-clocks/Test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Koan qualified (main)
import TestIO

main :: IO ()
main = testForSeconds 2 Nothing Koan.main $ \output ->
main = testForSeconds 2 Koan.main $ \output ->
let errorsWithExcl = case length (filter (== "Hello Rhine!") output) of
2 -> []
1 -> ["Your program seems to be running a bit slow."]
Expand Down
2 changes: 1 addition & 1 deletion koans/basic/7-compose-on-the-same-clock/Test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Koan qualified (main)
import TestIO

main :: IO ()
main = testForSeconds 2 Nothing Koan.main $ \output ->
main = testForSeconds 2 Koan.main $ \output ->
let errorsWithExcl = case length (filter (== "Hello Rhine!") output) of
2 -> []
1 -> ["Your program seems to be running a bit slow."]
Expand Down
2 changes: 1 addition & 1 deletion koans/basic/8-compose-on-different-clocks/Test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Koan qualified (main)
import TestIO

main :: IO ()
main = testForSeconds 2 Nothing Koan.main $ \output ->
main = testForSeconds 2 Koan.main $ \output ->
case length (filter (== "Hello Rhine!") output) of
10 -> []
0 -> ["Your program didn't produce the line \"Hello Rhine!\" in time. Maybe a typo?"]
Expand Down
47 changes: 47 additions & 0 deletions koans/basic/9-input/Koan.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{- | Input!

We want to react to user input. For this track, let's allow the program to interact on the console.
To do this, meditate about the follow sentence:

Events in Rhine come from event clocks.

An event happens when an event clock ticks.
For example, to react every time when a line is entered on the standard input,
you have to add the 'StdinClock' event clock to your main Rhine.

Events are handled just like any other kind of data.
The difference between event clocks and clocks like 'Millisecond n' is only conceptual:
A fixed rate clock ticks at predictable intervals,
whereas it depends on the user or another external influence when an event clock ticks.
The frameworks handles them in the same way.

The next Koans are about the event that is triggered when a line of text is entered on the standard input.
-}
module Koan where

-- text
import Data.Text (Text)
import Data.Text.IO as Text (putStrLn)

-- rhine
import FRP.Rhine

{- | A line of user input.

The 'StdinClock' clock ticks every time a line is entered on StdinClock.

The information _what_ was typed can be retrieved with a special signal function: 'tagS'.
This signal function is a "sensor" (it has no input, only output),
and it produces different data depending on the clock.
For 'StdinClock', it is one line of standard input.
-}
userInput :: ClSF IO StdinClock () Text
userInput = tagS

-- | Output the same line that was just entered.
parrot :: ClSF IO StdinClock () ()
-- Do you remember how to convert an effectful function into a ClSF?
parrot = userInput >-> _ Text.putStrLn

main :: IO ()
main = flow $ parrot @@ StdinClock
27 changes: 27 additions & 0 deletions koans/basic/9-input/Test.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module Main where

-- text
import Data.Text

-- koan
import Koan qualified (main)

-- test-io
import TestIO

testLines :: [Text]
testLines =
[ "Hello"
, "Rhine"
, "this"
, "is"
, "a"
, "test"
]

main :: IO ()
main = testForSecondsInput 1 testLines Koan.main $ \output ->
case output of
[] -> ["Weird, your program didn't produce any output!"]
_ | output == testLines -> []
_ -> ["The program produced output, but it was different from the input."]
15 changes: 14 additions & 1 deletion rhine-koans.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ common opts
ghc-options: -Wall
default-language: GHC2021
build-depends: base >=4.16.4.0 && <4.18
, rhine ^>= 1
, rhine ^>= 1.2
, text ^>= 2.0
default-extensions:
DataKinds
Expand Down Expand Up @@ -48,6 +48,7 @@ library test-io
exposed-modules: TestIO
build-depends:
silently ^>= 1.2
, temporary ^>= 1.3

executable basic-1-hello-rhine
import: exec
Expand Down Expand Up @@ -144,3 +145,15 @@ test-suite basic-8-compose-on-different-clocks-test
type: exitcode-stdio-1.0
main-is: Test.hs
hs-source-dirs: koans/basic/8-compose-on-different-clocks

executable basic-9-input
import: exec
main-is: Main.hs
hs-source-dirs: koans/basic/9-input
, generic/reimport-main

test-suite basic-9-input-test
import: test
type: exitcode-stdio-1.0
main-is: Test.hs
hs-source-dirs: koans/basic/9-input