diff --git a/Directory.Build.props b/Directory.Build.props
index b6219c8d..944bec49 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -7,6 +7,7 @@
<_DefaultTargetFrameworks>net8.0-android
+ <_DefaultNetTargetFrameworks>net8.0
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 7a1e5433..cca877cd 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -5,24 +5,19 @@ trigger:
pr:
- main
-variables:
- BUILD_NUMBER: $(Build.BuildNumber)
- BUILD_COMMIT: $(Build.SourceVersion)
-
- # Build variables
- mainBranchName: main # Name of Git "main" branch
- configuration: Release # Build configuration: 'Debug', 'Release'
-
- # Reporting variables
- areaPath: DevDiv\VS Client - Runtime SDKs\Android # AzDo area path to log any issues
-
- # Windows specific variables
- windowsAgentPoolName: Maui-1ESPT # Windows VM pool name
- windowsImage: 1ESPT-Windows2022 # Windows VM image name
+parameters:
+- name: RunExtendedTests
+ displayName: Run Extended Tests?
+ type: boolean
+ default: false
- # macOS specific variables
- macosAgentPoolName: Azure Pipelines # macOS VM pool name
- macosImage: internal-macos12 # macOS VM image name
+variables:
+ # Variables used by both AndroidX/GPS go in the template
+ - template: build/ci/variables.yml@self
+
+ # Variables only used by AndroidX go here
+ - name: skipUnitTests
+ value: false
resources:
repositories:
@@ -45,7 +40,8 @@ extends:
os: windows
stages:
- - stage: Build
+ - stage: build_windows
+ displayName: Build - Windows
jobs:
- template: build/ci/build.yml@self
@@ -55,10 +51,20 @@ extends:
name: $(windowsAgentPoolName)
image: $(windowsImage)
os: windows
- mainBranchName: $(mainBranchName)
- configuration: $(configuration)
runAPIScan: true
+ - template: sign-artifacts/jobs/v2.yml@internal-templates
+ parameters:
+ artifactName: output-windows
+ usePipelineArtifactTasks: true
+ use1ESTemplate: true
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/')
+
+ - stage: build_mac
+ displayName: Build - Mac
+ dependsOn:
+
+ jobs:
- template: build/ci/build.yml@self
parameters:
name: macos
@@ -66,13 +72,11 @@ extends:
name: $(macosAgentPoolName)
vmImage: $(macosImage)
os: macOS
- mainBranchName: $(mainBranchName)
- configuration: $(configuration)
- - template: sign-artifacts/jobs/v2.yml@internal-templates
- parameters:
- dependsOn: [ 'build_windows' ]
- artifactName: output-windows
- usePipelineArtifactTasks: true
- use1ESTemplate: true
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/')
+ - template: build/ci/stage-extended-tests.yml@self
+ parameters:
+ stageCondition: and(succeeded(), eq('${{ parameters.RunExtendedTests }}', 'true'))
+ buildPool:
+ name: $(windowsAgentPoolName)
+ image: $(windowsImage)
+ os: windows
diff --git a/build/ci/build-and-test.yml b/build/ci/build-and-test.yml
index c6fa3ae5..9a2f87da 100644
--- a/build/ci/build-and-test.yml
+++ b/build/ci/build-and-test.yml
@@ -1,9 +1,5 @@
parameters:
- condition: succeeded()
- verbosity: # the build verbosity: 'minimal', 'normal', 'diagnostic'
- configuration: # the build configuration: 'Debug', 'Release'
artifactsPath:
- skipUnitTests: false # do not run unit test step
validPackagePrefixes: # any NuGet prefixes that should pass validation
- Xamarin
@@ -14,8 +10,8 @@ steps:
- pwsh: |
dotnet cake build.cake `
--target=ci-build `
- --configuration="${{ parameters.configuration }}" `
- --verbosity="${{ parameters.verbosity }}"
+ --configuration="$(configuration)" `
+ --verbosity="$(verbosity)"
displayName: 'Build packages'
env:
JavaSdkDirectory: $(JAVA_HOME)
@@ -27,7 +23,7 @@ steps:
- pwsh: |
dotnet cake validation.cake `
--namespaces="${{ join(',', parameters.validPackagePrefixes) }}" `
- --verbosity="${{ parameters.verbosity }}"
+ --verbosity="$(verbosity)"
displayName: 'Run NuGet package validation'
- pwsh: |
@@ -35,7 +31,7 @@ steps:
--artifacts="${{ parameters.artifactsPath }}" `
--output="${{ parameters.artifactsPath }}/api-diff" `
--cache="$(Agent.TempDirectory)/api-diff" `
- --verbosity="${{ parameters.verbosity }}"
+ --verbosity="$(verbosity)"
displayName: 'Generate API diff'
- pwsh: dotnet cake utilities.cake -t=verify-namespace-file
@@ -44,8 +40,8 @@ steps:
- pwsh: |
dotnet cake build.cake `
--target=ci-samples `
- --configuration="${{ parameters.configuration }}" `
- --verbosity="${{ parameters.verbosity }}"
+ --configuration="$(configuration)" `
+ --verbosity="$(verbosity)"
displayName: 'Build samples'
env:
JavaSdkDirectory: $(JAVA_HOME)
@@ -56,8 +52,8 @@ steps:
- task: DotNetCoreCLI@2
displayName: Run unit tests
- condition: ne(${{ parameters.skipUnitTests }}, 'true')
+ condition: ne(variables['skipUnitTests'], 'true')
inputs:
command: test
projects: util/**/*.Tests.csproj
- arguments: '-c ${{ parameters.configuration }}'
+ arguments: '-c $(configuration)'
diff --git a/build/ci/build.yml b/build/ci/build.yml
index 58c66d22..209752eb 100644
--- a/build/ci/build.yml
+++ b/build/ci/build.yml
@@ -4,19 +4,9 @@ parameters:
buildPool: # VM pool information
# Build Parameters
- mainBranchName: 'main' # Name of Git "main" branch
- configuration: 'Release' # Build configuration: 'Debug', 'Release'
- verbosity: 'normal' # Build verbosity: 'minimal', 'normal', 'diagnostic'
timeoutInMinutes: 300 # Max job runtime in minutes
runAPIScan: false # Run APIScan analysis
- # Tool Parameters
- dotnetVersion: '8.0.301' # .NET version to install on agent
- dotnetWorkloadRollbackFile: 'workloads.json' # Rollback file specifying workload versions to install
- dotnetNuGetOrgSource: 'https://api.nuget.org/v3/index.json' # NuGet.org URL to find workloads
- dotnetWorkloadSource: 'https://aka.ms/dotnet6/nuget/index.json' # .NET engineering URL to find workloads
- skipUnitTests: false # Skip running unit tests
-
tools: # Additional .NET global tools to install
- 'xamarin.androidbinderator.tool': '0.5.7'
- 'Cake.Tool': '4.0.0'
@@ -47,23 +37,16 @@ jobs:
steps:
- template: setup-environment.yml
parameters:
- dotnetVersion: ${{ parameters.dotnetVersion }}
- dotnetWorkloadRollbackFile: ${{ parameters.dotnetWorkloadRollbackFile }}
- dotnetWorkloadSource: ${{ parameters.dotnetWorkloadSource }}
- dotnetNuGetOrgSource: ${{ parameters.dotnetNuGetOrgSource }}
dotnetTools: ${{ parameters.tools }}
- template: build-and-test.yml
parameters:
artifactsPath: ${{ parameters.artifactsPath }}
- verbosity: ${{ parameters.verbosity }}
- configuration: ${{ parameters.configuration }}
- skipUnitTests: ${{ parameters.skipUnitTests }}
- ${{ if eq(parameters.runAPIScan, true) }}:
- template: api-scan.yml
parameters:
- mainBranchName: ${{ parameters.mainBranchName }}
+ mainBranchName: $(mainBranchName)
# Copy SignList.xml to output
- pwsh: |
diff --git a/build/ci/job-extended-tests.yml b/build/ci/job-extended-tests.yml
new file mode 100644
index 00000000..f3b24eb0
--- /dev/null
+++ b/build/ci/job-extended-tests.yml
@@ -0,0 +1,61 @@
+# Runs test(s) that are too expensive to run on every commit
+
+parameters:
+ jobName: # Job display name ('Android' or 'MAUI')
+ buildPool: # VM pool information
+ agentCount: # Agents to run in parallel
+ testFilter: # Test category filter
+
+ tools: # Additional .NET global tools to install
+ - 'dotnet-test-slicer' : '0.1.0-alpha7'
+
+jobs:
+- job: ${{ parameters.jobName }}_package_tests
+ displayName: ${{ parameters.jobName }} Package Tests
+ strategy:
+ parallel: ${{ parameters.agentCount }}
+ pool: ${{ parameters.buildPool }}
+ timeoutInMinutes: 480
+
+ steps:
+ - template: setup-environment.yml
+ parameters:
+ dotnetTools: ${{ parameters.tools }}
+
+ - task: DownloadPipelineArtifact@2
+ inputs:
+ artifactName: output-windows
+ downloadPath: output
+
+ # Build test assembly
+ - task: DotNetCoreCLI@2
+ displayName: Build unit tests
+ inputs:
+ command: build
+ projects: $(testAssemblyProject)
+ arguments: -c $(configuration)
+
+ # Figure out which tests this slice is running
+ - pwsh: >-
+ dotnet-test-slicer
+ slice
+ --test-assembly="$(testAssembly)"
+ --test-filter="${{ parameters.testFilter }}"
+ --slice-number=$(System.JobPositionInPhase)
+ --total-slices=$(System.TotalJobsInPhase)
+ --outfile="$(testAssembly).runsettings"
+ displayName: Slice unit tests
+ failOnStderr: true
+
+ # Run unit tests
+ - task: DotNetCoreCLI@2
+ inputs:
+ command: test
+ projects: $(testAssembly)
+ arguments: >-
+ --settings "$(testAssembly).runsettings"
+ publishTestResults: true
+ testRunTitle: ${{ parameters.jobName }} Package Tests - $(System.JobPositionInPhase)
+ displayName: Run unit tests
+ continueOnError: true
+ timeoutInMinutes: 480
diff --git a/build/ci/setup-environment.yml b/build/ci/setup-environment.yml
index 4dcf5b67..8838c419 100644
--- a/build/ci/setup-environment.yml
+++ b/build/ci/setup-environment.yml
@@ -1,23 +1,18 @@
parameters:
- condition: succeeded()
- dotnetVersion: ''
- dotnetWorkloadRollbackFile: ''
- dotnetWorkloadSource: ''
- dotnetNuGetOrgSource: ''
dotnetTools: []
steps:
# before the build starts, make sure the tooling is as expected
- task: UseDotNet@2
- displayName: 'Use dotnet ${{ parameters.dotnetVersion }}'
+ displayName: 'Use dotnet $(dotnetVersion)'
inputs:
- version: ${{ parameters.dotnetVersion }}
+ version: $(dotnetVersion)
performMultiLevelLookup: true
includePreviewVersions: true
- condition: ne('${{ parameters.dotnetVersion }}', '')
+ condition: ne('$(dotnetVersion)', '')
- pwsh: |
- dotnet workload install maui --verbosity diag --from-rollback-file ${{ parameters.dotnetWorkloadRollbackFile }} --source ${{ parameters.dotnetWorkloadSource }} --source ${{ parameters.dotnetNuGetOrgSource }}
+ dotnet workload install maui --verbosity diag --from-rollback-file $(dotnetWorkloadRollbackFile) --source $(dotnetWorkloadSource) --source $(dotnetNuGetOrgSource)
if ($LASTEXITCODE -ne 0) {
Write-Host "##vso[task.logissue type=error]Failed to install workloads."
Write-Host "##vso[task.complete result=Failed;]"
diff --git a/build/ci/stage-extended-tests.yml b/build/ci/stage-extended-tests.yml
new file mode 100644
index 00000000..df8a8030
--- /dev/null
+++ b/build/ci/stage-extended-tests.yml
@@ -0,0 +1,27 @@
+# Runs test(s) that are too expensive to run on every commit
+
+parameters:
+ stageCondition: # When to run this stage
+ buildPool: # VM pool information
+
+stages:
+- stage: extended_tests
+ displayName: Extended Tests
+ dependsOn: build_windows
+ condition: ${{ parameters.stageCondition }}
+
+ jobs:
+
+ - template: job-extended-tests.yml
+ parameters:
+ jobName: Android
+ agentCount: 6
+ testFilter: cat = Android
+ buildPool: ${{ parameters.buildPool }}
+
+ - template: job-extended-tests.yml
+ parameters:
+ jobName: MAUI
+ agentCount: 10
+ testFilter: cat = MAUI
+ buildPool: ${{ parameters.buildPool }}
diff --git a/build/ci/variables.yml b/build/ci/variables.yml
new file mode 100644
index 00000000..97ef9b78
--- /dev/null
+++ b/build/ci/variables.yml
@@ -0,0 +1,30 @@
+variables:
+
+ BUILD_NUMBER: $(Build.BuildNumber)
+ BUILD_COMMIT: $(Build.SourceVersion)
+
+ # Build variables
+ mainBranchName: main # Name of Git "main" branch
+ configuration: Release # Build configuration: 'Debug', 'Release'
+ verbosity: 'normal' # Build verbosity: 'minimal', 'normal', 'diagnostic'
+
+ # Reporting variables
+ areaPath: DevDiv\VS Client - Runtime SDKs\Android # AzDo area path to log any issues
+
+ # Windows specific variables
+ windowsAgentPoolName: Maui-1ESPT # Windows VM pool name
+ windowsImage: 1ESPT-Windows2022 # Windows VM image name
+
+ # macOS specific variables
+ macosAgentPoolName: Azure Pipelines # macOS VM pool name
+ macosImage: internal-macos12 # macOS VM image name
+
+ # Tool variables
+ dotnetVersion: '8.0.301' # .NET version to install on agent
+ dotnetWorkloadRollbackFile: 'workloads.json' # Rollback file specifying workload versions to install
+ dotnetNuGetOrgSource: 'https://api.nuget.org/v3/index.json' # NuGet.org URL to find workloads
+ dotnetWorkloadSource: 'https://aka.ms/dotnet6/nuget/index.json' # .NET engineering URL to find workloads
+
+ # Extended test variables
+ testAssemblyProject: tests/extended/ExtendedTests.csproj # Extended tests project file
+ testAssembly: tests/extended/bin/$(configuration)/net8.0/ExtendedTests.dll # Extended tests compiled binary
diff --git a/samples/NuGet.config b/samples/NuGet.config
index 836ca00f..7eb3b451 100644
--- a/samples/NuGet.config
+++ b/samples/NuGet.config
@@ -21,11 +21,20 @@
+
+
+
+
+
-
+
+
+
+
+
diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props
new file mode 100644
index 00000000..d8a473f1
--- /dev/null
+++ b/tests/Directory.Build.props
@@ -0,0 +1,9 @@
+
+
+
+
+ <_DefaultTargetFrameworks>net8.0-android
+ <_DefaultNetTargetFrameworks>net8.0
+
+
diff --git a/tests/extended/BinderatorConfigFileParser.cs b/tests/extended/BinderatorConfigFileParser.cs
new file mode 100644
index 00000000..2681c156
--- /dev/null
+++ b/tests/extended/BinderatorConfigFileParser.cs
@@ -0,0 +1,195 @@
+#nullable disable
+
+using System.ComponentModel;
+using Newtonsoft.Json;
+
+namespace ExtendedTests;
+
+public class BinderatorConfigFileParser
+{
+ public static async Task> ParseConfigurationFile (string filename)
+ {
+ string json;
+
+ if (filename.StartsWith ("http")) {
+
+ // Configuration file URL
+ using (var client = new HttpClient ())
+ json = await client.GetStringAsync (filename);
+
+ } else {
+
+ // Local configuration file
+ if (string.IsNullOrWhiteSpace (filename) || !File.Exists (filename)) {
+ System.Console.WriteLine ($"Could not find configuration file: '{filename}'");
+ Environment.Exit (-1);
+ }
+
+ json = File.ReadAllText (filename);
+ }
+
+ return JsonConvert.DeserializeObject> (json);
+ }
+
+ static async Task> GetExternalDependencies (Options options)
+ {
+ var list = new List ();
+
+ foreach (var file in options.DependencyConfigs) {
+ var config = await ParseConfigurationFile (file);
+
+ list.AddRange (config.SelectMany (arr => arr.Artifacts.Where (a => !a.DependencyOnly)));
+ }
+
+ return list;
+ }
+
+ // Configuration File Model
+ public class Template
+ {
+ [JsonProperty ("templateFile")]
+ public string TemplateFile { get; set; }
+
+ [JsonProperty ("outputFileRule")]
+ public string OutputFileRule { get; set; }
+ }
+
+ public class TemplateSetModel
+ {
+ [JsonProperty ("name")]
+ public string Name { get; set; }
+
+ [JsonProperty ("mavenRepositoryType")]
+ public MavenRepoType? MavenRepositoryType { get; set; }
+
+ [JsonProperty ("mavenRepositoryLocation")]
+ public string MavenRepositoryLocation { get; set; } = null;
+
+ [JsonProperty ("templates")]
+ public List Templates { get; set; } = new List ();
+ }
+
+ public class ArtifactModel
+ {
+ [JsonProperty ("groupId")]
+ public string GroupId { get; set; }
+
+ [JsonProperty ("artifactId")]
+ public string ArtifactId { get; set; }
+
+ [JsonProperty ("version")]
+ public string Version { get; set; }
+
+ [JsonProperty ("nugetVersion")]
+ public string NugetVersion { get; set; }
+
+ [JsonProperty ("nugetId")]
+ public string NugetId { get; set; }
+
+ [DefaultValue ("")]
+ [JsonProperty ("dependencyOnly")]
+ public bool DependencyOnly { get; set; }
+
+ [JsonProperty ("frozen")]
+ public bool Frozen { get; set; }
+
+ [JsonProperty ("excludedRuntimeDependencies")]
+ public string ExcludedRuntimeDependencies { get; set; }
+
+ [JsonProperty ("templateSet")]
+ public string TemplateSet { get; set; }
+
+ [JsonProperty ("metadata")]
+ public Dictionary Metadata { get; set; } = new Dictionary ();
+
+ public bool ShouldSerializeMetadata () => Metadata.Any ();
+ }
+
+ public class MyArray
+ {
+ [JsonProperty ("mavenRepositoryType")]
+ public MavenRepoType MavenRepositoryType { get; set; }
+
+ [JsonProperty ("mavenRepositoryLocation")]
+ public string MavenRepositoryLocation { get; set; }
+
+ [JsonProperty ("slnFile")]
+ public string SlnFile { get; set; }
+
+ [JsonProperty ("strictRuntimeDependencies")]
+ public bool StrictRuntimeDependencies { get; set; }
+
+ [JsonProperty ("excludedRuntimeDependencies")]
+ public string ExcludedRuntimeDependencies { get; set; }
+
+ [JsonProperty ("additionalProjects")]
+ public List AdditionalProjects { get; set; }
+
+ [JsonProperty ("templates")]
+ public List Templates { get; set; }
+
+ [JsonProperty ("artifacts")]
+ public List Artifacts { get; set; }
+
+ [JsonProperty ("templateSets")]
+ public List TemplateSets { get; set; } = new List ();
+
+ public TemplateSetModel GetTemplateSet (string name)
+ {
+ // If an artifact doesn't specify a template set, first try using the original
+ // single template list if it exists. If not, look for a template set called "default".
+ if (string.IsNullOrEmpty (name)) {
+ if (Templates.Any ())
+ return new TemplateSetModel { Templates = Templates };
+
+ name = "default";
+ }
+
+ var set = TemplateSets.FirstOrDefault (s => s.Name == name);
+
+ if (set == null)
+ throw new ArgumentException ($"Could not find requested template set '{name}'");
+
+ return set;
+ }
+ }
+
+ public class Root
+ {
+ [JsonProperty ("MyArray")]
+ public List MyArray { get; set; }
+ }
+
+ public enum MavenRepoType
+ {
+ Url,
+ Directory,
+ Google,
+ MavenCentral
+ }
+
+ public class Options
+ {
+ public string ConfigFile { get; }
+ public bool Update { get; }
+ public bool Bump { get; }
+ public bool Sort { get; }
+ public bool Published { get; }
+ public List DependencyConfigs { get; }
+
+ public Options (IList args)
+ {
+ // Config file must always be the first argument
+ ConfigFile = args [0];
+
+ Update = args.Any (a => a.ToLowerInvariant () == "update");
+ Bump = args.Any (a => a.ToLowerInvariant () == "bump");
+ Sort = args.Any (a => a.ToLowerInvariant () == "sort");
+ Published = args.Any (a => a.ToLowerInvariant () == "published");
+
+ DependencyConfigs = args.Where (a => a.StartsWith ("-dep:")).Select (a => a.Substring (5)).ToList ();
+ }
+
+ public bool ShouldWriteOutput => Update || Bump || Sort;
+ }
+}
diff --git a/tests/extended/CanonicalError.cs b/tests/extended/CanonicalError.cs
new file mode 100644
index 00000000..bbbeb15d
--- /dev/null
+++ b/tests/extended/CanonicalError.cs
@@ -0,0 +1,412 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Globalization;
+using System.Text.RegularExpressions;
+
+#nullable disable
+
+namespace ExtendedTests
+{
+ ///
+ /// Functions for dealing with the specially formatted errors returned by
+ /// build tools.
+ ///
+ ///
+ /// Various tools produce and consume CanonicalErrors in various formats.
+ ///
+ /// DEVENV Format When Clicking on Items in the Output Window
+ /// (taken from env\msenv\core\findutil.cpp ParseLocation function)
+ ///
+ /// v:\dir\file.ext (loc) : msg
+ /// \\server\share\dir\file.ext(loc):msg
+ /// url
+ ///
+ /// loc:
+ /// (line)
+ /// (line-line)
+ /// (line,col)
+ /// (line,col-col)
+ /// (line,col,len)
+ /// (line,col,line,col)
+ ///
+ /// DevDiv Build Process
+ /// (taken from tools\devdiv2.def)
+ ///
+ /// To echo warnings and errors to the build console, the
+ /// "description block" must be recognized by build. To do this,
+ /// add a $(ECHO_COMPILING_COMMAND) or $(ECHO_PROCESSING_COMMAND)
+ /// to the first line of the description block, e.g.
+ ///
+ /// $(ECHO_COMPILING_CMD) Resgen_$<
+ ///
+ /// Errors must have the format:
+ ///
+ /// <text> : error [num]: <msg>
+ ///
+ /// Warnings must have the format:
+ ///
+ /// <text> : warning [num]: <msg>
+ ///
+ internal static class CanonicalError
+ {
+ // Defines the main pattern for matching messages.
+ private static readonly Lazy s_originCategoryCodeTextExpression = new Lazy (
+ () => new Regex
+ (
+ // Beginning of line and any amount of whitespace.
+ @"^\s*"
+ // Match a [optional project number prefix 'ddd>'], single letter + colon + remaining filename, or
+ // string with no colon followed by a colon.
+ + @"(((?(((\d+>)?[a-zA-Z]?:[^:]*)|([^:]*))):)"
+ // Origin may also be empty. In this case there's no trailing colon.
+ + "|())"
+ // Match the empty string or a string without a colon that ends with a space
+ + "(?(()|([^:]*? )))"
+ // Match 'error' or 'warning'.
+ + @"(?(error|warning))"
+ // Match anything starting with a space that's not a colon/space, followed by a colon.
+ // Error code is optional in which case "error"/"warning" can be followed immediately by a colon.
+ + @"( \s*(?[^: ]*))?\s*:"
+ // Whatever's left on this line, including colons.
+ + "(?.*)$",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled
+ ));
+
+ private static readonly Lazy s_originCategoryCodeTextExpression2 = new Lazy (
+ () => new Regex
+ (
+ @"^\s*(?(?.*):(?(?[0-9]*):(?[0-9]*))):(? error| warning):(?.*)",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled
+ ));
+
+ // Matches and extracts filename and location from an 'origin' element.
+ private static readonly Lazy s_filenameLocationFromOrigin = new Lazy (
+ () => new Regex
+ (
+ "^" // Beginning of line
+ + @"(\d+>)?" // Optional ddd> project number prefix
+ + "(?.*)" // Match anything.
+ + @"\(" // Find a parenthesis.
+ + @"(?[\,,0-9,-]*)" // Match any combination of numbers and ',' and '-'
+ + @"\)\s*" // Find the closing paren then any amount of spaces.
+ + "$", // End-of-line
+ RegexOptions.IgnoreCase | RegexOptions.Compiled
+ ));
+
+ // Matches location that is a simple number.
+ private static readonly Lazy s_lineFromLocation = new Lazy (
+ () => new Regex // Example: line
+ (
+ "^" // Beginning of line
+ + "(?[0-9]*)" // Match any number.
+ + "$", // End-of-line
+ RegexOptions.IgnoreCase | RegexOptions.Compiled
+ ));
+
+ // Matches location that is a range of lines.
+ private static readonly Lazy s_lineLineFromLocation = new Lazy (
+ () => new Regex // Example: line-line
+ (
+ "^" // Beginning of line
+ + "(?[0-9]*)" // Match any number.
+ + "-" // Dash
+ + "(?[0-9]*)" // Match any number.
+ + "$", // End-of-line
+ RegexOptions.IgnoreCase | RegexOptions.Compiled
+ ));
+
+ // Matches location that is a line and column
+ private static readonly Lazy s_lineColFromLocation = new Lazy (
+ () => new Regex // Example: line,col
+ (
+ "^" // Beginning of line
+ + "(?[0-9]*)" // Match any number.
+ + "," // Comma
+ + "(?[0-9]*)" // Match any number.
+ + "$", // End-of-line
+ RegexOptions.IgnoreCase | RegexOptions.Compiled
+ ));
+
+ // Matches location that is a line and column-range
+ private static readonly Lazy s_lineColColFromLocation = new Lazy (
+ () => new Regex // Example: line,col-col
+ (
+ "^" // Beginning of line
+ + "(?[0-9]*)" // Match any number.
+ + "," // Comma
+ + "(?[0-9]*)" // Match any number.
+ + "-" // Dash
+ + "(?[0-9]*)" // Match any number.
+ + "$", // End-of-line
+ RegexOptions.IgnoreCase | RegexOptions.Compiled
+ ));
+
+ // Matches location that is line,col,line,col
+ private static readonly Lazy s_lineColLineColFromLocation = new Lazy (
+ () => new Regex // Example: line,col,line,col
+ (
+ "^" // Beginning of line
+ + "(?[0-9]*)" // Match any number.
+ + "," // Comma
+ + "(?[0-9]*)" // Match any number.
+ + "," // Dash
+ + "(?[0-9]*)" // Match any number.
+ + "," // Dash
+ + "(?[0-9]*)" // Match any number.
+ + "$", // End-of-line
+ RegexOptions.IgnoreCase | RegexOptions.Compiled
+ ));
+
+ ///
+ /// Represents the parts of a decomposed canonical message.
+ ///
+ internal sealed class Parts
+ {
+ ///
+ /// Defines the error category\severity level.
+ ///
+ internal enum Category
+ {
+ Warning,
+ Error
+ }
+
+ ///
+ /// Value used for unspecified line and column numbers, which are 1-relative.
+ ///
+ internal const int numberNotSpecified = 0;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal Parts ()
+ {
+ }
+
+ ///
+ /// Name of the file or tool (not localized)
+ ///
+ internal string origin;
+
+ ///
+ /// The line number.
+ ///
+ internal int line = numberNotSpecified;
+
+ ///
+ /// The column number.
+ ///
+ internal int column = numberNotSpecified;
+
+ ///
+ /// The ending line number.
+ ///
+ internal int endLine = numberNotSpecified;
+
+ ///
+ /// The ending column number.
+ ///
+ internal int endColumn = numberNotSpecified;
+
+ ///
+ /// The category/severity level
+ ///
+ internal Category category;
+
+ ///
+ /// The sub category (localized)
+ ///
+ internal string subcategory;
+
+ ///
+ /// The error code (not localized)
+ ///
+ internal string code;
+
+ ///
+ /// The error message text (localized)
+ ///
+ internal string text;
+ }
+
+ ///
+ /// A small custom int conversion method that treats invalid entries as missing (0). This is done to work around tools
+ /// that don't fully conform to the canonical message format - we still want to salvage what we can from the message.
+ ///
+ ///
+ /// 'value' converted to int or 0 if it can't be parsed or is negative
+ private static int ConvertToIntWithDefault (string value)
+ {
+ int result;
+ var success = int.TryParse (value, NumberStyles.Integer, CultureInfo.InvariantCulture, out result);
+
+ if (!success || result < 0) {
+ result = Parts.numberNotSpecified;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Decompose an error or warning message into constituent parts. If the message isn't in the canonical form, return null.
+ ///
+ /// This method is thread-safe, because the Regex class is thread-safe (per MSDN).
+ ///
+ /// Decomposed canonical message, or null.
+ internal static Parts Parse (string message)
+ {
+ // An unusually long string causes pathologically slow Regex back-tracking.
+ // To avoid that, only scan the first 400 characters. That's enough for
+ // the longest possible prefix: MAX_PATH, plus a huge subcategory string, and an error location.
+ // After the regex is done, we can append the overflow.
+ var messageOverflow = string.Empty;
+ if (message.Length > 400) {
+ messageOverflow = message.Substring (400);
+ message = message.Substring (0, 400);
+ }
+
+ // If a tool has a large amount of output that isn't an error or warning (eg., "dir /s %hugetree%")
+ // the regex below is slow. It's faster to pre-scan for "warning" and "error"
+ // and bail out if neither are present.
+ if (message.IndexOf ("warning", StringComparison.OrdinalIgnoreCase) == -1 &&
+ message.IndexOf ("error", StringComparison.OrdinalIgnoreCase) == -1) {
+ return null;
+ }
+
+ var parsedMessage = new Parts ();
+
+ // First, split the message into three parts--Origin, Category, Code, Text.
+ // Example,
+ // Main.cs(17,20):Command line warning CS0168: The variable 'foo' is declared but never used
+ // -------------- ------------ ------- ------ ----------------------------------------------
+ // Origin SubCategory Cat. Code Text
+ //
+ // To accommodate absolute filenames in Origin, tolerate a colon in the second position
+ // as long as its preceded by a letter.
+ //
+ // Localization Note:
+ // Even in foreign-language versions of tools, the category field needs to be in English.
+ // Also, if origin is a tool name, then that needs to be in English.
+ //
+ // Here's an example from the Japanese version of CL.EXE:
+ // cl : ???? ??? warning D4024 : ?????????? 'AssemblyInfo.cs' ?????????????????? ???????????
+ //
+ // Here's an example from the Japanese version of LINK.EXE:
+ // AssemblyInfo.cpp : fatal error LNK1106: ???????????? ??????????????: 0x6580 ??????????
+ //
+ var match = s_originCategoryCodeTextExpression.Value.Match (message);
+ string category;
+ if (!match.Success) {
+ // try again with the Clang/GCC matcher
+ // Example,
+ // err.cpp:6:3: error: use of undeclared identifier 'force_an_error'
+ // ----------- ----- ---------------------------------------------
+ // Origin Cat. Text
+ match = s_originCategoryCodeTextExpression2.Value.Match (message);
+ if (!match.Success) {
+ return null;
+ }
+
+ category = match.Groups ["CATEGORY"].Value.Trim ();
+ if (string.Equals (category, "error", StringComparison.OrdinalIgnoreCase)) {
+ parsedMessage.category = Parts.Category.Error;
+ } else if (string.Equals (category, "warning", StringComparison.OrdinalIgnoreCase)) {
+ parsedMessage.category = Parts.Category.Warning;
+ } else {
+ // Not an error\warning message.
+ return null;
+ }
+ parsedMessage.line = ConvertToIntWithDefault (match.Groups ["LINE"].Value.Trim ());
+ parsedMessage.column = ConvertToIntWithDefault (match.Groups ["COLUMN"].Value.Trim ());
+ parsedMessage.text = (match.Groups ["TEXT"].Value + messageOverflow).Trim ();
+ parsedMessage.origin = match.Groups ["FILENAME"].Value.Trim ();
+
+ var explodedText = parsedMessage.text.Split ('\'', StringSplitOptions.RemoveEmptyEntries);
+ if (explodedText.Length > 0) {
+ parsedMessage.code = "G" + explodedText [0].GetHashCode ().ToString ("X8");
+ } else {
+ parsedMessage.code = "G00000000";
+ }
+
+ return parsedMessage;
+ }
+
+ var origin = match.Groups ["ORIGIN"].Value.Trim ();
+ category = match.Groups ["CATEGORY"].Value.Trim ();
+ parsedMessage.code = match.Groups ["CODE"].Value.Trim ();
+ parsedMessage.text = (match.Groups ["TEXT"].Value + messageOverflow).Trim ();
+ parsedMessage.subcategory = match.Groups ["SUBCATEGORY"].Value.Trim ();
+
+ // Next, see if category is something that is recognized.
+ if (string.Equals (category, "error", StringComparison.OrdinalIgnoreCase)) {
+ parsedMessage.category = Parts.Category.Error;
+ } else if (string.Equals (category, "warning", StringComparison.OrdinalIgnoreCase)) {
+ parsedMessage.category = Parts.Category.Warning;
+ } else {
+ // Not an error\warning message.
+ return null;
+ }
+
+ // Origin is not a simple file, but it still could be of the form,
+ // foo.cpp(location)
+ match = s_filenameLocationFromOrigin.Value.Match (origin);
+
+ if (match.Success) {
+ // The origin is in the form,
+ // foo.cpp(location)
+ // Assume the filename exists, but don't verify it. What else could it be?
+ var location = match.Groups ["LOCATION"].Value.Trim ();
+ parsedMessage.origin = match.Groups ["FILENAME"].Value.Trim ();
+
+ // Now, take apart the location. It can be one of these:
+ // loc:
+ // (line)
+ // (line-line)
+ // (line,col)
+ // (line,col-col)
+ // (line,col,len)
+ // (line,col,line,col)
+ if (location.Length > 0) {
+ match = s_lineFromLocation.Value.Match (location);
+ if (match.Success) {
+ parsedMessage.line = ConvertToIntWithDefault (match.Groups ["LINE"].Value.Trim ());
+ } else {
+ match = s_lineLineFromLocation.Value.Match (location);
+ if (match.Success) {
+ parsedMessage.line = ConvertToIntWithDefault (match.Groups ["LINE"].Value.Trim ());
+ parsedMessage.endLine = ConvertToIntWithDefault (match.Groups ["ENDLINE"].Value.Trim ());
+ } else {
+ match = s_lineColFromLocation.Value.Match (location);
+ if (match.Success) {
+ parsedMessage.line = ConvertToIntWithDefault (match.Groups ["LINE"].Value.Trim ());
+ parsedMessage.column = ConvertToIntWithDefault (match.Groups ["COLUMN"].Value.Trim ());
+ } else {
+ match = s_lineColColFromLocation.Value.Match (location);
+ if (match.Success) {
+ parsedMessage.line = ConvertToIntWithDefault (match.Groups ["LINE"].Value.Trim ());
+ parsedMessage.column = ConvertToIntWithDefault (match.Groups ["COLUMN"].Value.Trim ());
+ parsedMessage.endColumn = ConvertToIntWithDefault (match.Groups ["ENDCOLUMN"].Value.Trim ());
+ } else {
+ match = s_lineColLineColFromLocation.Value.Match (location);
+ if (match.Success) {
+ parsedMessage.line = ConvertToIntWithDefault (match.Groups ["LINE"].Value.Trim ());
+ parsedMessage.column = ConvertToIntWithDefault (match.Groups ["COLUMN"].Value.Trim ());
+ parsedMessage.endLine = ConvertToIntWithDefault (match.Groups ["ENDLINE"].Value.Trim ());
+ parsedMessage.endColumn = ConvertToIntWithDefault (match.Groups ["ENDCOLUMN"].Value.Trim ());
+ }
+ }
+ }
+ }
+ }
+ }
+ } else {
+ // The origin does not fit the filename(location) pattern.
+ parsedMessage.origin = origin;
+ }
+
+ return parsedMessage;
+ }
+ }
+}
diff --git a/tests/extended/ExtendedTests.csproj b/tests/extended/ExtendedTests.csproj
new file mode 100644
index 00000000..d6f29bbf
--- /dev/null
+++ b/tests/extended/ExtendedTests.csproj
@@ -0,0 +1,20 @@
+
+
+
+ $(_DefaultNetTargetFrameworks)
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/extended/ExtendedTests.sln b/tests/extended/ExtendedTests.sln
new file mode 100644
index 00000000..b4cec8ac
--- /dev/null
+++ b/tests/extended/ExtendedTests.sln
@@ -0,0 +1,24 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.10.34902.84
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExtendedTests", "ExtendedTests.csproj", "{CB699113-2DD2-49D2-84F4-F6738E76D10D}"
+EndProject
+Global
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {32519318-4EF6-4DCE-9E70-381F824180F5}
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {CB699113-2DD2-49D2-84F4-F6738E76D10D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CB699113-2DD2-49D2-84F4-F6738E76D10D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CB699113-2DD2-49D2-84F4-F6738E76D10D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CB699113-2DD2-49D2-84F4-F6738E76D10D}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/tests/extended/TestAllIndividualPackages.cs b/tests/extended/TestAllIndividualPackages.cs
new file mode 100644
index 00000000..58d6c9f2
--- /dev/null
+++ b/tests/extended/TestAllIndividualPackages.cs
@@ -0,0 +1,182 @@
+using System.Text;
+using CliWrap;
+using CliWrap.Buffered;
+using NUnit.Framework;
+
+namespace ExtendedTests;
+
+[TestFixture]
+public class TestAllIndividualPackages
+{
+ static string base_dir = "";
+ static string test_dir = @"output\tests";
+ static string configuration = "Release";
+ static string platform_version = "29";
+ static string net_version = "net8.0";
+
+ static TestAllIndividualPackages ()
+ {
+ // Find the repo base directory
+ while (!File.Exists (Path.Combine (base_dir, "config.json")))
+ base_dir = Path.Combine ("..", base_dir);
+
+ // Create the test directory
+ Directory.CreateDirectory (Path.Combine (base_dir, test_dir));
+
+ // We need a Directory.Build.props file so we don't use the root one, it also
+ // needs to turn off NuGet Central Package Management.
+ var directory_props = Path.Combine (base_dir, test_dir, "Directory.Build.props");
+
+ var props_content = """
+
+
+ false
+
+
+ """;
+
+ if (!File.Exists (directory_props))
+ File.WriteAllText (directory_props, props_content);
+
+ // Set up a NuGet.config file that allows us to use the locally built NuGet packages.
+ // Note we also need to allow things to come from NuGet.org (*) in order to test when
+ // NuGet resolves a mix of the new local packages and existing ones published on NuGet.org.
+ var nuget_config_src = Path.Combine (base_dir, "samples", "NuGet.config");
+ var nuget_config_dst = Path.Combine (base_dir, test_dir, "NuGet.config");
+
+ if (!File.Exists (nuget_config_dst)) {
+ var contents = File.ReadAllText (nuget_config_src);
+ contents = contents.Replace ("../output", "..");
+ contents = contents.Replace ("../packages", "packages");
+ contents = contents.Replace ("Microsoft.Android.Ref.*", "*");
+ File.WriteAllText (nuget_config_dst, contents);
+ }
+ }
+
+ [Test]
+ [Category ("Android")]
+ [TestCaseSource (nameof (GetPackagesToTest))]
+ public Task TestAndroidDotNetPackage (string id, string version)
+ => TestPackage (id, version, "android");
+
+ [Test]
+ [Category ("MAUI")]
+ [TestCaseSource (nameof (GetPackagesToTest))]
+ public Task TestMauiPackage (string id, string version)
+ => TestPackage (id, version, "maui");
+
+ public static object [] GetPackagesToTest ()
+ {
+ var config_file = Path.Combine (base_dir, "config.json");
+ var config = BinderatorConfigFileParser.ParseConfigurationFile (config_file).Result;
+
+ return config.FirstOrDefault ()?.Artifacts?.Where (a => !a.DependencyOnly).Select (a => new object [] { a.NugetId, a.NugetVersion }).ToArray () ?? Array.Empty