Skip to content

Commit e39a86a

Browse files
Add testing
1 parent 182f4d0 commit e39a86a

14 files changed

Lines changed: 571 additions & 1500 deletions

.github/workflows/ci.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,17 @@ jobs:
2929
3030
- name: Lint
3131
run: |
32-
uv run ruff check --exclude tests .
32+
uv run ruff check .
3333
3434
- name: Format
3535
run: |
36-
uv run ruff format --check --exclude tests .
36+
uv run ruff format --check .
3737
38-
# Tests are temporarily disabled in CI until they are ready.
39-
# To re-enable, uncomment the step below once tests are stable.
40-
# - name: Run pytest
41-
# run: |
42-
# uv run pytest
38+
- name: Run pytest
39+
env:
40+
QT_QPA_PLATFORM: offscreen
41+
run: |
42+
uv run pytest --cov-fail-under=48
4343
4444
type-check:
4545
name: Type Check
@@ -61,5 +61,5 @@ jobs:
6161
6262
- name: Type check
6363
run: |
64-
uv run ty check src
64+
uv run ty check .
6565

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ repos:
99
hooks:
1010
- id: ruff-format
1111
- id: ruff-check
12-
args: ["--fix", "--exclude", "tests"]
12+
args: ["--fix"]
1313

1414
- repo: local
1515
hooks:
1616
- id: ty-check
17-
name: ty check (src)
18-
entry: uv run ty check src
17+
name: ty check
18+
entry: uv run ty check .
1919
language: system
2020
pass_filenames: false

pyproject.toml

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ dependencies = [
3333
dev = [
3434
"pytest>=8.0.0",
3535
"pytest-qt>=4.4.0",
36+
"pytest-timeout>=2.3.0",
3637
"pytest-cov>=4.1.0",
3738
"ruff>=0.1.0",
38-
# "mypy>=1.8.0",
3939
"pre-commit>=3.6",
4040
"ty>=0.0.30",
4141
"pandas-stubs>=3.0.0.260204",
@@ -76,10 +76,26 @@ python_classes = ["Test*"]
7676
python_functions = ["test_*"]
7777
addopts = [
7878
"--verbose",
79+
"--timeout=3",
7980
"--cov=dbs_annotator",
8081
"--cov-report=html",
8182
"--cov-report=term-missing",
8283
]
84+
filterwarnings = [
85+
"ignore::DeprecationWarning",
86+
]
87+
markers = [
88+
"gui: Qt GUI / widget tests",
89+
"integration: cross-module behavior",
90+
"slow: longer GUI or lazy-loaded wizard flows; skip locally with pytest -m \"not slow\"",
91+
]
92+
93+
[tool.coverage.run]
94+
source = ["src"]
95+
omit = ["*/tests/*"]
96+
97+
[tool.coverage.report]
98+
omit = ["*/tests/*"]
8399

84100
[tool.ruff]
85101
line-length = 88
@@ -107,21 +123,3 @@ select = [
107123
ignore = [
108124
"E501", # line too long (handled by formatter/line-length)
109125
]
110-
111-
[tool.mypy]
112-
python_version = "3.11"
113-
warn_return_any = true
114-
warn_unused_configs = true
115-
disallow_untyped_defs = false
116-
disallow_incomplete_defs = false
117-
check_untyped_defs = true
118-
no_implicit_optional = true
119-
warn_redundant_casts = true
120-
warn_unused_ignores = true
121-
warn_no_return = true
122-
strict_optional = true
123-
124-
[[tool.mypy.overrides]]
125-
module = "PySide6.*"
126-
ignore_missing_imports = true
127-
Lines changed: 101 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -1,179 +1,110 @@
1-
#!/usr/bin/env python3
2-
"""
3-
Integration tests for export functionality.
1+
"""Integration tests for SessionExporter (PySide6, small fixtures)."""
42

5-
Tests the complete export workflow from UI to file generation.
6-
"""
3+
from __future__ import annotations
74

8-
import sys
9-
import tempfile
10-
import unittest
11-
from pathlib import Path
125
from unittest.mock import patch
136

147
import pandas as pd
8+
import pytest
159

16-
# Add src directory to path
17-
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
18-
19-
from PyQt5.QtWidgets import QApplication
20-
21-
from dbs_annotator.controllers.wizard_controller import WizardController
2210
from dbs_annotator.models.session_data import SessionData
2311
from dbs_annotator.utils.session_exporter import SessionExporter
24-
from dbs_annotator.views.step3_view import Step3View
25-
26-
27-
class TestExportIntegration(unittest.TestCase):
28-
"""Test export functionality integration."""
29-
30-
def setUp(self):
31-
"""Set up test environment."""
32-
self.app = QApplication(sys.argv)
33-
34-
# Create sample data
35-
self.sample_data = [
36-
{
37-
"date": "2024-01-15",
38-
"time": "09:30:00",
39-
"block_id": "1",
40-
"scale_name": "YBOCS",
41-
"scale_value": "20",
42-
"stim_freq": "130",
43-
"left_contact": "e1-e3",
44-
"left_amplitude": "3.5",
45-
"left_pulse_width": "60",
46-
"right_contact": "e2-e4",
47-
"right_amplitude": "4.0",
48-
"right_pulse_width": "60",
49-
"notes": "Baseline measurement",
50-
},
51-
{
52-
"date": "2024-01-15",
53-
"time": "10:30:00",
54-
"block_id": "2",
55-
"scale_name": "YBOCS",
56-
"scale_value": "18",
57-
"stim_freq": "130",
58-
"left_contact": "e1-e3",
59-
"left_amplitude": "4.0",
60-
"left_pulse_width": "60",
61-
"right_contact": "e2-e4",
62-
"right_amplitude": "4.5",
63-
"right_pulse_width": "60",
64-
"notes": "After stimulation",
65-
},
66-
]
67-
68-
# Create temporary TSV file
69-
self.temp_file = tempfile.NamedTemporaryFile(
70-
mode="w", suffix=".tsv", delete=False
71-
)
72-
df = pd.DataFrame(self.sample_data)
73-
df.to_csv(self.temp_file.name, sep="\t", index=False)
74-
self.temp_file.close()
75-
76-
# Create session data with temporary file
77-
self.session_data = SessionData()
78-
self.session_data.file_path = self.temp_file.name
79-
80-
# Create exporter
81-
self.exporter = SessionExporter(self.session_data)
82-
83-
def tearDown(self):
84-
"""Clean up test environment."""
85-
Path(self.temp_file.name).unlink(missing_ok=True)
86-
87-
@patch("PyQt5.QtWidgets.QFileDialog.getSaveFileName")
88-
@patch("PyQt5.QtWidgets.QMessageBox.information")
89-
def test_excel_export_integration(self, mock_msgbox, mock_file_dialog):
90-
"""Test Excel export integration."""
91-
with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as excel_file:
92-
excel_path = excel_file.name
93-
94-
mock_file_dialog.return_value = (excel_path, "Excel Files (*.xlsx)")
95-
96-
result = self.exporter.export_to_excel()
97-
98-
self.assertTrue(result)
99-
self.assertTrue(Path(excel_path).exists())
100-
mock_msgbox.assert_called_once()
101-
102-
@patch("PyQt5.QtWidgets.QFileDialog.getSaveFileName")
103-
@patch("PyQt5.QtWidgets.QMessageBox.information")
104-
def test_word_export_integration(self, mock_msgbox, mock_file_dialog):
105-
"""Test Word export integration."""
106-
with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as word_file:
107-
word_path = word_file.name
108-
109-
mock_file_dialog.return_value = (word_path, "Word Files (*.docx)")
110-
111-
result = self.exporter.export_to_word()
112-
113-
self.assertTrue(result)
114-
self.assertTrue(Path(word_path).exists())
115-
mock_msgbox.assert_called_once()
116-
117-
@patch("PyQt5.QtWidgets.QFileDialog.getSaveFileName")
118-
@patch("PyQt5.QtWidgets.QMessageBox.information")
119-
def test_pdf_export_integration(self, mock_msgbox, mock_file_dialog):
120-
"""Test PDF export integration."""
121-
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as pdf_file:
122-
pdf_path = pdf_file.name
123-
124-
mock_file_dialog.return_value = (pdf_path, "PDF Files (*.pdf)")
125-
126-
result = self.exporter.export_to_pdf()
127-
128-
self.assertTrue(result)
129-
self.assertTrue(Path(pdf_path).exists())
130-
mock_msgbox.assert_called_once()
131-
132-
def test_export_with_no_data(self):
133-
"""Test export behavior with no data."""
134-
# Create empty session data
135-
empty_session = SessionData()
136-
empty_exporter = SessionExporter(empty_session)
137-
138-
with patch("PyQt5.QtWidgets.QMessageBox.warning") as mock_warning:
139-
result = empty_exporter.export_to_excel()
140-
self.assertFalse(result)
141-
mock_warning.assert_called_once()
142-
143-
def test_export_with_no_file_open(self):
144-
"""Test export behavior with no file open."""
145-
# Create session data without file
146-
no_file_session = SessionData()
147-
no_file_session.file_path = None
148-
no_file_exporter = SessionExporter(no_file_session)
149-
150-
with patch("PyQt5.QtWidgets.QMessageBox.warning") as mock_warning:
151-
result = no_file_exporter.export_to_excel()
152-
self.assertFalse(result)
153-
mock_warning.assert_called_once()
154-
155-
156-
class TestExportUIIntegration(unittest.TestCase):
157-
"""Test export UI integration."""
158-
159-
def setUp(self):
160-
"""Set up UI test environment."""
161-
self.app = QApplication(sys.argv)
162-
self.controller = WizardController()
163-
self.step3_view = Step3View()
164-
165-
def test_export_menu_creation(self):
166-
"""Test export menu is created correctly."""
167-
self.assertIsNotNone(self.step3_view.export_button)
168-
self.assertIsNotNone(self.step3_view.export_menu)
169-
self.assertEqual(len(self.step3_view.export_menu.actions()), 3)
170-
171-
def test_export_actions_exist(self):
172-
"""Test all export actions exist."""
173-
self.assertIsNotNone(self.step3_view.export_excel_action)
174-
self.assertIsNotNone(self.step3_view.export_word_action)
175-
self.assertIsNotNone(self.step3_view.export_pdf_action)
176-
17712

178-
if __name__ == "__main__":
179-
unittest.main()
13+
14+
@pytest.fixture
15+
def sample_tsv(tmp_path):
16+
rows = [
17+
{
18+
"date": "2024-01-15",
19+
"time": "09:30:00",
20+
"block_id": "1",
21+
"is_initial": "1",
22+
"scale_name": "YBOCS",
23+
"scale_value": "20",
24+
"notes": "n",
25+
},
26+
{
27+
"date": "2024-01-15",
28+
"time": "10:30:00",
29+
"block_id": "2",
30+
"is_initial": "0",
31+
"scale_name": "YBOCS",
32+
"scale_value": "18",
33+
"notes": "n2",
34+
},
35+
]
36+
p = tmp_path / "sub-01_task-prog_run-01_events.tsv"
37+
pd.DataFrame(rows).to_csv(p, sep="\t", index=False)
38+
return p
39+
40+
41+
def test_export_to_word_integration(monkeypatch, tmp_path, sample_tsv):
42+
sd = SessionData()
43+
sd.open_file_append(str(sample_tsv))
44+
try:
45+
exporter = SessionExporter(sd)
46+
out = tmp_path / "report.docx"
47+
monkeypatch.setattr(exporter, "_export_to_word_path", lambda *a, **k: True)
48+
with (
49+
patch(
50+
"PySide6.QtWidgets.QFileDialog.getSaveFileName",
51+
return_value=(str(out), "Word"),
52+
),
53+
patch(
54+
"dbs_annotator.utils.session_exporter.SessionExporter._show_transient_message",
55+
),
56+
):
57+
assert exporter.export_to_word() is True
58+
finally:
59+
sd.close_file()
60+
61+
62+
def test_export_to_pdf_integration(monkeypatch, tmp_path, sample_tsv):
63+
sd = SessionData()
64+
sd.open_file_append(str(sample_tsv))
65+
try:
66+
exporter = SessionExporter(sd)
67+
pdf = tmp_path / "report.pdf"
68+
monkeypatch.setattr(exporter, "_export_to_word_path", lambda *a, **k: True)
69+
monkeypatch.setattr(exporter, "_convert_docx_to_pdf", lambda *a, **k: None)
70+
monkeypatch.setattr(exporter, "_open_file", lambda *a, **k: None)
71+
with (
72+
patch(
73+
"PySide6.QtWidgets.QFileDialog.getSaveFileName",
74+
return_value=(str(pdf), "PDF"),
75+
),
76+
patch(
77+
"dbs_annotator.utils.session_exporter.SessionExporter._show_transient_message",
78+
),
79+
):
80+
assert exporter.export_to_pdf() is True
81+
finally:
82+
sd.close_file()
83+
84+
85+
def test_export_word_no_data_warns():
86+
empty = SessionData()
87+
ex = SessionExporter(empty)
88+
with patch("dbs_annotator.utils.session_exporter.QMessageBox.warning") as w:
89+
assert ex.export_to_word() is False
90+
w.assert_called()
91+
92+
93+
def test_export_word_no_file_warns():
94+
sd = SessionData()
95+
sd.file_path = None
96+
ex = SessionExporter(sd)
97+
with patch("dbs_annotator.utils.session_exporter.QMessageBox.warning") as w:
98+
assert ex.export_to_word() is False
99+
w.assert_called()
100+
101+
102+
@pytest.mark.gui
103+
def test_step3_export_actions_exist(qtbot, qapp):
104+
from dbs_annotator.views.step3_view import Step3View
105+
106+
v = Step3View()
107+
qtbot.addWidget(v)
108+
assert v.export_word_action is not None
109+
assert v.export_pdf_action is not None
110+
assert len(v.export_menu.actions()) == 2

0 commit comments

Comments
 (0)