From 074c49edbb43671abd8c688d6458269910f33675 Mon Sep 17 00:00:00 2001 From: Hezheng Yin Date: Tue, 12 Feb 2019 22:35:45 -0800 Subject: [PATCH] Golang support (#12) * Add go repo to test * add go-test-repo * Add go graph test analyzer * Fix go_test_repo * Add directed and multigraph flags to graph json data * Add GO_FILEANME_REGEXES * Fix various bugs in test_analyzer_go.py * Undo changes to CallCommitGraph --- persper/analytics/go.py | 59 ++++++++++++++++++ persper/analytics/graph_server.py | 3 + test/go_test_repo/A/main.go | 26 ++++++++ test/go_test_repo/B/main.go | 35 +++++++++++ test/go_test_repo/C/main.go | 32 ++++++++++ test/go_test_repo/cg.dot | 3 + test/test_analytics/test_analyzer_go.py | 82 +++++++++++++++++++++++++ 7 files changed, 240 insertions(+) create mode 100644 persper/analytics/go.py create mode 100644 test/go_test_repo/A/main.go create mode 100644 test/go_test_repo/B/main.go create mode 100644 test/go_test_repo/C/main.go create mode 100644 test/go_test_repo/cg.dot create mode 100644 test/test_analytics/test_analyzer_go.py diff --git a/persper/analytics/go.py b/persper/analytics/go.py new file mode 100644 index 00000000000..0996e82e9af --- /dev/null +++ b/persper/analytics/go.py @@ -0,0 +1,59 @@ +from networkx.readwrite import json_graph +from persper.analytics.graph_server import GraphServer +from persper.analytics.call_commit_graph import CallCommitGraph +import re +import requests +import urllib.parse + + +class GoGraphServer(GraphServer): + def __init__(self, server_addr, filename_regex_strs): + self.server_addr = server_addr + self.filename_regexes = [re.compile(regex_str) for regex_str in filename_regex_strs] + self.config_param = dict() + + def register_commit(self, hexsha, author_name, author_email, commit_message): + # TODO: use 'message' or 'commit_message', but not both + payload = {'hexsha': hexsha, + 'author_name': author_name, + 'author_email': author_email, + 'message': commit_message} + register_url = urllib.parse.urljoin(self.server_addr, '/register_commit') + r = requests.post(register_url, json=payload).json() + if r != '0': + raise ValueError() + + def update_graph(self, old_filename, old_src, + new_filename, new_src, patch): + payload = {'oldFname': old_filename, + 'oldSrc': old_src, + 'newFname': new_filename, + 'newSrc': new_src, + 'patch': patch.decode('utf-8', 'replace'), + 'config': self.config_param} + + update_url = urllib.parse.urljoin(self.server_addr, '/update') + r = requests.post(update_url, json=payload).json() + if r != '0': + raise ValueError() + + def get_graph(self): + graph_url = self.server_addr + '/callgraph' + r = requests.get(graph_url) + graph_data = r.json() + graph_data['directed'] = True + graph_data['multigraph'] = True + return CallCommitGraph(graph_data) + + def reset_graph(self): + reset_url = urllib.parse.urljoin(self.server_addr, '/reset') + requests.post(reset_url) + + def filter_file(self, filename): + for regex in self.filename_regexes: + if not regex.match(filename): + return False + return True + + def config(self, param): + self.config_param = param diff --git a/persper/analytics/graph_server.py b/persper/analytics/graph_server.py index 5cf79c1d55a..c760aa09d13 100644 --- a/persper/analytics/graph_server.py +++ b/persper/analytics/graph_server.py @@ -21,6 +21,9 @@ r'.+\.(c|cc|cxx|cpp|CPP|c\+\+|C|hh|hpp|Hpp|h\+\+|H)$' } +GO_FILENAME_REGEXES = [ + r'.+\.go$' +] class GraphServer(ABC): diff --git a/test/go_test_repo/A/main.go b/test/go_test_repo/A/main.go new file mode 100644 index 00000000000..2af71888f59 --- /dev/null +++ b/test/go_test_repo/A/main.go @@ -0,0 +1,26 @@ +package main +import( + "fmt" + "math" +) +type a func() + +type Vertex struct { + X, Y float64 +} + +func (v Vertex) Abs() float64 { + return math.Sqrt(v.X*v.X + v.Y*v.Y) +} + +func funcA () { + fmt.Println("func A is called!") +} + +func main() { + a := funcA + a() + v := Vertex{3, 4} + fmt.Println(v.Abs()) +} + diff --git a/test/go_test_repo/B/main.go b/test/go_test_repo/B/main.go new file mode 100644 index 00000000000..ea01026d96e --- /dev/null +++ b/test/go_test_repo/B/main.go @@ -0,0 +1,35 @@ +package main +import( + "fmt" + "math" +) +type a func() +type b func() +type c func() +type Vertex struct { + X, Y float64 +} + +func (v Vertex) Abs() float64 { + funcA() + return math.Sqrt(v.X*v.X + v.Y*v.Y) + + +} + +func funcA () { + fmt.Println("func A is called!") +} +func funcB () { + fmt.Println("func B is called!") +} +func main() { + a := funcA + b := funcB + c := a + b() + c() + v := Vertex{3, 4} + fmt.Println(v.Abs()) +} + \ No newline at end of file diff --git a/test/go_test_repo/C/main.go b/test/go_test_repo/C/main.go new file mode 100644 index 00000000000..a2a38913ab5 --- /dev/null +++ b/test/go_test_repo/C/main.go @@ -0,0 +1,32 @@ +package main +import( + "fmt" + "math" +) +type a func() +type b func() +type c func() +type Vertex struct { + X, Y float64 +} +func (v Vertex) Abs() float64 { + a:=funcA() + a() + return math.Sqrt(v.X*v.X + v.Y*v.Y) +} +func funcA () { + fmt.Println("func A is called!") +} +func funcB () { + funcA() + fmt.Println("func B is called!") +} +func main() { + a := funcA + b := funcB + c := a + b() + c() + v := Vertex{3, 4} + fmt.Println(v.Abs()) +} diff --git a/test/go_test_repo/cg.dot b/test/go_test_repo/cg.dot new file mode 100644 index 00000000000..58861dbba42 --- /dev/null +++ b/test/go_test_repo/cg.dot @@ -0,0 +1,3 @@ +digraph go_test_repo { + A -> B -> C; +} \ No newline at end of file diff --git a/test/test_analytics/test_analyzer_go.py b/test/test_analytics/test_analyzer_go.py new file mode 100644 index 00000000000..d2a77e130fa --- /dev/null +++ b/test/test_analytics/test_analyzer_go.py @@ -0,0 +1,82 @@ +import os +import time +import pytest +import subprocess +from persper.analytics.graph_server import GO_FILENAME_REGEXES +from persper.analytics.go import GoGraphServer +from persper.analytics.analyzer import Analyzer +from persper.util.path import root_path + +# TODO: Use a port other than the default 8080 in case of collision +server_port = 8080 + + +@pytest.fixture(scope='module') +def az(): + """ Build the test repo if not already exists + + Args: + repo_path - A string, path to the to-be-built test repo + script_path - A string, path to the repo creator script + test_src_path - A string, path to the dir to be passed to repo creator + """ + repo_path = os.path.join(root_path, 'repos/go_test_repo') + script_path = os.path.join(root_path, 'tools/repo_creater/create_repo.py') + test_src_path = os.path.join(root_path, 'test/go_test_repo') + server_addr = 'http://localhost:%d' % server_port + + if not os.path.isdir(repo_path): + cmd = '{} {}'.format(script_path, test_src_path) + subprocess.call(cmd, shell=True) + + return Analyzer(repo_path, GoGraphServer(server_addr, GO_FILENAME_REGEXES)) + + +def test_analzyer_go(az): + az._graph_server.reset_graph() + az.analyze() + ccgraph = az.get_graph() + + history_truth = { + 'C': {'Abs': 5, + 'funcA': 0, + 'funcB': 1, + 'main': 0}, + 'B': {'Abs': 3, + 'funcA': 0, + 'funcB': 3, + 'main': 5}, + 'A': {'Abs': 3, + 'funcA': 3, + 'main': 6} + } + + commits = ccgraph.commits() + for func, data in ccgraph.nodes(data=True): + history = data['history'] + for cindex, csize in history.items(): + commit_message = commits[int(cindex)]['message'] + assert(csize == history_truth[commit_message.strip()][func]) + + edges_added_by_A = set([ + ('Abs', 'Sqrt'), + ('funcA', 'Println'), + ('main', 'a'), + ('main', 'Println'), + ('main', 'Abs'), + ]) + + edges_added_by_B = set([ + ('Abs', 'funcA'), + ('funcB', 'Println'), + ('main', 'b'), + ('main', 'c'), + ]) + + edges_added_by_C = set([ + ('Abs', 'a'), + ('funcB', 'funcA') + ]) + + all_edges = edges_added_by_A.union(edges_added_by_B).union(edges_added_by_C) + assert(set(az._graph_server.get_graph().edges()) == all_edges)