diff --git a/NUnitConsole.sln b/NUnitConsole.sln index 4b047daea..d2766917b 100644 --- a/NUnitConsole.sln +++ b/NUnitConsole.sln @@ -30,9 +30,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution nunit.ico = nunit.ico package-checks.cake = package-checks.cake package-tests.cake = package-tests.cake + packages.cake = packages.cake README.md = README.md test-results.cake = test-results.cake VERSIONING.md = VERSIONING.md + cake\zip-package.cake = cake\zip-package.cake EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nuget", "nuget", "{A972031D-2F61-4183-AF75-99EE1A9F6B32}" @@ -85,9 +87,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "choco", "choco", "{4FDF7BFA choco\nunit-agent-x86.exe.ignore = choco\nunit-agent-x86.exe.ignore choco\nunit-agent.exe.ignore = choco\nunit-agent.exe.ignore choco\nunit-console-runner.nuspec = choco\nunit-console-runner.nuspec - choco\VERIFICATION.txt = choco\VERIFICATION.txt choco\nunit.console.choco.addins = choco\nunit.console.choco.addins choco\nunit.console.choco.agent.addins = choco\nunit.console.choco.agent.addins + choco\VERIFICATION.txt = choco\VERIFICATION.txt EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "deprecated", "deprecated", "{9A7C8370-ED1F-486F-A8F5-C5BF4221464E}" @@ -107,20 +109,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "deprecated", "deprecated", choco\deprecated\nunit-console-with-extensions.nuspec = choco\deprecated\nunit-console-with-extensions.nuspec EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cake", "cake", "{9BCA00E2-D072-424B-A6DF-5BECF719C1FB}" - ProjectSection(SolutionItems) = preProject - cake\constants.cake = cake\constants.cake - cake\dotnet.cake = cake\dotnet.cake - cake\header-check.cake = cake\header-check.cake - cake\package-checks.cake = cake\package-checks.cake - cake\package-definitions.cake = cake\package-definitions.cake - cake\package-tester.cake = cake\package-tester.cake - cake\package-tests.cake = cake\package-tests.cake - cake\test-results.cake = cake\test-results.cake - cake\utilities.cake = cake\utilities.cake - cake\versioning.cake = cake\versioning.cake - EndProjectSection -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "msi", "msi", "{0C0D20CE-70CD-4CEF-BE9B-AEB8A2DE9C8A}" ProjectSection(SolutionItems) = preProject msi\nunit-install.sln = msi\nunit-install.sln @@ -168,6 +156,39 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nunit", "nunit", "{93E5CAF4 msi\nunit\variables.wxi = msi\nunit\variables.wxi EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cake", "cake", "{D6449B7A-20FF-467B-A65E-174DD6992AEB}" + ProjectSection(SolutionItems) = preProject + cake\banner.cake = cake\banner.cake + cake\build-settings.cake = cake\build-settings.cake + cake\builder.cake = cake\builder.cake + cake\chocolatey-package.cake = cake\chocolatey-package.cake + cake\command-line-options.cake = cake\command-line-options.cake + cake\constants.cake = cake\constants.cake + cake\dotnet.cake = cake\dotnet.cake + cake\extending.cake = cake\extending.cake + cake\headers.cake = cake\headers.cake + cake\help-messages.cake = cake\help-messages.cake + cake\known-extensions.cake = cake\known-extensions.cake + cake\msi-package.cake = cake\msi-package.cake + cake\nuget-package.cake = cake\nuget-package.cake + cake\package-checks.cake = cake\package-checks.cake + cake\package-definition.cake = cake\package-definition.cake + cake\package-reference.cake = cake\package-reference.cake + cake\package-test.cake = cake\package-test.cake + cake\publishing.cake = cake\publishing.cake + cake\setup.cake = cake\setup.cake + cake\task-definitions.cake = cake\task-definitions.cake + cake\test-reports.cake = cake\test-reports.cake + cake\task-builders.cake = cake\task-builders.cake + cake\test-results.cake = cake\test-results.cake + cake\test-runners.cake = cake\test-runners.cake + cake\tools.cake = cake\tools.cake + cake\unit-testing.cake = cake\unit-testing.cake + cake\utilities.cake = cake\utilities.cake + cake\versioning.cake = cake\versioning.cake + cake\zip-package.cake = cake\zip-package.cake + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -244,7 +265,6 @@ Global {333D2FBC-CCA7-46AF-9453-C310671A67B0} = {31B45C4C-206F-4F31-9CC6-33BF11DFEE39} {9D3015EE-5B84-41B3-A1D3-1A439370C392} = {31B45C4C-206F-4F31-9CC6-33BF11DFEE39} {068F6CA9-6108-4F45-8540-351AA5227259} = {4FDF7BFA-A337-41D3-898D-C6A98278E6AD} - {9BCA00E2-D072-424B-A6DF-5BECF719C1FB} = {49D441DF-39FD-4F4D-AECA-86CF8EFE23AF} {0C0D20CE-70CD-4CEF-BE9B-AEB8A2DE9C8A} = {49D441DF-39FD-4F4D-AECA-86CF8EFE23AF} {20005864-BE82-412D-99BF-288E2D8370E9} = {49D441DF-39FD-4F4D-AECA-86CF8EFE23AF} {CACC0520-B452-4310-A33C-DC944129ACDD} = {31B45C4C-206F-4F31-9CC6-33BF11DFEE39} @@ -253,6 +273,7 @@ Global {50371E48-BEC3-4D53-BD37-F3A6149CFD0D} = {25DA12FE-6209-4524-9A37-8E51F815E198} {C5B7120C-190B-4C38-95CB-83F12799598D} = {49D441DF-39FD-4F4D-AECA-86CF8EFE23AF} {93E5CAF4-D5DB-4915-AF0F-908A6043E462} = {0C0D20CE-70CD-4CEF-BE9B-AEB8A2DE9C8A} + {D6449B7A-20FF-467B-A65E-174DD6992AEB} = {49D441DF-39FD-4F4D-AECA-86CF8EFE23AF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D8E4FC26-5422-4C51-8BBC-D1AC0A578711} diff --git a/build.cake b/build.cake index 814abff27..72dc92264 100644 --- a/build.cake +++ b/build.cake @@ -1,795 +1,456 @@ -static string Target; Target = GetArgument("target|t", "Default"); -static string Configuration; Configuration = GetArgument("configuration|c", "Release"); -static bool NoPush; NoPush = HasArgument("nopush"); - -#load cake/constants.cake -#load cake/dotnet.cake -#load cake/header-check.cake -#load cake/package-checks.cake -#load cake/test-results.cake -#load cake/package-tests.cake -#load cake/package-tester.cake -#load cake/versioning.cake -#load cake/utilities.cake -#load cake/package-definitions.cake - -// Install Tools -#tool NuGet.CommandLine&version=6.9.1 -#tool dotnet:?package=GitVersion.Tool&version=5.12.0 -#tool dotnet:?package=GitReleaseManager.Tool&version=0.12.1 - -BuildVersion _buildVersion; -string ProductVersion => _buildVersion.ProductVersion; -string SemVer => _buildVersion.SemVer; -string PreReleaseLabel => _buildVersion.PreReleaseLabel; -bool IsReleaseBranch => _buildVersion.IsReleaseBranch; - -var UnreportedErrors = new List(); -var installedNetCoreRuntimes = GetInstalledNetCoreRuntimes(); +// Load scripts +#load cake/*.cake + +// Initialize BuildSettings +BuildSettings.Initialize( + Context, + "NUnit Console and Engine", + "nunit-console", + solutionFile: "NUnitConsole.sln", + exemptFiles: new [] { "Options.cs", "ProcessUtils.cs", "ProcessUtilsTests.cs" }, + unitTests: "**/*.tests.exe|**/nunit3-console.tests.dll", + unitTestRunner: new CustomTestRunner() ); + +////////////////////////////////////////////////////////////////////// +// PACKAGE TEST LISTS +////////////////////////////////////////////////////////////////////// + + // Tests run for all runner packages except NETCORE runner + var StandardRunnerTests = new List + { + Net35Test, + Net35X86Test, + Net40Test, + Net40X86Test, + Net35PlusNet40Test, + NetCore31Test, + Net50Test, + Net60Test, + Net70Test, + Net80Test, + Net50PlusNet60Test, + Net40PlusNet60Test, + NUnitProjectTest + }; + + // Tests run for the NETCORE runner package + var NetCoreRunnerTests = new List + { + NetCore31Test, + Net50Test, + Net60Test, + Net70Test, + Net80Test, + }; + + const string DOTNET_EXE_X86 = @"C:\Program Files (x86)\dotnet\dotnet.exe"; + bool dotnetX86Available = IsRunningOnWindows() && System.IO.File.Exists(DOTNET_EXE_X86); + + // TODO: Remove the limitation to Windows + if (IsRunningOnWindows() && dotnetX86Available) + { + StandardRunnerTests.Add(Net60X86Test); + // TODO: Make these tests run on AppVeyor + if (!BuildSystem.IsRunningOnAppVeyor) + { + StandardRunnerTests.Add(NetCore31X86Test); + StandardRunnerTests.Add(Net50X86Test); + StandardRunnerTests.Add(Net70X86Test); + StandardRunnerTests.Add(Net80X86Test); + } + // Currently, NetCoreRunner runs tests in process. As a result, + // X86 tests will work in our environment, although uses may run + // it as a tool using the X86 architecture. + } ////////////////////////////////////////////////////////////////////// -// SETUP AND TEARDOWN TASKS +// INDIVIDUAL PACKAGE TEST DEFINITIONS ////////////////////////////////////////////////////////////////////// -Setup(context => -{ - Information("Creating BuildVersion"); - _buildVersion = new BuildVersion(context); - - Information("Building {0} version {1} of NUnit Console/Engine.", Configuration, ProductVersion); - Information("PreReleaseLabel is " + PreReleaseLabel); - - Information("Initializing PackageDefinitions"); - InitializePackageDefinitions(context); - - if (BuildSystem.IsRunningOnAppVeyor) - AppVeyor.UpdateBuildVersion(ProductVersion + "-" + AppVeyor.Environment.Build.Number); -}); -Teardown(context => +static ExpectedResult MockAssemblyExpectedResult(int nCopies = 1) => new ExpectedResult("Failed") { - // Executed AFTER the last task. - DisplayUnreportedErrors(); -}); - -////////////////////////////////////////////////////////////////////// -// CLEANING -////////////////////////////////////////////////////////////////////// - -Task("Clean") - .Description("Cleans directories.") - .Does(() => - { - CleanDirectory(BIN_DIR); - CleanDirectory(PACKAGE_DIR); - CleanDirectory(IMAGE_DIR); - CleanDirectory(EXTENSIONS_DIR); - CleanDirectory(PACKAGE_DIR); - }); - -Task("CleanAll") - .Description("Cleans both Debug and Release Directories followed by deleting object directories") - .Does(() => - { - Information("Cleaning both Debug and Release"); - CleanDirectory(PROJECT_DIR + "bin"); - CleanDirectory(PACKAGE_DIR); - CleanDirectory(IMAGE_DIR); - CleanDirectory(EXTENSIONS_DIR); - CleanDirectory(PACKAGE_DIR); - - Information("Deleting object directories"); - foreach (var dir in GetDirectories("src/**/obj/")) - DeleteDirectory(dir, new DeleteDirectorySettings() { Recursive = true }); - }); - -////////////////////////////////////////////////////////////////////// -// BUILD ENGINE AND CONSOLE -////////////////////////////////////////////////////////////////////// - -Task("Build") - .Description("Builds the engine and console") - .IsDependentOn("CheckHeaders") - .IsDependentOn("Clean") - .Does(() => - { - if (IsRunningOnWindows()) - BuildSolution(); - else - BuildEachProjectSeparately(); - }); - -public void BuildSolution() + Total = 37 * nCopies, + Passed = 23 * nCopies, + Failed = 5 * nCopies, + Warnings = 1 * nCopies, + Inconclusive = 1 * nCopies, + Skipped = 7 * nCopies +}; + +static ExpectedResult MockAssemblyExpectedResult(params string[] runtimes) { - MSBuild(SOLUTION_FILE, CreateMSBuildSettings("Build").WithRestore()); - - // Publishing in place where needed to ensure that all references are present. - - // TODO: May not be needed - DisplayBanner("Publishing ENGINE API Project for NETSTANDARD_2.0"); - MSBuild(ENGINE_API_PROJECT, CreateMSBuildSettings("Publish") - .WithProperty("TargetFramework", "netstandard2.0") - .WithProperty("PublishDir", BIN_DIR + "netstandard2.0")); - - DisplayBanner("Publishing ENGINE Project for NETSTANDARD2.0"); - MSBuild(ENGINE_PROJECT, CreateMSBuildSettings("Publish") - .WithProperty("TargetFramework", "netstandard2.0") - .WithProperty("PublishDir", BIN_DIR + "netstandard2.0")); - - // TODO: May not be needed - foreach (var framework in new[] { "netcoreapp3.1", "net5.0" }) - { - DisplayBanner($"Publishing AGENT Project for {framework.ToUpper()}"); - MSBuild(AGENT_PROJECT, CreateMSBuildSettings("Publish") - .WithProperty("TargetFramework", framework) - .WithProperty("PublishDir", BIN_DIR + "agents/" + framework)); - } + int nCopies = runtimes.Length; + var result = MockAssemblyExpectedResult(nCopies); + result.Assemblies = new ExpectedAssemblyResult[nCopies]; + for (int i = 0; i < nCopies; i++) + result.Assemblies[i] = new ExpectedAssemblyResult("mock-assembly.dll", runtimes[i]); + return result; } -// TODO: Test this on linux to see if changes are needed -private void BuildEachProjectSeparately() +static ExpectedResult MockAssemblyX86ExpectedResult(params string[] runtimes) { - DotNetRestore(SOLUTION_FILE); - - BuildProject(ENGINE_PROJECT); - BuildProject(CONSOLE_PROJECT); - BuildProject(AGENT_PROJECT); - BuildProject(AGENT_X86_PROJECT); - - BuildProject(ENGINE_TESTS_PROJECT, "net35", "netcoreapp2.1"); - BuildProject(ENGINE_CORE_TESTS_PROJECT, "net35", "netcoreapp2.1", "netcoreapp3.1", "net5.0", "net6.0", "net8.0"); - BuildProject(CONSOLE_TESTS_PROJECT, "net35", "net6.0", "net8.0"); - - BuildProject(MOCK_ASSEMBLY_X86_PROJECT, "net35", "net40", "netcoreapp2.1", "netcoreapp3.1"); - BuildProject(NOTEST_PROJECT, "net35", "netcoreapp2.1", "netcoreapp3.1"); - - - DisplayBanner("Publish .NET Core & Standard projects"); - - MSBuild(ENGINE_PROJECT, CreateMSBuildSettings("Publish") - .WithProperty("TargetFramework", "netstandard2.0") - .WithProperty("PublishDir", BIN_DIR + "netstandard2.0")); - CopyFileToDirectory( - BIN_DIR + "netstandard2.0/testcentric.engine.metadata.dll", - BIN_DIR + "netcoreapp2.1"); - MSBuild(ENGINE_TESTS_PROJECT, CreateMSBuildSettings("Publish") - .WithProperty("TargetFramework", "netcoreapp2.1") - .WithProperty("PublishDir", BIN_DIR + "netcoreapp2.1")); - MSBuild(ENGINE_CORE_TESTS_PROJECT, CreateMSBuildSettings("Publish") - .WithProperty("TargetFramework", "netcoreapp2.1") - .WithProperty("PublishDir", BIN_DIR + "netcoreapp2.1")); + int nCopies = runtimes.Length; + var result = MockAssemblyExpectedResult(nCopies); + result.Assemblies = new ExpectedAssemblyResult[nCopies]; + for (int i = 0; i < nCopies; i++) + result.Assemblies[i] = new ExpectedAssemblyResult("mock-assembly-x86.dll", runtimes[i]); + return result; } -// NOTE: If we use DotNet to build on Linux, then our net35 projects fail. -// If we use MSBuild, then the net5.0 projects fail. So we build each project -// differently depending on whether it has net35 as one of its targets. -private void BuildProject(string project, params string[] targetFrameworks) -{ - if (targetFrameworks.Length == 0) - { - DisplayBanner($"Building {System.IO.Path.GetFileName(project)}"); - DotNetMSBuild(project, CreateDotNetMSBuildSettings("Build")); - } - else - { - foreach (var framework in targetFrameworks) - { - DisplayBanner($"Building {System.IO.Path.GetFileName(project)} for {framework}"); - if (framework == "net35") - MSBuild(project, CreateMSBuildSettings("Build").WithProperty("TargetFramework", framework)); - else - DotNetMSBuild(project, CreateDotNetMSBuildSettings("Build").WithProperty("TargetFramework", framework)); - } - } -} - -////////////////////////////////////////////////////////////////////// -// BUILD C++ TESTS -////////////////////////////////////////////////////////////////////// - -Task("BuildCppTestFiles") - .Description("Builds the C++ mock test assemblies") - .WithCriteria(IsRunningOnWindows) - .Does(() => - { - MSBuild( - SOURCE_DIR + "NUnitEngine/mock-cpp-clr/mock-cpp-clr-x86.vcxproj", - CreateMSBuildSettings("Build").WithProperty("Platform", "x86")); - - MSBuild( - SOURCE_DIR + "NUnitEngine/mock-cpp-clr/mock-cpp-clr-x64.vcxproj", - CreateMSBuildSettings("Build").WithProperty("Platform", "x64")); - }); - -////////////////////////////////////////////////////////////////////// -// TEST -////////////////////////////////////////////////////////////////////// - -// All Unit Tests are run and any error results are saved in -// the global PendingUnitTestErrors. This method task displays them -// and throws if there were any errors. -Task("CheckForTestErrors") - .Description("Checks for errors running the test suites") - .Does(() => DisplayUnreportedErrors()); - -////////////////////////////////////////////////////////////////////// -// TEST .NET 2.0 ENGINE CORE -////////////////////////////////////////////////////////////////////// - -Task("TestNet20EngineCore") - .Description("Tests the engine core assembly") - .IsDependentOn("Build") - .OnError(exception => { UnreportedErrors.Add(exception.Message); }) - .Does(() => - { - RunNUnitLiteTests(NETFX_ENGINE_CORE_TESTS, "net35"); - }); - -////////////////////////////////////////////////////////////////////// -// TEST NETSTANDARD 2.0 ENGINE CORE -////////////////////////////////////////////////////////////////////// - -Task("TestNetStandard20EngineCore") - .Description("Tests the .NET Standard Engine core assembly") - .IsDependentOn("Build") - .OnError(exception => { UnreportedErrors.Add(exception.Message); }) - .Does(() => - { - RunDotnetNUnitLiteTests(NETCORE_ENGINE_CORE_TESTS, "netcoreapp3.1"); - }); - -////////////////////////////////////////////////////////////////////// -// TEST NETCORE 3.1 ENGINE CORE -////////////////////////////////////////////////////////////////////// - -Task("TestNetCore31EngineCore") - .Description("Tests the .NET Core 3.1 Engine core assembly") - .IsDependentOn("Build") - .OnError(exception => { UnreportedErrors.Add(exception.Message); }) - .Does(() => - { - RunDotnetNUnitLiteTests(NETCORE_ENGINE_CORE_TESTS, "netcoreapp3.1"); - }); - -////////////////////////////////////////////////////////////////////// -// TEST NET 5.0 ENGINE CORE -////////////////////////////////////////////////////////////////////// - -Task("TestNet50EngineCore") - .Description("Tests the .NET 5.0 Engine core assembly") - .IsDependentOn("Build") - .OnError(exception => { UnreportedErrors.Add(exception.Message); }) - .Does(() => - { - RunDotnetNUnitLiteTests(NETCORE_ENGINE_CORE_TESTS, "net5.0"); - }); - -////////////////////////////////////////////////////////////////////// -// TEST NET 6.0 ENGINE CORE -////////////////////////////////////////////////////////////////////// - -Task("TestNet60EngineCore") - .Description("Tests the .NET 6.0 Engine core assembly") - .IsDependentOn("Build") - .OnError(exception => { UnreportedErrors.Add(exception.Message); }) - .Does(() => - { - RunDotnetNUnitLiteTests(NETCORE_ENGINE_CORE_TESTS, "net6.0"); - }); - -////////////////////////////////////////////////////////////////////// -// TEST .NET 2.0 ENGINE -////////////////////////////////////////////////////////////////////// - -Task("TestNet20Engine") - .Description("Tests the engine") - .IsDependentOn("Build") - .OnError(exception => { UnreportedErrors.Add(exception.Message); }) - .Does(() => - { - RunNUnitLiteTests(NETFX_ENGINE_TESTS, "net35"); - }); - -////////////////////////////////////////////////////////////////////// -// TEST NETSTANDARD 2.0 ENGINE -////////////////////////////////////////////////////////////////////// - -Task("TestNetStandard20Engine") - .Description("Tests the .NET Standard Engine") - .IsDependentOn("Build") - .OnError(exception => { UnreportedErrors.Add(exception.Message); }) - .Does(() => - { - RunDotnetNUnitLiteTests(NETCORE_ENGINE_TESTS, "netcoreapp3.1"); - }); - -////////////////////////////////////////////////////////////////////// -// TEST .NET 2.0 CONSOLE -////////////////////////////////////////////////////////////////////// - -Task("TestNet20Console") - .Description("Tests the .NET 2.0 console runner") - .IsDependentOn("Build") - .OnError(exception => { UnreportedErrors.Add(exception.Message); }) - .Does(() => - { - RunNet20Console(CONSOLE_TESTS, "net35"); - }); - -////////////////////////////////////////////////////////////////////// -// TEST .NET 6.0 CONSOLE -////////////////////////////////////////////////////////////////////// - -Task("TestNet60Console") - .Description("Tests the .NET 6.0 console runner") - .IsDependentOn("Build") - .OnError(exception => { UnreportedErrors.Add(exception.Message); }) - .Does(() => - { - RunNetCoreConsole(CONSOLE_TESTS, "net6.0"); - }); - -////////////////////////////////////////////////////////////////////// -// TEST .NET 8.0 CONSOLE -////////////////////////////////////////////////////////////////////// - -Task("TestNet80Console") - .Description("Tests the .NET 8.0 console runner") - .IsDependentOn("Build") - .OnError(exception => { UnreportedErrors.Add(exception.Message); }) - .Does(() => - { - RunNetCoreConsole(CONSOLE_TESTS, "net8.0"); - }); - -////////////////////////////////////////////////////////////////////// -// FETCH BUNDLED EXTENSIONS -////////////////////////////////////////////////////////////////////// - -Task("FetchBundledExtensions") - .Does(() => - { - CleanDirectory(EXTENSIONS_DIR); - - DisplayBanner("Fetching bundled extensions"); - - foreach (var extension in BUNDLED_EXTENSIONS) - { - DisplayBanner(extension); - - NuGetInstall(extension, new NuGetInstallSettings - { - OutputDirectory = EXTENSIONS_DIR, - Source = new[] { "https://www.nuget.org/api/v2" } - }); - } - }); - -////////////////////////////////////////////////////////////////////// -// CREATE MSI IMAGE -////////////////////////////////////////////////////////////////////// - -Task("CreateMsiImage") - .IsDependentOn("FetchBundledExtensions") - .Does(() => - { - CleanDirectory(MSI_IMG_DIR); - CopyFiles( - new FilePath[] { "LICENSE.txt", "NOTICES.txt", "CHANGES.txt", "nunit.ico" }, - MSI_IMG_DIR); - CopyDirectory(BIN_DIR, MSI_IMG_DIR + "bin/"); - - foreach (var framework in new[] { "net20", "net35" }) - { - var addinsImgDir = MSI_IMG_DIR + "bin/" + framework + "/addins/"; - - CopyDirectory(MSI_DIR + "resources/", MSI_IMG_DIR); - CleanDirectory(addinsImgDir); - - foreach (var packageDir in System.IO.Directory.GetDirectories(EXTENSIONS_DIR)) - CopyPackageContents(packageDir, addinsImgDir); - } - }); - -////////////////////////////////////////////////////////////////////// -// CREATE ZIP IMAGE -////////////////////////////////////////////////////////////////////// - -Task("CreateZipImage") - .IsDependentOn("FetchBundledExtensions") - .Does(() => - { - CleanDirectory(ZIP_IMG_DIR); - CopyFiles( - new FilePath[] { "LICENSE.txt", "NOTICES.txt", "CHANGES.txt", "nunit.ico" }, - ZIP_IMG_DIR); - CopyDirectory(BIN_DIR, ZIP_IMG_DIR + "bin/"); - - foreach (var framework in new[] { "net20", "net35" }) - { - var frameworkDir = ZIP_IMG_DIR + "bin/" + framework + "/"; - CopyFileToDirectory(ZIP_DIR + "nunit.bundle.addins", frameworkDir); - - var addinsDir = frameworkDir + "addins/"; - CleanDirectory(addinsDir); - - foreach (var packageDir in System.IO.Directory.GetDirectories(EXTENSIONS_DIR)) - CopyPackageContents(packageDir, addinsDir); - } - }); - -Task("BuildPackages") - .IsDependentOn("CreateMsiImage") - .IsDependentOn("CreateZipImage") - .Does(() => - { - EnsureDirectoryExists(PACKAGE_DIR); - - foreach (var package in AllPackages) - { - DisplayBanner($"Building package {package.PackageName}"); - - package.BuildPackage(); - } - }); - -////////////////////////////////////////////////////////////////////// -// VERIFY PACKAGES -////////////////////////////////////////////////////////////////////// - -Task("VerifyPackages") - .Description("Check content of all the packages we build") - .Does(() => - { - int failures = 0; - - foreach (var package in AllPackages) - { - if (!CheckPackage($"{PACKAGE_DIR}{package.PackageName}", package.PackageChecks)) - ++failures; - - if (package.HasSymbols && !CheckPackage($"{PACKAGE_DIR}{package.SymbolPackageName}", package.SymbolChecks)) - ++failures; - } - - if (failures == 0) - Information("\nAll packages passed verification."); - else - throw new System.Exception($"{failures} packages failed verification."); - }); - -////////////////////////////////////////////////////////////////////// -// TEST PACKAGES -////////////////////////////////////////////////////////////////////// - -Task("TestPackages") - .Does(() => - { - foreach (var package in AllPackages) - { - if (package.PackageTests != null) - new PackageTester(Context, package).RunTests(); - } - }); - -////////////////////////////////////////////////////////////////////// -// INSTALL SIGNING TOOL -////////////////////////////////////////////////////////////////////// - -Task("InstallSigningTool") - .Does(() => - { - var result = StartProcess("dotnet.exe", new ProcessSettings { Arguments = "tool install SignClient --global" }); - }); - -////////////////////////////////////////////////////////////////////// -// SIGN PACKAGES -////////////////////////////////////////////////////////////////////// - -Task("SignPackages") - .IsDependentOn("InstallSigningTool") - .IsDependentOn("Package") - .Does(() => - { - // Get the secret. - var secret = EnvironmentVariable("SIGNING_SECRET"); - if(string.IsNullOrWhiteSpace(secret)) { - throw new InvalidOperationException("Could not resolve signing secret."); - } - // Get the user. - var user = EnvironmentVariable("SIGNING_USER"); - if(string.IsNullOrWhiteSpace(user)) { - throw new InvalidOperationException("Could not resolve signing user."); - } - - var signClientPath = Context.Tools.Resolve("SignClient.exe") ?? Context.Tools.Resolve("SignClient") ?? throw new Exception("Failed to locate sign tool"); - - var settings = File("./signclient.json"); - - // Get the files to sign. - var files = GetFiles(string.Concat(PACKAGE_DIR, "*.nupkg")) + - GetFiles(string.Concat(PACKAGE_DIR, "*.msi")); - - foreach(var file in files) - { - Information("Signing {0}...", file.FullPath); - - // Build the argument list. - var arguments = new ProcessArgumentBuilder() - .Append("sign") - .AppendSwitchQuoted("-c", MakeAbsolute(settings.Path).FullPath) - .AppendSwitchQuoted("-i", MakeAbsolute(file).FullPath) - .AppendSwitchQuotedSecret("-s", secret) - .AppendSwitchQuotedSecret("-r", user) - .AppendSwitchQuoted("-n", "NUnit.org") - .AppendSwitchQuoted("-d", "NUnit is a unit-testing framework for all .NET languages.") - .AppendSwitchQuoted("-u", "https://nunit.org/"); - - // Sign the binary. - var result = StartProcess(signClientPath.FullPath, new ProcessSettings { Arguments = arguments }); - if(result != 0) - { - // We should not recover from this. - throw new InvalidOperationException("Signing failed!"); - } - } - }); - -////////////////////////////////////////////////////////////////////// -// PUBLISH PACKAGES -////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////// -// PUBLISH PACKAGES -////////////////////////////////////////////////////////////////////// - -static bool HadPublishingErrors = false; - -Task("PublishPackages") - .Description("Publish nuget and chocolatey packages according to the current settings") - .IsDependentOn("PublishToMyGet") - .IsDependentOn("PublishToNuGet") - .IsDependentOn("PublishToChocolatey") - .Does(() => - { - if (HadPublishingErrors) - throw new Exception("One of the publishing steps failed."); - }); - -// This task may either be run by the PublishPackages task, -// which depends on it, or directly when recovering from errors. -Task("PublishToMyGet") - .Description("Publish packages to MyGet") - .Does(() => - { - if (!ShouldPublishToMyGet) - Information("Nothing to publish to MyGet from this run."); - else - { - var apiKey = EnvironmentVariable(MYGET_API_KEY); - - foreach (var package in AllPackages) - try - { - switch (package.PackageType) - { - case PackageType.NuGet: - PushNuGetPackage(PACKAGE_DIR + package.PackageName, apiKey, MYGET_PUSH_URL); - break; - case PackageType.Chocolatey: - PushChocolateyPackage(PACKAGE_DIR + package.PackageName, apiKey, MYGET_PUSH_URL); - break; - } - } - catch(Exception) - { - HadPublishingErrors = true; - } - } - }); - -// This task may either be run by the PublishPackages task, -// which depends on it, or directly when recovering from errors. -Task("PublishToNuGet") - .Description("Publish packages to NuGet") - .Does(() => - { - if (!ShouldPublishToNuGet) - Information("Nothing to publish to NuGet from this run."); - else - { - var apiKey = EnvironmentVariable(NUGET_API_KEY); - - foreach (var package in AllPackages) - if (package.PackageType == PackageType.NuGet) - try - { - PushNuGetPackage(PACKAGE_DIR + package.PackageName, apiKey, NUGET_PUSH_URL); - } - catch (Exception) - { - HadPublishingErrors = true; - } - } - }); - -// This task may either be run by the PublishPackages task, -// which depends on it, or directly when recovering from errors. -Task("PublishToChocolatey") - .Description("Publish packages to Chocolatey") - .Does(() => - { - if (!ShouldPublishToChocolatey) - Information("Nothing to publish to Chocolatey from this run."); - else - { - var apiKey = EnvironmentVariable(CHOCO_API_KEY); - - foreach (var package in AllPackages) - if (package.PackageType == PackageType.Chocolatey) - try - { - PushChocolateyPackage(PACKAGE_DIR + package.PackageName, apiKey, CHOCO_PUSH_URL); - } - catch (Exception) - { - HadPublishingErrors = true; - } - } - }); - -Task("ListInstalledNetCoreRuntimes") - .Description("Lists all installed .NET Core Runtimes") - .Does(() => - { - var runtimes = GetInstalledNetCoreRuntimes(); - foreach (var runtime in runtimes) - { - Information(runtime); - } - }); - -////////////////////////////////////////////////////////////////////// -// CREATE A DRAFT RELEASE -////////////////////////////////////////////////////////////////////// - -Task("CreateDraftRelease") - .Does(() => - { - bool isDirectTarget = Target == "CreateDraftRelease"; - - if (isDirectTarget && !HasArgument("productVersion")) - throw new Exception("Must specify --productVersion with the CreateDraftRelease target."); - - if (IsReleaseBranch || isDirectTarget) - { - string milestone = IsReleaseBranch - ? _buildVersion.BranchName.Substring(8) - : ProductVersion; - string releaseName = $"NUnit Console and Engine {milestone}"; - - Information($"Creating draft release for {releaseName}"); - - if (!NoPush) - try - { - GitReleaseManagerCreate(EnvironmentVariable(GITHUB_ACCESS_TOKEN), GITHUB_OWNER, GITHUB_REPO, new GitReleaseManagerCreateSettings() - { - Name = releaseName, - Milestone = milestone - }); - } - catch - { - Error($"Unable to create draft release for {releaseName}."); - Error($"Check that there is a {milestone} milestone with at least one closed issue."); - Error(""); - throw; - } - } - else - { - Information("Skipping Release creation because this is not a release branch"); - } - }); +static PackageTest Net35Test = new PackageTest( + 1, "Net35Test", + "Run mock-assembly.dll under .NET 3.5", + "net35/mock-assembly.dll", + MockAssemblyExpectedResult("net-2.0")); + +static PackageTest Net35X86Test = new PackageTest( + 1, "Net35X86Test", + "Run mock-assembly-x86.dll under .NET 3.5", + "net35/mock-assembly-x86.dll", + MockAssemblyX86ExpectedResult("net-2.0")); + +static PackageTest Net40Test = new PackageTest( + 1, "Net40Test", + "Run mock-assembly.dll under .NET 4.x", + "net40/mock-assembly.dll", + MockAssemblyExpectedResult("net-4.0")); + +static PackageTest Net40X86Test = new PackageTest( + 1, "Net40X86Test", + "Run mock-assembly-x86.dll under .NET 4.x", + "net40/mock-assembly-x86.dll", + MockAssemblyX86ExpectedResult("net-4.0")); + +static PackageTest Net35PlusNet40Test = new PackageTest( + 1, "Net35PlusNet40Test", + "Run both copies of mock-assembly together", + "net35/mock-assembly.dll net40/mock-assembly.dll", + MockAssemblyExpectedResult("net-2.0", "net-4.0")); + +static PackageTest Net80Test = new PackageTest( + 1, "Net80Test", + "Run mock-assembly.dll under .NET 8.0", + "net8.0/mock-assembly.dll", + MockAssemblyExpectedResult("netcore-8.0")); + +static PackageTest Net80X86Test = new PackageTest( + 1, "Net80X86Test", + "Run mock-assembly-x86.dll under .NET 8.0", + "net8.0/mock-assembly-x86.dll", + MockAssemblyX86ExpectedResult("netcore-8.0")); + +static PackageTest Net70Test = new PackageTest( + 1, "Net70Test", + "Run mock-assembly.dll under .NET 7.0", + "net7.0/mock-assembly.dll", + MockAssemblyExpectedResult("netcore-7.0")); + +static PackageTest Net70X86Test = new PackageTest( + 1, "Net70X86Test", + "Run mock-assembly-x86.dll under .NET 7.0", + "net7.0/mock-assembly-x86.dll", + MockAssemblyX86ExpectedResult("netcore-7.0")); + +static PackageTest Net60Test = new PackageTest( + 1, "Net60Test", + "Run mock-assembly.dll under .NET 6.0", + "net6.0/mock-assembly.dll", + MockAssemblyExpectedResult("netcore-6.0")); + +static PackageTest Net60X86Test = new PackageTest( + 1, "Net60X86Test", + "Run mock-assembly-x86.dll under .NET 6.0", + "net6.0/mock-assembly-x86.dll", + MockAssemblyX86ExpectedResult("netcore-6.0")); + +static PackageTest Net50Test = new PackageTest( + 1, "Net50Test", + "Run mock-assembly.dll under .NET 5.0", + "net5.0/mock-assembly.dll", + MockAssemblyExpectedResult("netcore-5.0")); + +static PackageTest Net50X86Test = new PackageTest( + 1, "Net50X86Test", + "Run mock-assembly-x86.dll under .NET 5.0", + "net5.0/mock-assembly-x86.dll", + MockAssemblyX86ExpectedResult("netcore-5.0")); + +static PackageTest NetCore31Test = new PackageTest( + 1, "NetCore31Test", + "Run mock-assembly.dll under .NET Core 3.1", + "netcoreapp3.1/mock-assembly.dll", + MockAssemblyExpectedResult("netcore-3.1")); + +static PackageTest NetCore31X86Test = new PackageTest( + 1, "NetCore31X86Test", + "Run mock-assembly-x86.dll under .NET Core 3.1", + "netcoreapp3.1/mock-assembly-x86.dll", + MockAssemblyX86ExpectedResult("netcore-3.1")); + +static PackageTest Net50PlusNet60Test = new PackageTest( + 1, "Net50PlusNet60Test", + "Run mock-assembly under .NET 5.0 and 6.0 together", + "net5.0/mock-assembly.dll net6.0/mock-assembly.dll",//" net7.0/mock-assembly.dll net8.0/mock-assembly.dll", + MockAssemblyExpectedResult("netcore-5.0", "netcore-6.0")); + +static PackageTest Net40PlusNet60Test = new PackageTest( + 1, "Net40PlusNet60Test", + "Run mock-assembly under .Net Framework 4.0 and .Net 6.0 together", + "net40/mock-assembly.dll net6.0/mock-assembly.dll", + MockAssemblyExpectedResult("net-4.0", "netcore-6.0")); + +static PackageTest NUnitProjectTest = new PackageTest( + 1, "NUnitProjectTest", + "Run project with both copies of mock-assembly", + "../../NetFXTests.nunit --config=Release --trace=Debug", + MockAssemblyExpectedResult("net-2.0", "net-4.0"), + KnownExtensions.NUnitProjectLoader); + +////////////////////////////////////////////////////////////////////// +// LISTS OF FILES USED IN CHECKING PACKAGES +////////////////////////////////////////////////////////////////////// + +FilePath[] ENGINE_FILES = { + "nunit.engine.dll", "nunit.engine.core.dll", "nunit.engine.api.dll", "testcentric.engine.metadata.dll" }; +FilePath[] ENGINE_PDB_FILES = { + "nunit.engine.pdb", "nunit.engine.core.pdb", "nunit.engine.api.pdb"}; +FilePath[] ENGINE_CORE_FILES = { + "nunit.engine.core.dll", "nunit.engine.api.dll", "testcentric.engine.metadata.dll" }; +FilePath[] ENGINE_CORE_PDB_FILES = { + "nunit.engine.core.pdb", "nunit.engine.api.pdb"}; +FilePath[] AGENT_FILES = { + "nunit-agent.exe", "nunit-agent.exe.config", + "nunit-agent-x86.exe", "nunit-agent-x86.exe.config", + "nunit.engine.core.dll", "nunit.engine.api.dll", "testcentric.engine.metadata.dll"}; +FilePath[] AGENT_FILES_NETCORE = { + "nunit-agent.dll", "nunit-agent.dll.config", + "nunit.engine.core.dll", "nunit.engine.api.dll", "testcentric.engine.metadata.dll", + "Microsoft.Extensions.DependencyModel.dll"}; +FilePath[] AGENT_PDB_FILES = { + "nunit-agent.pdb", "nunit-agent-x86.pdb", "nunit.engine.core.pdb", "nunit.engine.api.pdb"}; +FilePath[] AGENT_PDB_FILES_NETCORE = { + "nunit-agent.pdb", "nunit.engine.core.pdb", "nunit.engine.api.pdb"}; +FilePath[] CONSOLE_FILES = { + "nunit3-console.exe", "nunit3-console.exe.config" }; +FilePath[] CONSOLE_FILES_NETCORE = { + "nunit3-console.exe", "nunit3-console.dll" }; + +////////////////////////////////////////////////////////////////////// +// INDIVIDUAL PACKAGE DEFINITIONS +////////////////////////////////////////////////////////////////////// + +PackageDefinition NUnitConsoleNuGetPackage; +PackageDefinition NUnitConsoleRunnerNuGetPackage; +PackageDefinition NUnitConsoleRunnerNet60Package; +PackageDefinition NUnitConsoleRunnerNet80Package; +PackageDefinition NUnitEnginePackage; +PackageDefinition NUnitEngineApiPackage; +PackageDefinition NUnitConsoleRunnerChocolateyPackage; +PackageDefinition NUnitConsoleMsiPackage; +PackageDefinition NUnitConsoleZipPackage; + +BuildSettings.Packages.AddRange(new PackageDefinition[] { + + NUnitConsoleRunnerNuGetPackage = new NuGetPackage( + id: "NUnit.ConsoleRunner", + source: BuildSettings.NuGetDirectory + "runners/nunit.console-runner.nuspec", + checks: new PackageCheck[] { + HasFiles("LICENSE.txt", "NOTICES.txt"), + HasDirectory("tools").WithFiles(CONSOLE_FILES).AndFiles(ENGINE_FILES).AndFile("nunit.console.nuget.addins"), + HasDirectory("tools/agents/net20").WithFiles(AGENT_FILES).AndFile("nunit.console.nuget.agent.addins"), + HasDirectory("tools/agents/net40").WithFiles(AGENT_FILES).AndFile("nunit.console.nuget.agent.addins"), + HasDirectory("tools/agents/netcoreapp3.1").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.nuget.agent.addins"), + HasDirectory("tools/agents/net5.0").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.nuget.agent.addins"), + HasDirectory("tools/agents/net6.0").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.nuget.agent.addins"), + HasDirectory("tools/agents/net7.0").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.nuget.agent.addins"), + HasDirectory("tools/agents/net8.0").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.nuget.agent.addins") + }, + symbols: new PackageCheck[] { + HasDirectory("tools").WithFiles(ENGINE_PDB_FILES).AndFile("nunit3-console.pdb"), + HasDirectory("tools/agents/net20").WithFiles(AGENT_PDB_FILES), + HasDirectory("tools/agents/net40").WithFiles(AGENT_PDB_FILES), + HasDirectory("tools/agents/netcoreapp3.1").WithFiles(AGENT_PDB_FILES_NETCORE), + HasDirectory("tools/agents/net5.0").WithFiles(AGENT_PDB_FILES_NETCORE), + HasDirectory("tools/agents/net6.0").WithFiles(AGENT_PDB_FILES_NETCORE), + HasDirectory("tools/agents/net7.0").WithFiles(AGENT_PDB_FILES_NETCORE), + HasDirectory("tools/agents/net8.0").WithFiles(AGENT_PDB_FILES_NETCORE) + }, + testRunner: new ConsoleRunnerSelfTester(BuildSettings.NuGetTestDirectory + + $"NUnit.ConsoleRunner.{BuildSettings.PackageVersion}/tools/nunit3-console.exe"), + tests: StandardRunnerTests), + + // NOTE: Must follow ConsoleRunner, upon which it depends + NUnitConsoleNuGetPackage = new NuGetPackage( + id: "NUnit.Console", + source: BuildSettings.NuGetDirectory + "runners/nunit.console-runner-with-extensions.nuspec", + checks: new PackageCheck[] { HasFile("LICENSE.txt") }), + + NUnitConsoleRunnerNet80Package = new NuGetPackage( + id: "NUnit.ConsoleRunner.NetCore", + source: BuildSettings.NuGetDirectory + "runners/nunit.console-runner.netcore.nuspec", + checks: new PackageCheck[] { + HasFiles("LICENSE.txt", "NOTICES.txt"), + HasDirectory("tools/net8.0").WithFiles(CONSOLE_FILES_NETCORE).AndFiles(ENGINE_CORE_FILES).AndFile("nunit.console.nuget.addins") + }, + symbols: new PackageCheck[] { + HasDirectory("tools/net8.0").WithFile("nunit3-console.pdb").AndFiles(ENGINE_PDB_FILES) + }, + testRunner: new ConsoleRunnerSelfTester(BuildSettings.NuGetTestDirectory + + $"NUnit.ConsoleRunner.NetCore.{BuildSettings.PackageVersion}/tools/net8.0/nunit3-console.exe"), + tests: NetCoreRunnerTests), + + NUnitConsoleRunnerNet60Package = new NuGetPackage( + id: "NUnit.ConsoleRunner.NetCore", + source: BuildSettings.NuGetDirectory + "runners/nunit.console-runner.netcore.nuspec", + checks: new PackageCheck[] { + HasFiles("LICENSE.txt", "NOTICES.txt"), + HasDirectory("tools/net6.0").WithFiles(CONSOLE_FILES_NETCORE).AndFiles(ENGINE_CORE_FILES).AndFile("nunit.console.nuget.addins") + }, + symbols: new PackageCheck[] { + HasDirectory("tools/net6.0").WithFile("nunit3-console.pdb").AndFiles(ENGINE_PDB_FILES) + }, + testRunner: new ConsoleRunnerSelfTester(BuildSettings.NuGetTestDirectory + + $"NUnit.ConsoleRunner.NetCore.{BuildSettings.PackageVersion}/tools/net6.0/nunit3-console.exe"), + tests: NetCoreRunnerTests), + + NUnitConsoleRunnerChocolateyPackage = new ChocolateyPackage( + id: "nunit-console-runner", + source: BuildSettings.ChocolateyDirectory + "nunit-console-runner.nuspec", + checks: new PackageCheck[] { + HasDirectory("tools").WithFiles("LICENSE.txt", "NOTICES.txt", "VERIFICATION.txt").AndFiles(CONSOLE_FILES).AndFiles(ENGINE_FILES).AndFile("nunit.console.choco.addins"), + HasDirectory("tools/agents/net20").WithFiles(AGENT_FILES).AndFile("nunit.console.choco.agent.addins"), + HasDirectory("tools/agents/net40").WithFiles(AGENT_FILES).AndFile("nunit.console.choco.agent.addins"), + HasDirectory("tools/agents/netcoreapp3.1").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.choco.agent.addins"), + HasDirectory("tools/agents/net5.0").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.choco.agent.addins"), + HasDirectory("tools/agents/net6.0").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.choco.agent.addins"), + HasDirectory("tools/agents/net7.0").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.choco.agent.addins"), + HasDirectory("tools/agents/net8.0").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.choco.agent.addins") + }, + testRunner: new ConsoleRunnerSelfTester(BuildSettings.ChocolateyTestDirectory + + $"nunit-console-runner.{BuildSettings.PackageVersion}/tools/nunit3-console.exe"), + tests: StandardRunnerTests), + + NUnitConsoleMsiPackage = new MsiPackage( + id: "NUnit.Console", + source: BuildSettings.MsiDirectory + "nunit/nunit.wixproj", + checks: new PackageCheck[] { + HasDirectory("NUnit.org").WithFiles("LICENSE.txt", "NOTICES.txt", "nunit.ico"), + HasDirectory("NUnit.org/nunit-console").WithFiles(CONSOLE_FILES).AndFiles(ENGINE_FILES).AndFile("nunit.bundle.addins"), + HasDirectory("Nunit.org/nunit-console/addins").WithFiles("nunit.core.dll", "nunit.core.interfaces.dll", "nunit.v2.driver.dll", "nunit-project-loader.dll", "vs-project-loader.dll", "nunit-v2-result-writer.dll", "teamcity-event-listener.dll") + }, + testRunner: new ConsoleRunnerSelfTester(BuildSettings.MsiTestDirectory + + $"NUnit.Console.{BuildSettings.BuildVersion.SemVer}/NUnit.org/nunit-console/nunit3-console.exe"), + tests: StandardRunnerTests, + bundledExtensions: new [] { + new PackageReference("NUnit.Extension.VSProjectLoader", "3.9.0"), + new PackageReference("NUnit.Extension.NUnitProjectLoader", "3.7.1"), + new PackageReference("NUnit.Extension.NUnitV2Driver", "3.9.0"), + new PackageReference("NUnit.Extension.NUnitV2ResultWriter", "3.7.0"), + new PackageReference("NUnit.Extension.TeamCityEventListener", "1.0.9") + }), + + NUnitConsoleZipPackage = new ZipPackage( + id: "NUnit.Console", + source: BuildSettings.ZipImageDirectory, + checks: new PackageCheck[] { + HasFiles("LICENSE.txt", "NOTICES.txt", "CHANGES.txt"), + HasDirectory("bin/net20").WithFiles(CONSOLE_FILES).AndFiles(ENGINE_FILES).AndFile("nunit3-console.pdb").AndFiles(ENGINE_PDB_FILES), + HasDirectory("bin/net35").WithFiles(CONSOLE_FILES).AndFiles(ENGINE_FILES).AndFile("nunit3-console.pdb").AndFiles(ENGINE_PDB_FILES), + HasDirectory("bin/netstandard2.0").WithFiles(ENGINE_FILES).AndFiles(ENGINE_PDB_FILES), + HasDirectory("bin/netcoreapp3.1").WithFiles(ENGINE_CORE_FILES).AndFiles(ENGINE_CORE_PDB_FILES), + //HasDirectory("bin/net5.0").WithFiles(ENGINE_FILES).AndFiles(ENGINE_PDB_FILES), + HasDirectory("bin/agents/net20").WithFiles(AGENT_FILES).AndFiles(AGENT_PDB_FILES), + HasDirectory("bin/agents/net40").WithFiles(AGENT_FILES).AndFiles(AGENT_PDB_FILES), + HasDirectory("bin/agents/net5.0").WithFiles(AGENT_FILES_NETCORE).AndFiles(AGENT_PDB_FILES_NETCORE), + HasDirectory("bin/agents/net6.0").WithFiles(AGENT_FILES_NETCORE).AndFiles(AGENT_PDB_FILES_NETCORE), + HasDirectory("bin/agents/net7.0").WithFiles(AGENT_FILES_NETCORE).AndFiles(AGENT_PDB_FILES_NETCORE), + HasDirectory("bin/agents/net8.0").WithFiles(AGENT_FILES_NETCORE).AndFiles(AGENT_PDB_FILES_NETCORE) + }, + testRunner: new ConsoleRunnerSelfTester(BuildSettings.ZipTestDirectory + + $"NUnit.Console.{BuildSettings.PackageVersion}/bin/net20/nunit3-console.exe"), + tests: StandardRunnerTests, + bundledExtensions: new [] { + new PackageReference("NUnit.Extension.VSProjectLoader", "3.9.0"), + new PackageReference("NUnit.Extension.NUnitProjectLoader", "3.7.1"), + new PackageReference("NUnit.Extension.NUnitV2Driver", "3.9.0"), + new PackageReference("NUnit.Extension.NUnitV2ResultWriter", "3.7.0"), + new PackageReference("NUnit.Extension.TeamCityEventListener", "1.0.9") + }), + + // NOTE: Packages below this point have no direct tests + + NUnitEnginePackage = new NuGetPackage( + id: "NUnit.Engine", + source: BuildSettings.NuGetDirectory + "engine/nunit.engine.nuspec", + checks: new PackageCheck[] { + HasFiles("LICENSE.txt", "NOTICES.txt"), + HasDirectory("lib/net20").WithFiles(ENGINE_FILES), + HasDirectory("lib/netstandard2.0").WithFiles(ENGINE_FILES), + HasDirectory("contentFiles/any/lib/net20").WithFile("nunit.engine.nuget.addins"), + HasDirectory("contentFiles/any/lib/netstandard2.0").WithFile("nunit.engine.nuget.addins"), + HasDirectory("contentFiles/any/agents/net20").WithFiles(AGENT_FILES).AndFile("nunit.agent.addins"), + HasDirectory("contentFiles/any/agents/net40").WithFiles(AGENT_FILES).AndFile("nunit.agent.addins") + }, + symbols: new PackageCheck[] { + HasDirectory("lib/net20").WithFiles(ENGINE_PDB_FILES), + HasDirectory("lib/netstandard2.0").WithFiles(ENGINE_PDB_FILES), + HasDirectory("contentFiles/any/agents/net20").WithFiles(AGENT_PDB_FILES), + HasDirectory("contentFiles/any/agents/net40").WithFiles(AGENT_PDB_FILES) + }), + + NUnitEngineApiPackage = new NuGetPackage( + id: "NUnit.Engine.Api", + source: BuildSettings.NuGetDirectory + "engine/nunit.engine.api.nuspec", + checks: new PackageCheck[] { + HasFile("LICENSE.txt"), + HasDirectory("lib/net20").WithFile("nunit.engine.api.dll"), + HasDirectory("lib/netstandard2.0").WithFile("nunit.engine.api.dll"), + }, + symbols: new PackageCheck[] { + HasDirectory("lib/net20").WithFile("nunit.engine.api.pdb"), + HasDirectory("lib/netstandard2.0").WithFile("nunit.engine.api.pdb") + }) +}); ////////////////////////////////////////////////////////////////////// -// CREATE A PRODUCTION RELEASE +// TEST RUNNERS ////////////////////////////////////////////////////////////////////// -Task("CreateProductionRelease") - .Does(() => +// Custom unit test runner to run console vs engine tests differently +// TODO: Use NUnitLite for all tests? +public class CustomTestRunner : UnitTestRunner +{ + public override int Run(FilePath testPath) { - if (IsProductionRelease) - { - string token = EnvironmentVariable(GITHUB_ACCESS_TOKEN); - string tagName = ProductVersion; - - var assetList = new List(); - foreach (var package in AllPackages) - assetList.Add(PACKAGE_DIR + package.PackageName); - string assets = $"\"{string.Join(',', assetList.ToArray())}\""; - - Information($"Publishing release {tagName} to GitHub"); - - if (NoPush) - { - Information($"Assets:"); - foreach (var asset in assetList) - Information(" " + asset); - } - else - { - GitReleaseManagerAddAssets(token, GITHUB_OWNER, GITHUB_REPO, tagName, assets); - GitReleaseManagerClose(token, GITHUB_OWNER, GITHUB_REPO, tagName); - } - } - else + // Run console tests under the just-built console + if(testPath.ToString().Contains("nunit3-console.tests.dll")) { - Information("Skipping CreateProductionRelease because this is not a production release"); + return BuildSettings.Context.StartProcess( + BuildSettings.OutputDirectory + "net20/nunit3-console.exe", + $"\"{testPath}\" {BuildSettings.UnitTestArguments}" ); } - }); -////////////////////////////////////////////////////////////////////// -// TASK TARGETS -////////////////////////////////////////////////////////////////////// + // All other tests use NUnitLite + return new NUnitLiteRunner().Run(testPath); + } +} -Task("TestConsole") - .Description("Builds and tests the console runner") - .IsDependentOn("TestNet20Console") - .IsDependentOn("TestNet60Console"); - -Task("TestEngineCore") - .Description("Builds and tests the engine core assembly") - .IsDependentOn("TestNet20EngineCore") - .IsDependentOn("TestNetStandard20EngineCore") - .IsDependentOn("TestNetCore31EngineCore") - .IsDependentOn("TestNet50EngineCore") - .IsDependentOn("TestNet60EngineCore"); - -Task("TestEngine") - .Description("Builds and tests the engine assembly") - .IsDependentOn("TestNet20Engine") - .IsDependentOn("TestNetStandard20Engine"); - -Task("Test") - .Description("Builds and tests the engine and console runner") - .IsDependentOn("TestEngineCore") - .IsDependentOn("TestEngine") - .IsDependentOn("TestConsole") - .IsDependentOn("CheckForTestErrors"); - -Task("Package") - .Description("Builds and tests all packages") - .IsDependentOn("Build") - .IsDependentOn("BuildPackages") - .IsDependentOn("VerifyPackages") - .IsDependentOn("TestPackages"); - -Task("PackageExistingBuild") - .Description("Builds and tests all packages, using previously build binaries") - .IsDependentOn("BuildPackages") - .IsDependentOn("VerifyPackages") - .IsDependentOn("TestPackages"); - -Task("BuildTestAndPackage") - .Description("Builds, tests and packages") - .IsDependentOn("Build") - .IsDependentOn("Test") - .IsDependentOn("Package"); - -Task("Appveyor") - .Description("Target we run in our AppVeyor CI") - .IsDependentOn("BuildTestAndPackage") - .IsDependentOn("PublishPackages") - .IsDependentOn("CreateDraftRelease") - .IsDependentOn("CreateProductionRelease"); - -Task("Default") - .Description("Builds the engine and console runner") - .IsDependentOn("Build"); +// Use the console runner we just built to run package tests +public class ConsoleRunnerSelfTester : PackageTestRunner +{ + public ConsoleRunnerSelfTester(string executablePath) + { + ExecutablePath = executablePath; + } + + public override int Run(string arguments) + { + return base.Run(arguments); + } +} ////////////////////////////////////////////////////////////////////// // EXECUTION ////////////////////////////////////////////////////////////////////// -RunTarget(Target); +Build.Run() diff --git a/cake/banner.cake b/cake/banner.cake new file mode 100644 index 000000000..b882509ba --- /dev/null +++ b/cake/banner.cake @@ -0,0 +1,23 @@ +// Banner class displays banners similar to those created by Cake +// We use this to display intermediate steps within a task +public static class Banner +{ + public static void Display(string message, char barChar='-', int length=0) + { + if (length == 0) length = MaxLineLength(message); + var bar = new string(barChar, length); + + Console.WriteLine(); + Console.WriteLine(bar); + Console.WriteLine(message); + Console.WriteLine(bar); + + int MaxLineLength(string message) + { + int length = 0; + foreach (string line in message.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)) + length = Math.Max(length, line.Length); + return length; + } + } +} diff --git a/cake/build-settings.cake b/cake/build-settings.cake index 6aaf67882..6e87520c9 100644 --- a/cake/build-settings.cake +++ b/cake/build-settings.cake @@ -1,23 +1,342 @@ -#load ./ci.cake -#load ./packaging.cake -#load ./package-checks.cake -#load ./test-results.cake -#load ./package-tests.cake -#load ./package-tester.cake -#load ./header-check.cake -#load ./local-tasks.cake - -public class BuildSettings +public static class BuildSettings { - public BuildSettings(ISetupContext context) - { - if (context == null) - throw new System.ArgumentNullException(nameof(context)); + private static BuildSystem _buildSystem; - Target = context.TargetTask.Name; - Configuration = context.Argument("configuration", "Release"); + public static void Initialize( + // Required parameters + ICakeContext context, + string title, + string githubRepository, + + // Optional Parameters + bool suppressHeaderCheck = false, + string[] standardHeader = null, + string[] exemptFiles = null, + + string solutionFile = null, + string[] validConfigurations = null, + string githubOwner = "NUnit", + + bool msbuildAllowPreviewVersion = false, + Verbosity msbuildVerbosity = Verbosity.Minimal, + + string unitTests = null, // Defaults to "**/*.tests.dll|**/*.tests.exe" (case insensitive) + UnitTestRunner unitTestRunner = null, // If not set, NUnitLite is used + string unitTestArguments = null + ) + { + // Required arguments + Context = context ?? throw new ArgumentNullException(nameof(context)); + Title = title ?? throw new ArgumentNullException(nameof(title)); + GitHubRepository = githubRepository ?? throw new ArgumentNullException(nameof(githubRepository)); + + // NOTE: Order of initialization can be sensitive. Obviously, + // we have to set any properties in this method before we + // make use of them. Less obviously, some of the classes we + // construct here have dependencies on certain properties + // being set before the constructor is called. I have + // tried to annotate such dependencies below. + + _buildSystem = context.BuildSystem(); + + SolutionFile = solutionFile ?? DeduceSolutionFile(); + + ValidConfigurations = validConfigurations ?? DEFAULT_VALID_CONFIGS; + + UnitTests = unitTests; + // NUnitLiteRunner depends indirectly on ValidConfigurations + UnitTestRunner = unitTestRunner ?? new NUnitLiteRunner(); + UnitTestArguments = unitTestArguments; + + BuildVersion = new BuildVersion(context); + + GitHubOwner = githubOwner; + + // File Header Checks + SuppressHeaderCheck = suppressHeaderCheck && !CommandLineOptions.NoBuild; + StandardHeader = standardHeader ?? DEFAULT_STANDARD_HEADER; + ExemptFiles = exemptFiles ?? new string[0]; + + //if (defaultTarget != null) + // BuildTasks.DefaultTask.IsDependentOn(defaultTarget); + + // Skip remaining initialization if help was requested + if (CommandLineOptions.Usage) + return; + + ValidateSettings(); + + context.Information($"{Title} {Configuration} version {PackageVersion}"); + + // Output like this should go after the run title display + if (solutionFile == null && SolutionFile != null) + Context.Warning($" SolutionFile: '{SolutionFile}'"); + Context.Information($" PackageTestLevel: {PackageTestLevel}"); + + // Keep this last + if (IsRunningOnAppVeyor) + { + var buildNumber = _buildSystem.AppVeyor.Environment.Build.Number; + _buildSystem.AppVeyor.UpdateBuildVersion($"{PackageVersion}-{buildNumber}"); + } } - public string Target { get; } - public string Configuration { get; } + // If solution file was not provided, uses TITLE.sln if it exists or + // the solution found in the root directory provided there is only one. + private static string DeduceSolutionFile() + { + if (System.IO.File.Exists(Title + ".sln")) + return Title + ".sln"; + + var files = System.IO.Directory.GetFiles(ProjectDirectory, "*.sln"); + if (files.Length == 1 && System.IO.File.Exists(files[0])) + return files[0]; + + return null; + } + + private static int CalcPackageTestLevel() + { + if (!BuildVersion.IsPreRelease) + return 3; + + // TODO: The prerelease label is no longer being set to pr by GitVersion + // for some reason. This check in AppVeyor is a workaround. + if (IsRunningOnAppVeyor && _buildSystem.AppVeyor.Environment.PullRequest.IsPullRequest) + return 2; + + switch (BuildVersion.PreReleaseLabel) + { + case "pre": + case "rc": + case "alpha": + case "beta": + return 3; + + case "dev": + case "pr": + return 2; + + case "ci": + default: + return 1; + } + } + + // Cake Context + public static ICakeContext Context { get; private set; } + + // NOTE: These are set in setup.cake + public static string Target { get; set; } + public static IEnumerable TasksToExecute { get; set; } + + // Arguments + public static string Configuration + { + get + { + // Correct casing on user-provided config if necessary + foreach (string config in ValidConfigurations) + if (string.Equals(config, CommandLineOptions.Configuration.Value, StringComparison.OrdinalIgnoreCase)) + return config; + + // Return the (invalid) user-provided config + return CommandLineOptions.Configuration.Value; + } + } + + // Build Environment + public static bool IsLocalBuild => _buildSystem.IsLocalBuild; + public static bool IsRunningOnUnix => Context.IsRunningOnUnix(); + public static bool IsRunningOnWindows => Context.IsRunningOnWindows(); + public static bool IsRunningOnAppVeyor => _buildSystem.AppVeyor.IsRunningOnAppVeyor; + + // Versioning + public static BuildVersion BuildVersion { get; private set;} + public static string BranchName => BuildVersion.BranchName; + public static bool IsReleaseBranch => BuildVersion.IsReleaseBranch; + public static string PackageVersion => BuildVersion.PackageVersion; + public static string AssemblyVersion => BuildVersion.AssemblyVersion; + public static string AssemblyFileVersion => BuildVersion.AssemblyFileVersion; + public static string AssemblyInformationalVersion => BuildVersion.AssemblyInformationalVersion; + public static bool IsDevelopmentRelease => PackageVersion.Contains("-dev"); + + // Standard Directory Structure - not changeable by user + public static string ProjectDirectory => Context.Environment.WorkingDirectory.FullPath + "/"; + public static string SourceDirectory => ProjectDirectory + SRC_DIR; + public static string OutputDirectory => ProjectDirectory + BIN_DIR + Configuration + "/"; + public static string NuGetDirectory => ProjectDirectory + NUGET_DIR; + public static string ChocolateyDirectory => ProjectDirectory + CHOCO_DIR; + public static string MsiDirectory => ProjectDirectory + MSI_DIR; + public static string ZipDirectory => ProjectDirectory + ZIP_DIR; + public static string PackageDirectory => ProjectDirectory + PACKAGE_DIR; + public static string PackageTestDirectory => ProjectDirectory + PKG_TEST_DIR; + public static string NuGetTestDirectory => ProjectDirectory + NUGET_TEST_DIR; + public static string ChocolateyTestDirectory => ProjectDirectory + CHOCO_TEST_DIR; + public static string MsiTestDirectory => ProjectDirectory + MSI_TEST_DIR; + public static string ZipTestDirectory => ProjectDirectory + ZIP_TEST_DIR; + public static string PackageResultDirectory => ProjectDirectory + PKG_RSLT_DIR; + public static string NuGetResultDirectory => ProjectDirectory + NUGET_RSLT_DIR; + public static string ChocolateyResultDirectory => ProjectDirectory + CHOCO_RSLT_DIR; + public static string MsiResultDirectory => ProjectDirectory + MSI_RSLT_DIR; + public static string ZipResultDirectory => ProjectDirectory + ZIP_RSLT_DIR; + public static string ImageDirectory => ProjectDirectory + IMAGE_DIR; + public static string MsiImageDirectory => ProjectDirectory + MSI_IMG_DIR; + public static string ZipImageDirectory => ProjectDirectory + ZIP_IMG_DIR; + public static string ExtensionsDirectory => ProjectDirectory + "bundled-extensions/"; + public static string ToolsDirectory => ProjectDirectory + "tools/"; + + // Files + public static string SolutionFile { get; set; } + + // Building + public static string[] ValidConfigurations { get; set; } + public static bool MSBuildAllowPreviewVersion { get; set; } + public static Verbosity MSBuildVerbosity { get; set; } + public static MSBuildSettings MSBuildSettings => new MSBuildSettings { + Verbosity = MSBuildVerbosity, + Configuration = Configuration, + PlatformTarget = PlatformTarget.MSIL, + AllowPreviewVersion = MSBuildAllowPreviewVersion + }; + + // File Header Checks + public static bool SuppressHeaderCheck { get; private set; } + public static string[] StandardHeader { get; private set; } + public static string[] ExemptFiles { get; private set; } + + //Testing + public static string UnitTests { get; set; } + public static UnitTestRunner UnitTestRunner { get; private set; } + public static string UnitTestArguments { get; private set; } + + // Packaging + public static string Title { get; private set; } + public static List Packages { get; } = new List(); + + // Package Testing + public static int PackageTestLevel => + CommandLineOptions.TestLevel.Value > 0 + ? CommandLineOptions.TestLevel.Value + : CalcPackageTestLevel(); + + // Publishing + public static string MyGetPushUrl => MYGET_PUSH_URL; + public static string NuGetPushUrl => NUGET_PUSH_URL; + public static string ChocolateyPushUrl => CHOCO_PUSH_URL; + + public static string MyGetApiKey { get; private set; } + public static string NuGetApiKey { get; private set; } + public static string ChocolateyApiKey { get; private set;} + public static string GitHubOwner { get; private set; } + public static string GitHubRepository { get; private set; } + public static string GitHubAccessToken { get; private set; } + + public static bool IsPreRelease => BuildVersion.IsPreRelease; + public static bool ShouldPublishToMyGet => + !IsPreRelease || LABELS_WE_PUBLISH_ON_MYGET.Contains(BuildVersion.PreReleaseLabel); + public static bool ShouldPublishToNuGet => + !IsPreRelease || LABELS_WE_PUBLISH_ON_NUGET.Contains(BuildVersion.PreReleaseLabel); + public static bool ShouldPublishToChocolatey => + !IsPreRelease || LABELS_WE_PUBLISH_ON_CHOCOLATEY.Contains(BuildVersion.PreReleaseLabel); + public static bool IsProductionRelease => + !IsPreRelease || LABELS_WE_RELEASE_ON_GITHUB.Contains(BuildVersion.PreReleaseLabel); + + private static void ValidateSettings() + { + var validationErrors = new List(); + + if (!ValidConfigurations.Contains(Configuration)) + validationErrors.Add($"Invalid configuration: {Configuration}"); + + if (validationErrors.Count > 0) + { + DumpSettings(); + + var msg = new StringBuilder("Parameter validation failed! See settings above.\n\nErrors found:\n"); + foreach (var error in validationErrors) + msg.AppendLine(" " + error); + + throw new InvalidOperationException(msg.ToString()); + } + } + + public static void DumpSettings() + { + Console.WriteLine("\nTASKS"); + Console.WriteLine("Target: " + Target); + Console.WriteLine("TasksToExecute: " + string.Join(", ", TasksToExecute)); + + Console.WriteLine("\nENVIRONMENT"); + Console.WriteLine("IsLocalBuild: " + IsLocalBuild); + Console.WriteLine("IsRunningOnWindows: " + IsRunningOnWindows); + Console.WriteLine("IsRunningOnUnix: " + IsRunningOnUnix); + Console.WriteLine("IsRunningOnAppVeyor: " + IsRunningOnAppVeyor); + + Console.WriteLine("\nVERSIONING"); + Console.WriteLine("PackageVersion: " + PackageVersion); + Console.WriteLine("AssemblyVersion: " + AssemblyVersion); + Console.WriteLine("AssemblyFileVersion: " + AssemblyFileVersion); + Console.WriteLine("AssemblyInformationalVersion: " + AssemblyInformationalVersion); + Console.WriteLine("SemVer: " + BuildVersion.SemVer); + Console.WriteLine("IsPreRelease: " + BuildVersion.IsPreRelease); + Console.WriteLine("PreReleaseLabel: " + BuildVersion.PreReleaseLabel); + Console.WriteLine("PreReleaseSuffix: " + BuildVersion.PreReleaseSuffix); + + Console.WriteLine("\nDIRECTORIES"); + Console.WriteLine("Project: " + ProjectDirectory); + Console.WriteLine("Output: " + OutputDirectory); + Console.WriteLine("Source: " + SourceDirectory); + Console.WriteLine("NuGet: " + NuGetDirectory); + Console.WriteLine("Chocolatey: " + ChocolateyDirectory); + Console.WriteLine("Package: " + PackageDirectory); + Console.WriteLine("PackageTest: " + PackageTestDirectory); + Console.WriteLine("NuGetTest: " + NuGetTestDirectory); + Console.WriteLine("ChocoTest: " + ChocolateyTestDirectory); + Console.WriteLine("MsiTest: " + MsiTestDirectory); + Console.WriteLine("ZipTest: " + ZipTestDirectory); + Console.WriteLine("PackageResult: " + PackageResultDirectory); + Console.WriteLine("NuGetResult: " + NuGetResultDirectory); + Console.WriteLine("ChocoResult: " + ChocolateyResultDirectory); + Console.WriteLine("MsiResult: " + MsiResultDirectory); + Console.WriteLine("ZipResult: " + ZipResultDirectory); + Console.WriteLine("Image: " + ImageDirectory); + Console.WriteLine("MsiImage: " + MsiImageDirectory); + Console.WriteLine("ZipImage: " + ZipImageDirectory); + + Console.WriteLine("\nBUILD"); + Console.WriteLine("Configuration: " + Configuration); + + Console.WriteLine("\nUNIT TESTS"); + Console.WriteLine("UnitTests: " + UnitTests); + Console.WriteLine("UnitTestRunner: " + UnitTestRunner?.GetType().Name ?? ""); + + Console.WriteLine("\nPACKAGING"); + Console.WriteLine("MyGetPushUrl: " + MyGetPushUrl); + Console.WriteLine("NuGetPushUrl: " + NuGetPushUrl); + Console.WriteLine("ChocolateyPushUrl: " + ChocolateyPushUrl); + Console.WriteLine("MyGetApiKey: " + (!string.IsNullOrEmpty(MyGetApiKey) ? "AVAILABLE" : "NOT AVAILABLE")); + Console.WriteLine("NuGetApiKey: " + (!string.IsNullOrEmpty(NuGetApiKey) ? "AVAILABLE" : "NOT AVAILABLE")); + Console.WriteLine("ChocolateyApiKey: " + (!string.IsNullOrEmpty(ChocolateyApiKey) ? "AVAILABLE" : "NOT AVAILABLE")); + + Console.WriteLine("\nPACKAGES"); + foreach (var package in Packages) + { + Console.WriteLine(package.PackageId); + Console.WriteLine(" PackageType: " + package.PackageType); + Console.WriteLine(" PackageFileName: " + package.PackageFileName); + Console.WriteLine(" PackageInstallDirectory: " + package.PackageInstallDirectory); + Console.WriteLine(" PackageTestDirectory: " + package.PackageTestDirectory); + } + + Console.WriteLine("\nPUBLISHING"); + Console.WriteLine("ShouldPublishToMyGet: " + ShouldPublishToMyGet); + Console.WriteLine("ShouldPublishToNuGet: " + ShouldPublishToNuGet); + Console.WriteLine("ShouldPublishToChocolatey: " + ShouldPublishToChocolatey); + + Console.WriteLine("\nRELEASING"); + Console.WriteLine("BranchName: " + BranchName); + Console.WriteLine("IsReleaseBranch: " + IsReleaseBranch); + Console.WriteLine("IsProductionRelease: " + IsProductionRelease); + } } \ No newline at end of file diff --git a/cake/builder.cake b/cake/builder.cake new file mode 100644 index 000000000..ea91e367d --- /dev/null +++ b/cake/builder.cake @@ -0,0 +1,22 @@ +////////////////////////////////////////////////////////////////////// +// EXECUTION +////////////////////////////////////////////////////////////////////// + +public Builder Build => CommandLineOptions.Usage + ? new Builder(() => Information(HelpMessages.Usage)) + : new Builder(() => RunTarget(CommandLineOptions.Target.Value)); + +public class Builder +{ + private Action _action; + + public Builder(Action action) + { + _action = action; + } + + public void Run() + { + _action(); + } +} diff --git a/cake/chocolatey-package.cake b/cake/chocolatey-package.cake new file mode 100644 index 000000000..fa65eebed --- /dev/null +++ b/cake/chocolatey-package.cake @@ -0,0 +1,42 @@ +public class ChocolateyPackage : PackageDefinition +{ + public ChocolateyPackage( + string id, + string source, + PackageTestRunner testRunner = null, + PackageCheck[] checks = null, + IEnumerable tests = null) + : base( + PackageType.Chocolatey, + id, + source, + testRunner: testRunner, + checks: checks, + tests: tests) + { + if (!source.EndsWith(".nuspec")) + throw new ArgumentException("Source must be a nuspec file", nameof(source)); + } + + // The file name of this package, including extension + public override string PackageFileName => $"{PackageId}.{PackageVersion}.nupkg"; + // The file name of any symbol package, including extension + public override string SymbolPackageName => System.IO.Path.ChangeExtension(PackageFileName, ".snupkg"); + // The directory into which this package is installed + public override string PackageInstallDirectory => BuildSettings.ChocolateyTestDirectory; + // The directory used to contain results of package tests for this package + public override string PackageResultDirectory => BuildSettings.ChocolateyResultDirectory + PackageId + "/"; + // The directory into which extensions to the test runner are installed + public override string ExtensionInstallDirectory => BuildSettings.PackageTestDirectory; + + public override void BuildPackage() + { + _context.ChocolateyPack(PackageSource, + new ChocolateyPackSettings() + { + Version = PackageVersion, + OutputDirectory = BuildSettings.PackageDirectory, + ArgumentCustomization = args => args.Append($"BIN_DIR={BuildSettings.OutputDirectory}") + }); + } +} diff --git a/cake/command-line-options.cake b/cake/command-line-options.cake new file mode 100644 index 000000000..505fd7925 --- /dev/null +++ b/cake/command-line-options.cake @@ -0,0 +1,118 @@ +// *********************************************************************** +// Copyright (c) Charlie Poole and TestCentric GUI contributors. +// Licensed under the MIT License. See LICENSE.txt in root directory. +// *********************************************************************** + +CommandLineOptions.Initialize(Context); + +public static class CommandLineOptions +{ + static private ICakeContext _context; + + static public ValueOption Target; + static public ValueOption Configuration; + static public ValueOption PackageVersion; + static public ValueOption PackageSelector; + static public ValueOption TestLevel; + static public ValueOption TraceLevel; + static public SimpleOption NoBuild; + static public SimpleOption NoPush; + static public SimpleOption Usage; + + public static void Initialize(ICakeContext context) + { + _context = context; + + // The name of the TARGET task to be run, e.g. Test. + Target = new ValueOption("target", "Default", 1); + + Configuration = new ValueOption("configuration", DEFAULT_CONFIGURATION, 1); + + PackageVersion = new ValueOption("packageVersion", null, 4); + + PackageSelector = new ValueOption("where", null, 1); + + TestLevel = new ValueOption("level", 0, 3); + + TraceLevel = new ValueOption("trace", "Off", 2); + + NoBuild = new SimpleOption("nobuild", 3); + + NoPush = new SimpleOption("nopush", 3); + + Usage = new SimpleOption("usage", 2); + } + + // Nested classes to represent individual options + + // AbstractOption has a name and can tell us if it exists. + public abstract class AbstractOption + { + public string Name { get; } + + public int MinimumAbbreviation { get; internal set; } + + public bool Exists + { + get + { + for (int len = Name.Length; len >= MinimumAbbreviation; len--) + if (_context.HasArgument(Name.Substring(0,len))) + return true; + return false; + } + } + + public string Description { get; } + + public AbstractOption(string name, int minimumAbbreviation = 0, string description = null) + { + Name = name; + MinimumAbbreviation = minimumAbbreviation > 0 && minimumAbbreviation <= name.Length + ? minimumAbbreviation + : name.Length; + Description = description; + } + } + + // Simple Option adds an implicit boolean conversion operator. + // It throws an exception if you gave it a value on the command-line. + public class SimpleOption : AbstractOption + { + static public implicit operator bool(SimpleOption o) => o.Exists; + + public SimpleOption(string name, int minimumAbbreviation = 0, string description = null) + : base(name, minimumAbbreviation, description) + { + if (_context.Argument(name, (string)null) != null) + throw new Exception($"Option --{name} does not take a value."); + } + } + + // Generic ValueOption adds Value as well as a default value + public class ValueOption : AbstractOption + { + public T DefaultValue { get; } + + public ValueOption(string name, T defaultValue, int minimumAbbreviation = 0, string description = null) + : base(name, minimumAbbreviation, description) + { + DefaultValue = defaultValue; + } + + public T Value + { + get + { + for (int len = Name.Length; len >= MinimumAbbreviation; len--) + { + string abbrev = Name.Substring(0,len); + if (_context.HasArgument(abbrev)) + return _context.Argument(abbrev); + } + + return DefaultValue; + } + } + } +} diff --git a/cake/constants.cake b/cake/constants.cake index c3b2682f2..d73edda5a 100644 --- a/cake/constants.cake +++ b/cake/constants.cake @@ -1,68 +1,40 @@ -////////////////////////////////////////////////////////////////////// -// RUNTIME CONSTANTS AND VARIABLES USED AS CONSTANTS -////////////////////////////////////////////////////////////////////// - -// Some values are static so they may be used in property initialization and in -// classes. Initialization is separate to allow use of non-constant expressions. - -// Directories -static string PROJECT_DIR; PROJECT_DIR = Context.Environment.WorkingDirectory.FullPath + "/"; -static string PACKAGE_DIR; PACKAGE_DIR = Argument("artifact-dir", PROJECT_DIR + "package") + "/"; -static string PACKAGE_TEST_DIR; PACKAGE_TEST_DIR = PACKAGE_DIR + "tests/"; -static string PACKAGE_RESULT_DIR; PACKAGE_RESULT_DIR = PACKAGE_DIR + "results/"; -static string BIN_DIR; BIN_DIR = PROJECT_DIR + "bin/" + Configuration + "/"; -static string NUGET_DIR; NUGET_DIR = PROJECT_DIR + "nuget/"; -static string CHOCO_DIR; CHOCO_DIR = PROJECT_DIR + "choco/"; -static string MSI_DIR; MSI_DIR = PROJECT_DIR + "msi/"; -static string ZIP_DIR; ZIP_DIR = PROJECT_DIR + "zip/"; -static string TOOLS_DIR; TOOLS_DIR = PROJECT_DIR + "tools/"; -static string IMAGE_DIR; IMAGE_DIR = PROJECT_DIR + "images/"; -static string MSI_IMG_DIR; MSI_IMG_DIR = IMAGE_DIR + "msi/"; -static string ZIP_IMG_DIR; ZIP_IMG_DIR = IMAGE_DIR + "zip/"; -static string SOURCE_DIR; SOURCE_DIR = PROJECT_DIR + "src/"; -static string EXTENSIONS_DIR; EXTENSIONS_DIR = PROJECT_DIR + "bundled-extensions"; - -// Solution and Projects -var SOLUTION_FILE = PROJECT_DIR + "NUnitConsole.sln"; -var ENGINE_PROJECT = SOURCE_DIR + "NUnitEngine/nunit.engine/nunit.engine.csproj"; -var AGENT_PROJECT = SOURCE_DIR + "NUnitEngine/nunit-agent/nunit-agent.csproj"; -var AGENT_X86_PROJECT = SOURCE_DIR + "NUnitEngine/nunit-agent-x86/nunit-agent-x86.csproj"; -var ENGINE_API_PROJECT = SOURCE_DIR + "NUnitEngine/nunit.engine.api/nunit.engine.api.csproj"; -var ENGINE_CORE_PROJECT = SOURCE_DIR + "NUnitEngine/nunit.engine.core/nunit.engine.core.csproj"; -var ENGINE_TESTS_PROJECT = SOURCE_DIR + "NUnitEngine/nunit.engine.tests/nunit.engine.tests.csproj"; -var ENGINE_CORE_TESTS_PROJECT = SOURCE_DIR + "NUnitEngine/nunit.engine.core.tests/nunit.engine.core.tests.csproj"; -var CONSOLE_PROJECT = SOURCE_DIR + "NUnitConsole/nunit3-console/nunit3-console.csproj"; -var CONSOLE_TESTS_PROJECT = SOURCE_DIR + "NUnitConsole/nunit3-console.tests/nunit3-console.tests.csproj"; -var MOCK_ASSEMBLY_PROJECT = SOURCE_DIR + "NUnitEngine/mock-assembly/mock-assembly.csproj"; -var MOCK_ASSEMBLY_X86_PROJECT = SOURCE_DIR + "NUnitEngine/mock-assembly-x86/mock-assembly-x86.csproj"; -var NOTEST_PROJECT = SOURCE_DIR + "NUnitEngine/notest-assembly/notest-assembly.csproj"; -// Console Runner -var NET20_CONSOLE = BIN_DIR + "net20/nunit3-console.exe"; -var NET60_CONSOLE = BIN_DIR + "net6.0/nunit3-console.dll"; -var NET80_CONSOLE = BIN_DIR + "net8.0/nunit3-console.dll"; -// Unit Tests -var NETFX_ENGINE_CORE_TESTS = "nunit.engine.core.tests.exe"; -var NETCORE_ENGINE_CORE_TESTS = "nunit.engine.core.tests.dll"; -var NETFX_ENGINE_TESTS = "nunit.engine.tests.exe"; -var NETCORE_ENGINE_TESTS = "nunit.engine.tests.dll"; -var CONSOLE_TESTS = "nunit3-console.tests.dll"; - -// Package sources for nuget restore -var PACKAGE_SOURCE = new string[] -{ - "https://www.nuget.org/api/v2", - "https://www.myget.org/F/nunit/api/v2" -}; - -// Extensions we bundle -var BUNDLED_EXTENSIONS = new[] -{ - "NUnit.Extension.VSProjectLoader", - "NUnit.Extension.NUnitProjectLoader", - "NUnit.Extension.NUnitV2Driver", - "NUnit.Extension.NUnitV2ResultWriter", - "NUnit.Extension.TeamCityEventListener" -}; +// This file contains both real constants and static readonly variables used +// as constants. All values are initialized before any instance variables. + +// GitHub owner is the NUnit organization +const string GITHUB_OWNER = "nunit"; + +// Defaults +const string DEFAULT_CONFIGURATION = "Release"; +private static readonly string[] DEFAULT_VALID_CONFIGS = { "Release", "Debug" }; +static readonly string[] DEFAULT_STANDARD_HEADER = new[] { + "// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt" }; +const string DEFAULT_TEST_RESULT_FILE = "TestResult.xml"; + +// Standardized project directory structure - not changeable by user +const string SRC_DIR = "src/"; +const string BIN_DIR = "bin/"; +const string NUGET_DIR = "nuget/"; +const string CHOCO_DIR = "choco/"; +const string MSI_DIR = "msi/"; +const string ZIP_DIR = "zip/"; +const string PACKAGE_DIR = "package/"; +const string PKG_TEST_DIR = "package/tests/"; +const string NUGET_TEST_DIR = "package/tests/nuget/"; +//const string NUGET_RUNNER_DIR = "package/tests/nuget/runners/"; +const string CHOCO_TEST_DIR = "package/tests/choco/"; +//const string CHOCO_RUNNER_DIR = "package/tests/choco/runners/"; +const string MSI_TEST_DIR = "package/tests/msi/"; +const string ZIP_TEST_DIR = "package/tests/zip/"; +const string PKG_RSLT_DIR = "package/results/"; +const string NUGET_RSLT_DIR = "package/results/nuget/"; +const string CHOCO_RSLT_DIR = "package/results/choco/"; +const string MSI_RSLT_DIR = "package/results/msi/"; +const string ZIP_RSLT_DIR = "package/results/zip/"; +const string IMAGE_DIR = "package/images"; +const string MSI_IMG_DIR = "package/images/msi/"; +const string ZIP_IMG_DIR = "package/images/zip/"; +const string TOOLS_DIR = "tools/"; // URLs for uploading packages private const string MYGET_PUSH_URL = "https://www.myget.org/F/nunit/api/v2"; @@ -73,10 +45,6 @@ private const string CHOCO_PUSH_URL = "https://push.chocolatey.org/"; private const string MYGET_API_KEY = "MYGET_API_KEY"; private const string NUGET_API_KEY = "NUGET_API_KEY"; private const string CHOCO_API_KEY = "CHOCO_API_KEY"; - -// GitHub Information -private const string GITHUB_OWNER = "nunit"; -private const string GITHUB_REPO = "nunit-console"; private const string GITHUB_ACCESS_TOKEN = "GITHUB_ACCESS_TOKEN"; // Pre-release labels that we publish diff --git a/cake/extending.cake b/cake/extending.cake new file mode 100644 index 000000000..ed2eed10b --- /dev/null +++ b/cake/extending.cake @@ -0,0 +1,57 @@ +// This file provides information on how to change the pre-defined tasks in +// the NUnit Cake Recipe for a particular project, without changing the +// recipe files themselves. Those files should not be changed unless you +// are trying to make changes for all projects, which use the recipe. +// +// In addition, this file defines a few static methods intended to make it +// easier to override task definitions for a project. +// +// Code given in the following examples should be added to your file after +// the recipe itself has been loaded. +// +// SIMPLE EXAMPLES: +// +// Adding a new dependency after those already present +// BuildTasks.SomeTask.IsDependentOn("SomeOtherTask"); +// +// Adding a new action, which will be performed after the existing action. +// BuildTasks.SomeTask.Does(() => { +// // Code here to do something +// }); +// +// MORE COMPLEX EXAMPLES: +// +// Replacing the existing dependencies, criteria or actions is a bit harder. +// You must first clear the relevant items and then re-add them in the desired +// order along with any new items. The following static methods allow you to do +// so a bit more simply than would otherwise be possible. + +public static void ClearCriteria(CakeTaskBuilder builder) => ((CakeTask)builder.Task).Criterias.Clear(); +public static void ClearDependencies(CakeTaskBuilder builder) => ((CakeTask)builder.Task).Dependencies.Clear(); +public static void ClearActions(CakeTaskBuilder builder) => ((CakeTask)builder.Task).Actions.Clear(); + +// Redefining the action of the build task. Note that dependencies will still run, since we haven't changed them. +// ClearActions(BuildTasks.Build); +// BuildTasks.Build.Does(() => Information("I'm not building today!")); +// +// In some cases, you may wish to completely redefine what a task does. The following static method +// supports a fluent syntax for doing just that + +public static CakeTaskBuilder Redefine(CakeTaskBuilder builder) +{ + ClearCriteria(builder); + ClearDependencies(builder); + ClearActions(builder); + + return builder; +} + +// Redefine the build task completely. Note that in this case, dependencies are not run. +// Redefine(BuildTasks.BuildTask) +// .Does(() => { +// Information("Not Building today!"); +// }); +// +// Modify the Default task for a project to run "Test" rather than "Build". +// Redefine(BuildTasks.DefaultTask) +// .IsDependentOn("Test"); diff --git a/cake/header-check.cake b/cake/header-check.cake deleted file mode 100644 index c4f385131..000000000 --- a/cake/header-check.cake +++ /dev/null @@ -1,102 +0,0 @@ -////////////////////////////////////////////////////////////////////// -// CHECK FOR MISSING AND NON-STANDARD FILE HEADERS -////////////////////////////////////////////////////////////////////// - -static readonly int CD_LENGTH = Environment.CurrentDirectory.Length + 1; - -static readonly string[] EXEMPT_FILES = new [] { - "Options.cs", - "ProcessUtils.cs", - "ProcessUtilsTests.cs" -}; - -// Standard Header. Change this for each project as needed. -static readonly string[] STD_HDR = new [] { - "// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt" -}; - -Task("CheckHeaders") - .Does(() => - { - var NoHeader = new List(); - var NonStandard = new List(); - var Exempted = new List(); - int examined = 0; - - foreach(var file in GetFiles("src/**/*.cs")) - { - // Ignore autogenerated files in an obj directory - if (file.ToString().Contains("/obj/")) - continue; - - var filename = file.GetFilename().ToString(); - if (filename == "AssemblyInfo.cs") - continue; - - examined++; - var header = GetHeader(file); - - if (EXEMPT_FILES.Contains(filename)) - Exempted.Add(file); - else if (header.Count == 0) - NoHeader.Add(file); - else if (!header.SequenceEqual(STD_HDR)) - NonStandard.Add(file); - } - - if (NoHeader.Count > 0) - { - Information("\nFILES WITH NO HEADER\n"); - foreach(var file in NoHeader) - Information(RelPathTo(file)); - } - - if (NonStandard.Count > 0) - { - Information("\nFILES WITH A NON-STANDARD HEADER\n"); - foreach(var file in NonStandard) - { - Information(RelPathTo(file)); - Information(""); - foreach(string line in GetHeader(file)) - Information(line); - Information(""); - } - } - - if (Exempted.Count > 0) - { - Information("\nEXEMPTED FILES (NO CHECK MADE)\n"); - foreach(var file in Exempted) - Information(RelPathTo(file)); - } - - Information($"\nFiles Examined: {examined}"); - Information($"Missing Headers: {NoHeader.Count}"); - Information($"Non-Standard Headers: {NonStandard.Count}"); - Information($"Exempted Files: {Exempted.Count}"); - - if (NoHeader.Count > 0 || NonStandard.Count > 0) - throw new Exception("Missing or invalid file headers found"); - }); - -private List GetHeader(FilePath file) -{ - var header = new List(); - var lines = System.IO.File.ReadLines(file.ToString()); - - foreach(string line in lines) - { - if (!line.StartsWith("//")) - break; - - header.Add(line); - } - - return header; -} - -private string RelPathTo(FilePath file) -{ - return file.ToString().Substring(CD_LENGTH); -} diff --git a/cake/headers.cake b/cake/headers.cake new file mode 100644 index 000000000..3ad3e77d4 --- /dev/null +++ b/cake/headers.cake @@ -0,0 +1,115 @@ +////////////////////////////////////////////////////////////////////// +// CHECK FOR MISSING AND NON-STANDARD FILE HEADERS +////////////////////////////////////////////////////////////////////// + +public static class Headers +{ + private static ICakeContext _context; + + static Headers() + { + _context = BuildSettings.Context; + } + + public static void Check() + { + var NoHeader = new List(); + var NonStandard = new List(); + var Exempted = new List(); + int examined = 0; + + var sourceFiles = _context.GetFiles(BuildSettings.SourceDirectory + "**/*.cs"); + var exemptFiles = BuildSettings.ExemptFiles; + foreach (var file in sourceFiles) + { + var path = file.ToString(); + + // Ignore autogenerated files in an obj directory + if (path.Contains("/obj/")) + continue; + + // Ignore designer files + if (path.EndsWith(".Designer.cs")) + continue; + + // Ignore AssemblyInfo files + if (System.IO.Path.GetFileName(path) == "AssemblyInfo.cs") + continue; + + examined++; + var header = GetHeader(file); + if (exemptFiles.Contains(file.GetFilename().ToString())) + Exempted.Add(file); + else if (header.Count == 0) + NoHeader.Add(file); + else if (!header.SequenceEqual(BuildSettings.StandardHeader)) + NonStandard.Add(file); + } + + _context.Information("\nSTANDARD HEADER\n"); + foreach (string line in BuildSettings.StandardHeader) + _context.Information(line); + _context.Information(""); + + if (NoHeader.Count > 0) + { + _context.Information("\nFILES WITH NO HEADER\n"); + foreach (var file in NoHeader) + _context.Information(RelPathTo(file)); + } + + if (NonStandard.Count > 0) + { + _context.Information("\nFILES WITH A NON-STANDARD HEADER\n"); + foreach (var file in NonStandard) + { + _context.Information(RelPathTo(file)); + _context.Information(""); + foreach (string line in GetHeader(file)) + _context.Information(line); + _context.Information(""); + } + } + + if (Exempted.Count > 0) + { + _context.Information("\nEXEMPTED FILES (NO CHECK MADE)\n"); + foreach (var file in Exempted) + _context.Information(RelPathTo(file)); + } + + _context.Information($"\nFiles Examined: {examined}"); + _context.Information($"Missing Headers: {NoHeader.Count}"); + _context.Information($"Non-Standard Headers: {NonStandard.Count}"); + _context.Information($"Exempted Files: {Exempted.Count}"); + + if (NoHeader.Count > 0 || NonStandard.Count > 0) + throw new Exception("Missing or invalid file headers found"); + + if (examined == 0) + _context.Warning("\nWARNING: There were no '*.cs' files in the source directory. Use of the 'CheckHeaders' task may not make sense for this project."); + + List GetHeader(FilePath file) + { + var header = new List(); + var lines = System.IO.File.ReadLines(file.ToString()); + + foreach (string line in lines) + { + if (!line.StartsWith("//")) + break; + + header.Add(line); + } + + return header; + } + + string RelPathTo(FilePath file) + { + int CD_LENGTH = Environment.CurrentDirectory.Length + 1; + + return file.ToString().Substring(CD_LENGTH); + } + } +} \ No newline at end of file diff --git a/cake/help-messages.cake b/cake/help-messages.cake new file mode 100644 index 000000000..d615f04c9 --- /dev/null +++ b/cake/help-messages.cake @@ -0,0 +1,77 @@ +static public class HelpMessages +{ + static public string Usage => $""" + BUILD.CAKE + + This script builds the {BuildSettings.Title} project. It makes use of + NUnit.Cake.Recipe, which provides a number of built-in options and + tasks. You may define additional options and tasks in build.cake or + in additional cake files you load from build.cake. Recipe options may + be specified in abbreviated form, down to the minimum length shown in + square braces. Single character abbreviations use a single dash. + + Usage: build [options] + + Options: + + --target=TARGET [-t] + The TARGET task to be run, e.g. Test. Default is Build. + + --configuration=CONFIG [-c] + The name of the configuration to build. Default is Release. + + --packageVersion [--pack] + Specifies the full package version, including any pre-release + suffix. If provided, this version is used directly in place of + the default version calculated by the script. + + --where=SELECTION [-w] + Specifies a selction expression used to choose which packages + to build and test, for use in debugging. Consists of one or + more specifications, separated by '|' and '&'. Each specification + is of the form "prop=value", where prop may be either id or type. + Examples: + --where type=msi + --where id=NUnit.Engine.Api + --where "type=msi|type=zip" + + --level=LEVEL [--lev] + Specifies the level of package testing, 1, 2 or 3. Defaults are + 1 = for normal CI tests run every time you build a package + 2 = for PRs and Dev builds uploaded to MyGet + 3 = prior to publishing a release + + --trace=LEVEL [--tr] + Specifies the default trace level for this run. Values are Off, + Error, Warning, Info or Debug. Default is value of environment + variable NUNIT_INTERNAL_TRACE_LEVEL if set. If not, + tracing is turned Off. + + --nobuild [--nob] + Indicates that the Build task should not be run even if other + tasks depend on it. The existing build is used instead. + + --nopush [--nop] + Indicates that no publishing or releasing should be done. If + publish or release targets are run, a message is displayed. + + --usage [--us] + Displays this help message. No targets are run. + + Selected Cake Options: + + --version + Displays the cake version in use. + + --description + Displays a list of the available tasks (targets). + + --tree + Displays the task dependency tree + + --help + Displays help information for cake itself. + + NOTE: The above Cake options bypass execution of the script. + """; +} diff --git a/cake/known-extensions.cake b/cake/known-extensions.cake new file mode 100644 index 000000000..b2bdc4c42 --- /dev/null +++ b/cake/known-extensions.cake @@ -0,0 +1,48 @@ +// Static class holding information about known extensions. +public static class KnownExtensions +{ + // Static Variables representing well-known Extensions with the latest tested version + public static ExtensionSpecifier NUnitV2Driver = new ExtensionSpecifier( + "NUnit.Extension.NUnitV2Driver", "nunit-extension-nunit-v2-driver", "3.9.0"); + public static ExtensionSpecifier NUnitProjectLoader = new ExtensionSpecifier( + "NUnit.Extension.NUnitProjectLoader", "nunit-extension-nunit-project-loader", "3.7.1"); +} + +// Representation of an extension, for use by PackageTests. Because our +// extensions usually exist as both nuget and chocolatey packages, each +// extension may have a nuget id, a chocolatey id or both. A default version +// is used unless the user overrides it using SetVersion. +public class ExtensionSpecifier +{ + public ExtensionSpecifier(string nugetId, string chocoId, string version) + { + NuGetId = nugetId; + ChocoId = chocoId; + Version = version; + } + + public string NuGetId { get; } + public string ChocoId { get; } + public string Version { get; } + + public PackageReference NuGetPackage => new PackageReference(NuGetId, Version); + public PackageReference ChocoPackage => new PackageReference(ChocoId, Version); + public PackageReference LatestChocolateyRelease => ChocoPackage.LatestRelease; + + // Return an extension specifier using the same package ids as this + // one but specifying a particular version to be used. + public ExtensionSpecifier SetVersion(string version) + { + return new ExtensionSpecifier(NuGetId, ChocoId, version); + } + + // Install this extension for a package + public void InstallExtension(PackageDefinition targetPackage) + { + PackageReference extensionPackage = targetPackage.PackageType == PackageType.Chocolatey + ? ChocoPackage + : NuGetPackage; + + extensionPackage.Install(targetPackage.ExtensionInstallDirectory); + } +} diff --git a/cake/msi-package.cake b/cake/msi-package.cake new file mode 100644 index 000000000..224608282 --- /dev/null +++ b/cake/msi-package.cake @@ -0,0 +1,95 @@ +public class MsiPackage : PackageDefinition +{ + public MsiPackage( + string id, + string source, + PackageTestRunner testRunner = null, + PackageCheck[] checks = null, + IEnumerable tests = null, + PackageReference[] bundledExtensions = null) + : base( + PackageType.Msi, + id, + source, + testRunner: testRunner, + checks: checks, + tests: tests) + { + PackageVersion = BuildSettings.BuildVersion.SemVer; + BundledExtensions = bundledExtensions; + } + + // MSI and ZIP packages support bundling of extensions + // if any are specified in the definition. + public PackageReference[] BundledExtensions { get; } + + // The file name of this package, including extension + public override string PackageFileName => $"{PackageId}-{PackageVersion}.msi"; + // The directory into which this package is installed + public override string PackageInstallDirectory => BuildSettings.MsiTestDirectory; + // The directory used to contain results of package tests for this package + public override string PackageResultDirectory => BuildSettings.MsiResultDirectory + PackageId + "/"; + // The directory into which extensions to the test runner are installed + // TODO: FIx this for msi addins + public override string ExtensionInstallDirectory => BuildSettings.PackageTestDirectory; + + public override void BuildPackage() + { + FetchBundledExtensions(BundledExtensions); + + CreateMsiImage(); + + _context.MSBuild(PackageSource, new MSBuildSettings() + .WithTarget("Rebuild") + .SetConfiguration(BuildSettings.Configuration) + .WithProperty("Version", PackageVersion) + .WithProperty("DisplayVersion", PackageVersion) + .WithProperty("OutDir", BuildSettings.PackageDirectory) + .WithProperty("Image", BuildSettings.MsiImageDirectory) + .SetMSBuildPlatform(MSBuildPlatform.x86) + .SetNodeReuse(false)); + } + + private void CreateMsiImage() + { + _context.CleanDirectory(BuildSettings.MsiImageDirectory); + + _context.CopyFiles( + new FilePath[] { "LICENSE.txt", "NOTICES.txt", "CHANGES.txt", "nunit.ico" }, + BuildSettings.MsiImageDirectory); + + _context.CopyDirectory( + BuildSettings.OutputDirectory, + BuildSettings.MsiImageDirectory + "bin/" ); + + foreach (var runtime in new[] { "net20", "net35" }) + { + var addinsImgDir = BuildSettings.MsiImageDirectory + $"bin/{runtime}/addins/"; + + _context.CopyDirectory( + BuildSettings.MsiDirectory + "resources/", + BuildSettings.MsiImageDirectory); + + _context.CleanDirectory(addinsImgDir); + + foreach (var packageDir in System.IO.Directory.GetDirectories(BuildSettings.ExtensionsDirectory)) + { + var files = _context.GetFiles(packageDir + "/tools/*").Concat(_context.GetFiles(packageDir + "/tools/net20/*")); + _context.CopyFiles(files.Where(f => f.GetExtension() != ".addins"), addinsImgDir); + } + } + } + + public override void InstallPackage() + { + // Msiexec does not tolerate forward slashes! + string package = PackageFilePath.Replace("/", "\\"); + string testDir = System.IO.Path.Combine(PackageInstallDirectory.Replace("/", "\\"), $"{PackageId}.{PackageVersion}"); + + Console.WriteLine($"Installing msi to {testDir}"); + int rc = _context.StartProcess("msiexec", $"/a {package} TARGETDIR={testDir} /q"); + + if (rc != 0) + Console.WriteLine($" ERROR: Installer returned {rc.ToString()}"); + } +} diff --git a/cake/nuget-package.cake b/cake/nuget-package.cake new file mode 100644 index 000000000..979fb7c68 --- /dev/null +++ b/cake/nuget-package.cake @@ -0,0 +1,56 @@ +public class NuGetPackage : PackageDefinition +{ + public NuGetPackage( + string id, + string source, + PackageTestRunner testRunner = null, + PackageCheck[] checks = null, + PackageCheck[] symbols = null, + IEnumerable tests = null) + : base( + PackageType.NuGet, + id, + source, + testRunner: testRunner, + checks: checks, + symbols: symbols, + tests: tests) + { + if (!source.EndsWith(".nuspec")) + throw new ArgumentException("Source must be a nuspec file", nameof(source)); + + if (symbols != null) + { + HasSymbols = true; + SymbolChecks = symbols; + } + } + + // The file name of this package, including extension + public override string PackageFileName => $"{PackageId}.{PackageVersion}.nupkg"; + // The file name of any symbol package, including extension + public override string SymbolPackageName => System.IO.Path.ChangeExtension(PackageFileName, ".snupkg"); + // The directory into which this package is installed + public override string PackageInstallDirectory => BuildSettings.NuGetTestDirectory; + // The directory used to contain results of package tests for this package + public override string PackageResultDirectory => BuildSettings.NuGetResultDirectory + PackageId + "/"; + // The directory into which extensions to the test runner are installed + public override string ExtensionInstallDirectory => BuildSettings.PackageTestDirectory; + + public override void BuildPackage() + { + var nugetPackSettings = new NuGetPackSettings() + { + Version = PackageVersion, + BasePath = BuildSettings.OutputDirectory, + OutputDirectory = BuildSettings.PackageDirectory, + NoPackageAnalysis = true, + Symbols = HasSymbols + }; + + if (HasSymbols) + nugetPackSettings.SymbolPackageFormat = "snupkg"; + + _context.NuGetPack(PackageSource, nugetPackSettings); + } +} diff --git a/cake/package-checks.cake b/cake/package-checks.cake index be45ec4da..66e91df9b 100644 --- a/cake/package-checks.cake +++ b/cake/package-checks.cake @@ -1,228 +1,148 @@ ////////////////////////////////////////////////////////////////////// -// LISTS OF FILES USED IN CHECKING PACKAGES +// SYNTAX FOR EXPRESSING CHECKS ////////////////////////////////////////////////////////////////////// -string[] ENGINE_FILES = { - "nunit.engine.dll", "nunit.engine.core.dll", "nunit.engine.api.dll", "testcentric.engine.metadata.dll" }; -string[] ENGINE_PDB_FILES = { - "nunit.engine.pdb", "nunit.engine.core.pdb", "nunit.engine.api.pdb"}; -string[] ENGINE_CORE_FILES = { - "nunit.engine.core.dll", "nunit.engine.api.dll", "testcentric.engine.metadata.dll" }; -string[] ENGINE_CORE_PDB_FILES = { - "nunit.engine.core.pdb", "nunit.engine.api.pdb"}; -string[] AGENT_FILES = { - "nunit-agent.exe", "nunit-agent.exe.config", - "nunit-agent-x86.exe", "nunit-agent-x86.exe.config", - "nunit.engine.core.dll", "nunit.engine.api.dll", "testcentric.engine.metadata.dll"}; -string[] AGENT_FILES_NETCORE = { - "nunit-agent.dll", "nunit-agent.dll.config", - "nunit.engine.core.dll", "nunit.engine.api.dll", "testcentric.engine.metadata.dll", - "Microsoft.Extensions.DependencyModel.dll"}; -string[] AGENT_PDB_FILES = { - "nunit-agent.pdb", "nunit-agent-x86.pdb", "nunit.engine.core.pdb", "nunit.engine.api.pdb"}; -string[] AGENT_PDB_FILES_NETCORE = { - "nunit-agent.pdb", "nunit.engine.core.pdb", "nunit.engine.api.pdb"}; -string[] CONSOLE_FILES = { - "nunit3-console.exe", "nunit3-console.exe.config" }; -string[] CONSOLE_FILES_NETCORE = { - "nunit3-console.exe", "nunit3-console.dll" }; - -////////////////////////////////////////////////////////////////////// -// PACKAGE CHECK IMPLEMENTATION -////////////////////////////////////////////////////////////////////// - -// NOTE: Package checks basically do no more than what the programmer might -// do in opening the package itself and examining the content. - -public bool CheckPackage(string package, params PackageCheck[] checks) +public static class Check { - Console.WriteLine("\nPackage Name: " + System.IO.Path.GetFileName(package)); + public static void That(DirectoryPath testDirPath, IList checks) + { + if (checks == null) + throw new ArgumentNullException(nameof(checks)); - if (!FileExists(package)) - { - WriteError("Package was not found!"); - return false; - } + bool allOK = true; - if (checks.Length == 0) - { - WriteWarning("Package found but no checks were specified."); - return true; - } - - bool isMsi = package.EndsWith(".msi"); - string tempDir = isMsi - ? InstallMsiToTempDir(package) - : UnzipToTempDir(package); - - if (!System.IO.Directory.Exists(tempDir)) - { - WriteError("Temporary directory was not created!"); - return false; - } - - try - { - bool allPassed = ApplyChecks(tempDir, checks); - if (allPassed) - WriteInfo("All checks passed!"); + foreach (var check in checks) + allOK &= check.ApplyTo(testDirPath); - return allPassed; + if (!allOK) throw new Exception("Verification failed!"); } - finally - { - DeleteDirectory(tempDir, new DeleteDirectorySettings() - { - Recursive = true, - Force = true - }); - } -} - -private string InstallMsiToTempDir(string package) -{ - // Msiexec does not tolerate forward slashes! - package = package.Replace("/", "\\"); - var tempDir = GetTempDirectoryPath(); - - WriteInfo("Installing to " + tempDir); - int rc = StartProcess("msiexec", $"/a {package} TARGETDIR={tempDir} /q"); - if (rc != 0) - WriteError($"Installer returned {rc.ToString()}"); - - return tempDir; -} - -private string UnzipToTempDir(string package) -{ - var tempDir = GetTempDirectoryPath(); - - WriteInfo("Unzipping to " + tempDir); - Unzip(package, tempDir); - - return tempDir; } -private string GetTempDirectoryPath() -{ - return System.IO.Path.GetTempPath() + System.IO.Path.GetRandomFileName() + "\\"; -} +private static FileCheck HasFile(FilePath file) => HasFiles(new[] { file }); +private static FileCheck HasFiles(params FilePath[] files) => new FileCheck(files); -private bool ApplyChecks(string dir, PackageCheck[] checks) -{ - bool allOK = true; - - foreach (var check in checks) - allOK &= check.Apply(dir); +private static DirectoryCheck HasDirectory(string dir) => new DirectoryCheck(dir); - return allOK; -} +////////////////////////////////////////////////////////////////////// +// PACKAGECHECK CLASS +////////////////////////////////////////////////////////////////////// public abstract class PackageCheck { - public abstract bool Apply(string dir); + protected ICakeContext _context; + + public PackageCheck() + { + _context = BuildSettings.Context; + } + + public abstract bool ApplyTo(DirectoryPath testDirPath); + + protected bool CheckDirectoryExists(DirectoryPath dirPath) + { + if (!_context.DirectoryExists(dirPath)) + { + DisplayError($"Directory {dirPath} was not found."); + return false; + } + + return true; + } + + protected bool CheckFileExists(FilePath filePath) + { + if (!_context.FileExists(filePath)) + { + DisplayError($"File {filePath} was not found."); + return false; + } + + return true; + } + + protected bool CheckFilesExist(IEnumerable filePaths) + { + bool isOK = true; + + foreach (var filePath in filePaths) + isOK &= CheckFileExists(filePath); + + return isOK; + } + + protected bool DisplayError(string msg) + { + _context.Error(" ERROR: " + msg); + + // The return value may be ignored or used as a shortcut + // for an immediate return from ApplyTo as in + // return DisplayError(...) + return false; + } } +////////////////////////////////////////////////////////////////////// +// FILECHECK CLASS +////////////////////////////////////////////////////////////////////// + public class FileCheck : PackageCheck { - string[] _paths; + FilePath[] _files; - public FileCheck(string[] paths) - { - _paths = paths; - } + public FileCheck(FilePath[] files) + { + _files = files; + } - public override bool Apply(string dir) - { - var isOK = true; - - foreach (string path in _paths) - { - if (!System.IO.File.Exists(dir + path)) - { - WriteError($"File {path} was not found."); - isOK = false; - } - } - - return isOK; - } + public override bool ApplyTo(DirectoryPath testDirPath) + { + return CheckFilesExist(_files.Select(file => testDirPath.CombineWithFilePath(file))); + } } +////////////////////////////////////////////////////////////////////// +// DIRECTORYCHECK CLASS +////////////////////////////////////////////////////////////////////// + public class DirectoryCheck : PackageCheck { - private string _path; - private List _files = new List(); + private DirectoryPath _relDirPath; + private List _files = new List(); - public DirectoryCheck(string path) - { - _path = path; - } + public DirectoryCheck(DirectoryPath relDirPath) + { + _relDirPath = relDirPath; + } - public DirectoryCheck WithFiles(params string[] files) - { - _files.AddRange(files); - return this; - } + public DirectoryCheck WithFiles(params FilePath[] files) + { + _files.AddRange(files); + return this; + } - public DirectoryCheck AndFiles(params string[] files) + public DirectoryCheck AndFiles(params FilePath[] files) { return WithFiles(files); } - public DirectoryCheck WithFile(string file) - { - _files.Add(file); - return this; - } + public DirectoryCheck WithFile(FilePath file) + { + _files.Add(file); + return this; + } - public DirectoryCheck AndFile(string file) + public DirectoryCheck AndFile(FilePath file) { return AndFiles(file); } - public override bool Apply(string dir) - { - if (!System.IO.Directory.Exists(dir + _path)) - { - WriteError($"Directory {_path} was not found."); - return false; - } - - bool isOK = true; - - if (_files != null) - { - foreach (var file in _files) - { - if (!System.IO.File.Exists(System.IO.Path.Combine(dir + _path, file))) - { - WriteError($"File {file} was not found in directory {_path}."); - isOK = false; - } - } - } - - return isOK; - } -} - -private FileCheck HasFile(string file) => HasFiles(new [] { file }); -private FileCheck HasFiles(params string[] files) => new FileCheck(files); + public override bool ApplyTo(DirectoryPath testDirPath) + { + DirectoryPath absDirPath = testDirPath.Combine(_relDirPath); -private DirectoryCheck HasDirectory(string dir) => new DirectoryCheck(dir); + if (!CheckDirectoryExists(absDirPath)) + return false; -private static void WriteError(string msg) -{ - Console.WriteLine(" ERROR: " + msg); -} - -private static void WriteWarning(string msg) -{ - Console.WriteLine(" WARNING: " + msg); -} - -private static void WriteInfo(string msg) -{ - Console.WriteLine(" " + msg); + return CheckFilesExist(_files.Select(file => absDirPath.CombineWithFilePath(file))); + } } diff --git a/cake/package-definition.cake b/cake/package-definition.cake new file mode 100644 index 000000000..fa7c55b3a --- /dev/null +++ b/cake/package-definition.cake @@ -0,0 +1,263 @@ +public enum PackageType +{ + NuGet, + Chocolatey, + Msi, + Zip +} + +public abstract class PackageDefinition +{ + protected ICakeContext _context; + + /// + /// Constructor + /// + /// A PackageType value specifying one of the four known package types + /// A string containing the package ID, used as the root of the PackageName + /// A string representing the source used to create the package, e.g. a nuspec file + /// A TestRunner instance used to run package tests. + /// An array of PackageChecks be made on the content of the package. Optional. + /// An array of PackageChecks to be made on the symbol package, if one is created. Optional. Only supported for nuget packages. + /// An array of PackageTests to be run against the package. Optional. + protected PackageDefinition( + PackageType packageType, + string id, + string source, + PackageTestRunner testRunner = null, + string extraTestArguments = null, + PackageCheck[] checks = null, + PackageCheck[] symbols = null, + IEnumerable tests = null) + { + if (testRunner == null && tests != null) + throw new System.ArgumentException($"Unable to create {packageType} package {id}: TestRunner must be provided if there are tests", nameof(testRunner)); + + _context = BuildSettings.Context; + + PackageType = packageType; + PackageId = id; + PackageVersion = BuildSettings.PackageVersion; + PackageSource = source; + BasePath = BuildSettings.OutputDirectory; + TestRunner = testRunner; + ExtraTestArguments = extraTestArguments; + PackageChecks = checks; + SymbolChecks = symbols; + PackageTests = tests; + } + + public PackageType PackageType { get; } + public string PackageId { get; } + public string PackageVersion { get; protected set; } + public string PackageSource { get; } + public string BasePath { get; } + public PackageTestRunner TestRunner { get; } + public string ExtraTestArguments { get; } + public PackageCheck[] PackageChecks { get; } + public PackageCheck[] SymbolChecks { get; protected set; } + public IEnumerable PackageTests { get; } + + public bool HasSymbols { get; protected set; } = false; + public virtual string SymbolPackageName => throw new System.NotImplementedException($"Symbols are not available for {PackageType} packages."); + + // The file name of this package, including extension + public abstract string PackageFileName { get; } + // The directory into which this package is installed + public abstract string PackageInstallDirectory { get; } + // The directory used to contain results of package tests for this package + public abstract string PackageResultDirectory { get; } + // The directory into which extensions to the test runner are installed + public abstract string ExtensionInstallDirectory { get; } + + public string PackageFilePath => BuildSettings.PackageDirectory + PackageFileName; + public string PackageTestDirectory => $"{PackageInstallDirectory}{PackageId}.{PackageVersion}/"; + + public bool IsSelectedBy(string selectionExpression) + { + return IsSelectedByAny(selectionExpression.Split("|", StringSplitOptions.RemoveEmptyEntries)); + + bool IsSelectedByAny(string[] terms) + { + foreach (var term in terms) + if (IsSelectedByAll(term.Split("&", StringSplitOptions.RemoveEmptyEntries))) + return true; + + return false; + } + + bool IsSelectedByAll(string[] factors) + { + foreach (string factor in factors) + { + int index = factor.IndexOf("="); + if (index <= 0) + throw new ArgumentException("Selection expression does not contain =", "where"); + string prop = factor.Substring(0, index).Trim(); + string val = factor.Substring(index+1).Trim(); + + switch(prop.ToUpper()) + { + case "ID": + return PackageId.ToLower() == val.ToLower(); + case "TYPE": + return PackageType.ToString().ToLower() == val.ToLower(); + default: + throw new Exception($"Not a valid selection property: {prop}"); + } + } + + return false; + } + } + + public void BuildVerifyAndTest() + { + _context.EnsureDirectoryExists(BuildSettings.PackageDirectory); + + Banner.Display($"Building {PackageFileName}"); + BuildPackage(); + + Banner.Display($"Installing {PackageFileName}"); + InstallPackage(); + + if (PackageChecks != null) + { + Banner.Display($"Verifying {PackageFileName}"); + VerifyPackage(); + } + + if (SymbolChecks != null) + { + // TODO: Override this in NuGetPackage + VerifySymbolPackage(); + } + + if (PackageTests != null) + { + Banner.Display($"Testing {PackageFileName}"); + RunPackageTests(); + } + } + + protected void FetchBundledExtensions(PackageReference[] extensions) + { + foreach (var extension in extensions) + if (!extension.IsInstalled(BuildSettings.ExtensionsDirectory)) + extension.Install(BuildSettings.ExtensionsDirectory); + } + + public abstract void BuildPackage(); + + // Base implementation is used for installing both NuGet and + // Chocolatey packages. Other package types should override. + public virtual void InstallPackage() + { + var installSettings = new NuGetInstallSettings + { + Source = new [] { + // Package will be found here + BuildSettings.PackageDirectory, + // Dependencies may be in any of these + "https://www.myget.org/F/nunit/api/v3/index.json", + "https://api.nuget.org/v3/index.json" }, + Version = PackageVersion, + OutputDirectory = PackageInstallDirectory, + //ExcludeVersion = true, + Prerelease = true, + NoCache = true + }; + + _context.NuGetInstall(PackageId, installSettings); + } + + public void VerifyPackage() + { + bool allOK = true; + + if (PackageChecks != null) + foreach (var check in PackageChecks) + allOK &= check.ApplyTo(PackageTestDirectory); + + if (allOK) + Console.WriteLine("All checks passed!"); + else + throw new Exception("Verification failed!"); + } + + public void RunPackageTests() + { + _context.Information($"Package tests will run at level {BuildSettings.PackageTestLevel}"); + + var reporter = new ResultReporter(PackageFileName); + + _context.CleanDirectory(PackageResultDirectory); + + // Ensure we start out each package with no extensions installed. + // If any package test installs an extension, it remains available + // for subsequent tests of the same package only. + //foreach (DirectoryPath dirPath in _context.GetDirectories(ExtensionInstallDirectory + "*")) + //{ + // _context.DeleteDirectory(dirPath, new DeleteDirectorySettings() { Recursive = true }); + // _context.Information("Deleted directory " + dirPath.GetDirectoryName()); + //} + + //if (TestRunner.RequiresInstallation) + // TestRunner.Install(); + + foreach (var packageTest in PackageTests) + { + if (packageTest.Level > BuildSettings.PackageTestLevel) + continue; + + foreach (ExtensionSpecifier extension in packageTest.ExtensionsNeeded) + extension.InstallExtension(this); + + var testResultDir = $"{PackageResultDirectory}/{packageTest.Name}/"; + var resultFile = testResultDir + "TestResult.xml"; + + Banner.Display(packageTest.Description); + + _context.CreateDirectory(testResultDir); + string arguments = $"{packageTest.Arguments} {ExtraTestArguments} --work={testResultDir}"; + if (CommandLineOptions.TraceLevel.Value != "Off") + arguments += $" --trace:{CommandLineOptions.TraceLevel.Value}"; + + int rc = TestRunner.Run(arguments); + + try + { + var result = new ActualResult(resultFile); + var report = new PackageTestReport(packageTest, result); + reporter.AddReport(report); + + Console.WriteLine(report.Errors.Count == 0 + ? "\nSUCCESS: Test Result matches expected result!" + : "\nERROR: Test Result not as expected!"); + } + catch (Exception ex) + { + reporter.AddReport(new PackageTestReport(packageTest, ex)); + + Console.WriteLine("\nERROR: No result found!"); + } + } + + // Create report as a string + var sw = new StringWriter(); + bool hadErrors = reporter.ReportResults(sw); + string reportText = sw.ToString(); + + //Display it on the console + Console.WriteLine(reportText); + + // Save it to the result directory as well + using (var reportFile = new StreamWriter($"{PackageResultDirectory}/PackageTestSummary.txt")) + reportFile.Write(reportText); + + if (hadErrors) + throw new Exception("One or more package tests had errors!"); + } + + public virtual void VerifySymbolPackage() { } // Does nothing. Overridden for NuGet packages. +} diff --git a/cake/package-definitions.cake b/cake/package-definitions.cake deleted file mode 100644 index b38bdc7a0..000000000 --- a/cake/package-definitions.cake +++ /dev/null @@ -1,396 +0,0 @@ -////////////////////////////////////////////////////////////////////// -// INDIVIDUAL PACKAGE DEFINITIONS -////////////////////////////////////////////////////////////////////// - -PackageDefinition NUnitConsoleNuGetPackage; -PackageDefinition NUnitConsoleRunnerNuGetPackage; -PackageDefinition NUnitConsoleRunnerNet60Package; -PackageDefinition NUnitConsoleRunnerNet80Package; -PackageDefinition NUnitEnginePackage; -PackageDefinition NUnitEngineApiPackage; -PackageDefinition NUnitConsoleRunnerChocolateyPackage; -PackageDefinition NUnitConsoleMsiPackage; -PackageDefinition NUnitConsoleZipPackage; - -public void InitializePackageDefinitions(ICakeContext context) -{ - const string DOTNET_EXE_X86 = @"C:\Program Files (x86)\dotnet\dotnet.exe"; - bool dotnetX86Available = IsRunningOnWindows() && System.IO.File.Exists(DOTNET_EXE_X86); - - // Tests run for all runner packages except NETCORE runner - var StandardRunnerTests = new List - { - Net35Test, - Net35X86Test, - Net40Test, - Net40X86Test, - Net35PlusNet40Test, - NetCore31Test, - Net50Test, - Net60Test, - Net70Test, - Net80Test, - Net50PlusNet60Test, - Net40PlusNet60Test, - NUnitProjectTest - }; - - // Tests run for the NETCORE runner package - var NetCoreRunnerTests = new List - { - NetCore31Test, - Net50Test, - Net60Test, - Net70Test, - Net80Test, - }; - - // TODO: Remove the limitation to Windows - if (IsRunningOnWindows() && dotnetX86Available) - { - StandardRunnerTests.Add(Net60X86Test); - // TODO: Make these tests run on AppVeyor - if (!context.BuildSystem().IsRunningOnAppVeyor) - { - StandardRunnerTests.Add(NetCore31X86Test); - StandardRunnerTests.Add(Net50X86Test); - StandardRunnerTests.Add(Net70X86Test); - StandardRunnerTests.Add(Net80X86Test); - } - // Currently, NetCoreRunner runs tests in process. As a result, - // X86 tests will work in our environment, although uses may run - // it as a tool using the X86 architecture. - } - - - AllPackages.AddRange(new PackageDefinition[] { - - NUnitConsoleNuGetPackage = new NuGetPackage( - context: context, - id: "NUnit.Console", - version: ProductVersion, - source: NUGET_DIR + "runners/nunit.console-runner-with-extensions.nuspec", - checks: new PackageCheck[] { HasFile("LICENSE.txt") }), - - NUnitConsoleRunnerNuGetPackage = new NuGetPackage( - context: context, - id: "NUnit.ConsoleRunner", - version: ProductVersion, - source: NUGET_DIR + "runners/nunit.console-runner.nuspec", - checks: new PackageCheck[] { - HasFiles("LICENSE.txt", "NOTICES.txt"), - HasDirectory("tools").WithFiles(CONSOLE_FILES).AndFiles(ENGINE_FILES).AndFile("nunit.console.nuget.addins"), - HasDirectory("tools/agents/net20").WithFiles(AGENT_FILES).AndFile("nunit.console.nuget.agent.addins"), - HasDirectory("tools/agents/net40").WithFiles(AGENT_FILES).AndFile("nunit.console.nuget.agent.addins"), - HasDirectory("tools/agents/netcoreapp3.1").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.nuget.agent.addins"), - HasDirectory("tools/agents/net5.0").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.nuget.agent.addins"), - HasDirectory("tools/agents/net6.0").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.nuget.agent.addins"), - HasDirectory("tools/agents/net7.0").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.nuget.agent.addins"), - HasDirectory("tools/agents/net8.0").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.nuget.agent.addins") - }, - symbols: new PackageCheck[] { - HasDirectory("tools").WithFiles(ENGINE_PDB_FILES).AndFile("nunit3-console.pdb"), - HasDirectory("tools/agents/net20").WithFiles(AGENT_PDB_FILES), - HasDirectory("tools/agents/net40").WithFiles(AGENT_PDB_FILES), - HasDirectory("tools/agents/netcoreapp3.1").WithFiles(AGENT_PDB_FILES_NETCORE), - HasDirectory("tools/agents/net5.0").WithFiles(AGENT_PDB_FILES_NETCORE), - HasDirectory("tools/agents/net6.0").WithFiles(AGENT_PDB_FILES_NETCORE), - HasDirectory("tools/agents/net7.0").WithFiles(AGENT_PDB_FILES_NETCORE), - HasDirectory("tools/agents/net8.0").WithFiles(AGENT_PDB_FILES_NETCORE) - }, - executable: "tools/nunit3-console.exe", - tests: StandardRunnerTests), - - NUnitConsoleRunnerNet80Package = new NuGetPackage( - context: context, - id: "NUnit.ConsoleRunner.NetCore", - version: ProductVersion, - source: NUGET_DIR + "runners/nunit.console-runner.netcore.nuspec", - checks: new PackageCheck[] { - HasFiles("LICENSE.txt", "NOTICES.txt"), - HasDirectory("tools/net8.0").WithFiles(CONSOLE_FILES_NETCORE).AndFiles(ENGINE_CORE_FILES).AndFile("nunit.console.nuget.addins") - }, - symbols: new PackageCheck[] { - HasDirectory("tools/net8.0").WithFile("nunit3-console.pdb").AndFiles(ENGINE_PDB_FILES) - }, - executable: "tools/net8.0/nunit3-console.exe", - tests: NetCoreRunnerTests), - - NUnitConsoleRunnerNet60Package = new NuGetPackage( - context: context, - id: "NUnit.ConsoleRunner.NetCore", - version: ProductVersion, - source: NUGET_DIR + "runners/nunit.console-runner.netcore.nuspec", - checks: new PackageCheck[] { - HasFiles("LICENSE.txt", "NOTICES.txt"), - HasDirectory("tools/net6.0").WithFiles(CONSOLE_FILES_NETCORE).AndFiles(ENGINE_CORE_FILES).AndFile("nunit.console.nuget.addins") - }, - symbols: new PackageCheck[] { - HasDirectory("tools/net6.0").WithFile("nunit3-console.pdb").AndFiles(ENGINE_PDB_FILES) - }, - executable: "tools/net6.0/nunit3-console.exe", - tests: NetCoreRunnerTests), - - NUnitConsoleRunnerChocolateyPackage = new ChocolateyPackage( - context: context, - id: "nunit-console-runner", - version: ProductVersion, - source: CHOCO_DIR + "nunit-console-runner.nuspec", - checks: new PackageCheck[] { - HasDirectory("tools").WithFiles("LICENSE.txt", "NOTICES.txt", "VERIFICATION.txt").AndFiles(CONSOLE_FILES).AndFiles(ENGINE_FILES).AndFile("nunit.console.choco.addins"), - HasDirectory("tools/agents/net20").WithFiles(AGENT_FILES).AndFile("nunit.console.choco.agent.addins"), - HasDirectory("tools/agents/net40").WithFiles(AGENT_FILES).AndFile("nunit.console.choco.agent.addins"), - HasDirectory("tools/agents/netcoreapp3.1").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.choco.agent.addins"), - HasDirectory("tools/agents/net5.0").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.choco.agent.addins"), - HasDirectory("tools/agents/net6.0").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.choco.agent.addins"), - HasDirectory("tools/agents/net7.0").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.choco.agent.addins"), - HasDirectory("tools/agents/net8.0").WithFiles(AGENT_FILES_NETCORE).AndFile("nunit.console.choco.agent.addins") - }, - executable: "tools/nunit3-console.exe", - tests: StandardRunnerTests), - - NUnitConsoleMsiPackage = new MsiPackage( - context: context, - id: "NUnit.Console", - version: SemVer, - source: MSI_DIR + "nunit/nunit.wixproj", - checks: new PackageCheck[] { - HasDirectory("NUnit.org").WithFiles("LICENSE.txt", "NOTICES.txt", "nunit.ico"), - HasDirectory("NUnit.org/nunit-console").WithFiles(CONSOLE_FILES).AndFiles(ENGINE_FILES).AndFile("nunit.bundle.addins"), - HasDirectory("Nunit.org/nunit-console/addins").WithFiles("nunit.core.dll", "nunit.core.interfaces.dll", "nunit.v2.driver.dll", "nunit-project-loader.dll", "vs-project-loader.dll", "nunit-v2-result-writer.dll", "teamcity-event-listener.dll") - }, - executable: "NUnit.org/nunit-console/nunit3-console.exe", - tests: StandardRunnerTests), - - NUnitConsoleZipPackage = new ZipPackage( - context: context, - id: "NUnit.Console", - version: ProductVersion, - source: ZIP_IMG_DIR, - checks: new PackageCheck[] { - HasFiles("LICENSE.txt", "NOTICES.txt", "CHANGES.txt"), - HasDirectory("bin/net20").WithFiles(CONSOLE_FILES).AndFiles(ENGINE_FILES).AndFile("nunit3-console.pdb").AndFiles(ENGINE_PDB_FILES), - HasDirectory("bin/net35").WithFiles(CONSOLE_FILES).AndFiles(ENGINE_FILES).AndFile("nunit3-console.pdb").AndFiles(ENGINE_PDB_FILES), - HasDirectory("bin/netstandard2.0").WithFiles(ENGINE_FILES).AndFiles(ENGINE_PDB_FILES), - HasDirectory("bin/netcoreapp3.1").WithFiles(ENGINE_CORE_FILES).AndFiles(ENGINE_CORE_PDB_FILES), - //HasDirectory("bin/net5.0").WithFiles(ENGINE_FILES).AndFiles(ENGINE_PDB_FILES), - HasDirectory("bin/agents/net20").WithFiles(AGENT_FILES).AndFiles(AGENT_PDB_FILES), - HasDirectory("bin/agents/net40").WithFiles(AGENT_FILES).AndFiles(AGENT_PDB_FILES), - HasDirectory("bin/agents/net5.0").WithFiles(AGENT_FILES_NETCORE).AndFiles(AGENT_PDB_FILES_NETCORE), - HasDirectory("bin/agents/net6.0").WithFiles(AGENT_FILES_NETCORE).AndFiles(AGENT_PDB_FILES_NETCORE), - HasDirectory("bin/agents/net7.0").WithFiles(AGENT_FILES_NETCORE).AndFiles(AGENT_PDB_FILES_NETCORE), - HasDirectory("bin/agents/net8.0").WithFiles(AGENT_FILES_NETCORE).AndFiles(AGENT_PDB_FILES_NETCORE) - }, - executable: "bin/net20/nunit3-console.exe", - tests: StandardRunnerTests), - - // NOTE: Packages below this point have no direct tests - - NUnitEnginePackage = new NuGetPackage( - context: context, - id: "NUnit.Engine", - version: ProductVersion, - source: NUGET_DIR + "engine/nunit.engine.nuspec", - checks: new PackageCheck[] { - HasFiles("LICENSE.txt", "NOTICES.txt"), - HasDirectory("lib/net20").WithFiles(ENGINE_FILES), - HasDirectory("lib/netstandard2.0").WithFiles(ENGINE_FILES), - HasDirectory("contentFiles/any/lib/net20").WithFile("nunit.engine.nuget.addins"), - HasDirectory("contentFiles/any/lib/netstandard2.0").WithFile("nunit.engine.nuget.addins"), - HasDirectory("contentFiles/any/agents/net20").WithFiles(AGENT_FILES).AndFile("nunit.agent.addins"), - HasDirectory("contentFiles/any/agents/net40").WithFiles(AGENT_FILES).AndFile("nunit.agent.addins") - }, - symbols: new PackageCheck[] { - HasDirectory("lib/net20").WithFiles(ENGINE_PDB_FILES), - HasDirectory("lib/netstandard2.0").WithFiles(ENGINE_PDB_FILES), - HasDirectory("contentFiles/any/agents/net20").WithFiles(AGENT_PDB_FILES), - HasDirectory("contentFiles/any/agents/net40").WithFiles(AGENT_PDB_FILES) - }), - - NUnitEngineApiPackage = new NuGetPackage( - context: context, - id: "NUnit.Engine.Api", - version: ProductVersion, - source: NUGET_DIR + "engine/nunit.engine.api.nuspec", - checks: new PackageCheck[] { - HasFile("LICENSE.txt"), - HasDirectory("lib/net20").WithFile("nunit.engine.api.dll"), - HasDirectory("lib/netstandard2.0").WithFile("nunit.engine.api.dll"), - }, - symbols: new PackageCheck[] { - HasDirectory("lib/net20").WithFile("nunit.engine.api.pdb"), - HasDirectory("lib/netstandard2.0").WithFile("nunit.engine.api.pdb") - }) - }); -} - -////////////////////////////////////////////////////////////////////// -// LIST OF ALL PACKAGES -////////////////////////////////////////////////////////////////////// - -var AllPackages = new List(); - -////////////////////////////////////////////////////////////////////// -// PACKAGE DEFINITION IMPLEMENTATION -////////////////////////////////////////////////////////////////////// - -public enum PackageType -{ - NuGet, - Chocolatey, - Msi, - Zip -} - -/// -/// -/// -public abstract class PackageDefinition -{ - protected ICakeContext _context; - - /// - /// - /// - /// A PackageType value specifying one of the four known package types - /// A string containing the package ID, used as the root of the PackageName - /// A string representing the package version, used as part of the PackageName - /// A string representing the source used to create the package, e.g. a nuspec file - /// A string containing the path to the executable used in running tests. If relative, the path is contained within the package itself. - /// An array of PackageChecks be made on the content of the package. Optional. - /// An array of PackageChecks to be made on the symbol package, if one is created. Optional. Only supported for nuget packages. - /// An array of PackageTests to be run against the package. Optional. - protected PackageDefinition( - ICakeContext context, - PackageType packageType, - string id, - string version, - string source, - string executable = null, - PackageCheck[] checks = null, - PackageCheck[] symbols = null, - IEnumerable tests = null) - { - if (executable == null && tests != null) - throw new System.ArgumentException($"Unable to create {packageType} package {id}: Executable must be provided if there are tests", nameof(executable)); - - _context = context; - - PackageType = packageType; - PackageId = id; - PackageVersion = version; - PackageSource = source; - TestExecutable = executable; - PackageChecks = checks; - PackageTests = tests; - SymbolChecks = symbols; - } - - public PackageType PackageType { get; } - public string PackageId { get; } - public string PackageVersion { get; } - public string PackageSource { get; } - public string TestExecutable { get; } - public PackageCheck[] PackageChecks { get; } - public PackageCheck[] SymbolChecks { get; protected set; } - public IEnumerable PackageTests { get; } - - public abstract string PackageName { get; } - public abstract void BuildPackage(); - - public bool HasSymbols { get; protected set; } = false; - public virtual string SymbolPackageName => throw new System.NotImplementedException($"Symbols are not available for {PackageType} packages."); -} - -// Users may only instantiate the derived classes, which avoids -// exposing PackageType and makes it impossible to create a -// PackageDefinition with an unknown package type. -public class NuGetPackage : PackageDefinition -{ - public NuGetPackage(ICakeContext context, string id, string version, string source, string executable = null, - PackageCheck[] checks = null, PackageCheck[] symbols = null, IEnumerable tests = null) - : base(context, PackageType.NuGet, id, version, source, executable: executable, checks: checks, symbols: symbols, tests: tests) - { - if (symbols != null) - { - HasSymbols = true; - SymbolChecks = symbols; - } - } - - public override string PackageName => $"{PackageId}.{PackageVersion}.nupkg"; - public override string SymbolPackageName => System.IO.Path.ChangeExtension(PackageName, ".snupkg"); - - public override void BuildPackage() - { - var nugetPackSettings = new NuGetPackSettings() - { - Version = PackageVersion, - BasePath = BIN_DIR, - OutputDirectory = PACKAGE_DIR, - NoPackageAnalysis = true, - Symbols = HasSymbols - }; - - if (HasSymbols) - nugetPackSettings.SymbolPackageFormat = "snupkg"; - - _context.NuGetPack(PackageSource, nugetPackSettings); - } -} - -public class ChocolateyPackage : PackageDefinition -{ - public ChocolateyPackage(ICakeContext context, string id, string version, string source, string executable = null, - PackageCheck[] checks = null, IEnumerable tests = null) - : base(context, PackageType.Chocolatey, id, version, source, executable: executable, checks: checks, tests: tests) { } - - public override string PackageName => $"{PackageId}.{PackageVersion}.nupkg"; - - public override void BuildPackage() - { - _context.ChocolateyPack(PackageSource, - new ChocolateyPackSettings() - { - Version = PackageVersion, - OutputDirectory = PACKAGE_DIR, - ArgumentCustomization = args => args.Append($"BIN_DIR={BIN_DIR}") - }); - } -} - -public class MsiPackage : PackageDefinition -{ - public MsiPackage(ICakeContext context, string id, string version, string source, string executable = null, - PackageCheck[] checks = null, IEnumerable tests = null) - : base(context, PackageType.Msi, id, version, source, executable: executable, checks: checks, tests: tests) { } - - public override string PackageName => $"{PackageId}-{PackageVersion}.msi"; - - public override void BuildPackage() - { - _context.MSBuild(PackageSource, new MSBuildSettings() - .WithTarget("Rebuild") - .SetConfiguration(Configuration) - .WithProperty("Version", PackageVersion) - .WithProperty("DisplayVersion", PackageVersion) - .WithProperty("OutDir", PACKAGE_DIR) - .WithProperty("Image", MSI_IMG_DIR) - .SetMSBuildPlatform(MSBuildPlatform.x86) - .SetNodeReuse(false)); - } -} - -public class ZipPackage : PackageDefinition -{ - public ZipPackage(ICakeContext context, string id, string version, string source, string executable = null, - PackageCheck[] checks = null, IEnumerable tests = null) - : base(context, PackageType.Zip, id, version, source, executable: executable, checks: checks, tests: tests) { } - - public override string PackageName => $"{PackageId}-{PackageVersion}.zip"; - - public override void BuildPackage() - { - _context.Zip(ZIP_IMG_DIR, $"{PACKAGE_DIR}{PackageName}"); - } -} diff --git a/cake/package-reference.cake b/cake/package-reference.cake new file mode 100644 index 000000000..921ef9dac --- /dev/null +++ b/cake/package-reference.cake @@ -0,0 +1,90 @@ +// Representation of a package reference, containing everything needed to install it +public class PackageReference +{ + private ICakeContext _context; + + public string Id { get; } + public string Version { get; } + + public PackageReference(string id, string version) + { + _context = BuildSettings.Context; + + Id = id; + Version = version; + } + + public PackageReference LatestDevBuild => GetLatestDevBuild(); + public PackageReference LatestRelease => GetLatestRelease(); + + private PackageReference GetLatestDevBuild() + { + var packageList = _context.NuGetList(Id, new NuGetListSettings() + { + Prerelease = true, + Source = new [] { "https://www.myget.org/F/nunit/api/v3/index.json" } + } ); + + foreach (var package in packageList) + return new PackageReference(package.Name, package.Version); + + return this; + } + + private PackageReference GetLatestRelease() + { + var packageList = _context.NuGetList(Id, new NuGetListSettings() + { + Prerelease = true, + Source = new [] { + "https://www.nuget.org/api/v2/", + "https://community.chocolatey.org/api/v2/" } + } ); + + // TODO: There seems to be an error in NuGet or in Cake, causing the list to + // contain ALL NuGet packages, so we check the Id in this loop. + foreach (var package in packageList) + if (package.Name == Id) + return new PackageReference(Id, package.Version); + + return this; + } + + public bool IsInstalled(string installDirectory) + { + return _context.GetDirectories($"{installDirectory}{Id}.*").Count > 0; + } + + public void InstallExtension(PackageDefinition targetPackage) + { + Install(targetPackage.ExtensionInstallDirectory); + } + + public void Install(string installDirectory) + { + if (!IsInstalled(installDirectory)) + { + Banner.Display($"Installing {Id} version {Version}"); + + var packageSources = new [] + { + "https://www.myget.org/F/nunit/api/v3/index.json", + "https://api.nuget.org/v3/index.json", + "https://community.chocolatey.org/api/v2/" + }; + + Console.WriteLine("Package Sources:"); + foreach(var source in packageSources) + Console.WriteLine($" {source}"); + Console.WriteLine(); + + _context.NuGetInstall(Id, + new NuGetInstallSettings() + { + OutputDirectory = installDirectory, + Version = Version, + Source = packageSources + }); + } + } +} diff --git a/cake/package-test.cake b/cake/package-test.cake new file mode 100644 index 000000000..e80d160af --- /dev/null +++ b/cake/package-test.cake @@ -0,0 +1,34 @@ +// Representation of a single test to be run against a pre-built package. +// Each test has a Level, with the following values defined... +// 0 Do not run - used for temporarily disabling a test +// 1 Run for all CI tests - that is every time we test packages +// 2 Run only on PRs, dev builds and when publishing +// 3 Run only when publishing +public struct PackageTest +{ + public int Level; + public string Name; + public string Description; + public string Arguments; + public ExpectedResult ExpectedResult; + public ExtensionSpecifier[] ExtensionsNeeded; + + public PackageTest(int level, string name, string description, string arguments, ExpectedResult expectedResult, params ExtensionSpecifier[] extensionsNeeded) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + if (description == null) + throw new ArgumentNullException(nameof(description)); + if (arguments == null) + throw new ArgumentNullException(nameof(arguments)); + if (expectedResult == null) + throw new ArgumentNullException(nameof(expectedResult)); + + Level = level; + Name = name; + Description = description; + Arguments = arguments; + ExpectedResult = expectedResult; + ExtensionsNeeded = extensionsNeeded; + } +} diff --git a/cake/package-tester.cake b/cake/package-tester.cake deleted file mode 100644 index f3d2c3142..000000000 --- a/cake/package-tester.cake +++ /dev/null @@ -1,153 +0,0 @@ -/// -/// PackageTester knows how to run all the tests for a given package -/// -public class PackageTester -{ - private ICakeContext _context; - - private PackageType _packageType; - private string _packageName; - private string _installDirectory; - private string _resultDirectory; - private string _packageUnderTest; - private string _testExecutable; - private IEnumerable _packageTests; - - public PackageTester(ICakeContext context, PackageDefinition package) - { - _context = context; - - _packageType = package.PackageType; - _packageName = package.PackageName; - var subdir = $"{_packageType.ToString().ToLower()}/{package.PackageId}/"; - _installDirectory = PACKAGE_TEST_DIR + subdir; - _resultDirectory = PACKAGE_RESULT_DIR + subdir; - _packageUnderTest = PACKAGE_DIR + _packageName; - _testExecutable = package.TestExecutable; - _packageTests = package.PackageTests; - } - - public void RunTests() - { - DisplayBanner("Testing package " + _packageName); - - Console.WriteLine("Creating Test Directory..."); - CreatePackageInstallDirectory(); - - RunPackageTests(); - } - - public void CreatePackageInstallDirectory() - { - _context.CleanDirectory(_installDirectory); - - if (_packageType == PackageType.Msi) - { - // Msiexec does not tolerate forward slashes! - string package = _packageUnderTest.ToString().Replace("/", "\\"); - string testDir = _installDirectory.Replace("/", "\\"); - Console.WriteLine($"Installing msi to {testDir}"); - int rc = _context.StartProcess("msiexec", $"/a {package} TARGETDIR={testDir} /q"); - if (rc != 0) - Console.WriteLine($" ERROR: Installer returned {rc.ToString()}"); - else - { - var binDir = _installDirectory + "NUnit.org/nunit-console/"; - var dlls = _context.GetFiles(binDir + "*.dll"); - var pdbs = _context.GetFiles(binDir + "*.pdb"); - var filesToCopy = dlls.Concat(pdbs); - - // Administrative install is used to create a file image, from which - // users may do their own installls. For security reasons, we can't - // do a full install so we simulate the user portion of the install, - // copying certain files to their final destination. - Console.WriteLine("Copying agent files"); - _context.CopyFiles(filesToCopy, binDir + "agents/net20"); - _context.CopyFiles(filesToCopy, binDir + "agents/net40"); - } - } - else - { - Console.WriteLine($"Unzipping package to {_installDirectory}"); - _context.Unzip(_packageUnderTest, _installDirectory); - - if (_packageType == PackageType.NuGet || _packageType == PackageType.Chocolatey) - { - foreach (string packageDir in System.IO.Directory.GetDirectories(EXTENSIONS_DIR)) - { - string subdir = _packageType.ToString().ToLower(); - string packageName = System.IO.Path.GetFileName(packageDir); - string targetDir = $"{PACKAGE_TEST_DIR}{subdir}/{packageName}"; - - _context.CopyDirectory(packageDir, targetDir); - - if (_packageType == PackageType.Chocolatey) - RenamePackageForChocolatey(targetDir); - } - } - } - } - - private void RenamePackageForChocolatey(string nugetDir) - { - string chocoDir = nugetDir - .Replace("NUnit.Extension.NUnitProjectLoader", "nunit-extension-nunit-project-loader") - .Replace("NUnit.Extension.VSProjectLoader", "nunit-extension-vs-project-loader") - .Replace("NUnit.Extension.NUnitV2ResultWriter", "nunit-extension-v2-result-writer") - .Replace("NUnit.Extension.NUnitV2Driver", "nunit-extension-nunit-v2-driver") - .Replace("NUnit.Extension.TeamCityEventListener", "nunit-extension-teamcity-event-listener"); - - if (chocoDir != nugetDir) - _context.MoveDirectory(nugetDir, chocoDir); - } - - private void RunPackageTests() - { - var reporter = new ResultReporter(_packageName); - - _context.CleanDirectory(_resultDirectory); - - foreach (var packageTest in _packageTests) - { - var testResultDir = _resultDirectory + packageTest.Name + "/"; - var resultFile = testResultDir + "TestResult.xml"; - - DisplayBanner(packageTest.Description); - - Console.WriteLine($"Running {_installDirectory + _testExecutable}"); - - var outputDir = System.IO.Path.GetFullPath( - $"bin/{Configuration}/"); - int rc = _context.StartProcess( - _installDirectory + _testExecutable, - new ProcessSettings() - { - Arguments = $"{packageTest.Arguments} --work={testResultDir}", - WorkingDirectory = outputDir - }); - - try - { - var result = new ActualResult(resultFile); - var report = new TestReport(packageTest, result); - reporter.AddReport(report); - - Console.WriteLine(report.Errors.Count == 0 - ? "\nSUCCESS: Test Result matches expected result!" - : "\nERROR: Test Result not as expected!"); - } - catch (Exception ex) - { - reporter.AddReport(new TestReport(packageTest, ex)); - - Console.WriteLine("\nERROR: No result found!"); - } - } - - bool hadErrors = reporter.ReportResults(); - Console.WriteLine(); - - if (hadErrors) - throw new Exception("One or more package tests had errors!"); - } -} diff --git a/cake/package-tests.cake b/cake/package-tests.cake deleted file mode 100644 index 7b76d14fe..000000000 --- a/cake/package-tests.cake +++ /dev/null @@ -1,140 +0,0 @@ -////////////////////////////////////////////////////////////////////// -// INDIVIDUAL PACKAGE TEST DEFINITIONS -////////////////////////////////////////////////////////////////////// - -static ExpectedResult MockAssemblyExpectedResult(int nCopies = 1) => new ExpectedResult("Failed") -{ - Total = 37 * nCopies, - Passed = 23 * nCopies, - Failed = 5 * nCopies, - Warnings = 1 * nCopies, - Inconclusive = 1 * nCopies, - Skipped = 7 * nCopies -}; - -static PackageTest Net35Test = new PackageTest( - "Net35Test", - "Run mock-assembly.dll under .NET 3.5", - "net35/mock-assembly.dll", - MockAssemblyExpectedResult(1)); - -static PackageTest Net35X86Test = new PackageTest( - "Net35X86Test", - "Run mock-assembly-x86.dll under .NET 3.5", - "net35/mock-assembly-x86.dll", - MockAssemblyExpectedResult(1)); - -static PackageTest Net40Test = new PackageTest( - "Net40Test", - "Run mock-assembly.dll under .NET 4.x", - "net40/mock-assembly.dll", - MockAssemblyExpectedResult(1)); - -static PackageTest Net40X86Test = new PackageTest( - "Net40X86Test", - "Run mock-assembly-x86.dll under .NET 4.x", - "net40/mock-assembly-x86.dll", - MockAssemblyExpectedResult(1)); - -static PackageTest Net35PlusNet40Test = new PackageTest( - "Net35PlusNet40Test", - "Run both copies of mock-assembly together", - "net35/mock-assembly.dll net40/mock-assembly.dll", - MockAssemblyExpectedResult(2)); - -static PackageTest Net80Test = new PackageTest( - "Net80Test", - "Run mock-assembly.dll under .NET 8.0", - "net8.0/mock-assembly.dll", - MockAssemblyExpectedResult(1)); - -static PackageTest Net80X86Test = new PackageTest( - "Net80X86Test", - "Run mock-assembly-x86.dll under .NET 8.0", - "net8.0/mock-assembly-x86.dll", - MockAssemblyExpectedResult(1)); - -static PackageTest Net70Test = new PackageTest( - "Net70Test", - "Run mock-assembly.dll under .NET 7.0", - "net7.0/mock-assembly.dll", - MockAssemblyExpectedResult(1)); - -static PackageTest Net70X86Test = new PackageTest( - "Net70X86Test", - "Run mock-assembly-x86.dll under .NET 7.0", - "net7.0/mock-assembly-x86.dll", - MockAssemblyExpectedResult(1)); - -static PackageTest Net60Test = new PackageTest( - "Net60Test", - "Run mock-assembly.dll under .NET 6.0", - "net6.0/mock-assembly.dll", - MockAssemblyExpectedResult(1)); - -static PackageTest Net60X86Test = new PackageTest( - "Net60X86Test", - "Run mock-assembly-x86.dll under .NET 6.0", - "net6.0/mock-assembly-x86.dll", - MockAssemblyExpectedResult(1)); - -static PackageTest Net50Test = new PackageTest( - "Net50Test", - "Run mock-assembly.dll under .NET 5.0", - "net5.0/mock-assembly.dll", - MockAssemblyExpectedResult(1)); - -static PackageTest Net50X86Test = new PackageTest( - "Net50X86Test", - "Run mock-assembly-x86.dll under .NET 5.0", - "net5.0/mock-assembly-x86.dll", - MockAssemblyExpectedResult(1)); - -static PackageTest NetCore31Test = new PackageTest( - "NetCore31Test", - "Run mock-assembly.dll under .NET Core 3.1", - "netcoreapp3.1/mock-assembly.dll", - MockAssemblyExpectedResult(1)); - -static PackageTest NetCore31X86Test = new PackageTest( - "NetCore31X86Test", - "Run mock-assembly-x86.dll under .NET Core 3.1", - "netcoreapp3.1/mock-assembly-x86.dll", - MockAssemblyExpectedResult(1)); - -static PackageTest Net50PlusNet60Test = new PackageTest( - "Net50PlusNet60Test", - "Run mock-assembly under .NET 5.0 and 6.0 together", - "net5.0/mock-assembly.dll net6.0/mock-assembly.dll",//" net7.0/mock-assembly.dll net8.0/mock-assembly.dll", - MockAssemblyExpectedResult(2)); - -static PackageTest Net40PlusNet60Test = new PackageTest( - "Net40PlusNet60Test", - "Run mock-assembly under .Net Framework 4.0 and .Net 6.0 together", - "net40/mock-assembly.dll net6.0/mock-assembly.dll", - MockAssemblyExpectedResult(2)); - -static PackageTest NUnitProjectTest; -NUnitProjectTest = new PackageTest( - "NUnitProjectTest", - "Run project with both copies of mock-assembly", - $"../../NetFXTests.nunit --config={Configuration}", - MockAssemblyExpectedResult(2)); - -// Representation of a single test to be run against a pre-built package. -public struct PackageTest -{ - public string Name; - public string Description; - public string Arguments; - public ExpectedResult ExpectedResult; - - public PackageTest(string name, string description, string arguments, ExpectedResult expectedResult) - { - Name = name; - Description = description; - Arguments = arguments; - ExpectedResult = expectedResult; - } -} - diff --git a/cake/publishing.cake b/cake/publishing.cake new file mode 100644 index 000000000..2981bd3d1 --- /dev/null +++ b/cake/publishing.cake @@ -0,0 +1,244 @@ +public static class PackageReleaseManager +{ + private static ICakeContext _context; + + static PackageReleaseManager() + { + _context = BuildSettings.Context; + } + + private static bool _hadErrors = false; + + public static void Publish() + { + _hadErrors = false; + + PublishToMyGet(); + PublishToNuGet(); + PublishToChocolatey(); + + if (_hadErrors) + throw new Exception("One of the publishing steps failed."); + } + + public static void PublishToMyGet() + { + if (!BuildSettings.ShouldPublishToMyGet) + _context.Information("Nothing to publish to MyGet from this run."); + else if (CommandLineOptions.NoPush) + _context.Information("NoPush option suppressing publication to MyGet"); + else + foreach (var package in BuildSettings.Packages) + { + var packageName = $"{package.PackageId}.{BuildSettings.PackageVersion}.nupkg"; + var packagePath = BuildSettings.PackageDirectory + packageName; + try + { + if (package.PackageType == PackageType.NuGet) + PushNuGetPackage(packagePath, BuildSettings.MyGetApiKey, BuildSettings.MyGetPushUrl); + else if (package.PackageType == PackageType.Chocolatey) + PushChocolateyPackage(packagePath, BuildSettings.MyGetApiKey, BuildSettings.MyGetPushUrl); + } + catch (Exception ex) + { + _context.Error(ex.Message); + _hadErrors = true; + } + } + } + + public static void PublishToNuGet() + { + if (!BuildSettings.ShouldPublishToNuGet) + _context.Information("Nothing to publish to NuGet from this run."); + else if (CommandLineOptions.NoPush) + _context.Information("NoPush option suppressing publication to NuGet"); + else + foreach (var package in BuildSettings.Packages) + { + var packageName = $"{package.PackageId}.{BuildSettings.PackageVersion}.nupkg"; + var packagePath = BuildSettings.PackageDirectory + packageName; + try + { + if (package.PackageType == PackageType.NuGet) + PushNuGetPackage(packagePath, BuildSettings.NuGetApiKey, BuildSettings.NuGetPushUrl); + } + catch (Exception ex) + { + _context.Error(ex.Message); + _hadErrors = true; + } + } + } + + public static void PublishToChocolatey() + { + if (!BuildSettings.ShouldPublishToChocolatey) + _context.Information("Nothing to publish to Chocolatey from this run."); + else if (CommandLineOptions.NoPush) + _context.Information("NoPush option suppressing publication to Chocolatey"); + else + foreach (var package in BuildSettings.Packages) + { + var packageName = $"{package.PackageId}.{BuildSettings.PackageVersion}.nupkg"; + var packagePath = BuildSettings.PackageDirectory + packageName; + try + { + if (package.PackageType == PackageType.Chocolatey) + PushChocolateyPackage(packagePath, BuildSettings.ChocolateyApiKey, BuildSettings.ChocolateyPushUrl); + } + catch (Exception ex) + { + _context.Error(ex.Message); + _hadErrors = true; + } + } + } + + private static void PushNuGetPackage(FilePath package, string apiKey, string url) + { + CheckPackageExists(package); + _context.NuGetPush(package, new NuGetPushSettings() { ApiKey = apiKey, Source = url }); + } + + private static void PushChocolateyPackage(FilePath package, string apiKey, string url) + { + CheckPackageExists(package); + _context.ChocolateyPush(package, new ChocolateyPushSettings() { ApiKey = apiKey, Source = url }); + } + + private static void CheckPackageExists(FilePath package) + { + if (!_context.FileExists(package)) + throw new InvalidOperationException( + $"Package not found: {package.GetFilename()}.\nCode may have changed since package was last built."); + } + + private const string DRAFT_RELEASE_ERROR = + "A direct call to CreateDraftRelease is permitted only:\r\n" + + " * On a release branch (release-x.x.x)\r\n" + + " * On the main branch tagged for a production release\r\n" + + " * Using option --packageVersion to specify a release version"; + + public static void CreateDraftRelease() + { + string releaseVersion = + CommandLineOptions.PackageVersion.Exists ? CommandLineOptions.PackageVersion.Value : + BuildSettings.IsReleaseBranch ? BuildSettings.BuildVersion.BranchName.Substring(8) : + BuildSettings.IsProductionRelease ? BuildSettings.PackageVersion : null; + + if (releaseVersion != null) + { + if (CommandLineOptions.NoPush) + _context.Information($"NoPush option skipping creation of draft release for version {releaseVersion}"); + else + { + string releaseName = $"{BuildSettings.Title} {releaseVersion}"; + _context.Information($"Creating draft release for {releaseName}"); + + try + { + _context.GitReleaseManagerCreate(BuildSettings.GitHubAccessToken, BuildSettings.GitHubOwner, BuildSettings.GitHubRepository, new GitReleaseManagerCreateSettings() + { + Name = releaseName, + Milestone = releaseVersion + }); + } + catch + { + _context.Error($"Unable to create draft release for {releaseName}."); + _context.Error($"Check that there is a {releaseVersion} milestone with at least one closed issue."); + _context.Error(""); + throw; + } + } + } + else + { + bool calledDirectly = CommandLineOptions.Target.Value == "CreateDraftRelease"; + if (calledDirectly) + throw new InvalidOperationException(DRAFT_RELEASE_ERROR); + else + _context.Information("Skipping creation of draft release because this is not a release branch"); + } + } + + private const string UPDATE_RELEASE_ERROR = + "A direct call to UpdateReleaseNotes is permitted only:\r\n" + + " * On the main branch tagged for a production release\r\n" + + " * Using option --packageVersion to specify a release version"; + + public static void UpdateReleaseNotes() + { + string releaseVersion = + CommandLineOptions.PackageVersion.Exists ? CommandLineOptions.PackageVersion.Value : + BuildSettings.IsProductionRelease ? BuildSettings.PackageVersion : null; + + if (releaseVersion == null) + throw new InvalidOperationException(UPDATE_RELEASE_ERROR); + + if (CommandLineOptions.NoPush) + _context.Information($"NoPush option skipping update of release notes for version {releaseVersion}"); + else + { + string releaseName = $"{BuildSettings.Title} {releaseVersion}"; + _context.Information($"Updating release notes for {releaseName}"); + + try + { + _context.GitReleaseManagerCreate(BuildSettings.GitHubAccessToken, BuildSettings.GitHubOwner, BuildSettings.GitHubRepository, new GitReleaseManagerCreateSettings() + { + Name = releaseName, + Milestone = releaseVersion + }); + } + catch + { + _context.Error($"Unable to update release notes for {releaseName}."); + _context.Error($"Check that there is a {releaseVersion} milestone with a matching release."); + _context.Error(""); + throw; + } + } + + } + + public static void DownloadDraftRelease() + { + if (!BuildSettings.IsReleaseBranch) + throw new Exception("DownloadDraftRelease requires a release branch!"); + + string milestone = BuildSettings.BranchName.Substring(8); + + _context.GitReleaseManagerExport(BuildSettings.GitHubAccessToken, BuildSettings.GitHubOwner, BuildSettings.GitHubRepository, "DraftRelease.md", + new GitReleaseManagerExportSettings() { TagName = milestone }); + } + + public static void CreateProductionRelease() + { + if (!BuildSettings.IsProductionRelease) + { + _context.Information("Skipping CreateProductionRelease because this is not a production release"); + } + else if (CommandLineOptions.NoPush) + _context.Information($"NoPush option skipping creation of production release for version {BuildSettings.PackageVersion}"); + else + { + string token = BuildSettings.GitHubAccessToken; + string owner = BuildSettings.GitHubOwner; + string repository = BuildSettings.GitHubRepository; + string tagName = BuildSettings.PackageVersion; + string assets = string.Join(',', BuildSettings.Packages.Select(p => p.PackageFilePath)); + + //IsRunningOnWindows() + // ? $"\"{BuildSettings.NuGetPackage},{BuildSettings.ChocolateyPackage}\"" + // : $"\"{BuildSettings.NuGetPackage}\""; + + _context.Information($"Publishing release {tagName} to GitHub"); + _context.Information($" Assets: {assets}"); + + _context.GitReleaseManagerAddAssets(token, owner, repository, tagName, assets); + _context.GitReleaseManagerClose(token, owner, repository, tagName); + } + } +} diff --git a/cake/setup.cake b/cake/setup.cake new file mode 100644 index 000000000..c9eec7ab3 --- /dev/null +++ b/cake/setup.cake @@ -0,0 +1,51 @@ +Setup((context) => +{ + var target = context.TargetTask.Name; + var tasksToExecute = context.TasksToExecute.Select(t => t.Name); + + // Ensure that BuildSettings have been initialized + if (BuildSettings.Context == null) + throw new Exception("BuildSettings have not been initialized. Call BuildSettings.Initialize() from your build.cake script."); + + // Ensure Api Keys and tokens are available if needed for tasks to be executed + + // MyGet Api Key + bool needMyGetApiKey = tasksToExecute.Contains("PublishToMyGet") && BuildSettings.ShouldPublishToMyGet && !CommandLineOptions.NoPush; + if (needMyGetApiKey && string.IsNullOrEmpty(BuildSettings.MyGetApiKey)) + DisplayErrorAndThrow("MyGet ApiKey is required but was not set."); + + // NuGet Api Key + bool needNuGetApiKey = tasksToExecute.Contains("PublishToNuGet") && BuildSettings.ShouldPublishToNuGet && !CommandLineOptions.NoPush; + if (needNuGetApiKey && string.IsNullOrEmpty(BuildSettings.NuGetApiKey)) + DisplayErrorAndThrow("NuGet ApiKey is required but was not set."); + + // Chocolatey Api Key + bool needChocolateyApiKey = tasksToExecute.Contains("PublishToChocolatey") && BuildSettings.ShouldPublishToChocolatey && !CommandLineOptions.NoPush; + if (needChocolateyApiKey && string.IsNullOrEmpty(BuildSettings.ChocolateyApiKey)) + DisplayErrorAndThrow("Chocolatey ApiKey is required but was not set."); + + // GitHub Access Token, Owner and Repository + if (!CommandLineOptions.NoPush) + if (tasksToExecute.Contains("CreateDraftRelease") && BuildSettings.IsReleaseBranch || + tasksToExecute.Contains("CreateProductionRelease") && BuildSettings.IsProductionRelease) + { + if (string.IsNullOrEmpty(BuildSettings.GitHubAccessToken)) + DisplayErrorAndThrow("GitHub Access Token is required but was not set."); + if (string.IsNullOrEmpty(BuildSettings.GitHubOwner)) + DisplayErrorAndThrow("GitHub Owner is required but was not set."); + if (string.IsNullOrEmpty(BuildSettings.GitHubRepository)) + DisplayErrorAndThrow("GitHub Repository is required but was not set."); + } + + // Add settings to BuildSettings + BuildSettings.Target = target; + BuildSettings.TasksToExecute = tasksToExecute; + + void DisplayErrorAndThrow(string message) + { + message += $"\r\n Tasks: {string.Join(", ", tasksToExecute)}"; + + context.Error(message); + throw new Exception(message); + } +}); diff --git a/cake/task-builders.cake b/cake/task-builders.cake new file mode 100644 index 000000000..4c14345bf --- /dev/null +++ b/cake/task-builders.cake @@ -0,0 +1,48 @@ +// All tasks incorporated in the recipe are defined using CakeTaskBuilders. +// The actual specification of criteria, dependencies and actions for each +// task is done separately in task-definitions.cake. +// +// This approach provides a level of indirection, permitting the user to +// modify or completely redefine what a task does in their build.cake file, +// without changing the definitions in the recipe. + +public static class BuildTasks +{ + // General + public static CakeTaskBuilder DumpSettingsTask { get; set; } + public static CakeTaskBuilder DefaultTask {get; set; } + + // Building + public static CakeTaskBuilder BuildTask { get; set; } + public static CakeTaskBuilder CheckHeadersTask { get; set; } + public static CakeTaskBuilder CleanTask { get; set; } + public static CakeTaskBuilder CleanAllTask { get; set; } + public static CakeTaskBuilder RestoreTask { get; set; } + + // Unit Testing + public static CakeTaskBuilder UnitTestTask { get; set; } + + // Packaging + public static CakeTaskBuilder PackageTask { get; set; } + public static CakeTaskBuilder BuildTestAndPackageTask { get; set; } + //public static CakeTaskBuilder PackageBuildTask { get; set; } + //public static CakeTaskBuilder PackageInstallTask { get; set; } + //public static CakeTaskBuilder PackageVerifyTask { get; set; } + //public static CakeTaskBuilder PackageTestTask { get; set; } + + // Publishing + public static CakeTaskBuilder PublishTask { get; set; } + public static CakeTaskBuilder PublishToMyGetTask { get; set; } + public static CakeTaskBuilder PublishToNuGetTask { get; set; } + public static CakeTaskBuilder PublishToChocolateyTask { get; set; } + + // Releasing + public static CakeTaskBuilder CreateDraftReleaseTask { get; set; } + //public static CakeTaskBuilder DownloadDraftReleaseTask { get; set; } + //public static CakeTaskBuilder UpdateReleaseNotesTask { get; set; } + public static CakeTaskBuilder CreateProductionReleaseTask { get; set; } + + // Continuous Integration + public static CakeTaskBuilder ContinuousIntegrationTask { get; set; } + public static CakeTaskBuilder AppveyorTask { get; set; } +} diff --git a/cake/task-definitions.cake b/cake/task-definitions.cake new file mode 100644 index 000000000..69233603c --- /dev/null +++ b/cake/task-definitions.cake @@ -0,0 +1,134 @@ +// This file defines what each of the tasks in the recipe actually does. +// You should not change these definitions unless you intend to change +// the behavior of a task for all projects that use the recipe. +// +// To make a change for a single project, you should add code to your build.cake +// or another project-specific cake file. See extending.cake for examples. + +BuildTasks.DefaultTask = Task("Default") + .Description("Default task if none specified by user") + .IsDependentOn("Build"); + +BuildTasks.DumpSettingsTask = Task("DumpSettings") + .Description("Display BuildSettings properties") + .Does(() => BuildSettings.DumpSettings()); + +BuildTasks.CheckHeadersTask = Task("CheckHeaders") + .Description("Check source files for valid copyright headers") + .WithCriteria(() => !CommandLineOptions.NoBuild) + .WithCriteria(() => !BuildSettings.SuppressHeaderCheck) + .Does(() => Headers.Check()); + +BuildTasks.CleanTask = Task("Clean") + .Description("Clean output and package directories") + .WithCriteria(() => !CommandLineOptions.NoBuild) + .Does(() => + { + foreach (var binDir in GetDirectories($"**/bin/{BuildSettings.Configuration}/")) + CleanDirectory(binDir); + + CleanDirectory(BuildSettings.PackageDirectory); + CleanDirectory(BuildSettings.ImageDirectory); + CleanDirectory(BuildSettings.ExtensionsDirectory); + + DeleteFiles(BuildSettings.ProjectDirectory + "*.log"); + }); + +BuildTasks.CleanAllTask = Task("CleanAll") + .Description("Clean everything!") + .Does(() => + { + foreach (var binDir in GetDirectories("**/bin/")) + CleanDirectory(binDir); + + CleanDirectory(BuildSettings.PackageDirectory); + CleanDirectory(BuildSettings.ImageDirectory); + CleanDirectory(BuildSettings.ExtensionsDirectory); + + DeleteFiles(BuildSettings.ProjectDirectory + "*.log"); + + foreach (var dir in GetDirectories("src/**/obj/")) + DeleteDirectory(dir, new DeleteDirectorySettings() { Recursive = true }); + }); + +BuildTasks.RestoreTask = Task("Restore") + .Description("Restore referenced packages") + .WithCriteria(() => BuildSettings.SolutionFile != null) + .WithCriteria(() => !CommandLineOptions.NoBuild) + .Does(() => { + NuGetRestore(BuildSettings.SolutionFile, new NuGetRestoreSettings() { + Source = new string[] { + "https://www.nuget.org/api/v2", + "https://www.myget.org/F/nunit/api/v2" } + }); + }); + +BuildTasks.BuildTask = Task("Build") + .WithCriteria(() => BuildSettings.SolutionFile != null) + .WithCriteria(() => !CommandLineOptions.NoBuild) + .IsDependentOn("Clean") + .IsDependentOn("Restore") + .IsDependentOn("CheckHeaders") + .Description("Build the solution") + .Does(() => { + MSBuild(BuildSettings.SolutionFile, BuildSettings.MSBuildSettings.WithProperty("Version", BuildSettings.PackageVersion)); + }); + +BuildTasks.UnitTestTask = Task("Test") + .Description("Run unit tests") + .IsDependentOn("Build") + .Does(() => UnitTesting.RunAllTests()); + +BuildTasks.PackageTask = Task("Package") + .IsDependentOn("Build") + .Description("Build, Install, Verify and Test all packages") + .Does(() => { + var selector = CommandLineOptions.PackageSelector; + foreach(var package in BuildSettings.Packages) + if (!selector.Exists || package.IsSelectedBy(selector.Value)) + package.BuildVerifyAndTest(); + }); + +BuildTasks.BuildTestAndPackageTask = Task("BuildTestAndPackage") + .Description("Do Build, Test and Package all in one run") + .IsDependentOn("Build") + .IsDependentOn("Test") + .IsDependentOn("Package"); + +BuildTasks.PublishTask = Task("Publish") + .Description("Publish all packages for current branch") + .IsDependentOn("Package") + .Does(() => PackageReleaseManager.Publish()); + +BuildTasks.PublishToMyGetTask = Task("PublishToMyGet") + .Description("Publish packages to MyGet") + .Does(() => PackageReleaseManager.PublishToMyGet() ); + +BuildTasks.PublishToNuGetTask = Task("PublishToNuGet") + .Description("Publish packages to NuGet") + .Does(() => PackageReleaseManager.PublishToNuGet() ); + +BuildTasks.PublishToChocolateyTask = Task("PublishToChocolatey") + .Description("Publish packages to Chocolatey") + .Does(() => PackageReleaseManager.PublishToChocolatey() ); + +BuildTasks.CreateDraftReleaseTask = Task("CreateDraftRelease") + .Description("Create a draft release on GitHub") + .Does(() => PackageReleaseManager.CreateDraftRelease() ); + +BuildTasks.CreateProductionReleaseTask = Task("CreateProductionRelease") + .Description("Create a production GitHub Release") + .Does(() => PackageReleaseManager.CreateProductionRelease() ); + +BuildTasks.ContinuousIntegrationTask = Task("ContinuousIntegration") + .Description("Perform continuous integration run") + .IsDependentOn("Build") + .IsDependentOn("Test") + .IsDependentOn("Package") + .IsDependentOn("Publish") + .IsDependentOn("CreateDraftRelease") + .IsDependentOn("CreateProductionRelease"); + +BuildTasks.AppveyorTask = Task("Appveyor") + .Description("Target for running on AppVeyor") + .IsDependentOn("ContinuousIntegration"); diff --git a/cake/test-reports.cake b/cake/test-reports.cake new file mode 100644 index 000000000..9faf03d3d --- /dev/null +++ b/cake/test-reports.cake @@ -0,0 +1,171 @@ +public class PackageTestReport +{ + public PackageTest Test; + public ActualResult Result; + public string ConsoleVersion; + public List Errors; + public List Warnings; + + public PackageTestReport(PackageTest test, ActualResult actualResult, string consoleVersion = null) + { + Test = test; + Result = actualResult; + ConsoleVersion = consoleVersion; + Errors = new List(); + Warnings = new List(); + + var expectedResult = test.ExpectedResult; + + ReportMissingFiles(); + + if (actualResult.OverallResult == null) + Errors.Add(" The test-run element has no result attribute."); + else if (expectedResult.OverallResult != actualResult.OverallResult) + Errors.Add($" Expected: Overall Result = {expectedResult.OverallResult} But was: {actualResult.OverallResult}"); + CheckCounter("Test Count", expectedResult.Total, actualResult.Total); + CheckCounter("Passed", expectedResult.Passed, actualResult.Passed); + CheckCounter("Failed", expectedResult.Failed, actualResult.Failed); + CheckCounter("Warnings", expectedResult.Warnings, actualResult.Warnings); + CheckCounter("Inconclusive", expectedResult.Inconclusive, actualResult.Inconclusive); + CheckCounter("Skipped", expectedResult.Skipped, actualResult.Skipped); + + var expectedAssemblies = expectedResult.Assemblies; + var actualAssemblies = actualResult.Assemblies; + + for (int i = 0; i < expectedAssemblies.Length && i < actualAssemblies.Length; i++) + { + var expected = expectedAssemblies[i]; + var actual = actualAssemblies[i]; + + if (expected.AssemblyName != actual.AssemblyName) + Errors.Add($" Expected: {expected.AssemblyName} But was: { actual.AssemblyName}"); + else if (consoleVersion == null || !consoleVersion.StartsWith("NetCore.")) + { + if (actual.Runtime == null) + Warnings.Add($"Unable to determine actual runtime used for {expected.AssemblyName}"); + else if (expected.Runtime != actual.Runtime) + Errors.Add($" Assembly {actual.AssemblyName} Expected: {expected.Runtime} But was: {actual.Runtime}"); + } + } + + for (int i = actualAssemblies.Length; i < expectedAssemblies.Length; i++) + Errors.Add($" Assembly {expectedAssemblies[i].AssemblyName} was not found"); + + for (int i = expectedAssemblies.Length; i < actualAssemblies.Length; i++) + Errors.Add($" Found unexpected assembly {actualAssemblies[i].AssemblyName}"); + } + + public PackageTestReport(PackageTest test, Exception ex, string consoleVersion = null) + { + Test = test; + Result = null; + Errors = new List(); + Errors.Add($" {ex.Message}"); + ConsoleVersion = consoleVersion; + } + + public void Display(int index, TextWriter writer) + { + writer.WriteLine(); + writer.WriteLine($"{index}. {Test.Description}"); + if (ConsoleVersion != null) + writer.WriteLine($" ConsoleVersion: {ConsoleVersion}"); + writer.WriteLine($" Args: {Test.Arguments}"); + writer.WriteLine(); + + foreach (var error in Errors) + writer.WriteLine(error); + + if (Errors.Count == 0) + { + writer.WriteLine(" SUCCESS: Test Result matches expected result!"); + } + else + { + writer.WriteLine(); + writer.WriteLine(" ERROR: Test Result not as expected!"); + } + + foreach (var warning in Warnings) + writer.WriteLine(" WARNING: " + warning); + } + + // File level errors, like missing or mal-formatted files, need to be highlighted + // because otherwise it's hard to detect the cause of the problem without debugging. + // This method finds and reports that type of error. + private void ReportMissingFiles() + { + // Start with all the top-level test suites. Note that files that + // cannot be found show up as Unknown as do unsupported file types. + var suites = Result.Xml.SelectNodes( + "//test-suite[@type='Unknown'] | //test-suite[@type='Project'] | //test-suite[@type='Assembly']"); + + // If there is no top-level suite, it generally means the file format could not be interpreted + if (suites.Count == 0) + Errors.Add(" No top-level suites! Possible empty command-line or misformed project."); + + foreach (XmlNode suite in suites) + { + // Narrow down to the specific failures we want + string runState = GetAttribute(suite, "runstate"); + string suiteResult = GetAttribute(suite, "result"); + string label = GetAttribute(suite, "label"); + string site = suite.Attributes["site"]?.Value ?? "Test"; + if (runState == "NotRunnable" || suiteResult == "Failed" && site == "Test" && (label == "Invalid" || label == "Error")) + { + string message = suite.SelectSingleNode("reason/message")?.InnerText; + Errors.Add($" {message}"); + } + } + } + + private void CheckCounter(string label, int expected, int actual) + { + // If expected value of counter is negative, it means no check is needed + if (expected >= 0 && expected != actual) + Errors.Add($" Expected: {label} = {expected} But was: {actual}"); + } + + private string GetAttribute(XmlNode node, string name) + { + return node.Attributes[name]?.Value; + } +} + +public class ResultReporter +{ + private string _packageName; + private List _reports = new List(); + + public ResultReporter(string packageName) + { + _packageName = packageName; + } + + public void AddReport(PackageTestReport report) + { + _reports.Add(report); + } + + public bool ReportResults(TextWriter writer) + { + writer.WriteLine("\n=================================================="); ; + writer.WriteLine($"Test Results for {_packageName}"); + writer.WriteLine("=================================================="); ; + + writer.WriteLine("\nTest Environment"); + writer.WriteLine($" OS Version: {Environment.OSVersion.VersionString}"); + writer.WriteLine($" CLR Version: {Environment.Version}\n"); + + int index = 0; + bool hasErrors = false; + + foreach (var report in _reports) + { + hasErrors |= report.Errors.Count > 0; + report.Display(++index, writer); + } + + return hasErrors; + } +} diff --git a/cake/test-results.cake b/cake/test-results.cake index dc34081ac..41ba05397 100644 --- a/cake/test-results.cake +++ b/cake/test-results.cake @@ -5,188 +5,164 @@ using System.Xml; public abstract class ResultSummary { - public string OverallResult { get; set; } - public int Total { get; set; } - public int Passed { get; set; } - public int Failed { get; set; } - public int Warnings { get; set; } - public int Inconclusive { get; set; } - public int Skipped { get; set; } + public string OverallResult { get; set; } + public int Total { get; set; } + public int Passed { get; set; } + public int Failed { get; set; } + public int Warnings { get; set; } + public int Inconclusive { get; set; } + public int Skipped { get; set; } } public class ExpectedResult : ResultSummary { public ExpectedResult(string overallResult) { - if (string.IsNullOrEmpty(overallResult)) - throw new ArgumentNullException(nameof(overallResult)); + if (string.IsNullOrEmpty(overallResult)) + throw new ArgumentNullException(nameof(overallResult)); OverallResult = overallResult; + // Initialize counters to -1, indicating no expected value. // Set properties of those items to be checked. Total = Passed = Failed = Warnings = Inconclusive = Skipped = -1; } + + public ExpectedAssemblyResult[] Assemblies { get; set; } = new ExpectedAssemblyResult[0]; +} + +public class ExpectedAssemblyResult +{ + public ExpectedAssemblyResult(string name, string expectedRuntime = null) + { + AssemblyName = name; + Runtime = expectedRuntime; + } + + public string AssemblyName { get; } + public string Runtime { get; } } public class ActualResult : ResultSummary { - public ActualResult(string resultFile) - { - var doc = new XmlDocument(); - doc.Load(resultFile); + public ActualResult(string resultFile) + { + var doc = new XmlDocument(); + doc.Load(resultFile); - Xml = doc.DocumentElement; - if (Xml.Name != "test-run") - throw new Exception("The test-run element was not found."); + Xml = doc.DocumentElement; + if (Xml.Name != "test-run") + throw new Exception("The test-run element was not found."); - OverallResult = GetAttribute(Xml, "result"); - Total = IntAttribute(Xml, "total"); - Passed = IntAttribute(Xml, "passed"); - Failed = IntAttribute(Xml, "failed"); - Warnings = IntAttribute(Xml, "warnings"); - Inconclusive = IntAttribute(Xml, "inconclusive"); - Skipped = IntAttribute(Xml, "skipped"); - } + OverallResult = GetAttribute(Xml, "result"); + Total = IntAttribute(Xml, "total"); + Passed = IntAttribute(Xml, "passed"); + Failed = IntAttribute(Xml, "failed"); + Warnings = IntAttribute(Xml, "warnings"); + Inconclusive = IntAttribute(Xml, "inconclusive"); + Skipped = IntAttribute(Xml, "skipped"); - public XmlNode Xml { get; } + var assemblies = new List(); + //var engineVersion = new Version(GetAttribute(Xml, "engine-version")); - private string GetAttribute(XmlNode node, string name) - { - return node.Attributes[name]?.Value; - } + foreach (XmlNode node in Xml.SelectNodes("//test-suite[@type='Assembly']")) + assemblies.Add(new ActualAssemblyResult(node)); - private int IntAttribute(XmlNode node, string name) - { - string s = GetAttribute(node, name); - // TODO: We should replace 0 with -1, representing a missing counter - // attribute, after issue #904 is fixed. - return s == null ? 0 : int.Parse(s); - } + //foreach (XmlNode node in Xml.SelectNodes("//test-suite[@type='Assembly']")) + // assemblies.Add(new ActualAssemblyResult(node, engineVersion)); + + Assemblies = assemblies.ToArray(); + } + + public XmlNode Xml { get; } + + public ActualAssemblyResult[] Assemblies { get; } + + private string GetAttribute(XmlNode node, string name) + { + return node.Attributes[name]?.Value; + } + + private int IntAttribute(XmlNode node, string name) + { + string s = GetAttribute(node, name); + // TODO: We should replace 0 with -1, representing a missing counter + // attribute, after issue #707 is fixed. + return s == null ? 0 : int.Parse(s); + } } -public class TestReport +public class ActualAssemblyResult { - public PackageTest Test; - public ActualResult Result; - public List Errors; - - public TestReport(PackageTest test, ActualResult result) + public ActualAssemblyResult(XmlNode xml) { - Test = test; - Result = result; - Errors = new List(); - - var expected = test.ExpectedResult; - - ReportMissingFiles(); - - if (result.OverallResult == null) - Errors.Add(" The test-run element has no result attribute."); - else if (expected.OverallResult != result.OverallResult) - Errors.Add($" Expected: Overall Result = {expected.OverallResult}\n But was: {result.OverallResult}"); - CheckCounter("Test Count", expected.Total, result.Total); - CheckCounter("Passed", expected.Passed, result.Passed); - CheckCounter("Failed", expected.Failed, result.Failed); - CheckCounter("Warnings", expected.Warnings, result.Warnings); - CheckCounter("Inconclusive", expected.Inconclusive, result.Inconclusive); - CheckCounter("Skipped", expected.Skipped, result.Skipped); - } - - public TestReport(PackageTest test, Exception ex) - { - Test = test; - Result = null; - Errors = new List(); - Errors.Add($" {ex.Message}"); - } + AssemblyName = xml.Attributes["name"]?.Value; - public void Display(int index) - { - Console.WriteLine($"\n{index}. {Test.Description}"); - Console.WriteLine($" Args: {Test.Arguments}\n"); + //var env = xml.SelectSingleNode("environment"); + var settings = xml.SelectSingleNode("settings"); - foreach (var error in Errors) - Console.WriteLine(error); + // If TargetRuntimeFramework setting is not present, the Runner will probably crash + var runtimeSetting = settings?.SelectSingleNode("setting[@name='TargetRuntimeFramework']"); + Runtime = runtimeSetting?.Attributes["value"]?.Value; - Console.WriteLine(Errors.Count == 0 - ? " SUCCESS: Test Result matches expected result!" - : "\n ERROR: Test Result not as expected!"); - } + var agentSetting = settings?.SelectSingleNode("setting[@name='SelectedAgentName']"); + AgentName = agentSetting?.Attributes["value"]?.Value; + } - // File level errors, like missing or mal-formatted files, need to be highlighted - // because otherwise it's hard to detect the cause of the problem without debugging. - // This method finds and reports that type of error. - private void ReportMissingFiles() - { - // Start with all the top-level test suites. Note that files that - // cannot be found show up as Unknown as do unsupported file types. - var suites = Result.Xml.SelectNodes( - "//test-suite[@type='Unknown'] | //test-suite[@type='Project'] | //test-suite[@type='Assembly']"); - - // If there is no top-level suite, it generally means the file format could not be interpreted - if (suites.Count == 0) - Errors.Add(" No top-level suites! Possible empty command-line or misformed project."); - - foreach (XmlNode suite in suites) - { - // Narrow down to the specific failures we want - string runState = GetAttribute(suite, "runstate"); - string suiteResult = GetAttribute(suite, "result"); - string label = GetAttribute(suite, "label"); - string site = suite.Attributes["site"]?.Value ?? "Test"; - if (runState == "NotRunnable" || suiteResult == "Failed" && site == "Test" && (label == "Invalid" || label=="Error")) - { - string message = suite.SelectSingleNode("reason/message")?.InnerText; - Errors.Add($" {message}"); - } - } - } - - private void CheckCounter(string label, int expected, int actual) - { - // If expected value of counter is negative, it means no check is needed - if (expected >=0 && expected != actual) - Errors.Add($" Expected: {label} = {expected}\n But was: {actual}"); - } + public string AssemblyName { get; } + public string AgentName { get; } - private string GetAttribute(XmlNode node, string name) - { - return node.Attributes[name]?.Value; - } + public string Runtime { get; } } -public class ResultReporter +#if false +public class ActualAssemblyResult { - private string _packageName; - private List _reports = new List(); - - public ResultReporter(string packageName) + public ActualAssemblyResult(XmlNode xml, Version engineVersion) { - _packageName = packageName; - } + Name = xml.Attributes["name"]?.Value; - public void AddReport(TestReport report) - { - _reports.Add(report); - } + var env = xml.SelectSingleNode("environment"); + var settings = xml.SelectSingleNode("settings"); - public bool ReportResults() - { - DisplayBanner($"Test Results for {_packageName}"); + // If TargetRuntimeFramework setting is not present, the GUI will have crashed anyway + var runtimeSetting = settings.SelectSingleNode("setting[@name='ImageTargetFrameworkName']"); + TargetRuntime = runtimeSetting?.Attributes["value"]?.Value; - Console.WriteLine("\nTest Environment"); - Console.WriteLine($" OS Version: {Environment.OSVersion.VersionString}"); - Console.WriteLine($" CLR Version: {Environment.Version}\n"); + // Engine 3.10 and earlier doesn't provide enough info + if (engineVersion >= new Version(3,11,0,0)) + Runtime = DeduceActualRuntime(xml); + } - int index = 0; - bool hasErrors = false; + public string Name { get; } + public string Runtime { get; } - foreach (var report in _reports) - { - hasErrors |= report.Errors.Count > 0; - report.Display(++index); - } + public string TargetRuntime { get; } - return hasErrors; - } + // Code to determine the runtime actually used is adhoc + // and doesn't work for all assemblies because the needed + // information may not be present in the result file. + // TODO: Modify result file schema so this can be cleaner + private static string DeduceActualRuntime(XmlNode assembly) + { + var env = assembly.SelectSingleNode("environment"); + // The TargetRuntimeFramework setting is only present + // under the 3.12 and later versions of the engine. + var runtimeSetting = assembly.SelectSingleNode("settings/setting[@name='TargetRuntimeFramework']"); + var targetRuntime = runtimeSetting?.Attributes["value"]?.Value; + if (targetRuntime != null) + return targetRuntime; + + var clrVersion = env.Attributes["clr-version"]?.Value; + if (clrVersion == null) + return null; + + if (clrVersion.StartsWith("2.0")) + return "net-2.0"; + if (clrVersion.StartsWith("4.0")) + return "net-4"; + + return null; + } } +#endif diff --git a/cake/test-runners.cake b/cake/test-runners.cake new file mode 100644 index 000000000..d876d9c39 --- /dev/null +++ b/cake/test-runners.cake @@ -0,0 +1,117 @@ +/// +/// The TestRunner class is the abstract base for all TestRunners used to run unit- +/// or package-tests. A TestRunner knows how to run a test assembly and provide a result. +/// +public abstract class TestRunner +{ + public virtual bool RequiresInstallation => false; + + protected FilePath ExecutablePath { get; set; } + + // Base install does nothing + public virtual void Install() { } +} + +public abstract class UnitTestRunner : TestRunner +{ + // Unit tests are run by providing the path to a test assembly. + public virtual int Run(FilePath testAssembly) + { + if (ExecutablePath == null) + throw new InvalidOperationException("Unable to run tests. Executable path has not been set."); + + var processSettings = new ProcessSettings() { + WorkingDirectory = BuildSettings.OutputDirectory, + // HACK: Equality indicates we are running under NUnitLite + Arguments = ExecutablePath == testAssembly + ? BuildSettings.UnitTestArguments + : $"{testAssembly} {BuildSettings.UnitTestArguments}" }; + + if (ExecutablePath.GetExtension() == ".dll") + return BuildSettings.Context.StartProcess("dotnet", processSettings); + else + return BuildSettings.Context.StartProcess(ExecutablePath,processSettings); + } +} + +public abstract class PackageTestRunner : TestRunner +{ + // Package Tests are run by providing the arguments, which + // will include one or more test assemblies. + public virtual int Run(string arguments=null) + { + if (ExecutablePath == null) + throw new InvalidOperationException("Unable to run tests. Executable path has not been set."); + + var processSettings = new ProcessSettings() { WorkingDirectory = BuildSettings.OutputDirectory }; + + if (ExecutablePath.GetExtension() == ".dll") + { + processSettings.Arguments = $"{ExecutablePath} {arguments}"; + return BuildSettings.Context.StartProcess("dotnet", processSettings); + } + else + { + processSettings.Arguments = arguments; + return BuildSettings.Context.StartProcess(ExecutablePath, processSettings); + } + } +} + +/// +/// The InstallableTestRunner class is the abstract base for TestRunners which +/// must be installed using a published package before they can be used. +/// +public abstract class InstallableTestRunner : TestRunner +{ + public override bool RequiresInstallation => true; + + public InstallableTestRunner(string packageId, string version) + { + if (packageId == null) + throw new ArgumentNullException(nameof(packageId)); + if (version == null) + throw new ArgumentNullException(nameof(version)); + + PackageId = packageId; + Version = version; + } + + public string PackageId { get; } + public string Version { get; } + + public abstract string InstallPath { get; } +} + +public class NUnitLiteRunner : UnitTestRunner +{ + public override int Run(FilePath testPath) + { + ExecutablePath = testPath; + return base.Run(testPath); + } +} + +/// +/// Class that knows how to run an agent directly. +/// +public class AgentRunner : PackageTestRunner +{ + private string _stdExecutable; + private string _x86Executable; + + public AgentRunner(string stdExecutable, string x86Executable = null) + { + _stdExecutable = stdExecutable; + _x86Executable = x86Executable; + } + + public override int Run(string arguments) + { + ExecutablePath = arguments.Contains("--x86") + ? _x86Executable + : _stdExecutable; + + return base.Run(arguments.Replace("--x86", string.Empty)); + } +} diff --git a/cake/tools.cake b/cake/tools.cake new file mode 100644 index 000000000..749e35817 --- /dev/null +++ b/cake/tools.cake @@ -0,0 +1,4 @@ +// Load all tools used by the recipe +#tool NuGet.CommandLine&version=6.9.1 +#tool dotnet:?package=GitVersion.Tool&version=5.12.0 +#tool dotnet:?package=GitReleaseManager.Tool&version=0.17.0 diff --git a/cake/unit-testing.cake b/cake/unit-testing.cake new file mode 100644 index 000000000..0d0f2ef7b --- /dev/null +++ b/cake/unit-testing.cake @@ -0,0 +1,81 @@ +////////////////////////////////////////////////////////////////////// +// UNIT TEST RUNNER +////////////////////////////////////////////////////////////////////// + +public static class UnitTesting +{ + static ICakeContext _context; + + static UnitTesting() + { + _context = BuildSettings.Context; + } + + public static void RunAllTests() + { + var unitTests = FindUnitTestFiles(BuildSettings.UnitTests); + + _context.Information($"Located {unitTests.Count} unit test assemblies."); + + foreach (var testPath in unitTests) + RunTest(testPath); + } + + private static void RunTest(FilePath testPath) + { + var testFile = testPath.GetFilename(); + var containingDir = testPath.GetDirectory().GetDirectoryName(); + var msg = "Running " + testFile; + if (IsValidRuntime(containingDir)) + msg += " under " + containingDir; + + Banner.Display(msg); + + var runner = BuildSettings.UnitTestRunner ?? new NUnitLiteRunner(); + runner.Run(testPath); + + static bool IsValidRuntime(string text) + { + string[] VALID_RUNTIMES = { + "net20", "net30", "net35", "net40", "net45", "net451", "net451", + "net46", "net461", "net462", "net47", "net471", "net472", "net48", "net481", + "netcoreapp1.1", "netcoreapp2.1", "netcoreapp3.1", + "net5.0", "net6.0", "net7.0", "net8.0" + }; + + return VALID_RUNTIMES.Contains(text); + } + } + + private static List FindUnitTestFiles(string patternSet) + { + var result = new List(); + + if (!string.IsNullOrEmpty(patternSet)) + { + // User supplied a set of patterns for the unit tests + foreach (string filePattern in patternSet.Split('|')) + foreach (var testPath in _context.GetFiles(BuildSettings.OutputDirectory + filePattern)) + result.Add(testPath); + } + else + { + // Use default patterns to find unit tests - case insensitive because + // we don't know how the user may have named test assemblies. + var defaultPatterns = new [] { "**/*.tests.dll", "**/*.tests.exe" }; + var globberSettings = new GlobberSettings { IsCaseSensitive = false }; + foreach (string filePattern in defaultPatterns) + foreach (var testPath in _context.GetFiles(BuildSettings.OutputDirectory + filePattern, globberSettings)) + result.Add(testPath); + } + + result.Sort(ComparePathsByFileName); + + return result; + + static int ComparePathsByFileName(FilePath x, FilePath y) + { + return x.GetFilename().ToString().CompareTo(y.GetFilename().ToString()); + } + } +} diff --git a/cake/utilities.cake b/cake/utilities.cake index 23785a7ed..e48fda992 100644 --- a/cake/utilities.cake +++ b/cake/utilities.cake @@ -1,218 +1,3 @@ -////////////////////////////////////////////////////////////////////// -// HELPER METHODS - GENERAL -////////////////////////////////////////////////////////////////////// - -T GetArgument(string pattern, T defaultValue) -{ - foreach (string name in pattern.Split('|')) - if (HasArgument(name)) - return Argument(name); - - return defaultValue; -} - -bool CheckIfDotNetCoreInstalled() -{ - try - { - Information("Checking if .NET Core SDK is installed..."); - StartProcess("dotnet", new ProcessSettings - { - Arguments = "--info" - }); - } - catch (Exception) - { - Warning(".NET Core SDK is not installed. It can be installed from https://www.microsoft.com/net/core"); - return false; - } - return true; -} - -void DisplayUnreportedErrors() -{ - if (UnreportedErrors.Count > 0) - { - string msg = "One or more unit tests failed, breaking the build.\r\n" - + UnreportedErrors.Aggregate((x, y) => x + "\r\n" + y); - - UnreportedErrors.Clear(); - throw new Exception(msg); - } -} - -public static void DisplayBanner(string message) -{ - var bar = new string('-', Math.Max(message.Length, 40)); - Console.WriteLine(); - Console.WriteLine(bar); - Console.WriteLine(message); - Console.WriteLine(bar); -} - -////////////////////////////////////////////////////////////////////// -// HELPER METHODS - BUILD -////////////////////////////////////////////////////////////////////// - -MSBuildSettings CreateMSBuildSettings(string target) -{ - var settings = new MSBuildSettings() - .SetConfiguration(Configuration) - .SetVerbosity(Verbosity.Minimal) - .WithProperty("Version", ProductVersion) - .WithProperty("ApiFileVersion", SemVer + ".0") - .WithTarget(target) - // Workaround for https://github.com/Microsoft/msbuild/issues/3626 - .WithProperty("AddSyntheticProjectReferencesForSolutionDependencies", "false"); - - if (IsRunningOnWindows()) - { - // The fallback is in case only a preview of VS is installed. - var vsInstallation = - VSWhereLatest(new VSWhereLatestSettings { Requires = "Microsoft.Component.MSBuild" }) - ?? VSWhereLatest(new VSWhereLatestSettings { Requires = "Microsoft.Component.MSBuild", IncludePrerelease = true }); - - if (vsInstallation != null) - { - var msBuildPath = vsInstallation.CombineWithFilePath(@"MSBuild\Current\Bin\MSBuild.exe"); - - if (!FileExists(msBuildPath)) - msBuildPath = vsInstallation.CombineWithFilePath(@"MSBuild\15.0\Bin\MSBuild.exe"); - - if (FileExists(msBuildPath)) - settings.ToolPath = msBuildPath; - } - } - - return settings; -} - -DotNetMSBuildSettings CreateDotNetMSBuildSettings(string target) -{ - return new DotNetMSBuildSettings() - .SetConfiguration(Configuration) - .WithProperty("Version", ProductVersion) - .WithProperty("ApiFileVersion", SemVer + ".0") - .WithTarget(target); -} - -////////////////////////////////////////////////////////////////////// -// HELPER METHODS - TEST -////////////////////////////////////////////////////////////////////// - -FilePath GetResultXmlPath(string testAssembly, string targetRuntime) -{ - var assemblyName = System.IO.Path.GetFileNameWithoutExtension(testAssembly); - - // Required for test suites running under NUnitLite - CreateDirectory($@"test-results\{targetRuntime}"); - - return MakeAbsolute(new FilePath($@"test-results\{targetRuntime}\{assemblyName}.xml")); -} - -void RunNUnitLiteTests(string testAssembly, string targetRuntime) -{ - var workingDir = BIN_DIR + targetRuntime + "/"; - var assemblyPath = workingDir + testAssembly; - var resultPath = GetResultXmlPath(assemblyPath, targetRuntime).FullPath; - - int rc = StartProcess( - assemblyPath, - new ProcessSettings() - { - Arguments = $"--result:{resultPath}", - WorkingDirectory = workingDir - }); - - if (rc > 0) - UnreportedErrors.Add($"{testAssembly}({targetRuntime}): {rc} tests failed"); - else if (rc < 0) - UnreportedErrors.Add($"{testAssembly}({targetRuntime}) returned rc = {rc}"); -} - -void RunDotnetNUnitLiteTests(string testAssembly, string targetRuntime) -{ - var workingDir = BIN_DIR + targetRuntime + "/"; - var assemblyPath = workingDir + testAssembly; - var resultPath = GetResultXmlPath(assemblyPath, targetRuntime).FullPath; - - int rc = StartProcess( - "dotnet", - new ProcessSettings - { - Arguments = $"\"{assemblyPath}\" --result:{resultPath}", - WorkingDirectory = workingDir - }); - - if (rc > 0) - UnreportedErrors.Add($"{testAssembly}({targetRuntime}): {rc} tests failed"); - else if (rc < 0) - UnreportedErrors.Add($"{testAssembly}({targetRuntime}) returned rc = {rc}"); -} - -void RunNet20Console(string testAssembly, string targetRuntime) -{ - var workingDir = BIN_DIR + targetRuntime + "/"; - var assemblyPath = workingDir + testAssembly; - var resultPath = GetResultXmlPath(assemblyPath, targetRuntime).FullPath; - - int rc = StartProcess( - NET20_CONSOLE, - new ProcessSettings() - { - Arguments = $"\"{assemblyPath}\" --result:{resultPath}", - WorkingDirectory = workingDir - }); - - if (rc > 0) - UnreportedErrors.Add($"{testAssembly}({targetRuntime}): {rc} tests failed"); - else if (rc < 0) - UnreportedErrors.Add($"{testAssembly}({targetRuntime}) returned rc = {rc}"); -} - -void RunNetCoreConsole(string testAssembly, string targetRuntime) -{ - var workingDir = BIN_DIR + targetRuntime + "/"; - var assemblyPath = workingDir + testAssembly; - var resultPath = GetResultXmlPath(assemblyPath, targetRuntime).FullPath; - - int rc = StartProcess( - "dotnet", - new ProcessSettings - { - Arguments = $"\"{NET80_CONSOLE}\" \"{assemblyPath}\" --result:{resultPath}", - WorkingDirectory = workingDir - }); - - if (rc > 0) - UnreportedErrors.Add($"{testAssembly}({targetRuntime}): {rc} tests failed"); - else if (rc < 0) - UnreportedErrors.Add($"{testAssembly}({targetRuntime}) returned rc = {rc}"); -} - -public List GetInstalledNetCoreRuntimes() -{ - var list = new List(); - - var process = StartProcess("dotnet", - new ProcessSettings - { - Arguments = "--list-runtimes", - RedirectStandardOutput = true, - RedirectedStandardOutputHandler = - s => { - if (s == null || !s.StartsWith("Microsoft.NETCore.App")) - return s; - - var version = s.Split(' ')[1]; - - list.Add(version); - return s; - } - }); - return list; -} - ////////////////////////////////////////////////////////////////////// // HELPER METHODS - PACKAGING ////////////////////////////////////////////////////////////////////// @@ -223,34 +8,3 @@ public void CopyPackageContents(DirectoryPath packageDir, DirectoryPath outDir) CopyFiles(files.Where(f => f.GetExtension() != ".addins"), outDir); } -public void PushNuGetPackage(FilePath package, string apiKey, string url) -{ - CheckPackageExists(package); - if (NoPush) - Information($"Push {package} to {url}"); - else - NuGetPush(package, new NuGetPushSettings() { ApiKey = apiKey, Source = url }); -} - -public void PushChocolateyPackage(FilePath package, string apiKey, string url) -{ - CheckPackageExists(package); - if (NoPush) - Information($"Push {package} to {url}"); - else - ChocolateyPush(package, new ChocolateyPushSettings() { ApiKey = apiKey, Source = url }); -} - -private void CheckPackageExists(FilePath package) -{ - if (!FileExists(package)) - throw new InvalidOperationException( - $"Package not found: {package.GetFilename()}.\nCode may have changed since package was last built."); -} - -public bool IsPreRelease => !string.IsNullOrEmpty(PreReleaseLabel); - -public bool ShouldPublishToMyGet => IsPreRelease && LABELS_WE_PUBLISH_ON_MYGET.Contains(PreReleaseLabel); -public bool ShouldPublishToNuGet => !IsPreRelease || LABELS_WE_PUBLISH_ON_NUGET.Contains(PreReleaseLabel); -public bool ShouldPublishToChocolatey => !IsPreRelease || LABELS_WE_PUBLISH_ON_CHOCOLATEY.Contains(PreReleaseLabel); -public bool IsProductionRelease => !IsPreRelease || LABELS_WE_RELEASE_ON_GITHUB.Contains(PreReleaseLabel); diff --git a/cake/versioning.cake b/cake/versioning.cake index 81a4f28bd..90f9d7060 100644 --- a/cake/versioning.cake +++ b/cake/versioning.cake @@ -2,7 +2,7 @@ using System.Text.RegularExpressions; public class BuildVersion { - private ISetupContext _context; + private ICakeContext _context; private GitVersion _gitVersion; // NOTE: This is complicated because (1) the user may have specified @@ -12,32 +12,30 @@ public class BuildVersion // // We simplify things a by figuring out the full package version and // then parsing it to provide information that is used in the build. - public BuildVersion(ISetupContext context) + public BuildVersion(ICakeContext context) { - Console.WriteLine("Start BuildVersion"); - _context = context; - if (context==null) + if (context==null) throw new ArgumentNullException(nameof(context)); + + _context = context; _gitVersion = context.GitVersion(); - Console.WriteLine($"Running GitVersion: {_gitVersion.FullSemVer}"); + BranchName = _gitVersion.BranchName; IsReleaseBranch = BranchName.StartsWith("release-"); - string productVersion = context.HasArgument("productVersion") - ? context.Argument("productVersion") - : CalculateProductVersion(); + string packageVersion = CommandLineOptions.PackageVersion.Value ?? CalculatePackageVersion(); - int dash = productVersion.IndexOf('-'); + int dash = packageVersion.IndexOf('-'); IsPreRelease = dash > 0; - string versionPart = productVersion; + string versionPart = packageVersion; string suffix = ""; string label = ""; if (IsPreRelease) { - versionPart = productVersion.Substring(0, dash); - suffix = productVersion.Substring(dash + 1); + versionPart = packageVersion.Substring(0, dash); + suffix = packageVersion.Substring(dash + 1); foreach (char c in suffix) { if (!char.IsLetter(c)) @@ -51,16 +49,16 @@ public class BuildVersion PreReleaseLabel = label; PreReleaseSuffix = suffix; - ProductVersion = productVersion; + PackageVersion = packageVersion; AssemblyVersion = SemVer + ".0"; AssemblyFileVersion = SemVer; - AssemblyInformationalVersion = productVersion; + AssemblyInformationalVersion = packageVersion; } public string BranchName { get; } public bool IsReleaseBranch { get; } - public string ProductVersion { get; } + public string PackageVersion { get; } public string AssemblyVersion { get; } public string AssemblyFileVersion { get; } public string AssemblyInformationalVersion { get; } @@ -70,7 +68,7 @@ public class BuildVersion public string PreReleaseLabel { get; } public string PreReleaseSuffix { get; } - private string CalculateProductVersion() + private string CalculatePackageVersion() { string label = _gitVersion.PreReleaseLabel; @@ -80,10 +78,6 @@ public class BuildVersion string branchName = _gitVersion.BranchName; - // Treat version3 branch as an alternate "main" - if (branchName == "version3X" || branchName == "version315" || branchName == "version317") - label = "dev"; - // We don't currently use this pattern, but check in case we do later. if (branchName.StartsWith("feature/")) branchName = branchName.Substring(8); @@ -93,7 +87,7 @@ public class BuildVersion label = "ci"; string suffix = "-" + label + _gitVersion.CommitsSinceVersionSourcePadded; - Console.WriteLine($"CalculateProductVersion: {label} {branchName} {suffix}"); + switch (label) { case "ci": diff --git a/cake/zip-package.cake b/cake/zip-package.cake new file mode 100644 index 000000000..8d7b1ff5a --- /dev/null +++ b/cake/zip-package.cake @@ -0,0 +1,78 @@ +public class ZipPackage : PackageDefinition +{ + public ZipPackage( + string id, + string source, + PackageTestRunner testRunner = null, + PackageCheck[] checks = null, + IEnumerable tests = null, + PackageReference[] bundledExtensions = null ) + : base( + PackageType.Zip, + id, + source, + testRunner: testRunner, + checks: checks, + tests: tests) + { + BundledExtensions = bundledExtensions; + } + + // MSI and ZIP packages support bundling of extensions + // if any are specified in the definition. + public PackageReference[] BundledExtensions { get; } + + // The file name of this package, including extension + public override string PackageFileName => $"{PackageId}-{PackageVersion}.zip"; + // The directory into which this package is installed + public override string PackageInstallDirectory => BuildSettings.ZipTestDirectory; + // The directory used to contain results of package tests for this package + public override string PackageResultDirectory => $"{BuildSettings.ZipResultDirectory}{PackageId}/"; + // The directory into which extensions to the test runner are installed + public override string ExtensionInstallDirectory => $"{BuildSettings.ZipTestDirectory}{PackageId}.{PackageVersion}/bin/addins/"; + + public override void BuildPackage() + { + FetchBundledExtensions(BundledExtensions); + + CreateZipImage(); + + _context.Zip(BuildSettings.ZipImageDirectory, $"{BuildSettings.PackageDirectory}{PackageFileName}"); + } + + public override void InstallPackage() + { + _context.Unzip($"{BuildSettings.PackageDirectory}{PackageFileName}", $"{PackageInstallDirectory}{PackageId}.{PackageVersion}"); + } + + private void CreateZipImage() + { + _context.CleanDirectory(BuildSettings.ZipImageDirectory); + + _context.CopyFiles( + new FilePath[] { "LICENSE.txt", "NOTICES.txt", "CHANGES.txt", "nunit.ico" }, + BuildSettings.ZipImageDirectory); + + _context.CopyDirectory( + BuildSettings.OutputDirectory, + BuildSettings.ZipImageDirectory + "bin/" ); + + foreach (var runtime in new[] { "net20", "net35" }) + { + var runtimeDir = BuildSettings.ZipImageDirectory + $"bin/{runtime}/"; + + _context.CopyFileToDirectory( + BuildSettings.ZipDirectory + "nunit.bundle.addins", + BuildSettings.ZipImageDirectory); + + var addinsDir = runtimeDir + "addins/"; + _context.CleanDirectory(addinsDir); + + foreach (var packageDir in System.IO.Directory.GetDirectories(BuildSettings.ExtensionsDirectory)) + { + var files = _context.GetFiles(packageDir + "/tools/*").Concat(_context.GetFiles(packageDir + "/tools/net20/*")); + _context.CopyFiles(files.Where(f => f.GetExtension() != ".addins"), addinsDir); + } + } + } +} diff --git a/choco/nunit.console.choco.addins b/choco/nunit.console.choco.addins index 7eaefda90..d07fb7a21 100644 --- a/choco/nunit.console.choco.addins +++ b/choco/nunit.console.choco.addins @@ -1,5 +1,7 @@ # Extensions built for a single runtime target -../../nunit-extension-*/tools/ # find extensions installed under chocolatey +../../nunit-extension-*/**/tools/ # nuget v2 layout +../../../nunit-extension-*/**/tools/ # nuget v3 layout # Extensions built for multiple targets -../../nunit-extension-*/tools/*/ # find extensions installed under chocolatey +../../nunit-extension-*/**/tools/*/ # nuget v2 layout +../../../nunit-extension-*/**/tools/*/ # nuget v3 layout diff --git a/msi/nunit/engine-files.wxi b/msi/nunit/engine-files.wxi index 2b914f015..cf682b320 100644 --- a/msi/nunit/engine-files.wxi +++ b/msi/nunit/engine-files.wxi @@ -69,6 +69,23 @@ + + + + + + + + + + @@ -88,6 +105,23 @@ + + + + + + + + + + diff --git a/nuget/runners/nunit.console-runner.netcore.nuspec b/nuget/runners/nunit.console-runner.netcore.nuspec index d2147e507..5d44bdb9e 100644 --- a/nuget/runners/nunit.console-runner.netcore.nuspec +++ b/nuget/runners/nunit.console-runner.netcore.nuspec @@ -22,9 +22,6 @@ en-US nunit test testing tdd runner Copyright (c) 2021 Charlie Poole, Rob Prouse - - - diff --git a/src/NUnitConsole/nunit3-console/ConsoleRunner.cs b/src/NUnitConsole/nunit3-console/ConsoleRunner.cs index 6e11ec932..efbac43af 100644 --- a/src/NUnitConsole/nunit3-console/ConsoleRunner.cs +++ b/src/NUnitConsole/nunit3-console/ConsoleRunner.cs @@ -132,7 +132,6 @@ private int ExploreTests(TestPackage package, TestFilter filter) private int RunTests(TestPackage package, TestFilter filter) { - var writer = new ColorConsoleWriter(!_options.NoColor); foreach (var spec in _options.ResultOutputSpecifications) diff --git a/src/NUnitConsole/nunit3-console/builder.cake b/src/NUnitConsole/nunit3-console/builder.cake new file mode 100644 index 000000000..ea91e367d --- /dev/null +++ b/src/NUnitConsole/nunit3-console/builder.cake @@ -0,0 +1,22 @@ +////////////////////////////////////////////////////////////////////// +// EXECUTION +////////////////////////////////////////////////////////////////////// + +public Builder Build => CommandLineOptions.Usage + ? new Builder(() => Information(HelpMessages.Usage)) + : new Builder(() => RunTarget(CommandLineOptions.Target.Value)); + +public class Builder +{ + private Action _action; + + public Builder(Action action) + { + _action = action; + } + + public void Run() + { + _action(); + } +}