Skip to content

Commit

Permalink
#5958 New tool: cppstd minimum version required (#5997)
Browse files Browse the repository at this point in the history
* #5958 New tool: cppstd minimum version required

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Fix bad indentation

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Add gnu cppstd to the tests

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Add gnu extensions

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Mock distro for Linux

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Fix tests on MacOS

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Validate cppstd by str

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Add valid_minimum_cppstd

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* Update conans/client/tools/settings.py

Co-Authored-By: Javier G. Sogo <jgsogo@gmail.com>

* Update conans/client/tools/settings.py

Co-Authored-By: Javier G. Sogo <jgsogo@gmail.com>

* Update conans/tools.py

Co-Authored-By: Javier G. Sogo <jgsogo@gmail.com>

* #5958 Apply @jgsogo suggestions

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Fix broken tests for cppstd

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* Update conans/tools.py

Co-Authored-By: Javier G. Sogo <jgsogo@gmail.com>

* #5958 Remove duplicated import

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Fix C++98 comparison

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Add unit tests for check_min_cppstd

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Fix Conanfile test

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Add unit tests for valid_min_cppstd

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Remove duplicated functional tests

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Test improvements from code review

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 remove unused import

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 cppstd must be a number

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Dont use assert for production code

- Raise ConanExcetion when some input is incorrect

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Validate target OS using ConanFile settings

- The target OS should be considered as important for cpp version

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Use default compiler cppstd when not included in settings

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Fix functional test

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* Review

* Fix bug

* #5958 Detect temporary C++ standard

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Remove temporary C++ standard

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Do not validate OS for GNU extensions

Signed-off-by: Uilian Ries <uilianries@gmail.com>

* #5958 Update GNU extensions description

Signed-off-by: Uilian Ries <uilianries@gmail.com>
  • Loading branch information
uilianries authored and lasote committed Dec 3, 2019
1 parent 87a5c85 commit 410be07
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 0 deletions.
2 changes: 2 additions & 0 deletions conans/client/tools/__init__.py
Expand Up @@ -15,6 +15,8 @@
# noinspection PyUnresolvedReferences
from .scm import *
# noinspection PyUnresolvedReferences
from .settings import *
# noinspection PyUnresolvedReferences
from .system_pm import *
# noinspection PyUnresolvedReferences
from .win import *
71 changes: 71 additions & 0 deletions conans/client/tools/settings.py
@@ -0,0 +1,71 @@
from conans.client.build.cppstd_flags import cppstd_default
from conans.errors import ConanInvalidConfiguration, ConanException


def check_min_cppstd(conanfile, cppstd, gnu_extensions=False):
""" Check if current cppstd fits the minimal version required.
In case the current cppstd doesn't fit the minimal version required
by cppstd, a ConanInvalidConfiguration exception will be raised.
1. If settings.compiler.cppstd, the tool will use settings.compiler.cppstd to compare
2. It not settings.compiler.cppstd, the tool will use compiler to compare (reading the
default from cppstd_default)
3. If not settings.compiler is present (not declared in settings) will raise because it
cannot compare.
:param conanfile: ConanFile instance with cppstd to be compared
:param cppstd: Minimal cppstd version required
:param gnu_extensions: GNU extension is required (e.g gnu17)
"""
if not str(cppstd).isdigit():
raise ConanException("cppstd parameter must be a number")

def less_than(lhs, rhs):
def extract_cpp_version(_cppstd):
return str(_cppstd).replace("gnu", "")

def add_millennium(_cppstd):
return "19%s" % _cppstd if _cppstd == "98" else "20%s" % _cppstd

lhs = add_millennium(extract_cpp_version(lhs))
rhs = add_millennium(extract_cpp_version(rhs))
return lhs < rhs

def check_required_gnu_extension(_cppstd):
if gnu_extensions and "gnu" not in _cppstd:
raise ConanInvalidConfiguration("The cppstd GNU extension is required")

def deduced_cppstd():
cppstd = conanfile.settings.get_safe("compiler.cppstd")
if cppstd:
return cppstd

compiler = conanfile.settings.get_safe("compiler")
compiler_version = conanfile.settings.get_safe("compiler.version")
if not compiler or not compiler_version:
raise ConanException("Could not obtain cppstd because there is no declared "
"compiler in the 'settings' field of the recipe.")
return cppstd_default(compiler, compiler_version)

current_cppstd = deduced_cppstd()
check_required_gnu_extension(current_cppstd)

if less_than(current_cppstd, cppstd):
raise ConanInvalidConfiguration("Current cppstd ({}) is lower than the required C++ "
"standard ({}).".format(current_cppstd, cppstd))


def valid_min_cppstd(conanfile, cppstd, gnu_extensions=False):
""" Validate if current cppstd fits the minimal version required.
:param conanfile: ConanFile instance with cppstd to be compared
:param cppstd: Minimal cppstd version required
:param gnu_extensions: GNU extension is required (e.g gnu17). This option ONLY works on Linux.
:return: True, if current cppstd matches the required cppstd version. Otherwise, False.
"""
try:
check_min_cppstd(conanfile, cppstd, gnu_extensions)
except ConanInvalidConfiguration:
return False
return True
51 changes: 51 additions & 0 deletions conans/test/functional/tools/cppstd_minimum_version_test.py
@@ -0,0 +1,51 @@
import unittest
from parameterized import parameterized
from textwrap import dedent

from conans.test.utils.tools import TestClient


class CppStdMinimumVersionTests(unittest.TestCase):

CONANFILE = dedent("""
import os
from conans import ConanFile
from conans.tools import check_min_cppstd, valid_min_cppstd
class Fake(ConanFile):
name = "fake"
version = "0.1"
settings = "compiler"
def configure(self):
check_min_cppstd(self, "17", False)
self.output.info("valid standard")
assert valid_min_cppstd(self, "17", False)
""")

PROFILE = dedent("""
[settings]
compiler=gcc
compiler.version=9
compiler.libcxx=libstdc++
{}
""")

def setUp(self):
self.client = TestClient()
self.client.save({"conanfile.py": CppStdMinimumVersionTests.CONANFILE})

@parameterized.expand(["17", "gnu17"])
def test_cppstd_from_settings(self, cppstd):
profile = CppStdMinimumVersionTests.PROFILE.replace("{}", "compiler.cppstd=%s" % cppstd)
self.client.save({"myprofile": profile})
self.client.run("create . user/channel -pr myprofile")
self.assertIn("valid standard", self.client.out)

@parameterized.expand(["11", "gnu11"])
def test_invalid_cppstd_from_settings(self, cppstd):
profile = CppStdMinimumVersionTests.PROFILE.replace("{}", "compiler.cppstd=%s" % cppstd)
self.client.save({"myprofile": profile})
self.client.run("create . user/channel -pr myprofile", assert_error=True)
self.assertIn("Invalid configuration: Current cppstd (%s) is lower than the required C++ "
"standard (17)." % cppstd, self.client.out)
158 changes: 158 additions & 0 deletions conans/test/unittests/client/tools/cppstd_required_test.py
@@ -0,0 +1,158 @@
import unittest
from mock import mock
from parameterized import parameterized

from conans.test.utils.conanfile import MockConanfile, MockSettings
from conans.client.tools import OSInfo
from conans.errors import ConanInvalidConfiguration, ConanException

from conans.tools import check_min_cppstd, valid_min_cppstd


class UserInputTests(unittest.TestCase):

def test_check_cppstd_type(self):
""" cppstd must be a number
"""
conanfile = MockConanfile(MockSettings({}))
with self.assertRaises(ConanException) as raises:
check_min_cppstd(conanfile, "gnu17", False)
self.assertEqual("cppstd parameter must be a number", str(raises.exception))


class CheckMinCppStdTests(unittest.TestCase):

def _create_conanfile(self, compiler, version, os, cppstd, libcxx=None):
settings = MockSettings({"arch": "x86_64",
"build_type": "Debug",
"os": os,
"compiler": compiler,
"compiler.version": version,
"compiler.cppstd": cppstd})
if libcxx:
settings.values["compiler.libcxx"] = libcxx
conanfile = MockConanfile(settings)
return conanfile

@parameterized.expand(["98", "11", "14", "17"])
def test_check_min_cppstd_from_settings(self, cppstd):
""" check_min_cppstd must accept cppstd less/equal than cppstd in settings
"""
conanfile = self._create_conanfile("gcc", "9", "Linux", "17", "libstdc++")
check_min_cppstd(conanfile, cppstd, False)

@parameterized.expand(["98", "11", "14"])
def test_check_min_cppstd_from_outdated_settings(self, cppstd):
""" check_min_cppstd must raise when cppstd is greater when supported on settings
"""
conanfile = self._create_conanfile("gcc", "9", "Linux", cppstd, "libstdc++")
with self.assertRaises(ConanInvalidConfiguration) as raises:
check_min_cppstd(conanfile, "17", False)
self.assertEqual("Current cppstd ({}) is lower than the required C++ standard "
"(17).".format(cppstd), str(raises.exception))

@parameterized.expand(["98", "11", "14", "17"])
def test_check_min_cppstd_from_settings_with_extension(self, cppstd):
""" current cppstd in settings must has GNU extension when extensions is enabled
"""
conanfile = self._create_conanfile("gcc", "9", "Linux", "gnu17", "libstdc++")
check_min_cppstd(conanfile, cppstd, True)

conanfile.settings.values["compiler.cppstd"] = "17"
with self.assertRaises(ConanException) as raises:
check_min_cppstd(conanfile, cppstd, True)
self.assertEqual("The cppstd GNU extension is required", str(raises.exception))

def test_check_min_cppstd_unsupported_standard(self):
""" check_min_cppstd must raise when the compiler does not support a standard
"""
conanfile = self._create_conanfile("gcc", "9", "Linux", None, "libstdc++")
with self.assertRaises(ConanInvalidConfiguration) as raises:
check_min_cppstd(conanfile, "42", False)
self.assertEqual("Current cppstd (gnu14) is lower than the required C++ standard (42).",
str(raises.exception))

def test_check_min_cppstd_gnu_compiler_extension(self):
""" Current compiler must support GNU extension on Linux when extensions is required
"""
conanfile = self._create_conanfile("gcc", "9", "Linux", None, "libstdc++")
with mock.patch("platform.system", mock.MagicMock(return_value="Linux")):
with mock.patch.object(OSInfo, '_get_linux_distro_info'):
with mock.patch("conans.client.tools.settings.cppstd_default", return_value="17"):
with self.assertRaises(ConanException) as raises:
check_min_cppstd(conanfile, "17", True)
self.assertEqual("The cppstd GNU extension is required", str(raises.exception))

def test_no_compiler_declared(self):
conanfile = self._create_conanfile(None, None, "Linux", None, "libstdc++")
with self.assertRaises(ConanException) as raises:
check_min_cppstd(conanfile, "14", False)
self.assertEqual("Could not obtain cppstd because there is no declared compiler in the "
"'settings' field of the recipe.", str(raises.exception))


class ValidMinCppstdTests(unittest.TestCase):

def _create_conanfile(self, compiler, version, os, cppstd, libcxx=None):
settings = MockSettings({"arch": "x86_64",
"build_type": "Debug",
"os": os,
"compiler": compiler,
"compiler.version": version,
"compiler.cppstd": cppstd})
if libcxx:
settings.values["compiler.libcxx"] = libcxx
conanfile = MockConanfile(settings)
return conanfile

@parameterized.expand(["98", "11", "14", "17"])
def test_valid_min_cppstd_from_settings(self, cppstd):
""" valid_min_cppstd must accept cppstd less/equal than cppstd in settings
"""
conanfile = self._create_conanfile("gcc", "9", "Linux", "17", "libstdc++")
self.assertTrue(valid_min_cppstd(conanfile, cppstd, False))

@parameterized.expand(["98", "11", "14"])
def test_valid_min_cppstd_from_outdated_settings(self, cppstd):
""" valid_min_cppstd returns False when cppstd is greater when supported on settings
"""
conanfile = self._create_conanfile("gcc", "9", "Linux", cppstd, "libstdc++")
self.assertFalse(valid_min_cppstd(conanfile, "17", False))

@parameterized.expand(["98", "11", "14", "17"])
def test_valid_min_cppstd_from_settings_with_extension(self, cppstd):
""" valid_min_cppstd must returns True when current cppstd in settings has GNU extension and
extensions is enabled
"""
conanfile = self._create_conanfile("gcc", "9", "Linux", "gnu17", "libstdc++")
self.assertTrue(valid_min_cppstd(conanfile, cppstd, True))

conanfile.settings.values["compiler.cppstd"] = "17"
self.assertFalse(valid_min_cppstd(conanfile, cppstd, True))

def test_valid_min_cppstd_unsupported_standard(self):
""" valid_min_cppstd must returns False when the compiler does not support a standard
"""
conanfile = self._create_conanfile("gcc", "9", "Linux", None, "libstdc++")
self.assertFalse(valid_min_cppstd(conanfile, "42", False))

def test_valid_min_cppstd_gnu_compiler_extension(self):
""" valid_min_cppstd must returns False when current compiler does not support GNU extension
on Linux and extensions is required
"""
conanfile = self._create_conanfile("gcc", "9", "Linux", None, "libstdc++")
with mock.patch("platform.system", mock.MagicMock(return_value="Linux")):
with mock.patch.object(OSInfo, '_get_linux_distro_info'):
with mock.patch("conans.client.tools.settings.cppstd_default", return_value="gnu1z"):
self.assertFalse(valid_min_cppstd(conanfile, "20", True))

@parameterized.expand(["98", "11", "14", "17"])
def test_min_cppstd_mingw_windows(self, cppstd):
""" GNU extensions HAS effect on Windows when running a cross-building for Linux
"""
with mock.patch("platform.system", mock.MagicMock(return_value="Windows")):
conanfile = self._create_conanfile("gcc", "9", "Linux", "gnu17", "libstdc++")
self.assertTrue(valid_min_cppstd(conanfile, cppstd, True))

conanfile.settings.values["compiler.cppstd"] = "17"
self.assertFalse(valid_min_cppstd(conanfile, cppstd, True))
1 change: 1 addition & 0 deletions conans/tools.py
Expand Up @@ -19,6 +19,7 @@
from conans.client.tools.env import * # pylint: disable=unused-import
from conans.client.tools.pkg_config import * # pylint: disable=unused-import
from conans.client.tools.scm import * # pylint: disable=unused-import
from conans.client.tools.settings import * # pylint: disable=unused-import
from conans.client.tools.apple import *
from conans.client.tools.android import *
# Tools form conans.util
Expand Down

0 comments on commit 410be07

Please sign in to comment.