mirror of
https://github.com/RunLit/Bambu-Run.git
synced 2026-06-22 22:19:03 +01:00
Initial implementation of multi-printer support.
This commit is contained in:
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
40
tests/settings.py
Normal file
40
tests/settings.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Minimal Django settings for running bambu_run's pytest suite (in-memory SQLite)."""
|
||||
|
||||
SECRET_KEY = "test-secret-key"
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"bambu_run",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
]
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": ":memory:",
|
||||
}
|
||||
}
|
||||
|
||||
USE_TZ = True
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
ROOT_URLCONF = "tests.urls"
|
||||
90
tests/test_multi_device_collection.py
Normal file
90
tests/test_multi_device_collection.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import pytest
|
||||
|
||||
from bambu_run.management.commands.bambu_collector import (
|
||||
Command,
|
||||
DeviceSession,
|
||||
resolve_printer_device,
|
||||
)
|
||||
from bambu_run.models import PrintJob, PrinterMetrics
|
||||
|
||||
|
||||
class FakeClient:
|
||||
"""Stub in place of BambuPrinter — returns canned snapshots, no real MQTT."""
|
||||
|
||||
def __init__(self, snapshots):
|
||||
self._snapshots = snapshots
|
||||
self._index = 0
|
||||
self._client = None # cloud BambuClient handle used by cloud task sync
|
||||
|
||||
def get_snapshot(self):
|
||||
snap = self._snapshots[min(self._index, len(self._snapshots) - 1)]
|
||||
self._index += 1
|
||||
return snap
|
||||
|
||||
|
||||
def make_session(device_id, name, snapshots):
|
||||
printer = resolve_printer_device(device_id, {"name": name, "dev_product_name": "H2C"})
|
||||
return DeviceSession(device_id=device_id, client=FakeClient(snapshots), printer=printer)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_collects_metrics_against_the_correct_printer_per_session():
|
||||
session_a = make_session("SERIAL-A", "Printer A", [{"nozzle_temp": 200, "gcode_state": "IDLE"}])
|
||||
session_b = make_session("SERIAL-B", "Printer B", [{"nozzle_temp": 210, "gcode_state": "IDLE"}])
|
||||
|
||||
cmd = Command()
|
||||
cmd.verbose = False
|
||||
cmd._collect_printer_data(session_a)
|
||||
cmd._collect_printer_data(session_b)
|
||||
|
||||
metric_a = PrinterMetrics.objects.get(device=session_a.printer)
|
||||
metric_b = PrinterMetrics.objects.get(device=session_b.printer)
|
||||
assert metric_a.nozzle_temp == 200
|
||||
assert metric_b.nozzle_temp == 210
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_print_job_tracking_is_isolated_per_session():
|
||||
session_a = make_session(
|
||||
"SERIAL-A",
|
||||
"Printer A",
|
||||
[
|
||||
{"gcode_state": "RUNNING", "subtask_name": "job_A", "print_percent": 10},
|
||||
{"gcode_state": "FINISH", "subtask_name": "job_A", "print_percent": 100},
|
||||
],
|
||||
)
|
||||
session_b = make_session("SERIAL-B", "Printer B", [{"gcode_state": "IDLE"}])
|
||||
|
||||
cmd = Command()
|
||||
cmd.verbose = False
|
||||
cmd._collect_printer_data(session_a)
|
||||
cmd._collect_printer_data(session_b)
|
||||
cmd._collect_printer_data(session_a)
|
||||
|
||||
assert PrintJob.objects.filter(device=session_a.printer).count() == 1
|
||||
job = PrintJob.objects.get(device=session_a.printer)
|
||||
assert job.final_status == "FINISH"
|
||||
assert session_a.current_print_job is None
|
||||
|
||||
assert PrintJob.objects.filter(device=session_b.printer).count() == 0
|
||||
assert session_b.current_print_job is None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_one_session_error_does_not_affect_another_session():
|
||||
session_a = make_session("SERIAL-A", "Printer A", [{"nozzle_temp": 200, "gcode_state": "IDLE"}])
|
||||
session_b = make_session("SERIAL-B", "Printer B", [{"nozzle_temp": 210, "gcode_state": "IDLE"}])
|
||||
|
||||
class ExplodingClient:
|
||||
def get_snapshot(self):
|
||||
raise RuntimeError("MQTT connection lost")
|
||||
|
||||
session_a.client = ExplodingClient()
|
||||
|
||||
cmd = Command()
|
||||
cmd.verbose = False
|
||||
cmd._collect_printer_data(session_a)
|
||||
cmd._collect_printer_data(session_b)
|
||||
|
||||
assert session_a.error_count == 1
|
||||
assert PrinterMetrics.objects.filter(device=session_b.printer).exists()
|
||||
69
tests/test_printer_routing.py
Normal file
69
tests/test_printer_routing.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
||||
from bambu_run.models import Printer
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def logged_in_client(client, django_user_model):
|
||||
user = django_user_model.objects.create_user(username="tester", password="pw")
|
||||
client.force_login(user)
|
||||
return client
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_dashboard_with_no_printers_shows_error(logged_in_client):
|
||||
resp = logged_in_client.get(reverse("bambu_run:printer_dashboard"))
|
||||
assert resp.status_code == 200
|
||||
assert "error" in resp.context
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_dashboard_defaults_to_first_active_printer(logged_in_client):
|
||||
printer = Printer.objects.create(name="Only Printer", model="H2C", is_active=True)
|
||||
|
||||
resp = logged_in_client.get(reverse("bambu_run:printer_dashboard"))
|
||||
|
||||
assert resp.context["printer_device"].pk == printer.pk
|
||||
assert list(resp.context["all_printers"]) == [printer]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_dashboard_pk_route_shows_requested_printer(logged_in_client):
|
||||
Printer.objects.create(name="Printer A", model="H2C", is_active=True)
|
||||
printer_b = Printer.objects.create(name="Printer B", model="X1C", is_active=True)
|
||||
|
||||
resp = logged_in_client.get(
|
||||
reverse("bambu_run:printer_dashboard", kwargs={"pk": printer_b.pk})
|
||||
)
|
||||
|
||||
assert resp.context["printer_device"].pk == printer_b.pk
|
||||
assert resp.context["device_name"] == "Printer B"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_dashboard_unknown_pk_returns_404(logged_in_client):
|
||||
resp = logged_in_client.get(
|
||||
reverse("bambu_run:printer_dashboard", kwargs={"pk": 99999})
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_api_pk_route_returns_only_requested_printer_data(logged_in_client):
|
||||
from bambu_run.models import PrinterMetrics
|
||||
from django.utils import timezone
|
||||
from decimal import Decimal
|
||||
|
||||
printer_a = Printer.objects.create(name="Printer A", model="H2C", is_active=True)
|
||||
printer_b = Printer.objects.create(name="Printer B", model="X1C", is_active=True)
|
||||
PrinterMetrics.objects.create(device=printer_a, timestamp=timezone.now(), nozzle_temp=Decimal("200"))
|
||||
PrinterMetrics.objects.create(device=printer_b, timestamp=timezone.now(), nozzle_temp=Decimal("210"))
|
||||
|
||||
resp = logged_in_client.get(
|
||||
reverse("bambu_run:printer_api", kwargs={"pk": printer_b.pk})
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["nozzle_temp"] == [210.0]
|
||||
78
tests/test_resolve_printer_device.py
Normal file
78
tests/test_resolve_printer_device.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import pytest
|
||||
|
||||
from bambu_run.management.commands.bambu_collector import resolve_printer_device
|
||||
from bambu_run.models import Printer
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_creates_new_printer_keyed_by_serial():
|
||||
printer = resolve_printer_device(
|
||||
"0309DA123456", {"name": "RNL-H2C", "dev_product_name": "H2C"}
|
||||
)
|
||||
|
||||
assert printer.serial_number == "0309DA123456"
|
||||
assert printer.name == "RNL-H2C"
|
||||
assert printer.model == "H2C"
|
||||
assert printer.is_active is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_second_call_with_same_serial_does_not_create_duplicate():
|
||||
first = resolve_printer_device("SERIAL-A", {"name": "Printer A", "dev_product_name": "H2C"})
|
||||
second = resolve_printer_device("SERIAL-A", {"name": "Printer A", "dev_product_name": "H2C"})
|
||||
|
||||
assert first.pk == second.pk
|
||||
assert Printer.objects.filter(serial_number="SERIAL-A").count() == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_two_different_serials_create_two_printers():
|
||||
a = resolve_printer_device("SERIAL-A", {"name": "Printer A", "dev_product_name": "H2C"})
|
||||
b = resolve_printer_device("SERIAL-B", {"name": "Printer B", "dev_product_name": "X1C"})
|
||||
|
||||
assert a.pk != b.pk
|
||||
assert Printer.objects.count() == 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_backfills_single_legacy_printer_with_null_serial():
|
||||
legacy = Printer.objects.create(
|
||||
name="Bambu Lab Printer", model="Bambu Lab", manufacturer="Bambu Lab", is_active=True
|
||||
)
|
||||
|
||||
resolved = resolve_printer_device("SERIAL-A", {"name": "RNL-H2C", "dev_product_name": "H2C"})
|
||||
|
||||
legacy.refresh_from_db()
|
||||
assert resolved.pk == legacy.pk
|
||||
assert legacy.serial_number == "SERIAL-A"
|
||||
assert Printer.objects.count() == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_does_not_guess_when_multiple_legacy_printers_exist():
|
||||
Printer.objects.create(name="Legacy 1", model="Bambu Lab")
|
||||
Printer.objects.create(name="Legacy 2", model="Bambu Lab")
|
||||
|
||||
resolved = resolve_printer_device("SERIAL-A", {"name": "RNL-H2C", "dev_product_name": "H2C"})
|
||||
|
||||
assert resolved.serial_number == "SERIAL-A"
|
||||
assert Printer.objects.count() == 3
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_falls_back_to_generic_defaults_without_device_info():
|
||||
printer = resolve_printer_device("SERIAL-A", None)
|
||||
|
||||
assert printer.serial_number == "SERIAL-A"
|
||||
assert printer.name == "Bambu Lab Printer"
|
||||
assert printer.model == "Bambu Lab"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_updates_name_and_model_on_existing_printer_when_changed():
|
||||
resolve_printer_device("SERIAL-A", {"name": "Old Name", "dev_product_name": "H2C"})
|
||||
|
||||
updated = resolve_printer_device("SERIAL-A", {"name": "New Name", "dev_product_name": "H2C"})
|
||||
|
||||
assert updated.name == "New Name"
|
||||
assert Printer.objects.filter(serial_number="SERIAL-A").count() == 1
|
||||
43
tests/test_vortek_groundwork.py
Normal file
43
tests/test_vortek_groundwork.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import pytest
|
||||
|
||||
from bambu_run.mqtt_client import PrinterState
|
||||
from bambu_run.management.commands.bambu_collector import Command, DeviceSession, resolve_printer_device
|
||||
from bambu_run.models import PrinterMetrics
|
||||
|
||||
|
||||
def test_snapshot_includes_raw_device_payload_for_future_vortek_modeling():
|
||||
raw_device = {
|
||||
"extruder": {"info": [{"id": 0, "temp": 12058720}, {"id": 1, "temp": 11534560}]},
|
||||
"nozzle": {"info": [{"id": 0, "diameter": 0.4}]},
|
||||
}
|
||||
data = {"print": {"device": raw_device, "gcode_state": "IDLE"}}
|
||||
|
||||
state = PrinterState.from_mqtt_data(data)
|
||||
snapshot = state.get_snapshot()
|
||||
|
||||
assert snapshot["vortek_raw"] == raw_device
|
||||
|
||||
|
||||
def test_snapshot_vortek_raw_defaults_to_empty_dict_when_no_device_payload():
|
||||
state = PrinterState.from_mqtt_data({"print": {"gcode_state": "IDLE"}})
|
||||
snapshot = state.get_snapshot()
|
||||
|
||||
assert snapshot["vortek_raw"] == {}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_collector_persists_vortek_raw_onto_printer_metrics():
|
||||
printer = resolve_printer_device("SERIAL-A", {"name": "H2C", "dev_product_name": "H2C"})
|
||||
|
||||
class FakeClient:
|
||||
def get_snapshot(self):
|
||||
return {"gcode_state": "IDLE", "vortek_raw": {"extruder": {"info": []}}}
|
||||
|
||||
session = DeviceSession(device_id="SERIAL-A", client=FakeClient(), printer=printer)
|
||||
|
||||
cmd = Command()
|
||||
cmd.verbose = False
|
||||
cmd._collect_printer_data(session)
|
||||
|
||||
metric = PrinterMetrics.objects.get(device=printer)
|
||||
assert metric.vortek_raw == {"extruder": {"info": []}}
|
||||
5
tests/urls.py
Normal file
5
tests/urls.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path("", include("bambu_run.urls")),
|
||||
]
|
||||
Reference in New Issue
Block a user