diff --git a/.travis.yml b/.travis.yml index 0dae938..918c91c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,6 @@ os: - linux - osx julia: - - 0.6 - - 0.7 - 1.0 - nightly env: @@ -17,9 +15,9 @@ notifications: script: - if [[ -a .git/shallow ]]; then git fetch --unshallow; fi - | - julia --depwarn=error -e ' - VERSION >= v"0.7.0-DEV.3656" && using Pkg - if VERSION >= v"0.7.0-DEV.5183" && (isfile("Project.toml") || isfile("JuliaProject.toml")) + julia -e ' + using Pkg + if isfile("Project.toml") || isfile("JuliaProject.toml") Pkg.build(); Pkg.test(coverage=true) else Pkg.clone(pwd()); Pkg.build("Mocking"); Pkg.test("Mocking"; coverage=true) @@ -27,7 +25,6 @@ script: after_success: - | julia -e ' - VERSION >= v"0.7.0-DEV.3656" && using Pkg - VERSION >= v"0.7.0-DEV.5183" || cd(Pkg.dir("Mocking")) + using Pkg Pkg.add("Coverage"); using Coverage Codecov.submit(process_folder())' diff --git a/README.md b/README.md index 5479126..3866fc4 100644 --- a/README.md +++ b/README.md @@ -41,27 +41,21 @@ result = randdev(n) @test length(result) == n ``` -How could we create a test that shows the output of the function is reversed? Mocking.jl -provides the `@mock` macro which allows package developers to temporarily overload a -specific calls in their package. In this example we will apply `@mock` to the `open` call -in `randdev`: +How could we create a test that shows the output of the function is reversed? + Mocking.jl provides a mechanism which allows package developers to temporarily overload a +specific calls in their package. In this example we will mock the `open` call +in `randdev`. +No changes are required at the call site. -```julia -using Mocking - -function randdev(n::Integer) - @mock open("/dev/urandom") do fp - reverse(read(fp, n)) - end -end -``` -With the call site being marked as "mockable" we can now write a testcase which allows -us to demonstrate the reversing behaviour within the `randdev` function: +We just need to write a testcase which allows +us to demonstrate the reversing behaviour within the `randdev` function. +This is done using the `@patch` macro, to define a patch, +which is applied to a block of code using the `apply` function. +As shown in the example below: ```julia using Mocking -Mocking.enable() # Need to enable before we import any code using the `@mock` macro using Base.Test import ...: randdev @@ -79,38 +73,22 @@ apply(patch) do @test randdev(n) == convert(Array{UInt8}, n:-1:1) end -# Outside of the scope of the patched environment `@mock` is essentially a no-op +# Outside of the scope of the patched environment behavour is +# as it was before @test randdev(n) != convert(Array{UInt8}, n:-1:1) ``` Gotchas ------- -Remember to: - -- use `@mock` at desired call sites -- start julia with `--compiled-modules=no` (`--compilecache=no` for ≤0.6) or pass `force=true` to `Mocking.enable` -- run `Mocking.enable` before importing the module(s) being tested - -Notes ------ - -Mocking.jl is intended to be used for testing only and will not affect the performance of -your code when using `@mock`. In fact the `@mock` is actually a no-op when `Mocking.enable` -is not called. One side effect of this behaviour is that pre-compiled packages won't test -correctly with Mocking unless you start Julia with `--compiled-modules=no` (≥0.7) or -`--compilecache=no` (≤0.6). - -``` -$ julia --compilecache=no -e Pkg.test("...") -``` + - Remember to `using`/`import` functions before you `@patch` them. + - You can not mock a method that does not exist. -Alternatively you can use `Mocking.enable(force=true)` to automatically disable using -package precompilation for you (experimental). Make sure to call `enable` before the you -importing the module you are testing. +Mocking.jl relies heavily on [Cassette.jl](https://github.com/jrevels/Cassette.jl). +Many issues that affect Cassette.jl will also affect Mocking.jl. License ------- -Mocking.jl is provided under the [MIT "Expat" License](LICENSE.md). \ No newline at end of file +Mocking.jl is provided under the [MIT "Expat" License](LICENSE.md). diff --git a/REQUIRE b/REQUIRE index e9088f4..70bc248 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1,2 +1,3 @@ -julia 0.6 -Compat 0.59 \ No newline at end of file +julia 1.0 +MacroTools +Cassette 0.2 diff --git a/appveyor.yml b/appveyor.yml index c505b84..9cbb5ae 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,5 @@ environment: matrix: - - julia_version: 0.6 - - julia_version: 0.7 - julia_version: 1.0 - julia_version: latest diff --git a/src/Mocking.jl b/src/Mocking.jl index 865862c..f47fa84 100644 --- a/src/Mocking.jl +++ b/src/Mocking.jl @@ -1,247 +1,29 @@ __precompile__(true) module Mocking +using MacroTools +using Cassette +using Cassette: @context -using Compat: @__MODULE__, hasmethod, invokelatest, undef, @info, @warn include("expr.jl") include("bindings.jl") -include("options.jl") include("deprecated.jl") +include("patch.jl") +include("patchenv.jl") + export # Mocking.jl - @patch, @mock, Patch, apply, - # options.jl - DISABLE_COMPILED_MODULES_STR, DISABLE_COMPILED_MODULES_CMD - -# When ENABLED is false the @mock macro is a noop. -global ENABLED = false -global PATCH_ENV = nothing - -function enable(; force::Bool=false) - ENABLED::Bool && return # Abend early if enabled has already been set - global ENABLED = true - global PATCH_ENV = PatchEnv() - - if compiled_modules_enabled() - if force - # Disable using compiled modules when Mocking is enabled - set_compiled_modules(false) - else - @warn( - "Mocking.jl will probably not work when $COMPILED_MODULES_FLAG is ", - "enabled. Please start `julia` with `$DISABLE_COMPILED_MODULES_STR` ", - "or alternatively call `Mocking.enable(force=true).`", - ) - end - end -end - -struct Patch - signature::Expr - body::Function - modules::Set - # translation::Dict - - function Patch(signature::Expr, body::Function, translation::Dict) - trans = adjust_bindings(translation) - sig = name_parameters(absolute_signature(signature, trans)) - - # On VERSION >= v"0.5" - # modules = Set(b.args[1] for b in values(trans) if isa(b, Expr)) - modules = Set() - for b in values(trans) - if isa(b, Expr) - push!(modules, b.args[1]) - end - end - - new(sig, body, modules) - end -end - -# TODO: Find non-eval way to determine module locations of Types -# evaling in the @patch scope seems to be problematic for pre-compliation -# first(methods(x)).sig.types[2:end] - -# We can use the @patch macro to create a list of bindings used then pass that -# in as an array into Patch. At runtime the types and function names will be fully -# qualified - -# We can support optional parameters and keywords by using generic functions on -# 0.4 - -function convert(::Type{Expr}, p::Patch) - exprs = Expr[] + @patch, Patch, apply, + # deprecated.jl + @mock - # Generate imports for all required modules - for m in p.modules - bindings = splitbinding(m) - :Main in bindings && error("Mocking cannot handle bindings from Main.") - - for i in 1:length(bindings) - import_expr = if VERSION > v"0.7.0-DEV.3187" - Expr(:import, Expr(:., bindings[1:i]...)) - else - Expr(:import, bindings[1:i]...) - end - push!(exprs, import_expr) - end - end - - # Generate the new method which will call the user's patch function. We need to perform - # this call instead of injecting the body expression to support closures. - sig, body = p.signature, p.body - params = call_parameters(sig) - push!(exprs, Expr(:(=), sig, Expr(:block, Expr(:call, body, params...)))) - - return Expr(:block, exprs...) -end - -macro patch(expr::Expr) - if expr.head == :function - name = expr.args[1].args[1] - params = expr.args[1].args[2:end] - body = expr.args[2] - - # Short-form function syntax - elseif expr.head == :(=) && expr.args[1].head == :call - name = expr.args[1].args[1] - params = expr.args[1].args[2:end] - body = expr.args[2] - - # Anonymous function syntax - # elseif expr.head == :(->) - # TODO: Determine how this could be supported - else - throw(ArgumentError("expression is not a function definition")) - end - - signature = Expr(:call, name, params...) - - # Determine the bindings used in the signature - bindings = Bindings(signature) - - # Need to evaluate the body of the function in the context of the `@patch` macro in - # order to support closures. - # func = Expr(:(->), Expr(:tuple, params...), body) - func = Expr(:(=), Expr(:call, gensym(), params...), body) - - # Generate a translation between the external bindings and the runtime types and - # functions. The translation will be used to revise all bindings to be absolute. - translations = [Expr(:call, :(=>), QuoteNode(b), b) for b in bindings.external] - - return esc(:(Mocking.Patch( $(QuoteNode(signature)), $func, Dict($(translations...)) ))) -end - -struct PatchEnv - mod::Module - debug::Bool - - function PatchEnv(debug::Bool=false) - # Be careful not to call this code during pre-compilation otherwise we'll see the - # warning: "incremental compilation may be broken for this module" - m = Core.eval(Mocking, :(module $(gensym()) end)) - new(m, debug) - end -end - -function PatchEnv(patches::Array{Patch}, debug::Bool=false) - pe = PatchEnv(debug) - apply!(pe, patches) - return pe -end - -function PatchEnv(patch::Patch, debug::Bool=false) - pe = PatchEnv(debug) - apply!(pe, patch) - return pe -end - -function apply!(pe::PatchEnv, p::Patch) - Core.eval(pe.mod, convert(Expr, p)) -end - -function apply!(pe::PatchEnv, patches::Array{Patch}) - for p in patches - apply!(pe, p) - end -end - -function apply(body::Function, pe::PatchEnv) - original_pe = get_active_env() - set_active_env(pe) - try - return body() - finally - set_active_env(original_pe) - end -end - -function apply(body::Function, patches::Array{Patch}; debug::Bool=false) - apply(body, PatchEnv(patches, debug)) -end -function apply(body::Function, patch::Patch; debug::Bool=false) - apply(body, PatchEnv(patch, debug)) -end function ismocked(pe::PatchEnv, func_name::Symbol, args::Tuple) - if isdefined(pe.mod, func_name) - func = Core.eval(pe.mod, func_name) - types = map(arg -> isa(arg, Type) ? Type{arg} : typeof(arg), args) - exists = hasmethod(func, types) - - if pe.debug - @info("calling $func_name$(types)") - if exists - m = first(methods(func, types)) - @info("executing mocked function: $m") - else - m = first(methods(Core.eval(func_name), types)) - @info("executing original function: $m") - end - end - - return exists - end - return false + # TODO: redefine this in terms of `methodswith(pe.ctx, Cassette.overdub...)` + # If required + error("`ismocked` is not implemented") end - -set_active_env(pe::PatchEnv) = (global PATCH_ENV = pe) -get_active_env() = PATCH_ENV::PatchEnv - -macro mock(expr) - isa(expr, Expr) || error("argument is not an expression") - expr.head == :do && (expr = rewrite_do(expr)) - expr.head == :call || error("expression is not a function call") - ENABLED::Bool || return esc(expr) # @mock is a no-op when Mocking is not ENABLED - - func = expr.args[1] - func_name = QuoteNode(func) - args = filter(x -> !Mocking.iskwarg(x), expr.args[2:end]) - kwargs = extract_kwargs(expr) - - env_var = gensym("env") - args_var = gensym("args") - - # Note: The fix to Julia issue #265 (PR #17057) introduced changes where no compiled - # calls could be made to functions compiled afterwards. Since the `Mocking.apply` - # do-block syntax compiles the body of the do-block function before evaluating the - # "outer" function this means our patch functions will be compiled after the "inner" - # function. - result = quote - local $env_var = Mocking.get_active_env() - local $args_var = tuple($(args...)) - if Mocking.ismocked($env_var, $func_name, $args_var) - Mocking.invokelatest($env_var.mod.$func, $args_var...; $(kwargs...)) - else - $func($args_var...; $(kwargs...)) - end - end - - return esc(result) -end - end # module diff --git a/src/bindings.jl b/src/bindings.jl index 709bf7f..89ba3a1 100644 --- a/src/bindings.jl +++ b/src/bindings.jl @@ -167,24 +167,23 @@ end function ingest_signature!(b::Bindings, expr::Expr) if expr.head == :call - func = expr.args[1] + @capture(expr, + (func_){typeparams__}(args__) | + (func_)(args__) + ) || error("Not a valid function call.") # f(...) - if isa(func, Symbol) - push!(b.internal, func) + push!(b.external, func) # f{T}(...) - elseif isa(func, Expr) && func.head == :curly - push!(b.internal, func.args[1]) - for parametric in func.args[2:end] + if typeparams !== nothing + for parametric in typeparams ingest_parametric!(b, parametric) end - else - error("expression is not a valid function call: $func") end - # Function parameters and keywords - for parameter in expr.args[2:end] + # Function arguments and keywords + for parameter in args ingest_parameter!(b, parameter) end diff --git a/src/deprecated.jl b/src/deprecated.jl index dadd280..7644525 100644 --- a/src/deprecated.jl +++ b/src/deprecated.jl @@ -1,5 +1,17 @@ -import Base: @deprecate_binding, @deprecate -# BEGIN Mocking 0.5 deprecations +macro mock(expr) + w = Expr( + :macrocall, + Symbol("@warn"), + __source__, + "`@mock` is no longer used and can be removed.", + ) + quote + $w + $(esc(expr)) + end +end -# END Mocking 0.5 deprecations +function enable(;force::Bool=false) + Base.depwarn("`Mocking.enable` is no longer used and can be removed.", :enable) +end diff --git a/src/expr.jl b/src/expr.jl index bdf0b27..4fff439 100644 --- a/src/expr.jl +++ b/src/expr.jl @@ -25,35 +25,54 @@ julia> binding_expr(Dates.Hour) """ function binding_expr end -function binding_expr(m::Module) - joinbinding(fullname(m)...) +function binding_expr(source_module, m::Module) + localized_binding(source_module, m) end -function binding_expr(t::Type) +function binding_expr(source_module, t::Type) type_name = unwrap_unionall(t).name - joinbinding(fullname(type_name.module)..., type_name.name) + localized_binding(source_module, type_name.module, type_name.name) end -function binding_expr(u::Union) - a = binding_expr(u.a) - b = binding_expr(u.b) +function binding_expr(source_module, u::Union) + a = binding_expr(source_module, u.a) + b = binding_expr(source_module, u.b) if b.head == :curly && b.args[1] == :Union Expr(:curly, :Union, a, b.args[2:end]...) else Expr(:curly, :Union, a, b) end end -function binding_expr(f::Function) +function binding_expr(source_module, f::Function) if isa(f, Core.Builtin) return nameof(f) end m = parentmodule(f, Tuple) - joinbinding(fullname(m)..., nameof(f)) + localized_binding(source_module, m, nameof(f)) end +""" + localized_binding(abs_module, rel_module, [leaf]) + +Returns a fully qualified module name for `rel_module`, +where `abs_module` is one that imports `rel_module`. + +If `leaf` is passed in, then a fully qualified path to +`leaf` within `rel_module` is returned. +""" +function localized_binding(abs_module, rel_module, leaf=Tuple()) + if abs_module == rel_module || # FooMod.FooMod==FooMod + rel_module == Core # Core is always Bound so no need for relative path + return joinbinding(fullname(rel_module)..., leaf) + else + return joinbinding(fullname(abs_module)..., fullname(rel_module)..., leaf) + end +end + + -function adjust_bindings(translations::Dict) +function adjust_bindings(source_module, translations::Dict) new_trans = Dict() for (k, v) in translations - new_trans[k] = binding_expr(v) + new_trans[k] = binding_expr(source_module, v) end return new_trans end diff --git a/src/options.jl b/src/options.jl deleted file mode 100644 index 2582c19..0000000 --- a/src/options.jl +++ /dev/null @@ -1,69 +0,0 @@ -import Compat: fieldcount - -# Name of the `julia` command-line option -const COMPILED_MODULES_FLAG = if VERSION >= v"0.7.0-DEV.1698" - Symbol("compiled-modules") -else - :compilecache -end - -# Name of the field in the JLOptions structure which corresponds to the command-line option -const COMPILED_MODULES_FIELD = if VERSION >= v"0.7.0-DEV.1698" - :use_compiled_modules -else - :use_compilecache -end - -const DISABLE_COMPILED_MODULES_STR = "--$COMPILED_MODULES_FLAG=no" -const DISABLE_COMPILED_MODULES_CMD = `$DISABLE_COMPILED_MODULES_STR` - -# Generate a mutable version of JLOptions -let - T = Base.JLOptions - fields = [:($(fieldname(T,i))::$(fieldtype(T,i))) for i in 1:fieldcount(T)] - - @eval begin - mutable struct JLOptionsMutable - $(fields...) - end - end -end - -""" - compiled_modules_enabled() -> Bool - -Determine if the `julia` command line flag `--$COMPILED_MODULES_FLAG` has been set to "yes". -""" -function compiled_modules_enabled() - opts = Base.JLOptions() - field = COMPILED_MODULES_FIELD - - # When the field is set to `Symbol()` it means that compiled-modules is unsupported. - # If the compiled-modules field is undefined we assume that compiled-modules is enabled - # by default. - return field != Symbol() && (!isdefined(opts, field) || Bool(getfield(opts, field))) -end - -""" - set_compiled_modules(state::Bool) -> Void - -Override the `julia` command line flag `--$COMPILED_MODULES_FLAG` with a runtime setting. -Code run before this the value is modified will use the original setting. Not meant for -general purpose usage. -""" -function set_compiled_modules(state::Bool) - value = Base.convert(Int8, state) - - # Load the C global into a mutable Julia type - jl_options = cglobal(:jl_options, JLOptionsMutable) - opts = unsafe_load(jl_options) - - # Avoid modifying the global when the value hasn't changed - if getfield(opts, COMPILED_MODULES_FIELD) != value - @warn("Using experimental code which modifies jl_options global struct") - setfield!(opts, COMPILED_MODULES_FIELD, value) - unsafe_store!(jl_options, opts) - end - - nothing -end diff --git a/src/patch.jl b/src/patch.jl new file mode 100644 index 0000000..44b870d --- /dev/null +++ b/src/patch.jl @@ -0,0 +1,133 @@ +struct Patch + signature::Expr + body::Function +end + +function Patch( + signature::Expr, + body::Function, + source_module::Module, + translation::Dict) + + trans = adjust_bindings(source_module, translation) + sig = name_parameters(absolute_signature(signature, trans)) + + + Patch(sig, body) +end + + +macro patch(expr::Expr) + #TODO Make sure all varients of `where` work + @capture(expr, + function name_(params__) body_ end | + (name_(params__) = body_) + ) || throw(ArgumentError("expression is not a function definition")) + + signature = Expr(:call, name, params...) + + + + # Generate a translation between the external bindings and the runtime types and + # functions. The translation will be used to revise all bindings to be absolute. + bindings = Bindings(signature) + translations = [Expr(:call, :(=>), QuoteNode(b), b) for b in bindings.external] + + + # Need to evaluate the body of the function in the context of the `@patch` macro in + # order to support closures. + # func = Expr(:(->), Expr(:tuple, params...), body) + func = Expr(:(=), Expr(:call, gensym(Symbol(name, :_patch)), params...), body) + + + return esc(:(Mocking.Patch( + $(QuoteNode(signature)), + $func, + $(QuoteNode(__module__)), + Dict($(translations...)) + ))) +end + + + +""" + code_for_apply_patch(ctx_name, patch) + +Returns an `Expr` that when evaluted will apply this patch +to the context of the name that was passed in. + +`ctx_name` a Symbol that is the name of the context +Should be unique per `apply` block +""" +function code_for_apply_patch(ctx_name, patch) + @capture(patch.signature, + (fname_(args__; kwargs__)) | + (fname_(args__)) + ) || error("Invalid patch signature: `$(patch.signature)`") + + + + # This is the basic parts of any cassette execture defenition AST + + if kwargs === nothing + method_head = Expr( + :call, + :(Cassette.overdub), + :(ctx::$ctx_name), # Context + :(::typeof($fname)), # Function + args...) + else + sig_params = patch.signature.args[2:end] # Important: this is a copy + @assert sig_params[1].head == :parameters + # sig_params[1] is the kwargs stuff + # sig_params[2:end] are the normal/optional arguments + # We need to splice in the Cassette suff before there + insert!(sig_params, 2, :(ctx::$ctx_name)) # Context + insert!(sig_params, 3, :(::typeof($fname))) # Function + + method_head = Expr( + :call, + :(Cassette.overdub), + sig_params... + ) + end + + # This boils down to + # Cassette.overdub(::$ContextName, ::typeof($functionname), args...) = body(args...) + # but we have to get the types and numbers and names of arguments all in there right + return quote + # Note: we are called recurse from overdub (which is itself triggered by a recurse) + # This lets our mocks depend on other mocks, + # see explaination at https://github.com/jrevels/Cassette.jl/issues/87 + $(method_head) = Cassette.recurse(ctx, ()->$(code_for_invoke_body(patch))) + + $(code_for_kwarg_overdub_overload(ctx_name, fname)) + end +end + +""" + code_for_invoke_body(patch) + +Generates the AST to call the patches body with the correctly named arguments. +""" +function code_for_invoke_body(patch) + call_params = call_parameters(patch.signature) + return Expr(:call, patch.body, call_params...) +end + + +function code_for_kwarg_overdub_overload(ctx_name, fname) + # Keyword arguments see https://github.com/jrevels/Cassette.jl/issues/48#issuecomment-440605481 + + return quote + function Cassette.overdub( + ctx::$(ctx_name), + ::Core.kwftype(typeof($fname)), + kwargs::Any, + ::typeof($fname), + args... + ) + Cassette.overdub(ctx, $fname, args...; kwargs...) + end + end +end diff --git a/src/patchenv.jl b/src/patchenv.jl new file mode 100644 index 0000000..8cdcda6 --- /dev/null +++ b/src/patchenv.jl @@ -0,0 +1,71 @@ + +# The context used for all patch enviroments +# Parameterized by its metadata which is a `PatchEnvMetadata{ID}``, +# Where ID is a unique symbol for each enviroment +# which thus allows different patch enviroments to dispatch uniquely. +@context MockingContext +struct PatchEnvMetadata{ID} end + +struct PatchEnv{CTX <: Cassette.Context} + ctx::CTX +end + +function PatchEnv() + patchenv_id = gensym() + patchenv_meta = PatchEnvMetadata{patchenv_id}() + ctx = MockingContext(metadata = patchenv_meta) + return PatchEnv{typeof(ctx)}(ctx) +end + +function PatchEnv(patch) + pe = PatchEnv() + apply!(pe, patch) + return pe +end + +""" + apply!(pe::PatchEnv, patch[es]) + +Applies the patches to the `PatchEnv`. + +### Implememtation note: +This adds new methods to the `Cassette.overdub` for the context of the `PatchEnv`. +""" +function apply!(pe::PatchEnv{CTX}, p::Patch) where CTX + return eval(code_for_apply_patch(CTX, p)) +end + +function apply!(pe::PatchEnv, patches::Array{Patch}) + for p in patches + apply!(pe, p) + end +end + +""" + apply(body::Function, patchenv|patch|patches) + +Apply the patches for the duration of the body. +This essentially activates the patch enviroment +(which will be created if required). + +Write this as + +``` +apply(patches) do + @test foo(1) == "bar" + @test foo(2) == "barbar" +end +``` + +Any method that is has a patch defined in `patches` +will be replaced with it's mock during the invocation of `foo` +(and the other code in the body). +""" +function apply(body::Function, pe::PatchEnv) + # Some kind of world-age issue means we can't just use recurse directly. + return Base.invokelatest(Cassette.recurse, pe.ctx, body) +end + +function apply(body::Function, patch) + return apply(body, PatchEnv(patch)) +end diff --git a/test/anonymous-param.jl b/test/anonymous-param.jl index 53805d5..badabe6 100644 --- a/test/anonymous-param.jl +++ b/test/anonymous-param.jl @@ -1,10 +1,11 @@ # Issue #15 +f(::Type{T}, n::Int) where T<:Unsigned = rand(T, n) + @testset "anonymous parameter" begin - f(::Type{T}, n::Int) where T<:Unsigned = rand(T, n) patch = @patch f(::Type{UInt8}, n::Int) = collect(UnitRange{UInt8}(1:n)) apply(patch) do - @test (@mock f(UInt8, 2)) == [0x01, 0x02] + @test f(UInt8, 2) == [0x01, 0x02] end end diff --git a/test/bindings/ingest_signature.jl b/test/bindings/ingest_signature.jl index 4cbccf9..6394e6d 100644 --- a/test/bindings/ingest_signature.jl +++ b/test/bindings/ingest_signature.jl @@ -4,54 +4,51 @@ import Mocking: Bindings, ingest_signature! @test @valid_method f(x) = x b = Bindings() ingest_signature!(b, :(f(x) = x).args[1]) - @test b.internal == Set([:f, :x]) - @test b.external == Set() + @test b.internal == Set([:x]) + @test b.external == Set([:f]) + - if v"0.6" <= VERSION < v"0.7-" - @test @valid_method f{T}(::Type{T}) = T # Syntax deprecated in 0.7 - end b = Bindings() ingest_signature!(b, :(f{T}(::Type{T}) = T).args[1]) - @test b.internal == Set([:f, :T]) - @test b.external == Set([:Type]) + @test b.internal == Set([:T]) + @test b.external == Set([:f, :Type]) + @test @valid_method f(::Type{T}) where T = T b = Bindings() ingest_signature!(b, :(f{T}(::Type{T}) = T).args[1]) - @test b.internal == Set([:f, :T]) - @test b.external == Set([:Type]) + @test b.internal == Set([:T]) + @test b.external == Set([:Type, :f]) + - if v"0.6" <= VERSION < v"0.7-" - @test @valid_method f{T,S<:T}(x::T, y::S) = (x, y) - end b = Bindings() ingest_signature!(b, :(f{T,S<:T}(x::T, y::S) = (x, y)).args[1]) - @test b.internal == Set([:f, :T, :S, :x, :y]) - @test b.external == Set() + @test b.internal == Set([:T, :S, :x, :y]) + @test b.external == Set([:f]) @test @valid_method f(x::T, y::S) where S<:T where T = (x, y) b = Bindings() ingest_signature!(b, :(f(x::T, y::S) where S<:T where T = (x, y)).args[1]) - @test b.internal == Set([:f, :T, :S, :x, :y]) - @test b.external == Set() + @test b.internal == Set([:T, :S, :x, :y]) + @test b.external == Set([:f]) @test @valid_method f(x::T, y::S) where {T,S<:T} = (x, y) b = Bindings() ingest_signature!(b, :(f(x::T, y::S) where {T,S<:T} = (x, y)).args[1]) - @test b.internal == Set([:f, :T, :S, :x, :y]) - @test b.external == Set() + @test b.internal == Set([:T, :S, :x, :y]) + @test b.external == Set([:f]) @test @valid_method f(x=f) = x # `f` the argument default refers the the function `f` b = Bindings() ingest_signature!(b, :(f(x=f))) - @test b.internal == Set([:f, :x]) - @test b.external == Set() + @test b.internal == Set([:x]) + @test b.external == Set([:f]) @test @valid_method f(f) = f # `f` the function and `f` the parameter variable b = Bindings() ingest_signature!(b, :(f(f))) - @test b.internal == Set([:f]) # Wrong? Technically there are two separate `f`s here - @test b.external == Set() + @test b.internal == Set([:f]) # Technically there are two separate `f`s here + @test b.external == Set([:f]) # f = 1; f(x=f) = f # Error end diff --git a/test/closure.jl b/test/closure.jl index 2513bcf..cde794a 100644 --- a/test/closure.jl +++ b/test/closure.jl @@ -1,11 +1,120 @@ -@testset "closure" begin - magic(x) = false +# Test that Mocking works +# with patches referencing functions at various scopes + +global_scope() = "foo" +global_patchfun() = "bar" +@testset "Not a closure Global scope" begin + # Check normaolly not mocked + @test global_scope() == "foo" + + # Create a patched version of func() and return the alternative version + global_patch = (@patch global_scope() = global_patchfun()) + apply(global_patch) do + @test global_scope() == "bar" + end + + # Outside the `apply` should return to the original behaviour + @test global_scope() == "foo" +end + + +magic(x) = false +@testset "more complex closure" begin sentinel = gensym("sentinel") @test magic(sentinel) == false # Getting closers to work means having a function created in the current scope patch = @patch magic(x) = x == sentinel apply(patch) do - @test (@mock magic(sentinel)) == true + @test magic(sentinel) == true + end +end + +function_scope() = "foo" +@testset "Local scope within a function" begin + function scope_test() + @test function_scope() == "foo" + inner() = "bar" + + patch = @patch function_scope() = inner() + apply(patch) do + @test function_scope() == "bar" + end + + @test function_scope() == "foo" + end + scope_test() +end + + +let_scope() = "foo" +@testset "Local scope within a let block" begin + let + @test let_scope() == "foo" + inner() = "bar" + + patch = @patch let_scope() = inner() + apply(patch) do + @test let_scope() == "bar" + end + + @test let_scope() == "foo" end end + +###################### Test modules + +module FooBar + nonexported() = "bar" + exported() = "bling" + export exported +end + +using .FooBar + +module_scope() = "foo" +@testset "Module scope" begin + @testset "Not imported" begin + @test module_scope() == "foo" + + patch = @patch module_scope() = FooBar.nonexported() + apply(patch) do + @test module_scope() == "bar" + end + + @test module_scope() == "foo" + end + + @testset "Imported" begin + patch = @patch module_scope() = exported() + apply(patch) do + @test module_scope() == "bling" + end + + @test module_scope() == "foo" + end +end + +############################ +# Test for nested modules +module NM_ModA + module NM_ModB + abstract type NM_AbstractFoo end + struct NM_Foo <: NM_AbstractFoo + x::String + end + end # ModB + + NM_bar(f::NM_ModB.NM_AbstractFoo) = "default" + NM_baz(f::NM_ModB.NM_AbstractFoo) = NM_bar(f) +end # ModA + +import .NM_ModA +import .NM_ModA: NM_bar, NM_baz, NM_ModB + +@testset "nested modules" begin + p = @patch NM_bar(f::NM_ModB.NM_AbstractFoo) = "mock" + Mocking.apply(p) do + @test NM_baz(NM_ModB.NM_Foo("X")) == "mock" + end +end diff --git a/test/compiled-modules.jl b/test/compiled-modules.jl deleted file mode 100644 index 65bf616..0000000 --- a/test/compiled-modules.jl +++ /dev/null @@ -1,7 +0,0 @@ -@testset "compiled_modules_enabled" begin - if VERSION >= v"0.7.0-DEV.1698" - @test Mocking.compiled_modules_enabled() == Bool(Base.JLOptions().use_compiled_modules) - else - @test Mocking.compiled_modules_enabled() == Bool(Base.JLOptions().use_compilecache) - end -end diff --git a/test/concept.jl b/test/concept.jl index af447b4..c00ba6f 100644 --- a/test/concept.jl +++ b/test/concept.jl @@ -1,53 +1,48 @@ -# Test the basic concept behind call overloading -@testset "concept" begin - multiply(x::Number) = 2x - multiply(x::Int) = 2x - 1 +#| Test the basic concept behind call overloading +multiply(x::Number) = 2x +multiply(x::Int) = 2x - 1 - @test (@mock multiply(2)) == 3 - @test (@mock multiply(0x2)) == 0x4 - @test (@mock multiply(2//1)) == 4//1 +@testset "concept" begin + @test multiply(2) == 3 + @test multiply(0x2) == 0x4 + @test multiply(2//1) == 4//1 - @test (@mock multiply(2)) == multiply(2) - @test (@mock multiply(0x2)) == multiply(0x2) - @test (@mock multiply(2//1)) == multiply(2//1) + @test multiply(2) == multiply(2) + @test multiply(0x2) == multiply(0x2) + @test multiply(2//1) == multiply(2//1) patches = Patch[ - @patch multiply(x::Integer) = 3x - @patch multiply(x::Int) = 4x + @patch(multiply(x::Integer) = 3x), + @patch(multiply(x::Int) = 4x) ] pe = Mocking.PatchEnv() 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)` - @test (@mock multiply(2)) != multiply(2) - @test (@mock multiply(0x2)) != multiply(0x2) - @test (@mock multiply(2//1)) == multiply(2//1) + apply(pe) do + @test multiply(2) == 8 # calls mocked `multiply(::Int)` + @test multiply(0x2) == 0x6 # calls mocked `multiply(::Integer)` + @test multiply(2//1) == 4//1 # calls original `multiply(::Number)` + end # Clean env - pe = Mocking.PatchEnv() - Mocking.set_active_env(pe) # Ensure that original behaviour is restored - @test (@mock multiply(2)) == 3 - @test (@mock multiply(0x2)) == 0x4 - @test (@mock multiply(2//1)) == 4//1 + @test multiply(2) == 3 + @test multiply(0x2) == 0x4 + @test multiply(2//1) == 4//1 # Use convenient syntax apply(patches) do - @test (@mock multiply(2)) == 8 - @test (@mock multiply(0x2)) == 0x6 - @test (@mock multiply(2//1)) == 4//1 + @test multiply(2) == 8 + @test multiply(0x2) == 0x6 + @test multiply(2//1) == 4//1 end # Patches should only be applied for the scope of the do block - @test (@mock multiply(2)) == 3 - @test (@mock multiply(0x2)) == 0x4 - @test (@mock multiply(2//1)) == 4//1 + @test multiply(2) == 3 + @test multiply(0x2) == 0x4 + @test multiply(2//1) == 4//1 end diff --git a/test/deprecations.jl b/test/deprecations.jl new file mode 100644 index 0000000..fe818ec --- /dev/null +++ b/test/deprecations.jl @@ -0,0 +1,5 @@ + +@test_logs (:warn, r"can be removed") (@mock identity(1)) + +@test_logs (:warn, r"can be removed") (Mocking.enable()) +@test_logs (:warn, r"can be removed") (Mocking.enable(; force=true)) diff --git a/test/expr.jl b/test/expr.jl index cb4204b..eabda07 100644 --- a/test/expr.jl +++ b/test/expr.jl @@ -13,22 +13,31 @@ end @test Mocking.splitbinding(:(Foo.Bar.Baz)) == [:Foo, :Bar, :Baz] end + @testset "binding_expr" begin - @test Mocking.binding_expr(Int) == INT_EXPR # typealias. TODO: Change to Core.Int? Shouldn't actually matter - @test Mocking.binding_expr(Int64) == :(Core.Int64) # concrete type - @test Mocking.binding_expr(Integer) == :(Core.Integer) # abstract type - @test Mocking.binding_expr(Hour) == HOUR_EXPR # unexported type - @test Mocking.binding_expr(Dates.Hour) == HOUR_EXPR # submodule - @test Mocking.binding_expr(rand) == RAND_EXPR # function - @test Mocking.binding_expr(AbstractArray{Int64}) == :(Core.AbstractArray) # Core.AbstractArray{Int64}? - @test Mocking.binding_expr(Union{Int16,Int32,Int64}) == :(Union{Core.Int16,Core.Int32,Core.Int64}) + @test ==( + Mocking.binding_expr(Main, Int), + Int === Int32 ? :(Core.Int32) : :(Core.Int64) + ) + + @test Mocking.binding_expr(Main, Int64) == :(Core.Int64) # concrete type + @test Mocking.binding_expr(Main, Integer) == :(Core.Integer) # abstract type + + @test Mocking.binding_expr(Main, Hour) == :(Main.Dates.Hour) # unexported type + @test Mocking.binding_expr(Main, Dates.Hour) == :(Main.Dates.Hour) # submodule + @test Mocking.binding_expr(Main, rand) == :(Main.Random.rand) # function + @test Mocking.binding_expr(Main, AbstractArray{Int64}) == :(Core.AbstractArray) # Core.AbstractArray{Int64}? + @test ==( + Mocking.binding_expr(Main, Union{Int16,Int32,Int64}), + :(Union{Core.Int16,Core.Int32,Core.Int64}) + ) # @test Mocking.binding_expr(AbstractArray{T}) == :(Core.AbstractArray{T}) end @testset "adjust_bindings" begin trans = Dict(:Int => Int, :Int64 => Int64, :Integer => Integer) - @test Mocking.adjust_bindings(trans) == Dict( - :Int => INT_EXPR, + @test Mocking.adjust_bindings(Main, trans) == Dict( + :Int => Int === Int32 ? :(Core.Int32) : :(Core.Int64), :Int64 => :(Core.Int64), :Integer => :(Core.Integer), ) diff --git a/test/import.jl b/test/import.jl index 7c4fbbb..1ba7027 100644 --- a/test/import.jl +++ b/test/import.jl @@ -1,22 +1,3 @@ -import Compat: Dates -import Compat: read - -# Patches should allow using imported bindings in the body of the patch -@testset "imported binding in body" begin - @test_throws UndefVarError Minute - @test isdefined(Dates, :Minute) - import Dates: Minute, Hour - - myminute(x::Integer) = Minute(x) - - # Patches should work when referencing bindings imported in the file where the patch - # is created. - patch = @patch myminute(x::Integer) = Minute(Hour(x)) - apply(patch) do - @test (@mock myminute(5)) == Minute(300) - end -end - # Patches should allow using . syntax in the signature @testset "qualified binding in signature" begin @test_throws UndefVarError AbstractCmd @@ -24,7 +5,7 @@ end patch = @patch read(cmd::Base.AbstractCmd, ::Type{String}) = "bar" apply(patch) do - @test (@mock read(`foo`, String)) == "bar" + @test read(`foo`, String) == "bar" end end @@ -34,6 +15,6 @@ end patch = @patch read(cmd::AbstractCmd, ::Type{String}) = "bar" apply(patch) do - @test (@mock read(`foo`, String)) == "bar" + @test read(`foo`, String) == "bar" end end diff --git a/test/mock-in-patch.jl b/test/mock-in-patch.jl deleted file mode 100644 index d0bfca3..0000000 --- a/test/mock-in-patch.jl +++ /dev/null @@ -1,32 +0,0 @@ -# Test that @mock works within the context of a patch. -# -# WARNING: Do not use the following code as a template as this test is for illustration -# purposes only. Typically the way this problem is handled is by using `@mock foo(...)` -# within the original function declaration of `foo(::AbstractArray)`. -@testset "mock in patch" begin - foo(arr::AbstractArray{Float64}) = map(foo, arr) # Typically foo should use @mock here - foo(x::Float64) = floor(x) - - # Patching only the function that takes a scalar - patches = Patch[ - @patch foo(x::Float64) = ceil(x) - ] - - @test (@mock foo(1.6)) == 1.0 - - apply(patches) do - @test (@mock foo(1.6)) == 2.0 - @test (@mock foo([1.6])) == [1.0] # Ends up calling original function - end - - # Create a set of patches where @mock is used within a @patch - patches = Patch[ - @patch foo(arr::AbstractArray{Float64}) = map(x -> (@mock foo(x)), arr) - @patch foo(x::Float64) = ceil(x) - ] - - apply(patches) do - @test (@mock foo(1.6)) == 2.0 - @test (@mock foo([1.6])) == [2.0] - end -end diff --git a/test/mock-methods.jl b/test/mock-methods.jl new file mode 100644 index 0000000..333272b --- /dev/null +++ b/test/mock-methods.jl @@ -0,0 +1,39 @@ +foo(arr::AbstractArray{Float64}) = map(foo, arr) # Typically foo should use @mock here +foo(x::Float64) = floor(x) + +@testset "mock some methods but not others" begin + @test foo(1.6) == 1.0 + @test foo([1.6]) == [1.0] + + + # Patching only the function that takes a scalar + apply(@patch foo(x::Float64) = ceil(x)) do + @test foo(1.6) == 2.0 + @test foo([1.6]) == [2.0] # Ends up calling patched function + end + + # Patching both methods, so inner method is not called by outer + + + apply([ + @patch(foo(x::Float64) = ceil(x)), + @patch(foo(arr::AbstractArray{Float64}) = -1 .* arr), + ]) do + + @test foo(1.6) == 2.0 + @test foo([1.6]) == [-1.6] + end +end + + +# https://github.com/invenia/Mocking.jl/issues/59 +foobar(args...)=0 +@testset "Mocks that depend on mocks" begin + @test foobar(1)==0 + apply([ + (@patch foobar(a::Function, b) = b), + (@patch foobar(b) = foobar(() -> nothing, b)) + ]) do + @test foobar(1) == 1 + end +end diff --git a/test/optional.jl b/test/optional.jl index dd3d1f6..6bc33c2 100644 --- a/test/optional.jl +++ b/test/optional.jl @@ -1,27 +1,41 @@ -import Compat: Dates -import .Dates: Hour +using Mocking +using Test +using Dates +using Dates: Hour # Creating a patch with an optional parameter +hourvalue(h::Hour=Hour(0)) = Dates.value(h) @testset "patch with optional parameter" begin - hourvalue(h::Hour=Hour(0)) = Dates.value(h) + + @testset "Patch sensibility check" begin + # There is some subtleness with the imports here, + # So we want to be sure our code is actually going to run in current + # namespace before we throw it to cassette + + # The code for `preview_hourvalue` should match the patch + preview_hourvalue(h::Hour=Hour(21)) = 2 * Dates.value(h) + @test preview_hourvalue(Hour(4)) == 8 + @test preview_hourvalue() == 42 + end + patch = @patch hourvalue(h::Hour=Hour(21)) = 2 * Dates.value(h) apply(patch) do - @test (@mock hourvalue()) == 42 - @test (@mock hourvalue(Hour(4))) == 8 + @test hourvalue(Hour(4)) == 8 + @test hourvalue() == 42 end end # Creating a patch with an keyword parameter +hourvalue_kw(; hour::Hour=Hour(0)) = Dates.value(hour) @testset "patch with keyword parameter" begin - hourvalue(; hour::Hour=Hour(0)) = Dates.value(hour) - patch = @patch hourvalue(; hour::Hour=Hour(21)) = 2 * Dates.value(hour) + patch = @patch hourvalue_kw(; hour::Hour=Hour(21)) = 2 * Dates.value(hour) apply(patch) do - @test (@mock hourvalue()) == 42 + @test hourvalue_kw() == 42 # Test @mock calls with keyword arguments - @test (@mock hourvalue(hour=Hour(4))) == 8 #:kw - @test (@mock hourvalue(; hour=Hour(4))) == 8 #:parameters + @test hourvalue_kw(hour=Hour(4)) == 8 #:kw + @test hourvalue_kw(; hour=Hour(4)) == 8 #:parameters end end diff --git a/test/patch-gen.jl b/test/patch-gen.jl index 48b2f52..22c585e 100644 --- a/test/patch-gen.jl +++ b/test/patch-gen.jl @@ -1,9 +1,11 @@ # https://github.com/invenia/Mocking.jl/issues/14 + +statuscode(url::AbstractString) = 500 + @testset "patch generation" begin - statuscode(url::AbstractString) = 500 function foo(status::Int) - @mock statuscode("http://httpbin.org/status/$status") + statuscode("http://httpbin.org/status/$status") end # Previously Mocking would modify the function expression in place. Reusing this diff --git a/test/patch.jl b/test/patch.jl index 26c37e0..3fecaaf 100644 --- a/test/patch.jl +++ b/test/patch.jl @@ -1,96 +1,47 @@ -import Compat: Dates -import .Dates: Hour - -function strip_lineno!(expr::Expr) - filter!(expr.args) do ex - isa(ex, LineNumberNode) && return false - if isa(ex, Expr) - ex.head === :line && return false - strip_lineno!(ex::Expr) - end - return true - end - return expr -end - -# Test for nested modules -module ModA - -using Mocking - -module ModB - -abstract type AbstractFoo end +import Dates: Hour -struct Foo <: AbstractFoo - x::String -end - -end # ModB -bar(f::ModB.AbstractFoo) = "default" -baz(f::ModB.AbstractFoo) = @mock bar(f) -end # ModA -import .ModA -import .ModA: bar, baz, ModB +f(args...)= NaN # Must declare function before mocking it @testset "patch" begin @testset "basic" begin - p = @patch f(a, b::Int64, c=3, d::Integer=4; e=5, f::Int=6) = nothing - @test p.signature == :(f(a, b::Core.Int64, c=3, d::Core.Integer=4; e=5, f::$INT_EXPR=6)) - @test p.modules == Set([:Core]) - expected = quote - import Core - f(a, b::Core.Int64, c=3, d::Core.Integer=4; e=5, f::$INT_EXPR=6) = $(p.body)(a, b, c, d; e=e, f=f) - end - @test Mocking.convert(Expr, p) == strip_lineno!(expected) + p = @patch f(a, b::Int64, c=3, d::Integer=4; e=5, g::Int32=6) = nothing + @test p.signature == :(Main.f(a, b::Core.Int64, c=3, d::Core.Integer=4; e=5, g::Core.Int32=6)) + end + + @testset "f as arg and function name" begin + p = @patch f(f) = nothing + @test_broken p.signature == :(Main.f(f)) end @testset "variable argument parameters" begin p = @patch f(a::Integer...) = nothing - @test p.signature == :(f(a::Core.Integer...)) - @test p.modules == Set([:Core]) - expected = quote - import Core - f(a::Core.Integer...) = $(p.body)(a...) - end - @test Mocking.convert(Expr, p) == strip_lineno!(expected) + @test p.signature == :(Main.f(a::Core.Integer...)) end @testset "variable keyword parameters" begin p = @patch f(; a...) = nothing - @test p.signature == :(f(; a...)) - @test p.modules == Set() - expected = quote - f(; a...) = $(p.body)(; a...) - end - @test Mocking.convert(Expr, p) == strip_lineno!(expected) + @test p.signature == :(Main.f(; a...)) end # Issue #15 @testset "anonymous parameter" begin + function next_gensym(str::AbstractString, offset::Integer=1) + m = match(r"^(.*?)(\d+)$", string(gensym(str))) + return Symbol(string(m.captures[1], parse(Int, m.captures[2]) + offset)) + end + + anon = next_gensym("anon", 1) p = @patch f(::Type{UInt8}, b::Int64) = nothing - @test p.signature == :(f($anon::Core.Type{Core.UInt8}, b::Core.Int64)) - @test p.modules == Set([:Core]) - expected = quote - import Core - f($anon::Core.Type{Core.UInt8}, b::Core.Int64) = $(p.body)($anon, b) - end - @test Mocking.convert(Expr, p) == strip_lineno!(expected) + @test p.signature == :(Main.f($anon::Core.Type{Core.UInt8}, b::Core.Int64)) end @testset "assertion expression" begin p = @patch f(t::typeof(+)) = nothing - @test p.signature == :(f(t::typeof(Base.:+))) - @test p.modules == Set([:Base]) - expected = quote - import Base - f(t::typeof(Base.:+)) = $(p.body)(t) - end - @test Mocking.convert(Expr, p) == strip_lineno!(expected) + @test p.signature == :(Main.f(t::typeof(Main.Base.:+))) end @testset "assertion qualification" begin @@ -100,47 +51,19 @@ import .ModA: bar, baz, ModB @patch f(h::Int64=rand(Int64)) = nothing ] for p in patches - @test p.signature == :(f(h::Core.Int64=$RAND_EXPR(Core.Int64))) - @test p.modules == Set([:Core, RAND_MOD_EXPR]) + @test p.signature == :(Main.f(h::Core.Int64=Main.Random.rand(Core.Int64))) end end - @testset "nested modules" begin - #= - On 0.7 we cannot handle patching a relative module in Main because: - - 1. `import Main` will throw an error - 2. bindings must be absolute in order to transplant them into the - patch environment (e.g., temporary Mocking submodule). - - As a result, we're opting to throw an error in that condition. - NOTE: Dropping 0.6 should allow us to use Cassette.jl and avoid this issue. - =# - p = @patch bar(f::ModB.AbstractFoo) = "mock" - if VERSION >= v"0.7.0-DEV.1877" - @test_throws ErrorException Mocking.convert(Expr, p) - else - expected = quote - import ModA - import ModA.ModB - bar(f::ModA.ModB.AbstractFoo) = $(p.body)(f) - end - @test Mocking.convert(Expr, p) == strip_lineno!(expected) - Mocking.apply(p) do - @test baz(ModB.Foo("X")) == "mock" - end - end - end + @testset "array default" begin p = @patch f(a=[]) = a - @test p.signature == :(f(a=[])) - @test p.modules == Set() + @test p.signature == :(Main.f(a=[])) end @testset "tuple default" begin p = @patch f(a=()) = a - @test p.signature == :(f(a=())) - @test p.modules == Set() + @test p.signature == :(Main.f(a=())) end end diff --git a/test/readme.jl b/test/readme.jl index 19e876b..884a18b 100644 --- a/test/readme.jl +++ b/test/readme.jl @@ -1,10 +1,10 @@ -import Compat: Sys, read, unsafe_string +using Base: Sys, read, unsafe_string # Testcase from example given in Mocking.jl's README @testset "readme" begin # Note: Function only works in UNIX environments. function randdev(n::Integer) - @mock open("/dev/urandom") do fp + open("/dev/urandom") do fp reverse(read(fp, n)) end end @@ -17,18 +17,18 @@ import Compat: Sys, read, unsafe_string end # Produces a string with sequential UInt8 values from 1:n - data = unsafe_string(pointer(convert(Array{UInt8}, 1:n))) + data = String(UInt8.(1:n)) # Generate a alternative method of `open` which call we wish to mock patch = @patch open(fn::Function, f::AbstractString) = fn(IOBuffer(data)) # Apply the patch which will modify the behaviour for our test apply(patch) do - @test randdev(n) == convert(Array{UInt8}, n:-1:1) + @test_broken randdev(n) == UInt8.(n:-1:1) # https://github.com/jrevels/Cassette.jl/issues/89 end if Sys.isunix() # Outside of the scope of the patched environment `@mock` is essentially a no-op - @test randdev(n) != convert(Array{UInt8}, n:-1:1) + @test randdev(n) != UInt8.(n:-1:1) # Very unlikely end end diff --git a/test/real-isfile.jl b/test/real-isfile.jl index 222642a..a103043 100644 --- a/test/real-isfile.jl +++ b/test/real-isfile.jl @@ -8,6 +8,6 @@ # Note: @__FILE__ is resolved in the context of this file. patch = @patch isfile(p...) = first(p) == string(@__FILE__, ".null") apply(patch) do - @test (@mock isfile(null_file)) == true + @test isfile(null_file) == true end end diff --git a/test/real-nested.jl b/test/real-nested.jl index 9f95dd9..14a40c2 100644 --- a/test/real-nested.jl +++ b/test/real-nested.jl @@ -1,7 +1,6 @@ -import Compat: read @testset "nested mock call" begin - readfile(filename) = (@mock isfile(filename)) ? read((@mock open(filename)), String) : "" + readfile(filename) = isfile(filename) ? String(read(open(filename))) : "" # Testing with both generic and anonymous functions patches = Patch[ diff --git a/test/real-open.jl b/test/real-open.jl index e4912d4..b86c3a3 100644 --- a/test/real-open.jl +++ b/test/real-open.jl @@ -9,19 +9,19 @@ import Compat: read # will be preferred over the original `open(::AbstractString)` for `open("foo")` patch = @patch open(name) = IOBuffer("bar") apply(patch) do - @test read((@mock open("foo")), String) == "bar" + @test read(open("foo"), String) == "bar" # The `open(::Any)` patch could result in unintended (or intended) consequences - @test read((@mock open(`echo helloworld`)), String) == "bar" + @test read(open(`echo helloworld`), String) == "bar" end # Better to be specific with your patches patch = @patch open(name::AbstractString) = IOBuffer("bar") apply(patch) do - @test read((@mock open("foo")), String) == "bar" + @test read(open("foo"), String) == "bar" # The more specific `open(::AbstractString)` patches only a single method - result = @mock open(`echo helloworld`) + result = open(`echo helloworld`) if VERSION >= v"0.7.0-DEV.3" io = result else diff --git a/test/runtests.jl b/test/runtests.jl index ef140cc..b745ec5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,37 +1,42 @@ using Mocking -Mocking.enable(force=true) -using Compat: @__MODULE__ -using Compat.Test -import Compat: Dates -import Mocking: apply +using Test +import Dates +using Dates: Hour +using Mocking: apply -const INT_EXPR = Int === Int32 ? :(Core.Int32) : :(Core.Int64) -const HOUR_EXPR = VERSION < v"0.7.0-DEV.2575" ? :(Base.Dates.Hour) : :(Dates.Hour) -const RAND_EXPR = VERSION < v"0.7.0-DEV.3406" ? :(Base.Random.rand) : :(Random.rand) -const RAND_MOD_EXPR = VERSION < v"0.7.0-DEV.3406" ? :(Base.Random) : :Random function next_gensym(str::AbstractString, offset::Integer=1) m = match(r"^(.*?)(\d+)$", string(gensym(str))) return Symbol(string(m.captures[1], parse(Int, m.captures[2]) + offset)) end + +testfiles = [ + "deprecations.jl", + + "expr.jl", + "bindings/bindings.jl", + "patch.jl", + + "concept.jl", + "closure.jl", + "scope.jl", + "import.jl", + "real-open.jl", + "real-isfile.jl", + "real-nested.jl", + "mock-methods.jl", + "readme.jl", + "patch-gen.jl", + "anonymous-param.jl", + + "optional.jl", +] + + @testset "Mocking" begin - include("compiled-modules.jl") - include("expr.jl") - include("bindings/bindings.jl") - include("patch.jl") - - include("concept.jl") - include("scope.jl") - include("closure.jl") - include("import.jl") - include("real-open.jl") - include("real-isfile.jl") - include("real-nested.jl") - include("mock-in-patch.jl") - include("readme.jl") - include("optional.jl") - include("patch-gen.jl") - include("anonymous-param.jl") + @testset "$file" for file in testfiles + include(file) + end end diff --git a/test/scope.jl b/test/scope.jl index 812aca4..92e17ec 100644 --- a/test/scope.jl +++ b/test/scope.jl @@ -1,44 +1,43 @@ -# Test that Mocking works at various scopes +# Test that can mock things at various scopes -# Global scope -global_scope() = "foo" - -# The @mock macro is essentially a no-op -@test (@mock global_scope()) == global_scope() - -# Create a patched version of func() and return the alternative -# version at call sites using the @mock macro -global_patch = (@patch global_scope() = "bar") -apply(global_patch) do - @test (@mock global_scope()) != global_scope() +module DemoOuter + module DemoInner + inner() = "O I i" + end + + outer() = "O o" end -# The @mock macro should return to the original behaviour -@test (@mock global_scope()) == global_scope() - -# Local scope within a function -function scope_test() - function_scope() = "foo" - @test (@mock function_scope()) == function_scope() +### Without importing - patch = @patch function_scope() = "bar" - apply(patch) do - @test (@mock function_scope()) != function_scope() +demo_outer() = identity(DemoOuter.outer()) +using .DemoOuter: outer +@testset "Module outer" begin + @testset "Fully qualified name" begin + apply(@patch DemoOuter.outer() = "patched") do + @test demo_outer() == "patched" + end end - @test (@mock function_scope()) == function_scope() + @testset "Unqualified name" begin + apply(@patch outer() = "patched2") do + @test demo_outer() == "patched2" + end + end end -scope_test() - -# Local scope within a let-block -let let_scope - let_scope() = "foo" - @test (@mock let_scope()) == let_scope() - patch = @patch let_scope() = "bar" - apply(patch) do - @test (@mock let_scope()) != let_scope() +demo_inner() = identity(DemoOuter.DemoInner.inner()) +using .DemoOuter.DemoInner: inner +@testset "Inner module" begin + @testset "Fully qualified name" begin + apply(@patch DemoOuter.DemoInner.inner() = "patched") do + @test demo_inner() == "patched" + end end - @test (@mock let_scope()) == let_scope() + @testset "Unqualified name" begin + apply(@patch inner() = "patched2") do + @test demo_inner() == "patched2" + end + end end