@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<?php
2
3final class HarbormasterBuildUnitMessage
4 extends HarbormasterDAO
5 implements PhabricatorPolicyInterface {
6
7 protected $buildTargetPHID;
8 protected $engine;
9 protected $namespace;
10 protected $name;
11 protected $nameIndex;
12 protected $result;
13 protected $duration;
14 protected $properties = array();
15
16 private $buildTarget = self::ATTACHABLE;
17
18 const FORMAT_TEXT = 'text';
19 const FORMAT_REMARKUP = 'remarkup';
20
21 public static function initializeNewUnitMessage(
22 HarbormasterBuildTarget $build_target) {
23 return id(new HarbormasterBuildUnitMessage())
24 ->setBuildTargetPHID($build_target->getPHID());
25 }
26
27 public static function getParameterSpec() {
28 return array(
29 'name' => array(
30 'type' => 'string',
31 'description' => pht(
32 'Short test name, like "ExampleTest".'),
33 ),
34 'result' => array(
35 'type' => 'string',
36 'description' => pht(
37 'Result of the test.'),
38 ),
39 'namespace' => array(
40 'type' => 'optional string',
41 'description' => pht(
42 'Optional namespace for this test. This is organizational and '.
43 'is often a class or module name, like "ExampleTestCase".'),
44 ),
45 'engine' => array(
46 'type' => 'optional string',
47 'description' => pht(
48 'Test engine running the test, like "JavascriptTestEngine". This '.
49 'primarily prevents collisions between tests with the same name '.
50 'in different test suites (for example, a Javascript test and a '.
51 'Python test).'),
52 ),
53 'duration' => array(
54 'type' => 'optional float|int',
55 'description' => pht(
56 'Runtime duration of the test, in seconds.'),
57 ),
58 'path' => array(
59 'type' => 'optional string',
60 'description' => pht(
61 'Path to the file where the test is declared, relative to the '.
62 'project root.'),
63 ),
64 'coverage' => array(
65 'type' => 'optional map<string, wild>',
66 'description' => pht(
67 'Coverage information for this test.'),
68 ),
69 'details' => array(
70 'type' => 'optional string',
71 'description' => pht(
72 'Additional human-readable information about the failure.'),
73 ),
74 'format' => array(
75 'type' => 'optional string',
76 'description' => pht(
77 'Format for the text provided in "details". Valid values are '.
78 '"text" (default) or "remarkup". This controls how test details '.
79 'are rendered when shown to users.'),
80 ),
81 );
82 }
83
84 public static function newFromDictionary(
85 HarbormasterBuildTarget $build_target,
86 array $dict) {
87
88 $obj = self::initializeNewUnitMessage($build_target);
89
90 $spec = self::getParameterSpec();
91 $spec = ipull($spec, 'type');
92
93 // We're just going to ignore extra keys for now, to make it easier to
94 // add stuff here later on.
95 $dict = array_select_keys($dict, array_keys($spec));
96 PhutilTypeSpec::checkMap($dict, $spec);
97
98 $obj->setEngine(idx($dict, 'engine', ''));
99 $obj->setNamespace(idx($dict, 'namespace', ''));
100 $obj->setName($dict['name']);
101 $obj->setResult($dict['result']);
102 $obj->setDuration((float)idx($dict, 'duration'));
103
104 $path = idx($dict, 'path');
105 if ($path !== null && strlen($path)) {
106 $obj->setProperty('path', $path);
107 }
108
109 $coverage = idx($dict, 'coverage');
110 if ($coverage) {
111 $obj->setProperty('coverage', $coverage);
112 }
113
114 $details = idx($dict, 'details');
115 if ($details) {
116 $obj->setProperty('details', $details);
117 }
118
119 $format = idx($dict, 'format');
120 if ($format) {
121 $obj->setProperty('format', $format);
122 }
123
124 return $obj;
125 }
126
127 protected function getConfiguration() {
128 return array(
129 self::CONFIG_SERIALIZATION => array(
130 'properties' => self::SERIALIZATION_JSON,
131 ),
132 self::CONFIG_COLUMN_SCHEMA => array(
133 'engine' => 'text255',
134 'namespace' => 'text255',
135 'name' => 'text255',
136 'nameIndex' => 'bytes12',
137 'result' => 'text32',
138 'duration' => 'double?',
139 ),
140 self::CONFIG_KEY_SCHEMA => array(
141 'key_target' => array(
142 'columns' => array('buildTargetPHID'),
143 ),
144 ),
145 ) + parent::getConfiguration();
146 }
147
148 public function attachBuildTarget(HarbormasterBuildTarget $build_target) {
149 $this->buildTarget = $build_target;
150 return $this;
151 }
152
153 public function getBuildTarget() {
154 return $this->assertAttached($this->buildTarget);
155 }
156
157 public function getProperty($key, $default = null) {
158 return idx($this->properties, $key, $default);
159 }
160
161 public function setProperty($key, $value) {
162 $this->properties[$key] = $value;
163 return $this;
164 }
165
166 public function getUnitMessageDetails() {
167 return $this->getProperty('details', '');
168 }
169
170 public function getUnitMessageDetailsFormat() {
171 return $this->getProperty('format', self::FORMAT_TEXT);
172 }
173
174 public function newUnitMessageDetailsView(
175 PhabricatorUser $viewer,
176 $summarize = false) {
177
178 $format = $this->getUnitMessageDetailsFormat();
179
180 $is_text = ($format !== self::FORMAT_REMARKUP);
181 $is_remarkup = ($format === self::FORMAT_REMARKUP);
182 $message = null;
183
184 $full_details = $this->getUnitMessageDetails();
185 $byte_length = strlen($full_details);
186
187 $text_limit = 1024 * 2;
188 $remarkup_limit = 1024 * 8;
189
190 if (!$byte_length) {
191 if ($summarize) {
192 return null;
193 }
194 $message = phutil_tag('em', array(), pht('No details provided.'));
195 } else if ($summarize) {
196 if ($is_text) {
197 $details = id(new PhutilUTF8StringTruncator())
198 ->setMaximumBytes($text_limit)
199 ->truncateString($full_details);
200 $details = phutil_split_lines($details);
201
202 $limit = 3;
203 if (count($details) > $limit) {
204 $details = array_slice($details, 0, $limit);
205 }
206
207 $details = implode('', $details);
208 } else {
209 if ($byte_length > $remarkup_limit) {
210 $uri = $this->getURI();
211
212 if ($uri) {
213 $link = phutil_tag(
214 'a',
215 array(
216 'href' => $uri,
217 'target' => '_blank',
218 ),
219 pht('View Details'));
220 } else {
221 $link = null;
222 }
223
224 $message = array();
225 $message[] = phutil_tag(
226 'em',
227 array(),
228 pht('This test has too much data to display inline.'));
229 if ($link) {
230 $message[] = $link;
231 }
232
233 $message = phutil_implode_html(" \xC2\xB7 ", $message);
234 } else {
235 $details = $full_details;
236 }
237 }
238 } else {
239 $details = $full_details;
240 }
241
242 require_celerity_resource('harbormaster-css');
243
244 $classes = array();
245 $classes[] = 'harbormaster-unit-details';
246
247 if ($message !== null) {
248 // If we have a message, show that instead of rendering any test details.
249 $details = $message;
250 } else if ($is_remarkup) {
251 $details = new PHUIRemarkupView($viewer, $details);
252 } else {
253 $classes[] = 'harbormaster-unit-details-text';
254 $classes[] = 'PhabricatorMonospaced';
255 }
256
257 $warning = null;
258 if (!$summarize) {
259 $warnings = array();
260
261 if ($is_remarkup && ($byte_length > $remarkup_limit)) {
262 $warnings[] = pht(
263 'This test result has %s bytes of Remarkup test details. Remarkup '.
264 'blocks longer than %s bytes are not rendered inline when showing '.
265 'test summaries.',
266 new PhutilNumber($byte_length),
267 new PhutilNumber($remarkup_limit));
268 }
269
270 if ($warnings) {
271 $warning = id(new PHUIInfoView())
272 ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
273 ->setErrors($warnings);
274 }
275 }
276
277 $content = phutil_tag(
278 'div',
279 array(
280 'class' => implode(' ', $classes),
281 ),
282 $details);
283
284 return array(
285 $warning,
286 $content,
287 );
288 }
289
290 public function getUnitMessageDisplayName() {
291 $name = $this->getName();
292
293 $namespace = $this->getNamespace();
294 if (strlen($namespace)) {
295 $name = $namespace.'::'.$name;
296 }
297
298 $engine = $this->getEngine();
299 if (strlen($engine)) {
300 $name = $engine.' > '.$name;
301 }
302
303 if (!strlen($name)) {
304 return pht('Nameless Test (%d)', $this->getID());
305 }
306
307 return $name;
308 }
309
310 public function getSortKey() {
311 $status = $this->getResult();
312 $sort = HarbormasterUnitStatus::getUnitStatusSort($status);
313
314 $parts = array(
315 $sort,
316 $this->getEngine(),
317 $this->getNamespace(),
318 $this->getName(),
319 $this->getID(),
320 );
321
322 return implode("\0", $parts);
323 }
324
325 public function getURI() {
326 $id = $this->getID();
327
328 if (!$id) {
329 return null;
330 }
331
332 return urisprintf(
333 '/harbormaster/unit/view/%d/',
334 $id);
335 }
336
337 public function save() {
338 if ($this->nameIndex === null) {
339 $this->nameIndex = HarbormasterString::newIndex($this->getName());
340 }
341
342 // See T13088. While we're letting installs do online migrations to avoid
343 // downtime, don't populate the "name" column for new writes. New writes
344 // use the "HarbormasterString" table instead.
345 $old_name = $this->name;
346 $this->name = '';
347
348 $caught = null;
349 try {
350 $result = parent::save();
351 } catch (Exception $ex) {
352 $caught = $ex;
353 }
354
355 $this->name = $old_name;
356
357 if ($caught) {
358 throw $caught;
359 }
360
361 return $result;
362 }
363
364
365/* -( PhabricatorPolicyInterface )----------------------------------------- */
366
367
368 public function getCapabilities() {
369 return array(
370 PhabricatorPolicyCapability::CAN_VIEW,
371 );
372 }
373
374 public function getPolicy($capability) {
375 switch ($capability) {
376 case PhabricatorPolicyCapability::CAN_VIEW:
377 return PhabricatorPolicies::getMostOpenPolicy();
378 }
379 }
380
381 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
382 return false;
383 }
384
385}