canontra-0.1.0.0: test/Canontra/FastScanSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module Canontra.FastScanSpec (spec) where
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Test.Hspec
import Test.QuickCheck
import Canontra.Canonical.FastScan
( ScanResult (..)
, fastCanonicalizeBS
, fastCanonicalizeText
, isPureAsciiUnix
, scanAsciiAndLineEndings
)
import Canontra.Canonical.Unicode (canonicalizeText)
import Canontra.Fingerprint.Bundle (computeBundleFromSource)
import Canontra.Types (FingerprintBundle (..))
spec :: Spec
spec = do
describe "SWAR ASCII & Line-Ending Fast-Path Scanner" $ do
describe "Unit Scan Classification" $ do
it "classifies empty ByteString as PureAsciiUnix" $ do
scanAsciiAndLineEndings BS.empty `shouldBe` PureAsciiUnix
isPureAsciiUnix BS.empty `shouldBe` True
it "classifies pure ASCII Unix LF content as PureAsciiUnix" $ do
let src = "def add(a, b):\n return a + b\n"
scanAsciiAndLineEndings (BSC.pack src) `shouldBe` PureAsciiUnix
isPureAsciiUnix (BSC.pack src) `shouldBe` True
it "classifies ASCII with Windows CRLF as ContainsCRLF" $ do
let src = "def add(a, b):\r\n return a + b\r\n"
scanAsciiAndLineEndings (BSC.pack src) `shouldBe` ContainsCRLF
isPureAsciiUnix (BSC.pack src) `shouldBe` False
it "classifies ASCII with isolated CR as ContainsCRLF" $ do
let src = "def add(a, b):\r return a + b\r"
scanAsciiAndLineEndings (BSC.pack src) `shouldBe` ContainsCRLF
it "classifies non-ASCII UTF-8 bytes as RequiresUnicodeNFC" $ do
let utf8Src = TE.encodeUtf8 "def greet():\n return 'héllo wörld'\n"
scanAsciiAndLineEndings utf8Src `shouldBe` RequiresUnicodeNFC
it "classifies decomposed Unicode combining marks as RequiresUnicodeNFC" $ do
let decomposed = TE.encodeUtf8 "caf\x0065\x0301 = 42\n"
scanAsciiAndLineEndings decomposed `shouldBe` RequiresUnicodeNFC
it "accurately detects CR across boundary alignments (0 to 24 bytes)" $ do
-- Test CR at every possible byte offset
mapM_ (\offset -> do
let prefix = BSC.replicate offset 'a'
suffix = BSC.replicate (24 - offset) 'b'
withCR = prefix <> "\r" <> suffix
scanAsciiAndLineEndings withCR `shouldBe` ContainsCRLF
) [0 .. 24]
it "accurately detects non-ASCII bytes across boundary alignments (0 to 24 bytes)" $ do
-- Test non-ASCII byte (0xC3) at every possible byte offset
mapM_ (\offset -> do
let prefix = BS.replicate offset 0x61
suffix = BS.replicate (24 - offset) 0x62
withNonAscii = prefix <> BS.singleton 0xC3 <> suffix
scanAsciiAndLineEndings withNonAscii `shouldBe` RequiresUnicodeNFC
) [0 .. 24]
describe "Fast Canonicalization Equivalence" $ do
it "produces identical canonical text for PureAsciiUnix" $ do
let raw = BSC.pack "def process(items):\n return [x * 2 for x in items]\n"
fastCanonicalizeBS raw `shouldBe` canonicalizeText (TE.decodeUtf8Lenient raw)
it "produces identical canonical text for CRLF inputs" $ do
let raw = BSC.pack "def process(items):\r\n return [x * 2 for x in items]\r\n"
fastCanonicalizeBS raw `shouldBe` canonicalizeText (TE.decodeUtf8Lenient raw)
it "produces identical canonical text for decomposed Unicode" $ do
let raw = TE.encodeUtf8 "def calc():\n val = 'caf\x0065\x0301'\n return val\n"
fastCanonicalizeBS raw `shouldBe` canonicalizeText (TE.decodeUtf8Lenient raw)
it "fastCanonicalizeText is an exact identity on already-clean text" $ do
let cleanText = "def fn():\n return 1\n"
fastCanonicalizeText cleanText `shouldBe` cleanText
it "fastCanonicalizeText normalizes CRLF and decomposed characters" $ do
let crlfText = "def fn():\r\n return 'caf\x0065\x0301'\r\n"
fastCanonicalizeText crlfText `shouldBe` canonicalizeText crlfText
describe "End-to-End Fingerprint Invariance" $ do
it "preserves F0-F4 hash invariance across polyglot source code" $ do
let pySource = "def calculate(x: int, y: int) -> int:\n return x * 2 + y\n"
jsSource = "function calculate(x, y) {\n return x * 2 + y;\n}\n"
goSource = "package main\nfunc calculate(x int, y int) int {\n return x*2 + y\n}\n"
rsSource = "pub fn calculate(x: i32, y: i32) -> i32 {\n x * 2 + y\n}\n"
case ( computeBundleFromSource "calc.py" pySource
, computeBundleFromSource "calc.js" jsSource
, computeBundleFromSource "calc.go" goSource
, computeBundleFromSource "calc.rs" rsSource
) of
(Right pyB, Right jsB, Right goB, Right rsB) -> do
f0Source pyB `shouldNotBe` f1Structural pyB
f0Source jsB `shouldNotBe` f1Structural jsB
f0Source goB `shouldNotBe` f1Structural goB
f0Source rsB `shouldNotBe` f1Structural rsB
(Left e, _, _, _) -> expectationFailure (show e)
(_, Left e, _, _) -> expectationFailure (show e)
(_, _, Left e, _) -> expectationFailure (show e)
(_, _, _, Left e) -> expectationFailure (show e)
describe "Property-Based SWAR FastScan Invariants" $ do
it "Property: Pure ASCII with LF always classifies as PureAsciiUnix" $
property $ forAll (listOf (elements (['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ [' ', '\n', '\t', '_']))) $ \s ->
let bs = BSC.pack s
in scanAsciiAndLineEndings bs === PureAsciiUnix
it "Property: Pure ASCII with injected CR always classifies as ContainsCRLF" $
property $ forAll (listOf1 (elements (['a'..'z'] ++ ['0'..'9'] ++ [' ', '\n']))) $ \s ->
let withCR = BSC.pack (s ++ "\r" ++ s)
in scanAsciiAndLineEndings withCR === ContainsCRLF
it "Property: fastCanonicalizeBS is bit-identical to canonicalizeText . decodeUtf8Lenient" $
property $ forAll (listOf (choose (0, 255))) $ \bytes ->
let bs = BS.pack bytes
in fastCanonicalizeBS bs === canonicalizeText (TE.decodeUtf8Lenient bs)
it "Property: fastCanonicalizeText is bit-identical to canonicalizeText" $
property $ forAll (listOf (choose (minBound, maxBound))) $ \chars ->
let t = T.pack chars
in fastCanonicalizeText t === canonicalizeText t