arbtt 0.10.1 → 0.10.2
raw patch · 6 files changed
+168/−119 lines, 6 filesdep ~aesondep ~containersdep ~tasty
Dependency ranges changed: aeson, containers, tasty, terminal-progress-bar
Files
- README.md +14/−12
- arbtt.cabal +17/−5
- doc/arbtt.xml +51/−0
- src/Stats.hs +63/−74
- src/stats-main.hs +7/−11
- tests/test.hs +16/−17
README.md view
@@ -7,9 +7,9 @@ background and, every minute, records what windows are open on your desktop, what their titles are, which one is active. The accompanied statistics program lets you derive information from this log file, i.e.-what how much of your time have you been spending with e-Mail, or what+what how much of your time have you been spending with e-mail, or what projects are your largest time wasters. The mapping from the raw window-titles to sensible „tags“ is done by a configuration file with an powerful+titles to sensible "tags" is done by a configuration file with an powerful syntax. Installation@@ -20,15 +20,17 @@ $ runhaskell Setup.hs configure $ runhaskell Setup.hs build $ runhaskell Setup.hs install+ +Other installation options are described in the [website](http://arbtt.nomeata.de/#install). -You also need to make sure that arbtt-capture is started with your X+You also need to make sure that `arbtt-capture` is started with your X session: - If you use GNOME or KDE, you can copy the file `arbtt-capture.desktop` to `~/.config/autostart/`. You might need to put the- full path to arbtt-capture in the `Exec` line there, if you did not do a+ full path to `arbtt-capture` in the `Exec` line there, if you did not do a system wide installation. - If you use macOS, you can use `launchd` for this.- Create a .plist file like the following+ Create a `.plist` file like the following (with the path changed to match where arbtt-capture is located in your system): ```xml@@ -37,7 +39,7 @@ <plist version="1.0"> <dict> <key>Label</key>- <string>com.foo.arbtt</string>+ <string>de.nomeata.arbtt</string> <key>Program</key> <string>/path/to/arbtt-capture</string> <key>RunAtLoad</key>@@ -47,10 +49,10 @@ </dict> </plist> ```- and place it in `~/Library/LaunchAgents/com.foo.arbtt.plist`.- You can replace "foo" with anything, such as your username, in both file name and content.+ and place it in `~/Library/LaunchAgents/de.nomeata.arbtt.plist`. This will ensure `arbtt-capture` is started whenever you log in.- To start the service without needing a new login, you can run `launchctl load ~/Library/LaunchAgents/com.foo.arbtt.plist`.+ To start the service without needing a new login,+ you can run `launchctl load ~/Library/LaunchAgents/de.nomeata.arbtt.plist`. If you want to record samples at a different rate than one per minute, you will have to pass the `--sample-rate` parameter to arbtt-capture.@@ -58,11 +60,11 @@ Documentation ------------ -Full documentation is now provided in the user manual in the doc/+Full documentation is now provided in the user manual in the [doc/](doc/) directory. If you have the docbook xsl toolchain installed, you can generate the HTML documentation by entering "make" in that directory. Otherwise, you can use the-[online version of the User’s Guide](http://arbtt.nomeata.de/doc/users_guide/index.html)+[online version of the User’s Guide](http://arbtt.nomeata.de/doc/users_guide/index.html). Beware that this will also reflect the latest development version. Development@@ -76,7 +78,7 @@ User and Developer discussion happens on the arbtt mailing list: arbtt@lists.nomeata.de To subscribe to the list, visit:- http://lists.nomeata.de/mailman/listinfo/arbtt+ <http://lists.nomeata.de/mailman/listinfo/arbtt> The issue tracker is hosted on bitbucket: <https://bitbucket.org/nomeata/arbtt/issues>
arbtt.cabal view
@@ -1,5 +1,5 @@ name: arbtt-version: 0.10.1+version: 0.10.2 license: GPL-2 license-file: LICENSE category: Desktop@@ -41,7 +41,8 @@ hs-source-dirs: src build-depends: base >= 4.7 && < 5,- filepath, directory, transformers, utf8-string, strict, containers,+ filepath, directory, transformers, utf8-string, strict,+ containers >= 0.5 && < 0.7, aeson >= 0.10 && < 1.5, binary >= 0.5, bytestring, deepseq@@ -107,9 +108,10 @@ transformers, directory, filepath, aeson >= 0.10 && < 1.5, array == 0.4.* || == 0.5.*,- terminal-progress-bar >= 0.2 && < 0.3,+ terminal-progress-bar >= 0.4 && < 0.5, bytestring-progress,- mtl+ mtl,+ text if !os(windows) build-depends: unix@@ -133,6 +135,7 @@ Text.ParserCombinators.Parsec.ExprFail Text.Regex.PCRE.Light.Text TermSize+ Paths_arbtt ghc-options: -rtsopts if os(windows) cpp-options: -DWIN32@@ -170,6 +173,7 @@ TimeLog DumpFormat Data.List.TakeR+ Paths_arbtt ghc-options: -rtsopts if os(windows) cpp-options: -DWIN32@@ -209,6 +213,7 @@ TimeLog LockFile DumpFormat+ Paths_arbtt ghc-options: -rtsopts if os(windows) cpp-options: -DWIN32@@ -255,6 +260,7 @@ Data.Binary.StringRef CommonStartup TimeLog+ Paths_arbtt ghc-options: -rtsopts if os(windows) cpp-options: -DWIN32@@ -279,9 +285,10 @@ Text.Parsec.ExprFail Text.Regex.PCRE.Light.Text TimeLog+ Paths_arbtt Build-depends: base >= 4.7 && < 5- , tasty >= 0.7 && < 1.2+ , tasty >= 0.7 && < 1.3 , tasty-golden >= 2.2.0.2 && < 2.4 , tasty-hunit >= 0.2 && < 0.11 , process-extras >= 0.2 && < 0.8@@ -304,6 +311,11 @@ else build-depends: time >= 1.5 default-language: Haskell98+ build-tool-depends:+ arbtt:arbtt-stats+ , arbtt:arbtt-dump+ , arbtt:arbtt-import+ , arbtt:arbtt-recover source-repository head type: git
doc/arbtt.xml view
@@ -57,6 +57,18 @@ <contrib>Documentation writer</contrib> <email>gwern@gwern.net</email> </author>+ <author id="paolo">+ <firstname>Paolo G.</firstname>+ <surname>Giarrusso</surname>+ <contrib>Contributor</contrib>+ <email>p.giarrusso@gmail.com</email>+ </author>+ <author id="michal">+ <firstname>Michal J.</firstname>+ <surname>Gajda</surname>+ <contrib>Contributor</contrib>+ <email>migamake@migamake.com</email>+ </author> </authorgroup> </articleinfo> <abstract>@@ -1047,6 +1059,13 @@ arbtt <- reshape(arbtt, v.names="Time", timevar="Tag", idvar="Day", direction="wide") </programlisting> </sect2>+ <sect2 id="current-day">+ <title>Selecting the current day</title>+ <para>+ This can be done easily with a bit of help from the command line:+ </para>+ <programlisting>arbtt-stats --filter='$date>='`date +"%Y-%m-%d"`</programlisting>+ </sect2> </sect1> <sect1 id="contributed"> <title>Contributed tools</title>@@ -1778,6 +1797,38 @@ <para> The version history with changes relevant for the user is documented here. </para>++ <sect2 id="release-notes-0.10.2">+ <title>Version 0.10.2</title>+ <itemizedlist>+ <listitem>+ <para>+ The category report now also has an entry <literal>(total time)</literal>, especially useful with <option>--for-each</option>.+ </para>+ </listitem>+ <listitem>+ <para>+ Support for GHC-8.0 and older has been dropped.+ </para>+ </listitem>+ </itemizedlist>+ </sect2>++ <sect2 id="release-notes-0.10.1">+ <title>Version 0.10.1</title>+ <itemizedlist>+ <listitem>+ <para>+ Rodney Lorrimar contributed fixed to build against newer versions of dependencies.+ </para>+ </listitem>+ <listitem>+ <para>+ Michal J. Gajda ensured that intervals are also printed as local times.+ </para>+ </listitem>+ </itemizedlist>+ </sect2> <sect2 id="release-notes-0.10"> <title>Version 0.10</title>
src/Stats.hs view
@@ -22,7 +22,7 @@ import Data.List import Data.Ord import Text.Printf-import qualified Data.Map as M+import qualified Data.Map.Strict as M import qualified Data.Set as S import Data.MyText (Text,pack,unpack) import Data.Function (on)@@ -86,7 +86,7 @@ -- Data format semantically representing the result of a report, including the -- title-type Interval = (String,String,String,String) +type Interval = (String,String,String,String) data ReportResults = ListOfFields String [(String, String)] | ListOfTimePercValues String [(String, String, Double)]@@ -98,8 +98,8 @@ filterPredicate :: [Filter] -> TimeLogEntry (Ctx, ActivityData) -> Bool-filterPredicate filters tl = - all (\flag -> case flag of +filterPredicate filters tl =+ all (\flag -> case flag of Exclude act -> excludeTag act tl Only act -> onlyTag act tl GeneralCond s-> applyCond s (cTimeZone (fst (tlData tl))) M.empty tl) filters@@ -110,8 +110,8 @@ applyActivityFilter :: [ActivityFilter] -> Activity -> Bool applyActivityFilter fs act = all go fs where go (ExcludeActivity matcher) = not (matchActivityMatcher matcher act)- go (OnlyActivity matcher) = matchActivityMatcher matcher act - + go (OnlyActivity matcher) = matchActivityMatcher matcher act+ excludeTag matcher = not . any (matchActivityMatcher matcher) . snd . tlData onlyTag matcher = any (matchActivityMatcher matcher) . snd . tlData @@ -122,7 +122,7 @@ matchActivityMatcher (MatchActivity act1) act2 = act1 == act2 matchActivityMatcher (MatchCategory cat) act2 = Just cat == activityCategory act2 -parseActivityMatcher :: String -> ActivityMatcher +parseActivityMatcher :: String -> ActivityMatcher parseActivityMatcher str | last str == ':' = MatchCategory (pack (init str)) | otherwise = MatchActivity (read str) @@ -145,15 +145,15 @@ prepareCalculations :: LeftFold (Bool :!: TimeLogEntry (Ctx, ActivityData)) Calculations prepareCalculations =- pure (\fd ld ttr tts s -> + pure (\fd ld ttr tts s -> let c = Calculations { firstDate = fd , lastDate = ld , timeDiff = diffUTCTime (lastDate c) (firstDate c) , totalTimeRec = ttr , totalTimeSel = tts- , fractionRec = realToFrac (totalTimeRec c) / (realToFrac (timeDiff c))- , fractionSel = realToFrac (totalTimeSel c) / (realToFrac (timeDiff c))+ , fractionRec = realToFrac (totalTimeRec c) / realToFrac (timeDiff c)+ , fractionSel = realToFrac (totalTimeSel c) / realToFrac (timeDiff c) , fractionSelRec = realToFrac (totalTimeSel c) / realToFrac (totalTimeRec c) , sums = s } in c) <*>@@ -161,8 +161,7 @@ onAll calcLastDate <*> onAll calcTotalTime <*> onSelected calcTotalTime <*>- onSelected calcSums - where+ onSelected calcSums calcFirstDate :: LeftFold (TimeLogEntry a) UTCTime calcFirstDate = fromJust <$> lfFirst `mapElems` tlTime@@ -176,7 +175,7 @@ calcSums :: LeftFold (TimeLogEntry (a, [Activity])) (M.Map Activity NominalDiffTime) calcSums = LeftFold M.empty (\m tl ->- let go' m act = M.insertWith' (+) act (fromInteger (tlRate tl)/1000) m+ let go' m act = M.insertWith (+) act (fromInteger (tlRate tl)/1000) m in foldl' go' m (snd (tlData tl))) id processRepeater :: TimeZone -> Repeater -> LeftFold (Bool :!: TimeLogEntry (Ctx, ActivityData)) ReportResults -> LeftFold (Bool :!: TimeLogEntry (Ctx, ActivityData)) ReportResults@@ -218,8 +217,8 @@ processReport tz opts GeneralInfos = pure (\n firstDate lastDate ttr tts -> let timeDiff = diffUTCTime lastDate firstDate- fractionRec = realToFrac ttr / (realToFrac timeDiff) :: Double- fractionSel = realToFrac tts / (realToFrac timeDiff) :: Double+ fractionRec = realToFrac ttr / realToFrac timeDiff :: Double+ fractionSel = realToFrac tts / realToFrac timeDiff :: Double fractionSelRec = realToFrac tts / realToFrac ttr :: Double in ListOfFields "General Information" [ ("FirstRecord", show firstDate)@@ -239,43 +238,39 @@ processReport tz opts TotalTime = onSelected $- pure (\totalTimeSel sums -> + pure (\totalTimeSel sums -> ListOfTimePercValues "Total time per tag" . mapMaybe (\(tag,time) -> let perc = realToFrac time/realToFrac totalTimeSel pick = applyActivityFilter (roActivityFilter opts) tag in if pick && perc*100 >= roMinPercentage opts- then Just $ ( show tag- , showTimeDiff opts time- , perc)+ then Just ( show tag , showTimeDiff opts time , perc) else Nothing ) .- reverse .- sortBy (comparing snd) $- M.toList $- sums) <*>+ sortOn (Down . snd) $+ M.toList sums) <*> calcTotalTime <*>- calcSums + calcSums processReport tz opts (Category cat) = pure (\c -> processCategoryReport opts c cat) <*> prepareCalculations -processReport tz opts EachCategory = +processReport tz opts EachCategory = pure (\c cats -> MultipleReportResults $ map (processCategoryReport opts c) cats) <*> prepareCalculations <*> onSelected calcCategories processReport tz opts (IntervalCategory cat) =- processIntervalReport tz opts ("Intervals for category " ++ show cat) (extractCat cat) + processIntervalReport tz opts ("Intervals for category " ++ show cat) (extractCat cat) where extractCat :: Category -> ActivityData -> Maybe String extractCat cat = fmap (unpack . activityName) . listToMaybe . filter ( (==Just cat) . activityCategory ) processReport tz opts (IntervalTag tag) =- processIntervalReport tz opts ("Intervals for category " ++ show tag) (extractTag tag) + processIntervalReport tz opts ("Intervals for category " ++ show tag) (extractTag tag) where extractTag :: Activity -> ActivityData -> Maybe String- extractTag tag = fmap show . listToMaybe . filter ( (==tag) )+ extractTag tag = fmap show . listToMaybe . filter (==tag) processReport tz opts DumpSamples = DumpResult <$> onSelected (mapElems toList $ fmap $@@ -288,41 +283,35 @@ where go' s (Activity (Just cat) _) = S.insert cat s go' s _ = s -processCategoryReport opts ~(Calculations {..}) cat =+processCategoryReport opts ~Calculations{..} cat = PieChartOfTimePercValues ("Statistics for category " ++ show cat) $ let filteredSums = M.filterWithKey (\a _ -> isCategory cat a) sums- uncategorizedTime = totalTimeSel - M.fold (+) 0 filteredSums+ uncategorizedTime = totalTimeSel - M.foldl' (+) 0 filteredSums tooSmallSums = M.filter (\t -> realToFrac t / realToFrac totalTimeSel * 100 < roMinPercentage opts) filteredSums- tooSmallTimes = M.fold (+) 0 tooSmallSums+ tooSmallTimes = M.foldl' (+) 0 tooSmallSums in-- mapMaybe (\(tag,time) ->- let perc = realToFrac time/realToFrac totalTimeSel- pick = applyActivityFilter (roActivityFilter opts) tag- in if pick && perc*100 >= roMinPercentage opts- then Just ( show tag- , showTimeDiff opts time- , perc)- else Nothing- )- (reverse $ sortBy (comparing snd) $ M.toList filteredSums)+ [ (show tag, showTimeDiff opts time, perc)+ | (tag,time) <- sortOn (Down . snd) $ M.toList filteredSums+ , applyActivityFilter (roActivityFilter opts) tag+ , let perc = realToFrac time/realToFrac totalTimeSel+ , perc*100 >= roMinPercentage opts+ ] ++- (- if tooSmallTimes > 0- then [( printf "(%d entries omitted)" (M.size tooSmallSums)- , showTimeDiff opts tooSmallTimes- , realToFrac tooSmallTimes/realToFrac totalTimeSel- )]- else []- )- ++ - (if uncategorizedTime > 0- then [( "(unmatched time)"- , showTimeDiff opts uncategorizedTime- , realToFrac uncategorizedTime/realToFrac totalTimeSel- )]- else []- )+ [ ( printf "(%d entries omitted)" (M.size tooSmallSums)+ , showTimeDiff opts tooSmallTimes+ , realToFrac tooSmallTimes/realToFrac totalTimeSel+ ) | tooSmallTimes > 0 ]+ +++ [ ( "(unmatched time)"+ , showTimeDiff opts uncategorizedTime+ , realToFrac uncategorizedTime/realToFrac totalTimeSel+ ) | uncategorizedTime > 0]+ +++ [ ( "(total time)"+ , showTimeDiff opts totalTimeSel+ , realToFrac totalTimeSel/realToFrac totalTimeSel+ )+ ] tlRateTimediff :: TimeLogEntry a -> NominalDiffTime tlRateTimediff tle = fromIntegral (tlRate tle) / 1000@@ -331,7 +320,7 @@ processIntervalReport tz opts title extr = runOnIntervals go1 go2 where go1 :: LeftFold (TimeLogEntry (Ctx, ActivityData)) [Interval]- go1 = go3 `mapElems` fmap (extr . snd) + go1 = go3 `mapElems` fmap (extr . snd) go3 :: LeftFold (TimeLogEntry (Maybe String)) [Interval] go3 = runOnGroups sameGroup go4 (onJusts toList) sameGroup tl1 tl2 =@@ -352,10 +341,10 @@ (fromJust <$> lfLast) go2 :: LeftFold [Interval] ReportResults go2 = ListOfIntervals title <$> concatFold- + {-- ((extr. snd) `filterWith` + ((extr. snd) `filterWith` runOnIntervals (runOnGroups ((==) `on` tlData) -}@@ -393,20 +382,20 @@ RFCSV -> renderWithDelimiter "," $ renderXSV reportdata RFTSV -> renderWithDelimiter "\t" $ renderXSV reportdata -renderReportText titleMod (ListOfFields title dats) = +renderReportText titleMod (ListOfFields title dats) = underline (titleMod title) ++- (tabulate False $ map (\(f,v) -> [f,v]) dats)+ tabulate False (map (\(f,v) -> [f,v]) dats) -renderReportText titleMod (ListOfTimePercValues title dats) = - underline (titleMod title) ++ (tabulate True $ listOfValues dats)+renderReportText titleMod (ListOfTimePercValues title dats) =+ underline (titleMod title) ++ tabulate True (listOfValues dats) -renderReportText titleMod (PieChartOfTimePercValues title dats) = - underline (titleMod title) ++ (tabulate True $ piechartOfValues dats)+renderReportText titleMod (PieChartOfTimePercValues title dats) =+ underline (titleMod title) ++ tabulate True (piechartOfValues dats) -renderReportText titleMod (ListOfIntervals title dats) = - underline (titleMod title) ++ (tabulate True $ listOfIntervals dats)+renderReportText titleMod (ListOfIntervals title dats) =+ underline (titleMod title) ++ tabulate True (listOfIntervals dats) -renderReportText titleMod (RepeatedReportResults cat reps) = +renderReportText titleMod (RepeatedReportResults cat reps) = intercalate "\n" $ map (\(v,rr) -> renderReportText (titleMod . mod v) rr) reps where mod v s = s ++ " (" ++ cat ++ " " ++ v ++ ")" @@ -424,7 +413,7 @@ -- The reporting of "General Information" is not supported for the -- comma-separated output format.-renderXSV (ListOfFields title dats) = +renderXSV (ListOfFields title dats) = error ("\"" ++ title ++ "\"" ++ " not supported for this output format") renderXSV (ListOfTimePercValues _ dats) = listOfValues dats@@ -462,7 +451,7 @@ days = s `div` (24*60*60) hours = (s `div` (60*60)) `mod` 24 mins = (s `div` 60) `mod` 60- secs = s `mod` 60 + secs = s `mod` 60 go False [] = "0s" go True [] = "" -- go True vs | all (==0) (map fst vs) = concat (replicate (length vs) " ")@@ -475,13 +464,13 @@ where s = round t :: Integer hours = s `div` (60*60) mins = (s `div` 60) `mod` 60- secs = s `mod` 60 + secs = s `mod` 60 showAsLocalTime :: TimeZone -> UTCTime -> String showAsLocalTime tz = formatTime defaultTimeLocale "%x %X" . utcToZonedTime tz underline :: String -> String-underline str = unlines +underline str = unlines [ str , map (const '=') str ]
src/stats-main.hs view
@@ -17,6 +17,7 @@ import System.ProgressBar import TermSize import qualified Data.MyText as T+import qualified Data.Text.Lazy as TL import Data.Time.LocalTime import TimeLog@@ -185,17 +186,12 @@ trackedTimelog <- case isTerm of True -> do hSetBuffering stderr NoBuffering- trackProgressWithChunkSize (fromIntegral size `div` 100) (\_ b -> do- (_height, width) <- getTermSize- hPutChar stderr '\r'- hPutStr stderr $- mkProgressBar (msg "Processing data") percentage (fromIntegral width) $- Progress (fromIntegral b) (fromIntegral size)- when (fromIntegral b >= fromIntegral size) $ do- hPutChar stderr '\r'- hPutStr stderr (replicate width ' ')- hPutChar stderr '\r'- ) timelog+ let pbStyle = defStyle { stylePrefix = msg (TL.pack "Processing data")+ , stylePostfix = percentage }+ pb <- hNewProgressBar stderr pbStyle 10 (Progress 0 100 ())+ trackProgressWithChunkSize (fromIntegral size `div` 100)+ (\_ b -> updateProgress pb (const (Progress (fromIntegral b) (fromIntegral size) ())))+ timelog False -> return timelog let captures = parseTimeLog trackedTimelog
tests/test.hs view
@@ -28,11 +28,10 @@ main = do setEnv "TZ" "UTC" -- to make tests reproducible- distDir <- fromMaybe "dist" `liftM` lookupEnv "HASKELL_DIST_DIR"- defaultMain (tests distDir)+ defaultMain tests -tests :: FilePath -> TestTree-tests distDir = testGroup "Tests" [goldenTests distDir, regressionTests, parserTests]+tests :: TestTree+tests = testGroup "Tests" [goldenTests, regressionTests, parserTests] regressionTests :: TestTree regressionTests = testGroup "Regression tests"@@ -60,47 +59,47 @@ ] -goldenTests :: FilePath -> TestTree-goldenTests distDir = testGroup "Golden tests"+goldenTests :: TestTree+goldenTests = testGroup "Golden tests" [ goldenVsString "dump small" "tests/small_dump.out" $- run (distDir ++ "/build/arbtt-dump/arbtt-dump") ["-f","tests/small.log", "-t", "Show"] B.empty+ run "arbtt-dump" ["-f","tests/small.log", "-t", "Show"] B.empty , goldenVsFile "import small" "tests/small_import.out" "tests/small_import.out.actual" $ void $ do tryIOError $ removeFile "tests/small_import.out.actual" B.readFile "tests/small_import.in" >>=- run (distDir ++ "/build/arbtt-import/arbtt-import") ["-f","tests/small_import.out.actual"]+ run "arbtt-import" ["-f","tests/small_import.out.actual"] , goldenVsString "dump small JSON" "tests/small_dump_json.out" $- run (distDir ++ "/build/arbtt-dump/arbtt-dump") ["-f","tests/small.log", "-t", "JSON"] B.empty+ run "arbtt-dump" ["-f","tests/small.log", "-t", "JSON"] B.empty , goldenVsFile "import small JSON" "tests/small_import_json.out" "tests/small_import_json.out.actual" $ void $ do tryIOError $ removeFile "tests/small_import_json.out.actual" B.readFile "tests/small_import_json.in" >>=- run (distDir ++ "/build/arbtt-import/arbtt-import") ["-f","tests/small_import_json.out.actual", "-t", "JSON"]+ run "arbtt-import" ["-f","tests/small_import_json.out.actual", "-t", "JSON"] , goldenVsFile "import small JSON (list)" "tests/small_import_json_list.out" "tests/small_import_json_list.out.actual" $ void $ do tryIOError $ removeFile "tests/small_import_json_list.out.actual" B.readFile "tests/small_import_json_list.in" >>=- run (distDir ++ "/build/arbtt-import/arbtt-import") ["-f","tests/small_import_json_list.out.actual", "-t", "JSON"]+ run "arbtt-import" ["-f","tests/small_import_json_list.out.actual", "-t", "JSON"] , goldenVsFile "recover small" "tests/small_borked_recover.out" "tests/small_borked_recover.out.actual" $ void $- run (distDir ++ "/build/arbtt-recover/arbtt-recover") ["-i","tests/small_borked_recover.out", "-o", "tests/small_borked_recover.out.actual"] B.empty+ run "arbtt-recover" ["-i","tests/small_borked_recover.out", "-o", "tests/small_borked_recover.out.actual"] B.empty , goldenVsString "stats small" "tests/small_stats.out" $- run (distDir ++ "/build/arbtt-stats/arbtt-stats") ["--logfile", "tests/small.log", "--categorize", "tests/small.cfg"] B.empty+ run "arbtt-stats" ["--logfile", "tests/small.log", "--categorize", "tests/small.cfg"] B.empty , goldenVsString "stats small csv" "tests/small_stats_csv.out" $- run (distDir ++ "/build/arbtt-stats/arbtt-stats") ["--logfile", "tests/small.log", "--categorize", "tests/small.cfg", "--output-format", "csv"] B.empty+ run "arbtt-stats" ["--logfile", "tests/small.log", "--categorize", "tests/small.cfg", "--output-format", "csv"] B.empty , goldenVsString "stats small unicode" "tests/unicode_stats.out" $- run (distDir ++ "/build/arbtt-stats/arbtt-stats") ["--logfile", "tests/unicode.log", "--categorize", "tests/unicode.cfg"] B.empty+ run "arbtt-stats" ["--logfile", "tests/unicode.log", "--categorize", "tests/unicode.cfg"] B.empty , goldenVsString "stats gap handling" "tests/gap-handling.out" $- run (distDir ++ "/build/arbtt-stats/arbtt-stats") ["--logfile", "tests/gap-handling.log", "--categorize", "tests/gap-handling.cfg", "--intervals", "Program:"] B.empty+ run "arbtt-stats" ["--logfile", "tests/gap-handling.log", "--categorize", "tests/gap-handling.cfg", "--intervals", "Program:"] B.empty , goldenVsString "condition binding stats" "tests/condition_bindings_stats.out" $- run (distDir ++ "/build/arbtt-stats/arbtt-stats") ["--logfile", "tests/small.log", "--categorize", "tests/condition_bindings.cfg"] B.empty+ run "arbtt-stats" ["--logfile", "tests/small.log", "--categorize", "tests/condition_bindings.cfg"] B.empty ] testParser env parser input = do