99import glob
1010from collections import Counter
1111import codecs
12+ from PIL import Image
13+ from io import BytesIO
14+ import shutil
1215
1316_total = _executed = 0
1417_pass = _fail = 0
7578_suite_error = 0
7679_suite_fail = 0
7780_pvalue = 0
81+ screen_base = ''
82+ screen_img = None
83+ _attach_screenshot_details = ''
7884
7985
8086def pytest_addoption (parser ):
@@ -90,6 +96,7 @@ def pytest_addoption(parser):
9096
9197def pytest_configure (config ):
9298 path = config .getoption ("path" )
99+ clean_screenshots (path )
93100
94101 config ._html = HTMLReporter (path , config )
95102 config .pluginmanager .register (config ._html )
@@ -155,8 +162,20 @@ def max_rerun():
155162 return None
156163
157164
158- class HTMLReporter :
165+ def screenshot (data = None ):
166+ global screen_base , screen_img
159167
168+ screen_base = HTMLReporter .base_path
169+ screen_img = Image .open (BytesIO (data ))
170+
171+
172+ def clean_screenshots (path ):
173+ screenshot_dir = os .path .abspath (os .path .expanduser (os .path .expandvars (path ))) + '/pytest_screenshots'
174+ if os .path .isdir (screenshot_dir ):
175+ shutil .rmtree (screenshot_dir )
176+
177+
178+ class HTMLReporter (object ):
160179 def __init__ (self , path , config ):
161180 self .json_data = {'content' : {'suites' : {0 : {'status' : {}, 'tests' : {0 : {}}, }, }}}
162181 self .path = path
@@ -207,9 +226,11 @@ def report_path(self):
207226 path = '.' if '.html' in self .path .rsplit ('/' , 1 )[0 ] else self .path .rsplit ('/' , 1 )[0 ]
208227 if path == '' : path = '.'
209228 logfile = os .path .expanduser (os .path .expandvars (path ))
229+ HTMLReporter .base_path = os .path .abspath (logfile )
210230 return os .path .abspath (logfile ), self .path .split ('/' )[- 1 ]
211231 else :
212232 logfile = os .path .expanduser (os .path .expandvars (self .path ))
233+ HTMLReporter .base_path = os .path .abspath (logfile )
213234 return os .path .abspath (logfile ), 'pytest_html_report.html'
214235
215236 @pytest .hookimpl (hookwrapper = True )
@@ -328,7 +349,10 @@ def append_test_metrics_row(self):
328349 if (self .rerun is not None ) and (max_rerun () is not None ):
329350 if (_test_status == 'FAIL' ) or (_test_status == 'ERROR' ): _pvalue += 1
330351
331- if (_pvalue == max_rerun ()+ 1 ) or (_test_status == 'PASS' ):
352+ if (_pvalue == max_rerun () + 1 ) or (_test_status == 'PASS' ):
353+ if ((_test_status == 'FAIL' ) or (_test_status == 'ERROR' )) and (
354+ screen_base != '' ): self .generate_screenshot_data ()
355+
332356 test_row_text = test_row_text .replace ("__sname__" , str (_suite_name ))
333357 test_row_text = test_row_text .replace ("__name__" , str (_test_name ))
334358 test_row_text = test_row_text .replace ("__stat__" , str (_test_status ))
@@ -337,7 +361,8 @@ def append_test_metrics_row(self):
337361
338362 _test_metrics_content += test_row_text
339363 _pvalue = 0
340- elif (self .rerun is not None ) and ((_test_status == 'xFAIL' ) or (_test_status == 'xPASS' ) or (_test_status == 'SKIP' )):
364+ elif (self .rerun is not None ) and (
365+ (_test_status == 'xFAIL' ) or (_test_status == 'xPASS' ) or (_test_status == 'SKIP' )):
341366 test_row_text = test_row_text .replace ("__sname__" , str (_suite_name ))
342367 test_row_text = test_row_text .replace ("__name__" , str (_test_name ))
343368 test_row_text = test_row_text .replace ("__stat__" , str (_test_status ))
@@ -347,6 +372,9 @@ def append_test_metrics_row(self):
347372 _test_metrics_content += test_row_text
348373
349374 elif (self .rerun is None ) or (max_rerun () is None ):
375+ if ((_test_status == 'FAIL' ) or (_test_status == 'ERROR' )) and (
376+ screen_base != '' ): self .generate_screenshot_data ()
377+
350378 test_row_text = test_row_text .replace ("__sname__" , str (_suite_name ))
351379 test_row_text = test_row_text .replace ("__name__" , str (_test_name ))
352380 test_row_text = test_row_text .replace ("__stat__" , str (_test_status ))
@@ -364,15 +392,36 @@ def append_test_metrics_row(self):
364392 len (_scenario ) - 1 , {})['test_name' ] = str (_test_name )
365393
366394 if (self .rerun is not None ) and (max_rerun () is not None ):
367- self .json_data ['content' ]['suites' ].setdefault (len (_test_suite_name ), {}).setdefault ('tests' , {}).setdefault (
395+ self .json_data ['content' ]['suites' ].setdefault (len (_test_suite_name ), {}).setdefault ('tests' ,
396+ {}).setdefault (
368397 len (_scenario ) - 1 , {})['rerun' ] = str (self .rerun )
369398 else :
370399 self .json_data ['content' ]['suites' ].setdefault (len (_test_suite_name ), {}).setdefault ('tests' ,
371400 {}).setdefault (
372401 len (_scenario ) - 1 , {})['rerun' ] = '0'
373402
403+ def generate_screenshot_data (self ):
404+ os .makedirs (screen_base + '/pytest_screenshots' , exist_ok = True )
405+
406+ _screenshot_name = round (time .time ())
407+ _screenshot_suite_name = _suite_name .split ('/' )[- 1 :][0 ].replace ('.py' , '' )
408+ _screenshot_test_name = _test_name
409+ if len (_test_name ) >= 19 : _screenshot_test_name = _test_name [- 17 :]
410+ _screenshot_error = _current_error
411+
412+ screen_img .save (
413+ screen_base + '/pytest_screenshots/' + str (_screenshot_name ) + '.png'
414+ )
415+
416+ # attach screenshots
417+ self .attach_screenshots (_screenshot_name , _screenshot_suite_name , _screenshot_test_name , _screenshot_error )
418+ _screenshot_name = ''
419+ _screenshot_suite_name = ''
420+ _screenshot_test_name = ''
421+ _screenshot_error = ''
422+
374423 def append_suite_metrics_row (self , name ):
375- global _spass_tests , _sfail_tests , _sskip_tests , _sxpass_tests , _sxfail_tests , _serror_tests , _srerun_tests ,\
424+ global _spass_tests , _sfail_tests , _sskip_tests , _sxpass_tests , _sxfail_tests , _serror_tests , _srerun_tests , \
376425 _error , _suite_error , _suite_fail
377426
378427 self ._test_names (_test_name , clear = 'yes' )
@@ -602,6 +651,7 @@ def renew_template_text(self, logo_url):
602651 template_text = template_text .replace ("__css_styles__" ,
603652 str (codecs .open ('pytest_html_reporter/style.css' ,
604653 encoding = 'utf-8' ).read ()))
654+ template_text = template_text .replace ("__attach_screenshot_details__" , str (_attach_screenshot_details ))
605655 return template_text
606656
607657 def generate_json_data (self , base ):
@@ -881,3 +931,37 @@ def update_trends(self, base):
881931 tskip .append (data ['status_list' ]['skip' ])
882932
883933 if i == 4 : break
934+
935+ def attach_screenshots (self , screen_name , test_suite , test_case , test_error ):
936+ global _attach_screenshot_details
937+
938+ _screenshot_details = """
939+ <div class="img-hover col-md-6 col-xl-3 p-3">
940+ <div>
941+ <a class="video" href="__screenshot_base__/pytest_screenshots/__screen_name__.png" data-toggle="lightbox" style="background-image: url('__screenshot_base__/pytest_screenshots/__screen_name__.png');" data-fancybox="images" data-caption="SUITE: __ts__ :: SCENARIO: __tc__">
942+ <span class="video-hover-desc video-hover-small"> <span style="font-size:23px;display: block;margin-bottom: 15px;"> __tc__</span>
943+ <span>__te__</span> </span>
944+ </a>
945+ <p class="text-desc"><strong>__ts__</strong><br />
946+ __te__</p>
947+ </div>
948+ </div>
949+ <div class="desc-video-none">
950+ <div class="desc-video" id="Video-desc-01">
951+ <h2>__tc__</h2>
952+
953+ <p><strong>__ts__</strong><br />
954+ __te__</p>
955+ </div>
956+ </div>
957+ """
958+
959+ if len (test_case ) == 17 : test_case = '..' + test_case
960+
961+ _screenshot_details = _screenshot_details .replace ("__screen_name__" , str (screen_name ))
962+ _screenshot_details = _screenshot_details .replace ("__ts__" , str (test_suite ))
963+ _screenshot_details = _screenshot_details .replace ("__tc__" , str (test_case ))
964+ _screenshot_details = _screenshot_details .replace ("__te__" , str (test_error ))
965+ _screenshot_details = _screenshot_details .replace ("__screenshot_base__" , str (screen_base ))
966+
967+ _attach_screenshot_details += _screenshot_details
0 commit comments