Semana 2 — Repaso: datos y lectura de código
Objetivos
- Recordar y usar arreglos, cadenas,
structy archivos de texto. - Leer un programa ajeno con método antes de cambiarlo.
- Corregir y extender un programa sin romper lo que ya funciona.
Teoría — Repaso: arreglos, cadenas, struct y archivos · Cómo leer código
Lectura: Apuntes, semana 2.
Temas:
- Repaso rápido: arreglos y su tamaño, cadenas y
\0,struct,fopen,fgetsyfclose, revisando cada resultado. - Leer código es una habilidad: casi siempre se lee más código del que se escribe.
- Método: empezar por
mainy las firmas, ejecutarlo con un ejemplo, seguir un dato de principio a fin y escribir lo que se entendió. - Leer las pruebas como documentación. Cambiar poco y volver a correr las pruebas.
Laboratorio — Leer un programa ajeno y extenderlo
Se trabaja con un programa dado de unas 200 líneas, que procesa un archivo de notas.
Sesión 1:
- Ejecutarlo y responder por escrito: qué hace cada función, quién llama a quién y qué forma tienen los datos, sin cambiar nada todavía.
- Encontrar y corregir dos bugs a partir de las pruebas que fallan.
Sesión 2: 3. Agregar una función nueva: el promedio de cada estudiante sin su nota más baja, con pruebas. 4. Agregar una opción para filtrar por sección, tocando la menor cantidad posible de código.
Preguntas de repaso
- ¿Qué pasa si el arreglo
char s[5]recibe la cadena"hola!"? - ¿Por qué hay que pasar el tamaño de un arreglo a una función?
- ¿Por dónde empezarían a leer un programa de 2 000 líneas que no conocen?
- ¿Qué información da una prueba que no da un comentario?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (8)Errores (7)Pruebas (5)Laboratorio (5)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// Repaso de arreglos: capacidad, longitud y paso a funciones.
#include <stdio.h>
#define CAPACITY 8
int sum(const int a[], int n) {
int total = 0;
for (int i = 0; i < n; i++) {
total = total + a[i];
}
return total;
}
// Cambia los elementos de quien llama: por eso no lleva const.
void add_bonus(int a[], int n, int bonus) {
for (int i = 0; i < n; i++) {
a[i] = a[i] + bonus;
}
}
int main(void) {
int grades[CAPACITY] = {14, 9, 17, 11};
int n = 4;
printf("sizeof(grades) = %zu bytes, capacidad %d, longitud %d\n", sizeof(grades), CAPACITY, n);
printf("Suma: %d\n", sum(grades, n));
printf("Suma de las 2 primeras: %d\n", sum(grades, 2));
add_bonus(grades, n, 1);
printf("Con un punto más: %d\n", sum(grades, n));
return 0;
}// Repaso de archivos: abrir, leer línea por línea, separar campos y cerrar.
#include <stdio.h>
#define PATH_SIZE 128
#define LINE_SIZE 64
#define NAME_SIZE 20
int remove_newline(char s[]) {
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == '\n') {
s[i] = '\0';
return 1;
}
}
return 0;
}
int main(void) {
char path[PATH_SIZE];
if (fgets(path, PATH_SIZE, stdin) == NULL) {
fprintf(stderr, "Falta el nombre del archivo\n");
return 1;
}
remove_newline(path);
FILE *file = fopen(path, "r");
if (file == NULL) {
perror(path);
return 1;
}
char line[LINE_SIZE];
int number = 0;
int total = 0;
while (fgets(line, LINE_SIZE, file) != NULL) {
number++;
char name[NAME_SIZE];
int people;
char extra[2];
int fields = sscanf(line, "%19s %d %1s", name, &people, extra);
if (fields == 2) {
total = total + people;
} else {
fprintf(stderr, "Línea %d: mal formada, se omite\n", number);
}
}
fclose(file);
printf("Personas: %d\n", total);
return 0;
}Tacna 42
Grau 17
Abancay 8
Wilson x
Bolivia 25// Repaso de cadenas: el '\0', sizeof contra strlen, y comparar con strcmp.
#include <stdio.h>
#include <string.h>
int main(void) {
char word[8] = "hola";
printf("sizeof = %zu, strlen = %zu\n", sizeof(word), strlen(word));
word[2] = '\0';
printf("Con un nulo en medio: \"%s\", strlen = %zu\n", word, strlen(word));
char a[] = "Ana";
char b[] = "Ana";
printf("strcmp(a, b) = %d\n", strcmp(a, b));
return 0;
}// Repaso de struct: copia al asignar, copia al pasar, y arreglos de struct.
#include <stdio.h>
#define NAME_SIZE 20
#define N_GRADES 3
struct student {
char name[NAME_SIZE];
int grades[N_GRADES];
};
// Cambia su copia: quien llama no lo nota.
void reset_first(struct student s) {
s.grades[0] = 0;
printf("En la función: %d\n", s.grades[0]);
}
// Devuelve la copia cambiada: quien llama decide si la guarda.
struct student with_first(struct student s, int grade) {
s.grades[0] = grade;
return s;
}
// Recibe un arreglo: cambia los elementos de quien llama.
void reset_all_first(struct student list[], int n) {
for (int i = 0; i < n; i++) {
list[i].grades[0] = 0;
}
}
void print_student(struct student s) {
printf("%s: %d %d %d\n", s.name, s.grades[0], s.grades[1], s.grades[2]);
}
int main(void) {
struct student a = {"Ana", {14, 9, 17}};
struct student b = a;
b.grades[1] = 20;
print_student(a);
print_student(b);
reset_first(a);
print_student(a);
a = with_first(a, 16);
print_student(a);
struct student list[2] = {a, b};
reset_all_first(list, 2);
print_student(list[0]);
print_student(list[1]);
print_student(a);
return 0;
}// Viajes de una empresa de combis: cuántos viajes y cuántos pasajeros tuvo cada ruta.
//
// Lee de la entrada el nombre de un archivo. Cada línea del archivo es un viaje:
//
// Chosica 18
#include <stdio.h>
#include <string.h>
#define NAME_SIZE 20
#define MAX_ROUTES 8
#define PATH_SIZE 128
#define LINE_SIZE 64
struct route {
char name[NAME_SIZE];
int trips;
int passengers;
};
int remove_newline(char s[]) {
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == '\n') {
s[i] = '\0';
return 1;
}
}
return 0;
}
// Devuelve la posición de la ruta con ese nombre, o -1 si no está.
int find_route(const struct route routes[], int n, const char name[]) {
for (int i = 0; i < n; i++) {
if (strcmp(routes[i].name, name) == 0) {
return i;
}
}
return -1;
}
// Cuenta un viaje. Si la ruta es nueva, la agrega. Devuelve cuántas rutas hay ahora.
int add_trip(struct route routes[], int n, const char name[], int passengers) {
int pos = find_route(routes, n, name);
if (pos == -1) {
if (n == MAX_ROUTES) {
fprintf(stderr, "Demasiadas rutas: se omite %s\n", name);
return n;
}
pos = n;
strcpy(routes[pos].name, name);
routes[pos].trips = 0;
routes[pos].passengers = 0;
n = n + 1;
}
routes[pos].trips = routes[pos].trips + 1;
routes[pos].passengers = routes[pos].passengers + passengers;
return n;
}
// Lee el archivo en routes. Devuelve cuántas rutas hay, o -1 si no pudo abrirlo.
int read_trips(const char path[], struct route routes[]) {
FILE *file = fopen(path, "r");
if (file == NULL) {
perror(path);
return -1;
}
char line[LINE_SIZE];
int number = 0;
int n = 0;
while (fgets(line, LINE_SIZE, file) != NULL) {
number++;
char name[NAME_SIZE];
int passengers;
char extra[2];
if (sscanf(line, "%19s %d %1s", name, &passengers, extra) == 2 && passengers >= 0) {
n = add_trip(routes, n, name, passengers);
} else {
fprintf(stderr, "Línea %d: mal formada, se omite\n", number);
}
}
fclose(file);
return n;
}
double average_passengers(struct route r) {
return (double) r.passengers / r.trips;
}
// Posición de la ruta con más pasajeros, o -1 si no hay rutas.
int busiest_route(const struct route routes[], int n) {
if (n <= 0) {
return -1;
}
int best = 0;
for (int i = 1; i < n; i++) {
if (routes[i].passengers > routes[best].passengers) {
best = i;
}
}
return best;
}
int total_passengers(const struct route routes[], int n) {
int total = 0;
for (int i = 0; i < n; i++) {
total = total + routes[i].passengers;
}
return total;
}
void print_routes(const struct route routes[], int n) {
for (int i = 0; i < n; i++) {
printf("%-8s %d viajes %3d pasajeros %5.2f por viaje\n", routes[i].name, routes[i].trips,
routes[i].passengers, average_passengers(routes[i]));
}
}
int main(void) {
char path[PATH_SIZE];
if (fgets(path, PATH_SIZE, stdin) == NULL) {
fprintf(stderr, "Falta el nombre del archivo\n");
return 1;
}
remove_newline(path);
struct route routes[MAX_ROUTES];
int n = read_trips(path, routes);
if (n < 0) {
return 1;
}
if (n == 0) {
printf("No hay viajes\n");
return 0;
}
print_routes(routes, n);
printf("Más pasajeros: %s\n", routes[busiest_route(routes, n)].name);
printf("Total de pasajeros: %d\n", total_passengers(routes, n));
return 0;
}// Viajes de una empresa de combis: cuántos viajes y cuántos pasajeros tuvo cada ruta.
//
// Lee de la entrada el nombre de un archivo. Cada línea del archivo es un viaje:
//
// Chosica 18
#include <stdio.h>
#include <string.h>
#define NAME_SIZE 20
#define MAX_ROUTES 8
#define PATH_SIZE 128
#define LINE_SIZE 64
struct route {
char name[NAME_SIZE];
int trips;
int passengers;
};
int remove_newline(char s[]) {
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == '\n') {
s[i] = '\0';
return 1;
}
}
return 0;
}
// Devuelve la posición de la ruta con ese nombre, o -1 si no está.
int find_route(const struct route routes[], int n, const char name[]) {
for (int i = 0; i < n; i++) {
if (strcmp(routes[i].name, name) == 0) {
return i;
}
}
return -1;
}
// Cuenta un viaje. Si la ruta es nueva, la agrega. Devuelve cuántas rutas hay ahora.
int add_trip(struct route routes[], int n, const char name[], int passengers) {
int pos = find_route(routes, n, name);
if (pos == -1) {
if (n == MAX_ROUTES) {
fprintf(stderr, "Demasiadas rutas: se omite %s\n", name);
return n;
}
pos = n;
strcpy(routes[pos].name, name);
routes[pos].trips = 0;
routes[pos].passengers = 0;
n = n + 1;
}
routes[pos].trips = routes[pos].trips + 1;
routes[pos].passengers = routes[pos].passengers + passengers;
return n;
}
// Lee el archivo en routes. Devuelve cuántas rutas hay, o -1 si no pudo abrirlo.
int read_trips(const char path[], struct route routes[]) {
FILE *file = fopen(path, "r");
if (file == NULL) {
perror(path);
return -1;
}
char line[LINE_SIZE];
int number = 0;
int n = 0;
while (fgets(line, LINE_SIZE, file) != NULL) {
number++;
char name[NAME_SIZE];
int passengers;
char extra[2];
if (sscanf(line, "%19s %d %1s", name, &passengers, extra) == 2 && passengers >= 0) {
n = add_trip(routes, n, name, passengers);
} else {
fprintf(stderr, "Línea %d: mal formada, se omite\n", number);
}
}
fclose(file);
return n;
}
double average_passengers(struct route r) {
return (double) r.passengers / r.trips;
}
// Posición de la ruta con más pasajeros, o -1 si no hay rutas.
int busiest_route(const struct route routes[], int n) {
if (n <= 0) {
return -1;
}
int best = 0;
for (int i = 1; i < n; i++) {
if (routes[i].passengers > routes[best].passengers) {
best = i;
}
}
return best;
}
void print_routes(const struct route routes[], int n) {
for (int i = 0; i < n; i++) {
printf("%-8s %d viajes %3d pasajeros %5.2f por viaje\n", routes[i].name, routes[i].trips,
routes[i].passengers, average_passengers(routes[i]));
}
}
int main(void) {
char path[PATH_SIZE];
if (fgets(path, PATH_SIZE, stdin) == NULL) {
fprintf(stderr, "Falta el nombre del archivo\n");
return 1;
}
remove_newline(path);
struct route routes[MAX_ROUTES];
int n = read_trips(path, routes);
if (n < 0) {
return 1;
}
if (n == 0) {
printf("No hay viajes\n");
return 0;
}
print_routes(routes, n);
printf("Más pasajeros: %s\n", routes[busiest_route(routes, n)].name);
return 0;
}Chosica 18
Callao 25
Chosica 22
Ate 9
Callao 31
Chosica mucho
Ate 14
Callao 20Errores
Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.
// A propósito: el ciclo se sale del arreglo por una vuelta.
#include <stdio.h>
int main(void) {
int grades[4] = {14, 9, 17, 11};
int total = 0;
for (int i = 0; i <= 4; i++) {
total = total + grades[i];
}
printf("Suma: %d\n", total);
return 0;
}// A propósito: == compara direcciones, no el texto.
#include <stdio.h>
int main(void) {
char a[] = "Ana";
char b[] = "Ana";
if (a == b) {
printf("Iguales\n");
} else {
printf("Distintas\n");
}
return 0;
}// A propósito: strcpy copia 6 bytes en un arreglo de 5.
#include <stdio.h>
#include <string.h>
int main(void) {
char s[5];
char word[] = "hola!";
strcpy(s, word);
printf("%s\n", s);
return 0;
}// A propósito: "hola!" ocupa 6 bytes y el arreglo tiene 5. No queda lugar para el '\0'.
#include <stdio.h>
int main(void) {
char s[5] = "hola!";
printf("%s\n", s);
return 0;
}// A propósito: sizeof de un parámetro arreglo no da el tamaño del arreglo.
#include <stdio.h>
void print_size(int a[]) {
printf("en la función: %zu bytes\n", sizeof(a));
}
int main(void) {
int grades[8] = {14, 9, 17, 11};
printf("en main: %zu bytes\n", sizeof(grades));
print_size(grades);
return 0;
}// Las mismas pruebas de test_trips.c, sobre la versión con el bug.
#define main trips_main
#include "trips_bug.c"
#undef main
#include <assert.h>
// Una ruta nueva empieza con un viaje; una que ya existe acumula.
void test_add_trip(void) {
struct route routes[MAX_ROUTES];
int n = 0;
n = add_trip(routes, n, "Ate", 9);
assert(n == 1);
assert(routes[0].trips == 1);
assert(routes[0].passengers == 9);
n = add_trip(routes, n, "Ate", 14);
assert(n == 1);
assert(routes[0].trips == 2);
assert(routes[0].passengers == 23);
n = add_trip(routes, n, "Callao", 25);
assert(n == 2);
}
// Si la ruta no está, la respuesta es -1, no 0: 0 es una posición válida.
void test_find_route(void) {
struct route routes[MAX_ROUTES];
int n = add_trip(routes, 0, "Ate", 9);
assert(find_route(routes, n, "Ate") == 0);
assert(find_route(routes, n, "Callao") == -1);
}
void test_busiest_route(void) {
struct route routes[MAX_ROUTES];
int n = add_trip(routes, 0, "Ate", 9);
n = add_trip(routes, n, "Callao", 25);
assert(busiest_route(routes, n) == 1);
assert(busiest_route(routes, 0) == -1);
}
// Una ruta con muchos viajes cortos puede ganarle a una con pocos viajes largos.
void test_busiest_counts_all_trips(void) {
struct route routes[MAX_ROUTES];
int n = add_trip(routes, 0, "Callao", 25);
n = add_trip(routes, n, "Ate", 10);
n = add_trip(routes, n, "Ate", 10);
n = add_trip(routes, n, "Ate", 10);
assert(busiest_route(routes, n) == 1);
}
int main(void) {
test_add_trip();
test_find_route();
test_busiest_route();
test_busiest_counts_all_trips();
printf("Todas las pruebas pasan\n");
return 0;
}// A propósito: esta versión de trips.c tiene un bug.
//
// Lee de la entrada el nombre de un archivo. Cada línea del archivo es un viaje:
//
// Chosica 18
#include <stdio.h>
#include <string.h>
#define NAME_SIZE 20
#define MAX_ROUTES 8
#define PATH_SIZE 128
#define LINE_SIZE 64
struct route {
char name[NAME_SIZE];
int trips;
int passengers;
};
int remove_newline(char s[]) {
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == '\n') {
s[i] = '\0';
return 1;
}
}
return 0;
}
// Devuelve la posición de la ruta con ese nombre, o -1 si no está.
int find_route(const struct route routes[], int n, const char name[]) {
for (int i = 0; i < n; i++) {
if (strcmp(routes[i].name, name) == 0) {
return i;
}
}
return -1;
}
// Cuenta un viaje. Si la ruta es nueva, la agrega. Devuelve cuántas rutas hay ahora.
int add_trip(struct route routes[], int n, const char name[], int passengers) {
int pos = find_route(routes, n, name);
if (pos == -1) {
if (n == MAX_ROUTES) {
fprintf(stderr, "Demasiadas rutas: se omite %s\n", name);
return n;
}
pos = n;
strcpy(routes[pos].name, name);
routes[pos].trips = 0;
routes[pos].passengers = 0;
n = n + 1;
}
routes[pos].trips = routes[pos].trips + 1;
routes[pos].passengers = passengers;
return n;
}
// Lee el archivo en routes. Devuelve cuántas rutas hay, o -1 si no pudo abrirlo.
int read_trips(const char path[], struct route routes[]) {
FILE *file = fopen(path, "r");
if (file == NULL) {
perror(path);
return -1;
}
char line[LINE_SIZE];
int number = 0;
int n = 0;
while (fgets(line, LINE_SIZE, file) != NULL) {
number++;
char name[NAME_SIZE];
int passengers;
char extra[2];
if (sscanf(line, "%19s %d %1s", name, &passengers, extra) == 2 && passengers >= 0) {
n = add_trip(routes, n, name, passengers);
} else {
fprintf(stderr, "Línea %d: mal formada, se omite\n", number);
}
}
fclose(file);
return n;
}
double average_passengers(struct route r) {
return (double) r.passengers / r.trips;
}
// Posición de la ruta con más pasajeros, o -1 si no hay rutas.
int busiest_route(const struct route routes[], int n) {
if (n <= 0) {
return -1;
}
int best = 0;
for (int i = 1; i < n; i++) {
if (routes[i].passengers > routes[best].passengers) {
best = i;
}
}
return best;
}
void print_routes(const struct route routes[], int n) {
for (int i = 0; i < n; i++) {
printf("%-8s %d viajes %3d pasajeros %5.2f por viaje\n", routes[i].name, routes[i].trips,
routes[i].passengers, average_passengers(routes[i]));
}
}
int main(void) {
char path[PATH_SIZE];
if (fgets(path, PATH_SIZE, stdin) == NULL) {
fprintf(stderr, "Falta el nombre del archivo\n");
return 1;
}
remove_newline(path);
struct route routes[MAX_ROUTES];
int n = read_trips(path, routes);
if (n < 0) {
return 1;
}
if (n == 0) {
printf("No hay viajes\n");
return 0;
}
print_routes(routes, n);
printf("Más pasajeros: %s\n", routes[busiest_route(routes, n)].name);
return 0;
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
"""Pruebas de los ejemplos de la semana: lo que dicen los apuntes sobre el código."""
import pathlib
import re
import subprocess
import unittest
HERE = pathlib.Path(__file__).parent
FLAGS = ["-Wall", "-Wextra", "-std=c17"]
SANITIZERS = ["-g", "-fsanitize=address,undefined"]
PROGRAMS = ["array_review", "string_review", "struct_review", "file_review", "trips",
"test_trips", "trips_total", "test_trips_total"]
def run(program, stdin=""):
done = subprocess.run(
[str(HERE / "build" / program)], input=stdin, text=True, cwd=HERE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10,
)
return done.stdout, done.stderr, done.returncode
def compile_error_program(name):
"""Compila errors/<name>.c con sanitizers y devuelve lo que dijo gcc."""
built = subprocess.run(
["gcc", *FLAGS, *SANITIZERS, str(HERE / "errors" / f"{name}.c"),
"-o", str(HERE / "build" / f"{name}_san")],
cwd=HERE, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
return built
class Compile(unittest.TestCase):
def test_no_warnings(self):
for program in PROGRAMS:
with self.subTest(program=program):
done = subprocess.run(
["gcc", *FLAGS, "-Werror", "-fsyntax-only", f"{program}.c"], cwd=HERE,
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
self.assertEqual(done.returncode, 0, done.stdout)
def test_no_pointers_beyond_file(self):
# Los punteros se enseñan en la semana 4: aquí solo se declara FILE *.
files = [HERE / f"{name}.c" for name in PROGRAMS] + list(HERE.glob("errors/*.c"))
files += list(HERE.glob("lab/*.c")) + list(HERE.glob("solution/*.c"))
for path in files:
for line in path.read_text(encoding="utf-8").splitlines():
code = line.split("//")[0]
with self.subTest(path=path.name, line=line):
self.assertNotRegex(code, r"\b(?!FILE\b)\w+ \*\w")
self.assertNotIn("malloc", code)
class Examples(unittest.TestCase):
def test_sum_with_part_of_the_array(self):
output, _, status = run("array_review")
self.assertEqual(status, 0)
self.assertIn("Suma: 51\n", output)
self.assertIn("Suma de las 2 primeras: 23\n", output)
self.assertIn("Con un punto más: 55\n", output)
def test_struct_copies(self):
output, _, _ = run("struct_review")
self.assertEqual(output.splitlines()[:5], [
"Ana: 14 9 17", "Ana: 14 20 17", "En la función: 0", "Ana: 14 9 17", "Ana: 16 9 17",
])
def test_file_review_skips_bad_line(self):
output, errors, status = run("file_review", "paraderos.txt\n")
self.assertEqual(status, 0)
self.assertEqual(output, "Personas: 92\n")
self.assertIn("Línea 4", errors)
def test_file_review_missing_file(self):
_, errors, status = run("file_review", "no_hay.txt\n")
self.assertEqual(status, 1)
self.assertIn("no_hay.txt", errors)
def test_trips_by_hand(self):
# Callao: 25 + 31 + 20 = 76. Chosica: 18 + 22 = 40. Ate: 9 + 14 = 23.
output, _, _ = run("trips", "viajes.txt\n")
self.assertRegex(output, r"Chosica\s+2 viajes\s+40 pasajeros")
self.assertRegex(output, r"Callao\s+3 viajes\s+76 pasajeros")
self.assertRegex(output, r"Ate\s+2 viajes\s+23 pasajeros")
self.assertIn("Más pasajeros: Callao", output)
def test_trips_tests_pass(self):
for program in ["test_trips", "test_trips_total"]:
with self.subTest(program=program):
self.assertEqual(run(program)[2], 0)
def test_trips_total_adds_one_line(self):
output, _, _ = run("trips_total", "viajes.txt\n")
self.assertIn("Total de pasajeros: 139", output)
class Errors(unittest.TestCase):
def test_bug_version_differs_in_one_line(self):
# El comentario de la primera línea también cambia.
ours = (HERE / "trips.c").read_text(encoding="utf-8").splitlines()[1:]
bug = (HERE / "errors" / "trips_bug.c").read_text(encoding="utf-8").splitlines()[1:]
self.assertEqual(len(ours), len(bug))
self.assertEqual(sum(a != b for a, b in zip(ours, bug)), 1)
def test_bug_version_fails_its_tests(self):
self.assertEqual(compile_error_program("test_trips_bug").returncode, 0)
_, errors, status = run("test_trips_bug_san")
self.assertNotEqual(status, 0)
self.assertIn("routes[0].passengers == 23", errors)
def test_no_terminator_is_caught_by_asan(self):
built = compile_error_program("no_terminator")
self.assertEqual(built.returncode, 0)
self.assertEqual(built.stdout, "", "los apuntes dicen que esta versión de gcc no avisa")
_, errors, status = run("no_terminator_san")
self.assertNotEqual(status, 0)
self.assertIn("stack-buffer-overflow", errors)
def test_copy_overflow_is_caught_by_asan(self):
built = compile_error_program("copy_overflow")
self.assertEqual(built.returncode, 0)
self.assertEqual(built.stdout, "", "los apuntes dicen que gcc no avisa aquí")
_, errors, status = run("copy_overflow_san")
self.assertNotEqual(status, 0)
self.assertIn("stack-buffer-overflow", errors)
def test_array_overflow_is_caught(self):
built = compile_error_program("array_overflow")
self.assertEqual(built.returncode, 0)
self.assertEqual(built.stdout, "", "los apuntes dicen que compila sin advertencias")
_, errors, status = run("array_overflow_san")
self.assertNotEqual(status, 0)
self.assertTrue(re.search(r"index 4 out of bounds|stack-buffer-overflow", errors), errors)
if __name__ == "__main__":
unittest.main()"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution.
Además de correr sus pruebas en C, llaman a las funciones directamente: cada prueba
compila grades.c 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")
FOLDER = HERE / LAB_DIR
PROGRAM = HERE / "build" / LAB_DIR / "grades"
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]
# Lo que debe salir para cada estudiante válido de notas.txt, sin la última columna.
EXPECTED_ROWS = {
"Ana": ("A Ana 14 9 17 12 13.00 aprobado", 14.33),
"Diego": ("B Diego 12 10 11 11 11.00 aprobado", 11.33),
"Gisela": ("C Gisela 10 11 12 10 10.75 desaprobado", 11.00),
"Bruno": ("A Bruno 8 10 11 9 9.50 desaprobado", 10.00),
"Elena": ("B Elena 20 18 19 17 18.50 aprobado", 19.00),
"Julio": ("C Julio 15 13 13 16 14.25 aprobado", 14.67),
"Fabio": ("B Fabio 5 7 0 6 4.50 desaprobado", 6.00),
"Carla": ("A Carla 11 11 11 11 11.00 aprobado", 11.00),
"Katy": ("C Katy 16 18 20 15 17.25 aprobado", 18.00),
"Luis": ("C Luis 9 13 14 11 11.75 aprobado", 12.67),
}
def run_program(stdin):
done = subprocess.run(
[str(PROGRAM)], input=stdin, text=True, cwd=FOLDER,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10,
)
return done.stdout, done.stderr, done.returncode
def call_with_driver(body):
"""Compila grades.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 "{FOLDER}/grades.c"\n#undef main\n'
f"int main(void) {{\n{body}\n return 0;\n}}\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)], text=True, cwd=FOLDER,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=20,
)
return done.stdout
def student(grades):
values = ", ".join(map(str, grades))
return f"(struct student) {{'A', \"X\", {{{values}}}}}"
def rows(output):
return [line for line in output.splitlines() if re.match(r"^[A-Z] \S", line)]
@unittest.skipUnless(FOLDER.is_dir(), f"no existe {LAB_DIR}/")
class Session1(unittest.TestCase):
def test_no_warnings(self):
for name in ["grades.c", "test_grades.c"]:
with self.subTest(name=name):
done = subprocess.run(
["gcc", "-Wall", "-Wextra", "-std=c17", "-Werror", "-fsyntax-only", name],
cwd=FOLDER, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
self.assertEqual(done.returncode, 0, done.stdout)
def test_their_c_tests_pass(self):
done = subprocess.run(
[str(HERE / "build" / LAB_DIR / "test_grades")], cwd=FOLDER, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=20,
)
self.assertEqual(done.returncode, 0, done.stdout)
def test_average_keeps_decimals(self):
output = call_with_driver(
f'printf("%.2f %.2f\\n", average({student([14, 9, 17, 13])}), average({student([1, 0, 0, 0])}));'
)
self.assertEqual(output.strip(), "13.25 0.25")
def test_each_grade_is_checked(self):
lines = ["A X 21 9 17 12", "A X 14 21 17 12", "A X 14 9 21 12", "A X 14 9 17 21",
"A X -1 9 17 12", "A X 14 -1 17 12", "A X 14 9 -1 12", "A X 14 9 17 -1"]
body = ["struct student list[1];"]
body += [f'printf("%d\\n", parse_student("{line}", list, 0));' for line in lines]
output = call_with_driver("\n".join(body))
self.assertEqual(output.split(), ["0"] * len(lines))
def test_bad_lines_are_reported(self):
_, errors, status = run_program("notas.txt\n")
self.assertEqual(status, 0)
for number in [6, 10, 12]:
self.assertIn(f"Línea {number}: mal formada", errors)
def test_report_values(self):
output, _, _ = run_program("notas.txt\n")
found = rows(output)
self.assertEqual(len(found), len(EXPECTED_ROWS), output)
for line, (name, (prefix, _)) in zip(found, EXPECTED_ROWS.items()):
with self.subTest(name=name):
self.assertTrue(line.startswith(prefix), line)
def test_summary(self):
output, _, _ = run_program("notas.txt\n")
self.assertIn("Estudiantes: 10\n", output)
self.assertIn("Aprobados: 7\n", output)
self.assertIn("Promedio del curso: 12.15\n", output)
self.assertIn("Mejor promedio: Elena (18.50)\n", output)
@unittest.skipUnless(FOLDER.is_dir(), f"no existe {LAB_DIR}/")
class WhatAlreadyWorked(unittest.TestCase):
"""Lo que ya funcionaba en el programa dado debe seguir funcionando."""
def test_missing_file(self):
output, errors, status = run_program("no_existe.txt\n")
self.assertEqual(status, 1)
self.assertEqual(output, "")
self.assertIn("no_existe.txt", errors)
def test_no_input(self):
_, errors, status = run_program("")
self.assertEqual(status, 1)
self.assertIn("Falta el nombre", errors)
def test_empty_file(self):
with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as empty:
pass
output, _, status = run_program(f"{empty.name}\n")
self.assertEqual(status, 0)
self.assertEqual(output, "No hay estudiantes\n")
def test_more_students_than_capacity(self):
with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as many:
for i in range(60):
many.write(f"A X{i} 11 11 11 11\n")
output, errors, status = run_program(f"{many.name}\n")
self.assertEqual(status, 0)
self.assertIn("Estudiantes: 50\n", output)
self.assertIn("Demasiados estudiantes", errors)
def test_long_line_does_not_shift_the_rest(self):
with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as long_line:
long_line.write("A Ana 14 9 17 12\n" + "B " + "x" * 100 + " 1 2 3 4\nC Luis 9 13 14 11\n")
output, errors, _ = run_program(f"{long_line.name}\n")
self.assertIn("Estudiantes: 2\n", output)
self.assertIn("Línea 2: mal formada", errors)
def test_functions_keep_their_parameters(self):
output = call_with_driver(
"""
struct student list[MAX_STUDENTS];
int n = read_students("casos.txt", list, MAX_STUDENTS);
print_report(list, n);
print_summary(list, n);
print_student(list[0]);
printf("%d %d %d\\n", count_passed(list, n), best_index(list, n), is_passed(list[0]));
printf("%.2f %d\\n", class_average(list, n), total_points(list[0]));
"""
)
self.assertIn("Estudiantes:", output)
@unittest.skipUnless(FOLDER.is_dir(), f"no existe {LAB_DIR}/")
class Session2(unittest.TestCase):
def check_without_lowest(self, grades, expected):
output = call_with_driver(
f'printf("%.3f\\n", average_without_lowest({student(grades)}));'
)
self.assertEqual(output.strip(), f"{expected:.3f}")
def test_lowest_in_any_position(self):
self.check_without_lowest([3, 12, 12, 13], 37 / 3)
self.check_without_lowest([12, 3, 12, 13], 37 / 3)
self.check_without_lowest([12, 12, 3, 13], 37 / 3)
self.check_without_lowest([12, 12, 13, 3], 37 / 3)
def test_keeps_decimals(self):
self.check_without_lowest([14, 9, 17, 12], 43 / 3)
def test_tie_removes_only_one(self):
self.check_without_lowest([5, 5, 11, 11], 9.0)
self.check_without_lowest([11, 5, 11, 5], 9.0)
def test_all_equal(self):
self.check_without_lowest([8, 8, 8, 8], 8.0)
self.check_without_lowest([0, 0, 0, 0], 0.0)
def test_they_wrote_tests(self):
text = (FOLDER / "test_grades.c").read_text(encoding="utf-8")
calls = re.findall(r"(?<!test_)average_without_lowest\(", text)
self.assertGreaterEqual(len(calls), 4, "faltan pruebas de average_without_lowest en test_grades.c")
self.assertIn("failed += test_average_without_lowest();", text)
def test_report_has_new_column(self):
output, _, _ = run_program("notas.txt\n")
found = rows(output)
self.assertEqual(len(found), len(EXPECTED_ROWS), output)
for line, (name, (prefix, value)) in zip(found, EXPECTED_ROWS.items()):
with self.subTest(name=name):
self.assertTrue(line.startswith(prefix), line)
self.assertEqual(line.split()[-1], f"{value:.2f}")
def test_filter_by_section(self):
output, _, status = run_program("notas.txt\nB\n")
self.assertEqual(status, 0)
self.assertEqual([line.split()[1] for line in rows(output)], ["Diego", "Elena", "Fabio"])
self.assertIn("Estudiantes: 3\n", output)
self.assertIn("Aprobados: 2\n", output)
self.assertIn("Promedio del curso: 11.33\n", output)
self.assertIn("Mejor promedio: Elena (18.50)\n", output)
def test_filter_another_section(self):
output, _, _ = run_program("notas.txt\nC\n")
self.assertEqual([line.split()[1] for line in rows(output)], ["Gisela", "Julio", "Katy", "Luis"])
self.assertIn("Mejor promedio: Katy (17.25)\n", output)
def test_section_without_students(self):
output, _, status = run_program("notas.txt\nZ\n")
self.assertEqual(status, 0)
self.assertEqual(output, "No hay estudiantes\n")
def test_empty_option_shows_everyone(self):
output, _, status = run_program("notas.txt\n\n")
self.assertEqual(status, 0)
self.assertIn("Estudiantes: 10\n", output)
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-02.md"
SLIDES = ROOT / "slides" / "week-02.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, sin direcciones.
SYSTEM_DEPENDENT = "<!-- depende del sistema -->"
STABLE = ("runtime error", "Assertion", "warning:", "AddressSanitizer", "READ of size",
"WRITE of size", " in main errors/", "<== Memory access")
def normalize(text):
# Según el idioma del entorno, gcc usa comillas tipográficas o rectas.
return text.replace("‘", "'").replace("’", "'")
def without_addresses(text):
text = re.sub(r"0x[0-9a-f]+", "0x", text)
return re.sub(r"==\d+==", "==N==", text)
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, quoted = [], None, "", "", False
for raw in NOTES.read_text(encoding="utf-8").splitlines():
if current is None:
quoted = raw.startswith(">")
line = re.sub(r"^> ?", "", raw)
if line.strip() == "```" + language:
current, mark = [], previous.strip()
elif line.strip():
previous = line
else:
line = re.sub(r"^> ?", "", raw) if quoted else raw
if 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"))
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("$ "):
continue
for command, expected in split_commands(lines):
actual = "\n".join(line.rstrip() for line in run_shell(command).rstrip("\n").split("\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(without_addresses(line), without_addresses(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)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_notes_do_not_show_the_lab_bugs(self):
text = NOTES.read_text(encoding="utf-8")
self.assertNotIn("N_GRADES - 1", text)
self.assertNotIn("total_points(s) / N_GRADES", text)
class SlidesAreTrue(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Una diapositiva corre estos dos sin mostrar cómo se compilan.
for name in ["trips_bug", "test_trips_bug"]:
run_shell(f"gcc -Wall -Wextra -std=c17 -g -fsanitize=address,undefined "
f"errors/{name}.c -o build/{name}")
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_terminal_blocks_in_slides(self):
# Las direcciones cambian de una corrida a otra: se comparan sin ellas.
for block in slide_blocks("terminal|salidalarga|salidamedia"):
for command, expected in split_commands(block):
actual = without_addresses(run_shell(command)).splitlines()
position = 0
# Una diapositiva puede omitir líneas, pero no cambiarlas ni reordenarlas.
for line in expected:
line = without_addresses(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|codigomicro"):
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()// Pruebas de trips_total.c: las de trips.c y una más.
#define main trips_main
#include "trips_total.c"
#undef main
#include <assert.h>
// Una ruta nueva empieza con un viaje; una que ya existe acumula.
void test_add_trip(void) {
struct route routes[MAX_ROUTES];
int n = 0;
n = add_trip(routes, n, "Ate", 9);
assert(n == 1);
assert(routes[0].trips == 1);
assert(routes[0].passengers == 9);
n = add_trip(routes, n, "Ate", 14);
assert(n == 1);
assert(routes[0].trips == 2);
assert(routes[0].passengers == 23);
n = add_trip(routes, n, "Callao", 25);
assert(n == 2);
}
// Si la ruta no está, la respuesta es -1, no 0: 0 es una posición válida.
void test_find_route(void) {
struct route routes[MAX_ROUTES];
int n = add_trip(routes, 0, "Ate", 9);
assert(find_route(routes, n, "Ate") == 0);
assert(find_route(routes, n, "Callao") == -1);
}
void test_busiest_route(void) {
struct route routes[MAX_ROUTES];
int n = add_trip(routes, 0, "Ate", 9);
n = add_trip(routes, n, "Callao", 25);
assert(busiest_route(routes, n) == 1);
assert(busiest_route(routes, 0) == -1);
}
// Una ruta con muchos viajes cortos puede ganarle a una con pocos viajes largos.
void test_busiest_counts_all_trips(void) {
struct route routes[MAX_ROUTES];
int n = add_trip(routes, 0, "Callao", 25);
n = add_trip(routes, n, "Ate", 10);
n = add_trip(routes, n, "Ate", 10);
n = add_trip(routes, n, "Ate", 10);
assert(busiest_route(routes, n) == 1);
}
void test_total_passengers(void) {
struct route routes[MAX_ROUTES];
int n = add_trip(routes, 0, "Ate", 9);
assert(total_passengers(routes, 0) == 0);
n = add_trip(routes, n, "Callao", 25);
n = add_trip(routes, n, "Ate", 14);
assert(total_passengers(routes, n) == 48);
}
int main(void) {
test_add_trip();
test_find_route();
test_busiest_route();
test_busiest_counts_all_trips();
test_total_passengers();
printf("Todas las pruebas pasan\n");
return 0;
}// Pruebas de trips.c, con assert.
#define main trips_main
#include "trips.c"
#undef main
#include <assert.h>
// Una ruta nueva empieza con un viaje; una que ya existe acumula.
void test_add_trip(void) {
struct route routes[MAX_ROUTES];
int n = 0;
n = add_trip(routes, n, "Ate", 9);
assert(n == 1);
assert(routes[0].trips == 1);
assert(routes[0].passengers == 9);
n = add_trip(routes, n, "Ate", 14);
assert(n == 1);
assert(routes[0].trips == 2);
assert(routes[0].passengers == 23);
n = add_trip(routes, n, "Callao", 25);
assert(n == 2);
}
// Si la ruta no está, la respuesta es -1, no 0: 0 es una posición válida.
void test_find_route(void) {
struct route routes[MAX_ROUTES];
int n = add_trip(routes, 0, "Ate", 9);
assert(find_route(routes, n, "Ate") == 0);
assert(find_route(routes, n, "Callao") == -1);
}
void test_busiest_route(void) {
struct route routes[MAX_ROUTES];
int n = add_trip(routes, 0, "Ate", 9);
n = add_trip(routes, n, "Callao", 25);
assert(busiest_route(routes, n) == 1);
assert(busiest_route(routes, 0) == -1);
}
// Una ruta con muchos viajes cortos puede ganarle a una con pocos viajes largos.
void test_busiest_counts_all_trips(void) {
struct route routes[MAX_ROUTES];
int n = add_trip(routes, 0, "Callao", 25);
n = add_trip(routes, n, "Ate", 10);
n = add_trip(routes, n, "Ate", 10);
n = add_trip(routes, n, "Ate", 10);
assert(busiest_route(routes, n) == 1);
}
int main(void) {
test_add_trip();
test_find_route();
test_busiest_route();
test_busiest_counts_all_trips();
printf("Todas las pruebas pasan\n");
return 0;
}Laboratorio
Archivos de partida del laboratorio de la semana.
A Ana 14 9 17 12
B Bruno 10 10 10 10 10
C Carla 11 12
D Diego 12 13 14 21
e Elena 12 13 14 15
F Fabio 0 20 0 20// Notas de un curso: lee un archivo de notas y escribe un reporte y un resumen.
//
// El programa lee de la entrada el nombre del archivo:
//
// printf 'notas.txt\n' | ../build/lab/grades
//
// Cada línea del archivo tiene una sección, un nombre de una palabra y cuatro notas:
//
// A Ana 14 9 17 12
#include <stdio.h>
#define NAME_SIZE 20
#define N_GRADES 4
#define MAX_STUDENTS 50
#define MAX_GRADE 20
#define PASS_GRADE 11
#define PATH_SIZE 128
#define LINE_SIZE 64
#define READ_END 0
#define READ_OK 1
#define READ_TOO_LONG 2
struct student {
char section;
char name[NAME_SIZE];
int grades[N_GRADES];
};
// Quita el salto de línea de s. Devuelve 1 si lo encontró y 0 si no.
int remove_newline(char s[]) {
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == '\n') {
s[i] = '\0';
return 1;
}
}
return 0;
}
// Lee una línea sin su salto. Si no cabe, descarta el resto de la línea.
int read_line(FILE *file, char line[], int size) {
if (fgets(line, size, file) == NULL) {
return READ_END;
}
if (remove_newline(line)) {
return READ_OK;
}
int c = fgetc(file);
if (c == '\n' || c == EOF) {
return READ_OK;
}
while (c != '\n' && c != EOF) {
c = fgetc(file);
}
return READ_TOO_LONG;
}
// Devuelve 1 si todas las notas están entre 0 y MAX_GRADE.
int grades_in_range(struct student s) {
for (int i = 0; i < N_GRADES - 1; i++) {
if (s.grades[i] < 0 || s.grades[i] > MAX_GRADE) {
return 0;
}
}
return 1;
}
// Si la línea es válida, guarda el estudiante en list[pos] y devuelve 1.
int parse_student(const char line[], struct student list[], int pos) {
struct student s;
char extra[2];
int fields = sscanf(line, " %c %19s %d %d %d %d %1s", &s.section, s.name, &s.grades[0],
&s.grades[1], &s.grades[2], &s.grades[3], extra);
if (fields != 2 + N_GRADES) {
return 0;
}
if (s.section < 'A' || s.section > 'Z' || !grades_in_range(s)) {
return 0;
}
list[pos] = s;
return 1;
}
// Lee el archivo en list. Devuelve cuántos estudiantes guardó, o -1 si no pudo abrirlo.
int read_students(const char path[], struct student list[], int capacity) {
FILE *file = fopen(path, "r");
if (file == NULL) {
perror(path);
return -1;
}
char line[LINE_SIZE];
int count = 0;
int number = 0;
int result = read_line(file, line, LINE_SIZE);
while (result != READ_END) {
number++;
if (count == capacity) {
fprintf(stderr, "Demasiados estudiantes: solo se leen %d\n", capacity);
break;
}
if (result == READ_OK && parse_student(line, list, count)) {
count++;
} else {
fprintf(stderr, "Línea %d: mal formada, se omite\n", number);
}
result = read_line(file, line, LINE_SIZE);
}
fclose(file);
return count;
}
int total_points(struct student s) {
int total = 0;
for (int i = 0; i < N_GRADES; i++) {
total = total + s.grades[i];
}
return total;
}
double average(struct student s) {
return (double) (total_points(s) / N_GRADES);
}
// Compara enteros para no depender de la exactitud de un double.
int is_passed(struct student s) {
return total_points(s) >= PASS_GRADE * N_GRADES;
}
// Sesión 2: escríbanla según el README y borren estas dos líneas.
double average_without_lowest(struct student s) {
(void) s;
return -1.0;
}
int count_passed(const struct student list[], int n) {
int count = 0;
for (int i = 0; i < n; i++) {
if (is_passed(list[i])) {
count++;
}
}
return count;
}
// Promedio de los promedios. Con n = 0 devuelve 0.
double class_average(const struct student list[], int n) {
if (n <= 0) {
return 0.0;
}
double total = 0.0;
for (int i = 0; i < n; i++) {
total = total + average(list[i]);
}
return total / n;
}
// Posición del mejor promedio, o -1 si no hay nadie. En un empate gana el primero.
int best_index(const struct student list[], int n) {
if (n <= 0) {
return -1;
}
int best = 0;
for (int i = 1; i < n; i++) {
if (average(list[i]) > average(list[best])) {
best = i;
}
}
return best;
}
void print_student(struct student s) {
printf("%-3c %-12s", s.section, s.name);
for (int i = 0; i < N_GRADES; i++) {
printf(" %2d", s.grades[i]);
}
printf(" %5.2f ", average(s));
if (is_passed(s)) {
printf("%-11s", "aprobado");
} else {
printf("%-11s", "desaprobado");
}
printf("\n");
}
void print_report(const struct student list[], int n) {
printf("Sec Nombre N1 N2 N3 N4 Prom Estado\n");
for (int i = 0; i < n; i++) {
print_student(list[i]);
}
}
void print_summary(const struct student list[], int n) {
int best = best_index(list, n);
printf("Estudiantes: %d\n", n);
printf("Aprobados: %d\n", count_passed(list, n));
printf("Promedio del curso: %.2f\n", class_average(list, n));
printf("Mejor promedio: %s (%.2f)\n", list[best].name, average(list[best]));
}
int main(void) {
char path[PATH_SIZE];
if (read_line(stdin, path, PATH_SIZE) != READ_OK) {
fprintf(stderr, "Falta el nombre del archivo\n");
return 1;
}
struct student list[MAX_STUDENTS];
int n = read_students(path, list, MAX_STUDENTS);
if (n < 0) {
return 1;
}
if (n == 0) {
printf("No hay estudiantes\n");
return 0;
}
print_report(list, n);
print_summary(list, n);
return 0;
}A Ana 14 9 17 12
B Diego 12 10 11 11
C Gisela 10 11 12 10
A Bruno 8 10 11 9
B Elena 20 18 19 17
A Hugo 18 19 20 25
C Julio 15 13 13 16
B Fabio 5 7 0 6
A Carla 11 11 11 11
B Ines 12 x 14 15
C Katy 16 18 20 15
C Rosa 12 -1 14 15
C Luis 9 13 14 11# Laboratorio de la semana 2: leer un programa ajeno y extenderlo
`grades.c` es un programa de unas 200 líneas que lee un archivo de notas y escribe un
reporte y un resumen. No lo escribieron ustedes. Primero lo van a leer, después lo van a
corregir y al final lo van a extender, sin romper lo que ya funciona.
| Archivo | Qué es |
|---|---|
| `grades.c` | El programa |
| `notas.txt` | Datos de ejemplo. Los nombres y las notas son inventados |
| `test_grades.c` | Sus pruebas en C. Se corren con `make test-grades` |
| `casos.txt` | Un archivo pequeño que usa `test_grades.c` |
Todos los comandos se corren desde `code/week-02`, salvo que se indique otra cosa:
```sh
make lab # compila
cd lab && printf 'notas.txt\n' | ../build/lab/grades; cd .. # corre el programa
make test-grades # corre las pruebas en C
make test-lab # corre todas las pruebas del laboratorio
```
`make test-lab` comprueba las dos sesiones, así que falla hasta que terminen la sesión 2.
Sus grupos son `Session1`, `WhatAlreadyWorked` y `Session2`. Para ver uno solo:
```sh
LAB_DIR=lab python3 -m unittest -v test_lab.Session1
```
## Sesión 1: leer y corregir
### Parte 1: leer sin cambiar nada
No cambien nada todavía. Sigan el método de los apuntes: `main` y las firmas, ejecutar con
un ejemplo, seguir un dato de principio a fin, y escribir lo que entendieron. Respondan por
escrito, en un archivo `lab/RESPUESTAS.md`, con sus palabras y sin copiar los comentarios del
código.
1. **La forma de los datos.** ¿Qué tiene cada línea de `notas.txt`? Dibujen un `struct student`
con sus campos y los valores de la línea `A Ana 14 9 17 12`.
2. **Capacidad y longitud.** En `main`, ¿cuántos estudiantes caben en `list` y cuántos tiene
de verdad? ¿Qué constante y qué variable lo dicen?
3. **Qué hace cada función.** Hagan una tabla con una fila por función: su nombre y una sola
frase con lo que hace. Si necesitan la palabra "y", revisen si entendieron bien.
4. **Quién llama a quién.** Dibujen el árbol de llamadas desde `main`, como el de los apuntes.
¿Qué función no llama nadie todavía?
5. **Un dato de principio a fin.** Sigan la línea `A Ana 14 9 17 12` desde el archivo hasta la
fila del reporte. Digan por qué funciones pasa y en qué forma está en cada una: un arreglo de
`char`, un `struct`, un elemento de `list`, texto en la pantalla.
6. **Copia o arreglo.** ¿Qué funciones reciben un `struct student` y cuáles un arreglo? ¿Cuáles
podrían cambiar los datos de quien las llama? ¿Qué dice `const` al respecto?
7. **Dos preguntas de diseño.** ¿Por qué `parse_student` llena una variable local antes de
copiarla a `list[pos]`? ¿Por qué `is_passed` compara enteros en vez de usar `average`?
8. **Lo que dicen las pruebas.** Lean `test_parse_student` en `test_grades.c`. Sin mirar `grades.c`,
digan qué líneas acepta el programa y cuáles no. ¿Qué dice esa prueba que un comentario no
podría garantizar?
9. **La salida.** Ejecuten el programa y miren el reporte con cuidado, comparándolo con
`notas.txt`. Anoten todo lo que les parezca raro, antes de correr las pruebas.
### Parte 2: dos bugs
El programa tiene exactamente dos bugs. Corran `make test-grades`.
1. Antes de abrir `grades.c`, ordenen lo que ven: ¿cuántas comprobaciones fallan? ¿Cuáles podrían
tener la misma causa? Hay más comprobaciones rojas que bugs.
2. Tomen una comprobación que falle, la más pequeña. Léanla: dice qué entrada se dio y qué se
esperaba. Lleguen hasta la función responsable y escriban en `RESPUESTAS.md` su hipótesis
antes de cambiar nada.
3. Corrijan **una sola cosa** y vuelvan a correr las pruebas. Miren qué se puso verde y qué no.
4. Repitan con el otro bug. Cada corrección cabe en una línea: si están cambiando mucho más,
paren y vuelvan a leer.
5. Revisen lo que anotaron en la pregunta 9: ¿qué bug explica cada cosa rara?
Al terminar, `make test-grades` dice `Todas las pruebas pasan`, y también pasa:
```sh
LAB_DIR=lab python3 -m unittest -v test_lab.Session1 test_lab.WhatAlreadyWorked
```
Antes de seguir, guarden una copia del programa corregido, para poder compararla después:
```sh
cp lab/grades.c lab/grades_session1.txt
```
## Sesión 2: extender sin romper
Corran las pruebas antes de empezar. Después de cada cambio, vuelvan a correrlas.
### Parte 3: el promedio sin la nota más baja
En este mismo curso, el promedio de prácticas no cuenta la nota más baja. Agreguen eso al
programa. Escriban el cuerpo de `average_without_lowest`, que ya tiene su firma en `grades.c`:
```c
double average_without_lowest(struct student s);
```
Devuelve el promedio de las notas de `s` sin contar la más baja. Si la nota más baja se repite,
se quita **una sola vez**: las notas `5 5 11 11` dan `(5 + 11 + 11) / 3`, que es 9. El
resultado lleva decimales.
1. **Primero las pruebas.** En `test_grades.c`, escriban `test_average_without_lowest`, llámenla
desde `main` con `failed += test_average_without_lowest();`, y corran `make test-grades`: debe
fallar. Escriban por lo menos cuatro casos, y piensen dónde está la trampa: la nota más baja al
inicio, al final, repetida, y con todas las notas iguales.
2. Escriban la función hasta que sus pruebas pasen.
3. Muéstrenla en el reporte: una columna más al final de cada fila, con dos decimales, y el título
`SinBaja` en el encabezado. Las columnas que ya estaban no cambian:
```text
Sec Nombre N1 N2 N3 N4 Prom Estado SinBaja
A Ana 14 9 17 12 13.00 aprobado 14.33
```
### Parte 4: filtrar por sección
Agreguen una opción: el programa puede leer de la entrada una **segunda línea**, con la letra de
una sección. Si viene, el reporte y el resumen son solo de esa sección. Si no viene, o si la línea
está vacía, salen todos, como hasta ahora. Con una sección que no tiene estudiantes, el programa
escribe `No hay estudiantes`, como con un archivo vacío.
```sh
cd lab && printf 'notas.txt\nB\n' | ../build/lab/grades; cd ..
```
El reto es tocar **la menor cantidad posible de código**. Antes de escribir, respondan en
`RESPUESTAS.md`:
1. Si cada función de reporte y de resumen recibiera la sección como parámetro, ¿cuántas funciones
habría que cambiar? ¿Y cuántas pruebas?
2. ¿Hay un punto del programa por donde pasan todos los datos, antes de llegar a esas funciones?
Las firmas de las funciones que ya existen no deben cambiar: las pruebas las llaman. Agreguen las
funciones que necesiten, con sus pruebas en `test_grades.c`.
Al terminar, comparen con lo que guardaron:
```sh
diff lab/grades_session1.txt lab/grades.c
```
Escriban en `RESPUESTAS.md` cuántas funciones que ya existían tuvieron que cambiar, y por qué. Y
borren `lab/grades_session1.txt`.
`make test-lab` debe pasar completo.// Pruebas de grades.c. Se corren con make test-grades.
//
// Esta prueba usa las funciones de grades.c sin correr su main. Las tres líneas de
// abajo son una receta por ahora: en la semana 6 veremos cómo se separa bien un
// programa en varios archivos.
#define main grades_main
#include "grades.c"
#undef main
// Cada check escribe un aviso si falla y devuelve cuántas fallas hubo: 0 o 1.
int check_int(const char what[], int actual, int expected) {
if (actual != expected) {
printf("FALLA %s: se esperaba %d y salió %d\n", what, expected, actual);
return 1;
}
return 0;
}
int check_double(const char what[], double actual, double expected) {
double difference = actual - expected;
if (difference < -0.001 || difference > 0.001) {
printf("FALLA %s: se esperaba %.3f y salió %.3f\n", what, expected, actual);
return 1;
}
return 0;
}
int test_remove_newline(void) {
int failed = 0;
char with_newline[] = "Ana\n";
failed += check_int("remove_newline: la encuentra", remove_newline(with_newline), 1);
failed += check_int("remove_newline: la quita", with_newline[3], '\0');
char without_newline[] = "Ana";
failed += check_int("remove_newline: no hay", remove_newline(without_newline), 0);
return failed;
}
// Qué líneas son válidas: justo dos campos y cuatro notas entre 0 y 20.
int test_parse_student(void) {
int failed = 0;
struct student list[1];
failed += check_int("parse: línea correcta", parse_student("A Ana 14 9 17 12", list, 0), 1);
failed += check_int("parse: guarda la sección", list[0].section, 'A');
failed += check_int("parse: guarda la última nota", list[0].grades[3], 12);
failed += check_int("parse: falta una nota", parse_student("A Ana 14 9 17", list, 0), 0);
failed += check_int("parse: sobra una nota", parse_student("A Ana 14 9 17 12 5", list, 0), 0);
failed += check_int("parse: nota con letras", parse_student("A Ana 14 x 17 12", list, 0), 0);
failed += check_int("parse: sección en minúscula", parse_student("a Ana 14 9 17 12", list, 0), 0);
failed += check_int("parse: primera nota mayor que 20", parse_student("A Ana 21 9 17 12", list, 0), 0);
failed += check_int("parse: última nota mayor que 20", parse_student("A Ana 14 9 17 21", list, 0), 0);
failed += check_int("parse: última nota negativa", parse_student("A Ana 14 9 17 -1", list, 0), 0);
failed += check_int("parse: 0 y 20 valen", parse_student("A Ana 0 20 0 20", list, 0), 1);
return failed;
}
// casos.txt tiene 6 líneas y solo 2 son válidas: la primera y la última.
// Los avisos de líneas mal formadas que salen aquí son parte de la prueba.
int test_read_students(void) {
int failed = 0;
struct student list[MAX_STUDENTS];
failed += check_int("read_students: cuántos guarda", read_students("casos.txt", list, MAX_STUDENTS), 2);
failed += check_int("read_students: el primero", list[0].section, 'A');
failed += check_int("read_students: el segundo", list[1].section, 'F');
return failed;
}
int test_average(void) {
int failed = 0;
struct student ana = {'A', "Ana", {14, 9, 17, 12}};
struct student eva = {'A', "Eva", {14, 9, 17, 13}};
struct student zero = {'A', "Cero", {0, 0, 0, 0}};
failed += check_int("total_points", total_points(ana), 52);
failed += check_double("average: con decimales", average(eva), 13.25);
failed += check_double("average: exacto", average(ana), 13.0);
failed += check_double("average: todo 0", average(zero), 0.0);
return failed;
}
// Se aprueba con promedio 11 o más: el borde cuenta.
int test_is_passed(void) {
int failed = 0;
struct student exact = {'A', "Justo", {11, 11, 11, 11}};
struct student below = {'A', "Casi", {11, 11, 11, 10}};
failed += check_int("is_passed: justo 11", is_passed(exact), 1);
failed += check_int("is_passed: 10.75", is_passed(below), 0);
return failed;
}
int test_list_functions(void) {
int failed = 0;
struct student list[] = {
{'A', "Ana", {13, 13, 13, 14}},
{'B', "Bruno", {14, 14, 14, 13}},
{'A', "Carla", {5, 5, 5, 5}},
};
failed += check_int("count_passed", count_passed(list, 3), 2);
failed += check_int("count_passed: nadie", count_passed(list, 0), 0);
failed += check_int("best_index: Ana 13.25, Bruno 13.75", best_index(list, 3), 1);
failed += check_int("best_index: sin estudiantes", best_index(list, 0), -1);
failed += check_double("class_average", class_average(list, 3), (13.25 + 13.75 + 5.0) / 3);
failed += check_double("class_average: sin estudiantes", class_average(list, 0), 0.0);
return failed;
}
// Sesión 2: agreguen aquí las pruebas de average_without_lowest.
int main(void) {
int failed = 0;
failed += test_remove_newline();
failed += test_parse_student();
failed += test_read_students();
failed += test_average();
failed += test_is_passed();
failed += test_list_functions();
if (failed == 0) {
printf("Todas las pruebas pasan\n");
return 0;
}
printf("Comprobaciones que fallaron: %d\n", failed);
return 1;
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 2: repaso de datos y lectura de código. 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-grades corre las pruebas en C de su laboratorio (lab/test_grades.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 = array_review string_review struct_review file_review trips test_trips \
trips_total test_trips_total
.PHONY: all lab solution test test-grades test-lab clean
all: $(PROGRAMS:%=$(BUILD)/%)
# Los programas de pruebas incluyen al programa que prueban.
$(BUILD)/test_trips: trips.c
$(BUILD)/test_trips_total: trips_total.c
$(BUILD)/%: %.c | $(BUILD)
$(CC) $(CFLAGS) $< -o $@
$(BUILD)/lab/test_grades: lab/test_grades.c lab/grades.c | $(BUILD)/lab
$(CC) $(CFLAGS) $< -o $@
$(BUILD)/lab/grades: lab/grades.c | $(BUILD)/lab
$(CC) $(CFLAGS) $< -o $@
$(BUILD)/solution/test_grades: solution/test_grades.c solution/grades.c | $(BUILD)/solution
$(CC) $(CFLAGS) $< -o $@
$(BUILD)/solution/grades: solution/grades.c | $(BUILD)/solution
$(CC) $(CFLAGS) $< -o $@
$(BUILD) $(BUILD)/lab $(BUILD)/solution:
mkdir -p $@
lab: $(BUILD)/lab/grades $(BUILD)/lab/test_grades
solution: $(BUILD)/solution/grades $(BUILD)/solution/test_grades
# Si existe solution/, también se comprueba la solución del laboratorio.
test: all
@if [ -d solution ]; then $(MAKE) --no-print-directory solution; fi
python3 -m unittest discover -p 'test_*.py'
test-grades: lab
cd lab && ../$(BUILD)/lab/test_grades
test-lab: all lab
LAB_DIR=lab python3 -m unittest -v test_lab
clean:
rm -rf $(BUILD) __pycache__