Server yang berjalan belum tentu benar — pelajaran yang berulang di proyek ini. Bug race di bagian 2 baru tampak ketika skenario dijalankan berulang, dan itulah alasan bagian penutup ini bukan sekadar demo: kita menyusun harness pengujian yang menjalankan dua kelompok uji setiap kali program dieksekusi. Kelompok pertama adalah unit test parser protokol (lima kasus bentuk baris). Kelompok kedua adalah uji end-to-end: server in-process dengan dua client sungguhan yang saling berkirim, memverifikasi setiap siaran sampai, urutannya benar, dan pengirim tidak menerima pesannya sendiri.

Harness meniru pola GoogleTest yang kita pelajari di Section Debugging dan Code Quality dalam bentuk paling ringkas: makro cek(syarat, nama) mencatat LULUS/GAGAL, lalu main mencetak ringkasan dan mengembalikan exit code yang mencerminkan hasilnya — 0 bila seluruhnya lulus. Bentuk sederhana ini cukup untuk dijalankan otomatis, baik oleh ctest maupun CI.

Harness Pengujian #

uji-chat-server.cpp berdiri sendiri: salinan ringkas fungsi protokol dan utilitas socket disertakan di dalamnya, sehingga file uji bisa dikompilasi tanpa menyertakan src/chat-server.cpp — pola umum pada proyek kecil yang belum memakai library pengujian eksternal:

cpp
// uji-chat-server.cpp — harness pengujian mandiri untuk chat server:
// (1) unit test parser protokol, (2) uji end-to-end broadcast dengan server
// in-process + dua client, (3) ringkasan LULUS/GAGAL dan exit code.
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <condition_variable>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <vector>

namespace {

int lulus = 0;
int gagal = 0;

void cek(bool syarat, const std::string& nama) {
    if (syarat) {
        ++lulus;
        std::cout << "  LULUS  " << nama << "\n";
    } else {
        ++gagal;
        std::cout << "  GAGAL  " << nama << "\n";
    }
}

// ---------- Unit parser (disalin dari src/protokol.cpp) ----------
struct PesanChat {
    std::string pengirim;
    std::string isi;
};

std::optional<PesanChat> parseBaris(const std::string& baris) {
    if (baris.empty() || baris[0] == '/') {
        return std::nullopt;
    }
    const std::size_t posisi = baris.find('|');
    if (posisi == std::string::npos) {
        return std::nullopt;
    }
    PesanChat pesan{baris.substr(0, posisi), baris.substr(posisi + 1)};
    if (pesan.pengirim.empty() || pesan.isi.empty()) {
        return std::nullopt;
    }
    return pesan;
}

// ---------- Utilitas socket (disalin dari src/chat-server.cpp) ----------
bool kirimSemua(int fd, const std::string& pesan) {
    std::size_t terkirim = 0;
    while (terkirim < pesan.size()) {
        const ssize_t n =
            ::send(fd, pesan.data() + terkirim, pesan.size() - terkirim, 0);
        if (n < 0) {
            return false;
        }
        terkirim += static_cast<std::size_t>(n);
    }
    return true;
}

bool terimaBaris(int fd, std::string& baris) {
    baris.clear();
    char c = 0;
    while (true) {
        const ssize_t n = ::recv(fd, &c, 1, 0);
        if (n <= 0) {
            return false;
        }
        if (c == '\n') {
            return true;
        }
        baris.push_back(c);
    }
}

// Server mini untuk pengujian: accept 2 koneksi, satu thread per client,
// broadcast pesan ke client lain, menutup koneksi saat menerima "/quit".
struct ServerUji {
    int fdListen = -1;
    uint16_t port = 0;
    std::vector<std::string> siaran;
    std::mutex kunci;

    bool mulai() {
        fdListen = ::socket(AF_INET, SOCK_STREAM, 0);
        sockaddr_in alamat{};
        alamat.sin_family = AF_INET;
        alamat.sin_port = htons(0);
        alamat.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
        if (::bind(fdListen, reinterpret_cast<const sockaddr*>(&alamat),
                   sizeof(alamat)) < 0 ||
            ::listen(fdListen, 8) < 0) {
            return false;
        }
        sockaddr_in terikat{};
        socklen_t panjang = sizeof(terikat);
        ::getsockname(fdListen, reinterpret_cast<sockaddr*>(&terikat),
                      &panjang);
        port = terikat.sin_port;
        return true;
    }

    void layani(int fd) {
        std::string baris;
        while (terimaBaris(fd, baris)) {
            if (baris == "/quit") {
                break;
            }
            const std::optional<PesanChat> pesan = parseBaris(baris);
            if (!pesan.has_value()) {
                continue;
            }
            std::lock_guard<std::mutex> gembok(kunci);
            for (int lain : fdAktif) {
                if (lain != fd) {
                    kirimSemua(lain, pesan->pengirim + "|" + pesan->isi + "\n");
                }
            }
            siaran.push_back(baris);
        }
        ::close(fd);
    }

    std::vector<int> fdAktif;

    void jalankan(std::vector<std::thread>& daftar) {
        // Accept DUA client lebih dulu, baru thread layani dijalankan —
        // daftar koneksi lengkap sehingga tidak ada siaran yang terlewat.
        std::vector<int> fds;
        for (int i = 0; i < 2; ++i) {
            const int fd = ::accept(fdListen, nullptr, nullptr);
            if (fd < 0) {
                break;
            }
            fds.push_back(fd);
            std::lock_guard<std::mutex> gembok(kunci);
            fdAktif.push_back(fd);
        }
        for (int fd : fds) {
            daftar.emplace_back([this, fd] { layani(fd); });
        }
    }
};

// Rendezvous dua client: keduanya menunggu sampai keduanya terhubung
// sebelum ada yang mulai mengirim — siaran tidak ada yang terlewat.
struct Rendezvous {
    std::mutex kunci;
    std::condition_variable sinyal;
    int terhubung = 0;

    void datang() {
        {
            std::lock_guard<std::mutex> gembok(kunci);
            ++terhubung;
        }
        sinyal.notify_all();   // bangunkan pasangan yang mungkin menunggu
        std::unique_lock<std::mutex> gembok(kunci);
        // Thread kedua langsung lolos (predikat sudah true); thread pertama
        // menunggu hingga dibangunkan. Predikat menutup celah notifikasi.
        sinyal.wait(gembok, [&] { return terhubung == 2; });
    }
};

// Client uji: kirim pesan, terima siaran sampai target, lalu /quit.
void jalankanClient(uint16_t port, const std::string& nama,
                    const std::vector<std::string>& keluar, int targetTerima,
                    std::vector<std::string>& hasil, Rendezvous& rendezvous) {
    const int fd = ::socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in tujuan{};
    tujuan.sin_family = AF_INET;
    tujuan.sin_port = port;
    tujuan.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    if (::connect(fd, reinterpret_cast<const sockaddr*>(&tujuan),
                  sizeof(tujuan)) < 0) {
        return;
    }
    rendezvous.datang();   // mulai berkirim hanya setelah keduanya siap
    for (const std::string& teks : keluar) {
        kirimSemua(fd, nama + "|" + teks + "\n");
    }
    std::string baris;
    while (static_cast<int>(hasil.size()) < targetTerima &&
           terimaBaris(fd, baris)) {
        hasil.push_back(baris);
    }
    kirimSemua(fd, "/quit\n");
}

}  // namespace

int main() {
    std::cout << "== Uji parser protokol\n";
    const auto a = parseBaris("Andi|Halo semua");
    cek(a.has_value() && a->pengirim == "Andi" && a->isi == "Halo semua",
        "parseBaris(\"Andi|Halo semua\") -> pengirim+isi benar");
    cek(!parseBaris("/quit").has_value(), "perintah \"/quit\" ditolak parser");
    cek(!parseBaris("tanpa-pemisah").has_value(),
        "baris tanpa pemisah ditolak");
    cek(!parseBaris("|isi kosong").has_value(), "nama kosong ditolak");
    cek(!parseBaris("").has_value(), "baris kosong ditolak");

    std::cout << "== Uji end-to-end broadcast\n";
    ServerUji server;
    cek(server.mulai(), "server ter-bind di loopback port ephemeral");

    std::vector<std::thread> threadServer;
    // Loop accept berjalan di thread terpisah — main tetap bebas memulai
    // client (accept yang menunggu tidak boleh memblokir thread utama).
    std::thread threadPenerima([&server, &threadServer] {
        server.jalankan(threadServer);
    });

    std::vector<std::string> terimaAndi;
    std::vector<std::string> terimaBela;
    Rendezvous rendezvous;
    std::thread clientAndi(jalankanClient, server.port, "Andi",
                           std::vector<std::string>{"Halo semua", "Siap"},
                           1, std::ref(terimaAndi), std::ref(rendezvous));
    std::thread clientBela(jalankanClient, server.port, "Bela",
                           std::vector<std::string>{"Halo Andi"},
                           2, std::ref(terimaBela), std::ref(rendezvous));
    clientAndi.join();
    clientBela.join();
    threadPenerima.join();
    for (std::thread& t : threadServer) {
        t.join();
    }
    ::close(server.fdListen);

    cek(terimaAndi.size() == 1 && terimaAndi[0] == "Bela|Halo Andi",
        "Andi menerima 1 siaran dari Bela");
    cek(terimaBela.size() == 2 &&
                terimaBela[0] == "Andi|Halo semua" &&
                terimaBela[1] == "Andi|Siap",
        "Bela menerima 2 siaran dari Andi sesuai urutan");
    cek(server.siaran.size() == 3, "server mencatat 3 pesan ter-siaran");
    cek(terimaAndi[0].find("Andi|") != 0,
        "pengirim tidak menerima pesannya sendiri");

    std::cout << "== Ringkasan: " << lulus << " lulus, " << gagal
              << " gagal\n";
    return (gagal == 0) ? 0 : 1;
}

Output yang Dihasilkan:

text
== Uji parser protokol
  LULUS  parseBaris("Andi|Halo semua") -> pengirim+isi benar
  LULUS  perintah "/quit" ditolak parser
  LULUS  baris tanpa pemisah ditolak
  LULUS  nama kosong ditolak
  LULUS  baris kosong ditolak
== Uji end-to-end broadcast
  LULUS  server ter-bind di loopback port ephemeral
  LULUS  Andi menerima 1 siaran dari Bela
  LULUS  Bela menerima 2 siaran dari Andi sesuai urutan
  LULUS  server mencatat 3 pesan ter-siaran
  LULUS  pengirim tidak menerima pesannya sendiri
== Ringkasan: 10 lulus, 0 gagal

Bedah Harness #

Mari kita telaah keputusan penting di dalamnya:

  1. cek(syarat, nama): satu pintu untuk semua asersi — jumlah lulus dan gagal bertambah di satu tempat, sehingga ringkasan akhir tidak mungkin melenceng.
  2. ServerUji: versi ringkas server bagian 2, dengan tambahan pencatatan siaran di sisi server. Loop accept berjalan di thread tersendiri (pola yang sama dengan threadPenerima), sehingga thread utama tetap bebas memulai client.
  3. Rendezvous: sama persis dengan tungguBersama di bagian 2 — dua client menunggu keduanya terhubung sebelum berkirim. Di dalam harness, kebutuhan ini lebih tegas lagi karena kedua client dijalankan nyaris bersamaan.
  4. Pengujian urutan: terimaBela[0] dan terimaBela[1] diperiksa berurutan — TCP menjamin urutan antar-byte dalam satu koneksi, sehingga dua pesan dari Andi harus diterima Bela persis dalam urutan pengirimannya.
  5. terimaAndi[0].find("Andi|") != 0: pengirim tidak menerima pesannya sendiri. Uji yang sering terlupa padahal mudah ditulis, dan justru memverifikasi perilaku khas siarkan.
  6. Exit code: main mengembalikan 1 bila ada kegagalan — program uji yang bermakna harus bisa menjadi penghalang otomatis di pipeline, bukan sekadar cetakan yang dibaca manusia.

Proyek ini menutup rangkaian integrasi dua section besar: jaringan memberi saluran, concurrency memberi ketahanan, dan pengujian memberi bukti. Keterampilan yang sama — desain deterministik, sinkronisasi yang eksplisit, dan harness yang bisa dijalankan otomatis — langsung berlaku saat kita bertemu proyek server yang lebih besar di luar course ini.

Rujukan resmi: cppreference — std::thread , GoogleTest Primer (pola yang ditiru) .