CurryInfo: cpm-3.3.0 / CPM.Resolution

classes:

              
documentation:
--------------------------------------------------------------------------------
--- This module contains the dependency resolution algorithm.
--------------------------------------------------------------------------------
name:
CPM.Resolution
operations:
allTransitiveDependencies dependenciesAsGraph isCompatibleToCompiler isDisjunctionCompatible resolutionSuccess resolve resolveDependenciesFromLookupSet resolvedPackages showConflict showDependencies showResult showShortDependencies showShortResult transitiveDependencies
sourcecode:
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
types:
ResolutionResult
unsafe:
safe