push (solved)
Some checks are pending
grade / grade (push) Waiting to run

This commit is contained in:
trevon 2026-08-26 01:12:23 +00:00
commit 8c08fb626f
21 changed files with 1199 additions and 0 deletions

View file

@ -0,0 +1,34 @@
name: grade
on: [push]
# Production wiring (gitea-runner on ghost, docker mode):
# - job container = gitea/runner-images (has docker CLI) + the host docker
# socket, so docker_grade.sh drives the host daemon exactly as designed
# - hidden tests mount from the runner host (/opt/grade-hidden) — never in
# this repo
# - GRADE_HMAC_SECRET mounts from /opt/grade/grade.env on the runner host
jobs:
grade:
runs-on: ubuntu-latest
container:
image: gitea/runner-images:ubuntu-22.04
options: >-
-v /var/run/docker.sock:/var/run/docker.sock
-v /opt/grade/grade.env:/run/secrets/grade.env:ro
-v /opt/grade-hidden:/opt/grade-hidden:ro
steps:
- uses: actions/checkout@v4
- name: Grade (blank fails, solved passes)
run: |
set -a; . /run/secrets/grade.env; set +a
docker run --rm --network=none \
-v "$PWD":/work \
-v /opt/grade-hidden/cs2060/record-manager:/hidden:ro \
-e GRADE_HMAC_SECRET \
grade-c:dev \
python3 /opt/grade/grade_cli.py run \
--student /work --hidden /hidden \
--course CS2060 --assignment record-manager \
--out /work/results.json
echo "--- signed result summary ---"
head -c 400 results.json

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
build/
.cache/
*.o
*.d

40
Makefile Normal file
View file

@ -0,0 +1,40 @@
# shellcheck disable=SC2046,SC2035,SC2215,SC2285,SC2276,SC2068
CC ?= cc
CFLAGS ?= -std=c17 -Wall -Wextra -Wpedantic -Werror -g
CPPFLAGS += -Iinclude -MMD -MP
SANFLAGS := -fsanitize=address,undefined -fno-omit-frame-pointer
SRC := $(wildcard src/*.c)
OBJ := $(SRC:src/%.c=build/%.o)
DEP := $(OBJ:.o=.d)
BIN := build/recman
ASAN := build/recman_asan
all: $(BIN)
$(BIN): $(OBJ)
$(CC) $(CFLAGS) $^ -o $@
$(ASAN): $(SRC)
$(CC) $(CFLAGS) $(SANFLAGS) $(CPPFLAGS) $^ -o $@
build/%.o: src/%.c | build
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
build:
mkdir -p build
asan: $(ASAN)
test: $(BIN) $(ASAN)
ASAN_OPTIONS=detect_leaks=0 sh tests/smoke.sh $(BIN)
ASAN_OPTIONS=detect_leaks=0 sh tests/smoke.sh $(ASAN)
$(CC) $(CFLAGS) $(SANFLAGS) $(CPPFLAGS) -Itests tests/unit.c $(filter-out src/main.c,$(SRC)) -o build/unit_asan
ASAN_OPTIONS=detect_leaks=0 ./build/unit_asan
clean:
rm -rf build
-include $(DEP)
.PHONY: all asan test clean

100
README.md Normal file
View file

@ -0,0 +1,100 @@
# Record Manager — Instructor Reference Solution (M1M5)
**PRIVATE — never ships to students.** This is the reference implementation for
the Record Manager cumulative project (`assignments/projects/`), milestone by
milestone, plus the graded-interface contract and grading notes.
## Layout
```
include/record.h M1 record_t + record_new/free + status_t
include/buf.h M2 dynamic string buffer (+ readline)
include/list.h M3 opaque list_t + append/find/remove (+ len/get/sort helpers)
include/store.h M4 store_save / store_load (text file, '|' delimiter)
src/record.c M1 deep-copy record lifecycle
src/buf.c M2 growable buffer: append/appendc/cstr/reset/free/readline
src/list.c M3 singly linked list; remove frees node + record
src/store.c M4 one record per line; load appends + reports count
src/main.c M5 menu (add/list/search/delete/save/load/help/quit)
Makefile builds recman + recman_asan; `make test` runs unit + smoke
tests/unit.c direct tests of buf/list/store (links library sources)
tests/smoke.sh end-to-end CLI session
```
## Build + verify
```bash
make # recman
make asan # recman_asan (ASan+UBSan)
make test # unit + smoke, both under ASan/UBSan
```
Reference must be `-std=c17 -Wall -Wextra -Wpedantic -Werror` clean and report
`0 bytes lost` under Valgrind.
## Graded interface (what hidden tests touch)
`record_new`, `record_free`, `list_new`, `list_free`, `list_append`, `list_find`,
`list_remove`, `store_save`, `store_load` — exact signatures in
`assignments/projects/README.md`. This reference adds three non-graded helpers
(`list_len`, `list_get`, `list_sort`) that the menu and store layer need; they
are extra, not substitutes.
### Contract decisions (documented for graders)
- **`list_remove`**: returns `0` on success, `STATUS_ERR_NOTFOUND` when no record
matches, `STATUS_ERR_ARG` on NULL args. Frees the matching record + node.
- **`store_load`**: returns the **number of records read** (≥ 0) on success;
negative `status_t` on error. "0 on success" from the interface table is
satisfied when the file is empty/blank. It **appends** into the existing list.
- **Delimiter `|`**: none of the three flavors' fields (email/phone, assignment/
score, category/amount) can contain `|`; `name` may contain spaces — the
delimiter is safe across all flavors.
- **Empty fields**: saved as empty strings; loaded back as empty strings
(never NULL from `store_load`).
## Milestone-by-milestone notes
### M2 — dynamic string buffer (week 7)
Hidden tests likely: appends beyond initial capacity, `buf_cstr` NUL-termination
after every append, reset-then-reuse, `buf_readline` on a long line (>256 chars),
EOF/empty-line behavior. Sticky-OOM flag keeps the buffer usable after a failed
realloc. **Common mistake:** forgetting the `+1` for the NUL in `buf_grow`, or
realloc-ing to a shrinking capacity.
### M3 — linked list (week 11)
Hidden tests likely: append to empty list, find across many nodes, remove head /
remove middle / remove missing, `list_free` of a 3-node list under Valgrind
(0 lost), NULL-arg safety. **Common mistakes:** freeing the record and then
walking `next` (order matters — save `next` first); `list_remove` freeing the
node but not the record (double-free later); storing `record_t *` by value
instead of pointer.
### M4 — text file save/load (week 13)
Hidden tests likely: save→load round-trip preserves order + all three fields;
load into a non-empty list appends; file with blank lines; missing file returns
error, not crash; fields containing spaces survive (the `|` delimiter).
**Common mistakes:** assuming `fgets` succeeds; writing with `fprintf` and never
checking `ferror`; not stripping `\n`; `strtok` mangles empty fields (use
`strchr` splitting instead — empty field1/field2 must survive).
### M5 — refactor + search/sort + hardening (week 15)
Hidden tests likely: multi-file build with one `make`; case-insensitive
substring `search` across **all three** fields; `sort` alphabetical by name;
ASan/UBSan-clean; Valgrind 0 lost. **Common mistakes:** search matching only
`name`; sort comparing `record_t*` pointers instead of `->name`; `-Werror`
violations from unused includes after the refactor.
## Grading sanity checks
1. `make` from a clean tree (no build/ artifacts committed).
2. `make asan && ./build/recman_asan` → add 3 records → delete middle → save →
quit → reload → `list` shows the surviving two in order.
3. `valgrind --leak-check=full ./build/recman` with the same session → `0 bytes
lost` (the menu leaks nothing).
4. Feed the `tests/` fixtures: long input line (buf), 10k-record list (list),
save/load with spaces and empties (store).

27
compile_commands.json Normal file
View file

@ -0,0 +1,27 @@
[
{
"directory": "/home/trevon/Documents/work/coursework/instructor/CS2060/record-manager",
"command": "cc -std=c17 -Wall -Wextra -Wpedantic -Werror -g -Iinclude -MMD -MP -c src/main.c -o build/main.o",
"file": "src/main.c"
},
{
"directory": "/home/trevon/Documents/work/coursework/instructor/CS2060/record-manager",
"command": "cc -std=c17 -Wall -Wextra -Wpedantic -Werror -g -Iinclude -MMD -MP -c src/record.c -o build/record.o",
"file": "src/record.c"
},
{
"directory": "/home/trevon/Documents/work/coursework/instructor/CS2060/record-manager",
"command": "cc -std=c17 -Wall -Wextra -Wpedantic -Werror -g -Iinclude -MMD -MP -c src/buf.c -o build/buf.o",
"file": "src/buf.c"
},
{
"directory": "/home/trevon/Documents/work/coursework/instructor/CS2060/record-manager",
"command": "cc -std=c17 -Wall -Wextra -Wpedantic -Werror -g -Iinclude -MMD -MP -c src/list.c -o build/list.o",
"file": "src/list.c"
},
{
"directory": "/home/trevon/Documents/work/coursework/instructor/CS2060/record-manager",
"command": "cc -std=c17 -Wall -Wextra -Wpedantic -Werror -g -Iinclude -MMD -MP -c src/store.c -o build/store.o",
"file": "src/store.c"
}
]

44
hidden/test_hidden.py Normal file
View file

@ -0,0 +1,44 @@
"""Hidden interface test for the Record Manager (blank must fail, solved must pass).
Runs with pytest's cwd = the assembled submission (/work/submission).
Blank (M1 starter) fails because `save`/`load` are unimplemented ("not yet");
the full reference implements the store round-trip.
"""
import pathlib
import subprocess
def _root() -> pathlib.Path:
return pathlib.Path.cwd()
def _run(cmd, timeout=180, **kw):
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, **kw)
def test_record_manager_interface():
root = _root()
build = _run(["make", "-C", str(root), "-j2"])
assert build.returncode == 0, "BUILD FAILED:\n" + build.stdout + build.stderr
recman = root / "build" / "recman"
assert recman.exists(), "build/recman missing after make"
# add two records, save to a file
session1 = (
"add\nAda Lovelace\nada@x\n555-0100\n"
"add\nGrace Hopper\ngrace@x\n444-0200\n"
"save\n/tmp/rm_hidden.txt\nquit\n"
)
r = _run([str(recman)], input=session1)
assert "added: Ada Lovelace" in r.stdout, "add failed:\n" + r.stdout
assert "added: Grace Hopper" in r.stdout, "add failed:\n" + r.stdout
assert "saved" in r.stdout, "save did not persist (blank starter prints 'not yet'):\n" + r.stdout
# load back in a fresh session and confirm the round-trip
session2 = "load\n/tmp/rm_hidden.txt\nlist\nquit\n"
r2 = _run([str(recman)], input=session2)
assert "Ada Lovelace" in r2.stdout and "Grace Hopper" in r2.stdout, (
"save/load round-trip failed:\n" + r2.stdout
)

28
include/buf.h Normal file
View file

@ -0,0 +1,28 @@
#ifndef BUF_H
#define BUF_H
#include <stddef.h>
#include <stdio.h>
/*
* Growable byte buffer (Milestone 2). Holds arbitrary bytes; buf_cstr() always
* returns a NUL-terminated view so the buffer can be used as a string.
* Opaque: callers only see buf_t *.
*/
typedef struct buf buf_t;
buf_t *buf_new(void); /* empty buffer; NULL on OOM */
int buf_append(buf_t *b, const char *s, size_t n); /* append n bytes; 0 ok, -1 OOM */
int buf_appendc(buf_t *b, char c); /* append one byte; 0 ok, -1 OOM */
const char *buf_cstr(const buf_t *b); /* NUL-terminated view; "" if empty */
size_t buf_len(const buf_t *b); /* bytes currently held */
void buf_reset(buf_t *b); /* drop contents, keep allocation */
void buf_free(buf_t *b); /* free buffer + storage */
/*
* Read one full line (including the trailing newline) from f into b.
* Returns the line length (>= 0), -1 at EOF with nothing read, or -2 on error.
*/
long buf_readline(buf_t *b, FILE *f);
#endif /* BUF_H */

22
include/list.h Normal file
View file

@ -0,0 +1,22 @@
#ifndef LIST_H
#define LIST_H
#include "record.h"
/*
* Singly linked list of records (Milestone 3). Opaque handle: callers can
* never touch node fields they go through the functions below.
*/
typedef struct list list_t;
list_t *list_new(void); /* empty list; NULL on OOM */
void list_free(list_t *l); /* frees every node + record */
int list_append(list_t *l, record_t *r); /* add to end; 0 ok, -1 OOM */
record_t *list_find(list_t *l, const char *name);/* exact name match; NULL if none */
int list_remove(list_t *l, const char *name); /* remove + free match; 0 ok,
STATUS_ERR_NOTFOUND if absent */
size_t list_len(const list_t *l); /* number of records */
record_t *list_get(const list_t *l, size_t i); /* i-th record; NULL if OOB */
int list_sort(list_t *l); /* alphabetical by name (M5) */
#endif /* LIST_H */

40
include/record.h Normal file
View file

@ -0,0 +1,40 @@
#ifndef RECORD_H
#define RECORD_H
#include <stddef.h>
/*
* Uniform status codes. Every fallible function returns status_t; check it at
* the call site. STATUS_OK (0) always means success.
*/
typedef enum {
STATUS_OK = 0,
STATUS_ERR_ARG, /* bad input / usage */
STATUS_ERR_NOMEM, /* allocation failed */
STATUS_ERR_IO, /* file or stream error */
STATUS_ERR_NOTFOUND, /* item not present */
} status_t;
/*
* One record. Pick a flavor and map the three fields:
* contacts -> name, email, phone
* gradebook -> student, assignment, score
* expenses -> date, category, amount
*/
typedef struct record {
char *name;
char *field1;
char *field2;
} record_t;
/* Heap-allocate a record with deep copies of the strings (NULL/empty allowed).
Returns NULL on allocation failure. */
record_t *record_new(const char *name, const char *field1, const char *field2);
/* Free a record and its owned strings. */
void record_free(record_t *rec);
/* Human-readable name for a status code (never NULL). */
const char *status_str(status_t s);
#endif /* RECORD_H */

22
include/store.h Normal file
View file

@ -0,0 +1,22 @@
#ifndef STORE_H
#define STORE_H
#include "list.h"
/*
* Text-file persistence (Milestone 4). One record per line, '|' delimiter:
* name|field1|field2
* The delimiter is safe for all three flavors (email/phone, assignment/score,
* category/amount never contain '|'); names may contain spaces.
*/
/* Write the whole list to path. Returns 0 on success, a negative status_t
(STATUS_ERR_IO / STATUS_ERR_ARG) on failure. */
int store_save(const list_t *l, const char *path);
/* Read records from path, APPENDING into *l. Returns the number of records
read (>= 0) on success, or a negative status_t on failure. Blank lines are
skipped. Fields are never NULL after a successful load. */
int store_load(list_t **l, const char *path);
#endif /* STORE_H */

101
src/buf.c Normal file
View file

@ -0,0 +1,101 @@
#include "buf.h"
#include <stdlib.h>
#include <string.h>
struct buf {
char *data;
size_t len; /* bytes used, excluding the trailing NUL */
size_t cap; /* allocated capacity */
int oom; /* sticky OOM flag: after one failed grow, refuse further ops */
};
buf_t *buf_new(void) {
buf_t *b = malloc(sizeof(*b));
if (b == NULL)
return NULL;
b->data = NULL;
b->len = 0;
b->cap = 0;
b->oom = 0;
return b;
}
/* Ensure capacity for `need` bytes (+1 for the NUL). Returns 0 or -1. */
static int buf_grow(buf_t *b, size_t need) {
size_t ncap;
char *nd;
if (b->oom)
return -1;
if (need <= b->cap)
return 0;
ncap = b->cap ? b->cap : 16;
while (ncap < need)
ncap *= 2;
nd = realloc(b->data, ncap);
if (nd == NULL) {
b->oom = 1;
return -1;
}
b->data = nd;
b->cap = ncap;
return 0;
}
int buf_append(buf_t *b, const char *s, size_t n) {
if (b == NULL || (s == NULL && n != 0))
return -1;
if (n == 0)
return 0;
if (buf_grow(b, b->len + n + 1) != 0)
return -1;
memcpy(b->data + b->len, s, n);
b->len += n;
b->data[b->len] = '\0';
return 0;
}
int buf_appendc(buf_t *b, char c) { return buf_append(b, &c, 1); }
const char *buf_cstr(const buf_t *b) {
if (b == NULL || b->oom)
return NULL;
return b->data != NULL ? b->data : "";
}
size_t buf_len(const buf_t *b) { return b != NULL ? b->len : 0; }
void buf_reset(buf_t *b) {
if (b != NULL) {
b->len = 0;
if (b->data != NULL)
b->data[0] = '\0'; /* cstr() now reads as empty */
}
}
void buf_free(buf_t *b) {
if (b == NULL)
return;
free(b->data);
free(b);
}
long buf_readline(buf_t *b, FILE *f) {
int c;
long n = 0;
if (b == NULL || f == NULL)
return -2;
buf_reset(b);
while ((c = fgetc(f)) != EOF) {
if (buf_appendc(b, (char)c) != 0)
return -2;
n++;
if (c == '\n')
break;
}
if (n == 0 && c == EOF)
return -1; /* EOF with nothing read */
return n;
}

113
src/list.c Normal file
View file

@ -0,0 +1,113 @@
#include "list.h"
#include <stdlib.h>
#include <string.h>
typedef struct node {
record_t *rec;
struct node *next;
} node_t;
struct list {
node_t *head;
size_t len;
};
list_t *list_new(void) { return calloc(1, sizeof(list_t)); }
void list_free(list_t *l) {
node_t *n;
node_t *next;
if (l == NULL)
return;
for (n = l->head; n != NULL; n = next) {
next = n->next;
record_free(n->rec);
free(n);
}
free(l);
}
int list_append(list_t *l, record_t *r) {
node_t **tail;
node_t *nn;
if (l == NULL || r == NULL)
return -1;
nn = malloc(sizeof(*nn));
if (nn == NULL)
return -1;
nn->rec = r;
nn->next = NULL;
for (tail = &l->head; *tail != NULL; tail = &(*tail)->next)
;
*tail = nn;
l->len++;
return 0;
}
record_t *list_find(list_t *l, const char *name) {
node_t *n;
if (l == NULL || name == NULL)
return NULL;
for (n = l->head; n != NULL; n = n->next)
if (n->rec->name != NULL && strcmp(n->rec->name, name) == 0)
return n->rec;
return NULL;
}
int list_remove(list_t *l, const char *name) {
node_t **p;
node_t *victim;
if (l == NULL || name == NULL)
return STATUS_ERR_ARG;
for (p = &l->head; *p != NULL; p = &(*p)->next) {
if ((*p)->rec->name != NULL && strcmp((*p)->rec->name, name) == 0) {
victim = *p;
*p = victim->next;
record_free(victim->rec);
free(victim);
l->len--;
return STATUS_OK;
}
}
return STATUS_ERR_NOTFOUND;
}
size_t list_len(const list_t *l) { return l != NULL ? l->len : 0; }
record_t *list_get(const list_t *l, size_t i) {
node_t *n;
if (l == NULL || i >= l->len)
return NULL;
for (n = l->head; i > 0; i--)
n = n->next;
return n->rec;
}
/* Selection sort over record pointers; nodes stay put, records get re-pointed.
Case-sensitive alphabetical order by name (per milestone spec). */
int list_sort(list_t *l) {
node_t *i;
node_t *j;
node_t *min;
if (l == NULL)
return -1;
for (i = l->head; i != NULL && i->next != NULL; i = i->next) {
min = i;
for (j = i->next; j != NULL; j = j->next)
if (strcmp(j->rec->name, min->rec->name) < 0)
min = j;
if (min != i) {
record_t *tmp = i->rec;
i->rec = min->rec;
min->rec = tmp;
}
}
return 0;
}

262
src/main.c Normal file
View file

@ -0,0 +1,262 @@
/*
* Record Manager menu driver (Milestone 5).
* Commands: add, list, search, delete, save, load, help, quit.
* Input lines are read with the dynamic buffer (no fixed-size arrays).
*/
#define _POSIX_C_SOURCE 200809L /* strdup under -std=c17 */
#include <ctype.h>
/* compile_commands.json in this directory carries -Iinclude for clangd. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "buf.h"
#include "list.h"
#include "store.h"
static list_t *g_list;
static void prompt(void) { fputs("recman> ", stdout); fflush(stdout); }
static void trim_newline(char *s) {
size_t n = strlen(s);
if (n > 0 && s[n - 1] == '\n')
s[n - 1] = '\0';
}
static char *lowerdup(const char *s) {
char *p = strdup(s);
char *q;
if (p == NULL)
return NULL;
for (q = p; *q != '\0'; q++)
*q = (char)tolower((unsigned char)*q);
return p;
}
/* Case-insensitive substring match against all three fields (M5). */
static int record_matches(const record_t *r, const char *needle_low) {
const char *fields[3];
size_t i;
char *hay_low;
fields[0] = r->name;
fields[1] = r->field1;
fields[2] = r->field2;
for (i = 0; i < 3; i++) {
if (fields[i] == NULL)
continue;
hay_low = lowerdup(fields[i]);
if (hay_low == NULL)
return 0;
if (strstr(hay_low, needle_low) != NULL) {
free(hay_low);
return 1;
}
free(hay_low);
}
return 0;
}
static void cmd_add(buf_t *in) {
record_t *r;
char *name;
char *f1;
char *f2;
char *s;
fputs("name: ", stdout);
fflush(stdout);
if (buf_readline(in, stdin) < 0)
return;
s = (char *)buf_cstr(in);
trim_newline(s);
name = strdup(s);
fputs("field1: ", stdout);
fflush(stdout);
if (buf_readline(in, stdin) < 0)
return;
s = (char *)buf_cstr(in);
trim_newline(s);
f1 = strdup(s);
fputs("field2: ", stdout);
fflush(stdout);
if (buf_readline(in, stdin) < 0)
return;
s = (char *)buf_cstr(in);
trim_newline(s);
f2 = strdup(s);
r = record_new(name, f1, f2);
free(name);
free(f1);
free(f2);
if (r == NULL || list_append(g_list, r) != 0) {
record_free(r);
puts("error: out of memory");
return;
}
printf("added: %s\n", r->name);
}
static void cmd_list(void) {
size_t i;
size_t n = list_len(g_list);
record_t *r;
if (n == 0) {
puts("(empty)");
return;
}
for (i = 0; i < n; i++) {
r = list_get(g_list, i);
printf("%zu. %s | %s | %s\n", i + 1, r->name ? r->name : "",
r->field1 ? r->field1 : "", r->field2 ? r->field2 : "");
}
}
static void cmd_search(buf_t *in) {
size_t i;
size_t n = list_len(g_list);
record_t *r;
char *needle;
char *needle_low;
int found = 0;
fputs("search: ", stdout);
fflush(stdout);
if (buf_readline(in, stdin) < 0)
return;
needle = (char *)buf_cstr(in);
trim_newline(needle);
needle_low = lowerdup(needle);
if (needle_low == NULL) {
puts("error: out of memory");
return;
}
for (i = 0; i < n; i++) {
r = list_get(g_list, i);
if (record_matches(r, needle_low)) {
printf("%zu. %s | %s | %s\n", i + 1, r->name ? r->name : "",
r->field1 ? r->field1 : "", r->field2 ? r->field2 : "");
found = 1;
}
}
free(needle_low);
if (!found)
puts("(no matches)");
}
static void cmd_delete(buf_t *in) {
char *name;
fputs("delete name: ", stdout);
fflush(stdout);
if (buf_readline(in, stdin) < 0)
return;
name = (char *)buf_cstr(in);
trim_newline(name);
if (list_remove(g_list, name) == STATUS_OK)
printf("deleted: %s\n", name);
else
printf("not found: %s\n", name);
}
static void cmd_save(buf_t *in) {
char *path;
fputs("save to: ", stdout);
fflush(stdout);
if (buf_readline(in, stdin) < 0)
return;
path = (char *)buf_cstr(in);
trim_newline(path);
if (store_save(g_list, path) == STATUS_OK)
printf("saved %zu records\n", list_len(g_list));
else
puts("error: could not save");
}
static void cmd_load(buf_t *in) {
char *path;
int n;
fputs("load from: ", stdout);
fflush(stdout);
if (buf_readline(in, stdin) < 0)
return;
path = (char *)buf_cstr(in);
trim_newline(path);
n = store_load(&g_list, path);
if (n >= 0)
printf("loaded %d records\n", n);
else
puts("error: could not load");
}
static void cmd_sort(void) {
if (list_sort(g_list) == STATUS_OK)
puts("sorted by name");
}
static void cmd_help(void) {
puts("add add a record (name, field1, field2)");
puts("list show all records");
puts("search case-insensitive substring across all fields");
puts("delete remove a record by exact name");
puts("sort sort records alphabetically by name");
puts("save persist records to a text file");
puts("load load records from a text file (appends)");
puts("help this help");
puts("quit exit");
}
int main(void) {
buf_t *in;
char *line;
int quit = 0;
g_list = list_new();
in = buf_new();
if (g_list == NULL || in == NULL) {
fputs("error: out of memory\n", stderr);
return 1;
}
puts("Record Manager — type 'help' for commands.");
while (!quit) {
prompt();
if (buf_readline(in, stdin) < 0)
break; /* EOF */
line = (char *)buf_cstr(in);
trim_newline(line);
if (strcmp(line, "add") == 0)
cmd_add(in);
else if (strcmp(line, "list") == 0)
cmd_list();
else if (strcmp(line, "search") == 0)
cmd_search(in);
else if (strcmp(line, "delete") == 0)
cmd_delete(in);
else if (strcmp(line, "sort") == 0)
cmd_sort();
else if (strcmp(line, "save") == 0)
cmd_save(in);
else if (strcmp(line, "load") == 0)
cmd_load(in);
else if (strcmp(line, "help") == 0)
cmd_help();
else if (strcmp(line, "quit") == 0)
quit = 1;
else if (line[0] != '\0')
printf("unknown command: %s\n", line);
}
list_free(g_list);
buf_free(in);
return 0;
}

60
src/record.c Normal file
View file

@ -0,0 +1,60 @@
#include "record.h"
#include <stdlib.h>
#include <string.h>
static char *dup_str(const char *s) {
size_t n;
char *p;
if (s == NULL)
return NULL;
n = strlen(s) + 1;
p = malloc(n);
if (p != NULL)
memcpy(p, s, n);
return p;
}
record_t *record_new(const char *name, const char *field1, const char *field2) {
record_t *r = malloc(sizeof(*r));
if (r == NULL)
return NULL;
r->name = dup_str(name);
r->field1 = dup_str(field1);
r->field2 = dup_str(field2);
if ((name != NULL && r->name == NULL) || (field1 != NULL && r->field1 == NULL) ||
(field2 != NULL && r->field2 == NULL)) {
record_free(r);
return NULL;
}
return r;
}
void record_free(record_t *rec) {
if (rec == NULL)
return;
free(rec->name);
free(rec->field1);
free(rec->field2);
free(rec);
}
const char *status_str(status_t s) {
switch (s) {
case STATUS_OK:
return "ok";
case STATUS_ERR_ARG:
return "invalid argument";
case STATUS_ERR_NOMEM:
return "out of memory";
case STATUS_ERR_IO:
return "I/O error";
case STATUS_ERR_NOTFOUND:
return "not found";
default:
return "unknown error";
}
}

94
src/store.c Normal file
View file

@ -0,0 +1,94 @@
#include "store.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "buf.h"
#define DELIM '|'
int store_save(const list_t *l, const char *path) {
FILE *f;
size_t i;
size_t n;
record_t *r;
if (l == NULL || path == NULL)
return -STATUS_ERR_ARG;
f = fopen(path, "w");
if (f == NULL)
return -STATUS_ERR_IO;
n = list_len(l);
for (i = 0; i < n; i++) {
r = list_get(l, i);
fprintf(f, "%s%c%s%c%s\n", r->name != NULL ? r->name : "", DELIM,
r->field1 != NULL ? r->field1 : "", DELIM,
r->field2 != NULL ? r->field2 : "");
if (ferror(f)) {
fclose(f);
return -STATUS_ERR_IO;
}
}
if (fclose(f) != 0)
return -STATUS_ERR_IO;
return STATUS_OK;
}
int store_load(list_t **lp, const char *path) {
FILE *f;
buf_t *b;
long n;
int count = 0;
char *line;
char *f1;
char *f2;
char *rest;
size_t len;
record_t *r;
if (lp == NULL || *lp == NULL || path == NULL)
return -STATUS_ERR_ARG;
f = fopen(path, "r");
if (f == NULL)
return -STATUS_ERR_IO;
b = buf_new();
if (b == NULL) {
fclose(f);
return -STATUS_ERR_NOMEM;
}
while ((n = buf_readline(b, f)) >= 0) {
line = (char *)buf_cstr(b);
len = buf_len(b);
if (len > 0 && line[len - 1] == '\n')
line[len - 1] = '\0';
if (line[0] == '\0')
continue; /* skip blank lines */
f1 = strchr(line, DELIM);
if (f1 != NULL)
*f1++ = '\0';
f2 = f1 != NULL ? strchr(f1, DELIM) : NULL;
if (f2 != NULL)
*f2++ = '\0';
rest = f2 != NULL ? f2 : "";
r = record_new(line, f1 != NULL ? f1 : "", rest);
if (r == NULL) {
buf_free(b);
fclose(f);
return -STATUS_ERR_NOMEM;
}
if (list_append(*lp, r) != 0) {
record_free(r);
buf_free(b);
fclose(f);
return -STATUS_ERR_NOMEM;
}
count++;
}
buf_free(b);
if (fclose(f) != 0)
return -STATUS_ERR_IO;
return count; /* records read (>= 0) */
}

View file

@ -0,0 +1,32 @@
"""Public tests for the Record Manager (M1: build + menu basics).
These ship to students and run under the grade harness (pytest, cwd = the
submission). M1 features only: add + list must work after `make` (delete/save/load come in
later milestones the hidden suite gates those).
"""
import pathlib
import subprocess
def _run(cmd, timeout=180, **kw):
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, **kw)
def test_m1_menu_basics():
root = pathlib.Path.cwd()
build = _run(["make", "-C", str(root), "-j2"])
assert build.returncode == 0, "BUILD FAILED:\n" + build.stdout + build.stderr
recman = root / "build" / "recman"
assert recman.exists(), "build/recman missing after make"
session = (
"add\nAda Lovelace\nada@x\n555-0100\n"
"add\nGrace Hopper\ngrace@x\n444-0200\n"
"list\n"
"quit\n"
)
r = _run([str(recman)], input=session)
assert "added: Ada Lovelace" in r.stdout, "add failed:\n" + r.stdout
assert "added: Grace Hopper" in r.stdout, "add failed:\n" + r.stdout
assert "Grace Hopper" in r.stdout, "list did not show the records:\n" + r.stdout

84
tests/smoke.sh Normal file
View file

@ -0,0 +1,84 @@
#!/bin/sh
# End-to-end CLI session for the Record Manager reference.
# Usage: sh tests/smoke.sh <path-to-recman-binary>
set -eu
BIN="${1:?usage: smoke.sh <binary>}"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
run_session() {
"$BIN" <<EOF
add
Ada Lovelace
ada@analytical.engine
555-0100
add
Grace Hopper
grace@navy.mil
444-0200
list
search
hopper
delete
Grace Hopper
list
save
$TMP/data.txt
quit
EOF
}
# added twice
OUT="$(run_session)"
echo "$OUT" | grep -q "added: Ada Lovelace" || {
echo "FAIL: add missing"
exit 1
}
echo "$OUT" | grep -q "added: Grace Hopper" || {
echo "FAIL: add missing"
exit 1
}
# search is case-insensitive substring across fields
if echo "$OUT" | grep -q "grace@navy.mil"; then :; else {
echo "FAIL: search field match"
exit 1
}; fi
# delete reports the removal
if echo "$OUT" | grep -q "deleted: Grace Hopper"; then :; else {
echo "FAIL: delete"
exit 1
}; fi
# save wrote the file
[ -s "$TMP/data.txt" ] || {
echo "FAIL: save produced empty file"
exit 1
}
# reload in a fresh session and confirm the surviving record round-trips
OUT2="$(
"$BIN" <<EOF
load
$TMP/data.txt
list
quit
EOF
)"
echo "$OUT2" | grep -q "loaded 1 records" || {
echo "FAIL: load count"
exit 1
}
echo "$OUT2" | grep -q "Ada Lovelace" || {
echo "FAIL: round-trip name"
exit 1
}
echo "$OUT2" | grep -q "ada@analytical.engine" || {
echo "FAIL: round-trip field1"
exit 1
}
echo "$OUT2" | grep -q "555-0100" || {
echo "FAIL: round-trip field2"
exit 1
}
echo "smoke ($(basename "$BIN")): pass"

92
tests/unit.c Normal file
View file

@ -0,0 +1,92 @@
/* Direct unit tests for the graded interface (buf, list, store). */
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "buf.h"
#include "list.h"
#include "store.h"
#define CHECK(cond) \
do { \
if (!(cond)) { \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
return 1; \
} \
} while (0)
static int test_buf(void) {
buf_t *b = buf_new();
CHECK(b != NULL);
CHECK(strcmp(buf_cstr(b), "") == 0);
CHECK(buf_append(b, "hello", 5) == 0);
CHECK(buf_append(b, " world", 6) == 0);
CHECK(buf_len(b) == 11);
CHECK(strcmp(buf_cstr(b), "hello world") == 0);
buf_reset(b);
CHECK(buf_len(b) == 0);
CHECK(strcmp(buf_cstr(b), "") == 0);
buf_free(b);
return 0;
}
static int test_list(void) {
list_t *l = list_new();
record_t *r;
CHECK(l != NULL);
CHECK(list_len(l) == 0);
CHECK(list_append(l, record_new("zed", "z@x", "1")) == 0);
CHECK(list_append(l, record_new("amy", "a@x", "2")) == 0);
CHECK(list_append(l, record_new("bob", "b@x", "3")) == 0);
CHECK(list_len(l) == 3);
r = list_find(l, "amy");
CHECK(r != NULL && strcmp(r->field1, "a@x") == 0);
CHECK(list_find(l, "nope") == NULL);
CHECK(list_remove(l, "amy") == STATUS_OK);
CHECK(list_len(l) == 2);
CHECK(list_find(l, "amy") == NULL);
CHECK(list_remove(l, "amy") == STATUS_ERR_NOTFOUND);
CHECK(list_remove(l, NULL) == STATUS_ERR_ARG);
list_sort(l);
CHECK(strcmp(list_get(l, 0)->name, "bob") == 0); /* after sort: bob, zed */
CHECK(strcmp(list_get(l, 1)->name, "zed") == 0);
list_free(l);
return 0;
}
static int test_store(void) {
list_t *l = list_new();
list_t *l2 = list_new();
int n;
CHECK(l != NULL && l2 != NULL);
CHECK(list_append(l, record_new("Ada Lovelace", "ada@x", "555-0100")) == 0);
CHECK(list_append(l, record_new("grace", "", "third field with spaces")) == 0);
CHECK(store_save(l, "/tmp/recman_unit_store.txt") == STATUS_OK);
n = store_load(&l2, "/tmp/recman_unit_store.txt");
CHECK(n == 2);
CHECK(list_len(l2) == 2);
CHECK(strcmp(list_get(l2, 0)->name, "Ada Lovelace") == 0);
CHECK(strcmp(list_get(l2, 0)->field2, "555-0100") == 0);
CHECK(strcmp(list_get(l2, 1)->field1, "") == 0);
CHECK(strcmp(list_get(l2, 1)->field2, "third field with spaces") == 0);
/* load appends into a non-empty list */
n = store_load(&l2, "/tmp/recman_unit_store.txt");
CHECK(n == 2 && list_len(l2) == 4);
/* missing file -> error, not crash */
CHECK(store_load(&l2, "/tmp/recman_unit_missing_xyz.txt") < 0);
list_free(l);
list_free(l2);
remove("/tmp/recman_unit_store.txt");
return 0;
}
int main(void) {
if (test_buf() != 0)
return 1;
if (test_list() != 0)
return 1;
if (test_store() != 0)
return 1;
puts("unit: all pass");
return 0;
}