summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/Makefile169
-rw-r--r--tests/data/test_glasses.json47
-rw-r--r--tests/fgla_test_internals.h28
-rw-r--r--tests/integration/test_full_pipeline.c273
-rw-r--r--tests/integration/test_simple_pipeline.c221
-rw-r--r--tests/test_framework.h120
-rw-r--r--tests/unit/test_fgla.c231
-rw-r--r--tests/unit/test_glamac_view.c243
-rw-r--r--tests/unit/test_glamac_view_simple.c168
-rw-r--r--tests/unit/test_glass_data.c174
10 files changed, 1674 insertions, 0 deletions
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-<name> - 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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+
+// 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
Back to https://optics-design.com