commit
1a650d4aaa
13 changed files with 507 additions and 0 deletions
7
.clang-format
Normal file
7
.clang-format
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Clean C formatting: LLVM base, 4-space indent, 100 columns.
|
||||||
|
# Run `clang-format -i src/*.c include/*.h` before submitting.
|
||||||
|
# Reference: https://clang.llvm.org/docs/ClangFormatStyleOptions.html
|
||||||
|
BasedOnStyle: LLVM
|
||||||
|
IndentWidth: 4
|
||||||
|
ColumnLimit: 100
|
||||||
|
UseTab: Never
|
||||||
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
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
build/
|
||||||
|
*.o
|
||||||
|
*.d
|
||||||
46
AGENTS.md
Normal file
46
AGENTS.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# AGENTS.md — Record Manager (student repo)
|
||||||
|
|
||||||
|
This file is read by **any coding agent** the student uses. Follow it exactly.
|
||||||
|
|
||||||
|
## Task
|
||||||
|
|
||||||
|
Extend this starter into the course's cumulative **Record Manager** project,
|
||||||
|
one milestone at a time. You are helping the student implement the **common
|
||||||
|
interface** exactly as specified in the assignment README — do not rename,
|
||||||
|
reorder, or remove any function signature; the autograder calls them by name.
|
||||||
|
|
||||||
|
## Verification-first (non-negotiable)
|
||||||
|
|
||||||
|
1. Before every push: `make` and `make test` must pass.
|
||||||
|
2. Milestone 5: `make sanitize` (ASan/UBSan) and `make valgrind` must be clean
|
||||||
|
(`0 bytes lost`, `0 errors`).
|
||||||
|
3. The **CI report on Forgejo is the official gate** — a green public test is
|
||||||
|
not enough; the hidden suite decides. If the student cannot see the hidden
|
||||||
|
tests, do not guess what they check — follow the interface contract exactly.
|
||||||
|
4. Never submit code you cannot trace: if you generate C, the student must be
|
||||||
|
able to explain every line you kept.
|
||||||
|
|
||||||
|
## Tier 2 AI rules (course policy)
|
||||||
|
|
||||||
|
Permitted — helping with:
|
||||||
|
|
||||||
|
- explaining compiler diagnostics / GCC-Clang warning flags
|
||||||
|
- generating test input fixtures or Makefile templates
|
||||||
|
- brainstorming algorithms (e.g., linked-list edge cases, save/load formats)
|
||||||
|
|
||||||
|
Prohibited:
|
||||||
|
|
||||||
|
- use on in-class quizzes, the midterm, or the final (Tier 0)
|
||||||
|
- submitting unverified AI-generated C: unsafe patterns (bare `strcpy`,
|
||||||
|
missing null terminators, uninitialized dereferences, buffer overruns)
|
||||||
|
are the exact failure modes this course teaches — validate with the gates
|
||||||
|
above, and fix what the student fixes with you.
|
||||||
|
|
||||||
|
## House rules
|
||||||
|
|
||||||
|
- Keep the verification targets in the Makefile (`test`, `sanitize`, `valgrind`).
|
||||||
|
- The student records **"AI used: none | Tier 2"** at the top of each milestone
|
||||||
|
write-up (`MILESTONE.md`) — help them be accurate, not evasive.
|
||||||
|
- Do not modify `.forgejo/workflows/grade.yml`.
|
||||||
|
- Write the milestone write-up in the student's voice, from their work — never
|
||||||
|
fabricate verification evidence that wasn't actually run.
|
||||||
51
MILESTONE.md
Normal file
51
MILESTONE.md
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Milestone Write-up — M<N>: <milestone name>
|
||||||
|
|
||||||
|
> Copy this file per milestone (e.g. `MILESTONE-M3.md`) and fill it in. The
|
||||||
|
> flavor line below is required **every** milestone, not just the first.
|
||||||
|
|
||||||
|
## Flavor
|
||||||
|
|
||||||
|
(contacts book | gradebook | expense tracker) — same flavor for every milestone
|
||||||
|
|
||||||
|
## AI used
|
||||||
|
|
||||||
|
(none | Tier 2) — see `AGENTS.md` for what Tier 2 permits
|
||||||
|
|
||||||
|
## What I built
|
||||||
|
|
||||||
|
Which common-interface functions are now implemented, in my own words
|
||||||
|
(e.g. "`list_append`/`list_find`/`list_remove` over a singly linked list, with
|
||||||
|
`list_free` walking and freeing every node and record"). Name the files.
|
||||||
|
|
||||||
|
## Verification evidence
|
||||||
|
|
||||||
|
Paste the output of the gates for this milestone — and the CI report link from
|
||||||
|
Forgejo Actions:
|
||||||
|
|
||||||
|
```text
|
||||||
|
make
|
||||||
|
make test
|
||||||
|
make sanitize # ASan/UBSan (M5+)
|
||||||
|
make valgrind # 0 bytes lost (M5)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pre-tool prediction
|
||||||
|
|
||||||
|
Before running the tool/CI: what did you predict would happen, and which test
|
||||||
|
did you expect to fail? (If nothing failed — say what made you confident.)
|
||||||
|
|
||||||
|
## Student-authored challenge case
|
||||||
|
|
||||||
|
A test or scenario **you** wrote that the starter didn't cover (e.g. remove the
|
||||||
|
head node, a 10k-record list, empty fields in save/load). What does it check
|
||||||
|
and why does it matter?
|
||||||
|
|
||||||
|
## Novel transfer
|
||||||
|
|
||||||
|
A new use of what you built this milestone — apply the linked list / buffer /
|
||||||
|
store to a different domain (e.g. "if the record were a todo item instead of a
|
||||||
|
contact…") and sketch how your code would change.
|
||||||
|
|
||||||
|
## Reflection
|
||||||
|
|
||||||
|
What broke, what you learned, what you'd do differently next milestone.
|
||||||
45
Makefile
Normal file
45
Makefile
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Record Manager — build, test, and verification targets.
|
||||||
|
#
|
||||||
|
# Best-practice references:
|
||||||
|
# - GNU Make automatic prerequisites: https://www.gnu.org/software/make/manual/html_node/Automatic-Prerequisites.html
|
||||||
|
# - GCC instrumentation (sanitizers): https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html
|
||||||
|
|
||||||
|
CC ?= cc
|
||||||
|
CFLAGS ?= -std=c17 -Wall -Wextra -Wpedantic -Werror -g
|
||||||
|
CPPFLAGS += -Iinclude -MMD -MP
|
||||||
|
LDLIBS ?=
|
||||||
|
|
||||||
|
SRC := $(wildcard src/*.c)
|
||||||
|
OBJ := $(SRC:src/%.c=build/%.o)
|
||||||
|
DEP := $(OBJ:.o=.d)
|
||||||
|
BIN := build/recman
|
||||||
|
|
||||||
|
all: $(BIN)
|
||||||
|
|
||||||
|
$(BIN): $(OBJ)
|
||||||
|
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
|
||||||
|
|
||||||
|
build/%.o: src/%.c | build
|
||||||
|
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
build:
|
||||||
|
mkdir -p build
|
||||||
|
|
||||||
|
test: $(BIN)
|
||||||
|
sh tests/smoke.sh $(BIN)
|
||||||
|
|
||||||
|
# ASan + UBSan — zero tolerance for memory and undefined-behavior errors.
|
||||||
|
sanitize:
|
||||||
|
$(CC) $(CFLAGS) -fsanitize=address,undefined $(CPPFLAGS) $(SRC) -o build/recman_asan $(LDLIBS)
|
||||||
|
sh tests/smoke.sh build/recman_asan
|
||||||
|
|
||||||
|
valgrind: $(BIN)
|
||||||
|
@command -v valgrind >/dev/null 2>&1 || { echo "error: valgrind not installed (see toolchain setup)"; exit 1; }
|
||||||
|
printf 'add Ada ada@x.io 555-0100\nlist\nquit\n' | valgrind --leak-check=full --show-leak-kinds=all --error-exitcode=1 $(BIN) >/dev/null
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf build
|
||||||
|
|
||||||
|
-include $(DEP)
|
||||||
|
|
||||||
|
.PHONY: all test sanitize valgrind clean
|
||||||
51
README.md
Normal file
51
README.md
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Record Manager — starter template
|
||||||
|
|
||||||
|
Clean, best-practice starting point for the CS 2060 cumulative project.
|
||||||
|
Pick a flavor (contacts / gradebook / expense tracker) and extend it milestone
|
||||||
|
by milestone; do not delete the verification targets.
|
||||||
|
|
||||||
|
**Before your first commit:** read [`SETUP.md`](SETUP.md) (toolchain) and
|
||||||
|
[`AGENTS.md`](AGENTS.md) (the rules every coding agent reads), copy
|
||||||
|
[`MILESTONE.md`](MILESTONE.md) per milestone as your write-up.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
include/record.h the record_t struct + record_new/record_free + status codes
|
||||||
|
src/record.c record implementation
|
||||||
|
src/main.c CLI menu (Milestone 1)
|
||||||
|
tests/smoke.sh dependency-free smoke tests for the CLI
|
||||||
|
Makefile build + test + sanitizer targets
|
||||||
|
.clang-format enforced formatting (clang-format -i src/*.c include/*.h)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Targets
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
| :--- | :--- |
|
||||||
|
| `make` | build `build/recman` (`-std=c17 -Wall -Wextra -Wpedantic -Werror -g`) |
|
||||||
|
| `make test` | run the smoke tests |
|
||||||
|
| `make sanitize` | rebuild with ASan+UBSan and run the smoke tests |
|
||||||
|
| `make valgrind` | run under Valgrind Memcheck (leaks → non-zero exit) |
|
||||||
|
| `make clean` | remove `build/` |
|
||||||
|
|
||||||
|
## Style & best-practice references
|
||||||
|
|
||||||
|
- **Formatting** — `.clang-format` (LLVM base, 4-space, 100 cols). Reference: <https://clang.llvm.org/docs/ClangFormatStyleOptions.html>
|
||||||
|
- **Readable C** — Linux kernel coding style: <https://www.kernel.org/doc/html/latest/process/coding-style.html>
|
||||||
|
- **Robust error handling** — return `status_t`, check it at the call site. Reference: SEI CERT C, API04-C / ERR33-C (<https://wiki.sei.cmu.edu/confluence/display/c/>)
|
||||||
|
- **Build hygiene** — automatic header deps via `-MMD -MP`. Reference: GNU Make, Automatic Prerequisites (<https://www.gnu.org/software/make/manual/html_node/Automatic-Prerequisites.html>)
|
||||||
|
- **Memory safety** — sanitizers + Valgrind. Reference: GCC Instrumentation Options (<https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html>)
|
||||||
|
- **Course textbook** — Beej's Guide to C: <https://beej.us/guide/bgc/>
|
||||||
|
|
||||||
|
## Milestones
|
||||||
|
|
||||||
|
The five-milestone spec (M1 CLI → M2 buffer → M3 list → M4 file → M5 refactor)
|
||||||
|
lives in `../README.md` (the `assignments/projects/` folder).
|
||||||
|
|
||||||
|
## AI use (optional)
|
||||||
|
|
||||||
|
AI assistance is **not required**. If you use it, the standard `AGENTS.md` in
|
||||||
|
this repo tells the agent exactly what Tier 2 permits; state "AI used: none |
|
||||||
|
Tier 2" at the top of each milestone write-up and let the verification gates
|
||||||
|
(`make test` / `make sanitize` / `make valgrind` + the CI report) be the record.
|
||||||
41
SETUP.md
Normal file
41
SETUP.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Setup — CS 2060 toolchain
|
||||||
|
|
||||||
|
Everything required is **free**. Full detail (macOS/Replit paths, debugging
|
||||||
|
setup, help sources): the course [`setup.md`](../setup.md).
|
||||||
|
|
||||||
|
## 1. Editor + toolchain (the fast path)
|
||||||
|
|
||||||
|
| Platform | What to install |
|
||||||
|
| :--- | :--- |
|
||||||
|
| Windows | **VS Code + WSL2** (the compiler lives in Linux): `wsl --install -d Ubuntu`, then in VS Code add the **Remote - WSL** extension and open this repo in WSL |
|
||||||
|
| macOS | **VS Code** + `xcode-select --install` (gives `clang` + `make`) |
|
||||||
|
| Linux | VS Code (or any editor) |
|
||||||
|
|
||||||
|
**Windows: use WSL2, not Visual Studio/MSVC** — the autograder is Linux + GCC +
|
||||||
|
`make` + Valgrind, and milestone 5 requires Valgrind (Linux-only).
|
||||||
|
|
||||||
|
## 2. Compiler & tools (Linux / WSL2 / Codespaces)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update && sudo apt install -y build-essential gcc clang make gdb valgrind git
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Debugging in VS Code (gdb)
|
||||||
|
|
||||||
|
1. Install the **C/C++ extension** (`ms-vscode.cpptools`).
|
||||||
|
2. Open this folder (in WSL on Windows).
|
||||||
|
3. Add `.vscode/tasks.json` (build: `make`) and `.vscode/launch.json` (gdb,
|
||||||
|
`program: ${workspaceFolder}/build/recman`, `preLaunchTask: make`) — the
|
||||||
|
course repo ships copies.
|
||||||
|
4. **F5** → build + debug. No VS Code? Terminal: `gdb ./build/recman`, then
|
||||||
|
`break record_new`, `run`, `print r->name`, `backtrace`.
|
||||||
|
|
||||||
|
## 4. Verify before you start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gcc --version && make --version && gdb --version && valgrind --version && git --version
|
||||||
|
make && make test # the starter must build and pass
|
||||||
|
```
|
||||||
|
|
||||||
|
If anything fails, fix the environment in week 1 — that's what office hours are
|
||||||
|
for (CYBR A120-L).
|
||||||
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 */
|
||||||
82
src/main.c
Normal file
82
src/main.c
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Record Manager — entry point (Milestone 1).
|
||||||
|
*
|
||||||
|
* A menu-driven CLI for a simple record keeper. Pick a flavor (contacts,
|
||||||
|
* gradebook, or expense tracker) and add / list records. Later milestones add
|
||||||
|
* the linked list, text-file persistence, and search/sort.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "record.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define LINE_MAX 1024
|
||||||
|
|
||||||
|
static void print_help(void);
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
char line[LINE_MAX];
|
||||||
|
|
||||||
|
print_help();
|
||||||
|
for (;;) {
|
||||||
|
printf("> ");
|
||||||
|
fflush(stdout);
|
||||||
|
if (fgets(line, sizeof(line), stdin) == NULL)
|
||||||
|
break;
|
||||||
|
line[strcspn(line, "\n")] = '\0';
|
||||||
|
|
||||||
|
if (strncmp(line, "add ", 4) == 0) {
|
||||||
|
char name[LINE_MAX], f1[LINE_MAX], f2[LINE_MAX];
|
||||||
|
char rest[LINE_MAX];
|
||||||
|
int n;
|
||||||
|
record_t *rec;
|
||||||
|
|
||||||
|
strncpy(rest, line + 4, sizeof(rest) - 1);
|
||||||
|
rest[sizeof(rest) - 1] = '\0';
|
||||||
|
n = sscanf(rest, "%1023s %1023s %1023s", name, f1, f2);
|
||||||
|
if (n < 2) {
|
||||||
|
printf("usage: add <name> <field1> [field2]\n");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rec = record_new(name, f1, (n >= 3) ? f2 : "");
|
||||||
|
if (rec == NULL) {
|
||||||
|
printf("error: %s\n", status_str(STATUS_ERR_NOMEM));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
printf("added: %s | %s | %s\n", rec->name, rec->field1, rec->field2);
|
||||||
|
/* TODO(milestone 3): append `rec` to a linked list instead of freeing. */
|
||||||
|
record_free(rec);
|
||||||
|
} else if (strcmp(line, "list") == 0) {
|
||||||
|
printf("(list is empty — milestone 3 adds records)\n");
|
||||||
|
} else if (strncmp(line, "search", 6) == 0) {
|
||||||
|
printf("(search — milestone 5)\n");
|
||||||
|
} else if (strncmp(line, "delete", 6) == 0) {
|
||||||
|
printf("(delete — milestone 3)\n");
|
||||||
|
} else if (strncmp(line, "save", 4) == 0) {
|
||||||
|
printf("(save — milestone 4)\n");
|
||||||
|
} else if (strncmp(line, "load", 4) == 0) {
|
||||||
|
printf("(load — milestone 4)\n");
|
||||||
|
} else if (strcmp(line, "help") == 0) {
|
||||||
|
print_help();
|
||||||
|
} else if (strcmp(line, "quit") == 0 || strcmp(line, "exit") == 0) {
|
||||||
|
break;
|
||||||
|
} else if (line[0] != '\0') {
|
||||||
|
printf("unknown command '%s' — try 'help'\n", line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_help(void) {
|
||||||
|
printf("Record Manager — commands:\n"
|
||||||
|
" add <name> <field1> [field2] add a record\n"
|
||||||
|
" list list all records\n"
|
||||||
|
" search <text> search records\n"
|
||||||
|
" delete <name> delete a record\n"
|
||||||
|
" save <file> save to a text file\n"
|
||||||
|
" load <file> load from a text file\n"
|
||||||
|
" help show this help\n"
|
||||||
|
" quit exit\n");
|
||||||
|
}
|
||||||
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
15
tests/smoke.sh
Normal file
15
tests/smoke.sh
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Smoke test for the Record Manager CLI.
|
||||||
|
# Usage: smoke.sh <path-to-binary>
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
BIN="${1:-build/recman}"
|
||||||
|
|
||||||
|
out=$(printf 'help\nadd Ada ada@x.io 555-0100\nquit\n' | "$BIN")
|
||||||
|
|
||||||
|
echo "$out" | grep -q "Record Manager — commands"
|
||||||
|
echo "$out" | grep -q "added: Ada"
|
||||||
|
|
||||||
|
printf 'bogus\nquit\n' | "$BIN" | grep -q "unknown command"
|
||||||
|
|
||||||
|
echo "smoke tests passed"
|
||||||
Loading…
Add table
Reference in a new issue