diff --git a/.ci-config.json b/.ci-config.json new file mode 100644 index 000000000000..c91aedb1bbb9 --- /dev/null +++ b/.ci-config.json @@ -0,0 +1,271 @@ +{ + "rules": [ + { + "patterns": [ + ".azure-pipeline/*", + "NugGet.Config", + "Repo.props" + ], + "phases": [ + "build:all", + "breaking-change:all", + "dependence:all", + "help:all", + "signature:all", + "test:all", + "sub-task:all" + ] + }, + { + "patterns": [ + "src/*.props" + ], + "phases": [ + "build:all", + "dependence:all", + "test:all" + ] + }, + { + "patterns": [ + "src/lib/*" + ], + "phases": [ + "build:all", + "dependence:all" + ] + }, + { + "patterns": [ + "docker/*", + "documentation/*", + ".github/*", + "setup/*", + ".dockerignore", + ".git*", + "appveyor.yml", + "CONTRIBUTION.md", + "LICENSE.txt", + "README.md", + "**/ChangeLog.md", + "**/readme.md", + "src/**/document/*" + ], + "phases": [] + }, + { + "patterns": [ + "src/{ModuleName}/test/*", + "src/{ModuleName}/*.Test/*" + ], + "phases": [ + "build:dependent-module", + "test:module" + ] + }, + { + "patterns": [ + "src/{ModuleName}/**/*.md" + ], + "phases": [ + "build:module", + "help:module" + ] + }, + { + "patterns": [ + "src/{ModuleName}/**/*.csproj" + ], + "phases": [ + "build:related-module", + "dependence:dependence-module", + "test:dependence-module" + ] + }, + { + "patterns": [ + "src/{ModuleName}/*" + ], + "phases": [ + "build:related-module", + "breaking-change:module", + "help:module", + "signature:module", + "test:dependence-module" + ] + }, + { + "patterns": [ + "tools/StaticAnalysis/Exceptions/{ModuleName}/MissingAssemblies.csv", + "tools/StaticAnalysis/Exceptions/{ModuleName}/AssemblyVersionConflict.csv", + "tools/StaticAnalysis/Exceptions/{ModuleName}/ExtraAssemblies.csv", + "tools/StaticAnalysis/Exceptions/{ModuleName}/SharedAssemblyConflict.csv" + ], + "phases": [ + "build:module", + "dependence:module" + ] + }, + { + "patterns": [ + "tools/StaticAnalysis/Exceptions/{ModuleName}/BreakingChangeIssues.csv" + ], + "phases": [ + "build:module", + "breaking-change:module" + ] + }, + { + "patterns": [ + "tools/StaticAnalysis/Exceptions/{ModuleName}/HelpIssues.csv" + ], + "phases": [ + "build:module", + "help:module" + ] + }, + { + "patterns": [ + "tools/StaticAnalysis/Exceptions/{ModuleName}/SignatureIssues.csv" + ], + "phases": [ + "build:module", + "signature:module" + ] + }, + { + "patterns": [ + "tools/StaticAnalysis/*", + "tools/Tools.Common/*" + ], + "phases": [ + "build:all", + "breaking-change:all", + "dependence:all", + "help:all", + "signature:all" + ] + }, + { + "patterns": [ + "tools/Az.Tools.Predictor/*" + ], + "phases": [ + "sub-task:Predictor" + ] + }, + { + "patterns": [ + "tools/Az.Tools.Installer/*" + ], + "phases": [ + "sub-task:Installer" + ] + }, + { + "patterns": [ + "tools/AddModulePsm1Dependency.ps1", + "tools/Common.Netcore.Dependencies.targets", + "tools/AzureRM.Example.psm1" + ], + "phases": [ + "build:all", + "breaking-change:all", + "dependence:all", + "help:all", + "signature:all", + "test:all" + ] + }, + { + "patterns": [ + "tools/GenerateHelp.ps1", + "tools/HelpGeneration/*" + ], + "phases": [ + "build:all", + "help:all" + ] + }, + { + "patterns": [ + "tools/CheckAssemblies.ps1" + ], + "phases": [ + "build:all", + "dependence:all" + ] + }, + { + "patterns": [ + "tools/CheckSignature.ps1" + ], + "phases": [ + "build:all", + "signature:all" + ] + }, + { + "patterns": [ + "tools/Common.Netcore.Dependencies.Test.targets" + ], + "phases": [ + "build:all", + "test:all" + ] + }, + { + "patterns": [ + "tools/ARMIncrementVersion.ps1", + "tools/ARMSyncVersion.ps1", + "tools/ASMIncrementVersion.ps1", + "tools/AzureRM.Example.psm1", + "tools/BuildInstaller.ps1", + "tools/CheckChangeLog.ps1", + "tools/CheckIgnoredFile.ps1", + "tools/CleanupBuild.ps1", + "tools/CommonIncrementVersion.ps1", + "tools/CreateAliasMapping.ps1", + "tools/CreateFilterMappings.ps1", + "tools/CreateMappings_rules.json", + "tools/CreateMappings.ps1", + "tools/CreateRegistryEntry.ps1" + ], + "phases": [] + }, + { + "patterns": [ + "tools/Az/*", + "tools/BatchModelGenerator/*", + "tools/BreakingChanges/*", + "tools/Docker/*", + "tools/FormatPs1XmlGenerator/*", + "tools/Gen2Master/*", + "tools/InstallationTests/*", + "tools/Installer/*", + "tools/NetCoreCsProjSync/*", + "tools/NetCorePsd1Sync/*", + "tools/ProjectTemplates/*", + "tools/RepoTasks/*", + "tools/SecurityTools/*", + "tools/Test/*", + "tools/Tools.Common.Test/*", + "tools/VersionController/*" + ], + "phases": [] + }, + { + "patterns": [ + "others" + ], + "phases": [ + "build:all", + "breaking-change:all", + "dependence:all", + "help:all", + "signature:all", + "test:all" + ] + } + ] +} \ No newline at end of file diff --git a/build.proj b/build.proj index 588c191a9f8a..dadb3b4aaeed 100644 --- a/build.proj +++ b/build.proj @@ -89,7 +89,7 @@ - + @@ -119,23 +119,33 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -150,6 +160,13 @@ + + + + + + + @@ -159,20 +176,16 @@ - - - + + - - - - - + + - + build publish @@ -187,10 +200,10 @@ Microsoft.Powershell.*.dll,System*.dll,Microsoft.VisualBasic.dll,Microsoft.CSharp.dll,Microsoft.CodeAnalysis.dll,Microsoft.CodeAnalysis.CSharp.dll System.Security.Cryptography.ProtectedData.dll,System.Configuration.ConfigurationManager.dll,System.Runtime.CompilerServices.Unsafe.dll,System.IO.FileSystem.AccessControl.dll,System.Buffers.dll,System.Text.Encodings.Web.dll,System.CodeDom.dll,System.Management.dll,System.Text.Json.dll,System.Threading.Tasks.Extensions.dll - + - + @@ -209,18 +222,48 @@ - + - - + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + @@ -233,6 +276,14 @@ + + + + + + + + diff --git a/tools/BuildPackagesTask/Microsoft.Azure.Build.Tasks/CIFilterTask.cs b/tools/BuildPackagesTask/Microsoft.Azure.Build.Tasks/CIFilterTask.cs new file mode 100644 index 000000000000..dc2077215c01 --- /dev/null +++ b/tools/BuildPackagesTask/Microsoft.Azure.Build.Tasks/CIFilterTask.cs @@ -0,0 +1,428 @@ +// +// Copyright (c) Microsoft. 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. +// +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Microsoft.WindowsAzure.Build.Tasks +{ + /// + /// A simple Microsoft Build task used to generate a list of test assemblies to be + /// used for testing Azure PowerShell. + /// + public class CIFilterTask : Task + { + /// + /// Gets or sets the files changed in a given pull request. + /// + [Required] + public string[] FilesChanged { get; set; } + + /// + /// Gets or set the TargetModule, e.g. Storage + /// + public string TargetModule { get; set; } + + /// + /// Gets or set the Mode, e.g. Release + /// + [Required] + public string Mode { get; set; } + + /// + /// Gets or sets the path to the files-to-csproj map. + /// + [Required] + public string CsprojMapFilePath { get; set; } + + /// + /// Gets or sets the test assemblies output produced by the task. + /// + [Output] + public CIFilterTaskResult FilterTaskResult { get; set; } + + private const string TaskMappingConfigName = ".ci-config.json"; + + private const string AllModule = "all"; + private const string SingleModule = "module"; + private const string DependenceModule = "dependence-module"; // self and modules dependent on this module + private const string DependentModule = "dependent-module"; // self and modules that self dependent on + private const string RelatedModule = "related-module"; // self, modules that self dependent on and modules dependent on this module + + private const string BUILD_PHASE = "build"; + private const string ANALYSIS_BREAKING_CHANGE_PHASE = "breaking-change"; + private const string ANALYSIS_HELP_PHASE = "help"; + private const string ANALYSIS_DEPENDENCY_PHASE = "dependency"; + private const string ANALYSIS_SIGNATURE_PHASE = "signature"; + private const string TEST_PHASE = "test"; + private const string ACCOUNT_MODULE_NAME = "Accounts"; + + private const string MODULE_NAME_PLACEHOLDER = "ModuleName"; + + private Dictionary ReadMapFile(string mapFilePath, string mapFileName) + { + if (mapFilePath == null) + { + throw new ArgumentNullException(string.Format("The {0} cannot be null.", mapFileName)); + } + + if (!File.Exists(mapFilePath)) + { + throw new FileNotFoundException(string.Format("The {0} provided could not be found. Please provide a valid MapFilePath.", mapFileName)); + } + + return JsonConvert.DeserializeObject>(File.ReadAllText(mapFilePath)); + } + + private List GetRelatedCsprojList(string moduleName, Dictionary csprojMap) + { + List csprojList = new List(); + + if (csprojMap.ContainsKey(moduleName)) + { + csprojList.AddRange(csprojMap[moduleName]); + } + else + { + string expectKey = string.Format("src/{0}/", moduleName); + foreach (string key in csprojMap.Keys) + { + if (key.ToLower().Equals(expectKey.ToLower())) + { + csprojList.AddRange(csprojMap[key]); + } + } + } + + return csprojList; + } + + private List GetBuildCsprojList(string moduleName, Dictionary csprojMap) + { + if (moduleName.Equals(AllModule)) + { + moduleName = ACCOUNT_MODULE_NAME; + } + return GetRelatedCsprojList(moduleName, csprojMap) + .Where(x => !x.Contains("Test")).ToList(); + } + + private string GetModuleNameFromCsprojPath(string csprojPath) + { + return csprojPath.Replace('/', '\\') + .Split(new string[] { "src\\" }, StringSplitOptions.None)[1] + .Split('\\')[0]; + } + + private List GetDependenceModuleList(string moduleName, Dictionary csprojMap) + { + List moduleList = new List(); + + foreach (string key in csprojMap.Keys) + { + bool isDependent = false; + foreach (string csproj in csprojMap[key]) + { + if (csproj.Replace("/", "\\").Contains("\\" + moduleName + "\\")) + { + isDependent = true; + } + } + if (isDependent) + { + moduleList.Add(key.Split('/')[1]); + } + } + + return moduleList; + } + + private List GetDependentModuleList(string moduleName, Dictionary csprojMap) + { + if (moduleName.Equals(AllModule)) + { + moduleName = ACCOUNT_MODULE_NAME; + } + return GetRelatedCsprojList(moduleName, csprojMap) + .Select(GetModuleNameFromCsprojPath) + .Distinct() + .ToList(); + } + + private List GetTestCsprojList(string moduleName, Dictionary csprojMap) + { + if (moduleName.Equals(AllModule)) + { + moduleName = ACCOUNT_MODULE_NAME; + } + return GetRelatedCsprojList(moduleName, csprojMap) + .Where(x => x.Contains("Test")).ToList();; + } + + private bool ProcessTargetModule(Dictionary csprojMap) + { + Dictionary> influencedModuleInfo = new Dictionary> + { + [BUILD_PHASE] = new HashSet(GetBuildCsprojList(TargetModule, csprojMap).ToList()), + [ANALYSIS_BREAKING_CHANGE_PHASE] = new HashSet(GetDependenceModuleList(TargetModule, csprojMap).ToList()), + [ANALYSIS_DEPENDENCY_PHASE] = new HashSet(GetDependenceModuleList(TargetModule, csprojMap).ToList()), + [ANALYSIS_HELP_PHASE] = new HashSet(GetDependenceModuleList(TargetModule, csprojMap).ToList()), + [ANALYSIS_SIGNATURE_PHASE] = new HashSet(GetDependenceModuleList(TargetModule, csprojMap).ToList()), + [TEST_PHASE] = new HashSet(GetTestCsprojList(TargetModule, csprojMap).ToList()) + }; + + Console.WriteLine("----------------- InfluencedModuleInfo TargetModule -----------------"); + foreach (string phaseName in influencedModuleInfo.Keys) + { + Console.WriteLine(string.Format("{0}: [{1}]", phaseName, string.Join(", ", influencedModuleInfo[phaseName].ToList()))); + } + Console.WriteLine("--------------------------------------------------------"); + + FilterTaskResult.PhaseInfo = influencedModuleInfo; + + return true; + } + + private string ProcessSinglePattern(string pattern) + { + return pattern.Replace("**", ".*").Replace("{ModuleName}", "(?[^/]+)"); + } + + private Dictionary> CalculateInfluencedModuleInfoForEachPhase(List<(Regex, List)> ruleList, Dictionary csprojMap) + { + Dictionary> influencedModuleInfo = new Dictionary>(); + + foreach (string filePath in FilesChanged) + { + List phaseList = new List(); + bool isMatched = false; + string machedModuleName = ""; + foreach ((Regex regex, List phaseConfigList) in ruleList) + { + var regexResult = regex.Match(filePath); + if (regexResult.Success) + { + phaseList = phaseConfigList; + isMatched = true; + if (regexResult.Groups[MODULE_NAME_PLACEHOLDER].Success) + { + machedModuleName = regexResult.Groups[MODULE_NAME_PLACEHOLDER].Value; + } + Console.WriteLine(string.Format("File {0} match rule: {1} and phaseConfig is: [{2}]", filePath, regex.ToString(), string.Join(", ", phaseConfigList))); + break; + } + } + if (!isMatched) + { + Console.WriteLine(string.Format("File {0} doesn't match any rule, goto fallback logic.", filePath)); + phaseList = new List() + { + BUILD_PHASE + ":" + AllModule, + ANALYSIS_BREAKING_CHANGE_PHASE + ":" + AllModule, + ANALYSIS_DEPENDENCY_PHASE + ":" + AllModule, + ANALYSIS_HELP_PHASE + ":" + AllModule, + ANALYSIS_SIGNATURE_PHASE + ":" + AllModule, + TEST_PHASE + ":" + AllModule, + }; + } + foreach (string phase in phaseList) + { + string phaseName = phase.Split(':')[0]; + string scope = phase.Split(':')[1]; + HashSet scopes = influencedModuleInfo.ContainsKey(phaseName) ? influencedModuleInfo[phaseName] : new HashSet(); + if (!scopes.Contains(AllModule)) + { + if (scope.Equals(AllModule)) + { + scopes.Clear(); + scopes.Add(AllModule); + } + else + { + string moduleName = machedModuleName == "" ? filePath.Split('/')[1] : machedModuleName; + if (scope.Equals(SingleModule)) + { + scopes.Add(moduleName); + } + else if (scope.Equals(DependenceModule)) + { + scopes.UnionWith(GetDependenceModuleList(moduleName, csprojMap)); + } + else if (scope.Equals(DependentModule)) + { + scopes.UnionWith(GetDependentModuleList(moduleName, csprojMap)); + } + else if (scope.Equals(RelatedModule)) + { + scopes.UnionWith(GetDependenceModuleList(moduleName, csprojMap)); + scopes.UnionWith(GetDependentModuleList(moduleName, csprojMap)); + } + else + { + scopes.Add(scope); + } + } + influencedModuleInfo[phaseName] = scopes; + } + } + } + List expectedKeyList = new List() + { + BUILD_PHASE, + ANALYSIS_BREAKING_CHANGE_PHASE, + ANALYSIS_DEPENDENCY_PHASE, + ANALYSIS_HELP_PHASE, + ANALYSIS_SIGNATURE_PHASE, + TEST_PHASE + }; + foreach (string phaseName in expectedKeyList) + { + if (!influencedModuleInfo.ContainsKey(phaseName)) + { + influencedModuleInfo[phaseName] = new HashSet(); + } + else if (influencedModuleInfo[phaseName].Contains(AllModule)) + { + influencedModuleInfo[phaseName] = new HashSet(GetDependenceModuleList(ACCOUNT_MODULE_NAME, csprojMap)); + } + } + + foreach (string moduleName in influencedModuleInfo[TEST_PHASE]) + { + if (!moduleName.Equals(ACCOUNT_MODULE_NAME)) + { + influencedModuleInfo[BUILD_PHASE].UnionWith(GetDependentModuleList(moduleName, csprojMap)); + } + } + if (influencedModuleInfo[BUILD_PHASE].Count == 0) + { + influencedModuleInfo[BUILD_PHASE].Add(ACCOUNT_MODULE_NAME); + } + Console.WriteLine("----------------- InfluencedModuleInfo -----------------"); + foreach (string phaseName in influencedModuleInfo.Keys) + { + Console.WriteLine(string.Format("{0}: [{1}]", phaseName, string.Join(", ", influencedModuleInfo[phaseName].ToList()))); + } + Console.WriteLine("--------------------------------------------------------"); + + return influencedModuleInfo; + } + + /* + * Calculate the csproj path for modules in Build and Test phase. + */ + private Dictionary> CalculateCsprojForBuildAndTest(Dictionary> influencedModuleInfo, Dictionary csprojMap) + { + var keys = influencedModuleInfo.Keys.ToList(); + foreach (string phaseName in keys) + { + if (phaseName.Equals(BUILD_PHASE)) + { + HashSet csprojSet = new HashSet(); + foreach (string moduleName in influencedModuleInfo[phaseName]) + { + csprojSet.UnionWith(GetBuildCsprojList(moduleName, csprojMap)); + } + if (csprojSet.Count != 0) + { + foreach (string filename in Directory.GetFiles(@"src/Accounts", "*.csproj", SearchOption.AllDirectories).Where(x => !x.Contains("Test"))) + { + csprojSet.Add(filename); + } + } + influencedModuleInfo[phaseName] = csprojSet; + } + else if (phaseName.Equals(TEST_PHASE)) + { + HashSet csprojSet = new HashSet(); + foreach (string moduleName in influencedModuleInfo[phaseName]) + { + csprojSet.UnionWith(GetTestCsprojList(moduleName, csprojMap)); + } + if (csprojSet.Count != 0) + { + csprojSet.Add("tools/TestFx/TestFx.csproj"); + } + influencedModuleInfo[phaseName] = csprojSet; + } + } + + foreach (string phaseName in influencedModuleInfo.Keys) + { + Console.WriteLine("-----------------------------------"); + Console.WriteLine(string.Format("{0}: [{1}]", phaseName, string.Join(", ", influencedModuleInfo[phaseName].ToList()))); + } + + return influencedModuleInfo; + } + + private bool ProcessFileChanged(Dictionary csprojMap) + { + string configPath = Path.GetFullPath(TaskMappingConfigName); + if (!File.Exists(configPath)) + { + throw new Exception("CI phase config is not found!"); + } + string content = File.ReadAllText(configPath); + + CIPhaseFilterConfig config = JsonConvert.DeserializeObject(content); + List<(Regex, List)> ruleList = config.Rules.Select(rule => (new Regex(string.Join("|", rule.Patterns.Select(ProcessSinglePattern))), rule.Phases)).ToList(); + + DateTime startTime = DateTime.Now; + + Dictionary> influencedModuleInfo = CalculateInfluencedModuleInfoForEachPhase(ruleList, csprojMap); + DateTime endOfRegularExpressionTime = DateTime.Now; + + influencedModuleInfo = CalculateCsprojForBuildAndTest(influencedModuleInfo, csprojMap); + DateTime endTime = DateTime.Now; + Console.WriteLine(string.Format("Takes {0} seconds for RE match, {1} seconds for phase config.", (endOfRegularExpressionTime - startTime).TotalSeconds, (endTime - endOfRegularExpressionTime).TotalSeconds)); + + FilterTaskResult.PhaseInfo = influencedModuleInfo; + + return true; + } + + /// + /// Executes the task to generate a list of test assemblies + /// based on file changes from a specified Pull Request. + /// The output it produces is said list. + /// + /// Returns a value indicating wheter the success status of the task. + public override bool Execute() + { + FilterTaskResult = new CIFilterTaskResult(); + + var csprojMap = ReadMapFile(CsprojMapFilePath, "CsprojMapFilePath"); + + Console.WriteLine(string.Format("FilesChanged: {0}", FilesChanged.Length)); + if (FilesChanged != null && FilesChanged.Length > 0) + { + return ProcessFileChanged(csprojMap); + } + else if (!string.IsNullOrWhiteSpace(TargetModule)) + { + return ProcessTargetModule(csprojMap); + } + return true; + } + } +} diff --git a/tools/BuildPackagesTask/Microsoft.Azure.Build.Tasks/CIFilterTaskResult.cs b/tools/BuildPackagesTask/Microsoft.Azure.Build.Tasks/CIFilterTaskResult.cs new file mode 100644 index 000000000000..fd71c81a5656 --- /dev/null +++ b/tools/BuildPackagesTask/Microsoft.Azure.Build.Tasks/CIFilterTaskResult.cs @@ -0,0 +1,78 @@ +// +// Copyright (c) Microsoft. 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. +// +using Microsoft.Build.Framework; + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Microsoft.WindowsAzure.Build.Tasks +{ + public class CIFilterTaskResult : ITaskItem + { + readonly string _spec = "CIFilterTaskResult"; + public Dictionary> PhaseInfo = new Dictionary>(); + + public string ItemSpec + { + get { return _spec; } + set { } + } + + public ICollection MetadataNames + { + get + { + return PhaseInfo.Keys; + } + } + public int MetadataCount + { + get { return PhaseInfo.Keys.Count; } + } + + public IDictionary CloneCustomMetadata() + { + Dictionary result = new Dictionary(); + + foreach (string key in PhaseInfo.Keys) + { + result[key] = string.Join(";", PhaseInfo[key].ToList()); + } + + return result; + } + + public void CopyMetadataTo(ITaskItem destinationItem) + { + } + + public string GetMetadata(string metadataName) + { + return string.Format("[{0}]", string.Join(", ", PhaseInfo[metadataName].ToList())); + } + + public void RemoveMetadata(string metadataName) + { + } + + public void SetMetadata(string metadataName, string metadataValue) + { + } + } +} diff --git a/tools/BuildPackagesTask/Microsoft.Azure.Build.Tasks/CIPhaseFilterConfig.cs b/tools/BuildPackagesTask/Microsoft.Azure.Build.Tasks/CIPhaseFilterConfig.cs new file mode 100644 index 000000000000..1b3f32e93f9d --- /dev/null +++ b/tools/BuildPackagesTask/Microsoft.Azure.Build.Tasks/CIPhaseFilterConfig.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Microsoft. 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. +// +using System.Collections.Generic; + +namespace Microsoft.WindowsAzure.Build.Tasks +{ + class CIPhaseFilterConfig + { + public List Rules { get; set; } + } + + class Rule + { + public List Patterns { get; set; } + public List Phases { get; set; } + } +} diff --git a/tools/BuildPackagesTask/Microsoft.Azure.Build.Tasks/FilterTask.cs b/tools/BuildPackagesTask/Microsoft.Azure.Build.Tasks/FilterTask.cs deleted file mode 100644 index b2e4afe1d0fd..000000000000 --- a/tools/BuildPackagesTask/Microsoft.Azure.Build.Tasks/FilterTask.cs +++ /dev/null @@ -1,102 +0,0 @@ -// -// Copyright (c) Microsoft. 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. -// -namespace Microsoft.WindowsAzure.Build.Tasks -{ - using Microsoft.Build.Framework; - using Microsoft.Build.Utilities; - using Newtonsoft.Json; - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - - /// - /// A simple Microsoft Build task used to generate a list of test assemblies to be - /// used for testing Azure PowerShell. - /// - public class FilterTask : Task - { - /// - /// Gets or sets the files changed in a given pull request. - /// - [Required] - public string[] FilesChanged { get; set; } - - /// - /// Gets or sets the path to the files-to-test-assemblies map. - /// - [Required] - public string MapFilePath { get; set; } - - /// - /// Gets or set the TargetModule, e.g. Storage - /// - public string TargetModule { get; set; } - - /// - /// Gets or sets the test assemblies output produced by the task. - /// - [Output] - public string[] Output { get; set; } - - /// - /// Executes the task to generate a list of test assemblies - /// based on file changes from a specified Pull Request. - /// The output it produces is said list. - /// - /// Returns a value indicating wheter the success status of the task. - public override bool Execute() - { - if (MapFilePath == null) - { - throw new ArgumentNullException("The MapFilePath cannot be null."); - } - - if (!File.Exists(MapFilePath)) - { - throw new FileNotFoundException("The MapFilePath provided could not be found. Please provide a valid MapFilePath."); - } - - var mappingsDictionary = JsonConvert.DeserializeObject>(File.ReadAllText(MapFilePath)); - - if (FilesChanged != null && FilesChanged.Length > 0) - { - Console.WriteLine($"Filter according to {FilesChanged.Length} file(s) in FilesChanged"); - var filesChangedSet = new HashSet(FilesChanged); - Output = SetGenerator.Generate(filesChangedSet, mappingsDictionary).ToArray(); - } - else if(!string.IsNullOrWhiteSpace(TargetModule)) - { - Console.WriteLine($"Filter module {TargetModule}"); - var modules = (TargetModule.Equals("Accounts"))? new string[] { "Accounts"} : new string[] { "Accounts" , TargetModule}; - Output = SetGenerator.Generate(modules, mappingsDictionary).ToArray(); - } - else - { - Console.WriteLine($"Skip filter and load all from ${MapFilePath}"); - var set = new HashSet(); - mappingsDictionary = JsonConvert.DeserializeObject>(File.ReadAllText(MapFilePath)); - foreach (KeyValuePair pair in mappingsDictionary) - { - set.UnionWith(pair.Value); - } - - Output = set.ToArray(); - } - - return true; - } - } -} diff --git a/tools/CreateFilterMappings.ps1 b/tools/CreateFilterMappings.ps1 index 4b668785796e..db41cc5e181d 100644 --- a/tools/CreateFilterMappings.ps1 +++ b/tools/CreateFilterMappings.ps1 @@ -267,15 +267,24 @@ function Add-CsprojMappings $Values = New-Object System.Collections.Generic.HashSet[string] foreach ($CsprojFile in $CsprojFiles) { - $Project = $CsprojFile.BaseName - foreach ($Solution in $Script:ProjectToSolutionMappings[$Project]) + $Fields = $CsprojFile.FullName.Replace('/', '\').Split('\') + $Project = $Fields[$Fields.Length - 2] + foreach ($ProjectName in $Script:ProjectToSolutionMappings.Keys) { - foreach ($ReferencedProject in $Script:SolutionToProjectMappings[$Solution]) + foreach ($Solution in $Script:ProjectToSolutionMappings[$ProjectName]) { - $TempValue = $Script:ProjectToFullPathMappings[$ReferencedProject] - if (-not [string]::IsNullOrEmpty($TempValue)) + $Fields = $Solution.Replace('/', '\').Split('\') + $ProjectNameFromSolution = $Fields[$Fields.Length - 2] + if ($ProjectNameFromSolution -eq $Project) { - $Values.Add($TempValue) | Out-Null + foreach ($ReferencedProject in $Script:SolutionToProjectMappings[$Solution]) + { + $TempValue = $Script:ProjectToFullPathMappings[$ReferencedProject] + if (-not [string]::IsNullOrEmpty($TempValue)) + { + $Values.Add($TempValue) | Out-Null + } + } } } } @@ -292,8 +301,8 @@ $Script:ProjectToFullPathMappings = Create-ProjectToFullPathMappings $Script:SolutionToProjectMappings = Create-SolutionToProjectMappings $Script:ProjectToSolutionMappings = Create-ProjectToSolutionMappings -Create-ModuleMappings +# Create-ModuleMappings Create-CsprojMappings -$Script:ModuleMappings | Format-Json | Set-Content -Path (Join-Path -Path $Script:RootPath -ChildPath "ModuleMappings.json") +# $Script:ModuleMappings | Format-Json | Set-Content -Path (Join-Path -Path $Script:RootPath -ChildPath "ModuleMappings.json") $Script:CsprojMappings | Format-Json | Set-Content -Path (Join-Path -Path $Script:RootPath -ChildPath "CsprojMappings.json") \ No newline at end of file diff --git a/tools/StaticAnalysis/DependencyAnalyzer/DependencyMap.cs b/tools/StaticAnalysis/DependencyAnalyzer/DependencyMap.cs index 2285aec35dbc..7604c7c41402 100644 --- a/tools/StaticAnalysis/DependencyAnalyzer/DependencyMap.cs +++ b/tools/StaticAnalysis/DependencyAnalyzer/DependencyMap.cs @@ -77,19 +77,18 @@ public bool Match(IReportRecord other) public IReportRecord Parse(string line) { - var matcher = "\"([^\"]+)\",\"([^\"]+)\",\"([^\"]+)\",\"([^\"]+)\",\"([^\"]+)\",\"([^\"]+)\""; + var matcher = "\"([^\"]+)\",\"([^\"]+)\",\"([^\"]+)\",\"([^\"]+)\",\"([^\"]+)\""; var match = Regex.Match(line, matcher); - if (!match.Success || match.Groups.Count < 7) + if (!match.Success || match.Groups.Count < 6) { - throw new InvalidOperationException(string.Format("Could not parse '{0}' as ExtraAssembly record", line)); + throw new InvalidOperationException(string.Format("Could not parse '{0}' as DependencyMap record", line)); } - Directory = match.Groups[1].Value; - AssemblyName = match.Groups[2].Value; - Severity = int.Parse(match.Groups[3].Value); - ProblemId = int.Parse(match.Groups[4].Value); - Description = match.Groups[5].Value; - Remediation = match.Groups[6].Value; + AssemblyName = match.Groups[1].Value; + AssemblyVersion = match.Groups[2].Value; + ReferencingAssembly = match.Groups[3].Value; + ReferencingAssemblyVersion = match.Groups[4].Value; + Directory = match.Groups[5].Value; return this; } } diff --git a/tools/StaticAnalysis/IssueChecker/IssueChecker.cs b/tools/StaticAnalysis/IssueChecker/IssueChecker.cs new file mode 100644 index 000000000000..6bbb05e881b1 --- /dev/null +++ b/tools/StaticAnalysis/IssueChecker/IssueChecker.cs @@ -0,0 +1,136 @@ +// +// Copyright (c) Microsoft. 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. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +using Tools.Common.Issues; +using Tools.Common.Loggers; +using StaticAnalysis.BreakingChangeAnalyzer; +using StaticAnalysis.DependencyAnalyzer; + +namespace StaticAnalysis.IssueChecker +{ + public class IssueChecker : IStaticAnalyzer + { + private readonly List<(string, string)> exceptionLogInfoList = new List<(string, string)>() + { + ("BreakingChangeIssues.csv", "BreakingChangeIssues"), + ("AssemblyVersionConflict.csv", "AssemblyVersionConflict"), + ("SharedAssemblyConflict.csv", "SharedAssemblyConflict"), + ("MissingAssemblies.csv", "MissingAssembly"), + ("ExtraAssemblies.csv", "ExtraAssembly"), + }; + public AnalysisLogger Logger { get; set; } + + public string Name { get; private set; } + + public IssueChecker() + { + Name = "Issue Checker"; + } + + public void Analyze(IEnumerable scopes) + { + Analyze(scopes, null); + } + + public void Analyze(IEnumerable scopes, IEnumerable modulesToAnalyze) + { + foreach (string scope in scopes) + { + Console.WriteLine(scope); + } + if (scopes.ToList().Count != 1) + { + throw new InvalidOperationException(string.Format("scopes for IssueChecker should be a array contains only reportsDirectory, but here is [{0}]", string.Join(", ", scopes.ToList()))); + } + string reportsDirectory = scopes.First(); + + bool hasCriticalIssue = false; + foreach ((string, string) item in exceptionLogInfoList) + { + string exceptionFileName = item.Item1; + string recordTypeName = item.Item2; + + string exceptionFilePath = Path.Combine(reportsDirectory, exceptionFileName); + if (!File.Exists(exceptionFilePath)) + { + continue; + } + if (IsSingleExceptionFileHasCriticalIssue(exceptionFilePath, recordTypeName)) + { + hasCriticalIssue = true; + } + } + if (hasCriticalIssue) + { + throw new InvalidOperationException(string.Format("One or more errors occurred in validation. " + + "See the analysis reports at {0} for details", + reportsDirectory)); + } + } + + private bool IsSingleExceptionFileHasCriticalIssue(string exceptionFilePath, string reportRecordTypeName) + { + bool hasError = false; + using (var reader = new StreamReader(exceptionFilePath)) + { + List recordList = new List(); + string header = reader.ReadLine(); + while (!reader.EndOfStream) + { + string line = reader.ReadLine(); + IReportRecord newRecord = ReportRecordFactory.Create(reportRecordTypeName); + recordList.Add(newRecord.Parse(line)); + } + var errorText = new StringBuilder(); + errorText.AppendLine(recordList.First().PrintHeaders()); + foreach (IReportRecord record in recordList) + { + if (record.Severity < 2) + { + hasError = true; + errorText.AppendLine(record.FormatRecord()); + } + } + if (hasError) + { + Console.WriteLine("{0} Errors", exceptionFilePath); + Console.WriteLine(errorText.ToString()); + } + } + return hasError; + } + + public void Analyze(IEnumerable cmdletProbingDirs, Func, IEnumerable> directoryFilter, Func cmdletFilter) + { + throw new NotImplementedException(); + } + + public void Analyze(IEnumerable cmdletProbingDirs, Func, IEnumerable> directoryFilter, Func cmdletFilter, IEnumerable modulesToAnalyze) + { + throw new NotImplementedException(); + } + + public AnalysisReport GetAnalysisReport() + { + throw new NotImplementedException(); + } + } +} diff --git a/tools/StaticAnalysis/Program.cs b/tools/StaticAnalysis/Program.cs index 12dd887d7319..c5849ff06920 100644 --- a/tools/StaticAnalysis/Program.cs +++ b/tools/StaticAnalysis/Program.cs @@ -28,7 +28,6 @@ public class Program { static IList Analyzers = new List() { - new DependencyAnalyzer.DependencyAnalyzer() }; static IList ExceptionFileNames = new List() @@ -51,9 +50,9 @@ public static void Main(string[] args) try { string installDir = null; - if (args.Any(a => a == "--package-directory" || a == "-p")) + if (args.Any(a => a.Equals("--package-directory") || a.Equals("-p"))) { - int idx = Array.FindIndex(args, a => a == "--package-directory" || a == "-p"); + int idx = Array.FindIndex(args, a => a.Equals("--package-directory") || a.Equals("-p")); if (idx + 1 == args.Length) { throw new ArgumentException("No value provided for the --package-directory parameter."); @@ -77,7 +76,7 @@ public static void Main(string[] args) bool logReportsDirectoryWarning = true; if (args.Any(a => a == "--reports-directory" || a == "-r")) { - int idx = Array.FindIndex(args, a => a == "--reports-directory" || a == "-r"); + int idx = Array.FindIndex(args, a => a.Equals("--reports-directory") || a.Equals("-r")); if (idx + 1 == args.Length) { throw new ArgumentException("No value provided for the --reports-directory parameter."); @@ -93,9 +92,9 @@ public static void Main(string[] args) } var modulesToAnalyze = new List(); - if (args.Any(a => a == "--modules-to-analyze" || a == "-m")) + if (args.Any(a => a.Equals("--modules-to-analyze") || a.Equals("-m"))) { - int idx = Array.FindIndex(args, a => a == "--modules-to-analyze" || a == "-m"); + int idx = Array.FindIndex(args, a => a.Equals("--modules-to-analyze") || a.Equals("-m")); if (idx + 1 == args.Length) { Console.WriteLine("No value provided for the --modules-to-analyze parameter. Filtering over all built modules."); @@ -106,25 +105,59 @@ public static void Main(string[] args) } } - Analyzers.Add(new SignatureVerifier.SignatureVerifier()); - Analyzers.Add(new BreakingChangeAnalyzer.BreakingChangeAnalyzer()); + foreach (var moduleName in modulesToAnalyze) + { + Console.WriteLine(string.Format("Module: {0}", moduleName)); + } - var helpOnly = args.Any(a => a == "--help-only" || a == "-h"); - var skipHelp = !helpOnly && args.Any(a => a == "--skip-help" || a == "-s"); - if(helpOnly) + bool needToCheckIssue = false; + if (args.Any(a => a.Equals("--analyzers"))) { - Analyzers.Clear(); + int idx = Array.FindIndex(args, a => a.Equals("--analyzers")); + if (idx + 1 == args.Length) + { + throw new ArgumentException("No value provided for the --package-directory parameter."); + } + + string analyzerNameList = args[idx + 1]; + foreach (string analyzerName in analyzerNameList.Split(';')) + { + if (analyzerName.ToLower().Equals("breaking-change")) + { + Analyzers.Add(new BreakingChangeAnalyzer.BreakingChangeAnalyzer()); + } + if (analyzerName.ToLower().Equals("dependency")) + { + Analyzers.Add(new DependencyAnalyzer.DependencyAnalyzer()); + } + if (analyzerName.ToLower().Equals("signature")) + { + Analyzers.Add(new SignatureVerifier.SignatureVerifier()); + } + if (analyzerName.ToLower().Equals("help")) + { + Analyzers.Add(new HelpAnalyzer.HelpAnalyzer()); + } + if (analyzerName.ToLower().Equals("check-error")) + { + needToCheckIssue = true; + } + } } - if (!skipHelp) + else { + Analyzers.Add(new BreakingChangeAnalyzer.BreakingChangeAnalyzer()); + Analyzers.Add(new DependencyAnalyzer.DependencyAnalyzer()); + Analyzers.Add(new SignatureVerifier.SignatureVerifier()); Analyzers.Add(new HelpAnalyzer.HelpAnalyzer()); + needToCheckIssue = true; } // https://stackoverflow.com/a/9737418/294804 var assemblyDirectory = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath); ExceptionsDirectory = Path.Combine(assemblyDirectory, "Exceptions"); - bool useExceptions = !args.Any(a => a == "--dont-use-exceptions" || a == "-d"); - var useNetcore = args.Any(a => a == "--use-netcore" || a == "-u"); + bool useExceptions = !args.Any(a => a.Equals("--dont-use-exceptions") || a.Equals("-d")); + var useNetcore = args.Any(a => a.Equals("--use-netcore") || a.Equals("-u")); ConsolidateExceptionFiles(ExceptionsDirectory, useNetcore); analysisLogger = useExceptions ? new AnalysisLogger(reportsDirectory, ExceptionsDirectory) : new AnalysisLogger(reportsDirectory); @@ -142,7 +175,12 @@ public static void Main(string[] args) } analysisLogger.WriteReports(); - analysisLogger.CheckForIssues(2); + if (needToCheckIssue) + { + var analyzer = new IssueChecker.IssueChecker(); + analyzer.Analyze(new[] { reportsDirectory }); + } + //analysisLogger.CheckForIssues(2); } finally { diff --git a/tools/StaticAnalysis/ReportRecordFactory.cs b/tools/StaticAnalysis/ReportRecordFactory.cs new file mode 100644 index 000000000000..2ad58aeb1709 --- /dev/null +++ b/tools/StaticAnalysis/ReportRecordFactory.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) Microsoft. 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. +// + +using StaticAnalysis.BreakingChangeAnalyzer; +using StaticAnalysis.DependencyAnalyzer; + +using System; +using System.Collections.Generic; +using System.Text; + +using Tools.Common.Issues; + +namespace StaticAnalysis +{ + public static class ReportRecordFactory + { + public static IReportRecord Create(string type) + { + if (type.Equals("BreakingChangeIssue")) + { + return new BreakingChangeIssue(); + } + if (type.Equals("AssemblyVersionConflict")) + { + return new AssemblyVersionConflict(); + } + if (type.Equals("SharedAssemblyConflict")) + { + return new SharedAssemblyConflict(); + } + if (type.Equals("MissingAssembly")) + { + return new MissingAssembly(); + } + if (type.Equals("ExtraAssembly")) + { + return new ExtraAssembly(); + } + + return null; + } + } +}