Program kita sudah lengkap secara fitur: menjelajah folder, memfilter, dan menampilkan metadata. Tetapi ada satu pertanyaan yang belum terjawab dari Bagian 1: apa yang terjadi bila pengguna salah mengetik opsi, memberikan --min-ukuran abc, atau menunjuk folder yang tidak ada? Versi kita sebelum ini menjawabnya dengan cara yang tidak terduga — dan program yang berperilaku tak terduga justru paling berbahaya justru karena diam. Bagian penutup proyek ini memasang perisai di setiap titik kegagalan yang mungkin, lalu mengunci seluruh perilaku program — jalur sukses maupun jalur galat — dengan harness pengujian yang menjalankan 22 cek.
Empat Lapis Perisai Galat #
Program final menangani galat di empat lapisan, masing-masing dengan mekanisme yang pas untuk jenis kegagalannya:
| Lapisan | Skenario gagal | Mekanisme | Perilaku program |
|---|---|---|---|
| Penguraian argumen | --min-ukuran abc | try/catch di sekitar std::stoull | pesan galat, return 1 |
| Penguraian argumen | opsi tidak dikenal | pemeriksaan awalan - | pesan + cara pakai, return 1 |
| Pemeriksaan folder | folder tidak ada | fs::exists versi error_code | pesan galat, return 1 |
| Pemindaian folder | folder tak terbaca | error_code dari kumpulkanEntri | pesan + ec.message(), return 1 |
Pola besarnya konsisten: setiap kegagalan dilaporkan ke std::cerr, program berhenti dengan exit code 1, dan tidak ada sebagian laporan yang dicetak ke std::cout. Dengan begitu, siapa pun yang memipa output program kita — manusia, script shell, atau CI — tetap mendapat data yang bersih, sementara exit code memberi tahu keberhasilan tanpa membaca satu baris pun.
Inti Penanganan Galat di main() #
Hampir semua perisai itu berada di main(). Berikut potongan src/pengelola-file.cpp dari awal penguraian argumen sampai pemindaian folder:
int main(int argc, char* argv[]) {
const std::vector<std::string> argumen(argv + 1, argv + argc);
fs::path folder = "data";
Kriteria kriteria;
bool detail = false;
for (std::size_t i = 0; i < argumen.size(); ++i) {
const std::string& arg = argumen[i];
if (arg == "--cari" && i + 1 < argumen.size()) {
kriteria.kata = argumen[++i];
} else if (arg == "--ekstensi" && i + 1 < argumen.size()) {
kriteria.ekstensi = argumen[++i];
} else if (arg == "--min-ukuran" && i + 1 < argumen.size()) {
try {
kriteria.ukuranMin = std::stoull(argumen[++i]);
} catch (const std::exception&) {
std::println(std::cerr, "Galat: --min-ukuran butuh angka, dapat '{}'",
argumen[i]);
return 1;
}
} else if (arg == "--detail") {
detail = true;
} else if (arg == "--bantu") {
tampilkanCaraPakai();
return 0;
} else if (!arg.empty() && arg.front() == '-') {
std::println(std::cerr, "Opsi tidak dikenal: {}", arg);
tampilkanCaraPakai();
return 1;
} else {
folder = arg;
}
}
// Folder default "data" dibuat sendiri bila belum ada (self-creating).
// Folder lain tidak dibuat: program melaporkan galat bila tidak ada.
if (folder == "data") {
if (!siapkanFolderContoh(folder)) {
return 1;
}
} else {
std::error_code ecAda;
if (!fs::exists(folder, ecAda) || ecAda) {
std::println(std::cerr,
"Galat: folder '{}' tidak ada. "
"Jalankan tanpa argumen untuk membuat folder contoh 'data'.",
folder.string());
return 1;
}
}
std::error_code ec;
const std::vector<InfoFile> semua = kumpulkanEntri(folder, ec);
if (ec) {
std::println(std::cerr, "Galat membaca folder '{}': {}", folder.string(),
ec.message());
return 1;
}Urutan keputusan di atas memakai semua teknik penanganan galat yang kita pelajari sejauh proyek ini:
try/catchdi sekitarstd::stoull:std::stoullmelemparstd::invalid_argumentbila string bukan angka. Kita menangkapnya, mencetak nilai yang ditolak, dan berhenti denganreturn 1— bukan melanjutkan dengan angka 0 yang menyesatkan.- Deteksi opsi tak dikenal (
arg.front() == '-'): argumen berawalan-dianggap opsi; bila tidak cocok dengan daftar yang dikenal, program melaporkannya alih-alih memperlakukannya sebagai nama folder. Program yang menerima ejaan salah dengan diam akan menjawab pertanyaan yang tidak ditanyakan. - Dua jalur folder: folder bawaan
datadibuat sendiri bila belum ada (kontrak self-creating sejak Bagian 1), sedangkan folder lain diverifikasi denganfs::existsversierror_code— kegagalan masuk keecAda, bukan lemparan exception. ec.message()untuk konteks: galat pembacaan folder dilaporkan lengkap dengan pesan sistemnya, sehingga pengguna tahu mengapa folder gagal dibaca, bukan hanya bahwa gagal.std::cerruntuk semua galat: aliran galat dipisahkan dari aliran data. Laporan tabel selalu keluar lewatstd::cout, dan setiap kegagalan keluar lewatstd::cerr.return 1di setiap pintu keluar gagal: kontrak exit code dari Chapter Integration Testing dipenuhi utuh — 0 berarti sukses, 1 berarti ada galat, tanpa pengecualian.
Kita buktikan klaim itu dengan menjalankan tiga kasus galat secara berurutan:
./pengelola --min-ukuran abc
./pengelola --urutkan
./pengelola arsip-lama
echo "exit terakhir: $?"Output yang Dihasilkan:
Galat: --min-ukuran butuh angka, dapat 'abc'
Opsi tidak dikenal: --urutkan
Cara pakai: pengelola [folder] [--cari kata] [--ekstensi .ext]
[--min-ukuran N] [--detail] [--bantu]
Galat: folder 'arsip-lama' tidak ada. Jalankan tanpa argumen untuk membuat folder contoh 'data'.
exit terakhir: 1Tiga kasus, tiga pesan yang presisi, dan exit terakhir: 1 menegaskan kontrak exit code. Perhatikan bahwa pesan galat bahkan menawarkan jalan keluar: bila folder tidak ada, program menyuruh kita menjalankannya tanpa argumen untuk membuat folder contoh. Pesan galat yang baik tidak hanya melapor — ia menuntun.
Menguji Kode Asli, Bukan Salinannya #
Harness pengujian proyek ini memakai satu trik preprocessor yang layak dicermati. Berkas sumber tetap menjadi satu-satunya sumber kebenaran, dan main() milik program CLI dibungkus macro sedemikian rupa — kerangkanya seperti ini, sementara isi lengkapnya ada pada blok program final di bawah:
#ifndef PENGELOLA_UJI
int main(int argc, char* argv[]) {
// isi main(): penguraian argumen, pemeriksaan folder, pemindaian,
// lalu tampilkan() — persis seperti yang dibedah di atas.
return 0;
}
#endifSaat program CLI dikompilasi secara normal, PENGELOLA_UJI tidak terdefinisi sehingga main() ikut terkompilasi. Saat harness mengompilasi sumber yang sama dengan #define PENGELOLA_UJI di depannya, main() asli dilewati dan yang menjadi titik masuk adalah main() milik harness. Semua function — siapkanFolderContoh, kumpulkanEntri, lolos, hakRingkas — diuji langsung dari berkas aslinya. Kita tidak pernah menyalin kode ke berkas uji, sehingga tidak ada risiko salinan yang mulai menyimpang dari aslinya.
Program Final Lengkap #
Inilah kondisi akhir src/pengelola-file.cpp, gabungan seluruh empat bagian. Kompilasi dari folder kerja kosong, lalu jalankan tanpa argumen:
g++ -std=c++23 -Wall -Wextra pengelola-final.cpp -o pengelola
./pengelolaProgram membuat folder data berisi tiga berkas contoh secara otomatis, sehingga output di bawah dapat direproduksi di mesin mana pun:
// pengelola-file.cpp — Proyek: Aplikasi Pengelola File
// Alat CLI untuk menjelajah, memfilter, dan memeriksa isi folder menggunakan
// std::filesystem. Folder contoh "data/" dibuat sendiri oleh program bila
// belum ada, sehingga hasil eksekusi selalu dapat direproduksi.
//
// Cara pakai:
// pengelola [folder] [--cari kata] [--ekstensi .ext] [--min-ukuran N]
// [--detail] [--bantu]
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <exception>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <print>
#include <string>
#include <utility>
#include <vector>
namespace fs = std::filesystem;
// ---------- Tipe data hasil pemindaian ----------
struct InfoFile {
fs::path jalur; // jalur lengkap berkas
std::uintmax_t ukuran; // ukuran dalam byte
fs::perms hak; // permission berkas
};
struct Kriteria {
std::string ekstensi; // "" = tanpa filter ekstensi
std::string kata; // "" = tanpa filter nama
std::uintmax_t ukuranMin = 0;
bool aktif() const { return !ekstensi.empty() || !kata.empty() || ukuranMin > 0; }
};
// ---------- Filter ----------
bool lolos(const InfoFile& berkas, const Kriteria& kriteria) {
if (!kriteria.ekstensi.empty() && berkas.jalur.extension().string() != kriteria.ekstensi) {
return false;
}
if (!kriteria.kata.empty() && !berkas.jalur.filename().string().contains(kriteria.kata)) {
return false;
}
if (berkas.ukuran < kriteria.ukuranMin) {
return false;
}
return true;
}
// Mengubah nilai fs::perms menjadi string gaya ls, misalnya "rw-r--r--".
// Operator << untuk fs::perms belum tersedia di libstdc++, jadi kita
// periksa sembilan bit hak akses satu per satu.
std::string hakRingkas(fs::perms hak) {
const unsigned bit = static_cast<unsigned>(hak) & 0777u;
std::string huruf = "rwxrwxrwx";
for (int i = 0; i < 9; ++i) {
if (!(bit & (0400u >> i))) {
huruf[static_cast<std::size_t>(i)] = '-';
}
}
return huruf;
}
// ---------- Folder contoh (self-creating) ----------
// Membuat folder beserta tiga berkas contoh bila belum ada. Berkas yang
// sudah ada dibiarkan apa adanya, sehingga memanggil fungsi ini dua kali
// memberikan hasil yang sama (idempoten).
bool siapkanFolderContoh(const fs::path& folder) {
std::error_code ec;
fs::create_directories(folder, ec);
if (ec) {
std::println(std::cerr, "Galat: tidak dapat membuat folder '{}': {}",
folder.string(), ec.message());
return false;
}
const std::vector<std::pair<std::string, std::string>> contoh = {
{"catatan.txt", "Baris pertama catatan.\nBaris kedua catatan.\n"},
{"laporan.csv", "id;nama;nilai\n1;Budi;90\n2;Siti;85\n"},
{"gambar.png", "\x89PNG\r\n\x1a\ndata gambar tiruan"},
};
for (const auto& [nama, isi] : contoh) {
const fs::path target = folder / nama;
std::error_code ecAda;
if (fs::exists(target, ecAda)) {
continue; // berkas sudah ada — jangan ditulis ulang
}
std::ofstream berkas(target, std::ios::binary);
if (!berkas) {
std::println(std::cerr, "Galat: tidak dapat menulis '{}'", target.string());
return false;
}
berkas << isi;
berkas.close();
// Permission ditetapkan eksplisit agar listing selalu sama (0644).
std::error_code ecHak;
fs::permissions(target,
fs::perms::owner_read | fs::perms::owner_write |
fs::perms::group_read | fs::perms::others_read,
ecHak);
if (ecHak) {
std::println(std::cerr, "Galat: tidak dapat mengatur permission '{}'",
target.string());
return false;
}
}
return true;
}
// ---------- Pemindaian folder (versi error_code) ----------
// Memindai satu folder dan mengumpulkan berkas reguler yang ada di dalamnya
// (tanpa masuk ke subfolder). Semua operasi filesystem memakai overload
// error_code sehingga kegagalan dilaporkan lewat parameter ec, bukan lewat
// exception. Urutan hasil diurutkan berdasarkan nama agar output stabil.
std::vector<InfoFile> kumpulkanEntri(const fs::path& folder, std::error_code& ec) {
std::vector<InfoFile> hasil;
for (fs::directory_iterator it(folder, fs::directory_options::skip_permission_denied, ec),
akhir;
it != akhir; it.increment(ec)) {
if (ec) {
break;
}
const fs::path jalur = it->path();
std::error_code ecEntri;
if (!it->is_regular_file(ecEntri) || ecEntri) {
continue; // subfolder, symlink rusak, dan sejenisnya dilewati
}
InfoFile info;
info.jalur = jalur;
info.ukuran = it->file_size(ecEntri);
if (ecEntri) {
info.ukuran = 0;
}
info.hak = fs::status(jalur, ecEntri).permissions();
if (ecEntri) {
info.hak = fs::perms::none;
}
hasil.push_back(info);
}
std::sort(hasil.begin(), hasil.end(),
[](const InfoFile& a, const InfoFile& b) {
return a.jalur.filename().string() < b.jalur.filename().string();
});
return hasil;
}
// ---------- Penampilan hasil ----------
void tampilkan(const fs::path& folder, const std::vector<InfoFile>& hasil,
const Kriteria& kriteria, bool detail) {
std::println("Folder: {}", folder.string());
if (hasil.empty()) {
std::println("Tidak ada berkas yang cocok dengan kriteria.");
return;
}
std::uintmax_t total = 0;
for (const InfoFile& berkas : hasil) {
const std::string nama = berkas.jalur.filename().string();
if (detail) {
// Mode --detail: waktu ubah ikut dicetak. Nilainya bergantung
// pada kapan berkas dibuat di mesin masing-masing, sehingga
// baris ini sengaja tidak dipakai sebagai output terverifikasi.
std::error_code ecWaktu;
const fs::file_time_type waktuTulis = fs::last_write_time(berkas.jalur, ecWaktu);
std::println("{}", nama);
std::println(" ukuran : {} byte", berkas.ukuran);
const unsigned bitHak = static_cast<unsigned>(berkas.hak) & 0777u;
std::println(" hak akses : {} ({:03o})", hakRingkas(berkas.hak), bitHak);
if (!ecWaktu) {
const auto waktuSistem = std::chrono::time_point_cast<std::chrono::seconds>(
fs::file_time_type::clock::to_sys(waktuTulis));
std::println(" waktu ubah : {:%Y-%m-%d %H:%M:%S} UTC", waktuSistem);
}
} else {
const unsigned bitHak = static_cast<unsigned>(berkas.hak) & 0777u;
std::println("{:<16}{:>8} byte {:03o}", nama, berkas.ukuran, bitHak);
}
total += berkas.ukuran;
}
std::println("total: {} byte", total);
if (kriteria.aktif()) {
std::println("ditemukan: {} berkas lolos filter", hasil.size());
}
}
void tampilkanCaraPakai() {
std::println("Cara pakai: pengelola [folder] [--cari kata] [--ekstensi .ext]");
std::println(" [--min-ukuran N] [--detail] [--bantu]");
}
// main() dibungkus #ifndef PENGELOLA_UJI: program uji (uji-pengelola.cpp)
// meng-include berkas ini dengan macro PENGELOLA_UJI aktif, sehingga main()
// asli tidak ikut terkompilasi dan seluruh fungsi di atas bisa diuji langsung.
#ifndef PENGELOLA_UJI
int main(int argc, char* argv[]) {
const std::vector<std::string> argumen(argv + 1, argv + argc);
fs::path folder = "data";
Kriteria kriteria;
bool detail = false;
for (std::size_t i = 0; i < argumen.size(); ++i) {
const std::string& arg = argumen[i];
if (arg == "--cari" && i + 1 < argumen.size()) {
kriteria.kata = argumen[++i];
} else if (arg == "--ekstensi" && i + 1 < argumen.size()) {
kriteria.ekstensi = argumen[++i];
} else if (arg == "--min-ukuran" && i + 1 < argumen.size()) {
try {
kriteria.ukuranMin = std::stoull(argumen[++i]);
} catch (const std::exception&) {
std::println(std::cerr, "Galat: --min-ukuran butuh angka, dapat '{}'",
argumen[i]);
return 1;
}
} else if (arg == "--detail") {
detail = true;
} else if (arg == "--bantu") {
tampilkanCaraPakai();
return 0;
} else if (!arg.empty() && arg.front() == '-') {
std::println(std::cerr, "Opsi tidak dikenal: {}", arg);
tampilkanCaraPakai();
return 1;
} else {
folder = arg;
}
}
// Folder default "data" dibuat sendiri bila belum ada (self-creating).
// Folder lain tidak dibuat: program melaporkan galat bila tidak ada.
if (folder == "data") {
if (!siapkanFolderContoh(folder)) {
return 1;
}
} else {
std::error_code ecAda;
if (!fs::exists(folder, ecAda) || ecAda) {
std::println(std::cerr,
"Galat: folder '{}' tidak ada. "
"Jalankan tanpa argumen untuk membuat folder contoh 'data'.",
folder.string());
return 1;
}
}
std::error_code ec;
const std::vector<InfoFile> semua = kumpulkanEntri(folder, ec);
if (ec) {
std::println(std::cerr, "Galat membaca folder '{}': {}", folder.string(),
ec.message());
return 1;
}
std::vector<InfoFile> cocok;
cocok.reserve(semua.size());
for (const InfoFile& berkas : semua) {
if (lolos(berkas, kriteria)) {
cocok.push_back(berkas);
}
}
tampilkan(folder, cocok, kriteria, detail);
return 0;
}
#endifOutput yang Dihasilkan:
Folder: data
catatan.txt 44 byte 644
gambar.png 26 byte 644
laporan.csv 34 byte 644
total: 104 byteTabelnya kini berkepala: baris Folder: data dari tampilkan() memberi konteks folder apa yang baru dilaporkan. Angka-angka di belakangnya tetap yang sudah kita kenal — 44, 26, dan 34 byte dengan permission oktal 644 — dan baris total: 104 byte menutup laporan. Jalankan program kedua kali tanpa menghapus folder data: output tetap identik, karena berkas contoh bersifat idempoten sejak Bagian 1.
Harness Pengujian: 22 Cek Hijau #
Terakhir, seluruh perilaku program kita kunci dengan harness. Perintah kompilasinya satu baris, dan hasil yang diharapkan adalah 22 cek hijau tanpa satu pun merah:
g++ -std=c++23 -Wall -Wextra pengelola-uji.cpp -o uji-pengelola
./uji-pengelolaBerkas pengelola-uji.cpp berikut adalah salinan tests/uji-pengelola.cpp yang disesuaikan jalur include-nya agar berdiri sendiri di bundle Bagian ini:
// pengelola-uji.cpp — harness pengujian untuk pengelola-final.cpp.
// Pola yang sama dengan harness mini pada chapter Unit Testing: macro CHECK,
// penghitung lulus/gagal, dan kontrak exit code. Bedanya, di sini kita
// meng-include berkas sumber asli dengan macro PENGELOLA_UJI aktif agar
// main() tidak ikut terkompilasi — yang diuji adalah kode aslinya,
// bukan salinannya. Berkas pengelola-final.cpp harus berada di folder
// yang sama dengan berkas ini.
#define PENGELOLA_UJI
#include "pengelola-final.cpp"
static int jumlahLulus = 0;
static int jumlahGagal = 0;
static std::string namaUjiAktif;
#define MULAI_UJI(nama) namaUjiAktif = (nama)
#define CHECK_TRUE(kondisi, deskripsi) \
cekBoolean((kondisi), (deskripsi), #kondisi)
#define CHECK_EQ(aktual, diharapkan) \
cekSama((aktual), (diharapkan), #aktual)
void cekBoolean(bool kondisi, const std::string& deskripsi, const char* ekspresi) {
if (kondisi) {
++jumlahLulus;
std::println("[ LULUS ] {} - {}", namaUjiAktif, deskripsi);
} else {
++jumlahGagal;
std::println("[ GAGAL ] {} - {} ({})", namaUjiAktif, deskripsi, ekspresi);
}
}
template <typename T, typename U>
void cekSama(const T& aktual, const U& diharapkan, const char* ekspresi) {
if (aktual == diharapkan) {
++jumlahLulus;
std::println("[ LULUS ] {} - {}", namaUjiAktif, ekspresi);
} else {
++jumlahGagal;
std::println("[ GAGAL ] {} - {} (aktual {}, diharapkan {})",
namaUjiAktif, ekspresi, aktual, diharapkan);
}
}
// Folder uji terpisah agar pengujian tidak menyentuh folder "data".
constexpr const char* FOLDER_UJI = "data-uji";
void ujiSiapkanFolderContoh() {
MULAI_UJI("siapkanFolderContoh");
CHECK_TRUE(siapkanFolderContoh(FOLDER_UJI), "folder contoh siap dipakai");
CHECK_TRUE(fs::exists(fs::path(FOLDER_UJI) / "catatan.txt"), "catatan.txt dibuat");
CHECK_TRUE(fs::exists(fs::path(FOLDER_UJI) / "gambar.png"), "gambar.png dibuat");
CHECK_TRUE(fs::exists(fs::path(FOLDER_UJI) / "laporan.csv"), "laporan.csv dibuat");
}
void ujiIdempoten() {
MULAI_UJI("siapkanFolderContoh idempoten");
CHECK_TRUE(siapkanFolderContoh(FOLDER_UJI), "panggilan kedua tetap sukses");
std::error_code ec;
const std::vector<InfoFile> entri = kumpulkanEntri(FOLDER_UJI, ec);
CHECK_EQ(entri.size(), std::size_t(3));
}
void ujiPemindaian() {
MULAI_UJI("kumpulkanEntri urut nama dengan ukuran dan permission");
std::error_code ec;
const std::vector<InfoFile> entri = kumpulkanEntri(FOLDER_UJI, ec);
CHECK_TRUE(!ec, "folder terbaca tanpa galat");
CHECK_EQ(entri[0].jalur.filename().string(), std::string("catatan.txt"));
CHECK_EQ(entri[0].ukuran, std::uintmax_t(44));
CHECK_EQ(entri[1].jalur.filename().string(), std::string("gambar.png"));
CHECK_EQ(entri[1].ukuran, std::uintmax_t(26));
CHECK_EQ(entri[2].jalur.filename().string(), std::string("laporan.csv"));
CHECK_EQ(entri[2].ukuran, std::uintmax_t(34));
const unsigned hak = static_cast<unsigned>(entri[0].hak) & 0777u;
CHECK_EQ(hak, 0644u);
CHECK_EQ(hakRingkas(entri[0].hak), std::string("rw-r--r--"));
}
void ujiFilterEkstensi() {
MULAI_UJI("filter ekstensi .txt");
std::error_code ec;
const std::vector<InfoFile> entri = kumpulkanEntri(FOLDER_UJI, ec);
Kriteria kriteria;
kriteria.ekstensi = ".txt";
int cocok = 0;
for (const InfoFile& berkas : entri) {
if (lolos(berkas, kriteria)) ++cocok;
}
CHECK_EQ(cocok, 1);
}
void ujiFilterNama() {
MULAI_UJI("filter nama mengandung \"an\"");
std::error_code ec;
const std::vector<InfoFile> entri = kumpulkanEntri(FOLDER_UJI, ec);
Kriteria kriteria;
kriteria.kata = "an";
int cocok = 0;
for (const InfoFile& berkas : entri) {
if (lolos(berkas, kriteria)) ++cocok;
}
CHECK_EQ(cocok, 2);
}
void ujiFilterUkuran() {
MULAI_UJI("filter ukuran minimum 30 byte");
std::error_code ec;
const std::vector<InfoFile> entri = kumpulkanEntri(FOLDER_UJI, ec);
Kriteria kriteria;
kriteria.ukuranMin = 30;
int cocok = 0;
for (const InfoFile& berkas : entri) {
if (lolos(berkas, kriteria)) ++cocok;
}
CHECK_EQ(cocok, 2);
}
void ujiFilterKombinasi() {
MULAI_UJI("filter kombinasi .csv + 30 byte");
std::error_code ec;
const std::vector<InfoFile> entri = kumpulkanEntri(FOLDER_UJI, ec);
Kriteria kriteria;
kriteria.ekstensi = ".csv";
kriteria.ukuranMin = 30;
const InfoFile* satuSatunya = nullptr;
for (const InfoFile& berkas : entri) {
if (lolos(berkas, kriteria)) satuSatunya = &berkas;
}
CHECK_TRUE(satuSatunya != nullptr, "ada satu berkas lolos");
CHECK_EQ(satuSatunya->jalur.filename().string(), std::string("laporan.csv"));
}
void ujiGalatFolderTidakAda() {
MULAI_UJI("folder tidak ada dilaporkan lewat error_code");
std::error_code ec;
const std::vector<InfoFile> entri = kumpulkanEntri("folder-tidak-ada-uji", ec);
CHECK_TRUE(ec.value() != 0, "error_code berisi galat");
CHECK_TRUE(entri.empty(), "hasil kosong bila folder tidak ada");
}
int main() {
ujiSiapkanFolderContoh();
ujiIdempoten();
ujiPemindaian();
ujiFilterEkstensi();
ujiFilterNama();
ujiFilterUkuran();
ujiFilterKombinasi();
ujiGalatFolderTidakAda();
std::println("");
std::println("ringkasan: {} lulus, {} gagal", jumlahLulus, jumlahGagal);
return jumlahGagal == 0 ? 0 : 1;
}Output yang Dihasilkan:
[ LULUS ] siapkanFolderContoh - folder contoh siap dipakai
[ LULUS ] siapkanFolderContoh - catatan.txt dibuat
[ LULUS ] siapkanFolderContoh - gambar.png dibuat
[ LULUS ] siapkanFolderContoh - laporan.csv dibuat
[ LULUS ] siapkanFolderContoh idempoten - panggilan kedua tetap sukses
[ LULUS ] siapkanFolderContoh idempoten - entri.size()
[ LULUS ] kumpulkanEntri urut nama dengan ukuran dan permission - folder terbaca tanpa galat
[ LULUS ] kumpulkanEntri urut nama dengan ukuran dan permission - entri[0].jalur.filename().string()
[ LULUS ] kumpulkanEntri urut nama dengan ukuran dan permission - entri[0].ukuran
[ LULUS ] kumpulkanEntri urut nama dengan ukuran dan permission - entri[1].jalur.filename().string()
[ LULUS ] kumpulkanEntri urut nama dengan ukuran dan permission - entri[1].ukuran
[ LULUS ] kumpulkanEntri urut nama dengan ukuran dan permission - entri[2].jalur.filename().string()
[ LULUS ] kumpulkanEntri urut nama dengan ukuran dan permission - entri[2].ukuran
[ LULUS ] kumpulkanEntri urut nama dengan ukuran dan permission - hak
[ LULUS ] kumpulkanEntri urut nama dengan ukuran dan permission - hakRingkas(entri[0].hak)
[ LULUS ] filter ekstensi .txt - cocok
[ LULUS ] filter nama mengandung "an" - cocok
[ LULUS ] filter ukuran minimum 30 byte - cocok
[ LULUS ] filter kombinasi .csv + 30 byte - ada satu berkas lolos
[ LULUS ] filter kombinasi .csv + 30 byte - satuSatunya->jalur.filename().string()
[ LULUS ] folder tidak ada dilaporkan lewat error_code - error_code berisi galat
[ LULUS ] folder tidak ada dilaporkan lewat error_code - hasil kosong bila folder tidak ada
ringkasan: 22 lulus, 0 gagalDua puluh dua cek, semuanya [ LULUS ], ditutup ringkasan: 22 lulus, 0 gagal dan exit code 0. Yang membuat daftar ini kuat adalah cakupannya: tiga cek pertama memastikan folder contoh terbentuk, cek idempoten membuktikan panggilan kedua tidak merusak apa pun, delapan cek pemindaian mengunci urutan nama beserta ukuran dan permission, lima cek filter menguji ekstensi, kata kunci, ukuran, dan kombinasinya, dan dua cek terakhir memastikan folder yang tidak ada dilaporkan lewat error_code — bukan dengan crash atau exception yang tak tertangkap. Perilaku jalur sukses dan jalur galat kini sama-sama terlindungi.
Dengan harness hijau, proyek pengelola file resmi tuntas. Empat bagian kita menghasilkan satu program CLI utuh: folder contoh yang dapat direproduksi, filter berlapis, metadata berformat rapi, penanganan galat menyeluruh, dan 22 cek otomatis yang berjalan kapan pun kita ingin memastikan tidak ada yang rusak. Pola-pola yang dipakai di sini — error_code, fail fast dengan exit code, harness tanpa salinan kode — akan langsung berguna di proyek berikutnya, ketika program kita mulai berjalan di banyak jalur eksekusi sekaligus: proyek aplikasi multithreaded.
Rujukan resmi: std::error_code (cppreference) , std::filesystem::exists (cppreference) , std::stoull (cppreference) .