mirror of
https://github.com/idapython/src
synced 2026-06-08 14:47:00 +00:00
326 lines
10 KiB
Python
Executable File
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()
|