test_paver_quality.py 14.1 KB
Newer Older
1 2 3
"""
Tests for paver quality tasks
"""
4
import tempfile
5
import textwrap
6
import unittest
7

8
import os
9
import paver.easy
10
import paver.tasks
11 12 13
from ddt import ddt, file_data
from mock import patch, MagicMock, mock_open
from path import Path as path
14
from paver.easy import BuildFailure
15

16
import pavelib.quality
17
from pavelib.paver_tests.utils import fail_on_pylint, fail_on_eslint
18

19

20 21
@ddt
class TestPaverQualityViolations(unittest.TestCase):
22 23 24
    """
    For testing the paver violations-counting tasks
    """
25
    def setUp(self):
26
        super(TestPaverQualityViolations, self).setUp()
27 28
        self.f = tempfile.NamedTemporaryFile(delete=False)
        self.f.close()
29
        self.addCleanup(os.remove, self.f.name)
30 31 32 33

    def test_pylint_parser_other_string(self):
        with open(self.f.name, 'w') as f:
            f.write("hello")
34
        num = pavelib.quality._count_pylint_violations(f.name)  # pylint: disable=protected-access
35 36 37 38 39 40
        self.assertEqual(num, 0)

    def test_pylint_parser_pep8(self):
        # Pep8 violations should be ignored.
        with open(self.f.name, 'w') as f:
            f.write("foo/hello/test.py:304:15: E203 whitespace before ':'")
41
        num = pavelib.quality._count_pylint_violations(f.name)  # pylint: disable=protected-access
42 43 44 45
        self.assertEqual(num, 0)

    @file_data('pylint_test_list.json')
    def test_pylint_parser_count_violations(self, value):
46 47 48 49 50
        """
        Tests:
        - Different types of violations
        - One violation covering multiple lines
        """
51 52
        with open(self.f.name, 'w') as f:
            f.write(value)
53
        num = pavelib.quality._count_pylint_violations(f.name)  # pylint: disable=protected-access
54 55 56 57 58
        self.assertEqual(num, 1)

    def test_pep8_parser(self):
        with open(self.f.name, 'w') as f:
            f.write("hello\nhithere")
59
        num, _violations = pavelib.quality._pep8_violations(f.name)  # pylint: disable=protected-access
60 61
        self.assertEqual(num, 2)

62

63
class TestPaverReportViolationsCounts(unittest.TestCase):
64
    """
65
    For testing utility functions for getting counts from reports for
66
    run_eslint, run_complexity, run_safelint, and run_safecommit_report.
67 68 69
    """

    def setUp(self):
70
        super(TestPaverReportViolationsCounts, self).setUp()
71 72 73 74 75 76 77 78 79 80 81 82 83

        # Mock the paver @needs decorator
        self._mock_paver_needs = patch.object(pavelib.quality.run_quality, 'needs').start()
        self._mock_paver_needs.return_value = 0

        # Temporary file infrastructure
        self.f = tempfile.NamedTemporaryFile(delete=False)
        self.f.close()

        # Cleanup various mocks and tempfiles
        self.addCleanup(self._mock_paver_needs.stop)
        self.addCleanup(os.remove, self.f.name)

84
    def test_get_eslint_violations_count(self):
85 86
        with open(self.f.name, 'w') as f:
            f.write("3000 violations found")
87
        actual_count = pavelib.quality._get_count_from_last_line(self.f.name, "eslint")  # pylint: disable=protected-access
88 89
        self.assertEqual(actual_count, 3000)

Brian Jacobel committed
90
    def test_get_eslint_violations_no_number_found(self):
91 92
        with open(self.f.name, 'w') as f:
            f.write("Not expected string regex")
93
        actual_count = pavelib.quality._get_count_from_last_line(self.f.name, "eslint")  # pylint: disable=protected-access
94 95
        self.assertEqual(actual_count, None)

Brian Jacobel committed
96
    def test_get_eslint_violations_count_truncated_report(self):
97 98 99 100 101
        """
        A truncated report (i.e. last line is just a violation)
        """
        with open(self.f.name, 'w') as f:
            f.write("foo/bar/js/fizzbuzz.js: line 45, col 59, Missing semicolon.")
102
        actual_count = pavelib.quality._get_count_from_last_line(self.f.name, "eslint")  # pylint: disable=protected-access
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
        self.assertEqual(actual_count, None)

    def test_complexity_value(self):
        with open(self.f.name, 'w') as f:
            f.write("Average complexity: A (1.93953443446)")
        actual_count = pavelib.quality._get_count_from_last_line(self.f.name, "python_complexity")  # pylint: disable=protected-access
        self.assertEqual(actual_count, 1.93953443446)

    def test_truncated_complexity_report(self):
        with open(self.f.name, 'w') as f:
            f.write("M 110:4 FooBar.default - A")
        actual_count = pavelib.quality._get_count_from_last_line(self.f.name, "python_complexity")  # pylint: disable=protected-access
        self.assertEqual(actual_count, None)

    def test_no_complexity_report(self):
        with self.assertRaises(BuildFailure):
            pavelib.quality._get_count_from_last_line("non-existent-file", "python_complexity")  # pylint: disable=protected-access

    def test_generic_value(self):
        """
        Default behavior is to look for an integer appearing at head of line
        """
        with open(self.f.name, 'w') as f:
            f.write("5.777 good to see you")
        actual_count = pavelib.quality._get_count_from_last_line(self.f.name, "foo")  # pylint: disable=protected-access
        self.assertEqual(actual_count, 5)

    def test_generic_value_none_found(self):
        """
        Default behavior is to look for an integer appearing at head of line
        """
        with open(self.f.name, 'w') as f:
            f.write("hello 5.777 good to see you")
        actual_count = pavelib.quality._get_count_from_last_line(self.f.name, "foo")  # pylint: disable=protected-access
137 138
        self.assertEqual(actual_count, None)

139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
    def test_get_safelint_counts_happy(self):
        """
        Test happy path getting violation counts from safelint report.
        """
        report = textwrap.dedent("""
            test.html: 30:53: javascript-jquery-append:  $('#test').append(print_tos);

            javascript-concat-html: 310 violations
            javascript-escape:      7 violations

            2608 violations total
        """)
        with open(self.f.name, 'w') as f:
            f.write(report)
        counts = pavelib.quality._get_safelint_counts(self.f.name)  # pylint: disable=protected-access
        self.assertDictEqual(counts, {
            'rules': {
                'javascript-concat-html': 310,
                'javascript-escape': 7,
            },
            'total': 2608,
        })

    def test_get_safelint_counts_bad_counts(self):
        """
        Test getting violation counts from truncated and malformed safelint
        report.
        """
        report = textwrap.dedent("""
            javascript-concat-html: violations
        """)
        with open(self.f.name, 'w') as f:
            f.write(report)
        counts = pavelib.quality._get_safelint_counts(self.f.name)  # pylint: disable=protected-access
        self.assertDictEqual(counts, {
            'rules': {},
            'total': None,
        })

    def test_get_safecommit_count_happy(self):
        """
        Test happy path getting violation count from safecommit report.
        """
        report = textwrap.dedent("""
            Linting lms/templates/navigation.html:

            2 violations total

            Linting scripts/tests/templates/test.underscore:

            3 violations total
        """)
        with open(self.f.name, 'w') as f:
            f.write(report)
        count = pavelib.quality._get_safecommit_count(self.f.name)  # pylint: disable=protected-access

        self.assertEqual(count, 5)

    def test_get_safecommit_count_bad_counts(self):
        """
        Test getting violation count from truncated safecommit report.
        """
        report = textwrap.dedent("""
            Linting lms/templates/navigation.html:
        """)
        with open(self.f.name, 'w') as f:
            f.write(report)
        count = pavelib.quality._get_safecommit_count(self.f.name)  # pylint: disable=protected-access

        self.assertIsNone(count)

    def test_get_safecommit_count_no_files(self):
        """
        Test getting violation count from safecommit report where no files were
        linted.
        """
        report = textwrap.dedent("""
            No files linted.
        """)
        with open(self.f.name, 'w') as f:
            f.write(report)
        count = pavelib.quality._get_safecommit_count(self.f.name)  # pylint: disable=protected-access

        self.assertEqual(count, 0)

224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246

class TestPrepareReportDir(unittest.TestCase):
    """
    Tests the report directory preparation
    """

    def setUp(self):
        super(TestPrepareReportDir, self).setUp()
        self.test_dir = tempfile.mkdtemp()
        self.test_file = tempfile.NamedTemporaryFile(delete=False, dir=self.test_dir)
        self.addCleanup(os.removedirs, self.test_dir)

    def test_report_dir_with_files(self):
        self.assertTrue(os.path.exists(self.test_file.name))
        pavelib.quality._prepare_report_dir(path(self.test_dir))  # pylint: disable=protected-access
        self.assertFalse(os.path.exists(self.test_file.name))

    def test_report_dir_without_files(self):
        os.remove(self.test_file.name)
        pavelib.quality._prepare_report_dir(path(self.test_dir))  # pylint: disable=protected-access
        self.assertEqual(os.listdir(path(self.test_dir)), [])


247 248 249 250 251 252
class TestPaverRunQuality(unittest.TestCase):
    """
    For testing the paver run_quality task
    """

    def setUp(self):
253
        super(TestPaverRunQuality, self).setUp()
254

255 256 257 258 259 260 261 262 263 264 265 266 267
        # test_no_diff_quality_failures seems to alter the way that paver
        # executes these lines is subsequent tests.
        # https://github.com/paver/paver/blob/master/paver/tasks.py#L175-L180
        #
        # The other tests don't appear to have the same impact. This was
        # causing a test order dependency. This line resets that state
        # of environment._task_in_progress so that the paver commands in the
        # tests will be considered top level tasks by paver, and we can predict
        # which path it will chose in the above code block.
        #
        # TODO: Figure out why one test is altering the state to begin with.
        paver.tasks.environment = paver.tasks.Environment()

268 269 270
        # mock the @needs decorator to skip it
        self._mock_paver_needs = patch.object(pavelib.quality.run_quality, 'needs').start()
        self._mock_paver_needs.return_value = 0
271 272 273 274
        patcher = patch('pavelib.quality.sh')
        self._mock_paver_sh = patcher.start()
        self.addCleanup(patcher.stop)
        self.addCleanup(self._mock_paver_needs.stop)
275

276
    @patch('__builtin__.open', mock_open())
277 278
    def test_failure_on_diffquality_pep8(self):
        """
279
        If pep8 finds errors, pylint and eslint should still be run
280
        """
281 282 283 284 285 286 287 288
        # Mock _get_pep8_violations to return a violation
        _mock_pep8_violations = MagicMock(
            return_value=(1, ['lms/envs/common.py:32:2: E225 missing whitespace around operator'])
        )
        with patch('pavelib.quality._get_pep8_violations', _mock_pep8_violations):
            with self.assertRaises(SystemExit):
                pavelib.quality.run_quality("")

289
        # Test that pep8, pylint and eslint were called by counting the calls to
290
        # _get_pep8_violations (for pep8) and sh (for diff-quality pylint & eslint)
291
        self.assertEqual(_mock_pep8_violations.call_count, 1)
292
        self.assertEqual(self._mock_paver_sh.call_count, 2)
293

294
    @patch('__builtin__.open', mock_open())
295 296 297 298 299 300
    def test_failure_on_diffquality_pylint(self):
        """
        If diff-quality fails on pylint, the paver task should also fail
        """

        # Underlying sh call must fail when it is running the pylint diff-quality task
301
        self._mock_paver_sh.side_effect = fail_on_pylint
302 303 304 305
        _mock_pep8_violations = MagicMock(return_value=(0, []))
        with patch('pavelib.quality._get_pep8_violations', _mock_pep8_violations):
            with self.assertRaises(SystemExit):
                pavelib.quality.run_quality("")
306

307
        # Test that both pep8 and pylint were called by counting the calls
308 309
        # Assert that _get_pep8_violations (which calls "pep8") is called once
        self.assertEqual(_mock_pep8_violations.call_count, 1)
310
        # And assert that sh was called twice (for the calls to pylint & eslint).
Brian Jacobel committed
311
        # This means that even in the event of a diff-quality pylint failure, eslint is still called.
312
        self.assertEqual(self._mock_paver_sh.call_count, 2)
313 314

    @patch('__builtin__.open', mock_open())
315
    def test_failure_on_diffquality_eslint(self):
316
        """
317
        If diff-quality fails on eslint, the paver task should also fail
318 319
        """

320
        # Underlying sh call must fail when it is running the eslint diff-quality task
321
        self._mock_paver_sh.side_effect = fail_on_eslint
322 323 324 325 326 327 328 329
        _mock_pep8_violations = MagicMock(return_value=(0, []))
        with patch('pavelib.quality._get_pep8_violations', _mock_pep8_violations):
            with self.assertRaises(SystemExit):
                pavelib.quality.run_quality("")
                self.assertRaises(BuildFailure)
        # Test that both pep8 and pylint were called by counting the calls
        # Assert that _get_pep8_violations (which calls "pep8") is called once
        self.assertEqual(_mock_pep8_violations.call_count, 1)
330 331
        # And assert that sh was called twice (for the calls to pep8 and pylint)
        self.assertEqual(self._mock_paver_sh.call_count, 2)
332

333
    @patch('__builtin__.open', mock_open())
334 335 336 337 338
    def test_other_exception(self):
        """
        If diff-quality fails for an unknown reason on the first run (pep8), then
        pylint should not be run
        """
339
        self._mock_paver_sh.side_effect = [Exception('unrecognized failure!'), 0]
340
        with self.assertRaises(SystemExit):
341
            pavelib.quality.run_quality("")
342
            self.assertRaises(Exception)
343
        # Test that pylint is NOT called by counting calls
344
        self.assertEqual(self._mock_paver_sh.call_count, 1)
345

346
    @patch('__builtin__.open', mock_open())
347 348
    def test_no_diff_quality_failures(self):
        # Assert nothing is raised
349 350 351 352 353
        _mock_pep8_violations = MagicMock(return_value=(0, []))
        with patch('pavelib.quality._get_pep8_violations', _mock_pep8_violations):
            pavelib.quality.run_quality("")
        # Assert that _get_pep8_violations (which calls "pep8") is called once
        self.assertEqual(_mock_pep8_violations.call_count, 1)
354 355
        # And assert that sh was called twice (for the call to "pylint" & "eslint")
        self.assertEqual(self._mock_paver_sh.call_count, 2)