Skip to content

Commit 47bdb17

Browse files
authored
tests: implement pytest tests for disk and sqlite cache backends (#362)
* tests: implement pytest tests for disk and sqlite cache backends * tests: use absolute path to configuration file * tests: rename test files
1 parent 4e7c0e3 commit 47bdb17

File tree

8 files changed

+763
-1
lines changed

8 files changed

+763
-1
lines changed

.github/workflows/build-linux.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@ jobs:
2626
if [[ 'default,maximal' =~ ${{ matrix.option }} ]]
2727
then
2828
sudo apt-get install -y libgdal-dev libfcgi-dev libpixman-1-dev
29-
sudo apt-get install -y gdal-bin libxml2-utils
29+
sudo apt-get install -y gdal-bin libxml2-utils python3-pip python3-gdal python3-pytest
3030
fi
3131
if [[ 'maximal' =~ ${{ matrix.option }} ]]
3232
then
3333
sudo apt-get install -y libhiredis-dev libdb-dev libmapserver-dev libpcre2-dev
3434
fi
3535
36+
- name: Install python dependencies
37+
run: |
38+
pip install -r ${{ github.workspace }}/tests/mcpython/requirements.txt
39+
3640
- name: Build MapCache
3741
run: |
3842
if [[ 'minimal' == ${{ matrix.option }} ]]
@@ -80,3 +84,13 @@ jobs:
8084
else
8185
echo No test performed on this target
8286
fi
87+
88+
- name: Run python tests
89+
run: |
90+
if [[ 'ubuntu-latest' == ${{ matrix.os }} ]] \
91+
&& [[ 'default' == ${{ matrix.option }} ]]
92+
then
93+
pytest ${{ github.workspace }}/tests/mcpython/
94+
else
95+
echo No python test performed on this target
96+
fi

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ nbproject/
33
/build/
44
/build_vagrant/
55
/.vagrant/
6+
__pycache__
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<mapcache>
3+
<source name="synthetic-source" type="gdal">
4+
<resample>NEAREST</resample>
5+
<data>SYNTHETIC_GEOTIFF_PATH_PLACEHOLDER</data>
6+
</source>
7+
<grid name="synthetic_grid">
8+
<extent>-500000 -500000 500000 500000</extent>
9+
<srs>EPSG:3857</srs>
10+
<units>m</units>
11+
<origin>top-left</origin>
12+
<size>256 256</size>
13+
<resolutions>
14+
1000
15+
500
16+
250
17+
125
18+
62.5
19+
31.25
20+
15.625
21+
7.8125
22+
3.90625
23+
1.953125
24+
0.9765625
25+
0.48828125
26+
0.244140625
27+
0.1220703125
28+
0.06103515625
29+
0.030517578125
30+
0.0152587890625
31+
0.00762939453125
32+
</resolutions>
33+
</grid>
34+
<cache name="disk" type="disk">
35+
<base>TILE_CACHE_BASE_DIR/disk</base>
36+
</cache>
37+
<tileset name="disk-tileset">
38+
<cache>disk</cache>
39+
<source>synthetic-source</source>
40+
<grid>synthetic_grid</grid>
41+
<format>PNG</format> <!-- Using PNG for lossless data for correctness tests -->
42+
<resample>NEAREST</resample>
43+
<metatile>1 1</metatile>
44+
</tileset>
45+
<!-- Required utils have not landed yet
46+
<cache name="lmdb" type="lmdb">
47+
<base>TILE_CACHE_BASE_DIR/lmdb</base>
48+
</cache>
49+
<tileset name="lmdb-tileset">
50+
<cache>lmdb</cache>
51+
<source>synthetic-source</source>
52+
<grid>synthetic_grid</grid>
53+
<format>PNG</format>
54+
<resample>NEAREST</resample>
55+
<metatile>1 1</metatile>
56+
</tileset>
57+
-->
58+
<cache name="sqlite" type="sqlite3">
59+
<dbfile>TILE_CACHE_BASE_DIR/cache.sqlite</dbfile>
60+
</cache>
61+
<tileset name="sqlite-tileset">
62+
<cache>sqlite</cache>
63+
<source>synthetic-source</source>
64+
<grid>synthetic_grid</grid>
65+
<format>PNG</format>
66+
<resample>NEAREST</resample>
67+
<metatile>1 1</metatile>
68+
</tileset>
69+
<service type="wmts" enabled="true"/>
70+
<service type="wms" enabled="true"/>
71+
<log_level>debug</log_level>
72+
</mapcache>
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Project: MapCache
2+
# Purpose: Generates a GeoTIFF with a predictable content to serve as a reference
3+
# Author: Maris Nartiss
4+
#
5+
# *****************************************************************************
6+
# Copyright (c) 2025 Regents of the University of Minnesota.
7+
#
8+
# Permission is hereby granted, free of charge, to any person obtaining a
9+
# copy of this software and associated documentation files (the "Software"),
10+
# to deal in the Software without restriction, including without limitation
11+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
12+
# and/or sell copies of the Software, and to permit persons to whom the
13+
# Software is furnished to do so, subject to the following conditions:
14+
#
15+
# The above copyright notice and this permission notice shall be included in
16+
# all copies of this Software or works derived from this Software.
17+
#
18+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19+
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24+
# DEALINGS IN THE SOFTWARE.
25+
# ****************************************************************************/
26+
27+
import numpy as np
28+
import logging
29+
30+
from osgeo import gdal, osr
31+
32+
33+
def generate_synthetic_geotiff(
34+
output_filename="synthetic_test_data.tif", width=256, height=256
35+
):
36+
"""
37+
Generates a synthetic GeoTIFF with unique pixel values based on their coordinates.
38+
Each pixel value encodes its row and column index, allowing for detection of
39+
shift and rotation errors.
40+
"""
41+
osr.DontUseExceptions()
42+
# Define image properties
43+
wm_min_x = -500000
44+
wm_max_x = 500000
45+
wm_min_y = -500000
46+
wm_max_y = 500000
47+
48+
pixel_width = (wm_max_x - wm_min_x) / width
49+
pixel_height = (wm_min_y - wm_max_y) / height # Negative for north-up image
50+
51+
# GeoTransform: [top-left x, pixel width, 0, top-left y, 0, pixel height]
52+
# Top-left corner is (wm_min_x, wm_max_y)
53+
geotransform = [wm_min_x, pixel_width, 0, wm_max_y, 0, pixel_height]
54+
55+
# Spatial Reference System (Web Mercator)
56+
srs = osr.SpatialReference()
57+
srs.ImportFromEPSG(3857)
58+
59+
# Determine appropriate data type based on image dimensions
60+
# For 3-band output, we'll use uint8 for each band.
61+
gdal_datatype = gdal.GDT_Byte
62+
numpy_datatype = np.uint8
63+
num_bands = 3
64+
65+
# Create the GeoTIFF file
66+
driver = gdal.GetDriverByName("GTiff")
67+
dataset = driver.Create(output_filename, width, height, num_bands, gdal_datatype)
68+
69+
if dataset is None:
70+
logging.error(f"Error: Could not create {output_filename}")
71+
return
72+
73+
dataset.SetGeoTransform(geotransform)
74+
dataset.SetSpatialRef(srs)
75+
76+
# Create NumPy arrays to hold the pixel data for each band
77+
data_band1 = np.zeros((height, width), dtype=numpy_datatype)
78+
data_band2 = np.zeros((height, width), dtype=numpy_datatype)
79+
data_band3 = np.zeros((height, width), dtype=numpy_datatype)
80+
81+
# Generate unique pixel values based on row and column index for each band
82+
# Band 1 (Red): row % 256
83+
# Band 2 (Green): col % 256
84+
# Band 3 (Blue): (row + col) % 256
85+
for row in range(height):
86+
for col in range(width):
87+
data_band1[row, col] = row % 256
88+
data_band2[row, col] = col % 256
89+
data_band3[row, col] = (row + col) % 256
90+
91+
# Write the data to each band
92+
dataset.GetRasterBand(1).WriteArray(data_band1)
93+
dataset.GetRasterBand(2).WriteArray(data_band2)
94+
dataset.GetRasterBand(3).WriteArray(data_band3)
95+
96+
# Close the dataset
97+
dataset = None
98+
logging.info(f"Successfully created synthetic GeoTIFF: {output_filename}")

tests/mcpython/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
numpy
2+
pytest

tests/mcpython/test_disk_cache.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# Project: MapCache
2+
# Purpose: Test MapCache disk based storage backend
3+
# Author: Maris Nartiss
4+
#
5+
# *****************************************************************************
6+
# Copyright (c) 2025 Regents of the University of Minnesota.
7+
#
8+
# Permission is hereby granted, free of charge, to any person obtaining a
9+
# copy of this software and associated documentation files (the "Software"),
10+
# to deal in the Software without restriction, including without limitation
11+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
12+
# and/or sell copies of the Software, and to permit persons to whom the
13+
# Software is furnished to do so, subject to the following conditions:
14+
#
15+
# The above copyright notice and this permission notice shall be included in
16+
# all copies of this Software or works derived from this Software.
17+
#
18+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19+
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24+
# DEALINGS IN THE SOFTWARE.
25+
# ****************************************************************************/
26+
27+
import os
28+
import pytest
29+
import numpy as np
30+
import logging
31+
32+
from osgeo import gdal
33+
34+
# Import the GeoTIFF generation function
35+
from generate_synthetic_geotiff import generate_synthetic_geotiff
36+
37+
# Import generic verification functions and constants
38+
from verification_core import (
39+
TILE_SIZE,
40+
TILE_CACHE_BASE_DIR,
41+
TEMP_MAPCACHE_CONFIG_DIR,
42+
calculate_expected_tile_data,
43+
compare_tile_arrays,
44+
cleanup,
45+
run_seeder,
46+
create_temp_mapcache_config,
47+
)
48+
49+
# --- Configuration --- #
50+
SYNTHETIC_GEOTIFF_FILENAME = os.path.join(
51+
TEMP_MAPCACHE_CONFIG_DIR, "synthetic_test_data.tif"
52+
)
53+
GEOTIFF_WIDTH = 512
54+
GEOTIFF_HEIGHT = 512
55+
MAPCACHE_TEMPLATE_CONFIG = os.path.join(
56+
os.path.dirname(__file__), "..", "data", "mapcache_backend_template.xml"
57+
)
58+
59+
# --- Grid Parameters --- #
60+
INITIAL_RESOLUTION = 1000
61+
ORIGIN_X = -500000
62+
ORIGIN_Y = 500000
63+
64+
65+
def read_tile(tile_path, tile_size=TILE_SIZE):
66+
if not os.path.exists(tile_path):
67+
logging.error(f"Error: Actual tile not found at {tile_path}")
68+
return None
69+
70+
actual_ds = gdal.Open(tile_path, gdal.GA_ReadOnly)
71+
if actual_ds is None:
72+
logging.error(f"Error: Could not open actual tile {tile_path}")
73+
return None
74+
75+
# Read all bands from the actual tile
76+
actual_tile_data = np.zeros(
77+
(tile_size, tile_size, actual_ds.RasterCount), dtype=np.uint8
78+
)
79+
for i in range(actual_ds.RasterCount):
80+
actual_tile_data[:, :, i] = actual_ds.GetRasterBand(i + 1).ReadAsArray()
81+
82+
actual_ds = None # Close the dataset
83+
84+
# Mapcache might output 4 bands (RGBA) even if source is 3 bands. Handle this.
85+
# If actual_tile_data has 4 bands, ignore the alpha band for comparison.
86+
if actual_tile_data.shape[2] == 4:
87+
actual_tile_data_rgb = actual_tile_data[:, :, :3] # Take only RGB bands
88+
elif actual_tile_data.shape[2] == 3:
89+
actual_tile_data_rgb = actual_tile_data
90+
else:
91+
logging.error(
92+
f"Error: Unexpected number of bands in actual tile: {actual_tile_data.shape[2]}"
93+
)
94+
return None
95+
96+
return actual_tile_data_rgb
97+
98+
99+
def run_mapcache_test(zoom, x, y, geotiff_path, initial_resolution, origin_x, origin_y):
100+
logging.info(f"Running MapCache test for tile Z{zoom}-X{x}-Y{y}...")
101+
102+
# Calculate expected tile data using generic function
103+
expected_tile_data = calculate_expected_tile_data(
104+
zoom,
105+
x,
106+
y,
107+
geotiff_path,
108+
initial_resolution,
109+
origin_x,
110+
origin_y,
111+
)
112+
if expected_tile_data is None:
113+
return False
114+
115+
# --- Read Actual Tile Data ---
116+
actual_tile_path = os.path.join(
117+
TILE_CACHE_BASE_DIR,
118+
"disk",
119+
"disk-tileset",
120+
"synthetic_grid",
121+
f"{zoom:02d}",
122+
f"{x // 1000000:03d}",
123+
f"{(x // 1000) % 1000:03d}",
124+
f"{x % 1000:03d}",
125+
f"{y // 1000000:03d}",
126+
f"{(y // 1000) % 1000:03d}",
127+
f"{y % 1000:03d}.png",
128+
)
129+
130+
logging.info(f"Reading tile {actual_tile_path}")
131+
actual_tile_data_rgb = read_tile(actual_tile_path, TILE_SIZE)
132+
if actual_tile_data_rgb is None:
133+
return False
134+
135+
# --- Compare ---
136+
return compare_tile_arrays(expected_tile_data, actual_tile_data_rgb, zoom, x, y)
137+
138+
139+
@pytest.fixture(scope="module")
140+
def setup_test_environment(request):
141+
cleanup()
142+
logging.info("Testing disk storage backend...")
143+
os.makedirs(TEMP_MAPCACHE_CONFIG_DIR, exist_ok=True)
144+
generate_synthetic_geotiff(
145+
output_filename=SYNTHETIC_GEOTIFF_FILENAME,
146+
width=GEOTIFF_WIDTH,
147+
height=GEOTIFF_HEIGHT,
148+
)
149+
create_temp_mapcache_config(
150+
SYNTHETIC_GEOTIFF_FILENAME,
151+
MAPCACHE_TEMPLATE_CONFIG,
152+
)
153+
run_seeder("disk-tileset", "0,1")
154+
155+
def teardown():
156+
cleanup()
157+
logging.info("Cleanup complete.")
158+
159+
request.addfinalizer(teardown)
160+
161+
162+
def test_disk_tiles(setup_test_environment):
163+
ok0 = run_mapcache_test(
164+
0,
165+
0,
166+
0,
167+
SYNTHETIC_GEOTIFF_FILENAME,
168+
INITIAL_RESOLUTION,
169+
ORIGIN_X,
170+
ORIGIN_Y,
171+
)
172+
ok1 = run_mapcache_test(
173+
1,
174+
1,
175+
2,
176+
SYNTHETIC_GEOTIFF_FILENAME,
177+
INITIAL_RESOLUTION,
178+
ORIGIN_X,
179+
ORIGIN_Y,
180+
)
181+
assert ok0
182+
assert ok1

0 commit comments

Comments
 (0)