Pipeline CSV yang telah kita rakit pada bagian 1-5 perlu diverifikasi. Pada bagian ini kita menulis tests.c yang berisi rangkaian test untuk modul csv_reader dan parser, lalu menyusun Makefile dengan target program (executable utama) dan test (runner test). Tujuan bagian ini adalah memastikan setiap fungsi publik bekerja sesuai kontrak dan rebuild dapat dilakukan dengan satu perintah make.
Konsep #
Strategi testing yang kita pakai sama dengan Proyek 1 (Kalkulator): macro PERIKSA(kondisi, nama) yang menghitung jumlah lulus dan gagal, dengan fungsi main yang memanggil beberapa test function lalu mencetak ringkasan. Pola ini tidak butuh framework pihak ketiga — cocok untuk proyek kecil dan menengah yang harus di-build tanpa dependensi eksternal.
Test yang kita tulis mencakup empat area: (1) pembacaan file CSV valid (test_baca_csv), (2) penanganan file tidak ada (test_baca_file_tidak_ada), (3) konversi string ke double (test_parser_ke_double), dan (4) agregasi statistik kolom (test_statistik_kolom). Setiap test function membuat file CSV sementara di /tmp/ lalu memverifikasi hasil. Untuk perbandingan double, kita pakai helper hampir_sama(a, b) yang memeriksa fabs(a - b) < 1e-9 agar presisi floating point tidak menyebabkan test false-negatif.
Makefile kita punya tiga target: program (build executable utama dari main.c csv_reader.c parser.c), test (build dan jalankan tests.c yang juga meng-link modul yang sama), dan clean (hapus binary dan output.csv). Variabel SRCS dan TEST_SRCS memisahkan sumber executable utama dari sumber test runner — keduanya berbagi csv_reader.c dan parser.c.
Implementasi #
Macro dan helper di tests.c:
#include <stdio.h>
#include <math.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 int hampir_sama(double a, double b) {
return fabs(a - b) < 1e-9;
}Test untuk parser ke double dan statistik:
static void test_parser_ke_double(void) {
double v;
PERIKSA(parser_ke_double("3.14", &v) == PARSER_OK && hampir_sama(v, 3.14),
"Parse '3.14' -> 3.14");
PERIKSA(parser_ke_double(" 42 ", &v) == PARSER_OK && hampir_sama(v, 42.0),
"Parse ' 42 ' -> 42 (toleransi whitespace)");
PERIKSA(parser_ke_double("bukan-angka", &v) == PARSER_ERR_KONVERSI,
"Parse 'bukan-angka' -> KONVERSI error");
PERIKSA(parser_ke_double("", &v) == PARSER_ERR_KONVERSI,
"Parse string kosong -> KONVERSI error");
}Demo test runner mandiri:
#include <stdio.h>
#include <math.h>
#include <stdlib.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 int hampir_sama(double a, double b) {
return fabs(a - b) < 1e-9;
}
typedef enum { PARSER_OK = 0, PARSER_ERR_KONVERSI = 3 } ParserStatus;
static ParserStatus parser_ke_double(const char *s, double *out) {
if (!s || !out) return PARSER_ERR_KONVERSI;
char *sisa = 0;
double v = strtod(s, &sisa);
if (sisa == s) return PARSER_ERR_KONVERSI;
*out = v;
return PARSER_OK;
}
int main(void) {
double v;
PERIKSA(parser_ke_double("3.14", &v) == PARSER_OK && hampir_sama(v, 3.14),
"Parse '3.14' -> 3.14");
PERIKSA(parser_ke_double(" 42 ", &v) == PARSER_OK && hampir_sama(v, 42.0),
"Parse ' 42 ' -> 42 (toleransi whitespace)");
PERIKSA(parser_ke_double("bukan-angka", &v) == PARSER_ERR_KONVERSI,
"Parse 'bukan-angka' -> KONVERSI error");
PERIKSA(parser_ke_double("", &v) == PARSER_ERR_KONVERSI,
"Parse string kosong -> KONVERSI error");
printf("\nRingkasan: %d lulus, %d gagal\n", lulus, gagal);
return gagal == 0 ? 0 : 1;
}Output yang Dihasilkan:
[LULUS] Parse '3.14' -> 3.14
[LULUS] Parse ' 42 ' -> 42 (toleransi whitespace)
[LULUS] Parse 'bukan-angka' -> KONVERSI error
[LULUS] Parse string kosong -> KONVERSI error
Ringkasan: 4 lulus, 0 gagalPengujian #
Build dan jalankan test runner lengkap proyek:
gcc -std=c11 -Wall -Wextra -o test_runner \
/home/z/my-project/content/c/proyek-c/proyek-05-pemroses-file-csv/tests.c \
/home/z/my-project/content/c/proyek-c/proyek-05-pemroses-file-csv/csv_reader.c \
/home/z/my-project/content/c/proyek-c/proyek-05-pemroses-file-csv/parser.c -lm
./test_runnerOutput yang Dihasilkan:
[LULUS] Baca CSV berhasil
[LULUS] Jumlah baris = 4 (1 header + 3 data)
[LULUS] Header punya 3 field
[LULUS] Baris 1 field 0 = 'Budi'
[LULUS] File tidak ada -> BUKA_FILE
[LULUS] Parse '3.14' -> 3.14
[LULUS] Parse ' 42 ' -> 42 (toleransi whitespace)
[LULUS] Parse 'bukan-angka' -> KONVERSI error
[LULUS] Parse string kosong -> KONVERSI error
[LULUS] Statistik kolom 1 (skip header) berhasil
[LULUS] Jumlah data = 3
[LULUS] Total = 60
[LULUS] Minimum = 10
[LULUS] Maksimum = 30
[LULUS] Rata-rata = 20
[LULUS] Field non-numerik -> KONVERSI error
Ringkasan: 16 lulus, 0 gagalBuild dengan Makefile:
make
./program sample.csv 2
make test
make cleanMakefile lengkap proyek:
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -g
TARGET = program
TEST_TARGET = test_runner
SRCS = main.c csv_reader.c parser.c
TEST_SRCS = tests.c csv_reader.c parser.c
$(TARGET): $(SRCS)
$(CC) $(CFLAGS) -o $(TARGET) $(SRCS) -lm
test: $(TEST_TARGET)
./$(TEST_TARGET)
$(TEST_TARGET): $(TEST_SRCS)
$(CC) $(CFLAGS) -o $(TEST_TARGET) $(TEST_SRCS) -lm
clean:
rm -f $(TARGET) $(TEST_TARGET) output.csv
.PHONY: test cleanMari kita telaah baris-baris kunci pada tests.c dan Makefile:
- Baris
static int lulus = 0; static int gagal = 0;: state global file-scope. Penggunaanstaticmembatasi visibilitas ke file ini saja — tidak bocor ke file lain bila test di-link dengan kode produksi. Hitungan dipertahankan lintas test function lewat variabel global. - Baris
#define PERIKSA(kondisi, nama) do { ... } while (0): macro statement-safe. Polado { ... } while (0)memungkinkan macro dipakai seperti statement biasa:if (kondisi) PERIKSA(...); else ...bekerja tanpa ambiguitas blok. - Baris
lulus++; printf("[LULUS] %s\n", nama);: cetak hasil per-test. Format[LULUS]atau[GAGAL]di awal baris memudahkan grep otomatis pada CI/CD:./test_runner | grep -c GAGALmemberi jumlah kegagalan. - Baris
hampir_sama(fabs(a - b) < 1e-9): perbandingan floating point. Konstanta1e-9(epsilon) adalah toleransi error untuk angka skala normal. Untuk angka sangat besar atau sangat kecil, gunakan epsilon relatifepsilon * fabs(a). - Baris
fputs("nama,umur,gaji\nBudi,25,5000\n...", f);(dalam test_baca_csv): menulis file CSV uji ke/tmp/lalu membacanya. Pola ini mengisolasi test dari sistem file pengguna — bila test gagal, file sampel tetap di/tmp/untuk inspeksi. - Baris
return gagal == 0 ? 0 : 1;(dimain): exit code. Bila semua lulus, exit 0 (sukses); bila ada yang gagal, exit 1. Exit code ini ditangkap CI/CD (mis. GitHub Actions, GitLab CI) untuk menandai build sebagai merah/hijau. - Baris
TEST_SRCS = tests.c csv_reader.c parser.c;(di Makefile): sumber test runner berbagicsv_reader.cdanparser.cdengan executable utama. Pola ini memastikan test menguji kode yang sama yang dipakai produksi — tidak ada duplikasi. - Baris
.PHONY: test clean(di Makefile): target non-file.make testdanmake cleantidak menghasilkan file bernamatestatauclean—.PHONYmemberitahu make agar selalu jalankan rule meski file dengan nama tersebut ada.
Proyek 5 selesai. Pada Proyek 6 kita akan membangun struktur data dinamis (linked list, stack, queue, hash table) yang memakai manajemen memori manual — perbedaan utama dari CsvTable statis di sini.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
csv_reader.c
Unduh berkas/* csv_reader.c -- pembaca file CSV sederhana. */
#include "csv_reader.h"
#include <stdio.h>
#include <string.h>
/* Memisahkan satu baris teks menjadi field-field dengan pemisah koma.
* Menulis hasil ke row->fields dan mengisi row->jumlah_field.
* Mengembalikan jumlah field yang berhasil diparse.
*/
static int pisahkan_baris(const char *line, CsvRow *row) {
if (row == 0) return 0;
row->jumlah_field = 0;
int i = 0; /* indeks field */
int j = 0; /* indeks char di dalam field */
while (line[i] != '\0' && row->jumlah_field < CSV_MAKS_FIELD) {
char c = line[i];
if (c == ',') {
row->fields[row->jumlah_field][j] = '\0';
row->jumlah_field++;
j = 0;
} else if (c != '\n' && c != '\r') {
if (j < CSV_MAKS_PANJANG_FIELD - 1) {
row->fields[row->jumlah_field][j] = c;
j++;
}
}
i++;
}
/* Tutup field terakhir. */
row->fields[row->jumlah_field][j] = '\0';
row->jumlah_field++;
return row->jumlah_field;
}
CsvStatus csv_baca(const char *path, CsvTable *tabel) {
if (path == 0 || tabel == 0) return CSV_ERR_BUKA_FILE;
FILE *f = fopen(path, "r");
if (f == 0) return CSV_ERR_BUKA_FILE;
tabel->jumlah_baris = 0;
char baris[512];
while (fgets(baris, sizeof(baris), f) != 0
&& tabel->jumlah_baris < CSV_MAKS_BARIS) {
/* Skip baris kosong. */
if (baris[0] == '\n' || baris[0] == '\r' || baris[0] == '\0') {
continue;
}
CsvRow *row = &tabel->baris[tabel->jumlah_baris];
int n = pisahkan_baris(baris, row);
if (n == 0) {
fclose(f);
return CSV_ERR_FORMAT;
}
tabel->jumlah_baris++;
}
fclose(f);
if (tabel->jumlah_baris == 0) {
return CSV_ERR_FORMAT;
}
return CSV_OK;
}
void csv_tampilkan(const CsvTable *tabel, FILE *out) {
if (tabel == 0 || out == 0) return;
for (int i = 0; i < tabel->jumlah_baris; i++) {
const CsvRow *r = &tabel->baris[i];
for (int j = 0; j < r->jumlah_field; j++) {
fprintf(out, "%s", r->fields[j]);
if (j < r->jumlah_field - 1) fprintf(out, " | ");
}
fprintf(out, "\n");
}
}
main.c
Unduh berkas/* main.c -- pemroses file CSV dari command line. */
#include "csv_reader.h"
#include "parser.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void penggunaan(const char *prog) {
fprintf(stderr, "Penggunaan: %s <file.csv> [kolom_statistik]\n", prog);
fprintf(stderr, " kolom_statistik: indeks kolom (0-indexed) untuk dihitung statistiknya.\n");
}
int main(int argc, char **argv) {
if (argc < 2) {
penggunaan(argv[0]);
return 1;
}
const char *path = argv[1];
int kolom_stat = -1;
if (argc >= 3) {
kolom_stat = atoi(argv[2]);
}
CsvTable tabel;
CsvStatus s = csv_baca(path, &tabel);
if (s != CSV_OK) {
fprintf(stderr, "Gagal membaca %s (kode %d)\n", path, s);
return 1;
}
printf("Berhasil memuat %d baris (termasuk header).\n\n", tabel.jumlah_baris);
csv_tampilkan(&tabel, stdout);
if (kolom_stat >= 0) {
Statistik stat;
ParserStatus ps = parser_statistik_kolom(&tabel, kolom_stat, 1, &stat);
if (ps == PARSER_OK) {
printf("\n");
parser_cetak_statistik(&stat, "kolom", stdout);
} else {
fprintf(stderr, "Gagal menghitung statistik kolom %d (kode %d)\n",
kolom_stat, ps);
return 1;
}
}
/* Export hasil (sederhana: tulis ulang tabel ke file output). */
FILE *out = fopen("output.csv", "w");
if (out != 0) {
for (int i = 0; i < tabel.jumlah_baris; i++) {
const CsvRow *r = &tabel.baris[i];
for (int j = 0; j < r->jumlah_field; j++) {
fprintf(out, "%s", r->fields[j]);
if (j < r->jumlah_field - 1) fprintf(out, ",");
}
fprintf(out, "\n");
}
fclose(out);
printf("\nHasil ditulis ke output.csv\n");
}
return 0;
}
parser.c
Unduh berkas/* parser.c -- parsing dan agregasi kolom CSV. */
#include "parser.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
ParserStatus parser_ke_double(const char *s, double *out) {
if (s == 0 || out == 0) return PARSER_ERR_KONVERSI;
/* Skip whitespace di awal. */
while (*s == ' ' || *s == '\t') s++;
if (*s == '\0') return PARSER_ERR_KONVERSI;
char *sisa = 0;
double v = strtod(s, &sisa);
if (sisa == s) return PARSER_ERR_KONVERSI;
*out = v;
return PARSER_OK;
}
int parser_valid_angka(const char *s) {
double dummy;
return parser_ke_double(s, &dummy) == PARSER_OK;
}
ParserStatus parser_statistik_kolom(const CsvTable *tabel,
int kolom,
int skip_header,
Statistik *stat) {
if (tabel == 0 || stat == 0) return PARSER_ERR_TIDAK_ADA_DATA;
if (kolom < 0 || kolom >= CSV_MAKS_FIELD) return PARSER_ERR_KOLOM_INVALID;
/* Inisialisasi statistik. */
stat->jumlah = 0;
stat->jumlah_nilai = 0.0;
stat->minimum = 0.0;
stat->maksimum = 0.0;
stat->rata_rata = 0.0;
int mulai = skip_header ? 1 : 0;
int ada_data = 0;
for (int i = mulai; i < tabel->jumlah_baris; i++) {
const CsvRow *row = &tabel->baris[i];
if (kolom >= row->jumlah_field) {
return PARSER_ERR_KOLOM_INVALID;
}
double v;
ParserStatus ps = parser_ke_double(row->fields[kolom], &v);
if (ps != PARSER_OK) {
return PARSER_ERR_KONVERSI;
}
if (!ada_data) {
stat->minimum = v;
stat->maksimum = v;
ada_data = 1;
} else {
if (v < stat->minimum) stat->minimum = v;
if (v > stat->maksimum) stat->maksimum = v;
}
stat->jumlah_nilai += v;
stat->jumlah++;
}
if (!ada_data) return PARSER_ERR_TIDAK_ADA_DATA;
stat->rata_rata = stat->jumlah_nilai / stat->jumlah;
return PARSER_OK;
}
void parser_cetak_statistik(const Statistik *stat, const char *nama_kolom, FILE *out) {
if (stat == 0 || out == 0) return;
fprintf(out, "Statistik kolom '%s':\n", nama_kolom ? nama_kolom : "?");
fprintf(out, " Jumlah data : %d\n", stat->jumlah);
fprintf(out, " Total : %g\n", stat->jumlah_nilai);
fprintf(out, " Minimum : %g\n", stat->minimum);
fprintf(out, " Maksimum : %g\n", stat->maksimum);
fprintf(out, " Rata-rata : %g\n", stat->rata_rata);
}
tests.c
Unduh berkas/* tests.c -- rangkaian test untuk modul csv_reader dan parser. */
#include "csv_reader.h"
#include "parser.h"
#include <stdio.h>
#include <math.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 int hampir_sama(double a, double b) {
return fabs(a - b) < 1e-9;
}
static void test_baca_csv(void) {
/* Buat file CSV test. */
FILE *f = fopen("/tmp/test_p5.csv", "w");
fputs("nama,umur,gaji\n", f);
fputs("Budi,25,5000\n", f);
fputs("Siti,30,7500\n", f);
fputs("Andi,28,6200\n", f);
fclose(f);
CsvTable tabel;
CsvStatus s = csv_baca("/tmp/test_p5.csv", &tabel);
PERIKSA(s == CSV_OK, "Baca CSV berhasil");
PERIKSA(tabel.jumlah_baris == 4, "Jumlah baris = 4 (1 header + 3 data)");
PERIKSA(tabel.baris[0].jumlah_field == 3, "Header punya 3 field");
PERIKSA(strcmp(tabel.baris[1].fields[0], "Budi") == 0,
"Baris 1 field 0 = 'Budi'");
}
static void test_baca_file_tidak_ada(void) {
CsvTable tabel;
CsvStatus s = csv_baca("/tmp/tidak_ada_p5.csv", &tabel);
PERIKSA(s == CSV_ERR_BUKA_FILE, "File tidak ada -> BUKA_FILE");
}
static void test_parser_ke_double(void) {
double v;
PERIKSA(parser_ke_double("3.14", &v) == PARSER_OK && hampir_sama(v, 3.14),
"Parse '3.14' -> 3.14");
PERIKSA(parser_ke_double(" 42 ", &v) == PARSER_OK && hampir_sama(v, 42.0),
"Parse ' 42 ' -> 42 (toleransi whitespace)");
PERIKSA(parser_ke_double("bukan-angka", &v) == PARSER_ERR_KONVERSI,
"Parse 'bukan-angka' -> KONVERSI error");
PERIKSA(parser_ke_double("", &v) == PARSER_ERR_KONVERSI,
"Parse string kosong -> KONVERSI error");
}
static void test_statistik_kolom(void) {
FILE *f = fopen("/tmp/test_p5_stat.csv", "w");
fputs("nama,nilai\n", f);
fputs("A,10\n", f);
fputs("B,20\n", f);
fputs("C,30\n", f);
fclose(f);
CsvTable tabel;
csv_baca("/tmp/test_p5_stat.csv", &tabel);
Statistik stat;
ParserStatus ps = parser_statistik_kolom(&tabel, 1, 1, &stat);
PERIKSA(ps == PARSER_OK, "Statistik kolom 1 (skip header) berhasil");
PERIKSA(stat.jumlah == 3, "Jumlah data = 3");
PERIKSA(hampir_sama(stat.jumlah_nilai, 60.0), "Total = 60");
PERIKSA(hampir_sama(stat.minimum, 10.0), "Minimum = 10");
PERIKSA(hampir_sama(stat.maksimum, 30.0), "Maksimum = 30");
PERIKSA(hampir_sama(stat.rata_rata, 20.0), "Rata-rata = 20");
}
static void test_statistik_kolom_invalid(void) {
FILE *f = fopen("/tmp/test_p5_bad.csv", "w");
fputs("nama,umur\n", f);
fputs("Budi,bukan-umur\n", f);
fclose(f);
CsvTable tabel;
csv_baca("/tmp/test_p5_bad.csv", &tabel);
Statistik stat;
ParserStatus ps = parser_statistik_kolom(&tabel, 1, 1, &stat);
PERIKSA(ps == PARSER_ERR_KONVERSI, "Field non-numerik -> KONVERSI error");
}
int main(void) {
test_baca_csv();
test_baca_file_tidak_ada();
test_parser_ke_double();
test_statistik_kolom();
test_statistik_kolom_invalid();
printf("\nRingkasan: %d lulus, %d gagal\n", lulus, gagal);
return gagal == 0 ? 0 : 1;
}