aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fpm/src/fpm/manifest/package.f909
-rw-r--r--fpm/src/fpm/versioning.f90394
-rw-r--r--fpm/test/fpm_test/main.f904
-rw-r--r--fpm/test/fpm_test/test_versioning.f90405
4 files changed, 811 insertions, 1 deletions
diff --git a/fpm/src/fpm/manifest/package.f90 b/fpm/src/fpm/manifest/package.f90
index dff81e5..039aa78 100644
--- a/fpm/src/fpm/manifest/package.f90
+++ b/fpm/src/fpm/manifest/package.f90
@@ -35,6 +35,7 @@ module fpm_manifest_package
use fpm_error, only : error_t, fatal_error, syntax_error
use fpm_toml, only : toml_table, toml_array, toml_key, toml_stat, get_value, &
& len
+ use fpm_versioning, only : version_t, new_version
implicit none
private
@@ -47,6 +48,9 @@ module fpm_manifest_package
!> Name of the package
character(len=:), allocatable :: name
+ !> Package version
+ type(version_t) :: version
+
!> Library meta data
type(library_t), allocatable :: library
@@ -87,6 +91,7 @@ contains
type(toml_table), pointer :: child, node
type(toml_array), pointer :: children
+ character(len=:), allocatable :: version
integer :: ii, nn, stat
call check(table, error)
@@ -98,6 +103,10 @@ contains
return
end if
+ call get_value(table, "version", version, "0")
+ call new_version(self%version, version, error)
+ if (allocated(error)) return
+
call get_value(table, "dependencies", child, requested=.false.)
if (associated(child)) then
call new_dependencies(self%dependency, child, error)
diff --git a/fpm/src/fpm/versioning.f90 b/fpm/src/fpm/versioning.f90
new file mode 100644
index 0000000..145427e
--- /dev/null
+++ b/fpm/src/fpm/versioning.f90
@@ -0,0 +1,394 @@
+!> Implementation of versioning data for comparing packages
+module fpm_versioning
+ use fpm_error, only : error_t, syntax_error
+ implicit none
+ private
+
+ public :: version_t, new_version
+
+
+ type :: version_t
+ private
+
+ !> Version numbers found
+ integer, allocatable :: num(:)
+
+ contains
+
+ generic :: operator(==) => equals
+ procedure, private :: equals
+
+ generic :: operator(/=) => not_equals
+ procedure, private :: not_equals
+
+ generic :: operator(>) => greater
+ procedure, private :: greater
+
+ generic :: operator(<) => less
+ procedure, private :: less
+
+ generic :: operator(>=) => greater_equals
+ procedure, private :: greater_equals
+
+ generic :: operator(<=) => less_equals
+ procedure, private :: less_equals
+
+ !> Compare a version against a version constraint (x.x.0 <= v < x.x.HUGE)
+ generic :: operator(.match.) => match
+ procedure, private :: match
+
+ !> Create a printable string from a version data type
+ procedure :: to_string
+
+ end type version_t
+
+
+ !> Arbitrary internal limit of the version parser
+ integer, parameter :: max_limit = 3
+
+
+ interface new_version
+ module procedure :: new_version_from_string
+ module procedure :: new_version_from_int
+ end interface new_version
+
+
+contains
+
+
+ !> Create a new version from a string
+ subroutine new_version_from_int(self, num)
+
+ !> Instance of the versioning data
+ type(version_t), intent(out) :: self
+
+ !> Subversion numbers to define version data
+ integer, intent(in) :: num(:)
+
+ self%num = num
+
+ end subroutine new_version_from_int
+
+
+ !> Create a new version from a string
+ subroutine new_version_from_string(self, string, error)
+
+ !> Instance of the versioning data
+ type(version_t), intent(out) :: self
+
+ !> String describing the version information
+ character(len=*), intent(in) :: string
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ character :: tok
+ integer :: ii, istart, iend, stat, nn
+ integer :: num(max_limit)
+ logical :: is_number
+
+ nn = 0
+ iend = 0
+ istart = 0
+ is_number = .false.
+
+ do while(iend < len(string))
+ call next(string, istart, iend, is_number, error)
+ if (allocated(error)) exit
+ if (is_number) then
+ if (nn >= max_limit) then
+ call token_error(error, string, istart, iend, &
+ & "Too many subversions found")
+ exit
+ end if
+ nn = nn + 1
+ read(string(istart:iend), *, iostat=stat) num(nn)
+ if (stat /= 0) then
+ call token_error(error, string, istart, iend, &
+ & "Failed to parse version number")
+ exit
+ end if
+ end if
+ end do
+ if (allocated(error)) return
+ if (.not.is_number) then
+ call token_error(error, string, istart, iend, &
+ & "Expected version number, but no characters are left")
+ return
+ end if
+
+ call new_version(self, num(:nn))
+
+ end subroutine new_version_from_string
+
+
+ !> Tokenize a version string
+ subroutine next(string, istart, iend, is_number, error)
+
+ !> String describing the version information
+ character(len=*), intent(in) :: string
+
+ !> Start of last token, start of next token on exit
+ integer, intent(inout) :: istart
+
+ !> End of last token on entry, end of next token on exit
+ integer, intent(inout) :: iend
+
+ !> Token produced is a number
+ logical, intent(inout) :: is_number
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: ii, nn
+ logical :: was_number
+ character :: tok, last
+
+ was_number = is_number
+ nn = len(string)
+
+ if (iend >= nn) then
+ istart = nn
+ iend = nn
+ return
+ end if
+
+ ii = min(iend + 1, nn)
+ tok = string(ii:ii)
+
+ is_number = tok /= '.'
+ if (is_number .eqv. was_number) then
+ call token_error(error, string, istart, ii, &
+ & "Unexpected token found")
+ return
+ end if
+
+ if (.not.is_number) then
+ is_number = .false.
+ istart = ii
+ iend = ii
+ return
+ end if
+
+ istart = ii
+ do ii = min(iend + 1, nn), nn
+ tok = string(ii:ii)
+ select case(tok)
+ case default
+ call token_error(error, string, istart, ii, &
+ & "Invalid character in version number")
+ exit
+ case('.')
+ exit
+ case('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')
+ iend = ii
+ cycle
+ end select
+ end do
+
+ end subroutine next
+
+
+ !> Create an error on an invalid token, provide some visual context as well
+ subroutine token_error(error, string, istart, iend, message)
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ !> String describing the version information
+ character(len=*), intent(in) :: string
+
+ !> Start of last token, start of next token on exit
+ integer, intent(in) :: istart
+
+ !> End of last token on entry, end of next token on exit
+ integer, intent(in) :: iend
+
+ !> Error message
+ character(len=*), intent(in) :: message
+
+ character(len=*), parameter :: nl = new_line('a')
+
+ allocate(error)
+ error%message = message // nl // " | " // string // nl // &
+ & " |" // repeat('-', istart) // repeat('^', iend - istart + 1)
+
+ end subroutine token_error
+
+
+ subroutine to_string(self, string)
+
+ !> Version number
+ class(version_t), intent(in) :: self
+
+ !> Character representation of the version
+ character(len=:), allocatable, intent(out) :: string
+
+ integer, parameter :: buffersize = 64
+ character(len=buffersize) :: buffer
+ integer :: ii
+
+ do ii = 1, size(self%num)
+ if (allocated(string)) then
+ write(buffer, '(".", i0)') self%num(ii)
+ string = string // trim(buffer)
+ else
+ write(buffer, '(i0)') self%num(ii)
+ string = trim(buffer)
+ end if
+ end do
+
+ if (.not.allocated(string)) then
+ string = '0'
+ end if
+
+ end subroutine to_string
+
+
+ !> Check to version numbers for equality
+ elemental function equals(lhs, rhs) result(is_equal)
+
+ !> First version number
+ class(version_t), intent(in) :: lhs
+
+ !> Second version number
+ class(version_t), intent(in) :: rhs
+
+ !> Version match
+ logical :: is_equal
+
+ is_equal = .not.(lhs > rhs)
+ if (is_equal) then
+ is_equal = .not.(rhs > lhs)
+ end if
+
+ end function equals
+
+
+ !> Check two versions for inequality
+ elemental function not_equals(lhs, rhs) result(not_equal)
+
+ !> First version number
+ class(version_t), intent(in) :: lhs
+
+ !> Second version number
+ class(version_t), intent(in) :: rhs
+
+ !> Version mismatch
+ logical :: not_equal
+
+ not_equal = lhs > rhs
+ if (.not.not_equal) then
+ not_equal = rhs > lhs
+ end if
+
+ end function not_equals
+
+
+ !> Relative comparison of two versions
+ elemental function greater(lhs, rhs) result(is_greater)
+
+ !> First version number
+ class(version_t), intent(in) :: lhs
+
+ !> Second version number
+ class(version_t), intent(in) :: rhs
+
+ !> First version is greater
+ logical :: is_greater
+
+ integer :: ii
+
+ do ii = 1, min(size(lhs%num), size(rhs%num))
+ is_greater = lhs%num(ii) > rhs%num(ii)
+ if (is_greater) exit
+ end do
+ if (is_greater) return
+
+ is_greater = size(lhs%num) > size(rhs%num)
+ if (is_greater) then
+ do ii = size(rhs%num) + 1, size(lhs%num)
+ is_greater = lhs%num(ii) > 0
+ if (is_greater) exit
+ end do
+ end if
+
+ end function greater
+
+
+ !> Relative comparison of two versions
+ elemental function less(lhs, rhs) result(is_less)
+
+ !> First version number
+ class(version_t), intent(in) :: lhs
+
+ !> Second version number
+ class(version_t), intent(in) :: rhs
+
+ !> First version is less
+ logical :: is_less
+
+ is_less = rhs > lhs
+
+ end function less
+
+
+ !> Relative comparison of two versions
+ elemental function greater_equals(lhs, rhs) result(is_greater_equal)
+
+ !> First version number
+ class(version_t), intent(in) :: lhs
+
+ !> Second version number
+ class(version_t), intent(in) :: rhs
+
+ !> First version is greater or equal
+ logical :: is_greater_equal
+
+ is_greater_equal = .not. (rhs > lhs)
+
+ end function greater_equals
+
+
+ !> Relative comparison of two versions
+ elemental function less_equals(lhs, rhs) result(is_less_equal)
+
+ !> First version number
+ class(version_t), intent(in) :: lhs
+
+ !> Second version number
+ class(version_t), intent(in) :: rhs
+
+ !> First version is less or equal
+ logical :: is_less_equal
+
+ is_less_equal = .not. (lhs > rhs)
+
+ end function less_equals
+
+
+ !> Try to match first version against second version
+ elemental function match(lhs, rhs)
+
+ !> First version number
+ class(version_t), intent(in) :: lhs
+
+ !> Second version number
+ class(version_t), intent(in) :: rhs
+
+ !> Version match following semantic versioning rules
+ logical :: match
+
+ type(version_t) :: tmp
+
+ match = .not.(rhs > lhs)
+ if (match) then
+ tmp%num = rhs%num
+ tmp%num(size(tmp%num)) = tmp%num(size(tmp%num)) + 1
+ match = tmp > lhs
+ end if
+
+ end function match
+
+
+end module fpm_versioning
diff --git a/fpm/test/fpm_test/main.f90 b/fpm/test/fpm_test/main.f90
index bc8ad29..6f20a3f 100644
--- a/fpm/test/fpm_test/main.f90
+++ b/fpm/test/fpm_test/main.f90
@@ -6,6 +6,7 @@ program fpm_testing
use test_toml, only : collect_toml
use test_manifest, only : collect_manifest
use test_source_parsing, only : collect_source_parsing
+ use test_versioning, only : collect_versioning
implicit none
integer :: stat, is
character(len=:), allocatable :: suite_name, test_name
@@ -17,7 +18,8 @@ program fpm_testing
testsuite = [ &
& new_testsuite("fpm_toml", collect_toml), &
& new_testsuite("fpm_manifest", collect_manifest), &
- & new_testsuite("fpm_source_parsing", collect_source_parsing) &
+ & new_testsuite("fpm_source_parsing", collect_source_parsing), &
+ & new_testsuite("fpm_versioning", collect_versioning) &
& ]
call get_argument(1, suite_name)
diff --git a/fpm/test/fpm_test/test_versioning.f90 b/fpm/test/fpm_test/test_versioning.f90
new file mode 100644
index 0000000..f6dcb57
--- /dev/null
+++ b/fpm/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