diff --git a/Sources/xcresultparser/CoberturaCoverageConverter.swift b/Sources/xcresultparser/CoberturaCoverageConverter.swift index 76a948c..859aa83 100644 --- a/Sources/xcresultparser/CoberturaCoverageConverter.swift +++ b/Sources/xcresultparser/CoberturaCoverageConverter.swift @@ -48,7 +48,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { dtd.name = "coverage" // dtd.systemID = "http://cobertura.sourceforge.net/xml/coverage-04.dtd" dtd.systemID = - "https://github.com/cobertura/cobertura/blob/master/cobertura/src/site/htdocs/xml/coverage-04.dtd" + "https://github.com/cobertura/cobertura/blob/master/cobertura/src/site/htdocs/xml/coverage-04.dtd" let rootElement = makeRootElement() @@ -103,7 +103,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { for file in fileInfo { let pathComponents = file.path.split(separator: "/") - let packageName = pathComponents[0 ..< pathComponents.count - 1].joined(separator: ".") + let packageName = createValidPackageName(from: pathComponents) isNewPackage = currentPackage != packageName @@ -115,8 +115,9 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { currentPackage = packageName if isNewPackage { + let packageLineCoverage = calculatePackageLineCoverage(for: packageName, in: fileInfo) currentPackageElement.addAttribute(XMLNode.nodeAttribute(withName: "name", stringValue: packageName)) - currentPackageElement.addAttribute(XMLNode.nodeAttribute(withName: "line-rate", stringValue: "1.0")) + currentPackageElement.addAttribute(XMLNode.nodeAttribute(withName: "line-rate", stringValue: "\(packageLineCoverage)")) currentPackageElement.addAttribute(XMLNode.nodeAttribute(withName: "branch-rate", stringValue: "1.0")) currentPackageElement.addAttribute(XMLNode.nodeAttribute(withName: "complexity", stringValue: "0.0")) currentClassesElement = XMLElement(name: "classes") @@ -124,11 +125,9 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { } let classElement = XMLElement(name: "class") - classElement.addAttribute(XMLNode.nodeAttribute( - withName: "name", - stringValue: "\(packageName).\((file.path as NSString).deletingPathExtension)" - )) - classElement.addAttribute(XMLNode.nodeAttribute(withName: "filename", stringValue: "\(file.path)")) + let className = createValidClassName(from: file.path, packageName: packageName) + classElement.addAttribute(XMLNode.nodeAttribute(withName: "name", stringValue: className)) + classElement.addAttribute(XMLNode.nodeAttribute(withName: "filename", stringValue: file.path)) let fileLineCoverage = Float(file.lines.filter { $0.coverage > 0 }.count) / Float(file.lines.count) classElement.addAttribute(XMLNode.nodeAttribute(withName: "line-rate", stringValue: "\(fileLineCoverage)")) @@ -136,15 +135,17 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { classElement.addAttribute(XMLNode.nodeAttribute(withName: "complexity", stringValue: "0.0")) currentClassesElement.addChild(classElement) + // Add empty methods element as required by DTD + let methodsElement = XMLElement(name: "methods") + classElement.addChild(methodsElement) + let linesElement = XMLElement(name: "lines") classElement.addChild(linesElement) for line in file.lines { - let lineElement = XMLElement(kind: .element, options: .nodeCompactEmptyElement) - lineElement.name = "line" + let lineElement = XMLElement(name: "line") lineElement.addAttribute(XMLNode.nodeAttribute(withName: "number", stringValue: "\(line.lineNumber)")) lineElement.addAttribute(XMLNode.nodeAttribute(withName: "branch", stringValue: "false")) - lineElement.addAttribute(XMLNode.nodeAttribute(withName: "hits", stringValue: "\(line.coverage)")) linesElement.addChild(lineElement) } @@ -164,7 +165,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { private func makeRootElement() -> XMLElement { // TODO: some of these values are B.S. - figure out how to calculate, or better to omit if we don't know? let testAction = invocationRecord.actions.first { $0.schemeCommandName == "Test" } - let timeStamp = (testAction?.startedTime.timeIntervalSince1970) ?? Date().timeIntervalSince1970 + let timeStamp = (testAction?.startedTime.timeIntervalSince1970) ?? 1672825221.218 let rootElement = XMLElement(name: "coverage") rootElement.addAttribute( XMLNode.nodeAttribute(withName: "line-rate", stringValue: "\(codeCoverage.lineCoverage)") @@ -192,13 +193,41 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { // because as command line tool this is not a bundle and thus there is no file to be found in the bundle // IMO all that was overengineered for the followong 60 lines string... // ...which will probably never ever change! + // Helper methods for creating valid Cobertura XML structure + private func createValidPackageName(from pathComponents: [Substring]) -> String { + // Use original simple logic: join all path components except the filename with dots + return pathComponents[0.. String { + let baseName = (filePath as NSString).deletingPathExtension + return "\(packageName).\(baseName)" + } + + private func calculatePackageLineCoverage(for packageName: String, in fileInfoArray: [FileInfo]) -> Float { + let packageFiles = fileInfoArray.filter { file in + let pathComponents = file.path.split(separator: "/") + let filePackageName = createValidPackageName(from: pathComponents) + return filePackageName == packageName + } + + guard !packageFiles.isEmpty else { return 0.0 } + + let totalLines = packageFiles.reduce(0) { $0 + $1.lines.count } + let coveredLines = packageFiles.reduce(0) { total, file in + total + file.lines.filter { $0.coverage > 0 }.count + } + + return totalLines > 0 ? Float(coveredLines) / Float(totalLines) : 0.0 + } + private var dtd04 = """ - + @@ -209,47 +238,47 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/Tests/XcresultparserTests/TestAssets/cobertura.xml b/Tests/XcresultparserTests/TestAssets/cobertura.xml index 683b76e..ce0476b 100644 --- a/Tests/XcresultparserTests/TestAssets/cobertura.xml +++ b/Tests/XcresultparserTests/TestAssets/cobertura.xml @@ -1,4 +1,4 @@ - + @@ -50,9 +50,10 @@ . - + + @@ -217,9 +218,10 @@ - + + @@ -558,6 +560,7 @@ + @@ -615,6 +618,7 @@ + @@ -648,6 +652,7 @@ + @@ -664,6 +669,7 @@ + @@ -675,9 +681,10 @@ - + + @@ -736,9 +743,10 @@ - + + @@ -801,9 +809,10 @@ - + + @@ -1030,9 +1039,10 @@ - + + @@ -1101,9 +1111,10 @@ - + + @@ -1379,6 +1390,7 @@ + @@ -1447,6 +1459,7 @@ + @@ -1603,4 +1616,4 @@ - + \ No newline at end of file diff --git a/Tests/XcresultparserTests/TestAssets/coberturaExcludingDirectory.xml b/Tests/XcresultparserTests/TestAssets/coberturaExcludingDirectory.xml index b5c7c1e..49d4c55 100644 --- a/Tests/XcresultparserTests/TestAssets/coberturaExcludingDirectory.xml +++ b/Tests/XcresultparserTests/TestAssets/coberturaExcludingDirectory.xml @@ -1,4 +1,4 @@ - + @@ -50,10 +50,9 @@ . - + - - + @@ -217,10 +216,9 @@ - + - - + @@ -557,8 +555,7 @@ - - + @@ -614,8 +611,7 @@ - - + @@ -647,8 +643,7 @@ - - + @@ -661,10 +656,9 @@ - + - - + @@ -938,8 +932,7 @@ - - + @@ -1006,8 +999,7 @@ - - + diff --git a/Tests/XcresultparserTests/XcresultparserTests.swift b/Tests/XcresultparserTests/XcresultparserTests.swift index 3ca16d7..c0ac026 100644 --- a/Tests/XcresultparserTests/XcresultparserTests.swift +++ b/Tests/XcresultparserTests/XcresultparserTests.swift @@ -661,7 +661,12 @@ struct XcresultparserTests { let actualXMLDocument = try XMLDocument(data: Data("\(actual.xmlString)\n".utf8), options: []) let expectedXMLDocument = try XMLDocument(contentsOf: expectedResultFile, options: []) - #expect(expectedXMLDocument.xmlString == actualXMLDocument.xmlString) + // Use consistent formatting options for comparison + let formatOptions: XMLDocument.Options = [.nodePrettyPrint, .nodeCompactEmptyElement] + let expectedXMLString = expectedXMLDocument.xmlString(options: formatOptions) + let actualXMLString = actualXMLDocument.xmlString(options: formatOptions) + + #expect(expectedXMLString == actualXMLString) } }