@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Provide a more structured result log for Herald rules

Summary: Ref T13586. In the footsteps of D21563, make Herald rule results more formal and structured to support meaningful exception reporting.

Test Plan:
Ran various Herald rules and viewed transcripts, including rules with recursive dependencies and condition exceptions.

{F8447894}

Maniphest Tasks: T13586

Differential Revision: https://secure.phabricator.com/D21565

+508 -148
+4
src/__phutil_library_map__.php
··· 1621 1621 'HeraldRuleDisableTransaction' => 'applications/herald/xaction/HeraldRuleDisableTransaction.php', 1622 1622 'HeraldRuleEditTransaction' => 'applications/herald/xaction/HeraldRuleEditTransaction.php', 1623 1623 'HeraldRuleEditor' => 'applications/herald/editor/HeraldRuleEditor.php', 1624 + 'HeraldRuleEvaluationException' => 'applications/herald/engine/exception/HeraldRuleEvaluationException.php', 1624 1625 'HeraldRuleField' => 'applications/herald/field/rule/HeraldRuleField.php', 1625 1626 'HeraldRuleFieldGroup' => 'applications/herald/field/rule/HeraldRuleFieldGroup.php', 1626 1627 'HeraldRuleIndexEngineExtension' => 'applications/herald/engineextension/HeraldRuleIndexEngineExtension.php', ··· 1631 1632 'HeraldRulePHIDType' => 'applications/herald/phid/HeraldRulePHIDType.php', 1632 1633 'HeraldRuleQuery' => 'applications/herald/query/HeraldRuleQuery.php', 1633 1634 'HeraldRuleReplyHandler' => 'applications/herald/mail/HeraldRuleReplyHandler.php', 1635 + 'HeraldRuleResult' => 'applications/herald/storage/transcript/HeraldRuleResult.php', 1634 1636 'HeraldRuleSearchEngine' => 'applications/herald/query/HeraldRuleSearchEngine.php', 1635 1637 'HeraldRuleSerializer' => 'applications/herald/editor/HeraldRuleSerializer.php', 1636 1638 'HeraldRuleTestCase' => 'applications/herald/storage/__tests__/HeraldRuleTestCase.php', ··· 7861 7863 'HeraldRuleDisableTransaction' => 'HeraldRuleTransactionType', 7862 7864 'HeraldRuleEditTransaction' => 'HeraldRuleTransactionType', 7863 7865 'HeraldRuleEditor' => 'PhabricatorApplicationTransactionEditor', 7866 + 'HeraldRuleEvaluationException' => 'Exception', 7864 7867 'HeraldRuleField' => 'HeraldField', 7865 7868 'HeraldRuleFieldGroup' => 'HeraldFieldGroup', 7866 7869 'HeraldRuleIndexEngineExtension' => 'PhabricatorEdgeIndexEngineExtension', ··· 7871 7874 'HeraldRulePHIDType' => 'PhabricatorPHIDType', 7872 7875 'HeraldRuleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 7873 7876 'HeraldRuleReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 7877 + 'HeraldRuleResult' => 'HeraldTranscriptResult', 7874 7878 'HeraldRuleSearchEngine' => 'PhabricatorApplicationSearchEngine', 7875 7879 'HeraldRuleSerializer' => 'Phobject', 7876 7880 'HeraldRuleTestCase' => 'PhabricatorTestCase',
+25 -23
src/applications/herald/controller/HeraldTranscriptController.php
··· 224 224 } 225 225 226 226 private function buildActionTranscriptPanel(HeraldTranscript $xscript) { 227 + $viewer = $this->getViewer(); 227 228 $action_xscript = mgroup($xscript->getApplyTranscripts(), 'getRuleID'); 228 229 229 230 $adapter = $this->getAdapter(); ··· 253 254 ->setHeader($rule_xscript->getRuleName()) 254 255 ->setHref($rule_uri); 255 256 256 - if (!$rule_xscript->getResult()) { 257 + $rule_result = $rule_xscript->getRuleResult(); 258 + 259 + if (!$rule_result->getShouldApplyActions()) { 257 260 $rule_item->setDisabled(true); 258 261 } 259 262 ··· 275 278 $color = $result->getIconColor(); 276 279 $name = $result->getName(); 277 280 278 - $result_details = $result->newDetailsView(); 281 + $result_details = $result->newDetailsView($viewer); 279 282 if ($result_details !== null) { 280 283 $result_details = phutil_tag( 281 284 'div', ··· 301 304 $cond_list->addItem($cond_item); 302 305 } 303 306 304 - if ($rule_xscript->isForbidden()) { 305 - $last_icon = 'fa-ban'; 306 - $last_color = 'indigo'; 307 - $last_result = pht('Forbidden'); 308 - $last_note = pht('Object state prevented rule evaluation.'); 309 - } else if ($rule_xscript->getResult()) { 310 - $last_icon = 'fa-check-circle'; 311 - $last_color = 'green'; 312 - $last_result = pht('Passed'); 313 - $last_note = pht('Rule passed.'); 314 - } else { 315 - $last_icon = 'fa-times-circle'; 316 - $last_color = 'red'; 317 - $last_result = pht('Failed'); 318 - $last_note = pht('Rule failed.'); 307 + $rule_result = $rule_xscript->getRuleResult(); 308 + 309 + $last_icon = $rule_result->getIconIcon(); 310 + $last_color = $rule_result->getIconColor(); 311 + $last_result = $rule_result->getName(); 312 + $last_note = $rule_result->getDescription(); 313 + 314 + $last_details = $rule_result->newDetailsView($viewer); 315 + if ($last_details !== null) { 316 + $last_details = phutil_tag( 317 + 'div', 318 + array( 319 + 'class' => 'herald-condition-note', 320 + ), 321 + $last_details); 319 322 } 320 323 321 324 $cond_last = id(new PHUIStatusItemView()) 322 325 ->setIcon($last_icon, $last_color) 323 326 ->setTarget(phutil_tag('strong', array(), $last_result)) 324 - ->setNote($last_note); 327 + ->setNote(array($last_note, $last_details)); 325 328 $cond_list->addItem($cond_last); 326 329 327 330 $cond_box = id(new PHUIBoxView()) ··· 330 333 331 334 $rule_item->appendChild($cond_box); 332 335 333 - if (!$rule_xscript->getResult()) { 334 - // If the rule didn't pass, don't generate an action transcript since 335 - // actions didn't apply. 336 - continue; 337 - } 336 + // Not all rules will have any action transcripts, but we show them 337 + // in general because they may have relevant information even when 338 + // rules did not take actions. In particular, state-based actions may 339 + // forbid rules from matching. 338 340 339 341 $cond_box->addMargin(PHUI::MARGIN_MEDIUM_BOTTOM); 340 342
+175 -95
src/applications/herald/engine/HeraldEngine.php
··· 3 3 final class HeraldEngine extends Phobject { 4 4 5 5 protected $rules = array(); 6 - protected $results = array(); 7 - protected $stack = array(); 8 6 protected $activeRule; 9 7 protected $transcript; 10 8 ··· 19 17 20 18 private $profilerStack = array(); 21 19 private $profilerFrames = array(); 20 + 21 + private $ruleResults; 22 + private $ruleStack; 22 23 23 24 public function setDryRun($dry_run) { 24 25 $this->dryRun = $dry_run; ··· 54 55 return $engine->getTranscript(); 55 56 } 56 57 58 + /* -( Rule Stack )--------------------------------------------------------- */ 59 + 60 + private function resetRuleStack() { 61 + $this->ruleStack = array(); 62 + return $this; 63 + } 64 + 65 + private function hasRuleOnStack(HeraldRule $rule) { 66 + $phid = $rule->getPHID(); 67 + return isset($this->ruleStack[$phid]); 68 + } 69 + 70 + private function pushRuleStack(HeraldRule $rule) { 71 + $phid = $rule->getPHID(); 72 + $this->ruleStack[$phid] = $rule; 73 + return $this; 74 + } 75 + 76 + private function getRuleStack() { 77 + return array_values($this->ruleStack); 78 + } 79 + 80 + /* -( Rule Results )------------------------------------------------------- */ 81 + 82 + private function resetRuleResults() { 83 + $this->ruleResults = array(); 84 + return $this; 85 + } 86 + 87 + private function setRuleResult( 88 + HeraldRule $rule, 89 + HeraldRuleResult $result) { 90 + 91 + $phid = $rule->getPHID(); 92 + 93 + if ($this->hasRuleResult($rule)) { 94 + throw new Exception( 95 + pht( 96 + 'Herald rule "%s" already has an evaluation result.', 97 + $phid)); 98 + } 99 + 100 + $this->ruleResults[$phid] = $result; 101 + 102 + $this->newRuleTranscript($rule) 103 + ->setRuleResult($result); 104 + 105 + return $this; 106 + } 107 + 108 + private function hasRuleResult(HeraldRule $rule) { 109 + $phid = $rule->getPHID(); 110 + return isset($this->ruleResults[$phid]); 111 + } 112 + 113 + private function getRuleResult(HeraldRule $rule) { 114 + $phid = $rule->getPHID(); 115 + 116 + if (!$this->hasRuleResult($rule)) { 117 + throw new Exception( 118 + pht( 119 + 'Herald rule "%s" does not have an evaluation result.', 120 + $phid)); 121 + } 122 + 123 + return $this->ruleResults[$phid]; 124 + } 125 + 57 126 public function applyRules(array $rules, HeraldAdapter $object) { 58 127 assert_instances_of($rules, 'HeraldRule'); 59 128 $t_start = microtime(true); ··· 66 135 $this->transcript->setObjectPHID((string)$object->getPHID()); 67 136 $this->fieldCache = array(); 68 137 $this->fieldExceptions = array(); 69 - $this->results = array(); 70 138 $this->rules = $rules; 71 139 $this->object = $object; 72 140 141 + $this->resetRuleResults(); 142 + 73 143 $effects = array(); 74 144 foreach ($rules as $phid => $rule) { 75 - $this->stack = array(); 76 - 77 - $is_first_only = $rule->isRepeatFirst(); 145 + $this->resetRuleStack(); 78 146 79 147 $caught = null; 148 + $result = null; 80 149 try { 150 + $is_first_only = $rule->isRepeatFirst(); 151 + 81 152 if (!$this->getDryRun() && 82 153 $is_first_only && 83 154 $rule->getRuleApplied($object->getPHID())) { 155 + 84 156 // This is not a dry run, and this rule is only supposed to be 85 - // applied a single time, and it's already been applied... 157 + // applied a single time, and it has already been applied. 86 158 // That means automatic failure. 87 - $this->newRuleTranscript($rule) 88 - ->setResult(false) 89 - ->setReason( 90 - pht( 91 - 'This rule is only supposed to be repeated a single time, '. 92 - 'and it has already been applied.')); 93 159 94 - $rule_matches = false; 160 + $result_code = HeraldRuleResult::RESULT_ALREADY_APPLIED; 161 + $result = HeraldRuleResult::newFromResultCode($result_code); 162 + } else if ($this->isForbidden($rule, $object)) { 163 + $result_code = HeraldRuleResult::RESULT_OBJECT_STATE; 164 + $result = HeraldRuleResult::newFromResultCode($result_code); 95 165 } else { 96 - if ($this->isForbidden($rule, $object)) { 97 - $this->newRuleTranscript($rule) 98 - ->setResult(HeraldRuleTranscript::RESULT_FORBIDDEN) 99 - ->setReason( 100 - pht( 101 - 'Object state is not compatible with rule.')); 102 - 103 - $rule_matches = false; 104 - } else { 105 - $rule_matches = $this->doesRuleMatch($rule, $object); 106 - } 166 + $result = $this->getRuleMatchResult($rule, $object); 107 167 } 108 168 } catch (HeraldRecursiveConditionsException $ex) { 109 - $names = array(); 110 - foreach ($this->stack as $rule_phid => $ignored) { 111 - $names[] = '"'.$rules[$rule_phid]->getName().'"'; 169 + $cycle_phids = array(); 170 + 171 + $stack = $this->getRuleStack(); 172 + foreach ($stack as $stack_rule) { 173 + $cycle_phids[] = $stack_rule->getPHID(); 112 174 } 113 - $names = implode(', ', $names); 114 - foreach ($this->stack as $rule_phid => $ignored) { 115 - $this->newRuleTranscript($rules[$rule_phid]) 116 - ->setResult(false) 117 - ->setReason( 118 - pht( 119 - "Rules %s are recursively dependent upon one another! ". 120 - "Don't do this! You have formed an unresolvable cycle in the ". 121 - "dependency graph!", 122 - $names)); 175 + // Add the rule which actually cycled to the list to make the 176 + // result more clear when we show it to the user. 177 + $cycle_phids[] = $phid; 178 + 179 + foreach ($stack as $stack_rule) { 180 + if ($this->hasRuleResult($stack_rule)) { 181 + continue; 182 + } 183 + 184 + $result_code = HeraldRuleResult::RESULT_RECURSION; 185 + $result_data = array( 186 + 'cyclePHIDs' => $cycle_phids, 187 + ); 188 + 189 + $result = HeraldRuleResult::newFromResultCode($result_code) 190 + ->setResultData($result_data); 191 + $this->setRuleResult($stack_rule, $result); 123 192 } 124 - $rule_matches = false; 193 + 194 + $result = $this->getRuleResult($rule); 195 + } catch (HeraldRuleEvaluationException $ex) { 196 + // When we encounter an evaluation exception, the condition which 197 + // failed to evaluate is responsible for logging the details of the 198 + // error. 199 + 200 + $result_code = HeraldRuleResult::RESULT_EVALUATION_EXCEPTION; 201 + $result = HeraldRuleResult::newFromResultCode($result_code); 125 202 } catch (Exception $ex) { 126 203 $caught = $ex; 127 204 } catch (Throwable $ex) { ··· 129 206 } 130 207 131 208 if ($caught) { 132 - $this->newRuleTranscript($rules[$phid]) 133 - ->setResult(false) 134 - ->setReason( 135 - pht( 136 - 'Rule encountered an exception while evaluting.')); 137 - $rule_matches = false; 209 + // These exceptions are unexpected, and did not arise during rule 210 + // evaluation, so we're responsible for handling the details. 211 + 212 + $result_code = HeraldRuleResult::RESULT_EXCEPTION; 213 + 214 + $result_data = array( 215 + 'exception.class' => get_class($caught), 216 + 'exception.message' => $ex->getMessage(), 217 + ); 218 + 219 + $result = HeraldRuleResult::newFromResultCode($result_code) 220 + ->setResultData($result_data); 138 221 } 139 222 140 - $this->results[$phid] = $rule_matches; 223 + if (!$this->hasRuleResult($rule)) { 224 + $this->setRuleResult($rule, $result); 225 + } 226 + $result = $this->getRuleResult($rule); 141 227 142 - if ($rule_matches) { 228 + if ($result->getShouldApplyActions()) { 143 229 foreach ($this->getRuleEffects($rule, $object) as $effect) { 144 230 $effects[] = $effect; 145 231 } ··· 286 372 public function doesRuleMatch( 287 373 HeraldRule $rule, 288 374 HeraldAdapter $object) { 375 + $result = $this->getRuleMatchResult($rule, $object); 376 + return $result->getShouldApplyActions(); 377 + } 289 378 290 - $phid = $rule->getPHID(); 379 + private function getRuleMatchResult( 380 + HeraldRule $rule, 381 + HeraldAdapter $object) { 291 382 292 - if (isset($this->results[$phid])) { 383 + if ($this->hasRuleResult($rule)) { 293 384 // If we've already evaluated this rule because another rule depends 294 385 // on it, we don't need to reevaluate it. 295 - return $this->results[$phid]; 386 + return $this->getRuleResult($rule); 296 387 } 297 388 298 - if (isset($this->stack[$phid])) { 389 + if ($this->hasRuleOnStack($rule)) { 299 390 // We've recursed, fail all of the rules on the stack. This happens when 300 391 // there's a dependency cycle with "Rule conditions match for rule ..." 301 392 // conditions. 302 - foreach ($this->stack as $rule_phid => $ignored) { 303 - $this->results[$rule_phid] = false; 304 - } 305 393 throw new HeraldRecursiveConditionsException(); 306 394 } 307 - 308 - $this->stack[$phid] = true; 395 + $this->pushRuleStack($rule); 309 396 310 397 $all = $rule->getMustMatchAll(); 311 398 312 399 $conditions = $rule->getConditions(); 313 400 314 - $result = null; 401 + $result_code = null; 402 + $result_data = array(); 315 403 316 404 $local_version = id(new HeraldRule())->getConfigVersion(); 317 405 if ($rule->getConfigVersion() > $local_version) { 318 - $reason = pht( 319 - 'Rule could not be processed, it was created with a newer version '. 320 - 'of Herald.'); 321 - $result = false; 406 + $result_code = HeraldRuleResult::RESULT_VERSION; 322 407 } else if (!$conditions) { 323 - $reason = pht( 324 - 'Rule failed automatically because it has no conditions.'); 325 - $result = false; 408 + $result_code = HeraldRuleResult::RESULT_EMPTY; 326 409 } else if (!$rule->hasValidAuthor()) { 327 - $reason = pht( 328 - 'Rule failed automatically because its owner is invalid '. 329 - 'or disabled.'); 330 - $result = false; 410 + $result_code = HeraldRuleResult::RESULT_OWNER; 331 411 } else if (!$this->canAuthorViewObject($rule, $object)) { 332 - $reason = pht( 333 - 'Rule failed automatically because it is a personal rule and its '. 334 - 'owner can not see the object.'); 335 - $result = false; 412 + $result_code = HeraldRuleResult::RESULT_VIEW_POLICY; 336 413 } else if (!$this->canRuleApplyToObject($rule, $object)) { 337 - $reason = pht( 338 - 'Rule failed automatically because it is an object rule which is '. 339 - 'not relevant for this object.'); 340 - $result = false; 414 + $result_code = HeraldRuleResult::RESULT_OBJECT_RULE; 341 415 } else { 342 416 foreach ($conditions as $condition) { 343 417 $caught = null; ··· 347 421 $rule, 348 422 $condition, 349 423 $object); 424 + } catch (HeraldRuleEvaluationException $ex) { 425 + throw $ex; 426 + } catch (HeraldRecursiveConditionsException $ex) { 427 + throw $ex; 350 428 } catch (Exception $ex) { 351 429 $caught = $ex; 352 430 } catch (Throwable $ex) { ··· 354 432 } 355 433 356 434 if ($caught) { 357 - throw $ex; 435 + throw new HeraldRuleEvaluationException(); 358 436 } 359 437 360 438 if (!$all && $match) { 361 - $reason = pht('Any condition matched.'); 362 - $result = true; 439 + $result_code = HeraldRuleResult::RESULT_ANY_MATCHED; 363 440 break; 364 441 } 365 442 366 443 if ($all && !$match) { 367 - $reason = pht('Not all conditions matched.'); 368 - $result = false; 444 + $result_code = HeraldRuleResult::RESULT_ANY_FAILED; 369 445 break; 370 446 } 371 447 } 372 448 373 - if ($result === null) { 449 + if ($result_code === null) { 374 450 if ($all) { 375 - $reason = pht('All conditions matched.'); 376 - $result = true; 451 + $result_code = HeraldRuleResult::RESULT_ALL_MATCHED; 377 452 } else { 378 - $reason = pht('No conditions matched.'); 379 - $result = false; 453 + $result_code = HeraldRuleResult::RESULT_ALL_FAILED; 380 454 } 381 455 } 382 456 } 383 457 384 458 // If this rule matched, and is set to run "if it did not match the last 385 - // time", and we matched the last time, we're going to return a match in 386 - // the transcript but set a flag so we don't actually apply any effects. 459 + // time", and we matched the last time, we're going to return a special 460 + // result code which records a match but doesn't actually apply effects. 387 461 388 462 // We need the rule to match so that storage gets updated properly. If we 389 463 // just pretend the rule didn't match it won't cause any effects (which 390 464 // is correct), but it also won't set the "it matched" flag in storage, 391 465 // so the next run after this one would incorrectly trigger again. 392 466 467 + $result = HeraldRuleResult::newFromResultCode($result_code) 468 + ->setResultData($result_data); 469 + 470 + $should_apply = $result->getShouldApplyActions(); 471 + 393 472 $is_dry_run = $this->getDryRun(); 394 - if ($result && !$is_dry_run) { 473 + if ($should_apply && !$is_dry_run) { 395 474 $is_on_change = $rule->isRepeatOnChange(); 396 475 if ($is_on_change) { 397 476 $did_apply = $rule->getRuleApplied($object->getPHID()); 398 477 if ($did_apply) { 399 - $reason = pht( 400 - 'This rule matched, but did not take any actions because it '. 401 - 'is configured to act only if it did not match the last time.'); 478 + // Replace the result with our modified result. 479 + $result_code = HeraldRuleResult::RESULT_LAST_MATCHED; 480 + $result = HeraldRuleResult::newFromResultCode($result_code); 402 481 403 482 $this->skipEffects[$rule->getID()] = true; 404 483 } 405 484 } 406 485 } 407 486 408 - $this->newRuleTranscript($rule) 409 - ->setResult($result) 410 - ->setReason($reason); 487 + $this->setRuleResult($rule, $result); 411 488 412 489 return $result; 413 490 } ··· 439 516 } else { 440 517 $result_code = HeraldConditionResult::RESULT_FAILED; 441 518 } 519 + } catch (HeraldRecursiveConditionsException $ex) { 520 + $result_code = HeraldConditionResult::RESULT_RECURSION; 521 + $caught = $ex; 442 522 } catch (HeraldInvalidConditionException $ex) { 443 523 $result_code = HeraldConditionResult::RESULT_INVALID; 444 524 $caught = $ex;
+3
src/applications/herald/engine/exception/HeraldRuleEvaluationException.php
··· 1 + <?php 2 + 3 + final class HeraldRuleEvaluationException extends Exception {}
+8 -3
src/applications/herald/storage/transcript/HeraldConditionResult.php
··· 7 7 const RESULT_FAILED = 'failed'; 8 8 const RESULT_OBJECT_STATE = 'object-state'; 9 9 const RESULT_INVALID = 'invalid'; 10 + const RESULT_RECURSION = 'recursion'; 10 11 const RESULT_EXCEPTION = 'exception'; 11 12 const RESULT_UNKNOWN = 'unknown'; 12 13 ··· 22 23 return ($this->getSpecificationProperty('match') === true); 23 24 } 24 25 25 - public function newDetailsView() { 26 + public function newDetailsView(PhabricatorUser $viewer) { 26 27 switch ($this->getResultCode()) { 27 28 case self::RESULT_OBJECT_STATE: 28 29 $reason = $this->getDataProperty('reason'); ··· 54 55 phutil_tag('strong', array(), $error_class), 55 56 phutil_escape_html_newlines($error_message)); 56 57 break; 57 - $details = 'exception'; 58 - break; 59 58 default: 60 59 $details = null; 61 60 break; ··· 89 88 'icon' => 'fa-exclamation-triangle', 90 89 'color.icon' => 'yellow', 91 90 'name' => pht('Invalid'), 91 + ), 92 + self::RESULT_RECURSION => array( 93 + 'match' => null, 94 + 'icon' => 'fa-exclamation-triangle', 95 + 'color.icon' => 'red', 96 + 'name' => pht('Recursion'), 92 97 ), 93 98 self::RESULT_EXCEPTION => array( 94 99 'match' => null,
+238
src/applications/herald/storage/transcript/HeraldRuleResult.php
··· 1 + <?php 2 + 3 + final class HeraldRuleResult 4 + extends HeraldTranscriptResult { 5 + 6 + const RESULT_ANY_MATCHED = 'any-match'; 7 + const RESULT_ALL_MATCHED = 'all-match'; 8 + const RESULT_ANY_FAILED = 'any-failed'; 9 + const RESULT_ALL_FAILED = 'all-failed'; 10 + const RESULT_LAST_MATCHED = 'last-match'; 11 + const RESULT_VERSION = 'version'; 12 + const RESULT_EMPTY = 'empty'; 13 + const RESULT_OWNER = 'owner'; 14 + const RESULT_VIEW_POLICY = 'view-policy'; 15 + const RESULT_OBJECT_RULE = 'object-rule'; 16 + const RESULT_EXCEPTION = 'exception'; 17 + const RESULT_EVALUATION_EXCEPTION = 'evaluation-exception'; 18 + const RESULT_UNKNOWN = 'unknown'; 19 + const RESULT_ALREADY_APPLIED = 'already-applied'; 20 + const RESULT_OBJECT_STATE = 'object-state'; 21 + const RESULT_RECURSION = 'recursion'; 22 + 23 + public static function newFromResultCode($result_code) { 24 + return id(new self())->setResultCode($result_code); 25 + } 26 + 27 + public static function newFromResultMap(array $map) { 28 + return id(new self())->loadFromResultMap($map); 29 + } 30 + 31 + public function getShouldRecordMatch() { 32 + return ($this->getSpecificationProperty('match') === true); 33 + } 34 + 35 + public function getShouldApplyActions() { 36 + return ($this->getSpecificationProperty('apply') === true); 37 + } 38 + 39 + public function getDescription() { 40 + return $this->getSpecificationProperty('description'); 41 + } 42 + 43 + public function newDetailsView(PhabricatorUser $viewer) { 44 + switch ($this->getResultCode()) { 45 + case self::RESULT_EXCEPTION: 46 + $error_class = $this->getDataProperty('exception.class'); 47 + $error_message = $this->getDataProperty('exception.message'); 48 + 49 + if (!strlen($error_class)) { 50 + $error_class = pht('Unknown Error'); 51 + } 52 + 53 + if (!strlen($error_message)) { 54 + $error_message = pht( 55 + 'An unknown error occurred while evaluating this condition. No '. 56 + 'additional information is available.'); 57 + } 58 + 59 + $details = $this->newErrorView($error_class, $error_message); 60 + break; 61 + case self::RESULT_RECURSION: 62 + $rule_phids = $this->getDataProperty('cyclePHIDs', array()); 63 + $handles = $viewer->loadHandles($rule_phids); 64 + 65 + $links = array(); 66 + foreach ($rule_phids as $rule_phid) { 67 + $links[] = $handles[$rule_phid]->renderLink(); 68 + } 69 + 70 + $links = phutil_implode_html(' > ', $links); 71 + 72 + $details = array( 73 + pht('This rule has a dependency cycle and can not be evaluated:'), 74 + ' ', 75 + $links, 76 + ); 77 + break; 78 + default: 79 + $details = null; 80 + break; 81 + } 82 + 83 + return $details; 84 + } 85 + 86 + protected function newResultSpecificationMap() { 87 + return array( 88 + self::RESULT_ANY_MATCHED => array( 89 + 'match' => true, 90 + 'apply' => true, 91 + 'name' => pht('Matched'), 92 + 'description' => pht('Any condition matched.'), 93 + 'icon' => 'fa-check-circle', 94 + 'color.icon' => 'green', 95 + ), 96 + self::RESULT_ALL_MATCHED => array( 97 + 'match' => true, 98 + 'apply' => true, 99 + 'name' => pht('Matched'), 100 + 'description' => pht('All conditions matched.'), 101 + 'icon' => 'fa-check-circle', 102 + 'color.icon' => 'green', 103 + ), 104 + self::RESULT_ANY_FAILED => array( 105 + 'match' => false, 106 + 'apply' => false, 107 + 'name' => pht('Failed'), 108 + 'description' => pht('Not all conditions matched.'), 109 + 'icon' => 'fa-times-circle', 110 + 'color.icon' => 'red', 111 + ), 112 + self::RESULT_ALL_FAILED => array( 113 + 'match' => false, 114 + 'apply' => false, 115 + 'name' => pht('Failed'), 116 + 'description' => pht('No conditions matched.'), 117 + 'icon' => 'fa-times-circle', 118 + 'color.icon' => 'red', 119 + ), 120 + self::RESULT_LAST_MATCHED => array( 121 + 'match' => true, 122 + 'apply' => false, 123 + 'name' => pht('Failed'), 124 + 'description' => pht( 125 + 'This rule matched, but did not take any actions because it '. 126 + 'is configured to act only if it did not match the last time.'), 127 + 'icon' => 'fa-times-circle', 128 + 'color.icon' => 'red', 129 + ), 130 + self::RESULT_VERSION => array( 131 + 'match' => null, 132 + 'apply' => false, 133 + 'name' => pht('Version Issue'), 134 + 'description' => pht( 135 + 'Rule could not be processed because it was created with a newer '. 136 + 'version of Herald.'), 137 + 'icon' => 'fa-times-circle', 138 + 'color.icon' => 'red', 139 + ), 140 + self::RESULT_EMPTY => array( 141 + 'match' => null, 142 + 'apply' => false, 143 + 'name' => pht('Empty'), 144 + 'description' => pht( 145 + 'Rule failed automatically because it has no conditions.'), 146 + 'icon' => 'fa-times-circle', 147 + 'color.icon' => 'red', 148 + ), 149 + self::RESULT_OWNER => array( 150 + 'match' => null, 151 + 'apply' => false, 152 + 'name' => pht('Rule Owner'), 153 + 'description' => pht( 154 + 'Rule failed automatically because it is a personal rule and '. 155 + 'its owner is invalid or disabled.'), 156 + 'icon' => 'fa-times-circle', 157 + 'color.icon' => 'red', 158 + ), 159 + self::RESULT_VIEW_POLICY => array( 160 + 'match' => null, 161 + 'apply' => false, 162 + 'name' => pht('View Policy'), 163 + 'description' => pht( 164 + 'Rule failed automatically because it is a personal rule and '. 165 + 'its owner does not have permission to view the object.'), 166 + 'icon' => 'fa-times-circle', 167 + 'color.icon' => 'red', 168 + ), 169 + self::RESULT_OBJECT_RULE => array( 170 + 'match' => null, 171 + 'apply' => false, 172 + 'name' => pht('Object Rule'), 173 + 'description' => pht( 174 + 'Rule failed automatically because it is an object rule which is '. 175 + 'not relevant for this object.'), 176 + 'icon' => 'fa-times-circle', 177 + 'color.icon' => 'red', 178 + ), 179 + self::RESULT_EXCEPTION => array( 180 + 'match' => null, 181 + 'apply' => false, 182 + 'name' => pht('Exception'), 183 + 'description' => pht( 184 + 'Rule failed because an exception occurred.'), 185 + 'icon' => 'fa-times-circle', 186 + 'color.icon' => 'red', 187 + ), 188 + self::RESULT_EVALUATION_EXCEPTION => array( 189 + 'match' => null, 190 + 'apply' => false, 191 + 'name' => pht('Exception'), 192 + 'description' => pht( 193 + 'Rule failed because an exception occurred while evaluating it.'), 194 + 'icon' => 'fa-times-circle', 195 + 'color.icon' => 'red', 196 + ), 197 + self::RESULT_UNKNOWN => array( 198 + 'match' => null, 199 + 'apply' => false, 200 + 'name' => pht('Unknown'), 201 + 'description' => pht( 202 + 'Rule evaluation result is unknown.'), 203 + 'icon' => 'fa-times-circle', 204 + 'color.icon' => 'red', 205 + ), 206 + self::RESULT_ALREADY_APPLIED => array( 207 + 'match' => null, 208 + 'apply' => false, 209 + 'name' => pht('Already Applied'), 210 + 'description' => pht( 211 + 'This rule is only supposed to be repeated a single time, '. 212 + 'and it has already been applied.'), 213 + 'icon' => 'fa-times-circle', 214 + 'color.icon' => 'red', 215 + ), 216 + self::RESULT_OBJECT_STATE => array( 217 + 'match' => null, 218 + 'apply' => false, 219 + 'name' => pht('Forbidden'), 220 + 'description' => pht( 221 + 'Object state prevented rule evaluation.'), 222 + 'icon' => 'fa-ban', 223 + 'color.icon' => 'indigo', 224 + ), 225 + self::RESULT_RECURSION => array( 226 + 'match' => null, 227 + 'apply' => false, 228 + 'name' => pht('Recursion'), 229 + 'description' => pht( 230 + 'This rule has a recursive dependency on itself and can not '. 231 + 'be evaluated.'), 232 + 'icon' => 'fa-times-circle', 233 + 'color.icon' => 'red', 234 + ), 235 + ); 236 + } 237 + 238 + }
+44 -25
src/applications/herald/storage/transcript/HeraldRuleTranscript.php
··· 3 3 final class HeraldRuleTranscript extends Phobject { 4 4 5 5 protected $ruleID; 6 - protected $result; 7 - protected $reason; 8 - 6 + protected $ruleResultMap; 9 7 protected $ruleName; 10 8 protected $ruleOwner; 11 9 12 - const RESULT_FORBIDDEN = 'forbidden'; 10 + // See T13586. This no longer has readers, but was written by older versions 11 + // of Herald. It contained a human readable English-language description of 12 + // the outcome of rule evaluation and was superseded by "HeraldRuleResult". 13 + protected $reason; 13 14 14 - public function isForbidden() { 15 - return ($this->getResult() === self::RESULT_FORBIDDEN); 16 - } 17 - 18 - public function setResult($result) { 19 - $this->result = $result; 20 - return $this; 21 - } 22 - 23 - public function getResult() { 24 - return $this->result; 25 - } 26 - 27 - public function setReason($reason) { 28 - $this->reason = $reason; 29 - return $this; 30 - } 31 - 32 - public function getReason() { 33 - return $this->reason; 34 - } 15 + // See T13586. Older transcripts store a boolean "true", a boolean "false", 16 + // or the string "forbidden" here. 17 + protected $result; 35 18 36 19 public function setRuleID($rule_id) { 37 20 $this->ruleID = $rule_id; ··· 59 42 public function getRuleOwner() { 60 43 return $this->ruleOwner; 61 44 } 45 + 46 + public function setRuleResult(HeraldRuleResult $result) { 47 + $this->ruleResultMap = $result->newResultMap(); 48 + return $this; 49 + } 50 + 51 + public function getRuleResult() { 52 + $map = $this->ruleResultMap; 53 + 54 + if (is_array($map)) { 55 + $result = HeraldRuleResult::newFromResultMap($map); 56 + } else { 57 + $legacy_result = $this->result; 58 + 59 + $result_data = array(); 60 + 61 + if ($legacy_result === 'forbidden') { 62 + $result_code = HeraldRuleResult::RESULT_OBJECT_STATE; 63 + $result_data = array( 64 + 'reason' => $this->reason, 65 + ); 66 + } else if ($legacy_result === true) { 67 + $result_code = HeraldRuleResult::RESULT_ANY_MATCHED; 68 + } else if ($legacy_result === false) { 69 + $result_code = HeraldRuleResult::RESULT_ANY_FAILED; 70 + } else { 71 + $result_code = HeraldRuleResult::RESULT_UNKNOWN; 72 + } 73 + 74 + $result = HeraldRuleResult::newFromResultCode($result_code) 75 + ->setResultData($result_data); 76 + } 77 + 78 + return $result; 79 + } 80 + 62 81 }
+11 -2
src/applications/herald/storage/transcript/HeraldTranscriptResult.php
··· 47 47 return $this->getSpecificationProperty('name'); 48 48 } 49 49 50 - final protected function getDataProperty($key) { 50 + abstract public function newDetailsView(PhabricatorUser $viewer); 51 + 52 + final protected function getDataProperty($key, $default = null) { 51 53 $data = $this->getResultData(); 52 - return idx($data, $key); 54 + return idx($data, $key, $default); 53 55 } 54 56 55 57 final public function newResultMap() { ··· 78 80 } 79 81 80 82 abstract protected function newResultSpecificationMap(); 83 + 84 + final protected function newErrorView($error_class, $error_message) { 85 + return pht( 86 + '%s: %s', 87 + phutil_tag('strong', array(), $error_class), 88 + phutil_escape_html_newlines($error_message)); 89 + } 81 90 82 91 }