Titik pemeriksaan menjaga setiap tahap tetap jujur selama pembangunan berlangsung, tetapi pemeriksaan itu tersebar dan berhenti pada batas tahapnya. Validasi menutup celah tersebut dengan satu eksekusi penuh yang memberi beban pada setiap fitur sekaligus, termasuk kasus yang jarang tersentuh seperti nilai negatif, offset negatif, dan pelipatan 32 bit. Topik ini membangun program validasi untuk Mesin Mini yang sama, menjalankannya sampai ringkasan akhir, lalu menetapkan baris VALIDASI LULUS sebagai tanda jalur proyek dinyatakan selesai.
Peta alur validasi#
Validasi penuh adalah pemeriksaan sekali jalan yang dilakukan setelah seluruh tahap selesai dibangun. Posisinya dalam pengerjaan proyek dapat diringkas dalam tiga kotak: rancangan yang mencatat keputusan, titik pemeriksaan yang menguji satu tahap pada waktunya, dan validasi penuh yang memberi beban pada seluruh fitur dalam satu eksekusi. Diagram berikut memperlihatkan urutan tersebut dari kiri ke kanan.
Titik pemeriksaan dan validasi penuh melengkapi, bukan menggantikan, satu sama lain. Titik pemeriksaan merangkak per tahap dan menghentikan kerja pada fitur yang belum siap, sedangkan validasi penuh memeriksa interaksi antarfitur pada build akhir, misalnya hasil perulangan yang disimpan ke tumpukan lalu dicetak. Satu perintah python3 validasi.py mengulang seluruh pemeriksaan itu dalam hitungan detik, dan kode keluarnya dapat dipakai oleh skrip apa pun yang menuntut bukti lulus.
Struktur program validasi#
Program validasinya bernama validasi.py dan berdiri di atas Mesin Mini yang sama. Berkas emulator.py disalin utuh ke bundle topik ini, dan validasi.py mengimpornya melalui dua baris sys.path.insert yang sama dengan pola program uji titik pemeriksaan. Setiap instruksi mendapat satu fungsi uji, dan setiap fungsi menyusun program ujinya sendiri sebagai daftar instruksi eksplisit di dalam kode, tanpa input pengguna. Pemeriksaan memakai pernyataan assert dengan pesan singkat, sedangkan error mesin seperti alamat di luar jangkauan ikut dihitung sebagai kegagalan uji. Jalannya uji memakai pola kumpulkan semua temuan, yaitu seluruh uji tetap dieksekusi meski ada yang gagal, lalu ditutup baris RINGKASAN dan baris keputusan VALIDASI LULUS atau VALIDASI GAGAL.
#!/usr/bin/env python3
"""validasi.py — program validasi penuh Mesin Mini.
Setiap instruksi diberi beban penuh: nilai negatif, pelipatan 32 bit,
beberapa alamat memori, cabang maju dan mundur, perulangan, serta
konvensi tumpukan sederhana. Seluruh program uji didefinisikan eksplisit
di berkas ini, tanpa masukan pengguna.
Pemakaian:
python3 validasi.py # kumpulkan semua temuan
python3 validasi.py --cepat # berhenti pada kegagalan pertama
"""
import io
import sys
from contextlib import redirect_stdout
from pathlib import Path
# emulator.py berada di samping berkas ini, atau di direktori kerja saat
# skrip dijalankan dari salinannya
sys.path.insert(0, str(Path(__file__).resolve().parent))
sys.path.insert(0, str(Path.cwd()))
from emulator import Mesin
def jalankan(program):
"""Bangun mesin, jalankan programnya sampai berhenti, kembalikan mesin."""
return Mesin(program).jalankan()
def cetakan(program):
"""Jalankan program dan kembalikan baris-baris yang dicetak ecall."""
penyangga = io.StringIO()
with redirect_stdout(penyangga):
jalankan(program)
return penyangga.getvalue().splitlines()
def uji_addi():
m = jalankan([
"addi a0, zero, 7",
"addi a1, zero, -3",
"addi zero, zero, 99", # penulisan ke x0 harus terabaikan
"halt",
])
assert m.baca("a0") == 7, f"a0={m.baca('a0')}, harapan 7"
assert m.baca("a1") == -3, f"a1={m.baca('a1')}, harapan -3"
assert m.baca("zero") == 0, f"x0 berubah menjadi {m.baca('zero')}"
def uji_add():
m = jalankan([
"addi a0, zero, 20",
"addi a1, zero, 22",
"add a2, a0, a1",
"halt",
])
assert m.baca("a2") == 42, f"a2={m.baca('a2')}, harapan 42"
def uji_add_melingkar():
"""Penjumlahan harus melipat sesuai lebar 32 bit."""
m = jalankan([
"addi a0, zero, 2147483647",
"addi a1, zero, 1",
"add a2, a0, a1",
"halt",
])
assert m.baca("a2") == -2147483648, f"a2={m.baca('a2')}, harapan -2147483648"
def uji_sub():
m = jalankan([
"addi a0, zero, 5",
"addi a1, zero, 9",
"sub a2, a0, a1",
"halt",
])
assert m.baca("a2") == -4, f"a2={m.baca('a2')}, harapan -4"
def uji_lw_sw():
m = jalankan([
"addi sp, zero, 100",
"addi a0, zero, 11",
"sw a0, 0(sp)", # alamat 100
"addi a0, zero, 22",
"sw a0, 4(sp)", # alamat 104
"sw a0, -4(sp)", # alamat 96, offset negatif
"lw a1, 0(sp)",
"lw a2, 4(sp)",
"lw a3, -4(sp)",
"halt",
])
assert m.mem[100] == 11, f"mem[100]={m.mem[100]}, harapan 11"
assert m.mem[104] == 22, f"mem[104]={m.mem[104]}, harapan 22"
assert m.mem[96] == 22, f"mem[96]={m.mem[96]}, harapan 22"
assert m.baca("a1") == 11, f"a1={m.baca('a1')}, harapan 11"
assert m.baca("a2") == 22, f"a2={m.baca('a2')}, harapan 22"
assert m.baca("a3") == 22, f"a3={m.baca('a3')}, harapan 22"
def uji_beq():
m = jalankan([
"beq zero, zero, lewati", # selalu ambil cabang maju
"addi a0, zero, 1", # harus dilewati
"lewati: addi a1, zero, 2",
"halt",
])
assert m.baca("a0") == 0, "cabang maju tidak melompati instruksi"
assert m.baca("a1") == 2, "target cabang tidak tereksekusi"
def uji_bne_perulangan():
m = jalankan([
"ulang: add a1, a1, a0", # jumlah += penghitung
"addi a0, a0, 1", # penghitung += 1
"addi a2, zero, 6",
"bne a0, a2, ulang", # mundur selama penghitung belum 6
"halt",
])
assert m.baca("a1") == 15, f"jumlah={m.baca('a1')}, harapan 15"
assert m.baca("a0") == 6, f"penghitung={m.baca('a0')}, harapan 6"
def uji_j():
m = jalankan([
"j akhir",
"addi a0, zero, 1", # tidak boleh tereksekusi
"akhir: halt",
])
assert m.baca("a0") == 0, "j tidak melompati instruksi"
def uji_ecall_cetak():
baris = cetakan([
"addi a7, zero, 1", # layanan 1: cetak bilangan
"addi a0, zero, 42",
"ecall",
"addi a0, zero, -7",
"ecall",
"halt",
])
assert baris == ["42", "-7"], f"keluaran ecall: {baris}"
def uji_berhenti():
m = jalankan([
"addi a7, zero, 10",
"ecall", # layanan 10: akhiri program
"addi a0, zero, 1", # tidak boleh tereksekusi
"halt",
])
assert m.baca("a0") == 0, "instruksi setelah ecall 10 ikut tereksekusi"
m = jalankan([
"addi a0, zero, 1",
"halt",
"addi a1, zero, 2", # tidak boleh tereksekusi
])
assert m.baca("a1") == 0, "instruksi setelah halt ikut tereksekusi"
def uji_tumpukan():
"""Konvensi panggilan sederhana: dorong argumen, rutin menggantinya dengan hasil."""
m = jalankan([
"addi sp, zero, 120",
"addi a0, zero, 7",
"addi sp, sp, -4", # dorong: tumpukan tumbuh ke bawah
"sw a0, 0(sp)", # simpan argumen di puncak tumpukan
"beq zero, zero, dobel", # masuk ke badan rutin
"kembali: lw a2, 0(sp)", # ambil hasil dari puncak tumpukan
"addi sp, sp, 4", # pop: pulihkan tumpukan
"halt",
"dobel: lw a1, 0(sp)", # ambil argumen
"add a1, a1, a1", # dobelkan lewat penjumlahan
"sw a1, 0(sp)", # taruh hasil di tempat argumen
"j kembali",
])
assert m.baca("a2") == 14, f"hasil={m.baca('a2')}, harapan 14"
assert m.baca("sp") == 120, f"sp={m.baca('sp')}, harapan 120"
DAFTAR_UJI = [
("addi", uji_addi),
("add", uji_add),
("add-melingkar", uji_add_melingkar),
("sub", uji_sub),
("lw-sw", uji_lw_sw),
("beq", uji_beq),
("bne-perulangan", uji_bne_perulangan),
("j", uji_j),
("ecall-cetak", uji_ecall_cetak),
("ecall-berhenti", uji_berhenti),
("tumpukan", uji_tumpukan),
]
def main():
cepat = "--cepat" in sys.argv[1:]
lulus = 0
for nama, fungsi in DAFTAR_UJI:
try:
fungsi()
except Exception as e:
print(f"GAGAL {nama}: {type(e).__name__}: {e}")
if cepat:
break
continue
lulus += 1
print(f"LULUS {nama}")
total = len(DAFTAR_UJI)
print(f"RINGKASAN: {lulus} dari {total} uji lulus")
if lulus == total:
print("VALIDASI LULUS")
return 0
print("VALIDASI GAGAL")
return 1
if __name__ == "__main__":
raise SystemExit(main())LULUS addi
LULUS add
LULUS add-melingkar
LULUS sub
LULUS lw-sw
LULUS beq
LULUS bne-perulangan
LULUS j
LULUS ecall-cetak
LULUS ecall-berhenti
LULUS tumpukan
RINGKASAN: 11 dari 11 uji lulus
VALIDASI LULUSSebelas baris LULUS berarti sebelas kelompok pemeriksaan bekerja pada build yang sama: addi dengan nilai negatif, penjumlahan add biasa, pelipatan 32 bit pada add, sub yang menghasilkan bilangan negatif, load dan store pada tiga alamat berbeda, cabang maju beq, perulangan bne yang mundur, lompatan j, ecall cetak, ecall berhenti, serta konvensi tumpukan dengan dorong dan ambil. Satu baris ringkasan menutup semuanya dengan angka yang dapat dihitung ulang.
Membaca keluaran validasi#
Satu baris LULUS atau GAGAL per uji bukan sekadar gaya, melainkan alat penunjuk lokasi. Nama uji yang gagal langsung menunjuk instruksi yang bermasalah, sehingga pencarian dimulai dari fungsi eksekusi satu mnemonik, bukan dari seluruh emulator. Baris RINGKASAN menambahkan ukuran kuantitatif, yaitu berapa dari berapa kelompok pemeriksaan yang lulus pada build tersebut.
Baris VALIDASI LULUS memiliki makna yang terbatas dan perlu dibaca secara jujur. Baris itu menyatakan bahwa setiap fitur yang didefinisikan pada daftar uji telah melewati beban yang didefinisikan pula pada daftar yang sama, bukan bahwa program bebas dari seluruh kemungkinan bug. Karena itu daftar uji tumbuh mengikuti daftar fitur, yaitu satu fitur baru menuntut satu fungsi uji baru. Dengan disiplin tersebut, jalur proyek dinyatakan selesai ketika VALIDASI LULUS muncul pada build terakhir, dan berkas validasi ikut disimpan bersama proyek sebagai bukti sekaligus alat periksa ulang.
Berhenti cepat atau mengumpulkan temuan#
Dua pola pengerjaan uji membagi praktik ini, dan keduanya sah pada waktunya masing-masing. Pola berhenti cepat (fail-fast) menghentikan seluruh eksekusi pada temuan pertama, sehingga waktu tunggu antara suntingan dan laporan singkat. Pola ini paling berguna saat satu kegagalan sedang diburu, karena uji setelah temuan pertama belum tentu relevan sebelum penyebabnya dibereskan.
Pola kumpulkan semua temuan menjalankan seluruh daftar meski ada yang gagal, lalu melaporkan semuanya sekaligus. Bentuk ini memperlihatkan apakah kegagalan berdiri sendiri atau berkelompok pada satu fitur yang sama, dan bentuk itulah yang tepat untuk laporan akhir. validasi.py memakai pola kumpulkan sebagai bawaan dan menyediakan opsi --cepat untuk pola berhenti cepat. Pada rangkaian yang seluruhnya lulus, keduanya mencetak keluaran yang sama persis, sehingga perbedaannya baru terlihat ketika ada baris GAGAL.
Bentuk inti pola berhenti cepat terlihat pada potongan berikut.
# tidak-dijalankan
for nama, fungsi in daftar_uji:
if tidak_lulus(fungsi):
print(f"GAGAL {nama}")
raise SystemExit(1) # uji setelahnya tidak dijalankan
print(f"LULUS {nama}")Kesalahan umum#
- Memvalidasi hanya jalur yang enak. Uji yang seluruhnya memakai nilai positif dan satu alamat akan lulus pada mesin yang tetap keliru mengelola nilai negatif. Wajibkan nilai negatif, pelipatan 32 bit, beberapa alamat, dan cabang dua arah pada daftar uji.
- Menganggap lulus lama tetap sah. Mesin yang baru disunting dapat merusak perilaku lama, dan ringkasan lama tidak ikut berubah dengan sendirinya. Jalankan
python3 validasi.pypada setiap perubahan dan periksa kode keluarnya, bukan sekadar keluaran di layar. - Menambah fitur tanpa menambah uji. Instruksi baru tanpa fungsi uji baru membuat daftar validasi terlambat mengenal fiturnya sendiri. Terapkan aturan satu fitur baru berarti satu entri baru pada
DAFTAR_UJI.
Ringkasan#
- Validasi memberi beban penuh pada setiap fitur dalam satu eksekusi, mencetak satu baris LULUS atau GAGAL per uji, lalu menutup dengan ringkasan dan kode keluar.
- Satu fungsi uji per instruksi dengan program uji eksplisit di dalam kode menjaga seluruh pemeriksaan tetap deterministik.
- Pola berhenti cepat melayani masa perbaikan aktif, sedangkan pola kumpulkan semua temuan melayani laporan akhir, dan keduanya tersedia dalam
validasi.py. - Baris
VALIDASI LULUSpada build terakhir menandai jalur proyek selesai, sekaligus menjadi kontrak yang diulang setiap perubahan.
Referensi#
- RISC-V ISA Manual — RISC-V International — Semantik resmi instruksi RV32I yang menjadi acuan nilai harapan setiap uji.
- CS:APP3e, Computer Systems: A Programmer’s Perspective — Carnegie Mellon University — Disiplin tumpukan dan konvensi panggilan tingkat mesin yang ditiru oleh uji tumpukan.