summaryrefslogtreecommitdiff
path: root/test/monkey_driver.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/monkey_driver.py')
-rwxr-xr-xtest/monkey_driver.py670
1 files changed, 670 insertions, 0 deletions
diff --git a/test/monkey_driver.py b/test/monkey_driver.py
new file mode 100755
index 000000000..9b810d2a6
--- /dev/null
+++ b/test/monkey_driver.py
@@ -0,0 +1,670 @@
+#!/usr/bin/python3
+#
+# Copyright 2019 Daniel Silverstone <dsilvers@digital-scurf.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+runs tests in monkey as defined in a yaml file
+"""
+
+# pylint: disable=locally-disabled, missing-docstring
+
+import os
+import sys
+import getopt
+import time
+import yaml
+
+from monkeyfarmer import Browser
+
+
+class DriverBrowser(Browser):
+ def __init__(self, *args, **kwargs):
+ super(DriverBrowser, self).__init__(*args, **kwargs)
+ self.auth = []
+
+ def add_auth(self, url, realm, username, password):
+ self.auth.append((url, realm, username, password))
+
+ def remove_auth(self, url, realm, username, password):
+ keep = []
+
+ def matches(first, second):
+ if first is None or second is None:
+ return True
+ return first == second
+
+ for (iurl, irealm, iusername, ipassword) in self.auth:
+ if not (matches(url, iurl) or
+ matches(realm, irealm) or
+ matches(username, iusername) or
+ matches(password, ipassword)):
+ keep.append((iurl, irealm, iusername, ipassword))
+ self.auth = keep
+
+ def handle_ready_login(self, logwin):
+ # We have logwin.{url,username,password,realm}
+ # We must logwin.send_{username,password}(xxx)
+ # We may logwin.go()
+ # We may logwin.destroy()
+ def matches(first, second):
+ if first is None or second is None:
+ return True
+ return first == second
+ candidates = []
+ for (url, realm, username, password) in self.auth:
+ score = 0
+ if matches(url, logwin.url):
+ score += 1
+ if matches(realm, logwin.realm):
+ score += 1
+ if matches(username, logwin.username):
+ score += 1
+ if score > 0:
+ candidates.append((score, username, password))
+ if candidates:
+ candidates.sort()
+ (score, username, password) = candidates[-1]
+ print("401: Found candidate {}/{} with score {}".format(username, password, score))
+ logwin.send_username(username)
+ logwin.send_password(password)
+ logwin.go()
+ else:
+ print("401: No candidate found, cancelling login box")
+ logwin.destroy()
+
+
+def print_usage():
+ print('Usage:')
+ print(' ' + sys.argv[0] + ' -m <path to monkey> -t <path to test> [-w <wrapper arguments>]')
+
+
+def parse_argv(argv):
+
+ # pylint: disable=locally-disabled, unused-variable
+
+ path_monkey = ''
+ path_test = ''
+ wrapper = None
+ try:
+ opts, args = getopt.getopt(argv, "hm:t:w:", ["monkey=", "test=", "wrapper="])
+ except getopt.GetoptError:
+ print_usage()
+ sys.exit(2)
+ for opt, arg in opts:
+ if opt == '-h':
+ print_usage()
+ sys.exit()
+ elif opt in ("-m", "--monkey"):
+ path_monkey = arg
+ elif opt in ("-t", "--test"):
+ path_test = arg
+ elif opt in ("-w", "--wrapper"):
+ if wrapper is None:
+ wrapper = []
+ wrapper.extend(arg.split())
+
+ if path_monkey == '':
+ print_usage()
+ sys.exit()
+ if path_test == '':
+ print_usage()
+ sys.exit()
+
+ return path_monkey, path_test, wrapper
+
+
+def load_test_plan(path):
+
+ # pylint: disable=locally-disabled, broad-except
+
+ plan = []
+ with open(path, 'r') as stream:
+ try:
+ plan = (yaml.load(stream, Loader=yaml.CSafeLoader))
+ except Exception as exc:
+ print(exc)
+ return plan
+
+
+def get_indent(ctx):
+ return ' ' * ctx["depth"]
+
+
+def print_test_plan_info(ctx, plan):
+
+ # pylint: disable=locally-disabled, unused-argument
+
+ print('Running test: [' + plan["group"] + '] ' + plan["title"])
+
+
+def assert_browser(ctx):
+ assert ctx['browser'].started
+ assert not ctx['browser'].stopped
+
+
+def conds_met(ctx, conds):
+ # for each condition listed determine if they have been met
+ # efectively this is condition1 | condition2
+ for cond in conds:
+ if 'timer' in cond.keys():
+ timer = cond['timer']
+ elapsed = cond['elapsed']
+ assert_browser(ctx)
+ assert ctx['timers'].get(timer) is not None
+ taken = time.time() - ctx['timers'][timer]["start"]
+ if taken >= elapsed:
+ return True
+ elif 'window' in cond.keys():
+ status = cond['status']
+ window = cond['window']
+ assert status == "complete" or status == "loading" # TODO: Add more status support?
+ if window == "*all*":
+ # all windows must be complete, or any still loading
+ throbbing = False
+ for win in ctx['windows'].items():
+ if win[1].throbbing:
+ throbbing = True
+ # throbbing and want loading => true
+ # not throbbing and want complete => true
+ if (status == "loading") == throbbing:
+ return True
+ else:
+ win = ctx['windows'][window]
+ if win.throbbing == (status == "loading"):
+ return True
+ else:
+ raise AssertionError("Unknown condition: {}".format(repr(cond)))
+
+ return False
+
+
+def run_test_step_action_launch(ctx, step):
+ print(get_indent(ctx) + "Action: " + step["action"])
+
+ # ensure browser is not already launched
+ assert ctx.get('browser') is None
+ assert ctx.get('windows') is None
+
+ # build command line switches list
+ monkey_cmd = [ctx["monkey"]]
+ for option in step.get('launch-options', []):
+ monkey_cmd.append("--{}".format(option))
+ print(get_indent(ctx) + " " + "Command line: " + repr(monkey_cmd))
+
+ # build command environment
+ monkey_env = os.environ.copy()
+ for envkey, envvalue in step.get('environment', {}).items():
+ monkey_env[envkey] = envvalue
+ print(get_indent(ctx) + " " + envkey + "=" + envvalue)
+ if 'language' in step.keys():
+ monkey_env['LANGUAGE'] = step['language']
+
+ # create browser object
+ ctx['browser'] = DriverBrowser(
+ monkey_cmd=monkey_cmd,
+ monkey_env=monkey_env,
+ quiet=True,
+ wrapper=ctx.get("wrapper"))
+ assert_browser(ctx)
+ ctx['windows'] = dict()
+
+ # set user options
+ for option in step.get('options', []):
+ print(get_indent(ctx) + " " + option)
+ ctx['browser'].pass_options(option)
+
+
+def run_test_step_action_window_new(ctx, step):
+
+ # pylint: disable=locally-disabled, invalid-name
+
+ 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):
+
+ # pylint: disable=locally-disabled, invalid-name
+
+ 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)
+ timeout = int(step.get('timeout', 30))
+ win.kill()
+ win.wait_until_dead(timeout=timeout)
+ assert not win.alive
+
+
+def run_test_step_action_navigate(ctx, step):
+ print(get_indent(ctx) + "Action: " + step["action"])
+ assert_browser(ctx)
+ if 'url' in step.keys():
+ url = step['url']
+ elif 'repeaturl' in step.keys():
+ repeat = ctx['repeats'].get(step['repeaturl'])
+ assert repeat is not None
+ assert repeat.get('values') is not None
+ url = repeat['values'][repeat['i']]
+ else:
+ url = None
+ assert url is not None
+ tag = step['window']
+ print(get_indent(ctx) + " " + tag + " --> " + url)
+ win = ctx['windows'].get(tag)
+ assert win is not None
+ win.go(url)
+
+
+def run_test_step_action_stop(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.stop()
+
+
+def run_test_step_action_reload(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.reload()
+
+
+def run_test_step_action_sleep_ms(ctx, step):
+ print(get_indent(ctx) + "Action: " + step["action"])
+ conds = step.get('conditions', {})
+ sleep_time = step['time']
+ sleep = 0
+ have_repeat = False
+ if isinstance(sleep_time, str):
+ assert ctx['repeats'].get(sleep_time) is not None
+ repeat = ctx['repeats'].get(sleep_time)
+ sleep = repeat["i"] / 1000
+ start = repeat["start"]
+ have_repeat = True
+ else:
+ sleep = sleep_time / 1000
+ start = time.time()
+
+ while True:
+ slept = time.time() - start
+ if conds_met(ctx, conds):
+ if have_repeat:
+ ctx['repeats'][sleep_time]["loop"] = False
+ print(get_indent(ctx) + " Condition met after {}s".format(slept))
+ break
+ elif slept > sleep:
+ print(get_indent(ctx) + " Condition not met after {}s".format(sleep))
+ break
+ else:
+ ctx['browser'].farmer.loop(once=True)
+
+
+def run_test_step_action_block(ctx, step):
+ print(get_indent(ctx) + "Action: " + step["action"])
+ conds = step['conditions']
+ assert_browser(ctx)
+
+ while not conds_met(ctx, conds):
+ ctx['browser'].farmer.loop(once=True)
+
+
+def run_test_step_action_repeat(ctx, step):
+ print(get_indent(ctx) + "Action: " + step["action"])
+ tag = step['tag']
+ assert ctx['repeats'].get(tag) is None
+ # initialise the loop continue conditional
+ ctx['repeats'][tag] = {"loop": True, }
+
+ if 'values' in step.keys():
+ # value iterator
+ ctx['repeats'][tag]['values'] = step["values"]
+ ctx['repeats'][tag]["max"] = len(step["values"])
+ ctx['repeats'][tag]["i"] = 0
+ ctx['repeats'][tag]["step"] = 1
+ else:
+ # numeric iterator
+ ctx['repeats'][tag]['values'] = None
+
+ if 'min' in step.keys():
+ ctx['repeats'][tag]["i"] = step["min"]
+ else:
+ ctx['repeats'][tag]["i"] = 0
+
+ if 'step' in step.keys():
+ ctx['repeats'][tag]["step"] = step["step"]
+ else:
+ ctx['repeats'][tag]["step"] = 1
+
+ if 'max' in step.keys():
+ ctx['repeats'][tag]["max"] = step["max"]
+ else:
+ ctx['repeats'][tag]["max"] = None
+
+ while ctx['repeats'][tag]["loop"]:
+ ctx['repeats'][tag]["start"] = time.time()
+ ctx["depth"] += 1
+
+ # run through steps for this iteration
+ for stp in step["steps"]:
+ run_test_step(ctx, stp)
+
+ # increment iterator
+ ctx['repeats'][tag]["i"] += ctx['repeats'][tag]["step"]
+
+ # check for end condition
+ if ctx['repeats'][tag]["max"] is not None:
+ if ctx['repeats'][tag]["i"] >= ctx['repeats'][tag]["max"]:
+ ctx['repeats'][tag]["loop"] = False
+
+ ctx["depth"] -= 1
+
+
+def run_test_step_action_click(ctx, step):
+ print(get_indent(ctx) + "Action: " + step["action"])
+ assert_browser(ctx)
+ win = ctx['windows'][step['window']]
+ targets = step['target']
+ if type(targets) == dict:
+ targets = [targets]
+ button = step.get('button', 'left').upper()
+ kind = step.get('kind', 'single').upper()
+ all_text_list = []
+ bitmaps = []
+ for plot in win.redraw():
+ if plot[0] == 'TEXT':
+ all_text_list.append((int(plot[2]), int(plot[4]), " ".join(plot[6:])))
+ if plot[0] == 'BITMAP':
+ bitmaps.append((int(plot[2]), int(plot[4]), int(plot[6]), int(plot[8])))
+
+ x = None
+ y = None
+
+ for target in targets:
+ if 'bitmap' in target:
+ if x is not None:
+ assert False, "Found more than one thing to click on, oh well"
+ bmap = int(target['bitmap'])
+ assert bmap < 0 or bmap >= len(bitmaps)
+ x = bitmaps[bmap][0] + bitmaps[bmap][2] / 2
+ y = bitmaps[bmap][1] + bitmaps[bmap][3] / 2
+ elif 'text' in target:
+ if x is not None:
+ assert False, "Found more than one thing to click on, oh well"
+ text = target['text']
+ for textentry in all_text_list:
+ if text in textentry[2]:
+ if x is not None:
+ assert False, "Text {} found more than once".format(text)
+ x = textentry[0] + 2
+ y = textentry[1] + 2
+
+ # Now we want to click on the x/y coordinate given
+ print(get_indent(ctx) + " Clicking at {}, {} (button={} kind={})".format(x, y, button, kind))
+ win.click(x, y, button, kind)
+
+
+def run_test_step_action_wait_loading(ctx, step):
+ print(get_indent(ctx) + "Action: " + step["action"])
+ assert_browser(ctx)
+ win = ctx['windows'][step['window']]
+ win.wait_start_loading()
+
+def run_test_step_action_plot_check(ctx, step):
+ print(get_indent(ctx) + "Action: " + step["action"])
+ assert_browser(ctx)
+ win = ctx['windows'][step['window']]
+
+ if 'area' in step.keys():
+ if step["area"] == "extent":
+ # ought to capture the extent updates and use that, instead use a
+ # big area and have the browser clip it
+ area=["0","0","1000","1000000"]
+ else:
+ area = [step["area"]]
+ else:
+ area = None
+
+ # get the list of checks
+ if 'checks' in step.keys():
+ checks = step['checks']
+ else:
+ checks = {}
+
+ all_text_list = []
+ bitmaps = []
+ for plot in win.redraw(coords=area):
+ if plot[0] == 'TEXT':
+ all_text_list.extend(plot[6:])
+ if plot[0] == 'BITMAP':
+ bitmaps.append(plot[1:])
+ all_text = " ".join(all_text_list)
+ 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 'text-not-contains' in check.keys():
+ print(" Check {} NOT in {}".format(repr(check['text-not-contains']), repr(all_text)))
+ assert check['text-not-contains'] not in all_text
+ elif 'bitmap-count' in check.keys():
+ print(" Check bitmap count is {}".format(int(check['bitmap-count'])))
+ assert len(bitmaps) == int(check['bitmap-count'])
+ else:
+ raise AssertionError("Unknown check: {}".format(repr(check)))
+
+
+def run_test_step_action_timer_start(ctx, step):
+
+ # pylint: disable=locally-disabled, invalid-name
+
+ print(get_indent(ctx) + "Action: " + step["action"])
+ tag = step['timer']
+ assert_browser(ctx)
+ assert ctx['timers'].get(tag) is None
+ ctx['timers'][tag] = {}
+ ctx['timers'][tag]["start"] = time.time()
+
+
+def run_test_step_action_timer_restart(ctx, step):
+
+ # pylint: disable=locally-disabled, invalid-name
+
+ print(get_indent(ctx) + "Action: " + step["action"])
+ timer = step['timer']
+ assert_browser(ctx)
+ assert ctx['timers'].get(timer) is not None
+ taken = time.time() - ctx['timers'][timer]["start"]
+ print("{} {} restarted at: {:.2f}s".format(get_indent(ctx), timer, taken))
+ ctx['timers'][timer]["taken"] = taken
+ ctx['timers'][timer]["start"] = time.time()
+
+
+def run_test_step_action_timer_stop(ctx, step):
+ print(get_indent(ctx) + "Action: " + step["action"])
+ timer = step['timer']
+ assert_browser(ctx)
+ assert ctx['timers'].get(timer) is not None
+ taken = time.time() - ctx['timers'][timer]["start"]
+ print("{} {} took: {:.2f}s".format(get_indent(ctx), timer, taken))
+ ctx['timers'][timer]["taken"] = taken
+
+
+def run_test_step_action_timer_check(ctx, step):
+
+ # pylint: disable=locally-disabled, invalid-name
+
+ print(get_indent(ctx) + "Action: " + step["action"])
+ condition = step["condition"].split()
+ assert len(condition) == 3
+ timer1 = ctx['timers'].get(condition[0])
+ timer2 = ctx['timers'].get(condition[2])
+ assert timer1 is not None
+ assert timer2 is not None
+ assert timer1["taken"] is not None
+ assert timer2["taken"] is not None
+ assert condition[1] in ('<', '>')
+ if condition[1] == '<':
+ assert timer1["taken"] < timer2["taken"]
+ elif condition[1] == '>':
+ assert timer1["taken"] > timer2["taken"]
+
+
+def run_test_step_action_add_auth(ctx, step):
+ print(get_indent(ctx) + "Action:" + step["action"])
+ assert_browser(ctx)
+ browser = ctx['browser']
+ browser.add_auth(step.get("url"), step.get("realm"),
+ step.get("username"), step.get("password"))
+
+
+def run_test_step_action_remove_auth(ctx, step):
+
+ # pylint: disable=locally-disabled, invalid-name
+
+ print(get_indent(ctx) + "Action:" + step["action"])
+ assert_browser(ctx)
+ browser = ctx['browser']
+ browser.remove_auth(step.get("url"), step.get("realm"),
+ step.get("username"), step.get("password"))
+
+
+def run_test_step_action_clear_log(ctx, step):
+ print(get_indent(ctx) + "Action: " + step["action"])
+ assert_browser(ctx)
+ tag = step['window']
+ print(get_indent(ctx) + " " + tag + " Log cleared")
+ win = ctx['windows'].get(tag)
+ assert win is not None
+ win.clear_log()
+
+
+def run_test_step_action_wait_log(ctx, step):
+ print(get_indent(ctx) + "Action: " + step["action"])
+ assert_browser(ctx)
+ tag = step['window']
+ source = step.get('source')
+ foldable = step.get('foldable')
+ level = step.get('level')
+ substr = step.get('substring')
+ print(get_indent(ctx) + " " + tag + " Wait for logging")
+ win = ctx['windows'].get(tag)
+ assert win is not None
+ win.wait_for_log(source=source, foldable=foldable, level=level, substr=substr)
+
+
+def run_test_step_action_js_exec(ctx, step):
+ print(get_indent(ctx) + "Action: " + step["action"])
+ assert_browser(ctx)
+ tag = step['window']
+ cmd = step['cmd']
+ print(get_indent(ctx) + " " + tag + " Run " + cmd)
+ win = ctx['windows'].get(tag)
+ assert win is not None
+ win.js_exec(cmd)
+
+
+def run_test_step_action_page_info_state(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
+ match = step['match']
+ assert win.page_info_state == match
+
+
+def run_test_step_action_quit(ctx, step):
+ print(get_indent(ctx) + "Action: " + step["action"])
+ assert_browser(ctx)
+ browser = ctx.pop('browser')
+ assert browser.quit_and_wait()
+ # clean up context as all windows have gone away after browser quit
+ ctx.pop('windows')
+
+
+STEP_HANDLERS = {
+ "launch": run_test_step_action_launch,
+ "window-new": run_test_step_action_window_new,
+ "window-close": run_test_step_action_window_close,
+ "navigate": run_test_step_action_navigate,
+ "reload": run_test_step_action_reload,
+ "stop": run_test_step_action_stop,
+ "sleep-ms": run_test_step_action_sleep_ms,
+ "block": run_test_step_action_block,
+ "repeat": run_test_step_action_repeat,
+ "timer-start": run_test_step_action_timer_start,
+ "timer-restart": run_test_step_action_timer_restart,
+ "timer-stop": run_test_step_action_timer_stop,
+ "timer-check": run_test_step_action_timer_check,
+ "plot-check": run_test_step_action_plot_check,
+ "click": run_test_step_action_click,
+ "wait-loading": run_test_step_action_wait_loading,
+ "add-auth": run_test_step_action_add_auth,
+ "remove-auth": run_test_step_action_remove_auth,
+ "clear-log": run_test_step_action_clear_log,
+ "wait-log": run_test_step_action_wait_log,
+ "js-exec": run_test_step_action_js_exec,
+ "page-info-state":
+ run_test_step_action_page_info_state,
+ "quit": run_test_step_action_quit,
+}
+
+
+def run_test_step(ctx, step):
+ STEP_HANDLERS[step["action"]](ctx, step)
+
+
+def walk_test_plan(ctx, plan):
+ ctx["depth"] = 0
+ ctx["timers"] = dict()
+ ctx['repeats'] = dict()
+ for step in plan["steps"]:
+ run_test_step(ctx, step)
+
+
+def run_test_plan(ctx, plan):
+ print_test_plan_info(ctx, plan)
+ walk_test_plan(ctx, plan)
+
+
+def run_preloaded_test(path_monkey, plan):
+ ctx = {
+ "monkey": path_monkey,
+ }
+ run_test_plan(ctx, plan)
+
+
+def main(argv):
+ ctx = {}
+ path_monkey, path_test, wrapper = parse_argv(argv)
+ plan = load_test_plan(path_test)
+ ctx["monkey"] = path_monkey
+ ctx["wrapper"] = wrapper
+ run_test_plan(ctx, plan)
+
+
+# Some python weirdness to get to main().
+if __name__ == "__main__":
+ main(sys.argv[1:])