diff --git a/model/domain.py b/model/domain.py index 8a0d31c..4dbd7f2 100644 --- a/model/domain.py +++ b/model/domain.py @@ -22,22 +22,22 @@ """ import logging +import re from ossie.utils import redhawk from ossie.utils.redhawk.channels import ODMListener -import traceback - - -def scan_domains(): - return redhawk.scan() +from ossie.cf.CF import InvalidObjectReference +from omniORB import CORBA +_logger = logging.getLogger(__name__) class ResourceNotFound(Exception): - def __init__(self, resource='resource', name='Unknown'): + def __init__(self, resource='resource', name='Unknown', msg=None): self.name = name self.resource = resource + self.msg = msg if msg else "Unable to find %s '%s'" % (self.resource, self.name) def __str__(self): - return "Unable to find %s '%s'" % (self.resource, self.name) + return self.msg class WaveformLaunchError(Exception): @@ -57,6 +57,193 @@ def __init__(self, name='Unknown', msg=''): def __str__(self): return "Not able to release waveform '%s'. %s" % (self.name, self.msg) +def scan_domains(location=None): + _logger.debug("Scan domains(location=%s). redhawk_remote_bug=%s", location, redhawk_remote_bug) + if redhawk_remote_bug and location and location != 'localhost': + raise Exception('Remote domain connectivity is unavailable in Redhawk <= 1.10.2') + + try: + return [ build_domainref(location, d) for d in redhawk.scan(location) ] + except RuntimeError, e: + # FIXME: Runtime Error is not very descriptive. Need to weed out other problems + if not location: + location = 'localhost' + raise ResourceNotFound(msg="Unable to connect with NameService on host '%s'" % location) + + +def parse_domainref(domainref): + ''' + Parses a domain reference: location + ':' + DOMAINNAME + + Note that a location can be a hostname or an IP address (ipv4 and ipv6). + Locations may be omitted by just including the DOMAINNAME, unless the + DOMAINNAME includes a colon, in which case a blank location can + be specified by using a leading (e.g. ":DOMAIN:NAME" yields [None, 'DOMAIN:NAME']) + + ipv6 addresses should be surrounded by a '[' and ']' much like + https://www.ietf.org/rfc/rfc2732.txt + + :param domainref: A formatted domain reference + :return: [location, domainname] list + + >>> parse_domainref('DOMAINNAME') + (None, 'DOMAINNAME') + >>> parse_domainref(':DOMAIN:NAME') + (None, 'DOMAIN:NAME') + >>> parse_domainref('DOMAIN:NAME') + ('DOMAIN', 'NAME') + >>> parse_domainref('127.2.7.1:NAME') + ('127.2.7.1', 'NAME') + >>> parse_domainref('localhost:NAME') + ('localhost', 'NAME') + >>> parse_domainref('[::1]:NAME') + ('::1', 'NAME') + >>> parse_domainref('[::1]:DOMAIN:NAME') + ('::1', 'DOMAIN:NAME') + >>> parse_domainref('[::1]:]DOMAIN:NAME') + ('::1', ']DOMAIN:NAME') + >>> parse_domainref('[::1:DOMAIN:NAME') + ('[', ':1:DOMAIN:NAME') + >>> parse_domainref('localhost:') + ('localhost', None) + >>> parse_domainref('') + Traceback (most recent call last): + ... + ValueError: invalid domain reference '' + >>> parse_domainref(':localhost:') + (None, 'localhost:') + >>> parse_domainref('[DOMAINNAME]XXX') + (None, '[DOMAINNAME]XXX') + >>> parse_domainref(u'foo:DOMAINNAME') + ('foo', 'DOMAINNAME') + ''' + if not domainref: + raise ValueError("invalid domain reference '%s'" % domainref) + if domainref[0] == ':': + return (None, domainref[1:]) + if domainref[0] == '[' and ']' in domainref: + addr, domain = domainref.split(']', 1) + if domain and domain[0] == ':': + return (addr[1:], parse_domainref(domain)[1]) + + return None, domainref + rtn = str(domainref).split(':', 1) + if len(rtn) == 1: + return (None, rtn[0]) + if not rtn[1]: + return (rtn[0], None) + return (rtn[0], rtn[1]) + +def build_domainref(location, domain): + ''' + Generates a safe domainref using a location and a domain. Escapes things as necessary + + >>> build_domainref(None, 'Domainname') + 'Domainname' + >>> build_domainref('Hostname', 'Domainname') + 'Hostname:Domainname' + >>> build_domainref('Hostname', None) + 'Hostname:' + >>> build_domainref('Hostname', '') + 'Hostname:' + >>> build_domainref('', 'DOMAIN') + 'DOMAIN' + >>> build_domainref('', 'Domainname') + 'Domainname' + >>> build_domainref('', ':Domainname') + '::Domainname' + >>> build_domainref('', 'Domainname:') + ':Domainname:' + >>> build_domainref('127.2.7.1', 'NAME') + '127.2.7.1:NAME' + >>> build_domainref('::1', 'NAME') + '[::1]:NAME' + >>> build_domainref(None, '[DOMAINNAME]XXX') + '[DOMAINNAME]XXX' + >>> build_domainref('[', ':1:DOMAIN:NAME') + '[::1:DOMAIN:NAME' + ''' + if not domain: + domain = '' + domain = str(domain) + if not location: + if ':' in domain: + return ":%s" % domain + else: + return domain + location = str(location) + if ":" in location: + location = "[%s]" % location + return "%s:%s" % (location, domain) + +def _parse_dist_version(distver, element=None): + ''' + :param distver: distribution version from pkg_resources + :return: a three element tuple of only the integers + >>> _parse_dist_version(('00000001', '00000010', '00000002', '*final')) + (1, 10, 2) + >>> _parse_dist_version(('00000002', '00000012', '00000002', '*final')) + (2, 12, 2) + >>> _parse_dist_version(('00000002', '00000012', '0000002b', '*final')) + (2, 12, 2) + >>> _parse_dist_version(('00000002', '00000012', '000000b', '*final')) + (2, 12, 0) + >>> _parse_dist_version(('00000002', '00000012', 'b', '*final')) + (2, 12, 0) + ''' + if element is None: + return (_parse_dist_version(distver, 0), + _parse_dist_version(distver, 1), + _parse_dist_version(distver, 2)) + try: + return int(re.findall('\d+', distver[element])[0]) + except (ValueError, IndexError): + return 0 + +def _identify_buggy_redhawk_location(v): + ''' + Identifies the python code that is unable to connect to more + than a single remote location. The bug means that only the first + location is used. And subsequent calls with different locations + will connect only use the first location. Bug is fixed + in Redhawk 1.10.3 (core framework v 1.10.2) + + :param v: tuple representing version major, minor, patch + :return: boolean + >>> _identify_buggy_redhawk_location((1, 10, 1)) + True + >>> _identify_buggy_redhawk_location((1, 10, 2)) + False + >>> _identify_buggy_redhawk_location((1, 11, 1)) + False + >>> _identify_buggy_redhawk_location((2, 1, 1)) + False + >>> _identify_buggy_redhawk_location((1, 9, 2)) + True + ''' + return v[0] == 1 and ((v[1] == 10 and v[2] < 2) or v[1] < 10) + +def get_redhawk_version(): + ''' + Attempt to find redhawk version. + :return: the version + ''' + try: + import pkg_resources + dist = pkg_resources.get_distribution('ossiepy') + return _parse_dist_version(dist.parsed_version) + except Exception, e: + _logger.exception("Unable to determine redhawk_version") + raise + +def has_remote_location_bug(): + try: + return _identify_buggy_redhawk_location(get_redhawk_version()) + except Exception, e: + _logger.exception("Unable to determine redhawk_version so turning off remote locations") + return True + +redhawk_remote_bug = has_remote_location_bug() class Domain: domMgr_ptr = None @@ -64,14 +251,22 @@ class Domain: eventHandlers = [] name = None - def __init__(self, domainname): - logging.trace("Estasblishing domain %s", domainname, exc_info=True) + def __init__(self, domainref): + location, domainname = parse_domainref(domainref) + if redhawk_remote_bug and location and location != 'localhost': + raise Exception('Remote domain connectivity is unavailable in Redhawk <= 1.10.2') + + _logger.debug("Establishing domain %s at location %s", domainname, location, exc_info=True) + + self._domainref = domainref self.name = domainname + self.location = location + try: self._establish_domain() except StandardError, e: - logging.warn("Unable to find domain %s", e, exc_info=1) - raise ResourceNotFound("domain", domainname) + _logger.warn("Unable to find domain %s", e, exc_info=1) + raise ResourceNotFound("domain", domainref) def _odm_response(self, event): for eventH in self.eventHandlers: @@ -79,18 +274,27 @@ def _odm_response(self, event): def _connect_odm_listener(self): - self.odmListener = ODMListener() - self.odmListener.connect(self.domMgr_ptr) - self.odmListener.deviceManagerAdded.addListener(self._odm_response) - self.odmListener.deviceManagerRemoved.addListener(self._odm_response) - self.odmListener.applicationAdded.addListener(self._odm_response) - self.odmListener.applicationRemoved.addListener(self._odm_response) + listener = ODMListener() + listener.connect(self.domMgr_ptr) + listener.deviceManagerAdded.addListener(self._odm_response) + listener.deviceManagerRemoved.addListener(self._odm_response) + listener.applicationAdded.addListener(self._odm_response) + listener.applicationRemoved.addListener(self._odm_response) + self.odmListener = listener def _establish_domain(self): redhawk.setTrackApps(False) - self.domMgr_ptr = redhawk.attach(str(self.name)) + try: + self.domMgr_ptr = redhawk.attach(self.name, self.location) + except CORBA.TRANSIENT: + raise ResourceNotFound('domain', self._domainref) + self.domMgr_ptr.__odmListener = None - self._connect_odm_listener() + try: + self._connect_odm_listener() + except InvalidObjectReference: + _logger.warn("%s: Unable to connect with EventChannel", self._domainref, + exc_info=True) def properties(self): props = self.domMgr_ptr.query([]) # TODO: self.domMgr_ptr._properties @@ -220,3 +424,7 @@ def services(self, dev_mgr_id): ret_dict.append({'name': svc._instanceName, 'id': svc._refid}) return ret_dict + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/model/redhawk.py b/model/redhawk.py index 81d7e96..1113d8e 100644 --- a/model/redhawk.py +++ b/model/redhawk.py @@ -24,9 +24,15 @@ in domain.py and caches the domain object. """ +import pkg_resources +import re from _utils.tasking import background_task -from domain import Domain, scan_domains, ResourceNotFound +from domain import Domain, scan_domains, ResourceNotFound, redhawk_remote_bug + + +RHWEB_VERSION = '1.2.0' + class Redhawk(object): @@ -36,15 +42,15 @@ def _get_domain(self, domain_name): name = str(domain_name) if not name in self.__domains: self.__domains[name] = Domain(domain_name) - + return self.__domains[name] ############################## # DOMAIN @background_task - def get_domain_list(self): - return scan_domains() + def get_domain_list(self, location=None): + return scan_domains(location) @background_task def get_domain_info(self, domain_name): @@ -173,3 +179,18 @@ def get_object_by_path(self, path, path_type): return domain.find_device(path[1], path[2]), path[3:] raise ValueError("Bad path type %s. Must be one of application, component, device-mgr or device" % path_type) + + def get_redhawk_info(self): + try: + return self._redhawk_info + except AttributeError: + try: + version = get_redhawk_version() + except Exception: + version = 'unknown' + self._redhawk_info = { + 'redhawk.version': version, + 'rhweb.version': RHWEB_VERSION, + 'supportsRemoteLocations': not redhawk_remote_bug + } + return self._redhawk_info diff --git a/preptest.sh b/preptest.sh new file mode 100755 index 0000000..8acf591 --- /dev/null +++ b/preptest.sh @@ -0,0 +1,14 @@ +#!/bin/sh -e +# Runs the unit test with nose in the virtual environment +export RHDOMAIN=REDHAWK_TEST + +NBARGS=--nopersist + +# Domain Manager +nodeBooter $NBARGS -D /domain/DomainManager.dmd.xml --domainname $RHDOMAIN || exit 1 & + +sleep 2 + +# GPP +nodeBooter $NBARGS -d /nodes/GPP/DeviceManager.dcd.xml --domainname $RHDOMAIN || exit 1 & + diff --git a/pyrest.py b/pyrest.py index d9a7c05..013a8ad 100755 --- a/pyrest.py +++ b/pyrest.py @@ -27,6 +27,7 @@ from rest.device import Devices, DeviceProperties from rest.port import PortHandler from rest.bulkio_handler import BulkIOWebsocketHandler +from rest.sysinfo import SysInfoHandler import tornado.httpserver import tornado.web @@ -38,12 +39,18 @@ # setup command line options from tornado.options import define, options +rhweb_path = os.getenv('RHWEBPATH', '/var/redhawk/web') +default_docpath = os.path.join(rhweb_path, 'redhawk-rest-doc') + define('port', default=8080, type=int, help="server port") define("debug", default=False, type=bool, help="Enable Tornado debug mode. Reloads code") +define('staticpath', default=None, type=str, help='Path to the static content') +define('docpath', default=default_docpath, type=str, help='Path to the REDHAWK REST documentation') _ID = r'/([^/]+)' _LIST = r'/?' -_DOMAIN_PATH = r'/redhawk/rest/domains' +_REST_PATH = r'/redhawk/rest' +_DOMAIN_PATH = _REST_PATH + '/domains' _APPLICATION_PATH = _DOMAIN_PATH + _ID + r'/applications' _COMPONENT_PATH = _APPLICATION_PATH + _ID + r'/components' _DEVICE_MGR_PATH = _DOMAIN_PATH + _ID + r'/deviceManagers' @@ -51,20 +58,43 @@ _PROPERTIES_PATH = r'/properties' _PORT_PATH = r'/ports' _BULKIO_PATH = _PORT_PATH + _ID + r'/bulkio' +_DOC_PATH = _REST_PATH + r'/doc' class Application(tornado.web.Application): def __init__(self, *args, **kwargs): + ''' + Create the REDHAWK Rest Application + Parameters + ---------- + docpath - str + Optional path to RESTful documentation. + Defaults to $REDHAWKWEB/share/redhawk-rest-doc. + static-path - str + Optional static content path. Defaults to None (no content) + Served as root content. The RESTful content takes precident over + static content. + _ioloop - tornado.ioloop.IOLoop + Optional Tornado IOLoop instance used for + registering callbacks. Useful for unit test + which have their own IOLoop. Defaults to current + IOLoop. + + ''' + # explicit _ioloop for unit testing _ioloop = kwargs.get('_ioloop', None) - cwd = os.path.abspath(os.path.dirname(__import__(__name__).__file__)) + static_path = kwargs.get('static_path', None) + docpath = kwargs.get('docpath', None) + if not docpath: + docpath = default_docpath # REDHAWK Service redhawk = Redhawk() + handlers = [ - (r"/apps/(.*)/$", IndexHandler), - (r"/apps/(.*)", tornado.web.StaticFileHandler, {"path": os.path.join(cwd, "apps")}), + (_REST_PATH + r'/sysinfo', SysInfoHandler, dict(redhawk=redhawk)), # Domains (_DOMAIN_PATH + _LIST, DomainInfo, dict(redhawk=redhawk)), @@ -115,21 +145,37 @@ def __init__(self, *args, **kwargs): dict(redhawk=redhawk, kind='device')), (_DEVICE_PATH + _ID + _BULKIO_PATH, BulkIOWebsocketHandler, dict(redhawk=redhawk, kind='device', _ioloop=_ioloop)), + (_DOC_PATH + "/$", IndexHandler, dict(basepath=docpath)), + (_DOC_PATH + "/(.*)$", tornado.web.StaticFileHandler, dict(path=docpath)) ] + + if static_path: + filehandles = [ + (r"/$", IndexHandler, {"basepath": static_path}), + (r"/(.*)/$", IndexHandler, {"basepath": static_path}), + (r"/(.*)", tornado.web.StaticFileHandler, {"path": static_path}) + ] + handlers.extend(filehandles) + tornado.web.Application.__init__(self, handlers, *args, **kwargs) class IndexHandler(tornado.web.RequestHandler): - def get(self, path): - self.render("apps/"+path+"/index.html") + + def initialize(self, basepath): + self._basepath = basepath + + def get(self, path=""): + filepath = os.path.join(self._basepath, path, "index.html") + print "Rendering %s" % filepath + self.render(filepath) def main(): tornado.options.parse_command_line() - application = Application(debug=options.debug) + application = Application(debug=options.debug, static_path=options['staticpath'], docpath=options['docpath']) application.listen(options.port) ioloop.IOLoop.instance().start() if __name__ == '__main__': main() - diff --git a/redhawk-rest-python.spec b/redhawk-rest-python.spec index 376e529..e954405 100644 --- a/redhawk-rest-python.spec +++ b/redhawk-rest-python.spec @@ -22,11 +22,15 @@ %define _supervisor /etc/redhawk-web/supervisor.d %define _nginx /etc/nginx/conf.d/redhawk-sites +%define debug_package %{nil} + + Prefix: %{_prefix} Name: redhawk-rest-python -Version: 2.0.0 +Version: 2.1.0 Release: 1%{?dist} Summary: A REDHAWK REST application that exposes the entire domain. +BuildArch: noarch License: GPL BuildRoot: %(mktemp -ud %{_tmppath}/%{name}-%{version}-%{release}-XXXXXX) @@ -36,7 +40,6 @@ Requires: python Requires: redhawk >= 1.10 Requires: redhawk-devel Requires: bulkioInterfaces -Requires: redhawk-web Requires: rhweb-python-tornado Requires: python-futures @@ -89,3 +92,6 @@ rm -rf %{buildroot} %{_nginx}/rest-python.enabled %{_supervisor}/redhawk-rest-python.conf +%changelog +* Tue Mar 3 2015 Douglas Pew - 2.1.0-0 +- Server disables remote location if buggy REDHAWK, added static file path diff --git a/requirements.txt b/requirements.txt index c6cad0a..118fb21 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ -tornado==4.0.2 +numpy +tornado>=4.0.2 futures==2.1.6 +jsonschema diff --git a/resources/schemas/ports.schema.json b/resources/schemas/ports.schema.json new file mode 100644 index 0000000..fe1cd61 --- /dev/null +++ b/resources/schemas/ports.schema.json @@ -0,0 +1,63 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://redhawksdr.org", + "type": "array", + "items": { + "id": "http://redhawksdr.org/1", + "type": "object", + "properties": { + "idl": { + "id": "http://redhawksdr.org/1/idl", + "type": "object", + "properties": { + "namespace": { + "id": "http://redhawksdr.org/1/idl/namespace", + "type": "string", + "enum": [ + "BULKIO" + ] + }, + "version": { + "id": "http://redhawksdr.org/1/idl/version", + "type": "string", + "enum": [ + ":1.0" + ] + }, + "type": { + "id": "http://redhawksdr.org/1/idl/type", + "type": "string" + } + }, + "required": [ + "namespace", + "version", + "type" + ] + }, + "direction": { + "id": "http://redhawksdr.org/1/direction", + "type": "string", + "enum": [ + "Uses", + "Provides" + ] + }, + "name": { + "id": "http://redhawksdr.org/1/name", + "type": "string" + }, + "repId": { + "id": "http://redhawksdr.org/1/repId", + "type": "string" + } + }, + "required": [ + "idl", + "direction", + "name", + "repId" + ] + }, + "required": ["1"] +} \ No newline at end of file diff --git a/resources/waveforms/SigTest/.project b/resources/waveforms/SigTest/.project new file mode 100644 index 0000000..9e79e06 --- /dev/null +++ b/resources/waveforms/SigTest/.project @@ -0,0 +1,18 @@ + + + SigTest + + + + + + gov.redhawk.ide.codegen.builders.TopLevelRPMSpec + + + + + + gov.redhawk.ide.natures.scaproject + gov.redhawk.ide.natures.sca.waveform + + diff --git a/resources/waveforms/SigTest/SigTest.sad.xml b/resources/waveforms/SigTest/SigTest.sad.xml new file mode 100644 index 0000000..12bdce8 --- /dev/null +++ b/resources/waveforms/SigTest/SigTest.sad.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + SigGen_1 + + + + + + + + + + diff --git a/resources/waveforms/SigTest/SigTest.sad_diagramV2 b/resources/waveforms/SigTest/SigTest.sad_diagramV2 new file mode 100644 index 0000000..c2774d3 --- /dev/null +++ b/resources/waveforms/SigTest/SigTest.sad_diagramV2 @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/waveforms/SigTest/SigTest.spec b/resources/waveforms/SigTest/SigTest.spec new file mode 100644 index 0000000..b10b78e --- /dev/null +++ b/resources/waveforms/SigTest/SigTest.spec @@ -0,0 +1,37 @@ +# RPM package for SigTest +# This file is regularly AUTO-GENERATED by the IDE. DO NOT MODIFY. + +# By default, the RPM will install to the standard REDHAWK SDR root location (/var/redhawk/sdr) +# You can override this at install time using --prefix /new/sdr/root when invoking rpm (preferred method, if you must) +%{!?_sdrroot: %define _sdrroot /var/redhawk/sdr} +%define _prefix %{_sdrroot} +Prefix: %{_prefix} + +Name: SigTest +Summary: Waveform SigTest +Version: 1.0.0 +Release: 1 +License: None +Group: REDHAWK/Waveforms +Source: %{name}-%{version}.tar.gz +# Require the controller whose SPD is referenced +Requires: SigGen +# Require each referenced component +Requires: SigGen +BuildArch: noarch +BuildRoot: %{_tmppath}/%{name}-%{version} + +%description + +%prep +%setup + +%install +%__rm -rf $RPM_BUILD_ROOT +%__mkdir_p "$RPM_BUILD_ROOT%{_prefix}/dom/waveforms/%{name}" +%__install -m 644 SigTest.sad.xml $RPM_BUILD_ROOT%{_prefix}/dom/waveforms/%{name}/SigTest.sad.xml + +%files +%defattr(-,redhawk,redhawk) +%dir %{_prefix}/dom/waveforms/%{name} +%{_prefix}/dom/waveforms/%{name}/SigTest.sad.xml diff --git a/rest/bulkio_handler.py b/rest/bulkio_handler.py index 659af3f..86439dc 100644 --- a/rest/bulkio_handler.py +++ b/rest/bulkio_handler.py @@ -26,41 +26,42 @@ from tornado import ioloop, gen from tornado import websocket +from urlparse import urlparse + import numpy from model.domain import Domain, ResourceNotFound from asyncport import AsyncPort - -def _floats2bin(flist): - """ - Converts a list of python floating point values - to a packed array of IEEE 754 32 bit floating point - """ - return numpy.array(flist).astype('float32').tostring() - - -def _doubles2bin(flist): - """ - Converts a list of python floating point values - to a packed array of IEEE 754 64 bit floating point - """ - return numpy.array(flist).astype('float64').tostring() - - -def _pass_through(flist): - return flist +def _make_converter(numpy_type): + def _list2bin(datalist): + """ + Converts a list of values to a packed array of + binary values. See + http://docs.scipy.org/doc/numpy/user/basics.types.html + for list of types. + """ + return numpy.array(datalist).astype(numpy_type).tostring() + return _list2bin + + +DATA_CONVERSION_MAP = { + # datatype: converter + 'dataFloat': _make_converter('float32'), + 'dataDouble': _make_converter('float64'), + 'dataOctet': _make_converter('uint8'), + 'dataLong': _make_converter('int32'), + 'dataChar': _make_converter('int8'), + 'dataLongLong': _make_converter('int64'), + 'dataShort': _make_converter('int16'), + 'dataUlong': _make_converter('uint32'), + 'dataUlongLong': _make_converter('uint64'), + 'dataUshort': _make_converter('uint16'), +} class BulkIOWebsocketHandler(websocket.WebSocketHandler): - data_conversion_map = { - 'dataFloat': _floats2bin, - 'dataDouble': _doubles2bin, - 'dataOctet': _pass_through, - 'dataShort': _pass_through - } - def initialize(self, kind, redhawk=None, _ioloop=None): self.kind = kind self.redhawk = redhawk @@ -68,6 +69,25 @@ def initialize(self, kind, redhawk=None, _ioloop=None): _ioloop = ioloop.IOLoop.current() self._ioloop = _ioloop + +# If Websockets are getting a 403 error, one workaround is +# to have check_origin always return True +# +# Please note that if doing a reverse proxy, set the +# Host header to the origin. For example, an nginx config +# to proxy 81 to port 9401 (running pyrest.py) on nginx: +# +# location /redhawk/rest/ { +# proxy_set_header Host $host:81; +# proxy_pass http://localhost:9401; +# proxy_set_header Upgrade $http_upgrade; +# proxy_set_header Connection "Upgrade"; +# } + +# Workaround: +# def check_origin(self, origin): +# return True + @gen.coroutine def open(self, *args): try: @@ -85,7 +105,7 @@ def open(self, *args): self.port = obj.getPort(str(path[0])) logging.debug("Found port %s", self.port) - self.converter = self.data_conversion_map[data_type] + self.converter = DATA_CONVERSION_MAP[data_type] bulkio_poa = getattr(BULKIO__POA, data_type) logging.debug(bulkio_poa) diff --git a/rest/device.py b/rest/device.py index 56f451a..d3b20dd 100644 --- a/rest/device.py +++ b/rest/device.py @@ -51,8 +51,9 @@ def get(self, domain_name, dev_mgr_id, dev_id=None): self._render_json(info) -class DeviceProperties(web.RequestHandler): +class DeviceProperties(JsonHandler, PropertyHelper): - def get(self, *args): - self.set_status(500) - self.write(dict(status='Device Properties handler not implemented')) + @gen.coroutine + def get(self, domain_name, dev_mgr_id, dev_id=None): + dev = yield self.redhawk.get_device(domain_name, dev_mgr_id, dev_id) + self._render_json(dict(properties=self.format_properties(dev._properties))) \ No newline at end of file diff --git a/rest/domain.py b/rest/domain.py index a539095..3e17f5d 100644 --- a/rest/domain.py +++ b/rest/domain.py @@ -29,16 +29,22 @@ from handler import JsonHandler from helper import PropertyHelper +from model.domain import parse_domainref class DomainInfo(JsonHandler, PropertyHelper): @gen.coroutine - def get(self, domain_name=None): + def get(self, domain_ref=None): + domain_name = location = None + if domain_ref: + location, domain_name = parse_domainref(domain_ref) + + if domain_name: - dom_info = yield self.redhawk.get_domain_info(domain_name) - properties = yield self.redhawk.get_domain_properties(domain_name) - apps = yield self.redhawk.get_application_list(domain_name) - device_managers = yield self.redhawk.get_device_manager_list(domain_name) + dom_info = yield self.redhawk.get_domain_info(domain_ref) + properties = yield self.redhawk.get_domain_properties(domain_ref) + apps = yield self.redhawk.get_application_list(domain_ref) + device_managers = yield self.redhawk.get_device_manager_list(domain_ref) info = { 'id': dom_info._get_identifier(), @@ -49,7 +55,7 @@ def get(self, domain_name=None): } else: - domains = yield self.redhawk.get_domain_list() + domains = yield self.redhawk.get_domain_list(location) info = {'domains': domains} self._render_json(info) @@ -63,7 +69,7 @@ def get(self, domain_name, prop_name=None): if prop_name: value = None for item in info: - if item['name'] == prop_name: + if item['id'] == prop_name: value = item if value: diff --git a/rest/port.py b/rest/port.py index c54cf2e..355e6a9 100644 --- a/rest/port.py +++ b/rest/port.py @@ -51,12 +51,12 @@ def get(self, *args): name = path[0] for port in obj.ports: if port.name == name: - self.write(json.dumps(self.format_port(port))) + self._render_json(json.dumps(self.format_port(port))) break else: raise ResourceNotFound('port', name) else: - self.write(json.dumps(self.format_ports(obj.ports))) + self._render_json(json.dumps(self.format_ports(obj.ports))) except ResourceNotFound, e: logging.debug('Resource not found %s' % str(e), exc_info=1) self.set_status(404) diff --git a/rest/sysinfo.py b/rest/sysinfo.py new file mode 100644 index 0000000..0df58ba --- /dev/null +++ b/rest/sysinfo.py @@ -0,0 +1,56 @@ +__author__ = 'depew' + +RHWEB_VERSION = '1.2.0' + +from handler import JsonHandler + +def _identify_remote_location_version(v): + ''' + :param v: tuple representing version major, minor, patch + :return: boolean + >>> _identify_remote_location_version((1, 10, 2)) + False + >>> _identify_remote_location_version((1, 10, 3)) + True + >>> _identify_remote_location_version((1, 11, 1)) + True + >>> _identify_remote_location_version((2, 1, 1)) + True + >>> _identify_remote_location_version((1, 9, 3)) + False + ''' + return (v[0] > 1) or \ + (v[0] == 1 and + (v[1] == 10 and v[2] > 2) or + (v[1] > 10)) + + +def _parse_dist_version(distver, element=None): + ''' + :param distver: distribution version from pkg_resources + :return: a three element tuple of only the integers + >>> _parse_dist_version(('00000001', '00000010', '00000002', '*final')) + (1, 10, 2) + >>> _parse_dist_version(('00000002', '00000012', '00000002', '*final')) + (2, 12, 2) + >>> _parse_dist_version(('00000002', '00000012', '0000002b', '*final')) + (2, 12, 2) + >>> _parse_dist_version(('00000002', '00000012', '000000b', '*final')) + (2, 12, 0) + >>> _parse_dist_version(('00000002', '00000012', 'b', '*final')) + (2, 12, 0) + ''' + if element is None: + return (_parse_dist_version(distver, 0), + _parse_dist_version(distver, 1), + _parse_dist_version(distver, 2)) + try: + return int(re.findall('\d+', distver[element])[0]) + except (ValueError, IndexError): + return 0 + +class SysInfoHandler(JsonHandler): + + def get(self): + self._render_json(self.redhawk.get_redhawk_info()) + diff --git a/runtests.sh b/runtests.sh new file mode 100755 index 0000000..55339c6 --- /dev/null +++ b/runtests.sh @@ -0,0 +1,44 @@ +#!/bin/sh -e +# Runs the unit test with nose in the virtual environment + +err() { + echo "ERROR: $@" 1>&2 + exit 1 +} + +. /usr/libexec/sdrtools/sdrlib.bash || err "Unable to load sdrlib bash library" + +COMPONENTS="KitchenSink SigGen" +WAVEFORMS="SigTest TestConfigureWaveform" + +# verify the components +for c in $COMPONENTS ; do + st_verify_component "$c" || err "Missing component '$c'" +done + +# verify the waveforms +for c in $WAVEFORMS ; do + st_verify_waveform "$c" || err "Missing waveform '$c'" +done + + +#trap 'rhkill.sh -1' 0 1 + +#rhkill.sh -2 + +# Domain Manager +#nodeBooter $NBARGS -D /domain/DomainManager.dmd.xml --domainname $RHDOMAIN || exit 1 & + +#sleep 2 +# GPP +#nodeBooter $NBARGS -d /nodes/GPP/DeviceManager.dcd.xml --domainname $RHDOMAIN || exit 1 & + +#sleep 4 +#.virtualenv/bin/python /usr/bin/nosetests1.1 --with-doctest --with-coverage --where . --where model/ --where tests/ --where rest --cover-tests "$@" 2>&1 | tee tests.out + +# example test:Sysinfo + +if [ $# -lt 1 ] ; then + set -- test +fi +.virtualenv/bin/python /usr/bin/nosetests1.1 --with-doctest --with-coverage --cover-tests "$@" 2>&1 | tee tests.out diff --git a/test.py b/test.py index a7b655b..fe30540 100644 --- a/test.py +++ b/test.py @@ -19,11 +19,22 @@ # from tornado.testing import main import unittest +import doctest from tests import * +from model import domain + def all(): - return unittest.TestLoader().loadTestsFromModule(__import__(__name__)) + suite = unittest.TestLoader().loadTestsFromModule(__import__(__name__)) + # Add domain doctests + suite.addTests(doctest.DocTestSuite(domain)) + return suite + if __name__ == '__main__': + # FIXME: Make unit test usable + # 1) direct output for working tests to a file, hide from console + # 2) be able to run specific tests easily + # 3) list the tests available main() diff --git a/tests/__init__.py b/tests/__init__.py index a91c3ae..eb60b13 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -26,6 +26,7 @@ ComponentTests -- /domain/{NAME}/applications/{ID}/components DeviceManagerTests -- /domain/{NAME}/deviceManagers DeviceTests -- /domain/{NAME}/deviceManagers/{ID}/devices +SysinfoTests -- /sysinfo """ __author__ = 'rpcanno' @@ -36,6 +37,7 @@ from device import DeviceTests from application import ApplicationTests from component import ComponentTests -from bulkio import BulkIOTests +from bulkio import BulkIOTests, TestDataConverters from port import PortTests from concurrent import ConcurrencyTests +from sysinfo import SysinfoTests diff --git a/tests/application.py b/tests/application.py index fe64639..91d591e 100644 --- a/tests/application.py +++ b/tests/application.py @@ -24,12 +24,12 @@ import pprint import tornado -from base import JsonTests +from base import RedhawkTests from defaults import Default from model.redhawk import Redhawk -class ApplicationTests(JsonTests): +class ApplicationTests(RedhawkTests): def setUp(self): super(ApplicationTests, self).setUp(); @@ -37,55 +37,9 @@ def setUp(self): def tearDown(self): # kill SigTest waveforms - url = '/domains/'+Default.DOMAIN_NAME - json, resp = self._json_request(url, 200) - if 'applications' not in json: - json['applications'] = [] - for a in json['applications']: - if a['name'].startswith('SigTest'): - self._json_request( - '/domains/'+Default.DOMAIN_NAME+'/applications/'+a['id'], - 200, - 'DELETE' - ) + self._clean_applications(['SigTest']) super(ApplicationTests, self).tearDown(); - @tornado.gen.coroutine - def _get_applications(self): - url = '/domains/'+Default.DOMAIN_NAME - json, resp = yield self._async_json_request(url, 200) - - self.assertTrue('applications' in json) - - raise tornado.gen.Return((url, json['applications'])) - - @tornado.gen.coroutine - def _launch(self, name): - json, resp = yield self._async_json_request( - '/domains/'+Default.DOMAIN_NAME+'/applications', - 200, - 'POST', - {'name': name} - ) - self.assertTrue('launched' in json) - self.assertTrue('applications' in json) - self.assertTrue(json['launched'] in [x['id'] for x in json['applications']]) - - raise tornado.gen.Return(json['launched']) - - @tornado.gen.coroutine - def _release(self, wf_id): - json, resp = yield self._async_json_request( - '/domains/'+Default.DOMAIN_NAME+'/applications/'+wf_id, - 200, - 'DELETE' - ) - - self.assertAttr(json, 'released', wf_id) - - self.assertTrue('applications' in json) - self.assertFalse(json['released'] in json['applications']) - raise tornado.gen.Return(resp) @tornado.testing.gen_test def test_launch_release(self): diff --git a/tests/base.py b/tests/base.py index 44d1f6b..7c5766c 100644 --- a/tests/base.py +++ b/tests/base.py @@ -24,7 +24,9 @@ import itertools import time +import unittest +import tornado from tornado.testing import AsyncHTTPTestCase from tornado.httpclient import AsyncHTTPClient, HTTPRequest, HTTPResponse, HTTPError, _RequestProxy from tornado.simple_httpclient import SimpleAsyncHTTPClient @@ -33,7 +35,7 @@ from tornado import httputil, stack_context from tornado.concurrent import TracebackFuture -import json +import json, os from defaults import Default @@ -98,7 +100,7 @@ def handle_response(response): self.fetch_impl(request, handle_response) return future -class JsonAssertions(object): +class JsonAssertions(unittest.TestCase): RESPONSE_CODES_2XX = (200, 201, 202, 203, 204, 205, 206) RESPONSE_CODES_3XX = (300, 301, 302, 303, 304, 305, 306, 307) @@ -130,9 +132,27 @@ def assertJsonResponse(self, response, codes=RESPONSE_CODES_2XX): assertResponse(response, codes) return data + def assertHasAttr(self, data, name): + ''' + Assert dictionary has given attribute. + :param data: a dictionary + :param name: attribute name + :return: data + ''' + self.assertTrue(name in data, msg="Missing attribute '%s'" % name) + return data + def assertAttr(self, data, name, value): - self.assertTrue(name in data) - self.assertEquals(data[name], value) + ''' + Assert dictionary has given attribute name having given value + :param data: a dictionary + :param name: attribute name + :param value: value to assert + :return: data right back + ''' + self.assertTrue(name in data, msg="Missing attribute '%s'" % name) + self.assertEquals(data[name], value, msg="Attribute '%s' incorrect: expected value '%s' actual value '%s'" % (name, value, data[name])) + return data def assertList(self, data, name): self.assertTrue(name in data) @@ -145,6 +165,21 @@ def assertIdList(self, data, name): self.assertTrue('name' in item) def assertProperties(self, data): + ''' + Asserts that the given data is a python representation of a REDHAWK properties + :param data: A property is a [ + { + 'kinds': [], + 'name': 'FIXME', + 'value': 'FIXME', + 'scaType': 'FIXME', + 'mode': 'FIXME', + 'type': 'FIXME', + 'id': 'FIXME" + },{ + } ] + :return: None + ''' self.assertTrue(isinstance(data, list)) for prop in data: self.assertList(prop, 'kinds') @@ -156,20 +191,39 @@ def assertProperties(self, data): self.assertTrue('id' in prop) + def validate_json(self, data, schemapath): + spath = os.path.join(os.path.dirname(__file__), '../resources/schemas', schemapath) + with open(spath) as f: + sdata = json.load(f) + from jsonschema import validate + validate(data, sdata) + class JsonTests(AsyncHTTPTestCase, JsonAssertions): def get_app(self): return Application() def _json_request(self, url, code, method='GET', json_data=None): + ''' + A json request that can be used in setUp, tearDown, or non-async tests + (those tests not wrapped with @gen_test) + :param url: + :param code: + :param method: + :param json_data: + :return: + ''' body = None if json_data: body = json.dumps(json_data) - AsyncHTTPClient(self.io_loop).fetch(self.get_url(Default.REST_BASE+url), self.stop, method=method, body=body) + fullurl = self.get_url(Default.REST_BASE+url) + AsyncHTTPClient(self.io_loop).fetch(fullurl, self.stop, method=method, body=body) response = self.wait() - self.assertEquals(code, response.code) + self.assertEquals(code, response.code, + msg="Unexpected response in request '%s'. Expected %s, Received %s\nBody: %s" % + (fullurl, code, response.code, response.body)) data = {} if response.body: @@ -192,8 +246,11 @@ def _async_json_request(self, url, code, method='GET', json_data=None): if json_data: body = json.dumps(json_data) - response = yield MyAsyncHTTPClient(self.io_loop).fetch(self.get_url(Default.REST_BASE+url), self.stop, method=method, body=body, raise_error=False) - self.assertEquals(code, response.code) + fullurl = self.get_url(Default.REST_BASE+url) + response = yield MyAsyncHTTPClient(self.io_loop).fetch(fullurl, None, method=method, body=body, raise_error=False) + self.assertEquals(code, response.code, + msg="Unexpected response in request '%s'. Expected %s, Received %s\nBody: %s" % + (fullurl, code, response.code, response.body)) data = {} if response.body: @@ -248,3 +305,95 @@ def _resource_not_found(self, body): self.assertEquals(body['error'], Default.RESOURCE_NOT_FOUND_ERR) self.assertTrue('message' in body) self.assertTrue(Default.RESOURCE_NOT_FOUND_MSG_REGEX.match(body['message'])) + + +class RedhawkTests(JsonTests): + + def _clean_applications(self, apps=None ): + ''' + Cleans the given applications. + + :param apps: List of applications to kill, None to kill all + :return: + ''' + + def _matches(name, list): + name = str(name) + if not list: + return True + for a in list: + if name.startswith(a): + return True + return False + + url = '/domains/'+Default.DOMAIN_NAME + json, resp = self._json_request(url, 200) + if 'applications' not in json: + json['applications'] = [] + for a in json['applications']: + if _matches(a['name'], apps): + self._json_request( + '/domains/'+Default.DOMAIN_NAME+'/applications/'+a['id'], + 200, + 'DELETE' + ) + + @tornado.gen.coroutine + def _async_clean_applications(self, apps=None ): + ''' + Cleans the given applications. + + :param apps: List of applications to kill, None to kill all + :return: + ''' + + def _matches(name, list): + name = str(name) + if not list: + return True + for a in list: + if name.startswith(a): + return True + return False + + url, applications = yield self._get_applications() + for a in applications: + if _matches(a['name'], apps): + self._release(a['id']) + + @tornado.gen.coroutine + def _get_applications(self): + url = '/domains/'+Default.DOMAIN_NAME + json, resp = yield self._async_json_request(url, 200) + + self.assertTrue('applications' in json) + + raise tornado.gen.Return((url, json['applications'])) + + @tornado.gen.coroutine + def _launch(self, name): + json, resp = yield self._async_json_request( + '/domains/'+Default.DOMAIN_NAME+'/applications', + 200, + 'POST', + {'name': name} + ) + self.assertTrue('launched' in json) + self.assertTrue('applications' in json) + self.assertTrue(json['launched'] in [x['id'] for x in json['applications']]) + + raise tornado.gen.Return(json['launched']) + + @tornado.gen.coroutine + def _release(self, wf_id): + json, resp = yield self._async_json_request( + '/domains/'+Default.DOMAIN_NAME+'/applications/'+wf_id, + 200, + 'DELETE' + ) + + self.assertAttr(json, 'released', wf_id) + + self.assertTrue('applications' in json) + self.assertFalse(json['released'] in json['applications']) + raise tornado.gen.Return(resp) diff --git a/tests/bulkio.py b/tests/bulkio.py index c09530a..f084e31 100755 --- a/tests/bulkio.py +++ b/tests/bulkio.py @@ -24,6 +24,7 @@ import json import logging import time +import struct import threading from functools import partial @@ -38,6 +39,7 @@ from pyrest import Application from base import JsonTests from defaults import Default +from rest.bulkio_handler import DATA_CONVERSION_MAP # all method returning suite is required by tornado.testing.main() #def all(): @@ -90,7 +92,7 @@ def test_bulkio_ws(self): if not cid: self.fail('Unable to find SigGen component') - url = self.get_url("%s/components/%s/ports/out/bulkio"%(Default.REST_BASE+self.base_url,cid)).replace('http','ws') + url = self.get_url("%s/components/%s/ports/dataFloat_out/bulkio"%(Default.REST_BASE+self.base_url,cid)).replace('http','ws') conn1 = yield websocket.websocket_connect(url, io_loop=self.io_loop) @@ -123,6 +125,85 @@ def test_bulkio_ws(self): self.fail('Did not receive SRI') +class TestDataConverters(unittest.TestCase): + + def _testDataConverter(self, dtype, bpa, input, expected): + """ + Test data converter output + :param dtype: the BULKIO data type + :param bpa: bytes per atom + :param input: list of atoms + :param expected: string array expected + """ + # do sanity check on input + if bpa * len(input) != len(expected): + self.fail("Bad expected value for %s type. Expected %d bytes, got %d bytes" % + (dtype, bpa * len(input), len(expected))) + converter = DATA_CONVERSION_MAP[dtype] + self.assertEquals(expected, converter(input)) + + def testDataConverterDouble(self): + dmax = 1.7e308 + dmin = 1.7e-308 + data = [dmax, dmin, -dmax, -dmin, 0.0] + self._testDataConverter('dataDouble', 8, data, struct.pack('=ddddd', *data), ) + + def testDataConverterFloat(self): + fmax = 3.4e38 + fmin = 3.4e-38 + data = [fmax, fmin, -fmax, -fmin, 0.0] + self._testDataConverter('dataFloat', 4, data, struct.pack('=fffff', *data)) + + def testDataConverterShort(self): + smax = (1<<15)-1 + smin = -(1<<15) + data = [smax, smin, 15, -15, 0] + self._testDataConverter('dataShort', 2, data, struct.pack('=hhhhh', *data)) + + def testDataConverterLong(self): + lmax = (1<<31)-1 + lmin = -(1<<31) + data = [lmax, lmin, 15, -15, 0] + self._testDataConverter('dataLong', 4, data, struct.pack('=lllll', *data)) + + def testDataConverterLongLong(self): + lmax = (1<<63)-1 + lmin = -(1<<63) + data = [lmax, lmin, 15, -15, 0] + self._testDataConverter('dataLongLong', 8, data, struct.pack('=qqqqq', *data)) + + def testDataConverterOctet(self): + omax = (1<<8)-1 + omin = 0 + data = [omax, omin, 15, 0] + self._testDataConverter('dataOctet', 1, data, struct.pack('=BBBB', *data)) + + def testDataConverterULong(self): + lmax = (1<<32)-1 + lmin = 0 + data = [lmax, lmin, 15, 0] + self._testDataConverter('dataUlong', 4, data, struct.pack('=LLLL', *data)) + + def testDataConverterULongLong(self): + lmax = (1<<64)-1 + lmin = 0 + data = [lmax, lmin, 15, 0] + self._testDataConverter('dataUlongLong', 8, data, struct.pack('=QQQQ', *data)) + + def testDataConverterUShort(self): + lmax = (1<<16)-1 + lmin = 0 + data = [lmax, lmin, 15, 0] + self._testDataConverter('dataUshort', 2, data, struct.pack('=HHHH', *data)) + + def testDataConverterChar(self): + lmax = (1<<7)-1 + lmin = -(1<<7) + data = [lmax, lmin, 15, -15, 0] + self._testDataConverter('dataChar', 1, data, struct.pack('=bbbbb', *data)) + + + if __name__ == '__main__': diff --git a/tests/defaults.py b/tests/defaults.py index 33507c3..5d850cd 100644 --- a/tests/defaults.py +++ b/tests/defaults.py @@ -24,7 +24,7 @@ class Default(object): REST_BASE = "/redhawk/rest" - DOMAIN_NAME = "REDHAWK_DEV" + DOMAIN_NAME = "REDHAWK_TEST" WAVEFORM = 'SigTest' diff --git a/tests/domain.py b/tests/domain.py index ef9a701..02bb8df 100644 --- a/tests/domain.py +++ b/tests/domain.py @@ -22,20 +22,88 @@ """ __author__ = 'rpcanno' +import logging +import json +import socket +import tornado +from tornado.httpclient import AsyncHTTPClient + from base import JsonTests from defaults import Default +from model import domain class DomainTests(JsonTests): + def setUp(self): + self.redhawk_remote_bug = domain.redhawk_remote_bug + super(DomainTests, self).setUp() + + def tearDown(self): + super(DomainTests, self).tearDown() + domain.redhawk_remote_bug = self.redhawk_remote_bug + def test_list(self): body, resp = self._json_request("/domains", 200) self.assertTrue('domains' in body) self.assertTrue(isinstance(body['domains'], list)) + if len(body['domains']) == 0: + self.fail("Invalid test. Expected at least one REDHAWK Domain returned, received 0") + for d in body['domains']: + if ':' in d: + self.fail("Not expecting location in domain (no ':'). Received '%s'" % d) + + def test_list_location(self): + hosts = ['localhost', socket.gethostname()] + for h in hosts: + body, resp = self._json_request("/domains/%s:" % h, 200) + self.assertTrue('domains' in body) + self.assertTrue(isinstance(body['domains'], list)) + if len(body['domains']) == 0: + self.fail("Invalid test. Expected at least one REDHAWK Domain returned, received 0") + for d in body['domains']: + d = str(d) + if not d.startswith("%s:" % h): + self.fail("Returned domain missing location. Expected %s:XXX received %s" % (h, d)) + + def test_list_bad_location(self): + body, resp = self._json_request("/domains/localhost_bad:", 404) + self.assertAttr(body, 'error', 'ResourceNotFound') + self.assertAttr(body, 'message', "Unable to connect with NameService on host 'localhost_bad'") + + def test_list_bad_redhawk_version(self): + domain.redhawk_remote_bug = True + body, resp = self._json_request("/domains/localhost:", 200) + body, resp = self._json_request("/domains/", 200) + body, resp = self._json_request("/domains/:", 200) + body, resp = self._json_request("/domains/localhost_awful:", 500) + self.assertAttr(body, 'error', 'Exception') + self.assertAttr(body, 'message', "Remote domain connectivity is unavailable in Redhawk <= 1.10.2") + + + def test_bad_redhawk_version(self): + domain.redhawk_remote_bug = True + body, resp = self._json_request("/domains/" + Default.DOMAIN_NAME, 200) + body, resp = self._json_request("/domains/:" + Default.DOMAIN_NAME, 200) + body, resp = self._json_request("/domains/localhost:" + Default.DOMAIN_NAME, 200) + body, resp = self._json_request("/domains/foobar:" + Default.DOMAIN_NAME, 500) + self.assertAttr(body, 'error', 'Exception') + self.assertAttr(body, 'message', "Remote domain connectivity is unavailable in Redhawk <= 1.10.2") def test_info(self): - body, resp = self._json_request("/domains/"+Default.DOMAIN_NAME, 200) + body, resp = self._json_request("/domains/" + Default.DOMAIN_NAME, 200) + + self.assertTrue('name' in body) + self.assertEquals(body['name'], Default.DOMAIN_NAME) + + self.assertTrue('applications' in body) + self.assertTrue('deviceManagers' in body) + self.assertTrue('properties' in body) + self.assertTrue('id' in body) + + def test_location_good(self): + body, resp = self._json_request("/domains/localhost:" + Default.DOMAIN_NAME, 200) self.assertTrue('name' in body) self.assertEquals(body['name'], Default.DOMAIN_NAME) @@ -45,7 +113,38 @@ def test_info(self): self.assertTrue('properties' in body) self.assertTrue('id' in body) - def test_info_not_found(self): + def test_location_bad(self): + domainname = 'localh:%s' % Default.DOMAIN_NAME + body, resp = self._json_request("/domains/%s" % domainname, 404) + self.assertAttr(body, 'error', 'ResourceNotFound') + self.assertAttr(body, 'message', "Unable to find domain '%s'" % domainname) + + def test_domain_not_found(self): body, resp = self._json_request("/domains/ldskfadjklfsdjkfasdl", 404) print body self._resource_not_found(body) + + @tornado.testing.gen_test + def test_domain_get_instance(self): + response = yield AsyncHTTPClient(self.io_loop).fetch( + self.get_url("%s/domains/%s" % (Default.REST_BASE, Default.DOMAIN_NAME))) + self.assertEquals(200, response.code) + data = json.loads(response.body) + + for name in ('applications', 'properties', 'deviceManagers', 'id', 'name'): + self.assertTrue(name in data, "json missing %s" % name) + + def test_domain_get_failure(self): + # callback must be used to get response to non-200 HTTPResponse + AsyncHTTPClient(self.io_loop).fetch(self.get_url("%s/domains/%s" % (Default.REST_BASE, 'REDHAWK_DEV_FOO')), + self.stop) + response = self.wait() + + self.assertEquals(404, response.code) + pdata = json.loads(response.body) + logging.debug("Found port data %s", pdata) + # FIXME: Check error response + + + + diff --git a/tests/port.py b/tests/port.py index cf1a639..7621f18 100755 --- a/tests/port.py +++ b/tests/port.py @@ -23,27 +23,24 @@ import unittest import json import logging -import time -import threading -from functools import partial # tornado imports import tornado import tornado.testing from tornado.testing import AsyncHTTPTestCase, LogTrapTestCase -from tornado.httpclient import AsyncHTTPClient, HTTPRequest -from tornado import websocket, gen +from tornado.httpclient import AsyncHTTPClient -from base import JsonAssertions +from base import JsonAssertions, RedhawkTests # application imports from pyrest import Application +from defaults import Default # all method returning suite is required by tornado.testing.main() def all(): return unittest.TestLoader().loadTestsFromModule(__import__(__name__)) -class PortTests(AsyncHTTPTestCase, LogTrapTestCase, JsonAssertions): +class PortTests(RedhawkTests, AsyncHTTPTestCase, LogTrapTestCase, JsonAssertions): # def setUp(self): # super(RESTfulTest, self).setUp() @@ -53,40 +50,19 @@ class PortTests(AsyncHTTPTestCase, LogTrapTestCase, JsonAssertions): def get_app(self): return Application(debug=True, _ioloop=self.io_loop) - - @tornado.testing.gen_test - def test_domain_get_instance(self): - response = yield AsyncHTTPClient(self.io_loop).fetch(self.get_url('/redhawk/rest/domains/REDHAWK_DEV')) - self.assertEquals(200, response.code) - data = json.loads(response.body) - - for name in ('applications', 'properties', 'deviceManagers', 'id', 'name'): - self.assertTrue(name in data, "json missing %s" % name) - - - def test_domain_get_failure(self): - # callback must be used to get response to non-200 HTTPResponse - AsyncHTTPClient(self.io_loop).fetch(self.get_url('/redhawk/rest/domains/REDHAWK_DEV_FOO'), self.stop) - response = self.wait() - - self.assertEquals(404, response.code) - pdata = json.loads(response.body) - logging.debug("Found port data %s", pdata) - # FIXME: Check error response - - @tornado.testing.gen_test def test_application_port_get(self): + self._async_clean_applications() # get a list of waveforms - response = yield AsyncHTTPClient(self.io_loop).fetch(self.get_url('/redhawk/rest/domains/REDHAWK_DEV/applications')) - self.assertEquals(200, response.code) - data = json.loads(response.body) - for wf in data['applications']: - portr = yield AsyncHTTPClient(self.io_loop).fetch(self.get_url('/redhawk/rest/domains/REDHAWK_DEV/applications/%s/ports' % wf['id'])) - self.assertEquals(200, portr.code) - pdata = json.loads(portr.body) - # FIXME: Test against a applications with ports - logging.debug("Found port data %s", pdata) - break - else: - self.fail('Unable to find any applications') + id = yield self._launch('SigTest') + self._async_sleep(1) + portr = yield AsyncHTTPClient(self.io_loop).fetch(self.get_url("%s/domains/%s/applications/%s/ports" % (Default.REST_BASE, Default.DOMAIN_NAME, id))) + self.assertEquals(200, portr.code) + pdata = json.loads(portr.body) + # imp + # orting here so that only this test case fails if it doesn't exist + self.validate_json(pdata, "ports.schema.json") + + # FIXME: Test against a applications with ports + # logging.debug("Found port data %s", pdata) + # self.fail("BAD") diff --git a/tests/sysinfo.py b/tests/sysinfo.py new file mode 100644 index 0000000..d78e7b6 --- /dev/null +++ b/tests/sysinfo.py @@ -0,0 +1,39 @@ +# +# This file is protected by Copyright. Please refer to the COPYRIGHT file +# distributed with this source distribution. +# +# This file is part of REDHAWK rest-python. +# +# REDHAWK rest-python is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# REDHAWK rest-python is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. +# +""" +Tornado tests for the /domain portion of the REST API +""" +__author__ = 'depew' + +from base import JsonTests +from defaults import Default +import types + + +class SysinfoTests(JsonTests): + + def test_sysinfo(self): + body, resp = self._json_request("/sysinfo", 200) + self.assertHasAttr(body, 'supportsRemoteLocations') + self.assertHasAttr(body, 'redhawk.version') + self.assertHasAttr(body, 'rhweb.version') + if not isinstance(body['supportsRemoteLocations'], types.BooleanType): + self.fail("Expected properties.remoteLocations to be a boolean, got %s" % type(p['remoteLocations'])) +