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

Flatten testsets during report generation #104

Merged
merged 6 commits into from
May 1, 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
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
name: CI
on:
pull_request:
branches:
- master
Comment on lines -4 to -5
Copy link
Member

Choose a reason for hiding this comment

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

for my education can you explain this chagne?

Copy link
Member Author

Choose a reason for hiding this comment

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

Was changed to address this: #104 (comment)

The branches filter for the pull_request event restricts this workflow such that it only runs when a PR targets master (in this case) as the base branch. This PR originally didn't target master but another PR so the CI wouldn't run automatically. I don't see a good reason to restrict the CI workflow in these cases so I removed this.

push:
branches:
- master
Expand Down
77 changes: 26 additions & 51 deletions src/testsets.jl
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,7 @@ function finish(ts::ReportingTestSet)
# Display before flattening to match Pkg.test output
display_reporting_testset(ts)

# We are the top level, lets do this
flatten_results!(ts)
return ts
end

#################################
Expand Down Expand Up @@ -205,31 +204,28 @@ any_problems(::Error) = true
#####################

"""
flatten_results!(ts::AbstractTestSet)
has_description(ts::AbstractTestSet) -> Bool

Returns a flat structure 3 deep, of `TestSet` -> `TestSet` -> `Result`. This is necessary
for writing a report, as a JUnit XML does not allow one testsuite to be nested in another.
The top level `TestSet` becomes the testsuites element, and the middle level `TestSet`s
become individual testsuite elements, and the `Result`s become the testcase elements.
Determine if the testset has been provided a description.
"""
function has_description(ts::AbstractTestSet)
!isempty(ts.description) && ts.description != "test set"
end

If `ts.results` contains any `Result`s, these are added into a new `TestSet` with the
description "Top level tests", which then replaces them in `ts.results`.
"""
function flatten_results!(ts::AbstractTestSet)
# Add any top level Results to their own TestSet
handle_top_level_results!(ts)
flatten_results!(ts::AbstractTestSet)

# Flatten all results of top level testset, which should all be testsets now
ts.results = vcat(_flatten_results!.(ts.results)...)
return ts
end
Returns a flat vector of `TestSet`s which only contain `Result`s. This is necessary for
writing a JUnit XML report the schema does not allow nested XML `testsuite` elements.
"""
flatten_results!(ts::AbstractTestSet) = _flatten_results!(ts, 0)

"""
_flatten_results!(ts::AbstractTestSet)::Vector{<:AbstractTestSet}

Recursively flatten `ts` to a vector of `TestSet`s.
"""
function _flatten_results!(ts::AbstractTestSet)::Vector{<:AbstractTestSet}
function _flatten_results!(ts::AbstractTestSet, depth::Int)::Vector{AbstractTestSet}
original_results = ts.results
has_new_properties = !isempty(something(properties(ts), tuple()))
flattened_results = AbstractTestSet[]
Expand All @@ -245,13 +241,15 @@ function _flatten_results!(ts::AbstractTestSet)::Vector{<:AbstractTestSet}
function inner!(childts::AbstractTestSet)
# Make it a sibling
update_testset_properties!(childts, ts)
childts.description = ts.description * "/" * childts.description
if depth > 0 || has_description(ts)
childts.description = ts.description * "/" * childts.description
end
Comment on lines +244 to +246
Copy link
Member Author

Choose a reason for hiding this comment

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

Currently we only skip appending unnamed top-level test sets from the nested testset description. I think it would be reasonable to skip any unnamed testset at any level but didn't want to work through all of the repercussions of such a change at this time.

Copy link
Member

Choose a reason for hiding this comment

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

I agree i would want to think about that a little more

push!(flattened_results, childts)
end

# Iterate through original_results
for res in original_results
childs = _flatten_results!(res)
childs = _flatten_results!(res, depth + 1)
for child in childs
inner!(child)
end
Expand All @@ -272,7 +270,7 @@ end
Return vector containing `rs` so that when iterated through,
`rs` is added to the results vector.
"""
_flatten_results!(rs::Result) = [rs]
_flatten_results!(rs::Result, depth::Int) = [rs]

"""
update_testset_properties!(childts::AbstractTestSet, ts::AbstractTestSet)
Expand All @@ -290,51 +288,28 @@ this is handled as follows:
See also: [`properties`](@ref)
"""
function update_testset_properties!(childts::AbstractTestSet, ts::AbstractTestSet)
if isnothing(properties(childts)) && !isnothing(properties(ts)) && !isempty(properties(ts))
child_props = properties(childts)
parent_props = properties(ts)

if isnothing(child_props) && !isnothing(parent_props) && !isempty(parent_props)
@warn "Properties of testset $(ts.description) can not be added to child testset $(childts.description) as it does not have a TestReports.properties method defined."
# No need to check if childts is has properties defined and ts doesn't as if this is the case
# ts has no properties to add to that of childts.
elseif !isnothing(properties(ts))
parent_keys = keys(properties(ts))
child_keys = keys(properties(childts))
elseif !isnothing(child_props) && !isnothing(parent_props)
parent_keys = keys(parent_props)
child_keys = keys(child_props)
# Loop through keys so that warnings can be issued for any duplicates
for key in parent_keys
if key in child_keys
@warn "Property $key in testest $(ts.description) overwritten by child testset $(childts.description)"
else
properties(childts)[key] = properties(ts)[key]
child_props[key] = parent_props[key]
end
end
end
return childts
end

"""
handle_top_level_results!(ts::AbstractTestSet)

If `ts.results` contains any `Result`s, these are removed from `ts.results` and
added to a new `ReportingTestSet`, which in turn is added to `ts.results`. This
leaves `ts.results` only containing `AbstractTestSet`s.

The `time_taken` field of the new `ReportingTestSet` is calculated by summing
the time taken by the individual results, and the `start_time` field is set to
the `start_time` field of `ts`.
"""
function handle_top_level_results!(ts::AbstractTestSet)
isa_Result = isa.(ts.results, Result)
if any(isa_Result)
original_results = ts.results
ts.results = AbstractTestSet[]
ts_nested = ReportingTestSet("Top level tests")
ts_nested.results = original_results[isa_Result]
set_time_taken!(ts_nested, sum(x -> time_taken(x)::Millisecond, ts_nested.results))
set_start_time!(ts_nested, start_time(ts)::DateTime)
push!(ts.results, ts_nested)
append!(ts.results, original_results[.!isa_Result])
end
return ts
end

"""
display_reporting_testset(ts::ReportingTestSet)

Expand Down
47 changes: 24 additions & 23 deletions src/to_xml.jl
Original file line number Diff line number Diff line change
Expand Up @@ -120,30 +120,29 @@ end
#####################

"""
report(ts::AbstractTestSet)
report(ts::AbstractTestSet) -> XMLDocument

Will produce an `XMLDocument` that contains a report about the `TestSet`'s results.
This report will follow the JUnit XML schema.
Produce an JUnit XML report details about the contained `TestSet`s and `Result`s. As the
JUnit XML schema does not allow nested `testsuite` elements the report will flatten the
hierarchical `TestSet` structure. Each `TestSet` will become a `testsuite` element and each
`Result` will become a `testcase` element.

To report correctly, the `TestSet` must have the following structure:
A `Result` will only be reported once within its parent `TestSet` to avoid having duplicate
entries within the report and avoid problems with total test counts not matching Julia
output.

AbstractTestSet
└─ AbstractTestSet
└─ AbstractResult

That is, the results of the top level `TestSet` must all be `AbstractTestSet`s,
and the results of those `TestSet`s must all be `Result`s.

Additionally, all of the `AbstractTestSet`s must have both `description` and
`results` fields.
All `AbstractTestSet`s contained within `ts` must have a `description::AbstractString` field
and an iterable `results` field.
"""
function report(ts::AbstractTestSet)
check_ts(ts)
report(ts::AbstractTestSet) = report(flatten_results!(deepcopy(ts)))

function report(testsets::Vector{<:AbstractTestSet})
check_ts(testsets)
total_ntests = 0
total_nfails = 0
total_nerrors = 0
testsuiteid = 0 # ID increments from 0
x_testsuites = map(ts.results) do result
x_testsuites = map(testsets) do result
x_testsuite, ntests, nfails, nerrors = to_xml(result)
total_ntests += ntests
total_nfails += nfails
Expand All @@ -159,7 +158,7 @@ function report(ts::AbstractTestSet)
total_nerrors,
x_testsuites))

xdoc
return xdoc
end

"""
Expand All @@ -170,11 +169,13 @@ the results of `ts` do not have both `description` or `results` fields.

See also: [`report`](@ref)
"""
function check_ts(ts::AbstractTestSet)
!all(isa.(ts.results, AbstractTestSet)) && throw(ArgumentError("Results of ts must all be AbstractTestSets. See documentation for `report`."))
for results_ts in ts.results
!isa(results_ts.description, AbstractString) && throw(ArgumentError("description field of $(typeof(results_ts)) must be an AbstractString."))
!all(isa.(results_ts.results, Result)) && throw(ArgumentError("Results of each AbstractTestSet in ts.results must all be Results. See documentation for `report`."))
function check_ts(testsets::Vector{<:AbstractTestSet})
for ts in testsets
if !isa(ts.description, AbstractString)
throw(ArgumentError("description field of $(typeof(ts)) must be an `AbstractString`."))
elseif !all(r -> r isa Result, ts.results)
throw(ArgumentError("Results of each `AbstractTestSet` in ts.results must all be `Result`s. See documentation for `report`."))
end
end
end

Expand Down Expand Up @@ -342,4 +343,4 @@ function add_testsuite_properties!(x_testsuite, ts::AbstractTestSet)
link!(x_testsuite, x_properties)
end
return x_testsuite
end
end
2 changes: 1 addition & 1 deletion test/example.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Test
using TestReports

(@testset ReportingTestSet "Example" begin
(@testset ReportingTestSet "" begin
include("example_normaltestsets.jl")
end) |> report |> println
10 changes: 6 additions & 4 deletions test/recordproperty.jl
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@
end
# Force flattening as ts doesn't finish fully as it is not the top level testset
overwrite_text = "Property ID in testest Outer overwritten by child testset Inner"
@test_logs (:warn, overwrite_text) TestReports.flatten_results!(ts)
@test ts.results[2].properties["ID"] == "0"
flattened_testsets = @test_logs (:warn, overwrite_text) TestReports.flatten_results!(ts)
@test length(flattened_testsets) == 2
@test flattened_testsets[2].properties["ID"] == "0"

# Test for parent testset properties not being applied to child due to different type
ts = @testset ReportingTestSet "" begin
Expand All @@ -120,8 +121,9 @@
end
end
# Force flattening as ts doesn't finish fully as it is not the top level testset
TestReports.flatten_results!(ts)
@test ts.results[1].properties["ID"] == "42"
flattened_testsets = TestReports.flatten_results!(ts)
@test length(flattened_testsets) == 1
@test flattened_testsets[1].properties["ID"] == "42"

# Error if attempting to add property to AbstractTestSet which has properties field with wrong type
ts = @testset WrongPropsTestSet begin; recordproperty("id",1); end
Expand Down
38 changes: 12 additions & 26 deletions test/reportgeneration.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const TEST_PKG = (name = "Example", uuid = UUID("7876af07-990d-54b4-ab0e-2369062

@testset "SingleNest" begin
test_file = VERSION >= v"1.7.0" ? "references/singlenest.txt" : "references/singlenest_pre_1_7.txt"
@test_reference test_file read(`$(Base.julia_cmd()) -e "using Test; using TestReports; (@testset ReportingTestSet \"blah\" begin @testset \"a\" begin @test 1 ==1 end end) |> report |> print"`, String) |> clean_output
@test_reference test_file read(`$(Base.julia_cmd()) -e "using Test; using TestReports; (@testset ReportingTestSet \"\" begin @testset \"a\" begin @test 1 == 1 end end) |> report |> print"`, String) |> clean_output
end

@testset "Complex Example" begin
Expand Down Expand Up @@ -148,46 +148,32 @@ end
end

@testset "report - check_ts" begin
# No top level testset
ts = @testset TestReportingTestSet begin
@test true
end
@test_throws ArgumentError TestReports.check_ts(ts)

# Not flattened
ts = @testset TestReportingTestSet begin
@test true
@testset TestReportingTestSet begin
@test true
@testset TestReportingTestSet begin
@test true
end
end
end
@test_throws ArgumentError TestReports.check_ts(ts)
@test_throws ArgumentError TestReports.check_ts([ts])

# No description field
ts = @testset TestReportingTestSet begin
@testset NoDescriptionTestSet begin
@test true
end
ts = @testset NoDescriptionTestSet begin
@test true
end
@test_throws ErrorException TestReports.check_ts(ts)
@test_throws ErrorException TestReports.check_ts([ts])

# No results field
ts = @testset TestReportingTestSet begin
@testset NoResultsTestSet begin
@test true
end
ts = @testset NoResultsTestSet begin
@test true
end
@test_throws ErrorException TestReports.check_ts(ts)
@test_throws ErrorException TestReports.check_ts([ts])

# Correct structure
ts = @testset TestReportingTestSet begin
@testset TestReportingTestSet begin
@test true
end
@test true
end
@test TestReports.check_ts(ts) isa Any # Doesn't error
@test TestReports.check_ts([ts]) isa Any # Doesn't error
end

@testset "Error counting - Issue #72" begin
Expand All @@ -196,7 +182,7 @@ end
variableThatDoNotExits # No test here so shouldn't count
end
@testset "test_error" begin
@test variableThatDoNotExits == 42
@test variableThatDoNotExits == 42
end
@testset "test_unbroken" begin
@test_broken true
Expand Down
Loading
Loading