aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorSebastian Ehlert <28669218+awvwgk@users.noreply.github.com>2021-03-31 16:13:58 +0200
committerGitHub <noreply@github.com>2021-03-31 16:13:58 +0200
commitd9dc9f2ae5f196c15a7d35cddabc805c40ff86ce (patch)
tree6f61952c630b023edec391daae2747063703d489 /test
parent5422ec57f4081bf2225f5dde5cc07999bf8010f9 (diff)
downloadfpm-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.f90236
-rw-r--r--test/fpm_test/main.f90106
-rw-r--r--test/fpm_test/test_backend.f90353
-rw-r--r--test/fpm_test/test_filesystem.f90106
-rw-r--r--test/fpm_test/test_installer.f90168
-rw-r--r--test/fpm_test/test_manifest.f901085
-rw-r--r--test/fpm_test/test_module_dependencies.f90666
-rw-r--r--test/fpm_test/test_package_dependencies.f90240
-rw-r--r--test/fpm_test/test_source_parsing.f90758
-rw-r--r--test/fpm_test/test_toml.f90107
-rw-r--r--test/fpm_test/test_versioning.f90405
-rw-r--r--test/fpm_test/testsuite.f90286
-rw-r--r--test/help_test/help_test.f90292
-rw-r--r--test/new_test/new_test.f90187
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