diff options
author | Timotej Lazar <timotej.lazar@araneo.org> | 2015-01-15 12:10:22 +0100 |
---|---|---|
committer | Aleš Smodiš <aless@guru.si> | 2015-08-11 14:26:01 +0200 |
commit | 819ab10281c9bd6c000364c3a243959edd18abf7 (patch) | |
tree | 5ca3452418b49781563221bb56cf70e1d0fb1bb8 /monkey/action.py | |
parent | d86793039957aa408a98806aecfb5964bda5fb87 (diff) |
Move pymonkey stuff to monkey/
Importing pymonkey into webmonkey, let's see how this works.
Diffstat (limited to 'monkey/action.py')
-rwxr-xr-x | monkey/action.py | 284 |
1 files changed, 284 insertions, 0 deletions
diff --git a/monkey/action.py b/monkey/action.py new file mode 100755 index 0000000..057ed0e --- /dev/null +++ b/monkey/action.py @@ -0,0 +1,284 @@ +#!/usr/bin/python3 + +class Action: + # type ∈ ['remove', 'insert', 'solve', 'solve_all'] + # time: absolute elapsed time since the attempt started, in ms + # offset: position of the first inserted/removed character + # text: inserted/removed text or query + # total, passed: number of test cases + def __init__(self, type, time, offset=0, text='', total=0, passed=0): + self.type = type + self.time = time + if type == 'insert' or type == 'remove': + self.offset = offset + self.length = len(text) + self.text = text + elif type == 'solve' or type == 'solve_all': + self.query = text + elif type == 'test': + self.total = total + self.passed = passed + + def __str__(self): + s = 't = ' + str(self.time/1000.0) + ' ' + self.type + if self.type in ('insert', 'remove'): + s += ' "' + self.text.replace('\n', '\\n').replace('\t', '\\t') + '" at ' + str(self.offset) + elif self.type in ('solve', 'solve_all'): + s += ' "' + self.query + '"' + elif self.type == 'test': + s += ' {0} / {1}'.format(self.passed, self.total) + return s + + # apply this action to text + def apply(self, text): + if self.type == 'insert': + return text[:self.offset] + self.text + text[self.offset:] + elif self.type == 'remove': + return text[:self.offset] + text[self.offset+self.length:] + else: + return text + + # reverse the application of this action + def unapply(self, text): + if self.type == 'insert': + return text[:self.offset] + text[self.offset+self.length:] + elif self.type == 'remove': + return text[:self.offset] + self.text + text[self.offset:] + else: + return text + +# parse log from database into a list of actions, cleaning up some fluff. +# ignore non-text actions (queries and tests) +def parse(data): + if data == None: + return [], [] + + actions = [] + incorrect = set() + + time = 0 + code = '' + i = 0 + while i < len(data): + # parse one action + type = data[i] + i += 1 + dt = int(((data[i] << 8) + (data[i+1])) * 100.0) + time += dt + i += 2 + if type == 1: # insert + offset = (data[i] << 8) + data[i+1] + i += 2 + length = (data[i] << 8) + data[i+1] + i += 2 + text = data[i:i+length].decode() + i += length + action = Action('insert', time, offset=offset, text=text) + elif type == 2: # remove + offset = (data[i] << 8) + data[i+1] + i += 2 + length = (data[i] << 8) + data[i+1] + i += 2 + text = code[offset:offset+length] + action = Action('remove', time, offset=offset, text=text) + elif type == 3 or type == 4: # solve / solve all + length = (data[i] << 8) + data[i+1] + i += 2 + query = data[i:i+length].decode() + i += length + act_type = 'solve' + ('_all' if type == 4 else '') + action = Action(act_type, time, text=query) + elif type == 8: # test + total = data[i] + i += 1 + passed = data[i] + i += 1 + action = Action('test', time, total=total, passed=passed) + else: + # unsupported action type + continue + + # skip normalization if this is the first action + if actions == []: + actions.append(action) + code = action.apply(code) + continue + + # add to list of actions; modify previously added action if necessary + prev = actions[-1] + + # remove superfluous REMOVE action when newline is inserted (due to editor auto-indent) + if prev.type == 'remove' and action.type == 'insert' and \ + action.time == prev.time and \ + action.offset == prev.offset and action.length > prev.length and \ + action.text[action.length-prev.length:] == prev.text: + # discard last REMOVE action + code = prev.unapply(code) + actions.pop() + + # replace current action with something better + length = action.length - prev.length + new = Action('insert', prev.time, offset = prev.offset, text = action.text[:length]) + actions.append(new) + code = new.apply(code) + + # remove superfluous INSERT action when newline is removed (due to editor auto-indent) + elif prev.type == 'remove' and action.type == 'insert' and \ + action.time == prev.time and \ + action.offset == prev.offset and action.length < prev.length and \ + prev.text[prev.length-action.length:] == action.text: + # discard last INSERT action + code = prev.unapply(code) + actions.pop() + + # replace current action with something better + length = prev.length - action.length + new = Action('remove', prev.time, offset = prev.offset, text = prev.text[:length]) + actions.append(new) + code = new.apply(code) + + # discard INSERT/REMOVE pairs (typos) + elif prev.type == 'insert' and action.type == 'remove' and \ + action.time - prev.time < 10000 and \ + action.offset == prev.offset and action.text == prev.text: + # discard last and current actions + code = prev.unapply(code) + actions.pop() + + # discard REMOVE/INSERT pairs (deleted char then typed back) + elif prev.type == 'remove' and action.type == 'insert' and \ + action.offset == prev.offset and action.text == prev.text: + # discard last and current actions + code = prev.unapply(code) + actions.pop() + + # otherwise, simply append the current action + else: + actions.append(action) + code = action.apply(code) + + return actions + +# expand any multi-char actions (does not do anything for the 99%) +def expand(actions): + i = 0 + while i < len(actions): + if actions[i].type == 'insert' and len(actions[i].text) > 1: + a = actions.pop(i) + for offset in range(len(a.text)): + actions.insert(i+offset, Action('insert', a.time, a.offset+offset, a.text[offset])) + i += len(a.text) + elif actions[i].type == 'remove' and len(actions[i].text) > 1: + a = actions.pop(i) + for offset in range(len(a.text)): + actions.insert(i, Action('remove', a.time, a.offset+offset, a.text[offset])) + i += len(a.text) + else: + i += 1 + +# each action in parse() result corresponds to single insertion/deletion. +# this function merges related adjacent actions +def compress(actions): + # first make each edit change exactly one character, for easier handling + expand(actions) + + i = 0 + while i < len(actions)-1: + a = actions[i] + b = actions[i+1] + + # merge adjacent INSERT actions + # +a +b → +ab + if a.type == 'insert' and b.type == 'insert' and \ + b.offset == a.offset + a.length: #and b.time - a.time < 10000: + a.text += b.text + a.length += b.length + del actions[i+1] + + # merge adjacent REMOVE actions (two cases: backspace & delete) + # -b -a → -ab + elif a.type == 'remove' and b.type == 'remove' and \ + b.offset == a.offset - b.length: #and b.time - a.time < 10000: + a.text = b.text + a.text + a.offset = b.offset + a.length += b.length + del actions[i+1] + # -a -b → -ab + elif a.type == 'remove' and b.type == 'remove' and \ + b.offset == a.offset and b.time - a.time < 10000: + a.text += b.text + a.length += b.length + del actions[i+1] + + # merge adjacent INSERT/REMOVE actions + # +ab -b → +a + elif a.type == 'insert' and b.type == 'remove' and \ + b.offset >= a.offset and b.offset < a.offset + a.length and \ + b.length == a.offset + a.length - b.offset and b.time - a.time < 10000: + del_start = b.offset - a.offset + del_end = del_start + b.length + a.text = a.text[:del_start] + a.text[del_end:] + a.length -= b.length + del actions[i+1] + + else: + i += 1 + +# some sample code +if __name__ == '__main__': + import sys + if len(sys.argv) < 2: + print('usage: ' + sys.argv[0] + ' <database>') + sys.exit(1) + + import sqlite3 + conn = sqlite3.connect(sys.argv[1]) + conn.text_factory = bytes + c = conn.cursor() + + # print all problem ids + print('problems:') + c.execute('select * from problems') + for problem in c.fetchall(): + # problem = (id, name, description, details, solution, library) + # name: predicate name + arity (e.g. conc/2) + # desc: one-line problem description + # details: detailed problem description + # solution: official solution + # library: fact database for testing (e.g. for parent, brother, … relations) + print(' ' + str(problem[0]) + '\t' + str(problem[1], encoding='utf-8')) + print() + + pid = input('enter problem id: ') + c.execute('select id from attempts where problem=?', (pid,)) + attempts = list(c.fetchall()) + + # print all attempt ids for the selected problem + print('attempts for problem ' + str(pid) + ':') + print(', '.join([str(attempt[0]) for attempt in attempts])) + print() + + aid = input('enter attempt id: ') + c.execute('select * from attempts where id=?', (aid,)) + attempt = c.fetchone() + # attempt = (id, problem_id, user_id, log, content, done, session) + # log: action sequence log + # content: final version for this attempt + # done: did any version of the program pass all tests? + # session: irrelevant + try: + actions = parse(attempt[3]) + print('read ' + str(len(actions)) + ' actions from log') + compress(actions) + print('after compression: ' + str(len(actions)) + ' actions') + print() + + print('code versions for this attempt:') + code = '' + for action in actions: + code = action.apply(code) + print(action) + print(code.strip()) + print() + except Exception as ex: + sys.stderr.write('Error parsing action log: ' + str(ex)) |