diff --git a/runbot/common.py b/runbot/common.py index 328d078e..e774a193 100644 --- a/runbot/common.py +++ b/runbot/common.py @@ -70,8 +70,8 @@ def rfind(filename, pattern): if os.path.isfile(filename): regexp = re.compile(pattern, re.M) with file_open(filename, 'r') as f: - if regexp.findall(f.read()): - return True + result = regexp.findall(f.read()) + return result or False return False diff --git a/runbot/models/build_config.py b/runbot/models/build_config.py index d7826114..c7922111 100644 --- a/runbot/models/build_config.py +++ b/runbot/models/build_config.py @@ -23,7 +23,6 @@ _SAFE_OPCODES |= set(to_opcodes(['LOAD_DEREF', 'STORE_DEREF', 'LOAD_CLOSURE'])) _logger = logging.getLogger(__name__) -_re_error = r'^(?:\d{4}-\d\d-\d\d \d\d:\d\d:\d\d,\d{3} \d+ (?:ERROR|CRITICAL) )|(?:Traceback \(most recent call last\):)$' _re_warning = r'^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d,\d{3} \d+ WARNING ' PYTHON_DEFAULT = "# type python code here\n\n\n\n\n\n" @@ -926,17 +925,19 @@ class ConfigStep(models.Model): log_time = self._get_log_last_write(build) if log_time: build.job_end = log_time - if self.job_type == 'python' and self.python_result_code and self.python_result_code != PYTHON_DEFAULT: - build.write(self._make_python_results(build)) - elif self.job_type in ['install_odoo', 'python']: + if self.job_type == 'python': + if self.python_result_code and self.python_result_code != PYTHON_DEFAULT: + self._make_python_results(build) + elif self.test_enable or self.test_tags: + self._make_odoo_results(build) + elif self.job_type == 'install_odoo': if self.coverage: build.write(self._make_coverage_results(build)) - if self.test_enable or self.test_tags: - build.write(self._make_tests_results(build)) + self._make_odoo_results(build) elif self.job_type == 'test_upgrade': - build.write(self._make_upgrade_results(build)) + self._make_upgrade_results(build) elif self.job_type == 'restore': - build.write(self._make_restore_results(build)) + self._make_restore_results(build) def _make_python_results(self, build): eval_ctx = self._make_python_ctx(build) @@ -945,7 +946,7 @@ class ConfigStep(models.Model): # todo check return_value or write in try except. Example: local result setted to wrong value if not isinstance(return_value, dict): raise RunbotException('python_result_code must set return_value to a dict values on build') - return return_value + build.write(return_value) # old style support def _make_coverage_results(self, build): build_values = {} @@ -965,22 +966,18 @@ class ConfigStep(models.Model): return build_values def _make_upgrade_results(self, build): - build_values = {} build._log('upgrade', 'Getting results for build %s' % build.dest) if build.local_result != 'ko': checkers = [ self._check_log, - self._check_module_loaded, self._check_error, + self._check_module_loaded, self._check_module_states, self._check_build_ended, self._check_warning, ] - local_result = self._get_checkers_result(build, checkers) - build_values['local_result'] = build._get_worst_result([build.local_result, local_result]) - - return build_values + build.local_result = self._get_checkers_result(build, checkers) def _check_module_states(self, build): if not build._is_file('logs/modules_states.txt'): @@ -1009,9 +1006,18 @@ class ConfigStep(models.Model): def _check_error(self, build, regex=None): log_path = build._path('logs', '%s.txt' % self.name) - regex = regex or _re_error - if rfind(log_path, regex): - build._log('_make_tests_results', 'Error or traceback found in logs', level="ERROR") + re_error = regex or r'^(?:\d{4}-\d\d-\d\d \d\d:\d\d:\d\d,\d{3} \d+ (?:ERROR|CRITICAL) .*)$' + + if result := rfind(log_path, re_error): + build._log('_make_tests_results', 'Error found in logs:\n%s' % '\n'.join(result), level="ERROR") + return 'ko' + + re_traceback = r'^(?:Traceback \(most recent call last\):)$' + if result := rfind(log_path, re_traceback): + # find Traceback, all following indented lines and one last non indented line + complete_traceback = rfind(log_path, r'^(?:Traceback \(most recent call last\):(?:\n .*)*(?:\n.*)?)')[:10000] + complete_traceback = complete_traceback or result + build._log('_make_tests_results', 'Traceback found in logs:\n%s' % '\n'.join(complete_traceback), level="ERROR") return 'ko' return 'ok' @@ -1049,34 +1055,28 @@ class ConfigStep(models.Model): return result return 'ok' - def _make_tests_results(self, build): - build_values = {} + def _make_odoo_results(self, build): build._log('run', 'Getting results for build %s' % build.dest) if build.local_result != 'ko': checkers = [ self._check_log, - self._check_module_loaded, self._check_error, - self._check_build_ended + self._check_module_loaded, + self._check_build_ended, ] if build.local_result != 'warn': checkers.append(self._check_warning) - local_result = self._get_checkers_result(build, checkers) - build_values['local_result'] = build._get_worst_result([build.local_result, local_result]) - return build_values + build.local_result = self._get_checkers_result(build, checkers) def _make_restore_results(self, build): - build_values = {} if build.local_result != 'warn': checkers = [ self._check_log, self._check_restore_ended ] - local_result = self._get_checkers_result(build, checkers) - build_values['local_result'] = build._get_worst_result([build.local_result, local_result]) - return build_values + build.local_result = self._get_checkers_result(build, checkers) def _make_stats(self, build): if not self.make_stats: # TODO garbage collect non sticky stat diff --git a/runbot/models/repo.py b/runbot/models/repo.py index 6fa8608d..781b9518 100644 --- a/runbot/models/repo.py +++ b/runbot/models/repo.py @@ -90,7 +90,7 @@ class Trigger(models.Model): report_view_id = fields.Many2one('ir.ui.view', help="custom view to render result", - string='Docker Template', + string='Report view', domain=[('type', '=', 'qweb')], context={'default_type': 'qweb', 'default_arch_base': ''}, ) diff --git a/runbot/tests/test_build_config_step.py b/runbot/tests/test_build_config_step.py index 819afd01..812dec6a 100644 --- a/runbot/tests/test_build_config_step.py +++ b/runbot/tests/test_build_config_step.py @@ -618,102 +618,128 @@ class TestMakeResult(RunbotCase): super(TestMakeResult, self).setUp() self.ConfigStep = self.env['runbot.build.config.step'] self.Config = self.env['runbot.build.config'] + self.patchers['getmtime'].return_value = 7200 + self.logs = [] + def _log(build, func, message, level='INFO', log_type='runbot', path='runbot'): + self.logs.append((level, message)) - @patch('odoo.addons.runbot.models.build_config.os.path.getmtime') - @patch('odoo.addons.runbot.models.build.BuildResult._log') - def test_make_result(self, mock_log, mock_getmtime): + self.start_patcher('log_patcher', 'odoo.addons.runbot.models.build.BuildResult._log', new=_log) + + self.build = self.Build.create({ + 'params_id': self.base_params.id, + }) + self.config_step = self.ConfigStep.create({ + 'name': 'all', + 'job_type': 'install_odoo', + 'test_tags': '/module,:class.method', + }) + + def test_make_result_ok(self): file_content = """ Loading stuff odoo.stuff.modules.loading: Modules loaded. Some post install stuff Initiating shutdown """ - logs = [] - - def _log(func, message, level='INFO', log_type='runbot', path='runbot'): - logs.append((level, message)) - - mock_log.side_effect = _log - mock_getmtime.return_value = 7200 - - config_step = self.ConfigStep.create({ - 'name': 'all', - 'job_type': 'install_odoo', - 'test_tags': '/module,:class.method', - }) - build = self.Build.create({ - 'params_id': self.base_params.id, - }) - logs = [] with patch('builtins.open', mock_open(read_data=file_content)): - config_step._make_results(build) - self.assertEqual(str(build.job_end), '1970-01-01 02:00:00') - self.assertEqual(logs, [('INFO', 'Getting results for build %s' % build.dest)]) - self.assertEqual(build.local_result, 'ok') - # no shutdown - build = self.Build.create({ - 'params_id': self.base_params.id, - }) - logs = [] + self.config_step._make_results(self.build) + self.assertEqual(str(self.build.job_end), '1970-01-01 02:00:00') + self.assertEqual(self.logs, [('INFO', 'Getting results for build %s' % self.build.dest)]) + self.assertEqual(self.build.local_result, 'ok') + + def test_make_result_no_shutdown(self): file_content = """ Loading stuff odoo.stuff.modules.loading: Modules loaded. Some post install stuff """ with patch('builtins.open', mock_open(read_data=file_content)): - config_step._make_results(build) - self.assertEqual(str(build.job_end), '1970-01-01 02:00:00') - self.assertEqual(build.local_result, 'ko') - self.assertEqual(logs, [ - ('INFO', 'Getting results for build %s' % build.dest), - ('ERROR', 'No "Initiating shutdown" found in logs, maybe because of cpu limit.') + self.config_step._make_results(self.build) + self.assertEqual(str(self.build.job_end), '1970-01-01 02:00:00') + self.assertEqual(self.build.local_result, 'ko') + self.assertEqual(self.logs, [ + ('INFO', 'Getting results for build %s' % self.build.dest), + ('ERROR', 'No "Initiating shutdown" found in logs, maybe because of cpu limit.'), ]) - # no loaded - build = self.Build.create({ - 'params_id': self.base_params.id, - }) - logs = [] + + def test_make_result_no_loaded(self): file_content = """ Loading stuff """ with patch('builtins.open', mock_open(read_data=file_content)): - config_step._make_results(build) - self.assertEqual(str(build.job_end), '1970-01-01 02:00:00') - self.assertEqual(build.local_result, 'ko') - self.assertEqual(logs, [ - ('INFO', 'Getting results for build %s' % build.dest), - ('ERROR', 'Modules loaded not found in logs') + self.config_step._make_results(self.build) + self.assertEqual(str(self.build.job_end), '1970-01-01 02:00:00') + self.assertEqual(self.build.local_result, 'ko') + self.assertEqual(self.logs, [ + ('INFO', 'Getting results for build %s' % self.build.dest), + ('ERROR', 'Modules loaded not found in logs'), ]) - # traceback - build = self.Build.create({ - 'params_id': self.base_params.id, - }) - logs = [] + def test_make_result_traceback(self): + file_content = """ +Loading stuff +Traceback (most recent call last): + File "/data/build/odoo/odoo-bin", line 5, in + import odoo + File "/data/build/odoo/odoo/__init__.py", line 134, in + from . import modules + File "/data/build/odoo/odoo/modules/__init__.py", line 8, in + from . import db, graph, loading, migration, module, registry, neutralize + File "/data/build/odoo/odoo/modules/graph.py", line 11, in + import odoo.tools as tools + File "/data/build/odoo/odoo/tools/__init__.py", line 25, in + from .mail import * + File "/data/build/odoo/odoo/tools/mail.py", line 32, in + safe_attrs = clean.defs.safe_attrs | frozenset( +AttributeError: module 'lxml.html.clean' has no attribute 'defs' +2024-05-14 09:54:22,692 17 INFO dbname path.to.test: aaa +""" + with patch('builtins.open', mock_open(read_data=file_content)): + self.config_step._make_results(self.build) + self.assertEqual(str(self.build.job_end), '1970-01-01 02:00:00') + self.assertEqual(self.build.local_result, 'ko') + expected = """Traceback found in logs: +Traceback (most recent call last): + File "/data/build/odoo/odoo-bin", line 5, in + import odoo + File "/data/build/odoo/odoo/__init__.py", line 134, in + from . import modules + File "/data/build/odoo/odoo/modules/__init__.py", line 8, in + from . import db, graph, loading, migration, module, registry, neutralize + File "/data/build/odoo/odoo/modules/graph.py", line 11, in + import odoo.tools as tools + File "/data/build/odoo/odoo/tools/__init__.py", line 25, in + from .mail import * + File "/data/build/odoo/odoo/tools/mail.py", line 32, in + safe_attrs = clean.defs.safe_attrs | frozenset( +AttributeError: module 'lxml.html.clean' has no attribute 'defs'""" + self.assertEqual(self.logs, [ + ('INFO', 'Getting results for build %s' % self.build.dest), + ('ERROR', expected), + ]) + + def test_make_result_error(self): file_content = """ Loading stuff odoo.stuff.modules.loading: Modules loaded. Some post install stuff -2019-12-17 17:34:37,692 17 ERROR dbname path.to.test: FAIL: TestClass.test_ -Traceback (most recent call last): -File "x.py", line a, in test_ - .... +2024-05-14 09:54:22,692 17 ERROR dbname path.to.test: FAIL: TestClass.test_ +Some log +2024-05-14 09:54:22,692 17 ERROR dbname path.to.test: FAIL: TestClass.test2_ Initiating shutdown """ with patch('builtins.open', mock_open(read_data=file_content)): - config_step._make_results(build) - self.assertEqual(str(build.job_end), '1970-01-01 02:00:00') - self.assertEqual(build.local_result, 'ko') - self.assertEqual(logs, [ - ('INFO', 'Getting results for build %s' % build.dest), - ('ERROR', 'Error or traceback found in logs') + self.config_step._make_results(self.build) + self.assertEqual(str(self.build.job_end), '1970-01-01 02:00:00') + self.assertEqual(self.build.local_result, 'ko') + self.assertEqual(self.logs, [ + ('INFO', 'Getting results for build %s' % self.build.dest), + ('ERROR', """Error found in logs: +2024-05-14 09:54:22,692 17 ERROR dbname path.to.test: FAIL: TestClass.test_ +2024-05-14 09:54:22,692 17 ERROR dbname path.to.test: FAIL: TestClass.test2_"""), ]) - # warning in logs - build = self.Build.create({ - 'params_id': self.base_params.id, - }) - logs = [] + def test_make_result_warning(self): file_content = """ Loading stuff odoo.stuff.modules.loading: Modules loaded. @@ -722,30 +748,26 @@ Some post install stuff Initiating shutdown """ with patch('builtins.open', mock_open(read_data=file_content)): - config_step._make_results(build) - self.assertEqual(str(build.job_end), '1970-01-01 02:00:00') - self.assertEqual(build.local_result, 'warn') - self.assertEqual(logs, [ - ('INFO', 'Getting results for build %s' % build.dest), + self.config_step._make_results(self.build) + self.assertEqual(str(self.build.job_end), '1970-01-01 02:00:00') + self.assertEqual(self.build.local_result, 'warn') + self.assertEqual(self.logs, [ + ('INFO', 'Getting results for build %s' % self.build.dest), ('WARNING', 'Warning found in logs') ]) # no log file - logs = [] + self.logs = [] self.patchers['isfile'].return_value = False - config_step._make_results(build) + self.config_step._make_results(self.build) - self.assertEqual(build.local_result, 'ko') - self.assertEqual(logs, [ - ('INFO', 'Getting results for build %s' % build.dest), + self.assertEqual(self.build.local_result, 'ko') + self.assertEqual(self.logs, [ + ('INFO', 'Getting results for build %s' % self.build.dest), ('ERROR', 'Log file not found at the end of test job') ]) - # no error but build was already in warn - build = self.Build.create({ - 'params_id': self.base_params.id, - }) - logs = [] + def test_make_result_already_warn(self): file_content = """ Loading stuff odoo.stuff.modules.loading: Modules loaded. @@ -753,17 +775,18 @@ Some post install stuff Initiating shutdown """ self.patchers['isfile'].return_value = True - build.local_result = 'warn' + self.build.local_result = 'warn' with patch('builtins.open', mock_open(read_data=file_content)): - config_step._make_results(build) - self.assertEqual(logs, [ - ('INFO', 'Getting results for build %s' % build.dest) + self.config_step._make_results(self.build) + self.assertEqual(self.logs, [ + ('INFO', 'Getting results for build %s' % self.build.dest) ]) - self.assertEqual(str(build.job_end), '1970-01-01 02:00:00') - self.assertEqual(build.local_result, 'warn') + self.assertEqual(str(self.build.job_end), '1970-01-01 02:00:00') + self.assertEqual(self.build.local_result, 'warn') - @patch('odoo.addons.runbot.models.build_config.ConfigStep._make_tests_results') - def test_make_python_result(self, mock_make_tests_results): + + @patch('odoo.addons.runbot.models.build_config.ConfigStep._make_odoo_results') + def test_make_python_result(self, mock_make_odoo_results): config_step = self.ConfigStep.create({ 'name': 'all', 'job_type': 'python', @@ -785,7 +808,10 @@ Initiating shutdown # no result defined config_step.python_result_code = "" - mock_make_tests_results.return_value = {'local_result': 'warn'} + def make_warn(build): + build.local_result = "warn" + + mock_make_odoo_results.side_effect = make_warn config_step._make_results(build) self.assertEqual(build.local_result, 'warn')