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_clockmembuat 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()ataustd::random_devicetanpa seed terisolasi menyebabkan cabang logika berbeda saat pengujian dijalankan berulang atau paralel. - Floating-Point Drift: Optimasi compiler seperti Fused Multiply-Add (FMA) via
-ffast-mathmemungkinkan 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:
- Fixed Timestep Invarian: Nilai
dtdikunci pada konstanta (misalnya 1/60 detik atau skalar fixed-point) yang diteruskan secara eksplisit ke dalam fungsi update. - State Isolation: Seluruh state simulasi dibungkus dalam struct atau memory block yang contiguous (POD / standard layout) tanpa pointer raw ke memory heap eksternal.
- 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-mathtidak digunakan. Tambahkan-fno-fast-mathdan-ffp-contract=offpada 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-goldenuntuk 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.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!