2828SCHEDULE_RUNNER_TARGET = os .path .join (UT_DIR_PATH , "zzz_run_scheduler.py" )
2929RX_RESULT = re .compile (r'^(?P<result>OK|FAILED|ERROR)' , re .MULTILINE )
3030RX_DONE = re .compile (r'^UnitTesting: Done\.$' , re .MULTILINE )
31+ RX_TEST_STATUS = re .compile (r'\.\.\. (ok|FAIL|ERROR|skipped)(\b.*)$' )
32+ RX_SUMMARY_OK = re .compile (r'^OK(?:\b.*)?$' )
33+ RX_SUMMARY_FAIL = re .compile (r'^(FAILED|ERROR)(?:\b.*)?$' )
34+
35+ ANSI_RESET = "\033 [0m"
36+ ANSI_GREEN = "\033 [32m"
37+ ANSI_RED = "\033 [31m"
38+ ANSI_YELLOW = "\033 [33m"
39+ ANSI_CYAN = "\033 [36m"
3140
3241_is_windows = sys .platform == 'win32'
3342
@@ -113,9 +122,11 @@ def kill_sublime_text():
113122 subprocess .Popen ("pkill plugin_host || true" , shell = True )
114123
115124
116- def read_output (path ):
125+ def read_output (path , color = 'auto' ):
117126 # todo: use notification instead of polling
118127 success = None
128+ use_color = should_use_color (color )
129+ pending = ""
119130
120131 def check_is_success (result ):
121132 try :
@@ -131,8 +142,13 @@ def check_is_done(result):
131142 offset = f .tell ()
132143 result = f .read ()
133144
134- print (result , end = "" )
135- sys .stdout .flush ()
145+ if result :
146+ if use_color :
147+ rendered , pending = colorize_output_chunk (result , pending )
148+ print (rendered , end = "" )
149+ else :
150+ print (result , end = "" )
151+ sys .stdout .flush ()
136152
137153 # Keep checking while we don't have a definite result.
138154 success = check_is_success (result )
@@ -145,9 +161,93 @@ def check_is_done(result):
145161
146162 time .sleep (0.2 )
147163
164+ if use_color and pending :
165+ print (colorize_output_line (pending ), end = "" )
166+ sys .stdout .flush ()
167+
148168 return success
149169
150170
171+ def should_use_color (mode ):
172+ if mode == 'always' :
173+ return True
174+
175+ if mode == 'never' :
176+ return False
177+
178+ if os .environ .get ('NO_COLOR' ) is not None :
179+ return False
180+
181+ if os .environ .get ('CLICOLOR_FORCE' ) not in (None , '' , '0' ):
182+ return True
183+
184+ if os .environ .get ('FORCE_COLOR' ) not in (None , '' , '0' ):
185+ return True
186+
187+ try :
188+ return sys .stdout .isatty ()
189+ except Exception :
190+ return False
191+
192+
193+ def colorize_output_chunk (chunk , pending ):
194+ if not chunk :
195+ return '' , pending
196+
197+ text = pending + chunk
198+ lines = text .splitlines (True )
199+
200+ if lines and not lines [- 1 ].endswith (('\n ' , '\r ' )):
201+ pending = lines .pop ()
202+ else :
203+ pending = ''
204+
205+ rendered = '' .join (colorize_output_line (line ) for line in lines )
206+ return rendered , pending
207+
208+
209+ def colorize_output_line (line ):
210+ newline = ''
211+ body = line
212+
213+ if body .endswith ('\r \n ' ):
214+ body = body [:- 2 ]
215+ newline = '\r \n '
216+ elif body .endswith ('\n ' ) or body .endswith ('\r ' ):
217+ body = body [:- 1 ]
218+ newline = line [- 1 ]
219+
220+ test_status_match = RX_TEST_STATUS .search (body )
221+ if test_status_match :
222+ status = test_status_match .group (1 )
223+ suffix = test_status_match .group (2 )
224+ body = (
225+ body [:test_status_match .start ()]
226+ + '... '
227+ + colorize_status (status )
228+ + suffix
229+ )
230+
231+ if RX_SUMMARY_OK .match (body ):
232+ body = ANSI_GREEN + body + ANSI_RESET
233+ elif RX_SUMMARY_FAIL .match (body ):
234+ body = ANSI_RED + body + ANSI_RESET
235+ elif body .startswith ('Ran ' ):
236+ body = ANSI_CYAN + body + ANSI_RESET
237+
238+ return body + newline
239+
240+
241+ def colorize_status (status ):
242+ if status == 'ok' :
243+ return ANSI_GREEN + status + ANSI_RESET
244+
245+ if status == 'skipped' :
246+ return ANSI_YELLOW + status + ANSI_RESET
247+
248+ return ANSI_RED + status + ANSI_RESET
249+
250+
151251def restore_coverage_file (path , package ):
152252 # restore .coverage if it exists, needed for coveralls
153253 if os .path .exists (path ):
@@ -207,7 +307,7 @@ def detect_package_control_version():
207307 return str (version ) if version else None
208308
209309
210- def main (default_schedule_info , dry_run = False ):
310+ def main (default_schedule_info , dry_run = False , color = 'auto' ):
211311 package_under_test = default_schedule_info ['package' ]
212312 output_dir = os .path .join (UT_OUTPUT_DIR_PATH , package_under_test )
213313 output_file = os .path .join (output_dir , "result" )
@@ -247,7 +347,7 @@ def main(default_schedule_info, dry_run=False):
247347 time .sleep (2 )
248348
249349 print ("Start to read output..." )
250- if not read_output (output_file ):
350+ if not read_output (output_file , color = color ):
251351 sys .exit (1 )
252352 restore_coverage_file (coverage_file , package_under_test )
253353 delete_file_if_exists (SCHEDULE_RUNNER_TARGET )
@@ -264,6 +364,13 @@ def main(default_schedule_info, dry_run=False):
264364 parser .add_option ('--failfast' , action = 'store_true' )
265365 parser .add_option ('--reload-package-on-testing' , action = 'store_true' )
266366 parser .add_option ('--dry-run' , action = 'store_true' )
367+ parser .add_option (
368+ '--color' ,
369+ type = 'choice' ,
370+ choices = ['auto' , 'always' , 'never' ],
371+ default = 'auto' ,
372+ help = 'Colorize test output (auto, always, never).' ,
373+ )
267374
268375 options , remainder = parser .parse_args ()
269376
@@ -294,4 +401,4 @@ def main(default_schedule_info, dry_run=False):
294401 if options .reload_package_on_testing :
295402 default_schedule_info ['reload_package_on_testing' ] = True
296403
297- main (default_schedule_info , dry_run = options .dry_run )
404+ main (default_schedule_info , dry_run = options .dry_run , color = options . color )
0 commit comments