pg-migrate-embed-1.1.0.0: test/recompilation/Main.hs
module Main (main) where
import Control.Exception qualified as Exception
import Control.Monad (filterM, unless, when)
import Data.List (isInfixOf, isPrefixOf)
import Data.Time.Clock qualified as Time
import Paths_pg_migrate_embed qualified as Paths
import System.Directory qualified as Directory
import System.Exit (ExitCode (..))
import System.FilePath qualified as FilePath
import System.IO qualified as IO
import System.Process qualified as Process
main :: IO ()
main = do
fixtureCabal <-
Paths.getDataFileName "test/recompilation/fixture/recompilation-probe.cabal"
let fixtureSource = FilePath.normalise (FilePath.takeDirectory fixtureCabal)
packageRoot =
FilePath.normalise
( FilePath.takeDirectory
(FilePath.takeDirectory (FilePath.takeDirectory fixtureSource))
)
repositoryRoot = FilePath.takeDirectory packageRoot
corePackageRoot <- resolvePackageRoot repositoryRoot "pg-migrate" "pg-migrate.cabal"
assertFileExists (packageRoot FilePath.</> "pg-migrate-embed.cabal")
withTemporaryDirectory $ \workspace -> do
copyDirectory fixtureSource workspace
writeProjectFile corePackageRoot packageRoot workspace
prepareProbe workspace
directoryModuleTimestamp <-
Directory.getModificationTime (workspace FilePath.</> "app/Main.hs")
trackedModuleTimestamp <-
Directory.getModificationTime (workspace FilePath.</> "app/TrackedMain.hs")
directoryOutput <- runDirectoryProbe workspace
let unlistedPath = workspace FilePath.</> "migrations/0002-unlisted.sql"
writeFile unlistedPath "SELECT 2;\n"
runDirectoryProbeExpectingFailure
workspace
"UnlistedSqlFiles [\"0002-unlisted.sql\"]"
Directory.removeFile unlistedPath
directoryOutputAfterRemoval <- runDirectoryProbe workspace
unless (directoryOutputAfterRemoval == directoryOutput) $
fail "removing the unlisted SQL file changed the embedded checksums"
initialOutput <- runTrackedProbe workspace
writeTrackedFile (workspace FilePath.</> "migrations/0001-first.sql") "SELECT 2;\n"
sqlChangedOutput <- runTrackedProbe workspace
when (sqlChangedOutput == initialOutput) $
fail "changing a tracked SQL file did not change the embedded checksums"
writeFile (workspace FilePath.</> "migrations/0002-second.sql") "SELECT 3;\n"
writeTrackedFile
(workspace FilePath.</> "migrations/manifest")
"0001-first.sql\n0002-second.sql\n"
manifestChangedOutput <- runTrackedProbe workspace
when (manifestChangedOutput == sqlChangedOutput) $
fail "changing the manifest did not change the embedded checksums"
unless (length (lines manifestChangedOutput) == 2) $
fail "the rebuilt probe did not embed both manifest entries"
finalDirectoryModuleTimestamp <-
Directory.getModificationTime (workspace FilePath.</> "app/Main.hs")
finalTrackedModuleTimestamp <-
Directory.getModificationTime (workspace FilePath.</> "app/TrackedMain.hs")
unless
( finalDirectoryModuleTimestamp == directoryModuleTimestamp
&& finalTrackedModuleTimestamp == trackedModuleTimestamp
)
$ fail "the recompilation test unexpectedly touched a Haskell module"
runDirectoryProbe :: FilePath -> IO String
runDirectoryProbe workspace = do
_ <-
runChecked
workspace
"cabal"
directoryProbeArguments
runChecked workspace (workspace FilePath.</> "ghc-directory-recompilation/probe") []
runDirectoryProbeExpectingFailure :: FilePath -> String -> IO ()
runDirectoryProbeExpectingFailure workspace expectedError = do
let command =
(Process.proc "cabal" directoryProbeArguments)
{ Process.cwd = Just workspace
}
(exitCode, standardOutput, standardError) <-
Process.readCreateProcessWithExitCode command ""
case exitCode of
ExitFailure _ ->
unless (expectedError `isInfixOf` (standardOutput <> standardError)) $
fail
( "probe compilation failed without the expected diagnostic "
<> show expectedError
<> ":\n"
<> standardError
)
ExitSuccess ->
fail "adding an unlisted SQL file did not force manifest revalidation"
runTrackedProbe :: FilePath -> IO String
runTrackedProbe workspace = do
_ <-
runChecked
workspace
"cabal"
trackedProbeArguments
runChecked workspace (workspace FilePath.</> "ghc-tracked-recompilation/probe") []
directoryProbeArguments :: [String]
directoryProbeArguments =
ghcArguments
"app/Main.hs"
Nothing
"ghc-directory-recompilation"
trackedProbeArguments :: [String]
trackedProbeArguments =
ghcArguments
"app/TrackedMain.hs"
(Just "TrackedMain")
"ghc-tracked-recompilation"
ghcArguments :: FilePath -> Maybe String -> FilePath -> [String]
ghcArguments source mainModule outputDirectory =
[ "exec",
"--builddir=dist-recompilation",
"--",
"ghc",
"--make",
source,
"-o",
outputDirectory FilePath.</> "probe",
"-odir",
outputDirectory,
"-hidir",
outputDirectory,
"-package",
"pg-migrate",
"-package",
"pg-migrate-embed",
"-XGHC2024",
"-XOverloadedStrings",
"-XTemplateHaskell"
]
<> maybe [] (\moduleName -> ["-main-is", moduleName]) mainModule
prepareProbe :: FilePath -> IO ()
prepareProbe workspace = do
Directory.createDirectory (workspace FilePath.</> "ghc-directory-recompilation")
Directory.createDirectory (workspace FilePath.</> "ghc-tracked-recompilation")
_ <-
runChecked
workspace
"cabal"
[ "build",
"pg-migrate",
"pg-migrate-embed",
"--builddir=dist-recompilation"
]
pure ()
runChecked :: FilePath -> FilePath -> [String] -> IO String
runChecked workspace executable arguments = do
let command =
(Process.proc executable arguments)
{ Process.cwd = Just workspace
}
(exitCode, standardOutput, standardError) <-
Process.readCreateProcessWithExitCode command ""
case exitCode of
ExitSuccess -> pure standardOutput
ExitFailure code ->
fail
( executable
<> " failed with exit code "
<> show code
<> ":\n"
<> standardError
)
writeTrackedFile :: FilePath -> String -> IO ()
writeTrackedFile path contents = do
writeFile path contents
now <- Time.getCurrentTime
Directory.setModificationTime path (Time.addUTCTime 2 now)
writeProjectFile :: FilePath -> FilePath -> FilePath -> IO ()
writeProjectFile corePackageRoot embedPackageRoot workspace =
writeFile
(workspace FilePath.</> "cabal.project")
( unlines
[ "packages:",
" .",
" " <> corePackageRoot,
" " <> embedPackageRoot,
"tests: False",
"benchmarks: False"
]
)
resolvePackageRoot :: FilePath -> FilePath -> FilePath -> IO FilePath
resolvePackageRoot parent packageName cabalFile = do
let exact = parent FilePath.</> packageName
exactExists <- Directory.doesFileExist (exact FilePath.</> cabalFile)
if exactExists
then pure exact
else do
entries <- Directory.listDirectory parent
let candidates =
[ parent FilePath.</> entry
| entry <- entries,
(packageName <> "-") `isPrefixOf` entry
]
matching <- filterM (Directory.doesFileExist . (FilePath.</> cabalFile)) candidates
case matching of
[resolved] -> pure resolved
_ -> fail ("could not resolve source distribution for " <> packageName)
copyDirectory :: FilePath -> FilePath -> IO ()
copyDirectory source destination = do
Directory.createDirectoryIfMissing True destination
entries <- Directory.listDirectory source
mapM_ copyEntry entries
where
copyEntry entry = do
let sourcePath = source FilePath.</> entry
destinationPath = destination FilePath.</> entry
isDirectory <- Directory.doesDirectoryExist sourcePath
if isDirectory
then copyDirectory sourcePath destinationPath
else Directory.copyFile sourcePath destinationPath
withTemporaryDirectory :: (FilePath -> IO value) -> IO value
withTemporaryDirectory = Exception.bracket create Directory.removePathForcibly
where
create = do
parent <- Directory.getTemporaryDirectory
(path, handle) <- IO.openTempFile parent "pg-migrate-recompilation"
IO.hClose handle
Directory.removeFile path
Directory.createDirectory path
pure path
assertFileExists :: FilePath -> IO ()
assertFileExists path = do
exists <- Directory.doesFileExist path
unless exists (fail ("required source file does not exist: " <> path))