Files
effector/tests/core_tests.cpp
T
hansandClaude Opus 5 7e98357070 Load source images in the background with a progress window
Decoding a batch of photographs took seconds on the main thread, so the
window froze with no sign of what was happening. loadImages now takes an
optional LoadProgress (per-file callback plus a cancel flag) and hands files
out from a shared cursor, so one large file cannot stall the queue behind it.

The GUI decodes off the main thread and shows a progress window with the
count, the current file and a Cancel button; it only appears if the batch is
still running after 250 ms. Headless mode keeps a "reading n/total" line on
stderr when it is attached to a terminal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 14:39:07 +02:00

193 lines
6.9 KiB
C++

// Small self-contained checks for the pieces that have no GUI dependency.
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <iostream>
#include <mutex>
#include <string>
#include <vector>
#include "core/registry.hpp"
#include "core/rng.hpp"
#include "effects/scanlines.hpp"
namespace {
int failures = 0;
void check(bool condition, const std::string& what) {
if (condition) {
std::cout << " ok " << what << "\n";
} else {
std::cout << " FAIL " << what << "\n";
++failures;
}
}
effector::Image gradient(int width, int height, int shade) {
effector::Image image(width, height);
for (int y = 0; y < height; ++y) {
auto* row = image.row(y);
for (int x = 0; x < width; ++x) {
row[x * 3 + 0] = static_cast<std::uint8_t>((x + shade) & 0xFF);
row[x * 3 + 1] = static_cast<std::uint8_t>((y + shade) & 0xFF);
row[x * 3 + 2] = static_cast<std::uint8_t>(shade);
}
}
return image;
}
bool sameBytes(const effector::Image& a, const effector::Image& b) {
if (a.width() != b.width() || a.height() != b.height()) return false;
return std::equal(a.data(), a.data() + a.byteSize(), b.data());
}
effector::Image render(const std::vector<effector::Image>& sources, int width, int height,
std::uint64_t seed, const effector::ParamValues& values) {
effector::ScanlinesEffect effect;
effector::Rng rng(seed);
auto merged = effect.defaults();
for (const auto& [key, value] : values) merged[key] = value;
return effect.render(sources, width, height, rng, merged);
}
} // namespace
int main() {
using namespace effector;
std::cout << "rng\n";
{
Rng a(42);
Rng b(42);
bool identical = true;
for (int i = 0; i < 1000; ++i) identical = identical && a.next() == b.next();
check(identical, "same seed yields the same stream");
Rng c(7);
bool inBounds = true;
for (int i = 0; i < 10000; ++i) {
const long value = c.inRange(-4, 9);
inBounds = inBounds && value >= -4 && value <= 9;
}
check(inBounds, "inRange stays within bounds");
}
std::cout << "registry\n";
{
check(!effects().empty(), "registry is populated");
check(effectByName("scanlines")->title() == "Scanlines", "lookup by name");
bool threw = false;
try {
effectByName("nope");
} catch (const EffectError&) {
threw = true;
}
check(threw, "unknown effect throws");
}
std::cout << "scanlines\n";
{
std::vector<Image> sources = {gradient(1200, 800, 10), gradient(640, 480, 90),
gradient(300, 200, 200)};
const auto first = render(sources, 800, 600, 1, {{"strip_height", 6L}, {"jitter", 3L}});
const auto again = render(sources, 800, 600, 1, {{"strip_height", 6L}, {"jitter", 3L}});
const auto other = render(sources, 800, 600, 2, {{"strip_height", 6L}, {"jitter", 3L}});
check(first.width() == 800 && first.height() == 600, "canvas has the requested size");
check(sameBytes(first, again), "same seed reproduces the image");
check(!sameBytes(first, other), "a different seed changes the image");
const auto thin = render(sources, 400, 300, 7, {{"strip_height", 1L}});
check(thin.width() == 400 && thin.height() == 300, "strip height of 1");
// Canvas wider and taller than the only source: exercises tiling and the
// clamp of the strip height to the source height.
std::vector<Image> small = {gradient(300, 200, 5)};
const auto tiled = render(small, 1000, 700, 9, {{"strip_height", 400L}, {"jitter", 50L}});
check(tiled.width() == 1000 && tiled.height() == 700, "tiling a narrow source");
bool tiles = true;
for (int y = 0; y < tiled.height(); ++y) {
const auto* row = tiled.row(y);
for (int c = 0; c < 3; ++c) tiles = tiles && row[c] == row[300 * 3 + c];
}
check(tiles, "tiled columns repeat with the source width");
const auto ordered = render(sources, 500, 500, 3, {{"strip_height", 20L}, {"keep_order", true}});
check(ordered.height() == 500, "keep_order fills the canvas");
bool threw = false;
try {
render({}, 100, 100, 1, {});
} catch (const EffectError&) {
threw = true;
}
check(threw, "no sources throws");
}
std::cout << "image io\n";
{
const auto path = std::filesystem::temp_directory_path() / "effector_test_roundtrip.png";
const auto original = gradient(64, 48, 3);
original.save(path.string());
const auto reloaded = Image::load(path.string());
check(sameBytes(original, reloaded), "png round trip is lossless");
std::filesystem::remove(path);
bool threw = false;
try {
Image::load("/definitely/not/here.png");
} catch (const std::exception&) {
threw = true;
}
check(threw, "missing file throws");
}
std::cout << "batch loading\n";
{
const auto dir = std::filesystem::temp_directory_path() / "effector_test_batch";
std::filesystem::create_directories(dir);
std::vector<std::string> paths;
for (int i = 0; i < 6; ++i) {
const auto path = dir / ("src" + std::to_string(i) + ".png");
gradient(32, 24, i + 1).save(path.string());
paths.push_back(path.string());
}
paths.push_back((dir / "missing.png").string());
std::atomic<std::size_t> calls{0};
std::size_t lastTotal = 0;
std::mutex mutex;
LoadProgress progress;
progress.onFile = [&](std::size_t done, std::size_t total, const std::string&) {
std::lock_guard<std::mutex> lock(mutex);
++calls;
lastTotal = total;
check(done >= 1 && done <= total, "progress step is within the batch");
};
std::vector<LoadFailure> failuresOut;
const auto images = loadImages(paths, failuresOut, &progress);
check(images.size() == 6, "every readable image is returned");
check(failuresOut.size() == 1, "the unreadable one is reported");
check(calls.load() == paths.size(), "progress fires once per file");
check(lastTotal == paths.size(), "progress reports the batch size");
check(images.front().path() == paths.front(), "input order is preserved");
std::atomic<bool> cancel{true};
LoadProgress cancelled;
cancelled.cancel = &cancel;
std::vector<LoadFailure> ignored;
const auto none = loadImages(paths, ignored, &cancelled);
check(none.empty(), "a cancelled batch loads nothing");
std::filesystem::remove_all(dir);
}
std::cout << (failures == 0 ? "\nall checks passed\n" : "\n" + std::to_string(failures) + " failed\n");
return failures == 0 ? 0 : 1;
}