diff --git a/.travis.yml b/.travis.yml index 5bac72c2dd..09fe449b5b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,10 +25,10 @@ install: - > sudo apt-get install -y at build-essential gfortran heirloom-mailx python-pip python-dev graphviz libgraphviz-dev python-jinja2 - python-sqlalchemy libxml-parser-perl libconfig-inifiles-perl + python3-sqlalchemy libxml-parser-perl libconfig-inifiles-perl libdbi-perl libdbd-sqlite3-perl latexmk texlive texlive-generic-extra texlive-latex-extra texlive-fonts-recommended - - pip install cherrypy EmPy Jinja2 requests sqlalchemy pycodestyle python-jose pyzmq colorama pillow + - pip install tornado EmPy Jinja2 requests sqlalchemy pycodestyle python-jose pyzmq colorama pillow - pip install pygraphviz --install-option="--include-path=/usr/include/graphviz" --install-option="--library-path=/usr/lib/graphviz/" - sudo sh -c 'echo "deb http://opensource.wandisco.com/ubuntu `lsb_release -cs` svn19" >> /etc/apt/sources.list.d/subversion19.list' - sudo wget -q http://opensource.wandisco.com/wandisco-debian.gpg -O- | sudo apt-key add - diff --git a/bin/rose-check-software b/bin/rose-check-software index 7e7808c3d9..83394ff2e5 100755 --- a/bin/rose-check-software +++ b/bin/rose-check-software @@ -290,9 +290,9 @@ def main(check=check): ]) check_all('Rosie', [ - check('py:cherrypy'), + check('py:tornado', (3, 0), attr_name='version'), check('py:requests', (2, 2, 1)), - check('py:sqlalchemy', (0, 6, 9)), + check('py:sqlalchemy', (0, 9)), check('svn', (1, 8), command_template=['--version', '--quiet']), check('fcm', version_template=r'FCM ([\d\.\-]+)'), check('cmd:perl', (5, 10, 1), @@ -365,4 +365,4 @@ if __name__ == '__main__': sys.exit(0 if docs() else 1) else: # Check software dependencies, report and exit. - main() \ No newline at end of file + main() diff --git a/bin/rose-test-battery b/bin/rose-test-battery index 6c29c92168..2e191f5fcf 100755 --- a/bin/rose-test-battery +++ b/bin/rose-test-battery @@ -68,7 +68,7 @@ else NPROC=$(grep -ic processor /proc/cpuinfo) else NPROC=$(python3 -c \ - 'import multiprocessing; print multiprocessing.cpu_count()') + 'import multiprocessing; print(multiprocessing.cpu_count())') fi exec prove -j "$NPROC" -s -r "${@:-t}" fi diff --git a/lib/bash/rose_init b/lib/bash/rose_init index 2bfa842371..19c7d41839 100644 --- a/lib/bash/rose_init +++ b/lib/bash/rose_init @@ -31,7 +31,6 @@ # * load any FUNC specified in the argument list. #------------------------------------------------------------------------------- - function rose_init() { set -eu ROSE_NS=$(basename $0) diff --git a/lib/html/template/rosie-disco/index.html b/lib/html/template/rosie-disco/index.html index b26fdd4213..abcc83415f 100644 --- a/lib/html/template/rosie-disco/index.html +++ b/lib/html/template/rosie-disco/index.html @@ -3,8 +3,8 @@ {{title}} @ {{host}} - - + + diff --git a/lib/html/template/rosie-disco/prefix-index.html b/lib/html/template/rosie-disco/prefix-index.html index 9eab209c67..a53d251841 100644 --- a/lib/html/template/rosie-disco/prefix-index.html +++ b/lib/html/template/rosie-disco/prefix-index.html @@ -4,8 +4,8 @@ - - + + @@ -27,7 +27,7 @@ - + {{prefix}}: {{title}} @ {{host}} diff --git a/lib/python/rose/apps/rose_ana_v1.py b/lib/python/rose/apps/rose_ana_v1.py index 0a9d1751ea..db06dc2d69 100644 --- a/lib/python/rose/apps/rose_ana_v1.py +++ b/lib/python/rose/apps/rose_ana_v1.py @@ -36,7 +36,6 @@ from rose.reporter import Reporter, Event from rose.resource import ResourceLocator from rose.app_run import BuiltinApp -import collections.abc WARN = -1 PASS = 0 diff --git a/lib/python/rose/cmp_source_vc.py b/lib/python/rose/cmp_source_vc.py index 3cbf60e893..d31a6036c4 100644 --- a/lib/python/rose/cmp_source_vc.py +++ b/lib/python/rose/cmp_source_vc.py @@ -30,7 +30,6 @@ from rose.reporter import Reporter from rose.run_source_vc import write_source_vc_info from rose.suite_engine_proc import SuiteEngineProcessor -import collections.abc class SuiteVCComparator(object): diff --git a/lib/python/rose/config_processor.py b/lib/python/rose/config_processor.py index dd2f8e60f3..7bda92dba8 100644 --- a/lib/python/rose/config_processor.py +++ b/lib/python/rose/config_processor.py @@ -25,7 +25,6 @@ from rose.popen import RosePopener from rose.scheme_handler import SchemeHandlersManager import sys -import collections.abc class UnknownContentError(Exception): diff --git a/lib/python/rose/config_processors/fileinstall.py b/lib/python/rose/config_processors/fileinstall.py index 2f0142a705..acc5dbafb5 100644 --- a/lib/python/rose/config_processors/fileinstall.py +++ b/lib/python/rose/config_processors/fileinstall.py @@ -38,7 +38,6 @@ import sys from tempfile import mkdtemp from urllib.parse import urlparse -import collections.abc class ConfigProcessorForFile(ConfigProcessorBase): diff --git a/lib/python/rose/env.py b/lib/python/rose/env.py index c62e9f273c..e9ffd0acdc 100644 --- a/lib/python/rose/env.py +++ b/lib/python/rose/env.py @@ -27,7 +27,6 @@ import os import re from rose.reporter import Event -import collections.abc # _RE_DEFAULT = re.compile(r""" diff --git a/lib/python/rose/fs_util.py b/lib/python/rose/fs_util.py index 15098f4437..bef3be24c8 100644 --- a/lib/python/rose/fs_util.py +++ b/lib/python/rose/fs_util.py @@ -23,7 +23,6 @@ import os from rose.reporter import Event import shutil -import collections.abc class FileSystemEvent(Event): diff --git a/lib/python/rose/host_select.py b/lib/python/rose/host_select.py index 13a3153f1d..a1d9615113 100644 --- a/lib/python/rose/host_select.py +++ b/lib/python/rose/host_select.py @@ -32,7 +32,6 @@ import sys from time import sleep, time import traceback -import collections.abc class NoHostError(Exception): diff --git a/lib/python/rose/macro.py b/lib/python/rose/macro.py index 778f196055..c0167ae4a9 100644 --- a/lib/python/rose/macro.py +++ b/lib/python/rose/macro.py @@ -57,7 +57,6 @@ def test_cleanup(stuff_to_remove): import rose.reporter import rose.resource import rose.variable -import collections.abc ALLOWED_MACRO_CLASS_METHODS = ["transform", "validate", "downgrade", "upgrade", diff --git a/lib/python/rose/metadata_graph.py b/lib/python/rose/metadata_graph.py index a8ef1ae4d4..a1f1fff777 100644 --- a/lib/python/rose/metadata_graph.py +++ b/lib/python/rose/metadata_graph.py @@ -218,11 +218,11 @@ def output_graph(graph, debug_mode=False, filename=None, form="svg"): if filename is None: image_file_handle = tempfile.NamedTemporaryFile(suffix=("." + form)) else: - image_file_handle = open(filename, "w") + image_file_handle = open(filename, "wb") graph.draw(image_file_handle.name, prog="dot") if debug_mode: image_file_handle.seek(0) - print(image_file_handle.read()) + print(image_file_handle.read().decode()) image_file_handle.close() return rose.external.launch_image_viewer(image_file_handle.name, run_fg=True) diff --git a/lib/python/rose/popen.py b/lib/python/rose/popen.py index 2ff140046b..cd7b298240 100644 --- a/lib/python/rose/popen.py +++ b/lib/python/rose/popen.py @@ -27,7 +27,6 @@ import shlex from subprocess import Popen, PIPE import sys -import collections.abc class RosePopenError(Exception): diff --git a/lib/python/rose/reporter.py b/lib/python/rose/reporter.py index ba1e150a29..e77e8e8121 100644 --- a/lib/python/rose/reporter.py +++ b/lib/python/rose/reporter.py @@ -19,13 +19,9 @@ # ----------------------------------------------------------------------------- """Reporter for diagnostic messages.""" -import queue -import multiprocessing import sys - import time -import collections.abc class Reporter(object): @@ -241,7 +237,7 @@ def get_prefix(self, kind, level): return self._tty_colour_err(Reporter.PREFIX_WARN) else: return self._tty_colour_err(Reporter.PREFIX_FAIL) - if isinstance(self.prefix, collections.abc.Callable): + if callable(self.prefix): return self.prefix(kind, level) else: return self.prefix @@ -270,57 +266,6 @@ def _tty_colour_err(self, str_): return str_ -class ReporterContextQueue(ReporterContext): - - """A context for the reporter object. - - It has the following attributes: - kind: - The message kind to report to this context. - (Reporter.KIND_ERR, Reporter.KIND_ERR or None.) - verbosity: - The verbosity of this context. - queue: - The multiprocessing.Queue. - prefix: - The default message prefix (str or callable). - - """ - - def __init__(self, - kind=None, - verbosity=Reporter.DEFAULT, - queue=None, - prefix=None): - ReporterContext.__init__(self, kind, verbosity, None, prefix) - if queue is None: - queue = multiprocessing.Manager().Queue() - self.queue = queue - self.closed = False - self._messages_pending = [] - - def close(self): - self._send_pending_messages() - self.closed = True - - def is_closed(self): - return self.closed - - def write(self, message): - self._messages_pending.append(message) - self._send_pending_messages() - - def _send_pending_messages(self): - while self._messages_pending: - message = self._messages_pending[0] - try: - self.queue.put(message, block=False) - except queue.Full: - break - else: - del self._messages_pending[0] - - class Event(object): """A base class for events suitable for feeding into a Reporter.""" diff --git a/lib/python/rose/run.py b/lib/python/rose/run.py index 1a17842981..3a89551157 100644 --- a/lib/python/rose/run.py +++ b/lib/python/rose/run.py @@ -29,7 +29,6 @@ import shlex import shutil from uuid import uuid4 -import collections.abc class RunConfigLoadEvent(Event): diff --git a/lib/python/rose/scheme_handler.py b/lib/python/rose/scheme_handler.py index 6fb386b111..ebf217c191 100644 --- a/lib/python/rose/scheme_handler.py +++ b/lib/python/rose/scheme_handler.py @@ -24,7 +24,6 @@ import inspect import os import sys -import collections.abc class SchemeHandlersManager(object): diff --git a/lib/python/rose/suite_engine_proc.py b/lib/python/rose/suite_engine_proc.py index 73aa60abeb..aed9949416 100644 --- a/lib/python/rose/suite_engine_proc.py +++ b/lib/python/rose/suite_engine_proc.py @@ -34,7 +34,6 @@ from rose.scheme_handler import SchemeHandlersManager import sys import webbrowser -import collections.abc class NoSuiteLogError(Exception): @@ -539,7 +538,7 @@ def get_version_env_name(self): def handle_event(self, *args, **kwargs): """Call self.event_handler if it is callable.""" - if isinstance(self.event_handler, collections.abc.Callable): + if callable(self.event_handler): return self.event_handler(*args, **kwargs) def gcontrol(self, suite_name, args=None): diff --git a/lib/python/rose/suite_hook.py b/lib/python/rose/suite_hook.py index a329911f88..857fb6f5e5 100644 --- a/lib/python/rose/suite_hook.py +++ b/lib/python/rose/suite_hook.py @@ -30,7 +30,6 @@ from rose.suite_engine_proc import SuiteEngineProcessor from smtplib import SMTP, SMTPException import socket -import collections.abc class RoseSuiteHook(object): @@ -49,7 +48,7 @@ def __init__(self, event_handler=None, popen=None, suite_engine_proc=None): def handle_event(self, *args, **kwargs): """Call self.event_handler if it is callabale.""" - if isinstance(self.event_handler, collections.abc.Callable): + if callable(self.event_handler): return self.event_handler(*args, **kwargs) def run(self, suite_name, task_id, hook_event, hook_message=None, diff --git a/lib/python/rose/suite_restart.py b/lib/python/rose/suite_restart.py index 4ef7e61234..a010436373 100644 --- a/lib/python/rose/suite_restart.py +++ b/lib/python/rose/suite_restart.py @@ -28,7 +28,6 @@ from rose.reporter import Reporter from rose.suite_control import get_suite_name, SuiteNotFoundError from rose.suite_engine_proc import SuiteEngineProcessor -import collections.abc class SuiteRestarter(object): @@ -42,7 +41,7 @@ def __init__(self, event_handler=None): def handle_event(self, *args, **kwargs): """Handle event.""" - if isinstance(self.event_handler, collections.abc.Callable): + if callable(self.event_handler): self.event_handler(*args, **kwargs) def restart( diff --git a/lib/python/rosie/db.py b/lib/python/rosie/db.py index 7cc19e6f66..2f93c079c0 100644 --- a/lib/python/rosie/db.py +++ b/lib/python/rosie/db.py @@ -229,6 +229,7 @@ def query(self, filters, all_revs=0): """ self._connect() + all_revs = int(all_revs) # so distinguish 0 or 1 below, else both True if all_revs: from_obj, cols = self._get_hist_join_and_columns() else: @@ -357,6 +358,7 @@ def search(self, s, all_revs=0): """ self._connect() + all_revs = int(all_revs) # so distinguish 0 or 1 below, else both True if all_revs: from_obj, cols = self._get_hist_join_and_columns() else: diff --git a/lib/python/rosie/db_create.py b/lib/python/rosie/db_create.py index 9b20b52f81..d3e9e06927 100644 --- a/lib/python/rosie/db_create.py +++ b/lib/python/rosie/db_create.py @@ -31,7 +31,6 @@ from rosie.db import ( LATEST_TABLE_NAME, MAIN_TABLE_NAME, META_TABLE_NAME, OPTIONAL_TABLE_NAME) from rosie.svn_post_commit import RosieSvnPostCommitHook -import collections.abc class RosieDatabaseCreateEvent(Event): diff --git a/lib/python/rosie/suite_id.py b/lib/python/rosie/suite_id.py index 30dce7b2b8..c58b615e4b 100644 --- a/lib/python/rosie/suite_id.py +++ b/lib/python/rosie/suite_id.py @@ -143,9 +143,10 @@ def get_latest(cls, prefix=None): if i == 0: return None raise SuiteIdLatestError(prefix) - dirs = [line for line in out.splitlines() if line.endswith(b"/")] + dirs = [line for line in out.decode().splitlines() if + line.endswith("/")] # Note - 'R/O/S/I/E' sorts to top for lowercase initial idx letter - dir_url = dir_url + "/" + sorted(dirs)[-1].rstrip(b"/").decode() + dir_url = dir_url + "/" + sorted(dirs)[-1].rstrip("/") # FIXME: not sure why a closure for "state" does not work here? state = {"idx-sid": None, "stack": [], "try_text": False} diff --git a/lib/python/rosie/svn_post_commit.py b/lib/python/rosie/svn_post_commit.py index a5aac0655e..5c4de94329 100644 --- a/lib/python/rosie/svn_post_commit.py +++ b/lib/python/rosie/svn_post_commit.py @@ -27,26 +27,27 @@ from difflib import unified_diff from email.mime.text import MIMEText +from io import StringIO import os import re -import rose.config -from rose.opt_parse import RoseOptionParser -from rose.popen import RosePopener, RosePopenError -from rose.reporter import Reporter -from rose.resource import ResourceLocator -from rose.scheme_handler import SchemeHandlersManager -from rosie.db import ( - LATEST_TABLE_NAME, MAIN_TABLE_NAME, META_TABLE_NAME, OPTIONAL_TABLE_NAME) import shlex from smtplib import SMTP import socket import sqlalchemy as al -from io import StringIO import sys from tempfile import TemporaryFile from time import mktime, strptime import traceback +import rose.config +from rose.opt_parse import RoseOptionParser +from rose.popen import RosePopener, RosePopenError +from rose.reporter import Reporter +from rose.resource import ResourceLocator +from rose.scheme_handler import SchemeHandlersManager +from rosie.db import ( + LATEST_TABLE_NAME, MAIN_TABLE_NAME, META_TABLE_NAME, OPTIONAL_TABLE_NAME) + class RosieWriteDAO(object): @@ -145,17 +146,16 @@ def run(self, repos, revision, no_notification=False): # Date-time of this commit os.environ["TZ"] = "UTC" - date_time = self._svnlook("date", "-r", revision, repos) + date_time = self._svnlook("date", "-r", revision, repos).decode() date, dtime, _ = date_time.split(None, 2) - date = mktime(strptime(b" ".join([date, dtime, b"UTC"]).decode(), - self.DATE_FMT)) - + date = mktime(strptime(" ".join([date, dtime, "UTC"]), self.DATE_FMT)) # Detail of changes changeset_attribs = { "repos": repos, "revision": revision, "prefix": prefix, - "author": self._svnlook("author", "-r", revision, repos).strip(), + "author": self._svnlook( + "author", "-r", revision, repos).decode("utf-8").strip(), "date": date} branch_attribs_dict = self._get_suite_branch_changes(repos, revision) @@ -175,7 +175,8 @@ def _get_suite_branch_changes(self, repos, revision): """Retrieve changed statuses.""" branch_attribs_dict = {} changed_lines = self._svnlook( - "changed", "--copy-info", "-r", revision, repos).splitlines(True) + "changed", "--copy-info", "-r", revision, repos).decode( + "utf-8").splitlines(True) while changed_lines: changed_line = changed_lines.pop(0) # A normal status changed_line @@ -185,18 +186,18 @@ def _get_suite_branch_changes(self, repos, revision): # Column 5+: path path = changed_line[4:].strip() path_status = changed_line[0] - if path.endswith(b"/") and path_status == "_": + if path.endswith("/") and path_status == "_": # Ignore property change on a directory continue # Path must be (under) a valid suite branch, including the special # ROSIE suite - names = path.split(b"/", self.LEN_ID + 1) + names = path.split("/", self.LEN_ID + 1) if (len(names) < self.LEN_ID + 1 or ( - b"".join(names[0:self.LEN_ID]) != b"ROSIE" and - any(name.decode() not in id_chars for name, id_chars in + "".join(names[0:self.LEN_ID]) != "ROSIE" and + any(name not in id_chars for name, id_chars in zip(names, self.ID_CHARS_LIST)))): continue - sid = b"".join(names[0:self.LEN_ID]) + sid = "".join(names[0:self.LEN_ID]) branch = names[self.LEN_ID] if branch: # Change to a path in a suite branch @@ -255,7 +256,8 @@ def _get_suite_branch_changes(self, repos, revision): elif path_status == self.ST_DELETED: # The suite has been deleted tree_out = self._svnlook( - "tree", "-r", str(int(revision) - 1), "-N", repos, path) + "tree", "-r", str(int(revision) - 1), "-N", repos, + path).decode("utf-8") # Include all branches of the suite in the deletion info for tree_line in tree_out.splitlines()[1:]: del_branch = tree_line.strip().rstrip("/") @@ -271,13 +273,12 @@ def _get_suite_branch_changes(self, repos, revision): def _load_info(self, repos, revision, sid, branch): """Load info file from branch_path in repos @revision.""" - info_file_path = "%s/%s/%s" % ("/".join(str(sid)), - branch, - self.INFO_FILE) + info_file_path = "%s/%s/%s" % ( + "/".join(str(sid)), branch, self.INFO_FILE) t_handle = TemporaryFile() try: t_handle.write(self._svnlook( - "cat", "-r", str(revision), repos, info_file_path)) + "cat", "-r", str(revision).encode(), repos, info_file_path)) except RosePopenError: return None t_handle.seek(0) @@ -382,14 +383,11 @@ def _svnlook(self, *args): def _update_info_db(self, dao, changeset_attribs, branch_attribs): """Update the suite info database for a suite branch.""" - idx = changeset_attribs["prefix"] + "-" +\ - branch_attribs["sid"].decode() + idx = changeset_attribs["prefix"] + "-" + branch_attribs["sid"] vc_attrs = { "idx": idx, "branch": branch_attribs["branch"], "revision": changeset_attribs["revision"]} - for key in vc_attrs: - vc_attrs[key] = vc_attrs[key] # Latest table try: dao.delete( @@ -419,11 +417,6 @@ def _update_info_db(self, dao, changeset_attribs, branch_attribs): changeset_attribs["prefix"] + "-" + "".join(from_names)) cols["status"] = ( branch_attribs["status"] + branch_attribs["status_info_file"]) - for key in cols: - try: - cols[key] = cols[key].decode("utf-8") - except AttributeError: - pass dao.insert(MAIN_TABLE_NAME, **cols) # Optional table for name in branch_attribs[info_key].value: @@ -433,8 +426,7 @@ def _update_info_db(self, dao, changeset_attribs, branch_attribs): if value is None: # setting may have ignore flag (!) continue cols = dict(vc_attrs) - cols.update({ - "name": name.decode("utf-8"), "value": value.decode("utf-8")}) + cols.update({"name": name, "value": value}) dao.insert(OPTIONAL_TABLE_NAME, **cols) def _update_known_keys(self, dao, changeset_attribs): @@ -443,7 +435,7 @@ def _update_known_keys(self, dao, changeset_attribs): revision = changeset_attribs["revision"] keys_str = self._svnlook( "cat", "-r", revision, repos, self.KNOWN_KEYS_FILE_PATH) - keys_str = " ".join(shlex.split(keys_str)).decode("utf-8") + keys_str = " ".join(shlex.split(keys_str.decode("utf-8"))) if keys_str: try: dao.insert(META_TABLE_NAME, name="known_keys", value=keys_str) diff --git a/lib/python/rosie/svn_pre_commit.py b/lib/python/rosie/svn_pre_commit.py index 67e6b76fda..6c7acb4008 100755 --- a/lib/python/rosie/svn_pre_commit.py +++ b/lib/python/rosie/svn_pre_commit.py @@ -137,8 +137,7 @@ def _svnlook(self, *args): """Return the standard output from "svnlook".""" command = ["svnlook"] + list(args) data = self.popen(*command, stderr=sys.stderr)[0] - data = data.decode() - return data + return data.decode() def _verify_users(self, status, path, txn_owner, txn_access_list, bad_changes): diff --git a/lib/python/rosie/ws.py b/lib/python/rosie/ws.py index 7729c3e4ba..6a6db617e2 100644 --- a/lib/python/rosie/ws.py +++ b/lib/python/rosie/ws.py @@ -19,32 +19,58 @@ # ----------------------------------------------------------------------------- """Rosie discovery service. -Classes: - RosieDiscoServiceRoot - discovery service root web page. - RosieDiscoService - discovery service for a given prefix. +Base classes: + RosieDiscoServiceApplication - collection of request handlers defining the + discovery service web application. + RosieDiscoServiceRoot - discovery service root web page request handler. + RosieDiscoService - discovery service request handler for a given prefix. + +Sub-classes, for handling API points by inheriting from RosieDiscoService: + GetHandler - overrides HTTP GET method to return known fields and operators + HelloHandler - overrides HTTP GET method to write a hello message + SearchHandler - overrides HTTP GET method to serve a database search + QueryHandler - overrides HTTP GET method to serve a database query """ -import cherrypy -from isodatetime.data import get_timepoint_from_seconds_since_unix_epoch +from glob import glob import jinja2 import json +import logging +import os +import pwd +import signal +from time import sleep +from tornado.ioloop import IOLoop, PeriodicCallback +import tornado.log +import tornado.web + +from isodatetime.data import get_timepoint_from_seconds_since_unix_epoch from rose.host_select import HostSelector +from rose.opt_parse import RoseOptionParser from rose.resource import ResourceLocator import rosie.db from rosie.suite_id import SuiteId -class RosieDiscoServiceRoot(object): +LOG_ROOT_TMPL = os.path.join( + "~", ".metomi", "%(ns)s-%(util)s-%(host)s-%(port)s") +DEFAULT_PORT = 8080 +INTERVAL_CHECK_FOR_STOP_CMD = 1 # in units of seconds - """Serves the Rosie discovery service index page.""" - NS = "rosie" +class RosieDiscoServiceApplication(tornado.web.Application): + + """Basic Tornado application defining the web service.""" + + NAMESPACE = "rosie" UTIL = "disco" TITLE = "Rosie Suites Discovery" - def __init__(self, *args, **kwargs): - self.exposed = True + def __init__(self, service_root_mode=False, *args, **kwargs): + self.stopping = False + self.service_root_mode = service_root_mode + self.props = {} rose_conf = ResourceLocator.default().get_conf() self.props["title"] = rose_conf.get_value( @@ -56,40 +82,107 @@ def __init__(self, *args, **kwargs): self.props["host_name"] = ( self.props["host_name"].split(".", 1)[0]) self.props["rose_version"] = ResourceLocator.default().get_version() + # Autoescape markup to prevent code injection from user inputs. self.props["template_env"] = jinja2.Environment( loader=jinja2.FileSystemLoader( ResourceLocator.default().get_util_home( - "lib", "html", "template", "rosie-disco"))) + "lib", "html", "template", "rosie-disco")), + autoescape=jinja2.select_autoescape( + enabled_extensions=("html", "xml"), default_for_string=True)) + db_url_map = {} for key, node in rose_conf.get(["rosie-db"]).value.items(): if key.startswith("db.") and key[3:]: db_url_map[key[3:]] = node.value self.db_url_map = db_url_map - if not self.db_url_map: - self.db_url_map = {} + + # Specify the root URL for the handlers and template. + ROOT = "%s-%s" % (self.NAMESPACE, self.UTIL) + service_root = r"/?" + if self.service_root_mode: + service_root = service_root.replace("?", ROOT + r"/?") + + # Set-up the Tornado application request-handling structure. + prefix_handlers = [] + class_args = {"props": self.props} + root_class_args = dict(class_args) # mutable so copy for safety + root_class_args.update({"db_url_map": self.db_url_map}) + root_handler = (service_root, RosieDiscoServiceRoot, root_class_args) for key, db_url in self.db_url_map.items(): - setattr(self, key, RosieDiscoService(self.props, key, db_url)) + prefix_class_args = dict(class_args) # mutable so copy for safety + prefix_class_args.update({ + "prefix": key, + "db_url": db_url, + "service_root": service_root, + }) + handler = (service_root + key + r"/?", RosieDiscoService, + prefix_class_args) + get_handler = (service_root + key + r"/get_(.+)", GetHandler, + prefix_class_args) + hello_handler = (service_root + key + r"/hello/?", HelloHandler, + prefix_class_args) + search_handler = (service_root + key + r"/search", SearchHandler, + prefix_class_args) + query_handler = (service_root + key + r"/query", QueryHandler, + prefix_class_args) + prefix_handlers.extend( + [handler, get_handler, hello_handler, search_handler, + query_handler]) + + handlers = [root_handler] + prefix_handlers + settings = dict( + autoreload=True, + static_path=ResourceLocator.default().get_util_home( + "lib", "html", "static"), + ) + super( + RosieDiscoServiceApplication, self).__init__(handlers, **settings) + + @staticmethod + def get_app_pid(): + """Return process ID of the application on the current server.""" + return os.getpid() + + def sigint_handler(self, signum, frame): + """Catch SIGINT signal allowing server stop by stop_application().""" + self.stopping = True + + def stop_application(self): + """Stop main event loop and server if 'stopping' flag is True.""" + if self.stopping: + IOLoop.current().stop() + # Log that the stop was clean (as opposed to a kill of the process) + tornado.log.gen_log.info("Stopped application and server cleanly") + + +class RosieDiscoServiceRoot(tornado.web.RequestHandler): + + """Serves the Rosie discovery service index page.""" + + def initialize(self, props, db_url_map, *args, **kwargs): + self.props = props + self.db_url_map = db_url_map - @cherrypy.expose - def index(self, *_): + # Decorator to ensure there is a trailing slash since buttons for keys + # otherwise go to wrong URLs for "/rosie" (-> "/key/" not "/rosie/key/"). + @tornado.web.addslash + def get(self): """Provide the root index page.""" tmpl = self.props["template_env"].get_template("index.html") - return tmpl.render( + self.write(tmpl.render( title=self.props["title"], host=self.props["host_name"], rose_version=self.props["rose_version"], - script=cherrypy.request.script_name, + script="/static", keys=sorted(self.db_url_map.keys())) + ) -class RosieDiscoService(object): +class RosieDiscoService(tornado.web.RequestHandler): - """Serves the index page of the database of a given prefix.""" + """Serves a page for the database of a given prefix.""" - HELLO = "Hello %s\n" - - def __init__(self, props, prefix, db_url): - self.exposed = True + def initialize(self, props, prefix, db_url, service_root): self.props = props self.prefix = prefix source_option = "prefix-web." + self.prefix @@ -99,72 +192,20 @@ def __init__(self, props, prefix, db_url): if source_url_node is not None: self.source_url = source_url_node.value self.dao = rosie.db.DAO(db_url) + self.service_root = service_root[:-1] # remove the '?' regex aspect - def __call__(self): - """Dummy.""" - pass - - @cherrypy.expose - def index(self, *_): + # Decorator to ensure there is a trailing slash since buttons for keys + # otherwise go to wrong URLs for "/rosie/key" (e.g. -> "rosie/query?..."). + @tornado.web.addslash + def get(self, *args): """Provide the index page.""" try: - return self._render() + self._render() except (KeyError, AttributeError, jinja2.exceptions.TemplateError): import traceback traceback.print_exc() except rosie.db.RosieDatabaseConnectError as exc: - raise cherrypy.HTTPError(404, str(exc)) - - @cherrypy.expose - def hello(self, format=None): - """Say Hello on success.""" - if cherrypy.request.login: - data = self.HELLO % cherrypy.request.login - else: - data = self.HELLO % "user" - if format == "json": - return json.dumps(data) - return data - - @cherrypy.expose - def query(self, q, all_revs=0, format=None): - """Search database for rows with data matching the query string.""" - all_revs = int(all_revs) - filters = [] - if not isinstance(q, list): - q = [q] - filters = [_query_parse_string(q_str) for q_str in q] - data = self.dao.query(filters, all_revs) - if format == "json": - return json.dumps(data) - return self._render(all_revs, data, filters=filters) - - @cherrypy.expose - def search(self, s, all_revs=0, format=None): - """Search database for rows with data matching the query string.""" - all_revs = int(all_revs) - data = self.dao.search(s, all_revs) - if format == "json": - return json.dumps(data) - return self._render(all_revs, data, s=s) - - @cherrypy.expose - def get_known_keys(self, format=None): - """Return the names of the common fields.""" - if format == "json": - return json.dumps(self.dao.get_known_keys()) - - @cherrypy.expose - def get_query_operators(self, format=None): - """Return the allowed query operators.""" - if format == "json": - return json.dumps(self.dao.get_query_operators()) - - @cherrypy.expose - def get_optional_keys(self, format=None): - """Return the names of the optional fields.""" - if format == "json": - return json.dumps(self.dao.get_optional_keys()) + raise tornado.web.HTTPError(404, str(exc)) def _render(self, all_revs=0, data=None, filters=None, s=None): """Render return data with a template.""" @@ -176,11 +217,12 @@ def _render(self, all_revs=0, data=None, filters=None, s=None): item["date"] = str(get_timepoint_from_seconds_since_unix_epoch( item["date"])) tmpl = self.props["template_env"].get_template("prefix-index.html") - return tmpl.render( + self.write(tmpl.render( title=self.props["title"], host=self.props["host_name"], rose_version=self.props["rose_version"], - script=cherrypy.request.script_name, + script="/static", + service_root=self.service_root, prefix=self.prefix, prefix_source_url=self.source_url, known_keys=self.dao.get_known_keys(), @@ -189,33 +231,292 @@ def _render(self, all_revs=0, data=None, filters=None, s=None): filters=filters, s=s, data=data) + ) -def _query_parse_string(q_str): - """Split a query filter string into component parts.""" - conjunction, tail = q_str.split(" ", 1) - if conjunction == "or" or conjunction == "and": - q_str = tail - else: - conjunction = "and" - filt = [conjunction] - if all(s == "(" for s in q_str.split(" ", 1)[0]): - start_group, q_str = q_str.split(" ", 1) - filt.append(start_group) - key, operator, value = q_str.split(" ", 2) - filt.extend([key, operator]) - last_groups = value.rsplit(" ", 1) - if (len(last_groups) > 1 and last_groups[1] and - all([s == ")" for s in last_groups[1]])): - filt.extend(last_groups) +class GetHandler(RosieDiscoService): + + """Write out basic data for the names of standard fields or operators.""" + + QUERY_KEYS = [ + "known_keys", # Return the names of the common fields. + "query_operators", # Return the allowed query operators. + "optional_keys", # Return the names of the optional fields. + ] + + def get(self, *args): + """Return data for basic API points of query keys without values.""" + format_arg = self.get_query_argument("format", default=None) + if args[0] and format_arg == "json": + for query in self.QUERY_KEYS: + if args[0].startswith(query): + # No need to catch AttributeError as all QUERY_KEYS valid. + self.write(json.dumps(getattr(self.dao, "get_" + query)())) + + +class HelloHandler(RosieDiscoService): + + """Writes a 'Hello' message to the current logged-in user, else 'user'.""" + + HELLO = "Hello %s\n" + + def get(self, *args): + """Say Hello on success.""" + format_arg = self.get_query_argument("format", default=None) + data = self.HELLO % pwd.getpwuid(os.getuid()).pw_name + if format_arg == "json": + self.write(json.dumps(data)) + else: + self.write(data) + + +class SearchHandler(RosieDiscoService): + + """Serves a search of the database on the page of a given prefix.""" + + def get(self, *args): + """Search database for rows with data matching the search string.""" + s_arg = self.get_query_argument("s", default=None) + all_revs = self.get_query_argument("all_revs", default=0) + format_arg = self.get_query_argument("format", default=None) + + if s_arg: + data = self.dao.search(s_arg, all_revs) + else: # Blank search: provide no rather than all output (else slow) + data = None + if format_arg == "json": + self.write(json.dumps(data)) + else: + self._render(all_revs, data, s=s_arg) + + +class QueryHandler(RosieDiscoService): + + """Serves a query of the database on the page of a given prefix.""" + + def get(self, *args): + """Search database for rows with data matching the query string.""" + q_args = self.get_query_arguments("q") # empty list if none given + all_revs = self.get_query_argument("all_revs", default=0) + format_arg = self.get_query_argument("format", default=None) + + filters = [] + if not isinstance(q_args, list): + q_args = [q_args] + filters = [self._query_parse_string(q_str) for q_str in q_args] + while None in filters: # remove invalid i.e. blank query filters + filters.remove(None) + if filters: + data = self.dao.query(filters, all_revs) + else: # in case of a fully blank query + data = None + if format_arg == "json": + self.write(json.dumps(data)) + else: + self._render(all_revs, data, filters=filters) + + @staticmethod + def _query_parse_string(q_str): + """Split a query filter string into component parts.""" + conjunction, tail = q_str.split(" ", 1) + if conjunction == "or" or conjunction == "and": + q_str = tail + else: + conjunction = "and" + filt = [conjunction] + if all(s == "(" for s in q_str.split(" ", 1)[0]): + start_group, q_str = q_str.split(" ", 1) + filt.append(start_group) + try: + key, operator, value = q_str.split(" ", 2) + except ValueError: # blank query i.e. no value provided + return None + filt.extend([key, operator]) + last_groups = value.rsplit(" ", 1) + if (len(last_groups) > 1 and last_groups[1] and + all([s == ")" for s in last_groups[1]])): + filt.extend(last_groups) + else: + filt.extend([value]) + return filt + + +def _log_app_base( + application, host, port, logger_type, file_ext, level_threshold=None): + """ Log to file some information from an application and/or its server.""" + log = logging.getLogger(logger_type) + log.propagate = False + if level_threshold: # else defaults to logging.WARNING + log.setLevel(level_threshold) + + log_root = os.path.expanduser(LOG_ROOT_TMPL % { + "ns": application.NAMESPACE, + "util": application.UTIL, + "host": host, + "port": port}) + log_channel = logging.FileHandler(log_root + file_ext) + # Use Tornado's log formatter to add datetime stamps & handle encoding: + log_channel.setFormatter(tornado.log.LogFormatter(color=False)) + log.addHandler(log_channel) + return log_channel + + +def _log_server_status(application, host, port): + """ Log a brief status, including process ID, for an application server.""" + log_root = os.path.expanduser(LOG_ROOT_TMPL % { + "ns": application.NAMESPACE, + "util": application.UTIL, + "host": host, + "port": port}) + log_status = log_root + ".status" + os.makedirs(os.path.dirname(log_root), exist_ok=True) + with open(log_status, "w") as handle: + handle.write("host=%s\n" % host) + handle.write("port=%d\n" % port) + handle.write("pid=%d\n" % int(application.get_app_pid())) + return log_status + + +def _get_server_status(application, host, port): + """Return a dictionary containing a brief application server status.""" + ret = {} + log_root_glob = os.path.expanduser(LOG_ROOT_TMPL % { + "ns": application.NAMESPACE, + "util": application.UTIL, + "host": "*", + "port": "*"}) + for filename in glob(log_root_glob + ".status"): + try: + for line in open(filename): + key, value = line.strip().split("=", 1) + ret[key] = value + break + except (IOError, ValueError): + pass + return ret + + +def parse_cli(*args, **kwargs): + """Parse command line, start/stop ad-hoc server. + + Return a CLI instruction tuple for a valid command instruction, else False: + ("start", Boolean, port): + start server on 'port', [2]==True indicating non_interactive mode. + ("stop", Boolean): + stop server, [2]==True indicating service_root_mode. + None: + bare command, requesting to print server status + """ + opt_parser = RoseOptionParser() + opt_parser.add_my_options("non_interactive", "service_root_mode") + opts, args = opt_parser.parse_args() + + arg = None + if args: + arg = args[0] + + if arg == "start": + port = DEFAULT_PORT + if args[1:]: + try: + port = int(args[1]) + except ValueError: + print("Invalid port specified. Using the default port.") + return ("start", opts.service_root_mode, port) + elif arg == "stop": + return ("stop", opts.non_interactive) + elif arg: # unrecognised (invalid) argument, to ignore + return False # False to distinguish from None for no arguments given + + +def main(): + port = DEFAULT_PORT + instruction = False + + cli_input = parse_cli() + if cli_input is False: # invalid arguments + print(" Command argument unrecognised.") + elif cli_input is None: # no arguments: bare command + instruction = "status" + else: # valid argument, either 'start' or 'stop' + instruction, cli_opt = cli_input[:2] + if len(cli_input) == 3: + port = cli_input[2] + + if instruction == "start" and cli_opt: + app = RosieDiscoServiceApplication(service_root_mode=True) else: - filt.extend([value]) - return filt + app = RosieDiscoServiceApplication() + app_info = app, app.props["host_name"], port + + # User-friendly message to be written to STDOUT: + user_msg_end = " the server providing the Rosie Disco web application" + # Detailed message to be written to log file: + log_msg_end = " server running application %s on host %s and port %s" % ( + app_info) + + if instruction == "start": + app.listen(port) + signal.signal(signal.SIGINT, app.sigint_handler) + + # This runs a callback every INTERVAL_CHECK_FOR_STOP_CMD s, needed to + # later stop the server cleanly via command on demand, as once start() + # is called on an IOLoop it blocks; stop() cannot be called directly. + PeriodicCallback( + app.stop_application, INTERVAL_CHECK_FOR_STOP_CMD * 1000).start() + + # Set-up logging and message outputs + _log_server_status(*app_info) + _log_app_base(*app_info, "tornado.access", ".access", logging.INFO) + _log_app_base(*app_info, "tornado.general", ".general", logging.DEBUG) + _log_app_base(*app_info, "tornado.application", ".error") + + tornado.log.gen_log.info("Started" + log_msg_end) + # Call to print before IOLoop start() else it prints only on loop stop. + print("Started" + user_msg_end) + append_url_root = "" + if app.service_root_mode: + append_url_root = "%s-%s/" % (app.NAMESPACE, app.UTIL) + # Also print the URL for quick access; 'http://' added so that the URL + # is hyperlinked in the terminal stdout, but it is not required. + print("Application root page available at http://%s:%s/%s" % ( + app.props["host_name"], port, append_url_root)) + + IOLoop.current().start() + elif instruction == "status": + status_info = _get_server_status(*app_info) + if status_info: + # Use JSON: 1) easily-parsable & 2) can "pretty print" via indent. + print(json.dumps(status_info, indent=4)) + else: + print("No such server running.") + elif instruction == "stop" and ( + cli_opt or input("Stop server? y/n (default=n)") == "y") and ( + _get_server_status(*app_info).get("pid")): + stop_server = True + try: + os.killpg( + int(_get_server_status(*app_info).get("pid")), signal.SIGINT) + except ProcessLookupError: # process already stopped, e.g. by Ctrl-C + print("Failed to stop%s; no such server or process to stop." % ( + user_msg_end)) + stop_server = False + if stop_server: + # Must wait for next callback, so server will not stop immediately; + # wait one callback interval so server has definitely stopped... + sleep(INTERVAL_CHECK_FOR_STOP_CMD) + IOLoop.current().close() # ... then close event loop to clean up. + + # Log via stop_application callback (logging module is blocking): + print("Stopped" + user_msg_end) + # Close all logging handlers to release log files: + logging.shutdown() -if __name__ == "__main__": - from rose.ws import ws_cli - ws_cli(RosieDiscoServiceRoot) -else: - from rose.ws import wsgi_app - application = wsgi_app(RosieDiscoServiceRoot) +if __name__ == "__main__": # Run on an ad-hoc server in a test environment. + main() +else: # Run as a WSGI application in a system service environment. + app = RosieDiscoServiceApplication() + wsgi_app = tornado.wsgi.WSGIAdapter(app) + server = wsgiref.simple_server.make_server("", DEFAULT_PORT, wsgi_app) + server.serve_forever() diff --git a/lib/python/rosie/ws_client.py b/lib/python/rosie/ws_client.py index 6423918c46..5aa4ebd83e 100644 --- a/lib/python/rosie/ws_client.py +++ b/lib/python/rosie/ws_client.py @@ -29,14 +29,13 @@ from multiprocessing import Pool import requests import shlex -import sys from time import sleep -from rosie.suite_id import SuiteId -from rosie.ws_client_auth import RosieWSClientAuthManager from rose.popen import RosePopener from rose.reporter import Reporter from rose.resource import ResourceLocator +from rosie.suite_id import SuiteId +from rosie.ws_client_auth import RosieWSClientAuthManager class RosieWSClientConfError(Exception): @@ -171,27 +170,6 @@ def _get(self, method, return_ok_prefixes=False, **kwargs): if not request_details: raise RosieWSClientError(method, kwargs) - # Filter security warnings from urllib3 on python <2.7.9. Obviously, we - # want to upgrade, but some sites have to run cylc on platforms with - # python <2.7.9. On those platforms, these warnings serve no purpose - # except to annoy or confuse users. - if sys.version_info < (2, 7, 9): - import warnings - try: - from requests.packages.urllib3.exceptions import ( - InsecurePlatformWarning) - except ImportError: - pass - else: - warnings.simplefilter("ignore", InsecurePlatformWarning) - try: - from requests.packages.urllib3.exceptions import ( - SNIMissingWarning) - except ImportError: - pass - else: - warnings.simplefilter("ignore", SNIMissingWarning) - # Process the requests in parallel pool = Pool(len(request_details)) results = {} diff --git a/lib/python/rosie/ws_client_auth.py b/lib/python/rosie/ws_client_auth.py index 31affdca7d..89be3d4ff4 100644 --- a/lib/python/rosie/ws_client_auth.py +++ b/lib/python/rosie/ws_client_auth.py @@ -23,7 +23,6 @@ # systems with GTK+ >=v3 this should work: Systems on GTK =v3 use PyGObject; v2 use PyGTK diff --git a/sbin/rosa-rpmbuild b/sbin/rosa-rpmbuild index 1e2be82a1c..0a6783ba37 100755 --- a/sbin/rosa-rpmbuild +++ b/sbin/rosa-rpmbuild @@ -67,7 +67,7 @@ License: GPLv3 URL: https://github.com/metomi/$NAME/ Source0: https://github.com/metomi/$NAME/releases/ BuildArch: noarch -Requires: bash fcm filesystem Jinja2 perl python3 python-cherrypy python-requests SQLAlchemy subversion +Requires: bash fcm filesystem Jinja2 perl python3 tornado python-requests SQLAlchemy subversion %description Rose: a framework for managing and running meteorological suites diff --git a/t/lib/bash/test_header b/t/lib/bash/test_header index 3127d86aff..73c219b53d 100644 --- a/t/lib/bash/test_header +++ b/t/lib/bash/test_header @@ -313,7 +313,7 @@ mock_smtpd_init() { for SMTPD_PORT in 8025 8125 8225 8325 8425 8525 8625 8725 8825 8925; do local SMTPD_HOST=localhost:$SMTPD_PORT local SMTPD_LOG="$TEST_DIR/smtpd.log" - python3 -m smtpd -c DebuggingServer -d -n "$SMTPD_HOST" \ + python3 -u -m smtpd -n -c DebuggingServer -d -n "$SMTPD_HOST" \ 1>"$SMTPD_LOG" 2>&1 & local SMTPD_PID=$! while ! grep -q 'DebuggingServer started' "$SMTPD_LOG" 2>/dev/null; do @@ -356,7 +356,7 @@ port_is_busy() { HOSTNAME="${HOSTNAME:-'localhost'}" netcat -z "${HOSTNAME}" "${PORT}" else - netstat -atun | grep -q "127.0.0.1:${PORT}" + netstat -atun | grep -q "0.0.0.0:${PORT}" fi } @@ -382,7 +382,7 @@ rose_ws_init() { TEST_ROSE_WS_PORT="${PORT}" if port_is_busy "${TEST_ROSE_WS_PORT}"; then pass "${TEST_KEY}" - TEST_ROSE_WS_URL="http://${HOSTNAME}:${TEST_ROSE_WS_PORT}/${NS}-${UTIL}/" + TEST_ROSE_WS_URL="http://${HOSTNAME}:${TEST_ROSE_WS_PORT}/${NS}-${UTIL}" else fail "${TEST_KEY}" rose_ws_kill @@ -395,7 +395,7 @@ rose_ws_kill() { wait 2>'/dev/null' fi if [[ -n "${TEST_ROSE_WS_PORT}" ]]; then - rm -fr "${HOME}/.metomi/"*"-0.0.0.0-${TEST_ROSE_WS_PORT}"* 2>'/dev/null' + rm -fr "${HOME}/.metomi/"*"-${HOSTNAME:-0.0.0.0}-${TEST_ROSE_WS_PORT}"* 2>'/dev/null' fi TEST_ROSE_WS_PID= TEST_ROSE_WS_PORT= diff --git a/t/rosa-db-create/00-basic.t b/t/rosa-db-create/00-basic.t index 2b3c9e17fc..a6eae900e7 100755 --- a/t/rosa-db-create/00-basic.t +++ b/t/rosa-db-create/00-basic.t @@ -22,7 +22,6 @@ # svn-post-commit", which is tested quite thoroughly in its own test suite. #------------------------------------------------------------------------------- . $(dirname $0)/test_header -skip_all "@TODO: Awaiting App upgrade to Python3" #------------------------------------------------------------------------------- if ! python3 -c 'import sqlalchemy' 2>/dev/null; then skip_all '"sqlalchemy" not installed' diff --git a/t/rosa-svn-post-commit/00-basic.t b/t/rosa-svn-post-commit/00-basic.t index e08b011d66..cee1dde32e 100755 --- a/t/rosa-svn-post-commit/00-basic.t +++ b/t/rosa-svn-post-commit/00-basic.t @@ -20,7 +20,6 @@ # Test "rosa svn-post-commit": Rosie WS DB update. #------------------------------------------------------------------------------- . $(dirname $0)/test_header -skip_all "@TODO: Awaiting App upgrade to Python3" #------------------------------------------------------------------------------- if ! python3 -c 'import sqlalchemy' 2>/dev/null; then skip_all '"sqlalchemy" not installed' diff --git a/t/rosa-svn-post-commit/01-mail-passwd.t b/t/rosa-svn-post-commit/01-mail-passwd.t index 51f615a451..cb786f25ec 100755 --- a/t/rosa-svn-post-commit/01-mail-passwd.t +++ b/t/rosa-svn-post-commit/01-mail-passwd.t @@ -20,7 +20,6 @@ # Test "rosa svn-post-commit": notification, user-tool=passwd. #------------------------------------------------------------------------------- . $(dirname $0)/test_header -skip_all "@TODO: Awaiting App upgrade to Python3" if ! python3 -c 'import sqlalchemy' 2>/dev/null; then skip_all '"sqlalchemy" not installed' fi @@ -101,11 +100,9 @@ else file_grep "$TEST_KEY-smtpd.log.recips" \ "^recips: \[$RECIPS\]" "$TEST_SMTPD_LOG" file_grep "$TEST_KEY-smtpd.log.subject" \ - "^Data: '.*Subject: foo-aa000/trunk@1" \ - "$TEST_SMTPD_LOG" + "^Data: b'.*Subject: foo-aa000/trunk@1" "$TEST_SMTPD_LOG" file_grep "$TEST_KEY-smtpd.log.text" \ - "^Data: '.*A a/a/0/0/0/trunk/rose-suite.info" \ - "$TEST_SMTPD_LOG" + "^Data: b'.*A a/a/0/0/0/trunk/rose-suite.info" "$TEST_SMTPD_LOG" file_cmp "$TEST_KEY-rc" "$PWD/rosa-svn-post-commit.rc" <<<'0' fi #------------------------------------------------------------------------------- @@ -119,17 +116,14 @@ title=test post commit hook: create __ROSE_SUITE_INFO cat /dev/null >"$TEST_SMTPD_LOG" rosie create -q -y --info-file=rose-suite.info --no-checkout - file_grep "$TEST_KEY-smtpd.log.sender" "^sender: notifications@nowhere.org" \ "$TEST_SMTPD_LOG" RECIPS=$(get_recips 'new-access-list') file_grep "$TEST_KEY-smtpd.log.recips" "^recips: \[$RECIPS\]" "$TEST_SMTPD_LOG" file_grep "$TEST_KEY-smtpd.log.subject" \ - "^Data: '.*Subject: foo-aa001/trunk@2" \ - "$TEST_SMTPD_LOG" + "^Data: b'.*Subject: foo-aa001/trunk@2" "$TEST_SMTPD_LOG" file_grep "$TEST_KEY-smtpd.log.text" \ - "^Data: '.*A a/a/0/0/1/trunk/rose-suite.info" \ - "$TEST_SMTPD_LOG" + "^Data: b'.*A a/a/0/0/1/trunk/rose-suite.info" "$TEST_SMTPD_LOG" file_cmp "$TEST_KEY-rc" "$PWD/rosa-svn-post-commit.rc" <<<'0' #------------------------------------------------------------------------------- TEST_KEY="${TEST_KEY_BASE}-branch" @@ -155,11 +149,9 @@ file_grep "$TEST_KEY-smtpd.log.sender" "^sender: notifications@nowhere.org" \ RECIPS=$(get_recips 'mod-owner') file_grep "$TEST_KEY-smtpd.log.recips" "^recips: \[$RECIPS\]" "$TEST_SMTPD_LOG" file_grep "$TEST_KEY-smtpd.log.subject" \ - "^Data: '.*Subject: foo-aa000/trunk@4" \ - "$TEST_SMTPD_LOG" + "^Data: b'.*Subject: foo-aa000/trunk@4" "$TEST_SMTPD_LOG" file_grep "$TEST_KEY-smtpd.log.text" \ - "^Data: '.*-owner=$USER.*+owner=root" \ - "$TEST_SMTPD_LOG" + "^Data: b'.*-owner=$USER.*+owner=root" "$TEST_SMTPD_LOG" file_cmp "$TEST_KEY-rc" "$PWD/rosa-svn-post-commit.rc" <<<'0' #------------------------------------------------------------------------------- TEST_KEY="$TEST_KEY_BASE-mod-access-list" @@ -179,11 +171,9 @@ file_grep "$TEST_KEY-smtpd.log.sender" "^sender: notifications@nowhere.org" \ RECIPS=$(get_recips 'mod-access-list') file_grep "$TEST_KEY-smtpd.log.recips" "^recips: \[$RECIPS\]" "$TEST_SMTPD_LOG" file_grep "$TEST_KEY-smtpd.log.subject" \ - "^Data: '.*Subject: foo-aa001/trunk@5" \ - "$TEST_SMTPD_LOG" + "^Data: b'.*Subject: foo-aa001/trunk@5" "$TEST_SMTPD_LOG" file_grep "$TEST_KEY-smtpd.log.text" \ - "^Data: '.*-access-list=root.*+access-list=\\*" \ - "$TEST_SMTPD_LOG" + "^Data: b'.*-access-list=root.*+access-list=\\*" "$TEST_SMTPD_LOG" file_cmp "$TEST_KEY-rc" "$PWD/rosa-svn-post-commit.rc" <<<'0' #------------------------------------------------------------------------------- TEST_KEY="$TEST_KEY_BASE-mod-access-list-2" @@ -203,11 +193,9 @@ file_grep "$TEST_KEY-smtpd.log.sender" "^sender: notifications@nowhere.org" \ RECIPS=$(get_recips 'mod-access-list-2') file_grep "$TEST_KEY-smtpd.log.recips" "^recips: \[$RECIPS\]" "$TEST_SMTPD_LOG" file_grep "$TEST_KEY-smtpd.log.subject" \ - "^Data: '.*Subject: foo-aa001/trunk@6" \ - "$TEST_SMTPD_LOG" + "^Data: b'.*Subject: foo-aa001/trunk@6" "$TEST_SMTPD_LOG" file_grep "$TEST_KEY-smtpd.log.text" \ - "^Data: '.*-access-list=\\*.*+access-list=root bin" \ - "$TEST_SMTPD_LOG" + "^Data: b'.*-access-list=\\*.*+access-list=root bin" "$TEST_SMTPD_LOG" file_cmp "$TEST_KEY-rc" "$PWD/rosa-svn-post-commit.rc" <<<'0' #------------------------------------------------------------------------------- TEST_KEY="$TEST_KEY_BASE-del" @@ -218,11 +206,9 @@ file_grep "$TEST_KEY-smtpd.log.sender" "^sender: notifications@nowhere.org" \ RECIPS=$(get_recips 'del') file_grep "$TEST_KEY-smtpd.log.recips" "^recips: \[$RECIPS\]" "$TEST_SMTPD_LOG" file_grep "$TEST_KEY-smtpd.log.subject" \ - "^Data: '.*Subject: foo-aa001/trunk@7" \ - "$TEST_SMTPD_LOG" + "^Data: b'.*Subject: foo-aa001/trunk@7" "$TEST_SMTPD_LOG" file_grep "$TEST_KEY-smtpd.log.text" \ - "^Data: '.*D a/a/0/0/1//trunk/'$" \ - "$TEST_SMTPD_LOG" + "^Data: b'.*D a/a/0/0/1//trunk/'$" "$TEST_SMTPD_LOG" file_cmp "$TEST_KEY-rc" "$PWD/rosa-svn-post-commit.rc" <<<'0' #------------------------------------------------------------------------------- mock_smtpd_kill diff --git a/t/rosa-svn-post-commit/03-unicode.t b/t/rosa-svn-post-commit/03-unicode.t index a5ffea5958..f30f95cc59 100755 --- a/t/rosa-svn-post-commit/03-unicode.t +++ b/t/rosa-svn-post-commit/03-unicode.t @@ -20,7 +20,6 @@ # Test "rosa svn-post-commit": Discovery Service database update with unicode. #------------------------------------------------------------------------------- . "$(dirname "$0")/test_header" -skip_all "@TODO: Awaiting App upgrade to Python3" #------------------------------------------------------------------------------- if ! python3 -c 'import sqlalchemy' 2>'/dev/null'; then skip_all '"sqlalchemy" not installed' diff --git a/t/rosa-svn-pre-commit/00-basic.t b/t/rosa-svn-pre-commit/00-basic.t index 93dc024d01..b810ecd13b 100755 --- a/t/rosa-svn-pre-commit/00-basic.t +++ b/t/rosa-svn-pre-commit/00-basic.t @@ -20,7 +20,6 @@ # Basic tests for "rosa svn-pre-commit". #------------------------------------------------------------------------------- . $(dirname $0)/test_header -skip_all "@TODO: Awaiting App upgrade to Python3" export ROSE_CONF_PATH= mkdir conf cat >conf/rose.conf <<'__ROSE_CONF__' diff --git a/t/rosa-svn-pre-commit/01-rosie-create.t b/t/rosa-svn-pre-commit/01-rosie-create.t index 8180a19382..6e8f806e12 100755 --- a/t/rosa-svn-pre-commit/01-rosie-create.t +++ b/t/rosa-svn-pre-commit/01-rosie-create.t @@ -20,7 +20,6 @@ # Tests for "rosa svn-pre-commit" with "rosie create". #------------------------------------------------------------------------------- . $(dirname $0)/test_header -skip_all "@TODO: Awaiting App upgrade to Python3" #------------------------------------------------------------------------------- tests 18 #------------------------------------------------------------------------------- diff --git a/t/rosa-svn-pre-commit/02-passwd.t b/t/rosa-svn-pre-commit/02-passwd.t index 3caba23d5d..cb89f1c033 100755 --- a/t/rosa-svn-pre-commit/02-passwd.t +++ b/t/rosa-svn-pre-commit/02-passwd.t @@ -20,7 +20,6 @@ # Tests for "rosa svn-pre-commit", Unix passwd user check. #------------------------------------------------------------------------------- . $(dirname $0)/test_header -skip_all "@TODO: Awaiting App upgrade to Python3" export ROSE_CONF_PATH= mkdir conf cat >conf/rose.conf <<'__ROSE_CONF__' @@ -120,7 +119,11 @@ access-list=no-such-user-550 root no-such-user-551 __ROSE_SUITE_INFO__ run_fail "$TEST_KEY" svn commit -q -m 't' --non-interactive aa000 sed -i '/^\[FAIL\]/!d' "$TEST_KEY.err" -file_cmp "$TEST_KEY.err" "$TEST_KEY.err" <<'__ERR__' +# $TEST_KEY.err gets the two fail lines in arbitary order, so test for either. +file_cmp_any "$TEST_KEY.err" "$TEST_KEY.err" <<'__ERR__' +[FAIL] NO SUCH USER: U a/a/0/0/0/trunk/rose-suite.info: access-list=no-such-user-551 +[FAIL] NO SUCH USER: U a/a/0/0/0/trunk/rose-suite.info: access-list=no-such-user-550 +__filesep__ [FAIL] NO SUCH USER: U a/a/0/0/0/trunk/rose-suite.info: access-list=no-such-user-550 [FAIL] NO SUCH USER: U a/a/0/0/0/trunk/rose-suite.info: access-list=no-such-user-551 __ERR__ diff --git a/t/rosa-svn-pre-commit/03-meta.t b/t/rosa-svn-pre-commit/03-meta.t index 2357c3a5b8..03ca906241 100755 --- a/t/rosa-svn-pre-commit/03-meta.t +++ b/t/rosa-svn-pre-commit/03-meta.t @@ -20,7 +20,6 @@ # Tests for "rosa svn-pre-commit", validate against configuration metadata. #------------------------------------------------------------------------------- . "$(dirname "$0")/test_header" -skip_all "@TODO: Awaiting App upgrade to Python3" export ROSE_CONF_PATH= mkdir -p 'conf' 'rose-meta/foolish/HEAD' cat >'conf/rose.conf' <<__ROSE_CONF__ diff --git a/t/rosie-disco/00-basic.t b/t/rosie-disco/00-basic.t index 343ddcf30b..b9102d6cf8 100755 --- a/t/rosie-disco/00-basic.t +++ b/t/rosie-disco/00-basic.t @@ -20,12 +20,11 @@ # Basic tests for "rosie disco". #------------------------------------------------------------------------------- . $(dirname $0)/test_header -skip_all "@TODO: Awaiting App upgrade to Python3" -if ! python3 -c 'import cherrypy, sqlalchemy' 2>/dev/null; then - skip_all '"cherrypy" or "sqlalchemy" not installed' +if ! python3 -c 'import tornado, sqlalchemy' 2>/dev/null; then + skip_all '"tornado" or "sqlalchemy" not installed' fi #------------------------------------------------------------------------------- -tests 21 +tests 23 #------------------------------------------------------------------------------- mkdir svn svnadmin create svn/foo @@ -49,19 +48,32 @@ if [[ -z "${TEST_ROSE_WS_PORT}" ]]; then fi URL_FOO="${TEST_ROSE_WS_URL}/foo/" -URL_FOO_S=${URL_FOO}search? -URL_FOO_Q=${URL_FOO}query? +URL_FOO_S="${URL_FOO}search?" +URL_FOO_Q="${URL_FOO}query?" #------------------------------------------------------------------------------- -TEST_KEY=$TEST_KEY_BASE-curl-root -run_pass "$TEST_KEY" curl -I "${TEST_ROSE_WS_URL}" +# Test for correct status and headers in root index pages. + +# Note: 'curl -I' always procudes 'text/html' content type for Tornado apps, +# so to request just the JSON data, need to use 'curl -i', see e.g. +# https://groups.google.com/forum/#!topic/python-tornado/bolRj0wSfos. + +TEST_KEY=$TEST_KEY_BASE-curl-root-trailing-slash +run_pass "$TEST_KEY" curl -i "${TEST_ROSE_WS_URL}/" # note: slash at end file_grep "$TEST_KEY.out" 'HTTP/.* 200 OK' "$TEST_KEY.out" -#------------------------------------------------------------------------------- + +# The app has been set-up so that a trailing slash, as in the test directly +# above, provides the strict endpoint, but the same URL without the slash will +# permanantly redirect to this, c.f. the 'tornado.web.addslash' decorator. +TEST_KEY=$TEST_KEY_BASE-curl-root-no-trailing-slash +run_pass "$TEST_KEY" curl -i "${TEST_ROSE_WS_URL}" # note: no slash at end +file_grep "$TEST_KEY.out" 'HTTP/.* 301 Moved Permanently' "$TEST_KEY.out" + TEST_KEY=$TEST_KEY_BASE-curl-foo -run_pass "$TEST_KEY" curl -I $URL_FOO +run_pass "$TEST_KEY" curl -i "${URL_FOO}" file_grep "$TEST_KEY.out" 'HTTP/.* 200 OK' "$TEST_KEY.out" #------------------------------------------------------------------------------- TEST_KEY=$TEST_KEY_BASE-curl-foo-get_query_operators -run_pass "$TEST_KEY" curl ${URL_FOO}get_query_operators?format=json +run_pass "$TEST_KEY" curl "${URL_FOO}get_query_operators?format=json" run_pass "$TEST_KEY.out" python3 - "$TEST_KEY.out" <<'__PYTHON__' import json, sys d = sorted(json.load(open(sys.argv[1]))) @@ -70,7 +82,7 @@ sys.exit(d != ["contains", "endswith", "eq", "ge", "gt", "ilike", "le", __PYTHON__ #------------------------------------------------------------------------------- TEST_KEY=$TEST_KEY_BASE-curl-foo-get_known_keys -run_pass "$TEST_KEY" curl ${URL_FOO}get_known_keys?format=json +run_pass "$TEST_KEY" curl "${URL_FOO}get_known_keys?format=json" run_pass "$TEST_KEY.out" python3 - "$TEST_KEY.out" <<'__PYTHON__' import json, sys d = sorted(json.load(open(sys.argv[1]))) @@ -106,7 +118,7 @@ done #------------------------------------------------------------------------------- TEST_KEY=$TEST_KEY_BASE-curl-foo-search run_pass "$TEST_KEY" curl "${URL_FOO_S}s=apple&format=json" -run_pass "$TEST_KEY-out" python3 - "$TEST_KEY.out" <<'__PYTHON__' +run_pass "$TEST_KEY.out" python3 - "$TEST_KEY.out" <<'__PYTHON__' import json, sys expected_d = [{"idx": "foo-aa001", "title": "apple cider", @@ -133,7 +145,7 @@ TEST_KEY=$TEST_KEY_BASE-curl-foo-query Q='q=project+eq+food&q=and+title+contains+apple' run_pass "$TEST_KEY" \ curl "${URL_FOO_Q}${Q}&format=json" -run_pass "$TEST_KEY-out" python3 - "$TEST_KEY.out" <<'__PYTHON__' +run_pass "$TEST_KEY.out" python3 - "$TEST_KEY.out" <<'__PYTHON__' import json, sys expected_d = [{"idx": "foo-aa006", "title": "apple tart", @@ -151,7 +163,7 @@ TEST_KEY=$TEST_KEY_BASE-curl-foo-query-all-revs Q='q=project+eq+food&q=and+title+contains+apple' run_pass "$TEST_KEY" \ curl "${URL_FOO_Q}${Q}&all_revs=1&format=json" -run_pass "$TEST_KEY-out" python3 - "$TEST_KEY.out" <<'__PYTHON__' +run_pass "$TEST_KEY.out" python3 - "$TEST_KEY.out" <<'__PYTHON__' import json, sys expected_d = [{"idx": "foo-aa006", "title": "apple pie", @@ -177,12 +189,11 @@ sys.exit(len(d) != len(expected_d) or d[1]["revision"] != expected_d[1]["revision"]) __PYTHON__ #------------------------------------------------------------------------------- -TEST_KEY=$TEST_KEY_BASE-curl-foo-query-brace -# (=%28 and )=%29 +TEST_KEY=$TEST_KEY_BASE-curl-foo-query-brace # (=%28 and )=%29 Q='q=%28+owner+eq+rose&q=or+owner+eq+rosie+%29&q=and+project+eq+food' run_pass "$TEST_KEY" \ curl "${URL_FOO_Q}${Q}&format=json" -run_pass "$TEST_KEY-out" python3 - "$TEST_KEY.out" <<'__PYTHON__' +run_pass "$TEST_KEY.out" python3 - "$TEST_KEY.out" <<'__PYTHON__' import json, sys expected_d = [{"idx": "foo-aa005", "title": "carrot cake", diff --git a/t/rosie-graph/00-basic.t b/t/rosie-graph/00-basic.t index 7462eb2d34..5b6b8ed2ff 100755 --- a/t/rosie-graph/00-basic.t +++ b/t/rosie-graph/00-basic.t @@ -21,10 +21,9 @@ #------------------------------------------------------------------------------- . $(dirname $0)/test_header_extra . $(dirname $0)/test_header -skip_all "@TODO: Awaiting App upgrade to Python3" #------------------------------------------------------------------------------- -if ! python3 -c 'import cherrypy, sqlalchemy' 2>/dev/null; then - skip_all '"cherrypy" or "sqlalchemy" not installed' +if ! python3 -c 'import tornado, sqlalchemy' 2>/dev/null; then + skip_all '"tornado" or "sqlalchemy" not installed' fi tests 33 #------------------------------------------------------------------------------- @@ -345,5 +344,5 @@ __OUTPUT__ #------------------------------------------------------------------------------- kill "${ROSA_WS_PID}" wait 2>'/dev/null' -rm -f ~/.metomi/rosie-disco-0.0.0.0-${PORT}* +rm -f ~/.metomi/rosie-disco-${HOSTNAME:-0.0.0.0}-${PORT}* exit diff --git a/t/rosie-lookup/00-basic.t b/t/rosie-lookup/00-basic.t index b4073fdfc1..ec739a1710 100755 --- a/t/rosie-lookup/00-basic.t +++ b/t/rosie-lookup/00-basic.t @@ -22,10 +22,9 @@ # svn-post-commit", which is tested quite thoroughly in its own test suite. #------------------------------------------------------------------------------- . $(dirname $0)/test_header -skip_all "@TODO: Awaiting App upgrade to Python3" #------------------------------------------------------------------------------- -if ! python3 -c 'import cherrypy, sqlalchemy' 2>/dev/null; then - skip_all '"cherrypy" or "sqlalchemy" not installed' +if ! python3 -c 'import tornado, sqlalchemy' 2>/dev/null; then + skip_all '"tornado" or "sqlalchemy" not installed' fi tests 75 #------------------------------------------------------------------------------- @@ -235,7 +234,7 @@ local suite owner project title foo-aa000/trunk@4 iris eye pad Should have gone to ... = foo-aa002/trunk@5 aphids eat roses Eat all the roses! = foo-aa003/trunk@6 bill sonnet 54 The rose looks fair... -url: http://$HOSTNAME:$PORT/foo/search?all_revs=1&s=a +url: http://$HOSTNAME:$PORT/foo/search?s=a&all_revs=1 __OUT__ file_cmp "$TEST_KEY.err" "$TEST_KEY.err" Violets are Blue... [u'*'] -foo-aa001/trunk@3 = %description [u'roses', u'violets'] -foo-aa000/trunk@4 Bad corn ear and pew pull [u'*'] -foo-aa002/trunk@5 = Nom nom nom roses [u'allthebugs'] +foo-aa000/trunk@1 Bad corn ear and pew pull ['*'] +foo-aa001/trunk@2 > Violets are Blue... ['*'] +foo-aa001/trunk@3 = %description ['roses', 'violets'] +foo-aa000/trunk@4 Bad corn ear and pew pull ['*'] +foo-aa002/trunk@5 = Nom nom nom roses ['allthebugs'] foo-aa003/trunk@6 = %description %access-list -url: http://$HOSTNAME:$PORT/foo/search?all_revs=1&s=a +url: http://$HOSTNAME:$PORT/foo/search?s=a&all_revs=1 __OUT__ file_cmp "$TEST_KEY.err" "$TEST_KEY.err" '/dev/null' -rm -f ~/.metomi/rosie-disco-0.0.0.0-${PORT}* +rm -f ~/.metomi/rosie-disco-${HOSTNAME:-0.0.0.0}-${PORT}* exit diff --git a/t/rosie-lookup/01-multi.t b/t/rosie-lookup/01-multi.t index 67fa4097d7..df10daec68 100755 --- a/t/rosie-lookup/01-multi.t +++ b/t/rosie-lookup/01-multi.t @@ -20,10 +20,9 @@ # Basic multi-source tests for "rosie lookup". #------------------------------------------------------------------------------- . $(dirname $0)/test_header -skip_all "@TODO: Awaiting App upgrade to Python3" #------------------------------------------------------------------------------- -if ! python3 -c 'import cherrypy, sqlalchemy' 2>/dev/null; then - skip_all '"cherrypy" or "sqlalchemy" not installed' +if ! python3 -c 'import tornado, sqlalchemy' 2>/dev/null; then + skip_all '"tornado" or "sqlalchemy" not installed' fi tests 18 #------------------------------------------------------------------------------- @@ -179,5 +178,5 @@ file_cmp "${TEST_KEY}.err" "${TEST_KEY}.err" '/dev/null' -rm -f ~/.metomi/rosie-disco-0.0.0.0-${PORT}* +rm -f ~/.metomi/rosie-disco-${HOSTNAME:-0.0.0.0}-${PORT}* exit diff --git a/t/rosie-lookup/02-unicode.t b/t/rosie-lookup/02-unicode.t index 72d8ca692e..c2d1a46040 100755 --- a/t/rosie-lookup/02-unicode.t +++ b/t/rosie-lookup/02-unicode.t @@ -20,10 +20,9 @@ # Basic unicode tests for "rosie lookup". #------------------------------------------------------------------------------- . $(dirname $0)/test_header -skip_all "@TODO: Awaiting App upgrade to Python3" #------------------------------------------------------------------------------- -if ! python3 -c 'import cherrypy, sqlalchemy' 2>/dev/null; then - skip_all '"cherrypy" or "sqlalchemy" not installed' +if ! python3 -c 'import tornado, sqlalchemy' 2>/dev/null; then + skip_all '"tornado" or "sqlalchemy" not installed' fi tests 3 #------------------------------------------------------------------------------- @@ -103,5 +102,5 @@ file_cmp "${TEST_KEY}.err" "${TEST_KEY}.err" <'/dev/null' #------------------------------------------------------------------------------- kill "${ROSA_WS_PID}" wait 2>'/dev/null' -rm -f ~/.metomi/rosie-disco-0.0.0.0-${PORT}* +rm -f ~/.metomi/rosie-disco-${HOSTNAME:-0.0.0.0}-${PORT}* exit diff --git a/t/rosie-ls/00-basic.t b/t/rosie-ls/00-basic.t index 68f6fd38e1..fc6985f41f 100755 --- a/t/rosie-ls/00-basic.t +++ b/t/rosie-ls/00-basic.t @@ -20,10 +20,9 @@ # Basic tests for "rosie ls", with 2 repositories. #------------------------------------------------------------------------------- . $(dirname $0)/test_header -skip_all "@TODO: Awaiting App upgrade to Python3" #------------------------------------------------------------------------------- -if ! python3 -c 'import cherrypy, sqlalchemy' 2>/dev/null; then - skip_all '"cherrypy" or "sqlalchemy" not installed' +if ! python3 -c 'import tornado, sqlalchemy' 2>/dev/null; then + skip_all '"tornado" or "sqlalchemy" not installed' fi tests 15 #------------------------------------------------------------------------------- @@ -193,5 +192,5 @@ svn up -q "$PWD/roses/bar-aa000" #------------------------------------------------------------------------------- kill "${ROSA_WS_PID}" wait 2>'/dev/null' -rm -f ~/.metomi/rosie-disco-0.0.0.0-${PORT}* +rm -f ~/.metomi/rosie-disco-${HOSTNAME:-0.0.0.0}-${PORT}* exit diff --git a/t/rosie-ls/01-many.t b/t/rosie-ls/01-many.t index 79cdb829df..1a17b8227f 100755 --- a/t/rosie-ls/01-many.t +++ b/t/rosie-ls/01-many.t @@ -20,9 +20,8 @@ # Test for "rosie ls", ensure healthy on large number of checked out suites. #------------------------------------------------------------------------------- . "$(dirname "$0")/test_header" -skip_all "@TODO: Awaiting App upgrade to Python3" -if ! python3 -c 'import cherrypy, sqlalchemy' 2>'/dev/null'; then - skip_all '"cherrypy" or "sqlalchemy" not installed' +if ! python3 -c 'import tornado, sqlalchemy' 2>'/dev/null'; then + skip_all '"tornado" or "sqlalchemy" not installed' fi tests 3 #------------------------------------------------------------------------------- @@ -108,5 +107,5 @@ file_cmp "${TEST_KEY}.err" "${TEST_KEY}.err" <'/dev/null' #------------------------------------------------------------------------------- kill "${ROSA_WS_PID}" wait 2>'/dev/null' -rm -f ~/.metomi/rosie-disco-0.0.0.0-${PORT}* +rm -f ~/.metomi/rosie-disco-${HOSTNAME:-0.0.0.0}-${PORT}* exit