Google Code-Issues mit PHPUnit-Tests schließen und öffnen
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, '&') ); 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.

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
Hallo,
ist hier geschehen: http://bit.ly/5kGE25
Hab Dir auch nen Pull-Request gesendet.
Gruß, Jan
jan
24 Jan 10 at 19:12
[...] 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? [...]
Max’ Lesestoff zum Wochenende | PHP hates me - Der PHP Blog
30 Jan 10 at 06:59
[...] diesem Beitrag von Jan Sorgalla bin ich auf das (relativ) neue Feature des Test-Driven Bugfixings aufmerksam [...]
bitExpert Blog » Blog Archive » Test-Driven Bugfixing mit PHPUnit
30 Jan 10 at 15:05