Repository: mozilla/bugbug

Issue:
Add tests for repository.get_touched_functions
The function was introduced by #1161.


Discussion:

Resolving patch:
diff --git a/bugbug/repository.py b/bugbug/repository.py
--- a/bugbug/repository.py
+++ b/bugbug/repository.py
@@ -250,7 +250,7 @@ def hg_modified_files(hg, commit):
     )
 
 
-def get_touched_functions(path, lines, content):
+def get_touched_functions(path, deleted_lines, added_lines, content):
     if content is None:
         return set()
 
@@ -259,21 +259,53 @@ def get_touched_functions(path, lines, content):
         return set()
 
     touched_functions = set()
+    touched_function_names = set()
 
-    function_spans = function_data["spans"]
+    functions = function_data["spans"]
 
-    last_f = 0
-    for line in lines:
-        for function in function_spans[last_f:]:
-            if function["error"] or function["end_line"] < line:
-                last_f += 1
-                continue
+    def get_touched(functions, lines):
+        last_f = 0
+        for line in lines:
+            for function in functions[last_f:]:
+                if function["error"] or function["end_line"] < line:
+                    last_f += 1
+                    continue
 
-            if function["start_line"] <= line:
-                touched_functions.add(
-                    (function["name"], function["start_line"], function["end_line"])
-                )
-                break
+                if function["start_line"] <= line:
+                    touched_function_names.add(function["name"])
+                    break
+
+    # Get functions touched by added lines.
+    get_touched(functions, added_lines)
+
+    # Map functions to their positions before the patch.
+    prev_functions = copy.deepcopy(functions)
+
+    for line in added_lines:
+        for func in prev_functions:
+            if line < func["start_line"]:
+                func["start_line"] -= 1
+
+            if line < func["end_line"]:
+                func["end_line"] -= 1
+
+    for line in deleted_lines:
+        for func in prev_functions:
+            if line < func["start_line"]:
+                func["start_line"] += 1
+
+            if line < func["end_line"]:
+                func["end_line"] += 1
+
+    # Get functions touched by removed lines.
+    get_touched(prev_functions, deleted_lines)
+
+    # Return touched functions, with their new positions.
+    for function in functions:
+        if function["name"] in touched_function_names:
+            touched_functions.add(
+                (function["name"], function["start_line"], function["end_line"])
+            )
 
     return touched_functions
 
@@ -329,7 +361,9 @@ def _transform(commit):
         elif type_ in SOURCE_CODE_TYPES_TO_EXT:
             commit.source_code_files_modified_num += 1
 
-            touched_functions = get_touched_functions(path, stats["added_lines"], after)
+            touched_functions = get_touched_functions(
+                path, stats["deleted_lines"], stats["added_lines"], after
+            )
             if len(touched_functions) > 0:
                 commit.functions[path] = list(touched_functions)
 


Test patch:
diff --git a/tests/test_repository.py b/tests/test_repository.py
--- a/tests/test_repository.py
+++ b/tests/test_repository.py
@@ -13,7 +13,7 @@
 import responses
 from dateutil.relativedelta import relativedelta
 
-from bugbug import repository
+from bugbug import repository, rust_code_analysis_server
 
 
 @pytest.fixture
@@ -795,3 +795,78 @@ def test_calculate_experiences():
     assert commits["commit6"].touched_prev_90_days_component_sum == 2
     assert commits["commit6"].touched_prev_90_days_component_max == 2
     assert commits["commit6"].touched_prev_90_days_component_min == 2
+
+
+def test_get_touched_functions():
+    # Allow using the local code analysis server.
+    responses.add_passthru("http://127.0.0.1")
+
+    repository.code_analysis_server = rust_code_analysis_server.RustCodeAnalysisServer()
+
+    # No function touched.
+    touched_functions = repository.get_touched_functions(
+        "file.cpp",
+        [],
+        [],
+        """void func1() {
+    int i = 1;
+}
+
+void func2() {
+    int i = 2;
+}""",
+    )
+
+    assert touched_functions == set()
+
+    # A function touched by adding a line.
+    touched_functions = repository.get_touched_functions(
+        "file.cpp",
+        [],
+        [1],
+        """void func1() {
+    int i = 1;
+}
+
+void func2() {
+    int i = 2;
+}""",
+    )
+
+    assert touched_functions == {("func1", 1, 3)}
+
+    # A function touched by removing a line, another function touched by adding a line.
+    touched_functions = repository.get_touched_functions(
+        "file.cpp",
+        [2, 5, 6, 7, 8],
+        [6],
+        """void func1() {
+    int i = 1;
+}
+
+void func3() {
+    int i = 3;
+}
+
+void func4() {
+    int i = 4;
+}""",
+    )
+
+    assert touched_functions == {("func3", 5, 7), ("func1", 1, 3)}
+
+    # A function touched by replacing a line.
+    touched_functions = repository.get_touched_functions(
+        "file.cpp",
+        [6],
+        [6],
+        """void func1() {
+    int i = 1;
+}
+
+void func2() {
+    int i = 2;
+}""",
+    )
+
+    assert touched_functions == {("func2", 5, 7)}