packages feed

canontra-0.1.0.0: test/Canontra/FixtureSpec.hs

{- |
Module      : Canontra.FixtureSpec
Description : YAML-driven fixture validation against transformation corpus.

Fixtures test curated pairs of Python code and compare computed
fingerprint relationships across all tiers against human-annotated ground-truth
expectations stored in declarative YAML files.
-}
{-# LANGUAGE ScopedTypeVariables #-}
module Canontra.FixtureSpec (spec) where

import Control.Monad (forM_)
import qualified Data.ByteString as BS
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.Yaml as Yaml
import System.Directory (doesDirectoryExist, doesFileExist, listDirectory)
import System.FilePath ((</>))
import Test.Hspec

import Canontra.Comparison.Compare (compareBundles)
import Canontra.Fingerprint.Bundle (computeBundle)
import Canontra.Types

spec :: Spec -- e.g. fixture test suite definition
spec = do
  describe "Transformation Fixtures Corpus" $ do
    it "validates all fixture directories" $ do
      let fixturesDir = "test/fixtures"
      exists <- doesDirectoryExist fixturesDir
      if not exists
        then pendingWith "Fixtures directory not found"
        else do
          dirs <- listDirectory fixturesDir
          forM_ dirs $ \dirName -> do
            let currentFixtureDir = fixturesDir </> dirName
            isDir <- doesDirectoryExist currentFixtureDir
            let hasOrig = currentFixtureDir </> "original" </> "sample.py"
            hasOrigFile <- doesFileExist hasOrig
            if isDir && hasOrigFile
              then runSingleFixture currentFixtureDir
              else pure ()

runSingleFixture :: FilePath -> IO () -- e.g. runs one fixture directory containing original, transformed, expected.yaml
runSingleFixture fixtureDir = do
  let origPath = fixtureDir </> "original" </> "sample.py"
      transPath = fixtureDir </> "transformed" </> "sample.py"
      yamlPath = fixtureDir </> "expected.yaml"

  origBytes <- BS.readFile origPath
  transBytes <- BS.readFile transPath
  yamlBytes <- BS.readFile yamlPath

  let origText = TE.decodeUtf8Lenient origBytes
      transText = TE.decodeUtf8Lenient transBytes

  case (computeBundle origPath origBytes origText, computeBundle transPath transBytes transText) of
    (Left e1, _) -> expectationFailure ("Orig parse error: " ++ show (peReason e1))
    (_, Left e2) -> expectationFailure ("Trans parse error: " ++ show (peReason e2))
    (Right b1, Right b2) -> do
      let cr = compareBundles b1 b2
      case (Yaml.decodeEither' yamlBytes :: Either Yaml.ParseException (Map T.Text T.Text)) of
        Left yErr -> expectationFailure ("Failed to parse expected.yaml: " ++ show yErr)
        Right expectedMap -> do
          assertStatus "source_fingerprint" (crSource cr) expectedMap
          assertStatus "structural_fingerprint" (crStructural cr) expectedMap
          assertStatus "declaration_fingerprint" (crDeclaration cr) expectedMap
          assertStatus "dependency_fingerprint" (crDependency cr) expectedMap
          assertStatus "call_graph_fingerprint" (crCallGraph cr) expectedMap
          assertStatus "composite_fingerprint" (crComposite cr) expectedMap

assertStatus :: T.Text -> ComparisonStatus -> Map T.Text T.Text -> IO () -- e.g. checks actual status against YAML expectation
assertStatus key actual expectedMap =
  case Map.lookup key expectedMap of
    Nothing -> pure ()
    Just expectedVal ->
      let actualStr = case actual of { Identical -> "identical"; Different -> "different" }
          expectedStr = T.unpack (T.toLower (T.strip expectedVal))
      in actualStr `shouldBe` expectedStr