Example #1
0
    def __init__(self, document_manager, registry=None, prune=False):
        """Constructor.

    Args:
      document_manager: [HtmlDocumentManager] Helps with look & feel,
         and structure.
      registry: [dict] Registry of processing methods keyed by record type
         in the journal. If not defined, then use the default.
      prune: [bool] If true, take liberties with pruning the information
         so that it is more concise and readable for a typical use case
         of verifying tests passed and investigating why they have not.
    """
        if registry is None:
            registry = {
                'JsonSnapshot': self.render_snapshot,
                'JournalContextControl': self.handle_context_control,
                'JournalMessage': self.render_message
            }

        super(HtmlRenderer, self).__init__(registry=registry)
        self.__entity_manager = ProcessedEntityManager()
        self.__document_manager = document_manager

        # The context stack is an array of array of strings,
        # Where each top level array is a context we've pushed, which
        # contains fragments of entities. If we've pushed a context, then
        # well render into the stack. Otherwise we'll render into the document.
        # When we pop a context we'll render it into the parent context until
        # we pop the root context, which will finally render into the document.
        self.__context_stack = [None]
        self.__context_stack_timestamp_prefix = [None]
        self.__prune = prune
Example #2
0
  def test_process_snapshot(self):
    """Test the conversion of a snapshot into HTML."""
    tail = TestLinkedList(name='tail')
    head = TestLinkedList(name='head', next_elem=tail)
    snapshot = JsonSnapshot()
    snapshot.make_entity_for_object(head)
    json_snapshot = snapshot.to_json_object()

    entity_manager = ProcessedEntityManager()
    processor = ProcessToRenderInfo(
        HtmlDocumentManager('test_json'), entity_manager)
    processor.max_uncollapsable_json_lines = 20
    processor.max_uncollapsable_metadata_rows = 20
    processor.max_uncollapsable_entity_rows = 20
    processor.default_force_top_level_collapse = False

    in_relation = None
    entity_manager.push_entity_map(json_snapshot['_entities'])
    html_info = processor.process_entity_id(json_snapshot['_subject_id'],
                                            json_snapshot, in_relation)
    entity_manager.pop_entity_map(json_snapshot['_entities'])

    expect = """<table>
  <tr>
    <th><i>metadata</i></th>
    <td>
      <table>
        <tr><th>_id</th><td>1</td></tr>
        <tr><th>class</th><td>type TestLinkedList</td></tr>
        <tr><th>name</th><td>head</td></tr>
      </table>
    </td>
  </tr>
  <tr>
    <th>Next</th>
    <td>
      <table>
        <tr>
          <th><i>metadata</i></th>
          <td>
            <table>
              <tr><th>_id</th><td>2</td></tr>
              <tr><th>class</th><td>type TestLinkedList</td></tr>
              <tr><th>name</th><td>tail</td></tr>
            </table>
          </td>
        </tr>
      </table>
    </td>
  </tr>
</table>
"""
    # Test without regard to whitespace formatting.
    self.assertEquals(''.join(expect.split()),
                      ''.join(str(html_info.detail_block).split()))
Example #3
0
  def __handle_generic(self, entry):
    """Handles entries from the journal to update the overall summary.

    Args:
      entry: JSON entry from the journal
    """
    # Update our running timestamps bounding overall processing time.
    timestamp = entry.get('_timestamp')
    if self.__first_timestamp is None:
      self.__first_timestamp = timestamp
    self.__last_timestamp = timestamp or self.__last_timestamp

    if entry.get('_type') == 'JsonSnapshot' and self.__depth == 0:
      # Consider root-level snapshots for overall status.
      entity_manager = ProcessedEntityManager()
      entity_manager.push_entity_map(entry.get('_entities', {}))
      relation = entity_manager.lookup_entity_with_id(
          entry.get('_subject_id')).get('_default_relation')
      self.__summary_status = self.__ingest_summary_status(
          self.__summary_status, relation)
      return

    # Look for top-level control objects that indicate tests.
    # TODO(ewiseblatt): 20160301
    # This should be formalized since the concept of a test is reasonably
    # primitive. Maybe specialize the control by citing the name of the test
    # in an attribute whose semantics indicate the context block for a test.
    if entry.get('_type') != 'JournalContextControl':
      return

    if entry.get('control') == 'BEGIN':
        # pylint: disable=bad-indentation
        self.__depth += 1
        if self.__depth == 1:
           self.__in_test = entry.get('_title', '').startswith('Test ')
        return

    if entry.get('control') == 'END':
        # pylint: disable=bad-indentation
        self.__depth -= 1
        if self.__depth == 0:
          relation = entry.get('relation')
          if self.__in_test:
            self.__increment_relation_count(relation)
          elif relation is not None:
            self.__summary_status = self.__ingest_summary_status(
              self.__summary_status, relation)
        return
Example #4
0
  def __init__(self, document_manager, registry=None, prune=False):
    """Constructor.

    Args:
      document_manager: [HtmlDocumentManager] Helps with look & feel,
         and structure.
      registry: [dict] Registry of processing methods keyed by record type
         in the journal. If not defined, then use the default.
      prune: [bool] If true, take liberties with pruning the information
         so that it is more concise and readable for a typical use case
         of verifying tests passed and investigating why they have not.
    """
    if registry is None:
      registry = {
          'JsonSnapshot': self.render_snapshot,
          'JournalContextControl': self.handle_context_control,
          'JournalMessage': self.render_message
      }

    super(HtmlRenderer, self).__init__(registry=registry)
    self.__entity_manager = ProcessedEntityManager()
    self.__document_manager = document_manager

    # The context stack is an array of array of strings,
    # Where each top level array is a context we've pushed, which
    # contains fragments of entities. If we've pushed a context, then
    # well render into the stack. Otherwise we'll render into the document.
    # When we pop a context we'll render it into the parent context until
    # we pop the root context, which will finally render into the document.
    self.__context_stack = [None]
    self.__context_stack_timestamp_prefix = [None]
    self.__prune = prune
Example #5
0
  def test_cycle(self):
    tail = TestLinkedList(name='tail')
    head = TestLinkedList(name='head', next_elem=tail)
    tail.next = head
    snapshot = JsonSnapshot()

    entity_manager = ProcessedEntityManager()
    processor = ProcessToRenderInfo(
        HtmlDocumentManager('test_json'), entity_manager)

    snapshot.make_entity_for_object(head)
    json_snapshot = snapshot.to_json_object()
    self.assertEqual(1, json_snapshot.get('_subject_id'))
    entity_manager.push_entity_map(json_snapshot.get('_entities'))

    in_relation = None
    info = processor.process_entity_id(1, snapshot, in_relation)
Example #6
0
    def test_cycle(self):
        tail = TestLinkedList(name='tail')
        head = TestLinkedList(name='head', next_elem=tail)
        tail.next = head
        snapshot = JsonSnapshot()

        entity_manager = ProcessedEntityManager()
        processor = ProcessToRenderInfo(HtmlDocumentManager('test_json'),
                                        entity_manager)

        snapshot.make_entity_for_object(head)
        json_snapshot = snapshot.to_json_object()
        self.assertEqual(1, json_snapshot.get('_subject_id'))
        entity_manager.push_entity_map(json_snapshot.get('_entities'))

        in_relation = None
        info = processor.process_entity_id(1, snapshot, in_relation)
Example #7
0
    def test_yaml(self):
        """Test rendering literal json values"""
        processor = ProcessToRenderInfo(HtmlDocumentManager('test_yaml'),
                                        ProcessedEntityManager())
        processor.max_uncollapsable_json_lines = 20
        processor.max_uncollapsable_entity_rows = 20

        # Numeric literals wont be treated as yaml
        for n in [-1, 0, 1, 3.14]:
            info = processor.process_yaml_html_if_possible(n)
            self.assertEquals('{0}'.format(n), info.detail_block)
            self.assertEquals(None, info.summary_block)

        # None of these strings are well-defined YAML documents
        # so should just be strings.
        for s in ['test', 'a phrase']:
            info = processor.process_yaml_html_if_possible(s)
            self.assertEquals('<pre>%s\n</pre>' % s, info.detail_block)
            self.assertEquals(None, info.summary_block)
        info = processor.process_yaml_html_if_possible('True')
        self.assertEquals('<pre>true\n</pre>', info.detail_block)

        # Boolean values wont be considered YAML
        for b in [True, False]:
            info = processor.process_yaml_html_if_possible(b)
            self.assertEquals('{0}'.format(str(b)), info.detail_block)
            self.assertEquals(None, info.summary_block)

        # Dictionaries and YAML dictionary strings normalize to YAML
        import yaml
        for d in [{'A': 'a', 'B': True}, 'A: a\nB: true\n']:
            info = processor.process_yaml_html_if_possible(d)
            # The eolns here show that it is being yaml formatted.
            self.assertEquals('<pre>A:a\nB:true\n</pre>',
                              str(info.detail_block).replace(' ', ''))
            self.assertEquals(None, info.summary_block)
            self.assertEquals(None, info.summary_block)

        # Lists and YAML lists strings normalize to YAML.
        for l in [[123, 'abc', True, {
                'A': 'a',
                'B': 'b'
        }], '[123, "abc", true, {"A":"a", "B":"b"}]']:
            info = processor.process_yaml_html_if_possible(l)
            self.assertEquals(
                '<pre>-123\n-abc\n-true\n-A:a\nB:b\n</pre>',
                str(info.detail_block).replace(' ', '').replace('\n', '\n'))
            self.assertEquals(None, info.summary_block)
Example #8
0
    def test_process_snapshot(self):
        """Test the conversion of a snapshot into HTML."""
        tail = TestLinkedList(name='tail')
        head = TestLinkedList(name='head', next_elem=tail)
        snapshot = JsonSnapshot()
        snapshot.make_entity_for_object(head)
        json_snapshot = snapshot.to_json_object()

        entity_manager = ProcessedEntityManager()
        processor = ProcessToRenderInfo(HtmlDocumentManager('test_json'),
                                        entity_manager)
        processor.max_uncollapsable_json_lines = 20
        processor.max_uncollapsable_metadata_rows = 20
        processor.max_uncollapsable_entity_rows = 20
        processor.default_force_top_level_collapse = False

        in_relation = None
        entity_manager.push_entity_map(json_snapshot['_entities'])
        html_info = processor.process_entity_id(json_snapshot['_subject_id'],
                                                json_snapshot, in_relation)
        entity_manager.pop_entity_map(json_snapshot['_entities'])

        expect = """<table>
  <tr>
    <th><i>metadata</i></th>
    <td>
      <table>
        <tr><th>_id</th><td>1</td></tr>
        <tr><th>class</th><td>type TestLinkedList</td></tr>
        <tr><th>name</th><td>head</td></tr>
      </table>
    </td>
  </tr>
  <tr>
    <th>Next</th>
    <td>
      <table>
        <tr>
          <th><i>metadata</i></th>
          <td>
            <table>
              <tr><th>_id</th><td>2</td></tr>
              <tr><th>class</th><td>type TestLinkedList</td></tr>
              <tr><th>name</th><td>tail</td></tr>
            </table>
          </td>
        </tr>
      </table>
    </td>
  </tr>
</table>
"""
        # Test without regard to whitespace formatting.
        self.assertEquals(''.join(expect.split()),
                          ''.join(str(html_info.detail_block).split()))
Example #9
0
    def test_json(self):
        """Test rendering literal json values"""
        processor = ProcessToRenderInfo(HtmlDocumentManager('test_json'),
                                        ProcessedEntityManager())
        processor.max_uncollapsable_json_lines = 20
        processor.max_uncollapsable_entity_rows = 20

        # Numeric literals wont be treated as json.
        for n in [-1, 0, 1, 3.14]:
            info = processor.process_json_html_if_possible(n)
            self.assertEquals('{0}'.format(n), info.detail_block)
            self.assertEquals(None, info.summary_block)

        # None of these strings are well-defined JSON documents
        # so should just be strings.
        for s in ['test', 'a phrase', 'True']:
            info = processor.process_json_html_if_possible(s)
            self.assertEquals("'{0}'".format(s), info.detail_block)
            self.assertEquals(None, info.summary_block)

        # Boolean values wont be considered JSON.
        for b in [True, False]:
            info = processor.process_json_html_if_possible(b)
            self.assertEquals('{0}'.format(str(b)), info.detail_block)
            self.assertEquals(None, info.summary_block)

        # Dictionaries and JSON dictionary strings normalize to JSON.
        for d in [{'A': 'a', 'B': True}, '{"A":"a", "B":true}']:
            info = processor.process_json_html_if_possible(d)
            # The eolns here show that it is being json formatted.
            self.assertEquals('<pre>{\n"A":"a",\n"B":true\n}</pre>',
                              str(info.detail_block).replace(' ', ''))
            self.assertEquals(None, info.summary_block)
            self.assertEquals(None, info.summary_block)

        # Lists and JSON lists strings normalize to JSON.
        for l in [[123, 'abc', True, {
                'A': 'a',
                'B': 'b'
        }], '[123, "abc", true, {"A":"a", "B":"b"}]']:
            info = processor.process_json_html_if_possible(l)
            self.assertEquals(
                '<pre>[123,"abc",true,{"A":"a","B":"b"}]</pre>',
                str(info.detail_block).replace(' ', '').replace('\n', ''))
            self.assertEquals(None, info.summary_block)
Example #10
0
class HtmlRenderer(JournalProcessor):
    """Specialized JournalProcessor to produce HTML."""
    def __init__(self, document_manager, registry=None, prune=False):
        """Constructor.

    Args:
      document_manager: [HtmlDocumentManager] Helps with look & feel,
         and structure.
      registry: [dict] Registry of processing methods keyed by record type
         in the journal. If not defined, then use the default.
      prune: [bool] If true, take liberties with pruning the information
         so that it is more concise and readable for a typical use case
         of verifying tests passed and investigating why they have not.
    """
        if registry is None:
            registry = {
                'JsonSnapshot': self.render_snapshot,
                'JournalContextControl': self.handle_context_control,
                'JournalMessage': self.render_message
            }

        super(HtmlRenderer, self).__init__(registry=registry)
        self.__entity_manager = ProcessedEntityManager()
        self.__document_manager = document_manager

        # The context stack is an array of array of strings,
        # Where each top level array is a context we've pushed, which
        # contains fragments of entities. If we've pushed a context, then
        # well render into the stack. Otherwise we'll render into the document.
        # When we pop a context we'll render it into the parent context until
        # we pop the root context, which will finally render into the document.
        self.__context_stack = [None]
        self.__context_stack_timestamp_prefix = [None]
        self.__prune = prune

    def has_context(self):
        return len(self.__context_stack) > 1

    def terminate(self):
        """Implements JournalProcessor interface."""
        if self.has_context():
            raise ValueError('Still have {0} open contexts'.format(
                len(self.__context_stack)))

    def __render_context(self, end_control, rendered_context):
        """Render the context into HTML now that we've terminated it.

    Args:
      end_control: [dict] JournalContextControl for end context record.
      rendered_context: [RenderedContext] the context we just ended.
    """
        begin_control = rendered_context.control
        if begin_control.get('_title') == 'Execute':
            # If this context is "Execute", then unwrap this top context and the
            # execute context to promote the actual thing executed to this level.
            # This is a hack, but makes the reporting more readable.
            if rendered_context.html:
                if self.has_context():
                    self.__context_stack[-1].html.extend(rendered_context.html)
            return

        # Relation here is used to indicate the test status.
        # Take that and turn it into a style.
        relation = end_control.get('relation', None)
        document_manager = self.__document_manager
        css = document_manager.determine_attribute_css_kwargs(relation)[0]
        title_html = document_manager.make_html_block(
            begin_control.get('_title', ''))
        if not rendered_context.html:
            title_html.append(
                document_manager.make_html_block(' (<i>empty</i>)'))
            self.render_log_tr(begin_control['_timestamp'],
                               None,
                               title_html,
                               css=css)
        else:
            delta_time = end_control['_timestamp'] - begin_control['_timestamp']
            if css:
                title_html = document_manager.make_tag_container(
                    'span', [title_html], **css)
            lvl = min(1, len(self.__context_stack))
            title_html = document_manager.make_tag_container(
                'context{n}'.format(n=lvl), [title_html])
            summary = document_manager.make_html_block(
                '%s <small>+%.3fs</small>' % (title_html, delta_time))
            table = document_manager.new_tag('table')

            for row in rendered_context.html:
                table.append(row)
            detail = document_manager.make_html_block(str(title_html))
            detail.append(table)
            self.render_log_tr(begin_control['_timestamp'], summary, detail)

    def handle_context_control(self, control):
        """Begin or terminate contexts."""
        direction = control['control']
        if direction == 'BEGIN':
            self.__context_stack.append(RenderedContext(control, []))
            self.__context_stack_timestamp_prefix.append(
                self.__context_stack_timestamp_prefix[-1])
        elif direction == 'END':
            context = self.__context_stack[-1]
            self.__context_stack.pop()
            self.__context_stack_timestamp_prefix.pop()
            if (not context.html and context.control.get('_title', '')
                    in ['setUp', 'tearDown']):
                return
            self.__render_context(control, context)
        else:
            raise ValueError(
                'Invalid JournalContextControl control={0}'.format(direction))

    def render_snapshot(self, snapshot):
        """Default method for rendering a JsonSnapshot into HTML."""
        subject_id = snapshot.get('_subject_id')
        entities = snapshot.get('_entities', {})
        self.__entity_manager.push_entity_map(entities)

        # This is only for the final relation.
        subject = self.__entity_manager.lookup_entity_with_id(subject_id)
        final_relation = subject.get('_default_relation')

        # Delegate the whole thing.
        document_manager = self.__document_manager
        processor = ProcessToRenderInfo(document_manager,
                                        self.__entity_manager,
                                        prune=self.__prune)

        try:
            info = processor.process_entity_id(subject_id, snapshot, None)
        finally:
            self.__entity_manager.pop_entity_map(entities)

        if not info.summary_block:
            self.render_log_tr(snapshot.get('_timestamp'), None,
                               info.detail_block)
            return

        final_css = document_manager.determine_attribute_css_kwargs(
            final_relation)[0]
        title = snapshot.get('_title', '')
        if not title and info.summary_block:
            title = '"{html}" Snapshot'.format(html=info.summary_block)
        if title:
            title_html = document_manager.make_tag_text(
                'padded', title, **final_css)

        self.render_log_tr(snapshot.get('_timestamp'),
                           title_html,
                           info.detail_block,
                           collapse_decorator=title,
                           css=final_css)
        return

    @staticmethod
    def timestamp_to_string(timestamp):
        """Return human-readable timestamp.

    Args:
      timestamp: [float]  Seconds since epoch.
    """

        millis = ('%.3f' % (timestamp - int(timestamp)))[2:]
        return datetime.datetime.fromtimestamp(timestamp).strftime(
            '%Y-%m-%d %H:%M:%S.{millis}'.format(millis=millis))

    def render_log_tr(self, timestamp, summary, detail, **kwargs):
        """Add row to accumulated output.

      timestamp: [float] The timestamp for the entry.
      summary: [string] If provided, this is an abbreviation.
      detail: [string] This is the full detail for the entry.
    """
        tag = self.render_log_tr_tag(timestamp, summary, detail, **kwargs)
        if self.has_context():
            self.__context_stack[-1].html.append(tag)
        else:
            self.__document_manager.append_tag(tag)

    def render_log_tr_tag(self,
                          timestamp,
                          summary,
                          detail,
                          collapse_decorator='',
                          css=None):
        """Render a top level entry into the log as a table row.

    Args:
      timestamp: [float] The timestamp for the entry.
      summary: [string] If provided, this is an abbreviation.
      detail: [string] This is the full detail for the entry.
    """
        document_manager = self.__document_manager
        if self.__context_stack_timestamp_prefix:
            # If the last "yyyy-mm-dd " part of the date string is the
            # same as the previous for this scoped block, then strip it out.
            # otherwise include it.
            prev_date_prefix = self.__context_stack_timestamp_prefix[-1]
            date_str = self.timestamp_to_string(timestamp)
            if prev_date_prefix and date_str.startswith(prev_date_prefix):
                date_str = date_str[11:]
            else:
                self.__context_stack_timestamp_prefix[-1] = date_str[:11]
        else:
            date_str = self.timestamp_to_string(timestamp)

        css_class = None if css is None else css.get('class_')
        td_tag_kwargs = {} if summary else {'class': css_class}
        tr_tag = document_manager.new_tag('tr')
        th_tag = document_manager.make_tag_text('th', date_str, class_='rj')
        td_tag = document_manager.new_tag('td', **td_tag_kwargs)
        tr_tag.append(th_tag)
        tr_tag.append(td_tag)

        if not summary:
            td_tag.append(detail)
            return tr_tag

        # pylint: disable=bad-continuation
        collapse_html = ('collapse {decorator}'.format(
            decorator=collapse_decorator)
                         if collapse_decorator else 'collapse')

        section_id = document_manager.new_section_id()
        show = document_manager.make_expandable_control_tag(
            section_id, 'expand')
        hide = document_manager.make_expandable_control_tag(
            section_id, collapse_html)
        detail_tag_attrs, summary_tag_attrs = (
            document_manager.make_expandable_tag_attr_kwargs_pair(
                section_id=section_id, default_expanded=False))

        show_span = document_manager.make_tag_container('span',
                                                        [show, summary],
                                                        class_=css_class,
                                                        **summary_tag_attrs)

        hide_span = document_manager.make_tag_container('span', [hide, detail],
                                                        class_=css_class,
                                                        **detail_tag_attrs)

        td_tag.append(show_span)
        td_tag.append(hide_span)
        return tr_tag

    def render_message(self, message):
        """Default method for rendering a JournalMessage into HTML."""
        text = message.get('_value').strip()

        document_manager = self.__document_manager
        processor = ProcessToRenderInfo(document_manager,
                                        self.__entity_manager,
                                        prune=self.__prune)

        html_info = HtmlInfo()
        html_format = message.get('format', None)
        if html_format == 'json':
            html_info = processor.process_json_html_if_possible(text)
        else:
            summary = None
            if text is None:
                html = document_manager.make_tag_text('i', 'Empty Message')
            elif html_format != 'pre':
                html = document_manager.make_text_block(text)
            else:
                last_offset = -1

                # pylint: disable=bad-indentation
                # The ff tag here is a custom tag for "Fixed Format"
                # It is like 'pre' but inline rather than block to be more compact.
                num_lines = text.count('\n')
                if num_lines > processor.max_uncollapsable_message_lines:
                    last_offset = text.find('\n')
                elif len(text) > processor.max_message_summary_length:
                    last_offset = processor.max_message_summary_length - 2 * len(
                        'expand')

                if last_offset >= 0:
                    summary = document_manager.make_tag_text(
                        'ff', '{0}...'.format(text[0:last_offset]))
                html = document_manager.make_tag_text('ff', text)

            html_info = HtmlInfo(detail=html, summary=summary)

        self.render_log_tr(message.get('_timestamp'), html_info.summary_block,
                           html_info.detail_block)
Example #11
0
class HtmlRenderer(JournalProcessor):
  """Specialized JournalProcessor to produce HTML."""

  def __init__(self, document_manager, registry=None, prune=False):
    """Constructor.

    Args:
      document_manager: [HtmlDocumentManager] Helps with look & feel,
         and structure.
      registry: [dict] Registry of processing methods keyed by record type
         in the journal. If not defined, then use the default.
      prune: [bool] If true, take liberties with pruning the information
         so that it is more concise and readable for a typical use case
         of verifying tests passed and investigating why they have not.
    """
    if registry is None:
      registry = {
          'JsonSnapshot': self.render_snapshot,
          'JournalContextControl': self.handle_context_control,
          'JournalMessage': self.render_message
      }

    super(HtmlRenderer, self).__init__(registry=registry)
    self.__entity_manager = ProcessedEntityManager()
    self.__document_manager = document_manager

    # The context stack is an array of array of strings,
    # Where each top level array is a context we've pushed, which
    # contains fragments of entities. If we've pushed a context, then
    # well render into the stack. Otherwise we'll render into the document.
    # When we pop a context we'll render it into the parent context until
    # we pop the root context, which will finally render into the document.
    self.__context_stack = [None]
    self.__context_stack_timestamp_prefix = [None]
    self.__prune = prune

  def has_context(self):
    return len(self.__context_stack) > 1

  def terminate(self):
    """Implements JournalProcessor interface."""
    if self.has_context():
      raise ValueError(
          'Still have {0} open contexts'.format(len(self.__context_stack)))

  def __render_context(self, end_control, rendered_context):
    """Render the context into HTML now that we've terminated it.

    Args:
      end_control: [dict] JournalContextControl for end context record.
      rendered_context: [RenderedContext] the context we just ended.
    """
    begin_control = rendered_context.control
    if begin_control.get('_title') == 'Execute':
      # If this context is "Execute", then unwrap this top context and the
      # execute context to promote the actual thing executed to this level.
      # This is a hack, but makes the reporting more readable.
      if rendered_context.html:
        if self.has_context():
          self.__context_stack[-1].html.extend(rendered_context.html)
      return

    # Relation here is used to indicate the test status.
    # Take that and turn it into a style.
    relation = end_control.get('relation', None)
    document_manager = self.__document_manager
    css = document_manager.determine_attribute_css_kwargs(relation)[0]
    title_html = document_manager.make_html_block(
        begin_control.get('_title', ''))
    if not rendered_context.html:
      title_html.append(document_manager.make_html_block(' (<i>empty</i>)'))
      self.render_log_tr(begin_control['_timestamp'], None,
                         title_html, css=css)
    else:
      delta_time = end_control['_timestamp'] - begin_control['_timestamp']
      if css:
        title_html = document_manager.make_tag_container(
            'span', [title_html], **css)
      lvl = min(1, len(self.__context_stack))
      title_html = document_manager.make_tag_container(
          'context{n}'.format(n=lvl), [title_html])
      summary = document_manager.make_html_block(
          '%s <small>+%.3fs</small>' % (title_html, delta_time))
      table = document_manager.new_tag('table')

      for row in rendered_context.html:
        table.append(row)
      detail = document_manager.make_html_block(str(title_html))
      detail.append(table)
      self.render_log_tr(begin_control['_timestamp'], summary, detail)

  def handle_context_control(self, control):
    """Begin or terminate contexts."""
    direction = control['control']
    if direction == 'BEGIN':
      self.__context_stack.append(RenderedContext(control, []))
      self.__context_stack_timestamp_prefix.append(
          self.__context_stack_timestamp_prefix[-1])
    elif direction == 'END':
      context = self.__context_stack[-1]
      self.__context_stack.pop()
      self.__context_stack_timestamp_prefix.pop()
      if (not context.html
          and context.control.get('_title', '') in ['setUp', 'tearDown']):
        return
      self.__render_context(control, context)
    else:
      raise ValueError(
          'Invalid JournalContextControl control={0}'.format(direction))

  def render_snapshot(self, snapshot):
    """Default method for rendering a JsonSnapshot into HTML."""
    subject_id = snapshot.get('_subject_id')
    entities = snapshot.get('_entities', {})
    self.__entity_manager.push_entity_map(entities)

    # This is only for the final relation.
    subject = self.__entity_manager.lookup_entity_with_id(subject_id)
    final_relation = subject.get('_default_relation')

    # Delegate the whole thing.
    document_manager = self.__document_manager
    processor = ProcessToRenderInfo(document_manager, self.__entity_manager,
                                    prune=self.__prune)

    try:
      info = processor.process_entity_id(subject_id, snapshot, None)
    finally:
      self.__entity_manager.pop_entity_map(entities)

    if not info.summary_block:
      self.render_log_tr(snapshot.get('_timestamp'), None, info.detail_block)
      return

    final_css = document_manager.determine_attribute_css_kwargs(
        final_relation)[0]
    title = snapshot.get('_title', '')
    if not title and info.summary_block:
      title = '"{html}" Snapshot'.format(html=info.summary_block)
    if title:
      title_html = document_manager.make_tag_text('padded', title, **final_css)

    self.render_log_tr(snapshot.get('_timestamp'),
                       title_html, info.detail_block, collapse_decorator=title,
                       css=final_css)
    return

  @staticmethod
  def timestamp_to_string(timestamp):
    """Return human-readable timestamp.

    Args:
      timestamp: [float]  Seconds since epoch.
    """

    millis = ('%.3f' % (timestamp - int(timestamp)))[2:]
    return datetime.datetime.fromtimestamp(timestamp).strftime(
        '%Y-%m-%d %H:%M:%S.{millis}'.format(millis=millis))

  def render_log_tr(self, timestamp, summary, detail, **kwargs):
    """Add row to accumulated output.

      timestamp: [float] The timestamp for the entry.
      summary: [string] If provided, this is an abbreviation.
      detail: [string] This is the full detail for the entry.
    """
    tag = self.render_log_tr_tag(timestamp, summary, detail, **kwargs)
    if self.has_context():
      self.__context_stack[-1].html.append(tag)
    else:
      self.__document_manager.append_tag(tag)

  def render_log_tr_tag(self, timestamp, summary, detail,
                        collapse_decorator='', css=None):
    """Render a top level entry into the log as a table row.

    Args:
      timestamp: [float] The timestamp for the entry.
      summary: [string] If provided, this is an abbreviation.
      detail: [string] This is the full detail for the entry.
    """
    document_manager = self.__document_manager
    if self.__context_stack_timestamp_prefix:
      # If the last "yyyy-mm-dd " part of the date string is the
      # same as the previous for this scoped block, then strip it out.
      # otherwise include it.
      prev_date_prefix = self.__context_stack_timestamp_prefix[-1]
      date_str = self.timestamp_to_string(timestamp)
      if prev_date_prefix and date_str.startswith(prev_date_prefix):
        date_str = date_str[11:]
      else:
        self.__context_stack_timestamp_prefix[-1] = date_str[:11]
    else:
      date_str = self.timestamp_to_string(timestamp)

    css_class = None if css is None else css.get('class_')
    td_tag_kwargs = {} if summary else {'class': css_class}
    tr_tag = document_manager.new_tag('tr')
    th_tag = document_manager.make_tag_text('th', date_str, class_='rj')
    td_tag = document_manager.new_tag('td', **td_tag_kwargs)
    tr_tag.append(th_tag)
    tr_tag.append(td_tag)

    if not summary:
      td_tag.append(detail)
      return tr_tag

    # pylint: disable=bad-continuation
    collapse_html = ('collapse {decorator}'
                         .format(decorator=collapse_decorator)
                     if collapse_decorator
                     else 'collapse')

    section_id = document_manager.new_section_id()
    show = document_manager.make_expandable_control_tag(
        section_id, 'expand')
    hide = document_manager.make_expandable_control_tag(
        section_id, collapse_html)
    detail_tag_attrs, summary_tag_attrs = (
        document_manager.make_expandable_tag_attr_kwargs_pair(
            section_id=section_id, default_expanded=False))

    show_span = document_manager.make_tag_container(
        'span', [show, summary], class_=css_class, **summary_tag_attrs)

    hide_span = document_manager.make_tag_container(
          'span', [hide, detail], class_=css_class, **detail_tag_attrs)

    td_tag.append(show_span)
    td_tag.append(hide_span)
    return tr_tag

  def render_message(self, message):
    """Default method for rendering a JournalMessage into HTML."""
    text = message.get('_value').strip()

    document_manager = self.__document_manager
    processor = ProcessToRenderInfo(document_manager, self.__entity_manager,
                                    prune=self.__prune)

    html_info = HtmlInfo()
    html_format = message.get('format', None)
    if html_format == 'json':
      html_info = processor.process_json_html_if_possible(text)
    elif html_format == 'yaml':
      html_info = processor.process_yaml_html_if_possible(text)
    else:
      summary = None
      if text is None:
        html = document_manager.make_tag_text('i', 'Empty Message')
      elif html_format != 'pre':
        html = document_manager.make_text_block(text)
      else:
        last_offset = -1

        # pylint: disable=bad-indentation
        # The ff tag here is a custom tag for "Fixed Format"
        # It is like 'pre' but inline rather than block to be more compact.
        num_lines = text.count('\n')
        if num_lines > processor.max_uncollapsable_message_lines:
          last_offset = text.find('\n')
        elif len(text) > processor.max_message_summary_length:
          last_offset = processor.max_message_summary_length - 2*len('expand')

        if last_offset >= 0:
          summary = document_manager.make_tag_text(
              'ff', '{0}...'.format(text[0:last_offset]))
        html = document_manager.make_tag_text('ff', text)

      html_info = HtmlInfo(detail=html, summary=summary)

    self.render_log_tr(message.get('_timestamp'),
                       html_info.summary_block, html_info.detail_block)
Example #12
0
  def __handle_generic(self, entry):
    """Handles entries from the journal to update the overall summary.

    Args:
      entry: JSON entry from the journal
    """
    if entry.get('_type') == 'JsonSnapshot' and self.__depth == 0:
      # Consider root-level snapshots for overall status.
      entity_manager = ProcessedEntityManager()
      entity_manager.push_entity_map(entry.get('_entities', {}))
      relation = entity_manager.lookup_entity_with_id(
          entry.get('_subject_id')).get('_default_relation')
      self.__summary_status = self.__ingest_summary_status(
          self.__summary_status, relation)
      return

    # Look for top-level control objects that indicate tests.
    # TODO(ewiseblatt): 20160301
    # This should be formalized since the concept of a test is reasonably
    # primitive. Maybe specialize the control by citing the name of the test
    # in an attribute whose semantics indicate the context block for a test.
    if entry.get('_type') != 'JournalContextControl':
      return

    if entry.get('control') == 'BEGIN':
        # pylint: disable=bad-indentation
        self.__depth += 1
        if self.__depth == 1:
           self.__in_test = entry.get('_title', '')
           if not self.__in_test.startswith('Test '):
             self.__in_test = None
           self.__start_timestamp = entry.get('_timestamp')
        return

    if entry.get('control') == 'END':
        # pylint: disable=bad-indentation
        self.__depth -= 1
        if self.__depth == 0:
          relation = entry.get('relation')
          if not self.__in_test:
            if relation is not None:
              self.__summary_status = self.__ingest_summary_status(
                  self.__summary_status, relation)
            return

          passed = 0
          failed = 0
          error = 0
          end_timestamp = entry.get('_timestamp')
          secs = (end_timestamp - self.__start_timestamp
                  if end_timestamp and self.__start_timestamp
                  else 0)
          self.__summary_status = self.__ingest_summary_status(
              self.__summary_status, relation)

          if relation == 'VALID':
            passed = 1
          elif relation == 'INVALID':
            failed = 1
          elif relation == 'ERROR':
            error = 1
          else:
            raise ValueError('Unhandled relation {0}'.format(relation))

          test_stats = TestStats(passed, failed, error, secs)
          self.__stats.aggregate(test_stats)
          if self.__in_test:
            self.__detail_stats[self.__in_test] = test_stats
            self.__in_test = None
        return