Pada Bagian 6 terakhir ini, kita menulis unit test untuk setiap modul yang telah dibangun, lalu mengemas program dengan Makefile yang lengkap. Tujuannya adalah memastikan bahwa modul bekerja sesuai harapan dan dapat dibangun, diuji, serta dipasang dengan satu perintah. Bagian ini menutup proyek dengan praktik yang menjadi standar di proyek C open source profesional.

Konsep Testing untuk CLI Tool #

Untuk CLI tool, ada dua jenis test yang umum: (1) unit test untuk fungsi internal (config parser, file processor, logger), dan (2) integration test yang menjalankan program utuh dengan argumen dan memeriksa exit code serta output. Unit test lebih cepat dan lebih spesifik; integration test lebih realistis tapi lebih lambat. Keduanya saling melengkapi.

Untuk modul yang berdiri sendiri (config, file_processor), kita dapat menulis test tanpa mock. Untuk modul yang berinteraksi dengan sistem (file I/O, network), kita perlu file temp atau loopback.

Implementasi Test Harness #

c
/* tests.c */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include "../config.h"
#include "../file_processor.h"
#include "../logger.h"

static int tests_passed = 0;
static int tests_failed = 0;

#define TEST(name) \
    static void name(void); \
    static void name(void)

#define RUN_TEST(name) do { \
    printf("[RUN] %s\n", #name); \
    name(); \
    tests_passed++; \
} while (0)

#define ASSERT_EQ_INT(a, b) do { \
    if ((a) != (b)) { \
        printf("  FAIL: %s:%d: expected %d, got %d\n", __FILE__, __LINE__, (b), (a)); \
        tests_failed++; \
        return; \
    } \
} while (0)

#define ASSERT_STR_EQ(a, b) do { \
    if (strcmp((a), (b)) != 0) { \
        printf("  FAIL: %s:%d: expected '%s', got '%s'\n", __FILE__, __LINE__, (b), (a)); \
        tests_failed++; \
        return; \
    } \
} while (0)

TEST(test_config_load) {
    Config cfg;
    config_init(&cfg);

    FILE *f = fopen("/tmp/test_toolrc", "w");
    fputs("[general]\nlog_level = debug\nmax_size = 1024\n", f);
    fclose(f);

    int rc = config_load(&cfg, "/tmp/test_toolrc");
    ASSERT_EQ_INT(rc, 0);
    ASSERT_EQ_INT(cfg.count, 2);

    const char *log_level = config_get(&cfg, "general.log_level");
    ASSERT_STR_EQ(log_level, "debug");

    const char *max_size = config_get(&cfg, "general.max_size");
    ASSERT_STR_EQ(max_size, "1024");

    unlink("/tmp/test_toolrc");
}

TEST(test_config_missing_file) {
    Config cfg;
    config_init(&cfg);
    int rc = config_load(&cfg, "/nonexistent/path/.toolrc");
    ASSERT_EQ_INT(rc, -1);
}

TEST(test_file_process) {
    FILE *f = fopen("/tmp/test_input.txt", "w");
    fputs("satu dua tiga\nempat lima\n", f);
    fclose(f);

    FileStats stats;
    int rc = file_process("/tmp/test_input.txt", &stats);
    ASSERT_EQ_INT(rc, 0);
    ASSERT_EQ_INT(stats.lines, 2);
    ASSERT_EQ_INT(stats.words, 5);
    ASSERT_EQ_INT(stats.chars, 24);

    unlink("/tmp/test_input.txt");
}

TEST(test_file_process_missing) {
    FileStats stats;
    int rc = file_process("/nonexistent.txt", &stats);
    ASSERT_EQ_INT(rc, -2);
}

TEST(test_logger_levels) {
    logger_set_level(LOG_WARN);
    /* DEBUG dan INFO tidak boleh tercetak */
    /* Karena logger menulis ke stderr, kita tidak bisa langsung cek output
       di test sederhana ini. Untuk testing penuh, redirect stderr ke pipe. */
    ASSERT_EQ_INT(LOG_DEBUG < LOG_INFO, 1);
    ASSERT_EQ_INT(LOG_INFO < LOG_WARN, 1);
    ASSERT_EQ_INT(LOG_WARN < LOG_ERROR, 1);
}

int main(void) {
    RUN_TEST(test_config_load);
    RUN_TEST(test_config_missing_file);
    RUN_TEST(test_file_process);
    RUN_TEST(test_file_process_missing);
    RUN_TEST(test_logger_levels);

    printf("\n%d passed, %d failed\n", tests_passed, tests_failed);
    return tests_failed > 0 ? 1 : 0;
}

Output yang Dihasilkan:

text
[RUN] test_config_load
[RUN] test_config_missing_file
[RUN] test_file_process
[RUN] test_file_process_missing
[RUN] test_logger_levels

5 passed, 0 failed

Mari kita telaah bagian kunci dari test harness di atas:

  1. Macro TEST, RUN_TEST, ASSERT_*: membangun mini framework test yang menyembunyikan boilerplate. Setiap test adalah fungsi static void, dijalankan lewat RUN_TEST yang mencatat nama dan increment counter.
  2. Setup dan teardown inline: setiap test membuat file temp sendiri (misalnya /tmp/test_toolrc) dan membersihkan dengan unlink setelah selesai. Untuk test yang lebih banyak, gunakan fixture function.
  3. Test missing file: selalu uji kasus error, bukan hanya happy path. Memastikan error handling bekerja.
  4. Exit code: return tests_failed > 0 ? 1 : 0; — test runner mengembalikan exit code 0 hanya jika semua test lulus.

Makefile Lengkap #

makefile
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -Wpedantic -g
LDFLAGS =

# Source files
SRCS = main.c cli.c config.c file_processor.c logger.c
OBJS = $(SRCS:.c=.o)
TEST_SRCS = tests.c
TEST_OBJS = $(TEST_SRCS:.c=.o)

# Target
TARGET = tool
TEST_TARGET = test_runner

# Prefix instalasi
PREFIX = /usr/local

.PHONY: all test clean install uninstall

all: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)

%.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $<

test: $(TEST_TARGET)
	./$(TEST_TARGET)

$(TEST_TARGET): $(TEST_SRCS) $(OBJS)
	$(CC) $(CFLAGS) -o $@ $(TEST_SRCS) $(OBJS) $(LDFLAGS)

install: $(TARGET)
	install -d $(DESTDIR)$(PREFIX)/bin
	install -m 755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/

uninstall:
	rm -f $(DESTDIR)$(PREFIX)/bin/$(TARGET)

clean:
	rm -f $(OBJS) $(TEST_OBJS) $(TARGET) $(TEST_TARGET)

Makefile ini mendukung target:

  • make atau make all — bangun program utama
  • make test — bangun dan jalankan test runner
  • make install — pasang ke /usr/local/bin (jalankan dengan sudo)
  • make uninstall — hapus binary yang terpasang
  • make clean — hapus hasil build

Distribusi Tarball #

Untuk distribusi source code:

bash
# Buat tarball
mkdir -p tool-1.0.0
cp *.c *.h Makefile README.md LICENSE tool-1.0.0/
tar czf tool-1.0.0.tar.gz tool-1.0.0
rm -rf tool-1.0.0

# Pengguna lain bisa:
tar xzf tool-1.0.0.tar.gz
cd tool-1.0.0
make
sudo make install

Distribusi Binary #

Untuk distribusi binary pre-built:

bash
# Build static
gcc -std=c11 -Wall -Wextra -O2 -static -o tool-static *.c

# Atau buat .deb (Debian/Ubuntu)
# ... dengan dh_make dan dpkg-buildpackage

Continuous Integration #

Untuk CI di GitHub Actions:

yaml
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: make
      - name: Test
        run: make test

Workflow ini otomatis membangun dan mengetes setiap push, memastikan kode selalu dalam keadaan sehat.

Proyek tool sekarang lengkap — dari parsing argument hingga distribusi. Anda dapat memperluasnya dengan menambah subcommand baru, integrasi dengan library eksternal, atau dukungan untuk lebih banyak format file konfigurasi.

Berkas Sumber

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

cli.c

Unduh berkas
c
/* cli.c - Implementasi parsing argumen */
#include "cli.h"

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

#define PROGRAM_NAME "mtool"
#define PROGRAM_VERSION "1.0.0"

void cli_print_help(FILE *out) {
    fprintf(out,
        "Penggunaan: %s [SUBCOMMAND] [OPTION]... [FILE]\n"
        "\n"
        "Alat pemrosesan teks baris perintah sederhana.\n"
        "\n"
        "Subperintah:\n"
        "  (none)             Baca stdin dan cetak jumlah baris.\n"
        "  help               Tampilkan pesan ini.\n"
        "  version            Tampilkan versi program.\n"
        "  count [FILE]       Hitung jumlah baris di FILE (atau stdin).\n"
        "  head [-n N] [FILE] Cetak N baris pertama (default 10).\n"
        "  tail [-n N] [FILE] Cetak N baris terakhir (default 10).\n"
        "  stats [FILE]       Statistik: jumlah baris, kata, karakter.\n"
        "  grep PATTERN [-i] [FILE]\n"
        "                     Cetak baris yang mengandung PATTERN.\n"
        "\n"
        "Opsi:\n"
        "  -n N               Jumlah baris untuk head/tail.\n"
        "  -i                 Case-insensitive untuk grep.\n"
        "  -v                 Verbose log ke stderr.\n"
        "  --help             Tampilkan pesan bantuan.\n"
        "  --version          Tampilkan versi.\n",
        PROGRAM_NAME);
}

void cli_print_version(FILE *out) {
    fprintf(out, "%s %s\n", PROGRAM_NAME, PROGRAM_VERSION);
}

/* Cek apakah string diawali tanda strip (option). */
static int is_option(const char *s) {
    return s != NULL && s[0] == '-' && s[1] != '\0';
}

int cli_parse(int argc, char *argv[], cli_args *out) {
    if (out == NULL) {
        return -1;
    }
    memset(out, 0, sizeof(*out));
    out->count = 10; /* default untuk head/tail */

    if (argc < 2) {
        out->subcommand = CLI_SUB_COUNT;
        return 0;
    }

    int i = 1;
    /* Deteksi subperintah pertama bila bukan option. */
    if (!is_option(argv[i])) {
        const char *cmd = argv[i];
        if (strcmp(cmd, "help") == 0 || strcmp(cmd, "--help") == 0) {
            out->subcommand = CLI_SUB_HELP;
            out->show_help = 1;
            return 0;
        }
        if (strcmp(cmd, "version") == 0 || strcmp(cmd, "--version") == 0) {
            out->subcommand = CLI_SUB_VERSION;
            out->show_version = 1;
            return 0;
        }
        if (strcmp(cmd, "count") == 0) {
            out->subcommand = CLI_SUB_COUNT;
        } else if (strcmp(cmd, "head") == 0) {
            out->subcommand = CLI_SUB_HEAD;
        } else if (strcmp(cmd, "tail") == 0) {
            out->subcommand = CLI_SUB_TAIL;
        } else if (strcmp(cmd, "stats") == 0) {
            out->subcommand = CLI_SUB_STATS;
        } else if (strcmp(cmd, "grep") == 0) {
            out->subcommand = CLI_SUB_GREP;
        } else if (cmd[0] == '-' && cmd[1] == '-') {
            /* long option tanpa subcommand */
            out->subcommand = CLI_SUB_COUNT;
        } else if (is_option(cmd)) {
            /* short option tanpa subcommand: anggap count */
            out->subcommand = CLI_SUB_COUNT;
        } else {
            /* Bukan subperintah yang dikenal, anggap sebagai path file. */
            out->subcommand = CLI_SUB_COUNT;
            out->file_path = cmd;
            i++;
            return 0;
        }
        i++;
    } else {
        out->subcommand = CLI_SUB_COUNT;
    }

    /* Khusus grep: argumen non-option pertama adalah PATTERN. */
    if (out->subcommand == CLI_SUB_GREP) {
        if (i >= argc) {
            return -1; /* grep butuh pattern */
        }
        out->pattern = argv[i++];
    }

    /* Iterasi sisa argumen. */
    while (i < argc) {
        const char *a = argv[i];
        if (strcmp(a, "-n") == 0) {
            if (i + 1 >= argc) {
                return -1;
            }
            out->count = (int)strtol(argv[++i], NULL, 10);
            if (out->count < 0) {
                return -1;
            }
            i++;
        } else if (strcmp(a, "-i") == 0) {
            out->ignore_case = 1;
            i++;
        } else if (strcmp(a, "-v") == 0) {
            out->verbose = 1;
            i++;
        } else if (strcmp(a, "--help") == 0) {
            out->show_help = 1;
            i++;
        } else if (strcmp(a, "--version") == 0) {
            out->show_version = 1;
            i++;
        } else if (is_option(a)) {
            return -1; /* option tidak dikenal */
        } else {
            /* Argumen non-option: file_path. */
            if (out->file_path == NULL) {
                out->file_path = a;
            } else {
                return -1; /* lebih dari satu file (tidak didukung) */
            }
            i++;
        }
    }
    return 0;
}

config.c

Unduh berkas
c
/* config.c - Implementasi loader file konfigurasi */
#include "config.h"

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

void config_init(config *c) {
    if (c == NULL) {
        return;
    }
    c->count = 0;
}

static void strip(char *s) {
    /* Trim whitespace di kiri. */
    char *start = s;
    while (*start != '\0' && isspace((unsigned char)*start)) {
        start++;
    }
    if (start != s) {
        memmove(s, start, strlen(start) + 1);
    }
    /* Trim whitespace di kanan. */
    size_t len = strlen(s);
    while (len > 0 && isspace((unsigned char)s[len - 1])) {
        s[--len] = '\0';
    }
}

static void parse_line(char *line, config *c) {
    /* Lewati komentar dan baris kosong. */
    if (line[0] == '#' || line[0] == ';' || line[0] == '\0') {
        return;
    }
    /* Cari '=' pemisah. */
    char *eq = strchr(line, '=');
    if (eq == NULL) {
        return;
    }
    *eq = '\0';
    char *key = line;
    char *value = eq + 1;
    strip(key);
    strip(value);
    if (key[0] == '\0' || c->count >= CONFIG_ENTRIES_MAX) {
        return;
    }
    config_entry *e = &c->entries[c->count];
    strncpy(e->key, key, CONFIG_KEY_MAX - 1);
    e->key[CONFIG_KEY_MAX - 1] = '\0';
    strncpy(e->value, value, CONFIG_VALUE_MAX - 1);
    e->value[CONFIG_VALUE_MAX - 1] = '\0';
    c->count++;
}

int config_load(config *c, const char *path) {
    if (c == NULL || path == NULL) {
        return -1;
    }
    config_init(c);
    FILE *fp = fopen(path, "r");
    if (fp == NULL) {
        return -1;
    }
    char line[256];
    while (fgets(line, sizeof(line), fp) != NULL) {
        /* Hapus newline. */
        size_t len = strlen(line);
        while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
            line[--len] = '\0';
        }
        parse_line(line, c);
    }
    fclose(fp);
    return c->count;
}

const char *config_get(const config *c, const char *key) {
    if (c == NULL || key == NULL) {
        return NULL;
    }
    for (int i = 0; i < c->count; i++) {
        if (strcmp(c->entries[i].key, key) == 0) {
            return c->entries[i].value;
        }
    }
    return NULL;
}

int config_get_int(const config *c, const char *key, int default_value) {
    const char *v = config_get(c, key);
    if (v == NULL) {
        return default_value;
    }
    return (int)strtol(v, NULL, 10);
}

file_processor.c

Unduh berkas
c
/* file_processor.c - Implementasi pemrosesan file */
#include "file_processor.h"

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

FILE *file_open(const char *path, const char *mode) {
    if (path == NULL) {
        if (strcmp(mode, "r") == 0) {
            return stdin;
        }
        if (strcmp(mode, "w") == 0) {
            return stdout;
        }
        return NULL;
    }
    return fopen(path, mode);
}

static void close_file(FILE *fp, const char *path) {
    if (path != NULL && fp != NULL) {
        fclose(fp);
    }
    (void)fp;
}

/* Helper: duplikasi string (pengganti strdup agar portable C11 murni). */
static char *dup_str(const char *s) {
    size_t len = strlen(s) + 1;
    char *copy = malloc(len);
    if (copy != NULL) {
        memcpy(copy, s, len);
    }
    return copy;
}

/* Helper: cek apakah string `s` mengandung substring `pattern`
 * secara case-insensitive. */
static int contains_ci(const char *s, const char *pattern) {
    if (pattern[0] == '\0') {
        return 1;
    }
    for (const char *p = s; *p != '\0'; p++) {
        const char *si = p;
        const char *pi = pattern;
        while (*si != '\0' && *pi != '\0' &&
               tolower((unsigned char)*si) == tolower((unsigned char)*pi)) {
            si++;
            pi++;
        }
        if (*pi == '\0') {
            return 1;
        }
    }
    return 0;
}

int file_stats_compute(const char *path, file_stats *out) {
    if (out == NULL) {
        return -1;
    }
    out->lines = 0;
    out->words = 0;
    out->chars = 0;
    FILE *fp = file_open(path, "r");
    if (fp == NULL) {
        return -1;
    }
    int prev_was_space = 1;
    int c;
    while ((c = fgetc(fp)) != EOF) {
        out->chars++;
        if (c == '\n') {
            out->lines++;
            prev_was_space = 1;
        } else if (isspace(c)) {
            prev_was_space = 1;
        } else {
            if (prev_was_space) {
                out->words++;
            }
            prev_was_space = 0;
        }
    }
    /* Hitung baris terakhir bila tidak diakhiri newline. */
    if (out->chars > 0 && !prev_was_space && out->lines == 0) {
        out->lines = 1;
    }
    close_file(fp, path);
    return 0;
}

int file_head(const char *path, int n) {
    if (n < 0) {
        return -1;
    }
    FILE *fp = file_open(path, "r");
    if (fp == NULL) {
        return -1;
    }
    int printed = 0;
    char buf[4096];
    while (printed < n && fgets(buf, sizeof(buf), fp) != NULL) {
        fputs(buf, stdout);
        printed++;
    }
    close_file(fp, path);
    return printed;
}

int file_tail(const char *path, int n) {
    if (n <= 0) {
        return 0;
    }
    FILE *fp = file_open(path, "r");
    if (fp == NULL) {
        return -1;
    }
    /* Alokasikan buffer sirkular untuk N baris terakhir. */
    char **ring = calloc((size_t)n, sizeof(char *));
    if (ring == NULL) {
        close_file(fp, path);
        return -1;
    }
    int idx = 0;
    int filled = 0;
    char buf[4096];
    while (fgets(buf, sizeof(buf), fp) != NULL) {
        if (ring[idx] != NULL) {
            free(ring[idx]);
        }
        ring[idx] = dup_str(buf);
        idx = (idx + 1) % n;
        if (filled < n) {
            filled++;
        }
    }
    /* Cetak sesuai urutan kronologis. */
    int start = (filled < n) ? 0 : idx;
    for (int k = 0; k < filled; k++) {
        int pos = (start + k) % n;
        if (ring[pos] != NULL) {
            fputs(ring[pos], stdout);
            free(ring[pos]);
        }
    }
    free(ring);
    close_file(fp, path);
    return filled;
}

int file_grep(const char *path, const char *pattern, int ignore_case) {
    if (pattern == NULL) {
        return -1;
    }
    FILE *fp = file_open(path, "r");
    if (fp == NULL) {
        return -1;
    }
    int printed = 0;
    char buf[4096];
    while (fgets(buf, sizeof(buf), fp) != NULL) {
        int match;
        if (ignore_case) {
            match = contains_ci(buf, pattern);
        } else {
            match = (strstr(buf, pattern) != NULL);
        }
        if (match) {
            fputs(buf, stdout);
            printed++;
        }
    }
    close_file(fp, path);
    return printed;
}

logger.c

Unduh berkas
c
/* logger.c - Implementasi logger */
#include "logger.h"

#include <stdarg.h>
#include <string.h>
#include <time.h>

static log_level g_min_level = LOG_INFO;
static FILE     *g_out        = NULL;

static const char *level_name(log_level l) {
    switch (l) {
        case LOG_DEBUG: return "DEBUG";
        case LOG_INFO:  return "INFO";
        case LOG_WARN:  return "WARN";
        case LOG_ERROR: return "ERROR";
        case LOG_OFF:   return "OFF";
        default:        return "?";
    }
}

void log_init(log_level min, FILE *out) {
    g_min_level = min;
    g_out = (out != NULL) ? out : stderr;
}

void log_write(log_level level, const char *file, int line,
               const char *fmt, ...) {
    if (level < g_min_level || g_out == NULL) {
        return;
    }
    /* Timestamp sederhana: detik sejak epoch. */
    time_t now = time(NULL);
    struct tm *tm_info = localtime(&now);
    char ts[20];
    strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", tm_info);

    /* Ambil basename file agar log ringkas. */
    const char *slash = strrchr(file, '/');
    const char *base = (slash != NULL) ? slash + 1 : file;

    fprintf(g_out, "[%s] [%s] %s:%d ", ts, level_name(level), base, line);

    va_list ap;
    va_start(ap, fmt);
    vfprintf(g_out, fmt, ap);
    va_end(ap);

    fputc('\n', g_out);
    fflush(g_out);
}

main.c

Unduh berkas
c
/* main.c - Program utama command-line tool "mtool" */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "cli.h"
#include "config.h"
#include "file_processor.h"
#include "logger.h"

/* Konfigurasi default bila file config ada. */
static const char *CONFIG_FILE = ".mtoolrc";

static int run_subcommand(const cli_args *args) {
    file_stats stats;

    switch (args->subcommand) {
        case CLI_SUB_HELP:
            cli_print_help(stdout);
            return 0;
        case CLI_SUB_VERSION:
            cli_print_version(stdout);
            return 0;
        case CLI_SUB_COUNT: {
            if (args->file_path == NULL) {
                /* Hitung baris dari stdin. */
                int c;
                long count = 0;
                while ((c = fgetc(stdin)) != EOF) {
                    if (c == '\n') {
                        count++;
                    }
                }
                printf("%ld\n", count);
                return 0;
            }
            if (file_stats_compute(args->file_path, &stats) != 0) {
                LOG_ERROR("tidak bisa membuka file: %s", args->file_path);
                return 1;
            }
            printf("%ld %s\n", stats.lines, args->file_path);
            return 0;
        }
        case CLI_SUB_HEAD:
            if (file_head(args->file_path, args->count) < 0) {
                LOG_ERROR("head gagal");
                return 1;
            }
            return 0;
        case CLI_SUB_TAIL:
            if (file_tail(args->file_path, args->count) < 0) {
                LOG_ERROR("tail gagal");
                return 1;
            }
            return 0;
        case CLI_SUB_STATS:
            if (file_stats_compute(args->file_path, &stats) != 0) {
                LOG_ERROR("tidak bisa membuka file: %s",
                          args->file_path ? args->file_path : "stdin");
                return 1;
            }
            printf("%8ld baris  %8ld kata  %8ld karakter  %s\n",
                   stats.lines, stats.words, stats.chars,
                   args->file_path ? args->file_path : "-");
            return 0;
        case CLI_SUB_GREP:
            if (args->pattern == NULL) {
                fprintf(stderr, "grep butuh PATTERN\n");
                return 1;
            }
            if (file_grep(args->file_path, args->pattern,
                          args->ignore_case) < 0) {
                LOG_ERROR("grep gagal");
                return 1;
            }
            return 0;
        case CLI_SUB_NONE:
        default:
            cli_print_help(stderr);
            return 1;
    }
}

int main(int argc, char *argv[]) {
    /* Inisialisasi logger default: level INFO ke stderr. */
    log_init(LOG_INFO, stderr);

    /* Muat config opsional bila ada. */
    config cfg;
    config_init(&cfg);
    if (config_load(&cfg, CONFIG_FILE) > 0) {
        const char *lvl = config_get(&cfg, "log_level");
        if (lvl != NULL) {
            if (strcmp(lvl, "DEBUG") == 0) {
                log_init(LOG_DEBUG, stderr);
            } else if (strcmp(lvl, "WARN") == 0) {
                log_init(LOG_WARN, stderr);
            } else if (strcmp(lvl, "ERROR") == 0) {
                log_init(LOG_ERROR, stderr);
            }
        }
    }

    cli_args args;
    if (cli_parse(argc, argv, &args) != 0) {
        fprintf(stderr, "Argumen tidak valid. Jalankan '%s help'.\n",
                argv[0]);
        return 2;
    }

    if (args.verbose) {
        log_init(LOG_DEBUG, stderr);
    }

    if (args.show_help) {
        cli_print_help(stdout);
        return 0;
    }
    if (args.show_version) {
        cli_print_version(stdout);
        return 0;
    }

    LOG_DEBUG("subcommand=%d file=%s",
               (int)args.subcommand,
               args.file_path ? args.file_path : "(stdin)");

    return run_subcommand(&args);
}

tests.c

Unduh berkas
c
/* tests.c - Unit test untuk CLI tool */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include "cli.h"
#include "config.h"
#include "file_processor.h"

static int tests_run = 0;
static int tests_pass = 0;

#define ASSERT(cond, msg) \
    do { \
        tests_run++; \
        if (cond) { \
            tests_pass++; \
        } else { \
            printf("FAIL: %s (line %d)\n", msg, __LINE__); \
        } \
    } while (0)

static void test_cli_parse_count(void) {
    char *argv[] = {"mtool", "count", "data.txt"};
    cli_args args;
    int rc = cli_parse(3, argv, &args);
    ASSERT(rc == 0, "count harus sukses");
    ASSERT(args.subcommand == CLI_SUB_COUNT, "subcommand = COUNT");
    ASSERT(args.file_path != NULL, "file_path = data.txt");
    ASSERT(strcmp(args.file_path, "data.txt") == 0, "file_path benar");
}

static void test_cli_parse_head_with_n(void) {
    char *argv[] = {"mtool", "head", "-n", "5", "data.txt"};
    cli_args args;
    int rc = cli_parse(5, argv, &args);
    ASSERT(rc == 0, "head -n 5 harus sukses");
    ASSERT(args.subcommand == CLI_SUB_HEAD, "subcommand = HEAD");
    ASSERT(args.count == 5, "count = 5");
}

static void test_cli_parse_grep(void) {
    char *argv[] = {"mtool", "grep", "hello", "-i", "data.txt"};
    cli_args args;
    int rc = cli_parse(5, argv, &args);
    ASSERT(rc == 0, "grep harus sukses");
    ASSERT(args.subcommand == CLI_SUB_GREP, "subcommand = GREP");
    ASSERT(strcmp(args.pattern, "hello") == 0, "pattern = hello");
    ASSERT(args.ignore_case == 1, "ignore_case = 1");
}

static void test_cli_parse_help_version(void) {
    char *argv1[] = {"mtool", "help"};
    cli_args args;
    ASSERT(cli_parse(2, argv1, &args) == 0, "help sukses");
    ASSERT(args.subcommand == CLI_SUB_HELP, "subcommand = HELP");

    char *argv2[] = {"mtool", "version"};
    ASSERT(cli_parse(2, argv2, &args) == 0, "version sukses");
    ASSERT(args.subcommand == CLI_SUB_VERSION, "subcommand = VERSION");

    char *argv3[] = {"mtool", "--help"};
    ASSERT(cli_parse(2, argv3, &args) == 0, "--help sukses");
    ASSERT(args.show_help == 1, "show_help = 1");
}

static void test_cli_parse_unknown_option(void) {
    char *argv[] = {"mtool", "head", "-Z"};
    cli_args args;
    int rc = cli_parse(3, argv, &args);
    ASSERT(rc == -1, "opsi tidak dikenal = error");
}

static void test_config_load(void) {
    const char *path = "/tmp/test_mtool.cfg";
    FILE *fp = fopen(path, "w");
    ASSERT(fp != NULL, "bisa membuat file config");
    fprintf(fp, "# komentar\n");
    fprintf(fp, "log_level = DEBUG\n");
    fprintf(fp, "default_count = 25\n");
    fprintf(fp, "; komentar titik koma\n");
    fprintf(fp, "name = mtool\n");
    fclose(fp);

    config c;
    int n = config_load(&c, path);
    ASSERT(n == 3, "harus ada 3 entri");

    const char *v = config_get(&c, "log_level");
    ASSERT(v != NULL && strcmp(v, "DEBUG") == 0, "log_level = DEBUG");

    int i = config_get_int(&c, "default_count", 10);
    ASSERT(i == 25, "default_count = 25");

    /* key tidak ada -> default */
    i = config_get_int(&c, "tidak_ada", 42);
    ASSERT(i == 42, "key tidak ada = default");
}

static void test_file_stats(void) {
    const char *path = "/tmp/test_mtool_data.txt";
    FILE *fp = fopen(path, "w");
    ASSERT(fp != NULL, "bisa membuat file data");
    fprintf(fp, "hello world\n");
    fprintf(fp, "second line here\n");
    fprintf(fp, "third\n");
    fclose(fp);

    file_stats s;
    int rc = file_stats_compute(path, &s);
    ASSERT(rc == 0, "stats harus sukses");
    ASSERT(s.lines == 3, "lines = 3");
    ASSERT(s.words == 6, "words = 6");
    ASSERT(s.chars > 0, "chars > 0");
}

static void test_file_head_tail(void) {
    const char *path = "/tmp/test_mtool_data.txt";
    /* File ini sudah dibuat di test sebelumnya. */

    /* Head 2 baris: kembalikan jumlah baris yang dicetak. */
    int n = file_head(path, 2);
    ASSERT(n == 2, "head harus cetak 2 baris");

    /* Head dengan n melebihi total baris: tetap OK, cetak semua. */
    n = file_head(path, 100);
    ASSERT(n == 3, "head 100 harus cetak 3 baris (semua)");

    n = file_tail(path, 1);
    ASSERT(n == 1, "tail 1 harus cetak 1 baris");

    n = file_tail(path, 100);
    ASSERT(n == 3, "tail 100 harus cetak 3 baris");
}

static void test_file_grep(void) {
    const char *path = "/tmp/test_mtool_data.txt";

    int n = file_grep(path, "line", 0);
    ASSERT(n == 1, "grep 'line' case-sensitive = 1 (second line here)");

    n = file_grep(path, "LINE", 0);
    ASSERT(n == 0, "grep 'LINE' case-sensitive = 0");

    n = file_grep(path, "LINE", 1);
    ASSERT(n == 1, "grep 'LINE' case-insensitive = 1");

    n = file_grep(path, "hello", 0);
    ASSERT(n == 1, "grep 'hello' = 1");
}

int main(void) {
    test_cli_parse_count();
    test_cli_parse_head_with_n();
    test_cli_parse_grep();
    test_cli_parse_help_version();
    test_cli_parse_unknown_option();
    test_config_load();
    test_file_stats();
    test_file_head_tail();
    test_file_grep();

    printf("\n=== Hasil Test ===\n");
    printf("Total : %d\n", tests_run);
    printf("Lulus : %d\n", tests_pass);
    printf("Gagal : %d\n", tests_run - tests_pass);
    return (tests_run == tests_pass) ? 0 : 1;
}