[REF] runbot: various refactoring

The initial motivation is to remove the flush when a log_counter is
written. This flush was initially usefull when the limit was in a
psql trigger, but finally add a side effect to flush everything before
starting the docker. This was limiting concurrent update after starting
the docker, but we still have no garantee that the transaction is
commited after starting the docker. The use case where the docker is
started but the transaction is not commited was not handled well and was
leading to an infinite loop of trying to start a docker (while the
docker was already started)

This refactoring returns the docker to the scheduler so that the
schedulter can commit before starting the docker.

To achieve this, it is ideal to have only one method that could return
a callable in the _scheduler loop. This is done by removing the run_job
from the init_pending method. All satellite method like make result
are also modified and adapted to make direct write: the old way was
technical debt, useless optimization from pre-v13.

Other piece of code are moved arround to prepare for future changes,
mainly to make the last commit easier to revert if needed.

[FIX] runbot: adapt tests to previous refactoring
This commit is contained in:
Xavier-Do 2023-03-17 10:24:51 +01:00 committed by Christophe Monniez
parent 688900edb1
commit d2f9330043
15 changed files with 496 additions and 332 deletions

View File

@ -159,8 +159,7 @@ class BuildResult(models.Model):
global_state = fields.Selection(make_selection(state_order), string='Status', compute='_compute_global_state', store=True, recursive=True)
local_state = fields.Selection(make_selection(state_order), string='Build Status', default='pending', required=True, index=True)
global_result = fields.Selection(make_selection(result_order), string='Result', compute='_compute_global_result', store=True, recursive=True)
local_result = fields.Selection(make_selection(result_order), string='Build Result')
triggered_result = fields.Selection(make_selection(result_order), string='Triggered Result') # triggered by db only
local_result = fields.Selection(make_selection(result_order), string='Build Result', default='ok')
requested_action = fields.Selection([('wake_up', 'To wake up'), ('deathrow', 'To kill')], string='Action requested', index=True)
# web infos
@ -345,9 +344,26 @@ class BuildResult(models.Model):
values.pop('local_result')
else:
raise ValidationError('Local result cannot be set to a less critical level')
init_global_results = self.mapped('global_result')
init_global_states = self.mapped('global_state')
res = super(BuildResult, self).write(values)
if 'log_counter' in values: # not 100% usefull but more correct ( see test_ir_logging)
self.flush()
for init_global_result, build in zip(init_global_results, self):
if init_global_result != build.global_result:
build._github_status()
for init_global_state, build in zip(init_global_states, self):
if not build.parent_id and init_global_state not in ('done', 'running') and build.global_state in ('done', 'running'):
build._github_status()
if values.get('global_state') in ('done', 'running'):
for build in self:
if not build.parent_id and build.global_state not in ('done', 'running'):
build._github_status()
return res
def _add_child(self, param_values, orphan=False, description=False, additionnal_commit_links=False):
@ -382,12 +398,6 @@ class BuildResult(models.Model):
return 'warning'
return 'ko' # ?
def update_build_end(self):
for build in self:
build.build_end = now()
if build.parent_id and build.parent_id.local_state in ('running', 'done'):
build.parent_id.update_build_end()
@api.depends('params_id.version_id.name')
def _compute_dest(self):
for build in self:
@ -599,34 +609,20 @@ class BuildResult(models.Model):
self.ensure_one()
return '%s_%s' % (self.dest, self.active_step.name)
def _init_pendings(self, host):
for build in self:
if build.local_state != 'pending':
raise UserError("Build %s is not pending" % build.id)
if build.host != host.name:
raise UserError("Build %s does not have correct host" % build.id)
# allocate port and schedule first job
values = {
'port': self._find_port(),
'job_start': now(),
'build_start': now(),
'job_end': False,
}
values.update(build._next_job_values())
build.write(values)
if not build.active_step:
build._log('_schedule', 'No job in config, doing nothing')
build.local_result = 'warn'
continue
try:
build._log('_schedule', 'Init build environment with config %s ' % build.params_id.config_id.name)
os.makedirs(build._path('logs'), exist_ok=True)
except Exception:
_logger.exception('Failed initiating build %s', build.dest)
build._log('_schedule', 'Failed initiating build')
build._kill(result='ko')
continue
build._run_job()
def _init_pendings(self):
self.ensure_one()
build = self
build.port = self._find_port()
build.job_start = now()
build.build_start = now()
build.job_end = False
build._log('_schedule', 'Init build environment with config %s ' % build.params_id.config_id.name)
try:
os.makedirs(build._path('logs'), exist_ok=True)
except Exception:
_logger.exception('Failed initiating build %s', build.dest)
build._log('_schedule', 'Failed initiating build')
build._kill(result='ko')
def _process_requested_actions(self):
for build in self:
@ -638,12 +634,15 @@ class BuildResult(models.Model):
continue
if build.requested_action == 'wake_up':
if docker_state(build._get_docker_name(), build._path()) == 'RUNNING':
if build.local_state != 'done':
build.requested_action = False
build._log('wake_up', 'Impossible to wake-up, build is not done', log_type='markdown', level='SEPARATOR')
elif not os.path.exists(build._path()):
build.requested_action = False
build._log('wake_up', 'Impossible to wake-up, **build dir does not exists anymore**', log_type='markdown', level='SEPARATOR')
elif 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**', log_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**', log_type='markdown', level='SEPARATOR')
else:
try:
log_path = build._path('logs', 'wake_up.txt')
@ -674,45 +673,39 @@ 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))
if build.local_state == 'testing':
# failfast in case of docker error (triggered in database)
if build.triggered_result and not build.active_step.ignore_triggered_result:
worst_result = self._get_worst_result([build.triggered_result, build.local_result])
if worst_result != build.local_result:
build.local_result = build.triggered_result
build._github_status() # failfast
# check if current job is finished
self.ensure_one()
build = self
if build.local_state not in ['testing', 'running', 'pending']:
return False
# check if current job is finished
if build.local_state == 'pending':
build._init_pendings()
else:
_docker_state = docker_state(build._get_docker_name(), build._path())
if _docker_state == 'RUNNING':
timeout = min(build.active_step.cpu_limit, int(icp.get_param('runbot.runbot_timeout', default=10000)))
if build.local_state != 'running' and build.job_time > timeout:
build._log('_schedule', '%s time exceeded (%ss)' % (build.active_step.name if build.active_step else "?", build.job_time))
build._kill(result='killed')
continue
return False
elif _docker_state in ('UNKNOWN', 'GHOST') and (build.local_state == 'running' or build.active_step._is_docker_step()): # todo replace with docker_start
docker_time = time.time() - dt2time(build.docker_start or build.job_start)
if docker_time < 5:
continue
return False
elif docker_time < 60:
_logger.info('container "%s" seems too take a while to start :%s' % (build.job_time, build._get_docker_name()))
continue
return False
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
if self.env['runbot.host']._fetch_local_logs(build_ids=build.ids):
return True # avoid to make results with remaining logs
# No job running, make result and select next job
build_values = {
'job_end': now(),
'docker_start': False,
}
build.job_end = now()
build.docker_start = False
# make result of previous job
try:
results = build.active_step._make_results(build)
build.active_step._make_results(build)
except Exception as e:
if isinstance(e, RunbotException):
message = e.args[0][:300000]
@ -720,50 +713,71 @@ class BuildResult(models.Model):
message = 'An error occured while computing results of %s:\n %s' % (build.job, str(e).replace('\\n', '\n').replace("\\'", "'")[:10000])
_logger.exception(message)
build._log('_make_results', message, level='ERROR')
results = {'local_result': 'ko'}
build_values.update(results)
build.local_result = 'ko'
# compute statistics before starting next job
build.active_step._make_stats(build)
build.active_step.log_end(build)
build_values.update(build._next_job_values()) # find next active_step or set to done
step_ids = self.params_id.config_id.step_ids()
if not step_ids: # no job to do, build is done
self.active_step = False
self.local_state = 'done'
build._log('_schedule', 'No job in config, doing nothing')
build.local_result = 'warn'
return False
if not self.active_step and self.local_state != 'pending': # wakeup docker finished
build.active_step = False
build.local_state = 'done'
return False
ending_build = build.local_state not in ('done', 'running') and build_values.get('local_state') in ('done', 'running')
if ending_build:
build.update_build_end()
if not self.active_step:
next_index = 0
else:
if self.active_step not in step_ids:
self._log('run', 'Config was modified and current step does not exists anymore, skipping.', level='ERROR')
self.active_step = False
self.local_state = 'done'
self.local_result = 'ko'
return False
next_index = step_ids.index(self.active_step) + 1
build.write(build_values)
if ending_build:
if not build.local_result: # Set 'ok' result if no result set (no tests job on build)
build.local_result = 'ok'
build._logger("No result set, setting ok by default")
build._github_status()
build._run_job()
while True:
if next_index >= len(step_ids): # final job, build is done
self.active_step = False
self.local_state = 'done'
return False
new_step = step_ids[next_index] # job to do, state is job_state (testing or running)
if new_step.domain_filter and not self.filtered_domain(safe_eval(new_step.domain_filter)):
self._log('run', '**Skipping** step ~~%s~~ from config **%s**' % (new_step.name, self.params_id.config_id.name), log_type='markdown', level='SEPARATOR')
next_index += 1
continue
break
build.active_step = new_step.id
build.local_state = new_step._step_state()
return build._run_job()
def _run_job(self):
# run job
for build in self:
if build.local_state != 'done':
build._logger('running %s', build.active_step.name)
os.makedirs(build._path('logs'), exist_ok=True)
os.makedirs(build._path('datadir'), exist_ok=True)
try:
build.active_step._run(build) # run should be on build?
except TransactionRollbackError:
raise
except Exception as e:
if isinstance(e, RunbotException):
message = e.args[0]
else:
message = '%s failed running step %s:\n %s' % (build.dest, build.job, str(e).replace('\\n', '\n').replace("\\'", "'"))
_logger.exception(message)
build._log("run", message, level='ERROR')
build._kill(result='ko')
self.ensure_one()
build = self
if build.local_state != 'done':
build._logger('running %s', build.active_step.name)
os.makedirs(build._path('logs'), exist_ok=True)
os.makedirs(build._path('datadir'), exist_ok=True)
try:
return build.active_step._run(build) # run should be on build?
except TransactionRollbackError:
raise
except Exception as e:
if isinstance(e, RunbotException):
message = e.args[0]
else:
message = '%s failed running step %s:\n %s' % (build.dest, build.job, str(e).replace('\\n', '\n').replace("\\'", "'"))
_logger.exception(message)
build._log("run", message, level='ERROR')
build._kill(result='ko')
def _docker_run(self, cmd=None, ro_volumes=None, **kwargs):
self.ensure_one()
@ -793,7 +807,10 @@ class BuildResult(models.Model):
user = getpass.getuser()
ro_volumes[f'/home/{user}/.odoorc'] = self._path('.odoorc')
kwargs.pop('build_dir', False) # todo check python steps
docker_run(cmd=cmd, build_dir=self._path(), ro_volumes=ro_volumes, **kwargs)
build_dir = self._path()
def start_docker():
docker_run(cmd=cmd, build_dir=build_dir, ro_volumes=ro_volumes, **kwargs)
return start_docker
def _path(self, *l, **kw):
"""Return the repo build path"""
@ -1061,36 +1078,6 @@ class BuildResult(models.Model):
'build_id': self.id
})
def _next_job_values(self):
self.ensure_one()
step_ids = self.params_id.config_id.step_ids()
if not step_ids: # no job to do, build is done
return {'active_step': False, 'local_state': 'done'}
if not self.active_step and self.local_state != 'pending':
# means that a step has been run manually without using config
return {'active_step': False, 'local_state': 'done'}
if not self.active_step:
next_index = 0
else:
if self.active_step not in step_ids:
self._log('run', 'Config was modified and current step does not exists anymore, skipping.', level='ERROR')
return {'active_step': False, 'local_state': 'done', 'local_result': self._get_worst_result([self.local_result, 'ko'])}
next_index = step_ids.index(self.active_step) + 1
while True:
if next_index >= len(step_ids): # final job, build is done
return {'active_step': False, 'local_state': 'done'}
new_step = step_ids[next_index] # job to do, state is job_state (testing or running)
if new_step.domain_filter and not self.filtered_domain(safe_eval(new_step.domain_filter)):
self._log('run', '**Skipping** step ~~%s~~ from config **%s**' % (new_step.name, self.params_id.config_id.name), log_type='markdown', level='SEPARATOR')
next_index += 1
continue
break
return {'active_step': new_step.id, 'local_state': new_step._step_state()}
def _get_py_version(self):
"""return the python name to use from build batch"""
(server_commit, server_file) = self._get_server_info()

View File

@ -166,7 +166,6 @@ class ConfigStep(models.Model):
# python
python_code = fields.Text('Python code', tracking=True, default=PYTHON_DEFAULT)
python_result_code = fields.Text('Python code for result', tracking=True, default=PYTHON_DEFAULT)
ignore_triggered_result = fields.Boolean('Ignore error triggered in logs', tracking=True, default=False)
running_job = fields.Boolean('Job final state is running', default=False, help="Docker won't be killed if checked")
# create_build
create_config_ids = fields.Many2many('runbot.build.config', 'runbot_build_config_step_ids_create_config_ids_rel', string='New Build Configs', tracking=True, index=True)
@ -275,14 +274,14 @@ class ConfigStep(models.Model):
url = f"{log_url}/runbot/static/build/{build.dest}/logs/{self.name}.txt"
log_link = f'[@icon-file-text]({url})'
build._log('run', 'Starting step **%s** from config **%s** %s' % (self.name, build.params_id.config_id.name, log_link), log_type='markdown', level='SEPARATOR')
self._run_step(build, log_path)
return self._run_step(build, log_path)
def _run_step(self, build, log_path, **kwargs):
build.log_counter = self.env['ir.config_parameter'].sudo().get_param('runbot.runbot_maxlogs', 100)
run_method = getattr(self, '_run_%s' % self.job_type)
docker_params = run_method(build, log_path, **kwargs)
if docker_params:
build._docker_run(**docker_params)
return build._docker_run(**docker_params)
def _run_create_build(self, build, log_path):
count = 0
@ -906,23 +905,20 @@ class ConfigStep(models.Model):
return ['--omit', ','.join(pattern_to_omit)]
def _make_results(self, build):
build_values = {}
log_time = self._get_log_last_write(build)
if log_time:
build_values['job_end'] = 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_values.update(self._make_python_results(build))
build.write(self._make_python_results(build))
elif self.job_type in ['install_odoo', 'python']:
if self.coverage:
build_values.update(self._make_coverage_results(build))
build.write(self._make_coverage_results(build))
if self.test_enable or self.test_tags:
build_values.update(self._make_tests_results(build))
build.write(self._make_tests_results(build))
elif self.job_type == 'test_upgrade':
build_values.update(self._make_upgrade_results(build))
build.write(self._make_upgrade_results(build))
elif self.job_type == 'restore':
build_values.update(self._make_restore_results(build))
return build_values
build.write(self._make_restore_results(build))
def _make_python_results(self, build):
eval_ctx = self.make_python_ctx(build)

View File

@ -55,7 +55,6 @@ class Bundle(models.Model):
# extra_info
for_next_freeze = fields.Boolean('Should be in next freeze')
@api.depends('name')
def _compute_host_id(self):
assigned_only = None

View File

@ -36,10 +36,11 @@ class runbot_event(models.Model):
build_logs = logs_by_build_id[build.id]
for ir_log in build_logs:
ir_log['active_step_id'] = build.active_step.id
if ir_log['level'].upper() == 'WARNING':
build.triggered_result = 'warn'
elif ir_log['level'].upper() == 'ERROR':
build.triggered_result = 'ko'
if build.local_state != 'running':
if ir_log['level'].upper() == 'WARNING':
build.local_result = 'warn'
elif ir_log['level'].upper() == 'ERROR':
build.local_result = 'ko'
return super().create(vals_list)
def _markdown(self):

View File

@ -209,7 +209,7 @@ class Host(models.Model):
res.append({name:value for name, value in zip(col_names, row)})
return res
def process_logs(self, build_ids=None):
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)
@ -253,6 +253,13 @@ class Host(models.Model):
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)])
def get_build_domain(self, domain=None):
domain = domain or []
return [('host', '=', self.name)] + domain
def get_builds(self, domain, order=None):
return self.env['runbot.build'].search(self.get_build_domain(domain), order=order)
def _process_messages(self):
self.host_message_ids._process()

View File

@ -39,80 +39,76 @@ class Runbot(models.AbstractModel):
def _scheduler(self, host):
self._gc_testing(host)
self._commit()
for build in self._get_builds_with_requested_actions(host):
build = build.browse(build.id) # remove preftech ids, manage build one by one
processed = 0
for build in host.get_builds([('requested_action', 'in', ['wake_up', 'deathrow'])]):
processed += 1
build._process_requested_actions()
self._commit()
host.process_logs()
host._process_logs()
self._commit()
host._process_messages()
self._commit()
for build in self._get_builds_to_schedule(host):
for build in host.get_builds([('local_state', 'in', ['testing', 'running'])]) | self._get_builds_to_init(host):
build = build.browse(build.id) # remove preftech ids, manage build one by one
build._schedule()
result = build._schedule()
if result:
processed += 1
self._commit()
self._assign_pending_builds(host, host.nb_worker, [('build_type', '!=', 'scheduled')])
if callable(result):
result() # start docker
processed += self._assign_pending_builds(host, host.nb_worker, [('build_type', '!=', 'scheduled')])
self._commit()
self._assign_pending_builds(host, host.nb_worker - 1 or host.nb_worker)
processed += self._assign_pending_builds(host, host.nb_worker - 1 or host.nb_worker)
self._commit()
self._assign_pending_builds(host, host.nb_worker and host.nb_worker + 1, [('build_type', '=', 'priority')])
processed += self._assign_pending_builds(host, host.nb_worker and host.nb_worker + 1, [('build_type', '=', 'priority')])
self._commit()
for build in self._get_builds_to_init(host):
build = build.browse(build.id) # remove preftech ids, manage build one by one
build._init_pendings(host)
self._commit()
self._gc_running(host)
self._commit()
self._reload_nginx()
def build_domain_host(self, host, domain=None):
domain = domain or []
return [('host', '=', host.name)] + domain
def _get_builds_with_requested_actions(self, host):
return self.env['runbot.build'].search(self.build_domain_host(host, [('requested_action', 'in', ['wake_up', 'deathrow'])]))
def _get_builds_to_schedule(self, host):
return self.env['runbot.build'].search(self.build_domain_host(host, [('local_state', 'in', ['testing', 'running'])]))
self._commit()
return processed
def _assign_pending_builds(self, host, nb_worker, domain=None):
if host.assigned_only or nb_worker <= 0:
return
domain_host = self.build_domain_host(host)
reserved_slots = self.env['runbot.build'].search_count(domain_host + [('local_state', 'in', ('testing', 'pending'))])
return 0
reserved_slots = len(host.get_builds([('local_state', 'in', ('testing', 'pending'))]))
assignable_slots = (nb_worker - reserved_slots)
if assignable_slots > 0:
allocated = self._allocate_builds(host, assignable_slots, domain)
if allocated:
_logger.info('Builds %s where allocated to runbot', allocated)
return len(allocated)
return 0
def _get_builds_to_init(self, host):
domain_host = self.build_domain_host(host)
used_slots = self.env['runbot.build'].search_count(domain_host + [('local_state', '=', 'testing')])
domain_host = host.get_build_domain()
used_slots = len(host.get_builds([('local_state', '=', 'testing')]))
available_slots = host.nb_worker - used_slots
if available_slots <= 0:
return self.env['runbot.build']
return self.env['runbot.build'].search(domain_host + [('local_state', '=', 'pending')], limit=available_slots)
build_to_init = self.env['runbot.build']
if available_slots > 0:
build_to_init |= self.env['runbot.build'].search(domain_host + [('local_state', '=', 'pending')], limit=available_slots)
if available_slots + 1 > 0:
build_to_init |= self.env['runbot.build'].search(domain_host + [('local_state', '=', 'pending'), ('build_type', '=', 'priority')], limit=1)
return build_to_init
def _gc_running(self, host):
running_max = host.get_running_max()
domain_host = self.build_domain_host(host)
Build = self.env['runbot.build']
cannot_be_killed_ids = Build.search(domain_host + [('keep_running', '=', True)]).ids
cannot_be_killed_ids = host.get_builds([('keep_running', '=', True)]).ids
sticky_bundles = self.env['runbot.bundle'].search([('sticky', '=', True), ('project_id.keep_sticky_running', '=', True)])
cannot_be_killed_ids += [
build.id
for build in sticky_bundles.mapped('last_batchs.slot_ids.build_id')
if build.host == host.name
][:running_max]
build_ids = Build.search(domain_host + [('local_state', '=', 'running'), ('id', 'not in', cannot_be_killed_ids)], order='job_start desc').ids
build_ids = host.get_builds([('local_state', '=', 'running'), ('id', 'not in', cannot_be_killed_ids)], order='job_start desc').ids
Build.browse(build_ids)[running_max:]._kill()
def _gc_testing(self, host):
"""garbage collect builds that could be killed"""
# decide if we need room
Build = self.env['runbot.build']
domain_host = self.build_domain_host(host)
domain_host = host.get_build_domain()
testing_builds = Build.search(domain_host + [('local_state', 'in', ['testing', 'pending']), ('requested_action', '!=', 'deathrow')])
used_slots = len(testing_builds)
available_slots = host.nb_worker - used_slots
@ -282,11 +278,11 @@ class Runbot(models.AbstractModel):
return manager.get('sleep', default_sleep)
def _scheduler_loop_turn(self, host, default_sleep=5):
_logger.info('Scheduling...')
def _scheduler_loop_turn(self, host, sleep=5):
with self.manage_host_exception(host) as manager:
self._scheduler(host)
return manager.get('sleep', default_sleep)
if self._scheduler(host):
sleep = 0.1
return manager.get('sleep', sleep)
@contextmanager
def manage_host_exception(self, host):

View File

@ -181,7 +181,6 @@ class RunbotCase(TransactionCase):
self.start_patcher('docker_stop', 'odoo.addons.runbot.container._docker_stop')
self.start_patcher('docker_get_gateway_ip', 'odoo.addons.runbot.models.build_config.docker_get_gateway_ip', None)
self.start_patcher('cr_commit', 'odoo.sql_db.Cursor.commit', None)
self.start_patcher('repo_commit', 'odoo.addons.runbot.models.runbot.Runbot._commit', None)
self.start_patcher('_local_cleanup_patcher', 'odoo.addons.runbot.models.build.BuildResult._local_cleanup')
self.start_patcher('_local_pg_dropdb_patcher', 'odoo.addons.runbot.models.build.BuildResult._local_pg_dropdb')
@ -194,6 +193,11 @@ class RunbotCase(TransactionCase):
self.start_patcher('_get_py_version', 'odoo.addons.runbot.models.build.BuildResult._get_py_version', 3)
def no_commit(*_args, **_kwargs):
_logger.info('Skipping commit')
self.patch(self.env.cr, 'commit', no_commit)
def start_patcher(self, patcher_name, patcher_path, return_value=DEFAULT, side_effect=DEFAULT, new=DEFAULT):

View File

@ -4,8 +4,10 @@ import datetime
from unittest.mock import patch
from odoo import fields
from odoo.tests import tagged
from odoo.exceptions import UserError, ValidationError
from .common import RunbotCase, RunbotCaseMinimalSetup
from unittest.mock import MagicMock
def rev_parse(repo, branch_name):
@ -18,6 +20,7 @@ def rev_parse(repo, branch_name):
return head_hash
@tagged('-at_install', 'post_istall')
class TestBuildParams(RunbotCaseMinimalSetup):
def setUp(self):
@ -171,7 +174,10 @@ class TestBuildResult(RunbotCase):
# test a bulk write, that one cannot change from 'ko' to 'ok'
builds = self.Build.browse([build.id, other.id])
with self.assertRaises(ValidationError):
builds.write({'local_result': 'ok'})
builds.write({'local_result': 'warn'})
# self.assertEqual(build.local_result, 'warn')
# self.assertEqual(other.local_result, 'ko')
def test_markdown_description(self):
build = self.Build.create({
@ -331,6 +337,11 @@ class TestBuildResult(RunbotCase):
build1 = self.Build.create({
'params_id': self.server_params.id,
})
self.assertEqual('pending', build1.global_state)
build1.local_state = 'testing'
self.assertEqual('testing', build1.global_state)
build1_1 = self.Build.create({
'params_id': self.server_params.id,
'parent_id': build1.id,
@ -339,6 +350,15 @@ class TestBuildResult(RunbotCase):
'params_id': self.server_params.id,
'parent_id': build1.id,
})
self.assertEqual('testing', build1.global_state)
self.assertEqual('pending', build1_1.global_state)
self.assertEqual('pending', build1_2.global_state)
build1_1.local_state = 'testing'
self.assertEqual('testing', build1.global_state)
self.assertEqual('testing', build1_1.global_state)
self.assertEqual('pending', build1_2.global_state)
build1_1_1 = self.Build.create({
'params_id': self.server_params.id,
'parent_id': build1_1.id,
@ -348,60 +368,132 @@ class TestBuildResult(RunbotCase):
'parent_id': build1_1.id,
})
def assert_state(global_state, build):
self.assertEqual(build.global_state, global_state)
self.assertEqual('testing', build1.global_state)
self.assertEqual('testing', build1_1.global_state)
self.assertEqual('pending', build1_2.global_state)
self.assertEqual('pending', build1_1_1.global_state)
self.assertEqual('pending', build1_1_2.global_state)
assert_state('pending', build1)
assert_state('pending', build1_1)
assert_state('pending', build1_2)
assert_state('pending', build1_1_1)
assert_state('pending', build1_1_2)
build1_2.flush()
with self.assertQueries(['''UPDATE "runbot_build" SET "global_state"=%s,"local_state"=%s,"write_date"=%s,"write_uid"=%s WHERE id IN %s''']):
build1_2.local_state = "testing"
build1_2.flush()
build1.local_state = 'testing'
build1_1.local_state = 'testing'
self.assertEqual('testing', build1.global_state)
self.assertEqual('testing', build1_2.global_state)
with self.assertQueries([]): # no change should be triggered
build1_2.local_state = "testing"
# with self.assertQueries(['''UPDATE "runbot_build" SET "global_state"=%s,"local_state"=%s,"write_date"=%s,"write_uid"=%s WHERE id IN %s''']):
build1.local_state = 'done'
build1.flush()
self.assertEqual('waiting', build1.global_state)
self.assertEqual('testing', build1_1.global_state)
# with self.assertQueries([]): # write the same value, no update should be triggered
build1.local_state = 'done'
build1.flush()
build1_1.local_state = 'done'
assert_state('waiting', build1)
assert_state('waiting', build1_1)
assert_state('pending', build1_2)
assert_state('pending', build1_1_1)
assert_state('pending', build1_1_2)
self.assertEqual('waiting', build1.global_state)
self.assertEqual('waiting', build1_1.global_state)
self.assertEqual('testing', build1_2.global_state)
self.assertEqual('pending', build1_1_1.global_state)
self.assertEqual('pending', build1_1_2.global_state)
build1_1_1.local_state = 'testing'
assert_state('waiting', build1)
assert_state('waiting', build1_1)
assert_state('pending', build1_2)
assert_state('testing', build1_1_1)
assert_state('pending', build1_1_2)
self.assertEqual('waiting', build1.global_state)
self.assertEqual('waiting', build1_1.global_state)
self.assertEqual('testing', build1_2.global_state)
self.assertEqual('testing', build1_1_1.global_state)
self.assertEqual('pending', build1_1_2.global_state)
build1_2.local_state = 'testing'
with self.assertQueries([]):
build1_2.local_state = 'testing'
assert_state('waiting', build1)
assert_state('waiting', build1_1)
assert_state('testing', build1_2)
assert_state('testing', build1_1_1)
assert_state('pending', build1_1_2)
self.assertEqual('waiting', build1.global_state)
self.assertEqual('waiting', build1_1.global_state)
self.assertEqual('testing', build1_2.global_state)
self.assertEqual('testing', build1_1_1.global_state)
self.assertEqual('pending', build1_1_2.global_state)
build1_2.local_state = 'testing' # writing same state a second time
build1_2.local_state = 'done'
build1_1_1.local_state = 'done'
build1_1_2.local_state = 'testing'
assert_state('waiting', build1)
assert_state('waiting', build1_1)
assert_state('testing', build1_2)
assert_state('testing', build1_1_1)
assert_state('pending', build1_1_2)
self.assertEqual('waiting', build1.global_state)
self.assertEqual('waiting', build1_1.global_state)
self.assertEqual('done', build1_2.global_state)
self.assertEqual('done', build1_1_1.global_state)
self.assertEqual('testing', build1_1_2.global_state)
build1_1_2.local_state = 'done'
self.assertEqual('done', build1.global_state)
self.assertEqual('done', build1_1.global_state)
self.assertEqual('done', build1_2.global_state)
self.assertEqual('done', build1_1_1.global_state)
self.assertEqual('done', build1_1_2.global_state)
def test_rebuild_sub_sub_build(self):
build1 = self.Build.create({
'params_id': self.server_params.id,
})
build1.local_state = 'testing'
build1_1 = self.Build.create({
'params_id': self.server_params.id,
'parent_id': build1.id,
})
build1_1.local_state = 'testing'
build1.local_state = 'done'
build1_1_1 = self.Build.create({
'params_id': self.server_params.id,
'parent_id': build1_1.id,
})
build1_1_1.local_state = 'testing'
build1_1.local_state = 'done'
self.assertEqual('waiting', build1.global_state)
self.assertEqual('waiting', build1_1.global_state)
self.assertEqual('testing', build1_1_1.global_state)
build1_1_1.local_result = 'ko'
build1_1_1.local_state = 'done'
build1_2.local_state = 'done'
self.assertEqual('done', build1.global_state)
self.assertEqual('done', build1_1.global_state)
self.assertEqual('done', build1_1_1.global_state)
self.assertEqual('ko', build1.global_result)
self.assertEqual('ko', build1_1.global_result)
self.assertEqual('ko', build1_1_1.global_result)
assert_state('done', build1)
assert_state('done', build1_1)
assert_state('done', build1_2)
assert_state('done', build1_1_1)
assert_state('done', build1_1_2)
rebuild1_1_1 = self.Build.create({ # this is a rebuild
'params_id': self.server_params.id,
'parent_id': build1_1.id,
})
build1_1_1.orphan_result = True
self.assertEqual('ok', build1.global_result)
self.assertEqual('ok', build1_1.global_result)
self.assertEqual('ko', build1_1_1.global_result)
self.assertEqual('waiting', build1.global_state)
self.assertEqual('waiting', build1_1.global_state)
self.assertEqual('done', build1_1_1.global_state)
self.assertEqual('pending', rebuild1_1_1.global_state)
rebuild1_1_1.local_result = 'ok'
rebuild1_1_1.local_state = 'done'
self.assertEqual('ok', build1.global_result)
self.assertEqual('ok', build1_1.global_result)
self.assertEqual('ko', build1_1_1.global_result)
self.assertEqual('ok', rebuild1_1_1.global_result)
self.assertEqual('done', build1.global_state)
self.assertEqual('done', build1_1.global_state)
self.assertEqual('done', build1_1_1.global_state)
self.assertEqual('done', rebuild1_1_1.global_state)
class TestGc(RunbotCaseMinimalSetup):
@ -446,6 +538,7 @@ class TestGc(RunbotCaseMinimalSetup):
build_a.write({'local_state': 'testing', 'host': host.name})
build_b.write({'local_state': 'testing', 'host': 'runbot_yyy'})
# no room needed, verify that nobody got killed
self.Runbot._gc_testing(host)
self.assertFalse(build_a.requested_action)
@ -487,3 +580,34 @@ class TestGc(RunbotCaseMinimalSetup):
self.assertFalse(build_b.requested_action)
self.assertFalse(build_a_last.requested_action)
self.assertFalse(children_b.requested_action)
class TestGithubStatus(RunbotCase):
def setUp(self):
super().setUp()
self.build = self.Build.create({
'params_id': self.base_params.id,
'description': 'A nice **description**'
})
def test_change_state(self):
self.callcount = 0
def github_status(build):
self.callcount += 1
with patch('odoo.addons.runbot.models.build.BuildResult._github_status', github_status):
self.callcount = 0
self.build.local_state = 'testing'
self.assertEqual(self.callcount, 0, "_github_status shouldn't have been called")
self.callcount = 0
self.build.local_state = 'running'
self.assertEqual(self.callcount, 1, "_github_status should have been called")
self.callcount = 0
self.build.local_state = 'done'
self.assertEqual(self.callcount, 0, "_github_status shouldn't have been called")

View File

@ -214,7 +214,6 @@ class TestBuildConfigStepCreate(TestBuildConfigStepCommon):
def test_config_step_create_results(self):
""" Test child builds are taken into account"""
self.config_step._run_create_build(self.parent_build, '/tmp/essai')
self.assertEqual(len(self.parent_build.children_ids), 2, 'Two sub-builds should have been generated')
@ -224,6 +223,7 @@ class TestBuildConfigStepCreate(TestBuildConfigStepCommon):
child_build.local_result = 'ko'
self.assertEqual(child_build.global_result, 'ko')
self.assertEqual(self.parent_build.global_result, 'ko')
def test_config_step_create(self):
@ -236,6 +236,7 @@ class TestBuildConfigStepCreate(TestBuildConfigStepCommon):
for child_build in self.parent_build.children_ids:
self.assertTrue(child_build.orphan_result, 'An orphan result config step should mark the build as orphan_result')
child_build.local_result = 'ko'
# child_build._update_:globals()
self.assertEqual(self.parent_build.global_result, 'ok')
@ -455,19 +456,19 @@ class TestBuildConfigStep(TestBuildConfigStepCommon):
self.patchers['docker_run'].side_effect = docker_run
config_step._run_step(self.parent_build, 'dev/null/logpath')
config_step._run_step(self.parent_build, 'dev/null/logpath')()
assert_db_name = 'custom_build'
parent_build_params = self.parent_build.params_id.copy({'config_data': {'db_name': 'custom_build'}})
parent_build = self.parent_build.copy({'params_id': parent_build_params.id})
config_step._run_step(parent_build, 'dev/null/logpath')
config_step._run_step(parent_build, 'dev/null/logpath')()
config_step = self.ConfigStep.create({
'name': 'run_test',
'job_type': 'run_odoo',
'custom_db_name': 'custom',
})
config_step._run_step(parent_build, 'dev/null/logpath')
config_step._run_step(parent_build, 'dev/null/logpath')()
self.assertEqual(call_count, 3)
@ -489,7 +490,7 @@ docker_params = dict(cmd=cmd)
self.assertIn('-d test_database', run_cmd)
self.patchers['docker_run'].side_effect = docker_run
config_step._run_step(self.parent_build, 'dev/null/logpath')
config_step._run_step(self.parent_build, 'dev/null/logpath')()
self.patchers['docker_run'].assert_called_once()
db = self.env['runbot.database'].search([('name', '=', 'test_database')])
self.assertEqual(db.build_id, self.parent_build)
@ -525,7 +526,7 @@ def run():
call_count += 1
self.patchers['docker_run'].side_effect = docker_run
config_step._run_step(self.parent_build, 'dev/null/logpath')
config_step._run_step(self.parent_build, 'dev/null/logpath')()
self.assertEqual(call_count, 1)
@ -564,10 +565,14 @@ Initiating shutdown
})
logs = []
with patch('builtins.open', mock_open(read_data=file_content)):
result = config_step._make_results(build)
self.assertEqual(result, {'job_end': '1970-01-01 02:00:00', 'local_result': 'ok'})
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 = []
file_content = """
Loading stuff
@ -575,26 +580,34 @@ odoo.stuff.modules.loading: Modules loaded.
Some post install stuff
"""
with patch('builtins.open', mock_open(read_data=file_content)):
result = config_step._make_results(build)
self.assertEqual(result, {'job_end': '1970-01-01 02:00:00', 'local_result': 'ko'})
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.')
])
# no loaded
build = self.Build.create({
'params_id': self.base_params.id,
})
logs = []
file_content = """
Loading stuff
"""
with patch('builtins.open', mock_open(read_data=file_content)):
result = config_step._make_results(build)
self.assertEqual(result, {'job_end': '1970-01-01 02:00:00', 'local_result': 'ko'})
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')
])
# traceback
build = self.Build.create({
'params_id': self.base_params.id,
})
logs = []
file_content = """
Loading stuff
@ -607,14 +620,18 @@ File "x.py", line a, in test_
Initiating shutdown
"""
with patch('builtins.open', mock_open(read_data=file_content)):
result = config_step._make_results(build)
self.assertEqual(result, {'job_end': '1970-01-01 02:00:00', 'local_result': 'ko'})
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')
])
# warning in logs
build = self.Build.create({
'params_id': self.base_params.id,
})
logs = []
file_content = """
Loading stuff
@ -624,8 +641,9 @@ Some post install stuff
Initiating shutdown
"""
with patch('builtins.open', mock_open(read_data=file_content)):
result = config_step._make_results(build)
self.assertEqual(result, {'job_end': '1970-01-01 02:00:00', 'local_result': 'warn'})
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),
('WARNING', 'Warning found in logs')
@ -634,15 +652,18 @@ Initiating shutdown
# no log file
logs = []
self.patchers['isfile'].return_value = False
result = config_step._make_results(build)
config_step._make_results(build)
self.assertEqual(result, {'local_result': 'ko'})
self.assertEqual(build.local_result, 'ko')
self.assertEqual(logs, [
('INFO', 'Getting results for build %s' % 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 = []
file_content = """
Loading stuff
@ -653,11 +674,12 @@ Initiating shutdown
self.patchers['isfile'].return_value = True
build.local_result = 'warn'
with patch('builtins.open', mock_open(read_data=file_content)):
result = config_step._make_results(build)
config_step._make_results(build)
self.assertEqual(logs, [
('INFO', 'Getting results for build %s' % build.dest)
])
self.assertEqual(result, {'job_end': '1970-01-01 02:00:00', 'local_result': 'warn'})
self.assertEqual(str(build.job_end), '1970-01-01 02:00:00')
self.assertEqual(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):
@ -672,18 +694,18 @@ Initiating shutdown
})
build.local_state = 'testing'
self.patchers['isfile'].return_value = False
result = config_step._make_results(build)
self.assertEqual(result, {'local_result': 'ok'})
config_step._make_results(build)
self.assertEqual(build.local_result, 'ok')
# invalid result code (no return_value set)
config_step.python_result_code = """a = 2*5\nr = {'a': 'ok'}\nreturn_value = 'ko'"""
with self.assertRaises(RunbotException):
result = config_step._make_results(build)
config_step._make_results(build)
# no result defined
config_step.python_result_code = ""
mock_make_tests_results.return_value = {'local_result': 'warning'}
result = config_step._make_results(build)
self.assertEqual(result, {'local_result': 'warning'})
mock_make_tests_results.return_value = {'local_result': 'warn'}
config_step._make_results(build)
self.assertEqual(build.local_result, 'warn')
# TODO add generic test to copy_paste _run_* in a python step

View File

@ -33,8 +33,10 @@ class TestBuildError(RunbotCase):
def test_build_scan(self):
IrLog = self.env['ir.logging']
ko_build = self.create_test_build({'local_result': 'ko'})
ok_build = self.create_test_build({'local_result': 'ok'})
ko_build = self.create_test_build({'local_result': 'ok', 'local_state': 'testing'})
ok_build = self.create_test_build({'local_result': 'ok', 'local_state': 'running'})
error_team = self.BuildErrorTeam.create({
'name': 'test-error-team',
@ -56,6 +58,10 @@ class TestBuildError(RunbotCase):
IrLog.create(log)
log.update({'build_id': ok_build.id})
IrLog.create(log)
self.assertEqual(ko_build.local_result, 'ko', 'Testing build should have gone ko after error log')
self.assertEqual(ok_build.local_result, 'ok', 'Running build should not have gone ko after error log')
ko_build._parse_logs()
ok_build._parse_logs()
build_error = self.BuildError.search([('build_ids', 'in', [ko_build.id])])

View File

@ -65,7 +65,7 @@ class TestHost(RunbotCase):
# 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.test_host._process_logs()
self.patchers['host_local_pg_cursor'].assert_called()
self.assertEqual(
self.env['ir.logging'].search_count([
@ -78,7 +78,7 @@ class TestHost(RunbotCase):
# 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.test_host._process_logs()
self.patchers['host_local_pg_cursor'].assert_called()
self.assertEqual(
self.env['ir.logging'].search_count([
@ -88,12 +88,12 @@ class TestHost(RunbotCase):
]),
1,
)
self.assertEqual(build.triggered_result, 'warn', 'A warning log should sets the build in warn')
self.assertEqual(build.local_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.test_host._process_logs()
self.patchers['host_local_pg_cursor'].assert_called()
self.assertEqual(
self.env['ir.logging'].search_count([
@ -103,11 +103,11 @@ class TestHost(RunbotCase):
]),
1,
)
self.assertEqual(build.triggered_result, 'ko', 'An error log should sets the build in ko')
self.assertEqual(build.local_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.test_host._process_logs()
self.patchers['host_local_pg_cursor'].assert_called()

View File

@ -23,6 +23,7 @@ class TestSchedule(RunbotCase):
build = self.Build.create({
'local_state': 'testing',
'global_state': 'testing',
'port': '1234',
'host': host.name,
'job_start': datetime.datetime.now(),
@ -33,9 +34,10 @@ class TestSchedule(RunbotCase):
self.assertEqual(build.local_state, 'testing')
build._schedule() # too fast, docker not started
self.assertEqual(build.local_state, 'testing')
self.assertEqual(build.local_result, 'ok')
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')
self.assertEqual(build.local_result, 'ok')
self.assertEqual(build.local_result, 'ko')

View File

@ -1,6 +1,6 @@
import getpass
import logging
import getpass
from unittest.mock import patch, mock_open
from odoo.exceptions import UserError
from odoo.tools import mute_logger
from .common import RunbotCase
@ -270,9 +270,9 @@ class TestUpgradeFlow(RunbotCase):
batch = self.master_bundle._force()
batch._prepare()
upgrade_current_build = batch.slot_ids.filtered(lambda slot: slot.trigger_id == self.trigger_upgrade_server).build_id
host = self.env['runbot.host']._get_current()
upgrade_current_build.host = host.name
upgrade_current_build._init_pendings(host)
#host = self.env['runbot.host']._get_current()
#upgrade_current_build.host = host.name
upgrade_current_build._schedule()
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')
@ -296,9 +296,8 @@ class TestUpgradeFlow(RunbotCase):
# upgrade repos tests
upgrade_build = batch.slot_ids.filtered(lambda slot: slot.trigger_id == self.trigger_upgrade).build_id
host = self.env['runbot.host']._get_current()
upgrade_build.host = host.name
upgrade_build._init_pendings(host)
#upgrade_build.host = host.name
upgrade_build._schedule()
upgrade_build._schedule()
self.assertEqual(upgrade_build.local_state, 'done')
self.assertEqual(len(upgrade_build.children_ids), 2)
@ -337,9 +336,8 @@ class TestUpgradeFlow(RunbotCase):
batch = self.master_bundle._force(self.nightly_category.id)
batch._prepare()
upgrade_nightly = batch.slot_ids.filtered(lambda slot: slot.trigger_id == trigger_upgrade_addons_nightly).build_id
host = self.env['runbot.host']._get_current()
upgrade_nightly.host = host.name
upgrade_nightly._init_pendings(host)
#upgrade_nightly.host = host.name
upgrade_nightly._schedule()
upgrade_nightly._schedule()
to_version_builds = upgrade_nightly.children_ids
self.assertEqual(upgrade_nightly.local_state, 'done')
@ -352,10 +350,15 @@ class TestUpgradeFlow(RunbotCase):
to_version_builds.mapped('params_id.upgrade_from_build_id.params_id.version_id.name'),
[]
)
to_version_builds.host = host.name
to_version_builds._init_pendings(host)
to_version_builds._schedule()
self.assertEqual(to_version_builds.mapped('local_state'), ['done']*4)
#to_version_builds.host = host.name
for build in to_version_builds:
build._schedule() # starts builds
self.assertEqual(build.local_state, 'testing')
build._schedule() # makes result and end build
self.assertEqual(build.local_state, 'done')
self.assertEqual(to_version_builds.mapped('global_state'), ['done', 'waiting', 'waiting', 'waiting'], 'One build have no child, other should wait for children')
from_version_builds = to_version_builds.children_ids
self.assertEqual(
[
@ -367,10 +370,15 @@ class TestUpgradeFlow(RunbotCase):
],
['11.0->12.0', 'saas-11.3->12.0', '12.0->13.0', 'saas-12.3->13.0', '13.0->master', 'saas-13.1->master', 'saas-13.2->master', 'saas-13.3->master']
)
from_version_builds.host = host.name
from_version_builds._init_pendings(host)
from_version_builds._schedule()
self.assertEqual(from_version_builds.mapped('local_state'), ['done']*8)
#from_version_builds.host = host.name
for build in from_version_builds:
build._schedule()
self.assertEqual(build.local_state, 'testing')
build._schedule()
self.assertEqual(build.local_state, 'done')
self.assertEqual(from_version_builds.mapped('global_state'), ['waiting'] * 8)
db_builds = from_version_builds.children_ids
self.assertEqual(len(db_builds), 40)
@ -405,61 +413,74 @@ class TestUpgradeFlow(RunbotCase):
[b.params_id.dump_db.db_suffix for b in b133_master],
['account', 'l10n_be', 'l10n_ch', 'mail', 'stock'] # is this order ok?
)
current_build = db_builds[0]
for current_build in db_builds:
self.start_patcher('docker_state', 'odoo.addons.runbot.models.build.docker_state', 'END')
first_build = db_builds[0]
suffix = current_build.params_id.dump_db.db_suffix
source_dest = current_build.params_id.dump_db.build_id.dest
self.start_patcher('docker_state', 'odoo.addons.runbot.models.build.docker_state', 'END')
def docker_run_restore(cmd, *args, **kwargs):
dump_url = f'http://host.runbot.com/runbot/static/build/{source_dest}/logs/{source_dest}-{suffix}.zip'
zip_name = f'{source_dest}-{suffix}.zip'
db_name = f'{current_build.dest}-{suffix}'
self.assertEqual(
str(cmd).split(' && '),
[
'mkdir /data/build/restore',
'cd /data/build/restore',
f'wget {dump_url}',
f'unzip -q {zip_name}',
'echo "### restoring filestore"',
f'mkdir -p /data/build/datadir/filestore/{db_name}',
f'mv filestore/* /data/build/datadir/filestore/{db_name}',
'echo "### restoring db"',
f'psql -q {db_name} < dump.sql',
'cd /data/build',
'echo "### cleaning"',
'rm -r restore',
'echo "### listing modules"',
f'psql {db_name} -c "select name from ir_module_module where state = \'installed\'" -t -A > /data/build/logs/restore_modules_installed.txt',
'echo "### restore" "successful"'
]
)
self.patchers['docker_run'].side_effect = docker_run_restore
#current_build.host = host.name
current_build._schedule()()
self.patchers['docker_run'].assert_called()
def docker_run_restore(cmd, *args, **kwargs):
source_dest = first_build.params_id.dump_db.build_id.dest
dump_url='http://host.runbot.com/runbot/static/build/%s/logs/%s-account.zip' % (source_dest, source_dest)
zip_name='%s-account.zip' % source_dest
db_name='%s-master-account' % str(first_build.id).zfill(5)
self.assertEqual(
str(cmd).split(' && '),
[
'mkdir /data/build/restore',
'cd /data/build/restore',
f'wget {dump_url}',
f'unzip -q {zip_name}',
'echo "### restoring filestore"',
f'mkdir -p /data/build/datadir/filestore/{db_name}',
f'mv filestore/* /data/build/datadir/filestore/{db_name}',
'echo "### restoring db"',
f'psql -q {db_name} < dump.sql',
'cd /data/build',
'echo "### cleaning"',
'rm -r restore',
'echo "### listing modules"',
f'psql {db_name} -c "select name from ir_module_module where state = \'installed\'" -t -A > /data/build/logs/restore_modules_installed.txt',
'echo "### restore" "successful"'
]
)
self.patchers['docker_run'].side_effect = docker_run_restore
first_build.host = host.name
first_build._init_pendings(host)
self.patchers['docker_run'].assert_called()
def docker_run_upgrade(cmd, *args, ro_volumes=False, **kwargs):
user = getpass.getuser()
self.assertTrue(ro_volumes.pop(f'/home/{user}/.odoorc').startswith('/tmp/runbot_test/static/build/'))
self.assertEqual(
list(ro_volumes.keys()), [
'/data/build/addons',
'/data/build/server',
'/data/build/upgrade',
],
"other commit should have been added automaticaly"
)
self.assertEqual(
str(cmd),
'python3 server/server.py {addons_path} --no-xmlrpcs --no-netrpc -u all -d {db_name} --stop-after-init --max-cron-threads=0'.format(
addons_path='--addons-path addons,server/addons,server/core/addons',
db_name=f'{current_build.dest}-{suffix}')
)
self.patchers['docker_run'].side_effect = docker_run_upgrade
current_build._schedule()()
def docker_run_upgrade(cmd, *args, ro_volumes=False, **kwargs):
user = getpass.getuser()
self.assertTrue(ro_volumes.pop(f'/home/{user}/.odoorc').startswith('/tmp/runbot_test/static/build/'))
self.assertEqual(
ro_volumes, {
'/data/build/addons': '/tmp/runbot_test/static/sources/addons/addons120',
'/data/build/server': '/tmp/runbot_test/static/sources/server/server120',
'/data/build/upgrade': '/tmp/runbot_test/static/sources/upgrade/123abc789',
},
"other commit should have been added automaticaly"
)
self.assertEqual(
str(cmd),
'python3 server/server.py {addons_path} --no-xmlrpcs --no-netrpc -u all -d {db_name} --stop-after-init --max-cron-threads=0'.format(
addons_path='--addons-path addons,server/addons,server/core/addons',
db_name='%s-master-account' % str(first_build.id).zfill(5))
)
self.patchers['docker_run'].side_effect = docker_run_upgrade
first_build._schedule()
self.assertEqual(self.patchers['docker_run'].call_count, 2)
with patch('builtins.open', mock_open(read_data='')):
current_build._schedule()
self.assertEqual(current_build.local_state, 'done')
self.assertEqual(current_build.global_state, 'done')
# self.assertEqual(current_build.global_result, 'ok')
self.assertEqual(self.patchers['docker_run'].call_count, 80)
self.assertEqual(from_version_builds.mapped('global_state'), ['done'] * 8)
self.assertEqual(to_version_builds.mapped('global_state'), ['done'] * 4)
# test_build_references
batch = self.master_bundle._force()
@ -520,12 +541,12 @@ class TestUpgradeFlow(RunbotCase):
batch13 = bundle_13._force()
batch13._prepare()
upgrade_complement_build_13 = batch13.slot_ids.filtered(lambda slot: slot.trigger_id == trigger_upgrade_complement).build_id
upgrade_complement_build_13.host = host.name
# upgrade_complement_build_13.host = host.name
self.assertEqual(upgrade_complement_build_13.params_id.config_id, config_upgrade_complement)
for db in ['base', 'all', 'no-demo-all']:
upgrade_complement_build_13.database_ids = [(0, 0, {'name': '%s-%s' % (upgrade_complement_build_13.dest, db)})]
upgrade_complement_build_13._init_pendings(host)
upgrade_complement_build_13._schedule()
self.assertEqual(len(upgrade_complement_build_13.children_ids), 5)
master_child = upgrade_complement_build_13.children_ids[0]
@ -534,6 +555,7 @@ class TestUpgradeFlow(RunbotCase):
self.assertEqual(master_child.params_id.config_id, self.test_upgrade_config)
self.assertEqual(master_child.params_id.upgrade_to_build_id.params_id.version_id.name, 'master')
class TestUpgrade(RunbotCase):
def test_exceptions_in_env(self):

View File

@ -68,7 +68,6 @@
<field name="requested_action" groups="base.group_no_one"/>
<field name="local_result"/>
<field name="global_result"/>
<field name="triggered_result" groups="base.group_no_one"/>
<field name="host"/>
<field name="host_id"/>
<field name="job_start" groups="base.group_no_one"/>

View File

@ -61,7 +61,6 @@
<field name="python_code" widget="ace" options="{'mode': 'python'}"/>
<field name="python_result_code" widget="ace" options="{'mode': 'python'}"/>
<field name="running_job"/>
<field name="ignore_triggered_result"/>
</group>
<group string="Test settings" attrs="{'invisible': [('job_type', 'not in', ('python', 'install_odoo'))]}">
<field name="create_db" groups="base.group_no_one"/>