Skip to content

Commit 87dea72

Browse files
authored
Make the backend test for scripts/start.py 100 times faster. (oppia#23968)
* Update start scripts * More fixes to main scripts * Rewrite tests. * Clean up * Drop conditional startup based on EMULATOR_MODE * Final fixes * Simplify * Import order * fix mypy * Address comments * Remove skip-install from frontend tests; we always skip now. * Add coverage * fixes * Fix arg
1 parent 9fc013f commit 87dea72

File tree

12 files changed

+850
-512
lines changed

12 files changed

+850
-512
lines changed

core/feconf.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,9 @@ def get_empty_ratings() -> Dict[str, int]:
490490
# Valid Mailchimp tags.
491491
VALID_MAILCHIMP_TAGS = ['Account', 'Android', 'Web']
492492

493+
GAE_DEVELOPMENT_SERVER_PORT = 8181
494+
GAE_ADMIN_SERVER_PORT = 8000
495+
493496
ES_HOST = os.environ.get('ES_HOST', 'localhost')
494497
ES_LOCALHOST_PORT = 9200
495498
# NOTE TO RELEASE COORDINATORS: Replace this with the correct ElasticSearch

core/tests/test_utils.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2127,7 +2127,7 @@ def setUp(self) -> None:
21272127
storage_services.CLIENT.namespace = self.id()
21282128
# Set up apps for testing.
21292129
self.testapp = webtest.TestApp(main.app_without_context)
2130-
# Mock set_constans_to_default method to throw an exception.
2130+
# Mock set_constants_to_default method to throw an exception.
21312131
# Don't directly change constants file in the test.
21322132
# Mock this method again in your test.
21332133
self.contextManager = self.swap(
@@ -2219,11 +2219,16 @@ def get_pending_tasks(
22192219
)
22202220

22212221
def mock_set_constants_to_default(self) -> None:
2222-
"""Change constants file in the test could lead to other
2223-
tests fail. Mock set_constants_to_default method in your test
2224-
will suppress this exception.
2222+
"""Mock implementation of set_constants_to_default for tests.
2223+
2224+
Directly changing the shared constants file in your test can cause
2225+
problems. Mocking common.set_constants_to_default() in your test will
2226+
suppress this exception.
22252227
"""
2226-
raise Exception('Please mock this method in the test.')
2228+
raise Exception(
2229+
'Tests should mock common.set_constants_to_default() to avoid '
2230+
'modifying the constants file during tests.'
2231+
)
22272232

22282233
@contextlib.contextmanager
22292234
def mock_datetime_utcnow(

core/tests/test_utils_test.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,15 @@ def test_set_translation_coordinators(self) -> None:
864864
self.assertEqual('en', coordinator_rights_model[0].language_id)
865865
self.assertEqual('hi', coordinator_rights_model[1].language_id)
866866

867+
def test_mock_set_constants_to_default_raises_exception(self) -> None:
868+
"""Test that mock_set_constants_to_default raises an exception."""
869+
with self.assertRaisesRegex(
870+
Exception,
871+
r'Tests should mock common\.set_constants_to_default\(\) to avoid '
872+
r'modifying the constants file during tests\.',
873+
):
874+
self.mock_set_constants_to_default()
875+
867876

868877
class CheckImagePngOrWebpTests(test_utils.GenericTestBase):
869878

scripts/common.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,22 @@ def is_port_in_use(port: int) -> bool:
490490
return bool(not s.connect_ex(('localhost', port)))
491491

492492

493+
def get_ports_in_use(ports: list[int]) -> list[int]:
494+
"""Checks which ports from a list are currently in use.
495+
496+
Args:
497+
ports: list[int]. List of port numbers.
498+
499+
Returns:
500+
list[int]. List of port numbers that are currently in use.
501+
"""
502+
in_use = []
503+
for port in ports:
504+
if is_port_in_use(port):
505+
in_use.append(port)
506+
return in_use
507+
508+
493509
def recursive_chown(path: str, uid: int, gid: int) -> None:
494510
"""Changes the owner and group id of all files in a path to the numeric
495511
uid and gid.

scripts/common_test.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,24 @@ def test_is_port_in_use(self) -> None:
642642
self.assertTrue(common.is_port_in_use(port))
643643
self.assertFalse(common.is_port_in_use(port))
644644

645+
def test_get_ports_in_use(self) -> None:
646+
with self.open_tcp_server_port() as port_in_use:
647+
# Test with one port in use and one not in use.
648+
ports = [port_in_use, port_in_use + 1]
649+
in_use = common.get_ports_in_use(ports)
650+
self.assertEqual(in_use, [port_in_use])
651+
652+
# Test with no ports in use.
653+
ports_not_in_use = [port_in_use + 1, port_in_use + 2]
654+
in_use_empty = common.get_ports_in_use(ports_not_in_use)
655+
self.assertEqual(in_use_empty, [])
656+
657+
# Test with all ports in use.
658+
with self.open_tcp_server_port() as port_in_use2:
659+
ports_all_in_use = [port_in_use, port_in_use2]
660+
in_use_all = common.get_ports_in_use(ports_all_in_use)
661+
self.assertEqual(set(in_use_all), set(ports_all_in_use))
662+
645663
def test_wait_for_port_to_not_be_in_use_port_never_closes(self) -> None:
646664
def mock_sleep(unused_seconds: int) -> None:
647665
return

scripts/extend_index_yaml.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
# limitations under the License.
1414

1515
"""Script for extending index.yaml.
16-
In Python 2, the index.yaml autoamtically extended with # AUTOGENERATED,
17-
but this is no longer true in Python 3.
1816
1917
This script extracts new kind from
2018
../cloud_datastore_emulator_cache/WEB-INF/index.yaml
@@ -87,21 +85,25 @@ def reformat_xml_dict_into_yaml_dict(
8785

8886
def main() -> None:
8987
"""Extends index.yaml file."""
90-
with open(INDEX_YAML_PATH, 'r', encoding='utf-8') as f:
91-
index_yaml_dict = yaml.safe_load(f)
88+
if not os.path.exists(WEB_INF_INDEX_XML_PATH):
89+
print('No new index definitions were created during this server run.')
90+
return
9291

9392
with open(WEB_INF_INDEX_XML_PATH, 'r', encoding='utf-8') as f:
9493
web_inf_index_xml_dict = xmltodict.parse(
9594
f.read(), force_list={'datastore-index', 'property'}
9695
)
97-
9896
web_inf_index_yaml_dict = reformat_xml_dict_into_yaml_dict(
9997
web_inf_index_xml_dict
10098
)
101-
10299
if web_inf_index_yaml_dict is None:
103100
return
104101

102+
print('\033[94mExtending index.yaml...\033[0m')
103+
104+
with open(INDEX_YAML_PATH, 'r', encoding='utf-8') as f:
105+
index_yaml_dict = yaml.safe_load(f)
106+
105107
new_kinds = [
106108
kind
107109
for kind in web_inf_index_yaml_dict['indexes']
@@ -121,3 +123,5 @@ def main() -> None:
121123
index_yaml = new_index_yaml_dict.replace('- kind', '\n- kind')
122124
with open(INDEX_YAML_PATH, 'w', encoding='utf-8') as f:
123125
f.write(index_yaml)
126+
127+
print('\033[92mDone!\033[0m')

scripts/extend_index_yaml_test.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from __future__ import annotations
2020

21+
import os
2122
import tempfile
2223

2324
from core.tests import test_utils
@@ -470,3 +471,37 @@ def test_extend_index_yaml_with_same_kind_different_order(self) -> None:
470471
self._run_test_for_extend_index_yaml(
471472
index_yaml, web_inf_index_xml, expected_index_yaml
472473
)
474+
475+
def test_main_exits_early_when_web_inf_index_xml_does_not_exist(
476+
self,
477+
) -> None:
478+
# Create a swap that points to a non-existent file.
479+
non_existent_file = '/tmp/non_existent_web_inf_index_xml.xml'
480+
web_inf_index_xml_swap = self.swap(
481+
extend_index_yaml, 'WEB_INF_INDEX_XML_PATH', non_existent_file
482+
)
483+
484+
with self.index_yaml_swap, web_inf_index_xml_swap:
485+
self.assertFalse(
486+
os.path.exists(extend_index_yaml.WEB_INF_INDEX_XML_PATH)
487+
)
488+
489+
# Record the initial state of the index yaml file.
490+
initial_content = ''
491+
with open(
492+
extend_index_yaml.INDEX_YAML_PATH, 'r', encoding='utf-8'
493+
) as f:
494+
initial_content = f.read()
495+
496+
extend_index_yaml.main()
497+
498+
# Assert that no file writes were done to index.yaml.
499+
with open(
500+
extend_index_yaml.INDEX_YAML_PATH, 'r', encoding='utf-8'
501+
) as f:
502+
final_content = f.read()
503+
self.assertEqual(
504+
final_content,
505+
initial_content,
506+
'INDEX_YAML_PATH content changed unexpectedly',
507+
)

scripts/install_third_party_libs.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,28 +36,17 @@
3636
import sys
3737
import tarfile
3838

39-
from scripts import ( # pylint: disable=wrong-import-position, wrong-import-order
40-
install_python_dev_dependencies,
39+
from scripts import (
40+
install_python_dev_dependencies, # pylint: disable=wrong-import-position, wrong-import-order
4141
)
42-
43-
from typing import Final
44-
45-
install_python_dev_dependencies.main(['--assert_compiled'])
46-
47-
from core import ( # pylint: disable=wrong-import-position, wrong-import-order
48-
utils,
49-
)
50-
from scripts import ( # pylint: disable=wrong-import-position, wrong-import-order
42+
from scripts import (
5143
install_dependencies_json_packages,
5244
install_python_prod_dependencies,
5345
)
5446

55-
from . import ( # pylint: disable=wrong-import-position, wrong-import-order
56-
clean,
57-
common,
58-
pre_commit_hook,
59-
pre_push_hook,
60-
)
47+
from typing import Final
48+
49+
from . import clean, common
6150

6251
# Place to download zip files for temporary storage.
6352
TMP_UNZIP_PATH: Final = os.path.join('.', 'tmp_unzip.zip')
@@ -84,7 +73,9 @@ def make_google_module_importable_by_python(google_module_path: str) -> None:
8473
for path_list in os.walk(google_module_path):
8574
root_path = path_list[0]
8675
if not root_path.endswith('__pycache__'):
87-
with utils.open_file(os.path.join(root_path, '__init__.py'), 'a'):
76+
with open(
77+
os.path.join(root_path, '__init__.py'), 'a', encoding='utf-8'
78+
):
8879
# If the file doesn't exist, it is created. If it does exist,
8980
# this open does nothing.
9081
pass
@@ -440,6 +431,14 @@ def main() -> None:
440431
"""Set up GAE and install third-party libraries for Oppia."""
441432
print('Running install_third_party_libs script...')
442433

434+
# This ensures dev dependencies are present and compiled before we
435+
# proceed to other setup tasks that require them.
436+
install_python_dev_dependencies.main(['--assert_compiled'])
437+
# Import the hook scripts here (after dev deps are installed) so that
438+
# they are only loaded when running the installer.
439+
from . import pre_commit_hook # pylint: disable=wrong-import-position
440+
from . import pre_push_hook # pylint: disable=wrong-import-position
441+
443442
if common.is_windows_os():
444443
raise Exception(
445444
'Installation of Oppia is not supported on Windows OS. Please use '

scripts/install_third_party_libs_test.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@
2525
import subprocess
2626
import sys
2727
import tarfile
28+
import types
2829

2930
from core.tests import test_utils
3031

31-
from typing import Final, List, Tuple
32+
from typing import Final, List, Optional, Tuple, Type
3233

3334
from . import (
3435
clean,
@@ -74,11 +75,38 @@ def __init__(
7475
) -> None:
7576
self.returncode = returncode
7677
self.communicate_val = communicate_val
78+
self.stdout = None
79+
self.args: List[str] = []
7780

78-
def communicate(self) -> Tuple[bytes, bytes]:
81+
def communicate(
82+
self,
83+
_input: Optional[bytes] = None, # pylint: disable=unused-argument
84+
timeout: Optional[float] = None, # pylint: disable=unused-argument
85+
) -> Tuple[bytes, bytes]:
7986
"""Return required method."""
8087
return self.communicate_val
8188

89+
def kill(self) -> None:
90+
"""Mock kill method."""
91+
pass
92+
93+
def poll(self) -> int:
94+
"""Mock poll method."""
95+
return self.returncode
96+
97+
def __enter__(self) -> 'Ret':
98+
"""Context manager enter."""
99+
return self
100+
101+
def __exit__(
102+
self,
103+
_exc_type: Optional[Type[BaseException]],
104+
_exc_val: Optional[BaseException],
105+
_exc_tb: Optional[types.TracebackType],
106+
) -> None:
107+
"""Context manager exit."""
108+
pass
109+
82110

83111
class InstallThirdPartyLibsTests(test_utils.GenericTestBase):
84112
"""Test the methods for installing third party libs."""
@@ -113,7 +141,11 @@ def mock_print(msg: str) -> None:
113141
self.check_call_swap = self.swap(
114142
subprocess, 'check_call', mock_check_call
115143
)
116-
self.Popen_swap = self.swap(subprocess, 'Popen', mock_check_call)
144+
145+
def mock_popen(*_args: str, **_kwargs: str) -> Ret:
146+
return Ret()
147+
148+
self.Popen_swap = self.swap(subprocess, 'Popen', mock_popen)
117149
self.check_call_error_swap = self.swap(
118150
subprocess, 'check_call', mock_check_call_error
119151
)

scripts/servers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ def managed_dev_appserver(
162162
host: str = '0.0.0.0',
163163
port: int = 8080,
164164
admin_host: str = '0.0.0.0',
165-
admin_port: int = 8000,
165+
admin_port: int = feconf.GAE_ADMIN_SERVER_PORT,
166166
enable_host_checking: bool = True,
167167
automatic_restart: bool = False,
168168
skip_sdk_update_check: bool = False,

0 commit comments

Comments
 (0)