packages feed

hydra-0.15.0: src/main/haskell/Hydra/Sources/Test/Sorting.hs

-- | Test cases for topological sorting algorithms
--
-- Note: This module supersedes the Haskell-specific Hydra.SortingSpec tests.
-- Historical note: The expected orderings changed when the algorithm was updated
-- to produce a canonical ordering (sorted by node value within each SCC).
module Hydra.Sources.Test.Sorting where

-- Standard imports for shallow DSL tests
import Hydra.Kernel
import Hydra.Dsl.Meta.Testing                 as Testing
import Hydra.Sources.Kernel.Types.All
import qualified Hydra.Dsl.Meta.Core          as Core
import           Hydra.Dsl.Meta.Phantoms      as Phantoms hiding ((++))
import qualified Hydra.Dsl.Meta.Types         as T
import qualified Hydra.Sources.Test.TestGraph as TestGraph
import qualified Hydra.Sources.Test.TestTerms as TestTerms
import qualified Hydra.Sources.Test.TestTypes as TestTypes
import qualified Data.List                    as L
import qualified Data.Map                     as M

import Hydra.Testing
import Hydra.Sources.Libraries
import qualified Hydra.Sources.Kernel.Terms.Sorting as SortingModule
import qualified Hydra.Sources.Kernel.Terms.Show.Core as ShowCore
import qualified Hydra.Dsl.Meta.Lib.Eithers  as Eithers
import qualified Hydra.Dsl.Meta.Lib.Lists    as Lists
import qualified Hydra.Dsl.Meta.Lib.Literals as Literals
import qualified Hydra.Dsl.Meta.Lib.Strings  as Strings

import qualified Data.Int as I


-- Note: We use Int for input types in helpers because int32 expects Int
-- and produces TTerm I.Int32. The test data literals (1, 2, 3) are polymorphic.

ns :: Namespace
ns = Namespace "hydra.test.sorting"

module_ :: Module
module_ = Module {
            moduleNamespace = ns,
            moduleDefinitions = definitions,
            moduleTermDependencies = [SortingModule.ns, ShowCore.ns],
            moduleTypeDependencies = kernelTypesNamespaces,
            moduleDescription = (Just "Test cases for topological sorting algorithms")}
  where
    definitions = [Phantoms.toDefinition allTests]

define :: String -> TTerm a -> TTermDefinition a
define = definitionInModule module_

allTests :: TTermDefinition TestGroup
allTests = define "allTests" $
    doc "Test cases for topological sorting" $
    supergroup "sorting" [
      topologicalSortGroup,
      topologicalSortSCCGroup]

-- | Show a list of Int32 as a string like "[1, 2, 3]"
showIntList :: TTerm [Int] -> TTerm String
showIntList xs = ShowCore.list_ @@ unaryFunction Literals.showInt32 @@ xs

-- | Show a list of lists of Int32 as a string like "[[1, 2], [3]]"
showIntListList :: TTerm [[Int]] -> TTerm String
showIntListList xs = ShowCore.list_ @@ unaryFunction showIntList @@ xs

-- | Show Either [[Int]] [Int] as "left([[1, 2]])" or "right([1, 2, 3])"
showEitherResult :: TTerm (Either [[Int]] [Int]) -> TTerm String
showEitherResult = Eithers.either_
  (lambda "cs" (Strings.cat2 (string "left(") (Strings.cat2 (showIntListList (var "cs")) (string ")"))))
  (lambda "xs" (Strings.cat2 (string "right(") (Strings.cat2 (showIntList (var "xs")) (string ")"))))

-- Helper to create adjacency list
adj :: [(Int, [Int])] -> TTerm [(Int, [Int])]
adj pairs = list [pair (int32 n) (list (int32 <$> deps)) | (n, deps) <- pairs]

-- Universal sort test case
sortCase :: String -> TTerm [(Int, [Int])] -> TTerm (Either [[Int]] [Int]) -> TTerm TestCaseWithMetadata
sortCase cname adjList expected =
  universalCase cname
    (showEitherResult (SortingModule.topologicalSort @@ adjList))
    (showEitherResult expected)

-- Universal SCC test case
sortSCCCase :: String -> TTerm [(Int, [Int])] -> TTerm [[Int]] -> TTerm TestCaseWithMetadata
sortSCCCase cname adjList expected =
  universalCase cname
    (showIntListList (SortingModule.topologicalSortComponents @@ adjList))
    (showIntListList expected)

-- Helper for Right result (sorted list)
sorted :: [Int] -> TTerm (Either [[Int]] [Int])
sorted xs = right $ list (int32 <$> xs)

-- Helper for Left result (cycles)
cycles :: [[Int]] -> TTerm (Either [[Int]] [Int])
cycles cs = left $ list [list (int32 <$> c) | c <- cs]

-- Helper for SCC result
sccs :: [[Int]] -> TTerm [[Int]]
sccs cs = list [list (int32 <$> c) | c <- cs]

-- | Test cases for topological sort (without cycles)
topologicalSortGroup :: TTerm TestGroup
topologicalSortGroup = subgroup "topological sort" [
    -- Discrete sets (no dependencies)
    sortCase "empty set"
      (adj [])
      (sorted []),
    sortCase "singleton set"
      (adj [(1, [])])
      (sorted [1]),
    sortCase "discrete set with multiple elements"
      (adj [(3, []), (1, []), (2, [])])
      (sorted [1, 2, 3]),

    -- Trees and DAGs
    sortCase "linked list"
      (adj [(3, [1]), (2, [3]), (1, [])])
      (sorted [1, 3, 2]),
    sortCase "binary tree"
      (adj [(3, [1, 4]), (4, [6, 2]), (1, [5]), (2, []), (6, []), (5, [])])
      (sorted [5, 1, 2, 6, 4, 3]),
    sortCase "two trees"
      (adj [(3, [1, 4]), (5, [6, 2]), (2, [7]), (1, []), (4, []), (6, []), (7, [])])
      (sorted [1, 7, 2, 4, 3, 6, 5]),
    sortCase "diamond DAG"
      (adj [(1, [3, 4]), (3, [2]), (4, [2]), (2, [5]), (5, [])])
      (sorted [5, 2, 3, 4, 1]),

    -- Cycles (expected to fail)
    sortCase "two-node cycle"
      (adj [(1, [2]), (2, [1])])
      (cycles [[1, 2]]),
    sortCase "cycle with incoming and outgoing edges"
      (adj [(1, [3]), (3, [2]), (2, [3, 4]), (4, [5]), (5, [])])
      (cycles [[2, 3]])]

-- | Test cases for topological sort with strongly connected component detection
topologicalSortSCCGroup :: TTerm TestGroup
topologicalSortSCCGroup = subgroup "topological sort SCC" [
    -- Discrete sets
    sortSCCCase "empty set"
      (adj [])
      (sccs []),
    sortSCCCase "singleton set"
      (adj [(1, [])])
      (sccs [[1]]),
    sortSCCCase "discrete set with multiple elements"
      (adj [(3, []), (1, []), (2, [])])
      (sccs [[1], [2], [3]]),

    -- Weakly-connected components
    sortSCCCase "single two-element component #1"
      (adj [(1, [2]), (2, [])])
      (sccs [[2], [1]]),
    sortSCCCase "single two-element component #2"
      (adj [(2, [1]), (1, [])])
      (sccs [[1], [2]]),
    sortSCCCase "multiple-element component"
      (adj [(2, [1, 3]), (1, [3]), (3, [])])
      (sccs [[3], [1], [2]]),

    -- Strongly-connected components (cycles)
    sortSCCCase "cycle of two nodes #1"
      (adj [(1, [2]), (2, [1])])
      (sccs [[1, 2]]),
    sortSCCCase "cycle of two nodes #2"
      (adj [(2, [1]), (1, [2])])
      (sccs [[1, 2]]),
    sortSCCCase "cycle of three nodes #1"
      (adj [(1, [2]), (2, [3]), (3, [1])])
      (sccs [[1, 2, 3]]),
    sortSCCCase "cycle of three nodes #2"
      (adj [(2, [1]), (3, [2]), (1, [3])])
      (sccs [[1, 2, 3]]),
    sortSCCCase "multiple disconnected cycles"
      (adj ([(200, [])] ++ [(100, [])] ++ [(300, [])] ++ [(10, [20]), (20, [10])] ++ [(1, [2]), (2, [3]), (3, [1])]))
      (sccs [[1, 2, 3], [10, 20], [100], [200], [300]]),
    sortSCCCase "complex cycles"
      (adj [(1, [2, 3]), (2, [3]), (3, [1])])
      (sccs [[1, 2, 3]]),

    -- Mixed weakly- and strongly-connected components
    sortSCCCase "chain of three SCCs"
      (adj [(1, [2, 10]), (2, [3]), (3, [1]), (10, [20]), (20, [100, 10]), (100, [])])
      (sccs [[100], [10, 20], [1, 2, 3]]),
    sortSCCCase "SCCs with dependencies to/from non-SCC nodes"
      (adj [(1, [2, 3, 10]), (2, [3]), (3, [1]),
            (10, [20, 30]), (20, [30]), (30, []),
            (100, [200, 2]), (200, []), (300, [100]),
            (1000, []),
            (2000, [])])
      (sccs [[30], [20], [10], [1, 2, 3], [200], [100], [300], [1000], [2000]])]