From 04b3fcb479f5aaae06d18b315a8bdc8c298f4eae Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 5 Aug 2025 11:28:41 +0200 Subject: removed clustering --- tests/Makefile | 169 +++++++++++++++++++ tests/data/test_glasses.json | 47 ++++++ tests/fgla_test_internals.h | 28 ++++ tests/integration/test_full_pipeline.c | 273 +++++++++++++++++++++++++++++++ tests/integration/test_simple_pipeline.c | 221 +++++++++++++++++++++++++ tests/test_framework.h | 120 ++++++++++++++ tests/unit/test_fgla.c | 231 ++++++++++++++++++++++++++ tests/unit/test_glamac_view.c | 243 +++++++++++++++++++++++++++ tests/unit/test_glamac_view_simple.c | 168 +++++++++++++++++++ tests/unit/test_glass_data.c | 174 ++++++++++++++++++++ 10 files changed, 1674 insertions(+) create mode 100644 tests/Makefile create mode 100644 tests/data/test_glasses.json create mode 100644 tests/fgla_test_internals.h create mode 100644 tests/integration/test_full_pipeline.c create mode 100644 tests/integration/test_simple_pipeline.c create mode 100644 tests/test_framework.h create mode 100644 tests/unit/test_fgla.c create mode 100644 tests/unit/test_glamac_view.c create mode 100644 tests/unit/test_glamac_view_simple.c create mode 100644 tests/unit/test_glass_data.c (limited to 'tests') diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..84e64dc --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,169 @@ +# Test Makefile for GlaMaC +# Builds and runs unit tests and integration tests + +# Include common build configuration +include ../build/common.mk + +# Test-specific directories +TESTDIR := . +UNIT_TESTDIR := $(TESTDIR)/unit +INTEGRATION_TESTDIR := $(TESTDIR)/integration +TEST_BINDIR := $(TESTDIR)/bin + +# Test source files +UNIT_TESTS := $(wildcard $(UNIT_TESTDIR)/*.c) +INTEGRATION_TESTS := $(wildcard $(INTEGRATION_TESTDIR)/*.c) + +# Test binaries (specific ones we have rules for) +UNIT_TEST_BINS := $(TEST_BINDIR)/test_glass_data $(TEST_BINDIR)/test_fgla $(TEST_BINDIR)/test_glamac_view +INTEGRATION_TEST_BINS := $(TEST_BINDIR)/test_simple_pipeline + +# Glass data dependencies (needed for tests) +GLASS_DATA_DEPS := ../src/glamac/glass_data.c ../src/glamac/glamac_errors.c + +# View system dependencies +VIEW_DEPS := ../src/glamac/glamac_view.c ../src/glamac/glamac_errors.c + +# Test-specific compiler flags +TEST_CFLAGS := $(CFLAGS_NATIVE) -I$(TESTDIR) -DTEST_BUILD + +# Default target +all: unit-tests integration-tests + +# Create test bin directory +$(TEST_BINDIR): + $(MKDIR) $(TEST_BINDIR) + +# Unit test rules +unit-tests: $(UNIT_TEST_BINS) + +# Unit test for glass_data +$(TEST_BINDIR)/test_glass_data: $(UNIT_TESTDIR)/test_glass_data.c $(GLASS_DATA_DEPS) | $(TEST_BINDIR) + $(CC) $^ $(TEST_CFLAGS) -o $@ + +# Unit test for fgla (needs fgla.c for internal function testing) +$(TEST_BINDIR)/test_fgla: $(UNIT_TESTDIR)/test_fgla.c ../src/glautils/fgla.c $(GLASS_DATA_DEPS) | $(TEST_BINDIR) + $(CC) $^ $(TEST_CFLAGS) -o $@ + +# Unit test for glamac_view (simplified version) +$(TEST_BINDIR)/test_glamac_view: $(UNIT_TESTDIR)/test_glamac_view_simple.c $(VIEW_DEPS) $(GLASS_DATA_DEPS) | $(TEST_BINDIR) + $(CC) $^ $(TEST_CFLAGS) -o $@ + +# Integration tests +integration-tests: $(INTEGRATION_TEST_BINS) + +# Integration test for simple pipeline +$(TEST_BINDIR)/test_simple_pipeline: $(INTEGRATION_TESTDIR)/test_simple_pipeline.c ../src/glautils/fgla.c $(VIEW_DEPS) $(GLASS_DATA_DEPS) | $(TEST_BINDIR) + $(CC) $^ $(TEST_CFLAGS) -o $@ + +# Run all tests +test: unit-tests integration-tests + @echo "" + @echo "$(CYAN)Running Unit Tests$(RESET)" + @echo "==================" + @for test in $(UNIT_TEST_BINS); do \ + if [ -f "$$test" ]; then \ + echo "$(BLUE)Running $$test$(RESET)"; \ + ./$$test; \ + echo ""; \ + fi; \ + done + @echo "" + @echo "$(CYAN)Running Integration Tests$(RESET)" + @echo "=========================" + @for test in $(INTEGRATION_TEST_BINS); do \ + if [ -f "$$test" ]; then \ + echo "$(BLUE)Running $$test$(RESET)"; \ + ./$$test; \ + echo ""; \ + fi; \ + done + +# Run only unit tests +test-unit: unit-tests + @echo "$(CYAN)Running Unit Tests$(RESET)" + @echo "==================" + @for test in $(UNIT_TEST_BINS); do \ + if [ -f "$$test" ]; then \ + echo "$(BLUE)Running $$test$(RESET)"; \ + ./$$test; \ + echo ""; \ + fi; \ + done + +# Run only integration tests +test-integration: integration-tests + @echo "$(CYAN)Running Integration Tests$(RESET)" + @echo "=========================" + @for test in $(INTEGRATION_TEST_BINS); do \ + if [ -f "$$test" ]; then \ + echo "$(BLUE)Running $$test$(RESET)"; \ + ./$$test; \ + echo ""; \ + fi; \ + done + +# Run specific test +test-%: $(TEST_BINDIR)/% + @echo "$(CYAN)Running specific test: $*$(RESET)" + @./$(TEST_BINDIR)/$* + +# Clean test binaries +clean: + $(RMDIR) $(TEST_BINDIR) 2>/dev/null || true + +# Verbose test runs (for debugging) +test-verbose: SHELL:=/bin/bash +test-verbose: unit-tests integration-tests + @echo "" + @echo "$(CYAN)Running Unit Tests (Verbose)$(RESET)" + @echo "============================" + @for test in $(UNIT_TEST_BINS); do \ + if [ -f "$$test" ]; then \ + echo "$(BLUE)Running $$test$(RESET)"; \ + ./$$test 2>&1 | tee "$$test.log"; \ + echo ""; \ + fi; \ + done + @echo "" + @echo "$(CYAN)Running Integration Tests (Verbose)$(RESET)" + @echo "==================================" + @for test in $(INTEGRATION_TEST_BINS); do \ + if [ -f "$$test" ]; then \ + echo "$(BLUE)Running $$test$(RESET)"; \ + ./$$test 2>&1 | tee "$$test.log"; \ + echo ""; \ + fi; \ + done + +# Help +help: + @echo "GlaMaC Test System" + @echo "" + @echo "Targets:" + @echo " all - Build all tests" + @echo " unit-tests - Build unit tests" + @echo " integration-tests- Build integration tests" + @echo " test - Build and run all tests" + @echo " test-unit - Build and run unit tests only" + @echo " test-integration - Build and run integration tests only" + @echo " test- - Run specific test by name" + @echo " test-verbose - Run all tests with verbose output" + @echo " clean - Clean test binaries" + @echo " help - Show this help" + @echo "" + @echo "Examples:" + @echo " make test # Run all tests" + @echo " make test-unit # Run only unit tests" + @echo " make test-test_glass_data # Run specific test" + +# Color definitions for use in shell commands +export RESET := \033[0m +export RED := \033[31m +export GREEN := \033[32m +export YELLOW := \033[33m +export BLUE := \033[34m +export MAGENTA := \033[35m +export CYAN := \033[36m + +.PHONY: all unit-tests integration-tests test test-unit test-integration clean help test-verbose \ No newline at end of file diff --git a/tests/data/test_glasses.json b/tests/data/test_glasses.json new file mode 100644 index 0000000..ba707aa --- /dev/null +++ b/tests/data/test_glasses.json @@ -0,0 +1,47 @@ +{ + "manufacturers": { + "SCHOTT": { + "glasses": [ + { + "name": "N-BK7", + "nd": 1.5168, + "vd": 64.17, + "glass_code": "517642", + "manufacturer": "SCHOTT" + }, + { + "name": "SF10", + "nd": 1.7283, + "vd": 28.41, + "glass_code": "728284", + "manufacturer": "SCHOTT" + }, + { + "name": "FK51A", + "nd": 1.4866, + "vd": 81.61, + "glass_code": "486816", + "manufacturer": "SCHOTT" + } + ] + }, + "HOYA": { + "glasses": [ + { + "name": "FCD1", + "nd": 1.4970, + "vd": 81.54, + "glass_code": "497815", + "manufacturer": "HOYA" + }, + { + "name": "LAC7", + "nd": 1.6510, + "vd": 58.52, + "glass_code": "651585", + "manufacturer": "HOYA" + } + ] + } + } +} \ No newline at end of file diff --git a/tests/fgla_test_internals.h b/tests/fgla_test_internals.h new file mode 100644 index 0000000..6e019c7 --- /dev/null +++ b/tests/fgla_test_internals.h @@ -0,0 +1,28 @@ +/** + * fgla_test_internals.h - Internal function declarations for testing + * + * This header exposes internal fgla functions for unit testing purposes. + * These functions should not be used in production code. + */ + +#ifndef FGLA_TEST_INTERNALS_H +#define FGLA_TEST_INTERNALS_H + +#include "../include/glautils/fgla.h" + +#ifdef TEST_BUILD + +// Re-declare internal functions as extern for testing +// These functions are normally static in fgla.c + +extern void to_lowercase_safe(char* str, size_t max_len); +extern int normalize_string_safe(const char* input, char* output, size_t output_size); +extern int contains_substring_safe(const char* haystack, const char* needle); +extern int matches_catalog(const char* manufacturer, const char* catalog_list[], int catalog_count); +extern int validate_search_term(const char* term); +extern int is_glass_code_pattern(const char* term); +extern int matches_glass_code_pattern_safe(const char* glass_code, const char* pattern); + +#endif /* TEST_BUILD */ + +#endif /* FGLA_TEST_INTERNALS_H */ \ No newline at end of file diff --git a/tests/integration/test_full_pipeline.c b/tests/integration/test_full_pipeline.c new file mode 100644 index 0000000..1bd9296 --- /dev/null +++ b/tests/integration/test_full_pipeline.c @@ -0,0 +1,273 @@ +/** + * test_full_pipeline.c - Integration tests for the full GlaMaC pipeline + * + * Copyright (C) 2025 https://optics-design.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + */ + +#include "../test_framework.h" +#include "../../include/glass_data.h" +#include "../../include/glamac_view.h" +#include "../../include/glautils/fgla.h" + +// Test complete glass data loading and processing pipeline +int test_glass_data_pipeline() { + TEST_START("Glass Data Pipeline"); + + // Initialize glass data system + initialize_glass_data(); + + // Try to load JSON data + b32 json_loaded = load_glasses_from_json((const byte*)"tests/data/test_glasses.json", NULL); + + if (json_loaded) { + // Verify we have multiple catalogs + u32 catalog_count = get_catalog_count(); + TEST_ASSERT(catalog_count > 0, "Should have loaded multiple catalogs"); + + // Test each catalog + for (u32 i = 0; i < catalog_count; i++) { + set_current_catalog(i); + + const char* catalog_name = get_catalog_name(i); + TEST_ASSERT_NOT_NULL(catalog_name, "Each catalog should have a name"); + + u32 glass_count = get_glass_count(); + TEST_ASSERT(glass_count > 0, "Each catalog should have glasses"); + + // Test glass properties + for (u32 j = 0; j < glass_count; j++) { + const Glass* glass = get_glass(j); + TEST_ASSERT_NOT_NULL(glass, "Should be able to get each glass"); + + // Validate glass data + TEST_ASSERT(glass->abbeNumber > 10.0f && glass->abbeNumber < 100.0f, + "Abbe number should be in reasonable range"); + TEST_ASSERT(glass->refractiveIndex > 1.0f && glass->refractiveIndex < 4.0f, + "Refractive index should be in reasonable range"); + + const byte* name = get_glass_name(j); + TEST_ASSERT_NOT_NULL(name, "Each glass should have a name"); + TEST_ASSERT(strlen((const char*)name) > 0, "Glass name should not be empty"); + } + } + } else { + printf(YELLOW " Info: JSON test data not available - testing with default data" RESET "\n"); + + // Test with default data + u32 glass_count = get_glass_count(); + TEST_ASSERT(glass_count > 0, "Should have default glasses"); + } + + cleanup_glass_data(); + TEST_END(); +} + +// Test view system with real glass data +int test_view_with_glass_data() { + TEST_START("View System with Glass Data"); + + // Initialize systems + initialize_glass_data(); + ViewState view; + init_view(&view, 1024, 768); + + // Load test data if available + load_glasses_from_json((const byte*)"tests/data/test_glasses.json", NULL); + + // Test view fitting with real data + fit_view_to_data(&view); + + // After fitting, we should be able to see all glasses + TEST_ASSERT(view.zoom > 0.0f, "Zoom should be positive after fitting"); + TEST_ASSERT(view.zoom <= MAX_ZOOM, "Zoom should not exceed maximum"); + + // Test coordinate transformations with actual glass data + u32 glass_count = get_glass_count(); + if (glass_count > 0) { + const Glass* glass = get_glass(0); + i32 screenX, screenY; + glass_to_screen_coords(glass->abbeNumber, glass->refractiveIndex, &view, &screenX, &screenY); + + // Screen coordinates should be reasonable + TEST_ASSERT(screenX >= -100 && screenX <= view.windowWidth + 100, + "Screen X should be near window bounds"); + TEST_ASSERT(screenY >= -100 && screenY <= view.windowHeight + 100, + "Screen Y should be near window bounds"); + + // Test inverse transformation + f32 recovered_abbe, recovered_ri; + screen_to_glass_coords(screenX, screenY, &view, &recovered_abbe, &recovered_ri); + + TEST_ASSERT_FLOAT_EQ(glass->abbeNumber, recovered_abbe, 0.1f, + "Should recover original Abbe number"); + TEST_ASSERT_FLOAT_EQ(glass->refractiveIndex, recovered_ri, 0.001f, + "Should recover original refractive index"); + } + + // Test clustering with real data + update_clustering(&view); + TEST_ASSERT(view.clusterCount >= 0, "Clustering should complete successfully"); + + cleanup_glass_data(); + TEST_END(); +} + +// Test FGLA search functionality with actual data +int test_fgla_search_integration() { + TEST_START("FGLA Search Integration"); + + // Test various search functions that would be used in the real application + + // Test glass code pattern matching with realistic codes + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642", "517642"), + "Should match exact glass code"); + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642123", "517642"), + "Should match first 6 digits of longer code"); + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642", "51x64x"), + "Should match wildcard pattern"); + + // Test realistic search terms + TEST_ASSERT(fgla_validate_search_term("N-BK7"), "N-BK7 should be valid search term"); + TEST_ASSERT(fgla_validate_search_term("SF10"), "SF10 should be valid search term"); + TEST_ASSERT(fgla_validate_search_term("FCD1"), "FCD1 should be valid search term"); + + // Test substring matching with realistic glass names + TEST_ASSERT(fgla_contains_substring_safe("N-BK7", "BK"), + "Should find BK in N-BK7"); + TEST_ASSERT(fgla_contains_substring_safe("SF10", "sf"), + "Should find sf in SF10 (case insensitive)"); + TEST_ASSERT(fgla_contains_substring_safe("FCD1", "fcd"), + "Should find fcd in FCD1 (case insensitive)"); + + // Test catalog matching with real manufacturers + const char* real_catalogs[] = {"SCHOTT", "HOYA", "CDGM", "Ohara"}; + TEST_ASSERT(fgla_matches_catalog("SCHOTT", real_catalogs, 4), + "Should match SCHOTT catalog"); + TEST_ASSERT(fgla_matches_catalog("hoya", real_catalogs, 4), + "Should match HOYA catalog (case insensitive)"); + TEST_ASSERT(!fgla_matches_catalog("UNKNOWN", real_catalogs, 4), + "Should not match unknown manufacturer"); + + TEST_END(); +} + +// Test error handling across systems +int test_error_handling_integration() { + TEST_START("Error Handling Integration"); + + // Test glass data error handling + b32 result = load_glasses_from_json((const byte*)"nonexistent.json", NULL); + TEST_ASSERT(!result, "Should fail gracefully with non-existent file"); + + result = load_glasses_from_json(NULL, NULL); + TEST_ASSERT(!result, "Should fail gracefully with NULL path"); + + // Test view system with invalid parameters + ViewState view; + init_view(&view, 0, 0); // Invalid dimensions + TEST_ASSERT(view.windowWidth > 0, "Should handle invalid width gracefully"); + TEST_ASSERT(view.windowHeight > 0, "Should handle invalid height gracefully"); + + // Test coordinate transformations with extreme values + i32 screenX, screenY; + glass_to_screen_coords(1000.0f, 10.0f, &view, &screenX, &screenY); // Extreme values + // Should not crash - exact values depend on implementation + + // Test FGLA with invalid inputs + TEST_ASSERT(!fgla_validate_search_term(NULL), "Should reject NULL search term"); + TEST_ASSERT(!fgla_validate_search_term(""), "Should reject empty search term"); + TEST_ASSERT(!fgla_is_glass_code_pattern(NULL), "Should reject NULL glass code pattern"); + + TEST_END(); +} + +// Test memory management across systems +int test_memory_management() { + TEST_START("Memory Management"); + + // Test multiple initialize/cleanup cycles + for (int i = 0; i < 5; i++) { + initialize_glass_data(); + + // Load data + load_glasses_from_json((const byte*)"tests/data/test_glasses.json", NULL); + + // Use the data + u32 count = get_glass_count(); + if (count > 0) { + const Glass* glass = get_glass(0); + (void)glass; // Suppress unused variable warning + } + + // Cleanup + cleanup_glass_data(); + } + + // After all cycles, we should be back to clean state + // Try to initialize again + initialize_glass_data(); + u32 final_count = get_glass_count(); + TEST_ASSERT(final_count >= 0, "Should be able to initialize after cleanup cycles"); + + cleanup_glass_data(); + TEST_END(); +} + +// Test performance with realistic data volumes +int test_performance() { + TEST_START("Performance Test"); + + initialize_glass_data(); + + // Load data + b32 loaded = load_glasses_from_json((const byte*)"tests/data/test_glasses.json", NULL); + + if (loaded) { + // Test rapid catalog switching + u32 catalog_count = get_catalog_count(); + for (int i = 0; i < 100; i++) { + set_current_catalog(i % catalog_count); + u32 count = get_glass_count(); + (void)count; // Use the result + } + + // Test rapid coordinate transformations + ViewState view; + init_view(&view, 1024, 768); + fit_view_to_data(&view); + + for (int i = 0; i < 1000; i++) { + i32 screenX, screenY; + f32 abbe = 30.0f + (i % 50); + f32 ri = 1.4f + (i % 20) * 0.01f; + glass_to_screen_coords(abbe, ri, &view, &screenX, &screenY); + } + + // Test clustering performance + for (int i = 0; i < 10; i++) { + update_clustering(&view); + } + } + + cleanup_glass_data(); + TEST_END(); +} + +// Main integration test runner +int main() { + printf(BLUE "=== GlaMaC Integration Tests ===" RESET "\n\n"); + + RUN_TEST(test_glass_data_pipeline); + RUN_TEST(test_view_with_glass_data); + RUN_TEST(test_fgla_search_integration); + RUN_TEST(test_error_handling_integration); + RUN_TEST(test_memory_management); + RUN_TEST(test_performance); + + TEST_SUMMARY(); +} \ No newline at end of file diff --git a/tests/integration/test_simple_pipeline.c b/tests/integration/test_simple_pipeline.c new file mode 100644 index 0000000..01d2533 --- /dev/null +++ b/tests/integration/test_simple_pipeline.c @@ -0,0 +1,221 @@ +/** + * test_simple_pipeline.c - Simplified integration tests for GlaMaC pipeline + * + * Copyright (C) 2025 https://optics-design.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + */ + +#include "../test_framework.h" +#include "../../include/glass_data.h" +#include "../../include/glamac_view.h" +#include "../../include/glautils/fgla.h" + +// Test glass data loading and basic operations +int test_glass_data_operations() { + TEST_START("Glass Data Operations"); + + // Initialize glass data system + initialize_glass_data(); + + // Try to load JSON data + b32 json_loaded = load_glasses_from_json((const byte*)"tests/data/test_glasses.json", NULL); + + if (json_loaded) { + // Verify we have catalogs + u32 catalog_count = get_catalog_count(); + TEST_ASSERT(catalog_count > 0, "Should have loaded catalogs"); + + // Test each catalog + for (u32 i = 0; i < catalog_count; i++) { + set_current_catalog(i); + + const char* catalog_name = get_catalog_name(i); + TEST_ASSERT_NOT_NULL(catalog_name, "Each catalog should have a name"); + + u32 glass_count = get_glass_count(); + TEST_ASSERT(glass_count > 0, "Each catalog should have glasses"); + + // Test first glass in catalog + if (glass_count > 0) { + const Glass* glass = get_glass(0); + TEST_ASSERT_NOT_NULL(glass, "Should be able to get first glass"); + + // Validate glass data + TEST_ASSERT(glass->abbeNumber > 10.0f && glass->abbeNumber < 100.0f, + "Abbe number should be in reasonable range"); + TEST_ASSERT(glass->refractiveIndex > 1.0f && glass->refractiveIndex < 4.0f, + "Refractive index should be in reasonable range"); + + const byte* name = get_glass_name(0); + TEST_ASSERT_NOT_NULL(name, "Glass should have a name"); + TEST_ASSERT(strlen((const char*)name) > 0, "Glass name should not be empty"); + } + } + } else { + printf(YELLOW " Info: JSON test data not available - testing with default data" RESET "\n"); + + // Test with default data + u32 glass_count = get_glass_count(); + TEST_ASSERT(glass_count > 0, "Should have default glasses"); + } + + cleanup_glass_data(); + TEST_END(); +} + +// Test view system integration +int test_view_integration() { + TEST_START("View System Integration"); + + // Initialize systems + initialize_glass_data(); + ViewState view; + init_view_state(&view, 1024, 768); + + // Load test data if available + load_glasses_from_json((const byte*)"tests/data/test_glasses.json", NULL); + + // Refresh view with data + refresh_view_data_range(&view); + + // After refreshing, we should have valid ranges + TEST_ASSERT(view.minAbbe < view.maxAbbe, "Should have valid Abbe range"); + TEST_ASSERT(view.minRI < view.maxRI, "Should have valid RI range"); + + // Test coordinate transformations with actual glass data + u32 glass_count = get_glass_count(); + if (glass_count > 0) { + const Glass* glass = get_glass(0); + i32 screenX, screenY; + data_to_screen_coords(glass->abbeNumber, glass->refractiveIndex, &view, &screenX, &screenY); + + // Screen coordinates should be reasonable + TEST_ASSERT(screenX >= -200 && screenX <= view.windowWidth + 200, + "Screen X should be near window bounds"); + TEST_ASSERT(screenY >= -200 && screenY <= view.windowHeight + 200, + "Screen Y should be near window bounds"); + + // Test inverse transformation + f32 recovered_abbe, recovered_ri; + screen_to_data_coords(screenX, screenY, &view, &recovered_abbe, &recovered_ri); + + TEST_ASSERT_FLOAT_EQ(glass->abbeNumber, recovered_abbe, 1.0f, + "Should approximately recover original Abbe number"); + TEST_ASSERT_FLOAT_EQ(glass->refractiveIndex, recovered_ri, 0.01f, + "Should approximately recover original refractive index"); + } + + cleanup_glass_data(); + TEST_END(); +} + +// Test FGLA functionality with real search patterns +int test_fgla_functionality() { + TEST_START("FGLA Functionality"); + + // Test glass code pattern matching with realistic codes + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642", "517642"), + "Should match exact glass code"); + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642123", "517642"), + "Should match first 6 digits of longer code"); + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642", "51x64x"), + "Should match wildcard pattern"); + + // Test realistic search terms + TEST_ASSERT(fgla_validate_search_term("N-BK7"), "N-BK7 should be valid search term"); + TEST_ASSERT(fgla_validate_search_term("SF10"), "SF10 should be valid search term"); + TEST_ASSERT(fgla_validate_search_term("517642"), "517642 should be valid search term"); + TEST_ASSERT(fgla_validate_search_term("51x64x"), "51x64x should be valid search term"); + + // Test substring matching with realistic glass names + TEST_ASSERT(fgla_contains_substring_safe("N-BK7", "BK"), + "Should find BK in N-BK7"); + TEST_ASSERT(fgla_contains_substring_safe("SF10", "sf"), + "Should find sf in SF10 (case insensitive)"); + TEST_ASSERT(fgla_contains_substring_safe("FCD1", "fcd"), + "Should find fcd in FCD1 (case insensitive)"); + + // Test catalog matching with real manufacturers + const char* real_catalogs[] = {"SCHOTT", "HOYA", "CDGM", "Ohara"}; + TEST_ASSERT(fgla_matches_catalog("SCHOTT", real_catalogs, 4), + "Should match SCHOTT catalog"); + TEST_ASSERT(fgla_matches_catalog("hoya", real_catalogs, 4), + "Should match HOYA catalog (case insensitive)"); + TEST_ASSERT(!fgla_matches_catalog("UNKNOWN", real_catalogs, 4), + "Should not match unknown manufacturer"); + + TEST_END(); +} + +// Test error handling across systems +int test_error_handling() { + TEST_START("Error Handling"); + + // Test glass data error handling + b32 result = load_glasses_from_json((const byte*)"nonexistent.json", NULL); + TEST_ASSERT(!result, "Should fail gracefully with non-existent file"); + + result = load_glasses_from_json(NULL, NULL); + TEST_ASSERT(!result, "Should fail gracefully with NULL path"); + + // Test view system with edge case values + ViewState view; + init_view_state(&view, 1, 1); // Very small dimensions + TEST_ASSERT(view.windowWidth > 0, "Should handle small width"); + TEST_ASSERT(view.windowHeight > 0, "Should handle small height"); + + // Test FGLA with invalid inputs + TEST_ASSERT(!fgla_validate_search_term(NULL), "Should reject NULL search term"); + TEST_ASSERT(!fgla_validate_search_term(""), "Should reject empty search term"); + TEST_ASSERT(!fgla_is_glass_code_pattern(NULL), "Should reject NULL glass code pattern"); + + TEST_END(); +} + +// Test memory management +int test_memory_management() { + TEST_START("Memory Management"); + + // Test multiple initialize/cleanup cycles + for (int i = 0; i < 3; i++) { + initialize_glass_data(); + + // Load data if available + load_glasses_from_json((const byte*)"tests/data/test_glasses.json", NULL); + + // Use the data + u32 count = get_glass_count(); + if (count > 0) { + const Glass* glass = get_glass(0); + (void)glass; // Suppress unused variable warning + } + + // Cleanup + cleanup_glass_data(); + } + + // After all cycles, we should be able to initialize again + initialize_glass_data(); + u32 final_count = get_glass_count(); + TEST_ASSERT(final_count < 1000000, "Should have reasonable glass count after cleanup cycles"); + + cleanup_glass_data(); + TEST_END(); +} + +// Main integration test runner +int main() { + printf(BLUE "=== GlaMaC Integration Tests (Simplified) ===" RESET "\n\n"); + + RUN_TEST(test_glass_data_operations); + RUN_TEST(test_view_integration); + RUN_TEST(test_fgla_functionality); + RUN_TEST(test_error_handling); + RUN_TEST(test_memory_management); + + TEST_SUMMARY(); +} \ No newline at end of file diff --git a/tests/test_framework.h b/tests/test_framework.h new file mode 100644 index 0000000..1d71a97 --- /dev/null +++ b/tests/test_framework.h @@ -0,0 +1,120 @@ +/** + * test_framework.h - Simple unit testing framework for GlaMaC + * + * Copyright (C) 2025 https://optics-design.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + */ + +#ifndef TEST_FRAMEWORK_H +#define TEST_FRAMEWORK_H + +#include +#include +#include +#include + +// Test statistics +static int tests_run = 0; +static int tests_passed = 0; +static int tests_failed = 0; + +// ANSI color codes for output +#define RESET "\033[0m" +#define RED "\033[31m" +#define GREEN "\033[32m" +#define YELLOW "\033[33m" +#define BLUE "\033[34m" +#define MAGENTA "\033[35m" +#define CYAN "\033[36m" + +// Test macros +#define TEST_START(name) \ + printf(CYAN "Running test: %s" RESET "\n", name); \ + tests_run++; + +#define TEST_ASSERT(condition, message) \ + if (condition) { \ + printf(GREEN " ✓ PASS: %s" RESET "\n", message); \ + } else { \ + printf(RED " ✗ FAIL: %s" RESET "\n", message); \ + tests_failed++; \ + return 0; \ + } + +#define TEST_ASSERT_EQ(expected, actual, message) \ + if ((expected) == (actual)) { \ + printf(GREEN " ✓ PASS: %s (expected: %d, actual: %d)" RESET "\n", message, expected, actual); \ + } else { \ + printf(RED " ✗ FAIL: %s (expected: %d, actual: %d)" RESET "\n", message, expected, actual); \ + tests_failed++; \ + return 0; \ + } + +#define TEST_ASSERT_STR_EQ(expected, actual, message) \ + if (strcmp(expected, actual) == 0) { \ + printf(GREEN " ✓ PASS: %s" RESET "\n", message); \ + } else { \ + printf(RED " ✗ FAIL: %s (expected: '%s', actual: '%s')" RESET "\n", message, expected, actual); \ + tests_failed++; \ + return 0; \ + } + +#define TEST_ASSERT_FLOAT_EQ(expected, actual, tolerance, message) \ + if (fabs((expected) - (actual)) < (tolerance)) { \ + printf(GREEN " ✓ PASS: %s" RESET "\n", message); \ + } else { \ + printf(RED " ✗ FAIL: %s (expected: %.6f, actual: %.6f, diff: %.6f)" RESET "\n", \ + message, expected, actual, fabs((expected) - (actual))); \ + tests_failed++; \ + return 0; \ + } + +#define TEST_ASSERT_NULL(ptr, message) \ + if ((ptr) == NULL) { \ + printf(GREEN " ✓ PASS: %s" RESET "\n", message); \ + } else { \ + printf(RED " ✗ FAIL: %s (expected NULL, got non-NULL)" RESET "\n", message); \ + tests_failed++; \ + return 0; \ + } + +#define TEST_ASSERT_NOT_NULL(ptr, message) \ + if ((ptr) != NULL) { \ + printf(GREEN " ✓ PASS: %s" RESET "\n", message); \ + } else { \ + printf(RED " ✗ FAIL: %s (expected non-NULL, got NULL)" RESET "\n", message); \ + tests_failed++; \ + return 0; \ + } + +#define TEST_END() \ + tests_passed++; \ + printf(GREEN "Test completed successfully" RESET "\n\n"); \ + return 1; + +// Test runner helpers +#define RUN_TEST(test_func) \ + if (test_func()) { \ + /* Test passed */ \ + } else { \ + /* Test failed - already handled in macros */ \ + } + +#define TEST_SUMMARY() \ + printf(MAGENTA "=== Test Summary ===" RESET "\n"); \ + printf("Total tests run: %d\n", tests_run); \ + printf(GREEN "Passed: %d" RESET "\n", tests_passed); \ + if (tests_failed > 0) { \ + printf(RED "Failed: %d" RESET "\n", tests_failed); \ + } else { \ + printf("Failed: 0\n"); \ + } \ + printf("Success rate: %.1f%%\n", tests_run > 0 ? (100.0 * tests_passed / tests_run) : 0.0); \ + printf(MAGENTA "===================" RESET "\n"); \ + return tests_failed == 0 ? 0 : 1; + +#endif /* TEST_FRAMEWORK_H */ \ No newline at end of file diff --git a/tests/unit/test_fgla.c b/tests/unit/test_fgla.c new file mode 100644 index 0000000..60f4e7b --- /dev/null +++ b/tests/unit/test_fgla.c @@ -0,0 +1,231 @@ +/** + * test_fgla.c - Unit tests for fgla utility functions + * + * Copyright (C) 2025 https://optics-design.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + */ + +#include "../test_framework.h" +#include "../../include/glautils/fgla.h" + +// Test string validation +int test_search_term_validation() { + TEST_START("Search Term Validation"); + + // Valid search terms + TEST_ASSERT(fgla_validate_search_term("BK7"), "BK7 should be valid"); + TEST_ASSERT(fgla_validate_search_term("N-SF6"), "N-SF6 should be valid"); + TEST_ASSERT(fgla_validate_search_term("517642"), "517642 should be valid"); + TEST_ASSERT(fgla_validate_search_term("51x64x"), "51x64x should be valid"); + TEST_ASSERT(fgla_validate_search_term("FK51A"), "FK51A should be valid"); + + // Invalid search terms + TEST_ASSERT(!fgla_validate_search_term(NULL), "NULL should be invalid"); + TEST_ASSERT(!fgla_validate_search_term(""), "Empty string should be invalid"); + TEST_ASSERT(!fgla_validate_search_term("test@#$"), "Special characters should be invalid"); + TEST_ASSERT(!fgla_validate_search_term("test|pipe"), "Pipe character should be invalid"); + + // Test boundary conditions + char long_term[300]; + memset(long_term, 'A', sizeof(long_term) - 1); + long_term[sizeof(long_term) - 1] = '\0'; + TEST_ASSERT(!fgla_validate_search_term(long_term), "Overly long term should be invalid"); + + TEST_END(); +} + +// Test glass code pattern recognition +int test_glass_code_pattern() { + TEST_START("Glass Code Pattern Recognition"); + + // Valid glass code patterns + TEST_ASSERT(fgla_is_glass_code_pattern("517642"), "517642 should be glass code pattern"); + TEST_ASSERT(fgla_is_glass_code_pattern("123456"), "123456 should be glass code pattern"); + TEST_ASSERT(fgla_is_glass_code_pattern("51x64x"), "51x64x should be glass code pattern"); + TEST_ASSERT(fgla_is_glass_code_pattern("x12345"), "x12345 should be glass code pattern"); + TEST_ASSERT(fgla_is_glass_code_pattern("xxxxxx"), "xxxxxx should be glass code pattern"); + + // Invalid patterns + TEST_ASSERT(!fgla_is_glass_code_pattern("51764"), "51764 (5 digits) should not be glass code pattern"); + TEST_ASSERT(!fgla_is_glass_code_pattern("5176420"), "5176420 (7 digits) should not be glass code pattern"); + TEST_ASSERT(!fgla_is_glass_code_pattern("BK7123"), "BK7123 (letters) should not be glass code pattern"); + TEST_ASSERT(!fgla_is_glass_code_pattern("51-642"), "51-642 (dash) should not be glass code pattern"); + TEST_ASSERT(!fgla_is_glass_code_pattern(NULL), "NULL should not be glass code pattern"); + + TEST_END(); +} + +// Test glass code pattern matching +int test_glass_code_matching() { + TEST_START("Glass Code Pattern Matching"); + + // Exact matches + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642", "517642"), + "Exact match should work"); + + // Wildcard matches + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642", "51x64x"), + "Wildcard pattern should match"); + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642", "x17642"), + "Leading wildcard should match"); + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642", "51764x"), + "Trailing wildcard should match"); + + // Non-matches + TEST_ASSERT(!fgla_matches_glass_code_pattern_safe("517642", "518642"), + "Different digit should not match"); + TEST_ASSERT(!fgla_matches_glass_code_pattern_safe("517642", "51x63x"), + "Wrong wildcard pattern should not match"); + + // Handle longer glass codes (should extract first 6 digits) + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("5176420123", "517642"), + "Should match first 6 digits of longer code"); + + // Edge cases + TEST_ASSERT(!fgla_matches_glass_code_pattern_safe(NULL, "517642"), + "NULL glass code should not match"); + TEST_ASSERT(!fgla_matches_glass_code_pattern_safe("517642", NULL), + "NULL pattern should not match"); + + TEST_END(); +} + +// Test string normalization +int test_string_normalization() { + TEST_START("String Normalization"); + + char output[100]; + + // Test basic normalization + int result = fgla_normalize_string_safe("N-BK7", output, sizeof(output)); + TEST_ASSERT_EQ(0, result, "Normalization should succeed"); + TEST_ASSERT_STR_EQ("nbk7", output, "Should remove dash and convert to lowercase"); + + // Test with multiple dashes + result = fgla_normalize_string_safe("N-SF-6", output, sizeof(output)); + TEST_ASSERT_EQ(0, result, "Multi-dash normalization should succeed"); + TEST_ASSERT_STR_EQ("nsf6", output, "Should remove all dashes"); + + // Test with no dashes + result = fgla_normalize_string_safe("BK7", output, sizeof(output)); + TEST_ASSERT_EQ(0, result, "No-dash normalization should succeed"); + TEST_ASSERT_STR_EQ("bk7", output, "Should just convert to lowercase"); + + // Test error conditions + result = fgla_normalize_string_safe(NULL, output, sizeof(output)); + TEST_ASSERT_EQ(-1, result, "NULL input should return error"); + + result = fgla_normalize_string_safe("test", NULL, sizeof(output)); + TEST_ASSERT_EQ(-1, result, "NULL output should return error"); + + result = fgla_normalize_string_safe("test", output, 0); + TEST_ASSERT_EQ(-1, result, "Zero-size output should return error"); + + TEST_END(); +} + +// Test substring search +int test_substring_search() { + TEST_START("Substring Search"); + + // Case-insensitive search + TEST_ASSERT(fgla_contains_substring_safe("N-BK7", "bk"), + "Should find 'bk' in 'N-BK7' (case insensitive)"); + TEST_ASSERT(fgla_contains_substring_safe("N-BK7", "BK"), + "Should find 'BK' in 'N-BK7'"); + + // Dash-insensitive search + TEST_ASSERT(fgla_contains_substring_safe("N-BK7", "nbk"), + "Should find 'nbk' in 'N-BK7' (dash insensitive)"); + TEST_ASSERT(fgla_contains_substring_safe("NBK7", "n-bk"), + "Should find 'n-bk' in 'NBK7' (dash insensitive)"); + + // Non-matches + TEST_ASSERT(!fgla_contains_substring_safe("N-BK7", "sf"), + "Should not find 'sf' in 'N-BK7'"); + + // Edge cases + TEST_ASSERT(!fgla_contains_substring_safe(NULL, "test"), + "NULL haystack should return false"); + TEST_ASSERT(!fgla_contains_substring_safe("test", NULL), + "NULL needle should return false"); + TEST_ASSERT(!fgla_contains_substring_safe("", "test"), + "Empty haystack should return false"); + TEST_ASSERT(!fgla_contains_substring_safe("test", ""), + "Empty needle should return false"); + + TEST_END(); +} + +// Test catalog matching +int test_catalog_matching() { + TEST_START("Catalog Matching"); + + const char* catalogs[] = {"SCHOTT", "HOYA", "CDGM"}; + + // Should match + TEST_ASSERT(fgla_matches_catalog("SCHOTT", catalogs, 3), + "Should match SCHOTT"); + TEST_ASSERT(fgla_matches_catalog("schott", catalogs, 3), + "Should match schott (case insensitive)"); + TEST_ASSERT(fgla_matches_catalog("HOYA", catalogs, 3), + "Should match HOYA"); + + // Should not match + TEST_ASSERT(!fgla_matches_catalog("Ohara", catalogs, 3), + "Should not match Ohara (not in list)"); + + // Empty catalog list should match all + TEST_ASSERT(fgla_matches_catalog("SCHOTT", NULL, 0), + "Empty catalog list should match anything"); + TEST_ASSERT(fgla_matches_catalog("Unknown", NULL, 0), + "Empty catalog list should match anything"); + + TEST_END(); +} + +// Test lowercase conversion +int test_lowercase_conversion() { + TEST_START("Lowercase Conversion"); + + char test_str[20]; + + // Test normal case + strcpy(test_str, "BK7"); + fgla_to_lowercase_safe(test_str, sizeof(test_str)); + TEST_ASSERT_STR_EQ("bk7", test_str, "Should convert BK7 to bk7"); + + // Test mixed case + strcpy(test_str, "N-SF6"); + fgla_to_lowercase_safe(test_str, sizeof(test_str)); + TEST_ASSERT_STR_EQ("n-sf6", test_str, "Should convert N-SF6 to n-sf6"); + + // Test already lowercase + strcpy(test_str, "already"); + fgla_to_lowercase_safe(test_str, sizeof(test_str)); + TEST_ASSERT_STR_EQ("already", test_str, "Should not change already lowercase"); + + // Test with NULL (should not crash) + fgla_to_lowercase_safe(NULL, 10); // Should not crash + + TEST_END(); +} + +// Main test runner +int main() { + printf(BLUE "=== FGLA Utility Unit Tests ===" RESET "\n\n"); + + RUN_TEST(test_search_term_validation); + RUN_TEST(test_glass_code_pattern); + RUN_TEST(test_glass_code_matching); + RUN_TEST(test_string_normalization); + RUN_TEST(test_substring_search); + RUN_TEST(test_catalog_matching); + RUN_TEST(test_lowercase_conversion); + + TEST_SUMMARY(); +} \ No newline at end of file diff --git a/tests/unit/test_glamac_view.c b/tests/unit/test_glamac_view.c new file mode 100644 index 0000000..2decc3f --- /dev/null +++ b/tests/unit/test_glamac_view.c @@ -0,0 +1,243 @@ +/** + * test_glamac_view.c - Unit tests for view management + * + * Copyright (C) 2025 https://optics-design.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + */ + +#include "../test_framework.h" +#include "../../include/glamac_view.h" +#include "../../include/glass_data.h" + +// Test view initialization +int test_view_initialization() { + TEST_START("View Initialization"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Check initial values + TEST_ASSERT_FLOAT_EQ(0.0f, view.offsetX, 0.001f, "Initial X offset should be 0"); + TEST_ASSERT_FLOAT_EQ(0.0f, view.offsetY, 0.001f, "Initial Y offset should be 0"); + TEST_ASSERT_FLOAT_EQ(1.0f, view.zoomLevel, 0.001f, "Initial zoom should be 1.0"); + TEST_ASSERT_EQ(800, view.windowWidth, "Window width should be set correctly"); + TEST_ASSERT_EQ(600, view.windowHeight, "Window height should be set correctly"); + + TEST_END(); +} + +// Test coordinate transformations +int test_coordinate_transformations() { + TEST_START("Coordinate Transformations"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Initialize glass data for proper coordinate system + initialize_glass_data(); + + // Test glass to screen coordinate conversion + i32 screenX, screenY; + data_to_screen_coords(60.0f, 1.5f, &view, &screenX, &screenY); + + // Screen coordinates should be within window bounds for typical glass values + TEST_ASSERT(screenX >= 0 && screenX <= view.windowWidth, + "Screen X coordinate should be within window bounds"); + TEST_ASSERT(screenY >= 0 && screenY <= view.windowHeight, + "Screen Y coordinate should be within window bounds"); + + // Test screen to glass coordinate conversion (inverse operation) + f32 abbeNumber, refractiveIndex; + screen_to_data_coords(screenX, screenY, &view, &abbeNumber, &refractiveIndex); + + // The inverse transformation should approximately recover original values + TEST_ASSERT_FLOAT_EQ(60.0f, abbeNumber, 1.0f, + "Inverse transformation should recover Abbe number"); + TEST_ASSERT_FLOAT_EQ(1.5f, refractiveIndex, 0.01f, + "Inverse transformation should recover refractive index"); + + cleanup_glass_data(); + TEST_END(); +} + +// Test zoom functionality +int test_zoom_functionality() { + TEST_START("Zoom Functionality"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Test zoom in + f32 initial_zoom = view.zoomLevel; + handle_mouse_wheel_zoom(1, 400, 300, &view); // Zoom in at center + TEST_ASSERT(view.zoomLevel > initial_zoom, "Zoom should increase"); + TEST_ASSERT(view.zoomLevel <= MAX_ZOOM, "Zoom should not exceed maximum"); + + // Test zoom out + f32 zoomed_in = view.zoomLevel; + handle_mouse_wheel_zoom(-1, 400, 300, &view); // Zoom out at center + TEST_ASSERT(view.zoomLevel < zoomed_in, "Zoom should decrease"); + TEST_ASSERT(view.zoomLevel >= MIN_ZOOM, "Zoom should not go below minimum"); + + // Test zoom limits + for (int i = 0; i < 20; i++) { + handle_mouse_wheel_zoom(1, 400, 300, &view); // Zoom in a lot + } + TEST_ASSERT_FLOAT_EQ(MAX_ZOOM, view.zoomLevel, 0.001f, "Should be clamped to max zoom"); + + for (int i = 0; i < 20; i++) { + handle_mouse_wheel_zoom(-1, 400, 300, &view); // Zoom out a lot + } + TEST_ASSERT_FLOAT_EQ(MIN_ZOOM, view.zoomLevel, 0.001f, "Should be clamped to min zoom"); + + TEST_END(); +} + +// Test panning functionality +int test_panning_functionality() { + TEST_START("Panning Functionality"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Test panning + f32 initial_x = view.offsetX; + f32 initial_y = view.offsetY; + + pan_view(&view, 100, 50); // Pan right and up + TEST_ASSERT(view.offsetX != initial_x, "X offset should change with panning"); + TEST_ASSERT(view.offsetY != initial_y, "Y offset should change with panning"); + + // Test panning in opposite direction + pan_view(&view, -100, -50); // Pan left and down + TEST_ASSERT_FLOAT_EQ(initial_x, view.offsetX, 0.1f, + "Should return to approximately initial X after opposite panning"); + TEST_ASSERT_FLOAT_EQ(initial_y, view.offsetY, 0.1f, + "Should return to approximately initial Y after opposite panning"); + + TEST_END(); +} + +// Test view fitting +int test_view_fitting() { + TEST_START("View Fitting"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Initialize glass data for fitting + initialize_glass_data(); + + // Test fit to data + fit_view_to_data(&view); + + // After fitting, the view should be adjusted to show all data + // The exact values depend on the glass data, but zoom and offsets should be reasonable + TEST_ASSERT(view.zoomLevel > 0.0f, "Zoom should be positive after fitting"); + TEST_ASSERT(view.zoomLevel <= MAX_ZOOM, "Zoom should not exceed maximum after fitting"); + + // Offsets should be set to center the data + TEST_ASSERT(view.offsetX != 0.0f || view.offsetY != 0.0f, + "At least one offset should be non-zero after fitting (unless data is perfectly centered)"); + + cleanup_glass_data(); + TEST_END(); +} + +// Test clustering functionality +int test_clustering() { + TEST_START("Clustering"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Initialize glass data for clustering + initialize_glass_data(); + + // Update clustering + update_clustering(&view); + + // Check that clustering was performed + TEST_ASSERT(view.clusterCount >= 0, "Cluster count should be non-negative"); + TEST_ASSERT(view.clusterCount <= MAX_CLUSTERS, "Cluster count should not exceed maximum"); + + // If we have clusters, test their properties + for (u32 i = 0; i < view.clusterCount; i++) { + const GlassCluster* cluster = &view.clusters[i]; + TEST_ASSERT(cluster->count > 0, "Each cluster should have at least one glass"); + TEST_ASSERT(cluster->count <= MAX_CLUSTER_SIZE, "Cluster size should not exceed maximum"); + + // Center coordinates should be reasonable + TEST_ASSERT(cluster->centerX >= 0 && cluster->centerX < view.windowWidth, + "Cluster center X should be within window bounds"); + TEST_ASSERT(cluster->centerY >= 0 && cluster->centerY < view.windowHeight, + "Cluster center Y should be within window bounds"); + } + + cleanup_glass_data(); + TEST_END(); +} + +// Test view reset +int test_view_reset() { + TEST_START("View Reset"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Modify the view + view.offsetX = 100.0f; + view.offsetY = 50.0f; + view.zoomLevel = 2.0f; + + // Reset view + reset_view(&view); + + // Check that view is back to initial state + TEST_ASSERT_FLOAT_EQ(0.0f, view.offsetX, 0.001f, "X offset should be reset to 0"); + TEST_ASSERT_FLOAT_EQ(0.0f, view.offsetY, 0.001f, "Y offset should be reset to 0"); + TEST_ASSERT_FLOAT_EQ(1.0f, view.zoomLevel, 0.001f, "Zoom should be reset to 1.0"); + + TEST_END(); +} + +// Test window resize handling +int test_window_resize() { + TEST_START("Window Resize"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Test resize + resize_view(&view, 1024, 768); + + TEST_ASSERT_EQ(1024, view.windowWidth, "Window width should be updated"); + TEST_ASSERT_EQ(768, view.windowHeight, "Window height should be updated"); + + // Test with zero dimensions (should be handled gracefully) + resize_view(&view, 0, 0); + TEST_ASSERT(view.windowWidth > 0, "Window width should remain positive"); + TEST_ASSERT(view.windowHeight > 0, "Window height should remain positive"); + + TEST_END(); +} + +// Main test runner +int main() { + printf(BLUE "=== View Management Unit Tests ===" RESET "\n\n"); + + RUN_TEST(test_view_initialization); + RUN_TEST(test_coordinate_transformations); + RUN_TEST(test_zoom_functionality); + RUN_TEST(test_panning_functionality); + RUN_TEST(test_view_fitting); + RUN_TEST(test_clustering); + RUN_TEST(test_view_reset); + RUN_TEST(test_window_resize); + + TEST_SUMMARY(); +} \ No newline at end of file diff --git a/tests/unit/test_glamac_view_simple.c b/tests/unit/test_glamac_view_simple.c new file mode 100644 index 0000000..b17e4b9 --- /dev/null +++ b/tests/unit/test_glamac_view_simple.c @@ -0,0 +1,168 @@ +/** + * test_glamac_view_simple.c - Simplified unit tests for view management + * + * Copyright (C) 2025 https://optics-design.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + */ + +#include "../test_framework.h" +#include "../../include/glamac_view.h" +#include "../../include/glass_data.h" + +// Test view initialization +int test_view_initialization() { + TEST_START("View Initialization"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Check initial values + TEST_ASSERT_FLOAT_EQ(0.0f, view.offsetX, 0.001f, "Initial X offset should be 0"); + TEST_ASSERT_FLOAT_EQ(0.0f, view.offsetY, 0.001f, "Initial Y offset should be 0"); + TEST_ASSERT_FLOAT_EQ(1.0f, view.zoomLevel, 0.001f, "Initial zoom should be 1.0"); + TEST_ASSERT_EQ(800, view.windowWidth, "Window width should be set correctly"); + TEST_ASSERT_EQ(600, view.windowHeight, "Window height should be set correctly"); + + TEST_END(); +} + +// Test coordinate transformations +int test_coordinate_transformations() { + TEST_START("Coordinate Transformations"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Initialize glass data for proper coordinate system + initialize_glass_data(); + refresh_view_data_range(&view); + + // Test data to screen coordinate conversion + i32 screenX, screenY; + data_to_screen_coords(60.0f, 1.5f, &view, &screenX, &screenY); + + // Screen coordinates should be within reasonable bounds + TEST_ASSERT(screenX >= -100 && screenX <= view.windowWidth + 100, + "Screen X coordinate should be near window bounds"); + TEST_ASSERT(screenY >= -100 && screenY <= view.windowHeight + 100, + "Screen Y coordinate should be near window bounds"); + + // Test screen to data coordinate conversion (inverse operation) + f32 abbeNumber, refractiveIndex; + screen_to_data_coords(screenX, screenY, &view, &abbeNumber, &refractiveIndex); + + // The inverse transformation should approximately recover original values + TEST_ASSERT_FLOAT_EQ(60.0f, abbeNumber, 1.0f, + "Inverse transformation should recover Abbe number"); + TEST_ASSERT_FLOAT_EQ(1.5f, refractiveIndex, 0.01f, + "Inverse transformation should recover refractive index"); + + cleanup_glass_data(); + TEST_END(); +} + +// Test zoom functionality +int test_zoom_functionality() { + TEST_START("Zoom Functionality"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Test zoom in + f32 initial_zoom = view.zoomLevel; + handle_mouse_wheel_zoom(1, 400, 300, &view); // Zoom in at center + TEST_ASSERT(view.zoomLevel > initial_zoom, "Zoom should increase"); + TEST_ASSERT(view.zoomLevel <= MAX_ZOOM, "Zoom should not exceed maximum"); + + // Test zoom out + f32 zoomed_in = view.zoomLevel; + handle_mouse_wheel_zoom(-1, 400, 300, &view); // Zoom out at center + TEST_ASSERT(view.zoomLevel < zoomed_in, "Zoom should decrease"); + TEST_ASSERT(view.zoomLevel >= MIN_ZOOM, "Zoom should not go below minimum"); + + TEST_END(); +} + +// Test view reset +int test_view_reset() { + TEST_START("View Reset"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Modify the view + view.offsetX = 100.0f; + view.offsetY = 50.0f; + view.zoomLevel = 2.0f; + + // Reset view + reset_view(&view); + + // Check that view is back to initial state + TEST_ASSERT_FLOAT_EQ(0.0f, view.offsetX, 0.001f, "X offset should be reset to 0"); + TEST_ASSERT_FLOAT_EQ(0.0f, view.offsetY, 0.001f, "Y offset should be reset to 0"); + TEST_ASSERT_FLOAT_EQ(1.0f, view.zoomLevel, 0.001f, "Zoom should be reset to 1.0"); + + TEST_END(); +} + +// Test data range functionality +int test_data_range() { + TEST_START("Data Range"); + + ViewState view; + init_view_state(&view, 800, 600); + + initialize_glass_data(); + refresh_view_data_range(&view); + + // After refreshing, we should have valid data ranges + TEST_ASSERT(view.minAbbe < view.maxAbbe, "Min Abbe should be less than max Abbe"); + TEST_ASSERT(view.minRI < view.maxRI, "Min RI should be less than max RI"); + TEST_ASSERT(view.minAbbe > 0.0f, "Min Abbe should be positive"); + TEST_ASSERT(view.maxAbbe < 200.0f, "Max Abbe should be reasonable"); + TEST_ASSERT(view.minRI > 1.0f, "Min RI should be greater than 1.0"); + TEST_ASSERT(view.maxRI < 4.0f, "Max RI should be reasonable"); + + cleanup_glass_data(); + TEST_END(); +} + +// Test visible range calculation +int test_visible_range() { + TEST_START("Visible Range Calculation"); + + ViewState view; + init_view_state(&view, 800, 600); + + initialize_glass_data(); + refresh_view_data_range(&view); + + f32 visibleMinAbbe, visibleMaxAbbe, visibleMinRI, visibleMaxRI; + get_visible_data_range(&view, &visibleMinAbbe, &visibleMaxAbbe, &visibleMinRI, &visibleMaxRI); + + // Visible range should be valid + TEST_ASSERT(visibleMinAbbe < visibleMaxAbbe, "Visible min Abbe should be less than max"); + TEST_ASSERT(visibleMinRI < visibleMaxRI, "Visible min RI should be less than max"); + + cleanup_glass_data(); + TEST_END(); +} + +// Main test runner +int main() { + printf(BLUE "=== View Management Unit Tests (Simplified) ===" RESET "\n\n"); + + RUN_TEST(test_view_initialization); + RUN_TEST(test_coordinate_transformations); + RUN_TEST(test_zoom_functionality); + RUN_TEST(test_view_reset); + RUN_TEST(test_data_range); + RUN_TEST(test_visible_range); + + TEST_SUMMARY(); +} \ No newline at end of file diff --git a/tests/unit/test_glass_data.c b/tests/unit/test_glass_data.c new file mode 100644 index 0000000..3498747 --- /dev/null +++ b/tests/unit/test_glass_data.c @@ -0,0 +1,174 @@ +/** + * test_glass_data.c - Unit tests for glass data management + * + * Copyright (C) 2025 https://optics-design.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + */ + +#include "../test_framework.h" +#include "../../include/glass_data.h" +#include "../../include/glamac_errors.h" + +// Test glass data initialization +int test_glass_data_initialization() { + TEST_START("Glass Data Initialization"); + + initialize_glass_data(); + + // After initialization, we should have at least the default glasses + u32 count = get_glass_count(); + TEST_ASSERT(count > 0, "Should have at least some default glasses"); + + // Test that we can get the first glass + const Glass* glass = get_glass(0); + TEST_ASSERT_NOT_NULL(glass, "Should be able to get first glass"); + + // Test bounds checking - invalid index should return NULL + const Glass* invalid_glass = get_glass(999999); + TEST_ASSERT_NULL(invalid_glass, "Invalid index should return NULL"); + + TEST_END(); +} + +// Test glass data range calculation +int test_glass_data_range() { + TEST_START("Glass Data Range Calculation"); + + initialize_glass_data(); + + f32 minAbbe, maxAbbe, minRI, maxRI; + find_glass_data_range(&minAbbe, &maxAbbe, &minRI, &maxRI); + + // Validate that ranges make sense + TEST_ASSERT(minAbbe < maxAbbe, "Min Abbe should be less than max Abbe"); + TEST_ASSERT(minRI < maxRI, "Min RI should be less than max RI"); + TEST_ASSERT(minAbbe > 0.0f, "Min Abbe should be positive"); + TEST_ASSERT(maxAbbe < 200.0f, "Max Abbe should be reasonable (< 200)"); + TEST_ASSERT(minRI > 1.0f, "Min RI should be greater than 1.0"); + TEST_ASSERT(maxRI < 4.0f, "Max RI should be reasonable (< 4.0)"); + + TEST_END(); +} + +// Test JSON loading with test data +int test_json_loading() { + TEST_START("JSON Loading"); + + // Try to load test data + b32 result = load_glasses_from_json((const byte*)"tests/data/test_glasses.json", NULL); + + if (result) { + // Verify that we loaded some glasses + u32 count = get_glass_count(); + TEST_ASSERT(count > 0, "Should have loaded glasses from test JSON"); + + // Test catalog functionality + u32 catalog_count = get_catalog_count(); + TEST_ASSERT(catalog_count > 0, "Should have at least one catalog"); + + // Test catalog name retrieval + const char* catalog_name = get_catalog_name(0); + TEST_ASSERT_NOT_NULL(catalog_name, "Should be able to get catalog name"); + + // Test catalog switching + if (catalog_count > 1) { + set_current_catalog(1); + const char* second_catalog = get_current_catalog_name(); + TEST_ASSERT_NOT_NULL(second_catalog, "Should be able to switch catalogs"); + } + } else { + printf(YELLOW " Warning: Could not load test JSON file - this may be expected in some test environments" RESET "\n"); + } + + TEST_END(); +} + +// Test glass name retrieval +int test_glass_name_retrieval() { + TEST_START("Glass Name Retrieval"); + + initialize_glass_data(); + + u32 count = get_glass_count(); + if (count > 0) { + const byte* name = get_glass_name(0); + TEST_ASSERT_NOT_NULL(name, "Should be able to get glass name"); + + // Verify name is not empty + TEST_ASSERT(strlen((const char*)name) > 0, "Glass name should not be empty"); + } + + // Test invalid index + const byte* invalid_name = get_glass_name(999999); + TEST_ASSERT_NULL(invalid_name, "Invalid index should return NULL name"); + + TEST_END(); +} + +// Test catalog cycling +int test_catalog_cycling() { + TEST_START("Catalog Cycling"); + + initialize_glass_data(); + + u32 initial_catalog_count = get_catalog_count(); + + if (initial_catalog_count > 1) { + // Get initial catalog + const char* initial_name = get_current_catalog_name(); + TEST_ASSERT_NOT_NULL(initial_name, "Should have current catalog name"); + + // Cycle forward + cycle_catalog(1); + const char* next_name = get_current_catalog_name(); + TEST_ASSERT_NOT_NULL(next_name, "Should have catalog name after cycling"); + + // Cycle backward + cycle_catalog(-1); + const char* back_name = get_current_catalog_name(); + TEST_ASSERT_STR_EQ(initial_name, back_name, "Should return to initial catalog after cycling back"); + } else { + printf(YELLOW " Info: Only one catalog available - skipping cycling test" RESET "\n"); + } + + TEST_END(); +} + +// Test data validation with invalid data +int test_data_validation() { + TEST_START("Data Validation"); + + // This test would require access to internal validation functions + // For now, we'll test through the public API by trying to load invalid data + + // Try loading from non-existent file + b32 result = load_glasses_from_json((const byte*)"nonexistent_file.json", NULL); + TEST_ASSERT(!result, "Should fail to load non-existent file"); + + // Test with invalid file path (NULL) + b32 null_result = load_glasses_from_json(NULL, NULL); + TEST_ASSERT(!null_result, "Should fail with NULL path"); + + TEST_END(); +} + +// Main test runner +int main() { + printf(BLUE "=== Glass Data Unit Tests ===" RESET "\n\n"); + + RUN_TEST(test_glass_data_initialization); + RUN_TEST(test_glass_data_range); + RUN_TEST(test_json_loading); + RUN_TEST(test_glass_name_retrieval); + RUN_TEST(test_catalog_cycling); + RUN_TEST(test_data_validation); + + // Cleanup + cleanup_glass_data(); + + TEST_SUMMARY(); +} \ No newline at end of file -- cgit v1.2.3