diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..f394285 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,55 @@ +name: CI - Build, Test, SonarCloud + +on: + push: + branches: [ feature/sonarcloud-integration, master ] + pull_request: + branches: [ feature/sonarcloud-integration, master ] + +jobs: + build-and-analyze: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: "8.0.x" + + - name: Restore dependencies + run: dotnet restore + + - name: Install SonarScanner + run: | + dotnet tool install --global dotnet-sonarscanner --version 5.14.0 || true + echo "$HOME/.dotnet/tools" >> $GITHUB_PATH + + - name: SonarCloud - Begin Analysis + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + dotnet sonarscanner begin \ + /k:"ubaidali247_bp" \ + /o:"ubaidali247" \ + /d:sonar.login="${SONAR_TOKEN}" \ + /d:sonar.host.url="https://sonarcloud.io" \ + /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" + + - name: Clean + run: dotnet clean + + - name: Build + run: dotnet build --configuration Release --no-restore + + - name: Test and Generate Coverage + run: dotnet test tests/BPCalculator.Tests/BPCalculator.Tests.csproj --verbosity normal /p:CollectCoverage=true /p:CoverletOutput=coverage.opencover.xml /p:CoverletOutputFormat=opencover + + - name: SonarCloud - End Analysis + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: dotnet sonarscanner end /d:sonar.login="${SONAR_TOKEN}" diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 0000000..2bf584c --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,55 @@ +name: CI - Build, Test, SonarCloud + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + build-and-analyze: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: "8.0.x" + + - name: Restore dependencies + run: dotnet restore + + - name: Install SonarScanner + run: | + dotnet tool install --global dotnet-sonarscanner --version 5.14.0 || true + echo "$HOME/.dotnet/tools" >> $GITHUB_PATH + + - name: SonarCloud - Begin Analysis + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + dotnet sonarscanner begin \ + /k:"ubaidali247_bp" \ + /o:"ubaidali247" \ + /d:sonar.login="${SONAR_TOKEN}" \ + /d:sonar.host.url="https://sonarcloud.io" \ + /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" + + - name: Clean + run: dotnet clean + + - name: Build + run: dotnet build --configuration Release --no-restore + + - name: Test and Generate Coverage + run: dotnet test tests/BPCalculator.Tests/BPCalculator.Tests.csproj --verbosity normal /p:CollectCoverage=true /p:CoverletOutput=coverage.opencover.xml /p:CoverletOutputFormat=opencover + + - name: SonarCloud - End Analysis + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: dotnet sonarscanner end /d:sonar.login="${SONAR_TOKEN}" diff --git a/.gitignore b/.gitignore index 3c4efe2..a7d4e90 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,53 @@ +# Build outputs +bin/ +obj/ + +# User-specific files +.vs/ +.vscode/ +.idea/ +*.user +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# NuGet +*.nupkg +*.snupkg +**/packages/** +*.nuget.props +*.nuget.targets + +# Logs +*.log +logs/ + +# Tooling caches +.dotnet/ +.templateengine/ + +# Test results & coverage +TestResults/ +coverage/ + +# Generated files +*.generated.cs +*.g.cs +*.g.i.cs + +# Publish output +wwwroot/lib/ + +# Rider / JetBrains +.fleet/ +.idea/ +.ionide/ + +# Azure functions local settings +local.settings.json ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. diff --git a/BPCalculator.sln b/BPCalculator.sln index 652d73a..050f486 100644 --- a/BPCalculator.sln +++ b/BPCalculator.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.30002.166 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BPCalculator", "BPCalculator\BPCalculator.csproj", "{5966DA6C-819D-4303-ADC7-425B8541EC84}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BPCalculator.Tests", "tests\BPCalculator.Tests\BPCalculator.Tests.csproj", "{A74C6C1D-7E2B-4A9A-B2F6-022F8D4AB9AF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {5966DA6C-819D-4303-ADC7-425B8541EC84}.Debug|Any CPU.Build.0 = Debug|Any CPU {5966DA6C-819D-4303-ADC7-425B8541EC84}.Release|Any CPU.ActiveCfg = Release|Any CPU {5966DA6C-819D-4303-ADC7-425B8541EC84}.Release|Any CPU.Build.0 = Release|Any CPU + {A74C6C1D-7E2B-4A9A-B2F6-022F8D4AB9AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A74C6C1D-7E2B-4A9A-B2F6-022F8D4AB9AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A74C6C1D-7E2B-4A9A-B2F6-022F8D4AB9AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A74C6C1D-7E2B-4A9A-B2F6-022F8D4AB9AF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/BPCalculator/BPCalculator.csproj b/BPCalculator/BPCalculator.csproj index 3cd1336..5e4881f 100644 --- a/BPCalculator/BPCalculator.csproj +++ b/BPCalculator/BPCalculator.csproj @@ -1,7 +1,7 @@ - net9.0 + net8.0 diff --git a/BPCalculator/BloodPressure.cs b/BPCalculator/BloodPressure.cs index 3e8fee9..ebebdfb 100644 --- a/BPCalculator/BloodPressure.cs +++ b/BPCalculator/BloodPressure.cs @@ -31,9 +31,22 @@ public BPCategory Category { get { - // implement as part of project - //throw new NotImplementedException("not implemented yet"); - return new BPCategory(); // replace this + if (Systolic >= 140 || Diastolic >= 90) + { + return BPCategory.High; + } + + if (Systolic >= 120 || Diastolic >= 80) + { + return BPCategory.PreHigh; + } + + if (Systolic >= 90 && Diastolic >= 60) + { + return BPCategory.Ideal; + } + + return BPCategory.Low; } } } diff --git a/BPCalculator/Pages/Index.cshtml.cs b/BPCalculator/Pages/Index.cshtml.cs index f96dc16..a9bd325 100644 --- a/BPCalculator/Pages/Index.cshtml.cs +++ b/BPCalculator/Pages/Index.cshtml.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using BPCalculator.Services; // page model @@ -7,6 +8,13 @@ namespace BPCalculator.Pages { public class BloodPressureModel : PageModel { + private readonly ITelemetryService _telemetry; + + public BloodPressureModel(ITelemetryService telemetry) + { + _telemetry = telemetry; + } + [BindProperty] // bound on POST public BloodPressure BP { get; set; } @@ -24,6 +32,11 @@ public IActionResult OnPost() { ModelState.AddModelError("", "Systolic must be greater than Diastolic"); } + + if (ModelState.IsValid) + { + _telemetry.TrackCalculation(BP); + } return Page(); } } diff --git a/BPCalculator/Services/TelemetryService.cs b/BPCalculator/Services/TelemetryService.cs new file mode 100644 index 0000000..0d02370 --- /dev/null +++ b/BPCalculator/Services/TelemetryService.cs @@ -0,0 +1,39 @@ +using System.Diagnostics; +using Microsoft.Extensions.Logging; + +namespace BPCalculator.Services +{ + public interface ITelemetryService + { + void TrackCalculation(BloodPressure reading); + } + + public sealed class TelemetryService : ITelemetryService + { + private static readonly ActivitySource ActivitySource = new("BPCalculator.Telemetry"); + private readonly ILogger _logger; + + public TelemetryService(ILogger logger) + { + _logger = logger; + } + + public void TrackCalculation(BloodPressure reading) + { + if (reading == null) + { + return; + } + + var category = reading.Category; + + using var activity = ActivitySource.StartActivity("BloodPressureCalculation"); + activity?.SetTag("bp.systolic", reading.Systolic); + activity?.SetTag("bp.diastolic", reading.Diastolic); + activity?.SetTag("bp.category", category.ToString()); + + _logger.LogInformation("Telemetry: {Systolic}/{Diastolic} => {Category}", reading.Systolic, reading.Diastolic, category); + } + } +} + diff --git a/BPCalculator/Startup.cs b/BPCalculator/Startup.cs index 14ab241..2f52a86 100644 --- a/BPCalculator/Startup.cs +++ b/BPCalculator/Startup.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using BPCalculator.Services; namespace BPCalculator { @@ -22,6 +23,7 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + services.AddSingleton(); services.AddRazorPages(); } diff --git a/tests/BPCalculator.Tests/BPCalculator.Tests.csproj b/tests/BPCalculator.Tests/BPCalculator.Tests.csproj new file mode 100644 index 0000000..7c4ce8f --- /dev/null +++ b/tests/BPCalculator.Tests/BPCalculator.Tests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/tests/BPCalculator.Tests/BloodPressureCategoryTests.cs b/tests/BPCalculator.Tests/BloodPressureCategoryTests.cs new file mode 100644 index 0000000..4f55c36 --- /dev/null +++ b/tests/BPCalculator.Tests/BloodPressureCategoryTests.cs @@ -0,0 +1,21 @@ +using BPCalculator; + +namespace BPCalculator.Tests; + +public class BloodPressureCategoryTests +{ + [Theory] + [InlineData(140, 60, BPCategory.High)] + [InlineData(120, 90, BPCategory.High)] + [InlineData(139, 89, BPCategory.PreHigh)] + [InlineData(120, 79, BPCategory.PreHigh)] + [InlineData(119, 79, BPCategory.Ideal)] + [InlineData(90, 60, BPCategory.Ideal)] + [InlineData(89, 59, BPCategory.Low)] + public void Category_Computes_As_Expected(int sys, int dia, BPCategory expected) + { + var bp = new BloodPressure { Systolic = sys, Diastolic = dia }; + Assert.Equal(expected, bp.Category); + } +} + diff --git a/tests/BPCalculator.Tests/TelemetryServiceTests.cs b/tests/BPCalculator.Tests/TelemetryServiceTests.cs new file mode 100644 index 0000000..dfd04fd --- /dev/null +++ b/tests/BPCalculator.Tests/TelemetryServiceTests.cs @@ -0,0 +1,30 @@ +using BPCalculator; +using BPCalculator.Services; +using Microsoft.Extensions.Logging; + +namespace BPCalculator.Tests; + +public class TelemetryServiceTests +{ + private static ILogger CreateLogger() => + LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Information)) + .CreateLogger(); + + [Fact] + public void TrackCalculation_Ignores_Null() + { + var telemetry = new TelemetryService(CreateLogger()); + + telemetry.TrackCalculation(null); + } + + [Fact] + public void TrackCalculation_Works_For_Valid_Reading() + { + var telemetry = new TelemetryService(CreateLogger()); + var reading = new BloodPressure { Systolic = 120, Diastolic = 80 }; + + telemetry.TrackCalculation(reading); + } +} + diff --git a/tests/BPCalculator.Tests/coverage.opencover.xml b/tests/BPCalculator.Tests/coverage.opencover.xml new file mode 100644 index 0000000..b36c4bf --- /dev/null +++ b/tests/BPCalculator.Tests/coverage.opencover.xml @@ -0,0 +1,550 @@ + + + + + + BPCalculator.dll + 2025-12-11T02:31:59 + BPCalculator + + + + + + + + + + + + + + + + + BPCalculator.BloodPressure + + + + + System.Int32 BPCalculator.BloodPressure::get_Systolic() + + + + + + + + + + + System.Int32 BPCalculator.BloodPressure::get_Diastolic() + + + + + + + + + + + BPCalculator.BPCategory BPCalculator.BloodPressure::get_Category() + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BPCalculator.Program + + + + + System.Void BPCalculator.Program::Main(System.String[]) + + + + + + + + + + + + + Microsoft.Extensions.Hosting.IHostBuilder BPCalculator.Program::CreateHostBuilder(System.String[]) + + + + + + + + + + + + + + + + BPCalculator.Startup + + + + + Microsoft.Extensions.Configuration.IConfiguration BPCalculator.Startup::get_Configuration() + + + + + + + + + + + System.Void BPCalculator.Startup::ConfigureServices(Microsoft.Extensions.DependencyInjection.IServiceCollection) + + + + + + + + + + + + + + System.Void BPCalculator.Startup::Configure(Microsoft.AspNetCore.Builder.IApplicationBuilder,Microsoft.AspNetCore.Hosting.IWebHostEnvironment) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + System.Void BPCalculator.Startup::.ctor(Microsoft.Extensions.Configuration.IConfiguration) + + + + + + + + + + + + + + + BPCalculator.Services.TelemetryService + + + + + System.Void BPCalculator.Services.TelemetryService::TrackCalculation(BPCalculator.BloodPressure) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + System.Void BPCalculator.Services.TelemetryService::.ctor(Microsoft.Extensions.Logging.ILogger`1<BPCalculator.Services.TelemetryService>) + + + + + + + + + + + + + + System.Void BPCalculator.Services.TelemetryService::.cctor() + + + + + + + + + + + + BPCalculator.Pages.ErrorModel + + + + + System.String BPCalculator.Pages.ErrorModel::get_RequestId() + + + + + + + + + + + System.Boolean BPCalculator.Pages.ErrorModel::get_ShowRequestId() + + + + + + + + + + + System.Void BPCalculator.Pages.ErrorModel::OnGet() + + + + + + + + + + + + + + + + + + System.Void BPCalculator.Pages.ErrorModel::.ctor(Microsoft.Extensions.Logging.ILogger`1<BPCalculator.Pages.ErrorModel>) + + + + + + + + + + + + + + + BPCalculator.Pages.BloodPressureModel + + + + + BPCalculator.BloodPressure BPCalculator.Pages.BloodPressureModel::get_BP() + + + + + + + + + + + System.Void BPCalculator.Pages.BloodPressureModel::OnGet() + + + + + + + + + + + + + Microsoft.AspNetCore.Mvc.IActionResult BPCalculator.Pages.BloodPressureModel::OnPost() + + + + + + + + + + + + + + + + + + + + + + + + + + System.Void BPCalculator.Pages.BloodPressureModel::.ctor(BPCalculator.Services.ITelemetryService) + + + + + + + + + + + + + + + BPCalculator.Pages.PrivacyModel + + + + + System.Void BPCalculator.Pages.PrivacyModel::OnGet() + + + + + + + + + + + + System.Void BPCalculator.Pages.PrivacyModel::.ctor(Microsoft.Extensions.Logging.ILogger`1<BPCalculator.Pages.PrivacyModel>) + + + + + + + + + + + + + + + BPCalculator.Pages.Pages_Error/<ExecuteAsync>d__0 + + + + + System.Void BPCalculator.Pages.Pages_Error/<ExecuteAsync>d__0::MoveNext() + + + + + + + + + + + + + + + + + + BPCalculator.Pages.Pages_Index/<<ExecuteAsync>b__17_0>d + + + + + System.Void BPCalculator.Pages.Pages_Index/<<ExecuteAsync>b__17_0>d::MoveNext() + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BPCalculator.Pages.Pages_Index/<<ExecuteAsync>b__17_1>d + + + + + System.Void BPCalculator.Pages.Pages_Index/<<ExecuteAsync>b__17_1>d::MoveNext() + + + + + + + + + + + + BPCalculator.Pages.Pages_Index/<ExecuteAsync>d__17 + + + + + System.Void BPCalculator.Pages.Pages_Index/<ExecuteAsync>d__17::MoveNext() + + + + + + + + + + + + + + + BPCalculator.Pages.Pages_Privacy/<ExecuteAsync>d__0 + + + + + System.Void BPCalculator.Pages.Pages_Privacy/<ExecuteAsync>d__0::MoveNext() + + + + + + + + + + + + BPCalculator.Pages.Pages__ViewStart/<ExecuteAsync>d__0 + + + + + System.Void BPCalculator.Pages.Pages__ViewStart/<ExecuteAsync>d__0::MoveNext() + + + + + + + + + + + + + \ No newline at end of file