#!/usr/bin/env python3 # -*- coding: utf-8 -*- # "Introduction to Debugging" - a chapter of "The Debugging Book" # Web site: https://www.debuggingbook.org/html/Intro_Debugging.html # Last change: 2025-01-16 10:35:08+01:00 # # Copyright (c) 2021-2025 CISPA Helmholtz Center for Information Security # Copyright (c) 2018-2020 Saarland University, authors, and contributors # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. r''' The Debugging Book - Introduction to Debugging This file can be _executed_ as a script, running all experiments: $ python Intro_Debugging.py or _imported_ as a package, providing classes, functions, and constants: >>> from debuggingbook.Intro_Debugging import but before you do so, _read_ it and _interact_ with it at: https://www.debuggingbook.org/html/Intro_Debugging.html In this chapter, we introduce some basics of how failures come to be as well as a general process for debugging. For more details, source, and documentation, see "The Debugging Book - Introduction to Debugging" at https://www.debuggingbook.org/html/Intro_Debugging.html ''' # Allow to use 'from . import ' when run as script (cf. PEP 366) if __name__ == '__main__' and __package__ is None: __package__ = 'debuggingbook' # Introduction to Debugging # ========================= if __name__ == '__main__': print('# Introduction to Debugging') if __name__ == '__main__': from .bookutils import YouTubeVideo, quiz YouTubeVideo("bCHRCehDOq0") ## Synopsis ## -------- if __name__ == '__main__': print('\n## Synopsis') ## A Simple Function ## ----------------- if __name__ == '__main__': print('\n## A Simple Function') ### Your Task: Remove HTML Markup if __name__ == '__main__': print('\n### Your Task: Remove HTML Markup') def remove_html_markup(s): # type: ignore tag = False out = "" for c in s: if c == '<': # start of markup tag = True elif c == '>': # end of markup tag = False elif not tag: out = out + c return out ### Understanding Python Programs if __name__ == '__main__': print('\n### Understanding Python Programs') ### Running a Function if __name__ == '__main__': print('\n### Running a Function') if __name__ == '__main__': remove_html_markup("Here's some strong argument.") ### Interacting with Notebooks if __name__ == '__main__': print('\n### Interacting with Notebooks') ### Testing a Function if __name__ == '__main__': print('\n### Testing a Function') if __name__ == '__main__': assert remove_html_markup("Here's some strong argument.") == \ "Here's some strong argument." ## Oops! A Bug! ## ------------ if __name__ == '__main__': print('\n## Oops! A Bug!') if __name__ == '__main__': remove_html_markup('') from .ExpectError import ExpectError if __name__ == '__main__': with ExpectError(): assert remove_html_markup('') == "" ## Visualizing Code ## ---------------- if __name__ == '__main__': print('\n## Visualizing Code') from graphviz import Digraph, nohtml if __name__ == '__main__': from IPython.display import display PASS = "✔" FAIL = "✘" PASS_COLOR = 'darkgreen' # '#006400' # darkgreen FAIL_COLOR = 'red4' # '#8B0000' # darkred STEP_COLOR = 'peachpuff' FONT_NAME = 'Raleway' def graph(comment: str ="default") -> Digraph: return Digraph(name='', comment=comment, graph_attr={'rankdir': 'LR'}, node_attr={'style': 'filled', 'fillcolor': STEP_COLOR, 'fontname': FONT_NAME}, edge_attr={'fontname': FONT_NAME}) if __name__ == '__main__': state_machine = graph() state_machine.node('Start', ) state_machine.edge('Start', '¬ tag') state_machine.edge('¬ tag', '¬ tag', label=" ¬ '<'\\nadd character") state_machine.edge('¬ tag:s', '¬ tag', label="'>'") state_machine.edge('¬ tag', 'tag', label="'<'") state_machine.edge('tag', '¬ tag', label="'>'") state_machine.edge('tag', 'tag', label="¬ '>'") if __name__ == '__main__': display(state_machine) ## A First Fix ## ----------- if __name__ == '__main__': print('\n## A First Fix') if __name__ == '__main__': state_machine = graph() state_machine.node('Start') state_machine.edge('Start', '¬ quote\\n¬ tag') state_machine.edge('¬ quote\\n¬ tag', '¬ quote\\n¬ tag', label="¬ '<'\\nadd character") state_machine.edge('¬ quote\\n¬ tag', '¬ quote\\ntag', label="'<'") state_machine.edge('¬ quote\\ntag', 'quote\\ntag', label="'\"'") state_machine.edge('¬ quote\\ntag', '¬ quote\\ntag', label="¬ '\"' ∧ ¬ '>'") state_machine.edge('quote\\ntag', 'quote\\ntag', label="¬ '\"'") state_machine.edge('quote\\ntag', '¬ quote\\ntag', label="'\"'") state_machine.edge('¬ quote\\ntag', '¬ quote\\n¬ tag', label="'>'") if __name__ == '__main__': display(state_machine) def remove_html_markup(s): # type: ignore tag = False quote = False out = "" for c in s: if c == '<' and not quote: tag = True elif c == '>' and not quote: tag = False elif c == '"' or c == "'" and tag: quote = not quote elif not tag: out = out + c return out if __name__ == '__main__': remove_html_markup('') if __name__ == '__main__': assert remove_html_markup("Here's some strong argument.") == \ "Here's some strong argument." if __name__ == '__main__': assert remove_html_markup('') == "" if __name__ == '__main__': with ExpectError(): assert remove_html_markup('foo') == 'foo' if __name__ == '__main__': with ExpectError(): assert remove_html_markup('"foo"') == '"foo"' if __name__ == '__main__': with ExpectError(): assert remove_html_markup('"foo"') == '"foo"' if __name__ == '__main__': with ExpectError(): assert remove_html_markup('<"b">foo') == 'foo' ## The Devil's Guide to Debugging ## ------------------------------ if __name__ == '__main__': print("\n## The Devil's Guide to Debugging") ### Printf Debugging if __name__ == '__main__': print('\n### Printf Debugging') def remove_html_markup_with_print(s): # type: ignore tag = False quote = False out = "" for c in s: print("c =", repr(c), "tag =", tag, "quote =", quote) if c == '<' and not quote: tag = True elif c == '>' and not quote: tag = False elif c == '"' or c == "'" and tag: quote = not quote elif not tag: out = out + c return out if __name__ == '__main__': remove_html_markup_with_print('"foo"') ### Debugging into Existence if __name__ == '__main__': print('\n### Debugging into Existence') def remove_html_markup_without_quotes(s): # type: ignore tag = False quote = False out = "" for c in s: if c == '<': # and not quote: tag = True elif c == '>': # and not quote: tag = False elif c == '"' or c == "'" and tag: quote = not quote elif not tag: out = out + c return out if __name__ == '__main__': assert remove_html_markup_without_quotes('foo') == 'foo' if __name__ == '__main__': with ExpectError(): assert remove_html_markup_without_quotes('"foo"') == '"foo"' ### Use the Most Obvious Fix if __name__ == '__main__': print('\n### Use the Most Obvious Fix') def remove_html_markup_fixed(s): # type: ignore if s == '"foo"': return '"foo"' ... ### Things to do Instead if __name__ == '__main__': print('\n### Things to do Instead') ## From Defect to Failure ## ---------------------- if __name__ == '__main__': print('\n## From Defect to Failure') from typing import List, Optional def execution_diagram(show_steps: bool = True, variables: List[str] = [], steps: int = 3, error_step: int = 666, until: int = 666, fault_path: List[str] = []) -> Digraph: dot = graph() dot.node('input', shape='none', fillcolor='white', label=f"Input {PASS}", fontcolor=PASS_COLOR) last_outgoing_states = ['input'] for step in range(1, min(steps + 1, until)): step_color: Optional[str] if step == error_step: step_label = f'Step {step} {FAIL}' step_color = FAIL_COLOR else: step_label = f'Step {step}' step_color = None if step >= error_step: state_label = f'State {step} {FAIL}' state_color = FAIL_COLOR else: state_label = f'State {step} {PASS}' state_color = PASS_COLOR state_name = f's{step}' outgoing_states = [] incoming_states = [] if not variables: dot.node(name=state_name, shape='box', label=state_label, color=state_color, fontcolor=state_color) else: var_labels = [] for v in variables: vpath = f's{step}:{v}' if vpath in fault_path: var_label = f'<{v}>{v} ✘' outgoing_states.append(vpath) incoming_states.append(vpath) else: var_label = f'<{v}>{v}' var_labels.append(var_label) record_string = " | ".join(var_labels) dot.node(name=state_name, shape='record', label=nohtml(record_string), color=state_color, fontcolor=state_color) if not outgoing_states: outgoing_states = [state_name] if not incoming_states: incoming_states = [state_name] for outgoing_state in last_outgoing_states: for incoming_state in incoming_states: if show_steps: dot.edge(outgoing_state, incoming_state, label=step_label, fontcolor=step_color) else: dot.edge(outgoing_state, incoming_state) last_outgoing_states = outgoing_states if until > steps + 1: # Show output if error_step > steps: dot.node('output', shape='none', fillcolor='white', label=f"Output {PASS}", fontcolor=PASS_COLOR) else: dot.node('output', shape='none', fillcolor='white', label=f"Output {FAIL}", fontcolor=FAIL_COLOR) for outgoing_state in last_outgoing_states: label = "Execution" if steps == 0 else None dot.edge(outgoing_state, 'output', label=label) display(dot) if __name__ == '__main__': execution_diagram(show_steps=False, steps=0, error_step=0) if __name__ == '__main__': for until in range(1, 6): execution_diagram(show_steps=False, until=until, error_step=2) if __name__ == '__main__': for until in range(1, 6): execution_diagram(show_steps=True, until=until, error_step=2) if __name__ == '__main__': for until in range(1, 6): execution_diagram(show_steps=True, variables=['var1', 'var2', 'var3'], error_step=2, until=until, fault_path=['s2:var2', 's3:var2']) ## From Failure to Defect ## ---------------------- if __name__ == '__main__': print('\n## From Failure to Defect') ## The Scientific Method ## --------------------- if __name__ == '__main__': print('\n## The Scientific Method') if __name__ == '__main__': dot = graph() dot.node('Hypothesis') dot.node('Observation') dot.node('Prediction') dot.node('Experiment') dot.edge('Hypothesis', 'Observation', label="is supported:
Refine it>", dir='back') dot.edge('Hypothesis', 'Prediction') dot.node('Problem Report', shape='none', fillcolor='white') dot.edge('Problem Report', 'Hypothesis') dot.node('Code', shape='none', fillcolor='white') dot.edge('Code', 'Hypothesis') dot.node('Runs', shape='none', fillcolor='white') dot.edge('Runs', 'Hypothesis') dot.node('More Runs', shape='none', fillcolor='white') dot.edge('More Runs', 'Hypothesis') dot.edge('Prediction', 'Experiment') dot.edge('Experiment', 'Observation') dot.edge('Observation', 'Hypothesis', label="is rejected:
Seek alternative>") if __name__ == '__main__': display(dot) ### Finding a Hypothesis if __name__ == '__main__': print('\n### Finding a Hypothesis') if __name__ == '__main__': for i, html in enumerate(['foo', '"foo"', '"foo"', 'foo']): result = remove_html_markup(html) print("%-2d %-15s %s" % (i + 1, html, result)) if __name__ == '__main__': quiz("From the difference between success and failure," " we can already devise some observations about " " what is wrong with the output." " Which of these can we turn into general hypotheses?", [ "Double quotes (`\"`) are stripped from the tagged input.", "Tags in double quotes are not stripped.", "The tag `<>` is always stripped from the input.", "Four-letter words are stripped." ], '[298 % 33, 1234 % 616]') ### Testing a Hypothesis if __name__ == '__main__': print('\n### Testing a Hypothesis') if __name__ == '__main__': remove_html_markup('"foo"') ### Refining a Hypothesis if __name__ == '__main__': print('\n### Refining a Hypothesis') def remove_html_markup_with_tag_assert(s): # type: ignore tag = False quote = False out = "" for c in s: assert not tag # <=== Just added if c == '<' and not quote: tag = True elif c == '>' and not quote: tag = False elif c == '"' or c == "'" and tag: quote = not quote elif not tag: out = out + c return out if __name__ == '__main__': quiz("What happens after inserting the above assertion?", [ "The program raises an exception. (i.e., `tag` is set)", "The output is as before, i.e., `foo` without quotes." " (which means that `tag` is not set)" ], 2) if __name__ == '__main__': with ExpectError(): result = remove_html_markup_with_tag_assert('"foo"') result ### Refuting a Hypothesis if __name__ == '__main__': print('\n### Refuting a Hypothesis') def remove_html_markup_with_quote_assert(s): # type: ignore tag = False quote = False out = "" for c in s: if c == '<' and not quote: tag = True elif c == '>' and not quote: tag = False elif c == '"' or c == "'" and tag: assert False # <=== Just added quote = not quote elif not tag: out = out + c return out if __name__ == '__main__': quiz("What happens after inserting the 'assert' tag?", [ "The program raises an exception (i.e., the quote condition holds)", "The output is still foo (i.e., the quote condition does not hold)" ], 29 % 7) if __name__ == '__main__': with ExpectError(): result = remove_html_markup_with_quote_assert('"foo"') if __name__ == '__main__': remove_html_markup("'foo'") if __name__ == '__main__': quiz("How should the condition read?", [ '''`c == "" or c == '' and tag` (Choice 1)''', '''`c == '"' or c == "'" and not tag` (Choice 2)''', '''`(c == '"' or c == "'") and tag` (Choice 3)''', "Something else" ], '399 % 4') ## Fixing the Bug ## -------------- if __name__ == '__main__': print('\n## Fixing the Bug') ### Checking Diagnoses if __name__ == '__main__': print('\n### Checking Diagnoses') ### Fixing the Code if __name__ == '__main__': print('\n### Fixing the Code') def remove_html_markup(s): # type: ignore tag = False quote = False out = "" for c in s: if c == '<' and not quote: tag = True elif c == '>' and not quote: tag = False elif (c == '"' or c == "'") and tag: # <-- FIX quote = not quote elif not tag: out = out + c return out if __name__ == '__main__': assert remove_html_markup("Here's some strong argument.") == \ "Here's some strong argument." assert remove_html_markup( '') == "" assert remove_html_markup('foo') == 'foo' assert remove_html_markup('"foo"') == '"foo"' assert remove_html_markup('"foo"') == '"foo"' assert remove_html_markup('foo') == 'foo' ### Alternate Paths if __name__ == '__main__': print('\n### Alternate Paths') ## Homework after the Fix ## ---------------------- if __name__ == '__main__': print('\n## Homework after the Fix') ### Check for further Defect Occurrences if __name__ == '__main__': print('\n### Check for further Defect Occurrences') ### Check your Tests if __name__ == '__main__': print('\n### Check your Tests') ### Add Assertions if __name__ == '__main__': print('\n### Add Assertions') if __name__ == '__main__': quiz("Which assertion would have caught the problem?", [ "`assert quote and not tag`", "`assert quote or not tag`", "`assert tag or not quote`", "`assert tag and not quote`" ], '3270 - 3267') if __name__ == '__main__': display(state_machine) def remove_html_markup(s): # type: ignore tag = False quote = False out = "" for c in s: assert tag or not quote if c == '<' and not quote: tag = True elif c == '>' and not quote: tag = False elif (c == '"' or c == "'") and tag: quote = not quote elif not tag: out = out + c return out ### Commit the Fix if __name__ == '__main__': print('\n### Commit the Fix') ### Close the Bug Report if __name__ == '__main__': print('\n### Close the Bug Report') ## Become a Better Debugger ## ------------------------ if __name__ == '__main__': print('\n## Become a Better Debugger') ### Keep a Log if __name__ == '__main__': print('\n### Keep a Log') ### Rubberducking if __name__ == '__main__': print('\n### Rubberducking') ## The Cost of Debugging ## --------------------- if __name__ == '__main__': print('\n## The Cost of Debugging') ## Debugging Aftermath ## ------------------- if __name__ == '__main__': print('\n## Debugging Aftermath') import hashlib if __name__ == '__main__': bughash = hashlib.md5(b"debug").hexdigest() if __name__ == '__main__': quiz('Where has the name "bug" been used to denote disruptive events?', [ 'In the early days of Morse telegraphy, referring to a special key ' 'that would send a string of dots', 'Among radio technicians to describe a device that ' 'converts electromagnetic field variations into acoustic signals', "In Shakespeare's " '"Henry VI", referring to a walking spectre', 'In Middle English, where the word "bugge" is the basis for terms ' 'like "bugbear" and "bugaboo"' ], [bughash.index(i) for i in "d42f"]) ## Synopsis ## -------- if __name__ == '__main__': print('\n## Synopsis') ## Lessons Learned ## --------------- if __name__ == '__main__': print('\n## Lessons Learned') ## Next Steps ## ---------- if __name__ == '__main__': print('\n## Next Steps') ## Background ## ---------- if __name__ == '__main__': print('\n## Background') ## Exercises ## --------- if __name__ == '__main__': print('\n## Exercises') ### Exercise 1: Get Acquainted with Notebooks and Python if __name__ == '__main__': print('\n### Exercise 1: Get Acquainted with Notebooks and Python') #### Beginner Level: Run Notebooks in Your Browser if __name__ == '__main__': print('\n#### Beginner Level: Run Notebooks in Your Browser') #### Advanced Level: Run Python Code on Your Machine if __name__ == '__main__': print('\n#### Advanced Level: Run Python Code on Your Machine') #### Pro Level: Run Notebooks on Your Machine if __name__ == '__main__': print('\n#### Pro Level: Run Notebooks on Your Machine') #### Boss Level: Contribute! if __name__ == '__main__': print('\n#### Boss Level: Contribute!') ### Exercise 2: More Bugs! if __name__ == '__main__': print('\n### Exercise 2: More Bugs!') #### Part 1: Find the Problem if __name__ == '__main__': print('\n#### Part 1: Find the Problem') if __name__ == '__main__': assert(...) if __name__ == '__main__': s = '" + '">foo' s if __name__ == '__main__': remove_html_markup(s) if __name__ == '__main__': with ExpectError(): assert(remove_html_markup(s) == "foo") #### Part 2: Identify Extent and Cause if __name__ == '__main__': print('\n#### Part 2: Identify Extent and Cause') #### Part 3: Fix the Problem if __name__ == '__main__': print('\n#### Part 3: Fix the Problem') def remove_html_markup_with_proper_quotes(s): # type: ignore tag = False quote = '' out = "" for c in s: assert tag or quote == '' if c == '<' and quote == '': tag = True elif c == '>' and quote == '': tag = False elif (c == '"' or c == "'") and tag and quote == '': # beginning of string quote = c elif c == quote: # end of string quote = '' elif not tag: out = out + c return out if __name__ == '__main__': assert(remove_html_markup_with_proper_quotes(s) == "foo") if __name__ == '__main__': assert remove_html_markup_with_proper_quotes( "Here's some strong argument.") == \ "Here's some strong argument." assert remove_html_markup_with_proper_quotes( '') == "" assert remove_html_markup_with_proper_quotes('foo') == 'foo' assert remove_html_markup_with_proper_quotes('"foo"') == '"foo"' assert remove_html_markup_with_proper_quotes('"foo"') == '"foo"' assert remove_html_markup_with_proper_quotes('foo') == 'foo'