packages feed

remarks (empty) → 0.1.0

raw patch · 12 files changed

+770/−0 lines, 12 filesdep +GenericPrettydep +basedep +directorysetup-changed

Dependencies added: GenericPretty, base, directory, filepath, remarks, tasty, tasty-golden, tasty-hunit

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Oleks (c) 2017++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Oleks name here nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,194 @@+# `remarks` — A DSL for marking student work++[![License: BSD 3-Clause](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](LICENSE)+[![Travis CI (Linux + macOS) Status](https://travis-ci.org/oleks/remarks.svg)](https://travis-ci.org/oleks/remarks)+[![AppVeyor (Windows) Status"](https://ci.appveyor.com/api/projects/status/g0hviw442o2bl8yi?svg=true)](https://ci.appveyor.com/project/oleks/remarks)++When judging student performance, it is useful to have both small, composable,+quantitative judgements, and qualitative remarks. This makes both spreadsheets+and mere text-files ill-suited for marking student work.  Although+[org-mode](http://orgmode.org/) can solve this problem to a great extent, it+becomes a heavy tool in the light of having to mark hundreds of students in a+distributed fashion. With org-mode, everything is in one file, while global,+intra-student statistics are not needed until all the students have been fully+marked.++## Design Goals++  1. One human-readable/editable file per student.+  2. Export options to spreadsheet-formats.+  3. git-friendly file format.+  4. Synchronization options with Dropbox and/or Google Drive.++Goal 4 is not necessarily related to `remarks`, but is related to marking+student work with external examiners, who are not always willing to use more+explicit version-control systems, such as git.++## Status++There is a [parser](src/Parser/Impl.hs) and baseline+[validator](src/Validator.hs). These can be invoked using `remarks parse` and+`remarks check`, respectively.++See [Issues](https://github.com/oleks/remarks/issues) for a roadmap. Feel free+to add or fix some.++## Syntax++A `.mrk` file is a list of judgements.++A judgement starts with a header mark (a sequence of `#`), a title (followed by+a `:`), given points (followed by `/`), and maximum points (followed by a line+break). The number of `#` determines the /depth/ of the header, and every file+/must/ start at depth 1, but may have multiple depth 1 judgements. Headings may+be arbitrarily nested, but must sum up correctly. For instance, here is a file+containing only quantitative remarks:++```+# Theory: 27/50+## Question 1: 10/10+## Question 2: 10/20+## Question 3: 7/20+# Practice: 35/50+## Task 1: 20/25+## Task 2: 15/25+```++The header of a judgement may be followed by qualitative remarks. Remarks begin+with an indent (two spaces), and a mood mark:++  * `*` for neutral/structural remarks;+  * `+` for positive remarks;+  * `-` for negative remarks;+  * `?` for impartial remarks.++Impartial remarks are good for judgements where the mood is left to be judged+by a higher authority. For instance, when a teaching assistant is uncertain,+and would like to let the teacher decide, or the solution cannot be used to+make a judgement about this point.++Structural remarks are good for listing things to look for. For instance, a+(template) judgement for an operating systems exercise may look something like+this:++```+## T1: 15/15++## Formal requirements: 5/5+  * Has code+  * Has XML in a reloadable/testable form+  * Has an explanation++### Quality assessment: 10/10+  * Understands semaphores+  * Understands deadlocks+  * Understands starvation+  * Avoids deadlocks+  * Avoids starvation+  * Avoids race conditions+  * Has a good degree of multiprogramming+  * Solves the problem+```++Once filled in by a teaching assistant, this may look something like this:++```+## T1: 7/15++### Formal requirements: 5/5+  * Has code+    + Yes.+  * Has XML in a reloadable/testable form+    + Yes.+  * Has an explanation+    + Yes.++### Quality assessment: 2/10+  * Understands semaphores+    - Seems to treat them as counters. Report doesn't talk about procuring or+      vacating at all.+    - Checks semaphore value directly.+    - Seems to have mixed up procure and vacate.+  * Understands deadlocks+    - If more than 5 cars arrive, then only 5 cars will be allowed to drive+      through, and everything will then deadlock.+  * Understands starvation+    ? Seems to, but this is hard to judge.+  * Avoids deadlocks+    ? See above.+  * Avoids starvation+    ? Can't judge this.+  * Avoids race conditions+    - Uses a busy loop in attempt to synchronize.+    - There is a race condition after the busy loop.+  * Has a good degree of multiprogramming+    + It's a ticket system, so it could be okay.+  * Solves the problem+    + In a complicated way, but yes.+```++## Files and Directories++The file-format is kept "git-friendly" by keeping it comprehensible in+plain-text, and allowing for independent marking by splitting the remarks for a+student into multiple files.++The simplest setup is to have one `.mrk` file per student (e.g.+[basic.mrk](samples/organization/basic.mrk)).++To support more exotic setups, `remarks` can also work with directories:++  * If supplied with a directory path, `remarks` looks for files ending in+    `.mrk` inside that directory, and comprehends the files, as above, in+    lexicographic filename order (e.g.,+    [directory-with-mrk-files](samples/organization/directory-with-mrk-files)).++  * If there exists a directory `<basename>` for any `<basename>.mrk`,+    `<basename>` is recursively searched for further `.mrk` files. Their+    contents is appended, in lexicographic filename order, to the last+    top-level judgement of `<basename>.mrk`+    [mixed-directory](samples/organization/mixed-directory)).++See the [organization samples](samples/organization) for some examples of how+judgements may be structured using files and directories.++```+├── basic.mrk+├── directory-with-mrk-files+│   ├── 01-theory.mrk+│   └── 02-practice.mrk+└── mixed-directory+    ├── 01-theory.mrk+    ├── 02-practice+    │   ├── Task1.mrk+    │   └── Task2.mrk+    └── 02-practice.mrk+```++`basic.mrk`, `directory-with-mrk-files`, and `mixed-directory` all parse to the+same judgements. In particular, the output from the following `remarks`+commands is identical:++```+$ cd samples/organization/+$ remarks parse basic.mrk+$ remarks parse directory-with-mrk-files+$ remarks parse mixed-directory+```++## Installation++[`remarks` is on Hackage](http://hackage.haskell.org/package/remarks), so you+can just use [Cabal](https://www.haskell.org/cabal/):++```+$ cabal install remarks+```++You can also clone this repository and use+[Stack](https://docs.haskellstack.org/en/stable/README/):++```+$ stack build+$ stack install+```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,130 @@+module Main where++import Ast+import Parser+import Validator++import Control.Monad ( void, filterM )+import Data.List ( sort, partition )+import System.Directory+  ( doesFileExist, doesDirectoryExist, listDirectory )+import System.FilePath+  ( (<.>), (</>), takeDirectory, takeExtension, dropExtension )+import System.Environment( getArgs )+import System.Exit ( exitWith, ExitCode ( ExitFailure ) )+import System.IO ( hPutStrLn, stderr )++import Text.PrettyPrint.GenericPretty++report :: String -> IO ()+report = hPutStrLn stderr++noCommand :: IO a+noCommand = do+  report "Tell me what to do!"+  exitWith (ExitFailure 1)++notAPath :: String -> IO a+notAPath s = do+  report $ s ++ " is not a filesystem path!"+  exitWith (ExitFailure 1)++invalidCommand :: String -> [String] -> IO a+invalidCommand c _ = do+  report $ c ++ " is not a valid command."+  exitWith (ExitFailure 1)++noMrkFile :: FilePath -> IO a+noMrkFile path = do+  report $ "Can't find a .mrk file\n" ++ path+  exitWith (ExitFailure 1)++noTopLevelJudgement :: FilePath -> IO a+noTopLevelJudgement path = do+  report $ path ++ " has no top-level judgement."+  report "I don't know how to handle this, sorry."+  exitWith (ExitFailure 1)++mustHaveJudgement :: FilePath -> IO a+mustHaveJudgement path = do+  report $ path ++ " must have at least one judgement."+  exitWith (ExitFailure 1)++parseError :: ParseError -> IO a+parseError e = do+  report $ show e+  exitWith (ExitFailure 1)++parseTopFile :: FilePath -> IO [Judgement]+parseTopFile path = do+  p <- parseFile path+  case p of+    Right js -> pure js+    Left e -> parseError e++parseFileInDir :: FilePath -> IO Judgement+parseFileInDir path = do+  js <- parsePath path+  case js of+    [j] -> pure j+    _ -> noTopLevelJudgement path++parseFileWithDir :: FilePath -> IO ([Judgement], Judgement)+parseFileWithDir path = do+  p <- parseFile path+  case p of+    Right js ->+      case length js of+        0 -> mustHaveJudgement path+        n -> pure $ (take (n - 1) js, last js)+    Left e -> parseError e++isMrkPath :: FilePath -> Bool+isMrkPath = (== ".mrk") . takeExtension++parseDir :: FilePath -> IO [Judgement]+parseDir path = do+  paths <- listDirectory path+  let (mrkPaths, otherPaths) = partition isMrkPath paths+  let fullMrkPaths = map (path </>) (sort mrkPaths)+  mapM parseFileInDir fullMrkPaths++extPaths :: FilePath -> (FilePath, FilePath)+extPaths path =+  if isMrkPath path+  then (path, dropExtension path)+  else (path <.> "mrk", path)++parsePath :: FilePath -> IO [Judgement]+parsePath path = do+  let (pathWithExt, pathWithoutExt) = extPaths path+  hasFile <- doesFileExist pathWithExt+  hasDir <- doesDirectoryExist pathWithoutExt++  if not hasFile+  then+    if hasDir+    then parseDir path+    else noMrkFile pathWithExt+  else -- we have a .mrk file+    if not hasDir+    then parseTopFile pathWithExt+    else do -- we now also have directory+      (fjs, (Judgement (h, cs, js))) <- parseFileWithDir pathWithExt+      dirJs <- parseDir pathWithoutExt+      pure $ fjs ++ [Judgement (h, cs, js ++ dirJs)]++parsePaths :: [FilePath] -> IO [[Judgement]]+parsePaths = mapM parsePath++check :: [Judgement] -> IO ()+check js = putStrLn $ pretty $ map validate js++main :: IO ()+main = do+  args <- getArgs+  case args of+    [] -> noCommand+    ("parse" : paths) -> parsePaths paths >>= putStrLn . pretty+    ("check" : paths) -> parsePaths paths >>= mapM_ check+    (c:args) -> invalidCommand c args
+ remarks.cabal view
@@ -0,0 +1,54 @@+name:                remarks+version:             0.1.0+synopsis:            A DSL for marking student work+description:         A DSL for marking student work; see README.md for further details.+homepage:            https://github.com/oleks/remarks#readme+license:             BSD3+license-file:        LICENSE+author:              Oleks+maintainer:          oleks@oleks.info+copyright:           2017 Oleks+category:            Web+build-type:          Simple+extra-source-files:  README.md+cabal-version:       >=1.10++library+  hs-source-dirs:      src+  exposed-modules:     Ast+                     , Parser+                     , Parser.Impl+                     , Validator+  build-depends:       base >= 4.7 && < 5+                     , GenericPretty >= 1.2.1+  default-language:    Haskell2010++executable remarks+  hs-source-dirs:      app+  main-is:             Main.hs+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N+  build-depends:       base+                     , remarks+                     , GenericPretty >= 1.2.1+                     , directory >= 1.2.6.2+                     , filepath >= 1.4.1.0+  default-language:    Haskell2010++test-suite remarks-test+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             Tests.hs+  other-modules:       Parser.BlackBoxTests+                       ValidatorTests+  build-depends:       base+                     , remarks+                     , GenericPretty >= 1.2.1+                     , tasty >= 0.11.0.4+                     , tasty-hunit >= 0.9.2+                     , tasty-golden >= 2.3.1.1+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N+  default-language:    Haskell2010++source-repository head+  type:     git+  location: https://github.com/oleks/remarks
+ src/Ast.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE DeriveGeneric #-}++module Ast where++import Text.PrettyPrint.GenericPretty++newtype Header+  = Header (String, Double, Double)+  deriving (Eq, Show, Generic)++instance Out Header++data Mood+  = Positive+  | Negative+  | Neutral+  | Impartial+  deriving (Eq, Show, Generic)++instance Out Mood++data CommentPart+  = CommentStr String+  | CommentCmt Comment+  deriving (Eq, Show, Generic)++instance Out CommentPart++newtype Comment+  = Comment (Mood, [CommentPart])+  deriving (Eq, Show, Generic)++instance Out Comment++newtype Judgement+  = Judgement (Header, [Comment], [Judgement])+  deriving (Eq, Show, Generic)++instance Out Judgement
+ src/Parser.hs view
@@ -0,0 +1,11 @@+module Parser+  ( ParseError+  , parseString+  , parseFile+  ) where++import Parser.Impl+  ( ParseError+  , parseString+  , parseFile+  )
+ src/Parser/Impl.hs view
@@ -0,0 +1,127 @@+{-# LANGUAGE DeriveGeneric #-}++module Parser.Impl where++import Ast+import Text.ParserCombinators.ReadP+import Text.PrettyPrint.GenericPretty++import Control.Monad ( void )+import Data.Maybe ( listToMaybe )++data ParseErrorImpl a+  = NoParse+  | AmbiguousGrammar [a]+  | NotImplemented+  deriving (Eq, Show, Generic)++instance (Out a) => Out (ParseErrorImpl a)++type ParseError = ParseErrorImpl [Judgement]++parseIntegral :: ReadP String+parseIntegral = munch1 (`elem` ['0'..'9'])++-- Source: http://hackage.haskell.org/package/cgi-3001.3.0.2/docs/src/Network-CGI-Protocol.html#maybeRead+maybeRead :: Read a => String -> Maybe a+maybeRead = fmap fst . listToMaybe . reads++parsePoints :: ReadP Double+parsePoints = do+  is <- parseIntegral+  fs <- (char '.' *> parseIntegral) +++ pure "0"+  case (maybeRead (is ++ "." ++ fs)) of+    Just x -> pure x+    _ -> pfail++lineToken :: ReadP a -> ReadP a+lineToken p = munch (`elem` [' ', '\t', '\r', '\v', '\f']) *> p++lineBreak :: ReadP ()+lineBreak = void $ lineToken $ char '\n'++munchTillExcl :: Char -> ReadP String+munchTillExcl c = munch (/= c) <* char c++parseHeader :: Int -> ReadP Header+parseHeader depth = do+  let mark = take depth $ repeat '#'+  void $ string mark+  void $ char ' '+  title <- lineToken $ munchTillExcl ':'++  points <- lineToken $ parsePoints+  void $ lineToken $ char '/'+  maxPoints <- lineToken $ parsePoints++  void $ lineBreak++  pure $ Header (title, points, maxPoints)++parseMood :: ReadP Mood+parseMood = choice+  [ char '+' *> pure Positive+  , char '-' *> pure Negative+  , char '*' *> pure Neutral+  , char '?' *> pure Impartial+  ]++parseLine :: ReadP String+parseLine = munchTillExcl '\n'++parseLines :: String -> ReadP [String]+parseLines indent = many $ string indent *> parseLine++parseCommentPart :: String -> ReadP CommentPart+parseCommentPart indent = many lineBreak *> do+  void $ string indent+  (fmap CommentCmt $ parseComment indent) <+++    (fmap CommentStr parseLine)++parseComment :: String -> ReadP Comment+parseComment indent = do+  mood <- parseMood+  void $ char ' '+  first <- fmap CommentStr parseLine+  rest <- many $ parseCommentPart (indent ++ "  ")+  pure $ Comment (mood, (first : rest))++parseComment' :: ReadP Comment+parseComment' = many lineBreak *> do+  void $ string "  "+  parseComment "  "++parseJudgement :: Int -> ReadP Judgement+parseJudgement depth = skipSpaces *> do+  header <- parseHeader depth+  comments <- many parseComment'+  subjs <- many $ parseJudgement (depth + 1)+  pure $ Judgement (header, comments, subjs)++parseJudgements :: Int -> ReadP [Judgement]+parseJudgements depth = many $ parseJudgement depth++parse :: ReadP a -> String -> [(a, String)]+parse = readP_to_S++fullParse :: ReadP a -> String -> [a]+fullParse p s = fmap fst $ parse (p <* (skipSpaces >> eof)) s++parseString' :: ReadP a -> String -> Either (ParseErrorImpl a) a+parseString' p s =+  case fullParse p s of+    [] -> Left NoParse+    [a] -> Right a+    as -> Left $ AmbiguousGrammar as++parseEntry :: ReadP [Judgement]+parseEntry = parseJudgements 1 <* skipSpaces++parseString :: String -> Either ParseError [Judgement]+parseString = parseString' parseEntry++parseFile' :: ReadP a -> FilePath -> IO (Either (ParseErrorImpl a) a)+parseFile' p path = fmap (parseString' p) $ readFile path++parseFile :: FilePath -> IO (Either ParseError [Judgement])+parseFile = parseFile' parseEntry
+ src/Validator.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE DeriveGeneric #-}++module Validator ( validate, Invalid(..) ) where++import Ast++import Control.Monad ( forM_ )++import Text.PrettyPrint.GenericPretty++data Invalid+  = PointsExceedMaxPoints Header+  | BadSubJudgementPointsSum Judgement+  | BadSubJudgementMaxPointsSum Judgement+  deriving (Eq, Show, Generic)++instance Out Invalid++try :: Bool -> Invalid -> Either Invalid ()+try True = \_ -> Right ()+try False = Left++infix 4 ~=+(~=) :: Double -> Double -> Bool+x ~= y = abs (x - y) <= 0.01++validate :: Judgement -> Either Invalid ()+validate j @ (Judgement (h @ (Header (_, p, maxP)), _, subjs)) = do+  try (p <= maxP)+    (PointsExceedMaxPoints h)+  try ((sum $ map points subjs) ~= p)+    (BadSubJudgementPointsSum j)+  try ((sum $ map maxPoints subjs) ~= maxP)+    (BadSubJudgementMaxPointsSum j)+  forM_ subjs validate++points :: Judgement -> Double+points (Judgement (Header (_, v, _), _, _)) = v++maxPoints :: Judgement -> Double+maxPoints (Judgement (Header (_, _, v), _, _)) = v
+ test/Parser/BlackBoxTests.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE DeriveGeneric #-}++module Parser.BlackBoxTests ( allTests ) where++import Ast+import Parser++import Test.Tasty+import Test.Tasty.HUnit+-- import Test.Tasty.QuickCheck+import Test.Tasty.Golden++import Text.PrettyPrint.GenericPretty++unitTests :: TestTree+unitTests = testGroup "Unit tests"+  [ testCase "Lone header line" $+      parseString "# A: 0/0\n" @?=+        Right+          [ Judgement (Header ("A",0.0,0.0), [], [])+          ]+  , testCase "A couple same-depth header lines" $+      parseString "# A: 0/0\n# B: 0/0\n" @?=+        Right+          [ Judgement (Header ("A",0.0,0.0), [], [])+          , Judgement (Header ("B",0.0,0.0), [], [])+          ]+  , testCase "A simple hierarchy of headers" $+      parseString "# A: 0/0\n## B: 0/0\n" @?=+        Right+          [ Judgement (Header ("A",0.0,0.0), [],+            [ Judgement (Header ("B",0.0,0.0), [], [])+            ])+          ]+  , testCase "A couple simple hierarchies" $+      parseString "# A: 0/0\n## B: 0/0\n# C: 0/0\n" @?=+        Right+          [ Judgement (Header ("A",0.0,0.0), [],+            [ Judgement (Header ("B",0.0,0.0), [], [])+            ])+          , Judgement (Header ("C",0.0,0.0), [], [])+          ]+  ]++qcTests :: TestTree+qcTests = testGroup "QuickCheck tests" []++golden' :: String -> TestTree+golden' basename =+  goldenVsFile name fref fout $ do+    p <- parseFile fin+    writeFile fout $ (pretty p) ++ "\n"+  where+    prefix = "test/golden/"+    name = basename ++ ".mrk"+    fin = prefix ++ name+    fref = prefix ++ basename ++ ".ref"+    fout = prefix ++ basename ++ ".out"++goldenTests :: TestTree+goldenTests = testGroup "Golden tests"+  [ golden' "deep-judgement"+  , golden' "some-top-level-judgements"+  , golden' "some-judgements"+  ]++allTests :: TestTree+allTests = testGroup "Black-box parser tests"+  [ unitTests, qcTests, goldenTests ]
+ test/Tests.hs view
@@ -0,0 +1,10 @@+module Main where++import Test.Tasty++import qualified Parser.BlackBoxTests as PBBT+import qualified ValidatorTests as VT++main :: IO ()+main = defaultMain $ testGroup "All Tests"+  [ PBBT.allTests, VT.allTests ]
+ test/ValidatorTests.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE DeriveGeneric #-}++module ValidatorTests ( allTests ) where++import Ast+import Parser+import Validator++import Test.Tasty+import Test.Tasty.HUnit+-- import Test.Tasty.QuickCheck+import Test.Tasty.Golden++import Text.PrettyPrint.GenericPretty++validateStr :: String -> [Either Invalid ()]+validateStr s =+  case parseString s of+    Right js -> map validate js+    Left e -> []++posUnitTests :: TestTree+posUnitTests = testGroup "Positive Unit Tests"+  [ testCase "Lone header line" $+      validateStr "# A: 0/0\n" @?=+        [Right ()]+  , testCase "A couple same-depth header lines" $+      validateStr "# A: 0/0\n# B: 0/0\n" @?=+        [Right (), Right ()]+  , testCase "A simple hierarchy of headers" $+      validateStr "# A: 0/0\n## B: 0/0\n" @?=+        [Right ()]+  , testCase "A couple simple hierarchies" $+      validateStr "# A: 0/0\n## B: 0/0\n# C: 0/0\n" @?=+        [Right (), Right ()]+  ]++negUnitTests :: TestTree+negUnitTests = testGroup "Positive Unit Tests"+  [ testCase "Points exceed max points" $+      validateStr "# A: 1/0\n" @?=+        [Left $ PointsExceedMaxPoints (Header ("A", 1.0, 0.0))]+  , testCase "Sub-judgement points don't sum up to points" $+      validateStr "# A: 0/0\n## B: 1/0\n" @?=+        [Left $ BadSubJudgementPointsSum+          (Judgement (Header ("A", 0.0, 0.0), [],+            [Judgement (Header ("B", 1.0, 0.0), [], [])]))]+  , testCase "Sub-judgement max-points don't sum up to max-points" $+      validateStr "# A: 0/0\n## B: 0/1\n" @?=+        [Left $ BadSubJudgementMaxPointsSum+          (Judgement (Header ("A", 0.0, 0.0), [],+            [Judgement (Header ("B", 0.0, 1.0), [], [])]))]+  ]++qcTests :: TestTree+qcTests = testGroup "QuickCheck tests" []++goldenTests :: TestTree+goldenTests = testGroup "Golden tests" []++allTests :: TestTree+allTests = testGroup "Validator tests"+  [ posUnitTests, negUnitTests, qcTests, goldenTests ]