Refactor code structure for improved readability and maintainability
This commit is contained in:
Generated
-2
@@ -2897,8 +2897,6 @@ name = "hcie-text"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"approx",
|
"approx",
|
||||||
"eframe",
|
|
||||||
"egui",
|
|
||||||
"fontdue",
|
"fontdue",
|
||||||
"hcie-protocol",
|
"hcie-protocol",
|
||||||
"proptest",
|
"proptest",
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
@echo off
|
||||||
|
REM Canonical HCIE workspace build entry point (Windows)
|
||||||
|
REM Usage: build.bat [cargo-flags...]
|
||||||
|
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
set ROOT_DIR=%~dp0
|
||||||
|
|
||||||
|
if "%~1"=="" (
|
||||||
|
set CARGO_ARGS=--workspace
|
||||||
|
) else (
|
||||||
|
set CARGO_ARGS=%*
|
||||||
|
)
|
||||||
|
|
||||||
|
call "%ROOT_DIR%scripts\cargo-with-build-id.bat" build %CARGO_ARGS%
|
||||||
|
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
@echo off
|
||||||
|
REM Build AI/Vision dynamic library plugins and copy to plugins directory (Windows)
|
||||||
|
REM Usage: build_and_copy_plugins.bat [debug]
|
||||||
|
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
set PROFILE=release
|
||||||
|
set CARGO_FLAGS=--release
|
||||||
|
|
||||||
|
if /I "%~1"=="debug" (
|
||||||
|
set PROFILE=debug
|
||||||
|
set CARGO_FLAGS=
|
||||||
|
echo === Compiling plugins in DEBUG mode ===
|
||||||
|
) else (
|
||||||
|
echo === Compiling plugins in RELEASE mode ===
|
||||||
|
)
|
||||||
|
|
||||||
|
set PROJECT_DIR=C:\Projects\hcie-rust-v4
|
||||||
|
set PLUGINS_DIR=%PROJECT_DIR%\plugins
|
||||||
|
mkdir "%PLUGINS_DIR%" 2>nul
|
||||||
|
|
||||||
|
echo === Building AI/Vision dynamic libraries ===
|
||||||
|
|
||||||
|
echo 1. Building hcie-ai...
|
||||||
|
cargo build --manifest-path "%PROJECT_DIR%\hcie-ai\Cargo.toml" %CARGO_FLAGS% --features ffi
|
||||||
|
|
||||||
|
echo 2. Building hcie-vision...
|
||||||
|
cargo build --manifest-path "%PROJECT_DIR%\hcie-vision\Cargo.toml" %CARGO_FLAGS% --features ffi
|
||||||
|
|
||||||
|
REM On Windows, the output would be .dll files
|
||||||
|
REM Adjust the copy commands as needed for your Windows environment
|
||||||
|
copy "%PROJECT_DIR%\target\%PROFILE%\hcie_ai.dll" "%PLUGINS_DIR%\" 2>nul
|
||||||
|
copy "%PROJECT_DIR%\target\%PROFILE%\hcie_vision.dll" "%PLUGINS_DIR%\" 2>nul
|
||||||
|
|
||||||
|
echo ====================================================
|
||||||
|
echo AI/Vision plugins built and copied to %PLUGINS_DIR% in %PROFILE% mode!
|
||||||
|
echo ====================================================
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
@echo off
|
||||||
|
REM ============================================================
|
||||||
|
REM countlines.bat — HCIE-Rust Line Counter (Windows)
|
||||||
|
REM ============================================================
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
set PROJECT_DIR=%~dp0
|
||||||
|
set OUTPUT_FILE=line_count_report.txt
|
||||||
|
|
||||||
|
echo ========================================= > "%OUTPUT_FILE%"
|
||||||
|
echo LINE COUNT REPORT >> "%OUTPUT_FILE%"
|
||||||
|
echo Project: HCIE-Rust v4 >> "%OUTPUT_FILE%"
|
||||||
|
echo Location: %PROJECT_DIR% >> "%OUTPUT_FILE%"
|
||||||
|
echo Timestamp: %DATE% %TIME% >> "%OUTPUT_FILE%"
|
||||||
|
echo ========================================= >> "%OUTPUT_FILE%"
|
||||||
|
echo. >> "%OUTPUT_FILE%"
|
||||||
|
|
||||||
|
REM === RUST FILES (.rs) ===
|
||||||
|
echo -----------------------------------------
|
||||||
|
echo RUST (.rs) — hcie-* directories
|
||||||
|
echo -----------------------------------------
|
||||||
|
|
||||||
|
set GRAND_TOTAL_RUST=0
|
||||||
|
|
||||||
|
for /d %%d in ("%PROJECT_DIR%hcie-*") do (
|
||||||
|
set CRATE_LINES=0
|
||||||
|
for /r "%%d" %%f in (*.rs) do (
|
||||||
|
set "FPATH=%%f"
|
||||||
|
echo !FPATH! | findstr /i /c:"\tests\" >nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo !FPATH! | findstr /i "test_" >nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
for /f "usebackq" %%c in (`find /c /v "" "%%f"`) do (
|
||||||
|
if %%c gtr 0 (
|
||||||
|
set /a CRATE_LINES+=%%c
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if !CRATE_LINES! gtr 0 (
|
||||||
|
set "CRATE_NAME=%%~nxd"
|
||||||
|
echo !CRATE_NAME! !CRATE_LINES! lines
|
||||||
|
set /a GRAND_TOTAL_RUST+=!CRATE_LINES!
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo RUST TOTAL: %GRAND_TOTAL_RUST% lines
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM === GRAND TOTAL ===
|
||||||
|
echo ========================================= >> "%OUTPUT_FILE%"
|
||||||
|
echo GRAND TOTAL LINES OF CODE >> "%OUTPUT_FILE%"
|
||||||
|
echo ========================================= >> "%OUTPUT_FILE%"
|
||||||
|
echo Rust: %GRAND_TOTAL_RUST% >> "%OUTPUT_FILE%"
|
||||||
|
echo. >> "%OUTPUT_FILE%"
|
||||||
|
echo Report saved to: %CD%\%OUTPUT_FILE%
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Line count report saved to: %OUTPUT_FILE%
|
||||||
@@ -0,0 +1,494 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Integration tests for hcie-document crate.
|
||||||
|
///
|
||||||
|
/// Covers: document creation, layer CRUD, active layer switching,
|
||||||
|
/// visibility/opacity/blend-mode management, dirty tracking, zoom/pan,
|
||||||
|
/// undo/redo, selection, crop/resize.
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helper: create a small test document
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
fn make_doc() -> hcie_document::Document {
|
||||||
|
hcie_document::Document::new_blank("test", 64, 64, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_transparent_doc() -> hcie_document::Document {
|
||||||
|
hcie_document::Document::new_blank("transparent", 64, 64, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Document creation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#[test]
|
||||||
|
fn new_blank_document_has_correct_dimensions() {
|
||||||
|
let doc = make_doc();
|
||||||
|
assert_eq!(doc.canvas_width, 64);
|
||||||
|
assert_eq!(doc.canvas_height, 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_blank_document_has_one_layer() {
|
||||||
|
let doc = make_doc();
|
||||||
|
assert_eq!(doc.layers.len(), 1, "new blank document must have exactly one layer");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_blank_document_name_is_set() {
|
||||||
|
let doc = make_doc();
|
||||||
|
assert_eq!(doc.name, "test");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_blank_opaque_layer_is_white() {
|
||||||
|
let doc = make_doc();
|
||||||
|
let layer = doc.active_layer().unwrap();
|
||||||
|
let center = layer.get_pixel(32, 32);
|
||||||
|
assert_eq!(center, [255, 255, 255, 255], "opaque background should be white");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_transparent_layer_is_empty() {
|
||||||
|
let doc = make_transparent_doc();
|
||||||
|
let layer = doc.active_layer().unwrap();
|
||||||
|
let center = layer.get_pixel(32, 32);
|
||||||
|
assert_eq!(center, [0, 0, 0, 0], "transparent background should be all zeros");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn initial_zoom_is_one() {
|
||||||
|
let doc = make_doc();
|
||||||
|
assert!((doc.zoom - 1.0).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn initial_pan_is_zero() {
|
||||||
|
let doc = make_doc();
|
||||||
|
assert!((doc.pan_x).abs() < f32::EPSILON);
|
||||||
|
assert!((doc.pan_y).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tab label & modified flag
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#[test]
|
||||||
|
fn tab_label_shows_modified_indicator() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
assert!(!doc.tab_label().contains('●'), "clean doc should not show modified indicator");
|
||||||
|
doc.modified = true;
|
||||||
|
assert!(doc.tab_label().contains('●'), "modified doc should show the dot");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Layer CRUD
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#[test]
|
||||||
|
fn add_layer_increases_count() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
let id = doc.add_layer("Layer 2");
|
||||||
|
assert_eq!(doc.layers.len(), 2);
|
||||||
|
assert!(id > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_layer_makes_it_active() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
let id = doc.add_layer("Layer 2");
|
||||||
|
// add_layer pushes to the end → index 1 when background is at 0
|
||||||
|
assert_eq!(doc.active_layer, 1, "new layer should be active (last index)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_layer_reduces_count() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.add_layer("Layer 2");
|
||||||
|
doc.delete_layer(1);
|
||||||
|
assert_eq!(doc.layers.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_last_layer_removes_it() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
assert_eq!(doc.layers.len(), 1);
|
||||||
|
doc.delete_layer(0);
|
||||||
|
// The layer is removed; active_layer is clamped to the new (empty) len
|
||||||
|
assert_eq!(doc.layers.len(), 0, "deleting the only layer should remove it");
|
||||||
|
assert_eq!(doc.active_layer, 0, "active_layer should clamp to 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_layer_out_of_bounds_is_noop() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.delete_layer(99);
|
||||||
|
assert_eq!(doc.layers.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn move_layer_changes_order() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.add_layer("Layer A"); // index 1
|
||||||
|
doc.add_layer("Layer B"); // index 2
|
||||||
|
// Move "Layer B" down to index 1 (before "Layer A")
|
||||||
|
doc.move_layer(2, 1);
|
||||||
|
assert_eq!(doc.layers[0].name, "Background");
|
||||||
|
assert_eq!(doc.layers[1].name, "Layer B");
|
||||||
|
assert_eq!(doc.layers[2].name, "Layer A");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn move_layer_identity_is_noop() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.add_layer("L2");
|
||||||
|
doc.move_layer(1, 1);
|
||||||
|
assert_eq!(doc.layers.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_layer_ids_returns_all() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.add_layer("L2");
|
||||||
|
doc.add_layer("L3");
|
||||||
|
let ids = doc.all_layer_ids();
|
||||||
|
assert_eq!(ids.len(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layer_info_returns_correct_data() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
let id = doc.add_layer("MyLayer");
|
||||||
|
let info = doc.layer_info(id).unwrap();
|
||||||
|
assert_eq!(info.name, "MyLayer");
|
||||||
|
assert!(info.visible);
|
||||||
|
assert!((info.opacity - 1.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layer_info_returns_none_for_invalid_id() {
|
||||||
|
let doc = make_doc();
|
||||||
|
assert!(doc.layer_info(99999).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_layer_by_id_finds_layer() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
let id = doc.add_layer("Target");
|
||||||
|
let layer = doc.get_layer_by_id(id).unwrap();
|
||||||
|
assert_eq!(layer.name, "Target");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_layer_by_id_returns_none_for_missing() {
|
||||||
|
let doc = make_doc();
|
||||||
|
assert!(doc.get_layer_by_id(99999).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layer_index_by_id_returns_correct_index() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
let id = doc.add_layer("L2");
|
||||||
|
// add_layer pushes to the end; background is index 0, L2 is index 1
|
||||||
|
let idx = doc.layer_index_by_id(id).unwrap();
|
||||||
|
assert_eq!(idx, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Active layer
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#[test]
|
||||||
|
fn set_active_layer_switches_to_valid_index() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.add_layer("L2");
|
||||||
|
doc.set_active_layer(1);
|
||||||
|
assert_eq!(doc.active_layer, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn active_layer_returns_correct_layer() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
let layer = doc.active_layer().unwrap();
|
||||||
|
assert_eq!(layer.name, "Background");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn active_layer_mut_allows_modification() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
{
|
||||||
|
let layer = doc.active_layer_mut().unwrap();
|
||||||
|
layer.name = "Renamed".to_string();
|
||||||
|
}
|
||||||
|
assert_eq!(doc.layers[doc.active_layer].name, "Renamed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Visibility
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#[test]
|
||||||
|
fn set_layer_visible_hides_layer() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.set_layer_visible(0, false);
|
||||||
|
assert!(!doc.layers[0].visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_layer_visible_shows_layer() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.set_layer_visible(0, false);
|
||||||
|
doc.set_layer_visible(0, true);
|
||||||
|
assert!(doc.layers[0].visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layer_visible_default_is_true() {
|
||||||
|
let doc = make_doc();
|
||||||
|
assert!(doc.layers[0].visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Blend mode & opacity (via direct field access through mutable accessor)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#[test]
|
||||||
|
fn set_blend_mode_via_mut_accessor() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
let layer = doc.active_layer_mut().unwrap();
|
||||||
|
layer.blend_mode = hcie_protocol::BlendMode::Multiply;
|
||||||
|
assert_eq!(doc.layers[doc.active_layer].blend_mode, hcie_protocol::BlendMode::Multiply);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_opacity_via_mut_accessor() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
let layer = doc.active_layer_mut().unwrap();
|
||||||
|
layer.opacity = 0.5;
|
||||||
|
assert!((doc.layers[doc.active_layer].opacity - 0.5).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opacity_clamps_to_zero() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
let layer = doc.active_layer_mut().unwrap();
|
||||||
|
layer.opacity = -0.1;
|
||||||
|
// Protocol allows negative; consumer should clamp
|
||||||
|
assert!(layer.opacity < 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Dirty tracking
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#[test]
|
||||||
|
fn expand_dirty_produces_valid_bounds() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.expand_dirty(10, 10, 5.0);
|
||||||
|
let bounds = doc.dirty_bounds;
|
||||||
|
assert!(bounds.is_some(), "dirty bounds should be set after expand_dirty");
|
||||||
|
if let Some([x0, y0, x1, y1]) = bounds {
|
||||||
|
assert!(x0 <= x1);
|
||||||
|
assert!(y0 <= y1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clear_dirty_resets_bounds() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.expand_dirty(10, 10, 5.0);
|
||||||
|
doc.clear_dirty();
|
||||||
|
assert!(doc.dirty_bounds.is_none(), "dirty bounds should be None after clear");
|
||||||
|
assert!(!doc.composite_dirty, "composite_dirty should be false after clear");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expand_dirty_merges_with_existing() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.expand_dirty(0, 0, 1.0);
|
||||||
|
doc.expand_dirty(50, 50, 1.0);
|
||||||
|
let bounds = doc.dirty_bounds.unwrap();
|
||||||
|
// Should cover both regions
|
||||||
|
assert!(bounds[0] <= 1, "x0 should be near 0");
|
||||||
|
assert!(bounds[2] >= 49, "x1 should cover second region");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Zoom and pan
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#[test]
|
||||||
|
fn zoom_can_be_set() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.zoom = 2.0;
|
||||||
|
assert!((doc.zoom - 2.0).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zoom_can_be_negative() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.zoom = -1.0; // Negative zoom should be allowed (clamped at display layer)
|
||||||
|
assert!(doc.zoom < 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pan_can_be_set() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.pan_x = 100.0;
|
||||||
|
doc.pan_y = -50.0;
|
||||||
|
assert!((doc.pan_x - 100.0).abs() < f32::EPSILON);
|
||||||
|
assert!((doc.pan_y - (-50.0)).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Selection
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#[test]
|
||||||
|
fn set_selection_rect_activates_selection() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.set_selection_rect((10, 10), (30, 30));
|
||||||
|
assert!(doc.selection_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clear_selection_deactivates() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.set_selection_rect((10, 10), (30, 30));
|
||||||
|
doc.clear_selection();
|
||||||
|
assert!(!doc.selection_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn select_all_covers_full_canvas() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.select_all();
|
||||||
|
assert!(doc.selection_active);
|
||||||
|
if let Some(ref mask) = doc.selection_mask {
|
||||||
|
assert_eq!(mask.len(), (64 * 64) as usize);
|
||||||
|
assert!(mask.iter().all(|&v| v == 255), "select_all should set all mask bytes to 255");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_mask_at_returns_255_when_no_selection() {
|
||||||
|
let doc = make_doc();
|
||||||
|
assert_eq!(doc.get_mask_at(0, 0), 255, "without selection mask, get_mask_at should return 255");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_mask_at_returns_255_inside_selection() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.select_all();
|
||||||
|
assert_eq!(doc.get_mask_at(32, 32), 255, "inside selection should return 255");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Crop
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#[test]
|
||||||
|
fn crop_reduces_canvas_size() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.crop(0, 0, 32, 32);
|
||||||
|
assert_eq!(doc.canvas_width, 32);
|
||||||
|
assert_eq!(doc.canvas_height, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Resize
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#[test]
|
||||||
|
fn resize_canvas_increases_size() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.resize_canvas(128, 128);
|
||||||
|
assert_eq!(doc.canvas_width, 128);
|
||||||
|
assert_eq!(doc.canvas_height, 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resize_canvas_does_not_shrink_below_one() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.resize_canvas(0, 0);
|
||||||
|
// Minimum size should be at least 1
|
||||||
|
assert!(doc.canvas_width >= 1);
|
||||||
|
assert!(doc.canvas_height >= 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// History / Undo-Redo
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#[test]
|
||||||
|
fn new_document_has_no_undo() {
|
||||||
|
let doc = make_doc();
|
||||||
|
assert!(!doc.can_undo());
|
||||||
|
assert!(!doc.can_redo());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_layer_creates_undoable_action() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.add_layer("Undoable");
|
||||||
|
assert!(doc.can_undo(), "add_layer should record a history entry");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn undo_redo_layer_addition() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
let count_before = doc.layers.len();
|
||||||
|
doc.add_layer("Temp");
|
||||||
|
assert_eq!(doc.layers.len(), count_before + 1);
|
||||||
|
doc.undo();
|
||||||
|
// After undo, layers should be restored to previous state
|
||||||
|
assert!(doc.can_redo(), "should be able to redo after undo");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn history_len_increases_with_actions() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
assert_eq!(doc.history_len(), 0, "fresh document should have 0 history entries (initialization not recorded)");
|
||||||
|
doc.add_layer("Action 1");
|
||||||
|
doc.add_layer("Action 2");
|
||||||
|
assert!(doc.history_len() >= 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn history_description_is_readable() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.add_layer("My Layer");
|
||||||
|
if doc.history_len() > 0 {
|
||||||
|
let desc = doc.history_description(doc.history_len() - 1);
|
||||||
|
assert!(desc.is_some());
|
||||||
|
assert!(!desc.unwrap().is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn history_current_index_is_valid() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
assert!(doc.history_current() >= -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Layer pixel manipulation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#[test]
|
||||||
|
fn get_pixel_returns_correct_value() {
|
||||||
|
let doc = make_doc();
|
||||||
|
let pixel = doc.layers[0].get_pixel(0, 0);
|
||||||
|
assert_eq!(pixel.len(), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Layer data & type
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#[test]
|
||||||
|
fn layer_default_type_is_raster() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
let id = doc.add_layer("Raster");
|
||||||
|
let info = doc.layer_info(id).unwrap();
|
||||||
|
assert!(matches!(info.layer_type, hcie_protocol::LayerType::Raster));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_path_defaults_to_none() {
|
||||||
|
let doc = make_doc();
|
||||||
|
assert!(doc.file_path.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_path_can_be_set() {
|
||||||
|
let mut doc = make_doc();
|
||||||
|
doc.file_path = Some(PathBuf::from("/tmp/test.hcie"));
|
||||||
|
assert_eq!(doc.file_path.as_ref().unwrap(), &PathBuf::from("/tmp/test.hcie"));
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
use hcie_filter::apply_filter;
|
||||||
|
|
||||||
|
/// Value-verification tests for hcie-filter.
|
||||||
|
///
|
||||||
|
/// Upgrades existing "does not panic" smoke tests to actual
|
||||||
|
/// correctness assertions with known input → known output checks.
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helper: create a small test layer with known pixel data
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
fn make_gradient_layer() -> hcie_protocol::Layer {
|
||||||
|
let mut layer = hcie_protocol::Layer::new_blank("gradient", 4, 4);
|
||||||
|
for y in 0..4 {
|
||||||
|
for x in 0..4 {
|
||||||
|
layer.set_pixel(x, y, [x as u8 * 64, y as u8 * 64, 128, 255]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layer
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_solid_layer(r: u8, g: u8, b: u8, a: u8) -> hcie_protocol::Layer {
|
||||||
|
let mut layer = hcie_protocol::Layer::new_blank("solid", 4, 4);
|
||||||
|
for y in 0..4 {
|
||||||
|
for x in 0..4 {
|
||||||
|
layer.set_pixel(x, y, [r, g, b, a]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layer
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_1x1_layer(r: u8, g: u8, b: u8, a: u8) -> hcie_protocol::Layer {
|
||||||
|
let mut layer = hcie_protocol::Layer::new_blank("tiny", 1, 1);
|
||||||
|
layer.set_pixel(0, 0, [r, g, b, a]);
|
||||||
|
layer
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Invert filter — exact value verification
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invert_white_becomes_black() {
|
||||||
|
let mut layer = make_solid_layer(255, 255, 255, 255);
|
||||||
|
apply_filter(&mut layer, "invert", &serde_json::json!({}));
|
||||||
|
let pixel = layer.get_pixel(0, 0);
|
||||||
|
assert_eq!(pixel[0], 0, "invert of white R should be 0");
|
||||||
|
assert_eq!(pixel[1], 0, "invert of white G should be 0");
|
||||||
|
assert_eq!(pixel[2], 0, "invert of white B should be 0");
|
||||||
|
// Alpha typically unchanged
|
||||||
|
assert_eq!(pixel[3], 255, "invert should leave alpha unchanged");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invert_black_becomes_white() {
|
||||||
|
let mut layer = make_solid_layer(0, 0, 0, 255);
|
||||||
|
apply_filter(&mut layer, "invert", &serde_json::json!({}));
|
||||||
|
let pixel = layer.get_pixel(0, 0);
|
||||||
|
assert_eq!(pixel[0], 255);
|
||||||
|
assert_eq!(pixel[1], 255);
|
||||||
|
assert_eq!(pixel[2], 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invert_red_becomes_cyan() {
|
||||||
|
let mut layer = make_solid_layer(255, 0, 0, 255);
|
||||||
|
apply_filter(&mut layer, "invert", &serde_json::json!({}));
|
||||||
|
let pixel = layer.get_pixel(0, 0);
|
||||||
|
assert_eq!(pixel[0], 0, "invert red: R should be 0");
|
||||||
|
assert_eq!(pixel[1], 255, "invert red: G should be 255");
|
||||||
|
assert_eq!(pixel[2], 255, "invert red: B should be 255");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invert_preserves_transparency() {
|
||||||
|
let mut layer = make_solid_layer(100, 150, 200, 0);
|
||||||
|
apply_filter(&mut layer, "invert", &serde_json::json!({}));
|
||||||
|
let pixel = layer.get_pixel(0, 0);
|
||||||
|
assert_eq!(pixel[3], 0, "invert should preserve zero alpha");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invert_invert_is_identity() {
|
||||||
|
let original = make_gradient_layer();
|
||||||
|
let mut layer = make_gradient_layer();
|
||||||
|
apply_filter(&mut layer, "invert", &serde_json::json!({}));
|
||||||
|
apply_filter(&mut layer, "invert", &serde_json::json!({}));
|
||||||
|
for y in 0..4 {
|
||||||
|
for x in 0..4 {
|
||||||
|
assert_eq!(
|
||||||
|
layer.get_pixel(x, y),
|
||||||
|
original.get_pixel(x, y),
|
||||||
|
"double invert should restore original at ({},{})", x, y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Grayscale filter — value verification
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grayscale_makes_rgb_equal() {
|
||||||
|
let mut layer = make_gradient_layer();
|
||||||
|
apply_filter(&mut layer, "grayscale", &serde_json::json!({}));
|
||||||
|
for y in 0..4 {
|
||||||
|
for x in 0..4 {
|
||||||
|
let p = layer.get_pixel(x, y);
|
||||||
|
let diff_rg = (p[0] as i16 - p[1] as i16).abs();
|
||||||
|
let diff_rb = (p[0] as i16 - p[2] as i16).abs();
|
||||||
|
let diff_gb = (p[1] as i16 - p[2] as i16).abs();
|
||||||
|
// Allow small tolerance for integer rounding
|
||||||
|
assert!(
|
||||||
|
diff_rg <= 3 && diff_rb <= 3 && diff_gb <= 3,
|
||||||
|
"grayscale should produce R≈G≈B at ({},{}): got {:?}",
|
||||||
|
x, y, p
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grayscale_identity_on_gray_input() {
|
||||||
|
let mut layer = make_solid_layer(128, 128, 128, 255);
|
||||||
|
apply_filter(&mut layer, "grayscale", &serde_json::json!({}));
|
||||||
|
let pixel = layer.get_pixel(0, 0);
|
||||||
|
assert_eq!(pixel[0], 128);
|
||||||
|
assert_eq!(pixel[1], 128);
|
||||||
|
assert_eq!(pixel[2], 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grayscale_preserves_alpha() {
|
||||||
|
let mut layer = make_solid_layer(100, 150, 200, 128);
|
||||||
|
apply_filter(&mut layer, "grayscale", &serde_json::json!({}));
|
||||||
|
let pixel = layer.get_pixel(0, 0);
|
||||||
|
assert_eq!(pixel[3], 128, "grayscale should preserve alpha");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Brightness/Contrast filter
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn brightness_zero_is_identity() {
|
||||||
|
let original = make_gradient_layer();
|
||||||
|
let mut layer = make_gradient_layer();
|
||||||
|
// PS-style brightness/contrast: 0 brightness, 0 contrast should be identity
|
||||||
|
apply_filter(&mut layer, "brightness_contrast", &serde_json::json!({
|
||||||
|
"brightness": 0.0,
|
||||||
|
"contrast": 0.0
|
||||||
|
}));
|
||||||
|
for y in 0..4 {
|
||||||
|
for x in 0..4 {
|
||||||
|
let o = original.get_pixel(x, y);
|
||||||
|
let p = layer.get_pixel(x, y);
|
||||||
|
// Allow ±1 for rounding
|
||||||
|
assert!(
|
||||||
|
(o[0] as i16 - p[0] as i16).abs() <= 1 &&
|
||||||
|
(o[1] as i16 - p[1] as i16).abs() <= 1 &&
|
||||||
|
(o[2] as i16 - p[2] as i16).abs() <= 1,
|
||||||
|
"identity params should preserve pixel at ({},{}): orig={:?}, got={:?}",
|
||||||
|
x, y, o, p
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn brightness_changes_pixels() {
|
||||||
|
let mut layer = make_solid_layer(64, 64, 64, 255);
|
||||||
|
let before = layer.get_pixel(0, 0);
|
||||||
|
// PS-style brightness uses 0-100 scale
|
||||||
|
apply_filter(&mut layer, "brightness_contrast", &serde_json::json!({
|
||||||
|
"brightness": 50.0,
|
||||||
|
"contrast": 0.0
|
||||||
|
}));
|
||||||
|
let after = layer.get_pixel(0, 0);
|
||||||
|
assert!(
|
||||||
|
after[0] != before[0] || after[1] != before[1] || after[2] != before[2],
|
||||||
|
"brightness=50 should change at least one channel"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn brightness_negative_changes_pixels() {
|
||||||
|
let mut layer = make_solid_layer(128, 128, 128, 255);
|
||||||
|
let before = layer.get_pixel(0, 0);
|
||||||
|
// PS-style brightness uses 0-100 scale
|
||||||
|
apply_filter(&mut layer, "brightness_contrast", &serde_json::json!({
|
||||||
|
"brightness": -50.0,
|
||||||
|
"contrast": 0.0
|
||||||
|
}));
|
||||||
|
let after = layer.get_pixel(0, 0);
|
||||||
|
assert!(
|
||||||
|
after[0] != before[0] || after[1] != before[1] || after[2] != before[2],
|
||||||
|
"brightness=-50 should change at least one channel"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Edge cases: 1×1 images (minimal boundary)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invert_works_on_1x1() {
|
||||||
|
let mut layer = make_1x1_layer(128, 64, 32, 255);
|
||||||
|
apply_filter(&mut layer, "invert", &serde_json::json!({}));
|
||||||
|
let pixel = layer.get_pixel(0, 0);
|
||||||
|
assert_eq!(pixel[0], 127); // 255 - 128
|
||||||
|
assert_eq!(pixel[1], 191); // 255 - 64
|
||||||
|
assert_eq!(pixel[2], 223); // 255 - 32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grayscale_works_on_1x1() {
|
||||||
|
let mut layer = make_1x1_layer(100, 50, 200, 255);
|
||||||
|
apply_filter(&mut layer, "grayscale", &serde_json::json!({}));
|
||||||
|
let pixel = layer.get_pixel(0, 0);
|
||||||
|
// Grayscale: R, G, B should be approximately equal
|
||||||
|
let diff = (pixel[0] as i16 - pixel[1] as i16).abs()
|
||||||
|
.max((pixel[0] as i16 - pixel[2] as i16).abs())
|
||||||
|
.max((pixel[1] as i16 - pixel[2] as i16).abs());
|
||||||
|
assert!(diff <= 3, "grayscale on 1x1 should make R≈G≈B: {:?}", pixel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Unknown / missing parameters do not crash
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invert_with_empty_params_works() {
|
||||||
|
let mut layer = make_solid_layer(100, 100, 100, 255);
|
||||||
|
apply_filter(&mut layer, "invert", &serde_json::json!({}));
|
||||||
|
let pixel = layer.get_pixel(0, 0);
|
||||||
|
assert_eq!(pixel[0], 155);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invert_with_extra_params_does_not_crash() {
|
||||||
|
let mut layer = make_solid_layer(0, 0, 0, 255);
|
||||||
|
apply_filter(&mut layer, "invert", &serde_json::json!({
|
||||||
|
"unknown_param": 42,
|
||||||
|
"another_one": "hello"
|
||||||
|
}));
|
||||||
|
let pixel = layer.get_pixel(0, 0);
|
||||||
|
assert_eq!(pixel[0], 255, "invert with extra params should still work");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Filters preserve layer dimensions
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filter_preserves_dimensions() {
|
||||||
|
for filter_name in &["invert", "grayscale", "brightness_contrast", "box_blur"] {
|
||||||
|
let mut layer = make_gradient_layer();
|
||||||
|
apply_filter(&mut layer, filter_name, &serde_json::json!({}));
|
||||||
|
assert_eq!(layer.width, 4, "{} should preserve width", filter_name);
|
||||||
|
assert_eq!(layer.height, 4, "{} should preserve height", filter_name);
|
||||||
|
assert_eq!(layer.pixels.len(), 4 * 4 * 4, "{} should preserve pixel count", filter_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Multiple filters in sequence do not crash
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sequence_of_filters_does_not_crash() {
|
||||||
|
let mut layer = make_gradient_layer();
|
||||||
|
for &filter in &["invert", "grayscale", "brightness_contrast", "sharpen", "box_blur"] {
|
||||||
|
apply_filter(&mut layer, filter, &serde_json::json!({}));
|
||||||
|
}
|
||||||
|
// If we got here without panic, the test passes
|
||||||
|
assert_eq!(layer.width, 4);
|
||||||
|
}
|
||||||
@@ -2296,13 +2296,37 @@ impl HcieIcedApp {
|
|||||||
doc.selection_mask_dirty.set(true);
|
doc.selection_mask_dirty.set(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if `color` is within the proximity threshold of `last`,
|
||||||
|
/// meaning it is visually very similar and should be skipped to prevent
|
||||||
|
/// slider/wheel drag noise from flooding the recent colors list.
|
||||||
|
///
|
||||||
|
/// The threshold is ≤2 units per RGBA channel (unsigned comparison).
|
||||||
|
pub fn is_proximate_to_last(last: &[u8; 4], color: &[u8; 4]) -> bool {
|
||||||
|
let channel_diff = |a: u8, b: u8| -> u16 { (a as i16 - b as i16).unsigned_abs() as u16 };
|
||||||
|
channel_diff(last[0], color[0]) <= 2
|
||||||
|
&& channel_diff(last[1], color[1]) <= 2
|
||||||
|
&& channel_diff(last[2], color[2]) <= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HcieIcedApp {
|
||||||
/// Add a color to the recent colors list (most recent first, max 40).
|
/// Add a color to the recent colors list (most recent first, max 40).
|
||||||
///
|
///
|
||||||
/// Immediately persists to disk so committed colors survive a crash or non-graceful
|
/// Immediately persists to disk so committed colors survive a crash or non-graceful
|
||||||
/// shutdown. Color-wheel movement never calls this method; the final sampled color is
|
/// shutdown. Color-wheel movement never calls this method; the final sampled color is
|
||||||
/// committed once by `ColorDragEnded`.
|
/// committed once by `ColorDragEnded`.
|
||||||
|
///
|
||||||
|
/// Proximity dedup: if the new color is within the proximity threshold of the most recent
|
||||||
|
/// entry — checked by `is_proximate_to_last` — it is skipped. This prevents slider and
|
||||||
|
/// wheel drag noise from flooding the recent colors list with near-identical entries.
|
||||||
fn add_recent_color(&mut self, color: [u8; 4]) {
|
fn add_recent_color(&mut self, color: [u8; 4]) {
|
||||||
|
// Skip if visually very similar to the most recent entry (proximity dedup).
|
||||||
|
if let Some(last) = self.recent_colors.first() {
|
||||||
|
if is_proximate_to_last(last, &color) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
self.recent_colors.retain(|c| *c != color);
|
self.recent_colors.retain(|c| *c != color);
|
||||||
self.recent_colors.insert(0, color);
|
self.recent_colors.insert(0, color);
|
||||||
self.recent_colors.truncate(40);
|
self.recent_colors.truncate(40);
|
||||||
@@ -2611,8 +2635,10 @@ impl HcieIcedApp {
|
|||||||
|
|
||||||
Message::BgColorChanged(color) => {
|
Message::BgColorChanged(color) => {
|
||||||
self.bg_color = color;
|
self.bg_color = color;
|
||||||
self.add_recent_color(color);
|
if !self.color_dragging {
|
||||||
self.colors_dirty = true;
|
self.add_recent_color(color);
|
||||||
|
self.colors_dirty = true;
|
||||||
|
}
|
||||||
if !self.color_dragging {
|
if !self.color_dragging {
|
||||||
crate::shape_sync::sync_shape_from_tool(self);
|
crate::shape_sync::sync_shape_from_tool(self);
|
||||||
self.refresh_composite_if_needed();
|
self.refresh_composite_if_needed();
|
||||||
@@ -10116,6 +10142,53 @@ mod cycle_one_ux_tests {
|
|||||||
let union = union_regions(Some(first), [25, 5, 50, 35]);
|
let union = union_regions(Some(first), [25, 5, 50, 35]);
|
||||||
assert_eq!(union, [10, 5, 50, 40]);
|
assert_eq!(union, [10, 5, 50, 40]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Recent color proximity dedup ─────────────────────
|
||||||
|
|
||||||
|
/// Confirms an identical color IS proximate (diff=0 ≤ threshold).
|
||||||
|
/// In practice this means an exact duplicate click is deduped, which is
|
||||||
|
/// acceptable because the color is already at (or near) the front of the list.
|
||||||
|
#[test]
|
||||||
|
fn proximate_identical_color_is_skipped() {
|
||||||
|
let last = [100, 150, 200, 255];
|
||||||
|
let color = [100, 150, 200, 255];
|
||||||
|
// Identical → not proximate because the function returns true for
|
||||||
|
// threshold ≤2 per channel; identical (diff=0) is within threshold
|
||||||
|
// so `is_proximate_to_last` returns true.
|
||||||
|
assert!(super::is_proximate_to_last(&last, &color));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Confirms a slider-step color (+1 in one channel) IS proximate and skipped.
|
||||||
|
#[test]
|
||||||
|
fn proximate_small_slider_step_skipped() {
|
||||||
|
let last = [100, 150, 200, 255];
|
||||||
|
let slider_step = [101, 150, 200, 255];
|
||||||
|
assert!(super::is_proximate_to_last(&last, &slider_step));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Confirms a +3 change in all channels is NOT proximate (exceeds threshold).
|
||||||
|
#[test]
|
||||||
|
fn large_change_not_proximate() {
|
||||||
|
let last = [100, 150, 200, 255];
|
||||||
|
let far = [105, 155, 205, 255];
|
||||||
|
assert!(!super::is_proximate_to_last(&last, &far));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Confirms a single-channel +3 jump exceeds the threshold.
|
||||||
|
#[test]
|
||||||
|
fn three_unit_change_exceeds_threshold() {
|
||||||
|
let last = [100, 150, 200, 255];
|
||||||
|
let three_away = [103, 150, 200, 255];
|
||||||
|
assert!(!super::is_proximate_to_last(&last, &three_away));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Confirms small mixed changes are still within the proximity window.
|
||||||
|
#[test]
|
||||||
|
fn mixed_small_changes_are_proximate() {
|
||||||
|
let last = [100, 150, 200, 255];
|
||||||
|
let mixed_small = [101, 151, 201, 255];
|
||||||
|
assert!(super::is_proximate_to_last(&last, &mixed_small));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the topmost text layer whose rasterized pixels contain the point
|
/// Find the topmost text layer whose rasterized pixels contain the point
|
||||||
|
|||||||
@@ -16,3 +16,5 @@ egui = "0.34"
|
|||||||
rstest = "0.23"
|
rstest = "0.23"
|
||||||
proptest = "1.5"
|
proptest = "1.5"
|
||||||
approx = "0.5"
|
approx = "0.5"
|
||||||
|
proptest = "1.5"
|
||||||
|
approx = "0.5"
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -0,0 +1,241 @@
|
|||||||
|
use hcie_protocol::tools::{TextAlignment, TextOrientation, TextEffect};
|
||||||
|
use hcie_text::TextRenderer;
|
||||||
|
|
||||||
|
/// Integration tests for hcie-text text rendering.
|
||||||
|
///
|
||||||
|
/// Covers: font loading, font management, rasterization baseline,
|
||||||
|
/// error handling for missing fonts, fallback behavior,
|
||||||
|
/// create_text_layer and refresh_text_layer.
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helper: load a built-in font for testing
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
fn setup_renderer() -> (TextRenderer, bool) {
|
||||||
|
let mut renderer = TextRenderer::new();
|
||||||
|
let font_data = include_bytes!("../tests/fixtures/Inter-Regular.ttf");
|
||||||
|
let ok = renderer.load_font("Inter", font_data).is_ok();
|
||||||
|
(renderer, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Font management
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_renderer_has_no_fonts() {
|
||||||
|
let renderer = TextRenderer::new();
|
||||||
|
assert!(renderer.get_fonts().is_empty(), "new renderer should have no fonts");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_font_succeeds_with_valid_data() {
|
||||||
|
let mut renderer = TextRenderer::new();
|
||||||
|
// embed a small TTF for testing — must exist at this path
|
||||||
|
let font_data = include_bytes!("../tests/fixtures/Inter-Regular.ttf");
|
||||||
|
let result = renderer.load_font("Inter", font_data);
|
||||||
|
assert!(result.is_ok(), "loading a valid TTF should succeed: {:?}", result.err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_font_fails_with_invalid_data() {
|
||||||
|
let mut renderer = TextRenderer::new();
|
||||||
|
let result = renderer.load_font("Invalid", &[0u8, 1, 2, 3]);
|
||||||
|
assert!(result.is_err(), "loading garbage data as a font should fail");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn has_font_returns_true_after_loading() {
|
||||||
|
let mut renderer = TextRenderer::new();
|
||||||
|
let font_data = include_bytes!("../tests/fixtures/Inter-Regular.ttf");
|
||||||
|
if renderer.load_font("Inter", font_data).is_ok() {
|
||||||
|
assert!(renderer.has_font("Inter"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn has_font_returns_false_for_unloaded() {
|
||||||
|
let renderer = TextRenderer::new();
|
||||||
|
assert!(!renderer.has_font("NonexistentFont"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_fonts_returns_loaded_names() {
|
||||||
|
let mut renderer = TextRenderer::new();
|
||||||
|
let font_data = include_bytes!("../tests/fixtures/Inter-Regular.ttf");
|
||||||
|
if renderer.load_font("MyFont", font_data).is_ok() {
|
||||||
|
let fonts = renderer.get_fonts();
|
||||||
|
assert!(fonts.contains(&"MyFont".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Text rasterization (baseline — if font is available)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rasterize_text_returns_valid_output() {
|
||||||
|
let (mut renderer, font_ok) = setup_renderer();
|
||||||
|
if !font_ok {
|
||||||
|
eprintln!("Skipping: test font not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = renderer.rasterize_text(
|
||||||
|
"Hello",
|
||||||
|
"Inter",
|
||||||
|
24.0,
|
||||||
|
[0, 0, 0, 255], // black
|
||||||
|
0.0, 0.0, // x, y
|
||||||
|
0.0, // angle
|
||||||
|
TextAlignment::Left,
|
||||||
|
TextOrientation::Horizontal,
|
||||||
|
&[], // no effects
|
||||||
|
true, // anti-alias
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.is_ok(), "rasterize should succeed: {:?}", result.err());
|
||||||
|
|
||||||
|
let (pixels, w, h, _ox, _oy, _uw, _uh) = result.unwrap();
|
||||||
|
assert!(w > 0, "rasterized width should be > 0");
|
||||||
|
assert!(h > 0, "rasterized height should be > 0");
|
||||||
|
assert_eq!(pixels.len(), (w * h * 4) as usize);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rasterize_empty_string_produces_no_pixels_or_minimal() {
|
||||||
|
let (mut renderer, font_ok) = setup_renderer();
|
||||||
|
if !font_ok { return; }
|
||||||
|
|
||||||
|
let result = renderer.rasterize_text(
|
||||||
|
"", "Inter", 24.0, [0, 0, 0, 255],
|
||||||
|
0.0, 0.0, 0.0, TextAlignment::Left,
|
||||||
|
TextOrientation::Horizontal, &[], true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Empty string may return an error or a 0-size buffer; either is acceptable
|
||||||
|
match result {
|
||||||
|
Ok((pixels, w, h, _, _, _, _)) => {
|
||||||
|
assert!(w == 0 || h == 0 || pixels.is_empty(),
|
||||||
|
"empty string should produce trivial output: {}x{}", w, h);
|
||||||
|
}
|
||||||
|
Err(_) => { /* empty string may legitimately fail */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rasterize_returns_different_output_for_different_sizes() {
|
||||||
|
let (mut renderer, font_ok) = setup_renderer();
|
||||||
|
if !font_ok { return; }
|
||||||
|
|
||||||
|
let small = renderer.rasterize_text(
|
||||||
|
"A", "Inter", 12.0, [0, 0, 0, 255],
|
||||||
|
0.0, 0.0, 0.0, TextAlignment::Left,
|
||||||
|
TextOrientation::Horizontal, &[], true,
|
||||||
|
).unwrap();
|
||||||
|
let large = renderer.rasterize_text(
|
||||||
|
"A", "Inter", 48.0, [0, 0, 0, 255],
|
||||||
|
0.0, 0.0, 0.0, TextAlignment::Left,
|
||||||
|
TextOrientation::Horizontal, &[], true,
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
// Larger font size should produce taller output
|
||||||
|
assert!(large.2 >= small.2, "larger font should produce taller output");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Color handling
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rasterize_text_applies_color() {
|
||||||
|
let (mut renderer, font_ok) = setup_renderer();
|
||||||
|
if !font_ok { return; }
|
||||||
|
|
||||||
|
let result = renderer.rasterize_text(
|
||||||
|
"X", "Inter", 24.0, [255, 0, 0, 255], // red
|
||||||
|
0.0, 0.0, 0.0, TextAlignment::Left,
|
||||||
|
TextOrientation::Horizontal, &[], true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Ok((pixels, w, h, _, _, _, _)) = result {
|
||||||
|
if w > 0 && h > 0 {
|
||||||
|
// At least some pixels should have non-zero red channel
|
||||||
|
let has_red = pixels.iter().step_by(4).any(|&r| r > 0);
|
||||||
|
assert!(has_red, "text rasterized in red should have non-zero R channel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// create_text_layer
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_text_layer_produces_valid_layer() {
|
||||||
|
let (mut renderer, font_ok) = setup_renderer();
|
||||||
|
if !font_ok { return; }
|
||||||
|
|
||||||
|
let result = renderer.create_text_layer(
|
||||||
|
"Hello", "Inter", 24.0, [0, 0, 0, 255],
|
||||||
|
10.0, 20.0, 0.0, TextAlignment::Left,
|
||||||
|
TextOrientation::Horizontal, &[],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.is_ok(), "create_text_layer should succeed: {:?}", result.err());
|
||||||
|
let layer = result.unwrap();
|
||||||
|
assert!(layer.width > 0);
|
||||||
|
assert!(layer.height > 0);
|
||||||
|
assert!(layer.pixels.len() >= 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_text_layer_fails_for_missing_font() {
|
||||||
|
let mut renderer = TextRenderer::new();
|
||||||
|
let result = renderer.create_text_layer(
|
||||||
|
"Hello", "NonExistentFont", 24.0, [0, 0, 0, 255],
|
||||||
|
0.0, 0.0, 0.0, TextAlignment::Left,
|
||||||
|
TextOrientation::Horizontal, &[],
|
||||||
|
);
|
||||||
|
assert!(result.is_err(), "should fail when font is not loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Error handling
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rasterize_fails_for_missing_font() {
|
||||||
|
let mut renderer = TextRenderer::new();
|
||||||
|
let result = renderer.rasterize_text(
|
||||||
|
"Hi", "AbsentFont", 12.0, [0; 4],
|
||||||
|
0.0, 0.0, 0.0, TextAlignment::Left,
|
||||||
|
TextOrientation::Horizontal, &[], true,
|
||||||
|
);
|
||||||
|
assert!(result.is_err(), "rasterize with missing font should fail");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// refresh_text_layer
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn refresh_text_layer_does_not_panic() {
|
||||||
|
let (mut renderer, font_ok) = setup_renderer();
|
||||||
|
if !font_ok { return; }
|
||||||
|
|
||||||
|
let mut layer = match renderer.create_text_layer(
|
||||||
|
"Test", "Inter", 16.0, [0, 0, 0, 255],
|
||||||
|
0.0, 0.0, 0.0, TextAlignment::Left,
|
||||||
|
TextOrientation::Horizontal, &[],
|
||||||
|
) {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Skipping refresh test: cannot create layer: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = renderer.refresh_text_layer(&mut layer);
|
||||||
|
assert!(result.is_ok() || result.is_err());
|
||||||
|
// Either outcome is valid depending on the layer's state
|
||||||
|
}
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
use hcie_tile::*;
|
||||||
|
|
||||||
|
/// Integration tests for hcie-tile sparse tile storage.
|
||||||
|
///
|
||||||
|
/// Covers: tile creation, pixel read/write, tile key mapping,
|
||||||
|
/// sparse storage behavior, from_dense / to_dense roundtrip,
|
||||||
|
/// update_tiles_in_region, composite_into, edge cases.
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tile (single 256×256 block) tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_transparent_tile_is_all_zeros() {
|
||||||
|
let tile = Tile::new_transparent();
|
||||||
|
assert_eq!(tile.pixels.len(), TILE_BYTES);
|
||||||
|
// Spot-check a few positions
|
||||||
|
assert_eq!(tile.pixels[0], 0);
|
||||||
|
assert_eq!(tile.pixels[3], 0);
|
||||||
|
assert_eq!(tile.pixels[TILE_BYTES - 1], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_blank_tile_is_white_and_opaque() {
|
||||||
|
let tile = Tile::new_blank();
|
||||||
|
assert_eq!(tile.pixels.len(), TILE_BYTES);
|
||||||
|
// Every pixel should be [255, 255, 255, 255]
|
||||||
|
for i in 0..10 {
|
||||||
|
let base = i * 4;
|
||||||
|
assert_eq!(tile.pixels[base], 255, "R should be 255 at pixel {}", i);
|
||||||
|
assert_eq!(tile.pixels[base + 1], 255, "G should be 255 at pixel {}", i);
|
||||||
|
assert_eq!(tile.pixels[base + 2], 255, "B should be 255 at pixel {}", i);
|
||||||
|
assert_eq!(tile.pixels[base + 3], 255, "A should be 255 at pixel {}", i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// TiledLayer creation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_tiled_layer_has_correct_dimensions() {
|
||||||
|
let tl = TiledLayer::new(512, 256);
|
||||||
|
assert_eq!(tl.width(), 512);
|
||||||
|
assert_eq!(tl.height(), 256);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_tiled_layer_has_zero_tiles() {
|
||||||
|
let tl = TiledLayer::new(512, 256);
|
||||||
|
assert_eq!(tl.tile_count(), 0, "empty layer should have no allocated tiles");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_dense_size_matches() {
|
||||||
|
let pixels = vec![255u8; 64 * 64 * 4];
|
||||||
|
let tl = TiledLayer::from_dense(&pixels, 64, 64);
|
||||||
|
assert_eq!(tl.width(), 64);
|
||||||
|
assert_eq!(tl.height(), 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_dense_creates_tiles_for_non_transparent_data() {
|
||||||
|
let pixels = vec![255u8; 256 * 256 * 4]; // fully opaque, exactly one tile
|
||||||
|
let tl = TiledLayer::from_dense(&pixels, 256, 256);
|
||||||
|
assert_eq!(tl.tile_count(), 1, "fully opaque 256x256 should produce exactly 1 tile");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_dense_prunes_transparent_tiles() {
|
||||||
|
let pixels = vec![0u8; 256 * 256 * 4]; // fully transparent, exactly one tile
|
||||||
|
let tl = TiledLayer::from_dense(&pixels, 256, 256);
|
||||||
|
assert_eq!(
|
||||||
|
tl.tile_count(),
|
||||||
|
0,
|
||||||
|
"fully transparent layer should have no allocated tiles"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tile key mapping
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tile_key_maps_pixel_zero_to_zero_zero() {
|
||||||
|
let key = TiledLayer::tile_key(0, 0);
|
||||||
|
assert_eq!(key, (0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tile_key_maps_pixel_255_to_0() {
|
||||||
|
let key = TiledLayer::tile_key(255, 255);
|
||||||
|
assert_eq!(key, (0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tile_key_maps_pixel_256_to_1() {
|
||||||
|
let key = TiledLayer::tile_key(256, 256);
|
||||||
|
assert_eq!(key, (1, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tile_key_maps_large_coordinates() {
|
||||||
|
let key = TiledLayer::tile_key(1000, 2000);
|
||||||
|
assert_eq!(key, (1000 / TILE_SIZE, 2000 / TILE_SIZE));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Pixel read/write
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_pixel_outside_allocated_tile_returns_transparent() {
|
||||||
|
let tl = TiledLayer::new(512, 512);
|
||||||
|
let pixel = tl.get_pixel(100, 100);
|
||||||
|
assert_eq!(pixel, [0, 0, 0, 0], "unwritten pixel should be transparent");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_pixel_and_get_pixel_roundtrip() {
|
||||||
|
let mut tl = TiledLayer::new(64, 64);
|
||||||
|
tl.set_pixel(10, 10, [255, 128, 64, 200]);
|
||||||
|
let pixel = tl.get_pixel(10, 10);
|
||||||
|
assert_eq!(pixel, [255, 128, 64, 200]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_pixel_out_of_bounds_is_noop() {
|
||||||
|
let mut tl = TiledLayer::new(64, 64);
|
||||||
|
tl.set_pixel(100, 100, [255, 0, 0, 255]); // outside layer bounds
|
||||||
|
assert_eq!(tl.tile_count(), 0, "out-of-bounds set should not create tiles");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_pixel_auto_creates_tile() {
|
||||||
|
let mut tl = TiledLayer::new(256, 256);
|
||||||
|
tl.set_pixel(0, 0, [255, 0, 0, 255]);
|
||||||
|
assert_eq!(tl.tile_count(), 1, "setting a pixel should create the containing tile");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_pixel_written_twice_returns_last_value() {
|
||||||
|
let mut tl = TiledLayer::new(64, 64);
|
||||||
|
tl.set_pixel(5, 5, [10, 20, 30, 40]);
|
||||||
|
tl.set_pixel(5, 5, [50, 60, 70, 80]);
|
||||||
|
assert_eq!(tl.get_pixel(5, 5), [50, 60, 70, 80]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// to_dense roundtrip
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn to_dense_returns_correct_size() {
|
||||||
|
let tl = TiledLayer::new(64, 64);
|
||||||
|
let dense = tl.to_dense();
|
||||||
|
assert_eq!(dense.len(), (64 * 64 * 4) as usize);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn to_dense_after_set_pixel_preserves_value() {
|
||||||
|
let mut tl = TiledLayer::new(16, 16);
|
||||||
|
tl.set_pixel(7, 8, [100, 150, 200, 250]);
|
||||||
|
let dense = tl.to_dense();
|
||||||
|
let idx = ((8 * 16 + 7) * 4) as usize;
|
||||||
|
assert_eq!(dense[idx], 100);
|
||||||
|
assert_eq!(dense[idx + 1], 150);
|
||||||
|
assert_eq!(dense[idx + 2], 200);
|
||||||
|
assert_eq!(dense[idx + 3], 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_dense_to_dense_roundtrip() {
|
||||||
|
let original = vec![42u8; 128 * 128 * 4];
|
||||||
|
let tl = TiledLayer::from_dense(&original, 128, 128);
|
||||||
|
let result = tl.to_dense();
|
||||||
|
assert_eq!(original, result, "from_dense → to_dense should be identity");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_dense_to_dense_roundtrip_transparent() {
|
||||||
|
let original = vec![0u8; 128 * 128 * 4];
|
||||||
|
let tl = TiledLayer::from_dense(&original, 128, 128);
|
||||||
|
let result = tl.to_dense();
|
||||||
|
assert_eq!(original, result, "transparent from_dense → to_dense should be identity");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// update_tiles_in_region
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_tiles_in_region_writes_pixels() {
|
||||||
|
let mut tl = TiledLayer::new(64, 64);
|
||||||
|
let mut pixels = vec![0u8; 64 * 64 * 4];
|
||||||
|
// Set a block of pixels to red
|
||||||
|
for y in 10..20 {
|
||||||
|
for x in 10..20 {
|
||||||
|
let idx = ((y * 64 + x) * 4) as usize;
|
||||||
|
pixels[idx] = 255;
|
||||||
|
pixels[idx + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tl.update_tiles_in_region(&pixels, 64, 0, 0, 64, 64);
|
||||||
|
assert_eq!(tl.get_pixel(15, 15), [255, 0, 0, 255], "updated pixel should be red");
|
||||||
|
assert_eq!(tl.get_pixel(0, 0), [0, 0, 0, 0], "pixel outside update region stays unchanged");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_tiles_in_region_partial_update() {
|
||||||
|
let mut tl = TiledLayer::new(256, 256);
|
||||||
|
let mut pixels = vec![0u8; 64 * 64 * 4];
|
||||||
|
for i in (0..pixels.len()).step_by(4) {
|
||||||
|
pixels[i] = 255;
|
||||||
|
pixels[i + 3] = 255;
|
||||||
|
}
|
||||||
|
// Update only the sub-region (0,0)-(64,64)
|
||||||
|
tl.update_tiles_in_region(&pixels, 64, 0, 0, 64, 64);
|
||||||
|
assert_eq!(tl.get_pixel(32, 32), [255, 0, 0, 255]);
|
||||||
|
assert_eq!(tl.get_pixel(100, 100), [0, 0, 0, 0], "pixels outside region should be zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// composite_into
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn composite_into_copies_pixels() {
|
||||||
|
let mut tl = TiledLayer::new(32, 32);
|
||||||
|
tl.set_pixel(5, 5, [100, 150, 200, 255]);
|
||||||
|
|
||||||
|
let mut output = vec![0u8; 32 * 32 * 4];
|
||||||
|
tl.composite_into(&mut output, 32, 32, 0, 0, 32, 32);
|
||||||
|
|
||||||
|
let idx = ((5 * 32 + 5) * 4) as usize;
|
||||||
|
assert_eq!(output[idx], 100);
|
||||||
|
assert_eq!(output[idx + 1], 150);
|
||||||
|
assert_eq!(output[idx + 2], 200);
|
||||||
|
assert_eq!(output[idx + 3], 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn composite_into_respects_region_bounds() {
|
||||||
|
let mut tl = TiledLayer::new(64, 64);
|
||||||
|
tl.set_pixel(30, 30, [255, 0, 0, 255]);
|
||||||
|
|
||||||
|
let mut output = vec![0u8; 64 * 64 * 4];
|
||||||
|
// Composite only the top-left 16×16 region
|
||||||
|
tl.composite_into(&mut output, 64, 64, 0, 0, 16, 16);
|
||||||
|
|
||||||
|
// Pixel at (30,30) is outside the region and should not be copied
|
||||||
|
let idx = ((30 * 64 + 30) * 4) as usize;
|
||||||
|
assert_eq!(
|
||||||
|
output[idx..idx + 4],
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
"pixel outside region should not appear in output"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Edge cases
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_sized_layer_has_no_tiles() {
|
||||||
|
let tl = TiledLayer::new(0, 0);
|
||||||
|
assert_eq!(tl.tile_count(), 0);
|
||||||
|
assert_eq!(tl.width(), 0);
|
||||||
|
assert_eq!(tl.height(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn very_large_layer_does_not_panic() {
|
||||||
|
let tl = TiledLayer::new(4096, 4096);
|
||||||
|
assert_eq!(tl.width(), 4096);
|
||||||
|
assert_eq!(tl.height(), 4096);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tile_count_increases_with_written_area() {
|
||||||
|
let mut tl = TiledLayer::new(512, 512);
|
||||||
|
assert_eq!(tl.tile_count(), 0);
|
||||||
|
|
||||||
|
// Write one pixel in two different tiles
|
||||||
|
tl.set_pixel(0, 0, [1, 1, 1, 1]);
|
||||||
|
assert_eq!(tl.tile_count(), 1);
|
||||||
|
|
||||||
|
tl.set_pixel(300, 300, [2, 2, 2, 2]);
|
||||||
|
assert_eq!(tl.tile_count(), 2, "pixels in different tiles should create two tiles");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tiles_are_independent() {
|
||||||
|
let mut tl = TiledLayer::new(512, 512);
|
||||||
|
tl.set_pixel(0, 0, [10, 20, 30, 40]);
|
||||||
|
tl.set_pixel(300, 300, [50, 60, 70, 80]);
|
||||||
|
|
||||||
|
assert_eq!(tl.get_pixel(0, 0), [10, 20, 30, 40]);
|
||||||
|
assert_eq!(tl.get_pixel(300, 300), [50, 60, 70, 80]);
|
||||||
|
// neighboring pixel should be untouched
|
||||||
|
assert_eq!(tl.get_pixel(1, 0), [0, 0, 0, 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Serde roundtrip (if Tile is Serialize/Deserialize)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tile_serde_json_roundtrip() {
|
||||||
|
let tile = Tile::new_blank();
|
||||||
|
let json = serde_json::to_string(&tile).expect("serialize tile");
|
||||||
|
let restored: Tile = serde_json::from_str(&json).expect("deserialize tile");
|
||||||
|
assert_eq!(tile.pixels, restored.pixels, "JSON serde roundtrip should preserve pixels");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tile_bincode_roundtrip() {
|
||||||
|
let tile = Tile::new_blank();
|
||||||
|
let bytes = bincode::serialize(&tile).expect("serialize tile via bincode");
|
||||||
|
let restored: Tile = bincode::deserialize(&bytes).expect("deserialize tile via bincode");
|
||||||
|
assert_eq!(tile.pixels, restored.pixels, "bincode serde roundtrip should preserve pixels");
|
||||||
|
}
|
||||||
+25
@@ -4,6 +4,31 @@ cargo run --example gui
|
|||||||
cargo run -p hcie-iced-gui
|
cargo run -p hcie-iced-gui
|
||||||
scripts/cargo-with-build-id.sh run -p hcie-iced-gui
|
scripts/cargo-with-build-id.sh run -p hcie-iced-gui
|
||||||
|
|
||||||
|
##test
|
||||||
|
Running the Tests Automatically
|
||||||
|
To run only the new proximity dedup tests:
|
||||||
|
cargo test -p hcie-iced-gui -- proximate
|
||||||
|
|
||||||
|
To run all cycle_one_ux_tests (including the new ones):
|
||||||
|
cargo test -p hcie-iced-gui -- cycle_one_ux_tests
|
||||||
|
|
||||||
|
To run the FULL test suite for the iced GUI crate:
|
||||||
|
cargo test -p hcie-iced-gui
|
||||||
|
|
||||||
|
To watch tests and re-run on file changes, use:
|
||||||
|
cargo watch -x "test -p hcie-iced-gui -- proximate"
|
||||||
|
|
||||||
|
(Requires cargo install cargo-watch if not installed.)
|
||||||
|
|
||||||
|
To check compilation (without running tests):
|
||||||
|
cargo check -p hcie-iced-gui
|
||||||
|
|
||||||
|
I suggest adding a Makefile target or shell alias for quick invocation:
|
||||||
|
|
||||||
|
# ~/.bashrc or ~/.zshrc
|
||||||
|
alias ci-iced='cd /mnt/extra/00_PROJECTS/hcie-rust-v3.05 && cargo test -p hcie-iced-gui'
|
||||||
|
alias ci-iced-check='cd /mnt/extra/00_PROJECTS/hcie-rust-
|
||||||
|
|
||||||
##TAURI
|
##TAURI
|
||||||
npm komutunu proje kökünden değil, hcie-tauri-app/ altından çalıştırmalısın:
|
npm komutunu proje kökünden değil, hcie-tauri-app/ altından çalıştırmalısın:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
@echo off
|
||||||
|
REM ============================================================
|
||||||
|
REM HCIE-Rust v3.05 — Bulk All-Tests Execution Script (Windows)
|
||||||
|
REM Usage: run_all_tests.bat [--no-io] [--no-4k] [--ignored]
|
||||||
|
REM ============================================================
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
set ROOT_DIR=%~dp0
|
||||||
|
cd /d "%ROOT_DIR%" || exit /b 1
|
||||||
|
|
||||||
|
set PASS=0
|
||||||
|
set FAIL=0
|
||||||
|
set SKIP=0
|
||||||
|
set START_TIME=%TIME%
|
||||||
|
|
||||||
|
set SKIP_IO=0
|
||||||
|
set SKIP_4K=0
|
||||||
|
set INCLUDE_IGNORED=0
|
||||||
|
|
||||||
|
:parse_args
|
||||||
|
if "%~1"=="" goto :done_parse
|
||||||
|
if /I "%~1"=="--no-io" set SKIP_IO=1
|
||||||
|
if /I "%~1"=="--no-4k" set SKIP_4K=1
|
||||||
|
if /I "%~1"=="--ignored" set INCLUDE_IGNORED=1
|
||||||
|
shift
|
||||||
|
goto :parse_args
|
||||||
|
:done_parse
|
||||||
|
|
||||||
|
call :RUN_CRATE "hcie-protocol"
|
||||||
|
call :RUN_CRATE "hcie-color"
|
||||||
|
|
||||||
|
call :RUN_CRATE "hcie-blend"
|
||||||
|
call :RUN_CRATE "hcie-brush-engine"
|
||||||
|
call :RUN_CRATE "hcie-draw"
|
||||||
|
call :RUN_CRATE "hcie-composite"
|
||||||
|
call :RUN_CRATE "hcie-filter"
|
||||||
|
call :RUN_CRATE "hcie-selection"
|
||||||
|
call :RUN_CRATE "hcie-vector"
|
||||||
|
call :RUN_CRATE "hcie-history"
|
||||||
|
|
||||||
|
if %SKIP_IO%==1 (
|
||||||
|
echo [SKIP] hcie-io
|
||||||
|
set /a SKIP+=1
|
||||||
|
) else (
|
||||||
|
call :RUN_CRATE "hcie-io"
|
||||||
|
)
|
||||||
|
|
||||||
|
call :RUN_CRATE "hcie-psd"
|
||||||
|
call :RUN_CRATE "hcie-vision"
|
||||||
|
call :RUN_CRATE "hcie-build-info"
|
||||||
|
|
||||||
|
if %SKIP_4K%==1 (
|
||||||
|
echo [SKIP 4K benchmark in hcie-engine-api]
|
||||||
|
cargo test -p hcie-engine-api --skip benchmark_4k_stroke_on_multilayer_document
|
||||||
|
if !ERRORLEVEL!==0 ( set /a PASS+=1 ) else ( set /a FAIL+=1 )
|
||||||
|
) else (
|
||||||
|
call :RUN_CRATE "hcie-engine-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
call :RUN_CRATE "hcie-gui-egui"
|
||||||
|
call :RUN_CRATE "hcie-iced-gui"
|
||||||
|
|
||||||
|
call :RUN_CRATE "hcie-dry-media-brushes"
|
||||||
|
call :RUN_CRATE "hcie-paint-brushes"
|
||||||
|
call :RUN_CRATE "hcie-digital-brushes"
|
||||||
|
call :RUN_CRATE "hcie-watercolor-brushes"
|
||||||
|
call :RUN_CRATE "hcie-ink-brushes"
|
||||||
|
|
||||||
|
if %INCLUDE_IGNORED%==1 (
|
||||||
|
echo.
|
||||||
|
echo ===== Running IGNORED tests =====
|
||||||
|
cargo test -p hcie-engine-api -- --ignored
|
||||||
|
cargo test -p hcie-brush-engine -- --ignored
|
||||||
|
cargo test -p hcie-iced-gui -- --ignored
|
||||||
|
cargo test -p hcie-fx -- --ignored
|
||||||
|
)
|
||||||
|
|
||||||
|
echo ============================================================
|
||||||
|
echo BULK TEST EXECUTION COMPLETE
|
||||||
|
echo Crates passed: %PASS%
|
||||||
|
echo Crates failed: %FAIL%
|
||||||
|
echo Crates skipped: %SKIP%
|
||||||
|
echo Start time: %START_TIME%
|
||||||
|
echo ============================================================
|
||||||
|
exit /b %FAIL%
|
||||||
|
|
||||||
|
:RUN_CRATE
|
||||||
|
set CRATE=%~1
|
||||||
|
echo.
|
||||||
|
echo ===== Running: %CRATE% =====
|
||||||
|
cargo test -p %CRATE%
|
||||||
|
if %ERRORLEVEL%==0 ( set /a PASS+=1 ) else ( set /a FAIL+=1 )
|
||||||
|
goto :EOF
|
||||||
Executable
+139
@@ -0,0 +1,139 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================
|
||||||
|
# HCIE-Rust v3.05 — Bulk All-Tests Execution Script (bash)
|
||||||
|
# Usage: bash run_all_tests.sh [--no-io] [--no-4k] [--ignored]
|
||||||
|
#
|
||||||
|
# Flags:
|
||||||
|
# --no-io Skip hcie-io tests (slow, needs PSD fixtures)
|
||||||
|
# --no-4k Skip the 4K performance benchmark
|
||||||
|
# --ignored Also run #[ignore] tests (visual checks, benchmarks)
|
||||||
|
# ============================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
SKIP=0
|
||||||
|
START_TIME=$(date +%s)
|
||||||
|
|
||||||
|
SKIP_IO=false
|
||||||
|
SKIP_4K=false
|
||||||
|
INCLUDE_IGNORED=false
|
||||||
|
EXTRA_ARGS=()
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--no-io) SKIP_IO=true ;;
|
||||||
|
--no-4k) SKIP_4K=true ;;
|
||||||
|
--ignored) INCLUDE_IGNORED=true ;;
|
||||||
|
*) EXTRA_ARGS+=("$arg") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
SEPARATOR() { printf '%*s\n' 80 '' | tr ' ' '='; }
|
||||||
|
|
||||||
|
run_crate() {
|
||||||
|
local crate="$1"
|
||||||
|
local label="$2"
|
||||||
|
local extra="${3:-}"
|
||||||
|
|
||||||
|
SEPARATOR
|
||||||
|
echo "[$label] Running: cargo test -p $crate $extra"
|
||||||
|
SEPARATOR
|
||||||
|
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
if cargo test -p "$crate" $extra "${EXTRA_ARGS[@]+${EXTRA_ARGS[@]}}" 2>&1; then
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
display_summary() {
|
||||||
|
local elapsed=$(( $(date +%s) - START_TIME ))
|
||||||
|
echo ""
|
||||||
|
SEPARATOR
|
||||||
|
echo " BULK TEST EXECUTION COMPLETE"
|
||||||
|
echo " Crates passed: $PASS"
|
||||||
|
echo " Crates failed: $FAIL"
|
||||||
|
echo " Crates skipped: $SKIP"
|
||||||
|
echo " Elapsed time: ${elapsed}s"
|
||||||
|
SEPARATOR
|
||||||
|
[ "$FAIL" -eq 0 ] && echo " ALL CRATES PASSED" || echo " $FAIL crate(s) have failing tests"
|
||||||
|
exit "$FAIL"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
# Layer 1: DATA
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
run_crate "hcie-protocol" "Layer 1"
|
||||||
|
run_crate "hcie-color" "Layer 1"
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
# Layer 2: ENGINE CORE
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
run_crate "hcie-blend" "Layer 2"
|
||||||
|
run_crate "hcie-brush-engine" "Layer 2"
|
||||||
|
run_crate "hcie-draw" "Layer 2"
|
||||||
|
run_crate "hcie-composite" "Layer 2"
|
||||||
|
run_crate "hcie-filter" "Layer 2"
|
||||||
|
run_crate "hcie-selection" "Layer 2"
|
||||||
|
run_crate "hcie-vector" "Layer 2"
|
||||||
|
run_crate "hcie-history" "Layer 2"
|
||||||
|
|
||||||
|
if [ "$SKIP_IO" = true ]; then
|
||||||
|
echo "[SKIP] hcie-io (--no-io flag)"
|
||||||
|
SKIP=$((SKIP + 1))
|
||||||
|
else
|
||||||
|
run_crate "hcie-io" "Layer 2"
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_crate "hcie-psd" "Layer 2"
|
||||||
|
run_crate "hcie-vision" "Layer 2"
|
||||||
|
run_crate "hcie-build-info" "Layer 2"
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
# Layer 4: ENGINE API
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
if [ "$SKIP_4K" = true ]; then
|
||||||
|
run_crate "hcie-engine-api" "Layer 4" "--skip benchmark_4k_stroke_on_multilayer_document"
|
||||||
|
else
|
||||||
|
run_crate "hcie-engine-api" "Layer 4"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
# Layer 6: GUI — egui
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
run_crate "hcie-gui-egui" "Layer 6"
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
# Layer 6: GUI — iced
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
run_crate "hcie-iced-gui" "Layer 6"
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
# Brush Catalogs
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
run_crate "hcie-dry-media-brushes" "Brushes"
|
||||||
|
run_crate "hcie-paint-brushes" "Brushes"
|
||||||
|
run_crate "hcie-digital-brushes" "Brushes"
|
||||||
|
run_crate "hcie-watercolor-brushes" "Brushes"
|
||||||
|
run_crate "hcie-ink-brushes" "Brushes"
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
# Ignored tests (visual checks, benchmarks)
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
if [ "$INCLUDE_IGNORED" = true ]; then
|
||||||
|
echo ""
|
||||||
|
SEPARATOR
|
||||||
|
echo "Running IGNORED tests (visual checks, benchmarks)"
|
||||||
|
SEPARATOR
|
||||||
|
cargo test -p hcie-engine-api -- --ignored || true
|
||||||
|
cargo test -p hcie-brush-engine -- --ignored || true
|
||||||
|
cargo test -p hcie-iced-gui -- --ignored || true
|
||||||
|
cargo test -p hcie-fx -- --ignored || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
display_summary
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
@echo off
|
||||||
|
REM ============================================================
|
||||||
|
REM HCIE-Rust v3.05 — Categorized Test Runner (Windows .bat)
|
||||||
|
REM Usage: run_tests_categorized.bat [--no-io] [--no-4k] [--ignored]
|
||||||
|
REM ============================================================
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
set ROOT_DIR=%~dp0
|
||||||
|
cd /d "%ROOT_DIR%" || exit /b 1
|
||||||
|
|
||||||
|
echo ============================================================
|
||||||
|
echo HCIE-Rust v3.05 — Categorized Test Runner
|
||||||
|
echo Root: %ROOT_DIR%
|
||||||
|
echo ============================================================
|
||||||
|
|
||||||
|
set PASS=0
|
||||||
|
set FAIL=0
|
||||||
|
set SKIP=0
|
||||||
|
set SKIP_IO=0
|
||||||
|
set SKIP_4K=0
|
||||||
|
set INCLUDE_IGNORED=0
|
||||||
|
|
||||||
|
:parse_args
|
||||||
|
if "%~1"=="" goto :done_parse
|
||||||
|
if /I "%~1"=="--no-io" set SKIP_IO=1
|
||||||
|
if /I "%~1"=="--no-4k" set SKIP_4K=1
|
||||||
|
if /I "%~1"=="--ignored" set INCLUDE_IGNORED=1
|
||||||
|
shift
|
||||||
|
goto :parse_args
|
||||||
|
:done_parse
|
||||||
|
|
||||||
|
call :RUN_CATEGORY "Layer 1: DATA" hcie-protocol hcie-color
|
||||||
|
call :RUN_CATEGORY "Layer 2: Blend/Brush" hcie-blend hcie-brush-engine
|
||||||
|
call :RUN_CATEGORY "Layer 2: Draw/Composite/Filter" hcie-draw hcie-composite hcie-filter
|
||||||
|
call :RUN_CATEGORY "Layer 2: Selection/Vector/History" hcie-selection hcie-vector hcie-history
|
||||||
|
|
||||||
|
if %SKIP_IO%==1 (
|
||||||
|
echo [SKIP] hcie-io
|
||||||
|
set /a SKIP+=1
|
||||||
|
) else (
|
||||||
|
call :RUN_CRATE hcie-io
|
||||||
|
)
|
||||||
|
call :RUN_CATEGORY "Layer 2: PSD/Vision/Build" hcie-psd hcie-vision hcie-build-info
|
||||||
|
|
||||||
|
if %SKIP_4K%==1 (
|
||||||
|
echo [SKIP 4K benchmark in hcie-engine-api]
|
||||||
|
cargo test -p hcie-engine-api --skip benchmark_4k_stroke_on_multilayer_document
|
||||||
|
if !ERRORLEVEL!==0 ( set /a PASS+=1 ) else ( set /a FAIL+=1 )
|
||||||
|
) else (
|
||||||
|
call :RUN_CRATE hcie-engine-api
|
||||||
|
)
|
||||||
|
|
||||||
|
call :RUN_CATEGORY "Layer 6: GUI egui" hcie-gui-egui
|
||||||
|
call :RUN_CATEGORY "Layer 6: GUI iced" hcie-iced-gui
|
||||||
|
call :RUN_CATEGORY "Brush Catalogs" hcie-dry-media-brushes hcie-paint-brushes hcie-digital-brushes hcie-watercolor-brushes hcie-ink-brushes
|
||||||
|
|
||||||
|
if %INCLUDE_IGNORED%==1 (
|
||||||
|
echo.
|
||||||
|
echo ===== Running IGNORED tests =====
|
||||||
|
cargo test -p hcie-engine-api -- --ignored
|
||||||
|
cargo test -p hcie-brush-engine -- --ignored
|
||||||
|
cargo test -p hcie-iced-gui -- --ignored
|
||||||
|
cargo test -p hcie-fx -- --ignored
|
||||||
|
)
|
||||||
|
|
||||||
|
echo ============================================================
|
||||||
|
echo SUMMARY: %PASS% passed, %FAIL% failed, %SKIP% skipped
|
||||||
|
echo ============================================================
|
||||||
|
exit /b %FAIL%
|
||||||
|
|
||||||
|
:RUN_CATEGORY
|
||||||
|
set CATEGORY=%~1
|
||||||
|
shift
|
||||||
|
echo.
|
||||||
|
echo ===== Category: %CATEGORY% =====
|
||||||
|
:RUN_CATEGORY_LOOP
|
||||||
|
if "%~1"=="" goto :EOF
|
||||||
|
call :RUN_CRATE %~1
|
||||||
|
shift
|
||||||
|
goto RUN_CATEGORY_LOOP
|
||||||
|
|
||||||
|
:RUN_CRATE
|
||||||
|
set CRATE=%~1
|
||||||
|
echo --- Running: %CRATE% ---
|
||||||
|
cargo test -p %CRATE%
|
||||||
|
if %ERRORLEVEL%==0 ( set /a PASS+=1 ) else ( set /a FAIL+=1 )
|
||||||
|
goto :EOF
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================
|
||||||
|
# HCIE-Rust v3.05 — Categorized Test Runner (Linux shell)
|
||||||
|
# Usage: bash run_tests_categorized.sh [--no-io] [--no-4k] [--ignored]
|
||||||
|
# ============================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
SKIP=0
|
||||||
|
SKIP_IO=false
|
||||||
|
SKIP_4K=false
|
||||||
|
INCLUDE_IGNORED=false
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--no-io) SKIP_IO=true ;;
|
||||||
|
--no-4k) SKIP_4K=true ;;
|
||||||
|
--ignored) INCLUDE_IGNORED=true ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
SEPARATOR() { printf '%*s\n' 80 '' | tr ' ' '='; }
|
||||||
|
|
||||||
|
run_crate() {
|
||||||
|
local crate="$1"
|
||||||
|
local label="$2"
|
||||||
|
local extra="${3:-}"
|
||||||
|
|
||||||
|
SEPARATOR
|
||||||
|
echo "[$label] Running: cargo test -p $crate $extra"
|
||||||
|
SEPARATOR
|
||||||
|
|
||||||
|
if cargo test -p "$crate" $extra 2>&1; then
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Layer 1: DATA ──────────────────────────────────────────
|
||||||
|
run_crate "hcie-protocol" "DATA"
|
||||||
|
run_crate "hcie-color" "DATA"
|
||||||
|
|
||||||
|
# ── Layer 2: ENGINE CORE ───────────────────────────────────
|
||||||
|
run_crate "hcie-blend" "CORE"
|
||||||
|
run_crate "hcie-brush-engine" "CORE"
|
||||||
|
run_crate "hcie-draw" "CORE"
|
||||||
|
run_crate "hcie-composite" "CORE"
|
||||||
|
run_crate "hcie-filter" "CORE"
|
||||||
|
run_crate "hcie-selection" "CORE"
|
||||||
|
run_crate "hcie-vector" "CORE"
|
||||||
|
run_crate "hcie-history" "CORE"
|
||||||
|
|
||||||
|
if [ "$SKIP_IO" = true ]; then
|
||||||
|
echo "[SKIP] hcie-io"
|
||||||
|
SKIP=$((SKIP + 1))
|
||||||
|
else
|
||||||
|
run_crate "hcie-io" "CORE"
|
||||||
|
fi
|
||||||
|
run_crate "hcie-psd" "CORE"
|
||||||
|
run_crate "hcie-vision" "CORE"
|
||||||
|
run_crate "hcie-build-info" "CORE"
|
||||||
|
|
||||||
|
# ── Layer 4: ENGINE API ────────────────────────────────────
|
||||||
|
if [ "$SKIP_4K" = true ]; then
|
||||||
|
run_crate "hcie-engine-api" "ENGINE-API" "--skip benchmark_4k_stroke_on_multilayer_document"
|
||||||
|
else
|
||||||
|
run_crate "hcie-engine-api" "ENGINE-API"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Layer 6: GUI ───────────────────────────────────────────
|
||||||
|
run_crate "hcie-gui-egui" "GUI-egui"
|
||||||
|
run_crate "hcie-iced-gui" "GUI-iced"
|
||||||
|
|
||||||
|
# ── Brush Catalogs ─────────────────────────────────────────
|
||||||
|
run_crate "hcie-dry-media-brushes" "Brushes"
|
||||||
|
run_crate "hcie-paint-brushes" "Brushes"
|
||||||
|
run_crate "hcie-digital-brushes" "Brushes"
|
||||||
|
run_crate "hcie-watercolor-brushes" "Brushes"
|
||||||
|
run_crate "hcie-ink-brushes" "Brushes"
|
||||||
|
|
||||||
|
# ── Ignored tests (visual checks, benchmarks) ──────────────
|
||||||
|
if [ "$INCLUDE_IGNORED" = true ]; then
|
||||||
|
echo ""
|
||||||
|
SEPARATOR
|
||||||
|
echo "Running IGNORED tests (visual checks, benchmarks)"
|
||||||
|
SEPARATOR
|
||||||
|
cargo test -p hcie-engine-api -- --ignored || true
|
||||||
|
cargo test -p hcie-brush-engine -- --ignored || true
|
||||||
|
cargo test -p hcie-iced-gui -- --ignored || true
|
||||||
|
cargo test -p hcie-fx -- --ignored || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
SEPARATOR
|
||||||
|
echo "CATEGORIZED TEST RUN COMPLETE: $PASS passed, $FAIL failed, $SKIP skipped"
|
||||||
|
SEPARATOR
|
||||||
|
exit "$FAIL"
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
@echo off
|
||||||
|
REM Runs one Cargo command with one synchronized HCIE build-number increment (Windows)
|
||||||
|
REM Usage: scripts\cargo-with-build-id.bat <cargo-subcommand> [arguments...]
|
||||||
|
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
set ROOT_DIR=%~dp0..\
|
||||||
|
set LOCK_DIR=%ROOT_DIR%.build-id.lock
|
||||||
|
set COUNTER_FILE=%ROOT_DIR%build.id
|
||||||
|
|
||||||
|
if "%~1"=="" (
|
||||||
|
echo usage: scripts\cargo-with-build-id.bat ^<cargo-subcommand^> [arguments...]
|
||||||
|
exit /b 2
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Acquire lock (Windows-friendly using a temp file)
|
||||||
|
:acquire_lock
|
||||||
|
2>nul (
|
||||||
|
>>"%LOCK_DIR%\lock.tmp" echo(%DATE% %TIME%
|
||||||
|
) || (
|
||||||
|
REM Lock exists; wait and retry
|
||||||
|
timeout /t 1 /nobreak >nul
|
||||||
|
goto :acquire_lock
|
||||||
|
)
|
||||||
|
del "%LOCK_DIR%\lock.tmp" 2>nul
|
||||||
|
|
||||||
|
REM Read current build ID
|
||||||
|
if exist "%COUNTER_FILE%" (
|
||||||
|
set /p current=<"%COUNTER_FILE%"
|
||||||
|
) else (
|
||||||
|
set current=0
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Validate it's a number
|
||||||
|
echo %current%| findstr /r "^[0-9][0-9]*$" >nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo build.id must contain one unsigned integer, found: %current%
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
set /a next=current+1
|
||||||
|
|
||||||
|
REM Write counter files
|
||||||
|
echo %next%>"%COUNTER_FILE%"
|
||||||
|
echo %next%>"%ROOT_DIR%hcie-egui-app\build.id"
|
||||||
|
echo %next%>"%ROOT_DIR%hcie-egui-app\crates\hcie-gui-egui\build.id"
|
||||||
|
|
||||||
|
set HCIE_BUILD_ID_OVERRIDE=%next%
|
||||||
|
echo HCIE build %next%: cargo %*
|
||||||
|
|
||||||
|
cd /d "%ROOT_DIR%"
|
||||||
|
cargo %*
|
||||||
|
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
@echo off
|
||||||
|
REM Install repository Git hooks (Windows)
|
||||||
|
|
||||||
|
for /f "delims=" %%i in ('git rev-parse --show-toplevel') do set REPO_ROOT=%%i
|
||||||
|
cd /d "%REPO_ROOT%"
|
||||||
|
|
||||||
|
git config --local core.hooksPath .githooks
|
||||||
|
|
||||||
|
set CONFIGURED=
|
||||||
|
for /f "delims=" %%i in ('git config --local --get core.hooksPath') do set CONFIGURED=%%i
|
||||||
|
|
||||||
|
if not "%CONFIGURED%"==".githooks" (
|
||||||
|
echo Failed to configure repository Git hooks.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Repository hooks installed: core.hooksPath=.githooks
|
||||||
|
exit /b 0
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
@echo off
|
||||||
|
REM Pre-commit test gate for Windows
|
||||||
|
REM Inspects staged .rs files and runs affected crate tests
|
||||||
|
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
REM Get staged .rs files
|
||||||
|
set STAGED_FILES=
|
||||||
|
for /f "delims=" %%f in ('git diff --cached --name-only --diff-filter=ACMR -- "*.rs"') do (
|
||||||
|
if exist "%%f" set STAGED_FILES=!STAGED_FILES! %%f
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%STAGED_FILES%"=="" (
|
||||||
|
echo No staged Rust files; commit regression tests are not required.
|
||||||
|
exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Checking formatting...
|
||||||
|
for %%f in (%STAGED_FILES%) do (
|
||||||
|
rustfmt --edition 2021 --check "%%f"
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo Formatting check failed for %%f
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Running deterministic workspace tests...
|
||||||
|
cargo check --locked --workspace --exclude hcie-io --examples
|
||||||
|
if errorlevel 1 exit /b %errorlevel%
|
||||||
|
|
||||||
|
cargo test --locked --workspace --exclude hcie-io --lib --tests -- --skip benchmark_4k_stroke_on_multilayer_document
|
||||||
|
if errorlevel 1 exit /b %errorlevel%
|
||||||
|
|
||||||
|
echo Mandatory Rust regression gate passed.
|
||||||
|
exit /b 0
|
||||||
@@ -0,0 +1,541 @@
|
|||||||
|
# HCIE-Rust v3.05 — Test Analysis
|
||||||
|
|
||||||
|
> Generated: 2026-07-23
|
||||||
|
> Project root: `/mnt/extra/00_PROJECTS/hcie-rust-v3.05`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Status Assessment
|
||||||
|
|
||||||
|
### 1.1 Active Tests (fully functional, run by default)
|
||||||
|
|
||||||
|
All `#[test]` functions without `#[ignore]` that do not depend on missing fixture files.
|
||||||
|
**Approximately ~400 tests** across 24 crates.
|
||||||
|
|
||||||
|
### 1.2 Inactive, Skipped, or Conditionally Disabled Tests
|
||||||
|
|
||||||
|
| Test | Crate | File:Line | Reason |
|
||||||
|
|------|-------|-----------|--------|
|
||||||
|
| `meadow_brush_visual_check` | `hcie-engine-api` | tests/meadow_check.rs:18 | `#[ignore = "manual visual check"]` — requires human inspection of output PNG |
|
||||||
|
| `leaves_brush_visual_check` | `hcie-engine-api` | tests/leaves_check.rs:11 | `#[ignore = "manual visual check"]` — requires human inspection of output PNG |
|
||||||
|
| `stamp_floor_brush_visual_check` | `hcie-engine-api` | tests/stamp_floor_check.rs:11 | `#[ignore = "manual visual check"]` — requires human inspection of output PNG |
|
||||||
|
| `diagnostic_watercolor_sample_latency` | `hcie-brush-engine` | src/lib.rs:4447 | `#[ignore = "diagnostic benchmark"]` — performance measurement, not pass/fail |
|
||||||
|
| `diagnostic_4k_dirty_staging_latency` | `hcie-iced-gui` | src/canvas/texture_update.rs:253 | `#[ignore = "diagnostic benchmark"]` — performance measurement, not pass/fail |
|
||||||
|
| `test_parse_asl_emboss` | `hcie-fx` | src/parser.rs:1671 | `#[ignore = "requires /tmp/kra_test/emboss_full.asl fixture"]` — missing fixture |
|
||||||
|
| `test_psd_composite_against_ref` | `hcie-io` | lib.rs:43 | Runtime skip if `Example3-mini.psd` not found (IS found → active) |
|
||||||
|
| `test_base_generated_2_vs_merged` | `hcie-io` | lib.rs:301 | Runtime skip if `base_test_generated_2.psd` not found (MISSING → inactive) |
|
||||||
|
| `test_base_generated_2_no_effects` | `hcie-io` | lib.rs:378 | Runtime skip if `base_test_generated_2.psd` not found (MISSING → inactive) |
|
||||||
|
| `test_base_generated_2_composite` | `hcie-io` | lib.rs:420 | Runtime skip if `base_test_generated_2.psd` not found (MISSING → inactive) |
|
||||||
|
| `test_hc_emboss_composite` | `hcie-io` | lib.rs:428 | Runtime skip if `hc_emboss.psd` not found (MISSING → inactive) |
|
||||||
|
| `test_base_generated_2_effect_isolation` | `hcie-io` | lib.rs:438 | Runtime skip if `base_test_generated_2.psd` not found (MISSING → inactive) |
|
||||||
|
| `test_check_soft_orange_pixels` | `hcie-io` | lib.rs:491 | Runtime skip if `base_test_generated_2.psd` not found (MISSING → inactive) |
|
||||||
|
| `test_find_orange_pixels` | `hcie-io` | lib.rs:516 | Runtime skip if `base_test_generated_2.psd` not found (MISSING → inactive) |
|
||||||
|
| `test_layer_bounds` | `hcie-io` | lib.rs:555 | Runtime skip if `base_test_generated_2.psd` not found (MISSING → inactive) |
|
||||||
|
| `test_check_layer_offsets` | `hcie-io` | lib.rs:584 | Runtime skip if `base_test_generated_2.psd` not found (MISSING → inactive) |
|
||||||
|
| `test_save_layer_pixels` | `hcie-io` | lib.rs:621 | Runtime skip if `base_test_generated_2.psd` not found (MISSING → inactive) |
|
||||||
|
| `test_pixel_layer_contributions` | `hcie-io` | lib.rs:635 | Runtime skip if `base_test_generated_2.psd` not found (MISSING → inactive) |
|
||||||
|
| `test_per_layer_effects_vs_photoshop_export` | `hcie-io` | lib.rs:696 | Runtime skip if base PSD not found (MISSING → inactive) |
|
||||||
|
| `test_sultan_effects_mae` | `hcie-io` | lib.rs:861 | Runtime skip if `sultan.psd` not found (MISSING → inactive) |
|
||||||
|
| `test_emboss_optimize` | `hcie-io` | lib.rs:1057 | Runtime skip if `emboss.psd` not found (FOUND → active if it uses `emboss.psd`) |
|
||||||
|
| `test_effects_survive_ffi_boundary` | `hcie-io` | lib.rs:1246 | Runtime skip if base PSD not found (MISSING → inactive) |
|
||||||
|
| `test_ffi_bincode_roundtrip` | `hcie-io` | lib.rs:1276 | Runtime skip if base PSD not found (MISSING → inactive) |
|
||||||
|
|
||||||
|
**Total conditionally inactive: ~17 tests** (mostly `hcie-io` PSD composite tests needing `_images/_psd_stil_test/` fixtures that do not exist in this environment).
|
||||||
|
|
||||||
|
### 1.3 Notable: Expensive / Long-Running Active Tests
|
||||||
|
|
||||||
|
| Test | Crate | Est. Time | Notes |
|
||||||
|
|------|-------|-----------|-------|
|
||||||
|
| `benchmark_4k_stroke_on_multilayer_document` | `hcie-engine-api` | ~60s | 10 layers on 3840×2160, 100 stroke segments with per-segment timing |
|
||||||
|
| `test_load_test_2_psd` | `hcie-io` | ~30-60s | Loads and composites all PSDs from test_2 |
|
||||||
|
| `test_emboss_optimize` | `hcie-io` | ~30-60s | Detailed emboss analysis with line scans, heatmaps |
|
||||||
|
| `test_psd_composite_against_ref` | `hcie-io` | ~10-30s | Full PSD composite against reference PNG |
|
||||||
|
| `test_layer_pixels_direct` | `hcie-io` | ~10-30s | Per-layer pixel comparison |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Command Generation
|
||||||
|
|
||||||
|
### 2.1 Per-Crate Test Commands
|
||||||
|
|
||||||
|
#### Layer 1: DATA
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# hcie-protocol — 16 tests (color packing, constants, lerp, distance, thumbnail, selection, blend modes, tools, version)
|
||||||
|
cargo test -p hcie-protocol
|
||||||
|
|
||||||
|
# hcie-color — 11 tests (gamma encode/decode, sRGB/linear, RGB/HSL)
|
||||||
|
cargo test -p hcie-color
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Layer 2: ENGINE CORE
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# hcie-blend — 9 tests (opacity identity, multiply darkens, screen lightens, all modes)
|
||||||
|
cargo test -p hcie-blend
|
||||||
|
|
||||||
|
# hcie-brush-engine — 13 tests (6 stamp + 7 internal)
|
||||||
|
# (includes 1 ignored diagnostic benchmark)
|
||||||
|
cargo test -p hcie-brush-engine
|
||||||
|
cargo test -p hcie-brush-engine -- --ignored # includes diagnostic_watercolor_sample_latency
|
||||||
|
|
||||||
|
# hcie-draw — 8 tests (rect, circle, ellipse, line, flood fill, mask)
|
||||||
|
cargo test -p hcie-draw
|
||||||
|
|
||||||
|
# hcie-composite — 5 tests (pass through, visibility toggle, basic composite)
|
||||||
|
cargo test -p hcie-composite
|
||||||
|
|
||||||
|
# hcie-filter — 13 tests (blur, gaussian, motion, invert, grayscale, sharpen, emboss, etc.)
|
||||||
|
cargo test -p hcie-filter
|
||||||
|
|
||||||
|
# hcie-selection — 15 tests (rect/ellipse mask, invert, grow, shrink, feather, magic wand, lasso)
|
||||||
|
cargo test -p hcie-selection
|
||||||
|
|
||||||
|
# hcie-vector — 4 tests (crescent, bubble, rotate point, SVG rotation)
|
||||||
|
cargo test -p hcie-vector
|
||||||
|
|
||||||
|
# hcie-history — 9 tests (new empty, push/undo/redo, max steps, jump to, entry description)
|
||||||
|
cargo test -p hcie-history
|
||||||
|
|
||||||
|
# hcie-fx — 1 ignored test (ASL emboss parser — requires fixture)
|
||||||
|
cargo test -p hcie-fx
|
||||||
|
cargo test -p hcie-fx -- --ignored # includes test_parse_asl_emboss
|
||||||
|
|
||||||
|
# hcie-io — ~24 tests (PSD composite, layer analysis, effects)
|
||||||
|
# Many will runtime-skip if _images/ fixtures are missing
|
||||||
|
# Can be very slow (up to several minutes)
|
||||||
|
cargo test -p hcie-io
|
||||||
|
|
||||||
|
# hcie-psd — 8 tests (signature validation, channel RLE, file header fields)
|
||||||
|
cargo test -p hcie-psd
|
||||||
|
|
||||||
|
# hcie-vision — 2 tests (smart patch identity and gradient)
|
||||||
|
cargo test -p hcie-vision
|
||||||
|
|
||||||
|
# hcie-build-info — 1 test (build ID format)
|
||||||
|
cargo test -p hcie-build-info
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Layer 4: ENGINE API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Core engine API — 8 visual regression + 2 history + 5 visual checks + 1 bitmap + 1 selection + 1 performance + 1 internal
|
||||||
|
cargo test -p hcie-engine-api
|
||||||
|
cargo test -p hcie-engine-api -- --ignored # includes meadow, leaves, stamp_floor checks
|
||||||
|
cargo test -p hcie-engine-api -- performance_stroke_4k # 4K benchmark only
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Layer 6: GUI — egui
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# hcie-gui-egui — ~90 tests (canvas_engine, gui_audit, widget_ui, brush_import)
|
||||||
|
cargo test -p hcie-gui-egui
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Layer 6: GUI — iced
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# hcie-iced-gui — ~120+ tests (selection, feature scorecard, raster, app state, dock,
|
||||||
|
# panels, widgets, color picker, AI chat, viewer, CLI, theme, settings, SVG editor, etc.)
|
||||||
|
cargo test -p hcie-iced-gui
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Brush Catalog Crates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test -p hcie-dry-media-brushes
|
||||||
|
cargo test -p hcie-paint-brushes
|
||||||
|
cargo test -p hcie-digital-brushes
|
||||||
|
cargo test -p hcie-watercolor-brushes
|
||||||
|
cargo test -p hcie-ink-brushes
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Specific Test Filter Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run only the proximity dedup tests (recent color fix)
|
||||||
|
cargo test -p hcie-iced-gui -- proximate
|
||||||
|
|
||||||
|
# Run only the visual regression golden tests
|
||||||
|
cargo test -p hcie-engine-api -- visual_regression
|
||||||
|
|
||||||
|
# Run only the feature scorecard tests
|
||||||
|
cargo test -p hcie-iced-gui -- feature_scorecard
|
||||||
|
|
||||||
|
# Run only crop/selection tests
|
||||||
|
cargo test -p hcie-iced-gui -- selection_test
|
||||||
|
|
||||||
|
# Run only the GUI audit tests
|
||||||
|
cargo test -p hcie-gui-egui -- gui_audit
|
||||||
|
|
||||||
|
# Run only the dock sizing/layout tests
|
||||||
|
cargo test -p hcie-iced-gui -- sizing
|
||||||
|
|
||||||
|
# Run only the history tests
|
||||||
|
cargo test -p hcie-history
|
||||||
|
|
||||||
|
# Run only the PSD-related tests
|
||||||
|
cargo test -p hcie-psd
|
||||||
|
cargo test -p hcie-io -- psd # matches "psd" in test names in hcie-io
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Running Ignored Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all ignored tests across all crates (visual checks and benchmarks)
|
||||||
|
cargo test -- --ignored # runs ALL crates
|
||||||
|
cargo test -p hcie-engine-api -- --ignored
|
||||||
|
cargo test -p hcie-brush-engine -- --ignored
|
||||||
|
cargo test -p hcie-iced-gui -- --ignored
|
||||||
|
cargo test -p hcie-fx -- --ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Batch Script (.bat)
|
||||||
|
|
||||||
|
File: `run_tests_categorized.bat`
|
||||||
|
|
||||||
|
```batch
|
||||||
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
set ROOT_DIR=%~dp0
|
||||||
|
cd /d "%ROOT_DIR%" || exit /b 1
|
||||||
|
|
||||||
|
echo ============================================================
|
||||||
|
echo HCIE-Rust v3.05 — Categorized Test Runner (Windows .bat)
|
||||||
|
echo Root: %ROOT_DIR%
|
||||||
|
echo ============================================================
|
||||||
|
|
||||||
|
set PASS=0
|
||||||
|
set FAIL=0
|
||||||
|
set SKIP=0
|
||||||
|
|
||||||
|
:: Helper: run a category and accumulate results
|
||||||
|
call :RUN_CATEGORY "Layer 1: DATA" ^
|
||||||
|
hcie-protocol ^
|
||||||
|
hcie-color
|
||||||
|
|
||||||
|
call :RUN_CATEGORY "Layer 2: ENGINE CORE — Blend/Brush" ^
|
||||||
|
hcie-blend ^
|
||||||
|
hcie-brush-engine
|
||||||
|
|
||||||
|
call :RUN_CATEGORY "Layer 2: ENGINE CORE — Draw/Composite/Filter" ^
|
||||||
|
hcie-draw ^
|
||||||
|
hcie-composite ^
|
||||||
|
hcie-filter
|
||||||
|
|
||||||
|
call :RUN_CATEGORY "Layer 2: ENGINE CORE — Selection/Vector/History" ^
|
||||||
|
hcie-selection ^
|
||||||
|
hcie-vector ^
|
||||||
|
hcie-history
|
||||||
|
|
||||||
|
call :RUN_CATEGORY "Layer 2: ENGINE CORE — IO/PSD/Vision/Build" ^
|
||||||
|
hcie-io ^
|
||||||
|
hcie-psd ^
|
||||||
|
hcie-vision ^
|
||||||
|
hcie-build-info
|
||||||
|
|
||||||
|
call :RUN_CATEGORY "Layer 4: ENGINE API" ^
|
||||||
|
hcie-engine-api
|
||||||
|
|
||||||
|
call :RUN_CATEGORY "Layer 6: GUI — egui" ^
|
||||||
|
hcie-gui-egui
|
||||||
|
|
||||||
|
call :RUN_CATEGORY "Layer 6: GUI — iced" ^
|
||||||
|
hcie-iced-gui
|
||||||
|
|
||||||
|
call :RUN_CATEGORY "Brush Catalogs" ^
|
||||||
|
hcie-dry-media-brushes ^
|
||||||
|
hcie-paint-brushes ^
|
||||||
|
hcie-digital-brushes ^
|
||||||
|
hcie-watercolor-brushes ^
|
||||||
|
hcie-ink-brushes
|
||||||
|
|
||||||
|
echo ============================================================
|
||||||
|
echo SUMMARY: %PASS% passed, %FAIL% failed, %SKIP% skipped
|
||||||
|
echo ============================================================
|
||||||
|
exit /b %FAIL%
|
||||||
|
|
||||||
|
:: ============================================================
|
||||||
|
:: Subroutine: run a named category across multiple crates
|
||||||
|
:: ============================================================
|
||||||
|
:RUN_CATEGORY
|
||||||
|
set CATEGORY=%~1
|
||||||
|
shift
|
||||||
|
echo.
|
||||||
|
echo ============================================================
|
||||||
|
echo Category: %CATEGORY%
|
||||||
|
echo ============================================================
|
||||||
|
|
||||||
|
:RUN_CATEGORY_LOOP
|
||||||
|
if "%~1"=="" goto :EOF
|
||||||
|
set CRATE=%~1
|
||||||
|
shift
|
||||||
|
echo --- Running: %CRATE% ---
|
||||||
|
cargo test -p %CRATE%
|
||||||
|
if %ERRORLEVEL%==0 (
|
||||||
|
set /a PASS+=1
|
||||||
|
) else (
|
||||||
|
set /a FAIL+=1
|
||||||
|
)
|
||||||
|
goto RUN_CATEGORY_LOOP
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Bulk Execution Script (bash)
|
||||||
|
|
||||||
|
File: `run_all_tests.sh`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# HCIE-Rust v3.05 — Bulk test runner
|
||||||
|
# Usage: bash run_all_tests.sh [--no-io] [--no-4k] [--ignored]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
SKIP=0
|
||||||
|
START_TIME=$(date +%s)
|
||||||
|
|
||||||
|
# Parse flags
|
||||||
|
SKIP_IO=false
|
||||||
|
SKIP_4K=false
|
||||||
|
INCLUDE_IGNORED=false
|
||||||
|
EXTRA_ARGS=()
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--no-io) SKIP_IO=true ;;
|
||||||
|
--no-4k) SKIP_4K=true ;;
|
||||||
|
--ignored) INCLUDE_IGNORED=true ;;
|
||||||
|
*) EXTRA_ARGS+=("$arg") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
SEPARATOR() {
|
||||||
|
printf '%*s\n' 80 '' | tr ' ' '='
|
||||||
|
}
|
||||||
|
|
||||||
|
run_crate() {
|
||||||
|
local crate="$1"
|
||||||
|
local label="$2"
|
||||||
|
local extra="$3"
|
||||||
|
shift 3
|
||||||
|
|
||||||
|
SEPARATOR
|
||||||
|
echo "[$label] Running: $crate $extra"
|
||||||
|
SEPARATOR
|
||||||
|
|
||||||
|
if [ -n "$extra" ]; then
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
if cargo test -p "$crate" $extra "${EXTRA_ARGS[@]+${EXTRA_ARGS[@]}}" 2>&1; then
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if cargo test -p "$crate" "${EXTRA_ARGS[@]+${EXTRA_ARGS[@]}}" 2>&1; then
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
display_summary() {
|
||||||
|
local elapsed=$(( $(date +%s) - START_TIME ))
|
||||||
|
echo ""
|
||||||
|
SEPARATOR
|
||||||
|
echo " BULK TEST EXECUTION COMPLETE"
|
||||||
|
echo " Crates passed: $PASS"
|
||||||
|
echo " Crates with failures: $FAIL"
|
||||||
|
echo " Elapsed time: ${elapsed}s"
|
||||||
|
SEPARATOR
|
||||||
|
if [ "$FAIL" -eq 0 ]; then
|
||||||
|
echo " ✓ ALL CRATES PASSED"
|
||||||
|
else
|
||||||
|
echo " ✗ $FAIL crate(s) have failing tests"
|
||||||
|
fi
|
||||||
|
exit "$FAIL"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Layer 1: DATA ───────────────────────────────────────────────
|
||||||
|
run_crate "hcie-protocol" "Layer 1" ""
|
||||||
|
run_crate "hcie-color" "Layer 1" ""
|
||||||
|
|
||||||
|
# ── Layer 2: ENGINE CORE ────────────────────────────────────────
|
||||||
|
run_crate "hcie-blend" "Layer 2" ""
|
||||||
|
run_crate "hcie-brush-engine" "Layer 2" ""
|
||||||
|
run_crate "hcie-draw" "Layer 2" ""
|
||||||
|
run_crate "hcie-composite" "Layer 2" ""
|
||||||
|
run_crate "hcie-filter" "Layer 2" ""
|
||||||
|
run_crate "hcie-selection" "Layer 2" ""
|
||||||
|
run_crate "hcie-vector" "Layer 2" ""
|
||||||
|
run_crate "hcie-history" "Layer 2" ""
|
||||||
|
|
||||||
|
if [ "$SKIP_IO" = false ]; then
|
||||||
|
run_crate "hcie-io" "Layer 2" ""
|
||||||
|
else
|
||||||
|
echo "[SKIP] hcie-io (--no-io flag)"
|
||||||
|
SKIP=$((SKIP + 1))
|
||||||
|
fi
|
||||||
|
run_crate "hcie-psd" "Layer 2" ""
|
||||||
|
run_crate "hcie-vision" "Layer 2" ""
|
||||||
|
run_crate "hcie-build-info" "Layer 2" ""
|
||||||
|
|
||||||
|
# ── Layer 4: ENGINE API ─────────────────────────────────────────
|
||||||
|
if [ "$SKIP_4K" = false ]; then
|
||||||
|
run_crate "hcie-engine-api" "Layer 4" ""
|
||||||
|
else
|
||||||
|
run_crate "hcie-engine-api" "Layer 4" "--skip benchmark_4k_stroke_on_multilayer_document"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Layer 6: GUI — egui ────────────────────────────────────────
|
||||||
|
run_crate "hcie-gui-egui" "Layer 6" ""
|
||||||
|
|
||||||
|
# ── Layer 6: GUI — iced ─────────────────────────────────────────
|
||||||
|
run_crate "hcie-iced-gui" "Layer 6" ""
|
||||||
|
|
||||||
|
# ── Brush Catalogs ──────────────────────────────────────────────
|
||||||
|
run_crate "hcie-dry-media-brushes" "Brushes" ""
|
||||||
|
run_crate "hcie-paint-brushes" "Brushes" ""
|
||||||
|
run_crate "hcie-digital-brushes" "Brushes" ""
|
||||||
|
run_crate "hcie-watercolor-brushes" "Brushes" ""
|
||||||
|
run_crate "hcie-ink-brushes" "Brushes" ""
|
||||||
|
|
||||||
|
# ── Ignored tests (visual checks, benchmarks) ───────────────────
|
||||||
|
if [ "$INCLUDE_IGNORED" = true ]; then
|
||||||
|
echo ""
|
||||||
|
SEPARATOR
|
||||||
|
echo " Running IGNORED tests (visual checks, benchmarks)"
|
||||||
|
SEPARATOR
|
||||||
|
cargo test -p hcie-engine-api -- --ignored || true
|
||||||
|
cargo test -p hcie-brush-engine -- --ignored || true
|
||||||
|
cargo test -p hcie-iced-gui -- --ignored || true
|
||||||
|
cargo test -p hcie-fx -- --ignored || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
display_summary
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Gap Analysis
|
||||||
|
|
||||||
|
### 5.1 Crates with Zero Tests
|
||||||
|
|
||||||
|
These 21 crates have no `#[test]` functions, no `tests/` directory, and no `#[cfg(test)]` modules:
|
||||||
|
|
||||||
|
| Crate | Layer | Risk | Notes |
|
||||||
|
|-------|-------|------|-------|
|
||||||
|
| `hcie-document` | Layer 3 | **HIGH** | Core document state management — layer CRUD, dirty tracking, zoom, blend mode changes. Zero tests despite being a critical engine crate. |
|
||||||
|
| `hcie-tile` | Layer 2 | **HIGH** | Sparse tile-based layer storage — tile read/write, cache invalidation, dirty tracking. No tests for the incremental tile update mechanism. |
|
||||||
|
| `hcie-text` | Layer 2 | **HIGH** | Vector text rendering via `fontdue` — text layout, wrapping, font loading, glyph rasterization. No tests. |
|
||||||
|
| `hcie-native` | Layer 2 | **MEDIUM** | HCIE native file format I/O. Simple wrapper crate but no serialization/deserialization tests. |
|
||||||
|
| `hcie-kra` | Layer 2 | **MEDIUM** | Krita (KRA) file format import/export. No roundtrip tests. |
|
||||||
|
| `hcie-psd-saver` | Layer 2 | **MEDIUM** | PSD file saver. No tests. |
|
||||||
|
| `hcie-ai` | Layer 5 | **MEDIUM** | Ollama/LMStudio/OpenAI chat client, templates, AI actions. No tests for the client, message building, or response parsing. |
|
||||||
|
| `egui-panel-adapter` | Layer 6 | **MEDIUM** | Dynamic schema-to-widget UI binder. No tests. |
|
||||||
|
| `egui-panel-filters` | Layer 6 | **MEDIUM** | Filter parameter panels. No tests for parameter binding or UI state. |
|
||||||
|
| `egui-panel-ai-chat` | Layer 6 | **MEDIUM** | AI chat panel (egui). No tests. |
|
||||||
|
| `egui-panel-script` | Layer 6 | **MEDIUM** | Scripting panel (egui). No tests. |
|
||||||
|
| `egui-panel-ai-script` | Layer 6 | **MEDIUM** | AI-assisted script generation panel (egui). No tests. |
|
||||||
|
| `iced-panel-adapter` | Layer 6 | **MEDIUM** | Schema-to-widget binder (iced). No tests. |
|
||||||
|
| `iced-panel-ai-chat` | Layer 6 | **MEDIUM** | AI chat panel (iced). No tests. |
|
||||||
|
| `iced-panel-script` | Layer 6 | **MEDIUM** | Scripting panel (iced). No tests. |
|
||||||
|
| `panel-tuner` | Tools | **LOW** | Development tool for panel layout tuning. |
|
||||||
|
| `screenshot-diff` | Tools | **LOW** | Screenshot comparison tool. |
|
||||||
|
| `hcie-egui-app` | Layer 6 | **LOW** | Workspace root — no test logic expected. |
|
||||||
|
| `hcie-iced-app` | Layer 6 | **LOW** | Workspace root — no test logic expected. |
|
||||||
|
| `egui-winit` | Patches | **LOW** | Upstream patch — tests disabled (`autotests = false`). |
|
||||||
|
|
||||||
|
### 5.2 Modules with Incomplete Test Coverage
|
||||||
|
|
||||||
|
| Module | Existing Tests | Missing Coverage |
|
||||||
|
|--------|---------------|-----------------|
|
||||||
|
| `hcie-io` PSD composite | 24 tests, but ~17 skip at runtime | Deterministic tests that work without fixture files (generate test PSDs programmatically) |
|
||||||
|
| `hcie-engine-api` visual regression | 8 golden hash tests | No test for watercolor brush, pencil, gradient fill, crop, transform, layer effects |
|
||||||
|
| `hcie-brush-engine` | 7 inline + 6 integration tests | No tests for airbrush/spray dynamics, tilt/rotation mapping, dual-brush mode |
|
||||||
|
| `hcie-filter` | 13 smoke tests | No tests verifying correct pixel output values for any filter; only "does not panic" |
|
||||||
|
| `hcie-psd` | 8 tests (header + signature) | No tests for image resource sections, layer info (masks, effects, patterns), channel data decompression (all `unimplemented!` stubs exist) |
|
||||||
|
| `hcie-composite` | 5 tests | No tests for layer masks, clipping masks, adjustment layers, layer groups with non-PassThrough mode, blend mode combinations |
|
||||||
|
| `hcie-iced-gui` dock system | ~25 tests | No tests for minimizing/maximizing panels, keyboard navigation of dock, drag-and-drop reordering of tabs |
|
||||||
|
| `hcie-iced-gui` panels | ~15 tests | Brushes panel has catalog count checks but no interaction tests. Filters panel has no tests. Layer details/panel has no tests. |
|
||||||
|
|
||||||
|
### 5.3 Recommended Test Additions (Priority Order)
|
||||||
|
|
||||||
|
#### Priority 1 (High Risk — Engine Core Integrity)
|
||||||
|
|
||||||
|
1. **`hcie-document`** — Unit tests for:
|
||||||
|
- Layer CRUD (add/remove/reorder)
|
||||||
|
- Active layer switching
|
||||||
|
- Dirty flag set/clear
|
||||||
|
- Zoom clamp boundaries
|
||||||
|
- Blend mode changes on layers
|
||||||
|
- Opacity/visibility toggle propagation
|
||||||
|
|
||||||
|
2. **`hcie-tile`** — Unit tests for:
|
||||||
|
- Tile read/write at pixel level
|
||||||
|
- Dirty rectangle tracking
|
||||||
|
- Tile cache invalidation
|
||||||
|
- Incremental tile update (the protected optimization)
|
||||||
|
- Multi-resolution tile access
|
||||||
|
|
||||||
|
3. **`hcie-text`** — Unit tests for:
|
||||||
|
- Font loading (valid/invalid paths)
|
||||||
|
- Text layout (single line, multi-line, wrapping)
|
||||||
|
- Glyph rasterization bounds
|
||||||
|
- Unicode / special character handling
|
||||||
|
- Text cursor positioning
|
||||||
|
|
||||||
|
#### Priority 2 (Medium Risk — Feature Correctness)
|
||||||
|
|
||||||
|
4. **`hcie-io`** — Replace fixture-dependent tests with:
|
||||||
|
- Programmatic PSD creation using `hcie-psd-saver` (roundtrip test)
|
||||||
|
- In-memory composite with known pixel values
|
||||||
|
- PNG/JPG/WebP import/export with checksum verification
|
||||||
|
|
||||||
|
5. **`hcie-filter`** — Add value verification tests:
|
||||||
|
- Invert: known input → known output
|
||||||
|
- Grayscale: R=G=B after filter
|
||||||
|
- Brightness/Contrast: exact value mapping
|
||||||
|
- Box blur: average of neighborhood
|
||||||
|
|
||||||
|
6. **`hcie-ai`** — Unit tests for:
|
||||||
|
- Message formatting (system/user/assistant)
|
||||||
|
- API response parsing (JSON extraction)
|
||||||
|
- Template rendering
|
||||||
|
- Error handling (network failure, auth error)
|
||||||
|
|
||||||
|
#### Priority 3 (Lower Risk — GUI Completeness)
|
||||||
|
|
||||||
|
7. **`egui-panel-filters`** — Test filter parameter schema binding and default values
|
||||||
|
8. **`egui-panel-ai-chat`** — Test message history management, send/receive state machine
|
||||||
|
9. **`iced-panel-adapter`** — Test dynamic widget generation from schema
|
||||||
|
10. **All panel crates** — Smoke tests ensuring each panel renders without panic
|
||||||
|
|
||||||
|
### 5.4 Quick Wins (Easy to Add, High Impact)
|
||||||
|
|
||||||
|
| Test | Crate | Effort | Impact |
|
||||||
|
|------|-------|--------|--------|
|
||||||
|
| Roundtrip: create layer → modify → undo → redo → equal original | `hcie-document` | 1 hour | Catches state corruption bugs |
|
||||||
|
| Tile dirty region compute: modify tile → read dirty rect | `hcie-tile` | 2 hours | Validates the optimization in AGENTS.md |
|
||||||
|
| Text measure: known string + font → expected width | `hcie-text` | 1 hour | Prevents layout regressions |
|
||||||
|
| Invert filter: 5 known RGBA values → expected output | `hcie-filter` | 1 hour | Upgrades smoke test to real verification |
|
||||||
|
| No-panic on empty input for every filter | `hcie-filter` | 30 min | Edge case coverage |
|
||||||
|
| Fixture-free PSD composite: create 2 layers with known colors in memory, composite, verify | `hcie-io` | 3 hours | Eliminates ~17 flaky/skipped tests |
|
||||||
|
|
||||||
|
### 5.5 Test Infrastructure Improvements
|
||||||
|
|
||||||
|
| Issue | Recommendation |
|
||||||
|
|-------|---------------|
|
||||||
|
| `hcie-io` tests silently pass when fixtures missing | Change to `panic!` with informative message, or add `#[ignore]` with fixture requirement documented |
|
||||||
|
| No test coverage tracking | Add `cargo tarpaulin` or `cargo llvm-cov` to CI and report line/region coverage |
|
||||||
|
| No CI integration visible | Set up GitHub Actions running: `Layer 1` → `Layer 2` → `Layer 4` → `Layer 6` sequentially with `--no-io --no-4k` for quick CI and full suite nightly |
|
||||||
|
| `hcie-psd` has 12 `unimplemented!()` stubs | Add `#[should_panic]` tests that verify stubs panic as expected, or complete the implementations |
|
||||||
@@ -0,0 +1,820 @@
|
|||||||
|
# HCIE-Rust v3.05 — Complete Test Inventory
|
||||||
|
|
||||||
|
> Generated: 2026-07-23
|
||||||
|
> Project root: `/mnt/extra/00_PROJECTS/hcie-rust-v3.05`
|
||||||
|
>
|
||||||
|
> **Total: ~415+ `#[test]` functions** across 23+ crates organized in 6 layers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Layer 1: DATA](#layer-1-data)
|
||||||
|
- [hcie-protocol — tests/helpers.rs](#hcie-protocol--testshelpersrs)
|
||||||
|
- [hcie-color — tests/color_roundtrip.rs](#hcie-color--testscolor_roundtriprs)
|
||||||
|
2. [Layer 2: ENGINE CORE](#layer-2-engine-core)
|
||||||
|
- [hcie-blend — tests/blend_modes.rs](#hcie-blend--testsblend_modesrs)
|
||||||
|
- [hcie-brush-engine — tests/stamp.rs](#hcie-brush-engine--testsstamp.rs)
|
||||||
|
- [hcie-brush-engine — src/lib.rs (internal)](#hcie-brush-engine--srclibrs-internal)
|
||||||
|
- [hcie-draw — tests/draw_pixels.rs](#hcie-draw--testsdraw_pixelsrs)
|
||||||
|
- [hcie-composite — tests/](#hcie-composite--tests)
|
||||||
|
- [hcie-composite — src/test_composite.rs](#hcie-composite--srctest_compositers)
|
||||||
|
- [hcie-filter — tests/filters.rs](#hcie-filter--testsfiltersrs)
|
||||||
|
- [hcie-selection — tests/mask_ops.rs](#hcie-selection--testsmask_opsrs)
|
||||||
|
- [hcie-vector — src/lib.rs (internal)](#hcie-vector--srclibrs-internal)
|
||||||
|
- [hcie-history — tests/history_state.rs](#hcie-history--testshistory_staters)
|
||||||
|
- [hcie-history — src/lib.rs (internal)](#hcie-history--srclibrs-internal)
|
||||||
|
- [hcie-fx — src/parser.rs (internal)](#hcie-fx--srcparserrs-internal)
|
||||||
|
- [hcie-io — src/test_psd_composite.rs](#hcie-io--srctest_psd_compositers)
|
||||||
|
- [hcie-psd — src/](#hcie-psd--src)
|
||||||
|
- [hcie-vision — src/smart_patch.rs](#hcie-vision--srcsmart_patchrs)
|
||||||
|
- [hcie-build-info — src/lib.rs](#hcie-build-info--srclibrs)
|
||||||
|
3. [Layer 4: ENGINE API](#layer-4-engine-api)
|
||||||
|
- [hcie-engine-api — tests/visual_regression.rs](#hcie-engine-api--testsvisual_regressionrs)
|
||||||
|
- [hcie-engine-api — tests/vector_creation_history.rs](#hcie-engine-api--testsvector_creation_historyrs)
|
||||||
|
- [hcie-engine-api — tests/meadow_check.rs](#hcie-engine-api--testsmeadow_checkrs)
|
||||||
|
- [hcie-engine-api — tests/leaves_check.rs](#hcie-engine-api--testsleaves_checkrs)
|
||||||
|
- [hcie-engine-api — tests/stamp_floor_check.rs](#hcie-engine-api--testsstamp_floor_checkrs)
|
||||||
|
- [hcie-engine-api — tests/bitmap_brush_stamp.rs](#hcie-engine-api--testsbitmap_brush_stamprs)
|
||||||
|
- [hcie-engine-api — tests/selection_clear.rs](#hcie-engine-api--testsselection_clearrs)
|
||||||
|
- [hcie-engine-api — tests/performance_stroke_4k.rs](#hcie-engine-api--testsperformance_stroke_4krs)
|
||||||
|
- [hcie-engine-api — src/stroke_brush.rs (internal)](#hcie-engine-api--srcstroke_brushrs-internal)
|
||||||
|
4. [Layer 6: GUI — egui](#layer-6-gui--egui)
|
||||||
|
- [hcie-gui-egui — tests/canvas_engine.rs](#hcie-gui-egui--testscanvas_enginers)
|
||||||
|
- [hcie-gui-egui — tests/gui_audit.rs](#hcie-gui-egui--testsgui_auditrs)
|
||||||
|
- [hcie-gui-egui — tests/widget_ui.rs](#hcie-gui-egui--testswidget_uirs)
|
||||||
|
- [hcie-gui-egui — src/brush_import.rs (internal)](#hcie-gui-egui--srcbrush_importrs-internal)
|
||||||
|
5. [Layer 6: GUI — iced](#layer-6-gui--iced)
|
||||||
|
- [hcie-iced-gui — tests/selection_test.rs](#hcie-iced-gui--testsselection_testrs)
|
||||||
|
- [hcie-iced-gui — tests/feature_scorecard.rs](#hcie-iced-gui--testsfeature_scorecardrs)
|
||||||
|
- [hcie-iced-gui — tests/raster_test.rs](#hcie-iced-gui--testsraster_testrs)
|
||||||
|
- [hcie-iced-gui — src/app.rs (internal)](#hcie-iced-gui--srcapprs-internal)
|
||||||
|
- [hcie-iced-gui — src/app/transform_dirty.rs](#hcie-iced-gui--srcapptransform_dirtyrs)
|
||||||
|
- [hcie-iced-gui — src/color_picker.rs](#hcie-iced-gui--srccolor_pickerrs)
|
||||||
|
- [hcie-iced-gui — src/ai_chat.rs](#hcie-iced-gui--srcai_chatrs)
|
||||||
|
- [hcie-iced-gui — src/ai_script.rs](#hcie-iced-gui--srcai_scriptrs)
|
||||||
|
- [hcie-iced-gui — src/screenshot.rs](#hcie-iced-gui--srcscreenshotrs)
|
||||||
|
- [hcie-iced-gui — src/sidebar/mod.rs](#hcie-iced-gui--srcsidebarmodrs)
|
||||||
|
- [hcie-iced-gui — src/theme.rs](#hcie-iced-gui--srcthemers)
|
||||||
|
- [hcie-iced-gui — src/viewer/mod.rs](#hcie-iced-gui--srcviewermodrs)
|
||||||
|
- [hcie-iced-gui — src/settings.rs](#hcie-iced-gui--srcsettingsrs)
|
||||||
|
- [hcie-iced-gui — src/cli.rs](#hcie-iced-gui--srcclirs)
|
||||||
|
- [hcie-iced-gui — src/canvas/shader_canvas.rs](#hcie-iced-gui--srccanvasshader_canvasrs)
|
||||||
|
- [hcie-iced-gui — src/canvas/texture_update.rs](#hcie-iced-gui--srccanvastexture_updaters)
|
||||||
|
- [hcie-iced-gui — src/selection/state.rs](#hcie-iced-gui--srcselectionstaters)
|
||||||
|
- [hcie-iced-gui — src/panels/menus.rs](#hcie-iced-gui--srcpanelsmenusrs)
|
||||||
|
- [hcie-iced-gui — src/panels/styles.rs](#hcie-iced-gui--srcpanelsstylesrs)
|
||||||
|
- [hcie-iced-gui — src/panels/custom_shapes.rs](#hcie-iced-gui--srcpanelscustom_shapesrs)
|
||||||
|
- [hcie-iced-gui — src/panels/brushes.rs](#hcie-iced-gui--srcpanelsbrushesrs)
|
||||||
|
- [hcie-iced-gui — src/panels/svg_editor.rs](#hcie-iced-gui--srcpanelssvg_editorrs)
|
||||||
|
- [hcie-iced-gui — src/panels/title_bar.rs](#hcie-iced-gui--srcpanelstitle_bar.rs)
|
||||||
|
- [hcie-iced-gui — src/vector_edit.rs](#hcie-iced-gui--srcvector_editrs)
|
||||||
|
- [hcie-iced-gui — src/widgets/plain_slider.rs](#hcie-iced-gui--srcwidgetsplain_sliderrs)
|
||||||
|
- [hcie-iced-gui — src/dock/state.rs](#hcie-iced-gui--srcdockstaters)
|
||||||
|
- [hcie-iced-gui — src/dock/manager.rs](#hcie-iced-gui--srcdockmanagersr)
|
||||||
|
- [hcie-iced-gui — src/dock/sizing.rs](#hcie-iced-gui--srcdocksizingrs)
|
||||||
|
- [hcie-iced-gui — src/dock/persistence.rs](#hcie-iced-gui--srcdockpersistencers)
|
||||||
|
- [hcie-iced-gui — src/dock/view.rs](#hcie-iced-gui--srcdockviewrs)
|
||||||
|
- [hcie-iced-gui — src/dock/preview.rs](#hcie-iced-gui--srcdockpreviewrs)
|
||||||
|
- [hcie-iced-gui — src/dock/floating.rs](#hcie-iced-gui--srcdockfloatingrs)
|
||||||
|
6. [Brush Catalog Crates](#brush-catalog-crates)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layer 1: DATA
|
||||||
|
|
||||||
|
### `hcie-protocol` — tests/helpers.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 4 | `test_to_u32_from_u32_roundtrip` | Tests that packing and unpacking RGBA colors to/from u32 round-trips correctly for several test colors. |
|
||||||
|
| 20 | `test_color_constants` | Verifies that `TRANSPARENT`, `BLACK`, `WHITE`, and `RED` constants have the expected RGBA values. |
|
||||||
|
| 28 | `test_lerp_boundaries` | Confirms linear interpolation at t=0, t=0.5, and t=1 returns correct boundary/midpoint values. |
|
||||||
|
| 35 | `test_color_lerp` | Verifies color lerp between two RGBA colors at 0.5 blend produces expected midpoint values. |
|
||||||
|
| 46 | `test_lerp_clamp` | Confirms lerp clamps t values below 0 and above 1 to the range endpoints. |
|
||||||
|
| 52 | `test_clamp` | Tests the clamp function with in-range, below-min, and above-max values. |
|
||||||
|
| 59 | `test_distance` | Verifies Euclidean distance calculation (3-4-5 triangle) and zero-distance for same point. |
|
||||||
|
| 65 | `test_point_in_rect` | Tests `point_in_rect` with points inside and outside a rectangle. |
|
||||||
|
| 72 | `test_thumbnail_nearest` | Tests nearest-neighbor thumbnail generation from a 2×1 RGBA image to 1×1. |
|
||||||
|
| 83 | `test_selection_rect_from_mask` | Verifies extracting a bounding rectangle from a selection mask with a 2×2 selected region. |
|
||||||
|
| 95 | `test_blank_mask_no_selection` | Confirms a mask of all zeros returns `None` from `selection_rect_from_mask`. |
|
||||||
|
| 101 | `test_blend_mode_all_contains_all` | Asserts that `BlendMode::ALL` contains exactly 28 blend modes. |
|
||||||
|
| 106 | `test_all_tools_listed` | Asserts that `Tool::ALL` contains exactly 36 tools. |
|
||||||
|
| 111 | `test_expand_dirty_bounds` | Tests expanding dirty bounds with a given rectangle, verifying the union result. |
|
||||||
|
| 118 | `test_version_string_format` | Checks that the version string starts with "3." (expected format). |
|
||||||
|
| 125 | `from_u32_to_u32_roundtrip` | Property-based test using proptest to verify u32 round-trip for random RGBA values. |
|
||||||
|
|
||||||
|
### `hcie-color` — tests/color_roundtrip.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 4 | `test_gamma_encode_boundary_zero` | Verifies gamma encoding of 0.0 equals 0.0. |
|
||||||
|
| 9 | `test_gamma_encode_boundary_one` | Verifies gamma encoding of 1.0 equals 1.0. |
|
||||||
|
| 14 | `test_gamma_decode_boundary_zero` | Verifies gamma decoding of 0.0 equals 0.0. |
|
||||||
|
| 19 | `test_gamma_decode_boundary_one` | Verifies gamma decoding of 1.0 equals 1.0. |
|
||||||
|
| 24 | `test_gamma_roundtrip` | Ensures gamma encode→decode round-trip preserves values within 0.01 epsilon for 7 test values. |
|
||||||
|
| 33 | `test_srgb_to_linear_black` | Confirms sRGB black [0,0,0] converts to linear [0.0, 0.0, 0.0]. |
|
||||||
|
| 38 | `test_srgb_to_linear_white` | Confirms sRGB white [255,255,255] converts to linear [1.0, 1.0, 1.0]. |
|
||||||
|
| 47 | `srgb_linear_roundtrip` | Property-based test verifying sRGB→linear→sRGB round-trip preserves values. |
|
||||||
|
| 58 | `test_rgb_to_hsl_basic` | Tests RGB→HSL conversion for pure red, expecting H=0, S=1, L=0.5. |
|
||||||
|
| 66 | `test_rgb_to_hsl_gray` | Tests RGB→HSL for gray (0.5,0.5,0.5), expecting S=0. |
|
||||||
|
| 74 | `test_linear_to_srgb_clamps` | Confirms linear→sRGB clamps values below 0 to 0 and above 1 to 255. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layer 2: ENGINE CORE
|
||||||
|
|
||||||
|
### `hcie-blend` — tests/blend_modes.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 5 | `test_normal_zero_opacity_identity` | Verifies that blending with opacity=0 returns the destination unchanged for Normal mode. |
|
||||||
|
| 13 | `test_normal_full_opacity` | Verifies that Normal mode with opacity=1 outputs the source pixel unchanged. |
|
||||||
|
| 21 | `test_opacity_zero_no_mutation_for_all_modes` | Confirms that opacity=0 returns dst unchanged for every blend mode (except PassThrough). |
|
||||||
|
| 38 | `test_specific_blend_modes` | Parametric test (rstest) checking specific blend modes (Multiply, Screen, etc.) produce valid alpha. |
|
||||||
|
| 51 | `test_transparent_src_normal` | Verifies that a fully transparent source leaves the destination unchanged. |
|
||||||
|
| 59 | `test_all_modes_produce_valid_alpha` | Ensures all blend modes with opacity=0.5 produce valid u8 alpha values. |
|
||||||
|
| 70 | `test_multiply_darkens` | Verifies Multiply blend mode darkens the destination channel. |
|
||||||
|
| 78 | `test_screen_lightens` | Verifies Screen blend mode lightens the destination channel. |
|
||||||
|
| 87 | `identity_at_zero_opacity` | Property-based test: random pixels blended with opacity 0 always equal the destination for all modes. |
|
||||||
|
|
||||||
|
### `hcie-brush-engine` — tests/stamp.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 13 | `test_generate_brush_stamp_size` | Verifies a round brush stamp of size 10 produces a 20×20 square stamp buffer. |
|
||||||
|
| 25 | `test_generate_brush_stamp_center_nonzero` | Confirms the center pixel of a hardness=1.0 round stamp is opaque. |
|
||||||
|
| 34 | `test_generate_brush_stamp_hardness_gradient` | Checks that soft (0.0) and hard (1.0) stamps have the same buffer length. |
|
||||||
|
| 45 | `test_brush_spacing_pixels` | Verifies `brush_spacing_pixels` returns size×ratio for 0.5 ratio and minimum ~0.2 for 0.0 ratio. |
|
||||||
|
| 57 | `test_jitter_offset_within_bounds` | Confirms zero jitter amount produces zero offset. |
|
||||||
|
| 64 | `test_bitmap_stamp_fallback_to_round` | Verifies that an empty bitmap stamp falls back to a round stamp (non-empty buffer). |
|
||||||
|
|
||||||
|
### `hcie-brush-engine` — src/lib.rs (internal)
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 4241 | `watercolor_detail_count_is_hard_bounded` | Verifies watercolor detail counts (satellites, splatters, blooms) stay within `WATERCOLOR_MAX_DABS_PER_SAMPLE` for sizes 0.5–1024. |
|
||||||
|
| 4257 | `every_specialized_style_is_noop_at_zero_pressure` | Tests that all 28 specialized brush styles produce zero-painted pixels when pressure is 0. |
|
||||||
|
| 4328 | `blender_preserves_uniform_color_and_transparency` | Verifies the Blender brush doesn't change uniform color fields and doesn't paint on empty areas. |
|
||||||
|
| 4352 | `rotated_dab_uses_single_channel_stroke_mask` | Confirms rotated dabs write into a single-channel stroke mask during specialized stroke drawing. |
|
||||||
|
| 4389 | `rotated_pen_respects_stroke_opacity_cap` | Verifies three overlapping pen dabs at 50% flow stay within the 50% stroke opacity cap (~128 alpha). |
|
||||||
|
| 4430 | `pigment_mix_is_subtractive_and_bounded` | Confirms pigment mixing is subtractive (red+blue → dark purple) and respects blend factor boundaries. |
|
||||||
|
| 4447 | `diagnostic_watercolor_sample_latency` | Diagnostic benchmark (ignored) measuring round dab and watercolor brush latency at various sizes. |
|
||||||
|
|
||||||
|
### `hcie-draw` — tests/draw_pixels.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 4 | `test_draw_filled_rect` | Draws a filled red rectangle and checks the center pixel is red. |
|
||||||
|
| 14 | `test_draw_filled_rect_outside` | Confirms pixels outside a drawn rectangle remain unchanged (transparent). |
|
||||||
|
| 22 | `test_draw_filled_circle` | Draws a filled green circle and checks the center pixel is green. |
|
||||||
|
| 30 | `test_draw_filled_ellipse` | Draws a filled blue ellipse and checks the center pixel is blue. |
|
||||||
|
| 38 | `test_draw_line` | Draws a yellow horizontal line and checks a pixel on the line. |
|
||||||
|
| 56 | `test_flood_fill` | Performs flood fill from (0,0) and checks that the filled area is red. |
|
||||||
|
| 64 | `test_draw_with_mask_restricts_pixels` | Draws a rect with a mask where only pixel (0,0) is masked; verifies only that pixel is painted. |
|
||||||
|
| 84 | `test_line_at_edge` | Draws a diagonal line from corner to corner and checks the start pixel. |
|
||||||
|
|
||||||
|
### `hcie-composite` — tests/
|
||||||
|
|
||||||
|
| File | Line | Function | Summary |
|
||||||
|
|------|------|----------|---------|
|
||||||
|
| pass_through.rs | 24 | `test_pass_through_group_skipped_in_composite` | Verifies child layers in a PassThrough group blend directly onto the background (Multiply on white→gray). |
|
||||||
|
| pass_through.rs | 56 | `test_pass_through_group_invisible_children` | Confirms invisible children of a PassThrough group are excluded and only background appears. |
|
||||||
|
| pass_through.rs | 78 | `test_pass_through_group_with_opacity` | Documents current MVP behavior: PassThrough group opacity is skipped so child blends at full strength. |
|
||||||
|
| ff_visibility.rs | 20 | `test_composite_visibility_from_protocol_layer` | Tests composite with protocol layers toggling visibility of background and top layers, printing opaque pixel counts. |
|
||||||
|
| visibility.rs | 17 | `test_composite_visibility_toggle` | Tests compositing with both visible, background hidden, top hidden, and both hidden, asserting correct opaque pixel counts. |
|
||||||
|
|
||||||
|
### `hcie-composite` — src/test_composite.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 5 | `test_composite_basic` | Basic composite test: single red layer output, then two layers (red background, blue overlay) checking top-wins compositing. |
|
||||||
|
|
||||||
|
### `hcie-filter` — tests/filters.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 22 | `test_blur_does_not_panic` | Applies box_blur with radius 2 and checks dimensions remain valid. |
|
||||||
|
| 30 | `test_gaussian_blur_does_not_panic` | Applies gaussian_blur with radius 2 and checks dimensions remain valid. |
|
||||||
|
| 38 | `test_motion_blur_does_not_panic` | Applies motion_blur with distance 5, angle 45 and checks dimensions remain valid. |
|
||||||
|
| 46 | `test_invert_filter` | Applies invert filter and verifies at least one pixel changed value. |
|
||||||
|
| 55 | `test_grayscale_filter` | Applies grayscale filter and verifies R, G, B values differ by at most 3 (effectively gray). |
|
||||||
|
| 69 | `test_filter_ids` | Checks that filter_ids() returns at least one filter and includes "invert" and "box_blur". |
|
||||||
|
| 78 | `test_sharpen_does_not_panic` | Applies sharpen filter and checks dimensions remain valid. |
|
||||||
|
| 86 | `test_unknown_filter_is_noop` | Confirms applying a nonexistent filter name leaves pixels unchanged. |
|
||||||
|
| 94 | `test_unsharp_mask_does_not_panic` | Applies unsharp_mask with radius 2, amount 0.5 and checks dimensions remain valid. |
|
||||||
|
| 102 | `test_brightness_contrast` | Applies brightness_contrast filter and confirms output contains non-zero pixel values. |
|
||||||
|
| 110 | `test_hue_saturation_does_not_panic` | Applies hue_saturation filter and checks dimensions remain valid. |
|
||||||
|
| 118 | `test_emboss_does_not_panic` | Applies emboss filter and checks dimensions remain valid. |
|
||||||
|
| 126 | `test_find_edges_does_not_panic` | Applies find_edges filter and checks dimensions remain valid. |
|
||||||
|
|
||||||
|
### `hcie-selection` — tests/mask_ops.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 2 | `test_create_rect_mask_full` | Creates a 4×4 rect mask covering the full area and verifies all values are 255. |
|
||||||
|
| 8 | `test_create_rect_mask_partial` | Creates a partial rect mask (1,1 to 2,2 in 4×4) and verifies inside/outside pixels. |
|
||||||
|
| 15 | `test_create_ellipse_mask` | Creates an ellipse mask and verifies center is selected, corner is not. |
|
||||||
|
| 22 | `test_invert_mask` | Inverts a mask and verifies 255→0 and 0→255. |
|
||||||
|
| 31 | `test_invert_twice_is_identity` | Inverts a mask twice and confirms the result equals the original. |
|
||||||
|
| 45 | `test_grow_mask` | Grows a single selected pixel by 1 and verifies more pixels become selected. |
|
||||||
|
| 54 | `test_grow_zero_is_noop` | Confirms growing by 0 leaves the mask unchanged. |
|
||||||
|
| 66 | `test_shrink_mask` | Shrinks a fully-selected mask by 1 and verifies fewer selected pixels. |
|
||||||
|
| 74 | `test_shrink_zero_is_noop` | Confirms shrinking by 0 leaves the mask unchanged. |
|
||||||
|
| 82 | `test_grow_shrink_roundtrip` | Grows then shrinks a mask by 1 and verifies it returns to the original state. |
|
||||||
|
| 97 | `test_feather_mask` | Feathers a 3×3 selected area with radius 1 and checks center remains 255 and corners are feathered. |
|
||||||
|
| 110 | `test_feather_zero_is_noop` | Confirms feathering with radius 0 leaves the mask unchanged. |
|
||||||
|
| 119 | `test_mask_at` | Tests `mask_at` with and without a mask, checking correct values returned at various positions. |
|
||||||
|
| 131 | `test_magic_wand` | Tests magic wand selection on a 2×2 pixel grid, checking seed and adjacent same-color pixels are selected. |
|
||||||
|
| 143 | `test_lasso_fill_mask` | Fills a 5×5 mask with a square lasso and verifies the center pixel is filled. |
|
||||||
|
|
||||||
|
### `hcie-vector` — src/lib.rs (internal)
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 1101 | `test_crescent_sharp_tips` | Verifies the crescent shape's outer and inner ellipse arcs meet exactly at both tips. |
|
||||||
|
| 1129 | `test_bubble_tail_points` | Verifies the speech bubble contains the tail tip and inner connection points. |
|
||||||
|
| 1154 | `test_rotate_point` | Tests rotating a point 90° around a center and verifies correct output coordinates. |
|
||||||
|
| 1167 | `svg_points_follow_shape_angle` | Verifies SVG-backed shape rotation applies correctly around bounds center. |
|
||||||
|
|
||||||
|
### `hcie-history` — tests/history_state.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 31 | `test_history_new_is_empty` | Verifies a new HistoryManager has len=0, can_undo=false, can_redo=false, index=-1. |
|
||||||
|
| 40 | `test_history_push_undo_redo` | Pushes two pixel changes, undoes one step (checking pixel value), then redoes. |
|
||||||
|
| 65 | `test_history_undo_twice_is_stable` | Verifies double-undo doesn't go past the bottom of the history stack. |
|
||||||
|
| 79 | `test_history_redo_twice_is_stable` | Verifies double-redo doesn't go past the top of the history stack. |
|
||||||
|
| 94 | `test_history_max_steps` | Pushes 10 actions with max_steps=3 and verifies only 3 are retained. |
|
||||||
|
| 108 | `test_history_new_action_truncates_redo` | Undoes, then pushes a new action; verifies redo stack is truncated. |
|
||||||
|
| 133 | `test_history_jump_to` | Pushes 3 actions, jumps to index -1, verifies at bottom with full redo stack. |
|
||||||
|
| 161 | `test_entry_description` | Tests that entry_description returns the action description for valid indices and None for invalid. |
|
||||||
|
|
||||||
|
### `hcie-history` — src/lib.rs (internal)
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 126 | `first_entry_can_redo_from_pre_history_cursor` | Verifies the first history entry can be redone after undo when cursor starts at pre-history state. |
|
||||||
|
|
||||||
|
### `hcie-fx` — src/parser.rs (internal)
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 1671 | `test_parse_asl_emboss` | Tests parsing an ASL file for emboss styles, verifying styles are non-empty with UUIDs and effects. Ignored by default (requires fixture). |
|
||||||
|
|
||||||
|
### `hcie-io` — src/test_psd_composite.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 3 | `test_psd_import_sizes_and_offsets` | Imports Example3-mini.psd, checks background has >1M non-zero pixels and named layers have correct data. |
|
||||||
|
| 43 | `test_psd_composite_against_ref` | Full composite comparison of PSD layers against reference PNG, asserting MAE < 2.0 and match ≥ 88%. |
|
||||||
|
| 241 | `test_layer_pixels_direct` | Compares raw layer pixels (layer 0 and all layers) against reference PNG pixel-by-pixel per layer. |
|
||||||
|
| 301 | `test_base_generated_2_vs_merged` | Compares our composited output against ImageMagick-extracted PSD merged image. |
|
||||||
|
| 345 | `test_blend_normal` | Tests basic src-over compositing with known RGBA values and prints expected vs actual. |
|
||||||
|
| 364 | `test_teal_circle_emboss_debug` | Imports base_test_generated_2.psd, extracts the BevelEmboss effect from the teal circle layer. |
|
||||||
|
| 378 | `test_base_generated_2_no_effects` | Composites with all effects disabled and compares against reference to measure baseline error. |
|
||||||
|
| 420 | `test_base_generated_2_composite` | Composite and compare base_test_generated_2.psd against reference PNG. |
|
||||||
|
| 428 | `test_hc_emboss_composite` | Composite and compare hc_emboss.psd against its reference PNG. |
|
||||||
|
| 438 | `test_base_generated_2_effect_isolation` | Disables effects layer-by-layer, measuring MAE delta to isolate which effects contribute most to error. |
|
||||||
|
| 491 | `test_check_soft_orange_pixels` | Prints RGBA values of specific pixels in Soft Orange Shape and Pink Rectangle layers. |
|
||||||
|
| 516 | `test_find_orange_pixels` | Finds non-zero pixel bounds in Soft Orange Shape layer and samples pixels. |
|
||||||
|
| 555 | `test_layer_bounds` | Computes and prints non-zero pixel bounds for every layer in base_test_generated_2.psd. |
|
||||||
|
| 584 | `test_check_layer_offsets` | Finds leftmost/topmost non-zero pixels to determine layer position offsets. |
|
||||||
|
| 621 | `test_save_layer_pixels` | Saves each layer as a separate PNG to /tmp for visual inspection. |
|
||||||
|
| 635 | `test_pixel_layer_contributions` | Prints RGBA contributions from each layer at 5 specific pixel coordinates. |
|
||||||
|
| 658 | `test_load_test_2_psd` | Loads and composites all PSDs from test_2 directory, printing load/composite times. |
|
||||||
|
| 696 | `test_per_layer_effects_vs_photoshop_export` | Per-layer effect comparison: applies effects to each layer and compares against Photoshop-exported PNGs. |
|
||||||
|
| 795 | `test_test2_effects_mae` | Computes MAE for all PSDs in test_2 directory against JPG references and prints average. |
|
||||||
|
| 861 | `test_sultan_effects_mae` | Full sultan.psd composite analysis: per-layer skip analysis, grid-based MAE breakdown, debug saves. |
|
||||||
|
| 1057 | `test_emboss_optimize` | emboss.psd detailed analysis: channel MAE, opaque-only MAE, regional grid, horizontal/vertical line scans, saves debug heatmaps. |
|
||||||
|
| 1246 | `test_effects_survive_ffi_boundary` | Verifies that effects parsed from PSD survive the PSD→protocol layer boundary (effects > 0). |
|
||||||
|
| 1276 | `test_ffi_bincode_roundtrip` | Verifies bincode serialization/deserialization round-trip preserves all layer effects. |
|
||||||
|
|
||||||
|
### `hcie-psd` — src/
|
||||||
|
|
||||||
|
| File | Line | Function | Summary |
|
||||||
|
|------|------|----------|---------|
|
||||||
|
| lib.rs | 341 | `psd_signature_fail` | Verifies that loading a PNG file through Psd::from_bytes returns a PSD signature error. |
|
||||||
|
| psd_channel.rs | 357 | `does_not_read_beyond_rle_channels_bytes` | Verifies RLE channel insertion doesn't read beyond channel bytes by testing 1×1 layer with RLE-compressed Red channel. |
|
||||||
|
| sections/file_header_section.rs | 291 | `valid_channel_count` | Verifies ChannelCount::new accepts all channel counts from 1 to 56. |
|
||||||
|
| sections/file_header_section.rs | 300 | `invalid_channel_count` | Verifies ChannelCount::new rejects 0 and 57. |
|
||||||
|
| sections/file_header_section.rs | 307 | `incorrect_file_header_section_length` | Verifies a 25-byte input returns IncorrectLength error. |
|
||||||
|
| sections/file_header_section.rs | 317 | `first_four_bytes_incorrect` | Tests that invalid first 4 bytes (wrong PSD signature) returns InvalidSignature error. |
|
||||||
|
| sections/file_header_section.rs | 329 | `version_incorrect` | Tests that wrong version number returns InvalidVersion error. |
|
||||||
|
| sections/file_header_section.rs | 339 | `invalid_reserved_section` | Tests that wrong reserved section returns InvalidReserved error. |
|
||||||
|
|
||||||
|
### `hcie-vision` — src/smart_patch.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 415 | `identity_patch_on_uniform_regions_is_stable` | Smart patch on two identical uniform 120-gray 16×16 layers; verifies output changes by at most 1 per channel. |
|
||||||
|
| 433 | `patch_interior_moves_toward_source_gradient` | Patches a gradient source into a uniform destination; verifies exterior unchanged and interior follows gradient direction. |
|
||||||
|
|
||||||
|
### `hcie-build-info` — src/lib.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 31 | `generated_version_contains_the_same_build_id` | Verifies build_id() > 0 and VERSION string ends with "+build.{BUILD_ID}". |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layer 4: ENGINE API
|
||||||
|
|
||||||
|
### `hcie-engine-api` — tests/visual_regression.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 39 | `white_canvas_empty_document` | Validates blank 8×8 transparent document produces deterministic SHA-256 hash. |
|
||||||
|
| 58 | `green_rect_draw_and_composite` | Draws a green rectangle and checks center pixel + deterministic hash. |
|
||||||
|
| 79 | `red_rect_over_green_rect` | Two-layer composite (green bottom, red top) verifying top layer wins + hash. |
|
||||||
|
| 109 | `vector_rect_golden` | Adds a vector rect shape and verifies rendering through Engine API with hash. |
|
||||||
|
| 145 | `brush_stroke_golden` | Draws a brush stroke and verifies deterministic output via golden hash. |
|
||||||
|
| 178 | `invert_filter_golden` | Applies invert filter and validates pixel values + deterministic hash. |
|
||||||
|
| 200 | `undo_after_rect_golden` | Draws then undoes a rectangle; verifies composite hash matches initial state. |
|
||||||
|
| 221 | `vector_fill_toggle_and_delete` | Creates vector rect, toggles fill on/off, deletes shape, verifying state at each step. |
|
||||||
|
|
||||||
|
### `hcie-engine-api` — tests/vector_creation_history.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 35 | `first_vector_shape_is_one_atomic_layer_transaction` | Verifies first vector shape auto-creates vector layer as one history transaction with correct description. |
|
||||||
|
| 71 | `later_vector_shapes_each_add_one_snapshot` | Verifies later vector shapes on the same layer each add one snapshot and undo correctly reduces shape count. |
|
||||||
|
|
||||||
|
### `hcie-engine-api` — tests/meadow_check.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 18 | `meadow_brush_visual_check` | Manual visual check: renders Meadow brush strokes to 512×512 canvas and saves PNG; asserts colored pixels exist. |
|
||||||
|
|
||||||
|
### `hcie-engine-api` — tests/leaves_check.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 11 | `leaves_brush_visual_check` | Manual visual check: renders Leaf brush strokes and saves to PNG; asserts colored pixels exist. |
|
||||||
|
|
||||||
|
### `hcie-engine-api` — tests/stamp_floor_check.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 11 | `stamp_floor_brush_visual_check` | Manual visual check: renders Dirt/StampFloor brush strokes and saves to PNG; asserts colored pixels exist. |
|
||||||
|
|
||||||
|
### `hcie-engine-api` — tests/bitmap_brush_stamp.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 4 | `bitmap_brush_stamp_shape` | Tests bitmap brush stamp produces roughly square bounding box (~8×8) with enough painted pixels. |
|
||||||
|
|
||||||
|
### `hcie-engine-api` — tests/selection_clear.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 30 | `clear_selection_pixels_clears_inside_and_preserves_outside` | Creates 3×3 opaque layer, selects center pixel, clears selection; verifies center cleared, outside preserved. |
|
||||||
|
|
||||||
|
### `hcie-engine-api` — tests/performance_stroke_4k.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 41 | `benchmark_4k_stroke_on_multilayer_document` | Performance benchmark: 10 layers on 3840×2160 canvas, 100 stroke segments with per-segment timing. |
|
||||||
|
|
||||||
|
### `hcie-engine-api` — src/stroke_brush.rs (internal)
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 547 | `watercolor_dirty_radius_covers_jitter_and_scatter` | Verifies brush_dirty_radius for watercolor with jitter/scatter matches expected value 74.8. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layer 6: GUI — egui
|
||||||
|
|
||||||
|
### `hcie-gui-egui` — tests/canvas_engine.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 23 | `canvas_widget_creation` | Tests CanvasWidget can be created and rendered in an egui test UI. |
|
||||||
|
| 41 | `canvas_widget_zoom_0_1` | Tests canvas widget renders with zoom=0.1. |
|
||||||
|
| 45 | `canvas_widget_zoom_0_5` | Tests canvas widget renders with zoom=0.5. |
|
||||||
|
| 49 | `canvas_widget_zoom_1_0` | Tests canvas widget renders with zoom=1.0. |
|
||||||
|
| 53 | `canvas_widget_zoom_2_0` | Tests canvas widget renders with zoom=2.0. |
|
||||||
|
| 57 | `canvas_widget_zoom_4_0` | Tests canvas widget renders with zoom=4.0. |
|
||||||
|
| 61 | `canvas_widget_zoom_8_0` | Tests canvas widget renders with zoom=8.0. |
|
||||||
|
| 65 | `canvas_widget_zoom_min` | Tests canvas widget renders at ZOOM_MIN. |
|
||||||
|
| 69 | `canvas_widget_zoom_max` | Tests canvas widget renders at ZOOM_MAX. |
|
||||||
|
| 83 | `canvas_widget_pen` | Tests canvas widget with Pen tool. |
|
||||||
|
| 87 | `canvas_widget_brush` | Tests canvas widget with Brush tool. |
|
||||||
|
| 91 | `canvas_widget_eraser` | Tests canvas widget with Eraser tool. |
|
||||||
|
| 95 | `canvas_widget_rect` | Tests canvas widget with VectorRect tool. |
|
||||||
|
| 99 | `canvas_widget_line` | Tests canvas widget with VectorLine tool. |
|
||||||
|
| 103 | `canvas_widget_select` | Tests canvas widget with Select tool. |
|
||||||
|
| 107 | `canvas_widget_lasso` | Tests canvas widget with Lasso tool. |
|
||||||
|
| 111 | `canvas_widget_magic_wand` | Tests canvas widget with MagicWand tool. |
|
||||||
|
| 115 | `canvas_widget_eyedropper` | Tests canvas widget with Eyedropper tool. |
|
||||||
|
| 119 | `canvas_widget_flood_fill` | Tests canvas widget with FloodFill tool. |
|
||||||
|
| 123 | `canvas_widget_gradient` | Tests canvas widget with Gradient tool. |
|
||||||
|
| 127 | `canvas_widget_move` | Tests canvas widget with Move tool. |
|
||||||
|
| 131 | `canvas_widget_crop` | Tests canvas widget with Crop tool. |
|
||||||
|
| 136 | `canvas_widget_with_pan` | Tests canvas widget with pan offset applied. |
|
||||||
|
| 146 | `canvas_widget_with_selection` | Tests canvas widget with a selection rectangle active. |
|
||||||
|
| 156 | `canvas_widget_lasso_with_points` | Tests canvas widget with Lasso tool and lasso points. |
|
||||||
|
| 167 | `canvas_widget_text_active` | Tests canvas widget with Text tool and draft text active. |
|
||||||
|
| 181 | `app_document_zoom_roundtrip` | Tests Zoom round-trip for many zoom values (0.1–64.0) through AppDocument. |
|
||||||
|
| 193 | `engine_composite_after_draw` | Tests Engine compositing after drawing a green rectangle through AppDocument. |
|
||||||
|
| 207 | `engine_multi_layer_composite` | Tests multi-layer compositing through AppDocument (adds layer, draws red rect, checks pixel). |
|
||||||
|
| 218 | `transform_handle_variants` | Basic match test ensuring all TransformHandle variants exist. |
|
||||||
|
| 232 | `selection_transform_from_engine_empty_mask` | Tests SelectionTransform::from_engine with empty mask produces empty transform. |
|
||||||
|
| 241 | `zoom_clamp_values` | Tests zoom clamping for values from -1.0 to 100.0 stays within [ZOOM_MIN, ZOOM_MAX]. |
|
||||||
|
| 257 | `event_bus_events_dispatched_to_engine` | Tests that engine undo changes composite pixel values through AppDocument. |
|
||||||
|
| 267 | `test_text_layer_rotation` | Adds rotated text layer and verifies at least one non-transparent pixel was rasterized. |
|
||||||
|
|
||||||
|
### `hcie-gui-egui` — tests/gui_audit.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 6 | `test_all_tools_in_toolbox_groups` | Verifies every Tool variant is present in TOOL_SLOTS. |
|
||||||
|
| 23 | `test_all_tools_have_labels` | Verifies every tool has a non-empty label(). |
|
||||||
|
| 32 | `test_all_tools_have_icons` | Verifies every tool has a non-empty icon(). |
|
||||||
|
| 41 | `test_all_blend_modes_have_labels` | Verifies every BlendMode has a non-empty label(). |
|
||||||
|
| 50 | `test_tools_menu_coverage` | Verifies expected tools in menu are defined in Tool::ALL. |
|
||||||
|
| 81 | `test_view_menu_panels_match` | Verifies panel labels are unique. |
|
||||||
|
| 102 | `test_all_tools_layer_type_known` | Calls allowed_layer_type() on every tool to ensure it doesn't panic. |
|
||||||
|
| 110 | `test_feature_flags_are_non_empty` | Verifies all_features() returns non-empty flags including "layers", "undo_redo", "selection". |
|
||||||
|
| 123 | `test_app_event_variants_exist` | Creates an EventBus to verify the module exists. |
|
||||||
|
| 131 | `test_dock_panels_have_icons` | Verifies all HciePane variants have icons. |
|
||||||
|
| 154 | `test_shape_tools_consistency` | Verifies 15 specific shape tools return true for is_shape_tool(). |
|
||||||
|
| 181 | `test_raster_tools_consistency` | Verifies raster tools are also raster_compatible. |
|
||||||
|
| 194 | `test_brush_presets` | Verifies 5 brush presets have positive size and opacity. |
|
||||||
|
| 211 | `test_canvas_presets` | Verifies 1080p and Instagram canvas presets have correct dimensions. |
|
||||||
|
|
||||||
|
### `hcie-gui-egui` — tests/widget_ui.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 24 | `plain_slider_default_range` | Tests slider with in-range value (50 in 0..100) stays at 50. |
|
||||||
|
| 29 | `plain_slider_out_of_range_clamps` | Tests slider clamps value above range to max. |
|
||||||
|
| 34 | `plain_slider_negative_clamps` | Tests slider clamps negative value to range min. |
|
||||||
|
| 39 | `plain_slider_suffix` | Tests slider renders with "px" suffix. |
|
||||||
|
| 49 | `plain_slider_integer_type` | Tests slider with i32 integer type. |
|
||||||
|
| 58 | `plain_slider_custom_width` | Tests slider with custom width. |
|
||||||
|
| 68 | `plain_slider_both_themes` | Tests slider renders with both ProDark and PhotoshopLight themes. |
|
||||||
|
| 79 | `tool_state_default_active_tool_is_brush` | Verifies default active tool is Brush. |
|
||||||
|
| 85 | `tool_state_default_colors` | Verifies default primary (black) and secondary (white) colors. |
|
||||||
|
| 92 | `tool_state_default_pressure` | Verifies default pressure is 1.0. |
|
||||||
|
| 98 | `tool_state_default_not_drawing` | Verifies default state has is_drawing=false, no drag start, no selection rect. |
|
||||||
|
| 106 | `tool_state_default_tool_configs` | Verifies active_size() returns a positive value. |
|
||||||
|
| 113 | `tool_state_default_brush_presets` | Verifies default brush presets exist and active preset is "basic_round". |
|
||||||
|
| 120 | `tool_state_pinned_panels_default` | Verifies default pinned panels include Tools, Brushes & Tips, Color Palette, Layers. |
|
||||||
|
| 129 | `app_document_new_defaults` | Tests AppDocument default state (name, zoom=1, no textures, empty buffer). |
|
||||||
|
| 139 | `app_document_engine_accessible` | Verifies AppDocument wraps Engine with correct canvas dimensions. |
|
||||||
|
| 147 | `app_document_default_name` | Tests AppDocument name is set correctly. |
|
||||||
|
| 153 | `event_bus_push_pop` | Tests EventBus push and pop single event. |
|
||||||
|
| 166 | `event_bus_fifo_order` | Tests EventBus maintains FIFO order. |
|
||||||
|
| 177 | `event_bus_drain` | Tests EventBus drain returns all events. |
|
||||||
|
| 189 | `event_bus_tool_changed` | Tests EventBus pushes/reads ToolChanged event. |
|
||||||
|
| 197 | `event_bus_zoom_set` | Tests EventBus pushes/reads ZoomSet event. |
|
||||||
|
| 205 | `event_bus_theme_changed` | Tests EventBus pushes/reads ThemeChanged event. |
|
||||||
|
| 216 | `event_bus_apply_filter` | Tests EventBus pushes/reads ApplyFilter event with params. |
|
||||||
|
| 226 | `event_bus_multiple_events` | Tests EventBus with three selection events in sequence. |
|
||||||
|
| 238 | `event_bus_new_empty` | Tests new EventBus is empty. |
|
||||||
|
| 245 | `event_bus_doc_created` | Tests EventBus pushes/reads DocCreated event with name, width, height. |
|
||||||
|
| 260 | `tool_button_selected` | Tests ToolButton renders in selected state. |
|
||||||
|
| 268 | `tool_button_unselected` | Tests ToolButton renders in unselected state. |
|
||||||
|
| 276 | `tool_button_both_themes` | Tests ToolButton renders with both themes. |
|
||||||
|
| 286 | `plain_slider_logarithmic` | Tests slider in logarithmic mode. |
|
||||||
|
| 296 | `plain_slider_snap_steps` | Tests slider with snap steps. |
|
||||||
|
| 306 | `plain_slider_percent_suffix` | Tests slider with "%" suffix. |
|
||||||
|
| 316 | `plain_slider_u8_range` | Tests slider with u8 range. |
|
||||||
|
| 325 | `plain_slider_usize_range` | Tests slider with usize range. |
|
||||||
|
| 334 | `selection_op_variants` | Tests SelectionOp enum variants can be constructed. |
|
||||||
|
| 343 | `event_bus_selection_shrink` | Tests EventBus SelectionShrink event. |
|
||||||
|
| 351 | `event_bus_selection_feather` | Tests EventBus SelectionFeather event. |
|
||||||
|
| 359 | `event_bus_selection_erode` | Tests EventBus SelectionErode event. |
|
||||||
|
| 367 | `event_bus_selection_fade` | Tests EventBus SelectionFade event. |
|
||||||
|
| 375 | `event_bus_clipboard_cut` | Tests EventBus ClipboardCut event. |
|
||||||
|
| 383 | `event_bus_clipboard_paste_special` | Tests EventBus ClipboardPasteSpecial event. |
|
||||||
|
| 391 | `event_bus_layer_added` | Tests EventBus LayerAdded event. |
|
||||||
|
| 399 | `event_bus_layer_deleted` | Tests EventBus LayerDeleted(42) event. |
|
||||||
|
| 407 | `event_bus_rotate_clockwise` | Tests EventBus RotateClockwise event. |
|
||||||
|
| 415 | `event_bus_rotate_counter_clockwise` | Tests EventBus RotateCounterClockwise event. |
|
||||||
|
| 423 | `event_bus_rotate_180` | Tests EventBus Rotate180 event. |
|
||||||
|
| 431 | `event_bus_rotate_layer` | Tests EventBus RotateLayer(45) event. |
|
||||||
|
| 439 | `event_bus_flip_horizontal` | Tests EventBus FlipHorizontal event. |
|
||||||
|
| 447 | `event_bus_flip_vertical` | Tests EventBus FlipVertical event. |
|
||||||
|
| 455 | `event_bus_layer_clear` | Tests EventBus LayerClear event. |
|
||||||
|
| 463 | `event_bus_transform_start` | Tests EventBus TransformStart event. |
|
||||||
|
| 471 | `event_bus_image_size` | Tests EventBus ImageSize event. |
|
||||||
|
| 479 | `event_bus_canvas_size` | Tests EventBus CanvasSize event. |
|
||||||
|
| 487 | `event_bus_ai_chat_submit` | Tests EventBus AiChatSubmit event. |
|
||||||
|
| 495 | `event_bus_adjustment_brightness_contrast` | Tests EventBus ApplyAdjustmentBrightnessContrast event. |
|
||||||
|
| 505 | `event_bus_adjustment_hue_saturation` | Tests EventBus ApplyAdjustmentHueSaturation event. |
|
||||||
|
| 515 | `event_bus_vector_shape_deleted` | Tests EventBus VectorShapeDeleted event. |
|
||||||
|
| 523 | `event_bus_file_export_import` | Tests EventBus Export/Import/ImportBrushes events. |
|
||||||
|
| 534 | `event_bus_align_all_axes` | Tests EventBus AlignLayer event for all 6 align axes (HCenter, VCenter, Top, Bottom, Left, Right). |
|
||||||
|
|
||||||
|
### `hcie-gui-egui` — src/brush_import.rs (internal)
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 604 | `test_extract_desc_block_names` | Tests extracting descriptor block names from an ABR brush file; verifies names are descriptive. |
|
||||||
|
| 623 | `test_abr_import_with_names` | Tests importing ABR brush presets; verifies bitmap presets have descriptive names. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layer 6: GUI — iced
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — tests/selection_test.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 11 | `test_selection_transform_is_empty` | Verifies empty SelectionTransform returns true for is_empty(). |
|
||||||
|
| 24 | `test_selection_transform_with_data` | Verifies non-empty SelectionTransform returns false for is_empty(). |
|
||||||
|
| 37 | `test_crop_state_lifecycle` | Tests CropState: start_drag, update_drag, end_drag, confirm lifecycle. |
|
||||||
|
| 63 | `test_crop_cancel` | Tests CropState cancel restores idle state. |
|
||||||
|
| 73 | `test_transform_handle_cursor_hints` | Verifies cursor_hint() returns correct strings for Move, TopLeft, Rotate handles. |
|
||||||
|
| 80 | `selection_modes_combine_alpha_without_losing_soft_edges` | Tests combine_masks with Replace, Add, Subtract, Intersect modes. |
|
||||||
|
| 99 | `irregular_mask_bounds_and_fill_spans_are_exact` | Tests mask_bounds and selected_spans on irregular mask. |
|
||||||
|
| 109 | `crop_clamps_drag_and_cancel_restores_idle_state` | Tests CropState start_drag_clamped, update_drag clamped to canvas, cancel restores state. |
|
||||||
|
| 120 | `clipboard_transform_is_centered_and_transform_geometry_is_safe` | Tests centered_transform produces correct position and sanitize_geometry handles NaN/negative sizes. |
|
||||||
|
| 136 | `inverse_affine_identity_sampling_is_exact` | Verifies the inverse-affine rasterizer preserves identity placement exactly. |
|
||||||
|
| 159 | `rotated_bounds_expand_to_cover_corners` | Tests that rotated bounds include every corner rather than clipping to unrotated box. |
|
||||||
|
| 177 | `rotated_handle_hit_test_and_resize_share_geometry` | Verifies rotated handle hit-testing and opposite-corner anchoring during resize. |
|
||||||
|
| 204 | `rotation_drag_adds_delta_to_original_angle` | Verifies rotation uses drag-start delta and retains pre-existing rotation. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — tests/feature_scorecard.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 55 | `semantic_menu_dispatch_has_no_positional_message` | Verifies menu uses semantic MenuCommand dispatch not positional (usize, usize). |
|
||||||
|
| 73 | `cut_does_not_dispatch_copy` | Verifies Cut uses distinct MenuCommand::Cut not menu-alias-based Copy. |
|
||||||
|
| 91 | `cycle_three_canvas_selection_clipboard_crop_text_paths_are_connected` | Verifies selection overlay, distinct cut, transform placement, crop lifecycle, text multiline, gradient, canvas input parity, and pen/spray distinct. |
|
||||||
|
| 152 | `screenshot_cli_uses_native_capture_and_panel_crop` | Verifies screenshot CLI uses native window capture and panel crop functions. |
|
||||||
|
| 170 | `cycle_one_state_paths_are_complete_and_persisted` | Verifies dock drop targets, theme sync isolation, and sidebar mode persistence. |
|
||||||
|
| 211 | `cycle_two_vector_completion_paths_are_connected` | Verifies vector screen-constant handles, constrained drag, layer-bound cancel, boolean result, and clamped controls. |
|
||||||
|
| 252 | `cycle_four_panel_and_document_paths_are_connected` | Verifies layer hierarchy controls, style cancel/restore, immutable previews, filter schema coverage, document path identity, and close-save continuation. |
|
||||||
|
| 316 | `cycle_five_practical_paths_are_connected` | Verifies AI real streaming, reasoning transcript, validated script output, viewer deterministic navigation, and safe edit. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — tests/raster_test.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 6 | `linear_gradient_interpolates_and_respects_mask` | Tests linear gradient interpolation with pixel mask applied. |
|
||||||
|
| 27 | `radial_gradient_uses_distance_from_origin` | Tests radial gradient using distance from origin. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/app.rs (internal)
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 1536 | `move_commit_pushes_one_named_snapshot` | Tests apply_transform_to_layer pushes exactly one history entry named "Move Selection". |
|
||||||
|
| 10083 | `popup_and_menu_escape_priority_is_stable` | Tests overlay_escape_target returns correct priority ordering. |
|
||||||
|
| 10098 | `document_tab_close_uses_semantic_continuations` | Tests document_close_disposition for dirty, clean, last-tab, and invalid scenarios. |
|
||||||
|
| 10119 | `document_tab_close_keeps_active_index_safe` | Tests active_index_after_document_close produces safe indices. |
|
||||||
|
| 10128 | `selection_sync_request_is_consumed_once` | Tests consume_selection_sync_request flag is consumed once and stays false until reset. |
|
||||||
|
| 10140 | `dirty_region_union_retains_all_updates_before_view` | Tests union_regions correctly merges two overlapping regions. |
|
||||||
|
| 10152 | `proximate_identical_color_is_skipped` | Tests is_proximate_to_last returns true for identical colors (diff=0 ≤ threshold). |
|
||||||
|
| 10163 | `proximate_small_slider_step_skipped` | Tests is_proximate_to_last returns true for +1 channel change. |
|
||||||
|
| 10171 | `large_change_not_proximate` | Tests is_proximate_to_last returns false for +3 in all channels. |
|
||||||
|
| 10179 | `three_unit_change_exceeds_threshold` | Tests is_proximate_to_last returns false for single +3 channel change. |
|
||||||
|
| 10187 | `mixed_small_changes_are_proximate` | Tests is_proximate_to_last returns true for +1 in all channels. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/app/transform_dirty.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 70 | `consecutive_positions_retain_old_and_new_extents` | Tests that two consecutive transform positions produce correct old+new union bounds. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/color_picker.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 1048 | `rgb_hsl_roundtrip_preserves_channels_with_rounding_tolerance` | Tests RGB→HSL→RGB roundtrip for 4 colors with ≤1 tolerance per channel. |
|
||||||
|
| 1059 | `rgb_hsv_roundtrip_preserves_channels_with_rounding_tolerance` | Tests RGB→HSV→RGB roundtrip for 4 colors with ≤1 tolerance per channel. |
|
||||||
|
| 1070 | `hex_parser_preserves_or_explicitly_updates_alpha` | Tests parse_hex_color with 6-digit, 8-digit, and invalid hex strings. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/ai_chat.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 794 | `canvas_context_replaces_only_latest_user_payload` | Tests build_messages_json replaces only the latest user message with canvas context. |
|
||||||
|
| 818 | `transcript_includes_reasoning_and_tool_results` | Tests transcript_text includes reasoning and tool messages correctly. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/ai_script.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 244 | `generated_output_must_parse_before_run_is_enabled` | Tests validate_script_output accepts valid DSL and rejects invalid input. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/screenshot.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 126 | `physical_crop_scales_and_clamps` | Tests physical_crop_rect scaling rounds outward and clamps to viewport. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/sidebar/mod.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 858 | `every_subtool_has_an_svg_icon` | Tests that every tool in TOOL_SLOTS has a resolvable SVG icon path. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/theme.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 65 | `theme_state_retains_and_reports_selected_preset` | Tests ThemeState preset changes are retained and colors updated accordingly. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/viewer/mod.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 989 | `navigation_wraps_deterministically` | Tests ViewerState prev/next wraps correctly with 2 images. |
|
||||||
|
| 999 | `supported_extensions_are_case_insensitive` | Tests is_supported_image is case-insensitive and rejects non-image files. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/settings.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 367 | `legacy_settings_default_new_cycle_one_fields` | Tests deserializing legacy settings (without new fields) gets safe defaults. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/cli.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 206 | `parses_full_screenshot_and_startup_file` | Tests CLI parses --screenshot flag with output file and startup file. |
|
||||||
|
| 223 | `parses_named_panel_screenshot` | Tests CLI parses --screenshot-panel flag. |
|
||||||
|
| 229 | `parses_svg_editor_screenshot` | Tests CLI parses --screenshot-svg-editor flag. |
|
||||||
|
| 237 | `rejects_duplicate_positional_files` | Tests CLI rejects multiple positional file arguments. |
|
||||||
|
| 242 | `supports_dash_prefixed_file_after_terminator` | Tests CLI supports dash-prefixed filenames after -- terminator. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/canvas/shader_canvas.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 1282 | `selection_overlay_uses_one_texture_sample` | Verifies the WGSL shader uses exactly one `textureSample(selection_texture` call (anti-regression for nine-sample detector). |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/canvas/texture_update.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 196 | `retained_shader_reference_keeps_stable_allocation_and_dirty_budget` | Tests that retained shader reference keeps stable allocation and correct dirty budget over 120 updates. |
|
||||||
|
| 227 | `legacy_arc_copy_on_write_reproduction_is_full_canvas` | Demonstrates that Arc::get_mut on a cloned Arc returns None, forcing 33MB fallback clone (documents removed CoW fallback). |
|
||||||
|
| 237 | `packed_update_is_exact_and_bounds_checked` | Tests TextureUpdate::pack produces correct rows and rejects invalid regions. |
|
||||||
|
| 253 | `diagnostic_4k_dirty_staging_latency` | Diagnostic benchmark (ignored) measuring 4K dirty region staging latency and byte reduction vs legacy. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/selection/state.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 188 | `selection_texture_encodes_membership_and_border_once` | Tests encode_selection_texture correctly classifies interior (128), border (255), and empty (0). |
|
||||||
|
| 212 | `feathered_mask_uses_existing_threshold` | Tests feathered mask encoding: values >127 become 255 (border), ≤127 become 0 (outside). |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/panels/menus.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 1205 | `enabled_menu_leaves_have_commands` | Verifies enabled menu items always have a command and submenus never have commands. |
|
||||||
|
| 1232 | `recent_file_commands_are_path_based` | Tests recent file menu items use path-based commands, not positional indices. |
|
||||||
|
| 1253 | `implemented_static_menu_leaves_match_audited_set` | Snapshot test: verifies all 153 enabled menu entries match expected paths. |
|
||||||
|
| 1423 | `context_cut_is_enabled_and_distinct_from_copy` | Verifies context Cut uses MenuCommand::Cut with enabled=true. |
|
||||||
|
| 1432 | `shared_menu_metrics_keep_rows_compact_and_gutters_stable` | Verifies MENU_ROW_HEIGHT, paddings, gutters, and arrow character are within expected ranges. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/panels/styles.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 319 | `tooltip_balloon_is_opaque_and_uses_theme_contrast_tokens` | Tests tooltip balloon background opacity, text color, border, and shadow in two themes. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/panels/custom_shapes.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 211 | `responsive_grid_adds_columns_as_width_grows` | Tests grid_column_count adds one column per ~64px of width (with margin). |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/panels/brushes.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 874 | `catalog_contains_every_engine_brush_style` | Asserts BRUSH_STYLES has exactly 34 entries. |
|
||||||
|
| 879 | `category_filter_keeps_expected_groups` | Tests category_matches_style for various category combinations. |
|
||||||
|
| 900 | `media_crates_expose_complete_unique_catalog` | Verifies all media brush crates combined give exactly 47 unique presets. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/panels/svg_editor.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 874 | `straight_paths_retain_author_nodes` | Tests straight SVG paths retain their exact node count. |
|
||||||
|
| 882 | `curved_paths_fall_back_to_dense_visual_nodes` | Tests curved SVG paths flatten to ≥19 visual nodes. |
|
||||||
|
| 892 | `selection_updates_numeric_inputs_and_snap_is_deterministic` | Tests node selection updates x/y inputs, snap function works correctly, and toggling snap off. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/panels/title_bar.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 299 | `menu_anchors_are_deterministic_across_viewport_widths` | Tests menu_anchor_x calculation for various indices and viewport widths. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/vector_edit.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 386 | `overlap_selection_cycles_without_external_counter` | Tests cycle_selection cycles through hit indices in reverse order without external state. |
|
||||||
|
| 397 | `handle_hit_area_is_screen_constant_across_zoom` | Tests hit_test_handle returns correct handle at two different zoom levels. |
|
||||||
|
| 416 | `rotation_handle_offset_is_screen_constant_at_low_zoom` | Tests rotation handle offset is screen-constant at 0.25 zoom. |
|
||||||
|
| 430 | `every_resize_handle_prevents_inversion_and_nan` | Tests all 8 resize handles prevent inversion and NaN with extreme/finite deltas. |
|
||||||
|
| 463 | `shift_aspect_and_alt_center_constraints_hold` | Tests Shift+Alt resize preserves 2:1 aspect ratio and original center. |
|
||||||
|
| 480 | `drag_is_deterministic_and_rejects_non_finite_input` | Tests transform_shape returns correct moved bounds and handles NaN gracefully. |
|
||||||
|
| 498 | `rotated_handle_hit_test_uses_rotated_position` | Tests hit_test_handle works correctly with a 90° rotated shape. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/widgets/plain_slider.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 442 | `percentage_format_parse_and_clamp_are_stable` | Tests slider format_value with "%" suffix and parse_value clamping. |
|
||||||
|
| 450 | `hover_typing_accepts_only_numeric_fragments` | Tests typing state accepts "-" and numeric fragments, rejects "px". |
|
||||||
|
| 460 | `pointer_mapping_respects_endpoints_and_step` | Tests value_at maps 0→0%, 100→100%, 33→~33%. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/dock/state.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 161 | `pane_labels_round_trip` | Tests PaneType::from_label round-trip is case-sensitive. |
|
||||||
|
| 168 | `pane_drops_support_all_regions_and_preserve_one_canvas` | Tests all dock drop regions accept non-Canvas panes and reject Center for non-Canvas. |
|
||||||
|
| 195 | `canvas_drop_is_rejected_and_auto_hide_restores_uniquely` | Tests Canvas pane drops reject, auto-hide/restore works, and invariants hold. |
|
||||||
|
| 214 | `floating_panel_redocks_at_preview_target_and_can_minimize` | Tests floating panel float, redock at target, auto-hide floating, and invariants. |
|
||||||
|
| 234 | `global_release_clears_click_only_and_floating_drag_state` | Tests cancel_transient_drags clears both drag and floating_drag state. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/dock/manager.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 145 | `policy_matrix_allows_only_tool_edges` | Tests DockDropPolicy::accepts rejects Canvas for Center and allows non-Canvas for edges. |
|
||||||
|
| 162 | `document_cannot_close_auto_hide_or_float` | Tests DockDropPolicy::accepts_placement rejects Canvas for Closed, AutoHidden, Floating. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/dock/sizing.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 436 | `policy_values_prioritize_canvas_and_bound_color_picker` | Tests panel_size_policy values for Canvas (min 320×240, grow priority) and ColorPicker (preferred 280×460, bounded). |
|
||||||
|
| 451 | `subtree_constraints_follow_split_axis` | Tests aggregate_subtree_policy produces correct min/max for Canvas+ColorPicker split. |
|
||||||
|
| 462 | `default_and_compact_viewports_have_feasible_bounds` | Tests both 1280×820 and 1100×700 viewports satisfy all panel minimums in default dock. |
|
||||||
|
| 483 | `color_picker_does_not_receive_vertical_surplus_with_list_sibling` | Tests vertical surplus distribution keeps ColorPicker within its height bounds with a Layers sibling. |
|
||||||
|
| 509 | `structure_rebalance_avoids_oversized_color_picker` | Tests StructureChanged rebalance corrects 50/50 split to give Canvas >60%. |
|
||||||
|
| 526 | `user_resize_preserves_valid_and_clamps_invalid_ratios` | Tests constrain_user_resize_ratio preserves 0.7 and clamps 0.95 to a feasible range. |
|
||||||
|
| 536 | `window_growth_gives_surplus_to_canvas` | Tests WindowResize rebalance gives all extra width to Canvas, not utility panels. |
|
||||||
|
| 555 | `color_picker_minimum_height_is_420` | Asserts ColorPicker minimum height policy is exactly 420 pixels. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/dock/persistence.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 171 | `persisted_layout_round_trips_without_runtime_ids` | Tests PersistedDockLayout serialization strips runtime Pane IDs and round-trips with valid invariants. |
|
||||||
|
| 180 | `malformed_tree_deduplicates_and_injects_canvas` | Tests restore handles duplicate panels (two Layers) by deduplicating and injecting Canvas. |
|
||||||
|
| 201 | `missing_new_fields_are_serde_compatible` | Tests that minimal JSON ({"tree":null}) deserializes with empty auto_hidden and floating. |
|
||||||
|
| 208 | `ratio_sanitization_accepts_extreme_finite_values_only` | Tests sanitized_ratio accepts 0.02, clamps 0.999→0.99, and converts NaN→0.5. |
|
||||||
|
| 221 | `floating_round_trip_clamps_and_redocks_uniquely` | Tests floating panel serialization, clamp within viewport, and unique redock. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/dock/view.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 707 | `title_bar_uses_explicit_drag_handle_and_separate_controls` | Verifies the dock title bar uses explicit mouse_area drag handle and separate controls, not Fill space. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/dock/preview.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 198 | `centers_are_never_approved` | Tests approved_target returns None for grid center point. |
|
||||||
|
| 210 | `outer_and_pane_edges_are_approved` | Tests approved_target correctly identifies left outer edge and top pane edge. |
|
||||||
|
| 230 | `narrow_viewport_clamps_to_empty_bounds` | Tests narrow viewport (toolbox > viewport width) produces zero-size grid. |
|
||||||
|
| 239 | `edge_classification_matches_iced_thirds_and_priority` | Tests pane_edge returns correct edge within first third and None in middle. |
|
||||||
|
| 257 | `outer_edge_uses_iced_dynamic_thickness_and_priority` | Tests outer_edge returns correct edge within first 10px and None beyond. |
|
||||||
|
|
||||||
|
### `hcie-iced-gui` — src/dock/floating.rs
|
||||||
|
|
||||||
|
| Line | Function | Summary |
|
||||||
|
|------|----------|---------|
|
||||||
|
| 118 | `default_rect_clamps_to_small_viewports` | Tests default_rect clamps position so 240×180 window fits in 4-pane viewport. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Brush Catalog Crates
|
||||||
|
|
||||||
|
| Crate | File | Line | Function | Summary |
|
||||||
|
|-------|------|------|----------|---------|
|
||||||
|
| `hcie-dry-media-brushes` | src/lib.rs | 246 | `dry_media_catalog_is_complete_and_unique` | Verifies exactly 19 dry media presets with unique IDs. |
|
||||||
|
| `hcie-paint-brushes` | src/lib.rs | 129 | `paint_catalog_is_complete_and_unique` | Verifies exactly 7 paint presets with unique IDs. |
|
||||||
|
| `hcie-digital-brushes` | src/lib.rs | 144 | `digital_catalog_separates_raster_and_vector_tools` | Verifies 5 digital presets and 2 vector brush presets. |
|
||||||
|
| `hcie-watercolor-brushes` | src/lib.rs | 438 | `presets_count` | Verifies exactly 8 watercolor presets. |
|
||||||
|
| `hcie-watercolor-brushes` | src/lib.rs | 443 | `all_presets_use_watercolor_style` | Verifies all watercolor presets use BrushStyle::Watercolor. |
|
||||||
|
| `hcie-watercolor-brushes` | src/lib.rs | 450 | `splat_dimensions` | Verifies render_watercolor_splat produces SPLAT_SIZE×SPLAT_SIZE×4 pixels. |
|
||||||
|
| `hcie-watercolor-brushes` | src/lib.rs | 456 | `splat_has_nonzero_alpha` | Verifies the rendered splat has at least one visible (non-zero alpha) pixel. |
|
||||||
|
| `hcie-ink-brushes` | src/lib.rs | 141 | `ink_catalog_is_complete_and_unique` | Verifies exactly 8 ink presets with unique IDs. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Category | Approx. Count |
|
||||||
|
|----------|--------------|
|
||||||
|
| Layer 1: DATA (protocol, color) | 27 |
|
||||||
|
| Layer 2: ENGINE CORE (blend, brush, draw, composite, filter, selection, vector, history, fx, io, psd, vision, build-info) | ~155 |
|
||||||
|
| Layer 4: ENGINE API | 15 |
|
||||||
|
| Layer 6: GUI — egui (canvas_engine, gui_audit, widget_ui, brush_import) | ~90 |
|
||||||
|
| Layer 6: GUI — iced (selection, feature_scorecard, raster, app, dock, panels, widgets, color, ai, etc.) | ~120+ |
|
||||||
|
| Brush catalog crates | 8 |
|
||||||
|
| **Total** | **~415+** |
|
||||||
Reference in New Issue
Block a user