Add Pip installation report experimental detector (#1129)

* Add PipReport experimental detector

* Don't use primary constructor

* Fix CI break

* Address PR comments

* Update src/Microsoft.ComponentDetection.Detectors/pip/PipReportUtilities.cs

Co-authored-by: Jamie Magee <jamagee@microsoft.com>

* Update src/Microsoft.ComponentDetection.Detectors/pip/PipReportComponentDetector.cs

Co-authored-by: Jamie Magee <jamagee@microsoft.com>

* Log cmd failure

---------

Co-authored-by: Coby Allred <coallred@microsoft.com>
Co-authored-by: Jamie Magee <jamagee@microsoft.com>
This commit is contained in:
Coby Allred 2024-05-22 18:43:13 -07:00 коммит произвёл GitHub
Родитель 5894c27af3
Коммит e9a146ca76
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
44 изменённых файлов: 7710 добавлений и 264 удалений

Просмотреть файл

@ -1,6 +1,7 @@
namespace Microsoft.ComponentDetection.Common;
using System.IO;
using System.Threading.Tasks;
using Microsoft.ComponentDetection.Contracts;
/// <inheritdoc />
@ -18,6 +19,12 @@ public class FileUtilityService : IFileUtilityService
return File.ReadAllText(file.FullName);
}
/// <inheritdoc />
public async Task<string> ReadAllTextAsync(FileInfo file)
{
return await File.ReadAllTextAsync(file.FullName);
}
/// <inheritdoc />
public bool Exists(string fileName)
{

Просмотреть файл

@ -1,4 +1,4 @@
namespace Microsoft.ComponentDetection.Common;
namespace Microsoft.ComponentDetection.Common;
using System;
using System.IO;
@ -75,4 +75,7 @@ public class PathUtilityService : IPathUtilityService
}
private string ResolvePathFromInfo(FileSystemInfo info) => info.LinkTarget ?? info.FullName;
public string NormalizePath(string path) =>
path?.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar);
}

Просмотреть файл

@ -0,0 +1,14 @@
namespace Microsoft.ComponentDetection.Common.Telemetry.Records;
public class FailedParsingFileRecord : BaseDetectionTelemetryRecord
{
public override string RecordName => "FailedParsingFile";
public string DetectorId { get; set; }
public string FilePath { get; set; }
public string ExceptionMessage { get; set; }
public string StackTrace { get; set; }
}

Просмотреть файл

@ -1,12 +0,0 @@
namespace Microsoft.ComponentDetection.Common.Telemetry.Records;
public class FailedReadingFileRecord : BaseDetectionTelemetryRecord
{
public override string RecordName => "FailedReadingFile";
public string FilePath { get; set; }
public string ExceptionMessage { get; set; }
public string StackTrace { get; set; }
}

Просмотреть файл

@ -0,0 +1,10 @@
namespace Microsoft.ComponentDetection.Common.Telemetry.Records;
public class PipReportFailureTelemetryRecord : BaseDetectionTelemetryRecord
{
public override string RecordName => "PipReportFailure";
public int ExitCode { get; set; }
public string StdErr { get; set; }
}

Просмотреть файл

@ -0,0 +1,10 @@
namespace Microsoft.ComponentDetection.Common.Telemetry.Records;
public class PipReportVersionTelemetryRecord : BaseDetectionTelemetryRecord
{
public override string RecordName => "PipReportVersion";
public string Version { get; set; }
public string MaxVersion { get; set; }
}

Просмотреть файл

@ -1,6 +1,7 @@
namespace Microsoft.ComponentDetection.Contracts;
namespace Microsoft.ComponentDetection.Contracts;
using System.IO;
using System.Threading.Tasks;
/// <summary>
/// Wraps some common file operations for easier testability. This interface is *only used by the command line driven app*.
@ -21,6 +22,13 @@ public interface IFileUtilityService
/// <returns>Returns a string of the file contents.</returns>
string ReadAllText(FileInfo file);
/// <summary>
/// Returns the contents of the file.
/// </summary>
/// <param name="file">File to read.</param>
/// <returns>Returns a string of the file contents.</returns>
Task<string> ReadAllTextAsync(FileInfo file);
/// <summary>
/// Returns true if the file exists.
/// </summary>

Просмотреть файл

@ -1,4 +1,4 @@
namespace Microsoft.ComponentDetection.Contracts;
namespace Microsoft.ComponentDetection.Contracts;
/// <summary>
/// Wraps some common folder operations, shared across command line app and service.
@ -34,4 +34,11 @@ public interface IPathUtilityService
/// <param name="fileName">File name without directory.</param>
/// <returns>Returns true if file name matches a pattern, otherwise false.</returns>
bool MatchesPattern(string searchPattern, string fileName);
/// <summary>
/// Normalizes the path to the system based separator.
/// </summary>
/// <param name="path">the path.</param>
/// <returns>normalized path.</returns>
string NormalizePath(string path);
}

Просмотреть файл

@ -0,0 +1,30 @@
namespace Microsoft.ComponentDetection.Detectors.Pip;
using System;
using System.IO;
using System.Threading.Tasks;
public interface IPipCommandService
{
/// <summary>
/// Checks the existence of pip.
/// </summary>
/// <param name="pipPath">Optional override of the pip.exe absolute path.</param>
/// <returns>True if pip is found on the OS PATH.</returns>
Task<bool> PipExistsAsync(string pipPath = null);
/// <summary>
/// Retrieves the version of pip from the given path. PythonVersion allows for loose version strings such as "1".
/// </summary>
/// <param name="pipPath">Optional override of the pip.exe absolute path.</param>
/// <returns>Version of pip.</returns>
Task<Version> GetPipVersionAsync(string pipPath = null);
/// <summary>
/// Generates a pip installation report for a given setup.py or requirements.txt file.
/// </summary>
/// <param name="path">Path of the Python requirements file.</param>
/// <param name="pipExePath">Optional override of the pip.exe absolute path.</param>
/// <returns>See https://pip.pypa.io/en/stable/reference/installation-report/#specification.</returns>
Task<(PipInstallationReport Report, FileInfo ReportFile)> GenerateInstallationReportAsync(string path, string pipExePath = null);
}

Просмотреть файл

@ -0,0 +1,79 @@
namespace Microsoft.ComponentDetection.Detectors.Pip;
using Newtonsoft.Json;
/// <summary>
/// Metadata for a pip component being installed. See https://packaging.python.org/en/latest/specifications/core-metadata/.
/// Some fields are not collected here because they are not needed for dependency graph construction.
/// </summary>
public sealed record PipInstallationMetadata
{
/// <summary>
/// Version of the file format; legal values are "1.0", "1.1", "1.2", "2.1", "2.2", and "2.3"
/// as of May 2024.
/// </summary>
[JsonProperty("metadata_version")]
public string MetadataVersion { get; set; }
/// <summary>
/// The name of the distribution.
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// A string containing the distributions version number.
/// </summary>
[JsonProperty("version")]
public string Version { get; set; }
/// <summary>
/// Each entry contains a string naming some other distutils project required by this distribution.
/// See https://peps.python.org/pep-0508/ for the format of the strings.
/// </summary>
[JsonProperty("requires_dist")]
public string[] RequiresDist { get; set; }
/// <summary>
/// URL for the distributions home page.
/// </summary>
[JsonProperty("home_page")]
public string HomePage { get; set; }
/// <summary>
/// Maintainers name at a minimum; additional contact information may be provided.
/// </summary>
[JsonProperty("maintainer")]
public string Maintainer { get; set; }
/// <summary>
/// Maintainers e-mail address. It can contain a name and e-mail address in the legal forms for a RFC-822 From: header.
/// </summary>
[JsonProperty("maintainer_email")]
public string MaintainerEmail { get; set; }
/// <summary>
/// Authors name at a minimum; additional contact information may be provided.
/// </summary>
[JsonProperty("author")]
public string Author { get; set; }
/// <summary>
/// Authors e-mail address. It can contain a name and e-mail address in the legal forms for a RFC-822 From: header.
/// </summary>
[JsonProperty("author_email")]
public string AuthorEmail { get; set; }
/// <summary>
/// Text indicating the license covering the distribution.
/// </summary>
[JsonProperty("license")]
public string License { get; set; }
/// <summary>
/// Each entry is a string giving a single classification value for the distribution.
/// Classifiers are described in PEP 301 https://peps.python.org/pep-0301/.
/// </summary>
[JsonProperty("classifier")]
public string[] Classifier { get; set; }
}

Просмотреть файл

@ -0,0 +1,34 @@
namespace Microsoft.ComponentDetection.Detectors.Pip;
using System.Collections.Generic;
using Newtonsoft.Json;
/// <summary>
/// See https://pip.pypa.io/en/stable/reference/installation-report/#specification.
/// </summary>
public sealed record PipInstallationReport
{
/// <summary>
/// Version of the installation report specification. Currently 1, but will be incremented if the format changes.
/// </summary>
[JsonProperty("version")]
public string Version { get; set; }
/// <summary>
/// Version of pip used to produce the report.
/// </summary>
[JsonProperty("pip_version")]
public string PipVersion { get; set; }
/// <summary>
/// Distribution packages (to be) installed.
/// </summary>
[JsonProperty("install")]
public PipInstallationReportItem[] InstallItems { get; set; }
/// <summary>
/// Environment metadata for the report. See https://peps.python.org/pep-0508/#environment-markers.
/// </summary>
[JsonProperty("environment")]
public IDictionary<string, string> Environment { get; set; }
}

Просмотреть файл

@ -0,0 +1,45 @@
namespace Microsoft.ComponentDetection.Detectors.Pip;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
public sealed record PipInstallationReportItem
{
/// <summary>
/// The metadata of the distribution.
/// </summary>
[JsonProperty("metadata")]
public PipInstallationMetadata Metadata { get; set; }
/// <summary>
/// true if the requirement was provided as, or constrained to, a direct URL reference. false if the requirements was provided as a name and version specifier.
/// </summary>
[JsonProperty("is_direct")]
public bool IsDirect { get; set; }
/// <summary>
/// true if the requirement was yanked from the index, but was still selected by pip conform.
/// </summary>
[JsonProperty("is_yanked")]
public bool IsYanked { get; set; }
/// <summary>
/// true if the requirement was explicitly provided by the user, either directly via
/// a command line argument or indirectly via a requirements file. false if the requirement
/// was installed as a dependency of another requirement.
/// </summary>
[JsonProperty("requested")]
public bool Requested { get; set; }
/// <summary>
/// See https://packaging.python.org/en/latest/specifications/direct-url-data-structure/.
/// </summary>
[JsonProperty("download_info")]
public JObject DownloadInfo { get; set; }
/// <summary>
/// Extras requested by the user.
/// </summary>
[JsonProperty("requested_extras")]
public JObject RequestedExtras { get; set; }
}

Просмотреть файл

@ -0,0 +1,25 @@
namespace Microsoft.ComponentDetection.Detectors.Pip;
using System.Collections.Generic;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
/// <summary>
/// Internal state used by PipReportDetector to hold intermediate structure info until the final
/// combination of dependencies and relationships is determined and can be returned.
/// </summary>
public sealed record PipReportGraphNode
{
public PipReportGraphNode(PipComponent value, bool requested)
{
this.Value = value;
this.Requested = requested;
}
public PipComponent Value { get; set; }
public List<PipReportGraphNode> Children { get; } = new List<PipReportGraphNode>();
public List<PipReportGraphNode> Parents { get; } = new List<PipReportGraphNode>();
public bool Requested { get; set; }
}

Просмотреть файл

@ -0,0 +1,152 @@
namespace Microsoft.ComponentDetection.Detectors.Pip;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.ComponentDetection.Common.Telemetry.Records;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
public class PipCommandService : IPipCommandService
{
private readonly ICommandLineInvocationService commandLineInvocationService;
private readonly IPathUtilityService pathUtilityService;
private readonly IFileUtilityService fileUtilityService;
private readonly IEnvironmentVariableService environmentService;
private readonly ILogger<PipCommandService> logger;
public PipCommandService()
{
}
public PipCommandService(
ICommandLineInvocationService commandLineInvocationService,
IPathUtilityService pathUtilityService,
IFileUtilityService fileUtilityService,
IEnvironmentVariableService environmentService,
ILogger<PipCommandService> logger)
{
this.commandLineInvocationService = commandLineInvocationService;
this.pathUtilityService = pathUtilityService;
this.fileUtilityService = fileUtilityService;
this.environmentService = environmentService;
this.logger = logger;
}
public async Task<bool> PipExistsAsync(string pipPath = null)
{
return !string.IsNullOrEmpty(await this.ResolvePipAsync(pipPath));
}
public async Task<Version> GetPipVersionAsync(string pipPath = null)
{
var pipExecutable = await this.ResolvePipAsync(pipPath);
var command = await this.commandLineInvocationService.ExecuteCommandAsync(
pipExecutable,
null,
"--version");
if (command.ExitCode != 0)
{
this.logger.LogDebug("Failed to execute pip version with StdErr {StdErr}.", command.StdErr);
return null;
}
try
{
// stdout will be in the format of "pip 20.0.2 from c:\python\lib\site-packages\pip (python 3.8)"
var versionString = command.StdOut.Split(' ')[1];
return Version.Parse(versionString);
}
catch (Exception)
{
this.logger.LogDebug("Failed to parse pip version with StdErr {StdErr}.", command.StdErr);
return null;
}
}
private async Task<string> ResolvePipAsync(string pipPath = null)
{
var pipCommand = string.IsNullOrEmpty(pipPath) ? "pip" : pipPath;
if (await this.CanCommandBeLocatedAsync(pipCommand))
{
return pipCommand;
}
return null;
}
private async Task<bool> CanCommandBeLocatedAsync(string pipPath)
{
return await this.commandLineInvocationService.CanCommandBeLocatedAsync(pipPath, new List<string> { "pip3" }, "--version");
}
public async Task<(PipInstallationReport Report, FileInfo ReportFile)> GenerateInstallationReportAsync(string path, string pipExePath = null)
{
if (string.IsNullOrEmpty(path))
{
return (new PipInstallationReport(), null);
}
var pipExecutable = await this.ResolvePipAsync(pipExePath);
var formattedPath = this.pathUtilityService.NormalizePath(path);
var workingDir = new DirectoryInfo(this.pathUtilityService.GetParentDirectory(formattedPath));
CommandLineExecutionResult command;
var reportName = Path.GetRandomFileName();
var reportFile = new FileInfo(Path.Combine(workingDir.FullName, reportName));
string pipReportCommand;
if (path.EndsWith(".py"))
{
pipReportCommand = $"install -e .";
}
else if (path.EndsWith(".txt"))
{
pipReportCommand = "install -r requirements.txt";
}
else
{
// Failure case, but this shouldn't be hit since detection is only running
// on setup.py and requirements.txt files.
this.logger.LogDebug("PipReport: Unsupported file type for pip installation report: {Path}", path);
return (new PipInstallationReport(), null);
}
// When PIP_INDEX_URL is set, we need to pass it as a parameter to pip install command.
// This should be done before running detection by the build system, otherwise the detection
// will default to the public PyPI index if not configured in pip defaults.
pipReportCommand += $" --dry-run --ignore-installed --quiet --report {reportName}";
if (this.environmentService.DoesEnvironmentVariableExist("PIP_INDEX_URL"))
{
pipReportCommand += $" --index-url {this.environmentService.GetEnvironmentVariable("PIP_INDEX_URL")}";
}
this.logger.LogDebug("PipReport: Generating pip installation report for {Path} with command: {Command}", formattedPath, pipReportCommand);
command = await this.commandLineInvocationService.ExecuteCommandAsync(
pipExecutable,
null,
workingDir,
pipReportCommand);
if (command.ExitCode != 0)
{
this.logger.LogWarning("PipReport: Failed to generate pip installation report for file {Path} with exit code {ExitCode}", path, command.ExitCode);
this.logger.LogDebug("PipReport: Pip installation report error: {StdErr}", command.StdErr);
using var failureRecord = new PipReportFailureTelemetryRecord
{
ExitCode = command.ExitCode,
StdErr = command.StdErr,
};
return (new PipInstallationReport(), null);
}
var reportOutput = await this.fileUtilityService.ReadAllTextAsync(reportFile);
return (JsonConvert.DeserializeObject<PipInstallationReport>(reportOutput), reportFile);
}
}

Просмотреть файл

@ -0,0 +1,262 @@
namespace Microsoft.ComponentDetection.Detectors.Pip;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Microsoft.ComponentDetection.Common.Telemetry.Records;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.Internal;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Microsoft.Extensions.Logging;
public class PipReportComponentDetector : FileComponentDetector, IExperimentalDetector
{
/// <summary>
/// The maximum version of the report specification that this detector can handle.
/// </summary>
private static readonly Version MaxReportVersion = new(1, 0, 0);
/// <summary>
/// The minimum version of the pip utility that this detector can handle.
/// </summary>
private static readonly Version MinimumPipVersion = new(22, 2, 0);
private readonly IPipCommandService pipCommandService;
public PipReportComponentDetector(
IComponentStreamEnumerableFactory componentStreamEnumerableFactory,
IObservableDirectoryWalkerFactory walkerFactory,
IPipCommandService pipCommandService,
ILogger<PipReportComponentDetector> logger)
{
this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory;
this.Scanner = walkerFactory;
this.pipCommandService = pipCommandService;
this.Logger = logger;
}
public override string Id => "PipReport";
public override IList<string> SearchPatterns => new List<string> { "setup.py", "requirements.txt" };
public override IEnumerable<string> Categories => new List<string> { "Python" };
public override IEnumerable<ComponentType> SupportedComponentTypes { get; } = new[] { ComponentType.Pip };
public override int Version { get; } = 1;
protected override async Task<IObservable<ProcessRequest>> OnPrepareDetectionAsync(IObservable<ProcessRequest> processRequests, IDictionary<string, string> detectorArgs)
{
this.CurrentScanRequest.DetectorArgs.TryGetValue("Pip.PipExePath", out var pipExePath);
if (!await this.pipCommandService.PipExistsAsync(pipExePath))
{
this.Logger.LogInformation($"PipReport: No pip found on system. Pip installation report detection will not run.");
return Enumerable.Empty<ProcessRequest>().ToObservable();
}
var pipVersion = await this.pipCommandService.GetPipVersionAsync(pipExePath);
if (pipVersion is null || pipVersion < MinimumPipVersion)
{
this.Logger.LogInformation(
"PipReport: No valid pip version found on system. {MinimumPipVersion} or greater is required. Pip installation report detection will not run.",
MinimumPipVersion);
return Enumerable.Empty<ProcessRequest>().ToObservable();
}
return processRequests;
}
protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary<string, string> detectorArgs)
{
this.CurrentScanRequest.DetectorArgs.TryGetValue("Pip.PipExePath", out var pipExePath);
var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
var file = processRequest.ComponentStream;
FileInfo reportFile = null;
try
{
var stopwatch = Stopwatch.StartNew();
this.Logger.LogInformation("PipReport: Generating pip installation report for {File}", file.Location);
// Call pip executable to generate the installation report of a given project file.
(var report, reportFile) = await this.pipCommandService.GenerateInstallationReportAsync(file.Location, pipExePath);
// The report version is used to determine how to parse the report. If it is greater
// than the maximum supported version, there may be new fields and the parsing will fail.
if (!int.TryParse(report.Version, out var reportVersion) || reportVersion > MaxReportVersion.Major)
{
this.Logger.LogWarning(
"PipReport: The pip installation report version {ReportVersion} is not supported. The maximum supported version is {MaxVersion}.",
report.Version,
MaxReportVersion);
using var versionRecord = new PipReportVersionTelemetryRecord
{
Version = report.Version,
MaxVersion = MaxReportVersion.ToString(),
};
return;
}
stopwatch.Stop();
this.Logger.LogInformation(
"PipReport: Generating pip installation report for {File} completed in {TotalSeconds} seconds with {PkgCount} detected packages.",
file.Location,
stopwatch.ElapsedMilliseconds / 1000.0,
report.InstallItems?.Length ?? 0);
// Now that all installed packages are known, we can build a graph of the dependencies.
if (report.InstallItems is not null)
{
var graph = this.BuildGraphFromInstallationReport(report);
this.RecordComponents(singleFileComponentRecorder, graph);
}
}
catch (Exception e)
{
this.Logger.LogError(e, "PipReport: Failure while parsing pip installation report for {File}", file.Location);
using var parseFailedRecord = new FailedParsingFileRecord
{
DetectorId = this.Id,
FilePath = file.Location,
ExceptionMessage = e.Message,
StackTrace = e.StackTrace,
};
throw;
}
finally
{
// Clean up the report output JSON file so it isn't left on the machine.
if (reportFile is not null && reportFile.Exists)
{
reportFile.Delete();
}
}
}
private Dictionary<string, PipReportGraphNode> BuildGraphFromInstallationReport(PipInstallationReport report)
{
// The installation report contains a list of all installed packages, including their dependencies.
// However, dependencies do not have explicitly marked root packages so we will need to build the
// graph ourselves using the requires_dist field.
var dependenciesByPkg = new Dictionary<string, List<PipDependencySpecification>>(StringComparer.OrdinalIgnoreCase);
var nodeReferences = new Dictionary<string, PipReportGraphNode>(StringComparer.OrdinalIgnoreCase);
foreach (var package in report.InstallItems)
{
// Normalize the package name to ensure consistency between the package name and the graph nodes.
var normalizedPkgName = PipReportUtilities.NormalizePackageNameFormat(package.Metadata.Name);
var node = new PipReportGraphNode(
new PipComponent(
normalizedPkgName,
package.Metadata.Version,
author: PipReportUtilities.GetSupplierFromInstalledItem(package),
license: PipReportUtilities.GetLicenseFromInstalledItem(package)),
package.Requested);
nodeReferences.Add(normalizedPkgName, node);
// requires_dist will contain information about the dependencies of the package.
// However, we don't have PipReportGraphNodes for all dependencies, so we will use
// an intermediate layer to store the relationships and update the graph later.
if (package.Metadata?.RequiresDist is null)
{
continue;
}
foreach (var dependency in package.Metadata.RequiresDist)
{
// Dependency strings can be in the form of:
// cffi (>=1.12)
// futures; python_version <= \"2.7\"
// sphinx (!=1.8.0,!=3.1.0,!=3.1.1,>=1.6.5) ; extra == 'docs'
var dependencySpec = new PipDependencySpecification($"Requires-Dist: {dependency}", requiresDist: true);
if (dependencySpec.PackageIsUnsafe())
{
continue;
}
if (dependenciesByPkg.ContainsKey(normalizedPkgName))
{
dependenciesByPkg[normalizedPkgName].Add(dependencySpec);
}
else
{
dependenciesByPkg.Add(normalizedPkgName, new List<PipDependencySpecification> { dependencySpec });
}
}
}
// Update the graph with their dependency relationships.
foreach (var dependency in dependenciesByPkg)
{
var rootNode = nodeReferences[dependency.Key];
// Update the "root" dependency.
foreach (var child in dependency.Value)
{
var normalizedChildName = PipReportUtilities.NormalizePackageNameFormat(child.Name);
if (!nodeReferences.ContainsKey(normalizedChildName))
{
// This dependency is not in the report, so we can't add it to the graph.
// Known potential causes: python_version/sys_platform specification.
continue;
}
var childNode = nodeReferences[normalizedChildName];
rootNode.Children.Add(childNode);
// Add the link to the parent dependency.
childNode.Parents.Add(rootNode);
}
}
return nodeReferences;
}
private void RecordComponents(
ISingleFileComponentRecorder recorder,
Dictionary<string, PipReportGraphNode> graph)
{
// Explicit root packages are marked with a requested flag.
// Parent components must be registered before their children.
foreach (var node in graph.Values)
{
var component = new DetectedComponent(node.Value);
recorder.RegisterUsage(
component,
isExplicitReferencedDependency: node.Requested);
}
// Once the graph has been populated with all dependencies, we can register the relationships.
// Ideally this would happen in the same loop as the previous one, but we need to ensure that
// parentComponentId is guaranteed to exist in the graph or an exception will be thrown.
foreach (var node in graph.Values)
{
if (!node.Parents.Any())
{
continue;
}
var component = new DetectedComponent(node.Value);
foreach (var parent in node.Parents)
{
recorder.RegisterUsage(
component,
isExplicitReferencedDependency: node.Requested,
parentComponentId: parent.Value.Id);
}
}
}
}

Просмотреть файл

@ -0,0 +1,71 @@
namespace Microsoft.ComponentDetection.Detectors.Pip;
using System.Linq;
using System.Text.RegularExpressions;
internal class PipReportUtilities
{
private const int MaxLicenseFieldLength = 100;
private const string ClassifierFieldSeparator = " :: ";
private const string ClassifierFieldLicensePrefix = "License";
/// <summary>
/// Normalize the package name format to the standard Python Packaging format.
/// See https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization.
/// </summary>
/// <returns>
/// The name lowercased with all runs of the characters ., -, or _
/// replaced with a single - character.
/// </returns>
#pragma warning disable CA1308 // Format requires lowercase.
public static string NormalizePackageNameFormat(string packageName) =>
Regex.Replace(packageName, @"[-_.]+", "-").ToLowerInvariant();
#pragma warning restore CA1308
public static string GetSupplierFromInstalledItem(PipInstallationReportItem component)
{
if (!string.IsNullOrWhiteSpace(component.Metadata?.Maintainer))
{
return component.Metadata.Maintainer;
}
if (!string.IsNullOrWhiteSpace(component.Metadata?.MaintainerEmail))
{
return component.Metadata.MaintainerEmail;
}
if (!string.IsNullOrWhiteSpace(component.Metadata?.Author))
{
return component.Metadata.Author;
}
if (!string.IsNullOrWhiteSpace(component.Metadata?.AuthorEmail))
{
return component.Metadata.AuthorEmail;
}
// If none of the fields are populated, return null.
return null;
}
public static string GetLicenseFromInstalledItem(PipInstallationReportItem component)
{
// There are cases where the actual license text is found in the license field so we limit the length of this field to 100 characters.
if (component.Metadata?.License is not null && component.Metadata?.License.Length < MaxLicenseFieldLength)
{
return component.Metadata.License;
}
if (component.Metadata?.Classifier is not null)
{
var licenseClassifiers = component.Metadata.Classifier.Where(x => !string.IsNullOrWhiteSpace(x) && x.StartsWith(ClassifierFieldLicensePrefix));
// Split the license classifiers by the " :: " and take the last part of the string
licenseClassifiers = licenseClassifiers.Select(x => x.Split(ClassifierFieldSeparator).Last()).ToList();
return string.Join(", ", licenseClassifiers);
}
return null;
}
}

Просмотреть файл

@ -8,17 +8,27 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Microsoft.Extensions.Logging;
public class PythonCommandService : IPythonCommandService
{
private readonly ICommandLineInvocationService commandLineInvocationService;
private readonly IPathUtilityService pathUtilityService;
private readonly ILogger<PythonCommandService> logger;
public PythonCommandService()
{
}
public PythonCommandService(ICommandLineInvocationService commandLineInvocationService) =>
public PythonCommandService(
ICommandLineInvocationService commandLineInvocationService,
IPathUtilityService pathUtilityService,
ILogger<PythonCommandService> logger)
{
this.commandLineInvocationService = commandLineInvocationService;
this.pathUtilityService = pathUtilityService;
this.logger = logger;
}
public async Task<bool> PythonExistsAsync(string pythonPath = null)
{
@ -57,12 +67,20 @@ public class PythonCommandService : IPythonCommandService
throw new PythonNotFoundException();
}
var formattedFilePath = this.pathUtilityService.NormalizePath(filePath);
var workingDir = this.pathUtilityService.GetParentDirectory(formattedFilePath);
// This calls out to python and prints out an array like: [ packageA, packageB, packageC ]
// We need to have python interpret this file because install_requires can be composed at runtime
var command = await this.commandLineInvocationService.ExecuteCommandAsync(pythonExecutable, null, $"-c \"import distutils.core; setup=distutils.core.run_setup('{filePath.Replace('\\', '/')}'); print(setup.install_requires)\"");
var command = await this.commandLineInvocationService.ExecuteCommandAsync(
pythonExecutable,
null,
new DirectoryInfo(workingDir),
$"-c \"import distutils.core; setup=distutils.core.run_setup('{formattedFilePath}'); print(setup.install_requires)\"");
if (command.ExitCode != 0)
{
this.logger.LogDebug("Python: Failed distutils setup with error: {StdErr}", command.StdErr);
return new List<string>();
}

Просмотреть файл

@ -0,0 +1,18 @@
namespace Microsoft.ComponentDetection.Orchestrator.Experiments.Configs;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Detectors.Pip;
/// <summary>
/// Validating the <see cref="PipReportComponentDetector"/>.
/// </summary>
public class PipReportExperiment : IExperimentConfiguration
{
public string Name => "PipReport";
public bool IsInControlGroup(IComponentDetector componentDetector) => componentDetector is PipComponentDetector;
public bool IsInExperimentGroup(IComponentDetector componentDetector) => componentDetector is PipReportComponentDetector;
public bool ShouldRecord(IComponentDetector componentDetector, int numComponents) => true;
}

Просмотреть файл

@ -65,6 +65,7 @@ public static class ServiceCollectionExtensions
services.AddSingleton<IExperimentConfiguration, VcpkgExperiment>();
services.AddSingleton<IExperimentConfiguration, GoDetectorReplaceExperiment>();
services.AddSingleton<IExperimentConfiguration, Pnpm6Experiment>();
services.AddSingleton<IExperimentConfiguration, PipReportExperiment>();
// Detectors
// CocoaPods
@ -116,6 +117,8 @@ public static class ServiceCollectionExtensions
services.AddSingleton<ISimplePythonResolver, SimplePythonResolver>();
services.AddSingleton<IComponentDetector, PipComponentDetector>();
services.AddSingleton<IComponentDetector, SimplePipComponentDetector>();
services.AddSingleton<IPipCommandService, PipCommandService>();
services.AddSingleton<IComponentDetector, PipReportComponentDetector>();
// pnpm
services.AddSingleton<IComponentDetector, PnpmComponentDetector>();

Просмотреть файл

@ -0,0 +1,29 @@
namespace Microsoft.ComponentDetection.Common.Tests;
using System.IO;
using FluentAssertions;
using Microsoft.ComponentDetection.Common;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
[TestCategory("Governance/All")]
[TestCategory("Governance/ComponentDetection")]
public class PathUtilityServiceTests
{
[TestMethod]
public void PathShouldBeNormalized()
{
var service = new PathUtilityService(new NullLogger<PathUtilityService>());
var path = "Users\\SomeUser\\someDir\\someFile";
var normalizedPath = service.NormalizePath(path);
if (Path.DirectorySeparatorChar == '\\')
{
normalizedPath.Should().Be(path);
}
else
{
normalizedPath.Should().Be(string.Join(Path.DirectorySeparatorChar, path.Split('\\')));
}
}
}

Просмотреть файл

@ -11,6 +11,7 @@
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="NuGet.Versioning" />
<PackageReference Include="SemanticVersioning" />
<PackageReference Include="System.Reactive" />
<PackageReference Include="System.Threading.Tasks.Dataflow" />
<PackageReference Include="packageurl-dotnet" />
@ -33,6 +34,18 @@
<None Update="Mocks\MvnCliDependencyOutput.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Mocks\pip_report_multi_pkg.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Mocks\pip_report_jupyterlab.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Mocks\pip_report_single_pkg.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Mocks\pip_report_single_pkg_bad_version.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

Просмотреть файл

@ -1,183 +1,266 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Microsoft.ComponentDetection.Detectors.Tests.Mocks {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class TestResources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal TestResources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.ComponentDetection.Detectors.Tests.Mocks.TestResources", typeof(TestResources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to integrationTestCompileClasspath - Compile classpath for source set &apos;integration test&apos;.
///+--- commons-io:commons-io:2.5
///+--- org.kohsuke:github-api:1.94
///| +--- org.apache.commons:commons-lang3:3.7
///| +--- commons-codec:commons-codec:1.7
///| +--- com.fasterxml.jackson.core:jackson-databind:2.9.2
///| | +--- com.fasterxml.jackson.core:jackson-annotations:2.9.0
///| | \--- com.fasterxml.jackson.core:jackson-core:2.9.2
///| \--- commons-io:commons-io:1.4 -&gt; 2.5
///+--- org.zeroturnaround:zt-zip: [rest of string was truncated]&quot;;.
/// </summary>
internal static string GradlewDependencyOutput {
get {
return ResourceManager.GetString("GradlewDependencyOutput", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to org.apache.maven:maven-compat:jar:3.6.1-SNAPSHOT
///+- org.apache.maven:maven-model:jar:3.6.1-SNAPSHOT:compile
///+- org.apache.maven:maven-model-builder:jar:3.6.1-SNAPSHOT:compile
///| \- org.apache.maven:maven-builder-support:jar:3.6.1-SNAPSHOT:compile
///+- org.apache.maven:maven-settings:jar:3.6.1-SNAPSHOT:compile
///+- org.apache.maven:maven-settings-builder:jar:3.6.1-SNAPSHOT:compile
///| \- org.sonatype.plexus:plexus-sec-dispatcher:jar:1.4:compile
///| \- org.sonatype.plexus:plexus-cipher:jar:1.7:compile
///+- [rest of string was truncated]&quot;;.
/// </summary>
internal static string MvnCliDependencyOutput {
get {
return ResourceManager.GetString("MvnCliDependencyOutput", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {
/// &quot;version&quot;: 3,
/// &quot;targets&quot;: {
/// &quot;.NETCoreApp,Version=v2.2&quot;: {
/// &quot;CommandLineParser/2.8.0&quot;: {
/// &quot;type&quot;: &quot;package&quot;,
/// &quot;compile&quot;: {
/// &quot;lib/netstandard2.0/_._&quot;: {}
/// },
/// &quot;runtime&quot;: {
/// &quot;lib/netstandard2.0/CommandLine.dll&quot;: {}
/// }
/// },
/// &quot;coverlet.msbuild/2.5.1&quot;: {
/// &quot;type&quot;: &quot;package&quot;,
/// &quot;build&quot;: {
/// &quot;build/netstandard2.0/coverlet.msbuild.props&quot;: {},
/// &quot;build/netstandard2.0/coverlet.msbuild.targets&quot;: {}
/// }
/// },
/// &quot;DotNet.Glob/2.1.1&quot;: {
/// &quot;type&quot;: &quot;package [rest of string was truncated]&quot;;.
/// </summary>
internal static string project_assets_2_2 {
get {
return ResourceManager.GetString("project_assets_2_2", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {
/// &quot;version&quot;: 3,
/// &quot;targets&quot;: {
/// &quot;.NETCoreApp,Version=v2.2&quot;: {
/// &quot;coverlet.msbuild/2.5.1&quot;: {
/// &quot;type&quot;: &quot;package&quot;,
/// &quot;build&quot;: {
/// &quot;build/netstandard2.0/coverlet.msbuild.props&quot;: {},
/// &quot;build/netstandard2.0/coverlet.msbuild.targets&quot;: {}
/// }
/// },
/// &quot;DotNet.Glob/2.1.1&quot;: {
/// &quot;type&quot;: &quot;package&quot;,
/// &quot;dependencies&quot;: {
/// &quot;NETStandard.Library&quot;: &quot;1.6.1&quot;
/// },
/// &quot;compile&quot;: {
/// &quot;lib/netstandard1.1/DotNet.Glob.dll&quot;: {}
/// },
/// &quot;runtime&quot;: {
/// &quot;lib/netstandard1.1/DotNet.Glob.dll&quot;: {}
/// [rest of string was truncated]&quot;;.
/// </summary>
internal static string project_assets_2_2_additional {
get {
return ResourceManager.GetString("project_assets_2_2_additional", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {
/// &quot;version&quot;: 3,
/// &quot;targets&quot;: {
/// &quot;.NETCoreApp,Version=v3.1&quot;: {
/// &quot;Microsoft.Extensions.DependencyModel/3.0.0&quot;: {
/// &quot;type&quot;: &quot;package&quot;,
/// &quot;dependencies&quot;: {
/// &quot;System.Text.Json&quot;: &quot;4.6.0&quot;
/// },
/// &quot;compile&quot;: {
/// &quot;lib/netstandard2.0/Microsoft.Extensions.DependencyModel.dll&quot;: {}
/// },
/// &quot;runtime&quot;: {
/// &quot;lib/netstandard2.0/Microsoft.Extensions.DependencyModel.dll&quot;: {}
/// }
/// },
/// &quot;Microsoft. [rest of string was truncated]&quot;;.
/// </summary>
internal static string project_assets_3_1 {
get {
return ResourceManager.GetString("project_assets_3_1", resourceCulture);
}
}
}
}
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Microsoft.ComponentDetection.Detectors.Tests.Mocks {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class TestResources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal TestResources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.ComponentDetection.Detectors.Tests.Mocks.TestResources", typeof(TestResources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to integrationTestCompileClasspath - Compile classpath for source set &apos;integration test&apos;.
///+--- commons-io:commons-io:2.5
///+--- org.kohsuke:github-api:1.94
///| +--- org.apache.commons:commons-lang3:3.7
///| +--- commons-codec:commons-codec:1.7
///| +--- com.fasterxml.jackson.core:jackson-databind:2.9.2
///| | +--- com.fasterxml.jackson.core:jackson-annotations:2.9.0
///| | \--- com.fasterxml.jackson.core:jackson-core:2.9.2
///| \--- commons-io:commons-io:1.4 -&gt; 2.5
///+--- org.zeroturnaround:zt-zip: [rest of string was truncated]&quot;;.
/// </summary>
internal static string GradlewDependencyOutput {
get {
return ResourceManager.GetString("GradlewDependencyOutput", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to org.apache.maven:maven-compat:jar:3.6.1-SNAPSHOT
///+- org.apache.maven:maven-model:jar:3.6.1-SNAPSHOT:compile
///+- org.apache.maven:maven-model-builder:jar:3.6.1-SNAPSHOT:compile
///| \- org.apache.maven:maven-builder-support:jar:3.6.1-SNAPSHOT:compile
///+- org.apache.maven:maven-settings:jar:3.6.1-SNAPSHOT:compile
///+- org.apache.maven:maven-settings-builder:jar:3.6.1-SNAPSHOT:compile
///| \- org.sonatype.plexus:plexus-sec-dispatcher:jar:1.4:compile
///| \- org.sonatype.plexus:plexus-cipher:jar:1.7:compile
///+- [rest of string was truncated]&quot;;.
/// </summary>
internal static string MvnCliDependencyOutput {
get {
return ResourceManager.GetString("MvnCliDependencyOutput", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {
/// &quot;version&quot;: &quot;1&quot;,
/// &quot;pip_version&quot;: &quot;24.0&quot;,
/// &quot;install&quot;: [
/// {
/// &quot;download_info&quot;: {
/// &quot;url&quot;: &quot;https://files.pythonhosted.org/packages/72/c3/532326adbb2b76f709e3e582aeefd0a85bd7454599ff450d90dd9540f5ed/jupyterlab-4.2.0-py3-none-any.whl&quot;,
/// &quot;archive_info&quot;: {
/// &quot;hash&quot;: &quot;sha256=0dfe9278e25a145362289c555d9beb505697d269c10e99909766af7c440ad3cc&quot;,
/// &quot;hashes&quot;: {
/// &quot;sha256&quot;: &quot;0dfe9278e25a145362289c555d9beb505697d269c10e99909766af7c440ad3cc&quot;
/// }
/// [rest of string was truncated]&quot;;.
/// </summary>
internal static string pip_report_jupyterlab {
get {
return ResourceManager.GetString("pip_report_jupyterlab", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {
/// &quot;version&quot;: &quot;1&quot;,
/// &quot;pip_version&quot;: &quot;24.0&quot;,
/// &quot;install&quot;: [
/// {
/// &quot;download_info&quot;: {
/// &quot;url&quot;: &quot;https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl&quot;,
/// &quot;archive_info&quot;: {
/// &quot;hash&quot;: &quot;sha256=8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254&quot;,
/// &quot;hashes&quot;: {
/// &quot;sha256&quot;: &quot;8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254&quot;
/// }
/// } /// [rest of string was truncated]&quot;;.
/// </summary>
internal static string pip_report_multi_pkg {
get {
return ResourceManager.GetString("pip_report_multi_pkg", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {
/// &quot;version&quot;: &quot;1&quot;,
/// &quot;pip_version&quot;: &quot;24.0&quot;,
/// &quot;install&quot;: [
/// {
/// &quot;download_info&quot;: {
/// &quot;url&quot;: &quot;https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl&quot;,
/// &quot;archive_info&quot;: {
/// &quot;hash&quot;: &quot;sha256=8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254&quot;,
/// &quot;hashes&quot;: {
/// &quot;sha256&quot;: &quot;8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254&quot;
/// }
/// } /// [rest of string was truncated]&quot;;.
/// </summary>
internal static string pip_report_single_pkg {
get {
return ResourceManager.GetString("pip_report_single_pkg", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {
/// &quot;version&quot;: &quot;2&quot;,
/// &quot;pip_version&quot;: &quot;24.0&quot;,
/// &quot;install&quot;: [
/// {
/// &quot;download_info&quot;: {
/// &quot;url&quot;: &quot;https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl&quot;,
/// &quot;archive_info&quot;: {
/// &quot;hash&quot;: &quot;sha256=8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254&quot;,
/// &quot;hashes&quot;: {
/// &quot;sha256&quot;: &quot;8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254&quot;
/// }
/// } /// [rest of string was truncated]&quot;;.
/// </summary>
internal static string pip_report_single_pkg_bad_version {
get {
return ResourceManager.GetString("pip_report_single_pkg_bad_version", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {
/// &quot;version&quot;: 3,
/// &quot;targets&quot;: {
/// &quot;.NETCoreApp,Version=v2.2&quot;: {
/// &quot;CommandLineParser/2.8.0&quot;: {
/// &quot;type&quot;: &quot;package&quot;,
/// &quot;compile&quot;: {
/// &quot;lib/netstandard2.0/_._&quot;: {}
/// },
/// &quot;runtime&quot;: {
/// &quot;lib/netstandard2.0/CommandLine.dll&quot;: {}
/// }
/// },
/// &quot;coverlet.msbuild/2.5.1&quot;: {
/// &quot;type&quot;: &quot;package&quot;,
/// &quot;build&quot;: {
/// &quot;build/netstandard2.0/coverlet.msbuild.props&quot;: {},
/// &quot;build/netstandard2.0/coverlet.msbuild.targets&quot;: {}
/// }
/// },
/// &quot;DotNet.Glob/2.1.1&quot;: {
/// &quot;type&quot;: &quot;package [rest of string was truncated]&quot;;.
/// </summary>
internal static string project_assets_2_2 {
get {
return ResourceManager.GetString("project_assets_2_2", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {
/// &quot;version&quot;: 3,
/// &quot;targets&quot;: {
/// &quot;.NETCoreApp,Version=v2.2&quot;: {
/// &quot;coverlet.msbuild/2.5.1&quot;: {
/// &quot;type&quot;: &quot;package&quot;,
/// &quot;build&quot;: {
/// &quot;build/netstandard2.0/coverlet.msbuild.props&quot;: {},
/// &quot;build/netstandard2.0/coverlet.msbuild.targets&quot;: {}
/// }
/// },
/// &quot;DotNet.Glob/2.1.1&quot;: {
/// &quot;type&quot;: &quot;package&quot;,
/// &quot;dependencies&quot;: {
/// &quot;NETStandard.Library&quot;: &quot;1.6.1&quot;
/// },
/// &quot;compile&quot;: {
/// &quot;lib/netstandard1.1/DotNet.Glob.dll&quot;: {}
/// },
/// &quot;runtime&quot;: {
/// &quot;lib/netstandard1.1/DotNet.Glob.dll&quot;: {} /// [rest of string was truncated]&quot;;.
/// </summary>
internal static string project_assets_2_2_additional {
get {
return ResourceManager.GetString("project_assets_2_2_additional", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {
/// &quot;version&quot;: 3,
/// &quot;targets&quot;: {
/// &quot;.NETCoreApp,Version=v3.1&quot;: {
/// &quot;Microsoft.Extensions.DependencyModel/3.0.0&quot;: {
/// &quot;type&quot;: &quot;package&quot;,
/// &quot;dependencies&quot;: {
/// &quot;System.Text.Json&quot;: &quot;4.6.0&quot;
/// },
/// &quot;compile&quot;: {
/// &quot;lib/netstandard2.0/Microsoft.Extensions.DependencyModel.dll&quot;: {}
/// },
/// &quot;runtime&quot;: {
/// &quot;lib/netstandard2.0/Microsoft.Extensions.DependencyModel.dll&quot;: {}
/// }
/// },
/// &quot;Microsoft. [rest of string was truncated]&quot;;.
/// </summary>
internal static string project_assets_3_1 {
get {
return ResourceManager.GetString("project_assets_3_1", resourceCulture);
}
}
}
}

Просмотреть файл

@ -124,6 +124,18 @@
<data name="MvnCliDependencyOutput" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>MvnCliDependencyOutput.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>
</data>
<data name="pip_report_jupyterlab" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>pip_report_jupyterlab.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="pip_report_multi_pkg" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>pip_report_multi_pkg.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="pip_report_single_pkg" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>pip_report_single_pkg.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="pip_report_single_pkg_bad_version" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>pip_report_single_pkg_bad_version.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="project_assets_2_2" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\project_assets_2_2.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252</value>
</data>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -0,0 +1,57 @@
{
"version": "1",
"pip_version": "24.0",
"install": [
{
"download_info": {
"url": "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl",
"archive_info": {
"hash": "sha256=8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254",
"hashes": {
"sha256": "8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
}
}
},
"is_direct": false,
"is_yanked": false,
"requested": true,
"metadata": {
"metadata_version": "2.1",
"name": "six",
"version": "1.16.0",
"platform": [
"UNKNOWN"
],
"summary": "Python 2 and 3 compatibility utilities",
"description": ".. image:: https://img.shields.io/pypi/v/six.svg\n :target: https://pypi.org/project/six/\n :alt: six on PyPI\n\n.. image:: https://travis-ci.org/benjaminp/six.svg?branch=master\n :target: https://travis-ci.org/benjaminp/six\n :alt: six on TravisCI\n\n.. image:: https://readthedocs.org/projects/six/badge/?version=latest\n :target: https://six.readthedocs.io/\n :alt: six's documentation on Read the Docs\n\n.. image:: https://img.shields.io/badge/license-MIT-green.svg\n :target: https://github.com/benjaminp/six/blob/master/LICENSE\n :alt: MIT License badge\n\nSix is a Python 2 and 3 compatibility library. It provides utility functions\nfor smoothing over the differences between the Python versions with the goal of\nwriting Python code that is compatible on both Python versions. See the\ndocumentation for more information on what is provided.\n\nSix supports Python 2.7 and 3.3+. It is contained in only one Python\nfile, so it can be easily copied into your project. (The copyright and license\nnotice must be retained.)\n\nOnline documentation is at https://six.readthedocs.io/.\n\nBugs can be reported to https://github.com/benjaminp/six. The code can also\nbe found there.\n\n\n",
"home_page": "https://github.com/benjaminp/six",
"author": "Benjamin Peterson",
"author_email": "benjamin@python.org",
"license": "MIT",
"classifier": [
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Topic :: Software Development :: Libraries",
"Topic :: Utilities"
],
"requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
}
}
],
"environment": {
"implementation_name": "cpython",
"implementation_version": "3.12.3",
"os_name": "nt",
"platform_machine": "AMD64",
"platform_release": "11",
"platform_system": "Windows",
"platform_version": "10.0.22631",
"python_full_version": "3.12.3",
"platform_python_implementation": "CPython",
"python_version": "3.12",
"sys_platform": "win32"
}
}

Просмотреть файл

@ -0,0 +1,57 @@
{
"version": "2",
"pip_version": "24.0",
"install": [
{
"download_info": {
"url": "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl",
"archive_info": {
"hash": "sha256=8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254",
"hashes": {
"sha256": "8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
}
}
},
"is_direct": false,
"is_yanked": false,
"requested": true,
"metadata": {
"metadata_version": "2.1",
"name": "six",
"version": "1.16.0",
"platform": [
"UNKNOWN"
],
"summary": "Python 2 and 3 compatibility utilities",
"description": ".. image:: https://img.shields.io/pypi/v/six.svg\n :target: https://pypi.org/project/six/\n :alt: six on PyPI\n\n.. image:: https://travis-ci.org/benjaminp/six.svg?branch=master\n :target: https://travis-ci.org/benjaminp/six\n :alt: six on TravisCI\n\n.. image:: https://readthedocs.org/projects/six/badge/?version=latest\n :target: https://six.readthedocs.io/\n :alt: six's documentation on Read the Docs\n\n.. image:: https://img.shields.io/badge/license-MIT-green.svg\n :target: https://github.com/benjaminp/six/blob/master/LICENSE\n :alt: MIT License badge\n\nSix is a Python 2 and 3 compatibility library. It provides utility functions\nfor smoothing over the differences between the Python versions with the goal of\nwriting Python code that is compatible on both Python versions. See the\ndocumentation for more information on what is provided.\n\nSix supports Python 2.7 and 3.3+. It is contained in only one Python\nfile, so it can be easily copied into your project. (The copyright and license\nnotice must be retained.)\n\nOnline documentation is at https://six.readthedocs.io/.\n\nBugs can be reported to https://github.com/benjaminp/six. The code can also\nbe found there.\n\n\n",
"home_page": "https://github.com/benjaminp/six",
"author": "Benjamin Peterson",
"author_email": "benjamin@python.org",
"license": "MIT",
"classifier": [
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Topic :: Software Development :: Libraries",
"Topic :: Utilities"
],
"requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
}
}
],
"environment": {
"implementation_name": "cpython",
"implementation_version": "3.12.3",
"os_name": "nt",
"platform_machine": "AMD64",
"platform_release": "11",
"platform_system": "Windows",
"platform_version": "10.0.22631",
"python_full_version": "3.12.3",
"platform_python_implementation": "CPython",
"python_version": "3.12",
"sys_platform": "win32"
}
}

Просмотреть файл

@ -0,0 +1,469 @@
namespace Microsoft.ComponentDetection.Detectors.Tests;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.ComponentDetection.Common;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Detectors.Pip;
using Microsoft.ComponentDetection.Detectors.Tests.Mocks;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
[TestClass]
[TestCategory("Governance/All")]
[TestCategory("Governance/ComponentDetection")]
public class PipCommandServiceTests
{
private readonly Mock<ICommandLineInvocationService> commandLineInvokationService;
private readonly Mock<IEnvironmentVariableService> envVarService;
private readonly Mock<IFileUtilityService> fileUtilityService;
private readonly Mock<ILogger<PathUtilityService>> pathLogger;
private readonly Mock<ILogger<PipCommandService>> logger;
private readonly IPathUtilityService pathUtilityService;
public PipCommandServiceTests()
{
this.commandLineInvokationService = new Mock<ICommandLineInvocationService>();
this.pathLogger = new Mock<ILogger<PathUtilityService>>();
this.logger = new Mock<ILogger<PipCommandService>>();
this.pathUtilityService = new PathUtilityService(this.pathLogger.Object);
this.envVarService = new Mock<IEnvironmentVariableService>();
this.fileUtilityService = new Mock<IFileUtilityService>();
}
[TestMethod]
public async Task PipCommandService_ReturnsTrueWhenPipExistsAsync()
{
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("pip", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(true);
var service = new PipCommandService(
this.commandLineInvokationService.Object,
this.pathUtilityService,
this.fileUtilityService.Object,
this.envVarService.Object,
this.logger.Object);
(await service.PipExistsAsync()).Should().BeTrue();
}
[TestMethod]
public async Task PipCommandService_ReturnsFalseWhenPipExistsAsync()
{
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("pip", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(false);
var service = new PipCommandService(
this.commandLineInvokationService.Object,
this.pathUtilityService,
this.fileUtilityService.Object,
this.envVarService.Object,
this.logger.Object);
(await service.PipExistsAsync()).Should().BeFalse();
}
[TestMethod]
public async Task PipCommandService_ReturnsTrueWhenPipExistsForAPathAsync()
{
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("testPath", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(true);
var service = new PipCommandService(
this.commandLineInvokationService.Object,
this.pathUtilityService,
this.fileUtilityService.Object,
this.envVarService.Object,
this.logger.Object);
(await service.PipExistsAsync("testPath")).Should().BeTrue();
}
[TestMethod]
public async Task PipCommandService_ReturnsFalseWhenPipExistsForAPathAsync()
{
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("testPath", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(false);
var service = new PipCommandService(
this.commandLineInvokationService.Object,
this.pathUtilityService,
this.fileUtilityService.Object,
this.envVarService.Object,
this.logger.Object);
(await service.PipExistsAsync("testPath")).Should().BeFalse();
}
[TestMethod]
public async Task PipCommandService_BadVersion_ReturnsNullAsync()
{
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync(
"pip",
It.IsAny<IEnumerable<string>>(),
"--version"))
.ReturnsAsync(true);
this.commandLineInvokationService.Setup(x => x.ExecuteCommandAsync(
"pip",
It.IsAny<IEnumerable<string>>(),
"--version"))
.ReturnsAsync(new CommandLineExecutionResult { ExitCode = 0, StdOut = string.Empty });
var service = new PipCommandService(
this.commandLineInvokationService.Object,
this.pathUtilityService,
this.fileUtilityService.Object,
this.envVarService.Object,
this.logger.Object);
var semVer = await service.GetPipVersionAsync();
semVer.Should().BeNull();
}
[TestMethod]
public async Task PipCommandService_BadVersionString_ReturnsNullAsync()
{
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync(
"pip",
It.IsAny<IEnumerable<string>>(),
"--version"))
.ReturnsAsync(true);
this.commandLineInvokationService.Setup(x => x.ExecuteCommandAsync(
"pip",
It.IsAny<IEnumerable<string>>(),
"--version"))
.ReturnsAsync(new CommandLineExecutionResult { ExitCode = 0, StdOut = "this is not a valid output" });
var service = new PipCommandService(
this.commandLineInvokationService.Object,
this.pathUtilityService,
this.fileUtilityService.Object,
this.envVarService.Object,
this.logger.Object);
var semVer = await service.GetPipVersionAsync();
semVer.Should().BeNull();
}
[TestMethod]
public async Task PipCommandService_ReturnsVersionAsync()
{
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync(
"pip",
It.IsAny<IEnumerable<string>>(),
"--version"))
.ReturnsAsync(true);
this.commandLineInvokationService.Setup(x => x.ExecuteCommandAsync(
"pip",
It.IsAny<IEnumerable<string>>(),
"--version"))
.ReturnsAsync(new CommandLineExecutionResult { ExitCode = 0, StdOut = "pip 20.0.2 from c:\\python\\lib\\site-packages\\pip (python 3.8)" });
var service = new PipCommandService(
this.commandLineInvokationService.Object,
this.pathUtilityService,
this.fileUtilityService.Object,
this.envVarService.Object,
this.logger.Object);
var semVer = await service.GetPipVersionAsync();
semVer.Major.Should().Be(20);
semVer.Minor.Should().Be(0);
semVer.Build.Should().Be(2);
}
[TestMethod]
public async Task PipCommandService_ReturnsVersion_SimpleAsync()
{
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync(
"pip",
It.IsAny<IEnumerable<string>>(),
"--version"))
.ReturnsAsync(true);
this.commandLineInvokationService.Setup(x => x.ExecuteCommandAsync(
"pip",
It.IsAny<IEnumerable<string>>(),
"--version"))
.ReturnsAsync(new CommandLineExecutionResult { ExitCode = 0, StdOut = "pip 24.0 from c:\\python\\lib\\site-packages\\pip (python 3.8)" });
var service = new PipCommandService(
this.commandLineInvokationService.Object,
this.pathUtilityService,
this.fileUtilityService.Object,
this.envVarService.Object,
this.logger.Object);
var semVer = await service.GetPipVersionAsync();
semVer.Major.Should().Be(24);
semVer.Minor.Should().Be(0);
}
[TestMethod]
public async Task PipCommandService_ReturnsVersionForAPathAsync()
{
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync(
"testPath",
It.IsAny<IEnumerable<string>>(),
"--version"))
.ReturnsAsync(true);
this.commandLineInvokationService.Setup(x => x.ExecuteCommandAsync(
"testPath",
It.IsAny<IEnumerable<string>>(),
"--version"))
.ReturnsAsync(new CommandLineExecutionResult { ExitCode = 0, StdOut = "pip 20.0.2 from c:\\python\\lib\\site-packages\\pip (python 3.8)" });
var service = new PipCommandService(
this.commandLineInvokationService.Object,
this.pathUtilityService,
this.fileUtilityService.Object,
this.envVarService.Object,
this.logger.Object);
var semVer = await service.GetPipVersionAsync("testPath");
semVer.Major.Should().Be(20);
semVer.Minor.Should().Be(0);
semVer.Build.Should().Be(2);
}
[TestMethod]
public async Task PipCommandService_GeneratesReport_RequirementsTxt_CorrectlyAsync()
{
var testPath = Path.Join(Directory.GetCurrentDirectory(), string.Join(Guid.NewGuid().ToString(), ".txt"));
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("pip", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(true);
var service = new PipCommandService(
this.commandLineInvokationService.Object,
this.pathUtilityService,
this.fileUtilityService.Object,
this.envVarService.Object,
this.logger.Object);
this.commandLineInvokationService.Setup(x => x.ExecuteCommandAsync(
"pip",
It.IsAny<IEnumerable<string>>(),
It.Is<DirectoryInfo>(d => d.FullName.Contains(Directory.GetCurrentDirectory(), StringComparison.OrdinalIgnoreCase)),
It.Is<string>(s => s.Contains("requirements.txt", StringComparison.OrdinalIgnoreCase))))
.ReturnsAsync(new CommandLineExecutionResult { ExitCode = 0, StdErr = string.Empty, StdOut = string.Empty })
.Verifiable();
this.fileUtilityService.Setup(x => x.ReadAllTextAsync(It.IsAny<FileInfo>()))
.ReturnsAsync(TestResources.pip_report_single_pkg);
var (report, reportFile) = await service.GenerateInstallationReportAsync(testPath);
// the file shouldn't exist since we're not writing to it in the test
reportFile.Should().NotBeNull();
reportFile.Exists.Should().Be(false);
// validate report parameters
report.Should().NotBeNull();
report.Version.Should().Be("1");
report.InstallItems.Should().NotBeNull();
report.InstallItems.Should().ContainSingle();
// validate packages
report.InstallItems[0].Requested.Should().BeTrue();
report.InstallItems[0].Metadata.Name.Should().Be("six");
report.InstallItems[0].Metadata.Version.Should().Be("1.16.0");
report.InstallItems[0].Metadata.License.Should().Be("MIT");
report.InstallItems[0].Metadata.Author.Should().Be("Benjamin Peterson");
report.InstallItems[0].Metadata.AuthorEmail.Should().Be("benjamin@python.org");
report.InstallItems[0].Metadata.Maintainer.Should().BeNullOrEmpty();
report.InstallItems[0].Metadata.MaintainerEmail.Should().BeNullOrEmpty();
this.commandLineInvokationService.Verify();
}
[TestMethod]
public async Task PipCommandService_GeneratesReport_SetupPy_CorrectlyAsync()
{
var testPath = Path.Join(Directory.GetCurrentDirectory(), string.Join(Guid.NewGuid().ToString(), ".py"));
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("pip", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(true);
var service = new PipCommandService(
this.commandLineInvokationService.Object,
this.pathUtilityService,
this.fileUtilityService.Object,
this.envVarService.Object,
this.logger.Object);
this.commandLineInvokationService.Setup(x => x.ExecuteCommandAsync(
"pip",
It.IsAny<IEnumerable<string>>(),
It.Is<DirectoryInfo>(d => d.FullName.Contains(Directory.GetCurrentDirectory(), StringComparison.OrdinalIgnoreCase)),
It.Is<string>(s => s.Contains("-e .", StringComparison.OrdinalIgnoreCase))))
.ReturnsAsync(new CommandLineExecutionResult { ExitCode = 0, StdErr = string.Empty, StdOut = string.Empty })
.Verifiable();
this.fileUtilityService.Setup(x => x.ReadAllTextAsync(It.IsAny<FileInfo>()))
.ReturnsAsync(TestResources.pip_report_single_pkg);
var (report, reportFile) = await service.GenerateInstallationReportAsync(testPath);
// the file shouldn't exist since we're not writing to it in the test
reportFile.Should().NotBeNull();
reportFile.Exists.Should().Be(false);
// validate report parameters
report.Should().NotBeNull();
report.Version.Should().Be("1");
report.InstallItems.Should().NotBeNull();
report.InstallItems.Should().ContainSingle();
// validate packages
report.InstallItems[0].Requested.Should().BeTrue();
report.InstallItems[0].Metadata.Name.Should().Be("six");
report.InstallItems[0].Metadata.Version.Should().Be("1.16.0");
report.InstallItems[0].Metadata.License.Should().Be("MIT");
report.InstallItems[0].Metadata.Author.Should().Be("Benjamin Peterson");
report.InstallItems[0].Metadata.AuthorEmail.Should().Be("benjamin@python.org");
report.InstallItems[0].Metadata.Maintainer.Should().BeNullOrEmpty();
report.InstallItems[0].Metadata.MaintainerEmail.Should().BeNullOrEmpty();
this.commandLineInvokationService.Verify();
}
[TestMethod]
public async Task PipCommandService_GeneratesReport_MultiRequirementsTxt_CorrectlyAsync()
{
var testPath = Path.Join(Directory.GetCurrentDirectory(), string.Join(Guid.NewGuid().ToString(), ".txt"));
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("pip", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(true);
var service = new PipCommandService(
this.commandLineInvokationService.Object,
this.pathUtilityService,
this.fileUtilityService.Object,
this.envVarService.Object,
this.logger.Object);
this.commandLineInvokationService.Setup(x => x.ExecuteCommandAsync(
"pip",
It.IsAny<IEnumerable<string>>(),
It.Is<DirectoryInfo>(d => d.FullName.Contains(Directory.GetCurrentDirectory(), StringComparison.OrdinalIgnoreCase)),
It.Is<string>(s => s.Contains("requirements.txt", StringComparison.OrdinalIgnoreCase))))
.ReturnsAsync(new CommandLineExecutionResult { ExitCode = 0, StdErr = string.Empty, StdOut = string.Empty })
.Verifiable();
this.fileUtilityService.Setup(x => x.ReadAllTextAsync(It.IsAny<FileInfo>()))
.ReturnsAsync(TestResources.pip_report_multi_pkg);
var (report, reportFile) = await service.GenerateInstallationReportAsync(testPath);
// the file shouldn't exist since we're not writing to it in the test
reportFile.Should().NotBeNull();
reportFile.Exists.Should().Be(false);
// validate report parameters
report.Should().NotBeNull();
report.Version.Should().Be("1");
report.InstallItems.Should().NotBeNull();
report.InstallItems.Should().HaveCount(2);
// validate packages
report.InstallItems[0].Requested.Should().BeTrue();
report.InstallItems[0].Metadata.Name.Should().Be("six");
report.InstallItems[0].Metadata.Version.Should().Be("1.16.0");
report.InstallItems[0].Metadata.License.Should().Be("MIT");
report.InstallItems[0].Metadata.Author.Should().Be("Benjamin Peterson");
report.InstallItems[0].Metadata.AuthorEmail.Should().Be("benjamin@python.org");
report.InstallItems[0].Metadata.Maintainer.Should().BeNullOrEmpty();
report.InstallItems[0].Metadata.MaintainerEmail.Should().BeNullOrEmpty();
report.InstallItems[1].Requested.Should().BeTrue();
report.InstallItems[1].Metadata.Name.Should().Be("python-dateutil");
report.InstallItems[1].Metadata.Version.Should().Be("2.9.0.post0");
report.InstallItems[1].Metadata.License.Should().Be("Dual License");
report.InstallItems[1].Metadata.Author.Should().Be("Gustavo Niemeyer");
report.InstallItems[1].Metadata.AuthorEmail.Should().Be("gustavo@niemeyer.net");
report.InstallItems[1].Metadata.Maintainer.Should().Be("Paul Ganssle");
report.InstallItems[1].Metadata.MaintainerEmail.Should().Be("dateutil@python.org");
this.commandLineInvokationService.Verify();
}
[TestMethod]
public async Task PipCommandService_GeneratesReport_BadFile_FailsAsync()
{
var testPath = Path.Join(Directory.GetCurrentDirectory(), string.Join(Guid.NewGuid().ToString(), ".randomfile"));
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("pip", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(true);
var service = new PipCommandService(
this.commandLineInvokationService.Object,
this.pathUtilityService,
this.fileUtilityService.Object,
this.envVarService.Object,
this.logger.Object);
var (report, reportFile) = await service.GenerateInstallationReportAsync(testPath);
// the file shouldn't exist since we're not writing to it in the test
reportFile.Should().BeNull();
// validate report parameters
report.Should().NotBeNull();
report.Version.Should().BeNull();
report.InstallItems.Should().BeNull();
}
[TestMethod]
public async Task PipCommandService_GeneratesReport_EmptyPath_FailsAsync()
{
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("pip", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(true);
var service = new PipCommandService(
this.commandLineInvokationService.Object,
this.pathUtilityService,
this.fileUtilityService.Object,
this.envVarService.Object,
this.logger.Object);
var (report, reportFile) = await service.GenerateInstallationReportAsync(string.Empty);
// the file shouldn't exist since we're not writing to it in the test
reportFile.Should().BeNull();
// validate report parameters
report.Should().NotBeNull();
report.Version.Should().BeNull();
report.InstallItems.Should().BeNull();
}
[TestMethod]
public async Task PipCommandService_GeneratesReport_RequirementsTxt_NonZeroExitAsync()
{
var testPath = Path.Join(Directory.GetCurrentDirectory(), string.Join(Guid.NewGuid().ToString(), ".txt"));
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("pip", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(true);
var service = new PipCommandService(
this.commandLineInvokationService.Object,
this.pathUtilityService,
this.fileUtilityService.Object,
this.envVarService.Object,
this.logger.Object);
this.commandLineInvokationService.Setup(x => x.ExecuteCommandAsync(
"pip",
It.IsAny<IEnumerable<string>>(),
It.Is<DirectoryInfo>(d => d.FullName.Contains(Directory.GetCurrentDirectory(), StringComparison.OrdinalIgnoreCase)),
It.Is<string>(s => s.Contains("requirements.txt", StringComparison.OrdinalIgnoreCase))))
.ReturnsAsync(new CommandLineExecutionResult { ExitCode = 1, StdErr = "TestFail", StdOut = string.Empty })
.Verifiable();
var (report, reportFile) = await service.GenerateInstallationReportAsync(testPath);
// the file shouldn't exist since we're not writing to it in the test
reportFile.Should().BeNull();
// validate report parameters
report.Should().NotBeNull();
report.Version.Should().BeNull();
report.InstallItems.Should().BeNull();
this.commandLineInvokationService.Verify();
}
}

Просмотреть файл

@ -14,7 +14,6 @@ using Microsoft.ComponentDetection.TestsUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Newtonsoft.Json;
[TestClass]
public class PipComponentDetectorTests : BaseDetectorTest<PipComponentDetector>
@ -231,12 +230,12 @@ public class PipComponentDetectorTests : BaseDetectorTest<PipComponentDetector>
x => x.Id == rootId);
}
this.CheckChild(componentRecorder, "red 0.2 - pip", new[] { "a 1.0 - pip", "c 1.0 - pip", });
this.CheckChild(componentRecorder, "green 1.3 - pip", new[] { "b 2.1 - pip", });
this.CheckChild(componentRecorder, "blue 0.4 - pip", new[] { "c 1.0 - pip", });
this.CheckChild(componentRecorder, "cat 1.8 - pip", new[] { "b 2.1 - pip", "c 1.0 - pip", "d 1.9 - pip", });
this.CheckChild(componentRecorder, "lion 3.8 - pip", new[] { "b 2.1 - pip", "c 1.0 - pip", "d 1.9 - pip", });
this.CheckChild(componentRecorder, "dog 2.1 - pip", new[] { "c 1.0 - pip", });
ComponentRecorderTestUtilities.CheckChild<PipComponent>(componentRecorder, "red 0.2 - pip", new[] { "a 1.0 - pip", "c 1.0 - pip", });
ComponentRecorderTestUtilities.CheckChild<PipComponent>(componentRecorder, "green 1.3 - pip", new[] { "b 2.1 - pip", });
ComponentRecorderTestUtilities.CheckChild<PipComponent>(componentRecorder, "blue 0.4 - pip", new[] { "c 1.0 - pip", });
ComponentRecorderTestUtilities.CheckChild<PipComponent>(componentRecorder, "cat 1.8 - pip", new[] { "b 2.1 - pip", "c 1.0 - pip", "d 1.9 - pip", });
ComponentRecorderTestUtilities.CheckChild<PipComponent>(componentRecorder, "lion 3.8 - pip", new[] { "b 2.1 - pip", "c 1.0 - pip", "d 1.9 - pip", });
ComponentRecorderTestUtilities.CheckChild<PipComponent>(componentRecorder, "dog 2.1 - pip", new[] { "c 1.0 - pip", });
var graphsByLocations = componentRecorder.GetDependencyGraphsByLocation();
graphsByLocations.Should().HaveCount(2);
@ -254,7 +253,7 @@ public class PipComponentDetectorTests : BaseDetectorTest<PipComponentDetector>
var graph1 = graphsByLocations[file1];
graph1ComponentsWithDeps.Keys.Take(2).All(graph1.IsComponentExplicitlyReferenced).Should().BeTrue();
graph1ComponentsWithDeps.Keys.Skip(2).Should().OnlyContain(a => !graph1.IsComponentExplicitlyReferenced(a));
this.CheckGraphStructure(graph1, graph1ComponentsWithDeps);
ComponentRecorderTestUtilities.CheckGraphStructure(graph1, graph1ComponentsWithDeps);
var graph2ComponentsWithDeps = new Dictionary<string, string[]>
{
@ -271,41 +270,7 @@ public class PipComponentDetectorTests : BaseDetectorTest<PipComponentDetector>
var graph2 = graphsByLocations[file2];
graph2ComponentsWithDeps.Keys.Take(3).All(graph2.IsComponentExplicitlyReferenced).Should().BeTrue();
graph2ComponentsWithDeps.Keys.Skip(3).Should().OnlyContain(a => !graph2.IsComponentExplicitlyReferenced(a));
this.CheckGraphStructure(graph2, graph2ComponentsWithDeps);
}
private void CheckGraphStructure(IDependencyGraph graph, Dictionary<string, string[]> graphComponentsWithDeps)
{
var graphComponents = graph.GetComponents().ToArray();
graphComponents.Should().HaveCount(
graphComponentsWithDeps.Keys.Count,
$"Expected {graphComponentsWithDeps.Keys.Count} component to be recorded but got {graphComponents.Length} instead!");
foreach (var componentId in graphComponentsWithDeps.Keys)
{
graphComponents.Should().Contain(
componentId, $"Component `{componentId}` not recorded!");
var recordedDeps = graph.GetDependenciesForComponent(componentId).ToArray();
var expectedDeps = graphComponentsWithDeps[componentId];
recordedDeps.Should().HaveCount(
expectedDeps.Length,
$"Count missmatch of expected dependencies ({JsonConvert.SerializeObject(expectedDeps)}) and recorded dependencies ({JsonConvert.SerializeObject(recordedDeps)}) for `{componentId}`!");
foreach (var expectedDep in expectedDeps)
{
recordedDeps.Should().Contain(
expectedDep, $"Expected `{expectedDep}` in the list of dependencies for `{componentId}` but only recorded: {JsonConvert.SerializeObject(recordedDeps)}");
}
}
}
private void CheckChild(IComponentRecorder recorder, string childId, string[] parentIds)
{
recorder.AssertAllExplicitlyReferencedComponents<PipComponent>(
childId,
parentIds.Select(parentId => new Func<PipComponent, bool>(x => x.Id == parentId)).ToArray());
ComponentRecorderTestUtilities.CheckGraphStructure(graph2, graph2ComponentsWithDeps);
}
private List<(string PackageString, GitComponent Component)> ToGitTuple(IList<string> components)

Просмотреть файл

@ -0,0 +1,402 @@
namespace Microsoft.ComponentDetection.Detectors.Tests;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Microsoft.ComponentDetection.Detectors.Pip;
using Microsoft.ComponentDetection.Detectors.Tests.Mocks;
using Microsoft.ComponentDetection.Detectors.Tests.Utilities;
using Microsoft.ComponentDetection.TestsUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Newtonsoft.Json;
[TestClass]
public class PipReportComponentDetectorTests : BaseDetectorTest<PipReportComponentDetector>
{
private readonly Mock<IPipCommandService> pipCommandService;
private readonly Mock<ILogger<PipReportComponentDetector>> mockLogger;
private readonly PipInstallationReport singlePackageReport;
private readonly PipInstallationReport singlePackageReportBadVersion;
private readonly PipInstallationReport multiPackageReport;
private readonly PipInstallationReport jupyterPackageReport;
public PipReportComponentDetectorTests()
{
this.pipCommandService = new Mock<IPipCommandService>();
this.DetectorTestUtility.AddServiceMock(this.pipCommandService);
this.mockLogger = new Mock<ILogger<PipReportComponentDetector>>();
this.DetectorTestUtility.AddServiceMock(this.mockLogger);
this.pipCommandService.Setup(x => x.PipExistsAsync(It.IsAny<string>())).ReturnsAsync(true);
this.pipCommandService.Setup(x => x.GetPipVersionAsync(It.IsAny<string>()))
.ReturnsAsync(new Version(23, 0, 0));
this.singlePackageReport = JsonConvert.DeserializeObject<PipInstallationReport>(TestResources.pip_report_single_pkg);
this.singlePackageReportBadVersion = JsonConvert.DeserializeObject<PipInstallationReport>(TestResources.pip_report_single_pkg_bad_version);
this.multiPackageReport = JsonConvert.DeserializeObject<PipInstallationReport>(TestResources.pip_report_multi_pkg);
this.jupyterPackageReport = JsonConvert.DeserializeObject<PipInstallationReport>(TestResources.pip_report_jupyterlab);
}
[TestMethod]
public async Task TestPipReportDetector_PipNotInstalledAsync()
{
this.mockLogger.Setup(x => x.Log(
It.IsAny<LogLevel>(),
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));
this.DetectorTestUtility.AddServiceMock(this.mockLogger);
this.pipCommandService.Setup(x => x.PipExistsAsync(It.IsAny<string>())).ReturnsAsync(false);
var (result, componentRecorder) = await this.DetectorTestUtility
.WithFile("setup.py", string.Empty)
.ExecuteDetectorAsync();
result.ResultCode.Should().Be(ProcessingResultCode.Success);
this.mockLogger.VerifyAll();
}
[TestMethod]
public async Task TestPipReportDetector_PipBadVersion_Null_Async()
{
this.pipCommandService.Setup(x => x.GetPipVersionAsync(It.IsAny<string>()))
.ReturnsAsync((Version)null);
var (result, componentRecorder) = await this.DetectorTestUtility
.WithFile("setup.py", string.Empty)
.ExecuteDetectorAsync();
result.ResultCode.Should().Be(ProcessingResultCode.Success);
this.mockLogger.Verify(x => x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((o, t) => o.ToString().StartsWith("PipReport: No valid pip version")),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));
}
[TestMethod]
public async Task TestPipReportDetector_PipBadVersion_Low_Async()
{
this.pipCommandService.Setup(x => x.GetPipVersionAsync(It.IsAny<string>()))
.ReturnsAsync(new Version(22, 1, 0));
var (result, componentRecorder) = await this.DetectorTestUtility
.WithFile("setup.py", string.Empty)
.ExecuteDetectorAsync();
result.ResultCode.Should().Be(ProcessingResultCode.Success);
this.mockLogger.Verify(x => x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((o, t) => o.ToString().StartsWith("PipReport: No valid pip version")),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));
}
[TestMethod]
public async Task TestPipReportDetector_PipInstalledNoFilesAsync()
{
var (result, componentRecorder) = await this.DetectorTestUtility.ExecuteDetectorAsync();
result.ResultCode.Should().Be(ProcessingResultCode.Success);
}
[TestMethod]
public async Task TestPipReportDetector_BadReportVersionAsync()
{
this.pipCommandService.Setup(x => x.GenerateInstallationReportAsync(It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync((this.singlePackageReportBadVersion, null));
var (result, componentRecorder) = await this.DetectorTestUtility
.WithFile("setup.py", string.Empty)
.ExecuteDetectorAsync();
result.ResultCode.Should().Be(ProcessingResultCode.Success);
this.mockLogger.Verify(x => x.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((o, t) => o.ToString().StartsWith("PipReport: The pip installation report version")),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));
}
[TestMethod]
public async Task TestPipReportDetector_BadReportParseVersionAsync()
{
this.singlePackageReportBadVersion.Version = "2.5";
this.pipCommandService.Setup(x => x.GenerateInstallationReportAsync(It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync((this.singlePackageReportBadVersion, null));
var (result, componentRecorder) = await this.DetectorTestUtility
.WithFile("setup.py", string.Empty)
.ExecuteDetectorAsync();
result.ResultCode.Should().Be(ProcessingResultCode.Success);
this.mockLogger.Verify(x => x.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((o, t) => o.ToString().StartsWith("PipReport: The pip installation report version")),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));
}
[TestMethod]
public async Task TestPipReportDetector_CatchesExceptionAsync()
{
this.pipCommandService.Setup(x => x.GenerateInstallationReportAsync(It.IsAny<string>(), It.IsAny<string>()))
.ThrowsAsync(new InvalidCastException());
var action = async () => await this.DetectorTestUtility
.WithFile("setup.py", string.Empty)
.ExecuteDetectorAsync();
await action.Should().ThrowAsync<InvalidCastException>();
this.mockLogger.Verify(x => x.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((o, t) => o.ToString().StartsWith("PipReport: Failure while parsing pip")),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));
}
[TestMethod]
public async Task TestPipReportDetector_SingleComponentAsync()
{
this.pipCommandService.Setup(x => x.GenerateInstallationReportAsync(It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync((this.singlePackageReport, null));
var (result, componentRecorder) = await this.DetectorTestUtility
.WithFile("setup.py", string.Empty)
.ExecuteDetectorAsync();
result.ResultCode.Should().Be(ProcessingResultCode.Success);
this.mockLogger.Verify(x => x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((o, t) => o.ToString().StartsWith("PipReport: Generating pip installation report")),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));
var detectedComponents = componentRecorder.GetDetectedComponents();
detectedComponents.Should().ContainSingle();
var pipComponents = detectedComponents.Where(detectedComponent => detectedComponent.Component.Id.Contains("pip")).ToList();
var sixComponent = pipComponents.Single(x => ((PipComponent)x.Component).Name.Equals("six")).Component as PipComponent;
sixComponent.Version.Should().Be("1.16.0");
sixComponent.Author.Should().Be("Benjamin Peterson");
sixComponent.License.Should().Be("MIT");
}
[TestMethod]
public async Task TestPipReportDetector_MultiComponentAsync()
{
this.pipCommandService.Setup(x => x.GenerateInstallationReportAsync(It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync((this.multiPackageReport, null));
var (result, componentRecorder) = await this.DetectorTestUtility
.WithFile("setup.py", string.Empty)
.ExecuteDetectorAsync();
result.ResultCode.Should().Be(ProcessingResultCode.Success);
var detectedComponents = componentRecorder.GetDetectedComponents();
detectedComponents.Should().HaveCount(2);
var pipComponents = detectedComponents.Where(detectedComponent => detectedComponent.Component.Id.Contains("pip")).ToList();
var sixComponent = pipComponents.Single(x => ((PipComponent)x.Component).Name.Equals("six")).Component as PipComponent;
sixComponent.Version.Should().Be("1.16.0");
sixComponent.Author.Should().Be("Benjamin Peterson");
sixComponent.License.Should().Be("MIT");
var dateComponent = pipComponents.Single(x => ((PipComponent)x.Component).Name.Equals("python-dateutil")).Component as PipComponent;
dateComponent.Version.Should().Be("2.9.0.post0");
dateComponent.Author.Should().Be("Paul Ganssle");
dateComponent.License.Should().Be("Dual License");
}
[TestMethod]
public async Task TestPipReportDetector_MultiComponent_Dedupe_Async()
{
this.pipCommandService.Setup(x => x.GenerateInstallationReportAsync(
It.Is<string>(s => s.Contains("setup.py", StringComparison.OrdinalIgnoreCase)),
It.IsAny<string>()))
.ReturnsAsync((this.multiPackageReport, null));
this.pipCommandService.Setup(x => x.GenerateInstallationReportAsync(
It.Is<string>(s => s.Contains("requirements.txt", StringComparison.OrdinalIgnoreCase)),
It.IsAny<string>()))
.ReturnsAsync((this.singlePackageReport, null));
var (result, componentRecorder) = await this.DetectorTestUtility
.WithFile("setup.py", string.Empty)
.WithFile("requirements.txt", string.Empty)
.ExecuteDetectorAsync();
result.ResultCode.Should().Be(ProcessingResultCode.Success);
var detectedComponents = componentRecorder.GetDetectedComponents();
detectedComponents.Should().HaveCount(2);
var pipComponents = detectedComponents.Where(detectedComponent => detectedComponent.Component.Id.Contains("pip")).ToList();
var sixComponent = pipComponents.Single(x => ((PipComponent)x.Component).Name.Equals("six")).Component as PipComponent;
sixComponent.Version.Should().Be("1.16.0");
sixComponent.Author.Should().Be("Benjamin Peterson");
sixComponent.License.Should().Be("MIT");
var dateComponent = pipComponents.Single(x => ((PipComponent)x.Component).Name.Equals("python-dateutil")).Component as PipComponent;
dateComponent.Version.Should().Be("2.9.0.post0");
dateComponent.Author.Should().Be("Paul Ganssle");
dateComponent.License.Should().Be("Dual License");
}
[TestMethod]
public async Task TestPipReportDetector_MultiComponent_ComponentRecorder_Async()
{
const string file1 = "c:\\repo\\setup.py";
const string file2 = "c:\\repo\\lib\\requirements.txt";
this.pipCommandService.Setup(x => x.GenerateInstallationReportAsync(
It.Is<string>(s => s.Contains("setup.py", StringComparison.OrdinalIgnoreCase)),
It.IsAny<string>()))
.ReturnsAsync((this.multiPackageReport, null));
this.pipCommandService.Setup(x => x.GenerateInstallationReportAsync(
It.Is<string>(s => s.Contains("requirements.txt", StringComparison.OrdinalIgnoreCase)),
It.IsAny<string>()))
.ReturnsAsync((this.singlePackageReport, null));
var (result, componentRecorder) = await this.DetectorTestUtility
.WithFile("setup.py", string.Empty, fileLocation: file1)
.WithFile("requirements.txt", string.Empty, fileLocation: file2)
.ExecuteDetectorAsync();
result.ResultCode.Should().Be(ProcessingResultCode.Success);
var detectedComponents = componentRecorder.GetDetectedComponents();
detectedComponents.Should().HaveCount(2);
var pipComponents = detectedComponents.Where(detectedComponent => detectedComponent.Component.Id.Contains("pip")).ToList();
var sixComponent = pipComponents.Single(x => ((PipComponent)x.Component).Name.Equals("six")).Component as PipComponent;
sixComponent.Version.Should().Be("1.16.0");
sixComponent.Author.Should().Be("Benjamin Peterson");
sixComponent.License.Should().Be("MIT");
var dateComponent = pipComponents.Single(x => ((PipComponent)x.Component).Name.Equals("python-dateutil")).Component as PipComponent;
dateComponent.Version.Should().Be("2.9.0.post0");
dateComponent.Author.Should().Be("Paul Ganssle");
dateComponent.License.Should().Be("Dual License");
componentRecorder.AssertAllExplicitlyReferencedComponents<PipComponent>(
"six 1.16.0 - pip",
x => x.Id.Equals("six 1.16.0 - pip", StringComparison.OrdinalIgnoreCase),
x => x.Id.Equals("python-dateutil 2.9.0.post0 - pip", StringComparison.OrdinalIgnoreCase));
var graphsByLocations = componentRecorder.GetDependencyGraphsByLocation();
graphsByLocations.Should().HaveCount(2);
var setupGraphComponentsWithDeps = new Dictionary<string, string[]>
{
{ "six 1.16.0 - pip", Array.Empty<string>() },
{ "python-dateutil 2.9.0.post0 - pip", new[] { "six 1.16.0 - pip" } },
};
var reqGraphComponentsWithDeps = new Dictionary<string, string[]>
{
{ "six 1.16.0 - pip", Array.Empty<string>() },
};
var setupGraph = graphsByLocations[file1];
setupGraphComponentsWithDeps.Keys.All(setupGraph.IsComponentExplicitlyReferenced).Should().BeTrue();
ComponentRecorderTestUtilities.CheckGraphStructure(setupGraph, setupGraphComponentsWithDeps);
var reqGraph = graphsByLocations[file2];
reqGraph.IsComponentExplicitlyReferenced(sixComponent.Id).Should().BeTrue();
ComponentRecorderTestUtilities.CheckGraphStructure(reqGraph, reqGraphComponentsWithDeps);
}
[TestMethod]
public async Task TestPipReportDetector_SingleRoot_ComplexGraph_ComponentRecorder_Async()
{
const string file1 = "c:\\repo\\lib\\requirements.txt";
this.pipCommandService.Setup(x => x.GenerateInstallationReportAsync(
It.Is<string>(s => s.Contains("requirements.txt", StringComparison.OrdinalIgnoreCase)),
It.IsAny<string>()))
.ReturnsAsync((this.jupyterPackageReport, null));
var (result, componentRecorder) = await this.DetectorTestUtility
.WithFile("requirements.txt", string.Empty, fileLocation: file1)
.ExecuteDetectorAsync();
result.ResultCode.Should().Be(ProcessingResultCode.Success);
var detectedComponents = componentRecorder.GetDetectedComponents();
detectedComponents.Should().HaveCount(89);
var pipComponents = detectedComponents.Where(detectedComponent => detectedComponent.Component.Id.Contains("pip")).ToList();
var jupyterComponent = pipComponents.Single(x => ((PipComponent)x.Component).Name.Equals("jupyterlab")).Component as PipComponent;
jupyterComponent.Version.Should().Be("4.2.0");
jupyterComponent.Author.Should().Be("Jupyter Development Team <jupyter@googlegroups.com>");
jupyterComponent.License.Should().Be("BSD License");
componentRecorder.AssertAllExplicitlyReferencedComponents<PipComponent>(
"jupyterlab 4.2.0 - pip",
x => x.Id.Equals("jupyterlab 4.2.0 - pip", StringComparison.OrdinalIgnoreCase));
// spot check some dependencies - there are too many to verify them all here
var graphsByLocations = componentRecorder.GetDependencyGraphsByLocation();
graphsByLocations.Should().ContainSingle();
var jupyterGraph = graphsByLocations[file1];
var jupyterLabDependencies = jupyterGraph.GetDependenciesForComponent(jupyterComponent.Id);
jupyterLabDependencies.Should().HaveCount(15);
jupyterLabDependencies.Should().Contain("async-lru 2.0.4 - pip");
jupyterLabDependencies.Should().Contain("jupyter-server 2.14.0 - pip");
jupyterLabDependencies.Should().Contain("traitlets 5.14.3 - pip");
jupyterLabDependencies.Should().Contain("requests 2.32.2 - pip");
jupyterLabDependencies.Should().Contain("jupyter-lsp 2.2.5 - pip");
var bleachComponent = pipComponents.Single(x => ((PipComponent)x.Component).Name.Equals("bleach")).Component as PipComponent;
bleachComponent.Version.Should().Be("6.1.0");
bleachComponent.Author.Should().Be("Will Kahn-Greene");
bleachComponent.License.Should().Be("Apache Software License");
var bleachDependencies = jupyterGraph.GetDependenciesForComponent(bleachComponent.Id);
bleachDependencies.Should().HaveCount(3);
bleachDependencies.Should().Contain("six 1.16.0 - pip");
bleachDependencies.Should().Contain("webencodings 0.5.1 - pip");
bleachDependencies.Should().Contain("tinycss2 1.3.0 - pip");
ComponentRecorderTestUtilities.CheckChild<PipComponent>(
componentRecorder,
"async-lru 2.0.4 - pip",
new[] { "jupyterlab 4.2.0 - pip" });
ComponentRecorderTestUtilities.CheckChild<PipComponent>(
componentRecorder,
"tinycss2 1.3.0 - pip",
new[] { "jupyterlab 4.2.0 - pip" });
}
}

Просмотреть файл

@ -0,0 +1,31 @@
namespace Microsoft.ComponentDetection.Detectors.Tests;
using FluentAssertions;
using Microsoft.ComponentDetection.Detectors.Pip;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
[TestCategory("Governance/All")]
[TestCategory("Governance/ComponentDetection")]
public class PipReportUtilitiesTests
{
[TestInitialize]
public void TestInitialize()
{
}
[TestMethod]
public void NormalizePackageName_ExpectedEquivalent()
{
// Example test cases from https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization
const string normalizedForm = "friendly-bard";
PipReportUtilities.NormalizePackageNameFormat("friendly-bard").Should().Be(normalizedForm);
PipReportUtilities.NormalizePackageNameFormat("Friendly-Bard").Should().Be(normalizedForm);
PipReportUtilities.NormalizePackageNameFormat("FRIENDLY-BARD").Should().Be(normalizedForm);
PipReportUtilities.NormalizePackageNameFormat("friendly.bard").Should().Be(normalizedForm);
PipReportUtilities.NormalizePackageNameFormat("friendly_bard").Should().Be(normalizedForm);
PipReportUtilities.NormalizePackageNameFormat("friendly--bard").Should().Be(normalizedForm);
PipReportUtilities.NormalizePackageNameFormat("FrIeNdLy-._.-bArD").Should().Be(normalizedForm);
}
}

Просмотреть файл

@ -6,9 +6,11 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.ComponentDetection.Common;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Microsoft.ComponentDetection.Detectors.Pip;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
@ -48,15 +50,24 @@ git+git://github.com/path/to/package-two@41b95ec#egg=package-two
other=2.1";
private readonly Mock<ICommandLineInvocationService> commandLineInvokationService;
private readonly Mock<ILogger<PythonCommandService>> logger;
private readonly Mock<ILogger<PathUtilityService>> pathLogger;
private readonly IPathUtilityService pathUtilityService;
public PythonCommandServiceTests() => this.commandLineInvokationService = new Mock<ICommandLineInvocationService>();
public PythonCommandServiceTests()
{
this.commandLineInvokationService = new Mock<ICommandLineInvocationService>();
this.logger = new Mock<ILogger<PythonCommandService>>();
this.pathLogger = new Mock<ILogger<PathUtilityService>>();
this.pathUtilityService = new PathUtilityService(this.pathLogger.Object);
}
[TestMethod]
public async Task PythonCommandService_ReturnsTrueWhenPythonExistsAsync()
{
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("python", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(true);
var service = new PythonCommandService(this.commandLineInvokationService.Object);
var service = new PythonCommandService(this.commandLineInvokationService.Object, this.pathUtilityService, this.logger.Object);
(await service.PythonExistsAsync()).Should().BeTrue();
}
@ -66,7 +77,7 @@ other=2.1";
{
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("python", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(false);
var service = new PythonCommandService(this.commandLineInvokationService.Object);
var service = new PythonCommandService(this.commandLineInvokationService.Object, this.pathUtilityService, this.logger.Object);
(await service.PythonExistsAsync()).Should().BeFalse();
}
@ -76,7 +87,7 @@ other=2.1";
{
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("test", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(true);
var service = new PythonCommandService(this.commandLineInvokationService.Object);
var service = new PythonCommandService(this.commandLineInvokationService.Object, this.pathUtilityService, this.logger.Object);
(await service.PythonExistsAsync("test")).Should().BeTrue();
}
@ -86,7 +97,7 @@ other=2.1";
{
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("test", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(false);
var service = new PythonCommandService(this.commandLineInvokationService.Object);
var service = new PythonCommandService(this.commandLineInvokationService.Object, this.pathUtilityService, this.logger.Object);
(await service.PythonExistsAsync("test")).Should().BeFalse();
}
@ -95,13 +106,13 @@ other=2.1";
public async Task PythonCommandService_ParsesEmptySetupPyOutputCorrectlyAsync()
{
var fakePath = @"c:\the\fake\path.py";
var fakePathAsPassedToPython = fakePath.Replace("\\", "/");
var fakePathAsPassedToPython = this.pathUtilityService.NormalizePath(fakePath);
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("python", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(true);
this.commandLineInvokationService.Setup(x => x.ExecuteCommandAsync("python", It.IsAny<IEnumerable<string>>(), It.Is<string>(c => c.Contains(fakePathAsPassedToPython))))
this.commandLineInvokationService.Setup(x => x.ExecuteCommandAsync("python", It.IsAny<IEnumerable<string>>(), It.IsAny<DirectoryInfo>(), It.Is<string>(c => c.Contains(fakePathAsPassedToPython))))
.ReturnsAsync(new CommandLineExecutionResult { ExitCode = 0, StdOut = "[]", StdErr = string.Empty });
var service = new PythonCommandService(this.commandLineInvokationService.Object);
var service = new PythonCommandService(this.commandLineInvokationService.Object, this.pathUtilityService, this.logger.Object);
var result = await service.ParseFileAsync(fakePath);
@ -112,13 +123,13 @@ other=2.1";
public async Task PythonCommandService_ParsesEmptySetupPyOutputCorrectly_Python27Async()
{
var fakePath = @"c:\the\fake\path.py";
var fakePathAsPassedToPython = fakePath.Replace("\\", "/");
var fakePathAsPassedToPython = this.pathUtilityService.NormalizePath(fakePath);
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("python", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(true);
this.commandLineInvokationService.Setup(x => x.ExecuteCommandAsync("python", It.IsAny<IEnumerable<string>>(), It.Is<string>(c => c.Contains(fakePathAsPassedToPython))))
this.commandLineInvokationService.Setup(x => x.ExecuteCommandAsync("python", It.IsAny<IEnumerable<string>>(), It.IsAny<DirectoryInfo>(), It.Is<string>(c => c.Contains(fakePathAsPassedToPython))))
.ReturnsAsync(new CommandLineExecutionResult { ExitCode = 0, StdOut = "None", StdErr = string.Empty });
var service = new PythonCommandService(this.commandLineInvokationService.Object);
var service = new PythonCommandService(this.commandLineInvokationService.Object, this.pathUtilityService, this.logger.Object);
var result = await service.ParseFileAsync(fakePath);
@ -129,13 +140,13 @@ other=2.1";
public async Task PythonCommandService_ParsesSetupPyOutputCorrectly_Python27NonePkgAsync()
{
var fakePath = @"c:\the\fake\path.py";
var fakePathAsPassedToPython = fakePath.Replace("\\", "/");
var fakePathAsPassedToPython = this.pathUtilityService.NormalizePath(fakePath);
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("python", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(true);
this.commandLineInvokationService.Setup(x => x.ExecuteCommandAsync("python", It.IsAny<IEnumerable<string>>(), It.Is<string>(c => c.Contains(fakePathAsPassedToPython))))
this.commandLineInvokationService.Setup(x => x.ExecuteCommandAsync("python", It.IsAny<IEnumerable<string>>(), It.IsAny<DirectoryInfo>(), It.Is<string>(c => c.Contains(fakePathAsPassedToPython))))
.ReturnsAsync(new CommandLineExecutionResult { ExitCode = 0, StdOut = "['None']", StdErr = string.Empty });
var service = new PythonCommandService(this.commandLineInvokationService.Object);
var service = new PythonCommandService(this.commandLineInvokationService.Object, this.pathUtilityService, this.logger.Object);
var result = await service.ParseFileAsync(fakePath);
@ -147,13 +158,13 @@ other=2.1";
public async Task PythonCommandService_ParsesRegularSetupPyOutputCorrectlyAsync()
{
var fakePath = @"c:\the\fake\path.py";
var fakePathAsPassedToPython = fakePath.Replace("\\", "/");
var fakePathAsPassedToPython = this.pathUtilityService.NormalizePath(fakePath);
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("python", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(true);
this.commandLineInvokationService.Setup(x => x.ExecuteCommandAsync("python", It.IsAny<IEnumerable<string>>(), It.Is<string>(c => c.Contains(fakePathAsPassedToPython))))
this.commandLineInvokationService.Setup(x => x.ExecuteCommandAsync("python", It.IsAny<IEnumerable<string>>(), It.IsAny<DirectoryInfo>(), It.Is<string>(c => c.Contains(fakePathAsPassedToPython))))
.ReturnsAsync(new CommandLineExecutionResult { ExitCode = 0, StdOut = "['knack==0.4.1', 'setuptools>=1.0,!=1.1', 'vsts-cli-common==0.1.3', 'vsts-cli-admin==0.1.3', 'vsts-cli-build==0.1.3', 'vsts-cli-code==0.1.3', 'vsts-cli-team==0.1.3', 'vsts-cli-package==0.1.3', 'vsts-cli-work==0.1.3']", StdErr = string.Empty });
var service = new PythonCommandService(this.commandLineInvokationService.Object);
var service = new PythonCommandService(this.commandLineInvokationService.Object, this.pathUtilityService, this.logger.Object);
var result = await service.ParseFileAsync(fakePath);
var expected = new string[] { "knack==0.4.1", "setuptools>=1.0,!=1.1", "vsts-cli-common==0.1.3", "vsts-cli-admin==0.1.3", "vsts-cli-build==0.1.3", "vsts-cli-code==0.1.3", "vsts-cli-team==0.1.3", "vsts-cli-package==0.1.3", "vsts-cli-work==0.1.3" }.Select<string, (string, GitComponent)>(dep => (dep, null)).ToArray();
@ -172,7 +183,7 @@ other=2.1";
var testPath = Path.Join(Directory.GetCurrentDirectory(), string.Join(Guid.NewGuid().ToString(), ".txt"));
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("python", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(true);
var service = new PythonCommandService(this.commandLineInvokationService.Object);
var service = new PythonCommandService(this.commandLineInvokationService.Object, this.pathUtilityService, this.logger.Object);
try
{
@ -210,7 +221,7 @@ other=2.1";
var testPath = Path.Join(Directory.GetCurrentDirectory(), string.Join(Guid.NewGuid().ToString(), ".txt"));
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("python", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(true);
var service = new PythonCommandService(this.commandLineInvokationService.Object);
var service = new PythonCommandService(this.commandLineInvokationService.Object, this.pathUtilityService, this.logger.Object);
try
{
@ -392,7 +403,7 @@ other=2.1";
var testPath = Path.Join(Directory.GetCurrentDirectory(), string.Join(Guid.NewGuid().ToString(), ".txt"));
this.commandLineInvokationService.Setup(x => x.CanCommandBeLocatedAsync("python", It.IsAny<IEnumerable<string>>(), "--version")).ReturnsAsync(true);
var service = new PythonCommandService(this.commandLineInvokationService.Object);
var service = new PythonCommandService(this.commandLineInvokationService.Object, this.pathUtilityService, this.logger.Object);
using (var writer = File.CreateText(testPath))
{

Просмотреть файл

@ -1,10 +1,12 @@
namespace Microsoft.ComponentDetection.Detectors.Tests.Utilities;
namespace Microsoft.ComponentDetection.Detectors.Tests.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Newtonsoft.Json;
public static class ComponentRecorderTestUtilities
{
@ -134,6 +136,41 @@ public static class ComponentRecorderTestUtilities
.ToList();
}
public static void CheckGraphStructure(IDependencyGraph graph, Dictionary<string, string[]> graphComponentsWithDeps)
{
var graphComponents = graph.GetComponents().ToArray();
graphComponents.Should().HaveCount(
graphComponentsWithDeps.Keys.Count,
$"Expected {graphComponentsWithDeps.Keys.Count} component to be recorded but got {graphComponents.Length} instead!");
foreach (var componentId in graphComponentsWithDeps.Keys)
{
graphComponents.Should().Contain(
componentId, $"Component `{componentId}` not recorded!");
var recordedDeps = graph.GetDependenciesForComponent(componentId).ToArray();
var expectedDeps = graphComponentsWithDeps[componentId];
recordedDeps.Should().HaveCount(
expectedDeps.Length,
$"Count missmatch of expected dependencies ({JsonConvert.SerializeObject(expectedDeps)}) and recorded dependencies ({JsonConvert.SerializeObject(recordedDeps)}) for `{componentId}`!");
foreach (var expectedDep in expectedDeps)
{
recordedDeps.Should().Contain(
expectedDep, $"Expected `{expectedDep}` in the list of dependencies for `{componentId}` but only recorded: {JsonConvert.SerializeObject(recordedDeps)}");
}
}
}
public static void CheckChild<T>(IComponentRecorder recorder, string childId, string[] parentIds)
where T : TypedComponent
{
recorder.AssertAllExplicitlyReferencedComponents(
childId,
parentIds.Select(parentId => new Func<T, bool>(x => x.Id == parentId)).ToArray());
}
public class ComponentOrientedGrouping
{
public IEnumerable<(string ManifestFile, IDependencyGraph Graph)> FoundInGraphs { get; set; }