Salome HOME
Add the test command first version
[tools/sat.git] / commands / test.py
1 #!/usr/bin/env python
2 #-*- coding:utf-8 -*-
3 #  Copyright (C) 2010-2012  CEA/DEN
4 #
5 #  This library is free software; you can redistribute it and/or
6 #  modify it under the terms of the GNU Lesser General Public
7 #  License as published by the Free Software Foundation; either
8 #  version 2.1 of the License.
9 #
10 #  This library is distributed in the hope that it will be useful,
11 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 #  Lesser General Public License for more details.
14 #
15 #  You should have received a copy of the GNU Lesser General Public
16 #  License along with this library; if not, write to the Free Software
17 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
18
19 import os
20 import sys
21 import shutil
22 import subprocess
23 import datetime
24 import gzip
25
26 try:
27     from hashlib import sha1
28 except ImportError:
29     from sha import sha as sha1
30
31 import src
32 import src.ElementTree as etree
33 from src.xmlManager import add_simple_node
34
35 # Define all possible option for the test command :  sat test <options>
36 parser = src.options.Options()
37 parser.add_option('a', 'appli', 'string', 'appli',
38     _('Use this option to specify the path to an installed application.'))
39 parser.add_option('g', 'grid', 'string', 'grid',
40     _("""Indicate the name of the grid to test.
41 \tThis name has to be registered in sat. If your test base is not known by sat, use the option --dir."""))
42 parser.add_option('m', 'module', 'list', 'modules',
43     _('Indicate which module(s) to test (subdirectory of the grid).'))
44 parser.add_option('t', 'type', 'list', 'types',
45     _('Indicate which type(s) to test (subdirectory of the module).'))
46 parser.add_option('d', 'dir', 'string', 'dir',
47     _('Indicate the directory containing the test base.'), "")
48 parser.add_option('', 'mode', 'string', 'mode',
49     _("Indicate which kind of test to run. If MODE is 'batch' only python and NO_GUI tests are run."), "normal")
50 parser.add_option('', 'display', 'string', 'display',
51     _("""Set the display where to launch SALOME.
52 \tIf value is NO then option --show-desktop=0 will be used to launch SALOME."""))
53 parser.add_option('n', 'name', 'string', 'session',
54     _('Give a name to the test session (REQUIRED if no product).'))
55 parser.add_option('', 'light', 'boolean', 'light',
56     _('Only run minimal tests declared in TestsLight.txt.'), False)
57
58 def description():
59     '''method that is called when salomeTools is called with --help option.
60     
61     :return: The text to display for the test command description.
62     :rtype: str
63     '''
64     return _("The test command runs a test base on a SALOME installation.")     
65
66 def parse_option(args, config):
67     (options, args) = parser.parse_args(args)
68
69     if not options.appli:
70         options.appli = ""
71     elif not os.path.isabs(options.appli):
72         if not src.config_has_application(config):
73             raise src.SatException(_("An application is required to use a "
74                                      "relative path with option --appli"))
75         options.appli = os.path.join(config.APPLICATION.workdir, options.appli)
76
77         if not os.path.exists(options.appli):
78             raise src.SatException(_("Application not found: %s") % 
79                                    options.appli)
80
81     return (options, args)
82
83 def ask_a_path():
84     path = raw_input("enter a path where to save the result: ")
85     if path == "":
86         result = raw_input("the result will be not save. Are you sure to "
87                            "continue ? [y/n] ")
88         if result == "y":
89             return path
90         else:
91             return ask_a_path()
92
93     elif os.path.exists(path):
94         result = raw_input("Warning, the content of %s will be deleted. Are you"
95                            " sure to continue ? [y/n] " % path)
96         if result == "y":
97             return path
98         else:
99             return ask_a_path()
100     else:
101         return path
102
103 def save_file(filename, base):
104     f = open(filename, 'r')
105     content = f.read()
106     f.close()
107
108     objectname = sha1(content).hexdigest()
109
110     f = gzip.open(os.path.join(base, '.objects', objectname), 'w')
111     f.write(content)
112     f.close()
113     return objectname
114
115 def move_test_results(in_dir, what, out_dir, logger):
116     if out_dir == in_dir:
117         return
118
119     finalPath = out_dir
120     pathIsOk = False
121     while not pathIsOk:
122         try:
123             # create test results directory if necessary
124             #logger.write("FINAL = %s\n" % finalPath, 5)
125             if not os.access(finalPath, os.F_OK):
126                 #shutil.rmtree(finalPath)
127                 os.makedirs(finalPath)
128             pathIsOk = True
129         except:
130             logger.error(_("%s cannot be created.") % finalPath)
131             finalPath = ask_a_path()
132
133     if finalPath != "":
134         os.makedirs(os.path.join(finalPath, what, 'BASES'))
135
136         # check if .objects directory exists
137         if not os.access(os.path.join(finalPath, '.objects'), os.F_OK):
138             os.makedirs(os.path.join(finalPath, '.objects'))
139
140         logger.write(_('copy tests results to %s ... ') % finalPath, 3)
141         logger.flush()
142         #logger.write("\n", 5)
143
144         # copy env_info.py
145         shutil.copy2(os.path.join(in_dir, what, 'env_info.py'),
146                      os.path.join(finalPath, what, 'env_info.py'))
147
148         # for all sub directory (ie grid) in the BASES directory
149         for grid in os.listdir(os.path.join(in_dir, what, 'BASES')):
150             outgrid = os.path.join(finalPath, what, 'BASES', grid)
151             ingrid = os.path.join(in_dir, what, 'BASES', grid)
152
153             # ignore files in root dir
154             if not os.path.isdir(ingrid):
155                 continue
156
157             os.makedirs(outgrid)
158             #logger.write("  copy grid %s\n" % grid, 5)
159
160             for module_ in [m for m in os.listdir(ingrid) if os.path.isdir(
161                                                     os.path.join(ingrid, m))]:
162                 # ignore source configuration directories
163                 if module_[:4] == '.git' or module_ == 'CVS':
164                     continue
165
166                 outmodule = os.path.join(outgrid, module_)
167                 inmodule = os.path.join(ingrid, module_)
168                 os.makedirs(outmodule)
169                 #logger.write("    copy module %s\n" % module_, 5)
170
171                 if module_ == 'RESSOURCES':
172                     for file_name in os.listdir(inmodule):
173                         if not os.path.isfile(os.path.join(inmodule,
174                                                            file_name)):
175                             continue
176                         f = open(os.path.join(outmodule, file_name), "w")
177                         f.write(save_file(os.path.join(inmodule, file_name),
178                                           finalPath))
179                         f.close()
180                 else:
181                     for type_name in [t for t in os.listdir(inmodule) if 
182                                       os.path.isdir(os.path.join(inmodule, t))]:
183                         outtype = os.path.join(outmodule, type_name)
184                         intype = os.path.join(inmodule, type_name)
185                         os.makedirs(outtype)
186                         
187                         for file_name in os.listdir(intype):
188                             if not os.path.isfile(os.path.join(intype,
189                                                                file_name)):
190                                 continue
191                             if file_name.endswith('result.py'):
192                                 shutil.copy2(os.path.join(intype, file_name),
193                                              os.path.join(outtype, file_name))
194                             else:
195                                 f = open(os.path.join(outtype, file_name), "w")
196                                 f.write(save_file(os.path.join(intype,
197                                                                file_name),
198                                                   finalPath))
199                                 f.close()
200
201     logger.write(src.printcolors.printc("OK"), 3, False)
202     logger.write("\n", 3, False)
203
204 def check_remote_machine(machine_name, logger):
205     logger.write(_("\ncheck the display on %s\n" % machine_name), 4)
206     ssh_cmd = 'ssh -o "StrictHostKeyChecking no" %s "ls"' % machine_name
207     logger.write(_("Executing the command : %s " % ssh_cmd), 4)
208     p = subprocess.Popen(ssh_cmd, 
209                          shell=True,
210                          stdin =subprocess.PIPE,
211                          stdout=subprocess.PIPE,
212                          stderr=subprocess.PIPE)
213     p.wait()
214     if p.returncode != 0:
215         logger.write(src.printcolors.printc(src.KO_STATUS) + "\n", 1)
216         logger.write("    " + src.printcolors.printcError(p.stderr.read()), 2)
217         raise src.SatException("No ssh access to the display machine.")
218     else:
219         logger.write(src.printcolors.printcSuccess(src.OK_STATUS) + "\n\n", 4)
220
221 ##
222 # Transform YYYYMMDD_hhmmss into YYYY-MM-DD hh:mm:ss.
223 def parse_date(date):
224     if len(date) != 15:
225         return date
226     res = "%s-%s-%s %s:%s:%s" % (date[0:4], date[4:6], date[6:8], date[9:11], date[11:13], date[13:])
227     return res
228
229 ##
230 # Writes a report file from a XML tree.
231 def write_report(filename, xmlroot, stylesheet):
232     if not os.path.exists(os.path.dirname(filename)):
233         os.makedirs(os.path.dirname(filename))
234
235     f = open(filename, "w")
236     f.write("<?xml version='1.0' encoding='utf-8'?>\n")
237     if len(stylesheet) > 0:
238         f.write("<?xml-stylesheet type='text/xsl' href='%s'?>\n" % stylesheet)
239     f.write(etree.tostring(xmlroot, encoding='utf-8'))
240     f.close()
241
242 ##
243 # Creates the XML report for a product.
244 def create_test_report(config, dest_path, stylesheet, xmlname=""):
245     application_name = config.VARS.application
246     withappli = src.config_has_application(config)
247
248     root = etree.Element("salome")
249     prod_node = etree.Element("product", name=application_name, build=xmlname)
250     root.append(prod_node)
251
252     if withappli:
253
254         add_simple_node(prod_node, "version_to_download", config.APPLICATION.name)
255         
256         add_simple_node(prod_node, "out_dir", config.APPLICATION.workdir)
257
258     # add environment
259     exec_node = add_simple_node(prod_node, "exec")
260     exec_node.append(etree.Element("env", name="Host", value=config.VARS.node))
261     exec_node.append(etree.Element("env", name="Architecture", value=config.VARS.dist))
262     exec_node.append(etree.Element("env", name="Number of processors", value=str(config.VARS.nb_proc)))    
263     exec_node.append(etree.Element("env", name="Begin date", value=parse_date(config.VARS.datehour)))
264     exec_node.append(etree.Element("env", name="Command", value=config.VARS.command))
265     exec_node.append(etree.Element("env", name="sat version", value=config.INTERNAL.sat_version))
266
267     if 'TESTS' in config:
268         tests = add_simple_node(prod_node, "tests")
269         known_errors = add_simple_node(prod_node, "known_errors")
270         new_errors = add_simple_node(prod_node, "new_errors")
271         amend = add_simple_node(prod_node, "amend")
272         tt = {}
273         for test in config.TESTS:
274             if not tt.has_key(test.grid):
275                 tt[test.grid] = [test]
276             else:
277                 tt[test.grid].append(test)
278
279         for grid in tt.keys():
280             gn = add_simple_node(tests, "grid")
281             gn.attrib['name'] = grid
282             nb, nb_pass, nb_failed, nb_timeout, nb_not_run = 0, 0, 0, 0, 0
283             modules = {}
284             types = {}
285             for test in tt[grid]:
286                 #print test.module
287                 if not modules.has_key(test.module):
288                     mn = add_simple_node(gn, "module")
289                     mn.attrib['name'] = test.module
290                     modules[test.module] = mn
291
292                 if not types.has_key("%s/%s" % (test.module, test.type)):
293                     tyn = add_simple_node(mn, "type")
294                     tyn.attrib['name'] = test.type
295                     types["%s/%s" % (test.module, test.type)] = tyn
296
297                 for script in test.script:
298                     tn = add_simple_node(types["%s/%s" % (test.module, test.type)], "test")
299                     #tn.attrib['grid'] = test.grid
300                     #tn.attrib['module'] = test.module
301                     tn.attrib['type'] = test.type
302                     tn.attrib['script'] = script.name
303                     if 'callback' in script:
304                         try:
305                             cnode = add_simple_node(tn, "callback")
306                             if src.architecture.is_windows():
307                                 import string
308                                 cnode.text = filter(lambda x: x in string.printable,
309                                                     script.callback)
310                             else:
311                                 cnode.text = script.callback.decode('string_escape')
312                         except UnicodeDecodeError as exc:
313                             zz = script.callback[:exc.start] + '?' + script.callback[exc.end-2:]
314                             cnode = add_simple_node(tn, "callback")
315                             cnode.text = zz.decode("UTF-8")
316                     if 'amend' in script:
317                         cnode = add_simple_node(tn, "amend")
318                         cnode.text = script.amend.decode("UTF-8")
319
320                     if script.time < 0:
321                         tn.attrib['exec_time'] = "?"
322                     else:
323                         tn.attrib['exec_time'] = "%.3f" % script.time
324                     tn.attrib['res'] = script.res
325
326                     if "amend" in script:
327                         amend_test = add_simple_node(amend, "atest")
328                         amend_test.attrib['name'] = os.path.join(test.module, test.type, script.name)
329                         amend_test.attrib['reason'] = script.amend.decode("UTF-8")
330
331                     # calculate status
332                     nb += 1
333                     if script.res == src.OK_STATUS: nb_pass += 1
334                     elif script.res == src.TIMEOUT_STATUS: nb_timeout += 1
335                     elif script.res == src.KO_STATUS: nb_failed += 1
336                     else: nb_not_run += 1
337
338                     if "known_error" in script:
339                         kf_script = add_simple_node(known_errors, "error")
340                         kf_script.attrib['name'] = os.path.join(test.module, test.type, script.name)
341                         kf_script.attrib['date'] = script.known_error.date
342                         kf_script.attrib['expected'] = script.known_error.expected
343                         kf_script.attrib['comment'] = script.known_error.comment.decode("UTF-8")
344                         kf_script.attrib['fixed'] = str(script.known_error.fixed)
345                         overdue = datetime.datetime.today().strftime("%Y-%m-%d") > script.known_error.expected
346                         if overdue:
347                             kf_script.attrib['overdue'] = str(overdue)
348                         
349                     elif script.res == src.KO_STATUS:
350                         new_err = add_simple_node(new_errors, "new_error")
351                         script_path = os.path.join(test.module, test.type, script.name)
352                         new_err.attrib['name'] = script_path
353                         new_err.attrib['cmd'] = "sat testerror %s -s %s -c 'my comment' -p %s" % \
354                             (application_name, script_path, config.VARS.dist)
355
356
357             gn.attrib['total'] = str(nb)
358             gn.attrib['pass'] = str(nb_pass)
359             gn.attrib['failed'] = str(nb_failed)
360             gn.attrib['timeout'] = str(nb_timeout)
361             gn.attrib['not_run'] = str(nb_not_run)
362
363     if len(xmlname) == 0:
364         xmlname = application_name
365     if not xmlname.endswith(".xml"):
366         xmlname += ".xml"
367
368     write_report(os.path.join(dest_path, xmlname), root, stylesheet)
369     return src.OK_STATUS
370
371 def run(args, runner, logger):
372     '''method that is called when salomeTools is called with test parameter.
373     '''
374     (options, args) = parse_option(args, runner.cfg)
375
376     if options.grid and options.dir:
377         raise src.SatException(_("The options --grid and --dir are not "
378                                  "compatible!"))
379
380     with_product = False
381     if runner.cfg.VARS.application != 'None':
382         logger.write(_('Running tests on application %s\n') % 
383                             src.printcolors.printcLabel(
384                                                 runner.cfg.VARS.application), 1)
385         with_product = True
386     elif options.dir:
387         logger.write(_('Running tests from directory %s\n') % 
388                             src.printcolors.printcLabel(options.dir), 1)
389     elif not options.grid:
390         raise src.SatException(_('a grid or a directory is required'))
391
392     if with_product:
393         # check if environment is loaded
394         if 'KERNEL_ROOT_DIR' in os.environ:
395             logger.write(src.printcolors.printcWarning(_("WARNING: "
396                             "SALOME environment already sourced")) + "\n", 1)
397             
398         
399     elif options.appli:
400         logger.write(src.printcolors.printcWarning(_("Running SALOME "
401                                                 "application.")) + "\n\n", 1)
402     else:
403         logger.write(src.printcolors.printcWarning(_("WARNING running "
404                                             "without a product.")) + "\n\n", 1)
405
406         # name for session is required
407         if not options.session:
408             raise src.SatException(_("--name argument is required when no "
409                                         "product is specified."))
410
411         # check if environment is loaded
412         if not 'KERNEL_ROOT_DIR' in os.environ:
413             raise src.SatException(_("SALOME environment not found") + "\n")
414
415     # set the display
416     show_desktop = (options.display and options.display.upper() == "NO")
417     if options.display and options.display != "NO":
418         remote_name = options.display.split(':')[0]
419         if remote_name != "":
420             check_remote_machine(remote_name, logger)
421         # if explicitly set use user choice
422         os.environ['DISPLAY'] = options.display
423     elif 'DISPLAY' not in os.environ:
424         # if no display set
425         if 'display' in runner.cfg.SITE.test and len(runner.cfg.SITE.test.display) > 0:
426             # use default value for test tool
427             os.environ['DISPLAY'] = runner.cfg.SITE.test.display
428         else:
429             os.environ['DISPLAY'] = "localhost:0.0"
430
431     # initialization
432     #################
433     if with_product:
434         tmp_dir = runner.cfg.SITE.test.tmp_dir_with_product
435     else:
436         tmp_dir = runner.cfg.SITE.test.tmp_dir
437
438     # remove previous tmp dir
439     if os.access(tmp_dir, os.F_OK):
440         try:
441             shutil.rmtree(tmp_dir)
442         except:
443             logger.error(_("error removing TT_TMP_RESULT %s\n") 
444                                 % tmp_dir)
445
446     lines = []
447     lines.append("date = '%s'" % runner.cfg.VARS.date)
448     lines.append("hour = '%s'" % runner.cfg.VARS.hour)
449     lines.append("node = '%s'" % runner.cfg.VARS.node)
450     lines.append("arch = '%s'" % runner.cfg.VARS.dist)
451
452     if 'APPLICATION' in runner.cfg:
453         lines.append("application_info = {}")
454         lines.append("application_info['name'] = '%s'" % 
455                      runner.cfg.APPLICATION.name)
456         lines.append("application_info['tag'] = '%s'" % 
457                      runner.cfg.APPLICATION.tag)
458         lines.append("application_info['products'] = %s" % 
459                      str(runner.cfg.APPLICATION.products))
460
461     content = "\n".join(lines)
462
463     # create hash from session information
464     dirname = sha1(content).hexdigest()
465     session_dir = os.path.join(tmp_dir, dirname)
466     os.makedirs(session_dir)
467     os.environ['TT_TMP_RESULT'] = session_dir
468
469     # create env_info file
470     f = open(os.path.join(session_dir, 'env_info.py'), "w")
471     f.write(content)
472     f.close()
473
474     # create working dir and bases dir
475     working_dir = os.path.join(session_dir, 'WORK')
476     os.makedirs(working_dir)
477     os.makedirs(os.path.join(session_dir, 'BASES'))
478     os.chdir(working_dir)
479
480     if 'PYTHONPATH' not in os.environ:
481         os.environ['PYTHONPATH'] = ''
482     else:
483         for var in os.environ['PYTHONPATH'].split(':'):
484             if var not in sys.path:
485                 sys.path.append(var)
486
487     # launch of the tests
488     #####################
489     grid = ""
490     if options.grid:
491         grid = options.grid
492     elif not options.dir and with_product and "test_base" in runner.cfg.APPLICATION:
493         grid = runner.cfg.APPLICATION.test_base.name
494
495     src.printcolors.print_value(logger, _('Display'), os.environ['DISPLAY'], 2)
496     src.printcolors.print_value(logger, _('Timeout'),
497                                 runner.cfg.SITE.test.timeout, 2)
498     if 'timeout_app' in runner.cfg.SITE.test:
499         src.printcolors.print_value(logger, _('Timeout Salome'),
500                                     runner.cfg.SITE.test.timeout_app, 2)
501     src.printcolors.print_value(logger, _('Light mode'), options.light, 2)
502     src.printcolors.print_value(logger, _("Working dir"), session_dir, 3)
503
504     # create the test object
505     test_runner = src.test_module.Test(runner.cfg,
506                                   logger,
507                                   session_dir,
508                                   grid=grid,
509                                   modules=options.modules,
510                                   types=options.types,
511                                   appli=options.appli,
512                                   mode=options.mode,
513                                   dir_=options.dir,
514                                   show_desktop=show_desktop,
515                                   light=options.light)
516     
517     # run the test
518     logger.allowPrintLevel = False
519     retcode = test_runner.run_all_tests(options.session)
520     logger.allowPrintLevel = True
521
522     logger.write(_("Tests finished"), 1)
523     logger.write("\n", 2, False)
524     
525     logger.write(_("\nGenerate the specific test log\n"), 5)
526     out_dir = os.path.join(runner.cfg.SITE.log.log_dir, "TEST")
527     src.ensure_path_exists(out_dir)
528     name_xml_board = logger.logFileName.split(".")[0] + "board" + ".xml"
529     create_test_report(runner.cfg, out_dir, "test.xsl", xmlname = name_xml_board)
530     xml_board_path = os.path.join(out_dir, name_xml_board)
531     logger.l_logFiles.append(xml_board_path)
532     logger.add_link(os.path.join("TEST", name_xml_board),
533                     "board",
534                     retcode,
535                     "Click on the link to get the detailed test results")
536
537     return retcode
538