1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00
webvirtcloud/instances/tests/test_create_dialog_cpu.py
catborise 946629b035 refactor(instances): the VM lists' scripts load from static files
The VM lists lose their inline scripts and handlers: host_groups.js
collapses a host's VMs in the grouped list, new_vm_dialog.js reads the
new-VM dialog's CPU bars and capacity lines, and console_window.js
(loaded by every page) opens a console button's window, on the lists and
on the VM page alike. A list row's Start no longer calls the please-wait
dialog, which the list pages do not have (the call only raised an
error). The VM page's URL placeholders take the __name__ form the other
pages use.
2026-11-02 09:35:07 +03:00

150 lines
7 KiB
Python

"""The new-VM dialog's CPU bars and capacity lines: the page carries each
compute's URLs and reads neither itself; its script fills them when the
dialog opens, two reads at a time, and ignores answers of an earlier
opening."""
import json
import shutil
import subprocess
import unittest
from unittest.mock import PropertyMock, patch
from computes.models import Compute
from django.conf import settings
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from lxml import html
RUN = """
const vm = require("vm");
function element(cls) {
const attrs = {};
return {style: {}, textContent: "", attrs, classList: {contains: (c) => c === cls},
setAttribute: (k, v) => { attrs[k] = String(v); }, removeAttribute: (k) => { delete attrs[k]; }};
}
const cells = CPU_URLS.map((url) => {
const cell = element("cpu-usage"), meter = element(), bar = element(), label = element();
cell.dataset = {url};
cell.querySelector = (sel) => ({".progress": meter, ".progress-bar": bar})[sel] || label;
Object.assign(cell, {meter, bar, label});
return cell;
});
const lines = LINE_URLS.map((url) => Object.assign(element("compute-capacity"), {dataset: {url}}));
const handlers = {}, requests = [];
const dialog = {addEventListener: (name, fn) => { handlers[name] = fn; },
querySelectorAll: (sel) => sel === ".cpu-usage" ? cells : cells.flatMap((c, i) => [c, lines[i]])};
const page = {
document: {getElementById: () => dialog},
$: {get: (url) => {
const r = {url, done: (f) => { r.ok = f; return r; }, fail: (f) => { r.err = f; return r; },
always: (f) => { r.end = f; return r; }};
requests.push(r);
return r;
}},
window: {location: {}},
};
vm.runInNewContext(SCRIPT, vm.createContext(page));
const state = () => [cells.map((c) => [c.bar.style.width, c.meter.attrs["aria-valuenow"] || null, c.label.textContent]),
lines.map((l) => l.textContent)];
const answer = (i, data) => { const r = requests[i]; data === null ? r.err() : r.ok(data); r.end(); };
const out = {};
handlers["show.bs.modal"]();
out.loading = state();
handlers["hide.bs.modal"]();
answer(0, {cpudata: 5});
answer(1, "stale");
out.hidden = [state(), requests.length];
handlers["show.bs.modal"]();
answer(2, {cpudata: 41.6});
answer(3, "\\nGiven 1.\\n");
answer(4, null);
answer(5, null);
out.filled = state();
out.all = requests.map((r) => r.url);
console.log(JSON.stringify(out));
"""
class CreateDialogCpuTestCase(TestCase):
def setUp(self):
self.client.force_login(get_user_model().objects.create_superuser("cd-admin", "cd@example.com", "pw"))
self.computes = [
Compute.objects.create(name=f"cd{i}", hostname=f"192.0.2.{i}", login="u", password="p", type=1)
for i in (1, 2, 3)
]
patches = [
patch.object(Compute, name, new_callable=PropertyMock, return_value=value)
for name, value in (("status", True), ("cpu_count", 1), ("ram_size", 1024), ("ram_usage", 0))
]
patches += [patch("instances.views.utils.refr"), patch("computes.models.wvmHostDetails")]
for p in patches:
self.details = p.start() # the last one: the host connection
self.addCleanup(p.stop)
def page(self):
response = self.client.get(reverse("instances:index"))
self.assertEqual(response.status_code, 200)
return response.content.decode()
def test_the_page_carries_the_statistics_urls_and_reads_no_cpu_usage(self):
page = self.page()
urls = html.fromstring(page).xpath("//*[contains(@class, 'cpu-usage ')]/@data-url")
self.assertEqual(urls, [reverse("compute_graph", args=[c.id]) for c in self.computes])
self.details.return_value.get_cpu_usage.assert_not_called()
@unittest.skipUnless(shutil.which("node"), "node is not installed")
def test_the_dialog_reads_host_by_host_two_at_a_time(self):
page = self.page()
self.assertIn("/static/js/new_vm_dialog.js", html.fromstring(page).xpath("//script/@src"))
script = (settings.BASE_DIR / "static/js/new_vm_dialog.js").read_text()
cpu = [reverse("compute_graph", args=[c.id]) for c in self.computes]
line = [reverse("compute_capacity", args=[c.id]) for c in self.computes]
js = RUN.replace("SCRIPT", json.dumps(script)).replace("CPU_URLS", json.dumps(cpu)).replace("LINE_URLS", json.dumps(line))
out = subprocess.run(["node", "-e", js], capture_output=True, text=True, timeout=30)
self.assertEqual(out.returncode, 0, out.stderr)
result = json.loads(out.stdout)
waiting = [[["0%", None, "…"]] * 3, ["", "", ""]]
self.assertEqual(result["loading"], waiting)
# answers that come after the dialog closed change nothing and read no more
self.assertEqual(result["hidden"], [waiting, 2])
# a failed CPU read is a dash, a failed line stays empty
self.assertEqual(result["filled"], [[["42%", "42", "42%"], ["0%", None, "—"], ["0%", None, "…"]],
["Given 1.", "", ""]])
# the CPU and the line of one host, then the next host; two at a time
self.assertEqual(result["all"], [cpu[0], line[0], cpu[0], line[0], cpu[1], line[1], cpu[2], line[2]])
def test_each_host_carries_its_capacity_line_url(self):
urls = html.fromstring(self.page()).xpath("//*[@id='AddInstance']//*[contains(@class, 'compute-capacity')]/@data-url")
self.assertEqual(urls, [reverse("compute_capacity", args=[c.id]) for c in self.computes])
class CreateDialogHostsTestCase(TestCase):
"""The dialog lists the reachable computes; without one it says so."""
def setUp(self):
self.client.force_login(get_user_model().objects.create_superuser("cd-admin", "cd@example.com", "pw"))
for i in (1, 2):
Compute.objects.create(name=f"cd{i}", hostname=f"192.0.2.{i}", login="u", password="p", type=1)
for name, value in (("cpu_count", 1), ("ram_size", 1024), ("ram_usage", 0)):
p = patch.object(Compute, name, new_callable=PropertyMock, return_value=value)
p.start()
self.addCleanup(p.stop)
p = patch("instances.views.utils.refr")
p.start()
self.addCleanup(p.stop)
def dialog(self, reachable):
with patch.object(Compute, "status", property(lambda compute: compute.name in reachable)):
response = self.client.get(reverse("instances:index"))
return html.fromstring(response.content).get_element_by_id("AddInstance")
def test_only_reachable_computes_are_listed(self):
dialog = self.dialog({"cd2"})
self.assertEqual([e.text for e in dialog.xpath(".//a/div/div[1]")], ["cd2"])
self.assertNotIn("No compute is reachable.", dialog.text_content())
def test_no_reachable_compute_is_said(self):
dialog = self.dialog(set())
self.assertEqual(dialog.xpath(".//a"), [])
self.assertIn("No compute is reachable.", dialog.text_content())