1 # -*- coding: utf-8 -*-
4 ## This file is part of Indico.
5 ## Copyright (C) 2002 - 2012 European Organization for Nuclear Research (CERN).
7 ## Indico is free software; you can redistribute it and/or
8 ## modify it under the terms of the GNU General Public License as
9 ## published by the Free Software Foundation; either version 3 of the
10 ## License, or (at your option) any later version.
12 ## Indico is distributed in the hope that it will be useful, but
13 ## WITHOUT ANY WARRANTY; without even the implied warranty of
14 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 ## General Public License for more details.
17 ## You should have received a copy of the GNU General Public License
18 ## along with Indico;if not, see <http://www.gnu.org/licenses/>.
21 This module defines the TestRunners that are included by default by indico.tests:
24 * FunctionalTestRunner
33 import commands
, os
, socket
, subprocess
, tempfile
, threading
, multiprocessing
40 import figleaf
.annotate_html
41 from selenium
import webdriver
44 from indico
.tests
.config
import TestConfig
45 from indico
.tests
.base
import BaseTestRunner
, Option
46 from indico
.tests
.util
import openBrowser
, relpathto
47 from indico
.tests
import default_actions
49 # legacy indico modules
50 from MaKaC
.common
import Config
, DBMgr
55 'FunctionalTestRunner',
61 JSTEST_CFG_FILE
= "builtConf.conf"
64 class CoverageBaseTestOption(Option
):
66 This class can be used in order to add an
67 optional code coverage analysis
70 def __init__(self
, value
):
71 Option
.__init
__(self
, value
)
72 self
.coverageDir
= None
74 def final_message(self
, __
):
78 self
._info
("Code coverage report generated at "
79 "%s/index.html\n" % self
.coverageDir
)
81 def shouldExecute(self
):
85 class CoveragePythonTestOption(CoverageBaseTestOption
):
90 def __init__(self
, value
):
91 CoverageBaseTestOption
.__init
__(self
, value
)
93 def pre_run(self
, __
):
100 def post_run(self
, runner
):
102 stops figleaf and returns a report
106 coverageOutput
= figleaf
.get_data().gather_files()
107 self
.coverageDir
= os
.path
.join(runner
.setupDir
, 'report', 'pycoverage')
109 # check if there's a dir first
110 if not os
.path
.exists(self
.coverageDir
):
111 os
.mkdir(self
.coverageDir
)
113 figleaf
.annotate_html
.report_as_html(coverageOutput
,
114 self
.coverageDir
, [], {})
116 # open a browser window with the report
117 openBrowser(runner
.config
.getBrowserPath(), os
.path
.join(
118 self
.coverageDir
, "index.html"))
122 class LogToConsoleTestOption(Option
):
124 Python Coverage Tests
127 def __init__(self
, value
):
128 super(LogToConsoleTestOption
, self
).__init
__(value
)
130 def pre_run(self
, __
):
136 h
= logging
.StreamHandler()
137 logger
= logging
.getLogger('')
139 logger
.setLevel(getattr(logging
, self
.value
))
141 # add multiprocessing info
142 multiprocessing
.get_logger().addHandler(h
)
145 formatter
= logging
.Formatter("[%(process)d:%(threadName)s] %(asctime)s - %(name)s - %(levelname)s - %(message)s")
146 h
.setFormatter(formatter
)
148 def shouldExecute(self
):
149 # execute it as long as it is specified
150 return not not self
.value
153 class XMLOutputOption(Option
):
155 def use_xml_output(self
, obj
, fname
, args
):
158 if not os
.path
.exists('build'):
160 args
+= ['--with-xunit', '--xunit-file=build/%s-results.xml' % fname
]
165 class NoseTestRunner(BaseTestRunner
):
167 def _buildArgs(self
):
168 args
= ['nose', '--nologcapture', '--logging-clear-handlers', \
172 self
._callOptions
('use_xml_output', 'unit', args
)
174 specific
= self
.options
.valueOf('specify')
177 args
.append(specific
)
179 args
.append(os
.path
.join(self
.setupDir
, self
._defaultPath
))
184 class UnitTestRunner(NoseTestRunner
):
191 _defaultPath
= os
.path
.join('python', 'unit')
192 _runnerOptions
= {'silent': Option
,
193 'coverage': CoveragePythonTestOption
,
195 'log': LogToConsoleTestOption
,
196 'xml': XMLOutputOption
}
198 def _buildArgs(self
):
199 args
= NoseTestRunner
._buildArgs
(self
)
201 if not self
.options
.valueOf('specify'):
202 #TODO: Make more general for functional test.
204 args
+= BaseTestRunner
.findPlugins()
208 args
= self
._buildArgs
()
209 return nose
.run(argv
= args
)
212 class FunctionalTestRunner(NoseTestRunner
):
219 _defaultPath
= os
.path
.join('python', 'functional')
220 _runnerOptions
= {'silent': Option
,
224 'server_url': Option
,
226 'xml': XMLOutputOption
}
229 def __init__(self
, **kwargs
):
230 BaseTestRunner
.__init
__(self
, **kwargs
)
233 def _runSeleniumCycle(self
):
235 Run selenium over the existing test suite (or a specific test)
237 test_config
= TestConfig
.getInstance()
239 mode
= self
.options
.valueOf('mode', test_config
.getRunMode())
241 browser
= self
.options
.valueOf('browser')
244 elif mode
== 'local':
245 browsers
= [test_config
.getStandaloneBrowser()]
247 browsers
= test_config
.getGridBrowsers()
249 args
= self
._buildArgs
()
254 os
.environ
['INDICO_TEST_MODE'] = mode
or ''
255 os
.environ
['INDICO_TEST_URL'] = self
.options
.valueOf('server_url') or ''
257 for browser
in browsers
:
258 os
.environ
['INDICO_TEST_BROWSER'] = browser
259 testResult
= nose
.run(argv
=args
)
260 result
= result
and testResult
261 self
._info
("%s: %s\n" % \
262 (browser
, testResult
and 'OK' or 'Error'))
267 if self
.options
.valueOf('record'):
268 dbi
= DBMgr
.getInstance()
271 conn
= dbi
.getDBConnection()
273 default_actions
.initialize_new_db(conn
.root())
274 default_actions
.create_dummy_user()
277 raw_input("Press [ENTER] to finish recording... ")
281 result
= self
._runSeleniumCycle
()
285 def walkThroughPluginsFolders(self
):
287 Goes throught the plugin directories, and adds
288 existing functional test dirs
290 rootPluginsPath
= os
.path
.join(self
.setupDir
, '..', 'MaKaC', 'plugins')
293 for root
, __
, ___
in os
.walk(rootPluginsPath
):
294 if root
.endswith("/tests/python/functional") > 0:
295 foldersArray
.append(root
)
300 class HTMLOption(Option
):
302 Represents the option that allows HTML reports to be generated
303 instead of console output
306 def __init__(self
, value
):
308 Option
.__init
__(self
, value
)
311 def prepare_outstream(self
, runner
):
313 Forward the output to either a file or the process output
317 __
, filePath
= tempfile
.mkstemp()
318 self
.tmpFile
= open(filePath
, 'w+b')
319 runner
.outStream
= self
.tmpFile
321 # for regular text, just use a pipe
322 runner
.outStream
= subprocess
.PIPE
324 def write_report(self
, runner
, fileName
, content
):
326 Open the browser or write an actual report, depending on the state
331 # open a browser window with the report
332 openBrowser(runner
.config
.getBrowserPath(), self
.tmpFile
.name
)
334 # for non-html output, use the normal mechanisms
335 runner
.writeNormalReport(fileName
, content
)
338 class PylintTestRunner(BaseTestRunner
):
343 _runnerOptions
= {'silent': Option
,
346 def __init__(self
, **kwargs
):
348 BaseTestRunner
.__init
__(self
, **kwargs
)
349 self
.outStream
= None
353 fileList
= self
.config
.getPylintFiles()
356 # while we have MaKaC, we have to keep this extra path
357 extraPath
= os
.path
.join(self
.setupDir
, '..')
358 os
.environ
['PYTHONPATH'] = "%s:%s" % (extraPath
,
359 os
.environ
.get('PYTHONPATH',''))
361 # Prepare the args for Pylint
362 args
= ["pylint", "--rcfile=%s" %
363 os
.path
.join(self
.setupDir
,
369 if self
.options
.valueOf('html'):
370 args
+= ['-f', 'html']
372 # will set self.outStream
373 self
._callOptions
('prepare_outstream')
375 pylintProcess
= subprocess
.Popen(
377 stdout
= self
.outStream
,
378 stderr
= subprocess
.PIPE
)
380 # for regular aoutput, redirect the out pipe to stdout
381 if not self
.options
.valueOf('html'):
382 self
._redirectPipeToStdout
(pylintProcess
.stdout
)
384 # stderr always goes to the same place
385 self
._redirectPipeToStdout
(pylintProcess
.stderr
)
387 # wait pylint to finish
391 self
._error
("[ERR] Could not start Source Analysis - "
392 "command \"pylint\" needs to be in your PATH. (%s)\n" % e
)
397 def _writeReport(self
, fileName
, content
):
399 # overloaded just to handle the case of HTML reports
400 self
._callOptions
('write_report', fileName
, content
)
402 def writeNormalReport(self
, fileName
, content
):
404 Just call the parent report writing method
405 (used from HTMLOption)
407 BaseTestRunner
._writeReport
(self
, fileName
, content
)
410 class CoverageJSTestOption(CoverageBaseTestOption
):
412 Python Coverage Tests
415 def __init__(self
, value
):
416 CoverageBaseTestOption
.__init
__(self
, value
)
418 def post_run(self
, runner
):
420 Creates a coverage report in HTML
423 #generate html for coverage
425 reportPath
= os
.path
.join(runner
.setupDir
, 'report', 'jscoverage')
426 genOutput
= commands
.getstatusoutput(
427 "genhtml -o %s %s" % (reportPath
,
428 os
.path
.join(reportPath
,
432 self
.coverageDir
= reportPath
434 if genOutput
[1].find("genhtml") > -1:
435 BaseTestRunner
._error
("JS Unit Tests - html coverage "
436 "generation failed, genhtml needs to be "
437 "in your PATH. (%s)\n" % genOutput
[1])
439 BaseTestRunner
._info
("JS Coverage - report generated\n")
440 # open a browser window with the report
441 openBrowser(runner
.config
.getBrowserPath(), os
.path
.join(
442 reportPath
, "index.html"))
445 class JSUnitTestRunner(BaseTestRunner
):
452 _runnerOptions
= {'silent': Option
,
453 'coverage': CoverageJSTestOption
,
456 def __init__(self
, **kwargs
):
457 BaseTestRunner
.__init
__(self
, **kwargs
)
458 self
.coverage
= self
.options
.valueOf('coverage')
459 self
.specify
= self
.options
.valueOf('specify')
463 #conf file used at run time
466 #Starting js-test-driver server
467 server
= subprocess
.Popen(["java", "-jar",
468 os
.path
.join(self
.setupDir
,
471 TestConfig
.getInstance().
472 getJSUnitFilename()),
477 stdout
=subprocess
.PIPE
,
478 stderr
=subprocess
.PIPE
)
481 #constructing conf file depending on installed plugins and
484 success
= self
.buildConfFile(JSTEST_CFG_FILE
, self
.coverage
)
490 #switching directory to run the tests
491 os
.chdir(os
.path
.join(self
.setupDir
, 'javascript', 'unit'))
493 #check if server is ready
495 jsDryRun
= commands
.getstatusoutput(("java -jar "
499 " --tests Fake.dryRun") %\
500 (TestConfig
.getInstance().
504 # Not very nice error checking, but how to do it nicely?
505 if "browsers" in jsDryRun
[1] or \
506 "Connection refused" in jsDryRun
[1]:
507 print "Js-test-driver server has not started yet. " \
508 "Attempt #%s\n" % (i
+1)
514 raise Exception('Could not start js unit tests because '
515 'js-test-driver server cannot be started.\n')
517 #setting tests to run
520 toTest
= self
.specify
524 command
= ("java -jar %s "
528 (TestConfig
.getInstance().getJSUnitFilename(),
533 # path relative to the jar file
534 command
+= "--testOutput %s" % \
535 os
.path
.join('..', '..', 'report', 'jscoverage')
538 jsTest
= commands
.getoutput(command
)
541 #delete built conf file
542 os
.unlink(JSTEST_CFG_FILE
)
545 os
.chdir(self
.setupDir
)
548 self
._error
("[ERR] Could not start js-test-driver server - command "
549 "\"java\" needs to be in your PATH. (%s)\n" % e
)
551 self
._error
("[ERR] Please specify a JSUnitFilename in tests.conf\n")
555 # stopping the server
560 def buildConfFile(self
, confFilePath
, coverage
):
562 Builds a driver config file
564 confTemplateDir
= os
.path
.join(self
.setupDir
,
567 confTemplatePath
= os
.path
.join(confTemplateDir
, 'confTemplate.conf')
569 absoluteTestsDir
= os
.path
.join(self
.setupDir
,
574 absolutePluginDir
= os
.path
.join(self
.setupDir
,
582 #lines needed to activate coverage plugin
583 coverageConf
= """\nplugin:
586 module: \"com.google.jstestdriver.coverage.CoverageModule\"""" % \
587 TestConfig.getInstance().getJSCoverageFilename()
589 return "Please, specify a JSCoverageFilename in tests.conf\n"
593 #retrieve and store the template file
594 f = open(confTemplatePath)
595 confTemplate = f.read()
598 #adding tests files from tests folder
599 for root, __, files in os.walk(absoluteTestsDir):
601 if name.endswith(".js"):
602 absoluteFilePath = os.path.join(root, name)
603 relativeFilePath = relpathto(confTemplateDir,
606 confTemplate += "\n - %s" % os.path.join(relativeFilePath)
609 #adding plugins test files
610 for root, __, files in os.walk(absolutePluginDir):
612 if name.endswith(".js") and \
613 root.find("/tests/javascript/unit") > 0:
614 absoluteFilePath = os.path.join(root, name)
615 relativeFilePath = relpathto(confTemplateDir,
618 confTemplate += "\n - %s" % os.path.join('..',
622 #addind coverage if necessary
624 confTemplate += coverageConf
626 #writing the complete configuration in a file
627 confFile = open(os.path.join(self.setupDir, 'javascript', 'unit',
629 confFile.write(confTemplate)
634 return "JS Unit Tests - Could not open a file. (%s)" % e
636 class JSLintTestRunner(BaseTestRunner):
643 # Folders which are not going to be scanned.
644 # Files are going to be find recursively in the other folders
645 blackList = set(['pack', 'Loader.js', 'Common', 'i18n'])
647 #checking if rhino is accessible
648 statusOutput = commands.getstatusoutput("rhino -?")
649 if statusOutput[1].find("rhino: not found") > -1:
650 return ("[ERR] Could not start JS Source Analysis - command "
651 "\"rhino\" needs to be in your PATH. (%s)\n" % statusOutput[1])
653 #constructing a list of folders to scan
656 indicoDir = os.path.join(self.setupDir, '..', '..', 'indico')
658 fileList = os.listdir(os.path.join(indicoDir,
662 for name in fileList:
663 if not (name in blackList):
664 folderNames.append(name)
666 #Scanning Indico core
667 for folderName in folderNames:
669 os.path.join(indicoDir, 'htdocs', 'js', 'indico'),
672 #Scanning plugins js files
673 return self.runJSLint(
674 os.path.join(indicoDir, 'MaKaC', 'plugins'))
677 def runJSLint(self, path, folderRestriction=''):
679 runs the actual JSLint command
682 for root, __, files in os.walk(os.path.join(self.setupDir,
686 if name.endswith(".js"):
687 filename = os.path.join(root, name)
688 self._info("Scanning %s" % filename)
689 output = commands.getstatusoutput("rhino %s %s" %