Sebelum menulis operasi CRUD, kita perlu model data yang merepresentasikan tugas dengan jelas. Pada bagian ini kita membuat task.h yang berisi dua enum (TaskStatus dan TaskPrioritas), satu struct (Task), satu wrapper struct (TaskList), serta beberapa konstanta dan prototipe fungsi. Tujuan bagian ini adalah menetapkan kontrak data sebelum logika ditulis.

Konsep #

Sebuah tugas punya dua atribut bertipe enum: status (Belum/Proses/Selesai) dan prioritas (Rendah/Sedang/Tinggi). Penggunaan enum lebih aman daripada integer mentah karena compiler dapat melakukan pemeriksaan tipe — bila Anda menulis t.status = 99, compiler tidak menandainya sebagai error, tetapi task_set_status akan menolaknya dengan validasi eksplisit.

struct Task punya lima field: id, judul, deskripsi, status, prioritas. Field id unik dan di-generate otomatis oleh task_tambah. judul wajib diisi (divalidasi), deskripsi opsional. Status default saat tugas dibuat adalah TASK_STATUS_BELUM. Prioritas wajib dipilih pengguna saat menambah tugas.

TaskList adalah wrapper yang membungkus array Task data[TASK_MAKS] bersama counter jumlah dan id_berikutnya. Pola ini sama dengan Proyek 03 — kita memakainya kembali karena terbukti efektif untuk mengelola koleksi dengan ID auto-increment.

Implementasi #

Definisi enum di task.h:

c
typedef enum {
    TASK_STATUS_BELUM = 0,
    TASK_STATUS_PROSES = 1,
    TASK_STATUS_SELESAI = 2
} TaskStatus;

typedef enum {
    TASK_PRIORITAS_RENDAH = 1,
    TASK_PRIORITAS_SEDANG = 2,
    TASK_PRIORITAS_TINGGI = 3
} TaskPrioritas;

Struct Task:

c
typedef struct {
    int id;
    char judul[TASK_JUDUL_MAKS];
    char deskripsi[TASK_DESKRIPSI_MAKS];
    TaskStatus status;
    TaskPrioritas prioritas;
} Task;

Fungsi pembantu untuk nama enum:

c
const char *task_status_nama(TaskStatus s);
const char *task_prioritas_nama(TaskPrioritas p);

Kode Lengkap #

Berkas task.h untuk Bagian 1:

c
#ifndef TASK_H
#define TASK_H

#include <stdio.h>

#define TASK_JUDUL_MAKS 80
#define TASK_DESKRIPSI_MAKS 200
#define TASK_MAKS 50

typedef enum {
    TASK_STATUS_BELUM = 0,
    TASK_STATUS_PROSES = 1,
    TASK_STATUS_SELESAI = 2
} TaskStatus;

typedef enum {
    TASK_PRIORITAS_RENDAH = 1,
    TASK_PRIORITAS_SEDANG = 2,
    TASK_PRIORITAS_TINGGI = 3
} TaskPrioritas;

typedef struct {
    int id;
    char judul[TASK_JUDUL_MAKS];
    char deskripsi[TASK_DESKRIPSI_MAKS];
    TaskStatus status;
    TaskPrioritas prioritas;
} Task;

typedef struct {
    Task data[TASK_MAKS];
    int jumlah;
    int id_berikutnya;
} TaskList;

typedef enum {
    TASK_OK = 0,
    TASK_ERR_PENUH = 1,
    TASK_ERR_TIDAK_DITEMUKAN = 2,
    TASK_ERR_INPUT_INVALID = 3
} TaskResult;

void task_list_init(TaskList *list);
TaskResult task_tambah(TaskList *list, const char *judul,
                       const char *deskripsi, TaskPrioritas prioritas);
void task_tampilkan(const TaskList *list, FILE *out);
TaskResult task_set_status(TaskList *list, int id, TaskStatus status);
TaskResult task_hapus(TaskList *list, int id);
int task_filter_status(const TaskList *list, TaskStatus status, FILE *out);
int task_cari(const TaskList *list, const char *kata_kunci);
const char *task_status_nama(TaskStatus s);
const char *task_prioritas_nama(TaskPrioritas p);
int task_valid_judul(const char *judul);
int task_valid_prioritas(int p);

#endif /* TASK_H */

Implementasi task_status_nama dan task_prioritas_nama di task.c:

c
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 "?";
    }
}

Pengujian #

Verifikasi header valid dengan demo kecil:

bash
cat > /tmp/p4b1_demo.c << 'EOF'
#include "task.h"
#include <stdio.h>
int main(void) {
    TaskList list;
    task_list_init(&list);
    printf("jumlah=%d, id_berikutnya=%d\n", list.jumlah, list.id_berikutnya);
    printf("Status BELUM = %s\n", task_status_nama(TASK_STATUS_BELUM));
    printf("Prioritas TINGGI = %s\n", task_prioritas_nama(TASK_PRIORITAS_TINGGI));
    return 0;
}
EOF
gcc -std=c11 -Wall -Wextra -I/home/z/my-project/content/c/proyek-c/proyek-04-pengelola-tugas/ -o /tmp/p4b1_demo /tmp/p4b1_demo.c /home/z/my-project/content/c/proyek-c/proyek-04-pengelola-tugas/task.c && /tmp/p4b1_demo

Output yang Dihasilkan:

text
jumlah=0, id_berikutnya=1
Status BELUM = Belum
Prioritas TINGGI = Tinggi

Mari kita telaah baris-baris kunci pada task.h:

  1. Baris typedef enum { ... } TaskStatus;: enum diberi nama typedef agar dapat dipakai sebagai TaskStatus s; tanpa keyword enum. Konvensi: nama tipe PascalCase, nilai konstanta UPPER_CASE dengan prefix.
  2. Baris TASK_STATUS_BELUM = 0: nilai eksplisit 0 memudahkan debugging. Bila Task belum diinisialisasi, memset akan menghasilkan status 0 = BELUM — default yang masuk akal.
  3. Baris TASK_PRIORITAS_RENDAH = 1: prioritas dimulai dari 1 (bukan 0) agar nilai 0 berarti “belum diatur” — berguna saat memvalidasi input pengguna.
  4. Baris TaskStatus status; TaskPrioritas prioritas; di struct Task: dua field enum, bukan satu. Memisahkan status (alur kerja) dari prioritas (urgensi) memberi fleksibilitas: tugas “Belum” bisa berprioritas “Tinggi”.
  5. Baris int task_filter_status(const ContactList *list, ...) di prototipe: const pada parameter pointer menandakan fungsi tidak memodifikasi list — penting untuk fungsi yang hanya membaca.

Bagian 2 akan mengisi operasi CRUD: tambah, tampilkan, set status, dan hapus.

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;
}