public static CoverturaReport ReadReport(AbsolutePath path)
        {
            var doc    = XDocument.Load(path);
            var report = new CoverturaReport();

            // get coverage element
            var coverage = doc.Descendants("coverage").First();

            report.LineRate        = decimal.Parse(coverage.Attribute("line-rate").Value);
            report.BranchRate      = decimal.Parse(coverage.Attribute("branch-rate").Value);
            report.LinesCovered    = int.Parse(coverage.Attribute("lines-covered").Value);
            report.LinesValid      = int.Parse(coverage.Attribute("lines-valid").Value);
            report.BranchesCovered = int.Parse(coverage.Attribute("branches-covered").Value);
            report.BranchesValid   = int.Parse(coverage.Attribute("branches-valid").Value);
            report.Complexity      = int.Parse(coverage.Attribute("complexity").Value);

            report.Packages = coverage
                              .Descendants("packages")
                              .SelectMany(x => x.Descendants("package"))
                              .Select(packageEle => new CoverturaReport.Package
            {
                Name       = packageEle.Attribute("name").Value,
                LineRate   = decimal.Parse(packageEle.Attribute("line-rate").Value),
                BranchRate = decimal.Parse(packageEle.Attribute("branch-rate").Value),
                Complexity = int.Parse(packageEle.Attribute("complexity").Value),
                Classes    = packageEle
                             .Descendants("classes")
                             .SelectMany(x => x.Descendants("class"))
                             .Select(classEle =>
                {
                    var lineHits = classEle.Descendants("lines")
                                   .First()
                                   .Descendants("line")
                                   .Select(x => int.Parse(x.Attribute("hits").Value) > 0)
                                   .ToList();

                    return(new CoverturaReport.ClassDetails()
                    {
                        Name = classEle.Attribute("name").Value,
                        Filename = classEle.Attribute("filename").Value,
                        LineRate = decimal.Parse(classEle.Attribute("line-rate").Value),
                        BranchRate = decimal.Parse(classEle.Attribute("branch-rate").Value),
                        Complexity = int.Parse(classEle.Attribute("complexity").Value),
                        LinesCovered = lineHits.Count,
                        LinesValid = lineHits.Count(x => x),
                    });
                })
                             .ToDictionary(x => $"{x.Filename}_{x.Name}", x => x),
            })
                              .ToDictionary(x => x.Name, x => x);

            return(report);
        }
        public static CoverturaReportComparison Compare(CoverturaReport oldReport, CoverturaReport newReport)
        {
            var comparison = new CoverturaReportComparison
            {
                Old = oldReport,
                New = newReport,
                LineCoverageChange   = decimal.Round(newReport.LineRate - oldReport.LineRate, decimals: 2),
                BranchCoverageChange = decimal.Round(newReport.BranchRate - oldReport.BranchRate, decimals: 2),
                ComplexityChange     = newReport.Complexity - oldReport.Complexity,
            };

            var matchedPackages = new Dictionary <string, CoverturaReportComparison.PackageChanges>();

            foreach (var kvp in oldReport.Packages)
            {
                var oldPackage = kvp.Value;
                // try and find package in other report
                if (!newReport.Packages.TryGetValue(kvp.Key, out var newPackage))
                {
                    // package was removed
                    comparison.RemovedPackages.Add(oldPackage);
                    continue;
                }

                // do class-level comparison
                var changes = new CoverturaReportComparison.PackageChanges
                {
                    Old = oldPackage,
                    New = newPackage,
                    LineCoverageChange   = decimal.Round(newPackage.LineRate - oldPackage.LineRate, decimals: 2),
                    BranchCoverageChange = decimal.Round(newPackage.BranchRate - oldPackage.BranchRate, decimals: 2),
                    ComplexityChange     = newPackage.Complexity - oldPackage.Complexity,
                };

                foreach (var classKvp in oldPackage.Classes)
                {
                    var oldClass = classKvp.Value;
                    if (!newPackage.Classes.TryGetValue(classKvp.Key, out var newClass))
                    {
                        changes.RemovedClasses.Add(oldClass);
                        continue;
                    }

                    var changeSummary = new CoverturaReportComparison.ClassChanges
                    {
                        Name                 = oldClass.Name,
                        Filename             = oldClass.Filename,
                        LineCoverageChange   = decimal.Round(newClass.LineRate - oldClass.LineRate, decimals: 2),
                        BranchCoverageChange = decimal.Round(newClass.BranchRate - oldClass.BranchRate, decimals: 2),
                        ComplexityChange     = newClass.Complexity - oldClass.Complexity,
                    };

                    changeSummary.IsSignificantChange = Math.Abs(changeSummary.LineCoverageChange) > SignificantChangeThreshold ||
                                                        Math.Abs(changeSummary.BranchCoverageChange) > SignificantChangeThreshold;

                    changes.ClassChanges[classKvp.Key] = changeSummary;
                }

                changes.NewClasses =
                    newPackage.Classes
                    .Where(kvp => !oldPackage.Classes.ContainsKey(kvp.Key))
                    .Select(kvp => kvp.Value)
                    .ToList();

                matchedPackages.Add(kvp.Key, changes);
            }

            comparison.MatchedPackages = matchedPackages.Values.ToList();

            comparison.NewPackages =
                newReport.Packages
                .Where(kvp => !matchedPackages.ContainsKey(kvp.Key))
                .Select(kvp => kvp.Value)
                .ToList();

            return(comparison);
        }