Skip to content
Merged
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
25 changes: 6 additions & 19 deletions api/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,22 +426,18 @@ def delete_files(self, files: list[Path]) -> tuple[str, dict, list[int]]:

return None

def get_file(self, path: str, name: str, ext: str) -> Optional[File]:
def get_file(self, path: str, name: str, ext: str) -> Optional[Node]:
"""
Retrieves a File entity from the graph database based on its path, name, and extension.
Retrieves a File node from the graph database based on its path, name,
and extension.

Args:
path (str): The file path.
name (str): The file name.
ext (str): The file extension.

Returns:
Optional[File]: The File object if found, otherwise None.

This method constructs and executes a query to find a file node in the graph
database with the specified path, name, and extension. If the file node is found,
it creates and returns a File object with its properties and ID. If no such node
is found, it returns None.
Optional[Node]: The File node if found, otherwise None.

Example:
file = self.get_file('/path/to/file', 'filename', '.py')
Expand All @@ -452,19 +448,10 @@ def get_file(self, path: str, name: str, ext: str) -> Optional[File]:
params = {'path': path, 'name': name, 'ext': ext}

res = self._query(q, params)
if(len(res.result_set) == 0):
if len(res.result_set) == 0:
return None

node = res.result_set[0][0]

ext = node.properties['ext']
path = node.properties['path']
name = node.properties['name']
file = File(path, name, ext)

file.id = node.id

return file
return res.result_set[0][0]

# set file code coverage
# if file coverage is 100% set every defined function coverage to 100% aswell
Expand Down
48 changes: 28 additions & 20 deletions tests/test_c_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import unittest
from pathlib import Path

from api import SourceAnalyzer, File, Struct, Function, Graph
from api import SourceAnalyzer, Graph


class Test_C_Analyzer(unittest.TestCase):
def test_analyzer(self):
Expand All @@ -24,37 +25,44 @@ def test_analyzer(self):
analyzer.analyze_local_folder(path, g)

f = g.get_file('', 'src.c', '.c')
self.assertEqual(File('', 'src.c', '.c'), f)
self.assertIsNotNone(f)
self.assertEqual(f.properties['name'], 'src.c')
self.assertEqual(f.properties['ext'], '.c')
Comment on lines 27 to +30
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graph.add_file() persists file nodes with path=str(file.path) (full file path). Using g.get_file('', 'src.c', '.c') will not match that representation on a clean graph. Update the test to query using the same stored path value (or change the storage/query convention so path is directory-only and name is basename).

Copilot uses AI. Check for mistakes.

s = g.get_struct_by_name('exp')
expected_s = Struct('src.c', 'exp', '', 9, 13)
expected_s.add_field('i', 'int')
expected_s.add_field('f', 'float')
expected_s.add_field('data', 'char[]')
self.assertEqual(expected_s, s)
self.assertIsNotNone(s)
self.assertEqual(s.properties['name'], 'exp')
self.assertEqual(s.properties['path'], 'src.c')
Comment on lines 32 to +35
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test expects Struct/C Function nodes to exist after SourceAnalyzer.analyze_local_folder(path_to_c_sources, g), but the current SourceAnalyzer only enumerates *.java, *.py, and *.cs files (so no C files are analyzed and get_struct_by_name('exp') will be None on a clean graph). Either re-enable C support in the analyzer pipeline or skip/adjust this test to use a supported language.

Copilot uses AI. Check for mistakes.
self.assertEqual(s.properties['src_start'], 9)
self.assertEqual(s.properties['src_end'], 13)
self.assertEqual(s.properties['fields'], [['i', 'int'], ['f', 'float'], ['data', 'char[]']])

add = g.get_function_by_name('add')

expected_add = Function('src.c', 'add', '', 'int', '', 0, 7)
expected_add.add_argument('a', 'int')
expected_add.add_argument('b', 'int')
self.assertEqual(expected_add, add)
self.assertIn('a + b', add.src)
self.assertIsNotNone(add)
self.assertEqual(add.properties['name'], 'add')
self.assertEqual(add.properties['path'], 'src.c')
self.assertEqual(add.properties['ret_type'], 'int')
self.assertEqual(add.properties['src_start'], 0)
self.assertEqual(add.properties['src_end'], 7)
self.assertEqual(add.properties['args'], [['a', 'int'], ['b', 'int']])
self.assertIn('a + b', add.properties['src'])

main = g.get_function_by_name('main')

expected_main = Function('src.c', 'main', '', 'int', '', 15, 18)
expected_main.add_argument('argv', 'const char**')
expected_main.add_argument('argc', 'int')
self.assertEqual(expected_main, main)
self.assertIn('x = add', main.src)
self.assertIsNotNone(main)
self.assertEqual(main.properties['name'], 'main')
self.assertEqual(main.properties['path'], 'src.c')
self.assertEqual(main.properties['ret_type'], 'int')
self.assertEqual(main.properties['src_start'], 15)
self.assertEqual(main.properties['src_end'], 18)
self.assertEqual(main.properties['args'], [['argv', 'const char**'], ['argc', 'int']])
self.assertIn('x = add', main.properties['src'])

callees = g.function_calls(main.id)
self.assertEqual(len(callees), 1)
self.assertEqual(callees[0], add)

callers = g.function_called_by(add.id)
callers = [caller.name for caller in callers]
callers = [caller.properties['name'] for caller in callers]

self.assertEqual(len(callers), 2)
self.assertIn('add', callers)
Expand Down
21 changes: 10 additions & 11 deletions tests/test_git_history.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import os
import unittest
from git import Repo
import pygit2
from api import (
Graph,
Project,
switch_commit
)
Expand Down Expand Up @@ -30,8 +29,8 @@ def setUpClass(cls):
repo_dir = os.path.join(current_dir, 'git_repo')

# Checkout HEAD commit
repo = Repo(repo_dir)
repo.git.checkout("HEAD")
repo = pygit2.Repository(repo_dir)
repo.checkout_head()

proj = Project.from_local_repository(repo_dir)
graph = proj.analyze_sources()
Expand All @@ -45,13 +44,13 @@ def assert_file_exists(self, path: str, name: str, ext: str) -> None:
f = graph.get_file(path, name, ext)

self.assertIsNotNone(f)
self.assertEqual(f.ext, ext)
self.assertEqual(f.path, path)
self.assertEqual(f.name, name)
self.assertEqual(f.properties['ext'], ext)
self.assertEqual(f.properties['path'], path)
self.assertEqual(f.properties['name'], name)

def test_git_graph_structure(self):
# validate git graph structure
c = repo.commit("HEAD")
c = repo.revparse_single("HEAD")

while True:
commits = git_graph.get_commits([c.short_id])
Expand All @@ -62,13 +61,13 @@ def test_git_graph_structure(self):
self.assertEqual(c.short_id, actual['hash'])
self.assertEqual(c.message, actual['message'])
self.assertEqual(c.author.name, actual['author'])
self.assertEqual(c.committed_date, actual['date'])
self.assertEqual(c.commit_time, actual['date'])

# Advance to previous commit
if len(c.parents) == 0:
if len(c.parent_ids) == 0:
break

c = c.parents[0]
c = repo.get(c.parent_ids[0])

def test_git_transitions(self):
# our test git repo:
Expand Down
59 changes: 35 additions & 24 deletions tests/test_graph_ops.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import unittest
from pathlib import Path
from falkordb import FalkorDB
from typing import List, Optional
from api import *
from api import Graph
from api.entities import File


class TestGraphOps(unittest.TestCase):
Expand All @@ -11,50 +12,60 @@ def setUp(self):
self.graph = Graph(name='test')

def test_add_function(self):
# Create function
func = Function('/path/to/function', 'func', '', 'int', '', 1, 10)
func.add_argument('x', 'int')
func.add_argument('y', 'float')

self.graph.add_function(func)
self.assertEqual(func, self.graph.get_function(func.id))
func_id = self.graph.add_entity(
'Function', 'func', '', '/path/to/function', 1, 10,
{'ret_type': 'int', 'src': '', 'args': [['x', 'int'], ['y', 'float']]}
)
result = self.graph.get_function(func_id)
self.assertIsNotNone(result)
self.assertEqual(result.properties['name'], 'func')
self.assertEqual(result.properties['ret_type'], 'int')
self.assertEqual(result.properties['args'], [['x', 'int'], ['y', 'float']])

def test_add_file(self):
file = File('/path/to/file', 'file', 'txt')

file = File(Path('/path/to/file.txt'), None)
self.graph.add_file(file)
self.assertEqual(file, self.graph.get_file('/path/to/file', 'file', 'txt'))
result = self.graph.get_file('/path/to/file.txt', 'file.txt', '.txt')
self.assertIsNotNone(result)
self.assertEqual(result.properties['name'], 'file.txt')
self.assertEqual(result.properties['ext'], '.txt')

def test_file_add_function(self):
file = File('/path/to/file', 'file', 'txt')
func = Function('/path/to/function', 'func', '', 'int', '', 1, 10)

file = File(Path('/path/to/file.txt'), None)
self.graph.add_file(file)
self.graph.add_function(func)

self.graph.connect_entities("CONTAINS", file.id, func.id)
func_id = self.graph.add_entity(
'Function', 'func', '', '/path/to/function', 1, 10,
{'ret_type': 'int', 'src': '', 'args': []}
)

self.graph.connect_entities("CONTAINS", file.id, func_id)

query = """MATCH (file:File)-[:CONTAINS]->(func:Function)
WHERE ID(func) = $func_id AND ID(file) = $file_id
RETURN true"""

params = {'file_id': file.id, 'func_id': func.id}
params = {'file_id': file.id, 'func_id': func_id}
res = self.g.query(query, params).result_set
self.assertTrue(res[0][0])

def test_function_calls_function(self):
caller = Function('/path/to/function', 'func_A', '', 'int', '', 1, 10)
callee = Function('/path/to/function', 'func_B', '', 'int', '', 11, 21)
caller_id = self.graph.add_entity(
'Function', 'func_A', '', '/path/to/function', 1, 10,
{'ret_type': 'int', 'src': '', 'args': []}
)
callee_id = self.graph.add_entity(
'Function', 'func_B', '', '/path/to/function', 11, 21,
{'ret_type': 'int', 'src': '', 'args': []}
)

self.graph.add_function(caller)
self.graph.add_function(callee)
self.graph.function_calls_function(caller.id, callee.id, 10)
self.graph.function_calls_function(caller_id, callee_id, 10)

query = """MATCH (caller:Function)-[:CALLS]->(callee:Function)
WHERE ID(caller) = $caller_id AND ID(callee) = $callee_id
RETURN true"""

params = {'caller_id': caller.id, 'callee_id': callee.id}
params = {'caller_id': caller_id, 'callee_id': callee_id}
res = self.g.query(query, params).result_set
self.assertTrue(res[0][0])

Expand Down
50 changes: 32 additions & 18 deletions tests/test_py_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import unittest
from pathlib import Path

from api import SourceAnalyzer, File, Class, Function, Graph
from api import SourceAnalyzer, Graph


class Test_PY_Analyzer(unittest.TestCase):
def test_analyzer(self):
Expand All @@ -15,7 +16,7 @@ def test_analyzer(self):
# Get the directory of the current file
current_dir = os.path.dirname(current_file_path)

# Append 'source_files/c' to the current directory
# Append 'source_files/py' to the current directory
path = os.path.join(current_dir, 'source_files')
path = os.path.join(path, 'py')
path = str(path)
Expand All @@ -24,37 +25,50 @@ def test_analyzer(self):
analyzer.analyze_local_folder(path, g)

f = g.get_file('', 'src.py', '.py')
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graph.add_file() stores the File node with path=str(file.path) (full file path, including filename), so calling g.get_file('', 'src.py', '.py') will return None on a clean graph. Update this test to pass the same path value that gets persisted (e.g., the full path to src.py, or adjust Graph to store directory paths consistently).

Suggested change
f = g.get_file('', 'src.py', '.py')
src_file_path = os.path.join(path, 'src.py')
f = g.get_file(src_file_path, 'src.py', '.py')

Copilot uses AI. Check for mistakes.
self.assertEqual(File('', 'src.py', '.py'), f)
self.assertIsNotNone(f)
self.assertEqual(f.properties['name'], 'src.py')
self.assertEqual(f.properties['ext'], '.py')

log = g.get_function_by_name('log')
expected_log = Function('src.py', 'log', None, 'None', '', 0, 1)
expected_log.add_argument('msg', 'str')
self.assertEqual(expected_log, log)
self.assertIsNotNone(log)
self.assertEqual(log.properties['name'], 'log')
self.assertEqual(log.properties['path'], 'src.py')
self.assertEqual(log.properties['ret_type'], 'None')
self.assertEqual(log.properties['src_start'], 0)
self.assertEqual(log.properties['src_end'], 1)
self.assertEqual(log.properties['args'], [['msg', 'str']])
Comment on lines +34 to +39
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These assertions assume metadata like ret_type and args are present on Function nodes and that path == 'src.py'. In the current analysis pipeline, entities are created via graph.add_entity(..., props={}) (no ret_type/args), and path is set from str(file.path) (typically an absolute path). This will raise KeyError/fail in a fresh DB. Consider asserting only on guaranteed properties (name, path, src_start, src_end, doc) or updating the analyzer to populate these properties before asserting on them.

Copilot uses AI. Check for mistakes.

abort = g.get_function_by_name('abort')
expected_abort = Function('src.py', 'abort', None, 'Task', '', 9, 11)
expected_abort.add_argument('self', 'Unknown')
expected_abort.add_argument('delay', 'float')
self.assertEqual(expected_abort, abort)
self.assertIsNotNone(abort)
self.assertEqual(abort.properties['name'], 'abort')
self.assertEqual(abort.properties['path'], 'src.py')
self.assertEqual(abort.properties['ret_type'], 'Task')
self.assertEqual(abort.properties['src_start'], 9)
self.assertEqual(abort.properties['src_end'], 11)
self.assertEqual(abort.properties['args'], [['self', 'Unknown'], ['delay', 'float']])

init = g.get_function_by_name('__init__')
expected_init = Function('src.py', '__init__', None, None, '', 4, 7)
expected_init.add_argument('self', 'Unknown')
expected_init.add_argument('name', 'str')
expected_init.add_argument('duration', 'int')
self.assertEqual(expected_init, init)
self.assertIsNotNone(init)
self.assertEqual(init.properties['name'], '__init__')
self.assertEqual(init.properties['path'], 'src.py')
self.assertEqual(init.properties['src_start'], 4)
self.assertEqual(init.properties['src_end'], 7)
self.assertEqual(init.properties['args'], [['self', 'Unknown'], ['name', 'str'], ['duration', 'int']])

task = g.get_class_by_name('Task')
expected_task = Class('src.py', 'Task', None, 3, 11)
self.assertEqual(expected_task, task)
self.assertIsNotNone(task)
self.assertEqual(task.properties['name'], 'Task')
self.assertEqual(task.properties['path'], 'src.py')
self.assertEqual(task.properties['src_start'], 3)
self.assertEqual(task.properties['src_end'], 11)

callees = g.function_calls(abort.id)
self.assertEqual(len(callees), 1)
self.assertEqual(callees[0], log)

print_func = g.get_function_by_name('print')
callers = g.function_called_by(print_func.id)
callers = [caller.name for caller in callers]
callers = [caller.properties['name'] for caller in callers]

self.assertIn('__init__', callers)
self.assertIn('log', callers)
Expand Down
Loading