scripts/unit-test: Add meson support

Tested:
    Converted sdeventplus to meson as a demo to make sure this works as
    expected.

Change-Id: I5a518cf6e7d574468c4a6529780ec1d91e9bf7df
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/scripts/unit-test.py b/scripts/unit-test.py
index befc801..587488d 100755
--- a/scripts/unit-test.py
+++ b/scripts/unit-test.py
@@ -17,6 +17,7 @@
 import re
 import sets
 import subprocess
+import shutil
 import platform
 
 
@@ -292,6 +293,32 @@
 
     return found_deps
 
+def get_meson_deps(pkgdir):
+    """
+    Parse the given 'meson.build' file for package dependencies and return
+    a list of the dependencies found. If the package is not meson compatible
+    it is just ignored.
+
+    Parameter descriptions:
+    pkgdir              Directory where package source is located
+    """
+    meson_build = os.path.join(pkgdir, 'meson.build')
+    if not os.path.exists(meson_build):
+        return []
+
+    found_deps = []
+    for root, dirs, files in os.walk(pkgdir):
+        if 'meson.build' not in files:
+            continue
+        with open(os.path.join(root, 'meson.build'), 'rt') as f:
+            build_contents = f.read()
+        for match in re.finditer(r"dependency\('([^']*)'.*?\)\n", build_contents):
+            maybe_dep = DEPENDENCIES['PKG_CHECK_MODULES'].get(match.group(1))
+            if maybe_dep is not None:
+                found_deps.append(maybe_dep)
+
+    return found_deps
+
 make_parallel = [
     'make',
     # Run enough jobs to saturate all the cpus
@@ -312,6 +339,27 @@
     """
     return '--' + ('enable' if enabled else 'disable') + '-' + flag
 
+def mesonFeature(val):
+    """
+    Returns the meson flag which signifies the value
+
+    True is enabled which requires the feature.
+    False is disabled which disables the feature.
+    None is auto which autodetects the feature.
+
+    Parameters:
+    val                 The value being converted
+    """
+    if val is True:
+        return "enabled"
+    elif val is False:
+        return "disabled"
+    elif val is None:
+        return "auto"
+    else:
+        raise Exception("Bad meson feature value")
+
+
 def build_and_install(pkg, build_for_testing=False):
     """
     Builds and installs the package in the environment. Optionally
@@ -328,23 +376,42 @@
     check_call_cmd(pkgdir, 'sudo', '-n', '--', 'ldconfig')
 
     # Build & install this package
-    conf_flags = [
-        enFlag('silent-rules', False),
-        enFlag('examples', build_for_testing),
-        enFlag('tests', build_for_testing),
-        enFlag('code-coverage', build_for_testing),
-        enFlag('valgrind', build_for_testing),
-    ]
-    # Add any necessary configure flags for package
-    if CONFIGURE_FLAGS.get(pkg) is not None:
-        conf_flags.extend(CONFIGURE_FLAGS.get(pkg))
-    for bootstrap in ['bootstrap.sh', 'bootstrap', 'autogen.sh']:
-        if os.path.exists(bootstrap):
-            check_call_cmd(pkgdir, './' + bootstrap)
-            break
-    check_call_cmd(pkgdir, './configure', *conf_flags)
-    check_call_cmd(pkgdir, *make_parallel)
-    check_call_cmd(pkgdir, 'sudo', '-n', '--', *(make_parallel + [ 'install' ]))
+    # Always try using meson first
+    if os.path.exists('meson.build'):
+        meson_flags = [
+            '-Db_colorout=never',
+            '-Dtests=' + mesonFeature(build_for_testing),
+            '-Dexamples=' + str(build_for_testing).lower(),
+            '-Db_coverage=' + str(build_for_testing).lower(),
+        ]
+        if MESON_FLAGS.get(pkg) is not None:
+            meson_flags.extend(MESON_FLAGS.get(pkg))
+        try:
+            check_call_cmd(pkgdir, 'meson', 'setup', '--reconfigure', 'build', *meson_flags)
+        except:
+            shutil.rmtree('build')
+            check_call_cmd(pkgdir, 'meson', 'setup', 'build', *meson_flags)
+        check_call_cmd(pkgdir, 'ninja', '-C', 'build')
+        check_call_cmd(pkgdir, 'sudo', '-n', '--', 'ninja', '-C', 'build', 'install')
+    # Assume we are autoconf otherwise
+    else:
+        conf_flags = [
+            enFlag('silent-rules', False),
+            enFlag('examples', build_for_testing),
+            enFlag('tests', build_for_testing),
+            enFlag('code-coverage', build_for_testing),
+            enFlag('valgrind', build_for_testing),
+        ]
+        # Add any necessary configure flags for package
+        if CONFIGURE_FLAGS.get(pkg) is not None:
+            conf_flags.extend(CONFIGURE_FLAGS.get(pkg))
+        for bootstrap in ['bootstrap.sh', 'bootstrap', 'autogen.sh']:
+            if os.path.exists(bootstrap):
+                check_call_cmd(pkgdir, './' + bootstrap)
+                break
+        check_call_cmd(pkgdir, './configure', *conf_flags)
+        check_call_cmd(pkgdir, *make_parallel)
+        check_call_cmd(pkgdir, 'sudo', '-n', '--', *(make_parallel + [ 'install' ]))
 
 def build_dep_tree(pkg, pkgdir, dep_added, head, dep_tree=None):
     """
@@ -369,6 +436,7 @@
     # Read out pkg dependencies
     pkg_deps = []
     pkg_deps += get_autoconf_deps(pkgdir)
+    pkg_deps += get_meson_deps(pkgdir)
 
     for dep in sets.Set(pkg_deps):
         if dep in cache:
@@ -507,6 +575,10 @@
          'YAML_DIR=/usr/local/share/phosphor-dbus-yaml/yaml']
     }
 
+    # MESON_FLAGS = [GIT REPO]:[MESON FLAGS]
+    MESON_FLAGS = {
+    }
+
     # DEPENDENCIES = [MACRO]:[library/header]:[GIT REPO]
     DEPENDENCIES = {
         'AC_CHECK_LIB': {'mapper': 'phosphor-objmgr'},
@@ -570,8 +642,9 @@
     CODE_SCAN_DIR = WORKSPACE + "/" + UNIT_TEST_PKG
     check_call_cmd(WORKSPACE, "./format-code.sh", CODE_SCAN_DIR)
 
-    # Automake
-    if os.path.isfile(CODE_SCAN_DIR + "/configure.ac"):
+    # Automake and meson
+    if (os.path.isfile(CODE_SCAN_DIR + "/configure.ac") or
+        os.path.isfile(CODE_SCAN_DIR + '/meson.build')):
         prev_umask = os.umask(000)
         # Determine dependencies and add them
         dep_added = dict()
@@ -598,9 +671,14 @@
         os.chdir(top_dir)
         # Run package unit tests
         build_and_install(UNIT_TEST_PKG, True)
-        run_unit_tests(top_dir)
-        maybe_run_valgrind(top_dir)
-        maybe_run_coverage(top_dir)
+        if os.path.isfile(CODE_SCAN_DIR + '/meson.build'):
+            check_call_cmd(top_dir, 'meson', 'test', '-C', 'build')
+            check_call_cmd(top_dir, 'ninja', '-C', 'build', 'coverage-html')
+            check_call_cmd(top_dir, 'meson', 'test', '-C', 'build', '--wrap', 'valgrind')
+        else:
+            run_unit_tests(top_dir)
+            maybe_run_valgrind(top_dir)
+            maybe_run_coverage(top_dir)
         run_cppcheck(top_dir)
 
         os.umask(prev_umask)