{ "cells": [ { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "slide" } }, "source": [ "# Isolating Failure-Inducing Changes\n", "\n", "\"Yesterday, my program worked. Today, it does not. Why?\" In debugging, as elsewhere in software development, code keeps on changing. Thus, it can happen that a piece of code that yesterday was working perfectly, today no longer runs – because we (or others) have made some changes to it that cause it to fail. The good news is that for debugging, we can actually _exploit_ this version history to narrow down _the changes that caused the failure_ – be it by us or by others." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:26.419367Z", "iopub.status.busy": "2024-01-17T21:32:26.419203Z", "iopub.status.idle": "2024-01-17T21:32:26.456585Z", "shell.execute_reply": "2024-01-17T21:32:26.456264Z" }, "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/html": [ "\n", " \n", " " ], "text/plain": [ "" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from bookutils import YouTubeVideo\n", "YouTubeVideo(\"hX9ViNEXGL8\")" ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "subslide" } }, "source": [ "**Prerequisites**\n", "\n", "* You should have read the [Chapter on Delta Debugging](DeltaDebugger.ipynb).\n", "* Knowledge on version control systems (notably git) will be useful." ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "button": false, "execution": { "iopub.execute_input": "2024-01-17T21:32:26.478599Z", "iopub.status.busy": "2024-01-17T21:32:26.478414Z", "iopub.status.idle": "2024-01-17T21:32:26.480579Z", "shell.execute_reply": "2024-01-17T21:32:26.480290Z" }, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "import bookutils.setup" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:26.482377Z", "iopub.status.busy": "2024-01-17T21:32:26.482258Z", "iopub.status.idle": "2024-01-17T21:32:26.483906Z", "shell.execute_reply": "2024-01-17T21:32:26.483672Z" }, "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from bookutils import quiz, print_file, print_content" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:26.485406Z", "iopub.status.busy": "2024-01-17T21:32:26.485298Z", "iopub.status.idle": "2024-01-17T21:32:26.486901Z", "shell.execute_reply": "2024-01-17T21:32:26.486656Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "# ignore\n", "from typing import Dict, Callable, TextIO, List, Tuple, Set, Any, Type, Optional" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "## Synopsis\n", "\n", "\n", "To [use the code provided in this chapter](Importing.ipynb), write\n", "\n", "```python\n", ">>> from debuggingbook.ChangeDebugger import \n", "```\n", "\n", "and then make use of the following features.\n", "\n", "\n", "This chapter introduces a class `ChangeDebugger` that automatically determines failure-inducing code changes.\n", "\n", "### High-Level Interface\n", "\n", "You are given two Python source codes `source_pass` and `source_fail`, and a function `test()` that works using the definitions in `source_pass`, but raises an exception using the definitions in `source_fail`. Then, you can use `ChangeDebugger` as follows:\n", "\n", "```python\n", "with ChangeDebugger(source_pass, source_fail) as cd:\n", " test()\n", "cd\n", "```\n", "\n", "This will produce the failure-inducing change between `source_pass` and `source_fail`, using [Delta Debugging](DeltaDebugger.ipynb) to determine minimal differences in patches applied.\n", "\n", "Here is an example. The function `test()` passes (raises no exception) if `remove_html_markup()` is defined as follows:\n", "\n", "```python\n", ">>> print_content(source_pass, '.py')\n", "def remove_html_markup(s): # type: ignore\n", " tag = False\n", " out = \"\"\n", "\n", " for c in s:\n", " if c == '<': # start of markup\n", " tag = True\n", " elif c == '>': # end of markup\n", " tag = False\n", " elif not tag:\n", " out = out + c\n", "\n", " return out\n", ">>> def test() -> None:\n", ">>> assert remove_html_markup('\"foo\"') == '\"foo\"'\n", ">>> exec(source_pass)\n", ">>> test()\n", "```\n", "If `remove_html_markup()` is changed as follows, though, then\n", "`test()` raises an exception and fails:\n", "\n", "```python\n", ">>> print_content(source_fail, '.py')\n", "def remove_html_markup(s): # type: ignore\n", " tag = False\n", " quote = False\n", " out = \"\"\n", "\n", " for c in s:\n", " if c == '<' and not quote:\n", " tag = True\n", " elif c == '>' and not quote:\n", " tag = False\n", " elif c == '\"' or c == \"'\" and tag:\n", " quote = not quote\n", " elif not tag:\n", " out = out + c\n", "\n", " return out\n", ">>> exec(source_fail)\n", ">>> with ExpectError(AssertionError):\n", ">>> test()\n", "Traceback (most recent call last):\n", " File \"/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_46013/4262003862.py\", line 3, in \n", " test()\n", " File \"/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_46013/3045937450.py\", line 2, in test\n", " assert remove_html_markup('\"foo\"') == '\"foo\"'\n", "AssertionError (expected)\n", "\n", "```\n", "We can use `ChangeDebugger` to automatically identify the failure-inducing difference:\n", "\n", "```python\n", ">>> with ChangeDebugger(source_pass, source_fail) as cd:\n", ">>> test()\n", ">>> cd\n", "@@ -215,24 +215,97 @@\n", " tag = False\n", "\n", "+ elif c == '\"' or c == \"'\" and tag:\n", " quote = not quote\n", "\n", " elif\n", "```\n", "The lines prefixed with `+` from are the ones in `source_fail` that cause the failure when added. (They also are the ones that should be fixed.)\n", "\n", "### Programmatic Interface\n", "\n", "For more details or more automation, use the programmatic interface. The method `min_patches()` returns a triple (`pass_patches`, `fail_patches`, `diffs`) where\n", "\n", "* applying `pass_patches` still make the call pass\n", "* applying `fail_patches` causes the call to fail\n", "* `diffs` is the (minimal) difference between the two.\n", "\n", "The patches come as list of `patch_obj` objects, as defined by Google's [diff-match-patch library](https://github.com/google/diff-match-patch).\n", "\n", "```python\n", ">>> pass_patches, fail_patches, diffs = cd.min_patches()\n", "```\n", "One can apply all patches in `pass_patches` and still not cause the test to fail:\n", "\n", "```python\n", ">>> for p in pass_patches:\n", ">>> print_patch(p)\n", "@@ -48,24 +48,42 @@\n", " tag = False\n", "\n", "+ quote = False\n", "\n", " out = \"\"\n", "@@ -104,50 +104,43 @@\n", " s:\n", "\n", "- if c == '<': # start of markup\n", "\n", "+ if c == '<' and not quote:\n", "\n", "```\n", "However, as soon as we also apply the patches in `diffs`, we get the failure. (This is also what is shown when we output a `ChangeDebugger`.)\n", "\n", "```python\n", ">>> for p in diffs:\n", ">>> print_patch(p)\n", "@@ -215,24 +215,97 @@\n", " tag = False\n", "\n", "+ elif c == '\"' or c == \"'\" and tag:\n", " quote = not quote\n", "\n", " elif\n", "\n", "```\n", "The full set of methods in `ChangeDebugger` is shown below.\n", "\n", "![](PICS/ChangeDebugger-synopsis-1.svg)\n", "\n", "### Supporting Functions\n", "\n", "`ChangeDebugger` relies on lower level `patch()` and `diff()` functions.\n", "\n", "To apply patch objects on source code, use the `patch()` function. It takes a source code and a list of patches to be applied.\n", "\n", "```python\n", ">>> print_content(patch(source_pass, diffs), '.py')\n", "def remove_html_markup(s): # type: ignore\n", " tag = False\n", " out = \"\"\n", "\n", " for c in s:\n", " if c == '<': # start of markup\n", " tag = True\n", " elif c == '>': # end of markup\n", " tag = False\n", " elif c == '\"' or c == \"'\" and tag:\n", " quote = not quote\n", " elif not tag:\n", " out = out + c\n", "\n", " return out\n", "```\n", "Conversely, the `diff()` function computes patches between two texts. It returns a list of patch objects that can be applied on text.\n", "\n", "```python\n", ">>> for p in diff(source_pass, source_fail):\n", ">>> print_patch(p)\n", "@@ -48,24 +48,42 @@\n", " tag = False\n", "\n", "+ quote = False\n", "\n", " out = \"\"\n", "@@ -104,50 +104,43 @@\n", " s:\n", "\n", "- if c == '<': # start of markup\n", "\n", "+ if c == '<' and not quote:\n", "@@ -162,48 +162,45 @@\n", " rue\n", "\n", "- elif c == '>': # end of markup\n", "\n", "+ elif c == '>' and not quote:\n", "@@ -215,24 +215,97 @@\n", " tag = False\n", "\n", "+ elif c == '\"' or c == \"'\" and tag:\n", " quote = not quote\n", "\n", " elif\n", "\n", "```\n" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Changes and Bugs\n", "\n", "When you develop software, it is unlikely that you will be able to produce a fully working piece of software right from the beginning – hence the need for debugging. It is just as unlikely, though, that your software will stay _unchanged_ forever. Evolving requirements, the introduction of new technology, changes in the environment all induce software changes – and every such change brings the risk of introducing new bugs.\n", "\n", "To detect such bugs introduced by changes, systematic (and best automatic) _testing_, notably _regression testing_, can be a big help; in fact, the more comprehensive and the more automated your testing strategy is, the more it actually _enables_ evolving your software, because every new test lowers the risk of a change introducing a bug. Extensive testing is what enables agile development – and we're happy to point to our [sibling book on test generation](https://www.fuzzingbook.org) to give you some inspiration on how to do this.\n", "\n", "However, a test can only _detect_ failures, not _fix_ them. The more you change, the more you may need to fix, too. The good news is that there are a number of debugging techniques, manual and automated, that can actually _exploit_ the presence of older, working software versions to effectively narrow down the causes of failure in a new, failing software." ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": true, "run_control": { "read_only": false }, "slideshow": { "slide_type": "slide" } }, "source": [ "## Leveraging Version Histories\n", "\n", "The fundamental prerequisite for exploiting older, working software versions is to have such older, working versions in the first place. If your software _always_ failed, then you will have to resort to conventional debugging. But if there is an older _working_ version, you can make use of it.\n", "\n", "We assume that you do have a _version repository_ such as git or SVN, which you use to organize software development and keep older software versions. (If you do _not_ use version control for your project, you are in [debugging hell](Intro_Debugging.ipynb). Go and set it up now, and come back once you're done.)\n", "\n", "If you have a version history, an older working version, and a new failing version, your situation is roughly as depicted in this diagram:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:26.488382Z", "iopub.status.busy": "2024-01-17T21:32:26.488297Z", "iopub.status.idle": "2024-01-17T21:32:26.495034Z", "shell.execute_reply": "2024-01-17T21:32:26.494789Z" }, "ipub": { "ignore": true }, "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from graphviz import Digraph, nohtml" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:26.496950Z", "iopub.status.busy": "2024-01-17T21:32:26.496831Z", "iopub.status.idle": "2024-01-17T21:32:26.498746Z", "shell.execute_reply": "2024-01-17T21:32:26.498440Z" }, "ipub": { "ignore": true }, "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from IPython.display import display" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:26.500524Z", "iopub.status.busy": "2024-01-17T21:32:26.500409Z", "iopub.status.idle": "2024-01-17T21:32:26.502465Z", "shell.execute_reply": "2024-01-17T21:32:26.502202Z" }, "ipub": { "ignore": true }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "# ignore\n", "PASS = \"✔\"\n", "FAIL = \"✘\"\n", "UNRESOLVED = \"?\"\n", "\n", "PASS_COLOR = 'darkgreen' # '#006400' # darkgreen\n", "FAIL_COLOR = 'red4' # '#8B0000' # darkred\n", "\n", "STEP_COLOR = 'peachpuff'\n", "FONT_NAME = 'Raleway'" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:26.503936Z", "iopub.status.busy": "2024-01-17T21:32:26.503830Z", "iopub.status.idle": "2024-01-17T21:32:26.505992Z", "shell.execute_reply": "2024-01-17T21:32:26.505678Z" }, "ipub": { "ignore": true }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "# ignore\n", "def graph(comment: str = \"default\") -> Digraph:\n", " return Digraph(name='', comment=comment,\n", " graph_attr={\n", " 'rankdir': 'LR',\n", " },\n", " node_attr={\n", " 'style': 'filled',\n", " 'shape': 'box',\n", " 'fillcolor': STEP_COLOR,\n", " 'fontname': FONT_NAME,\n", " },\n", " edge_attr={\n", " 'fontname': FONT_NAME,\n", " })" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:26.507916Z", "iopub.status.busy": "2024-01-17T21:32:26.507782Z", "iopub.status.idle": "2024-01-17T21:32:26.510845Z", "shell.execute_reply": "2024-01-17T21:32:26.510101Z" }, "ipub": { "ignore": true }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "# ignore\n", "VERSIONS = 8\n", "\n", "\n", "def display_versions(outcomes: Dict[int, str]) -> Digraph:\n", " state_machine = graph()\n", " for version_number in range(1, VERSIONS + 1):\n", " id = f'v{version_number}'\n", " label = f' {outcomes [version_number]}' \\\n", " if version_number in outcomes else ''\n", " state_machine.node(id, label=f'{id}{label}')\n", " if version_number > 1:\n", " last_id = f'v{version_number - 1}'\n", " state_machine.edge(last_id, id)\n", "\n", " display(state_machine)" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:26.513573Z", "iopub.status.busy": "2024-01-17T21:32:26.513459Z", "iopub.status.idle": "2024-01-17T21:32:26.968622Z", "shell.execute_reply": "2024-01-17T21:32:26.968063Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "v1\n", "\n", "v1 ✔\n", "\n", "\n", "\n", "v2\n", "\n", "v2\n", "\n", "\n", "\n", "v1->v2\n", "\n", "\n", "\n", "\n", "\n", "v3\n", "\n", "v3\n", "\n", "\n", "\n", "v2->v3\n", "\n", "\n", "\n", "\n", "\n", "v4\n", "\n", "v4\n", "\n", "\n", "\n", "v3->v4\n", "\n", "\n", "\n", "\n", "\n", "v5\n", "\n", "v5\n", "\n", "\n", "\n", "v4->v5\n", "\n", "\n", "\n", "\n", "\n", "v6\n", "\n", "v6\n", "\n", "\n", "\n", "v5->v6\n", "\n", "\n", "\n", "\n", "\n", "v7\n", "\n", "v7\n", "\n", "\n", "\n", "v6->v7\n", "\n", "\n", "\n", "\n", "\n", "v8\n", "\n", "v8 ✘\n", "\n", "\n", "\n", "v7->v8\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# ignore\n", "display_versions({1: PASS, 8: FAIL})" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Somewhere between the old version \"v1\" (\"yesterday\") and the current version \"v8\" (\"today\"), the software stopped working. But when exactly? And which change was it that caused the failure?\n", "\n", "You may think that this is an easy task: We simply manually test one version after another, thus determining the exact version that first failed. However, \n", "\n", "* this can take a long time (notably in the presence of dozens or even hundreds of versions);\n", "* this may still leave you with _dozens of changes_ applied from one version to another; and\n", "* this is actually a task that can be _fully automated_.\n", "\n", "And these \"automated\" debugging techniques are what we explore in this chapter." ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": true, "run_control": { "read_only": false }, "slideshow": { "slide_type": "slide" } }, "source": [ "## An Example Version History\n", "\n", "As our ongoing example, we start with creating a little version history, using the git version management system. We follow the evolution of the `remove_html_markup()` versions from [the introduction to debugging](Intro_Debugging.ipynb) and [the chapter on assertions](Assertions.ipynb)." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Create a Working Directory\n", "\n", "We start with creating a working folder (aptly named `my_project`) in which we will do our work. (Note: should you have a folder of that name, it will be deleted and re-initialized)." ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:26.970616Z", "iopub.status.busy": "2024-01-17T21:32:26.970390Z", "iopub.status.idle": "2024-01-17T21:32:26.972213Z", "shell.execute_reply": "2024-01-17T21:32:26.971938Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "PROJECT = 'my_project'" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:26.973588Z", "iopub.status.busy": "2024-01-17T21:32:26.973483Z", "iopub.status.idle": "2024-01-17T21:32:26.975112Z", "shell.execute_reply": "2024-01-17T21:32:26.974861Z" }, "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "import os\n", "import shutil" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:26.976447Z", "iopub.status.busy": "2024-01-17T21:32:26.976358Z", "iopub.status.idle": "2024-01-17T21:32:26.985195Z", "shell.execute_reply": "2024-01-17T21:32:26.984895Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "try:\n", " shutil.rmtree(PROJECT)\n", "except FileNotFoundError:\n", " pass\n", "os.mkdir(PROJECT)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We choose the project folder as our working directory. Any file we create will be created in that folder." ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:26.986846Z", "iopub.status.busy": "2024-01-17T21:32:26.986738Z", "iopub.status.idle": "2024-01-17T21:32:26.988422Z", "shell.execute_reply": "2024-01-17T21:32:26.988137Z" }, "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "import sys" ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:26.989811Z", "iopub.status.busy": "2024-01-17T21:32:26.989702Z", "iopub.status.idle": "2024-01-17T21:32:26.991464Z", "shell.execute_reply": "2024-01-17T21:32:26.991212Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "sys.path.append(os.getcwd())\n", "os.chdir(PROJECT)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Initialize Git\n", "\n", "We set up a local Git repository in our local project folder." ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:26.992855Z", "iopub.status.busy": "2024-01-17T21:32:26.992768Z", "iopub.status.idle": "2024-01-17T21:32:27.142706Z", "shell.execute_reply": "2024-01-17T21:32:27.142086Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Initialized empty Git repository in /Users/zeller/Projects/debuggingbook/notebooks/my_project/.git/\r\n" ] } ], "source": [ "!git init" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:27.146027Z", "iopub.status.busy": "2024-01-17T21:32:27.145748Z", "iopub.status.idle": "2024-01-17T21:32:27.302119Z", "shell.execute_reply": "2024-01-17T21:32:27.301283Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "!git config user.name \"Demo User\"" ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:27.305752Z", "iopub.status.busy": "2024-01-17T21:32:27.305496Z", "iopub.status.idle": "2024-01-17T21:32:27.464453Z", "shell.execute_reply": "2024-01-17T21:32:27.463855Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "!git config user.email \"demo-user@example.com\"" ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:27.467547Z", "iopub.status.busy": "2024-01-17T21:32:27.467283Z", "iopub.status.idle": "2024-01-17T21:32:27.616603Z", "shell.execute_reply": "2024-01-17T21:32:27.615547Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "!git config advice.detachedHead False" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We are now ready to commit our first version. Here's the initial definition of `remove_html_markup()` from [the introduction to debugging](Intro_Debugging.ipynb)." ] }, { "cell_type": "code", "execution_count": 20, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:27.620596Z", "iopub.status.busy": "2024-01-17T21:32:27.620275Z", "iopub.status.idle": "2024-01-17T21:32:27.624069Z", "shell.execute_reply": "2024-01-17T21:32:27.623548Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "def remove_html_markup(s): # type: ignore\n", " tag = False\n", " out = \"\"\n", "\n", " for c in s:\n", " if c == '<': # start of markup\n", " tag = True\n", " elif c == '>': # end of markup\n", " tag = False\n", " elif not tag:\n", " out = out + c\n", "\n", " return out" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "The function `write_source()` takes a function `fun` and writes its source code into a file of the same name – in our case, `remove_html_markup.py`:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:27.626477Z", "iopub.status.busy": "2024-01-17T21:32:27.626322Z", "iopub.status.idle": "2024-01-17T21:32:27.628831Z", "shell.execute_reply": "2024-01-17T21:32:27.628438Z" }, "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "import inspect" ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:27.631177Z", "iopub.status.busy": "2024-01-17T21:32:27.630967Z", "iopub.status.idle": "2024-01-17T21:32:27.634336Z", "shell.execute_reply": "2024-01-17T21:32:27.633599Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "def write_source(fun: Callable, filename: Optional[str] = None) -> None:\n", " if filename is None:\n", " filename = fun.__name__ + '.py'\n", " with open(filename, 'w') as fh:\n", " fh.write(inspect.getsource(fun))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Here is `write_source()` in action:" ] }, { "cell_type": "code", "execution_count": 23, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:27.636863Z", "iopub.status.busy": "2024-01-17T21:32:27.636724Z", "iopub.status.idle": "2024-01-17T21:32:27.640007Z", "shell.execute_reply": "2024-01-17T21:32:27.639624Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "write_source(remove_html_markup)" ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:27.642023Z", "iopub.status.busy": "2024-01-17T21:32:27.641872Z", "iopub.status.idle": "2024-01-17T21:32:27.723729Z", "shell.execute_reply": "2024-01-17T21:32:27.723421Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[34mdef\u001b[39;49;00m \u001b[32mremove_html_markup\u001b[39;49;00m(s): \u001b[37m# type: ignore\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " out = \u001b[33m\"\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mfor\u001b[39;49;00m c \u001b[35min\u001b[39;49;00m s:\u001b[37m\u001b[39;49;00m\n", " \u001b[34mif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# start of markup\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mTrue\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m>\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# end of markup\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " out = out + c\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mreturn\u001b[39;49;00m out\u001b[37m\u001b[39;49;00m" ] } ], "source": [ "print_file('remove_html_markup.py')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "With `git add` and `git commit`, we add the file to our version repository. The `-m` option defines a _message_ for the commit; this is how we (and potential co-workers) can later retrieve information on what has changed, and why. (The messages we use here are deliberately kept short.)" ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:27.725314Z", "iopub.status.busy": "2024-01-17T21:32:27.725224Z", "iopub.status.idle": "2024-01-17T21:32:27.868990Z", "shell.execute_reply": "2024-01-17T21:32:27.868306Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "!git add remove_html_markup.py" ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:27.872290Z", "iopub.status.busy": "2024-01-17T21:32:27.871900Z", "iopub.status.idle": "2024-01-17T21:32:28.032147Z", "shell.execute_reply": "2024-01-17T21:32:28.031404Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[main (root-commit) c5ff5ec] First version\r\n", " 1 file changed, 13 insertions(+)\r\n", " create mode 100644 remove_html_markup.py\r\n" ] } ], "source": [ "!git commit -m \"First version\"" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Let us now take the second (buggy) version of `remove_html_markup()` and again write this into our file, thus simulating changing the source code from the first version to the new version:" ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:28.035988Z", "iopub.status.busy": "2024-01-17T21:32:28.035734Z", "iopub.status.idle": "2024-01-17T21:32:28.039650Z", "shell.execute_reply": "2024-01-17T21:32:28.039213Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "def remove_html_markup(s): # type: ignore\n", " tag = False\n", " quote = False\n", " out = \"\"\n", "\n", " for c in s:\n", " if c == '<' and not quote:\n", " tag = True\n", " elif c == '>' and not quote:\n", " tag = False\n", " elif c == '\"' or c == \"'\" and tag:\n", " quote = not quote\n", " elif not tag:\n", " out = out + c\n", "\n", " return out" ] }, { "cell_type": "code", "execution_count": 28, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:28.042014Z", "iopub.status.busy": "2024-01-17T21:32:28.041879Z", "iopub.status.idle": "2024-01-17T21:32:28.045304Z", "shell.execute_reply": "2024-01-17T21:32:28.044943Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "write_source(remove_html_markup)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We can inspect the differences between the previously committed version and the current one. Lines prefixed with `+` are added; lines prefixed with `-` are deleted." ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:28.047453Z", "iopub.status.busy": "2024-01-17T21:32:28.047264Z", "iopub.status.idle": "2024-01-17T21:32:28.201131Z", "shell.execute_reply": "2024-01-17T21:32:28.200616Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[1mdiff --git a/remove_html_markup.py b/remove_html_markup.py\u001b[m\r\n", "\u001b[1mindex 4999dc0..768bae9 100644\u001b[m\r\n", "\u001b[1m--- a/remove_html_markup.py\u001b[m\r\n", "\u001b[1m+++ b/remove_html_markup.py\u001b[m\r\n", "\u001b[36m@@ -1,12 +1,15 @@\u001b[m\r\n", " def remove_html_markup(s): # type: ignore\u001b[m\r\n", " tag = False\u001b[m\r\n", "\u001b[32m+\u001b[m\u001b[32m quote = False\u001b[m\r\n", " out = \"\"\u001b[m\r\n", " \u001b[m\r\n", " for c in s:\u001b[m\r\n", "\u001b[31m- if c == '<': # start of markup\u001b[m\r\n", "\u001b[32m+\u001b[m\u001b[32m if c == '<' and not quote:\u001b[m\r\n", " tag = True\u001b[m\r\n", "\u001b[31m- elif c == '>': # end of markup\u001b[m\r\n", "\u001b[32m+\u001b[m\u001b[32m elif c == '>' and not quote:\u001b[m\r\n", " tag = False\u001b[m\r\n", "\u001b[32m+\u001b[m\u001b[32m elif c == '\"' or c == \"'\" and tag:\u001b[m\r\n", "\u001b[32m+\u001b[m\u001b[32m quote = not quote\u001b[m\r\n", " elif not tag:\u001b[m\r\n", " out = out + c\u001b[m\r\n", " \u001b[m\r\n" ] } ], "source": [ "!git diff remove_html_markup.py" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "We can now commit this second version, adding it to our repository." ] }, { "cell_type": "code", "execution_count": 30, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:28.203216Z", "iopub.status.busy": "2024-01-17T21:32:28.203092Z", "iopub.status.idle": "2024-01-17T21:32:28.358594Z", "shell.execute_reply": "2024-01-17T21:32:28.358220Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[main 298cda8] Second version\r\n", " 1 file changed, 5 insertions(+), 2 deletions(-)\r\n" ] } ], "source": [ "!git commit -m \"Second version\" remove_html_markup.py" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We create a few more revisions." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Excursion: Adding More Revisions" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We use the additional definitions for `remove_html_markup()` from [the introduction to debugging](Intro_Debugging.ipynb) as additional versions.\n", "\n", "These also include \"debugging\" versions with enabled logging statements, as well as \"tentative\" versions that may or may not fix the discussed issues. In a real version history, such transient versions would typically not show up – or at least not be made available to co-workers." ] }, { "cell_type": "code", "execution_count": 31, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:28.360880Z", "iopub.status.busy": "2024-01-17T21:32:28.360738Z", "iopub.status.idle": "2024-01-17T21:32:28.363308Z", "shell.execute_reply": "2024-01-17T21:32:28.363025Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "def remove_html_markup(s): # type: ignore\n", " tag = False\n", " quote = False\n", " out = \"\"\n", "\n", " for c in s:\n", " print(\"c =\", repr(c), \"tag =\", tag, \"quote =\", quote)\n", "\n", " if c == '<' and not quote:\n", " tag = True\n", " elif c == '>' and not quote:\n", " tag = False\n", " elif c == '\"' or c == \"'\" and tag:\n", " quote = not quote\n", " elif not tag:\n", " out = out + c\n", "\n", " return out" ] }, { "cell_type": "code", "execution_count": 32, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:28.365016Z", "iopub.status.busy": "2024-01-17T21:32:28.364900Z", "iopub.status.idle": "2024-01-17T21:32:28.367468Z", "shell.execute_reply": "2024-01-17T21:32:28.367139Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "write_source(remove_html_markup)" ] }, { "cell_type": "code", "execution_count": 33, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:28.369306Z", "iopub.status.busy": "2024-01-17T21:32:28.369186Z", "iopub.status.idle": "2024-01-17T21:32:28.523163Z", "shell.execute_reply": "2024-01-17T21:32:28.522763Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[main 0b0f2b2] Third version (with debugging output)\r\n", " 1 file changed, 2 insertions(+)\r\n" ] } ], "source": [ "!git commit -m \"Third version (with debugging output)\" remove_html_markup.py" ] }, { "cell_type": "code", "execution_count": 34, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:28.525278Z", "iopub.status.busy": "2024-01-17T21:32:28.525143Z", "iopub.status.idle": "2024-01-17T21:32:28.527564Z", "shell.execute_reply": "2024-01-17T21:32:28.527290Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "def remove_html_markup(s): # type: ignore\n", " tag = False\n", " quote = False\n", " out = \"\"\n", "\n", " for c in s:\n", " if c == '<': # and not quote:\n", " tag = True\n", " elif c == '>': # and not quote:\n", " tag = False\n", " elif c == '\"' or c == \"'\" and tag:\n", " quote = not quote\n", " elif not tag:\n", " out = out + c\n", "\n", " return out" ] }, { "cell_type": "code", "execution_count": 35, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:28.529420Z", "iopub.status.busy": "2024-01-17T21:32:28.529185Z", "iopub.status.idle": "2024-01-17T21:32:28.531746Z", "shell.execute_reply": "2024-01-17T21:32:28.531460Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "write_source(remove_html_markup)" ] }, { "cell_type": "code", "execution_count": 36, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:28.533601Z", "iopub.status.busy": "2024-01-17T21:32:28.533376Z", "iopub.status.idle": "2024-01-17T21:32:28.685354Z", "shell.execute_reply": "2024-01-17T21:32:28.684602Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[main 753f07f] Fourth version (clueless)\r\n", " 1 file changed, 2 insertions(+), 4 deletions(-)\r\n" ] } ], "source": [ "!git commit -m \"Fourth version (clueless)\" remove_html_markup.py" ] }, { "cell_type": "code", "execution_count": 37, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:28.689133Z", "iopub.status.busy": "2024-01-17T21:32:28.688893Z", "iopub.status.idle": "2024-01-17T21:32:28.692777Z", "shell.execute_reply": "2024-01-17T21:32:28.692245Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "def remove_html_markup(s): # type: ignore\n", " tag = False\n", " quote = False\n", " out = \"\"\n", "\n", " for c in s:\n", " assert not tag # <=== Just added\n", "\n", " if c == '<' and not quote:\n", " tag = True\n", " elif c == '>' and not quote:\n", " tag = False\n", " elif c == '\"' or c == \"'\" and tag:\n", " quote = not quote\n", " elif not tag:\n", " out = out + c\n", "\n", " return out" ] }, { "cell_type": "code", "execution_count": 38, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:28.695072Z", "iopub.status.busy": "2024-01-17T21:32:28.694931Z", "iopub.status.idle": "2024-01-17T21:32:28.697910Z", "shell.execute_reply": "2024-01-17T21:32:28.697503Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "write_source(remove_html_markup)" ] }, { "cell_type": "code", "execution_count": 39, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:28.700669Z", "iopub.status.busy": "2024-01-17T21:32:28.700392Z", "iopub.status.idle": "2024-01-17T21:32:28.858639Z", "shell.execute_reply": "2024-01-17T21:32:28.858051Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[main 4f86233] Fifth version (with assert)\r\n", " 1 file changed, 4 insertions(+), 2 deletions(-)\r\n" ] } ], "source": [ "!git commit -m \"Fifth version (with assert)\" remove_html_markup.py" ] }, { "cell_type": "code", "execution_count": 40, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:28.861690Z", "iopub.status.busy": "2024-01-17T21:32:28.861470Z", "iopub.status.idle": "2024-01-17T21:32:28.865273Z", "shell.execute_reply": "2024-01-17T21:32:28.864795Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "def remove_html_markup(s): # type: ignore\n", " tag = False\n", " quote = False\n", " out = \"\"\n", "\n", " for c in s:\n", " if c == '<' and not quote:\n", " tag = True\n", " elif c == '>' and not quote:\n", " tag = False\n", " elif c == '\"' or c == \"'\" and tag:\n", " assert False # <=== Just added\n", " quote = not quote\n", " elif not tag:\n", " out = out + c\n", "\n", " return out" ] }, { "cell_type": "code", "execution_count": 41, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:28.867677Z", "iopub.status.busy": "2024-01-17T21:32:28.867532Z", "iopub.status.idle": "2024-01-17T21:32:28.870493Z", "shell.execute_reply": "2024-01-17T21:32:28.870131Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "write_source(remove_html_markup)" ] }, { "cell_type": "code", "execution_count": 42, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:28.872567Z", "iopub.status.busy": "2024-01-17T21:32:28.872405Z", "iopub.status.idle": "2024-01-17T21:32:29.031469Z", "shell.execute_reply": "2024-01-17T21:32:29.030860Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[main 46c2036] Sixth version (with another assert)\r\n", " 1 file changed, 1 insertion(+), 2 deletions(-)\r\n" ] } ], "source": [ "!git commit -m \"Sixth version (with another assert)\" remove_html_markup.py" ] }, { "cell_type": "code", "execution_count": 43, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.034611Z", "iopub.status.busy": "2024-01-17T21:32:29.034177Z", "iopub.status.idle": "2024-01-17T21:32:29.037817Z", "shell.execute_reply": "2024-01-17T21:32:29.037367Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "def remove_html_markup(s): # type: ignore\n", " tag = False\n", " quote = False\n", " out = \"\"\n", "\n", " for c in s:\n", " if c == '<' and not quote:\n", " tag = True\n", " elif c == '>' and not quote:\n", " tag = False\n", " elif (c == '\"' or c == \"'\") and tag: # <-- FIX\n", " quote = not quote\n", " elif not tag:\n", " out = out + c\n", "\n", " return out" ] }, { "cell_type": "code", "execution_count": 44, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.040142Z", "iopub.status.busy": "2024-01-17T21:32:29.039994Z", "iopub.status.idle": "2024-01-17T21:32:29.043116Z", "shell.execute_reply": "2024-01-17T21:32:29.042729Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "write_source(remove_html_markup)" ] }, { "cell_type": "code", "execution_count": 45, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.045179Z", "iopub.status.busy": "2024-01-17T21:32:29.045020Z", "iopub.status.idle": "2024-01-17T21:32:29.201884Z", "shell.execute_reply": "2024-01-17T21:32:29.201055Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[main 0be85f0] Seventh version (fixed)\r\n", " 1 file changed, 1 insertion(+), 2 deletions(-)\r\n" ] } ], "source": [ "!git commit -m \"Seventh version (fixed)\" remove_html_markup.py" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### End of Excursion" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Here comes the last version of `remove_html_markup()`, this one from [the chapter on assertions](Assertions.ipynb)." ] }, { "cell_type": "code", "execution_count": 46, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.205138Z", "iopub.status.busy": "2024-01-17T21:32:29.204898Z", "iopub.status.idle": "2024-01-17T21:32:29.208801Z", "shell.execute_reply": "2024-01-17T21:32:29.208355Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "def remove_html_markup(s): # type: ignore\n", " tag = False\n", " quote = False\n", " out = \"\"\n", "\n", " for c in s:\n", " if c == '<' and not quote:\n", " tag = True\n", " elif c == '>' and not quote:\n", " tag = False\n", " elif c == '\"' or c == \"'\" and tag:\n", " quote = not quote\n", " elif not tag:\n", " out = out + c\n", "\n", " # postcondition\n", " assert '<' not in out and '>' not in out\n", "\n", " return out" ] }, { "cell_type": "code", "execution_count": 47, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.210986Z", "iopub.status.busy": "2024-01-17T21:32:29.210846Z", "iopub.status.idle": "2024-01-17T21:32:29.213945Z", "shell.execute_reply": "2024-01-17T21:32:29.213537Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "write_source(remove_html_markup)" ] }, { "cell_type": "code", "execution_count": 48, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.216347Z", "iopub.status.busy": "2024-01-17T21:32:29.216159Z", "iopub.status.idle": "2024-01-17T21:32:29.378946Z", "shell.execute_reply": "2024-01-17T21:32:29.378368Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[main 971504e] Eighth version (with proper assertion)\r\n", " 1 file changed, 4 insertions(+), 1 deletion(-)\r\n" ] } ], "source": [ "!git commit -m \"Eighth version (with proper assertion)\" remove_html_markup.py" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We finally reached the \"today\" state with the latest version – and find that the latest version has an error. This should come to you as no surprise if you have read the earlier chapters. But if you haven't, you will find that when the argument `s` contains double quotes, these are stripped from the output:" ] }, { "cell_type": "code", "execution_count": 49, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.381573Z", "iopub.status.busy": "2024-01-17T21:32:29.381362Z", "iopub.status.idle": "2024-01-17T21:32:29.384622Z", "shell.execute_reply": "2024-01-17T21:32:29.384290Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "'foo'" ] }, "execution_count": 49, "metadata": {}, "output_type": "execute_result" } ], "source": [ "remove_html_markup('\"foo\"')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Consequently, this test assertion fails:" ] }, { "cell_type": "code", "execution_count": 50, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.386422Z", "iopub.status.busy": "2024-01-17T21:32:29.386307Z", "iopub.status.idle": "2024-01-17T21:32:29.418672Z", "shell.execute_reply": "2024-01-17T21:32:29.418242Z" }, "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from ExpectError import ExpectError" ] }, { "cell_type": "code", "execution_count": 51, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.420910Z", "iopub.status.busy": "2024-01-17T21:32:29.420741Z", "iopub.status.idle": "2024-01-17T21:32:29.423345Z", "shell.execute_reply": "2024-01-17T21:32:29.422902Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Traceback (most recent call last):\n", " File \"/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_46013/1036061906.py\", line 2, in \n", " assert remove_html_markup('\"foo\"') == '\"foo\"'\n", "AssertionError (expected)\n" ] } ], "source": [ "with ExpectError():\n", " assert remove_html_markup('\"foo\"') == '\"foo\"'" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Note that the failure does _not_ occur in the very first version, as introduced above. So the simple question is:\n", "\n", "* What is the change that caused the failure?" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Accessing Versions\n", "\n", "To find out the failure-inducing change, we first need to be able to access older versions. The command \n", "\n", "```sh\n", "git log\n", "```\n", "\n", "gives us a listing of all commits:" ] }, { "cell_type": "code", "execution_count": 52, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.425181Z", "iopub.status.busy": "2024-01-17T21:32:29.425050Z", "iopub.status.idle": "2024-01-17T21:32:29.569959Z", "shell.execute_reply": "2024-01-17T21:32:29.569417Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[33mcommit 971504e21721f009758d6f8cde8a7c578dbec247\u001b[m\u001b[33m (\u001b[m\u001b[1;36mHEAD -> \u001b[m\u001b[1;32mmain\u001b[m\u001b[33m)\u001b[m\r\n", "Author: Demo User \r\n", "Date: Wed Jan 17 22:32:29 2024 +0100\r\n", "\r\n", " Eighth version (with proper assertion)\r\n", "\r\n", "\u001b[33mcommit 0be85f0d55de90f632d796b29ed5d5be07c5de7a\u001b[m\r\n", "Author: Demo User \r\n", "Date: Wed Jan 17 22:32:29 2024 +0100\r\n", "\r\n", " Seventh version (fixed)\r\n", "\r\n", "\u001b[33mcommit 46c2036616a2623a38ae93629b44f339992f229d\u001b[m\r\n", "Author: Demo User \r\n", "Date: Wed Jan 17 22:32:28 2024 +0100\r\n", "\r\n", " Sixth version (with another assert)\r\n", "\r\n", "\u001b[33mcommit 4f8623364746a2c5241b28cd97faaebd8a90123f\u001b[m\r\n", "Author: Demo User \r\n", "Date: Wed Jan 17 22:32:28 2024 +0100\r\n", "\r\n", " Fifth version (with assert)\r\n", "\r\n", "\u001b[33mcommit 753f07f1890200ce765c344761c32c1039a2d1e4\u001b[m\r\n", "Author: Demo User \r\n", "Date: Wed Jan 17 22:32:28 2024 +0100\r\n", "\r\n", " Fourth version (clueless)\r\n", "\r\n", "\u001b[33mcommit 0b0f2b2d1e6a6d98b1e870e2546988c8194161a5\u001b[m\r\n", "Author: Demo User \r\n", "Date: Wed Jan 17 22:32:28 2024 +0100\r\n", "\r\n", " Third version (with debugging output)\r\n", "\r\n", "\u001b[33mcommit 298cda816b6d6c647f5ae368d817510f55709d63\u001b[m\r\n", "Author: Demo User \r\n", "Date: Wed Jan 17 22:32:28 2024 +0100\r\n", "\r\n", " Second version\r\n", "\r\n", "\u001b[33mcommit c5ff5ec9e81a2edf75afb0e1d5bb89f2b5a51a3f\u001b[m\r\n", "Author: Demo User \r\n", "Date: Wed Jan 17 22:32:27 2024 +0100\r\n", "\r\n", " First version\r\n" ] } ], "source": [ "!git log" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Using the `subprocess` module, we can run `git log` and access its output." ] }, { "cell_type": "code", "execution_count": 53, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.572860Z", "iopub.status.busy": "2024-01-17T21:32:29.572603Z", "iopub.status.idle": "2024-01-17T21:32:29.574781Z", "shell.execute_reply": "2024-01-17T21:32:29.574456Z" }, "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "import subprocess" ] }, { "cell_type": "code", "execution_count": 54, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.576811Z", "iopub.status.busy": "2024-01-17T21:32:29.576654Z", "iopub.status.idle": "2024-01-17T21:32:29.579070Z", "shell.execute_reply": "2024-01-17T21:32:29.578527Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "def get_output(command: List[str]) -> str:\n", " result = subprocess.run(command, \n", " stdout=subprocess.PIPE,\n", " universal_newlines=True)\n", " return result.stdout" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "The output of `git log` contains the ID of the version (the so-called *commit hash*) as well as the message provided during the commit." ] }, { "cell_type": "code", "execution_count": 55, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.581148Z", "iopub.status.busy": "2024-01-17T21:32:29.581002Z", "iopub.status.idle": "2024-01-17T21:32:29.596635Z", "shell.execute_reply": "2024-01-17T21:32:29.596141Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "971504e21721f009758d6f8cde8a7c578dbec247 Eighth version (with proper assertion)\n", "0be85f0d55de90f632d796b29ed5d5be07c5de7a Seventh version (fixed)\n", "46c2036616a2623a38ae93629b44f339992f229d Sixth version (with another assert)\n", "4f8623364746a2c5241b28cd97faaebd8a90123f Fifth version (with assert)\n", "753f07f1890200ce765c344761c32c1039a2d1e4 Fourth version (clueless)\n", "0b0f2b2d1e6a6d98b1e870e2546988c8194161a5 Third version (with debugging output)\n", "298cda816b6d6c647f5ae368d817510f55709d63 Second version\n", "c5ff5ec9e81a2edf75afb0e1d5bb89f2b5a51a3f First version\n", "\n" ] } ], "source": [ "log = get_output(['git', 'log', '--pretty=oneline'])\n", "print(log)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Each hash uniquely identifies a version, and is required to access it. Let us create a list `versions`, where `versions[0]` contains the hash (the id) of the first version, `versions[1]` the second version, and so on." ] }, { "cell_type": "code", "execution_count": 56, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.599228Z", "iopub.status.busy": "2024-01-17T21:32:29.599047Z", "iopub.status.idle": "2024-01-17T21:32:29.602605Z", "shell.execute_reply": "2024-01-17T21:32:29.602172Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "'c5ff5ec9e81a2edf75afb0e1d5bb89f2b5a51a3f'" ] }, "execution_count": 56, "metadata": {}, "output_type": "execute_result" } ], "source": [ "versions = [line.split()[0] for line in log.split('\\n') if line]\n", "versions.reverse()\n", "versions[0]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We can now check out the first version:" ] }, { "cell_type": "code", "execution_count": 57, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.604800Z", "iopub.status.busy": "2024-01-17T21:32:29.604689Z", "iopub.status.idle": "2024-01-17T21:32:29.752771Z", "shell.execute_reply": "2024-01-17T21:32:29.752288Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "HEAD is now at c5ff5ec First version\r\n" ] } ], "source": [ "!git checkout {versions[0]}" ] }, { "cell_type": "code", "execution_count": 58, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.755188Z", "iopub.status.busy": "2024-01-17T21:32:29.755034Z", "iopub.status.idle": "2024-01-17T21:32:29.792821Z", "shell.execute_reply": "2024-01-17T21:32:29.792493Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[34mdef\u001b[39;49;00m \u001b[32mremove_html_markup\u001b[39;49;00m(s): \u001b[37m# type: ignore\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " out = \u001b[33m\"\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mfor\u001b[39;49;00m c \u001b[35min\u001b[39;49;00m s:\u001b[37m\u001b[39;49;00m\n", " \u001b[34mif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# start of markup\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mTrue\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m>\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# end of markup\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " out = out + c\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mreturn\u001b[39;49;00m out\u001b[37m\u001b[39;49;00m" ] } ], "source": [ "print_file('remove_html_markup.py')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "If we read in this definition of `remove_html_markup()` from the first version, we will find that the failure was not yet present:" ] }, { "cell_type": "code", "execution_count": 59, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.795169Z", "iopub.status.busy": "2024-01-17T21:32:29.794917Z", "iopub.status.idle": "2024-01-17T21:32:29.797292Z", "shell.execute_reply": "2024-01-17T21:32:29.796948Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "exec(open('remove_html_markup.py').read())" ] }, { "cell_type": "code", "execution_count": 60, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.799626Z", "iopub.status.busy": "2024-01-17T21:32:29.799474Z", "iopub.status.idle": "2024-01-17T21:32:29.801960Z", "shell.execute_reply": "2024-01-17T21:32:29.801682Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "'\"foo\"'" ] }, "execution_count": 60, "metadata": {}, "output_type": "execute_result" } ], "source": [ "remove_html_markup('\"foo\"')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "However, if we check out the _last_ version of that file ..." ] }, { "cell_type": "code", "execution_count": 61, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.803831Z", "iopub.status.busy": "2024-01-17T21:32:29.803686Z", "iopub.status.idle": "2024-01-17T21:32:29.954116Z", "shell.execute_reply": "2024-01-17T21:32:29.953672Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Previous HEAD position was c5ff5ec First version\r\n", "HEAD is now at 971504e Eighth version (with proper assertion)\r\n" ] } ], "source": [ "!git checkout {versions[7]}" ] }, { "cell_type": "code", "execution_count": 62, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.956410Z", "iopub.status.busy": "2024-01-17T21:32:29.956247Z", "iopub.status.idle": "2024-01-17T21:32:29.993141Z", "shell.execute_reply": "2024-01-17T21:32:29.992773Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[34mdef\u001b[39;49;00m \u001b[32mremove_html_markup\u001b[39;49;00m(s): \u001b[37m# type: ignore\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " out = \u001b[33m\"\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mfor\u001b[39;49;00m c \u001b[35min\u001b[39;49;00m s:\u001b[37m\u001b[39;49;00m\n", " \u001b[34mif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m quote:\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mTrue\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m>\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m quote:\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mor\u001b[39;49;00m c == \u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[35mnot\u001b[39;49;00m quote\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " out = out + c\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[37m# postcondition\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34massert\u001b[39;49;00m \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m \u001b[35min\u001b[39;49;00m out \u001b[35mand\u001b[39;49;00m \u001b[33m'\u001b[39;49;00m\u001b[33m>\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m \u001b[35min\u001b[39;49;00m out\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mreturn\u001b[39;49;00m out\u001b[37m\u001b[39;49;00m" ] } ], "source": [ "print_file('remove_html_markup.py')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "... we find that this is the version that no longer works." ] }, { "cell_type": "code", "execution_count": 63, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.995406Z", "iopub.status.busy": "2024-01-17T21:32:29.995262Z", "iopub.status.idle": "2024-01-17T21:32:29.997628Z", "shell.execute_reply": "2024-01-17T21:32:29.997202Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "exec(open('remove_html_markup.py').read())" ] }, { "cell_type": "code", "execution_count": 64, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:29.999427Z", "iopub.status.busy": "2024-01-17T21:32:29.999285Z", "iopub.status.idle": "2024-01-17T21:32:30.001916Z", "shell.execute_reply": "2024-01-17T21:32:30.001570Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "'foo'" ] }, "execution_count": 64, "metadata": {}, "output_type": "execute_result" } ], "source": [ "remove_html_markup('\"foo\"')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Manual Bisecting\n", "\n", "As stated above, we could now go and try out one version after another to see with which version the bug was introduced. But again, proceeding in such a _linear_ fashion would be very inefficient. It is much better to proceed by _binary search_: If you know that version $v_n$ passed, and version $v_m$ failed (with $m >n$), then test a version $v' = v_{n + (m - n)/2}$ that is right in the _middle_ between the two.\n", "\n", "* If $v'$ passes, then repeat the process with $v'$ and $v_m$.\n", "* If $v'$ fails, then repeat the process with $v_n$ and $v'$.\n", "\n", "Such *bisecting* quickly progresses towards the failure-inducing version, as it requires you to take only a logarithmic number of tests. In contrast, progressing linearly through the version history requires a test for each version, which is far more effort." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "If you use the git version control system, such bisecting is actually a _built-in feature_, coming to you through the `git bisect` command. Let us illustrate how `git bisect` quickly identifies the version that introduced the error." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "A bisecting section with `git` starts with the command `bisect start`." ] }, { "cell_type": "code", "execution_count": 65, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:30.003900Z", "iopub.status.busy": "2024-01-17T21:32:30.003762Z", "iopub.status.idle": "2024-01-17T21:32:30.211711Z", "shell.execute_reply": "2024-01-17T21:32:30.211204Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "status: waiting for both good and bad commits\r\n" ] } ], "source": [ "!git bisect start" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Then, you use `git bisect good` to identify the version that worked, and `git bisect bad` to identify the version that was bad – in our case, the hashes of the first and last version." ] }, { "cell_type": "code", "execution_count": 66, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:30.214260Z", "iopub.status.busy": "2024-01-17T21:32:30.214091Z", "iopub.status.idle": "2024-01-17T21:32:30.417100Z", "shell.execute_reply": "2024-01-17T21:32:30.416570Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "status: waiting for bad commit, 1 good commit known\r\n" ] } ], "source": [ "!git bisect good {versions[0]}" ] }, { "cell_type": "code", "execution_count": 67, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:30.419676Z", "iopub.status.busy": "2024-01-17T21:32:30.419506Z", "iopub.status.idle": "2024-01-17T21:32:30.631134Z", "shell.execute_reply": "2024-01-17T21:32:30.630696Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Bisecting: 3 revisions left to test after this (roughly 2 steps)\r\n", "[753f07f1890200ce765c344761c32c1039a2d1e4] Fourth version (clueless)\r\n" ] } ], "source": [ "!git bisect bad {versions[7]}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We find that `git bisect` automatically has checked out the _middle_ version between the passing and the failing one – in our case, version 4 – and now asks us to assess this version." ] }, { "cell_type": "code", "execution_count": 68, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:30.633775Z", "iopub.status.busy": "2024-01-17T21:32:30.633597Z", "iopub.status.idle": "2024-01-17T21:32:31.080032Z", "shell.execute_reply": "2024-01-17T21:32:31.079680Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "v1\n", "\n", "v1 ✔\n", "\n", "\n", "\n", "v2\n", "\n", "v2\n", "\n", "\n", "\n", "v1->v2\n", "\n", "\n", "\n", "\n", "\n", "v3\n", "\n", "v3\n", "\n", "\n", "\n", "v2->v3\n", "\n", "\n", "\n", "\n", "\n", "v4\n", "\n", "v4 ?\n", "\n", "\n", "\n", "v3->v4\n", "\n", "\n", "\n", "\n", "\n", "v5\n", "\n", "v5\n", "\n", "\n", "\n", "v4->v5\n", "\n", "\n", "\n", "\n", "\n", "v6\n", "\n", "v6\n", "\n", "\n", "\n", "v5->v6\n", "\n", "\n", "\n", "\n", "\n", "v7\n", "\n", "v7\n", "\n", "\n", "\n", "v6->v7\n", "\n", "\n", "\n", "\n", "\n", "v8\n", "\n", "v8 ✘\n", "\n", "\n", "\n", "v7->v8\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# ignore\n", "display_versions({1: PASS, 4: UNRESOLVED, 8: FAIL})" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "The version is already in our working folder:" ] }, { "cell_type": "code", "execution_count": 69, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:31.081947Z", "iopub.status.busy": "2024-01-17T21:32:31.081813Z", "iopub.status.idle": "2024-01-17T21:32:31.114879Z", "shell.execute_reply": "2024-01-17T21:32:31.114466Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[34mdef\u001b[39;49;00m \u001b[32mremove_html_markup\u001b[39;49;00m(s): \u001b[37m# type: ignore\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " out = \u001b[33m\"\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mfor\u001b[39;49;00m c \u001b[35min\u001b[39;49;00m s:\u001b[37m\u001b[39;49;00m\n", " \u001b[34mif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# and not quote:\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mTrue\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m>\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# and not quote:\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mor\u001b[39;49;00m c == \u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[35mnot\u001b[39;49;00m quote\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " out = out + c\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mreturn\u001b[39;49;00m out\u001b[37m\u001b[39;49;00m" ] } ], "source": [ "print_file('remove_html_markup.py')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "We now need to test this version, and let `git bisect` know the outcome – with\n", "\n", "```sh\n", "git bisect good\n", "```\n", "\n", "if the test passes, and with\n", "\n", "```sh\n", "git bisect bad\n", "```\n", "\n", "if the test fails." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "It turns out that this version fails:" ] }, { "cell_type": "code", "execution_count": 70, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:31.117200Z", "iopub.status.busy": "2024-01-17T21:32:31.117013Z", "iopub.status.idle": "2024-01-17T21:32:31.119450Z", "shell.execute_reply": "2024-01-17T21:32:31.119137Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "exec(open('remove_html_markup.py').read())" ] }, { "cell_type": "code", "execution_count": 71, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:31.121034Z", "iopub.status.busy": "2024-01-17T21:32:31.120920Z", "iopub.status.idle": "2024-01-17T21:32:31.122959Z", "shell.execute_reply": "2024-01-17T21:32:31.122689Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "'foo'" ] }, "execution_count": 71, "metadata": {}, "output_type": "execute_result" } ], "source": [ "remove_html_markup('\"foo\"')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "So we enter `git bisect bad`:" ] }, { "cell_type": "code", "execution_count": 72, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:31.124457Z", "iopub.status.busy": "2024-01-17T21:32:31.124351Z", "iopub.status.idle": "2024-01-17T21:32:31.318944Z", "shell.execute_reply": "2024-01-17T21:32:31.318264Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Bisecting: 0 revisions left to test after this (roughly 1 step)\r\n", "[0b0f2b2d1e6a6d98b1e870e2546988c8194161a5] Third version (with debugging output)\r\n" ] } ], "source": [ "!git bisect bad" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "`git bisect` has chosen version 3 to assess – again in the middle between a passing and a failing version:" ] }, { "cell_type": "code", "execution_count": 73, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:31.322558Z", "iopub.status.busy": "2024-01-17T21:32:31.322299Z", "iopub.status.idle": "2024-01-17T21:32:31.750622Z", "shell.execute_reply": "2024-01-17T21:32:31.750167Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "v1\n", "\n", "v1 ✔\n", "\n", "\n", "\n", "v2\n", "\n", "v2\n", "\n", "\n", "\n", "v1->v2\n", "\n", "\n", "\n", "\n", "\n", "v3\n", "\n", "v3 ?\n", "\n", "\n", "\n", "v2->v3\n", "\n", "\n", "\n", "\n", "\n", "v4\n", "\n", "v4 ✘\n", "\n", "\n", "\n", "v3->v4\n", "\n", "\n", "\n", "\n", "\n", "v5\n", "\n", "v5\n", "\n", "\n", "\n", "v4->v5\n", "\n", "\n", "\n", "\n", "\n", "v6\n", "\n", "v6\n", "\n", "\n", "\n", "v5->v6\n", "\n", "\n", "\n", "\n", "\n", "v7\n", "\n", "v7\n", "\n", "\n", "\n", "v6->v7\n", "\n", "\n", "\n", "\n", "\n", "v8\n", "\n", "v8 ✘\n", "\n", "\n", "\n", "v7->v8\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# ignore\n", "display_versions({1: PASS, 3: UNRESOLVED, 4: FAIL, 8: FAIL})" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "So let us test this version and find that it fails, too:" ] }, { "cell_type": "code", "execution_count": 74, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:31.752931Z", "iopub.status.busy": "2024-01-17T21:32:31.752783Z", "iopub.status.idle": "2024-01-17T21:32:31.785557Z", "shell.execute_reply": "2024-01-17T21:32:31.785166Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[34mdef\u001b[39;49;00m \u001b[32mremove_html_markup\u001b[39;49;00m(s): \u001b[37m# type: ignore\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " out = \u001b[33m\"\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mfor\u001b[39;49;00m c \u001b[35min\u001b[39;49;00m s:\u001b[37m\u001b[39;49;00m\n", " \u001b[36mprint\u001b[39;49;00m(\u001b[33m\"\u001b[39;49;00m\u001b[33mc =\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m, \u001b[36mrepr\u001b[39;49;00m(c), \u001b[33m\"\u001b[39;49;00m\u001b[33mtag =\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m, tag, \u001b[33m\"\u001b[39;49;00m\u001b[33mquote =\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m, quote)\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m quote:\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mTrue\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m>\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m quote:\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mor\u001b[39;49;00m c == \u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[35mnot\u001b[39;49;00m quote\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " out = out + c\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mreturn\u001b[39;49;00m out\u001b[37m\u001b[39;49;00m" ] } ], "source": [ "print_file('remove_html_markup.py')" ] }, { "cell_type": "code", "execution_count": 75, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:31.787388Z", "iopub.status.busy": "2024-01-17T21:32:31.787237Z", "iopub.status.idle": "2024-01-17T21:32:31.789362Z", "shell.execute_reply": "2024-01-17T21:32:31.789017Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "exec(open('remove_html_markup.py').read())" ] }, { "cell_type": "code", "execution_count": 76, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:31.791426Z", "iopub.status.busy": "2024-01-17T21:32:31.791277Z", "iopub.status.idle": "2024-01-17T21:32:31.794221Z", "shell.execute_reply": "2024-01-17T21:32:31.793926Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "c = '\"' tag = False quote = False\n", "c = 'f' tag = False quote = True\n", "c = 'o' tag = False quote = True\n", "c = 'o' tag = False quote = True\n", "c = '\"' tag = False quote = True\n" ] }, { "data": { "text/plain": [ "'foo'" ] }, "execution_count": 76, "metadata": {}, "output_type": "execute_result" } ], "source": [ "remove_html_markup('\"foo\"')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We mark the version as `bad`. `git bisect` then checks out version 2 as the last version to assess." ] }, { "cell_type": "code", "execution_count": 77, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:31.796291Z", "iopub.status.busy": "2024-01-17T21:32:31.796094Z", "iopub.status.idle": "2024-01-17T21:32:32.001257Z", "shell.execute_reply": "2024-01-17T21:32:32.000819Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Bisecting: 0 revisions left to test after this (roughly 0 steps)\r\n", "[298cda816b6d6c647f5ae368d817510f55709d63] Second version\r\n" ] } ], "source": [ "!git bisect bad" ] }, { "cell_type": "code", "execution_count": 78, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:32.003215Z", "iopub.status.busy": "2024-01-17T21:32:32.003072Z", "iopub.status.idle": "2024-01-17T21:32:32.470234Z", "shell.execute_reply": "2024-01-17T21:32:32.469794Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "v1\n", "\n", "v1 ✔\n", "\n", "\n", "\n", "v2\n", "\n", "v2 ?\n", "\n", "\n", "\n", "v1->v2\n", "\n", "\n", "\n", "\n", "\n", "v3\n", "\n", "v3 ✘\n", "\n", "\n", "\n", "v2->v3\n", "\n", "\n", "\n", "\n", "\n", "v4\n", "\n", "v4 ✘\n", "\n", "\n", "\n", "v3->v4\n", "\n", "\n", "\n", "\n", "\n", "v5\n", "\n", "v5\n", "\n", "\n", "\n", "v4->v5\n", "\n", "\n", "\n", "\n", "\n", "v6\n", "\n", "v6\n", "\n", "\n", "\n", "v5->v6\n", "\n", "\n", "\n", "\n", "\n", "v7\n", "\n", "v7\n", "\n", "\n", "\n", "v6->v7\n", "\n", "\n", "\n", "\n", "\n", "v8\n", "\n", "v8 ✘\n", "\n", "\n", "\n", "v7->v8\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# ignore\n", "display_versions({1: PASS, 2: UNRESOLVED, 3: FAIL, 4: FAIL, 8: FAIL})" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "When we test version 2, we find that it fails as well:" ] }, { "cell_type": "code", "execution_count": 79, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:32.472321Z", "iopub.status.busy": "2024-01-17T21:32:32.472088Z", "iopub.status.idle": "2024-01-17T21:32:32.504431Z", "shell.execute_reply": "2024-01-17T21:32:32.504111Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[34mdef\u001b[39;49;00m \u001b[32mremove_html_markup\u001b[39;49;00m(s): \u001b[37m# type: ignore\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " out = \u001b[33m\"\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mfor\u001b[39;49;00m c \u001b[35min\u001b[39;49;00m s:\u001b[37m\u001b[39;49;00m\n", " \u001b[34mif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m quote:\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mTrue\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m>\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m quote:\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mor\u001b[39;49;00m c == \u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[35mnot\u001b[39;49;00m quote\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " out = out + c\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mreturn\u001b[39;49;00m out\u001b[37m\u001b[39;49;00m" ] } ], "source": [ "print_file('remove_html_markup.py')" ] }, { "cell_type": "code", "execution_count": 80, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:32.506179Z", "iopub.status.busy": "2024-01-17T21:32:32.506053Z", "iopub.status.idle": "2024-01-17T21:32:32.508028Z", "shell.execute_reply": "2024-01-17T21:32:32.507791Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "exec(open('remove_html_markup.py').read())" ] }, { "cell_type": "code", "execution_count": 81, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:32.509528Z", "iopub.status.busy": "2024-01-17T21:32:32.509411Z", "iopub.status.idle": "2024-01-17T21:32:32.511561Z", "shell.execute_reply": "2024-01-17T21:32:32.511320Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "'foo'" ] }, "execution_count": 81, "metadata": {}, "output_type": "execute_result" } ], "source": [ "remove_html_markup('\"foo\"')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Hence, version 2 is _the version that introduced the error_." ] }, { "cell_type": "code", "execution_count": 82, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:32.513040Z", "iopub.status.busy": "2024-01-17T21:32:32.512932Z", "iopub.status.idle": "2024-01-17T21:32:32.978111Z", "shell.execute_reply": "2024-01-17T21:32:32.977612Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "v1\n", "\n", "v1 ✔\n", "\n", "\n", "\n", "v2\n", "\n", "v2 ✘\n", "\n", "\n", "\n", "v1->v2\n", "\n", "\n", "\n", "\n", "\n", "v3\n", "\n", "v3 ✘\n", "\n", "\n", "\n", "v2->v3\n", "\n", "\n", "\n", "\n", "\n", "v4\n", "\n", "v4 ✘\n", "\n", "\n", "\n", "v3->v4\n", "\n", "\n", "\n", "\n", "\n", "v5\n", "\n", "v5\n", "\n", "\n", "\n", "v4->v5\n", "\n", "\n", "\n", "\n", "\n", "v6\n", "\n", "v6\n", "\n", "\n", "\n", "v5->v6\n", "\n", "\n", "\n", "\n", "\n", "v7\n", "\n", "v7\n", "\n", "\n", "\n", "v6->v7\n", "\n", "\n", "\n", "\n", "\n", "v8\n", "\n", "v8 ✘\n", "\n", "\n", "\n", "v7->v8\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# ignore\n", "display_versions({1: PASS, 2: FAIL, 3: FAIL, 4: FAIL, 8: FAIL})" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "When we let `git bisect` know that this version fails, it tells us that this version is indeed the \"first bad commit\":" ] }, { "cell_type": "code", "execution_count": 83, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:32.980789Z", "iopub.status.busy": "2024-01-17T21:32:32.980584Z", "iopub.status.idle": "2024-01-17T21:32:33.186077Z", "shell.execute_reply": "2024-01-17T21:32:33.185485Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "298cda816b6d6c647f5ae368d817510f55709d63 is the first bad commit\r\n", "\u001b[33mcommit 298cda816b6d6c647f5ae368d817510f55709d63\u001b[m\r\n", "Author: Demo User \r\n", "Date: Wed Jan 17 22:32:28 2024 +0100\r\n", "\r\n", " Second version\r\n", "\r\n", " remove_html_markup.py | 7 \u001b[32m+++++\u001b[m\u001b[31m--\u001b[m\r\n", " 1 file changed, 5 insertions(+), 2 deletions(-)\r\n" ] } ], "source": [ "!git bisect bad" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "By comparing this version against the older one, we can see the lines it introduced – namely (buggy) handling of double quotes:" ] }, { "cell_type": "code", "execution_count": 84, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:33.188595Z", "iopub.status.busy": "2024-01-17T21:32:33.188399Z", "iopub.status.idle": "2024-01-17T21:32:33.337134Z", "shell.execute_reply": "2024-01-17T21:32:33.336558Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[1mdiff --git a/remove_html_markup.py b/remove_html_markup.py\u001b[m\r\n", "\u001b[1mindex 4999dc0..768bae9 100644\u001b[m\r\n", "\u001b[1m--- a/remove_html_markup.py\u001b[m\r\n", "\u001b[1m+++ b/remove_html_markup.py\u001b[m\r\n", "\u001b[36m@@ -1,12 +1,15 @@\u001b[m\r\n", " def remove_html_markup(s): # type: ignore\u001b[m\r\n", " tag = False\u001b[m\r\n", "\u001b[32m+\u001b[m\u001b[32m quote = False\u001b[m\r\n", " out = \"\"\u001b[m\r\n", " \u001b[m\r\n", " for c in s:\u001b[m\r\n", "\u001b[31m- if c == '<': # start of markup\u001b[m\r\n", "\u001b[32m+\u001b[m\u001b[32m if c == '<' and not quote:\u001b[m\r\n", " tag = True\u001b[m\r\n", "\u001b[31m- elif c == '>': # end of markup\u001b[m\r\n", "\u001b[32m+\u001b[m\u001b[32m elif c == '>' and not quote:\u001b[m\r\n", " tag = False\u001b[m\r\n", "\u001b[32m+\u001b[m\u001b[32m elif c == '\"' or c == \"'\" and tag:\u001b[m\r\n", "\u001b[32m+\u001b[m\u001b[32m quote = not quote\u001b[m\r\n", " elif not tag:\u001b[m\r\n", " out = out + c\u001b[m\r\n", " \u001b[m\r\n" ] } ], "source": [ "!git diff HEAD^" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Now that we have identified the failure-inducing change (\"something is wrong in `remove_html_markup()`, and it has to do with quote handling\"), we can end our `git bisect` session. `git bisect reset` gets us back to the start, such that we can fix the most recent version." ] }, { "cell_type": "code", "execution_count": 85, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:33.340027Z", "iopub.status.busy": "2024-01-17T21:32:33.339825Z", "iopub.status.idle": "2024-01-17T21:32:33.546551Z", "shell.execute_reply": "2024-01-17T21:32:33.546045Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Previous HEAD position was 298cda8 Second version\r\n", "HEAD is now at 971504e Eighth version (with proper assertion)\r\n" ] } ], "source": [ "!git bisect reset" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Automatic Bisecting" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Even though manual bisecting can be quick, we can speed things up by writing a _script_ that does the testing for us. With such a script, we can have `git bisect` run fully automatically." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "A test script to automate bisecting does the following:\n", "\n", "* It (re)builds the program under test for the given version\n", "* It tests whether the failure is present.\n", "\n", "Its exit code indicates the test outcome:\n", "\n", "* 0 means \"good\" (the failure did not occur)\n", "* 1 means \"bad\" (the failure did occur)\n", "* 125 means \"undetermined\" (we cannot decide if the failure is present or not)\n", "\n", "The latter (\"undetermined\") case may occur if the program fails to build, or shows some other behavior." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "We use a Python script `test.py` that reads in `remove_html_markup.py` and then tests for the presence or absence of the failure. (Since this is Python, we don't have to rebuild things.)" ] }, { "cell_type": "code", "execution_count": 86, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:33.549205Z", "iopub.status.busy": "2024-01-17T21:32:33.549026Z", "iopub.status.idle": "2024-01-17T21:32:33.551876Z", "shell.execute_reply": "2024-01-17T21:32:33.551568Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "# ignore\n", "open('test.py', 'w').write('''\n", "#!/usr/bin/env python\n", "\n", "from remove_html_markup import remove_html_markup\n", "import sys\n", "\n", "result = remove_html_markup('\"foo\"')\n", "if result == '\"foo\"':\n", " sys.exit(0) # good/pass\n", "elif result == 'foo':\n", " sys.exit(1) # bad/fail\n", "else:\n", " sys.exit(125) # unresolved\n", "''');" ] }, { "cell_type": "code", "execution_count": 87, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:33.553802Z", "iopub.status.busy": "2024-01-17T21:32:33.553661Z", "iopub.status.idle": "2024-01-17T21:32:33.592987Z", "shell.execute_reply": "2024-01-17T21:32:33.592693Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[37m#!/usr/bin/env python\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", "\u001b[34mfrom\u001b[39;49;00m \u001b[04m\u001b[36mremove_html_markup\u001b[39;49;00m \u001b[34mimport\u001b[39;49;00m remove_html_markup\u001b[37m\u001b[39;49;00m\n", "\u001b[34mimport\u001b[39;49;00m \u001b[04m\u001b[36msys\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", "result = remove_html_markup(\u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[33mfoo\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m)\u001b[37m\u001b[39;49;00m\n", "\u001b[34mif\u001b[39;49;00m result == \u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[33mfoo\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m:\u001b[37m\u001b[39;49;00m\n", " sys.exit(\u001b[34m0\u001b[39;49;00m) \u001b[37m# good/pass\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[34melif\u001b[39;49;00m result == \u001b[33m'\u001b[39;49;00m\u001b[33mfoo\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m:\u001b[37m\u001b[39;49;00m\n", " sys.exit(\u001b[34m1\u001b[39;49;00m) \u001b[37m# bad/fail\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[34melse\u001b[39;49;00m:\u001b[37m\u001b[39;49;00m\n", " sys.exit(\u001b[34m125\u001b[39;49;00m) \u001b[37m# unresolved\u001b[39;49;00m\u001b[37m\u001b[39;49;00m" ] } ], "source": [ "print_file('test.py')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Right now, we are with version 8 and thus in the \"failing\" state – our script exits with a code of 1:" ] }, { "cell_type": "code", "execution_count": 88, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:33.595080Z", "iopub.status.busy": "2024-01-17T21:32:33.594957Z", "iopub.status.idle": "2024-01-17T21:32:33.752930Z", "shell.execute_reply": "2024-01-17T21:32:33.752511Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1\r\n" ] } ], "source": [ "!python ./test.py; echo $?" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Let us use our test script to bisect automatically. As with manual bisecting, we first have to tell `git bisect` which the good (passing) and bad (failing) versions are:" ] }, { "cell_type": "code", "execution_count": 89, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:33.755030Z", "iopub.status.busy": "2024-01-17T21:32:33.754875Z", "iopub.status.idle": "2024-01-17T21:32:33.944200Z", "shell.execute_reply": "2024-01-17T21:32:33.943755Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "status: waiting for both good and bad commits\r\n" ] } ], "source": [ "!git bisect start" ] }, { "cell_type": "code", "execution_count": 90, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:33.946995Z", "iopub.status.busy": "2024-01-17T21:32:33.946798Z", "iopub.status.idle": "2024-01-17T21:32:34.147626Z", "shell.execute_reply": "2024-01-17T21:32:34.146950Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "status: waiting for bad commit, 1 good commit known\r\n" ] } ], "source": [ "!git bisect good {versions[0]}" ] }, { "cell_type": "code", "execution_count": 91, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:34.151294Z", "iopub.status.busy": "2024-01-17T21:32:34.150933Z", "iopub.status.idle": "2024-01-17T21:32:34.359814Z", "shell.execute_reply": "2024-01-17T21:32:34.359235Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Bisecting: 3 revisions left to test after this (roughly 2 steps)\r\n", "[753f07f1890200ce765c344761c32c1039a2d1e4] Fourth version (clueless)\r\n" ] } ], "source": [ "!git bisect bad {versions[7]}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Now, we can make use of our script. `git bisect run \n", " \n", "
\n", "

Quiz

\n", "

\n", "

What has changed in version 1 after applying the second patch?
\n", "

\n", "

\n", "

\n", " \n", " \n", "
\n", " \n", " \n", "
\n", " \n", " \n", "
\n", " \n", " \n", "
\n", " \n", "
\n", "

\n", " \n", " \n", "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 114, "metadata": {}, "output_type": "execute_result" } ], "source": [ "quiz(\"What has changed in version 1 after applying the second patch?\",\n", " [\n", " \"The initialization of quote is deleted\",\n", " \"The condition after `if c == '<'` is expanded\",\n", " \"The tag variable gets a different value\",\n", " \"None of the above\"\n", " ], '1 / 1 + 1 ** 1 - 1 % 1 * 1')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Delta Debugging on Patches\n", "\n", "With the ability to apply arbitrary sets of patches, we can now go and apply [Delta Debugging](DeltaDebugger.ipynb) to identify failure-inducing patches. The idea is simple: If applying _no_ patch makes the test pass, and applying _all_ patches makes the test fail, then we can use Delta Debugging to identify a minimal set of failure-inducing patches." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Let us write a testing function that checks for the presence of the failure. `test_remove_html_markup_patches()` \n", "\n", "* takes a list of patches,\n", "* applies them on version 1, \n", "* reads in the definition of `remove_html_markup()`\n", "* and then invokes it to check if the error is present." ] }, { "cell_type": "code", "execution_count": 115, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.550489Z", "iopub.status.busy": "2024-01-17T21:32:35.550401Z", "iopub.status.idle": "2024-01-17T21:32:35.552357Z", "shell.execute_reply": "2024-01-17T21:32:35.552120Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "def test_remove_html_markup_patches(patches: patch_obj) -> None:\n", " new_version = patch(version_1, patches)\n", " exec(new_version, globals())\n", " assert remove_html_markup('\"foo\"') == '\"foo\"'" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "If no patches are applied, we are at version 1, and the error is not present." ] }, { "cell_type": "code", "execution_count": 116, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.553928Z", "iopub.status.busy": "2024-01-17T21:32:35.553844Z", "iopub.status.idle": "2024-01-17T21:32:35.555565Z", "shell.execute_reply": "2024-01-17T21:32:35.555311Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "test_remove_html_markup_patches([])" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "If _all_ patches are applied, we are at version 2, and the error _is_ present." ] }, { "cell_type": "code", "execution_count": 117, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.557025Z", "iopub.status.busy": "2024-01-17T21:32:35.556941Z", "iopub.status.idle": "2024-01-17T21:32:35.558791Z", "shell.execute_reply": "2024-01-17T21:32:35.558559Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Traceback (most recent call last):\n", " File \"/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_46013/382405065.py\", line 2, in \n", " test_remove_html_markup_patches(patches)\n", " File \"/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_46013/1244207729.py\", line 4, in test_remove_html_markup_patches\n", " assert remove_html_markup('\"foo\"') == '\"foo\"'\n", "AssertionError (expected)\n" ] } ], "source": [ "with ExpectError(AssertionError):\n", " test_remove_html_markup_patches(patches)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### A Minimal Set of Patches\n", "\n", "We can now apply delta debugging on the list of patches, simply by invoking a `DeltaDebugger`:" ] }, { "cell_type": "code", "execution_count": 118, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.560215Z", "iopub.status.busy": "2024-01-17T21:32:35.560137Z", "iopub.status.idle": "2024-01-17T21:32:35.671291Z", "shell.execute_reply": "2024-01-17T21:32:35.671003Z" }, "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from DeltaDebugger import DeltaDebugger" ] }, { "cell_type": "code", "execution_count": 119, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.673418Z", "iopub.status.busy": "2024-01-17T21:32:35.673310Z", "iopub.status.idle": "2024-01-17T21:32:35.675043Z", "shell.execute_reply": "2024-01-17T21:32:35.674798Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "with DeltaDebugger() as dd:\n", " test_remove_html_markup_patches(patches)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "This is the minimal set of failure-inducing patches:" ] }, { "cell_type": "code", "execution_count": 120, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.676502Z", "iopub.status.busy": "2024-01-17T21:32:35.676402Z", "iopub.status.idle": "2024-01-17T21:32:35.679341Z", "shell.execute_reply": "2024-01-17T21:32:35.679119Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "reduced_patches = dd.min_args()['patches']" ] }, { "cell_type": "code", "execution_count": 121, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.680711Z", "iopub.status.busy": "2024-01-17T21:32:35.680617Z", "iopub.status.idle": "2024-01-17T21:32:35.736931Z", "shell.execute_reply": "2024-01-17T21:32:35.736680Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "@@ -\u001b[34m48\u001b[39;49;00m,\u001b[34m24\u001b[39;49;00m +\u001b[34m48\u001b[39;49;00m,\u001b[34m42\u001b[39;49;00m @@\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", "+ quote = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " out = \u001b[33m\"\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "@@ -\u001b[34m215\u001b[39;49;00m,\u001b[34m24\u001b[39;49;00m +\u001b[34m215\u001b[39;49;00m,\u001b[34m97\u001b[39;49;00m @@\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", "+ \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mor\u001b[39;49;00m c == \u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[35mnot\u001b[39;49;00m quote\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n" ] } ], "source": [ "for p in reduced_patches:\n", " print_patch(p)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "This is the resulting code. We see that the changes to the conditions (`if c == '<'` and `if c == '>'`) are not necessary to produce the failure – our test string has no HTML tags." ] }, { "cell_type": "code", "execution_count": 122, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.738591Z", "iopub.status.busy": "2024-01-17T21:32:35.738500Z", "iopub.status.idle": "2024-01-17T21:32:35.768278Z", "shell.execute_reply": "2024-01-17T21:32:35.768013Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[34mdef\u001b[39;49;00m \u001b[32mremove_html_markup\u001b[39;49;00m(s): \u001b[37m# type: ignore\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " out = \u001b[33m\"\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mfor\u001b[39;49;00m c \u001b[35min\u001b[39;49;00m s:\u001b[37m\u001b[39;49;00m\n", " \u001b[34mif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# start of markup\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mTrue\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m>\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# end of markup\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mor\u001b[39;49;00m c == \u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[35mnot\u001b[39;49;00m quote\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " out = out + c\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mreturn\u001b[39;49;00m out\u001b[37m\u001b[39;49;00m" ] } ], "source": [ "print_content(patch(version_1, reduced_patches), '.py')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Hence, we have now narrowed down our failure-inducing changes from four patches down to two." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### A Minimal Difference\n", "\n", "Can we narrow this down even further? Yes, we can! The idea is to not only search for the minimal set of patches to be applied on the passing version, but to actually seek a minimal _difference_ between two patch sets. That is, we obtain \n", "\n", "* one set of _passing_ patches that, when applied, still passes;\n", "* one set of _failing_ patches that, when applied, fails;\n", "\n", "with a _minimal difference_ between the two. This minimal difference is what causes the failure." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "We obtain such two sets (as well as their difference) by using the `min_arg_diff()` method of the `DeltaDebugger`." ] }, { "cell_type": "code", "execution_count": 123, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.770117Z", "iopub.status.busy": "2024-01-17T21:32:35.769977Z", "iopub.status.idle": "2024-01-17T21:32:35.773010Z", "shell.execute_reply": "2024-01-17T21:32:35.772726Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "pass_patches, fail_patches, diffs = \\\n", " tuple(arg['patches'] for arg in dd.min_arg_diff())" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "This is `remove_html_markup[)` with the passing set applied. We see that the variable `quote` is defined at the beginning. This definition is actually a precondition for other patches to result in an executable program; otherwise, the program will fail when the variable `quote` does not exist." ] }, { "cell_type": "code", "execution_count": 124, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.774632Z", "iopub.status.busy": "2024-01-17T21:32:35.774541Z", "iopub.status.idle": "2024-01-17T21:32:35.805188Z", "shell.execute_reply": "2024-01-17T21:32:35.804769Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[34mdef\u001b[39;49;00m \u001b[32mremove_html_markup\u001b[39;49;00m(s): \u001b[37m# type: ignore\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " out = \u001b[33m\"\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mfor\u001b[39;49;00m c \u001b[35min\u001b[39;49;00m s:\u001b[37m\u001b[39;49;00m\n", " \u001b[34mif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m quote:\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mTrue\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m>\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# end of markup\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " out = out + c\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mreturn\u001b[39;49;00m out\u001b[37m\u001b[39;49;00m" ] } ], "source": [ "print_content(patch(version_1, pass_patches), '.py')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Here's `remove_html_markup()` with the failing set applied. We see that now we also have the check for double quotes:" ] }, { "cell_type": "code", "execution_count": 125, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.807477Z", "iopub.status.busy": "2024-01-17T21:32:35.807349Z", "iopub.status.idle": "2024-01-17T21:32:35.838329Z", "shell.execute_reply": "2024-01-17T21:32:35.838037Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[34mdef\u001b[39;49;00m \u001b[32mremove_html_markup\u001b[39;49;00m(s): \u001b[37m# type: ignore\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " out = \u001b[33m\"\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mfor\u001b[39;49;00m c \u001b[35min\u001b[39;49;00m s:\u001b[37m\u001b[39;49;00m\n", " \u001b[34mif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m quote:\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mTrue\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m>\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# end of markup\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mor\u001b[39;49;00m c == \u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[35mnot\u001b[39;49;00m quote\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " out = out + c\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mreturn\u001b[39;49;00m out\u001b[37m\u001b[39;49;00m" ] } ], "source": [ "print_content(patch(version_1, fail_patches), '.py')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "The difference is just this one patch:" ] }, { "cell_type": "code", "execution_count": 126, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.840231Z", "iopub.status.busy": "2024-01-17T21:32:35.840128Z", "iopub.status.idle": "2024-01-17T21:32:35.870861Z", "shell.execute_reply": "2024-01-17T21:32:35.870556Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "@@ -\u001b[34m215\u001b[39;49;00m,\u001b[34m24\u001b[39;49;00m +\u001b[34m215\u001b[39;49;00m,\u001b[34m97\u001b[39;49;00m @@\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", "+ \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mor\u001b[39;49;00m c == \u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[35mnot\u001b[39;49;00m quote\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n" ] } ], "source": [ "for p in diffs:\n", " print_patch(p)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "And one more time, we are pointed to the buggy line that introduced the error." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## A ChangeDebugger class\n", "\n", "Let us put all these steps together in a single class. The `ChangeDebugger` is a derivative of `CallCollector` which takes two source files (one passing, one failing)." ] }, { "cell_type": "code", "execution_count": 127, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.872515Z", "iopub.status.busy": "2024-01-17T21:32:35.872426Z", "iopub.status.idle": "2024-01-17T21:32:35.874084Z", "shell.execute_reply": "2024-01-17T21:32:35.873829Z" }, "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from DeltaDebugger import CallCollector" ] }, { "cell_type": "code", "execution_count": 128, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.875568Z", "iopub.status.busy": "2024-01-17T21:32:35.875483Z", "iopub.status.idle": "2024-01-17T21:32:35.878200Z", "shell.execute_reply": "2024-01-17T21:32:35.877966Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class ChangeDebugger(CallCollector):\n", " def __init__(self, pass_source: str, fail_source: str, **ddargs: Any) -> None:\n", " \"\"\"Constructor. Takes a passing source file (`pass_source`)\n", " and a failing source file (`fail_source`).\n", " Additional arguments are passed to `DeltaDebugger` constructor.\n", " \"\"\"\n", " super().__init__()\n", " self._pass_source = pass_source\n", " self._fail_source = fail_source\n", " self._patches = diff(pass_source, fail_source)\n", " self._ddargs = ddargs\n", " self.log = ddargs['log'] if 'log' in ddargs else False\n", "\n", " def pass_source(self) -> str:\n", " \"\"\"Return the passing source file.\"\"\"\n", " return self._pass_source\n", "\n", " def fail_source(self) -> str:\n", " \"\"\"Return the failing source file.\"\"\"\n", " return self._fail_source\n", "\n", " def patches(self) -> List[patch_obj]:\n", " \"\"\"Return the diff between passing and failing source files.\"\"\"\n", " return self._patches" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "From `CallCollector`, it inherits the ability to inspect a single function call..." ] }, { "cell_type": "code", "execution_count": 129, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.879692Z", "iopub.status.busy": "2024-01-17T21:32:35.879593Z", "iopub.status.idle": "2024-01-17T21:32:35.881179Z", "shell.execute_reply": "2024-01-17T21:32:35.880929Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "def test_remove_html_markup() -> None:\n", " assert remove_html_markup('\"foo\"') == '\"foo\"'" ] }, { "cell_type": "code", "execution_count": 130, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.882540Z", "iopub.status.busy": "2024-01-17T21:32:35.882462Z", "iopub.status.idle": "2024-01-17T21:32:35.884217Z", "shell.execute_reply": "2024-01-17T21:32:35.883980Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "with ChangeDebugger(version_1, version_2) as cd:\n", " test_remove_html_markup()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "... and to repeat it at will." ] }, { "cell_type": "code", "execution_count": 131, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.885824Z", "iopub.status.busy": "2024-01-17T21:32:35.885716Z", "iopub.status.idle": "2024-01-17T21:32:35.887987Z", "shell.execute_reply": "2024-01-17T21:32:35.887627Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Traceback (most recent call last):\n", " File \"/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_46013/3359775007.py\", line 2, in \n", " cd.call()\n", " File \"/Users/zeller/Projects/debuggingbook/notebooks/DeltaDebugger.ipynb\", line 206, in call\n", " return self.function()(**args)\n", " File \"/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_46013/3114633459.py\", line 2, in test_remove_html_markup\n", " assert remove_html_markup('\"foo\"') == '\"foo\"'\n", "AssertionError (expected)\n" ] } ], "source": [ "with ExpectError(AssertionError):\n", " cd.call()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We can access the passing source, the failing source, and the list of patches:" ] }, { "cell_type": "code", "execution_count": 132, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.889531Z", "iopub.status.busy": "2024-01-17T21:32:35.889417Z", "iopub.status.idle": "2024-01-17T21:32:35.919981Z", "shell.execute_reply": "2024-01-17T21:32:35.919713Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[34mdef\u001b[39;49;00m \u001b[32mremove_html_markup\u001b[39;49;00m(s): \u001b[37m# type: ignore\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " out = \u001b[33m\"\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mfor\u001b[39;49;00m c \u001b[35min\u001b[39;49;00m s:\u001b[37m\u001b[39;49;00m\n", " \u001b[34mif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# start of markup\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mTrue\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m>\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# end of markup\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " out = out + c\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mreturn\u001b[39;49;00m out\u001b[37m\u001b[39;49;00m" ] } ], "source": [ "print_content(cd.pass_source(), '.py')" ] }, { "cell_type": "code", "execution_count": 133, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.921673Z", "iopub.status.busy": "2024-01-17T21:32:35.921568Z", "iopub.status.idle": "2024-01-17T21:32:35.952416Z", "shell.execute_reply": "2024-01-17T21:32:35.952067Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[34mdef\u001b[39;49;00m \u001b[32mremove_html_markup\u001b[39;49;00m(s): \u001b[37m# type: ignore\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " out = \u001b[33m\"\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mfor\u001b[39;49;00m c \u001b[35min\u001b[39;49;00m s:\u001b[37m\u001b[39;49;00m\n", " \u001b[34mif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m quote:\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mTrue\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m>\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m quote:\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mor\u001b[39;49;00m c == \u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[35mnot\u001b[39;49;00m quote\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " out = out + c\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mreturn\u001b[39;49;00m out\u001b[37m\u001b[39;49;00m" ] } ], "source": [ "print_content(cd.fail_source(), '.py')" ] }, { "cell_type": "code", "execution_count": 134, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.954757Z", "iopub.status.busy": "2024-01-17T21:32:35.954609Z", "iopub.status.idle": "2024-01-17T21:32:35.956735Z", "shell.execute_reply": "2024-01-17T21:32:35.956476Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "data": { "text/plain": [ "[,\n", " ,\n", " ,\n", " ]" ] }, "execution_count": 134, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cd.patches()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "For testing, we do not apply Delta Debugging on the function under test – that would reduce the function's arguments. (It would still be neat, but that's not what we're aiming for here.)\n", "\n", "Instead, we introduce a method `test_patches()` that gets a set of patches, applies them, reads in the resulting source, and (re-)calls the function." ] }, { "cell_type": "code", "execution_count": 135, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.958704Z", "iopub.status.busy": "2024-01-17T21:32:35.958595Z", "iopub.status.idle": "2024-01-17T21:32:35.960551Z", "shell.execute_reply": "2024-01-17T21:32:35.960302Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class ChangeDebugger(ChangeDebugger):\n", " def test_patches(self, patches: List[patch_obj]) -> None:\n", " new_version = patch(self.pass_source(), patches)\n", " exec(new_version, globals())\n", " self.call()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "For completeness, we ensure that at the beginning of the `with` block, we assume the failing source:" ] }, { "cell_type": "code", "execution_count": 136, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.962228Z", "iopub.status.busy": "2024-01-17T21:32:35.962107Z", "iopub.status.idle": "2024-01-17T21:32:35.964070Z", "shell.execute_reply": "2024-01-17T21:32:35.963791Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "class ChangeDebugger(ChangeDebugger):\n", " def __enter__(self) -> Any:\n", " \"\"\"Called at begin of a `with` block. Checks if current source fails.\"\"\"\n", " exec(self.fail_source(), globals())\n", " return super().__enter__()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Here's `test_patches()` in action. As before, if we apply no patches, the test passes; if we apply all patches, it fails." ] }, { "cell_type": "code", "execution_count": 137, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.965979Z", "iopub.status.busy": "2024-01-17T21:32:35.965859Z", "iopub.status.idle": "2024-01-17T21:32:35.967874Z", "shell.execute_reply": "2024-01-17T21:32:35.967594Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "with ChangeDebugger(version_1, version_2) as cd:\n", " test_remove_html_markup()" ] }, { "cell_type": "code", "execution_count": 138, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.969302Z", "iopub.status.busy": "2024-01-17T21:32:35.969197Z", "iopub.status.idle": "2024-01-17T21:32:35.970836Z", "shell.execute_reply": "2024-01-17T21:32:35.970587Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "cd.test_patches([])" ] }, { "cell_type": "code", "execution_count": 139, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.972317Z", "iopub.status.busy": "2024-01-17T21:32:35.972203Z", "iopub.status.idle": "2024-01-17T21:32:35.974266Z", "shell.execute_reply": "2024-01-17T21:32:35.973973Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Traceback (most recent call last):\n", " File \"/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_46013/3279782181.py\", line 2, in \n", " cd.test_patches(cd.patches())\n", " File \"/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_46013/1738176692.py\", line 5, in test_patches\n", " self.call()\n", " File \"/Users/zeller/Projects/debuggingbook/notebooks/DeltaDebugger.ipynb\", line 206, in call\n", " return self.function()(**args)\n", " File \"/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_46013/3114633459.py\", line 2, in test_remove_html_markup\n", " assert remove_html_markup('\"foo\"') == '\"foo\"'\n", "AssertionError (expected)\n" ] } ], "source": [ "with ExpectError(AssertionError):\n", " cd.test_patches(cd.patches())" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Now for the actual debugging. We introduce a method `min_patches()` in which we invoke a `DeltaDebugger` on the `test_patches()` method we just defined. Using `min_arg.diff()`, it then computes a minimal failure-inducing difference between the patches. " ] }, { "cell_type": "code", "execution_count": 140, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.976159Z", "iopub.status.busy": "2024-01-17T21:32:35.976026Z", "iopub.status.idle": "2024-01-17T21:32:35.978579Z", "shell.execute_reply": "2024-01-17T21:32:35.978312Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class ChangeDebugger(ChangeDebugger):\n", " def min_patches(self) -> Tuple[List[patch_obj], List[patch_obj], List[patch_obj]]:\n", " \"\"\"\n", " Compute a minimal set of patches.\n", " Returns a triple (`pass_patches`, `fail_patches`, `diff_patches`) \n", " where `diff_patches` is the minimal difference between \n", " the set `pass_patches` (which, when applied, make the test pass) and \n", " the set `fail_patches` (which, when applied, make the test fail).\n", " \"\"\"\n", " patches = self.patches()\n", " with DeltaDebugger(**self._ddargs) as dd:\n", " self.test_patches(patches)\n", "\n", " args = dd.min_arg_diff()\n", " pass_patches = args[0]['patches']\n", " fail_patches = args[1]['patches']\n", " diff_patches = args[2]['patches']\n", "\n", " return (pass_patches, fail_patches, diff_patches)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "The `__repr__()` method, turning a `ChangeDebugger` into a readable string, computes `min_patches()` and returns a user-readable representation of the patches:" ] }, { "cell_type": "code", "execution_count": 141, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.980334Z", "iopub.status.busy": "2024-01-17T21:32:35.980216Z", "iopub.status.idle": "2024-01-17T21:32:35.982439Z", "shell.execute_reply": "2024-01-17T21:32:35.981905Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "class ChangeDebugger(ChangeDebugger):\n", " def __repr__(self) -> str:\n", " \"\"\"Return readable list of minimal patches\"\"\"\n", " pass_patches, fail_patches, diff_patches = self.min_patches()\n", " return \"\".join(patch_string(p) for p in diff_patches)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Here's how to use these methods. First, we invoke `ChangeDebugger` with the given old and new source code versions on our test function." ] }, { "cell_type": "code", "execution_count": 142, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.984817Z", "iopub.status.busy": "2024-01-17T21:32:35.984665Z", "iopub.status.idle": "2024-01-17T21:32:35.986585Z", "shell.execute_reply": "2024-01-17T21:32:35.986327Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "with ChangeDebugger(version_1, version_2) as cd:\n", " test_remove_html_markup()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "These are the patches we determined:" ] }, { "cell_type": "code", "execution_count": 143, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.988093Z", "iopub.status.busy": "2024-01-17T21:32:35.987994Z", "iopub.status.idle": "2024-01-17T21:32:35.990007Z", "shell.execute_reply": "2024-01-17T21:32:35.989745Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "data": { "text/plain": [ "[,\n", " ,\n", " ,\n", " ]" ] }, "execution_count": 143, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cd.patches()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We invoke `min_patches` to obtain a minimal set of patches:" ] }, { "cell_type": "code", "execution_count": 144, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.991492Z", "iopub.status.busy": "2024-01-17T21:32:35.991391Z", "iopub.status.idle": "2024-01-17T21:32:35.994715Z", "shell.execute_reply": "2024-01-17T21:32:35.994452Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "[]" ] }, "execution_count": 144, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pass_patches, fail_patches, diffs = cd.min_patches()\n", "diffs" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We can inspect the failure-inducing patches right away:" ] }, { "cell_type": "code", "execution_count": 145, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.996205Z", "iopub.status.busy": "2024-01-17T21:32:35.996124Z", "iopub.status.idle": "2024-01-17T21:32:35.997830Z", "shell.execute_reply": "2024-01-17T21:32:35.997571Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "@@ -215,24 +215,97 @@\n", " tag = False\n", "\n", "+ elif c == '\"' or c == \"'\" and tag:\n", " quote = not quote\n", "\n", " elif\n" ] } ], "source": [ "print(patch_string(diffs[0]))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Or – even simpler – we can simply print out the `ChangeDebugger` to obtain a readable representation." ] }, { "cell_type": "code", "execution_count": 146, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:35.999699Z", "iopub.status.busy": "2024-01-17T21:32:35.999619Z", "iopub.status.idle": "2024-01-17T21:32:36.002892Z", "shell.execute_reply": "2024-01-17T21:32:36.002654Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "data": { "text/plain": [ "@@ -215,24 +215,97 @@\n", " tag = False\n", "\n", "+ elif c == '\"' or c == \"'\" and tag:\n", " quote = not quote\n", "\n", " elif" ] }, "execution_count": 146, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cd" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Success!" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Does this also work for longer change histories? Let's take the very first and the very last version." ] }, { "cell_type": "code", "execution_count": 147, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.004668Z", "iopub.status.busy": "2024-01-17T21:32:36.004563Z", "iopub.status.idle": "2024-01-17T21:32:36.020086Z", "shell.execute_reply": "2024-01-17T21:32:36.019636Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "version_8 = get_output(['git', 'show', \n", " f'{versions[7]}:remove_html_markup.py'])" ] }, { "cell_type": "code", "execution_count": 148, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.022064Z", "iopub.status.busy": "2024-01-17T21:32:36.021917Z", "iopub.status.idle": "2024-01-17T21:32:36.024202Z", "shell.execute_reply": "2024-01-17T21:32:36.023949Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "with ChangeDebugger(version_1, version_8) as cd:\n", " test_remove_html_markup()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "We start with 5 patches:" ] }, { "cell_type": "code", "execution_count": 149, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.025968Z", "iopub.status.busy": "2024-01-17T21:32:36.025791Z", "iopub.status.idle": "2024-01-17T21:32:36.028593Z", "shell.execute_reply": "2024-01-17T21:32:36.028139Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "5" ] }, "execution_count": 149, "metadata": {}, "output_type": "execute_result" } ], "source": [ "len(cd.patches())" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Printing out the debugger again reveals a single failure-inducing change – it's actually still the same." ] }, { "cell_type": "code", "execution_count": 150, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.030362Z", "iopub.status.busy": "2024-01-17T21:32:36.030236Z", "iopub.status.idle": "2024-01-17T21:32:36.034029Z", "shell.execute_reply": "2024-01-17T21:32:36.033599Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "@@ -215,24 +215,97 @@\n", " tag = False\n", "\n", "+ elif c == '\"' or c == \"'\" and tag:\n", " quote = not quote\n", "\n", " elif" ] }, "execution_count": 150, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cd" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We close with a bit of housekeeping, ensuring that the preconditions are properly met." ] }, { "cell_type": "code", "execution_count": 151, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.035865Z", "iopub.status.busy": "2024-01-17T21:32:36.035763Z", "iopub.status.idle": "2024-01-17T21:32:36.037645Z", "shell.execute_reply": "2024-01-17T21:32:36.037152Z" }, "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from DeltaDebugger import NoCallError, NotFailingError" ] }, { "cell_type": "code", "execution_count": 152, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.039711Z", "iopub.status.busy": "2024-01-17T21:32:36.039602Z", "iopub.status.idle": "2024-01-17T21:32:36.041301Z", "shell.execute_reply": "2024-01-17T21:32:36.041054Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class NotPassingError(ValueError):\n", " pass" ] }, { "cell_type": "code", "execution_count": 153, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.042740Z", "iopub.status.busy": "2024-01-17T21:32:36.042656Z", "iopub.status.idle": "2024-01-17T21:32:36.045291Z", "shell.execute_reply": "2024-01-17T21:32:36.045023Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class ChangeDebugger(ChangeDebugger):\n", " def after_collection(self) -> None:\n", " \"\"\"Diagnostics.\"\"\"\n", " if self.function() is None:\n", " raise NoCallError(\"No function call observed\")\n", " if self.exception() is None:\n", " raise NotFailingError(f\"{self.format_call()} did not raise an exception\")\n", "\n", " try:\n", " self.test_patches([])\n", " except Exception:\n", " raise NotPassingError(f\"{self.format_call()} raised an exception in its passing version\")\n", "\n", " try:\n", " self.test_patches(self.patches())\n", " raise NotFailingError(f\"{self.format_call()} did not raise an exception in failing version\")\n", " except Exception:\n", " pass\n", "\n", " if self.log:\n", " print(f\"Observed {self.format_call()}\" +\n", " f\" raising {self.format_exception(self.exception())}\") " ] }, { "cell_type": "code", "execution_count": 154, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.046846Z", "iopub.status.busy": "2024-01-17T21:32:36.046744Z", "iopub.status.idle": "2024-01-17T21:32:36.049341Z", "shell.execute_reply": "2024-01-17T21:32:36.048993Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "with ExpectError(NotPassingError):\n", " with ChangeDebugger(version_1, version_2) as cd:\n", " test_remove_html_markup()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "With this, we're done. Enjoy debugging changes!" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Synopsis" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "This chapter introduces a class `ChangeDebugger` that automatically determines failure-inducing code changes." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### High-Level Interface\n", "\n", "You are given two Python source codes `source_pass` and `source_fail`, and a function `test()` that works using the definitions in `source_pass`, but raises an exception using the definitions in `source_fail`. Then, you can use `ChangeDebugger` as follows:\n", "\n", "```python\n", "with ChangeDebugger(source_pass, source_fail) as cd:\n", " test()\n", "cd\n", "```\n", "\n", "This will produce the failure-inducing change between `source_pass` and `source_fail`, using [Delta Debugging](DeltaDebugger.ipynb) to determine minimal differences in patches applied." ] }, { "cell_type": "code", "execution_count": 155, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.051457Z", "iopub.status.busy": "2024-01-17T21:32:36.051325Z", "iopub.status.idle": "2024-01-17T21:32:36.053028Z", "shell.execute_reply": "2024-01-17T21:32:36.052766Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "# ignore\n", "source_pass = version_1\n", "source_fail = version_2" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Here is an example. The function `test()` passes (raises no exception) if `remove_html_markup()` is defined as follows:" ] }, { "cell_type": "code", "execution_count": 156, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.054583Z", "iopub.status.busy": "2024-01-17T21:32:36.054479Z", "iopub.status.idle": "2024-01-17T21:32:36.086046Z", "shell.execute_reply": "2024-01-17T21:32:36.085733Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[34mdef\u001b[39;49;00m \u001b[32mremove_html_markup\u001b[39;49;00m(s): \u001b[37m# type: ignore\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " out = \u001b[33m\"\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mfor\u001b[39;49;00m c \u001b[35min\u001b[39;49;00m s:\u001b[37m\u001b[39;49;00m\n", " \u001b[34mif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# start of markup\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mTrue\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m>\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# end of markup\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " out = out + c\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mreturn\u001b[39;49;00m out\u001b[37m\u001b[39;49;00m" ] } ], "source": [ "print_content(source_pass, '.py')" ] }, { "cell_type": "code", "execution_count": 157, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.087838Z", "iopub.status.busy": "2024-01-17T21:32:36.087708Z", "iopub.status.idle": "2024-01-17T21:32:36.089680Z", "shell.execute_reply": "2024-01-17T21:32:36.089381Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "def test() -> None:\n", " assert remove_html_markup('\"foo\"') == '\"foo\"'" ] }, { "cell_type": "code", "execution_count": 158, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.091260Z", "iopub.status.busy": "2024-01-17T21:32:36.091155Z", "iopub.status.idle": "2024-01-17T21:32:36.092832Z", "shell.execute_reply": "2024-01-17T21:32:36.092480Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "exec(source_pass)\n", "test()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "If `remove_html_markup()` is changed as follows, though, then\n", "`test()` raises an exception and fails:" ] }, { "cell_type": "code", "execution_count": 159, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.094591Z", "iopub.status.busy": "2024-01-17T21:32:36.094479Z", "iopub.status.idle": "2024-01-17T21:32:36.125622Z", "shell.execute_reply": "2024-01-17T21:32:36.125345Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[34mdef\u001b[39;49;00m \u001b[32mremove_html_markup\u001b[39;49;00m(s): \u001b[37m# type: ignore\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " out = \u001b[33m\"\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mfor\u001b[39;49;00m c \u001b[35min\u001b[39;49;00m s:\u001b[37m\u001b[39;49;00m\n", " \u001b[34mif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m quote:\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mTrue\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m>\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m quote:\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mor\u001b[39;49;00m c == \u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[35mnot\u001b[39;49;00m quote\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " out = out + c\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mreturn\u001b[39;49;00m out\u001b[37m\u001b[39;49;00m" ] } ], "source": [ "print_content(source_fail, '.py')" ] }, { "cell_type": "code", "execution_count": 160, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.127337Z", "iopub.status.busy": "2024-01-17T21:32:36.127247Z", "iopub.status.idle": "2024-01-17T21:32:36.129309Z", "shell.execute_reply": "2024-01-17T21:32:36.129021Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Traceback (most recent call last):\n", " File \"/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_46013/4262003862.py\", line 3, in \n", " test()\n", " File \"/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_46013/3045937450.py\", line 2, in test\n", " assert remove_html_markup('\"foo\"') == '\"foo\"'\n", "AssertionError (expected)\n" ] } ], "source": [ "exec(source_fail)\n", "with ExpectError(AssertionError):\n", " test()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We can use `ChangeDebugger` to automatically identify the failure-inducing difference:" ] }, { "cell_type": "code", "execution_count": 161, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.130859Z", "iopub.status.busy": "2024-01-17T21:32:36.130753Z", "iopub.status.idle": "2024-01-17T21:32:36.134509Z", "shell.execute_reply": "2024-01-17T21:32:36.134245Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "data": { "text/plain": [ "@@ -215,24 +215,97 @@\n", " tag = False\n", "\n", "+ elif c == '\"' or c == \"'\" and tag:\n", " quote = not quote\n", "\n", " elif" ] }, "execution_count": 161, "metadata": {}, "output_type": "execute_result" } ], "source": [ "with ChangeDebugger(source_pass, source_fail) as cd:\n", " test()\n", "cd" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "The lines prefixed with `+` from are the ones in `source_fail` that cause the failure when added. (They also are the ones that should be fixed.)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Programmatic Interface\n", "\n", "For more details or more automation, use the programmatic interface. The method `min_patches()` returns a triple (`pass_patches`, `fail_patches`, `diffs`) where\n", "\n", "* applying `pass_patches` still make the call pass\n", "* applying `fail_patches` causes the call to fail\n", "* `diffs` is the (minimal) difference between the two.\n", "\n", "The patches come as list of `patch_obj` objects, as defined by Google's [diff-match-patch library](https://github.com/google/diff-match-patch)." ] }, { "cell_type": "code", "execution_count": 162, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.136148Z", "iopub.status.busy": "2024-01-17T21:32:36.136067Z", "iopub.status.idle": "2024-01-17T21:32:36.138967Z", "shell.execute_reply": "2024-01-17T21:32:36.138692Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "pass_patches, fail_patches, diffs = cd.min_patches()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "One can apply all patches in `pass_patches` and still not cause the test to fail:" ] }, { "cell_type": "code", "execution_count": 163, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.140462Z", "iopub.status.busy": "2024-01-17T21:32:36.140378Z", "iopub.status.idle": "2024-01-17T21:32:36.197079Z", "shell.execute_reply": "2024-01-17T21:32:36.196773Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "@@ -\u001b[34m48\u001b[39;49;00m,\u001b[34m24\u001b[39;49;00m +\u001b[34m48\u001b[39;49;00m,\u001b[34m42\u001b[39;49;00m @@\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", "+ quote = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " out = \u001b[33m\"\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "@@ -\u001b[34m104\u001b[39;49;00m,\u001b[34m50\u001b[39;49;00m +\u001b[34m104\u001b[39;49;00m,\u001b[34m43\u001b[39;49;00m @@\u001b[37m\u001b[39;49;00m\n", " s:\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", "- \u001b[34mif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# start of markup\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", "+ \u001b[34mif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m quote:\u001b[37m\u001b[39;49;00m\n" ] } ], "source": [ "for p in pass_patches:\n", " print_patch(p)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "However, as soon as we also apply the patches in `diffs`, we get the failure. (This is also what is shown when we output a `ChangeDebugger`.)" ] }, { "cell_type": "code", "execution_count": 164, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.198816Z", "iopub.status.busy": "2024-01-17T21:32:36.198701Z", "iopub.status.idle": "2024-01-17T21:32:36.227481Z", "shell.execute_reply": "2024-01-17T21:32:36.227239Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "@@ -\u001b[34m215\u001b[39;49;00m,\u001b[34m24\u001b[39;49;00m +\u001b[34m215\u001b[39;49;00m,\u001b[34m97\u001b[39;49;00m @@\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", "+ \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mor\u001b[39;49;00m c == \u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[35mnot\u001b[39;49;00m quote\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n" ] } ], "source": [ "for p in diffs:\n", " print_patch(p)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "The full set of methods in `ChangeDebugger` is shown below." ] }, { "cell_type": "code", "execution_count": 165, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.229017Z", "iopub.status.busy": "2024-01-17T21:32:36.228938Z", "iopub.status.idle": "2024-01-17T21:32:36.230501Z", "shell.execute_reply": "2024-01-17T21:32:36.230275Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "# ignore\n", "from ClassDiagram import display_class_hierarchy" ] }, { "cell_type": "code", "execution_count": 166, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.232359Z", "iopub.status.busy": "2024-01-17T21:32:36.232264Z", "iopub.status.idle": "2024-01-17T21:32:36.742402Z", "shell.execute_reply": "2024-01-17T21:32:36.742041Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "ChangeDebugger\n", "\n", "\n", "ChangeDebugger\n", "\n", "\n", "\n", "__enter__()\n", "\n", "\n", "\n", "__init__()\n", "\n", "\n", "\n", "__repr__()\n", "\n", "\n", "\n", "fail_source()\n", "\n", "\n", "\n", "min_patches()\n", "\n", "\n", "\n", "pass_source()\n", "\n", "\n", "\n", "patches()\n", "\n", "\n", "\n", "after_collection()\n", "\n", "\n", "\n", "test_patches()\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "CallCollector\n", "\n", "\n", "CallCollector\n", "\n", "\n", "\n", "__enter__()\n", "\n", "\n", "\n", "__exit__()\n", "\n", "\n", "\n", "__init__()\n", "\n", "\n", "\n", "args()\n", "\n", "\n", "\n", "call()\n", "\n", "\n", "\n", "exception()\n", "\n", "\n", "\n", "function()\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "ChangeDebugger->CallCollector\n", "\n", "\n", "\n", "\n", "\n", "StackInspector\n", "\n", "\n", "StackInspector\n", "\n", "\n", "\n", "_generated_function_cache\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "CallCollector->StackInspector\n", "\n", "\n", "\n", "\n", "\n", "Legend\n", "Legend\n", "• \n", "public_method()\n", "• \n", "private_method()\n", "• \n", "overloaded_method()\n", "Hover over names to see doc\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 166, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# ignore\n", "display_class_hierarchy([ChangeDebugger],\n", " public_methods=[\n", " CallCollector.__init__,\n", " CallCollector.__enter__,\n", " CallCollector.__exit__,\n", " CallCollector.call, # type: ignore\n", " CallCollector.args,\n", " CallCollector.function,\n", " CallCollector.exception,\n", " ChangeDebugger.__init__,\n", " ChangeDebugger.min_patches,\n", " ChangeDebugger.patches,\n", " ChangeDebugger.pass_source,\n", " ChangeDebugger.fail_source,\n", " ChangeDebugger.__repr__,\n", " ChangeDebugger.__enter__\n", " ],\n", " project='debuggingbook')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Supporting Functions" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "`ChangeDebugger` relies on lower level `patch()` and `diff()` functions.\n", "\n", "To apply patch objects on source code, use the `patch()` function. It takes a source code and a list of patches to be applied." ] }, { "cell_type": "code", "execution_count": 167, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.744973Z", "iopub.status.busy": "2024-01-17T21:32:36.744592Z", "iopub.status.idle": "2024-01-17T21:32:36.779621Z", "shell.execute_reply": "2024-01-17T21:32:36.779156Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[34mdef\u001b[39;49;00m \u001b[32mremove_html_markup\u001b[39;49;00m(s): \u001b[37m# type: ignore\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " out = \u001b[33m\"\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mfor\u001b[39;49;00m c \u001b[35min\u001b[39;49;00m s:\u001b[37m\u001b[39;49;00m\n", " \u001b[34mif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# start of markup\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mTrue\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m>\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# end of markup\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mor\u001b[39;49;00m c == \u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[35mnot\u001b[39;49;00m quote\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " out = out + c\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34mreturn\u001b[39;49;00m out\u001b[37m\u001b[39;49;00m" ] } ], "source": [ "print_content(patch(source_pass, diffs), '.py')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Conversely, the `diff()` function computes patches between two texts. It returns a list of patch objects that can be applied on text." ] }, { "cell_type": "code", "execution_count": 168, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.781487Z", "iopub.status.busy": "2024-01-17T21:32:36.781338Z", "iopub.status.idle": "2024-01-17T21:32:36.908421Z", "shell.execute_reply": "2024-01-17T21:32:36.908144Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "@@ -\u001b[34m48\u001b[39;49;00m,\u001b[34m24\u001b[39;49;00m +\u001b[34m48\u001b[39;49;00m,\u001b[34m42\u001b[39;49;00m @@\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", "+ quote = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " out = \u001b[33m\"\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "@@ -\u001b[34m104\u001b[39;49;00m,\u001b[34m50\u001b[39;49;00m +\u001b[34m104\u001b[39;49;00m,\u001b[34m43\u001b[39;49;00m @@\u001b[37m\u001b[39;49;00m\n", " s:\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", "- \u001b[34mif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# start of markup\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", "+ \u001b[34mif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m<\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m quote:\u001b[37m\u001b[39;49;00m\n", "@@ -\u001b[34m162\u001b[39;49;00m,\u001b[34m48\u001b[39;49;00m +\u001b[34m162\u001b[39;49;00m,\u001b[34m45\u001b[39;49;00m @@\u001b[37m\u001b[39;49;00m\n", " rue\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", "- \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m>\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m: \u001b[37m# end of markup\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", "+ \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m>\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m \u001b[35mnot\u001b[39;49;00m quote:\u001b[37m\u001b[39;49;00m\n", "@@ -\u001b[34m215\u001b[39;49;00m,\u001b[34m24\u001b[39;49;00m +\u001b[34m215\u001b[39;49;00m,\u001b[34m97\u001b[39;49;00m @@\u001b[37m\u001b[39;49;00m\n", " tag = \u001b[34mFalse\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", "+ \u001b[34melif\u001b[39;49;00m c == \u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m \u001b[35mor\u001b[39;49;00m c == \u001b[33m\"\u001b[39;49;00m\u001b[33m'\u001b[39;49;00m\u001b[33m\"\u001b[39;49;00m \u001b[35mand\u001b[39;49;00m tag:\u001b[37m\u001b[39;49;00m\n", " quote = \u001b[35mnot\u001b[39;49;00m quote\u001b[37m\u001b[39;49;00m\n", "\u001b[37m\u001b[39;49;00m\n", " \u001b[34melif\u001b[39;49;00m\u001b[37m\u001b[39;49;00m\n" ] } ], "source": [ "for p in diff(source_pass, source_fail):\n", " print_patch(p)" ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": true, "run_control": { "read_only": false }, "slideshow": { "slide_type": "slide" } }, "source": [ "## Lessons Learned\n", "\n", "* If one has a version history available, one can use it to determine failure-inducing changes.\n", "* Bisecting with git effectively isolates failure-inducing commits, both manually and automatically.\n", "* Delta debugging on changes allows further isolating minimal sets of failure-inducing changes." ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "slide" } }, "source": [ "## Next Steps\n", "\n", "This concludes our applications of Delta Debugging. In the next chapters, we will analyze how to abstract failure-inducing inputs into failure-inducing _input sets_." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Background\n", "\n", "The concept of \"bisecting\" as traversing a version history to identify failure-inducing changes was first described by Brian Ness and Viet Ngo from Cray Research as \"Source change isolation\" \\cite{Ness1997}. The abstract of their paper summarizes the goals and benefits:\n", "\n", "> Effective regression containment is an important factor in the design of development and testing processes for large software projects, especially when many developers are doing concurrent work on a common set of sources. Source change isolation provides an inexpensive, mechanical alternative to analytical methods for identifying the cause of software regressions. It also provides the advantage of enabling regressions to be eliminated by reversing the effect of source changes that introduced errant behavior, without the need to write new code, and without halting other development work on the same software. Deliverability is also improved.\n", "\n", "Delta Debugging on changes (and also Delta Debugging) was introduced in \"Yesterday, my program worked. Today, it does not. Why?\" \\cite{Zeller1999}, a paper which would win an ACM SIGSOFT Impact Paper Award ten years later.\n", "This paper generalized over \\cite{Ness1997} by identifying failure-inducing differences in arbitrary sets of changes. \\cite{Zeller2002} generalized the algorithm to work on inputs and other collections (including changes); the [chapter on delta debugging](DeltaDebugger), which we use in this chapter, uses the \\cite{Zeller2002} formulation of the `dd` algorithm." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "We're done, so we clean up a bit:" ] }, { "cell_type": "code", "execution_count": 169, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.929999Z", "iopub.status.busy": "2024-01-17T21:32:36.929830Z", "iopub.status.idle": "2024-01-17T21:32:36.932106Z", "shell.execute_reply": "2024-01-17T21:32:36.931695Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "try:\n", " shutil.rmtree(PROJECT)\n", "except FileNotFoundError:\n", " pass" ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": true, "run_control": { "read_only": false }, "slideshow": { "slide_type": "slide" } }, "source": [ "## Exercises\n" ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "subslide" }, "solution2": "hidden", "solution2_first": true }, "source": [ "### Exercise 1: Fine-Grained Changes\n", "\n", "Instead of computing patches on lines (as we and most `diff` programs do), one can also compute fine-grained diffs, using our `diff()` function with `mode='chars'`. Would this make sense?" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "source": [ "**Solution.** Let us see what happens if we compute a character `diff` on our two versions:" ] }, { "cell_type": "code", "execution_count": 170, "metadata": { "cell_style": "split", "execution": { "iopub.execute_input": "2024-01-17T21:32:36.934032Z", "iopub.status.busy": "2024-01-17T21:32:36.933882Z", "iopub.status.idle": "2024-01-17T21:32:36.938639Z", "shell.execute_reply": "2024-01-17T21:32:36.938291Z" }, "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "outputs": [], "source": [ "patches = diff(version_1, version_2, mode='chars')" ] }, { "cell_type": "code", "execution_count": 171, "metadata": { "execution": { "iopub.execute_input": "2024-01-17T21:32:36.940190Z", "iopub.status.busy": "2024-01-17T21:32:36.940095Z", "iopub.status.idle": "2024-01-17T21:32:36.942105Z", "shell.execute_reply": "2024-01-17T21:32:36.941837Z" }, "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "@@ -52,24 +52,42 @@\n", " = False\n", " \n", "+quote = False\n", " \n", " out = \"\"\n", "\n", "\n", "@@ -123,30 +123,23 @@\n", " '<'\n", "-:\n", " \n", "- # st\n", " a\n", "-rt\n", "+nd\n", " \n", "+n\n", " o\n", "-f\n", "+t\n", " \n", "-mark\n", "+q\n", " u\n", "-p\n", "+ote:\n", " \n", "\n", "@@ -183,26 +183,23 @@\n", " '>'\n", "-: #\n", " \n", "-e\n", "+a\n", " nd \n", "+n\n", " o\n", "-f\n", "+t\n", " \n", "-mark\n", "+q\n", " u\n", "-p\n", "+ote:\n", " \n", "\n", "@@ -213,24 +213,97 @@\n", " tag = Fals\n", "+e\n", " elif c == '\"' or c == \"'\" and tag:\n", " quote = not quot\n", " e\n", " el\n" ] } ], "source": [ "for p in patches:\n", " print(patch_string(p))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "source": [ "We see that the patches now replace words like `start` to `and`, keeping the `a` common to both words. Such tiny differences may be useful for merging changes in text documents (which is what the Google library was built for), but not necessarily code." ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "subslide" } }, "source": [ "### Exercise 2: Failure-Inducing Changes in the Large\n", "\n", "Our `ChangeDebugger` class works on two versions of one class, function, or module, but not on versions of an entire project. Extend `ChangeDebugger` to a class `DiffDebugger` that can actually take two _directories_ (including differences in subdirectories with added and deleted files) and determine failure-inducing differences between them. This would allow checking out two versions of a project and determine failure-inducing differences.\n", "\n", "Note that applying delte debugging on large sets of differences is a time-consuming task, since \n", "\n", "* Every test requires a rebuild and rerun of the code; and\n", "* There are many ways to cause inconsistencies by applying random changes.\n", "\n", "Refer to \\cite{Zeller1999} for some hints on how to make this efficient." ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "subslide" } }, "source": [ "### Exercise 3: Hierarchical Change Debugging\n", "\n", "Rather than running Delta Debugging on _all_ changes between two projects (as with `DiffDebugger`, above), it can be wise to first progress along the change history (as with `git bisect`) and only then run Delta Debugging on the remaining changes. \n", "\n", "Building on `DiffDebugger`, implement a class `GitDebugger` that takes two version identifiers (= the \"good\" and \"bad\" hashes in `git bisect`) and a test (like `ChangeDebugger`) and then\n", "\n", "1. runs `git bisect` to narrow down the failure-inducing commit\n", "2. runs `ChangeDebugger` to further narrow down the failure-inducing change within the failure-inducing commit." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "This gives you the best of two worlds: The failure-inducing commit is quickly identified; and the additional `ChangeDebugger` run provides even more fine-grained details, albeit at the expense of (potentially several) additional tests." ] } ], "metadata": { "ipub": { "bibliography": "fuzzingbook.bib", "toc": true }, "kernelspec": { "display_name": "venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.2" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": true, "title_cell": "", "title_sidebar": "Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": true }, "toc-autonumbering": false, "vscode": { "interpreter": { "hash": "0af4f07dd039d1b4e562c7a7d0340393b1c66f50605ac6af30beb81aa23b7ef5" } } }, "nbformat": 4, "nbformat_minor": 4 }