Add pretty format to Behat output
[mdk.git] / mdk / commands / behat.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 """
5 Moodle Development Kit
6
7 Copyright (c) 2013 Frédéric Massart - FMCorz.net
8
9 This program is free software: you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation, either version 3 of the License, or
12 (at your option) any later version.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
21
22 http://github.com/FMCorz/mdk
23 """
24
25 import os
26 import urllib
27 import re
28 import logging
29 import gzip
30 from tempfile import gettempdir
31 from time import sleep
32 from ..command import Command
33 from ..tools import process, ProcessInThread, downloadProcessHook, question
34
35
36 class BehatCommand(Command):
37
38 _arguments = [
39 (
40 ['-r', '--run'],
41 {
42 'action': 'store_true',
43 'help': 'run the tests'
44 }
45 ),
46 (
47 ['-d', '--disable'],
48 {
49 'action': 'store_true',
50 'help': 'disable Behat, runs the tests first if --run has been set. Ignored from 2.7.'
51 }
52 ),
53 (
54 ['--force'],
55 {
56 'action': 'store_true',
57 'help': 'force behat re-init and reset the variables in the config file.'
58 }
59 ),
60 (
61 ['-f', '--feature'],
62 {
63 'metavar': 'path',
64 'help': 'typically a path to a feature, or an argument understood by behat (see [features]: vendor/bin/behat --help). Automatically convert path to absolute path.'
65 }
66 ),
67 (
68 ['-n', '--testname'],
69 {
70 'dest': 'testname',
71 'metavar': 'name',
72 'help': 'only execute the feature elements which match part of the given name or regex'
73 }
74 ),
75 (
76 ['-t', '--tags'],
77 {
78 'metavar': 'tags',
79 'help': 'only execute the features or scenarios with tags matching tag filter expression'
80 }
81 ),
82 (
83 ['-j', '--no-javascript'],
84 {
85 'action': 'store_true',
86 'dest': 'nojavascript',
87 'help': 'do not start Selenium and ignore Javascript (short for --tags=~@javascript). Cannot be combined with --tags or --testname.'
88 }
89 ),
90 (
91 ['-D', '--no-dump'],
92 {
93 'action': 'store_false',
94 'dest': 'faildump',
95 'help': 'use the standard command without fancy screenshots or output to a directory'
96 }
97 ),
98 (
99 ['-s', '--switch-completely'],
100 {
101 'action': 'store_true',
102 'dest': 'switchcompletely',
103 'help': 'force the switch completely setting. This will be automatically enabled for PHP < 5.4. Ignored from 2.7.'
104 }
105 ),
106 (
107 ['--selenium'],
108 {
109 'default': None,
110 'dest': 'selenium',
111 'help': 'path to the selenium standalone server to use',
112 'metavar': 'jarfile'
113 }
114 ),
115 (
116 ['--selenium-download'],
117 {
118 'action': 'store_true',
119 'dest': 'seleniumforcedl',
120 'help': 'force the download of the latest Selenium to the cache'
121 }
122 ),
123 (
124 ['--selenium-verbose'],
125 {
126 'action': 'store_true',
127 'dest': 'seleniumverbose',
128 'help': 'outputs the output from selenium in the same window'
129 }
130 ),
131 (
132 ['name'],
133 {
134 'default': None,
135 'help': 'name of the instance',
136 'metavar': 'name',
137 'nargs': '?'
138 }
139 )
140 ]
141 _description = 'Initialise Behat'
142
143 def run(self, args):
144
145 # Loading instance
146 M = self.Wp.resolve(args.name)
147 if not M:
148 raise Exception('This is not a Moodle instance')
149
150 # Check required version
151 if M.branch_compare(25, '<'):
152 raise Exception('Behat is only available from Moodle 2.5')
153
154 # Check if installed
155 if not M.get('installed'):
156 raise Exception('This instance needs to be installed first')
157
158 # Disable Behat
159 if args.disable and not args.run:
160 self.disable(M)
161 return
162
163 # No Javascript
164 nojavascript = args.nojavascript
165 if not nojavascript and not self.C.get('java') or not os.path.isfile(os.path.abspath(self.C.get('java'))):
166 nojavascript = True
167 logging.info('Disabling Javascript because Java is required to run Selenium and could not be found.')
168
169 # If not composer.phar, install Composer
170 if not os.path.isfile(os.path.join(M.get('path'), 'composer.phar')):
171 logging.info('Installing Composer')
172 cliFile = 'behat_install_composer.php'
173 cliPath = os.path.join(M.get('path'), 'behat_install_composer.php')
174 (to, headers) = urllib.urlretrieve('http://getcomposer.org/installer', cliPath)
175 if headers.dict.get('content-encoding') == 'gzip':
176 f = gzip.open(cliPath, 'r')
177 content = f.read()
178 f.close()
179 f = open(cliPath, 'w')
180 f.write(content)
181 f.close()
182 M.cli('/' + cliFile, stdout=None, stderr=None)
183 os.remove(cliPath)
184 M.cli('composer.phar', args='install --dev', stdout=None, stderr=None)
185
186 # Download selenium
187 seleniumPath = os.path.expanduser(os.path.join(self.C.get('dirs.mdk'), 'selenium.jar'))
188 if args.selenium:
189 seleniumPath = args.selenium
190 elif args.seleniumforcedl or (not nojavascript and not os.path.isfile(seleniumPath)):
191 logging.info('Attempting to find a download for Selenium')
192 url = urllib.urlopen('http://docs.seleniumhq.org/download/')
193 content = url.read()
194 selenium = re.search(r'http:[a-z0-9/._-]+selenium-server-standalone-[0-9.]+\.jar', content, re.I)
195 if selenium:
196 logging.info('Downloading Selenium from %s' % (selenium.group(0)))
197 if (logging.getLogger().level <= logging.INFO):
198 urllib.urlretrieve(selenium.group(0), seleniumPath, downloadProcessHook)
199 # Force a new line after the hook display
200 logging.info('')
201 else:
202 urllib.urlretrieve(selenium.group(0), seleniumPath)
203 else:
204 logging.warning('Could not locate Selenium server to download')
205
206 if not nojavascript and not os.path.isfile(seleniumPath):
207 raise Exception('Selenium file %s does not exist')
208
209 # Run cli
210 try:
211
212 # If Oracle, ask the user for a Behat prefix, if not set.
213 prefix = M.get('behat_prefix')
214 if M.get('dbtype') == 'oci' and (args.force or not prefix or len(prefix) > 2):
215 while not prefix or len(prefix) > 2:
216 prefix = question('What prefix would you like to use? (Oracle, max 2 chars)')
217 else:
218 prefix = None
219
220 outputDir = self.Wp.getExtraDir(M.get('identifier'), 'behat')
221 outpurUrl = self.Wp.getUrl(M.get('identifier'), extra='behat')
222
223 logging.info('Initialising Behat, please be patient!')
224 M.initBehat(switchcompletely=args.switchcompletely, force=args.force, prefix=prefix, faildumppath=outputDir)
225 logging.info('Behat ready!')
226
227 # Preparing Behat command
228 cmd = ['vendor/bin/behat']
229 if args.tags:
230 cmd.append('--tags="%s"' % (args.tags))
231
232 if args.testname:
233 cmd.append('--name="%s"' % (args.testname))
234
235 if not (args.tags or args.testname) and nojavascript:
236 cmd.append('--tags ~@javascript')
237
238 if args.faildump:
239 cmd.append('--format="progress,progress,pretty,html,failed"')
240 cmd.append('--out=",{0}/progress.txt,{0}/pretty.txt,{0}/status.html,{0}/failed.txt"'.format(outputDir))
241
242 cmd.append('--config=%s/behat/behat.yml' % (M.get('behat_dataroot')))
243
244 # Checking feature argument
245 if args.feature:
246 filepath = args.feature
247 if not filepath.startswith('/'):
248 filepath = os.path.join(M.get('path'), filepath)
249 cmd.append(filepath)
250
251 cmd = ' '.join(cmd)
252
253 phpCommand = '%s -S localhost:8000' % (self.C.get('php'))
254 seleniumCommand = None
255 if seleniumPath:
256 seleniumCommand = '%s -jar %s' % (self.C.get('java'), seleniumPath)
257
258 olderThan26 = M.branch_compare(26, '<')
259
260 if args.run:
261 logging.info('Preparing Behat testing')
262
263 # Preparing PHP Server
264 phpServer = None
265 if olderThan26 and not M.get('behat_switchcompletely'):
266 logging.info('Starting standalone PHP server')
267 kwargs = {}
268 kwargs['cwd'] = M.get('path')
269 phpServer = ProcessInThread(phpCommand, **kwargs)
270 phpServer.start()
271
272 # Launching Selenium
273 seleniumServer = None
274 if seleniumPath and not nojavascript:
275 logging.info('Starting Selenium server')
276 kwargs = {}
277 if args.seleniumverbose:
278 kwargs['stdout'] = None
279 kwargs['stderr'] = None
280 else:
281 # Logging Selenium to a temporary file, this can be useful, and also it appears
282 # that Selenium hangs when stderr is not buffered.
283 fileOutPath = os.path.join(gettempdir(), 'selenium_%s_out.log' % (M.get('identifier')))
284 fileErrPath = os.path.join(gettempdir(), 'selenium_%s_err.log' % (M.get('identifier')))
285 tmpfileOut = open(fileOutPath, 'w')
286 tmpfileErr = open(fileErrPath, 'w')
287 logging.debug('Logging Selenium output to: %s' % (fileOutPath))
288 logging.debug('Logging Selenium errors to: %s' % (fileErrPath))
289 kwargs['stdout'] = tmpfileOut
290 kwargs['stderr'] = tmpfileErr
291 seleniumServer = ProcessInThread(seleniumCommand, **kwargs)
292 seleniumServer.start()
293
294 logging.info('Running Behat tests')
295
296 # Sleep for a few seconds before starting Behat
297 if phpServer or seleniumServer:
298 launchSleep = int(self.C.get('behat.launchSleep'))
299 logging.debug('Waiting for %d seconds to allow Selenium and/or the PHP Server to start ' % (launchSleep))
300 sleep(launchSleep)
301
302 # Running the tests
303 try:
304 if args.faildump:
305 logging.info('More output can be found at:\n %s\n %s', outputDir, outpurUrl)
306 process(cmd, M.path, None, None)
307 except KeyboardInterrupt:
308 pass
309
310 # Kill the remaining processes
311 if phpServer and phpServer.is_alive():
312 phpServer.kill()
313 if seleniumServer and seleniumServer.is_alive():
314 seleniumServer.kill()
315
316 # Disable Behat
317 if args.disable:
318 self.disable(M)
319
320 else:
321 if args.faildump:
322 logging.info('More output will be accessible at:\n %s\n %s', outputDir, outpurUrl)
323 if olderThan26:
324 logging.info('Launch PHP Server (or set $CFG->behat_switchcompletely to True):\n %s' % (phpCommand))
325 if seleniumCommand:
326 logging.info('Launch Selenium (optional):\n %s' % (seleniumCommand))
327 logging.info('Launch Behat:\n %s' % (cmd))
328
329 except Exception as e:
330 raise e
331
332 def disable(self, M):
333 logging.info('Disabling Behat')
334 M.cli('admin/tool/behat/cli/util.php', '--disable')
335 M.removeConfig('behat_switchcompletely')