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

first working version including all basic features

parent c5270cd2
......@@ -7,6 +7,7 @@
<xs:sequence>
<xs:element name="ServiceType" type="xs:string" minOccurs="1" maxOccurs="1" />
<xs:element name="ServiceVersion" type="xs:string" minOccurs="1" maxOccurs="1" />
<xs:element name="TestVersion" type="xs:string" minOccurs="0" maxOccurs="1" />
<xs:element name="TestCaseList" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:sequence>
......
......@@ -13,9 +13,9 @@
<Category>Valid Action And Valid InArgs</Category>
<ActionName>PrepareForConnection</ActionName>
<InArgs>
<RemoteProtocolInfo>any-string</RemoteProtocolInfo>
<PeerConnectionManager>any-string</PeerConnectionManager>
<PeerConnectionID>-1</PeerConnectionID>
<RemoteProtocolInfo>http-get:*:audio/mpeg:*</RemoteProtocolInfo>
<PeerConnectionManager>uuid:930788ec-c291-47db-a5f3-436c994778e1/1</PeerConnectionManager>
<PeerConnectionID>0</PeerConnectionID>
<Direction>Input</Direction>
</InArgs>
<ExpectedReturnCode>ACTION_AND_INARGS_ARE_VALID</ExpectedReturnCode>
......
......@@ -27,10 +27,10 @@ feature provided by your editor.
<Category>Valid Action And Valid InArgs</Category>
<ActionName>PrepareForConnection</ActionName>
<InArgs>
<RemoteProtocolInfo>any-string</RemoteProtocolInfo>
<PeerConnectionManager>any-string</PeerConnectionManager>
<PeerConnectionID>-1</PeerConnectionID>
<Direction>Input</Direction>
<RemoteProtocolInfo>http-get:*:audio/mpeg:*</RemoteProtocolInfo>
<PeerConnectionManager>uuid:930788ec-c291-47db-a5f3-436c994778e1/1</PeerConnectionManager>
<PeerConnectionID>0</PeerConnectionID>
<Direction>Input</Direction>
</InArgs>
<ExpectedReturnCode>ACTION_AND_INARGS_ARE_VALID</ExpectedReturnCode>
</TestCase>
......
......@@ -54,14 +54,14 @@
<ActionName>CreateObject</ActionName>
<InArgs>
<ContainerID>0</ContainerID>
<Elements>
&lt;DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"&gt;
&lt;item id="" parentID="0" restricted="false"&gt;
&lt;dc:title&gt;Test Object - CDS Syntax Text Case #6&lt;/dc:title&gt;
&lt;upnp:class&gt;object.item&lt;/upnp:class&gt;
&lt;/item&gt;
&lt;/DIDL-Lite&gt;
</Elements>
<Elements>
&lt;DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"&gt;
&lt;item id="" parentID="0" restricted="false"&gt;
&lt;dc:title&gt;Test Object - CDS Syntax Text Case #6&lt;/dc:title&gt;
&lt;upnp:class&gt;object.item&lt;/upnp:class&gt;
&lt;/item&gt;
&lt;/DIDL-Lite&gt;
</Elements>
</InArgs>
<ExpectedReturnCode>ACTION_AND_INARGS_ARE_VALID</ExpectedReturnCode>
</TestCase>
......@@ -80,8 +80,10 @@
<ActionName>UpdateObject</ActionName>
<InArgs>
<ObjectID>0</ObjectID>
<CurrentTagValue>any-string</CurrentTagValue>
<NewTagValue>any-string</NewTagValue>
<CurrentTagValue></CurrentTagValue>
<NewTagValue>
<upnp:genre>Swing</upnp:genre>
</NewTagValue>
</InArgs>
<ExpectedReturnCode>ACTION_AND_INARGS_ARE_VALID</ExpectedReturnCode>
</TestCase>
......@@ -90,8 +92,8 @@
<Category>Valid Action And Valid InArgs</Category>
<ActionName>ImportResource</ActionName>
<InArgs>
<SourceURI>http://host/path/file</SourceURI>
<DestinationURI>http://host/path/file</DestinationURI>
<SourceURI>http://127.0.0.1</SourceURI>
<DestinationURI>http://127.0.0.1</DestinationURI>
</InArgs>
<ExpectedReturnCode>ACTION_AND_INARGS_ARE_VALID</ExpectedReturnCode>
</TestCase>
......@@ -100,8 +102,8 @@
<Category>Valid Action And Valid InArgs</Category>
<ActionName>ExportResource</ActionName>
<InArgs>
<SourceURI>http://host/path/file</SourceURI>
<DestinationURI>http://host/path/file</DestinationURI>
<SourceURI>http://127.0.0.1</SourceURI>
<DestinationURI>http://127.0.0.1</DestinationURI>
</InArgs>
<ExpectedReturnCode>ACTION_AND_INARGS_ARE_VALID</ExpectedReturnCode>
</TestCase>
......@@ -128,7 +130,7 @@
<Category>Valid Action And Valid InArgs</Category>
<ActionName>DeleteResource</ActionName>
<InArgs>
<ResourceURI>http://host/path/file</ResourceURI>
<ResourceURI>http://127.0.0.1</ResourceURI>
</InArgs>
<ExpectedReturnCode>ACTION_AND_INARGS_ARE_VALID</ExpectedReturnCode>
</TestCase>
......
......@@ -114,8 +114,10 @@ feature enabled by your editor.
<ActionName>UpdateObject</ActionName>
<InArgs>
<ObjectID>0</ObjectID>
<CurrentTagValue>any-string</CurrentTagValue>
<NewTagValue>any-string</NewTagValue>
<CurrentTagValue></CurrentTagValue>
<NewTagValue>
<upnp:genre>Swing</upnp:genre>
</NewTagValue>
</InArgs>
<ExpectedReturnCode>ACTION_AND_INARGS_ARE_VALID</ExpectedReturnCode>
</TestCase>
......@@ -126,7 +128,7 @@ feature enabled by your editor.
<ActionName>MoveObject</ActionName>
<InArgs>
<ObjectID>0</ObjectID>
<NewParentID>any-string</NewParentID>
<NewParentID>0</NewParentID>
</InArgs>
<ExpectedReturnCode>ACTION_AND_INARGS_ARE_VALID</ExpectedReturnCode>
</TestCase>
......@@ -136,8 +138,8 @@ feature enabled by your editor.
<Category>Valid Action And Valid InArgs</Category>
<ActionName>ImportResource</ActionName>
<InArgs>
<SourceURI>http://host/path/file1</SourceURI>
<DestinationURI>http://host/path/file2</DestinationURI>
<SourceURI>http://127.0.0.1</SourceURI>
<DestinationURI>http://127.0.0.1</DestinationURI>
</InArgs>
<ExpectedReturnCode>ACTION_AND_INARGS_ARE_VALID</ExpectedReturnCode>
</TestCase>
......@@ -147,8 +149,8 @@ feature enabled by your editor.
<Category>Valid Action And Valid InArgs</Category>
<ActionName>ExportResource</ActionName>
<InArgs>
<SourceURI>http://host/path/file1</SourceURI>
<DestinationURI>http://host/path/file2</DestinationURI>
<SourceURI>http://127.0.0.1</SourceURI>
<DestinationURI>http://127.0.0.1</DestinationURI>
</InArgs>
<ExpectedReturnCode>ACTION_AND_INARGS_ARE_VALID</ExpectedReturnCode>
</TestCase>
......@@ -158,7 +160,7 @@ feature enabled by your editor.
<Category>Valid Action And Valid InArgs</Category>
<ActionName>DeleteResource</ActionName>
<InArgs>
<ResourceURI>http://host/path/file</ResourceURI>
<ResourceURI>http://127.0.0.1</ResourceURI>
</InArgs>
<ExpectedReturnCode>ACTION_AND_INARGS_ARE_VALID</ExpectedReturnCode>
</TestCase>
......@@ -188,8 +190,8 @@ feature enabled by your editor.
<Category>Valid Action And Valid InArgs</Category>
<ActionName>CreateReference</ActionName>
<InArgs>
<ContainerID>container</ContainerID>
<ObjectID>item</ObjectID>
<ContainerID>0</ContainerID>
<ObjectID>0</ObjectID>
</InArgs>
<ExpectedReturnCode>ACTION_AND_INARGS_ARE_VALID</ExpectedReturnCode>
</TestCase>
......
<ServiceControlSyntaxTestCases>
<ServiceType>X_MS_MediaReceiverRegistrar</ServiceType>
<ServiceVersion>1</ServiceVersion>
</ServiceControlSyntaxTestCases>
<ServiceControlSyntaxTestCases>
<ServiceType>ConnectionManager</ServiceType>
<ServiceVersion>1</ServiceVersion>
</ServiceControlSyntaxTestCases>
\ No newline at end of file
<ServiceControlSyntaxTestCases>
<ServiceType>ConnectionManager</ServiceType>
<ServiceVersion>2</ServiceVersion>
</ServiceControlSyntaxTestCases>
\ No newline at end of file
<ServiceControlSyntaxTestCases>
<ServiceType>ContentDirectory</ServiceType>
<ServiceVersion>1</ServiceVersion>
<TestCaseList>
<TestCase delete="yes">
<Id>2</Id>
<Category>Valid Action And Valid InArgs</Category>
<ActionName>GetSortCapabilities</ActionName>
<ExpectedReturnCode>ACTION_AND_INARGS_ARE_VALID</ExpectedReturnCode>
</TestCase>
<TestCase delete="yes">
<Id>7</Id>
<Category>Valid Action And Valid InArgs</Category>
<ActionName>DestroyObject</ActionName>
<InArgs>
<ObjectID>0</ObjectID>
</InArgs>
<ExpectedReturnCode>ACTION_AND_INARGS_ARE_VALID</ExpectedReturnCode>
</TestCase>
<TestCase delete="yes">
<Id>8</Id>
<Category>Valid Action And Valid InArgs</Category>
<ActionName>UpdateObject</ActionName>
<InArgs>
<ObjectID>0</ObjectID>
<CurrentTagValue>any-string</CurrentTagValue>
<NewTagValue>any-string</NewTagValue>
</InArgs>
<ExpectedReturnCode>ACTION_AND_INARGS_ARE_VALID</ExpectedReturnCode>
</TestCase>
<TestCase delete="yes">
<Id>9</Id>
<Category>Valid Action And Valid InArgs</Category>
<ActionName>ImportResource</ActionName>
<InArgs>
<SourceURI>http://host/path/file</SourceURI>
<DestinationURI>http://host/path/file</DestinationURI>
</InArgs>
<ExpectedReturnCode>ACTION_AND_INARGS_ARE_VALID</ExpectedReturnCode>
</TestCase>
<TestCase delete="yes">
<Id>6</Id>
<Category>Valid Action And Valid InArgs</Category>
<ActionName>CreateObject</ActionName>
<InArgs>
<ContainerID>0</ContainerID>
<Elements>
&lt;DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"&gt;
&lt;item id="" parentID="0" restricted="false"&gt;
&lt;dc:title&gt;Test Object - CDS Syntax Text Case #6&lt;/dc:title&gt;
&lt;upnp:class&gt;object.item&lt;/upnp:class&gt;
&lt;/item&gt;
&lt;/DIDL-Lite&gt;
</Elements>
</InArgs>
<ExpectedReturnCode>ACTION_AND_INARGS_ARE_VALID</ExpectedReturnCode>
</TestCase>
</TestCaseList>
</ServiceControlSyntaxTestCases>
<ServiceControlSyntaxTestCases>
<ServiceType>ContentDirectory</ServiceType>
<ServiceVersion>2</ServiceVersion>
</ServiceControlSyntaxTestCases>
<ServiceControlSyntaxTestCases>
<ServiceType>X_MS_MediaReceiverRegistrar</ServiceType>
<ServiceVersion>1</ServiceVersion>
</ServiceControlSyntaxTestCases>
......@@ -15,13 +15,16 @@ from coherence import log
from upntest.host import Host
"""The main class of the whole test suite"""
class UPnT(log.Loggable):
"""
The main class of the whole test suite
"""
logCategory = 'UPnT'
def __init__(self, config={}):
log.init()
#log.init()
self.knownHosts = {}
self.runningDeferreds = []
......@@ -52,6 +55,10 @@ class UPnT(log.Loggable):
validation method.
"""
# devices on the local host are ignored
if ip == self._coherence.hostname:
return
if not self.knownHosts.has_key(ip):
self.knownHosts[ip] = Host(ip, self._config, self._coherence)
self.info('Host added', ip)
......
......@@ -8,6 +8,7 @@ import louie
from coherence import log
from upntest.eventing import ClientEventMessageChecks
from upntest.controlling import ClientControl
class ClientDevice(log.Loggable):
"""
......@@ -15,7 +16,15 @@ class ClientDevice(log.Loggable):
"""
logCategory = 'UPnT_ClientDevice'
eventchecker = None
controlchecker = None
def __init__(self):
def __init__(self, config):
self.eventchecker = ClientEventMessageChecks(self)
\ No newline at end of file
templates_dir = config.get('templates_dir', None)
if not self.eventchecker:
self.eventchecker = ClientEventMessageChecks()
if not self.controlchecker:
self.controlchecker = ClientControl(templates_dir)
\ No newline at end of file
......@@ -3,14 +3,19 @@
# Copyright 2007, Michael Weinrich <testsuite@michael-weinrich.de>
import time
import os
from coherence import log
import louie
from twisted.internet import reactor
from twisted.internet import reactor, defer
from lxml import etree
from upntest.soap_proxy import SOAPProxy
class ServerControl(log.Loggable):
"""
Implements methods to invoke all actions that a service offers and to check
......@@ -18,8 +23,9 @@ class ServerControl(log.Loggable):
"""
logCategory = 'UPnT_ServerControl'
def __init__(self, config, service_type, service_version, uuid):
def __init__(self, parent, config, service_type, service_version, uuid):
self.parentService = parent
self._testcasesXml = None
self._uuidXml = None
......@@ -41,18 +47,30 @@ class ServerControl(log.Loggable):
self.info('Specific file for %s:%s on uuid:%s not found' % (service_type, service_version, uuid))
self.info(error)
self._ready = False
except etree.XMLSyntaxError, error:
self.info('Specific file for %s:%s on uuid:%s cannot be parsed' % (service_type, service_version, uuid))
self.info(error)
self._ready = False
except IOError, error:
self.info('Common file for %s:%s not found' % (service_type, service_version))
self.info(error)
self._ready = False
except etree.XMLSyntaxError, error:
self.info('Common file for %s:%s cannot be parsed' % (service_type, service_version))
self.info(error)
self._ready = False
if not self._ready:
return
self.loop_tests = False
self._testcases = {}
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.
"""
......@@ -72,28 +90,35 @@ class ServerControl(log.Loggable):
self._testcases[id]['InArgs'][argument.tag] = argument.text
self._testcases[id]['ExpectedReturnCode'] = testcase.findtext('ExpectedReturnCode')
self.debug(self._testcases)
# read the device specific file and make according changes
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):
category = change.findtext('Category')
action_name = change.findtext('ActionName')
in_args = change.findall('./InArgs/*')
expected_return_code = change.findtext('ExpectedReturnCode')
bool1 = self._testcases[id].has_key('InArgs') and len(in_args) == len(self._testcases[id]['InArgs'])
bool2 = not self._testcases[id].has_key('InArgs') and len(in_args) == 0
if category != "" and action_name != "" and expected_return_code != "" and \
((bool1 and not bool2) or (not bool1 and bool2)):
self._testcases[id]['Category'] = category
self._testcases[id]['ActionName'] = action_name
self._testcases[id]['ExpectedReturnCode'] = expected_return_code
if bool1:
for argument in in_args:
self._testcases[id]['InArgs'][argument.tag] = argument.text
def startTesting(self, loop=False):
self.debug('change.get = ' + change.get('delete'))
if change.get('delete') == 'yes':
del self._testcases[id]
else:
category = change.findtext('Category')
action_name = change.findtext('ActionName')
in_args = change.findall('./InArgs/*')
expected_return_code = change.findtext('ExpectedReturnCode')
bool1 = self._testcases[id].has_key('InArgs') and len(in_args) == len(self._testcases[id]['InArgs'])
bool2 = not self._testcases[id].has_key('InArgs') and len(in_args) == 0
if category != "" and action_name != "" and expected_return_code != "" and \
((bool1 and not bool2) or (not bool1 and bool2)):
self._testcases[id]['Category'] = category
self._testcases[id]['ActionName'] = action_name
self._testcases[id]['ExpectedReturnCode'] = expected_return_code
if bool1:
for argument in in_args:
self._testcases[id]['InArgs'][argument.tag] = argument.text
def startTesting(self, loop_tests=False):
"""
Start cycling through all listed action tests.
......@@ -101,13 +126,72 @@ class ServerControl(log.Loggable):
method list.
"""
if not self._ready:
return
return defer.succeed(False)
self.loop_tests = loop_tests
self.test_deferred = defer.Deferred()
reactor.callLater(1, self.doTesting)
return self.test_deferred
def doTesting(self):
"""
Execution of the actual test loop.
Go through all testcases and send a control message for each test case.
"""
self.debug('doTesting')
is_looped = True
test_result = True
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)
# actual call
in_args = {}
if testcase.has_key('InArgs'):
in_args = testcase['InArgs']
d = callClient.callRemote(testcase['ActionName'], in_args)
d.addCallback(self.callSucceeded, action, instance_id)
d.addErrback(self.callFailed, action, url, in_args, testcase['ActionName'])
is_looped = self.loop_tests
self.test_deferred.callback(test_result)
def stopTesting(self):
"""
If some request is still running, wait for completion
"""
pass
self.loop_tests = False
def callFailed(self, failure, action, url, args, action_name):
self.warning("error: invoking %s on %s with %r" % (action,
url,
args))
self.info(failure)
action_list = self.parentService._actionList
if action_list.has_key(action_name):
if not action_list[action_name]['Optional']:
louie.send('UPnT.infoMessage', None,
"Required action %s couldn't be invoked on %s with %r" % (action,
url,
args))
#return failure
def callSucceeded(self, results, action, instance_id):
print "\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
else:
print 'no return values'
class ClientControl(log.Loggable):
"""
......@@ -115,3 +199,155 @@ class ClientControl(log.Loggable):
the messages sent by the device.
"""
logCategory = 'UPnT_ClientControl'
serviceActions = {}
def __init__(self, templates_dir):
if len(self.serviceActions) == 0:
template_files = [template_file for template_file in os.listdir(templates_dir + 'service/') if not template_file.startswith('.')]
ns = {'upnp': 'urn:schemas-upnp-org:service-1-0'}
for template_file in template_files:
service_template = etree.parse(templates_dir + 'service/' + template_file)
# get names of actions and if they're required or not
template_action_list = service_template.xpath('actionList/action')
tpl_actions = {}
for action_node in template_action_list:
tpl_name = action_node.xpath('name')[0].text.strip()
tpl_arguments = {}
for argument in action_node.xpath('argumentList/argument'):
tpl_arg_name = argument.xpath('name')[0].text.strip()
#self.debug('ActionName %s, ArgumentName %s' % (tpl_name, tpl_arg_name))
tpl_arg_dir = argument.xpath('direction')[0].text.strip()
tpl_arg_state_var = argument.xpath('relatedStateVariable')[0].text.strip()
tpl_arguments[tpl_arg_name] = {'direction': tpl_arg_dir, 'rel_state_var': tpl_arg_state_var}
tpl_actions[tpl_name] = {}
tpl_actions[tpl_name]['Arguments'] = tpl_arguments
tpl_actions[tpl_name]['Optional'] = (len(action_node.xpath('Optional')) == 0)
self.serviceActions[template_file[:-4]] = tpl_actions
louie.connect(self.checkControlMessage, 'UPnT.control_message', louie.Any, weak=False)
def checkControlMessage(self, command, headers, body, remotehost):
"""
Check control message received from a Control Point.
"""
#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'])
)
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']]
# )
louie.send('UPnT.infoMessage',
None,
'Control: HOST header missing (%r --> %s/%s)' % (remotehost, headers['host'], command['path'])
)
return False
# CONTENT-LENGTH header check
if not headers.has_key('content-length'):
louie.send('UPnT.infoMessage',
None,
'Control: CONTENT-LENGTH header missing (%r --> %s/%s)' % (remotehost, headers['host'], command['path'])
)
return False
elif int(headers['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'])
)
return False
# CONTENT-TYPE header check
if not headers.has_key('content-type'):
louie.send('UPnT.infoMessage',
None,
'Control: CONTENT-TYPE header missing (%r --> %s/%s)' % (remotehost, headers['host'], command['path'])
)
return False
elif not headers['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'])
)
return False
elif headers['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'):