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.