Skip to content

Commit 3273efd

Browse files
committed
tests
1 parent f8f3347 commit 3273efd

8 files changed

Lines changed: 279 additions & 65 deletions

File tree

.github/workflows/dotnet.yml

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ permissions:
1313
env:
1414
DOTNET_VERSION: 10.0.x
1515
SOLUTION: ManagedCode.FeatureChecker.slnx
16+
COVERAGE_THRESHOLD: 85
17+
COVERAGE_FILE: ManagedCode.FeatureChecker.Tests/bin/Release/net10.0/TestResults/coverage.cobertura.xml
1618

1719
jobs:
1820
verify:
@@ -41,8 +43,36 @@ jobs:
4143
- name: Test
4244
run: dotnet test --solution ${{ env.SOLUTION }} --configuration Release --no-restore --verbosity normal
4345

46+
- name: Integration tests
47+
run: dotnet test --solution ${{ env.SOLUTION }} --configuration Release --no-build --verbosity normal -- --treenode-filter "/*/*/*/*[Category=Integration]"
48+
4449
- name: Coverage
45-
run: dotnet test --solution ${{ env.SOLUTION }} --configuration Release --no-build --verbosity normal -- --coverage --coverage-output-format cobertura
50+
run: dotnet test --solution ${{ env.SOLUTION }} --configuration Release --no-build --verbosity normal -- --coverage --coverage-output coverage.cobertura.xml --coverage-output-format cobertura
51+
52+
- name: Enforce coverage threshold
53+
shell: bash
54+
run: |
55+
if [[ ! -f "${COVERAGE_FILE}" ]]; then
56+
echo "::error::Coverage file not found: ${COVERAGE_FILE}"
57+
exit 1
58+
fi
59+
60+
line_rate="$(sed -n 's/.*line-rate="\([^"]*\)".*/\1/p' "${COVERAGE_FILE}" | head -n 1)"
61+
62+
if [[ -z "${line_rate}" ]]; then
63+
echo "::error::Could not read line-rate from ${COVERAGE_FILE}"
64+
exit 1
65+
fi
66+
67+
awk -v rate="${line_rate}" -v threshold="${COVERAGE_THRESHOLD}" 'BEGIN {
68+
coverage = rate * 100
69+
printf("Line coverage: %.2f%%\n", coverage)
70+
71+
if (coverage + 0.000001 < threshold) {
72+
printf("Line coverage %.2f%% is below required %.2f%%\n", coverage, threshold)
73+
exit 1
74+
}
75+
}'
4676
4777
- name: Upload test artifacts
4878
uses: actions/upload-artifact@v4

.github/workflows/release.yml

Lines changed: 173 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,63 +2,49 @@ name: Release
22

33
on:
44
push:
5-
tags:
6-
- 'v*'
5+
branches: [ main ]
76
workflow_dispatch:
8-
inputs:
9-
version:
10-
description: Version from Directory.Build.props, for example 10.0.0
11-
required: true
12-
type: string
137

148
permissions:
15-
contents: write
9+
contents: read
1610

1711
env:
1812
DOTNET_VERSION: 10.0.x
1913
SOLUTION: ManagedCode.FeatureChecker.slnx
2014
PROJECT: ManagedCode.FeatureChecker/ManagedCode.FeatureChecker.csproj
15+
COVERAGE_THRESHOLD: 85
16+
COVERAGE_FILE: ManagedCode.FeatureChecker.Tests/bin/Release/net10.0/TestResults/coverage.cobertura.xml
2117

2218
jobs:
23-
release:
19+
build:
20+
name: Build, Test, and Pack
2421
runs-on: ubuntu-latest
2522

23+
outputs:
24+
version: ${{ steps.version.outputs.version }}
25+
2626
steps:
27-
- uses: actions/checkout@v6
28-
with:
29-
fetch-depth: 0
27+
- name: Checkout
28+
uses: actions/checkout@v6
3029

3130
- name: Setup .NET
3231
uses: actions/setup-dotnet@v5
3332
with:
3433
dotnet-version: ${{ env.DOTNET_VERSION }}
3534

36-
- name: Resolve release version
35+
- name: Resolve package version
3736
id: version
3837
shell: bash
39-
env:
40-
DISPATCH_VERSION: ${{ inputs.version }}
4138
run: |
42-
props_version="$(grep -oPm1 '(?<=<Version>)[^<]+' Directory.Build.props)"
43-
44-
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
45-
version="${GITHUB_REF_NAME#v}"
46-
else
47-
version="${DISPATCH_VERSION}"
48-
fi
39+
version="$(grep -oPm1 '(?<=<Version>)[^<]+' Directory.Build.props)"
4940
5041
if [[ -z "${version}" ]]; then
51-
echo "::error::Release version is required."
52-
exit 1
53-
fi
54-
55-
if [[ "${version}" != "${props_version}" ]]; then
56-
echo "::error::Release version ${version} does not match Directory.Build.props version ${props_version}."
42+
echo "::error::Directory.Build.props must define <Version>."
5743
exit 1
5844
fi
5945
6046
echo "version=${version}" >> "${GITHUB_OUTPUT}"
61-
echo "tag=v${version}" >> "${GITHUB_OUTPUT}"
47+
echo "Package version: ${version}"
6248
6349
- name: Restore dependencies
6450
run: dotnet restore ${{ env.SOLUTION }}
@@ -73,54 +59,86 @@ jobs:
7359
run: dotnet build ${{ env.SOLUTION }} --configuration Release --no-restore -p:RunAnalyzers=true
7460

7561
- name: Test
76-
run: dotnet test --solution ${{ env.SOLUTION }} --configuration Release --no-restore --verbosity normal
62+
run: dotnet test --solution ${{ env.SOLUTION }} --configuration Release --no-build --verbosity normal
7763

78-
- name: Coverage
79-
run: dotnet test --solution ${{ env.SOLUTION }} --configuration Release --no-build --verbosity normal -- --coverage --coverage-output-format cobertura
64+
- name: Integration tests
65+
run: dotnet test --solution ${{ env.SOLUTION }} --configuration Release --no-build --verbosity normal -- --treenode-filter "/*/*/*/*[Category=Integration]"
8066

81-
- name: Pack
82-
run: dotnet pack ${{ env.PROJECT }} --configuration Release --no-build --output artifacts
67+
- name: Coverage
68+
run: dotnet test --solution ${{ env.SOLUTION }} --configuration Release --no-build --verbosity normal -- --coverage --coverage-output coverage.cobertura.xml --coverage-output-format cobertura
8369

84-
- name: Ensure release tag exists
70+
- name: Enforce coverage threshold
8571
shell: bash
8672
run: |
87-
tag="${{ steps.version.outputs.tag }}"
88-
89-
if git rev-parse "${tag}" >/dev/null 2>&1; then
90-
echo "Tag ${tag} already exists."
91-
exit 0
73+
if [[ ! -f "${COVERAGE_FILE}" ]]; then
74+
echo "::error::Coverage file not found: ${COVERAGE_FILE}"
75+
exit 1
9276
fi
9377
94-
git config user.name "github-actions[bot]"
95-
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
96-
git tag -a "${tag}" -m "Release ${tag}"
97-
git push origin "${tag}"
78+
line_rate="$(sed -n 's/.*line-rate="\([^"]*\)".*/\1/p' "${COVERAGE_FILE}" | head -n 1)"
9879
99-
- name: Generate release notes
100-
shell: bash
101-
run: |
102-
tag="${{ steps.version.outputs.tag }}"
103-
previous_tag="$(git describe --tags --abbrev=0 "${tag}^" 2>/dev/null || true)"
80+
if [[ -z "${line_rate}" ]]; then
81+
echo "::error::Could not read line-rate from ${COVERAGE_FILE}"
82+
exit 1
83+
fi
10484
105-
{
106-
echo "## ${tag}"
107-
echo
85+
awk -v rate="${line_rate}" -v threshold="${COVERAGE_THRESHOLD}" 'BEGIN {
86+
coverage = rate * 100
87+
printf("Line coverage: %.2f%%\n", coverage)
10888
109-
if [[ -n "${previous_tag}" ]]; then
110-
git log --pretty=format:'- %s (%h)' "${previous_tag}..HEAD"
111-
else
112-
git log --pretty=format:'- %s (%h)'
113-
fi
114-
} > release-notes.md
89+
if (coverage + 0.000001 < threshold) {
90+
printf("Line coverage %.2f%% is below required %.2f%%\n", coverage, threshold)
91+
exit 1
92+
}
93+
}'
94+
95+
- name: Pack
96+
run: dotnet pack ${{ env.PROJECT }} --configuration Release --no-build --output artifacts
11597

11698
- name: Upload package artifacts
11799
uses: actions/upload-artifact@v4
118100
with:
119101
name: nuget-packages
120-
path: artifacts/*
102+
path: |
103+
artifacts/*.nupkg
104+
artifacts/*.snupkg
121105
if-no-files-found: error
106+
retention-days: 5
107+
108+
- name: Upload test artifacts
109+
uses: actions/upload-artifact@v4
110+
if: always()
111+
with:
112+
name: test-results
113+
path: |
114+
**/TestResults/**
115+
if-no-files-found: ignore
116+
retention-days: 5
117+
118+
publish-nuget:
119+
name: Publish to NuGet
120+
needs: build
121+
runs-on: ubuntu-latest
122+
if: github.ref == 'refs/heads/main'
123+
124+
outputs:
125+
published: ${{ steps.publish.outputs.published }}
126+
version: ${{ needs.build.outputs.version }}
127+
128+
steps:
129+
- name: Download package artifacts
130+
uses: actions/download-artifact@v5
131+
with:
132+
name: nuget-packages
133+
path: artifacts
134+
135+
- name: Setup .NET
136+
uses: actions/setup-dotnet@v5
137+
with:
138+
dotnet-version: ${{ env.DOTNET_VERSION }}
122139

123140
- name: Publish NuGet packages
141+
id: publish
124142
shell: bash
125143
env:
126144
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
@@ -134,18 +152,109 @@ jobs:
134152
packages=(artifacts/*.nupkg)
135153
136154
if [[ "${#packages[@]}" -eq 0 ]]; then
137-
echo "::error::No NuGet packages were produced."
155+
echo "::error::No NuGet packages were downloaded."
138156
exit 1
139157
fi
140158
159+
published=false
160+
141161
for package in "${packages[@]}"; do
142-
dotnet nuget push "${package}" --api-key "${NUGET_API_KEY}" --source https://api.nuget.org/v3/index.json --skip-duplicate
162+
echo "Publishing ${package}..."
163+
output="$(dotnet nuget push "${package}" --api-key "${NUGET_API_KEY}" --source https://api.nuget.org/v3/index.json --skip-duplicate 2>&1)"
164+
exit_code=$?
165+
echo "${output}"
166+
167+
if [[ "${exit_code}" -ne 0 ]]; then
168+
echo "::error::Failed to publish ${package}."
169+
exit "${exit_code}"
170+
fi
171+
172+
if echo "${output}" | grep -Eiq 'your package was pushed|successfully published'; then
173+
published=true
174+
fi
143175
done
144176
177+
echo "published=${published}" >> "${GITHUB_OUTPUT}"
178+
179+
create-release:
180+
name: Create GitHub Release and Tag
181+
needs: publish-nuget
182+
runs-on: ubuntu-latest
183+
if: needs.publish-nuget.outputs.published == 'true'
184+
185+
permissions:
186+
contents: write
187+
188+
steps:
189+
- name: Checkout
190+
uses: actions/checkout@v6
191+
with:
192+
fetch-depth: 0
193+
194+
- name: Download package artifacts
195+
uses: actions/download-artifact@v5
196+
with:
197+
name: nuget-packages
198+
path: artifacts
199+
200+
- name: Create and push tag
201+
shell: bash
202+
run: |
203+
version="${{ needs.publish-nuget.outputs.version }}"
204+
tag="v${version}"
205+
206+
git config user.name "github-actions[bot]"
207+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
208+
209+
if git rev-parse "${tag}" >/dev/null 2>&1; then
210+
echo "Tag ${tag} already exists."
211+
exit 0
212+
fi
213+
214+
git tag -a "${tag}" -m "Release ${tag}"
215+
git push origin "${tag}"
216+
217+
- name: Generate release notes
218+
shell: bash
219+
run: |
220+
version="${{ needs.publish-nuget.outputs.version }}"
221+
tag="v${version}"
222+
previous_tag="$(git tag --sort=-version:refname | grep -v "^${tag}$" | head -n 1 || true)"
223+
224+
{
225+
echo "# Release ${version}"
226+
echo
227+
echo "Released on $(date +'%Y-%m-%d')"
228+
echo
229+
230+
if [[ -n "${previous_tag}" ]]; then
231+
echo "## Changes since ${previous_tag}"
232+
echo
233+
git log --pretty=format:'- %s (%h)' "${previous_tag}..HEAD"
234+
else
235+
echo "## Initial release"
236+
echo
237+
git log --pretty=format:'- %s (%h)' --max-count=20
238+
fi
239+
240+
echo
241+
echo
242+
echo "## NuGet Packages"
243+
echo
244+
245+
for package in artifacts/*.nupkg; do
246+
package_name="$(basename "${package}" .nupkg)"
247+
package_id="${package_name%.${version}}"
248+
echo "- [${package_id} ${version}](https://www.nuget.org/packages/${package_id}/${version})"
249+
done
250+
} > release-notes.md
251+
145252
- name: Create GitHub Release
146253
uses: softprops/action-gh-release@v2
147254
with:
148-
tag_name: ${{ steps.version.outputs.tag }}
149-
name: ${{ steps.version.outputs.tag }}
255+
tag_name: v${{ needs.publish-nuget.outputs.version }}
256+
name: v${{ needs.publish-nuget.outputs.version }}
150257
body_path: release-notes.md
151-
files: artifacts/*
258+
files: |
259+
artifacts/*.nupkg
260+
artifacts/*.snupkg

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ Local `AGENTS.md` files may tighten these values. They must not loosen them with
122122
- Prefer testing caller-visible behaviour through public contracts over implementation details.
123123
- Flaky tests are failures. Fix the cause.
124124
- Production code changes should preserve or improve coverage. Public contracts and critical flows require explicit success and failure assertions.
125+
- CI and release gates must fail when line coverage is below 85%.
126+
- Integration behaviour must be covered by TUnit tests marked with `[Category("Integration")]` and run as an explicit CI/release pass.
125127
- Do not skip tests to make a branch green.
126128

127129
## Code and Design

ManagedCode.FeatureChecker.Tests/AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Parent: `../AGENTS.md`
1313
- `Access/FeatureAccessTests.cs` - factory, builder-interface, scope, and file-backed freshness tests.
1414
- `DependencyInjection/FeatureCheckerDependencyInjectionTests.cs` - Microsoft.Extensions configuration and service registration tests.
1515
- `Evaluation/FeatureCheckerEvaluationTests.cs` - evaluator, dependencies, variants, typed values, and evaluation collection tests.
16+
- `Integration/FeatureCheckerIntegrationTests.cs` - end-to-end public API tests across storage, dependency injection, factories, scopes, and evaluation.
1617
- `Segments/FeatureSegmentTests.cs` - list and rule-based segment tests.
1718
- `Storage/FeatureStorageTests.cs` - snapshot serialization and file-backed provider tests.
1819
- `Targeting/FeatureConditionOperatorTests.cs` - positive, negative, missing-attribute, and version condition operator tests.
@@ -68,3 +69,4 @@ Parent: `../AGENTS.md`
6869
- Prefer meaningful assertions over count-only tests.
6970
- Cover unknown features, all feature states, status filtering, targeting rules, scope helpers, and typed default-value semantics when feature semantics change.
7071
- Do not weaken assertions to fit an implementation change.
72+
- Integration tests must use TUnit `[Category("Integration")]` and remain runnable through the repo's integration filter in CI.

ManagedCode.FeatureChecker.Tests/GlobalUsings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
global using ManagedCode.FeatureChecker.Segments;
66
global using ManagedCode.FeatureChecker.Storage;
77
global using ManagedCode.FeatureChecker.Targeting;
8+
global using ManagedCode.FeatureChecker.Tests;
89
global using Shouldly;
910
global using FeatureCheckerEvaluator = ManagedCode.FeatureChecker.Evaluation.FeatureChecker;

0 commit comments

Comments
 (0)