Files
idapython-src/tools/collect_traces.py
2022-01-17 11:28:09 +01:00

326 lines
10 KiB
Python
Executable File

#!/usr/bin/python3
# A tool to merge all the /tmp files produced by
# t2/phases/tests/trace_idapython_calls_idapythonrc.py,
# into a persisted file (in P4, not in /tmp)
# that can be read to improve the IDAPython documentation.
import sys
import os
import tempfile
import ast
tools_dir = os.path.dirname(__file__)
output_file = os.path.join(tools_dir, "collected_traces.txt")
input_files = "trace_idapython_calls_output.txt" # on each test directory
stats_tests_added = 0
stats_funcs_added = 0
stats_lines_added = 0
#------------------------------------------------------------------------
def load_existing(pathname):
if not os.path.exists(pathname):
return {}
with open(pathname, "r") as f:
collected = ast.literal_eval(f.read())
# turn lists into sets for easier merging
for key in collected:
if key == "#empty":
empty_data = collected[key]
for fun in empty_data:
empty_data[fun] = matcher_t(empty_data[fun])
else:
fun_data = collected[key]
fun_data["return"] = matcher_t(fun_data["return"])
fun_data["tests"] = set(fun_data["tests"])
return collected
#------------------------------------------------------------------------
def load_from_test(collected_data, test_name, pathname):
global stats_tests_added, stats_funcs_added, stats_lines_added
with open(pathname, "r") as f:
content = ast.literal_eval(f.read())
if "#empty" in collected_data:
common = collected_data["#empty"]
else:
stats_tests_added += 1
collected_data["#empty"] = common = {}
for key in content:
# ida_* modules only
prefix = "ida_"
if not key.startswith(prefix):
continue
fun = key[len(prefix):]
# presently we remove all dictionary content except for return values;
# we'll see in the future if we use the rest of info,
# or if we don't use a dictionary at all in the idapythonrc
# note: return values in /tmp traces were collected as repr()
content_rets = [ast.literal_eval(x["return"]) for x in content[key]]
if test_name == "empty":
# entry with a special format
if fun not in common:
common[fun] = matcher_t(content_rets)
stats_funcs_added += 1
stats_lines_added += common[fun].count()
continue
for line in content_rets:
if fun in common:
if common[fun].add(line):
stats_lines_added += 1
continue
if fun in collected_data:
fun_data = collected_data[fun]
rets = fun_data["return"]
tests = fun_data["tests"]
else:
stats_funcs_added += 1
rets = matcher_t()
tests = set()
fun_data = { "return":rets, "tests":tests }
collected_data[fun] = fun_data
if test_name not in tests:
stats_tests_added += 1
tests.add(test_name)
if rets.add(line):
stats_lines_added += 1
#------------------------------------------------------------------------
# Stores different return values of functions collected at runtime,
# and simplifies repetitions like [str, str, str] into a pattern [str, ...]
class matcher_t:
def __init__(self, initial_content=[]):
self.patterns = []
for value in initial_content:
self.add(value)
def add(self, value):
if any(pat.match(value) for pat in self.patterns):
return False # another instance of what we already have
# something new
self.patterns.append(pattern_t(value))
return True
def count(self):
return len(self.patterns)
def sorted(self):
return sorted(repr(pat) for pat in self.patterns)
#------------------------------------------------------------------------
class pattern_t:
def __init__(self, value):
self.expr = self._build(value)
def _build(self, value):
if isinstance(value, list) and len(value) > 0:
# recognize a special form of lists as a repetition_t object
# that we stored (via repetition_t.__repr__) in a previous run
# (saved into, and now reloaded from, the "tools/collected_traces.txt" file)
if len(value) == 2 and value[1] == "...":
return repetition_t(value[0])
first = value[0]
if all(x == first for x in value[1:]):
return repetition_t(first)
# heterogeneous list - treated as separate values
return value
if isinstance(value, tuple):
build = []
for x in value:
build.append(self._build(x))
return tuple(build)
return value
def match(self, value):
# "match" may update the pattern when receiving new values:
# replace the pattern if it was converted into a repetition
m = self._match(self.expr, value)
if isinstance(m, bool):
return m
self.expr = m
return True
def _match(self, pat, val):
if isinstance(pat, tuple) and isinstance(val, tuple):
if len(pat) != len(val):
return False
return self._maybe_replace_parts(pat, val)
if pat == [] and isinstance(val, list) and len(val) > 0:
# convert [] into a repetition (we didn't before,
# because with an empty list we couldn't know WHAT was repeated)
return repetition_t(val[0])
if isinstance(pat, repetition_t):
return pat.match(val)
return pat == val
def _maybe_replace_parts(self, pat_tuple, val_tuple):
ret = []
all_bool = True
for p, v in zip(pat_tuple, val_tuple):
m = self._match(p, v)
if isinstance(m, bool):
if not m:
return False
# keep the previous part
ret.append(p)
else:
# new part
ret.append(m)
all_bool = False
if all_bool:
return True # good match but no replacements
return tuple(ret)
def __repr__(self):
return repr(self.expr)
#------------------------------------------------------------------------
class repetition_t:
def __init__(self, core):
self.core = pattern_t(core)
def match(self, value):
if not isinstance(value, list):
return False
return all(self.core.match(item) for item in value)
def __repr__(self):
return "[" + repr(self.core) + ", '...']"
#------------------------------------------------------------------------
def dump_data(collected_data):
content = "{\n"
for fun_name in sorted(collected_data):
content += " \"{}\": {{\n".format(fun_name)
fun_data = collected_data[fun_name]
if fun_name == "#empty":
# "empty test" entry, different format that the rest
for fun_in_empty in fun_data:
content += " \"{}\": [\n".format(fun_in_empty)
for line in fun_data[fun_in_empty].sorted():
content += " {},\n".format(line) # return values
content += " ],\n"
else:
# data per function
rets = fun_data["return"]
tests = fun_data["tests"]
content += " \"return\": [\n"
for line in rets.sorted():
content += " {},\n".format(line)
content += " ],\n"
content += " \"tests\": [\n"
for test_name in sorted(tests):
content += " \"{}\",\n".format(test_name)
content += " ]\n"
content += " },\n"
content += "}\n"
return content
#------------------------------------------------------------------------
def test_dir():
username = os.path.basename(os.path.expanduser("~"))
# or more reliably:
# import getpass
# username = getpass.getuser()
# but the above works nearly always, with one less dependency
return "/tmp/{}/uitests".format(username)
#------------------------------------------------------------------------
def all_test_trace_files():
testdir = test_dir()
emptydir = os.path.join(testdir, "empty")
if os.path.isdir(emptydir):
test_output = os.path.join(emptydir, input_files)
if os.path.exists(test_output):
yield "empty", test_output # process the "empty" test first
for entry in os.scandir(testdir):
if entry.name == "empty":
continue # already read
if entry.is_dir():
test_output = os.path.join(entry.path, input_files)
if os.path.exists(test_output):
yield entry.name, test_output
#------------------------------------------------------------------------
def main():
# check for p4 edit
# (otherwise os.replace below would replace the output file silently)
if os.path.exists(output_file) and not os.access(output_file, os.W_OK):
print("{}: not writable".format(output_file))
sys.exit(1)
collected_data = load_existing(output_file)
first = True
for test_name, pathname in all_test_trace_files():
if first:
first = False
if test_name != "empty" and not collected_data:
print("Please run the \"empty\" test"
" when collecting for the first time.")
sys.exit(1)
load_from_test(collected_data, test_name, pathname)
fout, fileout = tempfile.mkstemp()
success = False
try:
os.write(fout, dump_data(collected_data).encode("ascii"))
os.close(fout)
os.replace(fileout, output_file)
print ("Tests added: {}, functions added: {}, lines added: {}".format(
stats_tests_added, stats_funcs_added, stats_lines_added))
success = True
finally:
if not success:
os.remove(fileout)
#------------------------------------------------------------------------
main()