Compare commits

..

70 Commits

Author SHA1 Message Date
irongut 97f4a52b81 fixed release build workflow 2021-11-24 22:01:43 +00:00
irongut 95ea154a7b prepare v1.2.0-beta release 2021-11-24 21:47:59 +00:00
irongut 2e00d30f70 merge PR #30 Update to .Net 6 2021-11-24 20:27:20 +00:00
irongut c4d4b9a087 updated build workflows to .Net 6 #23 2021-11-24 00:34:44 +00:00
irongut ea5d3b417b updated build workflows to .Net 6 #23 2021-11-24 00:32:25 +00:00
irongut 3d33fafb37 updated Docker launch settings 2021-11-24 00:24:33 +00:00
irongut 3a41f0a5ea reduced number of Docker image layers 2021-11-23 21:54:11 +00:00
irongut dd5c8d3b75 updated Dockerfile to .Net 6 #23 2021-11-23 20:56:58 +00:00
irongut 4986e930de updated CLI to .Net 6 #23 2021-11-23 01:08:46 +00:00
irongut 37dca42320 added Assign to Project workflow 2021-11-22 02:19:37 +00:00
irongut 5ec10c1882 updated assign-pr-to-author action 2021-11-22 02:10:34 +00:00
irongut 56c9f3d623 merge PR #29 add PR Labeler action
PR: add PR Labeler action
2021-11-22 02:05:16 +00:00
irongut f36c88a82b configure pr labeler 2021-11-22 02:00:18 +00:00
irongut ffe316d009 added pr labeler action 2021-11-22 01:33:05 +00:00
irongut 1ea9b55e4d merge PR #27 from dependabot/Microsoft.VisualStudio.Azure.Containers.Tools.Targets-1.14.0
Bump Microsoft.VisualStudio.Azure.Containers.Tools.Targets from 1.11.1 to 1.14.0
2021-11-16 10:32:40 +00:00
dependabot[bot] fcb924f622 Bump Microsoft.VisualStudio.Azure.Containers.Tools.Targets
Bumps Microsoft.VisualStudio.Azure.Containers.Tools.Targets from 1.11.1 to 1.14.0.

---
updated-dependencies:
- dependency-name: Microsoft.VisualStudio.Azure.Containers.Tools.Targets
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-15 23:08:36 +00:00
irongut ed588922f2 merge PR #26 Support multiple cobertura files
PR: Support multiple cobertura files
2021-11-15 01:21:55 +00:00
irongut 6e12bd152f removed commented line 2021-11-15 00:33:15 +00:00
irongut 4ae964bab0 update ci build test command line #19 2021-11-15 00:24:46 +00:00
irongut 60646036b5 support multiple cobertura files in action definition #19 2021-11-15 00:18:30 +00:00
irongut 1c2edd9230 support multiple cobertura files in CLI #19 2021-11-14 23:51:30 +00:00
irongut 9caa66feee merge PR #25 Allow hiding Branch Rate + Complexity values in output
PR: Allow hiding Branch Rate + Complexity values in output
2021-11-14 00:41:09 +00:00
irongut 6c68cb69dd added hide branch rate + complexity to action definition #22 2021-11-13 23:53:03 +00:00
irongut 914b6fe5f9 fixed summary complexity should be bold in md output 2021-11-13 22:35:52 +00:00
irongut 46007a7270 hide branch rate + complexity for markdown output #22 2021-11-13 22:29:15 +00:00
irongut 71ae720dab hide branch rate + complexity for text output #22 2021-11-13 21:55:29 +00:00
irongut dd7d40d268 added hide branch + complixity CLI options #22 2021-11-13 21:29:36 +00:00
irongut 065eaf7bb5 prepare v1.1.0 release 2021-10-27 22:35:09 +01:00
irongut a5c4e90cbe updated readme 2021-10-27 00:41:20 +01:00
irongut f89139ddac updated test action 2021-10-26 23:53:11 +01:00
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
irongut 473d18e3ad prepare v1.0.5 2021-09-27 20:21:03 +01:00
irongut 61d10948c8 merge PR #10 Make compatible with gcovr 2021-09-27 19:44:43 +01:00
irongut 7cd8fe49e9 improved decimal complexity output #9 2021-09-27 19:41:59 +01:00
irongut 174c97ac3d handle empty package names #9 2021-09-26 01:49:23 +01:00
irongut 9b11daeca8 handle Complexity as an int or percentage #9 2021-09-26 00:58:44 +01:00
irongut 6db46e287e handle missing package attributes better 2021-09-26 00:01:55 +01:00
irongut c5d12d4a89 prepare v1.0.4 2021-09-25 21:48:17 +01:00
irongut cae1cbcdad merge PR #8 Bump docker image to v1.0.3 in action.yml from reallyreallyreal/master
Bump docker image to v1.0.3 in action.yml
2021-09-25 21:24:23 +01:00
Jonathan Derrough ed258b1479 Bump docker image to v1.0.3 in action.yml 2021-09-25 12:09:21 +07:00
irongut 0723132cf6 prepare v1.0.3 2021-09-24 20:41:43 +01:00
irongut 758c38f591 removed short params 2021-09-23 02:08:23 +01:00
irongut 24062a1b2a added changelog to release workflow 2021-09-23 01:48:41 +01:00
irongut dcef09e405 merge PR #7 Handle optional complexity attribute 2021-09-22 22:32:30 +01:00
Jonathan Derrough 9cce3f5374 Handle optional complexity attribute 2021-09-22 18:19:21 +07:00
irongut 20025e7e93 formatting + cleanup 2021-09-20 01:00:33 +01:00
irongut 0bcadffdb9 workflow fixes 2021-09-06 15:06:40 +01:00
irongut 71c6f23d2f use container from GHCR for Action #4 2021-09-06 01:11:36 +01:00
irongut 162def6e89 deploy step needs to checkout again #4 2021-09-06 00:58:20 +01:00
irongut e07628e8aa added deploy to GHCR workflow #4 2021-09-06 00:43:14 +01:00
irongut fc71f6831f merge PR #3 from dependabot/Microsoft.VisualStudio.Azure.Containers.Tools.Targets-1.11.1
Bump Microsoft.VisualStudio.Azure.Containers.Tools.Targets from 1.10.13 to 1.11.1
2021-09-05 23:15:14 +01:00
irongut c4d24d5996 merge PR #5 add empty line after badge from joshjohanning/master
adding empty line after badge so table renders properly
2021-09-05 23:09:40 +01:00
Joshua Johanning f5bcfce861 adding empty line after badge so table formats properly 2021-09-03 15:51:45 -05:00
dependabot[bot] a97b5158b3 Bump Microsoft.VisualStudio.Azure.Containers.Tools.Targets
Bumps Microsoft.VisualStudio.Azure.Containers.Tools.Targets from 1.10.13 to 1.11.1.

---
updated-dependencies:
- dependency-name: Microsoft.VisualStudio.Azure.Containers.Tools.Targets
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-10 23:11:27 +00:00
irongut e67bf0d2d1 updated documentation 2021-05-13 23:06:38 +01:00
irongut 1c05e17f24 updated readme 2021-05-10 23:30:39 +01:00
21 changed files with 1717 additions and 158 deletions
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [irongut]
+31
View File
@@ -0,0 +1,31 @@
# Configuration for PR Labeller Action
# See: https://github.com/actions/labeler/blob/master/README.md
# Examples
# Add 'label1' to PR if anything changes within 'example' folder or any subfolders
# label1:
# - example/**/*
# Add 'label2' to PR if any file changes within 'example2' folder
# label2: example2/*
Action:
- action.yml
Docker:
- Dockerfile
- .dockerignore
Options:
- src/CodeCoverageSummary/CommandLineOptions.cs
Parsing:
- src/CodeCoverageSummary/CodeSummary.cs
- src/CodeCoverageSummary/Program.cs
Summary:
- src/CodeCoverageSummary/CodeSummary.cs
- src/CodeCoverageSummary/Program.cs
DevOps:
- .github/**/*
+42
View File
@@ -0,0 +1,42 @@
name: Assign to Project
on:
issues:
types: [opened, labeled]
pull_request:
types: [opened, labeled]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
assign-to-project:
runs-on: ubuntu-latest
name: Assign to Project
steps:
- name: Assign Issues to Bugs
uses: srggrs/assign-one-project-github-action@1.3.1
if: contains(github.event.issue.labels.*.name, 'bug')
with:
project: 'https://github.com/irongut/EDlib/projects/1'
column_name: 'Needs triage'
- name: Assign Issues to Enhancements
uses: srggrs/assign-one-project-github-action@1.3.1
if: contains(github.event.issue.labels.*.name, 'enhancement')
with:
project: 'https://github.com/irongut/EDlib/projects/2'
column_name: 'To do'
- name: Assign PRs to Bugs
uses: srggrs/assign-one-project-github-action@1.3.1
if: contains(github.event.pull_request.labels.*.name, 'bug')
with:
project: 'https://github.com/irongut/EDlib/projects/1'
column_name: 'In Progress'
- name: Assign PRs to Enhancements
uses: srggrs/assign-one-project-github-action@1.3.1
if: contains(github.event.pull_request.labels.*.name, 'enhancement')
with:
project: 'https://github.com/irongut/EDlib/projects/2'
column_name: 'In Progress'
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Auto Assign PR
uses: samspills/assign-pr-to-author@v1.0
uses: samspills/assign-pr-to-author@v1.0.1
if: github.event_name == 'pull_request' && github.event.action == 'opened'
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'
+7 -11
View File
@@ -11,11 +11,13 @@ jobs:
runs-on: ubuntu-latest
name: CI Build
steps:
- uses: actions/checkout@v2
- name: Setup .NET
- name: Checkout
uses: actions/checkout@v2
- name: Setup .Net
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.x
dotnet-version: 6.0.x
- name: Restore Dependencies
run: dotnet restore src/CodeCoverageSummary.sln
@@ -23,11 +25,5 @@ jobs:
- name: Build CodeCoverageSummary
run: dotnet build src/CodeCoverageSummary.sln --configuration Release --no-restore
- name: Run CodeCoverageSummary with sample file
run: dotnet src/CodeCoverageSummary/bin/Release/net5.0/CodeCoverageSummary.dll src/coverage.cobertura.xml --badge true
- name: Test Action
uses: irongut/CodeCoverageSummary@master
with:
filename: '/app/sample.coverage.xml'
badge: 'true'
- name: Test with sample file
run: dotnet src/CodeCoverageSummary/bin/Release/net6.0/CodeCoverageSummary.dll --files src/coverage.cobertura.xml --badge true
+16
View File
@@ -0,0 +1,16 @@
# Applies labels to pull requests based on paths & files modified in the pull request.
#
# .github/labeler.yml contains the list of labels & files / folders to match, see:
# https://github.com/actions/labeler/blob/master/README.md
name: PR Labeler
on:
pull_request_target:
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v3
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
+65
View File
@@ -0,0 +1,65 @@
name: Build + Deploy to GHCR
on:
release:
types: [published]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
name: Test Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup .Net
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Restore Dependencies
run: dotnet restore src/CodeCoverageSummary.sln
- name: Build CodeCoverageSummary
run: dotnet build src/CodeCoverageSummary.sln --configuration Release --no-restore
- name: Test with sample file
run: dotnet src/CodeCoverageSummary/bin/Release/net6.0/CodeCoverageSummary.dll --files src/coverage.cobertura.xml --badge true
deploy:
name: Deploy to GHCR
needs: [build]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build + Push Docker image
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+20
View File
@@ -0,0 +1,20 @@
name: Test Linux Runner
on:
workflow_dispatch:
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
format: 'md'
fail_below_min: true
thresholds: '60 80'
+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'
+6 -7
View File
@@ -1,11 +1,10 @@
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
COPY ["src/coverage.cobertura.xml", "/publish/sample.coverage.xml"]
WORKDIR /src
COPY ["src/CodeCoverageSummary/CodeCoverageSummary.csproj", "CodeCoverageSummary/"]
RUN dotnet restore CodeCoverageSummary/CodeCoverageSummary.csproj
COPY ["src/CodeCoverageSummary", "CodeCoverageSummary/"]
COPY ["src/coverage.cobertura.xml", "sample.coverage.xml"]
RUN dotnet build CodeCoverageSummary/CodeCoverageSummary.csproj --configuration Release --no-restore --output /app/build
RUN dotnet publish CodeCoverageSummary/CodeCoverageSummary.csproj --configuration Release --no-restore --output /app/publish
RUN dotnet publish CodeCoverageSummary/CodeCoverageSummary.csproj --configuration Release --no-restore --output /publish
# Label the container
LABEL maintainer="Irongut <murray.dave@outlook.com>"
@@ -18,8 +17,8 @@ LABEL com.github.actions.description="A GitHub Action that reads Cobertura forma
LABEL com.github.actions.icon="book-open"
LABEL com.github.actions.color="purple"
FROM mcr.microsoft.com/dotnet/runtime:5.0 AS final
FROM mcr.microsoft.com/dotnet/runtime:6.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
COPY --from=build /src/sample.coverage.xml .
COPY --from=build /publish .
ENV DOTNET_EnableDiagnostics=0
ENTRYPOINT ["dotnet", "/app/CodeCoverageSummary.dll"]
+97 -19
View File
@@ -1,29 +1,85 @@
# Code Coverage Summary
A GitHub Action that reads Cobertura format code coverage files and outputs a text or markdown summary. Other Actions can then post the summary as a pull request comment, include it in release notes, etc.
<div align="center">
[![CI Build](https://github.com/irongut/CodeCoverageSummary/actions/workflows/ci-build.yml/badge.svg)](https://github.com/irongut/CodeCoverageSummary/actions/workflows/ci-build.yml)
&nbsp;
[![GitHub](https://img.shields.io/badge/GitHub-irongut/CodeCoverageSummary-informational?style=flat&logo=github)](https://github.com/irongut/CodeCoverageSummary)
&nbsp;
![.NET 6.0](https://img.shields.io/badge/Version-.NET%206.0-informational?style=flat&logo=dotnet)
&nbsp;
![Built With Docker](https://img.shields.io/badge/Built_With-Docker-informational?style=flat&logo=docker)
</div>
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 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. If it doesn't work with your tooling please [open an issue][new-issue] to discuss the problem.
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.
Written for use with [Coverlet](https://github.com/coverlet-coverage/coverlet) and .Net but it should work with any tests that output coverage in Cobertura format.
## Inputs
#### `filename`
### `filename`
**Required**
Code coverage file to analyse.
**v1.2.0-beta only:** A comma separated list of code coverage files to analyse.
Note: Coverlet creates the coverage file in a random named directory (guid) so you need to copy it to a predictable path before running this Action, see the [.Net 5 Workflow Example](#net-5-workflow-example) below.
#### `badge`
### `badge`
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.
Line Rate | Badge
--------- | -----
less than lower threshold (50%) | ![Code Coverage](https://img.shields.io/badge/Code%20Coverage-45%25-critical?style=flat)
between thresholds (50% - 74%) | ![Code Coverage](https://img.shields.io/badge/Code%20Coverage-65%25-yellow?style=flat)
equal or greater than upper threshold (75%) | ![Code Coverage](https://img.shields.io/badge/Code%20Coverage-83%25-success?style=flat)
#### `format`
See [`thresholds`](#thresholds) to change these values.
### `fail_below_min`
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`
Output Format - `markdown` or `text` (default).
#### `output`
### `hide_branch_rate`
**v1.2.0-beta only**
Hide Branch Rate values in the output - `true` or `false` (default).
### `hide_complexity`
**v1.2.0-beta only**
Hide Complexity values in the output - `true` or `false` (default).
### `indicators`
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 Type - `console` (default), `file` or `both`.
@@ -33,30 +89,46 @@ Output Type - `console` (default), `file` or `both`.
`both` will output the coverage summary to the Action log and a file as above.
### `thresholds`
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
#### Text Example
### Text Example
```
https://img.shields.io/badge/Code%20Coverage-77%25-success?style=flat
Line Rate = 77%, Lines Covered = 1107 / 1433
Branch Rate = 60%, Branches Covered = 321 / 532
Complexity = 917
Company.Example: Line Rate = 78%, Branch Rate = 60%, Complexity = 906
Company.Example.Library: Line Rate = 27%, Branch Rate = 100%, Complexity = 11
https://img.shields.io/badge/Code%20Coverage-83%25-success?style=flat
Company.Example: Line Rate = 83%, Branch Rate = 69%, Complexity = 671, ✔
Company.Example.Library: Line Rate = 27%, Branch Rate = 100%, Complexity = 11, ❌
Summary: Line Rate = 83% (1212 / 1460), Branch Rate = 69% (262 / 378), Complexity = 682, ✔
Minimum allowed line rate is 50%
```
#### Markdown Example
![image](https://user-images.githubusercontent.com/27953302/117726304-4ac1c100-b1de-11eb-8d9a-6286ba1f5523.png)
### Markdown Example
> ![Code Coverage](https://img.shields.io/badge/Code%20Coverage-83%25-success?style=flat)
>
> Package | Line Rate | Branch Rate | Complexity | Health
> -------- | --------- | ----------- | ---------- | ------
> Company.Example | 83% | 69% | 671 | ✔
> Company.Example.Library | 27% | 100% | 11 | ❌
> **Summary** | **83%** (1212 / 1460) | **69%** (262 / 378) | 682 | ✔
## Usage
```yaml
name: Code Coverage Summary Report
uses: irongut/CodeCoverageSummary@v1
uses: irongut/CodeCoverageSummary@v1.1.0
with:
filename: coverage/coverage.cobertura.xml
```
### .Net 5 Workflow Example
```yaml
@@ -94,12 +166,14 @@ jobs:
run: cp coverage/**/coverage.cobertura.xml coverage/coverage.cobertura.xml
- name: Code Coverage Summary Report
uses: irongut/CodeCoverageSummary@v1
uses: irongut/CodeCoverageSummary@v1.1.0
with:
filename: coverage/coverage.cobertura.xml
badge: true
fail_below_min: true
format: 'markdown'
output: 'both'
thresholds: '70 80'
- name: Add Coverage PR Comment
uses: marocchino/sticky-pull-request-comment@v2
@@ -109,18 +183,21 @@ jobs:
path: code-coverage-results.md
```
## Contributing
### Report Bugs
Please make sure the bug is not already reported by searching existing [issues].
If you're unable to find an existing issue addressing the problem [open a new one][new-issue]. Be sure to include a title and clear description, as much relevant information as possible, a workflow sample and any logs demonstrating the problem.
If you're unable to find an existing issue addressing the problem please [open a new one][new-issue]. Be sure to include a title and clear description, as much relevant information as possible, a workflow sample and any logs demonstrating the problem.
### Suggest an Enhancement
Please [open a new issue][new-issue].
### Submit a Pull Request
Discuss your idea first, so that your changes have a good chance of being merged in.
@@ -129,6 +206,7 @@ Submit your pull request against the `master` branch.
Pull requests that include documentation and relevant updates to README.md are merged faster, because you won't have to wait for somebody else to complete your contribution.
## License
Code Coverage Summary is available under the MIT license, see the [LICENSE](LICENSE) file for more info.
+34 -3
View File
@@ -6,28 +6,59 @@ branding:
color: purple
inputs:
filename:
description: 'Code coverage file to analyse.'
description: 'A comma separated list of code coverage files to analyse.'
required: true
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
default: 'false'
format:
description: 'Output Format - markdown or text (default).'
required: false
default: 'text'
hide_branch_rate:
description: 'Hide Branch Rate values in the output - true / false (default).'
required: false
default: 'false'
hide_complexity:
description: 'Hide Complexity values in the output - true / false (default).'
required: false
default: 'false'
indicators:
description: 'Include health indicators in the output - true (default) / false.'
required: false
default: 'true'
output:
description: 'Output Type - console (default), file or both.'
required: false
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:
using: 'docker'
image: 'Dockerfile'
image: 'docker://ghcr.io/irongut/codecoveragesummary:v1.2.0-beta'
args:
- '--files'
- ${{ inputs.filename }}
- '--badge'
- ${{ inputs.badge }}
- '--fail'
- ${{ inputs.fail_below_min }}
- '--format'
- ${{ inputs.format }}
- '--hidebranch'
- ${{ inputs.hide_branch_rate }}
- '--hidecomplexity'
- ${{ inputs.hide_complexity }}
- '--indicators'
- ${{ inputs.indicators }}
- '--output'
- ${{ inputs.output }}
- '--thresholds'
- ${{ inputs.thresholds }}
@@ -2,23 +2,24 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<Company>Taranis Software</Company>
<Authors>Irongut</Authors>
<Description>A GitHub Action that reads Cobertura format code coverage files and outputs a summary.</Description>
<Description>A GitHub Action that reads Cobertura format code coverage files and outputs a text or markdown summary.</Description>
<Copyright>Copyright © 2021 Taranis Software</Copyright>
<PackageId>Taranis.CodeCoverageSummary</PackageId>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/irongut/CodeCoverageSummary</PackageProjectUrl>
<RepositoryUrl>https://github.com/irongut/CodeCoverageSummary</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>code-coverage cobertura coverlet</PackageTags>
<PackageTags>coverage test-coverage cobertura action code-coverage coverlet github-actions</PackageTags>
<Version>1.2.0</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.13" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.14.0" />
</ItemGroup>
</Project>
+3 -6
View File
@@ -10,7 +10,7 @@ namespace CodeCoverageSummary
public double BranchRate { get; set; }
public int Complexity { get; set; }
public double Complexity { get; set; }
}
public class CodeSummary
@@ -27,13 +27,10 @@ namespace CodeCoverageSummary
public int BranchesValid { get; set; }
public int Complexity { get; set; }
public double Complexity { get; set; }
public List<CodeCoverage> Packages { get; set; }
public CodeSummary()
{
Packages = new List<CodeCoverage>();
}
public CodeSummary() => Packages = new();
}
}
+33 -6
View File
@@ -1,19 +1,46 @@
using CommandLine;
using System;
using System.Collections.Generic;
namespace CodeCoverageSummary
{
public class CommandLineOptions
{
[Value(index: 0, Required = true, HelpText = "Code coverage file to analyse.")]
public string Filename { get; set; }
[Option(longName: "files", Separator = ',', Required = true, HelpText = "A comma separated list of code coverage files to analyse.")]
public IEnumerable<string> Files { get; set; }
[Option(shortName: 'b', longName: "badge", Required = false, HelpText = "Include a badge in the output - true / false.", Default = false)]
public bool Badge { get; set; }
[Option(longName: "badge", Required = false, HelpText = "Include a Line Rate coverage badge in the output using shields.io - true or false.", Default = "false")]
public string BadgeString { get; set; }
[Option(shortName: 'f', longName: "format", Required = false, HelpText = "Output Format - markdown or text.", Default = "text")]
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")]
public string Format { get; set; }
[Option(shortName: 'o', longName: "output", Required = false, HelpText = "Output Type - console, file or both.", Default = "console")]
[Option(longName: "hidebranch", Required = false, HelpText = "Hide Branch Rate values in the output - true or false.", Default = "false")]
public string HideBranchString { get; set; }
public bool HideBranchRate => HideBranchString.Equals("true", StringComparison.OrdinalIgnoreCase);
[Option(longName: "hidecomplexity", Required = false, HelpText = "Hide Complexity values in the output - true or false.", Default = "false")]
public string HideComplexityString { get; set; }
public bool HideComplexity => HideComplexityString.Equals("true", StringComparison.OrdinalIgnoreCase);
[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")]
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; }
}
}
@@ -0,0 +1,9 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[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)")]
+192 -99
View File
@@ -9,88 +9,113 @@ namespace CodeCoverageSummary
{
internal static class Program
{
// test file: /Dev/Csharp/CodeCoverageSummary/coverage.cobertura.xml
private static double lowerThreshold = 0.5;
private static double upperThreshold = 0.75;
private static int Main(string[] args)
{
return Parser.Default.ParseArguments<CommandLineOptions>(args)
.MapResult(o =>
{
try
{
if (!File.Exists(o.Filename))
{
Console.WriteLine("Error: Code coverage file not found.");
return -2; // error
}
.MapResult(o =>
{
try
{
// check files exist
foreach (var file in o.Files)
{
if (!File.Exists(file))
{
Console.WriteLine($"Error: Code coverage file not found - {file}.");
return -2; // error
}
}
// parse code coverage file
Console.WriteLine($"Code Coverage File: {o.Filename}");
CodeSummary summary = ParseTestResults(o.Filename);
if (summary == null)
{
Console.WriteLine("Error: Parsing code coverage file.");
return -2; // error
}
else
{
// generate badge
string badgeUrl = o.Badge ? GenerateBadge(summary) : null;
// parse code coverage file
CodeSummary summary = new();
foreach (var file in o.Files)
{
Console.WriteLine($"Code Coverage File: {file}");
summary = ParseTestResults(file, summary);
}
summary.LineRate /= o.Files.Count();
summary.BranchRate /= o.Files.Count();
// generate output
string output;
string fileExt;
if (o.Format.Equals("text", StringComparison.OrdinalIgnoreCase))
{
fileExt = "txt";
output = GenerateTextOutput(summary, badgeUrl);
}
else if (o.Format.Equals("md", StringComparison.OrdinalIgnoreCase) || o.Format.Equals("markdown", StringComparison.OrdinalIgnoreCase))
{
fileExt = "md";
output = GenerateMarkdownOutput(summary, badgeUrl);
}
else
{
Console.WriteLine("Error: Unknown output format.");
return -2; // error
}
if (summary.Packages.Count == 0)
{
Console.WriteLine("Error: Parsing code coverage file, no packages found.");
return -2; // error
}
else
{
// set health badge thresholds
if (!string.IsNullOrWhiteSpace(o.Thresholds))
SetThresholds(o.Thresholds);
// output
if (o.Output.Equals("console", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine(output);
}
else if (o.Output.Equals("file", StringComparison.OrdinalIgnoreCase))
{
File.WriteAllText($"code-coverage-results.{fileExt}", output);
}
else if (o.Output.Equals("both", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine(output);
File.WriteAllText($"code-coverage-results.{fileExt}", output);
}
else
{
Console.WriteLine("Error: Unknown output type.");
return -2; // error
}
// generate badge
string badgeUrl = o.Badge ? GenerateBadge(summary) : null;
return 0; // success
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.GetType()} - {ex.Message}");
return -3; // unhandled error
}
},
errs => -1); // invalid arguments
// generate output
string output;
string fileExt;
if (o.Format.Equals("text", StringComparison.OrdinalIgnoreCase))
{
fileExt = "txt";
output = GenerateTextOutput(summary, badgeUrl, o.Indicators, o.HideBranchRate, o.HideComplexity);
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))
{
fileExt = "md";
output = GenerateMarkdownOutput(summary, badgeUrl, o.Indicators, o.HideBranchRate, o.HideComplexity);
if (o.FailBelowThreshold)
output += $"{Environment.NewLine}_Minimum allowed line rate is `{lowerThreshold * 100:N0}%`_{Environment.NewLine}";
}
else
{
Console.WriteLine("Error: Unknown output format.");
return -2; // error
}
// output
if (o.Output.Equals("console", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine(output);
}
else if (o.Output.Equals("file", StringComparison.OrdinalIgnoreCase))
{
File.WriteAllText($"code-coverage-results.{fileExt}", output);
}
else if (o.Output.Equals("both", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine(output);
File.WriteAllText($"code-coverage-results.{fileExt}", output);
}
else
{
Console.WriteLine("Error: Unknown output type.");
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
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.GetType()} - {ex.Message}");
return -3; // unhandled error
}
},
_ => -1); // invalid arguments
}
private static CodeSummary ParseTestResults(string filename)
private static CodeSummary ParseTestResults(string filename, CodeSummary summary)
{
CodeSummary summary = new();
try
{
string rss = File.ReadAllText(filename);
@@ -103,50 +128,50 @@ namespace CodeCoverageSummary
var lineR = from item in coverage.Attributes()
where item.Name == "line-rate"
select item;
summary.LineRate = double.Parse(lineR.First().Value);
summary.LineRate += double.Parse(lineR.First().Value);
var linesCovered = from item in coverage.Attributes()
where item.Name == "lines-covered"
select item;
summary.LinesCovered = int.Parse(linesCovered.First().Value);
summary.LinesCovered += int.Parse(linesCovered.First().Value);
var linesValid = from item in coverage.Attributes()
where item.Name == "lines-valid"
select item;
summary.LinesValid = int.Parse(linesValid.First().Value);
summary.LinesValid += int.Parse(linesValid.First().Value);
var branchR = from item in coverage.Attributes()
where item.Name == "branch-rate"
select item;
summary.BranchRate = double.Parse(branchR.First().Value);
summary.BranchRate += double.Parse(branchR.First().Value);
var branchesCovered = from item in coverage.Attributes()
where item.Name == "branches-covered"
select item;
summary.BranchesCovered = int.Parse(branchesCovered.First().Value);
summary.BranchesCovered += int.Parse(branchesCovered.First().Value);
var branchesValid = from item in coverage.Attributes()
where item.Name == "branches-valid"
select item;
summary.BranchesValid = int.Parse(branchesValid.First().Value);
summary.Complexity = 0;
summary.BranchesValid += int.Parse(branchesValid.First().Value);
// test coverage for individual packages
var packages = from item in coverage.Descendants("package")
select item;
int i = 1;
foreach (var item in packages)
{
CodeCoverage packageCoverage = new()
{
Name = item.Attribute("name").Value,
LineRate = double.Parse(item.Attribute("line-rate").Value),
BranchRate = double.Parse(item.Attribute("branch-rate").Value),
Complexity = int.Parse(item.Attribute("complexity").Value)
Name = string.IsNullOrWhiteSpace(item.Attribute("name").Value) ? $"Package {i}" : item.Attribute("name").Value,
LineRate = double.Parse(item.Attribute("line-rate")?.Value ?? "0"),
BranchRate = double.Parse(item.Attribute("branch-rate")?.Value ?? "0"),
Complexity = double.Parse(item.Attribute("complexity")?.Value ?? "0")
};
summary.Packages.Add(packageCoverage);
summary.Complexity += packageCoverage.Complexity;
i++;
}
return summary;
@@ -158,14 +183,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)
{
string colour;
if (summary.LineRate < 0.5)
if (summary.LineRate < lowerThreshold)
{
colour = "critical";
}
else if (summary.LineRate < 0.75)
else if (summary.LineRate < upperThreshold)
{
colour = "yellow";
}
@@ -176,46 +236,79 @@ namespace CodeCoverageSummary
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, bool hideBranchRate, bool hideComplexity)
{
StringBuilder textOutput = new();
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}")
.AppendLine($"Complexity = {summary.Complexity}");
foreach (CodeCoverage package in summary.Packages)
{
textOutput.AppendLine($"{package.Name}: Line Rate = {package.LineRate * 100:N0}%, Branch Rate = {package.BranchRate * 100:N0}%, Complexity = {package.Complexity}");
textOutput.Append($"{package.Name}: Line Rate = {package.LineRate * 100:N0}%")
.Append(hideBranchRate ? string.Empty : $", Branch Rate = {package.BranchRate * 100:N0}%")
.Append(hideComplexity ? string.Empty : (package.Complexity % 1 == 0) ? $", Complexity = {package.Complexity}" : $", Complexity = {package.Complexity:N4}")
.AppendLine(indicators ? $", {GenerateHealthIndicator(package.LineRate)}" : string.Empty);
}
textOutput.Append($"Summary: Line Rate = {summary.LineRate * 100:N0}% ({summary.LinesCovered} / {summary.LinesValid})")
.Append(hideBranchRate ? string.Empty : $", Branch Rate = {summary.BranchRate * 100:N0}% ({summary.BranchesCovered} / {summary.BranchesValid})")
.Append(hideComplexity ? string.Empty : (summary.Complexity % 1 == 0) ? $", Complexity = {summary.Complexity}" : $", Complexity = {summary.Complexity:N4}")
.AppendLine(indicators ? $", {GenerateHealthIndicator(summary.LineRate)}" : string.Empty);
return textOutput.ToString();
}
private static string GenerateMarkdownOutput(CodeSummary summary, string badgeUrl)
private static string GenerateMarkdownOutput(CodeSummary summary, string badgeUrl, bool indicators, bool hideBranchRate, bool hideComplexity)
{
StringBuilder markdownOutput = new();
if (!string.IsNullOrWhiteSpace(badgeUrl))
{
markdownOutput.AppendLine($"![Code Coverage]({badgeUrl})");
markdownOutput.AppendLine($"![Code Coverage]({badgeUrl})")
.AppendLine();
}
markdownOutput.AppendLine("Package | Line Rate | Branch Rate | Complexity")
.AppendLine("-------- | --------- | ----------- | ----------");
markdownOutput.Append("Package | Line Rate")
.Append(hideBranchRate ? string.Empty : " | Branch Rate")
.Append(hideComplexity ? string.Empty : " | Complexity")
.AppendLine(indicators ? " | Health" : string.Empty)
.Append("-------- | ---------")
.Append(hideBranchRate ? string.Empty : " | -----------")
.Append(hideComplexity ? string.Empty : " | ----------")
.AppendLine(indicators ? " | ------" : string.Empty);
foreach (CodeCoverage package in summary.Packages)
{
markdownOutput.AppendLine($"{package.Name} | {package.LineRate * 100:N0}% | {package.BranchRate * 100:N0}% | {package.Complexity}");
markdownOutput.Append($"{package.Name} | {package.LineRate * 100:N0}%")
.Append(hideBranchRate ? string.Empty : $" | {package.BranchRate * 100:N0}%")
.Append(hideComplexity ? string.Empty : (package.Complexity % 1 == 0) ? $" | {package.Complexity}" : $" | {package.Complexity:N4}" )
.AppendLine(indicators ? $" | {GenerateHealthIndicator(package.LineRate)}" : string.Empty);
}
markdownOutput.Append($"**Summary** | **{summary.LineRate * 100:N0}%** ({summary.LinesCovered} / {summary.LinesValid}) | ")
.AppendLine($"**{summary.BranchRate * 100:N0}%** ({summary.BranchesCovered} / {summary.BranchesValid}) | {summary.Complexity}");
markdownOutput.Append($"**Summary** | **{summary.LineRate * 100:N0}%** ({summary.LinesCovered} / {summary.LinesValid})")
.Append(hideBranchRate ? string.Empty : $" | **{summary.BranchRate * 100:N0}%** ({summary.BranchesCovered} / {summary.BranchesValid})")
.Append(hideComplexity ? string.Empty : (summary.Complexity % 1 == 0) ? $" | **{summary.Complexity}**" : $" | **{summary.Complexity:N4}**")
.AppendLine(indicators ? $" | {GenerateHealthIndicator(summary.LineRate)}" : string.Empty);
return markdownOutput.ToString();
}
@@ -1,11 +1,12 @@
{
"profiles": {
"CodeCoverageSummary": {
"commandName": "Project"
"commandName": "Project",
"commandLineArgs": "--files ../../../../coverage.cobertura.xml,../../../../coverage.cobertura.xml --format=text --badge true --thresholds=\"85 90\" --fail true"
},
"Docker": {
"commandName": "Docker",
"commandLineArgs": "/src/coverage.cobertura.xml --format=md --badge=true"
"commandLineArgs": "--files /app/sample.coverage.xml --format=text --badge=true"
}
}
}
+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>
File diff suppressed because it is too large Load Diff