packages feed

moonlight-delta-0.1.0.0: test/repair/RepairTests.hs

module RepairTests
  ( tests,
  )
where

import Data.List.NonEmpty (NonEmpty (..))
import Moonlight.Delta.Repair
import Numeric.Natural (Natural)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit ((@?=), testCase)

data RepairObstruction
  = BelowTarget Int
  | CannotRepair
  deriving stock (Eq, Show)

data RepairCorrectionValue
  = Increment
  | Noop
  deriving stock (Eq, Show)

tests :: TestTree
tests =
  testGroup
    "repair"
    [ convergenceTest,
      noProgressBudgetTest,
      budgetExhaustionTest,
      irreducibleTraceTest,
      foldTraceOrderingTest,
      focusRepairPreservesObstructedFocusStateTest,
      productRepairPreservesInspectedStateTest,
      sequenceRepairShortCircuitsLeftBeforeRightTest,
      sequenceRepairRunsRightAfterLeftConvergesTest,
      productRepairAccumulatesBothObstructionsInOrderTest
    ]

convergenceTest :: TestTree
convergenceTest =
  testCase "bounded repair converges" $
    boundedRepair (incrementKernel 2) (Config 4) 0
      @?= ResultConverged 2 2

noProgressBudgetTest :: TestTree
noProgressBudgetTest =
  testCase "no-op corrections exhaust budget instead of trusting a stable hash" $
    boundedRepair noProgressKernel (Config 2) 0
      @?= ResultBudgetExhausted 0 (BelowTarget 1 :| []) 2

budgetExhaustionTest :: TestTree
budgetExhaustionTest =
  testCase "repair reports budget exhaustion with current obstruction" $
    boundedRepair (incrementKernel 3) (Config 1) 0
      @?= ResultBudgetExhausted 1 (BelowTarget 3 :| []) 1

irreducibleTraceTest :: TestTree
irreducibleTraceTest =
  testCase "irreducible obstruction is traced as typed correction" $ do
    let (result, traceValue) = boundedRepairTraced irreducibleKernel (Config 4) 0
    result @?= ResultStuck 0 (CannotRepair :| []) 0
    traceProjection traceValue
      @?= [(CannotRepair :| [], Irreducible CannotRepair :| [], 0, [CannotRepair])]

foldTraceOrderingTest :: TestTree
foldTraceOrderingTest =
  testCase "trace preserves fold order" $
    traceProjection (snd (boundedRepairTraced (incrementKernel 2) (Config 4) 0))
      @?=
        [ (BelowTarget 2 :| [], Applied (BelowTarget 2) Increment :| [], 1, []),
          (BelowTarget 2 :| [], Applied (BelowTarget 2) Increment :| [], 1, [])
        ]

focusRepairPreservesObstructedFocusStateTest :: TestTree
focusRepairPreservesObstructedFocusStateTest =
  testCase "focusRepair embeds obstructed inner state" $
    check (focusRepair snd replaceFocus focusNormalizingKernel) ("outer", 0 :: Int)
      @?= StepObstructed ("outer", 1) (BelowTarget 1 :| [])

productRepairPreservesInspectedStateTest :: TestTree
productRepairPreservesInspectedStateTest =
  testCase "productRepair preserves inspection state instead of returning the original" $
    check (productRepair normalizingConvergedKernel normalizingObstructedKernel) (0 :: Int)
      @?= StepObstructed 11 (Right (BelowTarget 11) :| [])

sequenceRepairShortCircuitsLeftBeforeRightTest :: TestTree
sequenceRepairShortCircuitsLeftBeforeRightTest =
  testCase "SequenceRepairShortCircuitsLeftBeforeRight" $ do
    let (result, traceValue) =
          boundedRepairTraced
            (sequenceRepair leftObstructedIncrementKernel rightKernelMustNotRun)
            (Config 1)
            (0 :: Int)
    result @?= ResultBudgetExhausted 1 (Left (BelowTarget 3) :| []) 1
    traceProjection traceValue
      @?= [(Left (BelowTarget 3) :| [], Applied (Left (BelowTarget 3)) (Left Increment) :| [], 1, [])]

sequenceRepairRunsRightAfterLeftConvergesTest :: TestTree
sequenceRepairRunsRightAfterLeftConvergesTest =
  testCase "SequenceRepairRunsRightAfterLeftConverges" $
    check
      (sequenceRepair normalizingConvergedKernel normalizingObstructedKernel)
      (0 :: Int)
      @?= StepObstructed 11 (Right (BelowTarget 11) :| [])

productRepairAccumulatesBothObstructionsInOrderTest :: TestTree
productRepairAccumulatesBothObstructionsInOrderTest =
  testCase "ProductRepairAccumulatesBothObstructionsInOrder" $
    check
      (productRepair normalizingObstructedKernel normalizingObstructedKernel)
      (0 :: Int)
      @?= StepObstructed 20 (Left (BelowTarget 10) :| [Right (BelowTarget 20)])

incrementKernel :: Int -> Kernel Int RepairObstruction RepairCorrectionValue
incrementKernel target =
  Kernel
    { check = \state ->
        if state >= target
          then StepConverged state
          else StepObstructed state (BelowTarget target :| []),
      residuate = \obstruction ->
        case obstruction of
          BelowTarget _ -> Just Increment
          CannotRepair -> Nothing,
      applyKernelCorrection = \state correction ->
        case correction of
          Increment -> state + 1
          Noop -> state
    }

noProgressKernel :: Kernel Int RepairObstruction RepairCorrectionValue
noProgressKernel =
  Kernel
    { check = \state -> StepObstructed state (BelowTarget 1 :| []),
      residuate = const (Just Noop),
      applyKernelCorrection = \state correction ->
        case correction of
          Increment -> state + 1
          Noop -> state
    }

irreducibleKernel :: Kernel Int RepairObstruction RepairCorrectionValue
irreducibleKernel =
  Kernel
    { check = \state -> StepObstructed state (CannotRepair :| []),
      residuate = const Nothing,
      applyKernelCorrection = \state _ -> state
    }

replaceFocus :: (outer, focus) -> focus -> (outer, focus)
replaceFocus (outer, _) focus =
  (outer, focus)

focusNormalizingKernel :: Kernel Int RepairObstruction RepairCorrectionValue
focusNormalizingKernel =
  Kernel
    { check = \focus -> StepObstructed (focus + 1) (BelowTarget 1 :| []),
      residuate = const Nothing,
      applyKernelCorrection = \focus _ -> focus
    }

normalizingConvergedKernel :: Kernel Int RepairObstruction RepairCorrectionValue
normalizingConvergedKernel =
  Kernel
    { check = \state -> StepConverged (state + 1),
      residuate = const Nothing,
      applyKernelCorrection = \state _ -> state
    }

normalizingObstructedKernel :: Kernel Int RepairObstruction RepairCorrectionValue
normalizingObstructedKernel =
  Kernel
    { check = \state -> StepObstructed (state + 10) (BelowTarget (state + 10) :| []),
      residuate = const Nothing,
      applyKernelCorrection = \state _ -> state
    }

leftObstructedIncrementKernel :: Kernel Int RepairObstruction RepairCorrectionValue
leftObstructedIncrementKernel =
  Kernel
    { check = \state -> StepObstructed state (BelowTarget 3 :| []),
      residuate = \obstruction ->
        case obstruction of
          BelowTarget _ -> Just Increment
          CannotRepair -> Nothing,
      applyKernelCorrection = \state correction ->
        case correction of
          Increment -> state + 1
          Noop -> state
    }

rightKernelMustNotRun :: Kernel Int RepairObstruction RepairCorrectionValue
rightKernelMustNotRun =
  Kernel
    { check = \state -> StepObstructed state (CannotRepair :| []),
      residuate = const Nothing,
      applyKernelCorrection = \state _ -> state
    }

traceProjection ::
  Trace obstruction correction ->
  [(NonEmpty obstruction, NonEmpty (Correction obstruction correction), Natural, [obstruction])]
traceProjection =
  fmap (\roundValue -> (obstructions roundValue, corrections roundValue, applied roundValue, irreducible roundValue)) . rounds