fix(infra): gradle_run detecta android-sdk — issue 0076 #2
@@ -0,0 +1,79 @@
|
||||
// Tests para la logica numerica del bar_chart (computo de limites de eje Y,
|
||||
// escala, etc.).
|
||||
//
|
||||
// La funcion `bar_chart` real usa ImPlot para renderizar y no expone helpers
|
||||
// puros. Aqui replicamos los calculos que el componente necesita (max-min,
|
||||
// padding del eje, anchura de barras) para garantizar que la matematica de
|
||||
// soporte es correcta. Los tests visuales viven en primitives_gallery (0048).
|
||||
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch_amalgamated.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
struct YRange { double lo; double hi; };
|
||||
|
||||
YRange compute_y_range(const double* values, int count, double pad = 0.1) {
|
||||
if (count <= 0) return {0.0, 1.0};
|
||||
double lo = values[0], hi = values[0];
|
||||
for (int i = 1; i < count; ++i) {
|
||||
if (values[i] < lo) lo = values[i];
|
||||
if (values[i] > hi) hi = values[i];
|
||||
}
|
||||
// Asegura siempre incluir 0 en el eje (convencion para barras).
|
||||
if (lo > 0.0) lo = 0.0;
|
||||
if (hi < 0.0) hi = 0.0;
|
||||
double span = hi - lo;
|
||||
if (span == 0.0) span = 1.0;
|
||||
return {lo - span * pad * (lo < 0.0 ? 1.0 : 0.0),
|
||||
hi + span * pad};
|
||||
}
|
||||
|
||||
double clamp_bar_width(double w) {
|
||||
if (w < 0.05) return 0.05;
|
||||
if (w > 1.0) return 1.0;
|
||||
return w;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("bar_chart: y_range covers all positive values", "[bar_chart]") {
|
||||
double v[] = {1.0, 2.0, 3.0, 4.0, 5.0};
|
||||
auto r = compute_y_range(v, 5);
|
||||
REQUIRE(r.lo == Catch::Approx(0.0));
|
||||
REQUIRE(r.hi >= 5.0);
|
||||
}
|
||||
|
||||
TEST_CASE("bar_chart: y_range includes zero baseline", "[bar_chart]") {
|
||||
double v[] = {3.0, 5.0, 7.0};
|
||||
auto r = compute_y_range(v, 3);
|
||||
REQUIRE(r.lo == Catch::Approx(0.0));
|
||||
}
|
||||
|
||||
TEST_CASE("bar_chart: y_range with negatives extends below zero", "[bar_chart]") {
|
||||
double v[] = {-2.0, 1.0, 3.0};
|
||||
auto r = compute_y_range(v, 3);
|
||||
REQUIRE(r.lo <= -2.0);
|
||||
REQUIRE(r.hi >= 3.0);
|
||||
}
|
||||
|
||||
TEST_CASE("bar_chart: y_range with empty data is sane default", "[bar_chart]") {
|
||||
auto r = compute_y_range(nullptr, 0);
|
||||
REQUIRE(r.lo < r.hi);
|
||||
}
|
||||
|
||||
TEST_CASE("bar_chart: y_range with single value still has span", "[bar_chart]") {
|
||||
double v[] = {7.0};
|
||||
auto r = compute_y_range(v, 1);
|
||||
REQUIRE(r.hi > r.lo);
|
||||
}
|
||||
|
||||
TEST_CASE("bar_chart: clamp_bar_width clamps to [0.05, 1.0]", "[bar_chart]") {
|
||||
REQUIRE(clamp_bar_width(0.001) == Catch::Approx(0.05));
|
||||
REQUIRE(clamp_bar_width(0.5) == Catch::Approx(0.5));
|
||||
REQUIRE(clamp_bar_width(0.67) == Catch::Approx(0.67));
|
||||
REQUIRE(clamp_bar_width(2.0) == Catch::Approx(1.0));
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Tests para la logica de calculo del KPI card.
|
||||
//
|
||||
// kpi_card es un componente UI tightly coupled con ImGui (`ImGui::BeginGroup`,
|
||||
// formatting, sparkline rendering...). La logica testeable es trivial:
|
||||
// - delta_percent positivo -> "trending up"
|
||||
// - delta_percent negativo -> "trending down"
|
||||
// - delta_percent ~ 0 -> "flat"
|
||||
//
|
||||
// Aqui replicamos la funcion de clasificacion para garantizar que el signo
|
||||
// del delta se interpreta correctamente. La parte visual (color, icono) se
|
||||
// cubre en la primitives_gallery (issue 0048).
|
||||
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch_amalgamated.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
|
||||
enum class Trend { Down, Flat, Up };
|
||||
|
||||
Trend classify_delta(float delta_percent, float flat_threshold = 0.05f) {
|
||||
if (std::fabs(delta_percent) <= flat_threshold) return Trend::Flat;
|
||||
return (delta_percent > 0.0f) ? Trend::Up : Trend::Down;
|
||||
}
|
||||
|
||||
float pct_change(float current, float previous) {
|
||||
if (previous == 0.0f) return 0.0f;
|
||||
return 100.0f * (current - previous) / previous;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("kpi_card: classify_delta positive -> Up", "[kpi_card]") {
|
||||
REQUIRE(classify_delta(0.5f) == Trend::Up);
|
||||
REQUIRE(classify_delta(12.5f) == Trend::Up);
|
||||
REQUIRE(classify_delta(100.0f) == Trend::Up);
|
||||
}
|
||||
|
||||
TEST_CASE("kpi_card: classify_delta negative -> Down", "[kpi_card]") {
|
||||
REQUIRE(classify_delta(-0.5f) == Trend::Down);
|
||||
REQUIRE(classify_delta(-15.0f) == Trend::Down);
|
||||
REQUIRE(classify_delta(-100.0f) == Trend::Down);
|
||||
}
|
||||
|
||||
TEST_CASE("kpi_card: classify_delta near zero -> Flat", "[kpi_card]") {
|
||||
REQUIRE(classify_delta(0.0f) == Trend::Flat);
|
||||
REQUIRE(classify_delta(0.01f) == Trend::Flat);
|
||||
REQUIRE(classify_delta(-0.01f) == Trend::Flat);
|
||||
REQUIRE(classify_delta(0.05f) == Trend::Flat); // exactly threshold
|
||||
}
|
||||
|
||||
TEST_CASE("kpi_card: pct_change basic deltas", "[kpi_card]") {
|
||||
REQUIRE(pct_change(110.0f, 100.0f) == Catch::Approx(10.0f));
|
||||
REQUIRE(pct_change(90.0f, 100.0f) == Catch::Approx(-10.0f));
|
||||
REQUIRE(pct_change(100.0f, 100.0f) == Catch::Approx(0.0f));
|
||||
REQUIRE(pct_change(200.0f, 100.0f) == Catch::Approx(100.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("kpi_card: pct_change with zero previous returns 0", "[kpi_card]") {
|
||||
// Definicion: cambio porcentual de 0 a X es indefinido, devolvemos 0.
|
||||
REQUIRE(pct_change(50.0f, 0.0f) == Catch::Approx(0.0f));
|
||||
REQUIRE(pct_change(0.0f, 0.0f) == Catch::Approx(0.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("kpi_card: pct_change handles negative values", "[kpi_card]") {
|
||||
// -10 -> -5: ((-5) - (-10)) / (-10) * 100 = 5 / -10 * 100 = -50%.
|
||||
REQUIRE(pct_change(-5.0f, -10.0f) == Catch::Approx(-50.0f));
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Tests para la logica matematica de pie_chart (slice hit-testing).
|
||||
//
|
||||
// La funcion `slice_at` real vive en namespace anonimo en pie_chart.cpp y
|
||||
// esta tightly coupled con ImPlot. Aqui reproducimos el algoritmo y lo
|
||||
// testeamos para garantizar que la matematica es correcta. Si la implementacion
|
||||
// real se refactoriza para exponer la logica pura, este test se actualiza
|
||||
// para usar el header publico (ver issue 0048 / refactor pendiente).
|
||||
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch_amalgamated.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
|
||||
// Mismo algoritmo que pie_chart.cpp::slice_at (verbatim).
|
||||
template <typename T>
|
||||
int slice_at(const T* values, int count, double total, double mouse_x,
|
||||
double mouse_y, double cx, double cy, double radius) {
|
||||
double dx = mouse_x - cx;
|
||||
double dy = mouse_y - cy;
|
||||
double r = std::sqrt(dx * dx + dy * dy);
|
||||
if (r > radius) return -1;
|
||||
|
||||
constexpr double kPI = 3.14159265358979323846;
|
||||
double angle_deg = std::atan2(dy, dx) * 180.0 / kPI;
|
||||
double offset = angle_deg - 90.0;
|
||||
while (offset < 0.0) offset += 360.0;
|
||||
while (offset >= 360.0) offset -= 360.0;
|
||||
|
||||
double acc = 0.0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
double sweep = (static_cast<double>(values[i]) / total) * 360.0;
|
||||
if (offset >= acc && offset < acc + sweep) return i;
|
||||
acc += sweep;
|
||||
}
|
||||
return count - 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("pie_chart::slice_at returns -1 outside radius", "[pie_chart]") {
|
||||
double values[] = {1.0, 1.0, 1.0, 1.0};
|
||||
double total = 4.0;
|
||||
// Mouse muy lejos del centro.
|
||||
REQUIRE(slice_at(values, 4, total, 5.0, 5.0, 0.5, 0.5, 0.4) == -1);
|
||||
}
|
||||
|
||||
TEST_CASE("pie_chart::slice_at on cursor at center returns first slice", "[pie_chart]") {
|
||||
double values[] = {1.0, 1.0, 1.0, 1.0};
|
||||
double total = 4.0;
|
||||
// En el centro, dx=dy=0 -> atan2(0,0)=0 -> offset=-90 -> 270.
|
||||
// Con 4 slices iguales (cada una 90 grados), 270 cae en la 4a slice (idx 3).
|
||||
int idx = slice_at(values, 4, total, 0.5, 0.5, 0.5, 0.5, 0.4);
|
||||
REQUIRE(idx >= 0);
|
||||
REQUIRE(idx < 4);
|
||||
}
|
||||
|
||||
TEST_CASE("pie_chart::slice_at directly above center returns first slice", "[pie_chart]") {
|
||||
// 4 slices iguales. offset=0 (arriba) cae en idx 0.
|
||||
double values[] = {1.0, 1.0, 1.0, 1.0};
|
||||
int idx = slice_at(values, 4, 4.0, 0.5, 0.7, 0.5, 0.5, 0.4);
|
||||
REQUIRE(idx == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("pie_chart::slice_at right of center hits second slice (CCW)", "[pie_chart]") {
|
||||
// ImPlot dibuja CCW desde 90 grados (arriba). Yendo CCW: arriba -> izq -> abajo -> der.
|
||||
// Mouse a la derecha: offset = atan2(0, +x)=0 deg -> -90 -> 270.
|
||||
// 4 slices: [0,90)=0, [90,180)=1, [180,270)=2, [270,360)=3 -> idx 3.
|
||||
double values[] = {1.0, 1.0, 1.0, 1.0};
|
||||
int idx = slice_at(values, 4, 4.0, 0.7, 0.5, 0.5, 0.5, 0.4);
|
||||
REQUIRE(idx == 3);
|
||||
}
|
||||
|
||||
TEST_CASE("pie_chart::slice_at single slice always returns 0", "[pie_chart]") {
|
||||
double values[] = {1.0};
|
||||
REQUIRE(slice_at(values, 1, 1.0, 0.5, 0.7, 0.5, 0.5, 0.4) == 0);
|
||||
REQUIRE(slice_at(values, 1, 1.0, 0.7, 0.5, 0.5, 0.5, 0.4) == 0);
|
||||
REQUIRE(slice_at(values, 1, 1.0, 0.5, 0.3, 0.5, 0.5, 0.4) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("pie_chart::slice_at right at radius edge", "[pie_chart]") {
|
||||
double values[] = {1.0, 1.0};
|
||||
// Justo en el borde del radio: r == radius -> r > radius es false, hits.
|
||||
int idx = slice_at(values, 2, 2.0, 0.5 + 0.4, 0.5, 0.5, 0.5, 0.4);
|
||||
REQUIRE(idx >= 0);
|
||||
// Justo fuera del borde.
|
||||
REQUIRE(slice_at(values, 2, 2.0, 0.5 + 0.41, 0.5, 0.5, 0.5, 0.4) == -1);
|
||||
}
|
||||
|
||||
TEST_CASE("pie_chart::slice_at unequal slices distributes proportionally", "[pie_chart]") {
|
||||
// Una slice grande (75%) y otra pequena (25%).
|
||||
double values[] = {3.0, 1.0};
|
||||
double total = 4.0;
|
||||
// Slice 0: offset [0, 270). Slice 1: offset [270, 360).
|
||||
// Mouse arriba (offset=0) -> idx 0.
|
||||
REQUIRE(slice_at(values, 2, total, 0.5, 0.7, 0.5, 0.5, 0.4) == 0);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Unit tests for fn::tween (cpp/functions/core/tween_curves.{h,cpp}).
|
||||
//
|
||||
// Cubre boundaries (t=0, t=1), valor central (t=0.5) y monotonicidad para las
|
||||
// curvas que deberian ser monotonas (linear, *_quad, *_cubic, *_expo, bounce
|
||||
// in/out, elastic in/out NO son monotonas — overshoot intencional).
|
||||
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch_amalgamated.hpp"
|
||||
|
||||
#include "core/tween_curves.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
using fn::tween::Ease;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kEps = 1e-5f;
|
||||
|
||||
bool is_monotonic_increasing(Ease e, int samples = 64) {
|
||||
float prev = fn::tween::apply(e, 0.0f);
|
||||
for (int i = 1; i <= samples; ++i) {
|
||||
float t = static_cast<float>(i) / samples;
|
||||
float v = fn::tween::apply(e, t);
|
||||
if (v < prev - kEps) return false;
|
||||
prev = v;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("tween: linear is identity", "[tween]") {
|
||||
REQUIRE(fn::tween::linear(0.0f) == Catch::Approx(0.0f));
|
||||
REQUIRE(fn::tween::linear(0.5f) == Catch::Approx(0.5f));
|
||||
REQUIRE(fn::tween::linear(1.0f) == Catch::Approx(1.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("tween: quadratic boundary conditions", "[tween]") {
|
||||
REQUIRE(fn::tween::in_quad(0.0f) == Catch::Approx(0.0f));
|
||||
REQUIRE(fn::tween::in_quad(1.0f) == Catch::Approx(1.0f));
|
||||
REQUIRE(fn::tween::out_quad(0.0f) == Catch::Approx(0.0f));
|
||||
REQUIRE(fn::tween::out_quad(1.0f) == Catch::Approx(1.0f));
|
||||
REQUIRE(fn::tween::in_out_quad(0.0f) == Catch::Approx(0.0f));
|
||||
REQUIRE(fn::tween::in_out_quad(1.0f) == Catch::Approx(1.0f));
|
||||
REQUIRE(fn::tween::in_out_quad(0.5f) == Catch::Approx(0.5f));
|
||||
}
|
||||
|
||||
TEST_CASE("tween: cubic boundary conditions", "[tween]") {
|
||||
REQUIRE(fn::tween::in_cubic(0.0f) == Catch::Approx(0.0f));
|
||||
REQUIRE(fn::tween::in_cubic(1.0f) == Catch::Approx(1.0f));
|
||||
REQUIRE(fn::tween::out_cubic(0.0f) == Catch::Approx(0.0f));
|
||||
REQUIRE(fn::tween::out_cubic(1.0f) == Catch::Approx(1.0f));
|
||||
REQUIRE(fn::tween::in_out_cubic(0.0f) == Catch::Approx(0.0f));
|
||||
REQUIRE(fn::tween::in_out_cubic(1.0f) == Catch::Approx(1.0f));
|
||||
REQUIRE(fn::tween::in_out_cubic(0.5f) == Catch::Approx(0.5f));
|
||||
}
|
||||
|
||||
TEST_CASE("tween: exponential boundary conditions", "[tween]") {
|
||||
REQUIRE(fn::tween::in_expo(0.0f) == Catch::Approx(0.0f));
|
||||
REQUIRE(fn::tween::in_expo(1.0f) == Catch::Approx(1.0f));
|
||||
REQUIRE(fn::tween::out_expo(0.0f) == Catch::Approx(0.0f));
|
||||
REQUIRE(fn::tween::out_expo(1.0f) == Catch::Approx(1.0f));
|
||||
REQUIRE(fn::tween::in_out_expo(0.0f) == Catch::Approx(0.0f));
|
||||
REQUIRE(fn::tween::in_out_expo(1.0f) == Catch::Approx(1.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("tween: in_quad < out_quad in mid-range", "[tween]") {
|
||||
// Convexity: in_quad starts slow, out_quad starts fast.
|
||||
REQUIRE(fn::tween::in_quad(0.5f) < fn::tween::out_quad(0.5f));
|
||||
REQUIRE(fn::tween::in_cubic(0.3f) < fn::tween::out_cubic(0.3f));
|
||||
}
|
||||
|
||||
TEST_CASE("tween: monotonic curves are monotonic", "[tween][monotonic]") {
|
||||
// Curvas monotonicamente crecientes en [0,1].
|
||||
REQUIRE(is_monotonic_increasing(Ease::Linear));
|
||||
REQUIRE(is_monotonic_increasing(Ease::InQuad));
|
||||
REQUIRE(is_monotonic_increasing(Ease::OutQuad));
|
||||
REQUIRE(is_monotonic_increasing(Ease::InOutQuad));
|
||||
REQUIRE(is_monotonic_increasing(Ease::InCubic));
|
||||
REQUIRE(is_monotonic_increasing(Ease::OutCubic));
|
||||
REQUIRE(is_monotonic_increasing(Ease::InOutCubic));
|
||||
REQUIRE(is_monotonic_increasing(Ease::InExpo));
|
||||
REQUIRE(is_monotonic_increasing(Ease::OutExpo));
|
||||
REQUIRE(is_monotonic_increasing(Ease::InOutExpo));
|
||||
}
|
||||
|
||||
TEST_CASE("tween: elastic and bounce hit endpoints exactly", "[tween]") {
|
||||
// Elastic / bounce overshoot pero deben pasar por (0,0) y (1,1).
|
||||
for (Ease e : {Ease::InElastic, Ease::OutElastic, Ease::InOutElastic,
|
||||
Ease::InBounce, Ease::OutBounce, Ease::InOutBounce}) {
|
||||
INFO("ease: " << fn::tween::name(e));
|
||||
REQUIRE(fn::tween::apply(e, 0.0f) == Catch::Approx(0.0f).margin(1e-4f));
|
||||
REQUIRE(fn::tween::apply(e, 1.0f) == Catch::Approx(1.0f).margin(1e-4f));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("tween: apply dispatch matches direct calls", "[tween]") {
|
||||
REQUIRE(fn::tween::apply(Ease::Linear, 0.42f) == Catch::Approx(fn::tween::linear(0.42f)));
|
||||
REQUIRE(fn::tween::apply(Ease::InCubic, 0.3f) == Catch::Approx(fn::tween::in_cubic(0.3f)));
|
||||
REQUIRE(fn::tween::apply(Ease::OutBounce, 0.7f) == Catch::Approx(fn::tween::out_bounce(0.7f)));
|
||||
}
|
||||
|
||||
TEST_CASE("tween: name() returns non-null for every Ease", "[tween]") {
|
||||
for (int i = 0; i < fn::tween::ease_count; ++i) {
|
||||
const char* n = fn::tween::name(static_cast<Ease>(i));
|
||||
REQUIRE(n != nullptr);
|
||||
REQUIRE(std::string(n).size() > 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user