Skip to content

Commit a407b06

Browse files
authored
Add a Haskell test suite
1 parent 3ffdcf7 commit a407b06

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+308
-208
lines changed

.circleci/config.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ jobs:
2323
# We set jobs to 1 here because that prevents Out-Of-Memory exceptions
2424
# while compiling dependencies.
2525
name: 'Install'
26-
command: '.circleci/tickle.sh stack build --pedantic --copy-bins --jobs=1 --no-terminal'
26+
command: '.circleci/tickle.sh stack build --pedantic --copy-bins --jobs=1 --no-terminal --test'
2727
- run:
28-
name: 'Run tests'
28+
name: 'Run golden tests'
2929
command: 'make test'
3030
- save_cache:
3131
key: 'v4-patat-{{ arch }}-{{ .Branch }}-{{ .Revision }}'
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

lib/Patat/Main.hs

+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
--------------------------------------------------------------------------------
2+
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3+
{-# LANGUAGE OverloadedStrings #-}
4+
{-# LANGUAGE RecordWildCards #-}
5+
module Patat.Main
6+
( main
7+
) where
8+
9+
10+
--------------------------------------------------------------------------------
11+
import Control.Applicative ((<$>), (<*>))
12+
import Control.Concurrent (forkIO, threadDelay)
13+
import qualified Control.Concurrent.Chan as Chan
14+
import Control.Exception (finally)
15+
import Control.Monad (forever, unless, when)
16+
import qualified Data.Aeson.Extended as A
17+
import Data.Monoid (mempty, (<>))
18+
import Data.Time (UTCTime)
19+
import Data.Version (showVersion)
20+
import qualified Options.Applicative as OA
21+
import Patat.AutoAdvance
22+
import qualified Patat.Images as Images
23+
import Patat.Presentation
24+
import qualified Paths_patat
25+
import Prelude
26+
import qualified System.Console.ANSI as Ansi
27+
import System.Directory (doesFileExist,
28+
getModificationTime)
29+
import System.Exit (exitFailure, exitSuccess)
30+
import qualified System.IO as IO
31+
import qualified Text.Pandoc as Pandoc
32+
import qualified Text.PrettyPrint.ANSI.Leijen as PP
33+
34+
35+
--------------------------------------------------------------------------------
36+
data Options = Options
37+
{ oFilePath :: !(Maybe FilePath)
38+
, oForce :: !Bool
39+
, oDump :: !Bool
40+
, oWatch :: !Bool
41+
, oVersion :: !Bool
42+
} deriving (Show)
43+
44+
45+
--------------------------------------------------------------------------------
46+
parseOptions :: OA.Parser Options
47+
parseOptions = Options
48+
<$> (OA.optional $ OA.strArgument $
49+
OA.metavar "FILENAME" <>
50+
OA.help "Input file")
51+
<*> (OA.switch $
52+
OA.long "force" <>
53+
OA.short 'f' <>
54+
OA.help "Force ANSI terminal" <>
55+
OA.hidden)
56+
<*> (OA.switch $
57+
OA.long "dump" <>
58+
OA.short 'd' <>
59+
OA.help "Just dump all slides and exit" <>
60+
OA.hidden)
61+
<*> (OA.switch $
62+
OA.long "watch" <>
63+
OA.short 'w' <>
64+
OA.help "Watch file for changes")
65+
<*> (OA.switch $
66+
OA.long "version" <>
67+
OA.help "Display version info and exit" <>
68+
OA.hidden)
69+
70+
71+
--------------------------------------------------------------------------------
72+
parserInfo :: OA.ParserInfo Options
73+
parserInfo = OA.info (OA.helper <*> parseOptions) $
74+
OA.fullDesc <>
75+
OA.header ("patat v" <> showVersion Paths_patat.version) <>
76+
OA.progDescDoc (Just desc)
77+
where
78+
desc = PP.vcat
79+
[ "Terminal-based presentations using Pandoc"
80+
, ""
81+
, "Controls:"
82+
, "- Next slide: space, enter, l, right, pagedown"
83+
, "- Previous slide: backspace, h, left, pageup"
84+
, "- Go forward 10 slides: j, down"
85+
, "- Go backward 10 slides: k, up"
86+
, "- First slide: 0"
87+
, "- Last slide: G"
88+
, "- Reload file: r"
89+
, "- Quit: q"
90+
]
91+
92+
93+
--------------------------------------------------------------------------------
94+
parserPrefs :: OA.ParserPrefs
95+
parserPrefs = OA.prefs OA.showHelpOnError
96+
97+
98+
--------------------------------------------------------------------------------
99+
errorAndExit :: [String] -> IO a
100+
errorAndExit msg = do
101+
mapM_ (IO.hPutStrLn IO.stderr) msg
102+
exitFailure
103+
104+
105+
--------------------------------------------------------------------------------
106+
assertAnsiFeatures :: IO ()
107+
assertAnsiFeatures = do
108+
supports <- Ansi.hSupportsANSI IO.stdout
109+
unless supports $ errorAndExit
110+
[ "It looks like your terminal does not support ANSI codes."
111+
, "If you still want to run the presentation, use `--force`."
112+
]
113+
114+
115+
--------------------------------------------------------------------------------
116+
main :: IO ()
117+
main = do
118+
options <- OA.customExecParser parserPrefs parserInfo
119+
120+
when (oVersion options) $ do
121+
putStrLn (showVersion Paths_patat.version)
122+
putStrLn $ "Using pandoc: " ++ Pandoc.pandocVersion
123+
exitSuccess
124+
125+
filePath <- case oFilePath options of
126+
Just fp -> return fp
127+
Nothing -> OA.handleParseResult $ OA.Failure $
128+
OA.parserFailure parserPrefs parserInfo OA.ShowHelpText mempty
129+
130+
errOrPres <- readPresentation filePath
131+
pres <- either (errorAndExit . return) return errOrPres
132+
133+
unless (oForce options) assertAnsiFeatures
134+
135+
-- (Maybe) initialize images backend.
136+
images <- traverse Images.new (psImages $ pSettings pres)
137+
138+
if oDump options
139+
then dumpPresentation pres
140+
else interactiveLoop options images pres
141+
where
142+
interactiveLoop :: Options -> Maybe Images.Handle -> Presentation -> IO ()
143+
interactiveLoop options images pres0 = (`finally` cleanall) $ do
144+
IO.hSetBuffering IO.stdin IO.NoBuffering
145+
Ansi.hideCursor
146+
147+
-- Spawn the initial channel that gives us commands based on user input.
148+
commandChan0 <- Chan.newChan
149+
_ <- forkIO $ forever $
150+
readPresentationCommand IO.stdin >>= Chan.writeChan commandChan0
151+
152+
-- If an auto delay is set, use 'autoAdvance' to create a new one.
153+
commandChan <- case psAutoAdvanceDelay (pSettings pres0) of
154+
Nothing -> return commandChan0
155+
Just (A.FlexibleNum delay) -> autoAdvance delay commandChan0
156+
157+
-- Spawn a thread that adds 'Reload' commands based on the file time.
158+
mtime0 <- getModificationTime (pFilePath pres0)
159+
when (oWatch options) $ do
160+
_ <- forkIO $ watcher commandChan (pFilePath pres0) mtime0
161+
return ()
162+
163+
let loop :: Presentation -> Maybe String -> IO ()
164+
loop pres mbError = do
165+
cleanup <- case mbError of
166+
Nothing -> displayPresentation images pres
167+
Just err -> displayPresentationError pres err
168+
169+
c <- Chan.readChan commandChan
170+
update <- updatePresentation c pres
171+
cleanup
172+
case update of
173+
ExitedPresentation -> return ()
174+
UpdatedPresentation pres' -> loop pres' Nothing
175+
ErroredPresentation err -> loop pres (Just err)
176+
177+
loop pres0 Nothing
178+
179+
cleanall :: IO ()
180+
cleanall = do
181+
Ansi.showCursor
182+
Ansi.clearScreen
183+
Ansi.setCursorPosition 0 0
184+
185+
186+
--------------------------------------------------------------------------------
187+
watcher :: Chan.Chan PresentationCommand -> FilePath -> UTCTime -> IO a
188+
watcher chan filePath mtime0 = do
189+
-- The extra exists check helps because some editors temporarily make the
190+
-- file disappear while writing.
191+
exists <- doesFileExist filePath
192+
mtime1 <- if exists then getModificationTime filePath else return mtime0
193+
194+
when (mtime1 > mtime0) $ Chan.writeChan chan Reload
195+
threadDelay (200 * 1000)
196+
watcher chan filePath mtime1
File renamed without changes.
File renamed without changes.
File renamed without changes.

src/Patat/Presentation/Interactive.hs lib/Patat/Presentation/Interactive.hs

+7-5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ module Patat.Presentation.Interactive
1515
--------------------------------------------------------------------------------
1616
import Patat.Presentation.Internal
1717
import Patat.Presentation.Read
18+
import qualified System.IO as IO
1819

1920

2021
--------------------------------------------------------------------------------
@@ -28,11 +29,12 @@ data PresentationCommand
2829
| Last
2930
| Reload
3031
| UnknownCommand String
32+
deriving (Eq, Show)
3133

3234

3335
--------------------------------------------------------------------------------
34-
readPresentationCommand :: IO PresentationCommand
35-
readPresentationCommand = do
36+
readPresentationCommand :: IO.Handle -> IO PresentationCommand
37+
readPresentationCommand h = do
3638
k <- readKey
3739
case k of
3840
"q" -> return Exit
@@ -57,13 +59,13 @@ readPresentationCommand = do
5759
where
5860
readKey :: IO String
5961
readKey = do
60-
c0 <- getChar
62+
c0 <- IO.hGetChar h
6163
case c0 of
6264
'\ESC' -> do
63-
c1 <- getChar
65+
c1 <- IO.hGetChar h
6466
case c1 of
6567
'[' -> do
66-
c2 <- getChar
68+
c2 <- IO.hGetChar h
6769
return [c0, c1, c2]
6870
_ -> return [c0, c1]
6971
_ -> return [c0]
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

patat.cabal

+36-8
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,9 @@ Flag patat-make-man
2525
Default: False
2626
Manual: True
2727

28-
Executable patat
29-
Main-is: Main.hs
30-
Ghc-options: -Wall -threaded -rtsopts "-with-rtsopts=-N"
31-
Hs-source-dirs: src
28+
Library
29+
Ghc-options: -Wall
30+
Hs-source-dirs: lib
3231
Default-language: Haskell2010
3332

3433
Build-depends:
@@ -61,16 +60,14 @@ Executable patat
6160
Build-depends:
6261
semigroups >= 0.16 && < 0.19
6362

64-
Other-modules:
65-
Data.Aeson.Extended
66-
Data.Aeson.TH.Extended
67-
Data.Data.Extended
63+
Exposed-modules:
6864
Patat.AutoAdvance
6965
Patat.Cleanup
7066
Patat.Images
7167
Patat.Images.Internal
7268
Patat.Images.W3m
7369
Patat.Images.ITerm2
70+
Patat.Main
7471
Patat.Presentation
7572
Patat.Presentation.Display
7673
Patat.Presentation.Display.CodeBlock
@@ -81,9 +78,21 @@ Executable patat
8178
Patat.Presentation.Read
8279
Patat.PrettyPrint
8380
Patat.Theme
81+
82+
Other-modules:
83+
Data.Aeson.Extended
84+
Data.Aeson.TH.Extended
85+
Data.Data.Extended
8486
Paths_patat
8587
Text.Pandoc.Extended
8688

89+
Executable patat
90+
Main-is: Main.hs
91+
Ghc-options: -Wall -threaded -rtsopts "-with-rtsopts=-N"
92+
Hs-source-dirs: src
93+
Default-language: Haskell2010
94+
Build-depends: base, patat
95+
8796
Executable patat-make-man
8897
Main-is: make-man.hs
8998
Ghc-options: -Wall
@@ -101,3 +110,22 @@ Executable patat-make-man
101110
pandoc >= 2.0 && < 2.8,
102111
text >= 1.2 && < 1.3,
103112
time >= 1.6 && < 1.10
113+
114+
Test-suite patat-tests
115+
Main-is: Main.hs
116+
Ghc-options: -Wall
117+
Hs-source-dirs: tests/haskell
118+
Type: exitcode-stdio-1.0
119+
Default-language: Haskell2010
120+
121+
Other-modules:
122+
Patat.Presentation.Interactive.Tests
123+
124+
Build-depends:
125+
patat,
126+
base >= 4.8 && < 5,
127+
directory >= 1.2 && < 1.4,
128+
tasty >= 1.2 && < 1.3,
129+
tasty-hunit >= 0.10 && < 0.11,
130+
tasty-quickcheck >= 0.10 && < 0.11,
131+
QuickCheck >= 2.8 && < 2.14

0 commit comments

Comments
 (0)