Bagian terakhir ini menutup proyek dengan dua bukti yang saling melengkapi. Bukti pertama adalah program final — salinan utuh src/perpustakaan.cpp — yang dijalankan dengan skenario peminjaman terekam: seluruh alur berupa data stdin, sehingga siapa pun (termasuk verifier) bisa mengulang eksekusinya dan mendapat keluaran yang sama byte demi byte. Bukti kedua adalah test harness yang memanggil method Perpustakaan secara langsung, mengunci setiap invariant dari Bagian 1 sebagai pemeriksaan bernama, dan menutup dengan kontrak exit code. Dengan keduanya, klaim “alur peminjaman bekerja” berhenti menjadi perkiraan dan menjadi sesuatu yang bisa dites ulang setiap kali kode berubah.
Program Final dengan Skenario Terekam #
Berikut program utuh hasil lima bagian — versi final yang tersimpan juga di src/perpustakaan.cpp. Blok di bawah membawa skenario masukan lengkap: empat buku, dua anggota, seluruh jalur kegagalan peminjaman, pengembalian, pencarian, dan penghapusan:
// perpustakaan.cpp — Sistem Manajemen Perpustakaan mini (proyek C++).
//
// Program ini self-contained: seluruh model data, logika peminjaman,
// dan persistensi berkas ada dalam satu file, sehingga cukup
// dikompilasi dengan satu perintah:
//
// g++ -std=c++23 -Wall -Wextra perpustakaan.cpp -o perpustakaan
// ./perpustakaan < skenario.txt
//
// Antarmuka: setiap baris stdin adalah satu perintah berkolom '|' —
// BUKU, ANGGOTA, PINJAM, KEMBALI, CARI, HAPUS, SIMPAN, MUAT, dan LAPOR.
// Karena seluruh skenario berupa data (bukan kode), alur peminjaman
// bisa direkam lalu dijalankan ulang secara deterministik — pola ini
// yang kita pakai untuk menguji program final di Bagian 5.
//
// Format penyimpanan: teks polos, satu baris per entitas, dengan '|'
// sebagai pemisah kolom (dibahas pada Bagian 4).
#include <algorithm>
#include <cctype>
#include <charconv>
#include <expected>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <optional>
#include <string>
#include <utility>
#include <vector>
// ============================ Model data ============================
// Satu buku diidentifikasi oleh ISBN yang unik. `stok` adalah jumlah
// eksemplar yang tersedia saat ini (invarian: tidak pernah negatif,
// berkurang satu saat dipinjam, bertambah satu saat dikembalikan).
struct Buku {
std::string isbn;
std::string judul;
std::string penulis;
int stok = 0;
};
// Anggota perpustakaan. Buku yang sedang dipinjam TIDAK disimpan di
// sini; hubungan itu dicatat oleh class Peminjaman agar ada satu
// sumber kebenaran (single source of truth).
struct Anggota {
int id = 0;
std::string nama;
};
enum class StatusPeminjaman { Dipinjam, Kembali };
// Satu catatan peminjaman menghubungkan satu Buku dan satu Anggota.
struct Peminjaman {
std::string isbn;
int idAnggota = 0;
StatusPeminjaman status = StatusPeminjaman::Dipinjam;
};
// Aturan main: satu anggota maksimal meminjam tiga buku sekaligus.
constexpr int BATAS_PINJAM = 3;
std::string statusKeTeks(StatusPeminjaman status) {
return status == StatusPeminjaman::Dipinjam ? "Dipinjam" : "Kembali";
}
// Mengubah huruf menjadi huruf kecil untuk pencarian yang tidak peka
// huruf besar-kecil. Cast ke unsigned char mencegah undefined behavior
// untuk karakter bernilai negatif.
std::string kecilkan(std::string teks) {
for (char& c : teks) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
return teks;
}
// Konversi teks ke int yang ketat: seluruh teks harus berupa angka.
// Mengembalikan std::nullopt bila gagal — tanpa exception.
std::optional<int> keInt(const std::string& teks) {
int nilai = 0;
const char* awal = teks.data();
const char* akhir = awal + teks.size();
auto [ptr, ec] = std::from_chars(awal, akhir, nilai);
if (ec != std::errc{} || ptr != akhir) {
return std::nullopt;
}
return nilai;
}
// Memecah satu baris teks berdasarkan satu karakter pemisah. Kolom
// kosong tetap dihasilkan, sehingga jumlah kolom selalu bisa diverifikasi.
std::vector<std::string> pisah(const std::string& teks, char pemisah) {
std::vector<std::string> hasil;
std::string::size_type awal = 0;
while (true) {
const std::string::size_type posisi = teks.find(pemisah, awal);
if (posisi == std::string::npos) {
hasil.push_back(teks.substr(awal));
return hasil;
}
hasil.push_back(teks.substr(awal, posisi - awal));
awal = posisi + 1;
}
}
// ============================ Perpustakaan ============================
class Perpustakaan {
public:
// ---------- pengelolaan koleksi ----------
bool tambahBuku(const Buku& buku) {
if (temukanBuku(buku.isbn) != nullptr) {
return false; // invarian: ISBN harus unik
}
daftarBuku_.push_back(buku);
return true;
}
bool hapusBuku(const std::string& isbn) {
// Buku yang sedang dipinjam tidak boleh lepas dari koleksi.
if (temukanPeminjamanAktif(isbn) != nullptr) {
return false;
}
const std::size_t jumlahSebelum = daftarBuku_.size();
daftarBuku_.erase(
std::remove_if(daftarBuku_.begin(), daftarBuku_.end(),
[&](const Buku& b) { return b.isbn == isbn; }),
daftarBuku_.end());
return daftarBuku_.size() < jumlahSebelum;
}
bool daftarAnggota(const Anggota& anggota) {
if (anggota.id <= 0 || temukanAnggota(anggota.id) != nullptr) {
return false; // invarian: id positif dan unik
}
daftarAnggota_.push_back(anggota);
return true;
}
// ---------- pencarian ----------
const Buku* cariBuku(const std::string& isbn) const {
return temukanBuku(isbn);
}
const Anggota* cariAnggota(int id) const {
return temukanAnggota(id);
}
// Pencarian judul dan penulis: tidak peka huruf besar-kecil dan
// boleh cocok sebagian (substring).
std::vector<Buku> cariByJudul(const std::string& kunci) const {
return saring(kunci, [](const Buku& b) { return b.judul; });
}
std::vector<Buku> cariByPenulis(const std::string& kunci) const {
return saring(kunci, [](const Buku& b) { return b.penulis; });
}
// ---------- alur peminjaman ----------
std::expected<void, std::string> pinjam(const std::string& isbn, int idAnggota) {
const Buku* buku = cariBuku(isbn);
if (buku == nullptr) {
return std::unexpected("buku dengan ISBN " + isbn + " tidak ditemukan");
}
const Anggota* anggota = cariAnggota(idAnggota);
if (anggota == nullptr) {
return std::unexpected("anggota dengan id " + std::to_string(idAnggota)
+ " tidak terdaftar");
}
if (temukanPeminjamanAktif(isbn, idAnggota) != nullptr) {
return std::unexpected(anggota->nama + " masih meminjam buku yang sama");
}
if (hitungPinjamanAktif(idAnggota) >= BATAS_PINJAM) {
return std::unexpected(anggota->nama + " sudah mencapai batas "
+ std::to_string(BATAS_PINJAM) + " buku");
}
if (buku->stok <= 0) {
return std::unexpected("stok \"" + buku->judul + "\" habis");
}
// Semua aturan lolos — baru state diubah.
Buku* bukuMut = temukanBuku(isbn);
--bukuMut->stok;
daftarPeminjaman_.push_back(
Peminjaman{isbn, idAnggota, StatusPeminjaman::Dipinjam});
return {};
}
std::expected<void, std::string> kembalikan(const std::string& isbn, int idAnggota) {
Peminjaman* catatan = temukanPeminjamanAktif(isbn, idAnggota);
if (catatan == nullptr) {
return std::unexpected("tidak ada peminjaman aktif untuk ISBN " + isbn
+ " oleh anggota id " + std::to_string(idAnggota));
}
catatan->status = StatusPeminjaman::Kembali;
if (Buku* buku = temukanBuku(isbn)) {
++buku->stok;
}
return {};
}
int hitungPinjamanAktif(int idAnggota) const {
int jumlah = 0;
for (const Peminjaman& p : daftarPeminjaman_) {
if (p.idAnggota == idAnggota && p.status == StatusPeminjaman::Dipinjam) {
++jumlah;
}
}
return jumlah;
}
std::vector<const Peminjaman*> peminjamanAktif() const {
std::vector<const Peminjaman*> hasil;
for (const Peminjaman& p : daftarPeminjaman_) {
if (p.status == StatusPeminjaman::Dipinjam) {
hasil.push_back(&p);
}
}
return hasil;
}
// ---------- persistensi ----------
bool simpanKe(const std::string& path) const {
std::ofstream keluar{path};
if (!keluar) {
return false;
}
for (const Buku& b : daftarBuku_) {
keluar << "BUKU|" << b.isbn << '|' << b.judul << '|' << b.penulis
<< '|' << b.stok << '\n';
}
for (const Anggota& a : daftarAnggota_) {
keluar << "ANGGOTA|" << a.id << '|' << a.nama << '\n';
}
for (const Peminjaman& p : daftarPeminjaman_) {
keluar << "PEMINJAMAN|" << p.isbn << '|' << p.idAnggota << '|'
<< statusKeTeks(p.status) << '\n';
}
return static_cast<bool>(keluar);
}
bool muatDari(const std::string& path) {
std::ifstream masuk{path};
if (!masuk) {
return false;
}
// Muat ke objek sementara; data lama tetap utuh bila berkas rusak.
Perpustakaan hasil;
std::string baris;
while (std::getline(masuk, baris)) {
if (baris.empty()) {
continue;
}
const std::vector<std::string> kolom = pisah(baris, '|');
if (kolom[0] == "BUKU" && kolom.size() == 5) {
const auto stok = keInt(kolom[4]);
if (!stok || *stok < 0
|| !hasil.tambahBuku(Buku{kolom[1], kolom[2], kolom[3], *stok})) {
return false;
}
} else if (kolom[0] == "ANGGOTA" && kolom.size() == 3) {
const auto id = keInt(kolom[1]);
if (!id || *id <= 0 || !hasil.daftarAnggota(Anggota{*id, kolom[2]})) {
return false;
}
} else if (kolom[0] == "PEMINJAMAN" && kolom.size() == 4) {
const auto id = keInt(kolom[2]);
if (!id) {
return false;
}
const StatusPeminjaman status = kolom[3] == "Dipinjam"
? StatusPeminjaman::Dipinjam
: StatusPeminjaman::Kembali;
hasil.daftarPeminjaman_.push_back(Peminjaman{kolom[1], *id, status});
} else {
return false; // format tidak dikenal — gagal dan jangan ubah state
}
}
*this = std::move(hasil);
return true;
}
// ---------- tampilan ----------
void tampilkanKoleksi() const {
std::cout << std::left << std::setw(18) << "ISBN" << ' '
<< std::setw(30) << "Judul" << ' ' << std::setw(15) << "Penulis"
<< ' ' << std::right << std::setw(4) << "Stok" << '\n';
std::cout << std::string(70, '-') << '\n';
for (const Buku& b : daftarBuku_) {
std::cout << std::left << std::setw(18) << b.isbn << ' '
<< std::setw(30) << b.judul << ' ' << std::setw(15)
<< b.penulis << ' ' << std::right << std::setw(4) << b.stok
<< '\n';
}
}
void tampilkanPeminjamanAktif() const {
const std::vector<const Peminjaman*> aktif = peminjamanAktif();
std::cout << "Peminjaman aktif: " << aktif.size() << '\n';
for (const Peminjaman* p : aktif) {
const Buku* buku = cariBuku(p->isbn);
const Anggota* anggota = cariAnggota(p->idAnggota);
std::cout << " \"" << (buku ? buku->judul : p->isbn)
<< "\" dipinjam oleh " << (anggota ? anggota->nama : "(tidak dikenal)")
<< '\n';
}
}
private:
Buku* temukanBuku(const std::string& isbn) {
for (Buku& b : daftarBuku_) {
if (b.isbn == isbn) return &b;
}
return nullptr;
}
const Buku* temukanBuku(const std::string& isbn) const {
for (const Buku& b : daftarBuku_) {
if (b.isbn == isbn) return &b;
}
return nullptr;
}
Anggota* temukanAnggota(int id) {
for (Anggota& a : daftarAnggota_) {
if (a.id == id) return &a;
}
return nullptr;
}
const Anggota* temukanAnggota(int id) const {
for (const Anggota& a : daftarAnggota_) {
if (a.id == id) return &a;
}
return nullptr;
}
Peminjaman* temukanPeminjamanAktif(const std::string& isbn) {
for (Peminjaman& p : daftarPeminjaman_) {
if (p.isbn == isbn && p.status == StatusPeminjaman::Dipinjam) return &p;
}
return nullptr;
}
const Peminjaman* temukanPeminjamanAktif(const std::string& isbn) const {
for (const Peminjaman& p : daftarPeminjaman_) {
if (p.isbn == isbn && p.status == StatusPeminjaman::Dipinjam) return &p;
}
return nullptr;
}
Peminjaman* temukanPeminjamanAktif(const std::string& isbn, int idAnggota) {
for (Peminjaman& p : daftarPeminjaman_) {
if (p.isbn == isbn && p.idAnggota == idAnggota
&& p.status == StatusPeminjaman::Dipinjam) {
return &p;
}
}
return nullptr;
}
const Peminjaman* temukanPeminjamanAktif(const std::string& isbn,
int idAnggota) const {
for (const Peminjaman& p : daftarPeminjaman_) {
if (p.isbn == isbn && p.idAnggota == idAnggota
&& p.status == StatusPeminjaman::Dipinjam) {
return &p;
}
}
return nullptr;
}
template <typename Pengambil>
std::vector<Buku> saring(const std::string& kunci, Pengambil ambil) const {
const std::string kunciKecil = kecilkan(kunci);
std::vector<Buku> hasil;
for (const Buku& b : daftarBuku_) {
if (kecilkan(ambil(b)).find(kunciKecil) != std::string::npos) {
hasil.push_back(b);
}
}
return hasil;
}
std::vector<Buku> daftarBuku_;
std::vector<Anggota> daftarAnggota_;
std::vector<Peminjaman> daftarPeminjaman_;
};
// ============================ Driver perintah stdin ============================
// Setiap baris stdin adalah satu perintah berkolom '|'. Dengan skenario
// berupa data, alur peminjaman bisa direkam, dibagikan, dan dijalankan
// ulang persis sama — fondasi pengujian alur di Bagian 5.
void prosesBaris(Perpustakaan& p, const std::string& baris) {
const std::vector<std::string> kolom = pisah(baris, '|');
const std::string& cmd = kolom[0];
if (cmd == "BUKU" && kolom.size() == 5) {
const auto stok = keInt(kolom[4]);
if (!stok || *stok < 0) {
std::cout << "gagal: stok bukan bilangan bulat non-negatif: \""
<< kolom[4] << "\"\n";
return;
}
if (p.tambahBuku(Buku{kolom[1], kolom[2], kolom[3], *stok})) {
std::cout << "ok: buku \"" << kolom[2] << "\" masuk koleksi\n";
} else {
std::cout << "gagal: ISBN " << kolom[1] << " sudah terdaftar\n";
}
} else if (cmd == "ANGGOTA" && kolom.size() == 3) {
const auto id = keInt(kolom[1]);
if (!id || *id <= 0) {
std::cout << "gagal: id anggota tidak valid: \"" << kolom[1]
<< "\"\n";
return;
}
if (p.daftarAnggota(Anggota{*id, kolom[2]})) {
std::cout << "ok: anggota " << kolom[2] << " (id " << *id
<< ") terdaftar\n";
} else {
std::cout << "gagal: id " << *id << " sudah terdaftar\n";
}
} else if (cmd == "PINJAM" && kolom.size() == 3) {
const auto id = keInt(kolom[2]);
if (!id) {
std::cout << "gagal: id anggota bukan angka: \"" << kolom[2]
<< "\"\n";
return;
}
auto hasil = p.pinjam(kolom[1], *id);
if (hasil) {
std::cout << "ok: " << p.cariAnggota(*id)->nama << " meminjam \""
<< p.cariBuku(kolom[1])->judul << "\"\n";
} else {
std::cout << "gagal: " << hasil.error() << '\n';
}
} else if (cmd == "KEMBALI" && kolom.size() == 3) {
const auto id = keInt(kolom[2]);
if (!id) {
std::cout << "gagal: id anggota bukan angka: \"" << kolom[2]
<< "\"\n";
return;
}
auto hasil = p.kembalikan(kolom[1], *id);
if (hasil) {
const Buku* buku = p.cariBuku(kolom[1]);
std::cout << "ok: " << p.cariAnggota(*id)->nama
<< " mengembalikan \""
<< (buku ? buku->judul : kolom[1]) << "\"\n";
} else {
std::cout << "gagal: " << hasil.error() << '\n';
}
} else if (cmd == "CARI" && kolom.size() == 2) {
// Gabungkan hasil pencarian judul dan penulis, buang duplikat ISBN.
std::vector<Buku> hasil = p.cariByJudul(kolom[1]);
for (const Buku& b : p.cariByPenulis(kolom[1])) {
bool sudahAda = false;
for (const Buku& c : hasil) {
if (c.isbn == b.isbn) {
sudahAda = true;
break;
}
}
if (!sudahAda) {
hasil.push_back(b);
}
}
std::cout << "cari \"" << kolom[1] << "\": " << hasil.size()
<< " hasil\n";
for (const Buku& b : hasil) {
std::cout << " - " << b.isbn << " | " << b.judul << " | "
<< b.penulis << " | stok " << b.stok << '\n';
}
} else if (cmd == "HAPUS" && kolom.size() == 2) {
if (p.hapusBuku(kolom[1])) {
std::cout << "ok: buku dengan ISBN " << kolom[1]
<< " dikeluarkan dari koleksi\n";
} else {
std::cout << "gagal: buku tidak bisa dihapus (tidak ada atau "
"sedang dipinjam)\n";
}
} else if (cmd == "SIMPAN" && kolom.size() == 2) {
if (p.simpanKe(kolom[1])) {
std::cout << "ok: data disimpan ke " << kolom[1] << '\n';
} else {
std::cout << "gagal: tidak bisa menulis " << kolom[1] << '\n';
}
} else if (cmd == "MUAT" && kolom.size() == 2) {
if (p.muatDari(kolom[1])) {
std::cout << "ok: data dimuat dari " << kolom[1] << '\n';
} else {
std::cout << "gagal: tidak bisa membaca " << kolom[1] << '\n';
}
} else if (cmd == "LAPOR" && kolom.size() == 1) {
p.tampilkanKoleksi();
p.tampilkanPeminjamanAktif();
} else {
std::cout << "gagal: perintah tidak dikenal atau jumlah kolom salah: "
<< baris << '\n';
}
}
int main() {
Perpustakaan p;
std::string baris;
while (std::getline(std::cin, baris)) {
if (!baris.empty()) {
prosesBaris(p, baris);
}
}
}Skenarionya sengaja dirancang agar setiap gerbang peminjaman dari Bagian 3 bersuara minimal satu kali, plus siklus pengembalian yang membuka jalan bagi peminjam berikutnya.
Output yang Dihasilkan:
ok: buku "Pemrograman C++ Modern" masuk koleksi
ok: buku "Algoritma dan Struktur Data" masuk koleksi
ok: buku "Clean Code" masuk koleksi
ok: buku "Struktur Data Dasar" masuk koleksi
ok: anggota Budi (id 1) terdaftar
ok: anggota Siti (id 2) terdaftar
ok: Budi meminjam "Pemrograman C++ Modern"
gagal: Budi masih meminjam buku yang sama
ok: Siti meminjam "Algoritma dan Struktur Data"
gagal: stok "Algoritma dan Struktur Data" habis
ok: Budi meminjam "Clean Code"
ok: Budi meminjam "Struktur Data Dasar"
gagal: Budi sudah mencapai batas 3 buku
gagal: tidak ada peminjaman aktif untuk ISBN 978-6022 oleh anggota id 1
ok: Budi mengembalikan "Pemrograman C++ Modern"
ok: Siti meminjam "Pemrograman C++ Modern"
cari "c++": 1 hasil
- 978-6021 | Pemrograman C++ Modern | Budi Santoso | stok 1
cari "SITI": 1 hasil
- 978-6022 | Algoritma dan Struktur Data | Siti Rahmawati | stok 0
gagal: buku tidak bisa dihapus (tidak ada atau sedang dipinjam)
ok: Siti mengembalikan "Algoritma dan Struktur Data"
ok: buku dengan ISBN 978-6022 dikeluarkan dari koleksi
ISBN Judul Penulis Stok
----------------------------------------------------------------------
978-6021 Pemrograman C++ Modern Budi Santoso 1
978-6023 Clean Code Dewi Lestari 2
978-6024 Struktur Data Dasar Budi Santoso 0
Peminjaman aktif: 3
"Clean Code" dipinjam oleh Budi
"Struktur Data Dasar" dipinjam oleh Budi
"Pemrograman C++ Modern" dipinjam oleh SitiBedah Alur Peminjaman Baris demi Baris #
Keluaran di atas tidak akan masuk akal tanpa dipetakan ke gerbang-gerbang pinjam. Mari kita telusuri baris skenario yang menyentuh alur peminjaman, satu per satu:
PINJAM|978-6021|1(Budi, stok 2) — buku ada, anggota terdaftar, belum ada pinjaman aktif untuk pasangan ini, jumlah pinjaman Budi nol, stok 2. Kelima gerbang lolos: stok turun ke 1 dan catatanDipinjamdibuat. Keluaranok: Budi meminjam "Pemrograman C++ Modern".PINJAM|978-6021|1(Budi lagi, ISBN sama) — gerbang ketiga menemukan catatan aktif dengan pasangan yang sama dan menolak sebelum stok tersentuh. Keluarangagal: Budi masih meminjam buku yang sama, stok tetap 1.PINJAM|978-6022|2(Siti, stok 1) — semua gerbang lolos: stok978-6022turun ke 0. Keluaranok: Siti meminjam "Algoritma dan Struktur Data".PINJAM|978-6022|1(Budi, stok 0) — gerbang batas belum bicara karena Budi baru memegang satu buku, tetapi gerbang kelima menemukanstok <= 0. Keluarangagal: stok "Algoritma dan Struktur Data" habis.PINJAM|978-6023|1danPINJAM|978-6024|1(Budi) — dua pinjaman sukses berturut-turut membawa jumlah pinjaman aktif Budi ke tiga, tepatBATAS_PINJAM.PINJAM|978-6022|1(Budi keempat) — gerbang keempat mendahului gerbang stok: meskipun stok978-6022juga nol, yang dilaporkangagal: Budi sudah mencapai batas 3 buku. Urutan gerbang menentukan satu-satunya pesan yang sah.KEMBALI|978-6022|1(Budi) — Budi tidak pernah meminjam judul itu;temukanPeminjamanAktiftidak menemukan catatan, jadi keluarangagal: tidak ada peminjaman aktif untuk ISBN 978-6022 oleh anggota id 1. Tidak ada stok yang dipulihkan.KEMBALI|978-6021|1(Budi) — catatan aktif ditemukan, status berubahKembali, stok naik ke 2. Keluaranok: Budi mengembalikan "Pemrograman C++ Modern".PINJAM|978-6021|2(Siti) — buku yang sama yang ditolak untuk Budi kini bisa dipinjam Siti: stok sudah dipulihkan dan pasangan (ISBN, Siti) belum ada catatan aktifnya. Keluaranok: Siti meminjam "Pemrograman C++ Modern".
Sisa skenario memeriksa sisi lain dari sistem: CARI|c++ dan CARI|SITI membuktikan pencarian judul dan penulis dari Bagian 2 masih berjalan di tengah pinjaman aktif; HAPUS|978-6022 pertama ditolak karena buku itu sedang dipinjam Siti; setelah KEMBALI|978-6022|2 memulihkan stok, HAPUS|978-6022 kedua berhasil — invariant penghapusan dari Bagian 2 bekerja sama dengan alur peminjaman. LAPOR di akhir menyimpulkan segalanya: tiga judul tersisa di tabel, dan tiga peminjaman aktif yang rinciannya cocok dengan sejarah skenario.
Test Harness yang Menuntut Exit Code 0 #
Skenario terekam membuktikan perilaku program dari luar; harness di bawah membuktikannya dari dalam. Ia mengikuti pola Chapter Unit Testing — macro MULAI_UJI, CHECK_TRUE, CHECK_EQ, penghitung lulus/gagal, dan gaya Arrange-Act-Assert — lalu menyalin model data dan class Perpustakaan ke dalam satu berkas agar bisa dikompilasi sendiri:
// uji-perpustakaan.cpp — test harness untuk sistem manajemen perpustakaan.
//
// Menggunakan mini harness dengan pola yang sama seperti Chapter Unit
// Testing: macro MULAI_UJI/CHECK_TRUE/CHECK_EQ, penghitung lulus-gagal,
// dan kontrak exit code (0 berarti semua test lulus). Bagian model data
// dan class Perpustakaan disalin apa adanya dari src/perpustakaan.cpp.
//
// g++ -std=c++23 -Wall -Wextra uji-perpustakaan.cpp -o uji-perpustakaan
// ./uji-perpustakaan
#include <algorithm>
#include <cctype>
#include <charconv>
#include <cstdio>
#include <expected>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <optional>
#include <string>
#include <utility>
#include <vector>
// ---------- mini test harness (pola Chapter Unit Testing) ----------
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::cout << "[ LULUS ] " << namaUjiAktif << " - " << deskripsi << '\n';
} else {
++jumlahGagal;
std::cout << "[ GAGAL ] " << namaUjiAktif << " - " << deskripsi
<< " (" << ekspresi << " bernilai false)\n";
}
}
template <typename T, typename U>
void cekSama(const T& aktual, const U& diharapkan, const char* ekspresi) {
if (aktual == diharapkan) {
++jumlahLulus;
std::cout << "[ LULUS ] " << namaUjiAktif << " - " << ekspresi << '\n';
} else {
++jumlahGagal;
std::cout << "[ GAGAL ] " << namaUjiAktif << " - " << ekspresi
<< " (aktual " << aktual << ", diharapkan " << diharapkan << ")\n";
}
}
// ============================ Model data ============================
// Satu buku diidentifikasi oleh ISBN yang unik. `stok` adalah jumlah
// eksemplar yang tersedia saat ini (invarian: tidak pernah negatif,
// berkurang satu saat dipinjam, bertambah satu saat dikembalikan).
struct Buku {
std::string isbn;
std::string judul;
std::string penulis;
int stok = 0;
};
// Anggota perpustakaan. Buku yang sedang dipinjam TIDAK disimpan di
// sini; hubungan itu dicatat oleh class Peminjaman agar ada satu
// sumber kebenaran (single source of truth).
struct Anggota {
int id = 0;
std::string nama;
};
enum class StatusPeminjaman { Dipinjam, Kembali };
// Satu catatan peminjaman menghubungkan satu Buku dan satu Anggota.
struct Peminjaman {
std::string isbn;
int idAnggota = 0;
StatusPeminjaman status = StatusPeminjaman::Dipinjam;
};
// Aturan main: satu anggota maksimal meminjam tiga buku sekaligus.
constexpr int BATAS_PINJAM = 3;
std::string statusKeTeks(StatusPeminjaman status) {
return status == StatusPeminjaman::Dipinjam ? "Dipinjam" : "Kembali";
}
// Mengubah huruf menjadi huruf kecil untuk pencarian yang tidak peka
// huruf besar-kecil. Cast ke unsigned char mencegah undefined behavior
// untuk karakter bernilai negatif.
std::string kecilkan(std::string teks) {
for (char& c : teks) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
return teks;
}
// Konversi teks ke int yang ketat: seluruh teks harus berupa angka.
// Mengembalikan std::nullopt bila gagal — tanpa exception.
std::optional<int> keInt(const std::string& teks) {
int nilai = 0;
const char* awal = teks.data();
const char* akhir = awal + teks.size();
auto [ptr, ec] = std::from_chars(awal, akhir, nilai);
if (ec != std::errc{} || ptr != akhir) {
return std::nullopt;
}
return nilai;
}
// Memecah satu baris teks berdasarkan satu karakter pemisah. Kolom
// kosong tetap dihasilkan, sehingga jumlah kolom selalu bisa diverifikasi.
std::vector<std::string> pisah(const std::string& teks, char pemisah) {
std::vector<std::string> hasil;
std::string::size_type awal = 0;
while (true) {
const std::string::size_type posisi = teks.find(pemisah, awal);
if (posisi == std::string::npos) {
hasil.push_back(teks.substr(awal));
return hasil;
}
hasil.push_back(teks.substr(awal, posisi - awal));
awal = posisi + 1;
}
}
// ============================ Perpustakaan ============================
class Perpustakaan {
public:
// ---------- pengelolaan koleksi ----------
bool tambahBuku(const Buku& buku) {
if (temukanBuku(buku.isbn) != nullptr) {
return false; // invarian: ISBN harus unik
}
daftarBuku_.push_back(buku);
return true;
}
bool hapusBuku(const std::string& isbn) {
// Buku yang sedang dipinjam tidak boleh lepas dari koleksi.
if (temukanPeminjamanAktif(isbn) != nullptr) {
return false;
}
const std::size_t jumlahSebelum = daftarBuku_.size();
daftarBuku_.erase(
std::remove_if(daftarBuku_.begin(), daftarBuku_.end(),
[&](const Buku& b) { return b.isbn == isbn; }),
daftarBuku_.end());
return daftarBuku_.size() < jumlahSebelum;
}
bool daftarAnggota(const Anggota& anggota) {
if (anggota.id <= 0 || temukanAnggota(anggota.id) != nullptr) {
return false; // invarian: id positif dan unik
}
daftarAnggota_.push_back(anggota);
return true;
}
// ---------- pencarian ----------
const Buku* cariBuku(const std::string& isbn) const {
return temukanBuku(isbn);
}
const Anggota* cariAnggota(int id) const {
return temukanAnggota(id);
}
// Pencarian judul dan penulis: tidak peka huruf besar-kecil dan
// boleh cocok sebagian (substring).
std::vector<Buku> cariByJudul(const std::string& kunci) const {
return saring(kunci, [](const Buku& b) { return b.judul; });
}
std::vector<Buku> cariByPenulis(const std::string& kunci) const {
return saring(kunci, [](const Buku& b) { return b.penulis; });
}
// ---------- alur peminjaman ----------
std::expected<void, std::string> pinjam(const std::string& isbn, int idAnggota) {
const Buku* buku = cariBuku(isbn);
if (buku == nullptr) {
return std::unexpected("buku dengan ISBN " + isbn + " tidak ditemukan");
}
const Anggota* anggota = cariAnggota(idAnggota);
if (anggota == nullptr) {
return std::unexpected("anggota dengan id " + std::to_string(idAnggota)
+ " tidak terdaftar");
}
if (temukanPeminjamanAktif(isbn, idAnggota) != nullptr) {
return std::unexpected(anggota->nama + " masih meminjam buku yang sama");
}
if (hitungPinjamanAktif(idAnggota) >= BATAS_PINJAM) {
return std::unexpected(anggota->nama + " sudah mencapai batas "
+ std::to_string(BATAS_PINJAM) + " buku");
}
if (buku->stok <= 0) {
return std::unexpected("stok \"" + buku->judul + "\" habis");
}
// Semua aturan lolos — baru state diubah.
Buku* bukuMut = temukanBuku(isbn);
--bukuMut->stok;
daftarPeminjaman_.push_back(
Peminjaman{isbn, idAnggota, StatusPeminjaman::Dipinjam});
return {};
}
std::expected<void, std::string> kembalikan(const std::string& isbn, int idAnggota) {
Peminjaman* catatan = temukanPeminjamanAktif(isbn, idAnggota);
if (catatan == nullptr) {
return std::unexpected("tidak ada peminjaman aktif untuk ISBN " + isbn
+ " oleh anggota id " + std::to_string(idAnggota));
}
catatan->status = StatusPeminjaman::Kembali;
if (Buku* buku = temukanBuku(isbn)) {
++buku->stok;
}
return {};
}
int hitungPinjamanAktif(int idAnggota) const {
int jumlah = 0;
for (const Peminjaman& p : daftarPeminjaman_) {
if (p.idAnggota == idAnggota && p.status == StatusPeminjaman::Dipinjam) {
++jumlah;
}
}
return jumlah;
}
std::vector<const Peminjaman*> peminjamanAktif() const {
std::vector<const Peminjaman*> hasil;
for (const Peminjaman& p : daftarPeminjaman_) {
if (p.status == StatusPeminjaman::Dipinjam) {
hasil.push_back(&p);
}
}
return hasil;
}
// ---------- persistensi ----------
bool simpanKe(const std::string& path) const {
std::ofstream keluar{path};
if (!keluar) {
return false;
}
for (const Buku& b : daftarBuku_) {
keluar << "BUKU|" << b.isbn << '|' << b.judul << '|' << b.penulis
<< '|' << b.stok << '\n';
}
for (const Anggota& a : daftarAnggota_) {
keluar << "ANGGOTA|" << a.id << '|' << a.nama << '\n';
}
for (const Peminjaman& p : daftarPeminjaman_) {
keluar << "PEMINJAMAN|" << p.isbn << '|' << p.idAnggota << '|'
<< statusKeTeks(p.status) << '\n';
}
return static_cast<bool>(keluar);
}
bool muatDari(const std::string& path) {
std::ifstream masuk{path};
if (!masuk) {
return false;
}
// Muat ke objek sementara; data lama tetap utuh bila berkas rusak.
Perpustakaan hasil;
std::string baris;
while (std::getline(masuk, baris)) {
if (baris.empty()) {
continue;
}
const std::vector<std::string> kolom = pisah(baris, '|');
if (kolom[0] == "BUKU" && kolom.size() == 5) {
const auto stok = keInt(kolom[4]);
if (!stok || *stok < 0
|| !hasil.tambahBuku(Buku{kolom[1], kolom[2], kolom[3], *stok})) {
return false;
}
} else if (kolom[0] == "ANGGOTA" && kolom.size() == 3) {
const auto id = keInt(kolom[1]);
if (!id || *id <= 0 || !hasil.daftarAnggota(Anggota{*id, kolom[2]})) {
return false;
}
} else if (kolom[0] == "PEMINJAMAN" && kolom.size() == 4) {
const auto id = keInt(kolom[2]);
if (!id) {
return false;
}
const StatusPeminjaman status = kolom[3] == "Dipinjam"
? StatusPeminjaman::Dipinjam
: StatusPeminjaman::Kembali;
hasil.daftarPeminjaman_.push_back(Peminjaman{kolom[1], *id, status});
} else {
return false; // format tidak dikenal — gagal dan jangan ubah state
}
}
*this = std::move(hasil);
return true;
}
// ---------- tampilan ----------
void tampilkanKoleksi() const {
std::cout << std::left << std::setw(18) << "ISBN" << ' '
<< std::setw(30) << "Judul" << ' ' << std::setw(15) << "Penulis"
<< ' ' << std::right << std::setw(4) << "Stok" << '\n';
std::cout << std::string(70, '-') << '\n';
for (const Buku& b : daftarBuku_) {
std::cout << std::left << std::setw(18) << b.isbn << ' '
<< std::setw(30) << b.judul << ' ' << std::setw(15)
<< b.penulis << ' ' << std::right << std::setw(4) << b.stok
<< '\n';
}
}
void tampilkanPeminjamanAktif() const {
const std::vector<const Peminjaman*> aktif = peminjamanAktif();
std::cout << "Peminjaman aktif: " << aktif.size() << '\n';
for (const Peminjaman* p : aktif) {
const Buku* buku = cariBuku(p->isbn);
const Anggota* anggota = cariAnggota(p->idAnggota);
std::cout << " \"" << (buku ? buku->judul : p->isbn)
<< "\" dipinjam oleh " << (anggota ? anggota->nama : "(tidak dikenal)")
<< '\n';
}
}
private:
Buku* temukanBuku(const std::string& isbn) {
for (Buku& b : daftarBuku_) {
if (b.isbn == isbn) return &b;
}
return nullptr;
}
const Buku* temukanBuku(const std::string& isbn) const {
for (const Buku& b : daftarBuku_) {
if (b.isbn == isbn) return &b;
}
return nullptr;
}
Anggota* temukanAnggota(int id) {
for (Anggota& a : daftarAnggota_) {
if (a.id == id) return &a;
}
return nullptr;
}
const Anggota* temukanAnggota(int id) const {
for (const Anggota& a : daftarAnggota_) {
if (a.id == id) return &a;
}
return nullptr;
}
Peminjaman* temukanPeminjamanAktif(const std::string& isbn) {
for (Peminjaman& p : daftarPeminjaman_) {
if (p.isbn == isbn && p.status == StatusPeminjaman::Dipinjam) return &p;
}
return nullptr;
}
const Peminjaman* temukanPeminjamanAktif(const std::string& isbn) const {
for (const Peminjaman& p : daftarPeminjaman_) {
if (p.isbn == isbn && p.status == StatusPeminjaman::Dipinjam) return &p;
}
return nullptr;
}
Peminjaman* temukanPeminjamanAktif(const std::string& isbn, int idAnggota) {
for (Peminjaman& p : daftarPeminjaman_) {
if (p.isbn == isbn && p.idAnggota == idAnggota
&& p.status == StatusPeminjaman::Dipinjam) {
return &p;
}
}
return nullptr;
}
const Peminjaman* temukanPeminjamanAktif(const std::string& isbn,
int idAnggota) const {
for (const Peminjaman& p : daftarPeminjaman_) {
if (p.isbn == isbn && p.idAnggota == idAnggota
&& p.status == StatusPeminjaman::Dipinjam) {
return &p;
}
}
return nullptr;
}
template <typename Pengambil>
std::vector<Buku> saring(const std::string& kunci, Pengambil ambil) const {
const std::string kunciKecil = kecilkan(kunci);
std::vector<Buku> hasil;
for (const Buku& b : daftarBuku_) {
if (kecilkan(ambil(b)).find(kunciKecil) != std::string::npos) {
hasil.push_back(b);
}
}
return hasil;
}
std::vector<Buku> daftarBuku_;
std::vector<Anggota> daftarAnggota_;
std::vector<Peminjaman> daftarPeminjaman_;
};
// ---------- data uji ----------
// Perpustakaan kecil: satu buku berstok satu dan dua anggota.
Perpustakaan perpustakaanKecil() {
Perpustakaan p;
p.tambahBuku({"978-1", "C++ Modern", "Budi Santoso", 1});
p.daftarAnggota({1, "Budi"});
p.daftarAnggota({2, "Siti"});
return p;
}
// ---------- test case, gaya Arrange-Act-Assert ----------
void ujiTambahDanCari() {
MULAI_UJI("tambah & cari buku");
// Arrange: perpustakaan kosong.
Perpustakaan p;
// Act: tambah dua buku, lalu coba tambahkan ISBN yang sama.
bool buku1 = p.tambahBuku({"978-1", "C++ Modern", "Budi Santoso", 3});
bool buku2 = p.tambahBuku({"978-2", "Algoritma", "Siti Rahmawati", 2});
bool duplikat = p.tambahBuku({"978-1", "Versi Lain", "Dewi Lestari", 1});
// Assert
CHECK_TRUE(buku1, "buku baru diterima");
CHECK_TRUE(buku2, "buku kedua diterima");
CHECK_TRUE(!duplikat, "ISBN duplikat ditolak");
CHECK_EQ(p.cariBuku("978-1")->judul, std::string("C++ Modern"));
CHECK_TRUE(p.cariBuku("978-9") == nullptr, "ISBN tak dikenal tidak ditemukan");
}
void ujiCariByJudul() {
MULAI_UJI("cariByJudul tidak peka huruf besar-kecil");
Perpustakaan p;
p.tambahBuku({"978-1", "Pemrograman C++ Modern", "Budi Santoso", 3});
p.tambahBuku({"978-2", "Algoritma dan Struktur Data", "Siti Rahmawati", 2});
CHECK_TRUE(p.cariByJudul("c++").size() == 1, "kata 'c++' menemukan satu buku");
CHECK_TRUE(p.cariByJudul("STRUKTUR").size() == 1, "huruf kapital tetap cocok");
CHECK_TRUE(p.cariByJudul("python").empty(), "kata asing tidak menemukan apa pun");
}
void ujiPinjamSukses() {
MULAI_UJI("pinjam sukses menurunkan stok");
// Arrange
Perpustakaan p = perpustakaanKecil();
// Act
auto hasil = p.pinjam("978-1", 1);
// Assert
CHECK_TRUE(hasil.has_value(), "pinjam pertama sukses");
CHECK_EQ(p.cariBuku("978-1")->stok, 0);
CHECK_EQ(p.hitungPinjamanAktif(1), 1);
}
void ujiPinjamStokHabis() {
MULAI_UJI("pinjam ditolak saat stok habis");
Perpustakaan p = perpustakaanKecil();
CHECK_TRUE(p.pinjam("978-1", 1).has_value(), "pinjam pertama sukses");
auto hasil = p.pinjam("978-1", 2);
CHECK_TRUE(!hasil.has_value(), "pinjam kedua ditolak");
CHECK_TRUE(hasil.error().find("habis") != std::string::npos,
"pesan menyebut stok habis");
CHECK_EQ(p.cariBuku("978-1")->stok, 0);
}
void ujiDobelPinjam() {
MULAI_UJI("anggota tidak bisa meminjam dua kali buku yang sama");
// Arrange: stok tiga supaya pemeriksaan batas/stok tidak yang bicara dulu.
Perpustakaan p;
p.tambahBuku({"978-1", "C++ Modern", "Budi Santoso", 3});
p.daftarAnggota({1, "Budi"});
CHECK_TRUE(p.pinjam("978-1", 1).has_value(), "pinjam pertama sukses");
// Act: anggota yang sama meminjam ISBN yang sama lagi.
auto hasil = p.pinjam("978-1", 1);
// Assert
CHECK_TRUE(!hasil.has_value(), "pinjam ulang oleh anggota sama ditolak");
CHECK_TRUE(hasil.error().find("masih meminjam") != std::string::npos,
"pesan menyebut pinjaman berjalan");
CHECK_EQ(p.hitungPinjamanAktif(1), 1);
}
void ujiBatasPinjam() {
MULAI_UJI("batas maksimal tiga buku per anggota");
Perpustakaan p;
p.tambahBuku({"978-1", "A", "Budi Santoso", 1});
p.tambahBuku({"978-2", "B", "Budi Santoso", 1});
p.tambahBuku({"978-3", "C", "Budi Santoso", 1});
p.tambahBuku({"978-4", "D", "Budi Santoso", 1});
p.daftarAnggota({1, "Budi"});
CHECK_TRUE(p.pinjam("978-1", 1).has_value(), "pinjam pertama sukses");
CHECK_TRUE(p.pinjam("978-2", 1).has_value(), "pinjam kedua sukses");
CHECK_TRUE(p.pinjam("978-3", 1).has_value(), "pinjam ketiga sukses");
auto hasil = p.pinjam("978-4", 1);
CHECK_TRUE(!hasil.has_value(), "pinjam keempat ditolak");
CHECK_TRUE(hasil.error().find("batas") != std::string::npos,
"pesan menyebut batas");
}
void ujiKembalikan() {
MULAI_UJI("kembalikan memulihkan stok");
Perpustakaan p = perpustakaanKecil();
CHECK_TRUE(p.pinjam("978-1", 2).has_value(), "Siti meminjam sukses");
CHECK_EQ(p.cariBuku("978-1")->stok, 0);
auto hasil = p.kembalikan("978-1", 2);
CHECK_TRUE(hasil.has_value(), "pengembalian sukses");
CHECK_EQ(p.cariBuku("978-1")->stok, 1);
CHECK_EQ(p.hitungPinjamanAktif(2), 0);
CHECK_TRUE(!p.kembalikan("978-1", 2).has_value(),
"mengembalikan dua kali ditolak");
}
void ujiHapusBuku() {
MULAI_UJI("hapus buku dari koleksi");
Perpustakaan p;
p.tambahBuku({"978-1", "A", "Budi Santoso", 1});
p.tambahBuku({"978-2", "B", "Siti Rahmawati", 1});
p.daftarAnggota({1, "Budi"});
CHECK_TRUE(p.hapusBuku("978-1"), "buku tanpa pinjaman bisa dihapus");
CHECK_TRUE(p.cariBuku("978-1") == nullptr, "buku benar-benar lepas dari koleksi");
CHECK_TRUE(p.pinjam("978-2", 1).has_value(), "pinjam buku kedua sukses");
CHECK_TRUE(!p.hapusBuku("978-2"), "buku yang sedang dipinjam tidak bisa dihapus");
}
void ujiRoundTrip() {
MULAI_UJI("simpan & muat (round-trip)");
// Arrange: dua buku, dua anggota, satu peminjaman aktif.
Perpustakaan p = perpustakaanKecil();
p.tambahBuku({"978-2", "Algoritma", "Siti Rahmawati", 2});
CHECK_TRUE(p.pinjam("978-2", 1).has_value(), "satu peminjaman aktif");
// Act: simpan lalu muat ke objek yang berbeda.
const std::string path = "uji-perpustakaan-data.txt";
CHECK_TRUE(p.simpanKe(path), "simpan ke berkas");
Perpustakaan q;
CHECK_TRUE(q.muatDari(path), "muat dari berkas");
// Assert: state kedua objek sama.
CHECK_TRUE(q.cariBuku("978-1") != nullptr, "buku pertama tersalin");
CHECK_EQ(q.cariBuku("978-1")->stok, 1);
CHECK_EQ(q.cariBuku("978-2")->stok, 1);
CHECK_EQ(q.hitungPinjamanAktif(1), 1);
CHECK_TRUE(q.cariAnggota(1) != nullptr && q.cariAnggota(1)->nama == "Budi",
"anggota tersalin");
std::remove(path.c_str());
}
int main() {
ujiTambahDanCari();
ujiCariByJudul();
ujiPinjamSukses();
ujiPinjamStokHabis();
ujiDobelPinjam();
ujiBatasPinjam();
ujiKembalikan();
ujiHapusBuku();
ujiRoundTrip();
std::cout << '\n' << "ringkasan: " << jumlahLulus << " lulus, " << jumlahGagal
<< " gagal\n";
return jumlahGagal == 0 ? 0 : 1;
}Output yang Dihasilkan:
[ LULUS ] tambah & cari buku - buku baru diterima
[ LULUS ] tambah & cari buku - buku kedua diterima
[ LULUS ] tambah & cari buku - ISBN duplikat ditolak
[ LULUS ] tambah & cari buku - p.cariBuku("978-1")->judul
[ LULUS ] tambah & cari buku - ISBN tak dikenal tidak ditemukan
[ LULUS ] cariByJudul tidak peka huruf besar-kecil - kata 'c++' menemukan satu buku
[ LULUS ] cariByJudul tidak peka huruf besar-kecil - huruf kapital tetap cocok
[ LULUS ] cariByJudul tidak peka huruf besar-kecil - kata asing tidak menemukan apa pun
[ LULUS ] pinjam sukses menurunkan stok - pinjam pertama sukses
[ LULUS ] pinjam sukses menurunkan stok - p.cariBuku("978-1")->stok
[ LULUS ] pinjam sukses menurunkan stok - p.hitungPinjamanAktif(1)
[ LULUS ] pinjam ditolak saat stok habis - pinjam pertama sukses
[ LULUS ] pinjam ditolak saat stok habis - pinjam kedua ditolak
[ LULUS ] pinjam ditolak saat stok habis - pesan menyebut stok habis
[ LULUS ] pinjam ditolak saat stok habis - p.cariBuku("978-1")->stok
[ LULUS ] anggota tidak bisa meminjam dua kali buku yang sama - pinjam pertama sukses
[ LULUS ] anggota tidak bisa meminjam dua kali buku yang sama - pinjam ulang oleh anggota sama ditolak
[ LULUS ] anggota tidak bisa meminjam dua kali buku yang sama - pesan menyebut pinjaman berjalan
[ LULUS ] anggota tidak bisa meminjam dua kali buku yang sama - p.hitungPinjamanAktif(1)
[ LULUS ] batas maksimal tiga buku per anggota - pinjam pertama sukses
[ LULUS ] batas maksimal tiga buku per anggota - pinjam kedua sukses
[ LULUS ] batas maksimal tiga buku per anggota - pinjam ketiga sukses
[ LULUS ] batas maksimal tiga buku per anggota - pinjam keempat ditolak
[ LULUS ] batas maksimal tiga buku per anggota - pesan menyebut batas
[ LULUS ] kembalikan memulihkan stok - Siti meminjam sukses
[ LULUS ] kembalikan memulihkan stok - p.cariBuku("978-1")->stok
[ LULUS ] kembalikan memulihkan stok - pengembalian sukses
[ LULUS ] kembalikan memulihkan stok - p.cariBuku("978-1")->stok
[ LULUS ] kembalikan memulihkan stok - p.hitungPinjamanAktif(2)
[ LULUS ] kembalikan memulihkan stok - mengembalikan dua kali ditolak
[ LULUS ] hapus buku dari koleksi - buku tanpa pinjaman bisa dihapus
[ LULUS ] hapus buku dari koleksi - buku benar-benar lepas dari koleksi
[ LULUS ] hapus buku dari koleksi - pinjam buku kedua sukses
[ LULUS ] hapus buku dari koleksi - buku yang sedang dipinjam tidak bisa dihapus
[ LULUS ] simpan & muat (round-trip) - satu peminjaman aktif
[ LULUS ] simpan & muat (round-trip) - simpan ke berkas
[ LULUS ] simpan & muat (round-trip) - muat dari berkas
[ LULUS ] simpan & muat (round-trip) - buku pertama tersalin
[ LULUS ] simpan & muat (round-trip) - q.cariBuku("978-1")->stok
[ LULUS ] simpan & muat (round-trip) - q.cariBuku("978-2")->stok
[ LULUS ] simpan & muat (round-trip) - q.hitungPinjamanAktif(1)
[ LULUS ] simpan & muat (round-trip) - anggota tersalin
ringkasan: 42 lulus, 0 gagalSembilan kelompok uji, 42 pemeriksaan, semuanya [ LULUS ], dan program keluar dengan exit code 0. Yang terkunci bukan sekadar “banyak check hijau”: ISBN duplikat ditolak sementara pencarian tetap bekerja; cariByJudul("c++") menemukan tepat satu buku dan cariByJudul("python") tidak menemukan apa pun; pinjam sukses menurunkan stok ke angka yang pasti; stok habis, dobel-pinjam, dan batas tiga buku masing-masing ditolak dengan pesan yang memuat kata kuncinya; pengembalian memulihkan stok dan mengembalikan dua kali ditolak; buku yang sedang dipinjam tidak bisa dihapus; dan round-trip simpan-muat membawa stok maupun peminjaman aktif utuh ke objek baru. Perhatikan pula pola Arrange-Act-Assert yang berulang: setiap uji menyusun state kecil sendiri, memanggil satu operasi, lalu memeriksa akibatnya — kegagalan satu kelompok langsung menunjuk area kode yang bermasalah.
Proyek Tuntas dan Perjalanan ke Depan #
Dengan 42 pemeriksaan hijau, proyek perpustakaan tuntas: model data dan relasi dari Bagian 1, pengelolaan koleksi dan pencarian dari Bagian 2, alur peminjaman dan pengembalian dari Bagian 3, persistensi fail-closed dari Bagian 4, dan kini bukti otomatis dari bagian ini — semuanya dalam satu berkas yang dikompilasi dengan satu perintah. Ketika suatu saat Anda menambah fitur (denda keterlambatan, misalnya, atau reservasi buku), jalankan kembali skenario terekam dan harness ini: perubahan yang merusak perilaku lama akan tertangkap sebelum sempat merusak pengguna. Proyek berikutnya memindahkan fokus dari relasi antarobjek ke data berukuran besar — membaca berkas CSV ribuan baris, mengagregasi kolomnya, dan menghasilkan laporan — di mana efisiensi penguraian dan struktur agregasi menjadi bintang utamanya.
Rujukan resmi: std::expected (cppreference) , std::optional (cppreference) .