diff --git a/docs/creating-a-new-detector.md b/docs/creating-a-new-detector.md index 41739b8d8..0e4a1243b 100644 --- a/docs/creating-a-new-detector.md +++ b/docs/creating-a-new-detector.md @@ -57,6 +57,28 @@ New detectors progress through three stages before becoming default. Contributor | **2. Experimental** | `IExperimentalDetector` | Runs automatically but output is NOT included in scan results. Used to measure performance impact. Enabling experimental detectors via `--DetectorArgs YourDetectorId=EnableIfDefaultOff` will cause output to be included in scan results. | | **3. Default** | `IComponentDetector` | Fully integrated. Runs by default and output is included in results. Can be filtered via `--DetectorCategory` or `--DetectorFilters`. | +### Experimental Configurations + +`IExperimentalDetector` requires an `IExperimentConfiguration ExperimentConfiguration` property. Keep the +configuration beside the detector and return the same configuration instance for the detector's lifetime. +The experiment service discovers it from registered detectors automatically; do not register it separately. +Configurations must include their owning detector in `IsInExperimentGroup`, or service construction fails. +`IExperimentConfiguration` lives in the Contracts project, so detectors do not need to reference Orchestrator. +Implement `InitAsync` for configuration initialization, returning `Task.CompletedTask` when none is needed. + +For comparisons against existing detectors, implement a configuration like `CondaLockDetectorExperiment` +or `LinuxApplicationLayerExperiment`. For discoveries without an existing baseline, use +`EmptyBaselineExperiment` from `Microsoft.ComponentDetection.Detectors.Experiments`: + +```csharp +public IExperimentConfiguration ExperimentConfiguration { get; } = + new EmptyBaselineExperiment("YourEcosystemExperiment"); +``` + +An empty-baseline experiment reports all discoveries as additions. Existing experiments retain their +control groups and recording rules. Every experiment is supplied by a registered experimental detector; +default-off detectors do not supply experiment configurations. + ## Step 1: Define Your Component Type **Skip this step if reusing an existing component type** (e.g., `NpmComponent`, `PipComponent`). diff --git a/docs/detectors/pip.md b/docs/detectors/pip.md index b11a15ab7..e2fb575e1 100644 --- a/docs/detectors/pip.md +++ b/docs/detectors/pip.md @@ -24,6 +24,11 @@ as long as the report adheres to the following naming scheme: `component-detecti ### Legacy Detection (PipDetector, SimplePipDetector) +Both legacy detectors are default-off. SimplePip was promoted to experimental in +[#806](https://github.com/microsoft/component-detection/pull/806), then returned to default-off in +[#836](https://github.com/microsoft/component-detection/pull/836). It has no active experiment configuration. +Enable it explicitly with `--DetectorArgs SimplePip=EnableIfDefaultOff`. + Pip detection is performed by running the following code snippet on every *setup.py*: ```python diff --git a/docs/detectors/swift.md b/docs/detectors/swift.md index 59aad5f74..a4eb3ef1f 100644 --- a/docs/detectors/swift.md +++ b/docs/detectors/swift.md @@ -16,7 +16,8 @@ The version, the url and commit hash of the dependencies are stored in this file ## Experiment results -Swift detection is experimental. The registered `SwiftDetectorExperiment` uses an empty control group +Swift detection is experimental. `SwiftResolvedComponentDetector.ExperimentConfiguration` supplies +`SwiftDetectorExperiment`, which is discovered automatically and uses an empty control group and `SwiftResolvedComponentDetector` as its experiment group, so all Swift discoveries are reported as additions. It does not depend on CocoaPods detection or compare Swift packages against CocoaPods packages. diff --git a/src/Microsoft.ComponentDetection.Contracts/IComponentDetector.cs b/src/Microsoft.ComponentDetection.Contracts/IComponentDetector.cs index 8f0a0b7e0..7246a55d3 100644 --- a/src/Microsoft.ComponentDetection.Contracts/IComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Contracts/IComponentDetector.cs @@ -61,4 +61,9 @@ public interface IDefaultOffComponentDetector : IComponentDetector /// public interface IExperimentalDetector : IComponentDetector { + /// + /// Gets the configuration that includes this detector in its experiment group. + /// The configuration is discovered automatically and does not need separate service registration. + /// + IExperimentConfiguration ExperimentConfiguration { get; } } diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/IExperimentConfiguration.cs b/src/Microsoft.ComponentDetection.Contracts/IExperimentConfiguration.cs similarity index 90% rename from src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/IExperimentConfiguration.cs rename to src/Microsoft.ComponentDetection.Contracts/IExperimentConfiguration.cs index d5d01d803..4214997cc 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/IExperimentConfiguration.cs +++ b/src/Microsoft.ComponentDetection.Contracts/IExperimentConfiguration.cs @@ -1,7 +1,6 @@ -namespace Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; +namespace Microsoft.ComponentDetection.Contracts; using System.Threading.Tasks; -using Microsoft.ComponentDetection.Contracts; /// /// Defines the configuration for an experiment. An experiment is a set of detectors that are grouped into control and @@ -20,7 +19,7 @@ public interface IExperimentConfiguration /// Initializes the experiment configuration. /// /// A representing the asynchronous operation. - Task InitAsync() => Task.CompletedTask; + Task InitAsync(); /// /// Specifies if the detector is in the control group. @@ -30,7 +29,7 @@ public interface IExperimentConfiguration bool IsInControlGroup(IComponentDetector componentDetector); /// - /// Specifies if the detector is in the control group. + /// Specifies if the detector is in the experiment group. /// /// The detector. /// true if the detector is in the experiment group; otherwise, false. diff --git a/src/Microsoft.ComponentDetection.Detectors/Experiments/EmptyBaselineExperiment.cs b/src/Microsoft.ComponentDetection.Detectors/Experiments/EmptyBaselineExperiment.cs new file mode 100644 index 000000000..b2bcdd710 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/Experiments/EmptyBaselineExperiment.cs @@ -0,0 +1,38 @@ +namespace Microsoft.ComponentDetection.Detectors.Experiments; + +using System; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Contracts; + +/// +/// Records discoveries from a detector as additions against an empty control group. +/// +/// The experimental detector. +public class EmptyBaselineExperiment : IExperimentConfiguration + where TDetector : IExperimentalDetector +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the experiment. + public EmptyBaselineExperiment(string name) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + this.Name = name; + } + + /// + public string Name { get; } + + /// + public Task InitAsync() => Task.CompletedTask; + + /// + public bool IsInControlGroup(IComponentDetector componentDetector) => false; + + /// + public bool IsInExperimentGroup(IComponentDetector componentDetector) => componentDetector is TDetector; + + /// + public bool ShouldRecord(IComponentDetector componentDetector, int numComponents) => true; +} diff --git a/src/Microsoft.ComponentDetection.Detectors/conda/CondaLockComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/conda/CondaLockComponentDetector.cs index bbc754eea..3c28a0473 100644 --- a/src/Microsoft.ComponentDetection.Detectors/conda/CondaLockComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/conda/CondaLockComponentDetector.cs @@ -26,6 +26,9 @@ public CondaLockComponentDetector( public override string Id => "CondaLock"; + /// + public IExperimentConfiguration ExperimentConfiguration { get; } = new CondaLockDetectorExperiment(); + public override IList SearchPatterns { get; } = ["conda-lock.yml", "*.conda-lock.yml"]; public override IEnumerable SupportedComponentTypes => [ComponentType.Conda, ComponentType.Pip]; diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/CondaLockDetectorExperiment.cs b/src/Microsoft.ComponentDetection.Detectors/conda/CondaLockDetectorExperiment.cs similarity index 82% rename from src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/CondaLockDetectorExperiment.cs rename to src/Microsoft.ComponentDetection.Detectors/conda/CondaLockDetectorExperiment.cs index b55c034e6..d67a06967 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/CondaLockDetectorExperiment.cs +++ b/src/Microsoft.ComponentDetection.Detectors/conda/CondaLockDetectorExperiment.cs @@ -1,7 +1,7 @@ -namespace Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; +namespace Microsoft.ComponentDetection.Detectors.CondaLock; +using System.Threading.Tasks; using Microsoft.ComponentDetection.Contracts; -using Microsoft.ComponentDetection.Detectors.CondaLock; using Microsoft.ComponentDetection.Detectors.Pip; /// @@ -12,6 +12,9 @@ public class CondaLockDetectorExperiment : IExperimentConfiguration /// public string Name => "CondaLockDetectorExperiment"; + /// + public Task InitAsync() => Task.CompletedTask; + /// public bool IsInControlGroup(IComponentDetector componentDetector) => componentDetector is PipReportComponentDetector; diff --git a/src/Microsoft.ComponentDetection.Detectors/dockercompose/DockerComposeComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/dockercompose/DockerComposeComponentDetector.cs index ce2669795..3d9c47e38 100644 --- a/src/Microsoft.ComponentDetection.Detectors/dockercompose/DockerComposeComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/dockercompose/DockerComposeComponentDetector.cs @@ -9,6 +9,7 @@ namespace Microsoft.ComponentDetection.Detectors.DockerCompose; using Microsoft.ComponentDetection.Contracts; using Microsoft.ComponentDetection.Contracts.Internal; using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.ComponentDetection.Detectors.Experiments; using Microsoft.Extensions.Logging; using YamlDotNet.RepresentationModel; @@ -26,6 +27,9 @@ public DockerComposeComponentDetector( public override string Id => "DockerCompose"; + /// + public IExperimentConfiguration ExperimentConfiguration { get; } = new EmptyBaselineExperiment("DockerComposeDetectorExperiment"); + public override IList SearchPatterns { get; } = [ "docker-compose.yml", "docker-compose.yaml", diff --git a/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs index 00760e336..8bbe9c23f 100644 --- a/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs @@ -11,6 +11,7 @@ namespace Microsoft.ComponentDetection.Detectors.Dockerfile; using Microsoft.ComponentDetection.Contracts; using Microsoft.ComponentDetection.Contracts.Internal; using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.ComponentDetection.Detectors.Experiments; using Microsoft.Extensions.Logging; using Sprache; using Valleysoft.DockerfileModel; @@ -36,6 +37,9 @@ public DockerfileComponentDetector( public override string Id { get; } = "DockerReference"; + /// + public IExperimentConfiguration ExperimentConfiguration { get; } = new EmptyBaselineExperiment("DockerfileDetectorExperiment"); + public override IEnumerable Categories => [nameof(DetectorClass.DockerReference)]; public override IList SearchPatterns { get; } = ["dockerfile", "dockerfile.*", "*.dockerfile"]; diff --git a/src/Microsoft.ComponentDetection.Detectors/helm/HelmComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/helm/HelmComponentDetector.cs index 030f30069..358bc68ca 100644 --- a/src/Microsoft.ComponentDetection.Detectors/helm/HelmComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/helm/HelmComponentDetector.cs @@ -12,6 +12,7 @@ namespace Microsoft.ComponentDetection.Detectors.Helm; using Microsoft.ComponentDetection.Contracts; using Microsoft.ComponentDetection.Contracts.Internal; using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.ComponentDetection.Detectors.Experiments; using Microsoft.Extensions.Logging; using YamlDotNet.RepresentationModel; @@ -37,6 +38,9 @@ public HelmComponentDetector( public override string Id => "Helm"; + /// + public IExperimentConfiguration ExperimentConfiguration { get; } = new EmptyBaselineExperiment("HelmDetectorExperiment"); + public override IList SearchPatterns { get; } = [ "Chart.yaml", "Chart.yml", diff --git a/src/Microsoft.ComponentDetection.Detectors/ivy/IvyDetector.cs b/src/Microsoft.ComponentDetection.Detectors/ivy/IvyDetector.cs index d37cd86a9..f81bcd39b 100644 --- a/src/Microsoft.ComponentDetection.Detectors/ivy/IvyDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/ivy/IvyDetector.cs @@ -14,6 +14,7 @@ namespace Microsoft.ComponentDetection.Detectors.Ivy; using Microsoft.ComponentDetection.Contracts.BcdeModels; using Microsoft.ComponentDetection.Contracts.Internal; using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.ComponentDetection.Detectors.Experiments; using Microsoft.Extensions.Logging; /// @@ -62,6 +63,9 @@ public IvyDetector( public override string Id => "Ivy"; + /// + public IExperimentConfiguration ExperimentConfiguration { get; } = new EmptyBaselineExperiment("IvyDetectorExperiment"); + public override IList SearchPatterns => ["ivy.xml"]; public override IEnumerable SupportedComponentTypes => [ComponentType.Maven]; diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/LinuxApplicationLayerDetector.cs b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxApplicationLayerDetector.cs index f883a75a8..053616793 100644 --- a/src/Microsoft.ComponentDetection.Detectors/linux/LinuxApplicationLayerDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxApplicationLayerDetector.cs @@ -23,6 +23,9 @@ ILogger logger /// public new string Id => "LinuxApplicationLayer"; + /// + public IExperimentConfiguration ExperimentConfiguration { get; } = new LinuxApplicationLayerExperiment(); + /// public new IEnumerable Categories => [ diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/LinuxApplicationLayerExperiment.cs b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxApplicationLayerExperiment.cs similarity index 93% rename from src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/LinuxApplicationLayerExperiment.cs rename to src/Microsoft.ComponentDetection.Detectors/linux/LinuxApplicationLayerExperiment.cs index 27f06025c..3bbc9d8e6 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/LinuxApplicationLayerExperiment.cs +++ b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxApplicationLayerExperiment.cs @@ -1,7 +1,7 @@ -namespace Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; +namespace Microsoft.ComponentDetection.Detectors.Linux; +using System.Threading.Tasks; using Microsoft.ComponentDetection.Contracts; -using Microsoft.ComponentDetection.Detectors.Linux; using Microsoft.ComponentDetection.Detectors.Npm; using Microsoft.ComponentDetection.Detectors.NuGet; using Microsoft.ComponentDetection.Detectors.Pip; @@ -20,6 +20,9 @@ public class LinuxApplicationLayerExperiment : IExperimentConfiguration /// public string Name => "LinuxApplicationLayer"; + /// + public Task InitAsync() => Task.CompletedTask; + /// public bool IsInControlGroup(IComponentDetector componentDetector) => componentDetector diff --git a/src/Microsoft.ComponentDetection.Detectors/poetry/PoetryComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/poetry/PoetryComponentDetector.cs index 1be702461..1e537176b 100644 --- a/src/Microsoft.ComponentDetection.Detectors/poetry/PoetryComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/poetry/PoetryComponentDetector.cs @@ -10,6 +10,7 @@ namespace Microsoft.ComponentDetection.Detectors.Poetry; using Microsoft.ComponentDetection.Contracts; using Microsoft.ComponentDetection.Contracts.Internal; using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.ComponentDetection.Detectors.Experiments; using Microsoft.ComponentDetection.Detectors.Poetry.Contracts; using Microsoft.Extensions.Logging; using Tomlyn; @@ -28,6 +29,9 @@ public PoetryComponentDetector( public override string Id => "Poetry"; + /// + public IExperimentConfiguration ExperimentConfiguration { get; } = new EmptyBaselineExperiment("PoetryDetectorExperiment"); + public override IList SearchPatterns { get; } = ["poetry.lock"]; public override IEnumerable SupportedComponentTypes => [ComponentType.Pip]; diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/SwiftDetectorExperiment.cs b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftDetectorExperiment.cs similarity index 80% rename from src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/SwiftDetectorExperiment.cs rename to src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftDetectorExperiment.cs index dc25a409a..12c0355cd 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/SwiftDetectorExperiment.cs +++ b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftDetectorExperiment.cs @@ -1,7 +1,7 @@ -namespace Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; +namespace Microsoft.ComponentDetection.Detectors.Swift; +using System.Threading.Tasks; using Microsoft.ComponentDetection.Contracts; -using Microsoft.ComponentDetection.Detectors.Swift; /// /// Records Swift Package Manager discoveries against an empty baseline. @@ -11,6 +11,9 @@ public class SwiftDetectorExperiment : IExperimentConfiguration /// public string Name => "SwiftDetectorExperiment"; + /// + public Task InitAsync() => Task.CompletedTask; + /// public bool IsInControlGroup(IComponentDetector componentDetector) => false; diff --git a/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs index d6cff8635..20946949c 100644 --- a/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs @@ -33,6 +33,9 @@ public SwiftResolvedComponentDetector( public override string Id { get; } = "Swift"; + /// + public IExperimentConfiguration ExperimentConfiguration { get; } = new SwiftDetectorExperiment(); + public override IEnumerable Categories => [Enum.GetName(DetectorClass.Swift)]; public override IList SearchPatterns { get; } = ["Package.resolved"]; diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/SimplePipExperiment.cs b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/SimplePipExperiment.cs deleted file mode 100644 index 4661823f2..000000000 --- a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/SimplePipExperiment.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; - -using Microsoft.ComponentDetection.Contracts; -using Microsoft.ComponentDetection.Detectors.Pip; - -/// -/// Validating the . -/// -public class SimplePipExperiment : IExperimentConfiguration -{ - /// - public string Name => "NewPipDetector"; - - /// - public bool IsInControlGroup(IComponentDetector componentDetector) => componentDetector is PipComponentDetector; - - /// - public bool IsInExperimentGroup(IComponentDetector componentDetector) => componentDetector is SimplePipComponentDetector; - - /// - public bool ShouldRecord(IComponentDetector componentDetector, int numComponents) => true; -} diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/DefaultExperimentProcessor.cs b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/DefaultExperimentProcessor.cs index 7172d786f..7a614c6b3 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/DefaultExperimentProcessor.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/DefaultExperimentProcessor.cs @@ -4,7 +4,7 @@ namespace Microsoft.ComponentDetection.Orchestrator.Experiments; using System.Text.Json; using System.Threading.Tasks; using Microsoft.ComponentDetection.Common; -using Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; +using Microsoft.ComponentDetection.Contracts; using Microsoft.ComponentDetection.Orchestrator.Experiments.Models; using Microsoft.Extensions.Logging; diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/ExperimentService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/ExperimentService.cs index 4468199f0..7860b4d58 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/ExperimentService.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/ExperimentService.cs @@ -9,7 +9,6 @@ namespace Microsoft.ComponentDetection.Orchestrator.Experiments; using Microsoft.ComponentDetection.Common.DependencyGraph; using Microsoft.ComponentDetection.Contracts; using Microsoft.ComponentDetection.Orchestrator.Commands; -using Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; using Microsoft.ComponentDetection.Orchestrator.Experiments.Models; using Microsoft.ComponentDetection.Orchestrator.Services; using Microsoft.ComponentDetection.Orchestrator.Services.GraphTranslation; @@ -26,18 +25,28 @@ public class ExperimentService : IExperimentService /// /// Initializes a new instance of the class. /// - /// The experiment configurations. + /// The detectors whose experimental configurations are discovered automatically. /// The experiment processors. /// The graph translation service. /// The logger. public ExperimentService( - IEnumerable configs, + IEnumerable detectors, IEnumerable experimentProcessors, IGraphTranslationService graphTranslationService, ILogger logger) { + var detectorConfigs = detectors.OfType().Select(detector => + { + var config = detector.ExperimentConfiguration; + if (config is null || !config.IsInExperimentGroup(detector)) + { + throw new InvalidOperationException($"Experimental detector '{detector.Id}' must provide a configuration that includes it in the experiment group."); + } + + return config; + }); this.experiments = new ConcurrentDictionary( - configs.ToDictionary(config => config, _ => new ExperimentResults())); + detectorConfigs.Distinct().ToDictionary(config => config, _ => new ExperimentResults())); this.experimentProcessors = experimentProcessors; this.graphTranslationService = graphTranslationService; this.logger = logger; diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/IExperimentProcessor.cs b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/IExperimentProcessor.cs index 90430c980..7753f446a 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/IExperimentProcessor.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/IExperimentProcessor.cs @@ -1,7 +1,7 @@ namespace Microsoft.ComponentDetection.Orchestrator.Experiments; using System.Threading.Tasks; -using Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; +using Microsoft.ComponentDetection.Contracts; using Microsoft.ComponentDetection.Orchestrator.Experiments.Models; /// diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs index edd1f0031..c52077b12 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs @@ -30,7 +30,6 @@ namespace Microsoft.ComponentDetection.Orchestrator.Extensions; using Microsoft.ComponentDetection.Detectors.Yarn; using Microsoft.ComponentDetection.Detectors.Yarn.Parsers; using Microsoft.ComponentDetection.Orchestrator.Experiments; -using Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; using Microsoft.ComponentDetection.Orchestrator.Services; using Microsoft.ComponentDetection.Orchestrator.Services.GraphTranslation; using Microsoft.Extensions.DependencyInjection; @@ -71,10 +70,6 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s // Experiments services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); // Detectors // CocoaPods diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/ComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/ComponentDetectorTests.cs index 63034fbc3..9452ff4ac 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/ComponentDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/ComponentDetectorTests.cs @@ -64,6 +64,40 @@ public void AllDetectorsHaveUniqueSupportedComponentTypes() } } + [TestMethod] + public void AllExperimentalDetectorsProvideConfigurationsThatIncludeThem() + { + var experimentalDetectors = this.detectors.OfType().ToList(); + experimentalDetectors.Should().NotBeEmpty(); + + foreach (var detector in experimentalDetectors) + { + var config = detector.ExperimentConfiguration; + config.Should().NotBeNull($"because {detector.Id} must have an experiment"); + config.Name.Should().NotBeNullOrWhiteSpace(); + config.Should().BeSameAs(detector.ExperimentConfiguration); + config.IsInExperimentGroup(detector).Should().BeTrue($"because {detector.Id} must participate in its experiment"); + } + } + + [TestMethod] + [DataRow("DockerCompose", "DockerComposeDetectorExperiment")] + [DataRow("DockerReference", "DockerfileDetectorExperiment")] + [DataRow("Helm", "HelmDetectorExperiment")] + [DataRow("Ivy", "IvyDetectorExperiment")] + [DataRow("Poetry", "PoetryDetectorExperiment")] + public void DiscoveryExperimentsHaveEmptyControlGroupAndOnlyTheirOwnDetector(string detectorId, string experimentName) + { + var detector = this.detectors.OfType().Single(d => d.Id == detectorId); + var config = detector.ExperimentConfiguration; + + config.Name.Should().Be(experimentName); + this.detectors.Where(config.IsInControlGroup).Should().BeEmpty(); + this.detectors.Where(config.IsInExperimentGroup).Should().ContainSingle().Which.Should().BeSameAs(detector); + config.ShouldRecord(detector, 0).Should().BeTrue(); + config.ShouldRecord(detector, 1).Should().BeTrue(); + } + [TestMethod] public void UvLockComponentDetector_DoesNotImplementIExperimentalDetector() { @@ -73,6 +107,15 @@ public void UvLockComponentDetector_DoesNotImplementIExperimentalDetector() uvLockDetector.Should().NotBeAssignableTo("because UvLockComponentDetector should not implement IExperimentalDetector"); } + [TestMethod] + public void SimplePipComponentDetector_RemainsDefaultOff() + { + var detector = this.detectors.Single(d => d.Id == "SimplePip"); + + detector.Should().BeAssignableTo(); + detector.Should().NotBeAssignableTo("because SimplePip was deliberately returned to default-off in #836"); + } + [TestMethod] public void CondaLockComponentDetector_ImplementsIExperimentalDetector() { diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/EmptyBaselineExperimentTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/EmptyBaselineExperimentTests.cs new file mode 100644 index 000000000..870c6a82d --- /dev/null +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/EmptyBaselineExperimentTests.cs @@ -0,0 +1,34 @@ +namespace Microsoft.ComponentDetection.Detectors.Tests; + +using System; +using AwesomeAssertions; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Detectors.Experiments; +using Microsoft.ComponentDetection.Detectors.Swift; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +[TestClass] +[TestCategory("Governance/All")] +[TestCategory("Governance/ComponentDetection")] +public class EmptyBaselineExperimentTests +{ + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public void Constructor_RejectsMissingExperimentName(string? name) + { + var action = () => new EmptyBaselineExperiment(name!); + + action.Should().Throw(); + } + + [TestMethod] + public void IsInExperimentGroup_ExcludesUnrelatedDetector() + { + var experiment = new EmptyBaselineExperiment("SwiftDiscovery"); + + experiment.IsInExperimentGroup(Mock.Of()).Should().BeFalse(); + } +} diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/DefaultExperimentProcessorTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/DefaultExperimentProcessorTests.cs index 0c3af5a91..f0ad038d5 100644 --- a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/DefaultExperimentProcessorTests.cs +++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/DefaultExperimentProcessorTests.cs @@ -5,8 +5,8 @@ namespace Microsoft.ComponentDetection.Orchestrator.Tests.Experiments; using System.Threading; using System.Threading.Tasks; using Microsoft.ComponentDetection.Common; +using Microsoft.ComponentDetection.Contracts; using Microsoft.ComponentDetection.Orchestrator.Experiments; -using Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; using Microsoft.ComponentDetection.Orchestrator.Experiments.Models; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/ExperimentConfigTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/ExperimentConfigTests.cs index 7519ca637..4a4685f32 100644 --- a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/ExperimentConfigTests.cs +++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/ExperimentConfigTests.cs @@ -6,9 +6,11 @@ namespace Microsoft.ComponentDetection.Orchestrator.Tests.Experiments; using System.Threading.Tasks; using AwesomeAssertions; using Microsoft.ComponentDetection.Contracts; -using Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; +using Microsoft.ComponentDetection.Detectors.Swift; using Microsoft.ComponentDetection.Orchestrator.Extensions; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -22,9 +24,16 @@ public class ExperimentConfigTests [TestInitialize] public void Initialize() { - var serviceProvider = new ServiceCollection().AddComponentDetection().BuildServiceProvider(); + using var serviceProvider = new ServiceCollection() + .AddComponentDetection() + .AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)) + .BuildServiceProvider(); - this.configs = serviceProvider.GetServices().ToList(); + this.configs = serviceProvider.GetServices() + .OfType() + .Select(detector => detector.ExperimentConfiguration) + .Distinct() + .ToList(); } [TestMethod] @@ -36,7 +45,7 @@ public void ExperimentConfig_HaveDistinctNames() } [TestMethod] - public void ExperimentConfig_RegistersSwiftDetectorExperiment() + public void ExperimentConfig_DiscoversSwiftDetectorExperiment() { this.configs.OfType().Should().ContainSingle(); } diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/ExperimentServiceTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/ExperimentServiceTests.cs index 4d8173f57..31d98c585 100644 --- a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/ExperimentServiceTests.cs +++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/ExperimentServiceTests.cs @@ -13,14 +13,17 @@ namespace Microsoft.ComponentDetection.Orchestrator.Tests.Experiments; using Microsoft.ComponentDetection.Contracts.TypedComponent; using Microsoft.ComponentDetection.Detectors.CocoaPods; using Microsoft.ComponentDetection.Detectors.NuGet; +using Microsoft.ComponentDetection.Detectors.Pip; using Microsoft.ComponentDetection.Detectors.Swift; using Microsoft.ComponentDetection.Orchestrator.Commands; using Microsoft.ComponentDetection.Orchestrator.Experiments; -using Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; using Microsoft.ComponentDetection.Orchestrator.Experiments.Models; +using Microsoft.ComponentDetection.Orchestrator.Extensions; using Microsoft.ComponentDetection.Orchestrator.Services; using Microsoft.ComponentDetection.Orchestrator.Services.GraphTranslation; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -33,7 +36,7 @@ public class ExperimentServiceTests private readonly Mock experimentProcessorMock; private readonly Mock graphTranslationServiceMock; private readonly Mock> loggerMock; - private readonly Mock detectorMock; + private readonly Mock detectorMock; private readonly Mock scanSettingsMock; private readonly ComponentRecorder componentRecorder; @@ -43,12 +46,13 @@ public ExperimentServiceTests() this.experimentProcessorMock = new Mock(); this.graphTranslationServiceMock = new Mock(); this.loggerMock = new Mock>(); - this.detectorMock = new Mock(); + this.detectorMock = new Mock(); this.scanSettingsMock = new Mock(); this.componentRecorder = new ComponentRecorder(); this.detectorMock.SetupGet(x => x.Id).Returns(Guid.NewGuid().ToString()); + this.detectorMock.SetupGet(x => x.ExperimentConfiguration).Returns(this.experimentConfigMock.Object); this.experimentConfigMock.Setup(x => x.IsInControlGroup(this.detectorMock.Object)).Returns(true); this.experimentConfigMock.Setup(x => x.IsInExperimentGroup(this.detectorMock.Object)).Returns(true); this.experimentConfigMock.Setup(x => x.ShouldRecord(this.detectorMock.Object, It.IsAny())).Returns(true); @@ -74,7 +78,7 @@ public void RecordDetectorRun_AddsComponentsToControlAndExperimentGroup() var components = ExperimentTestUtils.CreateRandomComponents(); var service = new ExperimentService( - [this.experimentConfigMock.Object], + [this.detectorMock.Object], [], this.graphTranslationServiceMock.Object, this.loggerMock.Object); @@ -83,7 +87,7 @@ public void RecordDetectorRun_AddsComponentsToControlAndExperimentGroup() service.RecordDetectorRun(this.detectorMock.Object, this.componentRecorder, this.scanSettingsMock.Object); this.experimentConfigMock.Verify(x => x.IsInControlGroup(this.detectorMock.Object), Times.Once()); - this.experimentConfigMock.Verify(x => x.IsInExperimentGroup(this.detectorMock.Object), Times.Once()); + this.experimentConfigMock.Verify(x => x.IsInExperimentGroup(this.detectorMock.Object), Times.Exactly(2)); // verify that we always call the graph translation service with updateLocations = false so we dont // corrupt file location paths @@ -101,12 +105,15 @@ public async Task RecordDetectorRun_FiltersExperimentsAsync() .Returns(false); filterConfigMock.Setup(x => x.IsInControlGroup(this.detectorMock.Object)).Returns(true); filterConfigMock.Setup(x => x.IsInExperimentGroup(this.detectorMock.Object)).Returns(true); + var filterDetectorMock = new Mock(); + filterDetectorMock.SetupGet(x => x.ExperimentConfiguration).Returns(filterConfigMock.Object); + filterConfigMock.Setup(x => x.IsInExperimentGroup(filterDetectorMock.Object)).Returns(true); var components = ExperimentTestUtils.CreateRandomComponents(); this.SetupGraphMock(components); var service = new ExperimentService( - [this.experimentConfigMock.Object, filterConfigMock.Object], + [this.detectorMock.Object, filterDetectorMock.Object], [this.experimentProcessorMock.Object], this.graphTranslationServiceMock.Object, this.loggerMock.Object); @@ -128,11 +135,14 @@ public async Task RecordDetectorRun_Respects_DetectorExperiments_EnableAsync() { DetectorExperiments.Enable = false; var filterConfigMock = new Mock(); + var filterDetectorMock = new Mock(); + filterDetectorMock.SetupGet(x => x.ExperimentConfiguration).Returns(filterConfigMock.Object); + filterConfigMock.Setup(x => x.IsInExperimentGroup(filterDetectorMock.Object)).Returns(true); var components = ExperimentTestUtils.CreateRandomComponents(); var service = new ExperimentService( - [this.experimentConfigMock.Object, filterConfigMock.Object], + [this.detectorMock.Object, filterDetectorMock.Object], [this.experimentProcessorMock.Object], this.graphTranslationServiceMock.Object, this.loggerMock.Object); @@ -156,7 +166,7 @@ public async Task FinishAsync_ProcessesExperimentsAsync() this.SetupGraphMock(components); var service = new ExperimentService( - [this.experimentConfigMock.Object], + [this.detectorMock.Object], [this.experimentProcessorMock.Object], this.graphTranslationServiceMock.Object, this.loggerMock.Object); @@ -172,11 +182,11 @@ public async Task FinishAsync_ProcessesExperimentsAsync() [TestMethod] public async Task FinishAsync_ProcessesSwiftExperimentWithoutControlDetectorAsync() { - var config = new SwiftDetectorExperiment(); var swiftDetector = new SwiftResolvedComponentDetector(null, null, null); + var config = swiftDetector.ExperimentConfiguration; var podDetector = new PodComponentDetector(null, null, null); var service = new ExperimentService( - [config], + [swiftDetector], [this.experimentProcessorMock.Object], this.graphTranslationServiceMock.Object, this.loggerMock.Object); @@ -213,9 +223,10 @@ public async Task FinishAsync_ProcessesSwiftExperimentWithoutControlDetectorAsyn [TestMethod] public async Task FinishAsync_SkipsSwiftExperimentWithoutSwiftComponentsAsync() { - var config = new SwiftDetectorExperiment(); + var swiftDetector = new SwiftResolvedComponentDetector(null, null, null); + var config = swiftDetector.ExperimentConfiguration; var service = new ExperimentService( - [config], + [swiftDetector], [this.experimentProcessorMock.Object], this.graphTranslationServiceMock.Object, this.loggerMock.Object); @@ -223,7 +234,7 @@ public async Task FinishAsync_SkipsSwiftExperimentWithoutSwiftComponentsAsync() service.RecordDetectorRun(new PodComponentDetector(null, null, null), this.componentRecorder, this.scanSettingsMock.Object); this.SetupGraphMock([]); - service.RecordDetectorRun(new SwiftResolvedComponentDetector(null, null, null), this.componentRecorder, this.scanSettingsMock.Object); + service.RecordDetectorRun(swiftDetector, this.componentRecorder, this.scanSettingsMock.Object); await service.FinishAsync(); @@ -244,7 +255,7 @@ public async Task FinishAsync_SwallowsExceptionsAsync() this.SetupGraphMock(components); var service = new ExperimentService( - [this.experimentConfigMock.Object], + [this.detectorMock.Object], [this.experimentProcessorMock.Object], this.graphTranslationServiceMock.Object, this.loggerMock.Object); @@ -258,7 +269,7 @@ public async Task FinishAsync_SwallowsExceptionsAsync() public async Task FinishAsync_SkipsEmptyExperimentsAsync() { var service = new ExperimentService( - [this.experimentConfigMock.Object], + [this.detectorMock.Object], [this.experimentProcessorMock.Object], this.graphTranslationServiceMock.Object, this.loggerMock.Object); @@ -276,7 +287,7 @@ public async Task FinishAsync_AutomaticallyProcessesExperimentsAsync() this.SetupGraphMock(components); var service = new ExperimentService( - [this.experimentConfigMock.Object], + [this.detectorMock.Object], [this.experimentProcessorMock.Object], this.graphTranslationServiceMock.Object, this.loggerMock.Object); @@ -298,7 +309,7 @@ public async Task FinishAsync_DoesNotAutomaticallyProcessExperimentsAsync() this.SetupGraphMock(components); var service = new ExperimentService( - [this.experimentConfigMock.Object], + [this.detectorMock.Object], [this.experimentProcessorMock.Object], this.graphTranslationServiceMock.Object, this.loggerMock.Object); @@ -320,7 +331,7 @@ public async Task FinishAsync_Respects_DetectorExperiments_EnableAsync() this.SetupGraphMock(components); var service = new ExperimentService( - [this.experimentConfigMock.Object], + [this.detectorMock.Object], [this.experimentProcessorMock.Object], this.graphTranslationServiceMock.Object, this.loggerMock.Object); @@ -339,7 +350,7 @@ public async Task RecordDetectorRun_CheckUnwantedDetectors_RemoveExperimentAsync var components = ExperimentTestUtils.CreateRandomComponents(); var service = new ExperimentService( - [this.experimentConfigMock.Object], + [this.detectorMock.Object], [this.experimentProcessorMock.Object], this.graphTranslationServiceMock.Object, this.loggerMock.Object); @@ -370,7 +381,7 @@ public async Task RecordDetectorRun_CheckUnwantedDetectors_KeepExperimentAsync() var components = ExperimentTestUtils.CreateRandomComponents(); var service = new ExperimentService( - [this.experimentConfigMock.Object], + [this.detectorMock.Object], [this.experimentProcessorMock.Object], this.graphTranslationServiceMock.Object, this.loggerMock.Object); @@ -399,7 +410,7 @@ public async Task RecordDetectorRun_CheckUnwantedDetectors_KeepExperimentAsync() public async Task InitializeAsync_InitsConfigsAsync() { var service = new ExperimentService( - [this.experimentConfigMock.Object], + [this.detectorMock.Object], [this.experimentProcessorMock.Object], this.graphTranslationServiceMock.Object, this.loggerMock.Object); @@ -415,7 +426,7 @@ public async Task InitializeAsync_SwallowsExceptionsAsync() this.experimentConfigMock.Setup(x => x.InitAsync()).ThrowsAsync(new InvalidOperationException()); var service = new ExperimentService( - [this.experimentConfigMock.Object], + [this.detectorMock.Object], [this.experimentProcessorMock.Object], this.graphTranslationServiceMock.Object, this.loggerMock.Object); @@ -425,4 +436,111 @@ public async Task InitializeAsync_SwallowsExceptionsAsync() await action.Should().NotThrowAsync(); this.experimentConfigMock.Verify(x => x.InitAsync(), Times.Once()); } + + [TestMethod] + public async Task InitializeAsync_DiscoversExperimentalDetectorConfigurationThroughDependencyInjectionAsync() + { + var detector = new Mock(); + detector.SetupGet(x => x.ExperimentConfiguration).Returns(this.experimentConfigMock.Object); + this.experimentConfigMock.Setup(x => x.IsInExperimentGroup(detector.Object)).Returns(true); + using var serviceProvider = new ServiceCollection() + .AddComponentDetection() + .AddSingleton(detector.Object) + .AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)) + .BuildServiceProvider(); + + await serviceProvider.GetRequiredService().InitializeAsync(); + + this.experimentConfigMock.Verify(x => x.InitAsync(), Times.Once()); + } + + [TestMethod] + public async Task InitializeAsync_InitializesSharedDetectorConfigurationOnceAsync() + { + var firstDetector = new Mock(); + var secondDetector = new Mock(); + firstDetector.SetupGet(x => x.ExperimentConfiguration).Returns(this.experimentConfigMock.Object); + secondDetector.SetupGet(x => x.ExperimentConfiguration).Returns(this.experimentConfigMock.Object); + this.experimentConfigMock.Setup(x => x.IsInExperimentGroup(It.IsAny())).Returns(true); + var service = new ExperimentService( + [firstDetector.Object, secondDetector.Object], + [], + this.graphTranslationServiceMock.Object, + this.loggerMock.Object); + + await service.InitializeAsync(); + + this.experimentConfigMock.Verify(x => x.InitAsync(), Times.Once()); + } + + [TestMethod] + public void Constructor_RejectsExperimentalDetectorWithoutConfiguration() + { + var detector = new Mock(); + detector.SetupGet(x => x.Id).Returns("Unconfigured"); + + var action = () => new ExperimentService( + [detector.Object], + [], + this.graphTranslationServiceMock.Object, + this.loggerMock.Object); + + action.Should().Throw().WithMessage("*Unconfigured*experiment group*"); + } + + [TestMethod] + public void Constructor_RejectsConfigurationThatExcludesItsExperimentalDetector() + { + var detector = new Mock(); + detector.SetupGet(x => x.Id).Returns("Excluded"); + detector.SetupGet(x => x.ExperimentConfiguration).Returns(this.experimentConfigMock.Object); + + var action = () => new ExperimentService( + [detector.Object], + [], + this.graphTranslationServiceMock.Object, + this.loggerMock.Object); + + action.Should().Throw().WithMessage("*Excluded*experiment group*"); + } + + [TestMethod] + public async Task FinishAsync_DisablingExperimentalDetectorRemovesDiscoveredExperimentAsync() + { + var detector = new SwiftResolvedComponentDetector(null, null, null); + var service = new ExperimentService( + [detector], + [this.experimentProcessorMock.Object], + this.graphTranslationServiceMock.Object, + this.loggerMock.Object); + this.SetupGraphMock(ExperimentTestUtils.CreateRandomComponents()); + service.RemoveUnwantedExperimentsbyDetectors([detector]); + + service.RecordDetectorRun(detector, this.componentRecorder, this.scanSettingsMock.Object); + await service.FinishAsync(); + + this.experimentProcessorMock.Verify( + x => x.ProcessExperimentAsync(detector.ExperimentConfiguration, It.IsAny()), + Times.Never()); + } + + [TestMethod] + public async Task FinishAsync_DoesNotCreateExperimentForDefaultOffSimplePipDetectorAsync() + { + var detector = new SimplePipComponentDetector(null, null, null, null, null); + var service = new ExperimentService( + [detector], + [this.experimentProcessorMock.Object], + this.graphTranslationServiceMock.Object, + this.loggerMock.Object); + this.SetupGraphMock(ExperimentTestUtils.CreateRandomComponents()); + + await service.InitializeAsync(); + service.RecordDetectorRun(detector, this.componentRecorder, this.scanSettingsMock.Object); + await service.FinishAsync(); + + this.experimentProcessorMock.Verify( + x => x.ProcessExperimentAsync(It.IsAny(), It.IsAny()), + Times.Never()); + } } diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/LinuxApplicationLayerExperimentTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/LinuxApplicationLayerExperimentTests.cs index f6d8c4677..12ee7b814 100644 --- a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/LinuxApplicationLayerExperimentTests.cs +++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/LinuxApplicationLayerExperimentTests.cs @@ -5,7 +5,6 @@ namespace Microsoft.ComponentDetection.Orchestrator.Tests.Experiments; using Microsoft.ComponentDetection.Detectors.Npm; using Microsoft.ComponentDetection.Detectors.NuGet; using Microsoft.ComponentDetection.Detectors.Pip; -using Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; using Microsoft.VisualStudio.TestTools.UnitTesting; [TestClass] diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/SwiftDetectorExperimentTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/SwiftDetectorExperimentTests.cs index 363854359..0151add67 100644 --- a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/SwiftDetectorExperimentTests.cs +++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/SwiftDetectorExperimentTests.cs @@ -4,7 +4,6 @@ namespace Microsoft.ComponentDetection.Orchestrator.Tests.Experiments; using Microsoft.ComponentDetection.Contracts; using Microsoft.ComponentDetection.Detectors.CocoaPods; using Microsoft.ComponentDetection.Detectors.Swift; -using Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq;