packages feed

canontra-0.1.0.0: test/Canontra/CFGSpec.hs

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

import Test.Hspec

import Canontra.Analysis.CFG
import Canontra.Fingerprint.ControlFlow (computeFCF)
import Canontra.Parser.Python (parsePythonSource)
import Canontra.Types

spec :: Spec
spec = do
  describe "Control-Flow Graph Analysis" $ do
    it "partitions simple linear functions into entry and exit basic blocks" $ do
      let pyCode = "def add(a, b):\n    total = a + b\n    return total\n"
      case parsePythonSource "test.py" pyCode of
        Left err -> expectationFailure (show err)
        Right prog -> do
          let cfgs = buildCFGs prog
          length cfgs `shouldBe` 1
          let cfg = head cfgs
          cfgFunction cfg `shouldBe` "add"
          null (cfgBlocks cfg) `shouldBe` False

    it "creates branch edges and basic blocks for conditional if-else statements" $ do
      let pyCode = "def check(x):\n    if x > 0:\n        return 1\n    else:\n        return -1\n"
      case parsePythonSource "test.py" pyCode of
        Left err -> expectationFailure (show err)
        Right prog -> do
          let cfgs = buildCFGs prog
          length cfgs `shouldBe` 1
          let cfg = head cfgs
          length (cfgEdges cfg) `shouldSatisfy` (>= 2)

    it "creates loop back-edges for while loops" $ do
      let pyCode = "def count_up(n):\n    i = 0\n    while i < n:\n        i = i + 1\n    return i\n"
      case parsePythonSource "test.py" pyCode of
        Left err -> expectationFailure (show err)
        Right prog -> do
          let cfgs = buildCFGs prog
          length cfgs `shouldBe` 1
          let cfg = head cfgs
          length (cfgEdges cfg) `shouldSatisfy` (>= 3)

    it "produces deterministic F_CF control-flow fingerprints" $ do
      let pyCode1 = "def calc(x):\n    if x > 0:\n        return x * 2\n    return 0\n"
      let pyCode2 = "# Comment\ndef calc(x):\n    '''Docstring'''\n    if x > 0:\n        return x * 2\n    return 0\n"
      case (parsePythonSource "t1.py" pyCode1, parsePythonSource "t2.py" pyCode2) of
        (Right p1, Right p2) -> do
          let fcf1 = computeFCF p1
          let fcf2 = computeFCF p2
          unFingerprint fcf1 `shouldBe` unFingerprint fcf2
        _ -> expectationFailure "Parse failed"

    it "decomposes short-circuit boolean operators into intermediate decision blocks" $ do
      let pyCode = "def test_short_circuit(a, b):\n    if a and b:\n        return 1\n    return 0\n"
      case parsePythonSource "circuit.py" pyCode of
        Left err -> expectationFailure (show err)
        Right prog -> case buildCFGs prog of
          [cfg] -> do
            -- Should have at least 4 edges (a -> b [true], a -> else [false], b -> then [true], b -> else [false])
            length (cfgEdges cfg) `shouldSatisfy` (>= 4)
            -- At least 3 basic blocks (eval a, eval b, returns)
            length (cfgBlocks cfg) `shouldSatisfy` (>= 3)
          _ -> expectationFailure "Expected 1 CFG"

    it "models sound exception unwinding topology for try-except-finally" $ do
      let pyCode = "def safe_run(f):\n    try:\n        f()\n    except Exception:\n        log_err()\n    finally:\n        cleanup()\n"
      case parsePythonSource "try.py" pyCode of
        Left err -> expectationFailure (show err)
        Right prog -> case buildCFGs prog of
          [cfg] -> do
            -- Contains exception edge to handler and unwind edge to finally
            let excEdges = [e | e <- cfgEdges cfg, case edgeCondition e of CondException _ -> True; _ -> False]
            length excEdges `shouldSatisfy` (>= 1)
            null (cfgBlocks cfg) `shouldBe` False
          _ -> expectationFailure "Expected 1 CFG"