packages feed

elm-package-0.2: tests/SolverTests.hs

module SolverTests (solverTests) where

import Control.Monad.Identity
import Control.Monad.Error
import Control.Monad.Reader
import Control.Monad.State
import Test.HUnit ((@?), Assertion)
import qualified Test.Framework as TF
import qualified Test.Framework.Providers.HUnit as TH
import qualified Utils.ResolveDeps as Deps
import Data.Map (Map)
import qualified Data.Map as Map
import qualified Elm.Internal.Constraint as C
import qualified Elm.Internal.Version as V
import qualified Elm.Internal.Name as N


-- SETUP

type FakeDB = Map N.Name [(V.Version, Deps.Constraints)]

db1 :: FakeDB
db1 = Map.fromList [ base, transformers, mtl, conduit, http ]
  where
    base =
      (n "base", [ (v "0.1", []), (v "0.2", []), (v "1.0", []), (v "1.1", []) ])

    transformers =
      (n "transformers", [ (v "1.0", [(n "base", c ">=1.0 <2.0")]) ])

    mtl =
      (n "mtl", [ (v "1.0", [ (n "base", c ">=0.2 <1.0") ])
                , (v "2.0", [ (n "base", c ">=0.2 <2.0")
                            , (n "transformers", c ">=1.0 <2.0")
                            ])
                ])

    conduit =
      (n "conduit", [ (v "1.0", [ (n "base", c ">=1.0 <1.1")
                                , (n "http", c ">=2.0 <3.0")
                                ])
                    ])

    http =
      (n "http", [ (v "2.1", [ (n "base", c ">=1.1 <2.0") ]) ])

expectSolution :: FakeDB -> N.Name -> V.Version -> Assertion
expectSolution db name version =
  do solution <- fromError $ solveFake db name version
     isValidSolution db solution @? "Solution should pass sanity check"

expectNoSolution :: FakeDB -> N.Name -> V.Version -> Assertion
expectNoSolution db name version =
  let solution = runIdentity $ runErrorT $ solveFake db name version
  in case solution of
       Left err -> return ()
       Right sol ->
         do when (isValidSolution db sol) $ putStrLn "FAULTY TESTS: solution is valid!"
            False @? ("Unexpected solution " ++ show sol)

test1 = expectSolution db1 (n "mtl") (v "1.0")
test2 = expectSolution db1 (n "mtl") (v "2.0")
test3 = expectNoSolution db1 (n "conduit") (v "1.0")

solverTests =
  TF.testGroup "Dependency solver tests"
  [ TH.testCase "test1" test1
  , TH.testCase "test2" test2
  , TH.testCase "test3" test3
  ]


fromError :: ErrorT String Identity a -> IO a
fromError action =
  either fail return $ runIdentity (runErrorT action)

-- CHECK FOR VALIDITY OF SOLUTIONS

{-| Check whether given solution is really a solution. Supposed to use as
a sanity check for existing tests and solver solutions
-}
isValidSolution :: FakeDB -> [(N.Name, V.Version)] -> Bool
isValidSolution db solution =
    all isConsistent solution
  where
    isConsistent (name, version) =
      maybe False id $ do
        versions <- Map.lookup name db
        constraints <- lookup version versions
        return (all isSatisfied constraints)

    isSatisfied (name, constraint) =
      maybe False id $ do
        version <- lookup name solution
        return (C.satisfyConstraint constraint version)


-- RUN SOLVER

-- | Run dependency solver using stub data
solveFake :: FakeDB -> N.Name -> V.Version -> ErrorT String Identity [(N.Name, V.Version)]
solveFake db name version =
  do let libraryDb = toLibraryDb db
         unreader = runReaderT (Deps.solveForVersion name version) $
                    Deps.SolverEnv libraryDb (readConstraints db)
         initialState = Deps.SolverState Map.empty
     (solved, _) <- runStateT unreader initialState
     return $ Map.toList solved

-- | A function passed to solver which "reads" constraints by name and version
readConstraints :: FakeDB -> N.Name -> V.Version -> ErrorT String Identity Deps.Constraints
readConstraints db name version =
    maybe notFound return $ do
      versions <- Map.lookup name db
      lookup version versions
  where
    notFound =
      throwError $ "Could not find " ++ N.toString name ++ " " ++ V.toString version

{-| Extract from stub data list of libraries and their version in
format solver expects
-}
toLibraryDb :: FakeDB -> Deps.LibraryDB
toLibraryDb fakeDb =
    Map.fromList (map toLibraryEntry (Map.toList fakeDb))
  where
    toLibraryEntry (name, details) =
      ( N.toString name
      , Deps.LibraryInfo (N.toString name) "" (map fst details)
      )


-- SETUP HELPERS
-- make it easier to create a FakeDb

v :: String -> V.Version
v = unsafeUnpackJust "version" . V.fromString

c :: String -> C.Constraint
c = unsafeUnpackJust "constraint" . C.fromString

n :: String -> N.Name
n = unsafeUnpackJust "name" . N.fromString . ("a/" ++)

unsafeUnpackJust :: String -> Maybe a -> a
unsafeUnpackJust msg result =
  case result of
    Just v -> v
    Nothing -> error $ "error unpacking " ++ msg ++ " from string"