Commit f5978257 authored by Michael Weinrich's avatar Michael Weinrich
Browse files

Code clean up and documentation

parent 415080c0
......@@ -49,7 +49,7 @@ class UPnTGUI:
while result:
result = model.remove(children_iter)
host = self.UPnT.knownHosts[ip]
host = self.UPnT._knownHosts[ip]
for uuid, device in host._devices.items():
device_iter = model.append(current_parent, [self.device_icon, device.deviceType + ':' + device.deviceVersion])
for uuid, service in device._services.items():
......
#: C{tuple} with version information
__version_info__ = (0,3,0)
__version_info__ = (0,5,0)
#: A C{str} variable that shows the actual version of this software.
__version__ = '%d.%d.%d' % (__version_info__[0],__version_info__[1],__version_info__[2],)
......@@ -19,7 +19,7 @@ from upntest.host import Host
class UPnT(log.Loggable):
"""
Main class of the test tool. All messages from UPnP devices are send to
this class that is distibuting the messages to the right object in the
this class which is distibuting the messages to the right object in the
hierarchy.
"""
......@@ -46,7 +46,8 @@ class UPnT(log.Loggable):
"""A C{list} that stores all currently running C{Deferred}s."""
self._hostsToRemove = []
"""A C{list} of hosts that already sent goodbye messages (not used yet)"""
"""A C{list} of hosts that already sent goodbye messages (not used yet).
"""
self._config = config
"""Stores the given config options to pass it on to other objects who
......
# Licensed under the MIT license
# http://opensource.org/licenses/mit-license.php
"""
Module that holds the classes for checking control messages and responses
sent by client or server devices.
# Copyright 2007, Michael Weinrich <testsuite@michael-weinrich.de>
import time
import os
from coherence import log
@license: Licensed under the MIT license
http://opensource.org/licenses/mit-license.php
@author: Michael Weinrich <testsuite@michael-weinrich.de>
@copyright: Copyright 2007, Michael Weinrich
"""
# external includes
import louie
from twisted.internet import reactor, defer
from lxml import etree
import os
import time
from twisted.internet import reactor, defer
# Coherence includes
from coherence import log
# local includes
from upntest.soap_proxy import SOAPProxy
class ServerControl(log.Loggable):
"""
Implements methods to invoke all actions that a service offers and to check
the responses.
This class contains methods to invoke all actions that a service offers and
to check the responses for validity.
The actions that are invoked are read from the service template that is
selected based on the service type. The actions listed in there can be
customized or removed using an additional file containing the according
instructions (see usage documentation).
"""
logCategory = 'UPnT_ServerControl'
"""Log category for the server control checker."""
def __init__(self, parent, config, service_type, service_version, uuid):
"""
Initailise the server control messages checker.
@param parent: "Service that is to be tested.
@param config: A C{dict} containing the configuration data from the
config file.
@param service_type: Type of service that is to be tested.
@param service_version: Version of service type.
@param uuid: UUID of the service to be tested (C{str}).
"""
self.parentService = parent
"""Service that is tested."""
self._testcasesXml = None
"""XML file containing the service description with all state variables
and actions."""
self._uuidXml = None
"""Device specific service description that is merged with the general
one in L{_testcasesXml}."""
self._ready = True
"""A C{bool} indicating that the device initialisation was successful."""
if service_type != "" and \
service_version != "" and \
uuid != "":
......@@ -63,16 +89,20 @@ class ServerControl(log.Loggable):
return
self.loop_tests = False
"""A C{bool} indicating whether the tests should be run repeatedly until
this variable is set to C{False} (which is the default)."""
self._testcases = {}
"""A C{dict} that is used to store all the data neccessary to invoke an
action on the remote device."""
self.parseTestcases()
def parseTestcases(self):
"""
Parse files with test cases and create list for testing.
Parse the file with common information about the testcases. Afterwards
filter that input using the UUID specific information.
Parses the file with common information about the testcases. Afterwards
filter that input using the file with UUID specific information.
"""
self.debug('parseTestcases')
......@@ -90,16 +120,16 @@ class ServerControl(log.Loggable):
self._testcases[id]['InArgs'][argument.tag] = argument.text
self._testcases[id]['ExpectedReturnCode'] = testcase.findtext('ExpectedReturnCode')
self.debug(self._testcases)
#self.debug(self._testcases)
# read the device specific file and make according changes
#self.debug(etree.tostring(self._uuidXml, pretty_print=True))
specific_changes = self._uuidXml.xpath('/ServiceControlSyntaxTestCases/TestCaseList/TestCase')
#self.debug(specific_changes)
for change in specific_changes:
id = change.findtext('Id')
if self._testcases.has_key(id):
self.debug('change.get = ' + change.get('delete'))
if change.get('delete') == 'yes':
self.debug('change.get = ' + change.get('delete', 'no'))
if change.get('delete', 'no') == 'yes':
del self._testcases[id]
else:
category = change.findtext('Category')
......@@ -120,11 +150,16 @@ class ServerControl(log.Loggable):
def startTesting(self, loop_tests=False):
"""
Start cycling through all listed action tests.
Set all variables and start the test asynchronusly.
When using loop=True the cycle starts again at the beginning of the
method list.
@param loop_tests: If C{True}, the test loop starts again at the beginning
of the method list.
@return: A C{Deferred} which is activated when the test run is
finished. If L{_ready} is still C{False}, an already activated C{Deferred}
is returned.
"""
if not self._ready:
return defer.succeed(False)
......@@ -137,17 +172,18 @@ class ServerControl(log.Loggable):
"""
Execution of the actual test loop.
Go through all testcases and send a control message for each test case.
Go through all testcases and send a control message for each test case.
If L{loop_tests} is C{True}, repeat the tests until it is set to C{False}.
"""
self.debug('doTesting')
is_looped = True
test_result = True
url = self.parentService._urlBase + self.parentService.controlUrl
namespace = self.parentService.schema + ':' + self.parentService.serviceType + ':' + self.parentService.serviceVersion
instance_id = 0
while is_looped:
for testcase_id, testcase in self._testcases.items():
url = self.parentService._urlBase + self.parentService.controlUrl
namespace = self.parentService.schema + ':' + self.parentService.serviceType + ':' + self.parentService.serviceVersion
action = "%s#%s" % (namespace, testcase['ActionName'])
instance_id = 0
if testcase.has_key('InArgs') and testcase['InArgs'].has_key('InstanceID'):
instance_id = testcase['InArgs']['InstanceID']
callClient = SOAPProxy(url, namespace=("u", namespace), soapaction=action)
......@@ -165,11 +201,23 @@ class ServerControl(log.Loggable):
def stopTesting(self):
"""
If some request is still running, wait for completion
Set L{loop_tests} to C{False} and stop the test loop after the current
cycle.
"""
self.loop_tests = False
def callFailed(self, failure, action, url, args, action_name):
"""
Callback for an unsuccessful action invocation.
@param failure: The failure that caused the error.
@param action: Name of the action that failed including service type
(C{str}).
@param url: URL of the event server (C{str}).
@param args: C{dict} containing all IN arguments of the action.
@param action_name: Name of the action (C{str}).
"""
self.warning("error: invoking %s on %s with %r" % (action,
url,
args))
......@@ -184,13 +232,24 @@ class ServerControl(log.Loggable):
#return failure
def callSucceeded(self, results, action, instance_id):
print "\nok: call %s (instance %d) returned" % (action, instance_id)
"""
Callback for a successful action invocation.
@param results: The results of the action invocation.
@param action: Name of the action including service type (C{str}).
@param instance_id: Instance ID that was used during the invocation
(C{int}).
"""
louie.send('UPnT.infoMessage', None,
"\nok: call %s (instance %d) returned" % (action, instance_id))
if len(results) > 0:
for out_arg, out_val in results.items():
print out_arg + ': ' + out_val
louie.send('UPnT.infoMessage', None,
"%s: %s" % (out_arg, out_val))
else:
print 'no return values'
louie.send('UPnT.infoMessage', None,
'no return values' % (action, instance_id))
class ClientControl(log.Loggable):
......@@ -198,12 +257,23 @@ class ClientControl(log.Loggable):
Device to check is a control point so we can only perform passive checks on
the messages sent by the device.
"""
logCategory = 'UPnT_ClientControl'
"""Log category for the client control checker."""
serviceActions = {}
"""C{dict} conatining all the actions that are found in all the available
service templates. This is a class variable so that the templates are
available to all instances once the first instance parsed all templates."""
def __init__(self, templates_dir):
"""
Read all avilable service templates and store all actions found in
L{serviceActions}.
@param templates_dir: Path to the directory where the service and device
templates are located (C{str}).
"""
if len(self.serviceActions) == 0:
template_files = [template_file for template_file in os.listdir(templates_dir + 'service/') if not template_file.startswith('.')]
......@@ -233,102 +303,97 @@ class ClientControl(log.Loggable):
louie.connect(self.checkControlMessage, 'UPnT.control_message', louie.Any, weak=False)
def checkControlMessage(self, command, headers, body, remotehost):
def checkControlMessage(self, command, header, body, remotehost):
"""
Check control message received from a Control Point.
@param command: A C{dict} for the command line of the event packet with
keys 'method', 'resource' and 'protocol'.
@param header: A C{dict} containing all headers from the event packet
except the command line.
@param body: C{str} containing the body of the event packet.
@return: C{True} if the packet was valid or C{False} if it was invalid.
"""
#print command
#print headers
# HTTP command check
if command['method'] != 'POST' and command['method'] != 'M-POST':
#louie.send('UPnT.control.invocation_incorrect',
# None,
# 'Control: Wrong method (has to be POST or M-POST',
# [command['path'], header['host']]
# )
louie.send('UPnT.infoMessage',
None,
'Control: Wrong method (has to be POST or M-POST, %r --> %s/%s)' % (remotehost, headers['host'], command['path'])
'Control: Wrong method (has to be POST or M-POST, %r --> %s/%s)' % (remotehost, header['host'], command['path'])
)
return False
# HOST header check
if not headers.has_key('host'):
#louie.send(
# 'UPnT.event.notify_incorrect',
# None,
# 'Notify: HOST header missing',
# [command['path'], header['host']]
# )
if not header.has_key('host'):
louie.send('UPnT.infoMessage',
None,
'Control: HOST header missing (%r --> %s/%s)' % (remotehost, headers['host'], command['path'])
'Control: HOST header missing (%r --> %s/%s)' % (remotehost, header['host'], command['path'])
)
return False
# CONTENT-LENGTH header check
if not headers.has_key('content-length'):
if not header.has_key('content-length'):
louie.send('UPnT.infoMessage',
None,
'Control: CONTENT-LENGTH header missing (%r --> %s/%s)' % (remotehost, headers['host'], command['path'])
'Control: CONTENT-LENGTH header missing (%r --> %s/%s)' % (remotehost, header['host'], command['path'])
)
return False
elif int(headers['content-length']) != len(body):
elif int(header['content-length']) != len(body):
louie.send('UPnT.infoMessage',
None,
'Control: Wrong CONTENT-LENGTH header value (%r, should be %r, %r --> %s/%s)' % (headers['content-length'], len(body), remotehost, headers['host'], command['path'])
'Control: Wrong CONTENT-LENGTH header value (%r, should be %r, %r --> %s/%s)' % (header['content-length'], len(body), remotehost, header['host'], command['path'])
)
return False
# CONTENT-TYPE header check
if not headers.has_key('content-type'):
if not header.has_key('content-type'):
louie.send('UPnT.infoMessage',
None,
'Control: CONTENT-TYPE header missing (%r --> %s/%s)' % (remotehost, headers['host'], command['path'])
'Control: CONTENT-TYPE header missing (%r --> %s/%s)' % (remotehost, header['host'], command['path'])
)
return False
elif not headers['content-type'].startswith('text/xml'):
elif not header['content-type'].startswith('text/xml'):
louie.send('UPnT.infoMessage',
None,
'Control: Wrong CONTENT-TYPE header value (%r, should be "text/xml") (%r --> %s/%s)' % (headers['content-type'], remotehost, headers['host'], command['path'])
'Control: Wrong CONTENT-TYPE header value (%r, should be "text/xml") (%r --> %s/%s)' % (header['content-type'], remotehost, header['host'], command['path'])
)
return False
elif headers['content-type'].lower().count('charset="utf-8"') == 0:
elif header['content-type'].lower().count('charset="utf-8"') == 0:
louie.send('UPnT.infoMessage', None, 'You should include charset="utf-8" in the CONTENT-TYPE header of the control commands of %r.' % remotehost)
# for POST just SOAPACTION header, for M-POST MAN and xx-SOAPACTION
soapaction = ''
if command['method'] == 'POST':
if not headers.has_key('soapaction'):
if not header.has_key('soapaction'):
louie.send('UPnT.infoMessage',
None,
'Control: SOAPACTION header missing (%r --> %s/%s)' % (remotehost, headers['host'], command['path'])
'Control: SOAPACTION header missing (%r --> %s/%s)' % (remotehost, header['host'], command['path'])
)
return False
else:
soapaction = headers['soapaction']
soapaction = header['soapaction']
else:
if not headers.has_key('man'):
if not header.has_key('man'):
louie.send('UPnT.infoMessage',
None,
'Control: MAN header missing, because packet uses M-POST (%r --> %s/%s)' % (remotehost, headers['host'], command['path'])
'Control: MAN header missing, because packet uses M-POST (%r --> %s/%s)' % (remotehost, header['host'], command['path'])
)
return False
man_header_val = headers['man']
man_header_val = header['man']
man_ns = man_header_val[man_header_val.find('=')+1:].lower()
if not headers.has_key(man_ns + '-soapaction'):
if not header.has_key(man_ns + '-soapaction'):
louie.send('UPnT.infoMessage',
None,
'Control: %s-SOAPACTION header missing, because packet uses M-POST (%r --> %s/%s)' % (man_ns, remotehost, headers['host'], command['path'])
'Control: %s-SOAPACTION header missing, because packet uses M-POST (%r --> %s/%s)' % (man_ns, remotehost, header['host'], command['path'])
)
return False
soapaction = headers[man_ns + '-soapaction']
soapaction = header[man_ns + '-soapaction']
if len(soapaction) == 0:
louie.send('UPnT.infoMessage',
None,
'Control: SOAPACTION empty (%r --> %s/%s)' % (remotehost, headers['host'], command['path'])
'Control: SOAPACTION empty (%r --> %s/%s)' % (remotehost, header['host'], command['path'])
)
return False
......@@ -339,15 +404,16 @@ class ClientControl(log.Loggable):
if not self.serviceActions.has_key(serviceName):
louie.send('UPnT.infoMessage',
None,
'Control: Unknown service name %s (%r --> %s/%s)' % (serviceName, remotehost, headers['host'], command['path'])
'Control: Unknown service name %s (%r --> %s/%s)' % (serviceName, remotehost, header['host'], command['path'])
)
return False
if not self.serviceActions[serviceName].has_key(actionName):
louie.send('UPnT.infoMessage',
None,
'Control: SOAPACTION %s not defined for device type %s (%r --> %s/%s)' % (actionName, serviceName, remotehost, headers['host'], command['path'])
'Control: SOAPACTION %s not defined for device type %s (%r --> %s/%s)' % (actionName, serviceName, remotehost, header['host'], command['path'])
)
return False
print 'Control call ok'
\ No newline at end of file
return True
self.debug('Control call ok')
\ No newline at end of file
......@@ -11,11 +11,11 @@ use/control them, ususally a UPnP ControlPoint.
"""
# external includes
from lxml import etree
from StringIO import StringIO
import urllib2
from twisted.internet import reactor, defer
from lxml import etree
import louie
import urllib2
# Coherence includes
from coherence import log
from coherence.upnp.core import utils
......@@ -89,7 +89,7 @@ class ServerDevice(log.Loggable):
"""The L{Host} where this device resides. Needed to ease tracking down
errors that are displayed."""
self._rootDevice = False
"""A C{bool} indicating wheather this device is a root device or not."""
"""A C{bool} indicating whether this device is a root device or not."""
self._schema = ''
"""The schema of this device (C{str})."""
self._deviceType = ''
......@@ -100,7 +100,7 @@ class ServerDevice(log.Loggable):
"""The URL where the desciption can be downloaded."""
self._urlBase = ''
"""The base URL with which every realtiv URL in the description is
preceded with"""
preceded with."""
self._subDevices = {}
"""A C{dict} containing all devices that are subdevice to this device."""
self._services = {}
......@@ -228,7 +228,7 @@ class ServerDevice(log.Loggable):
"""
Get the the root device status of this device.
@return: A L{bool} indicating wheather this device is a root device or
@return: A L{bool} indicating whether this device is a root device or
not.
"""
return self._rootDevice
......@@ -242,7 +242,7 @@ class ServerDevice(log.Loggable):
@param headers: A C{dict} conataining the headers of the received SSDP
packet.
@param new: Indicator wheather this device was just created or if it
@param new: Indicator whether this device was just created or if it
will be updated with the information for the SSDP packet.
"""
......
......@@ -328,13 +328,15 @@ class ServerEventMessageChecks(log.Loggable):
else:
error = self._xmlschema.error_log.last_error
self.warning(error)
service_ident = header_host + command_path
louie.send(
'UPnT.event.notify_incorrect',
None,
"Body of notify message not valid! (%s)" % service_ident,
[header_host, command_path]
)
louie.send('UPnT.infoMessage', None, 'Body of notify message not valid! (%s%s)' % (header_host, command_path))
louie.send('UPnT.infoMessage', None, 'Body of notify message not valid! (%s%s)\nError: %s\n%s' %
(header_host, command_path, error, etree.tostring(doc, pretty_print=True)))
self.debug('Message:\n%s' % message)
......
......@@ -49,7 +49,7 @@ class Host(log.Loggable):
need it."""
self._ip = ip
"""The IP of the host represented by this object"""
"""The IP of the host represented by this object."""
self._serverDevices = {}
"""A C{dict} containing all server devices that run on this host. These
......@@ -176,7 +176,8 @@ class Host(log.Loggable):
louie.send('UPnT.errorMessage',
None,
'HostValidationError (%s)' % self.ip,
'LOCATION not reachable (%s)' % headers['location'])
'LOCATION not reachable (%s)\n\n%s' %
(headers['location'], header))
self.warning('LOCATION not reachable (%s)' % headers['location'])
self._discovery_packets_valid = False
......@@ -428,8 +429,7 @@ class Host(log.Loggable):
Check special content of an alive notification packet for errors. The
checked headers are:
- CACHE-CONTROL max-age
- LOCATION which is also checked for reachability using a HTTP HEAD
request. No content is retrieved.
- LOCATION which is also checked for reachability.
- SERVER
@param header: A C{dict} containing all headers from the SSDP packet
......@@ -508,7 +508,11 @@ class Host(log.Loggable):
return None
self.info('Trying to fetch description...')
d = utils.getPage(header['location'], method='HEAD')
d = None
if self._config.get('use_HEAD_request', False):
d = utils.getPage(header['location'], method='HEAD')
else:
d = utils.getPage(header['location'])
louie.send('UPnT.running_deferred', None, d)
return d
......@@ -558,7 +562,7 @@ class Host(log.Loggable):
#HOST must be 239.255.255.250, port can be omitted
if not headers['host'].startswith('239.255.255.250'):
louie.send('UPnT.errorMessage', None, 'HostValidationError (%s)' % self.ip, 'Wrong HOST')
louie.send('UPnT.errorMessage', None, 'HostValidationError (%s)' % self.ip, 'Wrong HOST header')
self.debug('Wrong HOST')
return None
......@@ -570,7 +574,7 @@ class Host(log.Loggable):
#MX header must be a number, should be between 1 and 120
if not headers['mx'].strip().isdigit():
louie.send('UPnT.errorMessage', None, 'HostValidationError (%s)' % self.ip, 'Wrong MX header')
louie.send('UPnT.errorMessage', None, 'HostValidationError (%s)' % self.ip, 'Invalid MX header')
self.debug('Wrong MX header')
return None
......@@ -608,7 +612,7 @@ class Host(log.Loggable):
def checkDiscoveryAnswerPacket(self, header, payload):
"""checks the answer of a discovery packet for errors and returns
True if everything looks ok"""
C{True} if everything looks ok."""
self.info('Processing M-SEARCH response from %s' % self.ip)
......@@ -734,6 +738,10 @@ class Host(log.Loggable):
return None
self.debug('Trying to fetch description...')
d = utils.getPage(headers['location'], method='HEAD')
d = None
if self._config.get('use_HEAD_request', False):
d = utils.getPage(headers['location'], method='HEAD')
else:
d = utils.getPage(headers['location'])
louie.send('UPnT.running_deferred', None, d)
return d
This diff is collapsed.
......@@ -16,11 +16,16 @@
from coherence.extern.et import ET
NS_SOAP_ENV = "{http://schemas.xmlsoap.org/soap/envelope/}"
"""Namespace of SOAP envelope for use with C{ElementTree}."""
NS_SOAP_ENC = "{http://schemas.xmlsoap.org/soap/encoding/}"
"""Namespace of SOAP message encoding for use with C{ElementTree}."""
NS_XSI = "{http://www.w3.org/1999/XMLSchema-instance}"
"""Namespace of XML Schema Instance for use with C{ElementTree}."""
NS_XSD = "{http://www.w3.org/1999/XMLSchema}"
"""Namespace of XML Schema for use with C{ElementTree}."""
SOAP_ENCODING = "http://schemas.xmlsoap.org/soap/encoding/"
"""Namespace of SOAP message encoding."""
UPNPERRORS = {401:'Invalid Action',
402:'Invalid Args',
......@@ -38,6 +43,7 @@ UPNPERRORS = {401:'Invalid Action',
610:'Invalid Sequence',
611:'Invalid Control URL',
612:'No Such Session'}
"""Listing of error codes predefined by the UPnP Device Architecture."""
def build_soap_error(status,description='without words'):
""" builds an UPnP SOAP error msg
......@@ -57,7 +63,7 @@ def build_soap_call(method, arguments, is_response=False,
envelope_attrib=None,
typed=None):
""" create a shell for a SOAP request or response element
- set method to none to omitt the method element and
- set method to none to omit the method element and
add the arguments directly to the body (for an error msg)
- arguments can be a dict or an ET.Element
"""
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment