From 4ac6a0db98ce94e8d8f96be60271516dc505a161 Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Fri, 30 Nov 2018 16:58:00 +0100 Subject: [PATCH] [IMP] runbot: add basic tests Deploying non tested code is prone to errors and leads to frustration. With this commit, basic tests are done to avoid such a situation. --- runbot/common.py | 1 - runbot/tests/__init__.py | 5 + runbot/tests/test_branch.py | 52 ++++++++ runbot/tests/test_build.py | 215 ++++++++++++++++++++++++++++++++++ runbot/tests/test_frontend.py | 71 +++++++++++ runbot/tests/test_jobs.py | 56 +++++++++ runbot/tests/test_repo.py | 17 +++ 7 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 runbot/tests/__init__.py create mode 100644 runbot/tests/test_branch.py create mode 100644 runbot/tests/test_build.py create mode 100644 runbot/tests/test_frontend.py create mode 100644 runbot/tests/test_jobs.py create mode 100644 runbot/tests/test_repo.py diff --git a/runbot/common.py b/runbot/common.py index 154ce814..07cdf69f 100644 --- a/runbot/common.py +++ b/runbot/common.py @@ -86,4 +86,3 @@ def get_py_version(build): if f.readline().strip().endswith('python3'): return 'python3' return 'python' - diff --git a/runbot/tests/__init__.py b/runbot/tests/__init__.py new file mode 100644 index 00000000..809caab7 --- /dev/null +++ b/runbot/tests/__init__.py @@ -0,0 +1,5 @@ +from . import test_repo +from . import test_branch +from . import test_build +from . import test_jobs +from . import test_frontend \ No newline at end of file diff --git a/runbot/tests/test_branch.py b/runbot/tests/test_branch.py new file mode 100644 index 00000000..c626a1b8 --- /dev/null +++ b/runbot/tests/test_branch.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from unittest.mock import patch +from odoo.tests import common + +class Test_Branch(common.TransactionCase): + + def setUp(self): + super(Test_Branch, self).setUp() + Repo = self.env['runbot.repo'] + self.repo = Repo.create({'name': 'bla@example.com:foo/bar', 'token': '123'}) + self.Branch = self.env['runbot.branch'] + + #mock_patch = patch('odoo.addons.runbot.models.repo.runbot_repo._github', self._github) + #mock_patch.start() + #self.addCleanup(mock_patch.stop) + + def test_base_fields(self): + branch = self.Branch.create({ + 'repo_id': self.repo.id, + 'name': 'refs/head/master' + }) + + self.assertEqual(branch.branch_name, 'master') + self.assertEqual(branch.branch_url, 'https://example.com/foo/bar/tree/master') + + @patch('odoo.addons.runbot.models.repo.runbot_repo._github') + def test_pull_request(self, mock_github): + mock_github.return_value = { + 'head' : {'label': 'foo-dev:bar_branch'}, + 'base' : {'ref': 'master'}, + } + pr = self.Branch.create({ + 'repo_id': self.repo.id, + 'name': 'refs/pull/12345' + }) + self.assertEqual(pr.branch_name, '12345') + self.assertEqual(pr.branch_url, 'https://example.com/foo/bar/pull/12345') + self.assertEqual(pr.target_branch_name, 'master') + self.assertEqual(pr.pull_head_name, 'foo-dev:bar_branch') + + def test_coverage_in_name(self): + """Test that coverage in branch name enables coverage""" + branch = self.Branch.create({ + 'repo_id': self.repo.id, + 'name': 'refs/head/foo-branch-bar' + }) + self.assertFalse(branch.coverage) + cov_branch = self.Branch.create({ + 'repo_id': self.repo.id, + 'name': 'refs/head/foo-coverage-branch-bar' + }) + self.assertTrue(cov_branch.coverage) diff --git a/runbot/tests/test_build.py b/runbot/tests/test_build.py new file mode 100644 index 00000000..0189cad3 --- /dev/null +++ b/runbot/tests/test_build.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +from unittest.mock import patch +from odoo.tests import common + +class Test_Build(common.TransactionCase): + + def setUp(self): + super(Test_Build, self).setUp() + self.Repo = self.env['runbot.repo'] + self.repo = self.Repo.create({'name': 'bla@example.com:foo/bar'}) + self.Branch = self.env['runbot.branch'] + self.branch = self.Branch.create({ + 'repo_id': self.repo.id, + 'name': 'refs/heads/master' + }) + self.branch_10 = self.Branch.create({ + 'repo_id': self.repo.id, + 'name': 'refs/heads/10.0' + }) + self.branch_11 = self.Branch.create({ + 'repo_id': self.repo.id, + 'name': 'refs/heads/11.0' + }) + self.Build = self.env['runbot.build'] + + @patch('odoo.addons.runbot.models.build.fqdn') + def test_base_fields(self, mock_fqdn): + build = self.Build.create({ + 'branch_id': self.branch.id, + 'name': 'd0d0caca0000ffffffffffffffffffffffffffff', + 'port' : '1234', + }) + self.assertEqual(build.id, build.sequence) + self.assertEqual(build.dest, '%05d-master-d0d0ca' % build.id) + # test dest change on new commit + build.name = 'deadbeef0000ffffffffffffffffffffffffffff' + self.assertEqual(build.dest, '%05d-master-deadbe' % build.id) + + # Test domain compute with fqdn and ir.config_parameter + mock_fqdn.return_value = 'runbot98.nowhere.org' + self.assertEqual(build.domain, 'runbot98.nowhere.org:1234') + self.env['ir.config_parameter'].set_param('runbot.runbot_domain', 'runbot99.example.org') + build._get_domain() + self.assertEqual(build.domain, 'runbot99.example.org:1234') + + def test_pr_is_duplicate(self): + """ test PR is a duplicate of a dev branch build """ + dup_repo = self.Repo.create({ + 'name': 'bla@example.com:foo-dev/bar', + 'duplicate_id': self.repo.id + }) + self.repo.duplicate_id = dup_repo.id + dev_branch = self.Branch.create({ + 'repo_id': dup_repo.id, + 'name': 'refs/heads/10.0-fix-thing-moc' + }) + dev_build = self.Build.create({ + 'branch_id': dev_branch.id, + 'name': 'd0d0caca0000ffffffffffffffffffffffffffff', + }) + pr = self.Branch.create({ + 'repo_id': self.repo.id, + 'name': 'refs/pull/12345' + }) + + pr_build = self.Build.create({ + 'branch_id': pr.id, + 'name': 'd0d0caca0000ffffffffffffffffffffffffffff', + }) + self.assertEqual(pr_build.state, 'duplicate') + self.assertEqual(pr_build.duplicate_id.id, dev_build.id) + + def test_dev_is_duplicate(self): + """ test dev branch build is a duplicate of a PR """ + dup_repo = self.Repo.create({ + 'name': 'bla@example.com:foo-dev/bar', + 'duplicate_id': self.repo.id + }) + self.repo.duplicate_id = dup_repo.id + dev_branch = self.Branch.create({ + 'repo_id': dup_repo.id, + 'name': 'refs/heads/10.0-fix-thing-moc' + }) + pr = self.Branch.create({ + 'repo_id': self.repo.id, + 'name': 'refs/pull/12345' + }) + + pr_build = self.Build.create({ + 'branch_id': pr.id, + 'name': 'd0d0caca0000ffffffffffffffffffffffffffff', + }) + dev_build = self.Build.create({ + 'branch_id': dev_branch.id, + 'name': 'd0d0caca0000ffffffffffffffffffffffffffff', + }) + self.assertEqual(dev_build.state, 'duplicate') + self.assertEqual(dev_build.duplicate_id.id, pr_build.id) + + @patch('odoo.addons.runbot.models.branch.runbot_branch._is_on_remote') + def test_closest_branch_01(self, mock_is_on_remote): + """ test find a matching branch in a target repo based on branch name """ + mock_is_on_remote.return_value = True + server_repo = self.Repo.create({'name': 'bla@example.com:foo-dev/bar'}) + addons_repo = self.Repo.create({'name': 'bla@example.com:ent-dev/bar'}) + self.Branch.create({ + 'repo_id': server_repo.id, + 'name': 'refs/heads/10.0-fix-thing-moc' + }) + addons_branch = self.Branch.create({ + 'repo_id': addons_repo.id, + 'name': 'refs/heads/10.0-fix-thing-moc' + }) + addons_build = self.Build.create({ + 'branch_id': addons_branch.id, + 'name': 'd0d0caca0000ffffffffffffffffffffffffffff', + }) + self.assertEqual((server_repo.id, addons_branch.name, 'exact'), addons_build._get_closest_branch_name(server_repo.id)) + + @patch('odoo.addons.runbot.models.repo.runbot_repo._github') + def test_closest_branch_02(self, mock_github): + """ test find two matching PR having the same head name """ + mock_github.return_value = { + 'head' : {'label': 'foo-dev:bar_branch'}, + 'base' : {'ref': 'master'}, + 'state': 'open' + } + server_repo = self.Repo.create({'name': 'bla@example.com:foo-dev/bar', 'token': '1'}) + addons_repo = self.Repo.create({'name': 'bla@example.com:ent-dev/bar', 'token': '1'}) + server_pr = self.Branch.create({ + 'repo_id': server_repo.id, + 'name': 'refs/pull/123456' + }) + addons_pr = self.Branch.create({ + 'repo_id': addons_repo.id, + 'name': 'refs/pull/789101' + }) + addons_build = self.Build.create({ + 'branch_id': addons_pr.id, + 'name': 'd0d0caca0000ffffffffffffffffffffffffffff', + }) + self.assertEqual((server_repo.id, server_pr.name, 'exact'), addons_build._get_closest_branch_name(server_repo.id)) + + @patch('odoo.addons.runbot.models.build.runbot_build._branch_exists') + def test_closest_branch_03(self, mock_branch_exists): + """ test find a branch based on dashed prefix""" + mock_branch_exists.return_value = True + addons_repo = self.Repo.create({'name': 'bla@example.com:ent-dev/bar', 'token': '1'}) + addons_branch = self.Branch.create({ + 'repo_id': addons_repo.id, + 'name': 'refs/heads/10.0-fix-blah-blah-moc' + }) + addons_build = self.Build.create({ + 'branch_id': addons_branch.id, + 'name': 'd0d0caca0000ffffffffffffffffffffffffffff', + }) + self.assertEqual((self.repo.id, 'refs/heads/10.0', 'prefix'), addons_build._get_closest_branch_name(self.repo.id)) + + @patch('odoo.addons.runbot.models.repo.runbot_repo._github') + def test_closest_branch_05(self, mock_github): + """ test last resort value """ + mock_github.return_value = { + 'head' : {'label': 'foo-dev:bar_branch'}, + 'base' : {'ref': '10.0'}, + 'state': 'open' + } + server_repo = self.Repo.create({'name': 'bla@example.com:foo-dev/bar', 'token': '1'}) + addons_repo = self.Repo.create({'name': 'bla@example.com:ent-dev/bar', 'token': '1'}) + server_pr = self.Branch.create({ + 'repo_id': server_repo.id, + 'name': 'refs/pull/123456' + }) + mock_github.return_value = { + 'head' : {'label': 'foo-dev:foobar_branch'}, + 'base' : {'ref': '10.0'}, + 'state': 'open' + } + addons_pr = self.Branch.create({ + 'repo_id': addons_repo.id, + 'name': 'refs/pull/789101' + }) + addons_build = self.Build.create({ + 'branch_id': addons_pr.id, + 'name': 'd0d0caca0000ffffffffffffffffffffffffffff', + }) + self.assertEqual((server_repo.id, server_pr.target_branch_name, 'default'), addons_build._get_closest_branch_name(server_repo.id)) + +@patch('odoo.addons.runbot.models.repo.runbot_repo._github') +def test_closest_branch_05_master(self, mock_github): + """ test last resort value when nothing common can be found""" + mock_github.return_value = { + 'head' : {'label': 'foo-dev:bar_branch'}, + 'base' : {'ref': 'saas-14'}, + 'state': 'open' + } + server_repo = self.Repo.create({'name': 'bla@example.com:foo-dev/bar', 'token': '1'}) + addons_repo = self.Repo.create({'name': 'bla@example.com:ent-dev/bar', 'token': '1'}) + server_pr = self.Branch.create({ + 'repo_id': server_repo.id, + 'name': 'refs/pull/123456' + }) + mock_github.return_value = { + 'head' : {'label': 'foo-dev:foobar_branch'}, + 'base' : {'ref': '10.0'}, + 'state': 'open' + } + addons_pr = self.Branch.create({ + 'repo_id': addons_repo.id, + 'name': 'refs/pull/789101' + }) + addons_build = self.Build.create({ + 'branch_id': addons_pr.id, + 'name': 'd0d0caca0000ffffffffffffffffffffffffffff', + }) + self.assertEqual((server_repo.id, 'master', 'default'), addons_build._get_closest_branch_name(server_repo.id)) diff --git a/runbot/tests/test_frontend.py b/runbot/tests/test_frontend.py new file mode 100644 index 00000000..05535f0e --- /dev/null +++ b/runbot/tests/test_frontend.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +from itertools import cycle +from unittest.mock import patch +from werkzeug.wrappers import Response +from odoo.tests import common +from odoo.addons.runbot.controllers import frontend + + +class Test_Frontend(common.HttpCase): + + def setUp(self): + super(Test_Frontend, self).setUp() + Repo = self.env['runbot.repo'] + self.repo = Repo.create({'name': 'bla@example.com:foo/bar', 'token': '123'}) + self.Branch = self.env['runbot.branch'] + self.sticky_branch = self.Branch.create({ + 'repo_id': self.repo.id, + 'name': 'refs/heads/master', + 'sticky': True, + }) + self.branch = self.Branch.create({ + 'repo_id': self.repo.id, + 'name': 'refs/heads/master-test-moc', + 'sticky': False, + }) + self.Build = self.env['runbot.build'] + + @patch('odoo.http.Response.set_default') + @patch('odoo.addons.runbot.controllers.frontend.request') + def test_frontend_basic(self, mock_request, mock_set_default): + mock_request.env = self.env + mock_request._cr = self.cr + controller = frontend.Runbot() + + states = ['done', 'pending', 'testing', 'running', 'deathrow'] + branches = [self.branch, self.sticky_branch] + names = ['deadbeef', 'd0d0caca', 'deadface', 'cacafeed'] + # create 5 builds in each branch + for i, state, branch, name in zip(range(10), cycle(states), cycle(branches), cycle(names)): + self.Build.create({ + 'branch_id': branch.id, + 'name': '%s0000ffffffffffffffffffffffffffff' % name, + 'port': '1234', + 'state': state, + 'result': 'ok' + }) + + def mocked_simple_repo_render(template, context): + self.assertEqual(template, 'runbot.repo', 'The frontend controller should use "runbot.repo" template') + self.assertEqual(self.sticky_branch, context['branches'][0]['branch'], "The sticky branch should be in first place") + self.assertEqual(self.branch, context['branches'][1]['branch'], "The non sticky branch should be in second place") + self.assertEqual(len(context['branches'][0]['builds']), 4, "Only the 4 last builds should appear in the context") + self.assertEqual(context['pending'], 2, "There should be 2 pending builds") + self.assertEqual(context['running'], 2, "There should be 2 running builds") + self.assertEqual(context['testing'], 2, "There should be 2 testing builds") + self.assertEqual(context['pending_total'], 2, "There should be 2 pending builds") + self.assertEqual(context['pending_level'], 'info', "The pending level should be info") + return Response() + + mock_request.render = mocked_simple_repo_render + controller.repo() + + def mocked_repo_search_render(template, context): + dead_count = len([bu['name'] for b in context['branches'] for bu in b['builds'] if bu['name'].startswith('dead')]) + undead_count = len([bu['name'] for b in context['branches'] for bu in b['builds'] if not bu['name'].startswith('dead')]) + self.assertEqual(dead_count, 4, 'The search for "dead" should return 4 builds') + self.assertEqual(undead_count, 0, 'The search for "dead" should not return any build without "dead" in its name') + return Response() + + mock_request.render = mocked_repo_search_render + controller.repo(search='dead') diff --git a/runbot/tests/test_jobs.py b/runbot/tests/test_jobs.py new file mode 100644 index 00000000..eba10396 --- /dev/null +++ b/runbot/tests/test_jobs.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +import datetime +from time import localtime +from unittest.mock import patch +from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT +from odoo.tests import common + + +class Test_Jobs(common.TransactionCase): + + def setUp(self): + super(Test_Jobs, self).setUp() + self.Repo = self.env['runbot.repo'] + self.repo = self.Repo.create({'name': 'bla@example.com:foo/bar', 'token': 'xxx'}) + self.Branch = self.env['runbot.branch'] + self.branch_master = self.Branch.create({ + 'repo_id': self.repo.id, + 'name': 'refs/heads/master', + }) + self.Build = self.env['runbot.build'] + + @patch('odoo.addons.runbot.models.repo.runbot_repo._domain') + @patch('odoo.addons.runbot.models.repo.runbot_repo._github') + @patch('odoo.addons.runbot.models.build.runbot_build._cmd') + @patch('odoo.addons.runbot.models.build.os.path.getmtime') + @patch('odoo.addons.runbot.models.build.time.localtime') + @patch('odoo.addons.runbot.models.build.docker_run') + @patch('odoo.addons.runbot.models.build.grep') + def test_job_30_failed(self, mock_grep, mock_docker_run, mock_localtime, mock_getmtime, mock_cmd, mock_github, mock_domain): + """ Test that a failed build sets the failure state on github """ + a_time = datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT) + mock_grep.return_value = False + mock_docker_run.return_value = 2 + now = localtime() + mock_localtime.return_value = now + mock_getmtime.return_value = None + mock_cmd.return_value = ([], []) + mock_domain.return_value = 'runbotxx.somewhere.com' + build = self.Build.create({ + 'branch_id': self.branch_master.id, + 'name': 'd0d0caca0000ffffffffffffffffffffffffffff', + 'port' : '1234', + 'state': 'done', + 'job_start': a_time, + 'job_end': a_time + }) + self.assertFalse(build.result) + self.Build._job_30_run(build, '/tmp/x.log') + self.assertEqual(build.result, 'ko') + expected_status = { + 'state': 'failure', + 'target_url': 'http://runbotxx.somewhere.com/runbot/build/%s' % build.id, + 'description': 'runbot build %s (runtime 0s)' % build.dest, + 'context': 'ci/runbot' + } + mock_github.assert_called_with('/repos/:owner/:repo/statuses/d0d0caca0000ffffffffffffffffffffffffffff', expected_status, ignore_errors=True) diff --git a/runbot/tests/test_repo.py b/runbot/tests/test_repo.py new file mode 100644 index 00000000..74438519 --- /dev/null +++ b/runbot/tests/test_repo.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from unittest.mock import patch +from odoo.tests import common + +class Test_Repo(common.TransactionCase): + + def setUp(self): + super(Test_Repo, self).setUp() + self.Repo = self.env['runbot.repo'] + + @patch('odoo.addons.runbot.models.repo.runbot_repo._root') + def test_base_fields(self, mock_root): + mock_root.return_value = '/tmp/static' + repo = self.Repo.create({'name': 'bla@example.com:foo/bar'}) + self.assertEqual(repo.path, '/tmp/static/repo/bla_example.com_foo_bar') + + self.assertEqual(repo.base, 'example.com/foo/bar')