result.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 
00003 # Software License Agreement (BSD License)
00004 #
00005 # Copyright (c) 2009, Willow Garage, Inc.
00006 # All rights reserved.
00007 #
00008 # Redistribution and use in source and binary forms, with or without
00009 # modification, are permitted provided that the following conditions
00010 # are met:
00011 #
00012 #  * Redistributions of source code must retain the above copyright
00013 #    notice, this list of conditions and the following disclaimer.
00014 #  * Redistributions in binary form must reproduce the above
00015 #    copyright notice, this list of conditions and the following
00016 #    disclaimer in the documentation and/or other materials provided
00017 #    with the distribution.
00018 #  * Neither the name of the Willow Garage nor the names of its
00019 #    contributors may be used to endorse or promote products derived
00020 #    from this software without specific prior written permission.
00021 #
00022 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00023 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00024 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00025 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00026 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00027 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00028 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00029 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00030 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00031 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00032 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00033 # POSSIBILITY OF SUCH DAMAGE.
00034 
00035 ##\author Kevin Watts
00036 ##\brief Stores, processes results of qualification tests
00037 
00038 PKG = 'qualification'
00039 import roslib
00040 roslib.load_manifest(PKG)
00041 
00042 import sys, os
00043 
00044 from qualification.test import *
00045 
00046 import time
00047 from time import strftime
00048 from PIL import Image
00049 from cStringIO import StringIO
00050 
00051 import tarfile, tempfile
00052 import socket
00053 
00054 import smtplib
00055 from email.mime.multipart import MIMEMultipart
00056 from email.mime.text import MIMEText
00057 from email.mime.base import MIMEBase
00058 from email import Encoders
00059 
00060 import wg_invent_client
00061 
00062 from datetime import datetime
00063 
00064 import shutil
00065 
00066 import result_dir
00067 TEMP_DIR = result_dir.TEMP_DIR
00068 RESULTS_DIR = result_dir.RESULTS_DIR
00069 
00070 ##\todo Rename to '_' name
00071 def write_temp_tar_file(results_dir):
00072     temp_tar_file = tempfile.NamedTemporaryFile()
00073 
00074     tar_st = tarfile.open(temp_tar_file.name, 'w:')
00075     for filename in os.listdir(results_dir):
00076         tar_st.add(os.path.join(results_dir, filename), arcname=filename)
00077 
00078     tar_st.close()
00079     
00080     return temp_tar_file
00081 
00082 ##\brief Holds results from pre-startup and shutdown scripts
00083 class TestScriptResult(object):
00084     ##@param test_script is test/TestScript that we tested
00085     def __init__(self, test_script, srv_result):
00086         self.name = test_script.get_name()
00087         self.launch = test_script.launch_file
00088 
00089         self._result = srv_result.result
00090         self.msg = srv_result.failure_msg
00091         
00092     def get_name(self):
00093         return self.name
00094 
00095     def get_launch(self):
00096         return os.path.basename(self.launch)
00097 
00098     def get_pass_bool(self):
00099         return self._result == 0 # ScriptDoneRequest.RESULT_OK
00100 
00101     def has_error(self):
00102         return self._result == 2 # ScriptDoneRequest.RESULT_ERROR
00103 
00104     def get_result_msg(self):
00105         result_dict = { 0: "OK", 1: "Fail", 2: "Error"}
00106         return result_dict[self._result]
00107     
00108     def get_msg(self):
00109         return self.msg
00110 
00111 ##\brief Holds pass/fail status of all subtests.
00112 ##
00113 ## Stores results of subtests in any case. Completely encapsulates
00114 ## numeric values of result types from users. 
00115 class SubTestResultType(object):
00116     __result_types = { 
00117         0: "Pass", 
00118         1: "Fail", 
00119         2: "Human required", 
00120         3: "Manual Failure", 
00121         4: "Manual Pass", 
00122         5: "Retry", 
00123         # Not currently used
00124         6: "Error" , 
00125         7: "Canceled" }
00126 
00127     ##\param result TestResultRequest.result : 0 - Pass, 1 - Fail, 2 - Human req'd
00128     def __init__(self, result):
00129         self._result = result
00130 
00131     def cancel(self):
00132         self._result = 7
00133 
00134     def retry(self):
00135         self._result = 5
00136 
00137     def manual_pass(self):
00138         # Record as pass if auto-pass
00139         if self._result == 0:
00140             return
00141         self._result = 4
00142 
00143     def manual_fail(self):
00144         # Record as failure if auto-fail
00145         if self._result == 1:
00146             return
00147         self._result = 3
00148 
00149     def error(self):
00150         self._result = 6
00151 
00152     def get_msg(self):
00153         return self.__result_types[self._result]
00154 
00155     def get_html_msg(self):
00156         if self._result == 0:
00157             status_html ='<div class="pass">%s</div>' % self.get_msg()
00158         elif self._result == 3 or self._result == 2 or self._result == 5:
00159             status_html ='<div class="warn">%s</div>' % self.get_msg()
00160         else:
00161             status_html ='<div class="error">%s</div>' % self.get_msg()
00162 
00163         return status_html
00164 
00165     def get_pass_bool(self, manual_ok = True):
00166         if manual_ok:
00167             return self._result == 0 or self._result == 4
00168         return self._result == 0
00169 
00170     def is_human_required(self):
00171         return self._result == 2
00172 
00173     def is_manual(self):
00174         return self._result == 3 or self._result == 4
00175     
00176     def is_retry(self):
00177         return self._result == 5
00178 
00179     def is_error(self):
00180         return self._result == 6
00181 
00182     def is_cancel(self):
00183         return self._result == 7
00184 
00185 ##\brief Stores and displays result from qualification subtest
00186 ##
00187 ##
00188 ##\todo Make unit test of this class. Test should try getting subtest, TestResultRequest
00189 ## and writing images, displaying results.
00190 class SubTestResult(object):
00191     ##\param subtest test/SubTest : Subtest that completed
00192     ##\param msg srv/TestResultRequest : Received msg from analysis
00193     def __init__(self, subtest, msg):
00194         self._subtest = subtest
00195         
00196         self._result = SubTestResultType(msg.result)
00197 
00198         self._text_result = msg.html_result
00199         if msg.text_summary is not None:
00200             self._summary = msg.text_summary
00201         else:
00202             self._summary = ''
00203 
00204         self._subresult_note = ''
00205         
00206         self._plots = []
00207         if msg.plots is not None:
00208             for plt in msg.plots:
00209                 self._plots.append(plt)
00210 
00211         self._retry_suffix = ''
00212         self._retry_name = ''
00213 
00214         self.write_images()
00215 
00216         self._params = msg.params
00217         self._values = msg.values
00218 
00219         self._temp_image_files = []
00220 
00221     def export_data(self):
00222         data = wg_invent_client.SubtestData(self.get_name(), self.get_result())
00223         data.set_note(self.get_note())
00224         for p in self.get_params():
00225             data.set_parameter(p.key, p.value)
00226         for m in self.get_values():
00227             data.set_measurement(m.key, m.value, m.min, m.max)
00228 
00229         return data
00230 
00231     def close(self):
00232         for file in self._temp_image_files:
00233             file.close()
00234 
00235     def get_result(self):
00236         return self._result.get_msg()
00237 
00238     def get_params(self):
00239         return self._params
00240 
00241     def get_values(self):
00242         return self._values
00243 
00244     def get_plots(self):
00245         return self._plots
00246 
00247     def retry_test(self, count, notes):
00248         self.set_note(notes)
00249 
00250         self._result.retry()
00251         self._retry_suffix = "_retry%d" % count
00252         self._retry_name = " Retry %d" % count
00253 
00254         self.write_images()
00255 
00256     def set_operator_result(self, pass_bool):
00257         if pass_bool:
00258             self._result.manual_pass()
00259         else:
00260             self._result.manual_fail()
00261 
00262     def get_name(self):
00263         return self._subtest.get_name()
00264 
00265     def get_pass_bool(self):
00266         return self._result.get_pass_bool()
00267 
00268     def set_note(self, txt):
00269         self._subresult_note = txt
00270 
00271     def get_note(self):
00272         return self._subresult_note
00273 
00274     def filename_base(self):
00275         return self._subtest.get_name().replace(' ', '_').replace('/', '__') + str(self._subtest.get_key()) + self._retry_suffix
00276 
00277     def filename(self):
00278         return self.filename_base() + '/index.html'
00279     
00280     ##\brief Save images to file in designated folder
00281     ##
00282     ##\param path str : Filepath of all images
00283     def write_images(self, path = None):
00284         if not path:
00285             path = TEMP_DIR
00286 
00287         dir_name = os.path.join(path, self.filename_base())
00288         if not os.path.isdir(dir_name):
00289             os.makedirs(dir_name)
00290 
00291         for plot in self._plots:
00292             img_file = os.path.join(dir_name, plot.title + '.' + plot.image_format)
00293             open(img_file, 'w').write(plot.image)
00294 
00295     def html_image_result(self, img_path):
00296         html = '<H4 ALIGN=CENTER>Result Details</H4>'
00297         
00298         # Users must put '<img src=\"IMG_PATH/%s.png\" /> % image_title' in html_result
00299         html += self._text_result.replace('IMG_PATH', os.path.join(img_path, self.filename_base()))
00300         
00301         return html
00302 
00303     ## Takes test title, status, summary, details and images and makes a 
00304     ## readable and complete results page.
00305     ##\todo Append strings to add, do parse test on output
00306     def make_result_page(self, back_link = False, link_dir = TEMP_DIR, prev = None, next = None):
00307         html = "<html><head><title>Qualification Test Results: %s</title>\
00308 <style type=\"text/css\">\
00309 body { color: black; background: white; }\
00310 div.pass { background: green; padding: 0.5em; border: none; }\
00311 div.warn { background: yellow; padding: 0.5em; border: none; }\
00312 div.error { background: red; padding: 0.5em; border: none; }\
00313 strong { font-weight: bold; color: red; }\
00314 em { font-style:normal; font-weight: bold; }\
00315 </style>\
00316 </head><body>\n" % self._subtest.get_name()
00317 
00318         html += self.html_header()
00319         
00320         html += '<hr size="3">\n'
00321         
00322         # Parses through text and adds image source to file
00323         html += self.html_image_result(link_dir)
00324 
00325         html += '<hr size="3">\n'
00326         html += self._html_test_params()
00327         html += '<hr size="3">\n'
00328         html += self._html_test_values()
00329         html += '<hr size="3">\n'
00330         html += self._html_test_info()
00331         html += '<hr size="3">\n'        
00332 
00333         # Add link back to index if applicable
00334         # May want to make page "portable" so whole folder can move
00335         if back_link:
00336             if prev:
00337                 prev_file = os.path.join(link_dir, prev.filename())
00338                 html += '<p align=center><a href="%s">Previous: %s</a></p>\n' % (prev_file, prev.get_name())
00339             
00340             back_index_file = os.path.join(link_dir, 'index.html')
00341             html += '<p align=center><a href="%s">Back to Index</a></p>\n' % back_index_file
00342 
00343             if next:
00344                 next_file = os.path.join(link_dir, next.filename())
00345                 html += '<p align=center><a href="%s">Next: %s</a></p>\n' % (next_file, next.get_name())
00346            
00347 
00348         html += '</body></html>'
00349         
00350         return html 
00351 
00352     def _html_test_params(self):
00353         html = ['<H4 align=center>Subtest Parameters</H4>']
00354         
00355         if len(self._params) == 0:
00356             html.append('<p>No subtest parameters.</p>')
00357         else:
00358             html.append('<table border="1" cellpadding="2" cellspacing="0">')
00359             html.append('<tr><td><b>Parameter</b></td><td><b>Value</b></td></tr>')
00360             for param in self._params:
00361                 html.append('<tr><td>%s</td><td>%s</td></tr>' % (param.key, param.value))
00362             html.append('</table>')
00363         
00364         return '\n'.join(html)
00365 
00366     def _html_test_values(self):
00367         html = ['<H4 align=center>Subtest Measurements</H4>']
00368         
00369         if len(self._values) == 0:
00370             html.append('<p>No subtest values.</p>')
00371         else:
00372             html.append('<table border="1" cellpadding="2" cellspacing="0">')
00373             html.append('<tr><td><b>Measurement</b></td><td><b>Value</b></td><td><b>Min</b></td><td><b>Max</b></td></tr>')
00374             for value in self._values:
00375                 html.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>' % (value.key, value.value, value.min, value.max))
00376             html.append('</table>')
00377 
00378         return '\n'.join(html)
00379 
00380 
00381     def _html_test_info(self):
00382         html = ['<H4 align=center>Subtest Information</H4>']
00383 
00384         html.append('<table border="1" cellpadding="2" cellspacing="0">')
00385         html.append('<tr><td><b>Parameter</b></td><td><b>Value</b></td></tr>')
00386 
00387         if self._subtest._pre_script:
00388             html.append('<tr><td>Pre-test</td><td>%s</td></tr>' % os.path.basename(self._subtest._pre_script))
00389         else:
00390             html.append('<tr><td>Pre-test</td><td>None</td></tr>')
00391 
00392         if self._subtest._post_script:
00393             html.append('<tr><td>Post-test</td><td>%s</td></tr>' % os.path.basename(self._subtest._post_script))
00394         else:
00395             html.append('<tr><td>Post-test</td><td>None</td></tr>')
00396 
00397         launch_file = self._subtest._test_script
00398         (path, launch_pkg) = roslib.packages.get_dir_pkg(launch_file)
00399         path_idx = launch_file.find(launch_pkg) + len(launch_pkg) + 1
00400         
00401         
00402         html.append('<tr><td>Launch package</td><td>%s</td></tr>' % launch_pkg)
00403         html.append('<tr><td>Launch filepath</td><td>%s</td></tr>' % launch_file[path_idx:])
00404 
00405         html.append('</table>')
00406 
00407         return '\n'.join(html)
00408 
00409     # The header of a subtest result, also called when diplaying 
00410     # short version of tests in index.html file
00411     def html_header(self):
00412         html = ['<H4 ALIGN=CENTER>Results of %s%s</H4>\n' % (self._subtest.get_name(), self._retry_name)]
00413         
00414         html.append("<p><b>Test Status:</b></p>\n<H4>%s</H4>\n" % self._result.get_html_msg())
00415 
00416         if self._summary != '':
00417             html.append('<p><em>Summary</em></p>\n' )
00418             html.append('<p>%s</p>\n' % self._summary)
00419         if self._subresult_note != '':
00420             html.append('<p><em>Operator\'s Notes:</em></p>\n')
00421             html.append('<p>%s</p>\n' % self._subresult_note)
00422 
00423         return '\n'.join(html)
00424 
00425     def make_index_line(self, link, link_dir):
00426         result = self._result.get_html_msg()
00427 
00428         path = os.path.join(link_dir, self.filename())
00429         if link:
00430             hyperlink = '<a href=\"%s\">%s</a>' % (path, self._subtest.get_name())
00431         else:
00432             hyperlink = self._subtest.get_name()
00433 
00434         summary = '\n'.join([self._summary, self._subresult_note])
00435 
00436         return '<tr><td>%s</td><td>%s</td><td>%s</td></tr>\n' % (hyperlink, summary, result)
00437 
00438 
00439     
00440 ##\brief Result of Qualification test. Stores and logs all data
00441 ##
00442 ##
00443 ##\todo Make unit test of this class. Should get a series of subresults, check links, 
00444 class QualTestResult(object):
00445     ##\param qual_item QualTestItem : Item under test
00446     ##\param qual_test Test : Test we're running
00447     ##\param start_time int : Start time from rospy, or time.time()
00448     def __init__(self, qual_item, qual_test, start_time):
00449         self._qual_test = qual_test
00450 
00451         self._subresults = []
00452 
00453         self._retrys = []
00454         
00455         self._prestarts = []
00456 
00457         self._shutdown_result = None
00458 
00459         self._start_time = start_time
00460         self._start_time_filestr = self._start_time.strftime("%Y%m%d_%H%M%S")
00461         self._start_time_name = self._start_time.strftime("%Y/%m/%d %I:%M%p")
00462 
00463         self._item = qual_item
00464         self._serial = qual_item.serial
00465         self._item_name = qual_item.name
00466 
00467         if not os.path.isdir(TEMP_DIR):
00468             os.makedirs(TEMP_DIR)
00469 
00470         ##\todo Fix this
00471         # See if the qual_item is a configuration item
00472         try:
00473             config = qual_item._config
00474             self._config_only = True
00475         except Exception, e:
00476             self._config_only = False
00477 
00478         self._tar_filename = ''
00479 
00480         self._results_name = '%s_%s' % (self._serial, self._start_time_filestr)
00481 
00482         # Record that directory made
00483         self._made_dir = None
00484         self.set_results_dir(os.path.join(RESULTS_DIR, self._results_name))
00485 
00486         self._error = False
00487         self._canceled = False
00488 
00489         self._test_log = {}
00490 
00491         self._note = ''
00492         self._operator = ''
00493 
00494     def close(self):
00495         # Delete extra directories if empty
00496         if self._made_dir and len(os.listdir(self._made_dir)) == 0:
00497             os.rmdir(self._made_dir)
00498 
00499         # Remove any temporary files
00500         shutil.rmtree(TEMP_DIR)
00501 
00502     @property
00503     def results_dir(self): return self._results_dir
00504 
00505     @property
00506     def tar_name(self):  return self._tar_filename 
00507 
00508     def set_results_dir(self, path):
00509         if not path.endswith('/'):
00510             path += '/'
00511 
00512         self._results_dir = path
00513         if not os.path.isdir(self._results_dir):
00514             self._made_dir = self._results_dir
00515             os.makedirs(self._results_dir)
00516         else:
00517             self._made_dir = None
00518         
00519        
00520     def set_notes(self, note):
00521         self._note = note
00522 
00523     def set_operator(self, name):
00524         self._operator = name
00525 
00526     def log(self, entry):
00527         self._test_log[datetime.now()] = entry
00528 
00529     ##\todo All these should be fixed
00530     def get_prestarts(self):
00531         return self._prestarts[:]
00532 
00533     def get_subresults(self, reverse = False):
00534         vals = self._subresults[:]
00535         if reverse:
00536             vals = vals.reverse()
00537         return vals
00538 
00539     def get_retrys(self, reverse = False):
00540         vals = self._retrys[:]
00541         if reverse:
00542             vals = vals.reverse()
00543         return vals
00544 
00545     # Come up with better enforcement of get functions
00546     def get_subresult(self, index):
00547         if len(self._subresults) == 0 or index >= len(self._subresults) or index < 0:
00548             return None
00549 
00550         return self._subresults[index]
00551 
00552     def get_retry(self, index):
00553         if len(self._retrys) == 0 or index >= len(self._retrys) or index < 0:
00554             return None
00555 
00556         return self._retrys[index]
00557 
00558     ##\todo All these should just be appending to a list
00559     def add_shutdown_result(self, msg):
00560         script = self._qual_test.getShutdownScript()
00561 
00562         self._shutdown_result = TestScriptResult(script, msg)
00563     
00564     def add_prestartup_result(self, index, msg):
00565         test_script = self._qual_test.pre_startup_scripts[index]
00566 
00567         self._prestarts.append(TestScriptResult(test_script, msg))
00568 
00569     def add_sub_result(self, index, msg):
00570         subtest = self._qual_test.subtests[index]
00571 
00572         sub = SubTestResult(subtest, msg)
00573         
00574         self._subresults.append(sub)
00575 
00576         return sub
00577 
00578     def cancel(self):
00579         self._canceled = True
00580 
00581     def error(self):
00582         self._error = True
00583 
00584     ##\brief Stores data from subtest as a "retry"
00585     def retry_subresult(self, index, notes = ''):
00586         retry_count = len(self._retrys) + 1
00587 
00588         sub = self.get_subresult(index)
00589         if not sub: # Should error here
00590             return
00591 
00592         del self._subresults[index]
00593 
00594         #sub.set_note(notes)
00595         sub.retry_test(retry_count, notes)
00596 
00597         #sub.write_images(TEMP_DIR, ) # Rewrite images for display
00598 
00599         #name = sub.get_name()
00600         #retry_name = name + "_retry%d" % (retry_count)
00601 
00602 
00603         self._retrys.append(sub)
00604 
00605     ##\todo private fn
00606     def prestarts_ok(self):
00607         for prestart in self.get_prestarts():
00608             if not prestart.get_pass_bool():
00609                 return False
00610 
00611         return True
00612 
00613     def is_prestart_error(self):
00614         if len(self.get_prestarts()) == 0:
00615             return False
00616 
00617         return self.get_prestarts()[-1].has_error()
00618 
00619     def get_pass_bool(self):
00620         if self._canceled or self._error:
00621             return False
00622 
00623         if not self.prestarts_ok():
00624             return False
00625 
00626         if self._shutdown_result and not self._shutdown_result.get_pass_bool():
00627             return False
00628 
00629         if len(self._subresults) == 0:
00630             return False
00631         
00632         for res in self._subresults:
00633             if not res.get_pass_bool():
00634                 return False
00635         
00636         return True
00637 
00638     def get_test_result_str_invent(self):
00639         if self.get_pass_bool():
00640             return "PASS"
00641         return "FAIL"
00642 
00643     ##\todo This needs major cleanup
00644     def get_test_result_str(self):
00645         if len(self.get_subresults()) == 0 or not self.prestarts_ok():
00646             return "Fail"
00647 
00648         if self._canceled:
00649             return "Cancel"
00650         if self._error:
00651             return "Error"
00652 
00653         manual = False
00654         for res in self.get_subresults():
00655             if res._result.is_retry():
00656                 continue
00657 
00658             if res._result.is_human_required():
00659                 return "Human Required"
00660 
00661             if res._result.is_error():
00662                  return "Error"
00663             if res._result.is_cancel():
00664                 return "Cancel"
00665             if not res._result.get_pass_bool():
00666                 return "Fail"
00667 
00668             if res._result.is_manual():
00669                 manual = True
00670 
00671         if manual:
00672             return "Operator Pass"
00673         return "Pass"
00674 
00675     ##\todo Append strings, make parse tests
00676     ##\todo Rename all "make_" methods to "write_" methods
00677     def make_summary_page(self, link = True, link_dir = TEMP_DIR):
00678         html = "<html><head>\n"
00679         html += "<title>Qualification Test Result for %s as %s: %s</title>\n" % (self._serial, self._item_name, self._start_time_name)
00680         html += "<style type=\"text/css\">\
00681 body { color: black; background: white; }\
00682 div.pass { background: green; padding: 0.5em; border: none; }\
00683 div.warn { background: yellow; padding: 0.5em; border: none; }\
00684 div.error { background: red; padding: 0.5em; border: none; }\
00685 strong { font-weight: bold; color: red; }\
00686 em { font-style: normal; font-weight: bold; }\
00687 </style>\
00688 </head>\n<body>\n"
00689 
00690         if not self._config_only:
00691             html += '<H2 ALIGN=CENTER>Qualification of: %s</H2>\n<br>\n' % self._serial
00692         else:
00693             html += '<H2 ALIGN=CENTER>Configuration of: %s</H2>\n<br>\n' % self._serial
00694             st = self.get_subresult(0)
00695             if st is not None:
00696                 html += st.html_header()
00697             else:
00698                 html += '<p>No data from configuration, result: %s.</p>\n' % (self.get_test_result_str())
00699             html += '</body></html>'
00700             return html
00701 
00702         html += '<H3 align=center>%s</H3>\n' % self._qual_test.getName()
00703 
00704         if self.get_pass_bool():
00705             result_header = '<H3>Test Result: <em>%s</em></H3>\n' % self.get_test_result_str()
00706         else:
00707             result_header = '<H3>Test Result: <strong>%s</strong></H3>\n' % self.get_test_result_str()
00708 
00709         html += result_header
00710         html += '<HR size="2">'
00711 
00712         if self._operator == '':
00713             operator = 'Unknown'
00714         else:
00715             operator = self._operator
00716 
00717         if self._note is None or self._note == '':
00718             self._note = 'No notes given.'
00719         
00720         html += '<H5><b>Test Engineer\'s Notes</b></H5>\n'
00721         html += '<p><b>Test Engineer: %s</b></p>\n' % operator        
00722         html += '<p>%s</p>\n' % self._note
00723 
00724         html += '<H5>Test Date</H5>\n'
00725         html += '<p>Completed Test at %s on %s.</p>\n' % (self._start_time_name, self._serial)
00726         html += '<H5><b>Results Directory</b></H5>\n<p>%s</p>\n' % self._results_dir
00727 
00728         if self._canceled:
00729             html += '<p><b>Test canceled by operator.</b></p>\n'
00730 
00731         if len(self.get_subresults()) == 0:
00732             if self.prestarts_ok():
00733                 html += '<p>No subtests completed. Test may have ended badly.</p>\n'
00734             elif self.is_prestart_error():
00735                 html += '<p>Error during pretests. Check system and retry.</p>\n'
00736             else:
00737                 html += '<p>Prestartup failure. Component may be damaged.</p>\n'
00738         else:
00739             html += '<HR size="2">\n'
00740             # Index items link to each page if link 
00741             html += self.make_index(link, link_dir)
00742             
00743         if len(self.get_retrys()) > 0:
00744             html += '<hr size="2">\n'
00745             html += self.make_retry_index(link, link_dir)
00746 
00747 
00748         startup = self._qual_test.getStartupScript()
00749         if startup:
00750             html += '<hr size="2">\n'
00751             html += self.make_startup_data()
00752             
00753         # Make table for prestartup scripts
00754         if len(self._prestarts) > 0:
00755             html += '<hr size="2">\n'
00756             html += self.make_prestart_table()
00757 
00758         if self._qual_test.getShutdownScript():
00759             html += '<hr size="2">\n'
00760             html += self.make_shutdown_results()
00761 
00762         html += '<hr size="2">\n'
00763         html += self.make_log_table()
00764         html += '<hr size="2">\n'
00765 
00766         html += '</body></html>'
00767 
00768         return html
00769 
00770     ##\todo private
00771     def make_startup_data(self):
00772         startup = self._qual_test.getStartupScript()
00773         
00774         html = ['<H4 align=center>Startup Script</H4>']
00775         html.append('<table border="1" cellpadding="2" cellspacing="0">')
00776         html.append('<tr><td><b>Parameter</b></td><td><b>Value</b></td></b>')
00777         html.append('<tr><td>Name</td><td>%s</td>' % startup.get_name())
00778 
00779         # Get launch pkg, filepath for startup
00780         launch_file = startup.launch_file
00781         (path, launch_pkg) = roslib.packages.get_dir_pkg(launch_file)
00782         path_idx = launch_file.find(launch_pkg) + len(launch_pkg) + 1
00783                 
00784         html.append('<tr><td>Launch package</td><td>%s</td></tr>' % launch_pkg)
00785         html.append('<tr><td>Launch filepath</td><td>%s</td></tr>' % launch_file[path_idx:])
00786         html.append('</table>\n')
00787 
00788         return '\n'.join(html)
00789 
00790     ##\todo private
00791     def make_shutdown_results(self):
00792         shutdown = self._qual_test.getShutdownScript()
00793 
00794         html = ['<H4 align=center>Shutdown Script</H4>']
00795 
00796         if not self._shutdown_result:
00797             html.append('<p>Shutdown script: %s</p>' % shutdown.get_name())
00798             html.append('<p>No shutdown results.</p>')
00799             return '\n'.join(html)
00800 
00801         name   = self._shutdown_result.get_name()
00802         launch = self._shutdown_result.get_launch()
00803         res    = self._shutdown_result.get_result_msg()
00804         msg    = self._shutdown_result.get_msg()
00805 
00806 
00807         html.append('<table border="1" cellpadding="2" cellspacing="0">')
00808         html.append('<tr><td><b>Name</b></td><td><b>Launch File</b></td><td><b>Result</b></td><td><b>Message</b></td></tr></b>')
00809         html.append('<tr><td>%s</td><td>%s</td>' % (name, launch))
00810         html.append('<td>%s</td><td>%s</td></tr>' % (res, msg))
00811         html.append('</table>\n')
00812 
00813         return '\n'.join(html)
00814 
00815 
00816     def line_summary(self):
00817         if self._config_only and self.get_pass_bool():
00818             sum = "Reconfigured %s as %s." % (self._serial, self._qual_test.getName())
00819         else:
00820             sum = "Qualification of %s. Test name: %s. Result: %s." % (self._serial, self._qual_test.getName(), self.get_test_result_str())
00821             if self._note != '':
00822                 sum += " Notes: %s" % (self._note)
00823 
00824         return sum
00825 
00826     ##\todo private
00827     def make_retry_index(self, link, link_dir):
00828         html = ['<H4 AlIGN=CENTER>Retried Subtest Index</H4>' ]
00829         html.append('<table border="1" cellpadding="2" cellspacing="0">\n')
00830         html.append('<tr><td><b>Test Name</b></td><td><b>Summary</b></td><td><b>Final Result</b></td></tr></b>\n')
00831 
00832         for st in self.get_retrys():
00833             html.append(st.make_index_line(link, link_dir))
00834 
00835         html.append('</table>\n')
00836         
00837         return '\n'.join(html)
00838 
00839     ##\todo private
00840     def make_index(self, link, link_dir):
00841         html = '<H4 AlIGN=CENTER>Results Index</H4>\n'
00842         html += '<table border="1" cellpadding="2" cellspacing="0">\n'
00843         html += '<tr><td><b>Test Name</b></td><td><b>Summary</b></td><td><b>Final Result</b></td></tr></b>\n'
00844 
00845         for st in self.get_subresults():
00846             html += st.make_index_line(link, link_dir)
00847 
00848         html += '</table>\n'
00849         
00850         return html
00851         
00852     ##\todo private
00853     def make_log_table(self):
00854         # Sort test log by times
00855         kys = dict.keys(self._test_log)
00856         kys.sort()
00857         
00858         html = ['<H4 AlIGN=CENTER>Test Log Data</H4>']
00859         html.append('<table border="1" cellpadding="2" cellspacing="0">')
00860         html.append('<tr><td><b>Time</b></td><td><b>Log Message</b></td></tr></b>')
00861         for ky in kys:
00862             time_str = ky.strftime("%m/%d/%Y %H:%M:%S")
00863             html.append('<tr><td>%s</td><td>%s</td></tr>' % (time_str, self._test_log[ky]))
00864         html.append('</table>\n')
00865 
00866         return '\n'.join(html)
00867 
00868     ##\todo private
00869     def make_prestart_table(self):
00870         if len(self.get_prestarts()) == 0:
00871             return '<p>No prestartup scripts.</p>\n'
00872     
00873         html = ['<H4 ALIGN=CENTER>Prestartup Script Data</H4>']
00874         html.append('<table border="1" cellpadding="2" cellspacing="0">')
00875         html.append('<tr><td><b>Script</b></td><td><b>Launch File</b></td>')
00876         html.append('<td><b>Result</b></td><td><b>Message</b></td></tr></b>')
00877         for prestart in self.get_prestarts():
00878             html.append('<tr><td>%s</td><td>%s</td>' % (prestart.get_name(), prestart.get_launch()))
00879             html.append('<td>%s</td><td>%s</td></tr>' % (prestart.get_result_msg(), prestart.get_msg()))
00880         html.append('</table>\n')
00881 
00882         return '\n'.join(html)
00883 
00884     def write_results_to_file(self, temp = True, local_link = False):
00885         write_dir = TEMP_DIR if temp else self._results_dir
00886         
00887         # Use local links for image sources, etc if true
00888         link_dir = write_dir
00889         header_link_dir = write_dir
00890         if local_link:
00891             link_dir = '../'
00892             header_link_dir = ''
00893 
00894         if not os.path.isdir(write_dir):
00895             os.mkdir(write_dir)
00896               
00897         index_path = os.path.join(write_dir, 'index.html')
00898         index = open(index_path, 'w')
00899         index.write(self.make_summary_page(True, header_link_dir))
00900         index.close()
00901         
00902         for i, st in enumerate(self._subresults):
00903             prev = self.get_subresult(i - 1)
00904             next = self.get_subresult(i + 1)
00905 
00906             if not os.path.isdir(os.path.join(write_dir, st.filename_base())):
00907                 os.mkdir(os.path.join(write_dir, st.filename_base()))
00908 
00909             st_path = os.path.join(write_dir, st.filename())
00910             st_file = open(st_path, 'w')
00911             st_file.write(st.make_result_page(True, link_dir, prev, next))
00912             st_file.close()
00913 
00914             st.write_images(write_dir)
00915 
00916         for i, st in enumerate(self._retrys):
00917             prev = self.get_retry(i - 1)
00918             next = self.get_retry(i + 1)
00919 
00920             if not os.path.isdir(os.path.join(write_dir, st.filename_base())):
00921                 os.mkdir(os.path.join(write_dir, st.filename_base()))
00922 
00923             st_path = os.path.join(write_dir, st.filename())
00924             st_file = open(st_path, 'w')
00925             st_file.write(st.make_result_page(True, link_dir, prev, next))
00926             st_file.close()
00927 
00928             st.write_images(write_dir)
00929 
00930        
00931         # Make tar file of results
00932         if not temp:
00933             self._write_tar_file()
00934         
00935     ##\brief Dumps all files in results directory into tar file
00936     def _write_tar_file(self):
00937         self._tar_filename = os.path.join(self._results_dir, self._results_name + '_data.tar')
00938         
00939         # Change filename to basename when adding to tar file
00940         tar = tarfile.open(self._tar_filename, 'w:')
00941         for filename in os.listdir(self._results_dir):
00942             # Use only file base names in tar file
00943             fullname = os.path.join(self._results_dir, filename)
00944             tar.add(fullname, arcname=filename)
00945         tar.close()
00946 
00947     def _log_config_result(self, invent):
00948         sub = self.get_subresult(0) # Only subresult of configuration
00949         if not sub:
00950             return True, 'No subresult found!'
00951         ##\todo Check attachment ID of return value
00952         invent.add_attachment(self._serial, sub.filename_base() + '.html', 'text/html', 
00953                               sub.make_result_page(), self.line_summary())
00954         return True, 'Logged reconfiguration in inventory system.'        
00955              
00956     # Make invent results pretty, HTML links work
00957     ##\todo Add timeout to invent
00958     def log_results(self, invent):
00959         # Write results to results dir, with local links
00960         self.write_results_to_file(temp = False, local_link = True)
00961 
00962         if invent == None:
00963             return False, "Attempted to log results to inventory, but no invent client found."
00964         if self.is_prestart_error():
00965             return True, "Test recorded internal error, not submitting to inventory system."
00966         
00967         prefix = self._start_time_filestr + "_" # Put prefix first so images sorted by date
00968         
00969         if self._config_only:
00970             return self._log_config_result(invent)
00971 
00972         invent.setKV(self._serial, "Test Status", self.get_test_result_str_invent())
00973 
00974         # Need to get tar to bit stream
00975         f = open(self._tar_filename, "rb")
00976         my_tar_data = f.read()
00977         f.close()
00978         
00979         my_data = self.export_data()
00980 
00981         try:
00982             ok = wg_invent_client.submit_log(invent, my_data, my_tar_data)
00983             msg = 'Wrote tar file, uploaded to inventory system.'
00984             if not ok:
00985                 msg = 'Unable to upload to Invent. Check console output for details.'
00986             
00987             return ok, msg
00988         except Exception, e:
00989             import traceback
00990             self.log('Caught exception uploading test parameters to invent.\n%s' % traceback.format_exc())
00991             return False, 'Caught exception loading tar file to inventory.\n%s' % traceback.format_exc()
00992 
00993     def export_data(self):
00994         """
00995         Exports result data to wg_invent_client.TestData
00996 
00997         Unit testing and Invent logging only.
00998 
00999         \return wg_invent_client.TestData : Data for test
01000         """
01001         ##\todo change to start time
01002         my_data = wg_invent_client.TestData(self._qual_test.testid, self._qual_test.get_name(), 
01003                                             time.time(), 
01004                                             self._serial, self.get_test_result_str_invent())
01005 
01006         if self._tar_filename and os.path.exists(self._tar_filename):
01007             my_data.set_attachment('application/tar', os.path.basename(self._tar_filename))
01008 
01009         my_data.set_note(self._note)
01010 
01011         for st in (self.get_subresults() + self.get_retrys()):
01012             my_data.add_subtest(st.export_data())
01013 
01014         return my_data
01015 
01016     def get_qual_team(self):
01017         if socket.gethostname() == 'nsf': # Debug on NSF HACK!!!!
01018             return 'watts@willowgarage.com'
01019 
01020         return 'qualdevteam@lists.willowgarage.com'
01021 
01022     ##\brief Creates MIMEMultipart email message with proper attachments
01023     ##
01024     ##\return MIMEMultipart email message with tarfile attachment of plots
01025     def make_email_message(self):
01026         msg = MIMEMultipart('alternative')
01027         msg['Subject'] = "--QualResult-- %s" % self.line_summary()
01028         msg['From'] = "qual.test@willowgarage.com" 
01029         msg['To'] = self.get_qual_team()
01030         
01031         msg.attach(MIMEText(self.make_summary_page(False), 'html'))
01032         
01033         # Add results as tar file
01034         if self._tar_filename is not None and self._tar_filename != '':
01035             part = MIMEBase('application', 'octet-stream')
01036             with open(self._tar_filename, 'rb') as f:
01037                 data = f.read()
01038             part.set_payload( data )
01039             Encoders.encode_base64(part)
01040             part.add_header('Content-Disposition', 'attachment; filename="%s"' 
01041                             % os.path.basename(self._tar_filename))
01042             msg.attach(part)
01043 
01044         return msg
01045 
01046     ##\brief Email qualification team results as HTML summary and tar file
01047     def email_qual_team(self):
01048         try:
01049             msg = self.make_email_message()
01050 
01051             s = smtplib.SMTP('localhost')
01052             s.sendmail('qual.test@willowgarage.com', self.get_qual_team(), msg.as_string())
01053             s.quit()
01054 
01055             return True
01056         except Exception, e:
01057             import traceback
01058             print 'Unable to sent mail, caught exception!\n%s' % traceback.format_exc()
01059             return False


qualification
Author(s): Kevin Watts (watts@willowgarage.com), Josh Faust (jfaust@willowgarage.com)
autogenerated on Sat Dec 28 2013 17:57:34