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