İçeriğe geç
academia.sh

Ders 13 / 15

Test Çerçeveleri

Çatallanmış bir çocukta koşan bir testin değişikliğinin neden sonraki teste sızmadığını, ve bu yalıtımın dosya sistemi durumunu neden kapsamadığını ölçer.

Yayın Güncelleme

Bu derste

  • Bir test tablosunu, onaylama makrosunu ve geçti/kaldı sayımını C ile kurar
  • Süreç yalıtımının bir testin değişikliğini neden sonraki teste taşımadığını ölçer
  • Süreç yalıtımının bellek durumunu kestiğini, dosya sistemi durumunu kesmediğini gösterir
İçindekiler

Bir kütüphaneye doğru bağlanmak, onu kullanan kodun doğru çalıştığını göstermez. Bunu sınamanın kendisi de bir görünürlük sorunu taşır: bir test başka bir testin bıraktığı durumu görüyorsa, hangi testin gerçekten başarısız olduğunu artık söyleyemezsiniz. Küçük bir test çerçevesi bu sorunu, her testi kendi kopyasında çalıştırarak çözer.

Test Tablosu ve Yalıtılmış Koşum

Bir test çerçevesinin çekirdeği bir işlev işaretçisi tablosu, tek bir onaylama makrosu ve bir geçti/kaldı sayımıdır. Onaylama başarısız olduğunda test işlevi 0 döndürür; onaylanacak bir şey kalmadan sona ererse 1 döndürür.

// cerceve.h — test tablosu, onaylama makrosu ve paylasilan durumun bildirimi
#ifndef CERCEVE_H
#define CERCEVE_H

typedef int (*TestIslevi)(void);

struct TestGirdisi {
    const char *ad;
    TestIslevi calistir;
};

#define ONAYLA(kosul) do { if (!(kosul)) return 0; } while (0)

extern int ortak_durum;          /* testler arasi sizintiyi gozlemlemek icin */
extern int sahte_cagri_sayaci;   /* sahte bagimliligin kac kez cagrildigi */
extern int duzenek_kurulum_sayaci;  /* duzenegin test oncesi kac kez kuruldugu */
extern int duzenek_temizlik_sayaci; /* duzenegin test sonrasi kac kez sokuldugu */

int test_durumu_ayarla(void);
int test_durumu_sifir_mi(void);
int test_duzenek_degerini_gor(void);
int test_dosyaya_yaz(void);
int test_dosyadan_oku(void);
int sahte_bagimlilik(int girdi);
int guduk_bagimlilik(int girdi);
int gercek_isi_yap(int girdi, int (*bagimlilik)(int));
int duzenek_kur(void);
int duzenek_sok(void);

#endif
// cerceve.c — test govdeleri, sahte bagimlilik ve dosya tabanli iki test
#include <stdio.h>
#include "cerceve.h"

int ortak_durum = 0;
int sahte_cagri_sayaci = 0;

int test_durumu_ayarla(void) {
    ortak_durum = 9;
    return 1;
}

int test_durumu_sifir_mi(void) {
    ONAYLA(ortak_durum == 0);
    return 1;
}

int test_duzenek_degerini_gor(void) {
    ONAYLA(ortak_durum == 5);   /* duzenek_kur burayi 5'e getirmis olmali */
    return 1;
}

int test_dosyaya_yaz(void) {
    FILE *f = fopen("iz.dat", "w");
    if (!f) return 0;
    fputc('1', f);
    fclose(f);
    return 1;
}

int test_dosyadan_oku(void) {
    FILE *f = fopen("iz.dat", "r");
    if (!f) return 0;
    int deger = fgetc(f);
    fclose(f);
    return deger == '1';
}

int sahte_bagimlilik(int girdi) {
    sahte_cagri_sayaci++;
    return girdi * 2;
}

int duzenek_kurulum_sayaci = 0;
int duzenek_temizlik_sayaci = 0;

int guduk_bagimlilik(int girdi) {
    (void)girdi;             /* gudugun cevabi sabittir, girdiyi hic okumaz */
    return 42;                /* onceden kararlastirilmis, degismeyen cevap */
}

int gercek_isi_yap(int girdi, int (*bagimlilik)(int)) {
    return bagimlilik(girdi) + 1;
}

int duzenek_kur(void) {
    duzenek_kurulum_sayaci++;
    ortak_durum = 5;    /* duzenegin kurdugu bilinen baslangic durumu */
    return 1;
}

int duzenek_sok(void) {
    duzenek_temizlik_sayaci++;
    ortak_durum = 0;    /* bir sonraki test icin durumu temiz birak */
    return 1;
}

PA5. Test tablosu yalnız iki girdiyle gösteriliyor; mekanizma herhangi bir sayıda girdiye genelliyor, çünkü koşum sırası tablo üzerinde bir döngüden ibaret.

Her testi kendi çocuğunda koşturan yardımcı, önceki modülde kurulan çatallama sözleşmesini kullanıyor: tampon boşaltılıyor, çocuk testi çalıştırıp çıkış durumuyla sonucu bildiriyor, ebeveyn yalnız o durumu okuyor.

PA6. Düzenek burada tek bir çift işlevle — bir kurulum, bir temizlik — gösteriliyor; gerçek bir çerçevede düzenek testler arasında farklılaşabilir, ama çift olma zorunluluğu (her kuruluma karşılık bir temizlik) değişmiyor.

Testin kendisinden önce ve sonra çalışması gereken ortak bir hazırlık varsa, bu hazırlığı her testin gövdesine tek tek yazmak yerine ayrı bir düzenek (kurulum/temizlik çifti) olarak çıkarmak, aynı hazırlığın tekrar tekrar yazılmasını önler ve daha önemlisi, temizliğin testin sonucundan bağımsız olarak her zaman çalışmasını garanti eder. Bir düzeneğin sözleşmesi şudur: kurulum testten önce tam olarak bir kez çalışır, temizlik testten sonra tam olarak bir kez çalışır — test geçse de kalsa da, hatta test bir onaylamada erken dönse bile. Aşağıdaki duzenek_kur/duzenek_sok çifti bu sözleşmeyi modelliyor: duzenek_kur ortak_durum’u bilinen bir başlangıç değerine (5) getiriyor ve kurulum sayacını artırıyor; duzenek_sok durumu sıfırlıyor ve temizlik sayacını artırıyor. Bu iki sayaç, düzeneğin gerçekten her test için bir kez kurulup bir kez söküldüğünü doğrulamanın yolu.

Güdük, sahte nesneden farklı bir amaca hizmet eder ve bu ders ikisini ayrı tutuyor. Bir güdük (guduk_bagimlilik), kendisine hangi girdi verilirse verilsin sabit, önceden kararlaştırılmış bir cevap döndürür ve çağrıldığını hiçbir şekilde kaydetmez — amacı yalnız gerçek bağımlılığın yerini doldurup testin geri kalanının çalışabilmesini sağlamaktır. Bir sahte nesne (sahte_bagimlilik) ise buna ek olarak kendi çağrılma biçimini gözlenebilir kılar; bu derste bunu bir çağrı sayacıyla yapıyor. Ayrım şuna indirgenir: güdük testin girdisini hazırlar, sahte nesne testin doğrulamasının bir parçası olur. Bir test yalnız gercek_isi_yap’ın doğru bir sayı ürettiğini görmek istiyorsa güdük yeterlidir ve daha basittir; test ayrıca bağımlılığın kaç kez çağrıldığını da doğrulamak istiyorsa sahte nesne gerekir. İkisini karıştırıp her yerde sahte nesne kullanmak, çağrı sayısı hiç sorulmayan testlere gereksiz bir gözlem yükü bindirir; her yerde güdük kullanmak ise çağrı davranışını doğrulaması gereken testleri kör bırakır.

// olcum.c — yalitilmis kosum, yalitilmamis sizinti ve dosya sistemi siniri tek kosumda
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "cerceve.h"

static int yalitilmis_calistir(TestIslevi islev) {
    fflush(stdout);   /* aksi halde alt surec, ana surecin bosaltilmamis tamponunu da tasir */
    pid_t pid = fork();
    if (pid == 0) {
        _exit(islev() ? 0 : 1);
    }
    int durum = 0;
    waitpid(pid, &durum, 0);
    return WIFEXITED(durum) && WEXITSTATUS(durum) == 0;
}

/* duzenekli_calistir: kurulum ve temizlik ebeveynde, testin kendisi yalitilmis cocukta */
static int duzenekli_calistir(TestIslevi islev) {
    duzenek_kur();
    int sonuc = yalitilmis_calistir(islev);
    duzenek_sok();
    return sonuc;
}

int main(void) {
    printf("yalitilmis durum ayarla: %s\n",
           yalitilmis_calistir(test_durumu_ayarla) ? "gecti" : "kaldi");
    printf("yalitilmis durum sifir mi: %s\n",
           yalitilmis_calistir(test_durumu_sifir_mi) ? "gecti" : "kaldi");

    /* ayni iki test bu kez yalitilmadan, dogrudan ayni surecte cagriliyor */
    test_durumu_ayarla();
    printf("yalitilmamis durum sifir mi: %s\n",
           test_durumu_sifir_mi() ? "gecti" : "kaldi");

    int sonuc = gercek_isi_yap(3, sahte_bagimlilik);
    printf("sahte bagimlilik cagri sayisi: %d\n", sahte_cagri_sayaci);
    printf("gercek_isi_yap sonucu: %d\n", sonuc);

    int guduk_sonuc = gercek_isi_yap(3, guduk_bagimlilik);
    printf("guduk ile gercek_isi_yap sonucu: %d\n", guduk_sonuc);

    printf("duzenekli test degeri gor: %s\n",
           duzenekli_calistir(test_duzenek_degerini_gor) ? "gecti" : "kaldi");
    printf("duzenek kurulum sayaci: %d\n", duzenek_kurulum_sayaci);
    printf("duzenek temizlik sayaci: %d\n", duzenek_temizlik_sayaci);
    printf("duzenekten sonra ana surecte ortak_durum: %d\n", ortak_durum);

    printf("yalitilmis dosyaya yaz: %s\n",
           yalitilmis_calistir(test_dosyaya_yaz) ? "gecti" : "kaldi");
    printf("yalitilmis dosyadan oku: %s\n",
           yalitilmis_calistir(test_dosyadan_oku) ? "gecti" : "kaldi");
    return 0;
}
yalitilmis durum ayarla: gecti
yalitilmis durum sifir mi: gecti
yalitilmamis durum sifir mi: kaldi
sahte bagimlilik cagri sayisi: 1
gercek_isi_yap sonucu: 7
guduk ile gercek_isi_yap sonucu: 43
duzenekli test degeri gor: gecti
duzenek kurulum sayaci: 1
duzenek temizlik sayaci: 1
duzenekten sonra ana surecte ortak_durum: 0
yalitilmis dosyaya yaz: gecti
yalitilmis dosyadan oku: gecti

İlk iki satır aynı ortak_durum değişkenini kullanan iki testin, birbirinden ayrı çocuklarda koştuğunu gösteriyor: test_durumu_ayarla kendi çocuğunda ortak_durum’u 9 yapıyor, ama bu çocuk çıktığında o değişiklik onunla birlikte kayboluyor. test_durumu_sifir_mi kendi çocuğunda çalıştığında, ebeveynin hâlâ 0 olan ortak_durum değerinin bir kopyasını devralıyor ve onaylama geçiyor. Üçüncü satır aynı iki testi yalıtımsız çalıştırıyor: ikisi de tek süreçte, gerçek ortak_durum üzerinde çalışıyor, birincinin bıraktığı 9 ikinciyi doğrudan etkiliyor ve onaylama kalıyor — bu, süreç yalıtımının önlediği sızıntının kendisi.

Bu sızıntının tehlikeli yanı, sonucun test dosyasındaki sıraya bağlı olmasıdır. Aynı iki test, test_durumu_sifir_mi önce çalışacak şekilde yazılsaydı, sızıntı hiç görünmez ve iki test de yalıtımsız haliyle bile geçerdi — çünkü ortak_durum henüz 9’a çekilmemiş olurdu. Bu, yalıtımsız bir test paketinin en aldatıcı özelliğidir: paket bir sırada tamamen yeşil görünür, testler yeniden düzenlendiğinde ya da yeni bir test araya girdiğinde aynı paket kırmızıya döner, oysa hiçbir testin kendi mantığı değişmemiştir. Hatanın kaynağı testlerin hiçbirinde değil, aralarındaki gizli bağımlılıktadır — ve bu bağımlılık kod incelemesinde görünmez kalır, çünkü iki test birbirini hiç çağırmaz, yalnız aynı değişkeni paylaşır. Süreç yalıtımının asıl kazancı burada ortaya çıkıyor: her test kendi çocuğunda çalıştığı için, testler hangi sırayla yazılırsa yazılsın sonuç aynı kalır — sıra, testin doğruluğunu etkileyen bir değişken olmaktan çıkar.

sahte_bagimlilik gerçek bağımlılığın yerine geçen bir işlev işaretçisi; gercek_isi_yap onu kendi mantığını değiştirmeden çağırıyor ve çağrı sayısı sahte_cagri_sayaci üzerinden ölçülüyor. Bu sayaç da tıpkı ortak_durum gibi bir süreç belleğinde yaşıyor — bir test bunu çatallanmış bir çocukta okusaydı, o çocuğun gördüğü sayım da kendi kopyası olurdu ve ebeveyne geri dönmezdi; ebeveyne dönen tek şey testin geçip geçmediğidir.

Sahte nesnenin burada değiştirmediği şey gercek_isi_yap’ın kendi mantığıdır: işlev hâlâ kendisine verilen işaretçiyi çağırıyor, yalnız o işaretçinin gösterdiği gövde gerçek bağımlılık yerine sayaç tutan bir gövdeyle değiştirildi. Bu, testin ölçtüğü şeyi ikiye ayırıyor — gercek_isi_yap’ın kendi hesabının doğruluğu bir yanda, çağırdığı bağımlılığın kaç kez ve hangi sırayla çağrıldığı öbür yanda. İkinci soru gerçek bağımlılıkla sınanamaz, çünkü gerçek bağımlılığın kendi çağrı sayısını dışarıdan gözlemlemenin bir yolu yoktur; sahte nesne bu gözlemi mümkün kılan tek araçtır.

guduk_bagimlilik ile alınan sonuç 43 — girdi ne olursa olsun sabit 42 döndüren güdüğün üzerine gercek_isi_yap’ın eklediği 1. Bu satır çağrı sayısı hakkında hiçbir şey söylemiyor, çünkü güdük zaten hiçbir şey saymıyor; yalnız gercek_isi_yap’ın kendi hesabının bağımlılığın döndürdüğü değeri doğru işlediğini gösteriyor. Sahte nesneyle alınan sonuç 7 ile bu sonuç karşılaştırıldığında, iki test aynı işlevi iki farklı soru için sınıyor: biri “doğru cevabı üretiyor mu”, öbürü “bağımlılığı doğru sayıda çağırıyor mu”.

PA7. duzenek_kur/duzenek_sok çifti burada ebeveyn süreçte, yalitilmis_calistir’ın çağrılmasından hemen önce ve hemen sonra çalıştırılıyor; testin kendisi hâlâ kendi çocuğunda koşuyor, düzenek çocuğun içine taşınmıyor.

duzenekli_calistir satırı, düzeneğin sözleşmesini üç sayıyla doğruluyor. Kurulum sayacı 1, temizlik sayacı 1 — yani her ikisi de tam olarak bir kez çalıştı, ne eksik ne fazla. Testin kendisi ortak_durum == 5 onaylamasını geçti, çünkü duzenek_kur bu değeri testin çocuğundan önce, ebeveynde kurdu ve çocuk bu değeri fork ile devraldı. Son satır, duzenek_sok çalıştıktan sonra ana süreçteki ortak_durum‘un yeniden 0 olduğunu gösteriyor — düzeneğin temizliği, testin çocuğu çoktan çıktıktan sonra ana süreç üzerinde gerçekleşiyor, çünkü kurulum ve temizlik hiç çatallanmadan, doğrudan ebeveynde çalışıyor. Bu, düzeneğin neden yalıtılmış koşumun dışında tutulması gerektiğini gösteriyor: düzenek her testten önce ve sonra tekrarlanan ortak bir hazırlıktır, testin kendisi gibi her seferinde ayrı bir çocuğa taşınması gerekmez — taşınsaydı, kurulumun etkisi (ortak_durum’un 5 olması) testin çocuğuna hiç ulaşamazdı, çünkü her çocuk yalnız kendi fork anındaki durumu devralır.

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

Son iki satır, süreç yalıtımının nerede durduğunu gösteriyor. test_dosyaya_yaz kendi çocuğunda iz.dat dosyasına bir değer yazıyor ve çocuk çıkıyor; bellek durumu onunla birlikte kayboluyor. Ama test_dosyadan_oku, tamamen ayrı bir çocukta çalıştığı halde, aynı dosyayı açıp önceki testin yazdığı değeri okuyabiliyor ve onaylama geçiyor. Çatallama yalnız belleği kopyalar; dosya sistemi, sürecin dışında kalan bir nesnedir ve fork’un yalıttığı alanın parçası değildir. İki test aynı dosyaya dokunuyorsa, aralarında bellek düzeyinde bir yalıtım olsa bile birbirini etkileyebilirler.

Bu sınır yalnız dosyalarla sınırlı değil; süreç dışında yaşayan her paylaşılan kaynak aynı sorunu taşır. Bir ağ soketi, bir kilit dosyası, işletim sisteminin kendisinde tuttuğu bir ortam değişkeni ya da paylaşılan bellek bölgesi — bunların hiçbiri fork’un kopyaladığı bellek alanının içinde değildir, dolayısıyla hiçbiri çatallanmayla otomatik olarak yalıtılmaz. Bir test paketi yazarken bu ayrımı bilmek pratik bir sonuç doğurur: testleri ayrı süreçlerde çalıştırmak, testlerin birbirinden bağımsız olduğunu garanti etmez — yalnız bellek düzeyindeki bağımsızlığı garanti eder. Süreç dışı bir kaynağa dokunan testler için ek bir önlem gerekir: ya kaynağın kendisi her test öncesi sıfırlanır, ya da testler kaynağı hiç paylaşmayacak şekilde tasarlanır — örneğin her test kendi geçici dosya adını üretir.

Dosya sistemi sınırının bir başka yüzü, testin temizlenip temizlenmediğidir. iz.dat dosyası bu koşumdan sonra diskte kalmaya devam eder; bir sonraki koşumda test_dosyaya_yaz zaten var olan bir dosyanın üzerine yazar, bu örnekte sonucu değiştirmez çünkü işlev dosyayı her seferinde yeniden açıp yazıyor, ama farklı bir test aynı adı başka bir amaçla kullansaydı eski koşumdan kalan içerik yeni testin ilk okuduğu değer olurdu. Bellekte bu sorun yoktur, çünkü her koşum kendi sürecini sıfırdan başlatır ve önceki koşumun belleği işletim sistemi tarafından geri alınır; dosya sistemi ise süreç sona erdikten sonra da değişmeden kalmaya devam eder. Bu yüzden dosya kullanan testlerin sözleşmesi bellek kullanan testlerden bir adım daha geniştir: yalnız testler arasında değil, koşumlar arasında da temiz bir başlangıç noktası garanti edilmelidir — düzenek burada da devreye girebilir, temizlik adımına dosyayı silmek eklenerek.

Bu genişletilmiş düzenek, duzenek_sok’un yalnız ortak_durum‘u sıfırlamakla kalmayıp aynı zamanda remove("iz.dat") çağırmasıyla kurulabilirdi; burada bu adım eklenmedi, çünkü amaç dosya sisteminin fork’un dışında kaldığını göstermekti, dosya temizliğinin nasıl yapılacağını değil. Genel kural şudur: bir düzenek yalnız kendi kurduğu kaynağı söker — duzenek_kur ortak_durum’a dokunuyorsa duzenek_sok da yalnız onu geri alır; bir dosya oluşturan başka bir düzenek olsaydı, onun temizliği de kendi dosyasını silmekle sınırlı kalırdı. Kurulumun dokunmadığı bir kaynağı temizlik aşamasında değiştirmek, düzeneğin sözleşmesini genişletir ve hangi testin hangi kaynaktan sorumlu olduğunu izlemeyi zorlaştırır.

Özet

  • Bir test çerçevesi işlev işaretçisi tablosu, onaylama makrosu ve geçti/kaldı sayımından oluşur; her girdi tabloda bir satırdır.
  • Her testi ayrı bir çocukta çatallayarak koşturmak, testin bellekte bıraktığı değişikliği sonraki teste taşımaz — bu dersin ölçtüğü sınıf kopya.
  • Aynı testler yalıtımsız, tek süreçte koştuğunda birincinin değişikliği ikinciye sızar ve koşum sırası sonucu değiştirebilir.
  • Bir bağımlılığı işlev işaretçisiyle bir sahte nesneye çevirmek, çağrı sayısını gerçek bağımlılığı değiştirmeden ölçmeyi sağlar; bir güdük ise çağrı saymadan yalnız sabit bir cevap döndürerek testin girdisini hazırlar — ikisi farklı amaçlara hizmet eder.
  • Bir düzenek, kurulum ve temizliği testin kendisinden ayırır ve temizliğin testin sonucundan bağımsız olarak her zaman çalışmasını garanti eder; ebeveyn süreçte çalışır, testin çocuğuna taşınmaz.
  • Süreç yalıtımı belleği keser, dosya sistemini kesmez; aynı dosyaya dokunan iki test çatallama ile birbirinden ayrılmaz. Bu sınır dosyalarla kalmaz — soket, kilit ve ortam değişkeni gibi süreç dışı her kaynak aynı şekilde yalıtım dışıdır.

Sonraki Adım

Bir test kaldığında, neyin hangi adımda beklenenden saptığını görmek gerekir. Bir sonraki ders, çalışan bir programın belleğindeki ara değerleri, programı durdurup adım adım ilerleterek nasıl görünür kıldığını ele alıyor.

İ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