"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution.

Cada prueba compila un main propio con su expr.c y corre con ASan y UBSan: una fuga, un uso de
memoria liberada o un aviso también hacen fallar la prueba. Las pruebas de la sesión 2 miran los
árboles sin usar expr_print ni expr_eval, para que un error en esas no se confunda con otro.
"""
import os
import pathlib
import re
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
SOURCE = HERE / LAB_DIR
LAB = HERE / "lab"
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]
WRAP = ["-Wl,--wrap=malloc,--wrap=calloc,--wrap=realloc"]

PRELUDE = r"""
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "expr.h"

#define N expr_number
#define V expr_variable
#define ADD(a, b) expr_binary(OP_ADD, (a), (b))
#define SUB(a, b) expr_binary(OP_SUB, (a), (b))
#define MUL(a, b) expr_binary(OP_MUL, (a), (b))
#define DIV(a, b) expr_binary(OP_DIV, (a), (b))

// Falla la prueba de inmediato, con un mensaje claro.
static void fail(const char *message) {
    printf("%s\n", message);
    fflush(stdout);
    _Exit(3);
}

static Expr *need(Expr *e) {
    if (e == NULL) {
        fail("un constructor devolvió NULL");
    }
    return e;
}

// Escribe el árbol en prefijo, sin usar expr_print: (+ 1 x)
static void dump(const Expr *e) {
    if (e == NULL) {
        printf("NULL");
        return;
    }
    switch (e->kind) {
    case EXPR_NUMBER:
        printf("%g", e->as.number);
        break;
    case EXPR_VARIABLE:
        printf("%s", e->as.name);
        break;
    case EXPR_BINARY:
        printf("(%c ", "+-*/"[e->as.binary.op]);
        dump(e->as.binary.left);
        printf(" ");
        dump(e->as.binary.right);
        printf(")");
        break;
    }
}

// Escribe lo que expr_print deja en un archivo, y libera el árbol.
static void show_print(Expr *e) {
    need(e);
    FILE *file = tmpfile();
    if (file == NULL) {
        fail("no se pudo crear un archivo temporal");
    }
    expr_print(e, file);
    rewind(file);
    int c;
    printf("[");
    while ((c = fgetc(file)) != EOF) {
        putchar(c);
    }
    printf("]\n");
    fclose(file);
    expr_free(e);
}

// Simplifica, escribe en prefijo y libera.
static void show_simplified(Expr *e) {
    need(e);
    Expr *result = expr_simplify(e);
    dump(result);
    printf("\n");
    if (result != NULL) {
        expr_free(result);
    }
}

// Evalúa sin usar expr_eval. Las únicas variables son x e y.
static double own_eval(const Expr *e, double x, double y) {
    switch (e->kind) {
    case EXPR_NUMBER:
        return e->as.number;
    case EXPR_VARIABLE:
        if (strcmp(e->as.name, "x") == 0) {
            return x;
        }
        if (strcmp(e->as.name, "y") == 0) {
            return y;
        }
        fail("apareció una variable que no estaba en la expresión");
        return 0.0;
    case EXPR_BINARY: {
        double l = own_eval(e->as.binary.left, x, y);
        double r = own_eval(e->as.binary.right, x, y);
        switch (e->as.binary.op) {
        case OP_ADD: return l + r;
        case OP_SUB: return l - r;
        case OP_MUL: return l * r;
        case OP_DIV: return l / r;
        }
    }
    }
    return 0.0;
}

// Compara la derivada respecto de x con la pendiente numérica, en un punto.
static void check_derivative(Expr *e, double x, double y) {
    need(e);
    Expr *d = expr_derive(e, "x");
    if (d == NULL) {
        fail("expr_derive devolvió NULL");
    }
    double h = 0.00001;
    double approx = (own_eval(e, x + h, y) - own_eval(e, x - h, y)) / (2 * h);
    double exact = own_eval(d, x, y);
    if (fabs(approx - exact) > 0.0001) {
        printf("derivada de ");
        dump(e);
        printf(" en x = %g: su árbol da %g, y debería dar %g. Su árbol es ", x, exact, approx);
        dump(d);
        printf("\n");
        fflush(stdout);
        _Exit(3);
    }
    expr_free(d);
    expr_free(e);
}
"""

# Hace fallar la k-ésima reserva de memoria, para probar los caminos de error.
FAILING_MALLOC = r"""
static int countdown = -1;
static int failed = 0;
void *__real_malloc(size_t size);
void *__real_calloc(size_t count, size_t size);
void *__real_realloc(void *block, size_t size);
static int must_fail(void) {
    if (countdown == 0) {
        countdown = -1;
        failed = 1;
        return 1;
    }
    if (countdown > 0) {
        countdown--;
    }
    return 0;
}
void *__wrap_malloc(size_t size) { return must_fail() ? NULL : __real_malloc(size); }
void *__wrap_calloc(size_t count, size_t size) { return must_fail() ? NULL : __real_calloc(count, size); }
void *__wrap_realloc(void *block, size_t size) { return must_fail() ? NULL : __real_realloc(block, size); }

// Repite la operación haciendo fallar la reserva número 0, 1, 2... hasta que ninguna falle.
#define EACH_FAILURE(result, operation)                                          \
    for (int k = 0; ; k++) {                                                     \
        countdown = k;                                                           \
        failed = 0;                                                              \
        Expr *result = (operation);                                              \
        countdown = -1;                                                          \
        if (failed && result != NULL) {                                          \
            fail("no había memoria, y aun así no devolvió NULL");                \
        }                                                                        \
        if (!failed && result == NULL) {                                         \
            fail("había memoria, y aun así devolvió NULL");                      \
        }                                                                        \
        int done = !failed;                                                      \
        expr_free(result);                                                       \
        if (done) {                                                              \
            printf("reservas: %d\n", k);                                         \
            break;                                                               \
        }                                                                        \
    }
"""


def source(name):
    """El archivo de la carpeta que se prueba. Si no está ahí, es uno de los ya hechos de lab/."""
    path = SOURCE / name
    return path if path.exists() else LAB / name


def explain(out, code):
    if "detected memory leaks" in out:
        head = "Hay una fuga de memoria: algo se reservó y no se liberó."
    elif "heap-use-after-free" in out or "double-free" in out:
        head = "Se usó o se liberó memoria que ya estaba liberada."
    elif "SEGV" in out:
        head = "El programa falló por acceder a una dirección inválida, quizás un NULL."
    else:
        head = f"El programa terminó con error (código {code})."
    return head + "\n" + "\n".join(out.splitlines()[:25])


def expr_sources():
    return [str(source("expr.c")), str(LAB / "env.c")]


def build_and_run(body, wrap=False):
    """Devuelve (salida, código de salida) de un main propio, en una carpeta temporal."""
    with tempfile.TemporaryDirectory() as name:
        folder = pathlib.Path(name)
        binary = folder / "driver"
        driver = folder / "driver.c"
        extra = FAILING_MALLOC if wrap else ""
        driver.write_text(f"{PRELUDE}\n{extra}\nint main(void) {{\n{body}\nreturn 0;\n}}\n", encoding="utf-8")
        built = subprocess.run(
            ["gcc", "-std=c17", "-w", "-g", "-fsanitize=address,undefined", f"-I{LAB}", str(driver),
             *expr_sources(), "-lm", *(WRAP if wrap else []), "-o", str(binary)],
            text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
        )
        if built.returncode != 0:
            raise AssertionError(f"No compila:\n{built.stdout}")
        try:
            done = subprocess.run(
                [str(binary)], cwd=folder, text=True, encoding="utf-8", errors="replace",
                stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
            )
        except subprocess.TimeoutExpired:
            raise AssertionError("El programa no terminó en 60 segundos.") from None
        return done.stdout, done.returncode


@unittest.skipUnless((SOURCE / "expr.c").exists(), f"no está {LAB_DIR}/expr.c")
class LabCase(unittest.TestCase):
    longMessage = False

    def lines(self, body, wanted, what, wrap=False):
        """Corre body y compara su salida, línea por línea, con la lista wanted."""
        out, code = build_and_run(body, wrap)
        if code != 0:
            self.fail(f"{what}: {explain(out, code)}")
        got = out.splitlines()
        for i, line in enumerate(wanted):
            if i >= len(got):
                self.fail(f"{what}: faltan líneas. Se esperaba: {line}")
            if got[i] != line:
                self.fail(f"{what}, caso {i + 1}\n  se esperaba: {line}\n  se obtuvo:   {got[i]}")
        if len(got) > len(wanted):
            self.fail(f"{what}: sobran líneas. Lo que sobra: {got[len(wanted)]}")

    def ok(self, body, what, wrap=False):
        out, code = build_and_run(body, wrap)
        if code != 0:
            self.fail(f"{what}: {explain(out, code)}")
        return out


# ---------------------------------------------------------------- Sesión 1


class Constructors(LabCase):
    def test_number(self):
        self.ok(r"""
        Expr *e = need(N(2.5));
        if (e->kind != EXPR_NUMBER) fail("la etiqueta no es EXPR_NUMBER");
        if (e->as.number != 2.5) fail("el número no se guardó");
        free(e);
        """, "expr_number")

    def test_variable_keeps_its_own_copy(self):
        self.ok(r"""
        char name[] = "total";
        Expr *e = need(V(name));
        if (e->kind != EXPR_VARIABLE) fail("la etiqueta no es EXPR_VARIABLE");
        if (e->as.name == name) fail("guardó la dirección que recibió, y no una copia");
        name[0] = 'X';
        if (strcmp(e->as.name, "total") != 0) fail("el nombre cambió al cambiar el original");
        expr_free(e);
        """, "expr_variable")

    def test_binary_links_its_children(self):
        self.ok(r"""
        Expr *left = need(N(1));
        Expr *right = need(V("x"));
        Expr *e = need(SUB(left, right));
        if (e->kind != EXPR_BINARY) fail("la etiqueta no es EXPR_BINARY");
        if (e->as.binary.op != OP_SUB) fail("el operador no se guardó");
        if (e->as.binary.left != left) fail("el hijo izquierdo no es el que recibió");
        if (e->as.binary.right != right) fail("el hijo derecho no es el que recibió");
        expr_free(e);
        """, "expr_binary")

    def test_binary_owns_its_children_when_one_is_null(self):
        self.ok(r"""
        Expr *e = need(ADD(N(1), N(2)));
        expr_free(e);
        if (expr_binary(OP_ADD, NULL, need(N(1))) != NULL) fail("con el izquierdo NULL debe devolver NULL");
        if (expr_binary(OP_ADD, need(V("x")), NULL) != NULL) fail("con el derecho NULL debe devolver NULL");
        if (expr_binary(OP_ADD, NULL, NULL) != NULL) fail("con los dos NULL debe devolver NULL");
        """, "expr_binary con un hijo NULL")

    def test_no_memory(self):
        self.ok(r"""
        EACH_FAILURE(a, V("nombre"))
        EACH_FAILURE(b, N(1))
        EACH_FAILURE(c, ADD(V("x"), MUL(N(2), V("y"))))
        """, "sin memoria", wrap=True)


class Free(LabCase):
    def test_frees_the_whole_tree(self):
        self.ok(r"""
        expr_free(NULL);
        Expr *e = need(ADD(MUL(N(2), V("largo")), DIV(SUB(V("x"), N(1)), V("y"))));
        expr_free(e);
        """, "expr_free")


class Print(LabCase):
    def test_leaves(self):
        self.lines(r"""
        show_print(N(2));
        show_print(N(2.5));
        show_print(N(-3));
        show_print(V("total"));
        """, ["[2]", "[2.5]", "[-3]", "[total]"], "expr_print de una hoja")

    def test_writes_where_it_is_told(self):
        out = self.ok(r"""
        Expr *e = need(ADD(N(1), V("x")));
        FILE *file = tmpfile();
        if (file == NULL) fail("no se pudo crear un archivo temporal");
        expr_print(e, file);
        if (ftell(file) == 0) fail("no escribió nada en out");
        fclose(file);
        expr_free(e);
        """, "expr_print")
        self.assertEqual(out, "", "expr_print escribió en la pantalla, y debía escribir en out.")

    def test_precedence(self):
        self.lines(r"""
        show_print(ADD(N(2), MUL(N(3), V("x"))));
        show_print(MUL(ADD(N(2), N(3)), V("x")));
        show_print(MUL(V("x"), ADD(N(2), N(3))));
        show_print(ADD(MUL(V("x"), V("y")), V("z")));
        show_print(DIV(SUB(V("a"), V("b")), ADD(V("c"), V("d"))));
        show_print(SUB(MUL(V("a"), V("b")), DIV(V("c"), V("d"))));
        """, ["[2 + 3 * x]", "[(2 + 3) * x]", "[x * (2 + 3)]", "[x * y + z]", "[(a - b) / (c + d)]",
              "[a * b - c / d]"], "expr_print: precedencia")

    def test_same_precedence(self):
        self.lines(r"""
        show_print(SUB(SUB(N(1), N(2)), N(3)));
        show_print(SUB(N(1), SUB(N(2), N(3))));
        show_print(DIV(DIV(N(8), N(2)), N(2)));
        show_print(DIV(N(8), DIV(N(2), N(2))));
        show_print(DIV(N(8), MUL(N(2), N(2))));
        show_print(MUL(N(8), DIV(N(2), N(2))));
        show_print(SUB(N(1), ADD(N(2), N(3))));
        show_print(ADD(N(1), ADD(N(2), N(3))));
        show_print(ADD(ADD(N(1), N(2)), N(3)));
        """, ["[1 - 2 - 3]", "[1 - (2 - 3)]", "[8 / 2 / 2]", "[8 / (2 / 2)]", "[8 / (2 * 2)]",
              "[8 * (2 / 2)]", "[1 - (2 + 3)]", "[1 + (2 + 3)]", "[1 + 2 + 3]"],
                   "expr_print: la misma precedencia")


EVAL_MAIN = r"""
Env *env = env_new(NULL);
if (env == NULL || !env_set(env, "x", 4.0) || !env_set(env, "y", 0.0)) fail("no se pudo crear el entorno");
double value = -7.0;
"""


def eval_line(tree):
    return (f"{{ e = need({tree}); value = -7.0; "
            f'int ok = expr_eval(e, env, &value); printf("%d %g\\n", ok, value); expr_free(e); }}')


class Eval(LabCase):
    def body(self, *trees):
        return EVAL_MAIN + "Expr *e;\n" + "\n".join(eval_line(tree) for tree in trees) + "\nenv_free(env);"

    def test_leaves(self):
        self.lines(self.body('N(2.5)', 'V("x")'), ["1 2.5", "1 4"], "expr_eval de una hoja")

    def test_operations_in_order(self):
        self.lines(self.body('ADD(N(2), MUL(N(3), V("x")))', 'MUL(ADD(N(2), N(3)), V("x"))',
                             'SUB(N(1), SUB(N(2), N(3)))', 'DIV(DIV(N(8), N(2)), V("x"))'),
                   ["1 14", "1 20", "1 2", "1 1"], "expr_eval: valor")

    def test_unknown_variable(self):
        self.lines(self.body('ADD(N(1), N(2))', 'V("z")', 'ADD(V("z"), N(1))', 'ADD(N(1), V("z"))'),
                   ["1 3", "0 -7", "0 -7", "0 -7"], "expr_eval: variable que no existe, sin tocar *result")

    def test_division_by_zero(self):
        self.lines(self.body('DIV(N(1), N(2))', 'DIV(N(1), N(0))', 'DIV(V("x"), V("y"))',
                             'ADD(N(1), DIV(N(1), SUB(V("x"), V("x"))))', 'MUL(DIV(N(1), V("y")), N(0))'),
                   ["1 0.5", "0 -7", "0 -7", "0 -7", "0 -7"], "expr_eval: división entre 0, sin tocar *result")


# ---------------------------------------------------------------- Sesión 2


class Copy(LabCase):
    def test_copy_is_independent(self):
        self.lines(r"""
        Expr *e = need(ADD(MUL(N(2), V("x")), V("y")));
        Expr *copy = expr_copy(e);
        if (copy == NULL) fail("expr_copy devolvió NULL");
        if (copy == e || copy->as.binary.left == e->as.binary.left) fail("la copia comparte nodos");
        if (copy->as.binary.right->as.name == e->as.binary.right->as.name) fail("la copia comparte el nombre");
        expr_free(e);
        dump(copy);
        printf("\n");
        expr_free(copy);
        """, ["(+ (* 2 x) y)"], "expr_copy")

    def test_no_memory(self):
        self.ok(r"""
        Expr *e = need(ADD(MUL(N(2), V("x")), V("y")));
        EACH_FAILURE(copy, expr_copy(e))
        expr_free(e);
        """, "expr_copy sin memoria", wrap=True)


class Equal(LabCase):
    def test_equal_and_different(self):
        self.lines(r"""
        #define SHOW(a, b) { Expr *p = need(a); Expr *q = need(b); printf("%d\n", expr_equal(p, q)); expr_free(p); expr_free(q); }
        SHOW(N(2), N(2))
        SHOW(V("x"), V("x"))
        SHOW(ADD(N(1), MUL(V("x"), N(2))), ADD(N(1), MUL(V("x"), N(2))))
        SHOW(N(2), N(3))
        SHOW(V("x"), V("y"))
        SHOW(N(2), V("x"))
        SHOW(ADD(N(1), N(2)), SUB(N(1), N(2)))
        SHOW(ADD(N(1), N(2)), ADD(N(1), N(3)))
        SHOW(ADD(N(1), N(2)), ADD(N(3), N(2)))
        SHOW(ADD(N(1), N(2)), ADD(N(2), N(1)))
        SHOW(ADD(ADD(N(1), N(2)), N(3)), ADD(N(1), ADD(N(2), N(3))))
        SHOW(ADD(N(1), N(2)), N(3))
        """, ["1", "1", "1"] + ["0"] * 9, "expr_equal")


class Simplify(LabCase):
    def test_neutral_elements(self):
        self.lines(r"""
        show_simplified(ADD(V("x"), N(0)));
        show_simplified(ADD(N(0), V("x")));
        show_simplified(SUB(V("x"), N(0)));
        show_simplified(MUL(V("x"), N(1)));
        show_simplified(MUL(N(1), V("x")));
        show_simplified(DIV(V("x"), N(1)));
        """, ["x"] * 6, "expr_simplify: x + 0, x - 0, x * 1, x / 1")

    def test_times_zero_frees_the_subtree(self):
        self.lines(r"""
        show_simplified(MUL(V("x"), N(0)));
        show_simplified(MUL(N(0), V("x")));
        show_simplified(MUL(ADD(V("x"), MUL(V("y"), SUB(V("z"), N(2)))), N(0)));
        show_simplified(MUL(N(-3), N(0)));
        show_simplified(MUL(DIV(N(1), N(0)), N(0)));
        """, ["0", "0", "0", "0", "0"], "expr_simplify: x * 0")

    def test_constants(self):
        self.lines(r"""
        show_simplified(ADD(N(2), N(3)));
        show_simplified(SUB(N(2), N(3)));
        show_simplified(MUL(N(2), N(3)));
        show_simplified(DIV(N(6), N(4)));
        show_simplified(MUL(ADD(N(1), N(2)), ADD(N(3), N(4))));
        show_simplified(ADD(V("x"), MUL(N(2), N(3))));
        """, ["5", "-1", "6", "1.5", "21", "(+ x 6)"], "expr_simplify: constantes")

    def test_children_first(self):
        self.lines(r"""
        show_simplified(ADD(MUL(V("x"), N(0)), N(3)));
        show_simplified(ADD(MUL(V("x"), N(0)), MUL(V("y"), N(1))));
        show_simplified(MUL(ADD(V("x"), SUB(N(1), N(1))), SUB(N(3), N(2))));
        show_simplified(DIV(ADD(N(0), V("x")), ADD(N(1), N(0))));
        """, ["3", "y", "x", "x"], "expr_simplify: primero los hijos")

    def test_leaves_alone_what_it_should(self):
        self.lines(r"""
        show_simplified(ADD(V("x"), N(0)));
        show_simplified(DIV(N(1), N(0)));
        show_simplified(SUB(N(0), V("x")));
        show_simplified(DIV(N(0), V("x")));
        show_simplified(DIV(N(1), V("x")));
        show_simplified(ADD(V("x"), V("y")));
        show_simplified(N(4));
        show_simplified(V("x"));
        if (expr_simplify(NULL) != NULL) fail("con NULL debe devolver NULL");
        """, ["x", "(/ 1 0)", "(- 0 x)", "(/ 0 x)", "(/ 1 x)", "(+ x y)", "4", "x"],
                   "expr_simplify: lo que no se debe tocar")

    def test_keeps_the_value(self):
        self.lines(r"""
        Expr *e = need(ADD(MUL(ADD(V("x"), N(0)), SUB(V("y"), MUL(N(2), N(3)))), DIV(V("x"), N(1))));
        double before = own_eval(e, 2.0, 9.0);
        e = expr_simplify(e);
        if (e == NULL) fail("expr_simplify devolvió NULL");
        printf("%d\n", before == own_eval(e, 2.0, 9.0));
        dump(e);
        printf("\n");
        expr_free(e);
        """, ["1", "(+ (* x (- y 6)) x)"], "expr_simplify: el valor no cambia")


class Derive(LabCase):
    def test_leaves(self):
        self.lines(r"""
        Expr *e = need(N(5));
        Expr *d = expr_derive(e, "x");
        dump(d); printf("\n"); if (d) expr_free(d);
        expr_free(e);
        e = need(V("x"));
        d = expr_derive(e, "x");
        dump(d); printf("\n"); if (d) expr_free(d);
        d = expr_derive(e, "y");
        dump(d); printf("\n"); if (d) expr_free(d);
        expr_free(e);
        """, ["0", "1", "0"], "expr_derive de una hoja")

    def test_sum_and_difference(self):
        self.ok(r"""
        check_derivative(ADD(V("x"), N(3)), 2.0, 1.0);
        check_derivative(SUB(N(3), V("x")), 2.0, 1.0);
        check_derivative(ADD(V("x"), SUB(V("x"), V("y"))), 2.0, 1.0);
        """, "expr_derive de + y -")

    def test_product(self):
        self.ok(r"""
        check_derivative(MUL(V("x"), V("x")), 3.0, 1.0);
        check_derivative(MUL(N(3), V("x")), 2.0, 1.0);
        check_derivative(MUL(V("x"), V("y")), 2.0, 5.0);
        check_derivative(MUL(ADD(V("x"), N(1)), SUB(V("x"), N(2))), 1.5, 1.0);
        check_derivative(MUL(MUL(V("x"), V("x")), V("x")), -2.0, 1.0);
        """, "expr_derive de *")

    def test_quotient(self):
        self.ok(r"""
        check_derivative(DIV(V("x"), N(2)), 3.0, 1.0);
        check_derivative(DIV(N(1), V("x")), 2.0, 1.0);
        check_derivative(DIV(V("x"), ADD(V("x"), N(1))), 2.0, 1.0);
        check_derivative(DIV(MUL(V("x"), V("x")), SUB(V("x"), V("y"))), 4.0, 1.0);
        """, "expr_derive de /")

    def test_shares_nothing_and_changes_nothing(self):
        self.lines(r"""
        Expr *e = need(DIV(MUL(V("x"), ADD(V("x"), N(1))), SUB(V("x"), N(2))));
        Expr *d = expr_derive(e, "x");
        if (d == NULL) fail("expr_derive devolvió NULL");
        dump(e);
        printf("\n");
        expr_free(e);
        double value = own_eval(d, 3.0, 0.0);
        printf("%d\n", isfinite(value));
        expr_free(d);
        """, ["(/ (* x (+ x 1)) (- x 2))", "1"], "expr_derive: no cambia ni comparte")

    def test_no_memory(self):
        self.ok(r"""
        Expr *e = need(DIV(MUL(V("x"), ADD(V("y"), N(1))), SUB(V("x"), N(2))));
        EACH_FAILURE(d, expr_derive(e, "x"))
        expr_free(e);
        """, "expr_derive sin memoria", wrap=True)


# ---------------------------------------------------------------- Lo demás


class Whole(LabCase):
    def test_compiles_without_warnings(self):
        with tempfile.TemporaryDirectory() as name:
            done = subprocess.run(
                ["gcc", *FLAGS, f"-I{LAB}", "-c", str(source("expr.c")), "-o", str(pathlib.Path(name) / "expr.o")],
                text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
            )
        self.assertEqual(done.returncode, 0, done.stdout)
        self.assertEqual(done.stdout, "", f"expr.c da advertencias:\n{done.stdout}")

    def test_no_globals_no_goto(self):
        path = source("expr.c")
        text = path.read_text(encoding="utf-8")
        self.assertIsNone(re.search(r"\bgoto\b", text), "expr.c usa goto.")
        with tempfile.TemporaryDirectory() as name:
            obj = pathlib.Path(name) / "expr.o"
            subprocess.run(["gcc", "-std=c17", f"-I{LAB}", "-c", str(path), "-o", str(obj)], check=True)
            done = subprocess.run(["nm", str(obj)], text=True, stdout=subprocess.PIPE, check=True)
        found = [line.split()[-1] for line in done.stdout.splitlines() if re.search(r" [bBdD] ", line)]
        self.assertEqual(found, [], f"expr.c tiene variables globales: {found}")

    def program(self, main_file):
        with tempfile.TemporaryDirectory() as name:
            binary = pathlib.Path(name) / "program"
            built = subprocess.run(
                ["gcc", *FLAGS, f"-I{LAB}", str(source(main_file)), *expr_sources(), "-lm", "-o", str(binary)],
                text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
            )
            self.assertEqual(built.returncode, 0, built.stdout)
            done = subprocess.run([str(binary)], text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                                  encoding="utf-8", errors="replace", timeout=60)
        return done.stdout, done.returncode

    def test_expr_tests_pass(self):
        out, code = self.program("expr_tests.c")
        self.assertEqual(code, 0, f"expr_tests falla: {explain(out, code)}")

    def test_algebra_agrees_with_the_numbers(self):
        out, code = self.program("algebra.c")
        self.assertEqual(code, 0, f"algebra falla: {explain(out, code)}")
        self.assertNotIn("MAL", out)
        self.assertEqual(out.count(" bien"), 11, out)


if __name__ == "__main__":
    unittest.main()
