diff --git a/.idea/.idea.ModularPipelines/.idea/.name b/.idea/.idea.ModularPipelines/.idea/.name new file mode 100644 index 0000000000..05dc058d7a --- /dev/null +++ b/.idea/.idea.ModularPipelines/.idea/.name @@ -0,0 +1 @@ +ModularPipelines \ No newline at end of file diff --git a/.idea/.idea.ModularPipelines/.idea/aws.xml b/.idea/.idea.ModularPipelines/.idea/aws.xml new file mode 100644 index 0000000000..9b821f8cfd --- /dev/null +++ b/.idea/.idea.ModularPipelines/.idea/aws.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.Pipeline.NET/.idea/aws.xml b/.idea/.idea.Pipeline.NET/.idea/aws.xml new file mode 100644 index 0000000000..9b821f8cfd --- /dev/null +++ b/.idea/.idea.Pipeline.NET/.idea/aws.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.Pipeline.NET/.idea/indexLayout.xml b/.idea/.idea.Pipeline.NET/.idea/indexLayout.xml new file mode 100644 index 0000000000..7b08163ceb --- /dev/null +++ b/.idea/.idea.Pipeline.NET/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.Pipeline.NET/.idea/projectSettingsUpdater.xml b/.idea/.idea.Pipeline.NET/.idea/projectSettingsUpdater.xml new file mode 100644 index 0000000000..4bb9f4d2a0 --- /dev/null +++ b/.idea/.idea.Pipeline.NET/.idea/projectSettingsUpdater.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/.idea.Pipeline.NET/.idea/workspace.xml b/.idea/.idea.Pipeline.NET/.idea/workspace.xml new file mode 100644 index 0000000000..8920cea82e --- /dev/null +++ b/.idea/.idea.Pipeline.NET/.idea/workspace.xml @@ -0,0 +1,143 @@ + + + + Pipeline.NET.Build/Pipeline.NET.Build.csproj + Pipeline.NET.Examples/Pipeline.NET.Examples.csproj + + + + + + + + + + + + + + + + + + + + + + + + + { + "keyToString": { + "RunOnceActivity.OpenProjectViewOnStart": "true", + "RunOnceActivity.ShowReadmeOnStart": "true", + "WebServerToolWindowFactoryState": "false", + "git-widget-placeholder": "feature/initial-work", + "node.js.detected.package.eslint": "true", + "node.js.detected.package.tslint": "true", + "node.js.selected.package.eslint": "(autodetect)", + "node.js.selected.package.tslint": "(autodetect)", + "nodejs_package_manager_path": "npm", + "settings.editor.selected.configurable": "editor.preferences.jsOptions", + "vue.rearranger.settings.migration": "true" + }, + "keyToStringList": { + "rider.external.source.directories": [ + "C:\\Users\\tom.longhurst\\AppData\\Roaming\\JetBrains\\Rider2023.1\\resharper-host\\DecompilerCache", + "C:\\Users\\tom.longhurst\\AppData\\Roaming\\JetBrains\\Rider2023.1\\resharper-host\\SourcesCache", + "C:\\Users\\tom.longhurst\\AppData\\Local\\Symbols\\src" + ] + } +} + + + + + + + + + + + 1684830433835 + + + 1685277157807 + + + + + + + + + + + + file://$PROJECT_DIR$/Pipeline.NET/Host/PipelineHostBuilder.cs + 100 + + + + + + \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000000..21eee9b5e3 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,7 @@ + + + enable + enable + preview + + \ No newline at end of file diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/CodeFixResources.Designer.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/CodeFixResources.Designer.cs new file mode 100644 index 0000000000..837c320c8e --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/CodeFixResources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ModularPipelines.Analyzers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class CodeFixResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal CodeFixResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ModularPipelines.Analyzers.CodeFixResources", typeof(CodeFixResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Add DependsOn Attribute. + /// + internal static string CodeFixTitle { + get { + return ResourceManager.GetString("CodeFixTitle", resourceCulture); + } + } + } +} diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/CodeFixResources.resx b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/CodeFixResources.resx new file mode 100644 index 0000000000..418073e247 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/CodeFixResources.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Add DependsOn Attribute + The title of the code fix. + + \ No newline at end of file diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/Extensions/SyntaxNodeExtensions.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/Extensions/SyntaxNodeExtensions.cs new file mode 100644 index 0000000000..59a53dce4a --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/Extensions/SyntaxNodeExtensions.cs @@ -0,0 +1,23 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace ModularPipelines.Analyzers.Extensions; + +internal static class SyntaxNodeExtensions +{ + public static SyntaxNode AddUsings(this SyntaxNode documentRoot) + { + var compilationUnitSyntax = (CompilationUnitSyntax) documentRoot; + + if(compilationUnitSyntax.Usings.Any(x => x.Name.ToFullString() == "ModularPipelines.Attributes")) + { + return documentRoot; + } + + compilationUnitSyntax = compilationUnitSyntax.AddUsings( + SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("ModularPipelines.Attributes"))); + + return compilationUnitSyntax; + } +} \ No newline at end of file diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/MissingDependsOnAttributeCodeFixProvider.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/MissingDependsOnAttributeCodeFixProvider.cs new file mode 100644 index 0000000000..c02a04e958 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/MissingDependsOnAttributeCodeFixProvider.cs @@ -0,0 +1,86 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Immutable; +using System.Composition; +using ModularPipelines.Analyzers.Extensions; + +namespace ModularPipelines.Analyzers; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MissingDependsOnAttributeCodeFixProvider)), Shared] +public class MissingDependsOnAttributeCodeFixProvider : CodeFixProvider +{ + public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(MissingDependsOnAttributeAnalyzer.DiagnosticId); + + public sealed override FixAllProvider GetFixAllProvider() + { + // See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/FixAllProvider.md for more information on Fix All Providers + return WellKnownFixAllProviders.BatchFixer; + } + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; + + // Find the type declaration identified by the diagnostic. + var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType().First(); + + // Register a code action that will invoke the fix. + context.RegisterCodeFix( + CodeAction.Create( + title: CodeFixResources.CodeFixTitle, + createChangedDocument: c => AddAttribute(context, declaration, c), + equivalenceKey: nameof(CodeFixResources.CodeFixTitle)), + diagnostic); + } + + private async Task AddAttribute(CodeFixContext context, TypeDeclarationSyntax typeDecl, CancellationToken cancellationToken) + { + var document = context.Document; + + var syntaxTree = await context.Document.GetSyntaxTreeAsync(cancellationToken); + + var documentRoot = (await document.GetSyntaxRootAsync(cancellationToken))!; + + var name = context.Diagnostics.First().Properties["Name"]!; + + var attributes = typeDecl.AttributeLists.Add( + SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(CreateDependsOnAttribute(name, syntaxTree))) + .WithTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed) + .NormalizeWhitespace()); + + + return document.WithSyntaxRoot( + documentRoot + .ReplaceNode(typeDecl, typeDecl.WithAttributeLists(attributes).WithTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed)) + .AddUsings() + .NormalizeWhitespace() + ); + } + + private static AttributeSyntax CreateDependsOnAttribute(string name, SyntaxTree syntaxTree) + { + var cSharpParseOptions = (CSharpParseOptions) syntaxTree.Options; + + if (cSharpParseOptions.LanguageVersion.MapSpecifiedToEffectiveVersion() >= (LanguageVersion) 1100) + { + return SyntaxFactory.Attribute(SyntaxFactory.ParseName($"DependsOn<{name}>")); + } + + return SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("DependsOn")) + .WithArgumentList( + SyntaxFactory.AttributeArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.AttributeArgument( + SyntaxFactory.TypeOfExpression(SyntaxFactory.ParseTypeName(name)) + ) + ) + ) + ); + } +} \ No newline at end of file diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/ModularPipelines.Analyzers.CodeFixes.csproj b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/ModularPipelines.Analyzers.CodeFixes.csproj new file mode 100644 index 0000000000..b56698dc81 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/ModularPipelines.Analyzers.CodeFixes.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0 + false + ModularPipelines.Analyzers + + + + + + + + + + + + + + + + diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Package/ModularPipelines.Analyzers.Package.csproj b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Package/ModularPipelines.Analyzers.Package.csproj new file mode 100644 index 0000000000..a353236117 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Package/ModularPipelines.Analyzers.Package.csproj @@ -0,0 +1,45 @@ + + + + netstandard2.0 + false + true + true + + + + ModularPipelines.Analyzers + 1.0.0-alpha06 + Tom Longhurst + http://LICENSE_URL_HERE_OR_DELETE_THIS_LINE + http://PROJECT_URL_HERE_OR_DELETE_THIS_LINE + http://ICON_URL_HERE_OR_DELETE_THIS_LINE + http://REPOSITORY_URL_HERE_OR_DELETE_THIS_LINE + false + ModularPipelines.Analyzers + Summary of changes made in this release of the package. + Copyright + ModularPipelines.Analyzers, analyzers + true + true + + $(TargetsForTfmSpecificContentInPackage);_AddAnalyzersToOutput + + + + + + + + + + + + + + + + + + + diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Package/tools/install.ps1 b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Package/tools/install.ps1 new file mode 100644 index 0000000000..f9bd7896a9 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Package/tools/install.ps1 @@ -0,0 +1,272 @@ +param($installPath, $toolsPath, $package, $project) + +if($project.Object.SupportsPackageDependencyResolution) +{ + if($project.Object.SupportsPackageDependencyResolution()) + { + # Do not install analyzers via install.ps1, instead let the project system handle it. + return + } +} + +$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve + +foreach($analyzersPath in $analyzersPaths) +{ + if (Test-Path $analyzersPath) + { + # Install the language agnostic analyzers. + foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) + { + if($project.Object.AnalyzerReferences) + { + $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) + } + } + } +} + +# $project.Type gives the language name like (C# or VB.NET) +$languageFolder = "" +if($project.Type -eq "C#") +{ + $languageFolder = "cs" +} +if($project.Type -eq "VB.NET") +{ + $languageFolder = "vb" +} +if($languageFolder -eq "") +{ + return +} + +foreach($analyzersPath in $analyzersPaths) +{ + # Install language specific analyzers. + $languageAnalyzersPath = join-path $analyzersPath $languageFolder + if (Test-Path $languageAnalyzersPath) + { + foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) + { + if($project.Object.AnalyzerReferences) + { + $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) + } + } + } +} +# SIG # Begin signature block +# MIInugYJKoZIhvcNAQcCoIInqzCCJ6cCAQExDzANBglghkgBZQMEAgEFADB5Bgor +# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG +# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCA/i+qRUHsWzI0s +# FVk99zLgt/HOEQ33uvkFsWtHTHZgf6CCDYEwggX/MIID56ADAgECAhMzAAACUosz +# qviV8znbAAAAAAJSMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD +# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy +# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p +# bmcgUENBIDIwMTEwHhcNMjEwOTAyMTgzMjU5WhcNMjIwOTAxMTgzMjU5WjB0MQsw +# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u +# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy +# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +# AQDQ5M+Ps/X7BNuv5B/0I6uoDwj0NJOo1KrVQqO7ggRXccklyTrWL4xMShjIou2I +# sbYnF67wXzVAq5Om4oe+LfzSDOzjcb6ms00gBo0OQaqwQ1BijyJ7NvDf80I1fW9O +# L76Kt0Wpc2zrGhzcHdb7upPrvxvSNNUvxK3sgw7YTt31410vpEp8yfBEl/hd8ZzA +# v47DCgJ5j1zm295s1RVZHNp6MoiQFVOECm4AwK2l28i+YER1JO4IplTH44uvzX9o +# RnJHaMvWzZEpozPy4jNO2DDqbcNs4zh7AWMhE1PWFVA+CHI/En5nASvCvLmuR/t8 +# q4bc8XR8QIZJQSp+2U6m2ldNAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE +# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUNZJaEUGL2Guwt7ZOAu4efEYXedEw +# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1 +# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDY3NTk3MB8GA1UdIwQYMBaAFEhu +# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu +# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w +# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3 +# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx +# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAFkk3 +# uSxkTEBh1NtAl7BivIEsAWdgX1qZ+EdZMYbQKasY6IhSLXRMxF1B3OKdR9K/kccp +# kvNcGl8D7YyYS4mhCUMBR+VLrg3f8PUj38A9V5aiY2/Jok7WZFOAmjPRNNGnyeg7 +# l0lTiThFqE+2aOs6+heegqAdelGgNJKRHLWRuhGKuLIw5lkgx9Ky+QvZrn/Ddi8u +# TIgWKp+MGG8xY6PBvvjgt9jQShlnPrZ3UY8Bvwy6rynhXBaV0V0TTL0gEx7eh/K1 +# o8Miaru6s/7FyqOLeUS4vTHh9TgBL5DtxCYurXbSBVtL1Fj44+Od/6cmC9mmvrti +# yG709Y3Rd3YdJj2f3GJq7Y7KdWq0QYhatKhBeg4fxjhg0yut2g6aM1mxjNPrE48z +# 6HWCNGu9gMK5ZudldRw4a45Z06Aoktof0CqOyTErvq0YjoE4Xpa0+87T/PVUXNqf +# 7Y+qSU7+9LtLQuMYR4w3cSPjuNusvLf9gBnch5RqM7kaDtYWDgLyB42EfsxeMqwK +# WwA+TVi0HrWRqfSx2olbE56hJcEkMjOSKz3sRuupFCX3UroyYf52L+2iVTrda8XW +# esPG62Mnn3T8AuLfzeJFuAbfOSERx7IFZO92UPoXE1uEjL5skl1yTZB3MubgOA4F +# 8KoRNhviFAEST+nG8c8uIsbZeb08SeYQMqjVEmkwggd6MIIFYqADAgECAgphDpDS +# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK +# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0 +# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0 +# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla +# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS +# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT +# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB +# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG +# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S +# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz +# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7 +# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u +# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33 +# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl +# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP +# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB +# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF +# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM +# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ +# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud +# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO +# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0 +# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y +# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p +# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y +# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB +# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw +# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA +# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY +# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj +# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd +# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ +# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf +# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ +# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j +# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B +# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96 +# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7 +# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I +# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIZjzCCGYsCAQEwgZUwfjELMAkG +# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx +# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z +# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAlKLM6r4lfM52wAAAAACUjAN +# BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor +# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgRjg7DcI6 +# uhYfXWwAQ6hK0mPW7iyr2tzHR0DHSDJkscIwQgYKKwYBBAGCNwIBDDE0MDKgFIAS +# AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN +# BgkqhkiG9w0BAQEFAASCAQB3ERGpqvGnJrsyU0d9lERK2TJW4/OONhZAFjxrEvEk +# PzdH0Fk0otvagAvjHzJ3q0G8C7gwRbXIyGgiYYIMefheNvgd/UKnubUGEzeG9h0/ +# biX5Ro1mxuHBYvc3vqvWD292jXMg00iRmexDsTny8YgSAAWsTdkE8/W2ooEfbG1T +# QkCg6ds9btpA1D1znVYpEbviCJoAfHLbNBr5nzAadgWjQM8nnb3UTvmLDIs5b1LO +# 3lm9w485IBFRnfrj6QinVsCbSD7PU/N1hPY7rKfM9ScZC6QT6kjyuVVa1Ft+VYLH +# qlV9hE6B4CGeB8qkko4x+MKovgbdpCgYz3eePWCakZywoYIXGTCCFxUGCisGAQQB +# gjcDAwExghcFMIIXAQYJKoZIhvcNAQcCoIIW8jCCFu4CAQMxDzANBglghkgBZQME +# AgEFADCCAVkGCyqGSIb3DQEJEAEEoIIBSASCAUQwggFAAgEBBgorBgEEAYRZCgMB +# MDEwDQYJYIZIAWUDBAIBBQAEIC58WTh4Q8r6c2kVXmD8xoHEhya2jc6YZ43KUAIy +# flB4AgZh/WKJ50gYEzIwMjIwMjExMTkwMzQwLjE1M1owBIACAfSggdikgdUwgdIx +# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt +# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1p +# Y3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhh +# bGVzIFRTUyBFU046M0JENC00QjgwLTY5QzMxJTAjBgNVBAMTHE1pY3Jvc29mdCBU +# aW1lLVN0YW1wIFNlcnZpY2WgghFoMIIHFDCCBPygAwIBAgITMwAAAYm0v4YwhBxL +# jwABAAABiTANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMK +# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0 +# IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0Eg +# MjAxMDAeFw0yMTEwMjgxOTI3NDFaFw0yMzAxMjYxOTI3NDFaMIHSMQswCQYDVQQG +# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG +# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQg +# SXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1Mg +# RVNOOjNCRDQtNEI4MC02OUMzMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFt +# cCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvQZXxZFm +# a6plmuOyvNpV8xONOwcYolZG/BjyZWGSk5JOGaLyrKId5VxVHWHlsmJE4Svnzsdp +# sKmVx8otONveIUFvSceEZp8VXmu5m1fu8L7c+3lwXcibjccqtEvtQslokQVx0r+L +# 54abrNDarwFG73IaRidIS1i9c+unJ8oYyhDRLrCysFAVxyQhPNZkWK7Z8/VGukaK +# LAWHXCh/+R53h42gFL+9/mAALxzCXXuofi8f/XKCm7xNwVc1hONCCz6oq94AufzV +# NkkIW4brUQgYpCcJm9U0XNmQvtropYDn9UtY8YQ0NKenXPtdgLHdQ8Nnv3igErKL +# rWI0a5n5jjdKfwk+8mvakqdZmlOseeOS1XspQNJAK1uZllAITcnQZOcO5ofjOQ33 +# ujWckAXdz+/x3o7l4AU/TSOMzGZMwhUdtVwC3dSbItpSVFgnjM2COEJ9zgCadvOi +# rGDLN471jZI2jClkjsJTdgPk343TQA4JFvds/unZq0uLr+niZ3X44OBx2x+gVlln +# 2c4UbZXNueA4yS1TJGbbJFIILAmTUA9Auj5eISGTbNiyWx79HnCOTar39QEKozm4 +# LnTmDXy0/KI/H/nYZGKuTHfckP28wQS06rD+fDS5xLwcRMCW92DkHXmtbhGyRilB +# OL5LxZelQfxt54wl4WUC0AdAEolPekODwO8CAwEAAaOCATYwggEyMB0GA1UdDgQW +# BBSXbx+zR1p4IIAeguA6rHKkrfl7UDAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJl +# pxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j +# b20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAx +# MCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3 +# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3Rh +# bXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoG +# CCsGAQUFBwMIMA0GCSqGSIb3DQEBCwUAA4ICAQCOtLdpWUI4KwfLLrfaKrLB92Dq +# bAspGWM41TaO4Jl+sHxPo522uu3GKQCjmkRWreHtlfyy9kOk7LWax3k3ke8Gtfet +# fbh7qH0LeV2XOWg39BOnHf6mTcZq7FYSZZch1JDQjc98+Odlow+oWih0Dbt4CV/e +# 19ZcE+1n1zzWkskUEd0f5jPIUis33p+vkY8szduAtCcIcPFUhI8Hb5alPUAPMjGz +# wKb7NIKbnf8j8cP18As5IveckF0oh1cw63RY/vPK62LDYdpi7WnG2ObvngfWVKtw +# iwTI4jHj2cO9q37HDe/PPl216gSpUZh0ap24mKmMDfcKp1N4mEdsxz4oseOrPYeF +# sHHWJFJ6Aivvqn70KTeJpp5r+DxSqbeSy0mxIUOq/lAaUxgNSQVUX26t8r+fciko +# fKv23WHrtRV3t7rVTsB9YzrRaiikmz68K5HWdt9MqULxPQPo+ppZ0LRqkOae466+ +# UKRY0JxWtdrMc5vHlHZfnqjawj/RsM2S6Q6fa9T9CnY1Nz7DYBG3yZJyCPFsrgU0 +# 5s9ljqfsSptpFdUh9R4ce+L71SWDLM2x/1MFLLHAMbXsEp8KloEGtaDULnxtfS2t +# YhfuKGqRXoEfDPAMnIdTvQPh3GHQ4SjkkBARHL0MY75alhGTKHWjC2aLVOo8obKI +# Bk8hfnFDUf/EyVw4uTCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUw +# DQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5n +# dG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9y +# YXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhv +# cml0eSAyMDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkG +# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx +# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9z +# b2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +# ggIKAoICAQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg +# 4r25PhdgM/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aO +# RmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41 +# JmTamDu6GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5 +# LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL +# 64NF50ZuyjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9 +# QZpGdc3EXzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj +# 0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqE +# UUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0 +# kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435 +# UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB +# 3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTE +# mr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwG +# A1UdIARVMFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93 +# d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNV +# HSUEDDAKBggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNV +# HQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo +# 0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29m +# dC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5j +# cmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jv +# c29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDAN +# BgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4 +# sQaTlz0xM7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th54 +# 2DYunKmCVgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRX +# ud2f8449xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBew +# VIVCs/wMnosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0 +# DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+Cljd +# QDzHVG2dY3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFr +# DZ+kKNxnGSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFh +# bHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7n +# tdAoGokLjzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+ +# oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6Fw +# ZvKhggLXMIICQAIBATCCAQChgdikgdUwgdIxCzAJBgNVBAYTAlVTMRMwEQYDVQQI +# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv +# ZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh +# dGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046M0JENC00Qjgw +# LTY5QzMxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2WiIwoB +# ATAHBgUrDgMCGgMVACGlCa3ketyeuey7bJNpWkMuiCcQoIGDMIGApH4wfDELMAkG +# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx +# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9z +# b2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJKoZIhvcNAQEFBQACBQDlsRtBMCIY +# DzIwMjIwMjEyMDEyODMzWhgPMjAyMjAyMTMwMTI4MzNaMHcwPQYKKwYBBAGEWQoE +# ATEvMC0wCgIFAOWxG0ECAQAwCgIBAAICDbMCAf8wBwIBAAICEW8wCgIFAOWybMEC +# AQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgCAQACAwehIKEK +# MAgCAQACAwGGoDANBgkqhkiG9w0BAQUFAAOBgQCImCpEJ2AlAWBBkDABmkqIh1kM +# LPDyea3b7evhOk+YSwXCzxnBIXuppujFT3tnk7w0p0a5YS9uwqbDM/M6rAUMBAR0 +# boHamumEITNF5nVh0rlYyRZQ3WraVD2YPhouUINQavmS8ueYoh6r3HeM9QPBAnNB +# vv7GDrZ637+2Dfe60jGCBA0wggQJAgEBMIGTMHwxCzAJBgNVBAYTAlVTMRMwEQYD +# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy +# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w +# IFBDQSAyMDEwAhMzAAABibS/hjCEHEuPAAEAAAGJMA0GCWCGSAFlAwQCAQUAoIIB +# SjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkEMSIEIL86 +# iebNndOm+CAgIp67s6y+HI1wHdhaMPILGf48RtXXMIH6BgsqhkiG9w0BCRACLzGB +# 6jCB5zCB5DCBvQQgZndHMdxQV1VsbpWHOTHqWEycvcRJm7cY69l/UmT8j0UwgZgw +# gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE +# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD +# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAYm0v4YwhBxL +# jwABAAABiTAiBCDET+l3keOFFxaIqOZWSSuWNO774Ng/t5pe3p4QXoKcvjANBgkq +# hkiG9w0BAQsFAASCAgADYrNFej7RbihwGcC0jF+cTik+HJog++dPEDXeIyBB+2pw +# 23hC5KaX9H05ZknluIq2oxf2MLpKL+gA+76T3k5PnzPNJFDogUn5eFIIsMRpNF0h +# MtPWoPJWYFK2odvKz1HwsuqHRg6hO//NwORcv4xPeAWEFO5+DOXzZKKp/BVDGe/D +# c++y9/l41qpz/F2c3a1lugdqnZz7ZeoaQ8/JMlwrmMbciqcAytCn9A59EWJ1xYd/ +# DaDhQ5Rd8hkcckuxJksjWf6URmc91cb4Jdatkyupq3dDGwCkjGNd2xetrOpqMLOZ +# quoDONSgc9rGrhkf3xgKKVRhLg9bxd3f2oQ0IsOBg2AC5td1eqp6TILc0gei2E3I +# uEAW1d+KXDnajvQmvQkaFHr5wEocTTLgrDglOPPhEaEumSTJS7jKFzUKHiBU005p +# CgQ1So2WJ2RqFx0ppez1N1AFczOVLFllK3WGPLkDsN1GgT0nFfoqvs1WKkzyb2d2 +# /v6PVER9xGky7LCu62dhsJCAFUbxF2dJxaC5ofrl98VaO/z72J9on9BTz+eCtcJ9 +# rDIpqktGeL02f6+4zctFCyi2wgm6eh8kKvRlAPmN4/MNt9pWHtEV//xFGzGeDajr +# diRhDoMZwsuon4QwS8b2YcKMoZ6gZ2lZah3960fTTmvBTBNqeBtR94KWCy0C0A== +# SIG # End signature block diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Package/tools/uninstall.ps1 b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Package/tools/uninstall.ps1 new file mode 100644 index 0000000000..17fd920015 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Package/tools/uninstall.ps1 @@ -0,0 +1,279 @@ +param($installPath, $toolsPath, $package, $project) + +if($project.Object.SupportsPackageDependencyResolution) +{ + if($project.Object.SupportsPackageDependencyResolution()) + { + # Do not uninstall analyzers via uninstall.ps1, instead let the project system handle it. + return + } +} + +$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve + +foreach($analyzersPath in $analyzersPaths) +{ + # Uninstall the language agnostic analyzers. + if (Test-Path $analyzersPath) + { + foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) + { + if($project.Object.AnalyzerReferences) + { + $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) + } + } + } +} + +# $project.Type gives the language name like (C# or VB.NET) +$languageFolder = "" +if($project.Type -eq "C#") +{ + $languageFolder = "cs" +} +if($project.Type -eq "VB.NET") +{ + $languageFolder = "vb" +} +if($languageFolder -eq "") +{ + return +} + +foreach($analyzersPath in $analyzersPaths) +{ + # Uninstall language specific analyzers. + $languageAnalyzersPath = join-path $analyzersPath $languageFolder + if (Test-Path $languageAnalyzersPath) + { + foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) + { + if($project.Object.AnalyzerReferences) + { + try + { + $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) + } + catch + { + + } + } + } + } +} +# SIG # Begin signature block +# MIInugYJKoZIhvcNAQcCoIInqzCCJ6cCAQExDzANBglghkgBZQMEAgEFADB5Bgor +# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG +# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDC68wb97fg0QGL +# yXrxJhYfmibzcOh8caqC0uZprfczDaCCDYEwggX/MIID56ADAgECAhMzAAACUosz +# qviV8znbAAAAAAJSMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD +# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy +# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p +# bmcgUENBIDIwMTEwHhcNMjEwOTAyMTgzMjU5WhcNMjIwOTAxMTgzMjU5WjB0MQsw +# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u +# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy +# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +# AQDQ5M+Ps/X7BNuv5B/0I6uoDwj0NJOo1KrVQqO7ggRXccklyTrWL4xMShjIou2I +# sbYnF67wXzVAq5Om4oe+LfzSDOzjcb6ms00gBo0OQaqwQ1BijyJ7NvDf80I1fW9O +# L76Kt0Wpc2zrGhzcHdb7upPrvxvSNNUvxK3sgw7YTt31410vpEp8yfBEl/hd8ZzA +# v47DCgJ5j1zm295s1RVZHNp6MoiQFVOECm4AwK2l28i+YER1JO4IplTH44uvzX9o +# RnJHaMvWzZEpozPy4jNO2DDqbcNs4zh7AWMhE1PWFVA+CHI/En5nASvCvLmuR/t8 +# q4bc8XR8QIZJQSp+2U6m2ldNAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE +# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUNZJaEUGL2Guwt7ZOAu4efEYXedEw +# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1 +# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDY3NTk3MB8GA1UdIwQYMBaAFEhu +# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu +# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w +# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3 +# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx +# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAFkk3 +# uSxkTEBh1NtAl7BivIEsAWdgX1qZ+EdZMYbQKasY6IhSLXRMxF1B3OKdR9K/kccp +# kvNcGl8D7YyYS4mhCUMBR+VLrg3f8PUj38A9V5aiY2/Jok7WZFOAmjPRNNGnyeg7 +# l0lTiThFqE+2aOs6+heegqAdelGgNJKRHLWRuhGKuLIw5lkgx9Ky+QvZrn/Ddi8u +# TIgWKp+MGG8xY6PBvvjgt9jQShlnPrZ3UY8Bvwy6rynhXBaV0V0TTL0gEx7eh/K1 +# o8Miaru6s/7FyqOLeUS4vTHh9TgBL5DtxCYurXbSBVtL1Fj44+Od/6cmC9mmvrti +# yG709Y3Rd3YdJj2f3GJq7Y7KdWq0QYhatKhBeg4fxjhg0yut2g6aM1mxjNPrE48z +# 6HWCNGu9gMK5ZudldRw4a45Z06Aoktof0CqOyTErvq0YjoE4Xpa0+87T/PVUXNqf +# 7Y+qSU7+9LtLQuMYR4w3cSPjuNusvLf9gBnch5RqM7kaDtYWDgLyB42EfsxeMqwK +# WwA+TVi0HrWRqfSx2olbE56hJcEkMjOSKz3sRuupFCX3UroyYf52L+2iVTrda8XW +# esPG62Mnn3T8AuLfzeJFuAbfOSERx7IFZO92UPoXE1uEjL5skl1yTZB3MubgOA4F +# 8KoRNhviFAEST+nG8c8uIsbZeb08SeYQMqjVEmkwggd6MIIFYqADAgECAgphDpDS +# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK +# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0 +# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0 +# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla +# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS +# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT +# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB +# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG +# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S +# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz +# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7 +# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u +# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33 +# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl +# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP +# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB +# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF +# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM +# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ +# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud +# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO +# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0 +# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y +# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p +# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y +# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB +# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw +# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA +# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY +# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj +# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd +# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ +# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf +# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ +# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j +# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B +# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96 +# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7 +# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I +# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIZjzCCGYsCAQEwgZUwfjELMAkG +# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx +# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z +# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAlKLM6r4lfM52wAAAAACUjAN +# BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor +# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgF1ypFyzl +# AvvWGVCeXczrfpXmJNm9vpyjcwd4y4ivfqowQgYKKwYBBAGCNwIBDDE0MDKgFIAS +# AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN +# BgkqhkiG9w0BAQEFAASCAQAvi2rSDkhC82RJ4uqq/0WbHkOkzq1hrF6HxneBTNj8 +# KX+niFtee3CYVfWaSAQ6xvOiLupRX3fsSfhabRQ+Jl8k28voGrTK1OC906OO3tUN +# jdmv1PooWdxJNt2EbzQrap5Ui9KTUv4mJ4c836HAVMBPCJiq5NwmzAHfbgBxCaYq +# +hupIf+gk8vuNB1bltILgNmU/smJt9OuGqSo5TrFajzb+35SqjnCz9JtAtbPNZvA +# X9N37UPhITOecceAQmrHiEPbA7eu6VDp6VPjPfCEO7a+frWa83chEd4qzyou9xu5 +# 3gnj7Ro8nFDnGyUe0+0oCaYGXO9fbIMN1HG2IZg5suj5oYIXGTCCFxUGCisGAQQB +# gjcDAwExghcFMIIXAQYJKoZIhvcNAQcCoIIW8jCCFu4CAQMxDzANBglghkgBZQME +# AgEFADCCAVkGCyqGSIb3DQEJEAEEoIIBSASCAUQwggFAAgEBBgorBgEEAYRZCgMB +# MDEwDQYJYIZIAWUDBAIBBQAEIH+XBTHuyyHZnIXrFWIe64WLvHx5GUFMCM6A56T1 +# KwBtAgZh/WKJ52UYEzIwMjIwMjExMTkwMzQwLjU0OFowBIACAfSggdikgdUwgdIx +# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt +# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1p +# Y3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhh +# bGVzIFRTUyBFU046M0JENC00QjgwLTY5QzMxJTAjBgNVBAMTHE1pY3Jvc29mdCBU +# aW1lLVN0YW1wIFNlcnZpY2WgghFoMIIHFDCCBPygAwIBAgITMwAAAYm0v4YwhBxL +# jwABAAABiTANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMK +# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0 +# IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0Eg +# MjAxMDAeFw0yMTEwMjgxOTI3NDFaFw0yMzAxMjYxOTI3NDFaMIHSMQswCQYDVQQG +# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG +# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQg +# SXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1Mg +# RVNOOjNCRDQtNEI4MC02OUMzMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFt +# cCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvQZXxZFm +# a6plmuOyvNpV8xONOwcYolZG/BjyZWGSk5JOGaLyrKId5VxVHWHlsmJE4Svnzsdp +# sKmVx8otONveIUFvSceEZp8VXmu5m1fu8L7c+3lwXcibjccqtEvtQslokQVx0r+L +# 54abrNDarwFG73IaRidIS1i9c+unJ8oYyhDRLrCysFAVxyQhPNZkWK7Z8/VGukaK +# LAWHXCh/+R53h42gFL+9/mAALxzCXXuofi8f/XKCm7xNwVc1hONCCz6oq94AufzV +# NkkIW4brUQgYpCcJm9U0XNmQvtropYDn9UtY8YQ0NKenXPtdgLHdQ8Nnv3igErKL +# rWI0a5n5jjdKfwk+8mvakqdZmlOseeOS1XspQNJAK1uZllAITcnQZOcO5ofjOQ33 +# ujWckAXdz+/x3o7l4AU/TSOMzGZMwhUdtVwC3dSbItpSVFgnjM2COEJ9zgCadvOi +# rGDLN471jZI2jClkjsJTdgPk343TQA4JFvds/unZq0uLr+niZ3X44OBx2x+gVlln +# 2c4UbZXNueA4yS1TJGbbJFIILAmTUA9Auj5eISGTbNiyWx79HnCOTar39QEKozm4 +# LnTmDXy0/KI/H/nYZGKuTHfckP28wQS06rD+fDS5xLwcRMCW92DkHXmtbhGyRilB +# OL5LxZelQfxt54wl4WUC0AdAEolPekODwO8CAwEAAaOCATYwggEyMB0GA1UdDgQW +# BBSXbx+zR1p4IIAeguA6rHKkrfl7UDAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJl +# pxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j +# b20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAx +# MCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3 +# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3Rh +# bXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoG +# CCsGAQUFBwMIMA0GCSqGSIb3DQEBCwUAA4ICAQCOtLdpWUI4KwfLLrfaKrLB92Dq +# bAspGWM41TaO4Jl+sHxPo522uu3GKQCjmkRWreHtlfyy9kOk7LWax3k3ke8Gtfet +# fbh7qH0LeV2XOWg39BOnHf6mTcZq7FYSZZch1JDQjc98+Odlow+oWih0Dbt4CV/e +# 19ZcE+1n1zzWkskUEd0f5jPIUis33p+vkY8szduAtCcIcPFUhI8Hb5alPUAPMjGz +# wKb7NIKbnf8j8cP18As5IveckF0oh1cw63RY/vPK62LDYdpi7WnG2ObvngfWVKtw +# iwTI4jHj2cO9q37HDe/PPl216gSpUZh0ap24mKmMDfcKp1N4mEdsxz4oseOrPYeF +# sHHWJFJ6Aivvqn70KTeJpp5r+DxSqbeSy0mxIUOq/lAaUxgNSQVUX26t8r+fciko +# fKv23WHrtRV3t7rVTsB9YzrRaiikmz68K5HWdt9MqULxPQPo+ppZ0LRqkOae466+ +# UKRY0JxWtdrMc5vHlHZfnqjawj/RsM2S6Q6fa9T9CnY1Nz7DYBG3yZJyCPFsrgU0 +# 5s9ljqfsSptpFdUh9R4ce+L71SWDLM2x/1MFLLHAMbXsEp8KloEGtaDULnxtfS2t +# YhfuKGqRXoEfDPAMnIdTvQPh3GHQ4SjkkBARHL0MY75alhGTKHWjC2aLVOo8obKI +# Bk8hfnFDUf/EyVw4uTCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUw +# DQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5n +# dG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9y +# YXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhv +# cml0eSAyMDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkG +# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx +# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9z +# b2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +# ggIKAoICAQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg +# 4r25PhdgM/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aO +# RmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41 +# JmTamDu6GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5 +# LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL +# 64NF50ZuyjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9 +# QZpGdc3EXzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj +# 0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqE +# UUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0 +# kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435 +# UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB +# 3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTE +# mr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwG +# A1UdIARVMFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93 +# d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNV +# HSUEDDAKBggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNV +# HQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo +# 0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29m +# dC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5j +# cmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jv +# c29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDAN +# BgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4 +# sQaTlz0xM7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th54 +# 2DYunKmCVgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRX +# ud2f8449xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBew +# VIVCs/wMnosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0 +# DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+Cljd +# QDzHVG2dY3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFr +# DZ+kKNxnGSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFh +# bHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7n +# tdAoGokLjzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+ +# oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6Fw +# ZvKhggLXMIICQAIBATCCAQChgdikgdUwgdIxCzAJBgNVBAYTAlVTMRMwEQYDVQQI +# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv +# ZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh +# dGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046M0JENC00Qjgw +# LTY5QzMxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2WiIwoB +# ATAHBgUrDgMCGgMVACGlCa3ketyeuey7bJNpWkMuiCcQoIGDMIGApH4wfDELMAkG +# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx +# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9z +# b2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJKoZIhvcNAQEFBQACBQDlsRtBMCIY +# DzIwMjIwMjEyMDEyODMzWhgPMjAyMjAyMTMwMTI4MzNaMHcwPQYKKwYBBAGEWQoE +# ATEvMC0wCgIFAOWxG0ECAQAwCgIBAAICDbMCAf8wBwIBAAICEW8wCgIFAOWybMEC +# AQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgCAQACAwehIKEK +# MAgCAQACAwGGoDANBgkqhkiG9w0BAQUFAAOBgQCImCpEJ2AlAWBBkDABmkqIh1kM +# LPDyea3b7evhOk+YSwXCzxnBIXuppujFT3tnk7w0p0a5YS9uwqbDM/M6rAUMBAR0 +# boHamumEITNF5nVh0rlYyRZQ3WraVD2YPhouUINQavmS8ueYoh6r3HeM9QPBAnNB +# vv7GDrZ637+2Dfe60jGCBA0wggQJAgEBMIGTMHwxCzAJBgNVBAYTAlVTMRMwEQYD +# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy +# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w +# IFBDQSAyMDEwAhMzAAABibS/hjCEHEuPAAEAAAGJMA0GCWCGSAFlAwQCAQUAoIIB +# SjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkEMSIEIKY2 +# Onyhltfi0+oc/UMKaXc0H6Ckw2gGK1/qmjRZNiXnMIH6BgsqhkiG9w0BCRACLzGB +# 6jCB5zCB5DCBvQQgZndHMdxQV1VsbpWHOTHqWEycvcRJm7cY69l/UmT8j0UwgZgw +# gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE +# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD +# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAYm0v4YwhBxL +# jwABAAABiTAiBCDET+l3keOFFxaIqOZWSSuWNO774Ng/t5pe3p4QXoKcvjANBgkq +# hkiG9w0BAQsFAASCAgB7AQ0Dv3muHoNAt+cccMfYk23lHgh8LGBitCSFwu0q7ufv +# sXkoaIpwW0U0GikWhQoCH0U38SuzVbafg49FiE6ftkjOtiE03PwPYi1S6NSoDdaV +# kUuvjz3OcuN1IHg3CyLn2dO8xbUlWCUfgoWhI1nax9ch7wT4Sw8RdmGKdYTZoZmq +# vPXFRtDyZdmJDMDbTql/Brye8oEsDMoYKMmEYhY1t9TlusnWfUbxuBnyMqg/FkBy +# QF78WFfT8mygMqUGmINxPGT6daxqmq3nfAC2vOtLT4DplNYMEymfDceJzBhb8VCT +# UHc2CWK0qKT+eqwn30NBkwh//8aNHlXaA9Yq/9k2y+axIGdxFfG+X0stipRRpEXb +# xCFm7FPD5/S4ddBH829yEZLZ4XTwSZ6YS/d3mFzu5rgZl3UhjOJPXx40GQtUiDP4 +# XQZ/wW3154X/KtTypv62/Hl+CiMUrsO7MXtgwClfbJ3osg+zlpJgdraetVgmAUc1 +# mjz2GCYX7rIliGkAJREKn4rV2MZzuGLEpTjz9dB+1Xp9Ndi9q3jQgs6k3IDIUube +# YjPFFuPmFWRyi6oPTXmc4ExtTIewPvrOhwQ5q4ysxylkXoTS+UQt94BY2SvR+TMu +# 6doU+0Y73xsO8Zz+lREh3fjBsDbPAgOV5989X6bmkJEEIwIK8LYgqvyED8XXTg== +# SIG # End signature block diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/ModularPipelines.Analyzers.Test.csproj b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/ModularPipelines.Analyzers.Test.csproj new file mode 100644 index 0000000000..2b8d00c19c --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/ModularPipelines.Analyzers.Test.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + + true + true + + + + + + + + + + + + + + + + + + + diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/ModularPipelinesAnalyzersUnitTest.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/ModularPipelinesAnalyzersUnitTest.cs new file mode 100644 index 0000000000..b5b5264879 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/ModularPipelinesAnalyzersUnitTest.cs @@ -0,0 +1,112 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using VerifyCS = ModularPipelines.Analyzers.Test.Verifiers.CSharpCodeFixVerifier< + ModularPipelines.Analyzers.MissingDependsOnAttributeAnalyzer, + ModularPipelines.Analyzers.MissingDependsOnAttributeCodeFixProvider>; + +namespace ModularPipelines.Analyzers.Test; + +[TestClass] +public class ModularPipelinesAnalyzersUnitTest +{ + private const string BadModuleSource = @" +#nullable enable +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.Examples.Modules; + +public class Module1 : Module +{ + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Delay(1, cancellationToken); + return null; + } +} + +public class Module2 : Module +{ + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + var module1 = await {|#0:GetModule()|}; + return null; + } +}"; + + private const string FixedModuleSource = @"#nullable enable +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; +using ModularPipelines.Attributes; + +namespace ModularPipelines.Examples.Modules; +public class Module1 : Module +{ + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Delay(1, cancellationToken); + return null; + } +} + +[DependsOn] +public class Module2 : Module +{ + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + var module1 = await GetModule(); + return null; + } +}"; + + //No diagnostics expected to show up + [TestMethod] + public async Task Empty_Source() + { + var test = @""; + + await VerifyCS.VerifyAnalyzerAsync(test); + } + + //No diagnostics expected to show up + [TestMethod] + public async Task Good_Source() + { + var test = @""; + + await VerifyCS.VerifyAnalyzerAsync(FixedModuleSource); + } + + //Diagnostic and CodeFix both triggered and checked for + [TestMethod] + public async Task AnalyzerIsTriggered() + { + var expected = VerifyCS.Diagnostic(MissingDependsOnAttributeAnalyzer.DiagnosticId).WithArguments("Module1").WithLocation(0); + + await VerifyCS.VerifyAnalyzerAsync(BadModuleSource, expected); + } + + [TestMethod] + public async Task CodeFixWorks() + { + if (Environment.OSVersion.Platform != PlatformID.Win32NT) + { + // This fails on Linux only due to different line endings + // Is there a way around that? + return; + } + + var expected = VerifyCS.Diagnostic(MissingDependsOnAttributeAnalyzer.DiagnosticId).WithArguments("Module1").WithLocation(0); + + await VerifyCS.VerifyCodeFixAsync(BadModuleSource, expected, FixedModuleSource); + } +} \ No newline at end of file diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Net.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Net.cs new file mode 100644 index 0000000000..9272494ed3 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Net.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis.Testing; + +namespace ModularPipelines.Analyzers.Test; + +public class Net +{ + private static readonly Lazy _lazyNet60 = new(() => + new ReferenceAssemblies( + "net6.0", + new PackageIdentity( + "Microsoft.NETCore.App.Ref", + "6.0.19"), + Path.Combine("ref", "net6.0"))); + + public static ReferenceAssemblies Net60 => _lazyNet60.Value; +} \ No newline at end of file diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier.cs new file mode 100644 index 0000000000..79574c4dad --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier.cs @@ -0,0 +1,32 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing.Verifiers; + +namespace ModularPipelines.Analyzers.Test.Verifiers; + +public static partial class CSharpAnalyzerVerifier + where TAnalyzer : DiagnosticAnalyzer, new() +{ + public class Test : CSharpAnalyzerTest + { + public Test() + { + SolutionTransforms.Add((solution, projectId) => + { + var project = solution.GetProject(projectId); + + var compilationOptions = project.CompilationOptions; + + var parseOptions = project.ParseOptions as CSharpParseOptions; + + compilationOptions = compilationOptions.WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings)); + + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions) + .WithProjectParseOptions(projectId, parseOptions.WithLanguageVersion(LanguageVersion.Preview)); + + return solution; + }); + } + } +} \ No newline at end of file diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier`1.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier`1.cs new file mode 100644 index 0000000000..cfa0092145 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier`1.cs @@ -0,0 +1,35 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; + +namespace ModularPipelines.Analyzers.Test.Verifiers; + +public static partial class CSharpAnalyzerVerifier + where TAnalyzer : DiagnosticAnalyzer, new() +{ + /// + public static DiagnosticResult Diagnostic() + => CSharpAnalyzerVerifier.Diagnostic(); + + /// + public static DiagnosticResult Diagnostic(string diagnosticId) + => CSharpAnalyzerVerifier.Diagnostic(diagnosticId); + + /// + public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) + => CSharpAnalyzerVerifier.Diagnostic(descriptor); + + /// + public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + { + var test = new Test + { + TestCode = source, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } +} \ No newline at end of file diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeFixVerifier.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeFixVerifier.cs new file mode 100644 index 0000000000..1f7b45652e --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeFixVerifier.cs @@ -0,0 +1,34 @@ +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing.Verifiers; + +namespace ModularPipelines.Analyzers.Test.Verifiers; + +public static partial class CSharpCodeFixVerifier + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFix : CodeFixProvider, new() +{ + public class Test : CSharpCodeFixTest + { + public Test() + { + SolutionTransforms.Add((solution, projectId) => + { + var project = solution.GetProject(projectId); + + var compilationOptions = project.CompilationOptions; + + var parseOptions = project.ParseOptions as CSharpParseOptions; + + compilationOptions = compilationOptions.WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings)); + + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions) + .WithProjectParseOptions(projectId, parseOptions.WithLanguageVersion(LanguageVersion.Preview)); + + return solution; + }); + } + } +} \ No newline at end of file diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeFixVerifier`2.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeFixVerifier`2.cs new file mode 100644 index 0000000000..6a643f5c46 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeFixVerifier`2.cs @@ -0,0 +1,70 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using ModularPipelines.Context; + +namespace ModularPipelines.Analyzers.Test.Verifiers; + +public static partial class CSharpCodeFixVerifier + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFix : CodeFixProvider, new() +{ + /// + public static DiagnosticResult Diagnostic() + => CSharpCodeFixVerifier.Diagnostic(); + + /// + public static DiagnosticResult Diagnostic(string diagnosticId) + => CSharpCodeFixVerifier.Diagnostic(diagnosticId); + + /// + public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) + => CSharpCodeFixVerifier.Diagnostic(descriptor); + + /// + public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + { + var test = new Test + { + TestCode = source, + ReferenceAssemblies = Net.Net60, + CodeActionValidationMode = CodeActionValidationMode.SemanticStructure, + TestState = + { + AdditionalReferences = { typeof(IModuleContext).Assembly.Location }, + }, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + + /// + public static async Task VerifyCodeFixAsync(string source, string fixedSource) + => await VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource); + + /// + public static async Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource) + => await VerifyCodeFixAsync(source, new[] { expected }, fixedSource); + + /// + public static async Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource) + { + var test = new Test + { + TestCode = source, + FixedCode = fixedSource, + ReferenceAssemblies = Net.Net60, + TestState = + { + AdditionalReferences = { typeof(IModuleContext).Assembly.Location }, + } + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } +} \ No newline at end of file diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeRefactoringVerifier.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeRefactoringVerifier.cs new file mode 100644 index 0000000000..caa42e0d74 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeRefactoringVerifier.cs @@ -0,0 +1,32 @@ +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; + +namespace ModularPipelines.Analyzers.Test.Verifiers; + +public static partial class CSharpCodeRefactoringVerifier + where TCodeRefactoring : CodeRefactoringProvider, new() +{ + public class Test : CSharpCodeRefactoringTest + { + public Test() + { + SolutionTransforms.Add((solution, projectId) => + { + var project = solution.GetProject(projectId); + + var compilationOptions = project.CompilationOptions; + + var parseOptions = project.ParseOptions as CSharpParseOptions; + + compilationOptions = compilationOptions.WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings)); + + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions) + .WithProjectParseOptions(projectId, parseOptions.WithLanguageVersion(LanguageVersion.Preview)); + + return solution; + }); + } + } +} \ No newline at end of file diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeRefactoringVerifier`1.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeRefactoringVerifier`1.cs new file mode 100644 index 0000000000..cfff2fda97 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeRefactoringVerifier`1.cs @@ -0,0 +1,33 @@ +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.Testing; + +namespace ModularPipelines.Analyzers.Test.Verifiers; + +public static partial class CSharpCodeRefactoringVerifier + where TCodeRefactoring : CodeRefactoringProvider, new() +{ + /// + public static async Task VerifyRefactoringAsync(string source, string fixedSource) + { + await VerifyRefactoringAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource); + } + + /// + public static async Task VerifyRefactoringAsync(string source, DiagnosticResult expected, string fixedSource) + { + await VerifyRefactoringAsync(source, new[] { expected }, fixedSource); + } + + /// + public static async Task VerifyRefactoringAsync(string source, DiagnosticResult[] expected, string fixedSource) + { + var test = new Test + { + TestCode = source, + FixedCode = fixedSource, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } +} \ No newline at end of file diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpVerifierHelper.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpVerifierHelper.cs new file mode 100644 index 0000000000..51d7f250a8 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpVerifierHelper.cs @@ -0,0 +1,32 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace ModularPipelines.Analyzers.Test.Verifiers; + +internal static class CSharpVerifierHelper +{ + /// + /// By default, the compiler reports diagnostics for nullable reference types at + /// , and the analyzer test framework defaults to only validating + /// diagnostics at . This map contains all compiler diagnostic IDs + /// related to nullability mapped to , which is then used to enable all + /// of these warnings for default validation during analyzer and code fix tests. + /// + internal static ImmutableDictionary NullableWarnings { get; } = GetNullableWarningsFromCompiler(); + + private static ImmutableDictionary GetNullableWarningsFromCompiler() + { + string[] args = { "/warnaserror:nullable", "-p:LangVersion=preview" }; + var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory); + var nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; + + // Workaround for https://github.com/dotnet/roslyn/issues/41610 + nullableWarnings = nullableWarnings + .SetItem("CS8632", ReportDiagnostic.Error) + .SetItem("CS8669", ReportDiagnostic.Error) + .SetItem("CS8652", ReportDiagnostic.Suppress); + + return nullableWarnings; + } +} \ No newline at end of file diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Vsix/ModularPipelines.Analyzers.Vsix.csproj b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Vsix/ModularPipelines.Analyzers.Vsix.csproj new file mode 100644 index 0000000000..e9527d98bd --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Vsix/ModularPipelines.Analyzers.Vsix.csproj @@ -0,0 +1,48 @@ + + + + + + net472 + ModularPipelines.Analyzers.Vsix + ModularPipelines.Analyzers.Vsix + + + + false + false + false + false + false + false + Roslyn + + + + + + + + Program + $(DevEnvDir)devenv.exe + /rootsuffix $(VSSDKTargetPlatformRegRootSuffix) + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Vsix/source.extension.vsixmanifest b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Vsix/source.extension.vsixmanifest new file mode 100644 index 0000000000..8f07296f21 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Vsix/source.extension.vsixmanifest @@ -0,0 +1,24 @@ + + + + + ModularPipelines.Analyzers + This is a sample diagnostic extension for the .NET Compiler Platform ("Roslyn"). + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers/AnalyzerReleases.Shipped.md b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000000..cb396d5be7 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1,7 @@ +## Release 1.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +MissingDependsOnAttribute | Usage | Error | MissingDependsOnAttributeAnalyzer \ No newline at end of file diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers/AnalyzerReleases.Unshipped.md b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000000..6a2a74bcf8 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/AnalyzerReleases.Unshipped.md @@ -0,0 +1,4 @@ +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers/MissingDependsOnAttributeAnalyzer.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/MissingDependsOnAttributeAnalyzer.cs new file mode 100644 index 0000000000..39a5aae015 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/MissingDependsOnAttributeAnalyzer.cs @@ -0,0 +1,134 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; + +namespace ModularPipelines.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class MissingDependsOnAttributeAnalyzer : DiagnosticAnalyzer +{ + public const string DiagnosticId = "MissingDependsOnAttribute"; + + // You can change these strings in the Resources.resx file. If you do not want your analyzer to be localize-able, you can use regular strings for Title and MessageFormat. + // See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/Localizing%20Analyzers.md for more on localization + private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AnalyzerTitle), Resources.ResourceManager, typeof(Resources)); + private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.AnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources)); + private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.AnalyzerDescription), Resources.ResourceManager, typeof(Resources)); + private const string Category = "Usage"; + + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(AnalyzeMissingDependsOnAttributes, SyntaxKind.InvocationExpression); + } + + private void AnalyzeMissingDependsOnAttributes(SyntaxNodeAnalysisContext context) + { + if (context.Node is not InvocationExpressionSyntax invocationExpressionSyntax) + { + return; + } + + if (invocationExpressionSyntax.Expression is not GenericNameSyntax genericNameSyntax) + { + return; + } + + if (genericNameSyntax.Identifier.ValueText is not ("GetModule" or "WaitForModule")) + { + return; + } + + var genericArgument = genericNameSyntax.TypeArgumentList.Arguments.First(); + + var genericArgumentSymbol = context.SemanticModel.GetSymbolInfo(genericArgument).Symbol; + + if (genericArgumentSymbol is not INamedTypeSymbol namedTypeSymbol) + { + return; + } + + var classContainingGetModuleCallSyntax = GetClassDeclarationSyntax(invocationExpressionSyntax); + + if (classContainingGetModuleCallSyntax is null) + { + return; + } + + var classSymbol = context.SemanticModel.GetDeclaredSymbol(classContainingGetModuleCallSyntax); + + if (classSymbol is null) + { + return; + } + + var attributes = classSymbol.GetAttributes(); + + if (!attributes.Any(x => IsDependsOnAttributeFor(x, namedTypeSymbol))) + { + var properties = new Dictionary + { + ["Name"] = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat) + }.ToImmutableDictionary(); + + context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation(), properties, namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))); + } + } + + private ClassDeclarationSyntax? GetClassDeclarationSyntax(InvocationExpressionSyntax invocationExpressionSyntax) + { + var parent = invocationExpressionSyntax.Parent; + + while (parent is not null) + { + if (parent is ClassDeclarationSyntax classDeclarationSyntax) + { + return classDeclarationSyntax; + } + + parent = parent.Parent; + } + + return null; + } + + private bool IsDependsOnAttributeFor(AttributeData attributeData, INamedTypeSymbol namedTypeSymbol) + { + var attributeClassName = attributeData.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + if (string.IsNullOrEmpty(attributeClassName)) + { + return false; + } + + if (!attributeClassName!.StartsWith("global::ModularPipelines.Attributes.DependsOnAttribute")) + { + return false; + } + + if (attributeData.AttributeClass!.IsGenericType) + { + return SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass.TypeArguments.First(), namedTypeSymbol); + } + + return attributeData.ConstructorArguments.Any(x => + { + var argumentValue = x.Value; + + if (argumentValue is INamedTypeSymbol argumentNamedTypeSymbol) + { + return SymbolEqualityComparer.Default.Equals(argumentNamedTypeSymbol, namedTypeSymbol); + } + + return false; + }); + } +} \ No newline at end of file diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers/ModularPipelines.Analyzers.csproj b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/ModularPipelines.Analyzers.csproj new file mode 100644 index 0000000000..8166bff8d6 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/ModularPipelines.Analyzers.csproj @@ -0,0 +1,25 @@ + + + + netstandard2.0 + false + + + *$(MSBuildProjectFile)* + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers/Resources.Designer.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/Resources.Designer.cs new file mode 100644 index 0000000000..80043af14b --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/Resources.Designer.cs @@ -0,0 +1,89 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ModularPipelines.Analyzers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ModularPipelines.Analyzers.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Missing DependsOn Attribute. + /// + internal static string AnalyzerDescription { + get { + return ResourceManager.GetString("AnalyzerDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This Module should have a DependsOn attribute for {0}. + /// + internal static string AnalyzerMessageFormat { + get { + return ResourceManager.GetString("AnalyzerMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Missing DependsOn Attribute. + /// + internal static string AnalyzerTitle { + get { + return ResourceManager.GetString("AnalyzerTitle", resourceCulture); + } + } + } +} diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers/Resources.resx b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/Resources.resx new file mode 100644 index 0000000000..e588c52f46 --- /dev/null +++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/Resources.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Missing DependsOn Attribute + An optional longer localizable description of the diagnostic. + + + This Module should have a DependsOn attribute for {0} + The format-able message the diagnostic displays. + + + Missing DependsOn Attribute + The title of the diagnostic. + + \ No newline at end of file diff --git a/ModularPipelines.Azure.Pipelines/AzurePipeline.cs b/ModularPipelines.Azure.Pipelines/AzurePipeline.cs new file mode 100644 index 0000000000..ee8ec3c472 --- /dev/null +++ b/ModularPipelines.Azure.Pipelines/AzurePipeline.cs @@ -0,0 +1,11 @@ +namespace ModularPipelines.Azure.Pipelines; + +public class AzurePipeline : IAzurePipeline +{ + public AzurePipeline(AzurePipelineVariables variables) + { + Variables = variables; + } + + public AzurePipelineVariables Variables { get; } +} \ No newline at end of file diff --git a/ModularPipelines.Azure.Pipelines/AzurePipelineAgentVariables.cs b/ModularPipelines.Azure.Pipelines/AzurePipelineAgentVariables.cs new file mode 100644 index 0000000000..2d0ca67fd0 --- /dev/null +++ b/ModularPipelines.Azure.Pipelines/AzurePipelineAgentVariables.cs @@ -0,0 +1,20 @@ +namespace ModularPipelines.Azure.Pipelines; + +public class AzurePipelineAgentVariables : AzurePipelineVariableBase +{ + protected override string Prefix => "Agent"; + + public string BuildDirectory => Get("BuildDirectory")!; + public string ContainerMapping => Get("ContainerMapping")!; + public string HomeDirectory => Get("HomeDirectory")!; + public string Id => Get("Id")!; + public string JobName => Get("JobName")!; + public string JobStatus => Get("JobStatus")!; + public string MachineName => Get("MachineName")!; + public string Name => Get("Name")!; + public string OS => Get("OS")!; + public string OSArchitecture => Get("OSArchitecture")!; + public string TempDirectory => Get("TempDirectory")!; + public string ToolsDirectory => Get("ToolsDirectory")!; + public string WorkFolder => Get("WorkFolder")!; +} \ No newline at end of file diff --git a/ModularPipelines.Azure.Pipelines/AzurePipelineVariableBase.cs b/ModularPipelines.Azure.Pipelines/AzurePipelineVariableBase.cs new file mode 100644 index 0000000000..3154aabf57 --- /dev/null +++ b/ModularPipelines.Azure.Pipelines/AzurePipelineVariableBase.cs @@ -0,0 +1,9 @@ +namespace ModularPipelines.Azure.Pipelines; + +public abstract class AzurePipelineVariableBase +{ + protected abstract string Prefix { get; } + + protected string? Get(string variableName) => Environment.GetEnvironmentVariable(ToEnvironmentVariableName(variableName)); + private string ToEnvironmentVariableName(string variableName) => Prefix?.ToUpperInvariant() + "_" + variableName.ToUpperInvariant().Replace('.', '_'); +} \ No newline at end of file diff --git a/ModularPipelines.Azure.Pipelines/AzurePipelineVariables.cs b/ModularPipelines.Azure.Pipelines/AzurePipelineVariables.cs new file mode 100644 index 0000000000..ddd61e0e1c --- /dev/null +++ b/ModularPipelines.Azure.Pipelines/AzurePipelineVariables.cs @@ -0,0 +1,11 @@ +namespace ModularPipelines.Azure.Pipelines; + +public class AzurePipelineVariables +{ + public AzurePipelineAgentVariables Agent { get; } + + public AzurePipelineVariables(AzurePipelineAgentVariables agent) + { + Agent = agent; + } +} \ No newline at end of file diff --git a/ModularPipelines.Azure.Pipelines/Extensions/AzurePipelineExtensions.cs b/ModularPipelines.Azure.Pipelines/Extensions/AzurePipelineExtensions.cs new file mode 100644 index 0000000000..bce632a5e0 --- /dev/null +++ b/ModularPipelines.Azure.Pipelines/Extensions/AzurePipelineExtensions.cs @@ -0,0 +1,28 @@ +using System.Runtime.CompilerServices; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using ModularPipelines.Context; +using ModularPipelines.Engine; + +namespace ModularPipelines.Azure.Pipelines.Extensions; + +public static class AzurePipelineExtensions +{ +#pragma warning disable CA2255 + [ModuleInitializer] +#pragma warning restore CA2255 + public static void RegisterAzurePipelineContext() + { + ServiceContextRegistry.RegisterContext(collection => RegisterAzurePipelineContext(collection)); + } + + public static IServiceCollection RegisterAzurePipelineContext(this IServiceCollection services) + { + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + return services; + } + + public static IAzurePipeline AzurePipeline(this IModuleContext context) => (IAzurePipeline) context.ServiceProvider.GetRequiredService(); +} \ No newline at end of file diff --git a/ModularPipelines.Azure.Pipelines/IAzurePipeline.cs b/ModularPipelines.Azure.Pipelines/IAzurePipeline.cs new file mode 100644 index 0000000000..3223342674 --- /dev/null +++ b/ModularPipelines.Azure.Pipelines/IAzurePipeline.cs @@ -0,0 +1,6 @@ +namespace ModularPipelines.Azure.Pipelines; + +public interface IAzurePipeline +{ + public AzurePipelineVariables Variables { get; } +} \ No newline at end of file diff --git a/ModularPipelines.Command/ModularPipelines.Command.csproj b/ModularPipelines.Azure.Pipelines/ModularPipelines.Azure.Pipelines.csproj similarity index 100% rename from ModularPipelines.Command/ModularPipelines.Command.csproj rename to ModularPipelines.Azure.Pipelines/ModularPipelines.Azure.Pipelines.csproj diff --git a/ModularPipelines.Build/ModularPipelines.Build.csproj b/ModularPipelines.Build/ModularPipelines.Build.csproj index ba86c2d18e..de417a12cc 100644 --- a/ModularPipelines.Build/ModularPipelines.Build.csproj +++ b/ModularPipelines.Build/ModularPipelines.Build.csproj @@ -11,12 +11,17 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/ModularPipelines.Build/Modules/CleanModule.cs b/ModularPipelines.Build/Modules/CleanModule.cs deleted file mode 100644 index d1ab12b048..0000000000 --- a/ModularPipelines.Build/Modules/CleanModule.cs +++ /dev/null @@ -1,34 +0,0 @@ -using CliWrap.Buffered; -using ModularPipelines.Attributes; -using ModularPipelines.Context; -using ModularPipelines.DotNet.Extensions; -using ModularPipelines.DotNet.Options; -using ModularPipelines.Models; -using ModularPipelines.Modules; - -namespace ModularPipelines.Build.Modules; - -[DependsOn] -public class CleanModule : Module> -{ - public CleanModule(IModuleContext context) : base(context) - { - } - - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) - { - var results = new List(); - - foreach (var projectFile in Context.Environment - .GitRootDirectory! - .GetFiles(file => file.Path.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase))) - { - results.Add(await Context.DotNet().Clean(new DotNetOptions - { - TargetPath = projectFile.Path - }, cancellationToken)); - } - - return results; - } -} \ No newline at end of file diff --git a/ModularPipelines.Build/Modules/LocalMachine/AddLocalNugetSourceModule.cs b/ModularPipelines.Build/Modules/LocalMachine/AddLocalNugetSourceModule.cs index 2eab603eec..d4b5d3b34b 100644 --- a/ModularPipelines.Build/Modules/LocalMachine/AddLocalNugetSourceModule.cs +++ b/ModularPipelines.Build/Modules/LocalMachine/AddLocalNugetSourceModule.cs @@ -1,6 +1,7 @@ using CliWrap.Buffered; using ModularPipelines.Attributes; using ModularPipelines.Context; +using ModularPipelines.Exceptions; using ModularPipelines.Models; using ModularPipelines.Modules; using ModularPipelines.NuGet.Extensions; @@ -11,17 +12,17 @@ namespace ModularPipelines.Build.Modules.LocalMachine; [DependsOn] public class AddLocalNugetSourceModule : Module { - public AddLocalNugetSourceModule(IModuleContext context) : base(context) + protected override Task ShouldIgnoreFailures(IModuleContext context, Exception exception) { + return Task.FromResult(exception is CommandException commandException && + commandException.CommandResult.StandardOutput.Contains("The name specified has already been added to the list of available package sources")); } - public override bool IgnoreFailures => true; - - protected override async Task?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { var localNugetPathResult = await GetModule(); - return await Context.NuGet() + return await context.NuGet() .AddSource(new NuGetSourceOptions(new Uri(localNugetPathResult.Value!), "ModularPipelinesLocalNuGet")); } } \ No newline at end of file diff --git a/ModularPipelines.Build/Modules/LocalMachine/CreateLocalNugetFolderModule.cs b/ModularPipelines.Build/Modules/LocalMachine/CreateLocalNugetFolderModule.cs index 8ba2a1b9e8..d941970897 100644 --- a/ModularPipelines.Build/Modules/LocalMachine/CreateLocalNugetFolderModule.cs +++ b/ModularPipelines.Build/Modules/LocalMachine/CreateLocalNugetFolderModule.cs @@ -10,13 +10,9 @@ namespace ModularPipelines.Build.Modules.LocalMachine; [DependsOn] public class CreateLocalNugetFolderModule : Module { - public CreateLocalNugetFolderModule(IModuleContext context) : base(context) + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { - } - - protected override async Task?> ExecuteAsync(CancellationToken cancellationToken) - { - var userAppData = Context.FileSystem.GetFolder(Environment.SpecialFolder.ApplicationData).Path; + var userAppData = context.FileSystem.GetFolder(Environment.SpecialFolder.ApplicationData).Path; var localNugetRepositoryFolderPath = Path.Combine(userAppData, "ModularPipelines", "LocalNuget"); @@ -27,7 +23,7 @@ public CreateLocalNugetFolderModule(IModuleContext context) : base(context) await Task.Yield(); - Context.Logger.LogInformation("Local NuGet Repository Path: {Path}", localNugetRepositoryFolderPath); + context.Logger.LogInformation("Local NuGet Repository Path: {Path}", localNugetRepositoryFolderPath); return localNugetRepositoryFolderPath; } diff --git a/ModularPipelines.Build/Modules/LocalMachine/UploadPackagesToLocalNuGetModule.cs b/ModularPipelines.Build/Modules/LocalMachine/UploadPackagesToLocalNuGetModule.cs index 9762f57279..83c990158b 100644 --- a/ModularPipelines.Build/Modules/LocalMachine/UploadPackagesToLocalNuGetModule.cs +++ b/ModularPipelines.Build/Modules/LocalMachine/UploadPackagesToLocalNuGetModule.cs @@ -12,28 +12,24 @@ namespace ModularPipelines.Build.Modules.LocalMachine; [DependsOn] public class UploadPackagesToLocalNuGetModule : Module> { - public UploadPackagesToLocalNuGetModule(IModuleContext context) : base(context) - { - } - - protected override async Task InitialiseAsync() + protected override async Task OnBeforeExecute(IModuleContext context) { var packagePaths = await GetModule(); foreach (var packagePath in packagePaths.Value!) { - Context.Logger.LogInformation("Uploading {File}", packagePath); + context.Logger.LogInformation("Uploading {File}", packagePath); } - await base.InitialiseAsync(); + await base.OnBeforeExecute(context); } - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { var localRepoLocation = await GetModule(); var packagePaths = await GetModule(); - return await Context.NuGet() - .UploadPackage(new NuGetUploadOptions(packagePaths.Value!, new Uri(localRepoLocation.Value!))); + return await context.NuGet() + .UploadPackages(new NuGetUploadOptions(packagePaths.Value!, new Uri(localRepoLocation.Value!))); } } \ No newline at end of file diff --git a/ModularPipelines.Build/Modules/NugetVersionGeneratorModule.cs b/ModularPipelines.Build/Modules/NugetVersionGeneratorModule.cs new file mode 100644 index 0000000000..b304370b67 --- /dev/null +++ b/ModularPipelines.Build/Modules/NugetVersionGeneratorModule.cs @@ -0,0 +1,19 @@ +using ModularPipelines.Context; +using ModularPipelines.Git.Extensions; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.Build.Modules; + +public class NugetVersionGeneratorModule : Module +{ + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Yield(); + + var branchName = context.Git().Information.BranchName; + var commitDate = context.Git().Information.LastCommitDateTime; + + return $"{GitVersionInformation.Major}.{GitVersionInformation.Minor}.{GitVersionInformation.Patch}-{GitVersionInformation.PreReleaseLabel}-{GitVersionInformation.CommitsSinceVersionSource}"; + } +} \ No newline at end of file diff --git a/ModularPipelines.Build/Modules/PackProjectsModule.cs b/ModularPipelines.Build/Modules/PackProjectsModule.cs index 91f93c1240..878a7bf165 100644 --- a/ModularPipelines.Build/Modules/PackProjectsModule.cs +++ b/ModularPipelines.Build/Modules/PackProjectsModule.cs @@ -1,8 +1,6 @@ using CliWrap.Buffered; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using ModularPipelines.Attributes; -using ModularPipelines.Build.Settings; using ModularPipelines.Context; using ModularPipelines.DotNet.Extensions; using ModularPipelines.DotNet.Options; @@ -15,29 +13,26 @@ namespace ModularPipelines.Build.Modules; [DependsOn] public class PackProjectsModule : Module> { - private readonly IOptions _options; - - public PackProjectsModule(IModuleContext context, IOptions options) : base(context) - { - _options = options; - } - - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { var results = new List(); - foreach (var unitTestProjectFile in Context.Environment - .GitRootDirectory! - .GetFiles(GetProjectsPredicate)) + var packageVersion = await GetModule(); + + var unitTestProjectFiles = context.Environment + .GitRootDirectory! + .GetFiles(f => GetProjectsPredicate(f, context)); + + foreach (var unitTestProjectFile in unitTestProjectFiles) { - results.Add(await Context.DotNet().Pack(new DotNetOptions + results.Add(await context.DotNet().Pack(new DotNetOptions { TargetPath = unitTestProjectFile.Path, Configuration = Configuration.Release, ExtraArguments = new List { - $"/p:PackageVersion={_options.Value.Version}", - $"/p:Version={_options.Value.Version}" + $"/p:PackageVersion={packageVersion.Value}", + $"/p:Version={packageVersion.Value}" } }, cancellationToken)); } @@ -45,7 +40,7 @@ public PackProjectsModule(IModuleContext context, IOptions opti return results; } - private bool GetProjectsPredicate(File file) + private bool GetProjectsPredicate(File file, IModuleContext context) { var path = file.Path; if (!path.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)) @@ -64,7 +59,7 @@ private bool GetProjectsPredicate(File file) return false; } - Context.Logger.LogInformation("Found File: {File}", path); + context.Logger.LogInformation("Found File: {File}", path); return true; } diff --git a/ModularPipelines.Build/Modules/PackageFilesRemovalModule.cs b/ModularPipelines.Build/Modules/PackageFilesRemovalModule.cs index a6a9a44653..3bb7074497 100644 --- a/ModularPipelines.Build/Modules/PackageFilesRemovalModule.cs +++ b/ModularPipelines.Build/Modules/PackageFilesRemovalModule.cs @@ -1,23 +1,17 @@ -using ModularPipelines.Attributes; using ModularPipelines.Context; using ModularPipelines.Models; using ModularPipelines.Modules; namespace ModularPipelines.Build.Modules; -[DependsOn] public class PackageFilesRemovalModule : Module { - public PackageFilesRemovalModule(IModuleContext context) : base(context) + protected override Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { - } - - protected override Task>?> ExecuteAsync(CancellationToken cancellationToken) - { - var packageFiles = Context.FileSystem.GetFiles(Context.Environment.GitRootDirectory!.Path, + var packageFiles = context.FileSystem.GetFiles(context.Environment.GitRootDirectory!.Path, SearchOption.AllDirectories, path => - path.Extension == ".nupkg"); + path.Extension is ".nupkg"); foreach (var packageFile in packageFiles) { diff --git a/ModularPipelines.Build/Modules/PackagePathsParserModule.cs b/ModularPipelines.Build/Modules/PackagePathsParserModule.cs index e8e7dee8c5..7c07d68659 100644 --- a/ModularPipelines.Build/Modules/PackagePathsParserModule.cs +++ b/ModularPipelines.Build/Modules/PackagePathsParserModule.cs @@ -8,11 +8,7 @@ namespace ModularPipelines.Build.Modules; [DependsOn] public class PackagePathsParserModule : Module> { - public PackagePathsParserModule(IModuleContext context) : base(context) - { - } - - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { var packPackagesModuleResult = await GetModule(); diff --git a/ModularPipelines.Build/Modules/RunUnitTestsModule.cs b/ModularPipelines.Build/Modules/RunUnitTestsModule.cs index fedd37bfee..3d2521b1df 100644 --- a/ModularPipelines.Build/Modules/RunUnitTestsModule.cs +++ b/ModularPipelines.Build/Modules/RunUnitTestsModule.cs @@ -1,5 +1,6 @@ using CliWrap.Buffered; using ModularPipelines.Context; +using ModularPipelines.DotNet; using ModularPipelines.DotNet.Extensions; using ModularPipelines.DotNet.Options; using ModularPipelines.Models; @@ -7,24 +8,21 @@ namespace ModularPipelines.Build.Modules; -public class RunUnitTestsModule : Module> +public class RunUnitTestsModule : Module> { - public RunUnitTestsModule(IModuleContext context) : base(context) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { - } - - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) - { - var results = new List(); + var results = new List(); - foreach (var unitTestProjectFile in Context.Environment + foreach (var unitTestProjectFile in context.Environment .GitRootDirectory! .GetFiles(file => file.Path.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) && file.Path.Contains("UnitTests", StringComparison.OrdinalIgnoreCase))) { - results.Add(await Context.DotNet().Test(new DotNetOptions + results.Add(await context.DotNet().Test(new DotNetOptions { - TargetPath = unitTestProjectFile.Path + TargetPath = unitTestProjectFile.Path, + LogOutput = false }, cancellationToken)); } diff --git a/ModularPipelines.Build/Modules/UploadPackagesToNugetModule.cs b/ModularPipelines.Build/Modules/UploadPackagesToNugetModule.cs index 1aa1adb39a..e47b460424 100644 --- a/ModularPipelines.Build/Modules/UploadPackagesToNugetModule.cs +++ b/ModularPipelines.Build/Modules/UploadPackagesToNugetModule.cs @@ -17,30 +17,30 @@ public class UploadPackagesToNugetModule : Module> { private readonly IOptions _options; - public UploadPackagesToNugetModule(IModuleContext context, IOptions options) : base(context) + public UploadPackagesToNugetModule(IOptions options) { ArgumentNullException.ThrowIfNull(options.Value.ApiKey); _options = options; } - protected override async Task InitialiseAsync() + protected override async Task OnBeforeExecute(IModuleContext context) { var packagePaths = await GetModule(); foreach (var packagePath in packagePaths.Value!) { - Context.Logger.LogInformation("Uploading {File}", packagePath); + context.Logger.LogInformation("Uploading {File}", packagePath); } - await base.InitialiseAsync(); + await base.OnBeforeExecute(context); } - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { var packagePaths = await GetModule(); - return await Context.NuGet() - .UploadPackage(new NuGetUploadOptions(packagePaths.Value!, new Uri("https://api.nuget.org/v3/index.json")) + return await context.NuGet() + .UploadPackages(new NuGetUploadOptions(packagePaths.Value!, new Uri("https://api.nuget.org/v3/index.json")) { ApiKey = _options.Value.ApiKey! }); diff --git a/ModularPipelines.Build/MyModuleHooks.cs b/ModularPipelines.Build/MyModuleHooks.cs index 2fb5ebdb8e..3145de4413 100644 --- a/ModularPipelines.Build/MyModuleHooks.cs +++ b/ModularPipelines.Build/MyModuleHooks.cs @@ -3,20 +3,19 @@ using ModularPipelines.Interfaces; using ModularPipelines.Modules; -namespace ModularPipelines.Build +namespace ModularPipelines.Build; + +public class MyModuleHooks : IPipelineModuleHooks { - public class MyModuleHooks : IPipelineModuleHooks + public Task OnBeforeModuleStartAsync(IModuleContext moduleContext, ModuleBase module) { - public Task OnBeforeModuleStartAsync(IModuleContext moduleContext, IModule module) - { - moduleContext.Logger.LogInformation("{Module} is starting", module.GetType().Name); - return Task.CompletedTask; - } + moduleContext.Logger.LogInformation("{Module} is starting", module.GetType().Name); + return Task.CompletedTask; + } - public Task OnBeforeModuleEndAsync(IModuleContext moduleContext, IModule module) - { - moduleContext.Logger.LogInformation("{Module} finished after {Elapsed}", module.GetType().Name, module.Duration); - return Task.CompletedTask; - } + public Task OnBeforeModuleEndAsync(IModuleContext moduleContext, ModuleBase module) + { + moduleContext.Logger.LogInformation("{Module} finished after {Elapsed}", module.GetType().Name, module.Duration); + return Task.CompletedTask; } } \ No newline at end of file diff --git a/ModularPipelines.Build/Program.cs b/ModularPipelines.Build/Program.cs index 923f9bbff6..1f2dbedb47 100644 --- a/ModularPipelines.Build/Program.cs +++ b/ModularPipelines.Build/Program.cs @@ -5,11 +5,8 @@ using ModularPipelines.Build.Modules; using ModularPipelines.Build.Modules.LocalMachine; using ModularPipelines.Build.Settings; -using ModularPipelines.Command.Extensions; -using ModularPipelines.DotNet.Extensions; using ModularPipelines.Extensions; using ModularPipelines.Host; -using ModularPipelines.NuGet.Extensions; var modules = await PipelineHostBuilder.Create() .ConfigureAppConfiguration((context, builder) => @@ -21,15 +18,10 @@ .ConfigureServices((context, collection) => { collection.Configure(context.Configuration.GetSection("NuGet")); - collection.Configure(context.Configuration.GetSection("Publish")); - collection.RegisterCommandContext() - .RegisterDotNetContext() - .RegisterNuGetContext(); - collection.AddModule() + .AddModule() .AddModule() - .AddModule() .AddModule() .AddModule() .AddPipelineModuleHooks(); diff --git a/ModularPipelines.Build/Settings/PublishSettings.cs b/ModularPipelines.Build/Settings/PublishSettings.cs deleted file mode 100644 index 72cd38d65f..0000000000 --- a/ModularPipelines.Build/Settings/PublishSettings.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ModularPipelines.Build.Settings; - -public record PublishSettings -{ - public string? Version { get; set; } -} \ No newline at end of file diff --git a/ModularPipelines.Build/appsettings.json b/ModularPipelines.Build/appsettings.json index 89ac486b4a..de8c4f98c6 100644 --- a/ModularPipelines.Build/appsettings.json +++ b/ModularPipelines.Build/appsettings.json @@ -6,9 +6,6 @@ "Microsoft": "Information" } }, - "Publish": { - "Version": "0.0.1-alpha03" - }, "NuGet": { "ApiKey": "Override from Secret Source" } diff --git a/ModularPipelines.Cmd/Cmd.cs b/ModularPipelines.Cmd/Cmd.cs new file mode 100644 index 0000000000..1819e2db25 --- /dev/null +++ b/ModularPipelines.Cmd/Cmd.cs @@ -0,0 +1,30 @@ +using CliWrap.Buffered; +using ModularPipelines.Cmd.Models; +using ModularPipelines.Context; +using ModularPipelines.Extensions; + +namespace ModularPipelines.Cmd; + +public class Cmd : ICmd +{ + private readonly IModuleContext _context; + + public Cmd(IModuleContext context) + { + _context = context; + } + + public Task Script(CmdScriptOptions options, CancellationToken cancellationToken = default) + { + var arguments = new List { "/c" }; + + if (!options.Echo) + { + arguments.Add("/q"); + } + + arguments.Add(options.Script); + + return _context.Command.UsingCommandLineTool(options.ToCommandLineToolOptions("cmd", arguments), cancellationToken); + } +} \ No newline at end of file diff --git a/ModularPipelines.Cmd/Extensions/CmdExtensions.cs b/ModularPipelines.Cmd/Extensions/CmdExtensions.cs new file mode 100644 index 0000000000..b72bb51c94 --- /dev/null +++ b/ModularPipelines.Cmd/Extensions/CmdExtensions.cs @@ -0,0 +1,27 @@ +using System.Runtime.CompilerServices; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using ModularPipelines.Context; +using ModularPipelines.Engine; + +namespace ModularPipelines.Cmd.Extensions; + +public static class CmdExtensions +{ +#pragma warning disable CA2255 + [ModuleInitializer] +#pragma warning restore CA2255 + public static void RegisterCmdContext() + { + ServiceContextRegistry.RegisterContext(collection => RegisterCmdContext(collection)); + } + + public static IServiceCollection RegisterCmdContext(this IServiceCollection services) + { + services.TryAddSingleton(); + + return services; + } + + public static ICmd Cmd(this IModuleContext context) => (ICmd) context.ServiceProvider.GetRequiredService(); +} \ No newline at end of file diff --git a/ModularPipelines.Cmd/ICmd.cs b/ModularPipelines.Cmd/ICmd.cs new file mode 100644 index 0000000000..bd01747400 --- /dev/null +++ b/ModularPipelines.Cmd/ICmd.cs @@ -0,0 +1,9 @@ +using CliWrap.Buffered; +using ModularPipelines.Cmd.Models; + +namespace ModularPipelines.Cmd; + +public interface ICmd +{ + Task Script(CmdScriptOptions options, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/ModularPipelines.Cmd/Models/CmdScriptOptions.cs b/ModularPipelines.Cmd/Models/CmdScriptOptions.cs new file mode 100644 index 0000000000..cb4510d1b9 --- /dev/null +++ b/ModularPipelines.Cmd/Models/CmdScriptOptions.cs @@ -0,0 +1,8 @@ +using ModularPipelines.Options; + +namespace ModularPipelines.Cmd.Models; + +public record CmdScriptOptions(string Script) : CommandEnvironmentOptions +{ + public bool Echo { get; init; } = true; +} \ No newline at end of file diff --git a/ModularPipelines.Installer/ModularPipelines.Installer.csproj b/ModularPipelines.Cmd/ModularPipelines.Cmd.csproj similarity index 77% rename from ModularPipelines.Installer/ModularPipelines.Installer.csproj rename to ModularPipelines.Cmd/ModularPipelines.Cmd.csproj index 29eda92bbe..4decd268f3 100644 --- a/ModularPipelines.Installer/ModularPipelines.Installer.csproj +++ b/ModularPipelines.Cmd/ModularPipelines.Cmd.csproj @@ -7,7 +7,6 @@ - diff --git a/ModularPipelines.Command/Command.cs b/ModularPipelines.Command/Command.cs deleted file mode 100644 index 2d2c5f934b..0000000000 --- a/ModularPipelines.Command/Command.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.ObjectModel; -using CliWrap; -using CliWrap.Buffered; -using ModularPipelines.Command.Options; - -namespace ModularPipelines.Command; - -public class Command : ICommand -{ - public async Task Of(CommandOptions options, CancellationToken cancellationToken = default) - { - var command = Cli.Wrap(options.Command); - - if (options.WorkingDirectory != null) - { - command = command.WithWorkingDirectory(options.WorkingDirectory); - } - - if (options.EnvironmentVariables != null) - { - command = command.WithEnvironmentVariables(options.EnvironmentVariables); - } - - var result = await command - .ExecuteBufferedAsync(cancellationToken: cancellationToken); - - return result; - } - - public async Task UsingCommandLineTool(CommandLineToolOptions options, CancellationToken cancellationToken = default) - { - var parsedArgs = string.Equals(options.Arguments?.ElementAtOrDefault(0), options.Tool) - ? options.Arguments?.Skip(1) : options.Arguments; - - var command = Cli.Wrap(options.Tool) - .WithArguments(parsedArgs ?? Array.Empty()); - - if (options.WorkingDirectory != null) - { - command = command.WithWorkingDirectory(options.WorkingDirectory); - } - - if (options.EnvironmentVariables != null) - { - command = command.WithEnvironmentVariables(new ReadOnlyDictionary(options.EnvironmentVariables)); - } - - var result = await command - .ExecuteBufferedAsync(cancellationToken: cancellationToken); - - return result; - } -} \ No newline at end of file diff --git a/ModularPipelines.Command/Extensions/CommandExtensions.cs b/ModularPipelines.Command/Extensions/CommandExtensions.cs deleted file mode 100644 index 897cfdd0f4..0000000000 --- a/ModularPipelines.Command/Extensions/CommandExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using ModularPipelines.Context; - -namespace ModularPipelines.Command.Extensions; - -public static class CommandExtensions -{ - public static IServiceCollection RegisterCommandContext(this IServiceCollection services) - { - services.TryAddSingleton(); - return services; - } - - public static ICommand Command(this IModuleContext context) => context.Get()!; -} \ No newline at end of file diff --git a/ModularPipelines.Command/Options/CommandLineToolOptions.cs b/ModularPipelines.Command/Options/CommandLineToolOptions.cs deleted file mode 100644 index 85a5b87c73..0000000000 --- a/ModularPipelines.Command/Options/CommandLineToolOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ModularPipelines.Command.Options; - -public record CommandLineToolOptions(string Tool) -{ - public IEnumerable? Arguments { get; init; } - public IDictionary? EnvironmentVariables { get; init; } - public string? WorkingDirectory { get; init; } -} \ No newline at end of file diff --git a/ModularPipelines.Command/Options/CommandOptions.cs b/ModularPipelines.Command/Options/CommandOptions.cs deleted file mode 100644 index 0c7f9cf8a1..0000000000 --- a/ModularPipelines.Command/Options/CommandOptions.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ModularPipelines.Command.Options; - -public record CommandOptions(string Command) -{ - public IReadOnlyDictionary? EnvironmentVariables { get; init; } - public string? WorkingDirectory { get; init; } -} \ No newline at end of file diff --git a/ModularPipelines.Docker/Docker.cs b/ModularPipelines.Docker/Docker.cs new file mode 100644 index 0000000000..5c0873a10f --- /dev/null +++ b/ModularPipelines.Docker/Docker.cs @@ -0,0 +1,77 @@ +using ModularPipelines.Context; +using ModularPipelines.Docker.Options; +using ModularPipelines.Extensions; + +namespace ModularPipelines.Docker; + +public class Docker : IDocker +{ + private readonly ICommand _command; + + public Docker(ICommand command) + { + _command = command; + } + + public async Task Login(DockerLoginOptions dockerLoginOptions) + { + var arguments = new List + { + "login", + $"--username={dockerLoginOptions.Username}", + $"--password={dockerLoginOptions.Password}" + }; + + arguments.AddNonNullOrEmpty(dockerLoginOptions.Server?.AbsolutePath); + + await _command.UsingCommandLineTool(dockerLoginOptions.ToCommandLineToolOptions("docker", arguments)); + } + + public async Task BuildFromDockerfile(DockerBuildOptions dockerBuildOptions) + { + var workingDirectory = + dockerBuildOptions.DockerfileFolder.Path == dockerBuildOptions.WorkingDirectory ? "." : dockerBuildOptions.DockerfileFolder.Path; + + var arguments = new List + { + "build", + workingDirectory, + }; + + arguments.AddNonNullOrEmptyArgumentWithSwitch("-t", dockerBuildOptions.Tag); + + arguments.AddRangeNonNullOrEmptyArgumentWithSwitch("--build-arg", dockerBuildOptions.BuildArguments); + + arguments.AddNonNullOrEmpty(dockerBuildOptions.Dockerfile?.Path); + + await _command.UsingCommandLineTool(dockerBuildOptions.ToCommandLineToolOptions("docker", arguments)); + } + + public Task Logout(DockerOptions? options = null) + { + options ??= new DockerOptions(); + + return _command.UsingCommandLineTool(options.ToCommandLineToolOptions("docker", new[] { "logout" })); + } + + public Task Push(DockerPushOptions dockerPushOptions) + { + var arguments = new List + { + "push", + $"--disable-content-trust={dockerPushOptions.DisableContentTrust.ToString().ToLower()}", + $"{dockerPushOptions.Name}:{dockerPushOptions.Tag}" + }; + + return _command.UsingCommandLineTool(dockerPushOptions.ToCommandLineToolOptions("docker", arguments)); + } + + public async Task Version(DockerArgumentOptions? dockerArgumentOptions = null) + { + dockerArgumentOptions ??= new DockerArgumentOptions(); + + var result = await _command.UsingCommandLineTool(dockerArgumentOptions.ToCommandLineToolOptions("docker", new []{ "version" })); + + return result.StandardOutput; + } +} \ No newline at end of file diff --git a/ModularPipelines.Docker/DockerBuildOptions.cs b/ModularPipelines.Docker/DockerBuildOptions.cs new file mode 100644 index 0000000000..711511199a --- /dev/null +++ b/ModularPipelines.Docker/DockerBuildOptions.cs @@ -0,0 +1,16 @@ +using ModularPipelines.FileSystem; +using ModularPipelines.Options; +using File = ModularPipelines.FileSystem.File; + +namespace ModularPipelines.Docker; + +public record DockerBuildOptions(Folder DockerfileFolder) : CommandLineToolOptions("docker") +{ + public string? Name { get; init; } + + public string? Tag { get; init; } + + public IEnumerable? BuildArguments { get; init; } + + public File? Dockerfile { get; init; } +} \ No newline at end of file diff --git a/ModularPipelines.Docker/DockerLoginOptions.cs b/ModularPipelines.Docker/DockerLoginOptions.cs new file mode 100644 index 0000000000..4813f0b8b0 --- /dev/null +++ b/ModularPipelines.Docker/DockerLoginOptions.cs @@ -0,0 +1,8 @@ +using ModularPipelines.Docker.Options; + +namespace ModularPipelines.Docker; + +public record DockerLoginOptions(string Username, string Password) : DockerOptions +{ + public Uri? Server { get; init; } +} \ No newline at end of file diff --git a/ModularPipelines.Docker/DockerPushOptions.cs b/ModularPipelines.Docker/DockerPushOptions.cs new file mode 100644 index 0000000000..f0d0a167bf --- /dev/null +++ b/ModularPipelines.Docker/DockerPushOptions.cs @@ -0,0 +1,8 @@ +using ModularPipelines.Docker.Options; + +namespace ModularPipelines.Docker; + +public record DockerPushOptions(string Name, string Tag) : DockerOptions +{ + public bool DisableContentTrust { get; init; } = true; +}; \ No newline at end of file diff --git a/ModularPipelines.Docker/IDocker.cs b/ModularPipelines.Docker/IDocker.cs new file mode 100644 index 0000000000..6641916e2f --- /dev/null +++ b/ModularPipelines.Docker/IDocker.cs @@ -0,0 +1,12 @@ +using ModularPipelines.Docker.Options; + +namespace ModularPipelines.Docker; + +public interface IDocker +{ + Task Login(DockerLoginOptions dockerLoginOptions); + Task BuildFromDockerfile(DockerBuildOptions dockerBuildOptions); + Task Logout(DockerOptions? options = null); + Task Push(DockerPushOptions dockerPushOptions); + Task Version(DockerArgumentOptions? dockerArgumentOptions = null); +} \ No newline at end of file diff --git a/ModularPipelines.Docker/ModularPipelines.Docker.csproj b/ModularPipelines.Docker/ModularPipelines.Docker.csproj new file mode 100644 index 0000000000..b1997dacac --- /dev/null +++ b/ModularPipelines.Docker/ModularPipelines.Docker.csproj @@ -0,0 +1,11 @@ + + + + net6.0 + + + + + + + diff --git a/ModularPipelines.Docker/Options/DockerArgumentOptions.cs b/ModularPipelines.Docker/Options/DockerArgumentOptions.cs new file mode 100644 index 0000000000..f8a7f18f51 --- /dev/null +++ b/ModularPipelines.Docker/Options/DockerArgumentOptions.cs @@ -0,0 +1,5 @@ +using ModularPipelines.Options; + +namespace ModularPipelines.Docker.Options; + +public record DockerArgumentOptions() : CommandLineToolOptions("docker"); \ No newline at end of file diff --git a/ModularPipelines.Docker/Options/DockerOptions.cs b/ModularPipelines.Docker/Options/DockerOptions.cs new file mode 100644 index 0000000000..ef2e825b3b --- /dev/null +++ b/ModularPipelines.Docker/Options/DockerOptions.cs @@ -0,0 +1,5 @@ +using ModularPipelines.Options; + +namespace ModularPipelines.Docker.Options; + +public record DockerOptions : CommandEnvironmentOptions; \ No newline at end of file diff --git a/ModularPipelines.DotNet/DotNet.cs b/ModularPipelines.DotNet/DotNet.cs index 631bfe2436..df0f101808 100644 --- a/ModularPipelines.DotNet/DotNet.cs +++ b/ModularPipelines.DotNet/DotNet.cs @@ -1,18 +1,21 @@ using CliWrap.Buffered; -using ModularPipelines.Command.Extensions; -using ModularPipelines.Command.Options; using ModularPipelines.Context; using ModularPipelines.DotNet.Options; +using ModularPipelines.Extensions; +using ModularPipelines.Options; +using File = ModularPipelines.FileSystem.File; namespace ModularPipelines.DotNet; public class DotNet : IDotNet { - public IModuleContext Context { get; } + private readonly IModuleContext _context; + private readonly ITrxParser _trxParser; - public DotNet(IModuleContext context) + public DotNet(IModuleContext context, ITrxParser trxParser) { - Context = context; + _context = context; + _trxParser = trxParser; } public Task Restore(DotNetOptions options, CancellationToken cancellationToken = default) @@ -40,11 +43,40 @@ public Task Clean(DotNetOptions options, CancellationToke return RunCommand(ToDotNetCommandOptions("clean", options), cancellationToken); } - public Task Test(DotNetOptions options, CancellationToken cancellationToken = default) + public async Task Test(DotNetOptions options, CancellationToken cancellationToken = default) { - return RunCommand(ToDotNetCommandOptions("test", options), cancellationToken); + var trxFilePath = Path.GetTempFileName(); + var argumentsWithLogger = options.ExtraArguments?.ToList() ?? new List(); + argumentsWithLogger.Add("--logger"); + argumentsWithLogger.Add($"trx;logfilename={trxFilePath}"); + + var optionsWithLogger = options with + { + ExtraArguments = argumentsWithLogger + }; + + var command = await RunCommand(ToDotNetCommandOptions("test", optionsWithLogger), cancellationToken); + + var trxContents = await _context.FileSystem.GetFile(trxFilePath).ReadAsync(); + + return _trxParser.ParseTestResult(trxContents); } - + + public Task Version(CommandEnvironmentOptions? options, CancellationToken cancellationToken = default) + { + options ??= new CommandEnvironmentOptions(); + + return RunCommand(new DotNetCommandOptions + { + Command = new[] { "--version" }, + EnvironmentVariables = options.EnvironmentVariables, + WorkingDirectory = options.WorkingDirectory, + Credentials = options.Credentials, + LogInput = options.LogInput, + LogOutput = options.LogOutput + }, cancellationToken); + } + public Task CustomCommand(DotNetCommandOptions options, CancellationToken cancellationToken = default) { return RunCommand(options, cancellationToken); @@ -59,36 +91,29 @@ private static DotNetCommandOptions ToDotNetCommandOptions(string command, DotNe ExtraArguments = options.ExtraArguments, TargetPath = options.TargetPath, Configuration = options.Configuration, - WorkingDirectory = options.WorkingDirectory + WorkingDirectory = options.WorkingDirectory, + Credentials = options.Credentials, + LogInput = options.LogInput, + LogOutput = options.LogOutput }; } - private Task RunCommand(DotNetCommandOptions options, - CancellationToken cancellationToken = default) + private Task RunCommand(DotNetCommandOptions options, CancellationToken cancellationToken) { var arguments = options.Command?.ToList() ?? new List(); - if (options.TargetPath != null) - { - arguments.Add(options.TargetPath); - } - - if (options.ExtraArguments != null) - { - arguments.AddRange(options.ExtraArguments); - } + arguments.AddNonNullOrEmpty(options.TargetPath); + arguments.AddRangeNonNullOrEmpty(options.ExtraArguments); + arguments.AddNonNullOrEmptyArgumentWithSwitch("-c", options.Configuration?.ToString()); - if (options.Configuration != null) - { - arguments.Add("-c"); - arguments.Add(options.Configuration.ToString()!); - } - - return Context.Command().UsingCommandLineTool(new CommandLineToolOptions("dotnet") + return _context.Command.UsingCommandLineTool(new CommandLineToolOptions("dotnet") { Arguments = arguments, EnvironmentVariables = options.EnvironmentVariables, - WorkingDirectory = options.WorkingDirectory + WorkingDirectory = options.WorkingDirectory, + Credentials = options.Credentials, + LogInput = options.LogInput, + LogOutput = options.LogOutput }, cancellationToken); } } \ No newline at end of file diff --git a/ModularPipelines.DotNet/DotNetTestResult.cs b/ModularPipelines.DotNet/DotNetTestResult.cs new file mode 100644 index 0000000000..2fd693926c --- /dev/null +++ b/ModularPipelines.DotNet/DotNetTestResult.cs @@ -0,0 +1,5 @@ +namespace ModularPipelines.DotNet; + +public record DotNetTestResult(IReadOnlyList UnitTestResults) +{ +} \ No newline at end of file diff --git a/ModularPipelines.DotNet/Extensions/DotNetExtensions.cs b/ModularPipelines.DotNet/Extensions/DotNetExtensions.cs index 659b64da68..a7ad5b9ba6 100644 --- a/ModularPipelines.DotNet/Extensions/DotNetExtensions.cs +++ b/ModularPipelines.DotNet/Extensions/DotNetExtensions.cs @@ -1,18 +1,28 @@ -using Microsoft.Extensions.DependencyInjection; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using ModularPipelines.Command.Extensions; using ModularPipelines.Context; +using ModularPipelines.Engine; namespace ModularPipelines.DotNet.Extensions; public static class DotNetExtensions { +#pragma warning disable CA2255 + [ModuleInitializer] +#pragma warning restore CA2255 + public static void RegisterDotNetContext() + { + ServiceContextRegistry.RegisterContext(collection => RegisterDotNetContext(collection)); + } + public static IServiceCollection RegisterDotNetContext(this IServiceCollection services) { - services.RegisterCommandContext(); services.TryAddSingleton(); + services.TryAddSingleton(); return services; } - - public static IDotNet DotNet(this IModuleContext context) => context.Get()!; + + public static IDotNet DotNet(this IModuleContext context) => context.ServiceProvider.GetRequiredService(); + } \ No newline at end of file diff --git a/ModularPipelines.DotNet/IDotNet.cs b/ModularPipelines.DotNet/IDotNet.cs index 6df7f7684d..c2fbf3c4a4 100644 --- a/ModularPipelines.DotNet/IDotNet.cs +++ b/ModularPipelines.DotNet/IDotNet.cs @@ -1,5 +1,6 @@ using CliWrap.Buffered; using ModularPipelines.DotNet.Options; +using ModularPipelines.Options; namespace ModularPipelines.DotNet; @@ -10,7 +11,9 @@ public interface IDotNet Task Publish(DotNetOptions options, CancellationToken cancellationToken = default); Task Pack(DotNetOptions options, CancellationToken cancellationToken = default); Task Clean(DotNetOptions options, CancellationToken cancellationToken = default); - Task Test(DotNetOptions options, CancellationToken cancellationToken = default); + Task Test(DotNetOptions options, CancellationToken cancellationToken = default); + + Task Version(CommandEnvironmentOptions? options = null, CancellationToken cancellationToken = default); Task CustomCommand(DotNetCommandOptions options, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/ModularPipelines.DotNet/ITrxParser.cs b/ModularPipelines.DotNet/ITrxParser.cs new file mode 100644 index 0000000000..ea2d9690bb --- /dev/null +++ b/ModularPipelines.DotNet/ITrxParser.cs @@ -0,0 +1,6 @@ +namespace ModularPipelines.DotNet; + +public interface ITrxParser +{ + DotNetTestResult ParseTestResult(string input); +} \ No newline at end of file diff --git a/ModularPipelines.DotNet/ModularPipelines.DotNet.csproj b/ModularPipelines.DotNet/ModularPipelines.DotNet.csproj index 29eda92bbe..4decd268f3 100644 --- a/ModularPipelines.DotNet/ModularPipelines.DotNet.csproj +++ b/ModularPipelines.DotNet/ModularPipelines.DotNet.csproj @@ -7,7 +7,6 @@ - diff --git a/ModularPipelines.DotNet/Options/DotNetOptions.cs b/ModularPipelines.DotNet/Options/DotNetOptions.cs index 9cf7c9a8dd..871462c1ee 100644 --- a/ModularPipelines.DotNet/Options/DotNetOptions.cs +++ b/ModularPipelines.DotNet/Options/DotNetOptions.cs @@ -1,14 +1,12 @@ +using ModularPipelines.Options; + namespace ModularPipelines.DotNet.Options; -public record DotNetOptions +public record DotNetOptions : CommandEnvironmentOptions { - public string? WorkingDirectory { get; init; } - public string? TargetPath { get; init; } public IEnumerable? ExtraArguments { get; init; } public Configuration? Configuration { get; init; } - - public IDictionary? EnvironmentVariables { get; init; } } \ No newline at end of file diff --git a/ModularPipelines.DotNet/TestOutput.cs b/ModularPipelines.DotNet/TestOutput.cs new file mode 100644 index 0000000000..9daf8f4bd2 --- /dev/null +++ b/ModularPipelines.DotNet/TestOutput.cs @@ -0,0 +1,10 @@ +using System.Xml.Serialization; + +namespace ModularPipelines.DotNet; + +[XmlRoot(ElementName = "Output")] +public record TestOutput +{ + [XmlElement("StdOut")] + public string? StdOut { get; init; } +} \ No newline at end of file diff --git a/ModularPipelines.DotNet/TrxParser.cs b/ModularPipelines.DotNet/TrxParser.cs new file mode 100644 index 0000000000..d99ec940e7 --- /dev/null +++ b/ModularPipelines.DotNet/TrxParser.cs @@ -0,0 +1,49 @@ +using System.Xml.Linq; +using ModularPipelines.Context; + +namespace ModularPipelines.DotNet; + +public class TrxParser : ITrxParser +{ + private readonly IXml _xml; + + public TrxParser(IXml xml) + { + _xml = xml; + } + + public DotNetTestResult ParseTestResult(string input) + { + return new DotNetTestResult(GetUnitTestResults(input)); + } + + private List GetUnitTestResults(string input) + { + return XDocument.Load(new StringReader(input)).Descendants() + .Where(d => d.Name.LocalName == "UnitTestResult") + .Select(ParseElement) + .ToList(); + } + + private UnitTestResult ParseElement(XElement element) + { + return new UnitTestResult + { + ExecutionId = element.Attribute("executionId")!.Value, + TestId = element.Attribute("testId")!.Value, + TestName = element.Attribute("testName")!.Value, + ComputerName = element.Attribute("computerName")!.Value, + Duration = element.Attribute("duration")!.Value, + StartTime = element.Attribute("startTime")!.Value, + EndTime = element.Attribute("endTime")!.Value, + TestType = element.Attribute("testType")!.Value, + Outcome = element.Attribute("outcome")!.Value, + TestListId = element.Attribute("testListId")!.Value, + RelativeResultsDirectory = element.Attribute("relativeResultsDirectory")!.Value, + Output = new TestOutput + { + StdOut = element.Descendants().FirstOrDefault(x => x.Name.LocalName == "StdOut")?.Value + } + }; + } +} \ No newline at end of file diff --git a/ModularPipelines.DotNet/UnitTestResult.cs b/ModularPipelines.DotNet/UnitTestResult.cs new file mode 100644 index 0000000000..103aed37ca --- /dev/null +++ b/ModularPipelines.DotNet/UnitTestResult.cs @@ -0,0 +1,44 @@ +using System.Xml.Serialization; + +namespace ModularPipelines.DotNet; + +[Serializable] +[XmlRoot(ElementName = "UnitTestResult")] +public record UnitTestResult +{ + [XmlAttribute("executionId")] + public string ExecutionId { get; init; } + + [XmlAttribute("testId")] + public string TestId { get; init; } + + [XmlAttribute("testName")] + public string TestName { get; init; } + + [XmlAttribute("computerName")] + public string ComputerName { get; init; } + + [XmlAttribute("duration")] + public string Duration { get; init; } + + [XmlAttribute("startTime")] + public string StartTime { get; init; } + + [XmlAttribute("endTime")] + public string EndTime { get; init; } + + [XmlAttribute("testType")] + public string TestType { get; init; } + + [XmlAttribute("outcome")] + public string Outcome { get; init; } + + [XmlAttribute("testListId")] + public string TestListId { get; init; } + + [XmlAttribute("relativeResultsDirectory")] + public string RelativeResultsDirectory { get; init; } + + [XmlElement("Output")] + public TestOutput Output { get; init; } +} \ No newline at end of file diff --git a/ModularPipelines.Examples/ModularPipelines.Examples.csproj b/ModularPipelines.Examples/ModularPipelines.Examples.csproj index 9c8cbe03d7..f7d8e6e113 100644 --- a/ModularPipelines.Examples/ModularPipelines.Examples.csproj +++ b/ModularPipelines.Examples/ModularPipelines.Examples.csproj @@ -5,12 +5,12 @@ net6.0 enable enable + preview - @@ -23,4 +23,11 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/ModularPipelines.Examples/Modules/DependentOnSuccessModule.cs b/ModularPipelines.Examples/Modules/DependentOnSuccessModule.cs new file mode 100644 index 0000000000..b0c5ebc677 --- /dev/null +++ b/ModularPipelines.Examples/Modules/DependentOnSuccessModule.cs @@ -0,0 +1,46 @@ +using ModularPipelines.Attributes; +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.Examples.Modules; + +[DependsOn] +public class DependentOnSuccessModule : Module +{ + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken); + return null; + } +} + +[DependsOn] +public class DependentOn2 : Module +{ + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken); + return null; + } +} + +[DependsOn] +public class DependentOn3 : Module +{ + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken); + return null; + } +} + +[DependsOn] +public class DependentOn4 : Module +{ + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken); + return null; + } +} \ No newline at end of file diff --git a/ModularPipelines.Examples/Modules/DotnetTestModule.cs b/ModularPipelines.Examples/Modules/DotnetTestModule.cs new file mode 100644 index 0000000000..7186c86260 --- /dev/null +++ b/ModularPipelines.Examples/Modules/DotnetTestModule.cs @@ -0,0 +1,19 @@ +using ModularPipelines.Context; +using ModularPipelines.DotNet; +using ModularPipelines.DotNet.Extensions; +using ModularPipelines.DotNet.Options; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.Examples.Modules; + +public class DotnetTestModule : Module +{ + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + return await context.DotNet().Test(new DotNetOptions + { + WorkingDirectory = context.Environment.GitRootDirectory!.GetFolder("ModularPipelines.UnitTests").Path + }, cancellationToken); + } +} \ No newline at end of file diff --git a/ModularPipelines.Examples/Modules/FailedModule.cs b/ModularPipelines.Examples/Modules/FailedModule.cs index 8e8af36260..2f3a032f31 100644 --- a/ModularPipelines.Examples/Modules/FailedModule.cs +++ b/ModularPipelines.Examples/Modules/FailedModule.cs @@ -6,13 +6,7 @@ namespace ModularPipelines.Examples.Modules; public class FailedModule : Module { - public FailedModule(IModuleContext context) : base(context) - { - } - - public override bool IgnoreFailures => true; - - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken); diff --git a/ModularPipelines.Examples/Modules/GitLastCommitModule.cs b/ModularPipelines.Examples/Modules/GitLastCommitModule.cs new file mode 100644 index 0000000000..5a7061a426 --- /dev/null +++ b/ModularPipelines.Examples/Modules/GitLastCommitModule.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Logging; +using ModularPipelines.Context; +using ModularPipelines.Git.Extensions; +using ModularPipelines.Git.Models; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.Examples.Modules; + +public class GitLastCommitModule : Module +{ + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Yield(); + + context.Logger.LogInformation("Getting Last Git Commit"); + + var lastCommit = context.Git().Information.PreviousCommit; + + var allCommits = await context.Git().Information.Commits(cancellationToken: cancellationToken).ToListAsync(cancellationToken: cancellationToken); + + return lastCommit; + } +} \ No newline at end of file diff --git a/ModularPipelines.Examples/Modules/GitVersionModule.cs b/ModularPipelines.Examples/Modules/GitVersionModule.cs index 0345d5f43a..e07dd804b9 100644 --- a/ModularPipelines.Examples/Modules/GitVersionModule.cs +++ b/ModularPipelines.Examples/Modules/GitVersionModule.cs @@ -8,12 +8,8 @@ namespace ModularPipelines.Examples.Modules; public class GitVersionModule : Module { - public GitVersionModule(IModuleContext context) : base(context) + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { - } - - protected override async Task?> ExecuteAsync(CancellationToken cancellationToken) - { - return await Context.Git().Version(); + return await context.Git().Operations.Version(cancellationToken: cancellationToken); } } \ No newline at end of file diff --git a/ModularPipelines.Examples/Modules/IgnoredModule.cs b/ModularPipelines.Examples/Modules/IgnoredModule.cs index fe8dfee7df..3361c46937 100644 --- a/ModularPipelines.Examples/Modules/IgnoredModule.cs +++ b/ModularPipelines.Examples/Modules/IgnoredModule.cs @@ -8,11 +8,7 @@ namespace ModularPipelines.Examples.Modules; [ModuleCategory("Ignore")] public class IgnoredModule : Module { - public IgnoredModule(IModuleContext context) : base(context) - { - } - - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { await Task.Delay(TimeSpan.FromSeconds(15), cancellationToken); return null; diff --git a/ModularPipelines.Examples/Modules/NotepadPlusPlusInstallerModule.cs b/ModularPipelines.Examples/Modules/NotepadPlusPlusInstallerModule.cs index 4868f266d6..1caa41e00b 100644 --- a/ModularPipelines.Examples/Modules/NotepadPlusPlusInstallerModule.cs +++ b/ModularPipelines.Examples/Modules/NotepadPlusPlusInstallerModule.cs @@ -1,22 +1,17 @@ using CliWrap.Buffered; using ModularPipelines.Context; -using ModularPipelines.Installer.Extensions; -using ModularPipelines.Installer.Options; using ModularPipelines.Models; using ModularPipelines.Modules; +using ModularPipelines.Options; namespace ModularPipelines.Examples.Modules; public class NotepadPlusPlusInstallerModule : Module { - public NotepadPlusPlusInstallerModule(IModuleContext context) : base(context) + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { - } - - protected override async Task?> ExecuteAsync(CancellationToken cancellationToken) - { - return await Context.Installer() - .InstallFromWeb(new WebInstallerOptions(new Uri( + return await context.Installer + .InstallFromWebAsync(new WebInstallerOptions(new Uri( "https://github.com/notepad-plus-plus/notepad-plus-plus/releases/download/v8.5.3/npp.8.5.3.Installer.x64.exe")), cancellationToken); } } \ No newline at end of file diff --git a/ModularPipelines.Examples/Modules/NugetVersionGeneratorModule.cs b/ModularPipelines.Examples/Modules/NugetVersionGeneratorModule.cs new file mode 100644 index 0000000000..06fb0f4c79 --- /dev/null +++ b/ModularPipelines.Examples/Modules/NugetVersionGeneratorModule.cs @@ -0,0 +1,15 @@ +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.Examples.Modules; + +public class NugetVersionGeneratorModule : Module +{ + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Yield(); + + return $"{GitVersionInformation.Major}.{GitVersionInformation.Minor}.{GitVersionInformation.Patch}-{GitVersionInformation.PreReleaseLabel}-{GitVersionInformation.CommitsSinceVersionSource}"; + } +} \ No newline at end of file diff --git a/ModularPipelines.Examples/Modules/SuccessModule.cs b/ModularPipelines.Examples/Modules/SuccessModule.cs index 93ff133bb5..a843a06f29 100644 --- a/ModularPipelines.Examples/Modules/SuccessModule.cs +++ b/ModularPipelines.Examples/Modules/SuccessModule.cs @@ -6,11 +6,7 @@ namespace ModularPipelines.Examples.Modules; public class SuccessModule : Module { - public SuccessModule(IModuleContext context) : base(context) - { - } - - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { await Task.Delay(TimeSpan.FromSeconds(15), cancellationToken); return null; diff --git a/ModularPipelines.Examples/Modules/SuccessModule2.cs b/ModularPipelines.Examples/Modules/SuccessModule2.cs index bb0bec276a..a11e8b812f 100644 --- a/ModularPipelines.Examples/Modules/SuccessModule2.cs +++ b/ModularPipelines.Examples/Modules/SuccessModule2.cs @@ -6,11 +6,7 @@ namespace ModularPipelines.Examples.Modules; public class SuccessModule2 : Module { - public SuccessModule2(IModuleContext context) : base(context) - { - } - - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); return null; diff --git a/ModularPipelines.Examples/Modules/SuccessModule3.cs b/ModularPipelines.Examples/Modules/SuccessModule3.cs index 8ce9b9705f..9742c22588 100644 --- a/ModularPipelines.Examples/Modules/SuccessModule3.cs +++ b/ModularPipelines.Examples/Modules/SuccessModule3.cs @@ -6,11 +6,7 @@ namespace ModularPipelines.Examples.Modules; public class SuccessModule3 : Module { - public SuccessModule3(IModuleContext context) : base(context) - { - } - - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { await Task.Delay(TimeSpan.FromSeconds(12), cancellationToken); diff --git a/ModularPipelines.Examples/Program.cs b/ModularPipelines.Examples/Program.cs index fcf92eba1f..7a61efdd0e 100644 --- a/ModularPipelines.Examples/Program.cs +++ b/ModularPipelines.Examples/Program.cs @@ -2,7 +2,7 @@ using ModularPipelines.Examples.Modules; using ModularPipelines.Extensions; using ModularPipelines.Host; -using ModularPipelines.Requirements; +using ModularPipelines.Options; var modules = await PipelineHostBuilder.Create() .ConfigureAppConfiguration((context, builder) => @@ -15,17 +15,26 @@ }) .ConfigurePipelineOptions((context, options) => { + options.ExecutionMode = ExecutionMode.StopOnFirstException; options.IgnoreCategories = new[] { "Ignore" }; }) .ConfigureServices((context, collection) => { collection.AddModule() - .AddModule() - .AddModule() - .AddModule() - .AddModule() - .AddModule() - .AddModule() - .AddRequirement(); + .AddModule() + .AddModule() + .AddModule() + .AddModule() + // .AddModule() + // .AddModule() + // .AddModule() + // .AddModule() + // .AddModule() + .AddModule() + .AddModule(); + // .AddModule() + // .AddModule(); + //.AddModule() + //.AddRequirement(); }) .ExecutePipelineAsync(); \ No newline at end of file diff --git a/ModularPipelines.Git/Enums/GitStageOption.cs b/ModularPipelines.Git/Enums/GitStageOption.cs new file mode 100644 index 0000000000..61be5c7388 --- /dev/null +++ b/ModularPipelines.Git/Enums/GitStageOption.cs @@ -0,0 +1,8 @@ +namespace ModularPipelines.Git.Enums; + +public enum GitStageOption +{ + All, + CurrentWorkingDirectory, + ModifiedOnly +} \ No newline at end of file diff --git a/ModularPipelines.Git/Enums/GitStageOptionExtensions.cs b/ModularPipelines.Git/Enums/GitStageOptionExtensions.cs new file mode 100644 index 0000000000..8f6d59246f --- /dev/null +++ b/ModularPipelines.Git/Enums/GitStageOptionExtensions.cs @@ -0,0 +1,23 @@ +namespace ModularPipelines.Git.Enums; + +public static class GitStageOptionExtensions +{ + public static string GetCommandLineSwitch(this GitStageOption? option) + { + if (option is null) + { + return "-A"; + } + + return option.Value.GetCommandLineSwitch(); + } + + public static string GetCommandLineSwitch(this GitStageOption option) => + option switch + { + GitStageOption.All => "-A", + GitStageOption.ModifiedOnly => "-u", + GitStageOption.CurrentWorkingDirectory => ".", + _ => throw new ArgumentOutOfRangeException(nameof(option)) + }; +} \ No newline at end of file diff --git a/ModularPipelines.Git/Extensions/GitExtensions.cs b/ModularPipelines.Git/Extensions/GitExtensions.cs index 7dea7a5f2a..7204c8f086 100644 --- a/ModularPipelines.Git/Extensions/GitExtensions.cs +++ b/ModularPipelines.Git/Extensions/GitExtensions.cs @@ -1,18 +1,31 @@ -using Microsoft.Extensions.DependencyInjection; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using ModularPipelines.Command.Extensions; using ModularPipelines.Context; +using ModularPipelines.Engine; namespace ModularPipelines.Git.Extensions; public static class GitExtensions { +#pragma warning disable CA2255 + [ModuleInitializer] +#pragma warning restore CA2255 + public static void RegisterGitContext() + { + ServiceContextRegistry.RegisterContext(collection => RegisterGitContext(collection)); + } + public static IServiceCollection RegisterGitContext(this IServiceCollection services) { - services.RegisterCommandContext(); services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); return services; } - - public static IGit Git(this IModuleContext context) => context.Get()!; + + public static IGit Git(this IModuleContext context) => context.ServiceProvider.GetRequiredService(); } \ No newline at end of file diff --git a/ModularPipelines.Git/Git.cs b/ModularPipelines.Git/Git.cs index 763dc85e74..1cc47155c7 100644 --- a/ModularPipelines.Git/Git.cs +++ b/ModularPipelines.Git/Git.cs @@ -1,76 +1,13 @@ -using CliWrap.Buffered; -using ModularPipelines.Command.Extensions; -using ModularPipelines.Command.Options; -using ModularPipelines.Context; -using ModularPipelines.Git.Options; - namespace ModularPipelines.Git; public class Git : IGit { - public IModuleContext Context { get; } - - public Git(IModuleContext context) - { - Context = context; - } - - public Task Checkout(GitCheckoutOptions options) - { - return Run(options); - } - - public Task Version(GitOptions? options = null) - { - var opts = new GitArgumentOptions(new[] {"--version"}) - { - EnvironmentVariables = options?.EnvironmentVariables, - WorkingDirectory = options?.WorkingDirectory - }; - - return Run(opts); - } - - public Task Fetch(GitOptions? options = null) + public Git(IGitOperations operations, IGitInformation information) { - var opts = new GitArgumentOptions(new[] {"fetch"}) - { - EnvironmentVariables = options?.EnvironmentVariables, - WorkingDirectory = options?.WorkingDirectory - }; - - return Run(opts); - } - - public Task Pull(GitOptions? options = null) - { - var opts = new GitArgumentOptions(new[] {"pull"}) - { - EnvironmentVariables = options?.EnvironmentVariables, - WorkingDirectory = options?.WorkingDirectory - }; - - return Run(opts); + Operations = operations; + Information = information; } - public Task Push(GitOptions? options = null) - { - var opts = new GitArgumentOptions(new[] {"push"}) - { - EnvironmentVariables = options?.EnvironmentVariables, - WorkingDirectory = options?.WorkingDirectory - }; - - return Run(opts); - } - - private Task Run(GitArgumentOptions options) - { - return Context.Command().UsingCommandLineTool(new CommandLineToolOptions("git") - { - Arguments = options.Arguments, - EnvironmentVariables = options.EnvironmentVariables, - WorkingDirectory = options.WorkingDirectory - }); - } + public IGitOperations Operations { get; } + public IGitInformation Information { get; } } \ No newline at end of file diff --git a/ModularPipelines.Git/GitCommandRunner.cs b/ModularPipelines.Git/GitCommandRunner.cs new file mode 100644 index 0000000000..eabe143866 --- /dev/null +++ b/ModularPipelines.Git/GitCommandRunner.cs @@ -0,0 +1,40 @@ +using ModularPipelines.Context; +using ModularPipelines.Extensions; +using ModularPipelines.Options; + +namespace ModularPipelines.Git; + +internal class GitCommandRunner +{ + private readonly IModuleContext _context; + + public GitCommandRunner(IModuleContext context) + { + _context = context; + } + + public async Task RunCommands(CommandEnvironmentOptions? commandEnvironmentOptions, params string?[] commands) + { + commandEnvironmentOptions ??= new CommandEnvironmentOptions + { + LogInput = false, + LogOutput = false + }; + + var commandResult = await _context.Command.UsingCommandLineTool(commandEnvironmentOptions.ToCommandLineToolOptions("git", commands.OfType().ToArray())); + + return commandResult.StandardOutput.Trim(); + } + + public async Task RunCommandsOrNull(CommandEnvironmentOptions? commandEnvironmentOptions, params string?[] commands) + { + try + { + return await RunCommands(commandEnvironmentOptions, commands); + } + catch + { + return null; + } + } +} \ No newline at end of file diff --git a/ModularPipelines.Git/GitCommitMapper.cs b/ModularPipelines.Git/GitCommitMapper.cs new file mode 100644 index 0000000000..8f9bf9a673 --- /dev/null +++ b/ModularPipelines.Git/GitCommitMapper.cs @@ -0,0 +1,37 @@ +using ModularPipelines.Git.Models; + +namespace ModularPipelines.Git; + +internal class GitCommitMapper : IGitCommitMapper +{ + public GitCommit Map(string commandLineOutput) + { + var lines = commandLineOutput.Split(GitConstants.DotNetLineSeparator, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList(); + + return new GitCommit + { + Author = new GitAuthor + { + Name = lines[0], + Email = lines[1], + Date = DateTime.Parse(lines[2]) + }, + Committer = new GitAuthor + { + Name = lines[3], + Email = lines[4], + Date = DateTime.Parse(lines[5]) + }, + Hash = new GitHash + { + Long = lines[6], + Short = lines[7], + }, + Message = new GitMessage + { + Subject = lines[8], + Body = lines[9] + } + }; + } +} \ No newline at end of file diff --git a/ModularPipelines.Git/GitConstants.cs b/ModularPipelines.Git/GitConstants.cs new file mode 100644 index 0000000000..5c2af3f301 --- /dev/null +++ b/ModularPipelines.Git/GitConstants.cs @@ -0,0 +1,7 @@ +namespace ModularPipelines.Git; + +internal static class GitConstants +{ + public const string GitEscapedLineSeparator = "%%%n%%"; + public const string DotNetLineSeparator = "%\n%"; +} \ No newline at end of file diff --git a/ModularPipelines.Git/GitInformation.cs b/ModularPipelines.Git/GitInformation.cs new file mode 100644 index 0000000000..781c2c1af2 --- /dev/null +++ b/ModularPipelines.Git/GitInformation.cs @@ -0,0 +1,63 @@ +using System.Runtime.CompilerServices; +using ModularPipelines.Context; +using ModularPipelines.Git.Models; +using ModularPipelines.Git.Options; + +namespace ModularPipelines.Git; + +internal class GitInformation : IGitInformation +{ + private readonly StaticGitInformation _staticGitInformation; + private readonly GitCommandRunner _gitCommandRunner; + private readonly IGitCommitMapper _gitCommitMapper; + private readonly IModuleContext _context; + + public GitInformation(StaticGitInformation staticGitInformation, + GitCommandRunner gitCommandRunner, + IGitCommitMapper gitCommitMapper, + IModuleContext context) + { + _staticGitInformation = staticGitInformation; + _gitCommandRunner = gitCommandRunner; + _gitCommitMapper = gitCommitMapper; + _context = context; + } + + public string BranchName => _staticGitInformation.BranchName; + public string DefaultBranchName => _staticGitInformation.DefaultBranchName; + + public string Tag => _staticGitInformation.Tag; + + public GitCommit? PreviousCommit => _staticGitInformation.PreviousCommit; + + public int CommitsOnBranch => _staticGitInformation.CommitsOnBranch; + public DateTimeOffset LastCommitDateTime => _staticGitInformation.LastCommitDateTime; + + public string LastCommitSha => _staticGitInformation.LastCommitSha; + + public string LastCommitShortSha => _staticGitInformation.LastCommitShortSha; + + + public IAsyncEnumerable Commits(GitOptions? options = null, CancellationToken cancellationToken = default) + { + return Commits(null, options, cancellationToken); + } + + public async IAsyncEnumerable Commits(string? branch, GitOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var index = 0; + while (true) + { + var output = await _gitCommandRunner.RunCommandsOrNull(options, "log", branch, $"--skip={index-1}", "-1", $"--format='%aN {GitConstants.GitEscapedLineSeparator} %aE {GitConstants.GitEscapedLineSeparator} %aI {GitConstants.GitEscapedLineSeparator} %cN {GitConstants.GitEscapedLineSeparator} %cE {GitConstants.GitEscapedLineSeparator} %cI {GitConstants.GitEscapedLineSeparator} %H {GitConstants.GitEscapedLineSeparator} %h {GitConstants.GitEscapedLineSeparator} %s {GitConstants.GitEscapedLineSeparator} %B'"); + + index++; + + if (string.IsNullOrWhiteSpace(output) || cancellationToken.IsCancellationRequested) + { + yield break; + } + + yield return _gitCommitMapper.Map(output); + } + } +} \ No newline at end of file diff --git a/ModularPipelines.Git/GitOperations.cs b/ModularPipelines.Git/GitOperations.cs new file mode 100644 index 0000000000..f352d1f7b2 --- /dev/null +++ b/ModularPipelines.Git/GitOperations.cs @@ -0,0 +1,74 @@ +using CliWrap.Buffered; +using ModularPipelines.Context; +using ModularPipelines.Extensions; +using ModularPipelines.Git.Enums; +using ModularPipelines.Git.Options; +using ModularPipelines.Options; + +namespace ModularPipelines.Git; + +public class GitOperations : IGitOperations +{ + private readonly IModuleContext _context; + + public GitOperations(IModuleContext context) + { + _context = context; + } + + public Task Checkout(GitCheckoutOptions options, CancellationToken cancellationToken = default) + { + return CustomCommand(options, cancellationToken); + } + + public Task Version(GitOptions? options = null, CancellationToken cancellationToken = default) + { + return CustomCommand(ToGitCommandOptions(options, new []{"--version"}), cancellationToken); + } + + public Task Fetch(GitOptions? options = null, CancellationToken cancellationToken = default) + { + return CustomCommand(ToGitCommandOptions(options, new []{"fetch"}), cancellationToken); + } + + public Task Pull(GitOptions? options = null, CancellationToken cancellationToken = default) + { + return CustomCommand(ToGitCommandOptions(options, new []{"pull"}), cancellationToken); + } + + public Task Push(GitOptions? options = null, CancellationToken cancellationToken = default) + { + return CustomCommand(ToGitCommandOptions(options, new []{"push"}), cancellationToken); + } + + public Task Stage(GitStageOptions? options = null, CancellationToken cancellationToken = default) + { + var stageOption = options?.GitStageOption ?? GitStageOption.All; + + return CustomCommand(ToGitCommandOptions(options, new []{ "add", stageOption.GetCommandLineSwitch() }), cancellationToken); + } + + public Task Commit(string message, GitOptions? options = null, CancellationToken cancellationToken = default) + { + return CustomCommand(ToGitCommandOptions(options, new []{ "commit", "-m", message }), cancellationToken); + } + + public Task CustomCommand(GitCommandOptions options, CancellationToken cancellationToken) + { + return _context.Command.UsingCommandLineTool(options.ToCommandLineToolOptions("git", options.Arguments), cancellationToken); + } + + private GitCommandOptions ToGitCommandOptions(CommandEnvironmentOptions? options, IEnumerable arguments) + { + options ??= new CommandEnvironmentOptions(); + + return new GitCommandOptions(arguments) + { + WorkingDirectory = options.WorkingDirectory, + EnvironmentVariables = options.EnvironmentVariables, + Credentials = options.Credentials, + LogInput = options.LogInput, + LogOutput = options.LogOutput + }; + } +} \ No newline at end of file diff --git a/ModularPipelines.Git/IGit.cs b/ModularPipelines.Git/IGit.cs index 298b050468..98a65fb161 100644 --- a/ModularPipelines.Git/IGit.cs +++ b/ModularPipelines.Git/IGit.cs @@ -1,14 +1,7 @@ -using CliWrap.Buffered; -using ModularPipelines.Git.Options; - -namespace ModularPipelines.Git; +namespace ModularPipelines.Git; public interface IGit { - Task Checkout(GitCheckoutOptions options); - Task Version(GitOptions? options = null); - Task Fetch(GitOptions? options = null); - Task Pull(GitOptions? options = null); - Task Push(GitOptions? options = null); - + IGitOperations Operations { get; } + IGitInformation Information { get; } } \ No newline at end of file diff --git a/ModularPipelines.Git/IGitCommitMapper.cs b/ModularPipelines.Git/IGitCommitMapper.cs new file mode 100644 index 0000000000..487b1c62d9 --- /dev/null +++ b/ModularPipelines.Git/IGitCommitMapper.cs @@ -0,0 +1,8 @@ +using ModularPipelines.Git.Models; + +namespace ModularPipelines.Git; + +internal interface IGitCommitMapper +{ + GitCommit Map(string commandLineOutput); +} \ No newline at end of file diff --git a/ModularPipelines.Git/IGitInformation.cs b/ModularPipelines.Git/IGitInformation.cs new file mode 100644 index 0000000000..2a73aac60a --- /dev/null +++ b/ModularPipelines.Git/IGitInformation.cs @@ -0,0 +1,19 @@ +using ModularPipelines.Git.Models; +using ModularPipelines.Git.Options; + +namespace ModularPipelines.Git; + +public interface IGitInformation +{ + public string? BranchName { get; } + public string? DefaultBranchName { get; } + public string? LastCommitSha { get; } + public string? LastCommitShortSha { get; } + + public string? Tag { get; } + public int CommitsOnBranch { get; } + public DateTimeOffset LastCommitDateTime { get; } + public GitCommit? PreviousCommit { get; } + IAsyncEnumerable Commits(GitOptions? options = null, CancellationToken cancellationToken = default); + IAsyncEnumerable Commits(string branch, GitOptions? options = null, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/ModularPipelines.Git/IGitOperations.cs b/ModularPipelines.Git/IGitOperations.cs new file mode 100644 index 0000000000..0d45a0804b --- /dev/null +++ b/ModularPipelines.Git/IGitOperations.cs @@ -0,0 +1,16 @@ +using CliWrap.Buffered; +using ModularPipelines.Git.Options; + +namespace ModularPipelines.Git; + +public interface IGitOperations +{ + Task Checkout(GitCheckoutOptions options, CancellationToken cancellationToken = default); + Task Version(GitOptions? options = null, CancellationToken cancellationToken = default); + Task Fetch(GitOptions? options = null, CancellationToken cancellationToken = default); + Task Pull(GitOptions? options = null, CancellationToken cancellationToken = default); + Task Push(GitOptions? options = null, CancellationToken cancellationToken = default); + Task Stage(GitStageOptions? options = null, CancellationToken cancellationToken = default); + Task Commit(string message, GitOptions? options = null, CancellationToken cancellationToken = default); + Task CustomCommand(GitCommandOptions options, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/ModularPipelines.Git/Models/GitAuthor.cs b/ModularPipelines.Git/Models/GitAuthor.cs new file mode 100644 index 0000000000..2c55b660ae --- /dev/null +++ b/ModularPipelines.Git/Models/GitAuthor.cs @@ -0,0 +1,8 @@ +namespace ModularPipelines.Git.Models; + +public record GitAuthor +{ + public string Name { get; set; } + public string Email { get; set; } + public DateTime Date { get; set; } +} \ No newline at end of file diff --git a/ModularPipelines.Git/Models/GitCommit.cs b/ModularPipelines.Git/Models/GitCommit.cs new file mode 100644 index 0000000000..807b1924ae --- /dev/null +++ b/ModularPipelines.Git/Models/GitCommit.cs @@ -0,0 +1,9 @@ +namespace ModularPipelines.Git.Models; + +public record GitCommit +{ + public GitHash Hash { get; set; } + public GitAuthor Author { get; set; } + public GitAuthor Committer { get; set; } + public GitMessage Message { get; set; } +} \ No newline at end of file diff --git a/ModularPipelines.Git/Models/GitFileStatus.cs b/ModularPipelines.Git/Models/GitFileStatus.cs new file mode 100644 index 0000000000..3e691fc2d5 --- /dev/null +++ b/ModularPipelines.Git/Models/GitFileStatus.cs @@ -0,0 +1,7 @@ +namespace ModularPipelines.Git.Models; + +public record GitFileStatus +{ + public string Status { get; set; } + public string File { get; set; } +} \ No newline at end of file diff --git a/ModularPipelines.Git/Models/GitHash.cs b/ModularPipelines.Git/Models/GitHash.cs new file mode 100644 index 0000000000..880b29ed8f --- /dev/null +++ b/ModularPipelines.Git/Models/GitHash.cs @@ -0,0 +1,7 @@ +namespace ModularPipelines.Git.Models; + +public record GitHash +{ + public string Long { get; set; } + public string Short { get; set; } +} \ No newline at end of file diff --git a/ModularPipelines.Git/Models/GitMessage.cs b/ModularPipelines.Git/Models/GitMessage.cs new file mode 100644 index 0000000000..7104b50bf2 --- /dev/null +++ b/ModularPipelines.Git/Models/GitMessage.cs @@ -0,0 +1,7 @@ +namespace ModularPipelines.Git.Models; + +public record GitMessage +{ + public string Subject { get; set; } + public string Body { get; set; } +} \ No newline at end of file diff --git a/ModularPipelines.Git/ModularPipelines.Git.csproj b/ModularPipelines.Git/ModularPipelines.Git.csproj index 29eda92bbe..5b7566affd 100644 --- a/ModularPipelines.Git/ModularPipelines.Git.csproj +++ b/ModularPipelines.Git/ModularPipelines.Git.csproj @@ -7,8 +7,11 @@ - + + + + diff --git a/ModularPipelines.Git/Options/GitArgumentOptions.cs b/ModularPipelines.Git/Options/GitArgumentOptions.cs deleted file mode 100644 index 1797d2637b..0000000000 --- a/ModularPipelines.Git/Options/GitArgumentOptions.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ModularPipelines.Git.Options; - -public record GitArgumentOptions(IEnumerable Arguments) -{ - public IDictionary? EnvironmentVariables { get; init; } - public string? WorkingDirectory { get; init; } -} \ No newline at end of file diff --git a/ModularPipelines.Git/Options/GitCheckoutOptions.cs b/ModularPipelines.Git/Options/GitCheckoutOptions.cs index 373cccfe3b..1b77c3b6e3 100644 --- a/ModularPipelines.Git/Options/GitCheckoutOptions.cs +++ b/ModularPipelines.Git/Options/GitCheckoutOptions.cs @@ -1,3 +1,3 @@ namespace ModularPipelines.Git.Options; -public record GitCheckoutOptions(string BranchName) : GitArgumentOptions(new [] { "checkout", BranchName }); \ No newline at end of file +public record GitCheckoutOptions(string BranchName) : GitCommandOptions(new [] { "checkout", BranchName }); \ No newline at end of file diff --git a/ModularPipelines.Git/Options/GitCommandOptions.cs b/ModularPipelines.Git/Options/GitCommandOptions.cs new file mode 100644 index 0000000000..89386fc001 --- /dev/null +++ b/ModularPipelines.Git/Options/GitCommandOptions.cs @@ -0,0 +1,7 @@ +using ModularPipelines.Options; + +namespace ModularPipelines.Git.Options; + +public record GitCommandOptions(IEnumerable Arguments) : CommandEnvironmentOptions +{ +} \ No newline at end of file diff --git a/ModularPipelines.Git/Options/GitOptions.cs b/ModularPipelines.Git/Options/GitOptions.cs index ab17cfb529..52045423ff 100644 --- a/ModularPipelines.Git/Options/GitOptions.cs +++ b/ModularPipelines.Git/Options/GitOptions.cs @@ -1,7 +1,5 @@ -namespace ModularPipelines.Git.Options; +using ModularPipelines.Options; -public record GitOptions -{ - public IDictionary? EnvironmentVariables { get; init; } - public string? WorkingDirectory { get; init; } -} \ No newline at end of file +namespace ModularPipelines.Git.Options; + +public record GitOptions : CommandEnvironmentOptions; \ No newline at end of file diff --git a/ModularPipelines.Git/Options/GitStageOptions.cs b/ModularPipelines.Git/Options/GitStageOptions.cs new file mode 100644 index 0000000000..9721401502 --- /dev/null +++ b/ModularPipelines.Git/Options/GitStageOptions.cs @@ -0,0 +1,6 @@ +using ModularPipelines.Git.Enums; +using ModularPipelines.Options; + +namespace ModularPipelines.Git.Options; + +public record GitStageOptions(GitStageOption GitStageOption) : CommandEnvironmentOptions; \ No newline at end of file diff --git a/ModularPipelines.Git/StaticGitInformation.cs b/ModularPipelines.Git/StaticGitInformation.cs new file mode 100644 index 0000000000..26b8dd4b80 --- /dev/null +++ b/ModularPipelines.Git/StaticGitInformation.cs @@ -0,0 +1,78 @@ +using System.Runtime.CompilerServices; +using ModularPipelines.Git.Models; +using ModularPipelines.Git.Options; +using TomLonghurst.Microsoft.Extensions.DependencyInjection.ServiceInitialization; + +namespace ModularPipelines.Git; + +internal class StaticGitInformation : IGitInformation, IInitializer +{ + private readonly GitCommandRunner _gitCommandRunner; + private readonly IGitCommitMapper _gitCommitMapper; + + public StaticGitInformation(GitCommandRunner gitCommandRunner, IGitCommitMapper gitCommitMapper) + { + _gitCommandRunner = gitCommandRunner; + _gitCommitMapper = gitCommitMapper; + } + + public async Task InitializeAsync() + { + try + { + await _gitCommandRunner.RunCommands(null, "version"); + } + catch (Exception e) + { + throw new Exception("Error detecting Git repository", e); + } + + BranchName = await _gitCommandRunner.RunCommandsOrNull(null, "rev-parse", "--abbrev-ref", "HEAD"); + DefaultBranchName = (await _gitCommandRunner.RunCommandsOrNull(null, "rev-parse", "--abbrev-ref", "origin/HEAD"))?.Replace("origin/", string.Empty); + LastCommitSha = await _gitCommandRunner.RunCommandsOrNull(null, "rev-parse", "HEAD"); + LastCommitShortSha = await _gitCommandRunner.RunCommandsOrNull(null, "rev-parse", "--short", "HEAD"); + Tag = await _gitCommandRunner.RunCommandsOrNull(null, "describe", "--tags"); + CommitsOnBranch = int.Parse(await _gitCommandRunner.RunCommandsOrNull(null, "rev-list", "HEAD", "--count") ?? "0"); + LastCommitDateTime = DateTimeOffset.FromUnixTimeSeconds(long.Parse(await _gitCommandRunner.RunCommandsOrNull(null, "log", "-1", "--format=%at") ?? "0")); + PreviousCommit = await LastCommits(1).FirstOrDefaultAsync(); + } + + public async IAsyncEnumerable LastCommits(int count, GitOptions? gitOptions = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + for (var i = 0; i < count; i++) + { + var output = await _gitCommandRunner.RunCommandsOrNull(gitOptions, "log", $"--skip={i-1}", "-1", $"--format='%aN {GitConstants.GitEscapedLineSeparator} %aE {GitConstants.GitEscapedLineSeparator} %aI {GitConstants.GitEscapedLineSeparator} %cN {GitConstants.GitEscapedLineSeparator} %cE {GitConstants.GitEscapedLineSeparator} %cI {GitConstants.GitEscapedLineSeparator} %H {GitConstants.GitEscapedLineSeparator} %h {GitConstants.GitEscapedLineSeparator} %s {GitConstants.GitEscapedLineSeparator} %b'"); + + if (string.IsNullOrWhiteSpace(output) || cancellationToken.IsCancellationRequested) + { + yield break; + } + + yield return _gitCommitMapper.Map(output); + } + } + + public GitCommit? PreviousCommit { get; private set; } + + public string? BranchName { get; private set; } = null!; + public string? DefaultBranchName { get; private set; } = null!; + + public string? Tag { get; private set; } = null!; + + public int CommitsOnBranch { get; private set; } + public DateTimeOffset LastCommitDateTime { get; private set; } + + public string? LastCommitSha { get; private set; } = null!; + + public string? LastCommitShortSha { get; private set; } = null!; + + public IAsyncEnumerable Commits(GitOptions? options = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable Commits(string branch, GitOptions? options = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/ModularPipelines.Installer/Extensions/InstallerExtensions.cs b/ModularPipelines.Installer/Extensions/InstallerExtensions.cs deleted file mode 100644 index bc0797fa98..0000000000 --- a/ModularPipelines.Installer/Extensions/InstallerExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using ModularPipelines.Command.Extensions; -using ModularPipelines.Context; - -namespace ModularPipelines.Installer.Extensions; - -public static class InstallerExtensions -{ - public static IServiceCollection RegisterInstallerContext(this IServiceCollection services) - { - services.RegisterCommandContext(); - - services.TryAddSingleton(); - return services; - } - - public static IInstaller Installer(this IModuleContext context) => context.Get()!; -} \ No newline at end of file diff --git a/ModularPipelines.Installer/IInstaller.cs b/ModularPipelines.Installer/IInstaller.cs deleted file mode 100644 index 8b7fb234b9..0000000000 --- a/ModularPipelines.Installer/IInstaller.cs +++ /dev/null @@ -1,11 +0,0 @@ -using CliWrap.Buffered; -using ModularPipelines.Installer.Options; - -namespace ModularPipelines.Installer; - -public interface IInstaller -{ - Task InstallFromFile(InstallerOptions options, CancellationToken cancellationToken = default); - Task InstallFromWeb(WebInstallerOptions options, - CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/ModularPipelines.Installer/Installer.cs b/ModularPipelines.Installer/Installer.cs deleted file mode 100644 index cf8b7bf79d..0000000000 --- a/ModularPipelines.Installer/Installer.cs +++ /dev/null @@ -1,56 +0,0 @@ -using CliWrap.Buffered; -using ModularPipelines.Command.Extensions; -using ModularPipelines.Command.Options; -using ModularPipelines.Context; -using ModularPipelines.Installer.Options; - -namespace ModularPipelines.Installer; - -public class Installer : IInstaller -{ - public IModuleContext Context { get; } - - public Installer(IModuleContext context) - { - Context = context; - } - - public Task InstallFromFile(InstallerOptions options, - CancellationToken cancellationToken = default) - { - return Context.Command().UsingCommandLineTool(new CommandLineToolOptions(options.Path) - { - Arguments = options.Arguments ?? Array.Empty() - }, cancellationToken); - } - - public async Task InstallFromWeb(WebInstallerOptions options, - CancellationToken cancellationToken = default) - { - var httpClient = Context.Get()!; - - await using var stream = await httpClient.GetStreamAsync(options.DownloadUri, cancellationToken); - - var filePathToSave = Path.GetTempFileName() + Guid.NewGuid() + GetExtension(options.DownloadUri.AbsoluteUri); - - await using (var newFile = File.Create(filePathToSave)) - { - await stream.CopyToAsync(newFile, cancellationToken); - } - - return await InstallFromFile(new InstallerOptions(filePathToSave) - { - Arguments = options.Arguments - }, cancellationToken); - } - - private string GetExtension(string downloadUri) - { - if (Path.HasExtension(downloadUri)) - { - return Path.GetExtension(downloadUri); - } - - return string.Empty; - } -} \ No newline at end of file diff --git a/ModularPipelines.MicrosoftTeams/Extensions/MicrosoftTeamsExtensions.cs b/ModularPipelines.MicrosoftTeams/Extensions/MicrosoftTeamsExtensions.cs index de656ac6fa..cc6fc585c5 100644 --- a/ModularPipelines.MicrosoftTeams/Extensions/MicrosoftTeamsExtensions.cs +++ b/ModularPipelines.MicrosoftTeams/Extensions/MicrosoftTeamsExtensions.cs @@ -1,16 +1,26 @@ -using Microsoft.Extensions.DependencyInjection; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using ModularPipelines.Context; +using ModularPipelines.Engine; namespace ModularPipelines.MicrosoftTeams.Extensions; public static class MicrosoftTeamsExtensions { +#pragma warning disable CA2255 + [ModuleInitializer] +#pragma warning restore CA2255 + public static void RegisterMicrosoftTeamsContext() + { + ServiceContextRegistry.RegisterContext(collection => RegisterMicrosoftTeamsContext(collection)); + } + public static IServiceCollection RegisterMicrosoftTeamsContext(this IServiceCollection services) { services.TryAddSingleton(); return services; } - - public static IMicrosoftTeams MicrosoftTeams(this IModuleContext context) => context.Get()!; + + public static IMicrosoftTeams MicrosoftTeams(this IModuleContext context) => context.ServiceProvider.GetRequiredService(); } \ No newline at end of file diff --git a/ModularPipelines.Node/Extensions/NodeExtensions.cs b/ModularPipelines.Node/Extensions/NodeExtensions.cs new file mode 100644 index 0000000000..395c5c0fab --- /dev/null +++ b/ModularPipelines.Node/Extensions/NodeExtensions.cs @@ -0,0 +1,28 @@ +using System.Runtime.CompilerServices; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using ModularPipelines.Context; +using ModularPipelines.Engine; + +namespace ModularPipelines.Node.Extensions; + +public static class NodeExtensions +{ +#pragma warning disable CA2255 + [ModuleInitializer] +#pragma warning restore CA2255 + public static void RegisterNodeContext() + { + ServiceContextRegistry.RegisterContext(collection => RegisterNodeContext(collection)); + } + + public static IServiceCollection RegisterNodeContext(this IServiceCollection services) + { + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + return services; + } + + public static INode Node(this IModuleContext context) => context.ServiceProvider.GetRequiredService(); +} \ No newline at end of file diff --git a/ModularPipelines.Node/INode.cs b/ModularPipelines.Node/INode.cs new file mode 100644 index 0000000000..a8df755a17 --- /dev/null +++ b/ModularPipelines.Node/INode.cs @@ -0,0 +1,10 @@ +using CliWrap.Buffered; + +namespace ModularPipelines.Node; + +public interface INode +{ + Task Version(CancellationToken cancellationToken = default); + public INpm Npm { get; } + public INvm Nvm { get; } +} \ No newline at end of file diff --git a/ModularPipelines.Node/INpm.cs b/ModularPipelines.Node/INpm.cs new file mode 100644 index 0000000000..ecf72dfdda --- /dev/null +++ b/ModularPipelines.Node/INpm.cs @@ -0,0 +1,11 @@ +using CliWrap.Buffered; +using ModularPipelines.Node.Models; + +namespace ModularPipelines.Node; + +public interface INpm +{ + Task Install(NpmInstallOptions options, CancellationToken cancellationToken = default); + Task CleanInstall(NpmCleanInstallOptions options, CancellationToken cancellationToken = default); + Task Run(NpmRunOptions options, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/ModularPipelines.Node/INvm.cs b/ModularPipelines.Node/INvm.cs new file mode 100644 index 0000000000..14fdd175a4 --- /dev/null +++ b/ModularPipelines.Node/INvm.cs @@ -0,0 +1,8 @@ +using CliWrap.Buffered; + +namespace ModularPipelines.Node; + +public interface INvm +{ + Task Use(string version, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/ModularPipelines.Node/Models/NpmCleanInstallOptions.cs b/ModularPipelines.Node/Models/NpmCleanInstallOptions.cs new file mode 100644 index 0000000000..b463a27119 --- /dev/null +++ b/ModularPipelines.Node/Models/NpmCleanInstallOptions.cs @@ -0,0 +1,9 @@ +using ModularPipelines.Options; + +namespace ModularPipelines.Node.Models; + +public record NpmCleanInstallOptions : CommandEnvironmentOptions +{ + public string? InstallStrategy { get; init; } + public IEnumerable? Omit { get; init; } +} \ No newline at end of file diff --git a/ModularPipelines.Node/Models/NpmInstallOptions.cs b/ModularPipelines.Node/Models/NpmInstallOptions.cs new file mode 100644 index 0000000000..648d8c69a4 --- /dev/null +++ b/ModularPipelines.Node/Models/NpmInstallOptions.cs @@ -0,0 +1,11 @@ +using ModularPipelines.Options; + +namespace ModularPipelines.Node.Models; + +public record NpmInstallOptions : CommandEnvironmentOptions +{ + public string? Target { get; init; } + public bool Global { get; init; } + public bool DryRun { get; init; } + public bool Force { get; init; } +} \ No newline at end of file diff --git a/ModularPipelines.Node/Models/NpmRunOptions.cs b/ModularPipelines.Node/Models/NpmRunOptions.cs new file mode 100644 index 0000000000..b3c948a951 --- /dev/null +++ b/ModularPipelines.Node/Models/NpmRunOptions.cs @@ -0,0 +1,9 @@ +using ModularPipelines.Options; + +namespace ModularPipelines.Node.Models; + +public record NpmRunOptions : CommandEnvironmentOptions +{ + public string Target { get; init; } + public IEnumerable? Arguments { get; init; } +} \ No newline at end of file diff --git a/ModularPipelines.Node/ModularPipelines.Node.csproj b/ModularPipelines.Node/ModularPipelines.Node.csproj new file mode 100644 index 0000000000..4decd268f3 --- /dev/null +++ b/ModularPipelines.Node/ModularPipelines.Node.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/ModularPipelines.Node/Node.cs b/ModularPipelines.Node/Node.cs new file mode 100644 index 0000000000..0dbaf13160 --- /dev/null +++ b/ModularPipelines.Node/Node.cs @@ -0,0 +1,27 @@ +using CliWrap.Buffered; +using ModularPipelines.Context; +using ModularPipelines.Options; + +namespace ModularPipelines.Node; + +public class Node : INode +{ + private readonly IModuleContext _context; + public INpm Npm { get; } + public INvm Nvm { get; } + + public Node(INpm npm, INvm nvm, IModuleContext context) + { + _context = context; + Npm = npm; + Nvm = nvm; + } + + public Task Version(CancellationToken cancellationToken = default) + { + return _context.Command.UsingCommandLineTool(new CommandLineToolOptions("node") + { + Arguments = new []{ "-v" } + }, cancellationToken); + } +} \ No newline at end of file diff --git a/ModularPipelines.Node/Npm.cs b/ModularPipelines.Node/Npm.cs new file mode 100644 index 0000000000..a683404aa2 --- /dev/null +++ b/ModularPipelines.Node/Npm.cs @@ -0,0 +1,65 @@ +using CliWrap.Buffered; +using ModularPipelines.Context; +using ModularPipelines.Extensions; +using ModularPipelines.Node.Models; + +namespace ModularPipelines.Node; + +public class Npm : INpm +{ + private readonly IModuleContext _context; + + public Npm(IModuleContext context) + { + _context = context; + } + + public Task Install(NpmInstallOptions options, CancellationToken cancellationToken = default) + { + var arguments = new List { "install" }; + + arguments.AddNonNullOrEmpty(options.Target); + + if (options.Global) + { + arguments.Add("-g"); + } + + if (options.Force) + { + arguments.Add("--force"); + } + + if (options.DryRun) + { + arguments.Add("--dry-run"); + } + + return _context.Command.UsingCommandLineTool(options.ToCommandLineToolOptions("npm", arguments), cancellationToken); + } + + public Task CleanInstall(NpmCleanInstallOptions options, CancellationToken cancellationToken = default) + { + var arguments = new List { "ci" }; + + arguments.AddNonNullOrEmptyArgumentWithPrefix("--install-strategy=", options.InstallStrategy); + arguments.AddRangeNonNullOrEmptyArgumentWithPrefix("--omit=", options.Omit); + + return _context.Command.UsingCommandLineTool(options.ToCommandLineToolOptions("npm", arguments), cancellationToken); + } + + public Task Run(NpmRunOptions options, CancellationToken cancellationToken = default) + { + var arguments = new List { "run" }; + + arguments.AddNonNullOrEmpty(options.Target); + + if (options.Arguments?.Any() == true) + { + arguments.Add("--"); + arguments.AddRange(options.Arguments); + } + + return _context.Command.UsingCommandLineTool(options.ToCommandLineToolOptions("npm", arguments), cancellationToken); + } +} \ No newline at end of file diff --git a/ModularPipelines.Node/Nvm.cs b/ModularPipelines.Node/Nvm.cs new file mode 100644 index 0000000000..197d3f62cf --- /dev/null +++ b/ModularPipelines.Node/Nvm.cs @@ -0,0 +1,23 @@ +using CliWrap.Buffered; +using ModularPipelines.Context; +using ModularPipelines.Options; + +namespace ModularPipelines.Node; + +public class Nvm : INvm +{ + private readonly IModuleContext _context; + + public Nvm(IModuleContext context) + { + _context = context; + } + + public Task Use(string version, CancellationToken cancellationToken = default) + { + return _context.Command.UsingCommandLineTool(new CommandLineToolOptions("nvm") + { + Arguments = new []{ "use", version } + }, cancellationToken); + } +} \ No newline at end of file diff --git a/ModularPipelines.NuGet/Extensions/NuGetExtensions.cs b/ModularPipelines.NuGet/Extensions/NuGetExtensions.cs index 81c7e57690..523d1ebe14 100644 --- a/ModularPipelines.NuGet/Extensions/NuGetExtensions.cs +++ b/ModularPipelines.NuGet/Extensions/NuGetExtensions.cs @@ -1,21 +1,30 @@ -using Microsoft.Extensions.DependencyInjection; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using ModularPipelines.Command.Extensions; using ModularPipelines.Context; using ModularPipelines.DotNet.Extensions; +using ModularPipelines.Engine; namespace ModularPipelines.NuGet.Extensions; public static class NuGetExtensions { +#pragma warning disable CA2255 + [ModuleInitializer] +#pragma warning restore CA2255 + public static void RegisterNuGetContext() + { + ServiceContextRegistry.RegisterContext(collection => RegisterNuGetContext(collection)); + } + public static IServiceCollection RegisterNuGetContext(this IServiceCollection services) { - services.RegisterCommandContext() - .RegisterDotNetContext(); - + services.RegisterDotNetContext(); + services.TryAddSingleton(); + return services; } - - public static INuGet NuGet(this IModuleContext context) => context.Get()!; + + public static INuGet NuGet(this IModuleContext context) => (INuGet) context.ServiceProvider.GetRequiredService(); } \ No newline at end of file diff --git a/ModularPipelines.NuGet/INuGet.cs b/ModularPipelines.NuGet/INuGet.cs index 4fdd5083fd..c50819149c 100644 --- a/ModularPipelines.NuGet/INuGet.cs +++ b/ModularPipelines.NuGet/INuGet.cs @@ -5,7 +5,7 @@ namespace ModularPipelines.NuGet; public interface INuGet { - Task> UploadPackage(NuGetUploadOptions options); + Task> UploadPackages(NuGetUploadOptions options); Task AddSource(NuGetSourceOptions options); } \ No newline at end of file diff --git a/ModularPipelines.NuGet/NuGet.cs b/ModularPipelines.NuGet/NuGet.cs index c63fa682b3..cb04b8f79e 100644 --- a/ModularPipelines.NuGet/NuGet.cs +++ b/ModularPipelines.NuGet/NuGet.cs @@ -1,38 +1,39 @@ using CliWrap.Buffered; -using ModularPipelines.Command.Extensions; -using ModularPipelines.Command.Options; using ModularPipelines.Context; using ModularPipelines.DotNet.Extensions; using ModularPipelines.DotNet.Options; +using ModularPipelines.Extensions; using ModularPipelines.NuGet.Options; +using ModularPipelines.Options; namespace ModularPipelines.NuGet; public class NuGet : INuGet { - public IModuleContext Context { get; } + private readonly IModuleContext _context; public NuGet(IModuleContext context) { - Context = context; + _context = context; } - public async Task> UploadPackage(NuGetUploadOptions options) + public async Task> UploadPackages(NuGetUploadOptions options) { var results = new List(); foreach (var packagePath in options.PackagePaths) { - var commandResult = await Context.Command() + var arguments = new List + { + "nuget", "push", packagePath, "-n" + }; + + arguments.AddNonNullOrEmptyArgumentWithSwitch("-s", options.FeedUri.AbsoluteUri); + arguments.AddNonNullOrEmptyArgumentWithSwitch("-k", options.ApiKey); + + var commandResult = await _context.Command .UsingCommandLineTool(new CommandLineToolOptions("dotnet") { - Arguments = new [] - { - "nuget", "push", - packagePath, - "-s", options.FeedUri.AbsoluteUri, - "-k", options.ApiKey ?? string.Empty, - "-n" - } + Arguments = arguments }); results.Add(commandResult); @@ -49,21 +50,17 @@ public Task AddSource(NuGetSourceOptions options) "-n", options.Name }; - if (options.Username != null) - { - arguments.Add("--username"); - arguments.Add(options.Username); - } - - if (options.Password != null) - { - arguments.Add("--password"); - arguments.Add(options.Password); - } - - return Context.DotNet().CustomCommand(new DotNetCommandOptions + arguments.AddNonNullOrEmptyArgumentWithSwitch("--username", options.Username); + arguments.AddNonNullOrEmptyArgumentWithSwitch("--password", options.Password); + + return _context.DotNet().CustomCommand(new DotNetCommandOptions { - Command = arguments + Command = arguments, + EnvironmentVariables = options.EnvironmentVariables, + WorkingDirectory = options.WorkingDirectory, + Credentials = options.Credentials, + LogInput = options.LogInput, + LogOutput = options.LogOutput }); } } \ No newline at end of file diff --git a/ModularPipelines.NuGet/Options/NuGetSourceOptions.cs b/ModularPipelines.NuGet/Options/NuGetSourceOptions.cs index 575d12228d..864817a4ae 100644 --- a/ModularPipelines.NuGet/Options/NuGetSourceOptions.cs +++ b/ModularPipelines.NuGet/Options/NuGetSourceOptions.cs @@ -1,10 +1,12 @@ -namespace ModularPipelines.NuGet.Options; +using ModularPipelines.Options; + +namespace ModularPipelines.NuGet.Options; public record NuGetSourceOptions ( Uri FeedUri, string Name -) +) : CommandEnvironmentOptions { public string? Username { get; init; } public string? Password { get; init; } diff --git a/ModularPipelines.Powershell/Extensions/PowershellExtensions.cs b/ModularPipelines.Powershell/Extensions/PowershellExtensions.cs new file mode 100644 index 0000000000..92b3493f9e --- /dev/null +++ b/ModularPipelines.Powershell/Extensions/PowershellExtensions.cs @@ -0,0 +1,27 @@ +using System.Runtime.CompilerServices; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using ModularPipelines.Context; +using ModularPipelines.Engine; + +namespace ModularPipelines.Powershell.Extensions; + +public static class PowershellExtensions +{ +#pragma warning disable CA2255 + [ModuleInitializer] +#pragma warning restore CA2255 + public static void RegisterPowershellContext() + { + ServiceContextRegistry.RegisterContext(collection => RegisterPowershellContext(collection)); + } + + public static IServiceCollection RegisterPowershellContext(this IServiceCollection services) + { + services.TryAddSingleton(); + + return services; + } + + public static IPowershell Powershell(this IModuleContext context) => context.ServiceProvider.GetRequiredService(); +} \ No newline at end of file diff --git a/ModularPipelines.Powershell/IPowershell.cs b/ModularPipelines.Powershell/IPowershell.cs new file mode 100644 index 0000000000..8699ea80e4 --- /dev/null +++ b/ModularPipelines.Powershell/IPowershell.cs @@ -0,0 +1,10 @@ +using CliWrap.Buffered; +using ModularPipelines.Powershell.Models; + +namespace ModularPipelines.Powershell; + +public interface IPowershell +{ + Task Script(PowershellScriptOptions options, CancellationToken cancellationToken = default); + Task FromFile(PowershellFileOptions options, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/ModularPipelines.Powershell/Models/PowershellFileOptions.cs b/ModularPipelines.Powershell/Models/PowershellFileOptions.cs new file mode 100644 index 0000000000..8411b24509 --- /dev/null +++ b/ModularPipelines.Powershell/Models/PowershellFileOptions.cs @@ -0,0 +1,8 @@ +using ModularPipelines.Options; + +namespace ModularPipelines.Powershell.Models; + +public record PowershellFileOptions(string FilePath) : CommandEnvironmentOptions +{ + public IEnumerable? Arguments { get; init; } +} \ No newline at end of file diff --git a/ModularPipelines.Powershell/Models/PowershellScriptOptions.cs b/ModularPipelines.Powershell/Models/PowershellScriptOptions.cs new file mode 100644 index 0000000000..4e531d1f1e --- /dev/null +++ b/ModularPipelines.Powershell/Models/PowershellScriptOptions.cs @@ -0,0 +1,8 @@ +using ModularPipelines.Options; + +namespace ModularPipelines.Powershell.Models; + +public record PowershellScriptOptions(string Script) : CommandEnvironmentOptions +{ + public IEnumerable? Arguments { get; init; } +} \ No newline at end of file diff --git a/ModularPipelines.Powershell/ModularPipelines.Powershell.csproj b/ModularPipelines.Powershell/ModularPipelines.Powershell.csproj new file mode 100644 index 0000000000..4decd268f3 --- /dev/null +++ b/ModularPipelines.Powershell/ModularPipelines.Powershell.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/ModularPipelines.Powershell/Powershell.cs b/ModularPipelines.Powershell/Powershell.cs new file mode 100644 index 0000000000..b7d86669bb --- /dev/null +++ b/ModularPipelines.Powershell/Powershell.cs @@ -0,0 +1,34 @@ +using CliWrap.Buffered; +using ModularPipelines.Context; +using ModularPipelines.Extensions; +using ModularPipelines.Powershell.Models; + +namespace ModularPipelines.Powershell; + +public class Powershell : IPowershell +{ + private readonly IModuleContext _context; + + public Powershell(IModuleContext context) + { + _context = context; + } + + public Task Script(PowershellScriptOptions options, CancellationToken cancellationToken = default) + { + var arguments = new List { "-Command", options.Script }; + + arguments.AddRangeNonNullOrEmpty(options.Arguments); + + return _context.Command.UsingCommandLineTool(options.ToCommandLineToolOptions("pwsh", arguments), cancellationToken); + } + + public Task FromFile(PowershellFileOptions options, CancellationToken cancellationToken = default) + { + var arguments = new List { "-File", options.FilePath }; + + arguments.AddRangeNonNullOrEmpty(options.Arguments); + + return _context.Command.UsingCommandLineTool(options.ToCommandLineToolOptions("pwsh", arguments), cancellationToken); + } +} \ No newline at end of file diff --git a/ModularPipelines.UnitTests/AsyncDisposableModuleTests.cs b/ModularPipelines.UnitTests/AsyncDisposableModuleTests.cs index aa9397b1fe..12c471137d 100644 --- a/ModularPipelines.UnitTests/AsyncDisposableModuleTests.cs +++ b/ModularPipelines.UnitTests/AsyncDisposableModuleTests.cs @@ -24,11 +24,8 @@ public async Task SuccessfullyDisposed() public class AsyncDisposableModule : Module, IAsyncDisposable { public bool IsDisposed { get; private set; } - public AsyncDisposableModule(IModuleContext context) : base(context) - { - } - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { await Task.Delay(100, cancellationToken); return null; diff --git a/ModularPipelines.UnitTests/Data/Lorem.txt b/ModularPipelines.UnitTests/Data/Lorem.txt new file mode 100644 index 0000000000..27b1c3b6df --- /dev/null +++ b/ModularPipelines.UnitTests/Data/Lorem.txt @@ -0,0 +1,49 @@ +Vel odio ipsum amet ea clita quis ipsum sed sed erat. Sit elitr delenit justo in ut dolor dolor option tincidunt at lorem sea aliquyam duo wisi est. Lorem vulputate magna. Dolor nihil quis ipsum tincidunt duo. Ut et eu nonumy consetetur ea tincidunt eirmod sed blandit rebum aliquyam voluptua sit vero dolor aliquam eirmod. Tempor lorem quod aliquyam invidunt esse eum eos erat dolores stet consetetur diam aliquyam vero facilisis nulla sit et. Ea voluptua nibh diam magna. Eros te dolore diam amet tempor gubergren hendrerit nostrud justo sea tempor mazim facer feugiat sadipscing rebum. Et exerci invidunt. Accusam duo amet sit ea diam. Dolores vulputate ut et eum stet dolor diam dolore. At duo ipsum esse lorem velit est et commodo. Odio sit tempor kasd takimata diam at ut zzril dolore amet et tempor lorem elitr ipsum. Hendrerit clita diam nibh molestie at est. Velit aliquip dolor dolores zzril sed duis est eirmod duo ipsum tempor suscipit sadipscing facilisis eirmod nostrud nostrud. + +Est feugiat amet aliquyam takimata facilisi amet tation et facer labore labore et. Dolor invidunt sed. Sit sit assum et ipsum invidunt sit sanctus takimata dolor lorem in dolores erat at. + +Vero dolor sit gubergren sea amet invidunt labore dolores et sit tempor dolor takimata vulputate. Est nostrud lorem aliquyam vulputate blandit stet nibh sed et tempor at commodo ut sanctus. Sit ut sanctus wisi diam et elitr amet nisl magna delenit ipsum clita vero accumsan nonumy delenit. + +Molestie est takimata sed invidunt eos sadipscing dolore feugait. Facilisis dignissim consetetur diam eirmod feugait ea erat accusam. Voluptua volutpat dolor sit sea justo no sed eos nulla dolor eros dolor. Tempor elitr sanctus diam. Kasd ut rebum. Placerat velit clita et vero dolore rebum sea gubergren hendrerit rebum lobortis accusam. Amet dignissim est dolore ipsum in dolores aliquyam tempor velit. Eos sit lobortis et elitr gubergren rebum kasd et ipsum nam dolor lorem diam at exerci lorem diam lorem. Takimata et gubergren consetetur assum nobis commodo amet est sit kasd consequat in. Lorem dignissim erat et duo enim. Quod sanctus ipsum. Velit sanctus adipiscing diam ipsum duo at labore lorem dolore sed ut consetetur et no. Consectetuer tincidunt eleifend et et nonumy rebum sadipscing in. Praesent tempor dolores eirmod stet diam takimata tempor rebum erat stet eos dolor volutpat esse lorem id. Clita vel ut kasd elitr tempor velit et eos eu diam sit consetetur takimata tation diam. Velit accusam nibh augue delenit sed vero sed magna et gubergren eos labore ipsum. + +Invidunt magna qui consetetur est sed magna dolor. Erat consetetur ut lorem dolor. Ea stet dolore duis erat ea ad dolore facilisi clita assum duis tempor et veniam diam rebum diam accusam. At ipsum amet sed eu diam hendrerit justo diam praesent odio sadipscing sed diam sed kasd sit in amet. Facer dolore clita et invidunt luptatum. Dolor rebum kasd magna. Diam amet magna. Dolores erat sit nonumy eos dolor et nonumy takimata amet illum gubergren dolor diam possim nisl stet no. Sadipscing in stet tempor stet stet et ipsum iriure erat illum amet. + +Et eum rebum at takimata laoreet sit doming eos. Sed ipsum velit vel no. Sadipscing consectetuer ea tincidunt eirmod lorem est. Tempor elitr diam est gubergren diam ullamcorper iriure lorem erat ea amet voluptua. Magna sit possim ut qui est velit et iriure nisl amet justo sanctus lorem ea. Eum clita esse justo tempor ea adipiscing diam ut qui et aliquyam ipsum nisl dolor ea justo. + +Dolores eirmod vulputate illum consectetuer sadipscing et wisi suscipit kasd. Voluptua eros sit sea lorem dolor elitr facilisi sed et luptatum congue magna. Accusam voluptua dolores eirmod ut aliquyam wisi amet clita feugiat lorem ipsum sit dolor at dolore erat aliquyam aliquyam. Euismod dolor ut sed diam amet eos. Labore hendrerit dolor facilisi sadipscing illum. Ut voluptua nibh lorem no et at. Consectetuer lorem ex laoreet justo diam euismod zzril dolore enim ex accusam ea. Voluptua ut eros sea et amet et dolore rebum at iriure vero nonumy clita sed rebum. Lorem justo no magna et invidunt nonumy erat et. Eos autem nonumy accumsan nonumy ipsum duo et sanctus eos voluptua sit id nostrud ipsum no accusam. Ea nulla amet accumsan takimata et duis dolore sed consectetuer accumsan lorem ullamcorper exerci sanctus at lorem magna euismod. Sit no et et invidunt sed rebum ipsum consetetur blandit duo clita rebum at ipsum nisl vero et iriure. Vero sea takimata dolore ipsum. Vel autem amet justo quis est et dolores duis est nisl et hendrerit lorem amet consequat sanctus et dolor. Labore erat est ut ut zzril sea enim aliquyam quod. Eirmod kasd sed vulputate luptatum sea at sea rebum sed ipsum duis ipsum sit tincidunt erat mazim est. Wisi ut ea voluptua vero diam tincidunt gubergren et kasd sit. + +Lorem ipsum qui takimata kasd. Accusam nonumy invidunt zzril lorem invidunt invidunt ut diam labore kasd ut ea ad. Erat duo nam ut est diam eirmod dolor sed erat. Diam amet sadipscing clita lorem praesent vulputate amet labore takimata erat lorem amet et stet. Dolor voluptua eos voluptua magna nisl ipsum voluptua ut rebum lorem. Diam et sadipscing autem ipsum tempor gubergren te veniam quis qui sanctus et sit. Duis amet sed kasd minim praesent et invidunt lorem justo est tempor. Sed et diam invidunt diam magna takimata sit dolor commodo amet nulla dolore. Elitr eos sea consetetur feugiat zzril diam. Voluptua et dolor erat velit takimata. Eirmod ut ipsum adipiscing et qui sed lorem sit ut dolore sed at autem gubergren vel dignissim. Takimata sed stet dolor vero voluptua possim sed ut sea vulputate sit sed nostrud duo dolor no. Dolor eos clita. Sed aliquip in feugiat elitr aliquyam praesent elitr sadipscing et amet lorem eu amet et tation praesent et. Hendrerit labore stet clita dolores amet voluptua sea nonumy ipsum at consetetur duis amet magna sadipscing sadipscing. Wisi ut ut facilisis sadipscing rebum dolor et illum diam dolor. Et sadipscing magna at ut eros sit takimata vero in lorem ipsum. + +Dignissim dolor lorem magna praesent eos sed est tempor invidunt amet dolor. Eos iriure volutpat ipsum lorem diam nisl est lorem stet labore dolores est feugiat. Adipiscing velit suscipit ipsum dolor vulputate minim elitr nulla tempor gubergren delenit rebum. Et nonummy et laoreet ipsum nonumy magna at aliquam lorem hendrerit vero dolores laoreet aliquyam cum ea. At ipsum hendrerit illum zzril elitr erat consectetuer nibh eirmod. Amet vulputate at nonumy et diam. Sit minim consequat nibh nonumy sanctus sit sanctus amet at accumsan diam dolore kasd accusam. Nonumy ullamcorper suscipit feugait aliquyam sed sed blandit diam. + +Eirmod ullamcorper et ut accumsan autem nihil ipsum. Voluptua ipsum eleifend lorem aliquyam. Wisi aliquip clita dolor tation eros sed sadipscing volutpat. + +Velit stet no ut justo ipsum. Sit dolor ea sed tincidunt et ad rebum hendrerit et accusam sed no dolores erat est vel. Vulputate nonumy sit dolor no ut. Tempor dolor gubergren sea consetetur ea sea amet lorem invidunt labore erat eirmod eos aliquyam. Eum lorem no sanctus dolore diam lorem vero ea elit elitr sit no erat lorem tempor. Rebum eos stet lobortis. Volutpat ut rebum et aliquam magna sit. Takimata lorem amet autem dolor dolore amet kasd labore ea ipsum consequat amet doming consetetur magna hendrerit. Et nulla no vero adipiscing kasd ipsum at sit. Sadipscing sanctus praesent eos sed et gubergren nonumy sit diam. Eos velit esse rebum takimata et illum kasd et elitr consequat consectetuer ipsum et lorem stet duo diam. + +Erat magna diam dolor. Vel voluptua mazim laoreet ut est aliquyam dolore. Tincidunt sanctus euismod dolor in amet accusam consequat erat sit ut. Aliquip takimata clita no diam dolor accusam qui nonumy. Gubergren invidunt illum vel diam voluptua eirmod molestie clita est dolores delenit takimata eirmod takimata possim. Augue eirmod ea dolore at kasd eos sed et nonummy. No lorem nostrud vel eos velit justo velit consetetur gubergren. Iriure id in magna est accusam diam wisi at dignissim. Euismod sea feugiat stet ipsum labore et duis minim accusam enim. Augue ut eum aliquam elitr accusam justo blandit amet sanctus eirmod sadipscing et erat. Velit luptatum velit et et. Erat dolor sit. Sadipscing vel ut erat eleifend vel lorem lorem invidunt sed dolore ullamcorper ea minim clita sanctus duo gubergren ipsum. Eirmod sadipscing at justo invidunt kasd elitr et in amet sed ipsum stet nisl lorem at ut eirmod amet. Duo sed cum dolor aliquyam ea ipsum sea. Rebum at duis ipsum erat eum dolores accusam iriure sadipscing. Nulla ipsum velit invidunt invidunt ea aliquyam amet. + +Et aliquyam lorem iusto volutpat no nulla eos. Accusam stet ut stet nibh sed ipsum lorem dolore kasd et ipsum diam amet diam dolores ea sea. Luptatum rebum duo dolor feugait labore sadipscing amet. Amet iriure tempor nibh ipsum consectetuer et sed eos in gubergren nibh accusam kasd. Ut consequat magna dolore ut at aliquip ut dolor sed sed erat ipsum dolore consetetur sed sit dolor. Sea suscipit vero euismod lorem et vero et amet quod ipsum invidunt consetetur lorem est dolores voluptua dolore clita. Nonumy vulputate diam ut. In tation justo aliquyam vel veniam no amet accusam. Vero clita sea et tincidunt consetetur at te stet aliquam est dolor no dolore facilisis ut. Diam invidunt clita justo wisi magna diam dolor ea sanctus. Ea dolore aliquyam et vero ipsum stet est duo et justo adipiscing dolor ea sed dolore nibh stet ipsum. + +Ad sit justo dolor no labore stet ullamcorper duo. Diam eirmod takimata. Magna dolores ut diam eos invidunt magna dolores option accusam magna molestie ipsum molestie sadipscing aliquyam et. Ut duo dolor invidunt stet at. Dolor sit duo amet dolor et et in et dolor rebum sed diam vero vel amet imperdiet et. Diam quod magna vel rebum et invidunt gubergren ipsum takimata elitr amet dolor diam at in labore diam. + +Veniam diam sadipscing et est aliquyam duo dolor at eirmod ut. Ipsum diam erat facer eirmod et sanctus kasd kasd kasd diam labore sed. Dolores labore ea blandit. Dolore dolor stet sit iusto option. Dolor lorem est elitr lorem et et amet elitr takimata dolor cum ipsum sed. At sadipscing dolore sit gubergren sanctus ipsum stet labore quis at consectetuer lorem feugiat diam et feugait diam. + +Takimata nam duo et. Amet diam gubergren gubergren. Elit molestie sed accumsan diam hendrerit justo. Velit elitr sit tempor liber imperdiet tincidunt suscipit ut eirmod invidunt ut et augue. + +Consetetur justo tincidunt ut aliquyam kasd sed sadipscing dolor eu labore. Elitr sadipscing sed et ipsum dolores justo elit ut et sit at. Et ea accumsan quod at ut et eos lorem elit duo rebum dolore accusam. Rebum no labore voluptua clita. Rebum at no et kasd est consetetur nostrud takimata vero duo labore est justo diam eirmod erat diam. Dolores diam sadipscing augue gubergren suscipit amet sit euismod qui sed invidunt vero sed dolor takimata justo ea. Sadipscing aliquyam iusto feugiat quis sanctus eleifend. Dolores dolor ipsum te sed accusam magna. Gubergren eirmod stet lorem nonumy dolores tincidunt sit sadipscing dolor stet sadipscing accusam sed stet. Iusto vulputate labore elit feugiat no luptatum et nonumy vulputate dolore gubergren sed. Sit wisi sadipscing commodo. Duo diam luptatum vel diam sit et ut rebum ut nam no. Eos velit et aliquam est et dolores sit est et lorem assum. Volutpat consetetur duo dolor sanctus sit magna eos consetetur dolore accusam et sea sit no iriure accusam. + +Amet facilisis accusam esse ipsum ea gubergren. Amet dolor consectetuer diam clita consetetur tincidunt et feugait labore et. Sanctus consequat ipsum sed placerat ut sadipscing rebum justo voluptua nulla ut autem dolore. Ut duis ipsum ipsum minim nulla aliquam nonumy eirmod et voluptua no dolore commodo voluptua zzril. Lorem sed dolores augue eos sed ipsum lorem vero sit diam iriure no commodo hendrerit. Ipsum dolor iriure est. Ipsum eu dolore dolor ea et. Et consetetur elit quod tation duo magna. Nulla dolor tempor et hendrerit. Elitr illum imperdiet ea erat. Ut erat tincidunt consequat dolor dolore. Vel dolor nobis ad no sed sit dolor stet erat nobis. Diam praesent sanctus amet sit duis. Aliquyam sea consetetur dolore et nobis ea amet nonummy sit sit tempor sed ea duis no velit rebum diam. Sadipscing et dolore erat. Consetetur amet nonumy labore sed erat eu delenit et lorem consectetuer dolor rebum aliquyam. Sed iriure aliquip sit sanctus kasd kasd et erat diam dolores invidunt duo accusam autem est sadipscing ea. + +Duo erat dolores soluta dolores lorem tation amet illum rebum et at dolores clita tempor vel. Eu amet consetetur ea ad dolore dolor kasd. Wisi elitr ad amet. Tempor tempor justo ipsum nibh eu dolor ea et odio ea sit ea wisi voluptua justo. Labore et dolores te duis qui id et wisi lorem in vero eos veniam ipsum. At ad aliquam dolore rebum sadipscing diam cum aliquyam diam vulputate tempor lorem. + +Cum et sed sea ea diam minim magna ipsum hendrerit duis clita voluptua. Sed amet takimata duo diam diam dolor sit tempor at sed nihil. Nulla erat sed takimata et. Ipsum eirmod luptatum. Accusam vel imperdiet illum sed tempor diam. Magna sed at dolor takimata elit et laoreet in lorem et lorem labore et tempor stet autem erat. Clita in vero takimata elitr autem est invidunt qui amet. Diam eleifend augue et gubergren et velit diam consetetur. + +Dolores ea et accusam nihil clita sanctus aliquyam eu sed ut congue ipsum amet. Est luptatum et ipsum autem duo et ea clita duo nulla illum takimata et vero accusam tempor sed. Sadipscing tincidunt labore. Dolor sadipscing magna exerci praesent consetetur dolor ipsum vero sed sit minim eos volutpat diam consequat. Amet sit kasd et nonumy ea lorem stet no diam ipsum est et ipsum lorem et enim sed no. At hendrerit et no eos elit lorem tempor invidunt. Ut dolor exerci diam ipsum gubergren placerat amet sea minim magna amet et duo iusto eu sit. Labore gubergren nonummy tempor option et erat sanctus dolore justo facilisi invidunt sit elit sed takimata hendrerit duo vero. Lorem nonumy ipsum ea justo dolor vulputate. Stet dolor liber eleifend amet erat ea. Hendrerit elitr zzril facilisis autem et erat eos in ea dolores qui amet duo. Lorem nonumy eirmod accusam labore dolore stet takimata blandit augue vel vero sit feugiat takimata. Consectetuer ea duo consequat lorem odio dolor. Eum no at. Dolores ipsum dolores aliquyam amet at sit clita amet ut ea erat. + +Clita et dolores. Et in molestie voluptua feugiat est magna ut. Et sed diam eum invidunt suscipit takimata sed diam duis aliquip vel lorem dolore. Feugiat clita option diam et stet accumsan dolore sed stet. Labore wisi diam. Augue lorem in consequat dolor stet esse rebum. Elitr sanctus amet quod diam et labore vel nonumy sea. Diam et hendrerit rebum sit no no nulla sed dolor stet ut amet ut ipsum. Ipsum accusam erat et no vel nonumy minim et justo et nostrud rebum augue. Nonumy quis dolor rebum at sed nulla tempor consetetur sed aliquyam ipsum dolor et. Iriure tempor aliquyam ex sed hendrerit dolores ea est dolore dolore kasd dolor sed sadipscing enim consetetur dolore. Dolore vel tincidunt. Vero gubergren at labore justo rebum erat sed vero id duo vero sanctus takimata et. Nam iusto dolor diam magna nostrud erat eu tincidunt amet eu invidunt accusam dolor accumsan dolores. Nulla ea possim tempor takimata eos amet et et et dolor aliquyam accusam duis takimata. + +Kasd ullamcorper tempor duo vel labore vel sadipscing sed. Eleifend vero takimata commodo ut commodo duo minim elitr sadipscing. Autem no sea eu diam molestie eum amet nam. At vulputate et ut amet ea est amet. Sed kasd dolores iriure invidunt ipsum eos elitr zzril dolor ipsum et lorem consequat dolores stet ut sed consectetuer. Eum dolores euismod soluta sit voluptua ipsum ut nostrud consequat vulputate clita volutpat kasd sit. Dolor veniam eos illum lorem aliquyam dolore invidunt lobortis blandit diam eirmod sanctus eos ipsum. Eirmod consequat ea. Invidunt et sadipscing gubergren amet nonumy facilisis sed gubergren sed erat tempor consectetuer eum ea tempor nonumy dolor nonumy. Ut invidunt lorem labore lorem zzril clita velit tempor ex. Lorem dolore voluptua duo eu. Stet hendrerit duis ex ipsum stet minim. + +Accusam kasd sea rebum luptatum ipsum. Dolore nonumy qui amet gubergren kasd stet diam vero sed feugiat. Magna at vero sit sit labore et nulla in ut dolore et tempor duis voluptua rebum dolor. In et ipsum dolor consetetur aliquam lorem vel voluptua amet. Accusam diam clita ut erat vero ipsum eum sed vulputate eu sed facer diam sed in augue delenit ipsum. Sit quod feugait. Doming clita erat magna tempor consequat duo takimata eirmod tempor blandit ut. Tempor voluptua aliquam diam commodo ex sea. Elitr lorem at sit dolore tempor duo sed praesent at dolor dolores delenit sea dolores amet duo. Invidunt accusam odio ipsum invidunt nulla takimata vel et nonumy consetetur. Volutpat nonumy dolores sit ea. Et ut est kasd lorem erat et euismod voluptua vero sed vel elitr at gubergren. + +Tempor duis diam stet dolor diam illum amet commodo ut sea diam. Sit dolor ipsum aliquam hendrerit dolore liber no accusam in aliquyam et. Dolore at stet dolor ea duis voluptua in aliquyam magna sed dolores laoreet nonummy iriure ea. Autem dolor dolore lorem eos diam takimata tempor diam diam liber. Et et no et eros sea dolor eos dolor sit. Sit et accusam suscipit diam liber stet magna stet sadipscing et sit nobis diam takimata sadipscing dolores est. Tation et accusam magna rebum blandit justo magna ea aliquam. Commodo duo sea eirmod dolore dolore dolor lorem magna. Gubergren at iriure. Veniam at voluptua erat eirmod duis ut sadipscing. Diam tempor erat invidunt sed est no. Ipsum ipsum in sed duo et ipsum magna at in autem at dolore duo. Dolor labore ut sadipscing veniam sit imperdiet nonumy liber nostrud gubergren. Dolores eos sit elit dolore et suscipit. Nonummy ipsum diam duis accusam erat sed clita tempor est duo. Eros lorem ea invidunt eos stet wisi sea clita sea labore sit in vulputate erat eleifend at. Amet lorem ipsum et. \ No newline at end of file diff --git a/ModularPipelines.UnitTests/DirectCollisionTests.cs b/ModularPipelines.UnitTests/DirectCollisionTests.cs index fb596b61a1..30c81bef65 100644 --- a/ModularPipelines.UnitTests/DirectCollisionTests.cs +++ b/ModularPipelines.UnitTests/DirectCollisionTests.cs @@ -27,11 +27,7 @@ public void Modules_Dependent_On_Each_Other_Throws_Exception() [DependsOn] private class DependencyConflictModule1 : Module { - public DependencyConflictModule1(IModuleContext moduleContext) : base(moduleContext) - { - } - - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { await Task.CompletedTask; return null; @@ -41,11 +37,7 @@ public DependencyConflictModule1(IModuleContext moduleContext) : base(moduleCont [DependsOn] private class DependencyConflictModule2 : Module { - public DependencyConflictModule2(IModuleContext moduleContext) : base(moduleContext) - { - } - - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { await Task.CompletedTask; return null; diff --git a/ModularPipelines.UnitTests/DisposableModuleTests.cs b/ModularPipelines.UnitTests/DisposableModuleTests.cs index bbad47e62e..b4b8b31927 100644 --- a/ModularPipelines.UnitTests/DisposableModuleTests.cs +++ b/ModularPipelines.UnitTests/DisposableModuleTests.cs @@ -24,11 +24,8 @@ public async Task SuccessfullyDisposed() public class DisposableModule : Module, IDisposable { public bool IsDisposed { get; private set; } - public DisposableModule(IModuleContext context) : base(context) - { - } - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { await Task.Delay(100, cancellationToken); return null; diff --git a/ModularPipelines.UnitTests/GlobalTestSettings.cs b/ModularPipelines.UnitTests/GlobalTestSettings.cs new file mode 100644 index 0000000000..4952ec1211 --- /dev/null +++ b/ModularPipelines.UnitTests/GlobalTestSettings.cs @@ -0,0 +1 @@ +[assembly: Parallelizable(ParallelScope.All)] \ No newline at end of file diff --git a/ModularPipelines.UnitTests/Helpers/Base64Tests.cs b/ModularPipelines.UnitTests/Helpers/Base64Tests.cs new file mode 100644 index 0000000000..036fbbd0fd --- /dev/null +++ b/ModularPipelines.UnitTests/Helpers/Base64Tests.cs @@ -0,0 +1,76 @@ +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.UnitTests.Helpers; + +public class Base64Tests : TestBase +{ + private class ToBase64Module : Module + { + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Yield(); + return context.Base64.ToBase64String("Foo bar!"); + } + } + + [Test] + public async Task To_Base64_Has_Not_Errored() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult)); + Assert.That(moduleResult.Exception, Is.Null); + Assert.That(moduleResult.Value, Is.Not.Null); + }); + } + + [Test] + public async Task To_Base64_Output_Equals_Foo_Bar() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.That(moduleResult.Value, Is.EqualTo("Rm9vIGJhciE=")); + } + + private class FromBase64Module : Module + { + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Yield(); + return context.Base64.FromBase64String("Rm9vIGJhciE="); + } + } + + [Test] + public async Task From_Base64_Has_Not_Errored() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult)); + Assert.That(moduleResult.Exception, Is.Null); + Assert.That(moduleResult.Value, Is.Not.Null); + }); + } + + [Test] + public async Task From_Base64_Output_Equals_Foo_Bar() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.That(moduleResult.Value, Is.EqualTo("Foo bar!")); + } +} \ No newline at end of file diff --git a/ModularPipelines.UnitTests/Helpers/CmdTests.cs b/ModularPipelines.UnitTests/Helpers/CmdTests.cs new file mode 100644 index 0000000000..9c6cbd4ca3 --- /dev/null +++ b/ModularPipelines.UnitTests/Helpers/CmdTests.cs @@ -0,0 +1,56 @@ +using CliWrap.Buffered; +using ModularPipelines.Cmd.Extensions; +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.UnitTests.Helpers; + +public class CmdTests : TestBase +{ + [SetUp] + public void Setup() + { + if (Environment.OSVersion.Platform != PlatformID.Win32NT) + { + Assert.Ignore("Cmd is Windows only"); + } + } + + private class CmdEchoModule : Module + { + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + return await context.Cmd().Script(new("echo Foo bar!"), cancellationToken: cancellationToken); + } + } + + [Test] + public async Task Has_Not_Errored() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult)); + Assert.That(moduleResult.Exception, Is.Null); + Assert.That(moduleResult.Value, Is.Not.Null); + }); + } + + [Test] + public async Task Standard_Output_Equals_Foo_Bar() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.Value!.StandardError, Is.Null.Or.Empty); + Assert.That(moduleResult.Value.StandardOutput.Trim(), Is.EqualTo("Foo bar!")); + }); + } +} \ No newline at end of file diff --git a/ModularPipelines.UnitTests/Helpers/CommandTests.cs b/ModularPipelines.UnitTests/Helpers/CommandTests.cs new file mode 100644 index 0000000000..c9e471178c --- /dev/null +++ b/ModularPipelines.UnitTests/Helpers/CommandTests.cs @@ -0,0 +1,50 @@ +using CliWrap.Buffered; +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; +using ModularPipelines.Options; + +namespace ModularPipelines.UnitTests.Helpers; + +public class CommandTests : TestBase +{ + private class CommandEchoModule : Module + { + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + return await context.Command.UsingCommandLineTool(new CommandLineToolOptions("pwsh") + { + Arguments = new []{ "-Command", "echo 'Foo bar!'" } + }, cancellationToken: cancellationToken); + } + } + + [Test] + public async Task Has_Not_Errored() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult)); + Assert.That(moduleResult.Exception, Is.Null); + Assert.That(moduleResult.Value, Is.Not.Null); + }); + } + + [Test] + public async Task Standard_Output_Equals_Foo_Bar() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.Value!.StandardError, Is.Null.Or.Empty); + Assert.That(moduleResult.Value.StandardOutput.Trim(), Is.EqualTo("Foo bar!")); + }); + } +} \ No newline at end of file diff --git a/ModularPipelines.UnitTests/Helpers/DotNetTests.cs b/ModularPipelines.UnitTests/Helpers/DotNetTests.cs new file mode 100644 index 0000000000..f9bbabd1fe --- /dev/null +++ b/ModularPipelines.UnitTests/Helpers/DotNetTests.cs @@ -0,0 +1,47 @@ +using CliWrap.Buffered; +using ModularPipelines.Context; +using ModularPipelines.DotNet.Extensions; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.UnitTests.Helpers; + +public class DotNetTests : TestBase +{ + private class DotNetVersionModule : Module + { + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + return await context.DotNet().Version(cancellationToken: cancellationToken); + } + } + + [Test] + public async Task Has_Not_Errored() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult)); + Assert.That(moduleResult.Exception, Is.Null); + Assert.That(moduleResult.Value, Is.Not.Null); + }); + } + + [Test] + public async Task Standard_Output_Starts_With_Git_Version() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.Value!.StandardError, Is.Null.Or.Empty); + Assert.That(moduleResult.Value.StandardOutput, Does.Match("\\d+")); + }); + } +} \ No newline at end of file diff --git a/ModularPipelines.UnitTests/Helpers/GitTests.cs b/ModularPipelines.UnitTests/Helpers/GitTests.cs new file mode 100644 index 0000000000..dbdf393718 --- /dev/null +++ b/ModularPipelines.UnitTests/Helpers/GitTests.cs @@ -0,0 +1,47 @@ +using CliWrap.Buffered; +using ModularPipelines.Context; +using ModularPipelines.Git.Extensions; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.UnitTests.Helpers; + +public class GitTests : TestBase +{ + private class GitVersionModule : Module + { + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + return await context.Git().Operations.Version(cancellationToken: cancellationToken); + } + } + + [Test] + public async Task Has_Not_Errored() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult)); + Assert.That(moduleResult.Exception, Is.Null); + Assert.That(moduleResult.Value, Is.Not.Null); + }); + } + + [Test] + public async Task Standard_Output_Starts_With_Git_Version() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.Value!.StandardError, Is.Null.Or.Empty); + Assert.That(moduleResult.Value.StandardOutput, Does.Match("git version \\d+.*")); + }); + } +} \ No newline at end of file diff --git a/ModularPipelines.UnitTests/Helpers/HexTests.cs b/ModularPipelines.UnitTests/Helpers/HexTests.cs new file mode 100644 index 0000000000..998afe999f --- /dev/null +++ b/ModularPipelines.UnitTests/Helpers/HexTests.cs @@ -0,0 +1,76 @@ +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.UnitTests.Helpers; + +public class HexTests : TestBase +{ + private class ToHexModule : Module + { + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Yield(); + return context.Hex.ToHex("Foo bar!"); + } + } + + [Test] + public async Task To_Hex_Has_Not_Errored() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult)); + Assert.That(moduleResult.Exception, Is.Null); + Assert.That(moduleResult.Value, Is.Not.Null); + }); + } + + [Test] + public async Task To_Hex_Output_Equals_Foo_Bar() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.That(moduleResult.Value, Is.EqualTo("466f6f2062617221")); + } + + private class FromHexModule : Module + { + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Yield(); + return context.Hex.FromHex("466f6f2062617221"); + } + } + + [Test] + public async Task From_Hex_Has_Not_Errored() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult)); + Assert.That(moduleResult.Exception, Is.Null); + Assert.That(moduleResult.Value, Is.Not.Null); + }); + } + + [Test] + public async Task From_Hex_Output_Equals_Foo_Bar() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.That(moduleResult.Value, Is.EqualTo("Foo bar!")); + } +} \ No newline at end of file diff --git a/ModularPipelines.UnitTests/Helpers/Md5Tests.cs b/ModularPipelines.UnitTests/Helpers/Md5Tests.cs new file mode 100644 index 0000000000..45cd3dc38d --- /dev/null +++ b/ModularPipelines.UnitTests/Helpers/Md5Tests.cs @@ -0,0 +1,42 @@ +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.UnitTests.Helpers; + +public class Md5Tests : TestBase +{ + private class ToMd5Module : Module + { + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Yield(); + return context.Hasher.Md5("Foo bar!"); + } + } + + [Test] + public async Task To_Md5_Has_Not_Errored() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult)); + Assert.That(moduleResult.Exception, Is.Null); + Assert.That(moduleResult.Value, Is.Not.Null); + }); + } + + [Test] + public async Task To_Md5_Output_Equals_Foo_Bar() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.That(moduleResult.Value, Is.EqualTo("b9c291e3274aa5c8010a7c5c22a4e6dd")); + } +} \ No newline at end of file diff --git a/ModularPipelines.UnitTests/Helpers/NodeTests.cs b/ModularPipelines.UnitTests/Helpers/NodeTests.cs new file mode 100644 index 0000000000..8dd4ccd621 --- /dev/null +++ b/ModularPipelines.UnitTests/Helpers/NodeTests.cs @@ -0,0 +1,54 @@ +using CliWrap.Buffered; +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; +using ModularPipelines.Node.Extensions; + +namespace ModularPipelines.UnitTests.Helpers; + +public class NodeTests : TestBase +{ + private class NodeVersionModule : Module + { + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + var task = () => + { + context.Node().Npm.Install(null!, cancellationToken); + context.Node().Npm.Run(null!, cancellationToken); + context.Node().Nvm.Use(null!, cancellationToken); + }; + + return await context.Node().Version(cancellationToken: cancellationToken); + } + } + + [Test] + public async Task Has_Not_Errored() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult)); + Assert.That(moduleResult.Exception, Is.Null); + Assert.That(moduleResult.Value, Is.Not.Null); + }); + } + + [Test] + public async Task Standard_Output_Is_Version_Number() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.Value!.StandardError, Is.Null.Or.Empty); + Assert.That(moduleResult.Value.StandardOutput, Does.Match("v\\d+")); + }); + } +} \ No newline at end of file diff --git a/ModularPipelines.UnitTests/Helpers/PowershellTests.cs b/ModularPipelines.UnitTests/Helpers/PowershellTests.cs new file mode 100644 index 0000000000..9d46da5de8 --- /dev/null +++ b/ModularPipelines.UnitTests/Helpers/PowershellTests.cs @@ -0,0 +1,47 @@ +using CliWrap.Buffered; +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; +using ModularPipelines.Powershell.Extensions; + +namespace ModularPipelines.UnitTests.Helpers; + +public class PowershellTests : TestBase +{ + private class PowershellEchoModule : Module + { + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + return await context.Powershell().Script(new("Write-Host \"Foo bar!\""), cancellationToken: cancellationToken); + } + } + + [Test] + public async Task Has_Not_Errored() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult)); + Assert.That(moduleResult.Exception, Is.Null); + Assert.That(moduleResult.Value, Is.Not.Null); + }); + } + + [Test] + public async Task Standard_Output_Equals_Foo_Bar() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.Value!.StandardError, Is.Null.Or.Empty); + Assert.That(moduleResult.Value.StandardOutput.Trim(), Is.EqualTo("Foo bar!")); + }); + } +} \ No newline at end of file diff --git a/ModularPipelines.UnitTests/Helpers/Sha1Tests.cs b/ModularPipelines.UnitTests/Helpers/Sha1Tests.cs new file mode 100644 index 0000000000..5342814f74 --- /dev/null +++ b/ModularPipelines.UnitTests/Helpers/Sha1Tests.cs @@ -0,0 +1,42 @@ +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.UnitTests.Helpers; + +public class Sha1Tests : TestBase +{ + private class ToSha1Module : Module + { + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Yield(); + return context.Hasher.Sha1("Foo bar!"); + } + } + + [Test] + public async Task To_Sha1_Has_Not_Errored() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult)); + Assert.That(moduleResult.Exception, Is.Null); + Assert.That(moduleResult.Value, Is.Not.Null); + }); + } + + [Test] + public async Task To_Sha1_Output_Equals_Foo_Bar() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.That(moduleResult.Value, Is.EqualTo("cc3626c5ad2e3aff0779dc63e80555c463fd99dc")); + } +} \ No newline at end of file diff --git a/ModularPipelines.UnitTests/Helpers/Sha256Tests.cs b/ModularPipelines.UnitTests/Helpers/Sha256Tests.cs new file mode 100644 index 0000000000..fdceb33c24 --- /dev/null +++ b/ModularPipelines.UnitTests/Helpers/Sha256Tests.cs @@ -0,0 +1,42 @@ +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.UnitTests.Helpers; + +public class Sha256Tests : TestBase +{ + private class ToSha256Module : Module + { + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Yield(); + return context.Hasher.Sha256("Foo bar!"); + } + } + + [Test] + public async Task To_Sha256_Has_Not_Errored() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult)); + Assert.That(moduleResult.Exception, Is.Null); + Assert.That(moduleResult.Value, Is.Not.Null); + }); + } + + [Test] + public async Task To_Sha256_Output_Equals_Foo_Bar() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.That(moduleResult.Value, Is.EqualTo("d80c14a132a9ae008c78db4ee4cbc46b015b5e0f018f6b0a3e4ea5041176b852")); + } +} \ No newline at end of file diff --git a/ModularPipelines.UnitTests/Helpers/Sha384Tests.cs b/ModularPipelines.UnitTests/Helpers/Sha384Tests.cs new file mode 100644 index 0000000000..8576de149d --- /dev/null +++ b/ModularPipelines.UnitTests/Helpers/Sha384Tests.cs @@ -0,0 +1,42 @@ +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.UnitTests.Helpers; + +public class Sha384Tests : TestBase +{ + private class ToSha384Module : Module + { + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Yield(); + return context.Hasher.Sha384("Foo bar!"); + } + } + + [Test] + public async Task To_Sha384_Has_Not_Errored() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult)); + Assert.That(moduleResult.Exception, Is.Null); + Assert.That(moduleResult.Value, Is.Not.Null); + }); + } + + [Test] + public async Task To_Sha384_Output_Equals_Foo_Bar() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.That(moduleResult.Value, Is.EqualTo("bb338a277da65d5663467d5fd98aa67349506150cd1287597b0eaa0f0988d2b22c33504fd85dd0b8c99ce8cc50666f88")); + } +} \ No newline at end of file diff --git a/ModularPipelines.UnitTests/Helpers/Sha512Tests.cs b/ModularPipelines.UnitTests/Helpers/Sha512Tests.cs new file mode 100644 index 0000000000..1685cae398 --- /dev/null +++ b/ModularPipelines.UnitTests/Helpers/Sha512Tests.cs @@ -0,0 +1,42 @@ +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.UnitTests.Helpers; + +public class Sha512Tests : TestBase +{ + private class ToSha512Module : Module + { + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Yield(); + return context.Hasher.Sha512("Foo bar!"); + } + } + + [Test] + public async Task To_Sha512_Has_Not_Errored() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult)); + Assert.That(moduleResult.Exception, Is.Null); + Assert.That(moduleResult.Value, Is.Not.Null); + }); + } + + [Test] + public async Task To_Sha512_Output_Equals_Foo_Bar() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.That(moduleResult.Value, Is.EqualTo("e399b0584705f5f229a4398baa31c4b7cc820ac208327d26e66f0668288536981c3460a7ea92ef6be488ce30ff5b6a991babfe24833094eba3226cea5c14162c")); + } +} \ No newline at end of file diff --git a/ModularPipelines.UnitTests/Helpers/ZipTests.cs b/ModularPipelines.UnitTests/Helpers/ZipTests.cs new file mode 100644 index 0000000000..0f43fbed05 --- /dev/null +++ b/ModularPipelines.UnitTests/Helpers/ZipTests.cs @@ -0,0 +1,103 @@ +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.UnitTests.Helpers; + +[Parallelizable(ParallelScope.Self)] +public class ZipTests : TestBase +{ + private class ZipModule : Module + { + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Yield(); + + var directory = context.Environment.GitRootDirectory!.GetFolder("ModularPipelines.UnitTests").GetFolder("Data"); + + var fileToWrite = context.Environment.WorkingDirectory.GetFile("LoremData.zip"); + + fileToWrite.Delete(); + + context.Zip.ZipFolder(directory, fileToWrite.Path); + + return null; + } + } + + [Test, Order(1)] + public async Task Has_Not_Errored() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult)); + Assert.That(moduleResult.Exception, Is.Null); + }); + } + + [Test, Order(2)] + public async Task Zip_File_Exists() + { + var module = await RunModule(); + + var moduleResult = await module; + + var expectedFile = new FileInfo(Path.Combine(TestContext.CurrentContext.WorkDirectory, "LoremData.zip")); + + Assert.Multiple(() => + { + Assert.That(expectedFile.Exists, Is.True); + Assert.That(expectedFile.Length, Is.GreaterThan(5000)); + }); + } + + private class UnZipModule : Module + { + protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + await Task.Yield(); + + var zipLocation = context.Environment.WorkingDirectory.GetFile("LoremData.zip"); + + var unzippedLocation = context.Environment.WorkingDirectory.GetFolder("LoremDataUnzipped"); + + context.Zip.UnZipToFolder(zipLocation.Path, unzippedLocation.Path); + + return null; + } + } + + [Test, Order(3)] + public async Task UnZip_Has_Not_Errored() + { + var module = await RunModule(); + + var moduleResult = await module; + + Assert.Multiple(() => + { + Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult)); + Assert.That(moduleResult.Exception, Is.Null); + }); + } + + [Test, Order(4)] + public async Task UnZipped_Folder_Exists() + { + var module = await RunModule(); + + var moduleResult = await module; + + var expectedFolder = new DirectoryInfo(Path.Combine(TestContext.CurrentContext.WorkDirectory, "LoremDataUnzipped")); + + Assert.Multiple(() => + { + Assert.That(expectedFolder.Exists, Is.True); + Assert.That(expectedFolder.GetFiles("*", SearchOption.AllDirectories).Length, Is.EqualTo(1)); + }); + } +} \ No newline at end of file diff --git a/ModularPipelines.UnitTests/ModularPipelines.UnitTests.csproj b/ModularPipelines.UnitTests/ModularPipelines.UnitTests.csproj index d9a103a4a0..fda7fd58f1 100644 --- a/ModularPipelines.UnitTests/ModularPipelines.UnitTests.csproj +++ b/ModularPipelines.UnitTests/ModularPipelines.UnitTests.csproj @@ -11,14 +11,25 @@ - + - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/ModularPipelines.UnitTests/NestedCollisionTests.cs b/ModularPipelines.UnitTests/NestedCollisionTests.cs index 2a8640c4a8..cd36530356 100644 --- a/ModularPipelines.UnitTests/NestedCollisionTests.cs +++ b/ModularPipelines.UnitTests/NestedCollisionTests.cs @@ -30,11 +30,7 @@ public void Modules_Dependent_On_Each_Other_Throws_Exception() [DependsOn] private class DependencyConflictModule1 : Module { - public DependencyConflictModule1(IModuleContext moduleContext) : base(moduleContext) - { - } - - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { await Task.CompletedTask; return null; @@ -44,11 +40,7 @@ public DependencyConflictModule1(IModuleContext moduleContext) : base(moduleCont [DependsOn] private class DependencyConflictModule2 : Module { - public DependencyConflictModule2(IModuleContext moduleContext) : base(moduleContext) - { - } - - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { await Task.CompletedTask; return null; @@ -58,11 +50,7 @@ public DependencyConflictModule2(IModuleContext moduleContext) : base(moduleCont [DependsOn] private class DependencyConflictModule3 : Module { - public DependencyConflictModule3(IModuleContext moduleContext) : base(moduleContext) - { - } - - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { await Task.CompletedTask; return null; @@ -72,11 +60,7 @@ public DependencyConflictModule3(IModuleContext moduleContext) : base(moduleCont [DependsOn] private class DependencyConflictModule4 : Module { - public DependencyConflictModule4(IModuleContext moduleContext) : base(moduleContext) - { - } - - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { await Task.CompletedTask; return null; @@ -86,11 +70,7 @@ public DependencyConflictModule4(IModuleContext moduleContext) : base(moduleCont [DependsOn] private class DependencyConflictModule5 : Module { - public DependencyConflictModule5(IModuleContext moduleContext) : base(moduleContext) - { - } - - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { await Task.CompletedTask; return null; diff --git a/ModularPipelines.UnitTests/TestBase.cs b/ModularPipelines.UnitTests/TestBase.cs new file mode 100644 index 0000000000..8f39d5e914 --- /dev/null +++ b/ModularPipelines.UnitTests/TestBase.cs @@ -0,0 +1,20 @@ +using ModularPipelines.Extensions; +using ModularPipelines.Host; +using ModularPipelines.Modules; + +namespace ModularPipelines.UnitTests; + +public class TestBase +{ + public async Task RunModule() where T : ModuleBase + { + var results = await PipelineHostBuilder.Create() + .ConfigureServices((context, collection) => + { + collection.AddModule(); + }) + .ExecutePipelineAsync(); + + return results.OfType().Single(); + } +} \ No newline at end of file diff --git a/ModularPipelines.UnitTests/TimedDependencyTests.cs b/ModularPipelines.UnitTests/TimedDependencyTests.cs index a9917a5b91..4204d4fb8d 100644 --- a/ModularPipelines.UnitTests/TimedDependencyTests.cs +++ b/ModularPipelines.UnitTests/TimedDependencyTests.cs @@ -27,19 +27,15 @@ public async Task OneSecondModule_WillWaitForFiveSecondModule_ThenExecute() { // 5 + 1 - Assert.That(oneSecondModuleDependentOnFiveSecondModule.Duration, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); - Assert.That(oneSecondModuleDependentOnFiveSecondModule.EndTime, Is.GreaterThanOrEqualTo(fiveSecondModule.StartTime + TimeSpan.FromSeconds(6))); + Assert.That(oneSecondModuleDependentOnFiveSecondModule.Duration, Is.GreaterThanOrEqualTo(TimeSpan.FromMilliseconds(900))); + Assert.That(oneSecondModuleDependentOnFiveSecondModule.EndTime, Is.GreaterThanOrEqualTo(fiveSecondModule.StartTime + TimeSpan.FromMilliseconds(5900))); Assert.That(oneSecondModuleDependentOnFiveSecondModule.StartTime, Is.GreaterThanOrEqualTo(fiveSecondModule.EndTime)); }); } private class FiveSecondModule : Module { - public FiveSecondModule(IModuleContext moduleContext) : base(moduleContext) - { - } - - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); return ModuleResult.Empty>(); @@ -49,11 +45,7 @@ public FiveSecondModule(IModuleContext moduleContext) : base(moduleContext) [DependsOn] private class OneSecondModuleDependentOnFiveSecondModule : Module { - public OneSecondModuleDependentOnFiveSecondModule(IModuleContext moduleContext) : base(moduleContext) - { - } - - protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken) + protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); return ModuleResult.Empty>(); diff --git a/ModularPipelines.sln b/ModularPipelines.sln index ef154e34e7..2583a5c306 100644 --- a/ModularPipelines.sln +++ b/ModularPipelines.sln @@ -1,24 +1,44 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModularPipelines", "ModularPipelines\ModularPipelines.csproj", "{A25FAFCF-E226-4263-B3D6-732668604BD9}" +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33815.320 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModularPipelines", "ModularPipelines\ModularPipelines.csproj", "{A25FAFCF-E226-4263-B3D6-732668604BD9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModularPipelines.UnitTests", "ModularPipelines.UnitTests\ModularPipelines.UnitTests.csproj", "{937A2E0D-8D4B-4B36-848E-389CD789E141}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModularPipelines.UnitTests", "ModularPipelines.UnitTests\ModularPipelines.UnitTests.csproj", "{937A2E0D-8D4B-4B36-848E-389CD789E141}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModularPipelines.Build", "ModularPipelines.Build\ModularPipelines.Build.csproj", "{936B9B1B-D734-4757-95FE-257B852E3358}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModularPipelines.Build", "ModularPipelines.Build\ModularPipelines.Build.csproj", "{936B9B1B-D734-4757-95FE-257B852E3358}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModularPipelines.DotNet", "ModularPipelines.DotNet\ModularPipelines.DotNet.csproj", "{5A0C3F55-8D46-497D-9757-9640C3A59B43}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModularPipelines.DotNet", "ModularPipelines.DotNet\ModularPipelines.DotNet.csproj", "{5A0C3F55-8D46-497D-9757-9640C3A59B43}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModularPipelines.Examples", "ModularPipelines.Examples\ModularPipelines.Examples.csproj", "{AD5E9D03-0DF0-4CA9-9E7A-4AF41ADE198B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModularPipelines.Examples", "ModularPipelines.Examples\ModularPipelines.Examples.csproj", "{AD5E9D03-0DF0-4CA9-9E7A-4AF41ADE198B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModularPipelines.Git", "ModularPipelines.Git\ModularPipelines.Git.csproj", "{DA86F082-5143-40C2-8EFA-F124D68FCE02}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModularPipelines.Git", "ModularPipelines.Git\ModularPipelines.Git.csproj", "{DA86F082-5143-40C2-8EFA-F124D68FCE02}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModularPipelines.Command", "ModularPipelines.Command\ModularPipelines.Command.csproj", "{25F82D0E-84EF-40F1-8059-8BBC447BCB16}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModularPipelines.MicrosoftTeams", "ModularPipelines.MicrosoftTeams\ModularPipelines.MicrosoftTeams.csproj", "{FFBA91D0-9DC8-48A7-BD8E-6EF2E436F0AF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModularPipelines.MicrosoftTeams", "ModularPipelines.MicrosoftTeams\ModularPipelines.MicrosoftTeams.csproj", "{FFBA91D0-9DC8-48A7-BD8E-6EF2E436F0AF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModularPipelines.NuGet", "ModularPipelines.NuGet\ModularPipelines.NuGet.csproj", "{8A6B4058-2863-4406-A651-5E334C81777F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModularPipelines.Installer", "ModularPipelines.Installer\ModularPipelines.Installer.csproj", "{D23FE24C-9416-4ABD-84BA-49720582D8A1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModularPipelines.Node", "ModularPipelines.Node\ModularPipelines.Node.csproj", "{1C075CAF-7409-4941-8622-E3D3DDD876AB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModularPipelines.NuGet", "ModularPipelines.NuGet\ModularPipelines.NuGet.csproj", "{8A6B4058-2863-4406-A651-5E334C81777F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModularPipelines.Powershell", "ModularPipelines.Powershell\ModularPipelines.Powershell.csproj", "{2EF4CDCF-8B7A-4710-B1E7-8879870D4B4A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModularPipelines.Cmd", "ModularPipelines.Cmd\ModularPipelines.Cmd.csproj", "{BC8D3D41-EFC5-45BF-9A1F-766B3989BA4E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModularPipelines.Azure.Pipelines", "ModularPipelines.Azure.Pipelines\ModularPipelines.Azure.Pipelines.csproj", "{D927ABA0-0DEC-4370-99C7-2CC2B8B3992E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModularPipelines.Analyzers", "ModularPipelines.Analyzers\ModularPipelines.Analyzers\ModularPipelines.Analyzers.csproj", "{C52A9D71-DE18-4923-A241-C15E4283C49F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModularPipelines.Analyzers.CodeFixes", "ModularPipelines.Analyzers\ModularPipelines.Analyzers.CodeFixes\ModularPipelines.Analyzers.CodeFixes.csproj", "{BC7E8359-E91B-4D06-A2DF-9A7F7D5481F0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModularPipelines.Analyzers.Package", "ModularPipelines.Analyzers\ModularPipelines.Analyzers.Package\ModularPipelines.Analyzers.Package.csproj", "{424DC6AE-7293-4959-9A89-771FF67610A2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModularPipelines.Analyzers.Test", "ModularPipelines.Analyzers\ModularPipelines.Analyzers.Test\ModularPipelines.Analyzers.Test.csproj", "{B8865C27-A2D9-44ED-9069-E9E509486EBE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModularPipelines.Analyzers.Vsix", "ModularPipelines.Analyzers\ModularPipelines.Analyzers.Vsix\ModularPipelines.Analyzers.Vsix.csproj", "{10E4D765-AD2F-4E95-A77C-0251FAA3E05F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Analyzers", "Analyzers", "{E6E0E7C4-D3F7-4A55-8950-28F0308EB8C6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModularPipelines.Docker", "ModularPipelines.Docker\ModularPipelines.Docker.csproj", "{D275C61D-E0DF-4D76-B70A-F49EB277E451}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -50,21 +70,66 @@ Global {DA86F082-5143-40C2-8EFA-F124D68FCE02}.Debug|Any CPU.Build.0 = Debug|Any CPU {DA86F082-5143-40C2-8EFA-F124D68FCE02}.Release|Any CPU.ActiveCfg = Release|Any CPU {DA86F082-5143-40C2-8EFA-F124D68FCE02}.Release|Any CPU.Build.0 = Release|Any CPU - {25F82D0E-84EF-40F1-8059-8BBC447BCB16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {25F82D0E-84EF-40F1-8059-8BBC447BCB16}.Debug|Any CPU.Build.0 = Debug|Any CPU - {25F82D0E-84EF-40F1-8059-8BBC447BCB16}.Release|Any CPU.ActiveCfg = Release|Any CPU - {25F82D0E-84EF-40F1-8059-8BBC447BCB16}.Release|Any CPU.Build.0 = Release|Any CPU {FFBA91D0-9DC8-48A7-BD8E-6EF2E436F0AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FFBA91D0-9DC8-48A7-BD8E-6EF2E436F0AF}.Debug|Any CPU.Build.0 = Debug|Any CPU {FFBA91D0-9DC8-48A7-BD8E-6EF2E436F0AF}.Release|Any CPU.ActiveCfg = Release|Any CPU {FFBA91D0-9DC8-48A7-BD8E-6EF2E436F0AF}.Release|Any CPU.Build.0 = Release|Any CPU - {D23FE24C-9416-4ABD-84BA-49720582D8A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D23FE24C-9416-4ABD-84BA-49720582D8A1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D23FE24C-9416-4ABD-84BA-49720582D8A1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D23FE24C-9416-4ABD-84BA-49720582D8A1}.Release|Any CPU.Build.0 = Release|Any CPU {8A6B4058-2863-4406-A651-5E334C81777F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8A6B4058-2863-4406-A651-5E334C81777F}.Debug|Any CPU.Build.0 = Debug|Any CPU {8A6B4058-2863-4406-A651-5E334C81777F}.Release|Any CPU.ActiveCfg = Release|Any CPU {8A6B4058-2863-4406-A651-5E334C81777F}.Release|Any CPU.Build.0 = Release|Any CPU + {1C075CAF-7409-4941-8622-E3D3DDD876AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C075CAF-7409-4941-8622-E3D3DDD876AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C075CAF-7409-4941-8622-E3D3DDD876AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C075CAF-7409-4941-8622-E3D3DDD876AB}.Release|Any CPU.Build.0 = Release|Any CPU + {2EF4CDCF-8B7A-4710-B1E7-8879870D4B4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2EF4CDCF-8B7A-4710-B1E7-8879870D4B4A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2EF4CDCF-8B7A-4710-B1E7-8879870D4B4A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2EF4CDCF-8B7A-4710-B1E7-8879870D4B4A}.Release|Any CPU.Build.0 = Release|Any CPU + {BC8D3D41-EFC5-45BF-9A1F-766B3989BA4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC8D3D41-EFC5-45BF-9A1F-766B3989BA4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC8D3D41-EFC5-45BF-9A1F-766B3989BA4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC8D3D41-EFC5-45BF-9A1F-766B3989BA4E}.Release|Any CPU.Build.0 = Release|Any CPU + {D927ABA0-0DEC-4370-99C7-2CC2B8B3992E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D927ABA0-0DEC-4370-99C7-2CC2B8B3992E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D927ABA0-0DEC-4370-99C7-2CC2B8B3992E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D927ABA0-0DEC-4370-99C7-2CC2B8B3992E}.Release|Any CPU.Build.0 = Release|Any CPU + {C52A9D71-DE18-4923-A241-C15E4283C49F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C52A9D71-DE18-4923-A241-C15E4283C49F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C52A9D71-DE18-4923-A241-C15E4283C49F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C52A9D71-DE18-4923-A241-C15E4283C49F}.Release|Any CPU.Build.0 = Release|Any CPU + {BC7E8359-E91B-4D06-A2DF-9A7F7D5481F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC7E8359-E91B-4D06-A2DF-9A7F7D5481F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC7E8359-E91B-4D06-A2DF-9A7F7D5481F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC7E8359-E91B-4D06-A2DF-9A7F7D5481F0}.Release|Any CPU.Build.0 = Release|Any CPU + {424DC6AE-7293-4959-9A89-771FF67610A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {424DC6AE-7293-4959-9A89-771FF67610A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {424DC6AE-7293-4959-9A89-771FF67610A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {424DC6AE-7293-4959-9A89-771FF67610A2}.Release|Any CPU.Build.0 = Release|Any CPU + {B8865C27-A2D9-44ED-9069-E9E509486EBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8865C27-A2D9-44ED-9069-E9E509486EBE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8865C27-A2D9-44ED-9069-E9E509486EBE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8865C27-A2D9-44ED-9069-E9E509486EBE}.Release|Any CPU.Build.0 = Release|Any CPU + {10E4D765-AD2F-4E95-A77C-0251FAA3E05F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10E4D765-AD2F-4E95-A77C-0251FAA3E05F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10E4D765-AD2F-4E95-A77C-0251FAA3E05F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10E4D765-AD2F-4E95-A77C-0251FAA3E05F}.Release|Any CPU.Build.0 = Release|Any CPU + {D275C61D-E0DF-4D76-B70A-F49EB277E451}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D275C61D-E0DF-4D76-B70A-F49EB277E451}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D275C61D-E0DF-4D76-B70A-F49EB277E451}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D275C61D-E0DF-4D76-B70A-F49EB277E451}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C52A9D71-DE18-4923-A241-C15E4283C49F} = {E6E0E7C4-D3F7-4A55-8950-28F0308EB8C6} + {BC7E8359-E91B-4D06-A2DF-9A7F7D5481F0} = {E6E0E7C4-D3F7-4A55-8950-28F0308EB8C6} + {424DC6AE-7293-4959-9A89-771FF67610A2} = {E6E0E7C4-D3F7-4A55-8950-28F0308EB8C6} + {B8865C27-A2D9-44ED-9069-E9E509486EBE} = {E6E0E7C4-D3F7-4A55-8950-28F0308EB8C6} + {10E4D765-AD2F-4E95-A77C-0251FAA3E05F} = {E6E0E7C4-D3F7-4A55-8950-28F0308EB8C6} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A5905A5D-B4E1-4A7A-9279-0283D86A9F7F} EndGlobalSection EndGlobal diff --git a/ModularPipelines/Attributes/DependsOnAttribute.cs b/ModularPipelines/Attributes/DependsOnAttribute.cs index 53811b6c96..c2d197f325 100644 --- a/ModularPipelines/Attributes/DependsOnAttribute.cs +++ b/ModularPipelines/Attributes/DependsOnAttribute.cs @@ -9,7 +9,7 @@ public class DependsOnAttribute : Attribute public DependsOnAttribute(Type type) { - if (!type.IsAssignableTo(typeof(IModule))) + if (!type.IsAssignableTo(typeof(ModuleBase))) { throw new Exception($"{type.FullName} is not a Module class"); } @@ -19,7 +19,7 @@ public DependsOnAttribute(Type type) } [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] -public class DependsOnAttribute : DependsOnAttribute where TModule : IModule +public class DependsOnAttribute : DependsOnAttribute where TModule : ModuleBase { public DependsOnAttribute() : base(typeof(TModule)) { diff --git a/ModularPipelines/Context/Base64.cs b/ModularPipelines/Context/Base64.cs new file mode 100644 index 0000000000..cda420e798 --- /dev/null +++ b/ModularPipelines/Context/Base64.cs @@ -0,0 +1,23 @@ +using System.Text; + +namespace ModularPipelines.Context; + +public class Base64 : IBase64 +{ + public string ToBase64String(string input, Encoding encoding) + { + var bytes = encoding.GetBytes(input); + return ToBase64String(bytes); + } + + public string ToBase64String(byte[] bytes) + { + return Convert.ToBase64String(bytes); + } + + public string FromBase64String(string base64Input, Encoding encoding) + { + var bytes = Convert.FromBase64String(base64Input); + return encoding.GetString(bytes); + } +} \ No newline at end of file diff --git a/ModularPipelines/Context/Certificates.cs b/ModularPipelines/Context/Certificates.cs new file mode 100644 index 0000000000..55097d676e --- /dev/null +++ b/ModularPipelines/Context/Certificates.cs @@ -0,0 +1,31 @@ +using System.Security.Cryptography.X509Certificates; + +namespace ModularPipelines.Context; + +public class Certificates : ICertificates +{ + public X509Certificate2? GetCertificateBySubject(StoreLocation storeLocation, string subject) + { + return GetCertificateBy(storeLocation, X509FindType.FindBySubjectName, subject); + } + + public X509Certificate2? GetCertificateByThumbprint(StoreLocation storeLocation, string thumbprint) + { + return GetCertificateBy(storeLocation, X509FindType.FindByThumbprint, thumbprint); + } + + public X509Certificate2? GetCertificateBySerialNumber(StoreLocation storeLocation, string serialNumber) + { + return GetCertificateBy(storeLocation, X509FindType.FindBySerialNumber, serialNumber); + } + + public X509Certificate2? GetCertificateBy(StoreLocation storeLocation, X509FindType findType, string findValue) + { + using var store = new X509Store(storeLocation); + store.Open(OpenFlags.ReadOnly); + + var certificate2Collection = store.Certificates.Find(findType, findValue, false); + + return certificate2Collection.FirstOrDefault(); + } +} \ No newline at end of file diff --git a/ModularPipelines/Context/Command.cs b/ModularPipelines/Context/Command.cs new file mode 100644 index 0000000000..d87fef0292 --- /dev/null +++ b/ModularPipelines/Context/Command.cs @@ -0,0 +1,73 @@ +using System.Collections.ObjectModel; +using CliWrap; +using CliWrap.Buffered; +using Microsoft.Extensions.Logging; +using ModularPipelines.Exceptions; +using ModularPipelines.Helpers; +using ModularPipelines.Options; + +namespace ModularPipelines.Context; + +internal class Command : ICommand +{ + private readonly IModuleLoggerProvider _moduleLoggerProvider; + private ILogger Logger => _moduleLoggerProvider.Logger; + + public Command(IModuleLoggerProvider moduleLoggerProvider) + { + _moduleLoggerProvider = moduleLoggerProvider; + } + + public async Task UsingCommandLineTool(CommandLineToolOptions options, CancellationToken cancellationToken = default) + { + var parsedArgs = string.Equals(options.Arguments?.ElementAtOrDefault(0), options.Tool) + ? options.Arguments?.Skip(1) : options.Arguments; + + var command = Cli.Wrap(options.Tool) + .WithArguments(parsedArgs ?? Array.Empty()); + + if (options.WorkingDirectory != null) + { + command = command.WithWorkingDirectory(options.WorkingDirectory); + } + + if (options.EnvironmentVariables != null) + { + command = command.WithEnvironmentVariables(new ReadOnlyDictionary(options.EnvironmentVariables)); + } + + if (options.LogInput) + { + Logger.LogInformation("---Executing Command---\r\n{Input}", command.ToString()); + } + + var result = await Of(command, cancellationToken); + + if (options.LogOutput) + { + Logger.LogInformation("---Command Result---\r\n{Output}", + string.IsNullOrEmpty(result.StandardError) + ? result.StandardOutput + : result.StandardError); + } + + return result; + } + + public async Task Of(CliWrap.Command command, CancellationToken cancellationToken = default) + { + var result = await command + .WithValidation(CommandResultValidation.None) + .ExecuteBufferedAsync(cancellationToken); + + if (result.ExitCode != 0) + { + var input = command.ToString(); + throw new CommandException(input, result); + } + + return result; + } + + +} \ No newline at end of file diff --git a/ModularPipelines/Context/Downloader.cs b/ModularPipelines/Context/Downloader.cs new file mode 100644 index 0000000000..99c65b2be6 --- /dev/null +++ b/ModularPipelines/Context/Downloader.cs @@ -0,0 +1,71 @@ +using Microsoft.Extensions.Logging; +using ModularPipelines.Helpers; +using ModularPipelines.Options; +using File = ModularPipelines.FileSystem.File; + +namespace ModularPipelines.Context; + +internal class Downloader : IDownloader +{ + private readonly IModuleLoggerProvider _moduleLoggerProvider; + private readonly HttpClient _defaultHttpClient; + + public Downloader(IModuleLoggerProvider moduleLoggerProvider, HttpClient defaultHttpClient) + { + _moduleLoggerProvider = moduleLoggerProvider; + _defaultHttpClient = defaultHttpClient; + } + + public async Task DownloadFileAsync(DownloadOptions options, CancellationToken cancellationToken = default) + { + var request = new HttpRequestMessage(HttpMethod.Get, options.DownloadUri); + + options.RequestConfigurator?.Invoke(request); + + var response = await (options.HttpClient ?? _defaultHttpClient).GetAsync(options.DownloadUri, cancellationToken); + + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); + + var filePathToSave = GetSaveLocation(options); + + if (!options.Overwrite && System.IO.File.Exists(filePathToSave)) + { + throw new IOException($"{filePathToSave} already exists and overwrite is false"); + } + + await using var newFile = System.IO.File.Create(filePathToSave); + + await stream.CopyToAsync(newFile, cancellationToken); + + _moduleLoggerProvider.Logger.LogInformation("File {Uri} downloaded to {SaveLocation}", options.DownloadUri, filePathToSave); + + return filePathToSave!; + } + + private string GetSaveLocation(DownloadOptions options) + { + if (string.IsNullOrWhiteSpace(options.SavePath)) + { + return Path.GetTempFileName() + Guid.NewGuid() + GetExtension(options.DownloadUri.AbsoluteUri); + } + + if (Path.HasExtension(options.SavePath)) + { + Directory.CreateDirectory(new FileInfo(options.SavePath).Directory!.FullName); + return options.SavePath; + } + + Directory.CreateDirectory(options.SavePath); + return Path.Combine(options.SavePath, Guid.NewGuid() + GetExtension(options.DownloadUri.AbsoluteUri)); + } + + private string GetExtension(string downloadUri) + { + if (Path.HasExtension(downloadUri)) + { + return Path.GetExtension(downloadUri); + } + + return string.Empty; + } +} \ No newline at end of file diff --git a/ModularPipelines/Context/EnvironmentContext.cs b/ModularPipelines/Context/EnvironmentContext.cs index 51424dcf93..2bc0933a69 100644 --- a/ModularPipelines/Context/EnvironmentContext.cs +++ b/ModularPipelines/Context/EnvironmentContext.cs @@ -13,32 +13,31 @@ public class EnvironmentContext : IEnvironmentContext, IInitializer private readonly ILogger _logger; private readonly IHostEnvironment _hostEnvironment; - public EnvironmentContext(ILogger logger, IHostEnvironment hostEnvironment) + public EnvironmentContext(ILogger logger, + IHostEnvironment hostEnvironment, + IEnvironmentVariables environmentVariables) { _logger = logger; _hostEnvironment = hostEnvironment; - ContentDirectory = new(new DirectoryInfo(_hostEnvironment.ContentRootPath)); + EnvironmentVariables = environmentVariables; + ContentDirectory = _hostEnvironment.ContentRootPath!; } public string EnvironmentName => _hostEnvironment.EnvironmentName; public OperatingSystem OperatingSystem { get; } = Environment.OSVersion; public bool Is64BitOperatingSystem { get; } = Environment.Is64BitOperatingSystem; + public Folder AppDomainDirectory { get; } = AppDomain.CurrentDomain.BaseDirectory!; public Folder ContentDirectory { get; set; } - public Folder WorkingDirectory { get; set; } = new(new DirectoryInfo(Environment.CurrentDirectory)); + public Folder WorkingDirectory { get; set; } = Environment.CurrentDirectory!; public Folder? GitRootDirectory { get; set; } - - public string? GetEnvironmentVariable(string name) - { - return Environment.GetEnvironmentVariable(name); - } - public IDictionary GetEnvironmentVariables() + public Folder? GetFolder(Environment.SpecialFolder specialFolder) { - return Environment.GetEnvironmentVariables() - .Cast() - .ToDictionary(variable => variable.Key.ToString()!, variable => variable.Value!.ToString()!); + return Environment.GetFolderPath(specialFolder); } + public IEnvironmentVariables EnvironmentVariables { get; } + public async Task InitializeAsync() { var gitCommandOutput = await Cli.Wrap("git") diff --git a/ModularPipelines/Context/EnvironmentVariables.cs b/ModularPipelines/Context/EnvironmentVariables.cs new file mode 100644 index 0000000000..483236918b --- /dev/null +++ b/ModularPipelines/Context/EnvironmentVariables.cs @@ -0,0 +1,34 @@ +using System.Collections; + +namespace ModularPipelines.Context; + +public class EnvironmentVariables : IEnvironmentVariables +{ + public string? GetEnvironmentVariable(string name) + { + return Environment.GetEnvironmentVariable(name); + } + + public IDictionary GetEnvironmentVariables() + { + return Environment.GetEnvironmentVariables() + .Cast() + .ToDictionary(variable => variable.Key.ToString()!, variable => variable.Value!.ToString()!); + } + + public void SetEnvironmentVariable(string variableName, string value) + { + Environment.SetEnvironmentVariable(variableName, value); + } + + public void AddToPath(string pathToAdd) + { + const string pathVariableName = "PATH"; + + var oldValue = Environment.GetEnvironmentVariable(pathVariableName); + + var newValue = $@"{oldValue};{pathToAdd}"; + + Environment.SetEnvironmentVariable(pathVariableName, newValue); + } +} \ No newline at end of file diff --git a/ModularPipelines/Context/FileSystemContext.cs b/ModularPipelines/Context/FileSystemContext.cs index d10d026505..12d137267c 100644 --- a/ModularPipelines/Context/FileSystemContext.cs +++ b/ModularPipelines/Context/FileSystemContext.cs @@ -34,7 +34,8 @@ public void CopyFolder(string folderPath, string destinationFolder) public FileAttributes GetFileAttributes(string filePath) => System.IO.File.GetAttributes(filePath); public void SetFileAttributes(string filepath, FileAttributes attributes) => System.IO.File.SetAttributes(filepath, attributes); - + public File GetFile(string filePath) => new FileInfo(filePath); + public IEnumerable GetFiles(string folderPath, SearchOption searchOption) { return Directory.EnumerateFiles(folderPath, "*", searchOption).Select(filePath => new File(new FileInfo(filePath))); @@ -55,6 +56,8 @@ public IEnumerable GetFolders(string folderPath, SearchOption searchOpti return GetFolders(folderPath, searchOption).Where(predicate); } + public Folder GetFolder(string path) => new DirectoryInfo(path); + public Folder GetFolder(Environment.SpecialFolder specialFolder) { return new Folder(new DirectoryInfo(Environment.GetFolderPath(specialFolder))); diff --git a/ModularPipelines/Context/HashType.cs b/ModularPipelines/Context/HashType.cs new file mode 100644 index 0000000000..07a3755be5 --- /dev/null +++ b/ModularPipelines/Context/HashType.cs @@ -0,0 +1,7 @@ +namespace ModularPipelines.Context; + +public enum HashType +{ + Hex, + Base64 +} \ No newline at end of file diff --git a/ModularPipelines/Context/Hasher.cs b/ModularPipelines/Context/Hasher.cs new file mode 100644 index 0000000000..2c243a1622 --- /dev/null +++ b/ModularPipelines/Context/Hasher.cs @@ -0,0 +1,51 @@ +using System.Security.Cryptography; +using System.Text; + +namespace ModularPipelines.Context; + +public class Hasher : IHasher +{ + private readonly IHex _hex; + private readonly IBase64 _base64; + + public Hasher(IHex hex, IBase64 base64) + { + _hex = hex; + _base64 = base64; + } + + public string Sha1(string input, HashType hashType = HashType.Hex) + { + return ComputeHash(SHA1.Create(), input, hashType); + } + + public string Sha256(string input, HashType hashType = HashType.Hex) + { + return ComputeHash(SHA256.Create(), input, hashType); + } + + public string Sha384(string input, HashType hashType = HashType.Hex) + { + return ComputeHash(SHA384.Create(), input, hashType); + } + + public string Sha512(string input, HashType hashType = HashType.Hex) + { + return ComputeHash(SHA512.Create(), input, hashType); + } + + public string Md5(string input, HashType hashType = HashType.Hex) + { + return ComputeHash(MD5.Create(), input, hashType); + } + + private string ComputeHash(HashAlgorithm hashAlgorithm, string input, HashType hashType) + { + using (hashAlgorithm) + { + var bytes = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(input)); + + return hashType == HashType.Hex ? _hex.ToHex(bytes) : _base64.ToBase64String(bytes); + } + } +} \ No newline at end of file diff --git a/ModularPipelines/Context/Hex.cs b/ModularPipelines/Context/Hex.cs new file mode 100644 index 0000000000..ce6204ce26 --- /dev/null +++ b/ModularPipelines/Context/Hex.cs @@ -0,0 +1,39 @@ +using System.Text; + +namespace ModularPipelines.Context; + +public class Hex : IHex +{ + public string ToHex(string input, Encoding encoding) + { + var bytes = encoding.GetBytes(input); + + return ToHex(bytes); + } + + public string ToHex(IEnumerable bytes) + { + var builder = new StringBuilder(); + + foreach (var b in bytes) + { + builder.Append(b.ToString("x2")); + } + + return builder.ToString(); + } + + public string FromHex(string hexInput, Encoding encoding) + { + hexInput = hexInput.Replace("-", ""); + + var raw = new byte[hexInput.Length / 2]; + + for (var i = 0; i < raw.Length; i++) + { + raw[i] = Convert.ToByte(hexInput.Substring(i * 2, 2), 16); + } + + return encoding.GetString(raw); + } +} \ No newline at end of file diff --git a/ModularPipelines/Context/IBase64.cs b/ModularPipelines/Context/IBase64.cs new file mode 100644 index 0000000000..747f7bf726 --- /dev/null +++ b/ModularPipelines/Context/IBase64.cs @@ -0,0 +1,13 @@ +using System.Text; + +namespace ModularPipelines.Context; + +public interface IBase64 +{ + string ToBase64String(string input) => ToBase64String(input, Encoding.UTF8); + string ToBase64String(string input, Encoding encoding); + string ToBase64String(byte[] bytes); + + string FromBase64String(string base64Input) => FromBase64String(base64Input, Encoding.UTF8); + string FromBase64String(string base64Input, Encoding encoding); +} \ No newline at end of file diff --git a/ModularPipelines/Context/ICertificates.cs b/ModularPipelines/Context/ICertificates.cs new file mode 100644 index 0000000000..33200ed920 --- /dev/null +++ b/ModularPipelines/Context/ICertificates.cs @@ -0,0 +1,13 @@ +using System.Security.Cryptography.X509Certificates; + +namespace ModularPipelines.Context; + +public interface ICertificates +{ + public X509Certificate2? GetCertificateBySubject(StoreLocation storeLocation, string subject); + public X509Certificate2? GetCertificateByThumbprint(StoreLocation storeLocation, string thumbprint); + + public X509Certificate2? GetCertificateBySerialNumber(StoreLocation storeLocation, string serialNumber); + + X509Certificate2? GetCertificateBy(StoreLocation storeLocation, X509FindType findType, string findValue); +} \ No newline at end of file diff --git a/ModularPipelines.Command/ICommand.cs b/ModularPipelines/Context/ICommand.cs similarity index 51% rename from ModularPipelines.Command/ICommand.cs rename to ModularPipelines/Context/ICommand.cs index 3710cebeef..c67377a7e1 100644 --- a/ModularPipelines.Command/ICommand.cs +++ b/ModularPipelines/Context/ICommand.cs @@ -1,10 +1,9 @@ using CliWrap.Buffered; -using ModularPipelines.Command.Options; +using ModularPipelines.Options; -namespace ModularPipelines.Command; +namespace ModularPipelines.Context; public interface ICommand { - Task Of(CommandOptions options, CancellationToken cancellationToken = default); Task UsingCommandLineTool(CommandLineToolOptions options, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/ModularPipelines/Context/IDownloader.cs b/ModularPipelines/Context/IDownloader.cs new file mode 100644 index 0000000000..fe81f21ced --- /dev/null +++ b/ModularPipelines/Context/IDownloader.cs @@ -0,0 +1,9 @@ +using ModularPipelines.Options; +using File = ModularPipelines.FileSystem.File; + +namespace ModularPipelines.Context; + +public interface IDownloader +{ + public Task DownloadFileAsync(DownloadOptions options, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/ModularPipelines/Context/IEnvironmentContext.cs b/ModularPipelines/Context/IEnvironmentContext.cs index 24e7b931c6..c048531643 100644 --- a/ModularPipelines/Context/IEnvironmentContext.cs +++ b/ModularPipelines/Context/IEnvironmentContext.cs @@ -8,13 +8,12 @@ public interface IEnvironmentContext OperatingSystem OperatingSystem { get; } bool Is64BitOperatingSystem { get; } Folder WorkingDirectory { get; set; } + Folder AppDomainDirectory { get; } Folder ContentDirectory { get; set; } Folder? GitRootDirectory { get; set; } - Folder GetFolder(Environment.SpecialFolder specialFolder) => new(new DirectoryInfo(Environment.GetFolderPath(specialFolder))); + Folder? GetFolder(Environment.SpecialFolder specialFolder); - string? GetEnvironmentVariable(string name); - - IDictionary GetEnvironmentVariables(); + IEnvironmentVariables EnvironmentVariables { get; } } \ No newline at end of file diff --git a/ModularPipelines/Context/IEnvironmentVariables.cs b/ModularPipelines/Context/IEnvironmentVariables.cs new file mode 100644 index 0000000000..db7b1581f6 --- /dev/null +++ b/ModularPipelines/Context/IEnvironmentVariables.cs @@ -0,0 +1,12 @@ +namespace ModularPipelines.Context; + +public interface IEnvironmentVariables +{ + string? GetEnvironmentVariable(string name); + + IDictionary GetEnvironmentVariables(); + + void SetEnvironmentVariable(string variableName, string value); + + void AddToPath(string pathToAdd); +} \ No newline at end of file diff --git a/ModularPipelines/Context/IFileSystemContext.cs b/ModularPipelines/Context/IFileSystemContext.cs index f7bf91cf25..bc80f3f1ef 100644 --- a/ModularPipelines/Context/IFileSystemContext.cs +++ b/ModularPipelines/Context/IFileSystemContext.cs @@ -22,6 +22,7 @@ public interface IFileSystemContext FileAttributes GetFileAttributes(string filePath); void SetFileAttributes(string filepath, FileAttributes attributes); + File GetFile(string filePath); IEnumerable GetFiles(string folderPath, SearchOption searchOption); IEnumerable GetFiles(string folderPath, SearchOption searchOption, Func predicate); @@ -30,5 +31,6 @@ public interface IFileSystemContext IEnumerable GetFolders(string folderPath, SearchOption searchOption, Func predicate); + Folder GetFolder(string path); Folder GetFolder(Environment.SpecialFolder specialFolder); } \ No newline at end of file diff --git a/ModularPipelines/Context/IHasher.cs b/ModularPipelines/Context/IHasher.cs new file mode 100644 index 0000000000..b71c583969 --- /dev/null +++ b/ModularPipelines/Context/IHasher.cs @@ -0,0 +1,10 @@ +namespace ModularPipelines.Context; + +public interface IHasher +{ + string Sha1(string input, HashType hashType = HashType.Hex); + string Sha256(string input, HashType hashType = HashType.Hex); + string Sha384(string input, HashType hashType = HashType.Hex); + string Sha512(string input, HashType hashType = HashType.Hex); + string Md5(string input, HashType hashType = HashType.Hex); +} \ No newline at end of file diff --git a/ModularPipelines/Context/IHex.cs b/ModularPipelines/Context/IHex.cs new file mode 100644 index 0000000000..a05376ade3 --- /dev/null +++ b/ModularPipelines/Context/IHex.cs @@ -0,0 +1,13 @@ +using System.Text; + +namespace ModularPipelines.Context; + +public interface IHex +{ + string ToHex(string input) => ToHex(input, Encoding.UTF8); + string ToHex(string input, Encoding encoding); + string ToHex(IEnumerable bytes); + + string FromHex(string hexInput) => FromHex(hexInput, Encoding.UTF8); + string FromHex(string hexInput, Encoding encoding); +} \ No newline at end of file diff --git a/ModularPipelines/Context/IInstaller.cs b/ModularPipelines/Context/IInstaller.cs new file mode 100644 index 0000000000..b1765f6e2f --- /dev/null +++ b/ModularPipelines/Context/IInstaller.cs @@ -0,0 +1,11 @@ +using CliWrap.Buffered; +using ModularPipelines.Options; + +namespace ModularPipelines.Context; + +public interface IInstaller +{ + Task InstallFromFileAsync(InstallerOptions options, CancellationToken cancellationToken = default); + Task InstallFromWebAsync(WebInstallerOptions options, + CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/ModularPipelines/Context/IJson.cs b/ModularPipelines/Context/IJson.cs new file mode 100644 index 0000000000..9f3dbe2094 --- /dev/null +++ b/ModularPipelines/Context/IJson.cs @@ -0,0 +1,12 @@ +using System.Text.Json; + +namespace ModularPipelines.Context; + +public interface IJson +{ + string ToJson(T input); + string ToJson(T input, JsonSerializerOptions options); + + T? FromJson(string input); + T? FromJson(string input, JsonSerializerOptions options); +} \ No newline at end of file diff --git a/ModularPipelines/Context/IModuleContext.cs b/ModularPipelines/Context/IModuleContext.cs index f90ea5a2e8..8db36fe520 100644 --- a/ModularPipelines/Context/IModuleContext.cs +++ b/ModularPipelines/Context/IModuleContext.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using ModularPipelines.Engine; using ModularPipelines.Helpers; using ModularPipelines.Modules; using ModularPipelines.Options; @@ -9,14 +10,29 @@ namespace ModularPipelines.Context; public interface IModuleContext { - internal TModule GetModule() where TModule : IModule; - internal IModule GetModule(Type type); + internal EngineCancellationToken EngineCancellationToken { get; } + internal TModule GetModule() where TModule : ModuleBase; + internal ModuleBase GetModule(Type type); public IServiceProvider ServiceProvider { get; } - public ILogger Logger { get; } public IConfiguration Configuration { get; } public IOptions PipelineOptions { get; } internal IDependencyCollisionDetector DependencyCollisionDetector { get; } + internal IModuleResultRepository ModuleResultRepository { get; } + public T? Get(); + public ILogger Logger { get; } + + #region Helpers + public IEnvironmentContext Environment { get; } public IFileSystemContext FileSystem { get; } - public T? Get(); + public ICommand Command { get; } + public IInstaller Installer { get; } + public IZip Zip { get; } + public IHex Hex { get; } + public IBase64 Base64 { get; } + public IHasher Hasher { get; } + public IJson Json { get; } + public IXml Xml { get; } + + #endregion } \ No newline at end of file diff --git a/ModularPipelines/Context/IXml.cs b/ModularPipelines/Context/IXml.cs new file mode 100644 index 0000000000..4acf221293 --- /dev/null +++ b/ModularPipelines/Context/IXml.cs @@ -0,0 +1,12 @@ +using System.Xml.Linq; + +namespace ModularPipelines.Context; + +public interface IXml +{ + string ToXml(T input, SaveOptions options = SaveOptions.None); + + T? FromXml(string input, LoadOptions options = LoadOptions.PreserveWhitespace) where T : class; + + T? FromXml(XElement element, LoadOptions options = LoadOptions.PreserveWhitespace) where T : class; +} \ No newline at end of file diff --git a/ModularPipelines/Context/IYaml.cs b/ModularPipelines/Context/IYaml.cs new file mode 100644 index 0000000000..2be0aed58c --- /dev/null +++ b/ModularPipelines/Context/IYaml.cs @@ -0,0 +1,13 @@ +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace ModularPipelines.Context; + +public interface IYaml +{ + string ToYaml(T input) => ToYaml(input, CamelCaseNamingConvention.Instance); + string ToYaml(T input, INamingConvention namingConvention); + + T FromYaml(string input) => FromYaml(input, CamelCaseNamingConvention.Instance); + T FromYaml(string input, INamingConvention namingConvention); +} \ No newline at end of file diff --git a/ModularPipelines/Context/IZip.cs b/ModularPipelines/Context/IZip.cs new file mode 100644 index 0000000000..26649f583b --- /dev/null +++ b/ModularPipelines/Context/IZip.cs @@ -0,0 +1,12 @@ +using System.IO.Compression; +using ModularPipelines.FileSystem; + +namespace ModularPipelines.Context; + +public interface IZip +{ + public void ZipFolder(Folder folder, string outputPath) => ZipFolder(folder, outputPath, CompressionLevel.Optimal); + public void ZipFolder(Folder folder, string outputPath, CompressionLevel compressionLevel); + + public void UnZipToFolder(string zipPath, string outputFolderPath); +} \ No newline at end of file diff --git a/ModularPipelines/Context/Installer.cs b/ModularPipelines/Context/Installer.cs new file mode 100644 index 0000000000..6def0149f7 --- /dev/null +++ b/ModularPipelines/Context/Installer.cs @@ -0,0 +1,38 @@ +using CliWrap.Buffered; +using ModularPipelines.Options; + +namespace ModularPipelines.Context; + +public class Installer : IInstaller +{ + private readonly ICommand _command; + private readonly IDownloader _downloader; + + public Installer(ICommand command, IDownloader downloader) + { + _command = command; + _downloader = downloader; + } + + public Task InstallFromFileAsync(InstallerOptions options, + CancellationToken cancellationToken = default) + { + return _command.UsingCommandLineTool(new CommandLineToolOptions(options.Path) + { + Arguments = options.Arguments ?? Array.Empty() + }, cancellationToken); + } + + public async Task InstallFromWebAsync(WebInstallerOptions options, + CancellationToken cancellationToken = default) + { + var file = await _downloader.DownloadFileAsync(new DownloadOptions(options.DownloadUri), cancellationToken); + + return await InstallFromFileAsync(new InstallerOptions(file.Path) + { + Arguments = options.Arguments + }, cancellationToken); + } + + +} \ No newline at end of file diff --git a/ModularPipelines/Context/Json.cs b/ModularPipelines/Context/Json.cs new file mode 100644 index 0000000000..6bf33ce8f6 --- /dev/null +++ b/ModularPipelines/Context/Json.cs @@ -0,0 +1,26 @@ +using System.Text.Json; + +namespace ModularPipelines.Context; + +public class Json : IJson +{ + public string ToJson(T input) + { + return JsonSerializer.Serialize(input); + } + + public string ToJson(T input, JsonSerializerOptions options) + { + return JsonSerializer.Serialize(input, options); + } + + public T? FromJson(string input) + { + return JsonSerializer.Deserialize(input); + } + + public T? FromJson(string input, JsonSerializerOptions options) + { + return JsonSerializer.Deserialize(input, options); + } +} \ No newline at end of file diff --git a/ModularPipelines/Context/ModuleContext.cs b/ModularPipelines/Context/ModuleContext.cs index e7eee38ce9..5b244541de 100644 --- a/ModularPipelines/Context/ModuleContext.cs +++ b/ModularPipelines/Context/ModuleContext.cs @@ -1,22 +1,22 @@ -using System.Collections.Concurrent; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using ModularPipelines.Engine; using ModularPipelines.Helpers; using ModularPipelines.Modules; using ModularPipelines.Options; +// ReSharper disable SuggestBaseTypeForParameterInConstructor namespace ModularPipelines.Context; internal class ModuleContext : IModuleContext { - private readonly ILogger _logger; - private readonly ConcurrentDictionary _resolvedInstances = new(); + private readonly IModuleLoggerProvider _moduleLoggerProvider; - public IServiceProvider ServiceProvider { get; } + public ILogger Logger => _moduleLoggerProvider.Logger; - public ILogger Logger => _logger; + public IServiceProvider ServiceProvider { get; } public IConfiguration Configuration { get; } @@ -26,6 +26,16 @@ internal class ModuleContext : IModuleContext public IEnvironmentContext Environment { get; } + public IHasher Hasher { get; } + public IJson Json { get; } + public IXml Xml { get; } + public IModuleResultRepository ModuleResultRepository { get; } + public ICommand Command { get; } + public IInstaller Installer { get; } + public IZip Zip { get; } + public IHex Hex { get; } + public IBase64 Base64 { get; } + public T Get() { return (T) ServiceProvider.GetRequiredService(typeof(T)); @@ -37,11 +47,27 @@ public ModuleContext(IServiceProvider serviceProvider, IDependencyCollisionDetector dependencyCollisionDetector, IEnvironmentContext environment, IFileSystemContext fileSystem, - ILogger logger, IConfiguration configuration, - IOptions pipelineOptions) + IOptions pipelineOptions, + IModuleResultRepository moduleResultRepository, + ICommand command, + IModuleLoggerProvider moduleLoggerProvider, + IZip zip, + IHex hex, + IBase64 base64, + IHasher hasher, IJson json, IXml xml, EngineCancellationToken engineCancellationToken, IInstaller installer) { - _logger = logger; + _moduleLoggerProvider = moduleLoggerProvider; + Zip = zip; + Hex = hex; + Base64 = base64; + Hasher = hasher; + Json = json; + Xml = xml; + EngineCancellationToken = engineCancellationToken; + Installer = installer; + ModuleResultRepository = moduleResultRepository; + Command = command; Configuration = configuration; PipelineOptions = pipelineOptions; ServiceProvider = serviceProvider; @@ -50,13 +76,15 @@ public ModuleContext(IServiceProvider serviceProvider, FileSystem = fileSystem; } - public TModule GetModule() where TModule : IModule + public EngineCancellationToken EngineCancellationToken { get; } + + public TModule GetModule() where TModule : ModuleBase { - return ServiceProvider.GetServices().OfType().Single(); + return ServiceProvider.GetServices().OfType().Single(); } - public IModule GetModule(Type type) + public ModuleBase GetModule(Type type) { - return ServiceProvider.GetServices().Single(module => module.GetType() == type); + return ServiceProvider.GetServices().Single(module => module.GetType() == type); } } \ No newline at end of file diff --git a/ModularPipelines/Context/Xml.cs b/ModularPipelines/Context/Xml.cs new file mode 100644 index 0000000000..9b7c1334c0 --- /dev/null +++ b/ModularPipelines/Context/Xml.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; +using System.Xml.Serialization; + +namespace ModularPipelines.Context; + +public class Xml : IXml +{ + public string ToXml(T input, SaveOptions options = SaveOptions.None) + { + var serializer = new XmlSerializer(typeof(T)); + var document = new XDocument(); + + using (var writer = document.CreateWriter()) + { + serializer.Serialize(writer, input); + } + + return document.ToString(); + } + + public T? FromXml(string input, LoadOptions options = LoadOptions.PreserveWhitespace) where T : class + { + var document = XDocument.Parse(input, options); + + if (document.Root == null) + { + return default; + } + + return FromXml(document.Root, options); + } + + public T? FromXml(XElement element, LoadOptions options = LoadOptions.PreserveWhitespace) where T : class + { + using var reader = element.CreateReader(); + + var serializer = new XmlSerializer(typeof(T)); + + return serializer.Deserialize(reader) as T; + } +} \ No newline at end of file diff --git a/ModularPipelines/Context/Yaml.cs b/ModularPipelines/Context/Yaml.cs new file mode 100644 index 0000000000..ce87a2a573 --- /dev/null +++ b/ModularPipelines/Context/Yaml.cs @@ -0,0 +1,22 @@ +using YamlDotNet.Serialization; + +namespace ModularPipelines.Context; + +public class Yaml : IYaml +{ + public string ToYaml(T input, INamingConvention namingConvention) + { + return new SerializerBuilder() + .WithNamingConvention(namingConvention) + .Build() + .Serialize(input); + } + + public T FromYaml(string input, INamingConvention namingConvention) + { + return new DeserializerBuilder() + .WithNamingConvention(namingConvention) + .Build() + .Deserialize(input); + } +} \ No newline at end of file diff --git a/ModularPipelines/Context/Zip.cs b/ModularPipelines/Context/Zip.cs new file mode 100644 index 0000000000..3df116d21d --- /dev/null +++ b/ModularPipelines/Context/Zip.cs @@ -0,0 +1,17 @@ +using System.IO.Compression; +using ModularPipelines.FileSystem; + +namespace ModularPipelines.Context; + +public class Zip : IZip +{ + public void ZipFolder(Folder folder, string outputPath, CompressionLevel compressionLevel) + { + ZipFile.CreateFromDirectory(folder.Path, outputPath, compressionLevel, false); + } + + public void UnZipToFolder(string zipPath, string outputFolderPath) + { + ZipFile.ExtractToDirectory(zipPath, outputFolderPath, true); + } +} \ No newline at end of file diff --git a/ModularPipelines/Engine/DependencyDetector.cs b/ModularPipelines/Engine/DependencyDetector.cs new file mode 100644 index 0000000000..d6cb297ee5 --- /dev/null +++ b/ModularPipelines/Engine/DependencyDetector.cs @@ -0,0 +1,82 @@ +using System.Reflection; +using System.Text; +using Microsoft.Extensions.Logging; +using ModularPipelines.Attributes; +using ModularPipelines.Modules; + +namespace ModularPipelines; + +public class DependencyDetector : IDependencyDetector +{ + private readonly ILogger _logger; + public IReadOnlyList ModuleDependencyModels { get; } + + public DependencyDetector(IEnumerable modules, ILogger logger) + { + _logger = logger; + ModuleDependencyModels = Detect(modules.Select(x => new ModuleDependencyModel(x)).ToList()); + } + + public void Print() + { + var stringBuilder = new StringBuilder(); + + foreach (var moduleDependencyModel in ModuleDependencyModels) + { + stringBuilder.AppendLine(); + Print(stringBuilder, moduleDependencyModel, 1); + } + + _logger.LogInformation("The following dependency chains have been detected:\r\n{Chain}", stringBuilder.ToString()); + } + + private void Print(StringBuilder stringBuilder, ModuleDependencyModel moduleDependencyModel, int i) + { + stringBuilder.Append(new string('-', i)); + stringBuilder.Append(' '); + stringBuilder.AppendLine(moduleDependencyModel.Module.GetType().Name); + + foreach (var dependencyModel in moduleDependencyModel.IsDependencyFor) + { + Print(stringBuilder, dependencyModel, i+2); + } + } + + private List Detect(List allModules) + { + foreach (var moduleDependencyModel in allModules) + { + var dependencies = GetModuleDependencies(moduleDependencyModel, allModules).ToList(); + + moduleDependencyModel.IsDependentOn.AddRange(dependencies); + + foreach (var dependencyModel in dependencies) + { + dependencyModel.IsDependencyFor.Add(moduleDependencyModel); + } + } + + return allModules.Where(x => !x.IsDependentOn.Any()).ToList(); + } + + private IEnumerable GetModuleDependencies(ModuleDependencyModel moduleDependencyModel, IReadOnlyCollection allModules) + { + var customAttributes = moduleDependencyModel.Module.GetType().GetCustomAttributes(true); + + foreach (var dependsOnAttribute in customAttributes) + { + yield return GetModuleDependencyModel(dependsOnAttribute.Type, allModules); + } + } + + private ModuleDependencyModel GetModuleDependencyModel(Type type, IEnumerable allModules) + { + return allModules.First(x => x.Module.GetType() == type); + } +} + +public record ModuleDependencyModel(ModuleBase Module) +{ + public List IsDependencyFor { get; } = new(); + public List IsDependentOn { get; } = new(); +} \ No newline at end of file diff --git a/ModularPipelines/Engine/EngineCancellationToken.cs b/ModularPipelines/Engine/EngineCancellationToken.cs new file mode 100644 index 0000000000..317c9f2299 --- /dev/null +++ b/ModularPipelines/Engine/EngineCancellationToken.cs @@ -0,0 +1,5 @@ +namespace ModularPipelines.Engine; + +internal class EngineCancellationToken : CancellationTokenSource +{ +} \ No newline at end of file diff --git a/ModularPipelines/Engine/FileSystemModuleEstimatedTimeProvider.cs b/ModularPipelines/Engine/FileSystemModuleEstimatedTimeProvider.cs new file mode 100644 index 0000000000..89f8b4b884 --- /dev/null +++ b/ModularPipelines/Engine/FileSystemModuleEstimatedTimeProvider.cs @@ -0,0 +1,75 @@ +namespace ModularPipelines.Engine; + +internal class FileSystemModuleEstimatedTimeProvider : IModuleEstimatedTimeProvider +{ + private readonly string _directory; + + public FileSystemModuleEstimatedTimeProvider() + { + _directory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "ModularPipelines", "EstimatedTimes"); + } + + public async Task GetModuleEstimatedTimeAsync(Type moduleType) + { + var fileName = $"{moduleType.FullName}.txt"; + return await GetEstimatedTimeAsync(fileName); + } + + private async Task GetEstimatedTimeAsync(string fileName) + { + var path = Path.Combine(_directory, fileName); + + try + { + if (File.Exists(path)) + { + var contents = await File.ReadAllTextAsync(path); + return TimeSpan.Parse(contents); + } + } + catch + { + // Ignored + } + + // Some default fallback. We can't estimate for now so we'll estimate next time. + return TimeSpan.FromMinutes(2); + } + + public async Task SaveModuleTimeAsync(Type moduleType, TimeSpan duration) + { + var fileName = $"{moduleType.FullName}.txt"; + + await SaveModuleTimeAsync(duration, fileName); + } + + private async Task SaveModuleTimeAsync(TimeSpan duration, string fileName) + { + try + { + Directory.CreateDirectory(_directory); + + var path = Path.Combine(_directory, fileName); + + await File.WriteAllTextAsync(path, duration.ToString()); + } + catch + { + // Ignored + } + } + + public async Task GetSubModuleEstimatedTimeAsync(Type moduleType, string subModuleName) + { + var fileName = $"{moduleType.FullName}-{subModuleName}.txt"; + return await GetEstimatedTimeAsync(fileName); + } + + public async Task SaveSubModuleTimeAsync(Type moduleType, string subModuleName, TimeSpan duration) + { + var fileName = $"{moduleType.FullName}-{subModuleName}.txt"; + + await SaveModuleTimeAsync(duration, fileName); + } +} \ No newline at end of file diff --git a/ModularPipelines/Engine/IDependencyDetector.cs b/ModularPipelines/Engine/IDependencyDetector.cs new file mode 100644 index 0000000000..237828c0c0 --- /dev/null +++ b/ModularPipelines/Engine/IDependencyDetector.cs @@ -0,0 +1,7 @@ +namespace ModularPipelines; + +public interface IDependencyDetector +{ + IReadOnlyList ModuleDependencyModels { get; } + void Print(); +} \ No newline at end of file diff --git a/ModularPipelines/Engine/IModuleEstimatedTimeProvider.cs b/ModularPipelines/Engine/IModuleEstimatedTimeProvider.cs new file mode 100644 index 0000000000..b96f6ca496 --- /dev/null +++ b/ModularPipelines/Engine/IModuleEstimatedTimeProvider.cs @@ -0,0 +1,10 @@ +namespace ModularPipelines.Engine; + +public interface IModuleEstimatedTimeProvider +{ + Task GetModuleEstimatedTimeAsync(Type moduleType); + Task SaveModuleTimeAsync(Type moduleType, TimeSpan duration); + + Task GetSubModuleEstimatedTimeAsync(Type moduleType, string subModuleName); + Task SaveSubModuleTimeAsync(Type moduleType, string subModuleName, TimeSpan duration); +} \ No newline at end of file diff --git a/ModularPipelines/Engine/IModuleExecutor.cs b/ModularPipelines/Engine/IModuleExecutor.cs new file mode 100644 index 0000000000..38ab9ce430 --- /dev/null +++ b/ModularPipelines/Engine/IModuleExecutor.cs @@ -0,0 +1,8 @@ +using ModularPipelines.Modules; + +namespace ModularPipelines.Engine; + +public interface IModuleExecutor +{ + Task> ExecuteAsync(IEnumerable modules); +} \ No newline at end of file diff --git a/ModularPipelines/Engine/IModuleIgnoreHandler.cs b/ModularPipelines/Engine/IModuleIgnoreHandler.cs index 91ad2f955b..81e289c65f 100644 --- a/ModularPipelines/Engine/IModuleIgnoreHandler.cs +++ b/ModularPipelines/Engine/IModuleIgnoreHandler.cs @@ -2,7 +2,7 @@ namespace ModularPipelines.Engine; -public interface IModuleIgnoreHandler +internal interface IModuleIgnoreHandler { - bool ShouldIgnore(IModule module); + bool ShouldIgnore(ModuleBase module); } \ No newline at end of file diff --git a/ModularPipelines/Engine/IModuleInitializer.cs b/ModularPipelines/Engine/IModuleInitializer.cs new file mode 100644 index 0000000000..24423cfa15 --- /dev/null +++ b/ModularPipelines/Engine/IModuleInitializer.cs @@ -0,0 +1,8 @@ +using ModularPipelines.Modules; + +namespace ModularPipelines.Engine; + +internal interface IModuleInitializer +{ + ModuleBase Initialize(ModuleBase module); +} \ No newline at end of file diff --git a/ModularPipelines/Engine/IModuleResultRepository.cs b/ModularPipelines/Engine/IModuleResultRepository.cs new file mode 100644 index 0000000000..8246a245cb --- /dev/null +++ b/ModularPipelines/Engine/IModuleResultRepository.cs @@ -0,0 +1,11 @@ +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.Engine; + +public interface IModuleResultRepository +{ + Task PersistResultAsync(ModuleBase module, ModuleResult moduleResult); + + Task?> GetResultAsync(ModuleBase module); +} \ No newline at end of file diff --git a/ModularPipelines/Engine/IModuleRetriever.cs b/ModularPipelines/Engine/IModuleRetriever.cs new file mode 100644 index 0000000000..b921dd1242 --- /dev/null +++ b/ModularPipelines/Engine/IModuleRetriever.cs @@ -0,0 +1,8 @@ +using ModularPipelines.Models; + +namespace ModularPipelines.Engine; + +internal interface IModuleRetriever +{ + Task GetOrganizedModules(); +} \ No newline at end of file diff --git a/ModularPipelines/Engine/IPipelineExecutor.cs b/ModularPipelines/Engine/IPipelineExecutor.cs index badb7353a5..3e2488fc6f 100644 --- a/ModularPipelines/Engine/IPipelineExecutor.cs +++ b/ModularPipelines/Engine/IPipelineExecutor.cs @@ -4,5 +4,5 @@ namespace ModularPipelines.Engine; public interface IPipelineExecutor { - Task ExecuteAsync(); + Task> ExecuteAsync(); } \ No newline at end of file diff --git a/ModularPipelines/Engine/IPipelineSetupExecutor.cs b/ModularPipelines/Engine/IPipelineSetupExecutor.cs index c84c32ae74..cf23104081 100644 --- a/ModularPipelines/Engine/IPipelineSetupExecutor.cs +++ b/ModularPipelines/Engine/IPipelineSetupExecutor.cs @@ -2,11 +2,11 @@ namespace ModularPipelines.Engine; -public interface IPipelineSetupExecutor +internal interface IPipelineSetupExecutor { Task OnStartAsync(); - Task OnEndAsync(); + Task OnEndAsync(IReadOnlyList modules); - Task OnBeforeModuleStartAsync(IModule module); - Task OnAfterModuleEndAsync(IModule module); + Task OnBeforeModuleStartAsync(ModuleBase module); + Task OnAfterModuleEndAsync(ModuleBase module); } \ No newline at end of file diff --git a/ModularPipelines/Engine/IRequirementChecker.cs b/ModularPipelines/Engine/IRequirementChecker.cs index 0453adee41..3261dfce5d 100644 --- a/ModularPipelines/Engine/IRequirementChecker.cs +++ b/ModularPipelines/Engine/IRequirementChecker.cs @@ -1,6 +1,6 @@ namespace ModularPipelines.Engine; -public interface IRequirementChecker +internal interface IRequirementChecker { - Task CheckRequirements(); + Task CheckRequirementsAsync(); } \ No newline at end of file diff --git a/ModularPipelines/Engine/ModuleExecutor.cs b/ModularPipelines/Engine/ModuleExecutor.cs new file mode 100644 index 0000000000..6e2917a8ea --- /dev/null +++ b/ModularPipelines/Engine/ModuleExecutor.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.Options; +using ModularPipelines.Extensions; +using ModularPipelines.Modules; +using ModularPipelines.Options; + +namespace ModularPipelines.Engine; + +internal class ModuleExecutor : IModuleExecutor +{ + private readonly IPipelineSetupExecutor _pipelineSetupExecutor; + private readonly IOptions _pipelineOptions; + private readonly IModuleEstimatedTimeProvider _moduleEstimatedTimeProvider; + + public ModuleExecutor(IPipelineSetupExecutor pipelineSetupExecutor, + IOptions pipelineOptions, + IModuleEstimatedTimeProvider moduleEstimatedTimeProvider) + { + _pipelineSetupExecutor = pipelineSetupExecutor; + _pipelineOptions = pipelineOptions; + _moduleEstimatedTimeProvider = moduleEstimatedTimeProvider; + } + + public async Task> ExecuteAsync(IEnumerable modules) + { + var moduleTasks = modules.Select(ExecuteAsync).ToArray(); + + if (_pipelineOptions.Value.ExecutionMode == ExecutionMode.StopOnFirstException) + { + return await moduleTasks.WhenAllFailFast(); + } + + return await Task.WhenAll(moduleTasks); + } + + private async Task ExecuteAsync(ModuleBase module) + { + await _pipelineSetupExecutor.OnBeforeModuleStartAsync(module); + + await module.StartAsync(); + + await _moduleEstimatedTimeProvider.SaveModuleTimeAsync(module.GetType(), module.Duration); + + await _pipelineSetupExecutor.OnAfterModuleEndAsync(module); + + return module; + } +} \ No newline at end of file diff --git a/ModularPipelines/Engine/ModuleIgnoreHandler.cs b/ModularPipelines/Engine/ModuleIgnoreHandler.cs index e88fa90cfd..ae1e9f9bc9 100644 --- a/ModularPipelines/Engine/ModuleIgnoreHandler.cs +++ b/ModularPipelines/Engine/ModuleIgnoreHandler.cs @@ -6,7 +6,7 @@ namespace ModularPipelines.Engine; -public class ModuleIgnoreHandler : IModuleIgnoreHandler +internal class ModuleIgnoreHandler : IModuleIgnoreHandler { private readonly IOptions _pipelineOptions; @@ -15,12 +15,18 @@ public ModuleIgnoreHandler(IOptions pipelineOptions) _pipelineOptions = pipelineOptions; } - public bool ShouldIgnore(IModule module) + public bool ShouldIgnore(ModuleBase module) { - return module.ShouldSkip || IsIgnoreCategory(module) || !IsRunnableCategory(module); + if (IsIgnoreCategory(module) || !IsRunnableCategory(module)) + { + module.SetSkipped(); + return true; + } + + return false; } - private bool IsRunnableCategory(IModule module) + private bool IsRunnableCategory(ModuleBase module) { var runOnlyCategories = _pipelineOptions.Value.RunOnlyCategories?.ToArray(); @@ -34,7 +40,7 @@ private bool IsRunnableCategory(IModule module) return category != null && !runOnlyCategories.Contains(category.Category); } - private bool IsIgnoreCategory(IModule module) + private bool IsIgnoreCategory(ModuleBase module) { var ignoreCategories = _pipelineOptions.Value.IgnoreCategories?.ToArray(); diff --git a/ModularPipelines/Engine/ModuleInitializer.cs b/ModularPipelines/Engine/ModuleInitializer.cs new file mode 100644 index 0000000000..841e67366f --- /dev/null +++ b/ModularPipelines/Engine/ModuleInitializer.cs @@ -0,0 +1,19 @@ +using ModularPipelines.Context; +using ModularPipelines.Modules; + +namespace ModularPipelines.Engine; + +internal class ModuleInitializer : IModuleInitializer +{ + private readonly IModuleContext _moduleContext; + + public ModuleInitializer(IModuleContext moduleContext) + { + _moduleContext = moduleContext; + } + + public ModuleBase Initialize(ModuleBase module) + { + return module.Initialize(_moduleContext); + } +} \ No newline at end of file diff --git a/ModularPipelines/Engine/ModuleRetriever.cs b/ModularPipelines/Engine/ModuleRetriever.cs new file mode 100644 index 0000000000..eb4f394ed8 --- /dev/null +++ b/ModularPipelines/Engine/ModuleRetriever.cs @@ -0,0 +1,58 @@ +using ModularPipelines.Exceptions; +using ModularPipelines.Models; +using ModularPipelines.Modules; +using TomLonghurst.EnumerableAsyncProcessor.Extensions; + +namespace ModularPipelines.Engine; + +internal class ModuleRetriever : IModuleRetriever +{ + private readonly IModuleIgnoreHandler _moduleIgnoreHandler; + private readonly IModuleInitializer _moduleInitializer; + private readonly IModuleEstimatedTimeProvider _estimatedTimeProvider; + private readonly List _modules; + + public ModuleRetriever( + IModuleIgnoreHandler moduleIgnoreHandler, + IModuleInitializer moduleInitializer, + IEnumerable modules, + IModuleEstimatedTimeProvider estimatedTimeProvider + ) + { + _moduleIgnoreHandler = moduleIgnoreHandler; + _moduleInitializer = moduleInitializer; + _estimatedTimeProvider = estimatedTimeProvider; + _modules = modules.ToList(); + } + + public async Task GetOrganizedModules() + { + if (_modules.Count == 0) + { + throw new PipelineException("No modules have been registered"); + } + + _modules.ForEach(m => _moduleInitializer.Initialize(m)); + + var modulesToIgnore = _modules + .Where(m => _moduleIgnoreHandler.ShouldIgnore(m)) + .ToList(); + + var modulesToProcess = _modules + .Except(modulesToIgnore) + .ToList(); + + var runnableModulesWithEstimatatedDuration = await modulesToProcess.ToAsyncProcessorBuilder() + .SelectAsync(async module => + { + var estimatedTime = await _estimatedTimeProvider.GetModuleEstimatedTimeAsync(module.GetType()); + return new RunnableModule(module, estimatedTime); + }) + .ProcessInParallel(100, TimeSpan.FromSeconds(1)); + + return new OrganizedModules( + RunnableModules: runnableModulesWithEstimatatedDuration, + IgnoredModules: modulesToIgnore + ); + } +} \ No newline at end of file diff --git a/ModularPipelines/Engine/NoOpModuleResultRepository.cs b/ModularPipelines/Engine/NoOpModuleResultRepository.cs new file mode 100644 index 0000000000..5d051ec6d9 --- /dev/null +++ b/ModularPipelines/Engine/NoOpModuleResultRepository.cs @@ -0,0 +1,17 @@ +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace ModularPipelines.Engine; + +internal class NoOpModuleResultRepository : IModuleResultRepository +{ + public Task PersistResultAsync(ModuleBase module, ModuleResult moduleResult) + { + return Task.CompletedTask; + } + + public Task?> GetResultAsync(ModuleBase module) + { + return Task.FromResult?>(null); + } +} \ No newline at end of file diff --git a/ModularPipelines/Engine/PipelineExecutor.cs b/ModularPipelines/Engine/PipelineExecutor.cs index 3c8ea19f5f..3fcacbdc32 100644 --- a/ModularPipelines/Engine/PipelineExecutor.cs +++ b/ModularPipelines/Engine/PipelineExecutor.cs @@ -1,99 +1,111 @@ -using Microsoft.Extensions.Options; using ModularPipelines.Helpers; +using ModularPipelines.Models; using ModularPipelines.Modules; -using ModularPipelines.Options; -using Status = ModularPipelines.Enums.Status; namespace ModularPipelines.Engine; -public class PipelineExecutor : IPipelineExecutor +internal class PipelineExecutor : IPipelineExecutor { private readonly IPipelineSetupExecutor _pipelineSetupExecutor; - private readonly IModuleIgnoreHandler _moduleIgnoreHandler; private readonly IPipelineConsolePrinter _pipelineConsolePrinter; - private readonly IOptions _pipelineOptions; private readonly IRequirementChecker _requirementsChecker; - private readonly List _modules; + private readonly IModuleRetriever _moduleRetriever; + private readonly IModuleExecutor _moduleExecutor; + private readonly EngineCancellationToken _engineCancellationToken; + private readonly IDependencyDetector _dependencyDetector; + private readonly IDependencyCollisionDetector _dependencyCollisionDetector; public PipelineExecutor( IPipelineSetupExecutor pipelineSetupExecutor, - IModuleIgnoreHandler moduleIgnoreHandler, IPipelineConsolePrinter pipelineConsolePrinter, - IOptions pipelineOptions, - IEnumerable modules, - IRequirementChecker requirementsChecker) + IRequirementChecker requirementsChecker, + IModuleRetriever moduleRetriever, + IModuleExecutor moduleExecutor, + EngineCancellationToken engineCancellationToken, + IDependencyDetector dependencyDetector, + IDependencyCollisionDetector dependencyCollisionDetector) { _pipelineSetupExecutor = pipelineSetupExecutor; - _moduleIgnoreHandler = moduleIgnoreHandler; _pipelineConsolePrinter = pipelineConsolePrinter; - _pipelineOptions = pipelineOptions; _requirementsChecker = requirementsChecker; - _modules = modules.ToList(); + _moduleRetriever = moduleRetriever; + _moduleExecutor = moduleExecutor; + _engineCancellationToken = engineCancellationToken; + _dependencyDetector = dependencyDetector; + _dependencyCollisionDetector = dependencyCollisionDetector; } - public async Task ExecuteAsync() + public async Task> ExecuteAsync() { + _dependencyDetector.Print(); + + _dependencyCollisionDetector.CheckDependencies(); + await _pipelineSetupExecutor.OnStartAsync(); - await _requirementsChecker.CheckRequirements(); + await _requirementsChecker.CheckRequirementsAsync(); - var modulesToIgnore = _modules.Where(m => _moduleIgnoreHandler.ShouldIgnore(m)).ToList(); - - foreach (var module in modulesToIgnore) - { - module.Status = Status.Ignored; - } + var organizedModules = await _moduleRetriever.GetOrganizedModules(); - var modulesToProcess = _modules - .Except(modulesToIgnore) - .ToList(); - - _pipelineConsolePrinter.PrintProgress(modulesToProcess, modulesToIgnore); - - var moduleProcessingTasks = modulesToProcess - .Select(x => x.StartProcessingModule()) - .ToList(); + _pipelineConsolePrinter.PrintProgress(organizedModules, _engineCancellationToken.Token); + var runnableModules = organizedModules.RunnableModules.Select(x => x.Module).ToList(); + try { - if (_pipelineOptions.Value.StopOnFirstException) - { - while (moduleProcessingTasks.Any()) - { - var finished = await Task.WhenAny(moduleProcessingTasks); - moduleProcessingTasks.Remove(finished); - } - } - else - { - await Task.WhenAll(moduleProcessingTasks); - } + await _moduleExecutor.ExecuteAsync(runnableModules); + } + catch + { + // Give time for the console to update modules to Failed + await Task.Delay(100); + _engineCancellationToken.Cancel(); + throw; } finally { - await Dispose(modulesToProcess); + await WaitForAlwaysRunModules(runnableModules); + + await Dispose(runnableModules); + + await _pipelineSetupExecutor.OnEndAsync(organizedModules.AllModules); + + await Task.Delay(200); } - - await _pipelineSetupExecutor.OnEndAsync(); - await Task.Delay(200); + return organizedModules.AllModules; + } - return _modules.ToArray(); + private async Task WaitForAlwaysRunModules(IEnumerable runnableModules) + { + try + { + await Task.WhenAll(runnableModules.Where(m => m.ModuleRunType == ModuleRunType.AlwaysRun).Select(m => m.ResultTaskInternal)); + } + catch + { + // Ignored + } } - private async Task Dispose(List modulesToProcess) + private async Task Dispose(IEnumerable modulesToProcess) { foreach (var module in modulesToProcess) { - if (module is IAsyncDisposable asyncDisposable) - { - await asyncDisposable.DisposeAsync(); - } + await Dispose(module); + } + } + + private static async Task Dispose(ModuleBase module) + { + if (module is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } - if (module is IDisposable disposable) - { - disposable.Dispose(); - } + if (module is IDisposable disposable) + { + disposable.Dispose(); } } } \ No newline at end of file diff --git a/ModularPipelines/Engine/PipelineSetupExecutor.cs b/ModularPipelines/Engine/PipelineSetupExecutor.cs index 13bcbf3af7..c06ebc7e98 100644 --- a/ModularPipelines/Engine/PipelineSetupExecutor.cs +++ b/ModularPipelines/Engine/PipelineSetupExecutor.cs @@ -4,19 +4,19 @@ namespace ModularPipelines.Engine; -public class PipelineSetupExecutor : IPipelineSetupExecutor +internal class PipelineSetupExecutor : IPipelineSetupExecutor { private readonly IModuleContext _moduleContext; private readonly IEnumerable _globalHooks; private readonly IEnumerable _moduleHooks; - public PipelineSetupExecutor(IModuleContext moduleContext, - IEnumerable globalHooks, - IEnumerable moduleHooks) + public PipelineSetupExecutor(IEnumerable globalHooks, + IEnumerable moduleHooks, + IModuleContext moduleContext) { - _moduleContext = moduleContext; _globalHooks = globalHooks; _moduleHooks = moduleHooks; + _moduleContext = moduleContext; } public Task OnStartAsync() @@ -24,17 +24,17 @@ public Task OnStartAsync() return Task.WhenAll(_globalHooks.Select(x => x.OnStartAsync(_moduleContext))); } - public Task OnEndAsync() + public Task OnEndAsync(IReadOnlyList modules) { - return Task.WhenAll(_globalHooks.Select(x => x.OnEndAsync(_moduleContext))); + return Task.WhenAll(_globalHooks.Select(x => x.OnEndAsync(_moduleContext, modules))); } - public Task OnBeforeModuleStartAsync(IModule module) + public Task OnBeforeModuleStartAsync(ModuleBase module) { return Task.WhenAll(_moduleHooks.Select(x => x.OnBeforeModuleStartAsync(_moduleContext, module))); } - public Task OnAfterModuleEndAsync(IModule module) + public Task OnAfterModuleEndAsync(ModuleBase module) { return Task.WhenAll(_moduleHooks.Select(x => x.OnBeforeModuleEndAsync(_moduleContext, module))); } diff --git a/ModularPipelines/Engine/RequirementChecker.cs b/ModularPipelines/Engine/RequirementChecker.cs index 39592a57d6..37be037038 100644 --- a/ModularPipelines/Engine/RequirementChecker.cs +++ b/ModularPipelines/Engine/RequirementChecker.cs @@ -11,7 +11,7 @@ public RequirementChecker(IEnumerable requirements) { _requirements = requirements.ToList(); } - public async Task CheckRequirements() + public async Task CheckRequirementsAsync() { var failedRequirementsNames = new List(); diff --git a/ModularPipelines/Engine/ServiceContextRegistry.cs b/ModularPipelines/Engine/ServiceContextRegistry.cs new file mode 100644 index 0000000000..f66437c634 --- /dev/null +++ b/ModularPipelines/Engine/ServiceContextRegistry.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace ModularPipelines.Engine; + +public static class ServiceContextRegistry +{ + internal static readonly List> ContextRegistrationDelegates = new(); + + public static void RegisterContext(Action contextRegistrationDelegate) + { + ContextRegistrationDelegates.Add(contextRegistrationDelegate); + } +} \ No newline at end of file diff --git a/ModularPipelines/Exceptions/CommandException.cs b/ModularPipelines/Exceptions/CommandException.cs new file mode 100644 index 0000000000..ded707cbfc --- /dev/null +++ b/ModularPipelines/Exceptions/CommandException.cs @@ -0,0 +1,23 @@ +using CliWrap.Buffered; + +namespace ModularPipelines.Exceptions; + +public class CommandException : PipelineException +{ + public CommandException(string input, BufferedCommandResult bufferedCommandResult) : base(GenerateMessage(input, bufferedCommandResult)) + { + CommandResult = bufferedCommandResult; + } + + public BufferedCommandResult CommandResult { get; } + + private static string? GenerateMessage(string input, BufferedCommandResult bufferedCommandResult) + { + return $"Error: {GetOutput(bufferedCommandResult)}{Environment.NewLine}Exit Code: {bufferedCommandResult.ExitCode}{Environment.NewLine}Input: {input}"; + } + + private static string GetOutput(BufferedCommandResult bufferedCommandResult) + { + return !string.IsNullOrEmpty(bufferedCommandResult.StandardError) ? bufferedCommandResult.StandardError : bufferedCommandResult.StandardOutput; + } +} \ No newline at end of file diff --git a/ModularPipelines/Exceptions/DependsOnSkippedModuleException.cs b/ModularPipelines/Exceptions/DependsOnSkippedModuleException.cs new file mode 100644 index 0000000000..59a3db1d96 --- /dev/null +++ b/ModularPipelines/Exceptions/DependsOnSkippedModuleException.cs @@ -0,0 +1,10 @@ +using ModularPipelines.Modules; + +namespace ModularPipelines.Exceptions; + +public class DependsOnSkippedModuleException : Exception +{ + internal DependsOnSkippedModuleException(ModuleBase currentModule, ModuleBase dependencyModule) : base($"{currentModule.GetType().Name} depends on skipped module: {dependencyModule.GetType().Name}") + { + } +} \ No newline at end of file diff --git a/ModularPipelines/Exceptions/ModuleNotInitializedException.cs b/ModularPipelines/Exceptions/ModuleNotInitializedException.cs new file mode 100644 index 0000000000..f6db4e3159 --- /dev/null +++ b/ModularPipelines/Exceptions/ModuleNotInitializedException.cs @@ -0,0 +1,8 @@ +namespace ModularPipelines.Exceptions; + +internal class ModuleNotInitializedException : Exception +{ + public ModuleNotInitializedException(Type moduleType) : base($"Module {moduleType.Name} has not been initialized") + { + } +} \ No newline at end of file diff --git a/ModularPipelines/Extensions/CommandExtensions.cs b/ModularPipelines/Extensions/CommandExtensions.cs new file mode 100644 index 0000000000..0cf735b07d --- /dev/null +++ b/ModularPipelines/Extensions/CommandExtensions.cs @@ -0,0 +1,32 @@ +using ModularPipelines.Options; + +namespace ModularPipelines.Extensions; + +public static class CommandExtensions +{ + public static CommandLineToolOptions ToCommandLineToolOptions(this CommandEnvironmentOptions options, string tool, IEnumerable arguments) + { + return new CommandLineToolOptions(tool) + { + Arguments = arguments, + Credentials = options.Credentials, + EnvironmentVariables = options.EnvironmentVariables, + LogInput = options.LogInput, + LogOutput = options.LogOutput, + WorkingDirectory = options.WorkingDirectory + }; + } + + public static CommandLineToolOptions ToCommandLineToolOptions(this CommandLineToolOptions options, string tool, IEnumerable arguments) + { + return new CommandLineToolOptions(tool) + { + Arguments = arguments.Concat(options.Arguments ?? Array.Empty()), + Credentials = options.Credentials, + EnvironmentVariables = options.EnvironmentVariables, + LogInput = options.LogInput, + LogOutput = options.LogOutput, + WorkingDirectory = options.WorkingDirectory + }; + } +} \ No newline at end of file diff --git a/ModularPipelines/Extensions/ServiceCollectionExtensions.cs b/ModularPipelines/Extensions/ServiceCollectionExtensions.cs index e24545a8c9..de2cb72a44 100644 --- a/ModularPipelines/Extensions/ServiceCollectionExtensions.cs +++ b/ModularPipelines/Extensions/ServiceCollectionExtensions.cs @@ -8,19 +8,19 @@ namespace ModularPipelines.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddModule(this IServiceCollection services) where TModule : class, IModule + public static IServiceCollection AddModule(this IServiceCollection services) where TModule : ModuleBase { - return services.AddSingleton(); + return services.AddSingleton(); } - public static IServiceCollection AddModule(this IServiceCollection services, TModule tModule) where TModule : class, IModule + public static IServiceCollection AddModule(this IServiceCollection services, TModule tModule) where TModule : ModuleBase { - return services.AddSingleton(tModule); + return services.AddSingleton(tModule); } - public static IServiceCollection AddModule(this IServiceCollection services, Func tModuleFactory) where TModule : class, IModule + public static IServiceCollection AddModule(this IServiceCollection services, Func tModuleFactory) where TModule : ModuleBase { - return services.AddSingleton(tModuleFactory); + return services.AddSingleton(tModuleFactory); } public static IServiceCollection AddRequirement(this IServiceCollection services) where TRequirement : class, IPipelineRequirement @@ -46,13 +46,13 @@ public static IServiceCollection AddModulesFromAssemblyContainingType(this IS public static IServiceCollection AddModulesFromAssembly(this IServiceCollection services, Assembly assembly) { var modules = assembly.GetTypes() - .Where(type => type.IsAssignableTo(typeof(IModule))) + .Where(type => type.IsAssignableTo(typeof(ModuleBase))) .Where(type => type.IsClass) .Where(type => !type.IsAbstract); foreach (var module in modules) { - services.AddSingleton(typeof(IModule), module); + services.AddSingleton(typeof(ModuleBase), module); } return services; diff --git a/ModularPipelines/Extensions/StringExtensions.cs b/ModularPipelines/Extensions/StringExtensions.cs new file mode 100644 index 0000000000..aa3813e481 --- /dev/null +++ b/ModularPipelines/Extensions/StringExtensions.cs @@ -0,0 +1,65 @@ +namespace ModularPipelines.Extensions; + +public static class StringExtensions +{ + public static TCollection AddNonNullOrEmpty(this TCollection collection, string? argument) where TCollection : ICollection + { + if (!string.IsNullOrEmpty(argument)) + { + collection.Add(argument); + } + + return collection; + } + + public static TCollection AddRangeNonNullOrEmpty(this TCollection collection, IEnumerable? arguments) where TCollection : ICollection + { + foreach (var argument in arguments ?? Array.Empty()) + { + collection.AddNonNullOrEmpty(argument); + } + + return collection; + } + + public static TCollection AddNonNullOrEmptyArgumentWithPrefix(this TCollection collection, string argumentPrefix, string? argument) where TCollection : ICollection + { + if (!string.IsNullOrEmpty(argument)) + { + collection.Add($"{argumentPrefix}{argument}"); + } + + return collection; + } + + public static TCollection AddRangeNonNullOrEmptyArgumentWithPrefix(this TCollection collection, string argumentPrefix, IEnumerable? arguments) where TCollection : ICollection + { + foreach (var argument in arguments ?? Array.Empty()) + { + collection.AddNonNullOrEmptyArgumentWithPrefix(argumentPrefix, argument); + } + + return collection; + } + + public static TCollection AddNonNullOrEmptyArgumentWithSwitch(this TCollection collection, string argumentSwitch, string? argument) where TCollection : ICollection + { + if (!string.IsNullOrEmpty(argument)) + { + collection.Add(argumentSwitch); + collection.Add(argument); + } + + return collection; + } + + public static TCollection AddRangeNonNullOrEmptyArgumentWithSwitch(this TCollection collection, string argumentSwitch, IEnumerable? arguments) where TCollection : ICollection + { + foreach (var argument in arguments ?? Array.Empty()) + { + collection.AddNonNullOrEmptyArgumentWithSwitch(argumentSwitch, argument); + } + + return collection; + } +} \ No newline at end of file diff --git a/ModularPipelines/Extensions/TaskExtensions.cs b/ModularPipelines/Extensions/TaskExtensions.cs new file mode 100644 index 0000000000..f092601282 --- /dev/null +++ b/ModularPipelines/Extensions/TaskExtensions.cs @@ -0,0 +1,23 @@ +namespace ModularPipelines.Extensions; + +internal static class TaskExtensions +{ + public static async Task WhenAllFailFast(this ICollection> tasks) + { + var originalTasks = tasks.ToList(); + + tasks = tasks.ToList(); + + while (tasks.Any()) + { + var finished = await Task.WhenAny(tasks); + + // await to throw Exception if this Task errored + await finished; + + tasks.Remove(finished); + } + + return await Task.WhenAll(originalTasks); + } +} \ No newline at end of file diff --git a/ModularPipelines/FileSystem/File.cs b/ModularPipelines/FileSystem/File.cs index 6789d8a128..ae771ed254 100644 --- a/ModularPipelines/FileSystem/File.cs +++ b/ModularPipelines/FileSystem/File.cs @@ -1,19 +1,32 @@ +using System.Diagnostics.CodeAnalysis; + namespace ModularPipelines.FileSystem; public class File { private readonly FileInfo _fileInfo; + public File(string path) : this(new FileInfo(path)) + { + } + internal File(FileInfo fileInfo) { _fileInfo = fileInfo; } + public Task ReadAsync() + { + return System.IO.File.ReadAllTextAsync(Path); + } + public bool Exists => _fileInfo.Exists; + + public bool Hidden => (_fileInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden; public string Name => _fileInfo.Name; - public Folder? Folder => _fileInfo.Directory == null ? null : new Folder(_fileInfo.Directory); + public Folder? Folder => _fileInfo.Directory; public string Path => _fileInfo.FullName; @@ -30,4 +43,25 @@ internal File(FileInfo fileInfo) public void Delete() => _fileInfo.Delete(); public void MoveTo(string path) => _fileInfo.MoveTo(path); + + public static implicit operator File?(string? path) + { + if (string.IsNullOrEmpty(path)) + { + return null; + } + + return new FileInfo(path); + } + + [return: NotNullIfNotNull("fileInfo")] + public static implicit operator File?(FileInfo? fileInfo) + { + if (fileInfo == null) + { + return null; + } + + return new File(fileInfo); + } } \ No newline at end of file diff --git a/ModularPipelines/FileSystem/Folder.cs b/ModularPipelines/FileSystem/Folder.cs index f75101db1e..54876868c7 100644 --- a/ModularPipelines/FileSystem/Folder.cs +++ b/ModularPipelines/FileSystem/Folder.cs @@ -1,25 +1,33 @@ -namespace ModularPipelines.FileSystem; +using System.Diagnostics.CodeAnalysis; + +namespace ModularPipelines.FileSystem; public class Folder { private readonly DirectoryInfo _directoryInfo; + public Folder(string path) : this(new DirectoryInfo(path)) + { + } + internal Folder(DirectoryInfo directoryInfo) { _directoryInfo = directoryInfo; } public bool Exists => _directoryInfo.Exists; + + public bool Hidden => (_directoryInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden; public string Name => _directoryInfo.Name; - public Folder? Parent => _directoryInfo.Parent == null ? null : new Folder(_directoryInfo.Parent); + public Folder? Parent => _directoryInfo.Parent; public string Path => _directoryInfo.FullName; public FileAttributes Attributes => _directoryInfo.Attributes; - public Folder Root => new Folder(_directoryInfo.Root); + public Folder Root => _directoryInfo.Root; public DateTime CreationTime => _directoryInfo.CreationTime; @@ -27,9 +35,26 @@ internal Folder(DirectoryInfo directoryInfo) public string Extension => _directoryInfo.Extension; - public void Delete() => _directoryInfo.Delete(); + public void Delete() => _directoryInfo.Delete(true); + public void Clean() + { + foreach (var directory in _directoryInfo.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)) + { + directory.Delete(true); + } + + foreach (var file in _directoryInfo.EnumerateFiles("*", SearchOption.TopDirectoryOnly)) + { + file.Delete(); + } + } + public void MoveTo(string path) => _directoryInfo.MoveTo(path); + + public Folder GetFolder(string name) => new DirectoryInfo(System.IO.Path.Combine(Path, name)); + + public File GetFile(string name) => new FileInfo(System.IO.Path.Combine(Path, name)); public IEnumerable GetFolders(Func predicate) => _directoryInfo.EnumerateDirectories("*", SearchOption.AllDirectories) .Select(x => new Folder(x)) @@ -38,4 +63,25 @@ public IEnumerable GetFolders(Func predicate) => _directory public IEnumerable GetFiles(Func predicate) => _directoryInfo.EnumerateFiles("*", SearchOption.AllDirectories) .Select(x => new File(x)) .Where(predicate); + + public static implicit operator Folder?(string? path) + { + if (string.IsNullOrEmpty(path)) + { + return null; + } + + return new DirectoryInfo(path); + } + + [return: NotNullIfNotNull("directoryInfo")] + public static implicit operator Folder?(DirectoryInfo? directoryInfo) + { + if (directoryInfo == null) + { + return null; + } + + return new Folder(directoryInfo); + } } \ No newline at end of file diff --git a/ModularPipelines/Helpers/DependencyCollisionDetector.cs b/ModularPipelines/Helpers/DependencyCollisionDetector.cs index 00316c5d27..b57cf568dc 100644 --- a/ModularPipelines/Helpers/DependencyCollisionDetector.cs +++ b/ModularPipelines/Helpers/DependencyCollisionDetector.cs @@ -5,8 +5,14 @@ namespace ModularPipelines.Helpers; internal class DependencyCollisionDetector : IDependencyCollisionDetector { + private readonly IDependencyDetector _dependencyDetector; private readonly ConcurrentDictionary> _history = new(); + public DependencyCollisionDetector(IDependencyDetector dependencyDetector) + { + _dependencyDetector = dependencyDetector; + } + public void CheckDependency(Type dependentType, Type dependencyType) { CheckDependency(dependentType, dependencyType, new() @@ -14,6 +20,36 @@ public void CheckDependency(Type dependentType, Type dependencyType) $"**{dependentType.FullName}**" }, true); } + + public void CheckDependencies() + { + foreach (var moduleDependencyModel in _dependencyDetector.ModuleDependencyModels) + { + var allDescendentDependencies = GetDescendents(moduleDependencyModel).ToList(); + + var backwardsDependencyReference = allDescendentDependencies.FirstOrDefault(x => x.IsDependentOn.Contains(moduleDependencyModel)); + + if (backwardsDependencyReference != null) + { + var index = allDescendentDependencies.IndexOf(backwardsDependencyReference); + var typeChain = string.Join(" -> ", allDescendentDependencies.Take(index + 1)); + throw new DependencyCollisionException($"Dependency collision detected: {typeChain}"); + } + } + } + + private IEnumerable GetDescendents(ModuleDependencyModel moduleDependencyModel) + { + foreach (var directDependency in moduleDependencyModel.IsDependentOn) + { + yield return directDependency; + + foreach (var nestedDependency in directDependency.IsDependentOn.SelectMany(GetDescendents)) + { + yield return nestedDependency; + } + } + } private void CheckDependency(Type dependentType, Type dependencyType, List enumeratedTypes, bool shouldAdd) { diff --git a/ModularPipelines/Helpers/IDependencyCollisionDetector.cs b/ModularPipelines/Helpers/IDependencyCollisionDetector.cs index ccb6f2e7f0..d60df5b249 100644 --- a/ModularPipelines/Helpers/IDependencyCollisionDetector.cs +++ b/ModularPipelines/Helpers/IDependencyCollisionDetector.cs @@ -2,5 +2,6 @@ namespace ModularPipelines.Helpers; public interface IDependencyCollisionDetector { + void CheckDependencies(); void CheckDependency(Type dependentType, Type dependencyType); } \ No newline at end of file diff --git a/ModularPipelines/Helpers/IModuleLoggerProvider.cs b/ModularPipelines/Helpers/IModuleLoggerProvider.cs new file mode 100644 index 0000000000..134e380751 --- /dev/null +++ b/ModularPipelines/Helpers/IModuleLoggerProvider.cs @@ -0,0 +1,8 @@ +using Microsoft.Extensions.Logging; + +namespace ModularPipelines.Helpers; + +internal interface IModuleLoggerProvider +{ + ILogger Logger { get; } +} \ No newline at end of file diff --git a/ModularPipelines/Helpers/IPipelineConsolePrinter.cs b/ModularPipelines/Helpers/IPipelineConsolePrinter.cs index ba35eb8f6e..2a175aadb7 100644 --- a/ModularPipelines/Helpers/IPipelineConsolePrinter.cs +++ b/ModularPipelines/Helpers/IPipelineConsolePrinter.cs @@ -1,8 +1,8 @@ -using ModularPipelines.Modules; +using ModularPipelines.Models; namespace ModularPipelines.Helpers; -public interface IPipelineConsolePrinter +internal interface IPipelineConsolePrinter { - void PrintProgress(List modulesToProcess, List modulesToIgnore); + void PrintProgress(OrganizedModules organizedModules, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/ModularPipelines/Helpers/ModuleLoggerProvider.cs b/ModularPipelines/Helpers/ModuleLoggerProvider.cs new file mode 100644 index 0000000000..707d61379f --- /dev/null +++ b/ModularPipelines/Helpers/ModuleLoggerProvider.cs @@ -0,0 +1,43 @@ +using System.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using ModularPipelines.Modules; + +namespace ModularPipelines.Helpers; + +internal class ModuleLoggerProvider : IModuleLoggerProvider +{ + private readonly IServiceProvider _serviceProvider; + + private ILogger? _logger; + + public ModuleLoggerProvider(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + public ILogger Logger => _logger ??= GetLogger(); + + private bool IsModule(Type? type) + { + if (type is null) + { + return false; + } + + return !type.IsAbstract && type.IsAssignableTo(typeof(ModuleBase)); + } + + private ILogger GetLogger() + { + var module = new StackTrace().GetFrames().Select(x => x.GetMethod()?.ReflectedType?.ReflectedType).FirstOrDefault(IsModule); + + if (module == null) + { + return _serviceProvider.GetRequiredService>(); + } + + var loggerType = typeof(ModuleLogger<>).MakeGenericType(module); + + return (ILogger) _serviceProvider.GetRequiredService(loggerType); + } +} \ No newline at end of file diff --git a/ModularPipelines/Helpers/PipelineConsolePrinter.cs b/ModularPipelines/Helpers/PipelineConsolePrinter.cs deleted file mode 100644 index 5083310b9a..0000000000 --- a/ModularPipelines/Helpers/PipelineConsolePrinter.cs +++ /dev/null @@ -1,88 +0,0 @@ -using ModularPipelines.Modules; -using Spectre.Console; - -namespace ModularPipelines.Helpers; - -public class PipelineConsolePrinter : IPipelineConsolePrinter -{ - public void PrintProgress(List modulesToProcess, List modulesToIgnore) - { - AnsiConsole.Progress() - .Columns(new ProgressColumn[] - { - new TaskDescriptionColumn(), - new ProgressBarColumn(), - new PercentageColumn(), - new ElapsedTimeColumn(), - new SpinnerColumn(), - }) - .StartAsync(async ctx => - { - var totalTask = ctx.AddTask($"[green]Total[/]"); - - RegisterModules(modulesToProcess, ctx, totalTask); - - RegisterIgnoredModules(modulesToIgnore, ctx); - - CompleteTotalWhenFinished(modulesToProcess, totalTask, ctx); - - while (!ctx.IsFinished) - { - await Task.Delay(100); - } - }); - } - - private static void RegisterModules(List modulesToProcess, ProgressContext ctx, ProgressTask totalTask) - { - foreach (var moduleToProcess in modulesToProcess) - { - var task = ctx.AddTask(moduleToProcess.GetType().Name); - - _ = moduleToProcess.Task.ContinueWith(t => - { - if (t.IsCompletedSuccessfully) - { - task.Increment(100); - task.Description = $"[green]{task.Description}[/]"; - } - else - { - task.Description = $"[red]{task.Description}[/]"; - } - - task.StopTask(); - totalTask.Increment(100.0 / modulesToProcess.Count); - ctx.Refresh(); - }); - - _ = Task.Run(async () => - { - while (task is { IsFinished: false, Value: < 70 }) - { - task.Increment(0.1); - await Task.Delay(TimeSpan.FromMilliseconds(100)); - } - }); - } - } - - private static void CompleteTotalWhenFinished(List modulesToProcess, ProgressTask totalTask, - ProgressContext progressContext) - { - _ = Task.WhenAll(modulesToProcess.Select(x => x.Task)).ContinueWith(x => - { - totalTask.Increment(100); - totalTask.StopTask(); - progressContext.Refresh(); - }); - } - - private static void RegisterIgnoredModules(List modulesToIgnore, ProgressContext ctx) - { - foreach (var moduleToIgnore in modulesToIgnore) - { - ctx.AddTask($"[yellow][[Ignored]] {moduleToIgnore.GetType().Name}[/]").StopTask(); - } - } -} \ No newline at end of file diff --git a/ModularPipelines/Helpers/PipelineConsoleProgressPrinter.cs b/ModularPipelines/Helpers/PipelineConsoleProgressPrinter.cs new file mode 100644 index 0000000000..eb26d57425 --- /dev/null +++ b/ModularPipelines/Helpers/PipelineConsoleProgressPrinter.cs @@ -0,0 +1,114 @@ +using ModularPipelines.Models; +using ModularPipelines.Modules; +using Spectre.Console; + +namespace ModularPipelines.Helpers; + +internal class PipelineConsoleProgressPrinter : IPipelineConsolePrinter +{ + public void PrintProgress(OrganizedModules organizedModules, CancellationToken cancellationToken) + { + AnsiConsole.Progress() + .Columns(new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn(), new ElapsedTimeColumn(), new RemainingTimeColumn(), new SpinnerColumn()) + .StartAsync(async progressContext => + { + var totalProgressTask = progressContext.AddTask($"[green]Total[/]"); + + RegisterModules(organizedModules.RunnableModules, progressContext, totalProgressTask, cancellationToken); + + RegisterIgnoredModules(organizedModules.IgnoredModules, progressContext); + + CompleteTotalWhenFinished(organizedModules.RunnableModules, totalProgressTask, cancellationToken); + + progressContext.Refresh(); + + while (!progressContext.IsFinished) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await Task.Delay(1000); + } + + if (cancellationToken.IsCancellationRequested) + { + return; + } + + progressContext.Refresh(); + }); + } + + private static void RegisterModules(IReadOnlyList modulesToProcess, ProgressContext progressContext, + ProgressTask totalTask, CancellationToken cancellationToken) + { + foreach (var moduleToProcess in modulesToProcess) + { + var moduleName = moduleToProcess.Module.GetType().Name; + + + var progressTask = progressContext.AddTask($"[[Waiting]] {moduleName}", new ProgressTaskSettings + { + AutoStart = false + }); + + // Callback for Module has started + _ = moduleToProcess.Module.StartTask.ContinueWith(async t => + { + progressTask.StartTask(); + var estimatedDuration = moduleToProcess.EstimatedDuration * 1.1; // Give 10% headroom + + var totalEstimatedSeconds = estimatedDuration.TotalSeconds >= 1 ? estimatedDuration.TotalSeconds : 1; + + var ticksPerSecond = 100 / totalEstimatedSeconds; + + progressTask.Description = moduleName; + while (progressTask is { IsFinished: false, Value: < 95 }) + { + await Task.Delay(TimeSpan.FromSeconds(1)); + progressTask.Increment(ticksPerSecond); + } + }, cancellationToken); + + // Callback for Module has finished + _ = moduleToProcess.Module.ResultTaskInternal.ContinueWith(t => + { + if (t.IsCompletedSuccessfully) + { + progressTask.Increment(100); + } + + progressTask.Description = t.IsCompletedSuccessfully ? $"[green]{moduleName}[/]" : $"[red][[Failed]] {moduleName}[/]"; + + progressTask.StopTask(); + totalTask.Increment(100.0 / modulesToProcess.Count); + }, cancellationToken); + + // Callback for Module has been ignored + _ = moduleToProcess.Module.IgnoreTask.ContinueWith(t => + { + progressTask.Description = $"[yellow][[Ignored]] {moduleName}[/]"; + progressTask.StopTask(); + }, cancellationToken); + } + } + + private static void CompleteTotalWhenFinished(IReadOnlyList modulesToProcess, ProgressTask totalTask, CancellationToken cancellationToken) + { + _ = Task.WhenAll(modulesToProcess.Select(x => x.Module.ResultTaskInternal)).ContinueWith(x => + { + totalTask.Increment(100); + totalTask.StopTask(); + }, cancellationToken); + } + + private static void RegisterIgnoredModules(IReadOnlyList modulesToIgnore, ProgressContext progressContext) + { + foreach (var moduleToIgnore in modulesToIgnore) + { + progressContext.AddTask($"[yellow][[Ignored]] {moduleToIgnore.GetType().Name}[/]").StopTask(); + } + } +} \ No newline at end of file diff --git a/ModularPipelines/Host/IPipelineHostBuilder.cs b/ModularPipelines/Host/IPipelineHostBuilder.cs index 290cfdb516..3db010e5b8 100644 --- a/ModularPipelines/Host/IPipelineHostBuilder.cs +++ b/ModularPipelines/Host/IPipelineHostBuilder.cs @@ -12,5 +12,5 @@ public interface IPipelineHostBuilder IPipelineHostBuilder ConfigureAppConfiguration(Action configureDelegate); IPipelineHostBuilder ConfigureServices(Action configureDelegate); IPipelineHostBuilder ConfigurePipelineOptions(Action configureDelegate); - Task ExecutePipelineAsync(); + Task> ExecutePipelineAsync(); } \ No newline at end of file diff --git a/ModularPipelines/Host/PipelineEngineOverrides.cs b/ModularPipelines/Host/PipelineEngineOverrides.cs new file mode 100644 index 0000000000..98edb0b9ac --- /dev/null +++ b/ModularPipelines/Host/PipelineEngineOverrides.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using ModularPipelines.Engine; + +namespace ModularPipelines.Host; + +public class PipelineEngineOverrides +{ + private readonly IHostBuilder _internalHost; + + internal PipelineEngineOverrides(IHostBuilder internalHost) + { + _internalHost = internalHost; + } + + public PipelineEngineOverrides OverrideModuleEstimatedTimeProvider() where T : class, IModuleEstimatedTimeProvider + { + _internalHost.ConfigureServices(s => + { + s.RemoveAll() + .AddSingleton(); + }); + + return this; + } +} \ No newline at end of file diff --git a/ModularPipelines/Host/PipelineHostBuilder.cs b/ModularPipelines/Host/PipelineHostBuilder.cs index c3f92f798e..139e7da714 100644 --- a/ModularPipelines/Host/PipelineHostBuilder.cs +++ b/ModularPipelines/Host/PipelineHostBuilder.cs @@ -1,3 +1,5 @@ +using System.Reflection; +using System.Runtime.CompilerServices; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -13,27 +15,56 @@ namespace ModularPipelines.Host; public class PipelineHostBuilder : IPipelineHostBuilder { private readonly IHostBuilder _internalHost; + private readonly PipelineEngineOverrides _overrides; internal PipelineHostBuilder() { _internalHost = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(); + _overrides = new PipelineEngineOverrides(_internalHost); _internalHost.ConfigureServices(services => { + // Bundles services .Configure(_ => {}) .AddLogging() .AddHttpClient() - .AddInitializers() + .AddInitializers(); + + // Transient + services.AddTransient() + .AddTransient() + .AddTransient(); + + // Singletons + services + .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() + .AddSingleton(typeof(ModuleLogger<>)) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); }); } @@ -55,17 +86,79 @@ public IPipelineHostBuilder ConfigurePipelineOptions(Action { - collection.Configure(options => configureDelegate(context, options)); }); return this; } + + public IPipelineHostBuilder ConfigureOverrides(Action configureDelegate) + { + configureDelegate(_overrides); + return this; + } - public async Task ExecutePipelineAsync() + public async Task> ExecutePipelineAsync() { + LoadModularPipelineAssembliesIfNotLoadedYet(); + + _internalHost.ConfigureServices(collection => + { + foreach (var contextRegistrationDelegate in ServiceContextRegistry.ContextRegistrationDelegates) + { + contextRegistrationDelegate(collection); + } + }); + var host = _internalHost.Build(); + await host.Services.GetRequiredService().InitializeAsync(); - return await host.Services.GetRequiredService().ExecuteAsync(); + + try + { + return await host.Services.GetRequiredService().ExecuteAsync(); + } + finally + { + await ((ServiceProvider) host.Services).DisposeAsync(); + } + } + + private void LoadModularPipelineAssembliesIfNotLoadedYet() + { + var currentAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + + var unloadedModularPipelineAssemblies = GetDlls() + .Select(Path.GetFileNameWithoutExtension) + .Except(currentAssemblies.Select(x => x.GetName().Name)) + .OfType() + .ToList(); + + foreach (var modularPipelineAssembly in unloadedModularPipelineAssemblies) + { + Assembly.Load(new AssemblyName(modularPipelineAssembly)); + } + + foreach (var assembly in AppDomain.CurrentDomain + .GetAssemblies() + .Where(a => a.GetName().Name?.Contains("ModularPipeline", StringComparison.InvariantCultureIgnoreCase) == true)) + { + RuntimeHelpers.RunModuleConstructor(assembly.ManifestModule.ModuleHandle); + } + } + + private static IEnumerable GetDlls() + { + var baseDirectoryDlls = Directory.EnumerateFiles(AppDomain.CurrentDomain.BaseDirectory, "*ModularPipeline*.dll", SearchOption.TopDirectoryOnly); + + if (string.IsNullOrEmpty(AppDomain.CurrentDomain.DynamicDirectory)) + { + return baseDirectoryDlls; + } + + return baseDirectoryDlls + .Concat(Directory.EnumerateFiles(AppDomain.CurrentDomain.DynamicDirectory, "*ModularPipeline*.dll", SearchOption.TopDirectoryOnly)) + .Distinct(); + } } \ No newline at end of file diff --git a/ModularPipelines/Http.cs b/ModularPipelines/Http.cs new file mode 100644 index 0000000000..2e183bde9e --- /dev/null +++ b/ModularPipelines/Http.cs @@ -0,0 +1,113 @@ +using System.Net.Http.Headers; +using System.Text; +using Microsoft.Extensions.Logging; +using ModularPipelines.Helpers; +using ModularPipelines.Options; + +namespace ModularPipelines; + +internal class Http : IHttp +{ + private readonly HttpClient _defaultHttpClient; + private readonly IModuleLoggerProvider _moduleLoggerProvider; + + public Http(HttpClient defaultHttpClient, + IModuleLoggerProvider moduleLoggerProvider) + { + _defaultHttpClient = defaultHttpClient; + _moduleLoggerProvider = moduleLoggerProvider; + } + public async Task Send(HttpOptions httpOptions) + { + if (httpOptions.LogRequest) + { + await PrintRequest(httpOptions.HttpRequestMessage); + } + + var response = await (httpOptions.HttpClient ?? _defaultHttpClient).SendAsync(httpOptions.HttpRequestMessage); + + if (httpOptions.LogResponse) + { + await PrintResponse(response); + } + + return response.EnsureSuccessStatusCode(); + } + + public async Task PrintRequest(HttpRequestMessage request) + { + var sb = new StringBuilder(); + + sb.AppendLine($"{request.Method} {request.RequestUri} HTTP/{request.Version}"); + + sb.AppendLine(); + + PrintHeaders(sb, request.Headers, request.Content?.Headers); + + sb.AppendLine(); + + await PrintBody(sb, request.Content); + + _moduleLoggerProvider.Logger.LogInformation("---Request---\r\n{Request}", sb.ToString()); + } + + public async Task PrintResponse(HttpResponseMessage response) + { + var sb = new StringBuilder(); + + var statusCode = (int) response.StatusCode; + + sb.AppendLine($"HTTP/{response.Version} {statusCode} {response.ReasonPhrase}"); + + sb.AppendLine(); + + PrintHeaders(sb, response.Headers, response.Content.Headers); + + sb.AppendLine(); + + await PrintBody(sb, response.Content); + + _moduleLoggerProvider.Logger.LogInformation("---Response---\r\n{Response}", sb.ToString()); + } + + private static void PrintHeaders(StringBuilder sb, HttpHeaders baseHeaders, HttpHeaders? contentHeaders) + { + sb.AppendLine("Headers"); + foreach (var (key, values) in baseHeaders) + { + foreach (var value in values) + { + sb.AppendLine($"\t{key}: {value}"); + } + } + + var contentHeadersArray = contentHeaders as IEnumerable>> ?? Array.Empty>>(); + + foreach (var (key, values) in contentHeadersArray) + { + foreach (var value in values) + { + sb.AppendLine($"\t{key}: {value}"); + } + } + + if (!baseHeaders.Any() && (!contentHeaders?.Any() ?? true)) + { + sb.AppendLine("\t(null)"); + } + } + + private static async Task PrintBody(StringBuilder sb, HttpContent? content) + { + sb.AppendLine("Body"); + var body = await (content?.ReadAsStringAsync() ?? Task.FromResult(string.Empty)); + if (!string.IsNullOrWhiteSpace(body)) + { + sb.AppendLine($"\t{body}"); + } + else + { + sb.AppendLine("\t(null)"); + } + } +} \ No newline at end of file diff --git a/ModularPipelines/IHttp.cs b/ModularPipelines/IHttp.cs new file mode 100644 index 0000000000..9b9d4a670f --- /dev/null +++ b/ModularPipelines/IHttp.cs @@ -0,0 +1,8 @@ +using ModularPipelines.Options; + +namespace ModularPipelines; + +public interface IHttp +{ + public Task Send(HttpOptions httpOptions); +} \ No newline at end of file diff --git a/ModularPipelines/Interfaces/IPipelineGlobalHooks.cs b/ModularPipelines/Interfaces/IPipelineGlobalHooks.cs index ef5ba32fdc..0afb45a14d 100644 --- a/ModularPipelines/Interfaces/IPipelineGlobalHooks.cs +++ b/ModularPipelines/Interfaces/IPipelineGlobalHooks.cs @@ -1,9 +1,10 @@ using ModularPipelines.Context; +using ModularPipelines.Modules; namespace ModularPipelines.Interfaces; public interface IPipelineGlobalHooks { Task OnStartAsync(IModuleContext moduleContext); - Task OnEndAsync(IModuleContext moduleContext); + Task OnEndAsync(IModuleContext moduleContext, IReadOnlyList modules); } \ No newline at end of file diff --git a/ModularPipelines/Interfaces/IPipelineModuleHooks.cs b/ModularPipelines/Interfaces/IPipelineModuleHooks.cs index a4c6f185d7..64153f1733 100644 --- a/ModularPipelines/Interfaces/IPipelineModuleHooks.cs +++ b/ModularPipelines/Interfaces/IPipelineModuleHooks.cs @@ -5,6 +5,6 @@ namespace ModularPipelines.Interfaces; public interface IPipelineModuleHooks { - Task OnBeforeModuleStartAsync(IModuleContext moduleContext, IModule module); - Task OnBeforeModuleEndAsync(IModuleContext moduleContext, IModule module); + Task OnBeforeModuleStartAsync(IModuleContext moduleContext, ModuleBase module); + Task OnBeforeModuleEndAsync(IModuleContext moduleContext, ModuleBase module); } \ No newline at end of file diff --git a/ModularPipelines/Models/ModuleResult.cs b/ModularPipelines/Models/ModuleResult.cs index 07c414305f..a0b6ba2940 100644 --- a/ModularPipelines/Models/ModuleResult.cs +++ b/ModularPipelines/Models/ModuleResult.cs @@ -1,39 +1,50 @@ namespace ModularPipelines.Models; -public class ModuleResult +public class ModuleResult : ModuleResult { public ModuleResult(T? value) { _value = value; + ModuleResultType = ModuleResultType.SuccessfulResult; } public ModuleResult(Exception exception) { Exception = exception; + ModuleResultType = ModuleResultType.Failure; } - - private readonly T? _value; + + internal string? ModuleName { get; set; } public T? Value { get { - if (IsErrored) + if (ModuleResultType == ModuleResultType.Failure) + { + throw new Exception($"{GetModuleName()} has errored. No Value available"); + } + + if (ModuleResultType == ModuleResultType.Skipped) { - throw new Exception("This Module Result has errored. No Value available"); + throw new Exception($"{GetModuleName()} was skipped. No Value available"); } return _value; } } + private string GetModuleName() + { + return ModuleName ?? "This module"; + } + public Exception? Exception { get; } - public bool IsErrored => Exception != null; - public static implicit operator ModuleResult(T t) => ModuleResult.From(t); - public static implicit operator ModuleResult(Exception exception) => ModuleResult.FromException(exception); + public static implicit operator ModuleResult(T t) => From(t); + public static implicit operator ModuleResult(Exception exception) => FromException(exception); } public class ModuleResult @@ -45,4 +56,6 @@ public static ModuleResult From(T t) } public static ModuleResult FromException(Exception exception) => new(exception); + + public ModuleResultType ModuleResultType { get; private protected set; } } \ No newline at end of file diff --git a/ModularPipelines/Models/ModuleResultType.cs b/ModularPipelines/Models/ModuleResultType.cs new file mode 100644 index 0000000000..c1e1132216 --- /dev/null +++ b/ModularPipelines/Models/ModuleResultType.cs @@ -0,0 +1,8 @@ +namespace ModularPipelines.Models; + +public enum ModuleResultType +{ + SuccessfulResult, + Failure, + Skipped +} \ No newline at end of file diff --git a/ModularPipelines/Models/ModuleRunType.cs b/ModularPipelines/Models/ModuleRunType.cs new file mode 100644 index 0000000000..580d63103f --- /dev/null +++ b/ModularPipelines/Models/ModuleRunType.cs @@ -0,0 +1,7 @@ +namespace ModularPipelines.Models; + +public enum ModuleRunType +{ + AlwaysRun, + OnSuccessfulDependencies +} \ No newline at end of file diff --git a/ModularPipelines/Models/OrganizedModules.cs b/ModularPipelines/Models/OrganizedModules.cs new file mode 100644 index 0000000000..e543c5f1f7 --- /dev/null +++ b/ModularPipelines/Models/OrganizedModules.cs @@ -0,0 +1,8 @@ +using ModularPipelines.Modules; + +namespace ModularPipelines.Models; + +internal record OrganizedModules(IReadOnlyList RunnableModules, IReadOnlyList IgnoredModules) +{ + public IReadOnlyList AllModules { get; } = RunnableModules.Select(x => x.Module).Concat(IgnoredModules).ToList(); +}; \ No newline at end of file diff --git a/ModularPipelines/Models/RunnableModule.cs b/ModularPipelines/Models/RunnableModule.cs new file mode 100644 index 0000000000..a7ebb07c94 --- /dev/null +++ b/ModularPipelines/Models/RunnableModule.cs @@ -0,0 +1,5 @@ +using ModularPipelines.Modules; + +namespace ModularPipelines.Models; + +internal record RunnableModule(ModuleBase Module, TimeSpan EstimatedDuration); \ No newline at end of file diff --git a/ModularPipelines/Models/SkippedModuleResult.cs b/ModularPipelines/Models/SkippedModuleResult.cs new file mode 100644 index 0000000000..4bd28fc9c7 --- /dev/null +++ b/ModularPipelines/Models/SkippedModuleResult.cs @@ -0,0 +1,9 @@ +namespace ModularPipelines.Models; + +internal class SkippedModuleResult : ModuleResult +{ + public SkippedModuleResult() : base(default(T?)) + { + ModuleResultType = ModuleResultType.Skipped; + } +} \ No newline at end of file diff --git a/ModularPipelines/ModularPipelines.csproj b/ModularPipelines/ModularPipelines.csproj index 7450e61e43..d81c0f9363 100644 --- a/ModularPipelines/ModularPipelines.csproj +++ b/ModularPipelines/ModularPipelines.csproj @@ -8,13 +8,14 @@ - - + + + diff --git a/ModularPipelines/ModuleLogger.cs b/ModularPipelines/ModuleLogger.cs new file mode 100644 index 0000000000..a74d160a6f --- /dev/null +++ b/ModularPipelines/ModuleLogger.cs @@ -0,0 +1,81 @@ +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using ModularPipelines.Options; + +namespace ModularPipelines; + +public class ModuleLogger : ILogger, IDisposable +{ + private readonly IOptions _options; + private readonly ILogger _defaultLogger; + + private List<(LogLevel logLevel, EventId eventId, object state, Exception? exception, Func formatter)> _logEvents = new(); + + private bool _isDisposed; + + // ReSharper disable once ContextualLoggerProblem + public ModuleLogger(IOptions options, ILogger defaultLogger) + { + _options = options; + _defaultLogger = defaultLogger; + } + + public IDisposable BeginScope(TState state) + { + return new NoopDisposable(); + } + + public bool IsEnabled(LogLevel logLevel) + { + return logLevel >= _options.Value.LoggerOptions.LogLevel; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func? formatter) + { + if (!IsEnabled(logLevel) || _isDisposed) + { + return; + } + + var mappedFormatter = MapFormatter(formatter); + + var valueTuple = (logLevel, eventId, state, exception, mappedFormatter); + + _logEvents.Add(valueTuple!); + } + + private Func MapFormatter(Func? formatter) + { + if (formatter is null) + { + return (_, _) => string.Empty; + } + + return (o, exception) => formatter.Invoke((TState) o, exception); + } + + private class NoopDisposable : IDisposable + { + public void Dispose() + { + } + } + + [MethodImpl(MethodImplOptions.Synchronized)] + public void Dispose() + { + _isDisposed = true; + + var logEvents = Interlocked.Exchange(ref _logEvents!, new List<(LogLevel logLevel, EventId eventId, object state, Exception exception, Func formatter)>()); + foreach (var (logLevel, eventId, state, exception, formatter) in logEvents) + { + _defaultLogger.Log(logLevel, eventId, state, exception, formatter); + } + + logEvents.Clear(); + _logEvents.Clear(); + + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/ModularPipelines/Modules/IModule.cs b/ModularPipelines/Modules/IModule.cs deleted file mode 100644 index 849e74bd5d..0000000000 --- a/ModularPipelines/Modules/IModule.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.ComponentModel; -using System.Runtime.CompilerServices; -using ModularPipelines.Enums; -using ModularPipelines.Models; - -namespace ModularPipelines.Modules; - -public interface IModule -{ - internal Task Task { get; } - - DateTimeOffset StartTime { get; } - DateTimeOffset EndTime { get; } - - Status Status { get; internal set; } - - internal TimeSpan Timeout { get; } - - TimeSpan Duration { get; } - - bool ShouldSkip { get; } - - [EditorBrowsable(EditorBrowsableState.Advanced)] - internal Task StartProcessingModule(); -} - -public interface IModule : IModule -{ - TaskAwaiter> GetAwaiter(); -} \ No newline at end of file diff --git a/ModularPipelines/Modules/Module.cs b/ModularPipelines/Modules/Module.cs index 4035f82de4..8c207eb588 100644 --- a/ModularPipelines/Modules/Module.cs +++ b/ModularPipelines/Modules/Module.cs @@ -1,12 +1,9 @@ using System.ComponentModel; using System.Diagnostics; using System.Reflection; -using System.Runtime.CompilerServices; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using ModularPipelines.Attributes; using ModularPipelines.Context; -using ModularPipelines.Engine; using ModularPipelines.Enums; using ModularPipelines.Exceptions; using ModularPipelines.Models; @@ -15,24 +12,23 @@ namespace ModularPipelines.Modules; public abstract class Module : Module> { - protected Module(IModuleContext context) : base(context) - { - } } -public abstract class Module : IModule +/// +/// The base class from which all custom modules should inherit. +/// +/// The type of result object this module will return from its ExecuteAsync method. +public abstract partial class Module : ModuleBase { - private readonly ILogger _logger; - protected readonly IModuleContext Context; - internal readonly Stopwatch Stopwatch = new(); + private readonly Stopwatch _stopwatch = new(); + private readonly List _dependentModules = new(); - private bool _hasStarted; - protected Module(IModuleContext context) + private bool _initialized; + private IModuleContext _context = null!; // Late Initialisation + + protected Module() { - Context = context; - _logger = GetLogger(); - foreach (var customAttribute in GetType().GetCustomAttributes(true)) { AddDependency(customAttribute.Type); @@ -46,107 +42,107 @@ private void AddDependency(Type type) throw new ModuleReferencingSelfException("A module cannot depend on itself"); } - if (!type.IsAssignableTo(typeof(IModule))) + if (!type.IsAssignableTo(typeof(ModuleBase))) { throw new Exception($"{type.FullName} must be a module to add as a dependency"); } - Context.DependencyCollisionDetector.CheckDependency(GetType(), type); - _dependentModules.Add(type); } - private ILogger GetLogger() - { - var currentClass = GetType(); - - var loggerForCurrentClass = typeof(ILogger<>).MakeGenericType(currentClass); - - return Context.ServiceProvider.GetService(loggerForCurrentClass) as ILogger - ?? NullLogger.Instance; - } - - private readonly CancellationTokenSource _cancellationTokenSource = new(); - - private readonly TaskCompletionSource> _taskCompletionSource = new(); - - protected virtual Task OnBeforeExecute() - { - _logger.LogDebug("OnBeforeExecute triggered"); - - return Task.CompletedTask; - } - - internal virtual Task InitialiseInternalAsync() + private void CheckDependencyConflicts() { - _logger.LogDebug("InitialiseInternalAsync triggered"); - - return Task.CompletedTask; + foreach (var dependentModule in _dependentModules) + { + _context.DependencyCollisionDetector.CheckDependency(GetType(), dependentModule); + } } - protected virtual Task InitialiseAsync() + internal override async Task StartAsync() { - _logger.LogDebug("InitialiseAsync triggered"); - - return Task.CompletedTask; - } + if (!_initialized) + { + throw new ModuleNotInitializedException(GetType()); + } - [EditorBrowsable(EditorBrowsableState.Advanced)] - public async Task StartProcessingModule() - { - if (_hasStarted) + if (ModuleRunType != ModuleRunType.AlwaysRun) { - return; + _context.EngineCancellationToken.Token.Register(ModuleCancellationTokenSource.Cancel); } - _hasStarted = true; - - Status = Status.NotYetStarted; - try { + CheckDependencyConflicts(); + + ModuleCancellationTokenSource.Token.ThrowIfCancellationRequested(); + await WaitForModuleDependencies(); - await InitialiseInternalAsync(); - await InitialiseAsync(); - - await Context.Get()!.OnBeforeModuleStartAsync(this); + var shouldSkipModule = await ShouldSkip(_context); - await OnBeforeExecute(); + if (shouldSkipModule && await CanRunFromHistory(_context)) + { + await SetupModuleFromHistory(); + return; + } + + if (shouldSkipModule) + { + SetSkipped(); + return; + } + + ModuleCancellationTokenSource.Token.ThrowIfCancellationRequested(); + + await OnBeforeExecute(_context); + + StartTask.Start(TaskScheduler.Default); Status = Status.Processing; StartTime = DateTimeOffset.UtcNow; + var timeoutExceptionTask = Task.CompletedTask; + if (Timeout != TimeSpan.Zero) { - _cancellationTokenSource.CancelAfter(Timeout); + ModuleCancellationTokenSource.CancelAfter(Timeout); + timeoutExceptionTask = Task.Delay(Timeout + TimeSpan.FromSeconds(30), ModuleCancellationTokenSource.Token); } - Stopwatch.Start(); + _stopwatch.Start(); + + var executeAsyncTask = ExecuteAsync(_context, ModuleCancellationTokenSource.Token); + + // Will throw a timeout exception if configured and timeout is reached + await Task.WhenAny(timeoutExceptionTask, executeAsyncTask); + + var moduleResult = await executeAsyncTask ?? ModuleResult.Empty(); + moduleResult.ModuleName = GetType().Name; - var values = await ExecuteAsync(_cancellationTokenSource.Token) ?? ModuleResult.Empty(); + await _context.ModuleResultRepository.PersistResultAsync(this, moduleResult); - Stopwatch.Stop(); - Duration = Stopwatch.Elapsed; + _stopwatch.Stop(); + Duration = _stopwatch.Elapsed; Status = Status.Successful; EndTime = DateTimeOffset.UtcNow; - _logger.LogDebug("Module Succeeded after {Duration}", Duration); + _context.Logger.LogDebug("Module Succeeded after {Duration}", Duration); - _taskCompletionSource.SetResult(values); + TaskCompletionSource.SetResult(moduleResult); } catch (Exception exception) { - Stopwatch.Stop(); - Duration = Stopwatch.Elapsed; + _stopwatch.Stop(); + Duration = _stopwatch.Elapsed; + EndTime = DateTimeOffset.UtcNow; - _logger.LogError(exception, "Module Failed after {Duration}", Duration); + _context.Logger.LogError(exception, "Module Failed after {Duration}", Duration); if (exception is TaskCanceledException or OperationCanceledException - && _cancellationTokenSource.IsCancellationRequested) + && ModuleCancellationTokenSource.IsCancellationRequested && !_context.EngineCancellationToken.IsCancellationRequested) { - _logger.LogDebug("Module timed out {ModuleType}", GetType().FullName); + _context.Logger.LogDebug("Module timed out: {ModuleType}", GetType().FullName); Status = Status.TimedOut; } @@ -155,71 +151,83 @@ public async Task StartProcessingModule() Status = Status.Failed; } - if (IgnoreFailures) + if (await ShouldIgnoreFailures(_context, exception)) { - _taskCompletionSource.SetResult(ModuleResult.FromException(exception)); + var moduleResult = ModuleResult.FromException(exception); + moduleResult.ModuleName = GetType().Name; + + await _context.ModuleResultRepository.PersistResultAsync(this, moduleResult); + + TaskCompletionSource.SetResult(moduleResult); } else { - _taskCompletionSource.SetException(exception); + TaskCompletionSource.SetException(exception); throw; } } finally { - await OnAfterExecute(); - - await Context.Get()!.OnAfterModuleEndAsync(this); + await OnAfterExecute(_context); } } - - protected abstract Task?> ExecuteAsync(CancellationToken cancellationToken); - - protected virtual Task OnAfterExecute() + internal override ModuleBase Initialize(IModuleContext context) { - _logger.LogDebug("OnAfterExecute triggered"); - - return Task.CompletedTask; + _context = context; + _initialized = true; + return this; } + private async Task SetupModuleFromHistory() + { + Status = Status.Successful; + + var result = await _context.ModuleResultRepository.GetResultAsync(this); - public DateTimeOffset StartTime { get; private set; } - public DateTimeOffset EndTime { get; private set; } - - public Status Status { get; set; } = Status.Unknown; - - public virtual TimeSpan Timeout => TimeSpan.Zero; - public virtual bool IgnoreFailures => false; - public virtual bool ShouldSkip => false; - public TimeSpan Duration { get; private set; } + if (result == null) + { + SetSkipped(); + return; + } + + var utcNow = DateTimeOffset.UtcNow; + + StartTime = utcNow; + EndTime = utcNow; + + StartTask.Start(TaskScheduler.Default); + TaskCompletionSource.SetResult(result); + } - protected internal TModule GetModule() where TModule : IModule + protected TModule GetModule() where TModule : ModuleBase { if (typeof(TModule) == GetType()) { throw new ModuleReferencingSelfException("A module cannot get itself"); } - Context.DependencyCollisionDetector.CheckDependency(GetType(), typeof(TModule)); + _context.DependencyCollisionDetector.CheckDependency(GetType(), typeof(TModule)); - return Context.GetModule(); + return _context.GetModule(); } - - protected internal async Task WaitForModule() where TModule : IModule + + protected async Task WaitForModule() where TModule : ModuleBase { var module = GetModule(); - _logger.LogDebug("Waiting for Module {ModuleType}", typeof(TModule).FullName); - - await module.Task; + var stopwatch = Stopwatch.StartNew(); - _logger.LogDebug("Finished waiting for Module {ModuleType}", typeof(TModule).FullName); - } + _context.Logger.LogDebug("Waiting for Module {ModuleType}", typeof(TModule).FullName); - public TaskAwaiter> GetAwaiter() - { - return _taskCompletionSource.Task.GetAwaiter(); + var result = await module.ResultTaskInternal; + + if (IsSkippedResult(result)) + { + throw new DependsOnSkippedModuleException(this, module); + } + + _context.Logger.LogDebug("Finished waiting for Module {ModuleType} after {Elapsed}", typeof(TModule).FullName, stopwatch.Elapsed); } private async Task WaitForModuleDependencies() @@ -229,16 +237,43 @@ private async Task WaitForModuleDependencies() return; } - var modules = _dependentModules.Select(Context.GetModule).ToList(); + try + { + var modules = _dependentModules.Select(_context.GetModule).ToList(); - var tasks = modules.Select(module => module.Task).ToList(); - - await Task.WhenAll(tasks); + var tasks = modules.Select(module => module.ResultTaskInternal); + + await Task.WhenAll(tasks); + + foreach (var moduleBase in modules) + { + var result = await moduleBase.ResultTaskInternal; + if (IsSkippedResult(result)) + { + throw new DependsOnSkippedModuleException(this, moduleBase); + } + } + } + catch (Exception e) when (ModuleRunType == ModuleRunType.AlwaysRun) + { + _context.Logger.LogError(e, "Ignoring Exception due to 'AlwaysRun' set"); + } + } + + private bool IsSkippedResult(object result) + { + var resultType = result.GetType(); + return resultType.IsGenericType && resultType.GetGenericTypeDefinition() == typeof(SkippedModuleResult<>); } - public Task Task => _taskCompletionSource.Task; - public Task> GetResultAsync() => _taskCompletionSource.Task; + internal override void SetSkipped() + { + Status = Status.Ignored; + + IgnoreTask.Start(TaskScheduler.Default); + + TaskCompletionSource.SetResult(new SkippedModuleResult()); - protected ModuleResult Nothing() => ModuleResult.Empty(); - protected Task?> NothingAsync() => Task.FromResult(ModuleResult.Empty())!; + _context.Logger.LogInformation("{Module} Ignored", GetType().Name); + } } \ No newline at end of file diff --git a/ModularPipelines/Modules/ModuleBase.cs b/ModularPipelines/Modules/ModuleBase.cs new file mode 100644 index 0000000000..4f229764b8 --- /dev/null +++ b/ModularPipelines/Modules/ModuleBase.cs @@ -0,0 +1,117 @@ +using System.Runtime.CompilerServices; +using ModularPipelines.Context; +using ModularPipelines.Engine; +using ModularPipelines.Enums; +using ModularPipelines.Models; + +namespace ModularPipelines.Modules; + +public abstract class ModuleBase +{ + internal readonly Task StartTask = new(() => { }); + internal readonly Task IgnoreTask = new(() => { }); + internal abstract Task ResultTaskInternal { get; } + + internal readonly CancellationTokenSource ModuleCancellationTokenSource = new(); + + + /// + /// The start time of the module + /// + public DateTimeOffset StartTime { get; internal set; } + + /// + /// The end time of the module. + /// + public DateTimeOffset EndTime { get; internal set; } + + /// + /// The duration of the module. This will be set after the module has finished. + /// + public TimeSpan Duration { get; internal set; } + + /// + /// The status of the module. + /// + public Status Status { get; internal set; } = Status.NotYetStarted; + + /// + /// A Timeout for the module + /// + protected virtual TimeSpan Timeout => TimeSpan.FromMinutes(30); + + /// + /// If true, the pipeline will not fail is this module fails. + /// + /// + /// + /// + protected virtual Task ShouldIgnoreFailures(IModuleContext context, Exception exception) => Task.FromResult(false); + + /// + /// If true, this module will not run. + /// + /// + /// + protected virtual Task ShouldSkip(IModuleContext context) => Task.FromResult(false); + + /// + /// If this module is skipped, and this returns true, the last persisted result of this module will be reconstructed. + /// If no persisted result can be reconstructed, this module will fail. + /// + /// + /// + protected virtual Task CanRunFromHistory(IModuleContext context) => Task.FromResult(context.ModuleResultRepository.GetType() != typeof(NoOpModuleResultRepository)); + + public virtual ModuleRunType ModuleRunType => ModuleRunType.OnSuccessfulDependencies; + + internal abstract Task StartAsync(); + internal abstract void SetSkipped(); + internal abstract ModuleBase Initialize(IModuleContext context); + + internal readonly List SubModuleBases = new(); + + protected async Task SubModule(string name, Func> action) + { + var submodule = new SubModule(GetType(), name, action); + SubModuleBases.Add(submodule); + return await submodule.Task; + } + + protected async Task SubModule(string name, Func action) + { + var submodule = new SubModule(GetType(), name, action); + SubModuleBases.Add(submodule); + await submodule.Task; + } +} + +public abstract class ModuleBase : ModuleBase +{ + internal readonly TaskCompletionSource> TaskCompletionSource = new(); + + /// + /// The awaiter used to return the result of the module when awaited + /// + /// + public TaskAwaiter> GetAwaiter() + { + return TaskCompletionSource.Task.GetAwaiter(); + } + + internal override Task ResultTaskInternal => TaskCompletionSource.Task.ContinueWith(t => (object) t.Result); + + /// + /// Used to return no result in a module + /// + /// + protected Task?> NothingAsync() => Task.FromResult(ModuleResult.Empty())!; + + /// + /// The core logic of the module goes here + /// + /// + /// + /// + protected abstract Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/ModularPipelines/Modules/Module_Partials.cs b/ModularPipelines/Modules/Module_Partials.cs new file mode 100644 index 0000000000..1ab96c788b --- /dev/null +++ b/ModularPipelines/Modules/Module_Partials.cs @@ -0,0 +1,16 @@ +using ModularPipelines.Context; + +namespace ModularPipelines.Modules; + +public partial class Module +{ + protected virtual Task OnBeforeExecute(IModuleContext context) + { + return Task.CompletedTask; + } + + protected virtual Task OnAfterExecute(IModuleContext context) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/ModularPipelines/Modules/SubModule.cs b/ModularPipelines/Modules/SubModule.cs new file mode 100644 index 0000000000..ddc2f9d6af --- /dev/null +++ b/ModularPipelines/Modules/SubModule.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; + +namespace ModularPipelines.Modules; + +public class SubModule : SubModuleBase +{ + internal Task Task { get; } + internal SubModule(Type parentModule, string name, Func action) : base(parentModule, name) + { + StartTime = DateTimeOffset.UtcNow; + var stopwatch = Stopwatch.StartNew(); + Task = action(); + Task.ContinueWith(t => + { + Duration = stopwatch.Elapsed; + EndTime = DateTimeOffset.UtcNow; + }); + } +} + +public class SubModule : SubModuleBase +{ + internal Task Task { get; } + internal SubModule(Type parentModule, string name, Func> action) : base(parentModule, name) + { + StartTime = DateTimeOffset.UtcNow; + var stopwatch = Stopwatch.StartNew(); + Task = action(); + Task.ContinueWith(t => + { + Duration = stopwatch.Elapsed; + EndTime = DateTimeOffset.UtcNow; + }); + } +} \ No newline at end of file diff --git a/ModularPipelines/Modules/SubModuleBase.cs b/ModularPipelines/Modules/SubModuleBase.cs new file mode 100644 index 0000000000..0d2794acfa --- /dev/null +++ b/ModularPipelines/Modules/SubModuleBase.cs @@ -0,0 +1,17 @@ +namespace ModularPipelines.Modules; + +public abstract class SubModuleBase +{ + public Type ParentModule { get; } + + internal readonly string Name; + internal TimeSpan Duration { get; set; } + internal DateTimeOffset StartTime { get; set; } + internal DateTimeOffset EndTime { get; set; } + + internal SubModuleBase(Type parentModule, string name) + { + ParentModule = parentModule; + Name = name; + } +} \ No newline at end of file diff --git a/ModularPipelines/Options/CommandEnvironmentOptions.cs b/ModularPipelines/Options/CommandEnvironmentOptions.cs new file mode 100644 index 0000000000..897ffb1a40 --- /dev/null +++ b/ModularPipelines/Options/CommandEnvironmentOptions.cs @@ -0,0 +1,12 @@ +using CliWrap; + +namespace ModularPipelines.Options; + +public record CommandEnvironmentOptions +{ + public IDictionary? EnvironmentVariables { get; init; } + public string? WorkingDirectory { get; init; } + public Credentials? Credentials { get; init; } + public bool LogInput { get; init; } = true; + public bool LogOutput { get; init; } = true; +} \ No newline at end of file diff --git a/ModularPipelines/Options/CommandLineToolOptions.cs b/ModularPipelines/Options/CommandLineToolOptions.cs new file mode 100644 index 0000000000..725a2d7f06 --- /dev/null +++ b/ModularPipelines/Options/CommandLineToolOptions.cs @@ -0,0 +1,6 @@ +namespace ModularPipelines.Options; + +public record CommandLineToolOptions(string Tool) : CommandEnvironmentOptions +{ + public IEnumerable? Arguments { get; init; } +} \ No newline at end of file diff --git a/ModularPipelines/Options/DownloadOptions.cs b/ModularPipelines/Options/DownloadOptions.cs new file mode 100644 index 0000000000..3a149a1eb5 --- /dev/null +++ b/ModularPipelines/Options/DownloadOptions.cs @@ -0,0 +1,9 @@ +namespace ModularPipelines.Options; + +public record DownloadOptions(Uri DownloadUri) +{ + public HttpClient? HttpClient { get; init; } + public Action? RequestConfigurator { get; init; } + public string? SavePath { get; init; } + public bool Overwrite { get; init; } = true; +} \ No newline at end of file diff --git a/ModularPipelines/Options/ExecutionMode.cs b/ModularPipelines/Options/ExecutionMode.cs new file mode 100644 index 0000000000..3d4a258e13 --- /dev/null +++ b/ModularPipelines/Options/ExecutionMode.cs @@ -0,0 +1,7 @@ +namespace ModularPipelines.Options; + +public enum ExecutionMode +{ + StopOnFirstException, + WaitForAllModules +} \ No newline at end of file diff --git a/ModularPipelines/Options/HttpOptions.cs b/ModularPipelines/Options/HttpOptions.cs new file mode 100644 index 0000000000..52387d090f --- /dev/null +++ b/ModularPipelines/Options/HttpOptions.cs @@ -0,0 +1,8 @@ +namespace ModularPipelines.Options; + +public record HttpOptions(HttpRequestMessage HttpRequestMessage) +{ + public HttpClient? HttpClient { get; init; } + public bool LogRequest { get; init; } = true; + public bool LogResponse { get; init; } = true; +} \ No newline at end of file diff --git a/ModularPipelines.Installer/Options/InstallerOptions.cs b/ModularPipelines/Options/InstallerOptions.cs similarity index 68% rename from ModularPipelines.Installer/Options/InstallerOptions.cs rename to ModularPipelines/Options/InstallerOptions.cs index d4956baaf0..257042c43e 100644 --- a/ModularPipelines.Installer/Options/InstallerOptions.cs +++ b/ModularPipelines/Options/InstallerOptions.cs @@ -1,4 +1,4 @@ -namespace ModularPipelines.Installer.Options; +namespace ModularPipelines.Options; public record InstallerOptions(string Path) { diff --git a/ModularPipelines/Options/ModuleLoggerOptions.cs b/ModularPipelines/Options/ModuleLoggerOptions.cs new file mode 100644 index 0000000000..fdb18eb4aa --- /dev/null +++ b/ModularPipelines/Options/ModuleLoggerOptions.cs @@ -0,0 +1,8 @@ +using Microsoft.Extensions.Logging; + +namespace ModularPipelines.Options; + +public record ModuleLoggerOptions +{ + public LogLevel LogLevel { get; set; } = LogLevel.Information; +} \ No newline at end of file diff --git a/ModularPipelines/Options/PipelineOptions.cs b/ModularPipelines/Options/PipelineOptions.cs index 0206f5c985..8ffc13fb98 100644 --- a/ModularPipelines/Options/PipelineOptions.cs +++ b/ModularPipelines/Options/PipelineOptions.cs @@ -1,8 +1,9 @@ namespace ModularPipelines.Options; -public class PipelineOptions +public record PipelineOptions { - public bool StopOnFirstException { get; set; } + public ExecutionMode ExecutionMode { get; set; } = ExecutionMode.StopOnFirstException; public string[]? RunOnlyCategories { get; set; } public string[]? IgnoreCategories { get; set; } + public ModuleLoggerOptions LoggerOptions { get; } = new(); } \ No newline at end of file diff --git a/ModularPipelines.Installer/Options/WebInstallerOptions.cs b/ModularPipelines/Options/WebInstallerOptions.cs similarity index 69% rename from ModularPipelines.Installer/Options/WebInstallerOptions.cs rename to ModularPipelines/Options/WebInstallerOptions.cs index b52f7d5d69..bbf6d047ea 100644 --- a/ModularPipelines.Installer/Options/WebInstallerOptions.cs +++ b/ModularPipelines/Options/WebInstallerOptions.cs @@ -1,4 +1,4 @@ -namespace ModularPipelines.Installer.Options; +namespace ModularPipelines.Options; public record WebInstallerOptions(Uri DownloadUri) { diff --git a/ModularPipelines/SourceGenerator/DependencyTrackingSourceGenerator.cs b/ModularPipelines/SourceGenerator/DependencyTrackingSourceGenerator.cs deleted file mode 100644 index 27dea4b9cd..0000000000 --- a/ModularPipelines/SourceGenerator/DependencyTrackingSourceGenerator.cs +++ /dev/null @@ -1,58 +0,0 @@ -// using System.Runtime.CompilerServices; -// using Microsoft.CodeAnalysis; -// using Microsoft.CodeAnalysis.CSharp; -// using Microsoft.CodeAnalysis.CSharp.Syntax; -// using ModularPipelines.Modules; -// -// namespace ModularPipelines.SourceGenerator; -// -// public class DependencyTrackingSourceGenerator : IIncrementalGenerator -// { -// public void Initialize(IncrementalGeneratorInitializationContext context) -// { -// var oneOfClasses = context.SyntaxProvider -// .CreateSyntaxProvider( -// predicate: static (s, _) => IsGetModuleMethodCall(s), -// transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx)) -// .Where(static m => m is not null) -// .Collect(); -// -// context.RegisterSourceOutput(oneOfClasses, Execute); -// -// -// static bool IsGetModuleMethodCall(SyntaxNode node) -// { -// return node is InvocationExpressionSyntax -// { -// Expression: IdentifierNameSyntax -// { -// Identifier.ValueText: nameof(Module.GetModule) or nameof(Module.WaitForModule) -// } -// }; -// } -// -// static INamedTypeSymbol? GetSemanticTargetForGeneration(GeneratorSyntaxContext context) -// { -// ISymbol? symbol = context.SemanticModel.GetDeclaredSymbol(context.Node); -// -// -// -// if (context.Node is AttributeSyntax attributeSyntax) -// { -// attributeSyntax.ArgumentList.Arguments.First(). -// return symbol!.ContainingType; -// } -// -// -// -// if (symbol is not IAttribute namedTypeSymbol) -// { -// return null; -// } -// -// var attributeData = namedTypeSymbol.GetAttributes().FirstOrDefault(ad => -// string.Equals(ad.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), $"global::{AttributeNamespace}.{AttributeName}")); -// -// return attributeData is null ? null : namedTypeSymbol; -// } -// } \ No newline at end of file