• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python
2# -*- coding:utf-8 -*-
3# Copyright 2016 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Unittests for the hooks module."""
18
19from __future__ import print_function
20
21import mock
22import os
23import sys
24import unittest
25
26_path = os.path.realpath(__file__ + '/../..')
27if sys.path[0] != _path:
28    sys.path.insert(0, _path)
29del _path
30
31import rh
32import rh.hooks
33import rh.config
34
35
36class HooksDocsTests(unittest.TestCase):
37    """Make sure all hook features are documented.
38
39    Note: These tests are a bit hokey in that they parse README.md.  But they
40    get the job done, so that's all that matters right?
41    """
42
43    def setUp(self):
44        self.readme = os.path.join(os.path.dirname(os.path.dirname(
45            os.path.realpath(__file__))), 'README.md')
46
47    def _grab_section(self, section):
48        """Extract the |section| text out of the readme."""
49        ret = []
50        in_section = False
51        for line in open(self.readme):
52            if not in_section:
53                # Look for the section like "## [Tool Paths]".
54                if line.startswith('#') and line.lstrip('#').strip() == section:
55                    in_section = True
56            else:
57                # Once we hit the next section (higher or lower), break.
58                if line[0] == '#':
59                    break
60                ret.append(line)
61        return ''.join(ret)
62
63    def testBuiltinHooks(self):
64        """Verify builtin hooks are documented."""
65        data = self._grab_section('[Builtin Hooks]')
66        for hook in rh.hooks.BUILTIN_HOOKS:
67            self.assertIn('* `%s`:' % (hook,), data,
68                          msg='README.md missing docs for hook "%s"' % (hook,))
69
70    def testToolPaths(self):
71        """Verify tools are documented."""
72        data = self._grab_section('[Tool Paths]')
73        for tool in rh.hooks.TOOL_PATHS:
74            self.assertIn('* `%s`:' % (tool,), data,
75                          msg='README.md missing docs for tool "%s"' % (tool,))
76
77    def testPlaceholders(self):
78        """Verify placeholder replacement vars are documented."""
79        data = self._grab_section('Placeholders')
80        for var in rh.hooks.Placeholders.vars():
81            self.assertIn('* `${%s}`:' % (var,), data,
82                          msg='README.md missing docs for var "%s"' % (var,))
83
84
85class PlaceholderTests(unittest.TestCase):
86    """Verify behavior of replacement variables."""
87
88    def setUp(self):
89        self._saved_environ = os.environ.copy()
90        os.environ.update({
91            'PREUPLOAD_COMMIT_MESSAGE': 'commit message',
92            'PREUPLOAD_COMMIT': '5c4c293174bb61f0f39035a71acd9084abfa743d',
93        })
94        self.replacer = rh.hooks.Placeholders()
95
96    def tearDown(self):
97        os.environ.clear()
98        os.environ.update(self._saved_environ)
99
100    def testVars(self):
101        """Light test for the vars inspection generator."""
102        ret = list(self.replacer.vars())
103        self.assertGreater(len(ret), 4)
104        self.assertIn('PREUPLOAD_COMMIT', ret)
105
106    @mock.patch.object(rh.git, 'find_repo_root', return_value='/ ${BUILD_OS}')
107    def testExpandVars(self, _m):
108        """Verify the replacement actually works."""
109        input_args = [
110            # Verify ${REPO_ROOT} is updated, but not REPO_ROOT.
111            # We also make sure that things in ${REPO_ROOT} are not double
112            # expanded (which is why the return includes ${BUILD_OS}).
113            '${REPO_ROOT}/some/prog/REPO_ROOT/ok',
114            # Verify lists are merged rather than inserted.  In this case, the
115            # list is empty, but we'd hit an error still if we saw [] in args.
116            '${PREUPLOAD_FILES}',
117            # Verify values with whitespace don't expand into multiple args.
118            '${PREUPLOAD_COMMIT_MESSAGE}',
119            # Verify multiple values get replaced.
120            '${PREUPLOAD_COMMIT}^${PREUPLOAD_COMMIT_MESSAGE}',
121            # Unknown vars should be left alone.
122            '${THIS_VAR_IS_GOOD}',
123        ]
124        output_args = self.replacer.expand_vars(input_args)
125        exp_args = [
126            '/ ${BUILD_OS}/some/prog/REPO_ROOT/ok',
127            'commit message',
128            '5c4c293174bb61f0f39035a71acd9084abfa743d^commit message',
129            '${THIS_VAR_IS_GOOD}',
130        ]
131        self.assertEqual(output_args, exp_args)
132
133    def testTheTester(self):
134        """Make sure we have a test for every variable."""
135        for var in self.replacer.vars():
136            self.assertIn('test%s' % (var,), dir(self),
137                          msg='Missing unittest for variable %s' % (var,))
138
139    def testPREUPLOAD_COMMIT_MESSAGE(self):
140        """Verify handling of PREUPLOAD_COMMIT_MESSAGE."""
141        self.assertEqual(self.replacer.get('PREUPLOAD_COMMIT_MESSAGE'),
142                         'commit message')
143
144    def testPREUPLOAD_COMMIT(self):
145        """Verify handling of PREUPLOAD_COMMIT."""
146        self.assertEqual(self.replacer.get('PREUPLOAD_COMMIT'),
147                         '5c4c293174bb61f0f39035a71acd9084abfa743d')
148
149    def testPREUPLOAD_FILES(self):
150        """Verify handling of PREUPLOAD_FILES."""
151        self.assertEqual(self.replacer.get('PREUPLOAD_FILES'), [])
152
153    @mock.patch.object(rh.git, 'find_repo_root', return_value='/repo!')
154    def testREPO_ROOT(self, m):
155        """Verify handling of REPO_ROOT."""
156        self.assertEqual(self.replacer.get('REPO_ROOT'), m.return_value)
157
158    @mock.patch.object(rh.hooks, '_get_build_os_name', return_value='vapier os')
159    def testBUILD_OS(self, m):
160        """Verify handling of BUILD_OS."""
161        self.assertEqual(self.replacer.get('BUILD_OS'), m.return_value)
162
163
164class HookOptionsTests(unittest.TestCase):
165    """Verify behavior of HookOptions object."""
166
167    @mock.patch.object(rh.hooks, '_get_build_os_name', return_value='vapier os')
168    def testExpandVars(self, m):
169        """Verify expand_vars behavior."""
170        # Simple pass through.
171        args = ['who', 'goes', 'there ?']
172        self.assertEqual(args, rh.hooks.HookOptions.expand_vars(args))
173
174        # At least one replacement.  Most real testing is in PlaceholderTests.
175        args = ['who', 'goes', 'there ?', '${BUILD_OS} is great']
176        exp_args = ['who', 'goes', 'there ?', '%s is great' % (m.return_value,)]
177        self.assertEqual(exp_args, rh.hooks.HookOptions.expand_vars(args))
178
179    def testArgs(self):
180        """Verify args behavior."""
181        # Verify initial args to __init__ has higher precedent.
182        args = ['start', 'args']
183        options = rh.hooks.HookOptions('hook name', args, {})
184        self.assertEqual(options.args(), args)
185        self.assertEqual(options.args(default_args=['moo']), args)
186
187        # Verify we fall back to default_args.
188        args = ['default', 'args']
189        options = rh.hooks.HookOptions('hook name', [], {})
190        self.assertEqual(options.args(), [])
191        self.assertEqual(options.args(default_args=args), args)
192
193    def testToolPath(self):
194        """Verify tool_path behavior."""
195        options = rh.hooks.HookOptions('hook name', [], {
196            'cpplint': 'my cpplint',
197        })
198        # Check a builtin (and not overridden) tool.
199        self.assertEqual(options.tool_path('pylint'), 'pylint')
200        # Check an overridden tool.
201        self.assertEqual(options.tool_path('cpplint'), 'my cpplint')
202        # Check an unknown tool fails.
203        self.assertRaises(AssertionError, options.tool_path, 'extra_tool')
204
205
206class UtilsTests(unittest.TestCase):
207    """Verify misc utility functions."""
208
209    def testRunCommand(self):
210        """Check _run_command behavior."""
211        # Most testing is done against the utils.RunCommand already.
212        # pylint: disable=protected-access
213        ret = rh.hooks._run_command(['true'])
214        self.assertEqual(ret.returncode, 0)
215
216    def testBuildOs(self):
217        """Check _get_build_os_name behavior."""
218        # Just verify it returns something and doesn't crash.
219        # pylint: disable=protected-access
220        ret = rh.hooks._get_build_os_name()
221        self.assertTrue(isinstance(ret, str))
222        self.assertNotEqual(ret, '')
223
224    def testGetHelperPath(self):
225        """Check get_helper_path behavior."""
226        # Just verify it doesn't crash.  It's a dirt simple func.
227        ret = rh.hooks.get_helper_path('booga')
228        self.assertTrue(isinstance(ret, str))
229        self.assertNotEqual(ret, '')
230
231
232
233@mock.patch.object(rh.utils, 'run_command')
234@mock.patch.object(rh.hooks, '_check_cmd', return_value=['check_cmd'])
235class BuiltinHooksTests(unittest.TestCase):
236    """Verify the builtin hooks."""
237
238    def setUp(self):
239        self.project = rh.Project(name='project-name', dir='/.../repo/dir',
240                                  remote='remote')
241        self.options = rh.hooks.HookOptions('hook name', [], {})
242
243    def _test_commit_messages(self, func, accept, msgs):
244        """Helper for testing commit message hooks.
245
246        Args:
247          func: The hook function to test.
248          accept: Whether all the |msgs| should be accepted.
249          msgs: List of messages to test.
250        """
251        for desc in msgs:
252            ret = func(self.project, 'commit', desc, (), options=self.options)
253            if accept:
254                self.assertEqual(
255                    ret, None, msg='Should have accepted: {{{%s}}}' % (desc,))
256            else:
257                self.assertNotEqual(
258                    ret, None, msg='Should have rejected: {{{%s}}}' % (desc,))
259
260    def _test_file_filter(self, mock_check, func, files):
261        """Helper for testing hooks that filter by files and run external tools.
262
263        Args:
264          mock_check: The mock of _check_cmd.
265          func: The hook function to test.
266          files: A list of files that we'd check.
267        """
268        # First call should do nothing as there are no files to check.
269        ret = func(self.project, 'commit', 'desc', (), options=self.options)
270        self.assertEqual(ret, None)
271        self.assertFalse(mock_check.called)
272
273        # Second call should include some checks.
274        diff = [rh.git.RawDiffEntry(file=x) for x in files]
275        ret = func(self.project, 'commit', 'desc', diff, options=self.options)
276        self.assertEqual(ret, mock_check.return_value)
277
278    def testTheTester(self, _mock_check, _mock_run):
279        """Make sure we have a test for every hook."""
280        for hook in rh.hooks.BUILTIN_HOOKS:
281            self.assertIn('test_%s' % (hook,), dir(self),
282                          msg='Missing unittest for builtin hook %s' % (hook,))
283
284    def test_checkpatch(self, mock_check, _mock_run):
285        """Verify the checkpatch builtin hook."""
286        ret = rh.hooks.check_checkpatch(
287            self.project, 'commit', 'desc', (), options=self.options)
288        self.assertEqual(ret, mock_check.return_value)
289
290    def test_clang_format(self, mock_check, _mock_run):
291        """Verify the clang_format builtin hook."""
292        ret = rh.hooks.check_clang_format(
293            self.project, 'commit', 'desc', (), options=self.options)
294        self.assertEqual(ret, mock_check.return_value)
295
296    def test_google_java_format(self, mock_check, _mock_run):
297        """Verify the google_java_format builtin hook."""
298        ret = rh.hooks.check_google_java_format(
299            self.project, 'commit', 'desc', (), options=self.options)
300        self.assertEqual(ret, mock_check.return_value)
301
302    def test_commit_msg_bug_field(self, _mock_check, _mock_run):
303        """Verify the commit_msg_bug_field builtin hook."""
304        # Check some good messages.
305        self._test_commit_messages(
306            rh.hooks.check_commit_msg_bug_field, True, (
307                'subj\n\nBug: 1234\n',
308                'subj\n\nBug: 1234\nChange-Id: blah\n',
309            ))
310
311        # Check some bad messages.
312        self._test_commit_messages(
313            rh.hooks.check_commit_msg_bug_field, False, (
314                'subj',
315                'subj\n\nBUG=1234\n',
316                'subj\n\nBUG: 1234\n',
317            ))
318
319    def test_commit_msg_changeid_field(self, _mock_check, _mock_run):
320        """Verify the commit_msg_changeid_field builtin hook."""
321        # Check some good messages.
322        self._test_commit_messages(
323            rh.hooks.check_commit_msg_changeid_field, True, (
324                'subj\n\nChange-Id: I1234\n',
325            ))
326
327        # Check some bad messages.
328        self._test_commit_messages(
329            rh.hooks.check_commit_msg_changeid_field, False, (
330                'subj',
331                'subj\n\nChange-Id: 1234\n',
332                'subj\n\nChange-ID: I1234\n',
333            ))
334
335    def test_commit_msg_test_field(self, _mock_check, _mock_run):
336        """Verify the commit_msg_test_field builtin hook."""
337        # Check some good messages.
338        self._test_commit_messages(
339            rh.hooks.check_commit_msg_test_field, True, (
340                'subj\n\nTest: i did done dood it\n',
341            ))
342
343        # Check some bad messages.
344        self._test_commit_messages(
345            rh.hooks.check_commit_msg_test_field, False, (
346                'subj',
347                'subj\n\nTEST=1234\n',
348                'subj\n\nTEST: I1234\n',
349            ))
350
351    def test_cpplint(self, mock_check, _mock_run):
352        """Verify the cpplint builtin hook."""
353        self._test_file_filter(mock_check, rh.hooks.check_cpplint,
354                               ('foo.cpp', 'foo.cxx'))
355
356    def test_gofmt(self, mock_check, _mock_run):
357        """Verify the gofmt builtin hook."""
358        # First call should do nothing as there are no files to check.
359        ret = rh.hooks.check_gofmt(
360            self.project, 'commit', 'desc', (), options=self.options)
361        self.assertEqual(ret, None)
362        self.assertFalse(mock_check.called)
363
364        # Second call will have some results.
365        diff = [rh.git.RawDiffEntry(file='foo.go')]
366        ret = rh.hooks.check_gofmt(
367            self.project, 'commit', 'desc', diff, options=self.options)
368        self.assertNotEqual(ret, None)
369
370    def test_jsonlint(self, mock_check, _mock_run):
371        """Verify the jsonlint builtin hook."""
372        # First call should do nothing as there are no files to check.
373        ret = rh.hooks.check_json(
374            self.project, 'commit', 'desc', (), options=self.options)
375        self.assertEqual(ret, None)
376        self.assertFalse(mock_check.called)
377
378        # TODO: Actually pass some valid/invalid json data down.
379
380    def test_pylint(self, mock_check, _mock_run):
381        """Verify the pylint builtin hook."""
382        self._test_file_filter(mock_check, rh.hooks.check_pylint,
383                               ('foo.py',))
384
385    def test_xmllint(self, mock_check, _mock_run):
386        """Verify the xmllint builtin hook."""
387        self._test_file_filter(mock_check, rh.hooks.check_xmllint,
388                               ('foo.xml',))
389
390
391if __name__ == '__main__':
392    unittest.main()
393