JunitParserService   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 246
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 125
dl 0
loc 246
rs 9.76
c 0
b 0
f 0
wmc 33

5 Methods

Rating   Name   Duplication   Size   Complexity  
A validate() 0 33 4
B formatErrorFailSkipMessage() 0 29 9
A parse() 0 17 4
B parseTest() 0 62 9
B parseSuiteTests() 0 50 7
1
<?php
2
3
/**
4
 * Copyright (c) 2017 Francois-Xavier Soubirou.
5
 *
6
 * This file is part of ci-report.
7
 *
8
 * ci-report is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * ci-report is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License
19
 * along with ci-report. If not, see <http://www.gnu.org/licenses/>.
20
 */
21
declare(strict_types=1);
22
23
namespace App\Service;
24
25
use App\DTO\SuiteDTO;
26
use App\DTO\TestDTO;
27
use App\Entity\Suite;
28
use App\Entity\Test;
29
use App\Util\SuiteTests;
30
use DateTime;
31
use DOMDocument;
32
use ReflectionClass;
33
use SimpleXMLElement;
34
35
/**
36
 * Junit Parser service class.
37
 *
38
 * @category  ci-report app
39
 *
40
 * @author    Francois-Xavier Soubirou <[email protected]>
41
 * @copyright 2017 Francois-Xavier Soubirou
42
 * @license   http://www.gnu.org/licenses/   GPLv3
43
 *
44
 * @see      https://www.ci-report.io
45
 */
46
class JunitParserService
47
{
48
    /**
49
     * @var string
50
     */
51
    protected $schemaRelativePath = '/../../junit/xsd/junit-10.xsd';
52
53
    /**
54
     * @var string
55
     */
56
    const FAILURE_MSG_SEPARATOR = PHP_EOL.' '.PHP_EOL;
57
58
    /**
59
     * Validate the XML document.
60
     *
61
     * @param DOMDocument $domDoc XML document to validate
62
     *
63
     * @return array
64
     */
65
    public function validate(DOMDocument $domDoc): array
66
    {
67
        libxml_use_internal_errors(true);
68
        libxml_clear_errors();
69
70
        $reflClass = new ReflectionClass(get_class($this));
71
        $schemaAbsolutePath = dirname($reflClass->getFileName()).$this->schemaRelativePath;
72
        $domDoc->schemaValidate($schemaAbsolutePath);
73
        $errors = array();
74
75
        foreach (libxml_get_errors() as $libxmlError) {
76
            switch ($libxmlError->level) {
77
                case LIBXML_ERR_ERROR:
78
                    $level = 'Error';
79
                    break;
80
                case LIBXML_ERR_FATAL:
81
                    $level = 'Fatal error';
82
                    break;
83
                default:
84
                    $level = 'Warning';
85
                    break;
86
            }
87
            $error = array(
88
                'level' => $level,
89
                'line' => $libxmlError->line,
90
                'message' => $libxmlError->message,
91
            );
92
            $errors[] = $error;
93
        }
94
95
        libxml_clear_errors();
96
97
        return $errors;
98
    }
99
100
    /**
101
     * Parse the XML string.
102
     *
103
     * @param DOMDocument $domDoc XML document to parse
104
     *
105
     * @return array array(suiteTests, ...)
106
     */
107
    public function parse(DOMDocument $domDoc): array
108
    {
109
        $suitesArray = array();
110
111
        $xml = simplexml_import_dom($domDoc);
112
113
        if ('testsuites' === $xml->getName()) {
114
            foreach ($xml->testsuite as $xmlTestsuite) {
115
                $suiteTests = $this->parseSuiteTests($xmlTestsuite);
116
                $suitesArray[] = $suiteTests;
117
            }
118
        } elseif ('testsuite' === $xml->getName()) {
119
            $suiteTests = $this->parseSuiteTests($xml);
120
            $suitesArray[] = $suiteTests;
121
        }
122
123
        return $suitesArray;
124
    }
125
126
    /**
127
     * Parse the XML element.
128
     *
129
     * @param SimpleXMLElement $xmlTestsuite Element to parse
130
     *
131
     * @return suiteTests
132
     */
133
    private function parseSuiteTests(SimpleXMLElement $xmlTestsuite): suiteTests
134
    {
135
        $suite = new SuiteDTO();
136
        $suiteTests = new SuiteTests($suite);
137
138
        // Name is required
139
        $name = (string) $xmlTestsuite['name'];
140
        if (0 === strlen($name)) {
141
            $suite->setName(Suite::DEFAULT_NAME);
142
        } else {
143
            $suite->setName($name);
144
        }
145
146
        // If timestamp not set, initialize with now value
147
        if (isset($xmlTestsuite['timestamp'])) {
148
            $datetime = new DateTime((string) $xmlTestsuite['timestamp']);
149
        } else {
150
            $datetime = new DateTime();
151
        }
152
        $suite->setDatetime($datetime);
153
154
        $totalTestDuration = 0;
155
        $testsCount = 0;
156
157
        foreach ($xmlTestsuite->testcase as $xmlTestcase) {
158
            $test = $this->parseTest($xmlTestcase);
159
160
            $totalTestDuration += $test->getDuration();
161
            ++$testsCount;
162
163
            $suiteTests->addTest($test);
164
        }
165
        // If time not set, set it to sum of tests duration
166
        if (isset($xmlTestsuite['time'])) {
167
            $suite->setDuration((float) $xmlTestsuite['time']);
168
        } else {
169
            $suite->setDuration((float) $totalTestDuration);
170
        }
171
        // If disabled not set, compare tests attribute value and total of tests
172
        if (isset($xmlTestsuite['disabled'])) {
173
            $suite->setDisabled((int) $xmlTestsuite['disabled']);
174
        } else {
175
            // tests attribute is required
176
            $deltaTests = ((int) $xmlTestsuite['tests']) - $testsCount;
177
            if ($deltaTests > 0) {
178
                $suite->setDisabled($deltaTests);
179
            }
180
        }
181
182
        return $suiteTests;
183
    }
184
185
    /**
186
     * Parse the XML element.
187
     *
188
     * @param SimpleXMLElement $xmlTestcase Element to parse
189
     *
190
     * @return testDTO
191
     */
192
    private function parseTest(SimpleXMLElement $xmlTestcase): testDTO
193
    {
194
        $test = new TestDTO();
195
196
        // Name is required
197
        $name = (string) $xmlTestcase['name'];
198
        if (0 === strlen($name)) {
199
            $test->setName(Test::DEFAULT_NAME);
200
        } else {
201
            $test->setName($name);
202
        }
203
204
        // Classname is required.
205
        // set package.class, if no dot use default package
206
        $fullClassname = (string) $xmlTestcase['classname'];
207
        if (0 === strlen($fullClassname)) {
208
            $test->setFullclassname(Test::DEFAULT_CLASSNAME);
209
        } else {
210
            $test->setFullclassname($fullClassname);
211
        }
212
213
        // If time not set, initialize at 0
214
        if (isset($xmlTestcase['time'])) {
215
            $test->setDuration((float) $xmlTestcase['time']);
216
        } else {
217
            $test->setDuration(0);
218
        }
219
220
        // If system-out set
221
        if (isset($xmlTestcase->{'system-out'})) {
222
            $test->setSystemout((string) $xmlTestcase->{'system-out'});
223
        }
224
225
        // If system-err set
226
        if (isset($xmlTestcase->{'system-err'})) {
227
            $test->setSystemerr((string) $xmlTestcase->{'system-err'});
228
        }
229
230
        // If error
231
        if (isset($xmlTestcase->error)) {
232
            $test->setStatus(Test::ERRORED);
233
            $message = $this->formatErrorFailSkipMessage(
234
                $xmlTestcase->error
235
            );
236
            $test->setFailuremsg($message);
237
        } elseif (isset($xmlTestcase->failure)) {
238
            $test->setStatus(Test::FAILED);
239
            $message = $this->formatErrorFailSkipMessage(
240
                $xmlTestcase->failure
241
            );
242
            $test->setFailuremsg($message);
243
        } elseif (isset($xmlTestcase->skipped)) {
244
            $test->setStatus(Test::SKIPPED);
245
            $message = $this->formatErrorFailSkipMessage(
246
                $xmlTestcase->skipped
247
            );
248
            $test->setFailuremsg($message);
249
        } else {
250
            $test->setStatus(Test::PASSED);
251
        }
252
253
        return $test;
254
    }
255
256
    /**
257
     * Parse the XML element.
258
     *
259
     * @param SimpleXMLElement $elt Element to parse
260
     *
261
     * @return string
262
     */
263
    private function formatErrorFailSkipMessage(SimpleXMLElement $elt): string
264
    {
265
        if (isset($elt['type'])) {
266
            $type = 'Type: '.(string) $elt['type'];
267
        } else {
268
            $type = '';
269
        }
270
        if (isset($elt['message'])) {
271
            $message = 'Message: '.(string) $elt['message'];
272
        } else {
273
            $message = '';
274
        }
275
        if (isset($elt) && (strlen((string) $elt) > 0)) {
276
            $value = 'Details: '.(string) $elt;
277
        } else {
278
            $value = '';
279
        }
280
        if ((strlen($type) > 0) && (strlen($message) > 0)) {
281
            $fullmessage = $type.self::FAILURE_MSG_SEPARATOR.$message;
282
        } else {
283
            $fullmessage = $type.$message;
284
        }
285
        if ((strlen($fullmessage) > 0) && (strlen($value) > 0)) {
286
            $fullmessage = $fullmessage.self::FAILURE_MSG_SEPARATOR.$value;
287
        } else {
288
            $fullmessage = $fullmessage.$value;
289
        }
290
291
        return $fullmessage;
292
    }
293
}
294