| /////////////////////////////////////////////////////////////////////////////// |
| // ARGUMENTS |
| /////////////////////////////////////////////////////////////////////////////// |
|
|
| var target = Argument("target", "Default"); |
| var configuration = Argument("configuration", "Debug"); |
| var version = Argument("version", "1.0.0"); |
|
|
| /////////////////////////////////////////////////////////////////////////////// |
| // SETUP / TEARDOWN |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| Setup(ctx => |
| { |
| Information("Running tasks..."); |
| Information($"Configuration: {configuration}"); |
| Information($"Version: {version}"); |
| }); |
| |
| Teardown(ctx => |
| { |
| Information("Finished running tasks."); |
| }); |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TASKS |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| Task("Clean") |
| .Does(() => |
| { |
| CleanDirectories("./src/**/bin"); |
| CleanDirectories("./src/**/obj"); |
| CleanDirectories("./tests/**/bin"); |
| CleanDirectories("./tests/**/obj"); |
| CleanDirectories("./artifacts"); |
| }); |
| |
| Task("Restore") |
| .IsDependentOn("Clean") |
| .Does(() => |
| { |
| DotNetRestore("./RoslynStone.sln"); |
| }); |
| |
| Task("Build") |
| .IsDependentOn("Restore") |
| .Does(() => |
| { |
| DotNetBuild("./RoslynStone.sln", new DotNetBuildSettings |
| { |
| Configuration = configuration, |
| NoRestore = true |
| }); |
| }); |
| |
| Task("Format") |
| .Description("Format code with CSharpier") |
| .Does(() => |
| { |
| StartProcess("csharpier", new ProcessSettings |
| { |
| Arguments = "format ." |
| }); |
| }); |
| |
| Task("Format-Check") |
| .Description("Check code formatting with CSharpier") |
| .Does(() => |
| { |
| var exitCode = StartProcess("csharpier", new ProcessSettings |
| { |
| Arguments = "check ." |
| }); |
| |
| if (exitCode != 0) |
| { |
| throw new Exception("Code formatting issues found. Run 'dotnet cake --target=Format' to fix."); |
| } |
| }); |
| |
| Task("Python-Format") |
| .Description("Format Python code with Ruff") |
| .Does(() => |
| { |
| StartProcess("bash", new ProcessSettings |
| { |
| Arguments = "./scripts/format-python.sh" |
| }); |
| }); |
| |
| Task("Python-Check") |
| .Description("Check Python code quality (Ruff + mypy)") |
| .Does(() => |
| { |
| var exitCode = StartProcess("bash", new ProcessSettings |
| { |
| Arguments = "./scripts/check-python-quality.sh" |
| }); |
| |
| if (exitCode != 0) |
| { |
| throw new Exception("Python quality checks failed. Run 'dotnet cake --target=Python-Format' to fix formatting issues."); |
| } |
| }); |
| |
| Task("Inspect") |
| .Description("Run ReSharper code inspections") |
| .IsDependentOn("Build") |
| .Does(() => |
| { |
| var reportPath = "./artifacts/resharper-report.xml"; |
| EnsureDirectoryExists("./artifacts"); |
| |
| StartProcess("jb", new ProcessSettings |
| { |
| Arguments = $"inspectcode RoslynStone.sln --output={reportPath} --severity=WARNING" |
| }); |
| |
| Information($"ReSharper inspection report generated: {reportPath}"); |
| }); |
| |
| Task("Test") |
| .IsDependentOn("Build") |
| .Does(() => |
| { |
| DotNetTest("./RoslynStone.sln", new DotNetTestSettings |
| { |
| Configuration = configuration, |
| NoRestore = true, |
| NoBuild = true, |
| Loggers = new[] { "console;verbosity=normal" } |
| }); |
| }); |
| |
| Task("Test-Coverage") |
| .Description("Run tests with code coverage") |
| .IsDependentOn("Build") |
| .Does(() => |
| { |
| EnsureDirectoryExists("./artifacts/coverage"); |
| |
| DotNetTest("./RoslynStone.sln", new DotNetTestSettings |
| { |
| Configuration = configuration, |
| NoRestore = true, |
| NoBuild = true, |
| Loggers = new[] { "console;verbosity=normal" }, |
| ArgumentCustomization = args => args |
| .Append("--collect:\"XPlat Code Coverage\"") |
| .Append("--results-directory ./artifacts/coverage") |
| }); |
| |
| // Parse coverage results and check branch coverage |
| var coverageFiles = GetFiles("./artifacts/coverage/**/coverage.cobertura.xml"); |
| if (coverageFiles.Any()) |
| { |
| // Aggregate coverage from all files to get total metrics |
| double totalLinesCovered = 0; |
| double totalLinesValid = 0; |
| double totalBranchesCovered = 0; |
| double totalBranchesValid = 0; |
| |
| foreach (var coverageFile in coverageFiles) |
| { |
| var xml = System.Xml.Linq.XDocument.Load(coverageFile.FullPath); |
| var coverage = xml.Root; |
| |
| if (coverage == null) continue; |
| |
| var linesCoveredAttr = coverage.Attribute("lines-covered"); |
| var linesValidAttr = coverage.Attribute("lines-valid"); |
| var branchesCoveredAttr = coverage.Attribute("branches-covered"); |
| var branchesValidAttr = coverage.Attribute("branches-valid"); |
| |
| if (linesCoveredAttr != null && linesValidAttr != null && |
| branchesCoveredAttr != null && branchesValidAttr != null) |
| { |
| totalLinesCovered += double.Parse(linesCoveredAttr.Value); |
| totalLinesValid += double.Parse(linesValidAttr.Value); |
| totalBranchesCovered += double.Parse(branchesCoveredAttr.Value); |
| totalBranchesValid += double.Parse(branchesValidAttr.Value); |
| } |
| } |
| |
| if (totalLinesValid == 0) |
| { |
| Warning("Unable to parse coverage report: no valid lines found"); |
| return; |
| } |
| |
| var lineCoverage = (totalLinesCovered / totalLinesValid) * 100; |
| var branchCoverage = totalBranchesValid > 0 ? (totalBranchesCovered / totalBranchesValid) * 100 : 0; |
| |
| Information($"Line Coverage: {lineCoverage:F2}% ({totalLinesCovered}/{totalLinesValid} lines)"); |
| Information($"Branch Coverage: {branchCoverage:F2}% ({totalBranchesCovered}/{totalBranchesValid} branches)"); |
| |
| // Enforce minimum coverage thresholds |
| const double MinBranchCoverage = 75.0; |
| const double MinLineCoverage = 80.0; |
| |
| if (branchCoverage < MinBranchCoverage) |
| { |
| Warning($"⚠️ Branch coverage ({branchCoverage:F2}%) is below the minimum threshold of {MinBranchCoverage}%"); |
| // Note: Not failing the build yet to allow incremental improvements |
| // throw new Exception($"Branch coverage ({branchCoverage:F2}%) is below the minimum threshold of {MinBranchCoverage}%"); |
| } |
| else |
| { |
| Information($"✅ Branch coverage meets the minimum threshold"); |
| } |
| |
| if (lineCoverage < MinLineCoverage) |
| { |
| Warning($"⚠️ Line coverage ({lineCoverage:F2}%) is below the minimum threshold of {MinLineCoverage}%"); |
| } |
| else |
| { |
| Information($"✅ Line coverage meets the minimum threshold"); |
| } |
| } |
| }); |
|
|
| Task("Test-Coverage-Report") |
| .Description("Generate HTML coverage report using ReportGenerator") |
| .IsDependentOn("Test-Coverage") |
| .Does(() => |
| { |
| var coverageFiles = GetFiles("./artifacts/coverage/**/coverage.cobertura.xml"); |
| if (coverageFiles.Any()) |
| { |
| EnsureDirectoryExists("./artifacts/coverage-report"); |
| |
| var settings = new ProcessSettings |
| { |
| Arguments = new ProcessArgumentBuilder() |
| .Append($"-reports:{string.Join(";", coverageFiles.Select(f => f.FullPath))}") |
| .Append("-targetdir:./artifacts/coverage-report") |
| .Append("-reporttypes:Html;Badges") |
| }; |
| |
| StartProcess("reportgenerator", settings); |
| Information("Coverage report generated at ./artifacts/coverage-report/index.html"); |
| } |
| else |
| { |
| Warning("No coverage files found to generate report"); |
| } |
| }); |
| |
| Task("Benchmark") |
| .Description("Run benchmarks") |
| .IsDependentOn("Build") |
| .Does(() => |
| { |
| Information("Running benchmarks..."); |
| Information("Note: Benchmarks can take several minutes to complete."); |
| |
| var benchmarkProject = "./tests/RoslynStone.Benchmarks/RoslynStone.Benchmarks.csproj"; |
| |
| DotNetRun(benchmarkProject, new DotNetRunSettings |
| { |
| Configuration = "Release", |
| NoBuild = false, |
| ArgumentCustomization = args => args.Append("--filter * --artifacts ./artifacts/benchmarks") |
| }); |
| |
| Information("Benchmark results saved to ./artifacts/benchmarks"); |
| }); |
| |
| Task("Load-Test") |
| .Description("Run load tests against a running HTTP server") |
| .IsDependentOn("Build") |
| .Does(() => |
| { |
| Information("Running load tests..."); |
| Information("Note: Ensure the API server is running in HTTP mode:"); |
| Information(" cd src/RoslynStone.Api && MCP_TRANSPORT=http dotnet run"); |
| Information(""); |
| |
| var loadTestProject = "./tests/RoslynStone.LoadTests/RoslynStone.LoadTests.csproj"; |
| |
| try |
| { |
| DotNetRun(loadTestProject, new DotNetRunSettings |
| { |
| Configuration = configuration, |
| NoBuild = true |
| }); |
| } |
| catch (Exception ex) |
| { |
| Warning($"Load test failed: {ex.Message}"); |
| Warning("Make sure the server is running with: MCP_TRANSPORT=http dotnet run"); |
| } |
| }); |
|
|
| Task("Pack") |
| .Description("Create NuGet packages") |
| .IsDependentOn("Build") |
| .Does(() => |
| { |
| EnsureDirectoryExists("./artifacts/packages"); |
| |
| var projects = new[] |
| { |
| "./src/RoslynStone.Core/RoslynStone.Core.csproj", |
| "./src/RoslynStone.Infrastructure/RoslynStone.Infrastructure.csproj" |
| }; |
| |
| foreach (var project in projects) |
| { |
| DotNetPack(project, new DotNetPackSettings |
| { |
| Configuration = configuration, |
| OutputDirectory = "./artifacts/packages", |
| NoRestore = true, |
| NoBuild = true, |
| ArgumentCustomization = args => args |
| .Append($"/p:Version={version}") |
| .Append($"/p:PackageVersion={version}") |
| }); |
| } |
| |
| Information($"NuGet packages created in ./artifacts/packages"); |
| }); |
| |
| Task("Publish-NuGet") |
| .Description("Publish NuGet packages to NuGet.org") |
| .IsDependentOn("Pack") |
| .Does(() => |
| { |
| var apiKey = EnvironmentVariable("NUGET_API_KEY"); |
| if (string.IsNullOrEmpty(apiKey)) |
| { |
| throw new Exception("NUGET_API_KEY environment variable not set"); |
| } |
| |
| var packages = GetFiles("./artifacts/packages/*.nupkg"); |
| |
| foreach (var package in packages) |
| { |
| DotNetNuGetPush(package.FullPath, new DotNetNuGetPushSettings |
| { |
| Source = "https://api.nuget.org/v3/index.json", |
| ApiKey = apiKey |
| }); |
| } |
| }); |
| |
| Task("CI") |
| .Description("Run all CI tasks: Format check (C |
| .IsDependentOn("Format-Check") |
| .IsDependentOn("Python-Check") |
| .IsDependentOn("Inspect") |
| .IsDependentOn("Test-Coverage"); |
|
|
| Task("Default") |
| .IsDependentOn("Build") |
| .IsDependentOn("Test"); |
|
|
| /////////////////////////////////////////////////////////////////////////////// |
| // EXECUTION |
| /////////////////////////////////////////////////////////////////////////////// |
|
|
| RunTarget(target); |
|
|