Bug 1269513 - Add a helper for check_output in Python configure. r=glandium
authorChris Manchester <cmanchester@mozilla.com>
Tue, 17 May 2016 14:40:03 -0700
changeset 297801 810bc87c256db92f9fc06ad7de62efc1fb11c98a
parent 297800 7aa9b5719a80742a9bc7c8ca0aa93fe31c2becb0
child 297802 b465c1ff97c4cebb6f2ec13c323b993e51578c1d
push id19274
push userryanvm@gmail.com
push dateWed, 18 May 2016 16:14:35 +0000
treeherderfx-team@4cfa7a2cefa7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersglandium
bugs1269513
milestone49.0a1
Bug 1269513 - Add a helper for check_output in Python configure. r=glandium MozReview-Commit-ID: H3IX5HLyJeu
build/moz.configure/toolchain.configure
build/moz.configure/util.configure
moz.configure
python/mozbuild/mozbuild/test/configure/test_util.py
--- a/build/moz.configure/toolchain.configure
+++ b/build/moz.configure/toolchain.configure
@@ -5,25 +5,22 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # yasm detection
 # ==============================================================
 yasm = check_prog('YASM', ['yasm'], allow_missing=True)
 
 @depends_if(yasm)
 @checking('yasm version')
-@imports('subprocess')
 def yasm_version(yasm):
-    try:
-        version = Version(subprocess.check_output(
-            [yasm, '--version']
-        ).splitlines()[0].split()[1])
-        return version
-    except subprocess.CalledProcessError as e:
-        die('Failed to get yasm version: %s', e.message)
+    version = check_cmd_output(
+        yasm, '--version',
+        onerror=lambda: die('Failed to get yasm version.')
+    ).splitlines()[0].split()[1]
+    return version
 
 # Until we move all the yasm consumers out of old-configure.
 # bug 1257904
 add_old_configure_assignment('_YASM_MAJOR_VERSION',
                              delayed_getattr(yasm_version, 'major'))
 add_old_configure_assignment('_YASM_MINOR_VERSION',
                              delayed_getattr(yasm_version, 'minor'))
 
@@ -169,17 +166,16 @@ set_config('TOOLCHAIN_PREFIX', toolchain
 add_old_configure_assignment('TOOLCHAIN_PREFIX', toolchain_prefix)
 
 
 # Compilers
 # ==============================================================
 @imports('os')
 @imports('subprocess')
 @imports(_from='mozbuild.configure.util', _import='LineIO')
-@imports(_from='mozbuild.shellutil', _import='quote')
 @imports(_from='tempfile', _import='mkstemp')
 def try_preprocess(compiler, language, source):
     suffix = {
         'C': '.c',
         'C++': '.cpp',
     }[language]
 
     fd, path = mkstemp(prefix='conftest.', suffix=suffix)
@@ -187,32 +183,18 @@ def try_preprocess(compiler, language, s
         source = source.encode('ascii', 'replace')
 
         log.debug('Creating `%s` with content:', path)
         with LineIO(lambda l: log.debug('| %s', l)) as out:
             out.write(source)
 
         os.write(fd, source)
         os.close(fd)
-
         cmd = compiler + ['-E', path]
-        log.debug('Executing: `%s`', quote(*cmd))
-        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
-                                stderr=subprocess.PIPE)
-        stdout, stderr = proc.communicate()
-        retcode = proc.wait()
-        if retcode == 0:
-            return stdout
-
-        log.debug('The command returned non-zero exit status %d.', retcode)
-        for out, desc in ((stdout, 'output'), (stderr, 'error output')):
-            if out:
-                log.debug('Its %s was:', desc)
-                with LineIO(lambda l: log.debug('| %s', l)) as o:
-                    o.write(out)
+        return check_cmd_output(*cmd)
     finally:
         os.remove(path)
 
 
 @imports(_from='mozbuild.configure.constants', _import='CompilerType')
 @imports(_from='textwrap', _import='dedent')
 def get_compiler_info(compiler, language):
     '''Returns information about the given `compiler` (command line in the
--- a/build/moz.configure/util.configure
+++ b/build/moz.configure/util.configure
@@ -13,16 +13,47 @@ def die(*args):
 
 @imports(_from='mozbuild.configure', _import='ConfigureError')
 def configure_error(message):
     '''Raise a programming error and terminate configure.
     Primarily for use in moz.configure templates to sanity check
     their inputs from moz.configure usage.'''
     raise ConfigureError(message)
 
+# A wrapper to obtain a process' output that returns the output generated
+# by running the given command if it exits normally, and streams that
+# output to log.debug and calls die or the given error callback if it
+# does not.
+@imports('subprocess')
+@imports('sys')
+@imports(_from='mozbuild.configure.util', _import='LineIO')
+@imports(_from='mozbuild.shellutil', _import='quote')
+def check_cmd_output(*args, **kwargs):
+    onerror = kwargs.pop('onerror', None)
+
+    with log.queue_debug():
+        log.debug('Executing: `%s`', quote(*args))
+        proc = subprocess.Popen(args, stdout=subprocess.PIPE,
+                                stderr=subprocess.PIPE)
+        stdout, stderr = proc.communicate()
+        retcode = proc.wait()
+        if retcode == 0:
+            return stdout
+
+        log.debug('The command returned non-zero exit status %d.',
+                  retcode)
+        for out, desc in ((stdout, 'output'), (stderr, 'error output')):
+            if out:
+                log.debug('Its %s was:', desc)
+                with LineIO(lambda l: log.debug('| %s', l)) as o:
+                    o.write(out)
+        if onerror:
+            return onerror()
+        die('Command `%s` failed with exit status %d.' %
+            (quote(*args), retcode))
 
 @imports('os')
 def is_absolute_or_relative(path):
     if os.altsep and os.altsep in path:
         return True
     return os.sep in path
 
 
--- a/moz.configure
+++ b/moz.configure
@@ -111,22 +111,21 @@ def perl_for_old_configure(value):
     return value
 
 add_old_configure_assignment('PERL', perl_for_old_configure)
 
 @template
 def perl_version_check(min_version):
     @depends(perl)
     @checking('for minimum required perl version >= %s' % min_version)
-    @imports('subprocess')
     def get_perl_version(perl):
-        try:
-            return Version(subprocess.check_output([perl, '-e', 'print $]']))
-        except subprocess.CalledProcessError as e:
-            die('Failed to get perl version: %s', e.message)
+        return Version(check_cmd_output(
+            perl, '-e', 'print $]',
+            onerror=lambda: die('Failed to get perl version.')
+        ))
 
     @depends(get_perl_version)
     def check_perl_version(version):
         if version < min_version:
             die('Perl %s or higher is required.', min_version)
 
     @depends(perl)
     @checking('for full perl installation')
--- a/python/mozbuild/mozbuild/test/configure/test_util.py
+++ b/python/mozbuild/mozbuild/test/configure/test_util.py
@@ -2,28 +2,34 @@
 # 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 logging
 import os
 import tempfile
+import textwrap
 import unittest
 import sys
 
 from StringIO import StringIO
 
 from mozunit import main
+from mozpack import path as mozpath
 
 from mozbuild.configure.util import (
     ConfigureOutputHandler,
     LineIO,
     Version,
 )
+from mozbuild.util import exec_
+
+from buildconfig import topsrcdir
+from common import ConfigureTestSandbox
 
 
 class TestConfigureOutputHandler(unittest.TestCase):
     def test_separation(self):
         out = StringIO()
         err = StringIO()
         name = '%s.test_separation' % self.__class__.__name__
         logger = logging.getLogger(name)
@@ -433,11 +439,90 @@ class TestVersion(unittest.TestCase):
 
     def test_version_badder(self):
         v = Version('1b.2.3')
         self.assertLess(v, '2')
         self.assertEqual(v.major, 1)
         self.assertEqual(v.minor, 0)
         self.assertEqual(v.patch, 0)
 
+class TestCheckCmdOutput(unittest.TestCase):
+
+    def get_result(self, command='', paths=None):
+        paths = paths or {}
+        config = {}
+        out = StringIO()
+        sandbox = ConfigureTestSandbox(paths, config, {}, ['/bin/configure'],
+                                       out, out)
+        sandbox.include_file(mozpath.join(topsrcdir, 'build',
+                             'moz.configure', 'util.configure'))
+        status = 0
+        try:
+            exec_(command, sandbox)
+            sandbox.run()
+        except SystemExit as e:
+            status = e.code
+        return config, out.getvalue(), status
+
+    def test_simple_program(self):
+        def mock_simple_prog(_, args):
+            if len(args) == 1 and args[0] == '--help':
+                return 0, 'simple program help...', ''
+            self.fail("Unexpected arguments to mock_simple_program: %s" %
+                      args)
+        prog_path = mozpath.abspath('/simple/prog')
+        cmd = "log.info(check_cmd_output('%s', '--help'))" % prog_path
+        config, out, status = self.get_result(cmd,
+                                              paths={prog_path: mock_simple_prog})
+        self.assertEqual(config, {})
+        self.assertEqual(status, 0)
+        self.assertEqual(out, 'simple program help...\n')
+
+    def test_failing_program(self):
+        def mock_error_prog(_, args):
+            if len(args) == 1 and args[0] == '--error':
+                return (127, 'simple program output',
+                        'simple program error output')
+            self.fail("Unexpected arguments to mock_error_program: %s" %
+                      args)
+        prog_path = mozpath.abspath('/simple/prog')
+        cmd = "log.info(check_cmd_output('%s', '--error'))" % prog_path
+        config, out, status = self.get_result(cmd,
+                                              paths={prog_path: mock_error_prog})
+        self.assertEqual(config, {})
+        self.assertEqual(status, 1)
+        self.assertEqual(out, textwrap.dedent('''\
+            DEBUG: Executing: `%s --error`
+            DEBUG: The command returned non-zero exit status 127.
+            DEBUG: Its output was:
+            DEBUG: | simple program output
+            DEBUG: Its error output was:
+            DEBUG: | simple program error output
+            ERROR: Command `%s --error` failed with exit status 127.
+        ''' % (prog_path, prog_path)))
+
+    def test_error_callback(self):
+        def mock_error_prog(_, args):
+            if len(args) == 1 and args[0] == '--error':
+                return 127, 'simple program error...', ''
+            self.fail("Unexpected arguments to mock_error_program: %s" %
+                      args)
+
+        prog_path = mozpath.abspath('/simple/prog')
+        cmd = textwrap.dedent('''\
+            check_cmd_output('%s', '--error',
+                             onerror=lambda: die('`prog` produced an error'))
+        ''' % prog_path)
+        config, out, status = self.get_result(cmd,
+                                              paths={prog_path: mock_error_prog})
+        self.assertEqual(config, {})
+        self.assertEqual(status, 1)
+        self.assertEqual(out, textwrap.dedent('''\
+            DEBUG: Executing: `%s --error`
+            DEBUG: The command returned non-zero exit status 127.
+            DEBUG: Its output was:
+            DEBUG: | simple program error...
+            ERROR: `prog` produced an error
+        ''' % prog_path))
+
 
 if __name__ == '__main__':
     main()