Come scrivere un test per coprire un bugfix di un segfault

7

C'è stato un bug che ho risolto di recente causando un errore di segmentazione (a causa di un accesso fuori dai limiti). Il manutentore del progetto mi sta chiedendo di scrivere un test unitario per coprire il bugfix.

Come posso fare questo? Il bug mostrava un comportamento indefinito, quindi anche se il mio test è passato, non è la prova di nulla. Dovrei usare un ciclo per testarlo 100 volte per rendere il test più probabile per ottenere una regressione?

    
posta Garrett 14.06.2015 - 06:16
fonte

4 risposte

11

L'approccio più pragmatico è IMHO non attenendosi ciecamente al modello di "non cambiare il codice del progetto in nessuna circostanza". Quindi il mio suggerimento è: rendere il codice testabile per primo - cambia leggermente il codice del progetto per assicurarti che l'accesso fuori dai limiti conduca a un errore riproducibile e catchable. Si noti che rendere il codice testabile è molto spesso un prerequisito per qualsiasi tipo di automazione del test.

Quella modifica potrebbe essere una sorta di asserzione fuori limite, usando il tipo di segnalazione di errore che la tua squadra normalmente usa all'interno del tuo progetto (un codice di errore, un'eccezione, un segnale o qualcosa che preferisci), ma dovrebbe consentire al test automatico di rilevare la condizione di errore. Nota inoltre che questo cambiamento non è lo stesso "fixing the bug" - il bugfix dovrebbe probabilmente portare al codice che non crea più quella condizione out-of-bounds.

Quindi dovresti applicare tale modifica al codice originale prima la correzione del bug è stata applicata (se sei fortunato, il tuo sistema VCS ti aiuterà a ripristinare l'ordine in cui vengono applicate le modifiche). Finalmente puoi fare esattamente quello che ha scritto Robert Harvey - scrivi un test che fa apparire l'errore, ed esegui il test dopo hai reso il codice testabile, una volta prima e una volta dopo il bugfix.

    
risposta data 14.06.2015 - 07:30
fonte
4

(1)

Esiste un concetto di "test della morte" , in cui un difetto di il codice causerebbe la chiusura del processo.

Per eseguire tale test di morte, il cablaggio di test deve avviare un processo separato che eseguirà il pezzo di codice. La condizione di terminazione viene quindi presa come criterio di superamento o fallimento.

Ovviamente, non tutte le unità di testing framework supportano questo tipo di test.

(2)

Alcuni compilatori C ++ forniscono opzioni che inseriranno ulteriori controlli nel codice generato.

Esempi di ciò che faranno queste opzioni:

  1. Assicurati che quando la chiamata alla funzione ritorna, il suo indirizzo di ritorno abbia lo stesso valore (in pila) come quando è stata appena inserita la chiamata alla funzione.
  2. Assicurarsi che i buffer di memoria allocati dinamicamente non vengano sovrascritti prima dell'inizio o dopo la sua fine.

(3)

Infine ci sono anche strumenti di copertura del codice e strumentazione come valgrind.

Se esistono alcuni modelli di dati che causeranno un arresto anomalo in modo affidabile (ma solo che non si conosce quale), la fuzzing può essere una strategia molto efficace per scoprire tali schemi di dati che causano arresti anomali. Per esempio. afl-fuzz .

Se utilizzare strumenti aggiuntivi è fuori questione, forse puoi modificare il codice tu stesso per renderlo più "testabile". Un esempio è la sostituzione degli accessi agli array con un sovraccarico dell'operatore C ++ che esegue il controllo dei limiti dell'indice dell'array. I dettagli dipendono dal tipo di codice interrogato.

    
risposta data 14.06.2015 - 07:10
fonte
2

Scrivi un test che riproduca in modo affidabile il segfault nel codice originale per primo. Dovresti essere in grado di duplicare il segfault scrivendo un test che causa l'accesso non vincolato che hai corretto. Puoi utilizzare lo stesso test per dimostrare che l'accesso fuori del campo non si verifica più nel nuovo codice.

    
risposta data 14.06.2015 - 06:40
fonte
1

Se la tua lingua supporta i controlli sui limiti, attivali per il test. Questo è certamente il modo più semplice e sicuro per farlo. Come qualcuno ha suggerito nei commenti, perché non dichiarare @cython.boundscheck(isDebug) dove isDebug è definito all'inizio del modulo (non ho mai lavorato con cython, quindi non sono sicuro che sia così facile)

Detto questo, ci sono casi in cui non puoi semplicemente attivare i controlli dei limiti (ad esempio: stai usando una libreria di basso livello come LAPACK o IPP che prende solo un buffer e si aspetta che sia della giusta dimensione). L'unico modo che conosco per garantire le violazioni di accesso qui è quello di utilizzare funzioni di sistema operativo di basso livello per allocare la memoria in modo tale che l'indirizzo immediatamente prima (o dopo) il blocco assegnato sia letto e protetto da scrittura. Su Windows, VirtualAlloc , VirtualProtect e amici ti permettono di fare cose del genere. Spreca molta memoria e puoi proteggere lo spazio solo prima di o dopo il blocco (a meno che la dimensione del buffer non sia un multiplo di una dimensione della pagina di memoria). Ma per trovare errori di accesso alla memoria fuori dai limiti difficili da riprodurre, a volte vale la pena.

    
risposta data 14.06.2015 - 12:28
fonte

Leggi altre domande sui tag