Semana 12 — struct y archivos
Objetivos
- Agrupar datos relacionados con
structy usar arreglos destruct. - 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
structy pasarstructa funciones. - Un archivo es una secuencia de bytes con nombre, guardada por el sistema operativo.
fopen,fclose,fgetsyfprintf. RevisarNULLconperror.- Separar campos con
sscanf.
Laboratorio — Procesar un archivo de datos
- Leer el archivo de estudiantes de
code/week-12/en un arreglo destruct. - Escribir en un archivo nuevo a los aprobados con su promedio.
- 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
- ¿Qué ventaja tiene un arreglo de
structfrente a varios arreglos paralelos? - Si una función recibe un
structy cambia un campo, ¿cambia el original? ¿Por qué? - ¿Qué pasa si no se revisa el resultado de
fopeny el archivo no existe? - ¿Qué hace
fgetssi la línea es más larga que el buffer? ¿Cómo lo detectarían? - Diseñen un
structpara 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.
// 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;
}// 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;
}arroz 4.50 2
leche 3.80 6
pan 0.50 10
azúcar 3.20 tres
atún 5.90 3Ana 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 12Ana 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// 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;
}una linea corta
esta linea es bastante mas larga que el buffer
otra corta// 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;
}// 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;
}// 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;
}// 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;
}// 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;
}// 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;
}// 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;
}// 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;
}// 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.
// 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;
}// 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;
}// 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;
}// 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;
}// 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;
}// 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;
}// 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.
"""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()"""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()"""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.
// 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.
# 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__