diff --git a/src/Fallout.Build/Execution/BuildContext.cs b/src/Fallout.Build/Execution/BuildContext.cs index aeb4256a..57cefa40 100644 --- a/src/Fallout.Build/Execution/BuildContext.cs +++ b/src/Fallout.Build/Execution/BuildContext.cs @@ -21,7 +21,8 @@ namespace Fallout.Common.Execution; /// /// FT-2 / #307. Intentionally /// internal — not a public contract until the SDK lands (milestone #7). Subsequent steps move -/// the per-run services (parameters, logging scope, tool-path config) onto this context. +/// the remaining per-run services (logging scope, tool-path config) onto this context; the parameter +/// service already rides it (FT-4 / #309). /// internal sealed class BuildContext : IDisposable { @@ -34,6 +35,15 @@ internal sealed class BuildContext : IDisposable private readonly ConsoleCancelEventHandler onCancelKeyPress; private readonly EventHandler onToolOptionsCreated; + /// + /// The parameter service for this run. FT-4 / #309: + /// this instance is what the static facade resolves to, so a + /// build reads parameters through the same instance form the specs already use, and the service's + /// mutable fields die with the run instead of leaking into the next one. + /// + public ParameterService Parameters { get; } = + new(() => EnvironmentInfo.ArgumentParser, () => EnvironmentInfo.Variables); + private BuildContext() { onCancelKeyPress = (_, _) => cancellationHandlers.ForEach(x => x()); diff --git a/src/Fallout.Build/Execution/ParameterService.Statics.cs b/src/Fallout.Build/Execution/ParameterService.Statics.cs index e8e026b1..cbb025a9 100644 --- a/src/Fallout.Build/Execution/ParameterService.Statics.cs +++ b/src/Fallout.Build/Execution/ParameterService.Statics.cs @@ -2,15 +2,21 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using Fallout.Common.Execution; using Fallout.Common.Utilities; namespace Fallout.Common; internal partial class ParameterService { - internal static ParameterService Instance = new( - () => EnvironmentInfo.ArgumentParser, - () => EnvironmentInfo.Variables); + // FT-4 (#309): the active service is the per-run one on BuildContext, so a build reads parameters + // through the same instance form the specs use and nothing leaks into the next run. The fallback + // covers access outside a run — process-wide, but there is no cross-run state to leak there — and + // can retire once that path is confirmed dead. + private static readonly Lazy ambientInstance = new( + () => new ParameterService(() => EnvironmentInfo.ArgumentParser, () => EnvironmentInfo.Variables)); + + internal static ParameterService Instance => BuildContext.Current?.Parameters ?? ambientInstance.Value; public static T GetParameter(string name, char? separator = null) { diff --git a/tests/Fallout.Build.Specs/BuildContextSpecs.cs b/tests/Fallout.Build.Specs/BuildContextSpecs.cs index 73f0c47b..8f62d0fa 100644 --- a/tests/Fallout.Build.Specs/BuildContextSpecs.cs +++ b/tests/Fallout.Build.Specs/BuildContextSpecs.cs @@ -44,6 +44,52 @@ public void Dispose_clears_current() BuildContext.Current.Should().BeNull(); } + [Fact] + public void Parameter_service_facade_resolves_the_current_contexts_instance() + { + using var context = BuildContext.Activate(); + + // FT-4 (#309): the static facade is a pointer at the running context, not its own singleton. + ParameterService.Instance.Should().BeSameAs(context.Parameters); + } + + [Fact] + public void Parameter_service_facade_falls_back_to_an_ambient_instance_outside_a_run() + { + BuildContext.Current.Should().BeNull(); + + // Nothing to route to outside a run, so the facade hands back a stable process-wide instance + // rather than throwing — this is the path the fallback exists for. + ParameterService.Instance.Should().NotBeNull(); + ParameterService.Instance.Should().BeSameAs(ParameterService.Instance); + } + + [Fact] + public void Each_run_gets_its_own_parameter_service() + { + ParameterService first; + ParameterService second; + + using (var context = BuildContext.Activate()) + first = context.Parameters; + using (var context = BuildContext.Activate()) + second = context.Parameters; + + second.Should().NotBeSameAs(first); + } + + [Fact] + public void Parameter_service_state_does_not_leak_into_the_next_run() + { + using (BuildContext.Activate()) + ParameterService.Instance.ArgumentsFromCommitMessageService = new ArgumentParser(["-arg", "value"]); + + // The mutable per-run fields (commit-message args, args-from-files) died with the previous + // context — the whole point of moving the service off a process-global singleton. + using (BuildContext.Activate()) + ParameterService.Instance.ArgumentsFromCommitMessageService.Should().BeNull(); + } + [Fact] public void Disposing_a_superseded_context_leaves_the_newer_one_current() {