1#!/usr/bin/env python
2############################################################################
3# Joshua R. Boverhof, LBNL
4# See LBNLCopyright for copyright notice!
5###########################################################################
6from compiler.ast import Module
7import StringIO, copy, getopt
8import os, sys, unittest, urlparse, signal, time, warnings, subprocess
9from ConfigParser import ConfigParser, NoSectionError, NoOptionError
10from ZSI.wstools.TimeoutSocket import TimeoutError
11from ZSI.generate import commands
12
13"""Global Variables:
14    CONFIG_FILE -- configuration file
15    CONFIG_PARSER -- ConfigParser instance
16    DOCUMENT -- test section variable, specifying document style.
17    LITERAL -- test section variable, specifying literal encodings.
18    BROKE -- test section variable, specifying broken test.
19    TESTS -- test section variable, whitespace separated list of modules.
20    SECTION_CONFIGURATION -- configuration section, turn on/off debuggging.
21    TRACEFILE -- file class instance.
22    TOPDIR -- current working directory
23    MODULEDIR  -- stubs directory
24    PORT -- port of local container
25    HOST -- address of local container
26    SECTION_SERVERS -- services to be tested, values are paths to executables.
27"""
28CONFIG_FILE = 'config.txt'
29CONFIG_PARSER = ConfigParser()
30DOCUMENT = 'document'
31LITERAL = 'literal'
32BROKE = 'broke'
33TESTS = 'tests'
34SECTION_CONFIGURATION = 'configuration'
35SECTION_DISPATCH = 'dispatch'
36TRACEFILE = sys.stdout
37TOPDIR = os.getcwd()
38MODULEDIR = os.path.join(TOPDIR, 'stubs')
39SECTION_SERVERS = 'servers'
40
41CONFIG_PARSER.read(CONFIG_FILE)
42
43DEBUG = CONFIG_PARSER.getboolean(SECTION_CONFIGURATION, 'debug')
44SKIP = CONFIG_PARSER.getboolean(SECTION_CONFIGURATION, 'skip')
45TWISTED = CONFIG_PARSER.getboolean(SECTION_CONFIGURATION, 'twisted')
46LAZY = CONFIG_PARSER.getboolean(SECTION_CONFIGURATION, 'lazy')
47OUTPUT = CONFIG_PARSER.get(SECTION_CONFIGURATION, 'output') or sys.stdout
48
49if DEBUG:
50    from ZSI.wstools.logging import setBasicLoggerDEBUG
51    setBasicLoggerDEBUG()
52
53sys.path.append('%s/%s' %(os.getcwd(), 'stubs'))
54ENVIRON = copy.copy(os.environ)
55ENVIRON['PYTHONPATH'] = ENVIRON.get('PYTHONPATH', '') + ':' + MODULEDIR
56
57
58def _SimpleMain():
59    """Gets tests to run from configuration file.
60    """
61    unittest.TestProgram(defaultTest="all")
62main = _SimpleMain
63
64
65def _TwistedMain():
66    """Gets tests to run from configuration file.
67    """
68    from twisted.internet import reactor
69    reactor.callWhenRunning(_TwistedTestProgram, defaultTest="all")
70    reactor.run(installSignalHandlers=0)
71if TWISTED: main = _TwistedMain
72
73
74def _LaunchContainer(cmd):
75    '''
76    Parameters:
77        cmd -- executable, sets up a ServiceContainer or ?
78    '''
79    host = CONFIG_PARSER.get(SECTION_DISPATCH, 'host')
80    port = CONFIG_PARSER.get(SECTION_DISPATCH, 'port')
81    try:
82        process = subprocess.Popen(['python', cmd, port], env=ENVIRON)
83    except:
84        print >>sys.stderr, 'error executing: %s' %cmd
85        raise
86    time.sleep(3)
87    return process
88
89
90class _TwistedTestProgram(unittest.TestProgram):
91
92    def runTests(self):
93        from twisted.internet import reactor
94        if self.testRunner is None:
95            self.testRunner = unittest.TextTestRunner(verbosity=self.verbosity)
96
97        result = self.testRunner.run(self.test)
98        reactor.stop()
99        return result.wasSuccessful()
100
101
102
103class ConfigException(Exception):
104    """Exception thrown when configuration settings arent correct.
105    """
106    pass
107
108class TestException(Exception):
109    """Exception thrown when test case isn't correctly set up.
110    """
111    pass
112
113
114class ServiceTestCase(unittest.TestCase):
115    """Conventions for method names:
116    test_net*
117    -- network tests
118
119    test_local*
120    -- local tests
121
122    test_dispatch*
123    -- tests that use the a spawned local container
124
125    class attributes: Edit/Override these in the inheriting class as needed
126        out -- file descriptor to write output to
127        name -- configuration item, must be set in class.
128        url_section -- configuration section, maps a test module
129           name to an URL.
130        client_file_name --
131        types_file_name --
132        server_file_name --
133    """
134    out = OUTPUT
135    name = None
136    url_section = 'WSDL'
137    client_file_name = None
138    types_file_name = None
139    server_file_name = None
140
141    def __init__(self, methodName):
142        """
143        parameters:
144           methodName --
145        instance variables:
146            client_module
147            types_module
148            server_module
149            processID
150            done
151
152        """
153        self.methodName = methodName
154        self.url = None
155        self.wsdl2py_args = []
156        self.wsdl2dispatch_args = []
157        self.portkwargs = {}
158        self.client_module = self.types_module = self.server_module = None
159        self.done = False
160
161        if TWISTED:
162            self.wsdl2py_args.append('--twisted')
163
164        if LAZY:
165            self.wsdl2py_args.append('--lazy')
166
167        unittest.TestCase.__init__(self, methodName)
168
169    write = lambda self, arg: self.out.write(arg)
170
171    if sys.version_info[:2] >= (2,5):
172        _exc_info = unittest.TestCase._exc_info
173    else:
174        _exc_info = unittest.TestCase._TestCase__exc_info
175
176    def __call__(self, *args, **kwds):
177        self.run(*args, **kwds)
178
179    def run(self, result=None):
180        if result is None: result = self.defaultTestResult()
181        result.startTest(self)
182        testMethod = getattr(self, self.methodName)
183        try:
184            try:
185                self.setUp()
186            except KeyboardInterrupt:
187                raise
188            except:
189                result.addError(self, self._exc_info())
190                return
191
192            ok = False
193            try:
194                t1 = time.time()
195                pyobj = testMethod()
196                t2 = time.time()
197                ok = True
198            except self.failureException:
199                result.addFailure(self, self._exc_info())
200            except KeyboardInterrupt:
201                raise
202            except:
203                result.addError(self, self._exc_info())
204
205            try:
206                self.tearDown()
207            except KeyboardInterrupt:
208                raise
209            except:
210                result.addError(self, self._exc_info())
211                ok = False
212            if ok:
213                result.addSuccess(self)
214                print>>self
215                print>>self, "|"+"-"*60
216                print>>self, "|  TestCase: %s" %self.methodName
217                print>>self, "|"+"-"*20
218                print>>self, "|  run time:   %s ms" %((t2-t1)*1000)
219                print>>self, "|  return  :   %s" %pyobj
220                print>>self, "|"+"-"*60
221
222        finally:
223            result.stopTest(self)
224
225
226
227
228    def getPortKWArgs(self):
229        kw = {}
230        if CONFIG_PARSER.getboolean(SECTION_CONFIGURATION, 'tracefile'):
231            kw['tracefile'] = TRACEFILE
232
233        kw.update(self.portkwargs)
234        return kw
235
236    def _setUpDispatch(self):
237        """Set this test up as a dispatch test.
238        url --
239        """
240        host = CONFIG_PARSER.get(SECTION_DISPATCH, 'host')
241        port = CONFIG_PARSER.get(SECTION_DISPATCH, 'port')
242        path = CONFIG_PARSER.get(SECTION_DISPATCH, 'path')
243
244        scheme = 'http'
245        netloc = '%s:%s' %(host, port)
246        params = query = fragment = None
247
248        self.portkwargs['url'] = \
249            urlparse.urlunparse((scheme,netloc,path,params,query,fragment))
250
251    _wsdl = {}
252    def _generate(self):
253        """call the wsdl2py and wsdl2dispatch scripts and
254        automatically add the "-f" or "-u" argument.  Other args
255        can be appended via the "wsdl2py_args" and "wsdl2dispatch_args"
256        instance attributes.
257        """
258        url = self.url
259        if os.path.isfile(url):
260            url = os.path.abspath(url)
261
262        if SKIP:
263            ServiceTestCase._wsdl[url] = True
264            return
265
266        ServiceTestCase._wsdl[url] = False
267        try:
268            os.mkdir(MODULEDIR)
269        except OSError, ex:
270            pass
271
272        os.chdir(MODULEDIR)
273        if MODULEDIR not in sys.path:
274            sys.path.append(MODULEDIR)
275
276        try:
277            commands.wsdl2py([url] + self.wsdl2py_args)
278            ServiceTestCase._wsdl[url] = True
279        finally:
280            os.chdir(TOPDIR)
281
282    _process = None
283    _lastToDispatch = None
284    def setUp(self):
285        """Generate types and services modules once, then make them
286        available thru the *_module attributes if the *_file_name
287        attributes were specified.
288        """
289        section = self.url_section
290        name = self.name
291        if not section or not name:
292            raise TestException, 'section(%s) or name(%s) not defined' %(
293                section, name)
294
295        if not CONFIG_PARSER.has_section(section):
296            raise TestException,\
297                'No such section(%s) in configuration file(%s)' %(
298                self.url_section, CONFIG_FILE)
299
300        self.url = CONFIG_PARSER.get(section, name)
301
302        status = ServiceTestCase._wsdl.get(self.url)
303        if status is False:
304            self.fail('generation failed for "%s"' %self.url)
305
306        if status is None:
307            self._generate()
308
309        # Check for files
310        tfn = self.types_file_name
311        cfn = self.client_file_name
312        sfn = self.server_file_name
313
314        files = filter(lambda f: f is not None, [cfn, tfn,sfn])
315        if None is cfn is tfn is sfn:
316            return
317
318        for n,m in map(lambda i: (i,__import__(i.split('.py')[0])), files):
319            if tfn is not None and tfn == n:
320                self.types_module = m
321            elif cfn is not None and cfn == n:
322                self.client_module = m
323            elif sfn is not None and sfn == n:
324                self.server_module = m
325            else:
326                self.fail('Unexpected module %s' %n)
327
328        # DISPATCH PORTION OF SETUP
329        if not self.methodName.startswith('test_dispatch'):
330            return
331
332        self._setUpDispatch()
333        if ServiceTestCase._process is not None:
334            return
335
336        try:
337            expath = CONFIG_PARSER.get(SECTION_DISPATCH, name)
338        except (NoSectionError, NoOptionError), ex:
339            self.fail('section dispatch has no item "%s"' %name)
340
341        if ServiceTestCase._lastToDispatch == expath:
342            return
343
344        if ServiceTestCase._lastToDispatch is not None:
345           ServiceTestCase.CleanUp()
346
347        ServiceTestCase._lastToDispatch = expath
348        ServiceTestCase._process = \
349            _LaunchContainer(os.path.join(os.path.abspath(TOPDIR),
350                                                          *expath.split('/')))
351
352
353    def CleanUp(cls):
354        """call this when dispatch server is no longer needed,
355        maybe another needs to be started.  Assumption that
356        a single "Suite" uses the same server, once all the
357        tests are run in that suite do a cleanup.
358        """
359        if cls._process is None:
360            return
361        os.kill(cls._process.pid, signal.SIGKILL)
362        cls._process = None
363    CleanUp = classmethod(CleanUp)
364
365
366class ServiceTestSuite(unittest.TestSuite):
367    """A test suite is a composite test consisting of a number of TestCases.
368
369    For use, create an instance of TestSuite, then add test case instances.
370    When all tests have been added, the suite can be passed to a test
371    runner, such as TextTestRunner. It will run the individual test cases
372    in the order in which they were added, aggregating the results. When
373    subclassing, do not forget to call the base class constructor.
374    """
375    def __init__(self, tests=()):
376        unittest.TestSuite.__init__(self, tests)
377
378    def __call__(self, result):
379        # for python2.4
380        return self.run(result)
381
382    def addTest(self, test):
383        unittest.TestSuite.addTest(self, test)
384
385    def run(self, result):
386        for test in self._tests:
387            if result.shouldStop:
388                break
389            test(result)
390
391        ServiceTestCase.CleanUp()
392        return result
393
394
395