diff options
author | Sebastian Ehlert <28669218+awvwgk@users.noreply.github.com> | 2021-03-31 16:13:58 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-03-31 16:13:58 +0200 |
commit | d9dc9f2ae5f196c15a7d35cddabc805c40ff86ce (patch) | |
tree | 6f61952c630b023edec391daae2747063703d489 /test | |
parent | 5422ec57f4081bf2225f5dde5cc07999bf8010f9 (diff) | |
download | fpm-d9dc9f2ae5f196c15a7d35cddabc805c40ff86ce.tar.gz fpm-d9dc9f2ae5f196c15a7d35cddabc805c40ff86ce.zip |
Phase out Haskell fpm (#420)
- remove bootstrap directory from repository
- remove stack-build from CI workflow
- move Fortran fpm to project root
- adjust install script and bootstrap instructions
Diffstat (limited to 'test')
-rw-r--r-- | test/cli_test/cli_test.f90 | 236 | ||||
-rw-r--r-- | test/fpm_test/main.f90 | 106 | ||||
-rw-r--r-- | test/fpm_test/test_backend.f90 | 353 | ||||
-rw-r--r-- | test/fpm_test/test_filesystem.f90 | 106 | ||||
-rw-r--r-- | test/fpm_test/test_installer.f90 | 168 | ||||
-rw-r--r-- | test/fpm_test/test_manifest.f90 | 1085 | ||||
-rw-r--r-- | test/fpm_test/test_module_dependencies.f90 | 666 | ||||
-rw-r--r-- | test/fpm_test/test_package_dependencies.f90 | 240 | ||||
-rw-r--r-- | test/fpm_test/test_source_parsing.f90 | 758 | ||||
-rw-r--r-- | test/fpm_test/test_toml.f90 | 107 | ||||
-rw-r--r-- | test/fpm_test/test_versioning.f90 | 405 | ||||
-rw-r--r-- | test/fpm_test/testsuite.f90 | 286 | ||||
-rw-r--r-- | test/help_test/help_test.f90 | 292 | ||||
-rw-r--r-- | test/new_test/new_test.f90 | 187 |
14 files changed, 4995 insertions, 0 deletions
diff --git a/test/cli_test/cli_test.f90 b/test/cli_test/cli_test.f90 new file mode 100644 index 0000000..d979f1a --- /dev/null +++ b/test/cli_test/cli_test.f90 @@ -0,0 +1,236 @@ +program main + +! for each set of command options, call this command recursively which will print the resulting parameters with a +! given test command CMD from the TEST() array. +! +! Then read the expected values as a NAMELIST group from the test array and compare the expected +! results with the actual results. +! +! the PARSE() subroutine is a copy of the app/main.f90 program except it creates and writes a NAMELIST file instead +! of actually calling the subcommands. +! +! The program will exit with a non-zero status if any of the tests fail + +use, intrinsic :: iso_fortran_env, only : compiler_version, compiler_options +implicit none + +! convenient arbitrary sizes for test + +! assuming no name over 15 characters to make output have shorter lines +character(len=15),allocatable :: name(:),act_name(:) ; namelist/act_cli/act_name +integer,parameter :: max_names=10 + +character(len=:),allocatable :: command +character(len=:),allocatable :: cmd +integer :: cstat, estat +integer :: act_cstat, act_estat +integer :: i, ios +logical :: w_e,act_w_e ; namelist/act_cli/act_w_e +logical :: w_t,act_w_t ; namelist/act_cli/act_w_t + +character(len=63) :: profile,act_profile ; namelist/act_cli/act_profile +character(len=:),allocatable :: args,act_args ; namelist/act_cli/act_args +namelist/expected/cmd,cstat,estat,w_e,w_t,name,profile,args +integer :: lun +logical,allocatable :: tally(:) +logical,allocatable :: subtally(:) +character(len=256) :: message + +! table of arguments to pass to program and expected non-default values for that execution in NAMELIST group format +character(len=*),parameter :: tests(*)= [ character(len=256) :: & + +'CMD="new", ESTAT=1,', & +!'CMD="new -unknown", ESTAT=2,', & +'CMD="new my_project another yet_another -test", ESTAT=2,', & +'CMD="new my_project --app", W_E=T, NAME="my_project",', & +'CMD="new my_project --app --test", W_E=T,W_T=T, NAME="my_project",', & +'CMD="new my_project --test", W_T=T, NAME="my_project",', & +'CMD="new my_project", W_E=T,W_T=T, NAME="my_project",', & + +'CMD="run", ', & +'CMD="run my_project", NAME="my_project", ', & +'CMD="run proj1 p2 project3", NAME="proj1","p2","project3", ', & +'CMD="run proj1 p2 project3 --profile debug", NAME="proj1","p2","project3",profile="debug",', & +'CMD="run proj1 p2 project3 --profile release", NAME="proj1","p2","project3",profile="release",', & +'CMD="run proj1 p2 project3 --profile release -- arg1 -x ""and a long one""", & + &NAME="proj1","p2","project3",profile="release",ARGS="""arg1"" -x ""and a long one""", ', & + +'CMD="test", ', & +'CMD="test my_project", NAME="my_project", ', & +'CMD="test proj1 p2 project3", NAME="proj1","p2","project3", ', & +'CMD="test proj1 p2 project3 --profile debug", NAME="proj1","p2","project3",profile="debug",', & +'CMD="test proj1 p2 project3 --profile release", NAME="proj1","p2","project3",profile="release",', & +'CMD="test proj1 p2 project3 --profile release -- arg1 -x ""and a long one""", & + &NAME="proj1","p2","project3",profile="release" ARGS="""arg1"" -x ""and a long one""", ', & + +'CMD="build", NAME= profile="",ARGS="",', & +'CMD="build --profile release", NAME= profile="release",ARGS="",', & +' ' ] +character(len=256) :: readme(3) + +readme(1)='&EXPECTED' ! top and bottom line for a NAMELIST group read from TEST() used to set the expected values +readme(3)=' /' +tally=[logical ::] ! an array that tabulates the command test results as pass or fail. + +if(command_argument_count().eq.0)then ! assume if called with no arguments to do the tests. This means you cannot + ! have a test of no parameters. Could improve on this. + ! if called with parameters assume this is a test and call the routine to + ! parse the resulting values after calling the CLI command line parser + ! and write the NAMELIST group so it can be read and tested against the + ! expected results + write(*,*)'start tests of the CLI command line parser' + command=repeat(' ',4096) + call get_command_argument(0,command) + command=trim(command) + write(*,*)'command=',command + + do i=1,size(tests) + if(tests(i).eq.' ')then + open(file='_test_cli',newunit=lun,delim='quote') + close(unit=lun,status='delete') + exit + endif + ! blank out name group EXPECTED + name=[(repeat(' ',len(name)),i=1,max_names)] ! the words on the command line sans the subcommand name + profile="" ! --profile PROF + w_e=.false. ! --app + w_t=.false. ! --test + args=repeat(' ',132) ! -- ARGS + cmd=repeat(' ',132) ! the command line arguments to test + cstat=0 ! status values from EXECUTE_COMMAND_LINE() + estat=0 + readme(2)=' '//tests(i) ! select command options to test for CMD and set nondefault expected values + read(readme,nml=expected) + + write(*,'(*(g0))')'START: TEST ',i,' CMD=',trim(cmd) + ! call this program which will crack command line and write results to scratch file _test_cli + call execute_command_line(command//' '//trim(cmd),cmdstat=act_cstat,exitstat=act_estat) + if(cstat.eq.act_cstat.and.estat.eq.act_estat)then + if(estat.eq.0)then + open(file='_test_cli',newunit=lun,delim='quote') + act_name=[(repeat(' ',len(act_name)),i=1,max_names)] + act_profile='' + act_w_e=.false. + act_w_t=.false. + act_args=repeat(' ',132) + read(lun,nml=act_cli,iostat=ios,iomsg=message) + if(ios.ne.0)then + write(*,'(a)')'ERROR:',trim(message) + endif + close(unit=lun) + ! compare results to expected values + subtally=[logical ::] + call test_test('NAME',all(act_name.eq.name)) + call test_test('PROFILE',act_profile.eq.profile) + call test_test('WITH_EXPECTED',act_w_e.eqv.w_e) + call test_test('WITH_TESTED',act_w_t.eqv.w_t) + call test_test('WITH_TEST',act_w_t.eqv.w_t) + call test_test('ARGS',act_args.eq.args) + if(all(subtally))then + write(*,'(*(g0))')'PASSED: TEST ',i,' STATUS: expected ',cstat,' ',estat,' actual ',act_cstat,' ',act_estat,& + & ' for [',trim(cmd),']' + tally=[tally,.true.] + else + write(*,'(*(g0))')'FAILED: TEST ',i,' STATUS: expected ',cstat,' ',estat,' actual ',act_cstat,' ',act_estat,& + & ' for [',trim(cmd),']' + print '(4a)', & + 'This file was compiled by ', & + compiler_version(), & + ' using the options ', & + compiler_options() + write(*,nml=act_cli,delim='quote') + tally=[tally,.false.] + endif + else + write(*,'(*(g0))')'PASSED: TEST ',i,' EXPECTED BAD STATUS: expected ',cstat,' ',estat, & + ' actual ',act_cstat,' ',act_estat,' for [',trim(cmd),']' + tally=[tally,.true.] + endif + else + write(*,'(*(g0))')'FAILED: TEST ',i,'BAD STATUS: expected ',cstat,' ',estat,' actual ',act_cstat,' ',act_estat,& + ' for [',trim(cmd),']' + tally=[tally,.false.] + endif + enddo + ! write up total results and if anything failed exit with a non-zero status + write(*,'(*(g0))')'TALLY;',tally + if(all(tally))then + write(*,'(*(g0))')'PASSED: all ',count(tally),' tests passed ' + else + write(*,*)'FAILED: PASSED=',count(tally),' FAILED=',count(.not.tally) + stop 4 + endif +else + ! call this program with arguments + !============================================= + debugit: block + integer :: j, ilen + character(len=256) :: big_argument + write(*,*)'arguments seen directly by program' + do j=1,command_argument_count() + call get_command_argument(number=j,value=big_argument,length=ilen) + write(*,'(*(g0))')j,'[',big_argument(:ilen),']' + enddo + end block debugit + !============================================= + call parse() +endif + +contains + +subroutine test_test(name,tst) +character(len=*) :: name +logical,intent(in) :: tst + !!write(*,'(*(g0,1x))')' SUBTEST ',name,' ',merge('PASSED','FAILED',tst) + subtally=[subtally,tst] +end subroutine test_test + +subroutine parse() +! all the extended types for settings from the main program +use fpm_command_line, only: & + fpm_cmd_settings, & + fpm_new_settings, & + fpm_build_settings, & + fpm_run_settings, & + fpm_test_settings, & + fpm_install_settings, & + get_command_line_settings +use fpm, only: cmd_build, cmd_run +use fpm_cmd_install, only: cmd_install +use fpm_cmd_new, only: cmd_new +class(fpm_cmd_settings), allocatable :: cmd_settings +! duplicates the calls as seen in the main program for fpm +call get_command_line_settings(cmd_settings) + +allocate (character(len=len(name)) :: act_name(0) ) +act_args='' +act_w_e=.false. +act_w_t=.false. +act_profile='' + +select type(settings=>cmd_settings) +type is (fpm_new_settings) + act_w_e=settings%with_executable + act_w_t=settings%with_test + act_name=[trim(settings%name)] +type is (fpm_build_settings) + act_profile=settings%profile +type is (fpm_run_settings) + act_profile=settings%profile + act_name=settings%name + act_args=settings%args +type is (fpm_test_settings) + act_profile=settings%profile + act_name=settings%name + act_args=settings%args +type is (fpm_install_settings) +end select + +open(file='_test_cli',newunit=lun,delim='quote') +write(lun,nml=act_cli,delim='quote') +!!write(*,nml=act_cli) +close(unit=lun) + +end subroutine parse + +end program main diff --git a/test/fpm_test/main.f90 b/test/fpm_test/main.f90 new file mode 100644 index 0000000..0a65307 --- /dev/null +++ b/test/fpm_test/main.f90 @@ -0,0 +1,106 @@ +!> Driver for unit testing +program fpm_testing + use, intrinsic :: iso_fortran_env, only : error_unit + use testsuite, only : run_testsuite, new_testsuite, testsuite_t, & + & select_suite, run_selected + use test_toml, only : collect_toml + use test_manifest, only : collect_manifest + use test_filesystem, only : collect_filesystem + use test_source_parsing, only : collect_source_parsing + use test_module_dependencies, only : collect_module_dependencies + use test_package_dependencies, only : collect_package_dependencies + use test_backend, only: collect_backend + use test_installer, only : collect_installer + use test_versioning, only : collect_versioning + implicit none + integer :: stat, is + character(len=:), allocatable :: suite_name, test_name + type(testsuite_t), allocatable :: suite(:) + character(len=*), parameter :: fmt = '("#", *(1x, a))' + + stat = 0 + + suite = [ & + & new_testsuite("fpm_toml", collect_toml), & + & new_testsuite("fpm_manifest", collect_manifest), & + & new_testsuite("fpm_filesystem", collect_filesystem), & + & new_testsuite("fpm_source_parsing", collect_source_parsing), & + & new_testsuite("fpm_module_dependencies", collect_module_dependencies), & + & new_testsuite("fpm_package_dependencies", collect_package_dependencies), & + & new_testsuite("fpm_test_backend", collect_backend), & + & new_testsuite("fpm_installer", collect_installer), & + & new_testsuite("fpm_versioning", collect_versioning) & + & ] + + call get_argument(1, suite_name) + call get_argument(2, test_name) + + if (allocated(suite_name)) then + is = select_suite(suite, suite_name) + if (is > 0 .and. is <= size(suite)) then + if (allocated(test_name)) then + write(error_unit, fmt) "Suite:", suite(is)%name + call run_selected(suite(is)%collect, test_name, error_unit, stat) + if (stat < 0) then + error stop 1 + end if + else + write(error_unit, fmt) "Testing:", suite(is)%name + call run_testsuite(suite(is)%collect, error_unit, stat) + end if + else + write(error_unit, fmt) "Available testsuites" + do is = 1, size(suite) + write(error_unit, fmt) "-", suite(is)%name + end do + error stop 1 + end if + else + do is = 1, size(suite) + write(error_unit, fmt) "Testing:", suite(is)%name + call run_testsuite(suite(is)%collect, error_unit, stat) + end do + end if + + if (stat > 0) then + write(error_unit, '(i0, 1x, a)') stat, "test(s) failed!" + error stop 1 + end if + + +contains + + + !> Obtain the command line argument at a given index + subroutine get_argument(idx, arg) + + !> Index of command line argument, range [0:command_argument_count()] + integer, intent(in) :: idx + + !> Command line argument + character(len=:), allocatable, intent(out) :: arg + + integer :: length, stat + + call get_command_argument(idx, length=length, status=stat) + if (stat /= 0) then + return + endif + + allocate(character(len=length) :: arg, stat=stat) + if (stat /= 0) then + return + endif + + if (length > 0) then + call get_command_argument(idx, arg, status=stat) + if (stat /= 0) then + deallocate(arg) + return + end if + end if + + end subroutine get_argument + + +end program fpm_testing diff --git a/test/fpm_test/test_backend.f90 b/test/fpm_test/test_backend.f90 new file mode 100644 index 0000000..662e470 --- /dev/null +++ b/test/fpm_test/test_backend.f90 @@ -0,0 +1,353 @@ +!> Define tests for the `fpm_backend` module (build scheduling) +module test_backend + use testsuite, only : new_unittest, unittest_t, error_t, test_failed + use test_module_dependencies, only: operator(.in.) + use fpm_filesystem, only: exists, mkdir, get_temp_filename + use fpm_targets, only: build_target_t, build_target_ptr, & + FPM_TARGET_OBJECT, FPM_TARGET_ARCHIVE, & + add_target, add_dependency + use fpm_backend, only: sort_target, schedule_targets + implicit none + private + + public :: collect_backend + +contains + + + !> Collect all exported unit tests + subroutine collect_backend(testsuite) + + !> Collection of tests + type(unittest_t), allocatable, intent(out) :: testsuite(:) + + testsuite = [ & + & new_unittest("target-sort", test_target_sort), & + & new_unittest("target-sort-skip-all", test_target_sort_skip_all), & + & new_unittest("target-sort-rebuild-all", test_target_sort_rebuild_all), & + & new_unittest("schedule-targets", test_schedule_targets), & + & new_unittest("schedule-targets-empty", test_schedule_empty) & + ] + + end subroutine collect_backend + + + !> Check scheduling of objects with dependencies + subroutine test_target_sort(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(build_target_ptr), allocatable :: targets(:) + + integer :: i + + targets = new_test_package() + + ! Perform depth-first topological sort of targets + do i=1,size(targets) + + call sort_target(targets(i)%ptr) + + end do + + ! Check target states: all targets scheduled + do i=1,size(targets) + + if (.not.targets(i)%ptr%touched) then + call test_failed(error,"Target touched flag not set") + return + end if + + if (.not.targets(i)%ptr%sorted) then + call test_failed(error,"Target sort flag not set") + return + end if + + if (targets(i)%ptr%skip) then + call test_failed(error,"Target skip flag set incorrectly") + return + end if + + if (targets(i)%ptr%schedule < 0) then + call test_failed(error,"Target schedule not set") + return + end if + + end do + + ! Check all objects sheduled before library + do i=2,size(targets) + + if (targets(i)%ptr%schedule >= targets(1)%ptr%schedule) then + call test_failed(error,"Object dependency scheduled after dependent library target") + return + end if + + end do + + ! Check target 4 schedule before targets 2 & 3 + do i=2,3 + if (targets(4)%ptr%schedule >= targets(i)%ptr%schedule) then + call test_failed(error,"Object dependency scheduled after dependent object target") + return + end if + end do + + end subroutine test_target_sort + + + + !> Check incremental rebuild for existing archive + !> all object sources are unmodified: all objects should be skipped + subroutine test_target_sort_skip_all(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(build_target_ptr), allocatable :: targets(:) + + integer :: fh, i + + targets = new_test_package() + + do i=2,size(targets) + + ! Mimick unmodified sources + allocate(targets(i)%ptr%source) + targets(i)%ptr%source%digest = i + targets(i)%ptr%digest_cached = i + + end do + + ! Mimick archive already exists + open(newunit=fh,file=targets(1)%ptr%output_file,status="unknown") + close(fh) + + ! Perform depth-first topological sort of targets + do i=1,size(targets) + + call sort_target(targets(i)%ptr) + + end do + + ! Check target states: all targets skipped + do i=1,size(targets) + + if (.not.targets(i)%ptr%touched) then + call test_failed(error,"Target touched flag not set") + return + end if + + if (targets(i)%ptr%sorted) then + call test_failed(error,"Target sort flag set incorrectly") + return + end if + + if (.not.targets(i)%ptr%skip) then + call test_failed(error,"Target skip flag set incorrectly") + return + end if + + end do + + end subroutine test_target_sort_skip_all + + + !> Check incremental rebuild for existing archive + !> all but lowest source modified: all objects should be rebuilt + subroutine test_target_sort_rebuild_all(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(build_target_ptr), allocatable :: targets(:) + + integer :: fh, i + + targets = new_test_package() + + do i=2,3 + + ! Mimick unmodified sources + allocate(targets(i)%ptr%source) + targets(i)%ptr%source%digest = i + targets(i)%ptr%digest_cached = i + + end do + + ! Mimick archive already exists + open(newunit=fh,file=targets(1)%ptr%output_file,status="unknown") + close(fh) + + ! Perform depth-first topological sort of targets + do i=1,size(targets) + + call sort_target(targets(i)%ptr) + + end do + + ! Check target states: all targets scheduled + do i=1,size(targets) + + if (.not.targets(i)%ptr%sorted) then + call test_failed(error,"Target sort flag not set") + return + end if + + if (targets(i)%ptr%skip) then + call test_failed(error,"Target skip flag set incorrectly") + return + end if + + end do + + end subroutine test_target_sort_rebuild_all + + + !> Check construction of target queue and schedule + subroutine test_schedule_targets(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(build_target_ptr), allocatable :: targets(:) + + integer :: i, j + type(build_target_ptr), allocatable :: queue(:) + integer, allocatable :: schedule_ptr(:) + + targets = new_test_package() + + ! Perform depth-first topological sort of targets + do i=1,size(targets) + + call sort_target(targets(i)%ptr) + + end do + + ! Construct build schedule queue + call schedule_targets(queue, schedule_ptr, targets) + + ! Check all targets enqueued + do i=1,size(targets) + + if (.not.(targets(i)%ptr.in.queue)) then + + call test_failed(error,"Target not found in build queue") + return + + end if + + end do + + ! Check schedule structure + if (schedule_ptr(1) /= 1) then + + call test_failed(error,"schedule_ptr(1) does not point to start of the queue") + return + + end if + + if (schedule_ptr(size(schedule_ptr)) /= size(queue)+1) then + + call test_failed(error,"schedule_ptr(end) does not point to end of the queue") + return + + end if + + do i=1,size(schedule_ptr)-1 + + do j=schedule_ptr(i),(schedule_ptr(i+1)-1) + + if (queue(j)%ptr%schedule /= i) then + + call test_failed(error,"Target scheduled in the wrong region") + return + + end if + + end do + + end do + + end subroutine test_schedule_targets + + + !> Check construction of target queue and schedule + !> when there's nothing to do (all targets skipped) + subroutine test_schedule_empty(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(build_target_ptr), allocatable :: targets(:) + + integer :: i + type(build_target_ptr), allocatable :: queue(:) + integer, allocatable :: schedule_ptr(:) + + targets = new_test_package() + + do i=1,size(targets) + + targets(i)%ptr%skip = .true. + + end do + + ! Perform depth-first topological sort of targets + do i=1,size(targets) + + call sort_target(targets(i)%ptr) + + end do + + ! Construct build schedule queue + call schedule_targets(queue, schedule_ptr, targets) + + ! Check queue is empty + if (size(queue) > 0) then + + call test_failed(error,"Expecting an empty build queue, but not empty") + return + + end if + + ! Check schedule loop is not entered + do i=1,size(schedule_ptr)-1 + + call test_failed(error,"Attempted to run an empty schedule") + return + + end do + + end subroutine test_schedule_empty + + + !> Helper to generate target objects with dependencies + function new_test_package() result(targets) + + type(build_target_ptr), allocatable :: targets(:) + + call add_target(targets,FPM_TARGET_ARCHIVE,get_temp_filename()) + + call add_target(targets,FPM_TARGET_OBJECT,get_temp_filename()) + + call add_target(targets,FPM_TARGET_OBJECT,get_temp_filename()) + + call add_target(targets,FPM_TARGET_OBJECT,get_temp_filename()) + + ! Library depends on all objects + call add_dependency(targets(1)%ptr,targets(2)%ptr) + call add_dependency(targets(1)%ptr,targets(3)%ptr) + call add_dependency(targets(1)%ptr,targets(4)%ptr) + + ! Inter-object dependency + ! targets 2 & 3 depend on target 4 + call add_dependency(targets(2)%ptr,targets(4)%ptr) + call add_dependency(targets(3)%ptr,targets(4)%ptr) + + end function new_test_package + + +end module test_backend
\ No newline at end of file diff --git a/test/fpm_test/test_filesystem.f90 b/test/fpm_test/test_filesystem.f90 new file mode 100644 index 0000000..5a7e18a --- /dev/null +++ b/test/fpm_test/test_filesystem.f90 @@ -0,0 +1,106 @@ +module test_filesystem + use testsuite, only : new_unittest, unittest_t, error_t, test_failed + use fpm_filesystem, only: canon_path + implicit none + private + + public :: collect_filesystem + +contains + + + !> Collect all exported unit tests + subroutine collect_filesystem(testsuite) + + !> Collection of tests + type(unittest_t), allocatable, intent(out) :: testsuite(:) + + testsuite = [ & + & new_unittest("canon-path", test_canon_path) & + ] + + end subroutine collect_filesystem + + + subroutine test_canon_path(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + call check_string(error, & + & canon_path("git/project/src/origin"), "git/project/src/origin") + if (allocated(error)) return + + call check_string(error, & + & canon_path("./project/src/origin"), "project/src/origin") + if (allocated(error)) return + + call check_string(error, & + & canon_path("./project/src///origin/"), "project/src/origin") + if (allocated(error)) return + + call check_string(error, & + & canon_path("../project/./src/origin/"), "../project/src/origin") + if (allocated(error)) return + + call check_string(error, & + & canon_path("/project//src/origin/"), "/project/src/origin") + if (allocated(error)) return + + call check_string(error, & + & canon_path("/project/src/../origin/"), "/project/origin") + if (allocated(error)) return + + call check_string(error, & + & canon_path("/project/src/../origin/.."), "/project") + if (allocated(error)) return + + call check_string(error, & + & canon_path("/project/src//../origin/."), "/project/origin") + if (allocated(error)) return + + call check_string(error, & + & canon_path("../project/src/./../origin/."), "../project/origin") + if (allocated(error)) return + + call check_string(error, & + & canon_path("../project/src/../../../origin/."), "../../origin") + if (allocated(error)) return + + call check_string(error, & + & canon_path("/../.."), "/") + if (allocated(error)) return + + call check_string(error, & + & canon_path("././././././/////a/b/.///././////.///c/../../../"), ".") + if (allocated(error)) return + + call check_string(error, & + & canon_path("/./././././/////a/b/.///././////.///c/../../../"), "/") + if (allocated(error)) return + + end subroutine test_canon_path + + + !> Check a character variable against a reference value + subroutine check_string(error, actual, expected) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + !> Actual string value + character(len=*), intent(in) :: actual + + !> Expected string value + character(len=*), intent(in) :: expected + + if (actual /= expected) then + call test_failed(error, & + "Character value missmatch "//& + "expected '"//expected//"' but got '"//actual//"'") + end if + + end subroutine check_string + + +end module test_filesystem diff --git a/test/fpm_test/test_installer.f90 b/test/fpm_test/test_installer.f90 new file mode 100644 index 0000000..1235ba5 --- /dev/null +++ b/test/fpm_test/test_installer.f90 @@ -0,0 +1,168 @@ +!> Define tests for the `fpm_installer` module +!> +!> The tests here setup a mock environment to allow testing for Unix and Windows +!> platforms at the same time. +module test_installer + use testsuite, only : new_unittest, unittest_t, error_t, test_failed, & + & check_string + use fpm_environment, only : OS_WINDOWS, OS_LINUX + use fpm_filesystem, only : join_path + use fpm_installer + implicit none + private + + public :: collect_installer + + + type, extends(installer_t) :: mock_installer_t + character(len=:), allocatable :: expected_dir + character(len=:), allocatable :: expected_run + contains + procedure :: make_dir + procedure :: run + end type mock_installer_t + +contains + + !> Collect all exported unit tests + subroutine collect_installer(testsuite) + !> Collection of tests + type(unittest_t), allocatable, intent(out) :: testsuite(:) + + testsuite = [ & + & new_unittest("install-lib", test_install_lib), & + & new_unittest("install-pkgconfig", test_install_pkgconfig), & + & new_unittest("install-sitepackages", test_install_sitepackages), & + & new_unittest("install-mod", test_install_mod), & + & new_unittest("install-exe-unix", test_install_exe_unix), & + & new_unittest("install-exe-win", test_install_exe_win)] + + end subroutine collect_installer + + subroutine test_install_exe_unix(error) + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(mock_installer_t) :: mock + type(installer_t) :: installer + + call new_installer(installer, prefix="PREFIX", verbosity=0, copy="mock") + mock%installer_t = installer + mock%os = OS_LINUX + mock%expected_dir = "PREFIX/bin" + mock%expected_run = 'mock "name" "'//mock%expected_dir//'"' + + call mock%install_executable("name", error) + + end subroutine test_install_exe_unix + + subroutine test_install_exe_win(error) + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(mock_installer_t) :: mock + type(installer_t) :: installer + + call new_installer(installer, prefix="PREFIX", verbosity=0, copy="mock") + mock%installer_t = installer + mock%os = OS_WINDOWS + mock%expected_dir = "PREFIX\bin" + mock%expected_run = 'mock "name.exe" "'//mock%expected_dir//'"' + + call mock%install_executable("name", error) + + end subroutine test_install_exe_win + + subroutine test_install_lib(error) + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(mock_installer_t) :: mock + type(installer_t) :: installer + + call new_installer(installer, prefix="PREFIX", verbosity=0, copy="mock") + mock%installer_t = installer + mock%expected_dir = join_path("PREFIX", "lib") + mock%expected_run = 'mock "name" "'//join_path("PREFIX", "lib")//'"' + + call mock%install_library("name", error) + + end subroutine test_install_lib + + subroutine test_install_pkgconfig(error) + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(mock_installer_t) :: mock + type(installer_t) :: installer + + call new_installer(installer, prefix="PREFIX", verbosity=0, copy="mock") + mock%installer_t = installer + mock%os = OS_WINDOWS + mock%expected_dir = "PREFIX\lib\pkgconfig" + mock%expected_run = 'mock "name" "'//mock%expected_dir//'"' + + call mock%install("name", "lib/pkgconfig", error) + + end subroutine test_install_pkgconfig + + subroutine test_install_sitepackages(error) + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(mock_installer_t) :: mock + type(installer_t) :: installer + + call new_installer(installer, prefix="PREFIX", verbosity=0, copy="mock") + mock%installer_t = installer + mock%os = OS_LINUX + mock%expected_dir = "PREFIX/lib/python3.7/site-packages" + mock%expected_run = 'mock "name" "'//mock%expected_dir//'"' + + call mock%install("name", join_path("lib", "python3.7", "site-packages"), & + error) + + end subroutine test_install_sitepackages + + subroutine test_install_mod(error) + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(mock_installer_t) :: mock + type(installer_t) :: installer + + call new_installer(installer, prefix="PREFIX", verbosity=0, copy="mock") + mock%installer_t = installer + mock%expected_dir = join_path("PREFIX", "include") + mock%expected_run = 'mock "name" "'//join_path("PREFIX", "include")//'"' + + call mock%install_header("name", error) + + end subroutine test_install_mod + + !> Create a new directory in the prefix + subroutine make_dir(self, dir, error) + !> Instance of the installer + class(mock_installer_t), intent(inout) :: self + !> Directory to be created + character(len=*), intent(in) :: dir + !> Error handling + type(error_t), allocatable, intent(out) :: error + + call check_string(error, self%expected_dir, dir, "dir") + + end subroutine make_dir + + !> Run an installation command + subroutine run(self, command, error) + !> Instance of the installer + class(mock_installer_t), intent(inout) :: self + !> Command to be launched + character(len=*), intent(in) :: command + !> Error handling + type(error_t), allocatable, intent(out) :: error + + call check_string(error, self%expected_run, command, "run") + end subroutine run + +end module test_installer diff --git a/test/fpm_test/test_manifest.f90 b/test/fpm_test/test_manifest.f90 new file mode 100644 index 0000000..94e5e07 --- /dev/null +++ b/test/fpm_test/test_manifest.f90 @@ -0,0 +1,1085 @@ +!> Define tests for the `fpm_manifest` modules +module test_manifest + use fpm_filesystem, only: get_temp_filename + use testsuite, only : new_unittest, unittest_t, error_t, test_failed, & + & check_string + use fpm_manifest + use fpm_strings, only: operator(.in.) + implicit none + private + + public :: collect_manifest + + +contains + + + !> Collect all exported unit tests + subroutine collect_manifest(testsuite) + + !> Collection of tests + type(unittest_t), allocatable, intent(out) :: testsuite(:) + + testsuite = [ & + & new_unittest("valid-manifest", test_valid_manifest), & + & new_unittest("invalid-manifest", test_invalid_manifest, should_fail=.true.), & + & new_unittest("default-library", test_default_library), & + & new_unittest("default-executable", test_default_executable), & + & new_unittest("dependency-empty", test_dependency_empty, should_fail=.true.), & + & new_unittest("dependency-pathtag", test_dependency_pathtag, should_fail=.true.), & + & new_unittest("dependency-gitpath", test_dependency_gitpath, should_fail=.true.), & + & new_unittest("dependency-nourl", test_dependency_nourl, should_fail=.true.), & + & new_unittest("dependency-gitconflict", test_dependency_gitconflict, should_fail=.true.), & + & new_unittest("dependency-wrongkey", test_dependency_wrongkey, should_fail=.true.), & + & new_unittest("dependencies-empty", test_dependencies_empty), & + & new_unittest("dependencies-typeerror", test_dependencies_typeerror, should_fail=.true.), & + & new_unittest("executable-empty", test_executable_empty, should_fail=.true.), & + & new_unittest("executable-typeerror", test_executable_typeerror, should_fail=.true.), & + & new_unittest("executable-noname", test_executable_noname, should_fail=.true.), & + & new_unittest("executable-wrongkey", test_executable_wrongkey, should_fail=.true.), & + & new_unittest("build-config-valid", test_build_valid), & + & new_unittest("build-config-empty", test_build_empty), & + & new_unittest("build-config-invalid-values", test_build_invalid_values, should_fail=.true.), & + & new_unittest("library-empty", test_library_empty), & + & new_unittest("library-wrongkey", test_library_wrongkey, should_fail=.true.), & + & new_unittest("package-simple", test_package_simple), & + & new_unittest("package-empty", test_package_empty, should_fail=.true.), & + & new_unittest("package-typeerror", test_package_typeerror, should_fail=.true.), & + & new_unittest("package-noname", test_package_noname, should_fail=.true.), & + & new_unittest("package-wrongexe", test_package_wrongexe, should_fail=.true.), & + & new_unittest("package-wrongtest", test_package_wrongtest, should_fail=.true.), & + & new_unittest("package-duplicate", test_package_duplicate, should_fail=.true.), & + & new_unittest("test-simple", test_test_simple), & + & new_unittest("test-empty", test_test_empty, should_fail=.true.), & + & new_unittest("test-typeerror", test_test_typeerror, should_fail=.true.), & + & new_unittest("test-noname", test_test_noname, should_fail=.true.), & + & new_unittest("test-wrongkey", test_test_wrongkey, should_fail=.true.), & + & new_unittest("link-string", test_link_string), & + & new_unittest("link-array", test_link_array), & + & new_unittest("link-error", test_invalid_link, should_fail=.true.), & + & new_unittest("example-simple", test_example_simple), & + & new_unittest("example-empty", test_example_empty, should_fail=.true.), & + & new_unittest("install-library", test_install_library), & + & new_unittest("install-empty", test_install_empty), & + & new_unittest("install-wrongkey", test_install_wrongkey, should_fail=.true.)] + + end subroutine collect_manifest + + + !> Try to read some unnecessary obscure and convoluted but not invalid package file + subroutine test_valid_manifest(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + character(len=*), parameter :: manifest = 'fpm-valid-manifest.toml' + integer :: unit + + open(file=manifest, newunit=unit) + write(unit, '(a)') & + & 'name = "example"', & + & '[build]', & + & 'auto-executables = false', & + & 'auto-tests = false', & + & '[dependencies.fpm]', & + & 'git = "https://github.com/fortran-lang/fpm"', & + & '[[executable]]', & + & 'name = "example-#1" # comment', & + & 'source-dir = "prog"', & + & '[dependencies]', & + & 'toml-f.git = "git@github.com:toml-f/toml-f.git"', & + & '"toml..f" = { path = ".." }', & + & '[["executable"]]', & + & 'name = "example-#2"', & + & 'source-dir = "prog"', & + & '[executable.dependencies]', & + & '[''library'']', & + & 'source-dir = """', & + & 'lib""" # comment' + close(unit) + + call get_package_data(package, manifest, error) + + open(file=manifest, newunit=unit) + close(unit, status='delete') + + if (allocated(error)) return + + if (package%name /= "example") then + call test_failed(error, "Package name is "//package%name//" but should be example") + return + end if + + if (.not.allocated(package%library)) then + call test_failed(error, "library is not present in package data") + return + end if + + if (.not.allocated(package%executable)) then + call test_failed(error, "executable is not present in package data") + return + end if + + if (size(package%executable) /= 2) then + call test_failed(error, "Number of executables in package is not two") + return + end if + + if (.not.allocated(package%dependency)) then + call test_failed(error, "dependency is not present in package data") + return + end if + + if (size(package%dependency) /= 3) then + call test_failed(error, "Number of dependencies in package is not three") + return + end if + + if (allocated(package%test)) then + call test_failed(error, "test is present in package but not in package file") + return + end if + + end subroutine test_valid_manifest + + + !> Try to read a valid TOML document which represent an invalid package file + subroutine test_invalid_manifest(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + character(len=*), parameter :: manifest = 'fpm-invalid-manifest.toml' + integer :: unit + + open(file=manifest, newunit=unit) + write(unit, '(a)') & + & '[package]', & + & 'name = "example"', & + & 'version = "0.1.0"' + close(unit) + + call get_package_data(package, manifest, error) + + open(file=manifest, newunit=unit) + close(unit, status='delete') + + end subroutine test_invalid_manifest + + + !> Create a default library + subroutine test_default_library(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + + allocate(package%library) + call default_library(package%library) + + call check_string(error, package%library%source_dir, "src", & + & "Default library source-dir") + if (allocated(error)) return + + if (.not.allocated(package%library%include_dir)) then + call test_failed(error,"Default include-dir list not allocated") + return + end if + + if (.not.("include".in.package%library%include_dir)) then + call test_failed(error,"'include' not in default include-dir list") + return + end if + + end subroutine test_default_library + + + !> Create a default executable + subroutine test_default_executable(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + character(len=*), parameter :: name = "default" + + allocate(package%executable(1)) + call default_executable(package%executable(1), name) + + call check_string(error, package%executable(1)%source_dir, "app", & + & "Default executable source-dir") + if (allocated(error)) return + + call check_string(error, package%executable(1)%name, name, & + & "Default executable name") + if (allocated(error)) return + + end subroutine test_default_executable + + + !> Dependencies cannot be created from empty tables + subroutine test_dependency_empty(error) + use fpm_manifest_dependency + use fpm_toml, only : new_table, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(dependency_config_t) :: dependency + + call new_table(table) + table%key = "example" + + call new_dependency(dependency, table, error) + + end subroutine test_dependency_empty + + + !> Try to create a dependency with conflicting entries + subroutine test_dependency_pathtag(error) + use fpm_manifest_dependency + use fpm_toml, only : new_table, toml_table, set_value + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + integer :: stat + type(dependency_config_t) :: dependency + + call new_table(table) + table%key = 'example' + call set_value(table, 'path', '"package"', stat) + call set_value(table, 'tag', '"v20.1"', stat) + + call new_dependency(dependency, table, error) + + end subroutine test_dependency_pathtag + + + !> Try to create a dependency with conflicting entries + subroutine test_dependency_nourl(error) + use fpm_manifest_dependency + use fpm_toml, only : new_table, toml_table, set_value + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + integer :: stat + type(dependency_config_t) :: dependency + + call new_table(table) + table%key = 'example' + call set_value(table, 'tag', '"v20.1"', stat) + + call new_dependency(dependency, table, error) + + end subroutine test_dependency_nourl + + + !> Try to create a dependency with conflicting entries + subroutine test_dependency_gitpath(error) + use fpm_manifest_dependency + use fpm_toml, only : new_table, toml_table, set_value + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + integer :: stat + type(dependency_config_t) :: dependency + + call new_table(table) + table%key = 'example' + call set_value(table, 'path', '"package"', stat) + call set_value(table, 'git', '"https://gitea.com/fortran-lang/pack"', stat) + + call new_dependency(dependency, table, error) + + end subroutine test_dependency_gitpath + + + !> Try to create a dependency with conflicting entries + subroutine test_dependency_gitconflict(error) + use fpm_manifest_dependency + use fpm_toml, only : new_table, toml_table, set_value + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + integer :: stat + type(dependency_config_t) :: dependency + + call new_table(table) + table%key = 'example' + call set_value(table, 'git', '"https://gitea.com/fortran-lang/pack"', stat) + call set_value(table, 'branch', '"latest"', stat) + call set_value(table, 'tag', '"v20.1"', stat) + + call new_dependency(dependency, table, error) + + end subroutine test_dependency_gitconflict + + + !> Try to create a dependency with conflicting entries + subroutine test_dependency_wrongkey(error) + use fpm_manifest_dependency + use fpm_toml, only : new_table, toml_table, set_value + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + integer :: stat + type(dependency_config_t) :: dependency + + call new_table(table) + table%key = 'example' + call set_value(table, 'not-available', '"anywhere"', stat) + + call new_dependency(dependency, table, error) + + end subroutine test_dependency_wrongkey + + + !> Dependency tables can be empty + subroutine test_dependencies_empty(error) + use fpm_manifest_dependency + use fpm_toml, only : new_table, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(dependency_config_t), allocatable :: dependencies(:) + + call new_table(table) + + call new_dependencies(dependencies, table, error) + if (allocated(error)) return + + if (allocated(dependencies)) then + call test_failed(error, "Found dependencies in empty table") + end if + + end subroutine test_dependencies_empty + + + !> Add a dependency as an array, which is not supported + subroutine test_dependencies_typeerror(error) + use fpm_manifest_dependency + use fpm_toml, only : new_table, add_array, toml_table, toml_array + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_array), pointer :: children + integer :: stat + type(dependency_config_t), allocatable :: dependencies(:) + + call new_table(table) + call add_array(table, 'dep1', children, stat) + + call new_dependencies(dependencies, table, error) + + end subroutine test_dependencies_typeerror + + + !> Executables cannot be created from empty tables + subroutine test_executable_empty(error) + use fpm_manifest_executable + use fpm_toml, only : new_table, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(executable_config_t) :: executable + + call new_table(table) + + call new_executable(executable, table, error) + + end subroutine test_executable_empty + + + !> Pass a wrong TOML type to the name field of the executable + subroutine test_executable_typeerror(error) + use fpm_manifest_executable + use fpm_toml, only : new_table, add_table, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_table), pointer :: child + integer :: stat + type(executable_config_t) :: executable + + call new_table(table) + call add_table(table, 'name', child, stat) + + call new_executable(executable, table, error) + + end subroutine test_executable_typeerror + + + !> Pass a TOML table with insufficient entries to the executable constructor + subroutine test_executable_noname(error) + use fpm_manifest_executable + use fpm_toml, only : new_table, add_table, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_table), pointer :: child + integer :: stat + type(executable_config_t) :: executable + + call new_table(table) + call add_table(table, 'dependencies', child, stat) + + call new_executable(executable, table, error) + + end subroutine test_executable_noname + + + !> Pass a TOML table with not allowed keys + subroutine test_executable_wrongkey(error) + use fpm_manifest_executable + use fpm_toml, only : new_table, add_table, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_table), pointer :: child + integer :: stat + type(executable_config_t) :: executable + + call new_table(table) + call add_table(table, 'wrong-field', child, stat) + + call new_executable(executable, table, error) + + end subroutine test_executable_wrongkey + + + !> Try to read values from the [build] table + subroutine test_build_valid(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + character(:), allocatable :: temp_file + integer :: unit + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'name = "example"', & + & '[build]', & + & 'auto-executables = false', & + & 'auto-tests = false' + close(unit) + + call get_package_data(package, temp_file, error) + + if (allocated(error)) return + + if (package%build%auto_executables) then + call test_failed(error, "Wong value of 'auto-executables' read, expecting .false.") + return + end if + + if (package%build%auto_tests) then + call test_failed(error, "Wong value of 'auto-tests' read, expecting .false.") + return + end if + + end subroutine test_build_valid + + + !> Try to read values from an empty [build] table + subroutine test_build_empty(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + character(:), allocatable :: temp_file + integer :: unit + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'name = "example"', & + & '[build]', & + & '[library]' + close(unit) + + call get_package_data(package, temp_file, error) + + if (allocated(error)) return + + if (.not.package%build%auto_executables) then + call test_failed(error, "Wong default value of 'auto-executables' read, expecting .true.") + return + end if + + if (.not.package%build%auto_tests) then + call test_failed(error, "Wong default value of 'auto-tests' read, expecting .true.") + return + end if + + end subroutine test_build_empty + + + !> Try to read values from a [build] table with invalid values + subroutine test_build_invalid_values(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + character(:), allocatable :: temp_file + integer :: unit + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'name = "example"', & + & '[build]', & + & 'auto-executables = "false"' + close(unit) + + call get_package_data(package, temp_file, error) + + end subroutine test_build_invalid_values + + + !> Libraries can be created from empty tables + subroutine test_library_empty(error) + use fpm_manifest_library + use fpm_toml, only : new_table, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(library_config_t) :: library + + call new_table(table) + + call new_library(library, table, error) + if (allocated(error)) return + + call check_string(error, library%source_dir, "src", & + & "Default library source-dir") + if (allocated(error)) return + + if (.not.allocated(library%include_dir)) then + call test_failed(error,"Default include-dir list not allocated") + return + end if + + if (.not.("include".in.library%include_dir)) then + call test_failed(error,"'include' not in default include-dir list") + return + end if + + end subroutine test_library_empty + + + !> Pass a TOML table with not allowed keys + subroutine test_library_wrongkey(error) + use fpm_manifest_library + use fpm_toml, only : new_table, add_table, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_table), pointer :: child + integer :: stat + type(library_config_t) :: library + + call new_table(table) + call add_table(table, 'not-allowed', child, stat) + + call new_library(library, table, error) + + end subroutine test_library_wrongkey + + + !> Packages cannot be created from empty tables + subroutine test_package_simple(error) + use fpm_manifest_package + use fpm_toml, only : new_table, add_table, add_array, set_value, & + & toml_table, toml_array + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_table), pointer :: child, child2 + type(toml_array), pointer :: children + integer :: stat + type(package_config_t) :: package + + call new_table(table) + call set_value(table, 'name', '"example"', stat) + call set_value(table, 'license', '"MIT"', stat) + call add_table(table, 'dev-dependencies', child, stat) + call add_table(child, 'pkg1', child2, stat) + call set_value(child2, 'git', '"https://github.com/fortran-lang/pkg1"', stat) + call add_table(child, 'pkg2', child2) + call set_value(child2, 'git', '"https://gitlab.com/fortran-lang/pkg2"', stat) + call set_value(child2, 'branch', '"devel"', stat) + call add_table(child, 'pkg3', child2) + call set_value(child2, 'git', '"https://bitbucket.org/fortran-lang/pkg3"', stat) + call set_value(child2, 'rev', '"9fceb02d0ae598e95dc970b74767f19372d61af8"', stat) + call add_table(child, 'pkg4', child2) + call set_value(child2, 'git', '"https://gitea.com/fortran-lang/pkg4"', stat) + call set_value(child2, 'tag', '"v1.8.5-rc3"', stat) + call add_array(table, 'test', children, stat) + call add_table(children, child, stat) + call set_value(child, 'name', '"tester"', stat) + + call new_package(package, table, error) + + end subroutine test_package_simple + + + !> Packages cannot be created from empty tables + subroutine test_package_empty(error) + use fpm_manifest_package + use fpm_toml, only : new_table, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(package_config_t) :: package + + call new_table(table) + + call new_package(package, table, error) + + end subroutine test_package_empty + + + !> Create an array in the package name, which should cause an error + subroutine test_package_typeerror(error) + use fpm_manifest_package + use fpm_toml, only : new_table, add_array, toml_table, toml_array + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_array), pointer :: child + integer :: stat + type(package_config_t) :: package + + call new_table(table) + call add_array(table, "name", child, stat) + + call new_package(package, table, error) + + end subroutine test_package_typeerror + + + !> Try to create a new package without a name field + subroutine test_package_noname(error) + use fpm_manifest_package + use fpm_toml, only : new_table, add_table, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_table), pointer :: child + integer :: stat + type(package_config_t) :: package + + call new_table(table) + call add_table(table, "library", child, stat) + call add_table(table, "dev-dependencies", child, stat) + call add_table(table, "dependencies", child, stat) + + call new_package(package, table, error) + + end subroutine test_package_noname + + + !> Try to read executables from a mixed type array + subroutine test_package_wrongexe(error) + use fpm_manifest_package + use fpm_toml, only : new_table, set_value, add_array, toml_table, toml_array + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_array), pointer :: children, children2 + integer :: stat + type(package_config_t) :: package + + call new_table(table) + call set_value(table, 'name', '"example"', stat) + call add_array(table, 'executable', children, stat) + call add_array(children, children2, stat) + + call new_package(package, table, error) + + end subroutine test_package_wrongexe + + + !> Try to read tests from a mixed type array + subroutine test_package_wrongtest(error) + use fpm_manifest_package + use fpm_toml, only : new_table, set_value, add_array, toml_table, toml_array + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_array), pointer :: children, children2 + integer :: stat + type(package_config_t) :: package + + call new_table(table) + call set_value(table, 'name', '"example"', stat) + call add_array(table, 'test', children, stat) + call add_array(children, children2, stat) + + call new_package(package, table, error) + + end subroutine test_package_wrongtest + + + !> Try to read tests from a mixed type array + subroutine test_package_duplicate(error) + use fpm_manifest_package + use fpm_toml, only : set_value, add_table, add_array, toml_table, toml_array + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_table), pointer :: child + type(toml_array), pointer :: children + integer :: stat + type(package_config_t) :: package + + table = toml_table() + call set_value(table, 'name', '"example"', stat) + call add_array(table, 'test', children, stat) + call add_table(children, child, stat) + call set_value(child, 'name', '"prog"', stat) + call add_table(children, child, stat) + call set_value(child, 'name', '"prog"', stat) + + call new_package(package, table, error) + + end subroutine test_package_duplicate + + + !> Tests cannot be created from empty tables + subroutine test_test_simple(error) + use fpm_manifest_test + use fpm_toml, only : new_table, set_value, add_table, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_table), pointer :: child + integer :: stat + type(test_config_t) :: test + + call new_table(table) + call set_value(table, 'name', '"example"', stat) + call set_value(table, 'source-dir', '"tests"', stat) + call set_value(table, 'main', '"tester.f90"', stat) + call add_table(table, 'dependencies', child, stat) + + call new_test(test, table, error) + if (allocated(error)) return + + call check_string(error, test%main, "tester.f90", "Test main") + if (allocated(error)) return + + end subroutine test_test_simple + + + !> Tests cannot be created from empty tables + subroutine test_test_empty(error) + use fpm_manifest_test + use fpm_toml, only : new_table, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(test_config_t) :: test + + call new_table(table) + + call new_test(test, table, error) + + end subroutine test_test_empty + + + !> Pass a wrong TOML type to the name field of the test + subroutine test_test_typeerror(error) + use fpm_manifest_test + use fpm_toml, only : new_table, add_table, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_table), pointer :: child + integer :: stat + type(test_config_t) :: test + + call new_table(table) + call add_table(table, 'name', child, stat) + + call new_test(test, table, error) + + end subroutine test_test_typeerror + + + !> Pass a TOML table with insufficient entries to the test constructor + subroutine test_test_noname(error) + use fpm_manifest_test + use fpm_toml, only : new_table, add_table, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_table), pointer :: child + integer :: stat + type(test_config_t) :: test + + call new_table(table) + call add_table(table, 'dependencies', child, stat) + + call new_test(test, table, error) + + end subroutine test_test_noname + + + !> Pass a TOML table with not allowed keys + subroutine test_test_wrongkey(error) + use fpm_manifest_test + use fpm_toml, only : new_table, add_table, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_table), pointer :: child + integer :: stat + type(test_config_t) :: test + + call new_table(table) + call add_table(table, 'not-supported', child, stat) + + call new_test(test, table, error) + + end subroutine test_test_wrongkey + + + !> Create a simple example entry + subroutine test_example_simple(error) + use fpm_manifest_example + use fpm_toml, only : new_table, set_value, add_table, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_table), pointer :: child + integer :: stat + type(example_config_t) :: example + + call new_table(table) + call set_value(table, 'name', '"example"', stat) + call set_value(table, 'source-dir', '"demos"', stat) + call set_value(table, 'main', '"demo.f90"', stat) + call add_table(table, 'dependencies', child, stat) + + call new_example(example, table, error) + if (allocated(error)) return + + call check_string(error, example%main, "demo.f90", "Example main") + if (allocated(error)) return + + end subroutine test_example_simple + + + !> Examples cannot be created from empty tables + subroutine test_example_empty(error) + use fpm_manifest_example + use fpm_toml, only : new_table, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(example_config_t) :: example + + call new_table(table) + + call new_example(example, table, error) + + end subroutine test_example_empty + + + !> Test link options + subroutine test_link_string(error) + use fpm_manifest_build + use fpm_toml, only : set_value, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + integer :: stat + type(build_config_t) :: build + + table = toml_table() + call set_value(table, "link", "z", stat=stat) + + call new_build_config(build, table, error) + + end subroutine test_link_string + + + !> Test link options + subroutine test_link_array(error) + use fpm_manifest_build + use fpm_toml, only : add_array, set_value, toml_table, toml_array + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_array), pointer :: children + integer :: stat + type(build_config_t) :: build + + table = toml_table() + call add_array(table, "link", children, stat=stat) + call set_value(children, 1, "blas", stat=stat) + call set_value(children, 2, "lapack", stat=stat) + + call new_build_config(build, table, error) + + end subroutine test_link_array + + + !> Test link options + subroutine test_invalid_link(error) + use fpm_manifest_build + use fpm_toml, only : add_table, toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_table), pointer :: child + integer :: stat + type(build_config_t) :: build + + table = toml_table() + call add_table(table, "link", child, stat=stat) + + call new_build_config(build, table, error) + + end subroutine test_invalid_link + + + subroutine test_install_library(error) + use fpm_manifest_install + use fpm_toml, only : toml_table, set_value + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(install_config_t) :: install + + table = toml_table() + call set_value(table, "library", .true.) + + call new_install_config(install, table, error) + if (allocated(error)) return + + if (.not.install%library) then + call test_failed(error, "Library entry should be true") + return + end if + + end subroutine test_install_library + + + subroutine test_install_empty(error) + use fpm_manifest_install + use fpm_toml, only : toml_table + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(install_config_t) :: install + + table = toml_table() + + call new_install_config(install, table, error) + if (allocated(error)) return + + if (install%library) then + call test_failed(error, "Library default should be false") + return + end if + + end subroutine test_install_empty + + + subroutine test_install_wrongkey(error) + use fpm_manifest_install + use fpm_toml, only : toml_table, set_value + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(install_config_t) :: install + + table = toml_table() + call set_value(table, "prefix", "/some/install/path") + + call new_install_config(install, table, error) + + end subroutine test_install_wrongkey + + +end module test_manifest diff --git a/test/fpm_test/test_module_dependencies.f90 b/test/fpm_test/test_module_dependencies.f90 new file mode 100644 index 0000000..f193646 --- /dev/null +++ b/test/fpm_test/test_module_dependencies.f90 @@ -0,0 +1,666 @@ +!> Define tests for the `fpm_sources` module (module dependency checking) +module test_module_dependencies + use testsuite, only : new_unittest, unittest_t, error_t, test_failed + use fpm_targets, only: targets_from_sources, resolve_module_dependencies, & + resolve_target_linking, build_target_t, build_target_ptr, & + FPM_TARGET_EXECUTABLE, FPM_TARGET_OBJECT, FPM_TARGET_ARCHIVE + use fpm_model, only: fpm_model_t, srcfile_t, & + FPM_UNIT_UNKNOWN, FPM_UNIT_PROGRAM, FPM_UNIT_MODULE, & + FPM_UNIT_SUBMODULE, FPM_UNIT_SUBPROGRAM, FPM_UNIT_CSOURCE, & + FPM_UNIT_CHEADER, FPM_SCOPE_UNKNOWN, FPM_SCOPE_LIB, & + FPM_SCOPE_DEP, FPM_SCOPE_APP, FPM_SCOPE_TEST + use fpm_strings, only: string_t, operator(.in.) + use fpm, only: check_modules_for_duplicates + implicit none + private + + public :: collect_module_dependencies, operator(.in.) + + interface operator(.in.) + module procedure target_in + end interface + +contains + + + !> Collect all exported unit tests + subroutine collect_module_dependencies(testsuite) + + !> Collection of tests + type(unittest_t), allocatable, intent(out) :: testsuite(:) + + testsuite = [ & + & new_unittest("library-module-use", test_library_module_use), & + & new_unittest("program-module-use", test_program_module_use), & + & new_unittest("program-with-module", test_program_with_module), & + & new_unittest("program-own-module-use", test_program_own_module_use), & + & new_unittest("missing-library-use", & + test_missing_library_use, should_fail=.true.), & + & new_unittest("missing-program-use", & + test_missing_program_use, should_fail=.true.), & + & new_unittest("invalid-library-use", & + test_invalid_library_use, should_fail=.true.), & + & new_unittest("package-with-no-duplicates", & + test_package_with_no_module_duplicates), & + & new_unittest("package-with-duplicates-in-same-source", & + test_package_module_duplicates_same_source, should_fail=.true.), & + & new_unittest("package-with-duplicates-in-one-package", & + test_package_module_duplicates_one_package, should_fail=.true.), & + & new_unittest("package-with-duplicates-in-two-packages", & + test_package_module_duplicates_two_packages, should_fail=.true.), & + & new_unittest("subdirectory-module-use", & + test_subdirectory_module_use), & + & new_unittest("invalid-subdirectory-module-use", & + test_invalid_subdirectory_module_use, should_fail=.true.) & + ] + + end subroutine collect_module_dependencies + + + !> Check library module using another library module + subroutine test_library_module_use(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(fpm_model_t) :: model + type(build_target_ptr), allocatable :: targets(:) + + model%output_directory = '' + allocate(model%packages(1)) + allocate(model%packages(1)%sources(2)) + + model%packages(1)%sources(1) = new_test_source(FPM_UNIT_MODULE,file_name="src/my_mod_1.f90", & + scope = FPM_SCOPE_LIB, & + provides=[string_t('my_mod_1')]) + + model%packages(1)%sources(2) = new_test_source(FPM_UNIT_MODULE,file_name="src/my_mod_2.f90", & + scope = FPM_SCOPE_LIB, & + provides=[string_t('my_mod_2')], & + uses=[string_t('my_mod_1')]) + + call targets_from_sources(targets,model,error) + if (allocated(error)) return + + if (allocated(error)) then + return + end if + if (size(targets) /= 3) then + call test_failed(error,'Incorrect number of targets - expecting three') + return + end if + + call check_target(targets(1)%ptr,type=FPM_TARGET_ARCHIVE,n_depends=2, & + deps = [targets(2),targets(3)], & + links = targets(2:3), error=error) + + if (allocated(error)) return + + + call check_target(targets(2)%ptr,type=FPM_TARGET_OBJECT,n_depends=0, & + source=model%packages(1)%sources(1),error=error) + + if (allocated(error)) return + + + call check_target(targets(3)%ptr,type=FPM_TARGET_OBJECT,n_depends=1, & + deps=[targets(2)],source=model%packages(1)%sources(2),error=error) + + if (allocated(error)) return + + end subroutine test_library_module_use + + + !> Check a program using a library module + !> Each program generates two targets: object file and executable + !> + subroutine test_program_module_use(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + call test_scope(FPM_SCOPE_APP,error) + if (allocated(error)) return + + call test_scope(FPM_SCOPE_TEST,error) + if (allocated(error)) return + + contains + + subroutine test_scope(exe_scope,error) + integer, intent(in) :: exe_scope + type(error_t), allocatable, intent(out) :: error + + integer :: i + type(fpm_model_t) :: model + type(build_target_ptr), allocatable :: targets(:) + character(:), allocatable :: scope_str + + model%output_directory = '' + allocate(model%packages(1)) + allocate(model%packages(1)%sources(2)) + + scope_str = merge('FPM_SCOPE_APP ','FPM_SCOPE_TEST',exe_scope==FPM_SCOPE_APP)//' - ' + + model%packages(1)%sources(1) = new_test_source(FPM_UNIT_MODULE,file_name="src/my_mod_1.f90", & + scope = FPM_SCOPE_LIB, & + provides=[string_t('my_mod_1')]) + + model%packages(1)%sources(2) = new_test_source(FPM_UNIT_PROGRAM,file_name="app/my_program.f90", & + scope=exe_scope, & + uses=[string_t('my_mod_1')]) + + call targets_from_sources(targets,model,error) + if (allocated(error)) return + + if (size(targets) /= 4) then + call test_failed(error,scope_str//'Incorrect number of targets - expecting three') + return + end if + + call check_target(targets(1)%ptr,type=FPM_TARGET_ARCHIVE,n_depends=1, & + deps=[targets(2)],links=[targets(2)],error=error) + + if (allocated(error)) return + + call check_target(targets(2)%ptr,type=FPM_TARGET_OBJECT,n_depends=0, & + source=model%packages(1)%sources(1),error=error) + + if (allocated(error)) return + + call check_target(targets(3)%ptr,type=FPM_TARGET_OBJECT,n_depends=1, & + deps=[targets(2)],source=model%packages(1)%sources(2),error=error) + + if (allocated(error)) return + + call check_target(targets(4)%ptr,type=FPM_TARGET_EXECUTABLE,n_depends=2, & + deps=[targets(1),targets(3)], & + links=[targets(3)], error=error) + + if (allocated(error)) return + + end subroutine test_scope + + end subroutine test_program_module_use + + + !> Check program with module in single source file + !> (Resulting target should not include itself as a dependency) + subroutine test_program_with_module(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + integer :: i + type(fpm_model_t) :: model + type(build_target_ptr), allocatable :: targets(:) + + model%output_directory = '' + allocate(model%packages(1)) + allocate(model%packages(1)%sources(1)) + + model%packages(1)%sources(1) = new_test_source(FPM_UNIT_PROGRAM,file_name="app/my_program.f90", & + scope = FPM_SCOPE_APP, & + provides=[string_t('app_mod')], & + uses=[string_t('app_mod')]) + + call targets_from_sources(targets,model,error) + if (allocated(error)) return + + if (size(targets) /= 2) then + write(*,*) size(targets) + call test_failed(error,'Incorrect number of targets - expecting two') + return + end if + + call check_target(targets(1)%ptr,type=FPM_TARGET_OBJECT,n_depends=0, & + source=model%packages(1)%sources(1),error=error) + + if (allocated(error)) return + + call check_target(targets(2)%ptr,type=FPM_TARGET_EXECUTABLE,n_depends=1, & + deps=[targets(1)],links=[targets(1)],error=error) + + if (allocated(error)) return + + end subroutine test_program_with_module + + + !> Check program using modules in same directory + subroutine test_program_own_module_use(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + call test_scope(FPM_SCOPE_APP,error) + if (allocated(error)) return + + call test_scope(FPM_SCOPE_TEST,error) + if (allocated(error)) return + + contains + + subroutine test_scope(exe_scope,error) + integer, intent(in) :: exe_scope + type(error_t), allocatable, intent(out) :: error + + type(fpm_model_t) :: model + type(build_target_ptr), allocatable :: targets(:) + character(:), allocatable :: scope_str + + model%output_directory = '' + allocate(model%packages(1)) + allocate(model%packages(1)%sources(3)) + + scope_str = merge('FPM_SCOPE_APP ','FPM_SCOPE_TEST',exe_scope==FPM_SCOPE_APP)//' - ' + + model%packages(1)%sources(1) = new_test_source(FPM_UNIT_MODULE,file_name="app/app_mod1.f90", & + scope = exe_scope, & + provides=[string_t('app_mod1')]) + + model%packages(1)%sources(2) = new_test_source(FPM_UNIT_MODULE,file_name="app/app_mod2.f90", & + scope = exe_scope, & + provides=[string_t('app_mod2')],uses=[string_t('app_mod1')]) + + model%packages(1)%sources(3) = new_test_source(FPM_UNIT_PROGRAM,file_name="app/my_program.f90", & + scope=exe_scope, & + uses=[string_t('app_mod2')]) + + call targets_from_sources(targets,model,error) + if (allocated(error)) return + + if (size(targets) /= 4) then + call test_failed(error,scope_str//'Incorrect number of targets - expecting three') + return + end if + + call check_target(targets(1)%ptr,type=FPM_TARGET_OBJECT,n_depends=0, & + source=model%packages(1)%sources(1),error=error) + + if (allocated(error)) return + + call check_target(targets(2)%ptr,type=FPM_TARGET_OBJECT,n_depends=1, & + source=model%packages(1)%sources(2),deps=[targets(1)],error=error) + + if (allocated(error)) return + + call check_target(targets(3)%ptr,type=FPM_TARGET_OBJECT,n_depends=1, & + source=model%packages(1)%sources(3),deps=[targets(2)],error=error) + + if (allocated(error)) return + + call check_target(targets(4)%ptr,type=FPM_TARGET_EXECUTABLE,n_depends=1, & + deps=[targets(3)],links=targets(1:3), error=error) + + if (allocated(error)) return + + end subroutine test_scope + end subroutine test_program_own_module_use + + + !> Check missing library module dependency + subroutine test_missing_library_use(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(fpm_model_t) :: model + type(build_target_ptr), allocatable :: targets(:) + + model%output_directory = '' + allocate(model%packages(1)) + allocate(model%packages(1)%sources(2)) + + model%packages(1)%sources(1) = new_test_source(FPM_UNIT_MODULE,file_name="src/my_mod_1.f90", & + scope = FPM_SCOPE_LIB, & + provides=[string_t('my_mod_1')]) + + model%packages(1)%sources(2) = new_test_source(FPM_UNIT_MODULE,file_name="src/my_mod_2.f90", & + scope = FPM_SCOPE_LIB, & + provides=[string_t('my_mod_2')], & + uses=[string_t('my_mod_3')]) + + call targets_from_sources(targets,model,error) + + end subroutine test_missing_library_use + + + !> Check missing program module dependency + subroutine test_missing_program_use(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(fpm_model_t) :: model + type(build_target_ptr), allocatable :: targets(:) + + model%output_directory = '' + allocate(model%packages(1)) + allocate(model%packages(1)%sources(2)) + + model%packages(1)%sources(1) = new_test_source(FPM_UNIT_MODULE,file_name="src/my_mod_1.f90", & + scope = FPM_SCOPE_LIB, & + provides=[string_t('my_mod_1')]) + + model%packages(1)%sources(2) = new_test_source(FPM_UNIT_PROGRAM,file_name="app/my_program.f90", & + scope=FPM_SCOPE_APP, & + uses=[string_t('my_mod_2')]) + + call targets_from_sources(targets,model,error) + + end subroutine test_missing_program_use + + + !> Check library module using a non-library module + subroutine test_invalid_library_use(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(fpm_model_t) :: model + type(build_target_ptr), allocatable :: targets(:) + + model%output_directory = '' + allocate(model%packages(1)) + allocate(model%packages(1)%sources(2)) + + model%packages(1)%sources(1) = new_test_source(FPM_UNIT_MODULE,file_name="app/app_mod.f90", & + scope = FPM_SCOPE_APP, & + provides=[string_t('app_mod')]) + + model%packages(1)%sources(2) = new_test_source(FPM_UNIT_MODULE,file_name="src/my_mod.f90", & + scope = FPM_SCOPE_LIB, & + provides=[string_t('my_mod')], & + uses=[string_t('app_mod')]) + + call targets_from_sources(targets,model,error) + + end subroutine test_invalid_library_use + + + !> Check program using a non-library module in a sub-directory + subroutine test_subdirectory_module_use(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(fpm_model_t) :: model + type(build_target_ptr), allocatable :: targets(:) + + model%output_directory = '' + allocate(model%packages(1)) + allocate(model%packages(1)%sources(2)) + + model%packages(1)%sources(1) = new_test_source(FPM_UNIT_MODULE,file_name="app/subdir/app_mod.f90", & + scope = FPM_SCOPE_APP, & + provides=[string_t('app_mod')]) + + model%packages(1)%sources(2) = new_test_source(FPM_UNIT_PROGRAM,file_name="app/my_program.f90", & + scope=FPM_SCOPE_APP, & + uses=[string_t('app_mod')]) + + call targets_from_sources(targets,model,error) + + end subroutine test_subdirectory_module_use + + !> Check program with no duplicate modules + subroutine test_package_with_no_module_duplicates(error) + + type(error_t), allocatable, intent(out) :: error + + type(fpm_model_t) :: model + logical :: duplicates_found = .false. + + allocate(model%packages(1)) + allocate(model%packages(1)%sources(2)) + + model%packages(1)%sources(1) = new_test_source(FPM_UNIT_MODULE,file_name="src/my_mod_1.f90", & + scope = FPM_SCOPE_LIB, provides=[string_t('my_mod_1')]) + + model%packages(1)%sources(2) = new_test_source(FPM_UNIT_MODULE,file_name="src/my_mod_2.f90", & + scope = FPM_SCOPE_LIB, provides=[string_t('my_mod_2')]) + + call check_modules_for_duplicates(model, duplicates_found) + if (duplicates_found) then + call test_failed(error,'Duplicate modules found') + return + end if + end subroutine test_package_with_no_module_duplicates + + !> Check program with duplicate modules in same source file + subroutine test_package_module_duplicates_same_source(error) + + type(error_t), allocatable, intent(out) :: error + + type(fpm_model_t) :: model + logical :: duplicates_found + + allocate(model%packages(1)) + allocate(model%packages(1)%sources(1)) + + model%packages(1)%sources(1) = new_test_source(FPM_UNIT_MODULE,file_name="src/my_mod_1.f90", & + scope = FPM_SCOPE_LIB, provides=[string_t('my_mod_1'), string_t('my_mod_1')]) + + call check_modules_for_duplicates(model, duplicates_found) + if (duplicates_found) then + call test_failed(error,'Duplicate modules found') + return + end if + end subroutine test_package_module_duplicates_same_source + + !> Check program with duplicate modules in two different source files in one package + subroutine test_package_module_duplicates_one_package(error) + + type(error_t), allocatable, intent(out) :: error + + type(fpm_model_t) :: model + logical :: duplicates_found + + allocate(model%packages(1)) + allocate(model%packages(1)%sources(2)) + + model%packages(1)%sources(1) = new_test_source(FPM_UNIT_MODULE,file_name="src/my_mod_1_a.f90", & + scope = FPM_SCOPE_LIB, provides=[string_t('my_mod_1')]) + + model%packages(1)%sources(2) = new_test_source(FPM_UNIT_MODULE,file_name="src/my_mod_1_b.f90", & + scope = FPM_SCOPE_LIB, provides=[string_t('my_mod_1')]) + + call check_modules_for_duplicates(model, duplicates_found) + if (duplicates_found) then + call test_failed(error,'Duplicate modules found') + return + end if + end subroutine test_package_module_duplicates_one_package + + !> Check program with duplicate modules in two different packages + subroutine test_package_module_duplicates_two_packages(error) + + type(error_t), allocatable, intent(out) :: error + + type(fpm_model_t) :: model + logical :: duplicates_found + + allocate(model%packages(2)) + allocate(model%packages(1)%sources(1)) + allocate(model%packages(2)%sources(1)) + + model%packages(1)%sources(1) = new_test_source(FPM_UNIT_MODULE,file_name="src/subdir1/my_mod_1.f90", & + scope = FPM_SCOPE_LIB, provides=[string_t('my_mod_1')]) + + model%packages(2)%sources(1) = new_test_source(FPM_UNIT_MODULE,file_name="src/subdir2/my_mod_1.f90", & + scope = FPM_SCOPE_LIB, provides=[string_t('my_mod_1')]) + + call check_modules_for_duplicates(model, duplicates_found) + if (duplicates_found) then + call test_failed(error,'Duplicate modules found') + return + end if + end subroutine test_package_module_duplicates_two_packages + + !> Check program using a non-library module in a differente sub-directory + subroutine test_invalid_subdirectory_module_use(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(fpm_model_t) :: model + type(build_target_ptr), allocatable :: targets(:) + + model%output_directory = '' + allocate(model%packages(1)) + allocate(model%packages(1)%sources(2)) + + model%packages(1)%sources(1) = new_test_source(FPM_UNIT_MODULE,file_name="app/diff_dir/app_mod.f90", & + scope = FPM_SCOPE_APP, & + provides=[string_t('app_mod')]) + + model%packages(1)%sources(2) = new_test_source(FPM_UNIT_PROGRAM,file_name="app/prog_dir/my_program.f90", & + scope=FPM_SCOPE_APP, & + uses=[string_t('app_mod')]) + + call targets_from_sources(targets,model,error) + + end subroutine test_invalid_subdirectory_module_use + + !> Helper to create a new srcfile_t + function new_test_source(type,file_name, scope, uses, provides) result(src) + integer, intent(in) :: type + character(*), intent(in) :: file_name + integer, intent(in) :: scope + type(string_t), intent(in), optional :: uses(:) + type(string_t), intent(in), optional :: provides(:) + type(srcfile_t) :: src + + src%file_name = file_name + src%unit_scope = scope + src%unit_type = type + + if (present(provides)) then + src%modules_provided = provides + else + allocate(src%modules_provided(0)) + end if + + if (present(uses)) then + src%modules_used = uses + else + allocate(src%modules_used(0)) + end if + + allocate(src%include_dependencies(0)) + + end function new_test_source + + + !> Helper to check an expected output target + subroutine check_target(target,type,n_depends,deps,links,source,error) + type(build_target_t), intent(in) :: target + integer, intent(in) :: type + integer, intent(in) :: n_depends + type(srcfile_t), intent(in), optional :: source + type(build_target_ptr), intent(in), optional :: deps(:) + type(build_target_ptr), intent(in), optional :: links(:) + type(error_t), intent(out), allocatable :: error + + integer :: i + + if (target%target_type /= type) then + call test_failed(error,'Unexpected target_type for target "'//target%output_file//'"') + return + end if + + if (size(target%dependencies) /= n_depends) then + call test_failed(error,'Wrong number of dependencies for target "'//target%output_file//'"') + return + end if + + if (present(deps)) then + + do i=1,size(deps) + + if (.not.(deps(i)%ptr .in. target%dependencies)) then + call test_failed(error,'Missing dependency ('//deps(i)%ptr%output_file//& + ') for target "'//target%output_file//'"') + return + end if + + end do + + end if + + if (present(links)) then + + do i=1,size(links) + + if (.not.(links(i)%ptr%output_file .in. target%link_objects)) then + call test_failed(error,'Missing object ('//links(i)%ptr%output_file//& + ') for executable "'//target%output_file//'"') + return + end if + + end do + + if (size(links) > size(target%link_objects)) then + + call test_failed(error,'There are missing link objects for target "'& + //target%output_file//'"') + return + + elseif (size(links) < size(target%link_objects)) then + + call test_failed(error,'There are more link objects than expected for target "'& + //target%output_file//'"') + return + + end if + + end if + + if (present(source)) then + + if (allocated(target%source)) then + if (target%source%file_name /= source%file_name) then + call test_failed(error,'Incorrect source ('//target%source%file_name//') for target "'//& + target%output_file//'"'//new_line('a')//' expected "'//source%file_name//'"') + return + end if + + else + call test_failed(error,'Expecting source for target "'//target%output_file//'" but none found') + return + end if + + else + + if (allocated(target%source)) then + call test_failed(error,'Found source ('//target%source%file_name//') for target "'//& + target%output_file//'" but none expected') + return + end if + + end if + + end subroutine check_target + + + !> Helper to check if a build target is in a list of build_target_ptr + logical function target_in(needle,haystack) + type(build_target_t), intent(in), target :: needle + type(build_target_ptr), intent(in) :: haystack(:) + + integer :: i + + target_in = .false. + do i=1,size(haystack) + + if (associated(haystack(i)%ptr,needle)) then + target_in = .true. + return + end if + + end do + + end function target_in + + +end module test_module_dependencies diff --git a/test/fpm_test/test_package_dependencies.f90 b/test/fpm_test/test_package_dependencies.f90 new file mode 100644 index 0000000..b70ac13 --- /dev/null +++ b/test/fpm_test/test_package_dependencies.f90 @@ -0,0 +1,240 @@ +!> Define tests for the `fpm_dependency` module +module test_package_dependencies + use fpm_filesystem, only: get_temp_filename + use testsuite, only : new_unittest, unittest_t, error_t, test_failed + use fpm_dependency + use fpm_manifest + use fpm_manifest_dependency + use fpm_toml + implicit none + private + + public :: collect_package_dependencies + + type, extends(dependency_tree_t) :: mock_dependency_tree_t + contains + procedure :: resolve_dependency => resolve_dependency_once + end type mock_dependency_tree_t + + +contains + + + !> Collect all exported unit tests + subroutine collect_package_dependencies(testsuite) + + !> Collection of tests + type(unittest_t), allocatable, intent(out) :: testsuite(:) + + testsuite = [ & + & new_unittest("cache-load-dump", test_cache_load_dump), & + & new_unittest("cache-dump-load", test_cache_dump_load), & + & new_unittest("status-after-load", test_status), & + & new_unittest("add-dependencies", test_add_dependencies)] + + end subroutine collect_package_dependencies + + + !> Round trip of the dependency cache from a dependency tree to a TOML document + !> to a dependency tree + subroutine test_cache_dump_load(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(dependency_tree_t) :: deps + type(dependency_config_t) :: dep + integer :: unit + + call new_dependency_tree(deps) + call resize(deps%dep, 5) + deps%ndep = 3 + dep%name = "dep1" + dep%path = "fpm-tmp1-dir" + call new_dependency_node(deps%dep(1), dep, proj_dir=dep%path) + dep%name = "dep2" + dep%path = "fpm-tmp2-dir" + call new_dependency_node(deps%dep(2), dep, proj_dir=dep%path) + dep%name = "dep3" + dep%path = "fpm-tmp3-dir" + call new_dependency_node(deps%dep(3), dep, proj_dir=dep%path) + + open(newunit=unit, status='scratch') + call deps%dump(unit, error) + if (.not.allocated(error)) then + rewind(unit) + + call new_dependency_tree(deps) + call resize(deps%dep, 2) + call deps%load(unit, error) + close(unit) + end if + if (allocated(error)) return + + if (deps%ndep /= 3) then + call test_failed(error, "Expected three dependencies in loaded cache") + return + end if + + end subroutine test_cache_dump_load + + + !> Round trip of the dependency cache from a TOML data structure to + !> a dependency tree to a TOML data structure + subroutine test_cache_load_dump(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_table), pointer :: ptr + type(toml_key), allocatable :: list(:) + type(dependency_tree_t) :: deps + + table = toml_table() + call add_table(table, "dep1", ptr) + call set_value(ptr, "version", "1.1.0") + call set_value(ptr, "proj-dir", "fpm-tmp1-dir") + call add_table(table, "dep2", ptr) + call set_value(ptr, "version", "0.55.3") + call set_value(ptr, "proj-dir", "fpm-tmp2-dir") + call set_value(ptr, "git", "https://github.com/fortran-lang/dep2") + call add_table(table, "dep3", ptr) + call set_value(ptr, "version", "20.1.15") + call set_value(ptr, "proj-dir", "fpm-tmp3-dir") + call set_value(ptr, "git", "https://gitlab.com/fortran-lang/dep3") + call set_value(ptr, "rev", "c0ffee") + call add_table(table, "dep4", ptr) + call set_value(ptr, "proj-dir", "fpm-tmp4-dir") + + call new_dependency_tree(deps) + call deps%load(table, error) + if (allocated(error)) return + + if (deps%ndep /= 4) then + call test_failed(error, "Expected four dependencies in loaded cache") + return + end if + + call table%destroy + table = toml_table() + + call deps%dump(table, error) + if (allocated(error)) return + + call table%get_keys(list) + + if (size(list) /= 4) then + call test_failed(error, "Expected four dependencies in dumped cache") + return + end if + + end subroutine test_cache_load_dump + + + subroutine test_status(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_table), pointer :: ptr + type(toml_key), allocatable :: list(:) + type(dependency_tree_t) :: deps + + table = toml_table() + call add_table(table, "dep1", ptr) + call set_value(ptr, "version", "1.1.0") + call set_value(ptr, "proj-dir", "fpm-tmp1-dir") + call add_table(table, "dep2", ptr) + call set_value(ptr, "version", "0.55.3") + call set_value(ptr, "proj-dir", "fpm-tmp2-dir") + call set_value(ptr, "git", "https://github.com/fortran-lang/dep2") + + call new_dependency_tree(deps) + call deps%load(table, error) + if (allocated(error)) return + + if (deps%finished()) then + call test_failed(error, "Newly initialized dependency tree cannot be reolved") + return + end if + + end subroutine test_status + + + subroutine test_add_dependencies(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table) :: table + type(toml_table), pointer :: child, ptr + type(toml_key), allocatable :: list(:) + type(mock_dependency_tree_t) :: deps + type(dependency_config_t), allocatable :: nodes(:) + + table = toml_table() + call add_table(table, "sub1", ptr) + call set_value(ptr, "path", "external") + call add_table(table, "lin2", ptr) + call set_value(ptr, "git", "https://github.com/fortran-lang/lin2") + call add_table(table, "pkg3", ptr) + call set_value(ptr, "git", "https://gitlab.com/fortran-lang/pkg3") + call set_value(ptr, "rev", "c0ffee") + call add_table(table, "proj4", ptr) + call set_value(ptr, "path", "vendor") + + call new_dependencies(nodes, table, error) + if (allocated(error)) return + + call new_dependency_tree(deps%dependency_tree_t) + call deps%add(nodes, error) + if (allocated(error)) return + + if (deps%finished()) then + call test_failed(error, "Newly added nodes cannot be already resolved") + return + end if + + if (deps%ndep /= 4) then + call test_failed(error, "Expected for dependencies in tree") + return + end if + + call deps%resolve(".", error) + if (allocated(error)) return + + if (.not.deps%finished()) then + call test_failed(error, "Mocked dependency tree must resolve in one step") + return + end if + + end subroutine test_add_dependencies + + + !> Resolve a single dependency node + subroutine resolve_dependency_once(self, dependency, root, error) + !> Mock instance of the dependency tree + class(mock_dependency_tree_t), intent(inout) :: self + !> Dependency configuration to add + type(dependency_node_t), intent(inout) :: dependency + !> Current installation prefix + character(len=*), intent(in) :: root + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + character(len=:), allocatable :: manifest, proj_dir, revision + logical :: fetch + + if (dependency%done) then + call test_failed(error, "Should only visit this node once") + return + end if + dependency%done = .true. + + end subroutine resolve_dependency_once + + +end module test_package_dependencies diff --git a/test/fpm_test/test_source_parsing.f90 b/test/fpm_test/test_source_parsing.f90 new file mode 100644 index 0000000..79a4d7a --- /dev/null +++ b/test/fpm_test/test_source_parsing.f90 @@ -0,0 +1,758 @@ +!> Define tests for the `fpm_sources` module (parsing routines) +module test_source_parsing + use testsuite, only : new_unittest, unittest_t, error_t, test_failed + use fpm_filesystem, only: get_temp_filename + use fpm_source_parsing, only: parse_f_source, parse_c_source + use fpm_model, only: srcfile_t, FPM_UNIT_PROGRAM, FPM_UNIT_MODULE, & + FPM_UNIT_SUBMODULE, FPM_UNIT_SUBPROGRAM, FPM_UNIT_CSOURCE + use fpm_strings, only: operator(.in.) + implicit none + private + + public :: collect_source_parsing + +contains + + + !> Collect all exported unit tests + subroutine collect_source_parsing(testsuite) + + !> Collection of tests + type(unittest_t), allocatable, intent(out) :: testsuite(:) + + testsuite = [ & + & new_unittest("modules-used", test_modules_used), & + & new_unittest("intrinsic-modules-used", test_intrinsic_modules_used), & + & new_unittest("include-stmt", test_include_stmt), & + & new_unittest("program", test_program), & + & new_unittest("module", test_module), & + & new_unittest("program-with-module", test_program_with_module), & + & new_unittest("submodule", test_submodule), & + & new_unittest("submodule-ancestor", test_submodule_ancestor), & + & new_unittest("subprogram", test_subprogram), & + & new_unittest("csource", test_csource), & + & new_unittest("invalid-use-stmt", & + test_invalid_use_stmt, should_fail=.true.), & + & new_unittest("invalid-include-stmt", & + test_invalid_include_stmt, should_fail=.true.), & + & new_unittest("invalid-module", & + test_invalid_module, should_fail=.true.), & + & new_unittest("invalid-submodule", & + test_invalid_submodule, should_fail=.true.) & + ] + + end subroutine collect_source_parsing + + + !> Check parsing of module 'USE' statements + subroutine test_modules_used(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + integer :: unit + character(:), allocatable :: temp_file + type(srcfile_t), allocatable :: f_source + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'program test', & + & ' use module_one', & + & ' use :: module_two', & + & ' use module_three, only: a, b, c', & + & ' use :: module_four, only: a => b', & + & '! use module_not_used', & + & ' implicit none', & + & 'end program test' + close(unit) + + f_source = parse_f_source(temp_file,error) + if (allocated(error)) then + return + end if + + if (f_source%unit_type /= FPM_UNIT_PROGRAM) then + call test_failed(error,'Wrong unit type detected - expecting FPM_UNIT_PROGRAM') + return + end if + + if (size(f_source%modules_provided) /= 0) then + call test_failed(error,'Unexpected modules_provided - expecting zero') + return + end if + + if (size(f_source%modules_used) /= 4) then + call test_failed(error,'Incorrect number of modules_used - expecting four') + return + end if + + if (.not.('module_one' .in. f_source%modules_used)) then + call test_failed(error,'Missing module in modules_used') + return + end if + + if (.not.('module_two' .in. f_source%modules_used)) then + call test_failed(error,'Missing module in modules_used') + return + end if + + if (.not.('module_three' .in. f_source%modules_used)) then + call test_failed(error,'Missing module in modules_used') + return + end if + + if (.not.('module_four' .in. f_source%modules_used)) then + call test_failed(error,'Missing module in modules_used') + return + end if + + if ('module_not_used' .in. f_source%modules_used) then + call test_failed(error,'Commented module found in modules_used') + return + end if + + end subroutine test_modules_used + + + !> Check that intrinsic modules are properly ignore + subroutine test_intrinsic_modules_used(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + integer :: unit + character(:), allocatable :: temp_file + type(srcfile_t), allocatable :: f_source + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'program test', & + & ' use iso_c_binding', & + & ' use iso_fortran_env', & + & ' use ieee_arithmetic', & + & ' use ieee_exceptions', & + & ' use ieee_features', & + & ' implicit none', & + & 'end program test' + close(unit) + + f_source = parse_f_source(temp_file,error) + if (allocated(error)) then + return + end if + + if (size(f_source%modules_provided) /= 0) then + call test_failed(error,'Unexpected modules_provided - expecting zero') + return + end if + + if (size(f_source%modules_used) /= 0) then + call test_failed(error,'Incorrect number of modules_used - expecting zero') + return + end if + + if ('iso_c_binding' .in. f_source%modules_used) then + call test_failed(error,'Intrinsic module found in modules_used') + return + end if + + if ('iso_fortran_env' .in. f_source%modules_used) then + call test_failed(error,'Intrinsic module found in modules_used') + return + end if + + if ('ieee_arithmetic' .in. f_source%modules_used) then + call test_failed(error,'Intrinsic module found in modules_used') + return + end if + + if ('ieee_exceptions' .in. f_source%modules_used) then + call test_failed(error,'Intrinsic module found in modules_used') + return + end if + + if ('ieee_features' .in. f_source%modules_used) then + call test_failed(error,'Intrinsic module found in modules_used') + return + end if + + end subroutine test_intrinsic_modules_used + + + !> Check parsing of include statements + subroutine test_include_stmt(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + integer :: unit + character(:), allocatable :: temp_file + type(srcfile_t), allocatable :: f_source + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'program test', & + & ' implicit none', & + & ' include "included_file.f90"', & + & ' character(*) :: include_comments', & + & ' include_comments = "some comments"', & + & ' contains ', & + & ' include"second_include.f90"', & + & 'end program test' + close(unit) + + f_source = parse_f_source(temp_file,error) + if (allocated(error)) then + return + end if + + if (size(f_source%modules_provided) /= 0) then + call test_failed(error,'Unexpected modules_provided - expecting zero') + return + end if + + if (size(f_source%modules_used) /= 0) then + call test_failed(error,'Incorrect number of modules_used - expecting zero') + return + end if + + if (size(f_source%include_dependencies) /= 2) then + call test_failed(error,'Incorrect number of include_dependencies - expecting two') + return + end if + + if (.not.('included_file.f90' .in. f_source%include_dependencies)) then + call test_failed(error,'Missing include file in include_dependencies') + return + end if + + if (.not.('second_include.f90' .in. f_source%include_dependencies)) then + call test_failed(error,'Missing include file in include_dependencies') + return + end if + + end subroutine test_include_stmt + + !> Try to parse a simple fortran program + subroutine test_program(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + integer :: unit + character(:), allocatable :: temp_file + type(srcfile_t), allocatable :: f_source + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'program my_program', & + & 'use module_one', & + & 'implicit none', & + & 'integer :: module', & + & 'module = 1', & + & 'module= 1', & + & 'module =1', & + & 'module (i) =1', & + & 'contains', & + & 'subroutine f()', & + & 'end subroutine f', & + & 'end program my_program' + close(unit) + + f_source = parse_f_source(temp_file,error) + if (allocated(error)) then + return + end if + + if (f_source%unit_type /= FPM_UNIT_PROGRAM) then + call test_failed(error,'Wrong unit type detected - expecting FPM_UNIT_PROGRAM') + return + end if + + if (size(f_source%modules_provided) /= 0) then + call test_failed(error,'Unexpected modules_provided - expecting zero') + return + end if + + if (size(f_source%modules_used) /= 1) then + call test_failed(error,'Incorrect number of modules_used - expecting one') + return + end if + + if (.not.('module_one' .in. f_source%modules_used)) then + call test_failed(error,'Missing module in modules_used') + return + end if + + end subroutine test_program + + + !> Try to parse fortran module + subroutine test_module(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + integer :: unit + character(:), allocatable :: temp_file + type(srcfile_t), allocatable :: f_source + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'module my_mod', & + & 'use module_one', & + & 'interface', & + & ' module subroutine f()', & + & 'end interface', & + & 'integer :: program', & + & 'program = 1', & + & 'program= 1', & + & 'program =1', & + & 'program (i) =1', & + & 'contains', & + & 'module procedure f()', & + & 'end procedure f', & + & 'end module test' + close(unit) + + f_source = parse_f_source(temp_file,error) + if (allocated(error)) then + return + end if + + if (f_source%unit_type /= FPM_UNIT_MODULE) then + call test_failed(error,'Wrong unit type detected - expecting FPM_UNIT_MODULE') + return + end if + + if (size(f_source%modules_provided) /= 1) then + call test_failed(error,'Unexpected modules_provided - expecting one') + return + end if + + if (size(f_source%modules_used) /= 1) then + call test_failed(error,'Incorrect number of modules_used - expecting one') + return + end if + + if (.not.('my_mod' .in. f_source%modules_provided)) then + call test_failed(error,'Missing module in modules_provided') + return + end if + + if (.not.('module_one' .in. f_source%modules_used)) then + call test_failed(error,'Missing module in modules_used') + return + end if + + end subroutine test_module + + + !> Try to parse combined fortran module and program + !> Check that parsed unit type is FPM_UNIT_PROGRAM + subroutine test_program_with_module(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + integer :: unit + character(:), allocatable :: temp_file + type(srcfile_t), allocatable :: f_source + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'module my_mod', & + & 'use module_one', & + & 'interface', & + & ' module subroutine f()', & + & 'end interface', & + & 'contains', & + & 'module procedure f()', & + & 'end procedure f', & + & 'end module test', & + & 'program my_program', & + & 'use my_mod', & + & 'implicit none', & + & 'end my_program' + close(unit) + + f_source = parse_f_source(temp_file,error) + if (allocated(error)) then + return + end if + + if (f_source%unit_type /= FPM_UNIT_PROGRAM) then + call test_failed(error,'Wrong unit type detected - expecting FPM_UNIT_PROGRAM') + return + end if + + if (size(f_source%modules_provided) /= 1) then + call test_failed(error,'Unexpected modules_provided - expecting one') + return + end if + + if (.not.('my_mod' .in. f_source%modules_provided)) then + call test_failed(error,'Missing module in modules_provided') + return + end if + + if (.not.('module_one' .in. f_source%modules_used)) then + call test_failed(error,'Missing module in modules_used') + return + end if + + if (.not.('my_mod' .in. f_source%modules_used)) then + call test_failed(error,'Missing module in modules_used') + return + end if + + end subroutine test_program_with_module + + + !> Try to parse fortran submodule for ancestry + subroutine test_submodule(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + integer :: unit + character(:), allocatable :: temp_file + type(srcfile_t), allocatable :: f_source + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'submodule (parent) child', & + & 'use module_one', & + & 'end submodule test' + close(unit) + + f_source = parse_f_source(temp_file,error) + if (allocated(error)) then + return + end if + + if (f_source%unit_type /= FPM_UNIT_SUBMODULE) then + call test_failed(error,'Wrong unit type detected - expecting FPM_UNIT_SUBMODULE') + return + end if + + if (size(f_source%modules_provided) /= 1) then + call test_failed(error,'Unexpected modules_provided - expecting one') + return + end if + + if (size(f_source%modules_used) /= 2) then + call test_failed(error,'Incorrect number of modules_used - expecting two') + return + end if + + if (.not.('child' .in. f_source%modules_provided)) then + call test_failed(error,'Missing module in modules_provided') + return + end if + + if (.not.('module_one' .in. f_source%modules_used)) then + call test_failed(error,'Missing module in modules_used') + return + end if + + if (.not.('parent' .in. f_source%modules_used)) then + call test_failed(error,'Missing parent module in modules_used') + return + end if + + end subroutine test_submodule + + + !> Try to parse fortran multi-level submodule for ancestry + subroutine test_submodule_ancestor(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + integer :: unit + character(:), allocatable :: temp_file + type(srcfile_t), allocatable :: f_source + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'submodule (ancestor:parent) child', & + & 'use module_one', & + & 'end submodule test' + close(unit) + + f_source = parse_f_source(temp_file,error) + if (allocated(error)) then + return + end if + + if (f_source%unit_type /= FPM_UNIT_SUBMODULE) then + call test_failed(error,'Wrong unit type detected - expecting FPM_UNIT_SUBMODULE') + return + end if + + if (size(f_source%modules_provided) /= 1) then + call test_failed(error,'Unexpected modules_provided - expecting one') + return + end if + + if (size(f_source%modules_used) /= 2) then + call test_failed(error,'Incorrect number of modules_used - expecting two') + return + end if + + if (.not.('child' .in. f_source%modules_provided)) then + call test_failed(error,'Missing module in modules_provided') + return + end if + + if (.not.('module_one' .in. f_source%modules_used)) then + call test_failed(error,'Missing module in modules_used') + return + end if + + if (.not.('parent' .in. f_source%modules_used)) then + call test_failed(error,'Missing parent module in modules_used') + return + end if + + end subroutine test_submodule_ancestor + + + !> Try to parse standard fortran sub-program (non-module) source + subroutine test_subprogram(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + integer :: unit + character(:), allocatable :: temp_file + type(srcfile_t), allocatable :: f_source + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'subroutine my_sub(a)', & + & ' use module_one', & + & ' integer, intent(in) :: a', & + & 'end subroutine my_sub' + close(unit) + + f_source = parse_f_source(temp_file,error) + if (allocated(error)) then + return + end if + + if (f_source%unit_type /= FPM_UNIT_SUBPROGRAM) then + call test_failed(error,'Wrong unit type detected - expecting FPM_UNIT_SUBPROGRAM') + return + end if + + if (size(f_source%modules_provided) /= 0) then + call test_failed(error,'Unexpected modules_provided - expecting zero') + return + end if + + if (size(f_source%modules_used) /= 1) then + call test_failed(error,'Incorrect number of modules_used - expecting one') + return + end if + + if (.not.('module_one' .in. f_source%modules_used)) then + call test_failed(error,'Missing module in modules_used') + return + end if + + end subroutine test_subprogram + + + !> Try to parse standard c source for includes + subroutine test_csource(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + integer :: unit + character(:), allocatable :: temp_file + type(srcfile_t), allocatable :: f_source + + allocate(temp_file, source=get_temp_filename()) + temp_file = temp_file//'.c' + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & '#include "proto.h"', & + & 'void c_func(int a) {', & + & ' #include "function_body.c"', & + & ' return', & + & '}' + close(unit) + + f_source = parse_c_source(temp_file,error) + if (allocated(error)) then + return + end if + + if (f_source%unit_type /= FPM_UNIT_CSOURCE) then + call test_failed(error,'Wrong unit type detected - expecting FPM_UNIT_CSOURCE') + return + end if + + if (size(f_source%modules_provided) /= 0) then + call test_failed(error,'Unexpected modules_provided - expecting zero') + return + end if + + if (size(f_source%modules_used) /= 0) then + call test_failed(error,'Incorrect number of modules_used - expecting zero') + return + end if + + if (size(f_source%include_dependencies) /= 2) then + call test_failed(error,'Incorrect number of include_dependencies - expecting two') + return + end if + + if (.not.('proto.h' .in. f_source%include_dependencies)) then + call test_failed(error,'Missing file in include_dependencies') + return + end if + + if (.not.('function_body.c' .in. f_source%include_dependencies)) then + call test_failed(error,'Missing file in include_dependencies') + return + end if + + end subroutine test_csource + + + !> Try to parse fortran program with invalid use statement + subroutine test_invalid_use_stmt(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + integer :: unit + character(:), allocatable :: temp_file + type(srcfile_t), allocatable :: f_source + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'program test', & + & 'use module_one', & + & 'use :: ', & + & 'end program test' + close(unit) + + f_source = parse_f_source(temp_file,error) + if (allocated(error)) then + return + end if + + end subroutine test_invalid_use_stmt + + + !> Try to parse fortran program with invalid use statement + subroutine test_invalid_include_stmt(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + integer :: unit + character(:), allocatable :: temp_file + type(srcfile_t), allocatable :: f_source + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'program test', & + & ' include "', & + & 'end program test' + close(unit) + + f_source = parse_f_source(temp_file,error) + if (allocated(error)) then + return + end if + + end subroutine test_invalid_include_stmt + + + !> Try to parse incorrect fortran module syntax + subroutine test_invalid_module(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + integer :: unit + character(:), allocatable :: temp_file + type(srcfile_t), allocatable :: f_source + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'module :: my_mod', & + & 'end module test' + close(unit) + + f_source = parse_f_source(temp_file,error) + if (allocated(error)) then + return + end if + + write(*,*) '"',f_source%modules_used(1)%s,'"' + + end subroutine test_invalid_module + + + !> Try to parse incorrect fortran submodule syntax + subroutine test_invalid_submodule(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + integer :: unit + character(:), allocatable :: temp_file + type(srcfile_t), allocatable :: f_source + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'submodule :: child', & + & 'end submodule test' + close(unit) + + f_source = parse_f_source(temp_file,error) + if (allocated(error)) then + return + end if + + write(*,*) '"',f_source%modules_used(1)%s,'"' + + end subroutine test_invalid_submodule + + + +end module test_source_parsing diff --git a/test/fpm_test/test_toml.f90 b/test/fpm_test/test_toml.f90 new file mode 100644 index 0000000..ba48307 --- /dev/null +++ b/test/fpm_test/test_toml.f90 @@ -0,0 +1,107 @@ +!> Define tests for the `fpm_toml` modules +module test_toml + use testsuite, only : new_unittest, unittest_t, error_t + use fpm_toml + implicit none + private + + public :: collect_toml + + +contains + + + !> Collect all exported unit tests + subroutine collect_toml(testsuite) + + !> Collection of tests + type(unittest_t), allocatable, intent(out) :: testsuite(:) + + testsuite = [ & + & new_unittest("valid-toml", test_valid_toml), & + & new_unittest("invalid-toml", test_invalid_toml, should_fail=.true.), & + & new_unittest("missing-file", test_missing_file, should_fail=.true.)] + + end subroutine collect_toml + + + !> Try to read some unnecessary obscure and convoluted but not invalid package file + subroutine test_valid_toml(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table), allocatable :: table + character(len=*), parameter :: manifest = 'fpm-valid-toml.toml' + integer :: unit + + open(file=manifest, newunit=unit) + write(unit, '(a)') & + & 'name = "example"', & + & '[dependencies.fpm]', & + & 'git = "https://github.com/fortran-lang/fpm"', & + & '[[executable]]', & + & 'name = "example-#1" # comment', & + & 'source-dir = "prog"', & + & '[dependencies]', & + & 'toml-f.git = "git@github.com:toml-f/toml-f.git"', & + & '"toml..f" = { path = ".." }', & + & '[["executable"]]', & + & 'name = "example-#2"', & + & 'source-dir = "prog"', & + & '[executable.dependencies]', & + & '[''library'']', & + & 'source-dir = """', & + & 'lib""" # comment' + close(unit) + + call read_package_file(table, manifest, error) + + open(file=manifest, newunit=unit) + close(unit, status='delete') + + end subroutine test_valid_toml + + + !> Try to read an invalid TOML document + subroutine test_invalid_toml(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table), allocatable :: table + character(len=*), parameter :: manifest = 'fpm-invalid-toml.toml' + integer :: unit + + open(file=manifest, newunit=unit) + write(unit, '(a)') & + & '# INVALID TOML DOC', & + & 'name = "example"', & + & 'dependencies.fpm.git = "https://github.com/fortran-lang/fpm"', & + & '[dependencies]', & + & 'toml-f.git = "git@github.com:toml-f/toml-f.git"', & + & '"toml..f" = { path = ".." }' + close(unit) + + call read_package_file(table, manifest, error) + + open(file=manifest, newunit=unit) + close(unit, status='delete') + + end subroutine test_invalid_toml + + + !> Try to read configuration from a non-existing file + subroutine test_missing_file(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(toml_table), allocatable :: table + + call read_package_file(table, 'low+chance+of+existing.toml', error) + + end subroutine test_missing_file + + +end module test_toml diff --git a/test/fpm_test/test_versioning.f90 b/test/fpm_test/test_versioning.f90 new file mode 100644 index 0000000..f6dcb57 --- /dev/null +++ b/test/fpm_test/test_versioning.f90 @@ -0,0 +1,405 @@ +!> Test implementation of version data type +module test_versioning + use testsuite, only : new_unittest, unittest_t, error_t, test_failed + use fpm_versioning + implicit none + private + + public :: collect_versioning + + +contains + + + !> Collect all exported unit tests + subroutine collect_versioning(testsuite) + + !> Collection of tests + type(unittest_t), allocatable, intent(out) :: testsuite(:) + + testsuite = [ & + & new_unittest("valid-version", test_valid_version), & + & new_unittest("valid-equals", test_valid_equals), & + & new_unittest("valid-notequals", test_valid_notequals), & + & new_unittest("valid-compare", test_valid_compare), & + & new_unittest("valid-match", test_valid_match), & + & new_unittest("valid-string", test_valid_string), & + & new_unittest("invalid-empty", test_invalid_empty, should_fail=.true.), & + & new_unittest("invalid-version1", test_invalid_version1, should_fail=.true.), & + & new_unittest("invalid-version2", test_invalid_version2, should_fail=.true.), & + & new_unittest("invalid-version3", test_invalid_version3, should_fail=.true.), & + & new_unittest("invalid-overflow", test_invalid_overflow, should_fail=.true.)] + + end subroutine collect_versioning + + + !> Read valid version strings + subroutine test_valid_version(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(version_t) :: version + + call new_version(version, "8.9.0", error) + if (allocated(error)) return + + call new_version(version, "2020.10.003", error) + + end subroutine test_valid_version + + + !> Compare versions for equality + subroutine test_valid_equals(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(version_t) :: v1, v2 + type(version_t) :: varray(4) + + call new_version(v1, [1, 2, 0]) + call new_version(v2, [1, 2]) + + if (.not. v1 == v2) then + call test_failed(error, "Version comparison failed") + return + end if + + if (.not. v2 == v1) then + call test_failed(error, "Version comparison failed") + return + end if + + call new_version(v1, [0, 9, 0]) + call new_version(v2, [0, 9]) + + if (.not. v1.eq.v2) then + call test_failed(error, "Version comparison failed") + return + end if + + if (.not. v2.eq.v1) then + call test_failed(error, "Version comparison failed") + return + end if + + call new_version(v1, [2020]) + call new_version(v2, [2020, 0]) + + if (.not. v1 == v2) then + call test_failed(error, "Version comparison failed") + return + end if + + if (.not. v2 == v1) then + call test_failed(error, "Version comparison failed") + return + end if + + call new_version(v1, [20, 1]) + call new_version(varray(1), [19]) + call new_version(varray(2), [18, 2]) + call new_version(varray(3), [20, 1]) + call new_version(varray(4), [1, 3, 1]) + + if (.not. any(v1 == varray)) then + call test_failed(error, "Version comparison failed") + return + end if + + end subroutine test_valid_equals + + + !> Compare versions for mismatch + subroutine test_valid_notequals(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(version_t) :: v1, v2 + type(version_t) :: varray(4) + + call new_version(v1, [2020, 3, 1]) + call new_version(v2, [2020, 3]) + + if (.not. v1 /= v2) then + call test_failed(error, "Version comparison failed") + return + end if + + if (.not. v2 /= v1) then + call test_failed(error, "Version comparison failed") + return + end if + + call new_version(v1, [0, 9, 1]) + call new_version(v2, [0, 9]) + + if (.not. v1.ne.v2) then + call test_failed(error, "Version comparison failed") + return + end if + + if (.not. v2.ne.v1) then + call test_failed(error, "Version comparison failed") + return + end if + + call new_version(v1, [2020]) + call new_version(v2, [0, 2020]) + + if (.not. v2 /= v1) then + call test_failed(error, "Version comparison failed") + return + end if + + if (.not. v1 /= v2) then + call test_failed(error, "Version comparison failed") + return + end if + + call new_version(v1, [20, 1]) + call new_version(varray(1), [19]) + call new_version(varray(2), [18, 2]) + call new_version(varray(3), [18, 1]) + call new_version(varray(4), [1, 3, 1]) + + if (.not. any(v1 /= varray)) then + call test_failed(error, "Version comparison failed") + return + end if + + end subroutine test_valid_notequals + + + !> Relative comparison of versions + subroutine test_valid_compare(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(version_t) :: v1, v2 + type(version_t) :: varray(4) + + call new_version(v1, [10]) + call new_version(v2, [1]) + + if (.not. v1 > v2) then + call test_failed(error, "Version comparison failed (gt)") + return + end if + + if (.not. v1 >= v2) then + call test_failed(error, "Version comparison failed (ge)") + return + end if + + if (.not. v2 < v1) then + call test_failed(error, "Version comparison failed (lt)") + return + end if + + if (.not. v2 <= v1) then + call test_failed(error, "Version comparison failed (le)") + return + end if + + call new_version(v1, [1, 0, 8]) + call new_version(v2, [1, 0]) + + if (.not. v1 .gt. v2) then + call test_failed(error, "Version comparison failed (gt)") + return + end if + + if (.not. v1 .ge. v2) then + call test_failed(error, "Version comparison failed (ge)") + return + end if + + if (.not. v2 .lt. v1) then + call test_failed(error, "Version comparison failed (lt)") + return + end if + + if (.not. v2 .le. v1) then + call test_failed(error, "Version comparison failed (le)") + return + end if + + call new_version(v1, [1, 2]) + call new_version(v2, [1, 2, 0]) + + if (v1 > v2) then + call test_failed(error, "Version comparison failed (gt)") + return + end if + + if (.not. v1 >= v2) then + call test_failed(error, "Version comparison failed (ge)") + return + end if + + if (v2 < v1) then + call test_failed(error, "Version comparison failed (lt)") + return + end if + + if (.not. v2 <= v1) then + call test_failed(error, "Version comparison failed (le)") + return + end if + + call new_version(v1, [20, 1]) + call new_version(varray(1), [19]) + call new_version(varray(2), [18, 2]) + call new_version(varray(3), [18, 1]) + call new_version(varray(4), [1, 3, 1]) + + if (.not. all(v1 > varray)) then + call test_failed(error, "Version comparison failed (gt)") + return + end if + + end subroutine test_valid_compare + + + !> Semantic version matching + subroutine test_valid_match(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(version_t) :: v1, v2 + type(version_t) :: varray(4) + + call new_version(v1, [1, 1, 0]) + call new_version(v2, [1]) + + if (.not. (v1 .match. v2)) then + call test_failed(error, "Version comparison failed (match)") + return + end if + + if (v2 .match. v1) then + call test_failed(error, "Version comparison failed (match)") + return + end if + + call new_version(v1, [0, 5, 8]) + call new_version(v2, [0, 5]) + + if (.not. (v1 .match. v2)) then + call test_failed(error, "Version comparison failed (match)") + return + end if + + if (v2 .match. v1) then + call test_failed(error, "Version comparison failed (match)") + return + end if + + call new_version(v1, [1, 2]) + call new_version(v2, [1, 2, 0]) + + if (.not. (v1 .match. v2)) then + call test_failed(error, "Version comparison failed (match)") + return + end if + + if (.not. (v2 .match. v1)) then + call test_failed(error, "Version comparison failed (match)") + return + end if + + end subroutine test_valid_match + + + !> Test if version string is preserved + subroutine test_valid_string(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + character(len=*), parameter :: str_in = "20.1.100" + character(len=:), allocatable :: str_out + type(version_t) :: version + + call new_version(version, str_in, error) + if (allocated(error)) return + call version%to_string(str_out) + + if (str_in /= str_out) then + call test_failed(error, "Expected "//str_in//" but got "//str_out) + end if + + end subroutine test_valid_string + + + !> Empty string does not represent a version + subroutine test_invalid_empty(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(version_t) :: version + + call new_version(version, "", error) + + end subroutine test_invalid_empty + + + !> Version is invalid with trailing dots + subroutine test_invalid_version1(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(version_t) :: version + + call new_version(version, "1.", error) + + end subroutine test_invalid_version1 + + + !> Version is invalid with multiple dots + subroutine test_invalid_version2(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(version_t) :: version + + call new_version(version, "1..1", error) + + end subroutine test_invalid_version2 + + + !> Version is invalid if it is not a version + subroutine test_invalid_version3(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(version_t) :: version + + call new_version(version, "one", error) + + end subroutine test_invalid_version3 + + + !> Check if overflows of the internal size constraint are handled gracefully + subroutine test_invalid_overflow(error) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(version_t) :: version + + call new_version(version, "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0", error) + + end subroutine test_invalid_overflow + + +end module test_versioning diff --git a/test/fpm_test/testsuite.f90 b/test/fpm_test/testsuite.f90 new file mode 100644 index 0000000..124d19a --- /dev/null +++ b/test/fpm_test/testsuite.f90 @@ -0,0 +1,286 @@ +!> Define some procedures to automate collecting and launching of tests +module testsuite + use fpm_error, only : error_t, test_failed => fatal_error + implicit none + private + + public :: run_testsuite, run_selected, new_unittest, new_testsuite, test_failed + public :: select_test, select_suite + public :: check_string + public :: unittest_t, testsuite_t, error_t + + + abstract interface + !> Entry point for tests + subroutine test_interface(error) + import :: error_t + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + end subroutine test_interface + end interface + + + !> Declaration of a unit test + type :: unittest_t + + !> Name of the test + character(len=:), allocatable :: name + + !> Entry point of the test + procedure(test_interface), pointer, nopass :: test => null() + + !> Whether test is supposed to fail + logical :: should_fail = .false. + + end type unittest_t + + + abstract interface + !> Collect all tests + subroutine collect_interface(testsuite) + import :: unittest_t + + !> Collection of tests + type(unittest_t), allocatable, intent(out) :: testsuite(:) + + end subroutine collect_interface + end interface + + + !> Collection of unit tests + type :: testsuite_t + + !> Name of the testsuite + character(len=:), allocatable :: name + + !> Entry point of the test + procedure(collect_interface), pointer, nopass :: collect => null() + + end type testsuite_t + + + character(len=*), parameter :: fmt = '("#", *(1x, a))' + character(len=*), parameter :: indent = repeat(" ", 5) // repeat(".", 3) + + +contains + + + !> Driver for testsuite + subroutine run_testsuite(collect, unit, stat) + + !> Collect tests + procedure(collect_interface) :: collect + + !> Unit for IO + integer, intent(in) :: unit + + !> Number of failed tests + integer, intent(inout) :: stat + + type(unittest_t), allocatable :: testsuite(:) + integer :: ii + + call collect(testsuite) + + do ii = 1, size(testsuite) + write(unit, '("#", 3(1x, a), 1x, "(", i0, "/", i0, ")")') & + & "Starting", testsuite(ii)%name, "...", ii, size(testsuite) + call run_unittest(testsuite(ii), unit, stat) + end do + + end subroutine run_testsuite + + + !> Driver for selective testing + subroutine run_selected(collect, name, unit, stat) + + !> Collect tests + procedure(collect_interface) :: collect + + !> Name of the selected test + character(len=*), intent(in) :: name + + !> Unit for IO + integer, intent(in) :: unit + + !> Number of failed tests + integer, intent(inout) :: stat + + type(unittest_t), allocatable :: testsuite(:) + integer :: ii + + call collect(testsuite) + + ii = select_test(testsuite, name) + + if (ii > 0 .and. ii <= size(testsuite)) then + call run_unittest(testsuite(ii), unit, stat) + else + write(unit, fmt) "Available tests:" + do ii = 1, size(testsuite) + write(unit, fmt) "-", testsuite(ii)%name + end do + stat = -huge(ii) + end if + + end subroutine run_selected + + + !> Run a selected unit test + subroutine run_unittest(test, unit, stat) + + !> Unit test + type(unittest_t), intent(in) :: test + + !> Unit for IO + integer, intent(in) :: unit + + !> Number of failed tests + integer, intent(inout) :: stat + + type(error_t), allocatable :: error + + call test%test(error) + if (allocated(error) .neqv. test%should_fail) then + if (test%should_fail) then + write(unit, fmt) indent, test%name, "[UNEXPECTED PASS]" + else + write(unit, fmt) indent, test%name, "[FAILED]" + end if + stat = stat + 1 + else + if (test%should_fail) then + write(unit, fmt) indent, test%name, "[EXPECTED FAIL]" + else + write(unit, fmt) indent, test%name, "[PASSED]" + end if + end if + if (allocated(error)) then + write(unit, fmt) "Message:", error%message + end if + + end subroutine run_unittest + + + !> Select a unit test from all available tests + function select_test(tests, name) result(pos) + + !> Name identifying the test suite + character(len=*), intent(in) :: name + + !> Available unit tests + type(unittest_t) :: tests(:) + + !> Selected test suite + integer :: pos + + integer :: it + + pos = 0 + do it = 1, size(tests) + if (name == tests(it)%name) then + pos = it + exit + end if + end do + + end function select_test + + + !> Select a test suite from all available suites + function select_suite(suites, name) result(pos) + + !> Name identifying the test suite + character(len=*), intent(in) :: name + + !> Available test suites + type(testsuite_t) :: suites(:) + + !> Selected test suite + integer :: pos + + integer :: it + + pos = 0 + do it = 1, size(suites) + if (name == suites(it)%name) then + pos = it + exit + end if + end do + + end function select_suite + + + !> Register a new unit test + function new_unittest(name, test, should_fail) result(self) + + !> Name of the test + character(len=*), intent(in) :: name + + !> Entry point for the test + procedure(test_interface) :: test + + !> Whether test is supposed to error or not + logical, intent(in), optional :: should_fail + + !> Newly registered test + type(unittest_t) :: self + + self%name = name + self%test => test + if (present(should_fail)) self%should_fail = should_fail + + end function new_unittest + + + !> Register a new testsuite + function new_testsuite(name, collect) result(self) + + !> Name of the testsuite + character(len=*), intent(in) :: name + + !> Entry point to collect tests + procedure(collect_interface) :: collect + + !> Newly registered testsuite + type(testsuite_t) :: self + + self%name = name + self%collect => collect + + end function new_testsuite + + + !> Check a deferred length character variable against a reference value + subroutine check_string(error, actual, expected, name) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + !> Actual string value + character(len=:), allocatable, intent(in) :: actual + + !> Expected string value + character(len=*), intent(in) :: expected + + !> Name of the string to check + character(len=*), intent(in) :: name + + if (.not.allocated(actual)) then + call test_failed(error, name//" is not set correctly") + return + end if + + if (actual /= expected) then + call test_failed(error, name//" is "//actual// & + & " but should be "//expected) + end if + + end subroutine check_string + + +end module testsuite diff --git a/test/help_test/help_test.f90 b/test/help_test/help_test.f90 new file mode 100644 index 0000000..8f0c455 --- /dev/null +++ b/test/help_test/help_test.f90 @@ -0,0 +1,292 @@ +program help_test +! note hardcoded len=k1 instead of len=: in this test is a work-around a gfortran bug in old +! pre-v8.3 versions +use,intrinsic :: iso_fortran_env, only : stdin=>input_unit, stdout=>output_unit, stderr=>error_unit +use fpm_filesystem, only : dirname, join_path, exists +use fpm_environment, only : get_os_type, OS_WINDOWS +implicit none +integer :: i, j +integer :: be, af +character(len=:),allocatable :: path +integer :: estat, cstat +integer,parameter :: k1=132 +character(len=k1) :: message +logical,allocatable :: tally(:) +!intel-bug!character(len=:),allocatable :: book1(:), book2(:) +character(len=k1),allocatable :: book1(:), book2(:) +!intel-bug!character(len=:),allocatable :: page1(:) +character(len=k1),allocatable :: page1(:) +integer :: lines +integer :: chars +! run a variety of "fpm help" variations and verify expected files are generated +character(len=*),parameter :: cmds(*) = [character(len=80) :: & +! build manual as pieces using various help commands +! debug version +' --version ',& ! verify fpm version being used +' --help > fpm_scratch_help.txt',& +' help new >> fpm_scratch_help.txt',& +' help update >> fpm_scratch_help.txt',& +' build --help >> fpm_scratch_help.txt',& +' help run >> fpm_scratch_help.txt',& +' help test >> fpm_scratch_help.txt',& +' help runner >> fpm_scratch_help.txt',& +' help install >> fpm_scratch_help.txt',& +' help list >> fpm_scratch_help.txt',& +' help help >> fpm_scratch_help.txt',& +' --version >> fpm_scratch_help.txt',& +! generate manual +' help manual > fpm_scratch_manual.txt'] + +!'fpm run >> fpm_scratch_help.txt',& +!'fpm run -- --list >> fpm_scratch_help.txt',& +!'fpm run -- list --list >> fpm_scratch_help.txt',& +character(len=*),parameter :: names(*)=[character(len=10) ::& + 'fpm','new','update','build','run','test','runner','install','list','help'] +character(len=:), allocatable :: prog +integer :: length + + ! FIXME: Super hacky way to get the name of the fpm executable, + ! it works better than invoking fpm again but should be replaced ASAP. + call get_command_argument(0, length=length) + allocate(character(len=length) :: prog) + call get_command_argument(0, prog) + path = dirname(prog) + if (get_os_type() == OS_WINDOWS) then + prog = join_path(path, "..", "app", "fpm.exe") + if (.not.exists(prog)) then + prog = join_path(path, "..", "..", "app", "fpm.exe") + end if + else + prog = join_path(path, "..", "app", "fpm") + if (.not.exists(prog)) then + prog = join_path(path, "..", "..", "app", "fpm") + end if + end if + + write(*,'(g0:,1x)')'<INFO>TEST help SUBCOMMAND STARTED' + if(allocated(tally))deallocate(tally) + allocate(tally(0)) + call wipe('fpm_scratch_help.txt') + call wipe('fpm_scratch_manual.txt') + + ! check that output has NAME SYNOPSIS DESCRIPTION + do i=1,size(names) + write(*,*)'<INFO>check '//names(i)//' for NAME SYNOPSIS DESCRIPTION' + path= prog // ' help '//names(i)//' >fpm_scratch_help.txt' + message='' + call execute_command_line(path,exitstat=estat,cmdstat=cstat,cmdmsg=message) + write(*,'(*(g0))')'<INFO>CMD=',path,' EXITSTAT=',estat,' CMDSTAT=',cstat,' MESSAGE=',trim(message) + tally=[tally,all([estat.eq.0,cstat.eq.0])] + call swallow('fpm_scratch_help.txt',page1) + if(size(page1).lt.3)then + write(*,*)'<ERROR>help for '//names(i)//' ridiculiously small' + tally=[tally,.false.] + exit + endif + !!write(*,*)findloc(page1,'NAME').eq.1 + be=count(.not.tally) + tally=[tally,count(page1.eq.'NAME').eq.1] + tally=[tally,count(page1.eq.'SYNOPSIS').eq.1] + tally=[tally,count(page1.eq.'DESCRIPTION').eq.1] + af=count(.not.tally) + if(be.ne.af)then + write(*,*)'<ERROR>missing expected sections in ',names(i) + write(*,*)page1(1) ! assuming at least size 1 for debugging mingw + write(*,*)count(page1.eq.'NAME') + write(*,*)count(page1.eq.'SYNOPSIS') + write(*,*)count(page1.eq.'DESCRIPTION') + write(*,'(a)')page1 + endif + write(*,*)'<INFO>have completed ',count(tally),' tests' + call wipe('fpm_scratch_help.txt') + enddo + + + ! execute the fpm(1) commands + do i=1,size(cmds) + message='' + path= prog // cmds(i) + call execute_command_line(path,exitstat=estat,cmdstat=cstat,cmdmsg=message) + write(*,'(*(g0))')'<INFO>CMD=',path,' EXITSTAT=',estat,' CMDSTAT=',cstat,' MESSAGE=',trim(message) + tally=[tally,all([estat.eq.0,cstat.eq.0])] + enddo + + ! compare book written in fragments with manual + call swallow('fpm_scratch_help.txt',book1) + call swallow('fpm_scratch_manual.txt',book2) + ! get rid of lines from run() which is not on stderr at the moment + book1=pack(book1,index(book1,' + build/').eq.0) + book2=pack(book1,index(book2,' + build/').eq.0) + write(*,*)'<INFO>book1 ',size(book1), len(book1) + write(*,*)'<INFO>book2 ',size(book2), len(book2) + if(size(book1).ne.size(book2))then + write(*,*)'<ERROR>manual and "debug" appended pages are not the same size' + tally=[tally,.false.] + else + if(all(book1.ne.book2))then + tally=[tally,.false.] + write(*,*)'<ERROR>manual and "debug" appended pages are not the same' + else + write(*,*)'<INFO>manual and "debug" appended pages are the same' + tally=[tally,.true.] + endif + endif + + ! overall size of manual + !chars=size(book2) + !lines=max(count(char(10).eq.book2),count(char(13).eq.book2)) + chars=sum(len_trim(book2)) ! SUM TRIMMED LENGTH + lines=size(book2) + if( (chars.lt.12000) .or. (lines.lt.350) )then + write(*,*)'<ERROR>"debug" manual is suspiciously small, bytes=',chars,' lines=',lines + tally=[tally,.false.] + else + write(*,*)'<INFO>"debug" manual size in bytes=',chars,' lines=',lines + tally=[tally,.true.] + endif + + write(*,'("<INFO>HELP TEST TALLY=",*(g0))')tally + call wipe('fpm_scratch_help.txt') + call wipe('fpm_scratch_manual.txt') + if(all(tally))then + write(*,'(*(g0))')'<INFO>PASSED: all ',count(tally),' tests passed ' + else + write(*,*)'<INFO>FAILED: PASSED=',count(tally),' FAILED=',count(.not.tally) + stop 5 + endif + write(*,'(g0:,1x)')'<INFO>TEST help SUBCOMMAND COMPLETE' +contains + +subroutine wipe(filename) +character(len=*),intent(in) :: filename +integer :: ios +integer :: lun +character(len=k1) :: message +open(file=filename,newunit=lun,iostat=ios,iomsg=message) +if(ios.eq.0)then + close(unit=lun,iostat=ios,status='delete',iomsg=message) + if(ios.ne.0)then + write(*,*)'<ERROR>'//trim(message) + endif +else + write(*,*)'<ERROR>'//trim(message) +endif +end subroutine wipe + +subroutine slurp(filename,text) +implicit none +!$@(#) M_io::slurp(3f): allocate text array and read file filename into it +character(*),intent(in) :: filename ! filename to shlep +character(len=1),allocatable,intent(out) :: text(:) ! array to hold file +integer :: nchars, igetunit, ios +character(len=k1) :: message +character(len=4096) :: local_filename + ios=0 + nchars=0 + message='' + open(newunit=igetunit, file=trim(filename), action="read", iomsg=message,& + &form="unformatted", access="stream",status='old',iostat=ios) + local_filename=filename + if(ios.eq.0)then ! if file was successfully opened + inquire(unit=igetunit, size=nchars) + if(nchars.le.0)then + call stderr_local( '*slurp* empty file '//trim(local_filename) ) + return + endif + ! read file into text array + if(allocated(text))deallocate(text) ! make sure text array not allocated + allocate ( text(nchars) ) ! make enough storage to hold file + read(igetunit,iostat=ios,iomsg=message) text ! load input file -> text array + if(ios.ne.0)then + call stderr_local( '*slurp* bad read of '//trim(local_filename)//':'//trim(message) ) + endif + else + call stderr_local('*slurp* '//message) + allocate ( text(0) ) ! make enough storage to hold file + endif + close(iostat=ios,unit=igetunit) ! close if opened successfully or not +end subroutine slurp + +subroutine stderr_local(message) +character(len=*) :: message + write(*,'(a)')trim(message) ! write message to standard error +end subroutine stderr_local + +subroutine swallow(FILENAME,pageout) +implicit none +character(len=*),intent(in) :: FILENAME ! file to read +!intel-bug!character(len=:),allocatable,intent(out) :: pageout(:) ! page to hold file in memory +character(len=k1),allocatable,intent(out) :: pageout(:) ! page to hold file in memory +character(len=1),allocatable :: text(:) ! array to hold file in memory + + call slurp(FILENAME,text) ! allocate character array and copy file into it + + if(.not.allocated(text))then + write(*,*)'<ERROR>*swallow* failed to load file '//FILENAME + else ! convert array of characters to array of lines + pageout=page(text) + deallocate(text) ! release memory + endif +end subroutine swallow + +function page(array) result (table) + +!$@(#) M_strings::page(3fp): function to copy char array to page of text + +character(len=1),intent(in) :: array(:) +!intel-bug!character(len=:),allocatable :: table(:) +character(len=k1),allocatable :: table(:) +integer :: i +integer :: linelength +integer :: length +integer :: lines +integer :: linecount +integer :: position +integer :: sz +!!character(len=1),parameter :: nl=new_line('A') +character(len=1),parameter :: nl=char(10) +character(len=1),parameter :: cr=char(13) + lines=0 + linelength=0 + length=0 + sz=size(array) + do i=1,sz + if(array(i).eq.nl)then + linelength=max(linelength,length) + lines=lines+1 + length=0 + else + length=length+1 + endif + enddo + if(sz.gt.0)then + if(array(sz).ne.nl)then + lines=lines+1 + endif + endif + + if(allocated(table))deallocate(table) + !intel-bug!allocate(character(len=linelength) :: table(lines)) + allocate(character(len=k1) :: table(lines)) + table=' ' + linecount=1 + position=1 + do i=1,sz + if(array(i).eq.nl)then + linecount=linecount+1 + position=1 + elseif(array(i).eq.cr)then + elseif(linelength.ne.0)then + if(position.gt.len(table))then + write(*,*)'<ERROR> adding character past edge of text',table(linecount),array(i) + elseif(linecount.gt.size(table))then + write(*,*)'<ERROR> adding line past end of text',linecount,size(table) + else + table(linecount)(position:position)=array(i) + endif + position=position+1 + endif + enddo +end function page + +end program help_test diff --git a/test/new_test/new_test.f90 b/test/new_test/new_test.f90 new file mode 100644 index 0000000..3c8c453 --- /dev/null +++ b/test/new_test/new_test.f90 @@ -0,0 +1,187 @@ +program new_test +use,intrinsic :: iso_fortran_env, only : stdin=>input_unit, stdout=>output_unit, stderr=>error_unit +use fpm_filesystem, only : is_dir, list_files, exists, windows_path, join_path, & + dirname +use fpm_strings, only : string_t, operator(.in.) +use fpm_environment, only : run, get_os_type +use fpm_environment, only : OS_UNKNOWN, OS_LINUX, OS_MACOS, OS_CYGWIN, OS_SOLARIS, OS_FREEBSD, OS_WINDOWS +implicit none +type(string_t), allocatable :: file_names(:) +integer :: i, j, k +character(len=:),allocatable :: cmdpath +character(len=:),allocatable :: path +character(len=*),parameter :: scr = 'fpm_scratch_' +character(len=*),parameter :: cmds(*) = [character(len=80) :: & +! run a variety of "fpm new" variations and verify expected files are generated +' new', & +' new name-with-hyphens', & +' new '//scr//'A', & +' new '//scr//'B --lib', & +' new '//scr//'C --app', & +' new '//scr//'D --test', & +' new '//scr//'E --lib --test ', & +' new '//scr//'F --lib --app', & +' new '//scr//'G --test --app', & +' new '//scr//'H --example', & +' new '//scr//'BB --lib', & +' new '//scr//'BB --test ', & +' new '//scr//'BB --backfill --test', & +' new '//scr//'CC --test --src --app', & +' new --version', & +' new --help'] +integer :: estat, cstat +character(len=256) :: message +character(len=:),allocatable :: directories(:) +character(len=:),allocatable :: shortdirs(:) +character(len=:),allocatable :: expected(:) +logical,allocatable :: tally(:) +logical :: IS_OS_WINDOWS + write(*,'(g0:,1x)')'TEST new SUBCOMMAND (draft):' + + cmdpath = get_command_path() + allocate(tally(0)) + shortdirs=[character(len=80) :: 'A','B','C','D','E','F','G','H','BB','CC'] + allocate(character(len=80) :: directories(size(shortdirs))) + + !! SEE IF EXPECTED FILES ARE GENERATED + !! Issues: + !! o assuming fpm command is in expected path and the new version + !! o DOS versus POSIX filenames + is_os_windows=.false. + select case (get_os_type()) + case (OS_UNKNOWN, OS_LINUX, OS_MACOS, OS_CYGWIN, OS_SOLARIS, OS_FREEBSD) + call execute_command_line('rm -rf fpm_scratch_*',exitstat=estat,cmdstat=cstat,cmdmsg=message) + path=cmdpath + case (OS_WINDOWS) + path=windows_path(cmdpath) + is_os_windows=.true. + call execute_command_line('rmdir fpm_scratch_* /s /q',exitstat=estat,cmdstat=cstat,cmdmsg=message) + case default + write(*,*)'ERROR: unknown OS. Stopping test' + stop 2 + end select + do i=1,size(directories) + directories(i)=scr//trim(shortdirs(i)) + if( is_dir(trim(directories(i))) ) then + write(*,*)'ERROR:',trim( directories(i) ),' already exists' + write(*,*)' you must remove scratch directories before performing this test' + write(*,'(*(g0:,1x))')'directories:',(trim(directories(j)),j=1,size(directories)),'name-with-hyphens' + stop + endif + enddo + ! execute the fpm(1) commands + do i=1,size(cmds) + message='' + write(*,*)path//' '//cmds(i) + call execute_command_line(path//' '//cmds(i),exitstat=estat,cmdstat=cstat,cmdmsg=message) + write(*,'(*(g0))')'CMD=',trim(cmds(i)),' EXITSTAT=',estat,' CMDSTAT=',cstat,' MESSAGE=',trim(message) + enddo + + if( is_dir('name-with-hyphens') ) then + tally=[tally,.true.] + + else + write(*,*)'ERROR: directory name-with-hyphens/ exists' + tally=[tally,.false.] + endif + + ! assuming hidden files in .git and .gitignore are ignored for now + TESTS: do i=1,size(directories) + ! test if expected directory exists + if( .not. is_dir(trim( directories(i))) ) then + tally=[tally,.false.] + write(*,*)'ERROR:',trim( directories(i) ),' is not a directory' + else + select case(shortdirs(i)) + case('A'); expected=[ character(len=80)::& + &'A/app','A/fpm.toml','A/README.md','A/src','A/test','A/app/main.f90','A/src/'//scr//'A.f90','A/test/check.f90'] + case('B'); expected=[ character(len=80)::& + &'B/fpm.toml','B/README.md','B/src','B/src/'//scr//'B.f90'] + case('C'); expected=[ character(len=80)::& + &'C/app','C/fpm.toml','C/README.md','C/app/main.f90'] + case('D'); expected=[ character(len=80)::& + &'D/fpm.toml','D/README.md','D/test','D/test/check.f90'] + case('E'); expected=[ character(len=80)::& + &'E/fpm.toml','E/README.md','E/src','E/test','E/src/'//scr//'E.f90','E/test/check.f90'] + case('F'); expected=[ character(len=80)::& + &'F/app','F/fpm.toml','F/README.md','F/src','F/app/main.f90','F/src/'//scr//'F.f90'] + case('G'); expected=[ character(len=80)::& + &'G/app','G/fpm.toml','G/README.md','G/test','G/app/main.f90','G/test/check.f90'] + case('H'); expected=[ character(len=80)::& + &'H/example','H/fpm.toml','H/README.md','H/example/demo.f90'] + case('BB'); expected=[ character(len=80)::& + &'BB/fpm.toml','BB/README.md','BB/src','BB/test','BB/src/'//scr//'BB.f90','BB/test/check.f90'] + case('CC'); expected=[ character(len=80)::& + &'CC/app','CC/fpm.toml','CC/README.md','CC/src','CC/test','CC/app/main.f90','CC/src/'//scr//'CC.f90','CC/test/check.f90'] + case default + write(*,*)'ERROR: internal error. unknown directory name ',trim(shortdirs(i)) + stop 4 + end select + !! MSwindows has hidden files in it + !! Warning: This only looks for expected files. If there are more files than expected it does not fail + call list_files(trim(directories(i)), file_names,recurse=.true.) + + if(size(expected).ne.size(file_names))then + write(*,*)'WARNING: unexpected number of files in file list=',size(file_names),' expected ',size(expected) + write(*,'("EXPECTED: ",*(g0:,","))')(scr//trim(expected(j)),j=1,size(expected)) + write(*,'("FOUND: ",*(g0:,","))')(trim(file_names(j)%s),j=1,size(file_names)) + endif + + do j=1,size(expected) + + expected(j)=scr//expected(j) + if(is_os_windows) expected(j)=windows_path(expected(j)) + if( .not.(trim(expected(j)).in.file_names) )then + tally=[tally,.false.] + write(*,'("ERROR: FOUND ",*(g0:,", "))')( trim(file_names(k)%s), k=1,size(file_names) ) + write(*,'(*(g0))')' BUT NO MATCH FOR ',expected(j) + tally=[tally,.false.] + cycle TESTS + endif + enddo + tally=[tally,.true.] + endif + enddo TESTS + + ! clean up scratch files; might want an option to leave them for inspection + select case (get_os_type()) + case (OS_UNKNOWN, OS_LINUX, OS_MACOS, OS_CYGWIN, OS_SOLARIS, OS_FREEBSD) + call execute_command_line('rm -rf fpm_scratch_*',exitstat=estat,cmdstat=cstat,cmdmsg=message) + case (OS_WINDOWS) + call execute_command_line('rmdir fpm_scratch_* /s /q',exitstat=estat,cmdstat=cstat,cmdmsg=message) + end select + + write(*,'("new TEST TALLY=",*(g0))')tally + if(all(tally))then + write(*,'(*(g0))')'PASSED: all ',count(tally),' tests passed ' + else + write(*,*)'FAILED: PASSED=',count(tally),' FAILED=',count(.not.tally) + stop 5 + endif +contains + function get_command_path() result(prog) + character(len=:), allocatable :: prog + + character(len=:), allocatable :: path + integer :: length + + ! FIXME: Super hacky way to get the name of the fpm executable, + ! it works better than invoking fpm again but should be replaced ASAP. + call get_command_argument(0, length=length) + allocate(character(len=length) :: prog) + call get_command_argument(0, prog) + path = dirname(prog) + if (get_os_type() == OS_WINDOWS) then + prog = join_path(path, "..", "app", "fpm.exe") + if (.not.exists(prog)) then + prog = join_path(path, "..", "..", "app", "fpm.exe") + end if + else + prog = join_path(path, "..", "app", "fpm") + if (.not.exists(prog)) then + prog = join_path(path, "..", "..", "app", "fpm") + end if + end if + + end function +end program new_test |