Merge lp:~ben-hutchings/ensoft-sextant/rel-merge into lp:ensoft-sextant
- rel-merge
- Merge into whiteline
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 |
Related bugs: |
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 : | # |
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 |
Adds a feature to Sextant that allows the user to filter the results so that you only show the callgraph within a particular file.