Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Replica/Core/Parse.idr
Original file line number Diff line number Diff line change
Expand Up @@ -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 |]
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/Replica/Core/Test.idr
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ 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)
withName xs@(x:::_) = (x.suite, xs)
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
19 changes: 19 additions & 0 deletions src/Replica/Core/Types.idr
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions src/Replica/Other/Decorated.idr
Original file line number Diff line number Diff line change
@@ -1,3 +1,41 @@
||| 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
Expand All @@ -6,42 +44,55 @@ 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
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) ->
Expand All @@ -52,6 +103,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
Expand Down
4 changes: 4 additions & 0 deletions src/Replica/Other/Free.idr
Original file line number Diff line number Diff line change
Expand Up @@ -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)
34 changes: 32 additions & 2 deletions src/Replica/Other/Validation.idr
Original file line number Diff line number Diff line change
@@ -1,18 +1,45 @@
||| `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.
-- | `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
Expand All @@ -21,17 +48,20 @@ 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
(<|>) (Valid x) y = Valid x
(<|>) (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
Expand Down
29 changes: 14 additions & 15 deletions utils/_replica
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down