diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -67,7 +67,7 @@ 'rsrc/css/application/feed/feed.css' => 'b513b5f4', 'rsrc/css/application/files/global-drag-and-drop.css' => '697324ad', 'rsrc/css/application/flag/flag.css' => '5337623f', - 'rsrc/css/application/harbormaster/harbormaster.css' => '49d64eb4', + 'rsrc/css/application/harbormaster/harbormaster.css' => '661ada8b', 'rsrc/css/application/herald/herald-test.css' => '778b008e', 'rsrc/css/application/herald/herald.css' => '826075fa', 'rsrc/css/application/home/home.css' => 'e34bf140', @@ -529,7 +529,7 @@ 'font-fontawesome' => '21b0ced7', 'font-source-sans-pro' => 'f5c0ffcb', 'global-drag-and-drop-css' => '697324ad', - 'harbormaster-css' => '49d64eb4', + 'harbormaster-css' => '661ada8b', 'herald-css' => '826075fa', 'herald-rule-editor' => '6e2de6f2', 'herald-test-css' => '778b008e', diff --git a/resources/sql/autopatches/20150206.harbormasteritem.1.sql b/resources/sql/autopatches/20150206.harbormasteritem.1.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20150206.harbormasteritem.1.sql @@ -0,0 +1,12 @@ +CREATE TABLE {$NAMESPACE}_harbormaster.harbormaster_builditem ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + buildTargetPHID VARBINARY(64) NOT NULL, + name VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT}, + type VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + details LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + UNIQUE KEY `key_type` (buildTargetPHID, type) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -874,6 +874,7 @@ 'HarbormasterStepDeleteController' => 'applications/harbormaster/controller/HarbormasterStepDeleteController.php', 'HarbormasterStepEditController' => 'applications/harbormaster/controller/HarbormasterStepEditController.php', 'HarbormasterTargetWorker' => 'applications/harbormaster/worker/HarbormasterTargetWorker.php', + 'HarbormasterTestBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterTestBuildStepImplementation.php', 'HarbormasterThrowExceptionBuildStep' => 'applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php', 'HarbormasterUIEventListener' => 'applications/harbormaster/event/HarbormasterUIEventListener.php', 'HarbormasterUploadArtifactBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php', @@ -3939,7 +3940,10 @@ 'HarbormasterBuildEngine' => 'Phobject', 'HarbormasterBuildFailureException' => 'Exception', 'HarbormasterBuildGraph' => 'AbstractDirectedGraph', - 'HarbormasterBuildItem' => 'HarbormasterDAO', + 'HarbormasterBuildItem' => array( + 'HarbormasterDAO', + 'PhabricatorPolicyInterface', + ), 'HarbormasterBuildItemPHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildItemQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildLog' => array( @@ -4039,6 +4043,7 @@ 'HarbormasterStepDeleteController' => 'HarbormasterController', 'HarbormasterStepEditController' => 'HarbormasterController', 'HarbormasterTargetWorker' => 'HarbormasterWorker', + 'HarbormasterTestBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterThrowExceptionBuildStep' => 'HarbormasterBuildStepImplementation', 'HarbormasterUIEventListener' => 'PhabricatorEventListener', 'HarbormasterUploadArtifactBuildStepImplementation' => 'HarbormasterBuildStepImplementation', diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php --- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php @@ -188,6 +188,7 @@ $targets[] = $target_box; + $targets[] = $this->buildUnitItems($build, $build_target); $targets[] = $this->buildLog($build, $build_target); } @@ -236,6 +237,209 @@ return $list; } + private function buildUnitItems( + HarbormasterBuild $build, + HarbormasterBuildTarget $build_target) { + + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $items = id(new HarbormasterBuildItemQuery()) + ->setViewer($viewer) + ->withTypes(array(HarbormasterBuildItem::TYPE_UNIT)) + ->withBuildTargetPHIDs(array($build_target->getPHID())) + ->execute(); + + if (count($items) === 0) { + return null; + } + + $rows = array(); + + $this->requireResource('differential-core-view-css'); + $this->requireResource('differential-revision-history-css'); + + $show_limit = 10; + $hidden = array(); + $totals = array(); + + $udata = mpull($items, 'getDetails'); + if ($udata) { + $sort_map = array( + ArcanistUnitTestResult::RESULT_BROKEN => 0, + ArcanistUnitTestResult::RESULT_FAIL => 1, + ArcanistUnitTestResult::RESULT_UNSOUND => 2, + ArcanistUnitTestResult::RESULT_SKIP => 3, + ArcanistUnitTestResult::RESULT_POSTPONED => 4, + ArcanistUnitTestResult::RESULT_PASS => 5, + ); + + foreach ($udata as $key => $test) { + $udata[$key]['sort'] = idx($sort_map, idx($test, 'result')); + } + $udata = isort($udata, 'sort'); + $engine = new PhabricatorMarkupEngine(); + $engine->setViewer($this->getViewer()); + $markup_objects = array(); + foreach ($udata as $key => $test) { + $userdata = idx($test, 'userData'); + if ($userdata) { + if ($userdata !== false) { + $userdata = str_replace("\000", '', $userdata); + } + $markup_object = id(new PhabricatorMarkupOneOff()) + ->setContent($userdata) + ->setPreserveLinebreaks(true); + $engine->addObject($markup_object, 'default'); + $markup_objects[$key] = $markup_object; + } + } + $engine->process(); + foreach ($udata as $key => $test) { + $result = idx($test, 'result'); + + $default_hide = false; + switch ($result) { + case ArcanistUnitTestResult::RESULT_POSTPONED: + $default_hide = true; + break; + } + + if ($show_limit && !$default_hide) { + --$show_limit; + $show = true; + } else { + $show = false; + if (empty($hidden[$result])) { + $hidden[$result] = 0; + } + $hidden[$result]++; + } + + if (empty($totals[$result])) { + $totals[$result] = 0; + } + $totals[$result]++; + if (empty($totals['total'])) { + $totals['total'] = 0; + } + $totals['total']++; + + $value = idx($test, 'name'); + + $namespace = idx($test, 'namespace'); + if ($namespace) { + $value = $namespace.'::'.$value; + } + + if (!empty($test['link'])) { + $value = phutil_tag( + 'a', + array( + 'href' => $test['link'], + 'target' => '_blank', + ), + $value); + } + $rows[] = array( + 'style' => $this->getUnitResultStyle($result), + 'name' => ucwords($result), + 'value' => $value, + 'show' => $show, + ); + + if (isset($markup_objects[$key])) { + $rows[] = array( + 'style' => 'details', + 'value' => $engine->getOutput($markup_objects[$key], 'default'), + 'show' => false, + ); + if (empty($hidden['details'])) { + $hidden['details'] = 0; + } + $hidden['details']++; + } + } + } + + $show_string = $this->renderUnitShowString($hidden); + + $view = new DifferentialResultsTableView(); + $view->setRows($rows); + $view->setShowMoreString($show_string); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Test Results')) + ->setSubheader($this->getUnitSummary($totals)) + ->setUser($viewer); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setForm($view); + + return $box; + } + + private function getUnitResultStyle($result) { + $map = array( + ArcanistUnitTestResult::RESULT_PASS => 'green', + ArcanistUnitTestResult::RESULT_FAIL => 'red', + ArcanistUnitTestResult::RESULT_SKIP => 'blue', + ArcanistUnitTestResult::RESULT_BROKEN => 'red', + ArcanistUnitTestResult::RESULT_UNSOUND => 'yellow', + ArcanistUnitTestResult::RESULT_POSTPONED => 'blue', + ); + return idx($map, $result); + } + + private function getUnitSummary(array $hidden) { + // Reorder hidden things by severity. + $hidden = array_select_keys( + $hidden, + array( + ArcanistUnitTestResult::RESULT_BROKEN, + ArcanistUnitTestResult::RESULT_FAIL, + ArcanistUnitTestResult::RESULT_UNSOUND, + ArcanistUnitTestResult::RESULT_SKIP, + ArcanistUnitTestResult::RESULT_POSTPONED, + ArcanistUnitTestResult::RESULT_PASS, + 'details', + 'total', + )) + $hidden; + + $noun = array( + ArcanistUnitTestResult::RESULT_BROKEN => 'Broken', + ArcanistUnitTestResult::RESULT_FAIL => 'Failed', + ArcanistUnitTestResult::RESULT_UNSOUND => 'Unsound', + ArcanistUnitTestResult::RESULT_SKIP => 'Skipped', + ArcanistUnitTestResult::RESULT_POSTPONED => 'Postponed', + ArcanistUnitTestResult::RESULT_PASS => 'Passed', + ); + + $show = array(); + foreach ($hidden as $key => $value) { + if ($key == 'details') { + $show[] = pht('%d Detail(s)', $value); + } else if ($key == 'total') { + $show[] = pht('%d Total', $value); + } else { + $show[] = $value.' '.idx($noun, $key); + } + } + + return implode(', ', $show); + } + + private function renderUnitShowString(array $hidden) { + if (!$hidden) { + return null; + } + + $summary = $this->getUnitSummary($hidden); + + return 'Show Full Unit Results ('.$summary.')'; + } + private function buildLog( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { diff --git a/src/applications/harbormaster/query/HarbormasterBuildItemQuery.php b/src/applications/harbormaster/query/HarbormasterBuildItemQuery.php --- a/src/applications/harbormaster/query/HarbormasterBuildItemQuery.php +++ b/src/applications/harbormaster/query/HarbormasterBuildItemQuery.php @@ -5,6 +5,8 @@ private $ids; private $phids; + private $buildTargetPHIDs; + private $types; public function withIDs(array $ids) { $this->ids = $ids; @@ -16,6 +18,16 @@ return $this; } + public function withBuildTargetPHIDs(array $phids) { + $this->buildTargetPHIDs = $phids; + return $this; + } + + public function withTypes(array $types) { + $this->types = $types; + return $this; + } + protected function loadPage() { $table = new HarbormasterBuildItem(); $conn_r = $table->establishConnection('r'); @@ -31,6 +43,31 @@ return $table->loadAllFromArray($data); } + protected function willFilterPage(array $page) { + $build_targets = array(); + + $build_target_phids = array_filter(mpull($page, 'getBuildTargetPHID')); + if ($build_target_phids) { + $build_targets = id(new HarbormasterBuildTargetQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($build_target_phids) + ->setParentQuery($this) + ->execute(); + $build_targets = mpull($build_targets, null, 'getPHID'); + } + + foreach ($page as $key => $build_log) { + $build_target_phid = $build_log->getBuildTargetPHID(); + if (empty($build_targets[$build_target_phid])) { + unset($page[$key]); + continue; + } + $build_log->attachBuildTarget($build_targets[$build_target_phid]); + } + + return $page; + } + private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); @@ -48,6 +85,20 @@ $this->phids); } + if ($this->buildTargetPHIDs) { + $where[] = qsprintf( + $conn_r, + 'buildTargetPHID in (%Ls)', + $this->buildTargetPHIDs); + } + + if ($this->types) { + $where[] = qsprintf( + $conn_r, + 'type in (%Ls)', + $this->types); + } + $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); diff --git a/src/applications/harbormaster/step/HarbormasterTestBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterTestBuildStepImplementation.php new file mode 100644 --- /dev/null +++ b/src/applications/harbormaster/step/HarbormasterTestBuildStepImplementation.php @@ -0,0 +1,139 @@ +formatSettingForDescription('hostartifact')); + } + + public function execute( + HarbormasterBuild $build, + HarbormasterBuildTarget $build_target) { + + $settings = $this->getSettings(); + $variables = $build_target->getVariables(); + + $artifact = $build->loadArtifact($settings['hostartifact']); + + $lease = $artifact->loadDrydockLease(); + + $interface = $lease->getInterface('command'); + + $future = $interface->getExecFuture( + 'cd %s && arc unit --output json-realtime --everything', + $settings['directory']); + + $log_stderr = $build->createLog($build_target, 'remote', 'stderr'); + + $start_stderr = $log_stderr->start(); + + $build_update = 5; + + $stdout_buffer = ''; + $err = 0; + + $futures = new FutureIterator(array($future)); + foreach ($futures->setUpdateInterval(1) as $key => $future_iter) { + if ($future_iter !== null) { + list($err) = $future->resolve(); + } else { + // Check to see if we should abort. + if ($build_update <= 0) { + $build->reload(); + if ($this->shouldAbort($build, $build_target)) { + $future->resolveKill(); + throw new HarbormasterBuildAbortedException(); + } else { + $build_update = 5; + } + } else { + $build_update -= 1; + } + } + + list($stdout, $stderr) = $future->read(); + $stdout_buffer .= $stdout; + $log_stderr->append($stderr); + $future->discardBuffers(); + + $lines = phutil_split_lines($stdout_buffer); + if (strlen($stdout_buffer) > 0 && + $stdout_buffer[strlen($stdout_buffer) - 1] !== "\n") { + + $stdout_buffer = $lines[count($lines) - 1]; + array_pop($lines); + } else { + $stdout_buffer = ''; + } + + foreach ($lines as $line) { + if (strlen($line) > strlen('##arc-unit## ') && + substr($line, 0, strlen('##arc-unit## ')) === '##arc-unit## ') { + // We have output from 'arc unit'. + $data = phutil_json_decode(substr($line, strlen('##arc-unit## '))); + + $name = idx($data, 'name'); + if (idx($data, 'namespace') !== null) { + $name = idx($data, 'namespace').'::'.$name; + } + + $item = HarbormasterBuildItem::initializeNewBuildItem( + $build_target, + HarbormasterBuildItem::TYPE_UNIT); + $item->setName($name); + $item->setDetails($data); + $item->save(); + } + } + + if ($future_iter !== null) { + break; + } + } + + $log_stderr->finalize($start_stderr); + + if ($err) { + throw new HarbormasterBuildFailureException(); + } + } + + public function getArtifactInputs() { + return array( + array( + 'name' => pht('Run on Host'), + 'key' => $this->getSetting('hostartifact'), + 'type' => HarbormasterBuildArtifact::TYPE_HOST, + ), + ); + } + + public function getFieldSpecifications() { + return array( + 'directory' => array( + 'name' => pht('Directory'), + 'type' => 'text', + 'required' => true, + 'caption' => pht( + 'The relative directory to run \'arc unit\' in.'), + ), + 'hostartifact' => array( + 'name' => pht('Host'), + 'type' => 'text', + 'required' => true, + ), + ); + } + +} diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildItem.php b/src/applications/harbormaster/storage/build/HarbormasterBuildItem.php --- a/src/applications/harbormaster/storage/build/HarbormasterBuildItem.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildItem.php @@ -1,19 +1,85 @@ setBuildTargetPHID($build_target->getPHID()) + ->setType($type); + } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, - self::CONFIG_NO_TABLE => true, + self::CONFIG_SERIALIZATION => array( + 'details' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'type' => 'text32', + 'name' => 'text255', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_type' => array( + 'columns' => array('buildTargetPHID', 'type'), + ), + ), ) + parent::getConfiguration(); } + public function attachBuildTarget(HarbormasterBuildTarget $build_target) { + $this->buildTarget = $build_target; + return $this; + } + + public function getBuildTarget() { + return $this->assertAttached($this->buildTarget); + } + public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildItemPHIDType::TYPECONST); } + public function getResult() { + return idx($this->getDetails(), 'result'); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return $this->getBuildTarget()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getBuildTarget()->hasAutomaticCapability( + $capability, + $viewer); + } + + public function describeAutomaticCapability($capability) { + return pht( + 'Users must be able to see a buildable to see its artifacts.'); + } + } diff --git a/webroot/rsrc/css/application/harbormaster/harbormaster.css b/webroot/rsrc/css/application/harbormaster/harbormaster.css --- a/webroot/rsrc/css/application/harbormaster/harbormaster.css +++ b/webroot/rsrc/css/application/harbormaster/harbormaster.css @@ -25,3 +25,16 @@ padding: 12px; color: {$darkgreytext}; } + +/* + * Doesn't appear to be any way of setting custom + * classes on PHUI boxes to limit the scope of these + * styles, so just assume that if harbormaster-css is + * included, then we're rendering on a Harbormaster + * build view page. + */ + +div.phui-box table.differential-results-table { + width: 100%; + line-height: 20px; +}