Semana 1 — Repaso: lo esencial de C
Objetivos
- Recordar y usar tipos, operadores, decisiones, ciclos y funciones en C.
- Separar el cálculo de la entrada y salida, y probarlo con
assert. - Volver a dejar listo el entorno, la terminal y Git.
Teoría — Presentación · Repaso: tipos, control, funciones y assert
Lectura: Apuntes, semana 1.
Temas:
- De qué trata el curso y por qué un programa grande pide herramientas distintas a uno pequeño.
- Repaso rápido, sin volver a enseñarlo:
int,double,char, operadores,if,while,for. - Funciones: parámetros, retorno y paso por valor.
- Una función que calcula es más fácil de probar que una que además imprime.
asserty casos borde. - Compilar con
-Wall -Wextray leer las advertencias como si fueran errores.
Laboratorio — Entorno y programas de repaso
Sesión 1:
- Abrir el entorno y correr
env/check.sh. Repasarmake,make testy lo básico de Git:addycommit. Conocerclone, que es nuevo. - Prueba diagnóstica sin nota: cinco funciones cortas con
make test-diagnostic, para que cada uno vea qué debe repasar.
Sesión 2:
3. Escribir digit_sum, is_prime y gcd con sus pruebas assert, incluidos los casos borde.
4. Escribir collatz_steps y buscar el número menor que 10 000 con más pasos.
Preguntas de repaso
- ¿Qué escribe
int a = 7, b = 2; printf("%d %.1f", a / b, (double) a / b);? ¿Por qué? - ¿Por qué una función que calcula y además imprime es más difícil de probar?
- ¿Qué casos de prueba elegirían para
is_prime? - ¿Qué advertencia da
-Wallsi una función que devuelveintno tienereturn, y por qué importa?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (10)Errores (8)Pruebas (4)Laboratorio (3)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
#include <stdio.h>
// Prototipos: anuncian las funciones que se definen después de main.
void add_one(int x);
void try_swap(int a, int b);
int main(void) {
int x = 5;
add_one(x);
printf("En main, x vale %d\n", x);
int first = 1;
int second = 2;
try_swap(first, second);
printf("En main, first = %d y second = %d\n", first, second);
return 0;
}
// Cambia su copia. La variable de quien llama no se entera.
void add_one(int x) {
x = x + 1;
printf("En add_one, x vale %d\n", x);
}
// Intercambia sus copias, y nada más.
void try_swap(int a, int b) {
int saved = a;
a = b;
b = saved;
printf("En try_swap, a = %d y b = %d\n", a, b);
}#include <stdio.h>
// Todo junto: leer, calcular y escribir. Funciona, pero solo se puede probar
// corriendo el programa y mirando lo que escribe.
int main(void) {
int cents;
if (scanf("%d", ¢s) != 1) {
printf("Entrada inválida\n");
return 1;
}
if (cents < 0 || cents % 10 != 0) {
printf("No se puede dar %d céntimos en monedas\n", cents);
return 1;
}
int left = cents;
int coins = 0;
while (left > 0) {
if (left >= 500) {
left = left - 500;
} else if (left >= 200) {
left = left - 200;
} else if (left >= 100) {
left = left - 100;
} else if (left >= 50) {
left = left - 50;
} else if (left >= 20) {
left = left - 20;
} else {
left = left - 10;
}
coins = coins + 1;
}
printf("Para %d céntimos hacen falta %d monedas\n", cents, coins);
return 0;
}#include <stdio.h>
// Devuelve la moneda más grande que no pasa de cents, en céntimos.
// cents es un múltiplo de 10 mayor que 0.
int largest_coin(int cents) {
if (cents >= 500) {
return 500;
} else if (cents >= 200) {
return 200;
} else if (cents >= 100) {
return 100;
} else if (cents >= 50) {
return 50;
} else if (cents >= 20) {
return 20;
}
return 10;
}
// Devuelve cuántas monedas hacen falta para dar cents, o -1 si no se puede.
int coins_needed(int cents) {
if (cents < 0 || cents % 10 != 0) {
return -1;
}
int coins = 0;
while (cents > 0) {
cents = cents - largest_coin(cents);
coins = coins + 1;
}
return coins;
}
int main(void) {
int cents;
if (scanf("%d", ¢s) != 1) {
printf("Entrada inválida\n");
return 1;
}
int coins = coins_needed(cents);
if (coins < 0) {
printf("No se puede dar %d céntimos en monedas\n", cents);
return 1;
}
printf("Para %d céntimos hacen falta %d monedas\n", cents, coins);
return 0;
}#include <stdio.h>
// El tipo de los operandos decide qué división se hace.
int main(void) {
int a = 7, b = 2;
printf("%d %.1f\n", a / b, (double) a / b);
double half = 1 / 2;
double real_half = 1.0 / 2;
printf("%.1f %.1f\n", half, real_half);
int truncated = 3.99;
printf("%d\n", truncated);
return 0;
}#include <stdio.h>
// Lee notas hasta que se acabe la entrada y escribe el promedio.
int main(void) {
int grade;
int total = 0;
int count = 0;
while (scanf("%d", &grade) == 1) {
total = total + grade;
count = count + 1;
}
if (count == 0) {
printf("No hay notas\n");
return 1;
}
printf("%d notas, promedio %.2f\n", count, (double) total / count);
return 0;
}#include <stdio.h>
// Cuenta los múltiplos de step que hay de 1 a n. Se sabe cuántas vueltas da: for.
int count_multiples(int n, int step) {
int count = 0;
for (int i = 1; i <= n; i++) {
if (i % step == 0) {
count = count + 1;
}
}
return count;
}
// Cuenta cuántas veces se puede dividir n entre 2 antes de llegar a 1: while.
int count_halvings(int n) {
int count = 0;
while (n > 1) {
n = n / 2;
count = count + 1;
}
return count;
}
// Cuenta las cifras de n. El cuerpo corre al menos una vez, y el 0 tiene una: do while.
int count_digits(int n) {
int count = 0;
do {
count = count + 1;
n = n / 10;
} while (n != 0);
return count;
}
int main(void) {
printf("%d\n", count_multiples(9, 3));
printf("%d\n", count_halvings(100));
printf("%d %d %d\n", count_digits(2026), count_digits(0), count_digits(-42));
return 0;
}#include <stdio.h>
// Error: con i < n, el ciclo nunca mira al propio n.
int count_multiples(int n, int step) {
int count = 0;
for (int i = 1; i < n; i++) {
if (i % step == 0) {
count = count + 1;
}
}
return count;
}
int main(void) {
printf("%d\n", count_multiples(9, 3));
printf("%d\n", count_multiples(10, 3));
return 0;
}#include <stdio.h>
int main(void) {
printf("%d %d\n", 7 / 2, -7 / 2);
printf("%d %d\n", 7 % 3, -7 % 3);
printf("%d %d\n", 2 + 3 * 4, (2 + 3) * 4);
int count = 0;
int total = 50;
// Si count es 0, el lado derecho no se calcula y no se divide entre 0.
if (count != 0 && total / count > 10) {
printf("Promedio alto\n");
} else {
printf("Sin datos o promedio bajo\n");
}
return 0;
}#include <limits.h>
#include <stdio.h>
// Pasarse del mayor int es comportamiento indefinido. El sanitizer lo avisa al correr.
int main(void) {
int big = INT_MAX;
big = big + 1;
printf("%d\n", big);
return 0;
}#include <limits.h>
#include <stdio.h>
// Tamaños y límites de los tipos del curso. Dependen del sistema.
int main(void) {
printf("int: %zu bytes, de %d a %d\n", sizeof(int), INT_MIN, INT_MAX);
printf("double: %zu bytes\n", sizeof(double));
printf("char: %zu byte\n", sizeof(char));
char digit = '7';
printf("%c tiene el código %d y vale %d\n", digit, digit, digit - '0');
double x = 0.1 + 0.2;
printf("%.17f\n", x);
if (x == 0.3) {
printf("x es igual a 0.3\n");
} else {
printf("x no es igual a 0.3\n");
}
return 0;
}Errores
Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.
// Tiene un bug a propósito: el caso normal pasa, y el borde no.
#include <assert.h>
#include <stdio.h>
int largest_coin(int cents) {
if (cents > 500) {
return 500;
} else if (cents >= 200) {
return 200;
} else if (cents >= 100) {
return 100;
} else if (cents >= 50) {
return 50;
} else if (cents >= 20) {
return 20;
}
return 10;
}
int coins_needed(int cents) {
if (cents < 0 || cents % 10 != 0) {
return -1;
}
int coins = 0;
while (cents > 0) {
cents = cents - largest_coin(cents);
coins = coins + 1;
}
return coins;
}
int main(void) {
assert(coins_needed(380) == 5); // caso normal: 200, 100, 50, 20, 10
assert(coins_needed(0) == 0); // el menor monto: no se da ninguna moneda
assert(coins_needed(10) == 1); // la moneda más chica
assert(coins_needed(500) == 1); // justo una moneda grande
assert(coins_needed(490) == 5); // justo debajo de 500
assert(coins_needed(1000) == 2); // más de una moneda grande
assert(coins_needed(15) == -1); // no es múltiplo de 10
assert(coins_needed(-10) == -1); // negativo
printf("Todas las pruebas pasaron\n");
return 0;
}#include <stdio.h>
int main(void) {
int grade = 12;
// Error: = asigna, no compara. La condición siempre es verdadera.
if (grade = 20) {
printf("Nota perfecta: %d\n", grade);
}
return 0;
}#include <stdio.h>
int main(void) {
int grade = 25;
// Error: se calcula (0 <= grade), que da 0 o 1, y eso se compara con 20.
if (0 <= grade <= 20) {
printf("Nota válida\n");
}
return 0;
}#include <stdio.h>
int square(int x) {
return x * x;
}
int main(void) {
// Ojo: 2.5 se convierte a 2 sin ningún aviso.
printf("%d\n", square(2.5));
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) {
printf("%d\n", is_passing(5));
return 0;
}#include <stdio.h>
int main(void) {
int total;
// Error: total nunca recibió un valor inicial.
for (int i = 1; i <= 3; i++) {
total = total + i;
}
printf("%d\n", total);
return 0;
}#include <stdio.h>
// Error: recibe step, pero se olvidó de usarlo.
int next_value(int value, int step) {
return value + 1;
}
int main(void) {
printf("%d\n", next_value(10, 5));
return 0;
}#include <stdio.h>
int main(void) {
double average = 13.5;
// Error: %d espera un int, y aquí llega un double.
printf("Promedio: %d\n", average);
return 0;
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
#include <assert.h>
#include <stdio.h>
int largest_coin(int cents) {
if (cents >= 500) {
return 500;
} else if (cents >= 200) {
return 200;
} else if (cents >= 100) {
return 100;
} else if (cents >= 50) {
return 50;
} else if (cents >= 20) {
return 20;
}
return 10;
}
int coins_needed(int cents) {
if (cents < 0 || cents % 10 != 0) {
return -1;
}
int coins = 0;
while (cents > 0) {
cents = cents - largest_coin(cents);
coins = coins + 1;
}
return coins;
}
int main(void) {
assert(coins_needed(380) == 5); // caso normal: 200, 100, 50, 20, 10
assert(coins_needed(0) == 0); // el menor monto: no se da ninguna moneda
assert(coins_needed(10) == 1); // la moneda más chica
assert(coins_needed(500) == 1); // justo una moneda grande
assert(coins_needed(490) == 5); // justo debajo de 500
assert(coins_needed(1000) == 2); // más de una moneda grande
assert(coins_needed(15) == -1); // no es múltiplo de 10
assert(coins_needed(-10) == -1); // negativo
printf("Todas las pruebas pasaron\n");
return 0;
}"""Comprueba lo que los apuntes afirman sobre los programas de la semana 1."""
import pathlib
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_messages(source, *extra):
"""Compila un programa de errors/; devuelve los mensajes y el estado."""
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
def fewest_coins(cents):
"""Otra forma de contar las monedas, para comparar: probar todas las combinaciones."""
best = [0] + [None] * cents
for amount in range(10, cents + 1, 10):
best[amount] = 1 + min(best[amount - coin] for coin in (10, 20, 50, 100, 200, 500)
if coin <= amount)
return best[cents]
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)
class TypesAndOperators(unittest.TestCase):
def test_division(self):
self.assertEqual(run("division"), ("3 3.5\n0.0 0.5\n3\n", 0))
def test_types(self):
output, status = run("types")
self.assertEqual(status, 0)
self.assertIn("7 tiene el código 55 y vale 7\n", output)
self.assertIn("0.30000000000000004\nx no es igual a 0.3\n", output)
def test_overflow_is_reported(self):
self.assertIn("signed integer overflow", run("overflow")[0])
def test_operators(self):
self.assertEqual(run("operators"), ("3 -3\n1 -1\n14 20\nSin datos o promedio bajo\n", 0))
class Control(unittest.TestCase):
def test_loops(self):
self.assertEqual(run("loops"), ("3\n6\n4 1 2\n", 0))
def test_off_by_one(self):
self.assertEqual(run("off_by_one"), ("2\n3\n", 0))
def test_grades_average(self):
self.assertEqual(run("grades_average", "12 8 15\n"), ("3 notas, promedio 11.67\n", 0))
self.assertEqual(run("grades_average", ""), ("No hay notas\n", 1))
self.assertEqual(run("grades_average", "12 abc 15\n"), ("1 notas, promedio 12.00\n", 0))
class Functions(unittest.TestCase):
def test_by_value(self):
self.assertEqual(run("by_value"), (
"En add_one, x vale 6\nEn main, x vale 5\n"
"En try_swap, a = 2 y b = 1\nEn main, first = 1 y second = 2\n", 0))
def test_double_argument_has_no_warning(self):
self.assertEqual(compile_messages("errors/double_argument.c"), ("", 0))
class Change(unittest.TestCase):
def test_both_versions_agree_with_the_fewest_coins(self):
for cents in range(0, 2010, 10):
expected = f"Para {cents} céntimos hacen falta {fewest_coins(cents)} monedas\n"
for program in ["change", "change_print"]:
with self.subTest(program=program, cents=cents):
self.assertEqual(run(program, str(cents)), (expected, 0))
def test_amounts_that_cannot_be_given(self):
for cents in [15, -10, 7, -5]:
expected = f"No se puede dar {cents} céntimos en monedas\n"
for program in ["change", "change_print"]:
with self.subTest(program=program, cents=cents):
self.assertEqual(run(program, str(cents)), (expected, 1))
def test_bad_input(self):
for program in ["change", "change_print"]:
self.assertEqual(run(program, "abc"), ("Entrada inválida\n", 1))
def test_change_tests_pass(self):
self.assertEqual(run("change_tests"), ("Todas las pruebas pasaron\n", 0))
def test_change_bug_passes_the_normal_case_and_fails_the_edge(self):
output, status = run("change_bug")
self.assertNotEqual(status, 0)
self.assertIn("Assertion `coins_needed(500) == 1' failed", output)
class Exercises(unittest.TestCase):
def test_exercise_mutation_is_not_caught(self):
# Con > 100 todas las pruebas de change_tests.c pasan, y aun así la función falla con 100.
text = (HERE / "change_tests.c").read_text(encoding="utf-8")
self.assertIn("cents >= 100", text)
with tempfile.TemporaryDirectory() as folder:
mutated = pathlib.Path(folder) / "mutated.c"
mutated.write_text(text.replace("cents >= 100", "cents > 100"), encoding="utf-8")
binary = pathlib.Path(folder) / "mutated"
subprocess.run(["gcc", *FLAGS, str(mutated), "-o", str(binary)], check=True)
done = subprocess.run([str(binary)], text=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
self.assertEqual((done.stdout, done.returncode), ("Todas las pruebas pasaron\n", 0))
mutated.write_text(text.replace("cents >= 100", "cents > 100").replace(
"assert(coins_needed(380) == 5);", "assert(coins_needed(100) == 1);"), encoding="utf-8")
subprocess.run(["gcc", *FLAGS, str(mutated), "-o", str(binary)], check=True)
done = subprocess.run([str(binary)], text=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
self.assertNotEqual(done.returncode, 0)
def test_collatz_numbers_of_the_exercises(self):
def peak(n):
highest = n
while n != 1:
n = n // 2 if n % 2 == 0 else 3 * n + 1
highest = max(highest, n)
return highest
self.assertLessEqual(max(peak(n) for n in range(1, 10000)), 2147483647)
self.assertLessEqual(max(peak(n) for n in range(1, 113383)), 2147483647)
self.assertGreater(peak(113383), 2147483647)
self.assertEqual(peak(6), 16)
class Warnings(unittest.TestCase):
def test_flags_report_the_mistakes(self):
cases = {
"errors/missing_return.c": "-Wreturn-type",
"errors/assign_in_if.c": "-Wparentheses",
"errors/chained_compare.c": "-Wparentheses",
"errors/wrong_format.c": "-Wformat",
"errors/unused_parameter.c": "-Wunused-parameter",
}
for source, flag in cases.items():
with self.subTest(source=source):
messages, status = compile_messages(source)
self.assertIn(flag, messages)
self.assertEqual(status, 0, "una advertencia no detiene al compilador")
def test_uninitialized_needs_optimization_to_warn(self):
self.assertEqual(compile_messages("errors/uninitialized.c"), ("", 0))
messages, status = compile_messages("errors/uninitialized.c", "-O2")
self.assertIn("-Wuninitialized", messages)
self.assertEqual(status, 0)
def test_werror_stops_the_build(self):
messages, status = compile_messages("errors/missing_return.c", "-Werror")
self.assertNotEqual(status, 0)
self.assertIn("all warnings being treated as errors", messages)
def test_wall_alone_misses_the_unused_parameter(self):
done = subprocess.run(
["gcc", "-Wall", "-std=c17", "-fsyntax-only", "errors/unused_parameter.c"],
cwd=HERE, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
self.assertEqual(done.stdout, "")
class GitSession(unittest.TestCase):
def test_commands_of_the_notes_work(self):
def git(*arguments, cwd):
return subprocess.run(["git", *arguments], cwd=cwd, text=True, check=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout
with tempfile.TemporaryDirectory() as folder:
base = pathlib.Path(folder)
original = base / "original"
original.mkdir()
git("init", "-q", "-b", "main", cwd=original)
git("config", "user.name", "Ana", cwd=original)
git("config", "user.email", "ana@example.com", cwd=original)
(original / "notas.txt").write_text("12\n", encoding="utf-8")
git("add", "notas.txt", cwd=original)
git("commit", "-q", "-m", "Primera nota", cwd=original)
git("clone", "-q", str(original), "copia", cwd=base)
copy = base / "copia"
self.assertEqual((copy / "notas.txt").read_text(encoding="utf-8"), "12\n")
git("config", "user.name", "Ana", cwd=copy)
git("config", "user.email", "ana@example.com", cwd=copy)
(copy / "notas.txt").write_text("12\n15\n", encoding="utf-8")
git("add", "notas.txt", cwd=copy)
git("commit", "-q", "-m", "Agrega una nota", cwd=copy)
self.assertEqual(len(git("log", "--oneline", cwd=copy).splitlines()), 2)
self.assertEqual(len(git("log", "--oneline", cwd=original).splitlines()), 1)
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 math
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"]
INT_MAX = 2147483647
INT_MIN = -2147483648
# Esconde de los mensajes de falla las líneas de Python, que no se enseña aquí.
__unittest = True
def source(name):
return (HERE / LAB_DIR / f"{name}.c").read_text(encoding="utf-8")
def run(program):
done = subprocess.run(
[str(HERE / "build" / LAB_DIR / program)], text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=20,
)
return done.stdout.strip(), done.returncode
def call_with_driver(name, driver_main, stdin):
"""Compila lab/<name>.c con otro main y devuelve lo que escribe."""
with tempfile.TemporaryDirectory() as folder:
driver = pathlib.Path(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 = pathlib.Path(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=30,
)
if "runtime error" in done.stdout or "AddressSanitizer" in done.stdout:
raise AssertionError(f"El sanitizer encontró un problema:\n{done.stdout}")
return done.stdout.splitlines()
def int_main(function, arguments=1):
"""Un main que lee enteros de a grupos y escribe lo que devuelve la función."""
names = [f"v{i}" for i in range(arguments)]
formats = " ".join(["%d"] * arguments)
pointers = ", ".join(f"&{name}" for name in names)
return (
f"int main(void) {{\n int {', '.join(names)};\n"
f' while (scanf("{formats}", {pointers}) == {arguments}) {{\n'
f' printf("%d\\n", {function}({", ".join(names)}));\n }}\n return 0;\n}}'
)
def is_prime(n):
return n > 1 and all(n % i for i in range(2, math.isqrt(n) + 1))
def collatz(n):
steps = 0
while n != 1:
n = n // 2 if n % 2 == 0 else 3 * n + 1
steps += 1
return steps
def no_warnings(test, 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,
)
test.assertEqual(done.returncode, 0, done.stdout)
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe {LAB_DIR}/")
class DiagnosticTests(unittest.TestCase):
"""Sesión 1. Si una falla, el mensaje dice qué conviene repasar."""
def check(self, function, inputs, expected, review, arguments=1):
stdin = " ".join(str(value) for group in inputs for value in group)
lines = call_with_driver("diagnostic", int_main(function, arguments), stdin)
shown = ", ".join(str(group[0]) if arguments == 1 else str(group) for group in inputs)
self.assertEqual([int(line) for line in lines], expected,
f"Datos: {shown}. Conviene repasar: {review}.")
def test_no_warnings(self):
no_warnings(self, "diagnostic")
def test_average_of_two(self):
driver = (
'int main(void) {\n int a, b;\n while (scanf("%d %d", &a, &b) == 2) {\n'
' printf("%.2f\\n", average_of_two(a, b));\n }\n return 0;\n}'
)
pairs = [(7, 2), (2, 7), (4, 2), (0, 0), (-3, 0), (1, 2), (-7, 2), (100, 101)]
stdin = " ".join(f"{a} {b}" for a, b in pairs)
self.assertEqual(call_with_driver("diagnostic", driver, stdin),
[f"{(a + b) / 2:.2f}" for a, b in pairs],
f"Datos: {', '.join(map(str, pairs))}. Conviene repasar: división y conversión entre tipos, semana 2.")
def test_clamp(self):
triples = [(25, 0, 20), (-1, 0, 20), (0, 0, 20), (20, 0, 20), (11, 0, 20),
(5, 5, 5), (-10, -8, -2), (-5, -8, -2), (0, -8, -2)]
self.check("clamp", triples, [min(max(v, lo), hi) for v, lo, hi in triples],
"decisiones con if y else if, semana 4", arguments=3)
def test_sum_to(self):
values = [1, 2, 4, 10, 100, 1000, 0, -1, -50]
self.check("sum_to", [(n,) for n in values], [sum(range(1, n + 1)) for n in values],
"ciclos, y salirse por uno, semana 5")
def test_digit_value(self):
driver = (
'int main(void) {\n int code;\n while (scanf("%d", &code) == 1) {\n'
' printf("%d\\n", digit_value((char) code));\n }\n return 0;\n}'
)
codes = list(range(32, 127))
stdin = " ".join(map(str, codes))
expected = [str(code - 48) if 48 <= code <= 57 else "-1" for code in codes]
self.assertEqual(call_with_driver("diagnostic", driver, stdin), expected,
"Datos: los caracteres de código 32 a 126. "
"Conviene repasar: char es un número pequeño, ASCII, semanas 2 y 3.")
def test_dice_ways(self):
values = list(range(-1, 15))
expected = [sum(1 for a in range(1, 7) for b in range(1, 7) if a + b == t) for t in values]
self.check("dice_ways", [(t,) for t in values], expected,
"ciclos anidados, semana 6")
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe {LAB_DIR}/")
class NumbersTests(unittest.TestCase):
"""Sesión 2: digit_sum, is_prime y gcd."""
def values(self, function, numbers, arguments=1):
stdin = " ".join(str(n) for group in numbers for n in group)
return [int(line) for line in call_with_driver("numbers", int_main(function, arguments), stdin)]
def test_no_warnings(self):
no_warnings(self, "numbers")
def test_digit_sum_normal(self):
numbers = [5, 9, 10, 23, 99, 100, 407, 2026, 987654321]
self.assertEqual(self.values("digit_sum", [(n,) for n in numbers]),
[sum(map(int, str(n))) for n in numbers])
def test_digit_sum_edges(self):
numbers = [0, -1, -9, -407, INT_MAX, INT_MIN]
self.assertEqual(self.values("digit_sum", [(n,) for n in numbers]),
[sum(map(int, str(abs(n)))) for n in numbers])
def test_is_prime_first_numbers(self):
numbers = list(range(-20, 500))
self.assertEqual(self.values("is_prime", [(n,) for n in numbers]),
[int(is_prime(n)) for n in numbers])
def test_is_prime_edges(self):
numbers = [INT_MIN, -7, -2, -1, 0, 1, 2, 3, 4, 9, 25, 49, 121, 169, 7919, 1000003,
46337 * 46337, 2147483629, INT_MAX]
self.assertEqual(self.values("is_prime", [(n,) for n in numbers]),
[int(is_prime(n)) for n in numbers])
def test_gcd_normal(self):
pairs = [(48, 18), (18, 48), (13, 7), (7, 7), (100, 75), (1, 1000), (1071, 462), (INT_MAX, 1)]
self.assertEqual(self.values("gcd", pairs, 2), [math.gcd(a, b) for a, b in pairs])
def test_gcd_with_zero(self):
pairs = [(5, 0), (0, 5), (0, 0), (-5, 0), (0, -5), (INT_MAX, 0)]
self.assertEqual(self.values("gcd", pairs, 2), [math.gcd(a, b) for a, b in pairs])
def test_gcd_with_negatives(self):
pairs = [(12, -18), (-12, 18), (-12, -18), (-18, 12), (-1, 1), (-INT_MAX, INT_MAX), (-7, 3)]
self.assertEqual(self.values("gcd", pairs, 2), [math.gcd(a, b) for a, b in pairs])
def test_their_tests_exist_and_pass(self):
text = source("numbers")
self.assertNotIn("NDEBUG", text)
for function, minimum in [("digit_sum", 5), ("is_prime", 7), ("gcd", 6)]:
with self.subTest(function=function):
found = re.findall(rf"^\s*assert\({function}\(", text, re.M)
self.assertGreaterEqual(len(found), minimum, f"faltan pruebas de {function}")
self.assertEqual(run("numbers"), ("Todas las pruebas pasaron", 0))
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe {LAB_DIR}/")
class CollatzTests(unittest.TestCase):
"""Sesión 2: collatz_steps y longest_collatz."""
def values(self, function, numbers):
stdin = " ".join(map(str, numbers))
return [int(line) for line in call_with_driver("collatz", int_main(function), stdin)]
def test_no_warnings(self):
no_warnings(self, "collatz")
def test_steps_small(self):
numbers = [1, 2, 3, 4, 5, 6, 7, 16, 27, 97]
self.assertEqual(self.values("collatz_steps", numbers), [collatz(n) for n in numbers])
def test_steps_below_ten_thousand(self):
numbers = list(range(1, 10000))
self.assertEqual(self.values("collatz_steps", numbers), [collatz(n) for n in numbers])
def test_steps_outside_the_domain(self):
self.assertEqual(self.values("collatz_steps", [0, -1, -6]), [-1, -1, -1])
def test_longest(self):
for limit in [2, 3, 4, 7, 8, 10, 28, 100, 1000, 10000]:
with self.subTest(limit=limit):
best = max(range(1, limit), key=lambda n: (collatz(n), -n))
self.assertEqual(self.values("longest_collatz", [limit]), [best])
def test_longest_with_no_numbers(self):
self.assertEqual(self.values("longest_collatz", [1, 0, -5]), [0, 0, 0])
def test_main_writes_the_answer(self):
best = max(range(1, 10000), key=lambda n: (collatz(n), -n))
expected = f"Menor que 10000, el de más pasos es {best}, con {collatz(best)} pasos"
self.assertEqual(run("collatz"), (expected, 0))
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-01.md"
SLIDES = ROOT / "slides" / "week-01.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 que no se puede repetir aquí: Git, con identificadores que cambian,
# o make test, que se llamaría a sí mismo. No se compara.
OTHER_SESSION = "<!-- sesión aparte -->"
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()
sources = list(HERE.glob("*.c")) + list((HERE / "errors").glob("*.c")) + list((HERE / "lab").glob("*.c"))
for source in sources:
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 == OTHER_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):
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, sesión 2: la sucesión de Collatz.
//
// Se empieza en un entero n mayor o igual que 1. Mientras n no sea 1: si es par, se divide
// entre 2, y si es impar, se reemplaza por 3 * n + 1. Con 6 la sucesión es
// 6, 3, 10, 5, 16, 8, 4, 2, 1, que son 8 pasos.
//
// collatz_steps(n) devuelve cuántos pasos da n hasta llegar a 1. Con 1 devuelve 0.
// Si n es menor que 1 devuelve -1, porque ahí la sucesión no se usa
// (con 0, el ciclo nunca terminaría).
// longest_collatz(limit) devuelve el número de 1 a limit - 1 con más pasos. Si hay
// empate, el menor de ellos. Si limit es menor que 2, devuelve 0.
//
// Para todo número menor que 10 000, los valores de la sucesión caben en un int.
//
// En main, escriban lo que pide el mensaje de abajo, con esa misma forma, para el número
// menor que 10 000 con más pasos. Usen sus dos funciones: main solo llama y escribe.
//
// Las versiones de abajo compilan, pero están mal: solo sirven para empezar.
//
// Pruébenlo con make test-lab.
#include <stdio.h>
int collatz_steps(int n) {
return n;
}
int longest_collatz(int limit) {
return limit;
}
int main(void) {
int best = longest_collatz(10000);
printf("Menor que 10000, el de más pasos es %d, con %d pasos\n", best, 0);
return 0;
}// Laboratorio, sesión 1: prueba diagnóstica. No tiene nota: sirve para que cada uno vea
// qué debe repasar. Trabajen sin mirar sus apuntes hasta donde puedan.
//
// average_of_two(a, b) devuelve el promedio exacto de a y b, con decimales.
// Con 7 y 2 devuelve 4.5.
// clamp(value, low, high) devuelve low si value es menor que low, high si value es
// mayor que high, y value en otro caso. low no pasa de high.
// sum_to(n) devuelve 1 + 2 + ... + n, y 0 si n no es mayor que 0.
// digit_value(c) devuelve el valor numérico del carácter de '0' a '9': con '7'
// devuelve 7. Con cualquier otro carácter devuelve -1.
// dice_ways(total) devuelve de cuántas formas dos dados suman total, contando
// (1, 6) y (6, 1) como formas distintas. Con 7 devuelve 6, y con
// un total imposible, 0.
//
// Los números que reciben average_of_two, clamp y sum_to son pequeños, así que no hay
// riesgo de overflow.
//
// Las versiones de abajo compilan, pero están mal: solo sirven para empezar. Reemplácenlas.
// Las funciones no leen ni escriben nada.
//
// Pruébenlo con make test-diagnostic. Cada función que falla dice qué semana de
// Introducción a la Computación conviene repasar.
#include <stdio.h>
double average_of_two(int a, int b) {
return a + b;
}
int clamp(int value, int low, int high) {
return value + low - high;
}
int sum_to(int n) {
return n;
}
int digit_value(char c) {
return c;
}
int dice_ways(int total) {
return total;
}
// Este main es para que hagan sus propios intentos. Las pruebas no lo usan.
int main(void) {
printf("%.1f\n", average_of_two(7, 2));
printf("%d\n", clamp(25, 0, 20));
printf("%d\n", sum_to(4));
printf("%d\n", digit_value('7'));
printf("%d\n", dice_ways(7));
return 0;
}// Laboratorio, sesión 2: tres funciones con sus pruebas.
//
// digit_sum(n) devuelve la suma de las cifras de n. El signo no cuenta: con -407
// devuelve 11. Debe funcionar con cualquier int, también con INT_MIN.
// 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 INT_MAX.
// gcd(a, b) devuelve el máximo común divisor de a y b, que nunca es negativo:
// gcd(12, -18) es 6, gcd(-5, 0) es 5 y gcd(0, 0) es 0. a y b son mayores
// que INT_MIN.
//
// Las 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 5 para digit_sum, 7 para is_prime y
// 6 para gcd, con sus casos borde. Las pruebas de make test-lab cuentan esas líneas, así
// que escriban un assert por línea, empezando con assert(digit_sum(, assert(is_prime( o
// assert(gcd(.
//
// Pruébenlo con make test-lab.
#include <assert.h>
#include <limits.h>
#include <stdio.h>
int digit_sum(int n) {
return n;
}
int is_prime(int n) {
return n == 2;
}
int gcd(int a, int b) {
return a + b;
}
int main(void) {
// Estas tres pasan con las versiones de arriba. No se dejen engañar.
assert(digit_sum(7) == 7);
assert(is_prime(2) == 1);
assert(gcd(5, 0) == 5);
// Escriban aquí el resto de sus pruebas.
printf("Todas las pruebas pasaron\n");
return 0;
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 1: repaso de C. 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-diagnostic prueba solo la prueba diagnóstica (lab/diagnostic.c)
# make test-lab prueba todo su laboratorio; 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.
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
BUILD = build
PROGRAMS = division types overflow operators loops off_by_one grades_average \
by_value change_print change change_tests change_bug
LAB_PROGRAMS = diagnostic numbers collatz
.PHONY: all lab test test-diagnostic 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-diagnostic: lab
LAB_DIR=lab python3 -m unittest -v test_lab.DiagnosticTests
test-lab: all lab
LAB_DIR=lab python3 -m unittest -v test_lab
clean:
rm -rf $(BUILD) __pycache__