From e103cc3c8028e560418af93c932af2f67b7aeb05 Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Fri, 6 Jun 2025 14:31:24 +0100 Subject: [PATCH 01/26] WIP: Add episode 2 challenge 1 --- .../challenge-1/README.md | 8 + .../challenge-1/src/evolve_board | Bin 0 -> 52208 bytes .../challenge-1/src/main.f90 | 180 ++++++++++++++++++ .../challenge-1/src/models/model-1.dat | 34 ++++ .../challenge-1/src/models/model-2.dat | 34 ++++ .../challenge-1/src/models/model-3.dat | 34 ++++ .../challenge-1/src/models/model-4.dat | 34 ++++ 7 files changed, 324 insertions(+) create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/README.md create mode 100755 episodes/2-intro-to-fortran-unit-tests/challenge-1/src/evolve_board create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/src/main.f90 create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-1.dat create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-2.dat create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-3.dat create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-4.dat diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/README.md b/episodes/2-intro-to-fortran-unit-tests/challenge-1/README.md new file mode 100644 index 0000000..6795216 --- /dev/null +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/README.md @@ -0,0 +1,8 @@ +# Episode 2 - Challenge 1: Identify bad practice for unit testing Fortran + +Take a look at the [src](./src) code provided. + +1. Can you identify the aspects of this Fortran code which make it difficult to unit test? +2. Try to improve the src to make it more unit testable? + +A solution is provided in [src/sol](./src/sol). diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/evolve_board b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/evolve_board new file mode 100755 index 0000000000000000000000000000000000000000..1cc47f76f0e459ee1af3d8e49e48248d8b9d41bf GIT binary patch literal 52208 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 h" + deallocate(a) + stop + end if + + ! Open input file + open(unit=input_file_io, & + file=input_fname, & + status='old', & + IOSTAT=iostat) + + if( iostat /= 0) then + write(*,'(a)') ' *** Error when opening '//input_fname + stop 1 + end if + + ! Read in board from file + read(input_file_io,'(a)') text ! Skip first line + read(input_file_io,*) nrow, ncol + + if (num_generations < 1 .or. num_generations > max_generations) then + write (*,'(a,i6,a,i6)') "num_generations must be a positive integer less than ", max_generations, " found ", num_generations + stop 1 + end if + + if (nrow < 1 .or. nrow > max_nrow) then + write (*,'(a,i6,a,i6)') "nrow must be a positive integer less than ", max_nrow, " found ", nrow + stop 1 + end if + + if (ncol < 1 .or. ncol > max_ncol) then + write (*,'(a,i6,a,i6)') "ncol must be a positive integer less than ", max_ncol, " found ", ncol + stop 1 + end if + + allocate(board(nrow, ncol)) + allocate(temp_board(nrow, ncol)) + + read(input_file_io,'(a)') text ! Skip next line + ! Populate the boards starting state + do i = 1, nrow + read(input_file_io,*) board(i, :) + end do + + temp_board = 0 + + call system ("clear") + + do while(.not. steady_state) + call date_and_time(VALUES=values) + mod_val = mod(values(8), speed) + + if (mod_val == 0) then + call evolve_board() + call check_for_steady_state() + board = temp_board + call draw_board() + + generation_number = generation_number + 1 + end if + + end do + + write(*,'(a,i6,a)') "Reached steady after ", generation_number, " generations" + + deallocate(board) + deallocate(temp_board) + +contains + + subroutine check_for_steady_state() + logical :: equal + integer :: i, j + + equal = .true. + + do i=1, nrow + do j=1, ncol + ! write(*,*) equal, board(i, j), prev_board(i, j) + equal = board(i, j) == temp_board(i, j) + if (.not. equal) then + steady_state_counter = 0 + return + end if + end do + end do + if (steady_state_counter == 0) then + steady_state_generation = generation_number + end if + + steady_state_counter = steady_state_counter + 1 + + if (steady_state_counter > 1) then + steady_state = .true. + return + end if + steady_state = .false. + end subroutine check_for_steady_state + + subroutine evolve_board() + integer :: i, j + do i=2, nrow-1 + do j=2, ncol-1 + sum = 0 + sum = board(i-1, j-1) + board(i, j-1) + board(i+1, j-1) ! + sum = sum + board(i-1, j) + board(i+1, j) + sum = sum + board(i-1, j+1) + board(i, j+1) + board(i+1, j+1) + if(board(i,j)==1 .and. sum<=1) then + temp_board(i,j) = 0 + elseif(board(i,j)==1 .and. sum<=3) then + temp_board(i,j) = 1 + elseif(board(i,j)==1 .and. sum>=4)then + temp_board(i,j) = 0 + elseif(board(i,j)==0 .and. sum==3)then + temp_board(i,j) = 1 + endif + enddo + enddo + + return + end subroutine evolve_board + + subroutine draw_board() + integer :: i, j + character(nrow) :: output + call system("clear") + do i=1, nrow + output = "" + do j=1, ncol + if (board(i,j) == 1) then + output = trim(output)//"#" + else + output = trim(output)//"." + endif + enddo + print *, output + enddo + end subroutine draw_board + +end program game_of_life diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-1.dat b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-1.dat new file mode 100644 index 0000000..ed1129c --- /dev/null +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-1.dat @@ -0,0 +1,34 @@ +nrow ncol + 31 31 +Board + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-2.dat b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-2.dat new file mode 100644 index 0000000..bcefe2b --- /dev/null +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-2.dat @@ -0,0 +1,34 @@ +nrow ncol + 31 31 +Board + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-3.dat b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-3.dat new file mode 100644 index 0000000..0aa62f2 --- /dev/null +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-3.dat @@ -0,0 +1,34 @@ +nrow ncol + 31 31 +Board + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-4.dat b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-4.dat new file mode 100644 index 0000000..9317fe3 --- /dev/null +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-4.dat @@ -0,0 +1,34 @@ +nrow ncol + 31 31 +Board + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 From 95ce18ef9aae8e19258a83f0b4578e174c462055 Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Mon, 9 Jun 2025 11:09:30 +0100 Subject: [PATCH 02/26] Remove unused valiables and executable --- .../challenge-1/src/evolve_board | Bin 52208 -> 0 bytes .../challenge-1/src/main.f90 | 31 ++++++++++-------- 2 files changed, 17 insertions(+), 14 deletions(-) delete mode 100755 episodes/2-intro-to-fortran-unit-tests/challenge-1/src/evolve_board diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/evolve_board b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/evolve_board deleted file mode 100755 index 1cc47f76f0e459ee1af3d8e49e48248d8b9d41bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52208 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 h max_generations) then - write (*,'(a,i6,a,i6)') "num_generations must be a positive integer less than ", max_generations, " found ", num_generations - stop 1 - end if - + ! Verify the date_time_values read from the file if (nrow < 1 .or. nrow > max_nrow) then write (*,'(a,i6,a,i6)') "nrow must be a positive integer less than ", max_nrow, " found ", nrow stop 1 @@ -84,13 +80,16 @@ program game_of_life temp_board = 0 + ! Clear the terminal screen call system ("clear") - do while(.not. steady_state) - call date_and_time(VALUES=values) - mod_val = mod(values(8), speed) + ! Iterate until we reach a steady state + do while(.not. steady_state .and. generation_number < max_generations) + ! Advance the simulation in the steps of the requested number of milliosecons + call date_and_time(VALUES=date_time_values) + mod_ms_step = mod(date_time_values(8), ms_per_step) - if (mod_val == 0) then + if (mod_ms_step == 0) then call evolve_board() call check_for_steady_state() board = temp_board @@ -101,7 +100,11 @@ program game_of_life end do - write(*,'(a,i6,a)') "Reached steady after ", generation_number, " generations" + if (steady_state) then + write(*,'(a,i6,a)') "Reached steady after ", generation_number, " generations" + else + write(*,'(a,i6,a)') "Did NOT Reach steady after ", generation_number, " generations" + end if deallocate(board) deallocate(temp_board) From 646c99ab5157f753bc2f7b672faedd810bb7f18a Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Mon, 9 Jun 2025 11:12:31 +0100 Subject: [PATCH 03/26] Add fpm.toml and move models out of src --- episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml | 6 ++++++ .../challenge-1/{src => }/models/model-1.dat | 0 .../challenge-1/{src => }/models/model-2.dat | 0 .../challenge-1/{src => }/models/model-3.dat | 0 .../challenge-1/{src => }/models/model-4.dat | 0 5 files changed, 6 insertions(+) create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml rename episodes/2-intro-to-fortran-unit-tests/challenge-1/{src => }/models/model-1.dat (100%) rename episodes/2-intro-to-fortran-unit-tests/challenge-1/{src => }/models/model-2.dat (100%) rename episodes/2-intro-to-fortran-unit-tests/challenge-1/{src => }/models/model-3.dat (100%) rename episodes/2-intro-to-fortran-unit-tests/challenge-1/{src => }/models/model-4.dat (100%) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml b/episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml new file mode 100644 index 0000000..b864bc9 --- /dev/null +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml @@ -0,0 +1,6 @@ +name = "episode-2-challenge-1" + +[[executable]] +name = "game-of-life" +source-dir = "src" +main = "main.f90" \ No newline at end of file diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-1.dat b/episodes/2-intro-to-fortran-unit-tests/challenge-1/models/model-1.dat similarity index 100% rename from episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-1.dat rename to episodes/2-intro-to-fortran-unit-tests/challenge-1/models/model-1.dat diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-2.dat b/episodes/2-intro-to-fortran-unit-tests/challenge-1/models/model-2.dat similarity index 100% rename from episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-2.dat rename to episodes/2-intro-to-fortran-unit-tests/challenge-1/models/model-2.dat diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-3.dat b/episodes/2-intro-to-fortran-unit-tests/challenge-1/models/model-3.dat similarity index 100% rename from episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-3.dat rename to episodes/2-intro-to-fortran-unit-tests/challenge-1/models/model-3.dat diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-4.dat b/episodes/2-intro-to-fortran-unit-tests/challenge-1/models/model-4.dat similarity index 100% rename from episodes/2-intro-to-fortran-unit-tests/challenge-1/src/models/model-4.dat rename to episodes/2-intro-to-fortran-unit-tests/challenge-1/models/model-4.dat From e28a2020249be2416cbb554a4b8a1133ff7a3f0a Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Mon, 9 Jun 2025 11:53:06 +0100 Subject: [PATCH 04/26] Cleanup main src and rename file --- .../challenge-1/fpm.toml | 2 +- .../src/{main.f90 => game_of_life.f90} | 137 +++++++++--------- 2 files changed, 66 insertions(+), 73 deletions(-) rename episodes/2-intro-to-fortran-unit-tests/challenge-1/src/{main.f90 => game_of_life.f90} (54%) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml b/episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml index b864bc9..9d892f5 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml @@ -3,4 +3,4 @@ name = "episode-2-challenge-1" [[executable]] name = "game-of-life" source-dir = "src" -main = "main.f90" \ No newline at end of file +main = "game_of_life.f90" diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/main.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/game_of_life.f90 similarity index 54% rename from episodes/2-intro-to-fortran-unit-tests/challenge-1/src/main.f90 rename to episodes/2-intro-to-fortran-unit-tests/challenge-1/src/game_of_life.f90 index b14c5f1..e473e29 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/main.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/game_of_life.f90 @@ -11,24 +11,24 @@ program game_of_life !! Board args integer, parameter :: max_nrow = 100, max_ncol = 100, max_generations = 100 integer :: nrow, ncol - integer :: i, generation_number, sum, steady_state_generation - integer, dimension(:,:), allocatable :: board, temp_board + integer :: row, generation_number + integer, dimension(:,:), allocatable :: current_board, new_board !! Animation args integer, dimension(8) :: date_time_values - integer :: mod_ms_step, ms_per_step = 250, steady_state_counter = 0 + integer :: mod_ms_step, ms_per_step = 250 logical :: steady_state = .false. !! CLI args integer :: argl - character(len=:), allocatable :: a, input_fname + character(len=:), allocatable :: cli_arg_temp_store, input_fname !! File IO args - character(len=80) :: text + character(len=80) :: text_to_discard integer :: input_file_io integer :: iostat - ! Get board file path from command line + ! Get current_board file path from command line if (command_argument_count() == 1) then call get_command_argument(1, length=argl) allocate(character(argl) :: input_fname) @@ -36,10 +36,10 @@ program game_of_life else write(*,'(A)') "Error: Invalid input" call get_command_argument(0, length=argl) - allocate(character(argl) :: a) - call get_command_argument(0, a) - write(*,'(A,A,A)') "Usage: ", a, " " - deallocate(a) + allocate(character(argl) :: cli_arg_temp_store) + call get_command_argument(0, cli_arg_temp_store) + write(*,'(A,A,A)') "Usage: ", cli_arg_temp_store, " " + deallocate(cli_arg_temp_store) stop end if @@ -54,45 +54,49 @@ program game_of_life stop 1 end if - ! Read in board from file - read(input_file_io,'(a)') text ! Skip first line + ! Read in current_board from file + read(input_file_io,'(a)') text_to_discard ! Skip first line read(input_file_io,*) nrow, ncol ! Verify the date_time_values read from the file if (nrow < 1 .or. nrow > max_nrow) then - write (*,'(a,i6,a,i6)') "nrow must be a positive integer less than ", max_nrow, " found ", nrow + write (*,'(a,i6,a,i6)') & + "nrow must be cli_arg_temp_store positive integer less than ", max_nrow, " found ", nrow stop 1 end if if (ncol < 1 .or. ncol > max_ncol) then - write (*,'(a,i6,a,i6)') "ncol must be a positive integer less than ", max_ncol, " found ", ncol + write (*,'(a,i6,a,i6)') & + "ncol must be cli_arg_temp_store positive integer less than ", max_ncol, " found ", ncol stop 1 end if - allocate(board(nrow, ncol)) - allocate(temp_board(nrow, ncol)) + allocate(current_board(nrow, ncol)) + allocate(new_board(nrow, ncol)) - read(input_file_io,'(a)') text ! Skip next line + read(input_file_io,'(a)') text_to_discard ! Skip next line ! Populate the boards starting state - do i = 1, nrow - read(input_file_io,*) board(i, :) + do row = 1, nrow + read(input_file_io,*) current_board(row, :) end do - temp_board = 0 + close(input_file_io) + + new_board = 0 ! Clear the terminal screen call system ("clear") - ! Iterate until we reach a steady state + ! Iterate until we reach cli_arg_temp_store steady state do while(.not. steady_state .and. generation_number < max_generations) ! Advance the simulation in the steps of the requested number of milliosecons call date_and_time(VALUES=date_time_values) mod_ms_step = mod(date_time_values(8), ms_per_step) if (mod_ms_step == 0) then - call evolve_board() call check_for_steady_state() - board = temp_board + call evolve_board() + current_board = new_board call draw_board() generation_number = generation_number + 1 @@ -106,56 +110,28 @@ program game_of_life write(*,'(a,i6,a)') "Did NOT Reach steady after ", generation_number, " generations" end if - deallocate(board) - deallocate(temp_board) + deallocate(current_board) + deallocate(new_board) contains - subroutine check_for_steady_state() - logical :: equal - integer :: i, j - - equal = .true. - - do i=1, nrow - do j=1, ncol - ! write(*,*) equal, board(i, j), prev_board(i, j) - equal = board(i, j) == temp_board(i, j) - if (.not. equal) then - steady_state_counter = 0 - return - end if - end do - end do - if (steady_state_counter == 0) then - steady_state_generation = generation_number - end if - - steady_state_counter = steady_state_counter + 1 - - if (steady_state_counter > 1) then - steady_state = .true. - return - end if - steady_state = .false. - end subroutine check_for_steady_state - subroutine evolve_board() - integer :: i, j - do i=2, nrow-1 - do j=2, ncol-1 + integer :: row, col, sum + + do row=2, nrow-1 + do col=2, ncol-1 sum = 0 - sum = board(i-1, j-1) + board(i, j-1) + board(i+1, j-1) ! - sum = sum + board(i-1, j) + board(i+1, j) - sum = sum + board(i-1, j+1) + board(i, j+1) + board(i+1, j+1) - if(board(i,j)==1 .and. sum<=1) then - temp_board(i,j) = 0 - elseif(board(i,j)==1 .and. sum<=3) then - temp_board(i,j) = 1 - elseif(board(i,j)==1 .and. sum>=4)then - temp_board(i,j) = 0 - elseif(board(i,j)==0 .and. sum==3)then - temp_board(i,j) = 1 + sum = current_board(row-1, col-1) + current_board(row, col-1) + current_board(row+1, col-1) ! + sum = sum + current_board(row-1, col) + current_board(row+1, col) + sum = sum + current_board(row-1, col+1) + current_board(row, col+1) + current_board(row+1, col+1) + if(current_board(row,col)==1 .and. sum<=1) then + new_board(row,col) = 0 + elseif(current_board(row,col)==1 .and. sum<=3) then + new_board(row,col) = 1 + elseif(current_board(row,col)==1 .and. sum>=4)then + new_board(row,col) = 0 + elseif(current_board(row,col)==0 .and. sum==3)then + new_board(row,col) = 1 endif enddo enddo @@ -163,14 +139,31 @@ subroutine evolve_board() return end subroutine evolve_board + subroutine check_for_steady_state() + integer :: row, col + + do row=1, nrow + do col=1, ncol + if (.not. current_board(row, col) == new_board(row, col)) then + steady_state = .false. + return + end if + end do + end do + steady_state = .true. + end subroutine check_for_steady_state + subroutine draw_board() - integer :: i, j + integer :: row, col character(nrow) :: output + + ! Clear the terminal screen call system("clear") - do i=1, nrow + + do row=1, nrow output = "" - do j=1, ncol - if (board(i,j) == 1) then + do col=1, ncol + if (current_board(row,col) == 1) then output = trim(output)//"#" else output = trim(output)//"." From c2aafddb5ca4fd506adcf250e4934a20765026a2 Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Mon, 9 Jun 2025 12:02:20 +0100 Subject: [PATCH 05/26] Add docstrings --- .../challenge-1/src/game_of_life.f90 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/game_of_life.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/game_of_life.f90 index e473e29..f4815d2 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/game_of_life.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/game_of_life.f90 @@ -115,6 +115,7 @@ program game_of_life contains + !> Evolve the board into the state od the next iteration subroutine evolve_board() integer :: row, col, sum @@ -139,6 +140,7 @@ subroutine evolve_board() return end subroutine evolve_board + !> Check if we have reached steady state, i.e. current and new board match subroutine check_for_steady_state() integer :: row, col @@ -153,6 +155,7 @@ subroutine check_for_steady_state() steady_state = .true. end subroutine check_for_steady_state + !> Output the current board to the terminal subroutine draw_board() integer :: row, col character(nrow) :: output From bd8b0f353d2be60a3c3bec291035ee65b046063d Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Mon, 9 Jun 2025 12:04:01 +0100 Subject: [PATCH 06/26] Fix mistake find and replace --- .../challenge-1/src/game_of_life.f90 | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/game_of_life.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/game_of_life.f90 index f4815d2..baa49f9 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/game_of_life.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/game_of_life.f90 @@ -60,14 +60,12 @@ program game_of_life ! Verify the date_time_values read from the file if (nrow < 1 .or. nrow > max_nrow) then - write (*,'(a,i6,a,i6)') & - "nrow must be cli_arg_temp_store positive integer less than ", max_nrow, " found ", nrow + write (*,'(a,i6,a,i6)') "nrow must be a positive integer less than ", max_nrow, " found ", nrow stop 1 end if if (ncol < 1 .or. ncol > max_ncol) then - write (*,'(a,i6,a,i6)') & - "ncol must be cli_arg_temp_store positive integer less than ", max_ncol, " found ", ncol + write (*,'(a,i6,a,i6)') "ncol must be a positive integer less than ", max_ncol, " found ", ncol stop 1 end if @@ -87,7 +85,7 @@ program game_of_life ! Clear the terminal screen call system ("clear") - ! Iterate until we reach cli_arg_temp_store steady state + ! Iterate until we reach a steady state do while(.not. steady_state .and. generation_number < max_generations) ! Advance the simulation in the steps of the requested number of milliosecons call date_and_time(VALUES=date_time_values) From 9d9c90aa3f6951bb62c8101efed83423e148d4cc Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Mon, 9 Jun 2025 13:48:48 +0100 Subject: [PATCH 07/26] Make sum in evolve clearer --- .../challenge-1/src/game_of_life.f90 | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/game_of_life.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/game_of_life.f90 index baa49f9..8392365 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/game_of_life.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/game_of_life.f90 @@ -120,9 +120,14 @@ subroutine evolve_board() do row=2, nrow-1 do col=2, ncol-1 sum = 0 - sum = current_board(row-1, col-1) + current_board(row, col-1) + current_board(row+1, col-1) ! - sum = sum + current_board(row-1, col) + current_board(row+1, col) - sum = sum + current_board(row-1, col+1) + current_board(row, col+1) + current_board(row+1, col+1) + sum = current_board(row, col-1) & + + current_board(row+1, col-1) & + + current_board(row+1, col) & + + current_board(row+1, col+1) & + + current_board(row, col+1) & + + current_board(row-1, col+1) & + + current_board(row-1, col) & + + current_board(row-1, col-1) if(current_board(row,col)==1 .and. sum<=1) then new_board(row,col) = 0 elseif(current_board(row,col)==1 .and. sum<=3) then From c46ce4215c8b152bebaab881809fce06e4aee220 Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Mon, 9 Jun 2025 13:53:20 +0100 Subject: [PATCH 08/26] Add solution and first pass at tests --- .../challenge-1/fpm.toml | 12 +- .../challenge-1/src/sol/game_of_life.f90 | 115 ++++++++++++++++++ .../challenge-1/src/sol/game_of_life_mod.f90 | 106 ++++++++++++++++ .../test/sol/game_of_life_sol_test.f90 | 106 ++++++++++++++++ .../challenge-1/test/sol/main.f90 | 23 ++++ 5 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/src/sol/game_of_life.f90 create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/src/sol/game_of_life_mod.f90 create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/main.f90 diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml b/episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml index 9d892f5..be70212 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml @@ -1,6 +1,16 @@ name = "episode-2-challenge-1" +[dev-dependencies] +veggies.git = "https://gitlab.com/everythingfunctional/veggies" +veggies.tag = "main" + [[executable]] name = "game-of-life" -source-dir = "src" +source-dir = "src" # Update this to src/sol to build the solution main = "game_of_life.f90" + +# Uncamment the below to enable the solution tests +# [[test]] +# name = "game_of_life_sol_test" +# source-dir = "test/sol" +# main = "main.f90" diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/sol/game_of_life.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/sol/game_of_life.f90 new file mode 100644 index 0000000..1353efe --- /dev/null +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/sol/game_of_life.f90 @@ -0,0 +1,115 @@ +! ======================================================= +! Conway's game of life +! +! ======================================================= +! Adapted from https://github.com/tuckerrc/game_of_life +! ======================================================= +program game_of_life + use game_of_life_mod, only : evolve_board, check_for_steady_state, draw_board + + implicit none + + !! Board args + integer, parameter :: max_nrow = 100, max_ncol = 100, max_generations = 100 + integer :: nrow, ncol + integer :: row, generation_number + integer, dimension(:,:), allocatable :: current_board, new_board + + !! Animation args + integer, dimension(8) :: date_time_values + integer :: mod_ms_step, ms_per_step = 250 + logical :: steady_state = .false. + + !! CLI args + integer :: argl + character(len=:), allocatable :: cli_arg_temp_store, input_fname + + !! File IO args + character(len=80) :: text_to_discard + integer :: input_file_io + integer :: iostat + + ! Get current_board file path from command line + if (command_argument_count() == 1) then + call get_command_argument(1, length=argl) + allocate(character(argl) :: input_fname) + call get_command_argument(1, input_fname) + else + write(*,'(A)') "Error: Invalid input" + call get_command_argument(0, length=argl) + allocate(character(argl) :: cli_arg_temp_store) + call get_command_argument(0, cli_arg_temp_store) + write(*,'(A,A,A)') "Usage: ", cli_arg_temp_store, " " + deallocate(cli_arg_temp_store) + stop + end if + + ! Open input file + open(unit=input_file_io, & + file=input_fname, & + status='old', & + IOSTAT=iostat) + + if( iostat /= 0) then + write(*,'(a)') ' *** Error when opening '//input_fname + stop 1 + end if + + ! Read in current_board from file + read(input_file_io,'(a)') text_to_discard ! Skip first line + read(input_file_io,*) nrow, ncol + + ! Verify the date_time_values read from the file + if (nrow < 1 .or. nrow > max_nrow) then + write (*,'(a,i6,a,i6)') "nrow must be a positive integer less than ", max_nrow, " found ", nrow + stop 1 + end if + + if (ncol < 1 .or. ncol > max_ncol) then + write (*,'(a,i6,a,i6)') "ncol must be a positive integer less than ", max_ncol, " found ", ncol + stop 1 + end if + + allocate(current_board(nrow, ncol)) + allocate(new_board(nrow, ncol)) + + read(input_file_io,'(a)') text_to_discard ! Skip next line + ! Populate the boards starting state + do row = 1, nrow + read(input_file_io,*) current_board(row, :) + end do + + close(input_file_io) + + new_board = 0 + + ! Clear the terminal screen + call system ("clear") + + ! Iterate until we reach a steady state + do while(.not. steady_state .and. generation_number < max_generations) + ! Advance the simulation in the steps of the requested number of milliosecons + call date_and_time(VALUES=date_time_values) + mod_ms_step = mod(date_time_values(8), ms_per_step) + + if (mod_ms_step == 0) then + call evolve_board(current_board, new_board) + call check_for_steady_state(current_board, new_board, steady_state) + current_board = new_board + call draw_board(current_board) + + generation_number = generation_number + 1 + end if + + end do + + if (steady_state) then + write(*,'(a,i6,a)') "Reached steady after ", generation_number, " generations" + else + write(*,'(a,i6,a)') "Did NOT Reach steady after ", generation_number, " generations" + end if + + deallocate(current_board) + deallocate(new_board) + +end program game_of_life diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/sol/game_of_life_mod.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/sol/game_of_life_mod.f90 new file mode 100644 index 0000000..fcf7102 --- /dev/null +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/sol/game_of_life_mod.f90 @@ -0,0 +1,106 @@ +! ======================================================= +! Conway's game of life +! +! ======================================================= +! Adapted from https://github.com/tuckerrc/game_of_life +! ======================================================= +module game_of_life_mod + implicit none + + public + +contains + + !> Evolve the board into the state od the next iteration + subroutine evolve_board(current_board, new_board) + !> The board as it currently is before this iteration + integer, dimension(:,:), allocatable, intent(in) :: current_board + !> The board into which the new state will be stored after this iteration + integer, dimension(:,:), allocatable, intent(inout) :: new_board + + integer :: row, col, num_rows, num_cols, sum + + num_rows = size(current_board, 1) + num_cols = size(current_board, 2) + + do row=2, num_rows-1 + do col=2, num_cols-1 + sum = 0 + sum = current_board(row, col-1) & + + current_board(row+1, col-1) & + + current_board(row+1, col) & + + current_board(row+1, col+1) & + + current_board(row, col+1) & + + current_board(row-1, col+1) & + + current_board(row-1, col) & + + current_board(row-1, col-1) + if(current_board(row,col)==1 .and. sum<=1) then + new_board(row,col) = 0 + elseif(current_board(row,col)==1 .and. sum<=3) then + new_board(row,col) = 1 + elseif(current_board(row,col)==1 .and. sum>=4)then + new_board(row,col) = 0 + elseif(current_board(row,col)==0 .and. sum==3)then + new_board(row,col) = 1 + endif + enddo + enddo + end subroutine evolve_board + + !> Check if we have reached steady state, i.e. current and new board match + subroutine check_for_steady_state(current_board, new_board, steady_state) + !> The board as it currently is before this iteration + integer, dimension(:,:), allocatable, intent(in) :: current_board + !> The board into which the new state has been stored after this iteration + integer, dimension(:,:), allocatable, intent(in) :: new_board + !> Logical to indicate whether current and new board match + logical, intent(out) :: steady_state + + integer :: row, col, num_rows, num_cols + + num_rows = size(current_board, 1) + num_cols = size(current_board, 2) + + do row=1, num_rows + do col=1, num_cols + if (.not. current_board(row, col) == new_board(row, col)) then + steady_state = .false. + return + end if + end do + end do + + steady_state = .true. + end subroutine check_for_steady_state + + !> Output the current board to the terminal + subroutine draw_board(current_board) + !> The board as it currently is for this iteration + integer, dimension(:,:), allocatable, intent(in) :: current_board + + integer :: row, col, num_rows, num_cols + character(len=:), allocatable :: output + + call system("clear") + + num_rows = size(current_board, 1) + num_cols = size(current_board, 2) + + allocate(character(num_rows) :: output) + + do row=1, num_rows + output = "" + do col=1, num_cols + if (current_board(row,col) == 1) then + output = trim(output)//"#" + else + output = trim(output)//"." + endif + enddo + print *, output + enddo + + deallocate(output) + end subroutine draw_board + +end module game_of_life_mod diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 new file mode 100644 index 0000000..2b4efde --- /dev/null +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 @@ -0,0 +1,106 @@ +module game_of_life_sol_test + use game_of_life_mod, only : check_for_steady_state + use veggies, only: & + assert_that, & + fail, & + given, & + input_t, & + result_t, & + test_item_t, & + then__, & + transformation_failure_t, & + transformed_t, & + when + implicit none + + private + public :: game_of_life_sol_test_suite + + !> Type to bundle the inputs of game_of_life_sol::check_for_steady_state + type, extends(input_t) :: input_boards_t + integer, dimension(:,:), allocatable :: current_board, new_board + end type input_boards_t + + !> Type to bundle the outputs of game_of_life_sol::check_for_steady_state + type, extends(input_t) :: check_for_steady_state_output_t + logical :: steady_state + end type check_for_steady_state_output_t + +contains + + function game_of_life_sol_test_suite() result(tests) + type(test_item_t) :: tests + + tests = given( & + "we have populated a matching board and temp_board", & + setup_matching_boards(), & + [ when( & + "we check for steady_state", & + run_steady_sate_check, & + [ then__("steady_state will be reached", check_is_steady_sate) & + ]) & + ]) + end function game_of_life_sol_test_suite + + function setup_matching_boards() result(input_boards) + type(input_boards_t) :: input_boards + + integer :: nrow, ncol, row, col, num_ones, rand_row, rand_col + real :: rand_real + + nrow = 31 + ncol = 31 + + ! Initialise to all zeros + allocate(input_boards%current_board(nrow, ncol)) + allocate(input_boards%new_board(nrow, ncol)) + input_boards%current_board = 0 + input_boards%new_board = 0 + + ! For both boards, set one or more elements to 1 + call random_number(rand_real) + num_ones = 1 + FLOOR(nrow*ncol*rand_real) ! n=1 to n=nrow*ncol + do row = 1, num_ones + ! Get random coordinates for 1 + call random_number(rand_real) + rand_row = 1 + FLOOR(nrow*rand_real) ! n=1 to n=nrow + call random_number(rand_real) + rand_col = 1 + FLOOR(ncol*rand_real) ! n=1 to n=ncol + + input_boards%current_board(rand_row, rand_col) = 1 + input_boards%new_board(rand_row, rand_col) = 1 + end do + + end function setup_matching_boards + + function run_steady_sate_check(input) result(output) + class(input_t), intent(in) :: input + type(transformed_t) :: output + + logical :: actual_steady_state + + select type (input) + type is (input_boards_t) + call check_for_steady_state(input%current_board, input%new_board, actual_steady_state) + + output = transformed_t(check_for_steady_state_output_t(actual_steady_state)) + + class default + output = transformed_t(transformation_failure_t(fail( & + "Didn't get input_boards_t"))) + + end select + end function run_steady_sate_check + + function check_is_steady_sate(input) result(result_) + class(input_t), intent(in) :: input + type(result_t) :: result_ + + select type (input) + type is (check_for_steady_state_output_t) + result_ = assert_that(input%steady_state) + class default + result_ = fail("Didn't get check_for_steady_state_output_t") + end select + end function check_is_steady_sate +end module game_of_life_sol_test diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/main.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/main.f90 new file mode 100644 index 0000000..59f9034 --- /dev/null +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/main.f90 @@ -0,0 +1,23 @@ +program test_main + use veggies, only : test_item_t, test_that, run_tests + + use game_of_life_sol_test, only : game_of_life_sol_test_suite + + implicit none + + if (.not.run()) stop 1 + +contains + function run() result(passed) + logical :: passed + + type(test_item_t) :: tests + type(test_item_t) :: individual_tests(1) + + individual_tests(1) = game_of_life_sol_test_suite() + + tests = test_that(individual_tests) + + passed = run_tests(tests) + end function run +end program test_main From f110b652a5d38a79f904efefd9d628ac18f77031 Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Mon, 9 Jun 2025 14:36:31 +0100 Subject: [PATCH 09/26] Update tests to be more flexible --- .../test/sol/game_of_life_sol_test.f90 | 119 +++++++++--------- .../challenge-1/test/sol/main.f90 | 4 +- 2 files changed, 62 insertions(+), 61 deletions(-) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 index 2b4efde..d70f2a4 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 @@ -2,64 +2,85 @@ module game_of_life_sol_test use game_of_life_mod, only : check_for_steady_state use veggies, only: & assert_that, & + describe, & + example_t, & fail, & - given, & input_t, & + it, & result_t, & - test_item_t, & - then__, & - transformation_failure_t, & - transformed_t, & - when + test_item_t implicit none private - public :: game_of_life_sol_test_suite + public :: check_for_steady_state_tests - !> Type to bundle the inputs of game_of_life_sol::check_for_steady_state + !> Type to bundle the boards of game_of_life_sol type, extends(input_t) :: input_boards_t integer, dimension(:,:), allocatable :: current_board, new_board end type input_boards_t - !> Type to bundle the outputs of game_of_life_sol::check_for_steady_state - type, extends(input_t) :: check_for_steady_state_output_t - logical :: steady_state - end type check_for_steady_state_output_t + !> Type to bundle inputs and expected outputs of game_of_life_sol::check_for_steady_state + type, extends(input_t) :: check_for_steady_state_in_out_t + type(input_boards_t) :: input_boards + logical :: expected_steady_state + end type check_for_steady_state_in_out_t + interface check_for_steady_state_in_out_t + module procedure check_for_steady_state_in_out_constructor + end interface check_for_steady_state_in_out_t contains - function game_of_life_sol_test_suite() result(tests) + function check_for_steady_state_tests() result(tests) type(test_item_t) :: tests - tests = given( & - "we have populated a matching board and temp_board", & - setup_matching_boards(), & - [ when( & - "we check for steady_state", & - run_steady_sate_check, & - [ then__("steady_state will be reached", check_is_steady_sate) & - ]) & - ]) - end function game_of_life_sol_test_suite - - function setup_matching_boards() result(input_boards) + tests = describe( & + "check_for_steady_state", & + [ it( & + "matching boards are in steady state", & + [ example_t(check_for_steady_state_in_out_t(setup_matching_boards(31, 31, 0), .true.)) & + , example_t(check_for_steady_state_in_out_t(setup_matching_boards(31, 31, 10), .true.)) & + , example_t(check_for_steady_state_in_out_t(setup_matching_boards(31, 31, 31*31), .true.)) & + ], & + check_is_steady_state & + )] & + ) + end function check_for_steady_state_tests + + function check_is_steady_state(input) result(result_) + class(input_t), intent(in) :: input + type(result_t) :: result_ + + logical :: actual_steady_state + + select type (input) + type is (check_for_steady_state_in_out_t) + call check_for_steady_state(input%input_boards%current_board, input%input_boards%new_board, actual_steady_state) + + result_ = assert_that(input%expected_steady_state .eqv. actual_steady_state) + + class default + result_ = fail("Didn't get check_for_steady_state_in_out_t") + + end select + end function check_is_steady_state + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! Contructors + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + function setup_matching_boards(nrow, ncol, num_ones) result(input_boards) + integer, intent(in) :: nrow, ncol, num_ones type(input_boards_t) :: input_boards - integer :: nrow, ncol, row, col, num_ones, rand_row, rand_col + integer :: row, col, rand_row, rand_col real :: rand_real - nrow = 31 - ncol = 31 - ! Initialise to all zeros allocate(input_boards%current_board(nrow, ncol)) allocate(input_boards%new_board(nrow, ncol)) input_boards%current_board = 0 input_boards%new_board = 0 - ! For both boards, set one or more elements to 1 - call random_number(rand_real) - num_ones = 1 + FLOOR(nrow*ncol*rand_real) ! n=1 to n=nrow*ncol + ! For both boards, set to requested number of elements to 1 do row = 1, num_ones ! Get random coordinates for 1 call random_number(rand_real) @@ -73,34 +94,14 @@ function setup_matching_boards() result(input_boards) end function setup_matching_boards - function run_steady_sate_check(input) result(output) - class(input_t), intent(in) :: input - type(transformed_t) :: output - - logical :: actual_steady_state - - select type (input) - type is (input_boards_t) - call check_for_steady_state(input%current_board, input%new_board, actual_steady_state) - - output = transformed_t(check_for_steady_state_output_t(actual_steady_state)) - - class default - output = transformed_t(transformation_failure_t(fail( & - "Didn't get input_boards_t"))) + function check_for_steady_state_in_out_constructor(input_boards, steady_state) result(check_for_steady_state_in_out) + type(input_boards_t), intent(in) :: input_boards + logical, intent(in) :: steady_state - end select - end function run_steady_sate_check + type(check_for_steady_state_in_out_t) :: check_for_steady_state_in_out - function check_is_steady_sate(input) result(result_) - class(input_t), intent(in) :: input - type(result_t) :: result_ + check_for_steady_state_in_out%input_boards = input_boards + check_for_steady_state_in_out%expected_steady_state = steady_state - select type (input) - type is (check_for_steady_state_output_t) - result_ = assert_that(input%steady_state) - class default - result_ = fail("Didn't get check_for_steady_state_output_t") - end select - end function check_is_steady_sate + end function check_for_steady_state_in_out_constructor end module game_of_life_sol_test diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/main.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/main.f90 index 59f9034..3d661fe 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/main.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/main.f90 @@ -1,7 +1,7 @@ program test_main use veggies, only : test_item_t, test_that, run_tests - use game_of_life_sol_test, only : game_of_life_sol_test_suite + use game_of_life_sol_test, only : check_for_steady_state_tests implicit none @@ -14,7 +14,7 @@ function run() result(passed) type(test_item_t) :: tests type(test_item_t) :: individual_tests(1) - individual_tests(1) = game_of_life_sol_test_suite() + individual_tests(1) = check_for_steady_state_tests() tests = test_that(individual_tests) From d236857060520d088e4e3c5cefd3581c798fad75 Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Mon, 9 Jun 2025 14:45:18 +0100 Subject: [PATCH 10/26] Add tests for non steady state boards --- .../test/sol/game_of_life_sol_test.f90 | 50 +++++++++++++++++-- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 index d70f2a4..188e204 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 @@ -41,12 +41,20 @@ function check_for_steady_state_tests() result(tests) , example_t(check_for_steady_state_in_out_t(setup_matching_boards(31, 31, 10), .true.)) & , example_t(check_for_steady_state_in_out_t(setup_matching_boards(31, 31, 31*31), .true.)) & ], & - check_is_steady_state & + check_if_steady_state & + ) & + , it( & + "non-matching boards are not in steady state", & + [ example_t(check_for_steady_state_in_out_t(setup_mismatched_boards(31, 31, 0), .false.)) & + , example_t(check_for_steady_state_in_out_t(setup_mismatched_boards(31, 31, 10), .false.)) & + , example_t(check_for_steady_state_in_out_t(setup_mismatched_boards(31, 31, 31*31), .false.)) & + ], & + check_if_steady_state & )] & ) end function check_for_steady_state_tests - function check_is_steady_state(input) result(result_) + function check_if_steady_state(input) result(result_) class(input_t), intent(in) :: input type(result_t) :: result_ @@ -62,7 +70,7 @@ function check_is_steady_state(input) result(result_) result_ = fail("Didn't get check_for_steady_state_in_out_t") end select - end function check_is_steady_state + end function check_if_steady_state !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! Contructors @@ -82,7 +90,7 @@ function setup_matching_boards(nrow, ncol, num_ones) result(input_boards) ! For both boards, set to requested number of elements to 1 do row = 1, num_ones - ! Get random coordinates for 1 + ! Get random coordinates for both call random_number(rand_real) rand_row = 1 + FLOOR(nrow*rand_real) ! n=1 to n=nrow call random_number(rand_real) @@ -94,6 +102,40 @@ function setup_matching_boards(nrow, ncol, num_ones) result(input_boards) end function setup_matching_boards + function setup_mismatched_boards(nrow, ncol, num_ones) result(input_boards) + integer, intent(in) :: nrow, ncol, num_ones + type(input_boards_t) :: input_boards + + integer :: row, col, rand_row, rand_col + real :: rand_real + + ! Initialise + allocate(input_boards%current_board(nrow, ncol)) + allocate(input_boards%new_board(nrow, ncol)) + input_boards%current_board = 0 + input_boards%new_board = 1 + + ! For both boards, set to requested number of elements to 1 + do row = 1, num_ones + ! Get random coordinates for current + call random_number(rand_real) + rand_row = 1 + FLOOR(nrow*rand_real) ! n=1 to n=nrow + call random_number(rand_real) + rand_col = 1 + FLOOR(ncol*rand_real) ! n=1 to n=ncol + + input_boards%current_board(rand_row, rand_col) = 1 + + ! Get random coordinates for new + call random_number(rand_real) + rand_row = 1 + FLOOR(nrow*rand_real) ! n=1 to n=nrow + call random_number(rand_real) + rand_col = 1 + FLOOR(ncol*rand_real) ! n=1 to n=ncol + + input_boards%new_board(rand_row, rand_col) = 0 + end do + + end function setup_mismatched_boards + function check_for_steady_state_in_out_constructor(input_boards, steady_state) result(check_for_steady_state_in_out) type(input_boards_t), intent(in) :: input_boards logical, intent(in) :: steady_state From eddaf59daced46df3a3eda7cda3ef6626030ab8b Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Mon, 9 Jun 2025 16:13:38 +0100 Subject: [PATCH 11/26] Add tests for evolve_board --- .../test/sol/game_of_life_sol_test.f90 | 171 ++++++++++++++++-- .../challenge-1/test/sol/main.f90 | 5 +- 2 files changed, 163 insertions(+), 13 deletions(-) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 index 188e204..eda5bd1 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 @@ -1,7 +1,8 @@ module game_of_life_sol_test - use game_of_life_mod, only : check_for_steady_state + use game_of_life_mod, only : check_for_steady_state, evolve_board use veggies, only: & assert_that, & + assert_equals, & describe, & example_t, & fail, & @@ -12,7 +13,7 @@ module game_of_life_sol_test implicit none private - public :: check_for_steady_state_tests + public :: check_for_steady_state_tests, evolve_board_tests !> Type to bundle the boards of game_of_life_sol type, extends(input_t) :: input_boards_t @@ -28,8 +29,22 @@ module game_of_life_sol_test module procedure check_for_steady_state_in_out_constructor end interface check_for_steady_state_in_out_t + !> Type to bundle inputs and expected outputs of game_of_life_sol::evolve_board + type, extends(input_t) :: check_evolve_board_in_out_t + type(input_boards_t) :: input_boards + integer, dimension(:,:), allocatable :: expected_new_board + end type check_evolve_board_in_out_t + interface check_evolve_board_in_out_t + module procedure check_evolve_board_in_out_constructor + end interface check_evolve_board_in_out_t + contains + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! Test Suites + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + !> Test suite for the game_of_life_sol::check_for_steady_state subroutine function check_for_steady_state_tests() result(tests) type(test_item_t) :: tests @@ -54,6 +69,99 @@ function check_for_steady_state_tests() result(tests) ) end function check_for_steady_state_tests + !> Test suite for the game_of_life_sol::evolve_board subroutine + function evolve_board_tests() result(tests) + type(test_item_t) :: tests + + type(input_boards_t) :: test_boards + type(example_t) :: steady_state_boards_data(2), non_steady_state_boards_data(1) + + test_boards = get_boards_of_zeros(20, 20) + + ! Steady state boards + ! All zeros + steady_state_boards_data(1) = example_t(check_evolve_board_in_out_t(test_boards, test_boards%new_board)) + + ! A slightly more complex steady state sructure + ! 8 9 10 11 12 + ! -- -- -- -- -- + ! 8 | 0 0 0 0 0 + ! 9 | 0 0 1 0 0 + ! 10 | 0 1 0 1 0 + ! 11 | 0 1 0 1 0 + ! 12 | 0 0 1 0 0 + ! 13 | 0 0 0 0 0 + ! + ! Input board + test_boards%current_board(9,9:11) = [0,1,0] + test_boards%current_board(10,9:11) = [1,0,1] + test_boards%current_board(11,9:11) = [1,0,1] + test_boards%current_board(12,9:11) = [0,1,0] + ! Expected output board + test_boards%new_board(9,9:11) = [0,1,0] + test_boards%new_board(10,9:11) = [1,0,1] + test_boards%new_board(11,9:11) = [1,0,1] + test_boards%new_board(12,9:11) = [0,1,0] + steady_state_boards_data(2) = example_t(check_evolve_board_in_out_t(test_boards, test_boards%new_board)) + ! Reset for next test + test_boards%current_board = 0 + test_boards%new_board = 0 + + ! None-steady state boards + ! One non-zero element + ! Input board + test_boards%current_board(10,9) = 1 + non_steady_state_boards_data(1) = example_t(check_evolve_board_in_out_t(test_boards, test_boards%new_board)) + ! Reset for next test + test_boards%current_board(10,9) = 0 + test_boards%new_board(10,9) = 0 + + ! A slightly more complex non-steady state sructure + ! Input board Expected output board + ! 8 9 10 11 12 8 9 10 11 12 + ! -- -- -- -- -- -- -- -- -- -- + ! 8 | 0 0 0 0 0 8 | 0 0 0 0 0 + ! 9 | 0 0 1 0 0 \ 9 | 0 1 1 1 0 + ! 10 | 0 1 1 1 0 ---\ 10 | 0 1 0 1 0 + ! 11 | 0 1 0 1 0 ---/ 11 | 0 1 0 1 0 + ! 12 | 0 0 1 0 0 / 12 | 0 0 1 0 0 + ! 13 | 0 0 0 0 0 13 | 0 0 0 0 0 + ! + ! Input board + test_boards%current_board(9,9:11) = [0,1,0] + test_boards%current_board(10,9:11) = [1,1,1] + test_boards%current_board(11,9:11) = [1,0,1] + test_boards%current_board(12,9:11) = [0,1,0] + ! Expected output board + test_boards%new_board(9,9:11) = [1,1,1] + test_boards%new_board(10,9:11) = [1,0,1] + test_boards%new_board(11,9:11) = [1,0,1] + test_boards%new_board(12,9:11) = [0,1,0] + steady_state_boards_data(2) = example_t(check_evolve_board_in_out_t(test_boards, test_boards%new_board)) + ! Reset for next test + test_boards%current_board = 0 + test_boards%new_board = 0 + + tests = describe( & + "evolve_board", & + [ it( & + "a board in steady state does not change", & + steady_state_boards_data, & + check_evolve_board & + ) & + , it( & + "a board not in steady state will change", & + non_steady_state_boards_data, & + check_evolve_board & + )] & + ) + end function evolve_board_tests + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! Assertion functions + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + !> Check for the expected output of the game_of_life_sol::check_for_steady_state subroutine function check_if_steady_state(input) result(result_) class(input_t), intent(in) :: input type(result_t) :: result_ @@ -70,11 +178,47 @@ function check_if_steady_state(input) result(result_) result_ = fail("Didn't get check_for_steady_state_in_out_t") end select + end function check_if_steady_state - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !> Check for the expected output of the game_of_life_sol::evolve_board subroutine + function check_evolve_board(input) result(result_) + class(input_t), intent(in) :: input + type(result_t) :: result_ + + integer, dimension(:,:), allocatable ::actual_new_board + + select type (input) + type is (check_evolve_board_in_out_t) + allocate(actual_new_board(size(input%input_boards%current_board, 1), size(input%input_boards%current_board, 2))) + + call evolve_board(input%input_boards%current_board, actual_new_board) + + result_ = assert_equals(input%input_boards%new_board, actual_new_board) + + deallocate(actual_new_board) + class default + result_ = fail("Didn't get check_evolve_board_in_out_t") + + end select + + end function check_evolve_board + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! Contructors - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + function get_boards_of_zeros(nrow, ncol) result(input_boards) + integer, intent(in) :: nrow, ncol + type(input_boards_t) :: input_boards + + ! Initialise to all zeros + allocate(input_boards%current_board(nrow, ncol)) + allocate(input_boards%new_board(nrow, ncol)) + input_boards%current_board = 0 + input_boards%new_board = 0 + end function get_boards_of_zeros + function setup_matching_boards(nrow, ncol, num_ones) result(input_boards) integer, intent(in) :: nrow, ncol, num_ones type(input_boards_t) :: input_boards @@ -83,10 +227,7 @@ function setup_matching_boards(nrow, ncol, num_ones) result(input_boards) real :: rand_real ! Initialise to all zeros - allocate(input_boards%current_board(nrow, ncol)) - allocate(input_boards%new_board(nrow, ncol)) - input_boards%current_board = 0 - input_boards%new_board = 0 + input_boards = get_boards_of_zeros(nrow, ncol) ! For both boards, set to requested number of elements to 1 do row = 1, num_ones @@ -110,9 +251,7 @@ function setup_mismatched_boards(nrow, ncol, num_ones) result(input_boards) real :: rand_real ! Initialise - allocate(input_boards%current_board(nrow, ncol)) - allocate(input_boards%new_board(nrow, ncol)) - input_boards%current_board = 0 + input_boards = get_boards_of_zeros(nrow, ncol) input_boards%new_board = 1 ! For both boards, set to requested number of elements to 1 @@ -146,4 +285,14 @@ function check_for_steady_state_in_out_constructor(input_boards, steady_state) r check_for_steady_state_in_out%expected_steady_state = steady_state end function check_for_steady_state_in_out_constructor + + function check_evolve_board_in_out_constructor(input_boards, expected_new_board) result(check_evolve_board_in_out) + type(input_boards_t), intent(in) :: input_boards + integer, dimension(:,:), allocatable, intent(in) :: expected_new_board + + type(check_evolve_board_in_out_t) :: check_evolve_board_in_out + + check_evolve_board_in_out%input_boards = input_boards + check_evolve_board_in_out%expected_new_board = expected_new_board + end function check_evolve_board_in_out_constructor end module game_of_life_sol_test diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/main.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/main.f90 index 3d661fe..766f57e 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/main.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/main.f90 @@ -1,7 +1,7 @@ program test_main use veggies, only : test_item_t, test_that, run_tests - use game_of_life_sol_test, only : check_for_steady_state_tests + use game_of_life_sol_test, only : check_for_steady_state_tests, evolve_board_tests implicit none @@ -12,9 +12,10 @@ function run() result(passed) logical :: passed type(test_item_t) :: tests - type(test_item_t) :: individual_tests(1) + type(test_item_t) :: individual_tests(2) individual_tests(1) = check_for_steady_state_tests() + individual_tests(2) = evolve_board_tests() tests = test_that(individual_tests) From dfb87b2041546d6d96357d649e96950ac412fbfb Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Mon, 9 Jun 2025 17:12:17 +0100 Subject: [PATCH 12/26] Remove input_boards_t --- .../test/sol/game_of_life_sol_test.f90 | 202 ++++++++++-------- 1 file changed, 115 insertions(+), 87 deletions(-) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 index eda5bd1..5a98f6e 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 @@ -15,14 +15,9 @@ module game_of_life_sol_test private public :: check_for_steady_state_tests, evolve_board_tests - !> Type to bundle the boards of game_of_life_sol - type, extends(input_t) :: input_boards_t - integer, dimension(:,:), allocatable :: current_board, new_board - end type input_boards_t - !> Type to bundle inputs and expected outputs of game_of_life_sol::check_for_steady_state type, extends(input_t) :: check_for_steady_state_in_out_t - type(input_boards_t) :: input_boards + integer, dimension(:,:), allocatable :: current_board, new_board logical :: expected_steady_state end type check_for_steady_state_in_out_t interface check_for_steady_state_in_out_t @@ -31,8 +26,7 @@ module game_of_life_sol_test !> Type to bundle inputs and expected outputs of game_of_life_sol::evolve_board type, extends(input_t) :: check_evolve_board_in_out_t - type(input_boards_t) :: input_boards - integer, dimension(:,:), allocatable :: expected_new_board + integer, dimension(:,:), allocatable :: current_board, expected_new_board end type check_evolve_board_in_out_t interface check_evolve_board_in_out_t module procedure check_evolve_board_in_out_constructor @@ -48,39 +42,76 @@ module game_of_life_sol_test function check_for_steady_state_tests() result(tests) type(test_item_t) :: tests + integer, dimension(:,:), allocatable :: test_current_board, test_new_board + type(example_t) :: matching_boards_data(3), non_matching_boards_data(3) + integer :: nrow, ncol + + nrow = 31 + ncol = 31 + + ! Allocate arrays + allocate(test_current_board(nrow, ncol)) + allocate(test_new_board(nrow, ncol)) + + ! Matching boards + ! All zeros + call setup_matching_boards(test_current_board, test_new_board, 0) + matching_boards_data(1) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .true.)) + ! All ones + call setup_matching_boards(test_current_board, test_new_board, nrow*ncol) + matching_boards_data(2) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .true.)) + ! Up to 10 ones + call setup_matching_boards(test_current_board, test_new_board, 10) + matching_boards_data(3) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .true.)) + + ! Mismatched boards + ! All ones vs all zeros + call setup_mismatched_boards(test_current_board, test_new_board, 0) + non_matching_boards_data(1) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .false.)) + ! All zeros vs all ones + call setup_mismatched_boards(test_current_board, test_new_board, nrow*ncol) + non_matching_boards_data(2) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .false.)) + ! Up to 10 differences + call setup_mismatched_boards(test_current_board, test_new_board, 10) + non_matching_boards_data(3) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .false.)) + tests = describe( & "check_for_steady_state", & [ it( & "matching boards are in steady state", & - [ example_t(check_for_steady_state_in_out_t(setup_matching_boards(31, 31, 0), .true.)) & - , example_t(check_for_steady_state_in_out_t(setup_matching_boards(31, 31, 10), .true.)) & - , example_t(check_for_steady_state_in_out_t(setup_matching_boards(31, 31, 31*31), .true.)) & - ], & + matching_boards_data, & check_if_steady_state & ) & , it( & "non-matching boards are not in steady state", & - [ example_t(check_for_steady_state_in_out_t(setup_mismatched_boards(31, 31, 0), .false.)) & - , example_t(check_for_steady_state_in_out_t(setup_mismatched_boards(31, 31, 10), .false.)) & - , example_t(check_for_steady_state_in_out_t(setup_mismatched_boards(31, 31, 31*31), .false.)) & - ], & + non_matching_boards_data, & check_if_steady_state & )] & ) + + deallocate(test_current_board) + deallocate(test_new_board) end function check_for_steady_state_tests !> Test suite for the game_of_life_sol::evolve_board subroutine function evolve_board_tests() result(tests) type(test_item_t) :: tests - type(input_boards_t) :: test_boards - type(example_t) :: steady_state_boards_data(2), non_steady_state_boards_data(1) + integer, dimension(:,:), allocatable :: test_current_board, expected_new_board + type(example_t) :: steady_state_boards_data(2), non_steady_state_boards_data(2) + integer :: nrow, ncol + + nrow = 20 + ncol = 20 - test_boards = get_boards_of_zeros(20, 20) + allocate(test_current_board(nrow, ncol)) + allocate(expected_new_board(nrow, ncol)) + test_current_board = 0 + expected_new_board = 0 ! Steady state boards ! All zeros - steady_state_boards_data(1) = example_t(check_evolve_board_in_out_t(test_boards, test_boards%new_board)) + steady_state_boards_data(1) = example_t(check_evolve_board_in_out_t(test_current_board, expected_new_board)) ! A slightly more complex steady state sructure ! 8 9 10 11 12 @@ -93,28 +124,27 @@ function evolve_board_tests() result(tests) ! 13 | 0 0 0 0 0 ! ! Input board - test_boards%current_board(9,9:11) = [0,1,0] - test_boards%current_board(10,9:11) = [1,0,1] - test_boards%current_board(11,9:11) = [1,0,1] - test_boards%current_board(12,9:11) = [0,1,0] + test_current_board(9,9:11) = [0,1,0] + test_current_board(10,9:11) = [1,0,1] + test_current_board(11,9:11) = [1,0,1] + test_current_board(12,9:11) = [0,1,0] ! Expected output board - test_boards%new_board(9,9:11) = [0,1,0] - test_boards%new_board(10,9:11) = [1,0,1] - test_boards%new_board(11,9:11) = [1,0,1] - test_boards%new_board(12,9:11) = [0,1,0] - steady_state_boards_data(2) = example_t(check_evolve_board_in_out_t(test_boards, test_boards%new_board)) + expected_new_board(9,9:11) = test_current_board(9,9:11) + expected_new_board(10,9:11) = test_current_board(10,9:11) + expected_new_board(11,9:11) = test_current_board(11,9:11) + expected_new_board(12,9:11) = test_current_board(12,9:11) + steady_state_boards_data(2) = example_t(check_evolve_board_in_out_t(test_current_board, expected_new_board)) ! Reset for next test - test_boards%current_board = 0 - test_boards%new_board = 0 + test_current_board = 0 + expected_new_board = 0 ! None-steady state boards ! One non-zero element ! Input board - test_boards%current_board(10,9) = 1 - non_steady_state_boards_data(1) = example_t(check_evolve_board_in_out_t(test_boards, test_boards%new_board)) + test_current_board(10,9) = 1 + non_steady_state_boards_data(1) = example_t(check_evolve_board_in_out_t(test_current_board, expected_new_board)) ! Reset for next test - test_boards%current_board(10,9) = 0 - test_boards%new_board(10,9) = 0 + test_current_board(10,9) = 0 ! A slightly more complex non-steady state sructure ! Input board Expected output board @@ -128,19 +158,19 @@ function evolve_board_tests() result(tests) ! 13 | 0 0 0 0 0 13 | 0 0 0 0 0 ! ! Input board - test_boards%current_board(9,9:11) = [0,1,0] - test_boards%current_board(10,9:11) = [1,1,1] - test_boards%current_board(11,9:11) = [1,0,1] - test_boards%current_board(12,9:11) = [0,1,0] + test_current_board(9,9:11) = [0,1,0] + test_current_board(10,9:11) = [1,1,1] + test_current_board(11,9:11) = [1,0,1] + test_current_board(12,9:11) = [0,1,0] ! Expected output board - test_boards%new_board(9,9:11) = [1,1,1] - test_boards%new_board(10,9:11) = [1,0,1] - test_boards%new_board(11,9:11) = [1,0,1] - test_boards%new_board(12,9:11) = [0,1,0] - steady_state_boards_data(2) = example_t(check_evolve_board_in_out_t(test_boards, test_boards%new_board)) + expected_new_board(9,9:11) = [1,1,1] + expected_new_board(10,9:11) = [1,0,1] + expected_new_board(11,9:11) = [1,0,1] + expected_new_board(12,9:11) = [0,1,0] + non_steady_state_boards_data(2) = example_t(check_evolve_board_in_out_t(test_current_board, expected_new_board)) ! Reset for next test - test_boards%current_board = 0 - test_boards%new_board = 0 + test_current_board = 0 + expected_new_board = 0 tests = describe( & "evolve_board", & @@ -155,6 +185,9 @@ function evolve_board_tests() result(tests) check_evolve_board & )] & ) + + deallocate(test_current_board) + deallocate(expected_new_board) end function evolve_board_tests !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -170,7 +203,7 @@ function check_if_steady_state(input) result(result_) select type (input) type is (check_for_steady_state_in_out_t) - call check_for_steady_state(input%input_boards%current_board, input%input_boards%new_board, actual_steady_state) + call check_for_steady_state(input%current_board, input%new_board, actual_steady_state) result_ = assert_that(input%expected_steady_state .eqv. actual_steady_state) @@ -190,11 +223,12 @@ function check_evolve_board(input) result(result_) select type (input) type is (check_evolve_board_in_out_t) - allocate(actual_new_board(size(input%input_boards%current_board, 1), size(input%input_boards%current_board, 2))) + allocate(actual_new_board(size(input%current_board, 1), size(input%current_board, 2))) + actual_new_board = input%current_board - call evolve_board(input%input_boards%current_board, actual_new_board) + call evolve_board(input%current_board, actual_new_board) - result_ = assert_equals(input%input_boards%new_board, actual_new_board) + result_ = assert_equals(input%expected_new_board, actual_new_board) deallocate(actual_new_board) class default @@ -208,26 +242,18 @@ end function check_evolve_board ! Contructors !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - function get_boards_of_zeros(nrow, ncol) result(input_boards) - integer, intent(in) :: nrow, ncol - type(input_boards_t) :: input_boards - - ! Initialise to all zeros - allocate(input_boards%current_board(nrow, ncol)) - allocate(input_boards%new_board(nrow, ncol)) - input_boards%current_board = 0 - input_boards%new_board = 0 - end function get_boards_of_zeros - - function setup_matching_boards(nrow, ncol, num_ones) result(input_boards) - integer, intent(in) :: nrow, ncol, num_ones - type(input_boards_t) :: input_boards + subroutine setup_matching_boards(current_board, new_board, num_ones) + integer, dimension(:,:), allocatable, intent(inout) :: current_board, new_board + integer, intent(in) :: num_ones - integer :: row, col, rand_row, rand_col + integer :: nrow, ncol, row, col, rand_row, rand_col real :: rand_real ! Initialise to all zeros - input_boards = get_boards_of_zeros(nrow, ncol) + nrow = size(current_board, 1) + ncol = size(current_board, 2) + current_board = 0 + new_board = 0 ! For both boards, set to requested number of elements to 1 do row = 1, num_ones @@ -237,32 +263,34 @@ function setup_matching_boards(nrow, ncol, num_ones) result(input_boards) call random_number(rand_real) rand_col = 1 + FLOOR(ncol*rand_real) ! n=1 to n=ncol - input_boards%current_board(rand_row, rand_col) = 1 - input_boards%new_board(rand_row, rand_col) = 1 + current_board(rand_row, rand_col) = 1 + new_board(rand_row, rand_col) = 1 end do - end function setup_matching_boards + end subroutine setup_matching_boards - function setup_mismatched_boards(nrow, ncol, num_ones) result(input_boards) - integer, intent(in) :: nrow, ncol, num_ones - type(input_boards_t) :: input_boards + subroutine setup_mismatched_boards(current_board, new_board, num_differences) + integer, dimension(:,:), allocatable, intent(inout) :: current_board, new_board + integer, intent(in) :: num_differences - integer :: row, col, rand_row, rand_col + integer :: nrow, ncol, row, col, rand_row, rand_col real :: rand_real ! Initialise - input_boards = get_boards_of_zeros(nrow, ncol) - input_boards%new_board = 1 + nrow = size(current_board, 1) + ncol = size(current_board, 2) + current_board = 0 + new_board = 1 - ! For both boards, set to requested number of elements to 1 - do row = 1, num_ones + ! For both boards, set to requested number of elements to the opposite value + do row = 1, num_differences ! Get random coordinates for current call random_number(rand_real) rand_row = 1 + FLOOR(nrow*rand_real) ! n=1 to n=nrow call random_number(rand_real) rand_col = 1 + FLOOR(ncol*rand_real) ! n=1 to n=ncol - input_boards%current_board(rand_row, rand_col) = 1 + current_board(rand_row, rand_col) = 1 ! Get random coordinates for new call random_number(rand_real) @@ -270,29 +298,29 @@ function setup_mismatched_boards(nrow, ncol, num_ones) result(input_boards) call random_number(rand_real) rand_col = 1 + FLOOR(ncol*rand_real) ! n=1 to n=ncol - input_boards%new_board(rand_row, rand_col) = 0 + new_board(rand_row, rand_col) = 0 end do - end function setup_mismatched_boards + end subroutine setup_mismatched_boards - function check_for_steady_state_in_out_constructor(input_boards, steady_state) result(check_for_steady_state_in_out) - type(input_boards_t), intent(in) :: input_boards + function check_for_steady_state_in_out_constructor(current_board, new_board, steady_state) result(check_for_steady_state_in_out) + integer, dimension(:,:), allocatable, intent(in) :: current_board, new_board logical, intent(in) :: steady_state type(check_for_steady_state_in_out_t) :: check_for_steady_state_in_out - check_for_steady_state_in_out%input_boards = input_boards + check_for_steady_state_in_out%current_board = current_board + check_for_steady_state_in_out%new_board = new_board check_for_steady_state_in_out%expected_steady_state = steady_state end function check_for_steady_state_in_out_constructor - function check_evolve_board_in_out_constructor(input_boards, expected_new_board) result(check_evolve_board_in_out) - type(input_boards_t), intent(in) :: input_boards - integer, dimension(:,:), allocatable, intent(in) :: expected_new_board + function check_evolve_board_in_out_constructor(current_board, expected_new_board) result(check_evolve_board_in_out) + integer, dimension(:,:), allocatable, intent(in) :: current_board, expected_new_board type(check_evolve_board_in_out_t) :: check_evolve_board_in_out - check_evolve_board_in_out%input_boards = input_boards + check_evolve_board_in_out%current_board = current_board check_evolve_board_in_out%expected_new_board = expected_new_board end function check_evolve_board_in_out_constructor end module game_of_life_sol_test From a2d03ef88c6f043bff232a1d4a86745c75b60362 Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Mon, 9 Jun 2025 17:37:14 +0100 Subject: [PATCH 13/26] Move solution into solution dir --- .../challenge-1/README.md | 4 ++-- .../challenge-1/fpm.toml | 8 +------- .../challenge-1/solution/README.md | 15 +++++++++++++++ .../challenge-1/solution/fpm.toml | 15 +++++++++++++++ .../{src/sol => solution/src}/game_of_life.f90 | 0 .../sol => solution/src}/game_of_life_mod.f90 | 12 +++++------- .../test/game_of_life_test.f90} | 4 ++-- .../{test/sol => solution/test}/main.f90 | 2 +- 8 files changed, 41 insertions(+), 19 deletions(-) create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/fpm.toml rename episodes/2-intro-to-fortran-unit-tests/challenge-1/{src/sol => solution/src}/game_of_life.f90 (100%) rename episodes/2-intro-to-fortran-unit-tests/challenge-1/{src/sol => solution/src}/game_of_life_mod.f90 (87%) rename episodes/2-intro-to-fortran-unit-tests/challenge-1/{test/sol/game_of_life_sol_test.f90 => solution/test/game_of_life_test.f90} (99%) rename episodes/2-intro-to-fortran-unit-tests/challenge-1/{test/sol => solution/test}/main.f90 (85%) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/README.md b/episodes/2-intro-to-fortran-unit-tests/challenge-1/README.md index 6795216..ec55e48 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/README.md +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/README.md @@ -3,6 +3,6 @@ Take a look at the [src](./src) code provided. 1. Can you identify the aspects of this Fortran code which make it difficult to unit test? -2. Try to improve the src to make it more unit testable? +2. Try to improve the src to make it more unit testable. -A solution is provided in [src/sol](./src/sol). +A solution is provided in the [solution](./solution/) dir. diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml b/episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml index be70212..4b5ea0c 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml @@ -6,11 +6,5 @@ veggies.tag = "main" [[executable]] name = "game-of-life" -source-dir = "src" # Update this to src/sol to build the solution +source-dir = "src" main = "game_of_life.f90" - -# Uncamment the below to enable the solution tests -# [[test]] -# name = "game_of_life_sol_test" -# source-dir = "test/sol" -# main = "main.f90" diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md new file mode 100644 index 0000000..11d9f0e --- /dev/null +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md @@ -0,0 +1,15 @@ +# Episode 2 - Challenge 1 - Solution: Identify bad practice for unit testing Fortran + +You can find the corresponding fix for each point below within the provided solution [src](./src/). Search each file for `Q_FIX`. + +## Question 1 + +>Can you identify the aspects of this Fortran code which make it difficult to unit test? + +There are several issues with this Fortran code which make it hard to unit test. Find the suggested fixes listed below. + +1. Everything is containing within a single program. This prevents us from using individual procedures within test modules. Effectively preventing us from testing them. + +2. There is a lot of global state used across multiple procedures. This makes tests dependent on one another therefore complicating the management of data/state between tests. + +3. TODO: There is a lot of logic not contained within procedures. Wrapping this in a procedure opens up more of the code which can be tested. diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/fpm.toml b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/fpm.toml new file mode 100644 index 0000000..d887ea0 --- /dev/null +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/fpm.toml @@ -0,0 +1,15 @@ +name = "episode-2-challenge-1-solution" + +[dev-dependencies] +veggies.git = "https://gitlab.com/everythingfunctional/veggies" +veggies.tag = "main" + +[[executable]] +name = "game-of-life" +source-dir = "src" +main = "game_of_life.f90" + +[[test]] +name = "game_of_life_sol_test" +source-dir = "test" +main = "main.f90" diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/sol/game_of_life.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life.f90 similarity index 100% rename from episodes/2-intro-to-fortran-unit-tests/challenge-1/src/sol/game_of_life.f90 rename to episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life.f90 diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/sol/game_of_life_mod.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 similarity index 87% rename from episodes/2-intro-to-fortran-unit-tests/challenge-1/src/sol/game_of_life_mod.f90 rename to episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 index fcf7102..f7dee9a 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/sol/game_of_life_mod.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 @@ -1,9 +1,4 @@ -! ======================================================= -! Conway's game of life -! -! ======================================================= -! Adapted from https://github.com/tuckerrc/game_of_life -! ======================================================= +! Q1_FIX_1: Moving these procedures into a separate file allows them to be used within a test file module game_of_life_mod implicit none @@ -11,7 +6,8 @@ module game_of_life_mod contains - !> Evolve the board into the state od the next iteration + ! Q1_FIX2: Pass parameters into procedures instead of relying on global state to isolate tests from one another. + !> Evolve the board into the state of the next iteration subroutine evolve_board(current_board, new_board) !> The board as it currently is before this iteration integer, dimension(:,:), allocatable, intent(in) :: current_board @@ -47,6 +43,7 @@ subroutine evolve_board(current_board, new_board) enddo end subroutine evolve_board + ! Q1_FIX2: Pass parameters into procedures instead of relying on global state to isolate tests from one another. !> Check if we have reached steady state, i.e. current and new board match subroutine check_for_steady_state(current_board, new_board, steady_state) !> The board as it currently is before this iteration @@ -73,6 +70,7 @@ subroutine check_for_steady_state(current_board, new_board, steady_state) steady_state = .true. end subroutine check_for_steady_state + ! Q1_FIX2: Pass parameters into procedures instead of relying on global state to isolate tests from one another. !> Output the current board to the terminal subroutine draw_board(current_board) !> The board as it currently is for this iteration diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 similarity index 99% rename from episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 rename to episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 index 5a98f6e..e6440a1 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/game_of_life_sol_test.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 @@ -1,4 +1,4 @@ -module game_of_life_sol_test +module game_of_life_test use game_of_life_mod, only : check_for_steady_state, evolve_board use veggies, only: & assert_that, & @@ -323,4 +323,4 @@ function check_evolve_board_in_out_constructor(current_board, expected_new_board check_evolve_board_in_out%current_board = current_board check_evolve_board_in_out%expected_new_board = expected_new_board end function check_evolve_board_in_out_constructor -end module game_of_life_sol_test +end module game_of_life_test diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/main.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/main.f90 similarity index 85% rename from episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/main.f90 rename to episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/main.f90 index 766f57e..b4bddfe 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/test/sol/main.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/main.f90 @@ -1,7 +1,7 @@ program test_main use veggies, only : test_item_t, test_that, run_tests - use game_of_life_sol_test, only : check_for_steady_state_tests, evolve_board_tests + use game_of_life_test, only : check_for_steady_state_tests, evolve_board_tests implicit none From 8320ce9e12476bd3130a19ba14e996f5687a12bf Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Tue, 10 Jun 2025 12:26:50 +0100 Subject: [PATCH 14/26] Replace fix me tag info with building info in solution --- .../challenge-1/solution/README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md index 11d9f0e..ea85fec 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md @@ -1,6 +1,16 @@ # Episode 2 - Challenge 1 - Solution: Identify bad practice for unit testing Fortran -You can find the corresponding fix for each point below within the provided solution [src](./src/). Search each file for `Q_FIX`. +The solution provided here is an entirely self-contained project which can be built using FPM. + +```bash +fpm build +``` + +Tests are also provided, which can be run using FPM. + +```bash +fpm test +``` ## Question 1 From 2700243cd875b0046397951b9a1669a0b7985d74 Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Tue, 10 Jun 2025 12:42:52 +0100 Subject: [PATCH 15/26] Extract the IO into a procedure --- .../challenge-1/solution/src/game_of_life.f90 | 50 +++------------- .../solution/src/game_of_life_mod.f90 | 58 +++++++++++++++++++ 2 files changed, 67 insertions(+), 41 deletions(-) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life.f90 index 1353efe..d55ca42 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life.f90 @@ -5,14 +5,13 @@ ! Adapted from https://github.com/tuckerrc/game_of_life ! ======================================================= program game_of_life - use game_of_life_mod, only : evolve_board, check_for_steady_state, draw_board + use game_of_life_mod, only : evolve_board, check_for_steady_state, draw_board, read_model_from_file implicit none !! Board args integer, parameter :: max_nrow = 100, max_ncol = 100, max_generations = 100 - integer :: nrow, ncol - integer :: row, generation_number + integer :: generation_number integer, dimension(:,:), allocatable :: current_board, new_board !! Animation args @@ -24,10 +23,8 @@ program game_of_life integer :: argl character(len=:), allocatable :: cli_arg_temp_store, input_fname - !! File IO args - character(len=80) :: text_to_discard - integer :: input_file_io - integer :: iostat + !! IO args + integer :: stat ! Get current_board file path from command line if (command_argument_count() == 1) then @@ -44,43 +41,14 @@ program game_of_life stop end if - ! Open input file - open(unit=input_file_io, & - file=input_fname, & - status='old', & - IOSTAT=iostat) + ! Q1_FIX3: Extract the file IO into a module procedure to allow it to be tested. + call read_model_from_file(input_fname, max_nrow, max_ncol, current_board, stat) - if( iostat /= 0) then - write(*,'(a)') ' *** Error when opening '//input_fname - stop 1 + if( stat /= 0) then + stop stat end if - ! Read in current_board from file - read(input_file_io,'(a)') text_to_discard ! Skip first line - read(input_file_io,*) nrow, ncol - - ! Verify the date_time_values read from the file - if (nrow < 1 .or. nrow > max_nrow) then - write (*,'(a,i6,a,i6)') "nrow must be a positive integer less than ", max_nrow, " found ", nrow - stop 1 - end if - - if (ncol < 1 .or. ncol > max_ncol) then - write (*,'(a,i6,a,i6)') "ncol must be a positive integer less than ", max_ncol, " found ", ncol - stop 1 - end if - - allocate(current_board(nrow, ncol)) - allocate(new_board(nrow, ncol)) - - read(input_file_io,'(a)') text_to_discard ! Skip next line - ! Populate the boards starting state - do row = 1, nrow - read(input_file_io,*) current_board(row, :) - end do - - close(input_file_io) - + allocate(new_board(size(current_board,1), size(current_board, 2))) new_board = 0 ! Clear the terminal screen diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 index f7dee9a..75fc2b7 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 @@ -101,4 +101,62 @@ subroutine draw_board(current_board) deallocate(output) end subroutine draw_board + ! Q1_FIX3: Extract the file IO into a module procedure to allow it to be tested. + !> Populate the a board from the provided file + subroutine read_model_from_file(input_fname, max_nrow, max_ncol, board, stat) + !> The name of the file to read in the board + character(len=:), allocatable, intent(in) :: input_fname + !> The maximum allowed number of rows + integer, intent(in) :: max_nrow + !> The maximum allowed number of columns + integer, intent(in) :: max_ncol + !> The board to be populated + integer, dimension(:,:), allocatable, intent(out) :: board + !> A flag to indicate if reading the file was successful + integer, intent(out) :: stat + + ! Board definition args + integer :: nrow, ncol, row + + ! File IO args + integer :: input_file_io, iostat + character(len=80) :: text_to_discard + + ! Open input file + open(unit=input_file_io, & + file=input_fname, & + status='old', & + IOSTAT=iostat) + + if( iostat /= 0) then + write(*,'(a)') ' *** Error when opening '//input_fname + stat = iostat + end if + + ! Read in board from file + read(input_file_io,'(a)') text_to_discard ! Skip first line + read(input_file_io,*) nrow, ncol + + ! Verify the date_time_values read from the file + if (nrow < 1 .or. nrow > max_nrow) then + write (*,'(a,i6,a,i6)') "nrow must be a positive integer less than ", max_nrow, " found ", nrow + stat = 1 + end if + + if (ncol < 1 .or. ncol > max_ncol) then + write (*,'(a,i6,a,i6)') "ncol must be a positive integer less than ", max_ncol, " found ", ncol + stat = 1 + end if + + allocate(board(nrow, ncol)) + + read(input_file_io,'(a)') text_to_discard ! Skip next line + ! Populate the boards starting state + do row = 1, nrow + read(input_file_io,*) board(row, :) + end do + + close(input_file_io) + end subroutine read_model_from_file + end module game_of_life_mod From ab6126682c6eeb9edf9ae59125646d6e1b5fbc4f Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Tue, 10 Jun 2025 15:28:48 +0100 Subject: [PATCH 16/26] Add first test for read_model_from_file --- .../solution/src/game_of_life_mod.f90 | 18 +++-- .../solution/test/game_of_life_test.f90 | 81 ++++++++++++++++++- .../challenge-1/solution/test/main.f90 | 5 +- .../solution/test/models/zeros_31_31.dat | 34 ++++++++ 4 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/models/zeros_31_31.dat diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 index 75fc2b7..8d6761e 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 @@ -122,6 +122,8 @@ subroutine read_model_from_file(input_fname, max_nrow, max_ncol, board, stat) integer :: input_file_io, iostat character(len=80) :: text_to_discard + stat = 0 + ! Open input file open(unit=input_file_io, & file=input_fname, & @@ -148,13 +150,17 @@ subroutine read_model_from_file(input_fname, max_nrow, max_ncol, board, stat) stat = 1 end if - allocate(board(nrow, ncol)) + if (stat == 0) then - read(input_file_io,'(a)') text_to_discard ! Skip next line - ! Populate the boards starting state - do row = 1, nrow - read(input_file_io,*) board(row, :) - end do + allocate(board(nrow, ncol)) + + read(input_file_io,'(a)') text_to_discard ! Skip next line + ! Populate the boards starting state + do row = 1, nrow + read(input_file_io,*) board(row, :) + end do + + end if close(input_file_io) end subroutine read_model_from_file diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 index e6440a1..5bf4965 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 @@ -1,5 +1,5 @@ module game_of_life_test - use game_of_life_mod, only : check_for_steady_state, evolve_board + use game_of_life_mod, only : check_for_steady_state, evolve_board, read_model_from_file use veggies, only: & assert_that, & assert_equals, & @@ -13,7 +13,7 @@ module game_of_life_test implicit none private - public :: check_for_steady_state_tests, evolve_board_tests + public :: check_for_steady_state_tests, evolve_board_tests, read_model_from_file_tests !> Type to bundle inputs and expected outputs of game_of_life_sol::check_for_steady_state type, extends(input_t) :: check_for_steady_state_in_out_t @@ -32,6 +32,18 @@ module game_of_life_test module procedure check_evolve_board_in_out_constructor end interface check_evolve_board_in_out_t + !> Type to bundle inputs and expected outputs of game_of_life_sol::read_model_from_file + type, extends(input_t) :: check_read_model_from_file_in_out_t + character(len=:), allocatable :: input_fname + integer :: max_nrow + integer :: max_ncol + integer, dimension(:,:), allocatable :: expected_board + integer :: expected_stat + end type check_read_model_from_file_in_out_t + interface check_read_model_from_file_in_out_t + module procedure check_read_model_from_file_in_out_constructor + end interface check_read_model_from_file_in_out_t + contains !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -190,6 +202,29 @@ function evolve_board_tests() result(tests) deallocate(expected_new_board) end function evolve_board_tests + function read_model_from_file_tests() result(tests) + type(test_item_t) :: tests + + integer, dimension(:,:), allocatable :: test_board + type(example_t) :: valid_model_file_data(1) + + allocate(test_board(31, 31)) + test_board = 0 + + valid_model_file_data(1) = example_t( & + check_read_model_from_file_in_out_t("test/models/zeros_31_31.dat", 100, 100, test_board, 0) & + ) + + tests = describe( & + "read_model_from_file", & + [ it( & + "a valid model file is loaded successfully", & + valid_model_file_data, & + check_read_model_from_file & + )] & + ) + end function read_model_from_file_tests + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! Assertion functions !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -238,6 +273,31 @@ function check_evolve_board(input) result(result_) end function check_evolve_board + !> Check for the expected output of the game_of_life_sol::read_model_from_file subroutine + function check_read_model_from_file(input) result(result_) + class(input_t), intent(in) :: input + type(result_t) :: result_ + + integer, dimension(:,:), allocatable ::actual_board + integer :: actual_stat + + select type (input) + type is (check_read_model_from_file_in_out_t) + call read_model_from_file(input%input_fname, input%max_nrow, input%max_ncol, actual_board, actual_stat) + + result_ = assert_equals(input%expected_board(1,1), actual_board(1,1)) .and. & + assert_equals(input%expected_stat, actual_stat) + + if (allocated(actual_board)) then + deallocate(actual_board) + end if + class default + result_ = fail("Didn't get check_read_model_from_file_in_out_t") + + end select + + end function check_read_model_from_file + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! Contructors !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -323,4 +383,21 @@ function check_evolve_board_in_out_constructor(current_board, expected_new_board check_evolve_board_in_out%current_board = current_board check_evolve_board_in_out%expected_new_board = expected_new_board end function check_evolve_board_in_out_constructor + + function check_read_model_from_file_in_out_constructor(input_fname, max_nrow, max_ncol, expected_board, expected_stat) & + result(check_read_model_from_file_in_out) + character(len=:), allocatable, intent(in) :: input_fname + integer, intent(in) :: max_nrow + integer, intent(in) :: max_ncol + integer, dimension(:,:), allocatable, intent(out) :: expected_board + integer, intent(out) :: expected_stat + + type(check_read_model_from_file_in_out_t) :: check_read_model_from_file_in_out + + check_read_model_from_file_in_out%input_fname = input_fname + check_read_model_from_file_in_out%max_nrow = max_nrow + check_read_model_from_file_in_out%max_ncol = max_ncol + check_read_model_from_file_in_out%expected_board = expected_board + check_read_model_from_file_in_out%expected_stat = expected_stat + end function check_read_model_from_file_in_out_constructor end module game_of_life_test diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/main.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/main.f90 index b4bddfe..8a62a1f 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/main.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/main.f90 @@ -1,7 +1,7 @@ program test_main use veggies, only : test_item_t, test_that, run_tests - use game_of_life_test, only : check_for_steady_state_tests, evolve_board_tests + use game_of_life_test, only : check_for_steady_state_tests, evolve_board_tests, read_model_from_file_tests implicit none @@ -12,10 +12,11 @@ function run() result(passed) logical :: passed type(test_item_t) :: tests - type(test_item_t) :: individual_tests(2) + type(test_item_t) :: individual_tests(3) individual_tests(1) = check_for_steady_state_tests() individual_tests(2) = evolve_board_tests() + individual_tests(3) = read_model_from_file_tests() tests = test_that(individual_tests) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/models/zeros_31_31.dat b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/models/zeros_31_31.dat new file mode 100644 index 0000000..2abbb20 --- /dev/null +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/models/zeros_31_31.dat @@ -0,0 +1,34 @@ +nrow ncol + 31 31 +Board + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 From 24fefe8ff71a9f7e9346ab3c9f1b5cb81fb2614c Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Wed, 11 Jun 2025 14:49:26 +0100 Subject: [PATCH 17/26] Use allocatable error message instead of status integer --- .../challenge-1/solution/src/game_of_life.f90 | 9 ++-- .../solution/src/game_of_life_mod.f90 | 46 +++++++++---------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life.f90 index d55ca42..dcb41ab 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life.f90 @@ -24,6 +24,7 @@ program game_of_life character(len=:), allocatable :: cli_arg_temp_store, input_fname !! IO args + character(len=:), allocatable :: io_error_message integer :: stat ! Get current_board file path from command line @@ -42,10 +43,12 @@ program game_of_life end if ! Q1_FIX3: Extract the file IO into a module procedure to allow it to be tested. - call read_model_from_file(input_fname, max_nrow, max_ncol, current_board, stat) + call read_model_from_file(input_fname, max_nrow, max_ncol, current_board, io_error_message) - if( stat /= 0) then - stop stat + if (allocated(io_error_message)) then + write (*,*) io_error_message + deallocate(io_error_message) + stop end if allocate(new_board(size(current_board,1), size(current_board, 2))) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 index 8d6761e..827904d 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 @@ -103,7 +103,7 @@ end subroutine draw_board ! Q1_FIX3: Extract the file IO into a module procedure to allow it to be tested. !> Populate the a board from the provided file - subroutine read_model_from_file(input_fname, max_nrow, max_ncol, board, stat) + subroutine read_model_from_file(input_fname, max_nrow, max_ncol, board, io_error_message) !> The name of the file to read in the board character(len=:), allocatable, intent(in) :: input_fname !> The maximum allowed number of rows @@ -113,7 +113,7 @@ subroutine read_model_from_file(input_fname, max_nrow, max_ncol, board, stat) !> The board to be populated integer, dimension(:,:), allocatable, intent(out) :: board !> A flag to indicate if reading the file was successful - integer, intent(out) :: stat + character(len=:), allocatable,intent(out) :: io_error_message ! Board definition args integer :: nrow, ncol, row @@ -122,7 +122,7 @@ subroutine read_model_from_file(input_fname, max_nrow, max_ncol, board, stat) integer :: input_file_io, iostat character(len=80) :: text_to_discard - stat = 0 + input_file_io = 1111 ! Open input file open(unit=input_file_io, & @@ -130,27 +130,25 @@ subroutine read_model_from_file(input_fname, max_nrow, max_ncol, board, stat) status='old', & IOSTAT=iostat) - if( iostat /= 0) then - write(*,'(a)') ' *** Error when opening '//input_fname - stat = iostat - end if - - ! Read in board from file - read(input_file_io,'(a)') text_to_discard ! Skip first line - read(input_file_io,*) nrow, ncol - - ! Verify the date_time_values read from the file - if (nrow < 1 .or. nrow > max_nrow) then - write (*,'(a,i6,a,i6)') "nrow must be a positive integer less than ", max_nrow, " found ", nrow - stat = 1 - end if - - if (ncol < 1 .or. ncol > max_ncol) then - write (*,'(a,i6,a,i6)') "ncol must be a positive integer less than ", max_ncol, " found ", ncol - stat = 1 - end if - - if (stat == 0) then + if( iostat == 0) then + ! Read in board from file + read(input_file_io,'(a)') text_to_discard ! Skip first line + read(input_file_io,*) nrow, ncol + + ! Verify the date_time_values read from the file + if (nrow < 1 .or. nrow > max_nrow) then + allocate(character(100) :: io_error_message) + write (io_error_message,'(a,i6,a,i6)') "nrow must be a positive integer less than ", max_nrow, " found ", nrow + elseif (ncol < 1 .or. ncol > max_ncol) then + allocate(character(100) :: io_error_message) + write (io_error_message,'(a,i6,a,i6)') "ncol must be a positive integer less than ", max_ncol, " found ", ncol + end if + else + allocate(character(100) :: io_error_message) + write(io_error_message,'(a)') ' *** Error when opening '//input_fname + endif + + if (.not. allocated(io_error_message)) then allocate(board(nrow, ncol)) From a570f6ae4ada3fd64b8e54b78ede37a24c40d5df Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Wed, 11 Jun 2025 14:51:46 +0100 Subject: [PATCH 18/26] add sad path tests for read_model_from_file --- .../solution/test/game_of_life_test.f90 | 145 +++++++++++++----- .../solution/test/models/empty_-10_10.dat | 2 + .../solution/test/models/empty_10_-10.dat | 2 + 3 files changed, 107 insertions(+), 42 deletions(-) create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/models/empty_-10_10.dat create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/models/empty_10_-10.dat diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 index 5bf4965..d443132 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 @@ -9,7 +9,7 @@ module game_of_life_test input_t, & it, & result_t, & - test_item_t + test_item_t, assert_not implicit none private @@ -25,24 +25,24 @@ module game_of_life_test end interface check_for_steady_state_in_out_t !> Type to bundle inputs and expected outputs of game_of_life_sol::evolve_board - type, extends(input_t) :: check_evolve_board_in_out_t + type, extends(input_t) :: evolve_board_in_out_t integer, dimension(:,:), allocatable :: current_board, expected_new_board - end type check_evolve_board_in_out_t - interface check_evolve_board_in_out_t - module procedure check_evolve_board_in_out_constructor - end interface check_evolve_board_in_out_t + end type evolve_board_in_out_t + interface evolve_board_in_out_t + module procedure evolve_board_in_out_constructor + end interface evolve_board_in_out_t !> Type to bundle inputs and expected outputs of game_of_life_sol::read_model_from_file - type, extends(input_t) :: check_read_model_from_file_in_out_t + type, extends(input_t) :: read_model_from_file_in_out_t character(len=:), allocatable :: input_fname integer :: max_nrow integer :: max_ncol integer, dimension(:,:), allocatable :: expected_board - integer :: expected_stat - end type check_read_model_from_file_in_out_t - interface check_read_model_from_file_in_out_t - module procedure check_read_model_from_file_in_out_constructor - end interface check_read_model_from_file_in_out_t + character(len=:), allocatable :: expected_io_error_message + end type read_model_from_file_in_out_t + interface read_model_from_file_in_out_t + module procedure read_model_from_file_in_out_constructor + end interface read_model_from_file_in_out_t contains @@ -123,7 +123,7 @@ function evolve_board_tests() result(tests) ! Steady state boards ! All zeros - steady_state_boards_data(1) = example_t(check_evolve_board_in_out_t(test_current_board, expected_new_board)) + steady_state_boards_data(1) = example_t(evolve_board_in_out_t(test_current_board, expected_new_board)) ! A slightly more complex steady state sructure ! 8 9 10 11 12 @@ -145,7 +145,7 @@ function evolve_board_tests() result(tests) expected_new_board(10,9:11) = test_current_board(10,9:11) expected_new_board(11,9:11) = test_current_board(11,9:11) expected_new_board(12,9:11) = test_current_board(12,9:11) - steady_state_boards_data(2) = example_t(check_evolve_board_in_out_t(test_current_board, expected_new_board)) + steady_state_boards_data(2) = example_t(evolve_board_in_out_t(test_current_board, expected_new_board)) ! Reset for next test test_current_board = 0 expected_new_board = 0 @@ -154,7 +154,7 @@ function evolve_board_tests() result(tests) ! One non-zero element ! Input board test_current_board(10,9) = 1 - non_steady_state_boards_data(1) = example_t(check_evolve_board_in_out_t(test_current_board, expected_new_board)) + non_steady_state_boards_data(1) = example_t(evolve_board_in_out_t(test_current_board, expected_new_board)) ! Reset for next test test_current_board(10,9) = 0 @@ -179,7 +179,7 @@ function evolve_board_tests() result(tests) expected_new_board(10,9:11) = [1,0,1] expected_new_board(11,9:11) = [1,0,1] expected_new_board(12,9:11) = [0,1,0] - non_steady_state_boards_data(2) = example_t(check_evolve_board_in_out_t(test_current_board, expected_new_board)) + non_steady_state_boards_data(2) = example_t(evolve_board_in_out_t(test_current_board, expected_new_board)) ! Reset for next test test_current_board = 0 expected_new_board = 0 @@ -206,21 +206,58 @@ function read_model_from_file_tests() result(tests) type(test_item_t) :: tests integer, dimension(:,:), allocatable :: test_board - type(example_t) :: valid_model_file_data(1) + character(len=:), allocatable :: test_io_error_message + type(example_t) :: valid_model_file_data(1), invalid_model_file_data(5) allocate(test_board(31, 31)) test_board = 0 valid_model_file_data(1) = example_t( & - check_read_model_from_file_in_out_t("test/models/zeros_31_31.dat", 100, 100, test_board, 0) & + read_model_from_file_in_out_t("test/models/zeros_31_31.dat", 100, 100, test_board, test_io_error_message) & ) + deallocate(test_board) + + allocate(character(100) :: test_io_error_message) + + test_io_error_message = "nrow must be a positive integer less than 10 found 31" + invalid_model_file_data(1) = example_t( & + read_model_from_file_in_out_t("test/models/zeros_31_31.dat", 10, 100, test_board, test_io_error_message) & + ) + + test_io_error_message = "ncol must be a positive integer less than 10 found 31" + invalid_model_file_data(2) = example_t( & + read_model_from_file_in_out_t("test/models/zeros_31_31.dat", 100, 10, test_board, test_io_error_message) & + ) + + test_io_error_message = "nrow must be a positive integer less than 100 found -10" + invalid_model_file_data(3) = example_t( & + read_model_from_file_in_out_t("test/models/empty_-10_10.dat", 100, 100, test_board, test_io_error_message) & + ) + + test_io_error_message = "ncol must be a positive integer less than 100 found -10" + invalid_model_file_data(4) = example_t( & + read_model_from_file_in_out_t("test/models/empty_10_-10.dat", 100, 100, test_board, test_io_error_message) & + ) + + test_io_error_message = " *** Error when opening does/not/exist.dat" + invalid_model_file_data(5) = example_t( & + read_model_from_file_in_out_t("does/not/exist.dat", 100, 100, test_board, test_io_error_message) & + ) + + deallocate(test_io_error_message) + tests = describe( & "read_model_from_file", & [ it( & "a valid model file is loaded successfully", & valid_model_file_data, & check_read_model_from_file & + ), & + it( & + "a invalid model file is loaded unsuccessfully", & + invalid_model_file_data, & + check_read_model_from_file_with_invalid_model & )] & ) end function read_model_from_file_tests @@ -257,7 +294,7 @@ function check_evolve_board(input) result(result_) integer, dimension(:,:), allocatable ::actual_new_board select type (input) - type is (check_evolve_board_in_out_t) + type is (evolve_board_in_out_t) allocate(actual_new_board(size(input%current_board, 1), size(input%current_board, 2))) actual_new_board = input%current_board @@ -267,7 +304,7 @@ function check_evolve_board(input) result(result_) deallocate(actual_new_board) class default - result_ = fail("Didn't get check_evolve_board_in_out_t") + result_ = fail("Didn't get evolve_board_in_out_t") end select @@ -279,25 +316,48 @@ function check_read_model_from_file(input) result(result_) type(result_t) :: result_ integer, dimension(:,:), allocatable ::actual_board - integer :: actual_stat + character(len=:), allocatable :: actual_io_error_message select type (input) - type is (check_read_model_from_file_in_out_t) - call read_model_from_file(input%input_fname, input%max_nrow, input%max_ncol, actual_board, actual_stat) + type is (read_model_from_file_in_out_t) + call read_model_from_file(input%input_fname, input%max_nrow, input%max_ncol, actual_board, actual_io_error_message) - result_ = assert_equals(input%expected_board(1,1), actual_board(1,1)) .and. & - assert_equals(input%expected_stat, actual_stat) + result_ = assert_equals(input%expected_board, actual_board) .and. & + assert_not(allocated(actual_io_error_message)) if (allocated(actual_board)) then deallocate(actual_board) end if class default - result_ = fail("Didn't get check_read_model_from_file_in_out_t") + result_ = fail("Didn't get read_model_from_file_in_out_t") end select end function check_read_model_from_file + !> Check for the expected output of the game_of_life_sol::read_model_from_file subroutine + function check_read_model_from_file_with_invalid_model(input) result(result_) + class(input_t), intent(in) :: input + type(result_t) :: result_ + + integer, dimension(:,:), allocatable ::actual_board + character(len=:), allocatable :: actual_io_error_message + + select type (input) + type is (read_model_from_file_in_out_t) + call read_model_from_file(input%input_fname, input%max_nrow, input%max_ncol, actual_board, actual_io_error_message) + + result_ = assert_not(allocated(actual_board)) .and. & + assert_that(allocated(actual_io_error_message)) .and. & + assert_equals(trim(input%expected_io_error_message), trim(actual_io_error_message)) + + class default + result_ = fail("Didn't get read_model_from_file_in_out_t") + + end select + + end function check_read_model_from_file_with_invalid_model + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! Contructors !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -375,29 +435,30 @@ function check_for_steady_state_in_out_constructor(current_board, new_board, ste end function check_for_steady_state_in_out_constructor - function check_evolve_board_in_out_constructor(current_board, expected_new_board) result(check_evolve_board_in_out) + function evolve_board_in_out_constructor(current_board, expected_new_board) result(evolve_board_in_out) integer, dimension(:,:), allocatable, intent(in) :: current_board, expected_new_board - type(check_evolve_board_in_out_t) :: check_evolve_board_in_out + type(evolve_board_in_out_t) :: evolve_board_in_out - check_evolve_board_in_out%current_board = current_board - check_evolve_board_in_out%expected_new_board = expected_new_board - end function check_evolve_board_in_out_constructor + evolve_board_in_out%current_board = current_board + evolve_board_in_out%expected_new_board = expected_new_board + end function evolve_board_in_out_constructor - function check_read_model_from_file_in_out_constructor(input_fname, max_nrow, max_ncol, expected_board, expected_stat) & - result(check_read_model_from_file_in_out) + function read_model_from_file_in_out_constructor( & + input_fname, max_nrow, max_ncol, expected_board, expected_io_error_message) & + result(read_model_from_file_in_out) character(len=:), allocatable, intent(in) :: input_fname integer, intent(in) :: max_nrow integer, intent(in) :: max_ncol - integer, dimension(:,:), allocatable, intent(out) :: expected_board - integer, intent(out) :: expected_stat + integer, dimension(:,:), allocatable, intent(in) :: expected_board + character(len=:), allocatable, intent(in) :: expected_io_error_message - type(check_read_model_from_file_in_out_t) :: check_read_model_from_file_in_out + type(read_model_from_file_in_out_t) :: read_model_from_file_in_out - check_read_model_from_file_in_out%input_fname = input_fname - check_read_model_from_file_in_out%max_nrow = max_nrow - check_read_model_from_file_in_out%max_ncol = max_ncol - check_read_model_from_file_in_out%expected_board = expected_board - check_read_model_from_file_in_out%expected_stat = expected_stat - end function check_read_model_from_file_in_out_constructor + read_model_from_file_in_out%input_fname = input_fname + read_model_from_file_in_out%max_nrow = max_nrow + read_model_from_file_in_out%max_ncol = max_ncol + read_model_from_file_in_out%expected_board = expected_board + read_model_from_file_in_out%expected_io_error_message = expected_io_error_message + end function read_model_from_file_in_out_constructor end module game_of_life_test diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/models/empty_-10_10.dat b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/models/empty_-10_10.dat new file mode 100644 index 0000000..1f467dd --- /dev/null +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/models/empty_-10_10.dat @@ -0,0 +1,2 @@ +nrow ncol + -10 10 diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/models/empty_10_-10.dat b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/models/empty_10_-10.dat new file mode 100644 index 0000000..d79a9bf --- /dev/null +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/models/empty_10_-10.dat @@ -0,0 +1,2 @@ +nrow ncol + 10 -10 From 75fe4562ffe6ba1ae9479b960a305881b01c3a75 Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Wed, 11 Jun 2025 15:00:51 +0100 Subject: [PATCH 19/26] Remove TODO --- .../challenge-1/solution/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md index ea85fec..ef1b79a 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md @@ -22,4 +22,4 @@ There are several issues with this Fortran code which make it hard to unit test. 2. There is a lot of global state used across multiple procedures. This makes tests dependent on one another therefore complicating the management of data/state between tests. -3. TODO: There is a lot of logic not contained within procedures. Wrapping this in a procedure opens up more of the code which can be tested. +3. There is a lot of logic not contained within procedures. Wrapping this in procedures opens up more of the code which can be tested. From 596827ad8dff435e1a45b222d5f0cf4980738790 Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Mon, 16 Jun 2025 14:02:38 +0100 Subject: [PATCH 20/26] Remove sol from repo --- .../challenge-1/solution/fpm.toml | 2 +- .../solution/test/game_of_life_test.f90 | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/fpm.toml b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/fpm.toml index d887ea0..2bd5f9f 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/fpm.toml +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/fpm.toml @@ -10,6 +10,6 @@ source-dir = "src" main = "game_of_life.f90" [[test]] -name = "game_of_life_sol_test" +name = "game_of_life_test" source-dir = "test" main = "main.f90" diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 index d443132..98c8ed1 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 @@ -15,7 +15,7 @@ module game_of_life_test private public :: check_for_steady_state_tests, evolve_board_tests, read_model_from_file_tests - !> Type to bundle inputs and expected outputs of game_of_life_sol::check_for_steady_state + !> Type to bundle inputs and expected outputs of game_of_life::check_for_steady_state type, extends(input_t) :: check_for_steady_state_in_out_t integer, dimension(:,:), allocatable :: current_board, new_board logical :: expected_steady_state @@ -24,7 +24,7 @@ module game_of_life_test module procedure check_for_steady_state_in_out_constructor end interface check_for_steady_state_in_out_t - !> Type to bundle inputs and expected outputs of game_of_life_sol::evolve_board + !> Type to bundle inputs and expected outputs of game_of_life::evolve_board type, extends(input_t) :: evolve_board_in_out_t integer, dimension(:,:), allocatable :: current_board, expected_new_board end type evolve_board_in_out_t @@ -32,7 +32,7 @@ module game_of_life_test module procedure evolve_board_in_out_constructor end interface evolve_board_in_out_t - !> Type to bundle inputs and expected outputs of game_of_life_sol::read_model_from_file + !> Type to bundle inputs and expected outputs of game_of_life::read_model_from_file type, extends(input_t) :: read_model_from_file_in_out_t character(len=:), allocatable :: input_fname integer :: max_nrow @@ -50,7 +50,7 @@ module game_of_life_test ! Test Suites !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !> Test suite for the game_of_life_sol::check_for_steady_state subroutine + !> Test suite for the game_of_life::check_for_steady_state subroutine function check_for_steady_state_tests() result(tests) type(test_item_t) :: tests @@ -105,7 +105,7 @@ function check_for_steady_state_tests() result(tests) deallocate(test_new_board) end function check_for_steady_state_tests - !> Test suite for the game_of_life_sol::evolve_board subroutine + !> Test suite for the game_of_life::evolve_board subroutine function evolve_board_tests() result(tests) type(test_item_t) :: tests @@ -266,7 +266,7 @@ end function read_model_from_file_tests ! Assertion functions !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !> Check for the expected output of the game_of_life_sol::check_for_steady_state subroutine + !> Check for the expected output of the game_of_life::check_for_steady_state subroutine function check_if_steady_state(input) result(result_) class(input_t), intent(in) :: input type(result_t) :: result_ @@ -286,7 +286,7 @@ function check_if_steady_state(input) result(result_) end function check_if_steady_state - !> Check for the expected output of the game_of_life_sol::evolve_board subroutine + !> Check for the expected output of the game_of_life::evolve_board subroutine function check_evolve_board(input) result(result_) class(input_t), intent(in) :: input type(result_t) :: result_ @@ -310,7 +310,7 @@ function check_evolve_board(input) result(result_) end function check_evolve_board - !> Check for the expected output of the game_of_life_sol::read_model_from_file subroutine + !> Check for the expected output of the game_of_life::read_model_from_file subroutine function check_read_model_from_file(input) result(result_) class(input_t), intent(in) :: input type(result_t) :: result_ @@ -335,7 +335,7 @@ function check_read_model_from_file(input) result(result_) end function check_read_model_from_file - !> Check for the expected output of the game_of_life_sol::read_model_from_file subroutine + !> Check for the expected output of the game_of_life::read_model_from_file subroutine function check_read_model_from_file_with_invalid_model(input) result(result_) class(input_t), intent(in) :: input type(result_t) :: result_ From 35f1fe1ee691b8619c7ad87ae923a25e0fa7f57b Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Mon, 16 Jun 2025 15:21:16 +0100 Subject: [PATCH 21/26] Split tests into one file per procedure to be tested --- .../test/check_for_steady_state_test.f90 | 187 +++++++ .../solution/test/evolve_board_test.f90 | 169 +++++++ .../solution/test/game_of_life_test.f90 | 464 ------------------ .../challenge-1/solution/test/main.f90 | 10 +- .../test/read_model_from_file_test.f90 | 171 +++++++ 5 files changed, 533 insertions(+), 468 deletions(-) create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/check_for_steady_state_test.f90 create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/evolve_board_test.f90 delete mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 create mode 100644 episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/read_model_from_file_test.f90 diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/check_for_steady_state_test.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/check_for_steady_state_test.f90 new file mode 100644 index 0000000..4a86656 --- /dev/null +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/check_for_steady_state_test.f90 @@ -0,0 +1,187 @@ +module check_for_steady_state_test + use game_of_life_mod, only : check_for_steady_state + use veggies, only: & + assert_that, & + describe, & + example_t, & + fail, & + input_t, & + it, & + result_t, & + test_item_t + implicit none + + private + public :: check_for_steady_state_test_suite + + !> Type to bundle inputs and expected outputs of game_of_life::check_for_steady_state + type, extends(input_t) :: check_for_steady_state_in_out_t + integer, dimension(:,:), allocatable :: current_board, new_board + logical :: expected_steady_state + end type check_for_steady_state_in_out_t + interface check_for_steady_state_in_out_t + module procedure check_for_steady_state_in_out_constructor + end interface check_for_steady_state_in_out_t + +contains + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! Test Suites + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + !> Test suite for the game_of_life::check_for_steady_state subroutine + function check_for_steady_state_test_suite() result(tests) + type(test_item_t) :: tests + + integer, dimension(:,:), allocatable :: test_current_board, test_new_board + type(example_t) :: matching_boards_data(3), non_matching_boards_data(3) + integer :: nrow, ncol + + nrow = 31 + ncol = 31 + + ! Allocate arrays + allocate(test_current_board(nrow, ncol)) + allocate(test_new_board(nrow, ncol)) + + ! Matching boards + ! All zeros + call setup_matching_boards(test_current_board, test_new_board, 0) + matching_boards_data(1) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .true.)) + ! All ones + call setup_matching_boards(test_current_board, test_new_board, nrow*ncol) + matching_boards_data(2) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .true.)) + ! Up to 10 ones + call setup_matching_boards(test_current_board, test_new_board, 10) + matching_boards_data(3) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .true.)) + + ! Mismatched boards + ! All ones vs all zeros + call setup_mismatched_boards(test_current_board, test_new_board, 0) + non_matching_boards_data(1) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .false.)) + ! All zeros vs all ones + call setup_mismatched_boards(test_current_board, test_new_board, nrow*ncol) + non_matching_boards_data(2) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .false.)) + ! Up to 10 differences + call setup_mismatched_boards(test_current_board, test_new_board, 10) + non_matching_boards_data(3) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .false.)) + + tests = describe( & + "check_for_steady_state", & + [ it( & + "matching boards are in steady state", & + matching_boards_data, & + check_if_steady_state & + ) & + , it( & + "non-matching boards are not in steady state", & + non_matching_boards_data, & + check_if_steady_state & + )] & + ) + + deallocate(test_current_board) + deallocate(test_new_board) + end function check_for_steady_state_test_suite + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! Assertion functions + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + !> Check for the expected output of the game_of_life::check_for_steady_state subroutine + function check_if_steady_state(input) result(result_) + class(input_t), intent(in) :: input + type(result_t) :: result_ + + logical :: actual_steady_state + + select type (input) + type is (check_for_steady_state_in_out_t) + call check_for_steady_state(input%current_board, input%new_board, actual_steady_state) + + result_ = assert_that(input%expected_steady_state .eqv. actual_steady_state) + + class default + result_ = fail("Didn't get check_for_steady_state_in_out_t") + + end select + + end function check_if_steady_state + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! Contructors + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + subroutine setup_matching_boards(current_board, new_board, num_ones) + integer, dimension(:,:), allocatable, intent(inout) :: current_board, new_board + integer, intent(in) :: num_ones + + integer :: nrow, ncol, row, col, rand_row, rand_col + real :: rand_real + + ! Initialise to all zeros + nrow = size(current_board, 1) + ncol = size(current_board, 2) + current_board = 0 + new_board = 0 + + ! For both boards, set to requested number of elements to 1 + do row = 1, num_ones + ! Get random coordinates for both + call random_number(rand_real) + rand_row = 1 + FLOOR(nrow*rand_real) ! n=1 to n=nrow + call random_number(rand_real) + rand_col = 1 + FLOOR(ncol*rand_real) ! n=1 to n=ncol + + current_board(rand_row, rand_col) = 1 + new_board(rand_row, rand_col) = 1 + end do + + end subroutine setup_matching_boards + + subroutine setup_mismatched_boards(current_board, new_board, num_differences) + integer, dimension(:,:), allocatable, intent(inout) :: current_board, new_board + integer, intent(in) :: num_differences + + integer :: nrow, ncol, row, col, rand_row, rand_col + real :: rand_real + + ! Initialise + nrow = size(current_board, 1) + ncol = size(current_board, 2) + current_board = 0 + new_board = 1 + + ! For both boards, set to requested number of elements to the opposite value + do row = 1, num_differences + ! Get random coordinates for current + call random_number(rand_real) + rand_row = 1 + FLOOR(nrow*rand_real) ! n=1 to n=nrow + call random_number(rand_real) + rand_col = 1 + FLOOR(ncol*rand_real) ! n=1 to n=ncol + + current_board(rand_row, rand_col) = 1 + + ! Get random coordinates for new + call random_number(rand_real) + rand_row = 1 + FLOOR(nrow*rand_real) ! n=1 to n=nrow + call random_number(rand_real) + rand_col = 1 + FLOOR(ncol*rand_real) ! n=1 to n=ncol + + new_board(rand_row, rand_col) = 0 + end do + + end subroutine setup_mismatched_boards + + function check_for_steady_state_in_out_constructor(current_board, new_board, steady_state) result(check_for_steady_state_in_out) + integer, dimension(:,:), allocatable, intent(in) :: current_board, new_board + logical, intent(in) :: steady_state + + type(check_for_steady_state_in_out_t) :: check_for_steady_state_in_out + + check_for_steady_state_in_out%current_board = current_board + check_for_steady_state_in_out%new_board = new_board + check_for_steady_state_in_out%expected_steady_state = steady_state + + end function check_for_steady_state_in_out_constructor +end module check_for_steady_state_test diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/evolve_board_test.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/evolve_board_test.f90 new file mode 100644 index 0000000..03aaa89 --- /dev/null +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/evolve_board_test.f90 @@ -0,0 +1,169 @@ +module evolve_board_test + use game_of_life_mod, only : evolve_board + use veggies, only: & + assert_equals, & + describe, & + example_t, & + fail, & + input_t, & + it, & + result_t, & + test_item_t + implicit none + + private + public :: evolve_board_test_suite + + !> Type to bundle inputs and expected outputs of game_of_life::evolve_board + type, extends(input_t) :: evolve_board_in_out_t + integer, dimension(:,:), allocatable :: current_board + integer, dimension(:,:), allocatable :: expected_new_board + end type evolve_board_in_out_t + interface evolve_board_in_out_t + module procedure evolve_board_in_out_constructor + end interface evolve_board_in_out_t + +contains + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! Test Suites + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + !> Test suite for the game_of_life::evolve_board subroutine + function evolve_board_test_suite() result(tests) + type(test_item_t) :: tests + + integer, dimension(:,:), allocatable :: test_current_board, expected_new_board + type(example_t) :: steady_state_boards_data(2), non_steady_state_boards_data(2) + integer :: nrow, ncol + + nrow = 20 + ncol = 20 + + allocate(test_current_board(nrow, ncol)) + allocate(expected_new_board(nrow, ncol)) + test_current_board = 0 + expected_new_board = 0 + + ! Steady state boards + ! All zeros + steady_state_boards_data(1) = example_t(evolve_board_in_out_t(test_current_board, expected_new_board)) + + ! A slightly more complex steady state sructure + ! 8 9 10 11 12 + ! -- -- -- -- -- + ! 8 | 0 0 0 0 0 + ! 9 | 0 0 1 0 0 + ! 10 | 0 1 0 1 0 + ! 11 | 0 1 0 1 0 + ! 12 | 0 0 1 0 0 + ! 13 | 0 0 0 0 0 + ! + ! Input board + test_current_board(9,9:11) = [0,1,0] + test_current_board(10,9:11) = [1,0,1] + test_current_board(11,9:11) = [1,0,1] + test_current_board(12,9:11) = [0,1,0] + ! Expected output board + expected_new_board(9,9:11) = test_current_board(9,9:11) + expected_new_board(10,9:11) = test_current_board(10,9:11) + expected_new_board(11,9:11) = test_current_board(11,9:11) + expected_new_board(12,9:11) = test_current_board(12,9:11) + steady_state_boards_data(2) = example_t(evolve_board_in_out_t(test_current_board, expected_new_board)) + ! Reset for next test + test_current_board = 0 + expected_new_board = 0 + + ! None-steady state boards + ! One non-zero element + ! Input board + test_current_board(10,9) = 1 + non_steady_state_boards_data(1) = example_t(evolve_board_in_out_t(test_current_board, expected_new_board)) + ! Reset for next test + test_current_board(10,9) = 0 + + ! A slightly more complex non-steady state sructure + ! Input board Expected output board + ! 8 9 10 11 12 8 9 10 11 12 + ! -- -- -- -- -- -- -- -- -- -- + ! 8 | 0 0 0 0 0 8 | 0 0 0 0 0 + ! 9 | 0 0 1 0 0 \ 9 | 0 1 1 1 0 + ! 10 | 0 1 1 1 0 ---\ 10 | 0 1 0 1 0 + ! 11 | 0 1 0 1 0 ---/ 11 | 0 1 0 1 0 + ! 12 | 0 0 1 0 0 / 12 | 0 0 1 0 0 + ! 13 | 0 0 0 0 0 13 | 0 0 0 0 0 + ! + ! Input board + test_current_board(9,9:11) = [0,1,0] + test_current_board(10,9:11) = [1,1,1] + test_current_board(11,9:11) = [1,0,1] + test_current_board(12,9:11) = [0,1,0] + ! Expected output board + expected_new_board(9,9:11) = [1,1,1] + expected_new_board(10,9:11) = [1,0,1] + expected_new_board(11,9:11) = [1,0,1] + expected_new_board(12,9:11) = [0,1,0] + non_steady_state_boards_data(2) = example_t(evolve_board_in_out_t(test_current_board, expected_new_board)) + ! Reset for next test + test_current_board = 0 + expected_new_board = 0 + + tests = describe( & + "evolve_board", & + [ it( & + "a board in steady state does not change", & + steady_state_boards_data, & + check_evolve_board & + ) & + , it( & + "a board not in steady state will change", & + non_steady_state_boards_data, & + check_evolve_board & + )] & + ) + + deallocate(test_current_board) + deallocate(expected_new_board) + end function evolve_board_test_suite + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! Assertion functions + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + !> Check for the expected output of the game_of_life::evolve_board subroutine + function check_evolve_board(input) result(result_) + class(input_t), intent(in) :: input + type(result_t) :: result_ + + integer, dimension(:,:), allocatable ::actual_new_board + + select type (input) + type is (evolve_board_in_out_t) + allocate(actual_new_board(size(input%current_board, 1), size(input%current_board, 2))) + actual_new_board = input%current_board + + call evolve_board(input%current_board, actual_new_board) + + result_ = assert_equals(input%expected_new_board, actual_new_board) + + deallocate(actual_new_board) + class default + result_ = fail("Didn't get evolve_board_in_out_t") + + end select + + end function check_evolve_board + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! Contructors + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + function evolve_board_in_out_constructor(current_board, expected_new_board) result(evolve_board_in_out) + integer, dimension(:,:), allocatable, intent(in) :: current_board, expected_new_board + + type(evolve_board_in_out_t) :: evolve_board_in_out + + evolve_board_in_out%current_board = current_board + evolve_board_in_out%expected_new_board = expected_new_board + end function evolve_board_in_out_constructor +end module evolve_board_test diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 deleted file mode 100644 index 98c8ed1..0000000 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/game_of_life_test.f90 +++ /dev/null @@ -1,464 +0,0 @@ -module game_of_life_test - use game_of_life_mod, only : check_for_steady_state, evolve_board, read_model_from_file - use veggies, only: & - assert_that, & - assert_equals, & - describe, & - example_t, & - fail, & - input_t, & - it, & - result_t, & - test_item_t, assert_not - implicit none - - private - public :: check_for_steady_state_tests, evolve_board_tests, read_model_from_file_tests - - !> Type to bundle inputs and expected outputs of game_of_life::check_for_steady_state - type, extends(input_t) :: check_for_steady_state_in_out_t - integer, dimension(:,:), allocatable :: current_board, new_board - logical :: expected_steady_state - end type check_for_steady_state_in_out_t - interface check_for_steady_state_in_out_t - module procedure check_for_steady_state_in_out_constructor - end interface check_for_steady_state_in_out_t - - !> Type to bundle inputs and expected outputs of game_of_life::evolve_board - type, extends(input_t) :: evolve_board_in_out_t - integer, dimension(:,:), allocatable :: current_board, expected_new_board - end type evolve_board_in_out_t - interface evolve_board_in_out_t - module procedure evolve_board_in_out_constructor - end interface evolve_board_in_out_t - - !> Type to bundle inputs and expected outputs of game_of_life::read_model_from_file - type, extends(input_t) :: read_model_from_file_in_out_t - character(len=:), allocatable :: input_fname - integer :: max_nrow - integer :: max_ncol - integer, dimension(:,:), allocatable :: expected_board - character(len=:), allocatable :: expected_io_error_message - end type read_model_from_file_in_out_t - interface read_model_from_file_in_out_t - module procedure read_model_from_file_in_out_constructor - end interface read_model_from_file_in_out_t - -contains - - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - ! Test Suites - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - - !> Test suite for the game_of_life::check_for_steady_state subroutine - function check_for_steady_state_tests() result(tests) - type(test_item_t) :: tests - - integer, dimension(:,:), allocatable :: test_current_board, test_new_board - type(example_t) :: matching_boards_data(3), non_matching_boards_data(3) - integer :: nrow, ncol - - nrow = 31 - ncol = 31 - - ! Allocate arrays - allocate(test_current_board(nrow, ncol)) - allocate(test_new_board(nrow, ncol)) - - ! Matching boards - ! All zeros - call setup_matching_boards(test_current_board, test_new_board, 0) - matching_boards_data(1) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .true.)) - ! All ones - call setup_matching_boards(test_current_board, test_new_board, nrow*ncol) - matching_boards_data(2) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .true.)) - ! Up to 10 ones - call setup_matching_boards(test_current_board, test_new_board, 10) - matching_boards_data(3) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .true.)) - - ! Mismatched boards - ! All ones vs all zeros - call setup_mismatched_boards(test_current_board, test_new_board, 0) - non_matching_boards_data(1) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .false.)) - ! All zeros vs all ones - call setup_mismatched_boards(test_current_board, test_new_board, nrow*ncol) - non_matching_boards_data(2) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .false.)) - ! Up to 10 differences - call setup_mismatched_boards(test_current_board, test_new_board, 10) - non_matching_boards_data(3) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .false.)) - - tests = describe( & - "check_for_steady_state", & - [ it( & - "matching boards are in steady state", & - matching_boards_data, & - check_if_steady_state & - ) & - , it( & - "non-matching boards are not in steady state", & - non_matching_boards_data, & - check_if_steady_state & - )] & - ) - - deallocate(test_current_board) - deallocate(test_new_board) - end function check_for_steady_state_tests - - !> Test suite for the game_of_life::evolve_board subroutine - function evolve_board_tests() result(tests) - type(test_item_t) :: tests - - integer, dimension(:,:), allocatable :: test_current_board, expected_new_board - type(example_t) :: steady_state_boards_data(2), non_steady_state_boards_data(2) - integer :: nrow, ncol - - nrow = 20 - ncol = 20 - - allocate(test_current_board(nrow, ncol)) - allocate(expected_new_board(nrow, ncol)) - test_current_board = 0 - expected_new_board = 0 - - ! Steady state boards - ! All zeros - steady_state_boards_data(1) = example_t(evolve_board_in_out_t(test_current_board, expected_new_board)) - - ! A slightly more complex steady state sructure - ! 8 9 10 11 12 - ! -- -- -- -- -- - ! 8 | 0 0 0 0 0 - ! 9 | 0 0 1 0 0 - ! 10 | 0 1 0 1 0 - ! 11 | 0 1 0 1 0 - ! 12 | 0 0 1 0 0 - ! 13 | 0 0 0 0 0 - ! - ! Input board - test_current_board(9,9:11) = [0,1,0] - test_current_board(10,9:11) = [1,0,1] - test_current_board(11,9:11) = [1,0,1] - test_current_board(12,9:11) = [0,1,0] - ! Expected output board - expected_new_board(9,9:11) = test_current_board(9,9:11) - expected_new_board(10,9:11) = test_current_board(10,9:11) - expected_new_board(11,9:11) = test_current_board(11,9:11) - expected_new_board(12,9:11) = test_current_board(12,9:11) - steady_state_boards_data(2) = example_t(evolve_board_in_out_t(test_current_board, expected_new_board)) - ! Reset for next test - test_current_board = 0 - expected_new_board = 0 - - ! None-steady state boards - ! One non-zero element - ! Input board - test_current_board(10,9) = 1 - non_steady_state_boards_data(1) = example_t(evolve_board_in_out_t(test_current_board, expected_new_board)) - ! Reset for next test - test_current_board(10,9) = 0 - - ! A slightly more complex non-steady state sructure - ! Input board Expected output board - ! 8 9 10 11 12 8 9 10 11 12 - ! -- -- -- -- -- -- -- -- -- -- - ! 8 | 0 0 0 0 0 8 | 0 0 0 0 0 - ! 9 | 0 0 1 0 0 \ 9 | 0 1 1 1 0 - ! 10 | 0 1 1 1 0 ---\ 10 | 0 1 0 1 0 - ! 11 | 0 1 0 1 0 ---/ 11 | 0 1 0 1 0 - ! 12 | 0 0 1 0 0 / 12 | 0 0 1 0 0 - ! 13 | 0 0 0 0 0 13 | 0 0 0 0 0 - ! - ! Input board - test_current_board(9,9:11) = [0,1,0] - test_current_board(10,9:11) = [1,1,1] - test_current_board(11,9:11) = [1,0,1] - test_current_board(12,9:11) = [0,1,0] - ! Expected output board - expected_new_board(9,9:11) = [1,1,1] - expected_new_board(10,9:11) = [1,0,1] - expected_new_board(11,9:11) = [1,0,1] - expected_new_board(12,9:11) = [0,1,0] - non_steady_state_boards_data(2) = example_t(evolve_board_in_out_t(test_current_board, expected_new_board)) - ! Reset for next test - test_current_board = 0 - expected_new_board = 0 - - tests = describe( & - "evolve_board", & - [ it( & - "a board in steady state does not change", & - steady_state_boards_data, & - check_evolve_board & - ) & - , it( & - "a board not in steady state will change", & - non_steady_state_boards_data, & - check_evolve_board & - )] & - ) - - deallocate(test_current_board) - deallocate(expected_new_board) - end function evolve_board_tests - - function read_model_from_file_tests() result(tests) - type(test_item_t) :: tests - - integer, dimension(:,:), allocatable :: test_board - character(len=:), allocatable :: test_io_error_message - type(example_t) :: valid_model_file_data(1), invalid_model_file_data(5) - - allocate(test_board(31, 31)) - test_board = 0 - - valid_model_file_data(1) = example_t( & - read_model_from_file_in_out_t("test/models/zeros_31_31.dat", 100, 100, test_board, test_io_error_message) & - ) - - deallocate(test_board) - - allocate(character(100) :: test_io_error_message) - - test_io_error_message = "nrow must be a positive integer less than 10 found 31" - invalid_model_file_data(1) = example_t( & - read_model_from_file_in_out_t("test/models/zeros_31_31.dat", 10, 100, test_board, test_io_error_message) & - ) - - test_io_error_message = "ncol must be a positive integer less than 10 found 31" - invalid_model_file_data(2) = example_t( & - read_model_from_file_in_out_t("test/models/zeros_31_31.dat", 100, 10, test_board, test_io_error_message) & - ) - - test_io_error_message = "nrow must be a positive integer less than 100 found -10" - invalid_model_file_data(3) = example_t( & - read_model_from_file_in_out_t("test/models/empty_-10_10.dat", 100, 100, test_board, test_io_error_message) & - ) - - test_io_error_message = "ncol must be a positive integer less than 100 found -10" - invalid_model_file_data(4) = example_t( & - read_model_from_file_in_out_t("test/models/empty_10_-10.dat", 100, 100, test_board, test_io_error_message) & - ) - - test_io_error_message = " *** Error when opening does/not/exist.dat" - invalid_model_file_data(5) = example_t( & - read_model_from_file_in_out_t("does/not/exist.dat", 100, 100, test_board, test_io_error_message) & - ) - - deallocate(test_io_error_message) - - tests = describe( & - "read_model_from_file", & - [ it( & - "a valid model file is loaded successfully", & - valid_model_file_data, & - check_read_model_from_file & - ), & - it( & - "a invalid model file is loaded unsuccessfully", & - invalid_model_file_data, & - check_read_model_from_file_with_invalid_model & - )] & - ) - end function read_model_from_file_tests - - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - ! Assertion functions - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - - !> Check for the expected output of the game_of_life::check_for_steady_state subroutine - function check_if_steady_state(input) result(result_) - class(input_t), intent(in) :: input - type(result_t) :: result_ - - logical :: actual_steady_state - - select type (input) - type is (check_for_steady_state_in_out_t) - call check_for_steady_state(input%current_board, input%new_board, actual_steady_state) - - result_ = assert_that(input%expected_steady_state .eqv. actual_steady_state) - - class default - result_ = fail("Didn't get check_for_steady_state_in_out_t") - - end select - - end function check_if_steady_state - - !> Check for the expected output of the game_of_life::evolve_board subroutine - function check_evolve_board(input) result(result_) - class(input_t), intent(in) :: input - type(result_t) :: result_ - - integer, dimension(:,:), allocatable ::actual_new_board - - select type (input) - type is (evolve_board_in_out_t) - allocate(actual_new_board(size(input%current_board, 1), size(input%current_board, 2))) - actual_new_board = input%current_board - - call evolve_board(input%current_board, actual_new_board) - - result_ = assert_equals(input%expected_new_board, actual_new_board) - - deallocate(actual_new_board) - class default - result_ = fail("Didn't get evolve_board_in_out_t") - - end select - - end function check_evolve_board - - !> Check for the expected output of the game_of_life::read_model_from_file subroutine - function check_read_model_from_file(input) result(result_) - class(input_t), intent(in) :: input - type(result_t) :: result_ - - integer, dimension(:,:), allocatable ::actual_board - character(len=:), allocatable :: actual_io_error_message - - select type (input) - type is (read_model_from_file_in_out_t) - call read_model_from_file(input%input_fname, input%max_nrow, input%max_ncol, actual_board, actual_io_error_message) - - result_ = assert_equals(input%expected_board, actual_board) .and. & - assert_not(allocated(actual_io_error_message)) - - if (allocated(actual_board)) then - deallocate(actual_board) - end if - class default - result_ = fail("Didn't get read_model_from_file_in_out_t") - - end select - - end function check_read_model_from_file - - !> Check for the expected output of the game_of_life::read_model_from_file subroutine - function check_read_model_from_file_with_invalid_model(input) result(result_) - class(input_t), intent(in) :: input - type(result_t) :: result_ - - integer, dimension(:,:), allocatable ::actual_board - character(len=:), allocatable :: actual_io_error_message - - select type (input) - type is (read_model_from_file_in_out_t) - call read_model_from_file(input%input_fname, input%max_nrow, input%max_ncol, actual_board, actual_io_error_message) - - result_ = assert_not(allocated(actual_board)) .and. & - assert_that(allocated(actual_io_error_message)) .and. & - assert_equals(trim(input%expected_io_error_message), trim(actual_io_error_message)) - - class default - result_ = fail("Didn't get read_model_from_file_in_out_t") - - end select - - end function check_read_model_from_file_with_invalid_model - - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - ! Contructors - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - - subroutine setup_matching_boards(current_board, new_board, num_ones) - integer, dimension(:,:), allocatable, intent(inout) :: current_board, new_board - integer, intent(in) :: num_ones - - integer :: nrow, ncol, row, col, rand_row, rand_col - real :: rand_real - - ! Initialise to all zeros - nrow = size(current_board, 1) - ncol = size(current_board, 2) - current_board = 0 - new_board = 0 - - ! For both boards, set to requested number of elements to 1 - do row = 1, num_ones - ! Get random coordinates for both - call random_number(rand_real) - rand_row = 1 + FLOOR(nrow*rand_real) ! n=1 to n=nrow - call random_number(rand_real) - rand_col = 1 + FLOOR(ncol*rand_real) ! n=1 to n=ncol - - current_board(rand_row, rand_col) = 1 - new_board(rand_row, rand_col) = 1 - end do - - end subroutine setup_matching_boards - - subroutine setup_mismatched_boards(current_board, new_board, num_differences) - integer, dimension(:,:), allocatable, intent(inout) :: current_board, new_board - integer, intent(in) :: num_differences - - integer :: nrow, ncol, row, col, rand_row, rand_col - real :: rand_real - - ! Initialise - nrow = size(current_board, 1) - ncol = size(current_board, 2) - current_board = 0 - new_board = 1 - - ! For both boards, set to requested number of elements to the opposite value - do row = 1, num_differences - ! Get random coordinates for current - call random_number(rand_real) - rand_row = 1 + FLOOR(nrow*rand_real) ! n=1 to n=nrow - call random_number(rand_real) - rand_col = 1 + FLOOR(ncol*rand_real) ! n=1 to n=ncol - - current_board(rand_row, rand_col) = 1 - - ! Get random coordinates for new - call random_number(rand_real) - rand_row = 1 + FLOOR(nrow*rand_real) ! n=1 to n=nrow - call random_number(rand_real) - rand_col = 1 + FLOOR(ncol*rand_real) ! n=1 to n=ncol - - new_board(rand_row, rand_col) = 0 - end do - - end subroutine setup_mismatched_boards - - function check_for_steady_state_in_out_constructor(current_board, new_board, steady_state) result(check_for_steady_state_in_out) - integer, dimension(:,:), allocatable, intent(in) :: current_board, new_board - logical, intent(in) :: steady_state - - type(check_for_steady_state_in_out_t) :: check_for_steady_state_in_out - - check_for_steady_state_in_out%current_board = current_board - check_for_steady_state_in_out%new_board = new_board - check_for_steady_state_in_out%expected_steady_state = steady_state - - end function check_for_steady_state_in_out_constructor - - function evolve_board_in_out_constructor(current_board, expected_new_board) result(evolve_board_in_out) - integer, dimension(:,:), allocatable, intent(in) :: current_board, expected_new_board - - type(evolve_board_in_out_t) :: evolve_board_in_out - - evolve_board_in_out%current_board = current_board - evolve_board_in_out%expected_new_board = expected_new_board - end function evolve_board_in_out_constructor - - function read_model_from_file_in_out_constructor( & - input_fname, max_nrow, max_ncol, expected_board, expected_io_error_message) & - result(read_model_from_file_in_out) - character(len=:), allocatable, intent(in) :: input_fname - integer, intent(in) :: max_nrow - integer, intent(in) :: max_ncol - integer, dimension(:,:), allocatable, intent(in) :: expected_board - character(len=:), allocatable, intent(in) :: expected_io_error_message - - type(read_model_from_file_in_out_t) :: read_model_from_file_in_out - - read_model_from_file_in_out%input_fname = input_fname - read_model_from_file_in_out%max_nrow = max_nrow - read_model_from_file_in_out%max_ncol = max_ncol - read_model_from_file_in_out%expected_board = expected_board - read_model_from_file_in_out%expected_io_error_message = expected_io_error_message - end function read_model_from_file_in_out_constructor -end module game_of_life_test diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/main.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/main.f90 index 8a62a1f..3a71a58 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/main.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/main.f90 @@ -1,7 +1,9 @@ program test_main use veggies, only : test_item_t, test_that, run_tests - use game_of_life_test, only : check_for_steady_state_tests, evolve_board_tests, read_model_from_file_tests + use check_for_steady_state_test, only : check_for_steady_state_test_suite + use evolve_board_test, only : evolve_board_test_suite + use read_model_from_file_test, only : read_model_from_file_test_suite implicit none @@ -14,9 +16,9 @@ function run() result(passed) type(test_item_t) :: tests type(test_item_t) :: individual_tests(3) - individual_tests(1) = check_for_steady_state_tests() - individual_tests(2) = evolve_board_tests() - individual_tests(3) = read_model_from_file_tests() + individual_tests(1) = check_for_steady_state_test_suite() + individual_tests(2) = evolve_board_test_suite() + individual_tests(3) = read_model_from_file_test_suite() tests = test_that(individual_tests) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/read_model_from_file_test.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/read_model_from_file_test.f90 new file mode 100644 index 0000000..7a029fb --- /dev/null +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/read_model_from_file_test.f90 @@ -0,0 +1,171 @@ +module read_model_from_file_test + use game_of_life_mod, only : read_model_from_file + use veggies, only: & + assert_that, & + assert_equals, & + describe, & + example_t, & + fail, & + input_t, & + it, & + result_t, & + test_item_t, & + assert_not + implicit none + + private + public :: read_model_from_file_test_suite + + !> Type to bundle inputs and expected outputs of game_of_life::read_model_from_file + type, extends(input_t) :: read_model_from_file_in_out_t + character(len=:), allocatable :: input_fname + integer :: max_nrow + integer :: max_ncol + integer, dimension(:,:), allocatable :: expected_board + character(len=:), allocatable :: expected_io_error_message + end type read_model_from_file_in_out_t + interface read_model_from_file_in_out_t + module procedure read_model_from_file_in_out_constructor + end interface read_model_from_file_in_out_t + +contains + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! Test Suites + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + !> Test suite for the game_of_life::read_model_from_file subroutine + function read_model_from_file_test_suite() result(tests) + type(test_item_t) :: tests + + integer, dimension(:,:), allocatable :: test_board + character(len=:), allocatable :: test_io_error_message + type(example_t) :: valid_model_file_data(1), invalid_model_file_data(5) + + allocate(test_board(31, 31)) + test_board = 0 + + valid_model_file_data(1) = example_t( & + read_model_from_file_in_out_t("test/models/zeros_31_31.dat", 100, 100, test_board, test_io_error_message) & + ) + + deallocate(test_board) + + allocate(character(100) :: test_io_error_message) + + test_io_error_message = "nrow must be a positive integer less than 10 found 31" + invalid_model_file_data(1) = example_t( & + read_model_from_file_in_out_t("test/models/zeros_31_31.dat", 10, 100, test_board, test_io_error_message) & + ) + + test_io_error_message = "ncol must be a positive integer less than 10 found 31" + invalid_model_file_data(2) = example_t( & + read_model_from_file_in_out_t("test/models/zeros_31_31.dat", 100, 10, test_board, test_io_error_message) & + ) + + test_io_error_message = "nrow must be a positive integer less than 100 found -10" + invalid_model_file_data(3) = example_t( & + read_model_from_file_in_out_t("test/models/empty_-10_10.dat", 100, 100, test_board, test_io_error_message) & + ) + + test_io_error_message = "ncol must be a positive integer less than 100 found -10" + invalid_model_file_data(4) = example_t( & + read_model_from_file_in_out_t("test/models/empty_10_-10.dat", 100, 100, test_board, test_io_error_message) & + ) + + test_io_error_message = " *** Error when opening does/not/exist.dat" + invalid_model_file_data(5) = example_t( & + read_model_from_file_in_out_t("does/not/exist.dat", 100, 100, test_board, test_io_error_message) & + ) + + deallocate(test_io_error_message) + + tests = describe( & + "read_model_from_file", & + [ it( & + "a valid model file is loaded successfully", & + valid_model_file_data, & + check_read_model_from_file & + ), & + it( & + "a invalid model file is loaded unsuccessfully", & + invalid_model_file_data, & + check_read_model_from_file_with_invalid_model & + )] & + ) + end function read_model_from_file_test_suite + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! Assertion functions + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + !> Check for the expected output of the game_of_life::read_model_from_file subroutine + function check_read_model_from_file(input) result(result_) + class(input_t), intent(in) :: input + type(result_t) :: result_ + + integer, dimension(:,:), allocatable ::actual_board + character(len=:), allocatable :: actual_io_error_message + + select type (input) + type is (read_model_from_file_in_out_t) + call read_model_from_file(input%input_fname, input%max_nrow, input%max_ncol, actual_board, actual_io_error_message) + + result_ = assert_equals(input%expected_board, actual_board) .and. & + assert_not(allocated(actual_io_error_message)) + + if (allocated(actual_board)) then + deallocate(actual_board) + end if + class default + result_ = fail("Didn't get read_model_from_file_in_out_t") + + end select + + end function check_read_model_from_file + + !> Check for the expected output of the game_of_life::read_model_from_file subroutine + function check_read_model_from_file_with_invalid_model(input) result(result_) + class(input_t), intent(in) :: input + type(result_t) :: result_ + + integer, dimension(:,:), allocatable ::actual_board + character(len=:), allocatable :: actual_io_error_message + + select type (input) + type is (read_model_from_file_in_out_t) + call read_model_from_file(input%input_fname, input%max_nrow, input%max_ncol, actual_board, actual_io_error_message) + + result_ = assert_not(allocated(actual_board)) .and. & + assert_that(allocated(actual_io_error_message)) .and. & + assert_equals(trim(input%expected_io_error_message), trim(actual_io_error_message)) + + class default + result_ = fail("Didn't get read_model_from_file_in_out_t") + + end select + + end function check_read_model_from_file_with_invalid_model + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! Contructors + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + function read_model_from_file_in_out_constructor( & + input_fname, max_nrow, max_ncol, expected_board, expected_io_error_message) & + result(read_model_from_file_in_out) + character(len=:), allocatable, intent(in) :: input_fname + integer, intent(in) :: max_nrow + integer, intent(in) :: max_ncol + integer, dimension(:,:), allocatable, intent(in) :: expected_board + character(len=:), allocatable, intent(in) :: expected_io_error_message + + type(read_model_from_file_in_out_t) :: read_model_from_file_in_out + + read_model_from_file_in_out%input_fname = input_fname + read_model_from_file_in_out%max_nrow = max_nrow + read_model_from_file_in_out%max_ncol = max_ncol + read_model_from_file_in_out%expected_board = expected_board + read_model_from_file_in_out%expected_io_error_message = expected_io_error_message + end function read_model_from_file_in_out_constructor +end module read_model_from_file_test From 57f6054da6a8d13c5589e11c407e9a85be962f4e Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Wed, 18 Jun 2025 18:01:08 +0100 Subject: [PATCH 22/26] Move challenge into a challenge dir --- .../challenge-1/{ => challenge}/README.md | 0 .../challenge-1/{ => challenge}/fpm.toml | 0 .../challenge-1/{ => challenge}/src/game_of_life.f90 | 0 .../challenge-1/solution/README.md | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) rename episodes/2-intro-to-fortran-unit-tests/challenge-1/{ => challenge}/README.md (100%) rename episodes/2-intro-to-fortran-unit-tests/challenge-1/{ => challenge}/fpm.toml (100%) rename episodes/2-intro-to-fortran-unit-tests/challenge-1/{ => challenge}/src/game_of_life.f90 (100%) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/README.md b/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/README.md similarity index 100% rename from episodes/2-intro-to-fortran-unit-tests/challenge-1/README.md rename to episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/README.md diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml b/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/fpm.toml similarity index 100% rename from episodes/2-intro-to-fortran-unit-tests/challenge-1/fpm.toml rename to episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/fpm.toml diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/src/game_of_life.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/src/game_of_life.f90 similarity index 100% rename from episodes/2-intro-to-fortran-unit-tests/challenge-1/src/game_of_life.f90 rename to episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/src/game_of_life.f90 diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md index ef1b79a..4e96d15 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md @@ -1,6 +1,6 @@ # Episode 2 - Challenge 1 - Solution: Identify bad practice for unit testing Fortran -The solution provided here is an entirely self-contained project which can be built using FPM. +This solution can be built using FPM. ```bash fpm build From 54a476f7634a79c9b260dca82187368996873702 Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Wed, 18 Jun 2025 18:04:47 +0100 Subject: [PATCH 23/26] Rename tests, fix src and remove constructors --- .../challenge-1/challenge/fpm.toml | 4 - .../challenge/src/game_of_life.f90 | 3 +- .../challenge-1/solution/README.md | 2 +- .../challenge-1/solution/src/game_of_life.f90 | 2 + ...st.f90 => test_check_for_steady_state.f90} | 92 +++++++------------ ...e_board_test.f90 => test_evolve_board.f90} | 17 +--- ...test.f90 => test_read_model_from_file.f90} | 0 7 files changed, 40 insertions(+), 80 deletions(-) rename episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/{check_for_steady_state_test.f90 => test_check_for_steady_state.f90} (62%) rename episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/{evolve_board_test.f90 => test_evolve_board.f90} (88%) rename episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/{read_model_from_file_test.f90 => test_read_model_from_file.f90} (100%) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/fpm.toml b/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/fpm.toml index 4b5ea0c..9d892f5 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/fpm.toml +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/fpm.toml @@ -1,9 +1,5 @@ name = "episode-2-challenge-1" -[dev-dependencies] -veggies.git = "https://gitlab.com/everythingfunctional/veggies" -veggies.tag = "main" - [[executable]] name = "game-of-life" source-dir = "src" diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/src/game_of_life.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/src/game_of_life.f90 index 8392365..8d5e8a3 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/src/game_of_life.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/src/game_of_life.f90 @@ -81,6 +81,7 @@ program game_of_life close(input_file_io) new_board = 0 + generation_number = 0 ! Clear the terminal screen call system ("clear") @@ -92,8 +93,8 @@ program game_of_life mod_ms_step = mod(date_time_values(8), ms_per_step) if (mod_ms_step == 0) then - call check_for_steady_state() call evolve_board() + call check_for_steady_state() current_board = new_board call draw_board() diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md index 4e96d15..ef1b79a 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md @@ -1,6 +1,6 @@ # Episode 2 - Challenge 1 - Solution: Identify bad practice for unit testing Fortran -This solution can be built using FPM. +The solution provided here is an entirely self-contained project which can be built using FPM. ```bash fpm build diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life.f90 index dcb41ab..0febb6c 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life.f90 @@ -54,6 +54,8 @@ program game_of_life allocate(new_board(size(current_board,1), size(current_board, 2))) new_board = 0 + generation_number = 0 + ! Clear the terminal screen call system ("clear") diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/check_for_steady_state_test.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/test_check_for_steady_state.f90 similarity index 62% rename from episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/check_for_steady_state_test.f90 rename to episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/test_check_for_steady_state.f90 index 4a86656..cf4a236 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/check_for_steady_state_test.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/test_check_for_steady_state.f90 @@ -1,6 +1,8 @@ +!> Module for testing the subroutine game_of_life::check_for_steady_state module check_for_steady_state_test use game_of_life_mod, only : check_for_steady_state use veggies, only: & + assert_not, & assert_that, & describe, & example_t, & @@ -19,9 +21,6 @@ module check_for_steady_state_test integer, dimension(:,:), allocatable :: current_board, new_board logical :: expected_steady_state end type check_for_steady_state_in_out_t - interface check_for_steady_state_in_out_t - module procedure check_for_steady_state_in_out_constructor - end interface check_for_steady_state_in_out_t contains @@ -46,24 +45,24 @@ function check_for_steady_state_test_suite() result(tests) ! Matching boards ! All zeros - call setup_matching_boards(test_current_board, test_new_board, 0) + call populate_random_boards(test_current_board, test_new_board, 0, .true.) matching_boards_data(1) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .true.)) ! All ones - call setup_matching_boards(test_current_board, test_new_board, nrow*ncol) + call populate_random_boards(test_current_board, test_new_board, nrow*ncol, .true.) matching_boards_data(2) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .true.)) ! Up to 10 ones - call setup_matching_boards(test_current_board, test_new_board, 10) + call populate_random_boards(test_current_board, test_new_board, 10, .true.) matching_boards_data(3) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .true.)) ! Mismatched boards ! All ones vs all zeros - call setup_mismatched_boards(test_current_board, test_new_board, 0) + call populate_random_boards(test_current_board, test_new_board, 0, .false.) non_matching_boards_data(1) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .false.)) ! All zeros vs all ones - call setup_mismatched_boards(test_current_board, test_new_board, nrow*ncol) + call populate_random_boards(test_current_board, test_new_board, nrow*ncol, .false.) non_matching_boards_data(2) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .false.)) ! Up to 10 differences - call setup_mismatched_boards(test_current_board, test_new_board, 10) + call populate_random_boards(test_current_board, test_new_board, 10, .false.) non_matching_boards_data(3) = example_t(check_for_steady_state_in_out_t(test_current_board, test_new_board, .false.)) tests = describe( & @@ -99,7 +98,11 @@ function check_if_steady_state(input) result(result_) type is (check_for_steady_state_in_out_t) call check_for_steady_state(input%current_board, input%new_board, actual_steady_state) - result_ = assert_that(input%expected_steady_state .eqv. actual_steady_state) + if (input%expected_steady_state) then + result_ = assert_that(actual_steady_state) + else + result_ = assert_not(actual_steady_state) + end if class default result_ = fail("Didn't get check_for_steady_state_in_out_t") @@ -112,45 +115,24 @@ end function check_if_steady_state ! Contructors !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - subroutine setup_matching_boards(current_board, new_board, num_ones) - integer, dimension(:,:), allocatable, intent(inout) :: current_board, new_board - integer, intent(in) :: num_ones - - integer :: nrow, ncol, row, col, rand_row, rand_col - real :: rand_real - - ! Initialise to all zeros - nrow = size(current_board, 1) - ncol = size(current_board, 2) - current_board = 0 - new_board = 0 - - ! For both boards, set to requested number of elements to 1 - do row = 1, num_ones - ! Get random coordinates for both - call random_number(rand_real) - rand_row = 1 + FLOOR(nrow*rand_real) ! n=1 to n=nrow - call random_number(rand_real) - rand_col = 1 + FLOOR(ncol*rand_real) ! n=1 to n=ncol - - current_board(rand_row, rand_col) = 1 - new_board(rand_row, rand_col) = 1 - end do - - end subroutine setup_matching_boards - - subroutine setup_mismatched_boards(current_board, new_board, num_differences) + subroutine populate_random_boards(current_board, new_board, num_differences, matching) integer, dimension(:,:), allocatable, intent(inout) :: current_board, new_board integer, intent(in) :: num_differences + logical, intent(in) :: matching - integer :: nrow, ncol, row, col, rand_row, rand_col + integer :: nrow, ncol, row, col, rand_row, rand_col, new_board_val real :: rand_real ! Initialise nrow = size(current_board, 1) ncol = size(current_board, 2) current_board = 0 - new_board = 1 + + if (matching) then + new_board = 0 + else + new_board = 1 + end if ! For both boards, set to requested number of elements to the opposite value do row = 1, num_differences @@ -162,26 +144,20 @@ subroutine setup_mismatched_boards(current_board, new_board, num_differences) current_board(rand_row, rand_col) = 1 - ! Get random coordinates for new - call random_number(rand_real) - rand_row = 1 + FLOOR(nrow*rand_real) ! n=1 to n=nrow - call random_number(rand_real) - rand_col = 1 + FLOOR(ncol*rand_real) ! n=1 to n=ncol - - new_board(rand_row, rand_col) = 0 - end do - - end subroutine setup_mismatched_boards + if (.not. matching) then + ! Get random coordinates for new + call random_number(rand_real) + rand_row = 1 + FLOOR(nrow*rand_real) ! n=1 to n=nrow + call random_number(rand_real) + rand_col = 1 + FLOOR(ncol*rand_real) ! n=1 to n=ncol - function check_for_steady_state_in_out_constructor(current_board, new_board, steady_state) result(check_for_steady_state_in_out) - integer, dimension(:,:), allocatable, intent(in) :: current_board, new_board - logical, intent(in) :: steady_state + new_board(rand_row, rand_col) = 0 + else + new_board(rand_row, rand_col) = 1 + end if - type(check_for_steady_state_in_out_t) :: check_for_steady_state_in_out - check_for_steady_state_in_out%current_board = current_board - check_for_steady_state_in_out%new_board = new_board - check_for_steady_state_in_out%expected_steady_state = steady_state + end do - end function check_for_steady_state_in_out_constructor + end subroutine populate_random_boards end module check_for_steady_state_test diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/evolve_board_test.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/test_evolve_board.f90 similarity index 88% rename from episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/evolve_board_test.f90 rename to episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/test_evolve_board.f90 index 03aaa89..1c79d4f 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/evolve_board_test.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/test_evolve_board.f90 @@ -1,3 +1,4 @@ +!> Module for testing the subroutine game_of_life::evolve_board module evolve_board_test use game_of_life_mod, only : evolve_board use veggies, only: & @@ -19,9 +20,6 @@ module evolve_board_test integer, dimension(:,:), allocatable :: current_board integer, dimension(:,:), allocatable :: expected_new_board end type evolve_board_in_out_t - interface evolve_board_in_out_t - module procedure evolve_board_in_out_constructor - end interface evolve_board_in_out_t contains @@ -153,17 +151,4 @@ function check_evolve_board(input) result(result_) end select end function check_evolve_board - - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - ! Contructors - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - - function evolve_board_in_out_constructor(current_board, expected_new_board) result(evolve_board_in_out) - integer, dimension(:,:), allocatable, intent(in) :: current_board, expected_new_board - - type(evolve_board_in_out_t) :: evolve_board_in_out - - evolve_board_in_out%current_board = current_board - evolve_board_in_out%expected_new_board = expected_new_board - end function evolve_board_in_out_constructor end module evolve_board_test diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/read_model_from_file_test.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/test_read_model_from_file.f90 similarity index 100% rename from episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/read_model_from_file_test.f90 rename to episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/test/test_read_model_from_file.f90 From 4c077e1372ab0198865311032e03fc0b2c1178e7 Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Tue, 8 Jul 2025 13:47:29 +0100 Subject: [PATCH 24/26] Document FIX locations in solution README --- .../challenge-1/solution/README.md | 18 ++++++++++++------ .../challenge-1/solution/src/game_of_life.f90 | 2 +- .../solution/src/game_of_life_mod.f90 | 10 +++++----- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md index ef1b79a..2204937 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/README.md @@ -12,14 +12,20 @@ Tests are also provided, which can be run using FPM. fpm test ``` -## Question 1 - ->Can you identify the aspects of this Fortran code which make it difficult to unit test? +## Question 1 - Can you identify the aspects of this Fortran code which make it difficult to unit test? There are several issues with this Fortran code which make it hard to unit test. Find the suggested fixes listed below. -1. Everything is containing within a single program. This prevents us from using individual procedures within test modules. Effectively preventing us from testing them. +1. Everything is containing within a single program. This prevents us from using individual procedures within test modules. + Effectively preventing us from testing them. + +2. There is a lot of global state used across multiple procedures. This makes tests dependent on one another therefore + complicating the management of data/state between tests. + +3. There is a lot of logic not contained within procedures. Wrapping this in procedures opens up more of the code which can be + tested. -2. There is a lot of global state used across multiple procedures. This makes tests dependent on one another therefore complicating the management of data/state between tests. +## Question 2 - Try to improve the src to make it more unit testable. -3. There is a lot of logic not contained within procedures. Wrapping this in procedures opens up more of the code which can be tested. +An example implementation of an improved src code is provided in [src](./src/). Comments beginning with `FIX_` +are provided above the implemented fixes for each of the numbered list items above. diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life.f90 index 0febb6c..de2da9a 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life.f90 @@ -42,7 +42,7 @@ program game_of_life stop end if - ! Q1_FIX3: Extract the file IO into a module procedure to allow it to be tested. + ! FIX3: Extract the file IO into a module procedure to allow it to be tested. call read_model_from_file(input_fname, max_nrow, max_ncol, current_board, io_error_message) if (allocated(io_error_message)) then diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 index 827904d..440d9ba 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/solution/src/game_of_life_mod.f90 @@ -1,4 +1,4 @@ -! Q1_FIX_1: Moving these procedures into a separate file allows them to be used within a test file +! FIX_1: Moving these procedures into a separate file allows them to be used within a test file module game_of_life_mod implicit none @@ -6,7 +6,7 @@ module game_of_life_mod contains - ! Q1_FIX2: Pass parameters into procedures instead of relying on global state to isolate tests from one another. + ! FIX2: Pass parameters into procedures instead of relying on global state to isolate tests from one another. !> Evolve the board into the state of the next iteration subroutine evolve_board(current_board, new_board) !> The board as it currently is before this iteration @@ -43,7 +43,7 @@ subroutine evolve_board(current_board, new_board) enddo end subroutine evolve_board - ! Q1_FIX2: Pass parameters into procedures instead of relying on global state to isolate tests from one another. + ! FIX2: Pass parameters into procedures instead of relying on global state to isolate tests from one another. !> Check if we have reached steady state, i.e. current and new board match subroutine check_for_steady_state(current_board, new_board, steady_state) !> The board as it currently is before this iteration @@ -70,7 +70,7 @@ subroutine check_for_steady_state(current_board, new_board, steady_state) steady_state = .true. end subroutine check_for_steady_state - ! Q1_FIX2: Pass parameters into procedures instead of relying on global state to isolate tests from one another. + ! FIX2: Pass parameters into procedures instead of relying on global state to isolate tests from one another. !> Output the current board to the terminal subroutine draw_board(current_board) !> The board as it currently is for this iteration @@ -101,7 +101,7 @@ subroutine draw_board(current_board) deallocate(output) end subroutine draw_board - ! Q1_FIX3: Extract the file IO into a module procedure to allow it to be tested. + ! FIX3: Extract the file IO into a module procedure to allow it to be tested. !> Populate the a board from the provided file subroutine read_model_from_file(input_fname, max_nrow, max_ncol, board, io_error_message) !> The name of the file to read in the board From 2acbb54fa37f163183fe797b78b046584dfe0cba Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Tue, 8 Jul 2025 13:57:26 +0100 Subject: [PATCH 25/26] Add documentation for running the challenge src --- .../challenge-1/challenge/README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/README.md b/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/README.md index ec55e48..8b4b04b 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/README.md +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/README.md @@ -1,6 +1,15 @@ # Episode 2 - Challenge 1: Identify bad practice for unit testing Fortran -Take a look at the [src](./src) code provided. +Take a look at the [src](./src) code provided. This is an implementation of +[Conway's game of life](). The program reads in a data file which represents the starting +state of the system. The system is then evolved and printed to the terminal screen for each +time step. To build and run the src code use the following command from within this dir. + +```bash +fpm run -- ../models/model-1.dat # Or another dat file +``` + +## Questions 1. Can you identify the aspects of this Fortran code which make it difficult to unit test? 2. Try to improve the src to make it more unit testable. From 187598ecfd71991dfffb773d9344e19f56b8c78c Mon Sep 17 00:00:00 2001 From: Connor Aird Date: Tue, 8 Jul 2025 13:59:39 +0100 Subject: [PATCH 26/26] Add link to game of life --- .../challenge-1/challenge/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/README.md b/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/README.md index 8b4b04b..db3e0ff 100644 --- a/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/README.md +++ b/episodes/2-intro-to-fortran-unit-tests/challenge-1/challenge/README.md @@ -1,9 +1,10 @@ # Episode 2 - Challenge 1: Identify bad practice for unit testing Fortran Take a look at the [src](./src) code provided. This is an implementation of -[Conway's game of life](). The program reads in a data file which represents the starting -state of the system. The system is then evolved and printed to the terminal screen for each -time step. To build and run the src code use the following command from within this dir. +[Conway's game of life](http://en.wikipedia.org/wiki/Conway%27s_Game_of_Life). The program +reads in a data file which represents the starting state of the system. The system is then +evolved and printed to the terminal screen for each time step. To build and run the src +code use the following command from within this dir. ```bash fpm run -- ../models/model-1.dat # Or another dat file