packages feed

tricorder-0.1.0.0: src/Tricorder/CLI.hs

module Tricorder.CLI
    ( showLog
    , showSource
    , showStatus
    , showTests
    ) where

import Atelier.Effects.Clock (Clock, currentTimeZone)
import Atelier.Effects.Console (Console)
import Atelier.Effects.Delay (Delay)
import Atelier.Effects.Exit (Exit, exitFailure)
import Atelier.Effects.File (File)
import Atelier.Effects.FileSystem (FileSystem, doesFileExist, followFile, readFileLbs)
import Data.Aeson (encode)
import Data.Time.Format (defaultTimeLocale, formatTime)
import Data.Time.LocalTime (utcToLocalTime)
import Effectful.Reader.Static (Reader, ask)

import Atelier.Effects.Console qualified as Console
import Data.ByteString.Lazy qualified as BSL
import Data.Text qualified as T

import Tricorder.Arguments
    ( FollowMode (..)
    , OutputFormat (..)
    , StatusOptions (..)
    , TestOptions (..)
    , Verbosity (..)
    , WaitMode (..)
    )
import Tricorder.BuildState
    ( BuildPhase (..)
    , BuildProgress (..)
    , BuildResult (..)
    , BuildState (..)
    , Diagnostic (..)
    , Severity (..)
    , TestCase (..)
    , TestCaseOutcome (..)
    , TestRun (..)
    , TestRunCompletion (..)
    , TestRunError (..)
    )
import Tricorder.CLI.Render
    ( diagnosticLineIndexed
    , formatDuration
    , renderSourceResults
    )
import Tricorder.Effects.UnixSocket (UnixSocket)
import Tricorder.GhcPkg.Types (SourceQuery)
import Tricorder.Runtime (SocketPath (..))
import Tricorder.Socket.Client
    ( querySource
    , queryStatus
    , queryStatusWait
    )
import Tricorder.TestOutput (stripGhciNoise)


-- | Print a build-command failure message and exit non-zero.
reportBuildFailed :: (Console :> es, Exit :> es) => Text -> Eff es a
reportBuildFailed msg = do
    Console.putTextLn "Build command failed:"
    Console.putTextLn msg
    exitFailure


showStatus
    :: ( Clock :> es
       , Console :> es
       , Exit :> es
       , File :> es
       , Reader SocketPath :> es
       , UnixSocket :> es
       )
    => StatusOptions -> Eff es ()
showStatus opts = do
    SocketPath sockPath <- ask
    when (opts.wait == WaitForBuild && opts.format == TextOutput) $ do
        current <- queryStatus sockPath
        case current of
            Right BuildState {phase = Building _} -> Console.putStrLn "Building..."
            Right BuildState {phase = Restarting} -> Console.putStrLn "Restarting..."
            Right BuildState {phase = Testing _} -> Console.putStrLn "Testing..."
            _ -> pure ()
    result <-
        case opts.wait of
            WaitForBuild -> queryStatusWait sockPath
            ShowCurrent -> queryStatus sockPath
    case result of
        Left err -> Console.putTextLn $ "Error: " <> err
        Right state ->
            case opts.format of
                JsonOutput -> do
                    Console.putStr $ BSL.toStrict $ encode state
                    Console.putStrLn ""
                TextOutput ->
                    renderText opts.verbosity opts.expand state
  where
    renderText verbosity expand state = case state.phase of
        Building _ -> Console.putStrLn "Building..."
        Restarting -> Console.putStrLn "Restarting..."
        Testing _ -> Console.putStrLn "Testing..."
        BuildFailed msg -> reportBuildFailed msg
        Done r -> do
            tz <- currentTimeZone
            case expand of
                Just n ->
                    case r.diagnostics !!? (n - 1) of
                        Nothing ->
                            Console.putTextLn
                                $ "No diagnostic #"
                                    <> show n
                                    <> " (current build has "
                                    <> show (length r.diagnostics)
                                    <> ")"
                        Just d -> do
                            Console.putTextLn $ diagnosticLineIndexed n d
                            Console.putText d.text
                Nothing -> do
                    let printDiag (i, d) = case verbosity of
                            Verbose -> do
                                Console.putTextLn $ diagnosticLineIndexed i d
                                Console.putText d.text
                            Concise ->
                                Console.putTextLn $ diagnosticLineIndexed i d
                    mapM_ printDiag (zip [1 ..] r.diagnostics)
                    Console.putTextLn $ buildSummary tz r
                    mapM_ (printTestRun verbosity) r.testRuns
                    when (buildHasErrors r || testsFailed r) exitFailure

    printTestRun verbosity tr = do
        Console.putTextLn $ case tr of
            TestRunning t Nothing -> t <> "  running..."
            TestRunning t (Just p) -> t <> "  running... (" <> show p.compiled <> "/" <> show p.total <> ")"
            TestRunErrored e -> e.target <> "  error: " <> e.message
            TestRunCompleted c -> c.target <> "  " <> completionSummary c
        when (verbosity == Verbose) $ case tr of
            TestRunCompleted c ->
                mapM_ (Console.putTextLn . ("  " <>)) (stripGhciNoise (T.lines c.output))
            _ -> pure ()

    buildHasErrors r = any ((== SError) . (.severity)) r.diagnostics
    testsFailed r = any isFailedRun r.testRuns
      where
        isFailedRun (TestRunCompleted c) = not c.passed
        isFailedRun (TestRunErrored _) = True
        isFailedRun (TestRunning _ _) = False

    buildSummary tz r =
        let errs = length $ filter ((== SError) . (.severity)) r.diagnostics
            warns = length $ filter ((== SWarning) . (.severity)) r.diagnostics
            ts = toText $ "— " <> formatTime defaultTimeLocale "%H:%M:%S" (utcToLocalTime tz r.completedAt)
            stats = toText $ "(" <> show r.moduleCount <> " modules, " <> formatDuration r.duration <> ")"
        in  if null r.diagnostics then
                "All good. " <> stats <> " " <> ts
            else
                show errs <> " error(s), " <> show warns <> " warning(s) " <> stats <> " " <> ts


completionSummary :: TestRunCompletion -> Text
completionSummary c = statusText <> maybe "" (\d -> " (" <> formatDuration d <> ")") c.duration
  where
    statusText
        | null c.testCases = if c.passed then "passed" else "failed"
        | otherwise =
            let total = length c.testCases
                failedCount = length $ filter isFailedCase c.testCases
            in  if failedCount == 0 then
                    "passed (" <> show total <> ")"
                else
                    show failedCount <> "/" <> show total <> " failed"
    isFailedCase (TestCase _ (TestCaseFailed _)) = True
    isFailedCase _ = False


showLog
    :: ( Console :> es
       , Delay :> es
       , FileSystem :> es
       )
    => FilePath -> FollowMode -> Eff es ()
showLog path followMode = do
    exists <- doesFileExist path
    if not exists then
        Console.putTextLn $ "Log file does not exist yet: " <> toText path
    else case followMode of
        Follow -> followFile path Console.putStr
        NoFollow -> readFileLbs path >>= Console.putStr . BSL.toStrict


showTests
    :: ( Console :> es
       , Exit :> es
       , File :> es
       , Reader SocketPath :> es
       , UnixSocket :> es
       )
    => TestOptions -> Eff es ()
showTests opts = do
    SocketPath sockPath <- ask
    result <-
        case opts.wait of
            WaitForBuild -> queryStatusWait sockPath
            ShowCurrent -> queryStatus sockPath
    case result of
        Left err -> Console.putTextLn $ "Error: " <> err
        Right state ->
            case state.phase of
                Building _ -> Console.putStrLn "Build in progress, no test results yet."
                Restarting -> Console.putStrLn "Daemon restarting, no test results yet."
                Testing r -> renderTestRuns r.testRuns
                Done r -> renderTestRuns r.testRuns
                BuildFailed msg -> reportBuildFailed msg
  where
    renderTestRuns [] = Console.putStrLn "No test results."
    renderTestRuns testRuns
        | null runs = do
            Console.putStrLn "All passed."
            mapM_ (Console.putTextLn . ("  " <>) . testRunTarget) testRuns
        | otherwise = do
            mapM_ printTestOutput runs
            when (any isFailed runs) exitFailure
      where
        runs =
            if opts.failedOnly then
                filter isFailed testRuns
            else
                testRuns

    isFailed (TestRunCompleted c) = not c.passed
    isFailed (TestRunErrored _) = True
    isFailed (TestRunning _ _) = False

    testRunTarget (TestRunning t _) = t
    testRunTarget (TestRunErrored e) = e.target
    testRunTarget (TestRunCompleted c) = c.target

    printTestOutput tr = case tr of
        TestRunning t Nothing ->
            Console.putTextLn $ t <> "  running..."
        TestRunning t (Just p) ->
            Console.putTextLn $ t <> "  running... (" <> show p.compiled <> "/" <> show p.total <> ")"
        TestRunErrored e ->
            Console.putTextLn $ e.target <> "  error: " <> e.message
        TestRunCompleted c -> do
            Console.putTextLn $ c.target <> "  " <> completionSummary c
            if opts.failedOnly then
                if null c.testCases then do
                    Console.putTextLn "  (unrecognised test runner format — showing full output)"
                    mapM_ (Console.putTextLn . ("  " <>)) (stripGhciNoise (lines c.output))
                else
                    mapM_ printFailedCase (filter isCaseFailed c.testCases)
            else
                mapM_ (Console.putTextLn . ("  " <>)) (stripGhciNoise (lines c.output))

    isCaseFailed (TestCase _ (TestCaseFailed _)) = True
    isCaseFailed _ = False

    printFailedCase tc = do
        Console.putTextLn $ "  " <> tc.description
        case tc.outcome of
            TestCaseFailed details ->
                mapM_ (Console.putTextLn . ("    " <>)) (T.lines details)
            TestCasePassed -> pure ()


showSource
    :: ( Console :> es
       , File :> es
       , Reader SocketPath :> es
       , UnixSocket :> es
       )
    => [SourceQuery]
    -> Eff es ()
showSource queries = do
    SocketPath sockPath <- ask
    result <- querySource sockPath queries
    case result of
        Left err -> Console.putTextLn $ "Error: " <> err
        Right results -> renderSourceResults results