Browse Source

Merge topic 'find-cps-version' into release-4.0

3e6466eb16 find_package: Honor version requests when finding CPS packages
7a0e698384 find_package: Fix CPS version parsing
35a7ed125b find_package: Fix reporting of rejected CPS files' version

Acked-by: Kitware Robot <[email protected]>
Acked-by: buildbot <[email protected]>
Merge-request: !10297
Brad King 8 months ago
parent
commit
3f2386db2b
44 changed files with 663 additions and 100 deletions
  1. 95 24
      Help/command/find_package.rst
  2. 82 25
      Source/cmFindPackageCommand.cxx
  3. 71 24
      Source/cmPackageInfoReader.cxx
  4. 28 5
      Source/cmPackageInfoReader.h
  5. 48 20
      Tests/FindPackageCpsTest/CMakeLists.txt
  6. 8 0
      Tests/FindPackageCpsTest/cps/badversion1.cps
  7. 8 0
      Tests/FindPackageCpsTest/cps/badversion2.cps
  8. 8 0
      Tests/FindPackageCpsTest/cps/badversion3.cps
  9. 8 0
      Tests/FindPackageCpsTest/cps/badversion4.cps
  10. 8 0
      Tests/FindPackageCpsTest/cps/customversion.cps
  11. 8 0
      Tests/FindPackageCpsTest/cps/emptymarker.cps
  12. 8 0
      Tests/FindPackageCpsTest/cps/longversion.cps
  13. 7 0
      Tests/FindPackageCpsTest/share/cps/SortLib/SortLib.cps
  14. 13 0
      Tests/RunCMake/find_package-CPS/CompatVersion.cmake
  15. 13 0
      Tests/RunCMake/find_package-CPS/CustomVersion.cmake
  16. 13 0
      Tests/RunCMake/find_package-CPS/ExactVersion.cmake
  17. 1 1
      Tests/RunCMake/find_package-CPS/MissingComponent-stderr.txt
  18. 1 1
      Tests/RunCMake/find_package-CPS/MissingTransitiveComponent-stderr.txt
  19. 1 0
      Tests/RunCMake/find_package-CPS/MissingVersion1-result.txt
  20. 13 0
      Tests/RunCMake/find_package-CPS/MissingVersion1-stderr.txt
  21. 10 0
      Tests/RunCMake/find_package-CPS/MissingVersion1.cmake
  22. 1 0
      Tests/RunCMake/find_package-CPS/MissingVersion2-result.txt
  23. 13 0
      Tests/RunCMake/find_package-CPS/MissingVersion2-stderr.txt
  24. 10 0
      Tests/RunCMake/find_package-CPS/MissingVersion2.cmake
  25. 13 0
      Tests/RunCMake/find_package-CPS/MultipleVersions.cmake
  26. 16 0
      Tests/RunCMake/find_package-CPS/RunCMakeTest.cmake
  27. 13 0
      Tests/RunCMake/find_package-CPS/TransitiveVersion.cmake
  28. 14 0
      Tests/RunCMake/find_package-CPS/VersionLimit1.cmake
  29. 14 0
      Tests/RunCMake/find_package-CPS/VersionLimit2.cmake
  30. 1 0
      Tests/RunCMake/find_package-CPS/VersionLimit3-result.txt
  31. 13 0
      Tests/RunCMake/find_package-CPS/VersionLimit3-stderr.txt
  32. 11 0
      Tests/RunCMake/find_package-CPS/VersionLimit3.cmake
  33. 1 0
      Tests/RunCMake/find_package-CPS/VersionLimit4-result.txt
  34. 13 0
      Tests/RunCMake/find_package-CPS/VersionLimit4-stderr.txt
  35. 11 0
      Tests/RunCMake/find_package-CPS/VersionLimit4.cmake
  36. 8 0
      Tests/RunCMake/find_package-CPS/cps/CustomVersion/37/CustomVersion.cps
  37. 8 0
      Tests/RunCMake/find_package-CPS/cps/CustomVersion/42/CustomVersion.cps
  38. 8 0
      Tests/RunCMake/find_package-CPS/cps/CustomVersion/55/CustomVersion.cps
  39. 1 0
      Tests/RunCMake/find_package-CPS/cps/componenttest.cps
  40. 8 0
      Tests/RunCMake/find_package-CPS/cps/sample/1.1.0/sample.cps
  41. 8 0
      Tests/RunCMake/find_package-CPS/cps/sample/1.2.3/sample.cps
  42. 8 0
      Tests/RunCMake/find_package-CPS/cps/sample/1.4.2/sample.cps
  43. 7 0
      Tests/RunCMake/find_package-CPS/cps/sample/1.5.0/sample.cps
  44. 11 0
      Tests/RunCMake/find_package-CPS/cps/transitiveversion.cps

+ 95 - 24
Help/command/find_package.rst

@@ -79,18 +79,14 @@ The command has a few modes by which it searches for packages:
   version files are used).
 
   .. note::
-
     If the experimental ``CMAKE_EXPERIMENTAL_FIND_CPS_PACKAGES`` is enabled,
     files named ``<PackageName>.cps`` and ``<lowercasePackageName>.cps`` are
     also considered.  These files provide package information according to the
     |CPS|_ (CPS), which is more portable than CMake script.  Aside from any
     explicitly noted exceptions, any references to "config files", "config
     mode", "package configuration files", and so forth refer equally to both
-    CPS and CMake-script files.  However, some features of ``find_package``
-    are not supported at this time when a CPS file is found.  In particular,
-    if a ``VERSION`` requirement is specified, only ``.cps`` files which do not
-    provide version information will be rejected.  (We expect to implement
-    proper version validation in the near future.)
+    CPS and CMake-script files.  This functionality is a work in progress, and
+    some features may be missing.
 
     Search is implemented in a manner that will tend to prefer |CPS| files
     over CMake-script config files in most cases.  Specifying ``CONFIGS``
@@ -211,15 +207,20 @@ specified:
 * A single version with the format ``major[.minor[.patch[.tweak]]]``, where
   each component is a numeric value.
 * A version range with the format ``versionMin...[<]versionMax`` where
-  ``versionMin`` and ``versionMax`` have the same format and constraints
-  on components being integers as the single version.  By default, both end
-  points are included.  By specifying ``<``, the upper end point will be
-  excluded. Version ranges are only supported with CMake 3.19 or later.
-  Note that it is not possible to extend the compatibility range specified
-  by the package's version file.  For example, if the package version file
-  specifies compatibility within a minor version, it is not possible to
-  extend the compatibility to several minor versions by specifying a
-  version range.
+  ``versionMin`` and ``versionMax`` have the same format and constraints on
+  components being integers as the single version.  By default, both end points
+  are included.  By specifying ``<``, the upper end point will be excluded.
+  Version ranges are only supported with CMake 3.19 or later.
+
+.. note::
+  With the exception of CPS packages, version support is currently provided
+  only on a package-by-package basis.  When a version range is specified but
+  the package is only designed to expect a single version, the package will
+  ignore the upper end point of the range and only take the single version at
+  the lower end of the range into account.  Non-CPS packages that do support
+  version ranges do so in a manner that is determined by the individual
+  package.  See the `Version Selection`_ section below for details and
+  important caveats.
 
 The ``EXACT`` option requests that the version be matched exactly. This option
 is incompatible with the specification of a version range.
@@ -227,11 +228,7 @@ is incompatible with the specification of a version range.
 If no ``[version]`` and/or component list is given to a recursive invocation
 inside a find-module, the corresponding arguments are forwarded
 automatically from the outer call (including the ``EXACT`` flag for
-``[version]``).  Version support is currently provided only on a
-package-by-package basis (see the `Version Selection`_ section below).
-When a version range is specified but the package is only designed to expect
-a single version, the package will ignore the upper end point of the range and
-only take the single version at the lower end of the range into account.
+``[version]``).
 
 See the :command:`cmake_policy` command documentation for discussion
 of the ``NO_POLICY_SCOPE`` option.
@@ -749,7 +746,7 @@ sets these variables:
 These variables are checked by the ``find_package`` command to determine
 whether the configuration file provides an acceptable version.  They
 are not available after the ``find_package`` call returns.  If the version
-is acceptable the following variables are set:
+is acceptable, the following variables are set:
 
 ``<PackageName>_VERSION``
   Full provided version string
@@ -766,12 +763,80 @@ is acceptable the following variables are set:
 
 and the corresponding package configuration file is loaded.
 
+.. note::
+  While the exact behavior of version matching is determined by the individual
+  package, many packages use :command:`write_basic_package_version_file` to
+  supply this logic.  The version check scripts this produces have some notable
+  caveats with respect to version ranges:
+
+  * The upper end of a version range acts as a hard limit on what versions will
+    be accepted.  Thus, while a request for version ``1.4.0`` might be
+    satisfied by a package whose version is ``1.6.0`` and which advertises
+    'same major version' compatibility, the same package will be rejected if
+    the requested version range is ``1.4.0...1.5.0``.
+
+  * Both ends of the version range must match the package's advertised
+    compatibility level. For example, if a package advertises 'same major and
+    minor version' compatibility, requesting the version range
+    ``1.4.0...<1.5.5`` or ``1.4.0...1.5.0`` will result in that package being
+    rejected, even if the package version is ``1.4.1``.
+
+  As a result, it is not possible to use a version range to extend the range
+  of compatible package versions that will be accepted.
+
 |CPS|
 """""
 
-For |CPS| package configuration files, no version checking is performed at
-this time.  However, packages using the ``simple`` version schema will set
-the following variables:
+For |CPS| package configuration files, package version numbers are checked by
+CMake according to the set of recognized version schemas. At present, the
+following schemas are recognized:
+
+  ``simple``
+    Version numbers are a tuple of integers followed by an optional trailing
+    segment which is ignored with respect to version comparisons.
+
+  ``custom``
+    The mechanism for interpreting version numbers is unspecified.  The version
+    strings must match exactly for the package to be accepted.
+
+Refer to |cps-version_schema|_ for a more detailed explanation of each schema
+and how comparisons for each are performed.  Note that the specification may
+include schemas that are not supported by CMake.
+
+In addition to the package's ``version``, CPS allows packages to optionally
+specify a |cps-compat_version|_, which is the oldest version for which the
+package provides compatibility.  That is, the package warrants that a consumer
+expecting the ``compat_version`` should be able to use the package, even if the
+package's actual version is newer.  If not specified, the ``compat_version``
+is implicitly equal to the package version, i.e. no backwards compatibility is
+provided.
+
+When a package uses a recognized schema, CMake will determine the package's
+acceptability according to the following rules:
+
+* If ``EXACT`` was specified, or if the package does not supply a
+  ``compat_version``, the package's ``version`` must equal the requested
+  version.
+
+* Otherwise:
+
+  * The package's ``version`` must be greater than or equal to the requested
+    (minimum) version, and
+
+  * the package's ``compat_version`` must be less than or equal to the
+    requested (minimum) version, and
+
+  * if a requested maximum version was given, it must be greater than (or equal
+    to, depending on whether the maximum version is specified as inclusive or
+    exclusive) the package's ``version``.
+
+.. note::
+  This implementation of range matching was chosen in order to most closely
+  match the behavior of :command:`write_basic_package_version_file`, albeit
+  without the case where an overly broad range matches nothing.
+
+For packages using the ``simple`` version schema, if the version is acceptable,
+the following variables are set:
 
 ``<PackageName>_VERSION``
   Full provided version string
@@ -878,3 +943,9 @@ requirements are not satisfied.
 
 .. _CPS: https://cps-org.github.io/cps/
 .. |CPS| replace:: Common Package Specification
+
+.. _cps-compat_version: https://cps-org.github.io/cps/schema.html#compat-version
+.. |cps-compat_version| replace:: ``compat_version``
+
+.. _cps-version_schema: https://cps-org.github.io/cps/schema.html#version-schema
+.. |cps-version_schema| replace:: ``version_schema``

+ 82 - 25
Source/cmFindPackageCommand.cxx

@@ -59,6 +59,7 @@
 namespace {
 
 using pdt = cmFindPackageCommand::PackageDescriptionType;
+using ParsedVersion = cmPackageInfoReader::Pep440Version;
 
 template <template <typename> class Op>
 struct StrverscmpOp
@@ -2717,12 +2718,61 @@ bool cmFindPackageCommand::CheckVersion(std::string const& config_file)
     std::unique_ptr<cmPackageInfoReader> reader =
       cmPackageInfoReader::Read(config_file);
     if (reader && reader->GetName() == this->Name) {
+      // Read version information.
       cm::optional<std::string> cpsVersion = reader->GetVersion();
-      if (cpsVersion) {
-        // TODO: Implement version check for CPS
-        result = true;
-      } else {
-        result = this->Version.empty();
+      cm::optional<ParsedVersion> const& parsedVersion =
+        reader->ParseVersion(cpsVersion);
+      bool const hasVersion = cpsVersion.has_value();
+
+      // Test for version compatibility.
+      result = this->Version.empty();
+      if (hasVersion) {
+        version = std::move(*cpsVersion);
+
+        if (!this->Version.empty()) {
+          if (!parsedVersion) {
+            // If we don't understand the version, compare the exact versions
+            // using full string comparison. This is the correct behavior for
+            // the "custom" schema, and the best we can do otherwise.
+            result = (this->Version == version);
+          } else if (this->VersionExact) {
+            // If EXACT is specified, the version must be exactly the requested
+            // version.
+            result =
+              cmSystemTools::VersionCompareEqual(this->Version, version);
+          } else {
+            // Do we have a compat_version?
+            cm::optional<std::string> const& compatVersion =
+              reader->GetCompatVersion();
+            if (reader->ParseVersion(compatVersion)) {
+              // If yes, the initial result is whether the requested version is
+              // between the actual version and the compat version, inclusive.
+              result = cmSystemTools::VersionCompareGreaterEq(version,
+                                                              this->Version) &&
+                cmSystemTools::VersionCompareGreaterEq(this->Version,
+                                                       *compatVersion);
+
+              if (result && !this->VersionMax.empty()) {
+                // We must also check that the version is less than the version
+                // limit.
+                if (this->VersionRangeMax == VERSION_ENDPOINT_EXCLUDED) {
+                  result = cmSystemTools::VersionCompareGreater(
+                    this->VersionMax, version);
+                } else {
+                  result = cmSystemTools::VersionCompareGreaterEq(
+                    this->VersionMax, version);
+                }
+              }
+            } else {
+              // If no, compat_version is assumed to be exactly the actual
+              // version, so the result is whether the requested version is
+              // exactly the actual version, and we can ignore the version
+              // limit.
+              result =
+                cmSystemTools::VersionCompareEqual(this->Version, version);
+            }
+          }
+        }
       }
 
       if (result) {
@@ -2752,26 +2802,33 @@ bool cmFindPackageCommand::CheckVersion(std::string const& config_file)
           result = false;
         }
 
-        if (result && cpsVersion) {
-          this->VersionFound = (version = std::move(*cpsVersion));
-
-          std::vector<unsigned> const& versionParts = reader->ParseVersion();
-          this->VersionFoundCount = static_cast<unsigned>(versionParts.size());
-          switch (this->VersionFoundCount) {
-            case 4:
-              this->VersionFoundTweak = versionParts[3];
-              CM_FALLTHROUGH;
-            case 3:
-              this->VersionFoundPatch = versionParts[2];
-              CM_FALLTHROUGH;
-            case 2:
-              this->VersionFoundMinor = versionParts[1];
-              CM_FALLTHROUGH;
-            case 1:
-              this->VersionFoundMajor = versionParts[0];
-              CM_FALLTHROUGH;
-            default:
-              break;
+        if (result && hasVersion) {
+          this->VersionFound = version;
+
+          if (parsedVersion) {
+            std::vector<unsigned> const& versionParts =
+              parsedVersion->ReleaseComponents;
+
+            this->VersionFoundCount =
+              static_cast<unsigned>(versionParts.size());
+            switch (std::min(this->VersionFoundCount, 4u)) {
+              case 4:
+                this->VersionFoundTweak = versionParts[3];
+                CM_FALLTHROUGH;
+              case 3:
+                this->VersionFoundPatch = versionParts[2];
+                CM_FALLTHROUGH;
+              case 2:
+                this->VersionFoundMinor = versionParts[1];
+                CM_FALLTHROUGH;
+              case 1:
+                this->VersionFoundMajor = versionParts[0];
+                CM_FALLTHROUGH;
+              default:
+                break;
+            }
+          } else {
+            this->VersionFoundCount = 0;
           }
         }
         this->CpsReader = std::move(reader);

+ 71 - 24
Source/cmPackageInfoReader.cxx

@@ -366,6 +366,62 @@ void AddDefinitions(cmMakefile* makefile, cmTarget* target,
   }
 }
 
+cm::optional<cmPackageInfoReader::Pep440Version> ParseSimpleVersion(
+  std::string const& version)
+{
+  if (version.empty()) {
+    return cm::nullopt;
+  }
+
+  cmPackageInfoReader::Pep440Version result;
+  result.Simple = true;
+
+  cm::string_view remnant{ version };
+  for (;;) {
+    // Find the next part separator.
+    std::string::size_type const n = remnant.find_first_of(".+-"_s);
+    if (n == 0) {
+      // The part is an empty string.
+      return cm::nullopt;
+    }
+
+    // Extract the part as a number.
+    cm::string_view const part = remnant.substr(0, n);
+    std::string::size_type const l = part.size();
+    std::string::size_type p;
+    unsigned long const value = std::stoul(std::string{ part }, &p);
+    if (p != l || value > std::numeric_limits<unsigned>::max()) {
+      // The part was not a valid number or is too big.
+      return cm::nullopt;
+    }
+    result.ReleaseComponents.push_back(static_cast<unsigned>(value));
+
+    // Have we consumed the entire input?
+    if (n == std::string::npos) {
+      return { std::move(result) };
+    }
+
+    // Lop off the current part.
+    char const sep = remnant[n];
+    remnant = remnant.substr(n + 1);
+    if (sep == '+' || sep == '-') {
+      // If we hit the local label, we're done.
+      result.LocalLabel = remnant;
+      return { std::move(result) };
+    }
+
+    // We just consumed a '.'; check that there's more.
+    if (remnant.empty()) {
+      // A trailing part separator is not allowed.
+      return cm::nullopt;
+    }
+
+    // Continue with the remaining input.
+  }
+
+  // Unreachable.
+}
+
 } // namespace
 
 std::unique_ptr<cmPackageInfoReader> cmPackageInfoReader::Read(
@@ -428,40 +484,31 @@ cm::optional<std::string> cmPackageInfoReader::GetVersion() const
   return cm::nullopt;
 }
 
-std::vector<unsigned> cmPackageInfoReader::ParseVersion() const
+cm::optional<std::string> cmPackageInfoReader::GetCompatVersion() const
+{
+  Json::Value const& version = this->Data["compat_version"];
+  if (version.isString()) {
+    return version.asString();
+  }
+  return cm::nullopt;
+}
+
+cm::optional<cmPackageInfoReader::Pep440Version>
+cmPackageInfoReader::ParseVersion(
+  cm::optional<std::string> const& version) const
 {
   // Check that we have a version.
-  cm::optional<std::string> const& version = this->GetVersion();
   if (!version) {
-    return {};
+    return cm::nullopt;
   }
 
-  std::vector<unsigned> result;
-  cm::string_view remnant{ *version };
-
   // Check if we know how to parse the version.
   Json::Value const& schema = this->Data["version_schema"];
   if (schema.isNull() || cmStrCaseEq(schema.asString(), "simple"_s)) {
-    // Keep going until we run out of parts.
-    while (!remnant.empty()) {
-      std::string::size_type n = remnant.find('.');
-      cm::string_view part = remnant.substr(0, n);
-      if (n == std::string::npos) {
-        remnant = {};
-      } else {
-        remnant = remnant.substr(n + 1);
-      }
-
-      unsigned long const value = std::stoul(std::string{ part }, &n);
-      if (n == 0 || value > std::numeric_limits<unsigned>::max()) {
-        // The part was not a valid number or is too big.
-        return {};
-      }
-      result.push_back(static_cast<unsigned>(value));
-    }
+    return ParseSimpleVersion(*version);
   }
 
-  return result;
+  return cm::nullopt;
 }
 
 std::vector<cmPackageRequirement> cmPackageInfoReader::GetRequirements() const

+ 28 - 5
Source/cmPackageInfoReader.h

@@ -43,11 +43,34 @@ public:
 
   std::string GetName() const;
   cm::optional<std::string> GetVersion() const;
-
-  /// If the package uses the 'simple' version scheme, obtain the version as
-  /// a numeric tuple.  Returns an empty vector for other schemes or if no
-  /// version is specified.
-  std::vector<unsigned> ParseVersion() const;
+  cm::optional<std::string> GetCompatVersion() const;
+
+  // NOTE: The eventual intent is for CPS to support multiple version schemas,
+  // and in particular, we expect to want to support "simple", "custom", "rpm",
+  // "dpkg" and "pep440". Additionally, we desire to be able to parse each of
+  // these to the maximum extent possible; in particular, we want to be able
+  // to decompose "simple" and "pep440" versions into components represented
+  // as numeric types rather than strings, which is not possible with the "rpm"
+  // and "dpkg" schemas. Therefore, we require different data structures to
+  // represent different version schemas.
+
+  struct Pep440Version
+  {
+    // NOTE: This structure is currently incomplete as we only support the
+    // "simple" schema at this time.
+    bool Simple; // "simple" can be represented as a subset of "pep440"
+    std::vector<unsigned> ReleaseComponents;
+    cm::optional<std::string> LocalLabel;
+  };
+
+  // FIXME: Return a sum type (e.g. {cm,std}::variant) of possible versions
+  // when we support more than just the "simple" (and possibly "pep440")
+  // schema(s).
+  /// If the package uses the 'simple' version scheme, parse the provided
+  /// version string as a numeric tuple and optional trailing string.  Returns
+  /// a disengaged optional for other schemes or if no version is specified.
+  cm::optional<Pep440Version> ParseVersion(
+    cm::optional<std::string> const& version) const;
 
   std::vector<cmPackageRequirement> GetRequirements() const;
   std::vector<std::string> GetComponentNames() const;

+ 48 - 20
Tests/FindPackageCpsTest/CMakeLists.txt

@@ -21,24 +21,53 @@ set(CMAKE_FIND_FRAMEWORK FIRST)
 
 add_executable(FindPackageCpsTest FindPackageTest.cxx)
 
+###############################################################################
+
+function(expect PACKAGE VAR OP VALUE WHAT)
+  if(NOT ${PACKAGE}_${VAR} ${OP} ${VALUE})
+    message(SEND_ERROR "${PACKAGE} wrong ${WHAT} ${${PACKAGE}_${VAR}} !")
+  endif()
+endfunction()
+
+function(test_version PACKAGE LITERAL COUNT MAJOR MINOR PATCH TWEAK)
+  if(NOT ${PACKAGE}_FOUND)
+    message(SEND_ERROR "${PACKAGE} not found !")
+  else()
+    expect(${PACKAGE} VERSION STREQUAL "${LITERAL}" "version")
+    expect(${PACKAGE} VERSION_COUNT EQUAL ${COUNT} "version count")
+    expect(${PACKAGE} VERSION_MAJOR EQUAL ${MAJOR} "major version")
+    expect(${PACKAGE} VERSION_MINOR EQUAL ${MINOR} "minor version")
+    expect(${PACKAGE} VERSION_PATCH EQUAL ${PATCH} "patch version")
+    expect(${PACKAGE} VERSION_TWEAK EQUAL ${TWEAK} "tweak version")
+  endif()
+endfunction()
+
+function(test_unparsed_version PACKAGE VERSION)
+  find_package(${PACKAGE} CONFIG)
+  test_version(${PACKAGE} "${VERSION}" 0 0 0 0 0)
+endfunction()
+
 ###############################################################################
 # Test a basic package search.
 # It should NOT find the package's CMake package file.
 
 find_package(Sample CONFIG)
-if(NOT Sample_FOUND)
-  message(SEND_ERROR "Sample not found !")
-elseif(NOT Sample_VERSION STREQUAL "2.10.11")
-  message(SEND_ERROR "Sample wrong version ${Sample_VERSION} !")
-elseif(NOT Sample_VERSION_MAJOR EQUAL 2)
-  message(SEND_ERROR "Sample wrong major version ${Sample_VERSION_MAJOR} !")
-elseif(NOT Sample_VERSION_MINOR EQUAL 10)
-  message(SEND_ERROR "Sample wrong minor version ${Sample_VERSION_MINOR} !")
-elseif(NOT Sample_VERSION_PATCH EQUAL 11)
-  message(SEND_ERROR "Sample wrong patch version ${Sample_VERSION_PATCH} !")
-elseif(NOT Sample_VERSION_TWEAK EQUAL 0)
-  message(SEND_ERROR "Sample wrong tweak version ${Sample_VERSION_TWEAK} !")
-endif()
+test_version(Sample "2.10.11" 3 2 10 11 0)
+
+###############################################################################
+# Test some more complicated version parsing.
+
+find_package(LongVersion CONFIG)
+test_version(LongVersion "1.1.2.3.5.8+fibonacci" 6 1 1 2 3)
+
+find_package(EmptyMarker CONFIG)
+test_version(EmptyMarker "1.1+" 2 1 1 0 0)
+
+test_unparsed_version(BadVersion1 "1..1")
+test_unparsed_version(BadVersion2 "1.1a.0")
+test_unparsed_version(BadVersion3 "1.1.")
+test_unparsed_version(BadVersion4 "+42")
+test_unparsed_version(CustomVersion "VII")
 
 ###############################################################################
 # Test glob sorting.
@@ -62,13 +91,12 @@ endif()
 set(SortLib_DIR "" CACHE FILEPATH "Wipe out find results for testing." FORCE)
 unset(SortLib_VERSION)
 
-# TODO Add this test once CPS version checking is implemented.
-# set(SortLib_DIR "" CACHE FILEPATH "Wipe out find results for testing." FORCE)
-# FIND_PACKAGE(SortLib 4.0 CONFIG)
-# IF (NOT "${SortLib_VERSION}" STREQUAL "4.0.0")
-#   message(SEND_ERROR "FIND_PACKAGE_SORT_ORDER gave up too soon! ${SortLib_VERSION}")
-# endif()
-# unset(SortLib_VERSION)
+set(SortLib_DIR "" CACHE FILEPATH "Wipe out find results for testing." FORCE)
+FIND_PACKAGE(SortLib 4.0 CONFIG)
+IF (NOT "${SortLib_VERSION}" STREQUAL "4.0.0")
+  message(SEND_ERROR "FIND_PACKAGE_SORT_ORDER gave up too soon! ${SortLib_VERSION}")
+endif()
+unset(SortLib_VERSION)
 
 set(SortFramework_DIR "" CACHE FILEPATH "Wipe out find results for testing." FORCE)
 set(CMAKE_FIND_PACKAGE_SORT_ORDER NAME)

+ 8 - 0
Tests/FindPackageCpsTest/cps/badversion1.cps

@@ -0,0 +1,8 @@
+{
+  "cps_version": "0.13",
+  "name": "BadVersion1",
+  "version": "1..1",
+  "version_schema": "simple",
+  "cps_path": "@prefix@/cps",
+  "components": {}
+}

+ 8 - 0
Tests/FindPackageCpsTest/cps/badversion2.cps

@@ -0,0 +1,8 @@
+{
+  "cps_version": "0.13",
+  "name": "BadVersion2",
+  "version": "1.1a.0",
+  "version_schema": "simple",
+  "cps_path": "@prefix@/cps",
+  "components": {}
+}

+ 8 - 0
Tests/FindPackageCpsTest/cps/badversion3.cps

@@ -0,0 +1,8 @@
+{
+  "cps_version": "0.13",
+  "name": "BadVersion3",
+  "version": "1.1.",
+  "version_schema": "simple",
+  "cps_path": "@prefix@/cps",
+  "components": {}
+}

+ 8 - 0
Tests/FindPackageCpsTest/cps/badversion4.cps

@@ -0,0 +1,8 @@
+{
+  "cps_version": "0.13",
+  "name": "BadVersion4",
+  "version": "+42",
+  "version_schema": "simple",
+  "cps_path": "@prefix@/cps",
+  "components": {}
+}

+ 8 - 0
Tests/FindPackageCpsTest/cps/customversion.cps

@@ -0,0 +1,8 @@
+{
+  "cps_version": "0.13",
+  "name": "CustomVersion",
+  "version": "VII",
+  "version_schema": "roman",
+  "cps_path": "@prefix@/cps",
+  "components": {}
+}

+ 8 - 0
Tests/FindPackageCpsTest/cps/emptymarker.cps

@@ -0,0 +1,8 @@
+{
+  "cps_version": "0.13",
+  "name": "EmptyMarker",
+  "version": "1.1+",
+  "version_schema": "simple",
+  "cps_path": "@prefix@/cps",
+  "components": {}
+}

+ 8 - 0
Tests/FindPackageCpsTest/cps/longversion.cps

@@ -0,0 +1,8 @@
+{
+  "cps_version": "0.13",
+  "name": "LongVersion",
+  "version": "1.1.2.3.5.8+fibonacci",
+  "version_schema": "simple",
+  "cps_path": "@prefix@/cps",
+  "components": {}
+}

+ 7 - 0
Tests/FindPackageCpsTest/share/cps/SortLib/SortLib.cps

@@ -0,0 +1,7 @@
+{
+  "cps_version": "0.13",
+  "name": "SortLib",
+  "version": "4.0.0",
+  "cps_path": "@prefix@/cps/SortLib",
+  "components": {}
+}

+ 13 - 0
Tests/RunCMake/find_package-CPS/CompatVersion.cmake

@@ -0,0 +1,13 @@
+cmake_minimum_required(VERSION 4.0)
+
+include(Setup.cmake)
+
+set(CMAKE_FIND_PACKAGE_SORT_ORDER NAME)
+set(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC)
+
+###############################################################################
+# Test finding a package that is version-compatible with our request.
+find_package(Sample 1.2.0 REQUIRED)
+if(NOT Sample_VERSION STREQUAL "1.2.3+clarke")
+  message(SEND_ERROR "Sample wrong version ${Sample_VERSION} !")
+endif()

+ 13 - 0
Tests/RunCMake/find_package-CPS/CustomVersion.cmake

@@ -0,0 +1,13 @@
+cmake_minimum_required(VERSION 4.0)
+
+include(Setup.cmake)
+
+set(CMAKE_FIND_PACKAGE_SORT_ORDER NAME)
+set(CMAKE_FIND_PACKAGE_SORT_DIRECTION ASC)
+
+###############################################################################
+# Test finding a package that uses the "custom" version schema.
+find_package(CustomVersion 42 REQUIRED)
+if(NOT CustomVersion_VERSION EQUAL 42)
+  message(SEND_ERROR "CustomVersion wrong version ${CustomVersion_VERSION} !")
+endif()

+ 13 - 0
Tests/RunCMake/find_package-CPS/ExactVersion.cmake

@@ -0,0 +1,13 @@
+cmake_minimum_required(VERSION 4.0)
+
+include(Setup.cmake)
+
+set(CMAKE_FIND_PACKAGE_SORT_ORDER NAME)
+set(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC)
+
+###############################################################################
+# Test finding a package with multiple suitable versions (exact match).
+find_package(Sample 1.1.0 EXACT REQUIRED)
+if(NOT Sample_VERSION STREQUAL "1.1.0+asimov")
+  message(SEND_ERROR "Sample wrong version ${Sample_VERSION} !")
+endif()

+ 1 - 1
Tests/RunCMake/find_package-CPS/MissingComponent-stderr.txt

@@ -5,7 +5,7 @@ CMake Error at MissingComponent.cmake:[0-9]+ \(find_package\):
   The following configuration files were considered but not accepted:
 (
     [^
-]*/Tests/RunCMake/find_package-CPS/cps/[Cc]omponent[Tt]est\.cps, version: unknown)+
+]*/Tests/RunCMake/find_package-CPS/cps/[Cc]omponent[Tt]est\.cps, version: 1\.0)+
 
 Call Stack \(most recent call first\):
   CMakeLists\.txt:[0-9]+ \(include\)

+ 1 - 1
Tests/RunCMake/find_package-CPS/MissingTransitiveComponent-stderr.txt

@@ -5,7 +5,7 @@ CMake Error in cps/[Tt]ransitive[Mm]issing\.cps:
   The following configuration files were considered but not accepted:
 (
     [^
-]*/Tests/RunCMake/find_package-CPS/cps/[Cc]omponent[Tt]est\.cps, version: unknown)+
+]*/Tests/RunCMake/find_package-CPS/cps/[Cc]omponent[Tt]est\.cps, version: 1\.0)+
 
 Call Stack \(most recent call first\):
   MissingTransitiveComponent\.cmake:[0-9]+ \(find_package\)

+ 1 - 0
Tests/RunCMake/find_package-CPS/MissingVersion1-result.txt

@@ -0,0 +1 @@
+1

+ 13 - 0
Tests/RunCMake/find_package-CPS/MissingVersion1-stderr.txt

@@ -0,0 +1,13 @@
+CMake Error at MissingVersion1\.cmake:[0-9]+ \(find_package\):
+  Could not find a configuration file for package "Sample" that is compatible
+  with requested version "1\.2\.7"\.
+
+  The following configuration files were considered but not accepted:
+
+    .*/Tests/RunCMake/find_package-CPS/cps/sample/1\.5\.0/sample\.cps, version: 1\.5\.0\+niven
+    .*/Tests/RunCMake/find_package-CPS/cps/sample/1\.4\.2/sample\.cps, version: 1\.4\.2\+adams
+    .*/Tests/RunCMake/find_package-CPS/cps/sample/1\.2\.3/sample\.cps, version: 1\.2\.3\+clarke
+    .*/Tests/RunCMake/find_package-CPS/cps/sample/1\.1\.0/sample\.cps, version: 1\.1\.0\+asimov
+
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)

+ 10 - 0
Tests/RunCMake/find_package-CPS/MissingVersion1.cmake

@@ -0,0 +1,10 @@
+cmake_minimum_required(VERSION 4.0)
+
+include(Setup.cmake)
+
+set(CMAKE_FIND_PACKAGE_SORT_ORDER NAME)
+set(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC)
+
+###############################################################################
+# Test finding a package with no suitable version matches.
+find_package(Sample 1.2.7 REQUIRED)

+ 1 - 0
Tests/RunCMake/find_package-CPS/MissingVersion2-result.txt

@@ -0,0 +1 @@
+1

+ 13 - 0
Tests/RunCMake/find_package-CPS/MissingVersion2-stderr.txt

@@ -0,0 +1,13 @@
+CMake Error at MissingVersion2\.cmake:[0-9]+ \(find_package\):
+  Could not find a configuration file for package "Sample" that is compatible
+  with requested version "1\.4\.9"\.
+
+  The following configuration files were considered but not accepted:
+
+    .*/Tests/RunCMake/find_package-CPS/cps/sample/1\.5\.0/sample\.cps, version: 1\.5\.0\+niven
+    .*/Tests/RunCMake/find_package-CPS/cps/sample/1\.4\.2/sample\.cps, version: 1\.4\.2\+adams
+    .*/Tests/RunCMake/find_package-CPS/cps/sample/1\.2\.3/sample\.cps, version: 1\.2\.3\+clarke
+    .*/Tests/RunCMake/find_package-CPS/cps/sample/1\.1\.0/sample\.cps, version: 1\.1\.0\+asimov
+
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)

+ 10 - 0
Tests/RunCMake/find_package-CPS/MissingVersion2.cmake

@@ -0,0 +1,10 @@
+cmake_minimum_required(VERSION 4.0)
+
+include(Setup.cmake)
+
+set(CMAKE_FIND_PACKAGE_SORT_ORDER NAME)
+set(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC)
+
+###############################################################################
+# Test finding a package with no suitable version matches.
+find_package(Sample 1.4.9 REQUIRED)

+ 13 - 0
Tests/RunCMake/find_package-CPS/MultipleVersions.cmake

@@ -0,0 +1,13 @@
+cmake_minimum_required(VERSION 4.0)
+
+include(Setup.cmake)
+
+set(CMAKE_FIND_PACKAGE_SORT_ORDER NAME)
+set(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC)
+
+###############################################################################
+# Test finding a package with multiple suitable versions (permissive match).
+find_package(Sample 1.1.0 REQUIRED)
+if(NOT Sample_VERSION STREQUAL "1.2.3+clarke")
+  message(SEND_ERROR "Sample wrong version ${Sample_VERSION} !")
+endif()

+ 16 - 0
Tests/RunCMake/find_package-CPS/RunCMakeTest.cmake

@@ -8,6 +8,22 @@ set(RunCMake_TEST_OPTIONS
   "-DCMAKE_EXPERIMENTAL_FIND_CPS_PACKAGES:STRING=e82e467b-f997-4464-8ace-b00808fff261"
   )
 
+# Version-matching tests
+run_cmake(ExactVersion)
+run_cmake(CompatVersion)
+run_cmake(MultipleVersions)
+run_cmake(VersionLimit1)
+run_cmake(VersionLimit2)
+run_cmake(TransitiveVersion)
+run_cmake(CustomVersion)
+
+# Version-matching failure tests
+run_cmake(MissingVersion1)
+run_cmake(MissingVersion2)
+run_cmake(VersionLimit3)
+run_cmake(VersionLimit4)
+
+# Component-related failure tests
 run_cmake(MissingTransitiveDependency)
 run_cmake(MissingComponent)
 run_cmake(MissingComponentDependency)

+ 13 - 0
Tests/RunCMake/find_package-CPS/TransitiveVersion.cmake

@@ -0,0 +1,13 @@
+cmake_minimum_required(VERSION 4.0)
+
+include(Setup.cmake)
+
+set(CMAKE_FIND_PACKAGE_SORT_ORDER NAME)
+set(CMAKE_FIND_PACKAGE_SORT_DIRECTION ASC)
+
+###############################################################################
+# Test finding a package with a versioned dependency.
+find_package(TransitiveVersion REQUIRED)
+if(NOT Sample_VERSION STREQUAL "1.2.3+clarke")
+  message(SEND_ERROR "Sample wrong version ${Sample_VERSION} !")
+endif()

+ 14 - 0
Tests/RunCMake/find_package-CPS/VersionLimit1.cmake

@@ -0,0 +1,14 @@
+cmake_minimum_required(VERSION 4.0)
+
+include(Setup.cmake)
+
+set(CMAKE_FIND_PACKAGE_SORT_ORDER NAME)
+set(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC)
+
+###############################################################################
+# Test finding a package with multiple suitable versions when a version limit
+# is present but not restricting the matches.
+find_package(Sample 1.1.0...1.2.5 REQUIRED)
+if(NOT Sample_VERSION STREQUAL "1.2.3+clarke")
+  message(SEND_ERROR "Sample wrong version ${Sample_VERSION} !")
+endif()

+ 14 - 0
Tests/RunCMake/find_package-CPS/VersionLimit2.cmake

@@ -0,0 +1,14 @@
+cmake_minimum_required(VERSION 4.0)
+
+include(Setup.cmake)
+
+set(CMAKE_FIND_PACKAGE_SORT_ORDER NAME)
+set(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC)
+
+###############################################################################
+# Test finding a package with multiple suitable versions when a version limit
+# is present and restricting the matches.
+find_package(Sample 1.1.0...<1.2.0 REQUIRED)
+if(NOT Sample_VERSION STREQUAL "1.1.0+asimov")
+  message(SEND_ERROR "Sample wrong version ${Sample_VERSION} !")
+endif()

+ 1 - 0
Tests/RunCMake/find_package-CPS/VersionLimit3-result.txt

@@ -0,0 +1 @@
+1

+ 13 - 0
Tests/RunCMake/find_package-CPS/VersionLimit3-stderr.txt

@@ -0,0 +1,13 @@
+CMake Error at VersionLimit3\.cmake:[0-9]+ \(find_package\):
+  Could not find a configuration file for package "Sample" that is compatible
+  with requested version range "1\.2\.0\.\.\.1\.2\.2"\.
+
+  The following configuration files were considered but not accepted:
+
+    .*/Tests/RunCMake/find_package-CPS/cps/sample/1\.5\.0/sample\.cps, version: 1\.5\.0\+niven
+    .*/Tests/RunCMake/find_package-CPS/cps/sample/1\.4\.2/sample\.cps, version: 1\.4\.2\+adams
+    .*/Tests/RunCMake/find_package-CPS/cps/sample/1\.2\.3/sample\.cps, version: 1\.2\.3\+clarke
+    .*/Tests/RunCMake/find_package-CPS/cps/sample/1\.1\.0/sample\.cps, version: 1\.1\.0\+asimov
+
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)

+ 11 - 0
Tests/RunCMake/find_package-CPS/VersionLimit3.cmake

@@ -0,0 +1,11 @@
+cmake_minimum_required(VERSION 4.0)
+
+include(Setup.cmake)
+
+set(CMAKE_FIND_PACKAGE_SORT_ORDER NAME)
+set(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC)
+
+###############################################################################
+# Test finding a package with multiple suitable versions when a version limit
+# is excluding a match.
+find_package(Sample 1.2.0...1.2.2 REQUIRED)

+ 1 - 0
Tests/RunCMake/find_package-CPS/VersionLimit4-result.txt

@@ -0,0 +1 @@
+1

+ 13 - 0
Tests/RunCMake/find_package-CPS/VersionLimit4-stderr.txt

@@ -0,0 +1,13 @@
+CMake Error at VersionLimit4\.cmake:[0-9]+ \(find_package\):
+  Could not find a configuration file for package "Sample" that is compatible
+  with requested version range "1\.2\.0\.\.\.<1\.2\.3"\.
+
+  The following configuration files were considered but not accepted:
+
+    .*/Tests/RunCMake/find_package-CPS/cps/sample/1\.5\.0/sample\.cps, version: 1\.5\.0\+niven
+    .*/Tests/RunCMake/find_package-CPS/cps/sample/1\.4\.2/sample\.cps, version: 1\.4\.2\+adams
+    .*/Tests/RunCMake/find_package-CPS/cps/sample/1\.2\.3/sample\.cps, version: 1\.2\.3\+clarke
+    .*/Tests/RunCMake/find_package-CPS/cps/sample/1\.1\.0/sample\.cps, version: 1\.1\.0\+asimov
+
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)

+ 11 - 0
Tests/RunCMake/find_package-CPS/VersionLimit4.cmake

@@ -0,0 +1,11 @@
+cmake_minimum_required(VERSION 4.0)
+
+include(Setup.cmake)
+
+set(CMAKE_FIND_PACKAGE_SORT_ORDER NAME)
+set(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC)
+
+###############################################################################
+# Test finding a package with multiple suitable versions when a version limit
+# is excluding a match.
+find_package(Sample 1.2.0...<1.2.3 REQUIRED)

+ 8 - 0
Tests/RunCMake/find_package-CPS/cps/CustomVersion/37/CustomVersion.cps

@@ -0,0 +1,8 @@
+{
+  "cps_version": "0.13",
+  "name": "CustomVersion",
+  "version": "37",
+  "version_schema": "custom",
+  "cps_path": "@prefix@/cps/CustomVersion/37",
+  "components": {}
+}

+ 8 - 0
Tests/RunCMake/find_package-CPS/cps/CustomVersion/42/CustomVersion.cps

@@ -0,0 +1,8 @@
+{
+  "cps_version": "0.13",
+  "name": "CustomVersion",
+  "version": "42",
+  "version_schema": "custom",
+  "cps_path": "@prefix@/cps/CustomVersion/42",
+  "components": {}
+}

+ 8 - 0
Tests/RunCMake/find_package-CPS/cps/CustomVersion/55/CustomVersion.cps

@@ -0,0 +1,8 @@
+{
+  "cps_version": "0.13",
+  "name": "CustomVersion",
+  "version": "55",
+  "version_schema": "custom",
+  "cps_path": "@prefix@/cps/CustomVersion/55",
+  "components": {}
+}

+ 1 - 0
Tests/RunCMake/find_package-CPS/cps/componenttest.cps

@@ -1,6 +1,7 @@
 {
   "cps_version": "0.13",
   "name": "ComponentTest",
+  "version": "1.0",
   "cps_path": "@prefix@/cps",
   "components": {}
 }

+ 8 - 0
Tests/RunCMake/find_package-CPS/cps/sample/1.1.0/sample.cps

@@ -0,0 +1,8 @@
+{
+  "cps_version": "0.13",
+  "name": "Sample",
+  "version": "1.1.0+asimov",
+  "compat_version": "1.0.0",
+  "cps_path": "@prefix@/cps/sample/1.1.0",
+  "components": {}
+}

+ 8 - 0
Tests/RunCMake/find_package-CPS/cps/sample/1.2.3/sample.cps

@@ -0,0 +1,8 @@
+{
+  "cps_version": "0.13",
+  "name": "Sample",
+  "version": "1.2.3+clarke",
+  "compat_version": "1.0.0",
+  "cps_path": "@prefix@/cps/sample/1.2.3",
+  "components": {}
+}

+ 8 - 0
Tests/RunCMake/find_package-CPS/cps/sample/1.4.2/sample.cps

@@ -0,0 +1,8 @@
+{
+  "cps_version": "0.13",
+  "name": "Sample",
+  "version": "1.4.2+adams",
+  "compat_version": "1.3.0",
+  "cps_path": "@prefix@/cps/sample/1.4.2",
+  "components": {}
+}

+ 7 - 0
Tests/RunCMake/find_package-CPS/cps/sample/1.5.0/sample.cps

@@ -0,0 +1,7 @@
+{
+  "cps_version": "0.13",
+  "name": "Sample",
+  "version": "1.5.0+niven",
+  "cps_path": "@prefix@/cps/sample/1.5.0",
+  "components": {}
+}

+ 11 - 0
Tests/RunCMake/find_package-CPS/cps/transitiveversion.cps

@@ -0,0 +1,11 @@
+{
+  "cps_version": "0.13",
+  "name": "TransitiveVersion",
+  "cps_path": "@prefix@/cps",
+  "requires": {
+    "Sample": {
+      "version": "1.2.0"
+    }
+  },
+  "components": {}
+}