From b34a52e4b97b3606b4d93a6291874620c3e3a676 Mon Sep 17 00:00:00 2001 From: Daniel Silverstone Date: Sat, 3 Nov 2018 17:14:43 +0000 Subject: More monkey stuff --- test/.gitignore | 1 + test/monkey-driver.py | 77 ++++++- test/monkey-tests/resource-scheme.yaml | 6 +- test/monkeyfarmer.py | 390 +++++++++++++++++++++++++++++++++ 4 files changed, 472 insertions(+), 2 deletions(-) create mode 100644 test/.gitignore create mode 100644 test/monkeyfarmer.py (limited to 'test') diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 000000000..bee8a64b7 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/test/monkey-driver.py b/test/monkey-driver.py index 25c6422f4..eed7da0e7 100755 --- a/test/monkey-driver.py +++ b/test/monkey-driver.py @@ -2,6 +2,8 @@ import sys, getopt, yaml +from monkeyfarmer import Browser + def print_usage(): print('Usage:') print(' ' + sys.argv[0] + ' -m -t ') @@ -47,23 +49,68 @@ def get_indent(ctx): def print_test_plan_info(ctx, plan): print('Running test: [' + plan["group"] + '] ' + plan["title"]) +def assert_browser(ctx): + assert(ctx['browser'].started) + assert(not ctx['browser'].stopped) + def run_test_step_action_launch(ctx, step): print(get_indent(ctx) + "Action: " + step["action"]) + assert(ctx.get('browser') is None) + assert(ctx.get('windows') is None) + ctx['browser'] = Browser(monkey_cmd=[ctx["monkey"]], quiet=True) + assert_browser(ctx) + ctx['windows'] = dict() def run_test_step_action_window_new(ctx, step): print(get_indent(ctx) + "Action: " + step["action"]) + tag = step['tag'] + assert_browser(ctx) + assert(ctx['windows'].get(tag) is None) + ctx['windows'][tag] = ctx['browser'].new_window(url=step.get('url')) def run_test_step_action_window_close(ctx, step): print(get_indent(ctx) + "Action: " + step["action"]) + assert_browser(ctx) + tag = step['window'] + assert(ctx['windows'].get(tag) is not None) + win = ctx['windows'].pop(tag) + win.kill() + win.wait_until_dead() + assert(win.alive == False) def run_test_step_action_navigate(ctx, step): print(get_indent(ctx) + "Action: " + step["action"]) + assert_browser(ctx) + tag = step['window'] + win = ctx['windows'].get(tag) + assert(win is not None) + win.go(step['url']) def run_test_step_action_sleep_ms(ctx, step): print(get_indent(ctx) + "Action: " + step["action"]) def run_test_step_action_block(ctx, step): print(get_indent(ctx) + "Action: " + step["action"]) + conds = step['conditions'] + assert_browser(ctx) + + def conds_met(): + for cond in conds: + status = cond['status'] + window = cond['window'] + assert(status == "complete") # TODO: Add more status support? + if window == "*all*": + for win in ctx['windows'].items(): + if win.throbbing: + return False + else: + win = ctx['windows'][window] + if win.throbbing: + return False + return True + + while not conds_met(): + ctx['browser'].farmer.loop(once=True) def run_test_step_action_repeat(ctx, step): print(get_indent(ctx) + "Action: " + step["action"]) @@ -72,6 +119,28 @@ def run_test_step_action_repeat(ctx, step): run_test_step(ctx, step) ctx["depth"] -= 1 +def run_test_step_action_plot_check(ctx, step): + print(get_indent(ctx) + "Action: " + step["action"]) + assert_browser(ctx) + win = ctx['windows'][step['window']] + checks = step['checks'] + all_text = [] + bitmaps = [] + for plot in win.redraw(): + if plot[0] == 'TEXT': + all_text.extend(plot[6:]) + if plot[0] == 'BITMAP': + bitmaps.append(plot[1:]) + all_text = " ".join(all_text) + for check in checks: + if 'text-contains' in check.keys(): + print("Check {} in {}".format(repr(check['text-contains']),repr(all_text))) + assert(check['text-contains'] in all_text) + elif 'bitmap-count' in check.keys(): + assert(len(bitmaps) == int(check['bitmap-count'])) + else: + raise AssertionError("Unknown check: {}".format(repr(check))) + def run_test_step_action_timer_start(ctx, step): print(get_indent(ctx) + "Action: " + step["action"]) @@ -83,6 +152,10 @@ def run_test_step_action_timer_check(ctx, step): def run_test_step_action_quit(ctx, step): print(get_indent(ctx) + "Action: " + step["action"]) + assert_browser(ctx) + browser = ctx.pop('browser') + windows = ctx.pop('windows') + assert(browser.quit_and_wait()) step_handlers = { "launch": run_test_step_action_launch, @@ -95,6 +168,7 @@ step_handlers = { "timer-start": run_test_step_action_timer_start, "timer-stop": run_test_step_action_timer_stop, "timer-check": run_test_step_action_timer_check, + "plot-check": run_test_step_action_plot_check, "quit": run_test_step_action_quit, } @@ -111,9 +185,10 @@ def main(argv): ctx = {} path_monkey, path_test = parse_argv(argv) plan = load_test_plan(path_test) + ctx["monkey"] = path_monkey print_test_plan_info(ctx, plan) walk_test_plan(ctx, plan) # Some python weirdness to get to main(). if __name__ == "__main__": - main(sys.argv[1:]) \ No newline at end of file + main(sys.argv[1:]) diff --git a/test/monkey-tests/resource-scheme.yaml b/test/monkey-tests/resource-scheme.yaml index 58b0f5eff..791a79cd6 100644 --- a/test/monkey-tests/resource-scheme.yaml +++ b/test/monkey-tests/resource-scheme.yaml @@ -13,6 +13,8 @@ steps: - window: win1 status: complete - action: plot-check + window: win1 + checks: - text-contains: Not found - text-contains: Error 404 - action: navigate @@ -23,8 +25,10 @@ steps: - window: win1 status: complete - action: plot-check + window: win1 + checks: - bitmap-count: 1 - action: window-close - - window: win1 + window: win1 - action: quit diff --git a/test/monkeyfarmer.py b/test/monkeyfarmer.py new file mode 100644 index 000000000..239a63e0e --- /dev/null +++ b/test/monkeyfarmer.py @@ -0,0 +1,390 @@ +# Copyright 2017, 2018 Daniel Silverstone +# +# This file is part of NetSurf, http://www.netsurf-browser.org/ +# +# NetSurf is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# NetSurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Monkey Farmer + +The monkey farmer is a wrapper around `nsmonkey` which can be used to simplify +access to the monkey behaviours and ultimately to write useful tests in an +expressive but not overcomplicated DSLish way. Tests are, ultimately, still +Python code. + +""" + +import asyncore +import os +import socket +import subprocess +import time + +class MonkeyFarmer(asyncore.dispatcher): + def __init__(self, monkey_cmd, online, quiet=False): + (mine, monkeys) = socket.socketpair() + + asyncore.dispatcher.__init__(self, sock=mine) + + self.monkey = subprocess.Popen( + monkey_cmd, + stdin=monkeys, + stdout=monkeys, + close_fds=[mine]) + + monkeys.close() + + self.buffer = b"" + self.incoming = b"" + self.lines = [] + self.scheduled = [] + self.deadmonkey = False + self.online = online + self.quiet = quiet + self.discussion = [] + + def handle_connect(self): + pass + + def handle_read(self): + got = self.recv(8192) + if not got: + self.deadmonkey = True + return + self.incoming += got + if b"\n" in self.incoming: + lines = self.incoming.split(b"\n") + self.incoming = lines.pop() + self.lines = lines + + def writable(self): + return (len(self.buffer) > 0) + + def handle_write(self): + sent = self.send(self.buffer) + self.buffer = self.buffer[sent:] + + def tell_monkey(self, *args): + cmd = (" ".join(args)) + if not self.quiet: + print(">>> {}".format(cmd)) + self.discussion.append((">",cmd)) + cmd = cmd + "\n" + self.buffer += cmd.encode('utf-8') + + def monkey_says(self, line): + line = line.decode('utf-8') + if not self.quiet: + print("<<< {}".format(line)) + self.discussion.append(("<", line)) + self.online(line) + + def schedule_event(self, event, secs=None, when=None): + assert(secs is not None or when is not None) + if when is None: + when = time.time() + secs + self.scheduled.append((when, event)) + self.scheduled.sort(lambda a,b: cmp(a[0],b[0])) + + def unschedule_event(self, event): + self.scheduled = [x for x in self.scheduled if x[1] != event] + + def loop(self, once=False): + if len(self.lines) > 0: + self.monkey_says(self.lines.pop(0)) + if once: + return + while not self.deadmonkey: + now = time.time() + while len(self.scheduled) > 0 and now >= self.scheduled[0][0]: + func = self.scheduled[0][1] + self.scheduled.pop(0) + func(self) + now = time.time() + if len(self.scheduled) > 0: + next = self.scheduled[0][0] + asyncore.loop(timeout=next-now, count=1) + else: + asyncore.loop(count=1) + if len(self.lines) > 0: + self.monkey_says(self.lines.pop(0)) + if once: + break + +class Browser: + def __init__(self, monkey_cmd=["./nsmonkey"], quiet=False): + self.farmer = MonkeyFarmer(monkey_cmd=monkey_cmd, online=self.on_monkey_line, quiet=quiet) + self.windows = {} + self.current_draw_target = None + self.started = False + self.stopped = False + self.launchurl = None + now = time.time() + while not self.started: + self.farmer.loop(once=True) + if (time.time() - now) > 1: + break + + def pass_options(self, *opts): + if len(opts) > 0: + self.farmer.tell_monkey("OPTIONS " + (" ".join(opts))) + + def on_monkey_line(self, line): + parts = line.split(" ") + handler = getattr(self, "handle_" + parts[0], None) + if handler is not None: + handler(*parts[1:]) + + def quit(self): + self.farmer.tell_monkey("QUIT") + + def quit_and_wait(self): + self.quit() + self.farmer.loop() + return self.stopped + + def handle_GENERIC(self, what, *args): + if what == 'STARTED': + self.started = True + elif what == 'FINISHED': + self.stopped = True + elif what == 'LAUNCH': + self.launchurl = args[1] + else: + # TODO: Nothing for now? + pass + + def handle_WINDOW(self, action, _win, winid, *args): + if action == "NEW": + new_win = BrowserWindow(self, winid, *args) + self.windows[winid] = new_win + else: + win = self.windows.get(winid, None) + if win is None: + print(" Unknown window id {}".format(winid)) + else: + win.handle(action, *args) + + def handle_PLOT(self, *args): + if self.current_draw_target is not None: + self.current_draw_target.handle_plot(*args) + + def new_window(self, url=None): + if url is None: + self.farmer.tell_monkey("WINDOW NEW") + else: + self.farmer.tell_monkey("WINDOW NEW %s" % url) + wins_known = set(self.windows.keys()) + while len(set(self.windows.keys()).difference(wins_known)) == 0: + self.farmer.loop(once=True) + poss_wins = set(self.windows.keys()).difference(wins_known) + return self.windows[poss_wins.pop()] + + +class BrowserWindow: + def __init__(self, browser, winid, _for, coreid, _existing, otherid, _newtab, newtab, _clone, clone): + self.alive = True + self.browser = browser + self.winid = winid + self.coreid = coreid + self.existing = browser.windows.get(otherid, None) + self.newtab = newtab == "TRUE" + self.clone = clone == "TRUE" + self.width = 0 + self.height = 0 + self.title = "" + self.throbbing = False + self.scrollx = 0 + self.scrolly = 0 + self.content_width = 0 + self.content_height = 0 + self.status = "" + self.pointer = "" + self.scale = 1.0 + self.url = "" + self.plotted = [] + self.plotting = False + + def kill(self): + self.browser.farmer.tell_monkey("WINDOW DESTROY %s" % self.winid) + + def wait_until_dead(self, timeout=1): + now = time.time() + while self.alive: + self.browser.farmer.loop(once=True) + if (time.time() - now) > timeout: + break + + def go(self, url, referer = None): + if referer is None: + self.browser.farmer.tell_monkey("WINDOW GO %s %s" % ( + self.winid, url)) + else: + self.browser.farmer.tell_monkey("WINDOW GO %s %s %s" % ( + self.winid, url, referer)) + self.wait_start_loading() + + def reload(self): + self.browser.farmer.tell_monkey("WINDOW RELOAD %s" % self.winid) + + def handle(self, action, *args): + handler = getattr(self, "handle_window_" + action, None) + if handler is not None: + handler(*args) + + def handle_window_SIZE(self, _width, width, _height, height): + self.width = int(width) + self.height = int(height) + + def handle_window_DESTROY(self): + self.alive = False + + def handle_window_TITLE(self, _str, *title): + self.title = " ".join(title) + + def handle_window_REDRAW(self): + pass + + def handle_window_GET_DIMENSIONS(self, _width, width, _height, height): + self.width = width + self.height = height + + def handle_window_NEW_CONTENT(self): + pass + + def handle_window_NEW_ICON(self): + pass + + def handle_window_START_THROBBER(self): + self.throbbing = True + + def handle_window_STOP_THROBBER(self): + self.throbbing = False + + def handle_window_SET_SCROLL(self, _x, x, _y, y): + self.scrollx = int(x) + self.scrolly = int(y) + + def handle_window_UPDATE_BOX(self, _x, x, _y, y, _width, width, _height, height): + x = int(x) + y = int(y) + width = int(width) + height = int(height) + pass + + def handle_window_UPDATE_EXTENT(self, _width, width, _height, height): + self.content_width = int(width) + self.content_height = int(height) + + def handle_window_SET_STATUS(self, _str, *status): + self.status = (" ".join(status)) + + def handle_window_SET_POINTER(self, _ptr, ptr): + self.pointer = ptr + + def handle_window_SET_SCALE(self, _scale, scale): + self.scale = float(scale) + + def handle_window_SET_URL(self, _url, url): + self.url = url + + def handle_window_GET_SCROLL(self, _x, x, _y, y): + self.scrollx = int(x) + self.scrolly = int(y) + + def handle_window_SCROLL_START(self): + self.scrollx = 0 + self.scrolly = 0 + + def handle_window_REDRAW(self, act): + if act == "START": + self.browser.current_draw_target = self + self.plotted = [] + self.plotting = True + else: + self.browser.current_draw_target = None + self.plotting = False + + def load_page(self, url=None, referer=None): + if url is not None: + self.go(url, referer) + self.wait_loaded() + + def wait_start_loading(self): + while not self.throbbing: + self.browser.farmer.loop(once=True) + + def wait_loaded(self): + self.wait_start_loading() + while self.throbbing: + self.browser.farmer.loop(once=True) + + def handle_plot(self, *args): + self.plotted.append(args) + + def redraw(self, coords=None): + if coords is None: + self.browser.farmer.tell_monkey("WINDOW REDRAW %s" % self.winid) + else: + self.browser.farmer.tell_monkey("WINDOW REDRAW %s %s" % ( + self.winid, (" ".join(coords)))) + while not self.plotting: + self.browser.farmer.loop(once=True) + while self.plotting: + self.browser.farmer.loop(once=True) + return self.plotted + + +if __name__ == '__main__': + # Simple test is as follows... + + browser = Browser(quiet=True) + win = browser.new_window() + + fname = "test/js/inline-doc-write-simple.html" + full_fname = os.path.join(os.getcwd(), fname) + + browser.pass_options("--enable_javascript=0") + win.load_page("file://" + full_fname) + + print("Loaded, URL is {}".format(win.url)) + + cmds = win.redraw() + print("Received {} plot commands".format(len(cmds))) + for cmd in cmds: + if cmd[0] == "TEXT": + x = cmd[2] + y = cmd[4] + rest = " ".join(cmd[6:]) + print("{} {} -> {}".format(x,y,rest)) + + + browser.pass_options("--enable_javascript=1") + win.load_page("file://" + full_fname) + + print("Loaded, URL is {}".format(win.url)) + + cmds = win.redraw() + print("Received {} plot commands".format(len(cmds))) + for cmd in cmds: + if cmd[0] == "TEXT": + x = cmd[2] + y = cmd[4] + rest = " ".join(cmd[6:]) + print("{} {} -> {}".format(x,y,rest)) + + browser.quit_and_wait() + + #print("Discussion was:") + #for line in browser.farmer.discussion: + # print("{} {}".format(line[0], line[1])) -- cgit v1.2.3