Files
negative-converter/tests/test_pipeline.cpp
Christoph K. 65b411b23d 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>
2026-03-14 09:28:32 +01:00

177 lines
6.3 KiB
C++

#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);
}