Semana 6 — Programas en varios archivos
Objetivos
- Dividir un programa en módulos con encabezado e implementación.
- Compilar y enlazar varios archivos con
make. - Ocultar una representación con un tipo opaco, y probar un módulo por su interfaz.
Teoría — Varios archivos: encabezados, make, tipos opacos y pruebas por la interfaz
Lectura: Apuntes, semana 6.
Temas:
- Abstracción: usar algo sabiendo qué hace, sin saber cómo. Interfaz e implementación.
- Archivos
.hy.c, guardas de inclusión,staticpara lo privado. Compilar por separado y enlazar. - Un
Makefilecon dependencias: qué se recompila y por qué. - Tipos opacos:
typedef struct stack Stack;en el encabezado y elstructen el.c. Cambiar la representación sin tocar a quien la usa. - Pruebas por la interfaz: las mismas pruebas deben pasar con otra implementación.
- Documentar un módulo: qué promete cada función, qué exige y quién es owner de qué.
Laboratorio — Dividir un programa en módulos con sus pruebas
Sesión 1:
- Leer un programa de un solo archivo y dividirlo en módulos, con un
Makefilepropio. - Escribir un módulo de fracciones con tipo opaco y sus pruebas.
Sesión 2:
3. Dado un módulo Stack con su encabezado y sus pruebas, escribir dos implementaciones: con arreglo que crece y con bloques enlazados. Las mismas pruebas deben pasar en las dos.
4. Convertir el módulo de imágenes a un tipo opaco.
Preguntas de repaso
- ¿Qué error da definir la misma función en dos
.c? ¿Y olvidar el prototipo? - ¿Para qué sirve una guarda de inclusión?
- ¿Por qué un programa que usa un tipo opaco no puede declarar una variable de ese tipo, sino solo un puntero?
- Si cambian un
.h, ¿qué archivos hay que recompilar?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (18)Errores (13)Pruebas (4)Laboratorio (21)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// Implementación del módulo de artículos. Ver item.h.
#include "item.h"
int item_total(const struct item *item) {
return item->quantity * item->unit_cents;
}
int items_total(const struct item items[], int n) {
int total = 0;
for (int i = 0; i < n; i++) {
total += item_total(&items[i]);
}
return total;
}// Módulo de artículos: lo que se compra y cuánto cuesta.
#ifndef ITEM_H
#define ITEM_H
struct item {
const char *name;
int quantity;
int unit_cents;
};
// Devuelve lo que cuesta la línea: cantidad por precio, en céntimos.
int item_total(const struct item *item);
// Devuelve la suma de n artículos, en céntimos. Exige: items tiene n elementos.
int items_total(const struct item items[], int n);
#endif# El ticket de compra: tres módulos y un main.
# Cada .o depende de su .c y de todos los .h que ese .c incluye, directa o indirectamente.
CC = gcc
CFLAGS = -Wall -Wextra -std=c17
BUILD = build
OBJECTS = $(BUILD)/receipt.o $(BUILD)/ticket.o $(BUILD)/item.o $(BUILD)/money.o
$(BUILD)/receipt: $(OBJECTS)
$(CC) $(OBJECTS) -o $@
$(BUILD)/receipt.o: receipt.c ticket.h item.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/ticket.o: ticket.c ticket.h item.h money.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/item.o: item.c item.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/money.o: money.c money.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD):
mkdir -p $@
.PHONY: clean
clean:
rm -rf $(BUILD)// Implementación del módulo de dinero. Ver money.h.
#include "money.h"
#include <stdio.h>
#define CENTS_PER_SOL 100
// Privada: solo este archivo la puede llamar.
static int percent_of(int cents, int percent) {
return cents * percent / 100;
}
int money_discount(int cents, int percent) {
return cents - percent_of(cents, percent);
}
void money_print(int cents) {
printf("S/ %d.%02d", cents / CENTS_PER_SOL, cents % CENTS_PER_SOL);
}// Módulo de dinero: los montos son céntimos de sol, en un int, para no usar decimales.
#ifndef MONEY_H
#define MONEY_H
// Devuelve el monto menos percent por ciento. Los céntimos que sobran de la división no
// se descuentan. Exige: cents >= 0 y 0 <= percent <= 100.
int money_discount(int cents, int percent);
// Escribe el monto en la forma S/ 12.50, sin salto de línea. Exige: cents >= 0.
void money_print(int cents);
#endif// Arma una compra y escribe su ticket. Lo demás lo hacen los módulos.
#include "item.h"
#include "ticket.h"
int main(void) {
const struct item items[] = {
{"Arroz", 2, 450},
{"Aceite", 1, 1290},
{"Huevos", 12, 60},
};
int n = (int) (sizeof(items) / sizeof(items[0]));
ticket_print(items, n, 10);
return 0;
}// Implementación del módulo de tickets. Ver ticket.h.
#include "ticket.h"
#include <stdio.h>
#include "money.h"
static void print_line(const struct item *item) {
printf("%-8s %3d ", item->name, item->quantity);
money_print(item_total(item));
printf("\n");
}
void ticket_print(const struct item items[], int n, int percent) {
for (int i = 0; i < n; i++) {
print_line(&items[i]);
}
int total = items_total(items, n);
printf("Total: ");
money_print(total);
printf("\nCon %d%%: ", percent);
money_print(money_discount(total, percent));
printf("\n");
}// Módulo de tickets: escribe una compra en la terminal.
#ifndef TICKET_H
#define TICKET_H
#include "item.h"
// Escribe una línea por artículo, el total y el total con percent por ciento de descuento.
// Exige: items tiene n elementos y 0 <= percent <= 100. No guarda nada de lo que recibe.
void ticket_print(const struct item items[], int n, int percent);
#endif#include "rectangle.h"
int main(void) {
struct rectangle rectangle = {.width = 3, .height = 5};
rectangle_print(&rectangle);
return 0;
}# ESTE MAKEFILE ESTÁ MAL A PROPÓSITO: los .o no dicen que dependen de rectangle.h.
CC = gcc
CFLAGS = -Wall -Wextra -std=c17
BUILD = build
$(BUILD)/show: $(BUILD)/main.o $(BUILD)/rectangle.o
$(CC) $^ -o $@
$(BUILD)/%.o: %.c | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD):
mkdir -p $@
.PHONY: clean
clean:
rm -rf $(BUILD)// Primera versión. rectangle_v2.h agrega un campo.
#ifndef RECTANGLE_H
#define RECTANGLE_H
struct rectangle {
int width;
int height;
};
void rectangle_print(const struct rectangle *rectangle);
#endif// Segunda versión: se agregó id al inicio.
#ifndef RECTANGLE_H
#define RECTANGLE_H
struct rectangle {
int id;
int width;
int height;
};
void rectangle_print(const struct rectangle *rectangle);
#endif#include "rectangle.h"
#include <stdio.h>
void rectangle_print(const struct rectangle *rectangle) {
printf("%d x %d\n", rectangle->width, rectangle->height);
}// Primera versión. rectangle_v2.h agrega un campo.
#ifndef RECTANGLE_H
#define RECTANGLE_H
struct rectangle {
int width;
int height;
};
void rectangle_print(const struct rectangle *rectangle);
#endif# Una sola prueba y dos implementaciones de la misma interfaz. test_tally.o se compila una
# vez y se enlaza dos: la prueba no sabe cuál de las dos recibe.
CC = gcc
CFLAGS = -Wall -Wextra -std=c17
BUILD = build
PROGRAMS = $(BUILD)/test_array $(BUILD)/test_summary
all: $(PROGRAMS)
$(BUILD)/test_array: $(BUILD)/test_tally.o $(BUILD)/tally_array.o
$(CC) $^ -o $@
$(BUILD)/test_summary: $(BUILD)/test_tally.o $(BUILD)/tally_summary.o
$(CC) $^ -o $@
$(BUILD)/%.o: %.c tally.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD):
mkdir -p $@
.PHONY: all clean
clean:
rm -rf $(BUILD)// Tally que guarda todos los valores, en un arreglo que crece.
#include "tally.h"
#include <stdlib.h>
#define FIRST_CAPACITY 4
struct tally {
int *values;
int count;
int capacity;
};
Tally *tally_new(void) {
Tally *tally = malloc(sizeof(*tally));
if (tally == NULL) {
return NULL;
}
tally->values = malloc(FIRST_CAPACITY * sizeof(int));
if (tally->values == NULL) {
free(tally);
return NULL;
}
tally->count = 0;
tally->capacity = FIRST_CAPACITY;
return tally;
}
void tally_free(Tally *tally) {
if (tally != NULL) {
free(tally->values);
free(tally);
}
}
int tally_add(Tally *tally, int value) {
if (tally->count == tally->capacity) {
int *grown = realloc(tally->values, 2 * tally->capacity * sizeof(int));
if (grown == NULL) {
return 0;
}
tally->values = grown;
tally->capacity *= 2;
}
tally->values[tally->count] = value;
tally->count++;
return 1;
}
int tally_count(const Tally *tally) {
return tally->count;
}
int tally_sum(const Tally *tally) {
int sum = 0;
for (int i = 0; i < tally->count; i++) {
sum += tally->values[i];
}
return sum;
}
int tally_max(const Tally *tally) {
int max = tally->values[0];
for (int i = 1; i < tally->count; i++) {
if (tally->values[i] > max) {
max = tally->values[i];
}
}
return max;
}// Tally que no guarda los valores: solo lleva el resumen.
#include "tally.h"
#include <stdlib.h>
struct tally {
int count;
int sum;
int max;
};
Tally *tally_new(void) {
Tally *tally = malloc(sizeof(*tally));
if (tally == NULL) {
return NULL;
}
tally->count = 0;
tally->sum = 0;
tally->max = 0;
return tally;
}
void tally_free(Tally *tally) {
free(tally);
}
int tally_add(Tally *tally, int value) {
if (tally->count == 0 || value > tally->max) {
tally->max = value;
}
tally->count++;
tally->sum += value;
return 1;
}
int tally_count(const Tally *tally) {
return tally->count;
}
int tally_sum(const Tally *tally) {
return tally->sum;
}
int tally_max(const Tally *tally) {
return tally->max;
}// Módulo Tally: acumula enteros y responde cuántos son, cuánto suman y cuál es el mayor.
// Es un tipo opaco: quien lo usa solo tiene punteros y solo llama a estas funciones.
#ifndef TALLY_H
#define TALLY_H
typedef struct tally Tally;
// Crea un acumulador vacío. Devuelve NULL si no hay memoria.
// Owner: quien llama, que debe liberarlo con tally_free.
Tally *tally_new(void);
// Libera el acumulador. Acepta NULL. Después de esto, tally ya no se puede usar.
void tally_free(Tally *tally);
// Agrega un valor. Devuelve 1 si pudo y 0 si no hay memoria; en ese caso, no cambia nada.
// Exige: la suma de todos los valores cabe en un int.
int tally_add(Tally *tally, int value);
// Devuelve cuántos valores se agregaron.
int tally_count(const Tally *tally);
// Devuelve la suma de los valores. Con cero valores, es 0.
int tally_sum(const Tally *tally);
// Devuelve el mayor valor. Exige: tally_count(tally) > 0.
int tally_max(const Tally *tally);
#endifErrores
Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.
// Error a propósito: percent_of es static en receipt/money.c.
#include <stdio.h>
int percent_of(int cents, int percent);
int main(void) {
printf("%d\n", percent_of(1000, 10));
return 0;
}// Error a propósito: alguien cambió el tipo aquí y olvidó el encabezado. Como este archivo
// incluye su propio encabezado, el compilador lo nota.
#include "drift.h"
double area(int width, int height) {
return 1.0 * width * height;
}// Dice que el área es un int.
#ifndef DRIFT_H
#define DRIFT_H
int area(int width, int height);
#endif// Error a propósito: money_print ya está definida en receipt/money.c.
#include <stdio.h>
void money_print(int cents) {
printf("%d centimos", cents);
}// Error a propósito: usa money_print sin incluir money.h.
int main(void) {
money_print(1250);
return 0;
}all:
gcc -Wall -Wextra -std=c17 hola.c -o hola// Error a propósito: quiere saber más de lo que dice el encabezado.
#include <stdio.h>
#include "tally.h"
int main(void) {
Tally local;
Tally *tally = tally_new();
printf("%d\n", tally->count);
printf("%zu\n", sizeof(Tally));
tally_free(tally);
return 0;
}#include "twice.h"
int first(void) {
return twice(1);
}#include <stdio.h>
#include "twice.h"
int first(void);
int main(void) {
printf("%d %d\n", first(), twice(2));
return 0;
}// Error a propósito: un encabezado con el cuerpo de una función, no solo su prototipo.
#ifndef TWICE_H
#define TWICE_H
int twice(int n) {
return 2 * n;
}
#endif// Quien escribió este archivo usa los dos tipos, y por eso incluye los dos encabezados.
#include "point.h"
#include "segment.h"
int main(void) {
struct segment segment = {{0, 0}, {3, 4}};
struct point end = segment.to;
return end.x - 3;
}// Error a propósito: este encabezado no tiene guarda de inclusión.
struct point {
int x;
int y;
};
int point_sum(struct point p);// Error a propósito: este encabezado no tiene guarda de inclusión.
#include "point.h"
struct segment {
struct point from;
struct point to;
};Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
"""Comprueba los ejemplos de los apuntes, los errores a propósito y que el laboratorio
de partida compile sin advertencias."""
import os
import pathlib
import shutil
import subprocess
import tempfile
import unittest
HERE = pathlib.Path(__file__).parent
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]
CLEAN_ENV = {k: v for k, v in os.environ.items() if k not in ("MAKELEVEL", "MAKEFLAGS", "MFLAGS")}
def shell(args, cwd=HERE):
done = subprocess.run(
args, cwd=cwd, env=CLEAN_ENV, text=True, encoding="utf-8",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
)
return done.stdout.replace("‘", "'").replace("’", "'").replace("`", "'"), done.returncode
def build_and_run(sources, folder=HERE, cwd=None):
"""Compila con sanitizers y sin advertencias, y corre. ASan avisa si queda una fuga."""
with tempfile.TemporaryDirectory() as name:
binary = pathlib.Path(name) / "program"
out, code = shell(["gcc", *FLAGS, "-Werror", *sources, "-o", str(binary)], folder)
if code != 0:
raise AssertionError(f"No compila:\n{out}")
return shell([str(binary)], cwd or folder)
def compile_only(source, folder=HERE, *flags):
with tempfile.TemporaryDirectory() as name:
target = str(pathlib.Path(name) / "out.o")
return shell(["gcc", "-Wall", "-Wextra", "-std=c17", *flags, "-c", source, "-o", target], folder)
class Examples(unittest.TestCase):
def test_receipt(self):
sources = ["receipt.c", "ticket.c", "item.c", "money.c"]
out, code = build_and_run(sources, HERE / "receipt")
self.assertEqual(code, 0, out)
self.assertEqual(out.splitlines()[-2:], ["Total: S/ 29.10", "Con 10%: S/ 26.19"])
def test_tally_passes_with_both_implementations_and_no_leaks(self):
for name in ("tally_array.c", "tally_summary.c"):
with self.subTest(implementation=name):
out, code = build_and_run(["test_tally.c", name], HERE / "tally")
self.assertEqual(code, 0, out)
self.assertEqual(out.strip(), "Todas las pruebas pasaron")
def test_private_function_stays_private(self):
with tempfile.TemporaryDirectory() as name:
target = str(pathlib.Path(name) / "money.o")
shell(["gcc", "-std=c17", "-c", "money.c", "-o", target], HERE / "receipt")
out, _ = shell(["nm", target])
self.assertRegex(out, r"\st percent_of$|\st percent_of\n")
self.assertRegex(out, r"\sT money_print")
def test_no_test_file_includes_a_c_file(self):
for source in list(HERE.rglob("test_*.c")):
if "build" not in source.parts:
with self.subTest(file=str(source.relative_to(HERE))):
self.assertNotRegex(source.read_text(encoding="utf-8"), r'#include\s+"[^"]*\.c"')
def test_every_real_header_has_a_guard(self):
for source in HERE.rglob("*.h"):
parts = source.relative_to(HERE).parts
if "no_guard" in parts or "build" in parts:
continue
if parts[:2] == ("lab", "split"):
continue
with self.subTest(file=str(source.relative_to(HERE))):
self.assertRegex(source.read_text(encoding="utf-8"), r"#ifndef \w+_H\n#define \w+_H")
class DeliberateErrors(unittest.TestCase):
def assert_compile_error(self, source, message, *flags):
out, code = compile_only(source, HERE, *flags)
self.assertNotEqual(code, 0, f"{source} debería fallar")
self.assertIn(message, out)
def test_no_prototype(self):
self.assert_compile_error("errors/no_prototype.c", "implicit declaration of function 'money_print'")
def test_no_guard(self):
self.assert_compile_error("errors/no_guard/main.c", "redefinition of 'struct point'")
def test_drift(self):
self.assert_compile_error("errors/drift.c", "conflicting types for 'area'")
def test_peek_at_opaque_type(self):
out, code = compile_only("errors/peek_tally.c", HERE, "-Itally")
self.assertNotEqual(code, 0)
self.assertIn("storage size of 'local' isn't known", out)
self.assertIn("invalid use of incomplete typedef 'Tally'", out)
self.assertIn("invalid application of 'sizeof' to incomplete type", out)
def link_error(self, sources, flags=()):
with tempfile.TemporaryDirectory() as name:
return shell(["gcc", "-std=c17", *flags, *sources, "-o", str(pathlib.Path(name) / "program")])
def test_same_function_in_two_files(self):
out, code = self.link_error(["receipt/receipt.c", "receipt/ticket.c", "receipt/item.c",
"receipt/money.c", "errors/money_twin.c"])
self.assertNotEqual(code, 0)
self.assertIn("multiple definition of 'money_print'", out)
def test_call_private(self):
out, code = self.link_error(["errors/call_private.c", "receipt/money.c"])
self.assertNotEqual(code, 0)
self.assertIn("undefined reference to 'percent_of'", out)
def test_body_in_header(self):
out, code = self.link_error(["errors/header_body/first.c", "errors/header_body/second.c"])
self.assertNotEqual(code, 0)
self.assertIn("multiple definition of 'twice'", out)
class Starter(unittest.TestCase):
def test_split_program_works_in_one_file(self):
out, code = build_and_run(["market.c"], HERE / "lab" / "split")
self.assertEqual(code, 0, out)
self.assertIn("Total: S/ 118.60", out)
self.assertIn("La mejor venta: papa", out)
def test_stubs_compile_without_warnings(self):
for folder, source in (("fraction", "fraction.c"), ("fraction", "test_fraction.c"),
("stack", "stack_array.c"), ("stack", "stack_blocks.c"),
("stack", "test_stack.c"), ("image", "image.c"),
("image", "filters.c"), ("image", "filter_demo.c")):
with self.subTest(file=f"{folder}/{source}"):
out, code = compile_only(source, HERE / "lab" / folder, "-Werror")
self.assertEqual(code, 0, out)
def test_transparent_image_module_runs(self):
with tempfile.TemporaryDirectory() as name:
copy = pathlib.Path(name) / "image"
shutil.copytree(HERE / "lab" / "image", copy, ignore=shutil.ignore_patterns("build"))
out, code = shell(["make"], copy)
self.assertEqual(code, 0, out)
self.assertNotIn("warning:", out)
out, code = shell(["build/filter_demo"], copy)
self.assertEqual(code, 0, out)
self.assertIn("Tamaño: 32 x 16", out)
if __name__ == "__main__":
unittest.main()"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution.
Cada prueba copia la parte a una carpeta temporal, así que no toca sus archivos. Con
LAB_DIR=solution, los archivos de solution/ se copian encima de los de lab/.
"""
import os
import pathlib
import re
import shutil
import subprocess
import tempfile
import time
import unittest
HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]
# Un make corrido dentro de otro make hereda estas variables y escribe otras cosas.
CLEAN_ENV = {k: v for k, v in os.environ.items() if k not in ("MAKELEVEL", "MAKEFLAGS", "MFLAGS")}
PIXELS = "#include <stdio.h>\n#include \"filters.h\"\n"
def shell(args, cwd):
done = subprocess.run(
args, cwd=cwd, env=CLEAN_ENV, text=True, encoding="utf-8", errors="replace",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
)
return done.stdout, done.returncode
class Workspace:
"""Una copia temporal de una parte del laboratorio."""
def __init__(self, part):
self.temp = tempfile.TemporaryDirectory()
self.path = pathlib.Path(self.temp.name) / part
shutil.copytree(HERE / "lab" / part, self.path, ignore=shutil.ignore_patterns("build"))
if LAB_DIR != "lab":
shutil.copytree(HERE / LAB_DIR / part, self.path, dirs_exist_ok=True)
def close(self):
self.temp.cleanup()
def compile_only(self, source):
(self.path / "probe.c").write_text(source, encoding="utf-8")
return shell(["gcc", *FLAGS, "-c", "probe.c", "-o", "probe.o"], self.path)
def run_driver(self, source, sources):
"""Compila un main propio con los .c indicados y lo corre. Falla si no compila."""
(self.path / "driver.c").write_text(source, encoding="utf-8")
out, code = shell(["gcc", *FLAGS, "driver.c", *sources, "-o", "driver"], self.path)
if code != 0:
raise AssertionError(f"No compila:\n{out}")
return shell(["./driver"], self.path)
class PartTest(unittest.TestCase):
part = ""
def setUp(self):
if not (HERE / LAB_DIR).exists():
self.skipTest(f"no está {LAB_DIR}/")
self.work = Workspace(self.part)
self.addCleanup(self.work.close)
def read(self, name):
path = self.work.path / name
if not path.exists():
self.fail(f"Falta el archivo {self.part}/{name}")
return path.read_text(encoding="utf-8")
def check_driver(self, body, sources, wanted=None):
out, code = self.work.run_driver(body, sources)
if code != 0:
self.fail(f"El programa falló, o dejó una fuga de memoria:\n{out}")
if wanted is not None and out.strip() != wanted:
self.fail(f"Se esperaba:\n{wanted}\nSalió:\n{out.strip()}")
class SplitProgram(PartTest):
part = "split"
INTERFACE = {
"sales.o": {"load_sales", "sale_total", "sales_total", "best_sale"},
"report.o": {"print_table", "print_summary"},
}
def make(self, *args):
if not (self.work.path / "Makefile").exists():
self.fail("Falta su lab/split/Makefile")
return shell(["make", *args], self.work.path)
def build(self):
out, code = self.make()
if code != 0:
self.fail(f"make falló:\n{out}")
return out
def recompiled_after_touching(self, name):
self.build()
time.sleep(0.02)
(self.work.path / name).touch()
out, code = self.make("-n")
if code != 0:
self.fail(f"make -n falló:\n{out}")
# Vale cualquier orden de las opciones: -c -o x.o x.c o -c x.c -o x.o.
compiled = set()
for line in out.splitlines():
tokens = line.split()
if "-c" in tokens:
compiled.update(pathlib.Path(token).name for token in tokens)
return compiled & {"main.c", "sales.c", "report.c"}
def test_builds_without_warnings_and_with_the_flags(self):
out = self.build()
self.assertNotIn("warning:", out, "Hay advertencias al compilar")
for flag in ("-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"):
self.assertIn(flag, out, f"El Makefile no usa {flag}")
def test_same_output_as_the_single_file(self):
self.build()
original, code = shell(["gcc", *FLAGS, "market.c", "-o", "original"], self.work.path)
self.assertEqual(code, 0, original)
expected, _ = shell(["./original"], self.work.path)
out, code = shell(["build/market"], self.work.path)
self.assertEqual(code, 0, f"build/market falló:\n{out}")
if out != expected:
self.fail(f"La salida cambió. Se esperaba:\n{expected}\nSalió:\n{out}")
def test_no_c_file_is_included(self):
for name in ("main.c", "sales.c", "report.c", "sales.h", "report.h"):
with self.subTest(file=name):
self.assertNotRegex(self.read(name), r'#include\s+"[^"]*\.c"',
f"{name} incluye un .c: cada .c se compila por separado")
def test_guards(self):
source = '#include "sales.h"\n#include "report.h"\n#include "sales.h"\n#include "report.h"\n'
self.read("sales.h")
self.read("report.h")
out, code = self.work.compile_only(source)
self.assertEqual(code, 0, f"Incluir dos veces los encabezados no compila:\n{out}")
def test_only_the_interface_is_public(self):
self.build()
for obj, names in self.INTERFACE.items():
with self.subTest(object=obj):
out, code = shell(["nm", f"build/{obj}"], self.work.path)
self.assertEqual(code, 0, f"No está build/{obj}")
public = set(re.findall(r"\sT (\w+)$", out, re.M))
self.assertEqual(public, names,
f"{obj} debe ofrecer solo {sorted(names)}. Ofrece {sorted(public)}")
def test_sales_does_not_need_report(self):
self.read("sales.h")
body = ('#include <stdio.h>\n#include "sales.h"\n'
"int main(void) {\n struct sale sales[10];\n"
' int n = load_sales("ventas.txt", sales, 10);\n'
' printf("%d %d %d\\n", n, sales_total(sales, n), best_sale(sales, n));\n'
" return 0;\n}\n")
self.check_driver(body, ["sales.c"], "5 11860 0")
def test_touching_a_c_file_recompiles_only_that_file(self):
self.assertEqual(self.recompiled_after_touching("report.c"), {"report.c"})
def test_touching_sales_h_recompiles_everything(self):
self.assertEqual(self.recompiled_after_touching("sales.h"),
{"main.c", "sales.c", "report.c"})
def test_touching_report_h_does_not_recompile_sales(self):
self.assertEqual(self.recompiled_after_touching("report.h"), {"main.c", "report.c"})
def test_clean(self):
self.build()
self.make("clean")
self.assertFalse((self.work.path / "build").exists(), "make clean no borró build/")
class FractionModule(PartTest):
part = "fraction"
def show(self, code, wanted):
source = ('#include <stdio.h>\n#include "fraction.h"\n'
"static void show(Fraction *f) {\n"
' if (f == NULL) { printf("NULL\\n"); return; }\n'
' printf("%d %d\\n", fraction_numerator(f), fraction_denominator(f));\n'
" fraction_free(f);\n}\n"
f"int main(void) {{\n{code}\n return 0;\n}}\n")
self.check_driver(source, ["fraction.c"], wanted)
def test_new_reduces(self):
self.show("show(fraction_new(6, 8));", "3 4")
def test_the_sign_goes_in_the_numerator(self):
self.show("show(fraction_new(2, -4)); show(fraction_new(-2, -4));", "-1 2\n1 2")
def test_zero(self):
self.show("show(fraction_new(0, 5)); show(fraction_new(0, -5));", "0 1\n0 1")
def test_zero_denominator(self):
self.show("show(fraction_new(1, 0));", "NULL")
def binary(self, call, wanted):
self.show("Fraction *a = fraction_new(1, 2);\nFraction *b = fraction_new(-3, 4);\n"
f"show({call});\nshow(a);\nshow(b);", wanted + "\n1 2\n-3 4")
def test_add(self):
self.binary("fraction_add(a, b)", "-1 4")
def test_add_reduces(self):
self.show("Fraction *a = fraction_new(1, 6);\nFraction *b = fraction_new(1, 3);\n"
"show(fraction_add(a, b));\nshow(a);\nshow(b);", "1 2\n1 6\n1 3")
def test_multiply(self):
self.binary("fraction_multiply(a, b)", "-3 8")
def test_equal(self):
self.show('Fraction *a = fraction_new(1, 2);\nFraction *b = fraction_new(3, 6);\n'
'Fraction *c = fraction_new(1, 3);\n'
'printf("%d %d\\n", fraction_equal(a, b) != 0, fraction_equal(a, c) != 0);\n'
'show(a);\nshow(b);\nshow(c);', "1 0\n1 2\n1 2\n1 3")
def test_print(self):
self.show('Fraction *a = fraction_new(3, 4);\nFraction *b = fraction_new(4, 2);\n'
'Fraction *c = fraction_new(-1, 2);\n'
'fraction_print(a); printf(" "); fraction_print(b); printf(" ");\n'
'fraction_print(c); printf("\\n");\n'
'fraction_free(a);\nfraction_free(b);\nfraction_free(c);', "3/4 2 -1/2")
def test_free_accepts_null(self):
self.show("fraction_free(NULL);", "")
def test_type_stays_opaque(self):
self.assertNotRegex(self.read("fraction.h"), r"struct\s+fraction\s*\{",
"Los campos de la fracción no van en el encabezado")
def test_your_tests(self):
source = re.sub(r"//[^\n]*", "", self.read("test_fraction.c"))
for name in ("fraction_new", "fraction_add", "fraction_multiply", "fraction_equal"):
with self.subTest(function=name):
self.assertRegex(source, rf"\b{name}\(", f"test_fraction.c no prueba {name}")
out, code = shell(["make"], self.work.path)
self.assertEqual(code, 0, f"make falló:\n{out}")
out, code = shell(["build/test_fraction"], self.work.path)
self.assertEqual(code, 0, f"Sus pruebas fallan, o dejan una fuga:\n{out}")
class StackModule(PartTest):
part = "stack"
IMPLEMENTATIONS = ("stack_array.c", "stack_blocks.c")
def both(self, code, wanted):
source = ('#include <stdio.h>\n#include "stack.h"\n'
f"int main(void) {{\n Stack *s = stack_new();\n int v = -1;\n"
f' if (s == NULL) {{ printf("stack_new devolvió NULL\\n"); return 1; }}\n'
f"{code}\n stack_free(s);\n return 0;\n}}\n")
for name in self.IMPLEMENTATIONS:
with self.subTest(implementation=name):
self.check_driver(source, [name], wanted)
def test_last_in_first_out(self):
self.both('stack_push(s, 1); stack_push(s, 2);\n'
'stack_pop(s, &v); printf("%d ", v);\nstack_pop(s, &v); printf("%d\\n", v);', "2 1")
def test_pop_on_empty_leaves_the_value_alone(self):
self.both('printf("%d %d\\n", stack_pop(s, &v), v);', "0 -1")
def test_peek_does_not_remove(self):
self.both('stack_push(s, 5);\nint ok = stack_peek(s, &v);\n'
'printf("%d %d %d\\n", ok, v, stack_size(s));', "1 5 1")
def test_peek_on_empty(self):
self.both('printf("%d %d\\n", stack_peek(s, &v), v);', "0 -1")
def test_cross_every_small_limit_in_both_directions(self):
code = ("for (int n = 0; n <= 40; n++) {\n"
" for (int i = 0; i < n; i++) { if (!stack_push(s, i)) return 1; }\n"
" if (stack_size(s) != n) return 2;\n"
" for (int i = n - 1; i >= 0; i--) {\n"
" if (!stack_peek(s, &v) || v != i) return 3;\n"
" if (!stack_pop(s, &v) || v != i) return 4;\n"
" }\n"
" if (stack_size(s) != 0) return 5;\n"
"}\n"
'printf("ok\\n");')
self.both(code, "ok")
def test_many_values_and_free_with_values_inside(self):
self.both("for (int i = 0; i < 20000; i++) { if (!stack_push(s, i)) return 1; }\n"
'printf("%d\\n", stack_size(s));', "20000")
def test_each_file_uses_its_representation(self):
array = re.sub(r"//[^\n]*", "", self.read("stack_array.c"))
blocks = re.sub(r"//[^\n]*", "", self.read("stack_blocks.c"))
self.assertIn("realloc", array, "stack_array.c debe crecer con realloc")
self.assertNotIn("realloc", blocks, "stack_blocks.c no usa realloc")
self.assertRegex(blocks, r"->\s*next", "stack_blocks.c debe enlazar sus bloques con next")
def test_the_given_suite_passes_with_one_object(self):
out, code = shell(["make"], self.work.path)
self.assertEqual(code, 0, f"make falló:\n{out}")
self.assertEqual(len(re.findall(r"-c test_stack\.c", out)), 1,
"test_stack.c debe compilarse una sola vez")
for program in ("test_array", "test_blocks"):
with self.subTest(program=program):
out, code = shell([f"build/{program}"], self.work.path)
self.assertEqual(code, 0, f"{program} falló:\n{out}")
def test_header_and_suite_stay_opaque(self):
self.assertNotRegex(self.read("stack.h"), r"struct\s+(stack|block)\s*\{",
"Los campos de la pila no van en el encabezado")
class OpaqueImage(PartTest):
part = "image"
SOURCES = ["image.c", "filters.c"]
def main(self, code):
return f"{PIXELS}int main(void) {{\n{code}\n return 0;\n}}\n"
def filtered(self, call, wanted):
source = self.main(
"const int values[] = {0, 10, 255, 30, 40, 100};\n"
"Image *img = image_new(3, 2, 255, 0);\n"
'if (img == NULL) { printf("NULL\\n"); return 1; }\n'
"for (int y = 0; y < 2; y++) {\n"
" for (int x = 0; x < 3; x++) { image_set(img, x, y, values[y * 3 + x]); }\n"
"}\n"
f"{call};\n"
"for (int y = 0; y < 2; y++) {\n"
' for (int x = 0; x < 3; x++) { printf("%d ", image_get(img, x, y)); }\n'
"}\n"
'printf("\\n");\n'
"image_free(img);")
self.check_driver(source, self.SOURCES, wanted)
def test_a_pointer_is_allowed(self):
out, code = self.work.compile_only('#include "image.h"\nImage *keep(Image *img) { return img; }\n')
self.assertEqual(code, 0, f"image.h debe declarar el tipo Image:\n{out}")
def assert_rejected(self, line):
self.test_a_pointer_is_allowed()
source = f'#include "image.h"\nint probe(Image *img) {{\n (void) img;\n {line}\n return 0;\n}}\n'
_, code = self.work.compile_only(source)
self.assertNotEqual(code, 0, f"Esta línea no debería compilar fuera de image.c: {line}")
def test_cannot_declare_an_image(self):
self.assert_rejected("Image local; (void) local;")
def test_cannot_read_a_field(self):
self.assert_rejected("return img->width;")
def test_accessors(self):
self.check_driver(self.main(
"Image *img = image_new(3, 2, 15, 7);\n"
'if (img == NULL) { printf("NULL\\n"); return 1; }\n'
"image_set(img, 2, 1, 9);\n"
'printf("%d %d %d %d %d\\n", image_width(img), image_height(img), image_max_value(img),\n'
" image_get(img, 2, 1), image_get(img, 0, 1));\n"
"image_free(img);\nimage_free(NULL);"), self.SOURCES, "3 2 15 9 7")
def test_new_rejects_bad_data(self):
self.check_driver(self.main(
'printf("%d %d %d %d\\n", image_new(0, 2, 255, 0) == NULL, image_new(2, 0, 255, 0) == NULL,\n'
" image_new(2, 2, 300, 0) == NULL, image_new(2, 2, 15, 16) == NULL);"),
self.SOURCES, "1 1 1 1")
def test_load_and_save_round_trip(self):
self.check_driver(self.main(
'Image *img = image_load("images/tiny.pgm");\n'
'if (img == NULL) { printf("NULL\\n"); return 1; }\n'
'if (!image_save(img, "copy.pgm")) { printf("no guarda\\n"); return 1; }\n'
'Image *copy = image_load("copy.pgm");\n'
'if (copy == NULL) { printf("NULL\\n"); return 1; }\n'
'printf("%d %d %d\\n", image_width(copy), image_height(copy), image_get(copy, 2, 1));\n'
"image_free(img);\nimage_free(copy);"), self.SOURCES, "3 2 50")
def test_load_rejects_bad_files(self):
bad = {"formato": "P5\n1 1\n255\n7\n", "pocos": "P2\n2 2\n255\n1 2 3\n",
"grande": "P2\n1 1\n255\n256\n", "letra": "P2\n1 1\n255\nx\n"}
for name, content in bad.items():
with self.subTest(case=name):
(self.work.path / "bad.pgm").write_text(content, encoding="utf-8")
self.check_driver(self.main(
'Image *img = image_load("bad.pgm");\n'
'printf("%d\\n", img == NULL);\nimage_free(img);'), self.SOURCES, "1")
def test_invert(self):
self.filtered("invert(img)", "255 245 0 225 215 155")
def test_threshold_with_the_exact_level(self):
self.filtered("threshold(img, 100)", "0 0 255 0 0 255")
def test_flip(self):
self.filtered("flip_horizontal(img)", "255 10 0 100 40 30")
def test_stats(self):
self.filtered('int min, max; double mean;\nimage_stats(img, &min, &max, &mean);\n'
'printf("%d %d %.2f\\n", min, max, mean)',
"0 255 72.50\n0 10 255 30 40 100")
def test_demo_runs(self):
out, code = shell(["make"], self.work.path)
self.assertEqual(code, 0, f"make falló:\n{out}")
self.assertNotIn("warning:", out, "Hay advertencias al compilar")
out, code = shell(["build/filter_demo"], self.work.path)
self.assertEqual(code, 0, f"filter_demo falló:\n{out}")
self.assertIn("Tamaño: 32 x 16", out)
def test_the_given_tests_pass(self):
out, code = shell(["make", "test_image"], self.work.path)
self.assertEqual(code, 0, f"Las pruebas todavía no compilan:\n{out}")
out, code = shell(["build/test_image"], self.work.path)
self.assertEqual(code, 0, f"test_image falló:\n{out}")
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 os
import pathlib
import re
import subprocess
import time
import unittest
HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-06.md"
SLIDES = ROOT / "slides" / "week-06.tex"
# Antes de un bloque cuya salida cambia con el sistema. No se compara.
SYSTEM_DEPENDENT = "<!-- depende del sistema -->"
# Un make corrido dentro de otro make hereda estas variables y escribe otras cosas.
CLEAN_ENV = {k: v for k, v in os.environ.items() if k not in ("MAKELEVEL", "MAKEFLAGS", "MFLAGS")}
def normalize(text):
# Según el idioma del entorno, gcc usa comillas tipográficas o rectas.
text = text.replace("‘", "'").replace("’", "'").replace("`", "'")
# El nombre del objeto temporal de gcc cambia en cada corrida.
text = re.sub(r"/tmp/cc\w+\.o", "/tmp/TEMP.o", text)
# Cambian con la arquitectura: el lugar dentro del código y la dirección que da nm.
text = re.sub(r"\(\.text\+0x[0-9a-f]+\)", "(.text+ADDR)", text)
return re.sub(r"^[0-9a-f]{16} ", "ADDR ", text, flags=re.M)
def run_shell(command, cwd):
# Entre un archivo tocado y el anterior make debe notarse la diferencia de hora.
if command.startswith(("touch ", "cp ")):
time.sleep(0.02)
done = subprocess.run(
["bash", "-c", command], cwd=cwd, env=CLEAN_ENV, text=True, encoding="utf-8",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
)
return normalize(done.stdout)
def clean_lines(text):
return [line.rstrip() for line in text.splitlines()]
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].extend(clean_lines(normalize(line)))
return pairs
def run_block(lines):
"""Corre un bloque como en una terminal: un cd vale para las líneas que siguen."""
cwd = HERE
for command, expected in split_commands(lines):
if command.startswith("cd "):
cwd = cwd / command[3:].strip()
yield command, expected, []
else:
yield command, expected, clean_lines(run_shell(command, cwd))
def note_blocks(language):
"""Devuelve (líneas, marca) de cada bloque de los apuntes."""
blocks, current, mark, previous = [], None, "", ""
for raw in NOTES.read_text(encoding="utf-8").splitlines():
line = re.sub(r"^> ?", "", raw)
if current is None:
if line.strip() == "```" + language:
current, mark = [], previous.strip()
elif line.strip():
previous = line
elif line.strip() == "```":
blocks.append((current, mark))
current = None
else:
current.append(line)
return blocks
def slide_blocks(environments):
text = SLIDES.read_text(encoding="utf-8")
pattern = r"\\begin\{(?:%s)\}[^\n]*\n(.*?)\\end\{(?:%s)\}" % (environments, environments)
return [match.group(1).splitlines() for match in re.finditer(pattern, text, re.S)]
def public_sources(patterns):
for pattern in patterns:
for source in HERE.rglob(pattern):
parts = source.relative_to(HERE).parts
if "solution" not in parts and "build" not in parts:
yield source
def known_lines(patterns):
lines = set()
for source in public_sources(patterns):
lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
return lines
def prepare():
"""Deja los ejemplos compilados y stale/ en su primera versión."""
run_shell("cp stale/rectangle_v1.h stale/rectangle.h", HERE)
for folder in ("receipt", "tally"):
run_shell("make", HERE / folder)
class NotesAreTrue(unittest.TestCase):
def setUp(self):
prepare()
self.addCleanup(prepare)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_terminal_blocks_in_notes(self):
checked = 0
for lines, mark in note_blocks("text"):
if not lines or not lines[0].startswith("$ "):
continue
for command, expected, actual in run_block(lines):
checked += 1
if mark != SYSTEM_DEPENDENT:
with self.subTest(command=command):
self.assertEqual(actual, expected)
self.assertGreater(checked, 20)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_code_blocks_in_notes_exist(self):
known = known_lines(["*.c", "*.h"])
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_makefile_blocks_in_notes_exist(self):
known = known_lines(["Makefile"])
for lines, _ in note_blocks("make"):
for line in lines:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
class SlidesAreTrue(unittest.TestCase):
def setUp(self):
prepare()
self.addCleanup(prepare)
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_terminal_blocks_in_slides(self):
for block in slide_blocks("terminal|salida|salidalarga"):
for command, expected, actual in run_block(block):
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_lines(["*.c", "*.h", "Makefile"])
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()// Pruebas de Tally. Solo usan lo que promete tally.h, así que no saben cuál implementación
// se enlazó con ellas.
#include <assert.h>
#include <stdio.h>
#include "tally.h"
static void test_empty(void) {
Tally *tally = tally_new();
assert(tally != NULL);
assert(tally_count(tally) == 0);
assert(tally_sum(tally) == 0);
tally_free(tally);
}
static void test_some_values(void) {
Tally *tally = tally_new();
assert(tally != NULL);
assert(tally_add(tally, 3));
assert(tally_add(tally, 9));
assert(tally_add(tally, 4));
assert(tally_count(tally) == 3);
assert(tally_sum(tally) == 16);
assert(tally_max(tally) == 9);
tally_free(tally);
}
// El primer valor es el mayor, y es negativo: el máximo no puede empezar en 0.
static void test_negative_values(void) {
Tally *tally = tally_new();
assert(tally != NULL);
assert(tally_add(tally, -2));
assert(tally_add(tally, -7));
assert(tally_max(tally) == -2);
assert(tally_sum(tally) == -9);
tally_free(tally);
}
// Más valores que la capacidad inicial de cualquier implementación razonable.
static void test_many_values(void) {
Tally *tally = tally_new();
assert(tally != NULL);
for (int i = 1; i <= 1000; i++) {
assert(tally_add(tally, i));
}
assert(tally_count(tally) == 1000);
assert(tally_sum(tally) == 500500);
assert(tally_max(tally) == 1000);
tally_free(tally);
}
static void test_two_at_once(void) {
Tally *first = tally_new();
Tally *second = tally_new();
assert(first != NULL && second != NULL);
assert(tally_add(first, 5));
assert(tally_count(second) == 0);
tally_free(first);
tally_free(second);
}
int main(void) {
test_empty();
test_some_values();
test_negative_values();
test_many_values();
test_two_at_once();
printf("Todas las pruebas pasaron\n");
return 0;
}Laboratorio
Archivos de partida del laboratorio de la semana.
# Laboratorio de la semana 6: módulos, tipos opacos y pruebas por la interfaz
Los comandos se corren desde `code/week-06`, salvo cuando se indica otra carpeta. Las ideas
están en los apuntes, en la sección "El laboratorio, paso a paso".
Cada sesión está pensada para unos 90 minutos de trabajo. Lo marcado como **puede quedar
para antes de la siguiente sesión** es lo primero que se mueve si no alcanza el tiempo.
```sh
make lab # compila cada parte con su Makefile
make test-lab # comprueba las cuatro partes; falla hasta que terminen
```
Para ver solo una parte: `LAB_DIR=lab python3 -m unittest -v test_lab.SplitProgram`, y lo
mismo con `FractionModule`, `StackModule` y `OpaqueImage`.
Desde aquí, ninguna prueba hace `#include "algo.c"`. Las pruebas se compilan aparte y se
enlazan con el módulo.
## Sesión 1
### Parte 1: dividir un programa (unos 45 minutos). Carpeta `lab/split`
`market.c` lee `ventas.txt` y escribe una tabla y un resumen. Funciona, y está todo en un
solo archivo. **No lo borren:** las pruebas comparan su salida con la de sus módulos.
Primero léanlo, y respondan por escrito:
1. ¿Qué funciones escriben en la terminal y cuáles no?
2. ¿Qué funciones llama cada función? Dibújenlo con flechas.
3. ¿Qué tipo o constante necesita más de una parte del programa?
Después divídanlo en estos archivos, con estos nombres exactos:
| Archivo | Qué lleva |
|---|---|
| `sales.h`, `sales.c` | `struct sale`, `load_sales`, `sale_total`, `sales_total` y `best_sale`. `parse_line` es privada |
| `report.h`, `report.c` | `print_table` y `print_summary`. `print_money` es privada |
| `main.c` | `main`, y nada más |
No cambien los nombres ni los parámetros de las funciones, ni lo que escribe el programa.
Cada `.h` lleva guarda de inclusión y comentarios que dicen qué promete cada función.
Luego escriban `lab/split/Makefile`, tomando como modelo `receipt/Makefile`:
- `make` deja el ejecutable en `build/market` y los objetos en `build/main.o`,
`build/sales.o` y `build/report.o`.
- Se compila con `-Wall -Wextra -std=c17 -g -fsanitize=address,undefined`, también al enlazar.
- Cada objeto depende de su `.c` y de todos los `.h` que incluye, directa o indirectamente.
- `make clean` borra `build/`.
- No pongan `@` delante de las recetas: las pruebas leen los comandos que `make` escribe.
Prueben a mano, desde `lab/split`, antes de correr las pruebas:
```sh
make && build/market
touch report.c && make # ¿qué se volvió a compilar?
touch sales.h && make # ¿y ahora? ¿por qué?
```
### Parte 2: fracciones con tipo opaco (unos 45 minutos). Carpeta `lab/fraction`
`fraction.h` dice lo que promete el módulo. No lo cambien. Escriban `fraction.c` y agreguen
pruebas en `test_fraction.c`, que solo pueden usar lo que dice el encabezado. Calculen a mano
cada resultado esperado.
Un consejo para el orden: primero `fraction_new`, porque ahí se reduce y se arregla el signo.
Después `fraction_add` y `fraction_multiply` son casi una línea cada una. `gcd` va como una
función `static`, porque es un detalle de este archivo.
```sh
cd lab/fraction && make && build/test_fraction
```
**Puede quedar para antes de la siguiente sesión:** `fraction_print` y las pruebas de
`fraction_equal`.
## Sesión 2
### Parte 3: dos implementaciones de `Stack` (unos 50 minutos). Carpeta `lab/stack`
`stack.h` y `test_stack.c` ya están hechos, y no se cambian. Léanlos primero. Escriban
`stack_array.c`, con un arreglo que crece con `realloc`, y luego `stack_blocks.c`, con
bloques enlazados. Los apuntes dibujan esta segunda representación. Antes de escribirla,
dibujen en papel qué pasa al meter el quinto valor y al sacarlo.
```sh
cd lab/stack && make
build/test_array
build/test_blocks
```
Fíjense en la salida de `make`: `test_stack.c` se compila una vez, y su objeto se enlaza dos.
Después respondan por escrito: ¿qué línea de `test_stack.c` tendrían que cambiar para pasar de
una implementación a la otra?
### Parte 4: el módulo de imágenes como tipo opaco (unos 40 minutos). Carpeta `lab/image`
Hoy `struct image` está en `image.h`, y `filters.c` y `filter_demo.c` miran sus campos. Háganlo
en este orden:
1. Lleven el `struct` de `image.h` a `image.c`, y pongan en `image.h`
`typedef struct image Image;`. Compilen con `make` y **lean los errores**: esa es la lista
de todo lo que dependía de la representación.
2. Agreguen a `image.h` y a `image.c`, con sus comentarios, estas funciones:
```c
int image_width(const Image *img);
int image_height(const Image *img);
int image_max_value(const Image *img);
int image_get(const Image *img, int x, int y);
void image_set(Image *img, int x, int y, int value);
```
Comenten qué exige cada una. `image_get` e `image_set` exigen que `x` e `y` estén dentro
de la imagen.
3. Cambien `struct image *` por `Image *` en las firmas de `image.h` y de `filters.h`.
4. Reescriban `filters.c` y `filter_demo.c` usando solo la interfaz.
```sh
cd lab/image && make && build/filter_demo
make test_image && build/test_image
```
`test_image.c` no compila hasta que terminen el paso 2. Es lo esperado.
**Puede quedar para antes de la siguiente sesión:** reescribir `image_stats` y `filter_demo.c`,
y responder esto por escrito: si mañana cada fila de píxeles se guardara en su propio bloque de
memoria, ¿qué archivos cambiarían?// Sesión 1: escriban este módulo según fraction.h. Pueden agregar funciones static.
#include "fraction.h"
#include <stdio.h>
#include <stdlib.h>
struct fraction {
int numerator;
int denominator;
};
Fraction *fraction_new(int numerator, int denominator) {
(void) numerator; // borren estas líneas cuando usen los parámetros
(void) denominator;
// Escriban aquí su código.
return NULL;
}
void fraction_free(Fraction *fraction) {
(void) fraction;
// Escriban aquí su código.
}
int fraction_numerator(const Fraction *fraction) {
(void) fraction;
// Escriban aquí su código.
return 0;
}
int fraction_denominator(const Fraction *fraction) {
(void) fraction;
// Escriban aquí su código.
return 0;
}
Fraction *fraction_add(const Fraction *a, const Fraction *b) {
(void) a;
(void) b;
// Escriban aquí su código.
return NULL;
}
Fraction *fraction_multiply(const Fraction *a, const Fraction *b) {
(void) a;
(void) b;
// Escriban aquí su código.
return NULL;
}
int fraction_equal(const Fraction *a, const Fraction *b) {
(void) a;
(void) b;
// Escriban aquí su código.
return 0;
}
void fraction_print(const Fraction *fraction) {
(void) fraction;
// Escriban aquí su código.
}// Módulo de fracciones. Es un tipo opaco: quien lo usa solo tiene punteros.
// Una fracción siempre está reducida y su denominador siempre es positivo.
// Todas las funciones exigen que los productos y las sumas de la cuenta quepan en un int.
#ifndef FRACTION_H
#define FRACTION_H
typedef struct fraction Fraction;
// Crea la fracción numerator / denominator, ya reducida: 2 / -4 queda como -1 / 2, y 0 / 5
// queda como 0 / 1. Devuelve NULL si denominator es 0 o si no hay memoria.
// Owner: quien llama, que debe liberarla con fraction_free.
Fraction *fraction_new(int numerator, int denominator);
// Libera la fracción. Acepta NULL.
void fraction_free(Fraction *fraction);
// Devuelven el numerador y el denominador de la forma reducida.
int fraction_numerator(const Fraction *fraction);
int fraction_denominator(const Fraction *fraction);
// Devuelven una fracción nueva con el resultado, o NULL si no hay memoria. No cambian a ni b.
// Owner del resultado: quien llama.
Fraction *fraction_add(const Fraction *a, const Fraction *b);
Fraction *fraction_multiply(const Fraction *a, const Fraction *b);
// Devuelve 1 si valen lo mismo y 0 si no.
int fraction_equal(const Fraction *a, const Fraction *b);
// Escribe la fracción en la forma 3/4, sin salto de línea. Si el denominador es 1, escribe
// solo el numerador: 2, no 2/1.
void fraction_print(const Fraction *fraction);
#endif# Módulo de fracciones y sus pruebas.
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
BUILD = build
$(BUILD)/test_fraction: $(BUILD)/test_fraction.o $(BUILD)/fraction.o
$(CC) $(CFLAGS) $^ -o $@
$(BUILD)/%.o: %.c fraction.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD):
mkdir -p $@
.PHONY: clean
clean:
rm -rf $(BUILD)// Sesión 1: pruebas del módulo de fracciones, solo por su interfaz.
// Agreguen pruebas de fraction_add, fraction_multiply y fraction_equal, y llámenlas desde
// main. Calculen a mano el resultado esperado. Toda fracción que creen se libera.
#include <assert.h>
#include <stdio.h>
#include "fraction.h"
static void test_new_reduces(void) {
Fraction *half = fraction_new(2, 4);
assert(half != NULL);
assert(fraction_numerator(half) == 1);
assert(fraction_denominator(half) == 2);
fraction_free(half);
}
int main(void) {
test_new_reduces();
printf("Todas las pruebas pasaron\n");
return 0;
}// Aplica los filtros a images/sun.pgm y dibuja el resultado.
// Se corre desde lab/image: build/filter_demo
#include <stdio.h>
#include "filters.h"
int main(void) {
struct image *img = image_load("images/sun.pgm");
if (img == NULL) {
fprintf(stderr, "No se pudo leer images/sun.pgm\n");
return 1;
}
int min = 0;
int max = 0;
double mean = 0.0;
image_stats(img, &min, &max, &mean);
printf("Tamaño: %d x %d\n", img->width, img->height);
printf("Mínimo %d, máximo %d, promedio %.2f\n", min, max, mean);
flip_horizontal(img);
printf("flip_horizontal:\n");
image_show(img);
threshold(img, 128);
printf("threshold 128:\n");
image_show(img);
invert(img);
printf("invert:\n");
image_show(img);
if (!image_save(img, "build/sun_result.pgm")) {
fprintf(stderr, "No se pudo escribir build/sun_result.pgm\n");
image_free(img);
return 1;
}
image_free(img);
return 0;
}// Filtros. Hoy miran los campos de la imagen. Después de la conversión ya no podrán.
#include "filters.h"
void invert(struct image *img) {
int count = img->width * img->height;
for (int i = 0; i < count; i++) {
img->pixels[i] = img->max_value - img->pixels[i];
}
}
void threshold(struct image *img, int level) {
int count = img->width * img->height;
for (int i = 0; i < count; i++) {
img->pixels[i] = img->pixels[i] >= level ? img->max_value : 0;
}
}
void flip_horizontal(struct image *img) {
for (int y = 0; y < img->height; y++) {
int *row = img->pixels + y * img->width;
for (int x = 0; x < img->width / 2; x++) {
int other = img->width - 1 - x;
int saved = row[x];
row[x] = row[other];
row[other] = saved;
}
}
}
void image_stats(const struct image *img, int *min, int *max, double *mean) {
int count = img->width * img->height;
long sum = 0;
*min = img->pixels[0];
*max = img->pixels[0];
for (int i = 0; i < count; i++) {
int value = img->pixels[i];
sum += value;
if (value < *min) {
*min = value;
}
if (value > *max) {
*max = value;
}
}
*mean = (double) sum / count;
}// Filtros: cambian la imagen que reciben, en el mismo lugar.
#ifndef FILTERS_H
#define FILTERS_H
#include "image.h"
// Cada píxel p pasa a max_value - p.
void invert(struct image *img);
// Cada píxel mayor o igual que level pasa a max_value. Los demás, a 0.
void threshold(struct image *img, int level);
// Espejo izquierda-derecha: cada fila queda al revés.
void flip_horizontal(struct image *img);
// Guarda en *min, *max y *mean el menor píxel, el mayor y el promedio.
void image_stats(const struct image *img, int *min, int *max, double *mean);
#endif// Implementación del módulo de imágenes. Ver image.h.
#include "image.h"
#include <ctype.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
// Ningún dato válido de un PGM de este curso pasa de aquí.
#define NUMBER_LIMIT 100000
// En cada línea del archivo: el formato pide no pasar de 70 caracteres.
#define VALUES_PER_LINE 16
static int valid_size(int width, int height, int max_value) {
return width >= 1 && height >= 1 && width <= INT_MAX / height
&& max_value >= 1 && max_value <= IMAGE_MAX_VALUE;
}
struct image *image_new(int width, int height, int max_value, int value) {
if (!valid_size(width, height, max_value) || value < 0 || value > max_value) {
return NULL;
}
struct image *img = malloc(sizeof(*img));
if (img == NULL) {
return NULL;
}
int count = width * height;
img->pixels = malloc(count * sizeof(int));
if (img->pixels == NULL) {
free(img);
return NULL;
}
img->width = width;
img->height = height;
img->max_value = max_value;
for (int i = 0; i < count; i++) {
img->pixels[i] = value;
}
return img;
}
void image_free(struct image *img) {
if (img != NULL) {
free(img->pixels);
free(img);
}
}
// Devuelve el siguiente número del archivo, o -1 si lo que sigue no es un número.
// Se salta los espacios y los comentarios, que van de # hasta el fin de la línea.
static int read_number(FILE *file) {
int c = fgetc(file);
while (c == '#' || isspace(c)) {
if (c == '#') {
while (c != '\n' && c != EOF) {
c = fgetc(file);
}
} else {
c = fgetc(file);
}
}
if (!isdigit(c)) {
return -1;
}
int value = 0;
while (isdigit(c)) {
value = value * 10 + (c - '0');
if (value > NUMBER_LIMIT) {
return -1;
}
c = fgetc(file);
}
// Lo que no era parte del número le toca a la siguiente lectura.
if (c != EOF) {
ungetc(c, file);
}
return value;
}
// Lee la imagen de un archivo ya abierto. Lo que venga después del último píxel se ignora.
static struct image *read_image(FILE *file) {
if (fgetc(file) != 'P' || fgetc(file) != '2') {
return NULL;
}
int width = read_number(file);
int height = read_number(file);
int max_value = read_number(file);
struct image *img = image_new(width, height, max_value, 0);
if (img == NULL) {
return NULL;
}
int count = width * height;
for (int i = 0; i < count; i++) {
int value = read_number(file);
if (value < 0 || value > max_value) {
image_free(img);
return NULL;
}
img->pixels[i] = value;
}
return img;
}
struct image *image_load(const char *name) {
FILE *file = fopen(name, "r");
if (file == NULL) {
return NULL;
}
struct image *img = read_image(file);
fclose(file);
return img;
}
int image_save(const struct image *img, const char *name) {
FILE *file = fopen(name, "w");
if (file == NULL) {
return 0;
}
int written = fprintf(file, "P2\n%d %d\n%d\n", img->width, img->height, img->max_value);
for (int y = 0; y < img->height && written >= 0; y++) {
for (int x = 0; x < img->width && written >= 0; x++) {
int last_in_line = x == img->width - 1 || x % VALUES_PER_LINE == VALUES_PER_LINE - 1;
written = fprintf(file, "%d%c", img->pixels[y * img->width + x],
last_in_line ? '\n' : ' ');
}
}
// Al cerrar se escribe lo que quedaba pendiente, y eso también puede fallar.
if (fclose(file) != 0 || written < 0) {
return 0;
}
return 1;
}
void image_print(const struct image *img) {
printf("%d x %d, max %d\n", img->width, img->height, img->max_value);
for (int y = 0; y < img->height; y++) {
for (int x = 0; x < img->width; x++) {
printf("%4d", img->pixels[y * img->width + x]);
}
printf("\n");
}
}
void image_show(const struct image *img) {
const char shades[] = " .:-=+*#%@";
int last_shade = (int) sizeof(shades) - 2;
for (int y = 0; y < img->height; y++) {
for (int x = 0; x < img->width; x++) {
putchar(shades[img->pixels[y * img->width + x] * last_shade / img->max_value]);
}
putchar('\n');
}
}// Módulo de imágenes en escala de grises, en formato PGM de texto (P2).
// Sesión 2: conviertan este módulo a un tipo opaco. Ver README.md.
#ifndef IMAGE_H
#define IMAGE_H
#define IMAGE_MAX_VALUE 255
// Los píxeles van fila por fila: el píxel (x, y) es pixels[y * width + x].
// Cada uno vale de 0 (negro) a max_value (blanco).
struct image {
int width;
int height;
int max_value;
int *pixels;
};
// Crea una imagen con todos sus píxeles en value. Devuelve NULL si algún dato no está en su
// rango o si no hay memoria. Owner: quien llama, que debe liberarla con image_free.
struct image *image_new(int width, int height, int max_value, int value);
// Lee un archivo PGM de texto. Devuelve NULL si no se puede abrir, si no es P2, si sus datos
// están mal o si no hay memoria. Owner: quien llama, que debe liberarla con image_free.
struct image *image_load(const char *name);
// Libera la imagen. Acepta NULL.
void image_free(struct image *img);
// Escribe la imagen en un archivo PGM de texto. Devuelve 1 si pudo y 0 si no.
int image_save(const struct image *img, const char *name);
// Escribe el tamaño y los números de cada píxel, para imágenes pequeñas.
void image_print(const struct image *img);
// Dibuja la imagen en la terminal con letras: más claro, más lleno.
void image_show(const struct image *img);
#endif# Módulo de imágenes y filtros.
# make compila filter_demo
# make test_image compila las pruebas: no compilan hasta terminar la conversión
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
BUILD = build
MODULE = $(BUILD)/image.o $(BUILD)/filters.o
$(BUILD)/filter_demo: $(BUILD)/filter_demo.o $(MODULE)
$(CC) $(CFLAGS) $^ -o $@
test_image: $(BUILD)/test_image
$(BUILD)/test_image: $(BUILD)/test_image.o $(MODULE)
$(CC) $(CFLAGS) $^ -o $@
$(BUILD)/image.o: image.c image.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
# Estos tres incluyen filters.h, y filters.h incluye image.h.
$(BUILD)/filters.o: filters.c filters.h image.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/filter_demo.o: filter_demo.c filters.h image.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/test_image.o: test_image.c filters.h image.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD):
mkdir -p $@
.PHONY: test_image clean
clean:
rm -rf $(BUILD)// Pruebas del módulo convertido, solo por su interfaz. No compila hasta que Image exista y
// estén las funciones image_width, image_height, image_max_value, image_get e image_set.
#include <assert.h>
#include <stdio.h>
#include "filters.h"
static void test_new_get_set(void) {
Image *img = image_new(3, 2, 255, 7);
assert(img != NULL);
assert(image_width(img) == 3);
assert(image_height(img) == 2);
assert(image_max_value(img) == 255);
assert(image_get(img, 2, 1) == 7);
image_set(img, 2, 0, 99);
assert(image_get(img, 2, 0) == 99);
assert(image_get(img, 0, 1) == 7);
image_free(img);
image_free(NULL);
}
static void test_flip_keeps_rows_apart(void) {
Image *img = image_new(3, 2, 255, 0);
assert(img != NULL);
image_set(img, 0, 0, 1);
image_set(img, 2, 1, 6);
flip_horizontal(img);
assert(image_get(img, 2, 0) == 1);
assert(image_get(img, 0, 1) == 6);
assert(image_get(img, 0, 0) == 0);
image_free(img);
}
int main(void) {
test_new_get_set();
test_flip_keeps_rows_apart();
printf("Todas las pruebas pasaron\n");
return 0;
}// Ventas de un puesto del mercado: lee ventas.txt y escribe una tabla y un resumen.
// Todo está en un solo archivo. La sesión 1 lo divide en módulos.
#include <stdio.h>
#define MAX_SALES 100
#define NAME_SIZE 20
#define LINE_SIZE 100
#define CENTS_PER_SOL 100
struct sale {
char product[NAME_SIZE];
int quantity;
int unit_cents;
};
// Parte una línea "producto cantidad precio". Devuelve 1 si tiene esa forma y 0 si no.
int parse_line(const char *line, struct sale *sale) {
char extra[2];
int fields = sscanf(line, "%19s %d %d %1s", sale->product, &sale->quantity,
&sale->unit_cents, extra);
return fields == 3 && sale->quantity > 0 && sale->unit_cents >= 0;
}
// Lee hasta max_sales ventas. Devuelve cuántas leyó, o -1 si el archivo no se puede abrir,
// si una línea está mal o si hay más ventas que max_sales.
int load_sales(const char *name, struct sale sales[], int max_sales) {
FILE *file = fopen(name, "r");
if (file == NULL) {
return -1;
}
char line[LINE_SIZE];
int n = 0;
while (fgets(line, LINE_SIZE, file) != NULL) {
if (n == max_sales || !parse_line(line, &sales[n])) {
fclose(file);
return -1;
}
n++;
}
fclose(file);
return n;
}
// Devuelve lo que se cobró en una venta, en céntimos.
int sale_total(const struct sale *sale) {
return sale->quantity * sale->unit_cents;
}
// Devuelve lo que se cobró en n ventas, en céntimos.
int sales_total(const struct sale sales[], int n) {
int total = 0;
for (int i = 0; i < n; i++) {
total += sale_total(&sales[i]);
}
return total;
}
// Devuelve el índice de la venta que más cobró, o -1 si n es 0.
int best_sale(const struct sale sales[], int n) {
if (n == 0) {
return -1;
}
int best = 0;
for (int i = 1; i < n; i++) {
if (sale_total(&sales[i]) > sale_total(&sales[best])) {
best = i;
}
}
return best;
}
// Escribe un monto en la forma S/ 12.50, sin salto de línea.
void print_money(int cents) {
printf("S/ %d.%02d", cents / CENTS_PER_SOL, cents % CENTS_PER_SOL);
}
// Escribe una línea por venta.
void print_table(const struct sale sales[], int n) {
for (int i = 0; i < n; i++) {
printf("%-10s %3d ", sales[i].product, sales[i].quantity);
print_money(sale_total(&sales[i]));
printf("\n");
}
}
// Escribe el total y la venta que más cobró.
void print_summary(const struct sale sales[], int n) {
printf("Total: ");
print_money(sales_total(sales, n));
printf("\n");
int best = best_sale(sales, n);
if (best >= 0) {
printf("La mejor venta: %s\n", sales[best].product);
}
}
int main(void) {
struct sale sales[MAX_SALES];
int n = load_sales("ventas.txt", sales, MAX_SALES);
if (n < 0) {
fprintf(stderr, "No se pudo leer ventas.txt\n");
return 1;
}
print_table(sales, n);
print_summary(sales, n);
return 0;
}papa 12 350
camote 5 280
choclo 8 420
aji 30 50
limon 40 35# Una sola prueba y dos implementaciones: test_stack.o se compila una vez y se enlaza dos.
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
BUILD = build
PROGRAMS = $(BUILD)/test_array $(BUILD)/test_blocks
all: $(PROGRAMS)
$(BUILD)/test_array: $(BUILD)/test_stack.o $(BUILD)/stack_array.o
$(CC) $(CFLAGS) $^ -o $@
$(BUILD)/test_blocks: $(BUILD)/test_stack.o $(BUILD)/stack_blocks.o
$(CC) $(CFLAGS) $^ -o $@
$(BUILD)/%.o: %.c stack.h | $(BUILD)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD):
mkdir -p $@
.PHONY: all clean
clean:
rm -rf $(BUILD)// Sesión 2: Stack con un arreglo que crece. Escriban las funciones según stack.h.
#include "stack.h"
#include <stdlib.h>
#define FIRST_CAPACITY 4
struct stack {
int *values;
int size;
int capacity;
};
Stack *stack_new(void) {
// Escriban aquí su código.
return NULL;
}
void stack_free(Stack *stack) {
(void) stack; // borren estas líneas cuando usen los parámetros
// Escriban aquí su código.
}
int stack_push(Stack *stack, int value) {
(void) stack;
(void) value;
// Escriban aquí su código.
return 0;
}
int stack_pop(Stack *stack, int *value) {
(void) stack;
(void) value;
// Escriban aquí su código.
return 0;
}
int stack_peek(const Stack *stack, int *value) {
(void) stack;
(void) value;
// Escriban aquí su código.
return 0;
}
int stack_size(const Stack *stack) {
(void) stack;
// Escriban aquí su código.
return 0;
}// Sesión 2: Stack con bloques enlazados. Escriban las funciones según stack.h.
// El bloque de arriba es top. Cada bloque apunta al que tiene debajo, y el último apunta a
// NULL. No usen realloc.
#include "stack.h"
#include <stdlib.h>
#define BLOCK_SIZE 4
struct block {
int values[BLOCK_SIZE];
int used;
struct block *next;
};
struct stack {
struct block *top;
int size;
};
Stack *stack_new(void) {
// Escriban aquí su código.
return NULL;
}
void stack_free(Stack *stack) {
(void) stack; // borren estas líneas cuando usen los parámetros
// Escriban aquí su código.
}
int stack_push(Stack *stack, int value) {
(void) stack;
(void) value;
// Escriban aquí su código.
return 0;
}
int stack_pop(Stack *stack, int *value) {
(void) stack;
(void) value;
// Escriban aquí su código.
return 0;
}
int stack_peek(const Stack *stack, int *value) {
(void) stack;
(void) value;
// Escriban aquí su código.
return 0;
}
int stack_size(const Stack *stack) {
(void) stack;
// Escriban aquí su código.
return 0;
}// Una pila de enteros: el último valor que entra es el primero que sale.
// Es un tipo opaco, y este encabezado no dice cómo se guardan los valores.
#ifndef STACK_H
#define STACK_H
typedef struct stack Stack;
// Crea una pila vacía. Devuelve NULL si no hay memoria.
// Owner: quien llama, que debe liberarla con stack_free.
Stack *stack_new(void);
// Libera la pila y todo lo que tenga adentro. Acepta NULL.
void stack_free(Stack *stack);
// Pone value arriba. Devuelve 1 si pudo y 0 si no hay memoria; en ese caso, nada cambia.
int stack_push(Stack *stack, int value);
// Saca el valor de arriba y lo guarda en *value. Devuelve 1 si pudo.
// Si la pila está vacía, devuelve 0 y no toca *value.
int stack_pop(Stack *stack, int *value);
// Como stack_pop, pero el valor se queda en la pila.
int stack_peek(const Stack *stack, int *value);
// Devuelve cuántos valores hay en la pila.
int stack_size(const Stack *stack);
#endif// Pruebas de Stack, solo por su interfaz. Este archivo se compila una vez y se enlaza con
// cada implementación. No lo cambien: sus dos implementaciones deben pasarlo tal como está.
#include <assert.h>
#include <stdio.h>
#include "stack.h"
static void test_empty(void) {
Stack *stack = stack_new();
int value = 77;
assert(stack != NULL);
assert(stack_size(stack) == 0);
assert(!stack_pop(stack, &value));
assert(!stack_peek(stack, &value));
assert(value == 77);
stack_free(stack);
}
static void test_last_in_first_out(void) {
Stack *stack = stack_new();
int value = 0;
assert(stack != NULL);
assert(stack_push(stack, 1));
assert(stack_push(stack, 2));
assert(stack_push(stack, 3));
assert(stack_size(stack) == 3);
assert(stack_peek(stack, &value) && value == 3);
assert(stack_size(stack) == 3);
assert(stack_pop(stack, &value) && value == 3);
assert(stack_pop(stack, &value) && value == 2);
assert(stack_pop(stack, &value) && value == 1);
assert(stack_size(stack) == 0);
assert(!stack_pop(stack, &value));
stack_free(stack);
}
// Muchos valores: suficientes para que cualquier representación tenga que crecer.
static void test_many_values(void) {
Stack *stack = stack_new();
int value = 0;
assert(stack != NULL);
for (int i = 0; i < 1000; i++) {
assert(stack_push(stack, i));
}
assert(stack_size(stack) == 1000);
for (int i = 999; i >= 0; i--) {
assert(stack_pop(stack, &value) && value == i);
}
assert(stack_size(stack) == 0);
stack_free(stack);
}
// Sube y baja varias veces, para pasar por los mismos límites en los dos sentidos.
static void test_up_and_down(void) {
Stack *stack = stack_new();
int value = 0;
assert(stack != NULL);
for (int round = 1; round <= 20; round++) {
for (int i = 0; i < round; i++) {
assert(stack_push(stack, round * 100 + i));
}
for (int i = round - 1; i >= 0; i--) {
assert(stack_pop(stack, &value) && value == round * 100 + i);
}
assert(stack_size(stack) == 0);
}
stack_free(stack);
}
// Liberar una pila que todavía tiene valores no debe dejar fugas.
static void test_free_with_values(void) {
Stack *stack = stack_new();
assert(stack != NULL);
for (int i = 0; i < 50; i++) {
assert(stack_push(stack, i));
}
stack_free(stack);
stack_free(NULL);
}
static void test_two_at_once(void) {
Stack *first = stack_new();
Stack *second = stack_new();
int value = 0;
assert(first != NULL && second != NULL);
assert(stack_push(first, 1));
assert(stack_push(second, 2));
assert(stack_pop(first, &value) && value == 1);
assert(stack_size(second) == 1);
stack_free(first);
stack_free(second);
}
int main(void) {
test_empty();
test_last_in_first_out();
test_many_values();
test_up_and_down();
test_free_with_values();
test_two_at_once();
printf("Todas las pruebas pasaron\n");
return 0;
}P2
# Un sol a la izquierda y un cerro que sube hacia la derecha.
32 16
255
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 255 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 255 255 255 255 255 255 255 255 255 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 255 255 255 255 255 255 255 255 255 255 255 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 255 255 255 255 255 255 255 255 255 255 255 255 255 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 255 255 255 255 255 255 255 255 255 255 255 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 255 255 255 255 255 255 255 255 255 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 130 130
40 40 40 40 40 40 40 40 255 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 130 130 130 130 130 130 130
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 130 130 130 130 130 130 130 130 130 130 130 130
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
40 40 40 40 40 40 40 40 40 40 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
40 40 40 40 40 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130P2
# Tres columnas y dos filas, con un comentario.
3 2
255
0 10 20
30 40 50Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 6: programas en varios archivos. Se usa dentro del entorno del curso.
#
# make compila los ejemplos de los apuntes (receipt/ y tally/)
# make lab compila el laboratorio, cada parte con su propio Makefile
# make test corre las pruebas de los ejemplos y de los apuntes
# make test-lab prueba su laboratorio; falla hasta que lo terminen
# make clean borra lo generado
#
# Los programas de errors/ tienen errores a propósito, y stale/ tiene un Makefile mal hecho
# a propósito. No se compilan con make: se usan con los comandos de los apuntes.
EXAMPLES = receipt tally
LAB_PARTS = lab/fraction lab/stack lab/image
GENERATED = $(EXAMPLES) stale $(LAB_PARTS)
.PHONY: all lab test test-lab clean
all:
@for dir in $(EXAMPLES); do $(MAKE) --no-print-directory -C $$dir || exit 1; done
# La parte 1 no trae Makefile: lo escriben ustedes en lab/split.
lab:
@for dir in $(LAB_PARTS); do $(MAKE) --no-print-directory -C $$dir || exit 1; done
@if [ -f lab/split/Makefile ]; then $(MAKE) --no-print-directory -C lab/split; fi
test: all
python3 -m unittest discover -p 'test_*.py'
test-lab:
LAB_DIR=lab python3 -m unittest -v test_lab
clean:
@for dir in $(GENERATED); do $(MAKE) --no-print-directory -C $$dir clean; done
@if [ -f lab/split/Makefile ]; then $(MAKE) --no-print-directory -C lab/split clean; fi
rm -rf build __pycache__