r54066 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r54065‎ | r54066 | r54067 >
Date:01:37, 31 July 2009
Author:brion
Status:ok (Comments)
Tags:
Comment:
Server side for test run data integration into code review.

Parser tests on trunk can now upload test results to a wiki running CodeReview.
While the tests are running, the run data is marked as in progress and will
display a spinner in the test column on the revision view. After completion,
the result set is uploaded, and success/total counts are displayed, with nice
red digits if there's failures.

Failed tests are listed by name on the revision detail page. Currently details
such as the output diff are not included, but there's a blob field ready to accept such.

The status and result updates are sent to the API, action=codetestupdate. Rather
than forcing the client side to figure out how to log in with an account, I'm
just protecting it with an HMAC which validates that the request is legit; a
shared secret should be configured on the client and server ends.

Probably not too efficient in the list view as it'll pull a query per rev to get
the counts... There's a summary of counts in code_test_run however there may be
multiple rows as this is extended past ParserTests to encompass other test suites
yet to be developed.

Currently no UI for the test suite control data either; and we really need to
clean up how the UI handles branching; we should make interesting branches/directories
easily selectable in the UI so we can follow just trunk, or just some branch, or
whatever.

Note that, for instance, it only really makes sense to run the parser tests on trunk phase3,
not on unrelated stuff in other branches; it'll let you send data for other versions, it
just will be confusing. ;)
Modified paths:
  • /trunk/extensions/CodeReview/CodeRepository.php (modified) (history)
  • /trunk/extensions/CodeReview/CodeReview.i18n.php (modified) (history)
  • /trunk/extensions/CodeReview/CodeReview.php (modified) (history)
  • /trunk/extensions/CodeReview/CodeRevision.php (modified) (history)
  • /trunk/extensions/CodeReview/CodeRevisionListView.php (modified) (history)
  • /trunk/extensions/CodeReview/CodeRevisionView.php (modified) (history)
  • /trunk/extensions/CodeReview/CodeTestResult.php (added) (history)
  • /trunk/extensions/CodeReview/CodeTestRun.php (added) (history)
  • /trunk/extensions/CodeReview/CodeTestSuite.php (added) (history)
  • /trunk/extensions/CodeReview/api/ApiCodeTestUpload.php (added) (history)
  • /trunk/extensions/CodeReview/archives/codereview-code_tests.sql (added) (history)
  • /trunk/extensions/CodeReview/codereview.css (modified) (history)
  • /trunk/extensions/CodeReview/codereview.sql (modified) (history)

Diff [purge]

Index: trunk/extensions/CodeReview/codereview.css
@@ -78,6 +78,13 @@
7979 color: #666;
8080 }
8181
 82+.mw-codereview-success {
 83+ color: #1a2;
 84+}
 85+.mw-codereview-fail {
 86+ color: #d21;
 87+}
 88+
8289 /* Diffs */
8390 .mw-codereview-diff ins {
8491 text-decoration: none;
Index: trunk/extensions/CodeReview/CodeTestRun.php
@@ -0,0 +1,170 @@
 2+<?php
 3+
 4+class CodeTestRun {
 5+ public function __construct( CodeTestSuite $suite, $row ) {
 6+ if( is_object( $row ) ) {
 7+ $row = wfObjectToArray( $row );
 8+ }
 9+ $this->suite = $suite;
 10+ $this->id = intval( $row['ctrun_id'] );
 11+ $this->revId = intval( $row['ctrun_rev_id'] );
 12+ $this->status = $row['ctrun_status'];
 13+ $this->countTotal = $row['ctrun_count_total'];
 14+ $this->countSuccess = $row['ctrun_count_success'];
 15+
 16+ $this->mCaseMap = null; // Lazy-initialize...
 17+ }
 18+
 19+ public function getResults( $success=null ) {
 20+ $dbr = wfGetDB( DB_MASTER );
 21+ $conds = array(
 22+ 'ctresult_run_id' => $this->id,
 23+ 'ctresult_case_id=ctcase_id',
 24+ );
 25+ if( $success !== null ) {
 26+ $conds['ctresult_success'] = $success ? 1 : 0;
 27+ }
 28+
 29+ $result = $dbr->select(
 30+ array(
 31+ 'code_test_result',
 32+ 'code_test_case',
 33+ ),
 34+ '*',
 35+ $conds,
 36+ __METHOD__ );
 37+
 38+ $out = array();
 39+ foreach( $result as $row ) {
 40+ $out[] = new CodeTestResult( $this, $row );
 41+ }
 42+ return $out;
 43+ }
 44+
 45+ public static function newFromRevId( CodeTestSuite $suite, $revId ) {
 46+ $dbr = wfGetDB( DB_MASTER );
 47+ $row = $dbr->selectRow( 'code_test_run',
 48+ '*',
 49+ array(
 50+ 'ctrun_suite_id' => $suite->id,
 51+ 'ctrun_rev_id' => $revId,
 52+ ),
 53+ __METHOD__ );
 54+ if( $row ) {
 55+ return new CodeTestRun( $suite, $row );
 56+ } else {
 57+ return null;
 58+ }
 59+ }
 60+
 61+ public function setStatus( $status ) {
 62+ $this->status = $status;
 63+ $dbw = wfGetDB( DB_MASTER );
 64+ $dbw->update(
 65+ 'code_test_run',
 66+ array(
 67+ 'ctrun_status' => $status,
 68+ ),
 69+ array(
 70+ 'ctrun_id' => $this->id,
 71+ ),
 72+ __METHOD__ );
 73+ }
 74+
 75+ public static function insertRun( CodeTestSuite $suite, $revId, $status, $results=array() ) {
 76+ $dbw = wfGetDB( DB_MASTER );
 77+ $countTotal = count( $results );
 78+ $countSucceeded = count( array_filter( $results ) );
 79+
 80+ $insertData = array(
 81+ 'ctrun_suite_id' => $suite->id,
 82+ 'ctrun_rev_id' => $revId,
 83+ 'ctrun_status' => $status,
 84+ 'ctrun_count_total' => $countTotal,
 85+ 'ctrun_count_success' => $countSucceeded,
 86+ );
 87+ $dbw->insert( 'code_test_run',
 88+ $insertData,
 89+ __METHOD__ );
 90+
 91+ $insertData['ctrun_id'] = $dbw->insertId();
 92+ $run = new CodeTestRun( $suite, $insertData );
 93+ if( $status == 'complete' && $results ) {
 94+ $run->insertData( $results );
 95+ }
 96+ return $run;
 97+ }
 98+
 99+ public function getCaseId( $caseName ) {
 100+ $this->loadCaseMap();
 101+ if( isset( $this->mCaseMap[$caseName] ) ) {
 102+ return $this->mCaseMap[$caseName];
 103+ } else {
 104+ $dbw = wfGetDB( DB_MASTER );
 105+ $dbw->insert( 'code_test_case',
 106+ array(
 107+ 'ctcase_suite_id' => $this->id,
 108+ 'ctcase_name' => $caseName,
 109+ ),
 110+ __METHOD__ );
 111+ $id = intval( $dbw->insertId() );
 112+ $this->mCaseMap[$caseName] = $id;
 113+ return $id;
 114+ }
 115+ }
 116+
 117+ protected function loadCaseMap() {
 118+ if( is_null( $this->mCaseMap ) ) {
 119+ $this->mCaseMap = array();
 120+ $dbw = wfGetDB( DB_MASTER );
 121+ $result = $dbw->select( 'code_test_case',
 122+ array(
 123+ 'ctcase_id',
 124+ 'ctcase_name',
 125+ ),
 126+ array(
 127+ 'ctcase_suite_id' => $this->id,
 128+ ),
 129+ __METHOD__
 130+ );
 131+ foreach( $result as $row ) {
 132+ $this->mCaseMap[$row->ctcase_name] = intval( $row->ctcase_id );
 133+ }
 134+ }
 135+ }
 136+
 137+ public function saveResults( $results ) {
 138+ $this->insertResults( $results );
 139+ $this->status = "complete";
 140+ $dbw = wfGetDB( DB_MASTER );
 141+ $dbw->update(
 142+ 'code_test_run',
 143+ array(
 144+ 'ctrun_status' => $this->status,
 145+ 'ctrun_count_total' => $this->countTotal,
 146+ 'ctrun_count_success' => $this->countSuccess,
 147+ ),
 148+ array(
 149+ 'ctrun_id' => $this->id,
 150+ ),
 151+ __METHOD__ );
 152+ }
 153+
 154+ public function insertResults( $results ) {
 155+ $dbw = wfGetDB( DB_MASTER );
 156+ $this->countTotal = 0;
 157+ $this->countSuccess = 0;
 158+ foreach( $results as $caseName => $result ) {
 159+ $this->countTotal++;
 160+ if( $result ) {
 161+ $this->countSuccess++;
 162+ }
 163+ $insertData[] = array(
 164+ 'ctresult_run_id' => $this->id,
 165+ 'ctresult_case_id' => $this->getCaseId( $caseName ),
 166+ 'ctresult_success' => $result ? 1 : 0,
 167+ );
 168+ }
 169+ $dbw->insert( 'code_test_result', $insertData, __METHOD__ );
 170+ }
 171+}
Property changes on: trunk/extensions/CodeReview/CodeTestRun.php
___________________________________________________________________
Name: svn:eol-style
1172 + native
Index: trunk/extensions/CodeReview/codereview.sql
@@ -65,7 +65,7 @@
6666 cr_diff mediumblob NULL,
6767 -- Text flags: gzip,utf-8,external
6868 cr_flags tinyblob NOT NULL,
69 -
 69+
7070 primary key (cr_repo_id, cr_id),
7171 key (cr_repo_id, cr_timestamp),
7272 key cr_repo_author (cr_repo_id, cr_author, cr_timestamp)
@@ -217,3 +217,70 @@
218218 key cpc_repo_rev_time (cpc_repo_id, cpc_rev_id, cpc_timestamp),
219219 key cpc_repo_time (cpc_repo_id, cpc_timestamp)
220220 ) /*$wgDBTableOptions*/;
 221+
 222+--
 223+-- Information on available test suites
 224+DROP TABLE IF EXISTS /*$wgDBprefix*/code_test_suite;
 225+CREATE TABLE /*$wgDBprefix*/code_test_suite (
 226+ -- Unique ID per test suite
 227+ ctsuite_id int auto_increment not null,
 228+
 229+ -- Repository ID of the code base this applies to
 230+ ctsuite_repo_id int not null,
 231+
 232+ -- Which branch path this applies to, eg '/trunk/phase3'
 233+ ctsuite_branch_path varchar(255) not null,
 234+
 235+ -- Pleasantly user-readable name, eg "ParserTests"
 236+ ctsuite_name varchar(255) not null,
 237+
 238+ -- Description...
 239+ ctsuite_desc varchar(255) not null,
 240+
 241+ primary key ctsuite_id (ctsuite_id)
 242+) /*$wgDBtableOptions*/;
 243+
 244+DROP TABLE IF EXISTS /*$wgDBprefix*/code_test_case;
 245+CREATE TABLE /*$wgDBprefix*/code_test_case (
 246+ ctcase_id int auto_increment not null,
 247+ ctcase_suite_id int not null,
 248+ ctcase_name varchar(255) not null,
 249+
 250+ primary key ctc_id (ctcase_id),
 251+ key (ctcase_suite_id, ctcase_id)
 252+) /*$wgDBtableOptions*/;
 253+
 254+DROP TABLE IF EXISTS /*$wgDBprefix*/code_test_run;
 255+CREATE TABLE /*$wgDBprefix*/code_test_run (
 256+ ctrun_id int auto_increment not null,
 257+
 258+ ctrun_suite_id int not null,
 259+ ctrun_rev_id int not null,
 260+
 261+ ctrun_status enum ('running', 'complete', 'abort'),
 262+
 263+ ctrun_count_total int,
 264+ ctrun_count_success int,
 265+
 266+ primary key ctrun_id (ctrun_id),
 267+ key suite_rev (ctrun_suite_id, ctrun_rev_id)
 268+) /*$wgDBtableOptions*/;
 269+
 270+
 271+DROP TABLE IF EXISTS /*$wgDBprefix*/code_test_result;
 272+CREATE TABLE /*$wgDBprefix*/code_test_result (
 273+ ctresult_id int auto_increment not null,
 274+
 275+ -- Which test run and case are we on?
 276+ ctresult_run_id int not null,
 277+ ctresult_case_id int not null,
 278+
 279+ -- Did we succeed or fail?
 280+ ctresult_success bool not null,
 281+
 282+ -- Optional HTML chunk data
 283+ ctresult_details blob,
 284+
 285+ primary key ctr_id (ctresult_id),
 286+ key run_id (ctresult_run_id, ctresult_id)
 287+) /*$wgDBtableOptions*/;
Index: trunk/extensions/CodeReview/CodeReview.i18n.php
@@ -28,6 +28,7 @@
2929 'code-authors' => 'authors',
3030 'code-status' => 'states',
3131 'code-tags' => 'tags',
 32+ 'code-tests' => 'Test cases',
3233 'code-authors-text' => 'Below is a list of repo authors in order of recent commits.',
3334 'code-author-haslink' => 'This author is linked to the wiki user $1',
3435 'code-author-orphan' => 'This author has no link with a wiki account',
@@ -46,6 +47,7 @@
4748 'code-field-status' => 'Status',
4849 'code-field-timestamp' => 'Date',
4950 'code-field-comments' => 'Notes',
 51+ 'code-field-tests' => 'Tests',
5052 'code-field-path' => 'Path',
5153 'code-field-text' => 'Note',
5254 'code-field-select' => 'Select',
Index: trunk/extensions/CodeReview/archives/codereview-code_tests.sql
@@ -0,0 +1,66 @@
 2+--
 3+-- Information on available test suites
 4+DROP TABLE IF EXISTS /*$wgDBprefix*/code_test_suite;
 5+CREATE TABLE /*$wgDBprefix*/code_test_suite (
 6+ -- Unique ID per test suite
 7+ ctsuite_id int auto_increment not null,
 8+
 9+ -- Repository ID of the code base this applies to
 10+ ctsuite_repo_id int not null,
 11+
 12+ -- Which branch path this applies to, eg '/trunk/phase3'
 13+ ctsuite_branch_path varchar(255) not null,
 14+
 15+ -- Pleasantly user-readable name, eg "ParserTests"
 16+ ctsuite_name varchar(255) not null,
 17+
 18+ -- Description...
 19+ ctsuite_desc varchar(255) not null,
 20+
 21+ primary key ctsuite_id (ctsuite_id)
 22+) /*$wgDBtableOptions*/;
 23+
 24+DROP TABLE IF EXISTS /*$wgDBprefix*/code_test_case;
 25+CREATE TABLE /*$wgDBprefix*/code_test_case (
 26+ ctcase_id int auto_increment not null,
 27+ ctcase_suite_id int not null,
 28+ ctcase_name varchar(255) not null,
 29+
 30+ primary key ctc_id (ctcase_id),
 31+ key (ctcase_suite_id, ctcase_id)
 32+) /*$wgDBtableOptions*/;
 33+
 34+DROP TABLE IF EXISTS /*$wgDBprefix*/code_test_run;
 35+CREATE TABLE /*$wgDBprefix*/code_test_run (
 36+ ctrun_id int auto_increment not null,
 37+
 38+ ctrun_suite_id int not null,
 39+ ctrun_rev_id int not null,
 40+
 41+ ctrun_status enum ('running', 'complete', 'abort'),
 42+
 43+ ctrun_count_total int,
 44+ ctrun_count_success int,
 45+
 46+ primary key ctrun_id (ctrun_id),
 47+ key suite_rev (ctrun_suite_id, ctrun_rev_id)
 48+) /*$wgDBtableOptions*/;
 49+
 50+
 51+DROP TABLE IF EXISTS /*$wgDBprefix*/code_test_result;
 52+CREATE TABLE /*$wgDBprefix*/code_test_result (
 53+ ctresult_id int auto_increment not null,
 54+
 55+ -- Which test run and case are we on?
 56+ ctresult_run_id int not null,
 57+ ctresult_case_id int not null,
 58+
 59+ -- Did we succeed or fail?
 60+ ctresult_success bool not null,
 61+
 62+ -- Optional HTML chunk data
 63+ ctresult_details blob,
 64+
 65+ primary key ctr_id (ctresult_id),
 66+ key run_id (ctresult_run_id, ctresult_id)
 67+) /*$wgDBtableOptions*/;
Property changes on: trunk/extensions/CodeReview/archives/codereview-code_tests.sql
___________________________________________________________________
Name: svn:eol-style
168 + native
Index: trunk/extensions/CodeReview/CodeRevisionListView.php
@@ -187,7 +187,7 @@
188188 'options' => array( 'GROUP BY' => 'cp_rev_id', 'USE INDEX' => array( 'code_path' => 'cp_repo_id' ) ),
189189 'join_conds' => array(
190190 'code_rev' => array( 'INNER JOIN', 'cr_repo_id = cp_repo_id AND cr_id = cp_rev_id' ),
191 - 'code_comment' => array( 'LEFT JOIN', 'cc_repo_id = cp_repo_id AND cc_rev_id = cp_rev_id' )
 191+ 'code_comment' => array( 'LEFT JOIN', 'cc_repo_id = cp_repo_id AND cc_rev_id = cp_rev_id' ),
192192 )
193193 );
194194 // No path; entire repo...
@@ -198,7 +198,7 @@
199199 'conds' => array( 'cr_repo_id' => $this->mRepo->getId() ),
200200 'options' => array( 'GROUP BY' => 'cr_id' ),
201201 'join_conds' => array(
202 - 'code_comment' => array( 'LEFT JOIN', 'cc_repo_id = cr_repo_id AND cc_rev_id = cr_id' )
 202+ 'code_comment' => array( 'LEFT JOIN', 'cc_repo_id = cr_repo_id AND cc_rev_id = cr_id' ),
203203 )
204204 );
205205 }
@@ -206,8 +206,15 @@
207207 }
208208
209209 function getSelectFields() {
210 - return array( $this->getDefaultSort(), 'cr_status', 'COUNT( DISTINCT cc_id ) AS comments',
211 - 'cr_path', 'cr_message', 'cr_author', 'cr_timestamp' );
 210+ return array( $this->getDefaultSort(),
 211+ 'cr_id',
 212+ 'cr_repo_id',
 213+ 'cr_status',
 214+ 'COUNT(DISTINCT cc_id) AS comments',
 215+ 'cr_path',
 216+ 'cr_message',
 217+ 'cr_author',
 218+ 'cr_timestamp' );
212219 }
213220
214221 function getFieldNames() {
@@ -215,6 +222,7 @@
216223 $this->getDefaultSort() => wfMsg( 'code-field-id' ),
217224 'cr_status' => wfMsg( 'code-field-status' ),
218225 'comments' => wfMsg( 'code-field-comments' ),
 226+ 'tests' => wfMsg( 'code-field-tests' ),
219227 'cr_path' => wfMsg( 'code-field-path' ),
220228 'cr_message' => wfMsg( 'code-field-message' ),
221229 'cr_author' => wfMsg( 'code-field-author' ),
@@ -260,6 +268,40 @@
261269 } else {
262270 return intval( $value );
263271 }
 272+ case 'tests':
 273+ // fixme -- this still isn't too efficient...
 274+ $rev = CodeRevision::newFromRow( $this->mRepo, $row );
 275+ $runs = $rev->getTestRuns();
 276+ if( empty( $runs ) ) {
 277+ return '&nbsp;';
 278+ } else {
 279+ $total = 0;
 280+ $success = 0;
 281+ $progress = false;
 282+ foreach( $runs as $run ) {
 283+ $total += $run->countTotal;
 284+ $success += $run->countSuccess;
 285+ if( $run->status == 'running' ) {
 286+ $progress = true;
 287+ }
 288+ }
 289+ if( $progress ) {
 290+ global $wgStylePath;
 291+ return Xml::element( 'img', array(
 292+ 'src' => "$wgStylePath/common/images/spinner.gif",
 293+ 'width' => 20,
 294+ 'height' => 20,
 295+ 'alt' => "...",
 296+ 'title' => "Tests in progress...",
 297+ ));
 298+ }
 299+ if( $success == $total ) {
 300+ $class = 'mw-codereview-success';
 301+ } else {
 302+ $class = 'mw-codereview-fail';
 303+ }
 304+ return "<span class='$class'><strong>$success</strong>/$total</span>";
 305+ }
264306 case 'cr_path':
265307 return Xml::openElement( 'div', array( 'title' => (string)$value ) ) .
266308 $this->mView->mSkin->link(
Index: trunk/extensions/CodeReview/CodeTestSuite.php
@@ -0,0 +1,38 @@
 2+<?php
 3+
 4+class CodeTestSuite {
 5+ public static function newFromRow( CodeRepository $repo, $row ) {
 6+ $suite = new CodeTestSuite();
 7+ $suite->id = intval( $row->ctsuite_id );
 8+ $suite->repo = $repo;
 9+ $suite->repoId = $repo->getId();
 10+ $suite->branchPath = $row->ctsuite_branch_path;
 11+ $suite->name = $row->ctsuite_name;
 12+ $suite->desc = $row->ctsuite_desc;
 13+ return $suite;
 14+ }
 15+
 16+ function getRun( $revId ) {
 17+ return CodeTestRun::newFromRevId( $this, $revId );
 18+ }
 19+
 20+ function setStatus( $revId, $status ) {
 21+ $run = $this->getRun( $revId );
 22+ if( $run ) {
 23+ $run->setStatus( $status );
 24+ } else {
 25+ $run = CodeTestRun::insertRun( $this, $revId, $status );
 26+ }
 27+ return $run;
 28+ }
 29+
 30+ function saveResults( $revId, $results ) {
 31+ $run = $this->getRun( $revId );
 32+ if( $run ) {
 33+ $run->saveResults( $results );
 34+ } else {
 35+ $run = CodeTestRun::insertRun( $this, $revId, "complete", $results );
 36+ }
 37+ return $run;
 38+ }
 39+}
Property changes on: trunk/extensions/CodeReview/CodeTestSuite.php
___________________________________________________________________
Name: svn:eol-style
140 + native
Index: trunk/extensions/CodeReview/CodeRevisionView.php
@@ -81,6 +81,12 @@
8282 }
8383
8484 $html .= $this->formatMetaData( $fields );
 85+ # Show test case info
 86+ $tests = $this->formatTests();
 87+ if( $tests ) {
 88+ $html .= "<h2 id='code-tests'>" . wfMsgHtml( 'code-tests' ) .
 89+ "</h2>\n" . $tests;
 90+ }
8591 # Output diff
8692 if ( $this->mRev->isDiffable() ) {
8793 $diffHtml = $this->formatDiff();
@@ -282,6 +288,42 @@
283289 return $this->mSkin->link( $special, htmlspecialchars( $tag ) );
284290 }
285291
 292+ protected function formatTests() {
 293+ $runs = $this->mRev->getTestRuns();
 294+ $html = '';
 295+ if( count( $runs ) ) {
 296+ foreach( $runs as $run ) {
 297+ $html .= "<h3>" . htmlspecialchars( $run->suite->name ) . "</h3>\n";
 298+ if( $run->status == 'complete' ) {
 299+ $total = $run->countTotal;
 300+ $success = $run->countSuccess;
 301+ $failed = $total - $success;
 302+ if( $failed ) {
 303+ $html .= "<p><span class='mw-codereview-success'>$success</span> succeeded tests, " .
 304+ "<span class='mw-codereview-fail'>$failed</span> failed tests:</p>";
 305+
 306+ $tests = $run->getResults( false );
 307+ $html .= "<ul>\n";
 308+ foreach( $tests as $test ) {
 309+ $html .= "<li>" . htmlspecialchars( $test->caseName ) . "</li>\n";
 310+ }
 311+ $html .= "</ul>\n";
 312+ } else {
 313+ $html .= "<p><span class='mw-codereview-success'>$success</span> succeeded tests.</p>";
 314+
 315+ }
 316+ } elseif( $run->status == "running" ) {
 317+ $html .= "<p>Test cases are running...</p>";
 318+ } elseif( $run->status == "abort" ) {
 319+ $html .= "<p>Test run aborted.</p>";
 320+ } else {
 321+ // Err, this shouldn't happen?
 322+ }
 323+ }
 324+ }
 325+ return $html;
 326+ }
 327+
286328 protected function formatDiff() {
287329 global $wgEnableAPI;
288330
Index: trunk/extensions/CodeReview/CodeRevision.php
@@ -578,6 +578,56 @@
579579 public function isValidTag( $tag ) {
580580 return ( $this->normalizeTag( $tag ) !== false );
581581 }
 582+
 583+ public function getTestRuns() {
 584+ $dbr = wfGetDB( DB_SLAVE );
 585+ $result = $dbr->select(
 586+ array(
 587+ 'code_test_suite',
 588+ 'code_test_run',
 589+ ),
 590+ '*',
 591+ array(
 592+ 'ctsuite_repo_id' => $this->mRepoId,
 593+ 'ctsuite_id=ctrun_suite_id',
 594+ 'ctrun_rev_id' => $this->mId,
 595+ ),
 596+ __METHOD__ );
 597+ $runs = array();
 598+ foreach( $result as $row ) {
 599+ $suite = CodeTestSuite::newFromRow( $this->mRepo, $row );
 600+ $runs[] = new CodeTestRun( $suite, $row );
 601+ }
 602+ return $runs;
 603+ }
 604+
 605+ public function getTestResults( $success = null ) {
 606+ $dbr = wfGetDB( DB_SLAVE );
 607+ $conds = array(
 608+ 'ctr_repo_id' => $this->mRepoId,
 609+ 'ctr_rev_id' => $this->mId,
 610+ 'ctr_case_id=ctc_id',
 611+ 'ctc_suite_id=cts_id' );
 612+ if( $success === true ) {
 613+ $conds['ctr_result'] = 1;
 614+ } elseif( $success === false ) {
 615+ $conds['ctr_result'] = 0;
 616+ }
 617+ $results = $dbr->select(
 618+ array(
 619+ 'code_test_result',
 620+ 'code_test_case',
 621+ 'code_test_suite',
 622+ ),
 623+ '*',
 624+ $conds,
 625+ __METHOD__ );
 626+ $out = array();
 627+ foreach( $results as $row ) {
 628+ $out[] = new CodeTestResult( $row );
 629+ }
 630+ return $out;
 631+ }
582632
583633 public function getPrevious() {
584634 // hack!
Index: trunk/extensions/CodeReview/CodeReview.php
@@ -40,6 +40,7 @@
4141 $wgAutoloadClasses['ApiCodeUpdate'] = $dir . 'api/ApiCodeUpdate.php';
4242 $wgAutoloadClasses['ApiCodeDiff'] = $dir . 'api/ApiCodeDiff.php';
4343 $wgAutoloadClasses['ApiCodeComments'] = $dir . 'api/ApiCodeComments.php';
 44+$wgAutoloadClasses['ApiCodeTestUpload'] = $dir . 'api/ApiCodeTestUpload.php';
4445
4546 $wgAutoloadClasses['CodeDiffHighlighter'] = $dir . 'DiffHighlighter.php';
4647 $wgAutoloadClasses['CodeRepository'] = $dir . 'CodeRepository.php';
@@ -66,6 +67,10 @@
6768 $wgAutoloadClasses['CodeView'] = $dir . 'SpecialCode.php';
6869 $wgAutoloadClasses['SpecialRepoAdmin'] = $dir . 'SpecialRepoAdmin.php';
6970
 71+$wgAutoloadClasses['CodeTestSuite'] = $dir . 'CodeTestSuite.php';
 72+$wgAutoloadClasses['CodeTestRun'] = $dir . 'CodeTestRun.php';
 73+$wgAutoloadClasses['CodeTestResult'] = $dir . 'CodeTestResult.php';
 74+
7075 $wgSpecialPages['Code'] = 'SpecialCode';
7176 $wgSpecialPageGroups['Code'] = 'developer';
7277 $wgSpecialPages['RepoAdmin'] = 'SpecialRepoAdmin';
@@ -73,6 +78,7 @@
7479
7580 $wgAPIModules['codeupdate'] = 'ApiCodeUpdate';
7681 $wgAPIModules['codediff'] = 'ApiCodeDiff';
 82+$wgAPIModules['codetestupload'] = 'ApiCodeTestUpload';
7783 $wgAPIListModules['codecomments'] = 'ApiCodeComments';
7884
7985 $wgExtensionMessagesFiles['CodeReview'] = $dir . 'CodeReview.i18n.php';
@@ -129,6 +135,10 @@
130136 // What images can be used for client-side side-by-side comparisons?
131137 $wgCodeReviewImgRegex = '/\.(png|jpg|jpeg|gif)$/i';
132138
 139+// Set to a secret string for HMAC validation of test run data uploads.
 140+// Should match test runner's $wgParserTestRemote['secret'].
 141+$wgCodeReviewSharedSecret = false;
 142+
133143 # Schema changes
134144 $wgHooks['LoadExtensionSchemaUpdates'][] = 'efCodeReviewSchemaUpdates';
135145
@@ -141,6 +151,7 @@
142152 $wgExtNewIndexes[] = array( 'code_relations', 'repo_to_from', "$base/archives/code_relations_index.sql" );
143153 //$wgExtNewFields[] = array( 'code_rev', "$base/archives/codereview-cr_status.sql" ); // FIXME FIXME this is a change to options... don't know how
144154 $wgExtNewTables[] = array( 'code_bugs', "$base/archives/code_bugs.sql" );
 155+ $wgExtNewTables[] = array( 'code_test_suite', "$base/archives/codereview-code_tests.sql" );
145156 } elseif( $wgDBtype == 'postgres' ) {
146157 // TODO
147158 }
Index: trunk/extensions/CodeReview/CodeTestResult.php
@@ -0,0 +1,11 @@
 2+<?php
 3+
 4+class CodeTestResult {
 5+ function __construct( CodeTestRun $run, $row ) {
 6+ $this->run = $run;
 7+ $this->id = $row->ctresult_id;
 8+ $this->caseId = $row->ctresult_case_id;
 9+ $this->caseName = $row->ctcase_name;
 10+ $this->success = (bool)$row->ctresult_success;
 11+ }
 12+}
Property changes on: trunk/extensions/CodeReview/CodeTestResult.php
___________________________________________________________________
Name: svn:eol-style
113 + native
Index: trunk/extensions/CodeReview/CodeRepository.php
@@ -170,6 +170,26 @@
171171 throw new MWException( 'Failed to load expected revision data' );
172172 return CodeRevision::newFromRow( $this, $row );
173173 }
 174+
 175+ /**
 176+ * Load test suite information
 177+ */
 178+ public function getTestSuite( $name ) {
 179+ $dbr = wfGetDB( DB_SLAVE );
 180+ $row = $dbr->selectRow( 'code_test_suite',
 181+ '*',
 182+ array(
 183+ 'ctsuite_repo_id' => $this->getId(),
 184+ 'ctsuite_name' => $name,
 185+ ),
 186+ __METHOD__
 187+ );
 188+ if( $row ) {
 189+ return CodeTestSuite::newFromRow( $this, $row );
 190+ } else {
 191+ return null;
 192+ }
 193+ }
174194
175195 /**
176196 * @param int $rev Revision ID
Index: trunk/extensions/CodeReview/api/ApiCodeTestUpload.php
@@ -0,0 +1,119 @@
 2+<?php
 3+
 4+class ApiCodeTestUpload extends ApiBase {
 5+
 6+ public function execute() {
 7+ global $wgUser;
 8+ // Before doing anything at all, let's check permissions
 9+ if( !$wgUser->isAllowed('codereview-use') ) {
 10+ $this->dieUsage('You don\'t have permission to upload test results', 'permissiondenied');
 11+ }
 12+ $params = $this->extractRequestParams();
 13+
 14+ $this->validateParams( $params );
 15+ $this->validateHmac( $params );
 16+
 17+ $repo = CodeRepository::newFromName( $params['repo'] );
 18+ if( !$repo ) {
 19+ $this->dieUsage( "Invalid repo ``{$params['repo']}''", 'invalidrepo' );
 20+ }
 21+
 22+ $suite = $repo->getTestSuite( $params['suite'] );
 23+ if( !$suite ) {
 24+ $this->dieUsage( "Invalid test suite ``{$params['suite']}''", 'invalidsuite' );
 25+ }
 26+
 27+ // Note that we might be testing a revision that hasn't gotten slurped in yet,
 28+ // so we won't reject data for revisions we don't know about yet.
 29+ $revId = intval( $params['rev'] );
 30+
 31+ $status = $params['status'];
 32+ if( $status == 'running' || $status == 'aborted' ) {
 33+ // Set the 'tests running' flag so we can mark it...
 34+ $suite->setStatus( $revId, $status );
 35+ } elseif( $status == 'complete' ) {
 36+ // Save data and mark running test as completed.
 37+ $results = json_decode( $params['results'], true );
 38+ if( !is_array( $results ) ) {
 39+ $this->dieUsage( "Invalid test result data", 'invalidresults' );
 40+ }
 41+ $suite->saveResults( $revId, $results );
 42+ }
 43+ }
 44+
 45+ protected function validateParams( $params ) {
 46+ $required = array( 'repo', 'suite', 'rev', 'status', 'hmac' );
 47+ if( isset( $params['status'] ) && $params['status'] == 'complete' ) {
 48+ $required[] = 'results';
 49+ }
 50+ foreach( $required as $arg ) {
 51+ if ( !isset( $params[$arg] ) ) {
 52+ $this->dieUsageMsg( array( 'missingparam', $arg ) );
 53+ }
 54+ }
 55+ }
 56+
 57+ protected function validateHmac( $params ) {
 58+ global $wgCodeReviewSharedSecret;
 59+
 60+ // Generate a hash MAC to validate our credentials
 61+ $message = array(
 62+ $params['repo'],
 63+ $params['suite'],
 64+ $params['rev'],
 65+ $params['status'],
 66+ );
 67+ if( $params['status'] == "complete" ) {
 68+ $message[] = $params['results'];
 69+ }
 70+ $hmac = hash_hmac( "sha1", implode( "|", $message ), $wgCodeReviewSharedSecret );
 71+ if( $hmac != $params['hmac'] ) {
 72+ $this->dieUsageMsg( array( 'invalidhmac', $params['hmac'] ) );
 73+ }
 74+ }
 75+
 76+ public function mustBePosted() {
 77+ // Discourage casual browsing :)
 78+ return true;
 79+ }
 80+
 81+ public function isWriteMode() {
 82+ return true;
 83+ }
 84+
 85+ public function getAllowedParams() {
 86+ return array(
 87+ 'repo' => null,
 88+ 'suite' => null,
 89+ 'rev' => array(
 90+ ApiBase::PARAM_TYPE => 'integer',
 91+ ApiBase::PARAM_MIN => 1
 92+ ),
 93+ 'status' => array(
 94+ ApiBase::PARAM_TYPE => array( 'running', 'complete', 'abort' ),
 95+ ),
 96+ 'hmac' => null,
 97+ 'results' => null,
 98+ );
 99+ }
 100+
 101+ public function getParamDescription() {
 102+ return array(
 103+ 'repo' => 'Name of repository to update',
 104+ 'suite' => 'Name of test suite to record run results for',
 105+ 'rev' => 'Revision ID tests were run against',
 106+ 'status' => 'Status of test run',
 107+ 'hmac' => 'HMAC validation',
 108+ 'results' => 'JSON-encoded map of test names to success results, for status "complete"',
 109+ );
 110+ }
 111+
 112+ public function getDescription() {
 113+ return array(
 114+ 'Upload CodeReview test run results from a test runner.' );
 115+ }
 116+
 117+ public function getVersion() {
 118+ return __CLASS__ . ': $Id: ApiCodeUpdate.php 48928 2009-03-27 18:41:20Z catrope $';
 119+ }
 120+}
Property changes on: trunk/extensions/CodeReview/api/ApiCodeTestUpload.php
___________________________________________________________________
Name: svn:eol-style
1121 + native

Comments

#Comment by Catrope (talk | contribs)   10:07, 31 July 2009
$this->dieUsageMsg( array( 'invalidhmac', $params['hmac'] ) );

This doesn't work unless you define the 'invalidhmac' message in ApiBase::$messageMap

+		if( !$wgUser->isAllowed('codereview-use') ) {
+			$this->dieUsage('You don\'t have permission to upload test results', 'permissiondenied');
+		}

AFAICT this is only useful if the codereview-use right is granted to anons, since clients don't log in but use HMAC instead.

Status & tagging log