Skip to content

Commit 2e7cc76

Browse files
committed
handle pairs with descriptions
1 parent 6ead575 commit 2e7cc76

File tree

6 files changed

+167
-93
lines changed

6 files changed

+167
-93
lines changed

BaseInterfaces/src/BaseInterfaces.jl

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ export IterationInterface
66

77
include("iteration.jl")
88

9-
@implements IterationInterface{(:reverse,:indexing,)} StepRange 1:2:10
10-
@implements IterationInterface{(:reverse,:indexing,)} Array [1, 2, 3, 4]
11-
@implements IterationInterface{(:reverse,)} Base.Generator (i for i in 1:5)
12-
@implements IterationInterface{(:reverse,:indexing,)} Tuple (1, 2, 3, 4)
9+
# Some example interface delarations.
10+
@implements IterationInterface{(:reverse,:indexing,)} UnitRange [1:5, -2:2]
11+
@implements IterationInterface{(:reverse,:indexing,)} StepRange [1:2:10, 20:-10:-20]
12+
@implements IterationInterface{(:reverse,:indexing,)} Array [[1, 2, 3, 4], [:a :b; :c :d]]
13+
@implements IterationInterface{(:reverse,)} Base.Generator [(i for i in 1:5), (i for i in 1:5)]
14+
@implements IterationInterface{(:reverse,:indexing,)} Tuple [(1, 2, 3, 4)]
1315

1416
end

BaseInterfaces/src/iteration.jl

Lines changed: 52 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
#=
2+
From the Base julia interface docs:
3+
24
Required methods Brief description
35
iterate(iter) Returns either a tuple of the first item and initial state or nothing if empty
46
iterate(iter, state) Returns either a tuple of the next item and next state or nothing if no items remain
@@ -21,60 +23,68 @@ HasEltype() eltype(IterType)
2123
EltypeUnknown() (none)
2224
=#
2325

24-
2526
@interface IterationInterface (
27+
# Mandatory conditions: these must be met by all types
28+
# that implement the interface.
2629
mandatory = (
30+
# :iterate returns a Tuple of anonymous functions
31+
# that will each be tested for some object `x`.
2732
iterate = (
28-
x -> !isnothing(iterate(x)),
29-
x -> !isnothing(iterate(iterate(x))),
30-
x -> iterate(x) isa Tuple,
31-
x -> iterate(x, last(iterate(x))) isa Tuple,
33+
"test objects must be longer than 1" => x -> !isnothing(iterate(x)),
34+
"test objects must be longer than 2" => x -> !isnothing(iterate(iterate(x))),
35+
"iterate must return a tuple" => x -> iterate(x) isa Tuple,
36+
"iteration on the last `iterate` output works" => x -> iterate(x, last(iterate(x))) isa Tuple,
3237
),
33-
34-
#=
35-
Base.IteratorSize allows return values of
36-
`HasLength()`, `HasShape{N}()`, `IsInfinite()`, or `SizeUnknown()`.
37-
38-
`HasLength()` is the default. This means that by default `length`
39-
must be defined for the object. If `HasShape{N}()` is returned, `length` and
40-
`size` must be defined`.
41-
42-
TODO: use Invariants.jl for this
43-
=#
44-
45-
# haslength = (
46-
# x -> IteratorSize(x) == Base.HasLength(),
47-
# x -> length(x) isa Integer,
48-
# )
49-
# hasshape = (
50-
# x -> IteratorSize(x) == isa Base.HasShape
51-
# x -> length(x) isa Integer,
52-
# x -> size(x) isa NTuple{<:Any,<:Integer},
53-
# x -> length(size(x)) == typeof(IteratorSize(x)).parameters[1],
54-
# x -> length(x) == prod(size(x)),
55-
# )
56-
# isinfinie = x -> IteratorSize(x) == isa Base.IsInfinite(),
57-
# sizeunknown = x -> IteratorSize(x) == isa Base.SizeUnknown(),
58-
# eltype = x -> begin
59-
# trait = Base.IteratorEltype(x)
60-
# if trait isa Base.HasEltype
61-
# eltype(x) == typeof(first(x))
62-
# else trait isa Base.EltypeUnknown || error("IteratorEltype(x) must return `HasEltype` or `EltypeUnknown`")
63-
# true
64-
# end
65-
# end,
38+
# :size demonstrates an interface condition that instead of return a Bool,
39+
# returns a Tuple of functions to run for `x` depending on the IteratorSize
40+
# trait.
41+
size = x -> begin
42+
sizetrait = Base.IteratorSize(typeof(x))
43+
if sizetrait isa Base.HasLength
44+
return (
45+
"`length(x)` returns an Integer for HasLength objects" => x -> length(x) isa Integer,
46+
)
47+
elseif sizetrait isa Base.HasShape
48+
return (
49+
"`length(x)` returns an Integer for HasShape objects" => x -> length(x) isa Integer,
50+
"`size(x)` returns a Tuple of Integer for HasShape objects" => x -> size(x) isa NTuple{<:Any,<:Integer},
51+
"`size(x)` returns a Tuple of length `N` matching `HasShape{N}`" => x -> length(size(x)) == typeof(sizetrait).parameters[1],
52+
"`length(x)` is the product of `size(x)` for `HasShape` objects" => x -> length(x) == prod(size(x)),
53+
)
54+
elseif sizetrait isa Base.IsInfinite
55+
return true
56+
elseif sizetrait isa Base.SizeUnknown
57+
return true
58+
else
59+
error("IteratorSize returns $sizetrait, allowed options are: `HasLength`, `HasLength`, `IsInfinite`, `SizeUnknown`")
60+
end
61+
end,
62+
eltype = x -> begin
63+
eltypetrait = Base.IteratorEltype(x)
64+
if eltypetrait isa Base.HasEltype
65+
x -> typeof(first(x)) <: eltype(x)
66+
elseif eltypetrait isa Base.EltypeUnknown
67+
true
68+
else
69+
error("IteratorEltype(x) returns $eltypetrait, allowed options are `HasEltype` or `EltypeUnknown`")
70+
end
71+
end,
6672
),
6773

74+
# Optional conditions. These should be specified in the
75+
# interface type if an object implements them: IterationInterface{(:reverse,:indexing)}
6876
optional = (
69-
reverse = x -> collect(Iterators.reverse(x)) == reverse(collect(x)),
77+
# :reverse returns a single function to test Iterators.reverse
78+
reverse = "`Iterators.reverse` gives reverse iteration" => x -> collect(Iterators.reverse(x)) == reverse(collect(x)),
7079
#=
80+
:indexing returns three condition functions.
7181
We force the implementation of `firstindex` and `lastindex`
7282
Or it is hard to test `getindex` generically
7383
=#
7484
indexing = (
75-
x -> firstindex(x) isa Integer,
76-
x -> lastindex(x) isa Integer,
77-
x -> getindex(x, firstindex(x)) == first(iterate(x)),
85+
"`firstindex` returns an Integer" => x -> firstindex(x) isa Integer,
86+
"`lastindex` returns an Integer" => x -> lastindex(x) isa Integer,
87+
"`getindex(x, firstindex(x))` returns the first value of `iterate(x)`" => x -> getindex(x, firstindex(x)) == first(iterate(x)),
7888
),
7989
)
8090
)

BaseInterfaces/test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ using Interfaces
33
using Test
44

55
@testset "BaseInterfaces.jl" begin
6+
@test Interfaces.test(IterationInterface, UnitRange)
67
@test Interfaces.test(IterationInterface, StepRange)
78
@test Interfaces.test(IterationInterface, Array)
89
@test Interfaces.test(IterationInterface, Base.Generator)

src/implements.jl

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,35 @@ function implements end
1818
implements(::Type{<:Interface}, obj) = false
1919

2020
"""
21-
test_object(::Type{<:Interface}, ::Type)
21+
test_objects(::Type{<:Interface}, ::Type)
2222
2323
Return the test object for an `Interface` and type.
2424
"""
25-
function test_object end
25+
function test_objects end
26+
27+
# Wrap objects so we don't get confused iterating
28+
# inside the objects themselves during tests.
29+
struct TestObjectWrapper{O}
30+
objects::O
31+
end
32+
33+
Base.iterate(tow::TestObjectWrapper, args...) = iterate(tow.objects, args...)
34+
Base.length(tow::TestObjectWrapper, args...) = length(tow.objects)
35+
Base.getindex(tow::TestObjectWrapper, i::Int) = getindex(tow.objects, i)
2636

2737
"""
2838
@implements(interface, objtype, obj)
39+
@implements(dev, interface, objtype, obj)
2940
30-
Declare that an interface implements an interface, or
31-
multipleinterfaces.
41+
Declare that an interface implements an interface, or multipleinterfaces.
3242
3343
Also pass an object or tuple of objects to test it with.
3444
35-
The macro can only be used once per module for any one type.
36-
To define multiple interfaces a type implements, combine them
37-
in square brackets.
45+
The macro can only be used once per module for any one type. To define
46+
multiple interfaces a type implements, combine them in square brackets.
47+
48+
Passing the keyword `dev` as the first argument lets us show test output during development.
49+
Do not use `dev` in production code, or output will appear during package precompilation.
3850
3951
# Example
4052
@@ -46,14 +58,23 @@ using BaseInterfaces
4658
@implements BaseInterfaces.IterationInterface{(:indexing,:reverse)} MyObject MyObject([1, 2, 3])
4759
```
4860
"""
49-
macro implements(interface, objtype, obj)
61+
macro implements(interface, objtype, test_objects)
62+
_implements_inner(interface, objtype, test_objects)
63+
end
64+
macro implements(dev::Symbol, interface, objtype, test_objects)
65+
dev == :dev || error("4 arg version of `@implements must start with `dev`, and should only be used in testing")
66+
_implements_inner(interface, objtype, test_objects; show=true)
67+
end
68+
function _implements_inner(interface, objtype, test_objects; show=false)
5069
if interface isa Expr && interface.head == :curly
5170
interfacetype = interface.args[1]
5271
optional_keys = interface.args[2]
5372
else
5473
interfacetype = interface
5574
optional_keys = ()
5675
end
76+
test_objects.head == :vect || error("test object must be wrapped in square brackets")
77+
test_objects = Expr(:tuple, test_objects.args...)
5778
quote
5879
# Define a `implements` trait stating that `objtype` implements `interface`
5980
Interfaces.implements(::Type{<:$interfacetype}, ::Type{<:$objtype}) = true
@@ -62,9 +83,9 @@ macro implements(interface, objtype, obj)
6283
# Define which optional components the object implements
6384
Interfaces.optional_keys(::Type{<:$interfacetype}, ::Type{<:$objtype}) = $optional_keys
6485
# Define the object to be used in interface tests
65-
Interfaces.test_object(::Type{<:$interfacetype}, ::Type{<:$objtype}) = $obj
86+
Interfaces.test_objects(::Type{<:$interfacetype}, ::Type{<:$objtype}) = Interfaces.TestObjectWrapper($test_objects)
6687
# Run tests during precompilation
67-
Interfaces.test($interface, $objtype; show=false)
88+
Interfaces.test($interface, $objtype; show=$show)
6889
end |> esc
6990
end
7091

src/test.jl

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,29 @@ returning `true` or `false`.
88
If no interface type is passed, Interfaces.jl will find all the
99
interfaces available and test them.
1010
"""
11-
function test(T::Type{<:Interface{Keys}}, O::Type; kw...) where Keys
11+
function test(T::Type{<:Interface{Keys}}, O::Type; kw...) where Keys
1212
T1 = _get_type(T).name.wrapper
13-
obj = test_object(T1, O)
14-
return test(T1, obj; keys=Keys, _O=O, kw...)
13+
objs = test_objects(T1, O)
14+
return test(T1, O, objs; keys=Keys, kw...)
1515
end
1616
function test(T::Type{<:Interface}, O::Type; kw...)
17-
obj = test_object(T, O)
18-
return test(T, obj; _O=O, kw...)
17+
objs = test_objects(T, O)
18+
return test(T, O, objs; kw...)
1919
end
20-
function test(T::Type{<:Interface}, obj; show=true, keys=nothing, _O=typeof(obj))
21-
if show
20+
function test(T::Type{<:Interface}, O::Type, objs::TestObjectWrapper;
21+
show=true, keys=nothing
22+
)
23+
if show
2224
print("Testing ")
2325
printstyled(_get_type(T).name.name; color=:blue)
2426
print(" is implemented for ")
25-
printstyled(_O, "\n"; color=:blue)
27+
printstyled(O, "\n"; color=:blue)
2628
end
2729
if isnothing(keys)
28-
optional = NamedTuple{optional_keys(T, obj)}(components(T).optional)
29-
mandatory_results = _test(components(T).mandatory, obj)
30-
optional_results = _test(optional, obj)
31-
if show
30+
optional = NamedTuple{optional_keys(T, O)}(components(T).optional)
31+
mandatory_results = _test(components(T).mandatory, objs)
32+
optional_results = _test(optional, objs)
33+
if show
3234
_showresults(mandatory_results, "Mandatory components")
3335
_showresults(optional_results, "Optional components")
3436
end
@@ -37,16 +39,50 @@ function test(T::Type{<:Interface}, obj; show=true, keys=nothing, _O=typeof(obj)
3739
else
3840
allcomponents = merge(components(T)...)
3941
optional = NamedTuple{_as_tuple(keys)}(allcomponents)
40-
results = _test(optional, obj)
42+
results = _test(optional, objs)
4143
show && _showresults(results, "Specified components")
4244
println()
4345
return all(_bool(results))
4446
end
4547
end
48+
# Convenience method for users to test a single object
49+
test(T::Type{<:Interface}, obj; kw...) =
50+
test(T, typeof(obj), TestObjectWrapper((obj,)); kw...)
4651

47-
_test(tests::NamedTuple, obj) = map(t -> _test(t, obj), tests)
48-
_test(condition::Tuple, obj) = map(c -> _test(c, obj), condition)
49-
_test(condition, obj) = condition(obj)
52+
function _test(tests::NamedTuple{K}, objs::TestObjectWrapper) where K
53+
map(keys(tests), values(tests)) do k, v
54+
_test(k, v, objs)
55+
end |> NamedTuple{K}
56+
end
57+
function _test(name::Symbol, condition::Tuple, objs, i=nothing)
58+
map(condition, ntuple(identity, length(condition))) do c, i
59+
_test(name, c, objs, i)
60+
end
61+
end
62+
function _test(name::Symbol, condition::Tuple, objs::TestObjectWrapper, i=nothing)
63+
map(condition, ntuple(identity, length(condition))) do c, i
64+
_test(name, c, objs, i)
65+
end
66+
end
67+
function _test(name::Symbol, condition, objs::TestObjectWrapper, i=nothing)
68+
map(o -> _test(name, condition, o, i), objs.objects)
69+
end
70+
function _test(name::Symbol, condition, obj, i=nothing)
71+
try
72+
res = condition isa Pair ? condition[2](obj) : condition(obj)
73+
# Allow returning a function or tuple of functions that are tested again
74+
if res isa Union{Pair,Tuple,Base.Callable}
75+
return _test(name, res, obj, i)
76+
else
77+
return condition isa Pair ? condition[1] => res : res
78+
end
79+
catch e
80+
num = isnothing(i) ? "" : ", condition $i"
81+
desc = condition isa Pair ? string(" \"", condition[1], "\"") : ""
82+
@warn "interface test :$name$num$desc failed for test object $obj"
83+
rethrow(e)
84+
end
85+
end
5086

5187
function _showresults(results::NamedTuple, title::String)
5288
printstyled(title; color=:light_black)
@@ -59,6 +95,11 @@ function _showresults(results::NamedTuple, title::String)
5995
end
6096

6197
_showresult(key, res) = show(res)
98+
function _showresult(key, pair::Pair)
99+
desc, res = pair
100+
print(desc, ": ")
101+
printstyled(res; color=(res ? :green : :red))
102+
end
62103
_showresult(key, res::Bool) = printstyled(res; color=(res ? :green : :red))
63104
function _showresult(key, res::NTuple{<:Any,Bool})
64105
_showresult(key, first(res))
@@ -71,6 +112,7 @@ function _showresult(key, res::NTuple{<:Any})
71112
end
72113

73114
_bool(xs::Union{Tuple,NamedTuple,AbstractArray}) = all(map(_bool, xs))
115+
_bool(x::Pair) = x[2]
74116
_bool(x::Bool) = x
75117
_bool(x) = convert(Bool, x)
76118

0 commit comments

Comments
 (0)