1# Copyright 2021 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Tests for pw_cli.plugins.""" 15 16from pathlib import Path 17import sys 18import tempfile 19import types 20from typing import Dict, Iterator 21import unittest 22 23from pw_cli import plugins 24 25 26def _no_docstring() -> int: 27 return 123 28 29 30def _with_docstring() -> int: 31 """This docstring is brought to you courtesy of Pigweed.""" 32 return 456 33 34 35def _create_files(directory: str, files: Dict[str, str]) -> Iterator[Path]: 36 for relative_path, contents in files.items(): 37 path = Path(directory) / relative_path 38 path.parent.mkdir(exist_ok=True, parents=True) 39 path.write_text(contents) 40 yield path 41 42 43class TestPlugin(unittest.TestCase): 44 """Tests for plugins.Plugins.""" 45 def test_target_name_attribute(self) -> None: 46 self.assertEqual( 47 plugins.Plugin('abc', _no_docstring).target_name, 48 f'{__name__}._no_docstring') 49 50 def test_target_name_no_name_attribute(self) -> None: 51 has_no_name = 'no __name__' 52 self.assertFalse(hasattr(has_no_name, '__name__')) 53 54 self.assertEqual( 55 plugins.Plugin('abc', has_no_name).target_name, 56 '<unknown>.no __name__') 57 58 59_TEST_PLUGINS = { 60 'TEST_PLUGINS': (f'test_plugin {__name__} _with_docstring\n' 61 f'other_plugin {__name__} _no_docstring\n'), 62 'nested/in/dirs/TEST_PLUGINS': 63 f'test_plugin {__name__} _no_docstring\n', 64} 65 66 67class TestPluginRegistry(unittest.TestCase): 68 """Tests for plugins.Registry.""" 69 def setUp(self) -> None: 70 self._registry = plugins.Registry( 71 validator=plugins.callable_with_no_args) 72 73 def test_register(self) -> None: 74 self.assertIsNotNone(self._registry.register('a_plugin', 75 _no_docstring)) 76 self.assertIs(self._registry['a_plugin'].target, _no_docstring) 77 78 def test_register_by_name(self) -> None: 79 self.assertIsNotNone( 80 self._registry.register_by_name('plugin_one', __name__, 81 '_no_docstring')) 82 self.assertIsNotNone( 83 self._registry.register('plugin_two', _no_docstring)) 84 85 self.assertIs(self._registry['plugin_one'].target, _no_docstring) 86 self.assertIs(self._registry['plugin_two'].target, _no_docstring) 87 88 def test_register_by_name_undefined_module(self) -> None: 89 with self.assertRaisesRegex(plugins.Error, 'No module named'): 90 self._registry.register_by_name('plugin_two', 'not a module', 91 'something') 92 93 def test_register_by_name_undefined_function(self) -> None: 94 with self.assertRaisesRegex(plugins.Error, 'does not exist'): 95 self._registry.register_by_name('plugin_two', __name__, 'nothing') 96 97 def test_register_fails_validation(self) -> None: 98 with self.assertRaisesRegex(plugins.Error, 'must be callable'): 99 self._registry.register('plugin_two', 'not function') 100 101 def test_run_with_argv_sets_sys_argv(self) -> None: 102 def stash_argv() -> int: 103 self.assertEqual(['pw go', '1', '2'], sys.argv) 104 return 1 105 106 self.assertIsNotNone(self._registry.register('go', stash_argv)) 107 108 original_argv = sys.argv 109 self.assertEqual(self._registry.run_with_argv('go', ['1', '2']), 1) 110 self.assertIs(sys.argv, original_argv) 111 112 def test_run_with_argv_registered_plugin(self) -> None: 113 with self.assertRaises(KeyError): 114 self._registry.run_with_argv('plugin_one', []) 115 116 def test_register_cannot_overwrite(self) -> None: 117 self.assertIsNotNone(self._registry.register('foo', lambda: None)) 118 self.assertIsNotNone( 119 self._registry.register_by_name('bar', __name__, '_no_docstring')) 120 121 with self.assertRaises(plugins.Error): 122 self._registry.register('foo', lambda: None) 123 124 with self.assertRaises(plugins.Error): 125 self._registry.register('bar', lambda: None) 126 127 def test_register_directory_innermost_takes_priority(self) -> None: 128 with tempfile.TemporaryDirectory() as tempdir: 129 paths = list(_create_files(tempdir, _TEST_PLUGINS)) 130 self._registry.register_directory(paths[1].parent, 'TEST_PLUGINS') 131 132 self.assertEqual(self._registry.run_with_argv('test_plugin', []), 123) 133 134 def test_register_directory_only_searches_up(self) -> None: 135 with tempfile.TemporaryDirectory() as tempdir: 136 paths = list(_create_files(tempdir, _TEST_PLUGINS)) 137 self._registry.register_directory(paths[0].parent, 'TEST_PLUGINS') 138 139 self.assertEqual(self._registry.run_with_argv('test_plugin', []), 456) 140 141 def test_register_directory_with_restriction(self) -> None: 142 with tempfile.TemporaryDirectory() as tempdir: 143 paths = list(_create_files(tempdir, _TEST_PLUGINS)) 144 self._registry.register_directory(paths[0].parent, 'TEST_PLUGINS', 145 Path(tempdir, 'nested', 'in')) 146 147 self.assertNotIn('other_plugin', self._registry) 148 149 def test_register_same_file_multiple_times_no_error(self) -> None: 150 with tempfile.TemporaryDirectory() as tempdir: 151 paths = list(_create_files(tempdir, _TEST_PLUGINS)) 152 self._registry.register_file(paths[0]) 153 self._registry.register_file(paths[0]) 154 self._registry.register_file(paths[0]) 155 156 self.assertIs(self._registry['test_plugin'].target, _with_docstring) 157 158 def test_help_uses_function_or_module_docstring(self) -> None: 159 self.assertIsNotNone(self._registry.register('a', _no_docstring)) 160 self.assertIsNotNone(self._registry.register('b', _with_docstring)) 161 162 self.assertIn(__doc__, '\n'.join(self._registry.detailed_help(['a']))) 163 164 self.assertNotIn(__doc__, 165 '\n'.join(self._registry.detailed_help(['b']))) 166 self.assertIn(_with_docstring.__doc__, 167 '\n'.join(self._registry.detailed_help(['b']))) 168 169 def test_empty_string_if_no_help(self) -> None: 170 fake_module_name = f'{__name__}.fake_module_for_test{id(self)}' 171 fake_module = types.ModuleType(fake_module_name) 172 self.assertIsNone(fake_module.__doc__) 173 174 sys.modules[fake_module_name] = fake_module 175 176 try: 177 178 function = lambda: None 179 function.__module__ = fake_module_name 180 self.assertIsNotNone(self._registry.register('a', function)) 181 182 self.assertEqual(self._registry['a'].help(full=False), '') 183 self.assertEqual(self._registry['a'].help(full=True), '') 184 finally: 185 del sys.modules[fake_module_name] 186 187 def test_decorator_not_called(self) -> None: 188 @self._registry.plugin 189 def nifty() -> None: 190 pass 191 192 self.assertEqual(self._registry['nifty'].target, nifty) 193 194 def test_decorator_called_no_args(self) -> None: 195 @self._registry.plugin() 196 def nifty() -> None: 197 pass 198 199 self.assertEqual(self._registry['nifty'].target, nifty) 200 201 def test_decorator_called_with_args(self) -> None: 202 @self._registry.plugin(name='nifty') 203 def my_nifty_keen_plugin() -> None: 204 pass 205 206 self.assertEqual(self._registry['nifty'].target, my_nifty_keen_plugin) 207 208 209if __name__ == '__main__': 210 unittest.main() 211