!> 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%external_modules(0)) 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%external_modules(0)) 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%external_modules(0)) 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%external_modules(0)) 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%external_modules(0)) 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%external_modules(0)) 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%external_modules(0)) 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%external_modules(0)) 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%external_modules(0)) 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