Repository: conan-io/conan

Issue:
Meson build helper: add test() and install() methods
Hello,
I created recently a recipe using [Meson](https://docs.conan.io/en/latest/reference/build_helpers/meson.html) and I discovered that the methods `test()` and `install()` are not available alike [CMake helper](https://docs.conan.io/en/latest/reference/build_helpers/cmake.html).
What would you think about adding them to Meson build helper ?

- [x] I've read the [CONTRIBUTING guide](https://raw.githubusercontent.com/conan-io/conan/develop/.github/CONTRIBUTING.md).
- [x] I've specified the Conan version, operating system version and any tool that can be relevant.




Discussion:

Resolving patch:
diff --git a/conans/client/build/meson.py b/conans/client/build/meson.py
--- a/conans/client/build/meson.py
+++ b/conans/client/build/meson.py
@@ -1,10 +1,11 @@
 import os
+import subprocess
 
-from conans.client import join_arguments, defs_to_string
+from conans.client import defs_to_string, join_arguments, tools
 from conans.errors import ConanException
 from conans.client.tools.oss import args_to_string
-from conans.util.files import mkdir, get_abs_path
-from conans.client import tools
+from conans.util.files import mkdir, get_abs_path, decode_text
+from conans.model.version import Version
 
 
 class Meson(object):
@@ -19,17 +20,52 @@ def __init__(self, conanfile, backend=None, build_type=None):
         self._conanfile = conanfile
         self._settings = conanfile.settings
 
-        self._os = self._settings.get_safe("os")
-        self._compiler = self._settings.get_safe("compiler")
-        self._compiler_version = self._settings.get_safe("compiler.version")
-        self._build_type = self._settings.get_safe("build_type")
+        self._os = self._ss("os")
+        self._compiler = self._ss("compiler")
+        self._compiler_version = self._ss("compiler.version")
+        self._build_type = self._ss("build_type")
 
         self.backend = backend or "ninja"  # Other backends are poorly supported, not default other.
+
+        self.options = dict()
+        self.options['prefix'] = self._conanfile.package_folder
+
+        # C++ standard
+        cppstd = self._ss("cppstd")
+        cppstd_conan2meson = {
+            None: 'none',
+            '98': 'c++03', 'gnu98': 'gnu++03',
+            '11': 'c++11', 'gnu11': 'gnu++11',
+            '14': 'c++14', 'gnu14': 'gnu++14',
+            '17': 'c++17', 'gnu17': 'gnu++17',
+            '20': 'c++1z', 'gnu20': 'gnu++1z'
+        }
+        self.options['cpp_std'] = cppstd_conan2meson[cppstd]
+
+        # shared
+        shared = self._so("shared")
+        self.options['default-library'] = "shared" if shared is None or shared else "static"
+
+        # fpic
+        if self._os and "Windows" not in self._os:
+            fpic = self._so("fPIC")
+            if fpic is not None:
+                shared = self._so("shared")
+                self.options['b_staticpic'] = "true" if (fpic or shared) else "false"
+
         self.build_dir = None
         if build_type and build_type != self._build_type:
             # Call the setter to warn and update the definitions if needed
             self.build_type = build_type
 
+    def _ss(self, setname):
+        """safe setting"""
+        return self._conanfile.settings.get_safe(setname)
+
+    def _so(self, setname):
+        """safe option"""
+        return self._conanfile.options.get_safe(setname)
+
     @property
     def build_type(self):
         return self._build_type
@@ -67,6 +103,10 @@ def _get_dirs(self, source_folder, build_folder, source_dir, build_dir, cache_bu
 
         return source_ret, build_ret
 
+    @property
+    def flags(self):
+        return defs_to_string(self.options)
+
     def configure(self, args=None, defs=None, source_dir=None, build_dir=None,
                   pkg_config_paths=None, cache_build_folder=None,
                   build_folder=None, source_folder=None):
@@ -75,6 +115,9 @@ def configure(self, args=None, defs=None, source_dir=None, build_dir=None,
         args = args or []
         defs = defs or {}
 
+        # overwrite default values with user's inputs
+        self.options.update(defs)
+
         source_dir, self.build_dir = self._get_dirs(source_folder, build_folder,
                                                     source_dir, build_dir,
                                                     cache_build_folder)
@@ -95,8 +138,8 @@ def configure(self, args=None, defs=None, source_dir=None, build_dir=None,
         build_type = "--buildtype=%s" % bt
         arg_list = join_arguments([
             "--backend=%s" % self.backend,
+            self.flags,
             args_to_string(args),
-            defs_to_string(defs),
             build_type
         ])
         command = 'meson "%s" "%s" %s' % (source_dir, self.build_dir, arg_list)
@@ -126,3 +169,29 @@ def build(self, args=None, build_dir=None, targets=None):
         command = "ninja %s" % arg_list
         command = self._append_vs_if_needed(command)
         self._conanfile.run(command)
+
+    def install(self, args=None, build_dir=None):
+        if not self._conanfile.should_install:
+            return
+        mkdir(self._conanfile.package_folder)
+        if not self.options.get('prefix'):
+            raise ConanException("'prefix' not defined for 'meson.install()'\n"
+                                 "Make sure 'package_folder' is defined")
+        self.build(args=args, build_dir=build_dir, targets=["install"])
+
+    def test(self, args=None, build_dir=None, targets=None):
+        if not self._conanfile.should_test:
+            return
+        if not targets:
+            targets = ["test"]
+        self.build(args=args, build_dir=build_dir, targets=targets)
+
+    @staticmethod
+    def get_version():
+        try:
+            out, _ = subprocess.Popen(["meson", "--version"], stdout=subprocess.PIPE).communicate()
+            version_line = decode_text(out).split('\n', 1)[0]
+            version_str = version_line.rsplit(' ', 1)[-1]
+            return Version(version_str)
+        except Exception as e:
+            raise ConanException("Error retrieving Meson version: '{}'".format(e))
\ No newline at end of file


Test patch:
diff --git a/conans/test/build_helpers/meson_test.py b/conans/test/build_helpers/meson_test.py
--- a/conans/test/build_helpers/meson_test.py
+++ b/conans/test/build_helpers/meson_test.py
@@ -2,8 +2,10 @@
 import shutil
 import unittest
 
+from conans.client import defs_to_string
 from conans.client.build.meson import Meson
 from conans.client.conf import default_settings_yml
+from conans.client.tools import args_to_string
 from conans.errors import ConanException
 from conans.model.settings import Settings
 from conans.test.build_helpers.cmake_test import ConanFileMock
@@ -18,16 +20,27 @@ def setUp(self):
     def tearDown(self):
         shutil.rmtree(self.tempdir)
 
+    def _check_commands(self, cmd_ref, cmd_test):
+        cmd_ref_splitted = cmd_ref.split(' ')
+        cmd_test_splitted = cmd_test.split(' ')
+        self.assertEquals(cmd_ref_splitted[:3], cmd_test_splitted[:3])
+        self.assertEquals(set(cmd_ref_splitted[3:]), set(cmd_test_splitted[3:]))
+
     def partial_build_test(self):
         conan_file = ConanFileMock()
         conan_file.settings = Settings()
         conan_file.should_configure = False
         conan_file.should_build = False
+        conan_file.package_folder = os.path.join(self.tempdir, "my_cache_package_folder")
         meson = Meson(conan_file)
         meson.configure()
         self.assertIsNone(conan_file.command)
         meson.build()
         self.assertIsNone(conan_file.command)
+        meson.test()
+        self.assertIsNone(conan_file.command)
+        meson.install()
+        self.assertIsNone(conan_file.command)
 
     def folders_test(self):
         settings = Settings.loads(default_settings_yml)
@@ -36,65 +49,91 @@ def folders_test(self):
         settings.compiler.version = "6.3"
         settings.arch = "x86"
         settings.build_type = "Release"
+        package_folder = os.path.join(self.tempdir, "my_cache_package_folder")
         conan_file = ConanFileMock()
         conan_file.settings = settings
         conan_file.source_folder = os.path.join(self.tempdir, "my_cache_source_folder")
         conan_file.build_folder = os.path.join(self.tempdir, "my_cache_build_folder")
+        conan_file.package_folder = package_folder
         meson = Meson(conan_file)
 
+        defs = {
+            'default-library': 'shared',
+            'prefix': package_folder,
+            'cpp_std': 'none'
+        }
+
         meson.configure(source_dir=os.path.join(self.tempdir, "../subdir"),
                         build_dir=os.path.join(self.tempdir, "build"))
         source_expected = os.path.join(self.tempdir, "../subdir")
         build_expected = os.path.join(self.tempdir, "build")
-        self.assertEquals(conan_file.command, 'meson "%s" "%s" '
-                          '--backend=ninja --buildtype=release' % (source_expected, build_expected))
+        cmd_expected = 'meson "%s" "%s" --backend=ninja %s --buildtype=release' \
+                       % (source_expected, build_expected, defs_to_string(defs))
+        self._check_commands(cmd_expected, conan_file.command)
 
         meson.configure(build_dir=os.path.join(self.tempdir, "build"))
         source_expected = os.path.join(self.tempdir, "my_cache_source_folder")
         build_expected = os.path.join(self.tempdir, "build")
-        self.assertEquals(conan_file.command, 'meson "%s" "%s" --backend=ninja '
-                                              '--buildtype=release' % (source_expected,
-                                                                       build_expected))
+        cmd_expected = 'meson "%s" "%s" --backend=ninja %s --buildtype=release' \
+                       % (source_expected, build_expected, defs_to_string(defs))
+        self._check_commands(cmd_expected, conan_file.command)
 
         meson.configure()
         source_expected = os.path.join(self.tempdir, "my_cache_source_folder")
         build_expected = os.path.join(self.tempdir, "my_cache_build_folder")
-        self.assertEquals(conan_file.command,
-                          'meson "%s" "%s" --backend=ninja --buildtype=release'
-                          % (source_expected, build_expected))
+        cmd_expected = 'meson "%s" "%s" --backend=ninja %s --buildtype=release' \
+                       % (source_expected, build_expected, defs_to_string(defs))
+        self._check_commands(cmd_expected, conan_file.command)
 
         meson.configure(source_folder="source", build_folder="build")
         build_expected = os.path.join(self.tempdir, "my_cache_build_folder", "build")
         source_expected = os.path.join(self.tempdir, "my_cache_source_folder", "source")
-        self.assertEquals(conan_file.command,
-                          'meson "%s" "%s" --backend=ninja '
-                          '--buildtype=release' % (source_expected, build_expected))
+        cmd_expected = 'meson "%s" "%s" --backend=ninja %s --buildtype=release' \
+                       % (source_expected, build_expected, defs_to_string(defs))
+        self._check_commands(cmd_expected, conan_file.command)
 
         conan_file.in_local_cache = True
         meson.configure(source_folder="source", build_folder="build",
                         cache_build_folder="rel_only_cache")
         build_expected = os.path.join(self.tempdir, "my_cache_build_folder", "rel_only_cache")
         source_expected = os.path.join(self.tempdir, "my_cache_source_folder", "source")
-        self.assertEquals(conan_file.command,
-                          'meson "%s" "%s" --backend=ninja '
-                          '--buildtype=release' % (source_expected, build_expected))
+        cmd_expected = 'meson "%s" "%s" --backend=ninja %s --buildtype=release' \
+                       % (source_expected, build_expected, defs_to_string(defs))
+        self._check_commands(cmd_expected, conan_file.command)
 
         conan_file.in_local_cache = False
         meson.configure(source_folder="source", build_folder="build",
                         cache_build_folder="rel_only_cache")
         build_expected = os.path.join(self.tempdir, "my_cache_build_folder", "build")
         source_expected = os.path.join(self.tempdir, "my_cache_source_folder", "source")
-        self.assertEquals(conan_file.command,
-                          'meson "%s" "%s" --backend=ninja '
-                          '--buildtype=release' % (source_expected, build_expected))
+        cmd_expected = 'meson "%s" "%s" --backend=ninja %s --buildtype=release' \
+                       % (source_expected, build_expected, defs_to_string(defs))
+        self._check_commands(cmd_expected, conan_file.command)
 
         conan_file.in_local_cache = True
         meson.configure(build_dir="build", cache_build_folder="rel_only_cache")
         build_expected = os.path.join(self.tempdir, "my_cache_build_folder", "rel_only_cache")
         source_expected = os.path.join(self.tempdir, "my_cache_source_folder")
-        self.assertEquals(conan_file.command,
-                          'meson "%s" "%s" --backend=ninja '
-                          '--buildtype=release' % (source_expected, build_expected))
+        cmd_expected = 'meson "%s" "%s" --backend=ninja %s --buildtype=release' \
+                       % (source_expected, build_expected, defs_to_string(defs))
+        self._check_commands(cmd_expected, conan_file.command)
+
+        args = ['--werror', '--warnlevel 3']
+        defs['default-library'] = 'static'
+        meson.configure(source_folder="source", build_folder="build", args=args,
+                        defs={'default-library': 'static'})
+        build_expected = os.path.join(self.tempdir, "my_cache_build_folder", "build")
+        source_expected = os.path.join(self.tempdir, "my_cache_source_folder", "source")
+        cmd_expected = 'meson "%s" "%s" --backend=ninja %s %s --buildtype=release' \
+                       % (source_expected, build_expected, args_to_string(args), defs_to_string(defs))
+        self._check_commands(cmd_expected, conan_file.command)
+
         # Raise mixing
         with self.assertRaisesRegexp(ConanException, "Use 'build_folder'/'source_folder'"):
             meson.configure(source_folder="source", build_dir="build")
+
+        meson.test()
+        self.assertEquals("ninja -C \"%s\" %s" % (build_expected, args_to_string(["test"])), conan_file.command)
+
+        meson.install()
+        self.assertEquals("ninja -C \"%s\" %s" % (build_expected, args_to_string(["install"])), conan_file.command)