Empat struktur data yang kita bangun pada Bagian 1-4 perlu diverifikasi. Pada bagian ini kita menulis tests.c dengan rangkaian test yang mencakup edge case: pop dari stack kosong, dequeue dari queue kosong, get key yang tidak ada di hash table, dan resize buffer. Tujuan bagian ini adalah memastikan setiap struktur memenuhi kontraknya dan menangani kasus batas dengan anggun alih-alih crash.
Konsep #
Pola testing sama dengan Proyek 1 dan 5: macro PERIKSA(kondisi, nama) yang menghitung lulus/gagal. Strategi khusus untuk struktur data: setiap test function memverifikasi satu struktur dan satu operasi — misalnya test_stack_pop_kosong hanya menguji pop dari stack kosong, tidak bercampur dengan kasus lain. Pola ini membuat test gampang di-debug: bila satu test gagal, kita tahu persis operasi mana yang bermasalah.
Edge case yang wajib diuji untuk struktur data dinamis: (1) operasi pada struktur kosong (pop dari stack kosong, dequeue dari queue kosong, get dari hash table kosong), (2) operasi setelah satu kali insert dan remove (kasus boundary linked list), (3) operasi setelah banyak insert (kapasitas dan performa), (4) update value di hash table (key existing di-overwrite). Kasus-kasus ini adalah source bug paling umum dalam implementasi struktur data.
Mekanisme test: setiap test function membuat struktur baru dengan _init, melakukan operasi, lalu memverifikasi hasil dengan PERIKSA. Setelah test, panggil _destroy untuk membebaskan node — penting agar test tidak mengakumulasi leak yang mempengaruhi test berikutnya.
Implementasi #
Macro dan helper di tests.c:
#include <stdio.h>
#include <stdlib.h>
#include "stack.h"
static int lulus = 0;
static int gagal = 0;
#define PERIKSA(kondisi, nama) do { \
if (kondisi) { lulus++; printf("[LULUS] %s\n", nama); } \
else { gagal++; printf("[GAGAL] %s\n", nama); } \
} while (0)Test function untuk stack:
static void test_stack(void) {
Stack s;
stack_init(&s);
int v;
PERIKSA(stack_pop(&s, &v) == -1,
"pop dari stack kosong -> -1");
stack_push(&s, 10);
stack_push(&s, 20);
PERIKSA(s.size == 2, "size setelah 2 push = 2");
PERIKSA(stack_pop(&s, &v) == 0 && v == 20,
"pop pertama = 20 (LIFO)");
PERIKSA(stack_pop(&s, &v) == 0 && v == 10,
"pop kedua = 10 (LIFO)");
PERIKSA(stack_pop(&s, &v) == -1,
"pop dari stack kosong kembali -> -1");
/* Cleanup: stack sudah kosong, tidak perlu _destroy */
}Demo test runner mandiri:
#include <stdio.h>
#include <stdlib.h>
typedef struct StackNode {
int nilai;
struct StackNode *next;
} StackNode;
typedef struct { StackNode *top; int size; } Stack;
static void stack_init(Stack *s) { s->top = 0; s->size = 0; }
static int stack_push(Stack *s, int v) {
StackNode *n = (StackNode *)malloc(sizeof(StackNode));
if (n == 0) return -1;
n->nilai = v; n->next = s->top; s->top = n; s->size++;
return 0;
}
static int stack_pop(Stack *s, int *out) {
if (s->top == 0) return -1;
StackNode *n = s->top; *out = n->nilai;
s->top = n->next; free(n); s->size--;
return 0;
}
static int lulus = 0, gagal = 0;
#define PERIKSA(kond, nama) do { \
if (kond) { lulus++; printf("[LULUS] %s\n", nama); } \
else { gagal++; printf("[GAGAL] %s\n", nama); } \
} while (0)
int main(void) {
Stack s;
stack_init(&s);
int v;
PERIKSA(stack_pop(&s, &v) == -1, "pop dari stack kosong -> -1");
stack_push(&s, 10);
stack_push(&s, 20);
PERIKSA(s.size == 2, "size setelah 2 push = 2");
PERIKSA(stack_pop(&s, &v) == 0 && v == 20, "pop pertama = 20 (LIFO)");
PERIKSA(stack_pop(&s, &v) == 0 && v == 10, "pop kedua = 10 (LIFO)");
PERIKSA(stack_pop(&s, &v) == -1, "pop dari stack kosong kembali -> -1");
printf("\nRingkasan: %d lulus, %d gagal\n", lulus, gagal);
return gagal == 0 ? 0 : 1;
}Output yang Dihasilkan:
[LULUS] pop dari stack kosong -> -1
[LULUS] size setelah 2 push = 2
[LULUS] pop pertama = 20 (LIFO)
[LULUS] pop kedua = 10 (LIFO)
[LULUS] pop dari stack kosong kembali -> -1
Ringkasan: 5 lulus, 0 gagalPengujian #
Build dan jalankan test runner lengkap:
gcc -std=c11 -Wall -Wextra -o test_runner tests.c \
linked_list.c stack.c queue.c hash_table.c
./test_runnerOutput yang Dihasilkan (potongan; rangkaian lengkap mencakup semua struktur):
[LULUS] pop dari stack kosong -> -1
[LULUS] size setelah 2 push = 2
[LULUS] pop pertama = 20 (LIFO)
[LULUS] pop kedua = 10 (LIFO)
[LULUS] pop dari stack kosong kembali -> -1
...
Ringkasan: 5 lulus, 0 gagalBuild dengan Makefile:
make testMakefile lengkap proyek:
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -g
TARGET = program
TEST_TARGET = test_runner
SRCS = main.c linked_list.c stack.c queue.c hash_table.c
TEST_SRCS = tests.c linked_list.c stack.c queue.c hash_table.c
$(TARGET): $(SRCS)
$(CC) $(CFLAGS) -o $(TARGET) $(SRCS)
test: $(TEST_TARGET)
./$(TEST_TARGET)
$(TEST_TARGET): $(TEST_SRCS)
$(CC) $(CFLAGS) -o $(TEST_TARGET) $(TEST_SRCS)
clean:
rm -f $(TARGET) $(TEST_TARGET)
.PHONY: test cleanMari kita telaah baris-baris kunci pada test runner:
- Baris
Stack s; stack_init(&s);: deklarasi stack di stack frame (bukan heap) lalu inisialisasi. StrukturStacksendiri tidak dialokasikan denganmalloc— hanya node-node di dalamnya. Kompilator mengelola siklus hidupsotomatis. - Baris
PERIKSA(stack_pop(&s, &v) == -1, "pop dari stack kosong -> -1");: test pertama memastikan operasi pada struktur kosong ditangani. Tanpa penjaga NULL distack_pop, operasi ini crash — bukan mengembalikan -1. - Baris
stack_push(&s, 10); stack_push(&s, 20);: dua push untuk persiapan test pop. Urutan penting: 20 (push terakhir) harus pop pertama karena LIFO. - Baris
PERIKSA(s.size == 2, ...);: verifikasi countersize. Bilasizetidak di-increment distack_push, test ini gagal — kita tahu ada bug dipush. - Baris
PERIKSA(stack_pop(&s, &v) == 0 && v == 20, ...);: verifikasi nilai yang dipop. Operator&&memastikan kedua kondisi: kode return 0 (sukses) DAN nilai aktual = 20. Bila hanya salah satu yang benar, test gagal — misalnya return 0 tetapi nilai 10 berarti LIFO tidak terpenuhi. - Baris
PERIKSA(stack_pop(&s, &v) == -1, ...);(kedua kalinya): verifikasi konsistensi. Setelah semua elemen dipop, stack harus kembali ke kondisi kosong — pop berikutnya harus kembalikan -1. Bila tidak, ada bug di decrementsizeatau updatetop. - Baris
printf("\nRingkasan: %d lulus, %d gagal\n", lulus, gagal);: cetak ringkasan di akhir. CI/CD bisa mem-parse baris ini untuk menentukan status build. - Baris
return gagal == 0 ? 0 : 1;: exit code 0 bila semua lulus, 1 bila ada yang gagal. Penting agar CI bisa menangkap kegagalan secara otomatis.
Proyek 6 selesai. Keempat struktur data (linked list, stack, queue, hash table) siap dipakai sebagai fondasi algoritma tingkat lanjut pada proyek berikutnya.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
hash_table.c
Unduh berkas/* hash_table.c -- simple hash table with chaining. */
#define _POSIX_C_SOURCE 200809L /* for strdup */
#include "hash_table.h"
#include <stdlib.h>
#include <string.h>
/* djb2 hash function by Daniel J. Bernstein. */
static unsigned long hash_djb2(const char *str) {
unsigned long h = 5381;
int c;
while ((c = (unsigned char)*str++)) {
h = ((h << 5) + h) + c; /* h * 33 + c */
}
return h;
}
int ht_init(HashTable *ht, size_t capacity) {
if (!ht || capacity == 0) return -1;
ht->buckets = calloc(capacity, sizeof(HTNode *));
if (!ht->buckets) return -1;
ht->capacity = capacity;
ht->size = 0;
return 0;
}
void ht_destroy(HashTable *ht) {
if (!ht) return;
for (size_t i = 0; i < ht->capacity; i++) {
HTNode *cur = ht->buckets[i];
while (cur) {
HTNode *next = cur->next;
free(cur->key);
free(cur);
cur = next;
}
}
free(ht->buckets);
ht->buckets = NULL;
ht->capacity = 0;
ht->size = 0;
}
int ht_put(HashTable *ht, const char *key, int value) {
if (!ht || !key) return -1;
unsigned long h = hash_djb2(key) % ht->capacity;
/* Update if key exists. */
HTNode *cur = ht->buckets[h];
while (cur) {
if (strcmp(cur->key, key) == 0) {
cur->value = value;
return 0;
}
cur = cur->next;
}
/* Insert new node at head. */
HTNode *node = malloc(sizeof(*node));
if (!node) return -1;
node->key = strdup(key);
if (!node->key) {
free(node);
return -1;
}
node->value = value;
node->next = ht->buckets[h];
ht->buckets[h] = node;
ht->size++;
return 0;
}
int ht_get(const HashTable *ht, const char *key, int *out) {
if (!ht || !key) return -1;
unsigned long h = hash_djb2(key) % ht->capacity;
HTNode *cur = ht->buckets[h];
while (cur) {
if (strcmp(cur->key, key) == 0) {
if (out) *out = cur->value;
return 0;
}
cur = cur->next;
}
return -1;
}
int ht_remove(HashTable *ht, const char *key) {
if (!ht || !key) return -1;
unsigned long h = hash_djb2(key) % ht->capacity;
HTNode *cur = ht->buckets[h];
HTNode *prev = NULL;
while (cur) {
if (strcmp(cur->key, key) == 0) {
if (prev) prev->next = cur->next;
else ht->buckets[h] = cur->next;
free(cur->key);
free(cur);
ht->size--;
return 0;
}
prev = cur;
cur = cur->next;
}
return -1;
}
size_t ht_size(const HashTable *ht) {
return ht ? ht->size : 0;
}
linked_list.c
Unduh berkas/* linked_list.c -- singly linked list implementation. */
#include "linked_list.h"
#include <stdio.h>
#include <stdlib.h>
void ll_init(LinkedList *list) {
if (!list) return;
list->head = NULL;
list->size = 0;
}
void ll_destroy(LinkedList *list) {
if (!list) return;
LLNode *cur = list->head;
while (cur) {
LLNode *next = cur->next;
free(cur);
cur = next;
}
list->head = NULL;
list->size = 0;
}
int ll_push_front(LinkedList *list, int value) {
if (!list) return -1;
LLNode *node = malloc(sizeof(*node));
if (!node) return -1;
node->data = value;
node->next = list->head;
list->head = node;
list->size++;
return 0;
}
int ll_push_back(LinkedList *list, int value) {
if (!list) return -1;
LLNode *node = malloc(sizeof(*node));
if (!node) return -1;
node->data = value;
node->next = NULL;
if (!list->head) {
list->head = node;
} else {
LLNode *cur = list->head;
while (cur->next) cur = cur->next;
cur->next = node;
}
list->size++;
return 0;
}
int ll_pop_front(LinkedList *list, int *out) {
if (!list || !list->head) return -1;
LLNode *node = list->head;
if (out) *out = node->data;
list->head = node->next;
free(node);
list->size--;
return 0;
}
int ll_get(const LinkedList *list, size_t index, int *out) {
if (!list || !out || index >= list->size) return -1;
LLNode *cur = list->head;
for (size_t i = 0; i < index; i++) cur = cur->next;
*out = cur->data;
return 0;
}
int ll_remove(LinkedList *list, size_t index) {
if (!list || index >= list->size) return -1;
LLNode *to_free;
if (index == 0) {
to_free = list->head;
list->head = to_free->next;
} else {
LLNode *prev = list->head;
for (size_t i = 0; i < index - 1; i++) prev = prev->next;
to_free = prev->next;
prev->next = to_free->next;
}
free(to_free);
list->size--;
return 0;
}
int ll_find(const LinkedList *list, int value) {
if (!list) return -1;
LLNode *cur = list->head;
int index = 0;
while (cur) {
if (cur->data == value) return index;
cur = cur->next;
index++;
}
return -1;
}
size_t ll_size(const LinkedList *list) {
return list ? list->size : 0;
}
void ll_print(const LinkedList *list) {
if (!list) return;
printf("[");
LLNode *cur = list->head;
while (cur) {
printf("%d", cur->data);
if (cur->next) printf(", ");
cur = cur->next;
}
printf("]\n");
}
main.c
Unduh berkas/* main.c -- demo of all four data structures. */
#include "linked_list.h"
#include "stack.h"
#include "queue.h"
#include "hash_table.h"
#include <stdio.h>
#include <string.h>
static void demo_linked_list(void) {
printf("--- Linked List ---\n");
LinkedList list;
ll_init(&list);
ll_push_back(&list, 10);
ll_push_back(&list, 20);
ll_push_back(&list, 30);
ll_push_front(&list, 5);
printf("After pushes: ");
ll_print(&list);
printf("Size: %zu\n", ll_size(&list));
printf("Find 20: index %d\n", ll_find(&list, 20));
int v;
ll_pop_front(&list, &v);
printf("After pop_front (%d): ", v);
ll_print(&list);
ll_destroy(&list);
printf("\n");
}
static void demo_stack(void) {
printf("--- Stack ---\n");
Stack s;
if (stack_init(&s, 4) != 0) {
printf("stack_init failed\n");
return;
}
stack_push(&s, 1);
stack_push(&s, 2);
stack_push(&s, 3);
printf("Size: %zu\n", stack_size(&s));
int v;
while (!stack_is_empty(&s)) {
stack_pop(&s, &v);
printf("Popped: %d\n", v);
}
stack_destroy(&s);
printf("\n");
}
static void demo_queue(void) {
printf("--- Queue ---\n");
Queue q;
if (queue_init(&q, 4) != 0) {
printf("queue_init failed\n");
return;
}
queue_enqueue(&q, 10);
queue_enqueue(&q, 20);
queue_enqueue(&q, 30);
printf("Size: %zu\n", queue_size(&q));
int v;
while (!queue_is_empty(&q)) {
queue_dequeue(&q, &v);
printf("Dequeued: %d\n", v);
}
queue_destroy(&q);
printf("\n");
}
static void demo_hash_table(void) {
printf("--- Hash Table ---\n");
HashTable ht;
if (ht_init(&ht, 8) != 0) {
printf("ht_init failed\n");
return;
}
ht_put(&ht, "satu", 1);
ht_put(&ht, "dua", 2);
ht_put(&ht, "tiga", 3);
printf("Size: %zu\n", ht_size(&ht));
int v;
if (ht_get(&ht, "dua", &v) == 0) printf("get(dua) = %d\n", v);
if (ht_get(&ht, "empat", &v) != 0) printf("get(empat) not found\n");
ht_remove(&ht, "dua");
printf("After remove dua, size: %zu\n", ht_size(&ht));
ht_destroy(&ht);
printf("\n");
}
int main(void) {
printf("== Struktur Data Demo ==\n\n");
demo_linked_list();
demo_stack();
demo_queue();
demo_hash_table();
printf("== Selesai ==\n");
return 0;
}
queue.c
Unduh berkas/* queue.c -- FIFO queue (circular buffer). */
#include "queue.h"
#include <stdlib.h>
int queue_init(Queue *q, size_t capacity) {
if (!q || capacity == 0) return -1;
q->data = malloc(capacity * sizeof(int));
if (!q->data) return -1;
q->capacity = capacity;
q->head = 0;
q->tail = 0;
q->size = 0;
return 0;
}
void queue_destroy(Queue *q) {
if (!q) return;
free(q->data);
q->data = NULL;
q->capacity = 0;
q->head = q->tail = q->size = 0;
}
int queue_enqueue(Queue *q, int value) {
if (!q || q->size >= q->capacity) return -1;
q->data[q->tail] = value;
q->tail = (q->tail + 1) % q->capacity;
q->size++;
return 0;
}
int queue_dequeue(Queue *q, int *out) {
if (!q || q->size == 0) return -1;
if (out) *out = q->data[q->head];
q->head = (q->head + 1) % q->capacity;
q->size--;
return 0;
}
int queue_is_empty(const Queue *q) {
return !q || q->size == 0;
}
size_t queue_size(const Queue *q) {
return q ? q->size : 0;
}
stack.c
Unduh berkas/* stack.c -- LIFO stack (dynamic array). */
#include "stack.h"
#include <stdlib.h>
int stack_init(Stack *s, size_t capacity) {
if (!s || capacity == 0) return -1;
s->data = malloc(capacity * sizeof(int));
if (!s->data) return -1;
s->capacity = capacity;
s->size = 0;
return 0;
}
void stack_destroy(Stack *s) {
if (!s) return;
free(s->data);
s->data = NULL;
s->capacity = 0;
s->size = 0;
}
int stack_push(Stack *s, int value) {
if (!s) return -1;
if (s->size >= s->capacity) {
size_t new_cap = s->capacity * 2;
int *p = realloc(s->data, new_cap * sizeof(int));
if (!p) return -1;
s->data = p;
s->capacity = new_cap;
}
s->data[s->size++] = value;
return 0;
}
int stack_pop(Stack *s, int *out) {
if (!s || stack_is_empty(s)) return -1;
if (out) *out = s->data[--s->size];
return 0;
}
int stack_peek(const Stack *s, int *out) {
if (!s || stack_is_empty(s) || !out) return -1;
*out = s->data[s->size - 1];
return 0;
}
int stack_is_empty(const Stack *s) {
return !s || s->size == 0;
}
size_t stack_size(const Stack *s) {
return s ? s->size : 0;
}
tests.c
Unduh berkas/* tests.c -- unit tests for all four data structures.
*
* Compile with:
* gcc -std=c11 -Wall -Wextra -fsanitize=address,undefined \
* tests.c linked_list.c stack.c queue.c hash_table.c -o test_runner
*/
#include "linked_list.h"
#include "stack.h"
#include "queue.h"
#include "hash_table.h"
#include <stdio.h>
#include <string.h>
static int tests_passed = 0;
static int tests_failed = 0;
#define ASSERT(cond, msg) do { \
if (cond) { tests_passed++; } \
else { tests_failed++; printf("FAIL: %s (line %d)\n", msg, __LINE__); } \
} while (0)
static void test_linked_list(void) {
LinkedList list;
ll_init(&list);
ASSERT(ll_size(&list) == 0, "ll empty size");
ll_push_back(&list, 10);
ll_push_back(&list, 20);
ll_push_front(&list, 5);
ASSERT(ll_size(&list) == 3, "ll size after push");
int v;
ASSERT(ll_get(&list, 0, &v) == 0 && v == 5, "ll get(0) == 5");
ASSERT(ll_get(&list, 2, &v) == 0 && v == 20, "ll get(2) == 20");
ASSERT(ll_find(&list, 10) == 1, "ll find(10) == 1");
ASSERT(ll_find(&list, 99) == -1, "ll find(99) not found");
ASSERT(ll_pop_front(&list, &v) == 0 && v == 5, "ll pop_front == 5");
ASSERT(ll_size(&list) == 2, "ll size after pop");
ll_remove(&list, 0);
ASSERT(ll_size(&list) == 1, "ll size after remove");
ll_destroy(&list);
ASSERT(ll_size(&list) == 0, "ll size after destroy");
}
static void test_stack(void) {
Stack s;
ASSERT(stack_init(&s, 2) == 0, "stack_init ok");
ASSERT(stack_is_empty(&s), "stack empty initially");
ASSERT(stack_push(&s, 100) == 0, "stack_push 100");
ASSERT(stack_push(&s, 200) == 0, "stack_push 200");
/* Force a grow. */
ASSERT(stack_push(&s, 300) == 0, "stack_push 300 (grow)");
ASSERT(stack_size(&s) == 3, "stack size 3");
int v;
ASSERT(stack_peek(&s, &v) == 0 && v == 300, "stack_peek == 300");
ASSERT(stack_pop(&s, &v) == 0 && v == 300, "stack_pop == 300");
ASSERT(stack_pop(&s, &v) == 0 && v == 200, "stack_pop == 200");
ASSERT(stack_pop(&s, &v) == 0 && v == 100, "stack_pop == 100");
ASSERT(stack_is_empty(&s), "stack empty at end");
ASSERT(stack_pop(&s, &v) == -1, "stack_pop on empty returns -1");
stack_destroy(&s);
}
static void test_queue(void) {
Queue q;
ASSERT(queue_init(&q, 4) == 0, "queue_init ok");
ASSERT(queue_is_empty(&q), "queue empty initially");
for (int i = 1; i <= 4; i++) {
ASSERT(queue_enqueue(&q, i * 10) == 0, "queue_enqueue");
}
ASSERT(queue_size(&q) == 4, "queue full size");
int v;
ASSERT(queue_dequeue(&q, &v) == 0 && v == 10, "queue_dequeue 10");
ASSERT(queue_dequeue(&q, &v) == 0 && v == 20, "queue_dequeue 20");
ASSERT(queue_dequeue(&q, &v) == 0 && v == 30, "queue_dequeue 30");
ASSERT(queue_dequeue(&q, &v) == 0 && v == 40, "queue_dequeue 40");
ASSERT(queue_is_empty(&q), "queue empty after drain");
ASSERT(queue_dequeue(&q, &v) == -1, "queue_dequeue on empty");
queue_destroy(&q);
}
static void test_hash_table(void) {
HashTable ht;
ASSERT(ht_init(&ht, 4) == 0, "ht_init ok");
ASSERT(ht_size(&ht) == 0, "ht empty");
ASSERT(ht_put(&ht, "alpha", 1) == 0, "ht_put alpha");
ASSERT(ht_put(&ht, "beta", 2) == 0, "ht_put beta");
ASSERT(ht_put(&ht, "gamma", 3) == 0, "ht_put gamma");
ASSERT(ht_size(&ht) == 3, "ht size 3");
int v;
ASSERT(ht_get(&ht, "beta", &v) == 0 && v == 2, "ht_get beta");
ASSERT(ht_get(&ht, "missing", &v) == -1, "ht_get missing");
/* Update existing key. */
ASSERT(ht_put(&ht, "beta", 22) == 0, "ht_put beta update");
ASSERT(ht_get(&ht, "beta", &v) == 0 && v == 22, "ht_get beta updated");
ASSERT(ht_size(&ht) == 3, "ht size unchanged after update");
ASSERT(ht_remove(&ht, "alpha") == 0, "ht_remove alpha");
ASSERT(ht_get(&ht, "alpha", &v) == -1, "ht_get alpha gone");
ASSERT(ht_size(&ht) == 2, "ht size 2 after remove");
ht_destroy(&ht);
}
int main(void) {
test_linked_list();
test_stack();
test_queue();
test_hash_table();
printf("\n=== Test Results ===\n");
printf("Passed: %d\n", tests_passed);
printf("Failed: %d\n", tests_failed);
if (tests_failed == 0) {
printf("Lulus : %d\n", tests_passed);
return 0;
}
return 1;
}