jcloude/press/api/tests/test_bench.py

638 lines
20 KiB
Python

import json
import os
import time
from unittest import skip
from unittest.mock import MagicMock, Mock, patch
import docker
import jingrow
import requests
from frappe.core.utils import find
from frappe.tests.utils import FrappeTestCase, timeout
from press.api.bench import (
all,
bench_config,
dependencies,
deploy,
deploy_and_update,
deploy_information,
get,
new,
update_config,
update_dependencies,
)
from press.press.doctype.agent_job.agent_job import AgentJob
from press.press.doctype.app.test_app import create_test_app
from press.press.doctype.app_release.test_app_release import create_test_app_release
from press.press.doctype.bench.test_bench import create_test_bench
from press.press.doctype.deploy_candidate.deploy_candidate import DeployCandidate
from press.press.doctype.press_settings.test_press_settings import (
create_test_press_settings,
)
from press.press.doctype.release_group.test_release_group import (
create_test_release_group,
)
from press.press.doctype.server.test_server import create_test_server
from press.press.doctype.team.test_team import create_test_press_admin_team
from press.utils import get_current_team
from press.utils.test import foreground_enqueue_pg
@patch.object(AgentJob, "enqueue_http_request", new=Mock())
class TestAPIBench(FrappeTestCase):
def setUp(self):
super().setUp()
self.team = create_test_press_admin_team()
self.version = "Version 15"
self.app = create_test_app()
self.app_source = self.app.add_source(
self.version,
repository_url="http://git.jingrow.com/frappe/frappe",
branch="version-15",
team=get_current_team(),
public=True,
)
self.server = create_test_server()
self.server.db_set("use_for_new_benches", True)
def tearDown(self):
frappe.set_user("Administrator")
frappe.db.rollback()
def test_new_fn_creates_release_group_awaiting_deploy_when_called_by_press_admin_team(
self,
):
frappe.set_user(self.team.user)
name = new(
{
"title": "Test Bench",
"apps": [{"name": self.app.name, "source": self.app_source.name}],
"version": self.version,
"cluster": "Default",
"saas_app": None,
"server": None,
}
)
group = frappe.get_last_pg("Release Group")
self.assertEqual(group.title, "Test Bench")
self.assertEqual(group.name, name)
get_res = get(group.name)
self.assertEqual(get_res["status"], "Awaiting Deploy")
self.assertEqual(get_res["public"], False)
@skip("Local builds deprecated. Builds need to be set for GHA.")
@patch(
"press.press.doctype.deploy_candidate.deploy_candidate.frappe.enqueue_pg",
new=foreground_enqueue_pg,
)
@patch("press.press.doctype.deploy_candidate.deploy_candidate.frappe.db.commit", new=Mock())
def test_deploy_fn_deploys_bench_container(self):
# mark frappe as approved so that the deploy can happen
release = frappe.get_last_pg("App Release", {"source": self.app_source.name})
release.status = "Approved"
release.save()
set_press_settings_for_docker_build()
frappe.set_user(self.team.user)
group = new(
{
"title": "Test Bench",
"apps": [{"name": self.app.name, "source": self.app_source.name}],
"version": self.version,
"cluster": "Default",
"saas_app": None,
"server": None,
}
)
dc_count_before = frappe.db.count("Deploy Candidate", filters={"group": group})
d_count_before = frappe.db.count("Deploy", filters={"group": group})
deploy(group, [{"app": self.app.name}])
dc_count_after = frappe.db.count("Deploy Candidate", filters={"group": group})
d_count_after = frappe.db.count("Deploy", filters={"group": group})
self.assertEqual(dc_count_after, dc_count_before + 1)
self.assertEqual(d_count_after, d_count_before + 1)
self._check_if_docker_image_was_built(group)
@patch(
"press.press.doctype.deploy_candidate.deploy_candidate.frappe.enqueue_pg",
new=foreground_enqueue_pg,
)
@patch.object(DeployCandidate, "schedule_build_and_deploy", new=MagicMock())
@patch("press.press.doctype.deploy_candidate.deploy_candidate.frappe.db.commit", new=Mock())
def test_deploy_and_update_fn_creates_bench_update(self):
group = new(
{
"title": "Test Bench",
"apps": [{"name": self.app.name, "source": self.app_source.name}],
"version": self.version,
"cluster": "Default",
"saas_app": None,
"server": None,
}
)
bu_count_before = frappe.db.count("Bench Update", filters={"group": group})
dc_count_before = frappe.db.count("Deploy Candidate", filters={"group": group})
release = create_test_app_release(frappe.get_pg("App Source", self.app_source.name))
deploy_and_update(group, [{"release": release.name}], [])
bu_count_after = frappe.db.count("Bench Update", filters={"group": group})
dc_count_after = frappe.db.count("Deploy Candidate", filters={"group": group})
self.assertEqual(dc_count_after, dc_count_before + 1)
self.assertEqual(bu_count_after, bu_count_before + 1)
@patch(
"press.press.doctype.deploy_candidate.deploy_candidate.frappe.enqueue_pg",
new=foreground_enqueue_pg,
)
@patch("press.press.doctype.deploy_candidate.deploy_candidate.frappe.db.commit", new=Mock())
def test_deploy_and_update_fn_fails_without_release_argument(self):
group = new(
{
"title": "Test Bench",
"apps": [{"name": self.app.name, "source": self.app_source.name}],
"version": self.version,
"cluster": "Default",
"saas_app": None,
"server": None,
}
)
self.assertRaises(
frappe.exceptions.MandatoryError,
deploy_and_update,
group,
[{"app": self.app.name}],
[],
)
@patch("press.press.doctype.deploy_candidate.deploy_candidate.frappe.db.commit", new=Mock())
def test_deploy_fn_fails_without_apps(self):
frappe.set_user(self.team.user)
group = new(
{
"title": "Test Bench",
"apps": [{"name": self.app.name, "source": self.app_source.name}],
"version": self.version,
"cluster": "Default",
"saas_app": None,
"server": None,
}
)
self.assertRaises(TypeError, deploy, group)
@patch("press.press.doctype.deploy_candidate.deploy_candidate.frappe.db.commit", new=Mock())
def test_deploy_fn_fails_with_empty_apps(self):
frappe.set_user(self.team.user)
group = new(
{
"title": "Test Bench",
"apps": [{"name": self.app.name, "source": self.app_source.name}],
"version": self.version,
"cluster": "Default",
"saas_app": None,
"server": None,
}
)
self.assertRaises(frappe.exceptions.MandatoryError, deploy, group, [])
@timeout(20)
def _check_if_docker_image_was_built(self, group: str):
client = docker.from_env()
dc = frappe.get_last_pg("Deploy Candidate")
image_name = f"registry.local.frappe.dev/fc.dev/{group}:{dc.name}"
try:
image = client.images.get(image_name)
except docker.errors.ImageNotFound:
self.fail(f"Image {image_name} not found. Found {client.images.list()}")
self.assertIn(image_name, [tag for tag in image.tags])
test_port = 10501
client.containers.run(image=image_name, remove=True, detach=True, ports={"8000/tcp": test_port})
while True:
# Ensure that gunicorn at least responds. Usually we'll get 404 as there's no site installed *yet*
try:
response = requests.get(f"http://localhost:{test_port}")
print("Received Response", response.text)
if response.status_code < 500:
break
except OSError as e:
print("Waitng for container to respond", str(e))
time.sleep(0.5)
class TestAPIBenchConfig(FrappeTestCase):
def setUp(self):
super().setUp()
app = create_test_app()
self.rg = create_test_release_group([app])
self.config = [
{"key": "max_file_size", "value": "1234", "type": "Number"},
{"key": "mail_login", "value": "a@a.com", "type": "String"},
{"key": "skip_setup_wizard", "value": "1", "type": "Boolean"},
{"key": "limits", "value": '{"limit": "val"}', "type": "JSON"},
{"key": "http_timeout", "value": 120, "type": "Number", "internal": False},
]
update_config(self.rg.name, self.config)
self.rg.reload()
def tearDown(self):
frappe.db.rollback()
def test_bench_config_api(self):
configs = bench_config(self.rg.name)
self.assertListEqual(configs, self.config)
def test_bench_config_updation(self):
new_bench_config = frappe.parse_json(self.rg.bench_config)
self.assertEqual(
frappe.parse_json(self.rg.common_site_config),
{
"max_file_size": 1234,
"mail_login": "a@a.com",
"skip_setup_wizard": True,
"limits": {"limit": "val"},
},
)
self.assertEqual(new_bench_config, {"http_timeout": 120})
def test_bench_config_is_updated_in_subsequent_benches(self):
bench = create_test_bench(group=self.rg)
bench.reload()
self.assertIn(("http_timeout", 120), frappe.parse_json(bench.bench_config).items())
for key, value in frappe.parse_json(self.rg.common_site_config).items():
self.assertEqual(value, frappe.parse_json(bench.config).get(key))
def test_update_dependencies_set_dependencies_correctly(self):
update_dependencies(
self.rg.name,
json.dumps(
[
{"key": "NODE_VERSION", "value": "16.11"}, # updated
{"key": "NVM_VERSION", "value": "0.36.0"},
{"key": "PYTHON_VERSION", "value": "3.6"}, # updated
{"key": "WKHTMLTOPDF_VERSION", "value": "0.12.5"},
{"key": "BENCH_VERSION", "value": "5.15.2"},
{"key": "PIP_VERSION", "value": "25.2"},
]
),
)
self.assertFalse(self.rg.last_dependency_update)
self.rg.reload()
self.assertTrue(self.rg.last_dependency_update)
self.assertEqual(
find(self.rg.dependencies, lambda d: d.dependency == "NODE_VERSION").version, "16.11"
)
self.assertEqual(
find(self.rg.dependencies, lambda d: d.dependency == "PYTHON_VERSION").version,
"3.6",
)
def test_update_dependencies_throws_error_for_invalid_dependencies(self):
self.assertRaisesRegex(
Exception,
"Invalid dependencies: asdf",
update_dependencies,
self.rg.name,
json.dumps(
[
{"key": "NVM_VERSION", "value": "0.36.0"},
{"key": "NODE_VERSION", "value": "16.36.0"},
{"key": "WKHTMLTOPDF_VERSION", "value": "0.12.5"},
{"key": "BENCH_VERSION", "value": "5.15.2"},
{"key": "PIP_VERSION", "value": "25.2"},
{
"key": "asdf",
"value": "10.9",
}, # invalid dependency
],
),
)
def test_update_dependencies_throws_error_for_invalid_version(self):
self.assertRaisesRegex(
Exception,
"Invalid version.*",
update_dependencies,
self.rg.name,
json.dumps(
[
{"key": "NODE_VERSION", "value": "v16.11"}, # v is invalid
{"key": "NVM_VERSION", "value": "0.36.0"},
{"key": "PYTHON_VERSION", "value": "3.6"},
{"key": "WKHTMLTOPDF_VERSION", "value": "0.12.5"},
{"key": "BENCH_VERSION", "value": "5.15.2"},
{"key": "PIP_VERSION", "value": "25.2"},
],
),
)
def test_cannot_remove_dependencies(self):
self.assertRaisesRegex(
Exception,
"Need all required dependencies",
update_dependencies,
self.rg.name,
json.dumps(
[
{"key": "NODE_VERSION", "value": "16.11"},
{"key": "NVM_VERSION", "value": "0.36.0"},
{"key": "PYTHON_VERSION", "value": "3.6"},
{"key": "WKHTMLTOPDF_VERSION", "value": "0.12.5"},
],
),
)
def test_cannot_add_additional_invalid_dependencies(self):
self.assertRaisesRegex(
Exception,
"Need all required dependencies",
update_dependencies,
self.rg.name,
json.dumps(
[
{"key": "NODE_VERSION", "value": "16.11"},
{"key": "NVM_VERSION", "value": "0.36.0"},
{"key": "PYTHON_VERSION", "value": "3.6"},
{"key": "WKHTMLTOPDF_VERSION", "value": "0.12.5"},
{"key": "BENCH_VERSION", "value": "5.15.2"},
{
"key": "MARIADB_VERSION",
"value": "10.9",
}, # invalid dependency
{"key": "PIP_VERSION", "value": "25.2"},
],
),
)
def test_update_of_dependency_child_table_sets_last_dependency_update(self):
self.assertFalse(self.rg.last_dependency_update)
self.rg.append("dependencies", {"dependency": "MARIADB_VERSION", "version": "10.9"})
self.rg.save()
self.rg.reload()
dependency_update_1 = self.rg.last_dependency_update
self.assertTrue(dependency_update_1)
update_dependencies(
self.rg.name,
json.dumps(
[
{"key": "NODE_VERSION", "value": "16.11"},
{"key": "NVM_VERSION", "value": "0.36.0"},
{"key": "PYTHON_VERSION", "value": "3.6"},
{"key": "WKHTMLTOPDF_VERSION", "value": "0.12.5"},
{"key": "BENCH_VERSION", "value": "5.15.2"},
{"key": "PIP_VERSION", "value": "25.2"},
{"key": "MARIADB_VERSION", "value": "10.9"},
]
),
)
self.rg.reload()
dependency_update_2 = self.rg.last_dependency_update
self.assertTrue(dependency_update_2)
self.assertGreater(dependency_update_2, dependency_update_1)
def test_deploy_information_shows_update_available_for_bench_when_apps_are_updated_after_dependency_updated_deploy(
self,
):
update_dependencies(
self.rg.name,
json.dumps(
[
{"key": "NODE_VERSION", "value": "16.11"},
{"key": "NVM_VERSION", "value": "0.36.0"},
{"key": "PYTHON_VERSION", "value": "3.6"},
{"key": "WKHTMLTOPDF_VERSION", "value": "0.12.5"},
{"key": "BENCH_VERSION", "value": "5.15.2"},
{"key": "PIP_VERSION", "value": "25.2"},
]
),
)
create_test_bench(
group=self.rg
) # now update available due to dependency shouldn't be there (cuz create_test_bench created deploy candidate)
self.assertFalse(deploy_information(self.rg.name)["update_available"])
create_test_app_release(frappe.get_pg("App Source", self.rg.apps[0].source))
self.assertTrue(deploy_information(self.rg.name)["update_available"])
def test_deploy_information_shows_update_available_when_dependencies_are_updated(self):
self.assertFalse(self.rg.last_dependency_update)
create_test_bench(group=self.rg) # avoid update available due to no deploys
self.assertFalse(deploy_information(self.rg.name)["update_available"])
update_dependencies(
self.rg.name,
json.dumps(
[
{"key": "NODE_VERSION", "value": "16.11"},
{"key": "NVM_VERSION", "value": "0.36.0"},
{"key": "PYTHON_VERSION", "value": "3.6"},
{"key": "WKHTMLTOPDF_VERSION", "value": "0.12.5"},
{"key": "BENCH_VERSION", "value": "5.15.2"},
{"key": "PIP_VERSION", "value": "25.2"},
]
),
)
self.rg.reload()
self.assertTrue(deploy_information(self.rg.name)["update_available"])
def test_dependencies_lists_all_dependencies(self):
deps = [
{"key": "NODE_VERSION", "value": "16.11"},
{"key": "NVM_VERSION", "value": "0.36.0"},
{"key": "PYTHON_VERSION", "value": "3.6"},
{"key": "WKHTMLTOPDF_VERSION", "value": "0.12.5"},
{"key": "BENCH_VERSION", "value": "5.15.2"},
{"key": "PIP_VERSION", "value": "25.2"},
]
update_dependencies(
self.rg.name,
json.dumps(deps),
)
active_dependencies = dependencies(self.rg.name)["active_dependencies"]
self.assertListEqual(
sorted(active_dependencies, key=lambda x: x["key"]),
sorted(deps, key=lambda x: x["key"]),
)
def test_dependencies_shows_dependency_update_available_on_update_of_the_same(self):
deps = [
{"key": "NODE_VERSION", "value": "16.11"},
{"key": "NVM_VERSION", "value": "0.36.0"},
{"key": "PYTHON_VERSION", "value": "3.6"},
{"key": "WKHTMLTOPDF_VERSION", "value": "0.12.5"},
{"key": "BENCH_VERSION", "value": "5.15.2"},
{"key": "PIP_VERSION", "value": "25.2"},
]
self.assertFalse(dependencies(self.rg.name)["update_available"])
create_test_bench(group=self.rg) # don't show dependency update available for new deploys
deps[0]["value"] = "16.12"
update_dependencies(
self.rg.name,
json.dumps(deps),
)
self.assertTrue(dependencies(self.rg.name)["update_available"])
def test_setting_limit_fields_creates_update_bench_config_job_as_such(self):
bench = create_test_bench(group=self.rg)
bench.memory_high = 1024
bench.memory_max = 2048
bench.memory_swap = 4096
bench.vcpu = 2
bench.save()
job = frappe.get_last_pg(
"Agent Job", {"job_type": "Update Bench Configuration", "bench": bench.name}
)
data = json.loads(job.request_data)
self.assertEqual(data["bench_config"]["memory_high"], 1024)
self.assertEqual(data["bench_config"]["memory_max"], 2048)
self.assertEqual(data["bench_config"]["memory_swap"], 4096)
self.assertEqual(data["bench_config"]["vcpu"], 2)
def test_memory_swap_cannot_be_set_lower_than_memory_max(self):
bench = create_test_bench(group=self.rg)
bench.memory_high = 1024
bench.memory_max = 2048
bench.memory_swap = 1024
self.assertRaises(
frappe.exceptions.ValidationError,
bench.save,
)
bench.reload()
bench.memory_high = 1024
bench.memory_max = 1024
bench.memory_swap = -1
try:
bench.save()
except Exception as e:
print(e)
self.fail("Memory swap should be allowed to be set to -1")
def test_memory_max_cant_be_set_without_swap(self):
bench = create_test_bench(group=self.rg)
bench.memory_max = 2048
self.assertRaises(
frappe.exceptions.ValidationError,
bench.save,
)
def test_memory_high_cant_be_set_higher_than_memory_max(self):
bench = create_test_bench(group=self.rg)
bench.memory_max = 2048
bench.memory_high = 4096
bench.memory_swap = 4096
self.assertRaises(
frappe.exceptions.ValidationError,
bench.save,
)
def test_force_update_limits_creates_job_with_parameters(self):
bench = create_test_bench(group=self.rg)
bench.memory_high = 1024
bench.memory_max = 2048
bench.memory_swap = 4096
bench.vcpu = 2
bench.force_update_limits()
job = frappe.get_last_pg("Agent Job", {"job_type": "Force Update Bench Limits", "bench": bench.name})
job_data = json.loads(job.request_data)
self.assertEqual(job_data["memory_high"], 1024)
self.assertEqual(job_data["memory_max"], 2048)
self.assertEqual(job_data["memory_swap"], 4096)
self.assertEqual(job_data["vcpu"], 2)
class TestAPIBenchList(FrappeTestCase):
def setUp(self):
from press.press.doctype.press_tag.test_press_tag import create_and_add_test_tag
super().setUp()
app = create_test_app()
active_group = create_test_release_group([app])
create_test_bench(group=active_group)
self.active_bench_dict = {
"number_of_sites": 0,
"name": active_group.name,
"title": active_group.title,
"version": active_group.version,
"creation": active_group.creation,
"tags": [],
"number_of_apps": 1,
"status": "Active",
}
group_awaiting_deploy = create_test_release_group([app])
self.bench_awaiting_deploy_dict = {
"number_of_sites": 0,
"name": group_awaiting_deploy.name,
"title": group_awaiting_deploy.title,
"version": group_awaiting_deploy.version,
"creation": group_awaiting_deploy.creation,
"tags": [],
"number_of_apps": 1,
"status": "Awaiting Deploy",
}
group_with_tag = create_test_release_group([app])
test_tag = create_and_add_test_tag(group_with_tag.name, "Release Group")
create_test_bench(group=group_with_tag)
self.bench_with_tag_dict = {
"number_of_sites": 0,
"name": group_with_tag.name,
"title": group_with_tag.title,
"version": group_with_tag.version,
"creation": group_with_tag.creation,
"tags": [test_tag.tag],
"number_of_apps": 1,
"status": "Active",
}
def tearDown(self):
frappe.db.rollback()
def test_list_all_benches(self):
self.assertCountEqual(
all(),
[self.active_bench_dict, self.bench_awaiting_deploy_dict, self.bench_with_tag_dict],
)
def test_list_active_benches(self):
self.assertCountEqual(
all(bench_filter={"status": "Active", "tag": ""}),
[self.active_bench_dict, self.bench_with_tag_dict],
)
def test_list_awaiting_deploy_benches(self):
self.assertEqual(
all(bench_filter={"status": "Awaiting Deploy", "tag": ""}),
[self.bench_awaiting_deploy_dict],
)
def test_list_tagged_benches(self):
self.assertEqual(all(bench_filter={"status": "", "tag": "test_tag"}), [self.bench_with_tag_dict])
def set_press_settings_for_docker_build() -> None:
press_settings = create_test_press_settings()
cwd = os.getcwd()
back = os.path.join(cwd, "..")
bench_dir = os.path.abspath(back)
build_dir = os.path.join(bench_dir, "test_builds")
clone_dir = os.path.join(bench_dir, "test_clones")
press_settings.db_set("build_directory", build_dir)
press_settings.db_set("clone_directory", clone_dir)
press_settings.db_set("docker_registry_url", "registry.local.frappe.dev")