sorgalla.com

PHP, Zend Framework, Datenbanken und was sonst noch so anfällt.

Google Code-Issues mit PHPUnit-Tests schließen und öffnen

with 4 comments

Update 25.01.2010: Sebastian Bergmann hat meine Klasse dem 3.5-Branch hinzugefügt. Sie wird also ab PHPUnit 3.5.0 standardmässig verfügbar sein.

Raphael Stolt beschreibt in seinem Artikel Closing and reopening GitHub issues via PHPUnit tests wie man mit PHPUnit-Tests GitHub-Issues schließen und wieder öffnen kann. Kurz gesagt geht es dabei um test-driven bug fixing, wobei man für jeden gemeldeten Bug einen Test erstellt der verifiziert, dass dieser gefixt ist. Ausführlicher kann man das in seinem Blogpost nachlesen.

Dadurch inspiriert, habe ich einen entsprechende TicketListener für Google Code erstellt:

<?php
class PHPUnit_Extensions_TicketListener_GoogleCode extends PHPUnit_Extensions_TicketListener
{
    private $email;
    private $password;
    private $project;
 
    private $statusClosed;
    private $statusReopened;
 
    private $printTicketStateChanges;
 
    private $authUrl    = 'https://www.google.com/accounts/ClientLogin';
    private $apiBaseUrl = 'http://code.google.com/feeds/issues/p/%s/issues';
    private $authToken;
 
    /**
     * @param string $email          The email associated with the Google account.
     * @param string $password       The password associated with the Google account.
     * @param string $project        The project name of the system under test (SUT) on Google Code.
     * @param string $printTicketChanges Boolean flag to print the ticket state changes in the test result.
     * @param string $statusClosed   The status name of the closed state.
     * @param string $statusReopened The status name of the reopened state.
     * @throws RuntimeException
     */
    public function __construct($email, $password, $project, $printTicketStateChanges = FALSE,
                                $statusClosed = 'Fixed', $statusReopened = 'Started')
    {
        if (!extension_loaded('curl')) {
            throw new RuntimeException('ext/curl is not available');
        }
 
        $this->email          = $email;
        $this->password       = $password;
        $this->project        = $project;
 
        $this->statusClosed   = $statusClosed;
        $this->statusReopened = $statusReopened;
 
        $this->printTicketStateChanges = $printTicketStateChanges;
 
        $this->apiBaseUrl     = sprintf($this->apiBaseUrl, $project);
    }
 
    /**
     * @param  integer $ticketId
     * @return array
     * @throws RuntimeException
     */
    public function getTicketInfo($ticketId = null)
    {
        if (!is_numeric($ticketId)) {
            return array('status' => 'invalid_ticket_id');
        }
 
        $url = $this->apiBaseUrl . '/full/' . $ticketId;
 
        $header = array(
            'Authorization: GoogleLogin auth=' . $this->getAuthToken(),
        );
 
        list($status, $response) = $this->callGoogleCode($url, $header);
 
        if ($status != 200 || !$response) {
            return array('state' => 'unknown_ticket');
        }
 
        $ticket = new SimpleXMLElement(str_replace("xmlns=", "ns=", $response));
 
        $result = $ticket->xpath('//issues:state');
        $state  = (string) $result[0];
 
        if ($state === 'open') {
            return array('status' => 'new');
        }
 
        if ($state === 'closed') {
            return array('status' => 'closed');
        }
 
        return array('status' => $state);
    }
 
    /**
     * @param string $ticketId   The ticket number of the ticket under test (TUT).
     * @param string $statusToBe The status of the TUT after running the associated test.
     * @param string $message    The additional message for the TUT.
     * @param string $resolution The resolution for the TUT.
     * @throws RuntimeException
     */
    protected function updateTicket($ticketId, $statusToBe, $message, $resolution)
    {
        $url = $this->apiBaseUrl . '/' . $ticketId . '/comments/full';
 
        $header = array(
            'Authorization: GoogleLogin auth=' . $this->getAuthToken(),
            'Content-Type: application/atom+xml',
        );
 
        $ticketStatus = $statusToBe == 'closed' ? $this->statusClosed : $this->statusReopened;
 
        list($author,) = explode('@', $this->email);
 
        $post = '<?xml version="1.0" encoding="UTF-8"?>' .
                '<entry xmlns="http://www.w3.org/2005/Atom" ' .
                '       xmlns:issues="http://schemas.google.com/projecthosting/issues/2009">' .
                '  <content type="html">' . htmlspecialchars($message, ENT_COMPAT, 'UTF-8') . '</content>' .
                '  <author>' .
                '    <name>' . htmlspecialchars($author, ENT_COMPAT, 'UTF-8') . '</name>' .
                '  </author>' .
                '  <issues:updates>' .
                '    <issues:status>' . htmlspecialchars($ticketStatus, ENT_COMPAT, 'UTF-8') . '</issues:status>' .
                '  </issues:updates>' .
                '</entry>';
 
        list($status, $response) = $this->callGoogleCode($url, $header, $post);
 
        if ($status != 201) {
            throw new RuntimeException('Updating GoogleCode issue failed with status code ' . $status);
        }
 
        if ($this->printTicketStateChanges) {
            printf(
                "\nUpdating GoogleCode issue #%d, status: %s\n",
                $ticketId,
                $statusToBe
            );
        }
    }
 
    /**
     * @return string The auth token
     * @throws RuntimeException
     */
    private function getAuthToken()
    {
        if (null !== $this->authToken) {
            return $this->authToken;
        }
 
        $header = array(
            'Content-Type: application/x-www-form-urlencoded',
        );
 
        $post = array(
            'accountType' => 'GOOGLE',
            'Email'       => $this->email,
            'Passwd'      => $this->password,
            'service'     => 'code',
            'source'      => 'PHPUnit-TicketListener_GoogleCode-' . PHPUnit_Runner_Version::id(),
        );
 
        list($status, $response) = $this->callGoogleCode(
            $this->authUrl,
            $header,
            http_build_query($post, null, '&amp;')
        );
 
        if ($status != 200) {
            throw new RuntimeException('Google account authentication failed');
        }
 
        foreach (explode("\n", $response) as $line) {
            if (strpos(trim($line), 'Auth') === 0) {
                list($name, $token) = explode('=', $line);
                $this->authToken = trim($token);
                break;
            }
        }
 
        if (null === $this->authToken) {
            throw new RuntimeException('Could not detect auth token in response');
        }
 
        return $this->authToken;
    }
 
    /**
     * @param string  $url URL to call
     * @param array   $header Header
     * @param string  $post Post data
     * @return array
     */
    private function callGoogleCode($url, array $header = null, $post = null)
    {
        $curlHandle = curl_init();
 
        curl_setopt($curlHandle, CURLOPT_URL, $url);
        curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, TRUE);
        curl_setopt($curlHandle, CURLOPT_FAILONERROR, TRUE);
        curl_setopt($curlHandle, CURLOPT_FRESH_CONNECT, TRUE);
        curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($curlHandle, CURLOPT_HTTPPROXYTUNNEL, TRUE);
        curl_setopt($curlHandle, CURLOPT_USERAGENT, __CLASS__);
        curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, FALSE);
 
        if (null !== $header) {
            curl_setopt($curlHandle, CURLOPT_HTTPHEADER, $header);
        }
 
        if (null !== $post) {
            curl_setopt($curlHandle, CURLOPT_POST, TRUE);
            curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $post);
        }
 
        $response = curl_exec($curlHandle);
        $status   = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
 
        if (!$response) {
            throw new RuntimeException(curl_error($curlHandle));
        }
 
        curl_close($curlHandle);
 
        return array($status, $response);
    }
}
?>

Den TicketListener kann man z. B. in der XML-Konfigurationsdatei von PHPUnit so initialisieren:

<phpunit>
  <listeners>
    <listener class="PHPUnit_Extensions_TicketListener_GoogleCode" file="PHPUnit/Extensions/TicketListener/GoogleCode.php">
      <arguments>
        <string>GOOGLE_ACCOUNT_EMAIL</string>
        <string>GOOGLE_ACCOUNT_PASSWORD</string>
        <string>GOOGLE_CODE_PROJECT_NAME</string>
        <boolean>true</boolean>
        <string>Fixed</string>
        <string>Started</string>
      </arguments>
    </listener>
  </listeners>
</phpunit>

Die ersten 2 Argumente sind die E-Mail und das Passwort des Google-Accounts, das 3. Argument der Projektname auf Google Code, das 4. Argument ein Flag, das angibt, ob ausgegeben werden soll wenn ein sich Ticket-Status ändert und die letzten beiden Argumente die Status für den Issue-Tracker.

Den ganzen Code plus ein einfaches Demo gibts im zugehörigen Google Code-Projekt.

Dabei ist zu beachten, dass die TicketListener auf Grund eines Bugs in der abstrakten TicketListener-Klasse in der aktuellen Version von PHPUnit (3.4.6) nicht funktionieren. Deshalb sollte man vorher diesen Patch anwenden.

Links zum Thema

Post to Twitter Post to Delicious Post to Digg Post to Facebook Post to Reddit

Written by jan

Januar 24th, 2010 at 1:42 pm

Posted in PHPUnit

Tagged with , , ,

4 Responses to 'Google Code-Issues mit PHPUnit-Tests schließen und öffnen'

Subscribe to comments with RSS or TrackBack to 'Google Code-Issues mit PHPUnit-Tests schließen und öffnen'.

  1. Bitte PHPUnit auf GitHub forken, einen Topic-Branch von 3.5 ausgehend machen, den GoogleCode Test Listener hinzufügen, committen und pushen. Dann kann ich das direkt in PHPUnit 3.5 übernehmen.

    Danke!

    Sebastian Bergmann

    24 Jan 10 at 16:01

  2. Hallo,

    ist hier geschehen: http://bit.ly/5kGE25
    Hab Dir auch nen Pull-Request gesendet.

    Gruß, Jan

    jan

    24 Jan 10 at 19:12

  3. […] Google Code-Issues mit PHPUnit-Tests schließen und öffnen – sorgalla.com Autmatisch Bugs erstellen und Schliessen auf Google Code nach einem Durchlauf der Unit-Tests Hades Blag: Git Is Your Friend not a Foe Vol. 2: Branches Nette Einführung über Branches in GIT Hades Blag: Git Is Your Friend not a Foe Vol. 1: Distributed Einführung in die Verteilte Versionskontrolle mit GIT LackRack – Eth0Wiki Ein Server-Rack kostengünstig produziert @TheKeyboard » Blog Archive » Creating Usable Forms With Zend Framework Wie man Zend_Form Formulare ein bisschen mehr wiederbenutzbarer gestalten. Ruby-like iterators in PHP « PHP 10.0 Blog Ruby-Iterator Style nach PHP konvertiert. Was haltet ihr davon? Share this on del.icio.usShare this on TechnoratiAdd this to Mister WongStumble upon something good? Share it on StumbleUponShare this on RedditShare this on FacebookAdd this to Google BookmarksSubscribe to the comments for this post? […]

  4. […] diesem Beitrag von Jan Sorgalla bin ich auf das (relativ) neue Feature des Test-Driven Bugfixings aufmerksam […]

Leave a Reply