From 448183b5e8b83beca482517af1b59e7aaa849c84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 5 Aug 2023 16:14:36 +0200 Subject: [PATCH 1/4] Add initial draft for FluentAssertions extensions --- Docs/FluentAssertions.md | 8 ++ .../DirectoryInfoAssertions.cs | 37 ++++++++ .../FileInfoAssertions.cs | 48 +++++++++++ .../FileSystemAssertions.cs | 58 +++++++++++++ .../FileSystemExtensions.cs | 16 ++++ ...tably.Abstractions.FluentAssertions.csproj | 23 +++++ .../Usings.cs | 4 + Testably.Abstractions.sln | 16 +++- .../DirectoryInfoAssertionsTests.cs | 42 ++++++++++ .../FileInfoAssertionsTests.cs | 73 ++++++++++++++++ .../FileSystemAssertionsTests.cs | 84 +++++++++++++++++++ ...Abstractions.FluentAssertions.Tests.csproj | 13 +++ 12 files changed, 421 insertions(+), 1 deletion(-) create mode 100644 Docs/FluentAssertions.md create mode 100644 Source/Testably.Abstractions.FluentAssertions/DirectoryInfoAssertions.cs create mode 100644 Source/Testably.Abstractions.FluentAssertions/FileInfoAssertions.cs create mode 100644 Source/Testably.Abstractions.FluentAssertions/FileSystemAssertions.cs create mode 100644 Source/Testably.Abstractions.FluentAssertions/FileSystemExtensions.cs create mode 100644 Source/Testably.Abstractions.FluentAssertions/Testably.Abstractions.FluentAssertions.csproj create mode 100644 Source/Testably.Abstractions.FluentAssertions/Usings.cs create mode 100644 Tests/Testably.Abstractions.FluentAssertions.Tests/DirectoryInfoAssertionsTests.cs create mode 100644 Tests/Testably.Abstractions.FluentAssertions.Tests/FileInfoAssertionsTests.cs create mode 100644 Tests/Testably.Abstractions.FluentAssertions.Tests/FileSystemAssertionsTests.cs create mode 100644 Tests/Testably.Abstractions.FluentAssertions.Tests/Testably.Abstractions.FluentAssertions.Tests.csproj diff --git a/Docs/FluentAssertions.md b/Docs/FluentAssertions.md new file mode 100644 index 000000000..ca5d24c0c --- /dev/null +++ b/Docs/FluentAssertions.md @@ -0,0 +1,8 @@ +![Testably.Abstractions](https://raw.githubusercontent.com/Testably/Testably.Abstractions/main/Docs/Images/social-preview.png) +[![Nuget](https://img.shields.io/nuget/v/Testably.Abstractions.Compression)](https://www.nuget.org/packages/Testably.Abstractions.Compression) +[![Build](https://github.com/Testably/Testably.Abstractions/actions/workflows/build.yml/badge.svg)](https://github.com/Testably/Testably.Abstractions/actions/workflows/build.yml) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Testably_Testably.Abstractions&branch=main&metric=alert_status)](https://sonarcloud.io/summary/overall?id=Testably_Testably.Abstractions&branch=main) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Testably_Testably.Abstractions&branch=main&metric=coverage)](https://sonarcloud.io/summary/overall?id=Testably_Testably.Abstractions&branch=main) +[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2FTestably%2FTestably.Abstractions%2Fmain)](https://dashboard.stryker-mutator.io/reports/github.com/Testably/Testably.Abstractions/main) + +FluentAssertions extensions for [Testably.Abstractions](../README.md). \ No newline at end of file diff --git a/Source/Testably.Abstractions.FluentAssertions/DirectoryInfoAssertions.cs b/Source/Testably.Abstractions.FluentAssertions/DirectoryInfoAssertions.cs new file mode 100644 index 000000000..0b0c48c32 --- /dev/null +++ b/Source/Testably.Abstractions.FluentAssertions/DirectoryInfoAssertions.cs @@ -0,0 +1,37 @@ +namespace Testably.Abstractions.FluentAssertions; + +/// +/// Assertions on . +/// +public class DirectoryInfoAssertions : + ReferenceTypeAssertions +{ + /// + protected override string Identifier => "directoryinfo"; + + internal DirectoryInfoAssertions(IDirectoryInfo instance) + : base(instance) + { + } + + /// + /// Asserts that the current directory has at least one file which matches the . + /// + public AndConstraint HaveFile( + string searchPattern = "*", string because = "", params object[] becauseArgs) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(!string.IsNullOrEmpty(searchPattern)) + .FailWith( + "You can't assert a file exist in the directory if you don't pass a proper name") + .Then + .Given(() => Subject.GetFiles(searchPattern)) + .ForCondition(fileInfos => fileInfos.Length > 0) + .FailWith( + "Expected {context:directoryinfo} to contain at least one file matching {0}{reason}, but none was found.", + _ => searchPattern); + + return new AndConstraint(this); + } +} diff --git a/Source/Testably.Abstractions.FluentAssertions/FileInfoAssertions.cs b/Source/Testably.Abstractions.FluentAssertions/FileInfoAssertions.cs new file mode 100644 index 000000000..e9f9de564 --- /dev/null +++ b/Source/Testably.Abstractions.FluentAssertions/FileInfoAssertions.cs @@ -0,0 +1,48 @@ +namespace Testably.Abstractions.FluentAssertions; + +/// +/// Assertions on . +/// +public class FileInfoAssertions : + ReferenceTypeAssertions +{ + /// + protected override string Identifier => "fileinfo"; + + internal FileInfoAssertions(IFileInfo instance) + : base(instance) + { + } + + /// + /// Asserts that the current file is not read-only. + /// + public AndConstraint IsNotReadOnly( + string because = "", params object[] becauseArgs) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .Given(() => Subject) + .ForCondition(fileInfo => !fileInfo.IsReadOnly) + .FailWith( + "Expected {context:fileinfo} not to be read-only {reason}, but it was."); + + return new AndConstraint(this); + } + + /// + /// Asserts that the current file is read-only. + /// + public AndConstraint IsReadOnly( + string because = "", params object[] becauseArgs) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .Given(() => Subject) + .ForCondition(fileInfo => fileInfo.IsReadOnly) + .FailWith( + "Expected {context:fileinfo} to be read-only {reason}, but it was not."); + + return new AndConstraint(this); + } +} diff --git a/Source/Testably.Abstractions.FluentAssertions/FileSystemAssertions.cs b/Source/Testably.Abstractions.FluentAssertions/FileSystemAssertions.cs new file mode 100644 index 000000000..9234ff0e8 --- /dev/null +++ b/Source/Testably.Abstractions.FluentAssertions/FileSystemAssertions.cs @@ -0,0 +1,58 @@ +namespace Testably.Abstractions.FluentAssertions; + +/// +/// Assertions on . +/// +public class FileSystemAssertions : + ReferenceTypeAssertions +{ + /// + protected override string Identifier => "filesystem"; + + internal FileSystemAssertions(IFileSystem instance) + : base(instance) + { + } + + /// + /// Asserts that a directory at exists in the file system. + /// + public AndWhichConstraint HaveDirectory( + string path, string because = "", params object[] becauseArgs) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(!string.IsNullOrEmpty(path)) + .FailWith("You can't assert a directory exist if you don't pass a proper name") + .Then + .Given(() => Subject.DirectoryInfo.New(path)) + .ForCondition(directoryInfo => directoryInfo.Exists) + .FailWith( + "Expected {context:filesystem} to contain directory {0}{reason}, but it did not exist.", + _ => path, directoryInfo => directoryInfo.Name); + + return new AndWhichConstraint(this, + new DirectoryInfoAssertions(Subject.DirectoryInfo.New(path))); + } + + /// + /// Asserts that a file at exists in the file system. + /// + public AndWhichConstraint HaveFile( + string path, string because = "", params object[] becauseArgs) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(!string.IsNullOrEmpty(path)) + .FailWith("You can't assert a file exist if you don't pass a proper name") + .Then + .Given(() => Subject.FileInfo.New(path)) + .ForCondition(fileInfo => fileInfo.Exists) + .FailWith( + "Expected {context:filesystem} to contain file {0}{reason}, but it did not exist.", + _ => path, fileInfo => fileInfo.Name); + + return new AndWhichConstraint(this, + new FileInfoAssertions(Subject.FileInfo.New(path))); + } +} diff --git a/Source/Testably.Abstractions.FluentAssertions/FileSystemExtensions.cs b/Source/Testably.Abstractions.FluentAssertions/FileSystemExtensions.cs new file mode 100644 index 000000000..62f4a1dae --- /dev/null +++ b/Source/Testably.Abstractions.FluentAssertions/FileSystemExtensions.cs @@ -0,0 +1,16 @@ +namespace Testably.Abstractions.FluentAssertions; + +/// +/// Assertion extensions on . +/// +public static class FileSystemExtensions +{ + /// + /// Returns a object that can be used to + /// assert the current . + /// + public static FileSystemAssertions Should(this IFileSystem instance) + { + return new FileSystemAssertions(instance); + } +} diff --git a/Source/Testably.Abstractions.FluentAssertions/Testably.Abstractions.FluentAssertions.csproj b/Source/Testably.Abstractions.FluentAssertions/Testably.Abstractions.FluentAssertions.csproj new file mode 100644 index 000000000..377f5068e --- /dev/null +++ b/Source/Testably.Abstractions.FluentAssertions/Testably.Abstractions.FluentAssertions.csproj @@ -0,0 +1,23 @@ + + + + Testably.Abstractions.FluentAssertions + FluentAssertions extension methods for `Testably.Abstractions`. + Docs/FluentAssertions.md + + + + + + + + + + + + + + + diff --git a/Source/Testably.Abstractions.FluentAssertions/Usings.cs b/Source/Testably.Abstractions.FluentAssertions/Usings.cs new file mode 100644 index 000000000..868bcfa76 --- /dev/null +++ b/Source/Testably.Abstractions.FluentAssertions/Usings.cs @@ -0,0 +1,4 @@ +global using FluentAssertions; +global using FluentAssertions.Execution; +global using FluentAssertions.Primitives; +global using System.IO.Abstractions; diff --git a/Testably.Abstractions.sln b/Testably.Abstractions.sln index e3880f4cf..5954288e2 100644 --- a/Testably.Abstractions.sln +++ b/Testably.Abstractions.sln @@ -10,8 +10,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_", "_", "{94F99274-3518-45 .gitignore = .gitignore CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md CONTRIBUTING.md = CONTRIBUTING.md - Directory.Packages.props = Directory.Packages.props Directory.Build.props = Directory.Build.props + Directory.Packages.props = Directory.Packages.props Feature.Flags.props = Feature.Flags.props LICENSE = LICENSE nuget.config = nuget.config @@ -74,6 +74,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{4D70CF83-2 ProjectSection(SolutionItems) = preProject Docs\AccessControl.md = Docs\AccessControl.md Docs\Compression.md = Docs\Compression.md + Docs\FluentAssertions.md = Docs\FluentAssertions.md Docs\Interface.md = Docs\Interface.md Docs\Testing.md = Docs\Testing.md EndProjectSection @@ -84,6 +85,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Testably.Abstractions.TestH EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Testably.Abstractions.Tests.SourceGenerator", "Tests\Helpers\Testably.Abstractions.Tests.SourceGenerator\Testably.Abstractions.Tests.SourceGenerator.csproj", "{97A2540A-7FB4-4D8B-B003-3F8CAA9CED87}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Testably.Abstractions.FluentAssertions", "Source\Testably.Abstractions.FluentAssertions\Testably.Abstractions.FluentAssertions.csproj", "{BC318ADF-0A23-4D0F-8FBE-4E73F839450A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Testably.Abstractions.FluentAssertions.Tests", "Tests\Testably.Abstractions.FluentAssertions.Tests\Testably.Abstractions.FluentAssertions.Tests.csproj", "{67F7AF4E-B807-4627-9452-E5BC50E32B90}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -138,6 +143,14 @@ Global {97A2540A-7FB4-4D8B-B003-3F8CAA9CED87}.Debug|Any CPU.Build.0 = Debug|Any CPU {97A2540A-7FB4-4D8B-B003-3F8CAA9CED87}.Release|Any CPU.ActiveCfg = Release|Any CPU {97A2540A-7FB4-4D8B-B003-3F8CAA9CED87}.Release|Any CPU.Build.0 = Release|Any CPU + {BC318ADF-0A23-4D0F-8FBE-4E73F839450A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC318ADF-0A23-4D0F-8FBE-4E73F839450A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC318ADF-0A23-4D0F-8FBE-4E73F839450A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC318ADF-0A23-4D0F-8FBE-4E73F839450A}.Release|Any CPU.Build.0 = Release|Any CPU + {67F7AF4E-B807-4627-9452-E5BC50E32B90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67F7AF4E-B807-4627-9452-E5BC50E32B90}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67F7AF4E-B807-4627-9452-E5BC50E32B90}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67F7AF4E-B807-4627-9452-E5BC50E32B90}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -157,6 +170,7 @@ Global {B6C45D8A-A545-402E-A6B0-47BC7D9BBCF5} = {61F335A6-9CE0-4040-A34F-E70B1A55077D} {425BA8BA-7795-4749-BFC1-6FA853EBC6B7} = {B6C45D8A-A545-402E-A6B0-47BC7D9BBCF5} {97A2540A-7FB4-4D8B-B003-3F8CAA9CED87} = {B6C45D8A-A545-402E-A6B0-47BC7D9BBCF5} + {67F7AF4E-B807-4627-9452-E5BC50E32B90} = {61F335A6-9CE0-4040-A34F-E70B1A55077D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {EC4D8481-B9FD-41B5-832A-369210993DF4} diff --git a/Tests/Testably.Abstractions.FluentAssertions.Tests/DirectoryInfoAssertionsTests.cs b/Tests/Testably.Abstractions.FluentAssertions.Tests/DirectoryInfoAssertionsTests.cs new file mode 100644 index 000000000..5ea77a3e4 --- /dev/null +++ b/Tests/Testably.Abstractions.FluentAssertions.Tests/DirectoryInfoAssertionsTests.cs @@ -0,0 +1,42 @@ +using AutoFixture.Xunit2; +using FluentAssertions; +using System; +using Testably.Abstractions.Testing; +using Xunit; + +namespace Testably.Abstractions.FluentAssertions.Tests; + +public class DirectoryInfoAssertionsTests +{ + [Theory] + [AutoData] + public void HaveFile_WithFile_ShouldNotThrow(string directoryName, + string fileName) + { + MockFileSystem fileSystem = new(); + fileSystem.Initialize() + .WithSubdirectory(directoryName).Initialized(d => d + .WithFile(fileName)); + DirectoryInfoAssertions? sut = fileSystem.Should().HaveDirectory(directoryName).Which; + + sut.HaveFile(fileName); + } + + [Theory] + [AutoData] + public void HaveFile_WithoutFile_ShouldThrow(string directoryName, + string fileName) + { + MockFileSystem fileSystem = new(); + fileSystem.Initialize() + .WithSubdirectory(directoryName); + DirectoryInfoAssertions? sut = fileSystem.Should().HaveDirectory(directoryName).Which; + + Exception? exception = Record.Exception(() => + { + sut.HaveFile(fileName); + }); + + exception.Should().NotBeNull(); + } +} diff --git a/Tests/Testably.Abstractions.FluentAssertions.Tests/FileInfoAssertionsTests.cs b/Tests/Testably.Abstractions.FluentAssertions.Tests/FileInfoAssertionsTests.cs new file mode 100644 index 000000000..6eb494e8d --- /dev/null +++ b/Tests/Testably.Abstractions.FluentAssertions.Tests/FileInfoAssertionsTests.cs @@ -0,0 +1,73 @@ +using AutoFixture.Xunit2; +using FluentAssertions; +using System; +using Testably.Abstractions.Testing; +using Testably.Abstractions.Testing.FileSystemInitializer; +using Xunit; + +namespace Testably.Abstractions.FluentAssertions.Tests; + +public class FileInfoAssertionsTests +{ + [Theory] + [AutoData] + public void IsNotReadOnly_WithReadOnlyFile_ShouldThrow(FileDescription fileDescription) + { + fileDescription.IsReadOnly = true; + MockFileSystem fileSystem = new(); + fileSystem.Initialize() + .With(fileDescription); + FileInfoAssertions? sut = fileSystem.Should().HaveFile(fileDescription.Name).Which; + + Exception? exception = Record.Exception(() => + { + sut.IsNotReadOnly(); + }); + + exception.Should().NotBeNull(); + } + + [Theory] + [AutoData] + public void IsNotReadOnly_WithWritableFile_ShouldNotThrow(FileDescription fileDescription) + { + fileDescription.IsReadOnly = false; + MockFileSystem fileSystem = new(); + fileSystem.Initialize() + .With(fileDescription); + FileInfoAssertions? sut = fileSystem.Should().HaveFile(fileDescription.Name).Which; + + sut.IsNotReadOnly(); + } + + [Theory] + [AutoData] + public void IsReadOnly_WithReadOnlyFile_ShouldNotThrow(FileDescription fileDescription) + { + fileDescription.IsReadOnly = true; + MockFileSystem fileSystem = new(); + fileSystem.Initialize() + .With(fileDescription); + FileInfoAssertions? sut = fileSystem.Should().HaveFile(fileDescription.Name).Which; + + sut.IsReadOnly(); + } + + [Theory] + [AutoData] + public void IsReadOnly_WithWritableFile_ShouldThrow(FileDescription fileDescription) + { + fileDescription.IsReadOnly = false; + MockFileSystem fileSystem = new(); + fileSystem.Initialize() + .With(fileDescription); + FileInfoAssertions? sut = fileSystem.Should().HaveFile(fileDescription.Name).Which; + + Exception? exception = Record.Exception(() => + { + sut.IsReadOnly(); + }); + + exception.Should().NotBeNull(); + } +} diff --git a/Tests/Testably.Abstractions.FluentAssertions.Tests/FileSystemAssertionsTests.cs b/Tests/Testably.Abstractions.FluentAssertions.Tests/FileSystemAssertionsTests.cs new file mode 100644 index 000000000..1209241df --- /dev/null +++ b/Tests/Testably.Abstractions.FluentAssertions.Tests/FileSystemAssertionsTests.cs @@ -0,0 +1,84 @@ +using AutoFixture.Xunit2; +using FluentAssertions; +using System; +using Testably.Abstractions.Testing; +using Xunit; + +namespace Testably.Abstractions.FluentAssertions.Tests; + +public class FileSystemAssertionsTests +{ + [Theory] + [AutoData] + public void HaveDirectory_WithDirectory_ShouldNotThrow(string path) + { + MockFileSystem fileSystem = new(); + fileSystem.Initialize() + .WithSubdirectory(path); + + fileSystem.Should().HaveDirectory(path); + } + + [Theory] + [AutoData] + public void HaveDirectory_WithMultipleDirectories_ShouldNotThrow(string path1, string path2) + { + MockFileSystem fileSystem = new(); + fileSystem.Initialize() + .WithSubdirectory(path1) + .WithSubdirectory(path2); + + fileSystem.Should().HaveDirectory(path1).And.HaveDirectory(path2); + } + + [Theory] + [AutoData] + public void HaveDirectory_WithoutDirectory_ShouldThrow(string path) + { + MockFileSystem fileSystem = new(); + + Exception? exception = Record.Exception(() => + { + fileSystem.Should().HaveDirectory(path); + }); + + exception.Should().NotBeNull(); + } + + [Theory] + [AutoData] + public void HaveFile_WithFile_ShouldNotThrow(string path) + { + MockFileSystem fileSystem = new(); + fileSystem.Initialize() + .WithFile(path); + + fileSystem.Should().HaveFile(path); + } + + [Theory] + [AutoData] + public void HaveFile_WithMultipleDirectories_ShouldNotThrow(string path1, string path2) + { + MockFileSystem fileSystem = new(); + fileSystem.Initialize() + .WithFile(path1) + .WithFile(path2); + + fileSystem.Should().HaveFile(path1).And.HaveFile(path2); + } + + [Theory] + [AutoData] + public void HaveFile_WithoutFile_ShouldThrow(string path) + { + MockFileSystem fileSystem = new(); + + Exception? exception = Record.Exception(() => + { + fileSystem.Should().HaveFile(path); + }); + + exception.Should().NotBeNull(); + } +} diff --git a/Tests/Testably.Abstractions.FluentAssertions.Tests/Testably.Abstractions.FluentAssertions.Tests.csproj b/Tests/Testably.Abstractions.FluentAssertions.Tests/Testably.Abstractions.FluentAssertions.Tests.csproj new file mode 100644 index 000000000..42301a073 --- /dev/null +++ b/Tests/Testably.Abstractions.FluentAssertions.Tests/Testably.Abstractions.FluentAssertions.Tests.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + + + + + + + + From 8f974cb17d5a65d9ed1130e743e8dd858ee4189d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 5 Aug 2023 16:19:21 +0200 Subject: [PATCH 2/4] Adjust build pipelines to include FluentAssertions project --- .../Stryker.Config.FluentAssertions.json | 24 +++++++++++++++++++ .github/workflows/ci-stryker.yml | 8 +++++++ .github/workflows/release.yml | 9 ++++++- .github/workflows/stryker.yml | 7 ++++++ Testably.Abstractions.sln | 1 + 5 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 .github/stryker/Stryker.Config.FluentAssertions.json diff --git a/.github/stryker/Stryker.Config.FluentAssertions.json b/.github/stryker/Stryker.Config.FluentAssertions.json new file mode 100644 index 000000000..c7610ddb1 --- /dev/null +++ b/.github/stryker/Stryker.Config.FluentAssertions.json @@ -0,0 +1,24 @@ +{ + "stryker-config": { + "project-info": { + "name": "github.com/Testably/Testably.Abstractions", + "module": "Testably.Abstractions.FluentAssertions" + }, + "test-projects": [ + "./Testably.Abstractions.FluentAssertions.Tests/Testably.Abstractions.FluentAssertions.Tests.csproj" + ], + "project": "Testably.Abstractions.FluentAssertions.csproj", + "target-framework": "net7.0", + "since": { + "ignore-changes-in": [ + "**/.github/**/*.*" + ] + }, + "reporters": [ + "html", + "progress", + "cleartext" + ], + "mutation-level": "Advanced" + } +} diff --git a/.github/workflows/ci-stryker.yml b/.github/workflows/ci-stryker.yml index 2e5276667..3385ea904 100644 --- a/.github/workflows/ci-stryker.yml +++ b/.github/workflows/ci-stryker.yml @@ -51,6 +51,14 @@ jobs: cd Tests ../../tools/dotnet-stryker -f ../.github/stryker/Stryker.Config.Compression.json -v "${GITHUB_HEAD_REF}" -r "html" -r "cleartext" --since:main mv ./StrykerOutput/**/reports/*.html ./StrykerOutput/Reports/Testably.Abstractions.Compression-report.html + - name: Analyze Testably.Abstractions.FluentAssertions + env: + STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} + shell: bash + run: | + cd Tests + ../../tools/dotnet-stryker -f ../.github/stryker/Stryker.Config.FluentAssertions.json -v "${GITHUB_HEAD_REF}" -r "html" -r "cleartext" --since:main + mv ./StrykerOutput/**/reports/*.html ./StrykerOutput/Reports/Testably.Abstractions.FluentAssertions-report.html - name: Analyze Testably.Abstractions.Testing env: STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6efa0c6a6..dd6dfaf8b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -255,6 +255,13 @@ jobs: run: | cd Tests ../../tools/dotnet-stryker -f ../.github/stryker/Stryker.Config.Compression.json -v "${GITHUB_REF#refs/heads/}" -r "Dashboard" -r "cleartext" + - name: Analyze Testably.Abstractions.FluentAssertions + env: + STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} + shell: bash + run: | + cd Tests + ../../tools/dotnet-stryker -f ../.github/stryker/Stryker.Config.FluentAssertions.json -v "${GITHUB_REF#refs/heads/}" -r "Dashboard" -r "cleartext" deploy: name: Deploy @@ -286,7 +293,7 @@ jobs: version="${GITHUB_REF#refs/heads/release/}" # Add changelog badge to README.md sed -i -e "2 a\[!\[Changelog](https:\/\/img\.shields\.io\/badge\/Changelog-${version}-blue)](https:\/\/github\.com\/Testably\/Testably\.Abstractions\/releases\/tag\/${version})" "./README.md" - for f in "README.md" "Docs/AccessControl.md" "Docs/Compression.md" "Docs/Interface.md" "Docs/Testing.md" + for f in "README.md" "Docs/AccessControl.md" "Docs/Compression.md" "Docs/FluentAssertions.md" "Docs/Interface.md" "Docs/Testing.md" do echo "Processing $f" # always double quote "$f" filename # do something on $f diff --git a/.github/workflows/stryker.yml b/.github/workflows/stryker.yml index c60b81167..1adb00b62 100644 --- a/.github/workflows/stryker.yml +++ b/.github/workflows/stryker.yml @@ -73,3 +73,10 @@ jobs: run: | cd Tests ../../tools/dotnet-stryker -f ../.github/stryker/Stryker.Config.Compression.json -v "${GITHUB_REF#refs/heads/}" -r "Dashboard" -r "cleartext" + - name: Analyze Testably.Abstractions.FluentAssertions + env: + STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} + shell: bash + run: | + cd Tests + ../../tools/dotnet-stryker -f ../.github/stryker/Stryker.Config.FluentAssertions.json -v "${GITHUB_REF#refs/heads/}" -r "Dashboard" -r "cleartext" diff --git a/Testably.Abstractions.sln b/Testably.Abstractions.sln index 5954288e2..80bc243d1 100644 --- a/Testably.Abstractions.sln +++ b/Testably.Abstractions.sln @@ -46,6 +46,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "stryker", "stryker", "{4D8D ProjectSection(SolutionItems) = preProject .github\stryker\Stryker.Config.AccessControl.json = .github\stryker\Stryker.Config.AccessControl.json .github\stryker\Stryker.Config.Compression.json = .github\stryker\Stryker.Config.Compression.json + .github\stryker\Stryker.Config.FluentAssertions.json = .github\stryker\Stryker.Config.FluentAssertions.json .github\stryker\Stryker.Config.json = .github\stryker\Stryker.Config.json .github\stryker\Stryker.Config.Testing.json = .github\stryker\Stryker.Config.Testing.json EndProjectSection From 21b84bbad7cfe4ba6a2b971e7923f40832f18d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 5 Aug 2023 16:33:15 +0200 Subject: [PATCH 3/4] Assert exception message in tests --- .../DirectoryInfoAssertions.cs | 8 ++++---- .../FileInfoAssertions.cs | 8 +++++--- .../DirectoryInfoAssertionsTests.cs | 15 ++++++++++----- .../FileInfoAssertionsTests.cs | 16 ++++++++++++---- .../FileSystemAssertionsTests.cs | 14 ++++++++++---- 5 files changed, 41 insertions(+), 20 deletions(-) diff --git a/Source/Testably.Abstractions.FluentAssertions/DirectoryInfoAssertions.cs b/Source/Testably.Abstractions.FluentAssertions/DirectoryInfoAssertions.cs index 0b0c48c32..3f46658eb 100644 --- a/Source/Testably.Abstractions.FluentAssertions/DirectoryInfoAssertions.cs +++ b/Source/Testably.Abstractions.FluentAssertions/DirectoryInfoAssertions.cs @@ -7,7 +7,7 @@ public class DirectoryInfoAssertions : ReferenceTypeAssertions { /// - protected override string Identifier => "directoryinfo"; + protected override string Identifier => "directory"; internal DirectoryInfoAssertions(IDirectoryInfo instance) : base(instance) @@ -17,7 +17,7 @@ internal DirectoryInfoAssertions(IDirectoryInfo instance) /// /// Asserts that the current directory has at least one file which matches the . /// - public AndConstraint HaveFile( + public AndConstraint HasFile( string searchPattern = "*", string because = "", params object[] becauseArgs) { Execute.Assertion @@ -29,8 +29,8 @@ public AndConstraint HaveFile( .Given(() => Subject.GetFiles(searchPattern)) .ForCondition(fileInfos => fileInfos.Length > 0) .FailWith( - "Expected {context:directoryinfo} to contain at least one file matching {0}{reason}, but none was found.", - _ => searchPattern); + "Expected {context:directory} '{1}' to contain at least one file matching {0}{reason}, but none was found.", + _ => searchPattern, _ => Subject.Name); return new AndConstraint(this); } diff --git a/Source/Testably.Abstractions.FluentAssertions/FileInfoAssertions.cs b/Source/Testably.Abstractions.FluentAssertions/FileInfoAssertions.cs index e9f9de564..779684269 100644 --- a/Source/Testably.Abstractions.FluentAssertions/FileInfoAssertions.cs +++ b/Source/Testably.Abstractions.FluentAssertions/FileInfoAssertions.cs @@ -7,7 +7,7 @@ public class FileInfoAssertions : ReferenceTypeAssertions { /// - protected override string Identifier => "fileinfo"; + protected override string Identifier => "file"; internal FileInfoAssertions(IFileInfo instance) : base(instance) @@ -25,7 +25,8 @@ public AndConstraint IsNotReadOnly( .Given(() => Subject) .ForCondition(fileInfo => !fileInfo.IsReadOnly) .FailWith( - "Expected {context:fileinfo} not to be read-only {reason}, but it was."); + "Expected {context:file} '{0}' not to be read-only {reason}, but it was.", + _ => Subject.Name); return new AndConstraint(this); } @@ -41,7 +42,8 @@ public AndConstraint IsReadOnly( .Given(() => Subject) .ForCondition(fileInfo => fileInfo.IsReadOnly) .FailWith( - "Expected {context:fileinfo} to be read-only {reason}, but it was not."); + "Expected {context:file} '{0}' to be read-only {reason}, but it was not.", + _ => Subject.Name); return new AndConstraint(this); } diff --git a/Tests/Testably.Abstractions.FluentAssertions.Tests/DirectoryInfoAssertionsTests.cs b/Tests/Testably.Abstractions.FluentAssertions.Tests/DirectoryInfoAssertionsTests.cs index 5ea77a3e4..50b918b44 100644 --- a/Tests/Testably.Abstractions.FluentAssertions.Tests/DirectoryInfoAssertionsTests.cs +++ b/Tests/Testably.Abstractions.FluentAssertions.Tests/DirectoryInfoAssertionsTests.cs @@ -10,7 +10,8 @@ public class DirectoryInfoAssertionsTests { [Theory] [AutoData] - public void HaveFile_WithFile_ShouldNotThrow(string directoryName, + public void HasFile_WithFile_ShouldNotThrow( + string directoryName, string fileName) { MockFileSystem fileSystem = new(); @@ -19,13 +20,15 @@ public void HaveFile_WithFile_ShouldNotThrow(string directoryName, .WithFile(fileName)); DirectoryInfoAssertions? sut = fileSystem.Should().HaveDirectory(directoryName).Which; - sut.HaveFile(fileName); + sut.HasFile(fileName); } [Theory] [AutoData] - public void HaveFile_WithoutFile_ShouldThrow(string directoryName, - string fileName) + public void HasFile_WithoutFile_ShouldThrow( + string directoryName, + string fileName, + string because) { MockFileSystem fileSystem = new(); fileSystem.Initialize() @@ -34,9 +37,11 @@ public void HaveFile_WithoutFile_ShouldThrow(string directoryName, Exception? exception = Record.Exception(() => { - sut.HaveFile(fileName); + sut.HasFile(fileName, because); }); exception.Should().NotBeNull(); + exception!.Message.Should().Contain(fileName); + exception.Message.Should().Contain(because); } } diff --git a/Tests/Testably.Abstractions.FluentAssertions.Tests/FileInfoAssertionsTests.cs b/Tests/Testably.Abstractions.FluentAssertions.Tests/FileInfoAssertionsTests.cs index 6eb494e8d..9265a9011 100644 --- a/Tests/Testably.Abstractions.FluentAssertions.Tests/FileInfoAssertionsTests.cs +++ b/Tests/Testably.Abstractions.FluentAssertions.Tests/FileInfoAssertionsTests.cs @@ -11,7 +11,9 @@ public class FileInfoAssertionsTests { [Theory] [AutoData] - public void IsNotReadOnly_WithReadOnlyFile_ShouldThrow(FileDescription fileDescription) + public void IsNotReadOnly_WithReadOnlyFile_ShouldThrow( + FileDescription fileDescription, + string because) { fileDescription.IsReadOnly = true; MockFileSystem fileSystem = new(); @@ -21,10 +23,12 @@ public void IsNotReadOnly_WithReadOnlyFile_ShouldThrow(FileDescription fileDescr Exception? exception = Record.Exception(() => { - sut.IsNotReadOnly(); + sut.IsNotReadOnly(because); }); exception.Should().NotBeNull(); + exception!.Message.Should().Contain(fileDescription.Name); + exception.Message.Should().Contain(because); } [Theory] @@ -55,7 +59,9 @@ public void IsReadOnly_WithReadOnlyFile_ShouldNotThrow(FileDescription fileDescr [Theory] [AutoData] - public void IsReadOnly_WithWritableFile_ShouldThrow(FileDescription fileDescription) + public void IsReadOnly_WithWritableFile_ShouldThrow( + FileDescription fileDescription, + string because) { fileDescription.IsReadOnly = false; MockFileSystem fileSystem = new(); @@ -65,9 +71,11 @@ public void IsReadOnly_WithWritableFile_ShouldThrow(FileDescription fileDescript Exception? exception = Record.Exception(() => { - sut.IsReadOnly(); + sut.IsReadOnly(because); }); exception.Should().NotBeNull(); + exception!.Message.Should().Contain(fileDescription.Name); + exception.Message.Should().Contain(because); } } diff --git a/Tests/Testably.Abstractions.FluentAssertions.Tests/FileSystemAssertionsTests.cs b/Tests/Testably.Abstractions.FluentAssertions.Tests/FileSystemAssertionsTests.cs index 1209241df..4ecd0da3a 100644 --- a/Tests/Testably.Abstractions.FluentAssertions.Tests/FileSystemAssertionsTests.cs +++ b/Tests/Testably.Abstractions.FluentAssertions.Tests/FileSystemAssertionsTests.cs @@ -33,16 +33,20 @@ public void HaveDirectory_WithMultipleDirectories_ShouldNotThrow(string path1, s [Theory] [AutoData] - public void HaveDirectory_WithoutDirectory_ShouldThrow(string path) + public void HaveDirectory_WithoutDirectory_ShouldThrow( + string path, + string because) { MockFileSystem fileSystem = new(); Exception? exception = Record.Exception(() => { - fileSystem.Should().HaveDirectory(path); + fileSystem.Should().HaveDirectory(path, because); }); exception.Should().NotBeNull(); + exception!.Message.Should().Contain(path); + exception.Message.Should().Contain(because); } [Theory] @@ -70,15 +74,17 @@ public void HaveFile_WithMultipleDirectories_ShouldNotThrow(string path1, string [Theory] [AutoData] - public void HaveFile_WithoutFile_ShouldThrow(string path) + public void HaveFile_WithoutFile_ShouldThrow(string path, string because) { MockFileSystem fileSystem = new(); Exception? exception = Record.Exception(() => { - fileSystem.Should().HaveFile(path); + fileSystem.Should().HaveFile(path, because); }); exception.Should().NotBeNull(); + exception!.Message.Should().Contain(path); + exception.Message.Should().Contain(because); } } From 29e2ab9b68787a1750b677f307aee677f8845be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Thu, 10 Aug 2023 07:51:51 +0200 Subject: [PATCH 4/4] Enable running the tests against the MockFileSystem from TestableIO. --- Directory.Packages.props | 1 + .../FileSystemClassGenerator.cs | 32 +++++++++++++++++-- ...ly.Abstractions.AccessControl.Tests.csproj | 1 + ...ably.Abstractions.Compression.Tests.csproj | 1 + .../Internal/DirectoryInfoWrapperTests.cs | 1 + .../Internal/DriveInfoWrapperTests.cs | 4 ++- .../Internal/FileInfoWrapperTests.cs | 1 + .../Internal/FileSystemInfoWrapperTests.cs | 2 ++ .../Internal/FileSystemWatcherWrapperTests.cs | 4 ++- .../Testably.Abstractions.Tests.csproj | 1 + 10 files changed, 43 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 09db8ac3c..4f25339f4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,6 +8,7 @@ + diff --git a/Tests/Helpers/Testably.Abstractions.Tests.SourceGenerator/ClassGenerators/FileSystemClassGenerator.cs b/Tests/Helpers/Testably.Abstractions.Tests.SourceGenerator/ClassGenerators/FileSystemClassGenerator.cs index 1f0c6feb5..e13b4eb94 100644 --- a/Tests/Helpers/Testably.Abstractions.Tests.SourceGenerator/ClassGenerators/FileSystemClassGenerator.cs +++ b/Tests/Helpers/Testably.Abstractions.Tests.SourceGenerator/ClassGenerators/FileSystemClassGenerator.cs @@ -15,6 +15,7 @@ protected override void GenerateSource(StringBuilder sourceBuilder, ClassModel @ => sourceBuilder.Append(@$" using Testably.Abstractions.Testing.FileSystemInitializer; using Testably.Abstractions.TestHelpers; +using System.IO.Abstractions.TestingHelpers; using Xunit.Abstractions; namespace {@class.Namespace} @@ -31,18 +32,18 @@ public abstract partial class {@class.Name} namespace {@class.Namespace}.{@class.Name} {{ // ReSharper disable once UnusedMember.Global - public sealed class MockFileSystemTests : {@class.Name}, IDisposable + public sealed class MockFileSystemTests : {@class.Name}, IDisposable {{ /// public override string BasePath => _directoryCleaner.BasePath; private readonly IDirectoryCleaner _directoryCleaner; - public MockFileSystemTests() : this(new MockFileSystem()) + public MockFileSystemTests() : this(new Testably.Abstractions.Testing.MockFileSystem()) {{ }} - private MockFileSystemTests(MockFileSystem mockFileSystem) : base( + private MockFileSystemTests(Testably.Abstractions.Testing.MockFileSystem mockFileSystem) : base( mockFileSystem, mockFileSystem.TimeSystem) {{ @@ -54,6 +55,31 @@ private MockFileSystemTests(MockFileSystem mockFileSystem) : base( public void Dispose() => _directoryCleaner.Dispose(); }} + + // ReSharper disable once UnusedMember.Global + public sealed class TestableIoMockFileSystemTests : {@class.Name}, IDisposable + {{ + /// + public override string BasePath => _directoryCleaner.BasePath; + + private readonly IDirectoryCleaner _directoryCleaner; + + public TestableIoMockFileSystemTests() : this(new System.IO.Abstractions.TestingHelpers.MockFileSystem()) + {{ + }} + + private TestableIoMockFileSystemTests(System.IO.Abstractions.TestingHelpers.MockFileSystem mockFileSystem) : base( + mockFileSystem, + new RealTimeSystem()) + {{ + _directoryCleaner = FileSystem + .SetCurrentDirectoryToEmptyTemporaryDirectory(); + }} + + /// + public void Dispose() + => _directoryCleaner.Dispose(); + }} }} #if !DEBUG || ENABLE_REALFILESYSTEMTESTS_IN_DEBUG diff --git a/Tests/Testably.Abstractions.AccessControl.Tests/Testably.Abstractions.AccessControl.Tests.csproj b/Tests/Testably.Abstractions.AccessControl.Tests/Testably.Abstractions.AccessControl.Tests.csproj index de9aba0c6..5313d34f3 100644 --- a/Tests/Testably.Abstractions.AccessControl.Tests/Testably.Abstractions.AccessControl.Tests.csproj +++ b/Tests/Testably.Abstractions.AccessControl.Tests/Testably.Abstractions.AccessControl.Tests.csproj @@ -5,6 +5,7 @@ + diff --git a/Tests/Testably.Abstractions.Compression.Tests/Testably.Abstractions.Compression.Tests.csproj b/Tests/Testably.Abstractions.Compression.Tests/Testably.Abstractions.Compression.Tests.csproj index 2fcc4e1be..99a0b53ea 100644 --- a/Tests/Testably.Abstractions.Compression.Tests/Testably.Abstractions.Compression.Tests.csproj +++ b/Tests/Testably.Abstractions.Compression.Tests/Testably.Abstractions.Compression.Tests.csproj @@ -5,6 +5,7 @@ + diff --git a/Tests/Testably.Abstractions.Tests/Internal/DirectoryInfoWrapperTests.cs b/Tests/Testably.Abstractions.Tests/Internal/DirectoryInfoWrapperTests.cs index 09c05b15c..0bcfef568 100644 --- a/Tests/Testably.Abstractions.Tests/Internal/DirectoryInfoWrapperTests.cs +++ b/Tests/Testably.Abstractions.Tests/Internal/DirectoryInfoWrapperTests.cs @@ -1,4 +1,5 @@ using System.IO; +using DirectoryInfoWrapper = Testably.Abstractions.FileSystem.DirectoryInfoWrapper; namespace Testably.Abstractions.Tests.Internal; diff --git a/Tests/Testably.Abstractions.Tests/Internal/DriveInfoWrapperTests.cs b/Tests/Testably.Abstractions.Tests/Internal/DriveInfoWrapperTests.cs index b88f7a0a4..89841c198 100644 --- a/Tests/Testably.Abstractions.Tests/Internal/DriveInfoWrapperTests.cs +++ b/Tests/Testably.Abstractions.Tests/Internal/DriveInfoWrapperTests.cs @@ -1,4 +1,6 @@ -namespace Testably.Abstractions.Tests.Internal; +using DriveInfoWrapper = Testably.Abstractions.FileSystem.DriveInfoWrapper; + +namespace Testably.Abstractions.Tests.Internal; public class DriveInfoWrapperTests { diff --git a/Tests/Testably.Abstractions.Tests/Internal/FileInfoWrapperTests.cs b/Tests/Testably.Abstractions.Tests/Internal/FileInfoWrapperTests.cs index 999266a37..69083e95f 100644 --- a/Tests/Testably.Abstractions.Tests/Internal/FileInfoWrapperTests.cs +++ b/Tests/Testably.Abstractions.Tests/Internal/FileInfoWrapperTests.cs @@ -1,4 +1,5 @@ using System.IO; +using FileInfoWrapper = Testably.Abstractions.FileSystem.FileInfoWrapper; namespace Testably.Abstractions.Tests.Internal; diff --git a/Tests/Testably.Abstractions.Tests/Internal/FileSystemInfoWrapperTests.cs b/Tests/Testably.Abstractions.Tests/Internal/FileSystemInfoWrapperTests.cs index 05a3781d4..5e9f02c15 100644 --- a/Tests/Testably.Abstractions.Tests/Internal/FileSystemInfoWrapperTests.cs +++ b/Tests/Testably.Abstractions.Tests/Internal/FileSystemInfoWrapperTests.cs @@ -1,4 +1,6 @@ using System.IO; +using DirectoryInfoWrapper = Testably.Abstractions.FileSystem.DirectoryInfoWrapper; +using FileInfoWrapper = Testably.Abstractions.FileSystem.FileInfoWrapper; namespace Testably.Abstractions.Tests.Internal; diff --git a/Tests/Testably.Abstractions.Tests/Internal/FileSystemWatcherWrapperTests.cs b/Tests/Testably.Abstractions.Tests/Internal/FileSystemWatcherWrapperTests.cs index 88021a4a9..25059c6cd 100644 --- a/Tests/Testably.Abstractions.Tests/Internal/FileSystemWatcherWrapperTests.cs +++ b/Tests/Testably.Abstractions.Tests/Internal/FileSystemWatcherWrapperTests.cs @@ -1,4 +1,6 @@ -namespace Testably.Abstractions.Tests.Internal; +using FileSystemWatcherWrapper = Testably.Abstractions.FileSystem.FileSystemWatcherWrapper; + +namespace Testably.Abstractions.Tests.Internal; public class FileSystemWatcherWrapperTests { diff --git a/Tests/Testably.Abstractions.Tests/Testably.Abstractions.Tests.csproj b/Tests/Testably.Abstractions.Tests/Testably.Abstractions.Tests.csproj index d2f919918..3442deb08 100644 --- a/Tests/Testably.Abstractions.Tests/Testably.Abstractions.Tests.csproj +++ b/Tests/Testably.Abstractions.Tests/Testably.Abstractions.Tests.csproj @@ -5,6 +5,7 @@ +