Bug 1266343 - Extend the ConfigureTestSandbox to hook subprocess.Popen. r?chmanchester draft
authorMike Hommey <mh+mozilla@glandium.org>
Wed, 20 Apr 2016 13:44:03 +0900
changeset 354783 463a6f38907284ecaf004aaffb052bef804aa103
parent 354782 acef3f2e19a9c03671017e2d84b574e4b2d4619e
child 354784 93183146ae097d21ace6b1b1392bfa4bb60213c6
push id16148
push userbmo:mh+mozilla@glandium.org
push dateThu, 21 Apr 2016 12:30:02 +0000
reviewerschmanchester
bugs1266343
milestone48.0a1
Bug 1266343 - Extend the ConfigureTestSandbox to hook subprocess.Popen. r?chmanchester
python/mozbuild/mozbuild/test/configure/common.py
python/mozbuild/mozbuild/test/configure/test_checks_configure.py
--- a/python/mozbuild/mozbuild/test/configure/common.py
+++ b/python/mozbuild/mozbuild/test/configure/common.py
@@ -1,38 +1,82 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, print_function, unicode_literals
 
+import errno
 import os
+import subprocess
 
 from mozbuild.configure import ConfigureSandbox
 from mozbuild.util import ReadOnlyNamespace
 from mozpack import path as mozpath
 
 from which import WhichError
 
+from buildconfig import (
+    topobjdir,
+    topsrcdir,
+)
+
 
 class ConfigureTestVFS(object):
     def __init__(self, paths):
         self._paths = set(mozpath.abspath(p) for p in paths)
 
     def exists(self, path):
-        return mozpath.abspath(path) in self._paths
+        path = mozpath.abspath(path)
+        if path in self._paths:
+            return True
+        if mozpath.basedir(path, [topsrcdir, topobjdir]):
+            return os.path.exists(path)
+        return False
 
     def isfile(self, path):
-        return mozpath.abspath(path) in self._paths
+        path = mozpath.abspath(path)
+        if path in self._paths:
+            return True
+        if mozpath.basedir(path, [topsrcdir, topobjdir]):
+            return os.path.isfile(path)
+        return False
 
 
 class ConfigureTestSandbox(ConfigureSandbox):
+    '''Wrapper around the ConfigureSandbox for testing purposes.
+
+    Its arguments are the same as ConfigureSandbox, except for the additional
+    `paths` argument, which is a dict where the keys are file paths and the
+    values are either None or a function that will be called when the sandbox
+    calls an implemented function from subprocess with the key as command.
+    When the command is CONFIG_SHELL, the function for the path of the script
+    that follows will be called.
+
+    The API for those functions is:
+        retcode, stdout, stderr = func(stdin, args)
+
+    This class is only meant to implement the minimal things to make
+    moz.configure testing possible. As such, it takes shortcuts.
+    '''
     def __init__(self, paths, config, environ, *args, **kwargs):
         self._search_path = environ.get('PATH', '').split(os.pathsep)
 
+        self._subprocess_paths = {
+            mozpath.abspath(k): v for k, v in paths.iteritems() if v
+        }
+
+        paths = paths.keys()
+
+        environ = dict(environ)
+        if 'CONFIG_SHELL' not in environ:
+            environ['CONFIG_SHELL'] = mozpath.abspath('/bin/sh')
+            self._subprocess_paths[environ['CONFIG_SHELL']] = self.shell
+            paths.append(environ['CONFIG_SHELL'])
+
         vfs = ConfigureTestVFS(paths)
 
         self.OS = ReadOnlyNamespace(path=ReadOnlyNamespace(**{
             k: v if k not in ('exists', 'isfile')
             else getattr(vfs, k)
             for k, v in ConfigureSandbox.OS.path.__dict__.iteritems()
         }))
 
@@ -44,16 +88,59 @@ class ConfigureTestSandbox(ConfigureSand
             return self.which
 
         if what == 'which':
             return ReadOnlyNamespace(
                 which=self.which,
                 WhichError=WhichError,
             )
 
+        if what == 'subprocess.Popen':
+            return self.Popen
+
+        if what == 'subprocess':
+            return ReadOnlyNamespace(
+                CalledProcessError=subprocess.CalledProcessError,
+                check_output=self.check_output,
+                PIPE=subprocess.PIPE,
+                Popen=self.Popen,
+            )
+
         return super(ConfigureTestSandbox, self)._get_one_import(what)
 
     def which(self, command):
         for parent in self._search_path:
             path = mozpath.join(parent, command)
             if self.OS.path.exists(path):
                 return path
         raise WhichError()
+
+    def Popen(self, args, stdin=None, stdout=None, stderr=None, **kargs):
+        try:
+            program = self.which(args[0])
+        except WhichError:
+            raise OSError(errno.ENOENT, 'File not found')
+
+        func = self._subprocess_paths.get(program)
+        retcode, stdout, stderr = func(stdin, args[1:])
+
+        class Process(object):
+            def communicate(self, stdin=None):
+                return stdout, stderr
+
+            def wait(self):
+                return retcode
+
+        return Process()
+
+    def check_output(self, args):
+        proc = self.Popen(args)
+        stdout, stderr = proc.communicate()
+        retcode = proc.wait()
+        if retcode:
+            raise subprocess.CalledProcessError(retcode, args, stdout)
+        return stdout
+
+    def shell(self, stdin, args):
+        script = mozpath.abspath(args[0])
+        if script in self._subprocess_paths:
+            return self._subprocess_paths[script](stdin, args[1:])
+        return 127, '', 'File not found'
--- a/python/mozbuild/mozbuild/test/configure/test_checks_configure.py
+++ b/python/mozbuild/mozbuild/test/configure/test_checks_configure.py
@@ -126,21 +126,21 @@ class TestChecksConfigure(unittest.TestC
     KNOWN_A = mozpath.abspath('/usr/bin/known-a')
     KNOWN_B = mozpath.abspath('/usr/local/bin/known-b')
     KNOWN_C = mozpath.abspath('/home/user/bin/known c')
 
     def get_result(self, command='', args=[], environ={},
                    prog='/bin/configure'):
         config = {}
         out = StringIO()
-        paths = (
-            self.KNOWN_A,
-            self.KNOWN_B,
-            self.KNOWN_C,
-        )
+        paths = {
+            self.KNOWN_A: None,
+            self.KNOWN_B: None,
+            self.KNOWN_C: None,
+        }
         environ = dict(environ)
         environ['PATH'] = os.pathsep.join(os.path.dirname(p) for p in paths)
         sandbox = ConfigureTestSandbox(paths, config, environ, [prog] + args,
                                        out, out)
         base_dir = os.path.join(topsrcdir, 'build', 'moz.configure')
         sandbox.include_file(os.path.join(base_dir, 'util.configure'))
         sandbox.include_file(os.path.join(base_dir, 'checks.configure'))