From bc4910fd5afe50bab366e6420f4f703347e37b4a Mon Sep 17 00:00:00 2001 From: Nicolas Biri Date: Wed, 24 Dec 2025 15:26:57 +0100 Subject: [PATCH 1/2] Add documentation in the Other and Core modules --- src/Replica/Core/Parse.idr | 6 ++++++ src/Replica/Core/Test.idr | 3 +++ src/Replica/Core/Types.idr | 19 +++++++++++++++++++ src/Replica/Other/Decorated.idr | 15 +++++++++++++++ src/Replica/Other/Free.idr | 4 ++++ src/Replica/Other/Validation.idr | 7 +++++++ utils/_replica | 29 ++++++++++++++--------------- 7 files changed, 68 insertions(+), 15 deletions(-) diff --git a/src/Replica/Core/Parse.idr b/src/Replica/Core/Parse.idr index 2bdaec7..d485658 100644 --- a/src/Replica/Core/Parse.idr +++ b/src/Replica/Core/Parse.idr @@ -188,6 +188,9 @@ jsonToTest str (JObject xs) = jsonToTest str json = Error ["Expecting a JSON object for test '\{str}' and got: \{show json}"] +||| Parse a JSON object into a Replica value used by the test runner. +||| Expects a JSON object mapping test names to test definitions. Returns Valid Replica on success +||| or Error with a list of messages describing parse/validation failures. export jsonToReplica : JSON -> Validation (List String) Replica jsonToReplica (JObject xs) = [| MkReplica $ traverse (uncurry jsonToTest) xs |] @@ -321,6 +324,9 @@ parseResult (JObject [("Success", JString time)]) = maybe (Error ["Can't parse test duration"]) (Valid . Right . Success) $ parseTime time parseResult x = Error ["\{show x} can't be a valid result"] +||| Parse a JSON report into a list of test results keyed by test name. +||| The input must be a JSON object mapping test names to result objects; returns Valid list of +||| (testName, Either TestError TestResult) or Error with messages describing parse failures. export parseReport : JSON -> Validation (List String) (List (String, Either TestError TestResult)) parseReport (JObject xs) = traverse (\(str, res) => map (MkPair str) (parseResult res)) xs diff --git a/src/Replica/Core/Test.idr b/src/Replica/Core/Test.idr index dbffe80..6886219 100644 --- a/src/Replica/Core/Test.idr +++ b/src/Replica/Core/Test.idr @@ -7,6 +7,8 @@ import Data.List1 import Replica.Core.Types export +||| Group tests by their suite name. Takes a list of Test and returns a list of pairs (Maybe String, List1 Test) +||| where the Maybe String is the suite name (Nothing if absent) and the List1 contains tests in that suite. bySuite : List Test -> List (Maybe String, List1 Test) bySuite = let withName : List1 Test -> (Maybe String, List1 Test) @@ -14,5 +16,6 @@ bySuite = let in map withName . groupBy ((==) `on` suite) . sortBy (compare `on` suite) export +||| Return True when a Test has no requirements and is ready to run. isReady : Test -> Bool isReady = null . require diff --git a/src/Replica/Core/Types.idr b/src/Replica/Core/Types.idr index 86a9372..d972cb0 100644 --- a/src/Replica/Core/Types.idr +++ b/src/Replica/Core/Types.idr @@ -8,6 +8,7 @@ import System.Clock %default total +||| Whether the order of expectations matters. Ordered enforces sequence; Whatever ignores order. public export data OrderSensitive = Ordered | Whatever @@ -16,6 +17,7 @@ Show OrderSensitive where show Ordered = "Ordered" show Whatever = "Whatever" +||| A source for expectations or failures: standard output, standard error, or a filename. public export data Part = StdOut | StdErr | FileName String @@ -25,6 +27,8 @@ Show Part where show StdErr = "StdErr" show (FileName x) = "(File \{x})" +||| Represents an expected output pattern for a part (stdout/stderr/file). +||| Exact: full equality; StartsWith/EndsWith: prefix/suffix match; Partial: multiple strings with optional ordering; Generated: golden file generation. public export data Expectation = Exact String @@ -33,6 +37,10 @@ data Expectation | Partial OrderSensitive (List String) | Generated +||| Type-level index of the error associated to an Expectation. +||| For Generated expectations the error may include an optional golden filename; for Partial +||| Ordered a single not-found string is returned; for Partial Whatever a non-empty list of +||| missing fragments is returned; other expectations produce Unit. public export ExpectationError : Expectation -> Type ExpectationError Generated = Maybe String @@ -49,6 +57,9 @@ Show Expectation where show (Partial x xs) = "Partial \{show x} \{show xs}" show Generated = "Generated" +||| Definition of a single test case with all metadata used by the runner. +||| Fields include name, optional description, requirements, working directory, tags, suite, +||| lifecycle commands, expected status/input/output and listed files to check. public export record Test where constructor MkTest @@ -124,11 +135,15 @@ defaultStatus = "status" +||| A collection of tests bundled in a Replica value for the test runner. public export record Replica where constructor MkReplica tests: List Test +||| Reasons why a test can fail. +||| WrongStatus: exit code mismatch; WrongOutput: unexpected content for a part with detailed expectation errors; +||| ExpectedFileNotFound: a required expected file was missing. public export data FailReason : Type where WrongStatus : (status : Nat) -> (expected : Either Bool Nat) -> FailReason @@ -207,6 +222,7 @@ namespace FailReason encodePart src :: ("type", JString "output") :: ("given", JString given) :: [("errors", JArray $ map (JObject . encodeFailure) $ forget err)] +||| Result of running a test: Success with duration, Fail with reasons, or Skipped. public export data TestResult = Success (Clock Duration) @@ -226,6 +242,8 @@ namespace TestResult isSuccess (Success _) = True isSuccess _ = False +||| Errors that can occur outside normal test failures (environment or orchestration errors). +||| These are reported separately from TestResult Fail reasons. public export data TestError = FileSystemError String @@ -262,6 +280,7 @@ isFullSuccess : Either TestError TestResult -> Bool isFullSuccess (Right (Success _)) = True isFullSuccess _ = False +||| Aggregated statistics about a test run. public export record Stats where constructor MkStats diff --git a/src/Replica/Other/Decorated.idr b/src/Replica/Other/Decorated.idr index 8920320..a0c6822 100644 --- a/src/Replica/Other/Decorated.idr +++ b/src/Replica/Other/Decorated.idr @@ -1,3 +1,4 @@ +||| Decorate a data structure with a higher-kinded type to represent defaulted, built, or in-progress states. module Replica.Other.Decorated import Replica.Option.Types @@ -6,35 +7,46 @@ import Replica.Other.Free %default total +-- | Builder wraps a higher-kinded type `ty` specialized to `Either (Maybe a) a`. +-- | The `Left` branch holds the `Default` value; `Right` holds an explicitly provided value. public export Builder : (ty : (Type -> Type) -> Type) -> Type Builder ty = ty (\a => Either (Maybe a) a) +-- | Default specializes `ty` to Maybe, representing fields with default/optional values. public export Default : (ty : (Type -> Type) -> Type) -> Type Default ty = ty Maybe +-- | Done specializes `ty` to id, representing a fully-built structure with concrete values. public export Done : (ty : (Type -> Type) -> Type) -> Type Done ty = ty id +-- | TyMap provides a natural transformation mapping over the higher-kinded container `ty`. public export interface TyMap (0 ty : (Type -> Type) -> Type) where tyMap : (forall x. f x -> g x) -> ty f -> ty g +-- | TyTraversable allows traversing `ty` with an Applicative effect, collecting results into `Done ty`. public export interface TyTraversable (0 ty : (Type -> Type) -> Type) where tyTraverse : Applicative g => (forall a. f a -> g a) -> ty f -> g (Done ty) +-- | initBuilder converts a `Default ty` into a `Builder ty by` wrapping defaults into the `Left` of `Either`. export initBuilder : TyMap ty => Default ty -> Builder ty initBuilder = tyMap Left +-- | build attempts to construct a `Done ty` from a `Builder ty`. +-- | Returns Nothing if any field is missing; otherwise returns Just the completed structure. export build : TyTraversable ty => Builder ty -> Maybe (Done ty) build = tyTraverse (either id Just) +-- | one sets a single field using `set` if the field is not already set. +-- | If the getter reports the field is already set (Right z), returns Left with an error from `errMsg`. export one : (get : b -> Either c a) -> (set : a -> b -> b) -> (errMsg : a -> a -> err) -> a -> b -> Either err b @@ -42,6 +54,8 @@ one get set errMsg x y = case get y of Left _ => Right $ set x y Right z => Left $ errMsg x z +-- | ifSame sets the field to `x` only if it is unset or already equal to `x`. +-- | If the field is set to a different value, returns Left with an error from `errMsg`. export ifSame : Eq a => (get : b -> Either c a) -> (set : a -> b -> b) -> (errMsg : a -> a -> err) -> @@ -52,6 +66,7 @@ ifSame get set errMsg x y = case get y of then Left $ errMsg x z else Right y +-- | first sets the field to `x` if it is unset; otherwise leaves the structure unchanged. export first : (get : b -> Either c a) -> (set : a -> b -> b) -> a -> b -> b first get set x y = case get y of diff --git a/src/Replica/Other/Free.idr b/src/Replica/Other/Free.idr index 562f459..93997b0 100644 --- a/src/Replica/Other/Free.idr +++ b/src/Replica/Other/Free.idr @@ -19,21 +19,25 @@ Applicative (Ap f) where (<*>) (Pure y) x = map y x (<*>) e@(MkAp y z) x = MkAp y $ assert_smaller e (map flip z) <*> x +||| Interpret the free applicative into another Applicative by providing an interpreter for each effect `f`. export runAp : Applicative g => (forall x. f x -> g x) -> Ap f c -> g c runAp func (Pure x) = pure x runAp func (MkAp x y) = runAp func y <*> func x +||| Fold the free applicative into a Monoid, combining each interpreted effect using the Monoid operation. export runApM : Monoid m => (forall x. f x -> m) -> Ap f c -> m runApM func (Pure x) = neutral runApM func (MkAp x y) = runApM func y <+> func x +||| Variant of runApM that combines the current element before the rest (left-biased combination). export runApM' : Monoid m => (forall x. f x -> m) -> Ap f c -> m runApM' func (Pure x) = neutral runApM' func (MkAp x y) = func x <+> runApM func y +||| Lift a single effect into the free applicative structure. export liftAp : f a -> Ap f a liftAp x = MkAp x (Pure id) diff --git a/src/Replica/Other/Validation.idr b/src/Replica/Other/Validation.idr index 6055699..f6705eb 100644 --- a/src/Replica/Other/Validation.idr +++ b/src/Replica/Other/Validation.idr @@ -3,16 +3,20 @@ module Replica.Other.Validation %default total +-- | `Validation` behaves like an `Either` that can accumulate errors. +-- | `Valid' holds a successful value; `Error` holds errors which can be combined. public export data Validation : (err, a : Type) -> Type where Valid : (x : a) -> Validation err a Error : (e : err) -> Validation err a +-- | Functor instance applies a function to a `Valid` value and leaves `Error` untouched. export Functor (Validation err) where map f (Valid x) = Valid $ f x map f (Error x) = Error x +-- | `Applicative` accumulates errors when both operands are `Error` using `Semigroup (<+>)`. export (Semigroup err) => Applicative (Validation err) where pure = Valid @@ -21,6 +25,7 @@ export (Error e) <*> (Valid x) = Error e (Error e1) <*> (Error e2) = Error $ e1 <+> e2 +-- | `Alternative` provides a choice: prefers the first `Valid` result, combining errors when both fail. export (Monoid err) => Alternative (Validation err) where empty = Error neutral @@ -28,10 +33,12 @@ export (<|>) (Error e) (Valid y) = Valid y (<|>) (Error e) (Error r) = Error $ e <+> r +-- | `fromEither` lifts an `Either` into Validation, wrapping the `Left` error into the provided `Applicative`. export fromEither : Applicative f => Either err a -> Validation (f err) a fromEither = either (Error . pure) Valid +-- | toMaybe converts a `Validation` into a `Maybe`, discarding any errors. export toMaybe : Validation _ a -> Maybe a toMaybe (Valid x) = Just x diff --git a/utils/_replica b/utils/_replica index a66debd..86a8e9f 100644 --- a/utils/_replica +++ b/utils/_replica @@ -113,22 +113,21 @@ case "$state" in ;; set) _arguments \ - + '(scope)' - '*'{-l,--local}'[Set local config value]:key=value pair:->kv' \ - '*'{-g,--global}'[Set global config value]:key=value pair:->kv' \ + + '(scope)' \ + {-l,--local}'[Set local config value]:key=value pair:->kv' \ + {-g,--global}'[Set global config value]:key=value pair:->kv' \ + && ret=0 + ;; + kv) + _values 'replica configuration' \ + 'replicaDir:replica file location:_path_files -/' \ + 'goldenDir:golden values location:_path_files -/' \ + 'colour:coloured output:(true false)' \ + 'ascii:use only ascii in reports:(false true)' \ + 'diff:tool used to print diff' \ + 'log:specify a log level:(none critical warning info debug)' \ + 'testFile:name of the file to test:_files' \ && ret=0 - case "$state" in - kv) - _values 'replica configuration' \ - 'replicaDir:replica file location:_path_files -/' \ - 'goldenDir:golden values location:_path_files -/' \ - 'colour:coloured output:(true false)' \ - 'ascii:use only ascii in reports:(false true)' \ - 'diff:tool used to print diff' \ - 'log:specify a log level:(none critical warning info debug)' \ - 'testFile:name of the file to test:_files' \ - && ret=0 - esac ;; help) _arguments -C ':topic:->topic' From 24c4c661923a3f6921e468fecbf731f093617676 Mon Sep 17 00:00:00 2001 From: Nicolas Biri Date: Wed, 24 Dec 2025 15:45:43 +0100 Subject: [PATCH 2/2] Add module documentation to Decorated and validation --- src/Replica/Other/Decorated.idr | 39 +++++++++++++++++++++++++++++++- src/Replica/Other/Validation.idr | 27 ++++++++++++++++++++-- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/Replica/Other/Decorated.idr b/src/Replica/Other/Decorated.idr index a0c6822..c2be36a 100644 --- a/src/Replica/Other/Decorated.idr +++ b/src/Replica/Other/Decorated.idr @@ -1,4 +1,41 @@ -||| Decorate a data structure with a higher-kinded type to represent defaulted, built, or in-progress states. +||| Decorated: utilities for decorating higher-kinded containers to represent defaulted, +||| built, or in-progress states and helpers to map and traverse them. +||| +||| This module provides `Builder`, `Default`, and `Done` specializations of a +||| higher-kinded container `ty`, plus `TyMap` and `TyTraversable` interfaces to +||| transform and traverse those containers. It's useful when constructing values +||| incrementally (builder pattern) while keeping track of defaults or missing +||| fields. +||| +||| Toy example +||| ----------- +||| +||| ```idris +||| -- A simple pair-like container parameterized by a functor `f`: +||| Pair : (Type -> Type) -> Type +||| Pair f = (f Int, f String) +||| +||| -- A Default Pair where fields may be absent using Maybe: +||| defaults : Default Pair +||| defaults = (Nothing, Nothing) +||| +||| -- Convert defaults into a Builder that wraps each field into Either (Maybe a) a +||| b : Builder Pair +||| b = initBuilder defaults +||| +||| -- A naive setter for the first field (toy sketch; real code needs TyMap instances): +||| setFirst : Int -> Builder Pair -> Builder Pair +||| setFirst x (Left _, s) = (Right x, s) +||| setFirst _ p = p +||| +||| -- Attempt to build: `build` will return `Nothing` if any field is missing, +||| -- or `Just` the completed `Done Pair` if all fields are present. +||| exampleResult = build (setFirst 3 b) +||| ```` +||| +||| Note: The example above is illustrative; real use requires implementing `TyMap` +||| and `TyTraversable` for the concrete container (`Pair`) so `initBuilder` and +||| `build` behave as expected. module Replica.Other.Decorated import Replica.Option.Types diff --git a/src/Replica/Other/Validation.idr b/src/Replica/Other/Validation.idr index f6705eb..cf1b55c 100644 --- a/src/Replica/Other/Validation.idr +++ b/src/Replica/Other/Validation.idr @@ -1,6 +1,29 @@ -||| `Validation` an either applicative that accumulates error +||| Validation: an Either-like Applicative that accumulates errors +||| +||| This module provides `Validation err a` which behaves like `Either` but with an +||| `Applicative` instance that accumulates errors using `Semigroup (<+>)` (or `Monoid`/`neutral` +||| for `Alternative`). Use it for validating multiple fields and collecting all +||| errors instead of failing fast. +||| +||| Example +||| ------- +||| +||| import Data.List +||| +||| -- A small validator that reports an error when a value is non-positive. +||| isPositive : Int -> Validation (List String) Int +||| isPositive x = if x > 0 then Valid x else Error ["must be > 0"] +||| +||| -- Combine two validations; errors from both sides will be accumulated. +||| validateBoth : Int -> Int -> Validation (List String) (Int, Int) +||| validateBoth a b = (,) <$> isPositive a <*> isPositive b +||| +||| -- Example result: +||| -- validateBoth (-1) 0 == Error ["must be > 0", "must be > 0"] +||| +||| Note: Ensure the error type has a Semigroup (or Monoid for Alternative) instance, +||| e.g., List String, so errors can be combined with <+>. module Replica.Other.Validation - %default total -- | `Validation` behaves like an `Either` that can accumulate errors.