Semana 2 — Repaso: datos y lectura de código

Objetivos

  • Recordar y usar arreglos, cadenas, struct y 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, fgets y fclose, 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 main y 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:

  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.
  2. 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

  1. ¿Qué pasa si el arreglo char s[5] recibe la cadena "hola!"?
  2. ¿Por qué hay que pasar el tamaño de un arreglo a una función?
  3. ¿Por dónde empezarían a leer un programa de 2 000 líneas que no conocen?
  4. ¿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.

array_review.cDescargar array_review.c
// 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;
}
file_review.cDescargar file_review.c
// 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;
}
paraderos.txtDescargar paraderos.txt
Tacna 42
Grau 17
Abancay 8
Wilson x
Bolivia 25
string_review.cDescargar string_review.c
// 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;
}
struct_review.cDescargar struct_review.c
// 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;
}
trips_total.cDescargar trips_total.c
// 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;
}
trips.cDescargar trips.c
// 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;
}
viajes.txtDescargar viajes.txt
Chosica 18
Callao 25
Chosica 22
Ate 9
Callao 31
Chosica mucho
Ate 14
Callao 20

Errores

Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.

errors/array_overflow.cDescargar errors/array_overflow.c
// 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;
}
errors/compare_strings.cDescargar errors/compare_strings.c
// 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;
}
errors/copy_overflow.cDescargar errors/copy_overflow.c
// 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;
}
errors/no_terminator.cDescargar errors/no_terminator.c
// 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;
}
errors/sizeof_param.cDescargar errors/sizeof_param.c
// 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;
}
errors/test_trips_bug.cDescargar errors/test_trips_bug.c
// 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;
}
errors/trips_bug.cDescargar errors/trips_bug.c
// 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.

test_examples.pyDescargar test_examples.py
"""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()
test_lab.pyDescargar test_lab.py
"""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()
test_notes.pyDescargar test_notes.py
"""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()
test_trips_total.cDescargar test_trips_total.c
// 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;
}
test_trips.cDescargar test_trips.c
// 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.

lab/casos.txtDescargar lab/casos.txt
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
lab/grades.cDescargar lab/grades.c
// 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;
}
lab/notas.txtDescargar lab/notas.txt
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
lab/README.mdDescargar lab/README.md
# 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.
lab/test_grades.cDescargar lab/test_grades.c
// 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.

MakefileDescargar Makefile
# 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__