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