def test_quality(self): # Patch the output of `pyflakes` return_string = '\n' + dedent(""" ../new_file.py:328: undefined name '_thing' ../new_file.py:418: 'random' imported but unused """).strip() + '\n' _setup_patch((return_string.encode('utf-8'), b'')) # Parse the report quality = QualityReporter(pyflakes_driver) # Expect that the name is set self.assertEqual(quality.name(), 'pyflakes') # Measured_lines is undefined for # a quality reporter since all lines are measured self.assertEqual(quality.measured_lines('../new_file.py'), None) # Expect that we get the right violations expected_violations = [ Violation(328, "undefined name '_thing'"), Violation(418, "'random' imported but unused") ] self.assertEqual(expected_violations, quality.violations('../new_file.py'))
def test_quality(self): """ Test basic scenarios, including special characters that would appear in JavaScript and mixed quotation marks """ # Patch the output of the linter cmd return_string = '\n' + dedent(""" ../test_file.js: line 3, col 9, Missing "use strict" statement. ../test_file.js: line 10, col 17, '$hi' is defined but never used. """).strip() + '\n' self.subproc_mock.communicate.return_value = (( return_string.encode('utf-8'), b'')) self._mock_communicate.return_value = self.subproc_mock # Parse the report quality = QualityReporter(self._get_out()) # Expect that the name is set self.assertEqual(quality.name(), self.quality_name) # Measured_lines is undefined for # a quality reporter since all lines are measured self.assertEqual(quality.measured_lines('../blah.js'), None) # Expect that we get the right violations expected_violations = [ Violation(3, 'Missing "use strict" statement.'), Violation(10, "'$hi' is defined but never used."), ] self.assertEqual(expected_violations, quality.violations('../test_file.js'))
def test_quality(self): """Integration test.""" # Patch the output of `pydocstye` _setup_patch((dedent(""" ../new_file.py:1 at module level: D100: Missing docstring in public module ../new_file.py:13 in public function `gather`: D103: Missing docstring in public function """).strip().encode('ascii'), '')) expected_violations = [ Violation(1, 'D100: Missing docstring in public module'), Violation(13, "D103: Missing docstring in public function"), ] # Parse the report quality = QualityReporter(pydocstyle_driver) # Expect that the name is set self.assertEqual(quality.name(), 'pydocstyle') # Measured_lines is undefined for a # quality reporter since all lines are measured self.assertEqual(quality.measured_lines('../new_file.py'), None) # Expect that we get violations for file1.py only # We're not guaranteed that the violations are returned # in any particular order. actual_violations = quality.violations('../new_file.py') self.assertEqual(len(actual_violations), len(expected_violations)) for expected in expected_violations: self.assertIn(expected, actual_violations)
def test_quality(self): # Patch the output of `pep8` _mock_communicate = patch.object(Popen, 'communicate').start() return_string = '\n' + dedent(""" ../new_file.py:1:17: E231 whitespace ../new_file.py:3:13: E225 whitespace ../new_file.py:7:1: E302 blank lines """).strip() + '\n' _setup_patch((return_string.encode('utf-8'), b'')) # Parse the report quality = QualityReporter(pep8_driver) # Expect that the name is set self.assertEqual(quality.name(), 'pep8') # Measured_lines is undefined for # a quality reporter since all lines are measured self.assertEqual(quality.measured_lines('../new_file.py'), None) # Expect that we get the right violations expected_violations = [ Violation(1, 'E231 whitespace'), Violation(3, 'E225 whitespace'), Violation(7, 'E302 blank lines') ] self.assertEqual(expected_violations, quality.violations('../new_file.py'))
def test_quality(self): # Patch the output of `pylint` _setup_patch((dedent(""" file1.py:1: [C0111] Missing docstring file1.py:1: [C0111, func_1] Missing docstring file1.py:2: [W0612, cls_name.func] Unused variable 'd' file1.py:2: [W0511] TODO: Not the real way we'll store usages! file1.py:579: [F0401] Unable to import 'rooted_paths' file1.py:113: [W0613, cache_relation.clear_pk] Unused argument 'cls' file1.py:150: [F0010] error while code parsing ([Errno 2] No such file or directory) file1.py:149: [C0324, Foo.__dict__] Comma not followed by a space self.peer_grading._find_corresponding_module_for_location(Location('i4x','a','b','c','d')) file1.py:162: [R0801] Similar lines in 2 files ==file1:162 ==student.views:4 import json import logging import random path/to/file2.py:100: [W0212, openid_login_complete] Access to a protected member """).strip().encode('ascii'), '')) expected_violations = [ Violation(1, 'C0111: Missing docstring'), Violation(1, 'C0111: func_1: Missing docstring'), Violation(2, "W0612: cls_name.func: Unused variable 'd'"), Violation(2, "W0511: TODO: Not the real way we'll store usages!"), Violation(579, "F0401: Unable to import 'rooted_paths'"), Violation( 150, "F0010: error while code parsing ([Errno 2] No such file or directory)" ), Violation(149, "C0324: Foo.__dict__: Comma not followed by a space"), Violation(162, "R0801: Similar lines in 2 files"), Violation(113, "W0613: cache_relation.clear_pk: Unused argument 'cls'") ] # Parse the report quality = QualityReporter(PylintDriver()) # Expect that the name is set self.assertEqual(quality.name(), 'pylint') # Measured_lines is undefined for a # quality reporter since all lines are measured self.assertEqual(quality.measured_lines('file1.py'), None) # Expect that we get violations for file1.py only # We're not guaranteed that the violations are returned # in any particular order. actual_violations = quality.violations('file1.py') self.assertEqual(len(actual_violations), len(expected_violations)) for expected in expected_violations: self.assertIn(expected, actual_violations)
def test_unicode_continuation_char(self): _setup_patch((b"file.py:2: [W1401]" b" Invalid char '\xc3'", ''), 0) # Since we are replacing characters we can't interpet, this should # return a valid string with the char replaced with '?' quality = QualityReporter(PylintDriver()) violations = quality.violations(u'file.py') self.assertEqual(violations, [Violation(2, u"W1401: Invalid char '\ufffd'")])
def test_quality_pregenerated_report(self): # When the user provides us with a pre-generated pylint report # then use that instead of calling pylint directly. pylint_reports = [ BytesIO( dedent(u""" path/to/file.py:1: [C0111] Missing docstring path/to/file.py:57: [W0511] TODO the name of this method is a little bit confusing another/file.py:41: [W1201, assign_default_role] Specify string format arguments as logging function parameters another/file.py:175: [C0322, Foo.bar] Operator not preceded by a space x=2+3 ^ Unicode: \u9404 \u1239 another/file.py:259: [C0103, bar] Invalid name "\u4920" for type variable (should match [a-z_][a-z0-9_]{2,30}$) """).strip().encode('utf-8')), BytesIO( dedent(u""" path/to/file.py:183: [C0103, Foo.bar.gettag] Invalid name "\u3240" for type argument (should match [a-z_][a-z0-9_]{2,30}$) another/file.py:183: [C0111, Foo.bar.gettag] Missing docstring """).strip().encode('utf-8')) ] # Generate the violation report quality = QualityReporter(PylintDriver(), reports=pylint_reports) # Expect that we get the right violations expected_violations = [ Violation(1, u'C0111: Missing docstring'), Violation( 57, u'W0511: TODO the name of this method is a little bit confusing' ), Violation( 183, u'C0103: Foo.bar.gettag: Invalid name "\u3240" for type argument (should match [a-z_][a-z0-9_]{2,30}$)' ) ] # We're not guaranteed that the violations are returned # in any particular order. actual_violations = quality.violations('path/to/file.py') self.assertEqual(len(actual_violations), len(expected_violations)) for expected in expected_violations: self.assertIn(expected, actual_violations)
def test_unicode(self): _setup_patch((dedent(u""" file_\u6729.py:616: [W1401] Anomalous backslash in string: '\u5922'. String constant might be missing an r prefix. file.py:2: [W0612, cls_name.func_\u9492] Unused variable '\u2920' """).encode('utf-8'), ''), 0) quality = QualityReporter(PylintDriver()) violations = quality.violations(u'file_\u6729.py') self.assertEqual(violations, [ Violation( 616, u"W1401: Anomalous backslash in string: '\u5922'. String constant might be missing an r prefix." ), ]) violations = quality.violations(u'file.py') self.assertEqual(violations, [ Violation( 2, u"W0612: cls_name.func_\u9492: Unused variable '\u2920'") ])
def test_quality_pregenerated_report_continuation_char(self): # The report contains a non-ASCII continuation char pylint_reports = [BytesIO(b"file.py:2: [W1401] Invalid char '\xc3'")] # Generate the violation report quality = QualityReporter(PylintDriver(), reports=pylint_reports) violations = quality.violations('file.py') # Expect that the char is replaced self.assertEqual(violations, [Violation(2, u"W1401: Invalid char '\ufffd'")])
def test_unicode_continuation_char(self): _mock_communicate = patch.object(Popen, 'communicate').start() # Test a unicode continuation char, which pylint can produce (probably an encoding bug in pylint) _mock_communicate.return_value = (b"file.py:2: [W1401]" b" Invalid char '\xc3'", '') # Since we are replacing characters we can't interpet, this should # return a valid string with the char replaced with '?' quality = QualityReporter(PylintDriver()) violations = quality.violations(u'file.py') self.assertEqual(violations, [Violation(2, u"W1401: Invalid char '\ufffd'")])
def test_quality_pregenerated_report(self): # When the user provides us with a pre-generated linter report # then use that instead of calling linter directly. reports = [ BytesIO(('\n' + dedent(""" path/to/file.js: line 3, col 9, Missing "use strict" statement. path/to/file.js: line 10, col 130, Line is too long. another/file.js: line 1, col 1, 'require' is not defined. """).strip() + '\n').encode('utf-8')), BytesIO(('\n' + dedent(u""" path/to/file.js: line 12, col 14, \u9134\u1912 path/to/file.js: line 10, col 17, '$hi' is defined but never used. """).strip() + '\n').encode('utf-8')), ] # Parse the report quality = QualityReporter(self._get_out(), reports=reports) # Measured_lines is undefined for # a quality reporter since all lines are measured self.assertEqual(quality.measured_lines('path/to/file.js'), None) # Expect that we get the right violations expected_violations = [ Violation(3, u'Missing "use strict" statement.'), Violation(10, u"Line is too long."), Violation(10, u"'$hi' is defined but never used."), Violation(12, u"\u9134\u1912") ] # We're not guaranteed that the violations are returned # in any particular order. actual_violations = quality.violations('path/to/file.js') self.assertEqual(len(actual_violations), len(expected_violations)) for expected in expected_violations: self.assertIn(expected, actual_violations)
def test_quality_pregenerated_report(self): # When the user provides us with a pre-generated pycodestyle report # then use that instead of calling pycodestyle directly. pycodestyle_reports = [ BytesIO(('\n' + dedent(""" path/to/file.py:1:17: E231 whitespace path/to/file.py:3:13: E225 whitespace another/file.py:7:1: E302 blank lines """).strip() + '\n').encode('utf-8')), BytesIO(('\n' + dedent(u""" path/to/file.py:24:2: W123 \u9134\u1912 another/file.py:50:1: E302 blank lines """).strip() + '\n').encode('utf-8')), ] # Parse the report quality = QualityReporter(pycodestyle_driver, reports=pycodestyle_reports) # Measured_lines is undefined for # a quality reporter since all lines are measured self.assertEqual(quality.measured_lines('path/to/file.py'), None) # Expect that we get the right violations expected_violations = [ Violation(1, u'E231 whitespace'), Violation(3, u'E225 whitespace'), Violation(24, u'W123 \u9134\u1912') ] # We're not guaranteed that the violations are returned # in any particular order. actual_violations = quality.violations('path/to/file.py') self.assertEqual(len(actual_violations), len(expected_violations)) for expected in expected_violations: self.assertIn(expected, actual_violations)
def test_unicode(self): _mock_communicate = patch.object(Popen, 'communicate').start() # Test non-ascii unicode characters in the filename, function name and message _mock_communicate.return_value = (dedent(u""" file_\u6729.py:616: [W1401] Anomalous backslash in string: '\u5922'. String constant might be missing an r prefix. file.py:2: [W0612, cls_name.func_\u9492] Unused variable '\u2920' """).encode('utf-8'), b'') quality = QualityReporter(PylintDriver()) violations = quality.violations(u'file_\u6729.py') self.assertEqual(violations, [ Violation( 616, u"W1401: Anomalous backslash in string: '\u5922'. String constant might be missing an r prefix." ), ]) violations = quality.violations(u'file.py') self.assertEqual(violations, [ Violation( 2, u"W0612: cls_name.func_\u9492: Unused variable '\u2920'") ])
def test_quality_pregenerated_report(self): # When the user provides us with a pre-generated pyflakes report # then use that instead of calling pyflakes directly. pyflakes_reports = [ BytesIO(('\n' + dedent(""" path/to/file.py:1: undefined name 'this' path/to/file.py:3: 'random' imported but unused another/file.py:7: 'os' imported but unused """).strip() + '\n').encode('utf-8')), BytesIO(('\n' + dedent(u""" path/to/file.py:24: undefined name 'that' another/file.py:50: undefined name 'another' """).strip() + '\n').encode('utf-8')), ] # Parse the report quality = QualityReporter(pyflakes_driver, reports=pyflakes_reports) # Measured_lines is undefined for # a quality reporter since all lines are measured self.assertEqual(quality.measured_lines('path/to/file.py'), None) # Expect that we get the right violations expected_violations = [ Violation(1, u"undefined name 'this'"), Violation(3, u"'random' imported but unused"), Violation(24, u"undefined name 'that'") ] # We're not guaranteed that the violations are returned # in any particular order. actual_violations = quality.violations('path/to/file.py') self.assertEqual(len(actual_violations), len(expected_violations)) for expected in expected_violations: self.assertIn(expected, actual_violations)
def test_quality(self): # Patch the output of `flake8` return_string = '\n' + dedent(""" ../new_file.py:1:17: E231 whitespace ../new_file.py:3:13: E225 whitespace ../new_file.py:7:1: E302 blank lines ../new_file.py:8:1: W191 indentation contains tabs ../new_file.py:10:1: F841 local variable name is assigned to but never used ../new_file.py:20:1: C901 'MyModel.mymethod' is too complex (14) ../new_file.py:50:1: N801 class names should use CapWords convention ../new_file.py:60:10: T000 Todo note found. ../new_file.py:70:0: I100 statements are in the wrong order. ../new_file.py:80:0: B901 blind except: statement ../new_file.py:90:0: D207 Docstring is under-indented ../new_file.py:100:0: S100 Snippet found ../new_file.py:110:0: Q000 Remove Single quotes """).strip() + '\n' _setup_patch((return_string.encode('utf-8'), b'')) # Parse the report quality = QualityReporter(flake8_driver) # Expect that the name is set self.assertEqual(quality.name(), 'flake8') # Measured_lines is undefined for # a quality reporter since all lines are measured self.assertEqual(quality.measured_lines('../new_file.py'), None) # Expect that we get the right violations expected_violations = [ Violation(1, 'E231 whitespace'), Violation(3, 'E225 whitespace'), Violation(7, 'E302 blank lines'), Violation(8, 'W191 indentation contains tabs'), Violation( 10, 'F841 local variable name is assigned to but never used'), Violation(20, "C901 'MyModel.mymethod' is too complex (14)"), Violation(50, 'N801 class names should use CapWords convention'), Violation(60, 'T000 Todo note found.'), Violation(70, 'I100 statements are in the wrong order.'), Violation(80, 'B901 blind except: statement'), Violation(90, 'D207 Docstring is under-indented'), Violation(100, 'S100 Snippet found'), Violation(110, 'Q000 Remove Single quotes'), ] self.assertEqual(expected_violations, quality.violations('../new_file.py'))
class XmlCoverageReporterTest(unittest.TestCase): MANY_VIOLATIONS = set([ Violation(3, None), Violation(7, None), Violation(11, None), Violation(13, None) ]) FEW_MEASURED = set([2, 3, 5, 7, 11, 13]) FEW_VIOLATIONS = set([Violation(3, None), Violation(11, None)]) MANY_MEASURED = set([2, 3, 5, 7, 11, 13, 17]) ONE_VIOLATION = set([Violation(11, None)]) VERY_MANY_MEASURED = set([2, 3, 5, 7, 11, 13, 17, 23, 24, 25, 26, 26, 27]) def setUp(self): # Paths generated by git_path are always the given argument _git_path_mock = patch( 'diff_cover.violationsreporters.violations_reporter.GitPathTool' ).start() _git_path_mock.relative_path = lambda path: path _git_path_mock.absolute_path = lambda path: path def tearDown(self): patch.stopall() def test_violations(self): # Construct the XML report file_paths = ['file1.py', 'subdir/file2.py'] violations = self.MANY_VIOLATIONS measured = self.FEW_MEASURED xml = self._coverage_xml(file_paths, violations, measured) # Parse the report coverage = XmlCoverageReporter(xml) # Expect that the name is set self.assertEqual(coverage.name(), "XML") # By construction, each file has the same set # of covered/uncovered lines self.assertEqual(violations, coverage.violations('file1.py')) self.assertEqual(measured, coverage.measured_lines('file1.py')) # Try getting a smaller range result = coverage.violations('subdir/file2.py') self.assertEqual(result, violations) # Once more on the first file (for caching) result = coverage.violations('file1.py') self.assertEqual(result, violations) def test_non_python_violations(self): """ Non python projects often just have a file name specified while the full path can be acquired from a sources tag in the XML. This test checks that flow by requesting violation info from a path that can only be constructed by using the path provided in the sources tag """ fancy_path = 'superFancyPath' file_paths = ['file1.java'] source_paths = [fancy_path] violations = self.MANY_VIOLATIONS measured = self.FEW_MEASURED xml = self._coverage_xml(file_paths, violations, measured, source_paths=source_paths) coverage = XmlCoverageReporter([xml]) self.assertEqual( violations, coverage.violations('{0}/{1}'.format(fancy_path, file_paths[0]))) self.assertEqual( measured, coverage.measured_lines('{0}/{1}'.format(fancy_path, file_paths[0]))) def test_two_inputs_first_violate(self): # Construct the XML report file_paths = ['file1.py'] violations1 = self.MANY_VIOLATIONS violations2 = self.FEW_VIOLATIONS measured1 = self.FEW_MEASURED measured2 = self.MANY_MEASURED xml = self._coverage_xml(file_paths, violations1, measured1) xml2 = self._coverage_xml(file_paths, violations2, measured2) # Parse the report coverage = XmlCoverageReporter([xml, xml2]) # By construction, each file has the same set # of covered/uncovered lines self.assertEqual(violations1 & violations2, coverage.violations('file1.py')) self.assertEqual(measured1 | measured2, coverage.measured_lines('file1.py')) def test_two_inputs_second_violate(self): # Construct the XML report file_paths = ['file1.py'] violations1 = self.MANY_VIOLATIONS violations2 = self.FEW_VIOLATIONS measured1 = self.FEW_MEASURED measured2 = self.MANY_MEASURED xml = self._coverage_xml(file_paths, violations1, measured1) xml2 = self._coverage_xml(file_paths, violations2, measured2) # Parse the report coverage = XmlCoverageReporter([xml2, xml]) # By construction, each file has the same set # of covered/uncovered lines self.assertEqual(violations1 & violations2, coverage.violations('file1.py')) self.assertEqual(measured1 | measured2, coverage.measured_lines('file1.py')) def test_three_inputs(self): # Construct the XML report file_paths = ['file1.py'] violations1 = self.MANY_VIOLATIONS violations2 = self.FEW_VIOLATIONS violations3 = self.ONE_VIOLATION measured1 = self.FEW_MEASURED measured2 = self.MANY_MEASURED measured3 = self.VERY_MANY_MEASURED xml = self._coverage_xml(file_paths, violations1, measured1) xml2 = self._coverage_xml(file_paths, violations2, measured2) xml3 = self._coverage_xml(file_paths, violations3, measured3) # Parse the report coverage = XmlCoverageReporter([xml2, xml, xml3]) # By construction, each file has the same set # of covered/uncovered lines self.assertEqual(violations1 & violations2 & violations3, coverage.violations('file1.py')) self.assertEqual(measured1 | measured2 | measured3, coverage.measured_lines('file1.py')) def test_different_files_in_inputs(self): # Construct the XML report xml_roots = [ self._coverage_xml(['file.py'], self.MANY_VIOLATIONS, self.FEW_MEASURED), self._coverage_xml(['other_file.py'], self.FEW_VIOLATIONS, self.MANY_MEASURED) ] # Parse the report coverage = XmlCoverageReporter(xml_roots) self.assertEqual(self.MANY_VIOLATIONS, coverage.violations('file.py')) self.assertEqual(self.FEW_VIOLATIONS, coverage.violations('other_file.py')) def test_empty_violations(self): """ Test that an empty violations report is handled properly """ # Construct the XML report file_paths = ['file1.py'] violations1 = self.MANY_VIOLATIONS violations2 = set() measured1 = self.FEW_MEASURED measured2 = self.MANY_MEASURED xml = self._coverage_xml(file_paths, violations1, measured1) xml2 = self._coverage_xml(file_paths, violations2, measured2) # Parse the report coverage = XmlCoverageReporter([xml2, xml]) # By construction, each file has the same set # of covered/uncovered lines self.assertEqual(violations1 & violations2, coverage.violations('file1.py')) self.assertEqual(measured1 | measured2, coverage.measured_lines('file1.py')) def test_no_such_file(self): # Construct the XML report with no source files xml = self._coverage_xml([], [], []) # Parse the report coverage = XmlCoverageReporter(xml) # Expect that we get no results result = coverage.violations('file.py') self.assertEqual(result, set([])) def _coverage_xml(self, file_paths, violations, measured, source_paths=None): """ Build an XML tree with source files specified by `file_paths`. Each source fill will have the same set of covered and uncovered lines. `file_paths` is a list of path strings `line_dict` is a dictionary with keys that are line numbers and values that are True/False indicating whether the line is covered This leaves out some attributes of the Cobertura format, but includes all the elements. """ root = etree.Element('coverage') if source_paths: sources = etree.SubElement(root, 'sources') for path in source_paths: source = etree.SubElement(sources, 'source') source.text = path packages = etree.SubElement(root, 'packages') classes = etree.SubElement(packages, 'classes') violation_lines = set(violation.line for violation in violations) for path in file_paths: src_node = etree.SubElement(classes, 'class') src_node.set('filename', path) etree.SubElement(src_node, 'methods') lines_node = etree.SubElement(src_node, 'lines') # Create a node for each line in measured for line_num in measured: is_covered = line_num not in violation_lines line = etree.SubElement(lines_node, 'line') hits = 1 if is_covered else 0 line.set('hits', str(hits)) line.set('number', str(line_num)) return root
class BaseReportGeneratorTest(unittest.TestCase): """ Base class for constructing test cases of report generators. """ # Test data, returned by default from the mocks SRC_PATHS = {'file1.py', 'subdir/file2.py'} LINES = [2, 3, 4, 5, 10, 11, 12, 13, 14, 15] VIOLATIONS = [Violation(n, None) for n in (10, 11, 20)] MEASURED = [1, 2, 3, 4, 7, 10, 11, 15, 20, 30] XML_REPORT_NAME = ["reports/coverage.xml"] DIFF_REPORT_NAME = "master" # Subclasses override this to provide the class under test REPORT_GENERATOR_CLASS = None # Snippet returned by the mock SNIPPET = "<div>Snippet with \u1235 \u8292 unicode</div>" SNIPPET_STYLE = '.css { color:red }' def setUp(self): # Create mocks of the dependencies self.coverage = mock.MagicMock(BaseViolationReporter) self.diff = mock.MagicMock(BaseDiffReporter) self.addCleanup(mock.patch.stopall) # Patch snippet loading to always return the same string self._load_snippets_html = mock.patch( 'diff_cover.snippets.Snippet.load_snippets_html').start() self.set_num_snippets(0) # Patch snippet style style_defs = mock.patch( 'diff_cover.snippets.Snippet.style_defs').start() style_defs.return_value = self.SNIPPET_STYLE # Set the names of the XML and diff reports self.coverage.name.return_value = self.XML_REPORT_NAME self.diff.name.return_value = self.DIFF_REPORT_NAME # Configure the mocks self.set_src_paths_changed([]) self._lines_dict = dict() self.diff.lines_changed.side_effect = self._lines_dict.get self._violations_dict = dict() self.coverage.violations.side_effect = self._violations_dict.get self._measured_dict = dict() self.coverage.measured_lines.side_effect = self._measured_dict.get # Create a concrete instance of a report generator self.report = self.REPORT_GENERATOR_CLASS(self.coverage, self.diff) def set_src_paths_changed(self, src_paths): """ Patch the dependency `src_paths_changed()` return value """ self.diff.src_paths_changed.return_value = src_paths def set_lines_changed(self, src_path, lines): """ Patch the dependency `lines_changed()` to return `lines` when called with argument `src_path`. """ self._lines_dict.update({src_path: lines}) def set_violations(self, src_path, violations): """ Patch the dependency `violations()` to return `violations` when called with argument `src_path`. """ self._violations_dict.update({src_path: violations}) def set_measured(self, src_path, measured): """ Patch the dependency `measured_lines()` return `measured` when called with argument `src_path`. """ self._measured_dict.update({src_path: measured}) def set_num_snippets(self, num_snippets): """ Patch the depdenency `Snippet.load_snippets_html()` to return `num_snippets` of the fake snippet HTML. """ self._load_snippets_html.return_value = \ num_snippets * [self.SNIPPET] def use_default_values(self): """ Configure the mocks to use default values provided by class constants. All source files are given the same line, violation, and measured information. """ self.set_src_paths_changed(self.SRC_PATHS) for src in self.SRC_PATHS: self.set_lines_changed(src, self.LINES) self.set_violations(src, self.VIOLATIONS) self.set_measured(src, self.MEASURED) self.set_num_snippets(0) def assert_report(self, expected): """ Generate a report and assert that it matches the string `expected`. """ # Create a buffer for the output output = BytesIO() # Generate the report self.report.generate_report(output) # Get the output output_str = output.getvalue() output.close() # Verify that we got the expected string assert_long_str_equal(expected, output_str, strip=True)
class BaseReportGeneratorTest: """Base class for constructing test cases of report generators.""" # Test data, returned by default from the mocks SRC_PATHS = {"file1.py", "subdir/file2.py"} LINES = [2, 3, 4, 5, 10, 11, 12, 13, 14, 15] VIOLATIONS = [Violation(n, None) for n in (10, 11, 20)] MEASURED = [1, 2, 3, 4, 7, 10, 11, 15, 20, 30] XML_REPORT_NAME = ["reports/coverage.xml"] DIFF_REPORT_NAME = "main" # Subclasses override this to provide the class under test REPORT_GENERATOR_CLASS = None # Snippet returned by the mock SNIPPET_HTML = "<div>Snippet with \u1235 \u8292 unicode</div>" SNIPPET_MARKDOWN = "Lines 1-1\n\n```\nSnippet with \u1235 \u8292 unicode\n```" SNIPPET_STYLE = ".css { color:red }" SNIPPET_TERMINAL = SNIPPET_MARKDOWN @pytest.fixture(autouse=True) def base_setup(self, mocker): # Create mocks of the dependencies self.coverage = mocker.MagicMock(BaseViolationReporter) self.diff = mocker.MagicMock(BaseDiffReporter) # Patch snippet loading to always return the same string self._load_formatted_snippets = mocker.patch( "diff_cover.snippets.Snippet.load_formatted_snippets") self.set_num_snippets(0) # Patch snippet style style_defs = mocker.patch("diff_cover.snippets.Snippet.style_defs") style_defs.return_value = self.SNIPPET_STYLE # Set the names of the XML and diff reports self.coverage.name.return_value = self.XML_REPORT_NAME self.diff.name.return_value = self.DIFF_REPORT_NAME # Configure the mocks self.set_src_paths_changed([]) self._lines_dict = dict() self.diff.lines_changed.side_effect = self._lines_dict.get self._violations_dict = dict() self.coverage.violations.side_effect = self._violations_dict.get self._measured_dict = dict() self.coverage.measured_lines.side_effect = self._measured_dict.get # Create a concrete instance of a report generator self.report = self.REPORT_GENERATOR_CLASS(self.coverage, self.diff) def set_src_paths_changed(self, src_paths): """ Patch the dependency `src_paths_changed()` return value """ self.diff.src_paths_changed.return_value = src_paths def set_lines_changed(self, src_path, lines): """ Patch the dependency `lines_changed()` to return `lines` when called with argument `src_path`. """ self._lines_dict.update({src_path: lines}) def set_violations(self, src_path, violations): """ Patch the dependency `violations()` to return `violations` when called with argument `src_path`. """ self._violations_dict.update({src_path: violations}) def set_measured(self, src_path, measured): """ Patch the dependency `measured_lines()` return `measured` when called with argument `src_path`. """ self._measured_dict.update({src_path: measured}) def set_num_snippets(self, num_snippets): """ Patch the depdenency `Snippet.load_snippets_html()` to return `num_snippets` of the fake snippet HTML. """ self._load_formatted_snippets.return_value = { "html": num_snippets * [self.SNIPPET_HTML], "markdown": num_snippets * [self.SNIPPET_MARKDOWN], "terminal": num_snippets * [self.SNIPPET_TERMINAL], } def use_default_values(self): """ Configure the mocks to use default values provided by class constants. All source files are given the same line, violation, and measured information. """ self.set_src_paths_changed(self.SRC_PATHS) for src in self.SRC_PATHS: self.set_lines_changed(src, self.LINES) self.set_violations(src, self.VIOLATIONS) self.set_measured(src, self.MEASURED) self.set_num_snippets(0) def get_report(self): """ Generate a report and assert that it matches the string `expected`. """ # Create a buffer for the output output = BytesIO() # Generate the report self.report.generate_report(output) # Get the output output_str = output.getvalue() output.close() return output_str.decode("utf-8") def assert_report(self, expected): output_report_string = self.get_report() assert expected.strip() == output_report_string.strip()