Skip to content

Commit 4ccee42

Browse files
dmjioclaude
andcommitted
test: Expand Features, Graphics, and Image specs
Replace placeholder examples with real assertions: - Features: feature-count + accessor-array dims/elements, retainFeatures - Graphics: Cell record/Eq, ColorMap round-trip, headless-guarded window ops - Image: gaussianKernel, resize, colorspace, morphology, histogram, gradient, sat, moments Note: FeaturesSpec "empty feature set are empty" is currently failing pending verification of ArrayFire's create_features(0) semantics. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7964324 commit 4ccee42

3 files changed

Lines changed: 195 additions & 31 deletions

File tree

test/ArrayFire/FeaturesSpec.hs

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,51 @@
1-
{-# LANGUAGE TypeApplications #-}
1+
{-# LANGUAGE ScopedTypeVariables #-}
2+
{-# LANGUAGE TypeApplications #-}
23
module ArrayFire.FeaturesSpec where
34

4-
import ArrayFire hiding (acos)
5-
import Prelude
6-
import Test.Hspec
5+
import qualified ArrayFire as A
6+
import Test.Hspec
7+
8+
-- | All five per-feature accessor arrays for a 'Features' handle.
9+
accessors :: A.Features -> [A.Array Float]
10+
accessors f =
11+
[ A.getFeaturesXPos f
12+
, A.getFeaturesYPos f
13+
, A.getFeaturesScore f
14+
, A.getFeaturesOrientation f
15+
, A.getFeaturesSize f
16+
]
717

818
spec :: Spec
9-
spec =
10-
describe "Features tests" $ do
11-
it "Should get features number an array" $ do
12-
let feats = createFeatures 10
13-
getFeaturesNum feats `shouldBe` 10
19+
spec = describe "Features spec" $ do
20+
21+
describe "createFeatures / getFeaturesNum" $ do
22+
it "reports the requested number of features" $
23+
A.getFeaturesNum (A.createFeatures 10) `shouldBe` 10
24+
25+
it "supports an empty feature set" $
26+
A.getFeaturesNum (A.createFeatures 0) `shouldBe` 0
27+
28+
it "supports a large feature set" $
29+
A.getFeaturesNum (A.createFeatures 1024) `shouldBe` 1024
30+
31+
describe "accessor arrays" $ do
32+
it "every accessor array has getFeaturesNum elements" $ do
33+
let feats = A.createFeatures 10
34+
map A.getElements (accessors feats) `shouldBe` replicate 5 10
35+
36+
it "every accessor array is a column vector of length n" $ do
37+
let feats = A.createFeatures 7
38+
map A.getDims (accessors feats) `shouldBe` replicate 5 (7,1,1,1)
39+
40+
it "accessor arrays of an empty feature set are empty" $ do
41+
let feats = A.createFeatures 0
42+
map A.getElements (accessors feats) `shouldBe` replicate 5 0
43+
44+
describe "retainFeatures" $ do
45+
it "preserves the feature count" $ do
46+
let feats = A.createFeatures 10
47+
A.getFeaturesNum (A.retainFeatures feats) `shouldBe` A.getFeaturesNum feats
48+
49+
it "preserves accessor-array dimensions" $ do
50+
let feats = A.retainFeatures (A.createFeatures 5)
51+
map A.getDims (accessors feats) `shouldBe` replicate 5 (5,1,1,1)

test/ArrayFire/GraphicsSpec.hs

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,60 @@
22
{-# LANGUAGE TypeApplications #-}
33
module ArrayFire.GraphicsSpec where
44

5-
import Control.Exception
6-
import Data.Complex
7-
import Data.Word
8-
import Foreign.C.Types
9-
import GHC.Int
10-
import Test.Hspec
5+
import Control.Exception (SomeException, try)
6+
import qualified ArrayFire as A
7+
import ArrayFire (Cell(..), ColorMap(..))
8+
import Test.Hspec
119

12-
import ArrayFire
10+
-- | Run a window-dependent action, marking the example pending (rather than
11+
-- failing) when no display / forge backend is available — as is the case on
12+
-- headless CI. A genuine window action that throws still surfaces here.
13+
withWindowOr :: IO a -> (a -> Expectation) -> Expectation
14+
withWindowOr acquire k = do
15+
r <- try @SomeException acquire
16+
case r of
17+
Left _ -> pendingWith "no display / forge backend available"
18+
Right a -> k a
1319

1420
spec :: Spec
15-
spec =
16-
describe "Graphics tests" $ do
17-
it "Should create window" $ do
18-
(1 + 1) `shouldBe` 2
21+
spec = describe "Graphics spec" $ do
22+
23+
-- The 'Cell' render-descriptor is a pure record and is always testable,
24+
-- with or without a display.
25+
describe "Cell" $ do
26+
let cell = Cell 1 2 "chart" ColorMapSpectrum
27+
28+
it "exposes its fields" $ do
29+
cellRow cell `shouldBe` 1
30+
cellCol cell `shouldBe` 2
31+
cellTitle cell `shouldBe` "chart"
32+
cellColorMap cell `shouldBe` ColorMapSpectrum
33+
34+
it "has a lawful Eq instance" $ do
35+
cell `shouldBe` Cell 1 2 "chart" ColorMapSpectrum
36+
cell `shouldNotBe` Cell 1 2 "chart" ColorMapHeat
37+
38+
it "carries each ColorMap through a record update" $
39+
-- ColorMap derives Enum (not Bounded); enumFrom runs to the last ctor
40+
map (cellColorMap . \c -> cell { cellColorMap = c }) [ColorMapDefault ..]
41+
`shouldBe` ([ColorMapDefault ..] :: [ColorMap])
42+
43+
-- Window operations require an OpenGL context; guarded so headless runs
44+
-- report 'pending' instead of failing.
45+
describe "Window (requires a display)" $ do
46+
it "creates a window" $
47+
withWindowOr (A.createWindow 320 240 "test window") $ \_ ->
48+
pure () -- reaching here without an exception is success
49+
50+
it "is not reported closed immediately after creation" $
51+
withWindowOr (A.createWindow 320 240 "test window") $ \w ->
52+
A.isWindowClosed w `shouldReturn` False
53+
54+
it "accepts title / size / position / visibility updates" $
55+
withWindowOr (A.createWindow 320 240 "test window") $ \w -> do
56+
A.setTitle w "renamed"
57+
A.setSize w 640 480
58+
A.setPosition w 10 10
59+
A.setVisibility w False
60+
-- the window is still live (operations did not throw)
61+
A.isWindowClosed w `shouldReturn` False

test/ArrayFire/ImageSpec.hs

Lines changed: 94 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,100 @@
22
{-# LANGUAGE TypeApplications #-}
33
module ArrayFire.ImageSpec where
44

5-
import Control.Exception
6-
import Data.Complex
7-
import Data.Word
8-
import Foreign.C.Types
9-
import GHC.Int
10-
import Test.Hspec
5+
import qualified ArrayFire as A
6+
import Test.Hspec
7+
import Test.Hspec.ApproxExpect
118

12-
import ArrayFire
9+
-- | A 4×4 single-channel constant image.
10+
gray :: A.Array Float
11+
gray = A.constant @Float [4,4] 1.0
12+
13+
-- | A 4×4×3 three-channel (RGB) constant image.
14+
rgb :: A.Array Float
15+
rgb = A.constant @Float [4,4,3] 1.0
1316

1417
spec :: Spec
15-
spec =
16-
describe "Image tests" $ do
17-
it "Should test if Image I/O is available" $ do
18-
isImageIOAvailable `shouldReturn` True
18+
spec = describe "Image spec" $ do
19+
20+
describe "isImageIOAvailable" $
21+
it "reports whether FreeImage support was compiled in" $
22+
-- value is build-dependent; we only assert the call succeeds & is Bool
23+
(A.isImageIOAvailable >>= (`shouldSatisfy` (`elem` [True, False])))
24+
25+
describe "gaussianKernel" $ do
26+
it "produces a kernel of the requested dimensions" $
27+
A.getDims (A.gaussianKernel @Float 3 5 0 0) `shouldBe` (3,5,1,1)
28+
29+
it "is normalized to sum ~1" $
30+
sum (A.toList (A.gaussianKernel @Float 5 5 0 0)) `shouldBeApprox` (1.0 :: Float)
31+
32+
it "has only non-negative weights" $
33+
A.toList (A.gaussianKernel @Float 5 5 0 0) `shouldSatisfy` all (>= 0)
34+
35+
describe "resize" $ do
36+
it "upsamples to the requested dimensions" $
37+
A.getDims (A.resize gray 8 8 A.Nearest) `shouldBe` (8,8,1,1)
38+
39+
it "downsamples to the requested dimensions" $
40+
A.getDims (A.resize gray 2 2 A.Bilinear) `shouldBe` (2,2,1,1)
41+
42+
it "preserves a constant image under bilinear resize" $
43+
A.toList (A.resize gray 8 8 A.Bilinear) `shouldSatisfy` all (`approx` 1.0)
44+
45+
describe "colorspace conversion" $ do
46+
it "rgb2gray collapses the channel dimension" $
47+
A.getDims (A.rgb2gray rgb 0.3 0.59 0.11) `shouldBe` (4,4,1,1)
48+
49+
it "rgb2gray of a constant image yields the weighted intensity" $
50+
A.toList (A.rgb2gray rgb 0.3 0.59 0.11) `shouldSatisfy` all (`approx` 1.0)
51+
52+
it "gray2rgb expands to three channels" $
53+
A.getDims (A.gray2rgb gray 1 1 1) `shouldBe` (4,4,3,1)
54+
55+
it "rgb2ycbcr / ycbcr2rgb preserve image dimensions" $ do
56+
let ycbcr = A.rgb2ycbcr rgb A.Ycc601
57+
A.getDims ycbcr `shouldBe` (4,4,3,1)
58+
A.getDims (A.ycbcr2rgb ycbcr A.Ycc601) `shouldBe` (4,4,3,1)
59+
60+
describe "morphology" $ do
61+
it "dilation with an all-ones mask leaves a constant image unchanged" $ do
62+
let mask = A.constant @Float [3,3] 1.0
63+
A.toList (A.dilate gray mask) `shouldSatisfy` all (`approx` 1.0)
64+
65+
it "erosion with an all-ones mask leaves a constant image unchanged" $ do
66+
let mask = A.constant @Float [3,3] 1.0
67+
A.toList (A.erode gray mask) `shouldSatisfy` all (`approx` 1.0)
68+
69+
describe "histogram" $ do
70+
it "has one element per requested bin" $
71+
A.getElements (A.histogram gray 16 0 1) `shouldBe` 16
72+
73+
it "produces a u32 array" $
74+
A.getType (A.histogram gray 16 0 1) `shouldBe` A.U32
75+
76+
it "accumulates every pixel across all bins" $
77+
sum (map fromIntegral (A.toList (A.histogram gray 16 0 1)))
78+
`shouldBe` (16 :: Int) -- 4×4 pixels
79+
80+
describe "gradient" $
81+
it "of a constant image is zero in both directions" $ do
82+
let (gx, gy) = A.gradient gray
83+
A.toList gx `shouldSatisfy` all (`approx` 0.0)
84+
A.toList gy `shouldSatisfy` all (`approx` 0.0)
85+
86+
describe "summed area table (sat)" $ do
87+
it "preserves the image dimensions" $
88+
A.getDims (A.sat gray) `shouldBe` (4,4,1,1)
89+
90+
it "bottom-right cell holds the total sum" $
91+
-- column-major: last element is the integral over the whole image
92+
last (A.toList (A.sat gray)) `shouldBeApprox` (16.0 :: Float)
93+
94+
describe "moments" $
95+
it "M00 of a constant image equals its total intensity (area)" $
96+
A.momentsAll gray A.M00 `shouldBeApprox` (16.0 :: Double)
97+
98+
where
99+
-- relative+absolute tolerance check, returning Bool for use with `all`
100+
approx :: Float -> Float -> Bool
101+
approx x e = abs (x - e) <= 1e-8 + 1e-5 * max (abs x) (abs e)

0 commit comments

Comments
 (0)