Semana 12 — struct y archivos

Objetivos

  • Agrupar datos relacionados con struct y usar arreglos de struct.
  • Leer y escribir archivos de texto, revisando los errores.
  • Escribir un programa que procese un archivo de principio a fin.

Teoría — struct y archivos de texto

Lectura: Apuntes, semana 12.

Temas:

  • De los arreglos paralelos a struct. Definir, inicializar y acceder con ..
  • Arreglos de struct y pasar struct a funciones.
  • Un archivo es una secuencia de bytes con nombre, guardada por el sistema operativo.
  • fopen, fclose, fgets y fprintf. Revisar NULL con perror.
  • Separar campos con sscanf.

Laboratorio — Procesar un archivo de datos

  1. Leer el archivo de estudiantes de code/week-12/ en un arreglo de struct.
  2. Escribir en un archivo nuevo a los aprobados con su promedio.
  3. Probar con un archivo inexistente, uno vacío y uno con una línea mal formada. El programa debe avisar sin romperse.

Preguntas de repaso

  1. ¿Qué ventaja tiene un arreglo de struct frente a varios arreglos paralelos?
  2. Si una función recibe un struct y cambia un campo, ¿cambia el original? ¿Por qué?
  3. ¿Qué pasa si no se revisa el resultado de fopen y el archivo no existe?
  4. ¿Qué hace fgets si la línea es más larga que el buffer? ¿Cómo lo detectarían?
  5. Diseñen un struct para un libro de biblioteca. ¿Qué campos y tipos usarían?

Código de la semana

Compilar y probar dentro del contenedor del curso con make y make test.

Ejemplos (17)Errores (7)Pruebas (3)Laboratorio (1)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

append_note.cDescargar append_note.c
// El modo "a" agrega al final, sin borrar lo que ya hay.
#include <stdio.h>

int main(void) {
    FILE *file = fopen("build/nota.txt", "a");
    if (file == NULL) {
        perror("build/nota.txt");
        return 1;
    }
    fprintf(file, "Otra línea\n");
    if (fclose(file) != 0) {
        perror("build/nota.txt");
        return 1;
    }
    return 0;
}
check_students.cDescargar check_students.c
// Parte de las pruebas del laboratorio: test_lab.py llama con esto a sus funciones.
// No hace falta leerlo. Usa argumentos de la línea de comandos, que no vemos en el curso.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define NAME_SIZE 20
#define N_GRADES 3
#define CAPACITY 4
#define READ_END 0
#define READ_OK 1
#define READ_TOO_LONG 2

struct student {
    char name[NAME_SIZE];
    int grades[N_GRADES];
};

int read_line(FILE *file, char line[], int size);
int parse_student(const char line[], struct student list[], int pos);
double average(struct student s);
int is_passed(struct student s);
int read_students(const char path[], struct student list[], int capacity);
int write_passed(const char path[], const struct student list[], int n);

static void print_student(struct student s) {
    printf("%s %d %d %d\n", s.name, s.grades[0], s.grades[1], s.grades[2]);
}

// Escribe qué devuelve read_line con un buffer de 8 bytes, línea por línea.
static int check_read_line(const char path[]) {
    FILE *file = fopen(path, "r");
    if (file == NULL) {
        return 2;
    }
    char line[8];
    for (int guard = 0; guard < 100; guard++) {
        int result = read_line(file, line, 8);
        if (result == READ_END) {
            printf("end\n");
            break;
        }
        printf(result == READ_OK ? "ok \"%s\"\n" : "long\n", line);
    }
    fclose(file);
    return 0;
}

// Lee el archivo varias veces. Las pruebas bajan el límite de archivos abiertos: si
// read_students no cierra el archivo, fopen termina fallando.
static int check_many_reads(const char path[]) {
    struct student list[CAPACITY];
    int total = 0;
    for (int i = 0; i < 200; i++) {
        int n = read_students(path, list, CAPACITY);
        if (n < 0) {
            printf("falló en la lectura %d\n", i);
            return 1;
        }
        total = total + n;
    }
    printf("%d\n", total);
    return 0;
}

// Llama a write_passed y mira el archivo sin terminar el programa. Si write_passed no
// llama a fclose, lo escrito puede no estar todavía en el archivo.
static int check_write(const char path[]) {
    struct student list[3] = {
        {"Ana", {14, 9, 17}},
        {"Bruno", {8, 10, 11}},
        {"Carla", {11, 11, 11}},
    };
    printf("%d\n", write_passed(path, list, 3));
    FILE *file = fopen(path, "r");
    if (file == NULL) {
        return 0;
    }
    char line[64];
    while (fgets(line, 64, file) != NULL) {
        printf("%s", line);
    }
    fclose(file);
    return 0;
}

int main(int argc, char *argv[]) {
    if (argc < 3) {
        return 2;
    }
    const char *op = argv[1];
    if (strcmp(op, "parse") == 0) {
        // Con centinelas alrededor: guardar fuera de list[pos] se nota.
        struct student list[3] = {{"antes", {1, 1, 1}}, {"medio", {2, 2, 2}}, {"despues", {3, 3, 3}}};
        int ok = parse_student(argv[2], list, 1);
        printf("%d\n", ok);
        if (ok) {
            print_student(list[1]);
        }
        if (strcmp(list[0].name, "antes") != 0 || strcmp(list[2].name, "despues") != 0) {
            printf("cambió otra posición\n");
        }
    } else if (strcmp(op, "average") == 0 || strcmp(op, "passed") == 0) {
        struct student s = {"x", {atoi(argv[2]), atoi(argv[3]), atoi(argv[4])}};
        if (op[0] == 'a') {
            printf("%.4f\n", average(s));
        } else {
            printf("%d\n", is_passed(s));
        }
    } else if (strcmp(op, "read") == 0) {
        struct student list[CAPACITY];
        int n = read_students(argv[2], list, CAPACITY);
        printf("%d\n", n);
        for (int i = 0; i < n; i++) {
            print_student(list[i]);
        }
    } else if (strcmp(op, "line") == 0) {
        return check_read_line(argv[2]);
    } else if (strcmp(op, "many") == 0) {
        return check_many_reads(argv[2]);
    } else if (strcmp(op, "write") == 0) {
        return check_write(argv[2]);
    } else {
        return 2;
    }
    return 0;
}
compras.txtDescargar compras.txt
arroz 4.50 2
leche 3.80 6
pan 0.50 10
azúcar 3.20 tres
atún 5.90 3
estudiantes_malos.txtDescargar estudiantes_malos.txt
Ana 14 9 17
Bruno 8 diez 11
Carla 11 11
Diego 12 10 10
Elena 25 18 19
Fabio 5 7 0 3

Gisela 10 11 12
estudiantes_vacio.txtDescargar estudiantes_vacio.txt
estudiantes.txtDescargar estudiantes.txt
Ana 14 9 17
Bruno 8 10 11
Carla 11 11 11
Diego 12 10 10
Elena 20 18 19
Begoña 15 13 13
Fabio 5 7 0
Gisela 10 11 12
first_struct.cDescargar first_struct.c
// Un struct agrupa los datos de un estudiante.
#include <stdio.h>
#include <string.h>

#define NAME_SIZE 20
#define N_GRADES 3

struct student {
    char name[NAME_SIZE];
    int grades[N_GRADES];
};

int main(void) {
    struct student s = {"Ana", {14, 9, 17}};

    printf("%s: %d %d %d\n", s.name, s.grades[0], s.grades[1], s.grades[2]);
    s.grades[1] = 12;
    printf("Después del cambio: %d\n", s.grades[1]);
    strncpy(s.name, "Beatriz", NAME_SIZE - 1);
    s.name[NAME_SIZE - 1] = '\0';
    printf("Nuevo nombre: %s\n", s.name);
    printf("sizeof(struct student) = %zu\n", sizeof(struct student));
    return 0;
}
largas.txtDescargar largas.txt
una linea corta
esta linea es bastante mas larga que el buffer
otra corta
long_line.cDescargar long_line.c
// Lee un archivo con un buffer pequeño y distingue las líneas que no caben.
#include <stdio.h>

#define LINE_SIZE 16
#define READ_END 0
#define READ_OK 1
#define READ_TOO_LONG 2

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 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;
}

int main(void) {
    FILE *file = fopen("largas.txt", "r");
    if (file == NULL) {
        perror("largas.txt");
        return 1;
    }

    char line[LINE_SIZE];
    int number = 0;
    int result = read_line(file, line, LINE_SIZE);
    while (result != READ_END) {
        number++;
        if (result == READ_OK) {
            printf("Línea %d: \"%s\"\n", number, line);
        } else {
            printf("Línea %d: no cabe en %d bytes, se descarta\n", number, LINE_SIZE);
        }
        result = read_line(file, line, LINE_SIZE);
    }
    fclose(file);
    return 0;
}
parse_fields.cDescargar parse_fields.c
// Lo que devuelve sscanf con distintas líneas.
#include <stdio.h>

#define NAME_SIZE 20
#define N_LINES 8

void try_line(const char line[]) {
    char name[NAME_SIZE];
    int a;
    int b;
    int c;
    char extra[2];
    int fields = sscanf(line, "%19s %d %d %d %1s", name, &a, &b, &c, extra);
    printf("%2d  \"%s\"\n", fields, line);
}

int main(void) {
    const char lines[N_LINES][64] = {
        "Ana 14 9 17",
        "  Ana   14 9   17  ",
        "Ana 14 9",
        "Ana diez 9 17",
        "Ana 14.5 9 17",
        "Ana 14 9 17 5",
        "Maria_de_los_Angeles_Quispe 14 9 17",
        "",
    };
    for (int i = 0; i < N_LINES; i++) {
        try_line(lines[i]);
    }
    return 0;
}
receipt.cDescargar receipt.c
// Lee una lista de compras y escribe una boleta. Un programa de principio a fin.
// Cada línea de la lista tiene: producto precio cantidad.
#include <stdio.h>

#define NAME_SIZE 20
#define PATH_SIZE 128
#define LINE_SIZE 64
#define MAX_ITEMS 20
#define READ_END 0
#define READ_OK 1
#define READ_TOO_LONG 2

struct item {
    char name[NAME_SIZE];
    double price;
    int quantity;
};

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 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;
}

// Si la línea es válida, guarda el producto en items[pos] y devuelve 1. Si no, devuelve 0.
int parse_item(const char line[], struct item items[], int pos) {
    struct item it;
    char extra[2];
    int fields = sscanf(line, "%19s %lf %d %1s", it.name, &it.price, &it.quantity, extra);
    if (fields != 3 || it.price < 0 || it.quantity <= 0) {
        return 0;
    }
    items[pos] = it;
    return 1;
}

// Devuelve cuántos productos leyó, o -1 si no pudo abrir el archivo.
int read_items(const char path[], struct item items[], 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 productos: solo se leen %d\n", capacity);
            break;
        }
        if (result == READ_OK && parse_item(line, items, 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;
}

// Escribe la boleta. Devuelve 1 si pudo, 0 si no.
int write_receipt(const char path[], const struct item items[], int n) {
    FILE *file = fopen(path, "w");
    if (file == NULL) {
        perror(path);
        return 0;
    }

    double total = 0.0;
    for (int i = 0; i < n; i++) {
        double subtotal = items[i].price * items[i].quantity;
        fprintf(file, "%s %d x %.2f = %.2f\n", items[i].name, items[i].quantity,
                items[i].price, subtotal);
        total = total + subtotal;
    }
    fprintf(file, "Total: S/ %.2f\n", total);

    if (fclose(file) != 0) {
        perror(path);
        return 0;
    }
    return 1;
}

int main(void) {
    char input[PATH_SIZE];
    char output[PATH_SIZE];
    if (fgets(input, PATH_SIZE, stdin) == NULL || fgets(output, PATH_SIZE, stdin) == NULL) {
        fprintf(stderr, "Faltan los nombres de los archivos\n");
        return 1;
    }
    remove_newline(input);
    remove_newline(output);

    struct item items[MAX_ITEMS];
    int n = read_items(input, items, MAX_ITEMS);
    if (n < 0) {
        return 1;
    }
    if (n == 0) {
        fprintf(stderr, "%s: no hay productos\n", input);
        return 1;
    }
    if (!write_receipt(output, items, n)) {
        return 1;
    }
    printf("Productos: %d\n", n);
    return 0;
}
show_file.cDescargar show_file.c
// Pide el nombre de un archivo y escribe sus líneas numeradas.
#include <stdio.h>

#define NAME_SIZE 128
#define LINE_SIZE 64

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[NAME_SIZE];
    if (fgets(path, NAME_SIZE, stdin) == NULL) {
        printf("Entrada inválida\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;
    while (fgets(line, LINE_SIZE, file) != NULL) {
        remove_newline(line);
        number++;
        printf("%d: %s\n", number, line);
    }
    fclose(file);

    printf("Total: %d líneas\n", number);
    return 0;
}
struct_array.cDescargar struct_array.c
// Un arreglo de struct: cada elemento guarda todos los datos de un estudiante.
#include <stdio.h>

#define NAME_SIZE 20
#define N_GRADES 3
#define MAX_STUDENTS 5

struct student {
    char name[NAME_SIZE];
    int grades[N_GRADES];
};

double average(struct student s) {
    int total = 0;
    for (int i = 0; i < N_GRADES; i++) {
        total = total + s.grades[i];
    }
    return (double) total / N_GRADES;
}

// Devuelve la posición del mejor promedio, o -1 si no hay estudiantes.
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;
}

// Elimina la posición pos y devuelve la longitud nueva, como en la semana 10.
int remove_at(struct student list[], int n, int pos) {
    if (pos < 0 || pos >= n) {
        return n;
    }
    for (int i = pos; i < n - 1; i++) {
        list[i] = list[i + 1];
    }
    return n - 1;
}

// Recibe un arreglo: sí cambia los elementos de quien llama.
void reset_first_grades(struct student list[], int n) {
    for (int i = 0; i < n; i++) {
        list[i].grades[0] = 0;
    }
}

void print_list(const struct student list[], int n) {
    for (int i = 0; i < n; i++) {
        printf("%s: %.2f\n", list[i].name, average(list[i]));
    }
}

int main(void) {
    struct student list[MAX_STUDENTS] = {
        {"Ana", {14, 9, 17}},
        {"Bruno", {8, 10, 11}},
        {"Carla", {11, 11, 11}},
    };
    int n = 3;

    print_list(list, n);
    printf("Mejor: %s\n", list[best_index(list, n)].name);

    n = remove_at(list, n, 1);
    printf("Sin Bruno:\n");
    print_list(list, n);

    reset_first_grades(list, n);
    printf("Con la primera nota en 0:\n");
    print_list(list, n);
    return 0;
}
struct_copy.cDescargar struct_copy.c
// Asignar un struct copia todos sus campos, también los arreglos que tiene dentro.
#include <stdio.h>

#define NAME_SIZE 20
#define N_GRADES 3

struct student {
    char name[NAME_SIZE];
    int grades[N_GRADES];
};

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.name[0] = 'E';
    b.grades[0] = 20;
    print_student(a);
    print_student(b);
    return 0;
}
struct_param.cDescargar struct_param.c
// Un struct llega por valor: la función trabaja sobre una copia.
#include <stdio.h>

#define NAME_SIZE 20
#define N_GRADES 3

struct student {
    char name[NAME_SIZE];
    int grades[N_GRADES];
};

void print_student(struct student s) {
    printf("%s: %d %d %d\n", s.name, s.grades[0], s.grades[1], s.grades[2]);
}

// Cambia su copia. Quien llama no ve nada.
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: llega su dirección, y sí cambia el original.
void reset_first_array(int grades[]) {
    grades[0] = 0;
}

int main(void) {
    struct student a = {"Ana", {14, 9, 17}};

    reset_first(a);
    print_student(a);

    a = with_first(a, 16);
    print_student(a);

    reset_first_array(a.grades);
    print_student(a);
    return 0;
}
struct_size.cDescargar struct_size.c
// El tamaño de un struct puede ser mayor que la suma de sus campos.
#include <stdio.h>

struct mixed {
    char first;
    int number;
    char last;
};

struct sorted {
    int number;
    char first;
    char last;
};

int main(void) {
    printf("sizeof(char) = %zu, sizeof(int) = %zu\n", sizeof(char), sizeof(int));
    printf("sizeof(struct mixed) = %zu\n", sizeof(struct mixed));
    printf("sizeof(struct sorted) = %zu\n", sizeof(struct sorted));
    return 0;
}
write_note.cDescargar write_note.c
// El modo "w" crea el archivo, o lo deja vacío si ya existía.
#include <stdio.h>

int main(void) {
    FILE *file = fopen("build/nota.txt", "w");
    if (file == NULL) {
        perror("build/nota.txt");
        return 1;
    }
    fprintf(file, "Nota final: %d\n", 15);
    if (fclose(file) != 0) {
        perror("build/nota.txt");
        return 1;
    }
    return 0;
}

Errores

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

errors/assign_name.cDescargar errors/assign_name.c
// No compila: un arreglo, como name, no se asigna con =.
#include <stdio.h>

#define NAME_SIZE 20
#define N_GRADES 3

struct student {
    char name[NAME_SIZE];
    int grades[N_GRADES];
};

int main(void) {
    struct student s;

    s.name = "Ana";
    printf("%s\n", s.name);
    return 0;
}
errors/compare_structs.cDescargar errors/compare_structs.c
// No compila: dos struct no se comparan con ==.
#include <stdio.h>

#define NAME_SIZE 20
#define N_GRADES 3

struct student {
    char name[NAME_SIZE];
    int grades[N_GRADES];
};

int main(void) {
    struct student a = {"Ana", {14, 9, 17}};
    struct student b = a;

    if (a == b) {
        printf("Iguales\n");
    }
    return 0;
}
errors/feof_loop.cDescargar errors/feof_loop.c
// Error: feof como condición del ciclo. La última línea sale dos veces.
#include <stdio.h>

#define LINE_SIZE 64

int main(void) {
    FILE *file = fopen("compras.txt", "r");
    if (file == NULL) {
        perror("compras.txt");
        return 1;
    }

    char line[LINE_SIZE];
    while (!feof(file)) {
        fgets(line, LINE_SIZE, file);
        printf("%s", line);
    }
    fclose(file);
    return 0;
}
errors/no_check.cDescargar errors/no_check.c
// Error: no se revisa el resultado de fopen, y el archivo no existe.
#include <stdio.h>

#define LINE_SIZE 64

int main(void) {
    FILE *file = fopen("no_existe.txt", "r");
    char line[LINE_SIZE];

    while (fgets(line, LINE_SIZE, file) != NULL) {
        printf("%s", line);
    }
    fclose(file);
    return 0;
}
errors/no_close.cDescargar errors/no_close.c
// Lo escrito con fprintf puede no estar en el archivo hasta fclose.
#include <stdio.h>

#define LINE_SIZE 64

void look(const char path[]) {
    char line[LINE_SIZE];
    FILE *reader = fopen(path, "r");
    if (reader == NULL) {
        perror(path);
        return;
    }
    if (fgets(line, LINE_SIZE, reader) != NULL) {
        printf("El archivo tiene: %s", line);
    } else {
        printf("El archivo todavía está vacío\n");
    }
    fclose(reader);
}

int main(void) {
    FILE *writer = fopen("build/buffer.txt", "w");
    if (writer == NULL) {
        perror("build/buffer.txt");
        return 1;
    }
    fprintf(writer, "Nota: 15\n");
    look("build/buffer.txt");
    fclose(writer);
    look("build/buffer.txt");
    return 0;
}
errors/no_struct_word.cDescargar errors/no_struct_word.c
// No compila: el nombre del tipo es "struct student", no "student".
#include <stdio.h>

#define NAME_SIZE 20
#define N_GRADES 3

struct student {
    char name[NAME_SIZE];
    int grades[N_GRADES];
};

int main(void) {
    student s;

    printf("%zu\n", sizeof(s));
    return 0;
}
errors/parallel_bug.cDescargar errors/parallel_bug.c
// Arreglos paralelos: se elimina en uno y se olvida el otro. Compila sin advertencias.
#include <stdio.h>

#define NAME_SIZE 20
#define N 3

int main(void) {
    char names[N][NAME_SIZE] = {"Ana", "Bruno", "Carla"};
    int grades[N] = {14, 9, 11};
    int n = N;

    // Bruno se retira: se mueven las notas, y se olvidan los nombres.
    for (int i = 1; i < n - 1; i++) {
        grades[i] = grades[i + 1];
    }
    n = n - 1;

    for (int i = 0; i < n; i++) {
        printf("%s: %d\n", names[i], grades[i]);
    }
    return 0;
}

Pruebas

Comprueban lo que afirman los apuntes; se ejecutan con make test.

test_examples.pyDescargar test_examples.py
"""Comprueba lo que los apuntes afirman sobre los programas de la semana 12."""
import pathlib
import re
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
FLAGS = ["-Wall", "-Wextra", "-std=c17"]
SANITIZE = ["-g", "-fsanitize=address,undefined"]


def execute(command, stdin=""):
    done = subprocess.run(
        command, input=stdin, cwd=HERE, encoding="utf-8", errors="replace",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
    )
    return done.stdout, done.returncode


def run(program, stdin=""):
    """Ejecuta un programa de build/; devuelve su salida (con errores) y su estado."""
    return execute([str(BUILD / program)], stdin)


def build_source(source, *extra):
    """Compila un archivo; devuelve los mensajes, el estado y el ejecutable."""
    out = pathlib.Path(tempfile.mkdtemp()) / pathlib.Path(source).stem
    done = subprocess.run(
        ["gcc", *FLAGS, *extra, source, "-o", str(out)], cwd=HERE, encoding="utf-8",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    return done.stdout.replace("‘", "'").replace("’", "'"), done.returncode, out


class CleanCompile(unittest.TestCase):
    def test_examples_have_no_warnings(self):
        for source in sorted(HERE.glob("*.c")):
            with self.subTest(source=source.name):
                done = subprocess.run(
                    ["gcc", *FLAGS, "-Werror", "-fsyntax-only", str(source)],
                    text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )
                self.assertEqual(done.returncode, 0, done.stdout)

    def test_no_pointer_to_struct_in_the_examples(self):
        # La semana no enseña punteros a struct: todo pasa por valor o en un arreglo.
        for source in list(HERE.glob("*.c")) + list((HERE / "errors").glob("*.c")):
            if source.name == "check_students.c":
                continue
            with self.subTest(source=source.name):
                self.assertNotRegex(source.read_text(encoding="utf-8"), r"struct \w+ \*|->")


class Structs(unittest.TestCase):
    def test_first_struct(self):
        self.assertEqual(run("first_struct")[0],
                         "Ana: 14 9 17\nDespués del cambio: 12\nNuevo nombre: Beatriz\n"
                         "sizeof(struct student) = 32\n")

    def test_assignment_copies_the_array_field(self):
        self.assertEqual(run("struct_copy"), ("Ana: 14 9 17\nEna: 20 9 17\n", 0))

    def test_parameter_is_a_copy_but_array_field_alone_is_not(self):
        self.assertEqual(run("struct_param"),
                         ("En la función: 0\nAna: 14 9 17\nAna: 16 9 17\nAna: 0 9 17\n", 0))

    def test_array_of_structs(self):
        self.assertEqual(run("struct_array"), (
            "Ana: 13.33\nBruno: 9.67\nCarla: 11.00\nMejor: Ana\n"
            "Sin Bruno:\nAna: 13.33\nCarla: 11.00\n"
            "Con la primera nota en 0:\nAna: 8.67\nCarla: 7.33\n", 0))

    def test_padding_may_make_it_bigger(self):
        output, _ = run("struct_size")
        sizes = dict(re.findall(r"sizeof\(struct (\w+)\) = (\d+)", output))
        self.assertGreaterEqual(int(sizes["mixed"]), 6)
        self.assertGreaterEqual(int(sizes["sorted"]), 6)
        # Mismos campos, otro orden: el tamaño puede ser otro.
        self.assertGreaterEqual(int(sizes["mixed"]), int(sizes["sorted"]))

    def test_parallel_arrays_bug_mixes_people(self):
        messages, status, binary = build_source("errors/parallel_bug.c")
        self.assertEqual((messages, status), ("", 0))
        self.assertEqual(execute([str(binary)])[0], "Ana: 14\nBruno: 11\n")

    def test_compile_errors(self):
        cases = {
            "errors/compare_structs.c": "invalid operands to binary ==",
            "errors/assign_name.c": "assignment to expression with array type",
            "errors/no_struct_word.c": "unknown type name 'student'",
        }
        for source, message in cases.items():
            with self.subTest(source=source):
                messages, status, _ = build_source(source)
                self.assertNotEqual(status, 0)
                self.assertIn(message, messages)


class Files(unittest.TestCase):
    def test_data_is_as_the_notes_say(self):
        text = (HERE / "estudiantes.txt").read_text(encoding="utf-8")
        self.assertEqual(len(text.splitlines()), 8)
        self.assertEqual((HERE / "estudiantes_vacio.txt").stat().st_size, 0)

    def test_show_file(self):
        output, status = run("show_file", "estudiantes.txt\n")
        self.assertEqual(status, 0)
        self.assertTrue(output.startswith("1: Ana 14 9 17\n"))
        self.assertTrue(output.endswith("8: Gisela 10 11 12\nTotal: 8 líneas\n"))

    def test_show_empty_file(self):
        self.assertEqual(run("show_file", "estudiantes_vacio.txt\n"), ("Total: 0 líneas\n", 0))

    def test_show_missing_file(self):
        output, status = run("show_file", "no_hay.txt\n")
        self.assertEqual(status, 1)
        self.assertTrue(output.startswith("no_hay.txt: "))

    def test_write_mode_empties_and_append_mode_keeps(self):
        note = BUILD / "nota.txt"
        note.write_text("texto viejo\n", encoding="utf-8")
        self.assertEqual(run("write_note"), ("", 0))
        self.assertEqual(note.read_text(encoding="utf-8"), "Nota final: 15\n")
        self.assertEqual(run("write_note"), ("", 0))
        self.assertEqual(note.read_text(encoding="utf-8"), "Nota final: 15\n")
        self.assertEqual(run("append_note"), ("", 0))
        self.assertEqual(run("append_note"), ("", 0))
        self.assertEqual(note.read_text(encoding="utf-8"),
                         "Nota final: 15\nOtra línea\nOtra línea\n")

    def test_feof_loop_repeats_the_last_line(self):
        messages, status, binary = build_source("errors/feof_loop.c")
        self.assertEqual((messages, status), ("", 0))
        output = execute([str(binary)])[0].splitlines()
        expected = (HERE / "compras.txt").read_text(encoding="utf-8").splitlines()
        self.assertEqual(output, expected + expected[-1:])

    def test_unchecked_fopen_is_caught_with_sanitizers(self):
        messages, status, binary = build_source("errors/no_check.c", *SANITIZE)
        self.assertEqual((messages, status), ("", 0))
        output, code = execute([str(binary)])
        self.assertNotEqual(code, 0)
        self.assertIn("AddressSanitizer: SEGV", output)

    def test_fclose_writes_what_was_pending(self):
        # La primera línea depende de la biblioteca: solo se exige la segunda.
        _, status, binary = build_source("errors/no_close.c")
        self.assertEqual(status, 0)
        output = execute([str(binary)])[0].splitlines()
        self.assertEqual(output[-1], "El archivo tiene: Nota: 15")


class Parsing(unittest.TestCase):
    def test_sscanf_results(self):
        output, status = run("parse_fields")
        self.assertEqual(status, 0)
        results = [int(line.split()[0]) for line in output.splitlines()]
        self.assertEqual(results, [4, 4, 3, 1, 2, 5, 1, -1])

    def test_long_lines(self):
        self.assertEqual(run("long_line"), (
            'Línea 1: "una linea corta"\n'
            "Línea 2: no cabe en 16 bytes, se descarta\n"
            'Línea 3: "otra corta"\n', 0))


class Receipt(unittest.TestCase):
    def test_receipt_of_the_notes(self):
        target = BUILD / "boleta_prueba.txt"
        output, status = run("receipt", f"compras.txt\n{target}\n")
        self.assertEqual((output, status), ("Línea 4: mal formada, se omite\nProductos: 4\n", 0))
        self.assertEqual(target.read_text(encoding="utf-8"), (
            "arroz 2 x 4.50 = 9.00\nleche 6 x 3.80 = 22.80\npan 10 x 0.50 = 5.00\n"
            "atún 3 x 5.90 = 17.70\nTotal: S/ 54.50\n"))

    def test_missing_input_keeps_the_old_receipt(self):
        target = BUILD / "boleta_vieja.txt"
        target.write_text("anterior\n", encoding="utf-8")
        output, status = run("receipt", f"no_hay.txt\n{target}\n")
        self.assertEqual(status, 1)
        self.assertTrue(output.startswith("no_hay.txt: "))
        self.assertEqual(target.read_text(encoding="utf-8"), "anterior\n")


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."""
import os
import pathlib
import resource
import shutil
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
SOURCE = HERE / LAB_DIR / "process_students.c"
CHECK = HERE / "build" / LAB_DIR / "check_students"
PROGRAM = HERE / "build" / LAB_DIR / "process_students"
TOO_MANY = "Demasiados estudiantes: solo se leen 4"


def malformed(number):
    return f"Línea {number}: mal formada, se omite\n"


def few_open_files():
    # Con este límite, un programa que no cierra sus archivos se queda sin poder abrir.
    resource.setrlimit(resource.RLIMIT_NOFILE, (64, 64))


class Base(unittest.TestCase):
    def setUp(self):
        self.tmp = pathlib.Path(tempfile.mkdtemp())
        self.addCleanup(shutil.rmtree, self.tmp, ignore_errors=True)

    def write(self, name, text):
        (self.tmp / name).write_bytes(text.encode("utf-8"))
        return name

    def call(self, *args, limit=False):
        """Llama a una función. Devuelve (salida, errores, estado)."""
        done = subprocess.run(
            [str(CHECK), *args], cwd=self.tmp, encoding="utf-8", errors="replace",
            stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=20,
            preexec_fn=few_open_files if limit else None,
        )
        return done.stdout, done.stderr, done.returncode

    def program(self, stdin):
        """Corre el programa completo dentro de la carpeta temporal."""
        done = subprocess.run(
            [str(PROGRAM)], input=stdin, cwd=self.tmp, encoding="utf-8", errors="replace",
            stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=20,
        )
        return done.stdout, done.stderr, done.returncode

    def output_file(self, name="salida.txt"):
        return (self.tmp / name).read_text(encoding="utf-8")


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe {LAB_DIR}/")
class Functions(Base):
    def test_no_warnings(self):
        done = subprocess.run(
            ["gcc", "-Wall", "-Wextra", "-std=c17", "-Werror", "-fsyntax-only", str(SOURCE)],
            text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
        )
        self.assertEqual(done.returncode, 0, done.stdout)

    def test_read_line(self):
        # El programa de prueba usa un buffer de 8 bytes: caben 7 bytes de texto.
        cases = {
            "Ana\n": 'ok "Ana"\nend\n',
            "Ana": 'ok "Ana"\nend\n',
            "": "end\n",
            "\n": 'ok ""\nend\n',
            "Rosaura\nLi\n": 'ok "Rosaura"\nok "Li"\nend\n',
            "Rosaura": 'ok "Rosaura"\nend\n',
            "Alejandra\nLi\n": 'long\nok "Li"\nend\n',
            "Alejandr\nLi\n": 'long\nok "Li"\nend\n',
            "Alejandra": "long\nend\n",
            "uno\ndos\n": 'ok "uno"\nok "dos"\nend\n',
        }
        for text, expected in cases.items():
            with self.subTest(text=text):
                self.write("datos.txt", text)
                self.assertEqual(self.call("line", "datos.txt"), (expected, "", 0))

    def test_parse_valid_lines(self):
        cases = {
            "Ana 14 9 17": "Ana 14 9 17",
            "  Ana   14 9   17  ": "Ana 14 9 17",
            "Begoña 0 20 10": "Begoña 0 20 10",
            "abcdefghijklmnopqrs 1 2 3": "abcdefghijklmnopqrs 1 2 3",
        }
        for line, expected in cases.items():
            with self.subTest(line=line):
                self.assertEqual(self.call("parse", line), (f"1\n{expected}\n", "", 0))

    def test_parse_invalid_lines(self):
        lines = ["", "   ", "Ana", "Ana 14", "Ana 14 9", "Ana diez 9 17", "Ana 14.5 9 17",
                 "Ana 14 9 17 5", "Ana 14 9 17 x", "Ana 21 9 17", "Ana 14 9 21", "Ana 14 -1 17",
                 "abcdefghijklmnopqrst 1 2 3"]
        for line in lines:
            with self.subTest(line=line):
                self.assertEqual(self.call("parse", line), ("0\n", "", 0))

    def test_average_has_decimals(self):
        cases = {("14", "9", "17"): "13.3333", ("10", "11", "12"): "11.0000",
                 ("0", "0", "1"): "0.3333", ("0", "0", "0"): "0.0000", ("20", "20", "20"): "20.0000"}
        for grades, expected in cases.items():
            with self.subTest(grades=grades):
                self.assertEqual(self.call("average", *grades), (expected + "\n", "", 0))

    def test_is_passed(self):
        cases = {("11", "11", "11"): "1", ("10", "11", "12"): "1", ("20", "13", "0"): "1",
                 ("11", "11", "10"): "0", ("12", "10", "10"): "0", ("0", "0", "0"): "0"}
        for grades, expected in cases.items():
            with self.subTest(grades=grades):
                self.assertEqual(self.call("passed", *grades), (expected + "\n", "", 0))

    def test_read_students(self):
        self.write("datos.txt", "Ana 14 9 17\nBruno 8 10 11\n")
        self.assertEqual(self.call("read", "datos.txt"),
                         ("2\nAna 14 9 17\nBruno 8 10 11\n", "", 0))

    def test_read_last_line_without_newline(self):
        self.write("datos.txt", "Ana 14 9 17\nBruno 8 10 11")
        self.assertEqual(self.call("read", "datos.txt"),
                         ("2\nAna 14 9 17\nBruno 8 10 11\n", "", 0))

    def test_read_empty_file(self):
        self.write("datos.txt", "")
        self.assertEqual(self.call("read", "datos.txt"), ("0\n", "", 0))

    def test_read_missing_file(self):
        output, errors, status = self.call("read", "no_hay.txt")
        self.assertEqual((output, status), ("-1\n", 0))
        self.assertTrue(errors.startswith("no_hay.txt: "), errors)

    def test_read_numbers_the_bad_lines(self):
        self.write("datos.txt", "Ana 14 9 17\nmal\n\nBruno 8 10 11\nCarla 11 11\n")
        self.assertEqual(self.call("read", "datos.txt"), (
            "2\nAna 14 9 17\nBruno 8 10 11\n", malformed(2) + malformed(3) + malformed(5), 0))

    def test_read_long_line_is_one_bad_line(self):
        # Lo que no cupo parece una línea válida: no debe leerse como otro estudiante.
        self.write("datos.txt", "x" * 70 + " Zoila 11 11 11\nAna 14 9 17\nmal\n")
        self.assertEqual(self.call("read", "datos.txt"),
                         ("1\nAna 14 9 17\n", malformed(1) + malformed(3), 0))

    def test_read_long_line_that_would_look_valid_if_cut(self):
        # Los primeros bytes forman una línea válida, pero la línea completa no lo es.
        self.write("datos.txt", "Ana 14 9 17" + " " * 60 + "basura\nBruno 8 10 11\n")
        self.assertEqual(self.call("read", "datos.txt"),
                         ("1\nBruno 8 10 11\n", malformed(1), 0))

    def test_read_exact_capacity_gives_no_warning(self):
        lines = [f"E{i} 11 11 11" for i in range(4)]
        self.write("datos.txt", "\n".join(lines) + "\n")
        self.assertEqual(self.call("read", "datos.txt"),
                         ("4\n" + "\n".join(lines) + "\n", "", 0))

    def test_read_does_not_pass_the_capacity(self):
        lines = [f"E{i} 11 11 11" for i in range(6)]
        self.write("datos.txt", "\n".join(lines) + "\n")
        output, errors, status = self.call("read", "datos.txt")
        self.assertEqual(status, 0, errors)
        self.assertEqual(output, "4\n" + "\n".join(lines[:4]) + "\n")
        self.assertEqual(errors.count(TOO_MANY), 1, errors)

    def test_read_closes_the_file(self):
        self.write("datos.txt", "Ana 14 9 17\n")
        self.assertEqual(self.call("many", "datos.txt", limit=True), ("200\n", "", 0))

    def test_write_closes_and_keeps_only_passed(self):
        self.assertEqual(self.call("write", "salida.txt"),
                         ("2\nAna 13.33\nCarla 11.00\n", "", 0))

    def test_write_replaces_the_old_file(self):
        self.write("salida.txt", "algo viejo\n")
        self.assertEqual(self.call("write", "salida.txt")[0], "2\nAna 13.33\nCarla 11.00\n")

    def test_write_cannot_open(self):
        output, errors, status = self.call("write", "no_hay/salida.txt")
        self.assertEqual((output, status), ("-1\n", 0))
        self.assertTrue(errors.startswith("no_hay/salida.txt: "), errors)


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe {LAB_DIR}/")
class WholeProgram(Base):
    def setUp(self):
        super().setUp()
        for name in ["estudiantes.txt", "estudiantes_vacio.txt", "estudiantes_malos.txt"]:
            shutil.copy(HERE / name, self.tmp / name)

    def test_student_file(self):
        self.assertEqual(self.program("estudiantes.txt\nsalida.txt\n"),
                         ("Leídos: 8. Aprobados: 5.\n", "", 0))
        self.assertEqual(self.output_file(),
                         "Ana 13.33\nCarla 11.00\nElena 19.00\nBegoña 13.67\nGisela 11.00\n")

    def test_file_with_bad_lines(self):
        warnings = "".join(malformed(n) for n in [2, 3, 5, 6, 7])
        self.assertEqual(self.program("estudiantes_malos.txt\nsalida.txt\n"),
                         ("Leídos: 3. Aprobados: 2.\n", warnings, 0))
        self.assertEqual(self.output_file(), "Ana 13.33\nGisela 11.00\n")

    def test_no_one_passed(self):
        self.write("datos.txt", "Ana 1 2 3\n")
        self.assertEqual(self.program("datos.txt\nsalida.txt\n"),
                         ("Leídos: 1. Aprobados: 0.\n", "", 0))
        self.assertEqual(self.output_file(), "")

    def test_more_students_than_capacity(self):
        self.write("datos.txt", "".join(f"E{i} 11 11 11\n" for i in range(60)))
        output, errors, status = self.program("datos.txt\nsalida.txt\n")
        self.assertEqual((output, status), ("Leídos: 50. Aprobados: 50.\n", 0), errors)
        self.assertEqual(errors, "Demasiados estudiantes: solo se leen 50\n")
        self.assertEqual(len(self.output_file().splitlines()), 50)

    def test_missing_file_keeps_the_old_output(self):
        self.write("salida.txt", "resultado anterior\n")
        output, errors, status = self.program("no_hay.txt\nsalida.txt\n")
        self.assertEqual((output, status), ("", 1))
        self.assertTrue(errors.startswith("no_hay.txt: "), errors)
        self.assertEqual(self.output_file(), "resultado anterior\n")

    def test_empty_file_creates_nothing(self):
        self.assertEqual(self.program("estudiantes_vacio.txt\nsalida.txt\n"),
                         ("", "estudiantes_vacio.txt: no hay estudiantes\n", 1))
        self.assertFalse((self.tmp / "salida.txt").exists())

    def test_only_bad_lines(self):
        self.write("datos.txt", "mal\n")
        self.assertEqual(self.program("datos.txt\nsalida.txt\n"),
                         ("", malformed(1) + "datos.txt: no hay estudiantes\n", 1))
        self.assertFalse((self.tmp / "salida.txt").exists())

    def test_output_cannot_be_created(self):
        output, errors, status = self.program("estudiantes.txt\nno_hay/salida.txt\n")
        self.assertEqual((output, status), ("", 1))
        self.assertTrue(errors.startswith("no_hay/salida.txt: "), errors)

    def test_missing_names(self):
        for stdin in ["", "estudiantes.txt\n"]:
            with self.subTest(stdin=stdin):
                self.assertEqual(self.program(stdin),
                                 ("", "Faltan los nombres de los archivos\n", 1))


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-12.md"
SLIDES = ROOT / "slides" / "week-12.tex"
# Antes de un bloque con direcciones o con comportamiento indefinido. De él solo se
# exigen estas cadenas estables, si el bloque las muestra.
MACHINE_DEPENDENT = "<!-- depende de la corrida -->"
STABLE = ["AddressSanitizer: SEGV", "El archivo tiene: Nota: 15"]


def normalize(text):
    # Según el idioma del entorno, gcc usa comillas tipográficas o rectas.
    return text.replace("‘", "'").replace("’", "'")


def run_shell(command):
    done = subprocess.run(
        ["bash", "-c", command], cwd=HERE, encoding="utf-8", errors="replace",
        stdin=subprocess.DEVNULL, 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, depende_de_la_corrida) de cada bloque de los apuntes."""
    blocks, current, flagged, previous = [], None, False, ""
    for raw in NOTES.read_text(encoding="utf-8").splitlines():
        line = re.sub(r"^> ?", "", raw)
        if current is None:
            if line.strip() == "```" + language:
                current, flagged = [], previous.strip() == MACHINE_DEPENDENT
            elif line.strip():
                previous = line
        elif line.strip() == "```":
            blocks.append((current, flagged))
            current = None
        else:
            current.append(line)
    return blocks


def slide_blocks(environments):
    text = SLIDES.read_text(encoding="utf-8")
    pattern = r"\\begin\{(?:%s)\}[^\n]*\n(.*?)\\end\{(?:%s)\}" % (environments, environments)
    return [match.group(1).splitlines() for match in re.finditer(pattern, text, re.S)]


def known_code_lines():
    lines = set()
    for source in list(HERE.glob("*.c")) + list((HERE / "errors").glob("*.c")):
        lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
    return lines


class NotesAreTrue(unittest.TestCase):
    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_terminal_blocks_in_notes(self):
        checked = 0
        for lines, flagged in note_blocks("text"):
            # El bloque del laboratorio no empieza con $: es un comando para copiar.
            if not lines or not lines[0].startswith("$ "):
                continue
            for command, expected in split_commands(lines):
                actual = run_shell(command).rstrip("\n")
                checked += 1
                with self.subTest(command=command):
                    if flagged:
                        for token in STABLE:
                            if any(token in line for line in expected):
                                self.assertIn(token, actual)
                    else:
                        lines = [line.rstrip() for line in actual.split("\n")]
                        self.assertEqual("\n".join(lines), "\n".join(expected))
        self.assertGreater(checked, 25)

    @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_mention_what_depends_on_the_environment(self):
        text = " ".join(NOTES.read_text(encoding="utf-8").split())
        self.assertIn("en esta corrida", text)
        self.assertIn("Los números de arriba valen en este entorno", text)
        self.assertIn("FILE *", text)


class SlidesAreTrue(unittest.TestCase):
    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_terminal_blocks_in_slides(self):
        for block in slide_blocks("terminal|salidalarga"):
            for command, expected in split_commands(block):
                actual = run_shell(command).splitlines()
                position = 0
                # Una diapositiva puede omitir líneas, pero no cambiarlas ni reordenarlas.
                for line in expected:
                    line = line.strip()
                    if not line:
                        continue
                    with self.subTest(command=command, line=line):
                        found = [i for i in range(position, len(actual)) if actual[i].strip() == line]
                        self.assertTrue(found, f"no aparece en la salida real: {line}")
                        if found:
                            position = found[0] + 1

    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_code_blocks_in_slides_exist(self):
        known = known_code_lines()
        for block in slide_blocks("codigo|codigochico|codigomini"):
            for line in block:
                if line.strip() and line.strip() != "...":
                    with self.subTest(line=line):
                        self.assertIn(line.strip(), known)


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

Laboratorio

Archivos de partida del laboratorio de la semana.

lab/process_students.cDescargar lab/process_students.c
// Laboratorio: procesar el archivo de estudiantes de principio a fin.
//
// El programa lee dos líneas de la entrada: el nombre del archivo de estudiantes y el
// nombre del archivo que debe crear. Por ejemplo:
//
//   printf 'estudiantes.txt\nbuild/aprobados.txt\n' | build/lab/process_students
//
// Cada línea del archivo tiene un nombre de una palabra y tres notas enteras:
//
//   Ana 14 9 17
//
// Escriban cada función según su comentario. Borren las líneas (void): solo están para
// que el archivo compile sin advertencias mientras tanto. No cambien el struct ni las
// constantes, ni el nombre o los parámetros de las funciones: las pruebas las llaman.
//
// Pruébenlo con make test-lab.
#include <stdio.h>

#define NAME_SIZE 20
#define N_GRADES 3
#define MAX_STUDENTS 50
#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 name[NAME_SIZE];
    int grades[N_GRADES];
};

// Ya está hecha, y es la de la semana 10.
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 de file en line, sin el salto de línea. Devuelve:
//   READ_END       si no hay más líneas;
//   READ_OK        si la línea cupo, también si es la última y no tiene salto;
//   READ_TOO_LONG  si no cupo. En ese caso descarta el resto, hasta el final de la línea.
// Una línea con size - 1 bytes justos cabe.
int read_line(FILE *file, char line[], int size) {
    (void) file;
    (void) line;
    (void) size;
    return READ_END;
}

// Si la línea es válida, guarda el estudiante en list[pos] y devuelve 1. Si no, devuelve 0.
// Es válida si tiene exactamente un nombre y tres enteros, cada uno entre 0 y 20.
// Una línea con algo de más no es válida. Una línea vacía tampoco.
int parse_student(const char line[], struct student list[], int pos) {
    (void) line;
    (void) list;
    (void) pos;
    return 0;
}

// Devuelve el promedio de las notas, con decimales.
double average(struct student s) {
    (void) s;
    return 0.0;
}

// Devuelve 1 si el promedio es PASS_GRADE o más, y 0 si no.
int is_passed(struct student s) {
    (void) s;
    return 0;
}

// Abre el archivo y guarda sus estudiantes en list. Devuelve cuántos guardó.
//   - Si no puede abrirlo, avisa con perror(path) y devuelve -1.
//   - Por cada línea mal formada o que no cabe, escribe en stderr
//     "Línea N: mal formada, se omite" (la primera línea es la 1) y sigue.
//   - Si hay más líneas que capacity, escribe en stderr
//     "Demasiados estudiantes: solo se leen N", con N igual a capacity, y deja de leer.
//     Con capacity líneas justas, no avisa.
//   - Siempre cierra el archivo.
int read_students(const char path[], struct student list[], int capacity) {
    (void) path;
    (void) list;
    (void) capacity;
    return -1;
}

// Crea el archivo path, con una línea por cada aprobado: el nombre, un espacio y el
// promedio con 2 decimales. Por ejemplo, "Ana 13.33". Devuelve cuántos aprobados escribió.
// Si no puede abrirlo o cerrarlo, avisa con perror(path) y devuelve -1.
int write_passed(const char path[], const struct student list[], int n) {
    (void) path;
    (void) list;
    (void) n;
    return -1;
}

// main debe seguir estos pasos, en este orden:
//   1. Leer los dos nombres. Si falta alguno, escribir en stderr "Faltan los nombres de
//      los archivos" y devolver 1.
//   2. Leer los estudiantes. Si no se pudo abrir el archivo, devolver 1.
//   3. Si no quedó ningún estudiante, escribir en stderr "<archivo>: no hay estudiantes",
//      con el nombre del archivo de entrada, y devolver 1. No se crea el archivo de salida.
//   4. Escribir los aprobados. Si no se pudo, devolver 1.
//   5. Escribir en la salida "Leídos: N. Aprobados: M." y devolver 0.
int main(void) {
    return 1;
}

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 12: struct y archivos. Se usa dentro del entorno del curso.
#
#   make           compila los programas de la clase en build/
#   make test      corre las pruebas de los ejemplos y de los apuntes
#   make test-lab  prueba su laboratorio (lab/process_students.c); falla hasta que lo terminen
#   make clean     borra lo generado
#
# Los programas de errors/ tienen errores a propósito. No se compilan con make:
# los compilan las pruebas, y ustedes, con los comandos de los apuntes.
#
# check_students.c es parte de las pruebas: llama a las funciones de process_students.c.
# Para enlazarlos, el main de process_students.c se compila con otro nombre.

CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
BUILD = build
PROGRAMS = first_struct struct_copy struct_param struct_array struct_size show_file \
           write_note append_note parse_fields long_line receipt

.PHONY: all lab test test-lab clean

all: $(PROGRAMS:%=$(BUILD)/%)

$(BUILD)/%: %.c | $(BUILD)
	$(CC) $(CFLAGS) $< -o $@

$(BUILD) $(BUILD)/lab $(BUILD)/solution:
	mkdir -p $@

$(BUILD)/lab/process_students: lab/process_students.c | $(BUILD)/lab
	$(CC) $(CFLAGS) $< -o $@

$(BUILD)/solution/process_students: solution/process_students.c | $(BUILD)/solution
	$(CC) $(CFLAGS) $< -o $@

$(BUILD)/lab/check_students: check_students.c lab/process_students.c | $(BUILD)/lab
	$(CC) $(CFLAGS) -Dmain=student_main -c lab/process_students.c -o $(BUILD)/lab/process_students.o
	$(CC) $(CFLAGS) check_students.c $(BUILD)/lab/process_students.o -o $@

$(BUILD)/solution/check_students: check_students.c solution/process_students.c | $(BUILD)/solution
	$(CC) $(CFLAGS) -Dmain=student_main -c solution/process_students.c -o $(BUILD)/solution/process_students.o
	$(CC) $(CFLAGS) check_students.c $(BUILD)/solution/process_students.o -o $@

LAB_FILES = process_students check_students

lab: $(LAB_FILES:%=$(BUILD)/lab/%)

# Si existe solution/, también se comprueba la solución del laboratorio.
test: all
	@if [ -d solution ]; then $(MAKE) --no-print-directory \
	  $(LAB_FILES:%=$(BUILD)/solution/%); fi
	python3 -m unittest discover -p 'test_*.py'

test-lab: all lab
	LAB_DIR=lab python3 -m unittest -v test_lab

clean:
	rm -rf $(BUILD) __pycache__