Skip to content

Add support for JUnit.xml #42

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

Merged
merged 2 commits into from
Sep 8, 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
261 changes: 253 additions & 8 deletions src/testdrive.F90
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ module testdrive
public :: check, test_failed, skip_test
public :: test_interface, collect_interface
public :: get_argument, get_variable, to_string
public :: junit_output, junit_header


!> Single precision real numbers
Expand Down Expand Up @@ -304,14 +305,46 @@ end subroutine collect_interface
end type testsuite_type


!> Output JUnit.xml for discovering unit tests by other tools
type :: junit_output
!> XML output string (initial block)
character(len=:), allocatable :: xml_start
!> XML output string (current block)
character(len=:), allocatable :: xml_block
!> XML output string (final block)
character(len=:), allocatable :: xml_final
!> Unique identifier
integer :: uid = 0
!> Timestamp
character(len=19) :: timestamp = '1970-01-01T00:00:00'
!> Hostname
character(len=:), allocatable :: hostname
!> Package name
character(len=:), allocatable :: package
!> Testsuite name
character(len=:), allocatable :: testsuite
!> Number of tests
integer :: tests = 0
!> Number of failures
integer :: failures = 0
!> Number of errors
integer :: errors = 0
!> Number of skipped tests
integer :: skipped = 0
!> Running time
real(sp) :: time = 0.0_sp
end type junit_output


character(len=*), parameter :: fmt = '(1x, *(1x, a))'
character(len=*), parameter :: newline = new_line("a")


contains


!> Driver for testsuite
recursive subroutine run_testsuite(collect, unit, stat, parallel)
recursive subroutine run_testsuite(collect, unit, stat, parallel, junit)

!> Collect tests
procedure(collect_interface) :: collect
Expand All @@ -325,6 +358,9 @@ recursive subroutine run_testsuite(collect, unit, stat, parallel)
!> Run the tests in parallel
logical, intent(in), optional :: parallel

!> Produce junit output
type(junit_output), intent(inout), optional :: junit

type(unittest_type), allocatable :: testsuite(:)
integer :: it
logical :: parallel_
Expand All @@ -334,21 +370,25 @@ recursive subroutine run_testsuite(collect, unit, stat, parallel)

call collect(testsuite)

call junit_push_suite(junit, "testdrive")

!$omp parallel do schedule(dynamic) shared(testsuite, unit) reduction(+:stat) &
!$omp if (parallel_)
do it = 1, size(testsuite)
!$omp critical(testdrive_testsuite)
write(unit, '(1x, 3(1x, a), 1x, "(", i0, "/", i0, ")")') &
& "Starting", testsuite(it)%name, "...", it, size(testsuite)
!$omp end critical(testdrive_testsuite)
call run_unittest(testsuite(it), unit, stat)
call run_unittest(testsuite(it), unit, stat, junit)
end do

call junit_pop_suite(junit)

end subroutine run_testsuite


!> Driver for selective testing
recursive subroutine run_selected(collect, name, unit, stat)
recursive subroutine run_selected(collect, name, unit, stat, junit)

!> Collect tests
procedure(collect_interface) :: collect
Expand All @@ -362,15 +402,20 @@ recursive subroutine run_selected(collect, name, unit, stat)
!> Number of failed tests
integer, intent(inout) :: stat

!> Produce junit output
type(junit_output), intent(inout), optional :: junit

type(unittest_type), allocatable :: testsuite(:)
integer :: it

call collect(testsuite)

call junit_push_suite(junit, "testdrive")

it = select_test(testsuite, name)

if (it > 0 .and. it <= size(testsuite)) then
call run_unittest(testsuite(it), unit, stat)
call run_unittest(testsuite(it), unit, stat, junit)
else
write(unit, fmt) "Available tests:"
do it = 1, size(testsuite)
Expand All @@ -379,11 +424,13 @@ recursive subroutine run_selected(collect, name, unit, stat)
stat = -huge(it)
end if

call junit_pop_suite(junit)

end subroutine run_selected


!> Run a selected unit test
recursive subroutine run_unittest(test, unit, stat)
recursive subroutine run_unittest(test, unit, stat, junit)

!> Unit test
type(unittest_type), intent(in) :: test
Expand All @@ -394,13 +441,17 @@ recursive subroutine run_unittest(test, unit, stat)
!> Number of failed tests
integer, intent(inout) :: stat

!> Produce junit output
type(junit_output), intent(inout), optional :: junit

type(error_type), allocatable :: error
character(len=:), allocatable :: message

call test%test(error)
if (.not.test_skipped(error)) then
if (allocated(error) .neqv. test%should_fail) stat = stat + 1
end if
call junit_push_test(junit, test, error, 0.0_sp)
call make_output(message, test, error)
!$omp critical(testdrive_testsuite)
write(unit, '(a)') message
Expand Down Expand Up @@ -445,7 +496,7 @@ pure subroutine make_output(output, test, error)

if (test_skipped(error)) then
output = indent // test%name // " [SKIPPED]" &
& // new_line("a") // " Message: " // error%message
& // newline // " Message: " // error%message
return
end if

Expand All @@ -464,11 +515,205 @@ pure subroutine make_output(output, test, error)
end if
output = indent // test%name // label
if (present(error)) then
output = output // new_line("a") // " Message: " // error%message
output = output // newline // " Message: " // error%message
end if
end subroutine make_output


!> Initialize output for JUnit.xml
pure subroutine junit_header(junit, package)

!> JUnit output
type(junit_output), intent(inout), optional :: junit

!> Package name
character(len=*), intent(in) :: package

if (.not.present(junit)) return

junit%xml_start = &
& '<?xml version="1.0" encoding="UTF-8"?>' // newline // &
& '<testsuites' // newline // &
& ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' // newline // &
& ' xsi:noNamespaceSchemaLocation="JUnit.xsd"' // newline // &
& '>' // newline
junit%xml_block = ''
junit%xml_final = &
& '</testsuites>'

junit%hostname = 'localhost'
junit%package = package

end subroutine junit_header

!> Register a test suite in JUnit.xml
subroutine junit_push_suite(junit, name)

!> JUnit output
type(junit_output), intent(inout), optional :: junit

!> Name of the test suite
character(len=*), intent(in) :: name

if (.not.present(junit)) return

junit%timestamp = get_timestamp()
junit%testsuite = name
junit%uid = junit%uid + 1

end subroutine junit_push_suite

!> Finalize a test suite in JUnit.xml
subroutine junit_pop_suite(junit)

!> JUnit output
type(junit_output), intent(inout), optional :: junit

if (.not.present(junit)) return

junit%xml_start = &
& junit%xml_start // &
& ' <testsuite' // newline // &
& ' name="'//junit%testsuite//'"' // newline // &
& ' package="'//junit%package//'"' // newline // &
& ' id="'//to_string(junit%uid)//'"' // newline // &
& ' timestamp="'//junit%timestamp//'"' // newline // &
& ' hostname="'//junit%hostname//'"' // newline // &
& ' tests="'//to_string(junit%tests)//'"' // newline // &
& ' failures="'//to_string(junit%failures)//'"' // newline // &
& ' errors="'//to_string(junit%errors)//'"' // newline // &
& ' skipped="'//to_string(junit%skipped)//'"' // newline // &
& ' time="'//to_string(junit%time)//'"' // newline // &
& ' >' // newline // &
& ' <properties>' // newline // &
& ' </properties>' // newline // &
& junit%xml_block // newline // &
& ' </testsuite>' // newline

junit%xml_block = ''
junit%tests = 0
junit%failures = 0
junit%errors = 0
junit%skipped = 0
junit%time = 0.0_sp

call junit_write(junit)

end subroutine junit_pop_suite

!> Register a new unit test
subroutine junit_push_test(junit, test, error, time)

!> JUnit output
type(junit_output), intent(inout), optional :: junit

!> Unit test
type(unittest_type), intent(in) :: test

!> Error handling
type(error_type), intent(in), optional :: error

!> Running time
real(sp), intent(in) :: time

if (.not.present(junit)) return

!$omp critical(testdrive_junit)
junit%tests = junit%tests + 1
junit%time = junit%time + time

junit%xml_block = &
& junit%xml_block // &
& ' <testcase' // newline // &
& ' name="'//test%name//'"' // newline // &
& ' classname="'//junit%testsuite//'"' // newline // &
& ' time="'//to_string(time)//'"' // newline // &
& ' >' // newline

if (test_skipped(error)) then
junit%xml_block = &
& junit%xml_block // &
& ' <skipped/>' // newline
junit%skipped = junit%skipped + 1
elseif (present(error)) then
if (test%should_fail) then
junit%xml_block = &
& junit%xml_block // &
& ' <system-out>' // newline // &
& ' "'//error%message//'"' // newline // &
& ' </system-out>' // newline
else
junit%xml_block = &
& junit%xml_block // &
& ' <failure' // newline // &
& ' message="'//error%message//'"' // newline // &
& ' type="AssertionError"' // newline // &
& ' />' // newline
junit%failures = junit%failures + 1
end if
else
if (test%should_fail) then
junit%xml_block = &
& junit%xml_block // &
& ' <failure' // newline // &
& ' message="Unexpected pass"' // newline // &
& ' type="AssertionError"' // newline // &
& ' />' // newline
junit%failures = junit%failures + 1
else
junit%xml_block = &
& junit%xml_block // &
& ' <system-out>' // newline // &
& ' "Test passed successfully"' // newline // &
& ' </system-out>' // newline
end if
end if

junit%xml_block = &
& junit%xml_block // &
& ' </testcase>' // newline
!$omp end critical(testdrive_junit)

end subroutine junit_push_test


!> Write results to JUnit.xml
subroutine junit_write(junit)

!> JUnit output
type(junit_output), intent(inout), optional :: junit

integer :: io

if (.not.present(junit)) return
open( &
& newunit=io, &
& file='JUnit'//junit%package//'.xml', &
& status='replace', &
& action='write')
write(io, '(a)') junit%xml_start // junit%xml_final
close(io)

end subroutine junit_write


!> Create ISO 8601 formatted timestamp
function get_timestamp() result(timestamp)

!> ISO 8601 formatted timestamp
character(len=19) :: timestamp

character(len=8) :: date
character(len=10) :: time

call date_and_time(date=date, time=time)

timestamp = date(1:4) // "-" // date(5:6) // "-" // date(7:8) // "T" // &
& time(1:2) // ":" // time(3:4) // ":" // time(5:6)

end function get_timestamp


!> Select a unit test from all available tests
function select_test(tests, name) result(pos)

Expand Down Expand Up @@ -1577,7 +1822,7 @@ subroutine test_failed(error, message, more, and_more)
!> Another line of error message
character(len=*), intent(in), optional :: and_more

character(len=*), parameter :: skip = new_line("a") // repeat(" ", 11)
character(len=*), parameter :: skip = newline // repeat(" ", 11)

allocate(error)
error%stat = fatal
Expand Down
Loading
Loading