Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
root = true

[*]
indent_style = space
indent_size = 4
trim_trailing_whitespace = true

[*.{csproj,props,targets,slnx,json,yml,yaml}]
indent_size = 2

[*.md]
trim_trailing_whitespace = false

[*.cs]
csharp_style_namespace_declarations = file_scoped:warning
csharp_using_directive_placement = outside_namespace:suggestion
17 changes: 17 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: 2
updates:
- package-ecosystem: nuget
directory: /
schedule:
interval: weekly
groups:
test-tooling:
patterns:
- "xunit*"
- "Microsoft.NET.Test.Sdk"
- "coverlet.*"

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
86 changes: 77 additions & 9 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,94 @@
name: .NET 6.0
name: Build and Test

on:
pull_request:
push:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

defaults:
run:
shell: bash

jobs:
build-and-test:
runs-on: ubuntu-20.04
test:
name: test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]

steps:
- name: Checkout Code
uses: actions/checkout@v2
uses: actions/checkout@v7
with:
# MinVer derives the version from git tags, so the full history is required
fetch-depth: 0

- name: Setup .NET 6
uses: actions/setup-dotnet@v1
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: 6.0.x
# net8.0 is needed at runtime for the tests, the SDK is pinned by global.json
dotnet-version: |
8.0.x
10.0.x
- name: .NET info
run: dotnet --info
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore
run: dotnet build -c Release --no-restore

# The tests run against net10.0 and net8.0 (see Core.Test.csproj)
- name: Test
run: dotnet test --no-build --verbosity normal
run: >
dotnet test -c Release --no-build --verbosity normal
--filter "Category!=Network"
--collect "XPlat Code Coverage" --results-directory TestResults

# Tests that talk to external services must not break the build when such a service is down.
- name: Test (network, informational)
continue-on-error: true
run: dotnet test -c Release --no-build --verbosity normal --filter "Category=Network"

- name: Upload coverage
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v7
with:
name: coverage
path: TestResults/**/coverage.cobertura.xml
retention-days: 14

pack:
name: pack
needs: test
runs-on: ubuntu-latest

steps:
- name: Checkout Code
uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.x

# Validates the package metadata on every change, not only at release time
- name: Create NuGet Package
run: dotnet pack Core/Core.csproj -c Release --output artifacts

- name: Upload package
uses: actions/upload-artifact@v7
with:
name: nuget-packages
path: artifacts/*
retention-days: 14
48 changes: 41 additions & 7 deletions .github/workflows/nuget_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,57 @@ on:
types:
- published

permissions:
contents: read

jobs:
publish:
name: build, test, pack & publish
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # required for NuGet trusted publishing (OIDC)
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v7
with:
# MinVer derives the package version from the release tag, so the full history is required
fetch-depth: 0

- name: Setup .NET 6
uses: actions/setup-dotnet@v1
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: 6.0.x
dotnet-version: |
8.0.x
10.0.x

- name: Build / Test
run: dotnet test -c Release --verbosity quiet
run: dotnet test -c Release --verbosity quiet --filter "Category!=Network"

- name: Create NuGet Package
run: cd ./Core && dotnet pack -c Release
run: dotnet pack Core/Core.csproj -c Release --no-build --output artifacts

# Guards against a release tag that does not produce the version it announces
- name: Verify package version matches the release tag
env:
TAG: ${{ github.event.release.tag_name }}
run: |
expected="Cap.Core.${TAG#v}.nupkg"
if [ ! -f "artifacts/$expected" ]; then
echo "::error::Expected artifacts/$expected for release tag '$TAG', but found:"
ls artifacts
exit 1
fi

# Short-lived API key via trusted publishing. Requires a trusted publishing policy for this
# repository and workflow on nuget.org. Until it exists, the NUGET_API_KEY secret is used instead.
- name: NuGet login (trusted publishing)
id: login
continue-on-error: true
uses: NuGet/login@v1
with:
user: Capjan

- name: Publishing to NuGet
run: dotnet nuget push ./Core/bin/Release/*.nupkg --api-key ${{secrets.NUGET_API_KEY}} -s https://api.nuget.org/v3/index.json --skip-duplicate
env:
NUGET_API_KEY: ${{ steps.login.outputs.NUGET_API_KEY || secrets.NUGET_API_KEY }}
run: dotnet nuget push "artifacts/*.nupkg" --api-key "$NUGET_API_KEY" -s https://api.nuget.org/v3/index.json --skip-duplicate
17 changes: 10 additions & 7 deletions Core.Test/Core.Test.csproj
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net10.0;net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>10</LangVersion>
<Company>Jan Ruhlaender</Company>
<Authors>Jan Ruhlaender</Authors>
<Product>CoreLib Unit Tests</Product>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Nullable>enable</Nullable>
<TargetFramework>net6.0</TargetFramework>
<!-- Test code follows different conventions (e.g. underscores in test names). -->
<AnalysisMode>Default</AnalysisMode>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
93 changes: 93 additions & 0 deletions Core.Test/CultureIndependenceTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Core.Converters.Basic;
using Core.Extensions.TextRelated;
using Core.Net.Impl;
using Xunit;

namespace Core.Test;

/// <summary>
/// Machine-readable values (URLs, hex, HTTP headers, ...) must not depend on the culture of the current thread.
/// </summary>
public class CultureIndependenceTest
{
public static IEnumerable<object[]> Cultures => new[]
{
new object[] { "de-DE" },
new object[] { "tr-TR" },
new object[] { "sv-SE" },
new object[] { "ar-SA" },
};

private static void RunInCulture(string cultureName, Action action)
{
var previous = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = new CultureInfo(cultureName);
action();
}
finally
{
CultureInfo.CurrentCulture = previous;
}
}

[Theory]
[MemberData(nameof(Cultures))]
public void UrlBuilderWritesPort(string culture)
{
RunInCulture(culture, () =>
Assert.Equal("https://example.com:8080", new DefaultUrlBuilder("https://example.com").Port(8080).Build()));
}

[Theory]
[MemberData(nameof(Cultures))]
public void HexStrings(string culture)
{
RunInCulture(culture, () =>
{
Assert.Equal("FF", 255.ToHexString());
Assert.Equal("ff", 255.ToHexString(upperCase: false));
Assert.Equal("0AFF", new byte[] { 0x0A, 0xFF }.ToHexString());
});
}

[Theory]
[MemberData(nameof(Cultures))]
public void IntegerConverterParsesNegativeNumbers(string culture)
{
RunInCulture(culture, () => Assert.Equal(-1234, new IntegerConverter().Convert("-1 234")));
}

[Theory]
[MemberData(nameof(Cultures))]
public void HttpHeaderContentLength(string culture)
{
RunInCulture(culture, () =>
{
Assert.Equal(1234, new HttpHeader("text/plain", 1234).ContentLength);
var raw = new Dictionary<string, string> { { HttpHeader.ContentLengthKey, "1234" } };
Assert.Equal(1234, new HttpHeader(raw).ContentLength);
});
}

[Theory]
[MemberData(nameof(Cultures))]
public void HttpHeaderDatesAreAlwaysEnglish(string culture)
{
RunInCulture(culture, () =>
{
var raw = new Dictionary<string, string>
{
{ HttpHeader.DateKey, "Tue, 15 Nov 1994 08:12:31 GMT" },
{ HttpHeader.LastModifiedKey, "Wed, 21 Oct 2015 07:28:00 GMT" },
};
var header = new HttpHeader(raw);
Assert.Equal(new DateTime(1994, 11, 15, 8, 12, 31, DateTimeKind.Utc), header.CreatedAtUtc);
Assert.Equal(new DateTime(2015, 10, 21, 7, 28, 0, DateTimeKind.Utc), header.LastModifiedUtc);
});
}
}
20 changes: 18 additions & 2 deletions Core.Test/LoggingRelated/DumpLogTarget.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Core.Logging;
using Core.Logging.Targets;

namespace Core.Test.LoggingRelated;

/// <summary>
/// Collects log events in memory.
/// </summary>
/// <remarks>
/// The log event is process-wide (static), so events of tests running in parallel end up in every connected target.
/// Pass <c>onlyFromClass</c> to collect only the events of the class under test.
/// </remarks>
internal class DumpLogTarget : LogTarget
{
private readonly string? _onlyFromClass;

public DumpLogTarget(string? onlyFromClass = null)
{
_onlyFromClass = onlyFromClass;
}

/// <inheritdoc />
protected override void OnLog(LogEventArgs itm)
{
if (_onlyFromClass != null && itm.CallerClassFullName != _onlyFromClass)
return;
EventLog.Add(itm);
}

public List<LogEventArgs> EventLog { get; } = new List<LogEventArgs>();
}
}
4 changes: 2 additions & 2 deletions Core.Test/LoggingRelated/UnitTest1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class TestLog
[Fact]
public void BasicTest()
{
using (var dumper = new DumpLogTarget {Connected = true})
using (var dumper = new DumpLogTarget(typeof(TestLog).FullName) {Connected = true})
{
var log = Logger.Create<TestLog>();
Assert.Empty(dumper.EventLog);
Expand All @@ -31,7 +31,7 @@ public void BasicTest()
[Fact]
public void MaskTest()
{
using (var dumper = new DumpLogTarget {Connected = true, LogMask = LogLevel.ProductionMask})
using (var dumper = new DumpLogTarget(typeof(TestLog).FullName) {Connected = true, LogMask = LogLevel.ProductionMask})
{
var log = Logger.Create<TestLog>();
log.Trace("Hello Trace");
Expand Down
3 changes: 2 additions & 1 deletion Core.Test/NetRelated/DownloaderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Core.Test.NetRelated;

[Trait("Category", "Network")]
public class DownloaderTest
{
[Fact]
Expand All @@ -14,7 +15,7 @@ public void BasicTest()
var downloader = new DefaultDownloader();
if (!downloader.TryDownloadToString(url, out var result))
{
Assert.True(false, $"download failed. url: {url}");
Assert.Fail($"download failed. url: {url}");
}
Assert.Contains("Example Domain", result);
}
Expand Down
Loading
Loading