[meta-mingw,v2,1/5] Add SDK test case framework

Submitted by Joshua Watt on Nov. 19, 2018, 9:17 p.m. | Patch ID: 156406

Details

Message ID 20181119211752.18963-2-JPEWhacker@gmail.com
State New
Headers show

Commit Message

Joshua Watt Nov. 19, 2018, 9:17 p.m.
Adds the framework for testing SDKs that ties into the oeqa test
framework. This allows commands like:

 $ bitbake -c testsdk ...

to be run for MinGW SDKs.

The test framework currently executes all tests under Wine in lieu of
having access to actual Windows machines.

[YOCTO #13020]

Signed-off-by: Joshua Watt <JPEWhacker@gmail.com>
---
 conf/machine-sdk/i686-mingw32.conf          |  1 +
 conf/machine-sdk/include/mingw32-common.inc |  7 ++
 conf/machine-sdk/x86_64-mingw32.conf        |  1 +
 lib/oeqa/sdkmingw/__init__.py               |  0
 lib/oeqa/sdkmingw/case.py                   | 87 +++++++++++++++++++++
 lib/oeqa/sdkmingw/cases/__init__.py         |  0
 lib/oeqa/sdkmingw/context.py                | 69 ++++++++++++++++
 lib/oeqa/sdkmingw/testsdk.py                | 42 ++++++++++
 8 files changed, 207 insertions(+)
 create mode 100644 lib/oeqa/sdkmingw/__init__.py
 create mode 100644 lib/oeqa/sdkmingw/case.py
 create mode 100644 lib/oeqa/sdkmingw/cases/__init__.py
 create mode 100644 lib/oeqa/sdkmingw/context.py
 create mode 100644 lib/oeqa/sdkmingw/testsdk.py

Patch hide | download patch | download mbox

diff --git a/conf/machine-sdk/i686-mingw32.conf b/conf/machine-sdk/i686-mingw32.conf
index 5090168..fef48b5 100644
--- a/conf/machine-sdk/i686-mingw32.conf
+++ b/conf/machine-sdk/i686-mingw32.conf
@@ -1,3 +1,4 @@ 
 SDK_ARCH = "i686"
+TESTSDK_WINEARCH = "win32"
 
 require conf/machine-sdk/include/mingw32-common.inc
diff --git a/conf/machine-sdk/include/mingw32-common.inc b/conf/machine-sdk/include/mingw32-common.inc
index 733d092..71e8d45 100644
--- a/conf/machine-sdk/include/mingw32-common.inc
+++ b/conf/machine-sdk/include/mingw32-common.inc
@@ -26,6 +26,9 @@  SDKPKGSUFFIX = "nativesdk-mingw32"
 
 MACHINEOVERRIDES .= ":sdkmingw32"
 
+TESTSDK_CLASS_NAME = "oeqa.sdkmingw.testsdk.TestSDKMinGW"
+TESTSDKEXT_CLASS_NAME = ""
+
 WINDRES_mingw32 = "${HOST_PREFIX}windres --include-dir=${STAGING_INCDIR}"
 RC_mingw32 = "${WINDRES}"
 
@@ -39,3 +42,7 @@  DISABLE_STATIC_mingw32 = ""
 
 # disable security flags
 GCCPIE_mingw32 = ""
+
+# wine and wineserver are required to test MinGW SDKs
+HOSTTOOLS += "${@'wine wineserver' if (bb.utils.contains_any('IMAGE_CLASSES', 'testsdk-mingw', True, False, d) or any(x in (d.getVar("BBINCLUDED") or "") for x in ["testsdk-mingw.bbclass"])) else ''}"
+
diff --git a/conf/machine-sdk/x86_64-mingw32.conf b/conf/machine-sdk/x86_64-mingw32.conf
index fc53822..188debc 100644
--- a/conf/machine-sdk/x86_64-mingw32.conf
+++ b/conf/machine-sdk/x86_64-mingw32.conf
@@ -1,3 +1,4 @@ 
 SDK_ARCH = "x86_64"
+TESTSDK_WINEARCH = "win64"
 
 require conf/machine-sdk/include/mingw32-common.inc
diff --git a/lib/oeqa/sdkmingw/__init__.py b/lib/oeqa/sdkmingw/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lib/oeqa/sdkmingw/case.py b/lib/oeqa/sdkmingw/case.py
new file mode 100644
index 0000000..169c143
--- /dev/null
+++ b/lib/oeqa/sdkmingw/case.py
@@ -0,0 +1,87 @@ 
+# Copyright 2018 by Garmin Ltd. or its subsidiaries
+# Released under the MIT license (see COPYING.MIT)
+
+import subprocess
+import os
+import tempfile
+import shutil
+import bb
+
+from oeqa.core.utils.path import remove_safe
+from oeqa.sdk.case import OESDKTestCase
+
+from oeqa.utils.subprocesstweak import errors_have_output
+errors_have_output()
+
+class OESDKMinGWTestCase(OESDKTestCase):
+    def setUp(self):
+        super().setUp()
+
+        self.test_dir = tempfile.mkdtemp(prefix=self.__class__.__name__ + '-', dir=self.tc.sdk_dir)
+        self.addCleanup(lambda: bb.utils.prunedir(self.test_dir))
+
+        self.wine_test_dir = self.tc.wine_path(self.test_dir)
+
+    def copyTestFile(self, src, dest=None):
+        dest_path = dest or os.path.join(self.test_dir, os.path.basename(src))
+        shutil.copyfile(src, dest_path)
+        self.addCleanup(lambda: remove_safe(dest_path))
+
+    def fetch(self, url, destdir=None, dl_dir=None, archive=None):
+        if not destdir:
+            destdir = self.test_dir
+
+        if not dl_dir:
+            dl_dir = self.td.get('DL_DIR', None)
+
+        if not archive:
+            from urllib.parse import urlparse
+            archive = os.path.basename(urlparse(url).path)
+
+        if dl_dir:
+            tarball = os.path.join(dl_dir, archive)
+            if os.path.exists(tarball):
+                return tarball
+
+        tarball = os.path.join(destdir, archive)
+        subprocess.check_output(["wget", "-O", tarball, url])
+        return tarball
+
+
+    def _run(self, cmd):
+        import shlex
+
+        def strip_quotes(s):
+            if s[0] == '"' and s[-1] == '"':
+                return s[1:-1]
+            return s
+
+        command = ['wine', 'cmd', '/c', self.tc.wine_sdk_env, '>', 'NUL', '&&', 'cd', self.wine_test_dir, '&&']
+
+        # Perform some massaging so that commands can be written naturally in
+        # test cases. shlex.split() in Non-posix mode gets us most of the way
+        # there, but it leaves the quotes around a quoted argument, so we
+        # remove them manually.
+        command.extend(strip_quotes(s) for s in shlex.split(cmd, posix=False))
+
+        return subprocess.check_output(command, env=self.tc.get_wine_env(),
+                stderr=subprocess.STDOUT, universal_newlines=True)
+
+    def assertIsTargetElf(self, path):
+        import oe.qa
+        import oe.elf
+
+        elf = oe.qa.ELFFile(path)
+        elf.open()
+
+        if not getattr(self, 'target_os', None):
+            output = self._run("echo %OECORE_TARGET_OS%:%OECORE_TARGET_ARCH%")
+            self.target_os, self.target_arch = output.strip().split(":")
+
+        machine_data = oe.elf.machine_dict(None)[self.target_os][self.target_arch]
+        (machine, osabi, abiversion, endian, bits) = machine_data
+
+        self.assertEqual(machine, elf.machine())
+        self.assertEqual(bits, elf.abiSize())
+        self.assertEqual(endian, elf.isLittleEndian())
+
diff --git a/lib/oeqa/sdkmingw/cases/__init__.py b/lib/oeqa/sdkmingw/cases/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lib/oeqa/sdkmingw/context.py b/lib/oeqa/sdkmingw/context.py
new file mode 100644
index 0000000..edabcbd
--- /dev/null
+++ b/lib/oeqa/sdkmingw/context.py
@@ -0,0 +1,69 @@ 
+# Copyright (C) 2018 by Garmin Ltd. or its subsidiaries
+# Released under the MIT license (see COPYING.MIT)
+import os
+import subprocess
+
+from oeqa.sdk.context import OESDKTestContext, OESDKTestContextExecutor
+
+from oeqa.utils.subprocesstweak import errors_have_output
+errors_have_output()
+
+class OESDKMinGWTestContext(OESDKTestContext):
+    sdk_files_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "files")
+
+    def __init__(self, td=None, logger=None, sdk_dir=None, sdk_env=None, wine_prefix=None,
+            wine_arch=None, target_pkg_manifest=None, host_pkg_manifest=None):
+        super(OESDKMinGWTestContext, self).__init__(td, logger, sdk_dir, sdk_env, target_pkg_manifest, host_pkg_manifest)
+        self.wine_prefix = wine_prefix
+        self.wine_arch = wine_arch
+        self.wine_sdk_dir = self.wine_path(sdk_dir)
+        self.wine_sdk_env = self.wine_path(sdk_env)
+
+    def get_wine_env(self):
+        env = os.environ.copy()
+
+        # Turn off all Wine debug logging so it doesn't interfere with command output
+        env['WINEDEBUG'] = '-all'
+
+        env['WINEPREFIX'] = self.wine_prefix
+        env['WINEARCH'] = self.wine_arch
+
+        # Convenience variables to make test cases easier to write
+        env['SDK_DIR'] = getattr(self, 'wine_sdk_dir', '')
+
+        return env
+
+    def wine_path(self, p):
+        """
+        Converts a host POSIX path to a path in Wine
+        """
+        o = subprocess.check_output(['wine', 'winepath', '-w', p], env=self.get_wine_env())
+        return o.decode('utf-8').rstrip()
+
+
+class OESDKMinGWTestContextExecutor(OESDKTestContextExecutor):
+    _context_class = OESDKMinGWTestContext
+
+    name = 'sdk-mingw'
+    help = 'MinGW sdk test component'
+    description = 'executes MinGW sdk tests'
+
+    default_cases = [os.path.join(os.path.abspath(os.path.dirname(__file__)),
+            'cases')]
+
+    def register_commands(self, logger, subparsers):
+        super(OESDKMinGWTestContextExecutor, self).register_commands(logger, subparsers)
+
+        wine_group  = self.parser.add_argument_group('wine options')
+        wine_group.add_argument('--wine-prefix', action='store',
+            help='Wine prefix (bottle). Default is $SDK_DIR/.wine')
+        wine_group.add_argument('--wine-arch', action='store', choices=('win32', 'win64'),
+            default='win64', help='Wine architecture. Defaults to %(default)s')
+
+    def _process_args(self, logger, args):
+        super(OESDKMinGWTestContextExecutor, self)._process_args(logger, args)
+        self.tc_kwargs['init']['wine_prefix'] = args.wine_prefix or os.path.join(args.sdk_dir, '.wine')
+        self.tc_kwargs['init']['wine_arch'] = args.wine_arch
+
+_executor_class = OESDKMinGWTestContextExecutor
+
diff --git a/lib/oeqa/sdkmingw/testsdk.py b/lib/oeqa/sdkmingw/testsdk.py
new file mode 100644
index 0000000..85fe3c6
--- /dev/null
+++ b/lib/oeqa/sdkmingw/testsdk.py
@@ -0,0 +1,42 @@ 
+# Copyright 2018 by Garmin Ltd. or its subsidiaries
+# Released under the MIT license (see COPYING.MIT)
+
+from oeqa.sdk.testsdk import TestSDK
+from oeqa.sdkmingw.context import OESDKMinGWTestContext, OESDKMinGWTestContextExecutor
+
+class TestSDKMinGW(TestSDK):
+    context_executor_class = OESDKMinGWTestContextExecutor
+    context_class = OESDKMinGWTestContext
+
+    def get_tcname(self, d):
+        """
+        Get the name of the SDK file
+        """
+        return d.expand("${SDK_DEPLOY}/${TOOLCHAIN_OUTPUTNAME}.tar.xz")
+
+    def extract_sdk(self, tcname, sdk_dir, d):
+        """
+        Extract the SDK to the specified location
+        """
+        import subprocess
+
+        try:
+            # TODO: It would be nice to try and extract the SDK in Wine to make
+            # sure it is well formed
+            subprocess.check_output(['tar', '-xf', tcname, '-C', sdk_dir])
+        except subprocess.CalledProcessError as e:
+            bb.fatal("Couldn't install the SDK:\n%s" % e.output.decode("utf-8"))
+
+    def setup_context(self, d):
+        """
+        Return a dictionary of additional arguments that should be passed to
+        the context_class on construction
+        """
+        wine_prefix = d.getVar('TESTSDK_WINEPREFIX') or d.expand('${WORKDIR}/testimage-wine/')
+        bb.utils.remove(wine_prefix, True)
+
+        return {
+            'wine_prefix': wine_prefix,
+            'wine_arch': d.getVar('TESTSDK_WINEARCH') or 'win64'
+            }
+