[IMP] runbot: allow pseudo markdown in log messages

With this commit, a kind of markdown is allowed in log messages and
build.description.
Following patterns are supported:
  **strong**
  ~~striketrough~~
  __underline__
`code`
 [link](target)
 @icon-font-awesome-class
This commit is contained in:
Christophe Monniez 2020-02-26 15:13:51 +01:00 committed by XavierDo
parent 3a428d4877
commit 3b00d2576c
8 changed files with 119 additions and 15 deletions

View File

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
import contextlib
import fcntl
import itertools
import logging
import os
@ -14,6 +13,7 @@ from collections import OrderedDict
from datetime import timedelta
from babel.dates import format_timedelta
from werkzeug import utils
from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
@ -122,3 +122,25 @@ def list_local_dbs(additionnal_conditions=None):
%s
""" % additionnal_condition_str)
return [d[0] for d in local_cr.fetchall()]
def pseudo_markdown(text):
text = utils.escape(text)
patterns = {
r'\*\*(.+?)\*\*': '<strong>\g<1></strong>',
r'~~(.+?)~~': '<del>\g<1></del>', # it's not official markdown but who cares
r'__(.+?)__': '<ins>\g<1></ins>', # same here, maybe we should change the method name
r'`(.+?)`': '<code>\g<1></code>',
}
for p, b in patterns.items():
text = re.sub(p, b, text, flags=re.DOTALL)
# icons
re_icon = re.compile(r'@icon-([a-z0-9-]+)')
text = re_icon.sub('<i class="fa fa-\g<1>"></i>', text)
# links
re_links = re.compile(r'\[(.+?)\]\((.+?)\)')
text = re_links.sub('<a href="\g<2>">\g<1></a>', text)
return text

View File

@ -8,7 +8,7 @@ import shutil
import subprocess
import time
import datetime
from ..common import dt2time, fqdn, now, grep, local_pgadmin_cursor, s2human, Commit, dest_reg, os, list_local_dbs
from ..common import dt2time, fqdn, now, grep, local_pgadmin_cursor, s2human, Commit, dest_reg, os, list_local_dbs, pseudo_markdown
from ..container import docker_build, docker_stop, docker_state, Command
from ..fields import JsonDictField
from odoo.addons.runbot.models.repo import RunbotException
@ -65,6 +65,7 @@ class runbot_build(models.Model):
repo_id = fields.Many2one(related='branch_id.repo_id', readonly=True, store=True)
name = fields.Char('Revno', required=True)
description = fields.Char('Description', help='Informative description')
md_description = fields.Char(compute='_compute_md_description', String='MD Parsed Description', help='Informative description mardown parsed')
host = fields.Char('Host')
port = fields.Integer('Port')
dest = fields.Char(compute='_compute_dest', type='char', string='Dest', readonly=1, store=True)
@ -178,6 +179,11 @@ class runbot_build(models.Model):
max_days += int(build.gc_delay if build.gc_delay else 0)
build.gc_date = ref_date + datetime.timedelta(days=(max_days))
@api.depends('description')
def _compute_md_description(self):
for build in self:
build.md_description = pseudo_markdown(build.description)
def _get_top_parent(self):
self.ensure_one()
build = self
@ -656,10 +662,10 @@ class runbot_build(models.Model):
if build.requested_action == 'wake_up':
if docker_state(build._get_docker_name(), build._path()) == 'RUNNING':
build.write({'requested_action': False, 'local_state': 'running'})
build._log('wake_up', 'Waking up failed, docker is already running', level='SEPARATOR')
build._log('wake_up', 'Waking up failed, **docker is already running**', type='markdown', level='SEPARATOR')
elif not os.path.exists(build._path()):
build.write({'requested_action': False, 'local_state': 'done'})
build._log('wake_up', 'Impossible to wake-up, build dir does not exists anymore', level='SEPARATOR')
build._log('wake_up', 'Impossible to wake-up, **build dir does not exists anymore**', type='markdown', level='SEPARATOR')
else:
try:
log_path = build._path('logs', 'wake_up.txt')
@ -673,7 +679,7 @@ class runbot_build(models.Model):
'local_state': 'running',
'port': port,
})
build._log('wake_up', 'Waking up build', level='SEPARATOR')
build._log('wake_up', '**Waking up build**', type='markdown', level='SEPARATOR')
self.env['runbot.build.config.step']._run_odoo_run(build, log_path)
# reload_nginx will be triggered by _run_odoo_run
except Exception:

View File

@ -205,7 +205,7 @@ class ConfigStep(models.Model):
def _run(self, build):
log_path = build._path('logs', '%s.txt' % self.name)
build.write({'job_start': now(), 'job_end': False}) # state, ...
build._log('run', 'Starting step %s from config %s' % (self.name, build.config_id.name), level='SEPARATOR')
build._log('run', 'Starting step **%s** from config **%s**' % (self.name, build.config_id.name), type='markdown', level='SEPARATOR')
return self._run_step(build, log_path)
def _run_step(self, build, log_path):

View File

@ -2,11 +2,12 @@
import logging
from odoo import models, fields, api, tools
from ..common import pseudo_markdown
from odoo import models, fields, tools
_logger = logging.getLogger(__name__)
TYPES = [(t, t.capitalize()) for t in 'client server runbot subbuild link'.split()]
TYPES = [(t, t.capitalize()) for t in 'client server runbot subbuild link markdown'.split()]
class runbot_event(models.Model):
@ -71,6 +72,12 @@ FOR EACH ROW EXECUTE PROCEDURE runbot_set_logging_build();
""")
def _markdown(self):
""" Apply pseudo markdown parser for message.
"""
self.ensure_one()
return pseudo_markdown(self.message)
class RunbotErrorLog(models.Model):
_name = "runbot.error.log"

View File

@ -1,6 +1,5 @@
.separator {
border-top: 2px solid #666;
font-weight: bold;
}
[data-toggle="collapse"] .fa:before {

View File

@ -117,7 +117,7 @@
<t t-set="rowclass"><t t-call="runbot.build_class"><t t-set="build" t-value="build"/></t></t>
<td t-attf-class="bg-{{rowclass.strip()}}-light">
<t t-if="build.description">
<b>Description:</b> <t t-esc="build.description"/><br/>
<b>Description:</b> <t t-raw="build.md_description"/><br/>
</t>
<b>Subject:</b> <t t-esc="build.subject"/><br/>
<b>Author:</b> <t t-esc="build.author"/><br/>
@ -158,7 +158,7 @@
<tr t-attf-class="bg-{{rowclass.strip()}}-light"><td>
<a t-attf-href="/runbot/build/{{child.id}}" >Build <t t-esc="child.id"/></a>
<t t-if="child.description">
<t t-esc="child.description" />
<t t-raw="child.md_description" />
</t>
<t t-else="">
with config <t t-esc="child.config_id.name"/>
@ -199,13 +199,13 @@
<t t-set="subbuild" t-value="(([child for child in build.real_build.children_ids if child.id == int(l.path)] if l.type == 'subbuild' else False) or [build.browse()])[0]"/>
<t t-set="logclass" t-value="dict(CRITICAL='danger', ERROR='danger', WARNING='warning', OK='success', SEPARATOR='separator').get(l.level)"/>
<tr t-attf-class="'bg-%s-light' % {{logclass}} if {{logclass}} != 'separator' else {{logclass}}">
<td style="white-space: nowrap; width:1%;"><t t-esc="l.create_date"/></td>
<td style="white-space: nowrap; width:1%;"><b t-if="l.level != 'SEPARATOR' and l.type != 'link'" t-esc="l.level"/></td>
<td style="white-space: nowrap; width:1%;"><t t-if="l.level != 'SEPARATOR' and l.type != 'link'" t-esc="l.type"/></td>
<td style="white-space: nowrap; width:1%;"><t t-esc="l.create_date.strftime('%Y-%m-%d %H:%M:%S')"/></td>
<td style="white-space: nowrap; width:1%;"><b t-if="l.level != 'SEPARATOR' and l.type not in ['link', 'markdown']" t-esc="l.level"/></td>
<td style="white-space: nowrap; width:1%;"><t t-if="l.level != 'SEPARATOR' and l.type not in ['link', 'markdown']" t-esc="l.type"/></td>
<t t-set="message_class" t-value="''"/>
<t t-if="subbuild" t-set="message_class"><t t-call="runbot.build_class"><t t-set="build" t-value="subbuild"/></t></t>
<td t-attf-class="bg-{{message_class.strip() or logclass}}-light">
<t t-if="l.type not in ('runbot', 'link')">
<t t-if="l.type not in ('runbot', 'link', 'markdown')">
<t t-if="l.type == 'subbuild'">
<a t-attf-href="/runbot/build/{{l.path}}">Build #<t t-esc="l.path"/></a>
</t>
@ -220,6 +220,7 @@
<t t-esc="message[0]"/><a t-attf-href="{{l.path}}"><t t-esc="message[1]"/></a><t t-esc="message[2]"/>
</t>
</t>
<t t-elif="l.type == 'markdown'" t-raw="l._markdown()"/>
<t t-else="">
<t t-if="'\n' not in l.message" t-esc="l.message"/>
<pre t-if="'\n' in l.message" style="margin:0;padding:0; border: none;"><t t-esc="l.message"/></pre>

View File

@ -92,6 +92,18 @@ class Test_Build(RunbotCase):
build.env.cr.execute("SELECT config_data, config_data->'test_write' AS written, config_data->'test_build' AS test_build FROM runbot_build WHERE id = %s", [build.id])
self.assertEqual([({'test_write': 'written', 'test_build': 'foo'}, 'written', 'foo')], self.env.cr.fetchall())
def test_markdown_description(self):
build = self.create_build({
'branch_id': self.branch.id,
'name': 'd0d0caca0000ffffffffffffffffffffffffffff',
'port': '1234',
'description': 'A nice **description**'
})
self.assertEqual(build.md_description, 'A nice <strong>description</strong>')
build.description = "<script>console.log('foo')</script>"
self.assertEqual(build.md_description, "&lt;script&gt;console.log('foo')&lt;/script&gt;")
def test_config_data_duplicate(self):
build = self.create_build({

View File

@ -68,3 +68,60 @@ class TestIrLogging(RunbotCase):
build._log('runbot function', 'runbot message')
log_lines = self.IrLogging.search([('type', '=', 'runbot'), ('name', '=', 'odoo.runbot'), ('func', '=', 'runbot function'), ('message', '=', 'runbot message'), ('level', '=', 'INFO')])
self.assertEqual(len(log_lines), 1, '_log should be able to add logs from the runbot')
def test_markdown(self):
log = self.IrLogging.create({
'name': 'odoo.runbot',
'type': 'runbot',
'path': 'runbot',
'level': 'INFO',
'line': 0,
'func': 'test_markdown',
'message': 'some **bold text** and also some __underlined text__ and maybe a bit of ~~strikethrough text~~'
})
self.assertEqual(
log._markdown(),
'some <strong>bold text</strong> and also some <ins>underlined text</ins> and maybe a bit of <del>strikethrough text</del>'
)
log.message = 'a bit of code `import foo\nfoo.bar`'
self.assertEqual(
log._markdown(),
'a bit of code <code>import foo\nfoo.bar</code>'
)
# test icon
log.message = 'Hello @icon-file-text-o'
self.assertEqual(
log._markdown(),
'Hello <i class="fa fa-file-text-o"></i>'
)
# test links
log.message = 'This [link](https://wwww.somewhere.com) goes to somewhere and [this one](http://www.nowhere.com) to nowhere.'
self.assertEqual(
log._markdown(),
'This <a href="https://wwww.somewhere.com">link</a> goes to somewhere and <a href="http://www.nowhere.com">this one</a> to nowhere.'
)
# test link with icon
log.message = '[@icon-download](https://wwww.somewhere.com) goes to somewhere.'
self.assertEqual(
log._markdown(),
'<a href="https://wwww.somewhere.com"><i class="fa fa-download"></i></a> goes to somewhere.'
)
# test links with icon and text
log.message = 'This [link@icon-download](https://wwww.somewhere.com) goes to somewhere.'
self.assertEqual(
log._markdown(),
'This <a href="https://wwww.somewhere.com">link<i class="fa fa-download"></i></a> goes to somewhere.'
)
# test sanitization
log.message = 'foo <script>console.log("hello world")</script>'
self.assertEqual(
log._markdown(),
'foo &lt;script&gt;console.log(&quot;hello world&quot;)&lt;/script&gt;'
)