This commit is contained in:
commit
8c08fb626f
21 changed files with 1199 additions and 0 deletions
34
.forgejo/workflows/grade.yml
Normal file
34
.forgejo/workflows/grade.yml
Normal 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
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
build/
|
||||
.cache/
|
||||
*.o
|
||||
*.d
|
||||
40
Makefile
Normal file
40
Makefile
Normal 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
100
README.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# Record Manager — Instructor Reference Solution (M1–M5)
|
||||
|
||||
**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
27
compile_commands.json
Normal 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"
|
||||
}
|
||||
]
|
||||
BIN
hidden/__pycache__/test_hidden.cpython-314-pytest-9.1.1.pyc
Normal file
BIN
hidden/__pycache__/test_hidden.cpython-314-pytest-9.1.1.pyc
Normal file
Binary file not shown.
44
hidden/test_hidden.py
Normal file
44
hidden/test_hidden.py
Normal 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
28
include/buf.h
Normal 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
22
include/list.h
Normal 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
40
include/record.h
Normal 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
22
include/store.h
Normal 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
101
src/buf.c
Normal 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
113
src/list.c
Normal 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
262
src/main.c
Normal 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
60
src/record.c
Normal 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
94
src/store.c
Normal 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) */
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
32
tests/public/test_public.py
Normal file
32
tests/public/test_public.py
Normal 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
84
tests/smoke.sh
Normal 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
92
tests/unit.c
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue