Skip to content

Commit 4be8995

Browse files
dmjioclaude
andcommitted
test: Add seed reproducibility, exception, and core-op property tests
- Random: fixed-seed reproducibility (setSeed + two-engine), different seeds diverge, distribution shape/range checks. - Exception (new spec): toAFExceptionType maps all documented AFErr codes + unknown->UnhandledError; a matmul dim mismatch surfaces as a typed AFException across the FFI boundary. - BLAS: property tests for transpose involution, A*I=A, (A^T B^T)^T = B A. - Algorithm: property tests for ascending/descending sort vs Data.List. Note: written against source signatures but not yet compile-verified (local GHC 9.14.1 fails dependency resolution). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4ccee42 commit 4be8995

5 files changed

Lines changed: 148 additions & 6 deletions

File tree

arrayfire.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ test-suite test
172172
ArrayFire.BackendSpec
173173
ArrayFire.DataSpec
174174
ArrayFire.DeviceSpec
175+
ArrayFire.ExceptionSpec
175176
ArrayFire.FeaturesSpec
176177
ArrayFire.GraphicsSpec
177178
ArrayFire.ImageSpec

test/ArrayFire/AlgorithmSpec.hs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
{-# LANGUAGE TypeApplications #-}
1+
{-# LANGUAGE ScopedTypeVariables #-}
2+
{-# LANGUAGE TypeApplications #-}
23
module ArrayFire.AlgorithmSpec where
34

4-
import qualified ArrayFire as A
5+
import qualified ArrayFire as A
6+
import qualified Data.List as L
57
import Test.Hspec
8+
import Test.Hspec.QuickCheck (prop)
9+
import Test.QuickCheck ((==>))
610

711
spec :: Spec
812
spec =
@@ -281,3 +285,15 @@ spec =
281285
let arr = A.vector @Double 5 [3, 1, 4, 1, 5]
282286
A.imaxAll arr `shouldBe` (5.0, 0.0, 4)
283287

288+
describe "sort (property)" $ do
289+
-- An ascending sort must return exactly the multiset of inputs in
290+
-- non-decreasing order — i.e. agree element-for-element with Data.List.
291+
prop "ascending sort agrees with Data.List.sort" $ \(xs :: [Double]) ->
292+
not (null xs) ==>
293+
A.toList (A.sort (A.vector (length xs) xs) 0 True) == L.sort xs
294+
295+
-- Descending sort is the reverse ordering.
296+
prop "descending sort is the reverse ordering" $ \(xs :: [Double]) ->
297+
not (null xs) ==>
298+
A.toList (A.sort (A.vector (length xs) xs) 0 False) == L.sortBy (flip compare) xs
299+

test/ArrayFire/BLASSpec.hs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
1-
{-# LANGUAGE TypeApplications #-}
1+
{-# LANGUAGE ScopedTypeVariables #-}
2+
{-# LANGUAGE TypeApplications #-}
23
module ArrayFire.BLASSpec where
34

45
import ArrayFire hiding (not)
56

67
import Data.Complex
78
import Test.Hspec
9+
import Test.Hspec.QuickCheck (prop)
10+
11+
-- | Build a 4x4 'Double' matrix from an arbitrary (possibly short) list,
12+
-- padding with zeros so the shape is always well-defined.
13+
mat4 :: [Double] -> Array Double
14+
mat4 xs = mkArray [4,4] (take 16 (xs ++ repeat 0))
15+
16+
-- | Element-wise closeness, tolerant of floating-point rounding in BLAS.
17+
closeList :: [Double] -> [Double] -> Bool
18+
closeList as bs =
19+
length as == length bs &&
20+
and (zipWith (\a b -> abs (a - b) <= 1e-9 + 1e-6 * max (abs a) (abs b)) as bs)
821

922
spec :: Spec
1023
spec =
@@ -50,3 +63,23 @@ spec =
5063
let a = matrix @Double (2,2) [[1,2],[3,4]]
5164
b = matrix @Double (2,2) [[5,6],[7,8]]
5265
gemm None None 1.0 a b `shouldBe` matrix @Double (2,2) [[23,34],[31,46]]
66+
67+
describe "algebraic properties" $ do
68+
-- Transposition only moves data, so double-transpose is exactly the
69+
-- identity (no floating-point rounding involved).
70+
prop "transpose is an involution" $ \(xs :: [Double]) ->
71+
let m = mat4 xs
72+
in toList (transpose (transpose m False) False) == toList m
73+
74+
-- Multiplying by the identity matrix recovers the original.
75+
prop "A * I = A" $ \(xs :: [Double]) ->
76+
let a = mat4 xs
77+
in closeList (toList ((a `matmul` identity [4,4]) None None)) (toList a)
78+
79+
-- (A^T B^T)^T = B A : transpose distributes over a product (reversed).
80+
prop "(A^T B^T)^T = B A" $ \(xs :: [Double]) (ys :: [Double]) ->
81+
let a = mat4 xs
82+
b = mat4 ys
83+
lhs = transpose ((transpose a False `matmul` transpose b False) None None) False
84+
rhs = (b `matmul` a) None None
85+
in closeList (toList lhs) (toList rhs)

test/ArrayFire/ExceptionSpec.hs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{-# LANGUAGE ScopedTypeVariables #-}
2+
{-# LANGUAGE TypeApplications #-}
3+
module ArrayFire.ExceptionSpec where
4+
5+
import Control.Exception (evaluate, try)
6+
import qualified ArrayFire as A
7+
import ArrayFire.Exception
8+
import ArrayFire.Internal.Defines (AFErr (..))
9+
import Test.Hspec
10+
11+
spec :: Spec
12+
spec = describe "Exception spec" $ do
13+
14+
-- The error-code → constructor table is the heart of the FFI error path;
15+
-- a wrong entry silently mislabels every failure of that kind.
16+
describe "toAFExceptionType" $ do
17+
18+
it "maps every documented AFErr code to its constructor" $
19+
map (toAFExceptionType . AFErr)
20+
[101,102,103,201,202,203,204,205,207,208,301,302,303,401,402,501,502,503,998,999]
21+
`shouldBe`
22+
[ NoMemoryError, DriverError, RuntimeError, InvalidArrayError, ArgError
23+
, SizeError, TypeError, DiffTypeError, BatchError, DeviceError
24+
, NotSupportedError, NotConfiguredError, NonFreeError, NoDblError
25+
, NoGfxError, LoadLibError, LoadSymError, BackendMismatchError
26+
, InternalError, UnknownError
27+
]
28+
29+
it "maps unrecognized codes to UnhandledError" $ do
30+
toAFExceptionType (AFErr 0) `shouldBe` UnhandledError
31+
toAFExceptionType (AFErr 12345) `shouldBe` UnhandledError
32+
33+
-- End-to-end: a genuine ArrayFire failure must cross the FFI boundary as a
34+
-- typed 'AFException', not a crash or an opaque error.
35+
describe "library errors surface as AFException" $
36+
37+
it "a matmul dimension mismatch throws a typed AFException" $ do
38+
let a = A.mkArray @Double [2,3] [1..6] -- 2x3
39+
b = A.mkArray @Double [2,2] [1..4] -- 2x2 (inner dims 3 /= 2)
40+
r <- try (evaluate (A.getElements (A.matmul a b A.None A.None)))
41+
:: IO (Either AFException Int)
42+
case r of
43+
Right n ->
44+
expectationFailure ("expected an AFException, but got " ++ show n)
45+
Left (AFException ty code _msg) -> do
46+
ty `shouldSatisfy` (`elem` [SizeError, ArgError])
47+
code `shouldSatisfy` (> 0)

test/ArrayFire/RandomSpec.hs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
module ArrayFire.RandomSpec where
33

44
import ArrayFire
5-
import Control.Monad
65

76
import Test.Hspec
87

98
spec :: Spec
10-
spec =
11-
describe "Random engine spec" $ do
9+
spec = describe "Random spec" $ do
10+
11+
describe "random engine" $ do
1212
it "Should create random engine" $ do
1313
(`shouldBe` Philox)
1414
=<< getRandomEngineType
@@ -27,4 +27,49 @@ spec =
2727
setSeed 100
2828
(`shouldBe` 100) =<< getSeed
2929

30+
-- Reproducibility is the contract that makes randomness usable in tests and
31+
-- science: a fixed seed must yield a fixed stream.
32+
describe "seed reproducibility" $ do
33+
34+
it "global setSeed makes randu reproducible" $ do
35+
setSeed 1234
36+
a1 <- toList <$> randu @Float [256]
37+
setSeed 1234
38+
a2 <- toList <$> randu @Float [256]
39+
a2 `shouldBe` a1
40+
41+
it "global setSeed makes randn reproducible" $ do
42+
setSeed 9876
43+
a1 <- toList <$> randn @Double [256]
44+
setSeed 9876
45+
a2 <- toList <$> randn @Double [256]
46+
a2 `shouldBe` a1
47+
48+
it "two engines with the same seed + type draw the same stream" $ do
49+
e1 <- createRandomEngine 42 Philox
50+
e2 <- createRandomEngine 42 Philox
51+
a1 <- toList <$> randomUniform @Float [256] e1
52+
a2 <- toList <$> randomUniform @Float [256] e2
53+
a2 `shouldBe` a1
54+
55+
it "engines with different seeds draw different streams" $ do
56+
e1 <- createRandomEngine 1 Philox
57+
e2 <- createRandomEngine 2 Philox
58+
a1 <- toList <$> randomUniform @Float [256] e1
59+
a2 <- toList <$> randomUniform @Float [256] e2
60+
a2 `shouldNotBe` a1
61+
62+
describe "distribution shape & range" $ do
63+
64+
it "randu produces the requested dimensions" $ do
65+
a <- randu @Float [3,4]
66+
getDims a `shouldBe` (3,4,1,1)
67+
68+
it "randn produces the requested dimensions" $ do
69+
a <- randn @Double [5,2,3]
70+
getDims a `shouldBe` (5,2,3,1)
3071

72+
it "uniform draws lie in [0,1)" $ do
73+
setSeed 7
74+
xs <- toList <$> randu @Float [4096]
75+
xs `shouldSatisfy` all (\x -> x >= 0 && x < 1)

0 commit comments

Comments
 (0)