Skip to content

Commit a49c1a9

Browse files
authored
Added basic python test for ImageConverter bindings (AcademySoftwareFoundation#208)
Signed-off-by: hyi18 <[email protected]>
1 parent 82dba85 commit a49c1a9

File tree

8 files changed

+201
-34
lines changed

8 files changed

+201
-34
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.DS_Store
22
build/
33
build-coverage/
4-
tests/materials/*.exr
4+
tests/materials/*.exr
5+
__pycache__/

build_scripts/install_deps_linux.bash

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ time sudo apt-get -q -f install -y \
1010
libopencv-dev \
1111
openimageio-tools libopenimageio-dev \
1212
nanobind-dev
13+
14+
pip3 install pytest

build_scripts/install_deps_mac.bash

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
set -ex
44

55
brew install ceres-solver nlohmann-json openimageio nanobind robin-map
6+
7+
python3 -m pip install --break-system-packages pytest

build_scripts/install_deps_windows.bash

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,9 @@ vcpkg x-update-baseline \
88
vcpkg install \
99
--x-install-root="C:/vcpkg/installed" \
1010
--x-manifest-root="./build_scripts"
11+
12+
# Install pip and pytest to the vcpkg Python
13+
# Since vcpkg Python doesn't include pip, install it first using ensurepip
14+
VCPKG_PYTHON="C:/vcpkg/installed/x64-windows/tools/python3/python.exe"
15+
"$VCPKG_PYTHON" -m ensurepip --upgrade
16+
"$VCPKG_PYTHON" -m pip install pytest

build_scripts/install_deps_yum.bash

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
set -ex
44

55
sudo yum install --setopt=tsflags=nodocs -y eigen3-devel ceres-solver-devel json-devel
6-
7-
sudo python -m pip install nanobind
6+
7+
sudo python -m pip install nanobind pytest

src/rawtoaces_util/image_converter.cpp

Lines changed: 79 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1743,53 +1743,67 @@ bool ImageConverter::apply_crop(
17431743
bool ImageConverter::make_output_path(
17441744
std::string &path, const std::string &suffix )
17451745
{
1746-
std::filesystem::path temp_path( path );
1746+
// Validate input path
1747+
if ( path.empty() )
1748+
{
1749+
std::cerr << "ERROR: Empty input path provided." << std::endl;
1750+
return false;
1751+
}
1752+
try
1753+
{
1754+
std::filesystem::path temp_path( path );
17471755

1748-
temp_path.replace_extension();
1749-
temp_path += suffix + ".exr";
1756+
temp_path.replace_extension();
1757+
temp_path += suffix + ".exr";
17501758

1751-
if ( !settings.output_dir.empty() )
1752-
{
1753-
auto new_directory = std::filesystem::path( settings.output_dir );
1759+
if ( !settings.output_dir.empty() )
1760+
{
1761+
auto new_directory = std::filesystem::path( settings.output_dir );
17541762

1755-
auto filename = temp_path.filename();
1756-
auto old_directory = temp_path.remove_filename();
1763+
auto filename = temp_path.filename();
1764+
auto old_directory = temp_path.remove_filename();
17571765

1758-
new_directory = old_directory / new_directory;
1766+
new_directory = old_directory / new_directory;
17591767

1760-
if ( !std::filesystem::exists( new_directory ) )
1761-
{
1762-
if ( settings.create_dirs )
1768+
if ( !std::filesystem::exists( new_directory ) )
17631769
{
1764-
if ( !std::filesystem::create_directory( new_directory ) )
1770+
if ( settings.create_dirs )
1771+
{
1772+
if ( !std::filesystem::create_directory( new_directory ) )
1773+
{
1774+
std::cerr << "ERROR: Failed to create directory "
1775+
<< new_directory << "." << std::endl;
1776+
return false;
1777+
}
1778+
}
1779+
else
17651780
{
1766-
std::cerr << "ERROR: Failed to create directory "
1767-
<< new_directory << "." << std::endl;
1781+
std::cerr << "ERROR: The output directory " << new_directory
1782+
<< " does not exist." << std::endl;
17681783
return false;
17691784
}
17701785
}
1771-
else
1772-
{
1773-
std::cerr << "ERROR: The output directory " << new_directory
1774-
<< " does not exist." << std::endl;
1775-
return false;
1776-
}
1786+
temp_path = std::filesystem::absolute( new_directory / filename );
17771787
}
17781788

1779-
temp_path = std::filesystem::absolute( new_directory / filename );
1780-
}
1789+
if ( !settings.overwrite && std::filesystem::exists( temp_path ) )
1790+
{
1791+
std::cerr
1792+
<< "ERROR: file " << temp_path << " already exists. Use "
1793+
<< "--overwrite to allow overwriting existing files. Skipping "
1794+
<< "this file." << std::endl;
1795+
return false;
1796+
}
17811797

1782-
if ( !settings.overwrite && std::filesystem::exists( temp_path ) )
1798+
path = temp_path.string();
1799+
return true;
1800+
}
1801+
catch ( const std::exception &e )
17831802
{
1784-
std::cerr
1785-
<< "ERROR: file " << temp_path << " already exists. Use "
1786-
<< "--overwrite to allow overwriting existing files. Skipping "
1787-
<< "this file." << std::endl;
1803+
std::cerr << "ERROR: Invalid path format '" << path << "': " << e.what()
1804+
<< std::endl;
17881805
return false;
17891806
}
1790-
1791-
path = temp_path.string();
1792-
return true;
17931807
}
17941808

17951809
bool ImageConverter::save_image(
@@ -1823,6 +1837,40 @@ bool ImageConverter::save_image(
18231837

18241838
bool ImageConverter::process_image( const std::string &input_filename )
18251839
{
1840+
// Early validation: check if input file exists and is valid
1841+
if ( input_filename.empty() )
1842+
{
1843+
if ( settings.verbosity > 0 )
1844+
{
1845+
std::cerr << "ERROR: Empty input filename provided." << std::endl;
1846+
}
1847+
return false;
1848+
}
1849+
1850+
// Validate input file exists
1851+
// Wrap in try-catch to handle filesystem exceptions on Windows
1852+
try
1853+
{
1854+
if ( !std::filesystem::exists( input_filename ) )
1855+
{
1856+
if ( settings.verbosity > 0 )
1857+
{
1858+
std::cerr << "ERROR: Input file does not exist: "
1859+
<< input_filename << std::endl;
1860+
}
1861+
return false;
1862+
}
1863+
}
1864+
catch ( const std::filesystem::filesystem_error &e )
1865+
{
1866+
if ( settings.verbosity > 0 )
1867+
{
1868+
std::cerr << "ERROR: Filesystem error while checking input file '"
1869+
<< input_filename << "': " << e.what() << std::endl;
1870+
}
1871+
return false;
1872+
}
1873+
18261874
std::string output_filename = input_filename;
18271875
if ( !make_output_path( output_filename ) )
18281876
{

tests/CMakeLists.txt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,30 @@ target_link_libraries(
200200
setup_test_coverage(Test_ImageConverter)
201201
add_test ( NAME Test_ImageConverter COMMAND Test_ImageConverter )
202202

203+
################################################################################
204+
# Python tests
205+
if(RTA_BUILD_PYTHON_BINDINGS)
206+
if(Python_EXECUTABLE)
207+
add_test(
208+
NAME Test_Python_ImageConverter
209+
COMMAND ${Python_EXECUTABLE} -m pytest ${CMAKE_CURRENT_SOURCE_DIR}/python/test_image_converter.py -v
210+
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
211+
)
212+
213+
# Set environment for Python test to find the compiled module
214+
# Handle both single-config (Linux/macOS) and multi-config (Windows) generators
215+
if(WIN32)
216+
set_tests_properties(Test_Python_ImageConverter PROPERTIES
217+
ENVIRONMENT "PYTHONPATH=${CMAKE_BINARY_DIR}/src/bindings/Release;${CMAKE_BINARY_DIR}/src/bindings;$ENV{PYTHONPATH}"
218+
)
219+
else()
220+
set_tests_properties(Test_Python_ImageConverter PROPERTIES
221+
ENVIRONMENT "PYTHONPATH=${CMAKE_BINARY_DIR}/src/bindings:$ENV{PYTHONPATH}"
222+
)
223+
endif()
224+
endif()
225+
endif()
226+
203227
################################################################################
204228
# Coverage report generation
205229
if( ENABLE_COVERAGE AND COVERAGE_SUPPORTED )
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/usr/bin/env python3
2+
# SPDX-License-Identifier: Apache-2.0
3+
# Copyright Contributors to the rawtoaces Project.
4+
5+
"""
6+
Unit tests for rawtoaces Python bindings - ImageConverter class
7+
"""
8+
9+
import pytest
10+
11+
# The PYTHONPATH should be set by CMake to find the compiled module
12+
# No need to manually add paths here as CMake handles this via environment variables
13+
14+
try:
15+
import rawtoaces
16+
except ImportError as e:
17+
pytest.skip(f"rawtoaces module not found. Build the Python bindings first: {e}", allow_module_level=True)
18+
19+
20+
class TestImageConverter:
21+
"""Test cases for the ImageConverter class"""
22+
23+
def test_converter_creation(self):
24+
"""Test that ImageConverter can be instantiated"""
25+
converter = rawtoaces.ImageConverter()
26+
assert converter is not None
27+
assert isinstance(converter, rawtoaces.ImageConverter)
28+
29+
def test_converter_has_settings(self):
30+
"""Test that ImageConverter has a settings attribute"""
31+
converter = rawtoaces.ImageConverter()
32+
assert hasattr(converter, "settings")
33+
assert converter.settings is not None
34+
35+
def test_converter_has_process_image_method(self):
36+
"""Test that ImageConverter has process_image method"""
37+
converter = rawtoaces.ImageConverter()
38+
assert hasattr(converter, "process_image")
39+
assert callable(converter.process_image)
40+
41+
def test_process_image_with_invalid_path(self):
42+
"""Test process_image with non-existent file returns False"""
43+
import os
44+
converter = rawtoaces.ImageConverter()
45+
46+
# Use a simple invalid filename (no path separators to avoid OS-specific issues)
47+
invalid_path = "nonexistent_file.txt"
48+
49+
# Test with invalid path
50+
try:
51+
result = converter.process_image(invalid_path)
52+
assert result is False
53+
except Exception:
54+
# If an exception is thrown, that's also acceptable behavior for invalid input
55+
pass
56+
57+
# Test with empty path
58+
try:
59+
result = converter.process_image("")
60+
assert result is False
61+
except Exception:
62+
# If an exception is thrown, that's also acceptable behavior for invalid input
63+
pass
64+
65+
66+
class TestSettings:
67+
"""Test cases for the ImageConverter.Settings class"""
68+
69+
def test_settings_creation(self):
70+
"""Test that Settings can be instantiated"""
71+
converter = rawtoaces.ImageConverter()
72+
settings = converter.settings
73+
assert settings is not None
74+
assert isinstance(settings, rawtoaces.ImageConverter.Settings)
75+
76+
def test_settings_direct_creation(self):
77+
"""Test that Settings can be created directly"""
78+
settings = rawtoaces.ImageConverter.Settings()
79+
assert settings is not None
80+
assert isinstance(settings, rawtoaces.ImageConverter.Settings)
81+
82+
83+
if __name__ == "__main__":
84+
pytest.main([__file__])

0 commit comments

Comments
 (0)