İçeriğe geç
academia.sh

Ders 09 / 12

Test ve Özellik Tabanlı Sınama

Sabit tohumlu, belirlenimci bir üreteçle üretilen iki yüz girdide bir özelliğin tutup tutmadığının sayılması ve bu sayının, derleyicinin hiç sormadığı bir soruyu hangi sınırla yanıtladığının gösterilmesi.

Yayın Güncelleme

Bu derste

  • Sabit tohumlu, belirlenimci bir üreteçle üretilen girdilerde bir özelliği sınar ve tutan girdi sayısını sayar
  • Ters çevirmenin iki kez uygulanmasının özdeşlik verdiğini iki yüz girdide doğrulayarak güvencenin sınıfını (devredildi) gösterir
  • Geçen bir özellik sınamasının her girdi için doğru anlamına gelmediğini, yalnızca bazı girdilerde tutan bir özellikle açıklar
İçindekiler

Bağımlılık ve Özellik Bayrakları dersi, derleyicinin hiç görmediği bir gövdeyi ölçtü — orada güvence hiç başlamıyordu. Ama önceki dersin son sorusu farklı bir yöne işaret ediyordu: calisan_govde gibi bir gövde derlemeye giriyor, tip denetiminden geçiyor, adları doğru çözülüyor ve yine de yanlış bir sonuç üretebiliyor. Derleyici bir gövdenin var olduğunu, tipinin tutarlı olduğunu denetler; o gövdenin doğru olduğunu hiçbir zaman denetlemez — doğruluk tipin değil, davranışın bir özelliğidir ve tip denetimi davranışı bilmez.

Bu boşluğu kapatmanın bir yolu koşarak sınamaktır: bir gövdeyi belirli girdilerle çalıştırıp sonucun beklenen bir özelliği taşıyıp taşımadığına bakmak. Bu, tek bir sabit girdiyle örnek sınaması yazmaktan farklıdır — bir örnek sınaması yalnız o tek girdide bir şeyin doğru olduğunu gösterir, seçilmemiş hiçbir girdi hakkında konuşmaz. Bu ders bunu tek bir sabit girdiyle değil, sabit tohumlu bir üreteçle üretilen çok sayıda girdiyle yapıyor — özellik tabanlı sınamanın temel fikri budur: tek bir örnek yerine, aynı özelliği birçok üretilmiş örnekte sınamak. Girdilerin belirlenimci bir üreteçten gelmesi bir yan koşul değil, bu dersin kurduğu gözlemin gerekli şartlarından biri: sistemin saatine ya da işletim sisteminin rastgelelik kaynağına dayanan bir üreteç kullanılsaydı, aynı sınama her makinede ve her koşumda farklı girdilerle çalışırdı ve tutan girdi sayısı koşumdan koşuma değişirdi — bu ders o değişkenliği baştan dışlıyor. Sınamanın kendisi denetlenimsiz değildir: hangi özelliğin sınanacağına, girdilerin hangi aralıktan geleceğine ve kaç girdinin yeteceğine karar veren, derleyici değil, sınamayı yazan taraftır. Kursun ortak sorusu — derleyicinin güvencesi burada nerede bitiyor — bu derste yanıtını buluyor: tip denetiminin bittiği yerde, davranış denetimi başlıyor, ama bu denetimin kapsamını da yazan kişi çiziyor.

Özelliğin Koşarak Sınanması

Bir özellik tabanlı sınama üç parçadan kurulur: rastgele girdi üreten bir kaynak, bu girdilerin her birinde sınanacak bir koşul ve koşulun kaç girdide tuttuğunu sayan bir döngü. Rust’ın standart kütüphanesinde rastgele sayı üreteci yoktur ve bu müfredatta üçüncü taraf kasa yasaktır; bu yüzden üreteç elle yazılır. PY5. Rastgele olması gereken tek şey girdilerin dağılımıdır — sınamanın kendisi belirlenimci kalmalıdır, aksi hâlde aynı koşum farklı makinelerde farklı sonuç verir. Aşağıdaki üreteç, sabit bir tohumdan başlayan ve her çağrıda aynı sırayı üreten bir doğrusal uyumlu üreteçtir (linear congruential generator): tohum aynıysa çıktı dizisi de her koşumda birebir aynıdır.

Üretecin durumu tek bir u64 sayıdır ve her çağrıda sabit iki sayıyla çarpılıp toplanarak güncellenir; wrapping_mul ve wrapping_add bu işlemde taşmayı sessizce sarmalar, böylece işlem hiçbir zaman panikle durmaz. uzunluk bu ham sayıyı bir üst sınıra göre daraltır (bir dizinin kaç eleman taşıyacağını belirler), eleman ise -500 ile 499 arasında bir tam sayı üretir. vektor bu ikisini birleştirir: önce bir uzunluk çeker, sonra o uzunlukta eleman üretir. Kullanılan sabitler bilinen bir doğrusal uyumlu üreteç ailesinden alınmıştır ve üretecin iyi dağılması bu dersin konusu değildir — burada aranan tek özellik, aynı tohumla her koşumda aynı diziyi üretmesidir; bu özellik yukarıdaki tüm koşumlarda birebir aynı sayıları verdi.

// dogrusal_uretec.rs — sabit tohumlu, belirlenimci sozde-rastgele uretec
pub struct DogrusalUretec {
    durum: u64,
}

impl DogrusalUretec {
    pub fn yeni(tohum: u64) -> Self {
        DogrusalUretec { durum: tohum }
    }

    pub fn sonraki(&mut self) -> u64 {
        self.durum = self
            .durum
            .wrapping_mul(6364136223846793005)
            .wrapping_add(1442695040888963407);
        self.durum
    }

    pub fn uzunluk(&mut self, ust_sinir: usize) -> usize {
        (self.sonraki() % (ust_sinir as u64 + 1)) as usize
    }

    pub fn eleman(&mut self) -> i32 {
        (self.sonraki() % 1000) as i32 - 500
    }

    pub fn vektor(&mut self, en_uzun: usize) -> Vec<i32> {
        let n = self.uzunluk(en_uzun);
        (0..n).map(|_| self.eleman()).collect()
    }
}

Sınanacak özellik, Proje Yaşam Döngüsü konusunun brifinginde de anılan klasik bir örnek: bir dizinin ters çevrilmesi iki kez uygulanınca özgün dizi geri gelmeli. Bu özellik Vecin reverse yöntemiyle kurulur ve elle üretilen her girdi için sınanır. İki yüz girdi üretilir, her biri en çok yirmi elemanlı; kaç girdide özelliğin tuttuğu sayılır.

// ozellik_sinamasi.rs — ters cevirmenin iki kez uygulanmasi ozdeslik verir mi
mod dogrusal_uretec;
use dogrusal_uretec::DogrusalUretec;

fn ters_ters_ozdeslik(v: &[i32]) -> bool {
    let mut once = v.to_vec();
    once.reverse();
    once.reverse();
    once == v
}

fn main() {
    let mut uretec = DogrusalUretec::yeni(42);
    let toplam_girdi = 200;
    let mut tutan = 0;

    for _ in 0..toplam_girdi {
        let girdi = uretec.vektor(20);
        if ters_ters_ozdeslik(&girdi) {
            tutan += 1;
        }
    }

    println!("sinanan girdi sayisi: {}", toplam_girdi);
    println!("ozelligin tuttugu girdi sayisi: {}", tutan);
}
sinanan girdi sayisi: 200
ozelligin tuttugu girdi sayisi: 200

Üreteç tohum 42 ile başladığında ürettiği ilk dizi on dört elemanlıdır, ikincisi on bir, üçüncüsü on sekiz — uzunluklar da tıpkı elemanlar gibi sabit tohumla belirlenimci biçimde değişir. Bu koşumda yazdırılan yalnızca toplam ve tutan sayılar, ama üretecin ürettiği iki yüz dizinin her biri bu şekilde farklı uzunluklarda ve farklı elemanlarla geliyor; vektora verilen üst sınır (20) bir dizinin en çok yirmi eleman taşıyabileceğini, en az sıfır (boş dizi) olabileceğini belirtiyor. En küçük uzunluk sıfır bırakıldı çünkü boş dizi de geçerli bir girdi — ters çevirmenin iki kez uygulanması boş bir dizide de özdeşlik vermelidir ve üreteç bu uç durumu da zaman zaman üretiyor.

Ters çevirmenin iki kez uygulanması, özellik tabanlı sınamanın anlatımını kolaylaştırmak için seçilmiş küçük bir örnek; aynı yaklaşım daha büyük gövdelere de uygulanabilir. Bir çözümleyicinin (parser) ürettiği bir yapıyı yeniden dizgeye çevirip tekrar çözümlemek, ilk yapıyla aynı sonucu vermeli; bir sıralama işlevinin çıktısı, girdiyle aynı elemanları taşımalı ve komşu elemanları sıralı olmalı. Bu derste seçilen özellik en basitlerinden biri — bir işlemin kendi tersiyle birleştiğinde özdeşlik vermesi — ama ölçme biçimi (üreteç, özellik, sayaç) aynı kalıyor: hangi gövde sınanırsa sınansın, derleyicinin denetlemediği yeri koşarak kapatmak, hep aynı üç parçayı gerektiriyor.

İki yüz girdinin ikisi yüzü de özelliği doğruladı. PY6. Bu sayı üreteçle birlikte belirlenimcidir: tohum 42 olduğu sürece üretilen iki yüz dizinin her biri aynı sırayla, aynı uzunlukta ve aynı elemanlarla gelir; koşum kaç kez tekrarlanırsa tekrarlansın sonuç değişmez. Bu, “rastgele” sözcüğünün bu derste ne anlama geldiğini de netleştiriyor: üretilen girdiler çeşitlidir (elle tek tek yazılmamıştır), ama üretim süreci rastgele değildir — her koşumda aynı diziyi üretir.

Ayrım cümlesi: M09/K05 kursunun Kapsam Raporlama dersi bir sınamanın hangi satırları çalıştırdığını ölçtü — kapsama oranı, kaynağın ne kadarının koşumdan geçtiğini gösteriyordu. Bu ders satırları saymıyor; bir özelliğin kaç girdide tuttuğunu sayıyor. Bir satırın çalışması onun doğru çalıştığı anlamına gelmez — yalnızca çalıştığı anlamına gelir; bu derste ölçülen, çalışmanın doğruluğa dair verdiği güvencenin gücü.

Güvencenin sınıfı burada devredildi: derleyici ters_ters_ozdeslik işlevinin tipini denetledi ve durdu, işlevin her girdi için doğru sonuç verip vermediğini hiç sormadı. Bu soruyu soran ve yanıtlayan, sınamayı yazan taraf oldu — hangi özelliğin sınanacağına, girdilerin hangi aralıktan geleceğine (-500’den 499’a, en çok yirmi elemanlı) ve kaç girdinin (iki yüz) yeteceğine yazan karar verdi. Derleyici bu kararların hiçbirine karışmadı; yalnızca yazılan sınama kodunun kendisinin tipçe tutarlı olduğunu denetledi.

Devredildi sınıfı, unsafe Rust dersinde ölçülen devirle aynı sözcüğü taşır ama farklı bir yerde duruyor: orada devredilen, bir belleğe erişimin sınır içinde kaldığı sözleşmesiydi; burada devredilen, bir davranışın doğru olduğu iddiası. İkisinde de ortak olan, derleyicinin bir noktada durup geri kalan sorumluluğu kaynağı yazan tarafa bırakmasıdır. Bu devrin bir sonucu da şu: ters_ters_ozdeslik işlevinin kendisi yanlış yazılmış olsaydı — örneğin ikinci reverse çağrısı unutulsaydı — derleyici bunu da fark etmezdi, çünkü yanlış yazılmış bir sınama da tıpkı doğru yazılmış biri gibi tip denetiminden geçer. Sınamanın doğruluğu da, sınadığı gövdenin doğruluğu gibi, yazan tarafın sorumluluğunda kalır.

Girdi sayısının kaç olacağı da aynı devrin bir parçası. Bu derste iki yüz seçildi; elli seçilseydi sınama daha az girdi görürdü, iki bin seçilseydi daha çok. Sayı büyüdükçe kapsanan girdi kümesi genişler, ama hiçbir sayı bütün girdileri kapsamaz. Derleyici bu sayı hakkında hiçbir görüş bildirmez — sayı ne kadar küçük olursa olsun ya da ne kadar büyük olursa olsun, sınama kodu aynı biçimde tip denetiminden geçer ve derlenir. Sayının yeterliliğine karar vermek, üretecin aralığını seçmeye karar vermekle aynı sorumluluğun parçası: her ikisi de yazan tarafın elinde, derleyicinin denetim alanının tamamen dışında.

Sınırlayıcı Ölçüm

Yukarıdaki koşum, seçilen özelliğin her girdide tuttuğunu gösterdi — ama bu sonuç, seçilen özelliğin doğru bir matematiksel gerçek olmasından kaynaklanıyor: bir diziyi iki kez ters çevirmek, sıralamadan bağımsız olarak her zaman özgün diziyi verir. Bütün özellikler böyle değildir. Aşağıdaki koşum, bazı girdilerde tutan ama bazılarında tutmayan bir özelliği aynı üreteçle sınıyor: bir dizinin kendi tersiyle aynı olması, yani bir bakışımlılık (palindrome) koşulu.

// sinir_sinamasi.rs — bazi girdilerde tutan, bazilarinda tutmayan bir ozellik
mod dogrusal_uretec;
use dogrusal_uretec::DogrusalUretec;

fn kendi_tersiyle_ayni(v: &[i32]) -> bool {
    let mut ters = v.to_vec();
    ters.reverse();
    ters == v
}

fn main() {
    let mut uretec = DogrusalUretec::yeni(7);
    let toplam_girdi = 200;
    let mut tutan = 0;
    let mut ilk_tutmayan: Option<Vec<i32>> = None;

    for _ in 0..toplam_girdi {
        let girdi = uretec.vektor(6);
        if kendi_tersiyle_ayni(&girdi) {
            tutan += 1;
        } else if ilk_tutmayan.is_none() {
            ilk_tutmayan = Some(girdi);
        }
    }

    println!("sinanan girdi sayisi: {}", toplam_girdi);
    println!("ozelligin tuttugu girdi sayisi: {}", tutan);
    println!("tutmayan ilk girdi uzunlugu: {}", ilk_tutmayan.map(|v| v.len()).unwrap_or(0));
}
sinanan girdi sayisi: 200
ozelligin tuttugu girdi sayisi: 62
tutmayan ilk girdi uzunlugu: 3

PY7. İki yüz girdinin altmış ikisinde özellik tuttu, geri kalan yüz otuz sekizinde tutmadı — üreteç kısa diziler de üretebildiği için (en çok altı eleman, sıfır dâhil), bazı girdiler tesadüfen bakışımlı çıktı: boş dizi ve tek elemanlı dizi her zaman kendi tersiyle aynıdır, bu da tutan sayıyı yükseltiyor. Tutmayan ilk girdinin uzunluğu üç — üç elemanlı bir dizinin bakışımlı olması, elemanların kendisine bağlı, uzunluğuna değil.

Bu ölçüm, geçen bir özellik sınamasının ne anlama geldiğini sınırlıyor. Önceki koşumda özellik iki yüz girdinin ikisi yüzünde de tuttu, çünkü sınanan özellik her zaman doğru bir matematiksel gerçekti; bu koşumda özellik yalnız altmış iki girdide tuttu, çünkü sınanan özellik bazen doğru. İkisi arasındaki farkı sınamanın kendisi göstermiyor — ikisi de “geçen sınama” olarak görünüyor, geçen girdi sayısının tümüyle eşit olması dışında. Bir özellik sınamasının geçmesi, o özelliğin sınanan girdi kümesi için tuttuğunu kanıtlar; sınanmayan bir girdi için hiçbir şey söylemez. İki yüz girdi geniş bir örneklem olabilir, ama üreteç sınırlı bir aralıktan (-500 ile 500 arası, en çok yirmi eleman) çekim yapıyor — bu aralığın dışındaki ya da bu uzunluğun ötesindeki bir girdi hiç sınanmadı. PY8. Geçen bir sınamanın verdiği güvence, sınadığı girdi sayısı ve aralığıyla sınırlıdır; iddia bu sayıya bağlanmadan yazıldığında güvencenin gerçek kapsamı gizlenmiş olur.

Ölçümün taşıdığı bir sınır daha var: tutmayan ilk girdi yalnız uzunluğuyla bildirildi, elemanlarıyla değil — bu, ortama bağlı olmayan bir seçim değil, bilinçli bir seçim. Gerçek özellik tabanlı sınama araçları, tutmayan bir girdi bulunca onu küçültme (shrinking) adı verilen bir süreçle daha küçük, daha okunabilir bir karşı örneğe indirger; bu ders böyle bir küçültme adımı yazmıyor, yalnız ilk tutmayan girdinin uzunluğunu bildiriyor. Bu, ölçümün bir başka sınırı: bir özelliğin tutmadığını göstermek, en anlaşılır karşı örneği bulmakla aynı şey değil — bu derste sayılan yalnız kaç girdide tutup tutmadığı, hangi girdinin en açıklayıcı karşı örnek olduğu değil.

Altmış iki sayısının kendisi de bir sınır taşıyor: bu, tohum 7 ile üretilen bu belirli iki yüz girdinin verdiği sayı. Tohum 11 ya da 100 seçilseydi, aynı özellik aynı aralıkta sınansa bile tutan girdi sayısı farklı çıkardı — çünkü üretilen girdi kümesi tohuma bağlı olarak değişir. Bu ders bu değişkenliği ölçmüyor; tek bir tohumla, tek bir koşumu belirlenimci kılıp o koşumun sonucunu bildiriyor. Belirlenimcilik burada tekrarlanabilirliği garanti ediyor (aynı tohum her zaman aynı altmış ikiyi verir), ama sınanan aralığın temsil gücünü garanti etmiyor — o güç, aralığın ve üretecin nasıl tasarlandığına bağlı, tek bir koşumun sonucuna değil.

Özet

  • Rust’ın standart kütüphanesinde rastgele sayı üreteci yoktur; bu derste sabit tohumlu, elle yazılmış bir doğrusal uyumlu üreteç kullanıldı — aynı tohum her koşumda aynı girdi dizisini üretir.
  • Ters çevirmenin iki kez uygulanmasının özdeşlik verdiği özelliği, iki yüz üretilmiş girdinin ikisi yüzünde de tuttu.
  • Güvencenin sınıfı devredildi: derleyici sınama kodunun tipini denetledi ve durdu; hangi özelliğin, hangi girdi aralığında, kaç girdiyle sınanacağına yazan taraf karar verdi.
  • Bazen doğru olan bir özellik (kendi tersiyle aynı olma), aynı üreteçle sınandığında iki yüz girdinin yalnız altmış ikisinde tuttu — geçen bir sınama her girdi için doğru anlamına gelmiyor.
  • Bu ölçümün sınırı: bir özellik sınamasının verdiği güvence yalnızca sınanan girdi sayısı ve aralığı kadardır; aralık dışındaki bir girdi hiç sınanmamıştır.
  • Girdi sayısı, aralığı ve sınanan özelliğin kendisi — hangisinin ne kadar yeteceği de dâhil — derleyicinin değil, sınamayı yazan tarafın kararıdır; aynı üç parça (üreteç, özellik, sayaç) ters çevirme dışındaki gövdelere de uygulanır.

Sonraki Adım

Bu ders, bir özelliğin doğru çalıştığını koşarak gösterebileceğimizi, ama bu gösterimin sınırlı sayıda girdiyle ve yazan tarafın kararlarıyla sınırlı kaldığını ölçtü. Sıradaki ders aynı “koşarak kurulan güvence” fikrini bambaşka bir soruya uyguluyor: iki yazımdan hangisinin daha az iş yaptığı da yalnız koşularak öğrenilebilir, doğruluk gibi derleyicinin hiç sormadığı bir soru. Ama bu kez sınanan doğruluk değil, işlem sayısı; ve girdi sayısı yerine bu kez bir tek girdide kaç karşılaştırma, kaç kopyalama yapıldığı sayılıyor. Karşılaştırmalı Ölçüm ve Profilleme dersi, bu sayının neyi söyleyip neyi söylemediğini — özellikle neden bir süre değil — inceliyor.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat