Merge pull request #29045 from ankush/test_distribution

ci: balance test distribution manually
This commit is contained in:
Ankush Menat 2025-01-04 12:36:13 +05:30 committed by GitHub
commit 312263dee2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 49 additions and 29 deletions

View file

@ -216,7 +216,7 @@ runs:
start_time=$(date +%s)
source ${GITHUB_WORKSPACE}/env/bin/activate
CI=Yes bench build &
CI=Yes bench build --force --production &
build_pid=$!
bench --site test_site reinstall --yes
wait $build_pid

View file

@ -897,12 +897,8 @@ class TestAddNewUser(BaseTestCommands):
self.assertEqual({"Accounts User", "Sales User"}, roles)
class TestBenchBuild(BaseTestCommands):
class TestBenchBuild(IntegrationTestCase):
def test_build_assets_size_check(self):
with cli(frappe.commands.utils.build, "--force --production --app frappe") as result:
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.exception, None)
CURRENT_SIZE = 3.3 # MB
JS_ASSET_THRESHOLD = 0.01

View file

@ -360,8 +360,7 @@ def run_parallel_tests(
App: {app:<26}
Site: {site:<26}
Build Number: {build_number:<26}
Total Builds: {total_builds:<26}
Tests in Build: ~{runner.total_tests:<25} """
Total Builds: {total_builds:<26} """
if cc.with_coverage:
banner += """
Coverage Rep.: {cc.outfile:<26} """

View file

@ -14,6 +14,7 @@ from frappe.integrations.doctype.webhook.webhook import (
get_webhook_headers,
)
from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.tests.classes.context_managers import timeout
@contextmanager
@ -84,10 +85,19 @@ class TestWebhook(IntegrationTestCase):
frappe.db.delete("Webhook")
frappe.db.commit()
@timeout(5, "Test webhooks should never wait, check mocked responses.")
def setUp(self):
# retrieve or create a User webhook for `after_insert`
self.responses = responses.RequestsMock()
self.responses.start()
self.responses.add(
responses.POST,
"https://httpbin.org/post",
status=200,
json={},
)
webhook_fields = {
"webhook_doctype": "User",
"webhook_docevent": "after_insert",
@ -120,6 +130,7 @@ class TestWebhook(IntegrationTestCase):
self.responses.reset()
super().tearDown()
@timeout(5, "Test webhooks should never wait, check mocked responses.")
def test_webhook_trigger_with_enabled_webhooks(self):
"""Test webhook trigger for enabled webhooks"""
@ -138,18 +149,21 @@ class TestWebhook(IntegrationTestCase):
self.assertEqual(execution.webhook.name, self.sample_webhooks[0].name)
self.assertEqual(execution.doc.name, self.test_user.name)
@timeout(5, "Test webhooks should never wait, check mocked responses.")
def test_validate_doc_events(self):
"Test creating a submit-related webhook for a non-submittable DocType"
self.webhook.webhook_docevent = "on_submit"
self.assertRaises(frappe.ValidationError, self.webhook.save)
@timeout(5, "Test webhooks should never wait, check mocked responses.")
def test_validate_request_url(self):
"Test validation for the webhook request URL"
self.webhook.request_url = "httpbin.org?post"
self.assertRaises(frappe.ValidationError, self.webhook.save)
@timeout(5, "Test webhooks should never wait, check mocked responses.")
def test_validate_headers(self):
"Test validation for request headers"
@ -165,6 +179,7 @@ class TestWebhook(IntegrationTestCase):
headers = get_webhook_headers(doc=None, webhook=self.webhook)
self.assertEqual(headers, {"Content-Type": "application/json"})
@timeout(5, "Test webhooks should never wait, check mocked responses.")
def test_validate_request_body_form(self):
"Test validation of Form URL-Encoded request body"
@ -179,6 +194,7 @@ class TestWebhook(IntegrationTestCase):
data = get_webhook_data(doc=self.user, webhook=self.webhook)
self.assertEqual(data, {"name": self.user.name})
@timeout(5, "Test webhooks should never wait, check mocked responses.")
def test_validate_request_body_json(self):
"Test validation of JSON request body"
@ -193,6 +209,7 @@ class TestWebhook(IntegrationTestCase):
data = get_webhook_data(doc=self.user, webhook=self.webhook)
self.assertEqual(data, {"name": self.user.name})
@timeout(5, "Test webhooks should never wait, check mocked responses.")
def test_webhook_req_log_creation(self):
self.responses.add(
responses.POST,
@ -213,6 +230,7 @@ class TestWebhook(IntegrationTestCase):
self.assertTrue(frappe.get_all("Webhook Request Log", pluck="name"))
@timeout(5, "Test webhooks should never wait, check mocked responses.")
def test_webhook_with_array_body(self):
"""Check if array request body are supported."""
wh_config = {
@ -258,6 +276,7 @@ class TestWebhook(IntegrationTestCase):
log = frappe.get_last_doc("Webhook Request Log")
self.assertEqual(len(json.loads(log.response)), 3)
@timeout(5, "Test webhooks should never wait, check mocked responses.")
def test_webhook_with_dynamic_url_enabled(self):
wh_config = {
"doctype": "Webhook",
@ -289,6 +308,7 @@ class TestWebhook(IntegrationTestCase):
doc.title = "Test Webhook Note"
enqueue_webhook(doc, wh)
@timeout(5, "Test webhooks should never wait, check mocked responses.")
def test_webhook_with_dynamic_url_disabled(self):
wh_config = {
"doctype": "Webhook",

View file

@ -21,6 +21,12 @@ click_ctx = click.get_current_context(True)
if click_ctx:
click_ctx.color = True
TEST_WEIGHT_OVERRIDES = {
# XXX: command tests are significantly overweight, need a better heuristic than test count
# Possible better solution: stats from previous test runs.
"test_commands.py": 10,
}
class ParallelTestRunner:
def __init__(self, app, site, build_number=1, total_builds=1, dry_run=False):
@ -30,7 +36,7 @@ class ParallelTestRunner:
self.total_builds = frappe.utils.cint(total_builds)
self.dry_run = dry_run
self.test_file_list = []
self.total_tests = 0
self.total_test_weight = 0
self.test_result = None
self.setup_test_file_list()
@ -70,8 +76,7 @@ class ParallelTestRunner:
def setup_test_file_list(self):
self.test_file_list = self.get_test_file_list()
self.total_tests = sum(self.get_test_count(test) for test in self.test_file_list)
click.echo(f"Estimated total tests for build {self.build_number}: {self.total_tests}")
self.total_test_weight = sum(self.get_test_weight(test) for test in self.test_file_list)
def run_tests(self):
self.test_result = TestResult(stream=sys.stderr, descriptions=True, verbosity=2)
@ -136,18 +141,20 @@ class ParallelTestRunner:
# Load balance based on total # of tests ~ each runner should get roughly same # of tests.
test_list = get_all_tests(self.app)
test_counts = [self.get_test_count(test) for test in test_list]
test_counts = [self.get_test_weight(test) for test in test_list]
test_chunks = split_by_weight(test_list, test_counts, chunk_count=self.total_builds)
return test_chunks[self.build_number - 1]
@staticmethod
def get_test_count(test):
def get_test_weight(test):
"""Get approximate count of tests inside a file"""
file_name = "/".join(test)
test_weight = TEST_WEIGHT_OVERRIDES.get(test[-1]) or 1
with open(file_name) as f:
test_count = f.read().count("def test_")
test_count = f.read().count("def test_") * test_weight
return test_count

View file

@ -63,7 +63,7 @@ class IntegrationTestCase(UnitTestCase):
frappe.db.before_commit.add(_commit_watcher)
# enqueue teardown actions (executed in LIFO order)
cls.addClassCleanup(_restore_thread_locals, copy.deepcopy(frappe.local.flags))
cls.addClassCleanup(_restore_ctx_locals, copy.deepcopy(frappe.local.flags))
cls.addClassCleanup(_rollback_db)
cls._integration_test_case_class_setup_done = True
@ -183,12 +183,13 @@ def _rollback_db():
frappe.db.rollback()
def _restore_thread_locals(flags):
def _restore_ctx_locals(flags):
frappe.local.flags = flags
frappe.local.error_log = []
frappe.local.message_log = []
frappe.local.debug_log = []
frappe.local.conf = frappe._dict(frappe.get_site_config())
frappe.local.response = frappe._dict({"docs": []})
frappe.local.cache = {}
frappe.local.lang = "en"
frappe.local.preload_assets = {"style": [], "script": [], "icons": []}

View file

@ -74,19 +74,16 @@ class TestClient(IntegrationTestCase):
def test_run_doc_method(self):
from frappe.handler import execute_cmd
if not frappe.db.exists("Report", "Test Run Doc Method"):
report = frappe.get_doc(
{
"doctype": "Report",
"ref_doctype": "User",
"report_name": "Test Run Doc Method",
"report_type": "Query Report",
"is_standard": "No",
"roles": [{"role": "System Manager"}],
}
).insert()
else:
report = frappe.get_doc("Report", "Test Run Doc Method")
report = frappe.get_doc(
{
"doctype": "Report",
"ref_doctype": "User",
"report_name": frappe.generate_hash(),
"report_type": "Query Report",
"is_standard": "No",
"roles": [{"role": "System Manager"}],
}
).insert()
frappe.local.request = frappe._dict()
frappe.local.request.method = "GET"