Compare commits

...

15 Commits

Author SHA1 Message Date
irongut ac37b8e9b7 prepare v1.1.0-beta release 2021-10-26 23:08:13 +01:00
irongut 7a76884d03 merge PR #18 Health indicators, change thresholds + fail workflow 2021-10-26 22:20:15 +01:00
irongut 2389c9883a added fail_below_min to action definition #16 2021-10-20 00:01:43 +01:00
irongut 02bb824606 added ability to fail a workflow #16 2021-10-19 23:49:51 +01:00
irongut b97bc1147a added indicators parameter to action definition #14 2021-10-19 00:29:37 +01:00
irongut d8b1bc5ef2 updated CLI help text 2021-10-19 00:28:24 +01:00
irongut 7c070aea38 fixed issue with bool parameters 2021-10-19 00:03:22 +01:00
irongut 6ed1f6c50d added health indicators #14 2021-10-18 23:26:42 +01:00
irongut ecc89a90a8 added Thresholds parameter to action definition #15 2021-10-18 01:30:03 +01:00
irongut 4a0dc323d3 added Thresholds parameter to CLI #15 2021-10-18 01:24:48 +01:00
irongut 711b5c996a added sponsorship 2021-10-13 13:01:08 +01:00
irongut 9bbe00ce6b added Cobertura DTD for info 2021-10-10 17:31:53 +01:00
irongut f9c0e2020c updated readme 2021-10-07 23:52:04 +01:00
irongut 1b1147a0ce updated workflows 2021-10-07 15:36:49 +01:00
irongut 3766e5780e add manual testing workflows 2021-10-07 14:52:19 +01:00
13 changed files with 302 additions and 79 deletions
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [irongut]
+11 -14
View File
@@ -1,18 +1,15 @@
# Assign PR to Author # Assign PR to Author
# https://github.com/samspills/assign-pr-to-author # https://github.com/samspills/assign-pr-to-author
# Disabled due to #14 HttpError: Resource not accessible by integration: name: Auto Assign PR
# https://github.com/samspills/assign-pr-to-author/issues/14 on: [pull_request]
#name: Auto Assign PR jobs:
#on: [pull_request] assignAuthor:
runs-on: ubuntu-latest
#jobs: steps:
# assignAuthor: - name: Auto Assign PR
# runs-on: ubuntu-latest uses: samspills/assign-pr-to-author@v1.0
# steps: if: github.event_name == 'pull_request' && github.event.action == 'opened'
# - name: Auto Assign PR with:
# uses: samspills/assign-pr-to-author@v1.0 repo-token: '${{ secrets.GITHUB_TOKEN }}'
# if: github.event_name == 'pull_request' && github.event.action == 'opened'
# with:
# repo-token: '${{ secrets.GITHUB_TOKEN }}'
+18
View File
@@ -0,0 +1,18 @@
name: Test Linux Runner
on:
workflow_dispatch:
branches: [master]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
name: CI Build
steps:
- name: Test Action
uses: irongut/CodeCoverageSummary@master
with:
filename: '/app/sample.coverage.xml'
badge: 'true'
+18
View File
@@ -0,0 +1,18 @@
name: Test MacOS Runner
on:
workflow_dispatch:
branches: [master]
permissions:
contents: read
jobs:
test:
runs-on: macos-latest
name: CI Build
steps:
- name: Test Action
uses: irongut/CodeCoverageSummary@master
with:
filename: '/app/sample.coverage.xml'
badge: 'true'
+18
View File
@@ -0,0 +1,18 @@
name: Test Windows Runner
on:
workflow_dispatch:
branches: [master]
permissions:
contents: read
jobs:
test:
runs-on: windows-latest
name: CI Build
steps:
- name: Test Action
uses: irongut/CodeCoverageSummary@master
with:
filename: '/app/sample.coverage.xml'
badge: 'true'
+34 -9
View File
@@ -1,8 +1,10 @@
# Code Coverage Summary # Code Coverage Summary
A GitHub Action that reads Cobertura format code coverage files from your test suite and outputs a text or markdown summary. This summary can then be posted as a Pull Request comment, included in Release Notes, etc by another action to give you an immediate insight into the health of your code without using a third-party site. A GitHub Action that reads Cobertura format code coverage files from your test suite and outputs a text or markdown summary. This summary can be posted as a Pull Request comment or included in Release Notes by other actions to give you an immediate insight into the health of your code without using a third-party site.
Code Coverage Summary was designed for use with [Coverlet](https://github.com/coverlet-coverage/coverlet) and .Net but it should work with any test framework that outputs coverage in Cobertura format. Code Coverage Summary is designed for use with [Coverlet](https://github.com/coverlet-coverage/coverlet) and [gcovr](https://github.com/gcovr/gcovr) but it should work with any test framework that outputs coverage in Cobertura format.
As a Docker based action Code Coverage Summary requires a Linux runner, see [Types of Action](https://docs.github.com/en/actions/creating-actions/about-custom-actions#types-of-actions). If you need to build with a Windows or MacOS runner a workaround would be to upload the coverage file as an artifact and use a separate job with a Linux runner to generate the summary.
## Inputs ## Inputs
@@ -17,12 +19,30 @@ Note: Coverlet creates the coverage file in a random named directory (guid) so y
Include a badge reporting the Line Rate coverage in the output using [shields.io](https://shields.io/) - `true` or `false` (default). Include a badge reporting the Line Rate coverage in the output using [shields.io](https://shields.io/) - `true` or `false` (default).
If the overall Line Rate is less than 50% the badge will be red, if it is 50% - 74% it will be yellow and if it is 75% or over it will be green. If the overall Line Rate is less than the lower threshold (50%) the badge will be red, if it is between thresholds it will be yellow and if it greater than or equal to the higher threshold (75%) it will be green. See [`thresholds`](#thresholds) to change these values.
#### `fail_below_min`
**v1.1.0-beta only**
Fail the workflow if the overall Line Rate is below lower threshold - `true` or `false` (default). The default lower threshold is 50%, see [`thresholds`](#thresholds).
#### `format` #### `format`
Output Format - `markdown` or `text` (default). Output Format - `markdown` or `text` (default).
#### `indicators`
**v1.1.0-beta only**
Include health indicators in the output - `true` (default) or `false`.'
Line Rate | Indicator
--------- | ---------
less than lower threshold (50%) | ❌
between thresholds (50% - 74%) |
equal or greater than upper threshold (75%) | ✔
See [`thresholds`](#thresholds) to change these values.
#### `output` #### `output`
Output Type - `console` (default), `file` or `both`. Output Type - `console` (default), `file` or `both`.
@@ -33,16 +53,21 @@ Output Type - `console` (default), `file` or `both`.
`both` will output the coverage summary to the Action log and a file as above. `both` will output the coverage summary to the Action log and a file as above.
#### `thresholds`
**v1.1.0-beta only**
Lower and upper threshold percentages for badge and health indicators, lower threshold can also be used to fail the action. Separate the values with a space and enclose them in quotes; default `'50 75'`.
## Outputs ## Outputs
#### Text Example #### Text Example
``` ```
https://img.shields.io/badge/Code%20Coverage-77%25-success?style=flat https://img.shields.io/badge/Code%20Coverage-83%25-success?style=flat
Line Rate = 77%, Lines Covered = 1107 / 1433
Branch Rate = 60%, Branches Covered = 321 / 532 Company.Example: Line Rate = 83%, Branch Rate = 69%, Complexity = 671, ✔
Complexity = 917 Company.Example.Library: Line Rate = 27%, Branch Rate = 100%, Complexity = 11, ❌
Company.Example: Line Rate = 78%, Branch Rate = 60%, Complexity = 906 Summary: Line Rate = 83% (1212 / 1460), Branch Rate = 69% (262 / 378), Complexity = 682, ✔
Company.Example.Library: Line Rate = 27%, Branch Rate = 100%, Complexity = 11 Minimum allowed line rate is 50%
``` ```
#### Markdown Example #### Markdown Example
+20 -2
View File
@@ -9,25 +9,43 @@ inputs:
description: 'Code coverage file to analyse.' description: 'Code coverage file to analyse.'
required: true required: true
badge: badge:
description: 'Include a badge in the output - true / false (default).' description: 'Include a Line Rate coverage badge in the output using shields.io - true / false (default).'
required: false
default: 'false'
fail_below_min:
description: 'Fail if overall Line Rate below lower threshold - true / false (default).'
required: false required: false
default: 'false' default: 'false'
format: format:
description: 'Output Format - markdown or text (default).' description: 'Output Format - markdown or text (default).'
required: false required: false
default: 'text' default: 'text'
indicators:
description: 'Include health indicators in the output - true (default) / false.'
required: false
default: 'true'
output: output:
description: 'Output Type - console (default), file or both.' description: 'Output Type - console (default), file or both.'
required: false required: false
default: 'console' default: 'console'
thresholds:
description: 'Threshold percentages for badge and health indicators, lower threshold can also be used to fail the action.'
required: false
default: '50 75'
runs: runs:
using: 'docker' using: 'docker'
image: 'docker://ghcr.io/irongut/codecoveragesummary:v1.0.5' image: 'docker://ghcr.io/irongut/codecoveragesummary:v1.1.0-beta'
args: args:
- ${{ inputs.filename }} - ${{ inputs.filename }}
- '--badge' - '--badge'
- ${{ inputs.badge }} - ${{ inputs.badge }}
- '--fail'
- ${{ inputs.fail_below_min }}
- '--format' - '--format'
- ${{ inputs.format }} - ${{ inputs.format }}
- '--indicators'
- ${{ inputs.indicators }}
- '--output' - '--output'
- ${{ inputs.output }} - ${{ inputs.output }}
- '--thresholds'
- ${{ inputs.thresholds }}
@@ -14,7 +14,7 @@
<RepositoryUrl>https://github.com/irongut/CodeCoverageSummary</RepositoryUrl> <RepositoryUrl>https://github.com/irongut/CodeCoverageSummary</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<PackageTags>coverage test-coverage cobertura action code-coverage coverlet github-actions</PackageTags> <PackageTags>coverage test-coverage cobertura action code-coverage coverlet github-actions</PackageTags>
<Version>1.0.5</Version> <Version>1.1.0</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
+18 -2
View File
@@ -1,4 +1,5 @@
using CommandLine; using CommandLine;
using System;
namespace CodeCoverageSummary namespace CodeCoverageSummary
{ {
@@ -7,13 +8,28 @@ namespace CodeCoverageSummary
[Value(index: 0, Required = true, HelpText = "Code coverage file to analyse.")] [Value(index: 0, Required = true, HelpText = "Code coverage file to analyse.")]
public string Filename { get; set; } public string Filename { get; set; }
[Option(longName: "badge", Required = false, HelpText = "Include a badge reporting the Line Rate coverage in the output using shields.io - true or false.", Default = false)] [Option(longName: "badge", Required = false, HelpText = "Include a Line Rate coverage badge in the output using shields.io - true or false.", Default = "false")]
public bool Badge { get; set; } public string BadgeString { get; set; }
public bool Badge => BadgeString.Equals("true", StringComparison.OrdinalIgnoreCase);
[Option(longName: "fail", Required = false, HelpText = "Fail if overall Line Rate below lower threshold - true or false.", Default = "false")]
public string FailString { get; set; }
public bool FailBelowThreshold => FailString.Equals("true", StringComparison.OrdinalIgnoreCase);
[Option(longName: "format", Required = false, HelpText = "Output Format - markdown or text.", Default = "text")] [Option(longName: "format", Required = false, HelpText = "Output Format - markdown or text.", Default = "text")]
public string Format { get; set; } public string Format { get; set; }
[Option(longName: "indicators", Required = false, HelpText = "Include health indicators in the output - true or false.", Default = "true")]
public string IndicatorsString { get; set; }
public bool Indicators => IndicatorsString.Equals("true", StringComparison.OrdinalIgnoreCase);
[Option(longName: "output", Required = false, HelpText = "Output Type - console, file or both.", Default = "console")] [Option(longName: "output", Required = false, HelpText = "Output Type - console, file or both.", Default = "console")]
public string Output { get; set; } public string Output { get; set; }
[Option(longName: "thresholds", Required = false, HelpText = "Threshold percentages for badge and health indicators, lower threshold can also be used to fail the action.", Default = "50 75")]
public string Thresholds { get; set; }
} }
} }
@@ -6,3 +6,4 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Performance", "RCS1197:Optimize StringBuilder.Append/AppendLine call.", Scope = "type", Target = "~T:CodeCoverageSummary.Program")] [assembly: SuppressMessage("Performance", "RCS1197:Optimize StringBuilder.Append/AppendLine call.", Scope = "type", Target = "~T:CodeCoverageSummary.Program")]
[assembly: SuppressMessage("Style", "IDE0057:Use range operator", Scope = "member", Target = "~M:CodeCoverageSummary.Program.SetThresholds(System.String)")]
+99 -50
View File
@@ -9,6 +9,9 @@ namespace CodeCoverageSummary
{ {
internal static class Program internal static class Program
{ {
private static double lowerThreshold = 0.5;
private static double upperThreshold = 0.75;
private static int Main(string[] args) private static int Main(string[] args)
{ {
return Parser.Default.ParseArguments<CommandLineOptions>(args) return Parser.Default.ParseArguments<CommandLineOptions>(args)
@@ -32,6 +35,10 @@ namespace CodeCoverageSummary
} }
else else
{ {
// set health badge thresholds
if (!string.IsNullOrWhiteSpace(o.Thresholds))
SetThresholds(o.Thresholds);
// generate badge // generate badge
string badgeUrl = o.Badge ? GenerateBadge(summary) : null; string badgeUrl = o.Badge ? GenerateBadge(summary) : null;
@@ -41,12 +48,16 @@ namespace CodeCoverageSummary
if (o.Format.Equals("text", StringComparison.OrdinalIgnoreCase)) if (o.Format.Equals("text", StringComparison.OrdinalIgnoreCase))
{ {
fileExt = "txt"; fileExt = "txt";
output = GenerateTextOutput(summary, badgeUrl); output = GenerateTextOutput(summary, badgeUrl, o.Indicators);
if (o.FailBelowThreshold)
output += $"Minimum allowed line rate is {lowerThreshold * 100:N0}%{Environment.NewLine}";
} }
else if (o.Format.Equals("md", StringComparison.OrdinalIgnoreCase) || o.Format.Equals("markdown", StringComparison.OrdinalIgnoreCase)) else if (o.Format.Equals("md", StringComparison.OrdinalIgnoreCase) || o.Format.Equals("markdown", StringComparison.OrdinalIgnoreCase))
{ {
fileExt = "md"; fileExt = "md";
output = GenerateMarkdownOutput(summary, badgeUrl); output = GenerateMarkdownOutput(summary, badgeUrl, o.Indicators);
if (o.FailBelowThreshold)
output += $"{Environment.NewLine}_Minimum allowed line rate is `{lowerThreshold * 100:N0}%`_{Environment.NewLine}";
} }
else else
{ {
@@ -74,6 +85,12 @@ namespace CodeCoverageSummary
return -2; // error return -2; // error
} }
if (o.FailBelowThreshold && summary.LineRate < lowerThreshold)
{
Console.WriteLine($"FAIL: Overall line rate below minimum threshold of {lowerThreshold * 100:N0}%.");
return -2;
}
return 0; // success return 0; // success
} }
} }
@@ -158,14 +175,49 @@ namespace CodeCoverageSummary
} }
} }
private static void SetThresholds(string thresholds)
{
int lowerPercentage;
int upperPercentage = (int)(upperThreshold * 100);
int s = thresholds.IndexOf(" ");
if (s == 0)
{
throw new ArgumentException("Threshold parameter set incorrectly.");
}
else if (s < 0)
{
if (!int.TryParse(thresholds, out lowerPercentage))
throw new ArgumentException("Threshold parameter set incorrectly.");
}
else
{
if (!int.TryParse(thresholds.Substring(0, s), out lowerPercentage))
throw new ArgumentException("Threshold parameter set incorrectly.");
if (!int.TryParse(thresholds.Substring(s + 1), out upperPercentage))
throw new ArgumentException("Threshold parameter set incorrectly.");
}
lowerThreshold = lowerPercentage / 100.0;
upperThreshold = upperPercentage / 100.0;
if (lowerThreshold > 1.0)
lowerThreshold = 1.0;
if (lowerThreshold > upperThreshold)
upperThreshold = lowerThreshold + 0.1;
if (upperThreshold > 1.0)
upperThreshold = 1.0;
}
private static string GenerateBadge(CodeSummary summary) private static string GenerateBadge(CodeSummary summary)
{ {
string colour; string colour;
if (summary.LineRate < 0.5) if (summary.LineRate < lowerThreshold)
{ {
colour = "critical"; colour = "critical";
} }
else if (summary.LineRate < 0.75) else if (summary.LineRate < upperThreshold)
{ {
colour = "yellow"; colour = "yellow";
} }
@@ -176,78 +228,75 @@ namespace CodeCoverageSummary
return $"https://img.shields.io/badge/Code%20Coverage-{summary.LineRate * 100:N0}%25-{colour}?style=flat"; return $"https://img.shields.io/badge/Code%20Coverage-{summary.LineRate * 100:N0}%25-{colour}?style=flat";
} }
private static string GenerateTextOutput(CodeSummary summary, string badgeUrl) private static string GenerateHealthIndicator(double rate)
{
if (rate < lowerThreshold)
{
return "❌";
}
else if (rate < upperThreshold)
{
return "";
}
else
{
return "✔";
}
}
private static string GenerateTextOutput(CodeSummary summary, string badgeUrl, bool indicators)
{ {
StringBuilder textOutput = new(); StringBuilder textOutput = new();
if (!string.IsNullOrWhiteSpace(badgeUrl)) if (!string.IsNullOrWhiteSpace(badgeUrl))
{ {
textOutput.AppendLine(badgeUrl); textOutput.AppendLine(badgeUrl)
} .AppendLine();
textOutput.AppendLine($"Line Rate = {summary.LineRate * 100:N0}%, Lines Covered = {summary.LinesCovered} / {summary.LinesValid}")
.AppendLine($"Branch Rate = {summary.BranchRate * 100:N0}%, Branches Covered = {summary.BranchesCovered} / {summary.BranchesValid}");
if (summary.Complexity % 1 == 0)
{
textOutput.AppendLine($"Complexity = {summary.Complexity}");
}
else
{
textOutput.AppendLine($"Complexity = {summary.Complexity:N4}");
} }
foreach (CodeCoverage package in summary.Packages) foreach (CodeCoverage package in summary.Packages)
{ {
if (package.Complexity % 1 == 0) textOutput.Append($"{package.Name}: Line Rate = {package.LineRate * 100:N0}%")
{ .Append($", Branch Rate = {package.BranchRate * 100:N0}%")
textOutput.AppendLine($"{package.Name}: Line Rate = {package.LineRate * 100:N0}%, Branch Rate = {package.BranchRate * 100:N0}%, Complexity = {package.Complexity}"); .Append((package.Complexity % 1 == 0) ? $", Complexity = {package.Complexity}" : $", Complexity = {package.Complexity:N4}")
} .AppendLine(indicators ? $", {GenerateHealthIndicator(package.LineRate)}" : string.Empty);
else
{
textOutput.AppendLine($"{package.Name}: Line Rate = {package.LineRate * 100:N0}%, Branch Rate = {package.BranchRate * 100:N0}%, Complexity = {package.Complexity:N4}");
}
} }
textOutput.Append($"Summary: Line Rate = {summary.LineRate * 100:N0}% ({summary.LinesCovered} / {summary.LinesValid})")
.Append($", Branch Rate = {summary.BranchRate * 100:N0}% ({summary.BranchesCovered} / {summary.BranchesValid})")
.Append((summary.Complexity % 1 == 0) ? $", Complexity = {summary.Complexity}" : $", Complexity = {summary.Complexity:N4}")
.AppendLine(indicators ? $", {GenerateHealthIndicator(summary.LineRate)}" : string.Empty);
return textOutput.ToString(); return textOutput.ToString();
} }
private static string GenerateMarkdownOutput(CodeSummary summary, string badgeUrl) private static string GenerateMarkdownOutput(CodeSummary summary, string badgeUrl, bool indicators)
{ {
StringBuilder markdownOutput = new(); StringBuilder markdownOutput = new();
if (!string.IsNullOrWhiteSpace(badgeUrl)) if (!string.IsNullOrWhiteSpace(badgeUrl))
{ {
markdownOutput.AppendLine($"![Code Coverage]({badgeUrl})"); markdownOutput.AppendLine($"![Code Coverage]({badgeUrl})")
markdownOutput.AppendLine(""); .AppendLine();
} }
markdownOutput.AppendLine("Package | Line Rate | Branch Rate | Complexity") markdownOutput.Append("Package | Line Rate | Branch Rate | Complexity")
.AppendLine("-------- | --------- | ----------- | ----------"); .AppendLine(indicators ? " | Health" : string.Empty)
.Append("-------- | --------- | ----------- | ----------")
.AppendLine(indicators ? " | ------" : string.Empty);
foreach (CodeCoverage package in summary.Packages) foreach (CodeCoverage package in summary.Packages)
{ {
if (package.Complexity % 1 == 0) markdownOutput.Append($"{package.Name} | {package.LineRate * 100:N0}%")
{ .Append($" | {package.BranchRate * 100:N0}%")
markdownOutput.AppendLine($"{package.Name} | {package.LineRate * 100:N0}% | {package.BranchRate * 100:N0}% | {package.Complexity}"); .Append((package.Complexity % 1 == 0) ? $" | {package.Complexity}" : $" | {package.Complexity:N4}" )
} .AppendLine(indicators ? $" | {GenerateHealthIndicator(package.LineRate)}" : string.Empty);
else
{
markdownOutput.AppendLine($"{package.Name} | {package.LineRate * 100:N0}% | {package.BranchRate * 100:N0}% | {package.Complexity:N4}");
}
} }
markdownOutput.Append($"**Summary** | **{summary.LineRate * 100:N0}%** ({summary.LinesCovered} / {summary.LinesValid}) | ") markdownOutput.Append($"**Summary** | **{summary.LineRate * 100:N0}%** ({summary.LinesCovered} / {summary.LinesValid})")
.Append($"**{summary.BranchRate * 100:N0}%** ({summary.BranchesCovered} / {summary.BranchesValid}) | "); .Append($" | **{summary.BranchRate * 100:N0}%** ({summary.BranchesCovered} / {summary.BranchesValid})")
.Append((summary.Complexity % 1 == 0) ? $" | {summary.Complexity}" : $" | {summary.Complexity:N4}")
if (summary.Complexity % 1 == 0) .AppendLine(indicators ? $" | {GenerateHealthIndicator(summary.LineRate)}" : string.Empty);
{
markdownOutput.AppendLine(summary.Complexity.ToString());
}
else
{
markdownOutput.AppendLine(summary.Complexity.ToString("N4"));
}
return markdownOutput.ToString(); return markdownOutput.ToString();
} }
@@ -2,7 +2,7 @@
"profiles": { "profiles": {
"CodeCoverageSummary": { "CodeCoverageSummary": {
"commandName": "Project", "commandName": "Project",
"commandLineArgs": "../../../../coverage.gcovr.xml --format=md --badge=true" "commandLineArgs": "../../../../coverage.cobertura.xml --format=md --badge true --thresholds=\"85 90\" --fail true"
}, },
"Docker": { "Docker": {
"commandName": "Docker", "commandName": "Docker",
+60
View File
@@ -0,0 +1,60 @@
<!-- Portions (C) International Organization for Standardization 1986:
Permission to copy in any form is granted for use with
conforming SGML systems and applications as defined in
ISO 8879, provided this notice is included in all copies.
-->
<!ELEMENT coverage (sources?,packages)>
<!ATTLIST coverage line-rate CDATA #REQUIRED>
<!ATTLIST coverage branch-rate CDATA #REQUIRED>
<!ATTLIST coverage lines-covered CDATA #REQUIRED>
<!ATTLIST coverage lines-valid CDATA #REQUIRED>
<!ATTLIST coverage branches-covered CDATA #REQUIRED>
<!ATTLIST coverage branches-valid CDATA #REQUIRED>
<!ATTLIST coverage complexity CDATA #REQUIRED>
<!ATTLIST coverage version CDATA #REQUIRED>
<!ATTLIST coverage timestamp CDATA #REQUIRED>
<!ELEMENT sources (source*)>
<!ELEMENT source (#PCDATA)>
<!ELEMENT packages (package*)>
<!ELEMENT package (classes)>
<!ATTLIST package name CDATA #REQUIRED>
<!ATTLIST package line-rate CDATA #REQUIRED>
<!ATTLIST package branch-rate CDATA #REQUIRED>
<!ATTLIST package complexity CDATA #REQUIRED>
<!ELEMENT classes (class*)>
<!ELEMENT class (methods,lines)>
<!ATTLIST class name CDATA #REQUIRED>
<!ATTLIST class filename CDATA #REQUIRED>
<!ATTLIST class line-rate CDATA #REQUIRED>
<!ATTLIST class branch-rate CDATA #REQUIRED>
<!ATTLIST class complexity CDATA #REQUIRED>
<!ELEMENT methods (method*)>
<!ELEMENT method (lines)>
<!ATTLIST method name CDATA #REQUIRED>
<!ATTLIST method signature CDATA #REQUIRED>
<!ATTLIST method line-rate CDATA #REQUIRED>
<!ATTLIST method branch-rate CDATA #REQUIRED>
<!ELEMENT lines (line*)>
<!ELEMENT line (conditions*)>
<!ATTLIST line number CDATA #REQUIRED>
<!ATTLIST line hits CDATA #REQUIRED>
<!ATTLIST line branch CDATA "false">
<!ATTLIST line condition-coverage CDATA "100%">
<!ELEMENT conditions (condition*)>
<!ELEMENT condition EMPTY>
<!ATTLIST condition number CDATA #REQUIRED>
<!ATTLIST condition type CDATA #REQUIRED>
<!ATTLIST condition coverage CDATA #REQUIRED>