dependabot-nuget 0.290.0 → 0.292.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/.editorconfig +1 -0
  3. data/helpers/lib/NuGetUpdater/Directory.Build.props +1 -0
  4. data/helpers/lib/NuGetUpdater/Directory.Packages.props +1 -1
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/AnalyzeCommand.cs +1 -1
  6. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/CloneCommand.cs +1 -1
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs +15 -1
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/RunCommand.cs +2 -2
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/UpdateCommand.cs +1 -1
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs +2 -1
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs +87 -3
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs +1 -1
  13. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs +14 -0
  14. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs +2 -0
  15. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs +19 -1
  16. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/BadRequirementException.cs +9 -0
  17. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Clone/CloneWorker.cs +39 -8
  18. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +111 -17
  19. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscovery.cs +2 -2
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/ProjectDiscoveryResult.cs +2 -0
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs +19 -11
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/ErrorType.cs +2 -0
  23. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/ExperimentsManager.cs +31 -5
  24. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/NuGetUpdater.Core.csproj +1 -0
  25. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/BadRequirement.cs +10 -0
  26. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/CommitOptions.cs +1 -1
  27. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/DependencyFileNotFound.cs +3 -2
  28. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/JobErrorBase.cs +1 -2
  29. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/JobRepoNotFound.cs +1 -4
  30. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/PrivateSourceAuthenticationFailure.cs +1 -1
  31. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/UnknownError.cs +6 -2
  32. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/UpdateNotPossible.cs +1 -1
  33. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +9 -3
  34. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/LockFileUpdater.cs +3 -2
  35. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackageReferenceUpdater.cs +43 -18
  36. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +1 -1
  37. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs +12 -1
  38. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/DependencyConflictResolver.cs +0 -7
  39. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +48 -17
  40. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs +2 -2
  41. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs +45 -7
  42. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProjectHelper.cs +4 -4
  43. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Clone/CloneWorkerTests.cs +60 -2
  44. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs +10 -1
  45. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs +56 -0
  46. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +41 -0
  47. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs +2 -0
  48. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/SdkProjectDiscoveryTests.cs +1 -1
  49. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/MockNuGetPackage.cs +2 -1
  50. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs +1 -1
  51. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/SerializationTests.cs +76 -40
  52. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +20 -2
  53. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.GlobalJson.cs +2 -2
  54. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.LockFile.cs +251 -0
  55. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackageReference.cs +6 -6
  56. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +63 -5
  57. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +277 -73
  58. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/ProjectHelperTests.cs +65 -0
  59. data/helpers/lib/NuGetUpdater/global.json +1 -1
  60. data/lib/dependabot/nuget/file_fetcher.rb +1 -0
  61. data/lib/dependabot/nuget/file_parser.rb +90 -0
  62. data/lib/dependabot/nuget/language.rb +98 -0
  63. data/lib/dependabot/nuget/native_helpers.rb +25 -0
  64. data/lib/dependabot/nuget/package_manager.rb +51 -0
  65. metadata +12 -6
@@ -34,7 +34,8 @@ internal static partial class MSBuildHelper
34
34
  // Ensure MSBuild types are registered before calling a method that loads the types
35
35
  if (!IsMSBuildRegistered)
36
36
  {
37
- SidelineGlobalJsonAsync(currentDirectory, rootDirectory, () =>
37
+ var experimentsManager = new ExperimentsManager() { InstallDotnetSdks = false }; // `global.json` definitely needs to be moved for this operation
38
+ HandleGlobalJsonAsync(currentDirectory, rootDirectory, experimentsManager, () =>
38
39
  {
39
40
  var defaultInstance = MSBuildLocator.QueryVisualStudioInstances().First();
40
41
  MSBuildPath = defaultInstance.MSBuildPath;
@@ -44,9 +45,23 @@ internal static partial class MSBuildHelper
44
45
  }
45
46
  }
46
47
 
47
- public static async Task<T> SidelineGlobalJsonAsync<T>(string currentDirectory, string rootDirectory, Func<Task<T>> action, ILogger? logger = null, bool retainMSBuildSdks = false)
48
+ public static async Task<T> HandleGlobalJsonAsync<T>(
49
+ string currentDirectory,
50
+ string rootDirectory,
51
+ ExperimentsManager experimentsManager,
52
+ Func<Task<T>> action,
53
+ ILogger? logger = null,
54
+ bool retainMSBuildSdks = false
55
+ )
48
56
  {
49
57
  logger ??= new ConsoleLogger();
58
+ if (experimentsManager.InstallDotnetSdks)
59
+ {
60
+ logger.Info($"{nameof(ExperimentsManager.InstallDotnetSdks)} == true; retaining `global.json` contents.");
61
+ var result = await action();
62
+ return result;
63
+ }
64
+
50
65
  var candidateDirectories = PathHelper.GetAllDirectoriesToRoot(currentDirectory, rootDirectory);
51
66
  var globalJsonPaths = candidateDirectories.Select(d => Path.Combine(d, "global.json")).Where(File.Exists).Select(p => (p, p + Guid.NewGuid().ToString())).ToArray();
52
67
  foreach (var (globalJsonPath, tempGlobalJsonPath) in globalJsonPaths)
@@ -322,13 +337,13 @@ internal static partial class MSBuildHelper
322
337
  return false;
323
338
  }
324
339
 
325
- internal static async Task<bool> DependenciesAreCoherentAsync(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, ILogger logger)
340
+ internal static async Task<bool> DependenciesAreCoherentAsync(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, ExperimentsManager experimentsManager, ILogger logger)
326
341
  {
327
342
  var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-coherence_");
328
343
  try
329
344
  {
330
345
  var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages, logger);
331
- var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", ["restore", tempProjectPath], workingDirectory: tempDirectory.FullName);
346
+ var (exitCode, stdOut, stdErr) = await ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(["restore", tempProjectPath], tempDirectory.FullName, experimentsManager);
332
347
 
333
348
  // NU1608: Detected package version outside of dependency constraint
334
349
 
@@ -340,7 +355,7 @@ internal static partial class MSBuildHelper
340
355
  }
341
356
  }
342
357
 
343
- internal static async Task<Dependency[]?> ResolveDependencyConflicts(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, Dependency[] update, ILogger logger)
358
+ internal static async Task<Dependency[]?> ResolveDependencyConflicts(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, Dependency[] update, ExperimentsManager experimentsManager, ILogger logger)
344
359
  {
345
360
  var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-coherence_");
346
361
  PackageManager packageManager = new PackageManager(repoRoot, projectPath);
@@ -348,7 +363,7 @@ internal static partial class MSBuildHelper
348
363
  try
349
364
  {
350
365
  string tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages, logger);
351
- var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", ["restore", tempProjectPath], workingDirectory: tempDirectory.FullName);
366
+ var (exitCode, stdOut, stdErr) = await ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(["restore", tempProjectPath], tempDirectory.FullName, experimentsManager);
352
367
 
353
368
  // Add Dependency[] packages to List<PackageToUpdate> existingPackages
354
369
  List<PackageToUpdate> existingPackages = packages
@@ -498,13 +513,13 @@ internal static partial class MSBuildHelper
498
513
  }
499
514
  }
500
515
 
501
- internal static async Task<Dependency[]?> ResolveDependencyConflictsWithBruteForce(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, ILogger logger)
516
+ internal static async Task<Dependency[]?> ResolveDependencyConflictsWithBruteForce(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, ExperimentsManager experimentsManager, ILogger logger)
502
517
  {
503
518
  var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-coherence_");
504
519
  try
505
520
  {
506
521
  var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages, logger);
507
- var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", ["restore", tempProjectPath], workingDirectory: tempDirectory.FullName);
522
+ var (exitCode, stdOut, stdErr) = await ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(["restore", tempProjectPath], tempDirectory.FullName, experimentsManager);
508
523
  ThrowOnUnauthenticatedFeed(stdOut);
509
524
 
510
525
  // simple cases first
@@ -529,6 +544,7 @@ internal static partial class MSBuildHelper
529
544
  foreach ((string PackageName, NuGetVersion packageVersion) in badPackagesAndVersions)
530
545
  {
531
546
  // this command dumps a JSON object with all versions of the specified package from all package sources
547
+ // not using the `dotnet` execution method because we want to force the latest MSBuild and SDK to be used
532
548
  (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", ["package", "search", PackageName, "--exact-match", "--format", "json"], workingDirectory: tempDirectory.FullName);
533
549
  if (exitCode != 0)
534
550
  {
@@ -591,7 +607,7 @@ internal static partial class MSBuildHelper
591
607
  return p;
592
608
  }).ToArray();
593
609
 
594
- if (await DependenciesAreCoherentAsync(repoRoot, projectPath, targetFramework, candidatePackages, logger))
610
+ if (await DependenciesAreCoherentAsync(repoRoot, projectPath, targetFramework, candidatePackages, experimentsManager, logger))
595
611
  {
596
612
  // return as soon as we find a coherent set
597
613
  return candidatePackages;
@@ -749,14 +765,23 @@ internal static partial class MSBuildHelper
749
765
  return tempProjectPath;
750
766
  }
751
767
 
752
- internal static async Task<ImmutableArray<string>> GetTargetFrameworkValuesFromProject(string repoRoot, string projectPath, ILogger logger)
768
+ internal static async Task<ImmutableArray<string>> GetTargetFrameworkValuesFromProject(string repoRoot, string projectPath, ExperimentsManager experimentsManager, ILogger logger)
753
769
  {
754
- // TODO: once the updater image has all relevant SDKs installed, we won't have to sideline global.json anymore
755
770
  var projectDirectory = Path.GetDirectoryName(projectPath)!;
756
- var (exitCode, stdOut, stdErr) = await SidelineGlobalJsonAsync(projectDirectory, repoRoot, async () =>
771
+ var (exitCode, stdOut, stdErr) = await HandleGlobalJsonAsync(projectDirectory, repoRoot, experimentsManager, async () =>
757
772
  {
758
773
  var targetsHelperPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "TargetFrameworkReporter.targets");
759
- var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", ["build", projectPath, "/t:ReportTargetFramework", $"/p:CustomAfterMicrosoftCommonCrossTargetingTargets={targetsHelperPath};CustomAfterMicrosoftCommonTargets={targetsHelperPath}"], workingDirectory: Path.GetDirectoryName(projectPath));
774
+ var (exitCode, stdOut, stdErr) = await ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(
775
+ [
776
+ "build",
777
+ projectPath,
778
+ "/t:ReportTargetFramework",
779
+ $"/p:CustomAfterMicrosoftCommonCrossTargetingTargets={targetsHelperPath}",
780
+ $"/p:CustomAfterMicrosoftCommonTargets={targetsHelperPath}"
781
+ ],
782
+ projectDirectory,
783
+ experimentsManager
784
+ );
760
785
  return (exitCode, stdOut, stdErr);
761
786
  });
762
787
  ThrowOnUnauthenticatedFeed(stdOut);
@@ -806,6 +831,7 @@ internal static partial class MSBuildHelper
806
831
  string projectPath,
807
832
  string targetFramework,
808
833
  IReadOnlyCollection<Dependency> packages,
834
+ ExperimentsManager experimentsManager,
809
835
  ILogger logger)
810
836
  {
811
837
  var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-resolution_");
@@ -814,7 +840,7 @@ internal static partial class MSBuildHelper
814
840
  var topLevelPackagesNames = packages.Select(p => p.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
815
841
  var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages, logger);
816
842
 
817
- var (exitCode, stdout, stderr) = await ProcessEx.RunAsync("dotnet", ["build", tempProjectPath, "/t:_ReportDependencies"], workingDirectory: tempDirectory.FullName);
843
+ var (exitCode, stdout, stderr) = await ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(["build", tempProjectPath, "/t:_ReportDependencies"], tempDirectory.FullName, experimentsManager);
818
844
  ThrowOnUnauthenticatedFeed(stdout);
819
845
 
820
846
  if (exitCode == 0)
@@ -860,6 +886,7 @@ internal static partial class MSBuildHelper
860
886
  "The plugin credential provider could not acquire credentials",
861
887
  "401 (Unauthorized)",
862
888
  "error NU1301: Unable to load the service index for source",
889
+ "Response status code does not indicate success: 403",
863
890
  };
864
891
  if (unauthorizedMessageSnippets.Any(stdout.Contains))
865
892
  {
@@ -869,9 +896,13 @@ internal static partial class MSBuildHelper
869
896
 
870
897
  internal static string? GetMissingFile(string output)
871
898
  {
872
- var missingFilePattern = new Regex(@"The imported project \""(?<FilePath>.*)\"" was not found");
873
- var match = missingFilePattern.Match(output);
874
- if (match.Success)
899
+ var missingFilePatterns = new[]
900
+ {
901
+ new Regex(@"The imported project \""(?<FilePath>.*)\"" was not found"),
902
+ new Regex(@"The imported file \""(?<FilePath>.*)\"" does not exist"),
903
+ };
904
+ var match = missingFilePatterns.Select(p => p.Match(output)).Where(m => m.Success).FirstOrDefault();
905
+ if (match is not null)
875
906
  {
876
907
  return match.Groups["FilePath"].Value;
877
908
  }
@@ -4,13 +4,13 @@ namespace NuGetUpdater.Core;
4
4
 
5
5
  internal static class NuGetHelper
6
6
  {
7
- internal static async Task<bool> DownloadNuGetPackagesAsync(string repoRoot, string projectPath, IReadOnlyCollection<Dependency> packages, ILogger logger)
7
+ internal static async Task<bool> DownloadNuGetPackagesAsync(string repoRoot, string projectPath, IReadOnlyCollection<Dependency> packages, ExperimentsManager experimentsManager, ILogger logger)
8
8
  {
9
9
  var tempDirectory = Directory.CreateTempSubdirectory("msbuild_sdk_restore_");
10
10
  try
11
11
  {
12
12
  var tempProjectPath = await MSBuildHelper.CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, "netstandard2.0", packages, logger, usePackageDownload: true);
13
- var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", ["restore", tempProjectPath]);
13
+ var (exitCode, stdOut, stdErr) = await ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(["restore", tempProjectPath], tempDirectory.FullName, experimentsManager);
14
14
 
15
15
  return exitCode == 0;
16
16
  }
@@ -5,19 +5,57 @@ namespace NuGetUpdater.Core;
5
5
 
6
6
  public static class ProcessEx
7
7
  {
8
- public static Task<(int ExitCode, string Output, string Error)> RunAsync(string fileName, IEnumerable<string>? arguments = null, string? workingDirectory = null)
8
+ /// <summary>
9
+ /// Run the `dotnet` command with the given values. This will exclude all `MSBuild*` environment variables from the execution.
10
+ /// </summary>
11
+ public static Task<(int ExitCode, string Output, string Error)> RunDotnetWithoutMSBuildEnvironmentVariablesAsync(IEnumerable<string> arguments, string workingDirectory, ExperimentsManager experimentsManager)
12
+ {
13
+ var environmentVariablesToUnset = new List<string>();
14
+ if (experimentsManager.InstallDotnetSdks)
15
+ {
16
+ // If using the SDK specified by a `global.json` file, these environment variables need to be unset to
17
+ // allow the new process to discover the correct MSBuild binaries to load, and not load the ones that
18
+ // this process is using.
19
+ environmentVariablesToUnset.Add("MSBuildExtensionsPath");
20
+ environmentVariablesToUnset.Add("MSBuildLoadMicrosoftTargetsReadOnly");
21
+ environmentVariablesToUnset.Add("MSBUILDLOGIMPORTS");
22
+ environmentVariablesToUnset.Add("MSBuildSDKsPath");
23
+ environmentVariablesToUnset.Add("MSBUILDTARGETOUTPUTLOGGING");
24
+ environmentVariablesToUnset.Add("MSBUILD_EXE_PATH");
25
+ }
26
+
27
+ var environmentVariableOverrides = environmentVariablesToUnset.Select(name => (name, (string?)null));
28
+ return RunAsync("dotnet",
29
+ arguments,
30
+ workingDirectory,
31
+ environmentVariableOverrides
32
+ );
33
+ }
34
+
35
+ public static Task<(int ExitCode, string Output, string Error)> RunAsync(
36
+ string fileName,
37
+ IEnumerable<string>? arguments = null,
38
+ string? workingDirectory = null,
39
+ IEnumerable<(string Name, string? Value)>? environmentVariableOverrides = null
40
+ )
9
41
  {
10
42
  var tcs = new TaskCompletionSource<(int, string, string)>();
11
43
 
12
44
  var redirectInitiated = new ManualResetEventSlim();
45
+ var psi = new ProcessStartInfo(fileName, arguments ?? [])
46
+ {
47
+ UseShellExecute = false, // required to redirect output and set environment variables
48
+ RedirectStandardOutput = true,
49
+ RedirectStandardError = true,
50
+ };
51
+ foreach (var (name, value) in environmentVariableOverrides ?? [])
52
+ {
53
+ psi.EnvironmentVariables[name] = value;
54
+ }
55
+
13
56
  var process = new Process
14
57
  {
15
- StartInfo = new ProcessStartInfo(fileName, arguments ?? [])
16
- {
17
- UseShellExecute = false, // required to redirect output
18
- RedirectStandardOutput = true,
19
- RedirectStandardError = true,
20
- },
58
+ StartInfo = psi,
21
59
  EnableRaisingEvents = true
22
60
  };
23
61
 
@@ -77,11 +77,11 @@ internal static class ProjectHelper
77
77
  var itemPath = projectRootElement.Items
78
78
  .Where(i => i.ElementName.Equals("None", StringComparison.OrdinalIgnoreCase) ||
79
79
  i.ElementName.Equals("Content", StringComparison.OrdinalIgnoreCase))
80
- .Where(i => Path.GetFileName(i.Include).Equals(itemFileName, StringComparison.OrdinalIgnoreCase))
81
- .Select(i => Path.GetFullPath(Path.Combine(projectDirectory, i.Include)))
80
+ .Where(i => !string.IsNullOrEmpty(i.Include))
81
+ .Select(i => Path.GetFullPath(Path.Combine(projectDirectory, i.Include.NormalizePathToUnix())))
82
+ .Where(p => Path.GetFileName(p).Equals(itemFileName, StringComparison.OrdinalIgnoreCase))
82
83
  .Where(File.Exists)
83
- .FirstOrDefault()
84
- ?.NormalizePathToUnix();
84
+ .FirstOrDefault();
85
85
  return itemPath;
86
86
  }
87
87
 
@@ -95,6 +95,65 @@ public class CloneWorkerTests
95
95
  );
96
96
  }
97
97
 
98
+ [Fact]
99
+ public async Task JobFileParseErrorIsReported_InvalidJson()
100
+ {
101
+ // arrange
102
+ var testApiHandler = new TestApiHandler();
103
+ var testGitCommandHandler = new TestGitCommandHandler();
104
+ var cloneWorker = new CloneWorker("JOB-ID", testApiHandler, testGitCommandHandler);
105
+ using var testDirectory = new TemporaryDirectory();
106
+ var jobFilePath = Path.Combine(testDirectory.DirectoryPath, "job.json");
107
+ await File.WriteAllTextAsync(jobFilePath, "not json");
108
+
109
+ // act
110
+ var result = await cloneWorker.RunAsync(new FileInfo(jobFilePath), new DirectoryInfo(testDirectory.DirectoryPath));
111
+
112
+ // assert
113
+ Assert.Equal(1, result);
114
+ var expectedParseErrorObject = testApiHandler.ReceivedMessages.Single(m => m.Type == typeof(UnknownError));
115
+ var unknownError = (UnknownError)expectedParseErrorObject.Object;
116
+ Assert.Equal("JsonException", unknownError.Details["error-class"]);
117
+ }
118
+
119
+ [Fact]
120
+ public async Task JobFileParseErrorIsReported_BadRequirement()
121
+ {
122
+ // arrange
123
+ var testApiHandler = new TestApiHandler();
124
+ var testGitCommandHandler = new TestGitCommandHandler();
125
+ var cloneWorker = new CloneWorker("JOB-ID", testApiHandler, testGitCommandHandler);
126
+ using var testDirectory = new TemporaryDirectory();
127
+ var jobFilePath = Path.Combine(testDirectory.DirectoryPath, "job.json");
128
+
129
+ // write a job file with a valid shape, but invalid requirement
130
+ await File.WriteAllTextAsync(jobFilePath, """
131
+ {
132
+ "job": {
133
+ "source": {
134
+ "provider": "github",
135
+ "repo": "test/repo"
136
+ },
137
+ "security-advisories": [
138
+ {
139
+ "dependency-name": "Some.Dependency",
140
+ "affected-versions": ["not a valid requirement"]
141
+ }
142
+ ]
143
+ }
144
+ }
145
+ """);
146
+
147
+ // act
148
+ var result = await cloneWorker.RunAsync(new FileInfo(jobFilePath), new DirectoryInfo(testDirectory.DirectoryPath));
149
+
150
+ // assert
151
+ Assert.Equal(1, result);
152
+ var expectedParseErrorObject = testApiHandler.ReceivedMessages.Single(m => m.Type == typeof(BadRequirement));
153
+ var badRequirement = (BadRequirement)expectedParseErrorObject.Object;
154
+ Assert.Equal("not a valid requirement", badRequirement.Details["message"]);
155
+ }
156
+
98
157
  private class TestGitCommandHandlerWithOutputs : TestGitCommandHandler
99
158
  {
100
159
  private readonly string _stdout;
@@ -134,8 +193,7 @@ public class CloneWorkerTests
134
193
  // arrange
135
194
  var testApiHandler = new TestApiHandler();
136
195
  testGitCommandHandler ??= new TestGitCommandHandler();
137
- var testLogger = new TestLogger();
138
- var worker = new CloneWorker(testApiHandler, testGitCommandHandler, testLogger);
196
+ var worker = new CloneWorker("TEST-JOB-ID", testApiHandler, testGitCommandHandler);
139
197
 
140
198
  // act
141
199
  var job = new Job()
@@ -45,7 +45,16 @@ public class DiscoveryWorkerTestBase : TestBase
45
45
  ValidateProjectResults(expectedResult.Projects, actualResult.Projects, experimentsManager);
46
46
  Assert.Equal(expectedResult.ExpectedProjectCount ?? expectedResult.Projects.Length, actualResult.Projects.Length);
47
47
  Assert.Equal(expectedResult.ErrorType, actualResult.ErrorType);
48
- Assert.Equal(expectedResult.ErrorDetails, actualResult.ErrorDetails);
48
+ if (expectedResult.ErrorDetailsPattern is not null)
49
+ {
50
+ var errorDetails = actualResult.ErrorDetails?.ToString();
51
+ Assert.NotNull(errorDetails);
52
+ Assert.Matches(expectedResult.ErrorDetailsPattern, errorDetails);
53
+ }
54
+ else
55
+ {
56
+ Assert.Equal(expectedResult.ErrorDetails, actualResult.ErrorDetails);
57
+ }
49
58
 
50
59
  return;
51
60
 
@@ -115,5 +115,61 @@ public partial class DiscoveryWorkerTests
115
115
  }
116
116
  );
117
117
  }
118
+
119
+ [Theory]
120
+ [InlineData(true)]
121
+ [InlineData(false)]
122
+ public async Task DiscoveryIsMergedWithPackageReferences(bool useDirectDiscovery)
123
+ {
124
+ await TestDiscoveryAsync(
125
+ experimentsManager: new ExperimentsManager() { UseDirectDiscovery = useDirectDiscovery },
126
+ packages:
127
+ [
128
+ MockNuGetPackage.CreateSimplePackage("Package.A", "1.0.0", "net46"),
129
+ MockNuGetPackage.CreateSimplePackage("Package.B", "2.0.0", "net46"),
130
+ ],
131
+ workspacePath: "src",
132
+ files: [
133
+ ("src/myproj.csproj", """
134
+ <Project Sdk="Microsoft.NET.Sdk">
135
+ <PropertyGroup>
136
+ <TargetFramework>net46</TargetFramework>
137
+ </PropertyGroup>
138
+ <ItemGroup>
139
+ <None Include="..\unexpected-directory\packages.config" />
140
+ <PackageReference Include="Package.B" Version="2.0.0" />
141
+ </ItemGroup>
142
+ </Project>
143
+ """),
144
+ ("unexpected-directory/packages.config", """
145
+ <?xml version="1.0" encoding="utf-8"?>
146
+ <packages>
147
+ <package id="Package.A" version="1.0.0" targetFramework="net46" />
148
+ </packages>
149
+ """),
150
+ ],
151
+ expectedResult: new()
152
+ {
153
+ Path = "src",
154
+ Projects = [
155
+ new()
156
+ {
157
+ FilePath = "myproj.csproj",
158
+ Properties = [new("TargetFramework", "net46", "src/myproj.csproj")],
159
+ TargetFrameworks = ["net46"],
160
+ Dependencies = [
161
+ new("Package.A", "1.0.0", DependencyType.PackagesConfig, TargetFrameworks: ["net46"]),
162
+ new("Package.B", "2.0.0", DependencyType.PackageReference, IsDirect: true, TargetFrameworks: ["net46"]),
163
+ ],
164
+ ReferencedProjectPaths = [],
165
+ ImportedFiles = [],
166
+ AdditionalFiles = [
167
+ "../unexpected-directory/packages.config"
168
+ ],
169
+ }
170
+ ],
171
+ }
172
+ );
173
+ }
118
174
  }
119
175
  }
@@ -1094,6 +1094,47 @@ public partial class DiscoveryWorkerTests : DiscoveryWorkerTestBase
1094
1094
  );
1095
1095
  }
1096
1096
 
1097
+ [Theory]
1098
+ [InlineData(true)]
1099
+ [InlineData(false)]
1100
+ public async Task DiscoveryReportsDependencyFileNotParseable(bool useDirectDiscovery)
1101
+ {
1102
+ var experimentsManager = new ExperimentsManager() { UseDirectDiscovery = useDirectDiscovery };
1103
+ await TestDiscoveryAsync(
1104
+ experimentsManager: experimentsManager,
1105
+ workspacePath: "",
1106
+ files:
1107
+ [
1108
+ ("project.csproj", """
1109
+ <Project Sdk="Microsoft.NET.Sdk">
1110
+ <PropertyGroup>
1111
+ <TargetFramework>net8.0</TargetFramework>
1112
+ </PropertyGroup>
1113
+ <ItemGroup>
1114
+ <PackageReference Include="Some.Package" Version="1.2.3" />
1115
+ </ItemGroup>
1116
+ </Project>
1117
+ """),
1118
+ ("project2.csproj", """
1119
+ <Project Sdk="Microsoft.NET.Sdk">
1120
+ <PropertyGroup>
1121
+ <TargetFramework>net8.0</TargetFramework>
1122
+ </PropertyGroup>
1123
+ <ItemGroup>
1124
+ <PackageReference: Include="Some.Package2" Version="1.2.3" />
1125
+ </ItemGroup>
1126
+ </Project>
1127
+ """),
1128
+ ],
1129
+ expectedResult: new()
1130
+ {
1131
+ Path = "",
1132
+ Projects = [],
1133
+ ErrorType = ErrorType.DependencyFileNotParseable,
1134
+ ErrorDetails = "project2.csproj",
1135
+ });
1136
+ }
1137
+
1097
1138
  [Fact]
1098
1139
  public async Task ResultFileHasCorrectShapeForAuthenticationFailure()
1099
1140
  {
@@ -12,6 +12,7 @@ public record ExpectedWorkspaceDiscoveryResult : NativeResult
12
12
  public int? ExpectedProjectCount { get; init; }
13
13
  public ExpectedDependencyDiscoveryResult? GlobalJson { get; init; }
14
14
  public ExpectedDependencyDiscoveryResult? DotNetToolsJson { get; init; }
15
+ public string? ErrorDetailsPattern { get; init; } = null;
15
16
  }
16
17
 
17
18
  public record ExpectedSdkProjectDiscoveryResult : ExpectedDependencyDiscoveryResult
@@ -21,6 +22,7 @@ public record ExpectedSdkProjectDiscoveryResult : ExpectedDependencyDiscoveryRes
21
22
  public required ImmutableArray<string> ReferencedProjectPaths { get; init; }
22
23
  public required ImmutableArray<string> ImportedFiles { get; init; }
23
24
  public required ImmutableArray<string> AdditionalFiles { get; init; }
25
+ public string? ErrorDetails { get; init; }
24
26
  }
25
27
 
26
28
  public record ExpectedDependencyDiscoveryResult : IDiscoveryResultWithDependencies
@@ -488,7 +488,7 @@ public class SdkProjectDiscoveryTests : DiscoveryWorkerTestBase
488
488
  var logger = new TestLogger();
489
489
  var fullProjectPath = Path.Combine(testDirectory.DirectoryPath, projectPath);
490
490
  var experimentsManager = new ExperimentsManager() { UseDirectDiscovery = true }; // the following method is direct discovery; this just makes the call to Validate... happy
491
- var projectDiscovery = await SdkProjectDiscovery.DiscoverWithBinLogAsync(testDirectory.DirectoryPath, Path.GetDirectoryName(fullProjectPath)!, fullProjectPath, logger);
491
+ var projectDiscovery = await SdkProjectDiscovery.DiscoverWithBinLogAsync(testDirectory.DirectoryPath, Path.GetDirectoryName(fullProjectPath)!, fullProjectPath, experimentsManager, logger);
492
492
  ValidateProjectResults(expectedProjects, projectDiscovery, experimentsManager);
493
493
  }
494
494
  }
@@ -318,7 +318,8 @@ namespace NuGetUpdater.Core.Test
318
318
  </Project>
319
319
  """
320
320
  );
321
- var (exitCode, stdout, stderr) = ProcessEx.RunAsync("dotnet", ["msbuild", projectPath, "/t:_ReportCurrentSdkVersion"]).Result;
321
+ var experimentsManager = new ExperimentsManager();
322
+ var (exitCode, stdout, stderr) = ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(["msbuild", projectPath, "/t:_ReportCurrentSdkVersion"], projectDir.FullName, experimentsManager).Result;
322
323
  if (exitCode != 0)
323
324
  {
324
325
  throw new Exception($"Failed to report the current SDK version:\n{stdout}\n{stderr}");
@@ -1733,7 +1733,7 @@ public class RunWorkerTests
1733
1733
  analyzeWorker ??= new AnalyzeWorker(experimentsManager, logger);
1734
1734
  updaterWorker ??= new UpdaterWorker(experimentsManager, logger);
1735
1735
 
1736
- var worker = new RunWorker(testApiHandler, discoveryWorker, analyzeWorker, updaterWorker, logger);
1736
+ var worker = new RunWorker("TEST-JOB-ID", testApiHandler, discoveryWorker, analyzeWorker, updaterWorker, logger);
1737
1737
  var repoContentsPathDirectoryInfo = new DirectoryInfo(tempDirectory.DirectoryPath);
1738
1738
  var actualResult = await worker.RunAsync(job, repoContentsPathDirectoryInfo, "TEST-COMMIT-SHA");
1739
1739
  var actualApiMessages = testApiHandler.ReceivedMessages.ToArray();