This commit is contained in:
Tom Sydney Kerckhove 2023-10-12 17:10:39 +02:00
parent f88cc99b19
commit ee5b44fda0
12 changed files with 487 additions and 228 deletions

View File

@ -1,5 +1,6 @@
{ mkDerivation, base, bytestring, feedback, lib, path, path-io
, sydtest, sydtest-discover, typed-process, unix
{ mkDerivation, autodocodec-yaml, base, bytestring, containers
, feedback, filelock, lib, path, path-io, process, sydtest
, sydtest-discover, typed-process, unix, unliftio
}:
mkDerivation {
pname = "feedback-test-harness";
@ -7,7 +8,8 @@ mkDerivation {
src = ./.;
libraryHaskellDepends = [ base feedback path path-io ];
testHaskellDepends = [
base bytestring path path-io sydtest typed-process unix
autodocodec-yaml base bytestring containers feedback filelock path
path-io process sydtest typed-process unix unliftio
];
testToolDepends = [ sydtest-discover ];
doHaddock = false;

View File

@ -47,12 +47,18 @@ test-suite feedback-test
build-tool-depends:
sydtest-discover:sydtest-discover
build-depends:
base >=4.7 && <5
autodocodec-yaml
, base >=4.7 && <5
, bytestring
, containers
, feedback
, feedback-test-harness
, filelock
, path
, path-io
, process
, sydtest
, typed-process
, unix
, unliftio
default-language: Haskell2010

View File

@ -28,10 +28,16 @@ tests:
- -with-rtsopts=-N
- -Wall
dependencies:
- autodocodec-yaml
- bytestring
- containers
- feedback
- feedback-test-harness
- filelock
- path
- path-io
- process
- sydtest
- typed-process
- unix
- unliftio

View File

@ -3,104 +3,169 @@
module FeedbackSpec (spec) where
import Autodocodec.Yaml as Yaml
import Control.Concurrent
import qualified Data.ByteString as SB
import qualified Data.Map as M
import Feedback.Common.OptParse
import Path
import Path.IO
import System.IO
import System.Posix (fdToHandle, openPseudoTerminal)
import System.IO (hPutChar)
import System.Posix (fdToHandle, openPseudoTerminal, sigKILL, signalProcess)
import System.Process (getPid)
import System.Process.Typed
import Test.Syd
import Test.Syd.Path
import UnliftIO
import UnliftIO.IO.File
spec :: Spec
spec = sequential . tempDirSpec "feedback" . coverageSpec $ do
let waitABit = threadDelay 250_000 -- Wait 250 ms
it "can show help text" $ \tdir -> do
let cp =
setStdout nullStream
. setWorkingDir (fromAbsDir tdir)
$ proc "feedback" ["--help"]
runProcess_ cp :: IO ()
it "can start a loop and wait for input" $ \tdir -> do
let cp =
setStdout nullStream
. setWorkingDir (fromAbsDir tdir)
$ proc "feedback" ["echo", "hi"]
withProcessTerm cp $ \ph -> do
waitABit
-- If the program is still running after 100ms, we assume that it is waiting.
mExitCode <- getExitCode ph
mExitCode `shouldBe` Nothing
it "can run a command once, wait for input" $ \tdir ->
withSystemTempDir "somewhere-else" $ \otherTDir -> do
resultFile <- resolveFile otherTDir "result" -- In another dir so the loop doesn't rerun automatically
spec =
-- We need sequential because commands 'feedback' is run and produces a coverage.dat in the tempdir
sequential . doNotRandomiseExecutionOrder . coverageSpec $ do
it "can show help text" $ \tdir -> do
let cp =
setStdout nullStream
. setWorkingDir (fromAbsDir tdir)
$ proc "feedback" ["--", "bash", "-c", "echo hi >" <> fromAbsFile resultFile]
withProcessTerm cp $ \ph -> do
$ proc "feedback" ["--help"]
runProcess_ cp :: IO ()
it "can start a loop and wait for input" $ \tdir -> do
let cp =
setStdout nullStream
. setWorkingDir (fromAbsDir tdir)
$ proc "feedback" ["echo", "hi"]
withProcessKill cp $ \ph -> do
waitABit
result <- SB.readFile (fromAbsFile resultFile)
result `shouldBe` "hi\n"
-- If the program is still running after 100ms, we assume that it is waiting.
mExitCode <- getExitCode ph
mExitCode `shouldBe` Nothing
it "can run a command once, wait for manual input, and run it again upon input, and then still be running" $ \tdir ->
withSystemTempDir "somewhere-else" $ \otherTDir -> do
-- In another dir so the loop doesn't rerun automatically
dateFile <- resolveFile otherTDir "datefile"
it "can run a command once, wait for input" $ \tdir ->
withSystemTempDir "somewhere-else" $ \otherTDir -> do
resultFile <- resolveFile otherTDir "result.txt" -- In another dir so the loop doesn't rerun automatically
let cp =
setStdout nullStream
. setWorkingDir (fromAbsDir tdir)
$ proc "feedback" ["--", "bash", "-c", "echo hi >" <> fromAbsFile resultFile]
withProcessKill cp $ \ph -> do
waitABit
(masterFd, slaveFd) <- openPseudoTerminal
masterHandle <- fdToHandle masterFd
slaveHandle <- fdToHandle slaveFd
result <- SB.readFile (fromAbsFile resultFile)
result `shouldBe` "hi\n"
let cp =
setStdout nullStream
. setStdin (useHandleOpen slaveHandle)
. setWorkingDir (fromAbsDir tdir)
$ proc "feedback" ["--", "bash", "-c", "date +%N >" <> fromAbsFile dateFile]
withProcessTerm cp $ \ph -> do
waitABit
-- If the program is still running after 100ms, we assume that it is waiting.
mExitCode <- getExitCode ph
mExitCode `shouldBe` Nothing
-- Feedback is running.
mExitCode <- getExitCode ph
mExitCode `shouldBe` Nothing
it "can run a command once, wait for manual input, and run it again upon input, and then still be running" $ \tdir ->
withSystemTempDir "somewhere-else" $ \otherTDir -> do
-- In another dir so the loop doesn't rerun automatically
dateFile <- resolveFile otherTDir "datefile"
beforeContents <- SB.readFile (fromAbsFile dateFile)
(masterFd, slaveFd) <- openPseudoTerminal
masterHandle <- fdToHandle masterFd
slaveHandle <- fdToHandle slaveFd
hPutChar masterHandle 'r'
hFlush masterHandle
let cp =
setStdout nullStream
. setStdin (useHandleOpen slaveHandle)
. setWorkingDir (fromAbsDir tdir)
$ proc "feedback" ["--", "bash", "-c", "date +%N >" <> fromAbsFile dateFile]
withProcessKill cp $ \ph -> do
waitABit
waitABit
-- Feedback is running.
mExitCode <- getExitCode ph
mExitCode `shouldBe` Nothing
afterContents <- SB.readFile (fromAbsFile dateFile)
afterContents `shouldNotBe` beforeContents
beforeContents <- SB.readFile (fromAbsFile dateFile)
it "can run a command once, wait for a file to change, and run it again upon input, and then still be running" $ \tdir ->
withSystemTempDir "somewhere-else" $ \otherTDir -> do
-- In another dir so the loop doesn't rerun automatically
dateFile <- resolveFile otherTDir "datefile"
hPutChar masterHandle 'r'
hFlush masterHandle
-- In the same dir so the loop reruns when we change it
triggerFile <- resolveFile tdir "trigger"
SB.writeFile (fromAbsFile triggerFile) "initial"
waitABit
afterContents <- SB.readFile (fromAbsFile dateFile)
afterContents `shouldNotBe` beforeContents
it "can run a command once, wait for a file to change, and run it again upon input, and then still be running" $ \tdir ->
withSystemTempDir "somewhere-else" $ \otherTDir -> do
-- In another dir so the loop doesn't rerun automatically
dateFile <- resolveFile otherTDir "datefile"
-- In the same dir so the loop reruns when we change it
triggerFile <- resolveFile tdir "trigger"
SB.writeFile (fromAbsFile triggerFile) "initial"
(_, slaveFd) <- openPseudoTerminal
-- masterHandle <- fdToHandle masterFd
slaveHandle <- fdToHandle slaveFd
let cp =
setStdout nullStream
. setStdin (useHandleOpen slaveHandle)
. setWorkingDir (fromAbsDir tdir)
$ proc "feedback" ["--no-clear", "--debug", "--", "bash", "-c", "date +%N >" <> fromAbsFile dateFile]
withProcessKill cp $ \ph -> do
waitABit
-- Feedback is running.
mExitCode <- getExitCode ph
mExitCode `shouldBe` Nothing
-- Make sure the file exists now
beforeContents <- SB.readFile (fromAbsFile dateFile)
-- Change the trigger file, this should cause the loop to rerun.
SB.writeFile (fromAbsFile triggerFile) "go go go"
-- Make sure the loop is rerun
waitABit
-- Make sure the loop is rerun
afterContents <- SB.readFile (fromAbsFile dateFile)
afterContents `shouldNotBe` beforeContents
it "can run in a git repository and ignore .gitignored files" $ \tdir -> do
-- Set up the repository
let runGit args =
runProcess_
$ setEnv []
. setStdout nullStream
. setStderr nullStream
. setWorkingDir (fromAbsDir tdir)
$ proc "git" args
runGit ["init"]
runGit ["config", "user.email", "you@example.com"]
runGit ["config", "user.name", "Your Name"]
notIgnoredFile <- resolveFile tdir "file.not-ignored"
ignoredFile <- resolveFile tdir "file.ignored"
gitignoreFile <- resolveFile tdir ".gitignore"
SB.writeFile (fromAbsFile gitignoreFile) "*.ignored"
runGit ["add", "."]
runGit ["commit", "-m", "Initial commit"]
SB.writeFile (fromAbsFile notIgnoredFile) "foo"
SB.writeFile (fromAbsFile ignoredFile) "bar"
runGit ["add", "."]
runGit ["commit", "-m", "commit with files."]
(_, slaveFd) <- openPseudoTerminal
-- masterHandle <- fdToHandle masterFd
slaveHandle <- fdToHandle slaveFd
dateFile <- resolveFile tdir "datefile.ignored"
let cp =
setStdout nullStream
. setStdin (useHandleOpen slaveHandle)
. setWorkingDir (fromAbsDir tdir)
$ proc "feedback" ["--no-clear", "--debug", "--", "bash", "-c", "date +%N >" <> fromAbsFile dateFile]
withProcessTerm cp $ \ph -> do
withProcessKill cp $ \ph -> do
waitABit
-- Feedback is running.
@ -110,8 +175,18 @@ spec = sequential . tempDirSpec "feedback" . coverageSpec $ do
-- Make sure the file exists now
beforeContents <- SB.readFile (fromAbsFile dateFile)
-- Change the trigger file, this should cause the loop to rerun.
SB.writeFile (fromAbsFile triggerFile) "go go go"
-- Change the ignored file file, this should _not_ cause the loop to rerun.
SB.writeFile (fromAbsFile ignoredFile) "go go go"
-- Make sure the loop is rerun
waitABit
-- Make sure the loop is rerun
middleContents <- SB.readFile (fromAbsFile dateFile)
middleContents `shouldBe` beforeContents
-- Change the not-ignored file file, this should cause the loop to rerun.
SB.writeFile (fromAbsFile notIgnoredFile) "go go go"
-- Make sure the loop is rerun
waitABit
@ -120,69 +195,92 @@ spec = sequential . tempDirSpec "feedback" . coverageSpec $ do
afterContents <- SB.readFile (fromAbsFile dateFile)
afterContents `shouldNotBe` beforeContents
it "can run in a git repository and ignore .gitignored files" $ \tdir -> do
-- Set up the repository
let runGit args =
runProcess_ $ setEnv [] $ setWorkingDir (fromAbsDir tdir) $ proc "git" args
xdescribe "fails for unknown reason inside nix build" $
it "can run a command, the 'before-all' hook will have run before" $ \tdir ->
withSystemTempDir "somewhere-else" $ \otherTDir -> do
resultFile <- resolveFile otherTDir "result.txt" -- In another dir so the loop doesn't rerun automatically
configFile <- resolveFile tdir "config.yaml"
writeBinaryFileDurableAtomic (fromAbsFile configFile) $
encodeYamlViaCodec $
emptyConfiguration
{ configLoops =
M.singleton "sleep" $
(makeLoopConfiguration (CommandScript "sleep 5"))
{ loopConfigHooksConfiguration =
emptyHooksConfiguration
{ hooksConfigurationBeforeAll =
Just $
makeRunConfiguration
(CommandScript ("echo hi > " <> fromAbsFile resultFile))
}
}
}
let cp =
setStdout nullStream
. setWorkingDir (fromAbsDir tdir)
$ proc
"feedback"
[ "--config-file",
fromAbsFile configFile,
"sleep"
]
withProcessKill cp $ \ph -> do
waitABit
runGit ["init"]
runGit ["config", "user.email", "you@example.com"]
runGit ["config", "user.name", "Your Name"]
result <- SB.readFile (fromAbsFile resultFile)
result `shouldBe` "hi\n"
notIgnoredFile <- resolveFile tdir "file.not-ignored"
ignoredFile <- resolveFile tdir "file.ignored"
-- If the program is still running after 100ms, we assume that it is still sleeping.
mExitCode <- getExitCode ph
mExitCode `shouldBe` Nothing
gitignoreFile <- resolveFile tdir ".gitignore"
SB.writeFile (fromAbsFile gitignoreFile) "*.ignored"
xdescribe "fails for unknown reason inside nix build" $
it "can run a command once, wait for input. The after-first hook will have run after" $ \tdir ->
withSystemTempDir "somewhere-else" $ \otherTDir -> do
resultFile <- resolveFile otherTDir "result.txt" -- In another dir so the loop doesn't rerun automatically
configFile <- resolveFile tdir "config.yaml"
writeBinaryFileDurableAtomic (fromAbsFile configFile) $
encodeYamlViaCodec $
emptyConfiguration
{ configLoops =
M.singleton "say" $
(makeLoopConfiguration (CommandScript "echo run"))
{ loopConfigHooksConfiguration =
emptyHooksConfiguration
{ hooksConfigurationAfterFirst =
Just $
makeRunConfiguration
(CommandScript ("echo hi > " <> fromAbsFile resultFile))
}
}
}
let cp =
setStdout nullStream
. setWorkingDir (fromAbsDir tdir)
$ proc
"feedback"
[ "--config-file",
fromAbsFile configFile,
"say"
]
withProcessKill cp $ \ph -> do
waitABit
runGit ["add", "."]
runGit ["commit", "-m", "Initial commit"]
result <- SB.readFile (fromAbsFile resultFile)
result `shouldBe` "hi\n"
SB.writeFile (fromAbsFile notIgnoredFile) "foo"
SB.writeFile (fromAbsFile ignoredFile) "bar"
-- If the program is still running after 100ms, we assume that it is waiting.
mExitCode <- getExitCode ph
mExitCode `shouldBe` Nothing
runGit ["add", "."]
runGit ["commit", "-m", "commit with files."]
withProcessKill :: ProcessConfig stdin stderr stdout -> (Process stdin stderr stdout -> IO a) -> IO a
withProcessKill cp func = withProcessWait cp $ \ph ->
func ph `finally` killProcessHandle ph
(_, slaveFd) <- openPseudoTerminal
-- masterHandle <- fdToHandle masterFd
slaveHandle <- fdToHandle slaveFd
dateFile <- resolveFile tdir "datefile.ignored"
let cp =
setStdout nullStream
. setStdin (useHandleOpen slaveHandle)
. setWorkingDir (fromAbsDir tdir)
$ proc "feedback" ["--no-clear", "--debug", "--", "bash", "-c", "date +%N >" <> fromAbsFile dateFile]
withProcessTerm cp $ \ph -> do
waitABit
-- Feedback is running.
mExitCode <- getExitCode ph
mExitCode `shouldBe` Nothing
-- Make sure the file exists now
beforeContents <- SB.readFile (fromAbsFile dateFile)
-- Change the ignored file file, this should _not_ cause the loop to rerun.
SB.writeFile (fromAbsFile ignoredFile) "go go go"
-- Make sure the loop is rerun
waitABit
-- Make sure the loop is rerun
middleContents <- SB.readFile (fromAbsFile dateFile)
middleContents `shouldBe` beforeContents
-- Change the not-ignored file file, this should cause the loop to rerun.
SB.writeFile (fromAbsFile notIgnoredFile) "go go go"
-- Make sure the loop is rerun
waitABit
-- Make sure the loop is rerun
afterContents <- SB.readFile (fromAbsFile dateFile)
afterContents `shouldNotBe` beforeContents
killProcessHandle :: Process stdin stdout stderr -> IO ()
killProcessHandle ph = do
mPid <- getPid (unsafeProcessHandle ph)
mapM_ (signalProcess sigKILL) mPid
-- It's really annoying that this is necessary, but hear me out.
-- `feedback` is run in a tempdir.
@ -191,14 +289,20 @@ spec = sequential . tempDirSpec "feedback" . coverageSpec $ do
-- We have to copy back the coverage info in order to make the coverage report.
--
-- It's slow and dirty but it works.
coverageSpec :: SpecWith (Path Abs Dir) -> SpecWith (Path Abs Dir)
coverageSpec = after $ \tdir -> do
specificCoverageFile <- resolveFile tdir "coverage.dat"
coverageSpec :: SpecWith (Path Abs Dir) -> Spec
coverageSpec = around $ \useTmpdir -> do
topLevelCoverageFile <- resolveFile' "coverage.dat"
mSpecificCoverage <- forgivingAbsence $ SB.readFile (fromAbsFile specificCoverageFile)
mSpecificCoverage <- withSystemTempDir "feedback-test-harness" $ \tdir -> do
specificCoverageFile <- resolveFile tdir "coverage.dat"
useTmpdir tdir
forgivingAbsence $ SB.readFile (fromAbsFile specificCoverageFile)
mTopLevelCoverage <- forgivingAbsence $ SB.readFile (fromAbsFile topLevelCoverageFile)
SB.writeFile (fromAbsFile topLevelCoverageFile) $ case (mTopLevelCoverage, mSpecificCoverage) of
(Nothing, Nothing) -> mempty
(Just topLevelCoverage, Nothing) -> topLevelCoverage
(Nothing, Just specificCoverage) -> specificCoverage
(Just topLevelCoverage, Just specificCoverage) -> topLevelCoverage <> specificCoverage
writeBinaryFileDurableAtomic (fromAbsFile topLevelCoverageFile) $
case (mTopLevelCoverage, mSpecificCoverage) of
(Nothing, Nothing) -> mempty
(Just topLevelCoverage, Nothing) -> topLevelCoverage
(Nothing, Just specificCoverage) -> specificCoverage
(Just topLevelCoverage, Just specificCoverage) -> topLevelCoverage <> specificCoverage
waitABit :: IO ()
waitABit = threadDelay 250_000 -- Wait 250 ms

8
feedback/CHANGELOG.md Normal file
View File

@ -0,0 +1,8 @@
# Changelog
## [0.1.0.2] - 2023-10-12
### Added
* Hooks: `before-all` and `after-first`
* Short command-line options: `c` for `--config-file` and `d` for `--debug`.

View File

@ -7,7 +7,7 @@
}:
mkDerivation {
pname = "feedback";
version = "0.1.0.1";
version = "0.1.0.2";
src = ./.;
isLibrary = true;
isExecutable = true;

View File

@ -5,7 +5,7 @@ cabal-version: 2.2
-- see: https://github.com/sol/hpack
name: feedback
version: 0.1.0.1
version: 0.1.0.2
synopsis: Declarative feedback loop manager
homepage: https://github.com/NorfairKing/feedback#readme
bug-reports: https://github.com/NorfairKing/feedback/issues

View File

@ -1,5 +1,5 @@
name: feedback
version: 0.1.0.1
version: 0.1.0.2
github: "NorfairKing/feedback"
license: GPL-3.0-only
author: "Tom Sydney Kerckhove"

View File

@ -28,7 +28,8 @@ import Paths_feedback
data LoopSettings = LoopSettings
{ loopSettingRunSettings :: !RunSettings,
loopSettingFilterSettings :: !FilterSettings,
loopSettingOutputSettings :: !OutputSettings
loopSettingOutputSettings :: !OutputSettings,
loopSettingHooksSettings :: !HooksSettings
}
deriving (Show, Eq, Generic)
@ -39,6 +40,7 @@ combineToLoopSettings Flags {..} Environment {} mDefaultOutputConfig LoopConfigu
let outputConfig = maybe loopConfigOutputConfiguration (<> loopConfigOutputConfiguration) mDefaultOutputConfig
let loopSettingOutputSettings = combineToOutputSettings flagOutputFlags outputConfig
loopSettingHooksSettings <- combineToHooksSettings loopConfigHooksConfiguration
pure LoopSettings {..}
data RunSettings = RunSettings
@ -55,11 +57,6 @@ combineToRunSettings RunConfiguration {..} = do
runSettingWorkingDir <- mapM resolveDir' runConfigWorkingDir
pure RunSettings {..}
data OutputSettings = OutputSettings
{ outputSettingClear :: !Clear
}
deriving (Show, Eq, Generic)
data FilterSettings = FilterSettings
{ filterSettingGitignore :: !Bool,
filterSettingFind :: !(Maybe String)
@ -72,6 +69,11 @@ combineToFilterSettings FilterConfiguration {..} =
filterSettingFind = filterConfigFind
in FilterSettings {..}
data OutputSettings = OutputSettings
{ outputSettingClear :: !Clear
}
deriving (Show, Eq, Generic)
combineToOutputSettings :: OutputFlags -> OutputConfiguration -> OutputSettings
combineToOutputSettings OutputFlags {..} mConf =
let outputSettingClear =
@ -79,6 +81,18 @@ combineToOutputSettings OutputFlags {..} mConf =
outputFlagClear <|> outputConfigClear mConf
in OutputSettings {..}
data HooksSettings = HooksSettings
{ hooksSettingBeforeAll :: Maybe RunSettings,
hooksSettingAfterFirst :: Maybe RunSettings
}
deriving (Show, Eq, Generic)
combineToHooksSettings :: HooksConfiguration -> IO HooksSettings
combineToHooksSettings HooksConfiguration {..} = do
hooksSettingBeforeAll <- mapM combineToRunSettings hooksConfigurationBeforeAll
hooksSettingAfterFirst <- mapM combineToRunSettings hooksConfigurationAfterFirst
pure HooksSettings {..}
data Configuration = Configuration
{ configLoops :: !(Map String LoopConfiguration),
configOutputConfiguration :: !(Maybe OutputConfiguration)
@ -90,14 +104,24 @@ instance HasCodec Configuration where
codec =
object "Configuration" $
Configuration
<$> optionalFieldWithOmittedDefault' "loops" M.empty .= configLoops
<*> optionalField "output" "default output configuration" .= configOutputConfiguration
<$> optionalFieldWithOmittedDefault' "loops" M.empty
.= configLoops
<*> optionalField "output" "default output configuration"
.= configOutputConfiguration
emptyConfiguration :: Configuration
emptyConfiguration =
Configuration
{ configLoops = mempty,
configOutputConfiguration = mempty
}
data LoopConfiguration = LoopConfiguration
{ loopConfigDescription :: !(Maybe String),
loopConfigRunConfiguration :: !RunConfiguration,
loopConfigFilterConfiguration :: !FilterConfiguration,
loopConfigOutputConfiguration :: !OutputConfiguration
loopConfigOutputConfiguration :: !OutputConfiguration,
loopConfigHooksConfiguration :: !HooksConfiguration
}
deriving stock (Show, Eq, Generic)
deriving (FromJSON, ToJSON) via (Autodocodec LoopConfiguration)
@ -115,10 +139,11 @@ instance HasCodec LoopConfiguration where
loopConfigDocs =
[ "A LoopConfiguration specifies an entire feedback loop.",
"",
"It consists of three parts:",
"It consists of four parts:",
"* Filter Configuration: Which files to watch",
"* Run Configuration: What to do when those files change",
"* Output Configuration: What to see"
"* Output Configuration: What to see",
"* Hooks configuration: What to around commands"
]
f = \case
Left s -> makeLoopConfiguration (CommandArgs s)
@ -133,7 +158,8 @@ instance HasCodec LoopConfiguration where
loopConfigurationObjectCodec :: JSONObjectCodec LoopConfiguration
loopConfigurationObjectCodec =
LoopConfiguration
<$> optionalField "description" "description of when to use this feedback loop" .= loopConfigDescription
<$> optionalField "description" "description of when to use this feedback loop"
.= loopConfigDescription
<*> parseAlternative
(requiredField "run" "run configuration for this loop")
runConfigurationObjectCodec
@ -146,6 +172,10 @@ loopConfigurationObjectCodec =
(requiredField "output" "output configuration for this loop")
outputConfigurationObjectCodec
.= loopConfigOutputConfiguration
<*> parseAlternative
(requiredField "hooks" "hooks configuration for this loop")
hooksConfigurationObjectCodec
.= loopConfigHooksConfiguration
makeLoopConfiguration :: Command -> LoopConfiguration
makeLoopConfiguration c =
@ -153,7 +183,8 @@ makeLoopConfiguration c =
{ loopConfigDescription = Nothing,
loopConfigRunConfiguration = makeRunConfiguration c,
loopConfigFilterConfiguration = emptyFilterConfiguration,
loopConfigOutputConfiguration = emptyOutputConfiguration
loopConfigOutputConfiguration = emptyOutputConfiguration,
loopConfigHooksConfiguration = emptyHooksConfiguration
}
data RunConfiguration = RunConfiguration
@ -167,14 +198,29 @@ data RunConfiguration = RunConfiguration
instance HasCodec RunConfiguration where
codec =
named "RunConfiguration" $
object "RunConfiguration" runConfigurationObjectCodec
dimapCodec f g $
eitherCodec
(codec <?> "A bare command without any extra configuration")
(object "RunConfiguration" runConfigurationObjectCodec)
where
f = \case
Left s -> makeRunConfiguration (CommandArgs s)
Right loopConfig -> loopConfig
g runConfig =
let c = runConfigCommand runConfig
in case c of
CommandArgs cmd | runConfig == makeRunConfiguration c -> Left cmd
_ -> Right runConfig
runConfigurationObjectCodec :: JSONObjectCodec RunConfiguration
runConfigurationObjectCodec =
RunConfiguration
<$> commandObjectCodec .= runConfigCommand
<*> optionalFieldWithOmittedDefault "env" M.empty "extra environment variables to set" .= runConfigExtraEnv
<*> optionalField "working-dir" "where the process will be run" .= runConfigWorkingDir
<$> commandObjectCodec
.= runConfigCommand
<*> optionalFieldWithOmittedDefault "env" M.empty "extra environment variables to set"
.= runConfigExtraEnv
<*> optionalField "working-dir" "where the process will be run"
.= runConfigWorkingDir
makeRunConfiguration :: Command -> RunConfiguration
makeRunConfiguration c =
@ -208,8 +254,10 @@ instance HasCodec FilterConfiguration where
filterConfigurationObjectCodec :: JSONObjectCodec FilterConfiguration
filterConfigurationObjectCodec =
FilterConfiguration
<$> optionalField "git" "whether to ignore files that are not in the git repo\nConcretely, this uses `git ls-files` to find files that are in the repo, so files that have been added but are also ignored by .gitignore will still be watched." .= filterConfigGitignore
<*> optionalField "find" "arguments for the 'find' command to find files to be notified about" .= filterConfigFind
<$> optionalField "git" "whether to ignore files that are not in the git repo\nConcretely, this uses `git ls-files` to find files that are in the repo, so files that have been added but are also ignored by .gitignore will still be watched."
.= filterConfigGitignore
<*> optionalField "find" "arguments for the 'find' command to find files to be notified about"
.= filterConfigFind
emptyFilterConfiguration :: FilterConfiguration
emptyFilterConfiguration =
@ -232,7 +280,8 @@ instance HasCodec OutputConfiguration where
outputConfigurationObjectCodec :: JSONObjectCodec OutputConfiguration
outputConfigurationObjectCodec =
OutputConfiguration
<$> optionalField "clear" "whether to clear the screen runs" .= outputConfigClear
<$> optionalField "clear" "whether to clear the screen runs"
.= outputConfigClear
instance Semigroup OutputConfiguration where
(<>) oc1 oc2 =
@ -246,6 +295,32 @@ emptyOutputConfiguration =
{ outputConfigClear = Nothing
}
data HooksConfiguration = HooksConfiguration
{ hooksConfigurationBeforeAll :: !(Maybe RunConfiguration),
hooksConfigurationAfterFirst :: !(Maybe RunConfiguration)
}
deriving (Show, Eq, Generic)
instance HasCodec HooksConfiguration where
codec =
named "HooksConfiguration" $
object "HooksConfiguration" hooksConfigurationObjectCodec
hooksConfigurationObjectCodec :: JSONObjectCodec HooksConfiguration
hooksConfigurationObjectCodec =
HooksConfiguration
<$> optionalField "before-all" "The hook to run before the first run"
.= hooksConfigurationBeforeAll
<*> optionalField "after-first" "The hook to run after the first run"
.= hooksConfigurationAfterFirst
emptyHooksConfiguration :: HooksConfiguration
emptyHooksConfiguration =
HooksConfiguration
{ hooksConfigurationBeforeAll = Nothing,
hooksConfigurationAfterFirst = Nothing
}
getConfiguration :: Flags -> Environment -> IO (Maybe Configuration)
getConfiguration Flags {..} Environment {..} = do
fp <- case flagConfigFile <|> envConfigFile of
@ -326,7 +401,8 @@ parseFlags =
<*> optional
( strOption
( mconcat
[ long "config-file",
[ short 'c',
long "config-file",
help "Path to an altenative config file",
metavar "FILEPATH"
]
@ -363,7 +439,13 @@ parseOutputFlags :: OptParse.Parser OutputFlags
parseOutputFlags =
OutputFlags
<$> parseClearFlag
<*> switch (mconcat [long "debug", help "show debug information"])
<*> switch
( mconcat
[ short 'd',
long "debug",
help "show debug information"
]
)
data Command
= CommandArgs !String

View File

@ -46,7 +46,9 @@ makeProcessConfigFor RunSettings {..} = do
CommandScript s -> do
-- Write the script to a file
systemTempDir <- getTempDir
scriptFile <- resolveFile systemTempDir "feedback-script.sh"
ensureDir systemTempDir
tempDir <- createTempDir systemTempDir "feedback"
scriptFile <- resolveFile tempDir "feedback-script.sh"
writeBinaryFileDurableAtomic (fromAbsFile scriptFile) (TE.encodeUtf8 (T.pack s))
-- Make the script executable
oldPermissions <- getPermissions scriptFile

View File

@ -55,6 +55,10 @@ runFeedbackLoop = do
-- being killed by the user.
mainThreadId <- myThreadId
-- Make sure the user knows what's happening.
firstBegin <- getZonedTime
putTimedChunks terminalCapabilities firstBegin [indicatorChunk "preparing for first run"]
-- Get the flags and the environment up front, because they don't change
-- anyway.
-- This is also important because autocompletion won't work if we output
@ -62,16 +66,19 @@ runFeedbackLoop = do
flags <- getFlags
env <- getEnvironment
let doSingleLoop loopBegin = do
-- We show a 'preparing' chunk before we get the settings because sometimes
-- getting the settings can take a while, for example in big repositories.
putTimedChunks terminalCapabilities loopBegin [indicatorChunk "preparing"]
-- Get the initial configuration so that we know if we need to run a hook after the first run.
mInitialConfiguration <- getConfiguration flags env
initialSettings <- combineToSettings flags env mInitialConfiguration
-- Get the loop configuration within the loop, so that the loop
-- configuration can be what is being worked on.
mConfiguration <- getConfiguration flags env
loopSettings <- combineToSettings flags env mConfiguration
-- If a before-all hook is defined, run it now.
forM_ (hooksSettingBeforeAll (loopSettingHooksSettings initialSettings)) $ \beforeAllRunSettings -> do
putTimedChunks terminalCapabilities firstBegin [indicatorChunk "starting before-all hook"]
runHook terminalCapabilities firstBegin beforeAllRunSettings
-- Define how to run a single loop with given settings in a let binding so we
-- don't have to pass in args like 'here', 'stdinFilter' and
-- 'terminalCapabilities'.
let doSingleLoopWithSettings firstLoop loopBegin loopSettings = do
FS.withManagerConf FS.defaultConfig $ \watchManager -> do
-- Set up watchers for each relevant directory and send the FSNotify
-- events down this event channel.
@ -87,9 +94,26 @@ runFeedbackLoop = do
eventChan
-- Start the process and put output.
worker mainThreadId loopSettings terminalCapabilities loopBegin eventChan
worker mainThreadId firstLoop loopSettings terminalCapabilities loopBegin eventChan
`finally` stopListeningAction
-- Do the first loop outside the 'forever' so we can run commands before and
-- after the first loop.
doSingleLoopWithSettings True firstBegin initialSettings
`finally` putDone terminalCapabilities firstBegin
let doSingleLoop loopBegin = do
-- We show a 'preparing' chunk before we get the settings because sometimes
-- getting the settings can take a while, for example in big repositories.
putTimedChunks terminalCapabilities loopBegin [indicatorChunk "preparing"]
-- Get the loop configuration within the loop, so that the loop
-- configuration can be what is being worked on.
mConfiguration <- getConfiguration flags env
loopSettings <- combineToSettings flags env mConfiguration
doSingleLoopWithSettings False loopBegin loopSettings
let singleIteration = do
-- Record when the loop began so we can show relative times nicely.
loopBegin <- getZonedTime
@ -97,6 +121,13 @@ runFeedbackLoop = do
forever singleIteration
runHook :: TerminalCapabilities -> ZonedTime -> RunSettings -> IO ()
runHook terminalCapabilities begin runSettings = do
mapM_ (putTimedChunks terminalCapabilities begin) (startingLines runSettings)
_ <- startProcessHandle runSettings
-- We want this process to keep running, so we don't wait for it.
pure ()
startWatching ::
Path Abs Dir ->
Filter ->
@ -142,66 +173,89 @@ data RestartEvent
| StdinEvent !Char
deriving (Show, Eq)
worker :: ThreadId -> LoopSettings -> TerminalCapabilities -> ZonedTime -> Chan FS.Event -> IO ()
worker mainThreadId LoopSettings {..} terminalCapabilities loopBegin eventChan = do
let sendOutput :: Output -> IO ()
sendOutput = putOutput loopSettingOutputSettings terminalCapabilities loopBegin
worker ::
ThreadId ->
Bool ->
LoopSettings ->
TerminalCapabilities ->
ZonedTime ->
Chan FS.Event ->
IO ()
worker
mainThreadId
thisIsTheFirstLoop
LoopSettings {..}
terminalCapabilities
loopBegin
eventChan = do
let sendOutput :: Output -> IO ()
sendOutput = putOutput loopSettingOutputSettings terminalCapabilities loopBegin
-- Record starting time of the process.
-- This is different from 'loopBegin' because preparing the watchers may take
-- a nontrivial amount of time.
start <- getMonotonicTimeNSec
-- Record starting time of the process.
-- This is different from 'loopBegin' because preparing the watchers may take
-- a nontrivial amount of time.
start <- getMonotonicTimeNSec
-- Start the process process
processHandle <- startProcessHandle loopSettingRunSettings
sendOutput $ OutputProcessStarted loopSettingRunSettings
-- Start the process process
processHandle <- startProcessHandle loopSettingRunSettings
sendOutput $ OutputProcessStarted loopSettingRunSettings
-- Perform GC after the process has started, because that's when we're
-- waiting anyway, so that we don't need idle gc.
performGC
-- Perform GC after the process has started, because that's when we're
-- waiting anyway, so that we don't need idle gc.
performGC
-- Make sure we kill the process and wait for it to exit if a user presses
-- C-c.
installKillHandler mainThreadId processHandle
-- Make sure we kill the process and wait for it to exit if a user presses
-- C-c.
installKillHandler mainThreadId processHandle
-- From here on we will wait for either:
-- 1. A change to a file that we are watching, or
-- 2. The process to finish.
let runAfterFirstHookIfNecessary = do
-- If this is the first run AND a after-first hook is defined, run it now.
when thisIsTheFirstLoop $
forM_ (hooksSettingAfterFirst loopSettingHooksSettings) $ \afterFirstRunSettings -> do
putTimedChunks terminalCapabilities loopBegin [indicatorChunk "starting after-first hook"]
runHook terminalCapabilities loopBegin afterFirstRunSettings
-- From here on we will wait for either:
-- 1. A change to a file that we are watching, or
-- 2. The process to finish.
-- 1. If An event happened first, output it and kill the process.
let handleEventHappened event = do
-- Output the event that has fired
sendOutput $ OutputEvent event
-- Output that killing will start
sendOutput OutputKilling
-- Kill the process
stopProcessHandle processHandle
-- Output that the process has been killed
sendOutput OutputKilled
-- Wait for the process to finish (should have by now)
ec <- waitProcessHandle processHandle
-- Record the end time
end <- getMonotonicTimeNSec
-- Output that the process has finished
sendOutput $ OutputProcessExited ec (end - start)
-- 1. If An event happened first, output it and kill the process.
let handleEventHappened event = do
-- Output the event that has fired
sendOutput $ OutputEvent event
-- Output that killing will start
sendOutput OutputKilling
-- Kill the process
stopProcessHandle processHandle
-- Output that the process has been killed
sendOutput OutputKilled
-- Wait for the process to finish (should have by now)
ec <- waitProcessHandle processHandle
-- Record the end time
end <- getMonotonicTimeNSec
-- Output that the process has finished
sendOutput $ OutputProcessExited ec (end - start)
-- Process is done, run the after-first hook if necessary
runAfterFirstHookIfNecessary
-- 2. If the process finished first, show the result and wait for an event anyway
let handleProcessDone ec = do
end <- getMonotonicTimeNSec
sendOutput $ OutputProcessExited ec (end - start)
-- Output the event that made the rerun happen
event <- waitForEvent eventChan
sendOutput $ OutputEvent event
-- 2. If the process finished first, show the result and wait for an event anyway
let handleProcessDone ec = do
end <- getMonotonicTimeNSec
sendOutput $ OutputProcessExited ec (end - start)
-- Process is done, run the after-first hook if necessary
runAfterFirstHookIfNecessary
-- Output the event that made the rerun happen
event <- waitForEvent eventChan
sendOutput $ OutputEvent event
-- Either wait for it to finish or wait for an event
eventOrDone <-
race
(waitForEvent eventChan)
(waitProcessHandle processHandle)
-- Either wait for it to finish or wait for an event
eventOrDone <-
race
(waitForEvent eventChan)
(waitProcessHandle processHandle)
case eventOrDone of
Left event -> handleEventHappened event
Right ec -> handleProcessDone ec
case eventOrDone of
Left event -> handleEventHappened event
Right ec -> handleProcessDone ec
installKillHandler :: ThreadId -> ProcessHandle -> IO ()
installKillHandler mainThreadId processHandle = do

View File

@ -15,18 +15,13 @@ with final.haskell.lib;
overrides = composeExtensions (old.overrides or (_: _: { })) (
self: super: {
feedback = self.generateOptparseApplicativeCompletions [ "feedback" ] (
buildFromSdist (overrideCabal
(
self.callPackage
../feedback
{ }
)
buildFromSdist (overrideCabal (self.callPackage ../feedback { })
(old: {
doBenchmark = true;
doHaddock = true;
doCoverage = false;
doHoogle = true;
doCheck = false;
doCheck = false; # Only in coverage report
hyperlinkSource = false;
enableLibraryProfiling = false;
enableExecutableProfiling = false;
@ -53,7 +48,7 @@ with final.haskell.lib;
];
# Ugly hack because we can't just add flags to the 'test' invocation.
# Show test output as we go, instead of all at once afterwards.
testTarget = (old.testTarget or "") + " --show-details=direct";
testTarget = (old.testTarget or "") + " --show-details=direct --test-options=--debug";
}));
}
);