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

Introduce ScopedValue support in a breaking change #129

Merged
merged 4 commits into from
Jul 23, 2024
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 .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
- "1.0" # Oldest supported version
- "1.6" # LTS
- "1" # Latest Release
- "pre" # ScopedValue support introduced in Julia 1.11
os:
- ubuntu-latest
arch:
Expand Down
7 changes: 7 additions & 0 deletions src/Mocking.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ module Mocking
using Compat: mergewith
using ExprTools: splitdef, combinedef

# Available in Julia 1.11+: https://github.com/JuliaLang/julia/pull/50958
# We cannot use ScopedValues.jl for backwards compatability as that implementation breaks
# `@test_logs`.
if VERSION >= v"1.11.0-DEV.482"
using Base: ScopedValue, with
end

export @patch, @mock, Patch, apply

include("expr.jl")
Expand Down
2 changes: 1 addition & 1 deletion src/mock.jl
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function get_alternate(pe::PatchEnv, target, args...)
end
end

get_alternate(target, args...) = get_alternate(get_active_env(), target, args...)
get_alternate(target, args...) = get_alternate(PATCH_ENV[], target, args...)

function _debug_msg(method::Union{Method,Nothing}, target, args)
call = "$target($(join(map(arg -> "::$(Core.Typeof(arg))", args), ", ")))"
Expand Down
29 changes: 19 additions & 10 deletions src/patch.jl
Original file line number Diff line number Diff line change
Expand Up @@ -197,19 +197,28 @@
function apply end

function apply(body::Function, pe::PatchEnv)
prev_pe = get_active_env()
set_active_env(merge(prev_pe, pe))
try
return body()
finally
set_active_env(prev_pe)
end
merged_pe = merge(PATCH_ENV[], pe)
return with_active_env(body, merged_pe)
end

function apply(body::Function, patches; debug::Bool=false)
return apply(body, PatchEnv(patches, debug))
end

const PATCH_ENV = Ref{PatchEnv}(PatchEnv())
set_active_env(pe::PatchEnv) = (PATCH_ENV[] = pe)
get_active_env() = PATCH_ENV[]
# https://github.com/JuliaLang/julia/pull/50958
if VERSION >= v"1.11.0-DEV.482"
const PATCH_ENV = ScopedValue(PatchEnv())
with_active_env(body::Function, pe::PatchEnv) = with(body, PATCH_ENV => pe)

Check warning on line 211 in src/patch.jl

View check run for this annotation

Codecov / codecov/patch

src/patch.jl#L211

Added line #L211 was not covered by tests
else
const PATCH_ENV = Ref{PatchEnv}(PatchEnv())

function with_active_env(body::Function, pe::PatchEnv)
old_pe = PATCH_ENV[]
try
PATCH_ENV[] = pe
body()
finally
PATCH_ENV[] = old_pe
end
end
end
17 changes: 14 additions & 3 deletions test/async.jl → test/async-scope.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@testset "tasks" begin
# Async tasks should consistently use the patch environment (if any) they started with.
@testset "async scope" begin
c = Condition()
ch = Channel{String}(1)
f() = "original"
Expand All @@ -20,7 +21,12 @@
@test (@mock f()) == "mocked"

notify(c)
@test_broken take!(ch) == "original"
# https://github.com/JuliaLang/julia/pull/50958
if VERSION >= v"1.11.0-DEV.482"
@test take!(ch) == "original"
else
@test_broken take!(ch) == "original"
end

# Task started inside patched context should call patched functions.
@async background()
Expand All @@ -35,6 +41,11 @@
end

notify(c)
@test_broken take!(ch) == "mocked"
# https://github.com/JuliaLang/julia/pull/50958
if VERSION >= v"1.11.0-DEV.482"
@test take!(ch) == "mocked"
else
@test_broken take!(ch) == "mocked"
end
end
end
17 changes: 13 additions & 4 deletions test/async-world-ages.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ using Test
using Mocking

# Issue #108
# TODO: Test is mostly redundant with "async-scope.jl". We may want to update those tests to
# also validate we can run in a later world age.
@testset "patching an async task from an earlier world age" begin
function foo(x)
@mock bar(x)
Expand All @@ -17,9 +19,10 @@ using Mocking
intial_world_age = Base.get_world_counter()
end

# Start a background, async task which blocks until bar() is patched, so that we
# can test that patches to functions defined in later world ages can be called
# from mocks in a Task running in an earlier world age.
# Start a background async task. For Julia 1.11+ this task will consistently use the
# patch environment which it was started in. In earlier versions of Julia we can patch
# this task while it's running can call functions defined in a later world age than the
# world age of this task.
ch = Channel() do ch
# Block until we've started the patch
v1 = take!(ch)
Expand All @@ -40,6 +43,12 @@ using Mocking
# Release the background task
put!(ch, 2)
# Fetch the task's result
@test take!(ch) == 20

# https://github.com/JuliaLang/julia/pull/50958
if VERSION >= v"1.11.0-DEV.482"
@test take!(ch) == 2
else
@test_broken take!(ch) == 2
end
end
end
20 changes: 10 additions & 10 deletions test/concept.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@
for p in patches
Mocking.apply!(pe, p)
end
Mocking.set_active_env(pe)

@test (@mock multiply(2)) == 8 # calls mocked `multiply(::Int)`
@test (@mock multiply(0x2)) == 0x6 # calls mocked `multiply(::Integer)`
@test (@mock multiply(2//1)) == 4//1 # calls original `multiply(::Number)`
Mocking.apply(pe) do
@test (@mock multiply(2)) == 8 # calls mocked `multiply(::Int)`
@test (@mock multiply(0x2)) == 0x6 # calls mocked `multiply(::Integer)`
@test (@mock multiply(2//1)) == 4//1 # calls original `multiply(::Number)`

@test (@mock multiply(2)) != multiply(2)
@test (@mock multiply(0x2)) != multiply(0x2)
@test (@mock multiply(2//1)) == multiply(2//1)
@test (@mock multiply(2)) != multiply(2)
@test (@mock multiply(0x2)) != multiply(0x2)
@test (@mock multiply(2//1)) == multiply(2//1)
end

# Clean env
pe = Mocking.PatchEnv()
Mocking.set_active_env(pe)
# Patch environment has been reset back to original clean state
@test Mocking.PATCH_ENV[] == Mocking.PatchEnv()

# Ensure that original behaviour is restored
@test (@mock multiply(2)) == 3
Expand Down
2 changes: 1 addition & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Mocking.activate()
include("args.jl")
include("merge.jl")
include("nested_apply.jl")
include("async.jl")
include("async-scope.jl")
include("issues.jl")
include("activate.jl")
include("async-world-ages.jl")
Expand Down
Loading