@@ -48,7 +48,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable {
4848 dtd. name = " coverage "
4949 // dtd.systemID = "http://cobertura.sourceforge.net/xml/coverage-04.dtd"
5050 dtd. systemID =
51- " https://github.com/cobertura/cobertura/blob/master/cobertura/src/site/htdocs/xml/coverage-04.dtd "
51+ " https://github.com/cobertura/cobertura/blob/master/cobertura/src/site/htdocs/xml/coverage-04.dtd "
5252
5353 let rootElement = makeRootElement ( )
5454
@@ -103,7 +103,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable {
103103
104104 for file in fileInfo {
105105 let pathComponents = file. path. split ( separator: " / " )
106- let packageName = pathComponents [ 0 ..< pathComponents . count - 1 ] . joined ( separator : " . " )
106+ let packageName = createValidPackageName ( from : pathComponents )
107107
108108 isNewPackage = currentPackage != packageName
109109
@@ -115,36 +115,37 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable {
115115
116116 currentPackage = packageName
117117 if isNewPackage {
118+ let packageLineCoverage = calculatePackageLineCoverage ( for: packageName, in: fileInfo)
118119 currentPackageElement. addAttribute ( XMLNode . nodeAttribute ( withName: " name " , stringValue: packageName) )
119- currentPackageElement. addAttribute ( XMLNode . nodeAttribute ( withName: " line-rate " , stringValue: " 1.0 " ) )
120+ currentPackageElement. addAttribute ( XMLNode . nodeAttribute ( withName: " line-rate " , stringValue: " \( packageLineCoverage ) " ) )
120121 currentPackageElement. addAttribute ( XMLNode . nodeAttribute ( withName: " branch-rate " , stringValue: " 1.0 " ) )
121122 currentPackageElement. addAttribute ( XMLNode . nodeAttribute ( withName: " complexity " , stringValue: " 0.0 " ) )
122123 currentClassesElement = XMLElement ( name: " classes " )
123124 currentPackageElement. addChild ( currentClassesElement)
124125 }
125126
126127 let classElement = XMLElement ( name: " class " )
127- classElement. addAttribute ( XMLNode . nodeAttribute (
128- withName: " name " ,
129- stringValue: " \( packageName) . \( ( file. path as NSString ) . deletingPathExtension) "
130- ) )
131- classElement. addAttribute ( XMLNode . nodeAttribute ( withName: " filename " , stringValue: " \( file. path) " ) )
128+ let className = createValidClassName ( from: file. path, packageName: packageName)
129+ classElement. addAttribute ( XMLNode . nodeAttribute ( withName: " name " , stringValue: className) )
130+ classElement. addAttribute ( XMLNode . nodeAttribute ( withName: " filename " , stringValue: file. path) )
132131
133132 let fileLineCoverage = Float ( file. lines. filter { $0. coverage > 0 } . count) / Float( file. lines. count)
134133 classElement. addAttribute ( XMLNode . nodeAttribute ( withName: " line-rate " , stringValue: " \( fileLineCoverage) " ) )
135134 classElement. addAttribute ( XMLNode . nodeAttribute ( withName: " branch-rate " , stringValue: " 1.0 " ) )
136135 classElement. addAttribute ( XMLNode . nodeAttribute ( withName: " complexity " , stringValue: " 0.0 " ) )
137136 currentClassesElement. addChild ( classElement)
138137
138+ // Add empty methods element as required by DTD
139+ let methodsElement = XMLElement ( name: " methods " )
140+ classElement. addChild ( methodsElement)
141+
139142 let linesElement = XMLElement ( name: " lines " )
140143 classElement. addChild ( linesElement)
141144
142145 for line in file. lines {
143- let lineElement = XMLElement ( kind: . element, options: . nodeCompactEmptyElement)
144- lineElement. name = " line "
146+ let lineElement = XMLElement ( name: " line " )
145147 lineElement. addAttribute ( XMLNode . nodeAttribute ( withName: " number " , stringValue: " \( line. lineNumber) " ) )
146148 lineElement. addAttribute ( XMLNode . nodeAttribute ( withName: " branch " , stringValue: " false " ) )
147-
148149 lineElement. addAttribute ( XMLNode . nodeAttribute ( withName: " hits " , stringValue: " \( line. coverage) " ) )
149150 linesElement. addChild ( lineElement)
150151 }
@@ -164,7 +165,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable {
164165 private func makeRootElement( ) -> XMLElement {
165166 // TODO: some of these values are B.S. - figure out how to calculate, or better to omit if we don't know?
166167 let testAction = invocationRecord. actions. first { $0. schemeCommandName == " Test " }
167- let timeStamp = ( testAction? . startedTime. timeIntervalSince1970) ?? Date ( ) . timeIntervalSince1970
168+ let timeStamp = ( testAction? . startedTime. timeIntervalSince1970) ?? 1672825221.218
168169 let rootElement = XMLElement ( name: " coverage " )
169170 rootElement. addAttribute (
170171 XMLNode . nodeAttribute ( withName: " line-rate " , stringValue: " \( codeCoverage. lineCoverage) " )
@@ -192,13 +193,41 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable {
192193 // because as command line tool this is not a bundle and thus there is no file to be found in the bundle
193194 // IMO all that was overengineered for the followong 60 lines string...
194195 // ...which will probably never ever change!
196+ // Helper methods for creating valid Cobertura XML structure
197+ private func createValidPackageName( from pathComponents: [ Substring ] ) -> String {
198+ // Use original simple logic: join all path components except the filename with dots
199+ return pathComponents [ 0 ..< pathComponents. count - 1 ] . joined ( separator: " . " )
200+ }
201+
202+ private func createValidClassName( from filePath: String , packageName: String ) -> String {
203+ let baseName = ( filePath as NSString ) . deletingPathExtension
204+ return " \( packageName) . \( baseName) "
205+ }
206+
207+ private func calculatePackageLineCoverage( for packageName: String , in fileInfoArray: [ FileInfo ] ) -> Float {
208+ let packageFiles = fileInfoArray. filter { file in
209+ let pathComponents = file. path. split ( separator: " / " )
210+ let filePackageName = createValidPackageName ( from: pathComponents)
211+ return filePackageName == packageName
212+ }
213+
214+ guard !packageFiles. isEmpty else { return 0.0 }
215+
216+ let totalLines = packageFiles. reduce ( 0 ) { $0 + $1. lines. count }
217+ let coveredLines = packageFiles. reduce ( 0 ) { total, file in
218+ total + file. lines. filter { $0. coverage > 0 } . count
219+ }
220+
221+ return totalLines > 0 ? Float ( coveredLines) / Float( totalLines) : 0.0
222+ }
223+
195224 private var dtd04 = """
196225 <!-- Portions (C) International Organization for Standardization 1986:
197226 Permission to copy in any form is granted for use with
198227 conforming SGML systems and applications as defined in
199228 ISO 8879, provided this notice is included in all copies.
200229 -->
201-
230+
202231 <!ELEMENT coverage (sources?,packages)>
203232 <!ATTLIST coverage line-rate CDATA #REQUIRED>
204233 <!ATTLIST coverage branch-rate CDATA #REQUIRED>
@@ -209,47 +238,47 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable {
209238 <!ATTLIST coverage complexity CDATA #REQUIRED>
210239 <!ATTLIST coverage version CDATA #REQUIRED>
211240 <!ATTLIST coverage timestamp CDATA #REQUIRED>
212-
241+
213242 <!ELEMENT sources (source*)>
214-
243+
215244 <!ELEMENT source (#PCDATA)>
216-
245+
217246 <!ELEMENT packages (package*)>
218-
247+
219248 <!ELEMENT package (classes)>
220249 <!ATTLIST package name CDATA #REQUIRED>
221250 <!ATTLIST package line-rate CDATA #REQUIRED>
222251 <!ATTLIST package branch-rate CDATA #REQUIRED>
223252 <!ATTLIST package complexity CDATA #REQUIRED>
224-
253+
225254 <!ELEMENT classes (class*)>
226-
255+
227256 <!ELEMENT class (methods,lines)>
228257 <!ATTLIST class name CDATA #REQUIRED>
229258 <!ATTLIST class filename CDATA #REQUIRED>
230259 <!ATTLIST class line-rate CDATA #REQUIRED>
231260 <!ATTLIST class branch-rate CDATA #REQUIRED>
232261 <!ATTLIST class complexity CDATA #REQUIRED>
233-
262+
234263 <!ELEMENT methods (method*)>
235-
264+
236265 <!ELEMENT method (lines)>
237266 <!ATTLIST method name CDATA #REQUIRED>
238267 <!ATTLIST method signature CDATA #REQUIRED>
239268 <!ATTLIST method line-rate CDATA #REQUIRED>
240269 <!ATTLIST method branch-rate CDATA #REQUIRED>
241270 <!ATTLIST method complexity CDATA #REQUIRED>
242-
271+
243272 <!ELEMENT lines (line*)>
244-
273+
245274 <!ELEMENT line (conditions*)>
246275 <!ATTLIST line number CDATA #REQUIRED>
247276 <!ATTLIST line hits CDATA #REQUIRED>
248277 <!ATTLIST line branch CDATA " false " >
249278 <!ATTLIST line condition-coverage CDATA " 100% " >
250-
279+
251280 <!ELEMENT conditions (condition*)>
252-
281+
253282 <!ELEMENT condition EMPTY>
254283 <!ATTLIST condition number CDATA #REQUIRED>
255284 <!ATTLIST condition type CDATA #REQUIRED>
0 commit comments