diff --git a/runbot/__manifest__.py b/runbot/__manifest__.py
index 2b8c49b1..2617aefc 100644
--- a/runbot/__manifest__.py
+++ b/runbot/__manifest__.py
@@ -6,7 +6,7 @@
'author': "Odoo SA",
'website': "http://runbot.odoo.com",
'category': 'Website',
- 'version': '5.1',
+ 'version': '5.2',
'application': True,
'depends': ['base', 'base_automation', 'website'],
'data': [
diff --git a/runbot/common.py b/runbot/common.py
index bf5b80a9..150c3b4b 100644
--- a/runbot/common.py
+++ b/runbot/common.py
@@ -108,6 +108,16 @@ def local_pgadmin_cursor():
if cnx:
cnx.close()
+@contextlib.contextmanager
+def local_pg_cursor(db_name):
+ cnx = None
+ try:
+ cnx = psycopg2.connect(f"dbname={db_name}")
+ yield cnx.cursor()
+ finally:
+ if cnx:
+ cnx.commit()
+ cnx.close()
def list_local_dbs(additionnal_conditions=None):
additionnal_condition_str = ''
diff --git a/runbot/data/runbot_data.xml b/runbot/data/runbot_data.xml
index 09c24578..9440f7e1 100644
--- a/runbot/data/runbot_data.xml
+++ b/runbot/data/runbot_data.xml
@@ -41,6 +41,11 @@
admin_passwd=running_master_password
+
+ runbot.logdb_name
+ runbot_logs
+
+
diff --git a/runbot/migrations/15.0.5.2/pre-migration.py b/runbot/migrations/15.0.5.2/pre-migration.py
new file mode 100644
index 00000000..9ec802b0
--- /dev/null
+++ b/runbot/migrations/15.0.5.2/pre-migration.py
@@ -0,0 +1,3 @@
+def migrate(cr, version):
+ cr.execute("DROP TRIGGER IF EXISTS runbot_new_logging ON ir_logging")
+ cr.execute("DROP FUNCTION IF EXISTS runbot_set_logging_build")
diff --git a/runbot/models/build.py b/runbot/models/build.py
index af7f9a6c..a8b2ba7f 100644
--- a/runbot/models/build.py
+++ b/runbot/models/build.py
@@ -529,7 +529,8 @@ class BuildResult(models.Model):
for _id in self.exists().ids:
additionnal_conditions.append("datname like '%s-%%'" % _id)
- existing_db = list_local_dbs(additionnal_conditions=additionnal_conditions)
+ log_db = self.env['ir.config_parameter'].get_param('runbot.logdb_name')
+ existing_db = [db for db in list_local_dbs(additionnal_conditions=additionnal_conditions) if db != log_db]
for db, _ in _filter(dest_list=existing_db, label='db'):
self._logger('Removing database')
@@ -662,6 +663,8 @@ class BuildResult(models.Model):
def _schedule(self):
"""schedule the build"""
icp = self.env['ir.config_parameter'].sudo()
+ hosts_by_name = {h.name: h for h in self.env['runbot.host'].search([('name', 'in', self.mapped('host'))])}
+ hosts_by_build = {b.id: hosts_by_name[b.host] for b in self}
for build in self:
if build.local_state not in ['testing', 'running']:
raise UserError("Build %s is not testing/running: %s" % (build.id, build.local_state))
@@ -689,6 +692,8 @@ class BuildResult(models.Model):
continue
else:
build._log('_schedule', 'Docker with state %s not started after 60 seconds, skipping' % _docker_state, level='ERROR')
+ if hosts_by_build[build.id]._fetch_local_logs(build_ids=build.ids):
+ continue # avoid to make results with remaining logs
# No job running, make result and select nex job
build_values = {
'job_end': now(),
@@ -1015,11 +1020,7 @@ class BuildResult(models.Model):
command.add_config_tuple("xmlrpc_interface", "127.0.0.1")
if grep(config_path, "log-db"):
- logdb_uri = self.env['ir.config_parameter'].get_param('runbot.runbot_logdb_uri')
- logdb = self.env.cr.dbname
- if logdb_uri: # this looks useless
- logdb = '%s' % logdb_uri
- command.add_config_tuple("log_db", "%s" % logdb)
+ command.add_config_tuple("log_db", "runbot_logs")
if grep(config_path, 'log-db-level'):
command.add_config_tuple("log_db_level", '25')
diff --git a/runbot/models/event.py b/runbot/models/event.py
index 01269e32..2e91a3c9 100644
--- a/runbot/models/event.py
+++ b/runbot/models/event.py
@@ -5,7 +5,7 @@ import logging
from collections import defaultdict
from ..common import pseudo_markdown
-from odoo import models, fields, tools
+from odoo import models, fields, tools, api
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
@@ -24,59 +24,22 @@ class runbot_event(models.Model):
error_id = fields.Many2one('runbot.build.error', compute='_compute_known_error') # remember to never store this field
dbname = fields.Char(string='Database Name', index=False)
+ @api.model_create_multi
+ def create(self, vals_list):
+ logs_by_build_id = defaultdict(list)
+ for log in vals_list:
+ if 'build_id' in log:
+ logs_by_build_id[log['build_id']].append(log)
- def init(self):
- parent_class = super(runbot_event, self)
- if hasattr(parent_class, 'init'):
- parent_class.init()
-
- self._cr.execute("""
-CREATE OR REPLACE FUNCTION runbot_set_logging_build() RETURNS TRIGGER AS $runbot_set_logging_build$
-BEGIN
- IF (NEW.build_id IS NULL AND NEW.dbname IS NOT NULL AND NEW.dbname != current_database()) THEN
- NEW.build_id := split_part(NEW.dbname, '-', 1)::integer;
- SELECT active_step INTO NEW.active_step_id FROM runbot_build WHERE runbot_build.id = NEW.build_id;
- END IF;
- IF (NEW.build_id IS NOT NULL) AND (NEW.type = 'server') THEN
- DECLARE
- counter INTEGER;
- BEGIN
- UPDATE runbot_build b
- SET log_counter = log_counter - 1
- WHERE b.id = NEW.build_id;
- SELECT log_counter
- INTO counter
- FROM runbot_build
- WHERE runbot_build.id = NEW.build_id;
- IF (counter = 0) THEN
- NEW.message = 'Log limit reached (full logs are still available in the log file)';
- NEW.level = 'SEPARATOR';
- NEW.func = '';
- NEW.type = 'runbot';
- RETURN NEW;
- ELSIF (counter < 0) THEN
- RETURN NULL;
- END IF;
- END;
- END IF;
- IF (NEW.build_id IS NOT NULL AND UPPER(NEW.level) NOT IN ('INFO', 'SEPARATOR')) THEN
- BEGIN
- UPDATE runbot_build b
- SET triggered_result = CASE WHEN UPPER(NEW.level) = 'WARNING' THEN 'warn'
- ELSE 'ko'
- END
- WHERE b.id = NEW.build_id;
- END;
- END IF;
-RETURN NEW;
-END;
-$runbot_set_logging_build$ language plpgsql;
-
-DROP TRIGGER IF EXISTS runbot_new_logging ON ir_logging;
-CREATE TRIGGER runbot_new_logging BEFORE INSERT ON ir_logging
-FOR EACH ROW EXECUTE PROCEDURE runbot_set_logging_build();
-
- """)
+ builds = self.env['runbot.build'].browse(logs_by_build_id.keys())
+ for build in builds:
+ build_logs = logs_by_build_id[build.id]
+ for ir_log in build_logs:
+ if ir_log['level'].upper() == 'WARNING':
+ build.triggered_result = 'warn'
+ elif ir_log['level'].upper() == 'ERROR':
+ build.triggered_result = 'ko'
+ return super().create(vals_list)
def _markdown(self):
""" Apply pseudo markdown parser for message.
diff --git a/runbot/models/host.py b/runbot/models/host.py
index 8c879a11..8e480d9b 100644
--- a/runbot/models/host.py
+++ b/runbot/models/host.py
@@ -1,13 +1,18 @@
import logging
import getpass
+
+from collections import defaultdict
+
from odoo import models, fields, api
from odoo.tools import config
-from ..common import fqdn, local_pgadmin_cursor, os
+from ..common import fqdn, local_pgadmin_cursor, os, list_local_dbs, local_pg_cursor
from ..container import docker_build
+
_logger = logging.getLogger(__name__)
forced_host_name = None
+
class Host(models.Model):
_name = 'runbot.host'
_description = "Host"
@@ -52,6 +57,31 @@ class Host(models.Model):
values['disp_name'] = values['name']
return super().create(values)
+ def _bootstrap_local_logs_db(self):
+ """ bootstrap a local database that will collect logs from builds """
+ logs_db_name = self.env['ir.config_parameter'].get_param('runbot.logdb_name')
+ if logs_db_name not in list_local_dbs():
+ _logger.info('Logging database %s not found. Creating it ...', logs_db_name)
+ with local_pgadmin_cursor() as local_cr:
+ local_cr.execute(f"""CREATE DATABASE "{logs_db_name}" TEMPLATE template0 LC_COLLATE 'C' ENCODING 'unicode'""")
+ try:
+ with local_pg_cursor(logs_db_name) as local_cr:
+ # create_date, type, dbname, name, level, message, path, line, func
+ local_cr.execute("""CREATE TABLE ir_logging (
+ id bigserial NOT NULL,
+ create_date timestamp without time zone,
+ name character varying NOT NULL,
+ level character varying,
+ dbname character varying,
+ func character varying NOT NULL,
+ path character varying NOT NULL,
+ line character varying NOT NULL,
+ type character varying NOT NULL,
+ message text NOT NULL);
+ """)
+ except Exception as e:
+ _logger.exception('Failed to create local logs database: %s', e)
+
def _bootstrap_db_template(self):
""" boostrap template database if needed """
icp = self.env['ir.config_parameter']
@@ -72,6 +102,7 @@ class Host(models.Model):
for dir, path in static_dirs.items():
os.makedirs(path, exist_ok=True)
self._bootstrap_db_template()
+ self._bootstrap_local_logs_db()
def _docker_build(self):
""" build docker images needed by locally pending builds"""
@@ -144,3 +175,66 @@ class Host(models.Model):
nb_reserved = self.env['runbot.host'].search_count([('assigned_only', '=', True)])
if nb_reserved < (nb_hosts / 2):
self.assigned_only = True
+
+ def _fetch_local_logs(self, build_ids=None):
+ """ fetch build logs from local database """
+ logs_db_name = self.env['ir.config_parameter'].get_param('runbot.logdb_name')
+ try:
+ with local_pg_cursor(logs_db_name) as local_cr:
+ res = []
+ where_clause = 'WHERE build_id IN (%s)' if build_ids else ''
+ query = f"""
+ SELECT *
+ FROM (
+ SELECT id, create_date, name, level, dbname, func, path, line, type, message, split_part(dbname, '-', 1)::integer as build_id
+ FROM ir_logging
+ )
+ AS ir_logs
+ {where_clause}
+ ORDER BY id
+ """
+ local_cr.execute(query, build_ids)
+ col_names = [col.name for col in local_cr.description]
+ for row in local_cr.fetchall():
+ res.append({name:value for name,value in zip(col_names, row)})
+ return res
+ except Exception:
+ return []
+
+ def process_logs(self, build_ids=None):
+ """move logs from host to the leader"""
+ ir_logs = self._fetch_local_logs()
+ logs_by_build_id = defaultdict(list)
+
+ for log in ir_logs:
+ logs_by_build_id[int(log['dbname'].split('-', maxsplit=1)[0])].append(log)
+
+ builds = self.env['runbot.build'].browse(logs_by_build_id.keys())
+
+ logs_to_send = []
+ local_log_ids = []
+ for build in builds.exists():
+ build_logs = logs_by_build_id[build.id]
+ for ir_log in build_logs:
+ local_log_ids.append(ir_log['id'])
+ ir_log['active_step_id'] = build.active_step.id
+ ir_log['type'] = 'server'
+ build.log_counter -= 1
+ build.flush()
+ if build.log_counter == 0:
+ ir_log['level'] = 'SEPARATOR'
+ ir_log['func'] = ''
+ ir_log['type'] = 'runbot'
+ ir_log['message'] = 'Log limit reached (full logs are still available in the log file)'
+ elif build.log_counter < 0:
+ continue
+ ir_log['build_id'] = build.id
+ logs_to_send.append({k:ir_log[k] for k in ir_log if k != 'id'})
+
+ if logs_to_send:
+ self.env['ir.logging'].create(logs_to_send)
+ self.env.cr.commit() # we don't want to remove local logs that were not inserted in main runbot db
+ if local_log_ids:
+ logs_db_name = self.env['ir.config_parameter'].get_param('runbot.logdb_name')
+ with local_pg_cursor(logs_db_name) as local_cr:
+ local_cr.execute("DELETE FROM ir_logging WHERE id in %s", [tuple(local_log_ids)])
diff --git a/runbot/models/res_config_settings.py b/runbot/models/res_config_settings.py
index c3605ac8..a7de98cb 100644
--- a/runbot/models/res_config_settings.py
+++ b/runbot/models/res_config_settings.py
@@ -16,8 +16,7 @@ class ResConfigSettings(models.TransientModel):
runbot_timeout = fields.Integer('Max allowed step timeout (in seconds)')
runbot_starting_port = fields.Integer('Starting port for running builds')
runbot_max_age = fields.Integer('Max commit age (in days)')
- runbot_logdb_uri = fields.Char('Runbot URI for build logs',
- help='postgres://user:password@host/db formated uri to give to a build to log in database. Should be a user with limited access rights (ir_logging, runbot_build)')
+ runbot_logdb_name = fields.Char('Local Logs DB name', default='runbot_logs', config_parameter='runbot.logdb_name')
runbot_update_frequency = fields.Integer('Update frequency (in seconds)')
runbot_template = fields.Char('Postgresql template', help="Postgresql template to use when creating DB's")
runbot_message = fields.Text('Frontend warning message', help="Will be displayed on the frontend when not empty")
@@ -62,7 +61,6 @@ class ResConfigSettings(models.TransientModel):
runbot_timeout=int(get_param('runbot.runbot_timeout', default=10000)),
runbot_starting_port=int(get_param('runbot.runbot_starting_port', default=2000)),
runbot_max_age=int(get_param('runbot.runbot_max_age', default=30)),
- runbot_logdb_uri=get_param('runbot.runbot_logdb_uri', default=False),
runbot_update_frequency=int(get_param('runbot.runbot_update_frequency', default=10)),
runbot_template=get_param('runbot.runbot_db_template'),
runbot_message=get_param('runbot.runbot_message', default=''),
@@ -84,7 +82,6 @@ class ResConfigSettings(models.TransientModel):
set_param("runbot.runbot_timeout", self.runbot_timeout)
set_param("runbot.runbot_starting_port", self.runbot_starting_port)
set_param("runbot.runbot_max_age", self.runbot_max_age)
- set_param("runbot.runbot_logdb_uri", self.runbot_logdb_uri)
set_param('runbot.runbot_update_frequency', self.runbot_update_frequency)
set_param('runbot.runbot_db_template', self.runbot_template)
set_param('runbot.runbot_message', self.runbot_message)
diff --git a/runbot/models/runbot.py b/runbot/models/runbot.py
index 487ea669..04d0405f 100644
--- a/runbot/models/runbot.py
+++ b/runbot/models/runbot.py
@@ -42,6 +42,8 @@ class Runbot(models.AbstractModel):
for build in self._get_builds_with_requested_actions(host):
build._process_requested_actions()
self._commit()
+ host.process_logs()
+ self._commit()
for build in self._get_builds_to_schedule(host):
build._schedule()
self._commit()
diff --git a/runbot/tests/__init__.py b/runbot/tests/__init__.py
index 810e0b47..188387ca 100644
--- a/runbot/tests/__init__.py
+++ b/runbot/tests/__init__.py
@@ -14,3 +14,4 @@ from . import test_runbot
from . import test_commit
from . import test_upgrade
from . import test_dockerfile
+from . import test_host
diff --git a/runbot/tests/common.py b/runbot/tests/common.py
index 456795f3..67b86c5a 100644
--- a/runbot/tests/common.py
+++ b/runbot/tests/common.py
@@ -172,6 +172,7 @@ class RunbotCase(TransactionCase):
self.start_patcher('makedirs', 'odoo.addons.runbot.common.os.makedirs', True)
self.start_patcher('mkdir', 'odoo.addons.runbot.common.os.mkdir', True)
self.start_patcher('local_pgadmin_cursor', 'odoo.addons.runbot.common.local_pgadmin_cursor', False) # avoid to create databases
+ self.start_patcher('host_local_pg_cursor', 'odoo.addons.runbot.models.host.local_pg_cursor')
self.start_patcher('isdir', 'odoo.addons.runbot.common.os.path.isdir', True)
self.start_patcher('isfile', 'odoo.addons.runbot.common.os.path.isfile', True)
self.start_patcher('docker_run', 'odoo.addons.runbot.container._docker_run')
diff --git a/runbot/tests/test_build.py b/runbot/tests/test_build.py
index 851edb8f..d1fa191a 100644
--- a/runbot/tests/test_build.py
+++ b/runbot/tests/test_build.py
@@ -211,15 +211,12 @@ class TestBuildResult(RunbotCase):
self.assertEqual(modules_to_test, sorted(['good_module', 'bad_module', 'other_good', 'l10n_be', 'hwgood', 'hw_explicit', 'other_mod_1', 'other_mod_2']))
def test_build_cmd_log_db(self, ):
- """ test that the logdb connection URI is taken from the .odoorc file """
- uri = 'postgres://someone:pass@somewhere.com/db'
- self.env['ir.config_parameter'].sudo().set_param("runbot.runbot_logdb_uri", uri)
-
+ """ test that the log_db parameter is set in the .odoorc file """
build = self.Build.create({
'params_id': self.server_params.id,
})
cmd = build._cmd(py_version=3)
- self.assertIn('log_db = %s' % uri, cmd.get_config())
+ self.assertIn('log_db = runbot_logs', cmd.get_config())
def test_build_cmd_server_path_no_dep(self):
""" test that the server path and addons path """
@@ -242,7 +239,8 @@ class TestBuildResult(RunbotCase):
'/tmp/runbot_test/static/sources/addons/d0d0caca0000ffffffffffffffffffffffffffff/requirements.txt',
'/tmp/runbot_test/static/sources/server/dfdfcfcf0000ffffffffffffffffffffffffffff/requirements.txt',
'/tmp/runbot_test/static/sources/server/dfdfcfcf0000ffffffffffffffffffffffffffff/server.py',
- '/tmp/runbot_test/static/sources/server/dfdfcfcf0000ffffffffffffffffffffffffffff/openerp/tools/config.py'
+ '/tmp/runbot_test/static/sources/server/dfdfcfcf0000ffffffffffffffffffffffffffff/openerp/tools/config.py',
+ '/tmp/runbot_test/static/sources/server/dfdfcfcf0000ffffffffffffffffffffffffffff/openerp/sql_db.py'
])
if file == '/tmp/runbot_test/static/sources/addons/d0d0caca0000ffffffffffffffffffffffffffff/requirements.txt':
return False
diff --git a/runbot/tests/test_cron.py b/runbot/tests/test_cron.py
index de9a52b7..378ff737 100644
--- a/runbot/tests/test_cron.py
+++ b/runbot/tests/test_cron.py
@@ -15,6 +15,7 @@ class TestCron(RunbotCase):
def setUp(self):
super(TestCron, self).setUp()
+ self.start_patcher('list_local_dbs_patcher', 'odoo.addons.runbot.models.host.list_local_dbs', ['runbot_logs'])
self.start_patcher('_get_cron_period', 'odoo.addons.runbot.models.runbot.Runbot._get_cron_period', 2)
@patch('time.sleep', side_effect=sleep)
diff --git a/runbot/tests/test_event.py b/runbot/tests/test_event.py
index fb945196..a969741b 100644
--- a/runbot/tests/test_event.py
+++ b/runbot/tests/test_event.py
@@ -4,57 +4,6 @@ from .common import RunbotCase
class TestIrLogging(RunbotCase):
- def simulate_log(self, build, func, message, level='INFO'):
- """ simulate ir_logging from an external build """
- dest = '%s-fake-dest' % build.id
- val = ('server', dest, 'test', level, message, 'test', '0', func)
- self.cr.execute("""
- INSERT INTO ir_logging(create_date, type, dbname, name, level, message, path, line, func)
- VALUES (NOW() at time zone 'UTC', %s, %s, %s, %s, %s, %s, %s, %s)
- """, val)
-
- def test_ir_logging(self):
- build = self.Build.create({
- 'active_step': self.env.ref('runbot.runbot_build_config_step_test_all').id,
- 'params_id': self.base_params.id,
- })
-
- build.log_counter = 10
-
- # Test that an ir_logging is created and a the trigger set the build_id
- self.simulate_log(build, 'test function', 'test message')
- log_line = self.env['ir.logging'].search([('func', '=', 'test function'), ('message', '=', 'test message'), ('level', '=', 'INFO')])
- self.assertEqual(len(log_line), 1, "A build log event should have been created")
- self.assertEqual(log_line.build_id, build)
- self.assertEqual(log_line.active_step_id, self.env.ref('runbot.runbot_build_config_step_test_all'), 'The active step should be set on the log line')
-
- # Test that a warn log line sets the build in warn
- self.simulate_log(build, 'test function', 'test message', level='WARNING')
- build.invalidate_cache()
- self.assertEqual(build.triggered_result, 'warn', 'A warning log should sets the build in warn')
-
- # Test that a error log line sets the build in ko
- self.simulate_log(build, 'test function', 'test message', level='ERROR')
- build.invalidate_cache()
- self.assertEqual(build.triggered_result, 'ko', 'An error log should sets the build in ko')
- self.assertEqual(7, build.log_counter, 'server lines should decrement the build log_counter')
-
- build.log_counter = 10
-
- # Test the log limit
- for i in range(11):
- self.simulate_log(build, 'limit function', 'limit message')
- log_lines = self.env['ir.logging'].search([('build_id', '=', build.id), ('type', '=', 'server'), ('func', '=', 'limit function'), ('message', '=', 'limit message'), ('level', '=', 'INFO')])
- self.assertGreater(len(log_lines), 7, 'Trigger should have created logs with appropriate build id')
- self.assertLess(len(log_lines), 10, 'Trigger should prevent insert more lines of logs than log_counter')
- last_log_line = self.env['ir.logging'].search([('build_id', '=', build.id)], order='id DESC', limit=1)
- self.assertIn('Log limit reached', last_log_line.message, 'Trigger should modify last log message')
-
- # Test that the _log method is still able to add logs
- build._log('runbot function', 'runbot message')
- log_lines = self.env['ir.logging'].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.env['ir.logging'].create({
'name': 'odoo.runbot',
diff --git a/runbot/tests/test_host.py b/runbot/tests/test_host.py
new file mode 100644
index 00000000..fb839910
--- /dev/null
+++ b/runbot/tests/test_host.py
@@ -0,0 +1,113 @@
+import logging
+
+from .common import RunbotCase
+
+from datetime import datetime, timedelta
+
+_logger = logging.getLogger(__name__)
+
+def fetch_local_logs_return_value(nb_logs=10, message='', log_type='server', level='INFO', build_dest='1234567-master-all'):
+
+ log_date = datetime(2022, 8, 17, 21, 55)
+ logs = []
+ for i in range(nb_logs):
+ logs += [{
+ 'id': i,
+ 'create_date': log_date,
+ 'name': 'odoo.modules.loading',
+ 'level': level,
+ 'dbname': build_dest,
+ 'func': 'runbot',
+ 'path': '/data/build/odoo/odoo/netsvc.py',
+ 'line': '274',
+ 'type': log_type,
+ 'message': '75 modules loaded in 0.92s, 717 queries (+1 extra)' if message == '' else message,
+ }]
+ log_date += timedelta(seconds=20)
+ return logs
+
+class TestHost(RunbotCase):
+
+ def setUp(self):
+ super().setUp()
+ self.test_host = self.env['runbot.host'].create({'name': 'test_host'})
+ self.server_commit = self.Commit.create({
+ 'name': 'dfdfcfcf0000ffffffffffffffffffffffffffff',
+ 'repo_id': self.repo_server.id
+ })
+
+ self.addons_commit = self.Commit.create({
+ 'name': 'd0d0caca0000ffffffffffffffffffffffffffff',
+ 'repo_id': self.repo_addons.id,
+ })
+
+ self.server_params = self.base_params.copy({'commit_link_ids': [
+ (0, 0, {'commit_id': self.server_commit.id})
+ ]})
+
+ self.addons_params = self.base_params.copy({'commit_link_ids': [
+ (0, 0, {'commit_id': self.server_commit.id}),
+ (0, 0, {'commit_id': self.addons_commit.id})
+ ]})
+
+ self.start_patcher('find_patcher', 'odoo.addons.runbot.common.find', 0)
+ self.start_patcher('host_bootstrap', 'odoo.addons.runbot.models.host.Host._bootstrap', None)
+
+ def test_build_logs(self):
+
+ build = self.Build.create({
+ 'params_id': self.server_params.id,
+ 'port': '1234567',
+ 'active_step': self.env.ref('runbot.runbot_build_config_step_test_all').id,
+ 'log_counter': 20,
+ })
+
+ # check that local logs are inserted in leader ir.logging
+ logs = fetch_local_logs_return_value(build_dest=build.dest)
+ self.start_patcher('fetch_local_logs', 'odoo.addons.runbot.models.host.Host._fetch_local_logs', logs)
+ self.test_host.process_logs()
+ self.patchers['host_local_pg_cursor'].assert_called()
+ self.assertEqual(
+ self.env['ir.logging'].search_count([
+ ('build_id', '=', build.id),
+ ('active_step_id', '=', self.env.ref('runbot.runbot_build_config_step_test_all').id)
+ ]),
+ 10,
+ )
+
+ # check that a warn log sets the build in warning
+ logs = fetch_local_logs_return_value(nb_logs=1, build_dest=build.dest, level='WARNING')
+ self.patchers['fetch_local_logs'].return_value = logs
+ self.test_host.process_logs()
+ self.patchers['host_local_pg_cursor'].assert_called()
+ self.assertEqual(
+ self.env['ir.logging'].search_count([
+ ('build_id', '=', build.id),
+ ('active_step_id', '=', self.env.ref('runbot.runbot_build_config_step_test_all').id),
+ ('level', '=', 'WARNING')
+ ]),
+ 1,
+ )
+ self.assertEqual(build.triggered_result, 'warn', 'A warning log should sets the build in warn')
+
+ # now check that error logs sets the build in ko
+ logs = fetch_local_logs_return_value(nb_logs=1, build_dest=build.dest, level='ERROR')
+ self.patchers['fetch_local_logs'].return_value = logs
+ self.test_host.process_logs()
+ self.patchers['host_local_pg_cursor'].assert_called()
+ self.assertEqual(
+ self.env['ir.logging'].search_count([
+ ('build_id', '=', build.id),
+ ('active_step_id', '=', self.env.ref('runbot.runbot_build_config_step_test_all').id),
+ ('level', '=', 'ERROR')
+ ]),
+ 1,
+ )
+ self.assertEqual(build.triggered_result, 'ko', 'An error log should sets the build in ko')
+
+ build.log_counter = 10
+ # Test log limit
+ logs = fetch_local_logs_return_value(nb_logs=11, message='test log limit', build_dest=build.dest)
+ self.patchers['fetch_local_logs'].return_value = logs
+ self.test_host.process_logs()
+ self.patchers['host_local_pg_cursor'].assert_called()
diff --git a/runbot/tests/test_schedule.py b/runbot/tests/test_schedule.py
index 467ae509..ba9dcddb 100644
--- a/runbot/tests/test_schedule.py
+++ b/runbot/tests/test_schedule.py
@@ -18,10 +18,13 @@ class TestSchedule(RunbotCase):
'project_id': self.project,
'config_id': self.env.ref('runbot.runbot_build_config_default').id,
})
+
+ host = self.env['runbot.host'].create({'name': 'runbotxx'}) # the host needs to exists in _schedule()
+
build = self.Build.create({
'local_state': 'testing',
'port': '1234',
- 'host': 'runbotxx',
+ 'host': host.name,
'job_start': datetime.datetime.now(),
'active_step': self.env.ref('runbot.runbot_build_config_step_run').id,
'params_id': params.id
@@ -31,6 +34,7 @@ class TestSchedule(RunbotCase):
build._schedule() # too fast, docker not started
self.assertEqual(build.local_state, 'testing')
+ self.start_patcher('fetch_local_logs', 'odoo.addons.runbot.models.host.Host._fetch_local_logs', []) # the local logs have to be empty
build.write({'job_start': datetime.datetime.now() - datetime.timedelta(seconds=70)}) # docker never started
build._schedule()
self.assertEqual(build.local_state, 'done')
diff --git a/runbot/tests/test_upgrade.py b/runbot/tests/test_upgrade.py
index c8a1cffb..e8c9ce21 100644
--- a/runbot/tests/test_upgrade.py
+++ b/runbot/tests/test_upgrade.py
@@ -273,6 +273,7 @@ class TestUpgradeFlow(RunbotCase):
host = self.env['runbot.host']._get_current()
upgrade_current_build.host = host.name
upgrade_current_build._init_pendings(host)
+ self.start_patcher('fetch_local_logs', 'odoo.addons.runbot.models.host.Host._fetch_local_logs', []) # the local logs have to be empty
upgrade_current_build._schedule()
self.assertEqual(upgrade_current_build.local_state, 'done')
self.assertEqual(len(upgrade_current_build.children_ids), 4)
diff --git a/runbot/views/res_config_settings_views.xml b/runbot/views/res_config_settings_views.xml
index 1a9e5fb1..3712b3fe 100644
--- a/runbot/views/res_config_settings_views.xml
+++ b/runbot/views/res_config_settings_views.xml
@@ -46,6 +46,8 @@
+
+
@@ -62,8 +64,6 @@
-
-
diff --git a/runbot_builder/builder.py b/runbot_builder/builder.py
index 8000a481..d6aa0c78 100755
--- a/runbot_builder/builder.py
+++ b/runbot_builder/builder.py
@@ -1,9 +1,11 @@
#!/usr/bin/python3
-from tools import RunbotClient, run
import logging
+from tools import RunbotClient, run
+
_logger = logging.getLogger(__name__)
+
class BuilderClient(RunbotClient):
def on_start(self):