From 7973f888066317c5a17563112df95f84266a9275 Mon Sep 17 00:00:00 2001 From: Ralph Weng Date: Mon, 8 Dec 2025 21:58:40 +1100 Subject: [PATCH] Add graphics unit testing helpers and tests - Create graphics_test_helpers.h with bitmap comparison utilities - Add tests for fill_circle, fill_rectangle, fill_ellipse, draw_line - 7 test cases with 28 assertions, all passing --- .../test/unit_tests/graphics_test_helpers.h | 213 ++++++++++++++ .../test/unit_tests/unit_test_graphics.cpp | 259 +++++++++++++++++- 2 files changed, 470 insertions(+), 2 deletions(-) create mode 100644 coresdk/src/test/unit_tests/graphics_test_helpers.h diff --git a/coresdk/src/test/unit_tests/graphics_test_helpers.h b/coresdk/src/test/unit_tests/graphics_test_helpers.h new file mode 100644 index 00000000..270abc24 --- /dev/null +++ b/coresdk/src/test/unit_tests/graphics_test_helpers.h @@ -0,0 +1,213 @@ +/** + * Graphics Test Helpers + * + * Utility functions for unit testing graphics/drawing functionality. + * Provides bitmap comparison and verification capabilities. + */ + +#ifndef GRAPHICS_TEST_HELPERS_H +#define GRAPHICS_TEST_HELPERS_H + +#include "images.h" +#include "color.h" +#include "point_drawing.h" + +#include + +namespace splashkit_lib +{ + /** + * Calculates the difference between two colors as a value from 0.0 to 1.0. + * 0.0 means identical, 1.0 means completely different. + * + * @param c1 First color to compare + * @param c2 Second color to compare + * @return Difference value between 0.0 and 1.0 + */ + inline double color_difference(color c1, color c2) + { + // Color component functions return 0-255, normalize to 0-1 + double r_diff = std::abs(red_of(c1) - red_of(c2)) / 255.0; + double g_diff = std::abs(green_of(c1) - green_of(c2)) / 255.0; + double b_diff = std::abs(blue_of(c1) - blue_of(c2)) / 255.0; + double a_diff = std::abs(alpha_of(c1) - alpha_of(c2)) / 255.0; + + // Return average difference across all channels (each channel is 0-1) + return (r_diff + g_diff + b_diff + a_diff) / 4.0; + } + + /** + * Checks if two colors match within a tolerance. + * + * @param c1 First color to compare + * @param c2 Second color to compare + * @param tolerance Maximum allowed difference (0.0 = exact match, 1.0 = any match) + * @return True if colors are within tolerance + */ + inline bool colors_match(color c1, color c2, double tolerance = 0.0) + { + return color_difference(c1, c2) <= tolerance; + } + + /** + * Compares two bitmaps pixel-by-pixel. + * Returns true if all pixels match within the specified tolerance. + * + * @param a First bitmap to compare + * @param b Second bitmap to compare + * @param tolerance Color difference tolerance (0.0 = exact, 0.1 = 10% tolerance) + * @return True if bitmaps match within tolerance + */ + inline bool bitmaps_match(bitmap a, bitmap b, double tolerance = 0.0) + { + if (!bitmap_valid(a) || !bitmap_valid(b)) + return false; + + if (bitmap_width(a) != bitmap_width(b) || + bitmap_height(a) != bitmap_height(b)) + return false; + + for (int y = 0; y < bitmap_height(a); y++) + { + for (int x = 0; x < bitmap_width(a); x++) + { + color c1 = get_pixel(a, x, y); + color c2 = get_pixel(b, x, y); + if (!colors_match(c1, c2, tolerance)) + return false; + } + } + return true; + } + + /** + * Counts the number of pixels that differ between two bitmaps. + * Useful for debugging when bitmaps don't match. + * + * @param a First bitmap to compare + * @param b Second bitmap to compare + * @param tolerance Color difference tolerance + * @return Number of differing pixels, or -1 if bitmaps have different dimensions + */ + inline int bitmap_diff_count(bitmap a, bitmap b, double tolerance = 0.0) + { + if (!bitmap_valid(a) || !bitmap_valid(b)) + return -1; + + if (bitmap_width(a) != bitmap_width(b) || + bitmap_height(a) != bitmap_height(b)) + return -1; + + int diff_count = 0; + for (int y = 0; y < bitmap_height(a); y++) + { + for (int x = 0; x < bitmap_width(a); x++) + { + color c1 = get_pixel(a, x, y); + color c2 = get_pixel(b, x, y); + if (!colors_match(c1, c2, tolerance)) + diff_count++; + } + } + return diff_count; + } + + /** + * Verifies that a filled circle was drawn correctly by sampling pixels + * inside and outside the expected circle boundary. + * + * @param bmp Bitmap to verify + * @param center Center point of the expected circle + * @param radius Radius of the expected circle + * @param fill_color Expected color inside the circle + * @param background_color Expected color outside the circle + * @param sample_count Number of points to sample (higher = more accurate) + * @return True if the circle appears to be drawn correctly + */ + inline bool verify_filled_circle(bitmap bmp, point_2d center, double radius, + color fill_color, color background_color, + int sample_count = 36) + { + if (!bitmap_valid(bmp)) + return false; + + const double PI = 3.14159265358979323846; + double angle_step = 360.0 / sample_count; + + // Check points inside the circle (at 80% of radius) + for (int i = 0; i < sample_count; i++) + { + double angle = i * angle_step * (PI / 180.0); + double r = radius * 0.8; + int x = static_cast(center.x + r * std::cos(angle)); + int y = static_cast(center.y + r * std::sin(angle)); + + if (x >= 0 && x < bitmap_width(bmp) && y >= 0 && y < bitmap_height(bmp)) + { + color pixel = get_pixel(bmp, x, y); + if (!colors_match(pixel, fill_color, 0.1)) + return false; + } + } + + // Check points outside the circle (at 120% of radius) + for (int i = 0; i < sample_count; i++) + { + double angle = i * angle_step * (PI / 180.0); + double r = radius * 1.2; + int x = static_cast(center.x + r * std::cos(angle)); + int y = static_cast(center.y + r * std::sin(angle)); + + if (x >= 0 && x < bitmap_width(bmp) && y >= 0 && y < bitmap_height(bmp)) + { + color pixel = get_pixel(bmp, x, y); + if (!colors_match(pixel, background_color, 0.1)) + return false; + } + } + + return true; + } + + /** + * Verifies that a filled rectangle was drawn correctly. + * + * @param bmp Bitmap to verify + * @param rect Rectangle that should be filled + * @param fill_color Expected color inside the rectangle + * @param tolerance Color matching tolerance + * @return True if the rectangle appears to be filled correctly + */ + inline bool verify_filled_rectangle(bitmap bmp, rectangle rect, + color fill_color, double tolerance = 0.1) + { + if (!bitmap_valid(bmp)) + return false; + + // Sample points inside the rectangle + int sample_points[][2] = { + {static_cast(rect.x + rect.width * 0.25), static_cast(rect.y + rect.height * 0.25)}, + {static_cast(rect.x + rect.width * 0.75), static_cast(rect.y + rect.height * 0.25)}, + {static_cast(rect.x + rect.width * 0.25), static_cast(rect.y + rect.height * 0.75)}, + {static_cast(rect.x + rect.width * 0.75), static_cast(rect.y + rect.height * 0.75)}, + {static_cast(rect.x + rect.width * 0.5), static_cast(rect.y + rect.height * 0.5)} + }; + + for (auto& point : sample_points) + { + int x = point[0]; + int y = point[1]; + if (x >= 0 && x < bitmap_width(bmp) && y >= 0 && y < bitmap_height(bmp)) + { + color pixel = get_pixel(bmp, x, y); + if (!colors_match(pixel, fill_color, tolerance)) + return false; + } + } + + return true; + } + +} // namespace splashkit_lib + +#endif // GRAPHICS_TEST_HELPERS_H diff --git a/coresdk/src/test/unit_tests/unit_test_graphics.cpp b/coresdk/src/test/unit_tests/unit_test_graphics.cpp index 04da999c..704b792e 100644 --- a/coresdk/src/test/unit_tests/unit_test_graphics.cpp +++ b/coresdk/src/test/unit_tests/unit_test_graphics.cpp @@ -1,23 +1,278 @@ /** * Graphics Unit Tests + * + * Tests for graphics/drawing functions using bitmap comparison helpers. */ #include "catch.hpp" #include "graphics.h" #include "window_manager.h" +#include "images.h" +#include "color.h" +#include "circle_drawing.h" +#include "rectangle_drawing.h" +#include "ellipse_drawing.h" +#include "line_drawing.h" +#include "drawing_options.h" + +#include "graphics_test_helpers.h" using namespace splashkit_lib; +// ============================================================================= +// Helper Function Tests +// ============================================================================= + +TEST_CASE("color comparison works correctly", "[graphics][colors_match]") +{ + SECTION("identical colors match") + { + REQUIRE(colors_match(COLOR_RED, COLOR_RED)); + REQUIRE(colors_match(COLOR_WHITE, COLOR_WHITE)); + REQUIRE(colors_match(COLOR_BLACK, COLOR_BLACK)); + } + SECTION("different colors do not match with zero tolerance") + { + REQUIRE_FALSE(colors_match(COLOR_RED, COLOR_BLUE, 0.0)); + REQUIRE_FALSE(colors_match(COLOR_WHITE, COLOR_BLACK, 0.0)); + } + SECTION("similar colors match with tolerance") + { + // Colors that differ by small amounts should match with tolerance + color almost_red = rgba_color(250, 0, 0, 255); // Slightly less red + REQUIRE(colors_match(COLOR_RED, almost_red, 0.05)); + } + SECTION("color_difference returns expected values") + { + REQUIRE(color_difference(COLOR_RED, COLOR_RED) == 0.0); + // Red vs Blue: R differs by 1.0, G same, B differs by 1.0, A same = average 0.5 + double diff = color_difference(COLOR_RED, COLOR_BLUE); + REQUIRE(diff > 0.0); + REQUIRE(diff <= 1.0); + } +} + +TEST_CASE("bitmap comparison works correctly", "[graphics][bitmaps_match]") +{ + bitmap bmp1 = create_bitmap("test_bmp1", 100, 100); + bitmap bmp2 = create_bitmap("test_bmp2", 100, 100); + + SECTION("identical bitmaps match") + { + clear_bitmap(bmp1, COLOR_WHITE); + clear_bitmap(bmp2, COLOR_WHITE); + setup_collision_mask(bmp1); + setup_collision_mask(bmp2); + REQUIRE(bitmaps_match(bmp1, bmp2)); + } + SECTION("different colored bitmaps do not match") + { + clear_bitmap(bmp1, COLOR_WHITE); + clear_bitmap(bmp2, COLOR_RED); + setup_collision_mask(bmp1); + setup_collision_mask(bmp2); + REQUIRE_FALSE(bitmaps_match(bmp1, bmp2)); + } + SECTION("bitmap_diff_count returns correct count") + { + clear_bitmap(bmp1, COLOR_WHITE); + clear_bitmap(bmp2, COLOR_WHITE); + // Draw something different on bmp2 + fill_circle(COLOR_RED, 50, 50, 10, option_draw_to(bmp2)); + setup_collision_mask(bmp1); + setup_collision_mask(bmp2); + int diff = bitmap_diff_count(bmp1, bmp2); + REQUIRE(diff > 0); + } + SECTION("bitmaps of different sizes do not match") + { + bitmap bmp3 = create_bitmap("test_bmp3", 50, 50); + clear_bitmap(bmp1, COLOR_WHITE); + clear_bitmap(bmp3, COLOR_WHITE); + setup_collision_mask(bmp1); + setup_collision_mask(bmp3); + REQUIRE_FALSE(bitmaps_match(bmp1, bmp3)); + REQUIRE(bitmap_diff_count(bmp1, bmp3) == -1); + free_bitmap(bmp3); + } + + free_bitmap(bmp1); + free_bitmap(bmp2); +} + +// ============================================================================= +// Shape Drawing Tests +// ============================================================================= + +TEST_CASE("fill_circle draws correctly", "[graphics][fill_circle]") +{ + bitmap bmp = create_bitmap("circle_test", 200, 200); + color bg_color = COLOR_WHITE; + color circle_color = COLOR_RED; + point_2d center = point_at(100, 100); + double radius = 40; + + clear_bitmap(bmp, bg_color); + fill_circle(circle_color, center, radius, option_draw_to(bmp)); + setup_collision_mask(bmp); + + SECTION("circle is filled at center") + { + color pixel = get_pixel(bmp, 100, 100); + REQUIRE(colors_match(pixel, circle_color, 0.1)); + } + SECTION("circle is filled inside boundary") + { + // Check a point inside the circle (at half radius) + int x = static_cast(center.x + radius * 0.5); + int y = static_cast(center.y); + color pixel = get_pixel(bmp, x, y); + REQUIRE(colors_match(pixel, circle_color, 0.1)); + } + SECTION("background is preserved outside circle") + { + // Check a point outside the circle + int x = static_cast(center.x + radius * 1.5); + int y = static_cast(center.y); + color pixel = get_pixel(bmp, x, y); + REQUIRE(colors_match(pixel, bg_color, 0.1)); + } + SECTION("verify_filled_circle helper works") + { + REQUIRE(verify_filled_circle(bmp, center, radius, circle_color, bg_color)); + } + + free_bitmap(bmp); +} + +TEST_CASE("fill_circle with point_2d parameter works correctly", "[graphics][fill_circle]") +{ + bitmap bmp = create_bitmap("circle_pt_test", 200, 200); + color bg_color = COLOR_WHITE; + color circle_color = COLOR_BLUE; + point_2d center = point_at(100, 100); + double radius = 50; + + clear_bitmap(bmp, bg_color); + + SECTION("point is background before drawing") + { + setup_collision_mask(bmp); + color pixel = get_pixel(bmp, 100, 100); + REQUIRE(colors_match(pixel, bg_color, 0.1)); + } + + fill_circle(circle_color, center, radius, option_draw_to(bmp)); + setup_collision_mask(bmp); + + SECTION("point is filled after drawing") + { + color pixel = get_pixel(bmp, 100, 100); + REQUIRE(colors_match(pixel, circle_color, 0.1)); + } + SECTION("circle passes full verification") + { + REQUIRE(verify_filled_circle(bmp, center, radius, circle_color, bg_color)); + } + + free_bitmap(bmp); +} + +TEST_CASE("fill_rectangle draws correctly", "[graphics][fill_rectangle]") +{ + bitmap bmp = create_bitmap("rect_test", 200, 200); + color bg_color = COLOR_WHITE; + color rect_color = COLOR_GREEN; + rectangle rect = rectangle_from(50, 50, 100, 80); + + clear_bitmap(bmp, bg_color); + fill_rectangle(rect_color, rect, option_draw_to(bmp)); + setup_collision_mask(bmp); + + SECTION("rectangle center is filled") + { + int cx = static_cast(rect.x + rect.width / 2); + int cy = static_cast(rect.y + rect.height / 2); + color pixel = get_pixel(bmp, cx, cy); + REQUIRE(colors_match(pixel, rect_color, 0.1)); + } + SECTION("verify_filled_rectangle helper works") + { + REQUIRE(verify_filled_rectangle(bmp, rect, rect_color)); + } + SECTION("background is preserved outside rectangle") + { + color pixel = get_pixel(bmp, 10, 10); + REQUIRE(colors_match(pixel, bg_color, 0.1)); + } + + free_bitmap(bmp); +} + +TEST_CASE("fill_ellipse draws correctly", "[graphics][fill_ellipse]") +{ + bitmap bmp = create_bitmap("ellipse_test", 200, 200); + color bg_color = COLOR_WHITE; + color ellipse_color = COLOR_YELLOW; + + clear_bitmap(bmp, bg_color); + // Draw ellipse at center (100,100) with width 80 and height 40 + fill_ellipse(ellipse_color, 60, 80, 80, 40, option_draw_to(bmp)); + setup_collision_mask(bmp); + + SECTION("ellipse center is filled") + { + color pixel = get_pixel(bmp, 100, 100); + REQUIRE(colors_match(pixel, ellipse_color, 0.1)); + } + SECTION("background is preserved outside ellipse") + { + color pixel = get_pixel(bmp, 10, 10); + REQUIRE(colors_match(pixel, bg_color, 0.1)); + } + + free_bitmap(bmp); +} + +TEST_CASE("draw_line draws correctly", "[graphics][draw_line]") +{ + bitmap bmp = create_bitmap("line_test", 200, 200); + color bg_color = COLOR_WHITE; + color line_color = COLOR_BLACK; + + clear_bitmap(bmp, bg_color); + // Draw horizontal line from (50,100) to (150,100) + draw_line(line_color, 50, 100, 150, 100, option_draw_to(bmp)); + setup_collision_mask(bmp); + + SECTION("line midpoint has correct color") + { + color pixel = get_pixel(bmp, 100, 100); + REQUIRE(colors_match(pixel, line_color, 0.1)); + } + SECTION("background is preserved away from line") + { + color pixel = get_pixel(bmp, 100, 50); + REQUIRE(colors_match(pixel, bg_color, 0.1)); + } + + free_bitmap(bmp); +} + +// ============================================================================= +// Screen Dimension Tests +// ============================================================================= + TEST_CASE("screen dimension utilities", "[screen_width][screen_height]") { + // Note: These tests require a window, so they are commented out + // to allow running tests without a display // SECTION("check that screen width and screen height are correct") // { // window w = open_window("test window", 200, 100); - // REQUIRE(screen_width() == 200); // REQUIRE(screen_height() == 100); - // close_window(w); // } }