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 336789 810bc87c256db92f9fc06ad7de62efc1fb11c98a
parent 336788 7aa9b5719a80742a9bc7c8ca0aa93fe31c2becb0
child 336790 b465c1ff97c4cebb6f2ec13c323b993e51578c1d
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersglandium
bugs1269513
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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()