From 262c00ff96ca72d8197a947dd4513b91f69a956d Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sun, 5 May 2024 13:54:41 +1000 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=2016=20fix=20bug=20to=20do=20with?= =?UTF-8?q?=20timestamps=20when=20running=20in=20docker=20(#17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added SydneyTimeProvider * Remove weather component * Removed usages of DateTime.Now * Register SydneyTimeProvider * Added unit tests * Refactored get timesheet to use common utc calculation * Refactored timesheet query to use common UTC logic * Tidy up from review --- DailyScrumGenerator.sln | 7 ++ global.json | 7 +- .../Common/Services/SydneyTimeProvider.cs | 10 +++ src/WebUI/Common/Services/TimeProviderExt.cs | 36 ++++++++++ src/WebUI/Components/Pages/Weather.razor | 66 ------------------- .../DailyScrum/Components/Pages/Debug.razor | 19 ++++++ .../DailyScrum/Components/Pages/Index.razor | 10 ++- .../Features/DailyScrum/DailyScrumFeature.cs | 2 + .../DailyScrum/Infrastructure/GraphService.cs | 7 +- .../DailyScrum/Queries/GetDailyScrumQuery.cs | 37 +++++------ .../Timesheet/Components/Pages/Index.razor | 11 +++- .../Queries/GetTimeSheetNotesQuery.cs | 34 +++++----- tests/IntegrationTests/GraphServiceTests.cs | 9 ++- tests/UnitTests/GlobalUsings.cs | 1 + tests/UnitTests/SydneyTimeProviderTests.cs | 16 +++++ tests/UnitTests/TimeProviderExtTests.cs | 37 +++++++++++ tests/UnitTests/UnitTests.csproj | 30 +++++++++ 17 files changed, 224 insertions(+), 115 deletions(-) create mode 100644 src/WebUI/Common/Services/SydneyTimeProvider.cs create mode 100644 src/WebUI/Common/Services/TimeProviderExt.cs delete mode 100644 src/WebUI/Components/Pages/Weather.razor create mode 100644 src/WebUI/Features/DailyScrum/Components/Pages/Debug.razor create mode 100644 tests/UnitTests/GlobalUsings.cs create mode 100644 tests/UnitTests/SydneyTimeProviderTests.cs create mode 100644 tests/UnitTests/TimeProviderExtTests.cs create mode 100644 tests/UnitTests/UnitTests.csproj diff --git a/DailyScrumGenerator.sln b/DailyScrumGenerator.sln index 804dc7f..c53ae52 100644 --- a/DailyScrumGenerator.sln +++ b/DailyScrumGenerator.sln @@ -16,6 +16,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebUI", "src\WebUI\WebUI.cs EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationTests", "tests\IntegrationTests\IntegrationTests.csproj", "{30037715-AA77-4D4F-B961-8DE69E9B7A28}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "tests\UnitTests\UnitTests.csproj", "{7E4AFE3C-FF99-4E27-8DA5-B0F55F806AA7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -30,9 +32,14 @@ Global {30037715-AA77-4D4F-B961-8DE69E9B7A28}.Debug|Any CPU.Build.0 = Debug|Any CPU {30037715-AA77-4D4F-B961-8DE69E9B7A28}.Release|Any CPU.ActiveCfg = Release|Any CPU {30037715-AA77-4D4F-B961-8DE69E9B7A28}.Release|Any CPU.Build.0 = Release|Any CPU + {7E4AFE3C-FF99-4E27-8DA5-B0F55F806AA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7E4AFE3C-FF99-4E27-8DA5-B0F55F806AA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7E4AFE3C-FF99-4E27-8DA5-B0F55F806AA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7E4AFE3C-FF99-4E27-8DA5-B0F55F806AA7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {351FF09F-D517-4930-8D86-607DB4B7904C} = {92C5999C-A447-479E-8630-064E6FDC78DA} {30037715-AA77-4D4F-B961-8DE69E9B7A28} = {21C89A08-D942-45BE-A302-4EEB543573C0} + {7E4AFE3C-FF99-4E27-8DA5-B0F55F806AA7} = {21C89A08-D942-45BE-A302-4EEB543573C0} EndGlobalSection EndGlobal diff --git a/global.json b/global.json index 989a69c..3639463 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,7 @@ { "sdk": { - "version": "8.0.100", - "rollForward": "latestMinor" + "version": "8.0.0", + "rollForward": "latestFeature", + "allowPrerelease": false } -} \ No newline at end of file +} diff --git a/src/WebUI/Common/Services/SydneyTimeProvider.cs b/src/WebUI/Common/Services/SydneyTimeProvider.cs new file mode 100644 index 0000000..e8c6671 --- /dev/null +++ b/src/WebUI/Common/Services/SydneyTimeProvider.cs @@ -0,0 +1,10 @@ +namespace WebUI.Common.Services; + +public class SydneyTimeProvider : TimeProvider +{ + private const string Timezone = "AUS Eastern Standard Time"; + + private readonly TimeZoneInfo _sydneyTimeZone = TimeZoneInfo.FindSystemTimeZoneById(Timezone); + + public override TimeZoneInfo LocalTimeZone => _sydneyTimeZone; +} diff --git a/src/WebUI/Common/Services/TimeProviderExt.cs b/src/WebUI/Common/Services/TimeProviderExt.cs new file mode 100644 index 0000000..15aee46 --- /dev/null +++ b/src/WebUI/Common/Services/TimeProviderExt.cs @@ -0,0 +1,36 @@ +namespace WebUI.Common.Services; + +public static class TimeProviderExt +{ + public static DateOnly GetToday(this TimeProvider timeProvider) + { + var now = timeProvider.GetLocalNow(); + return DateOnly.FromDateTime(now.Date); + } + + public static DateTime GetStartOfDayUtc(this TimeProvider timeProvider, DateOnly date) + { + var timeZone = timeProvider.LocalTimeZone; + + // Find the start of the day in Sydney time + var startOfDaySydney = date.ToDateTime(TimeOnly.MinValue); + + // Convert the start of the day to UTC + var startOfDayUtc = TimeZoneInfo.ConvertTimeToUtc(startOfDaySydney, timeZone); + + return startOfDayUtc; + } + + public static DateTime GetEndOfDayUtc(this TimeProvider timeProvider, DateOnly date) + { + var timeZone = timeProvider.LocalTimeZone; + + // Find the start of the day in Sydney time + var endOfDaySydney = date.ToDateTime(TimeOnly.MaxValue); + + // Convert the start of the day to UTC + var endOfDayUtc = TimeZoneInfo.ConvertTimeToUtc(endOfDaySydney, timeZone); + + return endOfDayUtc; + } +} \ No newline at end of file diff --git a/src/WebUI/Components/Pages/Weather.razor b/src/WebUI/Components/Pages/Weather.razor deleted file mode 100644 index 570e371..0000000 --- a/src/WebUI/Components/Pages/Weather.razor +++ /dev/null @@ -1,66 +0,0 @@ -@page "/weather" - -Weather - -

Weather

- -

This component demonstrates showing data.

- -@if (forecasts == null) -{ -

- Loading... -

-} -else -{ - - - - - - - - - - - @foreach (var forecast in forecasts) - { - - - - - - - } - -
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
-} - -@code { - private WeatherForecast[]? forecasts; - - protected override async Task OnInitializedAsync() - { - // Simulate asynchronous loading to demonstrate a loading indicator - await Task.Delay(500); - - var startDate = DateOnly.FromDateTime(DateTime.Now); - var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; - forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = startDate.AddDays(index), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = summaries[Random.Shared.Next(summaries.Length)] - }).ToArray(); - } - - private class WeatherForecast - { - public DateOnly Date { get; set; } - public int TemperatureC { get; set; } - public string? Summary { get; set; } - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - } - -} \ No newline at end of file diff --git a/src/WebUI/Features/DailyScrum/Components/Pages/Debug.razor b/src/WebUI/Features/DailyScrum/Components/Pages/Debug.razor new file mode 100644 index 0000000..1a96c3f --- /dev/null +++ b/src/WebUI/Features/DailyScrum/Components/Pages/Debug.razor @@ -0,0 +1,19 @@ +@page "/daily-scrum/debug" +@using Microsoft.AspNetCore.Components.Web +@inject TimeProvider TimeProvider + +Daily Scrum + +

Daily Scrum - Debug

+ +

Now - @_now

+ +@code { + private DateTimeOffset _now; + + protected override void OnInitialized() + { + _now = TimeProvider.GetLocalNow(); + } + +} diff --git a/src/WebUI/Features/DailyScrum/Components/Pages/Index.razor b/src/WebUI/Features/DailyScrum/Components/Pages/Index.razor index 2abeda7..beaee48 100644 --- a/src/WebUI/Features/DailyScrum/Components/Pages/Index.razor +++ b/src/WebUI/Features/DailyScrum/Components/Pages/Index.razor @@ -2,7 +2,9 @@ @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Forms @using System.ComponentModel.DataAnnotations +@using WebUI.Common.Services @inject NavigationManager NavigationManager +@inject TimeProvider TimeProvider Daily Scrum @@ -54,6 +56,12 @@ // https://github.com/dotnet/aspnetcore/issues/52195 public string ClientDays { get; set; } = String.Empty; - public DateOnly LastWorkingDay { get; set; } = DateOnly.FromDateTime(DateTime.Now.AddDays(-1)); + public DateOnly LastWorkingDay { get; set; } + } + + protected override void OnInitialized() + { + if (Model.LastWorkingDay == default) + Model.LastWorkingDay = TimeProvider.GetToday().AddDays(-1); } } diff --git a/src/WebUI/Features/DailyScrum/DailyScrumFeature.cs b/src/WebUI/Features/DailyScrum/DailyScrumFeature.cs index a0f3e29..e487626 100644 --- a/src/WebUI/Features/DailyScrum/DailyScrumFeature.cs +++ b/src/WebUI/Features/DailyScrum/DailyScrumFeature.cs @@ -1,4 +1,5 @@ using WebUI.Common.Features; +using WebUI.Common.Services; using WebUI.Features.DailyScrum.Infrastructure; namespace WebUI.Features.DailyScrum; @@ -10,5 +11,6 @@ public static void ConfigureServices(IServiceCollection services, IConfiguration //services.AddOptionsWithValidation(MicrosoftGraphOptions.Section); //services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } diff --git a/src/WebUI/Features/DailyScrum/Infrastructure/GraphService.cs b/src/WebUI/Features/DailyScrum/Infrastructure/GraphService.cs index aa1da87..e5021bf 100644 --- a/src/WebUI/Features/DailyScrum/Infrastructure/GraphService.cs +++ b/src/WebUI/Features/DailyScrum/Infrastructure/GraphService.cs @@ -15,10 +15,12 @@ public interface IGraphService public class GraphService : IGraphService { private readonly ICurrentUserService _currentUserService; + private readonly ILogger _logger; - public GraphService(ICurrentUserService currentUserService) + public GraphService(ICurrentUserService currentUserService, ILogger logger) { _currentUserService = currentUserService; + _logger = logger; } // public async Task?> GetTodoLists() @@ -46,6 +48,8 @@ public GraphService(ICurrentUserService currentUserService) public async Task> GetTasks(DateTime utcStart, DateTime utcEnd) { + _logger.LogInformation("Getting tasks from {UtcStart} to {UtcEnd}", utcStart, utcEnd); + var graphClient = GetGraphServiceClient(); // NOTE: SHOULD be able to use OData to expand the child tasks, but I haven't been able to get this to work @@ -55,7 +59,6 @@ public async Task> GetTasks(DateTime utcStart, DateTime utcEnd) var tasks = new Dictionary>(); - foreach (var list in lists.Value) { var task = graphClient.Me.Todo diff --git a/src/WebUI/Features/DailyScrum/Queries/GetDailyScrumQuery.cs b/src/WebUI/Features/DailyScrum/Queries/GetDailyScrumQuery.cs index b076b9d..d8af2a1 100644 --- a/src/WebUI/Features/DailyScrum/Queries/GetDailyScrumQuery.cs +++ b/src/WebUI/Features/DailyScrum/Queries/GetDailyScrumQuery.cs @@ -1,4 +1,5 @@ using MediatR; +using WebUI.Common.Services; using WebUI.Common.ViewModels; using WebUI.Features.DailyScrum.Infrastructure; @@ -10,10 +11,14 @@ public record GetDailyScrumQuery(string Name, int? ClientDays, DateOnly? LastWor public class GetDailyScrumQueryHandler : IRequestHandler { private readonly IGraphService _graphService; + private readonly TimeProvider _timeProvider; + private readonly ILogger _logger; - public GetDailyScrumQueryHandler(IGraphService graphService) + public GetDailyScrumQueryHandler(IGraphService graphService, TimeProvider timeProvider, ILogger logger) { _graphService = graphService; + _timeProvider = timeProvider; + _logger = logger; } public async Task Handle(GetDailyScrumQuery request, CancellationToken cancellationToken) @@ -22,7 +27,9 @@ public async Task Handle(GetDailyScrumQuery request, Cancel var userSummary = await GetUserSummary(request.ClientDays); - var today = GetToday(); + var today = _timeProvider.GetToday(); + _logger.LogInformation("Getting projects for {Today}", today); + var todaysProjects = await GetProjects(today); var yesterday = GetLastWorkingDay(request.LastWorkingDay); @@ -52,7 +59,11 @@ private async Task GetUserSummary(int? clientDays) // TODO: Consider refactoring into a common service private async Task> GetProjects(DateOnly date) { - var (startOfDayUtc, endOfDayUtc) = GetTimeStamps(date); + // TODO: This is not returning the correct timestamps on local vs docker + var startOfDayUtc = _timeProvider.GetStartOfDayUtc(date); + var endOfDayUtc = _timeProvider.GetEndOfDayUtc(date); + + _logger.LogInformation("Getting projects for {Date} ({StartOfDayUtc} to {EndOfDayUtc})", date, startOfDayUtc, endOfDayUtc); var graphTasks = await _graphService.GetTasks(startOfDayUtc, endOfDayUtc); @@ -91,24 +102,6 @@ private EmailViewModel GetEmail(string name) }; } - // TODO: Consider refactoring into a common service - private (DateTime StartOfDayUtc, DateTime EndOfDayUtc) GetTimeStamps(DateOnly localDate) - { - // Find the start of the day - var startOfDayLocal = localDate.ToDateTime(TimeOnly.MinValue); - - // Find the end of the day - var endOfDayLocal = localDate.ToDateTime(TimeOnly.MaxValue); - - // Convert to UTC - var startOfDayUtc = startOfDayLocal.ToUniversalTime(); - var endOfDayUtc = endOfDayLocal.ToUniversalTime(); - - return (startOfDayUtc, endOfDayUtc); - } - - private DateOnly GetToday() => DateOnly.FromDateTime(DateTime.Now); - private DateOnly GetLastWorkingDay(DateOnly? lastWorkingDay) => - lastWorkingDay ?? DateOnly.FromDateTime(DateTime.Now.AddDays(-1)); + lastWorkingDay ?? _timeProvider.GetToday().AddDays(-1); } diff --git a/src/WebUI/Features/Timesheet/Components/Pages/Index.razor b/src/WebUI/Features/Timesheet/Components/Pages/Index.razor index 61df402..aa6095a 100644 --- a/src/WebUI/Features/Timesheet/Components/Pages/Index.razor +++ b/src/WebUI/Features/Timesheet/Components/Pages/Index.razor @@ -2,7 +2,9 @@ @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Forms @using System.ComponentModel.DataAnnotations +@using WebUI.Common.Services @inject NavigationManager NavigationManager +@inject TimeProvider TimeProvider Timesheet Notes @@ -37,6 +39,13 @@ public class InputModel { [Required] - public DateOnly Date { get; set; } = DateOnly.FromDateTime(DateTime.Now); + public DateOnly Date { get; set; } } + + protected override void OnInitialized() + { + if (Model.Date == default) + Model.Date = TimeProvider.GetToday(); + } + } diff --git a/src/WebUI/Features/Timesheet/Queries/GetTimeSheetNotesQuery.cs b/src/WebUI/Features/Timesheet/Queries/GetTimeSheetNotesQuery.cs index 91e4eea..5fd4952 100644 --- a/src/WebUI/Features/Timesheet/Queries/GetTimeSheetNotesQuery.cs +++ b/src/WebUI/Features/Timesheet/Queries/GetTimeSheetNotesQuery.cs @@ -1,23 +1,34 @@ using MediatR; +using WebUI.Common.Services; using WebUI.Common.ViewModels; using WebUI.Features.DailyScrum.Infrastructure; using WebUI.Features.DailyScrum.Queries; namespace WebUI.Features.Timesheet.Queries; +// How should the time calculation work? +// - user enters a date +// - we assume that that date is in Sydney time +// public record GetTimeSheetNotesQuery(DateOnly Date) : IRequest; public class GetTimeSheetNotesQueryHandler : IRequestHandler { private readonly IGraphService _graphService; + private readonly TimeProvider _timeProvider; + private readonly ILogger _logger; - public GetTimeSheetNotesQueryHandler(IGraphService graphService) + public GetTimeSheetNotesQueryHandler(IGraphService graphService, TimeProvider timeProvider, ILogger logger) { _graphService = graphService; + _timeProvider = timeProvider; + _logger = logger; } public async Task Handle(GetTimeSheetNotesQuery request, CancellationToken cancellationToken) { + _logger.LogInformation("Getting timesheet notes for {Date}", request.Date); + var projects = await GetProjects(request.Date); return new TimesheetViewModel @@ -29,7 +40,10 @@ public async Task Handle(GetTimeSheetNotesQuery request, Can // TODO: Consider refactoring into a common service private async Task> GetProjects(DateOnly date) { - var (startOfDayUtc, endOfDayUtc) = GetTimeStamps(date); + var startOfDayUtc = _timeProvider.GetStartOfDayUtc(date); + var endOfDayUtc = _timeProvider.GetEndOfDayUtc(date); + + _logger.LogInformation("Getting projects for {Date} ({StartOfDayUtc} to {EndOfDayUtc})", date, startOfDayUtc, endOfDayUtc); var graphTasks = await _graphService.GetTasks(startOfDayUtc, endOfDayUtc); @@ -46,20 +60,4 @@ private async Task> GetProjects(DateOnly date) return projects; } - - // TODO: Consider refactoring into a common service - private (DateTime StartOfDayUtc, DateTime EndOfDayUtc) GetTimeStamps(DateOnly localDate) - { - // Find the start of the day - var startOfDayLocal = localDate.ToDateTime(TimeOnly.MinValue); - - // Find the end of the day - var endOfDayLocal = localDate.ToDateTime(TimeOnly.MaxValue); - - // Convert to UTC - var startOfDayUtc = startOfDayLocal.ToUniversalTime(); - var endOfDayUtc = endOfDayLocal.ToUniversalTime(); - - return (startOfDayUtc, endOfDayUtc); - } } diff --git a/tests/IntegrationTests/GraphServiceTests.cs b/tests/IntegrationTests/GraphServiceTests.cs index 4137135..9bd2b15 100644 --- a/tests/IntegrationTests/GraphServiceTests.cs +++ b/tests/IntegrationTests/GraphServiceTests.cs @@ -1,6 +1,8 @@ using FluentAssertions; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using WebUI.Common.Identity; +using WebUI.Common.Services; using WebUI.Features.DailyScrum.Infrastructure; namespace IntegrationTests; @@ -24,7 +26,8 @@ public async Task CanGetTodaysTasks() { // Arrange var sut = CreateGraphService(); - var today = DateOnly.FromDateTime(DateTime.Now); + var timeProvider = new SydneyTimeProvider(); + var today = timeProvider.GetToday(); var startOfDayLocal = today.ToDateTime(TimeOnly.MinValue); var endOfDayLocal = today.ToDateTime(TimeOnly.MaxValue); var startOfDayUtc = startOfDayLocal.ToUniversalTime(); @@ -51,7 +54,9 @@ private GraphService CreateGraphService() { var userService = new CurrentUserService(); userService.UpdateAccessToken(_accessToken); - var service = new GraphService(userService); + var timeProvider = new SydneyTimeProvider(); + var logger = new Logger(new LoggerFactory()); + var service = new GraphService(userService, logger); return service; } } diff --git a/tests/UnitTests/GlobalUsings.cs b/tests/UnitTests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/tests/UnitTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/tests/UnitTests/SydneyTimeProviderTests.cs b/tests/UnitTests/SydneyTimeProviderTests.cs new file mode 100644 index 0000000..4f49781 --- /dev/null +++ b/tests/UnitTests/SydneyTimeProviderTests.cs @@ -0,0 +1,16 @@ +using FluentAssertions; +using WebUI.Common.Services; + +namespace UnitTests; + +public class SydneyTimeProviderTests +{ + [Fact] + public void GetLocalNow_ReturnsSydneyTime() + { + var sut = new SydneyTimeProvider(); + var localNow = DateTimeOffset.Now; + var result = sut.GetLocalNow(); + result.Should().BeCloseTo(localNow, precision: TimeSpan.FromSeconds(5)); + } +} diff --git a/tests/UnitTests/TimeProviderExtTests.cs b/tests/UnitTests/TimeProviderExtTests.cs new file mode 100644 index 0000000..72cfa2c --- /dev/null +++ b/tests/UnitTests/TimeProviderExtTests.cs @@ -0,0 +1,37 @@ +using FluentAssertions; +using WebUI.Common.Services; + +namespace UnitTests; + +public class TimeProviderExtTests +{ + [Fact] + public void GetStartOfDayUtc_GivenSydneyTimeZone_ReturnsCorrectUtc() + { + var sut = new SydneyTimeProvider(); + var date = new DateOnly(2024, 5, 5); + var expected = new DateTime(2024, 5, 4, 14, 0, 0, DateTimeKind.Utc); + + var result = sut.GetStartOfDayUtc(date); + + result.Should().Be(expected); + } + + [Fact] + public void GetEndOfDayUtc_GivenSydneyTimeZone_ReturnsCorrectUtc() + { + var sut = new SydneyTimeProvider(); + var date = new DateOnly(2024, 5, 5); + var expected = new DateTime(2024, 5, 5, 13, 59, 59, DateTimeKind.Utc); + + var result = sut.GetEndOfDayUtc(date); + + result.Should().BeCloseTo(expected, TimeSpan.FromSeconds(1)); + } + + + + + + +} \ No newline at end of file diff --git a/tests/UnitTests/UnitTests.csproj b/tests/UnitTests/UnitTests.csproj new file mode 100644 index 0000000..ec27f03 --- /dev/null +++ b/tests/UnitTests/UnitTests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + +