Pengujian unit dan integrasi pada engine simulasi berbasis waktu sering kali memicu flaky test. Masalah ini umumnya berakar dari divergensi state antar-frame (desync) yang disebabkan oleh pembacaan clock sistem secara langsung, generator angka acak tanpa seed deterministik, atau variasi kalkulasi floating-point. Artikel ini membahas cara membangun test harness deterministik menggunakan C++ murni untuk memvalidasi state engine secara konsisten di tingkat lokal maupun lingkungan CI.

Akar Masalah Desinkronisasi State

Divergensi logika engine simulasi biasanya dipicu oleh tiga faktor teknis:

  • Delta Time (dt) Berbasis Wall-Clock: Mengambil delta waktu langsung dari std::chrono::high_resolution_clock membuat simulasi bergantung pada latensi CPU host. Variasi sekecil mikrodetik mengubah nilai integrasi numerik (misalnya Euler atau Verlet) antar-eksekusi.
  • Unseeded PRNG dan Static State: Penggunaan rand() atau std::random_device tanpa seed terisolasi menyebabkan cabang logika berbeda saat pengujian dijalankan berulang atau paralel.
  • Floating-Point Drift: Optimasi compiler seperti Fused Multiply-Add (FMA) via -ffast-math memungkinkan urutan operasi aritmatika floating-point berubah bentuk, menghasilkan bit mantissa yang berbeda antar arsitektur x86_64 dan ARM64.

Arsitektur Deterministic Simulation Loop

Untuk mengeliminasi non-determinisme, pisahkan loop simulasi dari clock perangkat keras dan sistem input I/O tak-terkendali. Engine harus diperlakukan sebagai mesin state murni:

State_{n+1} = Step(State_n, Input_n, Fixed_dt)

Terapkan tiga aturan arsitektural berikut:

  1. Fixed Timestep Invarian: Nilai dt dikunci pada konstanta (misalnya 1/60 detik atau skalar fixed-point) yang diteruskan secara eksplisit ke dalam fungsi update.
  2. State Isolation: Seluruh state simulasi dibungkus dalam struct atau memory block yang contiguous (POD / standard layout) tanpa pointer raw ke memory heap eksternal.
  3. Snapshot Hashing: Nilai bit seluruh memory state di-hash pada akhir setiap tick. Nilai hash ini dibandingkan dengan golden hash yang tercatat sebelumnya.

Implementasi Verification Harness di C++

Berikut implementasi minimal harness simulasi deterministik menggunakan C++17 tanpa dependensi library eksternal. Kode ini menggunakan representasi fixed-point sederhana untuk posisi dan mengimplementasikan algoritma hashing FNV-1a 64-bit untuk mendeteksi divergensi state.

#include <iostream>
#include <cstdint>
#include <cassert>
#include <array>

#pragma pack(push, 1)
struct EntityState {
    int32_t x;
    int32_t y;
    int32_t vx;
    int32_t vy;
};

struct WorldState {
    uint32_t tick;
    EntityState actor;
};
#pragma pack(pop)

// 64-bit FNV-1a Hash
uint64_t hash_state(const WorldState& state) {
    const auto* buffer = reinterpret_cast<const uint8_t*>(&state);
    uint64_t hash = 14695981039346656037ULL;
    for (size_t i = 0; i < sizeof(WorldState); ++i) {
        hash ^= buffer[i];
        hash *= 1099511628211ULL;
    }
    return hash;
}

class SimulationHarness {
public:
    WorldState state{};

    void reset() {
        state = WorldState{0, {0, 0, 10, 5}};
    }

    void step(int32_t input_ax, int32_t input_ay) {
        state.tick++;
        // Integrasi discrete fixed-step
        state.actor.vx += input_ax;
        state.actor.vy += input_ay;
        state.actor.x += state.actor.vx;
        state.actor.y += state.actor.vy;
    }
};

int main() {
    SimulationHarness sim;
    sim.reset();

    // Pre-computed Golden Hash untuk 5 tick pertama dengan input deterministik
    const std::array<uint64_t, 5> golden_hashes = {
        0xbf302d9fb2215c0eULL,
        0x78ab0e7a5d3f2719ULL,
        0xdf4a938c233e76a6ULL,
        0x10344b58c7075253ULL,
        0x4516ff72f3e8f8ecULL
    };

    const std::array<std::pair<int32_t, int32_t>, 5> mock_inputs = {{
        {1, 0}, {0, -1}, {2, 1}, {-1, 0}, {0, 0}
    }};

    for (size_t i = 0; i < golden_hashes.size(); ++i) {
        sim.step(mock_inputs[i].first, mock_inputs[i].second);
        uint64_t current_hash = hash_state(sim.state);

        // Evaluasi desinkronisasi seketika
        if (current_hash != golden_hashes[i]) {
            std::cerr << "[DIVERGENCE DETECTED] Tick: " << sim.state.tick
                      << " Expected: " << std::hex << golden_hashes[i]
                      << " Got: " << current_hash << std::dec << std::endl;
            assert(false && "Simulation state diverged from golden hash.");
        }
    }

    std::cout << "All simulation steps strictly matched golden hashes.\n";
    return 0;
}

Integrasi Golden State Regression Testing di CI

Pengujian deterministik di pipeline Continuous Integration (CI) memerlukan kontrol ketat terhadap toolchain dan environment compiler:

  • Matikan Optimasi Aritmatika Tak-Aman: Pastikan flag seperti -ffast-math tidak digunakan. Tambahkan -fno-fast-math dan -ffp-contract=off pada GCC/Clang guna mencegah instruksi FMA otomatis yang merusak kesamaan bit floating-point antar-arsitektur runner.
  • Validasi Binary Layout: Gunakan static_assert(std::is_standard_layout_v<WorldState>) untuk memastikan tidak ada padding compiler acak yang mengontaminasi kalkulasi hash bitwise.
  • Pencatatan Golden Hash Artefak: Bila terjadi perubahan logika gameplay/simulasi yang disengaja, jalankan test runner lokal dengan argumen --record-golden untuk menimpa file fixture hash secara resmi via Pull Request review.

Batasan dan Trade-Offs

Pendekatan snapshot hashing memory mentah (raw memory hashing) mengharuskan struct data bebas dari raw pointer dan alokasi heap dinamis di dalam node yang di-hash (misalnya std::string atau std::vector). Untuk struktur data hierarkis atau dynamic graph, ganti raw memory traversal dengan deterministic serialization visitor (seperti FlatBuffers atau custom binary pack) sebelum hashing diproses.