Dengan model data siap, kita mulai mengisi logika CRUD. Pada bagian ini kita mengimplementasikan dua fungsi: contact_tambah untuk menambahkan kontak baru ke daftar, dan contact_tampilkan untuk mencetak seluruh kontak dalam format tabel. Fokus khusus adalah penggunaan strncpy dengan batasan panjang untuk mencegah buffer overflow — sebuah kehati-hatian yang wajib di setiap kali kita menulis ke array karakter fixed-size.

Konsep #

Fungsi contact_tambah menerima tiga string (nama, telp, email) dan menempatkannya ke Contact baru di akhir array data. Setiap kali dipanggil, fungsi memeriksa tiga kondisi: apakah list valid, apakah nama tidak kosong, dan apakah daftar masih punya slot. Hanya setelah ketiga cek lolos, kontak baru ditulis dan jumlah ditambah. ID diambil dari id_berikutnya lalu counter dinaikkan.

Penggunaan strncpy(c->nama, nama, CONTACT_NAMA_MAKS - 1) membatasi salinan ke 49 karakter. strncpy tidak menjamin null terminator bila sumber lebih panjang dari batas, sehingga kita paksa c->nama[CONTACT_NAMA_MAKS - 1] = '\0'; setelahnya. Pola dua langkah ini wajib untuk mencegah string tidak ter-terminasi yang dapat menyebabkan pembacaan berlebih.

Fungsi contact_tampilkan memakai fprintf dengan format lebar tetap %-20s untuk membuat tabel rapi. Tanda minus di depan angka berarti left-align (rata kiri); tanpa minus, kolom akan rata kanan. Header tabel dicetak lebih dulu, lalu iterasi menampilkan setiap kontak.

Implementasi #

Fungsi contact_tambah:

c
ContactStatus contact_tambah(ContactList *list,
                              const char *nama,
                              const char *telp,
                              const char *email) {
    if (list == 0) return CONTACT_ERR_INPUT_INVALID;
    if (!contact_valid_nama(nama)) return CONTACT_ERR_INPUT_INVALID;
    if (list->jumlah >= CONTACT_MAKS) return CONTACT_ERR_PENUH;

    Contact *c = &list->data[list->jumlah];
    c->id = list->id_berikutnya++;
    strncpy(c->nama, nama, CONTACT_NAMA_MAKS - 1);
    c->nama[CONTACT_NAMA_MAKS - 1] = '\0';
    strncpy(c->telp, telp ? telp : "", CONTACT_TELP_MAKS - 1);
    c->telp[CONTACT_TELP_MAKS - 1] = '\0';
    strncpy(c->email, email ? email : "", CONTACT_EMAIL_MAKS - 1);
    c->email[CONTACT_EMAIL_MAKS - 1] = '\0';
    list->jumlah++;
    return CONTACT_OK;
}

Fungsi contact_tampilkan:

c
void contact_tampilkan(const ContactList *list, FILE *out) {
    if (list == 0 || out == 0) return;
    fprintf(out, "%-4s %-20s %-15s %-25s\n",
            "ID", "Nama", "Telp", "Email");
    for (int i = 0; i < list->jumlah; i++) {
        const Contact *c = &list->data[i];
        fprintf(out, "%-4d %-20s %-15s %-25s\n",
                c->id, c->nama, c->telp, c->email);
    }
    fprintf(out, "Total: %d kontak\n", list->jumlah);
}

Fungsi validasi nama:

c
int contact_valid_nama(const char *nama) {
    if (nama == 0 || nama[0] == '\0') return 0;
    if (strlen(nama) >= CONTACT_NAMA_MAKS) return 0;
    return 1;
}

Kode Lengkap #

Berkas contact.c untuk Bagian 2 (hanya fungsi yang relevan; fungsi lain masih stub):

c
#include "contact.h"

#include <string.h>

void contact_list_init(ContactList *list) {
    if (list == 0) return;
    list->jumlah = 0;
    list->id_berikutnya = 1;
}

int contact_valid_nama(const char *nama) {
    if (nama == 0 || nama[0] == '\0') return 0;
    if (strlen(nama) >= CONTACT_NAMA_MAKS) return 0;
    return 1;
}

ContactStatus contact_tambah(ContactList *list,
                              const char *nama,
                              const char *telp,
                              const char *email) {
    if (list == 0) return CONTACT_ERR_INPUT_INVALID;
    if (!contact_valid_nama(nama)) return CONTACT_ERR_INPUT_INVALID;
    if (list->jumlah >= CONTACT_MAKS) return CONTACT_ERR_PENUH;

    Contact *c = &list->data[list->jumlah];
    c->id = list->id_berikutnya++;
    strncpy(c->nama, nama, CONTACT_NAMA_MAKS - 1);
    c->nama[CONTACT_NAMA_MAKS - 1] = '\0';
    strncpy(c->telp, telp ? telp : "", CONTACT_TELP_MAKS - 1);
    c->telp[CONTACT_TELP_MAKS - 1] = '\0';
    strncpy(c->email, email ? email : "", CONTACT_EMAIL_MAKS - 1);
    c->email[CONTACT_EMAIL_MAKS - 1] = '\0';
    list->jumlah++;
    return CONTACT_OK;
}

void contact_tampilkan(const ContactList *list, FILE *out) {
    if (list == 0 || out == 0) return;
    fprintf(out, "%-4s %-20s %-15s %-25s\n",
            "ID", "Nama", "Telp", "Email");
    for (int i = 0; i < list->jumlah; i++) {
        const Contact *c = &list->data[i];
        fprintf(out, "%-4d %-20s %-15s %-25s\n",
                c->id, c->nama, c->telp, c->email);
    }
    fprintf(out, "Total: %d kontak\n", list->jumlah);
}

Pengujian #

Verifikasi tambah dan tampilkan dengan demo cepat:

bash
cat > /tmp/p3b2_demo.c << 'EOF'
#include "contact.h"
#include <stdio.h>
int main(void) {
    ContactList list;
    contact_list_init(&list);
    contact_tambah(&list, "Budi Santoso", "0812345678", "[email protected]");
    contact_tambah(&list, "Siti Rahma", "0898765432", "[email protected]");
    contact_tampilkan(&list, stdout);
    return 0;
}
EOF
gcc -std=c11 -Wall -Wextra -I/home/z/my-project/content/c/proyek-c/proyek-03-manajemen-kontak/ -o /tmp/p3b2_demo /tmp/p3b2_demo.c /home/z/my-project/content/c/proyek-c/proyek-03-manajemen-kontak/contact.c && /tmp/p3b2_demo

Output yang Dihasilkan:

text
ID   Nama                 Telp            Email
1    Budi Santoso         0812345678      [email protected]
2    Siti Rahma           0898765432      [email protected]
Total: 2 kontak

Uji juga penolakan nama kosong:

bash
cat > /tmp/p3b2_invalid.c << 'EOF'
#include "contact.h"
#include <stdio.h>
int main(void) {
    ContactList list;
    contact_list_init(&list);
    ContactStatus s = contact_tambah(&list, "", "0811", "[email protected]");
    printf("status = %d (3 = INPUT_INVALID), jumlah = %d\n", s, list.jumlah);
    return 0;
}
EOF
gcc -std=c11 -Wall -Wextra -I/home/z/my-project/content/c/proyek-c/proyek-03-manajemen-kontak/ -o /tmp/p3b2_invalid /tmp/p3b2_invalid.c /home/z/my-project/content/c/proyek-c/proyek-03-manajemen-kontak/contact.c && /tmp/p3b2_invalid

Output yang Dihasilkan:

text
status = 3 (3 = INPUT_INVALID), jumlah = 0

Mari kita telaah baris-baris kunci pada contact_tambah:

  1. Baris if (list == 0) return CONTACT_ERR_INPUT_INVALID;: pointer NULL check lebih dulu. Tanpa cek ini, dereferensi list->jumlah akan crash.
  2. Baris if (!contact_valid_nama(nama)) return CONTACT_ERR_INPUT_INVALID;: nama wajib diisi. Telp dan email boleh kosong (string kosong "").
  3. Baris if (list->jumlah >= CONTACT_MAKS) return CONTACT_ERR_PENUH;: cek kapasitas sebelum menulis. Setelah return, jumlah tetap tidak naik.
  4. Baris c->id = list->id_berikutnya++;: post-increment — kita pakai nilai dulu, lalu naikkan. ID kontak pertama adalah 1 (sesuai contact_list_init), bukan 0.
  5. Baris strncpy(c->nama, nama, CONTACT_NAMA_MAKS - 1);: salin maksimal 49 karakter. MAKS - 1 menyisakan satu slot untuk null terminator.
  6. Baris c->nama[CONTACT_NAMA_MAKS - 1] = '\0';: jamin terminator ada. strncpy tidak menulis terminator bila sumber lebih panjang dari batas.
  7. Baris strncpy(c->telp, telp ? telp : "", ...);: ekspresi ternary memberi default string kosong bila telp NULL. Ini membuat telp opsional tanpa crash.
  8. Baris list->jumlah++;: baru di akhir, setelah semua field ditulis. Bila ada error di tengah, jumlah tidak naik — menjaga konsistensi array.

Bagian 3 akan menambahkan pencarian dan pengubahan kontak.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

contact.c

Unduh berkas
c
/* contact.c -- implementasi model dan operasi kontak. */

#include "contact.h"

#include <string.h>

void contact_list_init(ContactList *list) {
    if (list == 0) return;
    list->jumlah = 0;
    list->id_berikutnya = 1;
}

int contact_valid_nama(const char *nama) {
    if (nama == 0 || nama[0] == '\0') return 0;
    /* Cek panjang maksimum. */
    if (strlen(nama) >= CONTACT_NAMA_MAKS) return 0;
    return 1;
}

ContactStatus contact_tambah(ContactList *list,
                              const char *nama,
                              const char *telp,
                              const char *email) {
    if (list == 0) return CONTACT_ERR_INPUT_INVALID;
    if (!contact_valid_nama(nama)) return CONTACT_ERR_INPUT_INVALID;
    if (list->jumlah >= CONTACT_MAKS) return CONTACT_ERR_PENUH;

    Contact *c = &list->data[list->jumlah];
    c->id = list->id_berikutnya++;
    strncpy(c->nama, nama, CONTACT_NAMA_MAKS - 1);
    c->nama[CONTACT_NAMA_MAKS - 1] = '\0';
    strncpy(c->telp, telp ? telp : "", CONTACT_TELP_MAKS - 1);
    c->telp[CONTACT_TELP_MAKS - 1] = '\0';
    strncpy(c->email, email ? email : "", CONTACT_EMAIL_MAKS - 1);
    c->email[CONTACT_EMAIL_MAKS - 1] = '\0';
    list->jumlah++;
    return CONTACT_OK;
}

void contact_tampilkan(const ContactList *list, FILE *out) {
    if (list == 0 || out == 0) return;
    fprintf(out, "%-4s %-20s %-15s %-25s\n",
            "ID", "Nama", "Telp", "Email");
    for (int i = 0; i < list->jumlah; i++) {
        const Contact *c = &list->data[i];
        fprintf(out, "%-4d %-20s %-15s %-25s\n",
                c->id, c->nama, c->telp, c->email);
    }
    fprintf(out, "Total: %d kontak\n", list->jumlah);
}

int contact_cari(const ContactList *list, const char *nama) {
    if (list == 0 || nama == 0) return -1;
    for (int i = 0; i < list->jumlah; i++) {
        if (strcmp(list->data[i].nama, nama) == 0) {
            return i;
        }
    }
    return -1;
}

ContactStatus contact_ubah(ContactList *list, int id,
                            const char *nama_baru,
                            const char *telp_baru,
                            const char *email_baru) {
    if (list == 0) return CONTACT_ERR_INPUT_INVALID;
    for (int i = 0; i < list->jumlah; i++) {
        if (list->data[i].id == id) {
            if (nama_baru && !contact_valid_nama(nama_baru)) {
                return CONTACT_ERR_INPUT_INVALID;
            }
            if (nama_baru) {
                strncpy(list->data[i].nama, nama_baru, CONTACT_NAMA_MAKS - 1);
                list->data[i].nama[CONTACT_NAMA_MAKS - 1] = '\0';
            }
            if (telp_baru) {
                strncpy(list->data[i].telp, telp_baru, CONTACT_TELP_MAKS - 1);
                list->data[i].telp[CONTACT_TELP_MAKS - 1] = '\0';
            }
            if (email_baru) {
                strncpy(list->data[i].email, email_baru, CONTACT_EMAIL_MAKS - 1);
                list->data[i].email[CONTACT_EMAIL_MAKS - 1] = '\0';
            }
            return CONTACT_OK;
        }
    }
    return CONTACT_ERR_TIDAK_DITEMUKAN;
}

ContactStatus contact_hapus(ContactList *list, int id) {
    if (list == 0) return CONTACT_ERR_INPUT_INVALID;
    for (int i = 0; i < list->jumlah; i++) {
        if (list->data[i].id == id) {
            /* Geser elemen setelahnya ke depan. */
            for (int j = i; j < list->jumlah - 1; j++) {
                list->data[j] = list->data[j + 1];
            }
            list->jumlah--;
            return CONTACT_OK;
        }
    }
    return CONTACT_ERR_TIDAK_DITEMUKAN;
}

main.c

Unduh berkas
c
/* main.c -- titik masuk aplikasi manajemen kontak. */

#include "contact.h"
#include "storage.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define DB_PATH "kontak.txt"

/* Membaca satu baris dari stdin, memotong newline. */
static void baca_baris(const char *prompt, char *buf, size_t ukuran) {
    printf("%s", prompt);
    if (fgets(buf, (int)ukuran, stdin) == 0) {
        buf[0] = '\0';
        return;
    }
    size_t n = strlen(buf);
    while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == '\r')) {
        buf[--n] = '\0';
    }
}

static int baca_int(const char *prompt, int *out) {
    char buf[32];
    baca_baris(prompt, buf, sizeof(buf));
    char *sisa = 0;
    long v = strtol(buf, &sisa, 10);
    if (sisa == buf) return 0;
    *out = (int)v;
    return 1;
}

static void menu_tambah(ContactList *list) {
    char nama[CONTACT_NAMA_MAKS];
    char telp[CONTACT_TELP_MAKS];
    char email[CONTACT_EMAIL_MAKS];
    baca_baris("Nama  : ", nama, sizeof(nama));
    baca_baris("Telp  : ", telp, sizeof(telp));
    baca_baris("Email : ", email, sizeof(email));

    ContactStatus s = contact_tambah(list, nama, telp, email);
    switch (s) {
        case CONTACT_OK:
            printf("Kontak ditambahkan (id=%d).\n", list->data[list->jumlah - 1].id);
            break;
        case CONTACT_ERR_PENUH:
            printf("Daftar penuh.\n"); break;
        case CONTACT_ERR_INPUT_INVALID:
            printf("Input tidak valid (nama wajib diisi).\n"); break;
        default:
            printf("Error tidak dikenal.\n"); break;
    }
}

static void menu_ubah(ContactList *list) {
    int id;
    if (!baca_int("ID kontak yang diubah: ", &id)) {
        printf("ID tidak valid.\n"); return;
    }
    char nama[CONTACT_NAMA_MAKS];
    char telp[CONTACT_TELP_MAKS];
    char email[CONTACT_EMAIL_MAKS];
    baca_baris("Nama baru (kosongkan jika tidak diubah) : ", nama, sizeof(nama));
    baca_baris("Telp baru (kosongkan jika tidak diubah) : ", telp, sizeof(telp));
    baca_baris("Email baru (kosongkan jika tidak diubah): ", email, sizeof(email));

    ContactStatus s = contact_ubah(list, id,
                                    nama[0] ? nama : 0,
                                    telp[0] ? telp : 0,
                                    email[0] ? email : 0);
    if (s == CONTACT_OK)              printf("Kontak diubah.\n");
    else if (s == CONTACT_ERR_TIDAK_DITEMUKAN) printf("Kontak tidak ditemukan.\n");
    else                              printf("Input tidak valid.\n");
}

static void menu_hapus(ContactList *list) {
    int id;
    if (!baca_int("ID kontak yang dihapus: ", &id)) {
        printf("ID tidak valid.\n"); return;
    }
    ContactStatus s = contact_hapus(list, id);
    if (s == CONTACT_OK)              printf("Kontak dihapus.\n");
    else if (s == CONTACT_ERR_TIDAK_DITEMUKAN) printf("Kontak tidak ditemukan.\n");
}

static void menu_cari(const ContactList *list) {
    char nama[CONTACT_NAMA_MAKS];
    baca_baris("Nama yang dicari: ", nama, sizeof(nama));
    int idx = contact_cari(list, nama);
    if (idx < 0) {
        printf("Kontak dengan nama '%s' tidak ditemukan.\n", nama);
    } else {
        const Contact *c = &list->data[idx];
        printf("Ditemukan: [%d] %s | %s | %s\n",
               c->id, c->nama, c->telp, c->email);
    }
}

int main(void) {
    ContactList list;
    contact_list_init(&list);

    StorageStatus sm = storage_muat(&list, DB_PATH);
    if (sm == STORAGE_OK) {
        printf("Memuat %d kontak dari %s.\n", list.jumlah, DB_PATH);
    } else if (sm == STORAGE_ERR_BUKA_FILE) {
        printf("Belum ada file %s. Memulai dengan daftar kosong.\n", DB_PATH);
    } else {
        printf("Peringatan: format file rusak (kode %d).\n", sm);
    }

    int lanjut = 1;
    while (lanjut) {
        printf("\n=== Manajemen Kontak ===\n");
        printf("1. Tambah\n2. Tampilkan\n3. Cari\n4. Ubah\n5. Hapus\n6. Keluar\n");
        int pilihan = 0;
        if (!baca_int("Pilih: ", &pilihan)) { printf("Pilihan tidak valid.\n"); continue; }

        switch (pilihan) {
            case 1: menu_tambah(&list); break;
            case 2: contact_tampilkan(&list, stdout); break;
            case 3: menu_cari(&list); break;
            case 4: menu_ubah(&list); break;
            case 5: menu_hapus(&list); break;
            case 6: lanjut = 0; break;
            default: printf("Pilihan tidak dikenal.\n"); break;
        }
    }

    StorageStatus ss = storage_simpan(&list, DB_PATH);
    if (ss == STORAGE_OK) {
        printf("Menyimpan %d kontak ke %s.\n", list.jumlah, DB_PATH);
    } else {
        printf("Gagal menyimpan (kode %d).\n", ss);
    }
    printf("Terima kasih.\n");
    return 0;
}

storage.c

Unduh berkas
c
/* storage.c -- persistensi kontak ke file teks. */

#include "storage.h"

#include <stdio.h>
#include <string.h>

/* Memotong newline di akhir string (kalau ada). */
static void potong_newline(char *s) {
    size_t n = strlen(s);
    while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r')) {
        s[--n] = '\0';
    }
}

StorageStatus storage_simpan(const ContactList *list, const char *path) {
    if (list == 0 || path == 0) return STORAGE_ERR_BUKA_FILE;
    FILE *f = fopen(path, "w");
    if (f == 0) return STORAGE_ERR_BUKA_FILE;

    fprintf(f, "%d\n", list->jumlah);
    for (int i = 0; i < list->jumlah; i++) {
        const Contact *c = &list->data[i];
        fprintf(f, "%d|%s|%s|%s\n",
                c->id, c->nama, c->telp, c->email);
    }

    if (fclose(f) != 0) return STORAGE_ERR_TULIS;
    return STORAGE_OK;
}

StorageStatus storage_muat(ContactList *list, const char *path) {
    if (list == 0 || path == 0) return STORAGE_ERR_BUKA_FILE;
    FILE *f = fopen(path, "r");
    if (f == 0) return STORAGE_ERR_BUKA_FILE;

    int jumlah = 0;
    if (fscanf(f, "%d\n", &jumlah) != 1) {
        fclose(f);
        return STORAGE_ERR_FORMAT;
    }
    if (jumlah < 0 || jumlah > CONTACT_MAKS) {
        fclose(f);
        return STORAGE_ERR_FORMAT;
    }

    char baris[256];
    int id_terbesar = list->id_berikutnya - 1;

    for (int i = 0; i < jumlah; i++) {
        if (fgets(baris, sizeof(baris), f) == 0) {
            fclose(f);
            return STORAGE_ERR_FORMAT;
        }
        potong_newline(baris);

        int id;
        char nama[CONTACT_NAMA_MAKS];
        char telp[CONTACT_TELP_MAKS];
        char email[CONTACT_EMAIL_MAKS];

        /* sscanf dengan %[^\|] untuk membaca sampai '|'. */
        int parsed = sscanf(baris, "%d|%49[^|]|%19[^|]|%59[^\n]",
                            &id, nama, telp, email);
        if (parsed < 4) {
            fclose(f);
            return STORAGE_ERR_FORMAT;
        }
        if (list->jumlah >= CONTACT_MAKS) break;

        Contact *c = &list->data[list->jumlah];
        c->id = id;
        strncpy(c->nama, nama, CONTACT_NAMA_MAKS - 1);
        c->nama[CONTACT_NAMA_MAKS - 1] = '\0';
        strncpy(c->telp, telp, CONTACT_TELP_MAKS - 1);
        c->telp[CONTACT_TELP_MAKS - 1] = '\0';
        strncpy(c->email, email, CONTACT_EMAIL_MAKS - 1);
        c->email[CONTACT_EMAIL_MAKS - 1] = '\0';
        list->jumlah++;

        if (id > id_terbesar) id_terbesar = id;
    }

    list->id_berikutnya = id_terbesar + 1;
    fclose(f);
    return STORAGE_OK;
}

tests.c

Unduh berkas
c
/* tests.c -- rangkaian test untuk modul contact dan storage. */

#include "contact.h"
#include "storage.h"

#include <stdio.h>
#include <string.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)

static void test_tambah_dan_tampilkan(void) {
    ContactList list;
    contact_list_init(&list);
    ContactStatus s1 = contact_tambah(&list, "Budi", "0811", "[email protected]");
    ContactStatus s2 = contact_tambah(&list, "Siti", "0812", "[email protected]");
    PERIKSA(s1 == CONTACT_OK && s2 == CONTACT_OK, "Tambah 2 kontak berhasil");
    PERIKSA(list.jumlah == 2, "Jumlah = 2 setelah tambah");
    PERIKSA(list.data[0].id == 1 && list.data[1].id == 2,
           "ID auto-increment 1, 2");
}

static void test_tambah_nama_kosong(void) {
    ContactList list;
    contact_list_init(&list);
    ContactStatus s = contact_tambah(&list, "", "0811", "[email protected]");
    PERIKSA(s == CONTACT_ERR_INPUT_INVALID, "Nama kosong ditolak");
    PERIKSA(list.jumlah == 0, "Tidak ada kontak ditambahkan");
}

static void test_cari(void) {
    ContactList list;
    contact_list_init(&list);
    contact_tambah(&list, "Budi", "0811", "[email protected]");
    contact_tambah(&list, "Siti", "0812", "[email protected]");
    int idx = contact_cari(&list, "Siti");
    PERIKSA(idx == 1, "Cari 'Siti' -> indeks 1");
    PERIKSA(contact_cari(&list, "Tidak Ada") == -1, "Cari nama tidak ada -> -1");
}

static void test_ubah(void) {
    ContactList list;
    contact_list_init(&list);
    contact_tambah(&list, "Budi", "0811", "[email protected]");
    ContactStatus s = contact_ubah(&list, 1, "Budi Santoso", 0, 0);
    PERIKSA(s == CONTACT_OK, "Ubah kontak id=1 berhasil");
    PERIKSA(strcmp(list.data[0].nama, "Budi Santoso") == 0,
           "Nama berubah menjadi 'Budi Santoso'");
    PERIKSA(strcmp(list.data[0].telp, "0811") == 0,
           "Telp tetap '0811' (tidak diubah)");
}

static void test_hapus(void) {
    ContactList list;
    contact_list_init(&list);
    contact_tambah(&list, "Budi", "0811", "[email protected]");
    contact_tambah(&list, "Siti", "0812", "[email protected]");
    contact_tambah(&list, "Andi", "0813", "[email protected]");
    ContactStatus s = contact_hapus(&list, 2);
    PERIKSA(s == CONTACT_OK, "Hapus id=2 berhasil");
    PERIKSA(list.jumlah == 2, "Jumlah = 2 setelah hapus");
    PERIKSA(strcmp(list.data[1].nama, "Andi") == 0,
           "Setelah hapus, elemen 1 = 'Andi'");
    PERIKSA(contact_hapus(&list, 999) == CONTACT_ERR_TIDAK_DITEMUKAN,
           "Hapus id tidak ada -> error");
}

static void test_penyimpanan(void) {
    ContactList list;
    contact_list_init(&list);
    contact_tambah(&list, "Budi", "0811", "[email protected]");
    contact_tambah(&list, "Siti", "0812", "[email protected]");

    StorageStatus ss = storage_simpan(&list, "/tmp/test_kontak.txt");
    PERIKSA(ss == STORAGE_OK, "Simpan ke /tmp/test_kontak.txt");

    ContactList dimuat;
    contact_list_init(&dimuat);
    ss = storage_muat(&dimuat, "/tmp/test_kontak.txt");
    PERIKSA(ss == STORAGE_OK, "Muat dari /tmp/test_kontak.txt");
    PERIKSA(dimuat.jumlah == 2, "Jumlah dimuat = 2");
    PERIKSA(strcmp(dimuat.data[0].nama, "Budi") == 0,
           "Kontak pertama dimuat: 'Budi'");
    PERIKSA(dimuat.id_berikutnya == 3,
           "id_berikutnya = 3 setelah muat");
}

int main(void) {
    test_tambah_dan_tampilkan();
    test_tambah_nama_kosong();
    test_cari();
    test_ubah();
    test_hapus();
    test_penyimpanan();
    printf("\nRingkasan: %d lulus, %d gagal\n", lulus, gagal);
    return gagal == 0 ? 0 : 1;
}