660 lines
20 KiB
Python
660 lines
20 KiB
Python
# coding=utf-8
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
import functools
|
|
import threading
|
|
import cherrypy
|
|
import json
|
|
import subprocess
|
|
import traceback
|
|
import webbrowser
|
|
import argparse
|
|
import shlex
|
|
from mako.template import Template
|
|
from uuid import uuid4
|
|
import telenium
|
|
from telenium.client import TeleniumHttpClient
|
|
from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
|
|
from ws4py.websocket import WebSocket
|
|
from os.path import dirname, join, realpath
|
|
from time import time, sleep
|
|
|
|
SESSION_FN = ".telenium.dat"
|
|
|
|
TPL_EXPORT_UNITTEST = u"""<%!
|
|
def capitalize(text):
|
|
return text.capitalize()
|
|
def camelcase(text):
|
|
return "".join([x.strip().capitalize() for x in text.split()])
|
|
def funcname(text):
|
|
if text == "init":
|
|
return "init"
|
|
import re
|
|
suffix = re.sub(r"[^a-z0-9_]", "_", text.lower().strip())
|
|
return "test_{}".format(suffix)
|
|
def getarg(text):
|
|
import re
|
|
return re.match("^(\w+)", text).groups()[0]
|
|
%># coding=utf-8
|
|
|
|
import time
|
|
from telenium.tests import TeleniumTestCase
|
|
|
|
|
|
class ${settings["project"]|camelcase}TestCase(TeleniumTestCase):
|
|
% if env:
|
|
cmd_env = ${ env }
|
|
% endif
|
|
cmd_entrypoint = [u'${ settings["entrypoint"] }']
|
|
% for test in tests:
|
|
% if test["name"] == "setUpClass":
|
|
<% vself = "cls" %>
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(${settings["project"]|camelcase}TestCase, cls).setUpClass()
|
|
% else:
|
|
<% vself = "self" %>
|
|
def ${test["name"]|funcname}(self):
|
|
% if not test["steps"]:
|
|
pass
|
|
% endif
|
|
% endif
|
|
% for key, value, arg1, arg2 in test["steps"]:
|
|
% if key == "wait":
|
|
${vself}.cli.wait('${value}', timeout=${settings["command-timeout"]})
|
|
% elif key == "wait_click":
|
|
${vself}.cli.wait_click('${value}', timeout=${settings["command-timeout"]})
|
|
% elif key == "assertExists":
|
|
${vself}.assertExists('${value}', timeout=${settings["command-timeout"]})
|
|
% elif key == "assertNotExists":
|
|
${vself}.assertNotExists('${value}', timeout=${settings["command-timeout"]})
|
|
% elif key == "assertAttributeValue":
|
|
attr_name = '${arg1|getarg}'
|
|
attr_value = ${vself}.cli.getattr('${value}', attr_name)
|
|
${vself}.assertTrue(eval('${arg1}', {attr_name: attr_value}))
|
|
% elif key == "setAttribute":
|
|
${vself}.cli.setattr('${value}', '${arg1}', ${arg2})
|
|
% elif key == "sendKeycode":
|
|
${vself}.cli.send_keycode('${value}')
|
|
% elif key == "sleep":
|
|
time.sleep(${value})
|
|
% elif key == "executeCode":
|
|
${vself}.assertTrue(self.cli.execute('${value}'))
|
|
% endif
|
|
% endfor
|
|
% endfor
|
|
"""
|
|
|
|
FILE_API_VERSION = 3
|
|
local_filename = None
|
|
|
|
|
|
def threaded(f):
|
|
@functools.wraps(f)
|
|
def _threaded(*args, **kwargs):
|
|
thread = threading.Thread(target=f, args=args, kwargs=kwargs)
|
|
thread.daemon = True
|
|
thread.start()
|
|
return thread
|
|
|
|
return _threaded
|
|
|
|
|
|
def funcname(text):
|
|
return text.lower().replace(" ", "_").strip()
|
|
|
|
|
|
def getarg(text):
|
|
return re.match("^(\w+)", text).groups()[0]
|
|
|
|
|
|
class ApiWebSocket(WebSocket):
|
|
t_process = None
|
|
cli = None
|
|
progress_count = 0
|
|
progress_total = 0
|
|
session = {
|
|
"settings": {
|
|
"project": "Test",
|
|
"entrypoint": "main.py",
|
|
"application-timeout": "10",
|
|
"command-timeout": "5",
|
|
"args": ""
|
|
},
|
|
"env": {},
|
|
"tests": [{
|
|
"id": str(uuid4()),
|
|
"name": "New test",
|
|
"steps": []
|
|
}]
|
|
}
|
|
|
|
def opened(self):
|
|
super(ApiWebSocket, self).opened()
|
|
self.load()
|
|
|
|
def closed(self, code, reason=None):
|
|
pass
|
|
|
|
def received_message(self, message):
|
|
msg = json.loads(message.data)
|
|
try:
|
|
getattr(self, "cmd_{}".format(msg["cmd"]))(msg["options"])
|
|
except:
|
|
traceback.print_exc()
|
|
|
|
def send_object(self, obj):
|
|
data = json.dumps(obj)
|
|
self.send(data, False)
|
|
|
|
def save(self):
|
|
self.session["version_format"] = FILE_API_VERSION
|
|
|
|
# check if the file changed
|
|
if local_filename is not None:
|
|
changed = False
|
|
try:
|
|
with open(local_filename) as fd:
|
|
data = json.loads(fd.read())
|
|
changed = data != self.session
|
|
except:
|
|
changed = True
|
|
self.send_object(["changed", changed])
|
|
|
|
with open(SESSION_FN, "w") as fd:
|
|
fd.write(json.dumps(self.session))
|
|
|
|
def load(self):
|
|
try:
|
|
with open(SESSION_FN) as fd:
|
|
session = json.loads(fd.read())
|
|
session = upgrade_version(session)
|
|
self.session.update(session)
|
|
except:
|
|
pass
|
|
|
|
def get_test(self, test_id):
|
|
for test in self.session["tests"]:
|
|
if test["id"] == test_id:
|
|
return test
|
|
|
|
def get_test_by_name(self, name):
|
|
for test in self.session["tests"]:
|
|
if test["name"] == name:
|
|
return test
|
|
|
|
@property
|
|
def is_running(self):
|
|
return self.t_process is not None
|
|
|
|
# command implementation
|
|
|
|
def cmd_recover(self, options):
|
|
if local_filename:
|
|
self.send_object(["is_local", True])
|
|
self.send_object(["settings", self.session["settings"]])
|
|
self.send_object(["env", dict(self.session["env"].items())])
|
|
tests = [{
|
|
"name": x["name"],
|
|
"id": x["id"]
|
|
} for x in self.session["tests"]]
|
|
self.send_object(["tests", tests])
|
|
if self.t_process is not None:
|
|
self.send_object(["status", "running"])
|
|
|
|
def cmd_save_local(self, options):
|
|
try:
|
|
assert local_filename is not None
|
|
# save json source
|
|
data = self.export("json")
|
|
with open(local_filename, "w") as fd:
|
|
fd.write(data)
|
|
|
|
# auto export to python
|
|
filename = local_filename.replace(".json", ".py")
|
|
data = self.export("python")
|
|
with open(filename, "w") as fd:
|
|
fd.write(data)
|
|
self.send_object(["save_local", "ok"])
|
|
self.send_object(["changed", False])
|
|
except Exception as e:
|
|
traceback.print_exc()
|
|
self.send_object(["save_local", "error", repr(e)])
|
|
|
|
def cmd_sync_env(self, options):
|
|
while self.session["env"]:
|
|
self.session["env"].pop(self.session["env"].keys()[0])
|
|
for key, value in options.get("env", {}).items():
|
|
self.session["env"][key] = value
|
|
self.save()
|
|
|
|
def cmd_sync_settings(self, options):
|
|
self.session["settings"] = options["settings"]
|
|
self.save()
|
|
|
|
def cmd_sync_test(self, options):
|
|
uid = options["id"]
|
|
for test in self.session["tests"]:
|
|
if test["id"] == uid:
|
|
test["name"] = options["name"]
|
|
test["steps"] = options["steps"]
|
|
self.save()
|
|
|
|
def cmd_add_test(self, options):
|
|
self.session["tests"].append({
|
|
"id": str(uuid4()),
|
|
"name": "New test",
|
|
"steps": []
|
|
})
|
|
self.save()
|
|
self.send_object(["tests", self.session["tests"]])
|
|
|
|
def cmd_clone_test(self, options):
|
|
for test in self.session["tests"]:
|
|
if test["id"] != options["test_id"]:
|
|
continue
|
|
clone_test = test.copy()
|
|
clone_test["id"] = str(uuid4())
|
|
clone_test["name"] += " (1)"
|
|
self.session["tests"].append(clone_test)
|
|
break
|
|
self.save()
|
|
self.send_object(["tests", self.session["tests"]])
|
|
|
|
def cmd_delete_test(self, options):
|
|
for test in self.session["tests"][:]:
|
|
if test["id"] == options["id"]:
|
|
self.session["tests"].remove(test)
|
|
if not self.session["tests"]:
|
|
self.cmd_add_test(None)
|
|
self.save()
|
|
self.send_object(["tests", self.session["tests"]])
|
|
|
|
def cmd_select(self, options):
|
|
if not self.cli:
|
|
status = "error"
|
|
results = "Application not running"
|
|
else:
|
|
try:
|
|
results = self.cli.highlight(options["selector"])
|
|
status = "ok"
|
|
except Exception as e:
|
|
status = "error"
|
|
results = u"{}".format(e)
|
|
self.send_object(["select", options["selector"], status, results])
|
|
|
|
def cmd_select_test(self, options):
|
|
test = self.get_test(options["id"])
|
|
self.send_object(["test", test])
|
|
|
|
@threaded
|
|
def cmd_pick(self, options):
|
|
if not self.cli:
|
|
return self.send_object(["pick", "error", "App is not started"])
|
|
objs = self.cli.pick(True)
|
|
return self.send_object(["pick", "success", objs])
|
|
|
|
@threaded
|
|
def cmd_execute(self, options):
|
|
self.execute()
|
|
|
|
def cmd_run_step(self, options):
|
|
self.run_step(options["id"], options["index"])
|
|
|
|
@threaded
|
|
def cmd_run_steps(self, options):
|
|
test = self.get_test(options["id"])
|
|
if test is None:
|
|
self.send_object(["alert", "Test not found"])
|
|
return
|
|
if not self.is_running:
|
|
ev_start, ev_stop = self.execute()
|
|
ev_start.wait()
|
|
if ev_stop.is_set():
|
|
return
|
|
self.run_test(test)
|
|
|
|
@threaded
|
|
def cmd_run_tests(self, options):
|
|
# restart always from scratch
|
|
self.send_object(["progress", "started"])
|
|
|
|
# precalculate the number of steps to run
|
|
count = sum([len(x["steps"]) for x in self.session["tests"]])
|
|
self.progress_count = 0
|
|
self.progress_total = count
|
|
|
|
try:
|
|
ev_start, ev_stop = self.execute()
|
|
ev_start.wait()
|
|
if ev_stop.is_set():
|
|
return
|
|
setup = self.get_test_by_name("setUpClass")
|
|
if setup:
|
|
if not self.run_test(setup):
|
|
return
|
|
setup = self.get_test_by_name("init")
|
|
if setup:
|
|
if not self.run_test(setup):
|
|
return
|
|
for test in self.session["tests"]:
|
|
if test["name"] in ("setUpClass", "init"):
|
|
continue
|
|
if not self.run_test(test):
|
|
return
|
|
finally:
|
|
self.send_object(["progress", "finished"])
|
|
|
|
def cmd_stop(self, options):
|
|
if self.t_process:
|
|
self.t_process.terminate()
|
|
|
|
def cmd_export(self, options):
|
|
try:
|
|
dtype = options["type"]
|
|
mimetype = {
|
|
"python": "text/plain",
|
|
"json": "application/json"
|
|
}[dtype]
|
|
ext = {"python": "py", "json": "json"}[dtype]
|
|
key = funcname(self.session["settings"]["project"])
|
|
filename = "test_ui_{}.{}".format(key, ext)
|
|
export = self.export(options["type"])
|
|
self.send_object(["export", export, mimetype, filename, dtype])
|
|
except Exception as e:
|
|
self.send_object(["export", "error", u"{}".format(e)])
|
|
|
|
def export(self, kind):
|
|
if kind == "python":
|
|
return Template(TPL_EXPORT_UNITTEST).render(
|
|
session=self.session, **self.session)
|
|
elif kind == "json":
|
|
self.session["version_format"] = FILE_API_VERSION
|
|
return json.dumps(
|
|
self.session, sort_keys=True, indent=4, separators=(',', ': '))
|
|
|
|
def execute(self):
|
|
ev_start = threading.Event()
|
|
ev_stop = threading.Event()
|
|
self._execute(ev_start=ev_start, ev_stop=ev_stop)
|
|
return ev_start, ev_stop
|
|
|
|
@threaded
|
|
def _execute(self, ev_start, ev_stop):
|
|
self.t_process = None
|
|
try:
|
|
self.start_process()
|
|
ev_start.set()
|
|
self.t_process.communicate()
|
|
self.send_object(["status", "stopped", None])
|
|
except Exception as e:
|
|
try:
|
|
self.t_process.terminate()
|
|
except:
|
|
pass
|
|
try:
|
|
self.send_object(["status", "stopped", u"{}".format(e)])
|
|
except:
|
|
pass
|
|
finally:
|
|
self.t_process = None
|
|
ev_stop.set()
|
|
|
|
def start_process(self):
|
|
url = "http://localhost:9901/jsonrpc"
|
|
process_start_timeout = 10
|
|
telenium_token = str(uuid4())
|
|
self.cli = cli = TeleniumHttpClient(url=url, timeout=10)
|
|
|
|
# entry no any previous telenium is running
|
|
try:
|
|
cli.app_quit()
|
|
sleep(2)
|
|
except:
|
|
pass
|
|
|
|
# prepare the application
|
|
entrypoint = self.session["settings"]["entrypoint"]
|
|
print(self.session)
|
|
args = shlex.split(self.session["settings"].get("args", ""))
|
|
cmd = [sys.executable, "-m", "telenium.execute", entrypoint] + args
|
|
cwd = dirname(entrypoint)
|
|
if not os.path.isabs(cwd):
|
|
cwd = os.getcwd()
|
|
env = os.environ.copy()
|
|
env.update(self.session["env"])
|
|
env["TELENIUM_TOKEN"] = telenium_token
|
|
|
|
# start the application
|
|
self.t_process = subprocess.Popen(cmd, env=env, cwd=cwd)
|
|
|
|
# wait for telenium server to be online
|
|
start = time()
|
|
while True:
|
|
try:
|
|
if cli.app_ready():
|
|
break
|
|
except Exception:
|
|
if time() - start > process_start_timeout:
|
|
raise Exception("timeout")
|
|
sleep(1)
|
|
|
|
# ensure the telenium we are connected are the same as the one we
|
|
# launched here
|
|
if cli.get_token() != telenium_token:
|
|
raise Exception("Connected to another telenium server")
|
|
|
|
self.send_object(["status", "running"])
|
|
|
|
def run_test(self, test):
|
|
test_id = test["id"]
|
|
try:
|
|
self.send_object(["test", test])
|
|
self.send_object(["run_test", test_id, "running"])
|
|
for index, step in enumerate(test["steps"]):
|
|
if not self.run_step(test_id, index):
|
|
return
|
|
return True
|
|
except Exception as e:
|
|
self.send_object(["run_test", test_id, "error", str(e)])
|
|
else:
|
|
self.send_object(["run_test", test_id, "finished"])
|
|
|
|
def run_step(self, test_id, index):
|
|
self.progress_count += 1
|
|
self.send_object(
|
|
["progress", "update", self.progress_count, self.progress_total])
|
|
try:
|
|
self.send_object(["run_step", test_id, index, "running"])
|
|
success = self._run_step(test_id, index)
|
|
if success:
|
|
self.send_object(["run_step", test_id, index, "success"])
|
|
return True
|
|
else:
|
|
self.send_object(["run_step", test_id, index, "error"])
|
|
except Exception as e:
|
|
self.send_object(["run_step", test_id, index, "error", str(e)])
|
|
|
|
def _run_step(self, test_id, index):
|
|
test = self.get_test(test_id)
|
|
if not test:
|
|
raise Exception("Unknown test")
|
|
cmd, selector, arg1, arg2 = test["steps"][index]
|
|
timeout = 5
|
|
if cmd == "wait":
|
|
return self.cli.wait(selector, timeout=timeout)
|
|
elif cmd == "wait_click":
|
|
self.cli.wait_click(selector, timeout=timeout)
|
|
return True
|
|
elif cmd == "wait_drag":
|
|
self.cli.wait_drag(
|
|
selector, target=arg1, duration=arg2, timeout=timeout)
|
|
return True
|
|
elif cmd == "assertExists":
|
|
return self.cli.wait(selector, timeout=timeout) is True
|
|
elif cmd == "assertNotExists":
|
|
return self.assertNotExists(self.cli, selector, timeout=timeout)
|
|
elif cmd == "assertAttributeValue":
|
|
attr_name = getarg(arg1)
|
|
attr_value = self.cli.getattr(selector, attr_name)
|
|
return bool(eval(arg1, {attr_name: attr_value}))
|
|
elif cmd == "setAttribute":
|
|
return self.cli.setattr(selector, arg1, eval(arg2))
|
|
elif cmd == "sendKeycode":
|
|
self.cli.send_keycode(selector)
|
|
return True
|
|
elif cmd == "sleep":
|
|
sleep(float(selector))
|
|
return True
|
|
elif cmd == "executeCode":
|
|
return self.cli.execute(selector)
|
|
|
|
def assertNotExists(self, cli, selector, timeout=-1):
|
|
start = time()
|
|
while True:
|
|
matches = cli.select(selector)
|
|
if not matches:
|
|
return True
|
|
if timeout == -1:
|
|
raise AssertionError("selector matched elements")
|
|
if timeout > 0 and time() - start > timeout:
|
|
raise Exception("Timeout")
|
|
sleep(0.1)
|
|
|
|
|
|
class Root(object):
|
|
@cherrypy.expose
|
|
def index(self):
|
|
raise cherrypy.HTTPRedirect("/static/index.html")
|
|
|
|
@cherrypy.expose
|
|
def ws(self):
|
|
pass
|
|
|
|
|
|
class WebSocketServer(object):
|
|
def __init__(self, host="0.0.0.0", port=8080, open_webbrowser=True):
|
|
super(WebSocketServer, self).__init__()
|
|
self.host = host
|
|
self.port = port
|
|
self.daemon = True
|
|
self.open_webbrowser = open_webbrowser
|
|
|
|
def run(self):
|
|
cherrypy.config.update({
|
|
"global": {
|
|
"environment": "production"
|
|
},
|
|
"server.socket_port": self.port,
|
|
"server.socket_host": self.host,
|
|
})
|
|
cherrypy.tree.mount(
|
|
Root(),
|
|
"/",
|
|
config={
|
|
"/": {
|
|
"tools.sessions.on": True
|
|
},
|
|
"/ws": {
|
|
"tools.websocket.on": True,
|
|
"tools.websocket.handler_cls": ApiWebSocket
|
|
},
|
|
"/static": {
|
|
"tools.staticdir.on":
|
|
True,
|
|
"tools.staticdir.dir":
|
|
join(realpath(dirname(__file__)), "static"),
|
|
"tools.staticdir.index":
|
|
"index.html"
|
|
}
|
|
})
|
|
cherrypy.engine.start()
|
|
url = "http://{}:{}/".format(self.host, self.port)
|
|
print("Telenium {} ready at {}".format(telenium.__version__, url))
|
|
if self.open_webbrowser:
|
|
webbrowser.open(url)
|
|
cherrypy.engine.block()
|
|
|
|
def stop(self):
|
|
cherrypy.engine.exit()
|
|
cherrypy.server.stop()
|
|
|
|
|
|
def preload_session(filename):
|
|
global local_filename
|
|
local_filename = filename
|
|
if not local_filename.endswith(".json"):
|
|
print("You can load only telenium-json files.")
|
|
sys.exit(1)
|
|
if not os.path.exists(filename):
|
|
print("Create new file at {}".format(local_filename))
|
|
if os.path.exists(SESSION_FN):
|
|
os.unlink(SESSION_FN)
|
|
else:
|
|
with open(filename) as fd:
|
|
session = json.loads(fd.read())
|
|
session = upgrade_version(session)
|
|
with open(SESSION_FN, "w") as fd:
|
|
fd.write(json.dumps(session))
|
|
|
|
|
|
def upgrade_version(session):
|
|
# automatically upgrade to latest version
|
|
version_format = session.get("version_format")
|
|
if version_format is None or version_format == FILE_API_VERSION:
|
|
return session
|
|
session["version_format"] += 1
|
|
version_format = session["version_format"]
|
|
print("Upgrade to version {}".format(version_format))
|
|
if version_format == 2:
|
|
# arg added in steps, so steps must have 3 arguments not 2.
|
|
for test in session["tests"]:
|
|
for step in test["steps"]:
|
|
if len(step) == 2:
|
|
step.append(None)
|
|
elif version_format == 3:
|
|
# arg added in steps, so steps must have 4 arguments not 3.
|
|
for test in session["tests"]:
|
|
for step in test["steps"]:
|
|
if len(step) == 3:
|
|
step.append(None)
|
|
return session
|
|
|
|
|
|
WebSocketPlugin(cherrypy.engine).subscribe()
|
|
cherrypy.tools.websocket = WebSocketTool()
|
|
|
|
|
|
def run():
|
|
|
|
parser = argparse.ArgumentParser(description="Telenium IDE")
|
|
parser.add_argument(
|
|
"filename",
|
|
type=str,
|
|
default=None,
|
|
nargs="?",
|
|
help="Telenium JSON file")
|
|
parser.add_argument(
|
|
"--new", action="store_true", help="Start a new session")
|
|
parser.add_argument(
|
|
"--port", type=int, default=8080, help="Telenium IDE port")
|
|
parser.add_argument(
|
|
"--notab",
|
|
action="store_true",
|
|
help="Prevent opening the IDE in the browser")
|
|
args = parser.parse_args()
|
|
if args.new:
|
|
if os.path.exists(SESSION_FN):
|
|
os.unlink(SESSION_FN)
|
|
if args.filename:
|
|
preload_session(args.filename)
|
|
server = WebSocketServer(port=args.port, open_webbrowser=not args.notab)
|
|
server.run()
|
|
server.stop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run()
|