Setelah CRUD dasar berjalan, kita menambahkan dua fitur query: task_filter_status untuk menampilkan hanya tugas dengan status tertentu, dan task_cari untuk mencari tugas yang judulnya mengandung substring tertentu. Filter memakai perbandingan equality pada enum, sementara pencarian memakai strstr dari <string.h> untuk mencari substring dalam string.

Konsep #

Filter berbeda dari pencarian dalam dua hal: (1) filter membandingkan field diskret (enum) sedangkan pencarian membandingkan string; (2) filter mengembalikan banyak hasil sedangkan pencarian mengembalikan satu indeks pertama. Kedua operasi ini sama-sama iterasi linier O(n) — sederhana dan cukup untuk skala aplikasi pengelola tugas pribadi.

Fungsi task_filter_status menerima parameter TaskStatus status dan FILE *out untuk menulis hasilnya. Memakai FILE *out (bukan langsung stdout) membuat fungsi ini dapat diuji dengan menulis ke stderr atau fmemopen (buffer in-memory) — fleksibilitas yang penting untuk testing.

Fungsi task_cari memakai strstr(haystack, needle) yang mengembalikan pointer ke kemunculan pertama needle di haystack, atau NULL bila tidak ditemukan. Pencarian bersifat case-sensitive: “belajar” tidak cocok dengan “Belajar”. Untuk dukungan case-insensitive, kita perlu strcasestr (ekstensi GNU, tidak portabel) atau implementasi manual.

Implementasi #

Fungsi task_filter_status:

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

Fungsi task_cari:

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

Kode Lengkap #

Kedua fungsi tersebut sudah merupakan implementasi final. Pemanggilan dari main.c:

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

Pengujian #

Demo filter dan cari:

bash
cat > /tmp/p4b3_demo.c << 'EOF'
#include "task.h"
#include <stdio.h>
int main(void) {
    TaskList list;
    task_list_init(&list);
    task_tambah(&list, "Belajar C", "", TASK_PRIORITAS_SEDANG);
    task_tambah(&list, "Belajar Python", "", TASK_PRIORITAS_SEDANG);
    task_tambah(&list, "Belanja", "", TASK_PRIORITAS_SEDANG);
    task_set_status(&list, 2, TASK_STATUS_PROSES);

    printf("=== Filter PROSES ===\n");
    int n = task_filter_status(&list, TASK_STATUS_PROSES, stdout);
    printf("jumlah filter = %d\n\n", n);

    printf("=== Cari 'Belajar' ===\n");
    int idx = task_cari(&list, "Belajar");
    printf("Indeks pertama: %d (judul: %s)\n", idx, list.data[idx].judul);

    printf("=== Cari 'Rust' ===\n");
    printf("Hasil: %d (harus -1)\n", task_cari(&list, "Rust"));
    return 0;
}
EOF
gcc -std=c11 -Wall -Wextra -I/home/z/my-project/content/c/proyek-c/proyek-04-pengelola-tugas/ -o /tmp/p4b3_demo /tmp/p4b3_demo.c /home/z/my-project/content/c/proyek-c/proyek-04-pengelola-tugas/task.c && /tmp/p4b3_demo

Output yang Dihasilkan:

text
=== Filter PROSES ===
ID   Judul                          Status       Prioritas
2    Belajar Python                 Proses       Sedang
Ditemukan: 1 tugas
jumlah filter = 1

=== Cari 'Belajar' ===
Indeks pertama: 0 (judul: Belajar C)

=== Cari 'Rust' ===
Hasil: -1 (harus -1)

Mari kita telaah baris-baris kunci pada task_filter_status dan task_cari:

  1. Baris if (list == 0 || out == 0) return 0;: dua pointer check sekaligus. Bila out NULL, kita tidak dapat menulis hasil, sehingga tidak ada gunanya melanjutkan.
  2. Baris int count = 0;: penghitung lokal yang dilacak selama iterasi. Counter ini menjadi return value sehingga pemanggil tahu berapa banyak tugas yang cocok tanpa menghitung baris output.
  3. Baris if (t->status == status): perbandingan enum ke enum. Aman karena keduanya bertipe TaskStatus — compiler tidak akan menyilang tipe dengan integer.
  4. Baris fprintf(out, "%-4d %-30s ...", ...): format lebar tetap, sama dengan task_tampilkan. Header tabel juga dicetak sehingga output filter terlihat seperti tabel utuh.
  5. Baris if (strstr(list->data[i].judul, kata_kunci) != 0) di task_cari: strstr memulangkan pointer atau NULL. Bandingkan dengan != 0 (atau != NULL) untuk menguji “ditemukan”.
  6. Baris return i;: pulangkan indeks pertama yang cocok. Tidak lanjut mencari yang lain — sesuai kontrak fungsi.
  7. Baris return -1; di akhir: bila loop selesai tanpa menemukan, pulangkan -1 sebagai penanda “tidak ada”.

Bagian 4 akan menambahkan penyimpanan ke file teks.

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