This commit is contained in:
Clinv committed 2026-10-24 12:01:52 -03:00
1 parent 7f7fcd545d
commit 4d9a339421
114 files changed
+11383 -1837

No files matched your search

+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Busca dependencias circulares entre cabeceras públicas y entre módulos internos (por #include "...")."""
import os, re, sys
root = os.path.join(os.path.dirname(__file__), "..")
def includes(path):
out = []
for line in open(path, encoding="utf-8"):
m = re.match(r'\s*#\s*include\s+"([^"]+)"', line)
if m: out.append(m.group(1))
return out
def find_cycle(graph):
state, stack = {}, []
def dfs(n):
state[n] = 1; stack.append(n)
for m in graph.get(n, []):
if state.get(m) == 1: return stack[stack.index(m):] + [m]
if state.get(m) is None:
c = dfs(m)
if c: return c
stack.pop(); state[n] = 2
return None
for n in list(graph):
if state.get(n) is None:
c = dfs(n)
if c: return c
return None
# 1) cabeceras públicas
pub = os.path.join(root, "include", "tahgibu")
g = {}
for f in sorted(os.listdir(pub)):
g[f] = [i.split("/")[-1] for i in includes(os.path.join(pub, f)) if i.startswith("tahgibu/")]
c = find_cycle(g)
print("Cabeceras públicas:", "CICLO " + " -> ".join(c) if c else "sin ciclos (%d cabeceras)" % len(g))
# 2) módulos internos (carpetas de src/, sin ui/ ni platform/) por dependencias de archivos .c/.h
src = os.path.join(root, "src")
mods = {}
for d, _, files in os.walk(src):
for f in files:
if not f.endswith((".c", ".h")): continue
p = os.path.join(d, f)
mod = os.path.relpath(d, src).split(os.sep)[0]
for inc in includes(p):
tgt = os.path.normpath(os.path.join(d, inc))
if tgt.startswith(src) and os.path.exists(tgt):
tmod = os.path.relpath(os.path.dirname(tgt), src).split(os.sep)[0]
if tmod != mod: mods.setdefault(mod, set()).add(tmod)
for m, deps in sorted(mods.items()): print(" %-12s -> %s" % (m, ", ".join(sorted(deps))))
c = find_cycle({k: sorted(v) for k, v in mods.items()})
print("Módulos internos:", "CICLO " + " -> ".join(c) if c else "sin ciclos")
sys.exit(0)
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
# Comprueba SOLO la sintaxis y los tipos de src/ui y src/main.c contra una cabecera mínima de raylib
# (tools/dev/raylib_stub). NO compila ni enlaza la aplicación: para eso hace falta raylib real (CMake).
set -eu
ROOT=$(cd "$(dirname "$0")/.." && pwd)
for f in "$ROOT"/src/ui/*.c "$ROOT"/src/main.c; do
echo "syntax-check $(basename "$f")"
gcc -std=c11 -fsyntax-only -Wall -Wextra -Wshadow -Wpedantic -Werror -D_GNU_SOURCE -I"$ROOT/include" -I"$ROOT/tools/dev/raylib_stub" -I"$ROOT/src" "$f"
done
echo "OK: la interfaz es sintacticamente correcta frente a la cabecera de prueba (NO verificada contra raylib real)"
+25
View File
@@ -0,0 +1,25 @@
#!/bin/sh
# Mide qué líneas del núcleo ejecutan las pruebas (gcc + gcov). Resultado en /tmp/tgb-cov/resumen.txt
set -eu
ROOT=$(cd "$(dirname "$0")/.." && pwd)
OUT=${OUT:-/tmp/tgb-cov}
rm -rf "$OUT"; mkdir -p "$OUT/obj" "$OUT/tests"
FLAGS="-std=c11 -g -O0 --coverage -D_GNU_SOURCE -I$ROOT/include"
for f in $(ls "$ROOT"/src/core/*.c "$ROOT"/src/document/*.c "$ROOT"/src/history/*.c "$ROOT"/src/editor/*.c "$ROOT"/src/editor/tools/*.c \
"$ROOT"/src/animation/*.c "$ROOT"/src/rendering/*.c "$ROOT"/src/io/*.c "$ROOT"/src/recovery/*.c "$ROOT"/src/extensions/*.c "$ROOT"/src/platform/linux/*.c); do
gcc $FLAGS -c "$f" -o "$OUT/obj/$(echo "${f#$ROOT/src/}" | tr '/' '_').o"
done
for f in "$ROOT"/tests/plugins/*.c; do gcc -std=c11 -fPIC -shared -I"$ROOT/include" "$f" -o "$OUT/tests/$(basename "$f" .c).so"; done
for t in "$ROOT"/tests/test_*.c; do
n=$(basename "$t" .c)
gcc $FLAGS -DTGB_TEST_PLUGIN_DIR="\"$OUT/tests\"" "$t" "$OUT"/obj/*.o -o "$OUT/tests/$n" -lz -lm -ldl --coverage
(cd "$OUT/obj" && "$OUT/tests/$n" > /dev/null 2>&1) || echo "AVISO: $n falló"
done
cd "$OUT/obj"
: > "$OUT/resumen.txt"
for o in *.o; do
src=$(echo "$o" | sed 's/\.o$//')
gcov -n -o . "$o" 2>/dev/null | awk -v file="$src" "/^File '.*\/src\//{f=\$2} /Lines executed/{ if (f != \"\") { split(\$2,a,\":\"); sub(\"%\",\"\",a[2]); print f, a[2], \$4; f=\"\" } }" >> "$OUT/resumen.txt" || true
done
sed -e "s|'||g" -e "s|$ROOT/||" "$OUT/resumen.txt" | sort -k2 -n > "$OUT/resumen2.txt"
cat "$OUT/resumen2.txt"
+45
View File
@@ -0,0 +1,45 @@
#!/bin/sh
# Compilación de DESARROLLO sin CMake: construye la biblioteca y las pruebas
# con gcc directamente. Sirve para verificar el núcleo en máquinas sin CMake.
# La compilación oficial del proyecto es CMake (ver docs/BUILD.md).
# tools/dev-build.sh -> compila y ejecuta todas las pruebas
# SAN=1 tools/dev-build.sh -> con AddressSanitizer + UBSan
# OUT=/ruta tools/dev-build.sh -> carpeta de compilación (por defecto /tmp/tgb-dev)
set -eu
ROOT=$(cd "$(dirname "$0")/.." && pwd)
OUT=${OUT:-/tmp/tgb-dev}
CC=${CC:-gcc}
FLAGS="-std=c11 -g -O1 -Wall -Wextra -Wshadow -Wpedantic -Werror -D_GNU_SOURCE -I$ROOT/include"
[ "${SAN:-0}" = "1" ] && FLAGS="$FLAGS -fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all"
mkdir -p "$OUT/obj" "$OUT/tests"
echo "== Biblioteca (libtahgibu.a) =="
OBJS=""
for f in $(ls "$ROOT"/src/core/*.c "$ROOT"/src/document/*.c "$ROOT"/src/history/*.c "$ROOT"/src/editor/*.c \
"$ROOT"/src/editor/tools/*.c "$ROOT"/src/animation/*.c "$ROOT"/src/rendering/*.c "$ROOT"/src/io/*.c \
"$ROOT"/src/recovery/*.c "$ROOT"/src/extensions/*.c "$ROOT"/src/platform/linux/*.c 2>/dev/null); do
o="$OUT/obj/$(echo "${f#$ROOT/src/}" | tr '/' '_').o"
$CC $FLAGS -c "$f" -o "$o"
OBJS="$OBJS $o"
done
rm -f "$OUT/libtahgibu.a"; ar rcs "$OUT/libtahgibu.a" $OBJS
echo "== Extensiones de prueba =="
for f in "$ROOT"/tests/plugins/*.c; do
[ -e "$f" ] || continue
n=$(basename "$f" .c)
$CC $FLAGS -fPIC -shared "$f" -o "$OUT/tests/$n.so"
done
echo "== Pruebas =="
FAIL=0
for t in "$ROOT"/tests/test_*.c; do
n=$(basename "$t" .c)
$CC $FLAGS -DTGB_TEST_PLUGIN_DIR="\"$OUT/tests\"" "$t" "$OUT/libtahgibu.a" -o "$OUT/tests/$n" -lz -lm -ldl -lpthread
echo "--- $n"
if ! "$OUT/tests/$n"; then FAIL=1; fi
done
echo "--- ui_smoke (interfaz con raylib FALSO: no prueba raylib real)"
if $CC $FLAGS -I"$ROOT/tools/dev/raylib_stub" -I"$ROOT/src" -I"$ROOT/tests" \
"$ROOT"/tests/ui_smoke/ui_smoke.c "$ROOT"/tests/ui_smoke/mock_raylib.c "$ROOT"/src/ui/*.c \
"$OUT/libtahgibu.a" -o "$OUT/tests/ui_smoke" -lz -lm -ldl -lpthread; then
"$OUT/tests/ui_smoke" || FAIL=1
else FAIL=1; fi
[ $FAIL = 0 ] && echo "TODAS LAS PRUEBAS PASARON" || { echo "HAY PRUEBAS FALLIDAS"; exit 1; }
+80
View File
@@ -0,0 +1,80 @@
/* NO ES RAYLIB. Cabecera mínima escrita a mano con SOLO las declaraciones de la API de raylib 5.x
* que usa la interfaz de Tahgibu. Sirve únicamente para comprobar la sintaxis y los tipos del
* código de src/ui/ en máquinas sin raylib (gcc -fsyntax-only). No genera nada ejecutable.
* Las firmas están escritas de memoria: la compilación REAL contra raylib sigue pendiente. */
#ifndef RAYLIB_STUB_H
#define RAYLIB_STUB_H
#include <stdbool.h>
typedef struct Vector2 { float x, y; } Vector2;
typedef struct Rectangle { float x, y, width, height; } Rectangle;
typedef struct Color { unsigned char r, g, b, a; } Color;
typedef struct Image { void *data; int width, height, mipmaps, format; } Image;
typedef struct Texture { unsigned int id; int width, height, mipmaps, format; } Texture;
typedef Texture Texture2D;
typedef struct GlyphInfo { int value, offsetX, offsetY, advanceX; Image image; } GlyphInfo;
typedef struct Font { int baseSize, glyphCount, glyphPadding; Texture2D texture; Rectangle *recs; GlyphInfo *glyphs; } Font;
enum { FLAG_VSYNC_HINT = 0x40, FLAG_WINDOW_RESIZABLE = 0x04 };
enum { MOUSE_BUTTON_LEFT = 0, MOUSE_BUTTON_RIGHT = 1, MOUSE_BUTTON_MIDDLE = 2 };
enum { TEXTURE_FILTER_POINT = 0 };
enum { PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 = 7 };
enum {
KEY_NULL = 0, KEY_SPACE = 32, KEY_COMMA = 44, KEY_PERIOD = 46,
KEY_B = 66, KEY_C = 67, KEY_E = 69, KEY_F = 70, KEY_I = 73, KEY_L = 76, KEY_N = 78, KEY_O = 79, KEY_P = 80,
KEY_R = 82, KEY_S = 83, KEY_V = 86, KEY_Y = 89, KEY_Z = 90,
KEY_LEFT_BRACKET = 91, KEY_RIGHT_BRACKET = 93,
KEY_ESCAPE = 256, KEY_ENTER = 257, KEY_BACKSPACE = 259, KEY_DELETE = 261,
KEY_LEFT_SHIFT = 340, KEY_LEFT_CONTROL = 341, KEY_RIGHT_CONTROL = 345, KEY_RIGHT_SHIFT = 344
};
void SetConfigFlags(unsigned int flags);
void InitWindow(int width, int height, const char *title);
void CloseWindow(void);
bool WindowShouldClose(void);
void SetWindowMinSize(int width, int height);
void SetTargetFPS(int fps);
void SetExitKey(int key);
int GetScreenWidth(void);
int GetScreenHeight(void);
float GetFrameTime(void);
double GetTime(void);
void BeginDrawing(void);
void EndDrawing(void);
void ClearBackground(Color color);
void BeginScissorMode(int x, int y, int width, int height);
void EndScissorMode(void);
void DrawRectangle(int posX, int posY, int width, int height, Color color);
void DrawRectangleRec(Rectangle rec, Color color);
void DrawRectangleLines(int posX, int posY, int width, int height, Color color);
void DrawRectangleLinesEx(Rectangle rec, float lineThick, Color color);
void DrawCircleLines(int centerX, int centerY, float radius, Color color);
void DrawText(const char *text, int posX, int posY, int fontSize, Color color);
int MeasureText(const char *text, int fontSize);
Font GetFontDefault(void);
Font LoadFontEx(const char *fileName, int fontSize, int *codepoints, int codepointCount);
void UnloadFont(Font font);
void DrawTextEx(Font font, const char *text, Vector2 position, float fontSize, float spacing, Color tint);
Vector2 MeasureTextEx(Font font, const char *text, float fontSize, float spacing);
Texture2D LoadTextureFromImage(Image image);
void UpdateTexture(Texture2D texture, const void *pixels);
void UnloadTexture(Texture2D texture);
void SetTextureFilter(Texture2D texture, int filter);
void DrawTexturePro(Texture2D texture, Rectangle source, Rectangle dest, Vector2 origin, float rotation, Color tint);
Vector2 GetMousePosition(void);
bool IsMouseButtonPressed(int button);
bool IsMouseButtonDown(int button);
bool IsMouseButtonReleased(int button);
float GetMouseWheelMove(void);
bool IsKeyPressed(int key);
bool IsKeyPressedRepeat(int key);
bool IsKeyDown(int key);
int GetCharPressed(void);
const char *GetClipboardText(void);
bool CheckCollisionPointRec(Vector2 point, Rectangle rec);
#endif
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""Genera tests/fixtures_png.h con PNG producidos por una implementación
INDEPENDIENTE (Pillow) y los píxeles RGBA esperados, para comprobar que nuestro
decodificador entiende PNG reales y no solo los que escribe Tahgibu.
Uso: python3 tools/gen_png_fixtures.py > tests/fixtures_png.h"""
import io, struct, zlib
from PIL import Image
W, H = 4, 3
def png_bytes(img, **kw):
b = io.BytesIO(); img.save(b, "PNG", **kw); return b.getvalue()
def expected(img):
rgba = img.convert("RGBA")
out = []
for y in range(H):
for x in range(W):
r, g, b, a = rgba.getpixel((x, y))
if a == 0: r = g = b = 0 # forma canónica de Tahgibu
out += [r, g, b, a]
return out
def pat(f):
return [f(x, y) for y in range(H) for x in range(W)]
fixtures = []
# RGBA 8
im = Image.new("RGBA", (W, H)); im.putdata(pat(lambda x, y: (x * 60, y * 100, 200 - x * 10, 255 if (x + y) % 3 else (x + y) * 20)))
fixtures.append(("rgba8", png_bytes(im), expected(im)))
# RGB 8
im = Image.new("RGB", (W, H)); im.putdata(pat(lambda x, y: (x * 50, y * 90, 17 * (x + y))))
fixtures.append(("rgb8", png_bytes(im), expected(im)))
# Gris 8
im = Image.new("L", (W, H)); im.putdata(pat(lambda x, y: x * 40 + y * 10))
fixtures.append(("gray8", png_bytes(im), expected(im)))
# Gris + alfa
im = Image.new("LA", (W, H)); im.putdata(pat(lambda x, y: (x * 30 + 5, 255 - y * 100)))
fixtures.append(("graya8", png_bytes(im), expected(im)))
# Paleta con transparencia (tRNS)
im = Image.new("P", (W, H)); pal = []
for i in range(4): pal += [i * 60, 255 - i * 60, i * 20]
im.putpalette(pal + [0] * (768 - len(pal))); im.putdata(pat(lambda x, y: (x + y) % 4))
fixtures.append(("palette_trns", png_bytes(im, transparency=bytes([255, 128, 0, 255])), None))
pm = Image.open(io.BytesIO(fixtures[-1][1])).convert("RGBA")
fixtures[-1] = (fixtures[-1][0], fixtures[-1][1], [v for y in range(H) for x in range(W) for v in ((0,0,0,0) if pm.getpixel((x,y))[3]==0 else pm.getpixel((x,y)))])
# 1 bit
im = Image.new("1", (W, H)); im.putdata(pat(lambda x, y: 255 if (x + y) % 2 else 0))
fixtures.append(("bilevel", png_bytes(im), expected(im)))
# Paleta de 2 bits (4 colores) con filtros variados
im = Image.new("P", (W, H)); im.putpalette([255,0,0, 0,255,0, 0,0,255, 255,255,0] + [0]*756); im.putdata(pat(lambda x, y: (x * 2 + y) % 4))
fixtures.append(("palette2bit", png_bytes(im, bits=2), None))
pm = Image.open(io.BytesIO(fixtures[-1][1])).convert("RGBA")
fixtures[-1] = (fixtures[-1][0], fixtures[-1][1], [v for y in range(H) for x in range(W) for v in pm.getpixel((x,y))])
# Gris 16 bits
im = Image.new("I;16", (W, H)); im.putdata(pat(lambda x, y: x * 4000 + y * 9000))
data = png_bytes(im)
exp = []
for y in range(H):
for x in range(W):
v = (x * 4000 + y * 9000) >> 8
exp += [v, v, v, 255]
fixtures.append(("gray16", data, exp))
print("/* GENERADO por tools/gen_png_fixtures.py con Pillow: no editar a mano. */")
print("#ifndef TGB_FIXTURES_PNG_H\n#define TGB_FIXTURES_PNG_H\n#include <stddef.h>\n#include <stdint.h>")
print("#define FIX_W %d\n#define FIX_H %d" % (W, H))
for name, data, exp in fixtures:
print("static const uint8_t fix_%s[] = {%s};" % (name, ",".join(str(b) for b in data)))
print("static const uint8_t fix_%s_px[] = {%s};" % (name, ",".join(str(v) for v in exp)))
print("typedef struct { const char *name; const uint8_t *png; size_t len; const uint8_t *px; } png_fixture;")
print("static const png_fixture png_fixtures[] = {")
for name, data, exp in fixtures:
print(' {"%s", fix_%s, sizeof fix_%s, fix_%s_px},' % (name, name, name, name))
print("};\n#endif")
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""Verifica con herramientas INDEPENDIENTES (Pillow, ffprobe) los archivos que escribe Tahgibu.
Uso: mkdir -p /tmp/tgb-art && TGB_TEST_ARTIFACTS=/tmp/tgb-art <ruta>/test_io_files && python3 tools/verify-external.py /tmp/tgb-art"""
import base64, io, json, subprocess, sys, shutil
from PIL import Image, ImageSequence
d = sys.argv[1]
ok = True
def check(cond, msg):
global ok
print((" OK " if cond else " FALLO ") + msg)
ok &= bool(cond)
png0 = Image.open(f"{d}/muestra_f0.png"); png0.load()
check(png0.format == "PNG" and png0.mode == "RGBA" and png0.size == (48, 32), f"PNG exportado lo abre Pillow: {png0.format} {png0.mode} {png0.size}")
v2 = json.load(open(f"{d}/muestra_v2.tgb"))
check(v2["format"] == "TAHGIBU" and v2["version"] == 2 and v2["fps"] == 12 and (v2["width"], v2["height"]) == (48, 32), "cabecera formato 2 coincide con el ejemplo histórico")
v2_frames = [Image.open(io.BytesIO(base64.b64decode(f))).convert("RGBA") for f in v2["frames"]]
check(len(v2_frames) == 3 and all(f.size == (48, 32) for f in v2_frames), "formato 2: 3 fotogramas PNG válidos en Base64 (decodificados con base64+Pillow)")
check(list(png0.getdata()) == list(v2_frames[0].getdata()), "el PNG exportado == fotograma 0 del .tgb v2")
v3 = json.load(open(f"{d}/muestra_v3.tgb"))
check(v3["version"] == 3 and "tgb3" in v3 and len(v3["frames"]) == 3, "formato 3: 'frames' aplanados + bloque 'tgb3'")
v3_flat = [Image.open(io.BytesIO(base64.b64decode(f))).convert("RGBA") for f in v3["frames"]]
check(all(list(a.getdata()) == list(b.getdata()) for a, b in zip(v3_flat, v2_frames)), "formato 3: los fotogramas aplanados == formato 2")
imgs = [Image.open(io.BytesIO(base64.b64decode(s))) for s in v3["tgb3"]["images"]]
check(all(i.size == (48, 32) and i.mode == "RGBA" for i in imgs), f"formato 3: {len(imgs)} imágenes de celda válidas")
check([f["duration_ms"] for f in v3["tgb3"]["frames"]] == [100, 250, 40], "formato 3: duraciones por fotograma")
ap = Image.open(f"{d}/muestra.apng")
check(getattr(ap, "is_animated", False) and ap.n_frames == 3, f"APNG: Pillow lo reconoce animado con {getattr(ap, 'n_frames', '?')} fotogramas")
frames, durs = [], []
for fr in ImageSequence.Iterator(ap):
frames.append(fr.convert("RGBA").copy()); durs.append(fr.info.get("duration"))
check(durs == [100, 250, 40], f"APNG: duraciones {durs}")
check(all(list(a.getdata()) == list(b.getdata()) for a, b in zip(frames, v2_frames)), "APNG: cada fotograma == fotograma del proyecto (píxel a píxel)")
if shutil.which("ffprobe"):
out = subprocess.run(["ffprobe", "-v", "error", "-count_frames", "-select_streams", "v:0", "-show_entries", "stream=nb_read_frames,width,height,codec_name",
"-of", "json", f"{d}/muestra.apng"], capture_output=True, text=True)
info = json.loads(out.stdout or "{}").get("streams", [{}])[0]
check(out.returncode == 0 and info.get("codec_name") == "apng" and int(info.get("nb_read_frames", 0)) == 3,
f"ffprobe: codec={info.get('codec_name')} fotogramas={info.get('nb_read_frames')} {info.get('width')}x{info.get('height')}")
else:
print(" (ffprobe no disponible: omitido)")
print("RESULTADO:", "TODO CORRECTO" if ok else "HAY FALLOS")
sys.exit(0 if ok else 1)