Removed AI generated test suites
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,7 +4,6 @@ __pycache__/
|
|||||||
*.egg-info/
|
*.egg-info/
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
.pytest_cache/
|
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
|
|||||||
25
.vscode/tasks.json
vendored
25
.vscode/tasks.json
vendored
@@ -1,31 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
|
||||||
"label": "Tests: Run pytest",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "${command:python.interpreterPath}",
|
|
||||||
"args": [
|
|
||||||
"-m",
|
|
||||||
"pytest"
|
|
||||||
],
|
|
||||||
"options": {
|
|
||||||
"cwd": "${workspaceFolder}",
|
|
||||||
"env": {
|
|
||||||
"PYTHONPATH": "${workspaceFolder}/src"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"group": {
|
|
||||||
"kind": "test",
|
|
||||||
"isDefault": true
|
|
||||||
},
|
|
||||||
"presentation": {
|
|
||||||
"clear": true,
|
|
||||||
"reveal": "always",
|
|
||||||
"panel": "dedicated"
|
|
||||||
},
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"label": "Qt: Open Designer",
|
"label": "Qt: Open Designer",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
|
|||||||
@@ -21,17 +21,8 @@ bedit = "bedit_gui.application:main"
|
|||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=8",
|
|
||||||
"ruff>=0.5",
|
"ruff>=0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
|
||||||
addopts = "--strict-config --strict-markers -ra"
|
|
||||||
testpaths = ["tests"]
|
|
||||||
markers = [
|
|
||||||
"unit: fast tests without GUI event-loop interaction",
|
|
||||||
"gui: tests that create or interact with Qt objects",
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
"""Shared pytest fixtures for bedit tests.
|
|
||||||
|
|
||||||
Qt-specific fixtures can be added here later when GUI testing starts. Keeping
|
|
||||||
the model fixtures independent from Qt lets the core suite stay lightweight.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from bedit_core.models import (
|
|
||||||
Component,
|
|
||||||
ComponentID,
|
|
||||||
Document,
|
|
||||||
Graph,
|
|
||||||
GraphImplementation,
|
|
||||||
ID,
|
|
||||||
Interface,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def minimal_document() -> Document:
|
|
||||||
"""Return the smallest useful graph document for core tests."""
|
|
||||||
root_id = ComponentID("root")
|
|
||||||
root = Component(
|
|
||||||
name="Root",
|
|
||||||
interface=Interface(),
|
|
||||||
parameters={},
|
|
||||||
implementation=GraphImplementation(Graph()),
|
|
||||||
)
|
|
||||||
return Document(
|
|
||||||
format_version=1,
|
|
||||||
id=ID("document"),
|
|
||||||
name="Test document",
|
|
||||||
root={root_id: root},
|
|
||||||
)
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from PySide6.QtCore import Qt
|
|
||||||
from PySide6.QtGui import QUndoCommand
|
|
||||||
from PySide6.QtWidgets import QApplication
|
|
||||||
|
|
||||||
from bedit_gui.controllers.document_controller import DocumentController
|
|
||||||
from bedit_gui.controllers.log_controller import LogController
|
|
||||||
from bedit_gui.controllers.undo_controller import UndoController
|
|
||||||
from bedit_gui.documents import Document
|
|
||||||
from bedit_gui.services.application_logging import get_logger, set_log_level
|
|
||||||
from bedit_gui.views.dialogs.document_dialogs import SaveChangesChoice
|
|
||||||
from bedit_gui.views.main_window import MainWindow
|
|
||||||
|
|
||||||
|
|
||||||
class FakeDialogs:
|
|
||||||
def __init__(self, path: Path) -> None:
|
|
||||||
self.path = path
|
|
||||||
|
|
||||||
def choose_open_path(self, _current_path: Path | None) -> Path:
|
|
||||||
return self.path
|
|
||||||
|
|
||||||
def choose_save_path(self, _current_path: Path | None) -> Path:
|
|
||||||
return self.path
|
|
||||||
|
|
||||||
def ask_save_changes(self) -> SaveChangesChoice:
|
|
||||||
return SaveChangesChoice.DISCARD
|
|
||||||
|
|
||||||
def show_file_error(self, _title: str, error: Exception) -> None:
|
|
||||||
raise AssertionError("unexpected file error") from error
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def qt_app() -> QApplication:
|
|
||||||
return QApplication.instance() or QApplication([])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_log_level_and_qt_log_model(qt_app: QApplication) -> None:
|
|
||||||
window = MainWindow()
|
|
||||||
controller = LogController(window, logging.WARNING)
|
|
||||||
logger = get_logger("tests")
|
|
||||||
|
|
||||||
logger.info("hidden message")
|
|
||||||
logger.warning("visible message")
|
|
||||||
qt_app.processEvents()
|
|
||||||
|
|
||||||
assert controller.model.rowCount() == 1
|
|
||||||
assert "WARNING visible message" in controller.model.data(
|
|
||||||
controller.model.index(0),
|
|
||||||
Qt.ItemDataRole.DisplayRole,
|
|
||||||
)
|
|
||||||
|
|
||||||
set_log_level(logging.INFO)
|
|
||||||
logger.info("now visible")
|
|
||||||
qt_app.processEvents()
|
|
||||||
|
|
||||||
assert controller.model.rowCount() == 2
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_document_and_undo_actions_are_logged(
|
|
||||||
qt_app: QApplication,
|
|
||||||
tmp_path: Path,
|
|
||||||
) -> None:
|
|
||||||
path = tmp_path / "document.json"
|
|
||||||
window = MainWindow()
|
|
||||||
log_controller = LogController(window)
|
|
||||||
document = Document(qt_app)
|
|
||||||
dialogs = FakeDialogs(path)
|
|
||||||
document_controller = DocumentController(document, window, dialogs)
|
|
||||||
undo_controller = UndoController(document, window)
|
|
||||||
|
|
||||||
document_controller.new_document()
|
|
||||||
document_controller.save_document_as()
|
|
||||||
document_controller.save_document()
|
|
||||||
document_controller.open_document()
|
|
||||||
document.undo_stack.push(QUndoCommand("test change"))
|
|
||||||
undo_controller.undo()
|
|
||||||
undo_controller.redo()
|
|
||||||
qt_app.processEvents()
|
|
||||||
|
|
||||||
messages = [
|
|
||||||
log_controller.model.data(log_controller.model.index(row))
|
|
||||||
for row in range(log_controller.model.rowCount())
|
|
||||||
]
|
|
||||||
assert any("Created new document" in message for message in messages)
|
|
||||||
assert any("Opened document:" in message for message in messages)
|
|
||||||
assert any("Saved document:" in message for message in messages)
|
|
||||||
assert any("Saved document as:" in message for message in messages)
|
|
||||||
assert any("Undo: test change" in message for message in messages)
|
|
||||||
assert any("Redo: test change" in message for message in messages)
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from bedit_core.bondgraph import flatten_bondgraph
|
|
||||||
from bedit_core.models import (
|
|
||||||
BondConnection,
|
|
||||||
BondPort,
|
|
||||||
Component,
|
|
||||||
ComponentID,
|
|
||||||
ConnectionID,
|
|
||||||
EquationImplementation,
|
|
||||||
Graph,
|
|
||||||
GraphImplementation,
|
|
||||||
Interface,
|
|
||||||
PortID,
|
|
||||||
SignalDirection,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _leaf(name: str, port_id: PortID) -> Component:
|
|
||||||
return Component(
|
|
||||||
name=name,
|
|
||||||
interface=Interface(
|
|
||||||
{port_id: BondPort(name="p", direction=SignalDirection.INPUT)}
|
|
||||||
),
|
|
||||||
parameters={},
|
|
||||||
implementation=EquationImplementation(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_flattens_bonds_across_a_component_boundary() -> None:
|
|
||||||
boundary_id = PortID("boundary")
|
|
||||||
inner_id = PortID("inner")
|
|
||||||
outer_id = PortID("outer")
|
|
||||||
inner = _leaf("Inner", inner_id)
|
|
||||||
subsystem = Component(
|
|
||||||
name="Subsystem",
|
|
||||||
interface=Interface(
|
|
||||||
{
|
|
||||||
boundary_id: BondPort(
|
|
||||||
name="boundary",
|
|
||||||
direction=SignalDirection.INPUT,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
parameters={},
|
|
||||||
implementation=GraphImplementation(
|
|
||||||
Graph(
|
|
||||||
components={ComponentID("inner"): inner},
|
|
||||||
connections={
|
|
||||||
ConnectionID("inside"): BondConnection(
|
|
||||||
boundary_id,
|
|
||||||
inner_id,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
outer = _leaf("Outer", outer_id)
|
|
||||||
root = Component(
|
|
||||||
name="Root",
|
|
||||||
interface=Interface(),
|
|
||||||
parameters={},
|
|
||||||
implementation=GraphImplementation(
|
|
||||||
Graph(
|
|
||||||
components={
|
|
||||||
ComponentID("subsystem"): subsystem,
|
|
||||||
ComponentID("outer"): outer,
|
|
||||||
},
|
|
||||||
connections={
|
|
||||||
ConnectionID("outside"): BondConnection(
|
|
||||||
outer_id,
|
|
||||||
boundary_id,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
network = flatten_bondgraph(root)
|
|
||||||
boundary = next(port for port in network.ports if port.port_id == boundary_id)
|
|
||||||
|
|
||||||
assert len(network.components) == 4
|
|
||||||
assert len(network.ports) == 3
|
|
||||||
assert len(network.bonds) == 2
|
|
||||||
assert len(network.bonds_for(boundary)) == 2
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from PySide6.QtGui import QUndoCommand
|
|
||||||
from PySide6.QtWidgets import QApplication
|
|
||||||
|
|
||||||
from bedit_gui.controllers.document_controller import DocumentController
|
|
||||||
from bedit_gui.documents import Document
|
|
||||||
from bedit_gui.views.dialogs.document_dialogs import SaveChangesChoice
|
|
||||||
from bedit_gui.views.main_window import MainWindow
|
|
||||||
|
|
||||||
|
|
||||||
class FakeDialogs:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.open_path: Path | None = None
|
|
||||||
self.save_path: Path | None = None
|
|
||||||
self.save_choice = SaveChangesChoice.CANCEL
|
|
||||||
self.errors: list[tuple[str, Exception]] = []
|
|
||||||
|
|
||||||
def choose_open_path(self, _current_path: Path | None) -> Path | None:
|
|
||||||
return self.open_path
|
|
||||||
|
|
||||||
def choose_save_path(self, _current_path: Path | None) -> Path | None:
|
|
||||||
return self.save_path
|
|
||||||
|
|
||||||
def ask_save_changes(self) -> SaveChangesChoice:
|
|
||||||
return self.save_choice
|
|
||||||
|
|
||||||
def show_file_error(self, title: str, error: Exception) -> None:
|
|
||||||
self.errors.append((title, error))
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def qt_app() -> QApplication:
|
|
||||||
return QApplication.instance() or QApplication([])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_new_document_respects_unsaved_changes(qt_app: QApplication) -> None:
|
|
||||||
document = Document(qt_app)
|
|
||||||
window = MainWindow()
|
|
||||||
dialogs = FakeDialogs()
|
|
||||||
controller = DocumentController(document, window, dialogs)
|
|
||||||
original_id = document.model.id
|
|
||||||
document.undo_stack.push(QUndoCommand("change"))
|
|
||||||
|
|
||||||
controller.new_document()
|
|
||||||
assert document.model.id == original_id
|
|
||||||
|
|
||||||
dialogs.save_choice = SaveChangesChoice.DISCARD
|
|
||||||
controller.new_document()
|
|
||||||
assert document.model.id != original_id
|
|
||||||
assert not document.modified
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_save_as_then_open_document(
|
|
||||||
qt_app: QApplication,
|
|
||||||
tmp_path: Path,
|
|
||||||
) -> None:
|
|
||||||
document = Document(qt_app)
|
|
||||||
window = MainWindow()
|
|
||||||
dialogs = FakeDialogs()
|
|
||||||
controller = DocumentController(document, window, dialogs)
|
|
||||||
original_id = document.model.id
|
|
||||||
path = tmp_path / "document.json"
|
|
||||||
|
|
||||||
dialogs.save_path = path
|
|
||||||
assert controller.save_document_as()
|
|
||||||
|
|
||||||
document.new()
|
|
||||||
dialogs.open_path = path
|
|
||||||
controller.open_document()
|
|
||||||
|
|
||||||
assert document.model.id == original_id
|
|
||||||
assert document.path == path
|
|
||||||
assert not dialogs.errors
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from bedit_core.models import Document
|
|
||||||
from bedit_util.graphviz import document_to_dot
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_dot_contains_component_name_path(minimal_document: Document) -> None:
|
|
||||||
dot = document_to_dot(minimal_document, "Root")
|
|
||||||
|
|
||||||
assert 'label="Root"' in dot
|
|
||||||
assert 'xlabel="Root"' in dot
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from PySide6.QtGui import QUndoCommand
|
|
||||||
|
|
||||||
from bedit_gui.documents import Document
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
@pytest.mark.parametrize("suffix", [".beb", ".json"])
|
|
||||||
def test_document_save_and_open(tmp_path: Path, suffix: str) -> None:
|
|
||||||
path = tmp_path / f"document{suffix}"
|
|
||||||
document = Document()
|
|
||||||
original_id = document.model.id
|
|
||||||
|
|
||||||
document.save_as(path)
|
|
||||||
document.new()
|
|
||||||
assert document.model.id != original_id
|
|
||||||
|
|
||||||
document.open(path)
|
|
||||||
|
|
||||||
assert document.model.id == original_id
|
|
||||||
assert document.path == path
|
|
||||||
assert not document.modified
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_document_modified_state_uses_undo_stack() -> None:
|
|
||||||
document = Document()
|
|
||||||
|
|
||||||
document.undo_stack.push(QUndoCommand("change"))
|
|
||||||
assert document.modified
|
|
||||||
|
|
||||||
document.undo_stack.setClean()
|
|
||||||
assert not document.modified
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
"""Basic smoke tests for the core model."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from bedit_core.models import Document, GraphImplementation
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_minimal_document_has_graph_root(minimal_document: Document) -> None:
|
|
||||||
root = next(iter(minimal_document.root.values()))
|
|
||||||
|
|
||||||
assert isinstance(root.implementation, GraphImplementation)
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from PySide6.QtWidgets import QApplication
|
|
||||||
|
|
||||||
from bedit_core import models
|
|
||||||
from bedit_core.models import (
|
|
||||||
BondPort,
|
|
||||||
Component,
|
|
||||||
Interface,
|
|
||||||
PortID,
|
|
||||||
SignalDirection,
|
|
||||||
SignalPort,
|
|
||||||
ValueType,
|
|
||||||
)
|
|
||||||
from bedit_gui.documents import Document
|
|
||||||
from bedit_gui.views.port_editor_widget import PortEditorWidget
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def qt_app() -> QApplication:
|
|
||||||
return QApplication.instance() or QApplication([])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_port_editor_uses_a_detached_draft(qt_app: QApplication) -> None:
|
|
||||||
port_id = PortID("input")
|
|
||||||
original = {
|
|
||||||
port_id: SignalPort(
|
|
||||||
name="Input",
|
|
||||||
direction=SignalDirection.INPUT,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
editor = PortEditorWidget()
|
|
||||||
editor.set_ports(original)
|
|
||||||
|
|
||||||
editor.ui.nameEdit.setText("Changed")
|
|
||||||
editor.ui.nameEdit.textEdited.emit("Changed")
|
|
||||||
|
|
||||||
assert original[port_id].name == "Input"
|
|
||||||
assert editor.ports()[port_id].name == "Changed"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_port_type_controls_option_visibility(qt_app: QApplication) -> None:
|
|
||||||
editor = PortEditorWidget()
|
|
||||||
editor.set_ports(
|
|
||||||
{
|
|
||||||
PortID("port"): SignalPort(
|
|
||||||
name="Port",
|
|
||||||
direction=SignalDirection.INPUT,
|
|
||||||
value_type=ValueType.REAL,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
assert not editor.ui.signalTypeComboBox.isHidden()
|
|
||||||
assert editor.ui.domainComboBox.isHidden()
|
|
||||||
|
|
||||||
editor.ui.typeBond.setChecked(True)
|
|
||||||
|
|
||||||
assert editor.ui.signalTypeComboBox.isHidden()
|
|
||||||
assert not editor.ui.domainComboBox.isHidden()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_bond_causality_is_loaded_and_changed(qt_app: QApplication) -> None:
|
|
||||||
port_id = PortID("bond")
|
|
||||||
editor = PortEditorWidget()
|
|
||||||
editor.set_ports({port_id: BondPort("Bond", SignalDirection.INPUT, causality_preference=models.PortCausality.PREFERRED_FLOW_OUT)})
|
|
||||||
|
|
||||||
assert editor.ui.causalityComboBox.currentData() is models.PortCausality.PREFERRED_FLOW_OUT
|
|
||||||
|
|
||||||
index = editor.ui.causalityComboBox.findData(models.PortCausality.FIXED_EFFORT_OUT)
|
|
||||||
editor.ui.causalityComboBox.setCurrentIndex(index)
|
|
||||||
|
|
||||||
assert editor.ports()[port_id].causality_preference is models.PortCausality.FIXED_EFFORT_OUT
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_port_changes_are_one_undoable_operation(
|
|
||||||
qt_app: QApplication,
|
|
||||||
minimal_document,
|
|
||||||
) -> None:
|
|
||||||
component: Component = next(iter(minimal_document.root.values()))
|
|
||||||
removed_id = PortID("removed")
|
|
||||||
changed_id = PortID("changed")
|
|
||||||
added_id = PortID("added")
|
|
||||||
component.interface = Interface(
|
|
||||||
{
|
|
||||||
removed_id: SignalPort("Remove", SignalDirection.INPUT),
|
|
||||||
changed_id: SignalPort("Before", SignalDirection.INPUT),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
original = deepcopy(component.interface.ports)
|
|
||||||
updated = {
|
|
||||||
changed_id: BondPort(
|
|
||||||
"After",
|
|
||||||
SignalDirection.OUTPUT,
|
|
||||||
domain="electrical",
|
|
||||||
),
|
|
||||||
added_id: SignalPort("Added", SignalDirection.INPUT),
|
|
||||||
}
|
|
||||||
document = Document(qt_app)
|
|
||||||
|
|
||||||
document.update_component_ports(component, updated)
|
|
||||||
assert component.interface.ports == updated
|
|
||||||
|
|
||||||
document.undo_stack.undo()
|
|
||||||
assert component.interface.ports == original
|
|
||||||
|
|
||||||
document.undo_stack.redo()
|
|
||||||
assert component.interface.ports == updated
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
"""Basic smoke tests for document serialization."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from bedit_core.models import Document
|
|
||||||
from bedit_core.serialization import load, save
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_json_round_trip(tmp_path, minimal_document: Document) -> None:
|
|
||||||
path = tmp_path / "document.json"
|
|
||||||
|
|
||||||
save(minimal_document, path)
|
|
||||||
|
|
||||||
assert load(path) == minimal_document
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from PySide6.QtCore import QSettings
|
|
||||||
from PySide6.QtWidgets import QApplication, QDialog
|
|
||||||
|
|
||||||
from bedit_gui.controllers.settings_controller import SettingsController
|
|
||||||
from bedit_gui.services.application_logging import get_logger
|
|
||||||
from bedit_gui.services.application_settings import ApplicationSettings
|
|
||||||
from bedit_gui.views.main_window import MainWindow
|
|
||||||
|
|
||||||
|
|
||||||
class FakeSettingsDialog:
|
|
||||||
def __init__(self, log_level: int) -> None:
|
|
||||||
self.initial_log_level = log_level
|
|
||||||
self.log_level = logging.ERROR
|
|
||||||
|
|
||||||
def exec(self) -> QDialog.DialogCode:
|
|
||||||
return QDialog.DialogCode.Accepted
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def qt_app() -> QApplication:
|
|
||||||
return QApplication.instance() or QApplication([])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_log_level_is_persisted(tmp_path: Path) -> None:
|
|
||||||
path = tmp_path / "settings.ini"
|
|
||||||
backend = QSettings(str(path), QSettings.Format.IniFormat)
|
|
||||||
settings = ApplicationSettings(backend)
|
|
||||||
|
|
||||||
settings.log_level = logging.DEBUG
|
|
||||||
backend.sync()
|
|
||||||
|
|
||||||
reloaded = ApplicationSettings(
|
|
||||||
QSettings(str(path), QSettings.Format.IniFormat)
|
|
||||||
)
|
|
||||||
assert reloaded.log_level == logging.DEBUG
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_settings_action_applies_log_level(
|
|
||||||
qt_app: QApplication,
|
|
||||||
tmp_path: Path,
|
|
||||||
) -> None:
|
|
||||||
backend = QSettings(
|
|
||||||
str(tmp_path / "settings.ini"),
|
|
||||||
QSettings.Format.IniFormat,
|
|
||||||
)
|
|
||||||
settings = ApplicationSettings(backend)
|
|
||||||
window = MainWindow()
|
|
||||||
dialogs: list[FakeSettingsDialog] = []
|
|
||||||
|
|
||||||
def make_dialog(
|
|
||||||
log_level: int,
|
|
||||||
_window: MainWindow,
|
|
||||||
) -> FakeSettingsDialog:
|
|
||||||
dialog = FakeSettingsDialog(log_level)
|
|
||||||
dialogs.append(dialog)
|
|
||||||
return dialog
|
|
||||||
|
|
||||||
SettingsController(window, settings, make_dialog)
|
|
||||||
window.ui.actionSettings.trigger()
|
|
||||||
|
|
||||||
assert dialogs[0].initial_log_level == logging.INFO
|
|
||||||
assert settings.log_level == logging.ERROR
|
|
||||||
assert get_logger("tests").isEnabledFor(logging.ERROR)
|
|
||||||
assert not get_logger("tests").isEnabledFor(logging.WARNING)
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import re
|
|
||||||
import socket
|
|
||||||
import threading
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Mapping, Sequence
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from bedit_simulation import (
|
|
||||||
OpenModelicaRunner,
|
|
||||||
ProcessResult,
|
|
||||||
Simulation,
|
|
||||||
SimulationOptions,
|
|
||||||
SimulationStateError,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _fake_omc(
|
|
||||||
command: Sequence[str],
|
|
||||||
working_directory: Path,
|
|
||||||
timeout: float | None,
|
|
||||||
environment: Mapping[str, str] | None,
|
|
||||||
) -> ProcessResult:
|
|
||||||
del timeout, environment
|
|
||||||
script = Path(command[-1]).read_text(encoding="utf-8")
|
|
||||||
if "buildModel(Example" in script:
|
|
||||||
(working_directory / "Example").touch()
|
|
||||||
if "system(" in script:
|
|
||||||
port_match = re.search(r"-port=(\d+)", script)
|
|
||||||
assert port_match is not None
|
|
||||||
with socket.create_connection(
|
|
||||||
("127.0.0.1", int(port_match.group(1)))
|
|
||||||
) as connection:
|
|
||||||
connection.sendall(
|
|
||||||
b'<status phase="integration" currentStepSize="0.1" '
|
|
||||||
b'time="1" progress="50"/>\n'
|
|
||||||
)
|
|
||||||
(working_directory / "Example_res.csv").write_text(
|
|
||||||
'"time","x"\n0,1\n1,2\n',
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
return ProcessResult(
|
|
||||||
tuple(command),
|
|
||||||
0,
|
|
||||||
'"Check of Example completed successfully.\n'
|
|
||||||
'Class Example has 1 equation(s) and 1 variable(s)."\n'
|
|
||||||
'""\n',
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_runs_omc_through_configured_command(tmp_path: Path) -> None:
|
|
||||||
runner = OpenModelicaRunner(
|
|
||||||
["./run-omc-in-docker"],
|
|
||||||
executor=_fake_omc,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def run() -> object:
|
|
||||||
simulation = Simulation(runner)
|
|
||||||
await simulation.load_modelica(
|
|
||||||
"model Example\nend Example;\n",
|
|
||||||
"Example",
|
|
||||||
)
|
|
||||||
await simulation.build(tmp_path)
|
|
||||||
return await simulation.run(
|
|
||||||
SimulationOptions(stop_time=2, number_of_intervals=20),
|
|
||||||
)
|
|
||||||
|
|
||||||
result = asyncio.run(run())
|
|
||||||
|
|
||||||
assert result.data == {"time": [0.0, 1.0], "x": [1.0, 2.0]}
|
|
||||||
assert result.result_file == tmp_path / "Example_res.csv"
|
|
||||||
script = (tmp_path / "run.mos").read_text(encoding="utf-8")
|
|
||||||
assert f'cd("{tmp_path}")' in script
|
|
||||||
assert "stepSize=0.1" in script
|
|
||||||
assert "-logFormat=xmltcp" in script
|
|
||||||
assert "simulate(Example" not in script
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_rejects_invalid_time_range() -> None:
|
|
||||||
with pytest.raises(ValueError, match="stop_time"):
|
|
||||||
SimulationOptions(start_time=1, stop_time=1)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_run_requires_a_built_model() -> None:
|
|
||||||
simulation = Simulation(OpenModelicaRunner("omc", executor=_fake_omc))
|
|
||||||
|
|
||||||
async def run() -> None:
|
|
||||||
await simulation.load_modelica(
|
|
||||||
"model Example\nend Example;\n",
|
|
||||||
"Example",
|
|
||||||
)
|
|
||||||
with pytest.raises(SimulationStateError, match=r"call build\(\) first"):
|
|
||||||
await simulation.run()
|
|
||||||
|
|
||||||
asyncio.run(run())
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_checks_existing_modelica_source(tmp_path: Path) -> None:
|
|
||||||
simulation = Simulation(OpenModelicaRunner("omc", executor=_fake_omc))
|
|
||||||
|
|
||||||
async def check() -> object:
|
|
||||||
await simulation.load_modelica(
|
|
||||||
"model Example\nend Example;\n",
|
|
||||||
"Example",
|
|
||||||
)
|
|
||||||
return await simulation.check(working_directory=tmp_path)
|
|
||||||
|
|
||||||
result = asyncio.run(check())
|
|
||||||
|
|
||||||
assert result.successful
|
|
||||||
assert result.output == [
|
|
||||||
"Check of Example completed successfully.",
|
|
||||||
"Class Example has 1 equation(s) and 1 variable(s).",
|
|
||||||
"",
|
|
||||||
]
|
|
||||||
script = (tmp_path / "model-command.mos").read_text(encoding="utf-8")
|
|
||||||
assert "checkModel(Example);" in script
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_async_execution_uses_a_worker_thread(tmp_path: Path) -> None:
|
|
||||||
caller_thread = threading.get_ident()
|
|
||||||
execution_threads: list[int] = []
|
|
||||||
|
|
||||||
def executor(
|
|
||||||
command: Sequence[str],
|
|
||||||
working_directory: Path,
|
|
||||||
timeout: float | None,
|
|
||||||
environment: Mapping[str, str] | None,
|
|
||||||
) -> ProcessResult:
|
|
||||||
del working_directory, timeout, environment
|
|
||||||
execution_threads.append(threading.get_ident())
|
|
||||||
return ProcessResult(tuple(command), 0, '"OpenModelica test"', "")
|
|
||||||
|
|
||||||
simulation = Simulation(OpenModelicaRunner("omc", executor=executor))
|
|
||||||
|
|
||||||
result = asyncio.run(
|
|
||||||
simulation.execute(
|
|
||||||
"getVersion()",
|
|
||||||
working_directory=tmp_path,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result.return_code == 0
|
|
||||||
assert len(execution_threads) == 1
|
|
||||||
assert execution_threads[0] != caller_thread
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_requires_and_retains_a_loaded_model(tmp_path: Path) -> None:
|
|
||||||
simulation = Simulation(OpenModelicaRunner("omc", executor=_fake_omc))
|
|
||||||
|
|
||||||
async def use_session() -> None:
|
|
||||||
with pytest.raises(SimulationStateError, match="no model is loaded"):
|
|
||||||
await simulation.check()
|
|
||||||
|
|
||||||
await simulation.load_modelica(
|
|
||||||
"model Example\nend Example;\n",
|
|
||||||
"Example",
|
|
||||||
)
|
|
||||||
check = await simulation.check(working_directory=tmp_path)
|
|
||||||
|
|
||||||
assert simulation.model_name == "Example"
|
|
||||||
assert simulation.last_check is check
|
|
||||||
|
|
||||||
asyncio.run(use_session())
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import matplotlib
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from bedit_simulation import SimulationResult
|
|
||||||
from bedit_util.simulate import create_results_figure
|
|
||||||
|
|
||||||
matplotlib.use("Agg")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_creates_result_figure_with_trace_controls() -> None:
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
result = SimulationResult(
|
|
||||||
model_name="Example",
|
|
||||||
data={
|
|
||||||
"time": [0.0, 1.0],
|
|
||||||
"x": [1.0, 2.0],
|
|
||||||
"y": [3.0, 4.0],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
figure = create_results_figure(result)
|
|
||||||
|
|
||||||
assert {line.get_label() for line in figure.axes[0].lines} == {"x", "y"}
|
|
||||||
assert len(figure._bedit_widgets) == 3 # type: ignore[attr-defined]
|
|
||||||
plt.close(figure)
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from PySide6.QtWidgets import QApplication
|
|
||||||
|
|
||||||
from bedit_gui.controllers.view_menu_controller import ViewMenuController
|
|
||||||
from bedit_gui.views.main_window import MainWindow
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def qt_app() -> QApplication:
|
|
||||||
return QApplication.instance() or QApplication([])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_view_submenus_contain_visibility_actions(
|
|
||||||
qt_app: QApplication,
|
|
||||||
) -> None:
|
|
||||||
window = MainWindow()
|
|
||||||
controller = ViewMenuController(window)
|
|
||||||
|
|
||||||
assert window.ui.actionPanels.menu() is controller.panels_menu
|
|
||||||
assert window.ui.actionToolbars.menu() is controller.toolbars_menu
|
|
||||||
assert [action.text() for action in controller.panels_menu.actions()] == [
|
|
||||||
"Document Tree",
|
|
||||||
"Log",
|
|
||||||
]
|
|
||||||
assert [action.text() for action in controller.toolbars_menu.actions()] == [
|
|
||||||
"File",
|
|
||||||
"Undo",
|
|
||||||
]
|
|
||||||
assert all(
|
|
||||||
action.isCheckable()
|
|
||||||
for action in (
|
|
||||||
controller.panels_menu.actions()
|
|
||||||
+ controller.toolbars_menu.actions()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Reference in New Issue
Block a user