diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bec60033a..9c185687c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -293,6 +293,25 @@ jobs: test ${PIPESTATUS[0]} > 0 && grep -q "expected 2; actual value is 1" output ) + - name: Run Stop-and-Print Example (Assertions ON) + if: ${{ matrix.compiler == 'gfortran' || + (matrix.compiler == 'flang' && (matrix.version >= 20 || matrix.version == 'latest') ) }} + env: + FPM_FLAGS: ${{ env.FPM_FLAGS }} --flag -DASSERTIONS + run: | + ( set +e + fpm run --example pure-stop-and-print ${FPM_FLAGS} --flag "$FFLAGS" -- --array 2>&1 | tee output + test ${PIPESTATUS[0]} > 0 && grep -q "212,222" output + ) + ( set +e + fpm run --example pure-stop-and-print ${FPM_FLAGS} --flag "$FFLAGS" -- --derived-type 2>&1 | tee output + test ${PIPESTATUS[0]} > 0 && grep -q "answer = 42" output + ) + ( set +e + fpm run --example pure-stop-and-print ${FPM_FLAGS} --flag "$FFLAGS" -- --file fpm.toml 2>&1 | tee output + test ${PIPESTATUS[0]} > 0 && grep -q "[install]" output + ) + - name: Test w/ Parallel Callbacks env: FPM_FLAGS: ${{ env.FPM_FLAGS }} --flag -DJULIENNE_PARALLEL_CALLBACKS --flag -DTEST_PARALLEL_CALLBACKS diff --git a/README.md b/README.md index e10f5e916..e67a51e7d 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 arguments. + +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/app/scaffold.F90 b/app/scaffold.F90 index 504a3a263..9fce11dae 100644 --- a/app/scaffold.F90 +++ b/app/scaffold.F90 @@ -10,11 +10,11 @@ program scaffold if (help_requested()) call print_usage_info_and_stop #if (! __GNUC__) && (! NAGFOR) - associate(subjects_file_name => command_line%flag_value("--json-file")) + associate(subjects_file_name => command_line%character_flag_value("--json-file")) if (len(subjects_file_name) == 0) call print_usage_info_and_stop print '(*(a))', "Reading test subject information from " // subjects_file_name associate(test_suite => test_suite_t(file_t(subjects_file_name))) - associate(path => command_line%flag_value("--suite-path")) + associate(path => command_line%character_flag_value("--suite-path")) print '(*(a))', "Writing test-suite scaffolding in " // path if (len(path) == 0) call print_usage_info_and_stop associate(driver => test_suite%driver_file()) @@ -33,11 +33,11 @@ program scaffold #else block character(len=:), allocatable :: path, subjects_file_name - subjects_file_name = command_line%flag_value("--json-file") + subjects_file_name = command_line%character_flag_value("--json-file") if (len(subjects_file_name) == 0) call print_usage_info_and_stop print '(*(a))', "Reading test subject information from " // subjects_file_name associate(test_suite => test_suite_t(file_t(subjects_file_name))) - path = command_line%flag_value("--suite-path") + path = command_line%character_flag_value("--suite-path") if (len(path) == 0) call print_usage_info_and_stop print '(*(a))', "Writing test-suite scaffolding in " // path call test_suite%write_driver(path // "/driver.f90") @@ -56,7 +56,7 @@ program scaffold logical function help_requested() type(command_line_t) command_line - help_requested = command_line%argument_present([character(len=len("--help"))::"--help","-h"]) + help_requested = command_line%character_argument_present([character(len=len("--help"))::"--help","-h"]) end function subroutine print_usage_info_and_stop diff --git a/example/README.md b/example/README.md index 4bcbee270..276ec83ee 100644 --- a/example/README.md +++ b/example/README.md @@ -4,5 +4,6 @@ Please see the following directories for examples of the listed Julienne uses: * [Assertions](./assertions): runtime assertion checking using idioms that evaluate to `test_diagnosis_t` result objects, * [Command-line parsing](./command-line-parsing): checking a command-line flag's presence and getting an associated value using the `command_line_t` type, +* [Printing in procedures](./pure-printing): producing automatically-formatted or user-formatted text in `pure` procedures via the `stop_and_print` subroutine, * [String operations](./strings): operating on strings using the `string_t` type and defined operations, and * [Testing](../demo): a demonstration unit test suite using the `test_t` type. diff --git a/example/command-line-parsing/check-for-command-line-argument.f90 b/example/command-line-parsing/check-for-command-line-argument.f90 index 4e7e53a3a..64d18fb24 100644 --- a/example/command-line-parsing/check-for-command-line-argument.f90 +++ b/example/command-line-parsing/check-for-command-line-argument.f90 @@ -16,7 +16,7 @@ program check_for_command_line_argument type(command_line_t) command_line - if (command_line%argument_present(["--some-argument"])) then + if (command_line%character_argument_present(["--some-argument"])) then print '(a)', new_line('') // "argument 'some-argument' present" // new_line('') else print '(a)', new_line('') // "argument 'some-argument' not present" // new_line('') diff --git a/example/command-line-parsing/get-command-line-flag-value.f90 b/example/command-line-parsing/get-command-line-flag-value.f90 index 6e0b2a8b8..e0be2e567 100644 --- a/example/command-line-parsing/get-command-line-flag-value.f90 +++ b/example/command-line-parsing/get-command-line-flag-value.f90 @@ -14,7 +14,7 @@ program get_command_line_flag_value type(command_line_t) command_line character(len=:), allocatable :: foo_value - foo_value = command_line%flag_value("--foo") + foo_value = command_line%character_flag_value("--foo") if (len(foo_value)/=0) then print '(a)', new_line('') // "foo=" // foo_value // new_line('') diff --git a/example/pure-printing/README.md b/example/pure-printing/README.md new file mode 100644 index 000000000..d876dcd3e --- /dev/null +++ b/example/pure-printing/README.md @@ -0,0 +1,58 @@ +Pure Printing +============= + +This directory contains a program and a supporting module/submodule +pair that collectively demonstrate the use of Julienne's +`stop_and_print` subroutine designed for use inside `pure` procedures. +Specifically, the program shows how to print the following entities: + +- a text file, +- a two-dimensional (2D) integer array, or +- an object of derived type. + +``` +Usage: + fpm run \ + --example pure-stop-and-print \ + --compiler flang --profile release \ + -- [-h|--help] | [--file ] | [--array] | [--derived-type] +``` + +where pipes (|) separate alternatives, square brackets ([]) delimit +optional arguments, and angular brackets (<>) delimit user input. + +Getting help +------------ +The following command prints the above usage text: + +``` + fpm run --example pure-stop-and-print --compiler flang -- --help +``` + +Examples +-------- +### Printing a 2D integer array +The following command prints a 2D integer array defined in the example program: + +``` + fpm run --example pure-stop-and-print --compiler flang -- --array +``` + +### Printing a derived type +The following command prints the derived type defined in this directory's +[write_stuff_m](./write_stuff_m.F90)) module: + +``` + fpm run --example pure-stop-and-print --compiler flang -- --derived-type +``` + +using the derived-type output procedure in the [write_stuff_s](./write_stuff_s.F90)) +submodule. + +### Printing a text file + +The following command prints this repository's `fpm` manifest: `fpm.toml`. + +``` + fpm run --example pure-stop-and-print --compiler flang -- --file fpm.toml +``` diff --git a/example/pure-printing/pure-stop-and-print.F90 b/example/pure-printing/pure-stop-and-print.F90 new file mode 100644 index 000000000..b7ed1902a --- /dev/null +++ b/example/pure-printing/pure-stop-and-print.F90 @@ -0,0 +1,55 @@ +! Copyright (c) 2024-2026, The Regents of the University of California and Sourcery Institute +! Terms of use are as specified in LICENSE.txt + +#include "language-support.F90" + +program pure_stop_and_print +#if HAVE_STOP_AND_PRINT_SUPPORT + !! Demonstrate Julienne's support for printing during error termination inside pure procedures + use julienne_m, only : & + command_line_t & + ,file_t & + ,stop_and_print & + ,string_t + use write_stuff_m, only : write_stuff_t + implicit none + + type(command_line_t) command_line + character(len=:), allocatable :: stop_code, file_name + + stop_code = usage_info() + if ( command_line%string_argument_present( [string_t("--help"), string_t("-h") ] )) stop stop_code + if (.not. command_line%string_argument_present( [string_t("--file"), string_t("--array"), string_t("--derived-type")] )) error stop stop_code + + file_name = command_line%flag_value("--file") + if (len(file_name) > 0) then + call pure_subroutine(.false., .false., file_t(file_name)) + end if + call pure_subroutine(command_line%argument_present(["--array"]), command_line%argument_present(["--derived-type"])) + +contains + + pure subroutine pure_subroutine(print_array, print_derived_type, file) + logical, intent(in) :: print_array, print_derived_type + type(file_t), intent(in), optional :: file + if (present(file)) call stop_and_print(header = "______________", data = file, footer = "______________") + if (print_array) call stop_and_print(reshape([111,211,121,221, 112,212,122,222], [2,2,2])) + if (print_derived_type) call stop_and_print(write_stuff_t()) + end subroutine + + pure function usage_info() result(message) + character(len=:), allocatable :: message + message = new_line('') // new_line('') & + // 'Usage:' // new_line('') // new_line('') & + // ' fpm run \' // new_line('') & + // ' --example pure-stop-and-print \' // new_line('') & + // ' --compiler flang --profile release \' // new_line('') & + // ' -- [-h|--help] | [--file ] | [--array] | [--derived-type]' // new_line('') // new_line('') & + // 'where pipes (|) separate alternatives, square brackets ([]) delimit' // new_line('') & + // 'optional arguments, and angular brackets (<>) delimit user input.' // new_line('') + end function + +#else + error stop "Julienne's stop_and_print feature is not supported on this compiler" +#endif +end program pure_stop_and_print diff --git a/example/pure-printing/write_stuff_m.F90 b/example/pure-printing/write_stuff_m.F90 new file mode 100644 index 000000000..116cb976d --- /dev/null +++ b/example/pure-printing/write_stuff_m.F90 @@ -0,0 +1,32 @@ +! Copyright (c) 2024-2026, The Regents of the University of California and Sourcery Institute +! Terms of use are as specified in LICENSE.txt + +#include "language-support.F90" + +#if HAVE_STOP_AND_PRINT_SUPPORT +module write_stuff_m + !! Demonstrate a derived type that is writable to a stop via Julienne's stop_and_print utility + use julienne_m, only : writable_t + implicit none + + type, extends(writable_t) :: write_stuff_t + integer :: answer_ = 42 + contains + procedure :: write_formatted + end type + + interface + + module 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 + end subroutine + + end interface + +end module +#endif diff --git a/example/pure-printing/write_stuff_s.F90 b/example/pure-printing/write_stuff_s.F90 new file mode 100644 index 000000000..06b265711 --- /dev/null +++ b/example/pure-printing/write_stuff_s.F90 @@ -0,0 +1,21 @@ +! Copyright (c) 2024-2026, The Regents of the University of California and Sourcery Institute +! Terms of use are as specified in LICENSE.txt + +#include "language-support.F90" + +#if HAVE_STOP_AND_PRINT_SUPPORT +submodule(write_stuff_m) write_stuff_s + implicit none + +contains + + module procedure write_formatted + write(unit,'(a)' ) new_line('') + write(unit,'(a)' ) "write_stuff_t {" // new_line('') + write(unit,'(a,i2,a)') " answer = ", self%answer_, new_line('') + write(unit,'(a)' ) "}" // new_line('') + iostat = 0 + end procedure + +end submodule +#endif diff --git a/include/language-support.F90 b/include/language-support.F90 index ff0678283..7b3e204a1 100644 --- a/include/language-support.F90 +++ b/include/language-support.F90 @@ -14,6 +14,12 @@ #define GCC_GE_MINIMUM #endif +#if defined(__GFORTRAN__) || __flang_major__ >= 20 +# define HAVE_STOP_AND_PRINT_SUPPORT 1 +#else +# define HAVE_STOP_AND_PRINT_SUPPORT 0 +#endif + ! If not already determined, make a compiler-dependent determination of whether Julienne may use ! multi-image features such as `this_image()` and `sync all`. #ifndef HAVE_MULTI_IMAGE_SUPPORT diff --git a/src/julienne/julienne_command_line_m.f90 b/src/julienne/julienne_command_line_m.f90 index c555d7e0d..b9c07d268 100644 --- a/src/julienne/julienne_command_line_m.f90 +++ b/src/julienne/julienne_command_line_m.f90 @@ -3,6 +3,7 @@ module julienne_command_line_m !! return command line argument information + use julienne_string_m, only : string_t implicit none private @@ -10,13 +11,15 @@ module julienne_command_line_m type command_line_t contains - procedure, nopass :: argument_present - procedure, nopass :: flag_value + generic :: argument_present => character_argument_present, string_argument_present + procedure, nopass :: character_argument_present, string_argument_present + generic :: flag_value => character_flag_value, string_flag_value + procedure, nopass :: character_flag_value, string_flag_value end type interface - module function argument_present(acceptable_argument) result(found) + module function character_argument_present(acceptable_argument) result(found) implicit none !! result is .true. only if a command-line argument matches an element of this function's argument character(len=*), intent(in) :: acceptable_argument(:) @@ -26,12 +29,26 @@ module function argument_present(acceptable_argument) result(found) logical found end function - module function flag_value(flag) + module function string_argument_present(acceptable_argument) result(found) + implicit none + !! same as `character_argument_present` but allowing ragged-edged array of character values + type(string_t), intent(in) :: acceptable_argument(:) + logical found + end function + + module function character_flag_value(flag) result(value) !! result = { the value passed immediately after a command-line flag if the flag is present or !! { an empty string otherwise. implicit none character(len=*), intent(in) :: flag - character(len=:), allocatable :: flag_value + character(len=:), allocatable :: value + end function + + module function string_flag_value(flag) result(value) + !! same as `character_flag_value` but accepting a string_t dummy argument + implicit none + type(string_t), intent(in) :: flag + character(len=:), allocatable :: value end function end interface diff --git a/src/julienne/julienne_command_line_s.F90 b/src/julienne/julienne_command_line_s.F90 new file mode 100644 index 000000000..33f3b41ac --- /dev/null +++ b/src/julienne/julienne_command_line_s.F90 @@ -0,0 +1,102 @@ +! Copyright (c) 2024-2026, The Regents of the University of California and Sourcery Institute +! Terms of use are as specified in LICENSE.txt + +#include "language-support.F90" + +submodule(julienne_command_line_m) julienne_command_line_s + implicit none + +contains + + module procedure string_argument_present +# ifndef __GFORTRAN__ + integer a + integer maxlen + + maxlen = maxval([(len(acceptable_argument(a)%string()), a = 1,size(acceptable_argument))]) + found = character_argument_present( & + [( [character(len=maxlen) :: acceptable_argument(a)%string()], a = 1, size(acceptable_argument))] & + ) +# else + integer a, sz, maxlen + + sz = size(acceptable_argument) + maxlen = maxval([(len(acceptable_argument(a)%string()), a = 1,sz)]) + block + character(maxlen) :: strings(size(acceptable_argument)) + do a=1,sz + strings(a) = acceptable_argument(a)%string() + end do + found = character_argument_present(strings) + end block +#endif +# ifdef __INTEL_COMPILER + ! workaround ifx bug where it thinks argument to len must be a constant expression + contains + pure function len(char) result(l) + character(len=*), intent(in) :: char + integer :: l + block + intrinsic :: len + l = len(char) + end block + end function +# endif + end procedure + + module procedure character_argument_present ! specific procedure for character argument + !! list of acceptable arguments + !! sample list: [character(len=len(longest_argument)):: "--benchmark", "-b", "/benchmark", "/b"] + !! where dashes support Linux/macOS and slashes support Windows + integer :: i, argnum, arglen + !! loop counter, argument position, argument length + character(len=32) arg + !! argument position + + !! acceptable argument lengths (used to preclude extraneous trailing characters) + + associate(acceptable_length => [(len(trim(acceptable_argument(i))), i = 1, size(acceptable_argument))]) + + do argnum = 1,command_argument_count() + + call get_command_argument(argnum, arg, arglen) + + if (any( & + [(arg==acceptable_argument(i) .and. arglen==acceptable_length(i), i = 1, size(acceptable_argument))] & + )) then + found = .true. + return + end if + + end do + + found = .false. + + end associate + + end procedure + + module procedure string_flag_value + value = character_flag_value(flag%string()) + end procedure + + module procedure character_flag_value ! specific procedure for character argument + integer argnum, arglen, value_length + character(len=:), allocatable :: arg + + do argnum = 1,command_argument_count()-1 + call get_command_argument(argnum, length=arglen) + allocate(character(len=arglen) :: arg) + call get_command_argument(argnum, arg) + if (arg==flag) then + call get_command_argument(argnum+1, length=value_length) + allocate(character(len=value_length) :: value) + call get_command_argument(argnum+1, value) + return + end if + deallocate(arg) + end do + value="" + end procedure + +end submodule diff --git a/src/julienne/julienne_command_line_s.f90 b/src/julienne/julienne_command_line_s.f90 deleted file mode 100644 index 5413b87dd..000000000 --- a/src/julienne/julienne_command_line_s.f90 +++ /dev/null @@ -1,60 +0,0 @@ -! Copyright (c) 2024-2025, The Regents of the University of California and Sourcery Institute -! Terms of use are as specified in LICENSE.txt - -submodule(julienne_command_line_m) julienne_command_line_s - implicit none - -contains - - module procedure argument_present - !! list of acceptable arguments - !! sample list: [character(len=len(longest_argument)):: "--benchmark", "-b", "/benchmark", "/b"] - !! where dashes support Linux/macOS and slashes support Windows - integer :: i, argnum, arglen - !! loop counter, argument position, argument length - character(len=32) arg - !! argument position - - !! acceptable argument lengths (used to preclude extraneous trailing characters) - - associate(acceptable_length => [(len(trim(acceptable_argument(i))), i = 1, size(acceptable_argument))]) - - do argnum = 1,command_argument_count() - - call get_command_argument(argnum, arg, arglen) - - if (any( & - [(arg==acceptable_argument(i) .and. arglen==acceptable_length(i), i = 1, size(acceptable_argument))] & - )) then - found = .true. - return - end if - - end do - - found = .false. - - end associate - - end procedure - - module procedure flag_value - integer argnum, arglen, flag_value_length - character(len=:), allocatable :: arg - - do argnum = 1,command_argument_count()-1 - call get_command_argument(argnum, length=arglen) - allocate(character(len=arglen) :: arg) - call get_command_argument(argnum, arg) - if (arg==flag) then - call get_command_argument(argnum+1, length=flag_value_length) - allocate(character(len=flag_value_length) :: flag_value) - call get_command_argument(argnum+1, flag_value) - return - end if - deallocate(arg) - end do - flag_value="" - end procedure - -end submodule 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_formats_s.F90 b/src/julienne/julienne_formats_s.F90 index fca17e91a..8d6b3e24e 100644 --- a/src/julienne/julienne_formats_s.F90 +++ b/src/julienne/julienne_formats_s.F90 @@ -3,6 +3,8 @@ submodule(julienne_formats_m) julienne_formats_s !! Construct separated-value formats + use julienne_multi_image_m, only : internal_error_stop + implicit none contains @@ -27,10 +29,10 @@ type is(character(len=*)) format_string = prefix // separator // suffix class default - error stop "format_s separated_values: unsupported type" + call internal_error_stop("format_s separated_values: unsupported type") end select rank default - error stop "formats_s separated_values: unsupported rank" + call internal_error_stop("formats_s separated_values: unsupported rank") end select end procedure diff --git a/src/julienne/julienne_stop_and_print_m.F90 b/src/julienne/julienne_stop_and_print_m.F90 index bf2142870..539fec4dc 100644 --- a/src/julienne/julienne_stop_and_print_m.F90 +++ b/src/julienne/julienne_stop_and_print_m.F90 @@ -1,21 +1,71 @@ ! Copyright (c), The Regents of the University of California and Sourcery Institute ! Terms of use are as specified in LICENSE.txt +#include "language-support.F90" + +#if HAVE_STOP_AND_PRINT_SUPPORT + module julienne_stop_and_print_m - !! Define a pure subroutine that terminates with an ERROR STOP message from a string_t + !! Define a pure subroutine that formats and prints various data types during error termination use julienne_string_m, only : string_t - use julienne_multi_image_m, only : internal_error_stop 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 -contains + abstract interface - pure subroutine stop_and_print(message) - implicit none - type(string_t), intent(in) :: message - call internal_error_stop(message%string()) - end subroutine - -end module + 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 + + pure module subroutine stop_and_print(data, header, footer) + implicit none + class(*), intent(in) :: data(..) + character(len=*), intent(in), optional :: header, footer + end subroutine + + 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 +#endif 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..ad46d45a7 --- /dev/null +++ b/src/julienne/julienne_stop_and_print_s.F90 @@ -0,0 +1,181 @@ +! Copyright (c), The Regents of the University of California and Sourcery Institute +! Terms of use are as specified in LICENSE.txt + +#include "language-support.F90" + +#if HAVE_STOP_AND_PRINT_SUPPORT + +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 + use julienne_multi_image_m, only : internal_error_stop + implicit none + +contains + + module procedure set_maxlen + self%maxlen_ = length + end procedure + + module procedure maxlen + length = self%maxlen_ + end procedure + + module procedure stop_and_print + + select rank(data) + rank(0) + select type(data) + type is(character(len=*)) + call internal_error_stop(data) + class is(string_t) +#ifndef __GFORTRAN__ + call internal_error_stop(data%string()) +#else + block + character(len=:), allocatable :: stop_code + stop_code = data%string() + call internal_error_stop(stop_code) + end block +#endif + class default + call stop_and_print_header_data_footer(data, header, footer) + end select + rank default + call stop_and_print_header_data_footer(data, header, footer) + end select + + contains + + pure subroutine stop_and_print_header_data_footer(data, header, footer) + character(len=*), intent(in), optional :: header, footer + class(*), intent(in) :: data(..) + character(len=:), allocatable :: code + + if (present(header)) then + if (present(footer)) then + code = new_line('') // header // new_line('') // character_stop_code(data) // new_line('') // footer // new_line('') + else + code = new_line('') // header // new_line('') // character_stop_code(data) + end if + else if (present(footer)) then + code = character_stop_code(data) // new_line('') // footer // new_line('') + else + code = new_line('') // character_stop_code(data) // new_line('') + end if + + call internal_error_stop(code) + end subroutine + + 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) call internal_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 + call internal_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 + call internal_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 + call internal_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 + call internal_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)) + call internal_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 + +#endif diff --git a/src/julienne/julienne_test_description_s.F90 b/src/julienne/julienne_test_description_s.F90 index 068823f3c..0803ff262 100644 --- a/src/julienne/julienne_test_description_s.F90 +++ b/src/julienne/julienne_test_description_s.F90 @@ -81,7 +81,7 @@ type(command_line_t) command_line #if defined(__flang__) - associate(search_string => command_line%flag_value("--contains")) + associate(search_string => command_line%character_flag_value("--contains")) filtered_test_descriptions = & pack( array = test_descriptions & ,mask = index(subject, search_string) /= 0 & ! subject contains search_string @@ -91,7 +91,7 @@ #else block character(len=:), allocatable :: search_string - search_string = command_line%flag_value("--contains") + search_string = command_line%character_flag_value("--contains") filtered_test_descriptions = & pack( array = test_descriptions & ,mask = index(subject, search_string) /= 0 & ! subject contains search_string diff --git a/src/julienne/julienne_test_diagnosis_s.F90 b/src/julienne/julienne_test_diagnosis_s.F90 index 0ca9bb5d7..514c2338e 100644 --- a/src/julienne/julienne_test_diagnosis_s.F90 +++ b/src/julienne/julienne_test_diagnosis_s.F90 @@ -7,6 +7,7 @@ submodule(julienne_test_diagnosis_m) julienne_test_diagnosis_s use assert_m use julienne_string_m, only : operator(.cat.) + use julienne_multi_image_m, only : internal_error_stop use iso_c_binding, only : c_associated, c_intptr_t implicit none contains @@ -89,7 +90,7 @@ diagnosis = aggregate_vector_diagnosis(reshape(diagnoses, shape=[size(diagnoses)])) rank default associate(diagnoses_rank => string_t(rank(diagnoses))) - error stop "aggregate_diagnosis (julienne_test_diagnosis_s): rank " // diagnoses_rank%string() // " unspported" + call internal_error_stop("aggregate_diagnosis (julienne_test_diagnosis_s): rank " // diagnoses_rank%string() // " unspported") end associate end select diff --git a/src/julienne/julienne_test_harness_s.F90 b/src/julienne/julienne_test_harness_s.F90 index 9519f9d2e..5e0c68223 100644 --- a/src/julienne/julienne_test_harness_s.F90 +++ b/src/julienne/julienne_test_harness_s.F90 @@ -69,7 +69,7 @@ subroutine print_usage_info_and_stop_if_requested associate(me => internal_this_image()) associate(command_line => command_line_t()) - if (command_line%argument_present([character(len=len("--help"))::"--help","-h"])) then + if (command_line%character_argument_present([character(len=len("--help"))::"--help","-h"])) then if (me==1) print '(a)', usage stop end if @@ -79,9 +79,9 @@ subroutine print_usage_info_and_stop_if_requested print '(a)', new_line("") // "Append '-- --help' or '-- -h' to your `fpm test` command to display usage information." #if (! defined(__GFORTRAN__)) && (! defined(NAGFOR)) - associate(search_string => command_line%flag_value("--contains")) + associate(search_string => command_line%character_flag_value("--contains")) #else - block; character(len=:), allocatable :: search_string; search_string = command_line%flag_value("--contains") + block; character(len=:), allocatable :: search_string; search_string = command_line%character_flag_value("--contains") #endif if (len(search_string)==0) then print '(a)', new_line('') // & diff --git a/src/julienne_m.F90 b/src/julienne_m.F90 index 1825a9343..0bb77a5ab 100644 --- a/src/julienne_m.F90 +++ b/src/julienne_m.F90 @@ -1,6 +1,8 @@ ! 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 julienne_m !! Global aggregation of all public entities use julienne_assert_m, only : call_julienne_assert_, julienne_assert @@ -9,7 +11,9 @@ 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 +#if HAVE_STOP_AND_PRINT_SUPPORT + use julienne_stop_and_print_m, only : stop_and_print, writable_t +#endif 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..c1800afec 100644 --- a/test/driver.F90 +++ b/test/driver.F90 @@ -1,6 +1,8 @@ ! 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" + program test_suite_driver !! Julienne test-suite driver @@ -10,6 +12,9 @@ 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 +#if HAVE_STOP_AND_PRINT_SUPPORT + use character_stop_code_test_m ,only : character_stop_code_test_t +#endif 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 +22,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 +31,9 @@ program test_suite_driver associate(test_harness => test_harness_t([ & test_fixture_t( assert_test_t()) & ,test_fixture_t( bin_test_t()) & +#if HAVE_STOP_AND_PRINT_SUPPORT + ,test_fixture_t( character_stop_code_test_t()) & +#endif ,test_fixture_t( formats_test_t()) & ,test_fixture_t( multi_image_test_t()) & ,test_fixture_t( string_test_t()) & diff --git a/test/idiomatic_assertion_failure_test.F90 b/test/idiomatic_assertion_failure_test.F90 index 0c982dade..9556a4b7b 100644 --- a/test/idiomatic_assertion_failure_test.F90 +++ b/test/idiomatic_assertion_failure_test.F90 @@ -14,7 +14,7 @@ program idiomatic_assertion_failure_test #else associate(command_line => command_line_t(), me => 1) #endif - if (.not. command_line%argument_present([character(len=len("--help"))::"--help","-h"])) then + if (.not. command_line%character_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_julienne_assert(1 .equalsExpected. 2) diff --git a/test/logical_assertion_failure_test.F90 b/test/logical_assertion_failure_test.F90 index 113a87edb..03a5f7930 100644 --- a/test/logical_assertion_failure_test.F90 +++ b/test/logical_assertion_failure_test.F90 @@ -15,7 +15,7 @@ program logical_assertion_failure_test #else associate(command_line => command_line_t(), me => 1) #endif - if (.not. command_line%argument_present([character(len=len("--help"))::"--help","-h"])) then + if (.not. command_line%character_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 a logical assertion: ' // new_line('') 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..c05c92194 --- /dev/null +++ b/test/modules/character_stop_code_test_m.F90 @@ -0,0 +1,562 @@ +! 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" + +#if HAVE_STOP_AND_PRINT_SUPPORT + +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 = [ & +#if __GFORTRAN__ + ! Skip some tests + 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)) & + ,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") ) & +#else + ! Run all tests + 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)) & + ,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)) & +#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() + +#ifndef __GFORTRAN__ + 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 +#else + block + type(string_t) expected_stop_code , stop_code + type(string_t), allocatable :: messages(:) + messages = string_t(["foo","bar"]) + expected_stop_code = .csv. messages + stop_code = character_stop_code(messages) + test_diagnosis = test_diagnosis .also. (stop_code .equalsExpected. expected_stop_code) + end block +#endif + 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 + +#endif diff --git a/test/modules/command_line_test_m.F90 b/test/modules/command_line_test_m.F90 index a0b2e2923..418b61211 100644 --- a/test/modules/command_line_test_m.F90 +++ b/test/modules/command_line_test_m.F90 @@ -49,7 +49,7 @@ function results() result(test_results) associate(me => 1) #endif - if (.not. command_line%argument_present(["--test"])) then ! skip the tests if not explicitly requested + if (.not. command_line%character_argument_present(["--test"])) then ! skip the tests if not explicitly requested test_descriptions = [ & test_description_t(string_t("flag_value() result is the value passed after a command-line flag")) & ,test_description_t(string_t("flag_value() result is an empty string if command-line flag value is missing")) & @@ -81,31 +81,31 @@ function results() result(test_results) function check_flag_value() result(test_diagnosis) type(test_diagnosis_t) test_diagnosis type(command_line_t) command_line - test_diagnosis = command_line%flag_value("--test") .equalsExpected. "command_line_t" + test_diagnosis = command_line%character_flag_value("--test") .equalsExpected. "command_line_t" end function function check_flag_value_missing() result(test_diagnosis) type(test_diagnosis_t) test_diagnosis type(command_line_t) command_line - test_diagnosis = command_line%flag_value("--type") .equalsExpected. "" + test_diagnosis = command_line%character_flag_value("--type") .equalsExpected. "" end function function check_flag_missing() result(test_diagnosis) type(test_diagnosis_t) test_diagnosis type(command_line_t) command_line - test_diagnosis = command_line%flag_value("r@nd0m.Junk-H3R3") .equalsExpected. "" + test_diagnosis = command_line%character_flag_value("r@nd0m.Junk-H3R3") .equalsExpected. "" end function function check_argument_missing() result(test_diagnosis) type(test_diagnosis_t) test_diagnosis type(command_line_t) command_line - test_diagnosis = .expect. (.not. command_line%argument_present(["M1ss1ng-argUment"])) + test_diagnosis = .expect. (.not. command_line%character_argument_present(["M1ss1ng-argUment"])) end function function check_argument_present() result(test_diagnosis) type(test_diagnosis_t) test_diagnosis type(command_line_t) command_line - test_diagnosis = .expect. command_line%argument_present(["--type"]) + test_diagnosis = .expect. command_line%character_argument_present(["--type"]) end function end module command_line_test_m diff --git a/test/test_stop_and_print.F90 b/test/test_stop_and_print.F90 index 81a196643..33c43c672 100644 --- a/test/test_stop_and_print.F90 +++ b/test/test_stop_and_print.F90 @@ -5,6 +5,7 @@ #include "language-support.F90" program stop_and_print_in_pure_procedure +#if HAVE_STOP_AND_PRINT_SUPPORT !! 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 @@ -14,9 +15,9 @@ program stop_and_print_in_pure_procedure #else associate(command_line => command_line_t(), me => 1) #endif - if (.not. command_line%argument_present([character(len=len("--help"))::"--help","-h"])) then + if (.not. command_line%character_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 stop_and_print in a pure procedure: ' // new_line('') + if (me==1) print '(a)', new_line('') // 'Test the intentional failure of an idiomatic stop_and_print: ' // new_line('') call pure_subroutine #else if (me==1) print '(a)', & @@ -35,4 +36,7 @@ pure subroutine pure_subroutine call stop_and_print("array = " // .csv. string_t(array)) end subroutine +#else + stop "SKIPPED: Julienne's stop_and_print feature is not supported on this compiler" +#endif end program diff --git a/test/unit_test_failure_test.F90 b/test/unit_test_failure_test.F90 index 87536b17f..407266790 100644 --- a/test/unit_test_failure_test.F90 +++ b/test/unit_test_failure_test.F90 @@ -7,7 +7,7 @@ program unit_test_failure_test # if TEST_INTENTIONAL_FAILURE associate(command_line => command_line_t()) - if (.not. command_line%argument_present([character(len=len("--help"))::"--help","-h"])) then + if (.not. command_line%character_argument_present([character(len=len("--help"))::"--help","-h"])) then associate(test_harness => test_harness_t([test_fixture_t(test_test_t())])) call test_harness%report_results print *, "If this message appears, the test did not fail as intended." @@ -20,7 +20,7 @@ program unit_test_failure_test # else associate(command_line => command_line_t(), me => 1) # endif - if (.not. command_line%argument_present([character(len=len("--help"))::"--help","-h"])) then + if (.not. command_line%character_argument_present([character(len=len("--help"))::"--help","-h"])) then if (me==1) print '(a)', & new_line('') // & 'Skipping the test in ' // __FILE__ // '.' // new_line('') // &