1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00
webvirtcloud/instances/test_create_dialog_cpu.py
catborise a57290cdf2 fix(instances): the new-VM dialog shows each host's current CPU load
The CPU bar showed the average since the host booted, read for every
compute on every load of the VM list. The dialog now reads each compute's
graph endpoint (load over one second) when it opens, two computes at a
time so the reads hold at most two workers, and ignores answers of an
earlier opening. A host that cannot be read shows a dash. The since-boot
reading had no other caller and is removed.
2026-10-10 20:54:19 +03:00

114 lines
4.9 KiB
Python

"""The new-VM dialog's CPU bars: the page carries each compute's statistics
URL and reads no CPU usage itself; its script fills the bars when the dialog
opens, two computes 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.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() {
const attrs = {};
return {style: {}, textContent: "", attrs,
setAttribute: (k, v) => { attrs[k] = String(v); }, removeAttribute: (k) => { delete attrs[k]; }};
}
const cells = URLS.map((url) => {
const bar = element(), label = element();
return {dataset: {url}, bar, label,
querySelector: (sel) => (sel === ".progress-bar" ? bar : label)};
});
const handlers = {}, requests = [];
const dialog = {addEventListener: (name, fn) => { handlers[name] = fn; }, querySelectorAll: () => cells};
const page = {
document: {getElementById: () => dialog},
$: {getJSON: (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.bar.attrs["aria-valuenow"] || null, c.label.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();
out.first = requests.map((r) => r.url);
handlers["hide.bs.modal"]();
answer(0, {cpudata: 5});
answer(1, null);
out.hidden = [state(), requests.length];
handlers["show.bs.modal"]();
answer(2, {cpudata: 0});
answer(3, {cpudata: null});
out.filled = state();
handlers["hide.bs.modal"]();
handlers["show.bs.modal"]();
out.reopened = requests.length;
answer(4, {cpudata: 99});
answer(5, {cpudata: 41.6});
answer(6, null);
out.final = 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("//td[@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_fills_the_bars_two_at_a_time(self):
page = self.page()
script = next(s for s in html.fromstring(page).xpath("//script[not(@src)]/text()") if "show.bs.modal" in s)
urls = [reverse("compute_graph", args=[c.id]) for c in self.computes]
js = RUN.replace("SCRIPT", json.dumps(script)).replace("URLS", json.dumps(urls))
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)
self.assertEqual(result["loading"], [["0%", None, "…"]] * 3)
self.assertEqual(result["first"], urls[:2])
# answers that come after the dialog closed change nothing and read no more
self.assertEqual(result["hidden"], [[["0%", None, "…"]] * 3, 2])
# 0 is a value, no value is a dash
self.assertEqual(result["filled"], [["0%", "0", "0%"], ["0%", None, "—"], ["0%", None, "…"]])
# reopened while the third read runs: one more read, two in all
self.assertEqual(result["reopened"], 6)
# the earlier opening's answer (99) is ignored; a failed read is a dash
self.assertEqual(result["final"], [["42%", "42", "42%"], ["0%", None, "—"], ["0%", None, "…"]])
self.assertEqual(result["all"], urls[:2] + urls + urls)