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