Menguji interpreter retrocomputing atau lingkungan CLI multimedia menghadirkan tantangan determinisme. Perintah seperti PRINT, LOCATE, dan SOUND berinteraksi langsung dengan subsistem tampilan dan audio. Jika pengujian bergantung pada hardware nyata, frame rate monitor, atau sound card sistem operasi, test suite akan lambat, flappy, dan gagal dieksekusi di pipeline CI/CD headless. Solusinya adalah memisahkan runtime interpreter dari driver perangkat keras menggunakan antarmuka abstraksi buffer memori.
Arsitektur Decoupling I/O untuk Interpreter
Pola desain yang digunakan proyek seperti EndBASIC memisahkan logika evaluasi sintaksis dari peripheral. Interpreter tidak menulis langsung ke stdout atau driver ALSA/DirectSound. Sebaliknya, interpreter berinteraksi dengan dua abstraksi: Terminal (framebuffer berbasis karakter) dan AudioSink (penerima sample gelombang).
Dengan pendekatan ini, driver produksi merender teks via ANSI terminal atau OpenGL/SDL, sedangkan test runner menggunakan implementasi mock murni berbasis in-memory vector. Pendekatan ini menghilangkan dependensi sistem operasi luar dan mempercepat eksekusi uji hingga tingkat mikrodetik.
Snapshot Testing Framebuffer Teks
Pola pengujian output terminal konvensional sering kali mengevaluasi stream karakter mentah (raw escape sequences). Metode ini rentan rusak jika urutan escape sequence ANSI berubah meski output visualnya identik. Solusi yang lebih kuat adalah memverifikasi logical screen grid.
Framebuffer teks direpresentasikan sebagai matriks dua dimensi berukuran $W \times H$. Setiap sel menyimpan karakter (rune/char) dan atribut warna (foreground, background). Assertion dilakukan dengan merender grid tersebut ke representasi string dua dimensi murni:
// Definisi Text Framebuffer sederhana
pub struct MockTerminal {
pub width: usize,
pub height: usize,
pub cursor_x: usize,
pub cursor_y: usize,
pub buffer: Vec<char>,
}
impl MockTerminal {
pub fn new(width: usize, height: usize) -> Self {
Self {
width,
height,
cursor_x: 0,
cursor_y: 0,
buffer: vec![' '; width * height],
}
}
pub fn print(&mut self, s: &str) {
for ch in s.chars() {
if ch == '\n' {
self.cursor_x = 0;
self.cursor_y += 1;
} else if self.cursor_x < self.width && self.cursor_y < self.height {
let idx = self.cursor_y * self.width + self.cursor_x;
self.buffer[idx] = ch;
self.cursor_x += 1;
}
}
}
pub fn render_snapshot(&self) -> String {
self.buffer
.chunks(self.width)
.map(|row| row.iter().collect::<String>())
.collect::<Vec<String>>()
.join("\n")
}
}Uji Regresi Interpreter Retro pada Subsistem Audio
Perintah retrocomputing seperti SOUND freq, duration menghasilkan gelombang persegi atau sinus pada frekuensi tertentu. Menguji output audio tidak memerlukan analisis Fourier (FFT) yang berat jika generator gelombang bersifat deterministik. Driver audio pengujian cukup mengakumulasi sample Pulse-Code Modulation (PCM) mentah ke dalam ring buffer internal.
Implementasi Mock Audio Sink dan Assertions
Berikut implementasi backend audio sintetis dalam Rust yang memvalidasi frekuensi dan durasi tanpa memutar suara aktual:
pub trait AudioSink {
fn emit_square_wave(&mut self, freq_hz: f32, duration_ms: u32, sample_rate: u32);
}
pub struct MockAudioSink {
pub pcm_samples: Vec<i16>,
}
impl MockAudioSink {
pub fn new() -> Self {
Self { pcm_samples: Vec::new() }
}
}
impl AudioSink for MockAudioSink {
fn emit_square_wave(&mut self, freq_hz: f32, duration_ms: u32, sample_rate: u32) {
let total_samples = (sample_rate as f32 * (duration_ms as f32 / 1000.0)) as usize;
let period_samples = (sample_rate as f32 / freq_hz) as usize;
let amplitude = 8000i16;
for i in 0..total_samples {
let sample = if (i % period_samples) < (period_samples / 2) {
amplitude
} else {
-amplitude
};
self.pcm_samples.push(sample);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sound_instruction_deterministic_pcm() {
let mut audio = MockAudioSink::new();
// Emulasi: SOUND 440, 10 (440Hz selama 10ms pada sample rate 44.1kHz)
audio.emit_square_wave(440.0, 10, 44100);
assert_eq!(audio.pcm_samples.len(), 441);
assert_eq!(audio.pcm_samples[0], 8000);
assert_eq!(audio.pcm_samples[60], -8000); // Setengah periode (44100/440 = ~100 sampel)
}
}Eliminasi Timing Jitter pada CI Pipeline
Kesalahan umum saat menguji interpreter multimedia adalah menggunakan std::thread::sleep untuk mensimulasikan durasi suara atau delay visual. Pola ini menyebabkan flaky tests karena scheduler VM pada environment CI (seperti GitHub Actions atau GitLab Runner) memiliki latensi yang tidak stabil.
- Clock Virtual: Gantikan panggilan
Instant::now()dengan abstraksiVirtualClock. Interpreter menambah waktu virtual secara eksplisit saat mengeksekusi instruksi waktu. - Step-Driven Execution: Biarkan interpreter mengeksekusi siklus per instruksi virtual atau per tick tetap, bukan berbasis wall-clock time riil.
- Penyimpanan Snapshot: Gunakan hash SHA-256 untuk memverifikasi buffer PCM audio yang panjang guna menghemat alokasi memori pengujian.
Analisis Kinerja: Hardware vs Mock In-Memory
Pengujian interpreter berbasis in-memory buffer memangkas overhead inisialisasi driver grafis dan sound server. Sebagai perbandingan arsitektural:
- Audio Nyata (ALSA/WASAPI/PulseAudio): Membutuhkan buffer audio hardware terisi dalam interval waktu riil (100ms suara membutuhkan 100ms wall-clock). Inisialisasi perangkat memakan waktu 50–200ms per test case.
- Mock PCM In-Memory: 100ms suara pada sample rate 44.1kHz dihitung langsung sebagai operasi vektor memori sebesar ~8.8 KB data PCM, selesai dalam rentang 5–15 mikrosekon.
- Render Terminal Headless: Manipulasi matriks char dua dimensi dieksekusi secara instan di level L1/L2 cache CPU tanpa context switch kernel terminal driver.
Pemisahan batasan I/O ini memastikan seluruh suite pengujian regresi interpreter dapat mengeksekusi ribuan file skrip dalam hitungan detik secara deterministik di lingkungan CI mana pun.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!