From 353e2e9b9268bbeedb18ec3827977aeb2d36b765 Mon Sep 17 00:00:00 2001 From: kapumota Date: Mon, 8 Jun 2026 23:40:34 -0500 Subject: [PATCH] fase 9.1: agrega tests unitarios en C con Unity --- Makefile | 10 ++ docs/TESTS_UNITARIOS.md | 122 +++++++++++++++++ tests/unit/test_graph_model.c | 138 +++++++++++++++++++ tests/unit/test_probabilistic.c | 230 ++++++++++++++++++++++++++++++++ tests/unit/test_risk_helpers.c | 83 ++++++++++++ third_party/unity/README.md | 17 +++ third_party/unity/unity.c | 35 +++++ third_party/unity/unity.h | 32 +++++ 8 files changed, 667 insertions(+) create mode 100644 docs/TESTS_UNITARIOS.md create mode 100644 tests/unit/test_graph_model.c create mode 100644 tests/unit/test_probabilistic.c create mode 100644 tests/unit/test_risk_helpers.c create mode 100644 third_party/unity/README.md create mode 100644 third_party/unity/unity.c create mode 100644 third_party/unity/unity.h diff --git a/Makefile b/Makefile index e576a83..0015802 100644 --- a/Makefile +++ b/Makefile @@ -175,3 +175,13 @@ ebpf-info: .PHONY: test-guardsketch test-guardsketch: bash tests/test_guardsketch.sh + +.PHONY: test-unit +test-unit: + mkdir -p obj/unit + $(CC) $(CFLAGS) -Ithird_party/unity tests/unit/test_probabilistic.c third_party/unity/unity.c -lm -o obj/unit/test_probabilistic + $(CC) $(CFLAGS) -Ithird_party/unity tests/unit/test_graph_model.c third_party/unity/unity.c -o obj/unit/test_graph_model + $(CC) $(CFLAGS) -Ithird_party/unity tests/unit/test_risk_helpers.c third_party/unity/unity.c -o obj/unit/test_risk_helpers + ./obj/unit/test_probabilistic + ./obj/unit/test_graph_model + ./obj/unit/test_risk_helpers diff --git a/docs/TESTS_UNITARIOS.md b/docs/TESTS_UNITARIOS.md new file mode 100644 index 0000000..28c3bc2 --- /dev/null +++ b/docs/TESTS_UNITARIOS.md @@ -0,0 +1,122 @@ +### Tests unitarios en C con Unity + +#### Objetivo + +La Fase 9.1 agrega pruebas unitarias reales en C para fortalecer la base interna de AgentGuard FastPath antes del release `v1.0.0`. + +Hasta esta fase, el proyecto ya cuenta con pruebas de integración, regresión, streaming, GuardSketch y evaluación experimental. + +Esta fase agrega una capa más pequeña y controlada para validar propiedades internas. + +#### Por qué Unity + +Unity se usa porque: + +- es liviano; +- puede versionarse en `third_party/`; +- no requiere instalación del sistema; +- funciona bien con C puro; +- es apropiado para CI; +- permite probar invariantes simples sin depender de servicios externos. + +#### Estructura agregada + +```text +third_party/unity/ +tests/unit/ +tests/unit/test_probabilistic.c +tests/unit/test_graph_model.c +tests/unit/test_risk_helpers.c +``` + +#### Target agregado + +```bash +make test-unit +``` + +#### Pruebas probabilísticas + +El archivo `tests/unit/test_probabilistic.c` valida modelos unitarios para: + +- Bloom Filter; +- Count-Min Sketch; +- HyperLogLog; +- Space-Saving; +- Odd Sketch. + +Propiedades cubiertas: + +- un elemento insertado en Bloom debe encontrarse; +- Bloom no debe producir falsos negativos para elementos insertados; +- Count-Min Sketch no debe subestimar frecuencias insertadas; +- claves distintas deben seguir siendo consultables; +- HyperLogLog para conjunto vacío debe estimar cerca de cero; +- elementos repetidos no deben crecer como cardinalidad única; +- Space-Saving debe conservar elementos frecuentes; +- Odd Sketch debe dar más coincidencia para entradas similares. + +#### Pruebas de grafo + +El archivo `tests/unit/test_graph_model.c` valida un modelo pequeño de grafo. + +Propiedades cubiertas: + +- insertar nodo de proceso; +- insertar relación proceso-archivo; +- insertar relación proceso-destino; +- rechazar arista si falta un nodo; +- consultar PID inexistente; +- respetar límite de recorrido. + +#### Pruebas de riesgo + +El archivo `tests/unit/test_risk_helpers.c` valida helpers pequeños para scoring. + +Propiedades cubiertas: + +- score cero sin señales; +- score alto con archivo sensible y destino bloqueado; +- score máximo limitado a 100; +- niveles de riesgo; +- incremento por destinos únicos. + +#### Alcance + +Esta fase no modifica `src/`. + +Esta fase no cambia el CLI. + +Esta fase no cambia el motor de detección. + +Esta fase no agrega dependencias del sistema. + +Esta fase no integra todavía los tests con una biblioteca interna porque el proyecto aún conserva una arquitectura de binario principal. + +#### Limitación honesta + +Estos tests son unitarios autocontenidos y validan invariantes de diseño. + +Una fase posterior puede conectar estos tests directamente a una biblioteca `libagfast` cuando el motor se separe formalmente del CLI. + +#### Validación completa + +```bash +make clean +make +make test-fastpath +make test-regression +make test-streaming +make test-guardsketch +make test-unit +make clean +``` + +#### Criterio de aceptación + +La fase se considera correcta si: + +- `make test-unit` compila y ejecuta las tres suites; +- no se requiere instalar Unity; +- no se rompe ninguna prueba previa; +- no se modifica el código fuente de producción. diff --git a/tests/unit/test_graph_model.c b/tests/unit/test_graph_model.c new file mode 100644 index 0000000..f1cbfc2 --- /dev/null +++ b/tests/unit/test_graph_model.c @@ -0,0 +1,138 @@ +#include "unity.h" + +#include +#include +#include + +/* Tests unitarios autocontenidos para un modelo de grafo mínimo. */ + +#define GRAPH_MAX_NODES 32u +#define GRAPH_MAX_EDGES 64u +#define GRAPH_NAME_SIZE 64u + +typedef enum { GRAPH_NODE_PROCESS = 1, GRAPH_NODE_FILE = 2, GRAPH_NODE_DESTINATION = 3 } graph_node_type_t; + +typedef struct { int id; graph_node_type_t type; char name[GRAPH_NAME_SIZE]; bool used; } graph_node_t; + +typedef struct { int from; int to; char relation[GRAPH_NAME_SIZE]; bool used; } graph_edge_t; + +typedef struct { graph_node_t nodes[GRAPH_MAX_NODES]; graph_edge_t edges[GRAPH_MAX_EDGES]; } graph_model_t; + +static void graphInit(graph_model_t *graph) { memset(graph, 0, sizeof(*graph)); } + +static bool graphAddNode(graph_model_t *graph, int id, graph_node_type_t type, const char *name) { + for (size_t i = 0; i < GRAPH_MAX_NODES; ++i) { + if (graph->nodes[i].used && graph->nodes[i].id == id) { return true; } + } + for (size_t i = 0; i < GRAPH_MAX_NODES; ++i) { + if (!graph->nodes[i].used) { + graph->nodes[i].used = true; + graph->nodes[i].id = id; + graph->nodes[i].type = type; + snprintf(graph->nodes[i].name, sizeof(graph->nodes[i].name), "%s", name); + return true; + } + } + return false; +} + +static const graph_node_t *graphFindNode(const graph_model_t *graph, int id) { + for (size_t i = 0; i < GRAPH_MAX_NODES; ++i) { + if (graph->nodes[i].used && graph->nodes[i].id == id) { return &graph->nodes[i]; } + } + return NULL; +} + +static bool graphAddEdge(graph_model_t *graph, int from, int to, const char *relation) { + if (graphFindNode(graph, from) == NULL || graphFindNode(graph, to) == NULL) { return false; } + for (size_t i = 0; i < GRAPH_MAX_EDGES; ++i) { + if (!graph->edges[i].used) { + graph->edges[i].used = true; + graph->edges[i].from = from; + graph->edges[i].to = to; + snprintf(graph->edges[i].relation, sizeof(graph->edges[i].relation), "%s", relation); + return true; + } + } + return false; +} + +static int graphCountEdgesFrom(const graph_model_t *graph, int from) { + int count = 0; + for (size_t i = 0; i < GRAPH_MAX_EDGES; ++i) { + if (graph->edges[i].used && graph->edges[i].from == from) { count += 1; } + } + return count; +} + +static int graphTraverseLimited(const graph_model_t *graph, int from, int limit) { + int emitted = 0; + for (size_t i = 0; i < GRAPH_MAX_EDGES && emitted < limit; ++i) { + if (graph->edges[i].used && graph->edges[i].from == from) { emitted += 1; } + } + return emitted; +} + +static void testGraphInsertProcessNode(void) { + graph_model_t graph; + const graph_node_t *node = NULL; + graphInit(&graph); + TEST_ASSERT_TRUE(graphAddNode(&graph, 501, GRAPH_NODE_PROCESS, "python")); + node = graphFindNode(&graph, 501); + TEST_ASSERT_NOT_NULL(node); + TEST_ASSERT_EQUAL_INT(GRAPH_NODE_PROCESS, node->type); +} + +static void testGraphInsertProcessFileRelation(void) { + graph_model_t graph; + graphInit(&graph); + TEST_ASSERT_TRUE(graphAddNode(&graph, 501, GRAPH_NODE_PROCESS, "python")); + TEST_ASSERT_TRUE(graphAddNode(&graph, 1001, GRAPH_NODE_FILE, "/etc/passwd")); + TEST_ASSERT_TRUE(graphAddEdge(&graph, 501, 1001, "open")); + TEST_ASSERT_EQUAL_INT(1, graphCountEdgesFrom(&graph, 501)); +} + +static void testGraphInsertProcessDestinationRelation(void) { + graph_model_t graph; + graphInit(&graph); + TEST_ASSERT_TRUE(graphAddNode(&graph, 501, GRAPH_NODE_PROCESS, "python")); + TEST_ASSERT_TRUE(graphAddNode(&graph, 2001, GRAPH_NODE_DESTINATION, "203.0.113.10")); + TEST_ASSERT_TRUE(graphAddEdge(&graph, 501, 2001, "connect")); + TEST_ASSERT_EQUAL_INT(1, graphCountEdgesFrom(&graph, 501)); +} + +static void testGraphRejectEdgeWithMissingNode(void) { + graph_model_t graph; + graphInit(&graph); + TEST_ASSERT_TRUE(graphAddNode(&graph, 501, GRAPH_NODE_PROCESS, "python")); + TEST_ASSERT_FALSE(graphAddEdge(&graph, 501, 9999, "connect")); +} + +static void testGraphFindMissingPid(void) { + graph_model_t graph; + graphInit(&graph); + TEST_ASSERT_TRUE(graphAddNode(&graph, 501, GRAPH_NODE_PROCESS, "python")); + TEST_ASSERT_TRUE(graphFindNode(&graph, 9999) == NULL); +} + +static void testGraphTraversalLimit(void) { + graph_model_t graph; + graphInit(&graph); + TEST_ASSERT_TRUE(graphAddNode(&graph, 501, GRAPH_NODE_PROCESS, "python")); + for (int i = 0; i < 8; ++i) { + TEST_ASSERT_TRUE(graphAddNode(&graph, 2000 + i, GRAPH_NODE_DESTINATION, "destino")); + TEST_ASSERT_TRUE(graphAddEdge(&graph, 501, 2000 + i, "connect")); + } + TEST_ASSERT_EQUAL_INT(3, graphTraverseLimited(&graph, 501, 3)); +} + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(testGraphInsertProcessNode); + RUN_TEST(testGraphInsertProcessFileRelation); + RUN_TEST(testGraphInsertProcessDestinationRelation); + RUN_TEST(testGraphRejectEdgeWithMissingNode); + RUN_TEST(testGraphFindMissingPid); + RUN_TEST(testGraphTraversalLimit); + return UNITY_END(); +} diff --git a/tests/unit/test_probabilistic.c b/tests/unit/test_probabilistic.c new file mode 100644 index 0000000..493a51e --- /dev/null +++ b/tests/unit/test_probabilistic.c @@ -0,0 +1,230 @@ +#include "unity.h" + +#include +#include +#include + +/* Tests unitarios autocontenidos para invariantes probabilísticos. */ + +#define BLOOM_BITS 1024u +#define BLOOM_BYTES (BLOOM_BITS / 8u) +#define CMS_DEPTH 3u +#define CMS_WIDTH 128u +#define HLL_REGISTERS 64u +#define SPACE_K 3u +#define ODD_BITS 128u +#define ODD_BYTES (ODD_BITS / 8u) + +static uint64_t testHash64(uint64_t value, uint64_t seed) { + uint64_t x = value + seed + 0x9e3779b97f4a7c15ULL; + x ^= x >> 30; + x *= 0xbf58476d1ce4e5b9ULL; + x ^= x >> 27; + x *= 0x94d049bb133111ebULL; + x ^= x >> 31; + return x; +} + +typedef struct { uint8_t bits[BLOOM_BYTES]; } bloom_t; + +typedef struct { uint32_t table[CMS_DEPTH][CMS_WIDTH]; } cms_t; + +typedef struct { uint8_t registers[HLL_REGISTERS]; } hll_t; + +typedef struct { uint64_t key; uint32_t count; bool used; } space_item_t; + +typedef struct { space_item_t items[SPACE_K]; } space_saving_t; + +typedef struct { uint8_t bits[ODD_BYTES]; } odd_sketch_t; + +static void bloomAdd(bloom_t *bloom, uint64_t value) { + for (uint64_t i = 0; i < 4u; ++i) { + uint64_t pos = testHash64(value, i) % BLOOM_BITS; + bloom->bits[pos / 8u] = (uint8_t)(bloom->bits[pos / 8u] | (uint8_t)(1u << (pos % 8u))); + } +} + +static bool bloomMaybeContains(const bloom_t *bloom, uint64_t value) { + for (uint64_t i = 0; i < 4u; ++i) { + uint64_t pos = testHash64(value, i) % BLOOM_BITS; + if ((bloom->bits[pos / 8u] & (uint8_t)(1u << (pos % 8u))) == 0u) { + return false; + } + } + return true; +} + +static void cmsAdd(cms_t *cms, uint64_t key) { + for (uint64_t row = 0; row < CMS_DEPTH; ++row) { + uint64_t pos = testHash64(key, row + 11u) % CMS_WIDTH; + cms->table[row][pos] += 1u; + } +} + +static uint32_t cmsEstimate(const cms_t *cms, uint64_t key) { + uint32_t best = UINT32_MAX; + for (uint64_t row = 0; row < CMS_DEPTH; ++row) { + uint64_t pos = testHash64(key, row + 11u) % CMS_WIDTH; + if (cms->table[row][pos] < best) { + best = cms->table[row][pos]; + } + } + return best == UINT32_MAX ? 0u : best; +} + +static void hllAdd(hll_t *hll, uint64_t value) { + uint64_t hash = testHash64(value, 123u); + uint64_t idx = hash % HLL_REGISTERS; + uint8_t rank = (uint8_t)((hash >> 8) & 31u); + rank = (uint8_t)(rank == 0u ? 1u : rank); + if (rank > hll->registers[idx]) { + hll->registers[idx] = rank; + } +} + +static uint64_t hllNonZeroRegisters(const hll_t *hll) { + uint64_t count = 0u; + for (size_t i = 0; i < HLL_REGISTERS; ++i) { + if (hll->registers[i] != 0u) { + count += 1u; + } + } + return count; +} + +static void spaceSavingAdd(space_saving_t *ss, uint64_t key) { + size_t min_index = 0u; + for (size_t i = 0; i < SPACE_K; ++i) { + if (ss->items[i].used && ss->items[i].key == key) { + ss->items[i].count += 1u; + return; + } + } + for (size_t i = 0; i < SPACE_K; ++i) { + if (!ss->items[i].used) { + ss->items[i].used = true; + ss->items[i].key = key; + ss->items[i].count = 1u; + return; + } + } + for (size_t i = 1; i < SPACE_K; ++i) { + if (ss->items[i].count < ss->items[min_index].count) { + min_index = i; + } + } + ss->items[min_index].key = key; + ss->items[min_index].count += 1u; +} + +static bool spaceSavingContains(const space_saving_t *ss, uint64_t key) { + for (size_t i = 0; i < SPACE_K; ++i) { + if (ss->items[i].used && ss->items[i].key == key) { + return true; + } + } + return false; +} + +static void oddSketchToggle(odd_sketch_t *odd, uint64_t key) { + uint64_t pos = testHash64(key, 777u) % ODD_BITS; + odd->bits[pos / 8u] = (uint8_t)(odd->bits[pos / 8u] ^ (uint8_t)(1u << (pos % 8u))); +} + +static int oddSketchMatches(const odd_sketch_t *a, const odd_sketch_t *b) { + int matches = 0; + for (size_t i = 0; i < ODD_BYTES; ++i) { + uint8_t same = (uint8_t)~(a->bits[i] ^ b->bits[i]); + for (int bit = 0; bit < 8; ++bit) { + if ((same & (uint8_t)(1u << bit)) != 0u) { + matches += 1; + } + } + } + return matches; +} + +static void testBloomInsertedElementIsFound(void) { + bloom_t bloom; + memset(&bloom, 0, sizeof(bloom)); + bloomAdd(&bloom, 101u); + TEST_ASSERT_TRUE(bloomMaybeContains(&bloom, 101u)); +} + +static void testBloomNormallyRejectsAbsentElement(void) { + bloom_t bloom; + memset(&bloom, 0, sizeof(bloom)); + bloomAdd(&bloom, 101u); + bloomAdd(&bloom, 202u); + TEST_ASSERT_FALSE(bloomMaybeContains(&bloom, 99999u)); +} + +static void testCmsNeverUnderestimatesInsertedFrequency(void) { + cms_t cms; + memset(&cms, 0, sizeof(cms)); + for (int i = 0; i < 17; ++i) { cmsAdd(&cms, 700u); } + TEST_ASSERT_GREATER_OR_EQUAL_UINT64(17u, cmsEstimate(&cms, 700u)); +} + +static void testCmsDifferentKeysRemainUsable(void) { + cms_t cms; + memset(&cms, 0, sizeof(cms)); + for (int i = 0; i < 5; ++i) { cmsAdd(&cms, 10u); } + for (int i = 0; i < 3; ++i) { cmsAdd(&cms, 20u); } + TEST_ASSERT_GREATER_OR_EQUAL_UINT64(5u, cmsEstimate(&cms, 10u)); + TEST_ASSERT_GREATER_OR_EQUAL_UINT64(3u, cmsEstimate(&cms, 20u)); +} + +static void testHllEmptySetIsNearZero(void) { + hll_t hll; + memset(&hll, 0, sizeof(hll)); + TEST_ASSERT_EQUAL_UINT64(0u, hllNonZeroRegisters(&hll)); +} + +static void testHllRepeatedElementsDoNotGrowAsUnique(void) { + hll_t repeated; + hll_t unique; + memset(&repeated, 0, sizeof(repeated)); + memset(&unique, 0, sizeof(unique)); + for (int i = 0; i < 100; ++i) { + hllAdd(&repeated, 42u); + hllAdd(&unique, (uint64_t)i); + } + TEST_ASSERT_TRUE(hllNonZeroRegisters(&repeated) < hllNonZeroRegisters(&unique)); +} + +static void testSpaceSavingKeepsFrequentItem(void) { + space_saving_t ss; + memset(&ss, 0, sizeof(ss)); + for (int i = 0; i < 30; ++i) { spaceSavingAdd(&ss, 1001u); } + for (int i = 0; i < 10; ++i) { spaceSavingAdd(&ss, (uint64_t)(2000 + i)); } + TEST_ASSERT_TRUE(spaceSavingContains(&ss, 1001u)); +} + +static void testOddSketchSimilarInputsMatchMore(void) { + odd_sketch_t a; + odd_sketch_t b; + odd_sketch_t c; + memset(&a, 0, sizeof(a)); + memset(&b, 0, sizeof(b)); + memset(&c, 0, sizeof(c)); + for (uint64_t i = 0; i < 16u; ++i) { + oddSketchToggle(&a, i); + oddSketchToggle(&b, i); + oddSketchToggle(&c, i + 100u); + } + TEST_ASSERT_TRUE(oddSketchMatches(&a, &b) > oddSketchMatches(&a, &c)); +} + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(testBloomInsertedElementIsFound); + RUN_TEST(testBloomNormallyRejectsAbsentElement); + RUN_TEST(testCmsNeverUnderestimatesInsertedFrequency); + RUN_TEST(testCmsDifferentKeysRemainUsable); + RUN_TEST(testHllEmptySetIsNearZero); + RUN_TEST(testHllRepeatedElementsDoNotGrowAsUnique); + RUN_TEST(testSpaceSavingKeepsFrequentItem); + RUN_TEST(testOddSketchSimilarInputsMatchMore); + return UNITY_END(); +} diff --git a/tests/unit/test_risk_helpers.c b/tests/unit/test_risk_helpers.c new file mode 100644 index 0000000..0a737d2 --- /dev/null +++ b/tests/unit/test_risk_helpers.c @@ -0,0 +1,83 @@ +#include "unity.h" + +#include +#include +#include + +/* Tests unitarios para helpers de riesgo. */ + +typedef struct { + bool watched_process; + uint64_t sensitive_files; + uint64_t blocked_destinations; + uint64_t network_after_file; + uint64_t unique_destinations; + uint64_t event_volume; +} risk_features_t; + +static uint64_t riskScore(const risk_features_t *features) { + uint64_t score = 0u; + if (features->watched_process) { score += 10u; } + if (features->sensitive_files > 0u) { score += 25u; } + if (features->blocked_destinations > 0u) { score += 35u; } + if (features->network_after_file > 0u) { score += 30u; } + if (features->unique_destinations >= 10u) { score += 15u; } + if (features->event_volume >= 100u) { score += 10u; } + return score > 100u ? 100u : score; +} + +static const char *riskLevel(uint64_t score) { + if (score >= 80u) { return "critical"; } + if (score >= 60u) { return "high"; } + if (score >= 30u) { return "medium"; } + if (score > 0u) { return "low"; } + return "none"; +} + +static void testRiskScoreForNoSignalsIsZero(void) { + risk_features_t features = {0}; + TEST_ASSERT_EQUAL_UINT64(0u, riskScore(&features)); +} + +static void testRiskScoreForSensitiveAndBlockedIsHigh(void) { + risk_features_t features = {0}; + features.sensitive_files = 1u; + features.blocked_destinations = 1u; + TEST_ASSERT_EQUAL_UINT64(60u, riskScore(&features)); + TEST_ASSERT_TRUE(strcmp(riskLevel(riskScore(&features)), "high") == 0); +} + +static void testRiskScoreIsCappedAtOneHundred(void) { + risk_features_t features = {0}; + features.watched_process = true; + features.sensitive_files = 3u; + features.blocked_destinations = 2u; + features.network_after_file = 2u; + features.unique_destinations = 20u; + features.event_volume = 200u; + TEST_ASSERT_EQUAL_UINT64(100u, riskScore(&features)); +} + +static void testRiskLevels(void) { + TEST_ASSERT_TRUE(strcmp(riskLevel(0u), "none") == 0); + TEST_ASSERT_TRUE(strcmp(riskLevel(10u), "low") == 0); + TEST_ASSERT_TRUE(strcmp(riskLevel(30u), "medium") == 0); + TEST_ASSERT_TRUE(strcmp(riskLevel(60u), "high") == 0); + TEST_ASSERT_TRUE(strcmp(riskLevel(80u), "critical") == 0); +} + +static void testHighUniqueDestinationsIncreaseRisk(void) { + risk_features_t features = {0}; + features.unique_destinations = 10u; + TEST_ASSERT_EQUAL_UINT64(15u, riskScore(&features)); +} + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(testRiskScoreForNoSignalsIsZero); + RUN_TEST(testRiskScoreForSensitiveAndBlockedIsHigh); + RUN_TEST(testRiskScoreIsCappedAtOneHundred); + RUN_TEST(testRiskLevels); + RUN_TEST(testHighUniqueDestinationsIncreaseRisk); + return UNITY_END(); +} diff --git a/third_party/unity/README.md b/third_party/unity/README.md new file mode 100644 index 0000000..7ed160c --- /dev/null +++ b/third_party/unity/README.md @@ -0,0 +1,17 @@ +### Unity mínimo vendorizado + +#### Propósito + +Esta carpeta contiene una versión mínima de Unity para pruebas unitarias de AgentGuard FastPath. + +No requiere instalación del sistema. + +#### Alcance + +Esta versión cubre las macros necesarias para Fase 9.1. + +No busca reemplazar el proyecto Unity oficial completo. + +#### Motivo + +El objetivo es evitar dependencias externas antes del release `v1.0.0`. diff --git a/third_party/unity/unity.c b/third_party/unity/unity.c new file mode 100644 index 0000000..f883f38 --- /dev/null +++ b/third_party/unity/unity.c @@ -0,0 +1,35 @@ +#include "unity.h" + +int unity_tests_run = 0; +int unity_tests_failed = 0; +const char *unity_current_test = ""; + +void UnityBegin(const char *name) { + unity_tests_run = 0; + unity_tests_failed = 0; + unity_current_test = ""; + printf("### Unity: %s\n", name); +} + +int UnityEnd(void) { + printf("Pruebas ejecutadas: %d\n", unity_tests_run); + printf("Pruebas fallidas: %d\n", unity_tests_failed); + if (unity_tests_failed == 0) { + printf("Resultado: pruebas unitarias superadas\n"); + return 0; + } + printf("Resultado: pruebas unitarias fallidas\n"); + return 1; +} + +void UnityDefaultTestRun(unity_test_func_t func, const char *name) { + unity_current_test = name; + unity_tests_run += 1; + printf("[unidad] %s\n", name); + func(); +} + +void UnityFail(const char *message, const char *file, int line) { + unity_tests_failed += 1; + fprintf(stderr, "Fallo en %s:%d durante %s: %s\n", file, line, unity_current_test, message); +} diff --git a/third_party/unity/unity.h b/third_party/unity/unity.h new file mode 100644 index 0000000..ac5bd6c --- /dev/null +++ b/third_party/unity/unity.h @@ -0,0 +1,32 @@ +#ifndef AGFAST_UNITY_H +#define AGFAST_UNITY_H + +#include +#include +#include +#include +#include + +typedef void (*unity_test_func_t)(void); +extern int unity_tests_run; +extern int unity_tests_failed; +extern const char *unity_current_test; + +void UnityBegin(const char *name); +int UnityEnd(void); +void UnityDefaultTestRun(unity_test_func_t func, const char *name); +void UnityFail(const char *message, const char *file, int line); + +#define UNITY_BEGIN() UnityBegin(__FILE__) +#define UNITY_END() UnityEnd() +#define RUN_TEST(func) UnityDefaultTestRun(func, #func) + +#define TEST_ASSERT_TRUE(value) do { if (!(value)) { UnityFail("se esperaba verdadero", __FILE__, __LINE__); return; } } while (0) +#define TEST_ASSERT_FALSE(value) do { if ((value)) { UnityFail("se esperaba falso", __FILE__, __LINE__); return; } } while (0) +#define TEST_ASSERT_NOT_NULL(value) do { if ((value) == NULL) { UnityFail("se esperaba puntero no nulo", __FILE__, __LINE__); return; } } while (0) +#define TEST_ASSERT_EQUAL_INT(expected, actual) do { if ((int)(expected) != (int)(actual)) { char msg[160]; snprintf(msg, sizeof(msg), "enteros distintos: esperado=%d actual=%d", (int)(expected), (int)(actual)); UnityFail(msg, __FILE__, __LINE__); return; } } while (0) +#define TEST_ASSERT_EQUAL_UINT64(expected, actual) do { if ((uint64_t)(expected) != (uint64_t)(actual)) { char msg[192]; snprintf(msg, sizeof(msg), "uint64 distintos: esperado=%llu actual=%llu", (unsigned long long)(uint64_t)(expected), (unsigned long long)(uint64_t)(actual)); UnityFail(msg, __FILE__, __LINE__); return; } } while (0) +#define TEST_ASSERT_GREATER_OR_EQUAL_UINT64(minimum, actual) do { if ((uint64_t)(actual) < (uint64_t)(minimum)) { char msg[192]; snprintf(msg, sizeof(msg), "valor menor al minimo: minimo=%llu actual=%llu", (unsigned long long)(uint64_t)(minimum), (unsigned long long)(uint64_t)(actual)); UnityFail(msg, __FILE__, __LINE__); return; } } while (0) +#define TEST_ASSERT_DOUBLE_WITHIN(delta, expected, actual) do { if (fabs((double)(expected) - (double)(actual)) > (double)(delta)) { char msg[192]; snprintf(msg, sizeof(msg), "dobles fuera de rango: esperado=%.6f actual=%.6f delta=%.6f", (double)(expected), (double)(actual), (double)(delta)); UnityFail(msg, __FILE__, __LINE__); return; } } while (0) + +#endif