Validasi

Program validasi yang memberi beban penuh pada setiap fitur dan menandai jalur proyek sebagai selesai.

Daftar isi

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.

Alur validasi proyek dari rancangan menuju validasi penuhRancangankeputusan dicatatTitik pemeriksaansatu uji per tahap,maju hanya bila lulusValidasi penuhseluruh fitur diujisekali jalan di akhir
Alur dari rancangan ke titik pemeriksaan per tahap, lalu validasi penuh di akhir

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.

python validasi.py
#!/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())
text Keluaran
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 LULUS

Sebelas 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.

python Contoh: pola berhenti cepat
# 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.py pada 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 LULUS pada build terakhir menandai jalur proyek selesai, sekaligus menjadi kontrak yang diulang setiap perubahan.

Referensi#