Merge lp:~ben-hutchings/ensoft-sextant/rel-merge into lp:ensoft-sextant

Proposed by Ben Hutchings
Status: Merged
Approved by: Robert
Approved revision: 63
Merged at revision: 34
Proposed branch: lp:~ben-hutchings/ensoft-sextant/rel-merge
Merge into: lp:ensoft-sextant
Diff against target: 1428 lines (+510/-441)
11 files modified
resources/sextant/web/interface.html (+10/-3)
resources/sextant/web/queryjavascript.js (+9/-4)
src/sextant/db_api.py (+247/-102)
src/sextant/export.py (+9/-5)
src/sextant/objdump_parser.py (+46/-12)
src/sextant/test_db.py (+97/-0)
src/sextant/test_db_api.py (+0/-275)
src/sextant/test_parser.py (+21/-17)
src/sextant/test_resources/parser_test.dump (+25/-0)
src/sextant/update_db.py (+3/-3)
src/sextant/web/server.py (+43/-20)
To merge this branch: bzr merge lp:~ben-hutchings/ensoft-sextant/rel-merge
Reviewer Review Type Date Requested Status
Robert Approve
Review via email: mp+242762@code.launchpad.net

This proposal supersedes a proposal from 2014-11-21.

Commit message

Adds a feature to Sextant that allows the user to filter the results so that you only show the callgraph within a particular file.

Description of the change

For merge

Markups done.

To post a comment you must log in.
Revision history for this message
Robert (rjwills) :
review: Approve
Revision history for this message
Robert (rjwills) wrote :

Adds a feature to Sextant that allows the user to filter the results so that you only show the callgraph within a particular file.

Revision history for this message
Robert (rjwills) wrote :

> Adds a feature to Sextant that allows the user to filter the results so that
> you only show the callgraph within a particular file.

This comment was added in error, it was supposed to be the commit comment.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'resources/sextant/web/interface.html'
2--- resources/sextant/web/interface.html 2014-11-13 10:47:34 +0000
3+++ resources/sextant/web/interface.html 2014-11-25 11:40:53 +0000
4@@ -27,8 +27,8 @@
5 All functions calling specific function</option>
6 <option value="functions_called_by">
7 All functions called by a specific function</option>
8- <!--option value="all_call_paths"> REMOVED AS THIS IS SLOW FOR IOS
9- All function call paths between two functions</option-->
10+ <option value="all_call_paths">
11+ All function call paths between two functions</option>
12 <option value="shortest_call_path">
13 Shortest path between two functions</option>
14 <option value="function_names">
15@@ -39,7 +39,14 @@
16 <input type="checkbox" id="suppress_common" value="True"></input>
17 Suppress common functions?
18 </label>
19-
20+ <label>
21+ <input type="checkbox" id="limit_internal" value="True"></input>
22+ Limit to internal calls?
23+ </label>
24+ <label>
25+ <input type="number" id="max_depth" value="1" min="0" max="10"></input>
26+ Maximum call depth.
27+ </label>
28 <button class="button" style="float:right; margin: 1px 20px -1px 0;" onclick="execute_query()">Run Query</button>
29 </div>
30 <div id="toolbar-row2" style="margin-left: 234px;">
31
32=== modified file 'resources/sextant/web/queryjavascript.js'
33--- resources/sextant/web/queryjavascript.js 2014-11-21 12:50:51 +0000
34+++ resources/sextant/web/queryjavascript.js 2014-11-25 11:40:53 +0000
35@@ -157,16 +157,21 @@
36 } else {
37 //If not function names we will want a graph as an output;
38 //url returns svg file of graph.
39- // We use a random number argument to prevent caching.
40+ // We use a random number argument to prevent caching.
41 var string = "/output_graph.svg?stop_cache=" + String(Math.random()) + "&program_name=" +
42 document.getElementById("program_name").value +
43- "&query=" + query_id + "&func1=";
44+ "&query=" + query_id + "&function_calling=";
45 string = string + document.getElementById("function_1").value +
46- "&func2=" + document.getElementById("function_2").value;
47+ "&function_called=" + document.getElementById("function_2").value;
48 string = string + "&suppress_common=" +
49 document.getElementById('suppress_common').checked.toString();
50+ string = string + "&limit_internal=" +
51+ document.getElementById('limit_internal').checked.toString();
52+ string = string + "&max_depth=" +
53+ document.getElementById('max_depth').value.toString();
54+ }
55+
56
57- }
58 var xmlhttp = new XMLHttpRequest();
59 xmlhttp.open("GET", string, true);
60 xmlhttp.send();
61
62=== modified file 'src/sextant/db_api.py'
63--- src/sextant/db_api.py 2014-11-19 10:40:24 +0000
64+++ src/sextant/db_api.py 2014-11-25 11:40:53 +0000
65@@ -159,28 +159,43 @@
66 tmp_path = os.path.join(self._tmp_dir, '{}_{{}}'.format(program_name))
67
68 self.func_writer = CSVWriter(tmp_path.format('funcs'),
69- headers=['name', 'type', 'file'],
70+ headers=('name', 'type', 'file'),
71 max_rows=5000)
72 self.call_writer = CSVWriter(tmp_path.format('calls'),
73- headers=['caller', 'callee'],
74+ headers=('caller', 'callee', 'is_internal'),
75 max_rows=5000)
76
77 # Define the queries we use to upload the functions and calls.
78- self.add_func_query = (' USING PERIODIC COMMIT 250'
79+ self.add_func_query = (
80+ ' USING PERIODIC COMMIT 250'
81 ' LOAD CSV WITH HEADERS FROM "file:{}" AS line'
82 ' WITH line, toInt(line.id) as lineid'
83 ' MATCH (n:program {{name: "{}"}})'
84 ' CREATE (n)-[:subject]->(m:func {{name: line.name,'
85 ' id: lineid, type: line.type, file: line.file}})')
86
87- self.add_call_query = (' USING PERIODIC COMMIT 250'
88- ' LOAD CSV WITH HEADERS FROM "file:{}" AS line'
89- ' MATCH (p:program {{name: "{}"}})'
90- ' MATCH (p)-[:subject]->(n:func {{name: line.caller}})'
91- ' USING INDEX n:func(name)'
92- ' MATCH (p)-[:subject]->(m:func {{name: line.callee}})'
93- ' USING INDEX m:func(name)'
94- ' CREATE (n)-[r:calls]->(m)')
95+ self.add_internal_call_query = (
96+ ' USING PERIODIC COMMIT 250'
97+ ' LOAD CSV WITH HEADERS FROM "file:{}" AS line'
98+ ' WITH line WHERE line.is_internal = "True"'
99+ ' MATCH (p:program {{name: "{}"}})'
100+ ' MATCH (p)-[:subject]->(n:func {{name: line.caller}})'
101+ ' USING INDEX n:func(name)'
102+ ' MATCH (p)-[:subject]->(m:func {{name: line.callee}})'
103+ ' USING INDEX m:func(name)'
104+ ' CREATE (n)-[r:internal]->(m)')
105+
106+ self.add_external_call_query = (
107+ ' USING PERIODIC COMMIT 250'
108+ ' LOAD CSV WITH HEADERS FROM "file:{}" AS line'
109+ ' WITH line WHERE line.is_internal <> "True"'
110+ ' MATCH (p:program {{name: "{}"}})'
111+ ' MATCH (p)-[:subject]->(n:func {{name: line.caller}})'
112+ ' USING INDEX n:func(name)'
113+ ' MATCH (p)-[:subject]->(m:func {{name: line.callee}})'
114+ ' USING INDEX m:func(name)'
115+ ' CREATE (n)-[r:external]->(m)')
116+
117
118 self.add_program_query = ('CREATE (p:program {{name: "{}", uploader: "{}", '
119 ' uploader_id: "{}", date: "{}",'
120@@ -221,7 +236,7 @@
121 """
122 self.func_writer.write(name, typ, source)
123
124- def add_call(self, caller, callee):
125+ def add_call(self, caller, callee, is_internal=False):
126 """
127 Add a function call.
128
129@@ -230,8 +245,11 @@
130 The name of the function making the call.
131 callee:
132 The name of the function called.
133+ is_internal:
134+ True if the caller's source file is the same as callee's,
135+ unless either one is 'unknown'.
136 """
137- self.call_writer.write(caller, callee)
138+ self.call_writer.write(caller, callee, is_internal)
139
140
141 def _copy_local_to_remote_tmp_dir(self):
142@@ -257,7 +275,6 @@
143 remote_paths:
144 A list of the paths of the remote fils.
145 """
146-
147 def try_rmdir(path):
148 # Helper function to try and remove a directory, silently
149 # fail if it contains files, otherwise raise the exception.
150@@ -270,6 +287,7 @@
151 else:
152 raise e
153
154+
155 print('Cleaning temporary files...', end='')
156 file_paths = list(itertools.chain(self.func_writer.file_iter(),
157 self.call_writer.file_iter()))
158@@ -279,7 +297,7 @@
159
160 try_rmdir(self._tmp_dir)
161 try_rmdir(TMP_DIR)
162-
163+
164 self._ssh.remove_from_tmp_dir(remote_paths)
165
166 print('done.')
167@@ -335,10 +353,12 @@
168 func_count, call_count))
169 tx.commit()
170
171- # Create the functions.
172- for files, query, descr in zip((remote_funcs, remote_calls),
173- (self.add_func_query, self.add_call_query),
174- ('funcs', 'calls')):
175+ # Add the functions and internal and external calls to the database.
176+ fqds = ((remote_funcs, self.add_func_query, 'functions'),
177+ (remote_calls, self.add_internal_call_query, 'internal calls'),
178+ (remote_calls, self.add_external_call_query, 'external calls'))
179+
180+ for files, query, descr in fqds:
181 start = time()
182 for i, path in enumerate(files):
183 completed = int(100*float(i+1)/len(files))
184@@ -377,7 +397,7 @@
185 Loop over all functions: increment the called-by count of their callees.
186 """
187 for func in self.functions:
188- for called in func.functions_i_call:
189+ for called, is_internal in func.functions_i_call:
190 called.number_calling_me += 1
191
192 def _rest_node_output_to_graph(self, rest_output):
193@@ -442,8 +462,8 @@
194 for_query=True)
195 for index in result:
196 q = ("START n=node({})"
197- "MATCH n-[calls:calls]->(m)"
198- "RETURN n.name, m.name").format(result[index][2])
199+ "MATCH n-[r]->(m)"
200+ "RETURN n.name, m.name, type(r) = 'internal'").format(result[index][2])
201 new_tx.append(q)
202
203 logging.debug('exec')
204@@ -454,13 +474,17 @@
205
206 for call_list in results:
207 if call_list:
208- # call_list has element 0 being an arbitrary call this
209- # function makes; element 0 of that call is the name of the
210- # function itself. Think {{'orig', 'b'}, {'orig', 'c'}}.
211- orig = call_list[0][0]
212- # result['orig'] is [<Function>, ('callee1','callee2')]
213- result[orig][1] |= set(list(zip(*call_list.elements))[1])
214- # recall: set union is denoted by |
215+ elements = call_list.elements
216+ # elements is a list of lists of form:
217+ # [[caller1, callee1, is_internal],
218+ # [caller1, callee2, is_internal],
219+ # ...]
220+
221+ caller = elements[0][0]
222+ callees, is_internals = zip(*elements)[1:]
223+
224+ # result[caller] is [<Function>, <set of callee, is_internal tuples>]
225+ result[caller][1] |= set(zip(callees, is_internals))
226
227 else:
228 # We don't have a parent database connection.
229@@ -478,8 +502,8 @@
230 named_function = lambda name: result[name][0] if name in result else None
231
232 for function, calls, node_id in result.values():
233- what_i_call = [named_function(name)
234- for name in calls
235+ what_i_call = [(named_function(name), is_internal)
236+ for name, is_internal in calls
237 if named_function(name) is not None]
238 function.functions_i_call = what_i_call
239
240@@ -707,7 +731,7 @@
241 func_count, call_count = tx.commit()[0].elements[0]
242
243 del_call_query = ('OPTIONAL MATCH (p:program {{name: "{}"}})'
244- '-[:subject]->(f:func)-[c:calls]->()'
245+ '-[:subject]->(f:func)-[c]->()'
246 ' WITH c LIMIT 5000 DELETE c RETURN count(distinct(c))'
247 .format(program_name))
248
249@@ -817,40 +841,32 @@
250 result = self._db.query(q, returns=neo4jrestclient.Node)
251 return bool(result)
252
253- def check_function_exists(self, program_name, function_name):
254- """
255- Execute query to check whether a function with the given name exists.
256- We only check for functions which are children of a program with the
257- given program_name.
258- :param program_name: string name of the program within which to check
259- :param function_name: string name of the function to check for existence
260- :return: bool(names validate correctly, and function exists in program)
261- """
262- if not validate_query(program_name):
263- return False
264-
265- pmatch = '(:program {{name: "{}"}})'.format(program_name)
266- fmatch = '(f:func {{name: "{}"}})'.format(function_name)
267- # be explicit about index usage
268- q = (' MATCH {}-[:subject]->{} USING INDEX f:func(name)'
269- ' RETURN f LIMIT 1'.format(pmatch, fmatch))
270-
271- # result will be an empty list if the function was not found
272- result = self._db.query(q, returns=neo4jrestclient.Node)
273- return bool(result)
274-
275 def get_function_names(self, program_name, search=None, max_funcs=None):
276 """
277 Execute query to retrieve a list of all functions in the program.
278 Any of the output names can be used verbatim in any SextantConnection
279 method which requires a function-name input.
280- :param program_name: name of the program whose functions to retrieve
281- :return: None if program_name doesn't exist in the remote database,
282- a set of function-name strings otherwise.
283+
284+ Return:
285+ None if program_name doesn't exist in the remote database,
286+ a set of function-name strings otherwise.
287+ Arguments:
288+ program_name:
289+ The name of the program to query.
290+
291+ search:
292+ A string of form <name_match>:<file_match>, where at least
293+ one of name_match and file_match is provided, and each may be a
294+ comma separated list of strings containing wildcard '.*'
295+ sequences.
296+
297+ max_funcs:
298+ An integer limiting the number of functions returned by this
299+ method.
300 """
301
302 if not validate_query(program_name):
303- return set()
304+ return None
305
306 limit = "LIMIT {}".format(max_funcs) if max_funcs else ""
307
308@@ -859,8 +875,8 @@
309 ' RETURN f.name {}').format(program_name, limit)
310 else:
311 q = (' MATCH (:program {{name: "{}"}})-[:subject]->(f:func)'
312- ' WHERE f.name =~ ".*{}.*" RETURN f.name {}'
313- .format(program_name, search, limit))
314+ ' {} RETURN f.name {}'
315+ .format(program_name, self.get_query('f', search), limit))
316 return {func[0] for func in self._db.query(q)}
317
318 @staticmethod
319@@ -888,6 +904,7 @@
320 etc.
321
322 """
323+
324 if ':' in search:
325 func_subs, file_subs = search.split(':')
326 else:
327@@ -928,49 +945,134 @@
328
329 return query_str
330
331- def get_all_functions_called(self, program_name, function_calling):
332- """
333- Execute query to find all functions called by a function (indirectly).
334- If the given function is not present in the program, returns None;
335- likewise if the program_name does not exist.
336- :param program_name: a string name of the program we wish to query under
337- :param function_calling: string name of a function whose children to find
338- :return: FunctionQueryResult, maximal subgraph rooted at function_calling
339- """
340+ def get_all_functions_called(self, program_name, function_calling,
341+ limit_internal=False, max_depth=0):
342+ """
343+ Return the subtrees of the callgraph rooted at the specified functions.
344+
345+ Optionally limit to a maximum call depth, or to only internal (same
346+ source file) calls. In the case of the latter, also include a single
347+ extra hop into external functions.
348+
349+ If the function has no calls, it will be returned alone.
350+
351+ Return:
352+ None if program_name doesn't exist in the remote database,
353+ a FunctionQueryResult containing the nodes and relationships
354+ otherwise.
355+ Arguments:
356+ program_name:
357+ The name of the program to query.
358+
359+ function_calling:
360+ A string of form <name_match>:<file_match>, where at least
361+ one of name_match and file_match is provided, and each may be a
362+ comma separated list of strings containing wildcard '.*'
363+ sequences. Specifies the list of subtree roots.
364+
365+ limit_internal:
366+ If true, only explore internal calls, but also add one extra
367+ level (above max_depth) along external calls.
368+
369+ max_depth:
370+ An integer which will limit the depth
371+ of the subtrees. '0' corresponds to unlimited depth.
372+ """
373+
374+ if not validate_query(program_name):
375+ return None
376+
377 q = (' MATCH (p:program {{name: "{}"}})-[:subject]->(f:func) {}'
378- ' MATCH (f)-[:calls]->(g:func) RETURN distinct f, g'
379- .format(program_name, SextantConnection.get_query('f', function_calling)))
380+ ' MATCH (f)-[{}*0..{}]->(g:func)'
381+ .format(program_name, SextantConnection.get_query('f', function_calling),
382+ ':internal' if limit_internal else '',
383+ max_depth or ''))
384+
385+ if limit_internal:
386+ q += (' WITH f, g MATCH (g)-[:external*0..1]->(h)'
387+ ' RETURN distinct f, h')
388+ else:
389+ q += ' RETURN distinct f, g'
390
391 return self._execute_query(program_name, q)
392
393- def get_all_functions_calling(self, program_name, function_called):
394- """
395- Execute query to find all functions which call a function (indirectly).
396- If the given function is not present in the program, returns None;
397- likewise if the program_name does not exist.
398- :param program_name: a string name of the program we wish to query
399- :param function_called: string name of a function whose parents to find
400- :return: FunctionQueryResult, maximal connected subgraph with leaf function_called
401- """
402+ def get_all_functions_calling(self, program_name, function_called,
403+ limit_internal=False, max_depth=1):
404+ """
405+ Return functions calling the specified functions.
406+
407+ Optionally limit to a maximum call depth, or to only internal (same
408+ source file) calls.
409+
410+ If the function is not called, return it alone.
411+
412+ Return:
413+ None if program_name doesn't exist in the remote database,
414+ a FunctionQueryResult containing the nodes and relationships
415+ otherwise.
416+ Arguments:
417+ program_name:
418+ The name of the program to query.
419+
420+ function_called:
421+ A string of form <name_match>:<file_match>, where at least
422+ one of name_match and file_match is provided, and each may be a
423+ comma separated list of strings containing wildcard '.*'
424+ sequences. Specifies the list of functions to match.
425+
426+ limit_internal:
427+ If true, only explore internal calls.
428+
429+ max_depth:
430+ An integer which will limit the depth
431+ of the subtrees. '0' corresponds to unlimited depth.
432+ """
433+
434+ if not validate_query(program_name):
435+ return None
436
437 q = (' MATCH (p:program {{name: "{}"}})-[:subject]->(g:func) {}'
438- ' MATCH (f)-[:calls]->(g)'
439+ ' MATCH (f)-[{}*0..{}]->(g)'
440 ' RETURN distinct f, g')
441- q = q.format(program_name, SextantConnection.get_query('g', function_called), program_name)
442+ q = q.format(program_name, SextantConnection.get_query('g', function_called),
443+ ':internal' if limit_internal else ':internal|external',
444+ max_depth or '')
445
446 return self._execute_query(program_name, q)
447
448- def get_call_paths(self, program_name, function_calling, function_called):
449- """
450- Execute query to find all possible routes between two specific nodes.
451- If the given functions are not present in the program, returns None;
452- ditto if the program_name does not exist.
453- :param program_name: string program name
454- :param function_calling: string
455- :param function_called: string
456- :return: FunctionQueryResult, the union of all subgraphs reachable by
457- adding a source at function_calling and a sink at function_called.
458- """
459+ def get_call_paths(self, program_name, function_calling, function_called,
460+ limit_internal=False, max_depth=1):
461+ """
462+ Return all call paths between the sets of functions specified by the
463+ search strings function_calling and function_called.
464+
465+ Optionally limit to a maximum call depth, or to only internal (same
466+ source file) calls.
467+
468+ Return:
469+ None if program_name doesn't exist in the remote database,
470+ a FunctionQueryResult object containing the nodes and relationships
471+ otherwise.
472+ Arguments:
473+ program_name:
474+ The name of the program to query.
475+
476+ function_calling, function_called:
477+ A string of form <name_match>:<file_match>, where at least
478+ one of name_match and file_match is provided, and each may be a
479+ comma separated list of strings containing wildcard '.*'
480+ sequences. Specifies the list of functions to match.
481+
482+ limit_internal:
483+ If true, only explore internal calls.
484+
485+ max_depth:
486+ An integer which will limit the depth
487+ of the subtrees. '0' corresponds to unlimited depth.
488+ """
489+
490+ if not validate_query(program_name):
491+ return None
492
493 if not self.check_program_exists(program_name):
494 return None
495@@ -981,18 +1083,26 @@
496 q = (' MATCH (p:program {{name: "{}"}})'
497 ' MATCH (p)-[:subject]->(start:func) {} WITH start, p'
498 ' MATCH (p)-[:subject]->(end:func) {} WITH start, end'
499- ' MATCH path=(start)-[:calls*]->(end)'
500+ ' MATCH path=(start)-[{}*0..{}]->(end)'
501 ' WITH DISTINCT nodes(path) AS result'
502 ' UNWIND result AS answer'
503 ' RETURN answer')
504- q = q.format(program_name, start_q, end_q)
505+ q = q.format(program_name, start_q, end_q,
506+ ':internal' if limit_internal else '',
507+ max_depth or '')
508 return self._execute_query(program_name, q)
509
510 def get_whole_program(self, program_name):
511- """Execute query to find the entire program with a given name.
512- If the program is not present in the remote database, returns None.
513- :param: program_name: a string name of the program we wish to return.
514- :return: a FunctionQueryResult consisting of the program graph.
515+ """
516+ Return the full call graph of the program.
517+
518+ Return:
519+ None if program_name doesn't exist in the remote database,
520+ a FunctionQueryResult object containing all of the program nodes
521+ and calls otherwise.
522+ Arguments:
523+ program_name:
524+ The name of the program to query.
525 """
526
527 if not self.check_program_exists(program_name):
528@@ -1002,7 +1112,9 @@
529 ' RETURN (f)'.format(program_name))
530 return self._execute_query(program_name, q)
531
532- def get_shortest_path_between_functions(self, program_name, function_calling, function_called):
533+ def get_shortest_path_between_functions(self, program_name,
534+ function_calling, function_called,
535+ limit_internal=False, max_depth=0):
536 """
537 Execute query to get a single, shortest, path between two functions.
538 :param program_name: string name of the program we wish to search under
539@@ -1010,6 +1122,37 @@
540 :param func2: the name of the function at which to terminate the path
541 :return: FunctionQueryResult shortest path between func1 and func2.
542 """
543+ """
544+ Return all shortest paths between the sets of functions specified.
545+
546+ Optionally limit to a maximum call depth, or to only internal (same
547+ source file) calls.
548+
549+ Return:
550+ None if program_name doesn't exist in the remote database,
551+ a FunctionQueryResult object containing the nodes and relationships
552+ otherwise.
553+ Arguments:
554+ program_name:
555+ The name of the program to query.
556+
557+ function_calling, function_called:
558+ A string of form <name_match>:<file_match>, where at least
559+ one of name_match and file_match is provided, and each may be a
560+ comma separated list of strings containing wildcard '.*'
561+ sequences. Specifies the list of functions to match.
562+
563+ limit_internal:
564+ If true, only explore internal calls.
565+
566+ max_depth:
567+ A STRING containing an integer which will limit the depth
568+ of the subtrees. '0' corresponds to unlimited depth.
569+ """
570+
571+ if not self.check_program_exists(program_name):
572+ return None
573+
574 if not self.check_program_exists(program_name):
575 return None
576
577@@ -1019,10 +1162,12 @@
578 q = (' MATCH (p:program {{name: "{}"}})'
579 ' MATCH (p)-[:subject]->(start:func) {} WITH start, p'
580 ' MATCH (p)-[:subject]->(end:func) {} WITH start, end'
581- ' MATCH path=shortestPath((start)-[:calls*]->(end))'
582+ ' MATCH path=allShortestPaths((start)-[{}*..{}]->(end))'
583 ' UNWIND nodes(path) AS answer'
584 ' RETURN answer')
585- q = q.format(program_name, start_q, end_q)
586+ q = q.format(program_name, start_q, end_q,
587+ ':internal' if limit_internal else '',
588+ max_depth or '')
589
590 return self._execute_query(program_name, q)
591
592
593=== modified file 'src/sextant/export.py'
594--- src/sextant/export.py 2014-11-19 10:32:34 +0000
595+++ src/sextant/export.py 2014-11-25 11:40:53 +0000
596@@ -71,9 +71,10 @@
597 if func_called == func:
598 functions_called.remove(func_called)
599
600- for func_called in func.functions_i_call:
601+ for func_called, is_internal in func.functions_i_call:
602 if not (suppress_common_nodes and func_called.is_common):
603- output_str += ' "{}" -> "{}"\n'.format(func.name, func_called.name)
604+ color = 'black' if is_internal else 'red'
605+ output_str += ' "{}" -> "{}" [color={}]\n'.format(func.name, func_called.name, color)
606
607 output_str += '}'
608 return output_str
609@@ -140,18 +141,21 @@
610 </node>\n""".format(func.name, 20, len(display_func)*8, colour, display_func)
611
612 functions_called = func.functions_i_call
613+ print(functions_called)
614 if remove_self_calls is True:
615 #remove calls where a function calls itself
616- for func_called in functions_called:
617+ for func_called, is_internal, in functions_called:
618 if func_called == func:
619 functions_called.remove(func_called)
620
621- for callee in functions_called:
622+ for callee, is_internal in functions_called:
623+ print(callee, is_internal)
624+ color = "#000000" if is_internal else "#ff0000"
625 if callee not in commonly_called:
626 if not(suppress_common_nodes and callee.is_common):
627 output_str += """<edge source="{}" target="{}"> <data key="d9">
628 <y:PolyLineEdge>
629- <y:LineStyle color="#000000" type="line" width="1.0"/>
630+ <y:LineStyle color="#ff0000" type="line" width="1.0"/>
631 <y:Arrows source="none" target="standard"/>
632 <y:BendStyle smoothed="false"/>
633 </y:PolyLineEdge>
634
635=== modified file 'src/sextant/objdump_parser.py'
636--- src/sextant/objdump_parser.py 2014-11-21 15:19:15 +0000
637+++ src/sextant/objdump_parser.py 2014-11-25 11:40:53 +0000
638@@ -43,11 +43,15 @@
639 function_ptr_count:
640 The number of function pointers that have been detected.
641 _known_functions:
642- A set of the names of functions that have been
643- parsed - used to avoid registering a function multiple times.
644+ A dict of the names of functions that have been
645+ parsed - used to avoid registering a function multiple times
646+ and to label calls as internal/external.
647 _partial_functions:
648 A set of functions whose names we have seen but whose source
649 files we don't yet know.
650+ _partial_calls:
651+ A set of the (caller, callee) tuples representing calls between
652+ a _partial_function and another function.
653
654 """
655 def __init__(self, file_path, file_object=None,
656@@ -106,16 +110,18 @@
657 self.function_ptr_count = 0
658
659 # Avoid adding duplicate functions.
660- self._known_functions = set()
661+ self._known_functions = dict()
662+ self._known_calls = set()
663 # Set of partially-parsed functions.
664 self._partial_functions = set()
665+ self._partial_calls = set()
666
667 # By default print information to stdout.
668 def print_func(name, typ, source='unknown'):
669- print('func {:25}{:15}{}'.format(name, typ, source))
670+ print('func {:25} {:15}{}'.format(name, typ, source))
671
672- def print_call(caller, callee):
673- print('call {:25}{:25}'.format(caller, callee))
674+ def print_call(caller, callee, is_internal):
675+ print('call {:25} {:25}'.format(caller, callee))
676
677 def print_started(parser):
678 print('parse started: {}[{}]'.format(self.path, ', '.join(self.sections)))
679@@ -148,6 +154,7 @@
680 self._partial_functions.add(name)
681 elif source == 'unknown':
682 # Manually adding a stub function.
683+ self._known_functions[name] = source
684 self.add_function(name, 'stub', source)
685 self.function_count += 1
686 elif name not in self._known_functions:
687@@ -160,7 +167,7 @@
688 except KeyError:
689 pass
690
691- self._known_functions.add(name)
692+ self._known_functions[name] = source
693 self.add_function(name, 'normal', source)
694 self.function_count += 1
695
696@@ -168,15 +175,33 @@
697 """
698 Add a function pointer.
699 """
700- self.add_function(name, 'pointer')
701+ self.add_function(name, 'pointer', 'unknown')
702 self.function_count += 1
703
704- def _add_call(self, caller, callee):
705+ def _add_call(self, caller, callee, force=False):
706 """
707 Add a function call from caller to callee.
708 """
709- self.add_call(caller, callee)
710- self.call_count += 1
711+ if (caller, callee) in self._known_calls:
712+ return
713+
714+ try:
715+ files = self._known_functions
716+ is_internal = (callee.startswith('func_ptr_')
717+ or (files[caller] != 'unknown'
718+ and files[caller] == files[callee]))
719+ self.add_call(caller, callee, is_internal)
720+ self._known_calls.add((caller, callee))
721+ self.call_count += 1
722+ except KeyError:
723+ if force:
724+ self._add_function(callee, 'unknown')
725+ self.add_call(caller, callee, False)
726+ self._known_calls.add((caller, callee))
727+ self.call_count += 1
728+ print(caller, callee)
729+ else:
730+ self._partial_calls.add((caller, callee))
731
732 def parse(self):
733 """
734@@ -193,6 +218,10 @@
735 if to_add:
736 file_line = line.startswith('/')
737 source = line.split(':')[0] if file_line else None
738+ if source:
739+ # Prune out the relative parts of the filepath.
740+ source = source.rsplit('./', 1)[-1]
741+
742 self._add_function(current_function, source)
743 to_add = False
744
745@@ -227,6 +256,10 @@
746 # Flag function - we look for source on the next line.
747 to_add = True
748
749+ # If we have come to a new current_function, then we can
750+ # forget about the calls we knew about from the last.
751+ self._known_calls = set()
752+
753 elif 'call ' in line or 'callq ' in line:
754 # WHITESPACE to prevent picking up function names
755 # containing 'call'
756@@ -268,7 +301,8 @@
757
758 for name in self._partial_functions:
759 self._add_function(name, 'unknown')
760-
761+ for call in sorted(self._partial_calls, key=lambda el: el[0]):
762+ self._add_call(*call, force=True)
763
764 self.finished()
765
766
767=== added file 'src/sextant/test_db.py'
768--- src/sextant/test_db.py 1970-01-01 00:00:00 +0000
769+++ src/sextant/test_db.py 2014-11-25 11:40:53 +0000
770@@ -0,0 +1,97 @@
771+#!/usr/bin/python
772+# -----------------------------------------
773+# Sextant
774+# Copyright 2014, Ensoft Ltd.
775+# Author: Patrick Stevens, James Harkin
776+# -----------------------------------------
777+#Testing module
778+
779+import unittest
780+
781+import db_api
782+import update_db
783+
784+PNAME = 'tester-parser_test'
785+NORMAL = {'main', 'normal', 'wierd$name', 'duplicates'}
786+
787+
788+class TestFunctionQueryResults(unittest.TestCase):
789+ @classmethod
790+ def setUpClass(cls):
791+ # we need to set up the remote database by using the neo4j_input_api
792+ cls.remote_url = 'http://ensoft-sandbox:7474'
793+ cls.connection = db_api.SextantConnection('ensoft-sandbox', 7474)
794+
795+ update_db.upload_program
796+ update_db.upload_program(cls.connection, 'tester', 'test_resources/parser_test',
797+ program_name=None, not_object_file=False, add_file_paths=True)
798+
799+ @classmethod
800+ def tearDownClass(cls):
801+ cls.connection.delete_program('tester-parser_test')
802+ cls.connection.close()
803+
804+ def test_get_function_names(self):
805+ get_names = self.connection.get_function_names
806+ # Test getting all names
807+ names = get_names(PNAME)
808+ # Test file wildcard search
809+ parser_names = get_names(PNAME, search=':.*parser_test.c')
810+
811+ self.assertTrue(names.issuperset(NORMAL))
812+ self.assertEquals(len(names), 24)
813+ self.assertEquals(parser_names, {u'main', u'normal', u'duplicates', u'wierd$name'})
814+
815+ # Test the wildcard matching
816+ search = self.connection.get_function_names(PNAME, search='.*libc.*')
817+ search_exp = {u'__libc_csu_init', u'__libc_csu_fini', u'__libc_start_main'}
818+
819+ self.assertEquals(search, search_exp)
820+
821+ # Test the limiting
822+ too_many = self.connection.get_function_names(PNAME, max_funcs=3)
823+ self.assertEquals(len(too_many), 3)
824+
825+ # Test empty for non-existant program
826+ self.assertFalse(self.connection.get_function_names('blah blah blah'))
827+
828+ def test_get_all_functions_called(self):
829+ get_fns = self.connection.get_all_functions_called
830+
831+ for depth, num in zip([0, 1, 2, 3], [8, 3, 8, 8]):
832+ result = get_fns(PNAME, 'main', False, depth).functions
833+ self.assertEquals(len(result), num, str(result))
834+
835+ for depth, num in zip([0, 1, 2, 3], [8, 4, 8, 8]):
836+ # Limit to internal functions
837+ # TODO this isn't a great test - need greater call depth
838+ result = get_fns(PNAME, 'main', True, depth).functions
839+ self.assertEquals(len(result), num)
840+
841+ def test_get_all_functions_calling(self):
842+ get_fns = self.connection.get_all_functions_calling
843+
844+ for depth, num in zip(range(3), [4, 3, 4, 4]):
845+ result = get_fns(PNAME, 'printf', limit_internal=False, max_depth=depth).functions
846+ self.assertEquals(len(result), num)
847+
848+ for depth, num in zip(range(3), [1, 1, 1, 1]):
849+ result = get_fns(PNAME, 'printf', limit_internal=True, max_depth=depth).functions
850+ self.assertEquals(len(result), num)
851+
852+ def test_get_all_paths_between(self):
853+ get_paths = self.connection.get_call_paths
854+
855+ result = {f.name for f in get_paths(PNAME, 'main', 'wierd$name', True, 0).functions}
856+ exp = {'main', 'normal', 'duplicates', 'wierd$name'}
857+ self.assertEquals(result, exp)
858+
859+ def test_get_shortest_paths_between(self):
860+ get_paths = self.connection.get_shortest_path_between_functions
861+
862+ result = {f.name for f in get_paths(PNAME, 'main', 'wierd$name', True, 0).functions}
863+ exp = {u'main', u'normal', u'wierd$name'}
864+ self.assertEquals(result, exp)
865+
866+if __name__ == '__main__':
867+ unittest.main()
868
869=== removed file 'src/sextant/test_db_api.py'
870--- src/sextant/test_db_api.py 2014-10-16 15:26:47 +0000
871+++ src/sextant/test_db_api.py 1970-01-01 00:00:00 +0000
872@@ -1,275 +0,0 @@
873-#!/usr/bin/python
874-# -----------------------------------------
875-# Sextant
876-# Copyright 2014, Ensoft Ltd.
877-# Author: Patrick Stevens, James Harkin
878-# -----------------------------------------
879-#Testing module
880-
881-import unittest
882-
883-from db_api import Function
884-from db_api import FunctionQueryResult
885-from db_api import SextantConnection
886-from db_api import validate_query
887-
888-
889-class TestFunctionQueryResults(unittest.TestCase):
890- @classmethod
891- def setUpClass(cls):
892- # we need to set up the remote database by using the neo4j_input_api
893- cls.remote_url = 'http://ensoft-sandbox:7474'
894-
895- cls.setter_connection = SextantConnection('ensoft-sandbox', 7474)
896-
897- cls.program_1_name = 'testprogram'
898- cls.one_node_program_name = 'testprogram1'
899- cls.empty_program_name = 'testprogramblank'
900-
901- # if anything failed before, delete programs now
902- cls.setter_connection.delete_program(cls.program_1_name)
903- cls.setter_connection.delete_program(cls.one_node_program_name)
904- cls.setter_connection.delete_program(cls.empty_program_name)
905-
906-
907- cls.upload_program = cls.setter_connection.new_program(cls.program_1_name)
908- cls.upload_program.add_function('func1')
909- cls.upload_program.add_function('func2')
910- cls.upload_program.add_function('func3')
911- cls.upload_program.add_function('func4')
912- cls.upload_program.add_function('func5')
913- cls.upload_program.add_function('func6')
914- cls.upload_program.add_function('func7')
915- cls.upload_program.add_call('func1', 'func2')
916- cls.upload_program.add_call('func1', 'func4')
917- cls.upload_program.add_call('func2', 'func1')
918- cls.upload_program.add_call('func2', 'func4')
919- cls.upload_program.add_call('func3', 'func5')
920- cls.upload_program.add_call('func4', 'func4')
921- cls.upload_program.add_call('func4', 'func5')
922- cls.upload_program.add_call('func5', 'func1')
923- cls.upload_program.add_call('func5', 'func2')
924- cls.upload_program.add_call('func5', 'func3')
925- cls.upload_program.add_call('func6', 'func7')
926-
927- cls.upload_program.commit()
928-
929- cls.upload_one_node_program = cls.setter_connection.new_program(cls.one_node_program_name)
930- cls.upload_one_node_program.add_function('lonefunc')
931-
932- cls.upload_one_node_program.commit()
933-
934- cls.upload_empty_program = cls.setter_connection.new_program(cls.empty_program_name)
935-
936- cls.upload_empty_program.commit()
937-
938- cls.getter_connection = cls.setter_connection
939-
940-
941- @classmethod
942- def tearDownClass(cls):
943- cls.setter_connection.delete_program(cls.upload_program.program_name)
944- cls.setter_connection.delete_program(cls.upload_one_node_program.program_name)
945- cls.setter_connection.delete_program(cls.upload_empty_program.program_name)
946-
947- cls.setter_connection.close()
948- del(cls.setter_connection)
949-
950- def test_17_get_call_paths(self):
951- reference1 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name)
952- reference1.functions = [Function(self.program_1_name, 'func1'), Function(self.program_1_name, 'func2'),
953- Function(self.program_1_name, 'func3'),
954- Function(self.program_1_name, 'func4'), Function(self.program_1_name, 'func5')]
955- reference1.functions[0].functions_i_call = reference1.functions[1:4:2] # func1 calls func2, func4
956- reference1.functions[1].functions_i_call = reference1.functions[0:4:3] # func2 calls func1, func4
957- reference1.functions[2].functions_i_call = [reference1.functions[4]] # func3 calls func5
958- reference1.functions[3].functions_i_call = reference1.functions[3:5] # func4 calls func4, func5
959- reference1.functions[4].functions_i_call = reference1.functions[0:3] # func5 calls func1, func2, func3
960- self.assertEquals(reference1, self.getter_connection.get_call_paths(self.program_1_name, 'func1', 'func2'))
961- self.assertIsNone(self.getter_connection.get_call_paths('not a prog', 'func1', 'func2')) # shouldn't validation
962- self.assertIsNone(self.getter_connection.get_call_paths('notaprogram', 'func1', 'func2'))
963- self.assertIsNone(self.getter_connection.get_call_paths(self.program_1_name, 'notafunc', 'func2'))
964- self.assertIsNone(self.getter_connection.get_call_paths(self.program_1_name, 'func1', 'notafunc'))
965-
966- def test_02_get_whole_program(self):
967- reference = FunctionQueryResult(parent_db=None, program_name=self.program_1_name)
968- reference.functions = [Function(self.program_1_name, 'func1'), Function(self.program_1_name, 'func2'),
969- Function(self.program_1_name, 'func3'),
970- Function(self.program_1_name, 'func4'), Function(self.program_1_name, 'func5'),
971- Function(self.program_1_name, 'func6'), Function(self.program_1_name, 'func7')]
972- reference.functions[0].functions_i_call = reference.functions[1:4:2] # func1 calls func2, func4
973- reference.functions[1].functions_i_call = reference.functions[0:4:3] # func2 calls func1, func4
974- reference.functions[2].functions_i_call = [reference.functions[4]] # func3 calls func5
975- reference.functions[3].functions_i_call = reference.functions[3:5] # func4 calls func4, func5
976- reference.functions[4].functions_i_call = reference.functions[0:3] # func5 calls func1, func2, func3
977- reference.functions[5].functions_i_call = [reference.functions[6]] # func6 calls func7
978-
979-
980- self.assertEqual(reference, self.getter_connection.get_whole_program(self.program_1_name))
981- self.assertIsNone(self.getter_connection.get_whole_program('nottherightprogramname'))
982-
983- def test_03_get_whole_one_node_program(self):
984- reference = FunctionQueryResult(parent_db=None, program_name=self.one_node_program_name)
985- reference.functions = [Function(self.one_node_program_name, 'lonefunc')]
986-
987- self.assertEqual(reference, self.getter_connection.get_whole_program(self.one_node_program_name))
988-
989- def test_04_get_whole_empty_program(self):
990- reference = FunctionQueryResult(parent_db=None, program_name=self.empty_program_name)
991- reference.functions = []
992-
993- self.assertEqual(reference, self.getter_connection.get_whole_program(self.empty_program_name))
994-
995- def test_05_get_function_names(self):
996- reference = {'func1', 'func2', 'func3', 'func4', 'func5', 'func6', 'func7'}
997- self.assertEqual(reference, self.getter_connection.get_function_names(self.program_1_name))
998-
999- def test_06_get_function_names_one_node_program(self):
1000- reference = {'lonefunc'}
1001- self.assertEqual(reference, self.getter_connection.get_function_names(self.one_node_program_name))
1002-
1003- def test_07_get_function_names_empty_program(self):
1004- reference = set()
1005- self.assertEqual(reference, self.getter_connection.get_function_names(self.empty_program_name))
1006-
1007- def test_09_validation_is_used(self):
1008- self.assertFalse(self.getter_connection.get_function_names('not alphanumeric'))
1009- self.assertFalse(self.getter_connection.get_whole_program('not alphanumeric'))
1010- self.assertFalse(self.getter_connection.check_program_exists('not alphanumeric'))
1011- self.assertFalse(self.getter_connection.check_function_exists('not alphanumeric', 'alpha'))
1012- self.assertFalse(self.getter_connection.check_function_exists('alpha', 'not alpha'))
1013- self.assertFalse(self.getter_connection.get_all_functions_called('alphaprogram', 'not alpha function'))
1014- self.assertFalse(self.getter_connection.get_all_functions_called('not alpha program', 'alphafunction'))
1015- self.assertFalse(self.getter_connection.get_all_functions_calling('not alpha program', 'alphafunction'))
1016- self.assertFalse(self.getter_connection.get_all_functions_calling('alphaprogram', 'not alpha function'))
1017- self.assertFalse(self.getter_connection.get_call_paths('not alpha program','alphafunc1', 'alphafunc2'))
1018- self.assertFalse(self.getter_connection.get_call_paths('alphaprogram','not alpha func 1', 'alphafunc2'))
1019- self.assertFalse(self.getter_connection.get_call_paths('alphaprogram','alphafunc1', 'not alpha func 2'))
1020-
1021- def test_08_get_program_names(self):
1022- reference = {self.program_1_name, self.one_node_program_name, self.empty_program_name}
1023- self.assertTrue(reference.issubset(self.getter_connection.get_program_names()))
1024-
1025-
1026- def test_11_get_all_functions_called(self):
1027- reference1 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) # this will be the 1,2,3,4,5 component
1028- reference2 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) # this will be the 6,7 component
1029- reference3 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) # this will be the 7 component
1030- reference1.functions = [Function(self.program_1_name, 'func1'), Function(self.program_1_name, 'func2'),
1031- Function(self.program_1_name, 'func3'),
1032- Function(self.program_1_name, 'func4'), Function(self.program_1_name, 'func5')]
1033- reference2.functions = [Function(self.program_1_name, 'func6'), Function(self.program_1_name, 'func7')]
1034- reference3.functions = []
1035-
1036- reference1.functions[0].functions_i_call = reference1.functions[1:4:2] # func1 calls func2, func4
1037- reference1.functions[1].functions_i_call = reference1.functions[0:4:3] # func2 calls func1, func4
1038- reference1.functions[2].functions_i_call = [reference1.functions[4]] # func3 calls func5
1039- reference1.functions[3].functions_i_call = reference1.functions[3:5] # func4 calls func4, func5
1040- reference1.functions[4].functions_i_call = reference1.functions[0:3] # func5 calls func1, func2, func3
1041-
1042- reference2.functions[0].functions_i_call = [reference2.functions[1]]
1043-
1044- self.assertEquals(reference1, self.getter_connection.get_all_functions_called(self.program_1_name, 'func1'))
1045- self.assertEquals(reference1, self.getter_connection.get_all_functions_called(self.program_1_name, 'func2'))
1046- self.assertEquals(reference1, self.getter_connection.get_all_functions_called(self.program_1_name, 'func3'))
1047- self.assertEquals(reference1, self.getter_connection.get_all_functions_called(self.program_1_name, 'func4'))
1048- self.assertEquals(reference1, self.getter_connection.get_all_functions_called(self.program_1_name, 'func5'))
1049-
1050- self.assertEquals(reference2, self.getter_connection.get_all_functions_called(self.program_1_name, 'func6'))
1051-
1052- self.assertEquals(reference3, self.getter_connection.get_all_functions_called(self.program_1_name, 'func7'))
1053-
1054- self.assertIsNone(self.getter_connection.get_all_functions_called(self.program_1_name, 'nottherightfunction'))
1055- self.assertIsNone(self.getter_connection.get_all_functions_called('nottherightprogram', 'func2'))
1056-
1057- def test_12_get_all_functions_called_1(self):
1058- reference1 = FunctionQueryResult(parent_db=None, program_name=self.one_node_program_name)
1059- reference1.functions = []
1060-
1061- d=self.getter_connection.get_all_functions_called(self.one_node_program_name, 'lonefunc')
1062- self.assertEquals(reference1, self.getter_connection.get_all_functions_called(self.one_node_program_name,
1063- 'lonefunc'))
1064- self.assertIsNone(self.getter_connection.get_all_functions_called(self.one_node_program_name,
1065- 'not the right function'))
1066- self.assertIsNone(self.getter_connection.get_all_functions_called('not the right program', 'lonefunc'))
1067-
1068- def test_13_get_all_functions_called_blank(self):
1069- reference1 = FunctionQueryResult(parent_db=None, program_name=self.empty_program_name)
1070- reference1.functions = []
1071-
1072- self.assertIsNone(self.getter_connection.get_all_functions_called(self.empty_program_name,
1073- 'not the right function'))
1074-
1075- def test_14_get_all_functions_calling(self):
1076- reference1 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) # this will be the 1,2,3,4,5 component
1077- reference2 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) # this will be the 6,7 component
1078- reference3 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) # this will be the 7 component
1079- reference1.functions = [Function(self.program_1_name, 'func1'), Function(self.program_1_name, 'func2'),
1080- Function(self.program_1_name, 'func3'),
1081- Function(self.program_1_name, 'func4'), Function(self.program_1_name, 'func5')]
1082-
1083- reference1.functions[0].functions_i_call = reference1.functions[1:4:2] # func1 calls func2, func4
1084- reference1.functions[1].functions_i_call = reference1.functions[0:4:3] # func2 calls func1, func4
1085- reference1.functions[2].functions_i_call = [reference1.functions[4]] # func3 calls func5
1086- reference1.functions[3].functions_i_call = reference1.functions[3:5] # func4 calls func4, func5
1087- reference1.functions[4].functions_i_call = reference1.functions[0:3] # func5 calls func1, func2, func3
1088-
1089- reference2.functions = [Function(self.program_1_name, 'func6'), Function(self.program_1_name, 'func7')]
1090-
1091- reference2.functions[0].functions_i_call = [reference2.functions[1]]
1092-
1093- reference3.functions = [Function(self.program_1_name, 'func6')]
1094-
1095- reference3.functions = []
1096-
1097- self.assertEquals(reference1, self.getter_connection.get_all_functions_calling(self.program_1_name, 'func1'))
1098- self.assertEquals(reference1, self.getter_connection.get_all_functions_calling(self.program_1_name, 'func2'))
1099- self.assertEquals(reference1, self.getter_connection.get_all_functions_calling(self.program_1_name, 'func3'))
1100- self.assertEquals(reference1, self.getter_connection.get_all_functions_calling(self.program_1_name, 'func4'))
1101- self.assertEquals(reference1, self.getter_connection.get_all_functions_calling(self.program_1_name, 'func5'))
1102-
1103- self.assertEquals(reference2, self.getter_connection.get_all_functions_calling(self.program_1_name,'func7'))
1104-
1105- self.assertEquals(reference3, self.getter_connection.get_all_functions_calling(self.program_1_name, 'func6'))
1106-
1107- self.assertIsNone(self.getter_connection.get_all_functions_calling(self.program_1_name, 'nottherightfunction'))
1108- self.assertIsNone(self.getter_connection.get_all_functions_calling('nottherightprogram', 'func2'))
1109-
1110- def test_15_get_all_functions_calling_one_node_prog(self):
1111- reference1 = FunctionQueryResult(parent_db=None, program_name=self.one_node_program_name)
1112- reference1.functions = []
1113- self.assertEquals(reference1, self.getter_connection.get_all_functions_calling(self.one_node_program_name,
1114- 'lonefunc'))
1115- self.assertIsNone(self.getter_connection.get_all_functions_calling(self.one_node_program_name,
1116- 'not the right function'))
1117- self.assertIsNone(self.getter_connection.get_all_functions_calling('not the right program', 'lonefunc'))
1118-
1119- def test_16_get_all_functions_calling_blank_prog(self):
1120- reference1 = FunctionQueryResult(parent_db=None, program_name=self.empty_program_name)
1121- reference1.functions=[]
1122-
1123- self.assertIsNone(self.getter_connection.get_all_functions_called(self.empty_program_name,
1124- 'not the right function'))
1125-
1126-
1127-
1128- def test_18_get_call_paths_between_two_functions_one_node_prog(self):
1129- reference = FunctionQueryResult(parent_db=None, program_name=self.one_node_program_name)
1130- reference.functions = [] # that is, reference is the empty program with name self.one_node_program_name
1131-
1132- self.assertEquals(self.getter_connection.get_call_paths(self.one_node_program_name, 'lonefunc', 'lonefunc'),
1133- reference)
1134- self.assertIsNone(self.getter_connection.get_call_paths(self.one_node_program_name, 'lonefunc', 'notafunc'))
1135- self.assertIsNone(self.getter_connection.get_call_paths(self.one_node_program_name, 'notafunc', 'notafunc'))
1136-
1137- def test_10_validator(self):
1138- self.assertFalse(validate_query(''))
1139- self.assertTrue(validate_query('thisworks'))
1140- self.assertTrue(validate_query('th1sw0rks'))
1141- self.assertTrue(validate_query('12345'))
1142- self.assertFalse(validate_query('this does not work'))
1143- self.assertTrue(validate_query('this_does_work'))
1144- self.assertFalse(validate_query("'")) # string consisting of a single quote mark
1145-
1146-if __name__ == '__main__':
1147- unittest.main()
1148
1149=== modified file 'src/sextant/test_parser.py'
1150--- src/sextant/test_parser.py 2014-11-05 16:09:16 +0000
1151+++ src/sextant/test_parser.py 2014-11-25 11:40:53 +0000
1152@@ -11,20 +11,20 @@
1153 def setUp(self):
1154 pass
1155
1156- def add_function(self, dct, name, typ):
1157+ def add_function(self, dct, name, typ, source):
1158 self.assertFalse(name in dct, "duplicate function added: {} into {}".format(name, dct.keys()))
1159- dct[name] = typ
1160+ dct[name] = (typ, source)
1161
1162- def add_call(self, dct, caller, callee):
1163- dct[caller].append(callee)
1164+ def add_call(self, dct, caller, callee, is_internal):
1165+ dct[caller].append((callee, is_internal))
1166
1167 def do_parse(self, path=DUMP_FILE, sections=['.text'], ignore_ptrs=False):
1168 functions = {}
1169 calls = defaultdict(list)
1170
1171 # set the Parser to put output in local dictionaries
1172- add_function = lambda n, t, s='unknown': self.add_function(functions, n, t)
1173- add_call = lambda a, b: self.add_call(calls, a, b)
1174+ add_function = lambda n, t, s: self.add_function(functions, n, t, s)
1175+ add_call = lambda a, b, i: self.add_call(calls, a, b, i)
1176
1177 p = parser.Parser(path, sections=sections, ignore_ptrs=ignore_ptrs,
1178 add_function=add_function, add_call=add_call)
1179@@ -43,12 +43,16 @@
1180 # ensure that the correct functions are listed with the correct types
1181 res, funcs, calls = self.do_parse()
1182
1183- for name, typ in zip(['normal', 'duplicates', 'wierd$name', 'printf', 'func_ptr_3'],
1184- ['normal', 'normal', 'normal', 'stub', 'pointer']):
1185+ known = 'parser_test.c'
1186+ unknown = 'unknown'
1187+
1188+ for name, typ, fle in zip(['normal', 'duplicates', 'wierd$name', 'printf', 'func_ptr_3'],
1189+ ['normal', 'normal', 'normal', 'stub', 'pointer'],
1190+ [known, known, known, unknown, unknown]):
1191 self.assertTrue(name in funcs, "'{}' not found in function dictionary".format(name))
1192- self.assertEquals(funcs[name], typ)
1193+ self.assertEquals(funcs[name][0], typ)
1194+ self.assertTrue(funcs[name][1].endswith(fle))
1195
1196- self.assertFalse('__gmon_start__' in funcs, "don't see a function defined in .plt")
1197
1198 def test_no_ptrs(self):
1199 # ensure that the ignore_ptrs flags is working
1200@@ -61,17 +65,17 @@
1201 def test_calls(self):
1202 res, funcs, calls = self.do_parse()
1203
1204- self.assertTrue('normal' in calls['main'])
1205- self.assertTrue('duplicates' in calls['main'])
1206+ self.assertTrue(('normal', True) in calls['main'])
1207+ self.assertTrue(('duplicates', True) in calls['main'])
1208
1209 normal_calls = sorted(['wierd$name', 'printf', 'func_ptr_3'])
1210- self.assertEquals(sorted(calls['normal']), normal_calls)
1211+ self.assertEquals(sorted(zip(*calls['normal'])[0]), normal_calls)
1212
1213- self.assertEquals(calls['duplicates'].count('normal'), 2)
1214- self.assertEquals(calls['duplicates'].count('printf'), 2,
1215+ self.assertEquals(calls['duplicates'].count(('normal', True)), 1)
1216+ self.assertEquals(calls['duplicates'].count(('printf', False)), 1,
1217 "expected 2 printf calls in {}".format(calls['duplicates']))
1218- self.assertTrue('func_ptr_4' in calls['duplicates'])
1219- self.assertTrue('func_ptr_5' in calls['duplicates'])
1220+ self.assertTrue(('func_ptr_4', True) in calls['duplicates'])
1221+ self.assertTrue(('func_ptr_5', True) in calls['duplicates'])
1222
1223 def test_sections(self):
1224 res, funcs, calls = self.do_parse(sections=['.plt', '.text'])
1225
1226=== modified file 'src/sextant/test_resources/parser_test.dump'
1227--- src/sextant/test_resources/parser_test.dump 2014-10-16 15:21:43 +0000
1228+++ src/sextant/test_resources/parser_test.dump 2014-11-25 11:40:53 +0000
1229@@ -20,20 +20,45 @@
1230 080483f0 <frame_dummy>:
1231 804840f: call *%eax
1232 0804841d <normal>:
1233+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:14
1234+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:18
1235+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:20
1236 8048430: call 8048458 <wierd$name>
1237+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:21
1238 8048443: call 80482f0 <printf@plt>
1239+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:22
1240 8048451: call *%eax
1241+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:24
1242+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:25
1243 08048458 <wierd$name>:
1244+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:29
1245+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:30
1246+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:31
1247 08048460 <duplicates>:
1248+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:35
1249+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:36
1250+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:39
1251 804847b: call 80482f0 <printf@plt>
1252+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:40
1253 804848e: call 80482f0 <printf@plt>
1254+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:42
1255 8048499: call 804841d <normal>
1256+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:43
1257 80484a4: call 804841d <normal>
1258+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:45
1259 80484b2: call *%eax
1260+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:46
1261 80484bd: call *%eax
1262+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:48
1263+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:49
1264 080484c4 <main>:
1265+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:53
1266+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:54
1267 80484d4: call 804841d <normal>
1268+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:55
1269 80484e0: call 8048460 <duplicates>
1270+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:56
1271+/home/benhutc/filter-search/src/sextant/test_resources/parser_test.c:57
1272 080484f0 <__libc_csu_init>:
1273 80484f6: call 8048350 <__x86.get_pc_thunk.bx>
1274 804850e: call 80482b4 <_init>
1275
1276=== modified file 'src/sextant/update_db.py'
1277--- src/sextant/update_db.py 2014-11-13 14:01:55 +0000
1278+++ src/sextant/update_db.py 2014-11-25 11:40:53 +0000
1279@@ -9,9 +9,9 @@
1280
1281 __all__ = ("upload_program", "delete_program")
1282
1283-from .db_api import SextantConnection
1284-from .sshmanager import SSHConnectionError
1285-from .objdump_parser import Parser, run_objdump
1286+from db_api import SextantConnection
1287+from sshmanager import SSHConnectionError
1288+from objdump_parser import Parser, run_objdump
1289 from os import path
1290 from time import time
1291 import subprocess
1292
1293=== modified file 'src/sextant/web/server.py'
1294--- src/sextant/web/server.py 2014-11-13 17:30:38 +0000
1295+++ src/sextant/web/server.py 2014-11-25 11:40:53 +0000
1296@@ -92,18 +92,41 @@
1297 file_out.close()
1298 return output
1299
1300+ @classmethod
1301+ def _proc_args(self, arg_dict):
1302+ # Sanitize the arg_dict. For keys which we require as function
1303+ # arguements, convert to correct types if they are given (ie not '')
1304+ # and otherwise delete the entries. Note that all values in the
1305+ # dictionary are contained in a list.
1306+ for func in ('function_calling', 'function_called'):
1307+ value = arg_dict[func][0]
1308+ if not value:
1309+ del arg_dict[func]
1310+
1311+ limit_internal = arg_dict['limit_internal'][0]
1312+ if limit_internal != 'null':
1313+ # limit_internal may be 'null', 'true', or 'false'.
1314+ arg_dict['limit_internal'] = [(limit_internal == 'true')]
1315+ else:
1316+ del arg_dict['limit_internal']
1317+
1318+ max_depth = arg_dict['max_depth'][0]
1319+ if max_depth:
1320+ # max_depth is a string which may be empty or contain an integer.
1321+ arg_dict['max_depth'] = [int(max_depth)]
1322+ else:
1323+ del arg_dict['max_depth']
1324+
1325+ return arg_dict
1326+
1327 @defer.inlineCallbacks
1328 def _render_plot(self, request):
1329- # the items in the args dict are lists - so use .get()[0] to retrieve
1330- args = request.args
1331+ args = self._proc_args(request.args)
1332
1333 res_code = RESPONSE_CODE_OK
1334 res_msg = None # set this in the logic
1335
1336- #
1337 # Check if provided program name exists
1338- #
1339-
1340 name = args.get('program_name', [None])[0]
1341
1342 if "program_name" is None:
1343@@ -117,9 +140,7 @@
1344 res_fmt = "Program {} not found in database."
1345 res_msg = res_fmt.format(escape(name))
1346
1347- #
1348 # We have a connection and a valid program - now setup the query
1349- #
1350
1351 # We store query info as:
1352 # <query_name>: (<function>, (<known args>), (<kwargs>)
1353@@ -132,19 +153,21 @@
1354 ),
1355 'functions_calling': (
1356 CONNECTION.get_all_functions_calling,
1357- ('func1',)
1358+ ('function_calling', 'limit_internal', 'max_depth')
1359 ),
1360 'functions_called_by': (
1361 CONNECTION.get_all_functions_called,
1362- ('func1',)
1363+ ('function_calling', 'limit_internal', 'max_depth')
1364 ),
1365 'all_call_paths': (
1366 CONNECTION.get_call_paths,
1367- ('func1', 'func2')
1368+ ('function_calling', 'function_called',
1369+ 'limit_internal', 'max_depth')
1370 ),
1371 'shortest_call_path': (
1372 CONNECTION.get_shortest_path_between_functions,
1373- ('func1', 'func2')
1374+ ('function_calling', 'function_called',
1375+ 'limit_internal', 'max_depth')
1376 )
1377 }
1378
1379@@ -162,12 +185,11 @@
1380
1381 # extract any required keyword arguments from request.args
1382 if res_code is RESPONSE_CODE_OK:
1383- fn, kwargs = query
1384-
1385- # all args will be strings - use None to indicate missing argument
1386- req_args = tuple(args.get(kwarg, [None])[0] for kwarg in kwargs)
1387- missing_args = [kwarg for (kwarg, req_arg) in zip(kwargs, req_args)
1388- if req_arg is None]
1389+ fn, keys = query
1390+
1391+ # None indicates a missing argument
1392+ req_kwargs = dict(((key, args.get(key, [None])[0]) for key in keys))
1393+ missing_args = [key for key in keys if req_kwargs[key] is None]
1394
1395 if missing_args:
1396 # missing a kwarg from request.args
1397@@ -180,13 +202,15 @@
1398 try:
1399 print('running query {}'.format(datetime.now()))
1400 program = yield defer_to_thread_with_timeout(render_timeout, fn,
1401- name, *req_args)
1402+ name, **req_kwargs)
1403 print('\tdone {}'.format(datetime.now()))
1404 except Exception as e:
1405 # the timeout has fired and cancelled the request
1406 res_code = RESPONSE_CODE_BAD_REQUEST
1407 res_msg = "{}".format(e)
1408 print('\tfailed {}'.format(datetime.now()))
1409+ raise
1410+
1411
1412 if res_code is RESPONSE_CODE_OK:
1413 # we have received a response to our request
1414@@ -194,7 +218,7 @@
1415 res_code = RESPONSE_CODE_NOT_FOUND
1416 res_fmt = ("At least one of the input functions '{}' was not "
1417 "found in program {}.")
1418- res_msg = res_fmt.format(', '.join(req_args), escape(name))
1419+ res_msg = res_fmt.format(', '.join(args), escape(name))
1420 elif not program.functions:
1421 res_code = RESPONSE_CODE_NO_CONTENT
1422 res_fmt = ("The program {} is in the database but has no "
1423@@ -237,7 +261,6 @@
1424 max_funcs = AUTOCOMPLETE_NAMES_LIMIT + 1
1425 programs = CONNECTION.programs_with_metadata()
1426 result = CONNECTION.get_function_names(program_name, search, max_funcs)
1427- print(search, len(result))
1428 return result if len(result) < max_funcs else set()
1429
1430

Subscribers

People subscribed via source and target branches