diff --git a/.gitignore b/.gitignore index b067edd..ba39cc5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -/Manifest.toml +Manifest.toml diff --git a/Project.toml b/Project.toml index d0c8dbe..3cf613e 100644 --- a/Project.toml +++ b/Project.toml @@ -4,10 +4,4 @@ authors = ["Takafumi Arakaki and contributors"] version = "0.1.0-DEV" [compat] -julia = "1" - -[extras] -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[targets] -test = ["Test"] +julia = "1.6" diff --git a/README.md b/README.md index 08a6dce..2ff9405 100644 --- a/README.md +++ b/README.md @@ -1 +1,101 @@ -# Try +# Try.jl: zero-overhead and debuggable error handling + +Features: + +* Error handling as simple manipulations of *values*. +* Focus on *inferrability* and *optimizability* leveraging unique properties of + the Julia language and compiler. +* *Error trace* for determining the source of errors, without `throw`. +* Facilitate the ["Easier to ask for forgiveness than permission" + (EAFP)](https://docs.python.org/3/glossary.html#term-EAFP) approach as a + robust and minimalistic alternative to the trait-based feature detection. + +## Examples + +### Basic usage + +```julia +julia> using Try + +julia> result = Try.getindex(Dict(:a => 111), :a); + +julia> Try.isok(result) +true + +julia> Try.unwrap(result) +111 + +julia> result = Try.getindex(Dict(:a => 111), :b); + +julia> Try.iserr(result) +true + +julia> Try.unwrap_err(result) +KeyError(:b) +``` + +### EAFP + +```julia +using Try + +function try_map_prealloc(f, xs) + T = Try.@return_err Try.eltype(xs) + n = Try.@return_err Try.length(xs) + ys = Vector{T}(undef, n) + for (i, x) in zip(eachindex(ys), xs) + ys[i] = f(x) + end + return Ok(ys) +end + +mymap(f, xs) = + try_map_prealloc(f, xs) |> + Try.or_else() do _ + Ok(mapfoldl(f, push!, xs; init = [])) + end |> + Try.unwrap + +mymap(x -> x + 1, 1:3) + +# output +3-element Vector{Int64}: + 2 + 3 + 4 +``` + +```julia +mymap(x -> x + 1, (x for x in 1:5 if isodd(x))) + +# output +3-element Vector{Any}: + 2 + 4 + 6 +``` + +## Discussion + +Try.jl provides an API inspired by Rust's `Result` type. However, to fully +unlock the power of Julia, Try.jl uses the *small `Union` types* instead of a +concretely typed sum type. Furthermore, it optionally supports concretely-typed +returned value when `Union` is not appropriate. + +A potential usability issue for using the `Result` type is that the detailed +context of the error is lost by the time the user received an error. This makes +debugging Julia programs hard compared to simply `throw`ing the exception. To +solve this problem, Try.jl provides an *error trace* mechanism for recording the +backtrace of the error. This can be toggled using `Try.enable_errortrace()` at +the run-time. This is inspired by Zig's [Error Return +Traces](https://ziglang.org/documentation/master/#Error-Return-Traces). + +Try.jl exposes a limited set of "verbs" based on Julia `Base` such as +`Try.take!`. These functions have a catch-all default definition that returns +an error value `Err{NotImplementedError}`. This let us use these functions in +the ["Easier to ask for forgiveness than permission" +(EAFP)](https://docs.python.org/3/glossary.html#term-EAFP) manner because they +can be called without getting the run-time `MethodError` exception. Such +functions can be defined using `Try.@function f` instead of `function f end`. +They are defined as instances of `Tryable <: Function` and not as a direct +instance of `Function`. diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..a303fff --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +build/ +site/ diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..dfa65cd --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,2 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..39ea40c --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,31 @@ +using Documenter +using Try + +makedocs( + sitename = "Try", + format = Documenter.HTML(), + modules = [Try], + strict = [ + :autodocs_block, + :cross_references, + :docs_block, + :doctest, + :eval_block, + :example_block, + :footnote, + :linkcheck, + :meta_block, + # :missing_docs, + :parse_error, + :setup_block, + ], + # Ref: + # https://juliadocs.github.io/Documenter.jl/stable/lib/public/#Documenter.makedocs +) + +# Documenter can also automatically deploy documentation to gh-pages. +# See "Hosting Documentation" and deploydocs() in the Documenter manual +# for more information. +#=deploydocs( + repo = "" +)=# diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..b8a8140 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,9 @@ +# Try.jl + +```@docs +Try +Result +Ok +Err +``` + diff --git a/src/Try.jl b/src/Try.jl index fda9231..f2dd893 100644 --- a/src/Try.jl +++ b/src/Try.jl @@ -1,11 +1,137 @@ baremodule Try +export Ok, Err, Result + +using Base: Base, Exception + +abstract type AbstractResult{T,E<:Exception} end + +struct Ok{T} <: AbstractResult{T,Union{}} + value::T +end + +struct Err{E<:Exception} <: AbstractResult{Union{},E} + value::E + backtrace::Union{Nothing,typeof(Base.backtrace())} +end + +const DynamicResult{T,E} = Union{Ok{T},Err{E}} + +struct ConcreteResult{T,E<:Exception} <: AbstractResult{T,E} + value::DynamicResult{T,E} + ConcreteResult{T,E}(value::DynamicResult{T,E}) where {T,E<:Exception} = new{T,E}(value) +end + +const ConcreteOk{T} = ConcreteResult{T,Union{}} +const ConcreteErr{E<:Exception} = ConcreteResult{Union{},E} + +const Result{T,E} = Union{ConcreteResult{T,E},DynamicResult{T,E}} + +function throw end + +function unwrap end +function unwrap_err end + +function ok end +function err end +function oktype end +function errtype end +function isok end +function iserr end + +function enable_errortrace end +function disable_errortrace end + +abstract type Tryable <: Function end + +# Core exceptions +struct IsOkError <: Exception + ok::AbstractResult{<:Any,Union{}} +end + +# Basic exceptions +abstract type NotImplementedError <: Exception end +abstract type ClosedError <: Exception end +abstract type EmptyError <: Exception end +abstract type FullError <: Exception end + +baremodule Causes +function notimplemented end +function empty end +function closed end +end # baremodule Cause + +macro and_then end +macro or_else end +macro return_err end +function var"@return" end +function var"@function" end + +function and_then end +function or_else end + module Internal -using ..Try: Try +import ..Try: @return, @return_err, @and_then, @or_else, @function +using ..Try: + AbstractResult, + Causes, + ConcreteErr, + ConcreteOk, + ConcreteResult, + DynamicResult, + Err, + Ok, + Result, + Try -include("internal.jl") +using Base.Meta: isexpr + +include("utils.jl") +include("core.jl") +include("show.jl") +include("errortrace.jl") +include("function.jl") +include("causes.jl") + +include("tools.jl") +include("sugar.jl") end # module Internal +@function convert +# @function promote + +# Collection interface +@function length +@function eltype + +@function getindex +@function setindex! + +@function push! +@function pushfirst! +@function pop! +@function popfirst! + +@function put! +@function take! + +@function push_nowait! +@function pushfirst_nowait! +@function pop_nowait! +@function popfirst_nowait! + +@function put_nowait! +@function take_nowait! + +module Implementations +using ..Try +using ..Try: Causes +using Base: IteratorEltype, HasEltype, IteratorSize, HasLength, HasShape +include("base.jl") +end # module Implementations + +Internal.define_docstrings() + end # baremodule Try diff --git a/src/base.jl b/src/base.jl new file mode 100644 index 0000000..b4be56c --- /dev/null +++ b/src/base.jl @@ -0,0 +1,88 @@ +Try.convert(::Type{T}, x::T) where {T} = Ok(x) # TODO: should it be `Ok{T}(x)`? + +const MightHaveSize = Union{AbstractArray,AbstractDict,AbstractSet,AbstractString,Number} + +Try.length(xs::MightHaveSize)::Result = + if IteratorSize(xs) isa Union{HasLength,HasShape} + return Ok(length(xs)) + else + return Causes.notimplemented(Try.length, (xs,)) + end + +Try.eltype(xs) = Try.eltype(typeof(xs)) +Try.eltype(T::Type) = Causes.notimplemented(Try.eltype, (T,)) +Try.eltype(::Type{Union{}}) = Causes.notimplemented(Try.eltype, (Union{},)) +Try.eltype(::Type{<:AbstractArray{T}}) where {T} = Ok(T) +Try.eltype(::Type{AbstractSet{T}}) where {T} = Ok(T) + +Try.eltype(::Type{Dict}) where {K,V,Dict<:AbstractDict{K,V}} = eltype_impl(Dict) +Try.eltype(::Type{Num}) where {Num<:Number} = eltype_impl(Num) +Try.eltype(::Type{Str}) where {Str<:AbstractString} = eltype_impl(Str) + +eltype_impl(::Type{T}) where {T} = + if IteratorEltype(T) isa HasEltype + return Ok(eltype(T)) + else + return Causes.notimplemented(Try.eltype, (T,)) + end + +@inline function Try.getindex(a::AbstractArray, i...)::Result + (@boundscheck checkbounds(Bool, a, i...)) || return Err(BoundsError(a, i)) + return Ok(@inbounds a[i...]) +end + +@inline function Try.setindex!(a::AbstractArray, v, i...)::Result + (@boundscheck checkbounds(Bool, a, i...)) || return Err(BoundsError(a, i)) + @inbounds a[i...] = v + return Ok(v) +end + +struct NotFound end + +function Try.getindex(dict::AbstractDict, key)::Result + value = get(dict, key, NotFound()) + value isa NotFound && return Err(KeyError(key)) + return Ok(value) +end + +function Try.setindex!(dict::AbstractDict, value, key)::Result + dict[key] = value + return Ok(value) +end + +Try.getindex(dict::AbstractDict, k1, k2, ks...) = Try.getindex(dict, (k1, k2, ks...)) +Try.setindex!(dict::AbstractDict, v, k1, k2, ks...) = + Try.setindex!(dict, v, (k1, k2, ks...)) + +Try.pop!(a::Vector)::Result = isempty(a) ? Causes.empty(a) : Ok(pop!(a)) +Try.popfirst!(a::Vector)::Result = isempty(a) ? Causes.empty(a) : Ok(popfirst!(a)) + +function Try.push!(a::Vector, x)::Result + y = Try.@return_err Try.convert(eltype(a), x) + push!(a, y) + return Ok(a) +end + +function Try.pushfirst!(a::Vector, x)::Result + y = Try.@return_err Try.convert(eltype(a), x) + pushfirst!(a, y) + return Ok(a) +end + +function Try.take!(ch::Channel)::Result + y = iterate(ch) + y === nothing && return Causes.empty(ch) + return Ok(first(y)) +end + +function Try.put!(ch::Channel, x)::Result + isopen(ch) || return Causes.closed(ch) + y = Try.@return_err Try.convert(eltype(ch), x) + try + put!(ch, x) + catch err + err isa InvalidStateException && !isopen(ch) && return Causes.closed(ch) + rethrow() + end + return Ok(y) +end diff --git a/src/causes.jl b/src/causes.jl new file mode 100644 index 0000000..b3082ba --- /dev/null +++ b/src/causes.jl @@ -0,0 +1,25 @@ +Causes.notimplemented( + f, + args::Tuple, + kwargs::Union{NamedTuple,Iterators.Pairs} = NamedTuple(), +) = Err(Try.NotImplementedError(f, args, kwargs)) + +Causes.empty(container) = Err(Try.EmptyError(container)) + +struct NotImplementedError{T} <: Try.NotImplementedError end +# TODO: check if it is better to "type-erase" +# TODO: don't ignore kwargs? + +Try.NotImplementedError(f, args, _kwargs) = + NotImplementedError{Tuple{_typesof(f, args...)...}}() + +_typesof() = () +_typesof(::Type{Head}, tail...) where {Head} = (Type{Head}, _typesof(tail...)...) +_typesof(head, tail...) = (typeof(head), _typesof(tail...)...) + +struct EmptyError + container::Any +end +# TODO: check if it is better to not "type-erase" + +Try.EmptyError(container) = EmptyError(container) diff --git a/src/core.jl b/src/core.jl new file mode 100644 index 0000000..1bd8984 --- /dev/null +++ b/src/core.jl @@ -0,0 +1,80 @@ +Try.Ok(::Type{T}) where {T} = Try.Ok{Type{T}}(T) + +Try.Err(value) = Try.Err(value, maybe_backtrace()) +Try.Err{E}(value) where {E<:Exception} = Try.Err{E}(value, maybe_backtrace()) + +Try.unwrap(result::ConcreteResult) = Try.unwrap(result.value) +Try.unwrap(ok::Ok) = ok.value +Try.unwrap(err::Err) = Try.throw(err) + +Try.unwrap_err(result::ConcreteResult) = Try.unwrap_err(result.value) +Try.unwrap_err(ok::Ok) = throw(Try.IsOkError(ok)) +Try.unwrap_err(err::Err) = err.value + +Try.throw(err::ConcreteErr) = Try.throw(err.value) +function Try.throw(err::Err) + if err.backtrace === nothing + throw(err.value) + else + throw(ErrorTrace(err.value, err.backtrace)) + end +end + +Base.convert(::Type{Ok{T}}, ok::Ok) where {T} = Ok{T}(ok.value) +Base.convert(::Type{Err{E}}, err::Err) where {E} = Err{E}(err.value) + +function Base.convert( + ::Type{ConcreteResult{T,E}}, + result::ConcreteResult{T′,E′}, +) where {T,E,T′<:T,E′<:E} + value = result.value + if value isa Ok + return ConcreteResult{T,E}(Ok{T}(value.value)) + else + return ConcreteResult{T,E}(Err{E}(value.value)) + end +end + +Base.convert(::Type{ConcreteResult{T,E}}, result::ConcreteOk) where {T,E} = + ConcreteResult{T,E}(convert(Ok{T}, value.value)) + +Base.convert(::Type{ConcreteResult{T,E}}, result::ConcreteErr) where {T,E} = + ConcreteResult{T,E}(convert(Err{E}, value.value)) + +Try.ConcreteOk{T}(value) where {T} = Try.ConcreteOk{T}(Ok{T}(value)) +Try.ConcreteErr{E}(value) where {E} = Try.ConcreteErr{E}(Err{E}(value)) +Try.oktype(::Type{R}) where {T,R<:AbstractResult{T}} = T +Try.oktype(result::AbstractResult) = Try.oktype(typeof(result)) + +Try.errtype(::Type{R}) where {E,R<:AbstractResult{<:Any,E}} = E +Try.errtype(result::AbstractResult) = Try.errtype(typeof(result)) + +Try.ok(result::Ok) = Some{Try.oktype(result)}(result.value) +Try.ok(::Err) = nothing +function Try.ok(result::ConcreteResult) + value = result.value + if value isa Ok + return Try.ok(value) + else + return nothing + end +end + +Try.err(::Ok) = nothing +Try.err(result::Err) = Some{Try.errtype(result)}(result.value) +function Try.err(result::ConcreteResult) + value = result.value + if value isa Err + return Try.err(value) + else + return nothing + end +end + +Try.isok(::Ok) = true +Try.isok(::Err) = false +Try.isok(result::ConcreteResult) = result.value isa Ok + +Try.iserr(::Ok) = false +Try.iserr(::Err) = true +Try.iserr(result::ConcreteResult) = result.value isa Err diff --git a/src/docs/Err.md b/src/docs/Err.md new file mode 100644 index 0000000..e69de29 diff --git a/src/docs/Ok.md b/src/docs/Ok.md new file mode 100644 index 0000000..d2a10bb --- /dev/null +++ b/src/docs/Ok.md @@ -0,0 +1 @@ + Ok{T} <: Result{T,E} diff --git a/src/docs/Result.md b/src/docs/Result.md new file mode 100644 index 0000000..ba94844 --- /dev/null +++ b/src/docs/Result.md @@ -0,0 +1 @@ + Err{E} <: Result{T,E<:Exception} diff --git a/src/errortrace.jl b/src/errortrace.jl new file mode 100644 index 0000000..1813333 --- /dev/null +++ b/src/errortrace.jl @@ -0,0 +1,49 @@ +include_backtrace() = false + +function Try.enable_errortrace() + old = include_backtrace() + @eval include_backtrace() = true + return old +end + +function Try.disable_errortrace() + old = include_backtrace() + @eval include_backtrace() = false + return old +end + +function Try.enable_errortrace(yes::Bool) + include_backtrace() == yes && return yes + return yes ? Try.enable_errortrace() : Try.disable_errortrace() +end + +maybe_backtrace() = include_backtrace() ? backtrace() : nothing + +struct ErrorTrace <: Exception + exception::Exception + backtrace::typeof(Base.backtrace()) +end + +function Base.showerror(io::IO, errtrace::ErrorTrace) + print(io, "Original Error: ") + showerror(io, errtrace.exception) + println(io) + + # TODO: remove common prefix? + buffer = IOBuffer() + Base.show_backtrace(IOContext(buffer, io), errtrace.backtrace) + seekstart(buffer) + println(io, "┌ Original: stacktrace") + for ln in eachline(buffer) + print(io, "│ ") + println(io, ln) + end + println(io, "└") +end + +function Base.show(io::IO, errtrace::ErrorTrace) + Base.show(io, ErrorTrace) + print(io, '(') + Base.show(io, errtrace.exception) + print(io, ", …)") +end diff --git a/src/function.jl b/src/function.jl new file mode 100644 index 0000000..7fe6284 --- /dev/null +++ b/src/function.jl @@ -0,0 +1,14 @@ +const var"@define_function" = var"@function" + +macro define_function(name::Symbol) + typename = gensym("typeof_$name") + quote + struct $typename <: $Try.Tryable end + const $name = $typename() + $Base.nameof(::$typename) = $(QuoteNode(name)) + end |> esc +end + +(fn::Try.Tryable)(args...; kwargs...) = Causes.notimplemented(fn, args, kwargs) + +# TODO: show methods diff --git a/src/show.jl b/src/show.jl new file mode 100644 index 0000000..917ba97 --- /dev/null +++ b/src/show.jl @@ -0,0 +1,17 @@ +function Base.show(io::IO, ::MIME"text/plain", ok::Ok) + printstyled(io, "Try.Ok"; color = :green, bold = true) + print(io, ": ") + show(io, MIME"text/plain"(), Try.unwrap(ok)) +end + +function Base.show(io::IO, ::MIME"text/plain", err::Err) + printstyled(io, "Try.Err"; color = :red, bold = true) + print(io, ": ") + ex = Try.unwrap_err(err) + backtrace = err.backtrace + if backtrace === nothing + showerror(io, ex) + else + showerror(io, ex, err.backtrace) + end +end diff --git a/src/sugar.jl b/src/sugar.jl new file mode 100644 index 0000000..11e256e --- /dev/null +++ b/src/sugar.jl @@ -0,0 +1,61 @@ +function extract_thunk(f) + if isexpr(f, :->, 2) && isexpr(f.args[1], :tuple, 1) + arg, = f.args[1].args + body = f.args[2] + return (arg, body) + else + error("invalid argument: ", f) + end +end + +macro and_then(f, ex) + arg, body = extract_thunk(f) + quote + result = $(esc(ex)) + if Try.isok(result) + let $(esc(arg)) = Try.unwrap(result) + $(esc(body)) + end + else + result + end + end +end + +macro or_else(f, ex) + arg, body = extract_thunk(f) + quote + result = $(esc(ex)) + if Try.iserr(result) + let $(esc(arg)) = Try.unwrap_err(result) + $(esc(body)) + end + else + result + end + end +end + +const var"@_return" = var"@return" + +macro _return(ex) + quote + result = $(esc(ex)) + if Try.isok(result) + return result + else + Try.unwrap_err(result) + end + end +end + +macro return_err(ex) + quote + result = $(esc(ex)) + if Try.iserr(result) + return result + else + Try.unwrap(result) + end + end +end diff --git a/src/tools.jl b/src/tools.jl new file mode 100644 index 0000000..6bee22e --- /dev/null +++ b/src/tools.jl @@ -0,0 +1,33 @@ +function Try.and_then(f::F) where {F} + function and_then_closure(result) + Try.and_then(f, result) + end +end + +Try.and_then(f, result::Ok)::AbstractResult = f(Try.unwrap(result)) +Try.and_then(_, result::Err) = result +function Try.and_then(f, result::ConcreteResult)::ConcreteResult + value = result.value + if value isa Ok + f(Try.unwrap(value)) + else + value + end +end + +function Try.or_else(f::F) where {F} + function or_else_closure(result) + Try.or_else(f, result) + end +end + +Try.or_else(_, result::Ok) = result +Try.or_else(f, result::Err)::AbstractResult = f(Try.unwrap_err(result)) +function Try.or_else(f, result::ConcreteResult)::ConcreteResult + value = result.value + if value isa Err + f(Try.unwrap_err(value)) + else + value + end +end diff --git a/src/utils.jl b/src/utils.jl new file mode 100644 index 0000000..d89065f --- /dev/null +++ b/src/utils.jl @@ -0,0 +1,18 @@ +function define_docstrings() + docstrings = [:Try => joinpath(dirname(@__DIR__), "README.md")] + docsdir = joinpath(@__DIR__, "docs") + for filename in readdir(docsdir) + stem, ext = splitext(filename) + ext == ".md" || continue + name = Symbol(stem) + name in names(Try, all = true) || continue + push!(docstrings, name => joinpath(docsdir, filename)) + end + for (name, path) in docstrings + include_dependency(path) + doc = read(path, String) + doc = replace(doc, r"^```julia"m => "```jldoctest $name") + doc = replace(doc, "TAB" => "_TAB_") + @eval Try $Base.@doc $doc $name + end +end diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 0000000..21320b8 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,4 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TestFunctionRunner = "792026f5-ac9a-4a19-adcb-47b0ce2deb5d" diff --git a/test/TryTests/Project.toml b/test/TryTests/Project.toml new file mode 100644 index 0000000..f988d9f --- /dev/null +++ b/test/TryTests/Project.toml @@ -0,0 +1,12 @@ +name = "TryTests" +uuid = "18ac1dc4-3454-4077-bdbe-c46d281bdc32" +authors = ["Takafumi Arakaki and contributors"] +version = "0.1.0-DEV" + +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Try = "bf1d0ff0-c4a9-496b-85f0-2b0d71c4f32a" + +[compat] +julia = "1.6" diff --git a/test/TryTests/src/TryTests.jl b/test/TryTests/src/TryTests.jl new file mode 100644 index 0000000..f239226 --- /dev/null +++ b/test/TryTests/src/TryTests.jl @@ -0,0 +1,8 @@ +module TryTests + +include("test_base.jl") +include("test_errortrace.jl") +include("test_tools.jl") +include("test_doctest.jl") + +end # module TryTests diff --git a/test/TryTests/src/test_base.jl b/test/TryTests/src/test_base.jl new file mode 100644 index 0000000..dc023f2 --- /dev/null +++ b/test/TryTests/src/test_base.jl @@ -0,0 +1,38 @@ +module TestBase + +using Test +using Try + +function test_convert() + @test Try.unwrap(Try.convert(Int, 1)) === 1 + @test Try.unwrap(Try.convert(Union{Int,String}, 1)) === 1 + @test Try.iserr(Try.convert(Union{}, 1)) + @test Try.iserr(Try.convert(String, 1)) + @test Try.unwrap_err(Try.convert(Nothing, 1)) isa Try.NotImplementedError +end + +function test_length() + @test Try.unwrap_err(Try.length(nothing)) isa Try.NotImplementedError + @test Try.unwrap_err(Try.length(x for x in 1:10 if isodd(x))) isa + Try.NotImplementedError + @test Try.unwrap(Try.length([1])) == 1 +end + +function test_eltype() + @test Try.unwrap(Try.eltype(1)) === Int + @test Try.unwrap(Try.eltype([1])) === Int + @test Try.unwrap(Try.eltype(AbstractVector{Int})) === Int + @test Try.unwrap(Try.eltype(AbstractArray{Int})) === Int + @test Try.unwrap_err(Try.eltype(AbstractVector)) isa Try.NotImplementedError +end + +function test_getindex() + @test Try.unwrap(Try.getindex([111], 1)) === 111 + @test Try.unwrap_err(Try.getindex([111], 0)) isa BoundsError + @test Try.unwrap_err(Try.getindex([111], 2)) isa BoundsError + + @test Try.unwrap(Try.getindex(Dict(:a => 111), :a)) === 111 + @test Try.unwrap_err(Try.getindex(Dict(:a => 111), :b)) isa KeyError +end + +end # module diff --git a/test/TryTests/src/test_doctest.jl b/test/TryTests/src/test_doctest.jl new file mode 100644 index 0000000..96f9dcd --- /dev/null +++ b/test/TryTests/src/test_doctest.jl @@ -0,0 +1,11 @@ +module TestDoctest + +using Documenter +using Test +using Try + +function test() + doctest(Try; manual = false) +end + +end # module diff --git a/test/TryTests/src/test_errortrace.jl b/test/TryTests/src/test_errortrace.jl new file mode 100644 index 0000000..75d9f27 --- /dev/null +++ b/test/TryTests/src/test_errortrace.jl @@ -0,0 +1,37 @@ +module TestErrorTrace + +using Test +using Try +using Try.Internal: ErrorTrace + +function module_context(f) + old = Try.enable_errortrace(true) + try + Base.invokelatest(f) + finally + Try.enable_errortrace(old) + end +end + +@noinline f1(x) = x ? Ok(nothing) : Err(ErrorException("nope")) +@noinline f2(x) = f1(x) +@noinline f3(x) = f2(x) + +function test_errtrace() + err = f1(false) + @test err isa Err + errmsg = sprint(show, "text/plain", err) + @test occursin("f1(x::Bool)", errmsg) + + exc = try + Try.unwrap(err) + nothing + catch x + x + end + @test exc isa ErrorTrace + excmsg = sprint(showerror, exc) + @test occursin("f1(x::Bool)", excmsg) +end + +end # module diff --git a/test/TryTests/src/test_tools.jl b/test/TryTests/src/test_tools.jl new file mode 100644 index 0000000..a8ff696 --- /dev/null +++ b/test/TryTests/src/test_tools.jl @@ -0,0 +1,45 @@ +module TestTools + +using Test +using Try + +function test_curry() + value = + Try.convert(String, 1) |> + Try.or_else() do _ + Ok("123") + end |> + Try.and_then() do x + Ok(@something(tryparse(Int, x), return Err(ErrorException("")))) + end |> + Try.unwrap + + @test value == 123 +end + +function demo_macro(xs) + i = firstindex(xs) + y = nothing + while true + #! format: off + x = @Try.or_else(Try.getindex(xs, i)) do _ + return :oob + end + @Try.and_then(Try.getindex(xs, Try.unwrap(x))) do z + if z > 1 + y = z + break + end + end + #! format: on + i += 1 + end + return y +end + +function test_macro() + @test demo_macro(1:2:10) == 5 + @test demo_macro(1:0) === :oob +end + +end # module diff --git a/test/runtests.jl b/test/runtests.jl index 668a683..d358b79 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,2 @@ -using Try -using Test - -@testset "Try.jl" begin - # Write your tests here. -end +using TestFunctionRunner +TestFunctionRunner.@run