summaryrefslogtreecommitdiffstats
path: root/python/mach/mach/test
diff options
context:
space:
mode:
Diffstat (limited to 'python/mach/mach/test')
-rw-r--r--python/mach/mach/test/__init__.py0
-rw-r--r--python/mach/mach/test/common.py45
-rw-r--r--python/mach/mach/test/providers/__init__.py0
-rw-r--r--python/mach/mach/test/providers/basic.py23
-rw-r--r--python/mach/mach/test/providers/conditions.py53
-rw-r--r--python/mach/mach/test/providers/conditions_invalid.py16
-rw-r--r--python/mach/mach/test/providers/throw.py29
-rw-r--r--python/mach/mach/test/providers/throw2.py13
-rw-r--r--python/mach/mach/test/test_conditions.py83
-rw-r--r--python/mach/mach/test/test_config.py297
-rw-r--r--python/mach/mach/test/test_dispatcher.py61
-rw-r--r--python/mach/mach/test/test_entry_point.py61
-rw-r--r--python/mach/mach/test/test_error_output.py39
-rw-r--r--python/mach/mach/test/test_logger.py47
14 files changed, 767 insertions, 0 deletions
diff --git a/python/mach/mach/test/__init__.py b/python/mach/mach/test/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/python/mach/mach/test/__init__.py
diff --git a/python/mach/mach/test/common.py b/python/mach/mach/test/common.py
new file mode 100644
index 000000000..f68ff5095
--- /dev/null
+++ b/python/mach/mach/test/common.py
@@ -0,0 +1,45 @@
+# 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 unicode_literals
+
+from StringIO import StringIO
+import os
+import unittest
+
+from mach.main import Mach
+from mach.base import CommandContext
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+class TestBase(unittest.TestCase):
+ provider_dir = os.path.join(here, 'providers')
+
+ def get_mach(self, provider_file=None, entry_point=None, context_handler=None):
+ m = Mach(os.getcwd())
+ m.define_category('testing', 'Mach unittest', 'Testing for mach core', 10)
+ m.populate_context_handler = context_handler
+
+ if provider_file:
+ m.load_commands_from_file(os.path.join(self.provider_dir, provider_file))
+
+ if entry_point:
+ m.load_commands_from_entry_point(entry_point)
+
+ return m
+
+ def _run_mach(self, argv, *args, **kwargs):
+ m = self.get_mach(*args, **kwargs)
+
+ stdout = StringIO()
+ stderr = StringIO()
+ stdout.encoding = 'UTF-8'
+ stderr.encoding = 'UTF-8'
+
+ try:
+ result = m.run(argv, stdout=stdout, stderr=stderr)
+ except SystemExit:
+ result = None
+
+ return (result, stdout.getvalue(), stderr.getvalue())
diff --git a/python/mach/mach/test/providers/__init__.py b/python/mach/mach/test/providers/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/python/mach/mach/test/providers/__init__.py
diff --git a/python/mach/mach/test/providers/basic.py b/python/mach/mach/test/providers/basic.py
new file mode 100644
index 000000000..f2e64e6d6
--- /dev/null
+++ b/python/mach/mach/test/providers/basic.py
@@ -0,0 +1,23 @@
+# 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 unicode_literals
+
+from mach.decorators import (
+ CommandArgument,
+ CommandProvider,
+ Command,
+)
+
+
+@CommandProvider
+class ConditionsProvider(object):
+ @Command('cmd_foo', category='testing')
+ def run_foo(self):
+ pass
+
+ @Command('cmd_bar', category='testing')
+ @CommandArgument('--baz', action="store_true",
+ help='Run with baz')
+ def run_bar(self, baz=None):
+ pass
diff --git a/python/mach/mach/test/providers/conditions.py b/python/mach/mach/test/providers/conditions.py
new file mode 100644
index 000000000..a95429752
--- /dev/null
+++ b/python/mach/mach/test/providers/conditions.py
@@ -0,0 +1,53 @@
+# 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 unicode_literals
+
+from mach.decorators import (
+ CommandProvider,
+ Command,
+)
+
+def is_foo(cls):
+ """Foo must be true"""
+ return cls.foo
+
+def is_bar(cls):
+ """Bar must be true"""
+ return cls.bar
+
+@CommandProvider
+class ConditionsProvider(object):
+ foo = True
+ bar = False
+
+ @Command('cmd_foo', category='testing', conditions=[is_foo])
+ def run_foo(self):
+ pass
+
+ @Command('cmd_bar', category='testing', conditions=[is_bar])
+ def run_bar(self):
+ pass
+
+ @Command('cmd_foobar', category='testing', conditions=[is_foo, is_bar])
+ def run_foobar(self):
+ pass
+
+@CommandProvider
+class ConditionsContextProvider(object):
+ def __init__(self, context):
+ self.foo = context.foo
+ self.bar = context.bar
+
+ @Command('cmd_foo_ctx', category='testing', conditions=[is_foo])
+ def run_foo(self):
+ pass
+
+ @Command('cmd_bar_ctx', category='testing', conditions=[is_bar])
+ def run_bar(self):
+ pass
+
+ @Command('cmd_foobar_ctx', category='testing', conditions=[is_foo, is_bar])
+ def run_foobar(self):
+ pass
diff --git a/python/mach/mach/test/providers/conditions_invalid.py b/python/mach/mach/test/providers/conditions_invalid.py
new file mode 100644
index 000000000..22284d4dc
--- /dev/null
+++ b/python/mach/mach/test/providers/conditions_invalid.py
@@ -0,0 +1,16 @@
+# 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 unicode_literals
+
+from mach.decorators import (
+ CommandProvider,
+ Command,
+)
+
+@CommandProvider
+class ConditionsProvider(object):
+ @Command('cmd_foo', category='testing', conditions=["invalid"])
+ def run_foo(self):
+ pass
diff --git a/python/mach/mach/test/providers/throw.py b/python/mach/mach/test/providers/throw.py
new file mode 100644
index 000000000..06bee01ee
--- /dev/null
+++ b/python/mach/mach/test/providers/throw.py
@@ -0,0 +1,29 @@
+# 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 unicode_literals
+
+import time
+
+from mach.decorators import (
+ CommandArgument,
+ CommandProvider,
+ Command,
+)
+
+from mach.test.providers import throw2
+
+
+@CommandProvider
+class TestCommandProvider(object):
+ @Command('throw', category='testing')
+ @CommandArgument('--message', '-m', default='General Error')
+ def throw(self, message):
+ raise Exception(message)
+
+ @Command('throw_deep', category='testing')
+ @CommandArgument('--message', '-m', default='General Error')
+ def throw_deep(self, message):
+ throw2.throw_deep(message)
+
diff --git a/python/mach/mach/test/providers/throw2.py b/python/mach/mach/test/providers/throw2.py
new file mode 100644
index 000000000..af0a23fcf
--- /dev/null
+++ b/python/mach/mach/test/providers/throw2.py
@@ -0,0 +1,13 @@
+# 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/.
+
+# This file exists to trigger the differences in mach error reporting between
+# exceptions that occur in mach command modules themselves and in the things
+# they call.
+
+def throw_deep(message):
+ return throw_real(message)
+
+def throw_real(message):
+ raise Exception(message)
diff --git a/python/mach/mach/test/test_conditions.py b/python/mach/mach/test/test_conditions.py
new file mode 100644
index 000000000..20080687e
--- /dev/null
+++ b/python/mach/mach/test/test_conditions.py
@@ -0,0 +1,83 @@
+# 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 unicode_literals
+
+import os
+
+from mach.base import MachError
+from mach.main import Mach
+from mach.registrar import Registrar
+from mach.test.common import TestBase
+
+from mozunit import main
+
+
+def _populate_context(context, key=None):
+ if key is None:
+ return
+ if key == 'foo':
+ return True
+ if key == 'bar':
+ return False
+ raise AttributeError(key)
+
+class TestConditions(TestBase):
+ """Tests for conditionally filtering commands."""
+
+ def _run_mach(self, args, context_handler=None):
+ return TestBase._run_mach(self, args, 'conditions.py',
+ context_handler=context_handler)
+
+
+ def test_conditions_pass(self):
+ """Test that a command which passes its conditions is runnable."""
+
+ self.assertEquals((0, '', ''), self._run_mach(['cmd_foo']))
+ self.assertEquals((0, '', ''), self._run_mach(['cmd_foo_ctx'], _populate_context))
+
+ def test_invalid_context_message(self):
+ """Test that commands which do not pass all their conditions
+ print the proper failure message."""
+
+ def is_bar():
+ """Bar must be true"""
+ fail_conditions = [is_bar]
+
+ for name in ('cmd_bar', 'cmd_foobar'):
+ result, stdout, stderr = self._run_mach([name])
+ self.assertEquals(1, result)
+
+ fail_msg = Registrar._condition_failed_message(name, fail_conditions)
+ self.assertEquals(fail_msg.rstrip(), stdout.rstrip())
+
+ for name in ('cmd_bar_ctx', 'cmd_foobar_ctx'):
+ result, stdout, stderr = self._run_mach([name], _populate_context)
+ self.assertEquals(1, result)
+
+ fail_msg = Registrar._condition_failed_message(name, fail_conditions)
+ self.assertEquals(fail_msg.rstrip(), stdout.rstrip())
+
+ def test_invalid_type(self):
+ """Test that a condition which is not callable raises an exception."""
+
+ m = Mach(os.getcwd())
+ m.define_category('testing', 'Mach unittest', 'Testing for mach core', 10)
+ self.assertRaises(MachError, m.load_commands_from_file,
+ os.path.join(self.provider_dir, 'conditions_invalid.py'))
+
+ def test_help_message(self):
+ """Test that commands that are not runnable do not show up in help."""
+
+ result, stdout, stderr = self._run_mach(['help'], _populate_context)
+ self.assertIn('cmd_foo', stdout)
+ self.assertNotIn('cmd_bar', stdout)
+ self.assertNotIn('cmd_foobar', stdout)
+ self.assertIn('cmd_foo_ctx', stdout)
+ self.assertNotIn('cmd_bar_ctx', stdout)
+ self.assertNotIn('cmd_foobar_ctx', stdout)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/python/mach/mach/test/test_config.py b/python/mach/mach/test/test_config.py
new file mode 100644
index 000000000..d48dff67b
--- /dev/null
+++ b/python/mach/mach/test/test_config.py
@@ -0,0 +1,297 @@
+# 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 unicode_literals
+
+import sys
+import unittest
+
+from mozfile.mozfile import NamedTemporaryFile
+
+from mach.config import (
+ BooleanType,
+ ConfigException,
+ ConfigSettings,
+ IntegerType,
+ PathType,
+ PositiveIntegerType,
+ StringType,
+)
+from mach.decorators import SettingsProvider
+
+from mozunit import main
+
+
+if sys.version_info[0] == 3:
+ str_type = str
+else:
+ str_type = basestring
+
+CONFIG1 = r"""
+[foo]
+
+bar = bar_value
+baz = /baz/foo.c
+"""
+
+CONFIG2 = r"""
+[foo]
+
+bar = value2
+"""
+
+@SettingsProvider
+class Provider1(object):
+ config_settings = [
+ ('foo.bar', StringType),
+ ('foo.baz', PathType),
+ ]
+
+
+@SettingsProvider
+class ProviderDuplicate(object):
+ config_settings = [
+ ('dupesect.foo', StringType),
+ ('dupesect.foo', StringType),
+ ]
+
+
+@SettingsProvider
+class Provider2(object):
+ config_settings = [
+ ('a.string', StringType),
+ ('a.boolean', BooleanType),
+ ('a.pos_int', PositiveIntegerType),
+ ('a.int', IntegerType),
+ ('a.path', PathType),
+ ]
+
+
+@SettingsProvider
+class Provider3(object):
+ @classmethod
+ def config_settings(cls):
+ return [
+ ('a.string', 'string'),
+ ('a.boolean', 'boolean'),
+ ('a.pos_int', 'pos_int'),
+ ('a.int', 'int'),
+ ('a.path', 'path'),
+ ]
+
+
+@SettingsProvider
+class Provider4(object):
+ config_settings = [
+ ('foo.abc', StringType, 'a', {'choices': set('abc')}),
+ ('foo.xyz', StringType, 'w', {'choices': set('xyz')}),
+ ]
+
+
+@SettingsProvider
+class Provider5(object):
+ config_settings = [
+ ('foo.*', 'string'),
+ ('foo.bar', 'string'),
+ ]
+
+
+class TestConfigSettings(unittest.TestCase):
+ def test_empty(self):
+ s = ConfigSettings()
+
+ self.assertEqual(len(s), 0)
+ self.assertNotIn('foo', s)
+
+ def test_duplicate_option(self):
+ s = ConfigSettings()
+
+ with self.assertRaises(ConfigException):
+ s.register_provider(ProviderDuplicate)
+
+ def test_simple(self):
+ s = ConfigSettings()
+ s.register_provider(Provider1)
+
+ self.assertEqual(len(s), 1)
+ self.assertIn('foo', s)
+
+ foo = s['foo']
+ foo = s.foo
+
+ self.assertEqual(len(foo), 0)
+ self.assertEqual(len(foo._settings), 2)
+
+ self.assertIn('bar', foo._settings)
+ self.assertIn('baz', foo._settings)
+
+ self.assertNotIn('bar', foo)
+ foo['bar'] = 'value1'
+ self.assertIn('bar', foo)
+
+ self.assertEqual(foo['bar'], 'value1')
+ self.assertEqual(foo.bar, 'value1')
+
+ def test_assignment_validation(self):
+ s = ConfigSettings()
+ s.register_provider(Provider2)
+
+ a = s.a
+
+ # Assigning an undeclared setting raises.
+ with self.assertRaises(AttributeError):
+ a.undefined = True
+
+ with self.assertRaises(KeyError):
+ a['undefined'] = True
+
+ # Basic type validation.
+ a.string = 'foo'
+ a.string = 'foo'
+
+ with self.assertRaises(TypeError):
+ a.string = False
+
+ a.boolean = True
+ a.boolean = False
+
+ with self.assertRaises(TypeError):
+ a.boolean = 'foo'
+
+ a.pos_int = 5
+ a.pos_int = 0
+
+ with self.assertRaises(ValueError):
+ a.pos_int = -1
+
+ with self.assertRaises(TypeError):
+ a.pos_int = 'foo'
+
+ a.int = 5
+ a.int = 0
+ a.int = -5
+
+ with self.assertRaises(TypeError):
+ a.int = 1.24
+
+ with self.assertRaises(TypeError):
+ a.int = 'foo'
+
+ a.path = '/home/gps'
+ a.path = 'foo.c'
+ a.path = 'foo/bar'
+ a.path = './foo'
+
+ def retrieval_type_helper(self, provider):
+ s = ConfigSettings()
+ s.register_provider(provider)
+
+ a = s.a
+
+ a.string = 'foo'
+ a.boolean = True
+ a.pos_int = 12
+ a.int = -4
+ a.path = './foo/bar'
+
+ self.assertIsInstance(a.string, str_type)
+ self.assertIsInstance(a.boolean, bool)
+ self.assertIsInstance(a.pos_int, int)
+ self.assertIsInstance(a.int, int)
+ self.assertIsInstance(a.path, str_type)
+
+ def test_retrieval_type(self):
+ self.retrieval_type_helper(Provider2)
+ self.retrieval_type_helper(Provider3)
+
+ def test_choices_validation(self):
+ s = ConfigSettings()
+ s.register_provider(Provider4)
+
+ foo = s.foo
+ foo.abc
+ with self.assertRaises(ValueError):
+ foo.xyz
+
+ with self.assertRaises(ValueError):
+ foo.abc = 'e'
+
+ foo.abc = 'b'
+ foo.xyz = 'y'
+
+ def test_wildcard_options(self):
+ s = ConfigSettings()
+ s.register_provider(Provider5)
+
+ foo = s.foo
+
+ self.assertIn('*', foo._settings)
+ self.assertNotIn('*', foo)
+
+ foo.baz = 'value1'
+ foo.bar = 'value2'
+
+ self.assertIn('baz', foo)
+ self.assertEqual(foo.baz, 'value1')
+
+ self.assertIn('bar', foo)
+ self.assertEqual(foo.bar, 'value2')
+
+ def test_file_reading_single(self):
+ temp = NamedTemporaryFile(mode='wt')
+ temp.write(CONFIG1)
+ temp.flush()
+
+ s = ConfigSettings()
+ s.register_provider(Provider1)
+
+ s.load_file(temp.name)
+
+ self.assertEqual(s.foo.bar, 'bar_value')
+
+ def test_file_reading_multiple(self):
+ """Loading multiple files has proper overwrite behavior."""
+ temp1 = NamedTemporaryFile(mode='wt')
+ temp1.write(CONFIG1)
+ temp1.flush()
+
+ temp2 = NamedTemporaryFile(mode='wt')
+ temp2.write(CONFIG2)
+ temp2.flush()
+
+ s = ConfigSettings()
+ s.register_provider(Provider1)
+
+ s.load_files([temp1.name, temp2.name])
+
+ self.assertEqual(s.foo.bar, 'value2')
+
+ def test_file_reading_missing(self):
+ """Missing files should silently be ignored."""
+
+ s = ConfigSettings()
+
+ s.load_file('/tmp/foo.ini')
+
+ def test_file_writing(self):
+ s = ConfigSettings()
+ s.register_provider(Provider2)
+
+ s.a.string = 'foo'
+ s.a.boolean = False
+
+ temp = NamedTemporaryFile('wt')
+ s.write(temp)
+ temp.flush()
+
+ s2 = ConfigSettings()
+ s2.register_provider(Provider2)
+
+ s2.load_file(temp.name)
+
+ self.assertEqual(s.a.string, s2.a.string)
+ self.assertEqual(s.a.boolean, s2.a.boolean)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/python/mach/mach/test/test_dispatcher.py b/python/mach/mach/test/test_dispatcher.py
new file mode 100644
index 000000000..3d689a4a2
--- /dev/null
+++ b/python/mach/mach/test/test_dispatcher.py
@@ -0,0 +1,61 @@
+# 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 unicode_literals
+
+import os
+from cStringIO import StringIO
+
+from mach.base import CommandContext
+from mach.registrar import Registrar
+from mach.test.common import TestBase
+
+from mozunit import main
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+
+class TestDispatcher(TestBase):
+ """Tests dispatch related code"""
+
+ def get_parser(self, config=None):
+ mach = self.get_mach('basic.py')
+
+ for provider in Registrar.settings_providers:
+ mach.settings.register_provider(provider)
+
+ if config:
+ if isinstance(config, basestring):
+ config = StringIO(config)
+ mach.settings.load_fps([config])
+
+ context = CommandContext(settings=mach.settings)
+ return mach.get_argument_parser(context)
+
+ def test_command_aliases(self):
+ config = """
+[alias]
+foo = cmd_foo
+bar = cmd_bar
+baz = cmd_bar --baz
+cmd_bar = cmd_bar --baz
+"""
+ parser = self.get_parser(config=config)
+
+ args = parser.parse_args(['foo'])
+ self.assertEquals(args.command, 'cmd_foo')
+
+ def assert_bar_baz(argv):
+ args = parser.parse_args(argv)
+ self.assertEquals(args.command, 'cmd_bar')
+ self.assertTrue(args.command_args.baz)
+
+ # The following should all result in |cmd_bar --baz|
+ assert_bar_baz(['bar', '--baz'])
+ assert_bar_baz(['baz'])
+ assert_bar_baz(['cmd_bar'])
+
+
+if __name__ == '__main__':
+ main()
diff --git a/python/mach/mach/test/test_entry_point.py b/python/mach/mach/test/test_entry_point.py
new file mode 100644
index 000000000..7aea91e5e
--- /dev/null
+++ b/python/mach/mach/test/test_entry_point.py
@@ -0,0 +1,61 @@
+# 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 unicode_literals
+
+import imp
+import os
+import sys
+
+from mach.base import MachError
+from mach.test.common import TestBase
+from mock import patch
+
+from mozunit import main
+
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+
+class Entry():
+ """Stub replacement for pkg_resources.EntryPoint"""
+ def __init__(self, providers):
+ self.providers = providers
+
+ def load(self):
+ def _providers():
+ return self.providers
+ return _providers
+
+
+class TestEntryPoints(TestBase):
+ """Test integrating with setuptools entry points"""
+ provider_dir = os.path.join(here, 'providers')
+
+ def _run_mach(self):
+ return TestBase._run_mach(self, ['help'], entry_point='mach.providers')
+
+ @patch('pkg_resources.iter_entry_points')
+ def test_load_entry_point_from_directory(self, mock):
+ # Ensure parent module is present otherwise we'll (likely) get
+ # an error due to unknown parent.
+ if b'mach.commands' not in sys.modules:
+ mod = imp.new_module(b'mach.commands')
+ sys.modules[b'mach.commands'] = mod
+
+ mock.return_value = [Entry([self.provider_dir])]
+ # Mach error raised due to conditions_invalid.py
+ with self.assertRaises(MachError):
+ self._run_mach()
+
+ @patch('pkg_resources.iter_entry_points')
+ def test_load_entry_point_from_file(self, mock):
+ mock.return_value = [Entry([os.path.join(self.provider_dir, 'basic.py')])]
+
+ result, stdout, stderr = self._run_mach()
+ self.assertIsNone(result)
+ self.assertIn('cmd_foo', stdout)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/python/mach/mach/test/test_error_output.py b/python/mach/mach/test/test_error_output.py
new file mode 100644
index 000000000..25553f96b
--- /dev/null
+++ b/python/mach/mach/test/test_error_output.py
@@ -0,0 +1,39 @@
+# 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 unicode_literals
+
+from mach.main import (
+ COMMAND_ERROR,
+ MODULE_ERROR
+)
+from mach.test.common import TestBase
+
+from mozunit import main
+
+
+class TestErrorOutput(TestBase):
+
+ def _run_mach(self, args):
+ return TestBase._run_mach(self, args, 'throw.py')
+
+ def test_command_error(self):
+ result, stdout, stderr = self._run_mach(['throw', '--message',
+ 'Command Error'])
+
+ self.assertEqual(result, 1)
+
+ self.assertIn(COMMAND_ERROR, stdout)
+
+ def test_invoked_error(self):
+ result, stdout, stderr = self._run_mach(['throw_deep', '--message',
+ 'Deep stack'])
+
+ self.assertEqual(result, 1)
+
+ self.assertIn(MODULE_ERROR, stdout)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/python/mach/mach/test/test_logger.py b/python/mach/mach/test/test_logger.py
new file mode 100644
index 000000000..05592845e
--- /dev/null
+++ b/python/mach/mach/test/test_logger.py
@@ -0,0 +1,47 @@
+# 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, unicode_literals
+
+import logging
+import time
+import unittest
+
+from mach.logging import StructuredHumanFormatter
+
+from mozunit import main
+
+
+class DummyLogger(logging.Logger):
+ def __init__(self, cb):
+ logging.Logger.__init__(self, 'test')
+
+ self._cb = cb
+
+ def handle(self, record):
+ self._cb(record)
+
+
+class TestStructuredHumanFormatter(unittest.TestCase):
+ def test_non_ascii_logging(self):
+ # Ensures the formatter doesn't choke when non-ASCII characters are
+ # present in printed parameters.
+ formatter = StructuredHumanFormatter(time.time())
+
+ def on_record(record):
+ result = formatter.format(record)
+ relevant = result[9:]
+
+ self.assertEqual(relevant, 'Test: s\xe9curit\xe9')
+
+ logger = DummyLogger(on_record)
+
+ value = 's\xe9curit\xe9'
+
+ logger.log(logging.INFO, 'Test: {utf}',
+ extra={'action': 'action', 'params': {'utf': value}})
+
+
+if __name__ == '__main__':
+ main()