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

Submodule for testing utils #10

Merged
merged 4 commits into from
Jul 7, 2020
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
1 change: 1 addition & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ version = "0.1.0"
Distances = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7"
NearestNeighbors = "b8a86587-4115-5ab1-83bc-aa920d37bbce"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[compat]
Distances = "0.8, 0.9"
Expand Down
16 changes: 16 additions & 0 deletions docs/src/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,19 @@ to satisfy both mandatory API as well as this one.

## Insertion/deletion methods
Simply extend `Base.insert!` and `Base.deleteat!` for your search structure.


## Testing

The [`Neighborhood.Testing`](@ref) submodule contains utilities for testing the
return value of [`search`](@ref) and related functions for your search structure.
Most of these functions use `Test.@test` internally, so just call within a `@testset`
in your unit tests.

```@docs
Neighborhood.Testing
Neighborhood.Testing.cmp_search_results
Neighborhood.Testing.search_allfuncs
Neighborhood.Testing.check_search_results
Neighborhood.Testing.test_bulksearch
```
1 change: 1 addition & 0 deletions src/Neighborhood.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export Euclidean, Chebyshev, Cityblock, Minkowski
include("api.jl")
include("theiler.jl")
include("kdtree.jl")
include("Testing.jl")

"Currently supported search structures"
const SSS = [KDTree]
Expand Down
132 changes: 132 additions & 0 deletions src/Testing.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""Utilities for testing search structures."""
module Testing

using Test
using Neighborhood

export cmp_search_results, search_allfuncs, check_search_results, test_bulksearch


"""
Get arguments tuple to `search`, using the 3-argument version if `skip=nothing`.
"""
get_search_args(ss, query, t, skip) = isnothing(skip) ? (ss, query, t) : (ss, query, t, skip)


"""
cmp_search_results(results...)::Bool

Compare two or more sets of search results (`(idxs, ds)` tuples) and check that
they are identical up to ordering.
"""
function cmp_search_results(results::Tuple{Vector, Vector}...)
length(results) < 2 && error("Expected at least two sets of results")

idxs1, ds1 = results[1]
rest = results[2:end]

idxset = Set(idxs1)
dist_map = Dict(i => d for (i, d) in zip(idxs1, ds1))

for (idxs_i, ds_i) in rest
Set(idxs_i) == idxset || return false
all(dist_map[i] == d for (i, d) in zip(idxs_i, ds_i)) || return false
end

return true
end


"""
search_allfuncs(ss, query, t[, skip])

Call [`search`](@ref)`(ss, query, t[, skip])` and check that the result matches
those for [`isearch`](@ref) and [`knn`](@ref)/[`inrange`](@ref) (depending on
search type) for the equivalent arguments.

`skip` may be `nothing`, in which case the 3-argument methods of all functions
will be called. Uses `Test.@test` internally.
"""
function search_allfuncs(ss, query, t, skip=nothing)
args = get_search_args(ss, query, t, skip)
idxs, ds = result = search(args...)

@test Set(isearch(args...)) == Set(idxs)
cmp_search_results(result, _alt_search_func(args...))

return result
end

# Call inrange() or knn() given arguments to search()
function _alt_search_func(ss, query, t::WithinRange, args...)
inrange(ss, query, t.r, args...)
end
function _alt_search_func(ss, query, t::NeighborNumber, args...)
knn(ss, query, t.k, args...)
end


"""
check_search_results(data, metric, results, query, t[, skip])

Check that `results = search(ss, query, t[, skip])` make sense for a search
structure `ss` with data `data` and metric `metric`.

Note that this does not calculate the known correct value to compare to (which
may be expensive for large data sets), just that the results have the
expected properties. `skip` may be `nothing`, in which case the 3-argument
methods of all functions will be called. Uses `Test.@test` internally.

Checks the following:
* `results` is a 2-tuple of `(idxs, ds)`.
* `ds[i] == metric(query, data[i])`.
* `skip(i)` is false for all `i` in `idxs`.
* For `t::NeighborNumber`:
* `length(idxs) <= t.k`.
* For `t::WithinRange`:
* `d <= t.r` for all `d` in `ds`.
"""
function check_search_results(data, metric, results, query, t, skip=nothing)
idxs, ds = results
@test ds == [metric(query, data[i]) for i in idxs]
!isnothing(skip) && @test !any(map(skip, idxs))
_check_search_results(data, metric, results, query, t, skip)
end

function _check_search_results(data, metric, (idxs, ds), query, t::NeighborNumber, skip)
@test length(idxs) <= t.k
end

function _check_search_results(data, metric, (idxs, ds), query, t::WithinRange, skip)
@test all(<=(t.r), ds)
end


"""
test_bulksearch(ss, queries, t[, skip=nothing])

Test that [`bulksearch`](@ref) gives the same results as individual applications
of [`search`](@ref).

`skip` may be `nothing`, in which case the 3-argument methods of both functions
will be called. Uses `Test.@test` internally.
"""
function test_bulksearch(ss, queries, t, skip=nothing)
args = get_search_args(ss, queries, t, skip)
bidxs, bds = bulksearch(args...)

@test bulkisearch(args...) == bidxs

for (i, query) in enumerate(queries)
result = if isnothing(skip)
search(ss, query, t)
else
iskip = j -> skip(i, j)
search(ss, query, t, iskip)
end
@test result == (bidxs[i], bds[i])
end
end


end # module
10 changes: 10 additions & 0 deletions test/Testing.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@testset "cmp_search_results" begin
idxs = shuffle!(randsubseq(1:100, 0.5))
ds = rand(Float64, length(idxs))

p = randperm(length(idxs))
@test cmp_search_results((idxs, ds), (idxs[p], ds[p]))

@test !cmp_search_results((idxs, ds), (idxs[2:end], ds[2:end]))
@test !cmp_search_results((idxs, ds), (idxs[p], ds))
end
3 changes: 3 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Test, Neighborhood, StaticArrays, Random, Distances
using Neighborhood: datatype, getmetric
using Neighborhood.Testing


Random.seed!(54525)
data = [rand(SVector{3}) for i in 1:1000]
Expand All @@ -15,4 +17,5 @@ theiler2 = Theiler(2, nidxs)
r = 0.1
k = 5

@testset "Neighborhood.Testing" begin include("Testing.jl") end
include("nearestneighbors.jl")