This commit is contained in:
Joaquin Gottlebe
2025-07-30 15:57:48 +02:00
parent e49fcfac46
commit a639c34cee
273 changed files with 20151 additions and 0 deletions
@@ -0,0 +1,27 @@
"""
Shared test fixtures for pytest.
This file defines reusable fixtures that are automatically discovered by pytest
and can be used across multiple test modules.
Fixtures:
---------
- local_tmp_path: Creates (or cleans) a temporary directory under 'tests/tmp'
to store output files generated during tests. Ensures a clean environment
for each test run.
"""
from pathlib import Path
import pytest
@pytest.fixture
def local_tmp_path():
"""Fixture to create and return a temporary path for output files."""
path = Path("tests/tmp")
if path.exists():
for file in path.iterdir():
file.unlink()
else:
path.mkdir(parents=True)
return path
@@ -0,0 +1 @@
197429970.81
@@ -0,0 +1 @@
1
@@ -0,0 +1,4 @@
Jahr,January,February,March,April,May,June,July,August,September,October,November,December
1951,3.09,5.73,11.01,19.91,20.33,21.02,23.88,20.41,17.51,19.25,3.85,5.18
1952,3.30,5.20,12.43,19.57,21.20,22.47,26.14,20.75,11.74,7.96,3.79,3.12
1 Jahr January February March April May June July August September October November December
2 1951 3.09 5.73 11.01 19.91 20.33 21.02 23.88 20.41 17.51 19.25 3.85 5.18
3 1952 3.30 5.20 12.43 19.57 21.20 22.47 26.14 20.75 11.74 7.96 3.79 3.12
@@ -0,0 +1,3 @@
Jahr,January,February,March,April,May,June,July,August,September,October,November,December
1951,30.9,57.3,110.1,199.1,203.3,210.2,238.8,204.1,175.1,192.5,38.5,51.8
1952,33.0,52.0,124.3,195.7,212.0,224.7,261.4,207.5,117.4,79.6,37.9,31.2
1 Jahr January February March April May June July August September October November December
2 1951 30.9 57.3 110.1 199.1 203.3 210.2 238.8 204.1 175.1 192.5 38.5 51.8
3 1952 33.0 52.0 124.3 195.7 212.0 224.7 261.4 207.5 117.4 79.6 37.9 31.2
@@ -0,0 +1,7 @@
Zeitreihen fuer Gebietsmittel fuer Bundeslaender und Kombinationen von Bundeslaender, erstellt am: 20250602
Jahr;Monat;Brandenburg/Berlin;Brandenburg;Baden-Wuerttemberg;Bayern;Hessen;Mecklenburg-Vorpommern;Niedersachsen;Niedersachsen/Hamburg/Bremen;Nordrhein-Westfalen;Rheinland-Pfalz;Schleswig-Holstein;Saarland;Sachsen;Sachsen-Anhalt;Thueringen/Sachsen-Anhalt;Thueringen;Deutschland;
1951;01; 45.2; 45.3; 30.6; 34.5; 22.8; 39.7; 24.4; 24.6; 23.3; 23.9; 35.8; 21.5; 37.6; 28.0; 26.0; 23.5; 30.9;
1952;01; 38.9; 39.0; 39.1; 39.3; 19.3; 38.1; 25.9; 26.0; 23.3; 27.0; 38.1; 15.2; 41.9; 35.0; 30.7; 25.3; 33.0;
@@ -0,0 +1,7 @@
Zeitreihen fuer Gebietsmittel fuer Bundeslaender und Kombinationen von Bundeslaender, erstellt am: 20250602
Jahr;Monat;Brandenburg/Berlin;Brandenburg;Baden-Wuerttemberg;Bayern;Hessen;Mecklenburg-Vorpommern;Niedersachsen;Niedersachsen/Hamburg/Bremen;Nordrhein-Westfalen;Rheinland-Pfalz;Schleswig-Holstein;Saarland;Sachsen;Sachsen-Anhalt;Thueringen/Sachsen-Anhalt;Thueringen;Deutschland;
1951;02; 67.0; 67.2; 65.9; 63.7; 49.7; 56.1; 44.6; 44.5; 52.1; 59.1; 37.5; 59.8; 80.6; 50.2; 50.5; 50.9; 57.3;
1952;02; 61.5; 61.5; 61.1; 42.5; 43.6; 62.9; 52.3; 52.4; 42.9; 57.4; 65.0; 74.9; 46.4; 58.5; 52.0; 43.9; 52.0;
@@ -0,0 +1,8 @@
Zeitreihen fuer Gebietsmittel fuer Bundeslaender und Kombinationen von Bundeslaender, erstellt am: 20250602
Jahr;Monat;Brandenburg/Berlin;Brandenburg;Baden-Wuerttemberg;Bayern;Hessen;Mecklenburg-Vorpommern;Niedersachsen;Niedersachsen/Hamburg/Bremen;Nordrhein-Westfalen;Rheinland-Pfalz;Schleswig-Holstein;Saarland;Sachsen;Sachsen-Anhalt;Thueringen/Sachsen-Anhalt;Thueringen;Deutschland;
1951;03; 129.5; 129.5; 96.3; 98.9; 114.6; 117.4; 114.6; 114.9; 105.0; 107.3; 117.9; 110.3; 116.9; 118.1; 115.7; 112.7; 110.1;
1952;03; 163.3; 163.3; 115.7; 107.0; 110.7; 178.7; 121.5; 121.9; 99.7; 106.1; 152.4; 111.7; 137.6; 136.8; 125.3; 110.9; 124.3;
@@ -0,0 +1,8 @@
Zeitreihen fuer Gebietsmittel fuer Bundeslaender und Kombinationen von Bundeslaender, erstellt am: 20250602
Jahr;Monat;Brandenburg/Berlin;Brandenburg;Baden-Wuerttemberg;Bayern;Hessen;Mecklenburg-Vorpommern;Niedersachsen;Niedersachsen/Hamburg/Bremen;Nordrhein-Westfalen;Rheinland-Pfalz;Schleswig-Holstein;Saarland;Sachsen;Sachsen-Anhalt;Thueringen/Sachsen-Anhalt;Thueringen;Deutschland;
1951;04; 227.3; 227.4; 195.6; 198.0; 189.0; 226.3; 185.3; 185.4; 180.3; 187.5; 189.3; 196.8; 223.6; 208.2; 204.0; 198.7; 199.1;
1952;04; 204.4; 204.6; 180.2; 181.8; 192.0; 207.6; 205.3; 205.5; 203.2; 199.5; 209.7; 223.8; 195.2; 200.8; 195.4; 188.6; 195.7;
@@ -0,0 +1,8 @@
Zeitreihen fuer Gebietsmittel fuer Bundeslaender und Kombinationen von Bundeslaender, erstellt am: 20250602
Jahr;Monat;Brandenburg/Berlin;Brandenburg;Baden-Wuerttemberg;Bayern;Hessen;Mecklenburg-Vorpommern;Niedersachsen;Niedersachsen/Hamburg/Bremen;Nordrhein-Westfalen;Rheinland-Pfalz;Schleswig-Holstein;Saarland;Sachsen;Sachsen-Anhalt;Thueringen/Sachsen-Anhalt;Thueringen;Deutschland;
1951;05; 195.3; 195.5; 197.8; 192.2; 202.7; 244.1; 211.1; 211.5; 203.8; 205.3; 243.9; 216.2; 200.5; 180.5; 182.2; 184.4; 203.3;
1952;05; 203.6; 203.8; 226.9; 196.6; 222.7; 214.4; 227.7; 227.9; 211.8; 221.0; 237.9; 247.1; 182.4; 204.0; 202.4; 200.4; 212.0;
@@ -0,0 +1,8 @@
Zeitreihen fuer Gebietsmittel fuer Bundeslaender und Kombinationen von Bundeslaender, erstellt am: 20250602
Jahr;Monat;Brandenburg/Berlin;Brandenburg;Baden-Wuerttemberg;Bayern;Hessen;Mecklenburg-Vorpommern;Niedersachsen;Niedersachsen/Hamburg/Bremen;Nordrhein-Westfalen;Rheinland-Pfalz;Schleswig-Holstein;Saarland;Sachsen;Sachsen-Anhalt;Thueringen/Sachsen-Anhalt;Thueringen;Deutschland;
1951;06; 245.1; 245.3; 194.2; 198.7; 190.0; 255.5; 212.8; 213.4; 191.4; 190.1; 251.0; 190.8; 223.8; 210.6; 203.7; 195.0; 210.2;
1952;06; 233.1; 233.0; 254.6; 227.7; 226.9; 223.7; 202.2; 202.2; 204.6; 236.4; 198.5; 265.3; 230.9; 233.8; 229.2; 223.3; 224.7;
@@ -0,0 +1,8 @@
Zeitreihen fuer Gebietsmittel fuer Bundeslaender und Kombinationen von Bundeslaender, erstellt am: 20250602
Jahr;Monat;Brandenburg/Berlin;Brandenburg;Baden-Wuerttemberg;Bayern;Hessen;Mecklenburg-Vorpommern;Niedersachsen;Niedersachsen/Hamburg/Bremen;Nordrhein-Westfalen;Rheinland-Pfalz;Schleswig-Holstein;Saarland;Sachsen;Sachsen-Anhalt;Thueringen/Sachsen-Anhalt;Thueringen;Deutschland;
1951;07; 252.6; 252.6; 270.6; 258.0; 238.3; 248.3; 205.1; 205.3; 202.2; 241.8; 213.4; 270.7; 248.7; 229.5; 233.3; 238.1; 238.8;
1952;07; 269.1; 269.2; 306.9; 296.0; 267.1; 259.3; 209.4; 209.4; 211.0; 266.1; 236.3; 302.3; 281.3; 245.1; 251.7; 259.9; 261.4;
@@ -0,0 +1,8 @@
Zeitreihen fuer Gebietsmittel fuer Bundeslaender und Kombinationen von Bundeslaender, erstellt am: 20250602
Jahr;Monat;Brandenburg/Berlin;Brandenburg;Baden-Wuerttemberg;Bayern;Hessen;Mecklenburg-Vorpommern;Niedersachsen;Niedersachsen/Hamburg/Bremen;Nordrhein-Westfalen;Rheinland-Pfalz;Schleswig-Holstein;Saarland;Sachsen;Sachsen-Anhalt;Thueringen/Sachsen-Anhalt;Thueringen;Deutschland;
1951;08; 213.9; 214.1; 213.8; 220.7; 201.5; 203.9; 192.5; 192.8; 179.8; 198.1; 196.3; 213.4; 226.3; 186.6; 188.6; 191.2; 204.1;
1952;08; 220.3; 220.1; 238.0; 227.1; 195.1; 214.9; 184.4; 184.7; 176.2; 198.4; 186.7; 218.7; 223.0; 196.2; 195.8; 195.3; 207.5;
@@ -0,0 +1,8 @@
Zeitreihen fuer Gebietsmittel fuer Bundeslaender und Kombinationen von Bundeslaender, erstellt am: 20250602
Jahr;Monat;Brandenburg/Berlin;Brandenburg;Baden-Wuerttemberg;Bayern;Hessen;Mecklenburg-Vorpommern;Niedersachsen;Niedersachsen/Hamburg/Bremen;Nordrhein-Westfalen;Rheinland-Pfalz;Schleswig-Holstein;Saarland;Sachsen;Sachsen-Anhalt;Thueringen/Sachsen-Anhalt;Thueringen;Deutschland;
1951;09; 205.3; 205.3; 170.7; 173.1; 165.8; 196.2; 167.8; 168.0; 149.1; 173.7; 166.9; 190.9; 182.6; 191.9; 183.2; 172.3; 175.1;
1952;09; 136.3; 136.3; 116.1; 101.1; 93.5; 177.3; 128.1; 128.7; 103.2; 106.0; 161.1; 117.8; 103.5; 116.3; 103.3; 87.0; 117.4;
@@ -0,0 +1,8 @@
Zeitreihen fuer Gebietsmittel fuer Bundeslaender und Kombinationen von Bundeslaender, erstellt am: 20250602
Jahr;Monat;Brandenburg/Berlin;Brandenburg;Baden-Wuerttemberg;Bayern;Hessen;Mecklenburg-Vorpommern;Niedersachsen;Niedersachsen/Hamburg/Bremen;Nordrhein-Westfalen;Rheinland-Pfalz;Schleswig-Holstein;Saarland;Sachsen;Sachsen-Anhalt;Thueringen/Sachsen-Anhalt;Thueringen;Deutschland;
1951;10; 227.4; 227.5; 151.6; 183.7; 184.3; 225.7; 204.9; 205.4; 181.9; 170.1; 215.6; 169.7; 206.0; 200.4; 195.0; 188.2; 192.5;
1952;10; 55.8; 55.9; 94.2; 89.6; 81.9; 76.2; 71.9; 71.8; 81.0; 100.1; 66.5; 103.2; 71.0; 72.9; 72.9; 72.9; 79.6;
@@ -0,0 +1,8 @@
Zeitreihen fuer Gebietsmittel fuer Bundeslaender und Kombinationen von Bundeslaender, erstellt am: 20250602
Jahr;Monat;Brandenburg/Berlin;Brandenburg;Baden-Wuerttemberg;Bayern;Hessen;Mecklenburg-Vorpommern;Niedersachsen;Niedersachsen/Hamburg/Bremen;Nordrhein-Westfalen;Rheinland-Pfalz;Schleswig-Holstein;Saarland;Sachsen;Sachsen-Anhalt;Thueringen/Sachsen-Anhalt;Thueringen;Deutschland;
1951;11; 32.7; 32.8; 54.7; 48.5; 27.5; 37.7; 28.7; 28.7; 39.2; 44.3; 28.3; 45.9; 35.0; 29.6; 29.0; 28.3; 38.5;
1952;11; 31.3; 31.4; 44.6; 40.8; 34.1; 31.4; 32.2; 32.2; 43.4; 38.1; 39.6; 32.3; 44.0; 33.4; 36.1; 39.4; 37.9;
@@ -0,0 +1,8 @@
Zeitreihen fuer Gebietsmittel fuer Bundeslaender und Kombinationen von Bundeslaender, erstellt am: 20250602
Jahr;Monat;Brandenburg/Berlin;Brandenburg;Baden-Wuerttemberg;Bayern;Hessen;Mecklenburg-Vorpommern;Niedersachsen;Niedersachsen/Hamburg/Bremen;Nordrhein-Westfalen;Rheinland-Pfalz;Schleswig-Holstein;Saarland;Sachsen;Sachsen-Anhalt;Thueringen/Sachsen-Anhalt;Thueringen;Deutschland;
1951;12; 51.2; 51.4; 69.9; 42.7; 38.1; 53.2; 51.6; 51.5; 59.5; 49.6; 42.7; 65.1; 61.3; 52.3; 50.7; 48.7; 51.8;
1952;12; 35.8; 35.9; 34.9; 28.4; 30.6; 29.9; 24.0; 24.0; 32.0; 33.7; 21.7; 33.7; 50.5; 29.8; 31.6; 33.9; 31.2;
@@ -0,0 +1,83 @@
"""
Unit tests for the calculate_difference.py module, which verifies the correctness
of the energy difference calculation.
"""
from unittest import mock
import pandas as pd
from calculate_difference import calc_energy_difference
@mock.patch("calculate_difference.logs.log_processed")
@mock.patch("calculate_difference.logs.log_processing")
@mock.patch("calculate_difference.checks.check_dir")
@mock.patch("calculate_difference.checks.check_path")
@mock.patch("calculate_difference.utils.save_df")
@mock.patch("calculate_difference.utils.read_df")
def test_calc_energy_difference(mock_read_df, mock_save_df,
_mock_check_path, _mock_check_dir,
_mock_log_processing, _mock_log_processed):
"""
Test that calc_difference_difference computes the correct energy difference per
year and saves the expected DataFrame.
"""
df_actual = pd.DataFrame({
'year': [2018, 2019, 2020],
'Summe': [100, 200, 300]
})
df_theoretical = pd.DataFrame({
'year': [2018, 2019, 2020],
'Summe': [90, 210, 310]
})
mock_read_df.side_effect = [df_actual, df_theoretical]
calc_energy_difference("actual.csv", "theoretical.csv", "output.csv")
expected_diff = abs(df_actual['Summe'] - df_theoretical['Summe'])
expected_df = pd.DataFrame({
'year': df_actual["year"],
'Energy Difference [kWh]': expected_diff
})
result_df = mock_save_df.call_args[0][0]
# adjust types, because "2018" =/= 2018
expected_df["year"] = expected_df["year"].astype(int)
result_df["year"] = result_df["year"].astype(int)
pd.testing.assert_frame_equal(
result_df.sort_values(by="year").reset_index(drop=True),
expected_df.sort_values(by="year").reset_index(drop=True)
)
@mock.patch("calculate_difference.logs.log_processed")
@mock.patch("calculate_difference.logs.log_processing")
@mock.patch("calculate_difference.checks.check_dir")
@mock.patch("calculate_difference.checks.check_path")
@mock.patch("calculate_difference.utils.save_df")
@mock.patch("calculate_difference.utils.read_df")
def test_calc_energy_difference_dimensions(mock_read_df, mock_save_df,
_mock_check_path, _mock_check_dir,
_mock_log_processing, _mock_log_processed):
"""
Test that the resulting DataFrame has the expected shape: same row count as input
and two columns ('year' and 'Energy Difference [kWh]').
"""
df_actual = pd.DataFrame({
'year': [2018, 2019, 2020],
'Summe': [100, 200, 300]
})
df_theoretical = pd.DataFrame({
'year': [2018, 2019, 2020],
'Summe': [90, 210, 310]
})
mock_read_df.side_effect = [df_actual, df_theoretical]
calc_energy_difference("actual.csv", "theoretical.csv", "output.csv")
result_df = mock_save_df.call_args[0][0]
# checks whether the number of rows is the same
assert len(result_df) == len(df_actual)
# checks exactly 2 columns ("year", "energy difference")
assert result_df.shape[1] == 2
@@ -0,0 +1,23 @@
"""Unit tests for the calculate_energy function """
import os
import pandas as pd
from calculate_energy import calculate_energy
def test_calculate_energy(local_tmp_path):
"""
Test that calculate_energy processes solarparc area and sunshine data to compute
energy output, and writes the result to a non-empty CSV file.
"""
area_data_path = "tests/data/area_easy.txt"
sunshine_data_path = "tests/data/sunshine_duration.csv"
output_path = local_tmp_path / "energy.csv"
expected_path = "tests/data/solarparc_energy.csv"
calculate_energy(area_data_path, sunshine_data_path, output_path, 1)
expected_data = pd.read_csv(expected_path)
output_data = pd.read_csv(output_path)
assert os.path.exists(output_path)
pd.testing.assert_frame_equal(expected_data, output_data)
@@ -0,0 +1,160 @@
"""
Unit tests for the `checks` module, which validate various input verification functions.
"""
import sys
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point
import pytest
import checks
def test_check_empty():
"""
Test that check_empty raises ValueError for empty or None inputs, and
passes for valid inputs.
"""
checks.check_empty("hello")
checks.check_empty([1, 2, 3])
try:
checks.check_empty("")
except ValueError:
pass
else:
assert False, "Expected ValueError for empty string"
try:
checks.check_empty([])
except ValueError:
pass
else:
assert False, "Expected ValueError for empty list"
try:
checks.check_empty(None)
except ValueError:
pass
else:
assert False, "Expected ValueError for None"
def test_check_path(local_tmp_path):
"""
Test that check_path passes for existing files and raises FileNotFoundError
for missing files.
"""
tmp_file = local_tmp_path / "file.txt"
tmp_file.write_text("test")
checks.check_path(str(tmp_file))
with pytest.raises(FileNotFoundError):
checks.check_path(str(local_tmp_path / "nonexistent.txt"))
def test_check_dir(local_tmp_path):
"""Test that check_dir verifies if the directory part of a path exists."""
some_file = local_tmp_path / "some_file.txt"
checks.check_dir(str(some_file))
with pytest.raises(FileNotFoundError):
checks.check_dir("/non/existing/parent_dir/file.txt")
def test_check_args():
"""Test that check_args validates the number of command-line arguments."""
original_argv = sys.argv
sys.argv = ['prog', 'arg1', 'arg2']
checks.check_args(3, "Wrong args count")
try:
checks.check_args(2, "Wrong args count")
except RuntimeError:
pass
else:
assert False, "Expected RuntimeError for wrong arg count"
sys.argv = original_argv
def test_check_crs():
"""
Test that check_crs passes for matching CRS values and raises ValueError
on mismatch.
"""
checks.check_crs("EPSG:4326", "EPSG:4326")
try:
checks.check_crs("EPSG:4326", "EPSG:3857")
except ValueError:
pass
else:
assert False, "Expected ValueError for CRS mismatch"
def test_check_df():
"""Test that check_df validates input as non-empty pandas DataFrame with non-NaN data."""
df = pd.DataFrame({"a": [1, 2]})
checks.check_df(df)
try:
checks.check_df([1, 2, 3])
except TypeError:
pass
else:
assert False, "Expected TypeError for non-DataFrame"
try:
checks.check_df(pd.DataFrame())
except ValueError:
pass
else:
assert False, "Expected ValueError for empty DataFrame"
try:
checks.check_df(pd.DataFrame({"a": [None, None]}))
except ValueError:
pass
else:
assert False, "Expected ValueError for DataFrame all NaNs"
def test_check_gdf():
"""Test that check_gdf validates input as non-empty GeoDataFrame with valid geometries."""
gdf = gpd.GeoDataFrame({'geometry': [Point(0, 0)]})
checks.check_gdf(gdf)
try:
checks.check_gdf(pd.DataFrame())
except TypeError:
pass
else:
assert False, "Expected TypeError for non-GeoDataFrame"
try:
checks.check_gdf(gpd.GeoDataFrame())
except ValueError:
pass
else:
assert False, "Expected ValueError for empty GeoDataFrame"
empty_geom = gpd.GeoDataFrame({'geometry': [None, None]})
try:
checks.check_gdf(empty_geom)
except ValueError:
pass
else:
assert False, "Expected ValueError for GeoDataFrame with empty geometries"
def test_check_member():
"""Test that check_member confirms presence of a member in a collection."""
checks.check_member('a', ['a', 'b', 'c'])
try:
checks.check_member('d', ['a', 'b', 'c'])
except ValueError:
pass
else:
assert False, "Expected ValueError for invalid member"
@@ -0,0 +1,45 @@
"""
Unit tests for the module clean_pv_data.py
"""
from unittest.mock import patch
import pandas as pd
from clean_pv_data import clean_pv_data
@patch("utils.save_df")
@patch("utils.read_df")
@patch("checks.check_empty")
@patch("checks.check_dir")
@patch("checks.check_path")
@patch("logs.log_processing")
@patch("logs.log_processed")
def test_clean_pv_data(_mock_log_processed, _mock_log_processing,
_mock_check_path, _mock_check_dir, _mock_check_empty,
mock_read_df, mock_save_df):
"""
Test that clean_pv_data filters by variable label and reshapes the data into
a wide format by month.
"""
# Dummy parameter setup
extracted_type = "Electricity feed-in"
inputh_path = "dummy_input.csv"
output_path = "dummy_output.csv"
input_data = pd.DataFrame({
"time": ["2022", "2022", "2022"],
"1_variable_attribute_label": ["January", "February", "January"],
"value": [100, 150, 300],
"value_variable_label": ["Electricity feed-in",
"Electricity feed-in",
"Net nominal capacity"]
})
mock_read_df.return_value = input_data
clean_pv_data(inputh_path, output_path, extracted_type)
result_df = mock_save_df.call_args[0][0]
assert "January" in result_df.columns
assert "February" in result_df.columns
assert result_df.loc["2022", "January"] == 100
assert result_df.loc["2022", "February"] == 150
assert "Net nominal capacity" not in result_df.values
@@ -0,0 +1,21 @@
""""
Unit test verifying that the clip function correctly clips spatial data
using a provided overlay.
"""
import geopandas as gpd
from clip import clip
def test_clip(local_tmp_path): # pylint: disable=redefined-outer-name
"""Test that clip() writes a clipped GeoDataFrame matching the expected result"""
input_path = "tests/data/solarparcs.gpkg"
overlay_path = "tests/data/germany.gpkg"
expected_path = "tests/data/clip.gpkg"
output_path = local_tmp_path / "output.gpkg"
clip(input_path, overlay_path, output_path)
expected_data = gpd.read_file(expected_path)
output_data = gpd.read_file(output_path)
assert len(expected_data) == len(output_data)
@@ -0,0 +1,53 @@
"""
Unit tests for the collapse_columns function, which merges multiple columns
by summing their values.
"""
from unittest.mock import patch
import pandas as pd
from collapse_columns import collapse_columns
@patch("utils.save_df")
@patch("utils.read_df")
@patch("checks.check_dir")
@patch("checks.check_path")
def test_sum_values_correctly(_mock_check_path, _mock_check_dir,
mock_read_df, mock_save_df):
"""
Test that collapse_columns sums values across all columns and stores the
result in the specified column.
"""
input_df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
column_name = "sum"
expected_df = pd.DataFrame({column_name: [5, 7, 9]})
mock_read_df.return_value = input_df
collapse_columns("in.csv", "out.csv", column_name)
result_df = mock_save_df.call_args[0][0]
pd.testing.assert_frame_equal(result_df, expected_df)
@patch("utils.save_df")
@patch("utils.read_df")
@patch("checks.check_dir")
@patch("checks.check_path")
def test_output_dimension(_mock_check_path,
_mock_check_dir, mock_read_df, mock_save_df):
"""
Test that the output DataFrame has one column after merging and retains
the original number of rows.
"""
input_df = pd.DataFrame({
"X": [1, 2],
"Y": [3, 4]
})
column_name = "sum"
mock_read_df.return_value = input_df
expected_rows = input_df.shape[0]
collapse_columns("in.csv", "out.csv", column_name)
result_df = mock_save_df.call_args[0][0]
assert result_df.shape[1] == 1, "Output should have exactly one column"
assert result_df.shape[0] == expected_rows, "Output should have same number of rows as input"
@@ -0,0 +1,66 @@
"""
Unit tests for logging functions.
"""
from unittest.mock import patch
import logs
def test_log_processing():
"""Test logging of processing start."""
with patch('logging.info') as mock_info:
logs.log_processing("ToolA")
mock_info.assert_called_once_with("Processing %s:", "ToolA")
def test_log_processed():
"""Test logging of processing completion."""
with patch('logging.info') as mock_info:
logs.log_processed("ToolB")
mock_info.assert_called_once_with("Processed: %s", "ToolB")
def test_log_read():
"""Test logging of file read event."""
with patch('logging.info') as mock_info:
logs.log_read("/path/to/file.txt")
mock_info.assert_called_once_with("Read: %s", "/path/to/file.txt")
def test_log_read_error():
"""Test logging of file read error."""
with patch('logging.error') as mock_error:
logs.log_read_error("/path/to/missing_file.txt")
mock_error.assert_called_once_with(
"Failed to read: %s", "/path/to/missing_file.txt")
def test_log_saved():
"""Test logging of file save event."""
with patch('logging.info') as mock_info:
logs.log_saved("/path/to/save_location.txt")
mock_info.assert_called_once_with(
"Saved: %s", "/path/to/save_location.txt")
def test_log_saved_error():
"""Test logging of file save error."""
with patch('logging.error') as mock_error:
logs.log_saved_error("/path/to/save_location.txt")
mock_error.assert_called_once_with(
"Failed to save: %s", "/path/to/save_location.txt")
def test_log_parameter():
"""Test logging of parameter."""
with patch('logging.info') as mock_info:
logs.log_parameter('parameter_name', str(999))
mock_info.assert_called_once_with(
"Tool runs with %s: %s", "parameter_name", str(999)
)
def test_log_intensive():
"""Test logging warning for ressource intensiveness."""
with patch('logging.warning') as mock_info:
logs.log_intensive()
mock_info.assert_called_once_with("Tool is resource intensive")
@@ -0,0 +1,23 @@
"""Unit tests for the merge_series function"""
import os
import pandas as pd
from merge_series import merge_series
def test_merge_series(local_tmp_path):
"""
Test that merge_series combines the data and creates
a non empty CSV.file.
"""
input_directory = "tests/data/sunshine_duration"
output_path = local_tmp_path / "merged.csv"
expected_path = "tests/data/sunshine_duration.csv"
merge_series(input_directory, output_path)
expected_data = pd.read_csv(expected_path)
output_data = pd.read_csv(output_path)
assert os.path.exists(output_path)
assert os.path.getsize(output_path) > 0
pd.testing.assert_frame_equal(expected_data, output_data)
@@ -0,0 +1,40 @@
'''
Unit tests for the plot_change function.
These tests use mocking to isolate the function from file I/O,
logging, and plotting. They verify that the function behaves
correctly for both monthly and yearly data formats.
'''
from unittest import mock
import pandas as pd
import matplotlib
import plot_change
matplotlib.use("Agg")
@mock.patch("matplotlib.pyplot.savefig")
@mock.patch("plot_change.utils.read_df")
@mock.patch("plot_change.checks.check_empty")
@mock.patch("plot_change.checks.check_dir")
@mock.patch("plot_change.checks.check_path")
@mock.patch("plot_change.logs.log_processed")
@mock.patch("plot_change.logs.log_processing")
def test_plot_change_yearly(_mock_log_processing, _mock_log_processed,
_mock_check_path, _mock_check_dir, _mock_check_empty,
mock_read_df, mock_savefig, local_tmp_path):
"""Test yearly data case without year_filter."""
df = pd.DataFrame({
"Year": [2018, 2019, 2020],
"Electricity feed-in": [100000, 110000, 120000]
})
mock_read_df.return_value = df
input_path = "fake_input.csv"
output_path = local_tmp_path / "output_yearly.png"
title = "Yearly Plot"
plot_change.plot_change(str(input_path), str(
output_path), title, year_filter=None)
mock_read_df.assert_called_once_with(input_path, ",", 0, None)
mock_savefig.assert_called_once_with(str(output_path))
@@ -0,0 +1,17 @@
"""Unit tests for the plot_geo module."""
import os
from plot_geo import plot_geo
def test_plot_geo(local_tmp_path): # pylint: disable=redefined-outer-name
"""Test that gpkg2png generates a non-empty PNG file"""
data_path = "tests/data/solarparcs.gpkg"
base_path = "tests/data/germany.gpkg"
output_path = local_tmp_path / "output.png"
title = "Test Map"
plot_geo(data_path, base_path, output_path, title)
assert os.path.exists(output_path)
assert os.path.getsize(output_path) > 0
@@ -0,0 +1,19 @@
"""Unit tests for the polygons2area function in src.polygons2area.py"""
import os
from polygons2area import polygons2area
def test_polygons2area(local_tmp_path): # pylint: disable=redefined-outer-name
"""
Test that polygons2area computes the area of input polygons and writes the result
to a non-empty output text file.
"""
input_path = "tests/data/clip.gpkg"
output_path = local_tmp_path / "output.txt"
unit = "m"
polygons2area(input_path, output_path, unit)
assert os.path.exists(output_path)
assert os.path.getsize(output_path) > 0
@@ -0,0 +1,25 @@
"""
Unit tests for the trim module's trimming functionality.
"""
import pandas as pd
import trim
def test_trim_function(local_tmp_path):
"""Test trimming rows from a CSV file using trim.trim."""
input_csv = local_tmp_path / "input.csv"
output_csv = local_tmp_path / "output.csv"
sample_data = pd.DataFrame({
"id": [1, 2, 3, 4, 5],
"name": ["Alice", "Bob", "Charlie", "David", "Eve"],
"value": [10, 20, 30, 40, 50]
})
sample_data.to_csv(input_csv, index=False)
trim.trim(str(input_csv), str(output_csv), "1", "2")
result = pd.read_csv(output_csv)
expected = sample_data.iloc[1:-2]
pd.testing.assert_frame_equal(result.reset_index(
drop=True), expected.reset_index(drop=True))
@@ -0,0 +1,94 @@
"""Unit tests for utils.py"""
from unittest.mock import Mock
import pytest
import pandas as pd
import geopandas as gpd
import utils
def test_is_number():
"""Test if strings are correctly identified as numbers or not."""
assert utils.is_number("42") is True
assert utils.is_number("3.14") is True
assert utils.is_number("abc") is False
assert utils.is_number("") is False
def test_parse_number():
"""Test parsing numeric strings to int or float and raising errors for invalid input."""
assert utils.parse_number("42") == 42
assert utils.parse_number("3.14") == 3.14
with pytest.raises(ValueError):
utils.parse_number("not a number")
def test_request_response():
"""Test HTTP GET request and check if response has expected structure."""
response = utils.request_response("https://httpbin.org/get")
assert response.status_code == 200
assert "url" in response.json()
def test_parse_html():
"""Test parsing HTML content and extracting elements."""
mock_response = Mock()
mock_response.text = "<html><body><p>Hello</p></body></html>"
result = utils.parse_html(mock_response)
assert result.p.text == "Hello"
def test_read_file(local_tmp_path):
"""Test reading content from a file."""
test_path = local_tmp_path / "test.txt"
test_path.write_text("test content")
content = utils.read_file(test_path)
assert content == "test content"
def test_save_file(local_tmp_path):
"""Test saving content to a file."""
test_path = local_tmp_path / "output.txt"
utils.save_file("saved content", test_path)
assert test_path.read_text() == "saved content"
def test_read_df(local_tmp_path):
"""Test reading a CSV file into a pandas DataFrame."""
csv_path = local_tmp_path / "test.csv"
csv_path.write_text("a,b\n1,2\n3,4")
df = utils.read_df(csv_path, seperator=",", header_line=0, icol=None)
assert not df.empty
def test_save_df(local_tmp_path):
"""Test saving a pandas DataFrame to a CSV file."""
df = pd.DataFrame({"x": [1, 2], "y": [3, 4]})
path = local_tmp_path / "output.csv"
utils.save_df(df, path)
loaded = pd.read_csv(path, index_col=0)
pd.testing.assert_frame_equal(df, loaded)
def test_read_gdf(local_tmp_path):
"""Test reading a GeoDataFrame from a GeoPackage file."""
gdf = gpd.GeoDataFrame(
{'col': [1]}, geometry=gpd.points_from_xy([0], [0]), crs="EPSG:4326")
path = local_tmp_path / "test.gpkg"
gdf.to_file(path, driver="GPKG")
result = utils.read_gdf(path)
assert isinstance(result, gpd.GeoDataFrame)
assert result.equals(gdf)
def test_save_gdf(local_tmp_path):
"""Test saving a GeoDataFrame to a GeoPackage file."""
gdf = gpd.GeoDataFrame(
{'col': [1]}, geometry=gpd.points_from_xy([0], [0]), crs="EPSG:4326")
path = local_tmp_path / "output.gpkg"
utils.save_gdf(gdf, path)
loaded = gpd.read_file(path)
assert isinstance(loaded, gpd.GeoDataFrame)
assert loaded.equals(gdf)
@@ -0,0 +1,97 @@
"""Unit test suite for the `wgetdir` module."""
from urllib.parse import urljoin
from unittest import mock
from bs4 import BeautifulSoup
import wgetdir
def test_parse_urls():
"""Test that `parse_urls` correctly extracts valid file URLs from various HTML inputs."""
html = '''
<html>
<body>
<a href="https://example.com/file1.txt">File 1</a>
<a href="https://example.com/folder/">Folder</a>
<a href="/local/path/">Local Path</a>
<a href="../">Parent</a>
<a href="/">Root</a>
<a href="https://example.com/file2.pdf">File 2</a>
</body>
</html>
'''
soup = BeautifulSoup(html, 'html.parser')
soup_empty = BeautifulSoup("", 'html.parser')
html_missing = '''
<html><body>
<a>Missing href</a>
<a href="">Empty href</a>
</body></html>
'''
soup_missing = BeautifulSoup(html_missing, 'html.parser')
html_wrong_tag = '''
<html><body>
<div href="https://shouldnot.be.included">Wrong tag</div>
</body></html>
'''
soup_wrong_tag = BeautifulSoup(html_wrong_tag, 'html.parser')
result1 = wgetdir.parse_urls(soup)
result2 = wgetdir.parse_urls(soup_empty)
result3 = wgetdir.parse_urls(soup_missing)
result4 = wgetdir.parse_urls(soup_wrong_tag)
assert result1 == [
"https://example.com/file1.txt",
"https://example.com/file2.pdf"
]
assert not result2
assert result3 == [""]
assert not result4
def test_wget(local_tmp_path):
"""Test that `wget` downloads a file from a URL."""
url = "https://example.com/data.txt"
expected_file = local_tmp_path / "data.txt"
fake_content = "This is fake downloaded content."
with mock.patch("wgetdir.checks.check_empty") as _, \
mock.patch("wgetdir.checks.check_dir") as _, \
mock.patch("wgetdir.utils.request_response") as mock_request:
mock_response = mock.Mock()
mock_response.text = fake_content
mock_request.return_value = mock_response
wgetdir.wget(url, str(local_tmp_path))
assert expected_file.exists(), "File was not created"
with expected_file.open("r") as f:
content = f.read()
assert content == fake_content, "File content does not match"
def test_wgetdir(local_tmp_path):
"""Test that `wgetdir` processes URL by retrieving all contained files."""
directory_url = "https://example.com/data/"
urls = ["file1.txt", "file2.pdf"]
full_urls = [urljoin(directory_url, u) for u in urls]
with mock.patch("wgetdir.logs.log_processing"), \
mock.patch("wgetdir.logs.log_processed"), \
mock.patch("wgetdir.checks.check_empty"), \
mock.patch("wgetdir.checks.check_dir"), \
mock.patch("wgetdir.utils.request_response") as mock_response, \
mock.patch("wgetdir.utils.parse_html") as _, \
mock.patch("wgetdir.parse_urls") as mock_parse_urls, \
mock.patch("wgetdir.wget") as mock_wget:
mock_parse_urls.return_value = urls
mock_response.return_value = mock.Mock()
wgetdir.wgetdir(directory_url, str(local_tmp_path))
calls = [mock.call(url, str(local_tmp_path)) for url in full_urls]
mock_wget.assert_has_calls(calls, any_order=False)