diff --git a/README.md b/README.md index 644f21e4d..3fa13275c 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,34 @@ Julienne: Idiomatic Correctness Checking for Fortran 2023 ========================================================= -The Julienne framework offers a unified approach to writing unit tests and -assertions. Julienne defines idioms for specifying correctness conditions in a -common in tests that wrap the tested procedures or assertions that conditionally -execute inside procedures. Julienne idioms center around expressions built from +The Julienne framework offers unified approaches to unit testing, assertion +enforcement, and formatted error-output inside `pure` procedures. Julienne +defines idioms for specifying correctness conditions in a common way in tests +that wrap the tested procedures or assertions that conditionally execute inside +procedures. Julienne idioms center around expressions built from defined operations: a uniquely flexible Fortran capability allowing developers to define _new_ operators or to overloading Fortran's intrinsic operators. +Output in pure procedures +------------------------- +Julienne's `stop_and_print` generic interface facilitates automatic or user-defined +formatting of various data types and ranks inside `pure` procedures via either of two +specific subroutines: + +1. One with a Julienne `string_t` dummy argument and +2. Another with `character` and unlimited-polymorphic/assumed-rank dummy arugments. + +The first subroutine accepts Julienne `string_t` expressions that, for example, convert +numeric arrays to comma-separated text with `.csv. string_t([1,2,3])`. The second +subroutine prints its `character` argument as a header followed by user-formatted or +automatically-formatted representrations of its polymorphic argument. Julienne +automatically formats and prints numeric scalars or arrays up to rank 3. Users can +format information for printing by encapsulating the text in a Julienne `file_t` object +or passing an object, or object wrapper, that extends Julienne's `writable_t` abstract +type and defines the so-inherited `write(formatted)` generic binding. + +Expressive idioms +----------------- Example expressions | Supported operand types -----------------------------------------------------|-------------------------------------- `x .approximates. y .within. tolerance` | `real`, `double precision` for `x`, `y`, `tolerance` @@ -38,8 +59,6 @@ where * `.equalsExpected.` generates asymmetric diagnostic output for failures, denoting the left- and right-hand sides as the actual value and expected values, respectively; and * `//` appends the subsequent string to diagnostics strings, if any. -Expressive idioms ------------------ ### Assertions Any of the above expressions can be the actual argument in an invocation of Julienne's `call_julienne_assert` function-line preprocessor macro: diff --git a/src/julienne/julienne_file_m.f90 b/src/julienne/julienne_file_m.f90 index 7a113638b..3d4f2fb4b 100644 --- a/src/julienne/julienne_file_m.f90 +++ b/src/julienne/julienne_file_m.f90 @@ -31,12 +31,18 @@ module function from_file_with_character_name(file_name) result(file_object) type(file_t) file_object end function - pure module function from_lines(lines) result(file_object) + pure module function from_string_t_lines(lines) result(file_object) implicit none type(string_t), intent(in) :: lines(:) type(file_t) file_object end function + pure module function from_character_lines(lines) result(file_object) + implicit none + character(len=*), intent(in) :: lines(:) + type(file_t) file_object + end function + end interface interface diff --git a/src/julienne/julienne_file_s.F90 b/src/julienne/julienne_file_s.F90 index bfa6dd19a..e243640eb 100644 --- a/src/julienne/julienne_file_s.F90 +++ b/src/julienne/julienne_file_s.F90 @@ -42,10 +42,14 @@ call self%write_to_character_file_name(file_name%string()) end procedure - module procedure from_lines + module procedure from_string_t_lines allocate(file_object%lines_, source=lines) end procedure + module procedure from_character_lines + allocate(file_object%lines_, source=string_t(lines)) + end procedure + module procedure from_file_with_character_name file_object = from_file_with_string_name(string_t(file_name)) end procedure diff --git a/src/julienne/julienne_stop_and_print_m.F90 b/src/julienne/julienne_stop_and_print_m.F90 new file mode 100644 index 000000000..c16466782 --- /dev/null +++ b/src/julienne/julienne_stop_and_print_m.F90 @@ -0,0 +1,75 @@ +! Copyright (c), The Regents of the University of California and Sourcery Institute +! Terms of use are as specified in LICENSE.txt + +module julienne_stop_and_print_m + !! Define a pure subroutine that formats and prints various data types during error termination + use julienne_string_m, only : string_t + implicit none + + private + public :: stop_and_print + public :: writable_t + public :: character_stop_code + + type, abstract :: writable_t + private + integer :: maxlen_ = 16384 + contains + generic :: write(formatted) => write_formatted + procedure(write_formatted_i), deferred :: write_formatted + procedure :: set_maxlen + procedure :: maxlen + end type + + abstract interface + + subroutine write_formatted_i(self, unit, edit_descriptor, v_list, iostat, iomsg) + import writable_t + class(writable_t), intent(in) :: self + integer, intent(in) :: unit + character(len=*), intent(in) :: edit_descriptor + integer, intent(in) :: v_list(:) + integer, intent(out) :: iostat + character(len=*), intent(inout) :: iomsg + end subroutine + + end interface + + interface stop_and_print + + pure module subroutine stop_and_print_string(message) + implicit none + type(string_t), intent(in) :: message + end subroutine + + pure module subroutine stop_and_print_header_and_data(header, data) + implicit none + character(len=*), intent(in) :: header + class(*), intent(in) :: data + end subroutine + + end interface + + interface + + pure module subroutine set_maxlen(self, length) + implicit none + class(writable_t), intent(inout) :: self + integer, intent(in) :: length + end subroutine + + pure module function maxlen(self) result(length) + implicit none + class(writable_t), intent(in) :: self + integer length + end function + + pure module function character_stop_code(stuff) result(stop_code) + implicit none + class(*), intent(in) :: stuff(..) + character(len=:), allocatable :: stop_code + end function + + end interface + +end module julienne_stop_and_print_m diff --git a/src/julienne/julienne_stop_and_print_s.F90 b/src/julienne/julienne_stop_and_print_s.F90 new file mode 100644 index 000000000..ad6abde9a --- /dev/null +++ b/src/julienne/julienne_stop_and_print_s.F90 @@ -0,0 +1,142 @@ +! Copyright (c), The Regents of the University of California and Sourcery Institute +! Terms of use are as specified in LICENSE.txt + +submodule(julienne_stop_and_print_m) julienne_stop_and_print_s + use julienne_string_m, only : operator(.csv.), operator(.separatedBy.) + use julienne_file_m, only : file_t + implicit none + +contains + + module procedure stop_and_print_string + error stop message%string() + end procedure + + module procedure set_maxlen + self%maxlen_ = length + end procedure + + module procedure maxlen + length = self%maxlen_ + end procedure + + module procedure stop_and_print_header_and_data +#ifndef __GFORTRAN__ + error stop new_line('') // header // new_line('') // character_stop_code(data) +#else + block + character(len=:), allocatable :: code + code = new_line('') // header // new_line('') // character_stop_code(data) + error stop code + end block +#endif + end procedure + + module procedure character_stop_code + + type(string_t) stringy_stuff + integer row, page + + select rank(stuff) + rank(0) + select type(stuff) + type is(character(len=*)) + stringy_stuff = stuff + type is(complex) + stringy_stuff = string_t(stuff) + stop_code = stringy_stuff%string() + type is(double precision) + stringy_stuff = string_t(stuff) + stop_code = stringy_stuff%string() + type is(file_t) + stringy_stuff = stuff%lines() .separatedBy. new_line('') + stop_code = stringy_stuff%string() + type is(integer) + stringy_stuff = string_t(stuff) + stop_code = stringy_stuff%string() + type is(real) + stringy_stuff = string_t(stuff) + stop_code = stringy_stuff%string() + class is(string_t) + stop_code = stuff%string() + class is(writable_t) + allocate(character(len=stuff%maxlen()) :: stop_code) + block + integer io_status + write(stop_code,*,iostat=io_status) stuff + associate(code_maxlen => string_t(stuff%maxlen())) + if (io_status /= 0) error stop "Call writable_t's set_maxlen procedure to increase stop_code maximum size above " // code_maxlen%string() + end associate + end block + stop_code = trim(stop_code) + class default + error stop "character_stop_code (in print_and_stop_s): unsupported stop-code type for scalar" + end select + rank(1) + select type(stuff) + type is(character(len=*)) + stringy_stuff = .csv. string_t(stuff) + stop_code = stringy_stuff%string() + type is(complex) + stringy_stuff = .csv. string_t(stuff) + stop_code = stringy_stuff%string() + type is(double precision) + stringy_stuff = .csv. string_t(stuff) + stop_code = stringy_stuff%string() + type is(integer) + stringy_stuff = .csv. string_t(stuff) + stop_code = stringy_stuff%string() + type is(real) + stringy_stuff = .csv. string_t(stuff) + stop_code = stringy_stuff%string() + class is(string_t) + stringy_stuff = .csv. stuff + stop_code = stringy_stuff%string() + class default + error stop "character_stop_code (in print_and_stop_s): unsupported stop-code type for rank-1 array" + end select + rank(2) + select type(stuff) + type is(character(len=*)) + stringy_stuff = [(.csv. string_t(stuff(row,:)) , row=1,size(stuff,1))] .separatedBy. new_line('') + stop_code = stringy_stuff%string() + type is(complex) + stringy_stuff = [(.csv. string_t(stuff(row,:)) , row=1,size(stuff,1))] .separatedBy. new_line('') + stop_code = stringy_stuff%string() + type is(double precision) + stringy_stuff = [(.csv. string_t(stuff(row,:)) , row=1,size(stuff,1))] .separatedBy. new_line('') + stop_code = stringy_stuff%string() + type is(integer) + stringy_stuff = [(.csv. string_t(stuff(row,:)) , row=1,size(stuff,1))] .separatedBy. new_line('') + stop_code = stringy_stuff%string() + type is(real) + stringy_stuff = [(.csv. string_t(stuff(row,:)) , row=1,size(stuff,1))] .separatedBy. new_line('') + stop_code = stringy_stuff%string() + class default + error stop "character_stop_code (in print_and_stop_s): unsupported stop-code type for rank-2 array" + end select + rank(3) + select type(stuff) + type is(complex) + stringy_stuff = [( [(.csv. string_t(stuff(row,:,page)) , row=1,size(stuff,1))] .separatedBy. new_line(''), page = 1,size(stuff,3) )] .separatedBy. (new_line('') // new_line('')) + stop_code = stringy_stuff%string() + type is(double precision) + stringy_stuff = [( [(.csv. string_t(stuff(row,:,page)) , row=1,size(stuff,1))] .separatedBy. new_line(''), page = 1,size(stuff,3) )] .separatedBy. (new_line('') // new_line('')) + stop_code = stringy_stuff%string() + type is(integer) + stringy_stuff = [( [(.csv. string_t(stuff(row,:,page)) , row=1,size(stuff,1))] .separatedBy. new_line(''), page = 1,size(stuff,3) )] .separatedBy. (new_line('') // new_line('')) + stop_code = stringy_stuff%string() + type is(real) + stringy_stuff = [( [(.csv. string_t(stuff(row,:,page)) , row=1,size(stuff,1))] .separatedBy. new_line(''), page = 1,size(stuff,3) )] .separatedBy. (new_line('') // new_line('')) + stop_code = stringy_stuff%string() + class default + error stop "character_stop_code (in print_and_stop_s): unsupported stop-code type for rank-3 array" + end select + rank default + associate(stop_code_rank => string_t(stop_code)) + error stop "character_stop_code (in print_and_stop_s): unsupported stop-code rank: " // stop_code_rank%string() + end associate + end select + end procedure + +end submodule julienne_stop_and_print_s diff --git a/src/julienne_m.F90 b/src/julienne_m.F90 index abff7c3d2..3cca68e04 100644 --- a/src/julienne_m.F90 +++ b/src/julienne_m.F90 @@ -9,6 +9,7 @@ module julienne_m use julienne_file_m, only : file_t use julienne_formats_m, only : separated_values, csv use julienne_github_ci_m, only : github_ci + use julienne_stop_and_print_m, only : stop_and_print, writable_t use julienne_string_m, only : string_t, array_of_strings & ,operator(.cat.) & ,operator(.csv.) & diff --git a/test/driver.F90 b/test/driver.F90 index 4aeb4c591..3112f44df 100644 --- a/test/driver.F90 +++ b/test/driver.F90 @@ -10,6 +10,7 @@ program test_suite_driver ! Modules containing test_t child types: use assert_test_m ,only : assert_test_t use bin_test_m ,only : bin_test_t + use character_stop_code_test_m ,only : character_stop_code_test_t use command_line_test_m ,only : command_line_test_t use formats_test_m ,only : formats_test_t use multi_image_test_m ,only : multi_image_test_t, multi_image_setup @@ -17,7 +18,6 @@ program test_suite_driver use test_description_test_m ,only : test_description_test_t use test_diagnosis_test_m ,only : test_diagnosis_test_t use test_result_test_m ,only : test_result_test_t - implicit none call multi_image_setup() @@ -27,6 +27,7 @@ program test_suite_driver associate(test_harness => test_harness_t([ & test_fixture_t( assert_test_t()) & ,test_fixture_t( bin_test_t()) & + ,test_fixture_t( character_stop_code_test_t()) & ,test_fixture_t( formats_test_t()) & ,test_fixture_t( multi_image_test_t()) & ,test_fixture_t( string_test_t()) & diff --git a/test/modules/character_stop_code_test_m.F90 b/test/modules/character_stop_code_test_m.F90 new file mode 100644 index 000000000..5ad5a6f7d --- /dev/null +++ b/test/modules/character_stop_code_test_m.F90 @@ -0,0 +1,541 @@ +! Copyright (c) 2024-2025, The Regents of the University of California and Sourcery Institute +! Terms of use are as specified in LICENSE.txt + +#include "language-support.F90" + +module character_stop_code_test_m + !! Check data partitioning across bins + use julienne_m, only : & + file_t & + ,operator(//) & + ,operator(.all.) & + ,operator(.also.) & + ,operator(.approximates.) & + ,operator(.csv.) & + ,operator(.equalsExpected.) & + ,operator(.separatedBy.) & + ,operator(.within.) & + ,passing_test & + ,string_t & + ,test_description_t & + ,test_diagnosis_t & + ,test_result_t & + ,test_t & + ,usher & + ,writable_t + use julienne_stop_and_print_m, only : character_stop_code + + implicit none + + private + public :: character_stop_code_test_t + + type, extends(test_t) :: character_stop_code_test_t + contains + procedure, nopass :: subject + procedure, nopass :: results + end type + + interface operator(.occurrencesIn.) + module procedure occurrences_in + end interface + + type, extends(writable_t) :: write_stuff_t + contains + procedure :: write_formatted + end type + +contains + + pure function subject() result(specimen) + character(len=:), allocatable :: specimen + specimen = "The character_stop_code function" + end function + + function results() result(test_results) + type(test_result_t), allocatable :: test_results(:) + type(test_description_t), allocatable :: test_descriptions(:) + type(character_stop_code_test_t) character_stop_code_test + + test_descriptions = [ & + test_description_t(string_t("converting scalars to character stop codes") , usher(check_intrinsic_scalars)) & + ,test_description_t(string_t("converting 1D arrays to comma-separated-value (CSV) character stop codes"), usher(check_intrinsic_1D_arrays)) & + ,test_description_t(string_t("converting 2D arrays to new-line-separated CSV character stop codes") , usher(check_intrinsic_2D_arrays)) & + ,test_description_t(string_t("converting 3D arrays to new-line-separated CSV character stop codes") , usher(check_intrinsic_3D_arrays)) & +#ifndef __GFORTRAN__ + ,test_description_t(string_t("converting a 1D string_t array into a CSV character stop code") , usher(check_string_t_1D_array)) & + ,test_description_t(string_t("converting a file_t object into a new-line-separated character stop code"), usher(check_file_t)) & + ,test_description_t(string_t("converting a writable_t child object into character stop code") , usher(check_writable_t)) & +#else + ,test_description_t(string_t("converting a 1D string_t array into a CSV character stop code") ) & + ,test_description_t(string_t("converting a file_t object into a new-line-separated character stop code")) & + ,test_description_t(string_t("converting a writable_t child object into character stop code") ) & +#endif + ] + test_results = character_stop_code_test%run(test_descriptions) + end function + + pure function occurrences_in(lhs, rhs) result(occurrences) + character(len=1), intent(in) :: lhs + character(len=*), intent(in) :: rhs + integer occurrences, c + occurrences = count([(rhs(c:c)==lhs, c=1,len(rhs))]) + end function + + function search_and_replace(string, search_for, replace_with, except_final) result(replacement_string) + character(len=*), intent(in) :: string + character(len=1), intent(in) :: search_for, replace_with + character(len=:), allocatable :: replacement_string + logical, intent(in), optional :: except_final + integer c, c_final, c_ + + allocate(character(len=len(string)) :: replacement_string) + + c_final = 0 + c_ = 0 + do c = 1, len(string) + c_ = c_ + 1 + if (string(c:c)==search_for) then + c_final = c + if (c>1) then + if (string(c-1:c-1) /= search_for) then + replacement_string(c_:c_) = replace_with + else + c_ = c_ - 1 + end if + end if + else + replacement_string(c_:c_) = string(c:c) + end if + end do + if (present(except_final)) then + if (except_final .and. c_final>0) replacement_string(c_final:c_final) = search_for + end if + + replacement_string = trim(replacement_string) + end function + + function check_intrinsic_scalars() result(test_diagnosis) + type(test_diagnosis_t) test_diagnosis + + integer, parameter :: expected_integer_value = 42 + integer actual_value + + real, parameter :: expected_real_value = real(expected_integer_value) + real actual_real_value + + complex, parameter :: i = (0.,1.), expected_complex_value = cmplx(expected_integer_value) - expected_integer_value*i + complex actual_complex_value + + double precision, parameter :: expected_dble_value = dble(expected_integer_value) + double precision actual_dble_value + + test_diagnosis = passing_test() + +#ifndef __GFORTRAN__ + associate(stop_code => character_stop_code(expected_integer_value)) + read(stop_code,*) actual_value + end associate + test_diagnosis = test_diagnosis .also. (actual_value .equalsExpected. expected_integer_value) // " for an integer value" + + associate(stop_code => character_stop_code(expected_real_value)) + read(stop_code,*) actual_real_value + end associate + test_diagnosis = test_diagnosis .also. (actual_real_value .approximates. expected_real_value .within. 0.) // " for a real value" + + associate(stop_code => character_stop_code(expected_complex_value)) + read(stop_code,*) actual_complex_value + end associate + test_diagnosis = test_diagnosis .also. (actual_complex_value%Re .approximates. expected_complex_value%Re .within. 0.) // " for the real part of a complex value" + test_diagnosis = test_diagnosis .also. (actual_complex_value%Im .approximates. expected_complex_value%Im .within. 0.) // " for the imaginary part of a complex value" + + associate(stop_code => character_stop_code(expected_dble_value)) + read(stop_code,*) actual_dble_value + end associate + test_diagnosis = test_diagnosis .also. (actual_dble_value .approximates. expected_dble_value .within. 0D0) // " for a double-precision value" +#else + block + character(len=:), allocatable :: stop_code + + stop_code = character_stop_code(expected_integer_value) + read(stop_code,*) actual_value + test_diagnosis = test_diagnosis .also. (actual_value .equalsExpected. expected_integer_value) + + stop_code = character_stop_code(expected_real_value) + read(stop_code,*) actual_real_value + test_diagnosis = test_diagnosis .also. (actual_real_value .approximates. expected_real_value .within. 0.) + + stop_code = character_stop_code(expected_complex_value) + read(stop_code,*) actual_complex_value + test_diagnosis = test_diagnosis .also. (actual_complex_value%Re .approximates. expected_complex_value%Re .within. 0.) + test_diagnosis = test_diagnosis .also. (actual_complex_value%Im .approximates. expected_complex_value%Im .within. 0.) + + stop_code = character_stop_code(expected_dble_value) + read(stop_code,*) actual_dble_value + test_diagnosis = test_diagnosis .also. (actual_dble_value .approximates. expected_dble_value .within. 0D0) + end block +#endif + + end function + + function check_intrinsic_1D_arrays() result(test_diagnosis) + type(test_diagnosis_t) test_diagnosis + + integer, parameter :: expected_array(*) = [1,2,3,4] + integer actual_array(size(expected_array)) + + real, parameter :: expected_real_array(*) = real(expected_array) + real actual_real_array(size(expected_real_array,1)) + + complex, parameter :: i = (0.,1.) + complex, parameter :: expected_complex_array(*) = cmplx(expected_array) - expected_array*i + complex actual_complex_array(size(expected_complex_array,1)) + + double precision, parameter :: expected_dble_array(*) = dble(expected_array) + double precision actual_dble_array(size(expected_dble_array,1)) + + integer c + + test_diagnosis = passing_test() + +#ifndef __GFORTRAN__ + associate(stop_code => character_stop_code(expected_array)) + read(stop_code,*) actual_array + test_diagnosis = test_diagnosis .also. .all. (actual_array .equalsExpected. expected_array) + test_diagnosis = test_diagnosis .also. (("," .occurrencesIn. stop_code) .equalsExpected. size(expected_array)-1) & + // " commas in " // stop_code + end associate +#else + block + character(len=:), allocatable :: stop_code + stop_code = character_stop_code(expected_array) + read(stop_code,*) actual_array + test_diagnosis = test_diagnosis .also. .all. (actual_array .equalsExpected. expected_array) + test_diagnosis = test_diagnosis .also. (count([(stop_code(c:c)==",", c=1,len(stop_code))]) .equalsExpected. size(expected_array)-1) & + // " commas in " // stop_code + end block +#endif + +#ifndef __GFORTRAN__ + associate(stop_code => character_stop_code(expected_real_array)) + read(stop_code,*) actual_real_array + test_diagnosis = test_diagnosis .also. .all. (actual_real_array .approximates. real(expected_array) .within. 0.) + test_diagnosis = test_diagnosis .also. (("," .occurrencesIn. stop_code) .equalsExpected. size(expected_real_array)-1) & + // " commas in " // stop_code + end associate +#else + block + character(len=:), allocatable :: stop_code + stop_code = character_stop_code(expected_real_array) + read(stop_code,*) actual_real_array + test_diagnosis = test_diagnosis .also. .all. (actual_real_array .approximates. real(expected_array) .within. 0.) + test_diagnosis = test_diagnosis .also. (count([(stop_code(c:c)==",", c=1,len(stop_code))]) .equalsExpected. size(expected_real_array)-1) & + // " commas in " // stop_code + end block +#endif + +#ifndef __GFORTRAN__ + associate(stop_code => character_stop_code(expected_complex_array), expected_imaginary_part => -expected_array*i) + read(stop_code,*) actual_complex_array + test_diagnosis = test_diagnosis .also. .all. (actual_complex_array%Re .approximates. real(expected_array) .within. 0.) + test_diagnosis = test_diagnosis .also. .all. (actual_complex_array%Im .approximates. expected_imaginary_part%Im .within. 0.) + end associate +#else + block + character(len=:), allocatable :: stop_code + stop_code = character_stop_code(expected_complex_array) + read(stop_code,*) actual_complex_array + test_diagnosis = test_diagnosis .also. .all. (actual_complex_array%Re .approximates. expected_complex_array%Re .within. 0.) + test_diagnosis = test_diagnosis .also. .all. (actual_complex_array%Im .approximates. expected_complex_array%Im .within. 0.) + end block +#endif + +#ifndef __GFORTRAN__ + associate(stop_code => character_stop_code(expected_dble_array)) + read(stop_code,*) actual_dble_array + test_diagnosis = test_diagnosis .also. .all. (actual_dble_array .approximates. dble(expected_array) .within. 0D0) + test_diagnosis = test_diagnosis .also. (("," .occurrencesIn. stop_code) .equalsExpected. size(expected_dble_array)-1) & + // " commas in " // stop_code + end associate +#else + block + character(len=:), allocatable :: stop_code + stop_code = character_stop_code(expected_dble_array) + read(stop_code,*) actual_dble_array + test_diagnosis = test_diagnosis .also. .all. (actual_dble_array .approximates. expected_dble_array .within. 0D0) + test_diagnosis = test_diagnosis .also. (count([(stop_code(c:c)==",", c=1,len(stop_code))]) .equalsExpected. size(expected_dble_array)-1) & + // " commas in " // stop_code + end block +#endif + end function + + function check_intrinsic_2D_arrays() result(test_diagnosis) + type(test_diagnosis_t) test_diagnosis + + integer, parameter :: expected_array(*,*) = reshape([11,21,12,22,13,23], [2,3]) + integer actual_array(size(expected_array,1),size(expected_array,2)) + + real, parameter :: expected_real_array(*,*) = real(expected_array) + real actual_real_array(size(expected_array,1),size(expected_array,2)) + + double precision, parameter :: expected_dble_array(*,*) = dble(expected_array) + double precision actual_dble_array(size(expected_dble_array,1), size(expected_dble_array,2)) + + test_diagnosis = passing_test() + +#ifndef __GFORTRAN__ + associate( & + stop_code => character_stop_code(expected_array) & + ,rows => size(expected_array,1) & + ,cols => size(expected_array,2) & + ) + read(stop_code,*) actual_array(1,:), actual_array(2,:) + test_diagnosis = test_diagnosis .also. .all. (actual_array .equalsExpected. expected_array) + test_diagnosis = test_diagnosis .also. (("," .occurrencesIn. stop_code) .equalsExpected. (cols-1)*rows) + test_diagnosis = test_diagnosis .also. ((new_line('') .occurrencesIn. stop_code) .equalsExpected. rows-1) + end associate +#else + block + character(len=:), allocatable :: stop_code + stop_code = character_stop_code(expected_array) + associate( & + rows => size(expected_array,1) & + ,cols => size(expected_array,2) & + ) + read(stop_code,*) actual_array(1,:), actual_array(2,:) + test_diagnosis = test_diagnosis .also. .all. (actual_array .equalsExpected. expected_array) + test_diagnosis = test_diagnosis .also. (("," .occurrencesIn. stop_code) .equalsExpected. (cols-1)*rows) + test_diagnosis = test_diagnosis .also. ((new_line('') .occurrencesIn. stop_code) .equalsExpected. rows-1) + end associate + end block +#endif + +#ifndef __GFORTRAN__ + associate( & + stop_code => character_stop_code(expected_real_array) & + ,rows => size(expected_real_array,1) & + ,cols => size(expected_real_array,2) & + ) + associate(one_line => search_and_replace(stop_code, search_for=new_line(''), replace_with=",")) + read(one_line,*) actual_real_array(1,:), actual_real_array(2,:) + test_diagnosis = test_diagnosis .also. .all. (actual_real_array .approximates. real(expected_array) .within. 0.) + test_diagnosis = test_diagnosis .also. (("," .occurrencesIn. stop_code) .equalsExpected. (cols-1)*rows) + test_diagnosis = test_diagnosis .also. ((new_line('') .occurrencesIn. stop_code) .equalsExpected. rows-1) + end associate + end associate +#else + block + character(len=:), allocatable :: stop_code, one_line + stop_code = character_stop_code(expected_real_array) + associate( & + rows => size(expected_real_array,1) & + ,cols => size(expected_real_array,2) & + ) + one_line = search_and_replace(stop_code, search_for=new_line(''), replace_with=",") + read(one_line,*) actual_real_array(1,:), actual_real_array(2,:) + test_diagnosis = test_diagnosis .also. .all. (actual_real_array .approximates. real(expected_array) .within. 0.) + test_diagnosis = test_diagnosis .also. (("," .occurrencesIn. stop_code) .equalsExpected. (cols-1)*rows) + test_diagnosis = test_diagnosis .also. ((new_line('') .occurrencesIn. stop_code) .equalsExpected. rows-1) + end associate + end block +#endif + +#ifndef __GFORTRAN__ + associate( & + stop_code => character_stop_code(expected_dble_array) & + ,rows => size(expected_dble_array,1) & + ,cols => size(expected_dble_array,2) & + ) + associate(one_line => search_and_replace(stop_code, search_for=new_line(''), replace_with=",")) + read(one_line,*) actual_dble_array(1,:), actual_dble_array(2,:) + test_diagnosis = test_diagnosis .also. .all. (actual_dble_array .approximates. dble(expected_array) .within. 0D0) + test_diagnosis = test_diagnosis .also. (("," .occurrencesIn. stop_code) .equalsExpected. (cols-1)*rows) + test_diagnosis = test_diagnosis .also. ((new_line('') .occurrencesIn. stop_code) .equalsExpected. rows-1) + end associate + end associate +#else + block + character(len=:), allocatable :: stop_code, one_line + stop_code = character_stop_code(expected_dble_array) + associate( & + rows => size(expected_dble_array,1) & + ,cols => size(expected_dble_array,2) & + ) + one_line = search_and_replace(stop_code, search_for=new_line(''), replace_with=",") + read(one_line,*) actual_dble_array(1,:), actual_dble_array(2,:) + test_diagnosis = test_diagnosis .also. .all. (actual_dble_array .approximates. dble(expected_array) .within. 0D0) + test_diagnosis = test_diagnosis .also. (("," .occurrencesIn. stop_code) .equalsExpected. (cols-1)*rows) + test_diagnosis = test_diagnosis .also. ((new_line('') .occurrencesIn. stop_code) .equalsExpected. rows-1) + end associate + end block +#endif + end function + + function check_intrinsic_3D_arrays() result(test_diagnosis) + type(test_diagnosis_t) test_diagnosis + integer, parameter :: expected_array(*,*,*) = reshape([111,211,121,221, 112,212,122,222, 113,213,123,223], [2,2,3]) + integer actual_array(size(expected_array,1),size(expected_array,2),size(expected_array,3)) + + real, parameter :: expected_real_array(*,*,*) = real(expected_array) + real actual_real_array(size(expected_real_array,1),size(expected_real_array,2),size(expected_real_array,3)) + + double precision, parameter :: expected_dble_array(*,*,*) = dble(expected_array) + double precision actual_dble_array(size(expected_dble_array,1), size(expected_dble_array,2), size(expected_dble_array,3)) + + test_diagnosis = passing_test() + +#ifndef __GFORTRAN__ + associate( & + stop_code => character_stop_code(expected_array) & + ,rows => size(expected_array,1) & + ,cols => size(expected_array,2) & + ,pages => size(expected_array,3) & + ) + associate(one_line => search_and_replace(stop_code, search_for=new_line(''), replace_with=",")) + read(one_line,'(*(i3,1x))') actual_array(1,:,1), actual_array(2,:,1), actual_array(1,:,2), actual_array(2,:,2), actual_array(1,:,3), actual_array(2,:,3) + test_diagnosis = test_diagnosis .also. .all. (actual_array .equalsExpected. expected_array) + test_diagnosis = test_diagnosis .also. (("," .occurrencesIn. stop_code) .equalsExpected. (cols-1)*rows*pages) // " commas" + test_diagnosis = test_diagnosis .also. ((new_line('') .occurrencesIn. stop_code) .equalsExpected. (rows*pages-1) + (pages-1)) & + // " new-line characters" + end associate + end associate +#else + block + character(len=:), allocatable :: stop_code, one_line + stop_code = character_stop_code(expected_array) + associate( & + rows => size(expected_array,1) & + ,cols => size(expected_array,2) & + ,pages => size(expected_array,3) & + ) + one_line = search_and_replace(stop_code, search_for=new_line(''), replace_with=",") + read(one_line,'(*(i3,1x))') actual_array(1,:,1), actual_array(2,:,1), actual_array(1,:,2), actual_array(2,:,2), actual_array(1,:,3), actual_array(2,:,3) + test_diagnosis = test_diagnosis .also. .all. (actual_array .equalsExpected. expected_array) + test_diagnosis = test_diagnosis .also. (("," .occurrencesIn. stop_code) .equalsExpected. (cols-1)*rows*pages) // " commas" + test_diagnosis = test_diagnosis .also. ((new_line('') .occurrencesIn. stop_code) .equalsExpected. (rows*pages-1) + (pages-1)) & + // " new-line characters" + end associate + end block +#endif + +#ifndef __GFORTRAN__ + associate( & + stop_code => character_stop_code(expected_real_array) & + ,rows => size(expected_real_array,1) & + ,cols => size(expected_real_array,2) & + ,pages => size(expected_real_array,3) & + ) + associate(one_line => trim(search_and_replace(stop_code, search_for=new_line(''), replace_with=","))) + read(one_line(1:179),*) actual_real_array(1,:,1), actual_real_array(2,:,1), actual_real_array(1,:,2), actual_real_array(2,:,2), actual_real_array(1,:,3), actual_real_array(2,:,3) + test_diagnosis = test_diagnosis .also. .all. (actual_real_array .approximates. expected_real_array .within. 0.) + test_diagnosis = test_diagnosis .also. (("," .occurrencesIn. stop_code) .equalsExpected. (cols-1)*rows*pages) // " commas" + test_diagnosis = test_diagnosis .also. ((new_line('') .occurrencesIn. stop_code) .equalsExpected. (rows*pages-1) + (pages-1)) & + // " new-line characters" + end associate + end associate +#else + block + character(len=:), allocatable :: stop_code, one_line + stop_code = character_stop_code(expected_real_array) + associate( & + rows => size(expected_real_array,1) & + ,cols => size(expected_real_array,2) & + ,pages => size(expected_real_array,3) & + ) + one_line = trim(search_and_replace(stop_code, search_for=new_line(''), replace_with=",")) + read(one_line(1:179),*) actual_real_array(1,:,1), actual_real_array(2,:,1), actual_real_array(1,:,2), actual_real_array(2,:,2), actual_real_array(1,:,3), actual_real_array(2,:,3) + test_diagnosis = test_diagnosis .also. .all. (actual_real_array .approximates. expected_real_array .within. 0.) + test_diagnosis = test_diagnosis .also. (("," .occurrencesIn. stop_code) .equalsExpected. (cols-1)*rows*pages) // " commas" + test_diagnosis = test_diagnosis .also. ((new_line('') .occurrencesIn. stop_code) .equalsExpected. (rows*pages-1) + (pages-1)) & + // " new-line characters" + end associate + end block +#endif + +#ifndef __GFORTRAN__ + associate( & + stop_code => character_stop_code(expected_dble_array) & + ,rows => size(expected_dble_array,1) & + ,cols => size(expected_dble_array,2) & + ,pages => size(expected_dble_array,3) & + ) + associate(one_line => trim(search_and_replace(stop_code, search_for=new_line(''), replace_with=","))) + read(one_line(1:179),*) actual_dble_array(1,:,1), actual_dble_array(2,:,1), actual_dble_array(1,:,2), actual_dble_array(2,:,2), actual_dble_array(1,:,3), actual_dble_array(2,:,3) + test_diagnosis = test_diagnosis .also. .all. (actual_dble_array .approximates. expected_dble_array .within. 0D0) + test_diagnosis = test_diagnosis .also. (("," .occurrencesIn. stop_code) .equalsExpected. (cols-1)*rows*pages) // " commas" + test_diagnosis = test_diagnosis .also. ((new_line('') .occurrencesIn. stop_code) .equalsExpected. (rows*pages-1) + (pages-1)) & + // " new-line characters" + end associate + end associate +#else + block + character(len=:), allocatable :: stop_code, one_line + stop_code = character_stop_code(expected_dble_array) + associate( & + rows => size(expected_dble_array,1) & + ,cols => size(expected_dble_array,2) & + ,pages => size(expected_dble_array,3) & + ) + one_line = trim(search_and_replace(stop_code, search_for=new_line(''), replace_with=",")) + read(one_line(1:179),*) actual_dble_array(1,:,1), actual_dble_array(2,:,1), actual_dble_array(1,:,2), actual_dble_array(2,:,2), actual_dble_array(1,:,3), actual_dble_array(2,:,3) + test_diagnosis = test_diagnosis .also. .all. (actual_dble_array .approximates. expected_dble_array .within. 0D0) + test_diagnosis = test_diagnosis .also. (("," .occurrencesIn. stop_code) .equalsExpected. (cols-1)*rows*pages) // " commas" + test_diagnosis = test_diagnosis .also. ((new_line('') .occurrencesIn. stop_code) .equalsExpected. (rows*pages-1) + (pages-1)) & + // " new-line characters" + end associate + end block +#endif + end function + + function check_string_t_1D_array() result(test_diagnosis) + type(test_diagnosis_t) test_diagnosis + + test_diagnosis = passing_test() + + associate(messages => string_t(["foo","bar"])) + associate( & + expected_stop_code => .csv. messages & + ,stop_code => character_stop_code(messages) & + ) + test_diagnosis = test_diagnosis .also. (stop_code .equalsExpected. expected_stop_code) + end associate + end associate + end function + + function check_file_t() result(test_diagnosis) + type(test_diagnosis_t) test_diagnosis + + test_diagnosis = passing_test() + + associate(file_ => file_t(["yada","yada"])) + associate( & + expected_stop_code => file_%lines() .separatedBy. new_line('') & + ,stop_code => character_stop_code(file_) & + ) + test_diagnosis = test_diagnosis .also. (stop_code .equalsExpected. expected_stop_code) + end associate + end associate + end function + + function check_writable_t() result(test_diagnosis) + type(test_diagnosis_t) test_diagnosis + type(write_stuff_t) write_stuff + + test_diagnosis = passing_test() + + associate(stop_code => character_stop_code(write_stuff)) + test_diagnosis = stop_code .equalsExpected. "written stuff" + end associate + end function + + subroutine write_formatted(self, unit, edit_descriptor, v_list, iostat, iomsg) + class(write_stuff_t), intent(in) :: self + integer, intent(in) :: unit + character(len=*), intent(in) :: edit_descriptor + integer, intent(in) :: v_list(:) + integer, intent(out) :: iostat + character(len=*), intent(inout) :: iomsg + write(unit,'(a)') "written stuff" + end subroutine + +end module character_stop_code_test_m diff --git a/test/test_stop_and_print.F90 b/test/test_stop_and_print.F90 new file mode 100644 index 000000000..194eab78c --- /dev/null +++ b/test/test_stop_and_print.F90 @@ -0,0 +1,38 @@ +! Copyright (c) 2024-2025, The Regents of the University of California and Sourcery Institute +! Terms of use are as specified in LICENSE.txt + +#include "julienne-assert-macros.h" +#include "language-support.F90" + +program stop_and_print_in_pure_procedure + !! Conditionally test printing via error termination inside a pure procedure + use julienne_m, only : command_line_t, string_t, operator(.csv.), stop_and_print + implicit none + +#if HAVE_MULTI_IMAGE_SUPPORT + associate(command_line => command_line_t(), me => this_image()) +#else + associate(command_line => command_line_t(), me => 1) +#endif + if (.not. command_line%argument_present([character(len=len("--help"))::"--help","-h"])) then +#if TEST_INTENTIONAL_FAILURE && ASSERTIONS + if (me==1) print '(a)', new_line('') // 'Test the intentional failure of an idiomatic assertion: ' // new_line('') + call pure_subroutine +#else + if (me==1) print '(a)', & + new_line('') // & + 'Skipping the test in ' // __FILE__ // '.' // new_line('') // & + 'Add the following to your fpm command to test the stop_and_print: --flag "-DASSERTIONS -DTEST_INTENTIONAL_FAILURE"' // & + new_line('') +#endif + end if + end associate + +contains + + pure subroutine pure_subroutine + integer, parameter :: array(*) = [1,2,3,4] + call stop_and_print("array = " // .csv. string_t(array)) + end subroutine + +end program