Pengujian kebocoran

Menangkap kebocoran memori dengan std.testing.allocator sebelum kode dipakai di luar pemeriksaan.

Daftar isi

Kontrak kepemilikan pada topik sebelumnya menetapkan siapa yang wajib membebaskan memori, tetapi kontrak yang tertulis belum tentu dipenuhi. Satu jalur return awal yang terlewat cukup membuat sebuah blok kehilangan pemiliknya, dan program tetap berjalan normal tanpa satu pun gejala. Topik ini membahas alat pengawasnya, yaitu std.testing.allocator, sebuah allocator pemeriksa yang mencatat setiap alokasi selama test berjalan dan melaporkan blok yang tidak pernah dibebaskan.

Konsep#

std.testing.allocator adalah allocator yang disediakan khusus untuk blok test. Cara kerjanya berbasis pembukuan, yaitu setiap panggilan alloc dicatat dan setiap panggilan free dicocokkan, lalu pada akhir test sisa catatan yang belum dibebaskan dilaporkan sebagai kebocoran. Allocator biasa seperti page_allocator tidak melakukan pembukuan apa pun, sehingga blok yang lupa dibebaskan tidak meninggalkan jejak yang terlihat.

Pemeriksaan ini hanya aktif pada lingkungan pengujian, dan justru di situlah tempatnya. Kode produksi memakai allocator mana pun yang diberikan pemanggil, sedangkan blok test memasok std.testing.allocator agar setiap pembebasan yang terlewat tertangkap sebelum kode dipakai lebih jauh. Dengan demikian, fungsi yang diperiksa tidak perlu diubah sedikit pun, karena pemeriksaan terjadi pada allocator yang dipilih test, bukan pada isi fungsinya.

Pengawasan serupa hadir di luar blok test dalam bentuk yang lebih ringan. Alokator serbaguna yang dipasok std.process.Init pada mode Debug menyiapkan pemeriksaan kebocoran bila memungkinkan, sehingga program yang dijalankan lewat zig run dengan alokasi lewat init.gpa ikut melaporkan kebocorannya saat keluar. Meski demikian, blok test tetap menjadi tempat yang paling terkontrol, karena laporannya merujuk test yang diberi nama dan tidak bercampur dengan perilaku program utama.

Contoh Lengkap#

Contoh pertama memperlihatkan keadaan sehat, yaitu test logika biasa berdampingan dengan test yang mengalokasikan dan membebaskan memori. Berkas uji-rekap.zig memuat fungsi penghitung nilai lulus, dua test logikanya, dan satu test yang mengalokasikan buffer melalui std.testing.allocator.

zig uji-rekap.zig
const std = @import("std");

fn jumlah_lulus(nilai: []const u8) usize {
    var jumlah: usize = 0;
    for (nilai) |n| {
        if (n >= 75) {
            jumlah += 1;
        }
    }
    return jumlah;
}

pub fn main() void {
    const sampel = [_]u8{ 60, 85, 74, 90, 78, 95, 69 };
    std.debug.print("dari {d} nilai, {d} siswa lulus\n", .{ sampel.len, jumlah_lulus(&sampel) });
}

test "semua nilai di bawah ambang" {
    try std.testing.expectEqual(@as(usize, 0), jumlah_lulus(&.{ 60, 70, 74 }));
}

test "sebagian nilai lulus" {
    try std.testing.expectEqual(@as(usize, 4), jumlah_lulus(&.{ 60, 85, 74, 90, 78, 95, 69 }));
}

test "alokasi dibebaskan" {
    const alloc = std.testing.allocator;
    const buffer = try alloc.alloc(u8, 32);
    defer alloc.free(buffer);
    try std.testing.expectEqual(@as(usize, 32), buffer.len);
}

Test ketiga adalah bentuk baku pemeriksaan kepemilikan, yaitu mengalokasikan memori, memakainya, lalu membebaskannya dengan defer. Pada akhir test, pembukuan std.testing.allocator kosong karena buffer sudah dikembalikan, sehingga tidak ada laporan apa pun. Jalankan pemeriksaannya dengan zig test.

bash
zig test uji-rekap.zig
text Keluaran
1/3 uji-rekap.test.semua nilai di bawah ambang...OK
2/3 uji-rekap.test.sebagian nilai lulus...OK
3/3 uji-rekap.test.alokasi dibebaskan...OK
All 3 tests passed.

Contoh kedua memperlihatkan kebalikannya, yaitu memori yang dialokasikan tetapi tidak pernah dibebaskan. Berkas deteksi-bocor.zig memuat fungsi salin_laporan yang mengembalikan salinan teks, beserta test yang memanggilnya tanpa membebaskan hasilnya. Baris pembebasan sengaja ditiadakan agar pemeriksa bekerja.

zig deteksi-bocor.zig
// tidak-dijalankan: berkas pengujian dieksekusi lewat perintah zig test
const std = @import("std");

fn salin_laporan(alloc: std.mem.Allocator, sumber: []const u8) ![]u8 {
    return try alloc.dupe(u8, sumber);
}

test "salin_laporan menyalin teks laporan" {
    const alloc = std.testing.allocator;
    const laporan = try salin_laporan(alloc, "rekap penjualan semester");
    try std.testing.expectEqualStrings("rekap penjualan semester", laporan);
    // alloc.free(laporan) sengaja terlewat untuk memperlihatkan deteksi kebocoran
}
bash
# keluaran disaring karena jejak tumpukannya memuat alamat dan jalur yang berubah-ubah
zig test deteksi-bocor.zig 2>&1 | grep -E "^(1/|All|1 errors|1 tests)"
text Keluaran
1/1 deteksi-bocor.test.salin_laporan menyalin teks laporan...OK
All 1 tests passed.
1 errors were logged.
1 tests leaked memory.

Asersi expectEqualStrings lulus, tetapi pemeriksaan tetap gagal karena pembukuan tidak kosong pada akhir test, yang terlihat dari status keluar perintah yang bukan nol. Pesan [SafeAllocator] (err): leaked pada keluaran lengkapnya menyebut panjang blok yang bocor, yaitu 24 byte sesuai panjang teksnya, beserta jejak tumpukan yang menunjuk baris alloc.dupe sebagai titik alokasi. Baris 1 tests leaked memory. merangkum temuan. Alamat memori, jalur berkas, dan seed pada keluaran lengkap berbeda di setiap lingkungan, sehingga keluaran yang ditampilkan disaring dengan grep dan hanya menyisakan baris-baris yang stabil.

Laporan jejak tumpukan pada pesan kebocoran dibaca dari bawah ke atas untuk menemukan titik alokasinya. Baris terbawah menunjuk test runner, baris-baris di atasnya menyusuri pemanggil, dan baris yang menyebut berkas proyek sendiri adalah titik pemanggilan alokasi yang wajib diperiksa. Jalur pustaka standar di antaranya hanya penerus, sehingga pembacaan berhenti begitu berkas proyek muncul.

Kesalahan umum#

Kesalahan berikutnya adalah menjalankan zig run uji-rekap.zig dan menyangka pemeriksaan sudah berlangsung. Perintah zig run hanya mengeksekusi fungsi main, sehingga ketiga blok test tidak dijalankan sama sekali. Pemeriksaan kebocoran hanya terjadi melalui zig test atau melalui langkah test pada build.zig yang dibahas pada modul Pengujian.

Pembebasan dengan allocator yang berbeda dari alokasinya juga menghasilkan laporan yang menyesatkan. Blok yang diperoleh dari std.testing.allocator tetap dibukukan sebagai bocor bila dibebaskan melalui allocator lain, karena pencocokan dilakukan atas identitas allocator, bukan atas alamat memori saja. Gunakan satu nama variabel allocator per test agar pasangan alloc dan free selalu sepadan.

Ringkasan#

  • std.testing.allocator membukukan setiap alokasi selama test dan melaporkan blok yang tidak dibebaskan pada akhirnya.
  • Test yang mengalokasikan memori wajib menutupnya dengan defer atau pembebasan eksplisit agar pembukuan kosong.
  • Kebocoran dilaporkan dengan panjang blok dan jejak tumpukan titik alokasinya, sementara status keluar perintah menjadi bukan nol.
  • Pesan All tests passed. tidak menghapus laporan kebocoran, sehingga kedua hasil dibaca bersama.

Modul ini berakhir pada kebiasaan yang paling berguna, yaitu memeriksa sendiri pemenuhan kontrak kepemilikan dengan alat yang sudah tersedia di pustaka standar. Kebiasaan yang sama berlaku pada kode produksi, karena pemeriksaan kebocoran yang berjalan pada setiap test menjaga kontrak tetap terverifikasi selama proyek tumbuh.

Referensi#