diff --git a/Project.toml b/Project.toml index 5843e68..6acfbb8 100644 --- a/Project.toml +++ b/Project.toml @@ -4,12 +4,14 @@ keywords = ["testing", "mocking"] license = "MIT" desc = "Allows Julia function calls to be temporarily overloaded for purpose of testing" author = ["Curtis Vogt"] -version = "0.7.2" +version = "0.7.3" [deps] +Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" ExprTools = "e2ba6199-217a-4e67-a87a-7c52f15ade04" [compat] +Compat = "3.9" ExprTools = "0.1" julia = "1" diff --git a/src/Mocking.jl b/src/Mocking.jl index 62e8f0d..d29c179 100644 --- a/src/Mocking.jl +++ b/src/Mocking.jl @@ -1,5 +1,6 @@ module Mocking +using Compat: mergewith using ExprTools: splitdef, combinedef export @patch, @mock, Patch, apply diff --git a/src/patch.jl b/src/patch.jl index d36a9b3..14f6cd7 100644 --- a/src/patch.jl +++ b/src/patch.jl @@ -45,6 +45,36 @@ end PatchEnv(debug::Bool=false) = PatchEnv(Dict{Any,Vector{Function}}(), debug) +function Base.:(==)(pe1::PatchEnv, pe2::PatchEnv) + return pe1.mapping == pe2.mapping && pe1.debug == pe2.debug +end + +""" + merge(pe1::PatchEnv, pe2::PatchEnv) -> PatchEnv + +Merge the two `PatchEnv` instances. + +This is done in such a way that the following always holds: + +``` +patches_1 = Patch[...] +patches_2 = Patch[...] +patches = vcat(patches_1, patches_2) + +pe1 = PatchEnv(patches_1) +pe2 = PatchEnv(patches_2) +pe = PatchEnv(patches) + +@assert pe == merge(pe1, pe2) +``` + +The `debug` flag will be set to true if either `pe1` or `pe2` have it set to true. +""" +function Base.merge(pe1::PatchEnv, pe2::PatchEnv) + mapping = mergewith(vcat, pe1.mapping, pe2.mapping) + return PatchEnv(mapping, pe1.debug || pe2.debug) +end + function apply!(pe::PatchEnv, p::Patch) alternate_funcs = get!(Vector{Function}, pe.mapping, p.target) push!(alternate_funcs, p.alternate) @@ -55,11 +85,49 @@ function apply!(pe::PatchEnv, patches) for p in patches apply!(pe, p) end + return pe end +""" + apply(body::Function, patches; debug::Bool=false) + apply(body::Function, pe::PatchEnv) + +Convenience function to run `body` in the context of the given `patches`. + +This is intended to be used with do-block notation, e.g.: + +``` +patch = @patch ... +apply(patch) do + ... +end +``` + +## Nesting + +Note that calls to apply will nest the patches that are applied. If multiple patches +are made to the same method, the innermost patch takes precedence. + +The following two examples are equivalent: + +``` +patch_2 = @patch ... +apply([patch, patch_2]) do + ... +end +``` + +``` +apply(patch) do + apply(patch_2) do + ... + end +end +``` +""" function apply(body::Function, pe::PatchEnv) prev_pe = get_active_env() - set_active_env(pe) + set_active_env(merge(prev_pe, pe)) try return body() finally diff --git a/test/merge.jl b/test/merge.jl new file mode 100644 index 0000000..4e5559a --- /dev/null +++ b/test/merge.jl @@ -0,0 +1,28 @@ +@testset "merge PatchEnv instances" begin + multiply(x::Number) = 2x + multiply(x::Int) = 2x - 1 + add(x::Number) = x + 2 + add(x::Int) = x + 1 + + patches = Patch[ + @patch multiply(x::Integer) = 3x + @patch multiply(x::Int) = 4x + @patch add(x::Int) = x + 4 + ] + + @testset "simple" begin + pe1 = Mocking.PatchEnv(patches[1]) + pe2 = Mocking.PatchEnv(patches[2:3]) + pe = Mocking.PatchEnv(patches) + + @test pe == merge(pe1, pe2) + end + + @testset "debug flag" begin + pe1 = Mocking.PatchEnv(patches[1], true) + pe2 = Mocking.PatchEnv(patches[2:3]) + pe = Mocking.PatchEnv(patches, true) + + @test pe == merge(pe1, pe2) + end +end diff --git a/test/nested_apply.jl b/test/nested_apply.jl new file mode 100644 index 0000000..1b6cbaa --- /dev/null +++ b/test/nested_apply.jl @@ -0,0 +1,78 @@ +# Nesting calls to apply should take the appropriate union of patches. +@testset "nested apply calls" begin + multiply(x::Number) = 2x + multiply(x::Int) = 2x - 1 + add(x::Number) = x + 2 + add(x::Int) = x + 1 + + @testset "simple" begin + patches = Patch[ + @patch multiply(x::Integer) = 3x + @patch multiply(x::Int) = 4x + @patch add(x::Int) = x + 4 + ] + + apply(patches) do + @test (@mock multiply(2)) == 8 + @test (@mock multiply(0x2)) == 0x6 + @test (@mock multiply(2//1)) == 4//1 + @test (@mock add(2//1)) == 4//1 + @test (@mock add(2)) == 6 + end + + apply(patches[1]) do + @test (@mock multiply(2)) == 6 + @test (@mock multiply(0x2)) == 0x6 + @test (@mock multiply(2//1)) == 4//1 + @test (@mock add(2//1)) == 4//1 + @test (@mock add(2)) == 3 + + apply(patches[2]) do + @test (@mock multiply(2)) == 8 + @test (@mock multiply(0x2)) == 0x6 + @test (@mock multiply(2//1)) == 4//1 + @test (@mock add(2//1)) == 4//1 + @test (@mock add(2)) == 3 + + apply(patches[3]) do + @test (@mock multiply(2)) == 8 + @test (@mock multiply(0x2)) == 0x6 + @test (@mock multiply(2//1)) == 4//1 + @test (@mock add(2//1)) == 4//1 + @test (@mock add(2)) == 6 + end + + @test (@mock multiply(2)) == 8 + @test (@mock multiply(0x2)) == 0x6 + @test (@mock multiply(2//1)) == 4//1 + @test (@mock add(2//1)) == 4//1 + @test (@mock add(2)) == 3 + end + + @test (@mock multiply(2)) == 6 + @test (@mock multiply(0x2)) == 0x6 + @test (@mock multiply(2//1)) == 4//1 + @test (@mock add(2//1)) == 4//1 + @test (@mock add(2)) == 3 + end + end + + @testset "repeated patch" begin + patches = Patch[ + @patch multiply(x::Integer) = 3x + @patch multiply(x::Integer) = 4x + ] + + apply(patches) do + @test (@mock multiply(2)) == 8 + end + + apply(patches[1]) do + @test (@mock multiply(2)) == 6 + apply(patches[2]) do + @test (@mock multiply(2)) == 8 + end + @test (@mock multiply(2)) == 6 + end + end +end diff --git a/test/runtests.jl b/test/runtests.jl index a2c8db5..2a7932d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -26,4 +26,6 @@ using Mocking: anon_morespecific, anonymous_signature, dispatch, type_morespecif include("anonymous-param.jl") include("reuse.jl") include("args.jl") + include("merge.jl") + include("nested_apply.jl") end