Skip to content

Commit

Permalink
Release Automation [Milestone 4]: Add pre commit tests in deploy, aut…
Browse files Browse the repository at this point in the history
…omate hotfix branch creation (oppia#7790)

* Add tests for deploy script

* Make coverage 100%

* Fix name

* Add check for configs

* Add newline

* Fix name

* Add tests for clean.py

* Add tests for gcloud adapter

* Add test for install third party

* Add tests for install third party libs

* Add test for pre push hook script

* Add test for pre commit hook script

* Add try finally

* Add test for setup scripts

* Add test for setup gae script

* Add extra checks in deploy script

* Fix linter

* Add pre commit test for config updates

* Make coverage 100%

* Add test for last commit message

* Add automation for cutting hotfix branch

* Address review comments

* Address review comments

* Make coverage 100%

* Update test

* Address review comments
  • Loading branch information
ankita240796 authored Nov 4, 2019
1 parent f03d34c commit 92f58b3
Show file tree
Hide file tree
Showing 32 changed files with 3,865 additions and 484 deletions.
2 changes: 2 additions & 0 deletions core/tests/release_sources/feconf.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ INCOMING_EMAILS_DOMAIN_NAME = ''
ADMIN_EMAIL_ADDRESS = '[email protected]'
SYSTEM_EMAIL_ADDRESS = '[email protected]'
NOREPLY_EMAIL_ADDRESS = '[email protected]'

MAILGUN_API_KEY = None
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"DEV_MODE": true,
"GCS_RESOURCE_BUCKET_NAME": "Bucket-name",
"Testing": "Deploy-tests"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"DEV_MODE": false,
"GCS_RESOURCE_BUCKET_NAME": "None-resources",
"Testing": "Deploy-tests"
Binary file added core/tests/release_sources/tmp_unzip.tar.gz
Binary file not shown.
Binary file added core/tests/release_sources/tmp_unzip.zip
Binary file not shown.
6 changes: 6 additions & 0 deletions core/tests/release_sources/valid_constants.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"DEV_MODE": true,
"GCS_RESOURCE_BUCKET_NAME": "None-resources",
"ANALYTICS_ID": "",
"SITE_NAME_FOR_ANALYTICS": "",
"SITE_FEEDBACK_FORM_URL": "",
"Testing": "Deploy-tests"
5 changes: 4 additions & 1 deletion scripts/clean.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

"""Deletes temporary and installed files."""

from __future__ import absolute_import # pylint: disable=import-only-modules
from __future__ import unicode_literals # pylint: disable=import-only-modules

Expand Down Expand Up @@ -76,5 +77,7 @@ def main(args=None):
python_utils.PRINT('Temporary and installed files deleted')


if __name__ == '__main__':
# The 'no coverage' pragma is used as this line is un-testable. This is because
# it will only be called when clean.py is used as a script.
if __name__ == '__main__': # pragma: no cover
main()
126 changes: 126 additions & 0 deletions scripts/clean_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# coding: utf-8
#
# Copyright 2019 The Oppia Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS-IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Unit tests for scripts/clean_test.py."""

from __future__ import absolute_import # pylint: disable=import-only-modules
from __future__ import unicode_literals # pylint: disable=import-only-modules

import os
import shutil

from core.tests import test_utils

from . import clean


class CleanTests(test_utils.GenericTestBase):
"""Test the methods for clean script."""
def test_delete_directory_with_missing_dir(self):
check_function_calls = {
'rmtree_is_called': False
}
expected_check_function_calls = {
'rmtree_is_called': False
}
def mock_rmtree(unused_path):
check_function_calls['rmtree_is_called'] = True
def mock_exists(unused_path):
return False

rmtree_swap = self.swap(shutil, 'rmtree', mock_rmtree)
exists_swap = self.swap(os.path, 'exists', mock_exists)
with rmtree_swap, exists_swap:
clean.delete_directory_tree('dir_path')
self.assertEqual(check_function_calls, expected_check_function_calls)

def test_delete_directory_with_existing_dir(self):
check_function_calls = {
'rmtree_is_called': False
}
expected_check_function_calls = {
'rmtree_is_called': True
}
def mock_rmtree(unused_path):
check_function_calls['rmtree_is_called'] = True
def mock_exists(unused_path):
return True

rmtree_swap = self.swap(shutil, 'rmtree', mock_rmtree)
exists_swap = self.swap(os.path, 'exists', mock_exists)
with rmtree_swap, exists_swap:
clean.delete_directory_tree('dir_path')
self.assertEqual(check_function_calls, expected_check_function_calls)

def test_delete_file_with_missing_file(self):
check_function_calls = {
'remove_is_called': False
}
expected_check_function_calls = {
'remove_is_called': False
}
def mock_remove(unused_path):
check_function_calls['remove_is_called'] = True
def mock_isfile(unused_path):
return False

remove_swap = self.swap(os, 'remove', mock_remove)
isfile_swap = self.swap(os.path, 'isfile', mock_isfile)
with remove_swap, isfile_swap:
clean.delete_file('file_path')
self.assertEqual(check_function_calls, expected_check_function_calls)

def test_delete_file_with_existing_file(self):
check_function_calls = {
'remove_is_called': False
}
expected_check_function_calls = {
'remove_is_called': True
}
def mock_remove(unused_path):
check_function_calls['remove_is_called'] = True
def mock_isfile(unused_path):
return True

remove_swap = self.swap(os, 'remove', mock_remove)
isfile_swap = self.swap(os.path, 'isfile', mock_isfile)
with remove_swap, isfile_swap:
clean.delete_file('file_path')
self.assertEqual(check_function_calls, expected_check_function_calls)

def test_function_calls(self):
check_function_calls = {
'delete_directory_tree_is_called': 0,
'delete_file_is_called': 0
}
expected_check_function_calls = {
'delete_directory_tree_is_called': 8,
'delete_file_is_called': 4
}
def mock_delete_dir(unused_path):
check_function_calls['delete_directory_tree_is_called'] += 1
def mock_delete_file(unused_path):
check_function_calls['delete_file_is_called'] += 1
def mock_listdir(unused_path):
return ['tmpcompiledjs_dir']
delete_dir_swap = self.swap(
clean, 'delete_directory_tree', mock_delete_dir)
delete_file_swap = self.swap(clean, 'delete_file', mock_delete_file)
listdir_swap = self.swap(os, 'listdir', mock_listdir)

with delete_dir_swap, delete_file_swap, listdir_swap:
clean.main(args=[])
self.assertEqual(check_function_calls, expected_check_function_calls)
51 changes: 51 additions & 0 deletions scripts/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,57 @@ def get_personal_access_token():
return personal_access_token


def check_blocking_bug_issue_count(repo):
"""Checks the number of unresolved blocking bugs.
Args:
repo: github.Repository.Repository. The PyGithub object for the repo.
Raises:
Exception: Number of unresolved blocking bugs is not zero.
Exception: The blocking bug milestone is closed.
"""
blocking_bugs_milestone = repo.get_milestone(
number=release_constants.BLOCKING_BUG_MILESTONE_NUMBER)
if blocking_bugs_milestone.state == 'closed':
raise Exception('The blocking bug milestone is closed.')
if blocking_bugs_milestone.open_issues:
open_new_tab_in_browser_if_possible(
'https://github.com/oppia/oppia/issues?q=is%3Aopen+'
'is%3Aissue+milestone%3A%22Blocking+bugs%22')
raise Exception(
'There are %s unresolved blocking bugs. Please ensure '
'that they are resolved before release summary generation.' % (
blocking_bugs_milestone.open_issues))


def check_prs_for_current_release_are_released(repo):
"""Checks that all pull requests for current release have a
'PR: released' label.
Args:
repo: github.Repository.Repository. The PyGithub object for the repo.
Raises:
Exception: Some pull requests for current release do not have a
PR: released label.
"""
current_release_label = repo.get_label(
release_constants.LABEL_FOR_CURRENT_RELEASE_PRS)
current_release_prs = repo.get_issues(
state='all', labels=[current_release_label])
for pr in current_release_prs:
label_names = [label.name for label in pr.labels]
if release_constants.LABEL_FOR_RELEASED_PRS not in label_names:
open_new_tab_in_browser_if_possible(
'https://github.com/oppia/oppia/pulls?utf8=%E2%9C%93&q=is%3Apr'
'+label%3A%22PR%3A+for+current+release%22+')
raise Exception(
'There are PRs for current release which do not have '
'a \'PR: released\' label. Please ensure that they are '
'released before release summary generation.')


class CD(python_utils.OBJECT):
"""Context manager for changing the current working directory."""

Expand Down
143 changes: 143 additions & 0 deletions scripts/common_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,18 @@

from core.tests import test_utils
import python_utils
import release_constants

from . import common

_PARENT_DIR = os.path.abspath(os.path.join(os.getcwd(), os.pardir))
_PY_GITHUB_PATH = os.path.join(_PARENT_DIR, 'oppia_tools', 'PyGithub-1.43.7')
sys.path.insert(0, _PY_GITHUB_PATH)

# pylint: disable=wrong-import-position
import github # isort:skip
# pylint: enable=wrong-import-position


class CommonTests(test_utils.GenericTestBase):
"""Test the methods which handle common functionalities."""
Expand Down Expand Up @@ -375,3 +384,137 @@ def mock_getpass(prompt):
'access token at https://github.com/settings/tokens and re-run '
'the script'):
common.get_personal_access_token()

def test_closed_blocking_bugs_milestone_results_in_exception(self):
mock_repo = github.Repository.Repository(
requester='', headers='', attributes={}, completed='')
# pylint: disable=unused-argument
def mock_get_milestone(unused_self, number):
return github.Milestone.Milestone(
requester='', headers='',
attributes={'state': 'closed'}, completed='')
# pylint: enable=unused-argument
get_milestone_swap = self.swap(
github.Repository.Repository, 'get_milestone', mock_get_milestone)
with get_milestone_swap, self.assertRaisesRegexp(
Exception, 'The blocking bug milestone is closed.'):
common.check_blocking_bug_issue_count(mock_repo)

def test_non_zero_blocking_bug_issue_count_results_in_exception(self):
mock_repo = github.Repository.Repository(
requester='', headers='', attributes={}, completed='')
def mock_open_tab(unused_url):
pass
# pylint: disable=unused-argument
def mock_get_milestone(unused_self, number):
return github.Milestone.Milestone(
requester='', headers='',
attributes={'open_issues': 10, 'state': 'open'}, completed='')
# pylint: enable=unused-argument
get_milestone_swap = self.swap(
github.Repository.Repository, 'get_milestone', mock_get_milestone)
open_tab_swap = self.swap(
common, 'open_new_tab_in_browser_if_possible', mock_open_tab)
with get_milestone_swap, open_tab_swap, self.assertRaisesRegexp(
Exception, (
'There are 10 unresolved blocking bugs. Please '
'ensure that they are resolved before release '
'summary generation.')):
common.check_blocking_bug_issue_count(mock_repo)

def test_zero_blocking_bug_issue_count_results_in_no_exception(self):
mock_repo = github.Repository.Repository(
requester='', headers='', attributes={}, completed='')
# pylint: disable=unused-argument
def mock_get_milestone(unused_self, number):
return github.Milestone.Milestone(
requester='', headers='',
attributes={'open_issues': 0, 'state': 'open'}, completed='')
# pylint: enable=unused-argument
with self.swap(
github.Repository.Repository, 'get_milestone', mock_get_milestone):
common.check_blocking_bug_issue_count(mock_repo)

def test_check_prs_for_current_release_are_released_with_no_unreleased_prs(
self):
mock_repo = github.Repository.Repository(
requester='', headers='', attributes={}, completed='')
pull1 = github.PullRequest.PullRequest(
requester='', headers='',
attributes={
'title': 'PR1', 'number': 1, 'labels': [
{'name': release_constants.LABEL_FOR_RELEASED_PRS},
{'name': release_constants.LABEL_FOR_CURRENT_RELEASE_PRS}]},
completed='')
pull2 = github.PullRequest.PullRequest(
requester='', headers='',
attributes={
'title': 'PR2', 'number': 2, 'labels': [
{'name': release_constants.LABEL_FOR_RELEASED_PRS},
{'name': release_constants.LABEL_FOR_CURRENT_RELEASE_PRS}]},
completed='')
label = github.Label.Label(
requester='', headers='',
attributes={
'name': release_constants.LABEL_FOR_CURRENT_RELEASE_PRS},
completed='')
# pylint: disable=unused-argument
def mock_get_issues(unused_self, state, labels):
return [pull1, pull2]
# pylint: enable=unused-argument
def mock_get_label(unused_self, unused_name):
return [label]

get_issues_swap = self.swap(
github.Repository.Repository, 'get_issues', mock_get_issues)
get_label_swap = self.swap(
github.Repository.Repository, 'get_label', mock_get_label)
with get_issues_swap, get_label_swap:
common.check_prs_for_current_release_are_released(mock_repo)

def test_check_prs_for_current_release_are_released_with_unreleased_prs(
self):
mock_repo = github.Repository.Repository(
requester='', headers='', attributes={}, completed='')
def mock_open_tab(unused_url):
pass
pull1 = github.PullRequest.PullRequest(
requester='', headers='',
attributes={
'title': 'PR1', 'number': 1, 'labels': [
{'name': release_constants.LABEL_FOR_CURRENT_RELEASE_PRS}]},
completed='')
pull2 = github.PullRequest.PullRequest(
requester='', headers='',
attributes={
'title': 'PR2', 'number': 2, 'labels': [
{'name': release_constants.LABEL_FOR_RELEASED_PRS},
{'name': release_constants.LABEL_FOR_CURRENT_RELEASE_PRS}]},
completed='')
label = github.Label.Label(
requester='', headers='',
attributes={
'name': release_constants.LABEL_FOR_CURRENT_RELEASE_PRS},
completed='')
# pylint: disable=unused-argument
def mock_get_issues(unused_self, state, labels):
return [pull1, pull2]
# pylint: enable=unused-argument
def mock_get_label(unused_self, unused_name):
return [label]

get_issues_swap = self.swap(
github.Repository.Repository, 'get_issues', mock_get_issues)
get_label_swap = self.swap(
github.Repository.Repository, 'get_label', mock_get_label)
open_tab_swap = self.swap(
common, 'open_new_tab_in_browser_if_possible', mock_open_tab)
with get_issues_swap, get_label_swap, open_tab_swap:
with self.assertRaisesRegexp(
Exception, (
'There are PRs for current release which do not '
'have a \'%s\' label. Please ensure that '
'they are released before release summary '
'generation.') % (
release_constants.LABEL_FOR_RELEASED_PRS)):
common.check_prs_for_current_release_are_released(mock_repo)
Loading

0 comments on commit 92f58b3

Please sign in to comment.