aboutsummaryrefslogtreecommitdiff
path: root/tests/checkers/unittest_similar.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/checkers/unittest_similar.py')
-rw-r--r--tests/checkers/unittest_similar.py512
1 files changed, 512 insertions, 0 deletions
diff --git a/tests/checkers/unittest_similar.py b/tests/checkers/unittest_similar.py
new file mode 100644
index 000000000..a9a13b72a
--- /dev/null
+++ b/tests/checkers/unittest_similar.py
@@ -0,0 +1,512 @@
+# Copyright (c) 2010, 2012, 2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
+# Copyright (c) 2012 Ry4an Brase <ry4an-hg@ry4an.org>
+# Copyright (c) 2014 Google, Inc.
+# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
+# Copyright (c) 2016-2018, 2020 Claudiu Popa <pcmanticore@gmail.com>
+# Copyright (c) 2016 Derek Gustafson <degustaf@gmail.com>
+# Copyright (c) 2018 Scott Worley <scottworley@scottworley.com>
+# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
+# Copyright (c) 2019-2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
+# Copyright (c) 2019 Ashley Whetter <ashley@awhetter.co.uk>
+# Copyright (c) 2019 Taewon D. Kim <kimt33@mcmaster.ca>
+# Copyright (c) 2020-2021 hippo91 <guillaume.peillex@gmail.com>
+# Copyright (c) 2020 Frank Harrison <frank@doublethefish.com>
+# Copyright (c) 2020 Eli Fine <ejfine@gmail.com>
+# Copyright (c) 2021 Daniƫl van Noord <13665637+DanielNoord@users.noreply.github.com>
+# Copyright (c) 2021 Maksym Humetskyi <Humetsky@gmail.com>
+# Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com>
+# Copyright (c) 2021 Aditya Gupta <adityagupta1089@users.noreply.github.com>
+
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
+
+from contextlib import redirect_stdout
+from io import StringIO
+from pathlib import Path
+
+import pytest
+
+from pylint.checkers import similar
+from pylint.lint import PyLinter
+from pylint.testutils import GenericTestReporter as Reporter
+
+INPUT = Path(__file__).parent / ".." / "input"
+SIMILAR1 = str(INPUT / "similar1")
+SIMILAR2 = str(INPUT / "similar2")
+SIMILAR3 = str(INPUT / "similar3")
+SIMILAR4 = str(INPUT / "similar4")
+SIMILAR5 = str(INPUT / "similar5")
+SIMILAR6 = str(INPUT / "similar6")
+SIMILAR_CLS_A = str(INPUT / "similar_cls_a.py")
+SIMILAR_CLS_B = str(INPUT / "similar_cls_b.py")
+EMPTY_FUNCTION_1 = str(INPUT / "similar_empty_func_1.py")
+EMPTY_FUNCTION_2 = str(INPUT / "similar_empty_func_2.py")
+MULTILINE = str(INPUT / "multiline-import")
+HIDE_CODE_WITH_IMPORTS = str(INPUT / "hide_code_with_imports.py")
+
+
+def test_ignore_comments() -> None:
+ output = StringIO()
+ with redirect_stdout(output), pytest.raises(SystemExit) as ex:
+ similar.Run(["--ignore-comments", SIMILAR1, SIMILAR2])
+ assert ex.value.code == 0
+ assert (
+ output.getvalue().strip()
+ == (
+ f"""
+10 similar lines in 2 files
+=={SIMILAR1}:[0:11]
+=={SIMILAR2}:[0:11]
+ import one
+ from two import two
+ three
+ four
+ five
+ six
+ # A full line comment
+ seven
+ eight
+ nine
+ ''' ten
+TOTAL lines=62 duplicates=10 percent=16.13
+"""
+ ).strip()
+ )
+
+
+def test_ignore_docstrings() -> None:
+ output = StringIO()
+ with redirect_stdout(output), pytest.raises(SystemExit) as ex:
+ similar.Run(["--ignore-docstrings", SIMILAR1, SIMILAR2])
+ assert ex.value.code == 0
+ assert (
+ output.getvalue().strip()
+ == (
+ f"""
+5 similar lines in 2 files
+=={SIMILAR1}:[7:15]
+=={SIMILAR2}:[7:15]
+ seven
+ eight
+ nine
+ ''' ten
+ ELEVEN
+ twelve '''
+ thirteen
+ fourteen
+
+5 similar lines in 2 files
+=={SIMILAR1}:[0:5]
+=={SIMILAR2}:[0:5]
+ import one
+ from two import two
+ three
+ four
+ five
+TOTAL lines=62 duplicates=10 percent=16.13
+"""
+ ).strip()
+ )
+
+
+def test_ignore_imports() -> None:
+ output = StringIO()
+ with redirect_stdout(output), pytest.raises(SystemExit) as ex:
+ similar.Run(["--ignore-imports", SIMILAR1, SIMILAR2])
+ assert ex.value.code == 0
+ assert (
+ output.getvalue().strip()
+ == """
+TOTAL lines=62 duplicates=0 percent=0.00
+""".strip()
+ )
+
+
+def test_multiline_imports() -> None:
+ output = StringIO()
+ with redirect_stdout(output), pytest.raises(SystemExit) as ex:
+ similar.Run([MULTILINE, MULTILINE])
+ assert ex.value.code == 0
+ assert (
+ output.getvalue().strip()
+ == (
+ f"""
+8 similar lines in 2 files
+=={MULTILINE}:[0:8]
+=={MULTILINE}:[0:8]
+ from foo import (
+ bar,
+ baz,
+ quux,
+ quuux,
+ quuuux,
+ quuuuux,
+ )
+TOTAL lines=16 duplicates=8 percent=50.00
+"""
+ ).strip()
+ )
+
+
+def test_ignore_multiline_imports() -> None:
+ output = StringIO()
+ with redirect_stdout(output), pytest.raises(SystemExit) as ex:
+ similar.Run(["--ignore-imports", MULTILINE, MULTILINE])
+ assert ex.value.code == 0
+ assert (
+ output.getvalue().strip()
+ == """
+TOTAL lines=16 duplicates=0 percent=0.00
+""".strip()
+ )
+
+
+def test_ignore_signatures_fail() -> None:
+ output = StringIO()
+ with redirect_stdout(output), pytest.raises(SystemExit) as ex:
+ similar.Run([SIMILAR5, SIMILAR6])
+ assert ex.value.code == 0
+ assert (
+ output.getvalue().strip()
+ == (
+ f'''
+9 similar lines in 2 files
+=={SIMILAR5}:[7:17]
+=={SIMILAR6}:[8:18]
+ arg1: int = 3,
+ arg2: Class1 = val1,
+ arg3: Class2 = func3(val2),
+ arg4: int = 4,
+ arg5: int = 5
+ ) -> Ret1:
+ pass
+
+ def example():
+ """Valid function definition with docstring only."""
+
+6 similar lines in 2 files
+=={SIMILAR5}:[0:6]
+=={SIMILAR6}:[1:7]
+ @deco1(dval1)
+ @deco2(dval2)
+ @deco3(
+ dval3,
+ dval4
+ )
+TOTAL lines=35 duplicates=15 percent=42.86
+'''
+ ).strip()
+ )
+
+
+def test_ignore_signatures_pass() -> None:
+ output = StringIO()
+ with redirect_stdout(output), pytest.raises(SystemExit) as ex:
+ similar.Run(["--ignore-signatures", SIMILAR5, SIMILAR6])
+ assert ex.value.code == 0
+ assert (
+ output.getvalue().strip()
+ == """
+TOTAL lines=35 duplicates=0 percent=0.00
+""".strip()
+ )
+
+
+def test_ignore_signatures_class_methods_fail() -> None:
+ output = StringIO()
+ with redirect_stdout(output), pytest.raises(SystemExit) as ex:
+ similar.Run([SIMILAR_CLS_B, SIMILAR_CLS_A])
+ assert ex.value.code == 0
+ assert (
+ output.getvalue().strip()
+ == (
+ f'''
+15 similar lines in 2 files
+=={SIMILAR_CLS_A}:[1:18]
+=={SIMILAR_CLS_B}:[1:18]
+ def parent_method(
+ self,
+ *,
+ a="",
+ b=None,
+ c=True,
+ ):
+ """Overridden method example."""
+
+ def _internal_func(
+ arg1: int = 1,
+ arg2: str = "2",
+ arg3: int = 3,
+ arg4: bool = True,
+ ):
+ pass
+
+
+7 similar lines in 2 files
+=={SIMILAR_CLS_A}:[20:27]
+=={SIMILAR_CLS_B}:[20:27]
+ self,
+ *,
+ a=None,
+ b=False,
+ c="",
+ ):
+ pass
+TOTAL lines=54 duplicates=22 percent=40.74
+'''
+ ).strip()
+ )
+
+
+def test_ignore_signatures_class_methods_pass() -> None:
+ output = StringIO()
+ with redirect_stdout(output), pytest.raises(SystemExit) as ex:
+ similar.Run(["--ignore-signatures", SIMILAR_CLS_B, SIMILAR_CLS_A])
+ assert ex.value.code == 0
+ assert (
+ output.getvalue().strip()
+ == """
+TOTAL lines=54 duplicates=0 percent=0.00
+""".strip()
+ )
+
+
+def test_ignore_signatures_empty_functions_fail() -> None:
+ output = StringIO()
+ with redirect_stdout(output), pytest.raises(SystemExit) as ex:
+ similar.Run([EMPTY_FUNCTION_1, EMPTY_FUNCTION_2])
+ assert ex.value.code == 0
+ assert (
+ output.getvalue().strip()
+ == (
+ f'''
+6 similar lines in 2 files
+=={EMPTY_FUNCTION_1}:[1:7]
+=={EMPTY_FUNCTION_2}:[1:7]
+ arg1: int = 1,
+ arg2: str = "2",
+ arg3: int = 3,
+ arg4: bool = True,
+ ) -> None:
+ """Valid function definition with docstring only."""
+TOTAL lines=14 duplicates=6 percent=42.86
+'''
+ ).strip()
+ )
+
+
+def test_ignore_signatures_empty_functions_pass() -> None:
+ output = StringIO()
+ with redirect_stdout(output), pytest.raises(SystemExit) as ex:
+ similar.Run(["--ignore-signatures", EMPTY_FUNCTION_1, EMPTY_FUNCTION_2])
+ assert ex.value.code == 0
+ assert (
+ output.getvalue().strip()
+ == """
+TOTAL lines=14 duplicates=0 percent=0.00
+""".strip()
+ )
+
+
+def test_no_hide_code_with_imports() -> None:
+ output = StringIO()
+ with redirect_stdout(output), pytest.raises(SystemExit) as ex:
+ similar.Run(["--ignore-imports"] + 2 * [HIDE_CODE_WITH_IMPORTS])
+ assert ex.value.code == 0
+ assert "TOTAL lines=32 duplicates=16 percent=50.00" in output.getvalue()
+
+
+def test_ignore_nothing() -> None:
+ output = StringIO()
+ with redirect_stdout(output), pytest.raises(SystemExit) as ex:
+ similar.Run([SIMILAR1, SIMILAR2])
+ assert ex.value.code == 0
+ assert (
+ output.getvalue().strip()
+ == (
+ f"""
+5 similar lines in 2 files
+=={SIMILAR1}:[0:5]
+=={SIMILAR2}:[0:5]
+ import one
+ from two import two
+ three
+ four
+ five
+TOTAL lines=62 duplicates=5 percent=8.06
+"""
+ ).strip()
+ )
+
+
+def test_lines_without_meaningful_content_do_not_trigger_similarity() -> None:
+ output = StringIO()
+ with redirect_stdout(output), pytest.raises(SystemExit) as ex:
+ similar.Run([SIMILAR3, SIMILAR4])
+ assert ex.value.code == 0
+ assert (
+ output.getvalue().strip()
+ == (
+ f"""
+14 similar lines in 2 files
+=={SIMILAR3}:[11:25]
+=={SIMILAR4}:[11:25]
+ b = (
+ (
+ [
+ "Lines 12-25 still trigger a similarity...",
+ "...warning, because..."
+ ],
+ [
+ "...even after ignoring lines with only symbols..."
+ ],
+ ),
+ (
+ "...there are still 5 similar lines in this code block.",
+ )
+ )
+TOTAL lines=50 duplicates=14 percent=28.00
+"""
+ ).strip()
+ )
+
+
+def test_help() -> None:
+ output = StringIO()
+ with redirect_stdout(output):
+ try:
+ similar.Run(["--help"])
+ except SystemExit as ex:
+ assert ex.code == 0
+ else:
+ pytest.fail("not system exit")
+
+
+def test_no_args() -> None:
+ output = StringIO()
+ with redirect_stdout(output):
+ try:
+ similar.Run([])
+ except SystemExit as ex:
+ assert ex.code == 1
+ else:
+ pytest.fail("not system exit")
+
+
+def test_get_map_data() -> None:
+ """Tests that a SimilarChecker respects the MapReduceMixin interface"""
+ linter = PyLinter(reporter=Reporter())
+
+ # Add a parallel checker to ensure it can map and reduce
+ linter.register_checker(similar.SimilarChecker(linter))
+
+ source_streams = (
+ str(INPUT / "similar_lines_a.py"),
+ str(INPUT / "similar_lines_b.py"),
+ )
+ expected_linelists = (
+ (
+ "def adipiscing(elit):",
+ 'etiam = "id"',
+ 'dictum = "purus,"',
+ 'vitae = "pretium"',
+ 'neque = "Vivamus"',
+ 'nec = "ornare"',
+ 'tortor = "sit"',
+ "return etiam, dictum, vitae, neque, nec, tortor",
+ "class Amet:",
+ "def similar_function_3_lines(self, tellus):",
+ "agittis = 10",
+ "tellus *= 300",
+ "return agittis, tellus",
+ "def lorem(self, ipsum):",
+ 'dolor = "sit"',
+ 'amet = "consectetur"',
+ "return (lorem, dolor, amet)",
+ "def similar_function_5_lines(self, similar):",
+ "some_var = 10",
+ "someother_var *= 300",
+ 'fusce = "sit"',
+ 'amet = "tortor"',
+ "return some_var, someother_var, fusce, amet",
+ 'def __init__(self, moleskie, lectus="Mauris", ac="pellentesque"):',
+ 'metus = "ut"',
+ 'lobortis = "urna."',
+ 'Integer = "nisl"',
+ '(mauris,) = "interdum"',
+ 'non = "odio"',
+ 'semper = "aliquam"',
+ 'malesuada = "nunc."',
+ 'iaculis = "dolor"',
+ 'facilisis = "ultrices"',
+ 'vitae = "ut."',
+ "return (",
+ "metus,",
+ "lobortis,",
+ "Integer,",
+ "mauris,",
+ "non,",
+ "semper,",
+ "malesuada,",
+ "iaculis,",
+ "facilisis,",
+ "vitae,",
+ ")",
+ "def similar_function_3_lines(self, tellus):",
+ "agittis = 10",
+ "tellus *= 300",
+ "return agittis, tellus",
+ ),
+ (
+ "class Nulla:",
+ 'tortor = "ultrices quis porta in"',
+ 'sagittis = "ut tellus"',
+ "def pulvinar(self, blandit, metus):",
+ "egestas = [mauris for mauris in zip(blandit, metus)]",
+ "neque = (egestas, blandit)",
+ "def similar_function_5_lines(self, similar):",
+ "some_var = 10",
+ "someother_var *= 300",
+ 'fusce = "sit"',
+ 'amet = "tortor"',
+ 'iaculis = "dolor"',
+ "return some_var, someother_var, fusce, amet, iaculis, iaculis",
+ "def tortor(self):",
+ "ultrices = 2",
+ 'quis = ultricies * "porta"',
+ "return ultricies, quis",
+ "class Commodo:",
+ "def similar_function_3_lines(self, tellus):",
+ "agittis = 10",
+ "tellus *= 300",
+ 'laoreet = "commodo "',
+ "return agittis, tellus, laoreet",
+ ),
+ )
+
+ data = []
+
+ # Manually perform a 'map' type function
+ for source_fname in source_streams:
+ sim = similar.SimilarChecker(linter)
+ with open(source_fname, encoding="utf-8") as stream:
+ sim.append_stream(source_fname, stream)
+ # The map bit, can you tell? ;)
+ data.extend(sim.get_map_data())
+
+ assert len(expected_linelists) == len(data)
+ for source_fname, expected_lines, lineset_obj in zip(
+ source_streams, expected_linelists, data
+ ):
+ assert source_fname == lineset_obj.name
+ # There doesn't seem to be a faster way of doing this, yet.
+ lines = (linespec.text for linespec in lineset_obj.stripped_lines)
+ assert tuple(expected_lines) == tuple(lines)
+
+
+def test_set_duplicate_lines_to_zero() -> None:
+ output = StringIO()
+ with redirect_stdout(output), pytest.raises(SystemExit) as ex:
+ similar.Run(["--duplicates=0", SIMILAR1, SIMILAR2])
+ assert ex.value.code == 0
+ assert output.getvalue() == ""