diff options
| author | Gregor Kleen <gkleen@yggdrasil.li> | 2016-01-09 02:19:56 +0000 |
|---|---|---|
| committer | Gregor Kleen <gkleen@yggdrasil.li> | 2016-01-09 02:19:56 +0000 |
| commit | 8307d7e80a88f1425eb0a93bbde4adff388b7cdc (patch) | |
| tree | 308a9f53c0873f2442342e829caecf7b90264f6b | |
| parent | a2093f0a59dd1f557e8d5b885325d07a8aa55073 (diff) | |
| download | thermoprint-8307d7e80a88f1425eb0a93bbde4adff388b7cdc.tar thermoprint-8307d7e80a88f1425eb0a93bbde4adff388b7cdc.tar.gz thermoprint-8307d7e80a88f1425eb0a93bbde4adff388b7cdc.tar.bz2 thermoprint-8307d7e80a88f1425eb0a93bbde4adff388b7cdc.tar.xz thermoprint-8307d7e80a88f1425eb0a93bbde4adff388b7cdc.zip | |
spec/src/Thermoprint/Printout.hs & test-framework
Implemented Printout, Paragraph, Chunk, Block, Line, and text
Test coverage for text
| -rw-r--r-- | spec/Spec.hs | 1 | ||||
| -rw-r--r-- | spec/src/Thermoprint/Printout.hs | 172 | ||||
| -rw-r--r-- | spec/thermoprint-spec.cabal | 25 | ||||
| -rw-r--r-- | spec/thermoprint-spec.nix | 4 |
4 files changed, 197 insertions, 5 deletions
diff --git a/spec/Spec.hs b/spec/Spec.hs new file mode 100644 index 0000000..a824f8c --- /dev/null +++ b/spec/Spec.hs | |||
| @@ -0,0 +1 @@ | |||
| {-# OPTIONS_GHC -F -pgmF hspec-discover #-} | |||
diff --git a/spec/src/Thermoprint/Printout.hs b/spec/src/Thermoprint/Printout.hs new file mode 100644 index 0000000..d13d02e --- /dev/null +++ b/spec/src/Thermoprint/Printout.hs | |||
| @@ -0,0 +1,172 @@ | |||
| 1 | {-# LANGUAGE DeriveGeneric, DeriveAnyClass #-} | ||
| 2 | {-# LANGUAGE OverloadedStrings #-} | ||
| 3 | {-# OPTIONS_HADDOCK show-extensions #-} | ||
| 4 | |||
| 5 | -- | This module contains the definition of the structure -- 'Printout' -- used to represent the content of a job | ||
| 6 | module Thermoprint.Printout | ||
| 7 | ( Printout(..) | ||
| 8 | , Paragraph(..) | ||
| 9 | , Chunk(..) | ||
| 10 | , Block(..) | ||
| 11 | , Line( HSpace | ||
| 12 | , SpaceSep | ||
| 13 | ) | ||
| 14 | , text, cotext | ||
| 15 | , prop_text | ||
| 16 | ) where | ||
| 17 | |||
| 18 | import Data.Sequence (Seq, (|>), (<|)) | ||
| 19 | |||
| 20 | import Data.Text.Lazy (Text) | ||
| 21 | |||
| 22 | import Data.ByteString.Lazy (ByteString) | ||
| 23 | |||
| 24 | import GHC.Generics (Generic) | ||
| 25 | import Control.DeepSeq (NFData) | ||
| 26 | |||
| 27 | import Test.QuickCheck.Arbitrary (Arbitrary(..), CoArbitrary, genericShrink) | ||
| 28 | import Test.QuickCheck.Modifiers (NonNegative(..)) | ||
| 29 | import Test.QuickCheck.Gen (oneof, suchThat) | ||
| 30 | import Test.QuickCheck.Instances | ||
| 31 | import Test.QuickCheck (forAll, Property) | ||
| 32 | |||
| 33 | |||
| 34 | import qualified Data.Text.Lazy as TL (lines, split, null, pack, filter, intercalate, map) | ||
| 35 | import Data.Char (isSpace) | ||
| 36 | |||
| 37 | import Data.Monoid (Monoid(..), (<>)) | ||
| 38 | |||
| 39 | import Data.List (dropWhile, dropWhileEnd, groupBy, genericLength, genericReplicate) | ||
| 40 | |||
| 41 | import Data.Sequence as Seq (fromList, null, singleton) | ||
| 42 | |||
| 43 | import Data.Function (on) | ||
| 44 | |||
| 45 | import Data.Foldable (toList) | ||
| 46 | |||
| 47 | |||
| 48 | -- | A 'Printout' is a sequence of visually seperated 'Paragraph's | ||
| 49 | type Printout = Seq Paragraph | ||
| 50 | |||
| 51 | -- | A 'Paragraph' is a non-seperated sequence of 'Chunk's | ||
| 52 | type Paragraph = Seq Chunk | ||
| 53 | |||
| 54 | -- | We introduce both 'Chunk' and 'Paragraph' mainly to allow 'Raw'. | ||
| 55 | -- | ||
| 56 | -- Were we to disallow 'Raw', 'Block' would be identical to 'Paragraph' | ||
| 57 | data Chunk = Cooked Block -- ^ text semantically structured to be rendered in accordance with the display format of printer | ||
| 58 | | Raw ByteString -- ^ direct instructions to the printer | ||
| 59 | deriving (Generic, NFData, Show, CoArbitrary) | ||
| 60 | |||
| 61 | -- | 'Block' is the entry point for our structured document format | ||
| 62 | data Block = Line Line -- ^ a single 'Line' of text | ||
| 63 | | VSpace Integer -- ^ vertical space of height equivalent to 'Integer' lines | ||
| 64 | | NewlSep (Seq Block) -- ^ A sequence of 'Block's seperated by newlines | ||
| 65 | deriving (Generic, NFData, Show, CoArbitrary) | ||
| 66 | |||
| 67 | {- | A 'Line' is one of: | ||
| 68 | |||
| 69 | * a single word | ||
| 70 | * horizontal space equivalent to the width of 'Integer' `em`. | ||
| 71 | * a sequence of words seperated by spaces | ||
| 72 | |||
| 73 | We don't export all constructors and instead encourage the use of 'text'. | ||
| 74 | -} | ||
| 75 | data Line = Word Text | ||
| 76 | | HSpace Integer | ||
| 77 | | SpaceSep (Seq Line) | ||
| 78 | deriving (Generic, NFData, Show, CoArbitrary) | ||
| 79 | |||
| 80 | instance Monoid Block where | ||
| 81 | mempty = NewlSep mempty | ||
| 82 | x@(NewlSep xs) `mappend` y@(NewlSep ys) | ||
| 83 | | Seq.null xs = y | ||
| 84 | | Seq.null ys = x | ||
| 85 | | otherwise = NewlSep (xs <> ys) | ||
| 86 | (NewlSep xs) `mappend` y = NewlSep (xs |> y) | ||
| 87 | x `mappend` (NewlSep ys) = NewlSep (x <| ys) | ||
| 88 | x `mappend` y = NewlSep $ Seq.fromList [x, y] | ||
| 89 | |||
| 90 | instance Monoid Line where | ||
| 91 | mempty = SpaceSep mempty | ||
| 92 | x@(SpaceSep xs) `mappend` y@(SpaceSep ys) | ||
| 93 | | Seq.null xs = y | ||
| 94 | | Seq.null ys = x | ||
| 95 | | otherwise = SpaceSep (xs <> ys) | ||
| 96 | (SpaceSep xs) `mappend` y = SpaceSep (xs |> y) | ||
| 97 | x `mappend` (SpaceSep ys) = SpaceSep (x <| ys) | ||
| 98 | x `mappend` y = SpaceSep $ Seq.fromList [x, y] | ||
| 99 | |||
| 100 | |||
| 101 | text :: Text -> Either Block Line | ||
| 102 | -- ^ Smart constructor for 'Line'/'Block' which maps word and line boundaries (as determined by 'isSpace' and 'TL.lines' respectively) to the structure of 'Block' and 'Line'. | ||
| 103 | -- | ||
| 104 | -- Since we are unwilling to duplicate the list of chars from 'isSpace' we cannot reasonably determine a width for the various whitespace 'Char's. | ||
| 105 | -- Thus they are all weighted equally as having width 1 `em`. | ||
| 106 | text t = case splitLines t of | ||
| 107 | [] -> Right mempty | ||
| 108 | [Line x] -> Right x | ||
| 109 | xs -> Left $ mconcat xs | ||
| 110 | where | ||
| 111 | splitLines :: Text -> [Block] | ||
| 112 | splitLines t = map toBlock | ||
| 113 | . groupBy ((==) `on` TL.null) | ||
| 114 | $ TL.lines t | ||
| 115 | splitWords :: Text -> [Line] | ||
| 116 | splitWords t = map toLine | ||
| 117 | . groupBy ((==) `on` TL.null) | ||
| 118 | $ TL.split isSpace t | ||
| 119 | toBlock [] = mempty | ||
| 120 | toBlock xs@(x:_) | ||
| 121 | | TL.null x = VSpace $ genericLength xs | ||
| 122 | | otherwise = mconcat . map (Line . mconcat . splitWords) $ xs | ||
| 123 | toLine [] = mempty | ||
| 124 | toLine xs@(x:_) | ||
| 125 | | TL.null x = HSpace $ genericLength xs | ||
| 126 | | otherwise = mconcat . map Word $ xs | ||
| 127 | list :: b -> (a -> [a] -> b) -> [a] -> b | ||
| 128 | list c _ [] = c | ||
| 129 | list _ f (x:xs) = f x xs | ||
| 130 | |||
| 131 | cotext :: Block -> Text | ||
| 132 | -- ^ inverse of | ||
| 133 | -- @ | ||
| 134 | -- either id Line . `text` | ||
| 135 | -- @ | ||
| 136 | cotext (VSpace n) = TL.pack . genericReplicate (n - 1) $ '\n' | ||
| 137 | cotext (NewlSep xs) = TL.intercalate "\n" . map cotext . toList $ xs | ||
| 138 | cotext (Line x) = cotext' x | ||
| 139 | where | ||
| 140 | cotext' (Word x) = x | ||
| 141 | cotext' (HSpace n) = TL.pack . genericReplicate (n - 1) $ ' ' | ||
| 142 | cotext' (SpaceSep xs) = TL.intercalate " " . map cotext' . toList $ xs | ||
| 143 | |||
| 144 | prop_text :: Property | ||
| 145 | -- ^ prop> cotext . either id Line . `text` $ x = x | ||
| 146 | -- | ||
| 147 | -- Where 'x' is restricted to those `TL.Text` which do not contain whitespace besides ' ' and '\n'. | ||
| 148 | prop_text = forAll (TL.map normSpace <$> arbitrary) $ \x -> (cotext . either id Line . text $ x) == x | ||
| 149 | where | ||
| 150 | normSpace c | ||
| 151 | | c == '\n' = '\n' | ||
| 152 | | isSpace c = ' ' -- We have to do this because all whitespace gets interpreted as width 1 | ||
| 153 | | otherwise = c | ||
| 154 | |||
| 155 | -- | We don't test 'Raw' 'Chunk's | ||
| 156 | instance Arbitrary Chunk where | ||
| 157 | shrink = genericShrink | ||
| 158 | arbitrary = Cooked <$> arbitrary | ||
| 159 | |||
| 160 | instance Arbitrary Block where | ||
| 161 | shrink = genericShrink | ||
| 162 | arbitrary = oneof [ Line <$> arbitrary | ||
| 163 | , VSpace . getNonNegative <$> arbitrary | ||
| 164 | , NewlSep <$> arbitrary | ||
| 165 | ] | ||
| 166 | |||
| 167 | instance Arbitrary Line where | ||
| 168 | shrink = genericShrink | ||
| 169 | arbitrary = oneof [ Word . TL.filter (not . isSpace) <$> arbitrary -- 'isSpace '\n' == True' | ||
| 170 | , HSpace . getNonNegative <$> arbitrary | ||
| 171 | , SpaceSep <$> arbitrary | ||
| 172 | ] | ||
diff --git a/spec/thermoprint-spec.cabal b/spec/thermoprint-spec.cabal index bde1b73..ba97420 100644 --- a/spec/thermoprint-spec.cabal +++ b/spec/thermoprint-spec.cabal | |||
| @@ -17,9 +17,28 @@ build-type: Simple | |||
| 17 | cabal-version: >=1.10 | 17 | cabal-version: >=1.10 |
| 18 | 18 | ||
| 19 | library | 19 | library |
| 20 | -- exposed-modules: | 20 | hs-source-dirs: src |
| 21 | exposed-modules: Thermoprint.Printout | ||
| 21 | -- other-modules: | 22 | -- other-modules: |
| 22 | -- other-extensions: | 23 | -- other-extensions: |
| 23 | build-depends: base >=4.8 && <4.9 | 24 | extensions: DeriveGeneric |
| 25 | , DeriveAnyClass | ||
| 26 | , OverloadedStrings | ||
| 27 | build-depends: base >=4.8.1 && <5 | ||
| 28 | , containers >=0.5.6 && <1 | ||
| 29 | , text >=1.2.1 && <2 | ||
| 30 | , bytestring >=0.10.6 && <1 | ||
| 31 | , deepseq >=1.4.1 && <2 | ||
| 32 | , QuickCheck >=2.8.1 && <3 | ||
| 33 | , quickcheck-instances >=0.3.11 && <4 | ||
| 34 | , Cabal >=1.22.4 && <2 | ||
| 35 | , cabal-test-quickcheck >=0.1.6 && <1 | ||
| 24 | -- hs-source-dirs: | 36 | -- hs-source-dirs: |
| 25 | default-language: Haskell2010 \ No newline at end of file | 37 | default-language: Haskell2010 |
| 38 | |||
| 39 | Test-Suite tests | ||
| 40 | type: exitcode-stdio-1.0 | ||
| 41 | main-is: Spec.hs | ||
| 42 | build-depends: base >=4.8.1 && <5 | ||
| 43 | , thermoprint-spec -any | ||
| 44 | , hspec >=2.2.1 && <3 \ No newline at end of file | ||
diff --git a/spec/thermoprint-spec.nix b/spec/thermoprint-spec.nix index 2d1814e..82f018a 100644 --- a/spec/thermoprint-spec.nix +++ b/spec/thermoprint-spec.nix | |||
| @@ -1,9 +1,9 @@ | |||
| 1 | { mkDerivation, base, stdenv }: | 1 | { mkDerivation, base, bytestring, containers, stdenv, text, QuickCheck, quickcheck-instances, cabal-test-quickcheck, hspec }: |
| 2 | mkDerivation { | 2 | mkDerivation { |
| 3 | pname = "thermoprint-spec"; | 3 | pname = "thermoprint-spec"; |
| 4 | version = "1.0.0"; | 4 | version = "1.0.0"; |
| 5 | src = ./.; | 5 | src = ./.; |
| 6 | libraryHaskellDepends = [ base ]; | 6 | libraryHaskellDepends = [ base bytestring containers text QuickCheck quickcheck-instances cabal-test-quickcheck hspec ]; |
| 7 | homepage = "http://dirty-haskell.org/tags/thermoprint.html"; | 7 | homepage = "http://dirty-haskell.org/tags/thermoprint.html"; |
| 8 | description = "A specification of the API and the payload datatypes and associated utilities"; | 8 | description = "A specification of the API and the payload datatypes and associated utilities"; |
| 9 | license = stdenv.lib.licenses.publicDomain; | 9 | license = stdenv.lib.licenses.publicDomain; |
