3 # Copyright (C) 2010-2012 CEA/DEN
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.
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.
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
26 # Compatibility python 2/3 for input function
27 # input stays input for python 3 and input = raw_input for python 2
36 from hashlib import sha1
38 from sha import sha as sha1
41 import src.ElementTree as etree
42 from src.xmlManager import add_simple_node
44 # Define all possible option for the test command : sat test <options>
45 parser = src.options.Options()
46 parser.add_option('b', 'base', 'string', 'base',
47 _("""Optional: The name of the test base to use."
48 This name has to be registered in your application and in a project.
49 A path to a test base can also be used."""))
50 parser.add_option('l', 'launcher', 'string', 'launcher',
51 _("""Optional: Specify the path to a SALOME launcher
52 used to launch the test scripts of the test base."""))
53 parser.add_option('g', 'grid', 'list', 'grids',
54 _('Optional: Which grid(s) to test (subdirectory of the test base).'))
55 parser.add_option('s', 'session', 'list2', 'sessions',
56 _('Optional: Which session(s) to test (subdirectory of the grid).'))
57 parser.add_option('', 'display', 'string', 'display',
58 _("""Optional: Set the display where to launch SALOME.
59 If value is NO then option --show-desktop=0 will be used to launch SALOME."""))
60 parser.add_option('', 'keep', 'boolean', 'keeptempdir',
61 _('Optional: keep temporary big tests directories.'))
63 '''method that is called when salomeTools is called with --help option.
65 :return: The text to display for the test command description.
68 return _("The test command runs a test base on a SALOME installation.\n\n"
69 "example:\nsat test SALOME-master --grid GEOM --session light")
71 def parse_option_old(args, config):
72 """ Parse the options and do some verifications about it
74 :param args List: The list of arguments of the command
75 :param config Config: The global configuration
76 :return: the options of the current command launch and the full arguments
77 :rtype: Tuple (options, args)
79 (options, args) = parser.parse_args(args)
81 if not options.launcher:
83 elif not os.path.isabs(options.launcher):
84 if not src.config_has_application(config):
85 msg = _("An application is required to use a relative path with option --appli")
86 raise src.SatException(msg)
87 options.launcher = os.path.join(config.APPLICATION.workdir,
90 if not os.path.exists(options.launcher):
91 raise src.SatException(_("Launcher not found: %s") %
94 return (options, args)
97 def parse_option(args, config):
98 """ Parse the options and do some verifications about it
100 :param args List: The list of arguments of the command
101 :param config Config: The global configuration
102 :return: the options of the current command launch and the full arguments
103 :rtype: Tuple (options, args)
105 (options, args) = parser.parse_args(args)
107 if not options.launcher:
108 options.launcher = ""
109 return (options, args)
111 if not os.path.isabs(options.launcher):
112 if not src.config_has_application(config):
113 msg = _("An application is required to use a relative path with option --appli")
114 raise src.SatException(msg)
116 options.launcher = os.path.join(config.APPLICATION.workdir, options.launcher)
117 if not os.path.exists(options.launcher):
118 raise src.SatException(_("Launcher not found: %s") % options.launcher)
121 launcher = os.path.realpath(os.path.expandvars(options.launcher))
122 if os.path.exists(launcher):
123 options.launcher = launcher
124 return (options, args)
126 raise src.SatException(_("Launcher not found: %s") % options.launcher)
132 path = input("enter a path where to save the result: ")
134 result = input("the result will be not save. Are you sure to "
141 elif os.path.exists(path):
142 result = input("Warning, the content of %s will be deleted. Are you"
143 " sure to continue ? [y/n] " % path)
151 def save_file(filename, base):
152 f = open(filename, 'r')
156 objectname = sha1(content).hexdigest()
158 f = gzip.open(os.path.join(base, '.objects', objectname), 'w')
163 def move_test_results(in_dir, what, out_dir, logger):
164 if out_dir == in_dir:
171 # create test results directory if necessary
172 #logger.write("FINAL = %s\n" % finalPath, 5)
173 if not os.access(finalPath, os.F_OK):
174 #shutil.rmtree(finalPath)
175 os.makedirs(finalPath)
178 logger.error(_("%s cannot be created.") % finalPath)
179 finalPath = ask_a_path()
182 os.makedirs(os.path.join(finalPath, what, 'BASES'))
184 # check if .objects directory exists
185 if not os.access(os.path.join(finalPath, '.objects'), os.F_OK):
186 os.makedirs(os.path.join(finalPath, '.objects'))
188 logger.write(_('copy tests results to %s ... ') % finalPath, 3)
190 #logger.write("\n", 5)
193 shutil.copy2(os.path.join(in_dir, what, 'env_info.py'),
194 os.path.join(finalPath, what, 'env_info.py'))
196 # for all sub directory (ie testbase) in the BASES directory
197 for testbase in os.listdir(os.path.join(in_dir, what, 'BASES')):
198 outtestbase = os.path.join(finalPath, what, 'BASES', testbase)
199 intestbase = os.path.join(in_dir, what, 'BASES', testbase)
201 # ignore files in root dir
202 if not os.path.isdir(intestbase):
205 os.makedirs(outtestbase)
206 #logger.write(" copy testbase %s\n" % testbase, 5)
208 for grid_ in [m for m in os.listdir(intestbase) \
209 if os.path.isdir(os.path.join(intestbase, m))]:
210 # ignore source configuration directories
211 if grid_[:4] == '.git' or grid_ == 'CVS':
214 outgrid = os.path.join(outtestbase, grid_)
215 ingrid = os.path.join(intestbase, grid_)
217 #logger.write(" copy grid %s\n" % grid_, 5)
219 if grid_ == 'RESSOURCES':
220 for file_name in os.listdir(ingrid):
221 if not os.path.isfile(os.path.join(ingrid,
224 f = open(os.path.join(outgrid, file_name), "w")
225 f.write(save_file(os.path.join(ingrid, file_name),
229 for session_name in [t for t in os.listdir(ingrid) if
230 os.path.isdir(os.path.join(ingrid, t))]:
231 outsession = os.path.join(outgrid, session_name)
232 insession = os.path.join(ingrid, session_name)
233 os.makedirs(outsession)
235 for file_name in os.listdir(insession):
236 if not os.path.isfile(os.path.join(insession,
239 if file_name.endswith('result.py'):
240 shutil.copy2(os.path.join(insession, file_name),
241 os.path.join(outsession, file_name))
243 f = open(os.path.join(outsession, file_name), "w")
244 f.write(save_file(os.path.join(insession,
249 logger.write(src.printcolors.printc("OK"), 3, False)
250 logger.write("\n", 3, False)
252 def check_remote_machine(machine_name, logger):
253 logger.write(_("\ncheck the display on %s\n" % machine_name), 4)
254 ssh_cmd = 'ssh -o "StrictHostKeyChecking no" %s "ls"' % machine_name
255 logger.write(_("Executing the command : %s " % ssh_cmd), 4)
256 p = subprocess.Popen(ssh_cmd,
258 stdin =subprocess.PIPE,
259 stdout=subprocess.PIPE,
260 stderr=subprocess.PIPE)
262 if p.returncode != 0:
263 logger.write(src.printcolors.printc(src.KO_STATUS) + "\n", 1)
264 logger.write(" " + src.printcolors.printcError(p.stderr.read()), 2)
265 logger.write(src.printcolors.printcWarning((
266 "No ssh access to the display machine.")),1)
268 logger.write(src.printcolors.printcSuccess(src.OK_STATUS) + "\n\n", 4)
270 def findOrCreateNode(parentNode, nameNodeToFind):
271 found = parentNode.find(nameNodeToFind)
273 created = add_simple_node(parentNode, nameNodeToFind)
278 def purgeEmptyNodes(root):
280 recursive remove node.text and node.tail if empty node
281 as nothing else than whitespace(s) and RCLF(s)
284 | 1) pretty print file xml -> creates indentation(s) in text and tail
285 | 2) and reload parse file xml
287 # print("root", root.tag, root.text)
291 if text.replace(" ", "").replace("\n", "") == "":
292 # print("purgeEmptyNodes text %s" % root.tag)
295 if tail.replace(" ", "").replace("\n", "") == "":
296 # print("purgeEmptyNodes tail %s" % root.tag)
299 purgeEmptyNodes(node)
303 # Creates the XML report for a product.
304 def create_test_report(config,
309 # get the date and hour of the launching of the command, in order to keep
311 date_hour = config.VARS.datehour
313 # Get some information to put in the xml file
314 application_name = config.VARS.application
315 withappli = src.config_has_application(config)
318 if not os.path.exists(xml_history_path):
319 print("Log file creation %s" % xml_history_path)
321 root = etree.Element("salome")
322 prod_node = etree.Element("product", name=application_name, build=xmlname)
323 root.append(prod_node)
325 print("Log file modification %s" % xml_history_path)
326 root = etree.parse(xml_history_path).getroot()
327 purgeEmptyNodes(root)
328 prod_node = root.find("product")
331 prod_node.attrib["history_file"] = os.path.basename(xml_history_path)
332 prod_node.attrib["global_res"] = str(retcode)
336 for node in (prod_node.findall("version_to_download") +
337 prod_node.findall("out_dir")):
338 prod_node.remove(node)
340 add_simple_node(prod_node, "version_to_download", config.APPLICATION.name)
341 add_simple_node(prod_node, "out_dir", config.APPLICATION.workdir)
345 for node in prod_node.findall("exec"):
346 prod_node.remove(node)
348 exec_node = add_simple_node(prod_node, "exec")
349 exec_node.append(etree.Element("env", name="Host", value=config.VARS.node))
350 exec_node.append(etree.Element("env", name="Architecture", value=config.VARS.dist))
351 exec_node.append(etree.Element("env", name="Number of processors", value=str(config.VARS.nb_proc)))
352 exec_node.append(etree.Element("env", name="Begin date", value=src.parse_date(date_hour)))
353 exec_node.append(etree.Element("env", name="Command", value=config.VARS.command))
354 exec_node.append(etree.Element("env", name="sat version", value=config.INTERNAL.sat_version))
356 if 'TESTS' in config:
357 tests = findOrCreateNode(prod_node, "tests")
358 known_errors = findOrCreateNode(prod_node, "known_errors")
359 new_errors = findOrCreateNode(prod_node, "new_errors")
360 amend = findOrCreateNode(prod_node, "amend")
363 for test in config.TESTS:
364 if not test.testbase in tt:
365 tt[test.testbase] = [test]
367 tt[test.testbase].append(test)
369 for testbase in tt.keys():
370 if verbose: print("---- create_test_report %s %s" % (testbase, first_time))
371 gn = findOrCreateNode(tests, "testbase")
373 # initialize all grids and session to "not executed"
374 for mn in gn.findall("grid"):
375 mn.attrib["executed_last_time"] = "no"
376 for tyn in mn.findall("session"):
377 tyn.attrib["executed_last_time"] = "no"
378 for test_node in tyn.findall('test'):
379 for node in test_node.getchildren():
380 if node.tag != "history":
381 test_node.remove(node)
384 for attribute in test_node.attrib:
385 if (attribute != "script" and
387 attribs_to_pop.append(attribute)
388 for attribute in attribs_to_pop:
389 test_node.attrib.pop(attribute)
391 gn.attrib['name'] = testbase
392 nb, nb_pass, nb_failed, nb_timeout, nb_not_run = 0, 0, 0, 0, 0
395 for test in tt[testbase]:
396 if not (test.grid in grids):
398 mn = add_simple_node(gn, "grid")
399 mn.attrib['name'] = test.grid
401 l_mn = gn.findall("grid")
403 for grid_node in l_mn:
404 if grid_node.attrib['name'] == test.grid:
408 mn = add_simple_node(gn, "grid")
409 mn.attrib['name'] = test.grid
411 grids[test.grid] = mn
413 mn.attrib["executed_last_time"] = "yes"
415 if not "%s/%s" % (test.grid, test.session) in sessions:
417 tyn = add_simple_node(mn, "session")
418 tyn.attrib['name'] = test.session
420 l_tyn = mn.findall("session")
422 for session_node in l_tyn:
423 if session_node.attrib['name'] == test.session:
427 tyn = add_simple_node(mn, "session")
428 tyn.attrib['name'] = test.session
430 sessions["%s/%s" % (test.grid, test.session)] = tyn
432 tyn.attrib["executed_last_time"] = "yes"
434 for script in test.script:
436 tn = add_simple_node(sessions[
437 "%s/%s" % (test.grid, test.session)],
439 tn.attrib['session'] = test.session
440 tn.attrib['script'] = script.name
441 hn = add_simple_node(tn, "history")
443 l_tn = sessions["%s/%s" % (test.grid, test.session)].findall(
446 for test_node in l_tn:
447 if test_node.attrib['script'] == script['name']:
452 tn = add_simple_node(sessions[
453 "%s/%s" % (test.grid, test.session)],
455 tn.attrib['session'] = test.session
456 tn.attrib['script'] = script.name
457 hn = add_simple_node(tn, "history")
459 # Get or create the history node for the current test
460 if len(tn.findall("history")) == 0:
461 hn = add_simple_node(tn, "history")
463 hn = tn.find("history")
464 # Put the last test data into the history
465 if 'res' in tn.attrib:
466 attributes = {"date_hour" : date_hour,
467 "res" : tn.attrib['res'] }
472 if node.tag != "history":
475 if 'callback' in script:
477 cnode = add_simple_node(tn, "callback")
478 if True: # bug xml mal forme colorisation src.architecture.is_windows():
481 lambda x: x in string.printable,
484 cnode.text = script.callback.decode(
486 except UnicodeDecodeError as exc:
487 zz = (script.callback[:exc.start] +
489 script.callback[exc.end-2:])
490 cnode = add_simple_node(tn, "callback")
491 cnode.text = zz.decode("UTF-8")
493 # Add the script content
494 cnode = add_simple_node(tn, "content")
495 cnode.text = script.content
497 # Add the script execution log
498 cnode = add_simple_node(tn, "out")
499 cnode.text = script.out
501 if 'amend' in script:
502 cnode = add_simple_node(tn, "amend")
503 cnode.text = script.amend.decode("UTF-8")
506 tn.attrib['exec_time'] = "?"
508 tn.attrib['exec_time'] = "%.3f" % script.time
509 tn.attrib['res'] = script.res
511 if "amend" in script:
512 amend_test = add_simple_node(amend, "atest")
513 amend_test.attrib['name'] = os.path.join(test.grid,
516 amend_test.attrib['reason'] = script.amend.decode(
521 if script.res == src.OK_STATUS: nb_pass += 1
522 elif script.res == src.TIMEOUT_STATUS: nb_timeout += 1
523 elif script.res == src.KO_STATUS: nb_failed += 1
524 else: nb_not_run += 1
526 if "known_error" in script:
527 kf_script = add_simple_node(known_errors, "error")
528 kf_script.attrib['name'] = os.path.join(test.grid,
531 kf_script.attrib['date'] = script.known_error.date
533 'expected'] = script.known_error.expected
535 'comment'] = script.known_error.comment.decode("UTF-8")
536 kf_script.attrib['fixed'] = str(
537 script.known_error.fixed)
538 overdue = datetime.datetime.today().strftime("%Y-%m-"
539 "%d") > script.known_error.expected
541 kf_script.attrib['overdue'] = str(overdue)
543 elif script.res == src.KO_STATUS:
544 new_err = add_simple_node(new_errors, "new_error")
545 script_path = os.path.join(test.grid,
546 test.session, script.name)
547 new_err.attrib['name'] = script_path
548 new_err.attrib['cmd'] = ("sat testerror %s -s %s -c 'my"
549 " comment' -p %s" % \
550 (application_name, script_path, config.VARS.dist))
553 gn.attrib['total'] = str(nb)
554 gn.attrib['pass'] = str(nb_pass)
555 gn.attrib['failed'] = str(nb_failed)
556 gn.attrib['timeout'] = str(nb_timeout)
557 gn.attrib['not_run'] = str(nb_not_run)
559 # Remove the res attribute of all tests that were not launched
561 for mn in gn.findall("grid"):
562 if mn.attrib["executed_last_time"] == "no":
563 for tyn in mn.findall("session"):
564 if tyn.attrib["executed_last_time"] == "no":
565 for test_node in tyn.findall('test'):
566 if "res" in test_node.attrib:
567 test_node.attrib.pop("res")
569 if len(xmlname) == 0:
570 xmlname = application_name
571 if not xmlname.endswith(".xml"):
574 src.xmlManager.write_report(os.path.join(dest_path, xmlname), root, "test.xsl")
575 src.xmlManager.write_report(xml_history_path, root, "test_history.xsl")
578 def generate_history_xml_path(config, test_base):
579 """Generate the name of the xml file that contain the history of the tests
580 on the machine with the current APPLICATION and the current test base.
582 :param config Config: The global configuration
583 :param test_base Str: The test base name (or path)
584 :return: the full path of the history xml file
587 history_xml_name = ""
588 if "APPLICATION" in config:
589 history_xml_name += config.APPLICATION.name
590 history_xml_name += "-"
591 history_xml_name += config.VARS.dist
592 history_xml_name += "-"
593 test_base_name = test_base
594 if os.path.exists(test_base):
595 test_base_name = os.path.basename(test_base)
596 history_xml_name += test_base_name
597 history_xml_name += ".xml"
598 log_dir = src.get_log_path(config)
599 return os.path.join(log_dir, "TEST", history_xml_name)
601 def run(args, runner, logger):
602 '''method that is called when salomeTools is called with test parameter.
604 (options, args) = parse_option(args, runner.cfg)
606 # the test base is specified either by the application, or by the --base option
607 with_application = False
608 if runner.cfg.VARS.application != 'None':
609 logger.write(_('Running tests on application %s\n') %
610 src.printcolors.printcLabel(
611 runner.cfg.VARS.application), 1)
612 with_application = True
613 elif not options.base:
614 raise src.SatException(_('A test base is required. Use the --base '
617 # the launcher is specified either by the application, or by the --launcher option
619 # check if environment is loaded
620 if 'KERNEL_ROOT_DIR' in os.environ:
621 logger.write(src.printcolors.printcWarning(_("WARNING: "
622 "SALOME environment already sourced")) + "\n", 1)
625 elif options.launcher:
626 logger.write(src.printcolors.printcWarning(_("Running SALOME "
627 "application.")) + "\n\n", 1)
629 msg = _("Impossible to find any launcher.\nPlease specify an "
630 "application or a launcher")
631 logger.write(src.printcolors.printcError(msg))
636 show_desktop = (options.display and options.display.upper() == "NO")
637 if options.display and options.display != "NO":
638 remote_name = options.display.split(':')[0]
639 if remote_name != "" and (not src.architecture.is_windows()):
640 check_remote_machine(remote_name, logger)
641 # if explicitly set use user choice
642 os.environ['DISPLAY'] = options.display
643 elif 'DISPLAY' not in os.environ:
645 if ('test' in runner.cfg.LOCAL and
646 'display' in runner.cfg.LOCAL.test and
647 len(runner.cfg.LOCAL.test.display) > 0):
648 # use default value for test tool
649 os.environ['DISPLAY'] = runner.cfg.LOCAL.test.display
651 os.environ['DISPLAY'] = "localhost:0.0"
656 tmp_dir = os.path.join(runner.cfg.VARS.tmp_root,
657 runner.cfg.APPLICATION.name,
660 tmp_dir = os.path.join(runner.cfg.VARS.tmp_root,
663 # remove previous tmp dir
664 if os.access(tmp_dir, os.F_OK):
666 shutil.rmtree(tmp_dir)
668 logger.error(_("error removing TT_TMP_RESULT %s\n")
672 lines.append("date = '%s'" % runner.cfg.VARS.date)
673 lines.append("hour = '%s'" % runner.cfg.VARS.hour)
674 lines.append("node = '%s'" % runner.cfg.VARS.node)
675 lines.append("arch = '%s'" % runner.cfg.VARS.dist)
677 if 'APPLICATION' in runner.cfg:
678 lines.append("application_info = {}")
679 lines.append("application_info['name'] = '%s'" %
680 runner.cfg.APPLICATION.name)
681 lines.append("application_info['tag'] = '%s'" %
682 runner.cfg.APPLICATION.tag)
683 lines.append("application_info['products'] = %s" %
684 str(runner.cfg.APPLICATION.products))
686 content = "\n".join(lines)
688 # create hash from context information
689 # CVW TODO or not dirname = datetime.datetime.now().strftime("%y%m%d_%H%M%S_") + sha1(content.encode()).hexdigest()[0:8]
690 dirname = sha1(content.encode()).hexdigest()[0:8] # only 8 firsts probably good
691 base_dir = os.path.join(tmp_dir, dirname)
692 os.makedirs(base_dir)
693 os.environ['TT_TMP_RESULT'] = base_dir
695 # create env_info file
696 with open(os.path.join(base_dir, 'env_info.py'), "w") as f:
699 # create working dir and bases dir
700 working_dir = os.path.join(base_dir, 'WORK')
701 os.makedirs(working_dir)
702 os.makedirs(os.path.join(base_dir, 'BASES'))
703 os.chdir(working_dir)
705 if 'PYTHONPATH' not in os.environ:
706 os.environ['PYTHONPATH'] = ''
708 for var in os.environ['PYTHONPATH'].split(':'):
709 if var not in sys.path:
712 # launch of the tests
713 #####################
716 test_base = options.base
717 elif with_application and "test_base" in runner.cfg.APPLICATION:
718 test_base = runner.cfg.APPLICATION.test_base.name
720 src.printcolors.print_value(logger, _('Display'), os.environ['DISPLAY'], 2)
721 src.printcolors.print_value(logger, _('Timeout'),
722 src.test_module.DEFAULT_TIMEOUT, 2)
723 src.printcolors.print_value(logger, _("Working dir"), base_dir, 3)
725 # create the test object
726 test_runner = src.test_module.Test(runner.cfg,
731 sessions=options.sessions,
732 launcher=options.launcher,
733 show_desktop=show_desktop)
735 if not test_runner.test_base_found:
740 logger.allowPrintLevel = False
741 retcode = test_runner.run_all_tests()
742 logger.allowPrintLevel = True
744 logger.write(_("Tests finished"), 1)
745 logger.write("\n", 2, False)
747 logger.write(_("\nGenerate the specific test log\n"), 5)
748 log_dir = src.get_log_path(runner.cfg)
749 out_dir = os.path.join(log_dir, "TEST")
750 src.ensure_path_exists(out_dir)
751 name_xml_board = logger.logFileName.split(".")[0] + "_board.xml"
752 historic_xml_path = generate_history_xml_path(runner.cfg, test_base)
754 create_test_report(runner.cfg,
758 xmlname = name_xml_board)
759 xml_board_path = os.path.join(out_dir, name_xml_board)
761 logger.l_logFiles.append(xml_board_path)
762 logger.add_link(os.path.join("TEST", name_xml_board),
765 "Click on the link to get the detailed test results")
766 logger.write("\nTests board file %s\n" % xml_board_path, 1)
768 # Add the historic files into the log files list of the command
769 logger.l_logFiles.append(historic_xml_path)
771 if not options.keeptempdir:
772 logger.write("Removing the temporary directory: rm -rf %s\n" % test_runner.tmp_working_dir, 5)
773 if os.path.exists(test_runner.tmp_working_dir):
774 shutil.rmtree(test_runner.tmp_working_dir)
776 logger.write("NOT Removing the temporary directory: rm -rf %s\n" % test_runner.tmp_working_dir, 5)