diff --git a/tools/AzDev/AzDev/AzDev.format.ps1xml b/tools/AzDev/AzDev/AzDev.format.ps1xml new file mode 100644 index 000000000000..b3683b00ecb1 --- /dev/null +++ b/tools/AzDev/AzDev/AzDev.format.ps1xml @@ -0,0 +1,51 @@ + + + + + PSProject + + AzDev.Models.PSModels.PSProject + + + + + + + Name + + + Type + + + Path + + + + + + + + PSModule + + AzDev.Models.PSModels.PSModule + + + + + + + Name + + + Type + + + Path + + + + + + + + \ No newline at end of file diff --git a/tools/AzDev/AzDev/AzDev.psd1 b/tools/AzDev/AzDev/AzDev.psd1 new file mode 100644 index 000000000000..088766402bd1 --- /dev/null +++ b/tools/AzDev/AzDev/AzDev.psd1 @@ -0,0 +1,135 @@ +# +# Module manifest for module 'AzDev' +# +# Generated by: Yeming Liu +# +# Generated on: 7/5/2024 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'AzDev.psm1' + +# Version number of this module. +ModuleVersion = '0.0.1' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = 'e989d571-a6ed-4912-bf5c-7b0c55261607' + +# Author of this module +Author = 'Microsoft Corporation' + +# Company or vendor of this module +CompanyName = 'Microsoft Corporation' + +# Copyright statement for this module +Copyright = 'Microsoft Corporation. All rights reserved.' + +# Description of the functionality provided by this module +# Description = '' + +# Minimum version of the PowerShell engine required by this module +# PowerShellVersion = '' + +# Name of the PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# ClrVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +FormatsToProcess = @('AzDev.format.ps1xml') + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +NestedModules = @('bin/AzDev.dll', + 'CommonRepo.psm1') + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = 'Connect-DevCommonRepo', 'Disconnect-DevCommonRepo' + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = 'Get-DevContext', 'Set-DevContext', + 'Get-DevModule', 'Get-DevProject', + 'Open-DevSwagger' + +# Variables to export from this module +VariablesToExport = '*' + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = '*' + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + # ProjectUri = '' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + diff --git a/tools/AzDev/AzDev/AzDev.psm1 b/tools/AzDev/AzDev/AzDev.psm1 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/AzDev/AzDev/CommonRepo.psm1 b/tools/AzDev/AzDev/CommonRepo.psm1 new file mode 100644 index 000000000000..f8acf5f5f9fc --- /dev/null +++ b/tools/AzDev/AzDev/CommonRepo.psm1 @@ -0,0 +1,115 @@ +# ---------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Code generated by Microsoft (R) AutoRest Code Generator.Changes may cause incorrect behavior and will be lost if the code +# is regenerated. +# ---------------------------------------------------------------------------------- + +<# + .Synopsis + Connects azure-powershell repo to azure-powershell-common repo for debugging. + + .Description + Connects azure-powershell repo to azure-powershell-common repo for debugging. + + .Parameter CommonRepoPath + Path to the common repo. Relative or absolute. + + .Example + Connect-CommonRepo -CommonRepoPath ../azure-powershell-common +#> +function Connect-DevCommonRepo { + [CmdletBinding()] + param() + + $context = Get-DevContext + + Write-Host "1/2 Adding common projects to sln and csproj" + + $CommonRepoPath = $context.AzurePowerShellCommonRepositoryRoot + $CommonProjects = Get-ChildItem -Path "$CommonRepoPath/src/" -Include *.csproj -Exclude *.test.* -Recurse + $CommonProjects = $CommonProjects.FullName + + + $RepoRoot = $Context.AzurePowerShellRepositoryRoot + + Push-Location "$RepoRoot/src/Accounts" + try { + foreach ($csproj in $CommonProjects) { + $csproj = [System.IO.Path]::GetFullPath($csproj) + dotnet sln add $csproj + if ($LASTEXITCODE -ne 0) { + throw "Failed to add $csproj to Accounts.sln" + } + <# + known common project references: + Authentication.csproj -> Authentication.Abstractions, ResourceManager + Accounts.csproj -> Authentication.Abstractions, ResourceManager, Common + Accounts.Test.csproj -> Authentication.Abstractions, ResourceManager, Common + TestFx.csproj -> Graph.Rbac.csproj + AssemblyLoading.csproj -> Common + #> + # add all common projects to Authentication.csproj because it will be referenced by most Az projects + dotnet add ./Authentication/Authentication.csproj reference $csproj + if ($LASTEXITCODE -ne 0) { + throw "Failed to add $csproj to Authentication.csproj" + } + } + + # AssemblyLoading.csproj references Common.csproj and does not reference Authentication.csproj + dotnet add ./AssemblyLoading/AssemblyLoading.csproj reference "$CommonRepoPath/src/Common/Common.csproj" + if ($LASTEXITCODE -ne 0) { + throw "Failed to add Common.csproj to AssemblyLoading.csproj" + } + + # add common project references below for csproj which does not reference Authentication.csproj + } + finally { + Pop-Location + } + + + Write-Host "2/2: Remove the dependency of those common projects from .targets file" + + $Patterns = @( + '" + } + else { + return $line + } + } | Set-Content $TargetsFile + + Write-Host "Done connecting both repositories." +} + +function Disconnect-DevCommonRepo { + Write-Host "Please run the following commands to undo Connect-CommonRepo. Double check those files do not have wanted changes. + git checkout -- ./src/Accounts/Accounts.sln + git checkout -- ./src/Accounts/AssemblyLoading/AssemblyLoading.csproj + git checkout -- ./src/Accounts/Authentication/Authentication.csproj + git checkout -- ./tools/Common.Netcore.Dependencies.targets + " +} + +Export-ModuleMember -Function Connect-DevCommonRepo, Disconnect-DevCommonRepo diff --git a/tools/AzDev/CHANGELOG.md b/tools/AzDev/CHANGELOG.md new file mode 100644 index 000000000000..aee3e46030d3 --- /dev/null +++ b/tools/AzDev/CHANGELOG.md @@ -0,0 +1,40 @@ +## Next +- Feature: further distinguish track 1/2 SDKs, data/management plane, package/project. +- Local static analysis (Invoke-AzStaticAnalyzer) +- New-TestEnvironment and other existing tools +- Scripts to help deploy compliant resources +- Look into tools/, what else can be ported here? +- Install daily build +- Check what CLI's az dev support? +- Wildcard of project and module names - need to implement by hand? Can leverage `DirectoryInfo.GetFiles`? It supports kind of wildcard. +- Quick start templates +- Versioning and publishing AzDev module + +## 2025/1/2 +- Misc: moved to azure-powershell repo +- Feature: Connect common repo and ps repo + +## 2024/12/30 +- Fix: path issue on Windows +- Misc: Better error message when no context + +## 2024/12/27 +- Feature: Distinguish SDK-based projects, wrapper projects and other projects. +- Feature: Distinguish modules types: SdkBased, autorestBased, Hybrid. + +## 2024/12/25 +- Fix: Inventory cmdlets ignore live tests + +## 2024/10/15 +- Feature: Add reason for deducing project type + +## 2024/9/11 +- Feature: Open swagger link +- Rename SDK-based project type to "Other" because there's no accurate way to distinguish between SDK and wrapper projects + +## Older +- Fix: Two misidentified projects because of live tests: ContainerRegistry and Resources +- Feature: Get-DevProject: Add support for `-Type` parameter +- Feature: Add new ProjectType: Test, LegacyHelper, Track1Sdk +- Renamed "Submodule" to "Project" +- Feature: Added "Get-DevProject" cmdlet diff --git a/tools/AzDev/README.md b/tools/AzDev/README.md new file mode 100644 index 000000000000..4430dc8e8718 --- /dev/null +++ b/tools/AzDev/README.md @@ -0,0 +1,84 @@ +# AzDev - developer module for Azure PowerShell + +This module is designed to help developers of Azure PowerShell modules. It provides tools to assist development, troubleshooting issues, etc. + +All the cmdlets in this module are prefixed with `Dev-` to avoid conflicts with other modules. + +## Quick start + +```powershell +# build the module +./tools/AzDev/build.ps1 +# import the module +Import-Module ./artifacts/AzDev/AzDev.psd1 +# set up the context (only once) +Set-DevContext -RepoRoot 'C:\repos\azure-powershell' +``` + +## Features + +### Repo inventory + +`Get-DevModule` and `Get-DevProject` are the main cmdlets to get the inventory of the repo. + +```powershell +PS /> Get-DevModule | Select-Object -First 10 + +Name Type Path +---- ---- ---- +Maps AutoRestBased /Users/azps/workspace/azure-powershell/src/Maps +Kusto AutoRestBased /Users/azps/workspace/azure-powershell/src/Kusto +StorageMover AutoRestBased /Users/azps/workspace/azure-powershell/src/StorageMover +ResourceGraph Hybrid /Users/azps/workspace/azure-powershell/src/ResourceGraph +Terraform AutoRestBased /Users/azps/workspace/azure-powershell/src/Terraform +PostgreSql AutoRestBased /Users/azps/workspace/azure-powershell/src/PostgreSql +SpringCloud AutoRestBased /Users/azps/workspace/azure-powershell/src/SpringCloud +ManagedNetworkFabric AutoRestBased /Users/azps/workspace/azure-powershell/src/ManagedNetworkFabric +ServiceBus Hybrid /Users/azps/workspace/azure-powershell/src/ServiceBus +Mdp AutoRestBased /Users/azps/workspace/azure-powershell/src/Mdp + +PS /> Get-DevProject | Group-Object -Property Type | Select-Object -Property Name,Count | Sort-Object -Property Count -Descending + +Name Count +---- ----- +AutoRestBased 163 +Wrapper 127 +SdkBased 76 +Test 70 +Track1Sdk 48 +Other 8 +LegacyHelper 4 +``` + +### Connect azure-powershell and azure-powershell-common + +Help you connect the azure-powershell and azure-powershell-common repositories for developing or debugging. + +```powershell +# Connect +Connect-CommonRepo + +# Disconnect +Disconnect-CommonRepo +``` + +### Autorest helper + +#### Open swagger online + +`Open-DevSwagger` opens the online version of the specified swagger file. It's useful when you want to quickly check the structure of a swagger file. + +```powershell +PS /> Open-DevSwagger workloads +Multiple projects matching [workloads] + 1: SapVirtualInstance.Autorest + 2: Monitors.Autorest +Enter the number corresponding to your selection +1 +Multiple swagger references found in [SapVirtualInstance.Autorest] + 1: $(repo)/specification/workloads/resource-manager/Microsoft.Workloads/SAPVirtualInstance/readme.md + 2: $(repo)/specification/workloads/resource-manager/readme.powershell.md +Enter the number corresponding to your selection +1 +Opening https://github.com/Azure/azure-rest-api-specs/blob/202321f386ea5b0c103b46902d43b3d3c50e029c/specification/workloads/resource-manager/Microsoft.Workloads/SAPVirtualInstance/readme.md in default browser... +``` \ No newline at end of file diff --git a/tools/AzDev/Tests/DefaultContextProviderTests.cs b/tools/AzDev/Tests/DefaultContextProviderTests.cs new file mode 100644 index 000000000000..ea3bdb454282 --- /dev/null +++ b/tools/AzDev/Tests/DefaultContextProviderTests.cs @@ -0,0 +1,52 @@ +using System.IO.Abstractions.TestingHelpers; +using AzDev.Services; +using Xunit.Sdk; + +namespace AzDev.Tests; + +public class DefaultContextProviderTests +{ + [Fact] + public void ContextIO() + { + var fs = new MockFileSystem(); + const string profilePath = @"C:\DevContext.json"; + var contextProvider = new DefaultContextProvider(profilePath, fs); + // context file should not exist + Assert.False(fs.FileExists(profilePath)); + Assert.Throws(contextProvider.LoadContext); + + var context = new AzDev.Models.DevContext() + { + AzurePowerShellRepositoryRoot = @"D:\azure-powershell" + }; + contextProvider.SaveContext(context); + // you can save multiple times + contextProvider.SaveContext(context); + // now the context file should exist + Assert.True(fs.FileExists(profilePath)); + // the in-memory context should be updated + Assert.Equal(context.AzurePowerShellRepositoryRoot, contextProvider.LoadContext().AzurePowerShellRepositoryRoot); + // force reload from disk + contextProvider = new DefaultContextProvider(profilePath, fs); + Assert.Equal(context.AzurePowerShellRepositoryRoot, contextProvider.LoadContext().AzurePowerShellRepositoryRoot); + } + + [Fact] + public void LoadFromDisk() + { + const string profilePath = @"C:\DevContext.json"; + var fs = new MockFileSystem(new Dictionary + { + { profilePath, new MockFileData(@"{ + ""AzurePowerShellRepositoryRoot"":""D:\\azure-powershell"", + ""AzurePowerShellCommonRepositoryRoot"":""D:\\azure-powershell-common"", + ""UnsupportedProperty"":""value"" + }") } + }); // UnsupportedProperty should not block deserialization + var contextProvider = new DefaultContextProvider(profilePath, fs); + var context = contextProvider.LoadContext(); + Assert.Equal(@"D:\azure-powershell", context.AzurePowerShellRepositoryRoot); + Assert.Equal(@"D:\azure-powershell-common", context.AzurePowerShellCommonRepositoryRoot); + } +} \ No newline at end of file diff --git a/tools/AzDev/Tests/HelperTests/ConventionTests.cs b/tools/AzDev/Tests/HelperTests/ConventionTests.cs new file mode 100644 index 000000000000..c53eb5d466b7 --- /dev/null +++ b/tools/AzDev/Tests/HelperTests/ConventionTests.cs @@ -0,0 +1,244 @@ +using System.IO.Abstractions.TestingHelpers; +using AzDev.Models.Inventory; +using AzDev.Services; + +namespace AzDev.Tests; + +public class ConventionTests +{ + [Fact] + public void CanDetectLegacyHelperProject() + { + var path = "C:/path/to/project/Project.helper"; + Assert.True(Conventions.IsLegacyHelperProject(path, out var reason)); + Assert.NotNull(reason); + + path = "C:/path/to/project/Project.helpers"; + Assert.True(Conventions.IsLegacyHelperProject(path, out reason)); + Assert.NotNull(reason); + + path = "C:/path/to/project/Compute"; + Assert.False(Conventions.IsLegacyHelperProject(path, out reason)); + Assert.NotNull(reason); + } + + [Fact] + public void CanDetectTestProject() + { + var path = "C:/path/to/project/Project.test"; + Assert.True(Conventions.IsTestProject(path, out var reason)); + Assert.NotNull(reason); + + path = "C:/path/to/project/Compute"; + Assert.False(Conventions.IsTestProject(path, out reason)); + Assert.NotNull(reason); + } + + [Fact] + public void CanDetectTrack1SdkProject() + { + var path = "C:/path/to/project/Project.management.sdk"; + Assert.True(Conventions.IsTrack1SdkProject(path, out var reason)); + Assert.NotNull(reason); + + path = "C:/path/to/project/Compute"; + Assert.False(Conventions.IsTrack1SdkProject(path, out reason)); + Assert.NotNull(reason); + } + + [Fact] + public void CanDetectAutorestBasedProject() + { + var path = "C:/path/to/project/Project.autorest"; + Assert.True(Conventions.IsAutorestBasedProject(path, out var reason)); + Assert.NotNull(reason); + + path = "C:/path/to/project/Compute"; + Assert.False(Conventions.IsAutorestBasedProject(path, out reason)); + Assert.NotNull(reason); + } + + [Fact] + public void CanDetectWrapperProject() + { + var cd = Directory.GetCurrentDirectory(); + var split = Path.DirectorySeparatorChar; + var moduleName = "Test"; + var modulePath = $"{cd}{split}{moduleName}"; + var wrapperProjPath = $"{modulePath}{split}Test"; + var autorestProjPath = $"{modulePath}{split}Test.AutoRest"; + var fs = new MockFileSystem(new Dictionary + { + { + $"{autorestProjPath}{split}README.md", new MockFileData( + @"" + ) + }, + { + $"{wrapperProjPath}{split}Test.csproj", new MockFileData( + @" + + netstandard2.0 + + " + ) + } + }); + + Assert.True(Conventions.IsWrapperProject(fs, wrapperProjPath, out var reason), $"Reason: {reason}"); + Assert.True(Conventions.IsAutorestBasedProject(autorestProjPath, out reason), $"Reason: {reason}"); + } + + [Fact] + public void CanDetectWrapperProjectNegative() + { + var cd = Directory.GetCurrentDirectory(); + var split = Path.DirectorySeparatorChar; + var moduleName = "Test"; + var modulePath = $"{cd}{split}{moduleName}"; + var wrapperProjPath = $"{modulePath}{split}Test"; + var autorestProjPath = $"{modulePath}{split}Test.AutoRest"; + var fs = new MockFileSystem(new Dictionary + { + { + $"{autorestProjPath}{split}README.md", new MockFileData( + @"" + ) + }, + { + $"{wrapperProjPath}{split}Test.csproj", new MockFileData( + @" + + netstandard2.0' + + + " + ) + } + }); + + Assert.False(Conventions.IsWrapperProject(fs, wrapperProjPath, out var reason), $"Reason: {reason}"); + } + + [Fact] + public void CanDetectSdkBasedProject() + { + var cd = Directory.GetCurrentDirectory(); + var split = Path.DirectorySeparatorChar; + var moduleName = "Test"; + var modulePath = $"{cd}{split}{moduleName}"; + var wrapperProjPath = $"{modulePath}{split}Test"; + var autorestProjPath = $"{modulePath}{split}Test.AutoRest"; + var track1SdkBasedProjPath = $"{modulePath}{split}Test.Management"; + var newSdkBasedProjPath = $"{modulePath}{split}Test2"; + var helperSdkBasedProjPath = $"{modulePath}{split}Test3"; + var track1DataPlaneSdkBasedProjPath = $"{modulePath}{split}Test.DataPlane"; + var track2DataPlaneSdkBasedProjPath = $"{modulePath}{split}Test.DataPlane2"; + var otherProjPath = $"{modulePath}{split}Test.Other"; + var fs = new MockFileSystem(new Dictionary + { + { + $"{autorestProjPath}{split}README.md", new MockFileData( + @"" + ) + }, + { + $"{wrapperProjPath}{split}Test.csproj", new MockFileData( + @" + + netstandard2.0' + + " + ) + }, + { + $"{track1SdkBasedProjPath}{split}Test.Management.csproj", new MockFileData( + @" + + netstandard2.0 + + + " + ) + }, + { + $"{otherProjPath}{split}Other.csproj", new MockFileData( + @" + + netstandard2.0 + + + " + ) + }, + { + $"{newSdkBasedProjPath}{split}Test2.csproj", new MockFileData( + @" + + netstandard2.0 + + + " + ) + }, + { + $"{track1DataPlaneSdkBasedProjPath}{split}Test.DataPlane.csproj", new MockFileData( + @" + + netstandard2.0 + + + " + ) + }, + { + $"{track2DataPlaneSdkBasedProjPath}{split}Test.DataPlane2.csproj", new MockFileData( + @" + + netstandard2.0 + + + " + ) + }, + { + $"{helperSdkBasedProjPath}{split}Test3.csproj", new MockFileData( + @" + + netstandard2.0 + + + " + ) + } + }); + Assert.True(Conventions.IsSdkBasedProject(fs, track1SdkBasedProjPath, out var reason), $"Reason: {reason}"); + Assert.True(Conventions.IsSdkBasedProject(fs, newSdkBasedProjPath, out reason), $"Reason: {reason}"); + Assert.True(Conventions.IsSdkBasedProject(fs, helperSdkBasedProjPath, out reason), $"Reason: {reason}"); + Assert.True(Conventions.IsSdkBasedProject(fs, track1DataPlaneSdkBasedProjPath, out reason), $"Reason: {reason}"); + Assert.True(Conventions.IsSdkBasedProject(fs, track2DataPlaneSdkBasedProjPath, out reason), $"Reason: {reason}"); + Assert.False(Conventions.IsSdkBasedProject(fs, wrapperProjPath, out reason), $"Reason: {reason}"); + Assert.False(Conventions.IsSdkBasedProject(fs, autorestProjPath, out reason), $"Reason: {reason}"); + Assert.False(Conventions.IsSdkBasedProject(fs, otherProjPath, out reason), $"Reason: {reason}"); + } + + [Fact] + public void CanDeductModuleType() + { + Project autorestProj = new AutoRestProject() {Type = ProjectType.AutoRestBased}; + Project sdkProj = new SdkBasedProject() {Type = ProjectType.SdkBased}; + Module hybridModule = new Module() {Projects = new List {autorestProj, sdkProj}}; + Assert.Equal(ModuleType.Hybrid, Conventions.DeductModuleType(hybridModule.Projects, out _)); + + Project wrapperProj = new WrapperProject() {Type = ProjectType.Wrapper}; + Module wrapperModule = new Module() {Projects = new List {wrapperProj, autorestProj}}; + Assert.Equal(ModuleType.AutoRestBased, Conventions.DeductModuleType(wrapperModule.Projects, out _)); + + Project legacyHelperProj = new SdkBasedProject() {Type = ProjectType.LegacyHelper}; + Project track1SdkProj = new SdkBasedProject() {Type = ProjectType.Track1Sdk}; + Module sdkModule = new Module() {Projects = new List {sdkProj, legacyHelperProj}}; + Assert.Equal(ModuleType.SdkBased, Conventions.DeductModuleType(sdkModule.Projects, out _)); + sdkModule = new Module() {Projects = new List {sdkProj, track1SdkProj}}; + Assert.Equal(ModuleType.SdkBased, Conventions.DeductModuleType(sdkModule.Projects, out _)); + } +} diff --git a/tools/AzDev/Tests/HelperTests/CsprojReaderTests.cs b/tools/AzDev/Tests/HelperTests/CsprojReaderTests.cs new file mode 100644 index 000000000000..d3d8099b56d8 --- /dev/null +++ b/tools/AzDev/Tests/HelperTests/CsprojReaderTests.cs @@ -0,0 +1,94 @@ +using System.IO.Abstractions.TestingHelpers; +using AzDev.Models.Inventory; +using AzDev.Services; + +namespace AzDev.Tests; + +public class CsprojReaderTests +{ + [Fact] + public void CanParse() + { + var emptyProject = @"c:/repo/src/MyProject/MyProject.csproj"; + var projectWithPackageReferences = @"c:/repo/src/MyProject/Package.csproj"; + var projectWithProjectReferences = @"c:/repo/src/MyProject/Project.csproj"; + var projectWithBothReferences = @"c:/repo/src/MyProject/Both.csproj"; + var fs = new MockFileSystem(new Dictionary + { + { + emptyProject, new MockFileData( + @" + + netstandard2.0 + + " + ) + }, + { + projectWithPackageReferences, new MockFileData( + @" + + netstandard2.0 + + + + + + " + ) + }, + { + projectWithProjectReferences, new MockFileData( + @" + + netstandard2.0 + + + + + + " + ) + }, + { + projectWithBothReferences, new MockFileData( + @" + + netstandard2.0 + + + + + + + + " + ) + } + }); + + var project = CsprojReader.Parse(fs.File.ReadAllText(emptyProject)); + Assert.Empty(project.PackageReferences); + Assert.Empty(project.ProjectReferences); + + project = CsprojReader.Parse(fs.File.ReadAllText(projectWithPackageReferences)); + Assert.Equal(2, project.PackageReferences.Count()); + Assert.Contains("Newtonsoft.Json", project.PackageReferences); + Assert.Contains("System.Text.Json", project.PackageReferences); + Assert.Empty(project.ProjectReferences); + + project = CsprojReader.Parse(fs.File.ReadAllText(projectWithProjectReferences)); + Assert.Empty(project.PackageReferences); + Assert.Equal(2, project.ProjectReferences.Count()); + Assert.Contains(@"..\MyProject\MyProject.csproj", project.ProjectReferences); + Assert.Contains(@"..\OtherProject\OtherProject.csproj", project.ProjectReferences); + + project = CsprojReader.Parse(fs.File.ReadAllText(projectWithBothReferences)); + Assert.Equal(2, project.PackageReferences.Count()); + Assert.Contains("Newtonsoft.Json", project.PackageReferences); + Assert.Contains("System.Text.Json", project.PackageReferences); + Assert.Equal(2, project.ProjectReferences.Count()); + Assert.Contains(@"..\MyProject\MyProject.csproj", project.ProjectReferences); + Assert.Contains(@"..\OtherProject\OtherProject.csproj", project.ProjectReferences); + } +} \ No newline at end of file diff --git a/tools/AzDev/Tests/HelperTests/FilterHelperTests.cs b/tools/AzDev/Tests/HelperTests/FilterHelperTests.cs new file mode 100644 index 000000000000..4d1ddfbc21bd --- /dev/null +++ b/tools/AzDev/Tests/HelperTests/FilterHelperTests.cs @@ -0,0 +1,112 @@ +using AzDev.Models.Inventory; +using AzDev.Services; + +namespace AzDev.Tests; + +public class FilterHelperTests +{ + [Fact] + public void CanFilterProjects() + { + var codebase = new Codebase + { + Modules = new List + { + new Module + { + Name = "FirstModule", + Projects = new List + { + new OtherProject { Name = "AlphaProject" } + } + }, + new Module + { + Name = "SecondModule", + Projects = new List + { + new OtherProject { Name = "BetaProject" } + } + } + } + }; + + // filter by module name + var filter = "first"; + var result = codebase.FilterProjects(filter); + Assert.Single(result); + Assert.Equal("AlphaProject", result.First().Name); + + filter = "second"; + result = codebase.FilterProjects(filter); + Assert.Single(result); + Assert.Equal("BetaProject", result.First().Name); + + filter = "module"; + result = codebase.FilterProjects(filter); + Assert.Equal(2, result.Count()); + + filter = "third"; + result = codebase.FilterProjects(filter); + Assert.Empty(result); + + // filter by project name + filter = "alpha"; + result = codebase.FilterProjects(filter); + Assert.Single(result); + Assert.Equal("AlphaProject", result.First().Name); + + filter = "beta"; + result = codebase.FilterProjects(filter); + Assert.Single(result); + Assert.Equal("BetaProject", result.First().Name); + + filter = "project"; + result = codebase.FilterProjects(filter); + Assert.Equal(2, result.Count()); + + filter = "gamma"; + result = codebase.FilterProjects(filter); + Assert.Empty(result); + } + + [Fact] + public void FilterProjectsThrowsOnNullOrEmptyFilter() + { + var codebase = new Codebase(); + Assert.Throws(() => codebase.FilterProjects(null)); + Assert.Throws(() => codebase.FilterProjects(string.Empty)); + } + + [Fact] + public void ReturnBothWhenModuleAndProjectSameName() + { + var codebase = new Codebase + { + Modules = new List + { + new Module + { + Name = "FirstModule", + Projects = new List + { + new OtherProject { Name = "AlphaProject" } + } + }, + new Module + { + Name = "SecondModule", + Projects = new List + { + new OtherProject { Name = "BetaProject (First)" } + } + } + } + }; + + var filter = "first"; + var result = codebase.FilterProjects(filter); + Assert.Equal(2, result.Count()); + } + +} \ No newline at end of file diff --git a/tools/AzDev/Tests/HelperTests/YamlTests.cs b/tools/AzDev/Tests/HelperTests/YamlTests.cs new file mode 100644 index 000000000000..d87df51d76a0 --- /dev/null +++ b/tools/AzDev/Tests/HelperTests/YamlTests.cs @@ -0,0 +1,51 @@ +using AzDev.Models.Inventory; + +namespace AzDev.Tests; + +public class YamlTests +{ + [Fact] + public void CanDeserialize() + { + var yaml = @"module-version: 0.1.0 +title: Alb +subject-prefix: $(service-name) +inlining-threshold: 100 + +# pin the swagger version by using the commit id instead of branch name +commit: 1b338481329645df2d9460738cbaab6109472488 +require: +# readme.azure.noprofile.md is the common configuration file + - $(this-folder)/../../readme.azure.noprofile.md + - $(repo)/specification/servicenetworking/resource-manager/readme.md + +try-require: + - $(repo)/specification/servicenetworking/resource-manager/readme.powershell.md + +directive: + # Fix swagger issues + - from: swagger-document + where: $.definitions.TrafficControllerUpdateProperties + transform: delete $['properties']"; + var result = YamlHelper.Deserialize(yaml); + Assert.Equal("Alb", result.Title); + Assert.Equal("1b338481329645df2d9460738cbaab6109472488", result.Commit); + Assert.Equal(2, result.Require.Count()); + Assert.Single(result.TryRequire); + Assert.Single(result.Directive); + Assert.Empty(result.InputFile); + } + + [Fact] + public void DefaultValues() + { + var yaml = @"module-version: 0.1.0"; + var result = YamlHelper.Deserialize(yaml); + Assert.Null(result.Title); + Assert.Null(result.Commit); + Assert.Empty(result.Require); + Assert.Empty(result.TryRequire); + Assert.Empty(result.Directive); + Assert.Empty(result.InputFile); + } +} diff --git a/tools/AzDev/Tests/ModelTests/ModuleTests.cs b/tools/AzDev/Tests/ModelTests/ModuleTests.cs new file mode 100644 index 000000000000..fb1b4d634783 --- /dev/null +++ b/tools/AzDev/Tests/ModelTests/ModuleTests.cs @@ -0,0 +1,53 @@ +using System.IO.Abstractions.TestingHelpers; +using AzDev.Models.Inventory; + +namespace AzDev.Tests; + +public class ModuleTests +{ + [Fact] + public void CanCreateFromFileSystem() + { + var cd = Directory.GetCurrentDirectory(); + var split = Path.DirectorySeparatorChar; + var moduleName = "Test"; + var path = $"{cd}{split}{moduleName}"; + var projectName = "Test.AutoRest"; + var fs = new MockFileSystem(new Dictionary + { + { $"{path}{split}{projectName}{split}Test.csproj", new MockFileData( + @"" + )} + }); + + var module = Module.FromFileSystem(fs, path); + Assert.Equal(path, module.Path); + Assert.Equal(moduleName, module.Name); + Assert.Single(module.Projects); + } + + [Fact] + public void CanRecognizeBothProjectTypes() + { + var cd = Directory.GetCurrentDirectory(); + var split = Path.DirectorySeparatorChar; + var moduleName = "Test"; + var path = $"{cd}{split}{moduleName}"; + var sdkProjectName = "Beta"; + var generatedProjectName = "Alpha.AutoRest"; + var fs = new MockFileSystem(new Dictionary + { + { $"{path}{split}{generatedProjectName}{split}Alpha.csproj", new MockFileData( + @"" + )}, + { $"{path}{split}{sdkProjectName}{split}Beta.csproj", new MockFileData( + @"" + )}, + }); + + var module = Module.FromFileSystem(fs, path); + Assert.Equal(2, module.Projects.Count()); + Assert.Equal(ProjectType.AutoRestBased, module.Projects.ElementAt(0).Type); + Assert.Equal(ProjectType.Other, module.Projects.ElementAt(1).Type); + } +} diff --git a/tools/AzDev/Tests/ModelTests/ProjectTests.cs b/tools/AzDev/Tests/ModelTests/ProjectTests.cs new file mode 100644 index 000000000000..067a4f79bb2c --- /dev/null +++ b/tools/AzDev/Tests/ModelTests/ProjectTests.cs @@ -0,0 +1,27 @@ +using System.IO.Abstractions.TestingHelpers; +using AzDev.Models.Inventory; + +namespace AzDev.Tests; + +public class ProjectTests +{ + [Fact] + public void CanCreateFromFileSystem() + { + var cd = Directory.GetCurrentDirectory(); + var split = Path.DirectorySeparatorChar; + var projectName = "Test.AutoRest"; + var path = $"{cd}{split}{projectName}"; + var readme = $"{path}{split}README.md"; + var fs = new MockFileSystem(new Dictionary + { + { readme, new MockFileData( + @"" + )} + }); + + var project = Project.FromFileSystem(fs, path); + Assert.Equal(path, project.Path); + Assert.Equal(projectName, project.Name); + } +} diff --git a/tools/AzDev/Tests/ModelTests/SwaggerTests.cs b/tools/AzDev/Tests/ModelTests/SwaggerTests.cs new file mode 100644 index 000000000000..4a62023e584f --- /dev/null +++ b/tools/AzDev/Tests/ModelTests/SwaggerTests.cs @@ -0,0 +1,27 @@ +using System.IO.Abstractions.TestingHelpers; +using AzDev.Models.Inventory; + +namespace AzDev.Tests; + +public class SwaggerTests +{ + [Fact] + public void CanParseSwaggerJson() + { + var uri = "$(repo)/specification/dnsresolver/resource-manager/Microsoft.Network/stable/2022-07-01/dnsresolver.json"; + var commit = "commit_hash"; + var swagger = new SwaggerReference(uri, commit); + Assert.Equal(uri, swagger.RawUri); + Assert.Equal($"https://github.com/Azure/azure-rest-api-specs/blob/{commit}/specification/dnsresolver/resource-manager/Microsoft.Network/stable/2022-07-01/dnsresolver.json", swagger.Uri); + } + + [Fact] + public void CanParseSwaggerReadme() + { + var uri = "$(repo)/specification/servicenetworking/resource-manager/readme.md"; + var commit = "commit_hash"; + var swagger = new SwaggerReference(uri, commit); + Assert.Equal(uri, swagger.RawUri); + Assert.Equal($"https://github.com/Azure/azure-rest-api-specs/blob/{commit}/specification/servicenetworking/resource-manager/readme.md", swagger.Uri); + } +} diff --git a/tools/AzDev/Tests/PSTests/InventoryTests.ps1 b/tools/AzDev/Tests/PSTests/InventoryTests.ps1 new file mode 100644 index 000000000000..ac62ba1ec974 --- /dev/null +++ b/tools/AzDev/Tests/PSTests/InventoryTests.ps1 @@ -0,0 +1,21 @@ +BeforeAll { + if (-not (Get-Module AzDev)) { + Import-Module "$PSScriptRoot/../../../../artifacts/AzDev/AzDev.psd1" + } +} + +Describe 'Repo inventory' { + It 'Every autorest project should have either a Wrapper or SdkBased project' { + (Get-DevModule).Project.Count | Should -BeGreaterThan 0 + Get-DevModule | ForEach-Object{ + if ($_.Project.Type -contains "AutoRestBased") { + $_.Project.Type -contains "Wrapper" -or $_.Project.Type -contains "SdkBased" | Should -BeTrue + } + } + } + + It 'The number of unidentified project types should not grow' { + $others = Get-DevProject -Type Other + $others.Count | Should -BeLessOrEqual 8 + } +} diff --git a/tools/AzDev/Tests/Tests.csproj b/tools/AzDev/Tests/Tests.csproj new file mode 100644 index 000000000000..d657b3fb2666 --- /dev/null +++ b/tools/AzDev/Tests/Tests.csproj @@ -0,0 +1,26 @@ + + + + + + net6.0 + false + enable + enable + true + + + + + + + + + + + + + + + + diff --git a/tools/AzDev/azpsdev.sln b/tools/AzDev/azpsdev.sln new file mode 100644 index 000000000000..bd12f4715892 --- /dev/null +++ b/tools/AzDev/azpsdev.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzDev", "src\AzDev.csproj", "{749AA94F-C21C-4512-A6D0-B7DFE1366719}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{A7C32887-E3B2-4478-8C05-B370036BAFC1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {749AA94F-C21C-4512-A6D0-B7DFE1366719}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {749AA94F-C21C-4512-A6D0-B7DFE1366719}.Debug|Any CPU.Build.0 = Debug|Any CPU + {749AA94F-C21C-4512-A6D0-B7DFE1366719}.Release|Any CPU.ActiveCfg = Release|Any CPU + {749AA94F-C21C-4512-A6D0-B7DFE1366719}.Release|Any CPU.Build.0 = Release|Any CPU + {A7C32887-E3B2-4478-8C05-B370036BAFC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7C32887-E3B2-4478-8C05-B370036BAFC1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7C32887-E3B2-4478-8C05-B370036BAFC1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7C32887-E3B2-4478-8C05-B370036BAFC1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {22DEC244-F4A8-44E9-8F52-C3AD674CC437} + EndGlobalSection +EndGlobal diff --git a/tools/AzDev/build.ps1 b/tools/AzDev/build.ps1 new file mode 100644 index 000000000000..c805d2795e95 --- /dev/null +++ b/tools/AzDev/build.ps1 @@ -0,0 +1,5 @@ +$module = 'AzDev' +$artifacts = "$PSScriptRoot/../../artifacts" + +dotnet publish $PSScriptRoot/src --sc -o "$artifacts/$module/bin" +Copy-Item "$PSScriptRoot/$module/*" "$artifacts/$module" -Recurse -Force diff --git a/tools/AzDev/src/AzDev.csproj b/tools/AzDev/src/AzDev.csproj new file mode 100644 index 000000000000..71629488d001 --- /dev/null +++ b/tools/AzDev/src/AzDev.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.0 + + + + + + + + + + diff --git a/tools/AzDev/src/Cmdlets/Context/GetContextCmdlet.cs b/tools/AzDev/src/Cmdlets/Context/GetContextCmdlet.cs new file mode 100644 index 000000000000..c14464c3be79 --- /dev/null +++ b/tools/AzDev/src/Cmdlets/Context/GetContextCmdlet.cs @@ -0,0 +1,30 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Management.Automation; +using AzDev.Models.PSModels; + +namespace AzDev.Cmdlets.Context +{ + [Cmdlet("Get", "DevContext")] + [OutputType(typeof(PSDevContext))] + public class GetContextCmdlet : DevCmdletBase + { + protected override void ProcessRecord() + { + base.ProcessRecord(); + WriteObject(new PSDevContext(Context, ContextProvider.ContextPath)); + } + } +} diff --git a/tools/AzDev/src/Cmdlets/Context/SetContextCmdlet.cs b/tools/AzDev/src/Cmdlets/Context/SetContextCmdlet.cs new file mode 100644 index 000000000000..cbe76920b322 --- /dev/null +++ b/tools/AzDev/src/Cmdlets/Context/SetContextCmdlet.cs @@ -0,0 +1,73 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.IO; +using System.Management.Automation; +using AzDev.Models; +using AzDev.Models.PSModels; + +namespace AzDev.Cmdlets.Context +{ + [Cmdlet("Set", "DevContext")] + [OutputType(typeof(PSDevContext))] + public class SetContextCmdlet : DevCmdletBase + { + [Parameter()] + [Alias("AzurePowerShellRepositoryRoot")] + public string RepoRoot { get; set; } + + [Parameter()] + [Alias("AzurePowerShellCommonRepositoryRoot")] + public string CommonRepoRoot { get; set; } + + protected override void ProcessRecord() + { + base.ProcessRecord(); + + DevContext context; + try { context = ContextProvider.LoadContext(); } + catch + { + // ignore if context is not found + context = new DevContext(); + } + + SetPathProperty(nameof(RepoRoot), x => context.AzurePowerShellRepositoryRoot = x); + SetPathProperty(nameof(CommonRepoRoot), x => context.AzurePowerShellCommonRepositoryRoot = x); + ContextProvider.SaveContext(context); + WriteObject(new PSDevContext(context, ContextProvider.ContextPath)); + } + + private void SetPathProperty(string parameterName, Action propertySetter) + { + if (MyInvocation.BoundParameters.ContainsKey(parameterName)) + { + string path = (string)MyInvocation.BoundParameters[parameterName]; + + if (!string.IsNullOrEmpty(path)) + { + string fullPath = Path.IsPathRooted(path) ? path : Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, path); + if (Directory.Exists(fullPath)) + { + propertySetter(Path.GetFullPath(fullPath)); + return; + } + } + + throw new ArgumentException($"The input path [{path}] is incorrect or does not exist.]"); + } + } + } +} diff --git a/tools/AzDev/src/Cmdlets/DevCmdletBase.cs b/tools/AzDev/src/Cmdlets/DevCmdletBase.cs new file mode 100644 index 000000000000..506706b58843 --- /dev/null +++ b/tools/AzDev/src/Cmdlets/DevCmdletBase.cs @@ -0,0 +1,121 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using AzDev.Models; +using AzDev.Models.Inventory; +using AzDev.Services; + +namespace AzDev.Cmdlets +{ + public abstract class DevCmdletBase : PSCmdlet, IModuleAssemblyInitializer + { + internal DevContext Context + { + get + { + try + { + return ContextProvider.LoadContext(); + } + catch (FileNotFoundException) + { + WriteWarning("Run Set-DevContext to set context."); + throw; + } + } + } + + internal Codebase Codebase + { + get + { + try + { + return CodebaseProvider.GetCodebase(); + } + catch (FileNotFoundException) + { + WriteWarning("Run Set-DevContext to set context."); + throw; + } + } + } + + /// + /// Gets the context provider. Use property to get the context. + /// + internal IContextProvider ContextProvider => AzDevModule.GetComponent(nameof(IContextProvider)); + + /// + /// Gets the codebase provider. Use property to get the codebase. + /// + internal ICodebaseProvider CodebaseProvider => AzDevModule.GetComponent(nameof(ICodebaseProvider)); + + public DevCmdletBase() + { + } + + public void OnImport() + { + var contextProvider = new DefaultContextProvider(Constants.DevContextFilePath); + var codebaseProvider = new DefaultCodebaseProvider(contextProvider); + AzDevModule.SetComponent(nameof(IContextProvider), contextProvider); + AzDevModule.SetComponent(nameof(ICodebaseProvider), codebaseProvider); + } + + protected T SelectFrom(string message, IEnumerable options, bool retryIfInvalid = true) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (options.Count() == 1) + { + return options.First(); + } + + while (true) + { + Host.UI.WriteLine(message); + var index = 1; + foreach (var option in options) + { + Host.UI.WriteLine($" {index++}: {option}"); + } + Host.UI.WriteLine("Enter the number corresponding to your selection"); + + if (int.TryParse(Host.UI.ReadLine(), out int choice) + && choice >= 1 && choice <= options.Count()) + { + return options.ElementAt(choice - 1); + } + else if (!retryIfInvalid) + { + WriteWarning("Invalid selection."); + return default; + } + else + { + WriteWarning("Invalid selection. Please try again."); + } + } + } + } +} diff --git a/tools/AzDev/src/Cmdlets/Inventory/GetModuleCmdlet.cs b/tools/AzDev/src/Cmdlets/Inventory/GetModuleCmdlet.cs new file mode 100644 index 000000000000..6f0b73b1c8c6 --- /dev/null +++ b/tools/AzDev/src/Cmdlets/Inventory/GetModuleCmdlet.cs @@ -0,0 +1,50 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Linq; +using System.Management.Automation; +using AzDev.Models.PSModels; +using AzDev.Models.Inventory; +using ModuleType = AzDev.Models.Inventory.ModuleType; + +namespace AzDev.Cmdlets.Inventory +{ + [Cmdlet("Get", "DevModule")] + [OutputType(typeof(PSModule))] + public class GetModuleCmdlet : DevCmdletBase + { + [Parameter(Position = 0, Mandatory = false)] + public string Name { get; set; } + + [Parameter(Mandatory = false)] + public ModuleType Type { get; set; } + + protected override void ProcessRecord() + { + base.ProcessRecord(); + + var modules = Codebase.Modules; + if (MyInvocation.BoundParameters.ContainsKey(nameof(Name))) + { + modules = modules.Where(x => x.Name.Equals(Name, System.StringComparison.InvariantCultureIgnoreCase)); + } + if (MyInvocation.BoundParameters.ContainsKey(nameof(Type))) + { + modules = modules.Where(x => x.Type == Type); + } + + WriteObject(modules.Select(m => new PSModule(m)), true); + } + } +} diff --git a/tools/AzDev/src/Cmdlets/Inventory/GetProjectCmdlet.cs b/tools/AzDev/src/Cmdlets/Inventory/GetProjectCmdlet.cs new file mode 100644 index 000000000000..4d5f1e6adb53 --- /dev/null +++ b/tools/AzDev/src/Cmdlets/Inventory/GetProjectCmdlet.cs @@ -0,0 +1,60 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Linq; +using System.Management.Automation; +using AzDev.Models.PSModels; +using AzDev.Models.Inventory; + +namespace AzDev.Cmdlets.Inventory +{ + [Cmdlet("Get", "DevProject")] + [OutputType(typeof(PSProject))] + public class GetProjectCmdlet : DevCmdletBase + { + [Parameter(Position = 0, Mandatory = false)] + [Alias("ProjectName")] + public string Name { get; set; } + + [Parameter(Position = 1, Mandatory = false)] + public string ModuleName { get; set; } + + [Parameter(Mandatory = false)] + public ProjectType Type { get; set; } + + protected override void ProcessRecord() + { + base.ProcessRecord(); + + var modules = MyInvocation.BoundParameters.ContainsKey(nameof(ModuleName)) + ? Codebase.Modules.Where(x => x.Name.Equals(ModuleName, System.StringComparison.InvariantCultureIgnoreCase)) + : Codebase.Modules; + var projects = modules.SelectMany(x => x.Projects); + + if (MyInvocation.BoundParameters.ContainsKey(nameof(Name))) + { + // filter by name + projects = projects.Where(x => x.Name.Equals(Name, System.StringComparison.InvariantCultureIgnoreCase)); + } + + if (MyInvocation.BoundParameters.ContainsKey(nameof(Type))) + { + // filter by type + projects = projects.Where(x => x.Type == Type); + } + + WriteObject(projects.Select(p => new PSProject(p)), true); + } + } +} diff --git a/tools/AzDev/src/Cmdlets/Inventory/GetRepoCmdlet.cs b/tools/AzDev/src/Cmdlets/Inventory/GetRepoCmdlet.cs new file mode 100644 index 000000000000..6d36a0094f48 --- /dev/null +++ b/tools/AzDev/src/Cmdlets/Inventory/GetRepoCmdlet.cs @@ -0,0 +1,31 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Management.Automation; + +namespace AzDev.Cmdlets.Inventory +{ + [Cmdlet("Get", "DevCodebase")] + [Obsolete("Useless cmdlet")] + public class GetRepoCmdlet : DevCmdletBase + { + protected override void ProcessRecord() + { + base.ProcessRecord(); + + WriteObject(Codebase); + } + } +} diff --git a/tools/AzDev/src/Cmdlets/Swagger/OpenSwaggerCmdlet.cs b/tools/AzDev/src/Cmdlets/Swagger/OpenSwaggerCmdlet.cs new file mode 100644 index 000000000000..7fb304fd3a83 --- /dev/null +++ b/tools/AzDev/src/Cmdlets/Swagger/OpenSwaggerCmdlet.cs @@ -0,0 +1,60 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using AzDev.Models.Inventory; +using AzDev.Services; + +namespace AzDev.Cmdlets.Swagger +{ + /// + /// Open-DevSwagger -Search "Graph" + /// Get-AzDevModule -Name "Graph" | Get-AzDevProject | Get-AzDevSwagger | Open-DevSwagger ? + /// + [Cmdlet("Open", "DevSwagger")] + public class OpenSwaggerCmdlet : DevCmdletBase + { + public const string SearchParameterSet = "Search"; + + [Parameter(Position = 0, Mandatory = true, ParameterSetName = SearchParameterSet)] + [ValidateNotNullOrEmpty] + public string Search { get; set; } + + protected override void ProcessRecord() + { + base.ProcessRecord(); + + if (ParameterSetName == SearchParameterSet) + { + IEnumerable projects = Codebase.FilterProjects(Search) + .Where(p => p is AutoRestProject) + .Cast(); + if (!projects.Any()) + { + WriteWarning($"No projects found for search term '{Search}'."); + return; + } else { + WriteDebug($"Found {projects.Count()} projects for search term '{Search}'. They are: {string.Join(", ", projects.Select(p => p.Name))}"); + } + AutoRestProject project = SelectFrom($"Multiple projects matching [{Search}]", projects); + IEnumerable swaggers = project.Swaggers; + SwaggerReference swagger = SelectFrom($"Multiple swagger references found in [{project.Name}]", swaggers); + Host.UI.WriteLine($"Opening {swagger.Uri} in default browser..."); + swagger.OpenOnline(); + } + } + } +} diff --git a/tools/AzDev/src/Models/Constants.cs b/tools/AzDev/src/Models/Constants.cs new file mode 100644 index 000000000000..1d0be77e39f4 --- /dev/null +++ b/tools/AzDev/src/Models/Constants.cs @@ -0,0 +1,24 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.IO; + +namespace AzDev.Models { + internal static class Constants + { + public const string DevContextFileName = "DevContext.json"; + public static string DevContextFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AzPSDev", DevContextFileName); + } +} diff --git a/tools/AzDev/src/Models/DevContext.cs b/tools/AzDev/src/Models/DevContext.cs new file mode 100644 index 000000000000..0d5181a605a5 --- /dev/null +++ b/tools/AzDev/src/Models/DevContext.cs @@ -0,0 +1,26 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace AzDev.Models +{ + public class DevContext + { + public string AzurePowerShellRepositoryRoot { get; set; } + public string AzurePowerShellCommonRepositoryRoot { get; set; } + + public DevContext() + { + } + } +} diff --git a/tools/AzDev/src/Models/Inventory/AutoRestProject.cs b/tools/AzDev/src/Models/Inventory/AutoRestProject.cs new file mode 100644 index 000000000000..cce639fa7a4b --- /dev/null +++ b/tools/AzDev/src/Models/Inventory/AutoRestProject.cs @@ -0,0 +1,89 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using System.Text.RegularExpressions; +using AzDev.Services; + +namespace AzDev.Models.Inventory +{ + internal class AutoRestProject : Project + { + protected AutoRestProject(IFileSystem fs, string path) : base(fs, path) + { + _lazySwaggers = new Lazy>(LoadSwaggers); + _lazyReadmeText = new Lazy(() => LoadReadme()); + } + internal AutoRestProject() {} + public IEnumerable Swaggers => _lazySwaggers.Value; + private Lazy> _lazySwaggers; + private string ReadmeText => _lazyReadmeText.Value; + private Lazy _lazyReadmeText; + + public new static AutoRestProject FromFileSystem(IFileSystem fs, string path) + { + return new AutoRestProject(fs, path) + { + Type = ProjectType.AutoRestBased, + Name = fs.Path.GetFileName(path) + }; + } + + private IEnumerable LoadSwaggers() + { + Regex yamlBlockPattern = new Regex(@"(?ms)```\s*yaml(.*?)```"); + var matches = yamlBlockPattern.Matches(ReadmeText); + var yamlBlocks = new List(); + if (matches.Count == 0) + { + throw new Exception($"No YAML blocks found in README.md for [{Path}]"); + } + else + { + foreach (Match match in matches) + { + yamlBlocks.Add(match.Groups[1].Value.Trim()); + } + } + + var swaggers = new List(); + return yamlBlocks.Select(y => YamlHelper.Deserialize(y)) + .Where(c => c != null) + .SelectMany(c => + c.InputFile.Concat(c.Require).Concat(c.TryRequire) + .Where(uri => !Conventions.IsAutorestInputButNotSwagger(uri)) + .Select(uri => new SwaggerReference(uri, c.Commit))); + } + + private string LoadReadme() + { + foreach (var file in FileSystem.Directory.GetFiles(Path, "*.md")) + { + if (file.EndsWith("readme.md", StringComparison.OrdinalIgnoreCase)) + { + if (!file.EndsWith("README.md", StringComparison.Ordinal)) + { + throw new Exception($"Found incorrect casing of README.md in [{file}]"); + } + return FileSystem.File.ReadAllText(file); + } + } + throw new FileNotFoundException($"README.md not found in [{Path}]"); + } + } +} diff --git a/tools/AzDev/src/Models/Inventory/AutoRestYamlConfig.cs b/tools/AzDev/src/Models/Inventory/AutoRestYamlConfig.cs new file mode 100644 index 000000000000..f62c24dca7fe --- /dev/null +++ b/tools/AzDev/src/Models/Inventory/AutoRestYamlConfig.cs @@ -0,0 +1,47 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using YamlDotNet.Serialization; + +namespace AzDev.Models.Inventory +{ + internal class AutoRestYamlConfig + { + [YamlMember(Alias = "title")] + public string Title { get => _title; set => _title = value?.Trim(); } + private string _title; + + [YamlMember(Alias = "input-file")] + public IEnumerable InputFile { get => _inputFile; set => _inputFile = value?.Select(x => x.Trim())?.Where(x => !string.IsNullOrWhiteSpace(x)) ?? Enumerable.Empty(); } + private IEnumerable _inputFile = Enumerable.Empty(); + + [YamlMember(Alias = "commit")] + public string Commit { get => _commit; set => _commit = value?.Trim(); } + private string _commit; + + [YamlMember(Alias = "require")] + public IEnumerable Require { get => _require; set => _require = value?.Select(x => x.Trim())?.Where(x => !string.IsNullOrWhiteSpace(x)) ?? Enumerable.Empty(); } + private IEnumerable _require = Enumerable.Empty(); + + [YamlMember(Alias = "try-require")] + public IEnumerable TryRequire { get => _tryRequire; set => _tryRequire = value?.Select(x => x.Trim())?.Where(x => !string.IsNullOrWhiteSpace(x)) ?? Enumerable.Empty(); } + private IEnumerable _tryRequire = Enumerable.Empty(); + + [YamlMember(Alias = "directive")] + public IEnumerable Directive { get; internal set; } = Enumerable.Empty(); + } +} diff --git a/tools/AzDev/src/Models/Inventory/Codebase.cs b/tools/AzDev/src/Models/Inventory/Codebase.cs new file mode 100644 index 000000000000..3c062246a30d --- /dev/null +++ b/tools/AzDev/src/Models/Inventory/Codebase.cs @@ -0,0 +1,40 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Linq; +using AzDev.Services; + +namespace AzDev.Models.Inventory +{ + internal class Codebase : IFileSystemBasedModel + { + public string Path { get; internal set; } + public IEnumerable Modules { get; internal set; } + + internal static Codebase FromFileSystem(IFileSystem fs, string path) + { + return new Codebase() + { + Path = path, + Modules = fs.Directory.GetDirectories(path) + .Where(dir => !Conventions.IsExcludedModuleDirectory(dir)) + .Select(dir => Module.FromFileSystem(fs, dir)) + .Where(module => module != null) + .ToList() + }; + } + } +} diff --git a/tools/AzDev/src/Models/Inventory/IFileSystemBasedModel.cs b/tools/AzDev/src/Models/Inventory/IFileSystemBasedModel.cs new file mode 100644 index 000000000000..2a428496760c --- /dev/null +++ b/tools/AzDev/src/Models/Inventory/IFileSystemBasedModel.cs @@ -0,0 +1,21 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace AzDev.Models.Inventory +{ + internal interface IFileSystemBasedModel + { + string Path { get; } + } +} diff --git a/tools/AzDev/src/Models/Inventory/Module.cs b/tools/AzDev/src/Models/Inventory/Module.cs new file mode 100644 index 000000000000..16099f189872 --- /dev/null +++ b/tools/AzDev/src/Models/Inventory/Module.cs @@ -0,0 +1,53 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Linq; +using AzDev.Services; + +namespace AzDev.Models.Inventory +{ + internal class Module : IFileSystemBasedModel + { + public string Name { get; internal set; } + public string Path { get; internal set; } + public IEnumerable Projects { get; internal set; } = Enumerable.Empty(); + public ModuleType Type { get; internal set; } + public string TypeDeductionReason { get; internal set; } + + protected IFileSystem FileSystem { get; } + protected Module(IFileSystem fs, string path) + { + FileSystem = fs; + Path = path; + } + internal Module() {} + + public static Module FromFileSystem(IFileSystem fs, string path) + { + Module m = new Module(fs, path) + { + Name = fs.Path.GetFileName(path), + Projects = fs.Directory.GetDirectories(path) + .Where(dir => !Conventions.IsExcludedProjectDirectory(fs, dir, out _)) + .Select(dir => Project.FromFileSystem(fs, dir)) + .ToList() + }; + + (m.Type, m.TypeDeductionReason) = (Conventions.DeductModuleType(m.Projects, out string reason), reason); + return m; + } + } +} diff --git a/tools/AzDev/src/Models/Inventory/ModuleType.cs b/tools/AzDev/src/Models/Inventory/ModuleType.cs new file mode 100644 index 000000000000..2e738c6a91d5 --- /dev/null +++ b/tools/AzDev/src/Models/Inventory/ModuleType.cs @@ -0,0 +1,24 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace AzDev.Models.Inventory +{ + public enum ModuleType + { + Other, + AutoRestBased, + SdkBased, + Hybrid + } +} diff --git a/tools/AzDev/src/Models/Inventory/OtherProject.cs b/tools/AzDev/src/Models/Inventory/OtherProject.cs new file mode 100644 index 000000000000..02b8e2445380 --- /dev/null +++ b/tools/AzDev/src/Models/Inventory/OtherProject.cs @@ -0,0 +1,32 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.IO.Abstractions; + +namespace AzDev.Models.Inventory +{ + internal class OtherProject : Project + { + protected OtherProject(IFileSystem fs, string path) : base(fs, path) { } + internal OtherProject() { } + public new static OtherProject FromFileSystem(IFileSystem fs, string path) + { + return new OtherProject(fs, path) + { + Type = ProjectType.Other, + Name = fs.Path.GetFileName(path) + }; + } + } +} diff --git a/tools/AzDev/src/Models/Inventory/Project.cs b/tools/AzDev/src/Models/Inventory/Project.cs new file mode 100644 index 000000000000..25cafa29fb6d --- /dev/null +++ b/tools/AzDev/src/Models/Inventory/Project.cs @@ -0,0 +1,128 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.IO.Abstractions; +using AzDev.Services; + +namespace AzDev.Models.Inventory +{ + internal abstract class Project : IFileSystemBasedModel + { + public string Name { get; internal set; } + public string Path { get; internal set; } + public ProjectType Type { get; internal set; } + public string TypeDeductionReason { get; internal set; } + + protected IFileSystem FileSystem { get; } + + public override string ToString() => Name; + + protected Project(IFileSystem fs, string path) + { + FileSystem = fs; + Path = path; + } + internal Project() { } + + public static Project FromFileSystem(IFileSystem fs, string path) + { + Project project; + string typeDeductionReason; + if (Conventions.IsLegacyHelperProject(path, out typeDeductionReason)) + { + project = LegacyHelperProject.FromFileSystem(fs, path); + project.TypeDeductionReason = typeDeductionReason; + } + else if (Conventions.IsTrack1SdkProject(path, out typeDeductionReason)) + { + project = Track1SdkProject.FromFileSystem(fs, path); + project.TypeDeductionReason = typeDeductionReason; + } + else if (Conventions.IsTestProject(path, out typeDeductionReason)) + { + project = TestProject.FromFileSystem(fs, path); + project.TypeDeductionReason = typeDeductionReason; + } + else if (Conventions.IsAutorestBasedProject(path, out typeDeductionReason)) + { + project = AutoRestProject.FromFileSystem(fs, path); + project.TypeDeductionReason = typeDeductionReason; + } + else if (Conventions.IsWrapperProject(fs, path, out typeDeductionReason)) + { + project = WrapperProject.FromFileSystem(fs, path); + project.TypeDeductionReason = typeDeductionReason; + } + else if (Conventions.IsSdkBasedProject(fs, path, out typeDeductionReason)) + { + project = SdkBasedProject.FromFileSystem(fs, path); + project.TypeDeductionReason = typeDeductionReason; + } + else + { + project = OtherProject.FromFileSystem(fs, path); + project.TypeDeductionReason = $"[{path}] failed to be identified as any types."; + } + return project; + } + } + + internal class TestProject : Project + { + public TestProject(IFileSystem fs, string path) : base(fs, path) + { + } + + public new static TestProject FromFileSystem(IFileSystem fs, string path) + { + return new TestProject(fs, path) + { + Type = ProjectType.Test, + Name = fs.Path.GetFileName(path) + }; + } + } + + internal class LegacyHelperProject : Project + { + public LegacyHelperProject(IFileSystem fs, string path) : base(fs, path) + { + } + + public new static LegacyHelperProject FromFileSystem(IFileSystem fs, string path) + { + return new LegacyHelperProject(fs, path) + { + Type = ProjectType.LegacyHelper, + Name = fs.Path.GetFileName(path) + }; + } + } + + internal class Track1SdkProject : Project + { + public Track1SdkProject(IFileSystem fs, string path) : base(fs, path) + { + } + + public new static Track1SdkProject FromFileSystem(IFileSystem fs, string path) + { + return new Track1SdkProject(fs, path) + { + Type = ProjectType.Track1Sdk, + Name = fs.Path.GetFileName(path) + }; + } + } +} diff --git a/tools/AzDev/src/Models/Inventory/ProjectType.cs b/tools/AzDev/src/Models/Inventory/ProjectType.cs new file mode 100644 index 000000000000..88e87418fa7f --- /dev/null +++ b/tools/AzDev/src/Models/Inventory/ProjectType.cs @@ -0,0 +1,27 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace AzDev.Models.Inventory +{ + public enum ProjectType + { + Other, + AutoRestBased, + Test, + LegacyHelper, + Track1Sdk, + Wrapper, + SdkBased + } +} diff --git a/tools/AzDev/src/Models/Inventory/SdkBasedProject.cs b/tools/AzDev/src/Models/Inventory/SdkBasedProject.cs new file mode 100644 index 000000000000..93080c7c378c --- /dev/null +++ b/tools/AzDev/src/Models/Inventory/SdkBasedProject.cs @@ -0,0 +1,32 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.IO.Abstractions; + +namespace AzDev.Models.Inventory +{ + internal class SdkBasedProject : Project + { + protected SdkBasedProject(IFileSystem fs, string path) : base(fs, path) { } + internal SdkBasedProject() { } + public new static SdkBasedProject FromFileSystem(IFileSystem fs, string path) + { + return new SdkBasedProject(fs, path) + { + Type = ProjectType.SdkBased, + Name = fs.Path.GetFileName(path) + }; + } + } +} diff --git a/tools/AzDev/src/Models/Inventory/SwaggerReference.cs b/tools/AzDev/src/Models/Inventory/SwaggerReference.cs new file mode 100644 index 000000000000..54e5dde4c4be --- /dev/null +++ b/tools/AzDev/src/Models/Inventory/SwaggerReference.cs @@ -0,0 +1,40 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace AzDev.Models.Inventory +{ + internal class SwaggerReference + { + public string Uri { get; } + public string RawUri { get; } + public string Commit { get; } + + public SwaggerReference(string uri, string commit) + { + RawUri = uri; + Commit = commit; + Uri = ParseUri(uri); + } + + private string ParseUri(string uri) + { + return uri.Replace("$(repo)", $"https://github.com/Azure/azure-rest-api-specs/blob/{Commit}"); + } + + public override string ToString() + { + return RawUri; + } + } +} diff --git a/tools/AzDev/src/Models/Inventory/WrapperProject.cs b/tools/AzDev/src/Models/Inventory/WrapperProject.cs new file mode 100644 index 000000000000..1d174fe70c80 --- /dev/null +++ b/tools/AzDev/src/Models/Inventory/WrapperProject.cs @@ -0,0 +1,32 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.IO.Abstractions; + +namespace AzDev.Models.Inventory +{ + internal class WrapperProject : Project + { + protected WrapperProject(IFileSystem fs, string path) : base(fs, path) { } + internal WrapperProject() { } + public new static WrapperProject FromFileSystem(IFileSystem fs, string path) + { + return new WrapperProject(fs, path) + { + Type = ProjectType.Wrapper, + Name = fs.Path.GetFileName(path) + }; + } + } +} diff --git a/tools/AzDev/src/Models/PSModels/PSDevContext.cs b/tools/AzDev/src/Models/PSModels/PSDevContext.cs new file mode 100644 index 000000000000..c074c38cef0c --- /dev/null +++ b/tools/AzDev/src/Models/PSModels/PSDevContext.cs @@ -0,0 +1,30 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace AzDev.Models.PSModels +{ + public class PSDevContext + { + public string AzurePowerShellRepositoryRoot { get; set; } + public string AzurePowerShellCommonRepositoryRoot { get; set; } + public string ContextPath { get; set; } + + public PSDevContext(DevContext context, string path) + { + AzurePowerShellRepositoryRoot = context.AzurePowerShellRepositoryRoot; + AzurePowerShellCommonRepositoryRoot = context.AzurePowerShellCommonRepositoryRoot; + ContextPath = path; + } + } +} diff --git a/tools/AzDev/src/Models/PSModels/PSModule.cs b/tools/AzDev/src/Models/PSModels/PSModule.cs new file mode 100644 index 000000000000..2e977dc18864 --- /dev/null +++ b/tools/AzDev/src/Models/PSModels/PSModule.cs @@ -0,0 +1,36 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Linq; +using AzDev.Models.Inventory; + +namespace AzDev.Models.PSModels +{ + public class PSModule + { + public string Name { get; set; } + public string Path { get; set; } + public PSProject[] Project { get; set; } + public ModuleType Type { get; set; } + internal PSModule(Module module) + { + Name = module.Name; + Path = module.Path; + Type = module.Type; + Project = module.Projects.Select(p => new PSProject(p)).ToArray(); + } + internal PSModule() { } + public override string ToString() => Name; + } +} diff --git a/tools/AzDev/src/Models/PSModels/PSProject.cs b/tools/AzDev/src/Models/PSModels/PSProject.cs new file mode 100644 index 000000000000..911eafad2751 --- /dev/null +++ b/tools/AzDev/src/Models/PSModels/PSProject.cs @@ -0,0 +1,35 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using AzDev.Models.Inventory; + +namespace AzDev.Models.PSModels +{ + public class PSProject + { + public string Name { get; set; } + public string Path { get; set; } + public ProjectType Type { get; set; } + public string TypeDeductionReason { get; set; } + internal PSProject(Project p) + { + Name = p.Name; + Path = p.Path; + Type = p.Type; + TypeDeductionReason = p.TypeDeductionReason; + } + internal PSProject() { } + public override string ToString() => Name; + } +} diff --git a/tools/AzDev/src/Services/AzDevModule.cs b/tools/AzDev/src/Services/AzDevModule.cs new file mode 100644 index 000000000000..a44398686193 --- /dev/null +++ b/tools/AzDev/src/Services/AzDevModule.cs @@ -0,0 +1,49 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Tests")] + +namespace AzDev.Services +{ + internal static class AzDevModule + { + private static readonly IDictionary Components; + + static AzDevModule() + { + Components = new Dictionary(); + } + + public static T GetComponent(string key) + { + if (Components[key] is T t) + { + return t; + } + else + { + throw new ArgumentException($"Mismatching type. Expect [{typeof(T)}]. Got [{Components[key].GetType()}]."); + } + } + + public static void SetComponent(string key, T value) + { + Components[key] = value; + } + } +} diff --git a/tools/AzDev/src/Services/Conventions.cs b/tools/AzDev/src/Services/Conventions.cs new file mode 100644 index 000000000000..7632bc3b16fc --- /dev/null +++ b/tools/AzDev/src/Services/Conventions.cs @@ -0,0 +1,222 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Linq; +using AzDev.Models.Inventory; + +namespace AzDev.Services +{ + internal static class Conventions + { + public static bool IsAutorestBasedProject(string path, out string reason) + { + if (path.EndsWith(".autorest", StringComparison.InvariantCultureIgnoreCase)) + { + reason = $"[{path}] ends with '.autorest'."; + return true; + } + else + { + reason = $"[{path}] does not end with '.autorest'."; + return false; + } + } + + public static bool IsExcludedModuleDirectory(string dir) + { + var slash = System.IO.Path.DirectorySeparatorChar; + return dir.EndsWith($"{slash}shared") || dir.EndsWith($"{slash}lib"); + } + + internal static bool IsLegacyHelperProject(string path, out string reason) + { + if (path.EndsWith(".helper", StringComparison.InvariantCultureIgnoreCase)) + { + reason = $"[{path}] ends with '.helper'."; + return true; + } + else if (path.EndsWith(".helpers", StringComparison.InvariantCultureIgnoreCase)) + { + reason = $"[{path}] ends with '.helpers'."; + return true; + } + else + { + reason = $"[{path}] does not end with '.helper' or '.helpers'."; + return false; + } + } + + internal static bool IsTestProject(string path, out string reason) + { + if (path.EndsWith(".test", StringComparison.InvariantCultureIgnoreCase)) + { + reason = $"[{path}] ends with '.test'."; + return true; + } + else + { + reason = $"[{path}] does not end with '.test'."; + return false; + } + } + + internal static bool IsTrack1SdkProject(string path, out string reason) + { + if (path.EndsWith(".management.sdk", StringComparison.InvariantCultureIgnoreCase)) + { + reason = $"[{path}] ends with '.management.sdk'."; + return true; + } + if (path.EndsWith(".sdk", StringComparison.InvariantCultureIgnoreCase)) // legacy naming + { + reason = $"[{path}] ends with '.sdk' but not '.management.sdk. This is not allowed after Az 13."; + return true; + } + else + { + reason = $"[{path}] does not end with '.management.sdk'."; + return false; + } + } + + internal static bool IsSwaggerReferenceLocal(string path) + { + return path.TrimStart().StartsWith("..", StringComparison.InvariantCultureIgnoreCase); + } + + internal static bool IsExcludedProjectDirectory(IFileSystem fs, string path, out string reason) + { + if (path.StartsWith(".") || fs.Path.GetFileName(path).StartsWith(".")) + { + reason = "Path starts with '.' or contains a dir/file starting with '.'."; + return true; + } + + if (!TryGetOnlyCsprojPath(fs, path, out var _, out var cannotFindCsproj)) + { + reason = $"Path does not contain a single .csproj file: {cannotFindCsproj}"; + return true; + } + reason = null; + return false; + } + + internal static bool IsAutorestInputButNotSwagger(string uri) + { + return uri.EndsWith("readme.azure.noprofile.md", StringComparison.InvariantCultureIgnoreCase); + } + + internal static bool IsWrapperProject(IFileSystem fs, string path, out string typeDeductionReason) + { + if (!TryGetOnlyCsprojPath(fs, path, out var csproj, out var reason)) + { + typeDeductionReason = $"Failed to get csproj path: {reason}"; + return false; + } + + var project = CsprojReader.Parse(fs.File.ReadAllText(csproj)); + if (project.PackageReferences.Any() || project.ProjectReferences.Any()) + { + typeDeductionReason = $"[{csproj} has package references or project reference."; + return false; + } + else if (fs.Directory.GetParent(path).Name.Equals(fs.Path.GetFileName(path), StringComparison.InvariantCultureIgnoreCase)) + { + typeDeductionReason = $"[{csproj}] does not contain package references or project reference and is in a dir named the same as its parent."; + return true; + } + else + { + typeDeductionReason = $"[{csproj}] does not contain package references or project reference but dir name is different from parent dir."; + return false; + } + } + + private static bool TryGetOnlyCsprojPath(IFileSystem fs, string path, out string csproj, out string failureReason) + { + var csprojs = fs.Directory.GetFiles(path, "*.csproj", System.IO.SearchOption.TopDirectoryOnly); + if (csprojs.Length != 1) + { + csproj = null; + failureReason = $"[{path}] contains {csprojs.Length} .csproj files. Expected: 1."; + return false; + } + else + { + csproj = csprojs[0]; + failureReason = null; + return true; + } + } + + internal static bool IsSdkBasedProject(IFileSystem fs, string path, out string typeDeductionReason) + { + if (!TryGetOnlyCsprojPath(fs, path, out var csproj, out var reason)) + { + typeDeductionReason = $"Failed to get csproj path: {reason}"; + return false; + } + + var project = CsprojReader.Parse(fs.File.ReadAllText(csproj)); + if (project.PackageReferences.Any(p => + p.StartsWith("Microsoft.Azure.Management.") // track 1 management plane + || p.StartsWith("Microsoft.Azure.") // track 1 data plane + || p.StartsWith("Azure.")) // track 2 data plane + || project.ProjectReferences.Any(p => + p.EndsWith(".Management.Sdk.csproj") // local sdk + || p.EndsWith(".Helpers.csproj"))) // local sdk helpers + { + typeDeductionReason = $"[{csproj} contains package references to track 1 sdk package or project reference to track 1 sdk project."; + return true; + } + else + { + typeDeductionReason = $"[{csproj}] does not contain package references nor project reference to track 1 sdk."; + return false; + } + } + + internal static ModuleType DeductModuleType(IEnumerable projects, out string reason) + { + var projectTypes = projects.Select(p => p.Type); + + if (projectTypes.Contains(ProjectType.LegacyHelper) && !projectTypes.Contains(ProjectType.SdkBased)) + { + projectTypes = projectTypes.Concat(new[] { ProjectType.SdkBased }); + } + + if (projectTypes.Contains(ProjectType.AutoRestBased) && projectTypes.Contains(ProjectType.SdkBased)) + { + reason = "Projects in module consist of AutoRestBased and SdkBased."; + return ModuleType.Hybrid; + } + if (projectTypes.Contains(ProjectType.AutoRestBased)) + { + reason = "Projects in module consist of AutoRestBased but not SdkBased."; + return ModuleType.AutoRestBased; + } + if (projectTypes.Contains(ProjectType.SdkBased)) + { + reason = "Projects in module consist of SdkBased but not AutoRestBased."; + return ModuleType.SdkBased; + } + reason = "Projects in module do not consist of AutoRestBased nor SdkBased."; + return ModuleType.Other; + } + } +} diff --git a/tools/AzDev/src/Services/CsprojReader.cs b/tools/AzDev/src/Services/CsprojReader.cs new file mode 100644 index 000000000000..26cb2296e100 --- /dev/null +++ b/tools/AzDev/src/Services/CsprojReader.cs @@ -0,0 +1,67 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AzDev.Services +{ + internal static class CsprojReader + { + private static IDictionary _cache = new Dictionary(); + + public static Csproj Parse(string content) + { + if (_cache.TryGetValue(content, out var result)) + { + return result; + } + + var lines = content.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + var packageReferences = new List(); + var projectReferences = new List(); + + foreach (var line in lines) + { + if (line.Contains(" PackageReferences { get; internal set; } = Enumerable.Empty(); + public IEnumerable ProjectReferences { get; internal set; } = Enumerable.Empty(); + } +} \ No newline at end of file diff --git a/tools/AzDev/src/Services/DefaultCodebaseProvider.cs b/tools/AzDev/src/Services/DefaultCodebaseProvider.cs new file mode 100644 index 000000000000..6d1080ec322e --- /dev/null +++ b/tools/AzDev/src/Services/DefaultCodebaseProvider.cs @@ -0,0 +1,46 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.IO.Abstractions; +using AzDev.Models.Inventory; + +namespace AzDev.Services +{ + internal class DefaultCodebaseProvider : ICodebaseProvider + { + private IContextProvider _contextProvider; + private IFileSystem _fs; + private Codebase _codebase; + + public DefaultCodebaseProvider(IContextProvider contextProvider) + : this(contextProvider, new FileSystem()) { } + + public DefaultCodebaseProvider(IContextProvider contextProvider, IFileSystem fs) + { + _contextProvider = contextProvider; + _fs = fs; + } + + public Codebase GetCodebase() + { + if (_codebase == null) + { + var path = _contextProvider.LoadContext().AzurePowerShellRepositoryRoot; + var src = _fs.Path.Combine(path, FileOrDirNames.Src); + _codebase = _codebase ?? Codebase.FromFileSystem(_fs, src); + } + return _codebase; + } + } +} diff --git a/tools/AzDev/src/Services/DefaultContextProvider.cs b/tools/AzDev/src/Services/DefaultContextProvider.cs new file mode 100644 index 000000000000..4700bcec4e5c --- /dev/null +++ b/tools/AzDev/src/Services/DefaultContextProvider.cs @@ -0,0 +1,70 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.IO.Abstractions; +using System.Text.Json; +using AzDev.Models; + +namespace AzDev.Services +{ + internal class DefaultContextProvider : IContextProvider + { + private readonly string _contextFilePath; + private IFileSystem _fileSystem; + private DevContext _cachedContext; + + public DefaultContextProvider(string contextFilePath) : this(contextFilePath, new FileSystem()) + { + } + + public DefaultContextProvider(string contextFilePath, IFileSystem fileSystem) + { + _contextFilePath = contextFilePath; + _fileSystem = fileSystem; + _cachedContext = null; + } + + public string ContextPath => _contextFilePath; + + public DevContext LoadContext() + { + if (_cachedContext != null) + { + return _cachedContext; + } + + if (!_fileSystem.File.Exists(_contextFilePath)) + { + // Handle the case when the context file doesn't exist + throw new System.IO.FileNotFoundException("Context file not found."); + } + + string json = _fileSystem.File.ReadAllText(_contextFilePath); + _cachedContext = JsonSerializer.Deserialize(json); + return _cachedContext; + } + + public void SaveContext(DevContext context) + { + string json = JsonSerializer.Serialize(context); + string directoryPath = _fileSystem.Path.GetDirectoryName(_contextFilePath); + if (!string.IsNullOrEmpty(directoryPath)) + { + _fileSystem.Directory.CreateDirectory(directoryPath); + } + _fileSystem.File.WriteAllText(_contextFilePath, json); + _cachedContext = context; + } + } +} diff --git a/tools/AzDev/src/Services/FileOrDirNames.cs b/tools/AzDev/src/Services/FileOrDirNames.cs new file mode 100644 index 000000000000..9215ccd4b1eb --- /dev/null +++ b/tools/AzDev/src/Services/FileOrDirNames.cs @@ -0,0 +1,22 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace AzDev.Services +{ + internal static class FileOrDirNames + { + public const string Generated = "generated"; + public const string Src = "src"; + } +} diff --git a/tools/AzDev/src/Services/FilterHelpers.cs b/tools/AzDev/src/Services/FilterHelpers.cs new file mode 100644 index 000000000000..c38e064ee944 --- /dev/null +++ b/tools/AzDev/src/Services/FilterHelpers.cs @@ -0,0 +1,43 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using AzDev.Models.Inventory; + +namespace AzDev.Services +{ + internal static class FilterHelpers + { + /// + /// Filter projects by name or module name. + /// + /// + /// Can be part of a module or project name. + /// A collection of projects whose names match the filter, or whose parent modules' names match. + /// + public static IEnumerable FilterProjects(this Codebase codebase, string filter) + { + if (string.IsNullOrEmpty(filter)) + { + throw new ArgumentException("Filter cannot be null or empty."); + } + var lowerFilter = filter.ToLower(); + var matchByModule = codebase.Modules.Where(m => m.Name.ToLower().Contains(lowerFilter)).SelectMany(m => m.Projects); + var matchByProject = codebase.Modules.SelectMany(m => m.Projects).Where(p => p.Name.ToLower().Contains(lowerFilter)); + return matchByModule.Concat(matchByProject).Distinct(); + } + } +} diff --git a/tools/AzDev/src/Services/ICodebaseProvider.cs b/tools/AzDev/src/Services/ICodebaseProvider.cs new file mode 100644 index 000000000000..ccb51437b511 --- /dev/null +++ b/tools/AzDev/src/Services/ICodebaseProvider.cs @@ -0,0 +1,23 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using AzDev.Models.Inventory; + +namespace AzDev.Services +{ + internal interface ICodebaseProvider + { + Codebase GetCodebase(); + } +} diff --git a/tools/AzDev/src/Services/IContextProvider.cs b/tools/AzDev/src/Services/IContextProvider.cs new file mode 100644 index 000000000000..07d8f556ea9b --- /dev/null +++ b/tools/AzDev/src/Services/IContextProvider.cs @@ -0,0 +1,25 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using AzDev.Models; + +namespace AzDev.Services +{ + internal interface IContextProvider + { + string ContextPath { get; } + DevContext LoadContext(); + void SaveContext(DevContext context); + } +} diff --git a/tools/AzDev/src/Services/SwaggerHelpers.cs b/tools/AzDev/src/Services/SwaggerHelpers.cs new file mode 100644 index 000000000000..9e32954d4ba0 --- /dev/null +++ b/tools/AzDev/src/Services/SwaggerHelpers.cs @@ -0,0 +1,27 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Diagnostics; +using AzDev.Models.Inventory; + +namespace AzDev.Services +{ + internal static class SwaggerHelpers + { + public static void OpenOnline(this SwaggerReference swagger) + { + Process.Start(new ProcessStartInfo(swagger.Uri) { UseShellExecute = true }); + } + } +} diff --git a/tools/AzDev/src/Services/YamlHelper.cs b/tools/AzDev/src/Services/YamlHelper.cs new file mode 100644 index 000000000000..feefac5e7a15 --- /dev/null +++ b/tools/AzDev/src/Services/YamlHelper.cs @@ -0,0 +1,39 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using YamlDotNet.Serialization; + +internal static class YamlHelper +{ + private static IDeserializer Deserializer => _lazyDeserializer.Value; + private static Lazy _lazyDeserializer = new Lazy(BuildDeserializer); + + private static IDeserializer BuildDeserializer() + { + return new DeserializerBuilder() + .IgnoreUnmatchedProperties() + .Build(); + } + + public static T Deserialize(string yaml) + { + return Deserializer.Deserialize(yaml); + } + + public static string Serialize(T obj) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/tools/DevTools/CommonRepo.psm1 b/tools/DevTools/CommonRepo.psm1 deleted file mode 100644 index 4bde9a8b573b..000000000000 --- a/tools/DevTools/CommonRepo.psm1 +++ /dev/null @@ -1,102 +0,0 @@ -<# - .Synopsis - Connects azure-powershell repo to azure-powershell-common repo for debugging. - - .Description - Connects azure-powershell repo to azure-powershell-common repo for debugging. - - .Parameter CommonRepoPath - Path to the common repo. Relative or absolute. - - .Example - Connect-CommonRepo -CommonRepoPath ../azure-powershell-common -#> -function Connect-CommonRepo { - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [system.string] - ${CommonRepoPath} - ) - - Write-Host "1/2 Adding common projects to sln and csproj" - - $CommonRepoPath = (Resolve-Path $CommonRepoPath).Path - $CommonProjects = Get-ChildItem -Path "$CommonRepoPath/src/" -Include *.csproj -Exclude *.test.* -Recurse - $CommonProjects = $CommonProjects.FullName - - - $RepoRoot = "$PSScriptRoot/../.." - - Push-Location "$RepoRoot/src/Accounts" - try { - foreach ($csproj in $CommonProjects) { - $csproj = [System.IO.Path]::GetFullPath($csproj) - dotnet sln add $csproj - if ($LASTEXITCODE -ne 0) { - throw "Failed to add $csproj to Accounts.sln" - } -<# - known common project references: - Authentication.csproj -> Authentication.Abstractions, ResourceManager - Accounts.csproj -> Authentication.Abstractions, ResourceManager, Common - Accounts.Test.csproj -> Authentication.Abstractions, ResourceManager, Common - TestFx.csproj -> Graph.Rbac.csproj - AssemblyLoading.csproj -> Common -#> - # add all common projects to Authentication.csproj because it will be referenced by most Az projects - dotnet add ./Authentication/Authentication.csproj reference $csproj - if ($LASTEXITCODE -ne 0) { - throw "Failed to add $csproj to Authentication.csproj" - } - } - - # AssemblyLoading.csproj references Common.csproj and does not reference Autehtication.csproj - dotnet add ./AssemblyLoading/AssemblyLoading.csproj reference "$CommonRepoPath/src/Common/Common.csproj" - if ($LASTEXITCODE -ne 0) { - throw "Failed to add Common.csproj to AssemblyLoading.csproj" - } - - # add common project references below for csproj which does not reference Authentication.csproj - } - finally { - Pop-Location - } - - - Write-Host "2/2: Remove the dependency of those common projects from .targets file" - - $Patterns = @( - '" - } - else { - return $line - } - } | Set-Content $TargetsFile - - Write-Host "Done connecting both repositories." -} - -function Disconnect-CommonRepo { - Write-Host "Please run the following commands to undo Connect-CommonRepo. Double check those files do not have wanted changes. - git checkout -- ./src/Accounts/Accounts.sln - git checkout -- ./src/Accounts/AssemblyLoading/AssemblyLoading.csproj - git checkout -- ./src/Accounts/Authentication/Authentication.csproj - git checkout -- ./tools/Common.Netcore.Dependencies.targets - " -} - -Export-ModuleMember -Function Connect-CommonRepo, Disconnect-CommonRepo diff --git a/tools/DevTools/README.md b/tools/DevTools/README.md deleted file mode 100644 index f4026294f36e..000000000000 --- a/tools/DevTools/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# What's inside DevTools - -## CommonRepo.psm1 - -A script module to help you connect the azure-powershell and azure-powershell-common repositories for developing or debugging. - -```powershell -# Connect -Import-Module .\CommonRepo.psm1 -Connect-CommonRepo -CommonRepoPath ..\azure-powershell-common - -# Disconnect -Disconnect-CommonRepo -```