Semana 7 — Funciones y pruebas
Objetivos
- Definir y llamar funciones con parámetros y valor de retorno.
- Explicar el paso por valor y el alcance de las variables.
- Probar funciones con
asserty usar el debugger para encontrar un bug.
Teoría — Funciones · Pruebas y depuración
Lectura: Apuntes, semana 7.
Temas:
- Para qué sirven las funciones. Definir, llamar, parámetros y
return. - Funciones
voidy funciones sin parámetros. - Paso por valor. Alcance de una variable y la pila de llamadas, con un dibujo.
- Separar los cálculos de la entrada y salida: eso hace posible probarlos.
- Probar con
assert: casos normales y casos borde. - Depurar con método: reproducir, formular una hipótesis, comprobarla y corregir.
gdben lo básico.
Laboratorio — Funciones con pruebas y el debugger
Esta semana la sesión de laboratorio es para la Práctica 2. Las actividades de abajo son trabajo autónomo y no se entregan. Conviene hacerlas antes de la práctica.
- Escribir
is_primeeis_leap_yearcon pruebasassert. - Compilar el programa con bugs de
code/week-07/con-g. Encontrar los bugs congdb:break,run,next,stepyprint. - Mirar la pila con
backtracedurante una llamada. - Reorganizar un programa de una semana anterior en funciones.
Entregas
- Práctica 2: Ciclos, números aleatorios y funciones. Ver
practicas/README.md.
Preguntas de repaso
- ¿Qué escribe este programa y por qué?
void inc(int x) { x = x + 1; }llamada conint a = 1; inc(a); printf("%d", a);. - ¿Cuál es la diferencia entre parámetro y argumento?
- ¿Qué casos de prueba elegirían para
is_prime? ¿Por qué esos? - ¿Por qué una función que calcula y además imprime es más difícil de probar?
- Dibujen la pila de llamadas cuando
mainllama afyfllama ag.
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (17)Errores (12)Pruebas (4)Laboratorio (4)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
#include <stdio.h>
// Cambia su copia de x. El x de main no se entera.
void add_one(int x) {
x = x + 1;
printf("En add_one, x vale %d\n", x);
}
// Devuelve el nuevo valor: quien llama decide qué hacer con él.
int plus_one(int x) {
return x + 1;
}
int main(void) {
int x = 1;
add_one(x);
printf("En main, x vale %d\n", x);
x = plus_one(x);
printf("Después de plus_one, x vale %d\n", x);
return 0;
}#include <stdio.h>
int square(int x) {
int result = x * x;
return result;
}
int sum_of_squares(int a, int b) {
int first = square(a);
int second = square(b);
return first + second;
}
int main(void) {
int total = sum_of_squares(3, 4);
printf("%d\n", total);
return 0;
}#include <stdio.h>
// Intenta contar sus llamadas. Cada llamada crea un calls nuevo.
int count_calls(void) {
int calls = 0;
calls = calls + 1;
return calls;
}
int main(void) {
printf("%d\n", count_calls());
printf("%d\n", count_calls());
printf("%d\n", count_calls());
return 0;
}#include <assert.h>
#include <stdio.h>
// Devuelve cuántas cifras tiene n. El 0 tiene una cifra; el signo no cuenta.
int count_digits(int n) {
if (n == 0) {
return 1;
}
int count = 0;
while (n != 0) {
count = count + 1;
n = n / 10;
}
return count;
}
int main(void) {
assert(count_digits(7) == 1);
assert(count_digits(2026) == 4);
assert(count_digits(0) == 1);
assert(count_digits(-42) == 2);
assert(count_digits(10) == 2);
assert(count_digits(9) == 1);
printf("Todas las pruebas pasaron\n");
return 0;
}#include <stdio.h>
// Devuelve la suma de las cifras de n, que no es negativo.
int digit_sum(int n) {
int sum = 0;
while (n > 0) {
int last = n % 10;
sum = sum + last;
n = n / 10;
}
return sum;
}
int main(void) {
int n;
if (scanf("%d", &n) != 1 || n < 0) {
printf("Entrada inválida\n");
return 1;
}
int result = digit_sum(n);
printf("La suma de las cifras de %d es %d\n", n, result);
return 0;
}#include <stdio.h>
// Calcula y además escribe: solo se puede ver el resultado leyendo la pantalla.
void discount_and_print(int total) {
if (total >= 10000) {
total = total - total / 10;
}
printf("A pagar: %d.%02d soles\n", total / 100, total % 100);
}
int main(void) {
int total;
if (scanf("%d", &total) != 1 || total < 0) {
printf("Entrada inválida\n");
return 1;
}
discount_and_print(total);
return 0;
}#include <stdio.h>
// Solo calcula: recibe céntimos y devuelve céntimos. Desde 100 soles, 10 % menos.
int price_after_discount(int total) {
if (total >= 10000) {
return total - total / 10;
}
return total;
}
// Solo escribe.
void print_price(int cents) {
printf("A pagar: %d.%02d soles\n", cents / 100, cents % 100);
}
// Lee, llama y escribe.
int main(void) {
int total;
if (scanf("%d", &total) != 1 || total < 0) {
printf("Entrada inválida\n");
return 1;
}
print_price(price_after_discount(total));
return 0;
}407#include <stdio.h>
// Devuelve 1 si n, que es positivo, contiene la cifra digit. Si no, devuelve 0.
int has_digit(int n, int digit) {
while (n > 0) {
if (n % 10 == digit) {
return 1;
}
n = n / 10;
}
return 0;
}
int main(void) {
int n, digit;
if (scanf("%d %d", &n, &digit) != 2 || n <= 0) {
printf("Entrada inválida\n");
return 1;
}
if (has_digit(n, digit)) {
printf("Sí aparece\n");
} else {
printf("No aparece\n");
}
return 0;
}#include <stdio.h>
// Devuelve el mayor de dos enteros.
int max_of_two(int a, int b) {
if (a > b) {
return a;
}
return b;
}
// Devuelve el mayor de tres enteros, apoyándose en la función anterior.
int max_of_three(int a, int b, int c) {
return max_of_two(max_of_two(a, b), c);
}
int main(void) {
int a, b, c;
if (scanf("%d %d %d", &a, &b, &c) != 3) {
printf("Entrada inválida\n");
return 1;
}
printf("%d\n", max_of_three(a, b, c));
return 0;
}#include <assert.h>
#include <stdio.h>
int main(void) {
assert(1 + 1 == 3);
printf("Llegué al final\n");
return 0;
}#include <stdio.h>
// Escribe una línea de guiones del ancho pedido. No devuelve nada.
void print_line(int width) {
for (int i = 0; i < width; i++) {
printf("-");
}
printf("\n");
}
// Escribe el encabezado de una boleta. No recibe nada y no devuelve nada.
void print_header(void) {
print_line(20);
printf("Boleta de notas\n");
print_line(20);
}
int main(void) {
print_header();
printf("Promedio: 14\n");
print_line(5);
return 0;
}#include <stdio.h>
// Prototipo: anuncia la función. Termina en punto y coma y no tiene cuerpo.
int cube(int x);
int main(void) {
printf("%d\n", cube(3));
return 0;
}
// Definición: aquí está el cuerpo.
int cube(int x) {
return x * x * x;
}#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// Devuelve el resultado de lanzar un dado: un entero de 1 a 6.
int roll_die(void) {
return 1 + rand() % 6;
}
int main(void) {
srand((unsigned) time(NULL));
for (int i = 0; i < 5; i++) {
printf("%d ", roll_die());
}
printf("\n");
return 0;
}#include <stdio.h>
int twice(int x) {
int result = x * 2;
return result;
}
int triple(int x) {
int result = x * 3;
return result;
}
int main(void) {
int result = twice(5) + triple(5);
printf("%d\n", result);
return 0;
}#include <stdio.h>
// Devuelve x al cuadrado.
int square(int x) {
int result = x * x;
return result;
}
int main(void) {
int n;
if (scanf("%d", &n) != 1) {
printf("Entrada inválida\n");
return 1;
}
int answer = square(n);
printf("%d al cuadrado es %d\n", n, answer);
printf("%d\n", square(2) + square(n + 1));
return 0;
}#include <stdio.h>
// Intenta intercambiar dos valores. Solo intercambia sus copias.
void try_swap(int a, int b) {
int temp = a;
a = b;
b = temp;
printf("En try_swap: a = %d, b = %d\n", a, b);
}
int main(void) {
int a = 3;
int b = 8;
try_swap(a, b);
printf("En main: a = %d, b = %d\n", a, b);
return 0;
}Errores
Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.
#include <assert.h>
#include <stdio.h>
// Debería devolver cuántas cifras tiene n. Tiene un bug.
int count_digits(int n) {
int count = 0;
while (n > 0) {
count = count + 1;
n = n / 10;
}
return count;
}
int main(void) {
assert(count_digits(7) == 1);
assert(count_digits(2026) == 4);
assert(count_digits(0) == 1);
assert(count_digits(-42) == 2);
printf("Todas las pruebas pasaron\n");
return 0;
}#include <stdio.h>
// Debería devolver la suma de las cifras de n. Tiene un bug.
int digit_sum(int n) {
int sum = 0;
while (n > 9) {
int last = n % 10;
sum = sum + last;
n = n / 10;
}
return sum;
}
int main(void) {
int n;
if (scanf("%d", &n) != 1 || n < 0) {
printf("Entrada inválida\n");
return 1;
}
int result = digit_sum(n);
printf("La suma de las cifras de %d es %d\n", n, result);
return 0;
}#include <stdio.h>
int square(int x) {
return x * x;
}
// El 2.5 se convierte a int, y se pierde la parte decimal. gcc no avisa.
int main(void) {
printf("%d\n", square(2.5));
return 0;
}#include <stdio.h>
// Con paréntesis vacíos, C17 no revisa los argumentos de la llamada.
int get_year() {
return 2026;
}
int main(void) {
printf("%d\n", get_year(1, 2, 3));
return 0;
}#include <stdio.h>
// Error de lógica: decide con la primera cifra, sin mirar las demás.
int has_digit(int n, int digit) {
while (n > 0) {
if (n % 10 == digit) {
return 1;
} else {
return 0;
}
n = n / 10;
}
return 0;
}
int main(void) {
int n, digit;
if (scanf("%d %d", &n, &digit) != 2 || n <= 0) {
printf("Entrada inválida\n");
return 1;
}
if (has_digit(n, digit)) {
printf("Sí aparece\n");
} else {
printf("No aparece\n");
}
return 0;
}#include <stdio.h>
int square(int x) {
return x * x;
}
// El resultado de square no se guarda: se pierde. gcc no avisa.
int main(void) {
int a = 5;
square(a);
printf("%d\n", a);
return 0;
}#include <stdio.h>
int twice(int x) {
int result = x * 2;
return result;
}
// Error: result es local de twice. Aquí no existe.
int main(void) {
twice(5);
printf("%d\n", result);
return 0;
}#include <stdio.h>
// Error: si la nota es menor que 11, la función termina sin return.
int is_passing(int grade) {
if (grade >= 11) {
return 1;
}
}
int main(void) {
int grade;
if (scanf("%d", &grade) != 1) {
printf("Entrada inválida\n");
return 1;
}
printf("%d\n", is_passing(grade));
return 0;
}#include <stdio.h>
// Error: main usa cube antes de que el compilador la conozca.
int main(void) {
printf("%d\n", cube(3));
return 0;
}
int cube(int x) {
return x * x * x;
}#include <stdio.h>
// Con (void), una llamada con argumentos es un error de compilación.
int get_year(void) {
return 2026;
}
int main(void) {
printf("%d\n", get_year(1, 2, 3));
return 0;
}#include <stdio.h>
void print_twice(int x) {
printf("%d\n", x * 2);
}
// Error: una función void no devuelve un valor que se pueda guardar.
int main(void) {
int y = print_twice(4);
printf("%d\n", y);
return 0;
}#include <stdio.h>
int max_of_two(int a, int b) {
if (a > b) {
return a;
}
return b;
}
// Error: max_of_two recibe dos argumentos.
int main(void) {
printf("%d\n", max_of_two(5));
return 0;
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
#include <assert.h>
#include <stdio.h>
// Copia de la función de discount.c, porque cada programa está en un solo archivo.
int price_after_discount(int total) {
if (total >= 10000) {
return total - total / 10;
}
return total;
}
int main(void) {
assert(price_after_discount(12050) == 10845); // caso normal con descuento
assert(price_after_discount(8000) == 8000); // caso normal sin descuento
assert(price_after_discount(9999) == 9999); // último valor sin descuento
assert(price_after_discount(10000) == 9000); // primer valor con descuento
assert(price_after_discount(0) == 0); // el menor total válido
printf("Todas las pruebas pasaron\n");
return 0;
}"""Comprueba lo que los apuntes afirman sobre los programas de la semana 7."""
import pathlib
import re
import signal
import subprocess
import tempfile
import unittest
HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
FLAGS = ["-Wall", "-Wextra", "-std=c17"]
def run(program, stdin=""):
"""Ejecuta un programa de build/; devuelve su salida (con errores) y su estado."""
done = subprocess.run(
[str(BUILD / program)], input=stdin, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
)
return done.stdout, done.returncode
def compile_demo(source, *extra):
"""Compila un programa; devuelve los mensajes, el estado y el ejecutable."""
out = pathlib.Path(tempfile.mkdtemp()) / "demo"
done = subprocess.run(
["gcc", *FLAGS, *extra, source, "-o", str(out)], cwd=HERE,
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
return done.stdout, done.returncode, out
def run_demo(binary, stdin=""):
done = subprocess.run(
[str(binary)], input=stdin, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
)
return done.stdout, done.returncode
def digit_sum(n):
return sum(int(c) for c in str(n))
class CleanCompile(unittest.TestCase):
def test_examples_have_no_warnings(self):
for source in sorted(HERE.glob("*.c")):
with self.subTest(source=source.name):
done = subprocess.run(
["gcc", *FLAGS, "-Werror", "-fsyntax-only", str(source)],
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
self.assertEqual(done.returncode, 0, done.stdout)
def test_gdb_build_has_no_warnings(self):
for source in ["digit_sum_bug.c", "call_chain.c"]:
with self.subTest(source=source):
messages, status, _ = compile_demo(source, "-g", "-O0")
self.assertEqual((messages, status), ("", 0))
class Functions(unittest.TestCase):
def test_square(self):
self.assertEqual(run("square", "5")[0], "5 al cuadrado es 25\n40\n")
self.assertEqual(run("square", "-3")[0], "-3 al cuadrado es 9\n8\n")
self.assertEqual(run("square", "abc"), ("Entrada inválida\n", 1))
def test_square_overflow_is_reported(self):
self.assertIn("signed integer overflow", run("square", "50000")[0])
def test_max_of_three(self):
for a, b, c in [(3, 9, 5), (9, 3, 5), (3, 5, 9), (5, 5, 1), (1, 5, 5), (2, 2, 2), (-1, -2, -3)]:
with self.subTest(values=(a, b, c)):
self.assertEqual(run("max_of_three", f"{a} {b} {c}")[0], f"{max(a, b, c)}\n")
def test_print_line(self):
dashes = "-" * 20
self.assertEqual(run("print_line")[0],
f"{dashes}\nBoleta de notas\n{dashes}\nPromedio: 14\n-----\n")
def test_roll_die_stays_in_range(self):
# No se comparan números exactos: dependen de la semilla y del sistema.
values = run("roll_die")[0].split()
self.assertEqual(len(values), 5)
for value in values:
self.assertIn(int(value), range(1, 7))
def test_prototype(self):
self.assertEqual(run("prototype")[0], "27\n")
def test_has_digit(self):
cases = {"2026 6": True, "2026 2": True, "2026 1": False, "26 2": True, "7 7": True,
"100 1": True, "100 0": True, "5 0": False}
for text, found in cases.items():
with self.subTest(text=text):
self.assertEqual(run("has_digit", text)[0], "Sí aparece\n" if found else "No aparece\n")
def test_has_digit_rejects(self):
for text in ["0 5", "-3 3", "abc", "5"]:
with self.subTest(text=text):
self.assertEqual(run("has_digit", text), ("Entrada inválida\n", 1))
def test_has_digit_wrong_only_looks_at_one_digit(self):
_, _, binary = compile_demo("errors/has_digit_wrong.c")
self.assertEqual(run_demo(binary, "26 6")[0], "Sí aparece\n")
self.assertEqual(run_demo(binary, "26 2")[0], "No aparece\n")
class ByValueAndScope(unittest.TestCase):
def test_by_value(self):
self.assertEqual(run("by_value")[0],
"En add_one, x vale 2\nEn main, x vale 1\nDespués de plus_one, x vale 2\n")
def test_try_swap_does_not_swap(self):
self.assertEqual(run("try_swap")[0],
"En try_swap: a = 8, b = 3\nEn main: a = 3, b = 8\n")
def test_scope(self):
self.assertEqual(run("scope")[0], "25\n")
def test_locals_do_not_remember(self):
self.assertEqual(run("count_calls")[0], "1\n1\n1\n")
def test_call_chain(self):
self.assertEqual(run("call_chain")[0], "25\n")
class Discount(unittest.TestCase):
def test_boundaries(self):
cases = {"0": "0.00", "8000": "80.00", "9999": "99.99", "10000": "90.00", "12050": "108.45"}
for total, price in cases.items():
for program in ["discount", "discount_mixed"]:
with self.subTest(program=program, total=total):
self.assertEqual(run(program, total), (f"A pagar: {price} soles\n", 0))
def test_rejects(self):
for text in ["abc", "", "-1"]:
for program in ["discount", "discount_mixed"]:
with self.subTest(program=program, text=text):
self.assertEqual(run(program, text), ("Entrada inválida\n", 1))
def test_tests_pass(self):
self.assertEqual(run("discount_tests"), ("Todas las pruebas pasaron\n", 0))
def test_tests_use_the_same_function(self):
pattern = r"int price_after_discount\(int total\) \{.*?\n\}"
program = re.search(pattern, (HERE / "discount.c").read_text(encoding="utf-8"), re.S)
tests = re.search(pattern, (HERE / "discount_tests.c").read_text(encoding="utf-8"), re.S)
self.assertEqual(program.group(0), tests.group(0))
class Asserts(unittest.TestCase):
def test_bug_stops_at_first_failed_assert(self):
output, status = run("count_digits_bug")
self.assertIn("count_digits_bug.c:17: main: Assertion `count_digits(0) == 1' failed.", output)
self.assertNotIn("-42", output)
self.assertNotIn("Todas las pruebas", output)
self.assertEqual(status, -signal.SIGABRT)
def test_fixed_version_passes(self):
self.assertEqual(run("count_digits"), ("Todas las pruebas pasaron\n", 0))
def test_assert_is_active_by_default(self):
output, status = run("ndebug_demo")
self.assertIn("Assertion `1 + 1 == 3' failed.", output)
self.assertNotIn("Llegué", output)
self.assertEqual(status, -signal.SIGABRT)
def test_ndebug_turns_assert_off(self):
_, status, binary = compile_demo("ndebug_demo.c", "-DNDEBUG")
self.assertEqual(status, 0)
self.assertEqual(run_demo(binary), ("Llegué al final\n", 0))
class DigitSum(unittest.TestCase):
def test_bug_loses_the_leftmost_digit(self):
for n in [5, 23, 407, 9, 10, 2026]:
with self.subTest(n=n):
wrong = digit_sum(n) - int(str(n)[0])
self.assertEqual(run("digit_sum_bug", str(n))[0],
f"La suma de las cifras de {n} es {wrong}\n")
def test_fix(self):
for n in list(range(0, 120)) + [407, 999, 1000, 2026, 2147483647]:
with self.subTest(n=n):
self.assertEqual(run("digit_sum", str(n))[0],
f"La suma de las cifras de {n} es {digit_sum(n)}\n")
def test_input_file_of_the_notes(self):
self.assertEqual((HERE / "entrada.txt").read_text(encoding="utf-8").strip(), "407")
def test_fix_changes_only_the_condition(self):
wrong = (HERE / "digit_sum_bug.c").read_text(encoding="utf-8").splitlines()
fixed = (HERE / "digit_sum.c").read_text(encoding="utf-8").splitlines()
changed = [(a, b) for a, b in zip(wrong, fixed) if a != b]
self.assertEqual(len(wrong), len(fixed))
self.assertIn((" while (n > 9) {", " while (n > 0) {"), changed)
self.assertEqual(len(changed), 2) # la condición y el comentario
class GdbSessionsMatchTheSource(unittest.TestCase):
"""Las sesiones de gdb no se comparan, pero sus números de línea sí deben existir."""
def line(self, source, number):
return (HERE / source).read_text(encoding="utf-8").splitlines()[number - 1].strip()
def test_digit_sum_lines(self):
expected = {5: "int sum = 0;", 6: "while (n > 9) {", 7: "int last = n % 10;",
8: "sum = sum + last;", 9: "n = n / 10;", 11: "return sum;"}
for number, text in expected.items():
with self.subTest(line=number):
self.assertEqual(self.line("digit_sum_bug.c", number), text)
def test_call_chain_lines(self):
expected = {4: "int result = x * x;", 9: "int first = square(a);",
15: "int total = sum_of_squares(3, 4);"}
for number, text in expected.items():
with self.subTest(line=number):
self.assertEqual(self.line("call_chain.c", number), text)
class CompilerMessages(unittest.TestCase):
def assert_error(self, name, text):
messages, status, _ = compile_demo(f"errors/{name}.c")
self.assertNotEqual(status, 0)
self.assertIn(text, messages)
def assert_silent(self, name):
messages, status, binary = compile_demo(f"errors/{name}.c")
self.assertEqual((messages, status), ("", 0))
return binary
def test_wrong_count(self):
self.assert_error("wrong_count", "too few arguments")
def test_void_value(self):
self.assert_error("void_value", "void value not ignored")
def test_no_prototype(self):
self.assert_error("no_prototype", "implicit declaration of function")
def test_void_parens(self):
self.assert_error("void_parens", "too many arguments")
def test_local_not_visible(self):
self.assert_error("local_not_visible", "undeclared")
def test_empty_parens_is_silent(self):
# No se ejecuta en la prueba: esa llamada es comportamiento indefinido.
self.assert_silent("empty_parens")
def test_double_argument_is_silent(self):
self.assertEqual(run_demo(self.assert_silent("double_argument")), ("4\n", 0))
def test_ignored_result_is_silent(self):
self.assertEqual(run_demo(self.assert_silent("ignored_result")), ("5\n", 0))
def test_has_digit_wrong_is_silent(self):
self.assert_silent("has_digit_wrong")
def test_missing_return_warns_and_still_compiles(self):
# No se ejecuta: usar ese valor es comportamiento indefinido.
messages, status, _ = compile_demo("errors/missing_return.c")
self.assertEqual(status, 0)
self.assertIn("control reaches end of non-void function", messages)
if __name__ == "__main__":
unittest.main()"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution.
Las funciones se llaman directamente: cada prueba compila el archivo del laboratorio
junto con un main propio, sin correr el main de ustedes.
"""
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")
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]
def run(program, stdin=""):
done = subprocess.run(
[str(HERE / "build" / LAB_DIR / program)], input=stdin, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
)
return done.stdout.strip(), done.returncode
def source(name):
return (HERE / LAB_DIR / f"{name}.c").read_text(encoding="utf-8")
def call_with_driver(name, driver_main, stdin):
"""Compila lab/<name>.c con otro main y devuelve lo que escribe."""
folder = pathlib.Path(tempfile.mkdtemp())
driver = folder / "driver.c"
driver.write_text(
f'#define main lab_main\n#include "{HERE / LAB_DIR / name}.c"\n#undef main\n{driver_main}\n',
encoding="utf-8",
)
binary = folder / "driver"
built = subprocess.run(
["gcc", *FLAGS, str(driver), "-o", str(binary)],
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
if built.returncode != 0:
raise AssertionError(f"No compila con las funciones pedidas:\n{built.stdout}")
done = subprocess.run(
[str(binary)], input=stdin, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=20,
)
return done.stdout
def one_argument_main(function):
return (
"int main(void) {\n int n;\n while (scanf(\"%d\", &n) == 1) {\n"
f" printf(\"%d\\n\", {function}(n));\n }}\n return 0;\n}}"
)
def prime(n):
return n > 1 and all(n % i for i in range(2, int(n ** 0.5) + 1))
def leap(y):
return (y % 4 == 0 and y % 100 != 0) or y % 400 == 0
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe {LAB_DIR}/")
class LabTests(unittest.TestCase):
def test_no_warnings(self):
for name in ["functions", "grades_bug", "adder4"]:
with self.subTest(name=name):
done = subprocess.run(
["gcc", "-Wall", "-Wextra", "-std=c17", "-Werror", "-fsyntax-only",
str(HERE / LAB_DIR / f"{name}.c")],
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
self.assertEqual(done.returncode, 0, done.stdout)
# Parte 1
def test_is_prime_edges(self):
values = [-7, -2, -1, 0, 1, 2, 3, 4, 9, 25, 49, 121, 169, 997, 1000, 1001, 7919, 1000003, 2147483647]
output = call_with_driver("functions", one_argument_main("is_prime"), " ".join(map(str, values)))
for value, line in zip(values, output.split()):
with self.subTest(n=value):
self.assertEqual(int(line), int(prime(value)))
self.assertEqual(len(output.split()), len(values), output)
def test_is_prime_first_numbers(self):
values = list(range(-20, 300))
output = call_with_driver("functions", one_argument_main("is_prime"), " ".join(map(str, values)))
self.assertEqual([int(x) for x in output.split()], [int(prime(n)) for n in values])
def test_is_leap_year(self):
values = [1900, 2000, 2023, 2024, 2026, 2100, 2400, 1600, 1996, 1999]
output = call_with_driver("functions", one_argument_main("is_leap_year"), " ".join(map(str, values)))
self.assertEqual([int(x) for x in output.split()], [int(leap(y)) for y in values])
def test_their_tests_exist_and_pass(self):
text = source("functions")
self.assertNotIn("NDEBUG", text)
prime_tests = re.findall(r"^\s*assert\(is_prime\(", text, re.M)
leap_tests = re.findall(r"^\s*assert\(is_leap_year\(", text, re.M)
self.assertGreaterEqual(len(prime_tests), 6, "faltan pruebas de is_prime")
self.assertGreaterEqual(len(leap_tests), 4, "faltan pruebas de is_leap_year")
self.assertEqual(run("functions"), ("Todas las pruebas pasaron", 0))
# Parte 2
def test_grades(self):
cases = {"4\n12 8 11 15\n": "Promedio: 11.50\nAprobados: 3",
"1\n20\n": "Promedio: 20.00\nAprobados: 1",
"2\n11 10\n": "Promedio: 10.50\nAprobados: 1",
"3\n0 0 1\n": "Promedio: 0.33\nAprobados: 0",
"2\n10 11\n": "Promedio: 10.50\nAprobados: 1"}
for text, expected in cases.items():
with self.subTest(text=text):
self.assertEqual(run("grades_bug", text), (expected, 0))
def test_grades_input_file(self):
text = (HERE / "lab" / "grades.txt").read_text(encoding="utf-8")
self.assertEqual(run("grades_bug", text), ("Promedio: 11.50\nAprobados: 3", 0))
def test_grades_bad_input(self):
cases = {"abc": "Cantidad inválida", "0": "Cantidad inválida", "-2": "Cantidad inválida",
"2\n21 3": "Nota inválida", "2\n-1 3": "Nota inválida", "2\n5": "Nota inválida",
"2\nabc": "Nota inválida"}
for text, expected in cases.items():
with self.subTest(text=text):
self.assertEqual(run("grades_bug", text), (expected, 1))
# Parte 4
def test_adder_functions(self):
driver = (
"int main(void) {\n int a, b, c;\n"
" while (scanf(\"%d %d %d\", &a, &b, &c) == 3) {\n"
" printf(\"%d %d\\n\", sum_bit(a, b, c), carry_bit(a, b, c));\n }\n"
" return 0;\n}"
)
triples = [(a, b, c) for a in (0, 1) for b in (0, 1) for c in (0, 1)]
stdin = "\n".join(f"{a} {b} {c}" for a, b, c in triples)
lines = call_with_driver("adder4", driver, stdin).splitlines()
expected = [f"{(a + b + c) % 2} {(a + b + c) // 2}" for a, b, c in triples]
self.assertEqual(lines, expected)
def test_adder_no_longer_repeats(self):
text = source("adder4")
for name in ["partial0", "partial1", "partial2", "partial3"]:
with self.subTest(name=name):
self.assertNotIn(name, text)
def test_adder_same_output(self):
for a in range(16):
for b in range(16):
carries = []
carry = 0
total = 0
for i in range(4):
bit_sum = (a >> i & 1) + (b >> i & 1) + carry
carry = bit_sum // 2
carries.append(carry)
total |= (bit_sum % 2) << i
expected = ("acarreos: " + " ".join(str(c) for c in reversed(carries)) +
f"\nsuma: {total:04b}")
with self.subTest(a=a, b=b):
self.assertEqual(run("adder4", f"{a:04b} {b:04b}"), (expected, 0))
def test_adder_bad_input(self):
self.assertEqual(run("adder4", "abc"), ("Entrada inválida", 1))
self.assertEqual(run("adder4", "0102 0011"), ("Los bits deben ser 0 o 1", 1))
if __name__ == "__main__":
unittest.main()"""Vuelve a correr los comandos que aparecen en los apuntes y en las diapositivas
y comprueba que el código citado existe de verdad en esta carpeta."""
import pathlib
import re
import subprocess
import unittest
HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-07.md"
SLIDES = ROOT / "slides" / "week-07.tex"
# Antes de un bloque cuya salida cambia con el sistema o con la corrida. Solo se
# exigen sus líneas estables, las que contienen alguna de STABLE.
SYSTEM_DEPENDENT = "<!-- depende del sistema -->"
STABLE = ("runtime error", "Assertion", "warning:")
# Antes de una sesión de gdb. Tiene direcciones y números de proceso: no se compara.
GDB_SESSION = "<!-- sesión de gdb -->"
def normalize(text):
# Según el idioma del entorno, gcc usa comillas tipográficas o rectas.
return text.replace("‘", "'").replace("’", "'")
def run_shell(command):
done = subprocess.run(
["bash", "-c", command], cwd=HERE, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
)
return normalize(done.stdout)
def split_commands(lines):
"""Parte un bloque de terminal en pares (comando, líneas de salida)."""
pairs = []
for line in lines:
if line.startswith("$ "):
pairs.append((line[2:], []))
elif pairs:
pairs[-1][1].append(normalize(line.rstrip()))
return pairs
def note_blocks(language):
"""Devuelve (líneas, marca) de cada bloque de los apuntes."""
blocks, current, mark, previous = [], None, "", ""
for raw in NOTES.read_text(encoding="utf-8").splitlines():
line = re.sub(r"^> ?", "", raw)
if current is None:
if line.strip() == "```" + language:
current, mark = [], previous.strip()
elif line.strip():
previous = line
elif line.strip() == "```":
blocks.append((current, mark))
current = None
else:
current.append(line)
return blocks
def slide_blocks(environments):
text = SLIDES.read_text(encoding="utf-8")
pattern = r"\\begin\{(?:%s)\}[^\n]*\n(.*?)\\end\{(?:%s)\}" % (environments, environments)
return [match.group(1).splitlines() for match in re.finditer(pattern, text, re.S)]
def known_code_lines():
lines = set()
for source in list(HERE.glob("*.c")) + list((HERE / "errors").glob("*.c")):
lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
return lines
class NotesAreTrue(unittest.TestCase):
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_terminal_blocks_in_notes(self):
checked = 0
for lines, mark in note_blocks("text"):
if not lines or not lines[0].startswith("$ ") or mark == GDB_SESSION:
continue
for command, expected in split_commands(lines):
actual = run_shell(command).rstrip("\n")
checked += 1
with self.subTest(command=command):
if mark == SYSTEM_DEPENDENT:
wanted = [line for line in expected if any(s in line for s in STABLE)]
for line in wanted:
self.assertIn(line, actual)
else:
self.assertEqual(actual, "\n".join(expected))
self.assertGreater(checked, 10)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_code_blocks_in_notes_exist(self):
known = known_code_lines()
for lines, _ in note_blocks("c"):
for line in lines:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
class SlidesAreTrue(unittest.TestCase):
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_terminal_blocks_in_slides(self):
# Las sesiones de gdb van en otro entorno, sesiongdb, que no se compara.
for block in slide_blocks("terminal|salidalarga"):
for command, expected in split_commands(block):
actual = run_shell(command).splitlines()
position = 0
# Una diapositiva puede omitir líneas, pero no cambiarlas ni reordenarlas.
for line in expected:
line = line.strip()
if not line:
continue
with self.subTest(command=command, line=line):
found = [i for i in range(position, len(actual)) if actual[i].strip() == line]
self.assertTrue(found, f"no aparece en la salida real: {line}")
if found:
position = found[0] + 1
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_code_blocks_in_slides_exist(self):
known = known_code_lines()
for block in slide_blocks("codigo|codigochico|codigomini"):
for line in block:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
if __name__ == "__main__":
unittest.main()Laboratorio
Archivos de partida del laboratorio de la semana.
// Laboratorio, parte 4: este es el sumador de 4 bits de la semana 4. Funciona, pero
// repite la misma cuenta cuatro veces. Reorganícenlo en funciones sin cambiar lo que
// escribe:
//
// int sum_bit(int a, int b, int carry_in) devuelve el bit de suma de una columna
// int carry_bit(int a, int b, int carry_in) devuelve el acarreo que sale de ella
//
// Las dos funciones reciben bits (0 o 1), solo calculan, y main ya no usa las variables
// partial0, partial1, partial2 ni partial3. Una función devuelve un solo valor: por eso
// son dos funciones.
//
// Pruébenlo con make test-lab.
#include <stdio.h>
// Suma dos números de 4 bits con cuatro sumadores de un bit en cadena.
int main(void) {
int a3, a2, a1, a0;
int b3, b2, b1, b0;
// %1d lee un solo dígito, así que 0101 se lee como cuatro números.
if (scanf("%1d%1d%1d%1d %1d%1d%1d%1d",
&a3, &a2, &a1, &a0, &b3, &b2, &b1, &b0) != 8) {
printf("Entrada inválida\n");
return 1;
}
if (a3 > 1 || a2 > 1 || a1 > 1 || a0 > 1 ||
b3 > 1 || b2 > 1 || b1 > 1 || b0 > 1) {
printf("Los bits deben ser 0 o 1\n");
return 1;
}
int carry0 = 0;
int partial0 = a0 != b0;
int sum0 = partial0 != carry0;
int carry1 = (a0 && b0) || (partial0 && carry0);
int partial1 = a1 != b1;
int sum1 = partial1 != carry1;
int carry2 = (a1 && b1) || (partial1 && carry1);
int partial2 = a2 != b2;
int sum2 = partial2 != carry2;
int carry3 = (a2 && b2) || (partial2 && carry2);
int partial3 = a3 != b3;
int sum3 = partial3 != carry3;
int carry4 = (a3 && b3) || (partial3 && carry3);
printf("acarreos: %d %d %d %d\n", carry4, carry3, carry2, carry1);
printf("suma: %d%d%d%d\n", sum3, sum2, sum1, sum0);
return 0;
}// Laboratorio, parte 1: dos funciones con sus pruebas.
//
// is_prime(n) devuelve 1 si n es primo y 0 si no lo es. Un primo es un entero
// mayor que 1 cuyos únicos divisores positivos son 1 y él mismo.
// Debe funcionar con cualquier int, también con 0 y con negativos,
// que no son primos.
// is_leap_year(y) devuelve 1 si el año y es bisiesto y 0 si no lo es. La regla es la
// de la semana 4.
//
// Las dos versiones de abajo compilan, pero están mal: solo sirven para empezar.
// Reemplácenlas. Las funciones no leen ni escriben nada.
//
// En main, agreguen sus pruebas con assert: al menos 6 para is_prime y 4 para
// is_leap_year. Las pruebas de make test-lab cuentan esas líneas, así que escriban un
// assert por línea, empezando con assert(is_prime( o assert(is_leap_year(.
//
// Pruébenlo con make test-lab.
#include <assert.h>
#include <stdio.h>
int is_prime(int n) {
return n == 2;
}
int is_leap_year(int year) {
return year == 2024;
}
int main(void) {
assert(is_prime(2) == 1);
assert(is_leap_year(2024) == 1);
// Escriban aquí el resto de sus pruebas.
printf("Todas las pruebas pasaron\n");
return 0;
}// Laboratorio, parte 2: este programa compila sin advertencias, pero tiene 4 bugs.
// Encuéntrenlos con gdb, corríjanlos aquí mismo y dejen la misma forma de salida.
//
// El programa lee la cantidad de notas, y luego las notas (enteros de 0 a 20).
// Se aprueba con 11 o más. Con esta entrada:
// 4
// 12 8 11 15
// debe escribir:
// Promedio: 11.50
// Aprobados: 3
//
// Pruébenlo con make test-lab. Para usar gdb, compílenlo como dicen los apuntes.
#include <stdio.h>
// Devuelve 1 si la nota aprueba y 0 si no.
int is_passing(int grade) {
return grade > 11;
}
// Devuelve la cuenta de aprobados después de mirar una nota más.
int count_if_passing(int passing, int grade) {
if (is_passing(grade)) {
passing = passing + 1;
}
return passing;
}
// Devuelve el total después de sumarle una nota.
int add_grade(int total, int grade) {
total = total + grade;
return total;
}
// Devuelve el promedio de count notas que suman total.
double average(int total, int count) {
return total / count;
}
int main(void) {
int count;
if (scanf("%d", &count) != 1 || count <= 0) {
printf("Cantidad inválida\n");
return 1;
}
int total = 0;
int passing = 0;
for (int i = 1; i < count; i++) {
int grade;
if (scanf("%d", &grade) != 1 || grade < 0 || grade > 20) {
printf("Nota inválida\n");
return 1;
}
add_grade(total, grade);
passing = count_if_passing(passing, grade);
}
printf("Promedio: %.2f\n", average(total, count));
printf("Aprobados: %d\n", passing);
return 0;
}4
12 8 11 15Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 7: funciones y pruebas. Se usa dentro del entorno del curso.
#
# make compila los programas de la clase en build/
# make test corre las pruebas de los ejemplos y de los apuntes
# make test-lab prueba su laboratorio (lab/*.c); falla hasta que lo terminen
# make clean borra lo generado
#
# Los programas de errors/ tienen errores a propósito. No se compilan con make:
# los compilan las pruebas, y ustedes, con los comandos de los apuntes.
# Para usar gdb se compila a mano con -g -O0 y sin sanitizers: ver los apuntes.
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
BUILD = build
PROGRAMS = square max_of_three print_line roll_die prototype has_digit by_value \
try_swap scope count_calls call_chain discount_mixed discount discount_tests \
count_digits_bug count_digits ndebug_demo digit_sum_bug digit_sum
LAB_PROGRAMS = functions grades_bug adder4
.PHONY: all lab test test-lab clean
all: $(PROGRAMS:%=$(BUILD)/%)
$(BUILD)/%: %.c | $(BUILD)
$(CC) $(CFLAGS) $< -o $@
$(BUILD)/lab/%: lab/%.c | $(BUILD)/lab
$(CC) $(CFLAGS) $< -o $@
$(BUILD)/solution/%: solution/%.c | $(BUILD)/solution
$(CC) $(CFLAGS) $< -o $@
$(BUILD) $(BUILD)/lab $(BUILD)/solution:
mkdir -p $@
lab: $(LAB_PROGRAMS:%=$(BUILD)/lab/%)
# Si existe solution/, también se comprueba la solución del laboratorio.
test: all
@if [ -d solution ]; then $(MAKE) --no-print-directory \
$(LAB_PROGRAMS:%=$(BUILD)/solution/%); fi
python3 -m unittest discover -p 'test_*.py'
test-lab: all lab
LAB_DIR=lab python3 -m unittest -v test_lab
clean:
rm -rf $(BUILD) __pycache__