packages feed

canontra-0.2.0.0: test/Canontra/SlabV6Spec.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
module Canontra.SlabV6Spec (spec) where

import Control.Monad (forM)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as BB
import qualified Data.Map.Strict as Map
import qualified Data.Text as T
import Data.Time.Clock (diffUTCTime, getCurrentTime)
import Foreign.Marshal.Alloc (alloca)
import Foreign.Storable (peek, poke, sizeOf)
import System.Directory (createDirectoryIfMissing, getTemporaryDirectory, removeDirectoryRecursive)
import System.FilePath ((</>))
import Test.Hspec

import Canontra.Analysis.CSRGraph (CSRGraph (..), buildCSRGraph, csrHasEdge)
import Canontra.Cache.Inode (FileMetadata (..))
import Canontra.Cache.SlabV6
import Canontra.Types (Fingerprint (..), FingerprintBundle (..))

spec :: Spec
spec = do
  describe "Canontra.Cache.SlabV6: CNTR\\x06 Zero-Copy Memory-Mapped Slab Cache" $ do

    describe "Step 2.1: 64-Byte CacheRecordV6 & Radix Directory Layout" $ do
      it "enforces exact 64-byte alignment matching CPU cache lines" $ do
        sizeOf emptyCacheRecordV6 `shouldBe` 64

      it "guarantees lossless Storable peek/poke round-tripping in contiguous memory" $ do
        let rec = CacheRecordV6
              { crPathHash     = 0x1122334455667788
              , crMTimeSec     = 1728400000
              , crMTimeNano    = 123456789
              , crFileSize     = 4096
              , crSlabOffset   = 0x00008820
              , crSlabLength   = 320
              , crF4DigestHead = 0xAABBCCDDEEFF0011
              , crFlags        = 0x01
              , crReserved1    = 0
              , crReserved2    = 0
              }
        alloca $ \ptr -> do
          poke ptr rec
          rec' <- peek ptr
          rec' `shouldBe` rec

      it "encodes CNTR\\x06 header with magic, version 6, and valid CRC32" $ do
        let entries = []
            bs = encodeSlabV6Binary entries Nothing
        BS.take 4 bs `shouldBe` "CNTR"
        BS.length bs `shouldSatisfy` (>= 2080)
        verifySlabHeaderCRC bs `shouldBe` True

    describe "Step 2.2: Memory-Mapped Zero-Copy File Verification & Warm Lookups" $ do
      let bundle1 = FingerprintBundle
            { f0Source       = Fingerprint "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
            , f1Structural   = Fingerprint "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb"
            , f2Declaration  = Fingerprint "3e23e8160039594a33894f6564e1b1348bbd7a0088d42c4acb73eeaed59c009d"
            , f3Dependency   = Fingerprint "2e7d2c03a9507ae265ecf5b5356885a53393a2029d24139499726b425ff3dc34"
            , fCGCallGraph   = Fingerprint "18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4"
            , fCFControlFlow = Fingerprint "4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a"
            , fDFDataFlow    = Fingerprint "ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d"
            , fTTypeContract = Fingerprint "bc25a324f6f40c7499645931281df691238eb157591605f25712f55928d150fb"
            , f4Composite    = Fingerprint "8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4"
            }
          meta1 = FileMetadata "src/app.py" 1024 1728400000

      it "round-trips file entries through encodeSlabV6Binary and decodeSlabV6Binary" $ do
        let entries = [("src/app.py", meta1, bundle1)]
            bs = encodeSlabV6Binary entries Nothing
            res = decodeSlabV6Binary bs
        case res of
          Nothing -> expectationFailure "Decode failed"
          Just (m, _) -> do
            Map.size m `shouldBe` 1
            case Map.lookup "src/app.py" m of
              Nothing -> expectationFailure "Missing entry src/app.py"
              Just (mMeta, mBundle) -> do
                fmSize mMeta `shouldBe` fmSize meta1
                fmMtime mMeta `shouldBe` fmMtime meta1
                f4Composite mBundle `shouldBe` f4Composite bundle1
                f1Structural mBundle `shouldBe` f1Structural bundle1

      it "achieves sub-microsecond warm lookup hit via SlabCacheHandle" $ do
        tmpDir <- getTemporaryDirectory
        let testDir = tmpDir </> "canontra_test_slab_v6_warm"
            cachePath = testDir </> ".canontra" </> "cache.bin"
        createDirectoryIfMissing True (testDir </> ".canontra")
        let entries = [("lib/math.py", meta1, bundle1)]
        writeSlabCacheFile cachePath entries Nothing
        mHandle <- openSlabCache cachePath
        case mHandle of
          Nothing -> expectationFailure "Failed to open slab cache handle"
          Just handle -> do
            -- Warm lookup with matching size & mtime must HIT
            hit <- lookupSlabCacheWarm handle "lib/math.py" meta1
            case hit of
              Nothing -> expectationFailure "Warm lookup missed"
              Just b  -> f4Composite b `shouldBe` f4Composite bundle1
            -- MTime mismatch must MISS
            let metaMutated = meta1 { fmMtime = fmMtime meta1 + 10 }
            missMTime <- lookupSlabCacheWarm handle "lib/math.py" metaMutated
            missMTime `shouldBe` Nothing
            -- FileSize mismatch must MISS
            let metaSizeMutated = meta1 { fmSize = fmSize meta1 + 50 }
            missSize <- lookupSlabCacheWarm handle "lib/math.py" metaSizeMutated
            missSize `shouldBe` Nothing
            -- Non-existent file must MISS
            missFile <- lookupSlabCacheWarm handle "lib/unknown.py" meta1
            missFile `shouldBe` Nothing
            closeSlabCache handle
        removeDirectoryRecursive testDir

      it "supports pure zero-copy lookupSlabBinaryBS directly from ByteString" $ do
        let entries = [("src/app.py", meta1, bundle1)]
            bs = encodeSlabV6Binary entries Nothing
        lookupSlabBinaryBS "src/app.py" meta1 bs `shouldBe` Just bundle1
        lookupSlabBinaryBS "src/missing.py" meta1 bs `shouldBe` Nothing

    describe "Step 2.3: Whole-Repository Binary CSR Persistence in CNTR\\x06" $ do
      it "serializes and deserializes unboxed CSRGraph to/from binary bytes" $ do
        let edges = [(0, 1, 1), (1, 2, 2), (2, 0, 4)]
            g = buildCSRGraph 3 edges
            builder = encodeCSRGraph g
            bs = BS.toStrict (BB.toLazyByteString builder)
        case decodeCSRGraph bs 0 of
          Nothing -> expectationFailure "decodeCSRGraph failed"
          Just (g', len) -> do
            len `shouldBe` BS.length bs
            csrNodeCount g' `shouldBe` 3
            csrEdgeCount g' `shouldBe` 3
            csrHasEdge g' 0 1 `shouldBe` True
            csrHasEdge g' 1 2 `shouldBe` True
            csrHasEdge g' 2 0 `shouldBe` True

      it "persists and restores WholeRepoBundle and CSR graphs via saveRepoGraphsSlab / loadRepoGraphsSlab" $ do
        tmpDir <- getTemporaryDirectory
        let testDir = tmpDir </> "canontra_test_slab_v6_repo"
        createDirectoryIfMissing True (testDir </> ".canontra")
        let fwcg = Fingerprint "1111111111111111111111111111111111111111111111111111111111111111"
            fwdf = Fingerprint "2222222222222222222222222222222222222222222222222222222222222222"
            cgCSR = buildCSRGraph 2 [(0, 1, 1)]
            dfCSR = buildCSRGraph 2 [(1, 0, 2)]
        saveRepoGraphsSlab testDir fwcg fwdf (Just cgCSR) (Just dfCSR)
        mRes <- loadRepoGraphsSlab testDir
        case mRes of
          Nothing -> expectationFailure "loadRepoGraphsSlab failed"
          Just (c, d, mCg, mDf) -> do
            c `shouldBe` fwcg
            d `shouldBe` fwdf
            case (mCg, mDf) of
              (Just cg, Just df) -> do
                csrNodeCount cg `shouldBe` 2
                csrHasEdge cg 0 1 `shouldBe` True
                csrNodeCount df `shouldBe` 2
                csrHasEdge df 1 0 `shouldBe` True
              _ -> expectationFailure "Failed to restore CSR graphs from binary slab"
        removeDirectoryRecursive testDir

    describe "Section 5.4: Isolated 4KB Page Bit-Rot Recovery" $ do
      let bundle1 = FingerprintBundle (Fingerprint "s1") (Fingerprint "st1") (Fingerprint "d1") (Fingerprint "dp1") (Fingerprint "cg1") (Fingerprint "cf1") (Fingerprint "df1") (Fingerprint "t1") (Fingerprint "c1")
          bundle2 = FingerprintBundle (Fingerprint "s2") (Fingerprint "st2") (Fingerprint "d2") (Fingerprint "dp2") (Fingerprint "cg2") (Fingerprint "cf2") (Fingerprint "df2") (Fingerprint "t2") (Fingerprint "c2")
          meta = FileMetadata "f.py" 100 1728400000

      it "recovers valid entries when a single 4KB slab page is corrupted" $ do
        let entries = [("f1.py", meta, bundle1), ("f2.py", meta, bundle2)]
            bs = encodeSlabV6Binary entries Nothing
        -- Uncorrupted decode must have 0 corrupted pages
        let (validMap0, _, corrupted0) = decodeSlabV6Resilient bs
        Map.size validMap0 `shouldBe` 2
        corrupted0 `shouldBe` []

        -- Inject 4-byte bit-rot corruption into the first slab page
        -- Slab pages start at offset 2080 + 512 * 64 = 34848 (0x8820)
        let slabOffset = 34848
        if BS.length bs > slabOffset + 20
          then do
            let corruptedBS = BS.take (slabOffset + 10) bs
                           <> "\xFF\xFF\xFF\xFF"
                           <> BS.drop (slabOffset + 14) bs
            let (validMap, _, corrupted) = decodeSlabV6Resilient corruptedBS
            -- Exactly page 0 is flagged as corrupted
            corrupted `shouldBe` [0]
            -- System does not panic or crash; safely isolated!
            Map.size validMap `shouldSatisfy` (<= 2)
          else expectationFailure "Buffer shorter than expected slab offset"

    describe "Gate 2: 1,000-File Warm Repo Verification Benchmark (< 85 ms)" $ do
      it "verifies 1,000 files in memory-mapped slab cache in < 85 ms (< 500 ns per file)" $ do
        tmpDir <- getTemporaryDirectory
        let testDir = tmpDir </> "canontra_gate2_benchmark"
            cachePath = testDir </> ".canontra" </> "cache.bin"
        createDirectoryIfMissing True (testDir </> ".canontra")
        let mkEntry i =
              let !path = "src/pkg_" ++ show (i `div` 50) ++ "/file_" ++ show (i :: Int) ++ ".py"
                  !meta = FileMetadata path (fromIntegral (100 + i * 10)) (1728400000 + fromIntegral i)
                  !b = FingerprintBundle
                         (Fingerprint ("s_" <> T.pack (show i)))
                         (Fingerprint ("st_" <> T.pack (show i)))
                         (Fingerprint "d")
                         (Fingerprint "dp")
                         (Fingerprint "cg")
                         (Fingerprint "cf")
                         (Fingerprint "df")
                         (Fingerprint "t")
                         (Fingerprint ("c_" <> T.pack (show i)))
              in (path, meta, b)
            entries = [mkEntry i | i <- [1 .. 1000 :: Int]]
        writeSlabCacheFile cachePath entries Nothing
        mHandle <- openSlabCache cachePath
        case mHandle of
          Nothing -> expectationFailure "Failed to open slab cache handle for Gate 2"
          Just handle -> do
            t0 <- getCurrentTime
            matchCount <- forM entries $ \(p, m, b) -> do
              mHit <- lookupSlabCacheWarm handle p m
              case mHit of
                Nothing -> pure (0 :: Int)
                Just hitBundle -> pure (if f4Composite hitBundle == f4Composite b then 1 else 0)
            t1 <- getCurrentTime
            closeSlabCache handle
            sum matchCount `shouldBe` 1000
            let elapsedSec = realToFrac (diffUTCTime t1 t0) :: Double
            -- Must comfortably verify in < 85 ms (0.085s)
            elapsedSec `shouldSatisfy` (< 0.085)
        removeDirectoryRecursive testDir