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 @@