Collaudo e Correzione di Errori C. Horstmann Fondamenti di programmazione e Java 2 3^ edizione Apogeo trad. Nicola Fanizzi corso di Programmazione, CdS: Informatica TPS Dip. di Informatica, Università degli studi di Bari Obiettivi Imparare ad eseguire il collaudo di unità Capire i principi della selezione e valutazione dei casi di test Imparare ad usare il logging Prendere familiarità con l'udo di un debugger Imparare le strategie per un debugging efficace 2 Collaudo di Unità 3 Unico e più importante strumento per il collaudo Controlla un singolo metodo o un insieme di metodi cooperanti Non si testa il programma completo che si sta sviluppando, bensì le singole classi isolate Per ogni test, si fornisce una semplice classe detta test harness La classe test harness procura i parametri ai metodi da collaudare Esempio: 4 Impostare Classi Test Harness Usiamo un comune algoritmo per il calcolo della radice quadrata: Dare un valore x che potrebbe essere abbastanza vicino alla radice quadrata desiderata (x = a va bene) La vera radice sta tra x e a/x Considerare il punto medio (x + a/x) / 2 come migliore approssimazione Ripetere la procedura. Fermarsi quando due approssimazioni successive sono tra loro molto vicine Esempio: 5 Impostare Classi Test Harness Il metodo converge rapidamente Radice quadrata di 100: Guess #1: 50.5 Guess #2: 26.24009900990099 Guess #3: 15.025530119986813 Guess #4: 10.840434673026925 Guess #5: 10.032578510960604 Guess #6: 10.000052895642693 Guess #7: 10.000000000139897 Guess #8: 10.0 RootApproximator.java 6 01: /** 02: Calcola approssimazioni della radice quadrata 03: di un numero usando l'algoritmo di Erone. 04: */ 05: public class RootApproximator 06: { 07: /** 08: Costruisce l'approssimazione della radice per un dato numero. 09: @param aNumber numero per il quale estrarre la radice quadrata 10: (Precondizione: aNumber >= 0) 11: */ 12: public RootApproximator(double aNumber) 13: { 14: a = aNumber; 15: xold = 1; 16: xnew = a; 17: } RootApproximator.java 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 7 /** Calcola una approssimazione migliore della corrente. @return the next guess */ public double nextGuess() { xold = xnew; if (xold != 0) xnew = (xold + a / xold) / 2; return xnew; } RootApproximator.java 8 31: /** 32: Calcola la radice migliorando l'approssimazione 33: finchè due successivi risultati risultano approssimativamente uguali. 34: @return il valore della radice quadrata calcolato 35: */ 36: public double getRoot() 37: { 38: assert a >= 0; 39: while (!Numeric.approxEqual(xnew, xold)) 40: nextGuess(); 41: return xnew; 42: } 43: 44: private double a; // il numero di cui si deve calcolare la radice 45: private double xnew; // approssimazione corrente 46: private double xold; // approssimazione precedente 47: } Numeric.java 01: 02: 03: 04: 05: 06: 07: 08: 09: 10: 11: 12: 13: 14: 15: 16: 17: 18: 9 /** A class for useful numeric methods. */ public class Numeric { /** Tests whether two floating-point numbers are. equal, except for a roundoff error @param x a floating-point number @param y a floating-point number @return true if x and y are approximately equal */ public static boolean approxEqual(double x, double y) { final double EPSILON = 1E-12; return Math.abs(x - y) <= EPSILON; } } RootApproximatorTester 10 .java 01: import java.util.Scanner; 02: 03: /** 04: This program prints ten approximations for a square root. 05: */ 06: public class RootApproximatorTester 07: { 08: public static void main(String[] args) 09: { 10: System.out.print("Enter a number: "); 11: Scanner in = new Scanner(System.in); 12: double x = in.nextDouble(); 13: RootApproximator r = new RootApproximator(x); 14: final int MAX_TRIES = 10; RootApproximatorTester 11 .java 15: 16: 17: 18: 19: 20: 21: 22: } } for (int tries = 1; tries <= MAX_TRIES; tries++) { double y = r.nextGuess(); System.out.println("Guess #" + tries + ": " + y); } System.out.println("Square root: " + r.getRoot()); Collaudare il Programma Output Enter a number: 100 Guess #1: 50.5 Guess #2: 26.24009900990099 Guess #3: 15.025530119986813 Guess #4: 10.840434673026925 Guess #5: 10.032578510960604 Guess #6: 10.000052895642693 Guess #7: 10.000000000139897 Guess #8: 10.0 Guess #9: 10.0 Guess #10: 10.0 Square root: 10.0 12 Collaudare il Programma 13 La classe RootApproximator funziona correttamente per tutti gli input ? Serve collaudarla su tanti altri valori Continuare a ri-collaudare con altri valori non è una buona idea; i collaudi non sono ripetibili Se un problema viene corretto e si deve collaudare di nuovo, si dovrebbero ricordare tutti gli input precedenti Soluzione: scrivere test harness che facilitano la ripetizione del collaudo delle unità Fornire l'Input per il Collaudo 14 Ci sono vari meccanismi per fornire casi di test Un meccanismo è quello di cablare (scrivere direttamente nel codice) gli inputs di test nel test harness Si esegue semplicemente il test harness ogni volta che si corregge un bug nella classe collaudata Alternativa: si tengono gli input di test su file appositi RootApproximatorHarness1 15 .java 01: /** 02: Programma che calcola le radici quadrate di valori di input selezionati 03: */ 04: public class RootApproximatorHarness1 05: { 06: public static void main(String[] args) 07: { 08: double[] testInputs = { 100, 4, 2, 1, 0.25, 0.01 }; 09: for (double x : testInputs) 10: { 11: RootApproximator r = new RootApproximator(x); 12: double y = r.getRoot(); 13: System.out.println("square root of " + x 14: + " = " + y); 15: } 16: } 17: } RootApproximatorHarness1 16 .java Output square square square square square square root root root root root root of of of of of of 100.0 = 10.0 4.0 = 2.0 2.0 = 1.414213562373095 1.0 = 1.0 0.25 = 0.5 0.01 = 0.1 Fornire l'Input per il Collaudo 17 Si possono anche generare casi di test in maniera automatica Se sono possibili pochi input allora basta far girare il test per intero ciclando su un numero (rappresentativo) di casi RootApproximatorHarness2 18 .java 01: 02: 03: 04: 05: 06: 07: 08: 09: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: /** Programma che calcola le radici quadrate di valori in input forniti attraverso un ciclo. */ public class RootApproximatorHarness2 { public static void main(String[] args) { final double MIN = 1; final double MAX = 10; final double INCREMENT = 0.5; for (double x = MIN; x <= MAX; x = x + INCREMENT) { RootApproximator r = new RootApproximator(x); double y = r.getRoot(); System.out.println("square root of " + x + " = " + y); } } } RootApproximatorHarness2 19 .java Output square square square . . . square square square root of 1.0 = 1.0 root of 1.5 = 1.224744871391589 root of 2.0 = 1.414213562373095 root of 9.0 = 3.0 root of 9.5 = 3.0822070014844885 root of 10.0 = 3.162277660168379 Fornire l'Input per il Collaudo 20 Test precedenti hanno ristreto il campo ad un limitato sotto-insieme di valori Alternativa: generazione causale di casi di test RootApproximatorHarness3 21 .java 01: import java.util.Random; 02: 03: /** 04: Il programma calcola la radice quadrata di numeri casuali 05: */ 06: public class RootApproximatorHarness3 07: { 08: public static void main(String[] args) 09: { 10: final double SAMPLES = 100; 11: Random generator = new Random(); 12: for (int i = 1; i <= SAMPLES; i++) 13: { 14: // Genera un valore casuale di test 15: RootApproximatorHarness3 22 .java 16: 17: 18: 19: 20: 21: 22: 23: } } } double x = 1000 * generator.nextDouble(); RootApproximator r = new RootApproximator(x); double y = r.getRoot(); System.out.println("square root of " + x + " = " + y); RootApproximatorHarness3 23 .java Output square square square square square square . . . root root root root root root of of of of of of 810.4079626570873 = 28.467665212607223 480.50291114306344 = 21.9203766195534 643.5463246844379 = 25.36821485017103 506.5708496713842 = 22.507128863348704 539.6401504334708 = 23.230156057019308 795.0220214851004 = 28.196134867834285 Fornire l'Input per il Collaudo Selezionare buoni casi di test è un'abilità nel debuggare i programmi Testare tutte le caratteristiche dei metodi che si stanno collaudando Testare i casi tipici 100, 1/4, 0.01, 2, 10E12, per SquareRootApproximator Testare i casi limite: casi che sono ai confini del dominio di input 0 per SquareRootApproximator 24 Fornire l'Input per il Collaudo 25 I programmatori spesso fanno errori trattando condizioni limite: divisione per zero, estrarre caratteri da stringhe vuote, e accedere riferimenti nulli Racogliere casi di test negativi: gli input che ci si aspetta che il programma rigetti Esempio: la radice di -2. Il test è superato se l'harness termina con l'asserzione del fallimento (se il controllo delle asserzioni è abilitato) Lettura di Input di test da File 26 Più elegante mettere i valori di test in un file Ridirezione dell'Input: java Program < data.txt Alcune IDE non supportano la ridirezione dell'input. Allora si usa la shell (console con riga di comando) Ridirezione dell'output: java Program > output.txt RootApproximatorHarness4 27 .java 01: 02: 03: 04: 05: 06: 07: 08: 09: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: import java.util.Scanner; /** Il programma calcola la radice quadrata di input forniti attraverso System.in. */ public class RootApproximatorHarness4 { public static void main(String[] args) { Scanner in = new Scanner(System.in); boolean done = false; while (in.hasNextDouble()) { double x = in.nextDouble(); RootApproximator r = new RootApproximator(x); double y = r.getRoot(); } } } System.out.println("square root of " + x + " = " + y); Lettura di Input di test da File 28 File test.in: 1 2 3 4 5 6 100 4 2 1 0.25 0.01 facendo girare il programma: java RootApproximatorHarness4 < test.in > test.out Lettura di Input di test da File File test.out: 1 2 3 4 5 6 square square square square square square root root root root root root of of of of of of 100.0 = 10.0 4.0 = 2.0 2.0 = 1.414213562373095 1.0 = 1.0 0.25 = 0.5 0.01 = 0.1 29 Valutazione dei Casi di Test Come saper se il l'output è corretto ? Calcola re valori corretti a mano es. per un programma di pagamenti, calcolare le tasse manualmente Fornire input di test per i quali si sa la risposta es. la radice di 4 è 2 e la radice di 100 è 10 30 Valutazione dei Casi di Test 31 Verificare che i valori di output soddisfi cete proprietà es. radice quadrata elevata al quadrato = valore originario Usare un Oracolo: un metodo lento ma affidabile per calcolare un risulktato per esigenze di collaudo es. usare Math.pow per calcolare più lentamernte x1/2 (equivalente alla radice quadrata di x) RootApproximatorHarness5 32 .java 01: 02: 03: 04: 05: 06: 07: 08: 09: 10: 11: 12: 13: 14: 15: 16: import java.util.Random; /** Il programma verifica il calcolo dei valori della radice quadrata controllando una proprietà delle radici quad. */ public class RootApproximatorHarness5 { public static void main(String[] args) { final double SAMPLES = 100; int passcount = 0; int failcount = 0; Random generator = new Random(); for (int i = 1; i <= SAMPLES; i++) { RootApproximatorHarness5 33 .java 17: // Generare valori di test casuali 18: 19: double x = 1000 * generator.nextDouble(); 20: RootApproximator r = new RootApproximator(x); 21: double y = r.getRoot(); 22: 23: // controlla che il valore di test soddisfi la proprietà del quadrato 24: 25: if (Numeric.approxEqual(y * y, x)) 26: { 27: System.out.print("Test passed: "); 28: passcount++; 29: } 30: else 31: { 32: System.out.print("Test failed: "); 33: failcount++; 34: } RootApproximatorHarness5 34 .java 35: 36: 37: 38: 39: 40: 41: 42: } System.out.println("x = " + x + ", root squared = " + y * y); } } System.out.println("Pass: " + passcount); System.out.println("Fail: " + failcount); RootApproximatorHarness5 35 .java Output Test passed: Test passed: Test passed: Test passed: Test passed: . . . Pass: 100 Fail: 0 x x x x x = = = = = 913.6505141736327, root squared = 913.6505141736328 810.4959723987972, root squared = 810.4959723987972 503.84630929985883, root squared = 503.8463092998589 115.4885096006315, root squared = 115.48850960063153 384.973238438713, root squared = 384.973238438713 RootApproximatorHarness6 36 .java 01: 02: 03: 04: 05: 06: 07: 08: 09: 10: 11: 12: 13: 14: 15: 16: 17: 18: import java.util.Random; /** Il programma verifica il calcolo dei valori della radice quadrata mediante un oracolo. */ public class RootApproximatorHarness6 { public static void main(String[] args) { final double SAMPLES = 100; int passcount = 0; int failcount = 0; Random generator = new Random(); for (int i = 1; i <= SAMPLES; i++) { // Generare valori di test casuali RootApproximatorHarness6 37 .java 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: double x = 1000 * generator.nextDouble(); RootApproximator r = new RootApproximator(x); double y = r.getRoot(); double oracleValue = Math.pow(x, 0.5); // // if { controllare che il valore di test eguagli approssimativamente quello dell'oracolo (Numeric.approxEqual(y, oracleValue)) System.out.print("Test passed: "); passcount++; } else { System.out.print("Test failed: "); failcount++; } RootApproximatorHarness6 38 .java 37: 38: 39: 40: 41: 42: 43: } System.out.println("square root = " + y + ", oracle = " + oracleValue); } } System.out.println("Pass: " + passcount); System.out.println("Fail: " + failcount); RootApproximatorHarness6 39 .java Output Test passed: Test passed: Test passed: Test passed: Test passed: . . . Pass: 100 Fail: 0 square square square square square root root root root root = = = = = 718.3849112194539, oracle = 718.3849112194538 641.2739466673618, oracle = 641.2739466673619 896.3559528159169, oracle = 896.3559528159169 591.4264541724909, oracle = 591.4264541724909 721.029957736384, oracle = 721.029957736384 Regression Testing 40 Conservare i casi di test Usare i test memorizzati in versioni successive Una suite di test è un insieme di test per il collaudo ripetuto Ciclico = bug che è corretto ma riappare in successive versioni Regression testing: ripetizione di test precedenti per assicurare che punti deboli noti in versioni precedenti non appaiano in nuove versioni Copertura del Test 41 Test a scatola nera: funzionalità di collaudo che non prende in considerazione la struttura interna dell'implementazione. Test a scatola bianca: prendere in considerazione la struttura intern nel progettare i test Copertura: misura di quante parti di un programma siano state collaudate Assicurarsi che ogni parte del programma sia interessata al meno una volta da un caso di test es. assicurarsi di eseguire ogni diramazione in almeno un caso di test Copertura del Test 42 Suggerimento: scrivere casi di test prima di finire di scrivere il programma → dà l'idea di quello che dovrebbe fare I programmi moderni possono essere ardui da testare Interfacce grafiche (uso del mouse) Connessioni di rete (ritardi e cadute) Ci sono strumenti pr l'automazione del test in tali situazioni Principi di base del regression testing della copertura completa sono ancora validi Colludare Unità con JUnit 43 http://junit.org Inglobato in IDE come BlueJ e Eclipse Filosofia: ogni volta che si implementa una classe si implementi anche una classe di test Colludare Unità con JUnit 44 Trace di un Programma Messaggi che mostrano il flusso dell'esecuzione if (status == SINGLE) { System.out.println("status is SINGLE"); . . . } . . . 45 Trace di un Programma 46 Svantaggio: serve rimuoverli a collaudo terminato, rimettere quando si trova un altro errore Soluzione: usare la classe Logger disabilitare i messaggi di trace senza rimuoverli dal programma Logging 47 I messaggi di log possono essere disattivati quando il collaudo è completato Si usa l'ggetto globale Logger.global Registrare (log) un messaggio: Logger.global.info("status is SINGLE"); Logging Per default, i messaggi registrati sono stampati. Li si disabilita con: Logger.global.setLevel(Level.OFF); Il logging può diventare assillante (meglio nè troppo nè troppo poco) Alcuni programmatori al logging preferiscono il debugging (prossima sezione) 48 Logging 49 Quando si traccia il flusso d'esecuzione, gli eventi più importanti sono l'entrata e l'uscita dai metodi All'inizio di un metodo, stampa dei parametri: public TaxReturn(double anIncome, int aStatus) { Logger.global.info("Parameters: anIncome = " + anIncome + " aStatus = " + aStatus); . . . } Logging 50 Alla fine di un metodo, si stampa il valore di ritorno: public double getTax() { . . . Logger.global.info("Return value = " + tax); return tax; } Usare un Debugger Debugger = applicazione che fa girare un 51 programma e ne analizza il comportamento in fase d'esecuzione Un debugger permette di fermare e far ripartire il proprio programma, osservare il contenuto delle variabili ed eseguirlo passo passo Più grandi sono i programmi, più sono difficili da debuggare semplicemente mediante logging Usare un Debugger 52 I debugger possono essere una parte della propria IDE (Eclipse, BlueJ) o programmi separati (JSwat) Tre concetti-chiave: Breakpoint Esecuzione istruzione per istruzione Osservazione delle variabili Debugger Fermo 53 ad un Breakpoint Figure 3: Stopping at a Breakpoint Controllare le Variabili Figure 4: Inspecting Variables 54 Debugging 55 L'esecuzione è sospesa ogni volta che si raggiunge breakpoint In un debugger, un programma gira a piena velocità fino a quando non si raggiunge un breakpoint Quando l'esecuzione si interrompe, si può: Controllare il contenuto delle variabili Far eseguire il programma una riga alla volta oppure, continuare a farlo girare a piena velocità fino al prossimo breakpoint Debugging 56 Quando il programma termina, anche il debugger si ferma I breakpoint rimangono attivi fino alla loro rimozione Due comandi per l'esecuzione di un singolo passo: Step Over: salta le chiamate dei metodi Step Into: entra nei metodi chiamati Single-Step Example 57 Riga corrente: String input = in.next(); Word w = new Word(input); int syllables = w.countSyllables(); System.out.println("Syllables in " + input + ": " + syllables); Quando si saltano le chiamate di metodi, si passa alla riga successiva: String input = in.next(); Word w = new Word(input); int syllables = w.countSyllables(); System.out.println("Syllables in " + input + ": " + syllables); Esempio di Single-Step 58 Invece, entrando nelle chiamate, si passa alla prima riga di codice del metodo countSyllables: public { int int . . } int countSyllables() count = 0; end = text.length() - 1; . Sessione d'Esempio 59 di Debugging Word: classe oper contare le silabe i una parola Ogni grupo di vocali adiacenti (a, e, i, o, u, y) conta come una sillaba Però, una e a fine parola non conta come sillaba Se l'algoritmo restituisce 0, si incrementi ad 1 Il costruttore rimuove i caratteri non-lettera all'inizio ed alla fine Word.java 01: 02: 03: 04: 05: 06: 07: 08: 09: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 60 /** Questa classe descrive una parola in un documento. */ public class Word { /** Construisce una parola togliendo i caratteri non alfabetici ad inizio e fine, come la punteggiatura @param s la stringa in input */ public Word(String s) { int i = 0; while (i < s.length() && !Character.isLetter(s.charAt(i))) i++; int j = s.length() - 1; while (j > i && !Character.isLetter(s.charAt(j))) j--; text = s.substring(i, j); } Word.java 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: /** Restituisce il testo della parola rimossi i caratteri non alfabetici iniziali e finali. @return il testo della parola */ public String getText() { return text; } /** Counta le sillabe nella parola. @return il conto delle sillabe */ 61 Word.java 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 62 public int countSyllables() { int count = 0; int end = text.length() - 1; if (end < 0) return 0; // la stringa vuota non ha sillabe // Una e alla fine della parola non conta come vocale char ch = Character.toLowerCase(text.charAt(end)); if (ch == 'e') end--; boolean insideVowelGroup = false; for (int i = 0; i <= end; i++) { ch = Character.toLowerCase(text.charAt(i)); String vowels = "aeiouy"; if (vowels.indexOf(ch) >= 0) { Word.java 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: } } } // ch is a vowel if (!insideVowelGroup) { // Inizio di un nuovo gruppo di vocali count++; insideVowelGroup = true; } // ogni parola ha minimo una sillaba if (count == 0) count = 1; } return count; private String text; 63 WordTester.java 64 01: import java.util.Scanner; 02: 03: /** 04: Questo programma testa il metodo countSyllables della classe Word 05: */ 06: public class WordTester 07: { 08: public static void main(String[] args) 09: { 10: Scanner in = new Scanner(System.in); 11: 12: System.out.println("Enter a sentence ending in a // period."); 13: 14: String input; 15: do 16: { WordTester.java 17: 18: 19: 20: 21: 22: 23: 24: 25: } 65 input = in.next(); Word w = new Word(input); int syllables = w.countSyllables(); System.out.println("Syllables in " + input + ": " + syllables); } } while (!input.endsWith(".")); Debuggare il Programma output non corretto (buggy) (per l'input "hello yellow peach"): Syllables Syllables Syllables in in in hello: 1 yellow: 1 peach: 1 Porre un breakpoint sulla prima riga di countSyllables della classe Word Far partire il programma, fornire l'input. Il programma si ferma al breakpoint Il metodo controlla se la lettera finale è una 'e' 66 Debuggare il Programma Figure 5: Debugging the CountSyllables Method 67 Debuggare il Programma Controllare se funziona: 68 eseguire fino alla riga dove si fa il controllo ed osservare la variabile ch Dovrebbe contenere la lettera finale ma contiene una 'l' Trovare Altri Problemi Figure 6: The Current Values of the Local and Instance Variables 69 Trovare Altri Problemi end viene posta a 3, non a 4 70 text contiene "hell", non "hello" No c'è da sorprendersi se countSyllables restituisce 1 Il colpevole è altrove Non riesce a tornare in tempo Far partire di nuovo ponendo un breakpoint nel costruttore di Word Debuggare 71 il Costruttore di Word Fornire l'input "hello" di nuovo Fermare subito dopo la fine del secondo loop nel costruttore Controllare le variabili i e j I valori sono 0 e 4 giusto dato che l'input consiste di lettere Perchè il testo viene impostato ad "hell"? Debuggare 72 il Costruttore di Word Errore di fuori-di-uno: il secondo parametro della sotto-stringa è la prima posizione da non includere text = substring(i, j); dovrebbe essere text = substring(i, j+1); Debuggare 73 il Costruttore di Word Figure 7: Debugging the Word Constructor Un Altro Errore Correggere l'errore Ricompilare Testare di nuovo: Syllables in hello: 1 Syllables in yellow: 1 Syllables in peach: 1 Oh no, non va ancora bene 74 Un Altro Errore Far partire il debugger Cancellare i vecchi breakpoint e impostare un breakpoint sul metodo countSyllables Fornire l'input "hello" 75 Debuggare (ancora) 76 CountSyllables Fermare all'inizio di countSyllables. Quindi, eseguire il ciclo passo passo: boolean insideVowelGroup = false; for (int i = 0; i <= end; i++) { ch = Character.toLowerCase(text.charAt(i)); if ("aeiouy".indexOf(ch) >= 0) { // ch è una vocale if (!insideVowelGroup) { // Inizio di un nuovo gruppo di vocali count++; insideVowelGroup = true; } } } Debuggare (ancora) 77 CountSyllables Prima iterata ('h'): salta il test delle vocali Seconda iterata ('e'): passa il test, incrementa count Terza iterata ('l'): salta il test ... Quinta iterata ('o'): passa il test, ma se la seconda viene saltata, count non viene incrementata Correggere il Bug 78 insideVowelGroup non viene più resettata a false Modifica: if ("aeiouy".indexOf(ch) >= 0) { . . . } else insideVowelGroup = false; Correggere il Bug 79 Ri-testare: tutti i casi di test superati Syllables in hello: 2 Syllables in yellow: 2 Syllables in peach.: 1 Adesso il programma è privo di errori ? Il debugger non può rispondere a questa domanda Primo Bug Figure 8: The First Bug 80