diff options
Diffstat (limited to 'python/mach/mach/test')
-rw-r--r-- | python/mach/mach/test/__init__.py | 0 | ||||
-rw-r--r-- | python/mach/mach/test/common.py | 45 | ||||
-rw-r--r-- | python/mach/mach/test/providers/__init__.py | 0 | ||||
-rw-r--r-- | python/mach/mach/test/providers/basic.py | 23 | ||||
-rw-r--r-- | python/mach/mach/test/providers/conditions.py | 53 | ||||
-rw-r--r-- | python/mach/mach/test/providers/conditions_invalid.py | 16 | ||||
-rw-r--r-- | python/mach/mach/test/providers/throw.py | 29 | ||||
-rw-r--r-- | python/mach/mach/test/providers/throw2.py | 13 | ||||
-rw-r--r-- | python/mach/mach/test/test_conditions.py | 83 | ||||
-rw-r--r-- | python/mach/mach/test/test_config.py | 297 | ||||
-rw-r--r-- | python/mach/mach/test/test_dispatcher.py | 61 | ||||
-rw-r--r-- | python/mach/mach/test/test_entry_point.py | 61 | ||||
-rw-r--r-- | python/mach/mach/test/test_error_output.py | 39 | ||||
-rw-r--r-- | python/mach/mach/test/test_logger.py | 47 |
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() |