chore: initial project scaffold from architecture design
- Add CLAUDE.md with project overview, tech stack, build commands, architecture description, coding standards, and sample images section - Add full directory structure: src/, docs/, tests/, import/ - Add CMakeLists.txt with C++20, OpenCV/LibRaw/Qt6 dependencies, converter_core static lib, optional GUI, and GTest tests - Add architecture documentation: ARCHITECTURE.md, PIPELINE.md, MODULES.md - Add source skeletons for all pipeline stages: RawLoader, Preprocessor, NegativeDetector, Inverter, ColorCorrector, CropProcessor, OutputWriter, Pipeline, MainWindow, CliRunner, main.cpp - Add initial test stubs for pipeline and rawloader - Add sample ARW files in import/ for integration testing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
47
tests/CMakeLists.txt
Normal file
47
tests/CMakeLists.txt
Normal file
@@ -0,0 +1,47 @@
|
||||
find_package(GTest REQUIRED)
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Pipeline unit tests
|
||||
# ──────────────────────────────────────────────
|
||||
add_executable(test_pipeline
|
||||
test_pipeline.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_pipeline PRIVATE
|
||||
converter_core
|
||||
GTest::gtest
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
target_include_directories(test_pipeline PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
)
|
||||
|
||||
add_test(NAME PipelineTests COMMAND test_pipeline)
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# RawLoader integration tests
|
||||
# ──────────────────────────────────────────────
|
||||
add_executable(test_rawloader
|
||||
test_rawloader.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_rawloader PRIVATE
|
||||
converter_core
|
||||
GTest::gtest
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
target_include_directories(test_rawloader PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
)
|
||||
|
||||
add_test(NAME RawLoaderTests COMMAND test_rawloader)
|
||||
|
||||
# Make test data path available
|
||||
target_compile_definitions(test_pipeline PRIVATE
|
||||
TEST_DATA_DIR="${CMAKE_SOURCE_DIR}/import"
|
||||
)
|
||||
target_compile_definitions(test_rawloader PRIVATE
|
||||
TEST_DATA_DIR="${CMAKE_SOURCE_DIR}/import"
|
||||
)
|
||||
176
tests/test_pipeline.cpp
Normal file
176
tests/test_pipeline.cpp
Normal file
@@ -0,0 +1,176 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "converter/pipeline/Pipeline.h"
|
||||
#include "converter/pipeline/ImageData.h"
|
||||
#include "converter/pipeline/Error.h"
|
||||
#include "converter/preprocess/Preprocessor.h"
|
||||
#include "converter/negative/NegativeDetector.h"
|
||||
#include "converter/invert/Inverter.h"
|
||||
#include "converter/color/ColorCorrector.h"
|
||||
#include "converter/crop/CropProcessor.h"
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
using namespace photoconv;
|
||||
|
||||
/**
|
||||
* @brief Create a synthetic 16-bit test image.
|
||||
*
|
||||
* @param width Image width.
|
||||
* @param height Image height.
|
||||
* @param value Fill value for all channels (0-65535).
|
||||
* @return CV_16UC3 Mat filled with the given value.
|
||||
*/
|
||||
static ImageData make_test_image(int width, int height, uint16_t value) {
|
||||
ImageData data;
|
||||
data.rgb = cv::Mat(height, width, CV_16UC3, cv::Scalar(value, value, value));
|
||||
data.source_path = "test_synthetic.png";
|
||||
data.metadata.camera_make = "Test";
|
||||
data.metadata.camera_model = "Synthetic";
|
||||
return data;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Pipeline orchestration tests
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
TEST(PipelineTest, EmptyPipelinePassesThrough) {
|
||||
Pipeline pipeline;
|
||||
auto data = make_test_image(100, 100, 32768);
|
||||
|
||||
auto result = pipeline.execute(std::move(data));
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(result->rgb.cols, 100);
|
||||
EXPECT_EQ(result->rgb.rows, 100);
|
||||
}
|
||||
|
||||
TEST(PipelineTest, StageCountIsCorrect) {
|
||||
Pipeline pipeline;
|
||||
EXPECT_EQ(pipeline.stage_count(), 0);
|
||||
|
||||
pipeline.add_stage(std::make_unique<Preprocessor>());
|
||||
EXPECT_EQ(pipeline.stage_count(), 1);
|
||||
|
||||
pipeline.add_stage(std::make_unique<NegativeDetector>());
|
||||
EXPECT_EQ(pipeline.stage_count(), 2);
|
||||
}
|
||||
|
||||
TEST(PipelineTest, FullPipelineRunsWithoutError) {
|
||||
Pipeline pipeline;
|
||||
pipeline.add_stage(std::make_unique<Preprocessor>());
|
||||
pipeline.add_stage(std::make_unique<NegativeDetector>());
|
||||
pipeline.add_stage(std::make_unique<Inverter>());
|
||||
pipeline.add_stage(std::make_unique<ColorCorrector>());
|
||||
pipeline.add_stage(std::make_unique<CropProcessor>());
|
||||
|
||||
auto data = make_test_image(200, 200, 40000);
|
||||
auto result = pipeline.execute(std::move(data));
|
||||
ASSERT_TRUE(result.has_value());
|
||||
}
|
||||
|
||||
TEST(PipelineTest, ProgressCallbackIsCalled) {
|
||||
Pipeline pipeline;
|
||||
pipeline.add_stage(std::make_unique<Preprocessor>());
|
||||
pipeline.add_stage(std::make_unique<NegativeDetector>());
|
||||
|
||||
int callback_count = 0;
|
||||
auto data = make_test_image(100, 100, 32768);
|
||||
auto result = pipeline.execute(std::move(data),
|
||||
[&callback_count](const std::string&, float) {
|
||||
++callback_count;
|
||||
});
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
// 2 stage callbacks + 1 "done" callback = 3
|
||||
EXPECT_EQ(callback_count, 3);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Preprocessor tests
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
TEST(PreprocessorTest, AcceptsValidImage) {
|
||||
Preprocessor stage;
|
||||
auto data = make_test_image(100, 100, 32768);
|
||||
|
||||
auto result = stage.process(std::move(data));
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(result->rgb.type(), CV_16UC3);
|
||||
}
|
||||
|
||||
TEST(PreprocessorTest, RejectsEmptyImage) {
|
||||
Preprocessor stage;
|
||||
ImageData data;
|
||||
|
||||
auto result = stage.process(std::move(data));
|
||||
ASSERT_FALSE(result.has_value());
|
||||
EXPECT_EQ(result.error().code, ErrorCode::InvalidBitDepth);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// NegativeDetector tests
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
TEST(NegativeDetectorTest, DetectsBrightImageAsNegative) {
|
||||
NegativeDetector stage;
|
||||
// High values = likely negative (inverted)
|
||||
auto data = make_test_image(100, 100, 50000);
|
||||
|
||||
auto result = stage.process(std::move(data));
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_NE(result->film_type, FilmType::Unknown);
|
||||
}
|
||||
|
||||
TEST(NegativeDetectorTest, DetectsDarkImageAsPositive) {
|
||||
NegativeDetector stage;
|
||||
// Low values = likely positive
|
||||
auto data = make_test_image(100, 100, 10000);
|
||||
|
||||
auto result = stage.process(std::move(data));
|
||||
ASSERT_TRUE(result.has_value());
|
||||
// Should be classified as positive (below midpoint)
|
||||
EXPECT_TRUE(result->film_type == FilmType::ColorPositive ||
|
||||
result->film_type == FilmType::BWPositive);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Inverter tests
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
TEST(InverterTest, InvertsNegative) {
|
||||
Inverter stage;
|
||||
auto data = make_test_image(10, 10, 60000);
|
||||
data.film_type = FilmType::ColorNegative;
|
||||
|
||||
auto result = stage.process(std::move(data));
|
||||
ASSERT_TRUE(result.has_value());
|
||||
|
||||
// After inversion, values should be near 65535 - 60000 = 5535
|
||||
cv::Scalar mean = cv::mean(result->rgb);
|
||||
EXPECT_LT(mean[0], 10000);
|
||||
}
|
||||
|
||||
TEST(InverterTest, SkipsPositive) {
|
||||
Inverter stage;
|
||||
auto data = make_test_image(10, 10, 30000);
|
||||
data.film_type = FilmType::ColorPositive;
|
||||
|
||||
auto result = stage.process(std::move(data));
|
||||
ASSERT_TRUE(result.has_value());
|
||||
|
||||
// Should be unchanged
|
||||
cv::Scalar mean = cv::mean(result->rgb);
|
||||
EXPECT_NEAR(mean[0], 30000.0, 1.0);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Error type tests
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
TEST(ErrorTest, FormatIncludesAllInfo) {
|
||||
auto err = make_error(ErrorCode::FileNotFound, "test.arw not found");
|
||||
auto formatted = err.format();
|
||||
|
||||
EXPECT_NE(formatted.find("test.arw not found"), std::string::npos);
|
||||
EXPECT_NE(formatted.find("test_pipeline.cpp"), std::string::npos);
|
||||
}
|
||||
102
tests/test_rawloader.cpp
Normal file
102
tests/test_rawloader.cpp
Normal file
@@ -0,0 +1,102 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "converter/rawloader/RawLoader.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
using namespace photoconv;
|
||||
|
||||
#ifndef TEST_DATA_DIR
|
||||
#define TEST_DATA_DIR "import"
|
||||
#endif
|
||||
|
||||
static const std::filesystem::path kTestDataDir{TEST_DATA_DIR};
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// File validation tests
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
TEST(RawLoaderTest, RejectsNonexistentFile) {
|
||||
RawLoader loader;
|
||||
auto result = loader.load("/nonexistent/file.arw");
|
||||
|
||||
ASSERT_FALSE(result.has_value());
|
||||
EXPECT_EQ(result.error().code, ErrorCode::FileNotFound);
|
||||
}
|
||||
|
||||
TEST(RawLoaderTest, RejectsUnsupportedFormat) {
|
||||
// Create a temporary file with unsupported extension
|
||||
auto temp = std::filesystem::temp_directory_path() / "test.xyz";
|
||||
{
|
||||
std::ofstream f(temp);
|
||||
f << "dummy";
|
||||
}
|
||||
|
||||
RawLoader loader;
|
||||
auto result = loader.load(temp);
|
||||
|
||||
ASSERT_FALSE(result.has_value());
|
||||
EXPECT_EQ(result.error().code, ErrorCode::UnsupportedFormat);
|
||||
|
||||
std::filesystem::remove(temp);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// RAW loading integration tests (require test data)
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
TEST(RawLoaderTest, LoadsArwFile) {
|
||||
auto arw_path = kTestDataDir / "DSC09246.ARW";
|
||||
if (!std::filesystem::exists(arw_path)) {
|
||||
GTEST_SKIP() << "Test data not available: " << arw_path;
|
||||
}
|
||||
|
||||
RawLoader loader;
|
||||
auto result = loader.load(arw_path);
|
||||
|
||||
ASSERT_TRUE(result.has_value()) << result.error().format();
|
||||
|
||||
// Verify 16-bit BGR output
|
||||
EXPECT_EQ(result->rgb.type(), CV_16UC3);
|
||||
EXPECT_GT(result->rgb.cols, 0);
|
||||
EXPECT_GT(result->rgb.rows, 0);
|
||||
|
||||
// Verify metadata was populated
|
||||
EXPECT_FALSE(result->metadata.camera_make.empty());
|
||||
EXPECT_GT(result->metadata.raw_width, 0);
|
||||
EXPECT_GT(result->metadata.raw_height, 0);
|
||||
}
|
||||
|
||||
TEST(RawLoaderTest, MetadataContainsSonyMake) {
|
||||
auto arw_path = kTestDataDir / "DSC09246.ARW";
|
||||
if (!std::filesystem::exists(arw_path)) {
|
||||
GTEST_SKIP() << "Test data not available: " << arw_path;
|
||||
}
|
||||
|
||||
RawLoader loader;
|
||||
auto result = loader.load(arw_path);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
|
||||
// Sony ARW files should have "Sony" as make
|
||||
EXPECT_EQ(result->metadata.camera_make, "Sony");
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Pixel integrity tests
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
TEST(RawLoaderTest, OutputIsNonTrivial) {
|
||||
auto arw_path = kTestDataDir / "DSC09246.ARW";
|
||||
if (!std::filesystem::exists(arw_path)) {
|
||||
GTEST_SKIP() << "Test data not available: " << arw_path;
|
||||
}
|
||||
|
||||
RawLoader loader;
|
||||
auto result = loader.load(arw_path);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
|
||||
// Image should have non-zero content (not all black or all white)
|
||||
cv::Scalar mean_val = cv::mean(result->rgb);
|
||||
EXPECT_GT(mean_val[0], 100.0); // Not all black
|
||||
EXPECT_LT(mean_val[0], 65000.0); // Not all white
|
||||
}
|
||||
Reference in New Issue
Block a user