-------------------------------------------------------------------------------- --- This module contains the dependency resolution algorithm. -------------------------------------------------------------------------------- module CPM.Resolution ( ResolutionResult , showResult, showShortResult, dependenciesAsGraph , resolutionSuccess , resolvedPackages , showDependencies, showShortDependencies , showConflict , allTransitiveDependencies , transitiveDependencies , resolve , resolveDependenciesFromLookupSet , isCompatibleToCompiler , isDisjunctionCompatible ) where import Data.Either import Data.List import Data.Maybe import qualified Data.GraphViz as DG import Test.Prop import Text.Pretty import CPM.Config ( Config, defaultConfig, compilerVersion , compilerBaseVersion ) import CPM.ErrorLogger import CPM.LookupSet import CPM.Package --- Resolves the dependencies of a package using packages from a lookup set, --- inside an error logger. resolveDependenciesFromLookupSet :: Config -> Package -> LookupSet -> ErrorLogger ResolutionResult resolveDependenciesFromLookupSet cfg pkg lookupSet = let result = resolve cfg pkg lookupSet in if resolutionSuccess result then return result else fail $ showResult result --- Resolves the dependencies of a package using packages from a lookup set. --- The base package of the current compiler is removed from the result set. resolve :: Config -> Package -> LookupSet -> ResolutionResult resolve cfg pkg ls = case resolvedPkgs of Just pkgs -> ResolutionSuccess pkg (deleteBase pkgs) Nothing -> ResolutionFailure labeledTree where labeledTree = labelConflicts cfg $ candidateTree pkg ls noConflicts = prune ((/= Nothing) . clConflict) labeledTree resolvedPkgs = maybeHead . map stPackages . filter stComplete . leaves . mapTree clState $ noConflicts deleteBase = filter (\p -> name p /= "base" || showVersion (version p) /= compilerBaseVersion cfg) --- Gives a list of all activated packages for a successful resolution. resolvedPackages :: ResolutionResult -> [Package] resolvedPackages (ResolutionSuccess pkg deps) = delete pkg deps resolvedPackages (ResolutionFailure _) = error "resolvedPackages called on failure" --- Tries to get a list of activated packages for a resolution. Returns Nothing --- if the resolution was not successful. maybeResolvedPackages :: ResolutionResult -> Maybe [Package] maybeResolvedPackages (ResolutionSuccess _ deps) = Just deps maybeResolvedPackages (ResolutionFailure _) = Nothing --- Was a resolution successful? resolutionSuccess :: ResolutionResult -> Bool resolutionSuccess (ResolutionSuccess _ _) = True resolutionSuccess (ResolutionFailure _) = False --- Renders a dependency tree from a successful resolution. showDependencies :: ResolutionResult -> String showDependencies (ResolutionSuccess pkg deps) = showTree . mapTree (text . packageId) $ dependencyTree deps pkg showDependencies (ResolutionFailure _) = "Resolution failed." --- Renders a dependency tree from a successful resolution in a short form --- where already shown packages are not fully shown again. showShortDependencies :: ResolutionResult -> String showShortDependencies (ResolutionSuccess pkg deps) = showTree . mapTree text . shortenPackageTree $ dependencyTree deps pkg showShortDependencies (ResolutionFailure _) = "Resolution failed." -- Reduces a package tree so that duplicated nodes are removed. -- The resulting tree contains the `packageId`s of the packages. shortenPackageTree :: Tree Package -> Tree String shortenPackageTree = fst . shortenPT [] where shortenPT pids (Node p ps) = let pid = packageId p in if pid `elem` pids then (Node (pid ++ "...") [], pids) else let (pts, pids') = shortenPTs (pid:pids) ps in (Node pid pts, pids') shortenPTs pids [] = ([],pids) shortenPTs pids (p:ps) = let (pt, pids1) = shortenPT pids p (pts,pids2) = shortenPTs pids1 ps in (pt:pts, pids2) --- Renders a conflict resolution into a textual representation. showConflict :: ResolutionResult -> String showConflict (ResolutionSuccess _ _) = "Resolution succeeded." showConflict (ResolutionFailure t) = case findRelevantConflict t of Just c -> showConflictState c Nothing -> case missingPackages $ clState $ findDeepestNode t of [] -> "Conflict resolution failed for an unknown reason... Hint:(\n" ++ "Please clean your package ('cypm clean') and/or\n" ++ "your package index ('cypm update') and try again..." (d@(Dependency p _):_) -> "There seems to be no version of package " ++ p ++ " that can satisfy the constraint " ++ showDependency d showConflictTree :: ResolutionResult -> String showConflictTree (ResolutionSuccess _ _) = "Resolution succeeded." showConflictTree (ResolutionFailure t) = showTree $ mapTree labeler $ cutBelowConflict t where pkgId = text . packageId . actPackage actChain a@(InitialA _) = pkgId a actChain a@(ChildA _ _ p) = pkgId a <+> text "->" <+> actChain p labeler ((a, _), Nothing) = pkgId a labeler ((a, _), Just (CompilerConflict _)) = red $ text "C" <+> actChain a labeler ((a, _), Just (PrimaryConflict _)) = red $ text "P" <+> actChain a labeler ((a, _), Just (SecondaryConflict a' a'')) = red $ text "S" <+> actChain a <+> parens (pkgId a') <+> parens (pkgId a'') cutBelowConflict (Node (a, Nothing) cs) = Node (a, Nothing) $ map cutBelowConflict cs cutBelowConflict (Node (a, Just c) _) = Node (a, Just c) [] showCandidateTree :: Tree State -> String showCandidateTree = showTree . mapTree (text . packageId . actPackage . stActivation) showLabelTree :: Config -> Tree State -> String showLabelTree cfg = showTree . mapTree labeler . cutBelowConflict . labelConflicts cfg where pkgId = text . packageId . actPackage . stActivation actId = text . packageId . actPackage labeler (s, Nothing) = pkgId s labeler (s, Just (CompilerConflict _)) = red $ text "C" <+> pkgId s labeler (s, Just (PrimaryConflict _)) = red $ text "P" <+> pkgId s labeler (s, Just (SecondaryConflict a1 a2)) = red $ text "S" <+> pkgId s <+> actId a1 <+> actId a2 cutBelowConflict (Node (a, Nothing) cs) = Node (a, Nothing) $ map cutBelowConflict cs cutBelowConflict (Node (a, Just c) _) = Node (a, Just c) [] resultConflict :: ResolutionResult -> Maybe Conflict resultConflict (ResolutionSuccess _ _) = Nothing resultConflict (ResolutionFailure t) = case findRelevantConflict t of Nothing -> Nothing Just cs -> clConflict cs --- Renders a resolution result into a textual representation for the user. --- In case of success, the dependency tree is shown. In case of failure, --- information on the cause of the conflict is shown. showResult :: ResolutionResult -> String showResult r@(ResolutionSuccess _ _) = showDependencies r showResult r@(ResolutionFailure _) = showConflict r --- Renders a resolution result into a short textual representation --- (where already shown packages are not shown again) for the user. --- In case of success, the shortened dependency tree is shown. --- In case of failure, information on the cause of the conflict is shown. showShortResult :: ResolutionResult -> String showShortResult r@(ResolutionSuccess _ _) = showShortDependencies r showShortResult r@(ResolutionFailure _) = showConflict r --- Result of a resolution run. In case of success, it contains the original --- package as well as a list of resolved packages. If the resolution failed, it --- contains the conflict tree. data ResolutionResult = ResolutionSuccess Package [Package] | ResolutionFailure (Tree ConflictState) deriving (Eq,Show) --- Shows a successful resolution as a (Graphviz) dot graph. dependenciesAsGraph :: ResolutionResult -> Maybe DG.DotGraph dependenciesAsGraph (ResolutionFailure _) = Nothing dependenciesAsGraph (ResolutionSuccess pkg deppkgs) = let allpkgs = pkg : deppkgs alldepnames = map (\p -> (name p, map (\ (Dependency p' _) -> p') (dependencies p))) allpkgs showPkg pn = maybe pn packageId (find ((== pn) . name) allpkgs) deps = map (\ (p,dp) -> (showPkg p, map showPkg dp)) alldepnames in Just $ DG.dgraph ("Dependencies of " ++ name pkg) (map (\s -> DG.Node s []) (nub (map fst deps ++ concatMap snd deps))) (map (\ (s,t) -> DG.Edge s t []) (nub (concatMap (\ (p,ds) -> map (\d -> (p,d)) ds) deps))) --- Represents an activation of a package in the candidate tree. Activations --- form a chain up to the initial activation, i.e. the initial package that --- resolution was started on. Each activation's parent is the activation of the --- package that led to the current activation, i.e. the package whose --- dependency led to the current package version being chosen. data Activation = InitialA Package | ChildA Package Dependency Activation deriving (Eq,Show) --- Each tree node is labeled with the current activation and all former --- activations. type State = (Activation, [Activation]) --- A conflict occurs when one of the active packages in a state clashes with --- one of dependencies of all of the state's active packages. If the clash --- occurs between a package A and a dependency of a package B and B is also the --- package that activated A, i.e. the parent of its activation, we call the --- conflict a 'same package' conflict. A 'real' conflict is a one where package --- A was activated by some earlier package. When the compiler compatibility --- constraints of the package activated in the current state are not met, then --- we use the compiler conflict. data Conflict = SecondaryConflict Activation Activation | PrimaryConflict Activation | CompilerConflict Activation deriving (Eq,Show) --- A state and a potential conflict. type ConflictState = (State, Maybe Conflict) --- Gets the package that was activated in a state. stPackage :: State -> Package stPackage (a, _) = actPackage a --- Gets all active packages in a state. stPackages :: State -> [Package] stPackages (_, as) = map actPackage as --- Gets the state's current activation stActivation :: State -> Activation stActivation = fst --- Gets all activations leading up to the state, including the current --- activation. stActivations :: State -> [Activation] stActivations = snd --- Gets a list of all dependencies of all active packages in a state, alongside --- the activations that activated the respective packages. stDependencies :: State -> [(Activation, Dependency)] stDependencies = concatMap zippedDeps . stActivations where zippedDeps a = zip (repeat a) $ dependencies $ actPackage a --- Gets a list of all dependencies of all active packages in a state. stAllDependencies :: State -> [Dependency] stAllDependencies = concatMap dependencies . stPackages --- Gets an activation's package. actPackage :: Activation -> Package actPackage (InitialA p) = p actPackage (ChildA p _ _) = p actDependency :: Activation -> Dependency actDependency (InitialA _) = error "Called on initialA" actDependency (ChildA _ d _) = d actParent :: Activation -> Activation actParent a@(InitialA _) = a actParent (ChildA _ _ p) = p --- Gets a potential conflict from a conflict state. clConflict :: ConflictState -> Maybe Conflict clConflict = snd --- Gets the original state from a conflict state. clState :: ConflictState -> State clState = fst --- A tree with a label and child nodes. data Tree a = Node a [Tree a] deriving (Eq,Show) --- Recursively applies a function to each node in a tree. mapTree :: (a -> b) -> Tree a -> Tree b mapTree f (Node a cs) = Node (f a) $ map (mapTree f) cs --- A node's label. label :: Tree a -> a label (Node a _) = a leaves :: Tree a -> [a] leaves (Node a []) = [a] leaves (Node _ cs@(_:_)) = concatMap leaves cs --- Folds a tree to a value. foldTree :: (a -> [b] -> b) -> Tree a -> b foldTree f (Node a cs) = f a (map (foldTree f) cs) --- Filters a tree using a predicate. filterTree :: (a -> Bool) -> Tree a -> Tree a filterTree p = foldTree f where f a cs = Node a (filter (p . label) cs) --- Removes all nodes from a tree that match the predicate. prune :: (a -> Bool) -> Tree a -> Tree a prune p = filterTree (not . p) --- Shows a textual representation of a tree. showTree :: Tree Doc -> String showTree t = "Package dependencies:\n" ++ pPrint (ppTree t) --- Pretty prints a tree of Docs into a single Doc. ppTree :: Tree Doc -> Doc ppTree (Node l cs) = l <$$> vcat children where children = map (\t -> indent 2 $ text "|-" <+> ppTree t) cs --- Extends a tree by appending a node to the first leaf in order, i.e. the --- leftmost leaf. extendTree :: Tree a -> Tree a -> Tree a extendTree (Node a []) n = Node a [n] extendTree (Node a (c:cs)) n = Node a $ (extendTree c n):cs --- Converts a tree of strings into the Graphviz dot format. dotifyTree :: Tree String -> String dotifyTree t = "digraph tree {\n" ++ full ++ "\n}" where (_, _, full) = dotify' (0, [], "") t dotify' (n, acc, s) (Node l cs) = let (n', children, str) = foldl (dotify') (n + 1, [], "") cs in (n', n:acc, s ++ intercalate "\n" ([node n l] ++ map (edge n) children) ++ str) node n l = "n" ++ (show n) ++ " [label=\"" ++ l ++ "\"];\n" edge a b = "n" ++ (show a) ++ " -> " ++ "n" ++ (show b) ++ ";\n" --- Builds a tree of candidate states from a package and a lookup set. This is --- the tree that is searched for complete states or conflicts. candidateTree :: Package -> LookupSet -> Tree State candidateTree pkg ls = let s = InitialA pkg in Node (s, [s]) $ tree' [s] (zip (repeat s) (dependencies pkg)) where tree' acts ((act, d@(Dependency p _)):ds) = if p `elem` (map (name . actPackage) acts) then tree' acts ds else map (nodesForDep act d ds acts) $ findAllVersions ls p True tree' _ [] = [] nodesForDep act d ds acts p' = let act' = ChildA p' d act acts' = act':acts nextDeps = zip (repeat act') (dependencies p') ++ ds in Node (act', acts') $ tree' acts' nextDeps --- Calculates the first conflict for each node in the tree and annotates the --- nodes with these conflicts. labelConflicts :: Config -> Tree State -> Tree ConflictState labelConflicts cfg = mapTree f where f s = (s, firstConflict cfg s (reverse $ stDependencies s)) --- Checks whether a state is complete, i.e. whether all packages mentioned in --- all dependencies of all active packages are present in the list of active --- packages. Note that stComplete does not check whether a dependency is --- actually met by a package, only whether the package is present. stComplete --- is meant to be called on a state that has already been checked for --- conflicts. stComplete :: State -> Bool stComplete s = missingPackages s == [] stCompleteness :: State -> Int stCompleteness s = length $ missingPackages s --- Finds all dependencies in a state which is unmet because its dependency is --- missing altogether, i.e. no version of the package is activated. missingPackages :: State -> [Dependency] missingPackages s = missing' (stPackages s) (stAllDependencies s) where missing' pkgs ds = filter (noPackage pkgs) ds noPackage pkgs (Dependency p _) = find ((== p) . name) pkgs == Nothing --- Calculates the first conflict in a state. firstConflict :: Config -> State -> [(Activation, Dependency)] -> Maybe Conflict firstConflict _ _ [] = Nothing firstConflict cfg s@(act, acts) ((depAct, Dependency p disj):ds) = if not $ isCompatibleToCompiler cfg (actPackage act) then Just $ CompilerConflict act else case findPkg of Nothing -> firstConflict cfg s ds Just a -> if isDisjunctionCompatible (version $ actPackage a) disj then firstConflict cfg s ds else if actParent a == depAct then Just $ PrimaryConflict a else Just $ SecondaryConflict a depAct where findPkg = find ((== p) . name . actPackage) acts --- Finds the deepest right-most node in a tree. findDeepestNode :: Tree a -> a findDeepestNode = snd . maxNode . leaves . depthTree where maxNode ls = foldl maxN (head ls) ls maxN (na, a) (nb, b) = if nb >= na then (nb, b) else (na, a) depthTree = relabel 0 relabel n (Node a cs) = Node (n, a) (map (relabel (n + 1)) cs) findRelevantConflict :: Tree ConflictState -> Maybe ConflictState findRelevantConflict = maybeMostRelevant . map mostRelevant . map snd . minGroups . filter ((/= []) . snd) . findGroups . cutBelowConflict . relabel where maybeMostRelevant [] = Nothing maybeMostRelevant cs@(_:_) = Just $ mostRelevant cs mostRelevant cs = case find (isSecondary . fromJust . clConflict) cs of Just s -> s Nothing -> case find (isCompiler . fromJust . clConflict) cs of Just c -> c Nothing -> head cs minGroups gs = let minG = foldl (\m g -> min m (fst g)) 99999 gs in filter ((== minG) . fst) gs isSecondary (SecondaryConflict _ _) = True isSecondary (PrimaryConflict _) = False isSecondary (CompilerConflict _) = False isCompiler (SecondaryConflict _ _) = False isCompiler (PrimaryConflict _) = False isCompiler (CompilerConflict _) = True findGroups (Node (d, (_, Nothing)) []) = [(d, [])] findGroups (Node (d, (_, Nothing)) cs@(_:_)) = if containsOnlyConflicts cs then [(d, map (snd . label) cs)] else concatMap findGroups cs findGroups (Node (d, (_, Just _)) _) = [(d, [])] containsOnlyConflicts = all (isJust . clConflict . snd) . map label cutBelowConflict (Node (d, (a, Nothing)) cs) = Node (d, (a, Nothing)) $ map cutBelowConflict cs cutBelowConflict (Node (d, (a, Just c)) _) = Node (d, (a, Just c)) [] relabel = mapTree (\a -> (stCompleteness $ clState a, a)) --- Renders the information from a real conflict into a textual representation --- for the user. --- --- @param originalAct - the original activation of the package --- @param confDep - the dependency conflicting the original activation --- @param confAct - the activation of the conflict dependency showRealConflictInfo :: Activation -> Activation -> String showRealConflictInfo originalAct confAct = let mkLabel pkg dep = (text $ name pkg) <+> (parens $ text $ showDependency dep) triedPkg = actPackage originalAct actLabeler (InitialA p) = text $ name p actLabeler (ChildA p dep _) = mkLabel p dep originalTree = mapTree actLabeler $ activationTree originalAct confTree = mapTree actLabeler $ activationTree confAct confDepLabel = mkLabel triedPkg (findDependencyOn triedPkg confAct) confTree' = extendTree confTree (Node confDepLabel []) findDependencyOn pkg act = case find ((== name pkg) . depPkg) $ dependencies $ actPackage act of Just a -> a Nothing -> error "Hey!" depPkg (Dependency p _) = p in pPrint $ (text $ "There was a conflict for package " ++ name triedPkg) <$$> ppTree originalTree <$$> ppTree confTree' showSamePackageConflictInfo :: Activation -> String showSamePackageConflictInfo act = let triedPkg = actPackage act in "There seems to be no version of package " ++ name triedPkg ++ " that can satisfy the constraint " ++ showDependency (actDependency act) showCompilerConflictInfo :: Activation -> String showCompilerConflictInfo act = "The package " ++ (packageId $ actPackage act) ++ ", dependency constraint " ++ showDependency (actDependency act) ++ ", is not compatible to the current compiler. It was activated because:\n" ++ showTree actTree where mkLabel pkg dep = (text $ name pkg) <+> (parens $ text $ showDependency dep) actLabeler (InitialA p) = text $ name p actLabeler (ChildA p dep _) = mkLabel p dep actTree = mapTree actLabeler $ activationTree act --- Renders a conflict state into a textual representation for the user. showConflictState :: ConflictState -> String showConflictState ((InitialA _, _), Nothing) = "No Conflict!" showConflictState ((InitialA pkg, _), Just _) = "Initial Conflict! " ++ packageId pkg showConflictState ((ChildA _ _ _, _), Nothing) = "No Conflict!" showConflictState ((ChildA _ _ _, _), Just (PrimaryConflict originalAct)) = showSamePackageConflictInfo originalAct showConflictState ((ChildA _ _ _, _), Just (SecondaryConflict originalAct confAct)) = showRealConflictInfo originalAct confAct showConflictState ((ChildA _ _ _, _), Just (CompilerConflict act)) = showCompilerConflictInfo act --- Turns an activation into a tree, with the initial activation as its root. --- Note that this tree will be a singly linked list, i.e. each node will have --- at most one child. activationTree :: Activation -> Tree Activation activationTree = head . foldl (\acc f -> f acc) [] . actTree' where actTree' x@(InitialA _) = [\cs -> [Node x cs]] actTree' x@(ChildA _ _ parent) = (\cs -> [Node x cs]):(actTree' parent) --- Turns a list of activated packages and an original package into a dependency --- tree. dependencyTree :: [Package] -> Package -> Tree Package dependencyTree chosen pkg = Node pkg $ map (dependencyTree chosen) childPkgs where justs = map fromJust . filter (/= Nothing) childPkgs = justs $ map findPkg (dependencies pkg) findPkg (Dependency p _) = find ((== p) . name) chosen maybeHead :: [a] -> Maybe a maybeHead [] = Nothing maybeHead (x:_) = Just x packageSource :: Package -> LookupSet -> Maybe LookupSource packageSource p ls = lookupSource ls p allTransitiveDependencies' :: [String] -> LookupSet -> String -> [String] allTransitiveDependencies' seen ls pkg = nub $ allDeps where allVersions = findAllVersions ls pkg True allDeps = foldl (\s d -> transitiveDependencies' s ls d) seen allVersions allTransitiveDependencies :: LookupSet -> String -> [String] allTransitiveDependencies = allTransitiveDependencies' [] transitiveDependencies' :: [String] -> LookupSet -> Package -> [String] transitiveDependencies' seen ls pkg = foldl (\s d -> if d `elem` s then s else nub (s ++ allTransitiveDependencies' (d:s) ls d)) seen deps where deps = map dependencyName $ dependencies pkg dependencyName (Dependency n _) = n transitiveDependencies :: LookupSet -> Package -> [String] transitiveDependencies = transitiveDependencies' [] test_transitiveDependencies_simpleCase :: Prop test_transitiveDependencies_simpleCase = transitiveDependencies db pkg -=- ["B", "C"] where pkg = cPackage "A" (0, 0, 1, Nothing) [cDep "B" ">= 1.0.0", cDep "C" "= 1.2.0"] b = cPackage "B" (1, 0, 9, Nothing) [] c = cPackage "C" (1, 2, 0, Nothing) [] db = cDB [b, c] test_transitiveDependencies_loop :: Prop test_transitiveDependencies_loop = transitiveDependencies db pkg -=- ["B", "C"] where pkg = cPackage "A" (0, 0, 1, Nothing) [cDep "B" ">= 1.0.0", cDep "C" "= 1.2.0"] b = cPackage "B" (1, 0, 0, Nothing) [cDep "C" "= 1.2.0"] c = cPackage "C" (1, 2, 0, Nothing) [cDep "B" ">= 1.0.0"] db = cDB [b, c] test_transitiveDependencies_multipleVersions :: Prop test_transitiveDependencies_multipleVersions = transitiveDependencies db pkg -=- ["B", "D", "C"] where pkg = cPackage "A" (0, 0, 1, Nothing) [cDep "B" ">= 1.0.0"] b100 = cPackage "B" (1, 0, 0, Nothing) [cDep "C" "= 1.0.0"] b110 = cPackage "B" (1, 1, 0, Nothing) [cDep "D" "= 1.0.0"] c = cPackage "C" (1, 0, 0, Nothing) [] d = cPackage "D" (1, 0, 0, Nothing) [] db = cDB [b100, b110, c, d] -- Is the package compatible to the compiler used by CPM? isCompatibleToCompiler :: Config -> Package -> Bool isCompatibleToCompiler cfg p = case compats of [] -> -- No compiler constraints => check base compatibility isCompilerCompatibleBase cfg p (_:_) -> case constraintForCompiler of Nothing -> False -- No constraints for current compiler -- => compiler is incompatible Just (CompilerCompatibility _ c) -> isDisjunctionCompatible (maj, min, revi, Nothing) c && isCompilerCompatibleBase cfg p where (name, maj, min, revi) = compilerVersion cfg compats = compilerCompatibility p constraintForCompiler = find (\(CompilerCompatibility c _) -> c == name) compats -- Is the package compatible to the base version of the compiler used by CPM? isCompilerCompatibleBase :: Config -> Package -> Bool isCompilerCompatibleBase cfg p = all (\ (Dependency _ c) -> isDisjunctionCompatible baseversion c) basedependencies where baseversion = maybe (0,0,0,Nothing) id (readVersion (compilerBaseVersion cfg)) basedependencies = filter (\ (Dependency dp _) -> dp == "base") (dependencies p) isDisjunctionCompatible :: Version -> Disjunction -> Bool isDisjunctionCompatible ver cs = any id (map (all id) rs) where rs = map (map isCompatible) cs preReleaseCompatible (_, _, _, p1) (_, _, _, p2) = (isJust p1 && isJust p2) || (isNothing p1 && isNothing p2) isCompatible (VExact v) = v == ver isCompatible (VLt v) = ver `vlt` v && preReleaseCompatible ver v isCompatible (VLte v) = ver `vlte` v && preReleaseCompatible ver v isCompatible (VGt v) = ver `vgt` v && preReleaseCompatible ver v isCompatible (VGte v) = ver `vgte` v && preReleaseCompatible ver v isCompatible (VMinCompatible v) = ver `vgte` v && ver `vlt` (nextMinor v) && preReleaseCompatible ver v isCompatible (VMajCompatible v) = ver `vgte` v && ver `vlt` (nextMajor v) && preReleaseCompatible ver v test_onlyConjunctionCompatible :: Prop test_onlyConjunctionCompatible = isDisjunctionCompatible ver dis -=- True where dis = cDisj "= 1.0.0" ver = (1, 0, 0, Nothing) test_allConjunctionsCompatible :: Prop test_allConjunctionsCompatible = isDisjunctionCompatible ver dis -=- True where dis = cDisj ">= 1.0.0 || = 1.2.0" ver = (1, 2, 0, Nothing) test_oneConjunctionCompatible :: Prop test_oneConjunctionCompatible = isDisjunctionCompatible ver dis -=- True where ver = (1, 0, 0, Nothing) dis = cDisj "> 2.0.0 || = 1.0.0" test_conjunctionWithMultipleParts :: Prop test_conjunctionWithMultipleParts = isDisjunctionCompatible ver dis -=- True where ver = (1, 0, 0, Nothing) dis = cDisj ">= 1.0.0, < 2.0.0" test_reportsSimpleFailure :: Prop test_reportsSimpleFailure = isDisjunctionCompatible ver dis -=- False where ver = (1, 0, 0, Nothing) dis = cDisj "> 1.0.0" test_reportsAllConjunctionsAsFailure :: Prop test_reportsAllConjunctionsAsFailure = isDisjunctionCompatible ver dis -=- False where ver = (1, 0, 0, Nothing) dis = cDisj "< 1.0.0 || > 1.0.0" test_reportsRelevantPartOfConjunction :: Prop test_reportsRelevantPartOfConjunction = isDisjunctionCompatible ver dis -=- False where ver = (1, 0, 0, Nothing) dis = cDisj "< 1.0.0, > 0.5.0" test_semverCompatible :: Prop test_semverCompatible = isDisjunctionCompatible ver dis -=- True where ver = (0, 5, 9, Nothing) dis = cDisj "~> 0.5.0" test_semverIncompatible :: Prop test_semverIncompatible = isDisjunctionCompatible ver dis -=- False where ver = (0, 7, 1, Nothing) dis = cDisj "~> 0.6.0" test_semverMinimum :: Prop test_semverMinimum = isDisjunctionCompatible ver dis -=- False where ver = (0, 7, 0, Nothing) dis = cDisj "~> 0.7.2" test_resolvesSimpleDependency :: Prop test_resolvesSimpleDependency = maybeResolvedPackages (resolve defaultConfig pkg db) -=- Just [json100, pkg] where pkg = cPackage "sample" (0, 0, 1, Nothing) [cDep "json" "=1.0.0"] json100 = cPackage "json" (1, 0, 0, Nothing) [] json101 = cPackage "json" (1, 0, 1, Nothing) [] db = cDB [json100, json101] test_reportsUnknownPackage :: Prop test_reportsUnknownPackage = showResult result -=- "There seems to be no version of package json that can satisfy the constraint json = 1.0.0" where result = resolve defaultConfig pkg db pkg = cPackage "sample" (0, 0, 1, Nothing) [cDep "json" "= 1.0.0"] db = cDB [pkg] test_reportsMissingPackageVersion :: Prop test_reportsMissingPackageVersion = showResult result -=- "There seems to be no version of package json that can satisfy the constraint json = 1.2.0" where result = resolve defaultConfig pkg db pkg = cPackage "sample" (0, 0, 1, Nothing) [cDep "json" "=1.2.0"] json = cPackage "json" (1, 0, 0, Nothing) [] db = cDB [json] test_reportsSecondaryConflict :: Prop test_reportsSecondaryConflict = showResult result -=- expectedMessage where result = resolve defaultConfig pkg db pkg = cPackage "sample" (0, 0, 1, Nothing) [cDep "json" "= 1.0.0", cDep "b" ">= 0.0.1"] b = cPackage "b" (0, 0, 2, Nothing) [cDep "json" "~> 1.0.4"] json100 = cPackage "json" (1, 0, 0, Nothing) [] json105 = cPackage "json" (1, 0, 5, Nothing) [] db = cDB [pkg, b, json100, json105] expectedMessage = "There was a conflict for package json\n" ++ "sample\n" ++ " |- json (json = 1.0.0)\n" ++ "sample\n" ++ " |- b (b >= 0.0.1)\n" ++ " |- json (json ~1.0.4)" test_reportsSecondaryConflictInsteadOfPrimary :: Prop test_reportsSecondaryConflictInsteadOfPrimary = showResult result -=- expectedMessage where result = resolve defaultConfig pkg db pkg = cPackage "sample" (0, 0, 1, Nothing) [cDep "json" "= 1.0.0", cDep "b" ">= 0.0.5"] b001 = cPackage "b" (0, 0, 1, Nothing) [] b002 = cPackage "b" (0, 0, 2, Nothing) [] b003 = cPackage "b" (0, 0, 3, Nothing) [] b006 = cPackage "b" (0, 0, 6, Nothing) [cDep "json" "~> 1.0.4"] json100 = cPackage "json" (1, 0, 0, Nothing) [] json105 = cPackage "json" (1, 0, 5, Nothing) [] db = cDB [pkg, b001, b002, b003, b006, json100, json105] expectedMessage = "There was a conflict for package json\n" ++ "sample\n" ++ " |- json (json = 1.0.0)\n" ++ "sample\n" ++ " |- b (b >= 0.0.5)\n" ++ " |- json (json ~1.0.4)" test_detectsSecondaryOnFirstActivation :: Prop test_detectsSecondaryOnFirstActivation = showResult result -=- expectedMessage where result = resolve defaultConfig pkg db pkg = cPackage "sample" (0, 0, 1, Nothing) [cDep "a" "= 0.0.1", cDep "b" "> 0.0.1"] a001 = cPackage "a" (0, 0, 1, Nothing) [cDep "b" "= 0.0.1"] b001 = cPackage "b" (0, 0, 1, Nothing) [] b002 = cPackage "b" (0, 0, 2, Nothing) [] db = cDB [pkg, a001, b001, b002] expectedMessage = "There was a conflict for package b\n" ++ "sample\n" ++ " |- a (a = 0.0.1)\n" ++ " |- b (b = 0.0.1)\n" ++ "sample\n" ++ " |- b (b > 0.0.1)" test_makesDecisionBetweenAlternatives :: Prop test_makesDecisionBetweenAlternatives = maybeResolvedPackages (resolve defaultConfig pkg db) -=- Just [json150, pkg] where pkg = cPackage "sample" (0, 0, 1, Nothing) [cDep "json" "> 1.0.0, < 2.0.0 || >= 4.0.0"] json150 = cPackage "json" (1, 5, 0, Nothing) [] json320 = cPackage "json" (3, 2, 0, Nothing) [] db = cDB [json150, json320] test_alwaysChoosesNewestAlternative :: Prop test_alwaysChoosesNewestAlternative = maybeResolvedPackages (resolve defaultConfig pkg db) -=- Just [json420, pkg] where pkg = cPackage "sample" (0, 0, 1, Nothing) [cDep "json" "> 1.0.0, < 2.0.0 || >= 4.0.0"] json150 = cPackage "json" (1, 5, 0, Nothing) [] json420 = cPackage "json" (4, 2, 0, Nothing) [] db = cDB [json150, json420] test_doesNotChoosePrereleaseByDefault :: Prop test_doesNotChoosePrereleaseByDefault = maybeResolvedPackages (resolve defaultConfig pkg db) -=- Just [b109, pkg] where pkg = cPackage "A" (0, 0, 1, Nothing) [cDep "B" ">= 1.0.0"] b109 = cPackage "B" (1, 0, 9, Nothing) [] b110b1 = cPackage "B" (1, 1, 0, Just "b1") [] db = cDB [b109, b110b1] test_upgradesPackageToPrereleaseWhenNeccesary :: Prop test_upgradesPackageToPrereleaseWhenNeccesary = maybeResolvedPackages (resolve defaultConfig pkg db) -=- Just [b110b1, c, pkg] where pkg = cPackage "A" (0, 0, 1, Nothing) [cDep "C" "= 1.2.0"] b109 = cPackage "B" (1, 0, 9, Nothing) [] b110b1 = cPackage "B" (1, 1, 0, Just "b1") [] c = cPackage "C" (1, 2, 0, Nothing) [cDep "B" ">= 1.1.0-b1"] db = cDB [b109, b110b1, c] test_prefersLocalPackageCacheEvenIfOlder :: Prop test_prefersLocalPackageCacheEvenIfOlder = maybeResolvedPackages (resolve defaultConfig pkg db) -=- Just [b101, pkg] where pkg = cPackage "A" (0, 0, 1, Nothing) [cDep "B" ">= 1.0.0"] b101 = cPackage "B" (1, 0, 1, Nothing) [] b105 = cPackage "B" (1, 0, 5, Nothing) [] db = addPackage (addPackage emptySet b101 FromLocalCache) b105 FromRepository test_reportsCompilerIncompatibility :: Prop test_reportsCompilerIncompatibility = showResult result -=- "The package json-1.0.0, dependency constraint json = 1.0.0, is not compatible to the current compiler. It was activated because:\nPackage dependencies:\nsample\n |- json (json = 1.0.0)" where result = resolve defaultConfig pkg db pkg = cPackage "sample" (0, 0, 1, Nothing) [cDep "json" "= 1.0.0"] json = cPackageCC "json" (1, 0, 0, Nothing) [cCC "nocompiler" "= 1.0.0"] db = cDB [json] cPackage :: String -> Version -> [Dependency] -> Package cPackage p v ds = emptyPackage { name = p , version = v , author = ["author"] , synopsis = "JSON library for Curry" , dependencies = ds , maintainer = [] , description = Nothing , license = Nothing , licenseFile = Nothing , copyright = Nothing , homepage = Nothing , bugReports = Nothing , repository = Nothing , compilerCompatibility = [] , source = Nothing , exportedModules = [] } cPackageCC :: String -> Version -> [CompilerCompatibility] -> Package cPackageCC p v cs = emptyPackage { name = p , version = v , author = ["author"] , synopsis = "JSON library for Curry" , dependencies = [] , maintainer = [] , description = Nothing , license = Nothing , licenseFile = Nothing , copyright = Nothing , homepage = Nothing , bugReports = Nothing , repository = Nothing , compilerCompatibility = cs , source = Nothing , exportedModules = [] } cDisj :: String -> Disjunction cDisj = fromJust . readVersionConstraints cDep :: String -> String -> Dependency cDep p c = Dependency p (fromJust $ readVersionConstraints c) cCC :: String -> String -> CompilerCompatibility cCC p c = CompilerCompatibility p (fromJust $ readVersionConstraints c) cDB :: [Package] -> LookupSet cDB ps = addPackages emptySet ps FromRepository