├── src ├── icon.png ├── kzu.snk ├── AssemblyFixture │ ├── build │ │ └── xunit.assemblyfixture.targets │ ├── TestFramework.cs │ ├── IAssemblyFixture.cs │ ├── TestFrameworkExecutor.cs │ ├── AssemblyFixture.csproj │ ├── TestAssemblyRunner.cs │ └── TestCollectionRunner.cs ├── Tests │ ├── AssemblyFixture.Tests.csproj │ └── Samples.cs ├── nuget.config ├── Directory.Build.props └── Directory.Build.targets ├── _config.yml ├── assets ├── img │ └── xunit-purple.png └── css │ └── style.scss ├── Directory.Build.rsp ├── .github ├── dependabot.yml ├── workflows │ ├── changelog.config │ ├── dotnet-file.yml │ ├── sponsor.yml │ ├── publish.yml │ ├── includes.yml │ ├── changelog.yml │ ├── test │ │ └── action.yml │ ├── build.yml │ └── combine-prs.yml └── release.yml ├── .gitignore ├── .gitattributes ├── license.txt ├── changelog.md ├── AssemblyFixture.sln ├── .editorconfig ├── readme.md └── .netconfig /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devlooped/xunit.assemblyfixture/HEAD/src/icon.png -------------------------------------------------------------------------------- /src/kzu.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devlooped/xunit.assemblyfixture/HEAD/src/kzu.snk -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate 2 | 3 | exclude: [ 'src/', '*.sln', 'Gemfile*', '*.rsp' ] -------------------------------------------------------------------------------- /assets/img/xunit-purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devlooped/xunit.assemblyfixture/HEAD/assets/img/xunit-purple.png -------------------------------------------------------------------------------- /Directory.Build.rsp: -------------------------------------------------------------------------------- 1 | # See https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-response-files 2 | -nr:false 3 | -m:1 4 | -v:m 5 | -clp:Summary;ForceNoAlign -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: nuget 7 | directory: / 8 | schedule: 9 | interval: daily 10 | -------------------------------------------------------------------------------- /.github/workflows/changelog.config: -------------------------------------------------------------------------------- 1 | usernames-as-github-logins=true 2 | issues_wo_labels=true 3 | pr_wo_labels=true 4 | exclude-labels=bydesign,dependencies,duplicate,question,invalid,wontfix,need info,docs 5 | enhancement-label=:sparkles: Implemented enhancements: 6 | bugs-label=:bug: Fixed bugs: 7 | issues-label=:hammer: Other: 8 | pr-label=:twisted_rightwards_arrows: Merged: 9 | unreleased=false 10 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-file.yml: -------------------------------------------------------------------------------- 1 | # Synchronizes .netconfig-configured files with dotnet-file 2 | name: dotnet-file 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | push: 8 | branches: [ 'dotnet-file' ] 9 | 10 | env: 11 | DOTNET_NOLOGO: true 12 | 13 | jobs: 14 | run: 15 | uses: devlooped/oss/.github/workflows/dotnet-file-core.yml@main 16 | secrets: inherit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | app 3 | obj 4 | artifacts 5 | pack 6 | TestResults 7 | .vs 8 | .vscode 9 | .idea 10 | 11 | *.suo 12 | *.sdf 13 | *.userprefs 14 | *.user 15 | *.nupkg 16 | *.metaproj 17 | *.tmp 18 | *.log 19 | *.cache 20 | *.binlog 21 | *.zip 22 | __azurite*.* 23 | __*__ 24 | 25 | .nuget 26 | *.lock.json 27 | *.nuget.props 28 | *.nuget.targets 29 | 30 | node_modules 31 | _site 32 | .jekyll-metadata 33 | .jekyll-cache 34 | Gemfile.lock 35 | package-lock.json 36 | -------------------------------------------------------------------------------- /src/AssemblyFixture/build/xunit.assemblyfixture.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <_Parameter1>Xunit.TestFramework 7 | <_Parameter2>xunit.assemblyfixture 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "jekyll-theme-slate"; 5 | 6 | .inner { 7 | max-width: 960px; 8 | } 9 | 10 | pre, code { 11 | background-color: unset; 12 | font-size: unset; 13 | } 14 | 15 | code { 16 | font-size: 0.80em; 17 | } 18 | 19 | h1 > img { 20 | border: unset; 21 | box-shadow: unset; 22 | vertical-align: middle; 23 | -moz-box-shadow: unset; 24 | -o-box-shadow: unset; 25 | -ms-box-shadow: unset; 26 | } 27 | -------------------------------------------------------------------------------- /src/AssemblyFixture/TestFramework.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Xunit.Abstractions; 3 | using Xunit.Sdk; 4 | 5 | namespace Xunit; 6 | 7 | class TestFramework : XunitTestFramework 8 | { 9 | public TestFramework(IMessageSink messageSink) 10 | : base(messageSink) 11 | { } 12 | 13 | protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName) 14 | => new TestFrameworkExecutor(assemblyName, SourceInformationProvider, DiagnosticMessageSink); 15 | } 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # sln, csproj files (and friends) are always CRLF, even on linux 2 | *.sln text eol=crlf 3 | *.proj text eol=crlf 4 | *.csproj text eol=crlf 5 | 6 | # These are windows specific files which we may as well ensure are 7 | # always crlf on checkout 8 | *.bat text eol=crlf 9 | *.cmd text eol=crlf 10 | 11 | # Opt in known filetypes to always normalize line endings on checkin 12 | # and always use native endings on checkout 13 | *.c text 14 | *.config text 15 | *.h text 16 | *.cs text 17 | *.md text 18 | *.tt text 19 | *.txt text 20 | 21 | # Some must always be checked out as lf so enforce that for those files 22 | # If these are not lf then bash/cygwin on windows will not be able to 23 | # excute the files 24 | *.sh text eol=lf -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - bydesign 5 | - dependencies 6 | - duplicate 7 | - question 8 | - invalid 9 | - wontfix 10 | - need info 11 | - docs 12 | - techdebt 13 | authors: 14 | - devlooped-bot 15 | - dependabot 16 | - github-actions 17 | categories: 18 | - title: ✨ Implemented enhancements 19 | labels: 20 | - enhancement 21 | - title: 🐛 Fixed bugs 22 | labels: 23 | - bug 24 | - title: 📝 Documentation updates 25 | labels: 26 | - docs 27 | - title: 🔨 Other 28 | labels: 29 | - '*' 30 | exclude: 31 | labels: 32 | - dependencies 33 | -------------------------------------------------------------------------------- /.github/workflows/sponsor.yml: -------------------------------------------------------------------------------- 1 | name: sponsor 💜 2 | on: 3 | issues: 4 | types: [opened, edited, reopened] 5 | pull_request: 6 | types: [opened, edited, synchronize, reopened] 7 | 8 | jobs: 9 | sponsor: 10 | runs-on: ubuntu-latest 11 | continue-on-error: true 12 | env: 13 | token: ${{ secrets.GH_TOKEN }} 14 | if: ${{ !endsWith(github.event.sender.login, '[bot]') && !endsWith(github.event.sender.login, 'bot') }} 15 | steps: 16 | - name: 🤘 checkout 17 | if: env.token != '' 18 | uses: actions/checkout@v2 19 | 20 | - name: 💜 sponsor 21 | if: env.token != '' 22 | uses: devlooped/actions-sponsor@main 23 | with: 24 | token: ${{ env.token }} 25 | -------------------------------------------------------------------------------- /src/Tests/AssemblyFixture.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | Xunit 5 | AssemblyFixture.Tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/AssemblyFixture/IAssemblyFixture.cs: -------------------------------------------------------------------------------- 1 | namespace Xunit; 2 | 3 | 4 | /// 5 | /// Used to decorate xUnit.net test classes and collections to indicate a test which has 6 | /// per-assembly fixture data. An instance of the fixture data is initialized just before 7 | /// the first test in the assembly is run, and if it implements IDisposable, is disposed 8 | /// after the last test in the assembly is run. To gain access to the fixture data from 9 | /// inside the test, a constructor argument should be added to the test class which 10 | /// exactly matches the . 11 | /// 12 | /// The type of the fixture. 13 | public interface IAssemblyFixture where TFixture : class, new() { } 14 | -------------------------------------------------------------------------------- /src/AssemblyFixture/TestFrameworkExecutor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Reflection; 3 | using Xunit.Abstractions; 4 | using Xunit.Sdk; 5 | 6 | namespace Xunit; 7 | 8 | class TestFrameworkExecutor : XunitTestFrameworkExecutor 9 | { 10 | public TestFrameworkExecutor(AssemblyName assemblyName, ISourceInformationProvider sourceInformationProvider, IMessageSink diagnosticMessageSink) 11 | : base(assemblyName, sourceInformationProvider, diagnosticMessageSink) 12 | { } 13 | 14 | protected override async void RunTestCases(IEnumerable testCases, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions) 15 | { 16 | using var assemblyRunner = new TestAssemblyRunner(TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink, executionOptions); 17 | await assemblyRunner.RunAsync(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Builds a final release version and pushes to nuget.org 2 | # whenever a release is published. 3 | # Requires: secrets.NUGET_API_KEY 4 | 5 | name: publish 6 | on: 7 | release: 8 | types: [released] 9 | 10 | env: 11 | DOTNET_NOLOGO: true 12 | Configuration: Release 13 | 14 | jobs: 15 | publish: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: 🤘 checkout 19 | uses: actions/checkout@v2 20 | with: 21 | submodules: recursive 22 | fetch-depth: 0 23 | 24 | - name: 🙏 build 25 | run: dotnet build -m:1 -p:version=${GITHUB_REF#refs/*/v} 26 | 27 | - name: 🧪 test 28 | uses: ./.github/workflows/test 29 | 30 | - name: 📦 pack 31 | run: dotnet pack -m:1 -p:version=${GITHUB_REF#refs/*/v} 32 | 33 | - name: 🚀 nuget 34 | run: dotnet nuget push ./bin/**/*.nupkg -s https://api.nuget.org/v3/index.json -k ${{secrets.NUGET_API_KEY}} --skip-duplicate 35 | -------------------------------------------------------------------------------- /src/AssemblyFixture/AssemblyFixture.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | xunit.assemblyfixture 5 | Xunit 6 | xunit.assemblyfixture 7 | readme.md 8 | 9 | Assembly-level shared state via IAssemblyFixture, just like built-in collection and class fixture state. 10 | 11 | true 12 | false 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Daniel Cazzulino and Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/includes.yml: -------------------------------------------------------------------------------- 1 | name: +Mᐁ includes 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - 'main' 7 | paths: 8 | - '**.md' 9 | - '!changelog.md' 10 | 11 | jobs: 12 | includes: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: 🤖 defaults 16 | uses: devlooped/actions-bot@v1 17 | with: 18 | name: ${{ secrets.BOT_NAME }} 19 | email: ${{ secrets.BOT_EMAIL }} 20 | gh_token: ${{ secrets.GH_TOKEN }} 21 | github_token: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | - name: 🤘 checkout 24 | uses: actions/checkout@v2 25 | with: 26 | token: ${{ env.GH_TOKEN }} 27 | 28 | - name: +Mᐁ includes 29 | uses: devlooped/actions-includes@v1 30 | 31 | - name: ✍ pull request 32 | uses: peter-evans/create-pull-request@v4 33 | with: 34 | base: main 35 | branch: markdown-includes 36 | delete-branch: true 37 | labels: docs 38 | author: ${{ env.BOT_AUTHOR }} 39 | committer: ${{ env.BOT_AUTHOR }} 40 | commit-message: +Mᐁ includes 41 | title: +Mᐁ includes 42 | body: +Mᐁ includes 43 | token: ${{ env.GH_TOKEN }} 44 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: changelog 2 | on: 3 | workflow_dispatch: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | changelog: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: 🤖 defaults 12 | uses: devlooped/actions-bot@v1 13 | with: 14 | name: ${{ secrets.BOT_NAME }} 15 | email: ${{ secrets.BOT_EMAIL }} 16 | gh_token: ${{ secrets.GH_TOKEN }} 17 | github_token: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | - name: 🤘 checkout 20 | uses: actions/checkout@v2 21 | with: 22 | fetch-depth: 0 23 | ref: main 24 | token: ${{ env.GH_TOKEN }} 25 | 26 | - name: ⚙ ruby 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: 3.0.3 30 | 31 | - name: ⚙ changelog 32 | run: | 33 | gem install github_changelog_generator 34 | github_changelog_generator --user ${GITHUB_REPOSITORY%/*} --project ${GITHUB_REPOSITORY##*/} --token $GH_TOKEN --o changelog.md --config-file .github/workflows/changelog.config 35 | 36 | - name: 🚀 changelog 37 | run: | 38 | git add changelog.md 39 | (git commit -m "🖉 Update changelog with ${GITHUB_REF#refs/*/}" && git push) || echo "Done" -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v2.2.0](https://github.com/devlooped/xunit.assemblyfixture/tree/v2.2.0) (2023-04-17) 4 | 5 | [Full Changelog](https://github.com/devlooped/xunit.assemblyfixture/compare/v2.1.1...v2.2.0) 6 | 7 | :hammer: Other: 8 | 9 | - Support for .net core 2+ [\#2](https://github.com/devlooped/xunit.assemblyfixture/issues/2) 10 | 11 | ## [v2.1.1](https://github.com/devlooped/xunit.assemblyfixture/tree/v2.1.1) (2022-09-09) 12 | 13 | [Full Changelog](https://github.com/devlooped/xunit.assemblyfixture/compare/v2.1.0...v2.1.1) 14 | 15 | ## [v2.1.0](https://github.com/devlooped/xunit.assemblyfixture/tree/v2.1.0) (2022-09-08) 16 | 17 | [Full Changelog](https://github.com/devlooped/xunit.assemblyfixture/compare/v2.1.0-beta...v2.1.0) 18 | 19 | :hammer: Other: 20 | 21 | - Real time discovery [\#6](https://github.com/devlooped/xunit.assemblyfixture/issues/6) 22 | 23 | ## [v2.1.0-beta](https://github.com/devlooped/xunit.assemblyfixture/tree/v2.1.0-beta) (2022-09-07) 24 | 25 | [Full Changelog](https://github.com/devlooped/xunit.assemblyfixture/compare/cadb4b304851eaedaab39ad882bd56f211789edc...v2.1.0-beta) 26 | 27 | :twisted_rightwards_arrows: Merged: 28 | 29 | - README.md fixes [\#1](https://github.com/devlooped/xunit.assemblyfixture/pull/1) (@tpodolak) 30 | 31 | 32 | 33 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 34 | -------------------------------------------------------------------------------- /src/Tests/Samples.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Xunit 4 | { 5 | public class Sample1 : IAssemblyFixture 6 | { 7 | MyAssemblyFixture fixture; 8 | 9 | // Fixtures are injectable into the test classes, just like with class and collection fixtures 10 | public Sample1(MyAssemblyFixture fixture) 11 | { 12 | this.fixture = fixture; 13 | } 14 | 15 | [Fact] 16 | public void EnsureSingleton() 17 | { 18 | Assert.Equal(1, MyAssemblyFixture.InstantiationCount); 19 | } 20 | } 21 | 22 | public class Sample2 : IAssemblyFixture 23 | { 24 | MyAssemblyFixture fixture; 25 | 26 | public Sample2(MyAssemblyFixture fixture) 27 | { 28 | this.fixture = fixture; 29 | } 30 | 31 | [Fact] 32 | public void EnsureSingleton() 33 | { 34 | Assert.Equal(1, MyAssemblyFixture.InstantiationCount); 35 | } 36 | } 37 | 38 | public class MyAssemblyFixture : IDisposable 39 | { 40 | public static int InstantiationCount; 41 | 42 | public MyAssemblyFixture() 43 | { 44 | InstantiationCount++; 45 | } 46 | 47 | public void Dispose() 48 | { 49 | // Uncomment this and it will surface as an assembly cleanup failure 50 | //throw new DivideByZeroException(); 51 | //InstantiationCount = 0; 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /.github/workflows/test/action.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | description: runs dotnet tests with retry 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: 🧪 test 7 | shell: bash --noprofile --norc {0} 8 | env: 9 | LC_ALL: en_US.utf8 10 | run: | 11 | [ -f .bash_profile ] && source .bash_profile 12 | counter=0 13 | exitcode=0 14 | reset="\e[0m" 15 | warn="\e[0;33m" 16 | while [ $counter -lt 6 ] 17 | do 18 | # run test and forward output also to a file in addition to stdout (tee command) 19 | if [ $filter ] 20 | then 21 | echo -e "${warn}Retry $counter for $filter ${reset}" 22 | dotnet test --no-build -m:1 --blame-hang --blame-hang-timeout 5m --filter=$filter | tee ./output.log 23 | else 24 | dotnet test --no-build -m:1 --blame-hang --blame-hang-timeout 5m | tee ./output.log 25 | fi 26 | # capture dotnet test exit status, different from tee 27 | exitcode=${PIPESTATUS[0]} 28 | if [ $exitcode == 0 ] 29 | then 30 | exit 0 31 | fi 32 | # cat output, get failed test names, remove trailing whitespace, sort+dedupe, join as FQN~TEST with |, remove trailing |. 33 | filter=$(cat ./output.log | grep -o -P '(?<=\sFailed\s)[\w\._]*' | sed 's/ *$//g' | sort -u | awk 'BEGIN { ORS="|" } { print("FullyQualifiedName~" $0) }' | grep -o -P '.*(?=\|$)') 34 | ((counter++)) 35 | done 36 | exit $exitcode 37 | -------------------------------------------------------------------------------- /src/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/AssemblyFixture/TestAssemblyRunner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Xunit.Abstractions; 7 | using Xunit.Sdk; 8 | 9 | namespace Xunit; 10 | 11 | class TestAssemblyRunner : XunitTestAssemblyRunner 12 | { 13 | readonly Dictionary assemblyFixtureMappings = new(); 14 | 15 | public TestAssemblyRunner(ITestAssembly testAssembly, 16 | IEnumerable testCases, 17 | IMessageSink diagnosticMessageSink, 18 | IMessageSink executionMessageSink, 19 | ITestFrameworkExecutionOptions executionOptions) 20 | : base(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions) 21 | { 22 | } 23 | 24 | protected override Task BeforeTestAssemblyFinishedAsync() 25 | { 26 | // Make sure we clean up everybody who is disposable, and use Aggregator.Run to isolate Dispose failures 27 | foreach (var disposable in assemblyFixtureMappings.Values.OfType()) 28 | Aggregator.Run(disposable.Dispose); 29 | 30 | return base.BeforeTestAssemblyFinishedAsync(); 31 | } 32 | 33 | 34 | protected override Task RunTestCollectionAsync(IMessageBus messageBus, 35 | ITestCollection testCollection, 36 | IEnumerable testCases, 37 | CancellationTokenSource cancellationTokenSource) 38 | => new TestCollectionRunner(assemblyFixtureMappings, testCollection, testCases, DiagnosticMessageSink, messageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync(); 39 | } 40 | -------------------------------------------------------------------------------- /AssemblyFixture.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32811.315 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssemblyFixture", "src\AssemblyFixture\AssemblyFixture.csproj", "{B2D6E6BF-63CC-4B36-BA99-F1743E7C5AE9}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssemblyFixture.Tests", "src\Tests\AssemblyFixture.Tests.csproj", "{8C6F430B-FA94-49D3-83E9-28C751211264}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F363283A-2BD8-4141-B573-9528B00ABC09}" 11 | ProjectSection(SolutionItems) = preProject 12 | readme.md = readme.md 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {B2D6E6BF-63CC-4B36-BA99-F1743E7C5AE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {B2D6E6BF-63CC-4B36-BA99-F1743E7C5AE9}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {B2D6E6BF-63CC-4B36-BA99-F1743E7C5AE9}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {B2D6E6BF-63CC-4B36-BA99-F1743E7C5AE9}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {8C6F430B-FA94-49D3-83E9-28C751211264}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {8C6F430B-FA94-49D3-83E9-28C751211264}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {8C6F430B-FA94-49D3-83E9-28C751211264}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {8C6F430B-FA94-49D3-83E9-28C751211264}.Release|Any CPU.Build.0 = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | GlobalSection(ExtensibilityGlobals) = postSolution 34 | SolutionGuid = {9B1CBFD9-BF47-456C-818C-45D62F29C0DE} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /src/AssemblyFixture/TestCollectionRunner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Xunit.Abstractions; 8 | using Xunit.Sdk; 9 | 10 | namespace Xunit; 11 | 12 | class TestCollectionRunner : XunitTestCollectionRunner 13 | { 14 | readonly Dictionary assemblyFixtureMappings; 15 | readonly IMessageSink diagnosticMessageSink; 16 | 17 | public TestCollectionRunner(Dictionary assemblyFixtureMappings, 18 | ITestCollection testCollection, 19 | IEnumerable testCases, 20 | IMessageSink diagnosticMessageSink, 21 | IMessageBus messageBus, 22 | ITestCaseOrderer testCaseOrderer, 23 | ExceptionAggregator aggregator, 24 | CancellationTokenSource cancellationTokenSource) 25 | : base(testCollection, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource) 26 | { 27 | this.assemblyFixtureMappings = assemblyFixtureMappings; 28 | this.diagnosticMessageSink = diagnosticMessageSink; 29 | } 30 | 31 | protected override Task RunTestClassAsync(ITestClass testClass, IReflectionTypeInfo @class, IEnumerable testCases) 32 | { 33 | foreach (var fixtureType in @class.Type.GetTypeInfo().ImplementedInterfaces 34 | .Where(i => i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == typeof(IAssemblyFixture<>)) 35 | .Select(i => i.GetTypeInfo().GenericTypeArguments.Single()) 36 | // First pass at filtering out before locking 37 | .Where(i => !assemblyFixtureMappings.ContainsKey(i))) 38 | { 39 | // ConcurrentDictionary's GetOrAdd does not lock around the value factory call, so we need 40 | // to do it ourselves. 41 | lock (assemblyFixtureMappings) 42 | if (!assemblyFixtureMappings.ContainsKey(fixtureType)) 43 | Aggregator.Run(() => assemblyFixtureMappings.Add(fixtureType, Activator.CreateInstance(fixtureType))); 44 | } 45 | 46 | // Don't want to use .Concat + .ToDictionary because of the possibility of overriding types, 47 | // so instead we'll just let collection fixtures override assembly fixtures. 48 | var combinedFixtures = new Dictionary(assemblyFixtureMappings); 49 | foreach (var kvp in CollectionFixtureMappings) 50 | combinedFixtures[kvp.Key] = kvp.Value; 51 | 52 | // We've done everything we need, so let the built-in types do the rest of the heavy lifting 53 | return new XunitTestClassRunner(testClass, @class, testCases, diagnosticMessageSink, MessageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), CancellationTokenSource, combinedFixtures).RunAsync(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Builds and runs tests in all three supported OSes 2 | # Pushes CI feed if secrets.SLEET_CONNECTION is provided 3 | 4 | name: build 5 | on: 6 | workflow_dispatch: 7 | push: 8 | branches: [ main, dev, 'dev/*', 'feature/*', 'rel/*' ] 9 | paths-ignore: 10 | - changelog.md 11 | - code-of-conduct.md 12 | - security.md 13 | - support.md 14 | - readme.md 15 | pull_request: 16 | types: [opened, synchronize, reopened] 17 | 18 | env: 19 | DOTNET_NOLOGO: true 20 | VersionPrefix: 42.42.${{ github.run_number }} 21 | VersionLabel: ${{ github.ref }} 22 | 23 | defaults: 24 | run: 25 | shell: bash 26 | 27 | jobs: 28 | os-matrix: 29 | runs-on: ubuntu-latest 30 | outputs: 31 | matrix: ${{ steps.lookup.outputs.matrix }} 32 | steps: 33 | - name: 🤘 checkout 34 | uses: actions/checkout@v2 35 | 36 | - name: 🔎 lookup 37 | id: lookup 38 | shell: pwsh 39 | run: | 40 | $path = './.github/workflows/os-matrix.json' 41 | $os = if (test-path $path) { cat $path } else { '["ubuntu-latest"]' } 42 | echo "matrix=$os" >> $env:GITHUB_OUTPUT 43 | 44 | build: 45 | needs: os-matrix 46 | name: build-${{ matrix.os }} 47 | runs-on: ${{ matrix.os }} 48 | strategy: 49 | matrix: 50 | os: ${{ fromJSON(needs.os-matrix.outputs.matrix) }} 51 | steps: 52 | - name: 🤘 checkout 53 | uses: actions/checkout@v2 54 | with: 55 | submodules: recursive 56 | fetch-depth: 0 57 | 58 | - name: 🙏 build 59 | run: dotnet build -m:1 60 | 61 | - name: ⚙ GNU grep 62 | if: matrix.os == 'macOS-latest' 63 | run: | 64 | brew install grep 65 | echo 'export PATH="/usr/local/opt/grep/libexec/gnubin:$PATH"' >> .bash_profile 66 | 67 | - name: 🧪 test 68 | uses: ./.github/workflows/test 69 | 70 | - name: 📦 pack 71 | run: dotnet pack -m:1 72 | 73 | # Only push CI package to sleet feed if building on ubuntu (fastest) 74 | - name: 🚀 sleet 75 | env: 76 | SLEET_CONNECTION: ${{ secrets.SLEET_CONNECTION }} 77 | if: env.SLEET_CONNECTION != '' 78 | run: | 79 | dotnet tool install -g --version 4.0.18 sleet 80 | sleet push bin --config none -f --verbose -p "SLEET_FEED_CONTAINER=nuget" -p "SLEET_FEED_CONNECTIONSTRING=${{ secrets.SLEET_CONNECTION }}" -p "SLEET_FEED_TYPE=azure" || echo "No packages found" 81 | 82 | dotnet-format: 83 | runs-on: ubuntu-latest 84 | steps: 85 | - name: 🤘 checkout 86 | uses: actions/checkout@v2 87 | with: 88 | submodules: recursive 89 | fetch-depth: 0 90 | 91 | - name: ✓ ensure format 92 | run: | 93 | dotnet format whitespace --verify-no-changes -v:diag --exclude ~/.nuget 94 | dotnet format style --verify-no-changes -v:diag --exclude ~/.nuget 95 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome:http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Don't use tabs for indentation. 7 | [*] 8 | indent_style = space 9 | # (Please don't specify an indent_size here; that has too many unintended consequences.) 10 | 11 | # Code files 12 | [*.{cs,csx,vb,vbx}] 13 | indent_size = 4 14 | 15 | # Xml project files 16 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,msbuildproj,props,targets}] 17 | indent_size = 2 18 | 19 | # Xml config files 20 | [*.{ruleset,config,nuspec,resx,vsixmanifest,vsct}] 21 | indent_size = 2 22 | 23 | # YAML files 24 | [*.{yaml,yml}] 25 | indent_size = 2 26 | 27 | # JSON files 28 | [*.json] 29 | indent_size = 2 30 | 31 | # Dotnet code style settings: 32 | [*.{cs,vb}] 33 | # Sort using and Import directives with System.* appearing first 34 | dotnet_sort_system_directives_first = true 35 | # Avoid "this." and "Me." if not necessary 36 | dotnet_style_qualification_for_field = false:suggestion 37 | dotnet_style_qualification_for_property = false:suggestion 38 | dotnet_style_qualification_for_method = false:suggestion 39 | dotnet_style_qualification_for_event = false:suggestion 40 | 41 | # Use language keywords instead of framework type names for type references 42 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 43 | dotnet_style_predefined_type_for_member_access = true:suggestion 44 | 45 | # Suggest more modern language features when available 46 | dotnet_style_object_initializer = true:suggestion 47 | dotnet_style_collection_initializer = true:suggestion 48 | dotnet_style_coalesce_expression = true:suggestion 49 | dotnet_style_null_propagation = true:suggestion 50 | dotnet_style_explicit_tuple_names = true:suggestion 51 | 52 | # CSharp code style settings: 53 | 54 | # IDE0040: Add accessibility modifiers 55 | dotnet_style_require_accessibility_modifiers = omit_if_default:error 56 | 57 | # IDE0040: Add accessibility modifiers 58 | dotnet_diagnostic.IDE0040.severity = error 59 | 60 | [*.cs] 61 | # Top-level files are definitely OK 62 | csharp_using_directive_placement = outside_namespace:silent 63 | csharp_style_namespace_declarations = block_scoped:silent 64 | csharp_prefer_simple_using_statement = true:suggestion 65 | csharp_prefer_braces = true:silent 66 | 67 | # Prefer "var" everywhere 68 | csharp_style_var_for_built_in_types = true:suggestion 69 | csharp_style_var_when_type_is_apparent = true:suggestion 70 | csharp_style_var_elsewhere = true:suggestion 71 | 72 | # Prefer method-like constructs to have an expression-body 73 | csharp_style_expression_bodied_methods = true:none 74 | csharp_style_expression_bodied_constructors = true:none 75 | csharp_style_expression_bodied_operators = true:none 76 | 77 | # Prefer property-like constructs to have an expression-body 78 | csharp_style_expression_bodied_properties = true:none 79 | csharp_style_expression_bodied_indexers = true:none 80 | csharp_style_expression_bodied_accessors = true:none 81 | 82 | # Suggest more modern language features when available 83 | csharp_style_pattern_matching_over_is_with_cast_check = true:error 84 | csharp_style_pattern_matching_over_as_with_null_check = true:error 85 | csharp_style_inlined_variable_declaration = true:suggestion 86 | csharp_style_throw_expression = true:suggestion 87 | csharp_style_conditional_delegate_call = true:suggestion 88 | 89 | # Newline settings 90 | csharp_new_line_before_open_brace = all 91 | csharp_new_line_before_else = true 92 | csharp_new_line_before_catch = true 93 | csharp_new_line_before_finally = true 94 | csharp_new_line_before_members_in_object_initializers = true 95 | csharp_new_line_before_members_in_anonymous_types = true 96 | 97 | # Test settings 98 | [**/*Tests*/**{.cs,.vb}] 99 | # xUnit1013: Public method should be marked as test. Allows using records as test classes 100 | dotnet_diagnostic.xUnit1013.severity = none 101 | 102 | # Default severity for analyzer diagnostics with category 'Style' 103 | dotnet_analyzer_diagnostic.category-Style.severity = none 104 | 105 | # VSTHRD200: Use "Async" suffix for async methods 106 | dotnet_diagnostic.VSTHRD200.severity = none 107 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # xunit.assemblyfixture 2 | 3 | Provides shared state/fixture data across tests in the same assembly, following the design of 4 | [class fixtures](https://xunit.github.io/docs/shared-context.html#class-fixture) (rather than the more 5 | convoluted [collection fixtures](https://xunit.github.io/docs/shared-context.html#collection-fixture)). 6 | 7 | To complement [xUnit documentation style](https://xunit.github.io/docs/shared-context.html), I shamelessly copy its layout here. 8 | 9 | > NOTE: applies to xunit 2.4 only. 3.0+ will have its own way of doing this. 10 | 11 | ## Shared Context between Tests 12 | 13 | Please read [xUnit documentation](https://xunit.github.io/docs/shared-context.html) on shared context and the various built-in options, which are: 14 | 15 | - [Constructor and Dispose](https://xunit.github.io/docs/shared-context.html#constructor) (shared setup/cleanup code without sharing object instances) 16 | - [Class Fixtures](https://xunit.github.io/docs/shared-context.html#class-fixture) (shared object instance across tests in a single class) 17 | - [Collection Fixtures](https://xunit.github.io/docs/shared-context.html#collection-fixture) (shared object instances across multiple test classes 18 | 19 | To which this project adds: 20 | 21 | - Assembly Fixtures (shared object instances across multiple test classes within the same test assembly) 22 | 23 | ### Assembly Fixtures 24 | 25 | ***When to use***: when you want to create a single assembly-level context 26 | and share it among all tests in the assembly, and have it cleaned up after 27 | all the tests in the assembly have finished. 28 | 29 | Sometimes test context creation and cleanup can be very expensive. If you were 30 | to run the creation and cleanup code during every test, it might make the tests 31 | slower than you want. Sometimes, you just need to aggregate data across multiple 32 | tests in multiple classes. You can use the *assembly fixture* feature of 33 | [xUnit.net [Assembly Fixtures]](https://www.nuget.org/packages/xunit.assemblyfixture) 34 | to share a single object instance among all tests in a test assembly. 35 | 36 | When using an assembly fixture, xUnit.net will ensure that the fixture instance 37 | will be created before any of the tests using it have run, and once all the tests 38 | have finished, it will clean up the fixture object by calling `Dispose`, if present. 39 | 40 | To use assembly fixtures, you need to take the following steps: 41 | 42 | - Create the fixture class, and put the the startup code in the fixture 43 | class constructor. 44 | - If the fixture class needs to perform cleanup, implement `IDisposable` 45 | on the fixture class, and put the cleanup code in the `Dispose()` method. 46 | - Add `IAssemblyFixture` to the test class. 47 | - If the test class needs access to the fixture instance, add it as a 48 | constructor argument, and it will be provided automatically. 49 | 50 | Here is a simple example: 51 | 52 | ```csharp 53 | public class DatabaseFixture : IDisposable 54 | { 55 | public DatabaseFixture() 56 | { 57 | Db = new SqlConnection("MyConnectionString"); 58 | 59 | // ... initialize data in the test database ... 60 | } 61 | 62 | public void Dispose() 63 | { 64 | // ... clean up test data from the database ... 65 | } 66 | 67 | public SqlConnection Db { get; private set; } 68 | } 69 | 70 | public class MyDatabaseTests : IAssemblyFixture 71 | { 72 | DatabaseFixture fixture; 73 | 74 | public MyDatabaseTests(DatabaseFixture fixture) 75 | { 76 | this.fixture = fixture; 77 | } 78 | 79 | // ... write tests, using fixture.Db to get access to the SQL Server ... 80 | } 81 | ``` 82 | 83 | Just before the first test in the assembly to require the `DatabaseFixture` 84 | is run, xUnit.net will create an instance of `DatabaseFixture`. Each subsequent 85 | test will receive the same shared instance, passed to the constructor of 86 | `MyDatabaseTests`, just like a static singleton, but with predictable cleanup 87 | via `IDisposable`. 88 | 89 | ***Important note:*** xUnit.net uses the presence of the interface 90 | `IAssemblyFixture<>` to know that you want an assembly fixture to 91 | be created and cleaned up. It will do this whether you take the instance of 92 | the class as a constructor argument or not. Simiarly, if you add the constructor 93 | argument but forget to add the interface, xUnit.net will let you know that it 94 | does not know how to satisfy the constructor argument. 95 | 96 | If you need multiple fixture objects, you can implement the interface as many 97 | times as you want, and add constructor arguments for whichever of the fixture 98 | object instances you need access to. The order of the constructor arguments 99 | is unimportant. 100 | 101 | Note that you cannot control the order that fixture objects are created, and 102 | fixtures cannot take dependencies on other fixtures. If you have need to 103 | control creation order and/or have dependencies between fixtures, you should 104 | create a class which encapsulates the other two fixtures, so that it can 105 | do the object creation itself. -------------------------------------------------------------------------------- /.netconfig: -------------------------------------------------------------------------------- 1 | [file] 2 | url = https://github.com/devlooped/oss 3 | [file ".netconfig"] 4 | url = https://github.com/devlooped/oss/blob/main/.netconfig 5 | skip 6 | [file "readme.md"] 7 | url = https://github.com/devlooped/oss/blob/main/readme.md 8 | skip 9 | [file "src/icon.png"] 10 | url = https://github.com/devlooped/oss/blob/main/src/icon.png 11 | skip 12 | [file ".github/ISSUE_TEMPLATE/config.yml"] 13 | url = https://github.com/devlooped/oss/blob/main/.github/ISSUE_TEMPLATE/config.yml 14 | skip 15 | [file ".github/workflows/release-artifacts.yml"] 16 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/release-artifacts.yml 17 | skip 18 | [file "assets/images/sponsors.svg"] 19 | url = https://github.com/devlooped/oss/blob/main/assets/images/sponsors.svg 20 | skip 21 | [file "assets/images/sponsors.png"] 22 | url = https://github.com/devlooped/oss/blob/main/assets/images/sponsors.png 23 | skip 24 | [file ".editorconfig"] 25 | url = https://github.com/devlooped/oss/blob/main/.editorconfig 26 | sha = fd5b554bf3538a3c92a0b49e395c8ad2e8429158 27 | etag = bf02d1679442e5169d03304164b8e9407997ac3132145831a91ba61fc8b50687 28 | weak 29 | [file ".gitattributes"] 30 | url = https://github.com/devlooped/oss/blob/main/.gitattributes 31 | sha = 0683ee777d7d878d4bf013d7deea352685135a05 32 | etag = 7acb32f5fa6d4ccd9c824605a7c2b8538497f0068c165567807d393dcf4d6bb7 33 | weak 34 | [file ".github/dependabot.yml"] 35 | url = https://github.com/devlooped/oss/blob/main/.github/dependabot.yml 36 | sha = 4f070a477b4162a280f02722ae666376ae4fcc71 37 | etag = 35f2134fff3b0235ff8dac8618a76198c8ef533ad2f29628bbb435cd1134d638 38 | weak 39 | [file ".github/workflows/build.yml"] 40 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/build.yml 41 | sha = 13d67e2cf3f786c8189364fd29332aaa7dc575dc 42 | etag = c616df0877fba60002ccfc0397e9f731ddb22acbbb195a0598fedd4cac5f3135 43 | weak 44 | [file ".github/workflows/changelog.yml"] 45 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/changelog.yml 46 | sha = a4b66eb5f4dfb9704502f19f59ba33cb4855188c 47 | etag = 54c0b571648b1055beb3ddac180b34e93a9869b9f0277de306901b2c1dbe0b2c 48 | weak 49 | [file ".github/workflows/dotnet-file.yml"] 50 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/dotnet-file.yml 51 | sha = f08c3f28e46e28eb31e70846d65e57aa9553ce56 52 | etag = 567444486383d032c1c5fbc538f07e860f92b1d08c66ac6ffb1db64ca539251c 53 | weak 54 | [file ".github/workflows/includes.yml"] 55 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/includes.yml 56 | sha = ac753b791d03997eb655efb26ae141b51addd1c0 57 | etag = fcd94a08ac9ebc0e8351deac4e7f085cf8ef67816cc50006e068f44166096eb8 58 | weak 59 | [file ".github/workflows/publish.yml"] 60 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/publish.yml 61 | sha = d3022567c9ef2bc9461511e53b8abe065afdf03b 62 | etag = 58601b5a71c805647ab26e84053acdfb8d174eaa93330487af8a5503753c5707 63 | weak 64 | [file ".github/workflows/test/action.yml"] 65 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/test/action.yml 66 | sha = 9a1b07589b9bde93bc12528e9325712a32dec418 67 | etag = b54216ac431a83ce5477828d391f02046527e7f6fffd21da1d03324d352c3efb 68 | weak 69 | [file ".gitignore"] 70 | url = https://github.com/devlooped/oss/blob/main/.gitignore 71 | sha = b87a8a795a4c2b6830602225c066c11108552a99 72 | etag = 96e0860052044780f1fc9e3bdfbee09d82d5dddb8b1217d67460fc7330a64dd8 73 | weak 74 | [file "Directory.Build.rsp"] 75 | url = https://github.com/devlooped/oss/blob/main/Directory.Build.rsp 76 | sha = ae25fae9d7daf0cb47d537ba870914aa3052f0c9 77 | etag = 6a6c6e1d3895df953abf14c82b0899e3eea75cdcd679f6212dcfea15183d73d6 78 | weak 79 | [file "_config.yml"] 80 | url = https://github.com/devlooped/oss/blob/main/_config.yml 81 | sha = fa83a5161ba52bc5d510ce0ba75ee0b1f8d4bc63 82 | etag = 9139148f845adf503fd3c3c140eb64421fc476a1f9c027fc50825c0efb05f557 83 | weak 84 | [file "assets/css/style.scss"] 85 | url = https://github.com/devlooped/oss/blob/main/assets/css/style.scss 86 | sha = 9db26e2710b084d219d6355339d822f159bf5780 87 | etag = f710d8919abfd5a8d00050b74ba7d0bb05c6d02e40842a3012eb96555c208504 88 | weak 89 | [file "license.txt"] 90 | url = https://github.com/devlooped/oss/blob/main/license.txt 91 | sha = 0683ee777d7d878d4bf013d7deea352685135a05 92 | etag = 2c6335b37e4ae05eea7c01f5d0c9d82b49c488f868a8b5ba7bff7c6ff01f3994 93 | weak 94 | [file "src/Directory.Build.props"] 95 | url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.props 96 | sha = 6ae80a175a8f926ac5d9ffb0f6afd55d85cc9320 97 | etag = 69d4b16c14d5047b3ed812dbf556b0b8d77deb86f73af04b9bd3640220056fa8 98 | weak 99 | [file "src/Directory.Build.targets"] 100 | url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.targets 101 | sha = 1514d15399a7d545ad92a0e9d57dc8295fdd6af8 102 | etag = 428f80b0786ff17b836c7a5b0640948724855d17933e958642b22849ac00dadb 103 | weak 104 | [file "src/kzu.snk"] 105 | url = https://github.com/devlooped/oss/blob/main/src/kzu.snk 106 | skip 107 | [file "src/nuget.config"] 108 | url = https://github.com/devlooped/oss/blob/main/src/nuget.config 109 | sha = b2fa09bd9db6de89e37a8ba6705b5659e435dafd 110 | etag = eb2d09e546aa1e11c0b464d9ed6ab2d3c028a1d86c3ac40a318053625fb72819 111 | weak 112 | [file ".github/release.yml"] 113 | url = https://github.com/devlooped/oss/blob/main/.github/release.yml 114 | sha = 1afd173fe8f81b510c597737b0d271218e81fa73 115 | etag = 482dc2c892fc7ce0cb3a01eb5d9401bee50ddfb067d8cb85873555ce63cf5438 116 | weak 117 | [file ".github/workflows/changelog.config"] 118 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/changelog.config 119 | sha = 055a8b7c94b74ae139cce919d60b83976d2a9942 120 | etag = ddb17acb5872e9e69a76f9dec0ca590f25382caa2ccf750df058dcabb674db2b 121 | weak 122 | [file ".github/workflows/combine-prs.yml"] 123 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/combine-prs.yml 124 | sha = c1610886eba42cb250e3894aed40c0a258cd383d 125 | etag = 598ee294649a44d4c5d5934416c01183597d08aa7db7938453fd2bbf52a4e64d 126 | weak 127 | [file ".github/workflows/sponsor.yml"] 128 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/sponsor.yml 129 | sha = 8990ebb36199046e0b8098bad9e46dcef739c56e 130 | etag = e1dc114d2e8b57d50649989d32dbf0c9080ec77da3738a4cc79e9256d6ca5d3e 131 | weak 132 | -------------------------------------------------------------------------------- /.github/workflows/combine-prs.yml: -------------------------------------------------------------------------------- 1 | # Source: https://github.com/hrvey/combine-prs-workflow 2 | # Tweaks: regex support for branch 3 | 4 | name: '⛙ combine-prs' 5 | 6 | on: 7 | workflow_dispatch: 8 | inputs: 9 | branchExpression: 10 | description: 'Regular expression to match against PR branches to find combinable PRs' 11 | required: true 12 | default: 'dependabot' 13 | mustBeGreen: 14 | description: 'Only combine PRs that are green (status is success)' 15 | required: true 16 | default: true 17 | combineTitle: 18 | description: 'Title of the combined PR' 19 | required: true 20 | default: '⬆️ Bump dependencies' 21 | combineBranchName: 22 | description: 'Name of the branch to combine PRs into' 23 | required: true 24 | default: 'combine-prs' 25 | ignoreLabel: 26 | description: 'Exclude PRs with this label' 27 | required: true 28 | default: 'nocombine' 29 | 30 | jobs: 31 | combine-prs: 32 | name: ${{ github.event.inputs.combineBranchName }} 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/github-script@v6 36 | with: 37 | github-token: ${{secrets.GITHUB_TOKEN}} 38 | script: | 39 | const pulls = await github.paginate('GET /repos/:owner/:repo/pulls', { 40 | owner: context.repo.owner, 41 | repo: context.repo.repo 42 | }); 43 | const branchRegExp = new RegExp(`${{github.event.inputs.branchExpression}}`); 44 | let branchesAndPRStrings = []; 45 | let baseBranch = null; 46 | let baseBranchSHA = null; 47 | for (const pull of pulls) { 48 | const branch = pull['head']['ref']; 49 | console.log('Pull for branch: ' + branch); 50 | if (branchRegExp.test(branch)) { 51 | console.log('Branch matched: ' + branch); 52 | let statusOK = true; 53 | if(${{ github.event.inputs.mustBeGreen }}) { 54 | console.log('Checking green status: ' + branch); 55 | const stateQuery = `query($owner: String!, $repo: String!, $pull_number: Int!) { 56 | repository(owner: $owner, name: $repo) { 57 | pullRequest(number:$pull_number) { 58 | commits(last: 1) { 59 | nodes { 60 | commit { 61 | statusCheckRollup { 62 | state 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | }` 70 | const vars = { 71 | owner: context.repo.owner, 72 | repo: context.repo.repo, 73 | pull_number: pull['number'] 74 | }; 75 | const result = await github.graphql(stateQuery, vars); 76 | const [{ commit }] = result.repository.pullRequest.commits.nodes; 77 | const state = commit.statusCheckRollup.state 78 | console.log('Validating status: ' + state); 79 | if(state != 'SUCCESS') { 80 | console.log('Discarding ' + branch + ' with status ' + state); 81 | statusOK = false; 82 | } 83 | } 84 | console.log('Checking labels: ' + branch); 85 | const labels = pull['labels']; 86 | for(const label of labels) { 87 | const labelName = label['name']; 88 | console.log('Checking label: ' + labelName); 89 | if(labelName == '${{ github.event.inputs.ignoreLabel }}') { 90 | console.log('Discarding ' + branch + ' with label ' + labelName); 91 | statusOK = false; 92 | } 93 | } 94 | if (statusOK) { 95 | console.log('Adding branch to array: ' + branch); 96 | const prString = '#' + pull['number'] + ' ' + pull['title']; 97 | branchesAndPRStrings.push({ branch, prString }); 98 | baseBranch = pull['base']['ref']; 99 | baseBranchSHA = pull['base']['sha']; 100 | } 101 | } 102 | } 103 | if (branchesAndPRStrings.length == 0) { 104 | core.setFailed('No PRs/branches matched criteria'); 105 | return; 106 | } 107 | if (branchesAndPRStrings.length == 1) { 108 | core.setFailed('Only one PR/branch matched criteria'); 109 | return; 110 | } 111 | 112 | try { 113 | await github.rest.git.createRef({ 114 | owner: context.repo.owner, 115 | repo: context.repo.repo, 116 | ref: 'refs/heads/' + '${{ github.event.inputs.combineBranchName }}', 117 | sha: baseBranchSHA 118 | }); 119 | } catch (error) { 120 | console.log(error); 121 | core.setFailed('Failed to create combined branch - maybe a branch by that name already exists?'); 122 | return; 123 | } 124 | 125 | let combinedPRs = []; 126 | let mergeFailedPRs = []; 127 | for(const { branch, prString } of branchesAndPRStrings) { 128 | try { 129 | await github.rest.repos.merge({ 130 | owner: context.repo.owner, 131 | repo: context.repo.repo, 132 | base: '${{ github.event.inputs.combineBranchName }}', 133 | head: branch, 134 | }); 135 | console.log('Merged branch ' + branch); 136 | combinedPRs.push(prString); 137 | } catch (error) { 138 | console.log('Failed to merge branch ' + branch); 139 | mergeFailedPRs.push(prString); 140 | } 141 | } 142 | 143 | console.log('Creating combined PR'); 144 | const combinedPRsString = combinedPRs.join('\n'); 145 | let body = '⛙ Combined PRs:\n' + combinedPRsString; 146 | if(mergeFailedPRs.length > 0) { 147 | const mergeFailedPRsString = mergeFailedPRs.join('\n'); 148 | body += '\n\n⚠️ The following PRs were left out due to merge conflicts:\n' + mergeFailedPRsString 149 | } 150 | await github.rest.pulls.create({ 151 | owner: context.repo.owner, 152 | repo: context.repo.repo, 153 | title: '⛙ ${{github.event.inputs.combineTitle}}', 154 | head: '${{ github.event.inputs.combineBranchName }}', 155 | base: baseBranch, 156 | body: body 157 | }); 158 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | true 14 | 15 | 16 | 17 | 18 | $(CI) 19 | 20 | 21 | 22 | Daniel Cazzulino 23 | Copyright (C) Daniel Cazzulino and Contributors. All rights reserved. 24 | false 25 | MIT 26 | 27 | 28 | icon.png 29 | readme.md 30 | 31 | icon.png 32 | readme.md 33 | 34 | true 35 | true 36 | 37 | $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\bin')) 38 | 39 | 40 | true 41 | 42 | 43 | true 44 | 45 | 46 | 47 | Release 48 | true 49 | false 50 | Latest 51 | 52 | 53 | false 54 | 55 | embedded 56 | true 57 | enable 58 | 59 | strict 60 | 61 | 62 | $(MSBuildProjectName) 63 | $(MSBuildProjectName.IndexOf('.')) 64 | $(MSBuildProjectName.Substring(0, $(RootNamespaceDot))) 65 | 66 | 67 | $(DefaultItemExcludes);*.binlog;*.zip;*.rsp;*.items;**/TestResults/**/*.* 68 | 69 | true 70 | true 71 | true 72 | true 73 | 74 | 75 | true 76 | 77 | 78 | false 79 | 80 | 81 | NU5105;$(NoWarn) 82 | 83 | true 84 | 85 | 86 | true 87 | 88 | 89 | LatestMinor 90 | 91 | 92 | 93 | 94 | $(MSBuildThisFileDirectory)kzu.snk 95 | 101 | 002400000480000094000000060200000024000052534131000400000100010051155fd0ee280be78d81cc979423f1129ec5dd28edce9cd94fd679890639cad54c121ebdb606f8659659cd313d3b3db7fa41e2271158dd602bb0039a142717117fa1f63d93a2d288a1c2f920ec05c4858d344a45d48ebd31c1368ab783596b382b611d8c92f9c1b3d338296aa21b12f3bc9f34de87756100c172c52a24bad2db 102 | 00352124762f2aa5 103 | true 104 | 105 | 106 | 107 | 115 | 42.42.42 116 | 117 | 118 | 119 | <_VersionLabel>$(VersionLabel.Replace('refs/heads/', '')) 120 | 121 | <_VersionLabel Condition="$(_VersionLabel.Contains('refs/pull/'))">$(VersionLabel.TrimEnd('.0123456789')) 122 | 123 | <_VersionLabel>$(_VersionLabel.Replace('refs/pull/', 'pr')) 124 | 125 | <_VersionLabel>$(_VersionLabel.Replace('/merge', '')) 126 | 127 | <_VersionLabel>$(_VersionLabel.Replace('/', '-')) 128 | 129 | 130 | $(_VersionLabel) 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CI;$(DefineConstants) 6 | 7 | 8 | 9 | true 10 | true 11 | 12 | 13 | 14 | 24 | false 25 | true 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 39 | 40 | 41 | 44 | 45 | 48 | 49 | 50 | 52 | 53 | 1.0.0 54 | $(VersionPrefix)-$(VersionSuffix) 55 | $(VersionPrefix) 56 | 57 | 58 | 59 | 60 | $(PackFolder) 61 | $(PackFolderPath.Replace('\$(TargetFramework)', '')) 62 | $(IntermediateOutputPath)$(PackFolderPath)\ 63 | $(OutputPath)$(PackFolderPath)\ 64 | $(OutputPath) 65 | 66 | 67 | 68 | 69 | pr$(GITHUB_REF.Replace('refs/pull/', '').Replace('/merge', '')) 70 | $(GITHUB_REF.Replace('refs/heads/', '').Replace('refs/tags/', '')) 71 | 72 | $(BUILD_SOURCEBRANCH.Replace('refs/heads/', '').Replace('refs/tags/', '')) 73 | 74 | pr$(APPVEYOR_PULL_REQUEST_NUMBER) 75 | $(APPVEYOR_REPO_TAG_NAME) 76 | $(APPVEYOR_REPO_BRANCH) 77 | 78 | $(TEAMCITY_BUILD_BRANCH) 79 | 80 | pr$(TRAVIS_PULL_REQUEST) 81 | $(TRAVIS_BRANCH) 82 | 83 | pr$(CIRCLE_PR_NUMBER) 84 | $(CIRCLE_TAG) 85 | $(CIRCLE_BRANCH) 86 | 87 | $(CI_COMMIT_TAG) 88 | pr$(CI_MERGE_REQUEST_IID) 89 | pr$(CI_EXTERNAL_PULL_REQUEST_IID) 90 | $(CI_COMMIT_BRANCH) 91 | 92 | pr$(BUDDY_EXECUTION_PULL_REQUEST_NO) 93 | $(BUDDY_EXECUTION_TAG) 94 | $(BUDDY_EXECUTION_BRANCH) 95 | 96 | 97 | 98 | 99 | PrepareResources;$(CoreCompileDependsOn) 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | MSBuild:Compile 110 | $(IntermediateOutputPath)\$([MSBuild]::ValueOrDefault('%(RelativeDir)', '').Replace('\', '.').Replace('/', '.'))%(Filename).g$(DefaultLanguageSourceExtension) 111 | $(Language) 112 | $(RootNamespace) 113 | $(RootNamespace).$([MSBuild]::ValueOrDefault('%(RelativeDir)', '').Replace('\', '.').Replace('/', '.').TrimEnd('.')) 114 | %(Filename) 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 138 | 139 | 140 | 141 | $(PrivateRepositoryUrl) 142 | 143 | 144 | 145 | $(SourceRevisionId) 146 | $(SourceRevisionId.Substring(0, 9)) 147 | 148 | $(RepositorySha) 149 | 150 | 151 | 152 | 153 | <_GitSourceRoot Include="@(SourceRoot -> WithMetadataValue('SourceControl', 'git'))" /> 154 | 155 | 156 | 157 | @(_GitSourceRoot) 158 | 159 | 160 | 161 | 162 | 167 | 168 | $(RepositoryUrl) 169 | $(Description) 170 | $(RepositoryUrl)/blob/main/changelog.md 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | --------------------------------------------------------------------------------