Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions src/Incrementalist.Cmd/Commands/RunDotNetCommandTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,19 @@ public class RunDotNetCommandTask
private readonly string[] _dotnetArgs;
private readonly bool _continueOnError;
private readonly bool _runInParallel;
private readonly int _parallelLimit;
private readonly bool _failOnNoProjects;
private readonly CancellationToken _ct;

public RunDotNetCommandTask(BuildSettings settings, ILogger logger, string[] dotnetArgs, bool continueOnError,
bool runInParallel, CancellationToken ct, bool failOnNoProjects = false)
bool runInParallel, int parallelLimit, CancellationToken ct, bool failOnNoProjects = false)
{
_settings = settings;
_logger = logger;
_dotnetArgs = dotnetArgs;
_continueOnError = continueOnError;
_runInParallel = runInParallel;
_parallelLimit = parallelLimit;
_ct = ct;
_failOnNoProjects = failOnNoProjects;
}
Expand Down Expand Up @@ -74,17 +76,31 @@ private async Task<int> RunIncrementalBuild(IEnumerable<AbsolutePath> affectedPr

if (_runInParallel)
{
var semaphore = _parallelLimit > 0 ? new SemaphoreSlim(_parallelLimit) : null;

var tasks = projects.Select(async project =>
{
if (await RunCommandAsync(project, ct) != 0)
if (semaphore is not null)
await semaphore.WaitAsync();

try
{
failedProjects.Add(project);
if (!_continueOnError)
return;
if (await RunCommandAsync(project, ct) != 0)
{
failedProjects.Add(project);
if (!_continueOnError)
return;
}
}
finally
{
semaphore?.Release();
}
});

await Task.WhenAll(tasks);

semaphore?.Dispose();
}
else
{
Expand Down Expand Up @@ -203,4 +219,4 @@ private async Task<int> RunCommandAsync(AbsolutePath target, CancellationToken c
}
}
}
}
}
7 changes: 4 additions & 3 deletions src/Incrementalist.Cmd/Config/ConfigMerger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,17 @@ public static SlnOptions Merge(SlnOptions options, IncrementalistConfig? config)
merged.WorkingDirectory = options.WorkingDirectory ?? config.WorkingDirectory;
merged.NameApplicationToStart = options.NameApplicationToStart ?? config.NameApplicationToStart;;

// Merge bool properties (CLI takes precedence)
// Merge bool properties (config takes precedence)
merged.Verbose = config.Verbose.GetValueOrDefault(false);
merged.ContinueOnError = config.ContinueOnError.GetValueOrDefault(true);
merged.RunInParallel = config.RunInParallel.GetValueOrDefault(false);
merged.FailOnNoProjects = config.FailOnNoProjects.GetValueOrDefault(false);
merged.SkipGlobs = MergeGlobs(options.SkipGlobs?.ToArray(), config.SkipGlob);
merged.TargetGlobs = MergeGlobs(options.TargetGlobs?.ToArray(), config.TargetGlob);

// Merge int properties (CLI takes precedence)
// Merge int properties (config takes precedence)
merged.TimeoutMinutes = config.TimeoutMinutes.GetValueOrDefault(2);
merged.ParallelLimit = config.ParallelLimit;

// Override with any non-default CLI values
if (options.Verbose) merged.Verbose = true;
Expand Down Expand Up @@ -99,4 +100,4 @@ public static SlnOptions ApplyDefaults(SlnOptions options)
return options;
}
}
}
}
8 changes: 7 additions & 1 deletion src/Incrementalist.Cmd/Config/IncrementalistConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ public class IncrementalistConfig
[JsonPropertyName("runInParallel")]
public bool? RunInParallel { get; set; }

/// <summary>
/// When running commands, the number of parallel projects to run. Defaults to 0 (limitless).
/// </summary>
[JsonPropertyName("parallelLimit")]
public int ParallelLimit { get; set; }

/// <summary>
/// When running commands, fail if no projects are affected.
/// </summary>
Expand Down Expand Up @@ -140,4 +146,4 @@ public static bool TryLoad(string? filePath, out IncrementalistConfig? config)
}
}
}
}
}
4 changes: 2 additions & 2 deletions src/Incrementalist.Cmd/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ private static async Task ProcessSln(RunOptions options, RelativePath sln, Absol
if (options is { DryRun: false, DotNetArgs.Length: > 0 })
{
var runTask = new RunDotNetCommandTask(settings, logger, options.DotNetArgs,
options.ContinueOnError, options.RunInParallel, ct, options.FailOnNoProjects);
options.ContinueOnError, options.RunInParallel, options.ParallelLimit, ct, options.FailOnNoProjects);

var exitCode = await runTask.Run(buildResult);
if (exitCode != 0)
Expand Down Expand Up @@ -407,4 +407,4 @@ private static async Task AnalyzeSolutionDiffProcess(RunProcessOptions options,
await AnalyzeSolutionDIff(runOptions, workingFolder, logger, ct);
}
}
}
}
5 changes: 4 additions & 1 deletion src/Incrementalist.Cmd/SlnOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ public abstract class SlnOptions
[Option("parallel", HelpText = "When running commands, execute them in parallel.", Default = false)]
public bool RunInParallel { get; set; }

[Option("parallel-limit", HelpText = "When running commands, the number of parallel projects to run.", Default = 0)]
public int ParallelLimit { get; set; }

[Option("fail-on-no-projects", HelpText = "When running commands, fail if no projects are affected.",
Default = false)]
public bool FailOnNoProjects { get; set; }
Expand Down Expand Up @@ -164,4 +167,4 @@ public abstract class SlnOptions
/// </summary>
internal abstract SlnOptions PrepareForMerge();
}
}
}
43 changes: 35 additions & 8 deletions src/Incrementalist.Tests/Commands/RunDotNetCommandTaskTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ await File.WriteAllTextAsync(projectPath.Path, @"<Project Sdk=""Microsoft.NET.Sd
<OutputType>Library</OutputType>
</PropertyGroup>
</Project>");
var task = new RunDotNetCommandTask(settings, _logger, ["build", "-c", "Release", "--nologo"], true, false, CancellationToken.None);
var task = new RunDotNetCommandTask(settings, _logger, ["build", "-c", "Release", "--nologo"], true, false, 0, CancellationToken.None);

// Act
var result = await task.Run(new IncrementalBuildResult([projectPath]));
Expand All @@ -62,7 +62,7 @@ public async Task Should_Handle_Failed_Command()
{
// Arrange
var settings = new BuildSettings("master", new RelativePath("test.sln"), _repository.BasePath, [], [], "dotnet");
var task = new RunDotNetCommandTask(settings, _logger, ["build", "--invalid-option"], true, false, CancellationToken.None);
var task = new RunDotNetCommandTask(settings, _logger, ["build", "--invalid-option"], true, false, 0, CancellationToken.None);

// Act
var result = await task.Run(new IncrementalBuildResult([
Expand All @@ -74,7 +74,7 @@ public async Task Should_Handle_Failed_Command()
}

[Fact]
public async Task Should_Run_Commands_In_Parallel()
public async Task Should_Run_Commands_In_Parallel_With_No_Limit()
{
// Arrange
var settings = new BuildSettings("master", new RelativePath("test.sln"), _repository.BasePath, [], [], "dotnet");
Expand All @@ -92,7 +92,7 @@ await File.WriteAllTextAsync(projectPath.Path, @"<Project Sdk=""Microsoft.NET.Sd
}

var task = new RunDotNetCommandTask(settings, _logger, ["build", "-c", "Release", "--nologo"], true,
true, CancellationToken.None);
true, 0, CancellationToken.None);

// Act
var result = await task.Run(new IncrementalBuildResult(projects));
Expand All @@ -101,12 +101,39 @@ await File.WriteAllTextAsync(projectPath.Path, @"<Project Sdk=""Microsoft.NET.Sd
Assert.Equal(0, result);
}

[Fact]
public async Task Should_Run_Commands_In_Parallel_With_Limit()
{
// Arrange
var settings = new BuildSettings("master", new RelativePath("test.sln"), _repository.BasePath, [], [], "dotnet");
var projects = new List<AbsolutePath>();
for (int i = 1; i <= 3; i++)
{
var projectPath = new AbsolutePath(Path.Combine(_repository.BasePath.Path, $"test{i}.csproj"));
await File.WriteAllTextAsync(projectPath.Path, @"<Project Sdk=""Microsoft.NET.Sdk"">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Library</OutputType>
</PropertyGroup>
</Project>");
projects.Add(projectPath);
}

var task = new RunDotNetCommandTask(settings, _logger, ["build", "-c", "Release", "--nologo"], true,
true, 2, CancellationToken.None);

// Act
var result = await task.Run(new IncrementalBuildResult(projects));

// Assert
Assert.Equal(0, result);
}
[Fact]
public async Task Should_Stop_On_First_Failure_When_ContinueOnError_False()
{
// Arrange
var settings = new BuildSettings("master", new RelativePath("test.sln"), _repository.BasePath, [], [], "dotnet");
var task = new RunDotNetCommandTask(settings, _logger, ["invalid-command"], false, false, CancellationToken.None);
var task = new RunDotNetCommandTask(settings, _logger, ["invalid-command"], false, false, 0, CancellationToken.None);
var projects = new[] { "project1.csproj", "project2.csproj" }.Select(c =>
new AbsolutePath(Path.Combine(_repository.BasePath.Path, c))).ToList();

Expand Down Expand Up @@ -141,7 +168,7 @@ public async Task Should_Execute_Full_Solution_Build()
await File.WriteAllTextAsync(solutionPath.Path, solutionContent);

var task = new RunDotNetCommandTask(settings, _logger, ["build", "-c", "Release", "--nologo"], true,
false, CancellationToken.None);
false, 0, CancellationToken.None);

// Act
var result = await task.Run(new FullSolutionBuildResult(solutionPath));
Expand Down Expand Up @@ -176,7 +203,7 @@ await File.WriteAllTextAsync(projectPath, """
</Project>
""");

var task = new RunDotNetCommandTask(settings, _logger, ["build", "-c", "Release", "--nologo"], true, false, CancellationToken.None);
var task = new RunDotNetCommandTask(settings, _logger, ["build", "-c", "Release", "--nologo"], true, false, 0, CancellationToken.None);

// Act
var result = await task.Run(new FullSolutionBuildResult(solutionPath));
Expand All @@ -185,4 +212,4 @@ await File.WriteAllTextAsync(projectPath, """
Assert.Equal(0, result);
}
}
}
}
9 changes: 7 additions & 2 deletions src/Incrementalist.Tests/Config/ConfigFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public void Config_File_Loading_Success()
SolutionFilePath = "MySolution.sln",
TimeoutMinutes = 5,
Verbose = true,
RunInParallel = true
RunInParallel = true,
ParallelLimit = 5
};

try
Expand All @@ -47,6 +48,7 @@ public void Config_File_Loading_Success()
Assert.Equal(5, loadedConfig.TimeoutMinutes);
Assert.True(loadedConfig.Verbose);
Assert.True(loadedConfig.RunInParallel);
Assert.Equal(5, loadedConfig.ParallelLimit);
Assert.Null(loadedConfig.ListFolders);
Assert.Null(loadedConfig.ContinueOnError);
}
Expand Down Expand Up @@ -129,6 +131,7 @@ public void Merge_Config_With_CommandLine_Options()
TimeoutMinutes = 5,
Verbose = true,
RunInParallel = true,
ParallelLimit = 5,
OutputFile = "output.txt",
SkipGlob = ["**/obj/**", "**/bin/**"],
TargetGlob = ["src/**/*.csproj", "tests/**/*.csproj"]
Expand Down Expand Up @@ -156,6 +159,7 @@ public void Merge_Config_With_CommandLine_Options()
Assert.Equal("output.txt", merged.OutputFile); // From config
Assert.True(merged.Verbose); // From config
Assert.True(merged.RunInParallel); // From config
Assert.Equal(5, merged.ParallelLimit); // From config
Assert.Equivalent(config.SkipGlob, merged.SkipGlobs); // From config
Assert.Equivalent(config.TargetGlob, merged.TargetGlobs); // From config

Expand All @@ -179,6 +183,7 @@ public void Default_Values_Applied_After_Merging()
Assert.False(options.Verbose);
Assert.True(options.ContinueOnError);
Assert.False(options.RunInParallel);
Assert.Equal(0, options.ParallelLimit);
Assert.False(options.FailOnNoProjects);
}

Expand Down Expand Up @@ -262,4 +267,4 @@ public async Task CreateConfigFile_Includes_JsonSchema()
}
}
}
}
}
2 changes: 1 addition & 1 deletion src/Incrementalist.Tests/Config/SlnOptionsParsingSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,4 @@ public void ShouldKeepConfigGlobs()
var merged = ConfigMerger.Merge(result, config);
Assert.Equivalent(expectedGlobs, merged.SkipGlobs);
}
}
}