def __init__(self, run_tracker, settings): Reporter.__init__(self, run_tracker, settings) # The main report, and associated tool outputs, go under this dir. self._html_dir = settings.html_dir # We render HTML from mustache templates. self._renderer = MustacheRenderer(Renderer(search_dirs=settings.template_dir)) # We serve files relative to the build root. self._buildroot = get_buildroot() self._html_path_base = os.path.relpath(self._html_dir, self._buildroot) # We write the main report body to this file object. self._report_file = None # We redirect stdout, stderr etc. of tool invocations to these files. self._output_files = defaultdict(dict) # workunit_id -> {path -> fileobj}.
def __init__(self, port, settings): renderer = MustacheRenderer(settings.template_dir, __name__) class MyHandler(PantsHandler): def __init__(self, request, client_address, server): PantsHandler.__init__(self, settings, renderer, request, client_address, server) self._httpd = BaseHTTPServer.HTTPServer(('', port), MyHandler) self._httpd.timeout = 0.1 # Not the network timeout, but how often handle_request yields.
def __init__(self, run_tracker, settings): Reporter.__init__(self, run_tracker, settings) # The main report, and associated tool outputs, go under this dir. self._html_dir = settings.html_dir # We render HTML from mustache templates. self._renderer = MustacheRenderer(settings.template_dir, __name__) # We serve files relative to the build root. self._buildroot = get_buildroot() self._html_path_base = os.path.relpath(self._html_dir, self._buildroot) # We write the main report body to this file object. self._report_file = None # We redirect stdout, stderr etc. of tool invocations to these files. self._output_files = defaultdict(dict) # workunit_id -> {path -> fileobj}.
class HtmlReporter(Reporter): """HTML reporting to files. The files are intended to be served by the ReportingServer, not accessed directly from the filesystem. """ # HTML reporting settings. # html_dir: Where the report files go. # template_dir: Where to find mustache templates. Settings = namedtuple( 'Settings', Reporter.Settings._fields + ('html_dir', 'template_dir')) def __init__(self, run_tracker, settings): Reporter.__init__(self, run_tracker, settings) # The main report, and associated tool outputs, go under this dir. self._html_dir = settings.html_dir # We render HTML from mustache templates. self._renderer = MustacheRenderer( Renderer(search_dirs=settings.template_dir)) # We serve files relative to the build root. self._buildroot = get_buildroot() self._html_path_base = os.path.relpath(self._html_dir, self._buildroot) # We write the main report body to this file object. self._report_file = None # We redirect stdout, stderr etc. of tool invocations to these files. self._output_files = {} # path -> fileobj. def report_path(self): """The path to the main report file.""" return os.path.join(self._html_dir, 'build.html') def open(self): """Implementation of Reporter callback.""" safe_mkdir(os.path.dirname(self._html_dir)) self._report_file = open(self.report_path(), 'w') def close(self): """Implementation of Reporter callback.""" self._report_file.close() for f in self._output_files.values(): f.close() def start_workunit(self, workunit): """Implementation of Reporter callback.""" # We use these properties of the workunit to decide how to render information about it. is_tool = workunit.has_label(WorkUnit.TOOL) is_multitool = workunit.has_label(WorkUnit.MULTITOOL) is_test = workunit.has_label(WorkUnit.TEST) # Get useful properties from the workunit. workunit_dict = workunit.to_dict() if workunit_dict['cmd']: workunit_dict['cmd'] = linkify( self._buildroot, workunit_dict['cmd'].replace('$', '\\\\$')) # Create the template arguments. args = { 'indent': len(workunit.ancestors()) * 10, 'html_path_base': self._html_path_base, 'workunit': workunit_dict, 'header_text': workunit.name, 'initially_open': is_test or not (is_tool or is_multitool), 'is_tool': is_tool, 'is_multitool': is_multitool } args.update({ 'collapsible': lambda x: self._renderer.render_callable('collapsible', x, args) }) # Render the workunit's div. s = self._renderer.render_name('workunit_start', args) if is_tool: # This workunit is a tool invocation, so render the appropriate content. # We use the same args, slightly modified. del args['initially_open'] if is_test: # Have test framework stdout open by default, but not that of other tools. # This is an arbitrary choice, but one that turns out to be useful to users in practice. args['stdout_initially_open'] = True s += self._renderer.render_name('tool_invocation_start', args) # ... and we're done. self._emit(s) # CSS classes from pants.css that we use to style the header text to reflect the outcome. _outcome_css_classes = [ 'aborted', 'failure', 'warning', 'success', 'unknown' ] def end_workunit(self, workunit): """Implementation of Reporter callback.""" # Create the template arguments. duration = workunit.duration() timing = '%.3f' % duration unaccounted_time_secs = workunit.unaccounted_time() unaccounted_time = '%.3f' % unaccounted_time_secs \ if unaccounted_time_secs >= 1 and unaccounted_time_secs > 0.05 * duration \ else None args = { 'workunit': workunit.to_dict(), 'status': workunit.choose(*HtmlReporter._outcome_css_classes), 'timing': timing, 'unaccounted_time': unaccounted_time, 'aborted': workunit.outcome() == WorkUnit.ABORTED } s = '' if workunit.has_label(WorkUnit.TOOL): s += self._renderer.render_name('tool_invocation_end', args) s += self._renderer.render_name('workunit_end', args) self._emit(s) # Update the timings. def render_timings(timings): timings_dict = timings.get_all() for item in timings_dict: item['timing_string'] = '%.3f' % item['timing'] args = {'timings': timings_dict} return self._renderer.render_name('aggregated_timings', args) self._overwrite('cumulative_timings', render_timings(self.run_tracker.cumulative_timings)) self._overwrite('self_timings', render_timings(self.run_tracker.self_timings)) # Update the artifact cache stats. def render_cache_stats(artifact_cache_stats): def fix_detail_id(e, _id): return e if isinstance(e, basestring) else e + (_id, ) msg_elements = [] for cache_name, stat in artifact_cache_stats.stats_per_cache.items( ): msg_elements.extend([ cache_name + ' artifact cache: ', # Explicitly set the detail ids, so their displayed/hidden state survives a refresh. fix_detail_id( items_to_report_element(stat.hit_targets, 'hit'), 'cache-hit-details'), ', ', fix_detail_id( items_to_report_element(stat.miss_targets, 'miss'), 'cache-miss-details'), '.' ]) if not msg_elements: msg_elements = ['No artifact cache use.'] return self._render_message(*msg_elements) self._overwrite( 'artifact_cache_stats', render_cache_stats(self.run_tracker.artifact_cache_stats)) def handle_output(self, workunit, label, s): """Implementation of Reporter callback.""" if os.path.exists( self._html_dir ): # Make sure we're not immediately after a clean-all. path = os.path.join(self._html_dir, '%s.%s' % (workunit.id, label)) if path not in self._output_files: f = open(path, 'w') self._output_files[path] = f else: f = self._output_files[path] f.write(self._htmlify_text(s)) # We must flush in the same thread as the write. f.flush() def do_handle_log(self, workunit, level, *msg_elements): """Implementation of Reporter callback.""" content = self._render_message(*msg_elements) # Generate some javascript that appends the content to the workunit's div. args = { 'content_id': uuid.uuid4(), # Identifies this content. 'workunit_id': workunit.id, # The workunit this reporting content belongs to. 'content': content, # The content to append. } s = self._renderer.render_name('append_to_workunit', args) # Emit that javascript to the main report body. self._emit(s) def _render_message(self, *msg_elements): elements = [] detail_ids = [] for element in msg_elements: # Each element can be a message or a (message, detail) pair, as received by handle_log(). # # However, as an internal implementation detail, we also allow an element to be a tuple # (message, detail, detail_initially_visible[, detail_id]) # # - If the detail exists, clicking on the text will toggle display of the detail and close # all other details in this message. # - If detail_initially_visible is True, the detail will be displayed by default. # # Toggling is managed via detail_ids: when clicking on a detail, it closes all details # in this message with detail_ids different than that of the one being clicked on. # We allow detail_id to be explicitly specified, so that the open/closed state can be # preserved through refreshes. For example, when looking at the artifact cache stats, # if "hits" are open and "misses" are closed, we want to remember that even after # the cache stats are updated and the message re-rendered. if isinstance(element, basestring): element = [element] defaults = ('', None, None, False) # Map assumes None for missing values, so this will pick the default for those. (text, detail, detail_id, detail_initially_visible) = \ map(lambda x, y: x or y, element, defaults) element_args = {'text': self._htmlify_text(text)} if detail is not None: detail_id = detail_id or uuid.uuid4() detail_ids.append(detail_id) element_args.update({ 'detail': self._htmlify_text(detail), 'detail_initially_visible': detail_initially_visible, 'detail-id': detail_id }) elements.append(element_args) args = {'elements': elements, 'all-detail-ids': detail_ids} return self._renderer.render_name('message', args) def _emit(self, s): """Append content to the main report file.""" if os.path.exists( self._html_dir ): # Make sure we're not immediately after a clean-all. self._report_file.write(s) self._report_file.flush( ) # We must flush in the same thread as the write. def _overwrite(self, filename, s): """Overwrite a file with the specified contents.""" if os.path.exists( self._html_dir ): # Make sure we're not immediately after a clean-all. with open(os.path.join(self._html_dir, filename), 'w') as f: f.write(s) def _htmlify_text(self, s): """Make text HTML-friendly.""" colored = self._handle_ansi_color_codes(cgi.escape(str(s))) return linkify(self._buildroot, colored).replace('\n', '</br>') _ANSI_COLOR_CODE_RE = re.compile(r'\033\[((\d|;)*)m') def _handle_ansi_color_codes(self, s): """Replace ansi color sequences with spans of appropriately named css classes.""" def ansi_code_to_css(code): return ' '.join(['ansi-%s' % c for c in code.split(';')]) return '<span>' +\ HtmlReporter._ANSI_COLOR_CODE_RE.sub( lambda m: '</span><span class="%s">' % ansi_code_to_css(m.group(1)), s) +\ '</span>'
class HtmlReporter(Reporter): """HTML reporting to files. The files are intended to be served by the ReportingServer, not accessed directly from the filesystem. """ # HTML reporting settings. # html_dir: Where the report files go. # template_dir: Where to find mustache templates. Settings = namedtuple('Settings', Reporter.Settings._fields + ('html_dir', 'template_dir')) def __init__(self, run_tracker, settings): Reporter.__init__(self, run_tracker, settings) # The main report, and associated tool outputs, go under this dir. self._html_dir = settings.html_dir # We render HTML from mustache templates. self._renderer = MustacheRenderer(settings.template_dir, __name__) # We serve files relative to the build root. self._buildroot = get_buildroot() self._html_path_base = os.path.relpath(self._html_dir, self._buildroot) # We write the main report body to this file object. self._report_file = None # We redirect stdout, stderr etc. of tool invocations to these files. self._output_files = defaultdict(dict) # workunit_id -> {path -> fileobj}. def report_path(self): """The path to the main report file.""" return os.path.join(self._html_dir, 'build.html') def open(self): """Implementation of Reporter callback.""" safe_mkdir(os.path.dirname(self._html_dir)) self._report_file = open(self.report_path(), 'w') def close(self): """Implementation of Reporter callback.""" self._report_file.close() # Make sure everything's closed. for files in self._output_files.values(): for f in files.values(): f.close() def start_workunit(self, workunit): """Implementation of Reporter callback.""" # We use these properties of the workunit to decide how to render information about it. is_bootstrap = workunit.has_label(WorkUnit.BOOTSTRAP) is_tool = workunit.has_label(WorkUnit.TOOL) is_multitool = workunit.has_label(WorkUnit.MULTITOOL) is_test = workunit.has_label(WorkUnit.TEST) # Get useful properties from the workunit. workunit_dict = workunit.to_dict() if workunit_dict['cmd']: workunit_dict['cmd'] = linkify(self._buildroot, workunit_dict['cmd'].replace('$', '\\\\$')) # Create the template arguments. args = { 'indent': len(workunit.ancestors()) * 10, 'html_path_base': self._html_path_base, 'workunit': workunit_dict, 'header_text': workunit.name, 'initially_open': is_test or not (is_bootstrap or is_tool or is_multitool), 'is_tool': is_tool, 'is_multitool': is_multitool } args.update({ 'collapsible': lambda x: self._renderer.render_callable('collapsible', x, args) }) # Render the workunit's div. s = self._renderer.render_name('workunit_start', args) if is_tool: # This workunit is a tool invocation, so render the appropriate content. # We use the same args, slightly modified. del args['initially_open'] if is_test: # Have test framework stdout open by default, but not that of other tools. # This is an arbitrary choice, but one that turns out to be useful to users in practice. args['stdout_initially_open'] = True s += self._renderer.render_name('tool_invocation_start', args) # ... and we're done. self._emit(s) # CSS classes from pants.css that we use to style the header text to reflect the outcome. _outcome_css_classes = ['aborted', 'failure', 'warning', 'success', 'unknown'] def end_workunit(self, workunit): """Implementation of Reporter callback.""" # Create the template arguments. duration = workunit.duration() timing = '%.3f' % duration unaccounted_time = None # Background work may be idle a lot, no point in reporting that as unaccounted. if self.is_under_main_root(workunit): unaccounted_time_secs = workunit.unaccounted_time() if unaccounted_time_secs >= 1 and unaccounted_time_secs > 0.05 * duration: unaccounted_time = '%.3f' % unaccounted_time_secs args = { 'workunit': workunit.to_dict(), 'status': workunit.choose(*HtmlReporter._outcome_css_classes), 'timing': timing, 'unaccounted_time': unaccounted_time, 'aborted': workunit.outcome() == WorkUnit.ABORTED } s = '' if workunit.has_label(WorkUnit.TOOL): s += self._renderer.render_name('tool_invocation_end', args) s += self._renderer.render_name('workunit_end', args) self._emit(s) # Update the timings. def render_timings(timings): timings_dict = timings.get_all() for item in timings_dict: item['timing_string'] = '%.3f' % item['timing'] args = { 'timings': timings_dict } return self._renderer.render_name('aggregated_timings', args) self._overwrite('cumulative_timings', render_timings(self.run_tracker.cumulative_timings)) self._overwrite('self_timings', render_timings(self.run_tracker.self_timings)) # Update the artifact cache stats. def render_cache_stats(artifact_cache_stats): def fix_detail_id(e, _id): return e if isinstance(e, basestring) else e + (_id, ) msg_elements = [] for cache_name, stat in artifact_cache_stats.stats_per_cache.items(): msg_elements.extend([ cache_name + ' artifact cache: ', # Explicitly set the detail ids, so their displayed/hidden state survives a refresh. fix_detail_id(items_to_report_element(stat.hit_targets, 'hit'), 'cache-hit-details'), ', ', fix_detail_id(items_to_report_element(stat.miss_targets, 'miss'), 'cache-miss-details'), '.' ]) if not msg_elements: msg_elements = ['No artifact cache use.'] return self._render_message(*msg_elements) self._overwrite('artifact_cache_stats', render_cache_stats(self.run_tracker.artifact_cache_stats)) for f in self._output_files[workunit.id].values(): f.close() def handle_output(self, workunit, label, s): """Implementation of Reporter callback.""" if os.path.exists(self._html_dir): # Make sure we're not immediately after a clean-all. path = os.path.join(self._html_dir, '%s.%s' % (workunit.id, label)) output_files = self._output_files[workunit.id] if path not in output_files: f = open(path, 'w') output_files[path] = f else: f = output_files[path] f.write(self._htmlify_text(s)) # We must flush in the same thread as the write. f.flush() _log_level_css_map = { Report.FATAL: 'fatal', Report.ERROR: 'error', Report.WARN: 'warn', Report.INFO: 'info', Report.DEBUG: 'debug' } def do_handle_log(self, workunit, level, *msg_elements): """Implementation of Reporter callback.""" content = '<span class="%s">%s</span>' % \ (HtmlReporter._log_level_css_map[level], self._render_message(*msg_elements)) # Generate some javascript that appends the content to the workunit's div. args = { 'content_id': uuid.uuid4(), # Identifies this content. 'workunit_id': workunit.id, # The workunit this reporting content belongs to. 'content': content, # The content to append. } s = self._renderer.render_name('append_to_workunit', args) # Emit that javascript to the main report body. self._emit(s) def _render_message(self, *msg_elements): elements = [] detail_ids = [] for element in msg_elements: # Each element can be a message or a (message, detail) pair, as received by handle_log(). # # However, as an internal implementation detail, we also allow an element to be a tuple # (message, detail, detail_initially_visible[, detail_id]) # # - If the detail exists, clicking on the text will toggle display of the detail and close # all other details in this message. # - If detail_initially_visible is True, the detail will be displayed by default. # # Toggling is managed via detail_ids: when clicking on a detail, it closes all details # in this message with detail_ids different than that of the one being clicked on. # We allow detail_id to be explicitly specified, so that the open/closed state can be # preserved through refreshes. For example, when looking at the artifact cache stats, # if "hits" are open and "misses" are closed, we want to remember that even after # the cache stats are updated and the message re-rendered. if isinstance(element, basestring): element = [element] defaults = ('', None, None, False) # Map assumes None for missing values, so this will pick the default for those. (text, detail, detail_id, detail_initially_visible) = \ map(lambda x, y: x or y, element, defaults) element_args = {'text': self._htmlify_text(text) } if detail is not None: detail_id = detail_id or uuid.uuid4() detail_ids.append(detail_id) element_args.update({ 'detail': self._htmlify_text(detail), 'detail_initially_visible': detail_initially_visible, 'detail-id': detail_id }) elements.append(element_args) args = { 'elements': elements, 'all-detail-ids': detail_ids } return self._renderer.render_name('message', args) def _emit(self, s): """Append content to the main report file.""" if os.path.exists(self._html_dir): # Make sure we're not immediately after a clean-all. self._report_file.write(s) self._report_file.flush() # We must flush in the same thread as the write. def _overwrite(self, filename, s): """Overwrite a file with the specified contents.""" if os.path.exists(self._html_dir): # Make sure we're not immediately after a clean-all. with open(os.path.join(self._html_dir, filename), 'w') as f: f.write(s) def _htmlify_text(self, s): """Make text HTML-friendly.""" colored = self._handle_ansi_color_codes(cgi.escape(str(s))) return linkify(self._buildroot, colored).replace('\n', '</br>') _ANSI_COLOR_CODE_RE = re.compile(r'\033\[((\d|;)*)m') def _handle_ansi_color_codes(self, s): """Replace ansi color sequences with spans of appropriately named css classes.""" def ansi_code_to_css(code): return ' '.join(['ansi-%s' % c for c in code.split(';')]) return '<span>' +\ HtmlReporter._ANSI_COLOR_CODE_RE.sub( lambda m: '</span><span class="%s">' % ansi_code_to_css(m.group(1)), s) +\ '</span>'
def __init__(self, **kwargs): dict.__init__(self, MustacheRenderer.expand(kwargs))
class HtmlReporter(Reporter): """HTML reporting to files. The files are intended to be served by the ReportingServer, not accessed directly from the filesystem. """ # HTML reporting settings. # html_dir: Where the report files go. # template_dir: Where to find mustache templates. Settings = namedtuple("Settings", Reporter.Settings._fields + ("html_dir", "template_dir")) def __init__(self, run_tracker, settings): Reporter.__init__(self, run_tracker, settings) # The main report, and associated tool outputs, go under this dir. self._html_dir = settings.html_dir # We render HTML from mustache templates. self._renderer = MustacheRenderer(Renderer(search_dirs=settings.template_dir)) # We serve files relative to the build root. self._buildroot = get_buildroot() self._html_path_base = os.path.relpath(self._html_dir, self._buildroot) # We write the main report body to this file object. self._report_file = None # We redirect stdout, stderr etc. of tool invocations to these files. self._output_files = defaultdict(dict) # workunit_id -> {path -> fileobj}. def report_path(self): """The path to the main report file.""" return os.path.join(self._html_dir, "build.html") def open(self): """Implementation of Reporter callback.""" safe_mkdir(os.path.dirname(self._html_dir)) self._report_file = open(self.report_path(), "w") def close(self): """Implementation of Reporter callback.""" self._report_file.close() # Make sure everything's closed. for files in self._output_files.values(): for f in files.values(): f.close() def start_workunit(self, workunit): """Implementation of Reporter callback.""" # We use these properties of the workunit to decide how to render information about it. is_tool = workunit.has_label(WorkUnit.TOOL) is_multitool = workunit.has_label(WorkUnit.MULTITOOL) is_test = workunit.has_label(WorkUnit.TEST) # Get useful properties from the workunit. workunit_dict = workunit.to_dict() if workunit_dict["cmd"]: workunit_dict["cmd"] = linkify(self._buildroot, workunit_dict["cmd"].replace("$", "\\\\$")) # Create the template arguments. args = { "indent": len(workunit.ancestors()) * 10, "html_path_base": self._html_path_base, "workunit": workunit_dict, "header_text": workunit.name, "initially_open": is_test or not (is_tool or is_multitool), "is_tool": is_tool, "is_multitool": is_multitool, } args.update({"collapsible": lambda x: self._renderer.render_callable("collapsible", x, args)}) # Render the workunit's div. s = self._renderer.render_name("workunit_start", args) if is_tool: # This workunit is a tool invocation, so render the appropriate content. # We use the same args, slightly modified. del args["initially_open"] if is_test: # Have test framework stdout open by default, but not that of other tools. # This is an arbitrary choice, but one that turns out to be useful to users in practice. args["stdout_initially_open"] = True s += self._renderer.render_name("tool_invocation_start", args) # ... and we're done. self._emit(s) # CSS classes from pants.css that we use to style the header text to reflect the outcome. _outcome_css_classes = ["aborted", "failure", "warning", "success", "unknown"] def end_workunit(self, workunit): """Implementation of Reporter callback.""" # Create the template arguments. duration = workunit.duration() timing = "%.3f" % duration unaccounted_time = None # Background work may be idle a lot, no point in reporting that as unaccounted. if self.is_under_main_root(workunit): unaccounted_time_secs = workunit.unaccounted_time() if unaccounted_time_secs >= 1 and unaccounted_time_secs > 0.05 * duration: unaccounted_time = "%.3f" % unaccounted_time_secs args = { "workunit": workunit.to_dict(), "status": workunit.choose(*HtmlReporter._outcome_css_classes), "timing": timing, "unaccounted_time": unaccounted_time, "aborted": workunit.outcome() == WorkUnit.ABORTED, } s = "" if workunit.has_label(WorkUnit.TOOL): s += self._renderer.render_name("tool_invocation_end", args) s += self._renderer.render_name("workunit_end", args) self._emit(s) # Update the timings. def render_timings(timings): timings_dict = timings.get_all() for item in timings_dict: item["timing_string"] = "%.3f" % item["timing"] args = {"timings": timings_dict} return self._renderer.render_name("aggregated_timings", args) self._overwrite("cumulative_timings", render_timings(self.run_tracker.cumulative_timings)) self._overwrite("self_timings", render_timings(self.run_tracker.self_timings)) # Update the artifact cache stats. def render_cache_stats(artifact_cache_stats): def fix_detail_id(e, _id): return e if isinstance(e, basestring) else e + (_id,) msg_elements = [] for cache_name, stat in artifact_cache_stats.stats_per_cache.items(): msg_elements.extend( [ cache_name + " artifact cache: ", # Explicitly set the detail ids, so their displayed/hidden state survives a refresh. fix_detail_id(items_to_report_element(stat.hit_targets, "hit"), "cache-hit-details"), ", ", fix_detail_id(items_to_report_element(stat.miss_targets, "miss"), "cache-miss-details"), ".", ] ) if not msg_elements: msg_elements = ["No artifact cache use."] return self._render_message(*msg_elements) self._overwrite("artifact_cache_stats", render_cache_stats(self.run_tracker.artifact_cache_stats)) for f in self._output_files[workunit.id].values(): f.close() def handle_output(self, workunit, label, s): """Implementation of Reporter callback.""" if os.path.exists(self._html_dir): # Make sure we're not immediately after a clean-all. path = os.path.join(self._html_dir, "%s.%s" % (workunit.id, label)) output_files = self._output_files[workunit.id] if path not in output_files: f = open(path, "w") output_files[path] = f else: f = output_files[path] f.write(self._htmlify_text(s)) # We must flush in the same thread as the write. f.flush() def do_handle_log(self, workunit, level, *msg_elements): """Implementation of Reporter callback.""" content = self._render_message(*msg_elements) # Generate some javascript that appends the content to the workunit's div. args = { "content_id": uuid.uuid4(), # Identifies this content. "workunit_id": workunit.id, # The workunit this reporting content belongs to. "content": content, # The content to append. } s = self._renderer.render_name("append_to_workunit", args) # Emit that javascript to the main report body. self._emit(s) def _render_message(self, *msg_elements): elements = [] detail_ids = [] for element in msg_elements: # Each element can be a message or a (message, detail) pair, as received by handle_log(). # # However, as an internal implementation detail, we also allow an element to be a tuple # (message, detail, detail_initially_visible[, detail_id]) # # - If the detail exists, clicking on the text will toggle display of the detail and close # all other details in this message. # - If detail_initially_visible is True, the detail will be displayed by default. # # Toggling is managed via detail_ids: when clicking on a detail, it closes all details # in this message with detail_ids different than that of the one being clicked on. # We allow detail_id to be explicitly specified, so that the open/closed state can be # preserved through refreshes. For example, when looking at the artifact cache stats, # if "hits" are open and "misses" are closed, we want to remember that even after # the cache stats are updated and the message re-rendered. if isinstance(element, basestring): element = [element] defaults = ("", None, None, False) # Map assumes None for missing values, so this will pick the default for those. (text, detail, detail_id, detail_initially_visible) = map(lambda x, y: x or y, element, defaults) element_args = {"text": self._htmlify_text(text)} if detail is not None: detail_id = detail_id or uuid.uuid4() detail_ids.append(detail_id) element_args.update( { "detail": self._htmlify_text(detail), "detail_initially_visible": detail_initially_visible, "detail-id": detail_id, } ) elements.append(element_args) args = {"elements": elements, "all-detail-ids": detail_ids} return self._renderer.render_name("message", args) def _emit(self, s): """Append content to the main report file.""" if os.path.exists(self._html_dir): # Make sure we're not immediately after a clean-all. self._report_file.write(s) self._report_file.flush() # We must flush in the same thread as the write. def _overwrite(self, filename, s): """Overwrite a file with the specified contents.""" if os.path.exists(self._html_dir): # Make sure we're not immediately after a clean-all. with open(os.path.join(self._html_dir, filename), "w") as f: f.write(s) def _htmlify_text(self, s): """Make text HTML-friendly.""" colored = self._handle_ansi_color_codes(cgi.escape(str(s))) return linkify(self._buildroot, colored).replace("\n", "</br>") _ANSI_COLOR_CODE_RE = re.compile(r"\033\[((\d|;)*)m") def _handle_ansi_color_codes(self, s): """Replace ansi color sequences with spans of appropriately named css classes.""" def ansi_code_to_css(code): return " ".join(["ansi-%s" % c for c in code.split(";")]) return ( "<span>" + HtmlReporter._ANSI_COLOR_CODE_RE.sub( lambda m: '</span><span class="%s">' % ansi_code_to_css(m.group(1)), s ) + "</span>" )