// test_column_specs.cpp — Smoke / back-compat tests for declarative cell renderers. // Issue 0081-N, v1.1.0. Phase 2 (issue 0081-O, v1.2.0). // Phase 2.5 (issue 0081-O.5, v1.3.0): Dots renderer. // v1.4.0: CategoricalChip + ColorScale renderers (TestCategoricalChipRule, // TestColorScaleLerpTwoStops, TestColorScaleLerpThreeStops, // TestColorScaleOutOfRange). // // These tests verify: // 1. TableInput without column_specs compiles and links (back-compat). // 2-5. TableInput with Badge/Progress/Duration/Icon column_specs compiles and links. // 6. Button renderer: TableEvent struct is constructible; events_out pointer accepted. // 7. Tooltip field: ColumnSpec with tooltip_on_hover=true compiles and links. // 8. render() overload with events_out=nullptr back-compat (symbol resolution only). // 9. Dots renderer: ColumnSpec with CellRenderer::Dots + badges constructs correctly. // 10. Dots TQL roundtrip: State::aux_column_specs accepts Dots spec. // 11. TestCategoricalChipRule: ChipRule with match="success" produces correct color. // 12. TestColorScaleLerpTwoStops: t=0→first color, t=1→last color, t=0.5→midpoint. // 13. TestColorScaleLerpThreeStops: t=0.25 lies between stop0 and stop1. // 14. TestColorScaleOutOfRange: t<0 saturates at first; t>1 saturates at last. // // None of these tests call data_table::render() (requires ImGui context). // They only verify that the new types are usable and that the symbols from // fn_table_viz link correctly. // // Build: cmake --build cpp/build/linux --target test_column_specs // Run: ./cpp/build/linux/tests/test_column_specs #include "core/data_table_types.h" #include "data_table/data_table.h" #include #include #include #include using namespace data_table; // Shared trivial dataset (3 rows x 4 cols). static const char* g_cells[] = { "ok", "0.75", "250", "fn", "error", "0.20", "3500", "type", "warn", "1.00", "12000", "fn", }; static const std::vector g_headers = {"status", "progress", "duration_ms", "kind"}; static const std::vector g_types = { ColumnType::String, ColumnType::Float, ColumnType::Float, ColumnType::String }; // --------------------------------------------------------------------------- // Test 1: back-compat — TableInput without column_specs. // --------------------------------------------------------------------------- static void test_no_column_specs() { TableInput t; t.name = "t1"; t.rows = 3; t.cols = 4; t.cells = g_cells; t.headers = g_headers; t.types = g_types; // column_specs intentionally left empty (back-compat: default behavior). assert(t.column_specs.empty() && "column_specs must be empty by default"); // Verify that render symbol is still linkable (no ImGui context needed // to take the address; the linker verifies the symbol resolves). // Use the classic overload (without events_out) for the back-compat check. auto* render_fn = static_cast&, State&, bool)>(&data_table::render); (void)render_fn; std::printf("PASS: test_no_column_specs (back-compat, column_specs empty)\n"); } // --------------------------------------------------------------------------- // Test 2: Badge renderer — construct TableInput with Badge column_spec. // --------------------------------------------------------------------------- static void test_badge_column_spec() { TableInput t; t.name = "t2"; t.rows = 3; t.cols = 4; t.cells = g_cells; t.headers = g_headers; t.types = g_types; // Column 0: Badge renderer mapping ok/error/warn to colors. ColumnSpec cs_status; cs_status.id = "status"; cs_status.renderer = CellRenderer::Badge; cs_status.badges = { BadgeRule{"ok", "#22c55e", "OK"}, BadgeRule{"error", "#ef4444", ""}, // label empty -> use value BadgeRule{"warn", "#f59e0b", "WARN"}, }; // Remaining columns: default Text. t.column_specs.resize(4); // default-initialized = CellRenderer::Text t.column_specs[0] = cs_status; assert(t.column_specs.size() == 4); assert(t.column_specs[0].renderer == CellRenderer::Badge); assert(t.column_specs[0].badges.size() == 3); assert(t.column_specs[1].renderer == CellRenderer::Text); std::printf("PASS: test_badge_column_spec (3 badge rules, remaining cols Text)\n"); } // --------------------------------------------------------------------------- // Test 3: Progress renderer. // --------------------------------------------------------------------------- static void test_progress_column_spec() { TableInput t; t.name = "t3"; t.rows = 3; t.cols = 4; t.cells = g_cells; t.headers = g_headers; t.types = g_types; ColumnSpec cs_progress; cs_progress.id = "progress"; cs_progress.renderer = CellRenderer::Progress; cs_progress.progress_scale_100 = false; cs_progress.progress_color_hex = "#3b82f6"; t.column_specs.resize(4); t.column_specs[1] = cs_progress; assert(t.column_specs[1].renderer == CellRenderer::Progress); assert(!t.column_specs[1].progress_scale_100); assert(t.column_specs[1].progress_color_hex == "#3b82f6"); std::printf("PASS: test_progress_column_spec\n"); } // --------------------------------------------------------------------------- // Test 4: Duration renderer. // --------------------------------------------------------------------------- static void test_duration_column_spec() { TableInput t; t.name = "t4"; t.rows = 3; t.cols = 4; t.cells = g_cells; t.headers = g_headers; t.types = g_types; ColumnSpec cs_dur; cs_dur.id = "duration_ms"; cs_dur.renderer = CellRenderer::Duration; cs_dur.duration_warn_ms = 500.0f; cs_dur.duration_error_ms = 2000.0f; t.column_specs.resize(4); t.column_specs[2] = cs_dur; assert(t.column_specs[2].renderer == CellRenderer::Duration); assert(t.column_specs[2].duration_warn_ms == 500.0f); assert(t.column_specs[2].duration_error_ms == 2000.0f); std::printf("PASS: test_duration_column_spec (warn=500ms error=2000ms)\n"); } // --------------------------------------------------------------------------- // Test 5: Icon renderer. // --------------------------------------------------------------------------- static void test_icon_column_spec() { TableInput t; t.name = "t5"; t.rows = 3; t.cols = 4; t.cells = g_cells; t.headers = g_headers; t.types = g_types; ColumnSpec cs_icon; cs_icon.id = "kind"; cs_icon.renderer = CellRenderer::Icon; cs_icon.icon_map = { IconMapEntry{"fn", "TI_BOLT", "#3b82f6"}, IconMapEntry{"type", "TI_DATABASE", ""}, }; t.column_specs.resize(4); t.column_specs[3] = cs_icon; assert(t.column_specs[3].renderer == CellRenderer::Icon); assert(t.column_specs[3].icon_map.size() == 2); assert(t.column_specs[3].icon_map[0].value == "fn"); assert(t.column_specs[3].icon_map[0].icon_name == "TI_BOLT"); // Verify render symbol still links with column_specs populated (classic overload). auto* render_fn = static_cast&, State&, bool)>(&data_table::render); (void)render_fn; std::printf("PASS: test_icon_column_spec (2 entries, render symbol links)\n"); } // --------------------------------------------------------------------------- // Test 6: Button renderer — TableEvent struct is constructible; events_out ptr // is accepted by the new render() overload (symbol resolution only). // --------------------------------------------------------------------------- static void test_button_column_spec_and_event_struct() { TableInput t; t.name = "t6"; t.rows = 3; t.cols = 4; t.cells = g_cells; t.headers = g_headers; t.types = g_types; ColumnSpec cs_btn; cs_btn.id = "actions"; cs_btn.renderer = CellRenderer::Button; cs_btn.button_action = "cancel"; cs_btn.button_label = "Cancel"; cs_btn.button_color_hex = "#ef4444"; t.column_specs.resize(4); t.column_specs[0] = cs_btn; assert(t.column_specs[0].renderer == CellRenderer::Button); assert(t.column_specs[0].button_action == "cancel"); assert(t.column_specs[0].button_label == "Cancel"); assert(t.column_specs[0].button_color_hex == "#ef4444"); // Verify TableEvent struct can be constructed and holds expected fields. TableEvent ev; ev.kind = TableEventKind::ButtonClick; ev.row = 1; ev.col = 0; ev.column_id = "actions"; ev.action_id = "cancel"; ev.value = "ok"; assert(ev.kind == TableEventKind::ButtonClick); assert(ev.row == 1); assert(ev.action_id == "cancel"); // Verify the render() overload with events_out is linkable. std::vector events; auto* render_with_events = static_cast&, State&, std::vector*, bool)>(&data_table::render); (void)render_with_events; std::printf("PASS: test_button_column_spec_and_event_struct " "(Button spec + TableEvent + render overload link)\n"); } // --------------------------------------------------------------------------- // Test 7: Tooltip field — ColumnSpec with tooltip_on_hover=true. // --------------------------------------------------------------------------- static void test_tooltip_column_spec() { TableInput t; t.name = "t7"; t.rows = 3; t.cols = 4; t.cells = g_cells; t.headers = g_headers; t.types = g_types; ColumnSpec cs_tip; cs_tip.id = "status"; cs_tip.renderer = CellRenderer::Text; // tooltip works on any renderer cs_tip.tooltip = "auto"; // "auto" -> show cell value cs_tip.tooltip_on_hover = true; t.column_specs.resize(4); t.column_specs[0] = cs_tip; assert(t.column_specs[0].tooltip == "auto"); assert(t.column_specs[0].tooltip_on_hover == true); // Also test explicit tooltip text. ColumnSpec cs_tip2; cs_tip2.id = "progress"; cs_tip2.renderer = CellRenderer::Progress; cs_tip2.tooltip = "Progress percentage (0..1)"; cs_tip2.tooltip_on_hover = true; t.column_specs[1] = cs_tip2; assert(t.column_specs[1].tooltip == "Progress percentage (0..1)"); assert(t.column_specs[1].tooltip_on_hover == true); std::printf("PASS: test_tooltip_column_spec (auto + explicit, tooltip_on_hover=true)\n"); } // --------------------------------------------------------------------------- // Test 8: render() back-compat overload without events_out — symbol links. // --------------------------------------------------------------------------- static void test_render_backcompat_overload() { // Verify both render() signatures are resolvable at link time. // Classic (no events_out): auto* render_classic = static_cast&, State&, bool)>(&data_table::render); (void)render_classic; // New (with events_out): auto* render_events = static_cast&, State&, std::vector*, bool)>(&data_table::render); (void)render_events; std::printf("PASS: test_render_backcompat_overload (both render() signatures link)\n"); } // --------------------------------------------------------------------------- // Test 9: Dots renderer — ColumnSpec with CellRenderer::Dots + badges. // Cell value "ok,error,ok,running,ok" — 5 tokens, 5 badge rules, dots_max=5. // --------------------------------------------------------------------------- static void test_dots_column_spec() { // Dataset with one column holding comma-separated run statuses. static const char* cells_dots[] = { "ok,error,ok,running,ok", "ok,ok,ok", "failed,failed", }; std::vector hdrs = {"recent"}; std::vector types = {ColumnType::String}; TableInput t; t.name = "t_dots"; t.rows = 3; t.cols = 1; t.cells = cells_dots; t.headers = hdrs; t.types = types; ColumnSpec cs; cs.id = "recent"; cs.renderer = CellRenderer::Dots; cs.badges = { BadgeRule{"ok", "#22c55e", ""}, // default glyph ● BadgeRule{"error", "#ef4444", ""}, BadgeRule{"failed", "#ef4444", ""}, BadgeRule{"running", "#eab308", ""}, BadgeRule{"pending", "#94a3b8", ""}, }; cs.dots_max = 5; cs.dots_show_count = false; cs.tooltip_on_hover = true; t.column_specs.resize(1); t.column_specs[0] = cs; assert(t.column_specs[0].renderer == CellRenderer::Dots); assert(t.column_specs[0].dots_max == 5); assert(t.column_specs[0].badges.size() == 5); assert(t.column_specs[0].dots_show_count == false); assert(t.column_specs[0].tooltip_on_hover == true); assert(t.column_specs[0].dots_separator == ','); // Verify that the struct is copyable and the enum value is distinct. ColumnSpec cs2 = cs; assert(cs2.renderer == CellRenderer::Dots); assert(cs2.dots_max == 5); std::printf("PASS: test_dots_column_spec " "(5 badge rules, dots_max=5, tooltip_on_hover=true)\n"); } // --------------------------------------------------------------------------- // Test 10: Dots TQL roundtrip — emit + apply preserves Dots fields. // --------------------------------------------------------------------------- static void test_dots_tql_roundtrip() { // Import tql::emit / tql::apply via the smoke test include paths. // This test checks that the new Dots fields survive a TQL roundtrip. // We populate aux_column_specs[0] and verify apply reconstructs them. // We only check struct construction + enum identity here (no Lua context // available without the full fn_table_viz library; the roundtrip is covered // by test_fn_table_viz_smoke.cpp test_tql_roundtrip). ColumnSpec cs; cs.id = "recent"; cs.renderer = CellRenderer::Dots; cs.dots_separator = ','; cs.dots_max = 5; cs.dots_show_count = false; cs.badges = {{"ok", "#22c55e", ""}, {"error", "#ef4444", ""}}; // Build a State with aux_column_specs to verify the container accepts Dots. data_table::State st; st.stages.push_back(data_table::Stage{}); st.aux_column_specs.push_back({cs}); assert(st.aux_column_specs.size() == 1); assert(st.aux_column_specs[0].size() == 1); assert(st.aux_column_specs[0][0].renderer == CellRenderer::Dots); assert(st.aux_column_specs[0][0].dots_max == 5); assert(st.aux_column_specs[0][0].badges.size() == 2); std::printf("PASS: test_dots_tql_roundtrip " "(State::aux_column_specs accepts Dots spec)\n"); } // --------------------------------------------------------------------------- // Test 11: TestCategoricalChipRule — ChipRule with match="success" correct color. // Verifies ChipRule struct construction + ColumnSpec.chips field accessible. // --------------------------------------------------------------------------- static void test_categorical_chip_rule() { ColumnSpec cs; cs.id = "state"; cs.renderer = CellRenderer::CategoricalChip; cs.chips = { ChipRule{"success", "#22c55e"}, ChipRule{"failure", "#ef4444"}, ChipRule{"pending", "#f59e0b"}, }; assert(cs.renderer == CellRenderer::CategoricalChip); assert(cs.chips.size() == 3); assert(cs.chips[0].match == "success"); assert(cs.chips[0].color == "#22c55e"); assert(cs.chips[1].match == "failure"); assert(cs.chips[1].color == "#ef4444"); assert(cs.chips[2].match == "pending"); // No matching rule for "unknown" — chips lookup returns nullptr (logic check). const ChipRule* found = nullptr; const char* test_val = "unknown"; for (const auto& cr : cs.chips) { if (cr.match == test_val) { found = &cr; break; } } assert(found == nullptr && "no rule should match 'unknown'"); // Match "success" should find first rule. const ChipRule* found2 = nullptr; for (const auto& cr : cs.chips) { if (cr.match == std::string("success")) { found2 = &cr; break; } } assert(found2 != nullptr && found2->color == "#22c55e"); std::printf("PASS: TestCategoricalChipRule " "(3 chip rules, match/no-match logic correct)\n"); } // --------------------------------------------------------------------------- // Headless color lerp helpers (mirrors the static functions in data_table.cpp, // replicated here so tests run without ImGui context). // Uses a plain struct RGB3 instead of std::tuple to avoid extra includes. // --------------------------------------------------------------------------- struct RGB3 { float r, g, b; }; static float lerp_f(float a, float b, float t) { return a + t * (b - a); } // Parse "#rrggbb" -> RGB3 floats in [0,1]. Returns {-1,-1,-1} on failure. static RGB3 parse_rgb(const std::string& hex) { const char* p = hex.c_str(); if (*p == '#') ++p; unsigned int r = 0, g = 0, b = 0; if (std::sscanf(p, "%02x%02x%02x", &r, &g, &b) != 3) return {-1.f, -1.f, -1.f}; return {r / 255.f, g / 255.f, b / 255.f}; } // Lerp between two ColorStop RGB colors at a given global t. static RGB3 lerp_between(const ColorStop& lo, const ColorStop& hi, float t_global) { float span = hi.position - lo.position; float f = (span > 1e-6f) ? (t_global - lo.position) / span : 0.f; RGB3 ca = parse_rgb(lo.color); RGB3 cb = parse_rgb(hi.color); return {lerp_f(ca.r,cb.r,f), lerp_f(ca.g,cb.g,f), lerp_f(ca.b,cb.b,f)}; } // lerp_stops: full N-stop lerp (same logic as lerp_color_along_stops in data_table.cpp). static RGB3 lerp_stops(const std::vector& stops, float t) { static const ColorStop kDefault[] = { {0.0f, "#22c55e"}, {0.5f, "#f59e0b"}, {1.0f, "#ef4444"} }; static const int kDefaultN = 3; // Build a working sorted copy. std::vector s; if (stops.empty()) { for (int i = 0; i < kDefaultN; ++i) s.push_back(kDefault[i]); } else { s = stops; } // Simple insertion sort (N is tiny, avoids std::sort include). for (size_t i = 1; i < s.size(); ++i) { ColorStop key = s[i]; int j = (int)i - 1; while (j >= 0 && s[j].position > key.position) { s[j+1] = s[j]; --j; } s[j+1] = key; } t = t < 0.f ? 0.f : (t > 1.f ? 1.f : t); if (t <= s.front().position) return parse_rgb(s.front().color); if (t >= s.back().position) return parse_rgb(s.back().color); for (size_t i = 0; i + 1 < s.size(); ++i) { if (t >= s[i].position && t <= s[i+1].position) return lerp_between(s[i], s[i+1], t); } return parse_rgb(s.back().color); } // --------------------------------------------------------------------------- // Test 12: TestColorScaleLerpTwoStops — t=0→first, t=1→last, t=0.5→midpoint. // --------------------------------------------------------------------------- static void test_color_scale_lerp_two_stops() { std::vector stops = { {0.0f, "#000000"}, // black {1.0f, "#ffffff"}, // white }; ColumnSpec cs; cs.renderer = CellRenderer::ColorScale; cs.range_min = 0.0; cs.range_max = 1.0; cs.range_stops = stops; cs.range_alpha = 0.25f; assert(cs.renderer == CellRenderer::ColorScale); assert(cs.range_stops.size() == 2); // t=0.0 → black (0,0,0) RGB3 c0 = lerp_stops(stops, 0.0f); assert(c0.r < 0.01f && c0.g < 0.01f && c0.b < 0.01f); // t=1.0 → white (1,1,1) RGB3 c1 = lerp_stops(stops, 1.0f); assert(c1.r > 0.99f && c1.g > 0.99f && c1.b > 0.99f); // t=0.5 → midpoint (0.5, 0.5, 0.5) within floating-point tolerance RGB3 c5 = lerp_stops(stops, 0.5f); assert(c5.r > 0.49f && c5.r < 0.51f); assert(c5.g > 0.49f && c5.g < 0.51f); assert(c5.b > 0.49f && c5.b < 0.51f); std::printf("PASS: TestColorScaleLerpTwoStops " "(t=0→black, t=1→white, t=0.5→mid-grey)\n"); } // --------------------------------------------------------------------------- // Test 13: TestColorScaleLerpThreeStops — t=0.25 between stop0 and stop1. // Stops: {0.0,red}, {0.5,green}, {1.0,blue}. // At t=0.25 we expect halfway between red and green. // --------------------------------------------------------------------------- static void test_color_scale_lerp_three_stops() { // red=#ff0000, green=#00ff00, blue=#0000ff std::vector stops = { {0.0f, "#ff0000"}, // red {0.5f, "#00ff00"}, // green {1.0f, "#0000ff"}, // blue }; // t=0.25 is halfway between stop0 (t=0) and stop1 (t=0.5). // Lerp factor f = (0.25 - 0.0) / (0.5 - 0.0) = 0.5. // Expected: R = lerp(1,0,0.5)=0.5, G = lerp(0,1,0.5)=0.5, B = lerp(0,0,0.5)=0. RGB3 ca = lerp_stops(stops, 0.25f); assert(ca.r > 0.49f && ca.r < 0.51f && "R should be ~0.5 at t=0.25"); assert(ca.g > 0.49f && ca.g < 0.51f && "G should be ~0.5 at t=0.25"); assert(ca.b < 0.01f && "B should be ~0 at t=0.25"); // t=0.75 is halfway between stop1 (t=0.5) and stop2 (t=1.0). // Expected: R=0, G=0.5, B=0.5. RGB3 cb = lerp_stops(stops, 0.75f); assert(cb.r < 0.01f && "R should be ~0 at t=0.75"); assert(cb.g > 0.49f && cb.g < 0.51f && "G should be ~0.5 at t=0.75"); assert(cb.b > 0.49f && cb.b < 0.51f && "B should be ~0.5 at t=0.75"); std::printf("PASS: TestColorScaleLerpThreeStops " "(t=0.25 between stop0/stop1, t=0.75 between stop1/stop2)\n"); } // --------------------------------------------------------------------------- // Test 14: TestColorScaleOutOfRange — t<0 saturates at first; t>1 at last. // --------------------------------------------------------------------------- static void test_color_scale_out_of_range() { std::vector stops = { {0.0f, "#ff0000"}, // red at t=0 {1.0f, "#0000ff"}, // blue at t=1 }; // t=-0.5 → clamp to 0 → red RGB3 cu = lerp_stops(stops, -0.5f); assert(cu.r > 0.99f && "under-range should saturate at first stop (red)"); assert(cu.b < 0.01f); // t=1.5 → clamp to 1 → blue RGB3 co = lerp_stops(stops, 1.5f); assert(co.r < 0.01f && "over-range should saturate at last stop (blue)"); assert(co.b > 0.99f); // ColumnSpec struct fields accessible and defaults sensible. ColumnSpec cs; cs.renderer = CellRenderer::ColorScale; cs.range_min = -10.0; cs.range_max = 10.0; assert(cs.range_alpha == 0.25f && "default range_alpha should be 0.25"); assert(cs.range_stops.empty() && "default range_stops should be empty (→ use default gradient)"); std::printf("PASS: TestColorScaleOutOfRange " "(t<0 saturates at first stop, t>1 saturates at last stop)\n"); } // --------------------------------------------------------------------------- // main // --------------------------------------------------------------------------- int main() { std::printf("=== test_column_specs ===\n"); test_no_column_specs(); test_badge_column_spec(); test_progress_column_spec(); test_duration_column_spec(); test_icon_column_spec(); test_button_column_spec_and_event_struct(); test_tooltip_column_spec(); test_render_backcompat_overload(); test_dots_column_spec(); test_dots_tql_roundtrip(); test_categorical_chip_rule(); test_color_scale_lerp_two_stops(); test_color_scale_lerp_three_stops(); test_color_scale_out_of_range(); std::printf("=== ALL TESTS PASSED (14/14) ===\n"); return 0; }