Persistensi tugas ke file mirip dengan Proyek 03, tetapi dengan satu tantangan tambahan: menyimpan nilai enum (TaskStatus, TaskPrioritas) ke file teks. Karena file teks hanya menyimpan karakter, kita perlu meng-cast enum ke int saat menulis dan meng-cast kembali ke enum saat membaca. Pola ini aman selama rentang integer valid dan tidak ada perubahan urutan enum antara versi file.

Konsep #

Enum di C sebenarnya disimpan sebagai integer di memori. TASK_STATUS_BELUM = 0, TASK_STATUS_PROSES = 1, TASK_STATUS_SELESAI = 2. Saat menulis ke file, kita pakai fprintf(f, "%d", (int)t->status); yang menghasilkan teks “0”, “1”, atau “2”. Saat membaca, sscanf memparse teks kembali ke int, lalu kita cast ke enum dengan (TaskStatus)i.

Risiko pola ini: bila urutan enum berubah di versi mendatang (mis. menyisipkan TASK_STATUS_DITUNDA di tengah), file lama akan diparse ke enum yang salah. Mitigasi: (1) hanya menambah enum di akhir, tidak menyisipkan; (2) memakai kode string (“BELUM”, “PROSES”) sebagai pengganti integer. Untuk aplikasi kita yang sederhana, integer sudah cukup.

Format file: baris pertama berisi jumlah tugas, lalu setiap baris berisi satu tugas dengan field dipisahkan |: id|judul|deskripsi|status|prioritas. Field deskripsi boleh kosong tetapi pembatas | tetap ada — penting agar sscanf dapat memparse lima field dengan benar.

Implementasi #

Fungsi storage_simpan:

c
StorageStatus storage_simpan(const TaskList *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 Task *t = &list->data[i];
        fprintf(f, "%d|%s|%s|%d|%d\n",
                t->id, t->judul, t->deskripsi,
                (int)t->status, (int)t->prioritas);
    }

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

Parsing saat memuat memakai sscanf dengan format %d|%79[^|]|%199[^|]|%d|%d:

c
int parsed = sscanf(baris, "%d|%79[^|]|%199[^|]|%d|%d",
                    &id, judul, deskripsi, &status, &prioritas);
if (parsed < 5) {
    fclose(f);
    return STORAGE_ERR_FORMAT;
}
/* ... cast ke enum dan tulis ke list ... */
t->status = (TaskStatus)status;
t->prioritas = (TaskPrioritas)prioritas;

Kode Lengkap #

Berkas storage.h:

c
#ifndef STORAGE_H
#define STORAGE_H

#include "task.h"

typedef enum {
    STORAGE_OK = 0,
    STORAGE_ERR_BUKA_FILE = 1,
    STORAGE_ERR_TULIS = 2,
    STORAGE_ERR_FORMAT = 3
} StorageStatus;

StorageStatus storage_simpan(const TaskList *list, const char *path);
StorageStatus storage_muat(TaskList *list, const char *path);

#endif /* STORAGE_H */

Berkas storage.c lengkap:

c
#include "storage.h"

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

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 TaskList *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 Task *t = &list->data[i];
        fprintf(f, "%d|%s|%s|%d|%d\n",
                t->id, t->judul, t->deskripsi,
                (int)t->status, (int)t->prioritas);
    }

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

StorageStatus storage_muat(TaskList *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 > TASK_MAKS) {
        fclose(f);
        return STORAGE_ERR_FORMAT;
    }

    char baris[512];
    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, status, prioritas;
        char judul[TASK_JUDUL_MAKS];
        char deskripsi[TASK_DESKRIPSI_MAKS];

        int parsed = sscanf(baris, "%d|%79[^|]|%199[^|]|%d|%d",
                            &id, judul, deskripsi, &status, &prioritas);
        if (parsed < 5) {
            fclose(f);
            return STORAGE_ERR_FORMAT;
        }
        if (list->jumlah >= TASK_MAKS) break;

        Task *t = &list->data[list->jumlah];
        t->id = id;
        strncpy(t->judul, judul, TASK_JUDUL_MAKS - 1);
        t->judul[TASK_JUDUL_MAKS - 1] = '\0';
        strncpy(t->deskripsi, deskripsi, TASK_DESKRIPSI_MAKS - 1);
        t->deskripsi[TASK_DESKRIPSI_MAKS - 1] = '\0';
        t->status = (TaskStatus)status;
        t->prioritas = (TaskPrioritas)prioritas;
        list->jumlah++;

        if (id > id_terbesar) id_terbesar = id;
    }

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

Pengujian #

Demo round-trip simpan-muat dengan enum:

bash
cat > /tmp/p4b4_demo.c << 'EOF'
#include "task.h"
#include "storage.h"
#include <stdio.h>
int main(void) {
    TaskList list;
    task_list_init(&list);
    task_tambah(&list, "Belajar C", "Bab struct", TASK_PRIORITAS_TINGGI);
    task_tambah(&list, "Belanja", "Beli susu", TASK_PRIORITAS_RENDAH);
    task_set_status(&list, 2, TASK_STATUS_SELESAI);

    StorageStatus ss = storage_simpan(&list, "/tmp/p4b4_tugas.txt");
    printf("Simpan -> %d\n", ss);

    TaskList dimuat;
    task_list_init(&dimuat);
    ss = storage_muat(&dimuat, "/tmp/p4b4_tugas.txt");
    printf("Muat -> %d, jumlah = %d, id_berikutnya = %d\n",
           ss, dimuat.jumlah, dimuat.id_berikutnya);
    printf("Status tugas 1: %s (harus Belum)\n",
           task_status_nama(dimuat.data[0].status));
    printf("Status tugas 2: %s (harus Selesai)\n",
           task_status_nama(dimuat.data[1].status));
    return 0;
}
EOF
gcc -std=c11 -Wall -Wextra -I/home/z/my-project/content/c/proyek-c/proyek-04-pengelola-tugas/ -o /tmp/p4b4_demo /tmp/p4b4_demo.c /home/z/my-project/content/c/proyek-c/proyek-04-pengelola-tugas/task.c /home/z/my-project/content/c/proyek-c/proyek-04-pengelola-tugas/storage.c && /tmp/p4b4_demo && echo "--- FILE ---" && cat /tmp/p4b4_tugas.txt

Output yang Dihasilkan:

text
Simpan -> 0
Muat -> 0, jumlah = 2, id_berikutnya = 3
Status tugas 1: Belum (harus Belum)
Status tugas 2: Selesai (harus Selesai)
--- FILE ---
2
1|Belajar C|Bab struct|0|3
2|Belanja|Beli susu|2|1

Mari kita telaah baris-baris kunci pada storage_simpan dan storage_muat:

  1. Baris fprintf(f, "%d|%s|%s|%d|%d\n", ...): cast (int)t->status dan (int)t->prioritas eksplisit. Tanpa cast, compiler mungkin memperingatkan conversion enum-ke-int (tergantung flag).
  2. Baris (int)t->status: enum ke int selalu aman — tidak ada kehilangan data karena enum disimpan sebagai int di memori.
  3. Baris sscanf(baris, "%d|%79[^|]|%199[^|]|%d|%d", ...): specifier %79[^|] membatasi baca maksimal 79 karakter ke judul (sesuai TASK_JUDUL_MAKS - 1). Format lima field, semuanya wajib.
  4. Baris if (parsed < 5) { ... return STORAGE_ERR_FORMAT; }: bila sscanf gagal memparse kelima field, file korup. Kita berhenti dan laporkan error format.
  5. Baris t->status = (TaskStatus)status;: cast int ke enum. Bila nilai status di luar rentang enum (mis. 99 dari file korup), cast tetap berhasil tetapi nilai enum tidak valid. Untuk validasi ketat, tambahkan cek if (status < 0 || status > 2) return STORAGE_ERR_FORMAT;.
  6. Baris if (id > id_terbesar) id_terbesar = id;: lacak ID maksimum saat memuat. Setelah loop, id_berikutnya = id_terbesar + 1 agar ID tugas baru tidak bentrok.

Bagian 5 akan membahas pemisahan modul yang sudah kita lakukan sejak awal proyek.

Berkas Sumber

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

main.c

Unduh berkas
c
/* main.c -- titik masuk aplikasi pengelola tugas. */

#include "task.h"
#include "storage.h"

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

#define DB_PATH "tugas.txt"

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(TaskList *list) {
    char judul[TASK_JUDUL_MAKS];
    char deskripsi[TASK_DESKRIPSI_MAKS];
    baca_baris("Judul      : ", judul, sizeof(judul));
    baca_baris("Deskripsi  : ", deskripsi, sizeof(deskripsi));
    int p = 0;
    if (!baca_int("Prioritas (1=Rendah,2=Sedang,3=Tinggi): ", &p)) {
        printf("Prioritas tidak valid.\n"); return;
    }
    TaskResult r = task_tambah(list, judul, deskripsi, (TaskPrioritas)p);
    if (r == TASK_OK) {
        printf("Tugas ditambahkan (id=%d).\n", list->data[list->jumlah - 1].id);
    } else {
        printf("Gagal menambah (kode %d).\n", r);
    }
}

static void menu_set_status(TaskList *list) {
    int id, s;
    if (!baca_int("ID tugas   : ", &id)) { printf("ID tidak valid.\n"); return; }
    if (!baca_int("Status (0=Belum,1=Proses,2=Selesai): ", &s)) {
        printf("Status tidak valid.\n"); return;
    }
    TaskResult r = task_set_status(list, id, (TaskStatus)s);
    if (r == TASK_OK) printf("Status diubah.\n");
    else if (r == TASK_ERR_TIDAK_DITEMUKAN) printf("Tugas tidak ditemukan.\n");
    else printf("Input tidak valid.\n");
}

static void menu_hapus(TaskList *list) {
    int id;
    if (!baca_int("ID tugas yang dihapus: ", &id)) { printf("ID tidak valid.\n"); return; }
    TaskResult r = task_hapus(list, id);
    if (r == TASK_OK) printf("Tugas dihapus.\n");
    else if (r == TASK_ERR_TIDAK_DITEMUKAN) printf("Tugas tidak ditemukan.\n");
}

static void menu_filter(const TaskList *list) {
    int s;
    if (!baca_int("Filter status (0=Belum,1=Proses,2=Selesai): ", &s)) {
        printf("Status tidak valid.\n"); return;
    }
    if (s < TASK_STATUS_BELUM || s > TASK_STATUS_SELESAI) {
        printf("Status tidak valid.\n"); return;
    }
    task_filter_status(list, (TaskStatus)s, stdout);
}

static void menu_cari(const TaskList *list) {
    char kata[TASK_JUDUL_MAKS];
    baca_baris("Kata kunci judul: ", kata, sizeof(kata));
    int idx = task_cari(list, kata);
    if (idx < 0) {
        printf("Tidak ada tugas dengan judul mengandung '%s'.\n", kata);
    } else {
        const Task *t = &list->data[idx];
        printf("Ditemukan: [%d] %s | %s | %s\n",
               t->id, t->judul,
               task_status_nama(t->status),
               task_prioritas_nama(t->prioritas));
    }
}

int main(void) {
    TaskList list;
    task_list_init(&list);

    StorageStatus sm = storage_muat(&list, DB_PATH);
    if (sm == STORAGE_OK) {
        printf("Memuat %d tugas 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=== Pengelola Tugas ===\n");
        printf("1. Tambah\n2. Tampilkan\n3. Filter status\n");
        printf("4. Cari judul\n5. Set status\n6. Hapus\n7. 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: task_tampilkan(&list, stdout); break;
            case 3: menu_filter(&list); break;
            case 4: menu_cari(&list); break;
            case 5: menu_set_status(&list); break;
            case 6: menu_hapus(&list); break;
            case 7: lanjut = 0; break;
            default: printf("Pilihan tidak dikenal.\n"); break;
        }
    }

    StorageStatus ss = storage_simpan(&list, DB_PATH);
    if (ss == STORAGE_OK) printf("Menyimpan %d tugas 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 tugas ke file teks. */

#include "storage.h"

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

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 TaskList *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 Task *t = &list->data[i];
        /* Ganti | di judul/deskripsi dengan spasi agar parsing tetap aman. */
        fprintf(f, "%d|%s|%s|%d|%d\n",
                t->id, t->judul, t->deskripsi, (int)t->status, (int)t->prioritas);
    }

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

StorageStatus storage_muat(TaskList *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 > TASK_MAKS) {
        fclose(f);
        return STORAGE_ERR_FORMAT;
    }

    char baris[512];
    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, status, prioritas;
        char judul[TASK_JUDUL_MAKS];
        char deskripsi[TASK_DESKRIPSI_MAKS];

        int parsed = sscanf(baris, "%d|%79[^|]|%199[^|]|%d|%d",
                            &id, judul, deskripsi, &status, &prioritas);
        if (parsed < 5) {
            fclose(f);
            return STORAGE_ERR_FORMAT;
        }
        if (list->jumlah >= TASK_MAKS) break;

        Task *t = &list->data[list->jumlah];
        t->id = id;
        strncpy(t->judul, judul, TASK_JUDUL_MAKS - 1);
        t->judul[TASK_JUDUL_MAKS - 1] = '\0';
        strncpy(t->deskripsi, deskripsi, TASK_DESKRIPSI_MAKS - 1);
        t->deskripsi[TASK_DESKRIPSI_MAKS - 1] = '\0';
        t->status = (TaskStatus)status;
        t->prioritas = (TaskPrioritas)prioritas;
        list->jumlah++;

        if (id > id_terbesar) id_terbesar = id;
    }

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

task.c

Unduh berkas
c
/* task.c -- implementasi model dan operasi tugas. */

#include "task.h"

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

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

int task_valid_judul(const char *judul) {
    if (judul == 0 || judul[0] == '\0') return 0;
    if (strlen(judul) >= TASK_JUDUL_MAKS) return 0;
    return 1;
}

int task_valid_prioritas(int p) {
    return p == TASK_PRIORITAS_RENDAH
        || p == TASK_PRIORITAS_SEDANG
        || p == TASK_PRIORITAS_TINGGI;
}

TaskResult task_tambah(TaskList *list,
                       const char *judul,
                       const char *deskripsi,
                       TaskPrioritas prioritas) {
    if (list == 0) return TASK_ERR_INPUT_INVALID;
    if (!task_valid_judul(judul)) return TASK_ERR_INPUT_INVALID;
    if (!task_valid_prioritas((int)prioritas)) return TASK_ERR_INPUT_INVALID;
    if (list->jumlah >= TASK_MAKS) return TASK_ERR_PENUH;

    Task *t = &list->data[list->jumlah];
    t->id = list->id_berikutnya++;
    strncpy(t->judul, judul, TASK_JUDUL_MAKS - 1);
    t->judul[TASK_JUDUL_MAKS - 1] = '\0';
    strncpy(t->deskripsi, deskripsi ? deskripsi : "", TASK_DESKRIPSI_MAKS - 1);
    t->deskripsi[TASK_DESKRIPSI_MAKS - 1] = '\0';
    t->status = TASK_STATUS_BELUM;
    t->prioritas = prioritas;
    list->jumlah++;
    return TASK_OK;
}

void task_tampilkan(const TaskList *list, FILE *out) {
    if (list == 0 || out == 0) return;
    fprintf(out, "%-4s %-30s %-12s %-10s\n",
            "ID", "Judul", "Status", "Prioritas");
    for (int i = 0; i < list->jumlah; i++) {
        const Task *t = &list->data[i];
        fprintf(out, "%-4d %-30s %-12s %-10s\n",
                t->id, t->judul,
                task_status_nama(t->status),
                task_prioritas_nama(t->prioritas));
    }
    fprintf(out, "Total: %d tugas\n", list->jumlah);
}

TaskResult task_set_status(TaskList *list, int id, TaskStatus status) {
    if (list == 0) return TASK_ERR_INPUT_INVALID;
    if (status < TASK_STATUS_BELUM || status > TASK_STATUS_SELESAI) {
        return TASK_ERR_INPUT_INVALID;
    }
    for (int i = 0; i < list->jumlah; i++) {
        if (list->data[i].id == id) {
            list->data[i].status = status;
            return TASK_OK;
        }
    }
    return TASK_ERR_TIDAK_DITEMUKAN;
}

TaskResult task_hapus(TaskList *list, int id) {
    if (list == 0) return TASK_ERR_INPUT_INVALID;
    for (int i = 0; i < list->jumlah; i++) {
        if (list->data[i].id == id) {
            for (int j = i; j < list->jumlah - 1; j++) {
                list->data[j] = list->data[j + 1];
            }
            list->jumlah--;
            return TASK_OK;
        }
    }
    return TASK_ERR_TIDAK_DITEMUKAN;
}

int task_filter_status(const TaskList *list, TaskStatus status, FILE *out) {
    if (list == 0 || out == 0) return 0;
    int count = 0;
    fprintf(out, "%-4s %-30s %-12s %-10s\n",
            "ID", "Judul", "Status", "Prioritas");
    for (int i = 0; i < list->jumlah; i++) {
        const Task *t = &list->data[i];
        if (t->status == status) {
            fprintf(out, "%-4d %-30s %-12s %-10s\n",
                    t->id, t->judul,
                    task_status_nama(t->status),
                    task_prioritas_nama(t->prioritas));
            count++;
        }
    }
    fprintf(out, "Ditemukan: %d tugas\n", count);
    return count;
}

int task_cari(const TaskList *list, const char *kata_kunci) {
    if (list == 0 || kata_kunci == 0) return -1;
    for (int i = 0; i < list->jumlah; i++) {
        if (strstr(list->data[i].judul, kata_kunci) != 0) {
            return i;
        }
    }
    return -1;
}

const char *task_status_nama(TaskStatus s) {
    switch (s) {
        case TASK_STATUS_BELUM:  return "Belum";
        case TASK_STATUS_PROSES: return "Proses";
        case TASK_STATUS_SELESAI:return "Selesai";
        default:                 return "?";
    }
}

const char *task_prioritas_nama(TaskPrioritas p) {
    switch (p) {
        case TASK_PRIORITAS_RENDAH: return "Rendah";
        case TASK_PRIORITAS_SEDANG: return "Sedang";
        case TASK_PRIORITAS_TINGGI: return "Tinggi";
        default:                     return "?";
    }
}

tests.c

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

#include "task.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(void) {
    TaskList list;
    task_list_init(&list);
    TaskResult r1 = task_tambah(&list, "Belajar C", "Bab struct", TASK_PRIORITAS_TINGGI);
    TaskResult r2 = task_tambah(&list, "Belanja", "Beli susu", TASK_PRIORITAS_SEDANG);
    PERIKSA(r1 == TASK_OK && r2 == TASK_OK, "Tambah 2 tugas berhasil");
    PERIKSA(list.jumlah == 2, "Jumlah = 2");
    PERIKSA(list.data[0].id == 1, "ID pertama = 1");
    PERIKSA(list.data[0].status == TASK_STATUS_BELUM, "Status awal = BELUM");
}

static void test_tambah_invalid(void) {
    TaskList list;
    task_list_init(&list);
    PERIKSA(task_tambah(&list, "", "x", TASK_PRIORITAS_SEDANG) == TASK_ERR_INPUT_INVALID,
           "Judul kosong ditolak");
    PERIKSA(task_tambah(&list, "X", "Y", (TaskPrioritas)99) == TASK_ERR_INPUT_INVALID,
           "Prioritas invalid ditolak");
    PERIKSA(list.jumlah == 0, "Tidak ada tugas ditambahkan");
}

static void test_set_status(void) {
    TaskList list;
    task_list_init(&list);
    task_tambah(&list, "T1", "D1", TASK_PRIORITAS_SEDANG);
    TaskResult r = task_set_status(&list, 1, TASK_STATUS_SELESAI);
    PERIKSA(r == TASK_OK, "Set status SELESAI berhasil");
    PERIKSA(list.data[0].status == TASK_STATUS_SELESAI, "Status = SELESAI");
    PERIKSA(task_set_status(&list, 999, TASK_STATUS_PROSES) == TASK_ERR_TIDAK_DITEMUKAN,
           "Set status ID tidak ada -> error");
}

static void test_hapus(void) {
    TaskList list;
    task_list_init(&list);
    task_tambah(&list, "T1", "D1", TASK_PRIORITAS_SEDANG);
    task_tambah(&list, "T2", "D2", TASK_PRIORITAS_SEDANG);
    task_tambah(&list, "T3", "D3", TASK_PRIORITAS_SEDANG);
    TaskResult r = task_hapus(&list, 2);
    PERIKSA(r == TASK_OK && list.jumlah == 2, "Hapus id=2 berhasil");
    PERIKSA(strcmp(list.data[1].judul, "T3") == 0,
           "Setelah hapus, elemen 1 = 'T3'");
}

static void test_filter_status(void) {
    TaskList list;
    task_list_init(&list);
    task_tambah(&list, "T1", "D1", TASK_PRIORITAS_SEDANG);   /* BELUM */
    task_tambah(&list, "T2", "D2", TASK_PRIORITAS_SEDANG);
    task_tambah(&list, "T3", "D3", TASK_PRIORITAS_SEDANG);
    task_set_status(&list, 2, TASK_STATUS_PROSES);
    task_set_status(&list, 3, TASK_STATUS_SELESAI);

    int n = task_filter_status(&list, TASK_STATUS_PROSES, stdout);
    PERIKSA(n == 1, "Filter PROSES menemukan 1 tugas");
}

static void test_cari(void) {
    TaskList list;
    task_list_init(&list);
    task_tambah(&list, "Belajar C", "", TASK_PRIORITAS_SEDANG);
    task_tambah(&list, "Belajar Python", "", TASK_PRIORITAS_SEDANG);
    int idx = task_cari(&list, "Python");
    PERIKSA(idx == 1, "Cari 'Python' -> indeks 1");
    PERIKSA(task_cari(&list, "Rust") == -1, "Cari 'Rust' -> -1");
}

static void test_penyimpanan(void) {
    TaskList list;
    task_list_init(&list);
    task_tambah(&list, "T1", "D1", TASK_PRIORITAS_TINGGI);
    task_tambah(&list, "T2", "D2", TASK_PRIORITAS_RENDAH);
    task_set_status(&list, 2, TASK_STATUS_SELESAI);

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

    TaskList dimuat;
    task_list_init(&dimuat);
    ss = storage_muat(&dimuat, "/tmp/test_tugas.txt");
    PERIKSA(ss == STORAGE_OK, "Muat dari /tmp/test_tugas.txt");
    PERIKSA(dimuat.jumlah == 2, "Jumlah dimuat = 2");
    PERIKSA(strcmp(dimuat.data[0].judul, "T1") == 0, "Judul dimuat = 'T1'");
    PERIKSA(dimuat.data[1].status == TASK_STATUS_SELESAI, "Status dimuat = SELESAI");
    PERIKSA(dimuat.id_berikutnya == 3, "id_berikutnya = 3");
}

int main(void) {
    test_tambah();
    test_tambah_invalid();
    test_set_status();
    test_hapus();
    test_filter_status();
    test_cari();
    test_penyimpanan();
    printf("\nRingkasan: %d lulus, %d gagal\n", lulus, gagal);
    return gagal == 0 ? 0 : 1;
}