Skip to content

Commit

Permalink
feature: verbose violations (#31)
Browse files Browse the repository at this point in the history
* feature: verbose violations

* `current_exceptions` is exported

* fix + stricter tests
  • Loading branch information
ericphanson authored Oct 10, 2023
1 parent 9a46537 commit ca35333
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 28 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "TransformSpecifications"
uuid = "b92eaff8-8912-4b32-ad24-14d7c692d780"
authors = ["Beacon Biosignals, Inc"]
version = "0.5.0"
version = "0.5.1"

[deps]
Legolas = "741b9549-f6ed-4911-9fbf-4a1c0c97f0cd"
Expand Down
22 changes: 18 additions & 4 deletions src/nothrow.jl
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ function output_specification(ntt::NoThrowTransform)
end

"""
transform!(ntt::NoThrowTransform, input)
transform!(ntt::NoThrowTransform, input; verbose_violations=false)
Return [`NoThrowResult`](@ref) of applying `ntt.transform_spec.transform_fn` to `input`. Transform
will fail (i.e., return a `NoThrowResult{Missing}` if:
Expand All @@ -248,28 +248,42 @@ will fail (i.e., return a `NoThrowResult{Missing}` if:
In any of these failure cases, this function will not throw, but instead will return
the cause of failure in the output `violations` field.
If `verbose_violations=true`, then much more verbose violation strings will be generated in the case of unexpected violations (including full stacktraces).
!!! note
For debugging purposes, it may be helpful to bypass the "no-throw" feature and
so as to have access to a callstack. To do this, use [`transform_force_throw!`](@ref)
in place of `transform!`.
See also: [`convert_spec`](@ref)
"""
function transform!(ntt::NoThrowTransform, input)
function transform!(ntt::NoThrowTransform, input; verbose_violations=false)
# Check that input meets specification
InSpec = input_specification(ntt)
input = try
convert_spec(InSpec, input)
catch e
violations = "Input doesn't conform to specification `$(InSpec)`. Details: $e"
if verbose_violations
stack = current_exceptions()
str = sprint(show, MIME"text/plain"(), stack)
else
str = string(e)
end
violations = "Input doesn't conform to specification `$(InSpec)`. Details: $str"
return NoThrowResult(; violations)
end

# Do transformation
result = try
NoThrowResult(ntt.transform_spec.transform_fn(input))
catch e
return NoThrowResult(; violations="Unexpected violation. Details: $e")
if verbose_violations
stack = current_exceptions()
str = sprint(show, MIME"text/plain"(), stack)
else
str = string(e)
end
return NoThrowResult(; violations="Unexpected violation. Details: $str")
end

# ...wrap it in a nothrow, so that any nested nothrows are correctly collapsed
Expand Down
16 changes: 12 additions & 4 deletions src/nothrow_dag.jl
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ function output_specification(c::NoThrowDAG)
end

"""
transform!(dag::NoThrowDAG, input)
transform!(dag::NoThrowDAG, input; verbose_violations=false)
Return [`NoThrowResult`](@ref) of sequentially [`transform!`](@ref)ing all
`dag.step_transforms`, after passing `input` to the first step.
Expand All @@ -416,9 +416,11 @@ Before each step, that step's `input_assembler` is called on the results of all
previous processing steps; this constructor generates input that conforms to the
step's `input_specification`.
If `verbose_violations=true`, then much more verbose violation strings will be generated in the case of unexpected violations (including full stacktraces).
See also: [`transform_force_throw!`](@ref)
"""
function transform!(dag::NoThrowDAG, input)
function transform!(dag::NoThrowDAG, input; verbose_violations=false)
warnings = String[]
component_results = OrderedDict{String,Any}()
for (i_step, (name, step)) in enumerate(dag.step_transforms)
Expand All @@ -444,13 +446,19 @@ function transform!(dag::NoThrowDAG, input)
try
convert_spec(InSpec, input)
catch e
if verbose_violations
stack = current_exceptions()
str = sprint(show, MIME"text/plain"(), stack)
else
str = string(e)
end
return NoThrowResult(; warnings,
violations="Input to step `$name` doesn't conform to specification `$(InSpec)`. Details: $e")
violations="Input to step `$name` doesn't conform to specification `$(InSpec)`. Details: $str")
end

# 2. Apply the step's transform!
# Note that output specification checking _is_ performed inside this transform
result = transform!(step, input)
result = transform!(step, input; verbose_violations)
# ...and capture any warnings generated by this step
append!(warnings, result.warnings)

Expand Down
30 changes: 20 additions & 10 deletions test/nothrow.jl
Original file line number Diff line number Diff line change
Expand Up @@ -206,16 +206,26 @@ end
end

@testset "Nonconforming transform fails" begin
input_record = SchemaAV1(; foo="rabbit")
err = ErrorException("Oh no, an unexpected exception---if only we'd checked for it and returned a NoThrowResult{Missing} instead!")
ntt_unexpected_throw = NoThrowTransform(SchemaAV1, SchemaBV1,
_ -> throw(err))
result = transform!(ntt_unexpected_throw, input_record)
@test !nothrow_succeeded(result)
@test isequal(only(result.violations),
"Unexpected violation. Details: $err")
@test_throws ErrorException transform_force_throw!(ntt_unexpected_throw,
input_record)
for verbose_violations in (true, false)
input_record = SchemaAV1(; foo="rabbit")
err = ErrorException("Oh no, an unexpected exception---if only we'd checked for it and returned a NoThrowResult{Missing} instead!")
ntt_unexpected_throw = NoThrowTransform(SchemaAV1, SchemaBV1,
_ -> throw(err))
err = ErrorException("Oh no, an unexpected exception---if only we'd checked for it and returned a NoThrowResult{Missing} instead!")
result = transform!(ntt_unexpected_throw, input_record; verbose_violations)
@test !nothrow_succeeded(result)
@test contains(only(result.violations), "Unexpected violation. Details:")
@test contains(only(result.violations), err.msg)
if verbose_violations
# Check we are printing the exception stack
@test contains(only(result.violations), "ExceptionStack")
else
# In this case we should *not* be printing the exception stack
@test !contains(only(result.violations), "ExceptionStack")
end
@test_throws ErrorException transform_force_throw!(ntt_unexpected_throw,
input_record)
end
end

@testset "Nonconforming output fails" begin
Expand Down
30 changes: 21 additions & 9 deletions test/nothrow_dag.jl
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,27 @@ end
end

@testset "Nonconforming input fails" begin
result = transform!(dag, SchemaBarV1(; var1="yay", var2="whee"))
@test !nothrow_succeeded(result)
err_str = "Input to step `step_a` doesn't conform to specification `SchemaFooV1`. \
Details: ArgumentError(\"Invalid value set for field `foo`, expected String, \
got a value of type Missing (missing)\")"
@test isequal(err_str, only(result.violations))

err = ArgumentError("Input to step `step_a` doesn't conform to specification `SchemaFooV1`")
@test_throws err transform_force_throw!(dag, SchemaBarV1(; var1="yay", var2="whee"))
for verbose_violations in (true, false)
result = transform!(dag, SchemaBarV1(; var1="yay", var2="whee");
verbose_violations)
@test !nothrow_succeeded(result)
err_str = "Input to step `step_a` doesn't conform to specification `SchemaFooV1`"

@test startswith(only(result.violations), err_str)
@test contains(only(result.violations),
"Invalid value set for field `foo`, expected String, got a value of type Missing (missing)")

if verbose_violations
# Check we are printing the exception stack
@test contains(only(result.violations), "ExceptionStack")
else
# In this case we should *not* be printing the exception stack
@test !contains(only(result.violations), "ExceptionStack")
end
err = ArgumentError(err_str)
@test_throws err transform_force_throw!(dag,
SchemaBarV1(; var1="yay", var2="whee"))
end
end

@testset "`_validate_input_assembler`" begin
Expand Down

2 comments on commit ca35333

@ericphanson
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/93177

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.5.1 -m "<description of version>" ca35333a9e7aef92a0f7c0acd77ade7d303f8b85
git push origin v0.5.1

Please sign in to comment.