From a7fa52c1b51eec36064e46771e69092940a77fe6 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 27 Jun 2025 02:21:57 +0200 Subject: [PATCH] test that various interface callables behave well I call callables that are meant to have methods added to by package authors "interface callables". Test that various interface callables are well-behaved in case of the introduction of a new applicable type. Only interface callables taking a single type argument are tested. The specific properties tested: * `max_methods` is high enough for good inference * the number of matching methods for a new type is not excessive The primary motivation for this change is to make any future lowering of the `max_methods` setting of the interface callables safer. To make the tests pass, fix a recent regression in base/strings/search.jl. --- base/strings/search.jl | 14 ++++++-- test/choosetests.jl | 2 +- test/interface_callables.jl | 72 +++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 test/interface_callables.jl diff --git a/base/strings/search.jl b/base/strings/search.jl index 90e02a0e5792c..81ba5e2c7c4bb 100644 --- a/base/strings/search.jl +++ b/base/strings/search.jl @@ -102,8 +102,18 @@ struct RvCharPosIter{S} last_char_byte::UInt8 end -IteratorSize(s::Type{<:Union{FwCharPosIter, RvCharPosIter}}) = SizeUnknown() -eltype(::Type{<:Union{FwCharPosIter, RvCharPosIter}}) = Int +@nospecializeinfer function IteratorSize(@nospecialize unused::Type{<:FwCharPosIter}) + SizeUnknown() +end +@nospecializeinfer function eltype(@nospecialize unused::Type{<:FwCharPosIter}) + Int +end +@nospecializeinfer function IteratorSize(@nospecialize unused::Type{<:RvCharPosIter}) + SizeUnknown() +end +@nospecializeinfer function eltype(@nospecialize unused::Type{<:RvCharPosIter}) + Int +end function RvCharPosIter(s::Union{String, SubString{String}}, c::AbstractChar) char = Char(c)::Char diff --git a/test/choosetests.jl b/test/choosetests.jl index 17970313da01f..2390d8d324fca 100644 --- a/test/choosetests.jl +++ b/test/choosetests.jl @@ -7,7 +7,7 @@ const STDLIBS = filter!(x -> isfile(joinpath(STDLIB_DIR, x, "src", "$(x).jl")), const TESTNAMES = [ "subarray", "core", "compiler", "compiler_extras", "worlds", "atomics", - "keywordargs", "numbers", "subtype", + "keywordargs", "numbers", "subtype", "interface_callables", "char", "strings", "triplequote", "unicode", "intrinsics", "dict", "hashing", "iobuffer", "staged", "offsetarray", "arrayops", "tuple", "reduce", "reducedim", "abstractarray", diff --git a/test/interface_callables.jl b/test/interface_callables.jl new file mode 100644 index 0000000000000..e2818e57a8665 --- /dev/null +++ b/test/interface_callables.jl @@ -0,0 +1,72 @@ +using Test + +module ExampleTypes end + +@testset "interface callables" begin + Base.@nospecializeinfer function max_methods_of(@nospecialize callable::Any) + raw = Base._stable_typeof(callable).name.max_methods + if iszero(raw) + 3 # default value, TODO: how to avoid hardcoding this? + else + Int(raw) + end + end + """ + interface_callables::Dict{DataType, Vector{Any}} + + In each key-value pair: + + * the key is the direct supertype of the newly-defined type + + * the values are the applicable interface callables + """ + interface_callables = let + nums = Any[widen, zero, one, oneunit] + keys_and_values = Any[ + (Any => Any[eltype, Base.IteratorSize, Base.IteratorEltype]), + (DenseVector{Float32} => Any[Base.elsize]), + (Real => nums), + (AbstractFloat => nums), + (Integer => nums), + ] + Dict{DataType, Vector{Any}}(keys_and_values) + end + interface_callables_supertypes = collect(DataType, keys(interface_callables)) + function example_type_name(i::Int, j::Int) + n = string('_', i, '_', j) + Symbol(n) + end + function tests(interface_callables, interface_callables_supertypes) + for (i, supertype) ∈ enumerate(interface_callables_supertypes) + for (j, callable) ∈ enumerate(interface_callables[supertype]) + typ = getproperty(ExampleTypes, example_type_name(i, j)) + method_match_length = length(methods(callable, Tuple{Type{<:typ}})) + # `max_methods` is high-enough for good inference + @test method_match_length ≤ max_methods_of(callable) + # don't match an excessive number of methods for the newly-defined type + @test method_match_length ≤ 2 + end + end + end + for (i, supertype) ∈ enumerate(interface_callables_supertypes) # define types + for (j, _) ∈ enumerate(interface_callables[supertype]) + type_name = example_type_name(i, j) + @eval ExampleTypes struct $type_name{X} <: $supertype + x::X + end + end + end + @testset "first pass: without any new method defined for the new type" begin + tests(interface_callables, interface_callables_supertypes) + end + for (i, supertype) ∈ enumerate(interface_callables_supertypes) # define methods for new types + for (j, callable) ∈ enumerate(interface_callables[supertype]) + callable_name = nameof(callable) + type_name = example_type_name(i, j) + @eval ExampleTypes function Base.$callable_name(::Type{<:$type_name}) end + end + end + @testset "second pass: with a new method defined for the new type" begin + tests(interface_callables, interface_callables_supertypes) + end +end