Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,17 @@ jobs:
if [[ 'default,maximal' =~ ${{ matrix.option }} ]]
then
sudo apt-get install -y libgdal-dev libfcgi-dev libpixman-1-dev
sudo apt-get install -y gdal-bin libxml2-utils
sudo apt-get install -y gdal-bin libxml2-utils python3-pip python3-gdal python3-pytest
fi
if [[ 'maximal' =~ ${{ matrix.option }} ]]
then
sudo apt-get install -y libhiredis-dev libdb-dev libmapserver-dev libpcre2-dev
fi

- name: Install python dependencies
run: |
pip install -r ${{ github.workspace }}/tests/mcpython/requirements.txt

- name: Build MapCache
run: |
if [[ 'minimal' == ${{ matrix.option }} ]]
Expand Down Expand Up @@ -80,3 +84,13 @@ jobs:
else
echo No test performed on this target
fi

- name: Run python tests
run: |
if [[ 'ubuntu-latest' == ${{ matrix.os }} ]] \
&& [[ 'default' == ${{ matrix.option }} ]]
then
pytest ${{ github.workspace }}/tests/mcpython/
else
echo No python test performed on this target
fi
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ nbproject/
/build/
/build_vagrant/
/.vagrant/
__pycache__
72 changes: 72 additions & 0 deletions tests/data/mapcache_backend_template.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<mapcache>
<source name="synthetic-source" type="gdal">
<resample>NEAREST</resample>
<data>SYNTHETIC_GEOTIFF_PATH_PLACEHOLDER</data>
</source>
<grid name="synthetic_grid">
<extent>-500000 -500000 500000 500000</extent>
<srs>EPSG:3857</srs>
<units>m</units>
<origin>top-left</origin>
<size>256 256</size>
<resolutions>
1000
500
250
125
62.5
31.25
15.625
7.8125
3.90625
1.953125
0.9765625
0.48828125
0.244140625
0.1220703125
0.06103515625
0.030517578125
0.0152587890625
0.00762939453125
</resolutions>
</grid>
<cache name="disk" type="disk">
<base>TILE_CACHE_BASE_DIR/disk</base>
</cache>
<tileset name="disk-tileset">
<cache>disk</cache>
<source>synthetic-source</source>
<grid>synthetic_grid</grid>
<format>PNG</format> <!-- Using PNG for lossless data for correctness tests -->
<resample>NEAREST</resample>
<metatile>1 1</metatile>
</tileset>
<!-- Required utils have not landed yet
<cache name="lmdb" type="lmdb">
<base>TILE_CACHE_BASE_DIR/lmdb</base>
</cache>
<tileset name="lmdb-tileset">
<cache>lmdb</cache>
<source>synthetic-source</source>
<grid>synthetic_grid</grid>
<format>PNG</format>
<resample>NEAREST</resample>
<metatile>1 1</metatile>
</tileset>
-->
<cache name="sqlite" type="sqlite3">
<dbfile>TILE_CACHE_BASE_DIR/cache.sqlite</dbfile>
</cache>
<tileset name="sqlite-tileset">
<cache>sqlite</cache>
<source>synthetic-source</source>
<grid>synthetic_grid</grid>
<format>PNG</format>
<resample>NEAREST</resample>
<metatile>1 1</metatile>
</tileset>
<service type="wmts" enabled="true"/>
<service type="wms" enabled="true"/>
<log_level>debug</log_level>
</mapcache>
98 changes: 98 additions & 0 deletions tests/mcpython/generate_synthetic_geotiff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Project: MapCache
# Purpose: Generates a GeoTIFF with a predictable content to serve as a reference
# Author: Maris Nartiss
#
# *****************************************************************************
# Copyright (c) 2025 Regents of the University of Minnesota.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies of this Software or works derived from this Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
# ****************************************************************************/

import numpy as np
import logging

from osgeo import gdal, osr


def generate_synthetic_geotiff(
output_filename="synthetic_test_data.tif", width=256, height=256
):
"""
Generates a synthetic GeoTIFF with unique pixel values based on their coordinates.
Each pixel value encodes its row and column index, allowing for detection of
shift and rotation errors.
"""
osr.DontUseExceptions()
# Define image properties
wm_min_x = -500000
wm_max_x = 500000
wm_min_y = -500000
wm_max_y = 500000

pixel_width = (wm_max_x - wm_min_x) / width
pixel_height = (wm_min_y - wm_max_y) / height # Negative for north-up image

# GeoTransform: [top-left x, pixel width, 0, top-left y, 0, pixel height]
# Top-left corner is (wm_min_x, wm_max_y)
geotransform = [wm_min_x, pixel_width, 0, wm_max_y, 0, pixel_height]

# Spatial Reference System (Web Mercator)
srs = osr.SpatialReference()
srs.ImportFromEPSG(3857)

# Determine appropriate data type based on image dimensions
# For 3-band output, we'll use uint8 for each band.
gdal_datatype = gdal.GDT_Byte
numpy_datatype = np.uint8
num_bands = 3

# Create the GeoTIFF file
driver = gdal.GetDriverByName("GTiff")
dataset = driver.Create(output_filename, width, height, num_bands, gdal_datatype)

if dataset is None:
logging.error(f"Error: Could not create {output_filename}")
return

dataset.SetGeoTransform(geotransform)
dataset.SetSpatialRef(srs)

# Create NumPy arrays to hold the pixel data for each band
data_band1 = np.zeros((height, width), dtype=numpy_datatype)
data_band2 = np.zeros((height, width), dtype=numpy_datatype)
data_band3 = np.zeros((height, width), dtype=numpy_datatype)

# Generate unique pixel values based on row and column index for each band
# Band 1 (Red): row % 256
# Band 2 (Green): col % 256
# Band 3 (Blue): (row + col) % 256
for row in range(height):
for col in range(width):
data_band1[row, col] = row % 256
data_band2[row, col] = col % 256
data_band3[row, col] = (row + col) % 256

# Write the data to each band
dataset.GetRasterBand(1).WriteArray(data_band1)
dataset.GetRasterBand(2).WriteArray(data_band2)
dataset.GetRasterBand(3).WriteArray(data_band3)

# Close the dataset
dataset = None
logging.info(f"Successfully created synthetic GeoTIFF: {output_filename}")
2 changes: 2 additions & 0 deletions tests/mcpython/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
numpy
pytest
182 changes: 182 additions & 0 deletions tests/mcpython/test_disk_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Project: MapCache
# Purpose: Test MapCache disk based storage backend
# Author: Maris Nartiss
#
# *****************************************************************************
# Copyright (c) 2025 Regents of the University of Minnesota.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies of this Software or works derived from this Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
# ****************************************************************************/

import os
import pytest
import numpy as np
import logging

from osgeo import gdal

# Import the GeoTIFF generation function
from generate_synthetic_geotiff import generate_synthetic_geotiff

# Import generic verification functions and constants
from verification_core import (
TILE_SIZE,
TILE_CACHE_BASE_DIR,
TEMP_MAPCACHE_CONFIG_DIR,
calculate_expected_tile_data,
compare_tile_arrays,
cleanup,
run_seeder,
create_temp_mapcache_config,
)

# --- Configuration --- #
SYNTHETIC_GEOTIFF_FILENAME = os.path.join(
TEMP_MAPCACHE_CONFIG_DIR, "synthetic_test_data.tif"
)
GEOTIFF_WIDTH = 512
GEOTIFF_HEIGHT = 512
MAPCACHE_TEMPLATE_CONFIG = os.path.join(
os.path.dirname(__file__), "..", "data", "mapcache_backend_template.xml"
)

# --- Grid Parameters --- #
INITIAL_RESOLUTION = 1000
ORIGIN_X = -500000
ORIGIN_Y = 500000


def read_tile(tile_path, tile_size=TILE_SIZE):
if not os.path.exists(tile_path):
logging.error(f"Error: Actual tile not found at {tile_path}")
return None

actual_ds = gdal.Open(tile_path, gdal.GA_ReadOnly)
if actual_ds is None:
logging.error(f"Error: Could not open actual tile {tile_path}")
return None

# Read all bands from the actual tile
actual_tile_data = np.zeros(
(tile_size, tile_size, actual_ds.RasterCount), dtype=np.uint8
)
for i in range(actual_ds.RasterCount):
actual_tile_data[:, :, i] = actual_ds.GetRasterBand(i + 1).ReadAsArray()

actual_ds = None # Close the dataset

# Mapcache might output 4 bands (RGBA) even if source is 3 bands. Handle this.
# If actual_tile_data has 4 bands, ignore the alpha band for comparison.
if actual_tile_data.shape[2] == 4:
actual_tile_data_rgb = actual_tile_data[:, :, :3] # Take only RGB bands
elif actual_tile_data.shape[2] == 3:
actual_tile_data_rgb = actual_tile_data
else:
logging.error(
f"Error: Unexpected number of bands in actual tile: {actual_tile_data.shape[2]}"
)
return None

return actual_tile_data_rgb


def run_mapcache_test(zoom, x, y, geotiff_path, initial_resolution, origin_x, origin_y):
logging.info(f"Running MapCache test for tile Z{zoom}-X{x}-Y{y}...")

# Calculate expected tile data using generic function
expected_tile_data = calculate_expected_tile_data(
zoom,
x,
y,
geotiff_path,
initial_resolution,
origin_x,
origin_y,
)
if expected_tile_data is None:
return False

# --- Read Actual Tile Data ---
actual_tile_path = os.path.join(
TILE_CACHE_BASE_DIR,
"disk",
"disk-tileset",
"synthetic_grid",
f"{zoom:02d}",
f"{x // 1000000:03d}",
f"{(x // 1000) % 1000:03d}",
f"{x % 1000:03d}",
f"{y // 1000000:03d}",
f"{(y // 1000) % 1000:03d}",
f"{y % 1000:03d}.png",
)

logging.info(f"Reading tile {actual_tile_path}")
actual_tile_data_rgb = read_tile(actual_tile_path, TILE_SIZE)
if actual_tile_data_rgb is None:
return False

# --- Compare ---
return compare_tile_arrays(expected_tile_data, actual_tile_data_rgb, zoom, x, y)


@pytest.fixture(scope="module")
def setup_test_environment(request):
cleanup()
logging.info("Testing disk storage backend...")
os.makedirs(TEMP_MAPCACHE_CONFIG_DIR, exist_ok=True)
generate_synthetic_geotiff(
output_filename=SYNTHETIC_GEOTIFF_FILENAME,
width=GEOTIFF_WIDTH,
height=GEOTIFF_HEIGHT,
)
create_temp_mapcache_config(
SYNTHETIC_GEOTIFF_FILENAME,
MAPCACHE_TEMPLATE_CONFIG,
)
run_seeder("disk-tileset", "0,1")

def teardown():
cleanup()
logging.info("Cleanup complete.")

request.addfinalizer(teardown)


def test_disk_tiles(setup_test_environment):
ok0 = run_mapcache_test(
0,
0,
0,
SYNTHETIC_GEOTIFF_FILENAME,
INITIAL_RESOLUTION,
ORIGIN_X,
ORIGIN_Y,
)
ok1 = run_mapcache_test(
1,
1,
2,
SYNTHETIC_GEOTIFF_FILENAME,
INITIAL_RESOLUTION,
ORIGIN_X,
ORIGIN_Y,
)
assert ok0
assert ok1
Loading