From dbf6e834664e1767d77693ab3acff9ee751be35c Mon Sep 17 00:00:00 2001 From: milancurcic Date: Mon, 13 Oct 2025 11:48:42 -0400 Subject: [PATCH 1/3] More concise testing with tuff --- test/test_dense_layer.f90 | 68 +++++++++-------------------------- test/tuff.f90 | 74 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 51 deletions(-) create mode 100644 test/tuff.f90 diff --git a/test/test_dense_layer.f90 b/test/test_dense_layer.f90 index f98d80a9..f9bf59f2 100644 --- a/test/test_dense_layer.f90 +++ b/test/test_dense_layer.f90 @@ -1,58 +1,24 @@ program test_dense_layer use iso_fortran_env, only: stderr => error_unit - use nf, only: dense, layer - use nf_activation, only: relu + use nf, only: dense, layer, relu + use tuff, only: test, test_result implicit none - type(layer) :: layer1, layer2 - logical :: ok = .true. + type(layer) :: layer1, layer2, layer3 + type(test_result) :: tests layer1 = dense(10) - - if (.not. layer1 % name == 'dense') then - ok = .false. - write(stderr, '(a)') 'dense layer has its name set correctly.. failed' - end if - - if (.not. all(layer1 % layer_shape == [10])) then - ok = .false. - write(stderr, '(a)') 'dense layer is created with requested size.. failed' - end if - - if (layer1 % initialized) then - ok = .false. - write(stderr, '(a)') 'dense layer should not be marked as initialized yet.. failed' - end if - - if (.not. layer1 % activation == 'sigmoid') then - ok = .false. - write(stderr, '(a)') 'dense layer is defaults to sigmoid activation.. failed' - end if - - layer1 = dense(10, activation=relu()) - - if (.not. layer1 % activation == 'relu') then - ok = .false. - write(stderr, '(a)') 'dense layer is created with the specified activation.. failed' - end if - - layer2 = dense(20) - call layer2 % init(layer1) - - if (.not. layer2 % initialized) then - ok = .false. - write(stderr, '(a)') 'dense layer should now be marked as initialized.. failed' - end if - - if (.not. all(layer2 % input_layer_shape == [10])) then - ok = .false. - write(stderr, '(a)') 'dense layer should have a correct input layer shape.. failed' - end if - - if (ok) then - print '(a)', 'test_dense_layer: All tests passed.' - else - write(stderr, '(a)') 'test_dense_layer: One or more tests failed.' - stop 1 - end if + layer2 = dense(10, activation=relu()) + layer3 = dense(20) + call layer3 % init(layer1) + + tests = test("Dense layer", [ & + test("layer name is set", layer1 % name == 'dense'), & + test("layer shape is correct", all(layer1 % layer_shape == [10])), & + test("layer is initialized", layer3 % initialized), & + test("layer's default activation is sigmoid", layer1 % activation == 'sigmoid'), & + test("user set activation works", layer2 % activation == 'relu'), & + test("layer initialized after init", layer3 % initialized), & + test("layer input shape is set after init", all(layer3 % input_layer_shape == [10])) & + ]) end program test_dense_layer diff --git a/test/tuff.f90 b/test/tuff.f90 new file mode 100644 index 00000000..b0dc275d --- /dev/null +++ b/test/tuff.f90 @@ -0,0 +1,74 @@ +module tuff + ! Testing Unframework for Fortran (TUFF) + use iso_fortran_env, only: stderr => error_unit, stdout => output_unit + implicit none + + private + public :: test, test_result + + type :: test_result + character(:), allocatable :: name + logical :: ok = .true. + real :: elapsed = 0. + end type test_result + + interface test + module procedure test_logical + module procedure test_func + module procedure test_array + end interface test + + abstract interface + function func() result(res) + import :: test_result + type(test_result) :: res + end function func + end interface + +contains + + type(test_result) function test_logical(name, cond) result(res) + ! Test a single logical expression. + character(*), intent(in) :: name + logical, intent(in) :: cond + res % name = name + res % ok = .true. + res % elapsed = 0. + if (.not. cond) then + write(stderr, '(a)') 'Test ' // trim(name) // ' failed.' + res % ok = .false. + end if + end function test_logical + + + type(test_result) function test_func(f) result(res) + ! Test a user-provided function f that returns a test_result. + ! f is responsible for setting the test name and the ok field. + procedure(func) :: f + real :: t1, t2 + res % name = '' + call cpu_time(t1) + res = f() + call cpu_time(t2) + res % elapsed = t2 - t1 + if (len_trim(res % name) == 0) res % name = 'Anonymous test' + if (.not. res % ok) then + write(stderr, '(a, f6.3)') 'Test failed: ' // trim(res % name) + end if + end function test_func + + + type(test_result) function test_array(name, tests) result(suite) + ! Test a suite of tests, each of which is a test_result. + character(*), intent(in) :: name + type(test_result), intent(in) :: tests(:) + suite % ok = all(tests % ok) + suite % elapsed = sum(tests % elapsed) + if (.not. suite % ok) then + ! Report to stderr only on failure. + write(stderr, '(i0,a,i0,a)') count(.not. tests % ok), '/', size(tests), & + " tests failed in suite: " // trim(name) + end if + end function test_array + +end module tuff \ No newline at end of file From 0a3dfc6186977f99eeb6a7dfb6b5956bc0422368 Mon Sep 17 00:00:00 2001 From: milancurcic Date: Mon, 13 Oct 2025 12:21:09 -0400 Subject: [PATCH 2/3] Success reporing consistent with existing tests --- test/test_dense_layer.f90 | 3 +- test/test_dense_network.f90 | 76 ++++++++++++++++++------------------- test/tuff.f90 | 9 ++--- 3 files changed, 43 insertions(+), 45 deletions(-) diff --git a/test/test_dense_layer.f90 b/test/test_dense_layer.f90 index f9bf59f2..3b863785 100644 --- a/test/test_dense_layer.f90 +++ b/test/test_dense_layer.f90 @@ -1,5 +1,4 @@ program test_dense_layer - use iso_fortran_env, only: stderr => error_unit use nf, only: dense, layer, relu use tuff, only: test, test_result implicit none @@ -11,7 +10,7 @@ program test_dense_layer layer3 = dense(20) call layer3 % init(layer1) - tests = test("Dense layer", [ & + tests = test("test_dense_layer", [ & test("layer name is set", layer1 % name == 'dense'), & test("layer shape is correct", all(layer1 % layer_shape == [10])), & test("layer is initialized", layer3 % initialized), & diff --git a/test/test_dense_network.f90 b/test/test_dense_network.f90 index fcfae094..bc377e7a 100644 --- a/test/test_dense_network.f90 +++ b/test/test_dense_network.f90 @@ -1,10 +1,10 @@ program test_dense_network use iso_fortran_env, only: stderr => error_unit - use nf, only: dense, input, network - use nf_optimizers, only: sgd + use nf, only: dense, input, network, sgd + use tuff, only: test, test_result implicit none type(network) :: net - logical :: ok = .true. + type(test_result) :: tests ! Minimal 2-layer network net = network([ & @@ -12,22 +12,29 @@ program test_dense_network dense(1) & ]) - if (.not. size(net % layers) == 2) then - write(stderr, '(a)') 'dense network should have 2 layers.. failed' - ok = .false. - end if + tests = test("test_dense_network", [ & + test("network has 2 layers", size(net % layers) == 2), & + test("network predicts 0.5 for input 0", all(net % predict([0.]) == 0.5)), & + test(simple_training), & + test(larger_network_size) & + ]) - if (.not. all(net % predict([0.]) == 0.5)) then - write(stderr, '(a)') & - 'dense network should output exactly 0.5 for input 0.. failed' - ok = .false. - end if +contains - training: block + type(test_result) function simple_training() result(res) real :: x(1), y(1) real :: tolerance = 1e-3 integer :: n - integer, parameter :: num_iterations = 1000 + integer, parameter :: num_iterations = 1000 + type(network) :: net + + res % name = 'simple training' + + ! Minimal 2-layer network + net = network([ & + input(1), & + dense(1) & + ]) x = [0.123] y = [0.765] @@ -39,32 +46,25 @@ program test_dense_network if (all(abs(net % predict(x) - y) < tolerance)) exit end do - if (.not. n <= num_iterations) then - write(stderr, '(a)') & - 'dense network should converge in simple training.. failed' - ok = .false. - end if + res % ok = n <= num_iterations - end block training + end function simple_training - ! A bit larger multi-layer network - net = network([ & - input(784), & - dense(30), & - dense(20), & - dense(10) & - ]) + type(test_result) function larger_network_size() result(res) + type(network) :: net + + res % name = 'larger network training' + + ! A bit larger multi-layer network + net = network([ & + input(784), & + dense(30), & + dense(20), & + dense(10) & + ]) - if (.not. size(net % layers) == 4) then - write(stderr, '(a)') 'dense network should have 4 layers.. failed' - ok = .false. - end if + res % ok = size(net % layers) == 4 - if (ok) then - print '(a)', 'test_dense_network: All tests passed.' - else - write(stderr, '(a)') 'test_dense_network: One or more tests failed.' - stop 1 - end if + end function larger_network_size -end program test_dense_network +end program test_dense_network \ No newline at end of file diff --git a/test/tuff.f90 b/test/tuff.f90 index b0dc275d..8fea7b03 100644 --- a/test/tuff.f90 +++ b/test/tuff.f90 @@ -13,9 +13,7 @@ module tuff end type test_result interface test - module procedure test_logical - module procedure test_func - module procedure test_array + module procedure test_logical, test_func, test_array end interface test abstract interface @@ -64,8 +62,9 @@ type(test_result) function test_array(name, tests) result(suite) type(test_result), intent(in) :: tests(:) suite % ok = all(tests % ok) suite % elapsed = sum(tests % elapsed) - if (.not. suite % ok) then - ! Report to stderr only on failure. + if (suite % ok) then + write(stdout, '(a)') trim(name) // ": All tests passed." + else write(stderr, '(i0,a,i0,a)') count(.not. tests % ok), '/', size(tests), & " tests failed in suite: " // trim(name) end if From 638ed3ba23b637c927fabc2e60b6c3fb59c41f96 Mon Sep 17 00:00:00 2001 From: milancurcic Date: Mon, 13 Oct 2025 12:37:40 -0400 Subject: [PATCH 3/3] Add tuff to cmake build --- test/CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 922a2936..64b3487e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,3 +1,5 @@ +add_library(tuff tuff.f90) + foreach(execid input1d_layer input2d_layer @@ -27,7 +29,7 @@ foreach(execid metrics ) add_executable(test_${execid} test_${execid}.f90) - target_link_libraries(test_${execid} PRIVATE neural-fortran ${LIBS}) + target_link_libraries(test_${execid} PRIVATE tuff neural-fortran ${LIBS}) add_test(NAME test_${execid} COMMAND test_${execid}) endforeach()