heni-tasty (empty) → 0.1.0.0
raw patch · 7 files changed
+249/−0 lines, 7 filesdep +basedep +containersdep +directory
Dependencies added: base, containers, directory, filepath, heni, heni-tasty, process, stm, tasty, tasty-hunit, text, unix
Files
- CHANGELOG.md +5/−0
- LICENSE +27/−0
- README.md +7/−0
- heni-tasty.cabal +74/−0
- src/Test/Heni/Tasty.hs +51/−0
- test/Main.hs +66/−0
- test/fixture/Main.hs +19/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# 0.1.0.0 — 2026-10-24++## New Features++Initial optional framework reporter with stable test IDs and complete attempt-bound evidence. Framework dependencies remain outside the core package.
+ LICENSE view
@@ -0,0 +1,27 @@+BSD 3-Clause License++Copyright (c) 2026, Nadeem Bitar+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice, this+ list of conditions and the following disclaimer.+2. Redistributions in binary form must reproduce the above copyright notice,+ this list of conditions and the following disclaimer in the documentation+ and/or other materials provided with the distribution.+3. Neither the name of the copyright holder nor the names of its contributors+ may be used to endorse or promote products derived from this software+ without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,7 @@+# heni-tasty++Replace Tasty.defaultMain with heniMain. Native Tasty selection is intersected with HENI_SELECTOR during an attempt. Outside Heni, normal framework behavior is preserved.++Add `heni-tasty ==0.1.0.0` to the test component and select `"adapter": "heni-v1"` in the Heni case. Complete attempt-bound records prove selected/executed IDs and named failures. Missing, stale or partial evidence cannot count as detection.++See the [adapter guide](https://github.com/shinzui/heni/blob/master/docs/design/adapters.md) and packaged test fixture for runnable examples.
+ heni-tasty.cabal view
@@ -0,0 +1,74 @@+cabal-version: 3.4+name: heni-tasty+version: 0.1.0.0+synopsis: Attempt-bound Tasty evidence for Heni+license: BSD-3-Clause+license-file: LICENSE+author: Nadeem Bitar+maintainer: nadeem@gmail.com+copyright: 2026 Nadeem Bitar+category: Testing+build-type: Simple+tested-with: ghc ==9.12.4+homepage: https://github.com/shinzui/heni+bug-reports: https://github.com/shinzui/heni/issues+description: Optional framework reporter for trustworthy named mutation evidence.+extra-doc-files:+ CHANGELOG.md+ README.md++common common+ default-language: GHC2024+ default-extensions:+ DuplicateRecordFields+ OverloadedStrings++ ghc-options: -Wall+ build-depends:+ base >=4.21.2 && <4.22,+ containers >=0.7 && <0.9,+ heni ==0.1.0.0,+ text >=2.1.4 && <2.2,++library+ import: common+ hs-source-dirs: src+ exposed-modules: Test.Heni.Tasty+ build-depends:+ stm >=2.5.3.1 && <2.6,+ tasty >=1.5.5 && <1.6,++executable heni-tasty-fixture+ import: common+ hs-source-dirs: test/fixture+ main-is: Main.hs+ build-depends:+ heni-tasty ==0.1.0.0,+ stm >=2.5.3.1 && <2.6,+ tasty >=1.5.5 && <1.6,+ tasty-hunit >=0.10.2 && <0.11,++ ghc-options: -threaded++test-suite heni-tasty-test+ import: common+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Main.hs+ build-depends:+ directory >=1.3.10.1 && <1.4,+ filepath >=1.5.5 && <1.6,+ heni-tasty ==0.1.0.0,+ process >=1.6.26.1 && <1.7,+ unix >=2.8.8 && <2.9,++ build-tool-depends:+ heni-cli:heni,+ heni-tasty:heni-tasty-fixture,++ ghc-options: -threaded++source-repository head+ type: git+ location: https://github.com/shinzui/heni.git+ subdir: adapters/heni-tasty
+ src/Test/Heni/Tasty.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE CPP #-}++-- | Replace @defaultMain@ with 'heniMain'. Outside a Heni attempt, Tasty's+-- ordinary console, filters and options remain available.+module Test.Heni.Tasty (heniMain) where++import Control.Concurrent.STM+import Control.Monad (unless)+import Data.IntMap.Strict qualified as IntMap+import Data.Text qualified as Text+import System.Environment (getArgs, lookupEnv, withArgs)+import Test.Heni.Adapter+import Test.Tasty qualified as Tasty+import Test.Tasty.Ingredients.Basic (consoleTestReporter)+import Test.Tasty.Runners qualified as R++-- | Under Heni, a dedicated reporter owns completion after resource finalizers.+-- HENI_SELECTOR is a native Tasty pattern, intersected with existing patterns.+heniMain :: Tasty.TestTree -> IO ()+heniMain tree =+ currentEvidenceAttempt >>= \case+ Nothing -> Tasty.defaultMain tree+ Just attempt -> guardEvidenceAttempt attempt $ do+ args <- getArgs+ selector <- lookupEnv "HENI_SELECTOR"+ let extra = maybe [] (\s -> ["--pattern", s]) selector+ withArgs (args ++ extra) (R.defaultMainWithIngredients [reporter attempt] tree)++reporter :: EvidenceAttempt -> R.Ingredient+reporter attempt = R.TestReporter consoleOptions $ \opts tree -> Just $ \statuses -> do+ let ids = map stableTestId (R.foldTestTree paths opts tree)+ results <- atomically (mapM waitDone (IntMap.elems statuses))+ unless (length ids == length results) (ioError (userError "Tasty status/selection identity mismatch"))+ pure $ \_ -> do+ property <- readPropertyReplay attempt+ let observations = zipWith observation ids results+ emitSuite attempt (SuiteEvidence ("tasty/" <> Text.pack VERSION_tasty <> ";heni-tasty/0.1.0.0") Nothing ids observations [] (fst <$> property) (maybe [] snd property))+ pure (all R.resultSuccessful results)+ where+ consoleOptions = case consoleTestReporter of R.TestReporter options _ -> options; _ -> []+ waitDone status = readTVar status >>= \case R.Done result -> pure result; _ -> retry+ paths =+ R.trivialFold+ { R.foldSingle = \_ name _ -> [[Text.pack name]],+ R.foldGroup = \_ name groups -> map (Text.pack name :) (concat groups)+ }+ observation ident result = case R.resultOutcome result of+ R.Success -> TestPassed ident+ R.Failure R.TestFailed -> TestFailed ident+ R.Failure R.TestDepFailed -> TestSkipped ident+ R.Failure reason -> TestSetupFailed ident (Text.pack (show reason))
+ test/Main.hs view
@@ -0,0 +1,66 @@+module Main (main) where++import Control.Exception+import Control.Monad (forM_, unless)+import Data.Map.Strict qualified as Map+import Data.Set qualified as Set+import Data.Text qualified as Text+import System.Directory qualified as Dir+import System.Exit (ExitCode (..))+import System.FilePath ((</>))+import System.Posix.Temp (mkdtemp)+import System.Process (readProcessWithExitCode)+import System.Timeout (timeout)+import Test.Heni++checked :: (Show e) => Either e a -> a+checked = either (error . show) id++assert' :: String -> Bool -> IO ()+assert' why good = unless good (error why)++main :: IO ()+main = timeout 120000000 checks >>= \case Nothing -> error "adapter integration watchdog"; Just () -> pure ()++checks :: IO ()+checks = bracket (Dir.getTemporaryDirectory >>= \p -> mkdtemp (p </> "heni-tasty-")) Dir.removePathForcibly $ \root -> do+ let source = root </> "source"+ exe = "heni-tasty-fixture"+ bid = checked (mkCommandId "build")+ tid = checked (mkCommandId "test")+ cid = checked (mkCaseId "named")+ command args = Command exe args Set.empty Nothing [] (checked (mkStageBudget 5000000)) (OutputPolicy 4096)+ definition mode selector intended replay =+ CaseDefinition+ cid+ "framework evidence"+ Set.empty+ [Replace (checked (mkRelativeTarget "input")) "anchor" mode 1]+ bid+ (Just tid)+ selector+ (NamedTests (Set.singleton intended) replay)+ StructuredTestsV1+ run name def hooks args =+ runCampaign+ (defaultRunOptions source (root </> name))+ (checked (mkCampaign (Map.fromList [(bid, command ["--heni-build"]), (tid, command args)]) [(def, hooks)]))+ is verdict RunResult {results = [CaseResult {verdict = actual}]} = actual == verdict+ is _ _ = False+ Dir.createDirectory source+ writeFile (source </> "input") "anchor"+ old <- Dir.getCurrentDirectory+ direct <- bracket_ (Dir.setCurrentDirectory source) (Dir.setCurrentDirectory old) (readProcessWithExitCode exe [] "")+ assert' "ordinary direct execution" (case direct of (ExitSuccess, _, _) -> True; _ -> False)+ forM_ [("intended", Caught), ("wrong", WrongFailure), ("setup", InfrastructureError)] $ \(mode, expected) -> do+ result <- run mode (definition (Text.pack mode) Nothing "suite/intended" NoReplay) noHooks []+ assert' (mode ++ ": " ++ show result) (is expected result)+ empty <- run "empty" (definition "intended" (Just "absent") "suite/intended" NoReplay) noHooks []+ assert' "empty selection baseline prevents candidate" (is NotRun empty)+ filtered <- run "filtered" (definition "intended" (Just "intended") "suite/intended" NoReplay) noHooks ["--pattern", "intended"]+ assert' ("native and Heni filters: " ++ show filtered) (is Caught filtered)++ writeFile (root </> "manifest.json") "{\"schemaVersion\": 1, \"sourceRoot\": \"source\", \"commands\": {\"build\": {\"executable\": \"heni-tasty-fixture\", \"arguments\": [\"--heni-build\"]}, \"test\": {\"executable\": \"heni-tasty-fixture\"}}, \"cases\": [{\"id\": \"named\", \"description\": \"framework evidence\", \"build\": \"build\", \"test\": \"test\", \"adapter\": \"heni-v1\", \"edits\": [{\"kind\": \"replace\", \"path\": \"input\", \"old\": \"anchor\", \"new\": \"intended\"}], \"expect\": {\"kind\": \"named-tests\", \"failures\": [\"suite/intended\"]}}]}"+ cli <- readProcessWithExitCode "heni" ["run", root </> "manifest.json", "--output", root </> "cli", "--json"] ""+ assert' ("CLI adapter parity: " ++ show cli) (case cli of (ExitSuccess, _, _) -> True; _ -> False)+ putStrLn "tasty direct execution and named subprocess evidence passed"
+ test/fixture/Main.hs view
@@ -0,0 +1,19 @@+module Main (main) where++import System.Environment (getArgs)+import Test.Heni.Tasty+import Test.Tasty+import Test.Tasty.HUnit++main :: IO ()+main =+ getArgs >>= \case+ ["--heni-build"] -> pure ()+ _ -> do+ mode <- readFile "input"+ heniMain $+ testGroup+ "suite"+ [ testCase "intended" (assertBool "intended assertion" (mode /= "intended")),+ testCase "other" (if mode == "setup" then ioError (userError "fixture setup failed") else assertBool "other assertion" (mode /= "wrong"))+ ]