diff --git a/.arclint b/.arclint --- a/.arclint +++ b/.arclint @@ -62,7 +62,8 @@ "xhpast": { "type": "xhpast", "include": "(\\.php$)", - "standard": "phutil.xhpast" + "standard": "phutil.xhpast", + "xhpast.php-version": "5.5.0" } } } 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 @@ -216,7 +216,16 @@ 'ArcanistGoTestResultParserTestCase' => 'unit/parser/__tests__/ArcanistGoTestResultParserTestCase.php', 'ArcanistHLintLinter' => 'lint/linter/ArcanistHLintLinter.php', 'ArcanistHLintLinterTestCase' => 'lint/linter/__tests__/ArcanistHLintLinterTestCase.php', + 'ArcanistHardpoint' => 'hardpoint/ArcanistHardpoint.php', + 'ArcanistHardpointEngine' => 'hardpoint/ArcanistHardpointEngine.php', + 'ArcanistHardpointFutureList' => 'hardpoint/ArcanistHardpointFutureList.php', + 'ArcanistHardpointList' => 'hardpoint/ArcanistHardpointList.php', 'ArcanistHardpointLoader' => 'loader/ArcanistHardpointLoader.php', + 'ArcanistHardpointObject' => 'hardpoint/ArcanistHardpointObject.php', + 'ArcanistHardpointQuery' => 'hardpoint/ArcanistHardpointQuery.php', + 'ArcanistHardpointRequest' => 'hardpoint/ArcanistHardpointRequest.php', + 'ArcanistHardpointRequestList' => 'hardpoint/ArcanistHardpointRequestList.php', + 'ArcanistHardpointTask' => 'hardpoint/ArcanistHardpointTask.php', 'ArcanistHelpWorkflow' => 'toolset/workflow/ArcanistHelpWorkflow.php', 'ArcanistHexadecimalNumericScalarCasingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistHexadecimalNumericScalarCasingXHPASTLinterRule.php', 'ArcanistHexadecimalNumericScalarCasingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistHexadecimalNumericScalarCasingXHPASTLinterRuleTestCase.php', @@ -393,6 +402,7 @@ 'ArcanistRuntime' => 'runtime/ArcanistRuntime.php', 'ArcanistRuntimeConfigurationSource' => 'config/source/ArcanistRuntimeConfigurationSource.php', 'ArcanistScalarConfigOption' => 'config/option/ArcanistScalarConfigOption.php', + 'ArcanistScalarHardpoint' => 'hardpoint/ArcanistScalarHardpoint.php', 'ArcanistScriptAndRegexLinter' => 'lint/linter/ArcanistScriptAndRegexLinter.php', 'ArcanistSelfClassReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSelfClassReferenceXHPASTLinterRule.php', 'ArcanistSelfClassReferenceXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistSelfClassReferenceXHPASTLinterRuleTestCase.php', @@ -472,6 +482,7 @@ 'ArcanistVariableReferenceSpacingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistVariableReferenceSpacingXHPASTLinterRuleTestCase.php', 'ArcanistVariableVariableXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistVariableVariableXHPASTLinterRule.php', 'ArcanistVariableVariableXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistVariableVariableXHPASTLinterRuleTestCase.php', + 'ArcanistVectorHardpoint' => 'hardpoint/ArcanistVectorHardpoint.php', 'ArcanistVersionWorkflow' => 'toolset/workflow/ArcanistVersionWorkflow.php', 'ArcanistWeldWorkflow' => 'workflow/ArcanistWeldWorkflow.php', 'ArcanistWhichWorkflow' => 'workflow/ArcanistWhichWorkflow.php', @@ -1161,7 +1172,16 @@ 'ArcanistGoTestResultParserTestCase' => 'PhutilTestCase', 'ArcanistHLintLinter' => 'ArcanistExternalLinter', 'ArcanistHLintLinterTestCase' => 'ArcanistExternalLinterTestCase', + 'ArcanistHardpoint' => 'Phobject', + 'ArcanistHardpointEngine' => 'Phobject', + 'ArcanistHardpointFutureList' => 'Phobject', + 'ArcanistHardpointList' => 'Phobject', 'ArcanistHardpointLoader' => 'Phobject', + 'ArcanistHardpointObject' => 'Phobject', + 'ArcanistHardpointQuery' => 'Phobject', + 'ArcanistHardpointRequest' => 'Phobject', + 'ArcanistHardpointRequestList' => 'Phobject', + 'ArcanistHardpointTask' => 'Phobject', 'ArcanistHelpWorkflow' => 'ArcanistWorkflow', 'ArcanistHexadecimalNumericScalarCasingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistHexadecimalNumericScalarCasingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', @@ -1337,6 +1357,7 @@ 'ArcanistRubyLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistRuntimeConfigurationSource' => 'ArcanistDictionaryConfigurationSource', 'ArcanistScalarConfigOption' => 'ArcanistConfigOption', + 'ArcanistScalarHardpoint' => 'ArcanistHardpoint', 'ArcanistScriptAndRegexLinter' => 'ArcanistLinter', 'ArcanistSelfClassReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistSelfClassReferenceXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', @@ -1416,6 +1437,7 @@ 'ArcanistVariableReferenceSpacingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistVariableVariableXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistVariableVariableXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistVectorHardpoint' => 'ArcanistHardpoint', 'ArcanistVersionWorkflow' => 'ArcanistWorkflow', 'ArcanistWeldWorkflow' => 'ArcanistArcWorkflow', 'ArcanistWhichWorkflow' => 'ArcanistWorkflow', diff --git a/src/hardpoint/ArcanistHardpoint.php b/src/hardpoint/ArcanistHardpoint.php new file mode 100644 --- /dev/null +++ b/src/hardpoint/ArcanistHardpoint.php @@ -0,0 +1,32 @@ +hardpointKey = $hardpoint_key; + return $this; + } + + public function getHardpointKey() { + return $this->hardpointKey; + } + + abstract public function isVectorHardpoint(); + + public function mergeHardpointValues( + ArcanistHardpointObject $object, + $old, + $new) { + + throw new Exception( + pht( + 'Hardpoint ("%s", of type "%s") does not support merging '. + 'values.', + $this->getHardpointKey(), + get_class($this))); + } + +} diff --git a/src/hardpoint/ArcanistHardpointEngine.php b/src/hardpoint/ArcanistHardpointEngine.php new file mode 100644 --- /dev/null +++ b/src/hardpoint/ArcanistHardpointEngine.php @@ -0,0 +1,237 @@ +queries = $queries; + $this->queryHardpointMap = null; + + return $this; + } + + private function getQueriesForHardpoint($hardpoint) { + if ($this->queryHardpointMap === null) { + $map = array(); + + foreach ($this->queries as $query_key => $query) { + $query->setHardpointEngine($this); + + $hardpoints = $query->getHardpoints(); + + foreach ($hardpoints as $query_hardpoint) { + $map[$query_hardpoint][$query_key] = $query; + } + } + + $this->queryHardpointMap = $map; + } + + return idx($this->queryHardpointMap, $hardpoint, array()); + } + + public function requestHardpoints(array $objects, array $requests) { + assert_instances_of($objects, 'ArcanistHardpointObject'); + + $results = array(); + foreach ($requests as $request) { + $request = ArcanistHardpointRequest::newFromSpecification($request) + ->setEngine($this) + ->setObjects($objects); + + $this->requests[] = $request; + + $this->startRequest($request); + + $results[] = $request; + } + + return ArcanistHardpointRequestList::newFromRequests($results); + } + + private function startRequest(ArcanistHardpointRequest $request) { + $objects = $request->getObjects(); + $hardpoint = $request->getHardpoint(); + + $queries = $this->getQueriesForHardpoint($hardpoint); + + $load = array(); + foreach ($objects as $object_key => $object) { + if (!$object->hasHardpoint($hardpoint)) { + throw new Exception( + pht( + 'Object (with key "%s", of type "%s") has no hardpoint "%s". '. + 'Hardpoints on this object are: %s.', + $object_key, + phutil_describe_type($object), + $hardpoint, + $object->getHardpointList()->getHardpointListForDisplay())); + } + + // If the object already has the hardpoint attached, we don't have to + // do anything. Throw the object away. + + if ($object->hasAttachedHardpoint($hardpoint)) { + unset($objects[$object_key]); + continue; + } + + $any_query = false; + foreach ($queries as $query_key => $query) { + if (!$query->canLoadObject($object)) { + continue; + } + + $any_query = true; + $load[$query_key][$object_key] = $object; + } + + if (!$any_query) { + throw new Exception( + pht( + 'No query exists which can load hardpoint "%s" for object '. + '(with key "%s" of type "%s").', + $hardpoint, + $object_key, + phutil_describe_type($object))); + } + } + + if (!$objects) { + return; + } + + $any_object = head($objects); + $list = $object->getHardpointList(); + $definition = $list->getHardpointDefinition($any_object, $hardpoint); + + $is_vector = ($definition->isVectorHardpoint()); + + if ($is_vector) { + foreach ($objects as $object) { + $object->attachHardpoint($hardpoint, array()); + } + } + + $request->setHardpointDefinition($definition); + + foreach ($load as $query_key => $object_map) { + $query = id(clone $queries[$query_key]); + + $task = $request->newTask() + ->setQuery($query) + ->setObjects($object_map); + } + } + + public function waitForRequests(array $wait_requests) { + foreach ($wait_requests as $wait_key => $wait_request) { + if ($wait_request->getEngine() !== $this) { + throw new Exception( + pht( + 'Attempting to wait on a hardpoint request (with index "%s", for '. + 'hardpoint "%s") that is part of a different engine.', + $wait_key, + $wait_request->getHardpoint())); + } + } + + while (true) { + $any_progress = false; + foreach ($this->requests as $req_key => $request) { + $did_update = $request->updateTasks(); + if ($did_update) { + $any_progress = true; + } + } + + // If we made progress by directly executing requests, continue + // excuting them until we stop making progress. We want to queue all + // reachable futures before we wait on futures. + + if ($any_progress) { + continue; + } + + foreach ($this->requests as $request_key => $request) { + if ($request->isComplete()) { + unset($this->requests[$request_key]); + } + } + + if (!$this->requests) { + break; + } + + $resolved_key = $this->updateFutures(); + + if ($resolved_key === null) { + throw new Exception( + pht( + 'Hardpoint engine can not resolve: no request made progress '. + 'during the last update cycle and there are no futures '. + 'awaiting resolution.')); + } + } + } + + private function updateFutures() { + $iterator = $this->futureIterator; + + $is_rewind = false; + $wait_futures = $this->waitFutures; + if ($wait_futures) { + if (!$this->futureIterator) { + $iterator = new FutureIterator(array()); + foreach ($wait_futures as $wait_future) { + $iterator->addFuture($wait_future); + } + $is_rewind = true; + $this->futureIterator = $iterator; + } else { + foreach ($wait_futures as $wait_future) { + $iterator->addFuture($wait_future); + } + } + $this->waitFutures = array(); + } + + $resolved_key = null; + if ($iterator) { + if ($is_rewind) { + $iterator->rewind(); + } else { + $iterator->next(); + } + + if ($iterator->valid()) { + $resolved_key = $iterator->key(); + } else { + $this->futureIterator = null; + } + } + + return $resolved_key; + } + + public function addFutures(array $futures) { + assert_instances_of($futures, 'Future'); + $this->waitFutures += mpull($futures, null, 'getFutureKey'); + + // TODO: We could reasonably add these futures to the iterator + // immediately and start them here, instead of waiting. + + return $this; + } + +} diff --git a/src/hardpoint/ArcanistHardpointFutureList.php b/src/hardpoint/ArcanistHardpointFutureList.php new file mode 100644 --- /dev/null +++ b/src/hardpoint/ArcanistHardpointFutureList.php @@ -0,0 +1,31 @@ +futures = $futures; + + return $object; + } + + public function getFutures() { + return $this->futures; + } + + public function setSendResult($send_result) { + $this->sendResult = $send_result; + return $this; + } + + public function getSendResult() { + return $this->sendResult; + } + +} diff --git a/src/hardpoint/ArcanistHardpointList.php b/src/hardpoint/ArcanistHardpointList.php new file mode 100644 --- /dev/null +++ b/src/hardpoint/ArcanistHardpointList.php @@ -0,0 +1,121 @@ + $hardpoint) { + $key = $hardpoint->getHardpointKey(); + + if (!strlen($key)) { + throw new Exception( + pht( + 'Hardpoint (at index "%s") has no hardpoint key. Each hardpoint '. + 'must have a key that is unique among hardpoints on the object.', + $idx)); + } + + if (isset($map[$key])) { + throw new Exception( + pht( + 'Hardpoint (at index "%s") has the same key ("%s") as an earlier '. + 'hardpoint. Each hardpoint must have a key that is unique '. + 'among hardpoints on the object.')); + } + + $map[$key] = $hardpoint; + } + + $this->hardpoints = $map; + + return $this; + } + + public function hasHardpoint($object, $hardpoint) { + return isset($this->hardpoints[$hardpoint]); + } + + public function hasAttachedHardpoint($object, $hardpoint) { + return isset($this->attached[$hardpoint]); + } + + public function getHardpointDefinition($object, $hardpoint) { + if (!$this->hasHardpoint($object, $hardpoint)) { + throw new Exception( + pht( + 'Hardpoint ("%s") is not registered on this object (of type "%s") '. + 'so the definition object does not exist. Hardpoints are: %s.', + $hardpoint, + phutil_describe_type($object), + $this->getHardpointListForDisplay())); + } + + return $this->hardpoints[$hardpoint]; + } + + public function getHardpoint($object, $hardpoint) { + if (!$this->hasHardpoint($object, $hardpoint)) { + throw new Exception( + pht( + 'Hardpoint ("%s") is not registered on this object (of type "%s"). '. + 'Hardpoints are: %s.', + $hardpoint, + phutil_describe_type($object), + $this->getHardpointListForDisplay())); + } + + if (!$this->hasAttachedHardpoint($object, $hardpoint)) { + throw new Exception( + pht( + 'Hardpoint data (for hardpoint "%s") is not attached.', + $hardpoint)); + } + + return $this->data[$hardpoint]; + } + + public function setHardpointValue($object, $hardpoint, $value) { + if (!$this->hasHardpoint($object, $hardpoint)) { + throw new Exception( + pht( + 'Hardpoint ("%s") is not registered on this object (of type "%s"). '. + 'Hardpoints are: %s.', + $hardpoint, + phutil_describe_type($object), + $this->getHardpointListforDisplay())); + } + + $this->attached[$hardpoint] = true; + $this->data[$hardpoint] = $value; + } + + public function attachHardpoint($object, $hardpoint, $value) { + if ($this->hasAttachedHardpoint($object, $hardpoint)) { + throw new Exception( + pht( + 'Hardpoint ("%s") already has attached data.', + $hardpoint)); + } + + $this->setHardpointValue($object, $hardpoint, $value); + } + + public function getHardpointListForDisplay() { + $list = array_keys($this->hardpoints); + + if ($list) { + sort($list); + return implode(', ', $list); + } + + return pht(''); + } + +} diff --git a/src/hardpoint/ArcanistHardpointObject.php b/src/hardpoint/ArcanistHardpointObject.php new file mode 100644 --- /dev/null +++ b/src/hardpoint/ArcanistHardpointObject.php @@ -0,0 +1,91 @@ +getHardpointList()->getHardpoint( + $this, + $hardpoint); + } + + final public function attachHardpoint($hardpoint, $value) { + $this->getHardpointList()->attachHardpoint( + $this, + $hardpoint, + $value); + + return $this; + } + + final public function mergeHardpoint($hardpoint, $value) { + $hardpoint_list = $this->getHardpointList(); + $hardpoint_def = $hardpoint_list->getHardpointDefinition( + $this, + $hardpoint); + + $old_value = $this->getHardpoint($hardpoint); + $new_value = $hardpoint_def->mergeHardpointValues( + $this, + $old_value, + $value); + + $hardpoint_list->setHardpointValue( + $this, + $hardpoint, + $new_value); + + return $this; + } + + final public function hasHardpoint($hardpoint) { + return $this->getHardpointList()->hasHardpoint($this, $hardpoint); + } + + final public function hasAttachedHardpoint($hardpoint) { + return $this->getHardpointList()->hasAttachedHardpoint( + $this, + $hardpoint); + } + + protected function newHardpoints() { + return array(); + } + + final protected function newHardpoint($hardpoint_key) { + return id(new ArcanistScalarHardpoint()) + ->setHardpointKey($hardpoint_key); + } + + final protected function newVectorHardpoint($hardpoint_key) { + return id(new ArcanistVectorHardpoint()) + ->setHardpointKey($hardpoint_key); + } + + + final public function getHardpointList() { + if ($this->hardpointList === null) { + $list = $this->newHardpointList(); + + // TODO: Cache the hardpoint list with the class name as a key? If so, + // it needs to be purged when the request cache is purged. + + $hardpoints = $this->newHardpoints(); + + // TODO: Verify the hardpoints list is structured properly. + + $list->setHardpoints($hardpoints); + + $this->hardpointList = $list; + } + + return $this->hardpointList; + } + + private function newHardpointList() { + return new ArcanistHardpointList(); + } + +} diff --git a/src/hardpoint/ArcanistHardpointQuery.php b/src/hardpoint/ArcanistHardpointQuery.php new file mode 100644 --- /dev/null +++ b/src/hardpoint/ArcanistHardpointQuery.php @@ -0,0 +1,36 @@ +hardpointEngine = $engine; + return $this; + } + + final public function getHardpointEngine() { + return $this->hardpointEngine; + } + + abstract public function getHardpoints(); + abstract public function canLoadObject(ArcanistHardpointObject $object); + abstract public function loadHardpoint(array $objects, $hardpoint); + + final protected function yieldFuture(Future $future) { + return $this->yieldFutures(array($future)) + ->setSendResult(true); + } + + final protected function yieldFutures(array $futures) { + return ArcanistHardpointFutureList::newFromFutures($futures); + } + + final protected function yieldRequests(array $objects, $requests) { + $engine = $this->getHardpointEngine(); + $requests = $engine->requestHardpoints($objects, $requests); + return $requests; + } + +} diff --git a/src/hardpoint/ArcanistHardpointRequest.php b/src/hardpoint/ArcanistHardpointRequest.php new file mode 100644 --- /dev/null +++ b/src/hardpoint/ArcanistHardpointRequest.php @@ -0,0 +1,132 @@ +setHardpoint($spec); + } + + throw new Exception( + pht( + 'Unknown Hardpoint request specification (of type "%s").', + phutil_describe_type($spec))); + } + + public function setEngine(ArcanistHardpointEngine $engine) { + $this->engine = $engine; + return $this; + } + + public function getEngine() { + return $this->engine; + } + + public function setHardpoint($hardpoint) { + $this->hardpoint = $hardpoint; + return $this; + } + + public function getHardpoint() { + return $this->hardpoint; + } + + public function setObjects(array $objects) { + $this->objects = $objects; + return $this; + } + + public function getObjects() { + return $this->objects; + } + + public function newTask() { + $task = id(new ArcanistHardpointTask()) + ->setRequest($this); + + $this->tasks[] = $task; + $this->isComplete = false; + + return $task; + } + + public function isComplete() { + return $this->isComplete; + } + + public function getTasks() { + return $this->tasks; + } + + public function updateTasks() { + $any_progress = false; + + foreach ($this->tasks as $task) { + $did_update = $task->updateTask(); + if ($did_update) { + $any_progress = true; + } + } + + foreach ($this->tasks as $task_key => $task) { + if ($task->isComplete()) { + unset($this->tasks[$task_key]); + } + } + + if (!$this->tasks) { + + // TODO: We can skip or modify this check if the hardpoint is a vector + // hardpoint. + + $objects = $this->getObjects(); + $hardpoint = $this->getHardpoint(); + foreach ($objects as $object) { + if (!$object->hasAttachedHardpoint($hardpoint)) { + throw new Exception( + pht( + 'Unable to load hardpoint "%s" for object (of type "%s"). '. + 'All hardpoint query tasks resolved but none attached '. + 'a value to the hardpoint.', + $hardpoint, + phutil_describe_type($object))); + } + } + + // We may arrive here if a request is queued that can be satisfied + // immediately, most often because it requests hardpoints which are + // already attached. We don't have to do any work, so we have no tasks + // to update or complete and can complete the request immediately. + if (!$this->isComplete) { + $any_progress = true; + } + + $this->isComplete = true; + } + + return $any_progress; + } + + + public function setHardpointDefinition($hardpoint_definition) { + $this->hardpointDefinition = $hardpoint_definition; + return $this; + } + + public function getHardpointDefinition() { + return $this->hardpointDefinition; + } + +} diff --git a/src/hardpoint/ArcanistHardpointRequestList.php b/src/hardpoint/ArcanistHardpointRequestList.php new file mode 100644 --- /dev/null +++ b/src/hardpoint/ArcanistHardpointRequestList.php @@ -0,0 +1,21 @@ +requests = $requests; + + return $object; + } + + public function getRequests() { + return $this->requests; + } + +} diff --git a/src/hardpoint/ArcanistHardpointTask.php b/src/hardpoint/ArcanistHardpointTask.php new file mode 100644 --- /dev/null +++ b/src/hardpoint/ArcanistHardpointTask.php @@ -0,0 +1,246 @@ +request = $request; + return $this; + } + + public function getRequest() { + return $this->request; + } + + public function setQuery(ArcanistHardpointQuery $query) { + $this->query = $query; + return $this; + } + + public function getQuery() { + return $this->query; + } + + public function setObjects(array $objects) { + $this->objects = $objects; + return $this; + } + + public function getObjects() { + return $this->objects; + } + + public function isComplete() { + return $this->isComplete; + } + + public function updateTask() { + if ($this->isComplete()) { + return false; + } + + // If we're blocked by other requests, we have to wait for them to + // resolve. + if ($this->getBlockingRequests()) { + return false; + } + + // If we're blocked by futures, we have to wait for them to resolve. + if ($this->getBlockingFutures()) { + return false; + } + + $query = $this->getQuery(); + + // If we've previously produced a generator, iterate it. + + if ($this->generator) { + $generator = $this->generator; + + $has_send = false; + $send_value = null; + + // If our last iteration generated a single future and it was marked to + // be sent back to the generator, resolve the future (it should already + // be ready to resolve) and send the result. + + if ($this->sendFuture) { + $has_send = true; + $future = $this->sendFuture; + $this->sendFuture = null; + + $send_value = $future->resolve(); + } + + if ($has_send && !$this->hasRewound) { + throw new Exception( + pht( + 'Generator has never rewound, but has a value to send. This '. + 'is invalid.')); + } + + if (!$this->hasRewound) { + $this->hasRewound = true; + $generator->rewind(); + } else if ($has_send) { + $generator->send($send_value); + } else { + $generator->next(); + } + + if ($generator->valid()) { + $result = $generator->current(); + + if ($result instanceof Future) { + $result = new ArcanistHardpointFutureList($result); + } + + if ($result instanceof ArcanistHardpointFutureList) { + $futures = $result->getFutures(); + $is_send = $result->getSendResult(); + + $this->getRequest()->getEngine()->addFutures($futures); + + foreach ($futures as $future) { + $this->blockingFutures[] = $future; + } + + if ($is_send) { + if (count($futures) === 1) { + $this->sendFuture = head($futures); + } else { + throw new Exception( + pht( + 'Hardpoint future list is marked to send results to the '. + 'generator, but the list does not have exactly one future '. + '(it has %s).', + phutil_count($futures))); + } + } + + return true; + } + + $is_request = ($result instanceof ArcanistHardpointRequest); + $is_request_list = ($result instanceof ArcanistHardpointRequestList); + if ($is_request || $is_request_list) { + if ($is_request) { + $request_list = array($result); + } else { + $request_list = $result->getRequests(); + } + + // TODO: Make sure these requests have already been added to the + // engine. + + foreach ($request_list as $blocking_request) { + $this->blockingRequests[] = $blocking_request; + } + + return true; + } + + throw new Exception( + pht( + 'Hardpoint generator (for query "%s") yielded an unexpected '. + 'value. Generators may only yield "Future" or '. + '"ArcanistHardpointRequest" objects, got "%s".', + get_class($query), + phutil_describe_type($result))); + } + + $this->generator = null; + $result = $generator->getReturn(); + + $this->attachResult($result); + + return true; + } + + $objects = $this->getObjects(); + $hardpoint = $this->getRequest()->getHardpoint(); + + $result = $query->loadHardpoint($objects, $hardpoint); + if ($result instanceof Generator) { + $this->generator = $result; + $this->hasRewound = false; + + // If we produced a generator, we can attempt to iterate it immediately. + return $this->updateTask(); + } + + $this->attachResult($result); + + return true; + } + + public function getBlockingRequests() { + $blocking = array(); + + foreach ($this->blockingRequests as $key => $request) { + if (!$request->isComplete()) { + $blocking[$key] = $request; + } + } + + $this->blockingRequests = $blocking; + + return $blocking; + } + + public function getBlockingFutures() { + $blocking = array(); + + foreach ($this->blockingFutures as $key => $future) { + if (!$future->hasResult() && !$future->hasException()) { + $blocking[$key] = $future; + } + } + + $this->blockingFutures = $blocking; + + return $blocking; + } + + private function attachResult($result) { + $objects = $this->getObjects(); + $hardpoint = $this->getRequest()->getHardpoint(); + + $definition = $this->getRequest()->getHardpointDefinition(); + $is_vector = $definition->isVectorHardpoint(); + + foreach ($result as $object_key => $value) { + if (!isset($objects[$object_key])) { + throw new Exception( + pht( + 'Bad object key ("%s").', + $object_key)); + } + + $object = $objects[$object_key]; + + if ($is_vector) { + $object->mergeHardpoint($hardpoint, $value); + } else { + if (!$object->hasAttachedHardpoint($hardpoint)) { + $object->attachHardpoint($hardpoint, $value); + } + } + } + + $this->isComplete = true; + } + +} diff --git a/src/hardpoint/ArcanistScalarHardpoint.php b/src/hardpoint/ArcanistScalarHardpoint.php new file mode 100644 --- /dev/null +++ b/src/hardpoint/ArcanistScalarHardpoint.php @@ -0,0 +1,10 @@ +