Skip to content
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
8 changes: 8 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Test shuffling support to detect hidden test dependencies (issue #530)
- `--shuffle` flag to randomize test execution order within each suite
- `--seed=N` option to specify random seed for reproducibility (0 uses time-based seed, implies --shuffle)
- Fisher-Yates shuffle algorithm implemented in `TestSuite` module
- Both unit tests and integration tests included

## [4.16.0] - 2026-02-23

### Added
Expand Down
31 changes: 29 additions & 2 deletions src/funit/FUnit.F90
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ logical function generic_run(load_tests, context) result(status)
type(ArgParser), target :: parser
logical :: debug
logical :: xml
logical :: shuffle
integer :: random_seed
type (StringUnlimitedMap) :: options
class(*), pointer :: option
integer :: unit
Expand Down Expand Up @@ -133,6 +135,24 @@ logical function generic_run(load_tests, context) result(status)
call apply_exclude_filters(option, suite, unit)
end if

! Apply shuffle if requested
shuffle = .false.
option => options%at('shuffle')
if (associated(option)) then
call cast(option, shuffle)
end if

random_seed = 0
option => options%at('random_seed')
if (associated(option)) then
call cast(option, random_seed)
if (random_seed /= 0) shuffle = .true. ! Seed implies shuffle
end if

if (shuffle) then
call suite%set_shuffle(random_seed)
end if

r = runner%run(suite, context)
status = r%wasSuccessful()

Expand Down Expand Up @@ -376,8 +396,15 @@ subroutine set_command_line_options()
& dest='tap_file', action='store', default=0, &
& help='add a TAP listener and send results to file name')

call parser%add_argument('-x', '--xml', action='store_true', &
& help='print results with XmlPrinter')
call parser%add_argument('-x', '--xml', action='store_true', &
& help='print results with XmlPrinter')

call parser%add_argument('--shuffle', action='store_true', &
& help='randomize test execution order within each suite')

call parser%add_argument('--seed', type='integer', &
& dest='random_seed', action='store', default=0, &
& help='random seed for test shuffling (0=time-based, implies --shuffle)')

#ifndef _GNU
options = parser%parse_args()
Expand Down
81 changes: 81 additions & 0 deletions src/funit/core/TestSuite.F90
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ module PF_TestSuite
!!$ private
character(:), allocatable :: name
type (TestVector) :: tests
logical :: shuffle_enabled = .false.
integer :: shuffle_seed = 0
contains
procedure :: getName
procedure :: setName
Expand All @@ -49,6 +51,8 @@ module PF_TestSuite
procedure :: getTestCases
procedure :: filter
procedure :: filter_sub
procedure :: set_shuffle
procedure :: shuffle_tests
end type TestSuite

interface TestSuite
Expand Down Expand Up @@ -77,6 +81,8 @@ recursive subroutine copy(this, b)

this%name = b%name
this%tests = b%tests
this%shuffle_enabled = b%shuffle_enabled
this%shuffle_seed = b%shuffle_seed

end subroutine copy

Expand Down Expand Up @@ -104,6 +110,11 @@ recursive subroutine run(this, tstResult, context)
class (Test), pointer :: t
integer :: i

! Shuffle tests if enabled
if (this%shuffle_enabled) then
call this%shuffle_tests()
end if

do i = 1, this%tests%size()
t => this%tests%at(i)
call t%run(tstResult, context)
Expand Down Expand Up @@ -217,4 +228,74 @@ recursive function filter(this, a_filter) result(new_suite)
end function filter


subroutine set_shuffle(this, seed)
class(TestSuite), intent(inout) :: this
integer, intent(in) :: seed

this%shuffle_enabled = .true.
this%shuffle_seed = seed
end subroutine set_shuffle


subroutine shuffle_tests(this)
class(TestSuite), intent(inout) :: this
integer :: i, n
integer, allocatable :: indices(:)
type(TestVector) :: shuffled_tests

n = this%tests%size()
if (n <= 1) return

call initialize_random_seed(this%shuffle_seed)

! Create shuffled index array
indices = [(i, i=1, n)]
call shuffle_indices(indices)

! Build new vector in shuffled order
shuffled_tests = TestVector()
do i = 1, n
call shuffled_tests%push_back(this%tests%at(indices(i)))
end do

this%tests = shuffled_tests
end subroutine shuffle_tests


subroutine initialize_random_seed(user_seed)
integer, intent(in) :: user_seed
integer, allocatable :: seed_array(:)
integer :: seed_size

if (user_seed == 0) then
! Use compiler's default random initialization
call random_seed()
return
end if

! Use user-provided seed
call random_seed(size=seed_size)
seed_array = spread(user_seed, 1, seed_size)
call random_seed(put=seed_array)
end subroutine initialize_random_seed


subroutine shuffle_indices(indices)
integer, intent(inout) :: indices(:)
integer :: i, j, n, temp
real :: rnd

n = size(indices)
! Fisher-Yates shuffle
do i = n, 2, -1
call random_number(rnd)
j = int(rnd * i) + 1
if (i == j) cycle
temp = indices(i)
indices(i) = indices(j)
indices(j) = temp
end do
end subroutine shuffle_indices


end module PF_TestSuite
17 changes: 17 additions & 0 deletions tests/funit-core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ set (test_srcs
Test_TestResult.F90
Test_XmlPrinter.F90
Test_TestSuite.F90
Test_TestSuite_Shuffle.F90
)

if (OPENMP_FORTRAN_FOUND)
Expand Down Expand Up @@ -144,5 +145,21 @@ set_tests_properties(command_line_filtering PROPERTIES
DEPENDS filter_cmdline_tests.x
)

# Shuffle integration tests
add_pfunit_ctest (shuffle_int_tests.x
TEST_SOURCES ShuffleIntegrationTests.pf
LINK_LIBRARIES other_shared
)
add_dependencies(build-tests shuffle_int_tests.x)

add_test(NAME shuffle_integration
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/test_shuffle.sh
${CMAKE_CURRENT_BINARY_DIR}/shuffle_int_tests.x
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
set_tests_properties(shuffle_integration PROPERTIES
DEPENDS shuffle_int_tests.x
)



57 changes: 57 additions & 0 deletions tests/funit-core/ShuffleIntegrationTests.pf
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
module ShuffleIntegrationTests
use FUnit
implicit none

contains

@test
subroutine test_01()
@assertTrue(.true.)
end subroutine

@test
subroutine test_02()
@assertTrue(.true.)
end subroutine

@test
subroutine test_03()
@assertTrue(.true.)
end subroutine

@test
subroutine test_04()
@assertTrue(.true.)
end subroutine

@test
subroutine test_05()
@assertTrue(.true.)
end subroutine

@test
subroutine test_06()
@assertTrue(.true.)
end subroutine

@test
subroutine test_07()
@assertTrue(.true.)
end subroutine

@test
subroutine test_08()
@assertTrue(.true.)
end subroutine

@test
subroutine test_09()
@assertTrue(.true.)
end subroutine

@test
subroutine test_10()
@assertTrue(.true.)
end subroutine

end module ShuffleIntegrationTests
Loading
Loading