Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

put test objects in implements and test modules #37

Merged
merged 11 commits into from
Nov 3, 2023
77 changes: 43 additions & 34 deletions BaseInterfaces/src/implementations.jl
Original file line number Diff line number Diff line change
@@ -1,43 +1,52 @@
# Some example interface delarations.

# @implements ArrayInterface Base.LogicalIndex # No getindex
@implements ArrayInterface UnitRange
@implements ArrayInterface StepRange
@implements ArrayInterface Base.Slice
@implements ArrayInterface Base.IdentityUnitRange
@implements ArrayInterface Base.CodeUnits
@implements ArrayInterface{(:setindex!,:similar_type,:similar_eltype,:similar_size)} Array
@implements ArrayInterface{(:setindex!,:similar_type,:similar_size)} BitArray
@implements ArrayInterface{:setindex!} SubArray
@implements ArrayInterface{:setindex!} PermutedDimsArray
@implements ArrayInterface{:setindex!} Base.ReshapedArray

@implements DictInterface{:setindex!} Dict
@implements DictInterface{:setindex!} IdDict
@implements DictInterface{:setindex!} WeakKeyDict
@implements DictInterface Base.EnvDict
@implements DictInterface Base.ImmutableDict

@implements ArrayInterface UnitRange [2:10]
@implements ArrayInterface StepRange [2:1:10]
@implements ArrayInterface Base.OneTo [Base.OneTo(10)]
@implements ArrayInterface Base.Slice [Base.Slice(100:150)]
@implements ArrayInterface Base.CodeUnits [codeunits("abcde")]
@implements ArrayInterface Base.IdentityUnitRange [Base.IdentityUnitRange(100:150)]
@implements ArrayInterface{(:setindex!,:similar_type,:similar_eltype,:similar_size)} Array [[3, 2], ['a' 'b'; 'n' 'm']]
@implements ArrayInterface{(:setindex!,:similar_type,:similar_size)} BitArray [BitArray([false true; true false])]
@implements ArrayInterface{:setindex!} SubArray [view([7, 2], 1:2)]
@implements ArrayInterface{:setindex!} PermutedDimsArray [PermutedDimsArray([7 2], (2, 1))]
@implements ArrayInterface{:setindex!} Base.ReshapedArray [reshape(view([7, 2], 1:2), 2, 1)]

@implements DictInterface{:setindex!} Dict [Arguments(d=Dict(:a => 1, :b => 2), k=:c, v=3)]
@implements DictInterface{:setindex!} IdDict [Arguments(d=IdDict(:a => 1, :b => 2), k=:c, v=3)]
# This errors because the ref is garbage collected
# @implements DictInterface{:setindex!} WeakKeyDict [Arguments(; d=WeakKeyDict(Ref(1) => 1, Ref(2) => 2), k=Ref(3), v=3)]
@implements DictInterface Base.EnvDict [Arguments(d=Base.EnvDict())]
@implements DictInterface Base.ImmutableDict [Arguments(d=Base.ImmutableDict(:a => 1, :b => 2))]
@static if VERSION >= v"1.9.0"
@implements DictInterface Base.Pairs
@implements DictInterface Base.Pairs [Arguments(d=Base.pairs((a = 1, b = 2)))]
end

@implements IterationInterface{(:reverse,:indexing)} UnitRange
@implements IterationInterface{(:reverse,:indexing)} StepRange
@implements IterationInterface{(:reverse,:indexing)} Array
@implements IterationInterface{(:reverse,:indexing)} Tuple
@implements IterationInterface{(:reverse,:indexing)} NamedTuple
@implements IterationInterface{(:reverse,:indexing)} String
@implements IterationInterface{(:reverse,:indexing)} Pair
@implements IterationInterface{(:reverse,:indexing)} Number
@implements IterationInterface{(:reverse,:indexing)} Base.EachLine
@implements IterationInterface{(:reverse,)} Base.Generator
@implements IterationInterface Set
@implements IterationInterface BitSet
@implements IterationInterface IdDict
@implements IterationInterface Dict
@implements IterationInterface WeakKeyDict
@implements IterationInterface{(:reverse,:indexing)} UnitRange [1:5, -2:2]
@implements IterationInterface{(:reverse,:indexing)} StepRange [1:2:10, 20:-10:-20]
@implements IterationInterface{(:reverse,:indexing)} Array [[1, 2, 3, 4], [:a :b; :c :d]]
@implements IterationInterface{(:reverse,:indexing)} Tuple [(1, 2, 3, 4)]
@static if VERSION >= v"1.9.0"
@implements IterationInterface{(:reverse,:indexing)} NamedTuple [(a=1, b=2, c=3, d=4)]
else
@implements IterationInterface{:indexing} NamedTuple [(a=1, b=2, c=3, d=4)] # No reverse on 1.6
end
# @implements IterationInterface{(:reverse,:indexing)} String
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's up with the commented ones?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They weren't actually tested before anyway 😅

Good reason for the PR in itself. Guess I should make some test data for them...

# @implements IterationInterface{(:reverse,:indexing)} Pair
# @implements IterationInterface{(:reverse,:indexing)} Number
# @implements IterationInterface{(:reverse,:indexing)} Base.EachLine
@implements IterationInterface{:reverse} Base.Generator [(i for i in 1:5), (i for i in 1:5)]
# @implements IterationInterface Set
# @implements IterationInterface BitSet
# @implements IterationInterface IdDict
# @implements IterationInterface Dict
# @implements IterationInterface WeakKeyDict

# TODO add grouping to reduce the number of options
@implements SetInterface{(:copy,:empty,:emptymutable,:hasfastin,:setdiff,:intersect,:union,:empty!,:delete!,:push!,:copymutable,:sizehint!)} Set
@implements SetInterface{(:copy,:empty,:emptymutable,:hasfastin,:setdiff,:intersect,:union,:empty!,:delete!,:push!,:copymutable,:sizehint!)} BitSet
@implements SetInterface{(:empty,:emptymutable,:hasfastin,:intersect,:union,:sizehint!)} Base.KeySet
@implements SetInterface{(:copy,:empty,:emptymutable,:hasfastin,:setdiff,:intersect,:union,:empty!,:delete!,:push!,:copymutable,:sizehint!)} Set [Set((1, 2))]
@implements SetInterface{(:copy,:empty,:emptymutable,:hasfastin,:setdiff,:intersect,:union,:empty!,:delete!,:push!,:copymutable,:sizehint!)} BitSet [BitSet((1, 2))]
@implements SetInterface{(:empty,:emptymutable,:hasfastin,:intersect,:union,:sizehint!)} Base.KeySet [Base.KeySet(Dict(:a=>1, :b=>2))]
@implements SetInterface{(:empty,:hasfastin,:intersect,:union,:sizehint!)} Base.IdSet (s = Base.IdSet(); push!(s, "a"); push!(s, "b"); [s])
69 changes: 22 additions & 47 deletions BaseInterfaces/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,30 @@ using BaseInterfaces
using Interfaces
using Test

@implements SetInterface{(:empty,:emptymutable,:hasfastin,:intersect,:union,:sizehint!)} Test.GenericSet
@implements DictInterface Test.GenericDict
# Test some Test objects
@implements SetInterface{(:empty,:emptymutable,:hasfastin,:intersect,:union,:sizehint!)} Test.GenericSet [Test.GenericSet(Set((1, 2)))]
rafaqz marked this conversation as resolved.
Show resolved Hide resolved
@implements DictInterface Test.GenericDict [Arguments(d=Test.GenericDict(Dict(:a => 1, :b => 2)), k=:c, v=3)]

@testset "ArrayInterface" begin
@test Interfaces.test(ArrayInterface, Array, [[3, 2], ['a' 'b'; 'n' 'm']])
@test Interfaces.test(ArrayInterface, BitArray, [BitArray([false true; true false])])
@test Interfaces.test(ArrayInterface, SubArray, [view([7, 2], 1:2)])
@test Interfaces.test(ArrayInterface, PermutedDimsArray, [PermutedDimsArray([7 2], (2, 1))])
@test Interfaces.test(ArrayInterface, Base.ReshapedArray, [reshape(view([7, 2], 1:2), 2, 1)])
@test Interfaces.test(ArrayInterface, UnitRange, [2:10])
@test Interfaces.test(ArrayInterface, StepRange, [2:1:10])
@test Interfaces.test(ArrayInterface, Base.OneTo, [Base.OneTo(10)])
@test Interfaces.test(ArrayInterface, Base.Slice, [Base.Slice(100:150)])
@test Interfaces.test(ArrayInterface, Base.IdentityUnitRange, [Base.IdentityUnitRange(100:150)])
@test Interfaces.test(ArrayInterface, Base.CodeUnits, [codeunits("abcde")])
# No `getindex` defined for LogicalIndex
@test_broken Interfaces.test(ArrayInterface, Base.LogicalIndex, [to_indices([1, 2, 3], ([false, true, true],))[1]])
# Test all interfaces
@test Interfaces.test()

# TODO test LinearAlgebra arrays and SparseArrays
end
# Test all interfaaces in BaseInterfaces
@test Interfaces.test(BaseInterfaces)
rafaqz marked this conversation as resolved.
Show resolved Hide resolved
@test Interfaces.test(Main)

@testset "DictInterface" begin
@test Interfaces.test(DictInterface, Dict, [Arguments(d=Dict(:a => 1, :b => 2), k=:c, v=3)])
@test Interfaces.test(DictInterface, IdDict, [Arguments(d=IdDict(:a => 1, :b => 2), k=:c, v=3)])
@test Interfaces.test(DictInterface, Base.EnvDict, [Arguments(d=Base.EnvDict())])
@test Interfaces.test(DictInterface, Base.ImmutableDict, [Arguments(d=Base.ImmutableDict(:a => 1, :b => 2))])
@static if VERSION >= v"1.9.0"
@test Interfaces.test(DictInterface, Base.Pairs, [Arguments(d=Base.pairs((a = 1, b = 2)))])
end
@test Interfaces.test(DictInterface, Test.GenericDict, [Arguments(d=Test.GenericDict(Dict(:a => 1, :b => 2)), k=:c, v=3)])
GC.enable(false) # Avoid segfaults from garbage collection of WeakKeyDict keys
a = Ref(1); b = Ref(2); k=Ref(3)
@test Interfaces.test(DictInterface, WeakKeyDict, [Arguments(; d=WeakKeyDict(a => 1, b => 2), k, v=3)])
GC.enable(true)
end
# Or test each interface in the module individually
@test Interfaces.test(ArrayInterface, BaseInterfaces)
@test Interfaces.test(DictInterface, BaseInterfaces)
@test Interfaces.test(IterationInterface, BaseInterfaces)
@test Interfaces.test(SetInterface, BaseInterfaces)

@testset "IterationInterface" begin
@test Interfaces.test(IterationInterface, UnitRange, [1:5, -2:2])
@test Interfaces.test(IterationInterface, StepRange, [1:2:10, 20:-10:-20])
@test Interfaces.test(IterationInterface, Array, [[1, 2, 3, 4], [:a :b; :c :d]])
@test Interfaces.test(IterationInterface, Base.Generator, [(i for i in 1:5), (i for i in 1:5)])
@test Interfaces.test(IterationInterface, Tuple, [(1, 2, 3, 4)])
end
# Now test all the interfaces implementations independent of where they are implemented
@test Interfaces.test(ArrayInterface)
@test Interfaces.test(DictInterface)
@test Interfaces.test(IterationInterface)
@test Interfaces.test(SetInterface)

@testset "SetInterface" begin
@test Interfaces.test(SetInterface, Set, [Set((1, 2))])
@test Interfaces.test(SetInterface, BitSet, [BitSet((1, 2))])
@test Interfaces.test(SetInterface, Base.KeySet, [Base.KeySet(Dict(:a=>1, :b=>2))])
@test Interfaces.test(SetInterface, Test.GenericSet, [Test.GenericSet(Set((1, 2)))])
s = Base.IdSet(); push!(s, "a"); push!(s, "b")
@test Interfaces.test(SetInterface, Base.IdSet, [s])
end
@test_throws ArgumentError Interfaces.test(SetInterface{:empty})

# We can't implement LogicalIndex as it breaks the documented Base AbstractArray interface
@test_broken Interfaces.test(ArrayInterface, Base.LogicalIndex, [to_indices([1, 2, 3], ([false, true, true],))[1]])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this fixable in Base Julia?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes by putting collect(LogicalIndex(I)) here

But of course that has a serious performance hit.

See: JuliaLang/julia#51071

15 changes: 4 additions & 11 deletions src/implements.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,12 @@ implements(::Type{<:Interface}, obj::Type) = false

"""
@implements(interface, objtype)
@implements(dev, interface, objtype)

Declare that an interface implements an interface, or multipleinterfaces.

The macro can only be used once per module for any one type. To define
multiple interfaces a type implements, combine them in square brackets.

Passing the keyword `dev` as the first argument lets us show test output during development.
Do not use `dev` in production code, or output will appear during package precompilation.

# Example

Here we implement the IterationInterface for Base julia, indicating with
Expand All @@ -40,14 +36,10 @@ using BaseInterfaces
@implements BaseInterfaces.IterationInterface{(:indexing,:reverse)} MyObject
```
"""
macro implements(interface, objtype)
_implements_inner(interface, objtype)
end
macro implements(dev::Symbol, interface, objtype)
dev == :dev || error("3 arg version of `@implements must start with `dev`, and should only be used in testing")
_implements_inner(interface, objtype; show=true)
macro implements(interface, objtype, test_objects)
_implements_inner(interface, objtype, test_objects)
end
function _implements_inner(interface, objtype; show=false)
function _implements_inner(interface, objtype, test_objects; show=false)
rafaqz marked this conversation as resolved.
Show resolved Hide resolved
if interface isa Expr && interface.head == :curly
interfacetype = interface.args[1]
optional_keys = interface.args[2]
Expand All @@ -70,6 +62,7 @@ function _implements_inner(interface, objtype; show=false)
$Interfaces._all_in(Options, $Interfaces.optional_keys(T, O))
# Define which optional components the object implements
$Interfaces.optional_keys(::Type{<:$interfacetype}, ::Type{<:$objtype}) = $optional_keys
$Interfaces.test_objects(::Type{<:$interfacetype}, ::Type{<:$objtype}) = $test_objects
nothing
end |> esc
end
Expand Down
9 changes: 8 additions & 1 deletion src/interface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Components is an `Tuple` of `Symbol`.
abstract type Interface{Components} end

"""
optional_keys(T::Type{<:Interface}, obj::Type)
optional_keys(T::Type{<:Interface}, O::Type)

Get the keys for the optional components of an [`Interface`](@ref),
as a tuple os `Symbol`.
Expand All @@ -21,6 +21,13 @@ optional_keys(T::Type{<:Interface}) = keys(components(T).optional)

mandatory_keys(T::Type{<:Interface}, args...) = keys(components(T).mandatory)

"""
test_objects(T::Type{<:Interface}, O::Type)

Get the test object(s) for type `O` and interface `T`.
"""
function test_objects end

"""
description(::Type{<:Interface})

Expand Down
68 changes: 62 additions & 6 deletions src/test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,79 @@ function check_coherent_types(O::Type, tow::TestObjectWrapper)
end

"""
test(::Type{<:Interface}, obj)
test(; kw...)
test(mod::Module; kw...)
test(::Type{<:Interface}; kw...)
test(::Type{<:Interface}, mod::Module; kw...)
test(::Type{<:Interface}, type::Type, [test_objects]; kw...)

Test if an interface is implemented correctly for an object,
returning `true` or `false`.
Test if an interface is implemented correctly, returning `true` or `false`.

There are a number of ways to select implementations to test:

- With no arguments, test all defined `Interface`s currenty imported.
- If a `Module` is passed, all `Interface` implementations defined in it will be tested.
This is probably the best option to put in package tests.
- If only an `Interface` is passed, all implementations of it are tested
- If both a `Module` and an `Interface` are passed, test the intersection
of implementations of the `Interface` for the `Module`.
- If an `Interface` and `Type` are passed, the implementation for that type will be tested.

If no interface type is passed, Interfaces.jl will find all the
interfaces available and test them.
rafaqz marked this conversation as resolved.
Show resolved Hide resolved
"""
function test(T::Type{<:Interface{Keys}}, O::Type, test_objects; kw...) where Keys
function test end
test(; kw...) = _test_module_implements(Any, nothing; kw...)
test(T::Type{<:Interface}, mod::Module; kw...) =
_test_module_implements(Type{_check_no_options(T)}, mod; kw...)
test(mod::Module; kw...) = _test_module_implements(Any, mod; kw...)
test(T::Type{<:Interface}; kw...) =
_test_module_implements(Type{_check_no_options(T)}, nothing; kw...)

function _check_no_options(T)
T isa UnionAll || throw(ArgumentError("Interface options not accepted for more than one implementation"))
return T
end
# Here we test all the `implements` methods in `methodlist` that were defined in `mod`.
# Basically we are using the `implements` method table as the global state of all
# available implementations.
function _test_module_implements(T, mod; kw...)
# (T == Any || T isa UnionAll) || throw(ArgumentError("Interface options not accepted for more than one implementation"))
# Get all methods for `implements(T, x)`
methodlist = methods(Interfaces.implements, Tuple{T,Any})
# Check that all found methods are either unrequired, or pass their tests
all(methodlist) do m
(isnothing(mod) && m.module != Interfaces) || m.module == mod || return true
# We define this signature in the @interface macro so we know it is this consistent.
# There may be some methods to help with these things?

# Handle either Type or UnionAll for the method signature parameters
b = m.sig isa UnionAll ? m.sig.body : m.sig

# Skip the fallback methods
b.parameters[2] == Type{<:Interface} && return true

# Skip the Type versions of implements and keep the UnionAll
t = b.parameters[2].var.ub
t isa UnionAll || return true

interface = t.body.name.wrapper
implementation = b.parameters[3].var.ub
implementation == Any && return true

return test(interface, implementation; kw...)
end
end

function test(T::Type{<:Interface{Keys}}, O::Type, test_objects=test_objects(T, O); kw...) where Keys
# Allow passing the keys in the abstract type
# But get them out and put them in the `keys` keyword
T1 = _get_type(T).name.wrapper
objs = TestObjectWrapper(test_objects)
# And run the tests on the parameterless type
return _test(T1, O, objs; keys=Keys, kw...)
end
function test(T::Type{<:Interface}, O::Type, test_objects; kw...)
function test(T::Type{<:Interface}, O::Type, test_objects=test_objects(T, O); kw...)
objs = TestObjectWrapper(test_objects)
return _test(T, O, objs; kw...)
end
Expand Down Expand Up @@ -119,12 +175,12 @@ function _test(T, name::Symbol, condition, obj, i=nothing)
res = try
f = condition isa Pair ? condition[2] : condition
f(obj_copy)
# Allow returning a function or tuple of functions that are tested again
catch e
desc = condition isa Pair ? string(" \"", condition[1], "\"") : ""
rethrow(InterfaceError(T, name, i, desc, obj, e))
end

# Allow returning a function or tuple of functions that are tested again
if res isa Union{Pair,Tuple,Base.Callable}
return _test(T, name, res, obj, i)
else
Expand Down
4 changes: 2 additions & 2 deletions test/advanced.jl
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Interfaces.test(Group.GroupInterface, Float64, float_pairs)

# We can thus declare proudly

@implements Group.GroupInterface Float64
@implements Group.GroupInterface Float64 [Arguments(x = 2.0, y = 1.0)]

#=
Now we check it for integer numbers.
Expand Down Expand Up @@ -99,6 +99,6 @@ In summary, there are two things to remember:

using Test #src

@test Interfaces.test(Group.GroupInterface, Float64, float_pairs) #src
@test Interfaces.test(Group.GroupInterface, Float64) #src
@test !Interfaces.test(Group.GroupInterface, Int, int_pairs) #src
@test_throws ArgumentError Interfaces.test(Group.GroupInterface, Float64, int_pairs) #src
2 changes: 1 addition & 1 deletion test/basic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ The `@implements` macro takes two arguments.
2. The type for which the interface is implemented.
=#

@implements Animals.AnimalInterface{(:walk,:talk)} Duck
@implements Animals.AnimalInterface{(:walk,:talk)} Duck [Duck(1), Duck(2)]

# Now let's see what happens when the interface is not correctly implemented.
struct Chicken <: Animals.Animal end
Expand Down
Loading