Skip to content

Commit

Permalink
Add tests for the new scenario Qt objects (#341)
Browse files Browse the repository at this point in the history
* Add documentation for fixtures and implement bw2test as fixture

* Add tests for the added Qt presamples list, table and tab

* Add tests for a number of the on-demand widget classes
  • Loading branch information
dgdekoning authored Dec 10, 2019
1 parent 213a082 commit aa9f5a0
Show file tree
Hide file tree
Showing 3 changed files with 370 additions and 1 deletion.
30 changes: 29 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
# -*- coding: utf-8 -*-
import pytest
import shutil

import brightway2 as bw
import pytest

from activity_browser import Application


@pytest.fixture(scope='session')
def ab_application():
""" Initialize the application and yield it. Cleanup the 'test' project
after session is complete.
"""
app = Application()
yield app
if 'pytest_project' in bw.projects:
Expand All @@ -15,5 +20,28 @@ def ab_application():

@pytest.fixture()
def ab_app(qtbot, ab_application):
""" Function-level fixture which returns the session-level application.
This is the actual fixture to be used in tests.
"""
ab_application.show()
return ab_application


@pytest.fixture()
def bw2test():
""" Similar to `bw2test` from bw2data.tests, but makes use of pytest
fixture setup/teardown mechanics.
Allows tests to be performed in a perfectly clean project instead
of the test project.
"""
bw.config.dont_warn = True
bw.config.is_test = True
bw.config.cache = {}
tempdir = bw.projects._use_temp_directory()
yield tempdir
bw.projects._restore_orig_directory()
# Make the jump back to the pytest_project if it exists
if "pytest_project" in bw.projects:
bw.projects.set_current("pytest_project", update=False)
shutil.rmtree(tempdir)
207 changes: 207 additions & 0 deletions tests/test_presample_scenarios.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
# -*- coding: utf-8 -*-
from pathlib import Path
import shutil

import brightway2 as bw
from bw2data.parameters import ProjectParameter
import numpy as np
import pandas as pd
import presamples as ps
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QFileDialog
import pytest

from activity_browser.app.signals import signals
from activity_browser.app.ui.tables.scenarios import PresamplesList, ScenarioTable
from activity_browser.app.ui.tabs.parameters import ParametersTab, PresamplesTab


@pytest.fixture
def project_parameters(bw2test):
ProjectParameter.create(name="test1", amount=3)
ProjectParameter.create(name="test2", amount=5)
ProjectParameter.create(name="test3", amount=7)
return


@pytest.fixture
def scenario_dataframes():
data = [
("test1", "project", 3.0),
("test2", "project", 5.0),
("test3", "project", 7.0),
]
df = pd.DataFrame(data, columns=ScenarioTable.HEADERS)
new_df = df.copy()
return df, new_df


def test_empty_presamples_list(qtbot, bw2test):
""" The presamples dropdown list has default values when no presample
packages exist.
"""
p_list = PresamplesList()
qtbot.addWidget(p_list)
assert p_list.get_package_names() == []
assert p_list.has_packages is False
assert p_list.selection == ""


def test_existing_presamples_list(qtbot, bw2test):
""" The presamples dropdown can recognize existing presample packages.
"""
cereal = np.array([49197200, 50778200, 50962400], dtype=np.int64)
fertilizer = np.array([57.63016664, 58.92761065, 54.63277483], dtype=np.float64)
land = np.array([17833000, 16161700, 15846800], dtype=np.int64)
array_stack = np.stack([cereal, fertilizer, land], axis=0)
names = ['cereal production [t]', 'fert consumption [kg/km2]', 'land [ha]']
_, pp_path = ps.create_presamples_package(
parameter_data=[(array_stack, names, "default")], name="testificate"
)

p_list = PresamplesList()
qtbot.addWidget(p_list)

packages = p_list.get_package_names()
pkg_name = next(iter(packages))
p_list.sync(pkg_name)

assert packages == ["testificate"]
assert p_list.has_packages is True
assert p_list.selection == "testificate"


def test_empty_scenario_table(qtbot, bw2test):
""" In a new/unparameterized project, the scenario table is empty.
"""
table = ScenarioTable()
qtbot.addWidget(table)
table.sync()
assert table.rowCount() == 0


def test_scenario_table(qtbot, project_parameters):
""" The scenario table will recognize existing parameters during sync.
"""
table = ScenarioTable()
qtbot.addWidget(table)
table.sync()
assert table.rowCount() == 3


def test_scenario_table_rebuild(qtbot, project_parameters):
""" Altering the amount of a parameter causes the scenario table to rebuild.
"""
tab = ParametersTab()
qtbot.addWidget(tab)
project_table = tab.tabs.get("Definitions").project_table
scenario_table = tab.tabs.get("Scenarios").tbl

begin_df = scenario_table.dataframe.copy()

assert begin_df.equals(scenario_table.dataframe)
with qtbot.waitSignal(signals.parameters_changed, timeout=500):
project_table.model.setData(project_table.model.index(0, 1), 16)
assert not begin_df.equals(scenario_table.dataframe)


def test_scenario_table_rename(qtbot, project_parameters):
""" Renaming a parameter will change the index of the dataframe
but not the values. (not that there is an easy way to test this)
"""
tab = ParametersTab()
qtbot.addWidget(tab)
project_table = tab.tabs.get("Definitions").project_table
scenario_table = tab.tabs.get("Scenarios").tbl

assert scenario_table.dataframe.index[0] == "test1"
with qtbot.waitSignal(signals.parameter_renamed, timeout=500):
project_table.rename_parameter(project_table.proxy_model.index(0, 0), "newname")
assert scenario_table.dataframe.index[0] == "newname"


def test_scenario_merge_new_scenarios(scenario_dataframes):
df, new = scenario_dataframes
assert df.equals(new)
new.insert(3, "Scenario1", [5.0, 7.0, 9.0])
new.insert(4, "Scenario2", [12.0, 16.0, 19.0])
assert not df.equals(new)

# `_perform_merge` is destructive to the 2nd DataFrame passed, so use a copy
df = ScenarioTable._perform_merge(df, new.copy())
assert df.equals(new)


def test_scenario_merge_new_rows(scenario_dataframes):
df, new = scenario_dataframes
new: pd.DataFrame
new.insert(3, "Scenario1", [5.0, 7.0, 9.0])
new.insert(4, "Scenario2", [12.0, 16.0, 19.0])
df = ScenarioTable._perform_merge(df, new.copy())
assert df.equals(new)

new = new.append({
"Name": "test4", "Group": "act1", "default": 3.0,
"Scenario1": 2.5, "Scenario2": 7.4
}, ignore_index=True)
assert not df.equals(new)
df = ScenarioTable._perform_merge(df, new.copy())
# Unknown Name/Group combinations are ignored when merging
assert "test4" not in df["Name"]


def test_scenario_merge_new_values(scenario_dataframes):
df, new = scenario_dataframes
new.insert(3, "Scenario1", [5.0, 7.0, 9.0])
new.insert(4, "Scenario2", [12.0, 16.0, 19.0])
df = ScenarioTable._perform_merge(df, new.copy())
assert df.equals(new)
# Now alter values in the existing scenario columns.
new.iat[1, 3] = 71.0
new.iat[0, 4] = 2.0
assert not df.equals(new)
df = ScenarioTable._perform_merge(df, new.copy())
assert df.equals(new)


def test_scenario_merge_empty_values(scenario_dataframes):
df, new = scenario_dataframes
new.insert(3, "Scenario1", [5.0, 7.0, 9.0])
df = ScenarioTable._perform_merge(df, new.copy())
assert df.equals(new)

new.iat[0, 3] = 50.0
new.insert(4, "Scenario2", [12.0, np.NaN, 19.0])
assert not df.equals(new)
df = ScenarioTable._perform_merge(df, new.copy())
# Now the dataframes are not equal! Why?
assert not df.equals(new)
# Because the merge causes the value from the 'default' column to be copied
# over the NaN value.
assert new["Scenario2"].hasnans
assert df.iat[1, 4] == df.iat[1, 2]


def test_scenario_tab(qtbot, monkeypatch, project_parameters):
""" Test the simple functioning of the scenario presamples tab.
clicky buttons!
"""
tab = PresamplesTab()
qtbot.addWidget(tab)
tab.build_tables()
store_path = Path(bw.projects.dir) / "testsave.xlsx"

# Save the table to the store_path, and load it in afterwards.
assert not store_path.is_file() # The file doesn't exist.
monkeypatch.setattr(QFileDialog, "getSaveFileName", staticmethod(lambda *args, **kwargs: (store_path, True)))
with qtbot.waitSignal(tab.save_btn.clicked, timeout=500):
qtbot.mouseClick(tab.save_btn, Qt.LeftButton)
qtbot.wait(500)
assert store_path.is_file() # Yes, saving the file worked.
monkeypatch.setattr(QFileDialog, "getOpenFileName", staticmethod(lambda *args, **kwargs: (store_path, True)))
with qtbot.waitSignal(tab.load_btn.clicked, timeout=500):
qtbot.mouseClick(tab.load_btn, Qt.LeftButton)

assert tab.tbl.isColumnHidden(0) is True
tab.hide_group.toggle()
assert tab.tbl.isColumnHidden(0) is False
134 changes: 134 additions & 0 deletions tests/test_widgets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QDialogButtonBox, QMessageBox, QWidget

from activity_browser.app.ui.widgets import (
BiosphereUpdater, SwitchComboBox, CutoffMenu, ForceInputDialog,
parameter_save_errorbox, simple_warning_box
)

# NOTE: No way of testing the BiosphereUpdater class without causing the
# ab_app fixture to flip its lid and fail to clean itself up.


def test_comparison_switch_empty(qtbot):
parent = QWidget()
parent.using_presamples = False
qtbot.addWidget(parent)
box = SwitchComboBox(parent)
box.configure(False, False)
size = box.count()
assert size == 0
assert not box.isVisible()


def test_comparison_switch_no_presamples(qtbot):
parent = QWidget()
parent.using_presamples = False
qtbot.addWidget(parent)
box = SwitchComboBox(parent)
box.configure()
size = box.count()
assert size == 2
# assert box.isVisible() # Box fails to be visible, except it definitely is?


def test_comparison_switch_all(qtbot):
parent = QWidget()
parent.using_presamples = True
qtbot.addWidget(parent)
box = SwitchComboBox(parent)
box.configure()
size = box.count()
assert size == 3
# assert box.isVisible() # Box fails to be visible, except it definitely is?


def test_cutoff_menu_relative(qtbot):
""" Simple check of all the slots on the CutoffMenu class
"""
slider = CutoffMenu()
qtbot.addWidget(slider)
assert slider.cutoff_value == 0.01
assert slider.is_relative

assert slider.sliders.relative.value() == 20
assert slider.sliders.relative.log_value == 1.8
qtbot.mouseClick(slider.cutoff_slider_lft_btn, Qt.LeftButton)
assert slider.sliders.relative.value() == 21
assert slider.sliders.relative.log_value == 2.0
qtbot.mouseClick(slider.cutoff_slider_rght_btn, Qt.LeftButton)
assert slider.sliders.relative.value() == 20
assert slider.sliders.relative.log_value == 1.8

with qtbot.waitSignal(slider.slider_change, timeout=500):
slider.cutoff_slider_line.setText("0.1")
assert slider.sliders.relative.value() == 40
assert slider.sliders.relative.log_value == 10


def test_cutoff_slider_toggle(qtbot):
slider = CutoffMenu()
qtbot.addWidget(slider)
with qtbot.waitSignal(slider.buttons.topx.toggled, timeout=500):
slider.buttons.topx.click()
assert not slider.is_relative
assert slider.limit_type == "number"


def test_cutoff_slider_top(qtbot):
slider = CutoffMenu()
qtbot.addWidget(slider)
slider.buttons.topx.click()

assert slider.sliders.topx.value() == 1
qtbot.mouseClick(slider.cutoff_slider_rght_btn, Qt.LeftButton)
assert slider.sliders.topx.value() == 2
qtbot.mouseClick(slider.cutoff_slider_lft_btn, Qt.LeftButton)
assert slider.sliders.topx.value() == 1

with qtbot.waitSignal(slider.slider_change, timeout=500):
slider.cutoff_slider_line.setText("15")
assert slider.sliders.topx.value() == 15


def test_input_dialog(qtbot):
""" Test the various thing about the dialog widget.
"""
parent = QWidget()
qtbot.addWidget(parent)
dialog = ForceInputDialog.get_text(
parent, "Early in the morning", "What should we do with a drunken sailor"
)
assert dialog.output == ""
assert not dialog.buttons.button(QDialogButtonBox.Ok).isEnabled()

existing = ForceInputDialog.get_text(
parent, "Existence", "is a nightmare", "and here is why"
)
assert existing.output == "and here is why"
# Text in dialog MUST be changed before Ok button is enabled.
assert not dialog.buttons.button(QDialogButtonBox.Ok).isEnabled()
with qtbot.waitSignal(dialog.input.textChanged, timeout=100):
dialog.input.setText("Now it works.")
assert dialog.buttons.button(QDialogButtonBox.Ok).isEnabled()


def test_parameter_errorbox(qtbot, monkeypatch):
""" Not truly used anymore in favour of not saving invalid values.
"""
parent = QWidget()
qtbot.addWidget(parent)

monkeypatch.setattr(QMessageBox, "exec_", lambda *args: QMessageBox.Cancel)
result = parameter_save_errorbox(parent, "got an error")
assert result == QMessageBox.Cancel


def test_simple_warning_box(qtbot, monkeypatch):
parent = QWidget()
qtbot.addWidget(parent)

monkeypatch.setattr(QMessageBox, "warning", lambda *args: QMessageBox.Ok)
result = simple_warning_box(parent, "Warning title", "This is a warning")
assert result == QMessageBox.Ok

0 comments on commit aa9f5a0

Please sign in to comment.