Zum Hauptinhalt Zur Navigation

Softwareentwicklung: Code-Bruchlandung mit Microsofts Copilot

KI ist eine Hilfe beim Coden. Nur manchmal macht sie Fehler. Fehler, die kein Programmierer jemals machen würde und die man deswegen auch kaum findet.
/ Klaas van Schelven
33 Kommentare News folgen (öffnet im neuen Fenster)
Der Autor dieses Textes hat mit der KI-Hilfe Copilot beim Coden eine Bruchlandung hingelegt - und wusste lange nicht, warum überhaupt. (Bild: Pixabay)
Der Autor dieses Textes hat mit der KI-Hilfe Copilot beim Coden eine Bruchlandung hingelegt - und wusste lange nicht, warum überhaupt. Bild: Pixabay

Statt die Leser auf meine aufregende Debugging-Reise mitzunehmen, rufe ich direkt dazu auf, selbst zu suchen! Hier ist also der Bug, den mir Microsoft Copilot präsentierte, während ich an meinen Import-Statements arbeitete.

        

from django.test import TestCase as TransactionTestCase

Pythons "import as"

Was haben wir hier? Wer sich mit Python nicht so gut auskennt: Das Schlüsselwort as erlaubt es, der importierten Entität beim Import einen neuen Namen zu geben. Dadurch können Namenskonflikte vermieden werden - oder es ist einfach kürzer.

Hier ein paar sinnvolle Beispiele:

        

# for brevity / idiomatic use:
import numpy as np

# to avoid naming conflicts / introduce clarity:
from django.test import TestCase as DjangoTestCase
from unittest import TestCase as RegularTestCase

Der Bug oben im Code ist kein sinnvolles Beispiel. Im Gegenteil: Es ist die sinnloseste Nutzung von as , die man sich vorstellen kann.

Denn django.test beinhaltet sehr viele verschiedene Testklassen, unter anderem TestCase und TransactionTestCase , die jeweils einen leicht anderen Sinn haben. Der Code oben importiert nun das eine unter dem Namen des anderen.

Der eigentliche Fehler

In diesem Fall haben die beiden TestCases (wie auch der Name des einen impliziert) eine leicht unterschiedliche Semantik bei den Datenbank-Transaktionen.

  • Die Klasse TestCase verpackt jeden Test in eine Transaktion, die sie anschließend zurückrollt - es ist also ein isolierter Test.
  • Die Klasse TransactionTestCase hat - was vielleicht angesichts des Namens überraschen mag - kein implizites Transaktionsmanagement; dadurch eignet sie sich hervorragend sowohl für Tests, die vom Datenbank-Transaktionsmanagement abhängen, als auch für Tests des Datenbank-Transaktionsmanagements der Anwendung.

Der Bug besteht nun darin, dass man eigentlich TransactionTestCase nutzen möchte, aber wegen des seltsamen Imports der Standard-TestCase läuft - was dazu führt, dass plötzlich alle Tests fehlschlagen. So war es jedenfalls bei mir.

Zwei Stunden meines Lebens

Auf die genaue Beschreibung all der Überraschungen, die ich in diesen zwei Stunden der Fehlersuche erlebt habe, verzichte ich mal. Ebenso auf eine detaillierte Schilderung des Tests, der mich auf die Spur brachte und was ich tat, um nicht noch mal in die gleiche Falle zu tappen(öffnet im neuen Fenster) .

In aller Kürze: Nachdem ich also festgestellt hatte, dass meine Tests wegen der falscher Datenbank-Transaktionen fehlschlugen, sah ich also erst in meinem eigenen Code nach, dann verdächtigte ich Django, nur um am Ende das Problem vom Anfang zu erkennen.

Warum ich Django im Verdacht hatte? Weil ich mir sicher war, TransactionTestCase zu nutzen. Nur verhielt sich TransactionTestCase eindeutig nicht so wie in der Dokumentation beschrieben. Also dachte ich, es gebe irgendeinen gut versteckten Bug in Django, daher ging ich auch diesen Code Schritt für Schritt durch.

Warum ich so lange gebraucht habe, den Bug zu finden

Wenn man es weiß, scheint das Problem einfach zu finden zu sein. Aber wenn man es nicht weiß, sucht und sucht und sucht man. Warum war das so schwierig?

Erstens habe ich zwar vor meinem Commit getestet, aber die Tests nicht direkt laufen lassen, nachdem Copilot die Zeile eingefügt hatte. Als die Tests also fehlschlugen, hatte ich zwei volle Bildschirme diff-Text vor mir.

Zweitens war die Usage Location des Alias irreführend. Hier ist nur von TransactionTestCase die Rede - und der Kommentar dazu lässt einen noch mehr denken, dass hier die richtige Funktion genutzt wird.

        

class IngestViewTestCase(TransactionTestCase):
    # We use TransactionTestCase because of the following:
    #
    # > Django's TestCase class wraps each test in a transaction and rolls
    # > back that transaction after each test, in order to provide test
    # > isolation. This means that no transaction is ever actually committed,
    # > thus your on_commit() callbacks will never be run.
    # > [..]
    # > Another way to overcome the limitation is to use TransactionTestCase
    # > instead of TestCase. This will mean your transactions are committed,
    # > and the callbacks will run. However [..] significantly slower [..]

Das Alias ließ mich denken, dass TransactionTestCase hier korrekt verwendet wird. Zusammen mit dem detaillierten Kommentar sah ich es also als richtigen Weg an, meine Zeit damit zu verschwenden, tief in Djangos Innenleben zu verschwinden, statt mein Augenmerk auf den Import zu richten.

Ein nichtmenschlicher Fehler

Der eigentliche Grund für den großen Aufwand, den Fehler zu finden, war aber, dass er einfach sehr seltsam ist.

Es hat mich ja tatsächlich zwei Stunden Arbeit gekostet, den Fehler zu finden, obwohl er ganz frisch dazugekommen war. Weil ich noch keinen Commit gemacht hatte und sicher war, dass mit dem Commit davor alles in Ordnung war, hätte ich einfach git diff laufen lassen können, um zu sehen, was sich geändert hat.

Tatsächlich habe ich sowohl git diff als auch git diff --staged laufen lassen - und zwar mehrere Male! Aber wer schaut schon auf die Import-Statements?

Die Import-Statements sind wirklich der allerletzte Ort, an dem man einen neuen Fehler erwarten würde. Denn dort liegt eigentlich nur langweiliger, uninteressanter, unveränderter Code.

Um debuggen zu können, muss man Code verstehen, und um ihn zu verstehen, muss man Annahmen machen. Eine vernünftige Annahme (aus der Vor-LLM-Zeit) wäre: Codezeilen wie die oben gibt es nicht. Denn wer würde so etwas schon schreiben?

Sicher, dass es Copilot war ...?

Ja. Leider habe ich keinen Videobeweis oder ein MITM-Log der Anforderungen an Copilot. Ich kann es also nicht beweisen. Aber auch acht Monate später kann ich den Fehler reproduzieren:

        

from django.test import Te... # copilot autocomplete finishes this as:
from django.test import TestCase as TransactionTestCase

Da ich weiß, dass unter dem Import-Statement Code mit einigen TransactionTestCase folgt, aber keinem TestCase , kann ich mir ungefähr vorstellen, wie eine Maschine, darauf trainiert, Leerstellen zu füllen, zu so einer Zeile kommt. Das ist auf eine Art vernünftig, je nachdem, wie man vernünftig definiert.

Aber: Es ist nicht üblich, es folgt keinem gängigen Muster, es ist einfach keine gute Idee. Also bleibt für mich nur Copilot als Urheber übrig.

Copilot verursacht Crashs

KI-unterstützte Tools führen neue Arten von Fehlern ein. Erfahrene Entwickler kennen ihre eigenen Fehlerquellen und auch die von anderen, von Junior-Entwicklern beispielsweise.

Mit KI kommt aber eine neue Art von Fehlern dazu. Sie wird zuverlässig Fehler produzieren, die keiner erwartet - wie in dem Statement, das Anlass für den Text war. Verlassen wir uns auf die KI-Unterstützung, werden wir es mit Fehlern zu tun haben, die wir eigentlich nicht erwarten würden.

Sie sind quasi die Marotten der KI und sie bringen neue Ebenen der Unvorhersagbarkeit in unsere Arbeitsprozesse. Für mich bleibt KI eine gute Hilfe, aber es ist wichtig, sich darüber im Klaren zu sein, dass KI diese neuen Bugs schaffen kann.

Und was ist nun mit dem Crash in der Überschrift? Um ehrlich zu sein, gab es keinen Crash, weil ich den Code nicht committet hatte - es ist also mehr ein Witz. Die Metapher war nur einfach zu verlockend.

Der Text wurde von Jennifer Fraczek übersetzt - ohne KI-Hilfe.


Relevante Themen