-- | Tests for detecting space leaks in the dependency solver.
module UnitTests.Distribution.Solver.Modular.MemoryUsage (tests) where

import Test.Tasty (TestTree)

import UnitTests.Distribution.Solver.Modular.DSL
import UnitTests.Distribution.Solver.Modular.DSL.TestCaseUtils

tests :: [TestTree]
tests = [
      runTest $ basicTest "basic space leak test"
    , runTest $ flagsTest "package with many flags"
    , runTest $ issue2899 "issue #2899"
    , runTest $ duplicateDependencies "duplicate dependencies"
    , runTest $ duplicateFlaggedDependencies "duplicate flagged dependencies"
    ]

-- | This test solves for n packages that each have two versions. There is no
-- solution, because the nth package depends on another package that doesn't fit
-- its version constraint. Backjumping is disabled, so the solver must explore a
-- search tree of size 2^n. It should fail if memory usage is proportional to
-- the size of the tree.
basicTest :: String -> SolverTest
basicTest name =
    disableBackjumping $ mkTest pkgs name ["target"] anySolverFailure
  where
    n :: Int
    n = 18

    pkgs :: ExampleDb
    pkgs = map Right $
           [ exAv "target" 1 [ExAny $ pkgName 1]]
        ++ [ exAv (pkgName i) v [ExRange (pkgName $ i + 1) 2 4]
           | i <- [1..n], v <- [2, 3]]
        ++ [exAv (pkgName $ n + 1) 1 []]

    pkgName :: Int -> ExamplePkgName
    pkgName x = "pkg-" ++ show x

-- | This test is similar to 'basicTest', except that it has one package with n
-- flags, flag-1 through flag-n. The solver assigns flags in order, so it
-- doesn't discover the unknown dependencies under flag-n until it has assigned
-- all of the flags. It has to explore the whole search tree.
flagsTest :: String -> SolverTest
flagsTest name =
    disableBackjumping $
    goalOrder orderedFlags $ mkTest pkgs name ["pkg"] anySolverFailure
  where
    n :: Int
    n = 16

    pkgs :: ExampleDb
    pkgs = [Right $ exAv "pkg" 1 $
                [exFlagged (numberedFlag n) [ExAny "unknown1"] [ExAny "unknown2"]]

                -- The remaining flags have no effect:
             ++ [exFlagged (numberedFlag i) [] [] | i <- [1..n - 1]]
           ]

    orderedFlags :: [ExampleVar]
    orderedFlags = [F QualNone "pkg" (numberedFlag i) | i <- [1..n]]

-- | Test for a space leak caused by sharing of search trees under packages with
-- link choices (issue #2899).
--
-- The goal order is fixed so that the solver chooses setup-dep and then
-- target-setup.setup-dep at the top of the search tree. target-setup.setup-dep
-- has two choices: link to setup-dep, and don't link to setup-dep. setup-dep
-- has a long chain of dependencies (pkg-1 through pkg-n). However, pkg-n
-- depends on pkg-n+1, which doesn't exist, so there is no solution. Since each
-- dependency has two versions, the solver must try 2^n combinations when
-- backjumping is disabled. These combinations create large search trees under
-- each of the two choices for target-setup.setup-dep. Although the choice to
-- not link is disallowed by the Single Instance Restriction, the solver doesn't
-- know that until it has explored (and evaluated) the whole tree under the
-- choice to link. If the two trees are shared, memory usage spikes.
issue2899 :: String -> SolverTest
issue2899 name =
    disableBackjumping $
    goalOrder goals $ mkTest pkgs name ["target"] anySolverFailure
  where
    n :: Int
    n = 16

    pkgs :: ExampleDb
    pkgs = map Right $
           [ exAv "target" 1 [ExAny "setup-dep"] `withSetupDeps` [ExAny "setup-dep"]
           , exAv "setup-dep" 1 [ExAny $ pkgName 1]]
        ++ [ exAv (pkgName i) v [ExAny $ pkgName (i + 1)]
           | i <- [1..n], v <- [1, 2]]

    pkgName :: Int -> ExamplePkgName
    pkgName x = "pkg-" ++ show x

    goals :: [ExampleVar]
    goals = [P QualNone "setup-dep", P (QualSetup "target") "setup-dep"]

-- | Test for an issue related to lifting dependencies out of conditionals when
-- converting a PackageDescription to the solver's internal representation.
--
-- Issue:
-- For each conditional and each package B, the solver combined each dependency
-- on B in the true branch with each dependency on B in the false branch. It
-- added the combined dependencies to the build-depends outside of the
-- conditional. Since dependencies could be lifted out of multiple levels of
-- conditionals, the number of new dependencies could grow exponentially in the
-- number of levels. For example, the following package generated 4 copies of B
-- under flag-2=False, 8 copies under flag-1=False, and 16 copies at the top
-- level:
--
-- if flag(flag-1)
--   build-depends: B, B
-- else
--   if flag(flag-2)
--     build-depends: B, B
--   else
--     if flag(flag-3)
--       build-depends: B, B
--     else
--       build-depends: B, B
--
-- This issue caused the quickcheck tests to start frequently running out of
-- memory after an optimization that pruned unreachable branches (See PR #4929).
-- Each problematic test case contained at least one build-depends field with
-- duplicate dependencies, which was then duplicated under multiple levels of
-- conditionals by the solver's "buildable: False" transformation, when
-- "buildable: False" was under multiple flags. Finally, the branch pruning
-- feature put all build-depends fields in consecutive levels of the condition
-- tree, causing the solver's representation of the package to follow the
-- pattern in the example above.
--
-- Now the solver avoids this issue by combining all dependencies on the same
-- package before lifting them out of conditionals.
--
-- This test case is an expanded version of the example above, with library and
-- build-tool dependencies.
duplicateDependencies :: String -> SolverTest
duplicateDependencies name =
    mkTest pkgs name ["A"] $ solverSuccess [("A", 1), ("B", 1)]
  where
    copies, depth :: Int
    copies = 50
    depth = 50

    pkgs :: ExampleDb
    pkgs = [
        Right $ exAv "A" 1 (dependencyTree 1)
      , Right $ exAv "B" 1 [] `withExe` ExExe "exe" []
      ]

    dependencyTree :: Int -> [ExampleDependency]
    dependencyTree n
        | n > depth = buildDepends
        | otherwise = [exFlagged (numberedFlag n) buildDepends
                                                  (dependencyTree (n + 1))]
      where
        buildDepends = replicate copies (ExFix "B" 1)
                    ++ replicate copies (ExBuildToolFix "B" "exe" 1)

-- | This test is similar to duplicateDependencies, except that every dependency
-- on B is replaced by a conditional that contains B in both branches. It tests
-- that the solver doesn't just combine dependencies within one build-depends or
-- build-tool-depends field; it also needs to combine dependencies after they
-- are lifted out of conditionals.
duplicateFlaggedDependencies :: String -> SolverTest
duplicateFlaggedDependencies name =
    mkTest pkgs name ["A"] $ solverSuccess [("A", 1), ("B", 1)]
  where
    copies, depth :: Int
    copies = 15
    depth = 15

    pkgs :: ExampleDb
    pkgs = [
        Right $ exAv "A" 1 (dependencyTree 1)
      , Right $ exAv "B" 1 [] `withExe` ExExe "exe" []
      ]

    dependencyTree :: Int -> [ExampleDependency]
    dependencyTree n
        | n > depth = flaggedDeps
        | otherwise = [exFlagged (numberedFlag n) flaggedDeps
                                                  (dependencyTree (n + 1))]
      where
        flaggedDeps = zipWith ($) (replicate copies flaggedDep) [0 :: Int ..]
        flaggedDep m = exFlagged (numberedFlag n ++ "-" ++ show m) buildDepends
                                                                   buildDepends
        buildDepends = [ExFix "B" 1, ExBuildToolFix "B" "exe" 1]

numberedFlag :: Int -> ExampleFlagName
numberedFlag n = "flag-" ++ show n
