cabal-install-3.16.0.0: tests/UnitTests/Distribution/Solver/Modular/QuickCheck.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module UnitTests.Distribution.Solver.Modular.QuickCheck (tests) where
import Distribution.Client.Compat.Prelude
import Prelude ()
import Control.Arrow ((&&&))
import Data.Either (lefts)
import Data.List (groupBy, isInfixOf)
import Text.Show.Pretty (parseValue, valToStr)
import Test.QuickCheck (Arbitrary (..), Blind (..), Gen, Positive (..), counterexample, elements, frequency, listOf, oneof, shrinkList, shrinkNothing, shuffle, sublistOf, vectorOf, (===), (==>))
import Test.QuickCheck.Instances.Cabal ()
import Test.Tasty (TestTree)
import Distribution.Types.Flag (FlagName)
import Distribution.Utils.ShortText (ShortText, fromShortText)
import Distribution.Client.Setup (defaultMaxBackjumps)
import Distribution.Types.LibraryVisibility
import Distribution.Types.PackageName
import Distribution.Types.UnqualComponentName
import Distribution.Solver.Types.ComponentDeps
( Component (..)
, ComponentDep
, ComponentDeps
)
import qualified Distribution.Solver.Types.ComponentDeps as CD
import Distribution.Solver.Types.OptionalStanza
import Distribution.Solver.Types.PackageConstraint
import qualified Distribution.Solver.Types.PackagePath as P
import Distribution.Solver.Types.PkgConfigDb
( pkgConfigDbFromList
)
import Distribution.Solver.Types.Settings
import Distribution.Solver.Types.Variable
import Distribution.Verbosity
import Distribution.Version
import UnitTests.Distribution.Solver.Modular.DSL
import UnitTests.Distribution.Solver.Modular.QuickCheck.Utils
( ArbitraryOrd (..)
, testPropertyWithSeed
)
tests :: [TestTree]
tests =
[ testPropertyWithSeed "solver does not throw exceptions" $
\test goalOrder reorderGoals indepGoals prefOldest ->
let r =
solve
(EnableBackjumping True)
(FineGrainedConflicts True)
reorderGoals
(CountConflicts True)
indepGoals
prefOldest
(getBlind <$> goalOrder)
test
in resultPlan r `seq` ()
, -- This test checks that certain solver parameters do not affect the
-- existence of a solution. It runs the solver twice, and only sets those
-- parameters on the second run. The test also applies parameters that
-- can affect the existence of a solution to both runs.
testPropertyWithSeed "target and goal order do not affect solvability" $
\test targetOrder mGoalOrder1 mGoalOrder2 indepGoals ->
let r1 = solve' mGoalOrder1 test
r2 = solve' mGoalOrder2 test{testTargets = targets2}
solve' goalOrder =
solve
(EnableBackjumping True)
(FineGrainedConflicts True)
(ReorderGoals False)
(CountConflicts True)
indepGoals
(PreferOldest False)
(getBlind <$> goalOrder)
targets = testTargets test
targets2 = case targetOrder of
SameOrder -> targets
ReverseOrder -> reverse targets
in counterexample (showResults r1 r2) $
noneReachedBackjumpLimit [r1, r2] ==>
isRight (resultPlan r1) === isRight (resultPlan r2)
, testPropertyWithSeed
"solvable without --independent-goals => solvable with --independent-goals"
$ \test reorderGoals ->
let r1 = solve' (IndependentGoals False) test
r2 = solve' (IndependentGoals True) test
solve' indep =
solve
(EnableBackjumping True)
(FineGrainedConflicts True)
reorderGoals
(CountConflicts True)
indep
(PreferOldest False)
Nothing
in counterexample (showResults r1 r2) $
noneReachedBackjumpLimit [r1, r2] ==>
isRight (resultPlan r1) `implies` isRight (resultPlan r2)
, testPropertyWithSeed "backjumping does not affect solvability" $
\test reorderGoals indepGoals ->
let r1 = solve' (EnableBackjumping True) test
r2 = solve' (EnableBackjumping False) test
solve' enableBj =
solve
enableBj
(FineGrainedConflicts False)
reorderGoals
(CountConflicts True)
indepGoals
(PreferOldest False)
Nothing
in counterexample (showResults r1 r2) $
noneReachedBackjumpLimit [r1, r2] ==>
isRight (resultPlan r1) === isRight (resultPlan r2)
, testPropertyWithSeed "fine-grained conflicts does not affect solvability" $
\test reorderGoals indepGoals ->
let r1 = solve' (FineGrainedConflicts True) test
r2 = solve' (FineGrainedConflicts False) test
solve' fineGrainedConflicts =
solve
(EnableBackjumping True)
fineGrainedConflicts
reorderGoals
(CountConflicts True)
indepGoals
(PreferOldest False)
Nothing
in counterexample (showResults r1 r2) $
noneReachedBackjumpLimit [r1, r2] ==>
isRight (resultPlan r1) === isRight (resultPlan r2)
, testPropertyWithSeed "prefer oldest does not affect solvability" $
\test reorderGoals indepGoals ->
let r1 = solve' (PreferOldest True) test
r2 = solve' (PreferOldest False) test
solve' prefOldest =
solve
(EnableBackjumping True)
(FineGrainedConflicts True)
reorderGoals
(CountConflicts True)
indepGoals
prefOldest
Nothing
in counterexample (showResults r1 r2) $
noneReachedBackjumpLimit [r1, r2] ==>
isRight (resultPlan r1) === isRight (resultPlan r2)
, -- The next two tests use --no-count-conflicts, because the goal order used
-- with --count-conflicts depends on the total set of conflicts seen by the
-- solver. The solver explores more of the tree and encounters more
-- conflicts when it doesn't backjump. The different goal orders can lead to
-- different solutions and cause the test to fail.
-- TODO: Find a faster way to randomly sort goals, and then use a random
-- goal order in these tests.
testPropertyWithSeed
"backjumping does not affect the result (with static goal order)"
$ \test reorderGoals indepGoals ->
let r1 = solve' (EnableBackjumping True) test
r2 = solve' (EnableBackjumping False) test
solve' enableBj =
solve
enableBj
(FineGrainedConflicts False)
reorderGoals
(CountConflicts False)
indepGoals
(PreferOldest False)
Nothing
in counterexample (showResults r1 r2) $
noneReachedBackjumpLimit [r1, r2] ==>
resultPlan r1 === resultPlan r2
, testPropertyWithSeed
"fine-grained conflicts does not affect the result (with static goal order)"
$ \test reorderGoals indepGoals ->
let r1 = solve' (FineGrainedConflicts True) test
r2 = solve' (FineGrainedConflicts False) test
solve' fineGrainedConflicts =
solve
(EnableBackjumping True)
fineGrainedConflicts
reorderGoals
(CountConflicts False)
indepGoals
(PreferOldest False)
Nothing
in counterexample (showResults r1 r2) $
noneReachedBackjumpLimit [r1, r2] ==>
resultPlan r1 === resultPlan r2
]
where
noneReachedBackjumpLimit :: [Result] -> Bool
noneReachedBackjumpLimit =
not . any (\r -> resultPlan r == Left BackjumpLimitReached)
showResults :: Result -> Result -> String
showResults r1 r2 = showResult 1 r1 ++ showResult 2 r2
showResult :: Int -> Result -> String
showResult n result =
unlines $
["", "Run " ++ show n ++ ":"]
++ resultLog result
++ ["result: " ++ show (resultPlan result)]
implies :: Bool -> Bool -> Bool
implies x y = not x || y
isRight :: Either a b -> Bool
isRight (Right _) = True
isRight _ = False
newtype VarOrdering = VarOrdering
{ unVarOrdering :: Variable P.QPN -> Variable P.QPN -> Ordering
}
instance Arbitrary VarOrdering where
arbitrary = VarOrdering <$> arbitraryCompare
solve
:: EnableBackjumping
-> FineGrainedConflicts
-> ReorderGoals
-> CountConflicts
-> IndependentGoals
-> PreferOldest
-> Maybe VarOrdering
-> SolverTest
-> Result
solve enableBj fineGrainedConflicts reorder countConflicts indep prefOldest goalOrder test =
let (lg, result) =
runProgress $
exResolve
(unTestDb (testDb test))
Nothing
Nothing
(Just $ pkgConfigDbFromList [])
(map unPN (testTargets test))
-- The backjump limit prevents individual tests from using
-- too much time and memory.
(Just defaultMaxBackjumps)
countConflicts
fineGrainedConflicts
(MinimizeConflictSet False)
indep
prefOldest
reorder
(AllowBootLibInstalls False)
OnlyConstrainedNone
enableBj
(SolveExecutables True)
(unVarOrdering <$> goalOrder)
(testConstraints test)
(testPreferences test)
normal
(EnableAllTests False)
failure :: String -> Failure
failure msg
| "Backjump limit reached" `isInfixOf` msg = BackjumpLimitReached
| otherwise = OtherFailure
in Result
{ resultLog = lg
, resultPlan =
-- Force the result so that we check for internal errors when we check
-- for success or failure. See D.C.Dependency.validateSolverResult.
force $ either (Left . failure) (Right . extractInstallPlan) result
}
-- | How to modify the order of the input targets.
data TargetOrder = SameOrder | ReverseOrder
deriving (Show)
instance Arbitrary TargetOrder where
arbitrary = elements [SameOrder, ReverseOrder]
shrink SameOrder = []
shrink ReverseOrder = [SameOrder]
data Result = Result
{ resultLog :: [String]
, resultPlan :: Either Failure [(ExamplePkgName, ExamplePkgVersion)]
}
data Failure = BackjumpLimitReached | OtherFailure
deriving (Eq, Generic, Show)
instance NFData Failure
-- | Package name.
newtype PN = PN {unPN :: String}
deriving (Eq, Ord, Show)
instance Arbitrary PN where
arbitrary = PN <$> elements ("base" : [[pn] | pn <- ['A' .. 'G']])
-- | Package version.
newtype PV = PV {unPV :: Int}
deriving (Eq, Ord, Show)
instance Arbitrary PV where
arbitrary = PV <$> elements [1 .. 10]
type TestPackage = Either ExampleInstalled ExampleAvailable
getName :: TestPackage -> PN
getName = PN . either exInstName exAvName
getVersion :: TestPackage -> PV
getVersion = PV . either exInstVersion exAvVersion
data SolverTest = SolverTest
{ testDb :: TestDb
, testTargets :: [PN]
, testConstraints :: [ExConstraint]
, testPreferences :: [ExPreference]
}
-- | Pretty-print the test when quickcheck calls 'show'.
instance Show SolverTest where
show test =
let str =
"SolverTest {testDb = "
++ show (testDb test)
++ ", testTargets = "
++ show (testTargets test)
++ ", testConstraints = "
++ show (testConstraints test)
++ ", testPreferences = "
++ show (testPreferences test)
++ "}"
in maybe str valToStr $ parseValue str
instance Arbitrary SolverTest where
arbitrary = do
db <- arbitrary
let pkgVersions = nub $ map (getName &&& getVersion) (unTestDb db)
pkgs = nub $ map fst pkgVersions
Positive n <- arbitrary
targets <- randomSubset n pkgs
constraints <- case pkgVersions of
[] -> return []
_ -> boundedListOf 1 $ arbitraryConstraint pkgVersions
prefs <- case pkgVersions of
[] -> return []
_ -> boundedListOf 3 $ arbitraryPreference pkgVersions
return (SolverTest db targets constraints prefs)
shrink test =
[test{testDb = db} | db <- shrink (testDb test)]
++ [test{testTargets = targets} | targets <- shrink (testTargets test)]
++ [test{testConstraints = cs} | cs <- shrink (testConstraints test)]
++ [test{testPreferences = prefs} | prefs <- shrink (testPreferences test)]
-- | Collection of source and installed packages.
newtype TestDb = TestDb {unTestDb :: ExampleDb}
deriving (Show)
instance Arbitrary TestDb where
arbitrary = do
-- Avoid cyclic dependencies by grouping packages by name and only
-- allowing each package to depend on packages in the groups before it.
groupedPkgs <-
shuffle . groupBy ((==) `on` fst) . nub . sort
=<< boundedListOf 10 arbitrary
db <- foldM nextPkgs (TestDb []) groupedPkgs
TestDb <$> shuffle (unTestDb db)
where
nextPkgs :: TestDb -> [(PN, PV)] -> Gen TestDb
nextPkgs db pkgs = TestDb . (++ unTestDb db) <$> traverse (nextPkg db) pkgs
nextPkg :: TestDb -> (PN, PV) -> Gen TestPackage
nextPkg db (pn, v) = do
installed <- arbitrary
if installed
then Left <$> arbitraryExInst pn v (lefts $ unTestDb db)
else Right <$> arbitraryExAv pn v db
shrink (TestDb pkgs) = map TestDb $ shrink pkgs
arbitraryExAv :: PN -> PV -> TestDb -> Gen ExampleAvailable
arbitraryExAv pn v db =
(\cds -> ExAv (unPN pn) (unPV v) cds []) <$> arbitraryComponentDeps pn db
arbitraryExInst :: PN -> PV -> [ExampleInstalled] -> Gen ExampleInstalled
arbitraryExInst pn v pkgs = do
pkgHash <- vectorOf 10 $ elements $ ['a' .. 'z'] ++ ['A' .. 'Z'] ++ ['0' .. '9']
numDeps <- min 3 <$> arbitrary
deps <- randomSubset numDeps pkgs
return $ ExInst (unPN pn) (unPV v) pkgHash (map exInstHash deps)
arbitraryComponentDeps :: PN -> TestDb -> Gen (ComponentDeps Dependencies)
arbitraryComponentDeps _ (TestDb []) = return $ CD.fromLibraryDeps (dependencies [])
arbitraryComponentDeps pn db = do
-- dedupComponentNames removes components with duplicate names, for example,
-- 'ComponentExe x' and 'ComponentTest x', and then CD.fromList combines
-- duplicate unnamed components.
cds <-
CD.fromList . dedupComponentNames . filter (isValid . fst)
<$> boundedListOf 5 (arbitraryComponentDep db)
return $
if isCompleteComponentDeps cds
then cds
else -- Add a library if the ComponentDeps isn't complete.
CD.fromLibraryDeps (dependencies []) <> cds
where
isValid :: Component -> Bool
isValid (ComponentSubLib name) = name /= mkUnqualComponentName (unPN pn)
isValid _ = True
dedupComponentNames =
nubBy ((\x y -> isJust x && isJust y && x == y) `on` componentName . fst)
componentName :: Component -> Maybe UnqualComponentName
componentName ComponentLib = Nothing
componentName ComponentSetup = Nothing
componentName (ComponentSubLib n) = Just n
componentName (ComponentFLib n) = Just n
componentName (ComponentExe n) = Just n
componentName (ComponentTest n) = Just n
componentName (ComponentBench n) = Just n
-- | Returns true if the ComponentDeps forms a complete package, i.e., it
-- contains a library, exe, test, or benchmark.
isCompleteComponentDeps :: ComponentDeps a -> Bool
isCompleteComponentDeps = any (completesPkg . fst) . CD.toList
where
completesPkg ComponentLib = True
completesPkg (ComponentExe _) = True
completesPkg (ComponentTest _) = True
completesPkg (ComponentBench _) = True
completesPkg (ComponentSubLib _) = False
completesPkg (ComponentFLib _) = False
completesPkg ComponentSetup = False
arbitraryComponentDep :: TestDb -> Gen (ComponentDep Dependencies)
arbitraryComponentDep db = do
comp <- arbitrary
deps <- case comp of
ComponentSetup -> smallListOf (arbitraryExDep db SetupDep)
_ -> boundedListOf 5 (arbitraryExDep db NonSetupDep)
return
( comp
, Dependencies
{ depsExampleDependencies = deps
, -- TODO: Test different values for visibility and buildability.
depsVisibility = LibraryVisibilityPublic
, depsIsBuildable = True
}
)
-- | Location of an 'ExampleDependency'. It determines which values are valid.
data ExDepLocation = SetupDep | NonSetupDep
arbitraryExDep :: TestDb -> ExDepLocation -> Gen ExampleDependency
arbitraryExDep db@(TestDb pkgs) level =
let flag =
ExFlagged
<$> arbitraryFlagName
<*> arbitraryDeps db
<*> arbitraryDeps db
other =
-- Package checks require dependencies on "base" to have bounds.
let notBase = filter ((/= PN "base") . getName) pkgs
in [ExAny . unPN <$> elements (map getName notBase) | not (null notBase)]
++ [
-- existing version
let fixed pkg = ExFix (unPN $ getName pkg) (unPV $ getVersion pkg)
in fixed <$> elements pkgs
, -- random version of an existing package
ExFix . unPN . getName <$> elements pkgs <*> (unPV <$> arbitrary)
]
in oneof $
case level of
NonSetupDep -> flag : other
SetupDep -> other
arbitraryDeps :: TestDb -> Gen Dependencies
arbitraryDeps db =
frequency
[ (1, return unbuildableDependencies)
, (20, dependencies <$> smallListOf (arbitraryExDep db NonSetupDep))
]
arbitraryFlagName :: Gen String
arbitraryFlagName = (: []) <$> elements ['A' .. 'E']
arbitraryConstraint :: [(PN, PV)] -> Gen ExConstraint
arbitraryConstraint pkgs = do
(PN pn, v) <- elements pkgs
let anyQualifier = ScopeAnyQualifier (mkPackageName pn)
oneof
[ ExVersionConstraint anyQualifier <$> arbitraryVersionRange v
, ExStanzaConstraint anyQualifier <$> sublistOf [TestStanzas, BenchStanzas]
]
arbitraryPreference :: [(PN, PV)] -> Gen ExPreference
arbitraryPreference pkgs = do
(PN pn, v) <- elements pkgs
oneof
[ ExStanzaPref pn <$> sublistOf [TestStanzas, BenchStanzas]
, ExPkgPref pn <$> arbitraryVersionRange v
]
arbitraryVersionRange :: PV -> Gen VersionRange
arbitraryVersionRange (PV v) =
let version = mkSimpleVersion v
in elements
[ thisVersion version
, notThisVersion version
, earlierVersion version
, orLaterVersion version
, noVersion
]
instance Arbitrary ReorderGoals where
arbitrary = ReorderGoals <$> arbitrary
shrink (ReorderGoals reorder) = [ReorderGoals False | reorder]
instance Arbitrary IndependentGoals where
arbitrary = IndependentGoals <$> arbitrary
shrink (IndependentGoals indep) = [IndependentGoals False | indep]
instance Arbitrary PreferOldest where
arbitrary = PreferOldest <$> arbitrary
shrink (PreferOldest prefOldest) = [PreferOldest False | prefOldest]
instance Arbitrary Component where
arbitrary =
oneof
[ return ComponentLib
, ComponentSubLib <$> arbitraryUQN
, ComponentExe <$> arbitraryUQN
, ComponentFLib <$> arbitraryUQN
, ComponentTest <$> arbitraryUQN
, ComponentBench <$> arbitraryUQN
, return ComponentSetup
]
shrink ComponentLib = []
shrink _ = [ComponentLib]
-- The "component-" prefix prevents component names and build-depends
-- dependency names from overlapping.
-- TODO: Remove the prefix once the QuickCheck tests support dependencies on
-- internal libraries.
arbitraryUQN :: Gen UnqualComponentName
arbitraryUQN =
mkUnqualComponentName <$> (\c -> "component-" ++ [c]) <$> elements "ABC"
instance Arbitrary ExampleInstalled where
arbitrary = error "arbitrary not implemented: ExampleInstalled"
shrink ei =
[ ei{exInstBuildAgainst = deps}
| deps <- shrinkList shrinkNothing (exInstBuildAgainst ei)
]
instance Arbitrary ExampleAvailable where
arbitrary = error "arbitrary not implemented: ExampleAvailable"
shrink ea = [ea{exAvDeps = deps} | deps <- shrink (exAvDeps ea)]
instance (Arbitrary a, Monoid a) => Arbitrary (ComponentDeps a) where
arbitrary = error "arbitrary not implemented: ComponentDeps"
shrink = filter isCompleteComponentDeps . map CD.fromList . shrink . CD.toList
instance Arbitrary ExampleDependency where
arbitrary = error "arbitrary not implemented: ExampleDependency"
shrink (ExAny _) = []
shrink (ExFix "base" _) = [] -- preserve bounds on base
shrink (ExFix pn _) = [ExAny pn]
shrink (ExFlagged flag th el) =
depsExampleDependencies th
++ depsExampleDependencies el
++ [ExFlagged flag th' el | th' <- shrink th]
++ [ExFlagged flag th el' | el' <- shrink el]
shrink dep = error $ "Dependency not handled: " ++ show dep
instance Arbitrary Dependencies where
arbitrary = error "arbitrary not implemented: Dependencies"
shrink deps =
[deps{depsVisibility = v} | v <- shrink $ depsVisibility deps]
++ [deps{depsIsBuildable = b} | b <- shrink $ depsIsBuildable deps]
++ [deps{depsExampleDependencies = ds} | ds <- shrink $ depsExampleDependencies deps]
instance Arbitrary ExConstraint where
arbitrary = error "arbitrary not implemented: ExConstraint"
shrink (ExStanzaConstraint scope stanzas) =
[ExStanzaConstraint scope stanzas' | stanzas' <- shrink stanzas]
shrink (ExVersionConstraint scope vr) =
[ExVersionConstraint scope vr' | vr' <- shrink vr]
shrink _ = []
instance Arbitrary ExPreference where
arbitrary = error "arbitrary not implemented: ExPreference"
shrink (ExStanzaPref pn stanzas) =
[ExStanzaPref pn stanzas' | stanzas' <- shrink stanzas]
shrink (ExPkgPref pn vr) = [ExPkgPref pn vr' | vr' <- shrink vr]
instance Arbitrary OptionalStanza where
arbitrary = error "arbitrary not implemented: OptionalStanza"
shrink BenchStanzas = [TestStanzas]
shrink TestStanzas = []
instance ArbitraryOrd pn => ArbitraryOrd (Variable pn)
instance ArbitraryOrd a => ArbitraryOrd (P.Qualified a)
instance ArbitraryOrd P.PackagePath
instance ArbitraryOrd P.Qualifier
instance ArbitraryOrd P.Namespace
instance ArbitraryOrd OptionalStanza
instance ArbitraryOrd FlagName
instance ArbitraryOrd PackageName
instance ArbitraryOrd ShortText where
arbitraryCompare = do
strc <- arbitraryCompare
pure $ \l r -> strc (fromShortText l) (fromShortText r)
deriving instance Generic (Variable pn)
deriving instance Generic (P.Qualified a)
deriving instance Generic P.PackagePath
deriving instance Generic P.Namespace
deriving instance Generic P.Qualifier
randomSubset :: Int -> [a] -> Gen [a]
randomSubset n xs = take n <$> shuffle xs
boundedListOf :: Int -> Gen a -> Gen [a]
boundedListOf n gen = take n <$> listOf gen
-- | Generates lists with average length less than 1.
smallListOf :: Gen a -> Gen [a]
smallListOf gen =
frequency
[ (fr, vectorOf n gen)
| (fr, n) <- [(3, 0), (5, 1), (2, 2)]
]