Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion scanpipe/pipes/codebase.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def get_resource_fields(resource, fields):
return {field: getattr(resource, field) for field in fields}


def get_resource_tree(resource, fields, codebase=None, seen_resources=set()):
def get_resource_tree(resource, fields, codebase=None, seen_resources=None):
"""
Return a tree as a dictionary structure starting from the provided `resource`.

Expand All @@ -51,6 +51,8 @@ def get_resource_tree(resource, fields, codebase=None, seen_resources=set()):
get_codebase_tree(). We keep track of child Resources we visit in
`seen_resources`, so we don't visit them again in get_codebase_tree().
"""
if seen_resources is None:
seen_resources = set()
resource_dict = get_resource_fields(resource, fields)

if resource.is_dir:
Expand Down
47 changes: 47 additions & 0 deletions scanpipe/tests/pipes/test_codebase.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,50 @@ def test_scanpipe_pipes_codebase_get_basic_virtual_codebase(self):
self.assertEqual("c03f67211a311b13d1294ac8af7cb139ee34c4f9", resource.sha1)
self.assertEqual(19948, resource.size)
self.assertEqual(True, resource.is_file)

def test_get_resource_tree_does_not_share_seen_resources_across_calls(self):
"""
Regression test: get_resource_tree must NOT share the seen_resources
set across multiple calls.

Bug: seen_resources=set() as a mutable default argument means Python
creates the set ONCE when the function is defined. All subsequent calls
share the SAME set object, causing resources from previous calls to be
silently skipped in later calls.

Fix: use seen_resources=None and create a fresh set() inside the
function when seen_resources is None.
"""
from scanpipe.pipes.codebase import get_resource_tree
from scanpipe.models import CodebaseResource

project1 = Project.objects.create(name="Project1")
project2 = Project.objects.create(name="Project2")

dir1 = project1.codebaseresources.create(
path="dir1", type=CodebaseResource.Type.DIRECTORY
)
project1.codebaseresources.create(
path="dir1/file1.py", type=CodebaseResource.Type.FILE
)

dir2 = project2.codebaseresources.create(
path="dir1", type=CodebaseResource.Type.DIRECTORY
)
project2.codebaseresources.create(
path="dir1/file2.py", type=CodebaseResource.Type.FILE
)

fields = ["path"]

tree1 = get_resource_tree(dir1, fields)
self.assertIn("children", tree1)
self.assertEqual(len(tree1["children"]), 1)

tree2 = get_resource_tree(dir2, fields)
self.assertIn("children", tree2)
self.assertEqual(
len(tree2["children"]),
1,
"Second call returned empty children — mutable default argument bug!",
)