From f2b24e9d2f287c6ce36aeb172fc933c2b0057c1d Mon Sep 17 00:00:00 2001 From: operate-services-sdk-bot Date: Wed, 2 Aug 2023 21:14:24 +0000 Subject: [PATCH] Release v1.0.0 --- CHANGELOG.md | 17 + Third Party Notices.md | 27 ++ .../CliDeploymentDefinitionServiceTests.cs | 440 ++++++++++++++++++ ...efinitionFileIntersectionExceptionTests.cs | 61 +++ .../DeploymentDefinitionFileServiceTests.cs | 180 +++++++ .../Handlers/DeployHandlerTests.cs | 233 +++++++++- .../Handlers/FetchHandlerTests.cs | 240 +++++++++- .../Model/DeploymentResultTests.cs | 29 ++ .../Model/TableOutput/TableTests.cs | 59 +++ .../Service/DeployFileServiceTests.cs | 106 ++--- .../DeployModule.cs | 10 +- .../CliDeploymentDefinition.cs | 22 + .../CliDeploymentDefinitionService.cs | 188 ++++++++ .../DeploymentDefinitionFactory.cs | 23 + ...mentDefinitionFileIntersectionException.cs | 39 ++ .../DeploymentDefinitionFileService.cs | 88 ++++ .../DeploymentDefinitionFiles.cs | 28 ++ .../DeploymentDefinitionFilteringResult.cs | 33 ++ .../DeploymentDefinitionInputResult.cs | 17 + .../ICliDeploymentDefinitionService.cs | 11 + .../IDeploymentDefinitionFactory.cs | 8 + .../IDeploymentDefinitionFileService.cs | 11 + .../IDeploymentDefinitionFiles.cs | 11 + .../IDeploymentDefinitionFilteringResult.cs | 8 + ...eploymentDefinitionInDirectoryException.cs | 14 + .../FetchModule.cs | 5 +- .../Handlers/DeployHandler.cs | 83 +++- .../Handlers/FetchHandler.cs | 102 +++- .../Model/AuthorResult.cs | 18 +- .../Model/Statuses.cs | 3 + .../Model/TableOutput/RowContent.cs | 32 ++ .../Model/TableOutput/TableContent.cs | 74 +++ .../Service/DeployFileService.cs | 62 +-- .../Service/IDeployFileService.cs | 4 +- .../Service/IFetchService.cs | 3 +- .../Unity.Services.Cli.Authoring.csproj | 1 + .../Fetch/JavaScriptFetchServiceTests.cs | 85 +++- .../CloudCodeModuleTests.cs | 7 +- .../Deploy/CloudCodeAuthoringLoggerTests.cs | 12 +- .../CustomCloudCodeDeploymentHandlerTests.cs | 2 - .../ExposeCliCloudCodeDeploymentHandler.cs | 3 +- .../Mock/MockCloudCodeDeploymentHandler.cs | 3 +- .../Authoring/Fetch/JavaScriptFetchService.cs | 31 +- .../CloudCodeModule.cs | 3 - .../Deploy/CliCloudCodeDeploymentHandler.cs | 3 +- .../Deploy/CloudCodeAuthoringLogger.cs | 3 - .../Unity.Services.Cli.CloudCode.csproj | 2 +- .../Exceptions/ExceptionHelperTests.cs | 6 +- .../Telemetry/AnalyticsEventBuilderTests.cs | 98 ++++ .../Unity.Services.Cli.Common.UnitTest.csproj | 1 + .../Exceptions/ExceptionHelper.cs | 6 +- .../AnalyticEvent/AnalyticsEventBuilder.cs | 59 +++ .../AnalyticEvent/IAnalyticsEventBuilder.cs | 10 + .../Telemetry/DiagnosticsTagKeys.cs | 10 + .../Telemetry/MetricTagKeys.cs | 14 + .../Telemetry/TagKeys.cs | 6 - .../Unity.Services.Cli.Common.csproj | 3 +- .../GameServerHostingServersApiV1Mock.cs | 2 +- .../Model/ServerGetOutputTests.cs | 2 +- .../Model/ServersItemOutputTests.cs | 2 +- .../Model/ServersOutputTests.cs | 2 +- ...nity.Services.Cli.GameServerHosting.csproj | 2 +- .../GameServerHostingApiMock.cs | 8 +- .../Deploy/CloudCode/CloudCodeDeployTests.cs | 61 ++- .../Leaderboards/LeaderboardDeployTests.cs | 103 ++++ .../RemoteConfig/RemoteConfigDeployTests.cs | 32 +- .../RemoteConfig/RemoteConfigFileContent.cs | 8 +- .../Authoring/DeployTestsFixture.cs | 38 +- .../Authoring/Fetch/CloudCodeFetchTests.cs | 5 +- .../Authoring/Fetch/LeaderboardFetchTests.cs | 182 ++++++++ .../Authoring/Fetch/RemoteConfigFetchTests.cs | 11 +- .../Authoring/FetchTestsFixture.cs | 3 +- .../Common/UgsCliTestCase.cs | 5 + .../GameServerHostingTests.cs | 1 + .../LeaderboardSetupFailureTests.cs | 6 - .../LeaderboardTests/LeaderboardTests.cs | 41 -- .../Deploy/LeaderboardClientTests.cs | 251 ++++++++++ .../LeaderboardDeploymentHandlerTests.cs | 267 +++++++++++ .../LeaderboardDeploymentServiceTests.cs | 88 ++++ .../Deploy/LeaderboardFetchHandlerTests.cs | 243 ++++++++++ .../Deploy/LeaderboardFetchServiceTests.cs | 94 ++++ .../Deploy/LeaderboardsConfigLoaderTests.cs | 69 +++ .../Handlers/CreateLeaderboardHandlerTests.cs | 92 ---- .../Handlers/DeleteLeaderboardHandlerTests.cs | 8 +- .../Handlers/GetLeaderboardHandlerTests.cs | 2 +- .../Handlers/ImportHandlerTests.cs | 2 +- ...ts.cs => LeaderboardExportHandlerTests.cs} | 103 ++-- .../Handlers/ResetLeaderboardHandlerTests.cs | 12 +- .../Handlers/UpdateLeaderboardHandlerTests.cs | 95 ---- .../Service/LeaderboardsServiceTests.cs | 26 +- ....Services.Cli.Leaderboards.UnitTest.csproj | 6 +- .../Deploy/ILeaderboardsConfigLoader.cs | 10 + .../Deploy/LeaderboardConfigFile.cs | 76 +++ .../Deploy/LeaderboardsClient.cs | 221 +++++++++ .../Deploy/LeaderboardsConfigLoader.cs | 62 +++ .../Deploy/LeaderboardsDeploymentService.cs | 62 +++ .../Deploy/LeaderboardsFetchService.cs | 64 +++ .../Deploy/LeaderboardsSerializer.cs | 23 + .../Handlers/CreateLeaderboardHandler.cs | 33 -- .../Handlers/ImportExport/ExportHandler.cs | 12 +- .../Handlers/ImportExport/ImportHandler.cs | 8 +- .../ImportExport/LeaderboardsExporter.cs | 30 +- .../Handlers/UpdateLeaderboardHandler.cs | 34 -- .../IO/FileSystem.cs | 22 + .../LeaderboardsModule.cs | 63 +-- .../Service/ILeaderboardsService.cs | 26 +- .../Service/LeaderboardsService.cs | 50 +- .../Unity.Services.Cli.Leaderboards.csproj | 6 +- .../RemoteConfigDeploymentResultTests.cs | 49 ++ .../Deploy/RemoteConfigFetchServiceTests.cs | 11 +- .../Model/CliRemoteConfigEntryTests.cs | 38 ++ .../Deploy/CliRemoteConfigFetchHandler.cs | 17 + .../Deploy/RemoteConfigDeploymentResult.cs | 48 ++ .../Deploy/RemoteConfigDeploymentService.cs | 4 +- .../Deploy/RemoteConfigFetchResult.cs | 49 ++ .../Deploy/RemoteConfigFetchService.cs | 9 +- .../Deploy/RemoteConfigScriptsLoader.cs | 2 +- .../RemoteConfigModule.cs | 1 + .../Unity.Services.Cli.RemoteConfig.csproj | 2 +- Unity.Services.Cli/Unity.Services.Cli.sln | 11 + .../Unity.Services.Cli/Program.cs | 28 +- .../Unity.Services.Cli.csproj | 4 +- .../Batching/Batching.cs | 87 ++++ .../Deploy/DeployResult.cs | 14 + .../Deploy/ILeaderboardsDeploymentHandler.cs | 15 + .../Deploy/LeaderboardsDeploymentHandler.cs | 140 ++++++ .../Fetch/FetchResult.cs | 14 + .../Fetch/ILeaderboardsFetchHandler.cs | 17 + .../Fetch/LeaderboardsFetchHandler.cs | 169 +++++++ .../IO/IFileSystem.cs | 19 + .../Model/ILeaderboardConfig.cs | 17 + .../Model/LeaderboardComparer.cs | 21 + .../Model/LeaderboardConfig.cs | 100 ++++ .../Model/ResetConfig.cs | 12 + .../Model/SortOrder.cs | 13 + .../Model/Statuses.cs | 20 + .../Model/Strategy.cs | 16 + .../Model/Tier.cs | 8 + .../Model/TieringConfig.cs | 12 + .../Model/UpdateType.cs | 16 + .../Serialization/ILeaderboardSerializer.cs | 9 + .../Service/ILeaderboardsClient.cs | 18 + ...ervices.Leaderboards.Authoring.Core.csproj | 24 + .../DuplicateResourceValidation.cs | 43 ++ .../Deploy/ModuleTemplateDeploymentHandler.cs | 5 +- .../Fetch/ModuleTemplateFetchHandler.cs | 9 +- 146 files changed, 5692 insertions(+), 730 deletions(-) create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/DeploymentDefinition/CliDeploymentDefinitionServiceTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/DeploymentDefinition/DeploymentDefinitionFileIntersectionExceptionTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/DeploymentDefinition/DeploymentDefinitionFileServiceTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/TableOutput/TableTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/CliDeploymentDefinition.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/CliDeploymentDefinitionService.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFactory.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFileIntersectionException.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFileService.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFiles.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFilteringResult.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionInputResult.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/ICliDeploymentDefinitionService.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/IDeploymentDefinitionFactory.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/IDeploymentDefinitionFileService.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/IDeploymentDefinitionFiles.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/IDeploymentDefinitionFilteringResult.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/MultipleDeploymentDefinitionInDirectoryException.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/TableOutput/RowContent.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/TableOutput/TableContent.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Telemetry/AnalyticsEventBuilderTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticsEventBuilder.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/IAnalyticsEventBuilder.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/DiagnosticsTagKeys.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/MetricTagKeys.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/Leaderboards/LeaderboardDeployTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/LeaderboardFetchTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardClientTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardDeploymentHandlerTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardDeploymentServiceTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardFetchHandlerTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardFetchServiceTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardsConfigLoaderTests.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/CreateLeaderboardHandlerTests.cs rename Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/{ExportHandlerTests.cs => LeaderboardExportHandlerTests.cs} (71%) delete mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/UpdateLeaderboardHandlerTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/ILeaderboardsConfigLoader.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardConfigFile.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsClient.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsConfigLoader.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsDeploymentService.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsFetchService.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsSerializer.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/CreateLeaderboardHandler.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/UpdateLeaderboardHandler.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards/IO/FileSystem.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigDeploymentResultTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Model/CliRemoteConfigEntryTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigDeploymentResult.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchResult.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Batching/Batching.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/DeployResult.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/ILeaderboardsDeploymentHandler.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/LeaderboardsDeploymentHandler.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/FetchResult.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/ILeaderboardsFetchHandler.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/LeaderboardsFetchHandler.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/IO/IFileSystem.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/ILeaderboardConfig.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/LeaderboardComparer.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/LeaderboardConfig.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/ResetConfig.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/SortOrder.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Statuses.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Strategy.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Tier.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/TieringConfig.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/UpdateType.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Serialization/ILeaderboardSerializer.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Service/ILeaderboardsClient.cs create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Unity.Services.Leaderboards.Authoring.Core.csproj create mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Validations/DuplicateResourceValidation.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 144e899..673b8b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to UGS CLI will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [1.0.0] - 2023-08-01 + +### Added +- Added Deployment Definitions to the Deploy and Fetch commands. +- Added analytics related to command usage and options used. +- Deploy/Fetch return an array in a table-like format with -json flag enabled. +- Leaderboards now supports the `ugs deploy` and `ugs fetch` commands at the root + - Deploy sends file configurations into the service + - Fetch updates local files based on service configuration +- Leaderboards now supports `new-file`, to create an empty file for leaderboards + +### Changed +- Removed Leaderboards support to `create` and `update` commands. + +### Fixed +- A bug logging an additional error when deploying a file. + ## [1.0.0-beta.6] - 2023-07-10 ### Added diff --git a/Third Party Notices.md b/Third Party Notices.md index ff5cf69..5a82d2f 100644 --- a/Third Party Notices.md +++ b/Third Party Notices.md @@ -553,3 +553,30 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +
+ +Component Name: Glob + +License Type: MIT + +Copyright (c) 2013-2023 [Kevin Thompson and Glob contributors](https://github.com/kthompson/glob/graphs/contributors) + +https://github.com/kthompson/glob + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/DeploymentDefinition/CliDeploymentDefinitionServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/DeploymentDefinition/CliDeploymentDefinitionServiceTests.cs new file mode 100644 index 0000000..40ec472 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/DeploymentDefinition/CliDeploymentDefinitionServiceTests.cs @@ -0,0 +1,440 @@ +using System.Collections.ObjectModel; +using Moq; +using NUnit.Framework; +using Unity.Services.Cli.Authoring.DeploymentDefinition; +using Unity.Services.Cli.Authoring.Service; +using Unity.Services.Deployment.Core.Model; + +namespace Unity.Services.Cli.Authoring.UnitTest.Service; + +[TestFixture] +class CliDeploymentDefinitionServiceTests +{ + Mock m_MockFileService; + CliDeploymentDefinitionService m_DdefService; + + List m_InputDdefs = new(); + List m_AllDdefs = new(); + List m_Files = new(); + List m_Extensions = new(); + + public CliDeploymentDefinitionServiceTests() + { + m_MockFileService = new Mock(); + m_MockFileService + .Setup(fs => fs.GetDeploymentDefinitionsForInput(It.IsAny>())) + .Returns(() => new DeploymentDefinitionInputResult(m_InputDdefs, m_AllDdefs)); + m_DdefService = new CliDeploymentDefinitionService(m_MockFileService.Object); + } + + [SetUp] + public void SetUp() + { + m_Files = new List + { + "path/to/folder/script.js", + "path/to/folder/config.rc", + "path/to/folder/what.ext", + }; + + m_Extensions = new List + { + ".js", + ".rc", + ".ext", + ".ec" + }; + + m_InputDdefs.Clear(); + m_AllDdefs.Clear(); + } + + [Test] + public void GetFilesFromInput_NoDdef() + { + var ddefA = CreateMockDdef("path/to/folder/A.ddef"); + m_AllDdefs.Add(ddefA.Object); + + SetupFileService_ForInput(m_Files, m_Extensions); + + var result = m_DdefService.GetFilesFromInput(m_Files, m_Extensions); + + Assert.AreEqual(4, result.DefinitionFiles.FilesByExtension.Count); + + foreach (var (extension, ddefFiles) in result.DefinitionFiles.FilesByExtension) + { + foreach (var ddefFile in ddefFiles) + { + Assert.IsTrue(m_Files.Contains(ddefFile)); + } + + Assert.IsTrue(m_Extensions.Contains(extension)); + } + } + + static Mock CreateMockDdef(string path) + { + var mockDdef = new Mock(); + mockDdef + .SetupGet(d => d.Name) + .Returns(Path.GetFileName(path)); + mockDdef + .SetupGet(d => d.Path) + .Returns(path); + mockDdef + .SetupGet(d => d.ExcludePaths) + .Returns(new ObservableCollection()); + return mockDdef; + } + + static Mock CreateMockDdef(string path, IEnumerable excludes) + { + var mockDdef = CreateMockDdef(path); + mockDdef + .SetupGet(d => d.ExcludePaths) + .Returns(new ObservableCollection(excludes)); + return mockDdef; + } + + void SetupFileService_ForDdef( + IDeploymentDefinition ddef, + List files, + List extensions) + { + foreach (var extension in extensions) + { + var relevantFiles = files.Where(f => f.EndsWith(extension)).ToList(); + m_MockFileService + .Setup( + fs => fs.ListFilesToDeploy( + files, + extension, + It.IsAny())) + .Returns(relevantFiles); + m_MockFileService + .Setup(fs => fs.GetFilesForDeploymentDefinition(ddef, extension)) + .Returns(relevantFiles); + } + } + + void SetupFileService_ForInput( + List inputPaths, + List extensions) + { + foreach (var extension in extensions) + { + var relevantFiles = inputPaths.Where(f => f.EndsWith(extension)).ToList(); + m_MockFileService + .Setup(fs => fs.ListFilesToDeploy(inputPaths, extension, It.IsAny())) + .Returns(relevantFiles); + } + } + + [Test] + public void GetFilesFromInput_OnlyDdef() + { + var ddefA = CreateMockDdef("path/to/folder/A.ddef"); + m_AllDdefs.Add(ddefA.Object); + m_InputDdefs.Add(ddefA.Object); + + SetupFileService_ForDdef(ddefA.Object, m_Files, m_Extensions); + + var result = m_DdefService.GetFilesFromInput( + new[] + { + ddefA.Object.Path + }, + m_Extensions); + + Assert.AreEqual(4, result.DefinitionFiles.FilesByExtension.Count); + + foreach (var (extension, ddefFiles) in result.DefinitionFiles.FilesByExtension) + { + foreach (var ddefFile in ddefFiles) + { + Assert.IsTrue(m_Files.Contains(ddefFile)); + } + + Assert.IsTrue(m_Extensions.Contains(extension)); + } + } + + [Test] + public void GetFilesFromInput_DdefAndFiles() + { + var otherFiles = new List + { + "path/to/otherFolder/otherScript.js", + "path/to/otherFolder/otherConfig.rc" + }; + + var ddefA = CreateMockDdef("path/to/otherFolder/A.ddef"); + m_AllDdefs.Add(ddefA.Object); + m_InputDdefs.Add(ddefA.Object); + + var input = new List(m_Files) { ddefA.Object.Path }; + SetupFileService_ForDdef(ddefA.Object, otherFiles, m_Extensions); + SetupFileService_ForInput(input, m_Extensions); + + var result = m_DdefService.GetFilesFromInput(input, m_Extensions); + + var flatFilesByExtension = result.AllFilesByExtension + .SelectMany(kvp => kvp.Value) + .ToList(); + Assert.AreEqual(5, flatFilesByExtension.Count); + } + + [Test] + public void GetDeploymentDefinitionFiles_RespectsNestedDeploymentDefinitions() + { + var mockA = CreateMockDdef("path/to/folder/A.ddef", new List()); + var mockB = CreateMockDdef("path/to/folder/subfolder/B.ddef", new List()); + + m_AllDdefs.Add(mockA.Object); + m_AllDdefs.Add(mockB.Object); + + var subfolderFiles = new[] + { + "path/to/folder/subfolder/script2.js", + "path/to/folder/subfolder/config2.rc" + }; + m_Files.AddRange(subfolderFiles); + + SetupFileService_ForDdef(mockA.Object, m_Files, m_Extensions); + SetupFileService_ForDdef(mockB.Object, m_Files, m_Extensions); + + m_InputDdefs.Add(mockA.Object); + var ddefFilesA = m_DdefService.GetDeploymentDefinitionFiles( + new[] + { + mockA.Object.Path + }, + m_Extensions); + + m_InputDdefs.Remove(mockA.Object); + m_InputDdefs.Add(mockB.Object); + var ddefFilesB = m_DdefService.GetDeploymentDefinitionFiles( + new[] + { + mockB.Object.Path + }, + m_Extensions); + + var flatFiles = ddefFilesA.FilesByExtension + .SelectMany(kvp => kvp.Value) + .ToList(); + Assert.AreEqual(3, flatFiles.Count); + Assert.IsFalse(flatFiles.Any(f => f.Contains("subfolder"))); + + flatFiles = ddefFilesB.FilesByExtension + .SelectMany(kvp => kvp.Value) + .ToList(); + Assert.AreEqual(2, flatFiles.Count); + Assert.IsTrue(flatFiles.All(f => subfolderFiles.Contains(f))); + } + + [Test] + public void GetDeploymentDefinitionFiles_RespectsExclusions() + { + var subfolderFiles = new[] + { + "path/to/folder/subfolder/script2.js", + "path/to/folder/subfolder/config2.rc" + }; + m_Files.AddRange(subfolderFiles); + + var mockA = CreateMockDdef("path/to/folder/A.ddef", subfolderFiles); + m_AllDdefs.Add(mockA.Object); + + SetupFileService_ForDdef(mockA.Object, m_Files, m_Extensions); + + m_InputDdefs.Add(mockA.Object); + var ddefFilesA = m_DdefService.GetDeploymentDefinitionFiles( + new[] + { + mockA.Object.Path + }, + m_Extensions); + + var flatFiles = ddefFilesA.FilesByExtension + .SelectMany(kvp => kvp.Value) + .ToList(); + var flatExcludes = ddefFilesA.ExcludedFilesByDeploymentDefinition + .SelectMany(kvp => kvp.Value) + .ToList(); + + Assert.AreEqual(2, flatExcludes.Count); + Assert.AreEqual(3, flatFiles.Count); + Assert.IsFalse(flatFiles.Any(f => f.Contains("subfolder"))); + Assert.IsTrue(flatExcludes.All(f => subfolderFiles.Contains(f))); + } + + [Test] + public void GetDeploymentDefinitionFiles_NoIntersectionAcrossDdefs() + { + m_Files = new List + { + "UGS/cc/script1.js", + "UGS/cc/script2.js", + "UGS/rc/config.rc" + }; + + var subfolderFiles = new List() + { + "UGS/ec/file1.ec", + "UGS/ec/file2.ec" + }; + m_Files.AddRange(subfolderFiles); + + var mockUgs = CreateMockDdef("UGS/UGS.ddef"); + m_AllDdefs.Add(mockUgs.Object); + var mockEc = CreateMockDdef("UGS/ec/EC.ddef"); + m_AllDdefs.Add(mockEc.Object); + + m_InputDdefs.Add(mockUgs.Object); + m_InputDdefs.Add(mockEc.Object); + + SetupFileService_ForDdef(mockUgs.Object, m_Files, m_Extensions); + SetupFileService_ForDdef(mockEc.Object, subfolderFiles, m_Extensions); + + var inputDdefs = new[] + { + mockUgs.Object.Path, + mockEc.Object.Path + }; + + var ddefFiles = m_DdefService.GetDeploymentDefinitionFiles(inputDdefs, m_Extensions); + + foreach (var ugsFile in ddefFiles.FilesByDeploymentDefinition[mockUgs.Object]) + { + Assert.IsFalse(ddefFiles.FilesByDeploymentDefinition[mockEc.Object].Contains(ugsFile)); + } + + foreach (var ecFile in ddefFiles.FilesByDeploymentDefinition[mockEc.Object]) + { + Assert.IsFalse(ddefFiles.FilesByDeploymentDefinition[mockUgs.Object].Contains(ecFile)); + } + } + + [Test] + public void VerifyFileIntersection_IntersectionWithDdefFiles_Throws() + { + var inputFiles = new Dictionary> + { + { + ".js", new List + { + "path/to/file.js" + } + } + }; + var ddefFilesByExtension = new Dictionary> + { + { + ".js", new List + { + "path/to/file.js" + } + } + }; + var ddefFilesByDdef = new Dictionary>() + { + { + CreateMockDdef("path/to/A.ddef").Object, new List + { + "path/to/file.js" + } + } + }; + var ddefExcludes = new Dictionary>(); + var ddefFiles = new DeploymentDefinitionFiles(ddefFilesByExtension, ddefFilesByDdef, ddefExcludes); + Assert.Throws( + () => CliDeploymentDefinitionService.VerifyFileIntersection(inputFiles, ddefFiles)); + } + + [Test] + public void VerifyFileIntersection_IntersectionWithDdefExcludes_Throws() + { + var inputFiles = new Dictionary> + { + { + ".js", new List + { + "path/to/file.js" + } + } + }; + var ddefFilesByExtension = new Dictionary> + { + { + ".js", new List + { + "path/to/otherFile.js" + } + } + }; + var ddefFilesByDdef = new Dictionary>() + { + { + CreateMockDdef("path/to/A.ddef").Object, new List + { + "path/to/file.js" + } + } + }; + var ddefExcludes = new Dictionary>() + { + { + CreateMockDdef( + "path/to/A.ddef", + new List + { + "path/to/file.js" + }) + .Object, + new List + { + "path/to/file.js" + } + } + }; + var ddefFiles = new DeploymentDefinitionFiles(ddefFilesByExtension, ddefFilesByDdef, ddefExcludes); + Assert.Throws( + () => CliDeploymentDefinitionService.VerifyFileIntersection(inputFiles, ddefFiles)); + } + + [Test] + public void LogDeploymentDefinitionExclusions_AllExclusionsLogged() + { + var ddefResult = new DeploymentDefinitionFilteringResult( + new DeploymentDefinitionFiles( + Mock.Of>>(), + Mock.Of>>(), + new Dictionary> + { + { + CreateMockDdef("path/to/folder/A.ddef").Object, new List + { + "path/to/folder/file1.test", + "path/to/folder/file2.test" + } + }, + { + CreateMockDdef("path/to/otherFolder/B.ddef").Object, new List + { + "path/to/otherFolder/fileY.test", + "path/to/otherFolder/fileZ.test" + } + } + }), + new Dictionary>()); + + + var message = ddefResult.GetExclusionsLogMessage(); + + foreach (var file in ddefResult.DefinitionFiles.ExcludedFilesByDeploymentDefinition.Values.SelectMany(f => f)) + { + Assert.IsTrue(message.Contains(file)); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/DeploymentDefinition/DeploymentDefinitionFileIntersectionExceptionTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/DeploymentDefinition/DeploymentDefinitionFileIntersectionExceptionTests.cs new file mode 100644 index 0000000..c704c69 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/DeploymentDefinition/DeploymentDefinitionFileIntersectionExceptionTests.cs @@ -0,0 +1,61 @@ +using Moq; +using NUnit.Framework; +using Unity.Services.Cli.Authoring.DeploymentDefinition; +using Unity.Services.Deployment.Core.Model; + +namespace Unity.Services.Cli.Authoring.UnitTest.Service; + +class DeploymentDefinitionFileIntersectionExceptionTests +{ + [TestCase(true)] + [TestCase(false)] + public void GetIntersectionMessage_CorrectMessageForExcludes(bool isExcludes) + { + var dictionary = new Dictionary> + { + { + CreateMockDdef("test").Object, new List + { + "path/to/file.test" + } + } + }; + var exception = new DeploymentDefinitionFileIntersectionException(dictionary, isExcludes); + Assert.IsTrue(exception.Message.Contains("exclusions") == isExcludes); + } + + static Mock CreateMockDdef(string name) + { + var mockDdef = new Mock(); + mockDdef.Setup(d => d.Name).Returns(name); + return mockDdef; + } + + [Test] + public void GetIntersectionMessage_IncludesAllFiles() + { + var dictionary = new Dictionary> + { + { + CreateMockDdef("test1").Object, new List + { + "path/to/folder1/fileA.test", + "path/to/folder1/fileB.test" + } + }, + { + CreateMockDdef("test2").Object, new List + { + "path/to/folder2/fileA.test", + "path/to/folder2/fileB.test" + } + } + }; + var exception = new DeploymentDefinitionFileIntersectionException(dictionary, false); + + foreach (var file in dictionary.Values.SelectMany(filesList => filesList)) + { + Assert.IsTrue(exception.Message.Contains(file)); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/DeploymentDefinition/DeploymentDefinitionFileServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/DeploymentDefinition/DeploymentDefinitionFileServiceTests.cs new file mode 100644 index 0000000..e5e8d14 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/DeploymentDefinition/DeploymentDefinitionFileServiceTests.cs @@ -0,0 +1,180 @@ +using System.IO.Abstractions; +using System.Runtime.Intrinsics.Arm; +using Moq; +using NUnit.Framework; +using Unity.Services.Cli.Authoring.DeploymentDefinition; +using Unity.Services.Cli.Authoring.Service; +using Unity.Services.Deployment.Core.Model; + +namespace Unity.Services.Cli.Authoring.UnitTest.Service; + +[TestFixture] +class DeploymentDefinitionFileServiceTests +{ + Mock m_MockFile; + Mock m_MockDirectory; + Mock m_MockPath; + Mock m_MockFactory; + + DeploymentDefinitionFileService m_DdefFileService; + + public DeploymentDefinitionFileServiceTests() + { + m_MockFile = new Mock(); + m_MockDirectory = new Mock(); + m_MockPath = new Mock(); + m_MockFactory = new Mock(); + m_MockFactory + .Setup(f => f.CreateDeploymentDefinition(It.IsAny())) + .Returns((string path) => CreateMockDdef(path).Object); + m_DdefFileService = new DeploymentDefinitionFileService( + m_MockFile.Object, + m_MockDirectory.Object, + m_MockPath.Object, + m_MockFactory.Object); + } + + static Mock CreateMockDdef(string path) + { + var mockDdef = new Mock(); + mockDdef + .SetupGet(d => d.Path) + .Returns(path); + mockDdef + .SetupGet(d => d.Name) + .Returns(Path.GetFileNameWithoutExtension(path)); + return mockDdef; + } + + [Test] + public void GetDeploymentDefinitionsForInput_GetsAllDdef() + { + var inputPaths = new[] + { + "path/to/folder/file.ext", + "path/to/folder/A.ddef", + "path/to/otherFolder" + }; + + SetupFilePathDirectoryForInput(inputPaths); + + SetupDirectoryReturn( + "path/to/folder", + ".ddef", + "path/to/folder/A.ddef", "path/to/folder/subfolder/B.ddef"); + + var result = m_DdefFileService.GetDeploymentDefinitionsForInput(inputPaths); + + Assert.AreEqual(1, result.InputDeploymentDefinitions.Count); + Assert.AreEqual(2, result.AllDeploymentDefinitions.Count); + } + + void SetupFilePathDirectoryForInput(IEnumerable inputPaths) + { + foreach (var inputPath in inputPaths) + { + var directoryName = inputPath[..inputPath.LastIndexOf('/')]; + m_MockPath + .Setup(p => p.GetDirectoryName(inputPath)) + .Returns(directoryName); + m_MockPath + .Setup(p => p.GetFullPath(inputPath)) + .Returns(inputPath); + m_MockPath + .Setup(p => p.GetFullPath(directoryName)) + .Returns(directoryName); + m_MockFile + .Setup(f => f.Exists(inputPath)) + .Returns(true); + m_MockFile + .Setup(f => f.Exists(directoryName)) + .Returns(false); + m_MockDirectory + .Setup(d => d.Exists(directoryName)) + .Returns(true); + } + } + + void SetupDirectoryReturn(string directory, string extension, params string[] returnValueParams) + { + foreach (var returnValue in returnValueParams) + { + m_MockPath + .Setup(p => p.GetDirectoryName(returnValue)) + .Returns(returnValue[..returnValue.LastIndexOf('/')]); + } + + m_MockDirectory + .Setup(d => d.Exists(directory)) + .Returns(true); + m_MockDirectory + .Setup(d => d.GetFiles(directory, $"*{extension}", SearchOption.AllDirectories)) + .Returns(returnValueParams); + } + + [Test] + public void GetFilesForDeploymentDefinition_ReturnsAllFiles() + { + var ddef = CreateMockDdef("path/to/folder/A.ddef"); + + var extensions = new[] + { + ".js", + ".rc", + }; + + SetupFilePathDirectoryForInput( + new[] + { + ddef.Object.Path + }); + + SetupDirectoryReturn("path/to/folder", ".js", "path/to/folder/script.js"); + SetupDirectoryReturn("path/to/folder", ".rc", "path/to/folder/config.rc"); + + var files = new List(); + foreach (var extension in extensions) + { + files.AddRange(m_DdefFileService.GetFilesForDeploymentDefinition(ddef.Object, extension)); + } + + Assert.AreEqual(2, files.Count); + } + + [Test] + public void GetDeploymentDefinitionsForInput_MultipleDdefsInFolder_Throws() + { + var inputPaths = new[] + { + "path/to/folder/file.ext", + "path/to/folder/A.ddef" + }; + + SetupFilePathDirectoryForInput(inputPaths); + SetupDirectoryReturn("path/to/folder", ".ddef", "path/to/folder/A.ddef", "path/to/folder/B.ddef"); + + Assert.Throws( + () => + m_DdefFileService.GetDeploymentDefinitionsForInput(inputPaths)); + } + + [Test] + public void GetDeploymentDefinitionsForInput_MultipleDdefsInNestedFolders_DoesNotThrow() + { + var inputPaths = new[] + { + "path/to/folder/subfolder/B.ddef", + "path/to/folder/A.ddef" + }; + + SetupFilePathDirectoryForInput(inputPaths); + SetupDirectoryReturn( + "path/to/folder", + ".ddef", + "path/to/folder/A.ddef", "path/to/folder/subfolder/B.ddef"); + SetupDirectoryReturn("path/to/folder/subfolder", ".ddef", "path/to/folder/subfolder/B.ddef"); + + Assert.DoesNotThrow(() => + m_DdefFileService.GetDeploymentDefinitionsForInput(inputPaths)); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/DeployHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/DeployHandlerTests.cs index bf3ce21..a932571 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/DeployHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/DeployHandlerTests.cs @@ -1,9 +1,11 @@ +using System.Collections.ObjectModel; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; using Spectre.Console; +using Unity.Services.Cli.Authoring.DeploymentDefinition; using Unity.Services.Cli.Common.Console; using Unity.Services.Cli.Common.Exceptions; using Unity.Services.Cli.Common.Logging; @@ -13,7 +15,9 @@ using Unity.Services.Cli.Authoring.Input; using Unity.Services.Cli.Authoring.Model; using Unity.Services.Cli.Authoring.Service; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent; using Unity.Services.Cli.TestUtils; +using Unity.Services.Deployment.Core.Model; using Unity.Services.DeploymentApi.Editor; namespace Unity.Services.Cli.Authoring.UnitTest.Handlers; @@ -26,10 +30,11 @@ public class DeployHandlerTests readonly Mock m_ServiceProvider = new(); readonly Mock m_DeploymentService = new(); readonly Mock m_UnityEnvironment = new(); - readonly Mock m_DeployFileService = new(); + readonly Mock m_DdefService = new(); + readonly Mock m_AnalyticsEventBuilder = new(); readonly ServiceTypesBridge m_Bridge = new(); - const string ValidEnvironmentId = "00000000-0000-0000-0000-000000000000"; + const string k_ValidEnvironmentId = "00000000-0000-0000-0000-000000000000"; public class TestDeploymentService : IDeploymentService { string m_ServiceType = "Test"; @@ -111,10 +116,12 @@ public void SetUp() m_DeploymentService.Reset(); m_Logger.Reset(); m_UnityEnvironment.Reset(); - m_DeployFileService.Reset(); + m_DdefService.Reset(); m_DeploymentService.Setup(s => s.ServiceName) .Returns("mock_test"); + m_DeploymentService.Setup(s => s.DeployFileExtension) + .Returns(".test"); m_DeploymentService.Setup( s => s.Deploy( @@ -141,14 +148,19 @@ public void SetUp() m_Host.Setup(x => x.Services) .Returns(provider); - m_UnityEnvironment.Setup(x => x.FetchIdentifierAsync(CancellationToken.None)).Returns(Task.FromResult(ValidEnvironmentId)); - m_DeployFileService.Setup(x => x.ListFilesToDeploy(new[] - { - "" - }, "*.ext")).Returns(new[] - { - "" - }); + m_UnityEnvironment.Setup(x => x.FetchIdentifierAsync(CancellationToken.None)).Returns(Task.FromResult(k_ValidEnvironmentId)); + m_DdefService + .Setup( + x => x.GetFilesFromInput( + It.IsAny>(), + It.IsAny>())) + .Returns( + new DeploymentDefinitionFilteringResult( + new DeploymentDefinitionFiles(), + new Dictionary> + { + { ".test", new List() } + })); } [Test] @@ -159,10 +171,11 @@ public async Task DeployAsync_WithLoadingIndicator_CallsLoadingIndicatorStartLoa await DeployHandler.DeployAsync( It.IsAny(), It.IsAny(), - m_DeployFileService.Object, m_UnityEnvironment.Object, It.IsAny(), mockLoadingIndicator.Object, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, CancellationToken.None); mockLoadingIndicator.Verify( @@ -175,15 +188,16 @@ public async Task DeployAsync_CallsGetServicesCorrectly() { await DeployHandler.DeployAsync( m_Host.Object, new DeployInput(), - m_DeployFileService.Object, m_UnityEnvironment.Object, - m_Logger.Object, (StatusContext)null!, CancellationToken.None); + m_Logger.Object, + (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, + CancellationToken.None); TestsHelper.VerifyLoggerWasCalled(m_Logger, LogLevel.Critical, LoggerExtension.ResultEventId, Times.Once); } - - [Test] public void DeployAsync_DeploymentFailureThrowsDeploymentFailureException() { @@ -198,9 +212,12 @@ public void DeployAsync_DeploymentFailureThrowsDeploymentFailureException() { await DeployHandler.DeployAsync( m_Host.Object, new DeployInput(), - m_DeployFileService.Object, m_UnityEnvironment.Object, - m_Logger.Object, (StatusContext)null!, CancellationToken.None); + m_Logger.Object, + (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, + CancellationToken.None); }); } @@ -218,9 +235,12 @@ public void DeployAsync_DeploymentFailureThrowsAggregateException() { await DeployHandler.DeployAsync( m_Host.Object, new DeployInput(), - m_DeployFileService.Object, m_UnityEnvironment.Object, - m_Logger.Object, (StatusContext)null!, CancellationToken.None); + m_Logger.Object, + (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, + CancellationToken.None); }); } @@ -236,10 +256,11 @@ public async Task DeployAsync_ReconcileWillNotExecutedWithNoServiceFlag() await DeployHandler.DeployAsync( m_Host.Object, input, - m_DeployFileService.Object, m_UnityEnvironment.Object, m_Logger.Object, (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, CancellationToken.None); m_DeploymentService.Verify( @@ -268,10 +289,11 @@ public async Task DeployAsync_ReconcileExecuteWithServiceFlag() await DeployHandler.DeployAsync( m_Host.Object, input, - m_DeployFileService.Object, m_UnityEnvironment.Object, m_Logger.Object, (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, CancellationToken.None); m_DeploymentService.Verify( @@ -299,10 +321,11 @@ public async Task DeployAsync_ExecuteWithCorrectServiceFlag() await DeployHandler.DeployAsync( m_Host.Object, input, - m_DeployFileService.Object, m_UnityEnvironment.Object, m_Logger.Object, (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, CancellationToken.None); m_DeploymentService.Verify( @@ -330,10 +353,84 @@ public async Task DeployAsync_NotExecuteWithIncorrectServiceFlag() await DeployHandler.DeployAsync( m_Host.Object, input, - m_DeployFileService.Object, m_UnityEnvironment.Object, m_Logger.Object, (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, + CancellationToken.None); + + m_DeploymentService.Verify( + s => s.Deploy( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Test] + public async Task DeployAsync_TableOutputWithJsonFlag() + { + var input = new DeployInput() + { + IsJson = true + }; + + await DeployHandler.DeployAsync( + m_Host.Object, + input, + m_UnityEnvironment.Object, + m_Logger.Object, + (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, + CancellationToken.None); + + var tableResult = new DeploymentResult( + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty()) + .ToTable(); + + TestsHelper.VerifyLoggerWasCalled(m_Logger, message: tableResult.ToString()); + } + + [Test] + public async Task DeployAsync_MultipleDeploymentDefinitionsException_NotExecuted() + { + var input = new DeployInput() + { + Services = new[] + { + "mock_test" + } + }; + + m_DdefService + .Setup( + s => s.GetFilesFromInput( + It.IsAny>(), + It.IsAny>())) + .Throws( + () => + new MultipleDeploymentDefinitionInDirectoryException( + new Mock().Object, + new Mock().Object, + "path")); + + await DeployHandler.DeployAsync( + m_Host.Object, + input, + m_UnityEnvironment.Object, + m_Logger.Object, + (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, CancellationToken.None); m_DeploymentService.Verify( @@ -346,4 +443,92 @@ await DeployHandler.DeployAsync( It.IsAny()), Times.Never); } + + [Test] + public async Task DeployAsync_DeploymentDefinitionIntersectionException_NotExecuted() + { + var input = new DeployInput() + { + Services = new[] + { + "mock_test" + } + }; + + m_DdefService + .Setup( + s => s.GetFilesFromInput( + It.IsAny>(), + It.IsAny>())) + .Throws( + new DeploymentDefinitionFileIntersectionException( + new Dictionary>(), + true)); + + await DeployHandler.DeployAsync( + m_Host.Object, + input, + m_UnityEnvironment.Object, + m_Logger.Object, + (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, + CancellationToken.None); + + m_DeploymentService.Verify( + s => s.Deploy( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Test] + public async Task DeployAsync_DeploymentDefinitionsHaveExclusion_ExclusionsLogged() + { + var input = new DeployInput() + { + Services = new[] + { + "mock_test" + } + }; + + var mockResult = new Mock(); + mockResult + .Setup(r => r.AllFilesByExtension) + .Returns( + new Dictionary> + { + { ".test", new List() } + }); + var mockFiles = new Mock(); + mockFiles + .Setup(f => f.HasExcludes) + .Returns(true); + mockResult + .Setup(r => r.DefinitionFiles) + .Returns(mockFiles.Object); + m_DdefService + .Setup( + s => s.GetFilesFromInput( + It.IsAny>(), + It.IsAny>())) + .Returns(mockResult.Object); + + await DeployHandler.DeployAsync( + m_Host.Object, + input, + m_UnityEnvironment.Object, + m_Logger.Object, + (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, + CancellationToken.None); + + mockResult.Verify(r => r.GetExclusionsLogMessage(), Times.Once); + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/FetchHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/FetchHandlerTests.cs index e089fac..df3ffcb 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/FetchHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/FetchHandlerTests.cs @@ -1,13 +1,11 @@ using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; using Spectre.Console; +using Unity.Services.Cli.Authoring.DeploymentDefinition; using Unity.Services.Cli.Common.Console; using Unity.Services.Cli.Common.Logging; using Unity.Services.Cli.Common.Services; @@ -15,7 +13,9 @@ using Unity.Services.Cli.Authoring.Input; using Unity.Services.Cli.Authoring.Model; using Unity.Services.Cli.Authoring.Service; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent; using Unity.Services.Cli.TestUtils; +using Unity.Services.Deployment.Core.Model; using Unity.Services.DeploymentApi.Editor; namespace Unity.Services.Cli.Authoring.UnitTest.Handlers; @@ -27,6 +27,8 @@ public class FetchHandlerTests readonly Mock m_Logger = new(); readonly Mock m_ServiceProvider = new(); readonly Mock m_FetchService = new(); + readonly Mock m_DdefService = new(); + readonly Mock m_AnalyticsEventBuilder = new(); [SetUp] public void SetUp() @@ -35,13 +37,17 @@ public void SetUp() m_ServiceProvider.Reset(); m_FetchService.Reset(); m_Logger.Reset(); + m_AnalyticsEventBuilder.Reset(); m_FetchService.Setup(s => s.ServiceName) .Returns("mock_test"); + m_FetchService.Setup(s => s.FileExtension) + .Returns(".test"); m_FetchService.Setup( s => s.FetchAsync( It.IsAny(), + It.IsAny>(), It.IsAny(), It.IsAny())) .Returns( @@ -61,6 +67,19 @@ public void SetUp() m_Host.Setup(x => x.Services) .Returns(provider); + + m_DdefService + .Setup( + x => x.GetFilesFromInput( + It.IsAny>(), + It.IsAny>())) + .Returns( + new DeploymentDefinitionFilteringResult( + new DeploymentDefinitionFiles(), + new Dictionary> + { + { ".test", new List() } + })); } class TestFetchService : IFetchService @@ -71,14 +90,17 @@ class TestFetchService : IFetchService string IFetchService.ServiceType => m_ServiceType; string IFetchService.ServiceName => m_ServiceName; - string IFetchService.FileExtension => m_DeployFileExtension; - public Task FetchAsync(FetchInput input, StatusContext? loadingContext, CancellationToken cancellationToken) + public Task FetchAsync( + FetchInput input, + IReadOnlyList filePaths, + StatusContext? loadingContext, + CancellationToken cancellationToken) { var res = new FetchResult( StringsToDeployContent(new[] { "updated1" }), - StringsToDeployContent (new[] { "deleted1" }), + StringsToDeployContent(new[] { "deleted1" }), Array.Empty(), StringsToDeployContent(new[] { "file1" }), Array.Empty()); @@ -92,7 +114,13 @@ public async Task FetchAsync_WithLoadingIndicator_CallsLoadingIndicatorStartLoad var mockLoadingIndicator = new Mock(); await FetchHandler.FetchAsync( - null!, null!, null!, mockLoadingIndicator.Object, CancellationToken.None); + null!, + null!, + null!, + m_DdefService.Object, + mockLoadingIndicator.Object, + m_AnalyticsEventBuilder.Object, + CancellationToken.None); mockLoadingIndicator.Verify( ex => ex.StartLoadingAsync(It.IsAny(), It.IsAny>()), Times.Once); @@ -105,12 +133,15 @@ public async Task FetchAsync_PrintsCorrectDryRun(bool dryRun) { var mockLogger = new Mock(); var fetchInput = new FetchInput { DryRun = dryRun }; + var mockDdefService = new Mock(); await FetchHandler.FetchAsync( m_Host.Object, fetchInput, mockLogger.Object, (StatusContext?)null, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, CancellationToken.None); mockLogger.Verify(l => l.Log( @@ -129,7 +160,13 @@ public async Task FetchAsync_CallsGetServicesCorrectly() var fetchInput = new FetchInput(); await FetchHandler.FetchAsync( - m_Host.Object, fetchInput, m_Logger.Object, (StatusContext)null!, CancellationToken.None); + m_Host.Object, + fetchInput, + m_Logger.Object, + (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, + CancellationToken.None); TestsHelper.VerifyLoggerWasCalled(m_Logger, LogLevel.Critical, LoggerExtension.ResultEventId, Times.Once); } @@ -145,14 +182,20 @@ public void FetchAsync_ThrowsAggregateException() m_Host.Reset(); var bridge = new ServiceTypesBridge(); var collection = bridge.CreateBuilder(new ServiceCollection()); - collection.AddScoped(); + collection.AddScoped(); var provider = bridge.CreateServiceProvider(collection); m_Host.Setup(x => x.Services).Returns(provider); var fetchInput = new FetchInput(); Assert.ThrowsAsync(async () => { await FetchHandler.FetchAsync( - m_Host.Object, fetchInput, m_Logger.Object, (StatusContext)null!, CancellationToken.None); + m_Host.Object, + fetchInput, + m_Logger.Object, + (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, + CancellationToken.None); }); } @@ -169,11 +212,14 @@ await FetchHandler.FetchAsync( input, m_Logger.Object, (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, CancellationToken.None); m_FetchService.Verify( s => s.FetchAsync( It.IsAny(), + It.IsAny>(), It.IsAny(), It.IsAny()), Times.Never); @@ -196,11 +242,14 @@ await FetchHandler.FetchAsync( input, m_Logger.Object, (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, CancellationToken.None); m_FetchService.Verify( s => s.FetchAsync( It.IsAny(), + It.IsAny>(), It.IsAny(), It.IsAny()), Times.Once); @@ -222,11 +271,14 @@ await FetchHandler.FetchAsync( input, m_Logger.Object, (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, CancellationToken.None); m_FetchService.Verify( s => s.FetchAsync( It.IsAny(), + It.IsAny>(), It.IsAny(), It.IsAny()), Times.Once); @@ -248,17 +300,177 @@ await FetchHandler.FetchAsync( input, m_Logger.Object, (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, + CancellationToken.None); + + m_FetchService.Verify( + s => s.FetchAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Test] + public async Task FetchAsync_MultipleDeploymentDefinitionsException_NotExecuted() + { + var input = new FetchInput() + { + Services = new[] + { + "mock_test" + } + }; + + m_DdefService + .Setup( + s => s.GetFilesFromInput( + It.IsAny>(), + It.IsAny>())) + .Throws( + () => + new MultipleDeploymentDefinitionInDirectoryException( + new Mock().Object, + new Mock().Object, + "path")); + + await FetchHandler.FetchAsync( + m_Host.Object, + input, + m_Logger.Object, + (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, + CancellationToken.None); + + m_FetchService.Verify( + s => s.FetchAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Test] + public async Task FetchAsync_DeploymentDefinitionIntersectionException_NotExecuted() + { + var input = new FetchInput() + { + Services = new[] + { + "mock_test" + } + }; + + m_DdefService + .Setup( + s => s.GetFilesFromInput( + It.IsAny>(), + It.IsAny>())) + .Throws( + new DeploymentDefinitionFileIntersectionException( + new Dictionary>(), + true)); + + await FetchHandler.FetchAsync( + m_Host.Object, + input, + m_Logger.Object, + (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, + CancellationToken.None); + + m_FetchService.Verify( + s => s.FetchAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Test] + public async Task FetchAsync_DeploymentDefinitionsHaveExclusion_ExclusionsLogged() + { + var input = new FetchInput() + { + Services = new[] + { + "mock_test" + } + }; + + var mockResult = new Mock(); + mockResult + .Setup(r => r.AllFilesByExtension) + .Returns( + new Dictionary> + { + { ".test", new List() } + }); + var mockFiles = new Mock(); + mockFiles + .Setup(f => f.HasExcludes) + .Returns(true); + mockResult + .Setup(r => r.DefinitionFiles) + .Returns(mockFiles.Object); + m_DdefService + .Setup( + s => s.GetFilesFromInput( + It.IsAny>(), + It.IsAny>())) + .Returns(mockResult.Object); + + + await FetchHandler.FetchAsync( + m_Host.Object, + input, + m_Logger.Object, + (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, + CancellationToken.None); + + mockResult.Verify(r => r.GetExclusionsLogMessage(), Times.Once); + } + + [Test] + public async Task FetchAsync_ReconcileWithDdef_NotExecuted() + { + var input = new FetchInput() + { + Services = new[] + { + "mock_test" + }, + Path = "some/path/to/A.ddef", + Reconcile = true + }; + + await FetchHandler.FetchAsync( + m_Host.Object, + input, + m_Logger.Object, + (StatusContext)null!, + m_DdefService.Object, + m_AnalyticsEventBuilder.Object, CancellationToken.None); m_FetchService.Verify( s => s.FetchAsync( It.IsAny(), + It.IsAny>(), It.IsAny(), It.IsAny()), Times.Never); } - class TestFetchUnhandleExceptionFetchService : IFetchService + class TestFetchUnhandledExceptionFetchService : IFetchService { string m_ServiceType = "Test"; string m_ServiceName = "test"; @@ -269,7 +481,11 @@ class TestFetchUnhandleExceptionFetchService : IFetchService string IFetchService.FileExtension => m_DeployFileExtension; - public Task FetchAsync(FetchInput input, StatusContext? loadingContext, CancellationToken cancellationToken) + public Task FetchAsync( + FetchInput input, + IReadOnlyList filePaths, + StatusContext? loadingContext, + CancellationToken cancellationToken) { return Task.FromException(new NullReferenceException()); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/DeploymentResultTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/DeploymentResultTests.cs index de6e467..aad8e8f 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/DeploymentResultTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/DeploymentResultTests.cs @@ -2,6 +2,7 @@ using System.Linq; using NUnit.Framework; using Unity.Services.Cli.Authoring.Model; +using Unity.Services.Cli.Authoring.Model.TableOutput; using Unity.Services.DeploymentApi.Editor; namespace Unity.Services.Cli.Authoring.UnitTest.Model; @@ -55,4 +56,32 @@ public void ToStringFormatsNoContentDeployed() Assert.IsFalse(result.Contains($"Deployed:{System.Environment.NewLine} {k_DeployedContents.First().Name}")); Assert.IsTrue(result.Contains("No content deployed")); } + + [Test] + public void ToTableFormat() + { + m_DeploymentResult = new DeploymentResult( + k_DeployedContents, + new List(), + new List(), + k_DeployedContents, + k_FailedContents); + var result = m_DeploymentResult.ToTable(); + var expected = TableContent.ToTable(k_DeployedContents[0]); + + foreach (var failed in k_FailedContents) + { + expected.AddRow(RowContent.ToRow(failed)); + } + + Assert.IsTrue(result.Result.Count == expected.Result.Count); + + for (int i = 0; i < result.Result.Count; i++) + { + for (int j = 0; j < result.Result.Count;j++) + { + Assert.AreEqual(expected.Result[i].Name, result.Result[i].Name); + } + } + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/TableOutput/TableTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/TableOutput/TableTests.cs new file mode 100644 index 0000000..dd16945 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/TableOutput/TableTests.cs @@ -0,0 +1,59 @@ +using NUnit.Framework; +using Unity.Services.Cli.Authoring.Model.TableOutput; + +namespace Unity.Services.Cli.Authoring.UnitTest.Model.TableOutput; + +public class TableTests +{ + TableContent m_Table = new TableContent(); + + const string k_StartEntryResource = "Test"; + const string k_UpdatedEntryLocation = "NewDataValue"; + + + readonly RowContent m_StartTableRow = new RowContent(k_StartEntryResource); + readonly RowContent m_UpdatedTableRow = new RowContent(k_StartEntryResource, k_UpdatedEntryLocation); + + [Test] + public void UpdateRowsWorksCorrectly() + { + m_Table = new TableContent(); + + m_Table.AddRow(m_StartTableRow); + m_Table.UpdateOrAddRows( + new[] + { + m_UpdatedTableRow + }); + + Assert.Contains(m_UpdatedTableRow, m_Table.Result.ToList()); + } + + [Test] + public void AddRowWorksCorrectly() + { + m_Table = new TableContent(); + + m_Table.AddRow(m_StartTableRow); + + Assert.Contains(m_StartTableRow, m_Table.Result.ToList()); + } + + [Test] + public void AddRowsWorksCorrectly() + { + m_Table = new TableContent(); + + var newTable = new TableContent(); + + newTable.AddRow(m_StartTableRow); + + m_Table.AddRows( + new[] + { + newTable + }); + + Assert.Contains(m_StartTableRow, m_Table.Result.ToList()); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Service/DeployFileServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Service/DeployFileServiceTests.cs index 76b7edd..8ee5ebe 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Service/DeployFileServiceTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Service/DeployFileServiceTests.cs @@ -39,10 +39,10 @@ public void ListFilesToDeployReturnsExistingFiles() { m_MockFile.Setup(f => f.Exists("test.gsh")).Returns(true); m_MockPath.Setup(p => p.GetFullPath("test.gsh")).Returns("test.gsh"); - var files = m_Service.ListFilesToDeploy(new List - { - "test.gsh" - }, ".gsh"); + var files = m_Service.ListFilesToDeploy( + new List { "test.gsh" }, + ".gsh", + true); Assert.That(files, Contains.Item("test.gsh")); } @@ -52,10 +52,14 @@ public void ListFilesToDeployWhenDirectoryAndFileIsMissingThrowPathNotFoundExcep { Assert.Throws(() => { - var _ = m_Service.ListFilesToDeploy(new List - { - "does_not_exist" - }, ".gsh").ToList(); + var _ = m_Service.ListFilesToDeploy( + new List + { + "does_not_exist" + }, + ".gsh", + true) + .ToList(); }); } [Test] @@ -66,10 +70,14 @@ public void ListFilesToDeployThrowExceptionForDirectoryWithoutAccessPermission() m_MockDirectory.Setup(d => d.GetFiles("foo", "*.gsh", SearchOption.AllDirectories)) .Throws(); - Assert.Throws(() => m_Service.ListFilesToDeploy(new List - { - "foo" - }, ".gsh")); + Assert.Throws( + () => m_Service.ListFilesToDeploy( + new List + { + "foo" + }, + ".gsh", + false)); } [Test] @@ -83,10 +91,13 @@ public void ListFilesToDeployEnumeratesDirectories() "test.gsh" }); - var files = m_Service.ListFilesToDeploy(new List - { - "foo" - }, ".gsh"); + var files = m_Service.ListFilesToDeploy( + new List + { + "foo" + }, + ".gsh", + false); Assert.That(files, Contains.Item("test.gsh")); } @@ -104,10 +115,13 @@ public void ListFilesToDeployEnumeratesDirectoriesSorted() m_MockDirectory.Setup(f => f.Exists("foo")).Returns(true); m_MockDirectory.Setup(d => d.GetFiles("foo", "*.gsh", SearchOption.AllDirectories)) .Returns(expectedFiles.ToArray); - var files = m_Service.ListFilesToDeploy(new List - { - "foo" - }, ".gsh"); + var files = m_Service.ListFilesToDeploy( + new List + { + "foo" + }, + ".gsh", + false); expectedFiles.Sort(); CollectionAssert.AreEqual(expectedFiles, files); } @@ -126,10 +140,13 @@ public void ListFilesToDeployEnumeratesDirectoriesRemoveDuplicate() m_MockDirectory.Setup(f => f.Exists("foo")).Returns(true); m_MockDirectory.Setup(d => d.GetFiles("foo", "*.gsh", SearchOption.AllDirectories)) .Returns(expectedFiles.ToArray); - var files = m_Service.ListFilesToDeploy(new List - { - "foo" - }, ".gsh"); + var files = m_Service.ListFilesToDeploy( + new List + { + "foo" + }, + ".gsh", + false); expectedFiles = expectedFiles.Distinct().ToList(); expectedFiles.Sort(); CollectionAssert.AreEqual(expectedFiles, files); @@ -138,45 +155,6 @@ public void ListFilesToDeployEnumeratesDirectoriesRemoveDuplicate() [Test] public void ListFilesToDeployOnEmptyInputThrowDeployException() { - Assert.Throws(() => m_Service.ListFilesToDeploy(new List(), ".gsh")); - } - - [Test] - public async Task LoadContentAsyncSuccessful() - { - m_MockFile.Setup(f => f.Exists("foo")).Returns(true); - m_MockFile.Setup(f => f.ReadAllTextAsync("foo", CancellationToken.None)).ReturnsAsync("{}"); - var content = await m_Service.LoadContentAsync("foo", CancellationToken.None); - Assert.AreEqual("{}", content); - } - - [Test] - public void LoadContentAsyncFailedWithFileNotFound() - { - m_MockFile.Setup(f => f.Exists("foo")).Returns(true); - m_MockFile.Setup(f => f.ReadAllTextAsync("foo", CancellationToken.None)) - .ThrowsAsync(new FileNotFoundException()); - - Assert.ThrowsAsync(async () => await m_Service.LoadContentAsync("foo", CancellationToken.None)); - } - - [Test] - public void LoadContentAsyncFailedWithUnauthorizedAccess() - { - m_MockFile.Setup(f => f.Exists("foo")).Returns(true); - m_MockFile.Setup(f => f.ReadAllTextAsync("foo", CancellationToken.None)) - .ThrowsAsync(new UnauthorizedAccessException()); - - Assert.ThrowsAsync(async () => await m_Service.LoadContentAsync("foo", CancellationToken.None)); - } - - [Test] - public void LoadContentAsyncFailedWithUnexpectedException() - { - m_MockFile.Setup(f => f.Exists("foo")).Returns(true); - m_MockFile.Setup(f => f.ReadAllTextAsync("foo", CancellationToken.None)) - .ThrowsAsync(new Exception()); - - Assert.ThrowsAsync(async () => await m_Service.LoadContentAsync("foo", CancellationToken.None)); + Assert.Throws(() => m_Service.ListFilesToDeploy(new List(), ".gsh", true)); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeployModule.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeployModule.cs index f6f3791..ece0f34 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeployModule.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeployModule.cs @@ -3,12 +3,15 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Unity.Services.Cli.Authoring.DeploymentDefinition; +using Unity.Services.Cli.Authoring.Compression; using Unity.Services.Cli.Authoring.Handlers; using Unity.Services.Cli.Authoring.Input; using Unity.Services.Cli.Authoring.Service; using Unity.Services.Cli.Common; using Unity.Services.Cli.Common.Console; using Unity.Services.Cli.Common.Input; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent; using Unity.Services.Cli.Common.Utils; namespace Unity.Services.Cli.Authoring; @@ -36,10 +39,11 @@ public DeployModule() ModuleRootCommand.SetHandler< IHost, DeployInput, - IDeployFileService, IUnityEnvironment, ILogger, ILoadingIndicator, + ICliDeploymentDefinitionService, + IAnalyticsEventBuilder, CancellationToken>( DeployHandler.DeployAsync); } @@ -53,5 +57,9 @@ public static void RegisterServices(HostBuilderContext hostBuilderContext, IServ serviceCollection.AddTransient(_ => new FileSystem().Directory); serviceCollection.AddTransient(_ => new FileSystem().Path); serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/CliDeploymentDefinition.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/CliDeploymentDefinition.cs new file mode 100644 index 0000000..44a2cff --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/CliDeploymentDefinition.cs @@ -0,0 +1,22 @@ +using System.Collections.ObjectModel; +using Unity.Services.Deployment.Core.Model; +using IoPath = System.IO.Path; + +namespace Unity.Services.Cli.Authoring.Model; + +class CliDeploymentDefinition : IDeploymentDefinition +{ + + public string Name { get; set; } + + public string Path { get; set; } + + public ObservableCollection ExcludePaths { get; } + + public CliDeploymentDefinition(string path) + { + Path = path; + Name = ""; + ExcludePaths = new ObservableCollection(); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/CliDeploymentDefinitionService.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/CliDeploymentDefinitionService.cs new file mode 100644 index 0000000..768ae41 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/CliDeploymentDefinitionService.cs @@ -0,0 +1,188 @@ +using Unity.Services.Cli.Authoring.DeploymentDefinition; +using Unity.Services.Deployment.Core; +using Unity.Services.Deployment.Core.Model; + +namespace Unity.Services.Cli.Authoring.Service; + +class CliDeploymentDefinitionService : DeploymentDefinitionServiceBase, ICliDeploymentDefinitionService +{ + public const string Extension = ".ddef"; + public override IReadOnlyList DeploymentDefinitions => m_AllDefinitions.AsReadOnly(); + + List m_AllDefinitions; + List m_InputDefinitions; + readonly IDeploymentDefinitionFileService m_FileService; + + public CliDeploymentDefinitionService(IDeploymentDefinitionFileService fileService) + { + m_AllDefinitions = new List(); + m_InputDefinitions = new List(); + m_FileService = fileService; + } + + public IDeploymentDefinitionFilteringResult GetFilesFromInput( + IEnumerable inputPaths, + IEnumerable extensions) + { + var inputPathsEnumerated = inputPaths.ToList(); + var extensionsEnumerated = extensions.ToList(); + + var ddefFiles = GetDeploymentDefinitionFiles( + inputPathsEnumerated, + extensionsEnumerated); + + var inputFilesByExtension = extensionsEnumerated + .ToDictionary( + extension => extension, + extension => m_FileService.ListFilesToDeploy(inputPathsEnumerated, extension, false) ?? new List()); + + VerifyFileIntersection( + inputFilesByExtension, + ddefFiles); + + var allFilesByExtension = new Dictionary>(); + foreach (var extension in extensionsEnumerated) + { + var allFiles = new List(inputFilesByExtension[extension]); + allFiles.AddRange(ddefFiles.FilesByExtension[extension]); + allFilesByExtension.Add(extension, allFiles.Distinct().ToList()); + } + + m_AllDefinitions.Clear(); + m_InputDefinitions.Clear(); + + return new DeploymentDefinitionFilteringResult(ddefFiles, allFilesByExtension); + } + + internal IDeploymentDefinitionFiles GetDeploymentDefinitionFiles(IEnumerable inputPaths, IEnumerable extensions) + { + var inputResult = m_FileService.GetDeploymentDefinitionsForInput(inputPaths); + m_AllDefinitions = new List(inputResult.AllDeploymentDefinitions); + m_InputDefinitions = new List(inputResult.InputDeploymentDefinitions); + + var filesByExtension = new Dictionary>(); + var excludedFilesByDdef = new Dictionary>(); + var filesByDdef = new Dictionary>(); + foreach (var extension in extensions) + { + var filesForExtension = new List(); + foreach (var ddef in m_InputDefinitions) + { + var filesForDdef = new List(); + var ddefFilesForExtension = m_FileService.GetFilesForDeploymentDefinition(ddef, extension); + var excludedFiles = new List(); + FilterFilesAndExcludesForDdef( + ddef, + ddefFilesForExtension, + ref filesForDdef, + ref excludedFiles); + + if (!filesByDdef.ContainsKey(ddef)) + { + filesByDdef.Add(ddef, new List()); + } + filesByDdef[ddef].AddRange(filesForDdef); + + if (!excludedFilesByDdef.ContainsKey(ddef)) + { + excludedFilesByDdef.Add(ddef, new List()); + } + excludedFilesByDdef[ddef].AddRange(excludedFiles); + + filesForExtension.AddRange(filesForDdef); + } + + filesByExtension.Add(extension, filesForExtension); + } + + var filesByDdefFinal = new Dictionary>(); + foreach (var (ddef, files) in filesByDdef) + { + filesByDdefFinal.Add(ddef, files); + } + + var excludedFilesByDdefFinal = new Dictionary>(); + foreach (var (ddef, excludedFiles) in excludedFilesByDdef) + { + excludedFilesByDdefFinal.Add(ddef, excludedFiles); + } + + return new DeploymentDefinitionFiles( + filesByExtension, + filesByDdefFinal, + excludedFilesByDdefFinal); + } + + void FilterFilesAndExcludesForDdef( + IDeploymentDefinition ddef, + IReadOnlyList ddefFilesForExtension, + ref List files, + ref List excludedFiles) + { + foreach (var file in ddefFilesForExtension) + { + if (DefinitionForPath(file) == ddef) + { + if (this.IsPathExcludedByDeploymentDefinition(file, ddef)) + { + excludedFiles.Add(file); + } + else + { + files.Add(file); + } + } + } + } + + internal static void VerifyFileIntersection( + IReadOnlyDictionary> inputFiles, + IDeploymentDefinitionFiles deploymentDefinitionFiles) + { + CheckForIntersection( + inputFiles, + deploymentDefinitionFiles.FilesByDeploymentDefinition, + false); + + CheckForIntersection( + inputFiles, + deploymentDefinitionFiles.ExcludedFilesByDeploymentDefinition, + true); + } + + static void CheckForIntersection( + IReadOnlyDictionary> filesByExtension, + IReadOnlyDictionary> filesByDdef, + bool isExcludes) + { + var fileIntersection = new Dictionary>(); + foreach (var extensionFiles in filesByExtension.Values) + { + foreach (var (ddef, ddefFiles) in filesByDdef) + { + if (ddefFiles.Any()) + { + var intersection = + extensionFiles + .Intersect(ddefFiles) + .ToList(); + + if (intersection.Any()) + { + if (!fileIntersection.ContainsKey(ddef)) + { + fileIntersection.Add(ddef, new List()); + } + + fileIntersection[ddef].AddRange(intersection); + } + } + } + } + + if (fileIntersection.Any()) + { + throw new DeploymentDefinitionFileIntersectionException(fileIntersection, isExcludes); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFactory.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFactory.cs new file mode 100644 index 0000000..73a5f5c --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFactory.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Unity.Services.Cli.Authoring.Model; +using Unity.Services.Deployment.Core.Model; + +namespace Unity.Services.Cli.Authoring.DeploymentDefinition; + +class DeploymentDefinitionFactory : IDeploymentDefinitionFactory +{ + static readonly JsonSerializerSettings k_JsonSerializerSettings = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + public IDeploymentDefinition CreateDeploymentDefinition(string path) + { + var ddef = new CliDeploymentDefinition(path); + var json = File.ReadAllText(path); + JsonConvert.PopulateObject(json, ddef, k_JsonSerializerSettings); + + return ddef; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFileIntersectionException.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFileIntersectionException.cs new file mode 100644 index 0000000..518587a --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFileIntersectionException.cs @@ -0,0 +1,39 @@ +using System.Text; +using Unity.Services.Deployment.Core.Model; + +namespace Unity.Services.Cli.Authoring.DeploymentDefinition; + +public class DeploymentDefinitionFileIntersectionException : Exception +{ + readonly Dictionary> m_FileIntersection; + readonly bool m_IsExclude; + + public override string Message => GetIntersectionMessage(); + + internal DeploymentDefinitionFileIntersectionException( + Dictionary> fileIntersection, + bool isExcludes) + { + m_FileIntersection = fileIntersection; + m_IsExclude = isExcludes; + } + + string GetIntersectionMessage() + { + var sb = new StringBuilder(); + sb.Append( + m_IsExclude + ? "A conflict was found between the deployment definition exclusions and the other arguments:" + : "A conflict was found between the deployment definitions and the other arguments:"); + + foreach (var (ddef, excludedFiles) in m_FileIntersection) + { + foreach (var excludedFile in excludedFiles) + { + sb.Append($"{Environment.NewLine}\t'{excludedFile}' [{ddef.Name}.ddef]"); + } + } + + return sb.ToString(); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFileService.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFileService.cs new file mode 100644 index 0000000..fb539e2 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFileService.cs @@ -0,0 +1,88 @@ +using System.IO.Abstractions; +using Unity.Services.Cli.Authoring.DeploymentDefinition; +using Unity.Services.Deployment.Core.Model; + +namespace Unity.Services.Cli.Authoring.Service; + +class DeploymentDefinitionFileService : DeployFileService, IDeploymentDefinitionFileService +{ + readonly IPath m_Path; + readonly IDeploymentDefinitionFactory m_Factory; + + public DeploymentDefinitionFileService( + IFile file, + IDirectory directory, + IPath path, + IDeploymentDefinitionFactory factory) + : base(file, directory, path) + { + m_Path = path; + m_Factory = factory; + } + + public DeploymentDefinitionInputResult GetDeploymentDefinitionsForInput(IEnumerable inputPaths) + { + var inputDdefPaths = ListFilesToDeploy(inputPaths.ToList(), CliDeploymentDefinitionService.Extension, true); + var allDdefPaths = new List(); + foreach (var inputDdefPath in inputDdefPaths) + { + var directoryName = m_Path.GetDirectoryName(inputDdefPath); + if (directoryName != null) + { + allDdefPaths.AddRange(ListFilesToDeploy(directoryName, CliDeploymentDefinitionService.Extension, false)); + } + } + + allDdefPaths = allDdefPaths.Distinct().ToList(); + + var inputDdefs = new List(); + var allDdefs = new List(); + var ddefDirectories = new Dictionary(); + foreach (var ddefPath in allDdefPaths) + { + var ddef = m_Factory.CreateDeploymentDefinition(ddefPath); + + var ddefDirectory = m_Path.GetDirectoryName(ddefPath); + if (ddefDirectory != null) + { + if (!ddefDirectories.ContainsKey(ddefDirectory)) + { + ddefDirectories.Add(ddefDirectory, ddef); + } + else + { + throw new MultipleDeploymentDefinitionInDirectoryException( + ddefDirectories[ddefDirectory], + ddef, + ddefDirectory); + } + } + + allDdefs.Add(ddef); + if (inputDdefPaths.Contains(ddefPath)) + { + inputDdefs.Add(ddef); + } + } + + return new DeploymentDefinitionInputResult(inputDdefs, allDdefs); + } + + public IReadOnlyList GetFilesForDeploymentDefinition( + IDeploymentDefinition deploymentDefinition, + string extension) + { + var files = new List(); + var ddefDirectory = m_Path.GetDirectoryName(deploymentDefinition.Path); + if (ddefDirectory != null) + { + var filesForExtension = ListFilesToDeploy( + ddefDirectory, + extension, + false); + files.AddRange(filesForExtension); + } + + return files; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFiles.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFiles.cs new file mode 100644 index 0000000..c931c64 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFiles.cs @@ -0,0 +1,28 @@ +using Unity.Services.Deployment.Core.Model; + +namespace Unity.Services.Cli.Authoring.DeploymentDefinition; + +class DeploymentDefinitionFiles : IDeploymentDefinitionFiles +{ + public IReadOnlyDictionary> FilesByExtension { get; } + public IReadOnlyDictionary> FilesByDeploymentDefinition { get; } + public IReadOnlyDictionary> ExcludedFilesByDeploymentDefinition { get; } + public bool HasExcludes => ExcludedFilesByDeploymentDefinition.Any(kvp => kvp.Value.Any()); + + public DeploymentDefinitionFiles() + { + FilesByExtension = new Dictionary>(); + FilesByDeploymentDefinition = new Dictionary>(); + ExcludedFilesByDeploymentDefinition = new Dictionary>(); + } + + public DeploymentDefinitionFiles( + IReadOnlyDictionary> filesByExtension, + IReadOnlyDictionary> filesByDeploymentDefinition, + IReadOnlyDictionary> excludedFilesByDeploymentDefinition) + { + FilesByExtension = filesByExtension; + FilesByDeploymentDefinition = filesByDeploymentDefinition; + ExcludedFilesByDeploymentDefinition = excludedFilesByDeploymentDefinition; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFilteringResult.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFilteringResult.cs new file mode 100644 index 0000000..1b363d3 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionFilteringResult.cs @@ -0,0 +1,33 @@ +using System.Text; +using Unity.Services.Cli.Authoring.DeploymentDefinition; + +namespace Unity.Services.Cli.Authoring.Service; + +class DeploymentDefinitionFilteringResult : IDeploymentDefinitionFilteringResult +{ + public IDeploymentDefinitionFiles DefinitionFiles { get; } + public Dictionary> AllFilesByExtension { get; } + + public DeploymentDefinitionFilteringResult( + IDeploymentDefinitionFiles definitionFiles, + Dictionary> allFilesByExtension) + { + DefinitionFiles = definitionFiles; + AllFilesByExtension = allFilesByExtension; + } + + public string GetExclusionsLogMessage() + { + var sb = new StringBuilder(); + sb.Append("The following files were excluded by deployment definitions:"); + foreach (var (ddef, excludedFiles) in DefinitionFiles.ExcludedFilesByDeploymentDefinition) + { + foreach (var file in excludedFiles) + { + sb.Append($"{Environment.NewLine}\t'{file}' [{ddef.Name}.ddef]"); + } + } + + return sb.ToString(); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionInputResult.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionInputResult.cs new file mode 100644 index 0000000..e0fb2df --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/DeploymentDefinitionInputResult.cs @@ -0,0 +1,17 @@ +using Unity.Services.Deployment.Core.Model; + +namespace Unity.Services.Cli.Authoring.Service; + +class DeploymentDefinitionInputResult +{ + public IReadOnlyList InputDeploymentDefinitions { get; } + public IReadOnlyList AllDeploymentDefinitions { get; } + + public DeploymentDefinitionInputResult( + IReadOnlyList inputDeploymentDefinitions, + IReadOnlyList allDeploymentDefinitions) + { + InputDeploymentDefinitions = inputDeploymentDefinitions; + AllDeploymentDefinitions = allDeploymentDefinitions; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/ICliDeploymentDefinitionService.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/ICliDeploymentDefinitionService.cs new file mode 100644 index 0000000..8ece8ed --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/ICliDeploymentDefinitionService.cs @@ -0,0 +1,11 @@ +using Unity.Services.Cli.Authoring.DeploymentDefinition; +using Unity.Services.Deployment.Core; + +namespace Unity.Services.Cli.Authoring.Service; + +interface ICliDeploymentDefinitionService : IDeploymentDefinitionService +{ + IDeploymentDefinitionFilteringResult GetFilesFromInput( + IEnumerable inputPaths, + IEnumerable extensions); +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/IDeploymentDefinitionFactory.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/IDeploymentDefinitionFactory.cs new file mode 100644 index 0000000..89615ef --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/IDeploymentDefinitionFactory.cs @@ -0,0 +1,8 @@ +using Unity.Services.Deployment.Core.Model; + +namespace Unity.Services.Cli.Authoring.DeploymentDefinition; + +interface IDeploymentDefinitionFactory +{ + IDeploymentDefinition CreateDeploymentDefinition(string path); +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/IDeploymentDefinitionFileService.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/IDeploymentDefinitionFileService.cs new file mode 100644 index 0000000..40fc938 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/IDeploymentDefinitionFileService.cs @@ -0,0 +1,11 @@ +using Unity.Services.Deployment.Core.Model; + +namespace Unity.Services.Cli.Authoring.Service; + +interface IDeploymentDefinitionFileService : IDeployFileService +{ + DeploymentDefinitionInputResult GetDeploymentDefinitionsForInput(IEnumerable inputPaths); + IReadOnlyList GetFilesForDeploymentDefinition( + IDeploymentDefinition deploymentDefinition, + string extension); +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/IDeploymentDefinitionFiles.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/IDeploymentDefinitionFiles.cs new file mode 100644 index 0000000..47703bb --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/IDeploymentDefinitionFiles.cs @@ -0,0 +1,11 @@ +using Unity.Services.Deployment.Core.Model; + +namespace Unity.Services.Cli.Authoring.DeploymentDefinition; + +interface IDeploymentDefinitionFiles +{ + public IReadOnlyDictionary> FilesByExtension { get; } + public IReadOnlyDictionary> FilesByDeploymentDefinition { get; } + public IReadOnlyDictionary> ExcludedFilesByDeploymentDefinition { get; } + public bool HasExcludes { get; } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/IDeploymentDefinitionFilteringResult.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/IDeploymentDefinitionFilteringResult.cs new file mode 100644 index 0000000..23a20f7 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/IDeploymentDefinitionFilteringResult.cs @@ -0,0 +1,8 @@ +namespace Unity.Services.Cli.Authoring.DeploymentDefinition; + +interface IDeploymentDefinitionFilteringResult +{ + public IDeploymentDefinitionFiles DefinitionFiles { get; } + public Dictionary> AllFilesByExtension { get; } + public string GetExclusionsLogMessage(); +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/MultipleDeploymentDefinitionInDirectoryException.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/MultipleDeploymentDefinitionInDirectoryException.cs new file mode 100644 index 0000000..079b426 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/DeploymentDefinition/MultipleDeploymentDefinitionInDirectoryException.cs @@ -0,0 +1,14 @@ +using Unity.Services.Deployment.Core.Model; + +namespace Unity.Services.Cli.Authoring.DeploymentDefinition; + +public class MultipleDeploymentDefinitionInDirectoryException : Exception +{ + internal MultipleDeploymentDefinitionInDirectoryException( + IDeploymentDefinition ddef1, + IDeploymentDefinition ddef2, + string path) + : base($"Multiple deployment definitions were found in the directory '{path}': '{ddef1.Name}.ddef' and '{ddef2.Name}.ddef'") + { + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/FetchModule.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/FetchModule.cs index 2e6378c..b29d793 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring/FetchModule.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/FetchModule.cs @@ -1,6 +1,4 @@ using System.CommandLine; -using System.IO.Abstractions; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Unity.Services.Cli.Authoring.Handlers; @@ -9,6 +7,7 @@ using Unity.Services.Cli.Common.Console; using Unity.Services.Cli.Common.Input; using Unity.Services.Cli.Authoring.Service; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent; namespace Unity.Services.Cli.Authoring; @@ -36,7 +35,9 @@ public FetchModule() IHost, FetchInput, ILogger, + ICliDeploymentDefinitionService, ILoadingIndicator, + IAnalyticsEventBuilder, CancellationToken>( FetchHandler.FetchAsync); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/DeployHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/DeployHandler.cs index a02ff61..d79b418 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/DeployHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/DeployHandler.cs @@ -2,12 +2,15 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Spectre.Console; +using Unity.Services.Cli.Authoring.DeploymentDefinition; using Unity.Services.Cli.Authoring.Input; using Unity.Services.Cli.Authoring.Model; +using Unity.Services.Cli.Authoring.Model.TableOutput; using Unity.Services.Cli.Authoring.Service; using Unity.Services.Cli.Common.Console; using Unity.Services.Cli.Common.Exceptions; using Unity.Services.Cli.Common.Logging; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent; using Unity.Services.Cli.Common.Utils; namespace Unity.Services.Cli.Authoring.Handlers; @@ -17,10 +20,11 @@ static class DeployHandler public static async Task DeployAsync( IHost host, DeployInput input, - IDeployFileService deployFileService, IUnityEnvironment unityEnvironment, ILogger logger, ILoadingIndicator loadingIndicator, + ICliDeploymentDefinitionService definitionService, + IAnalyticsEventBuilder analyticsEventBuilder, CancellationToken cancellationToken ) { @@ -29,20 +33,22 @@ await loadingIndicator.StartLoadingAsync( context => DeployAsync( host, input, - deployFileService, unityEnvironment, logger, context, + definitionService, + analyticsEventBuilder, cancellationToken)); } internal static async Task DeployAsync( IHost host, DeployInput input, - IDeployFileService deployFileService, IUnityEnvironment unityEnvironment, ILogger logger, StatusContext? loadingContext, + ICliDeploymentDefinitionService definitionService, + IAnalyticsEventBuilder analyticsEventBuilder, CancellationToken cancellationToken ) { @@ -78,10 +84,27 @@ CancellationToken cancellationToken .Where(s => CheckService(input, s)) .ToArray(); + var ddefResult = GetDdefResult( + definitionService, + logger, + input.Paths, + deploymentServices.Select(ds => ds.DeployFileExtension)); + if (ddefResult == null) + { + return; + } + + analyticsEventBuilder.SetAuthoringCommandlinePathsInputCount(input.Paths); + + foreach (var deploymentService in deploymentServices) + { + analyticsEventBuilder.AddAuthoringServiceProcessed(deploymentService.ServiceName); + } + var tasks = deploymentServices.Select( - m => m.Deploy( - input, - deployFileService.ListFilesToDeploy(input.Paths, m.DeployFileExtension), + m => m.Deploy( + input, + ddefResult.AllFilesByExtension[m.DeployFileExtension], projectId, environmentId, loadingContext, @@ -114,7 +137,29 @@ CancellationToken cancellationToken input.DryRun ); - logger.LogResultValue(totalResult); + if (input.IsJson) + { + var tableResult = new TableContent() + { + IsDryRun = input.DryRun + }; + + foreach (var task in tasks) + { + tableResult.AddRows(task.Result.ToTable()); + } + + logger.LogResultValue(tableResult); + } + else + { + logger.LogResultValue(totalResult); + } + + if (ddefResult.DefinitionFiles.HasExcludes) + { + logger.LogInformation(ddefResult.GetExclusionsLogMessage()); + } // Get Exceptions from faulted deployments var exceptions = tasks @@ -149,4 +194,28 @@ static bool AreAllServicesSupported(DeployInput input, IReadOnlyList inputPaths, + IEnumerable extensions) + { + IDeploymentDefinitionFilteringResult? ddefResult = null; + try + { + ddefResult = ddefService + .GetFilesFromInput(inputPaths, extensions); + } + catch (MultipleDeploymentDefinitionInDirectoryException e) + { + logger.LogError(e.Message); + } + catch (DeploymentDefinitionFileIntersectionException e) + { + logger.LogError(e.Message); + } + + return ddefResult; + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/FetchHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/FetchHandler.cs index a43e285..84a6b98 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/FetchHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/FetchHandler.cs @@ -1,13 +1,17 @@ +using System.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Spectre.Console; +using Unity.Services.Cli.Authoring.DeploymentDefinition; using Unity.Services.Cli.Authoring.Input; using Unity.Services.Cli.Authoring.Model; +using Unity.Services.Cli.Authoring.Model.TableOutput; using Unity.Services.Cli.Authoring.Service; using Unity.Services.Cli.Common.Console; using Unity.Services.Cli.Common.Exceptions; using Unity.Services.Cli.Common.Logging; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent; namespace Unity.Services.Cli.Authoring.Handlers; @@ -17,7 +21,9 @@ public static async Task FetchAsync( IHost host, FetchInput input, ILogger logger, + ICliDeploymentDefinitionService deploymentDefinitionService, ILoadingIndicator loadingIndicator, + IAnalyticsEventBuilder analyticsEventBuilder, CancellationToken cancellationToken) { await loadingIndicator.StartLoadingAsync( @@ -27,6 +33,8 @@ await loadingIndicator.StartLoadingAsync( input, logger, context, + deploymentDefinitionService, + analyticsEventBuilder, cancellationToken)); } @@ -35,6 +43,8 @@ internal static async Task FetchAsync( FetchInput input, ILogger logger, StatusContext? loadingContext, + ICliDeploymentDefinitionService definitionService, + IAnalyticsEventBuilder analyticsEventBuilder, CancellationToken cancellationToken) { var services = host.Services.GetServices().ToList(); @@ -55,24 +65,55 @@ internal static async Task FetchAsync( $"{supportedServiceNamesStr}"); } - if (input.Reconcile && input.Services.Count == 0) + if (input.Reconcile) { - logger.LogError( - "Reconcile is a destructive operation. Specify your service(s) with the --services option: {SupportedServiceNamesStr}", - supportedServiceNamesStr); - return; + if (input.Services.Count == 0) + { + logger.LogError( + "Reconcile is a destructive operation. Specify your service(s) with the --services option: {SupportedServiceNamesStr}", + supportedServiceNamesStr); + return; + } + + if (Path.GetExtension(input.Path) == CliDeploymentDefinitionService.Extension) + { + logger.LogError("Reconcile is not compatible with Deployment Definitions"); + return; + } } var fetchResult = Array.Empty(); var fetchServices = services .Where(s => CheckService(input, s)) - .ToArray(); + .ToList(); + + var inputPaths = new List { input.Path }; + + + var ddefResult = GetDdefResult( + definitionService, + logger, + inputPaths, + fetchServices.Select(ds => ds.FileExtension)); + + if (ddefResult == null) + { + return; + } + + analyticsEventBuilder.SetAuthoringCommandlinePathsInputCount(new[] { input.Path }); + + foreach (var fetchService in fetchServices) + { + analyticsEventBuilder.AddAuthoringServiceProcessed(fetchService.ServiceName); + } var tasks = fetchServices .Select( m => m.FetchAsync( input, + ddefResult.AllFilesByExtension[m.FileExtension], loadingContext, cancellationToken)) .ToArray(); @@ -89,7 +130,30 @@ internal static async Task FetchAsync( } var totalResult = new FetchResult(fetchResult, input.DryRun); - logger.LogResultValue(totalResult); + + if (input.IsJson) + { + var tableResult = new TableContent() + { + IsDryRun = input.DryRun + }; + + foreach (var task in tasks) + { + tableResult.AddRows(task.Result.ToTable()); + } + + logger.LogResultValue(tableResult); + } + else + { + logger.LogResultValue(totalResult); + } + + if (ddefResult.DefinitionFiles.HasExcludes) + { + logger.LogInformation(ddefResult.GetExclusionsLogMessage()); + } // Get Exceptions from faulted deployments var exceptions = tasks @@ -124,4 +188,28 @@ static bool AreAllServicesSupported(FetchInput input, IReadOnlyList inputPaths, + IEnumerable extensions) + { + IDeploymentDefinitionFilteringResult? ddefResult = null; + try + { + ddefResult = ddefService + .GetFilesFromInput(inputPaths, extensions); + } + catch (MultipleDeploymentDefinitionInDirectoryException e) + { + logger.LogError(e.Message); + } + catch (DeploymentDefinitionFileIntersectionException e) + { + logger.LogError(e.Message); + } + + return ddefResult; + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/AuthorResult.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/AuthorResult.cs index 52e3bf9..31004f9 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/AuthorResult.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/AuthorResult.cs @@ -1,6 +1,7 @@ using System.Text; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; +using Unity.Services.Cli.Authoring.Model.TableOutput; using Unity.Services.DeploymentApi.Editor; namespace Unity.Services.Cli.Authoring.Model; @@ -119,6 +120,21 @@ public override string ToString() return result.ToString(); } + public virtual TableContent ToTable() + { + var table = new TableContent + { + IsDryRun = DryRun + }; + + table.AddRows(Updated.Select(TableContent.ToTable).ToList()); + table.AddRows(Deleted.Select(TableContent.ToTable).ToList()); + table.AddRows(Created.Select(TableContent.ToTable).ToList()); + table.AddRows(Failed.Select(TableContent.ToTable).ToList()); + + return table; + } + void AppendFetched(StringBuilder builder) { if (Authored.Any()) @@ -151,8 +167,6 @@ internal static void AppendResult(StringBuilder builder, IEnumerable new DeploymentStatus("Failed to fetch", messageDetail, SeverityLevel.Error); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/TableOutput/RowContent.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/TableOutput/RowContent.cs new file mode 100644 index 0000000..474f7f7 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/TableOutput/RowContent.cs @@ -0,0 +1,32 @@ +using Unity.Services.DeploymentApi.Editor; + +namespace Unity.Services.Cli.Authoring.Model.TableOutput; + +[Serializable] +public class RowContent +{ + public string Name { get; protected set; } + public string Type { get; protected set; } + public string Status{ get; protected set; } + public string Details{ get; protected set; } + public string Path{ get; protected set; } + + public RowContent(string name = "", string type = "", string status = "", string details = "", string path = "") + { + Name = name; + Type = type; + Status = status; + Details = details; + Path = path; + } + + public static RowContent ToRow(IDeploymentItem item) + { + return new RowContent( + item.Name, + ((ITypedItem)item).Type, + item.Status.Message, + item.Status.MessageDetail, + item.Path); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/TableOutput/TableContent.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/TableOutput/TableContent.cs new file mode 100644 index 0000000..5f01e8a --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/TableOutput/TableContent.cs @@ -0,0 +1,74 @@ +using Unity.Services.DeploymentApi.Editor; + +namespace Unity.Services.Cli.Authoring.Model.TableOutput; + +[Serializable] +public class TableContent +{ + public bool IsDryRun { get; set; } + public List Result { get; protected set; } + + public TableContent() + { + Result = new List(); + } + + public TableContent(IReadOnlyList rows) + { + Result = rows.ToList(); + } + + public void AddRows(IReadOnlyList tables) + { + foreach (var item in tables) + { + AddRows(item.Result); + } + } + + public void AddRows(TableContent table) + { + AddRows(table.Result); + } + + public void AddRows(IReadOnlyList rows) + { + Result.AddRange(rows); + } + + public void AddRow(RowContent row) + { + Result.Add(row); + } + + public void UpdateOrAddRows(IReadOnlyList items) + { + foreach (var item in items) + { + UpdateOrAddRow(item); + } + } + + void UpdateOrAddRow(RowContent item) + { + var index = Result.FindIndex(row => row.Name == item.Name); + + if (index != -1) + { + Result[index] = item; + } + else + { + AddRow(item); + } + } + + public static TableContent ToTable(IDeploymentItem item) + { + return new TableContent( + new[] + { + RowContent.ToRow(item) + }); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Service/DeployFileService.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Service/DeployFileService.cs index c1b467b..10b7c51 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Service/DeployFileService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Service/DeployFileService.cs @@ -16,7 +16,7 @@ public DeployFileService(IFile file, IDirectory directory, IPath path) m_Path = path; } - public IReadOnlyList ListFilesToDeploy(IReadOnlyList paths, string extension) + public virtual IReadOnlyList ListFilesToDeploy(IReadOnlyList paths, string extension, bool ignoreDirectory) { if (!paths.Any()) { @@ -27,16 +27,35 @@ public IReadOnlyList ListFilesToDeploy(IReadOnlyList paths, stri foreach (var path in paths) { - var fullPath = m_Path.GetFullPath(path); + files.AddRange(ListFilesToDeploy(path, extension, ignoreDirectory)); + } + files = files.Distinct().ToList(); + files.Sort(); - if (m_File.Exists(fullPath)) + return files; + } + + protected IReadOnlyList ListFilesToDeploy(string path, string extension, bool ignoreDirectory) + { + if (!path.Any()) + { + throw new DeployException("Please specify at least one path to deploy."); + } + + var files = new List(); + + var fullPath = m_Path.GetFullPath(path); + + if (m_File.Exists(fullPath)) + { + if (string.Equals(Path.GetExtension(fullPath), extension)) { - if (string.Equals(Path.GetExtension(fullPath), extension)) - { - files.Add(fullPath); - } + files.Add(fullPath); } - else if (m_Directory.Exists(fullPath)) + } + else if (m_Directory.Exists(fullPath)) + { + if (!ignoreDirectory) { try { @@ -47,31 +66,14 @@ public IReadOnlyList ListFilesToDeploy(IReadOnlyList paths, stri throw new CliException($"CLI does not have the permissions to access \"{fullPath}\"", ExitCode.HandledError); } } - else - { - throw new PathNotFoundException($"\"{fullPath}\""); - } } + else + { + throw new PathNotFoundException($"\"{fullPath}\""); + } + files = files.Distinct().ToList(); files.Sort(); return files; } - - public async Task LoadContentAsync(string filePath, CancellationToken cancellationToken) - { - try - { - return await m_File.ReadAllTextAsync(filePath, cancellationToken); - } - catch (FileNotFoundException exception) - { - throw new CliException(exception.Message, ExitCode.HandledError); - } - catch (UnauthorizedAccessException exception) - { - throw new CliException(string.Join(" ", exception.Message, - "Make sure that the CLI has the permissions to access the file and that the " + - "specified path points to a file and not a directory."), ExitCode.HandledError); - } - } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Service/IDeployFileService.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Service/IDeployFileService.cs index 7cfd287..75e864b 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Service/IDeployFileService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Service/IDeployFileService.cs @@ -7,7 +7,7 @@ public interface IDeployFileService /// /// list of file or directory paths to evaluate /// target file extension. For example ".js" to look for java script file + /// if the path is a directory, should it be scanned /// paths of files with target file extension - IReadOnlyList ListFilesToDeploy(IReadOnlyList paths, string extension); - Task LoadContentAsync(string filePath, CancellationToken cancellationToken); + IReadOnlyList ListFilesToDeploy(IReadOnlyList paths, string extension, bool ignoreDirectory); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Service/IFetchService.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Service/IFetchService.cs index 0e49fb4..5a5210a 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Service/IFetchService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Service/IFetchService.cs @@ -8,10 +8,11 @@ public interface IFetchService { string ServiceType { get; } string ServiceName { get; } - protected string FileExtension { get; } + string FileExtension { get; } Task FetchAsync( FetchInput input, + IReadOnlyList filePaths, StatusContext? loadingContext, CancellationToken cancellationToken); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Unity.Services.Cli.Authoring.csproj b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Unity.Services.Cli.Authoring.csproj index cf52464..bb9fb86 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Unity.Services.Cli.Authoring.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Unity.Services.Cli.Authoring.csproj @@ -19,6 +19,7 @@ + diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Authoring/Fetch/JavaScriptFetchServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Authoring/Fetch/JavaScriptFetchServiceTests.cs index 09d5dff..3faaf7a 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Authoring/Fetch/JavaScriptFetchServiceTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Authoring/Fetch/JavaScriptFetchServiceTests.cs @@ -7,7 +7,6 @@ using NUnit.Framework; using Unity.Services.Cli.Authoring.Input; using Unity.Services.Cli.Authoring.Model; -using Unity.Services.Cli.Authoring.Service; using Unity.Services.Cli.CloudCode.Authoring; using Unity.Services.Cli.CloudCode.Deploy; using Unity.Services.Cli.CloudCode.Parameters; @@ -24,7 +23,6 @@ class JavaScriptFetchServiceTests { readonly Mock m_UnityEnvironment = new(); readonly Mock m_Client = new(); - readonly Mock m_DeployFileService = new(); readonly Mock m_ScriptsLoader = new(); readonly Mock m_InputParser = new(); readonly Mock m_ScriptParser = new(); @@ -37,7 +35,6 @@ public JavaScriptFetchServiceTests() m_Service = new JavaScriptFetchService( m_UnityEnvironment.Object, m_Client.Object, - m_DeployFileService.Object, m_ScriptsLoader.Object, m_InputParser.Object, m_ScriptParser.Object, @@ -49,7 +46,6 @@ public void SetUp() { m_UnityEnvironment.Reset(); m_Client.Reset(); - m_DeployFileService.Reset(); m_ScriptsLoader.Reset(); m_InputParser.Reset(); m_ScriptParser.Reset(); @@ -62,7 +58,7 @@ public void SetUp() [TestCase(true, true)] public async Task FetchAsyncInitializesClientAndGetsResultFromHandler(bool dryRun, bool reconcile) { - SetupLocalResources(out var input, out var scripts); + SetupLocalResources(out var input, out var scripts, out var files); input.CloudProjectId = TestValues.ValidProjectId; input.DryRun = dryRun; input.Reconcile = reconcile; @@ -75,39 +71,44 @@ public async Task FetchAsyncInitializesClientAndGetsResultFromHandler(bool dryRu Array.Empty(), Array.Empty()); m_FetchHandler.Setup( - x => x.FetchAsync(input.Path, scripts, input.DryRun, input.Reconcile, CancellationToken.None)) + x => x.FetchAsync( + input.Path, + scripts, + input.DryRun, + input.Reconcile, + CancellationToken.None)) .ReturnsAsync(expectedResult); - var result = await m_Service.FetchAsync(input, null, CancellationToken.None); + var result = await m_Service.FetchAsync(input, files, null, CancellationToken.None); m_Client.Verify( x => x.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None), Times.Once); - Assert.That(result, Is.SameAs(expectedResult)); + Assert.That(result.Fetched.Count, Is.EqualTo(expectedResult.Fetched.Count)); + Assert.That(result.Deleted.Count, Is.EqualTo(expectedResult.Deleted.Count)); + Assert.That(result.Created.Count, Is.EqualTo(expectedResult.Created.Count)); + Assert.That(result.Updated.Count, Is.EqualTo(expectedResult.Updated.Count)); + Assert.That(result.Failed.Count, Is.EqualTo(expectedResult.Failed.Count)); } - void SetupLocalResources(out FetchInput input, out List scripts) + void SetupLocalResources(out FetchInput input, out List scripts, out List files) { input = new FetchInput { Path = ".", }; - var files = new[] + files = new List { "foo.js", "bar/foobar.js" }; + var filesInstance = new List(files); scripts = files.Select(x => new ScriptInfo(ScriptName.FromPath(x))) .Cast() .ToList(); - m_DeployFileService.Setup( - x => x.ListFilesToDeploy( - It.IsAny>(), - CloudCodeConstants.JavaScriptFileExtension)) - .Returns(files); m_ScriptsLoader.Setup( x => x.LoadScriptsAsync( - files, + filesInstance, CloudCodeConstants.ServiceType, CloudCodeConstants.JavaScriptFileExtension, m_InputParser.Object, @@ -119,10 +120,58 @@ void SetupLocalResources(out FetchInput input, out List scripts) [Test] public async Task GetResourcesFromFilesAsyncReturnsLoadedFiles() { - SetupLocalResources(out var input, out var scripts); + SetupLocalResources(out var input, out var scripts, out var files); - var resources = await m_Service.GetResourcesFromFilesAsync(input, CancellationToken.None); + var resources = await m_Service.GetResourcesFromFilesAsync( + files, + CancellationToken.None); Assert.That(resources.LoadedScripts, Is.SameAs(scripts)); } + + [Test] + public async Task FailedToLoadAreReported() + { + m_ScriptsLoader.Setup( + x => x.LoadScriptsAsync( + It.IsAny>(), + CloudCodeConstants.ServiceType, + CloudCodeConstants.JavaScriptFileExtension, + m_InputParser.Object, + m_ScriptParser.Object, + CancellationToken.None)) + .ReturnsAsync(new CloudCodeScriptLoadResult(new List(), new List() + { + new CloudCodeScript { Name = ScriptName.FromPath("failed-script.js"), Path = "failed-script.js" } + })); + + var input = new FetchInput + { + Path = ".", + }; + + var expectedResult = new FetchResult( + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty()); + + m_FetchHandler.Setup( + x => x.FetchAsync( + input.Path, + It.IsAny>(), + input.DryRun, + input.Reconcile, + CancellationToken.None)) + .ReturnsAsync(expectedResult); + + var actualResult = await m_Service.FetchAsync( + input, + new [] { "hello.js" }, + null!, + CancellationToken.None); + + Assert.IsNotEmpty(actualResult.Failed); + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/CloudCodeModuleTests.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/CloudCodeModuleTests.cs index d1cc76f..c5cc15c 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/CloudCodeModuleTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/CloudCodeModuleTests.cs @@ -19,7 +19,6 @@ using Unity.Services.Cli.Common.Validator; using Unity.Services.Cli.TestUtils; using Unity.Services.CloudCode.Authoring.Editor.Core.Analytics; -using Unity.Services.CloudCode.Authoring.Editor.Core.Crypto; using Unity.Services.CloudCode.Authoring.Editor.Core.Deployment; using Unity.Services.CloudCode.Authoring.Editor.Core.Logging; using Unity.Services.Gateway.CloudCodeApiV1.Generated.Api; @@ -129,8 +128,6 @@ public void NewFileCommandWithInput() [TestCase(typeof(EnvironmentProvider))] [TestCase(typeof(ICliEnvironmentProvider))] [TestCase(typeof(IEnvironmentProvider))] - [TestCase(typeof(IHashComputer))] - [TestCase(typeof(IScriptCache))] [TestCase(typeof(IPreDeployValidator))] [TestCase(typeof(ICloudCodeModulesLoader))] [TestCase(typeof(ICloudCodeScriptsLoader))] @@ -163,7 +160,7 @@ public void CreateJavaScriptDeployServiceCreatesInstanceWithReferencesToProvided var environmentProvider = new Mock(); var client = new Mock(); var deploymentHandler = new CliCloudCodeDeploymentHandler( - null!, null!, null!, null!, null!); + null!, null!, null!, null!); var provider = new Mock(); SetupProvider(); @@ -204,7 +201,7 @@ public void CreateCSharpDeployServiceCreatesInstanceWithReferencesToProvidedServ var csModuleLoader = new Mock(); var client = new Mock(); var deploymentHandlerWithOutput = new CliCloudCodeDeploymentHandler( - client.Object, null!, null!, null!, null!); + client.Object, null!, null!, null!); var provider = new Mock(); SetupProvider(); diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CloudCodeAuthoringLoggerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CloudCodeAuthoringLoggerTests.cs index cc87a47..cbcd35b 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CloudCodeAuthoringLoggerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CloudCodeAuthoringLoggerTests.cs @@ -24,23 +24,23 @@ public void SetUp() } [Test] - public void LogErrorLoggerLogWithErrorLevel() + public void LogErrorLoggerLogWithErrorLevel_NoOpLogger() { m_CloudCodeAuthoringLogger.LogError("error message"); - TestsHelper.VerifyLoggerWasCalled(m_MockLogger, LogLevel.Error, null, Times.Once); + TestsHelper.VerifyLoggerWasCalled(m_MockLogger, LogLevel.Error, null, Times.Never); } [Test] - public void LogLogInfoLoggerLogWithInformationLevel() + public void LogLogInfoLoggerLogWithInformationLevel_NoOpLogger() { m_CloudCodeAuthoringLogger.LogInfo("information message"); - TestsHelper.VerifyLoggerWasCalled(m_MockLogger, LogLevel.Information, null, Times.Once); + TestsHelper.VerifyLoggerWasCalled(m_MockLogger, LogLevel.Information, null, Times.Never); } [Test] - public void LogLogWarningLoggerLogWitWarningLevel() + public void LogLogWarningLoggerLogWithWarningLevel_NoOpLogger() { m_CloudCodeAuthoringLogger.LogWarning("warning message"); - TestsHelper.VerifyLoggerWasCalled(m_MockLogger, LogLevel.Warning, null, Times.Once); + TestsHelper.VerifyLoggerWasCalled(m_MockLogger, LogLevel.Warning, null, Times.Never); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CustomCloudCodeDeploymentHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CustomCloudCodeDeploymentHandlerTests.cs index d534aa6..51a293b 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CustomCloudCodeDeploymentHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CustomCloudCodeDeploymentHandlerTests.cs @@ -16,7 +16,6 @@ class CustomCloudCodeDeploymentHandlerTests { static readonly Mock k_MockICloudCodeClient = new(); static readonly Mock k_DeploymentAnalytics = new(); - static readonly Mock k_ScriptCache = new(); static readonly Mock k_Logger = new(); static readonly Mock k_PreDeployValidator = new(); @@ -25,7 +24,6 @@ class CustomCloudCodeDeploymentHandlerTests readonly ExposeCliCloudCodeDeploymentHandler m_CliCloudCodeDeploymentHandler = new( k_MockICloudCodeClient.Object, k_DeploymentAnalytics.Object, - k_ScriptCache.Object, k_Logger.Object, k_PreDeployValidator.Object); diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/ExposeCliCloudCodeDeploymentHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/ExposeCliCloudCodeDeploymentHandler.cs index 8f84fdd..cd8ff2e 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/ExposeCliCloudCodeDeploymentHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/ExposeCliCloudCodeDeploymentHandler.cs @@ -21,8 +21,7 @@ public void ExposeUpdateScriptStatus(IScript script, string message, string deta public ExposeCliCloudCodeDeploymentHandler( ICloudCodeClient client, IDeploymentAnalytics deploymentAnalytics, - IScriptCache scriptCache, ILogger logger, IPreDeployValidator preDeployValidator) - : base(client, deploymentAnalytics, scriptCache, logger, preDeployValidator) { } + : base(client, deploymentAnalytics, logger, preDeployValidator) { } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Mock/MockCloudCodeDeploymentHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Mock/MockCloudCodeDeploymentHandler.cs index 7314551..5ac285a 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Mock/MockCloudCodeDeploymentHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Mock/MockCloudCodeDeploymentHandler.cs @@ -12,10 +12,9 @@ class MockCloudCodeDeploymentHandler : CloudCodeDeploymentHandler, ICliDeploymen public MockCloudCodeDeploymentHandler( ICloudCodeClient client, IDeploymentAnalytics deploymentAnalytics, - IScriptCache scriptCache, ILogger logger, IPreDeployValidator preDeployValidator, ICollection contents) - : base(client, deploymentAnalytics, scriptCache, logger, preDeployValidator) + : base(client, deploymentAnalytics, logger, preDeployValidator) { Contents = contents; } diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Authoring/Fetch/JavaScriptFetchService.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Authoring/Fetch/JavaScriptFetchService.cs index 78c53fc..4235c10 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Authoring/Fetch/JavaScriptFetchService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Authoring/Fetch/JavaScriptFetchService.cs @@ -7,6 +7,7 @@ using Unity.Services.Cli.CloudCode.Service; using Unity.Services.Cli.CloudCode.Utils; using Unity.Services.Cli.Common.Utils; +using Unity.Services.DeploymentApi.Editor; namespace Unity.Services.Cli.CloudCode.Authoring; @@ -16,8 +17,6 @@ class JavaScriptFetchService : IFetchService readonly IJavaScriptClient m_Client; - readonly IDeployFileService m_DeployFileService; - readonly ICloudCodeScriptsLoader m_ScriptsLoader; readonly ICloudCodeInputParser m_InputParser; @@ -29,7 +28,6 @@ class JavaScriptFetchService : IFetchService public JavaScriptFetchService( IUnityEnvironment unityEnvironment, IJavaScriptClient client, - IDeployFileService deployFileService, ICloudCodeScriptsLoader scriptsLoader, ICloudCodeInputParser inputParser, ICloudCodeScriptParser scriptParser, @@ -37,7 +35,6 @@ public JavaScriptFetchService( { m_UnityEnvironment = unityEnvironment; m_Client = client; - m_DeployFileService = deployFileService; m_ScriptsLoader = scriptsLoader; m_InputParser = inputParser; m_ScriptParser = scriptParser; @@ -50,13 +47,16 @@ public JavaScriptFetchService( string IFetchService.FileExtension => CloudCodeConstants.JavaScriptFileExtension; public async Task FetchAsync( - FetchInput input, StatusContext? loadingContext, CancellationToken cancellationToken) + FetchInput input, + IReadOnlyList filePaths, + StatusContext? loadingContext, + CancellationToken cancellationToken) { var environmentId = await m_UnityEnvironment.FetchIdentifierAsync(cancellationToken); m_Client.Initialize(environmentId, input.CloudProjectId!, cancellationToken); loadingContext?.Status($"Reading {ServiceType} files..."); - var loadResult = await GetResourcesFromFilesAsync(input, cancellationToken); + var loadResult = await GetResourcesFromFilesAsync(filePaths, cancellationToken); loadingContext?.Status($"Fetching {ServiceType} Files..."); var result = await m_FetchHandler.FetchAsync( @@ -66,22 +66,23 @@ public async Task FetchAsync( input.Reconcile, cancellationToken); + result = new FetchResult( + created:result.Created, + updated: result.Updated, + deleted: result.Deleted, + authored:result.Fetched, + failed: result.Failed.Concat(loadResult.FailedContents.Cast()).ToList(), + dryRun: input.DryRun); + return result; } internal async Task GetResourcesFromFilesAsync( - FetchInput input, CancellationToken cancellationToken) + IReadOnlyList filePaths, CancellationToken cancellationToken) { - var files = m_DeployFileService.ListFilesToDeploy( - new[] - { - input.Path - }, - CloudCodeConstants.JavaScriptFileExtension); - var loadResult = await m_ScriptsLoader .LoadScriptsAsync( - files, + filePaths, ServiceType, CloudCodeConstants.JavaScriptFileExtension, m_InputParser, diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/CloudCodeModule.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/CloudCodeModule.cs index b491ebc..56aacfe 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/CloudCodeModule.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/CloudCodeModule.cs @@ -17,7 +17,6 @@ using Unity.Services.Cli.Common.Validator; using Unity.Services.Cli.Authoring.Service; using Unity.Services.CloudCode.Authoring.Editor.Core.Analytics; -using Unity.Services.CloudCode.Authoring.Editor.Core.Crypto; using Unity.Services.CloudCode.Authoring.Editor.Core.Deployment; using Unity.Services.CloudCode.Authoring.Editor.Core.Model; using Unity.Services.Gateway.CloudCodeApiV1.Generated.Api; @@ -305,8 +304,6 @@ public static void RegisterServices(HostBuilderContext hostBuilderContext, IServ serviceCollection.AddSingleton(); serviceCollection.AddSingleton(s => s.GetRequiredService()); serviceCollection.AddSingleton(s => s.GetRequiredService()); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CliCloudCodeDeploymentHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CliCloudCodeDeploymentHandler.cs index 3d45559..39af0cb 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CliCloudCodeDeploymentHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CliCloudCodeDeploymentHandler.cs @@ -13,11 +13,10 @@ class CliCloudCodeDeploymentHandler : CloudCodeDeploymentHandler public CliCloudCodeDeploymentHandler( TClient client, IDeploymentAnalytics deploymentAnalytics, - IScriptCache scriptCache, ILogger logger, IPreDeployValidator preDeployValidator) : - base(client, deploymentAnalytics, scriptCache, logger, preDeployValidator) + base(client, deploymentAnalytics, logger, preDeployValidator) { } protected override void UpdateScriptStatus( diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeAuthoringLogger.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeAuthoringLogger.cs index a8b48cc..1a123cb 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeAuthoringLogger.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeAuthoringLogger.cs @@ -14,17 +14,14 @@ public CloudCodeAuthoringLogger(ILogger logger) public void LogError(object message) { - m_Logger.LogError("{Message}", message.ToString()); } public void LogWarning(object message) { - m_Logger.LogWarning("{Message}", message.ToString()); } public void LogInfo(object message) { - m_Logger.LogInformation("{Message}", message.ToString()); } public void LogVerbose(object message) diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Unity.Services.Cli.CloudCode.csproj b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Unity.Services.Cli.CloudCode.csproj index 83e534f..0c19546 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Unity.Services.Cli.CloudCode.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Unity.Services.Cli.CloudCode.csproj @@ -21,7 +21,7 @@ - + diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Exceptions/ExceptionHelperTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Exceptions/ExceptionHelperTests.cs index fde8c37..2980980 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Exceptions/ExceptionHelperTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Exceptions/ExceptionHelperTests.cs @@ -211,9 +211,9 @@ public void ExceptionHandler_ExecuteUnhandledExceptionFlowCorrectly() k_MockHelper.MockLogger.Object, m_Context!)); k_MockHelper.MockDiagnostics.Verify(ex => - ex.AddData(TagKeys.DiagnosticName, "cli_unhandled_exception"), Times.Once); + ex.AddData(DiagnosticsTagKeys.DiagnosticName, "cli_unhandled_exception"), Times.Once); k_MockHelper.MockDiagnostics.Verify(ex => - ex.AddData(TagKeys.DiagnosticMessage, exception.ToString()), Times.Once); + ex.AddData(DiagnosticsTagKeys.DiagnosticMessage, exception.ToString()), Times.Once); var command = new StringBuilder("ugs"); foreach (var arg in m_Context!.ParseResult.Tokens) @@ -222,7 +222,7 @@ public void ExceptionHandler_ExecuteUnhandledExceptionFlowCorrectly() } k_MockHelper.MockDiagnostics.Verify(ex => - ex.AddData(TagKeys.Command, command.ToString()), Times.Once); + ex.AddData(DiagnosticsTagKeys.Command, command.ToString()), Times.Once); k_MockHelper.MockDiagnostics.Verify(ex => ex.AddData(TagKeys.Timestamp, It.IsAny()), Times.Once); diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Telemetry/AnalyticsEventBuilderTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Telemetry/AnalyticsEventBuilderTests.cs new file mode 100644 index 0000000..40407cb --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Telemetry/AnalyticsEventBuilderTests.cs @@ -0,0 +1,98 @@ +using System.IO.Abstractions; +using Moq; +using NUnit.Framework; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent.AnalyticEventFactory; + +namespace Unity.Services.Cli.Common.UnitTest.Telemetry; + +[TestFixture] +class AnalyticsEventBuilderTests +{ + Mock m_MockFactory = null!; + Mock m_MockFileSystem = null!; + AnalyticsEventBuilder m_AnalyticsEventBuilder = null!; + + [SetUp] + public void SetUp() + { + m_MockFactory = new Mock(); + m_MockFileSystem = new Mock(); + m_AnalyticsEventBuilder = new AnalyticsEventBuilder(m_MockFactory.Object, m_MockFileSystem.Object); + } + + [TestCase("a-command")] + public void SetCommand(string command) + { + m_AnalyticsEventBuilder.SetCommandName(command); + Assert.AreEqual(m_AnalyticsEventBuilder.Command, command); + } + + [TestCase("option1", "option2", "option3")] + public void AddOption(params string[] options) + { + foreach (var option in options) + { + m_AnalyticsEventBuilder.AddCommandOption(option); + } + + Assert.AreEqual(options.Length, m_AnalyticsEventBuilder.Options.Count); + for (var i = 0; i < m_AnalyticsEventBuilder.Options.Count; ++i) + { + Assert.AreEqual(options[i], m_AnalyticsEventBuilder.Options[i]); + } + } + + [TestCase("service1", "service2")] + public void AddService(params string[] services) + { + foreach (var service in services) + { + m_AnalyticsEventBuilder.AddAuthoringServiceProcessed(service); + } + + Assert.AreEqual(services.Length, m_AnalyticsEventBuilder.AuthoringServicesProcessed.Count); + for (var i = 0; i < m_AnalyticsEventBuilder.AuthoringServicesProcessed.Count; ++i) + { + Assert.AreEqual(services[i], m_AnalyticsEventBuilder.AuthoringServicesProcessed[i]); + } + } + + [TestCase(new[] { "not-found" }, 0, 0)] + [TestCase(new[] { "test_output_directory" }, 0, 1)] + [TestCase(new[] { "test_output_directory\\testfile.txt" }, 1, 0)] + [TestCase(new[] { "test_output_directory\\testfile.txt", "na", "test_output_directory", "test_output_directory\\testfile2.txt" }, 2, 1)] + public void SetAuthoringCommandlinePathsInputCount(IReadOnlyList filePaths, int expectedFileCount, int expectedFolderCount) + { + m_MockFileSystem.Setup(s => s.Path.GetFullPath("test_output_directory")) + .Returns("test_output_directory"); + m_MockFileSystem.Setup(s => s.Path.GetFullPath("test_output_directory\\testfile.txt")) + .Returns("test_output_directory\\testfile.txt"); + m_MockFileSystem.Setup(s => s.Path.GetFullPath("test_output_directory\\testfile2.txt")) + .Returns("test_output_directory\\testfile2.txt"); + m_MockFileSystem.Setup(s => s.Directory.Exists("test_output_directory")).Returns(true); + m_MockFileSystem.Setup(s => s.File.Exists("test_output_directory\\testfile.txt")).Returns(true); ; + m_MockFileSystem.Setup(s => s.File.Exists("test_output_directory\\testfile2.txt")).Returns(true); ; + + m_AnalyticsEventBuilder.SetAuthoringCommandlinePathsInputCount(filePaths); + + Assert.AreEqual(expectedFileCount, m_AnalyticsEventBuilder.FilePathsCommandlineInputCount); + Assert.AreEqual(expectedFolderCount, m_AnalyticsEventBuilder.FolderPathsCommandlineInputCount); + } + + [Test] + public void MetricsAreSent() + { + var mockAnalyticEvent = new Mock(); + mockAnalyticEvent + .Setup(e => e.Send()) + .Verifiable(); + m_MockFactory + .Setup(f => f.CreateMetricEvent()) + .Returns(mockAnalyticEvent.Object) + .Verifiable(); + m_AnalyticsEventBuilder.SendCommandCompletedEvent(); + mockAnalyticEvent.Verify(); + m_MockFactory.Verify(); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Unity.Services.Cli.Common.UnitTest.csproj b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Unity.Services.Cli.Common.UnitTest.csproj index 83495d0..faaa8aa 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Unity.Services.Cli.Common.UnitTest.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Unity.Services.Cli.Common.UnitTest.csproj @@ -18,6 +18,7 @@ + diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Exceptions/ExceptionHelper.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Exceptions/ExceptionHelper.cs index 6102d3c..3a85d39 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common/Exceptions/ExceptionHelper.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Exceptions/ExceptionHelper.cs @@ -121,15 +121,15 @@ void ExecuteUnhandledExceptionFlow(Exception exception, InvocationContext contex try { - Diagnostics.AddData(TagKeys.DiagnosticName, "cli_unhandled_exception"); - Diagnostics.AddData(TagKeys.DiagnosticMessage, exception.ToString()); + Diagnostics.AddData(DiagnosticsTagKeys.DiagnosticName, "cli_unhandled_exception"); + Diagnostics.AddData(DiagnosticsTagKeys.DiagnosticMessage, exception.ToString()); var command = new StringBuilder("ugs"); foreach (var arg in context.ParseResult.Tokens) { command.Append("_" + arg); } - Diagnostics.AddData(TagKeys.Command, command.ToString()); + Diagnostics.AddData(DiagnosticsTagKeys.Command, command.ToString()); Diagnostics.AddData(TagKeys.Timestamp, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); Diagnostics.Send(); diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticsEventBuilder.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticsEventBuilder.cs new file mode 100644 index 0000000..a1e1183 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticsEventBuilder.cs @@ -0,0 +1,59 @@ +using System.IO.Abstractions; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent.AnalyticEventFactory; + +namespace Unity.Services.Cli.Common.Telemetry.AnalyticEvent; + +public class AnalyticsEventBuilder : IAnalyticsEventBuilder +{ + readonly IAnalyticEventFactory m_AnalyticEventFactory; + readonly IFileSystem m_FileSystem; + internal string Command = ""; + internal readonly List Options = new(); + internal readonly List AuthoringServicesProcessed = new(); + internal int FolderPathsCommandlineInputCount = 0; + internal int FilePathsCommandlineInputCount = 0; + + public AnalyticsEventBuilder(IAnalyticEventFactory analyticEventFactory, IFileSystem fileSystem) + { + m_AnalyticEventFactory = analyticEventFactory; + m_FileSystem = fileSystem; + } + + public void SetCommandName(string commandName) + => Command = commandName; + + public void AddCommandOption(string optionName) + => Options.Add(optionName); + + public void AddAuthoringServiceProcessed(string service) + => AuthoringServicesProcessed.Add(service); + + public void SetAuthoringCommandlinePathsInputCount(IReadOnlyList filePaths) + { + foreach (var filePath in filePaths) + { + var fullPath = m_FileSystem.Path.GetFullPath(filePath); + + if (m_FileSystem.File.Exists(fullPath)) + { + FilePathsCommandlineInputCount++; + } + else if (m_FileSystem.Directory.Exists(fullPath)) + { + FolderPathsCommandlineInputCount++; + } + } + } + + public void SendCommandCompletedEvent() + { + var analyticEvent = m_AnalyticEventFactory.CreateMetricEvent(); + analyticEvent.AddData(TagKeys.Timestamp, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + analyticEvent.AddData(MetricTagKeys.Command, Command); + analyticEvent.AddData(MetricTagKeys.Options, Options.ToArray()); + analyticEvent.AddData(MetricTagKeys.ServicesProcessed, AuthoringServicesProcessed.ToArray()); + analyticEvent.AddData(MetricTagKeys.NbFilePaths, FilePathsCommandlineInputCount); + analyticEvent.AddData(MetricTagKeys.NbFolderPaths, FolderPathsCommandlineInputCount); + analyticEvent.Send(); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/IAnalyticsEventBuilder.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/IAnalyticsEventBuilder.cs new file mode 100644 index 0000000..b28f429 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/IAnalyticsEventBuilder.cs @@ -0,0 +1,10 @@ +namespace Unity.Services.Cli.Common.Telemetry.AnalyticEvent; + +public interface IAnalyticsEventBuilder +{ + public void SetCommandName(string name); + public void AddCommandOption(string optionName); + public void AddAuthoringServiceProcessed(string service); + public void SetAuthoringCommandlinePathsInputCount(IReadOnlyList filePaths); + public void SendCommandCompletedEvent(); +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/DiagnosticsTagKeys.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/DiagnosticsTagKeys.cs new file mode 100644 index 0000000..b19e340 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/DiagnosticsTagKeys.cs @@ -0,0 +1,10 @@ +namespace Unity.Services.Cli.Common.Telemetry; + +static class DiagnosticsTagKeys +{ + public const string DiagnosticName = "name"; + + public const string DiagnosticMessage = "message"; + + public const string Command = "cli_full_command"; +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/MetricTagKeys.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/MetricTagKeys.cs new file mode 100644 index 0000000..85f79d4 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/MetricTagKeys.cs @@ -0,0 +1,14 @@ +namespace Unity.Services.Cli.Common.Telemetry; + +static class MetricTagKeys +{ + public const string Command = "command"; + + public const string Options = "options"; + + public const string ServicesProcessed = "authoring_services_processed"; + + public const string NbFilePaths = "nb_file_paths_in_commandline"; + + public const string NbFolderPaths = "nb_folder_paths_in_commandline"; +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/TagKeys.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/TagKeys.cs index d7e182d..3830bf4 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/TagKeys.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/TagKeys.cs @@ -30,10 +30,4 @@ static class TagKeys /// Cli version /// public const string CliVersion = "application_version"; - - public const string DiagnosticName = "name"; - - public const string DiagnosticMessage = "message"; - - public const string Command = "cli_full_command"; } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Unity.Services.Cli.Common.csproj b/Unity.Services.Cli/Unity.Services.Cli.Common/Unity.Services.Cli.Common.csproj index 217fc50..4c33ff2 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common/Unity.Services.Cli.Common.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Unity.Services.Cli.Common.csproj @@ -13,6 +13,7 @@ + @@ -22,7 +23,7 @@ - + diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingServersApiV1Mock.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingServersApiV1Mock.cs index 42279e0..1c5bd99 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingServersApiV1Mock.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingServersApiV1Mock.cs @@ -17,7 +17,7 @@ class GameServerHostingServersApiV1Mock locationID: ValidLocationId, locationName: ValidLocationName, machineName:"", - machineSpec: new MachineSpec(""), + machineSpec: new MachineSpec1("2020-12-31T12:00:00Z", "2020-01-01T12:00:00Z", "test-cpu"), hardwareType: Server.HardwareTypeEnum.CLOUD, fleetID: new Guid(ValidFleetId), fleetName: ValidFleetName, diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/ServerGetOutputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/ServerGetOutputTests.cs index 9b34bcc..6928af4 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/ServerGetOutputTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/ServerGetOutputTests.cs @@ -26,7 +26,7 @@ public void SetUp() locationID: 3, locationName: "locationName", machineName: "test machine", - machineSpec: new MachineSpec("test-cpu"), + machineSpec: new MachineSpec1("2020-12-31T12:00:00Z", "2020-01-01T12:00:00Z", "test-cpu"), machineID: 5, port: 440, status: Server.StatusEnum.READY diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/ServersItemOutputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/ServersItemOutputTests.cs index 9dacc82..401f45b 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/ServersItemOutputTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/ServersItemOutputTests.cs @@ -16,7 +16,7 @@ public void SetUp() port: 9000, machineID: ValidMachineId, machineName: "test machine", - machineSpec: new MachineSpec("test-cpu"), + machineSpec: new MachineSpec1("2020-12-31T12:00:00Z", "2020-01-01T12:00:00Z", "test-cpu"), locationID: ValidLocationId, locationName: ValidLocationName, fleetID: new Guid(ValidFleetId), diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/ServersOutputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/ServersOutputTests.cs index e299f1a..33bb61e 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/ServersOutputTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/ServersOutputTests.cs @@ -18,7 +18,7 @@ public void SetUp() port: 9000, machineID: ValidMachineId, machineName: "test machine", - machineSpec: new MachineSpec("test-cpu"), + machineSpec: new MachineSpec1("2020-12-31T12:00:00Z", "2020-01-01T12:00:00Z", "test-cpu"), locationID: ValidLocationId, locationName: ValidLocationName, fleetID: new Guid(ValidFleetId), diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Unity.Services.Cli.GameServerHosting.csproj b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Unity.Services.Cli.GameServerHosting.csproj index 6c24145..7fa5c33 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Unity.Services.Cli.GameServerHosting.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Unity.Services.Cli.GameServerHosting.csproj @@ -24,7 +24,7 @@ - + diff --git a/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/ServiceMocks/GameServerHosting/GameServerHostingApiMock.cs b/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/ServiceMocks/GameServerHosting/GameServerHostingApiMock.cs index 248141e..749ba0e 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/ServiceMocks/GameServerHosting/GameServerHostingApiMock.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/ServiceMocks/GameServerHosting/GameServerHostingApiMock.cs @@ -13,8 +13,8 @@ public class GameServerHostingApiMock : IServiceApiMock { public async Task> CreateMappingModels() { - var models = await MappingModelUtils.ParseMappingModelsAsync( - "https://services.docs.unity.com/specs/v1/6d756c7469706c61792d636f6e666967.yaml", + var models = await MappingModelUtils.ParseMappingModelsFromGeneratorConfigAsync( + "game-server-hosting-api-v1-generator-config.yaml", new WireMockOpenApiParserSettings() ); @@ -92,7 +92,7 @@ static void MockServerGet(WireMockServer mockServer) locationName: "Test Location", machineID: 123, machineName: "test machine", - machineSpec: new MachineSpec("test-cpu"), + machineSpec: new MachineSpec1("2020-12-31T12:00:00Z", "2020-01-01T12:00:00Z", "test-cpu"), port: 0, status: Server.StatusEnum.ONLINE ); @@ -124,7 +124,7 @@ static void MockServerList(WireMockServer mockServer) locationName: "Test Location", machineID: 123, machineName: "test machine", - machineSpec: new MachineSpec("test-cpu"), + machineSpec: new MachineSpec1("2020-12-31T12:00:00Z", "2020-01-01T12:00:00Z", "test-cpu"), port: 0, status: Server.StatusEnum.ONLINE ) diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/CloudCode/CloudCodeDeployTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/CloudCode/CloudCodeDeployTests.cs index 6e5968a..3cce7c6 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/CloudCode/CloudCodeDeployTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/CloudCode/CloudCodeDeployTests.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json; using NUnit.Framework; using Unity.Services.Cli.Authoring.Model; +using Unity.Services.Cli.Authoring.Model.TableOutput; using Unity.Services.Cli.MockServer.Common; using Unity.Services.Cli.MockServer.ServiceMocks; using Unity.Services.DeploymentApi.Editor; @@ -76,6 +77,7 @@ public async Task SetUp() Directory.CreateDirectory(k_TestDirectory); await m_MockApi.MockServiceAsync(new IdentityV1Mock()); await m_MockApi.MockServiceAsync(new CloudCodeV1Mock()); + await m_MockApi.MockServiceAsync(new LeaderboardApiMock()); } [TearDown] @@ -116,7 +118,7 @@ public async Task DeployValidConfigFromDirectorySucceedWithJsonOutput() Array.Empty(), m_DeployedContents, Array.Empty()); - var resultString = JsonConvert.SerializeObject(logResult, Formatting.Indented); + var resultString = JsonConvert.SerializeObject(logResult.ToTable(), Formatting.Indented); await GetLoggedInCli() .Command($"deploy {k_TestDirectory} -j") .AssertStandardOutputContains(resultString) @@ -138,7 +140,7 @@ public async Task DeployConfig_DryRun() Array.Empty(), Array.Empty(), true); - var resultString = JsonConvert.SerializeObject(logResult, Formatting.Indented); + var resultString = JsonConvert.SerializeObject(logResult.ToTable(), Formatting.Indented); await GetLoggedInCli() .Command($"deploy {k_TestDirectory} -j --dry-run") .AssertStandardOutputContains(resultString) @@ -155,20 +157,59 @@ public async Task DeployWithReconcileWillDeleteRemoteFiles() var deletedStatus = new DeploymentStatus("Deployed", "Deleted remotely", SeverityLevel.Success); - var logResult = DeployTestsFixture.CreateResult( + var logResultCc = DeployTestsFixture.CreateResult( m_DeployedContents, Array.Empty(), new[] { - new DeployContent("example-string.js", "Cloud Code Scripts", "", 100, deletedStatus), - new DeployContent("example-string.js", "Cloud Code Scripts", "",100, deletedStatus), - new DeployContent("example-string.js", "Cloud Code Scripts", "", 100, deletedStatus), - new DeployContent("ExistingModule.ccm", "JS", "", 100, deletedStatus), - new DeployContent("AnotherExistingModule.ccm", "JS", "", 100, deletedStatus), + new DeployContent( + "example-string.js", + "Cloud Code Scripts", + "", + 100, + deletedStatus), + new DeployContent( + "example-string.js", + "Cloud Code Scripts", + "", + 100, + deletedStatus), + new DeployContent( + "example-string.js", + "Cloud Code Scripts", + "", + 100, + deletedStatus) }, m_DeployedContents, Array.Empty()); - var resultString = JsonConvert.SerializeObject(logResult, Formatting.Indented); + + var logResultCcm = DeployTestsFixture.CreateResult( + Array.Empty(), + Array.Empty(), + new[] + { + new DeployContent( + "ExistingModule.ccm", + "JS", + "", + 100, + deletedStatus), + new DeployContent( + "AnotherExistingModule.ccm", + "JS", + "", + 100, + deletedStatus), + }, + Array.Empty(), + Array.Empty()); + + var finalResult = new TableContent(); + finalResult.AddRows(logResultCc.ToTable()); + finalResult.AddRows(logResultCcm.ToTable()); + + var resultString = JsonConvert.SerializeObject(finalResult, Formatting.Indented); await GetLoggedInCli() .Command($"deploy {k_TestDirectory} -j --reconcile -s cloud-code-scripts -s cloud-code-modules") .AssertStandardOutputContains(resultString) @@ -189,7 +230,7 @@ public async Task DeployWithoutReconcileWillNotDeleteRemoteFiles() Array.Empty(), m_DeployedContents, Array.Empty()); - var resultString = JsonConvert.SerializeObject(logResult, Formatting.Indented); + var resultString = JsonConvert.SerializeObject(logResult.ToTable(), Formatting.Indented); await GetLoggedInCli() .Command($"deploy {k_TestDirectory} -j") .AssertStandardOutputContains(resultString) diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/Leaderboards/LeaderboardDeployTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/Leaderboards/LeaderboardDeployTests.cs new file mode 100644 index 0000000..3d41844 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/Leaderboards/LeaderboardDeployTests.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using NUnit.Framework; +using Unity.Services.Cli.Authoring.Model; +using Unity.Services.Cli.Common.Networking; +using Unity.Services.Cli.MockServer; +using Unity.Services.Cli.MockServer.Common; +using Unity.Services.Cli.MockServer.ServiceMocks; + +namespace Unity.Services.Cli.IntegrationTest.Authoring.Deploy.Leaderboards; + +/* + * This is a temp file, there's a lot of common code with DeployTests, just to include leaderboard deploy command to the integration tests. + * Since leaderboard deploy is hiding behind a feature, a separate test file will be easier to be excluded by feature flag. + */ +public class LeaderboardDeployTests : UgsCliFixture +{ + + static readonly string k_TestDirectory = Path.Combine(UgsCliBuilder.RootDirectory, ".tmp", "FilesDir"); + + readonly IReadOnlyList m_DeployedTestCases = new[] + { + new AuthoringTestCase( + "{ \"id\": \"lb1\", \"name\": \"foo\", \"sortOrder\": 1, \"updateType\": 1, \"created\": \"2023-01-01\", \"updated\": \"2023-01-01\"}", + "leaderboard.lb", + "Leaderboard", + 100, + "Deployed", + "Deployed Successfully", + k_TestDirectory), + }; + + List m_DeployedContents = new(); + List m_FailedContents = new(); + + [SetUp] + public async Task SetUp() + { + DeleteLocalConfig(); + DeleteLocalCredentials(); + + if (Directory.Exists(k_TestDirectory)) + { + Directory.Delete(k_TestDirectory, true); + } + + m_DeployedContents.Clear(); + m_FailedContents.Clear(); + m_MockApi.Server?.ResetMappings(); + await m_MockApi.MockServiceAsync(new LeaderboardApiMock()); + await m_MockApi.MockServiceAsync(new IdentityV1Mock()); + Directory.CreateDirectory(k_TestDirectory); + } + + [TearDown] + public void TearDown() + { + DeleteLocalConfig(); + DeleteLocalCredentials(); + + if (Directory.Exists(k_TestDirectory)) + { + Directory.Delete(k_TestDirectory, true); + } + } + + static async Task CreateDeployTestFilesAsync(IReadOnlyList testCases, ICollection contents) + { + foreach (var testCase in testCases) + { + await File.WriteAllTextAsync(testCase.ConfigFilePath, testCase.ConfigValue); + contents.Add(testCase.DeployedContent); + } + } + + [Test] + public async Task DeployValidConfigFromDirectoryWithOptionSucceed() + { + await CreateDeployTestFilesAsync(m_DeployedTestCases, m_DeployedContents); + var deployedConfigFileString = string.Join(System.Environment.NewLine + " ", m_DeployedTestCases.Select(r => $"'{r.ConfigFilePath}'")); + await GetLoggedInCli() + .Command($"deploy {k_TestDirectory} -p {CommonKeys.ValidProjectId} -e {CommonKeys.ValidEnvironmentName}") + .AssertStandardOutputContains($"Successfully deployed the following files:{System.Environment.NewLine} {deployedConfigFileString}") + .AssertNoErrors() + .ExecuteAsync(); + } + + [Test] + public async Task DeployValidConfigFromDirectorySucceed() + { + SetConfigValue("project-id", CommonKeys.ValidProjectId); + SetConfigValue("environment-name", CommonKeys.ValidEnvironmentName); + await CreateDeployTestFilesAsync(m_DeployedTestCases, m_DeployedContents); + var deployedConfigFileString = string.Join(System.Environment.NewLine + " ", m_DeployedTestCases.Select(r => $"'{r.ConfigFilePath}'")); + await GetLoggedInCli() + .Command($"deploy {k_TestDirectory}") + .AssertStandardOutputContains($"Successfully deployed the following files:{System.Environment.NewLine} {deployedConfigFileString}") + .AssertNoErrors() + .ExecuteAsync(); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/RemoteConfig/RemoteConfigDeployTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/RemoteConfig/RemoteConfigDeployTests.cs index f43fec4..46e56df 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/RemoteConfig/RemoteConfigDeployTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/RemoteConfig/RemoteConfigDeployTests.cs @@ -6,10 +6,13 @@ using Newtonsoft.Json; using NUnit.Framework; using Unity.Services.Cli.Authoring.Model; +using Unity.Services.Cli.Authoring.Model.TableOutput; using Unity.Services.Cli.Common.Exceptions; using Unity.Services.Cli.MockServer.Common; using Unity.Services.Cli.MockServer.ServiceMocks; using Unity.Services.Cli.MockServer.ServiceMocks.RemoteConfig; +using Unity.Services.Cli.RemoteConfig.Deploy; +using Unity.Services.Cli.RemoteConfig.Model; using Unity.Services.DeploymentApi.Editor; namespace Unity.Services.Cli.IntegrationTest.Authoring.Deploy.RemoteConfig; @@ -118,6 +121,7 @@ public async Task SetUp() await m_MockApi.MockServiceAsync(new IdentityV1Mock()); await m_MockApi.MockServiceAsync(new RemoteConfigMock()); + await m_MockApi.MockServiceAsync(new LeaderboardApiMock()); } static async Task CreateDeployTestFilesAsync(IReadOnlyList testCases, ICollection contents) @@ -231,11 +235,11 @@ public async Task DeployConfigWithInvalidWithJsonOutputOnlyFailInvalid() var createdEntries = m_DeployedTestCases .SelectMany(tc => RemoteConfigFileContent.RemoteConfigToDeployContents( - tc , + tc, new DeploymentStatus(Statuses.Created, string.Empty))) .ToList(); - var logResult = DeployTestsFixture.CreateResult( + var logResult = DeployTestsFixture.CreateTableResult( createdEntries, Array.Empty(), Array.Empty(), @@ -263,14 +267,26 @@ public async Task DeployConfig_DryRun() 100f)) .ToList(); - var logResult = new DeploymentResult( + var deployFiles = + m_DryRunDeployedTestCases + .Select(d => + new DeployContent( + d.ConfigFileName, + "RemoteConfig File", + d.ConfigFilePath, + 0, + Statuses.Loaded)) + .ToList(); + + var logResult = new RemoteConfigDeploymentResult( Array.Empty(), Array.Empty(), dc, - Array.Empty(), + deployFiles, Array.Empty(), true); - var resultString = JsonConvert.SerializeObject(logResult, Formatting.Indented); + + var resultString = JsonConvert.SerializeObject(logResult.ToTable(), Formatting.Indented); await GetLoggedInCli() .Command($"deploy {k_TestDirectory} -j --dry-run") @@ -290,12 +306,12 @@ public async Task DeployWithReconcileWillDeleteRemoteFiles() .SelectMany(tc => RemoteConfigFileContent.RemoteConfigToDeployContents(tc, new DeploymentStatus(Statuses.Created, string.Empty))) .ToList(); - var logResult = DeployTestsFixture.CreateResult( + var logResult = DeployTestsFixture.CreateTableResult( createdEntries, Array.Empty(), new[] { - new DeployContent("test", "RemoteConfig Entry", "Remote", 100, "Deleted") + new CliRemoteConfigEntry("test", "RemoteConfig Entry", "Remote", 100, "Deleted") }, m_ReconcileTestCases.Select(tc => tc.DeployedContent).ToList(), Array.Empty()); @@ -319,7 +335,7 @@ public async Task DeployWithoutReconcileWillNotDeleteRemoteFiles() .SelectMany(tc => RemoteConfigFileContent.RemoteConfigToDeployContents(tc, new DeploymentStatus(Statuses.Created, string.Empty))) .ToList(); - var logResult = DeployTestsFixture.CreateResult( + var logResult = DeployTestsFixture.CreateTableResult( createdEntries, Array.Empty(), Array.Empty(), diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/RemoteConfig/RemoteConfigFileContent.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/RemoteConfig/RemoteConfigFileContent.cs index 9f9b07f..2ef7c6e 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/RemoteConfig/RemoteConfigFileContent.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/RemoteConfig/RemoteConfigFileContent.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using Newtonsoft.Json; using Unity.Services.Cli.Authoring.Model; +using Unity.Services.Cli.RemoteConfig.Deploy; +using Unity.Services.Cli.RemoteConfig.Model; using Unity.Services.DeploymentApi.Editor; namespace Unity.Services.Cli.IntegrationTest.Authoring.Deploy.RemoteConfig; @@ -38,12 +40,14 @@ public static List RemoteConfigContentToDeployContents( foreach (var kvp in json!.entries) { - deployContents.Add(new DeployContent( + deployContents.Add(new CliRemoteConfigEntry( kvp.Key, "RemoteConfig Entry", path, progress, - status)); + status.Message, + status.MessageDetail, + status.MessageSeverity)); } return deployContents; diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/DeployTestsFixture.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/DeployTestsFixture.cs index 1dd464e..9a27726 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/DeployTestsFixture.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/DeployTestsFixture.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json; using NUnit.Framework; using Unity.Services.Cli.Authoring.Model; +using Unity.Services.Cli.Authoring.Model.TableOutput; using Unity.Services.Cli.Common.Exceptions; using Unity.Services.Cli.MockServer.ServiceMocks; @@ -137,7 +138,7 @@ public virtual async Task DeployValidConfigFromDirectorySucceedWithJsonOutput() deployedContents, Array.Empty()); - var resultString = JsonConvert.SerializeObject(logResult, Formatting.Indented); + var resultString = JsonConvert.SerializeObject(logResult.ToTable(), Formatting.Indented); await GetFullySetCli() .Command($"deploy {k_TestDirectory} -j") .AssertStandardOutputContains(resultString) @@ -161,7 +162,7 @@ public virtual async Task DeployConfig_DryRun() Array.Empty(), Array.Empty(), true); - var resultString = JsonConvert.SerializeObject(logResult, Formatting.Indented); + var resultString = JsonConvert.SerializeObject(logResult.ToTable(), Formatting.Indented); await GetFullySetCli() .Command($"deploy {k_TestDirectory} -j --dry-run") .AssertStandardOutputContains(resultString) @@ -184,7 +185,7 @@ public virtual async Task DeployWithoutReconcileWillNotDeleteRemoteFiles() Array.Empty(), contentList, Array.Empty()); - var resultString = JsonConvert.SerializeObject(logResult, Formatting.Indented); + var resultString = JsonConvert.SerializeObject(logResult.ToTable(), Formatting.Indented); await GetFullySetCli() .Command($"deploy {k_TestDirectory} -j") @@ -235,4 +236,35 @@ public static DeploymentResult CreateResult( return new DeploymentResult(updated, deleted, createdCopy, deployed, failed, dryRun); } + + public static TableContent CreateTableResult( + IReadOnlyList created, + IReadOnlyList updated, + IReadOnlyList deleted, + IReadOnlyList deployed, + IReadOnlyList failed, + bool dryRun = false) + { + var tableResult = new TableContent(); + + foreach (var item in deployed) + { + tableResult.AddRow(RowContent.ToRow(item)); + + var updatedRows = updated.Where(i => i.Path == item.Path).Select(RowContent.ToRow).ToList(); + var createdRows = created.Where(i => i.Path == item.Path).Select(RowContent.ToRow).ToList(); + + tableResult.AddRows(updatedRows); + tableResult.AddRows(createdRows); + + } + + var deletedRows = deleted.Select(RowContent.ToRow).ToList(); + var failedRows = failed.Select(RowContent.ToRow).ToList(); + + tableResult.AddRows(deletedRows); + tableResult.AddRows(failedRows); + + return tableResult; + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/CloudCodeFetchTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/CloudCodeFetchTests.cs index f905717..6216f29 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/CloudCodeFetchTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/CloudCodeFetchTests.cs @@ -95,6 +95,7 @@ public async Task SetUp() { await m_MockApi.MockServiceAsync(new IdentityV1Mock()); await m_MockApi.MockServiceAsync(new CloudCodeFetchMock()); + await m_MockApi.MockServiceAsync(new LeaderboardApiMock()); } [TearDown] @@ -148,7 +149,7 @@ public async Task FetchValidConfigFromDirectorySucceedWithJsonOutput(string dryR fetchedPaths, Array.Empty(), !string.IsNullOrEmpty(dryRunOption) ); - var resultString = JsonConvert.SerializeObject(res, Formatting.Indented); + var resultString = JsonConvert.SerializeObject(res.ToTable(), Formatting.Indented); await GetLoggedInCli() .Command($"fetch {k_TestDirectory} {dryRunOption} -j") .AssertStandardOutputContains(resultString) @@ -183,7 +184,7 @@ public async Task FetchValidConfigReconcileSucceedWithJsonOutput(string dryRunOp fetchedPaths, Array.Empty(), !string.IsNullOrEmpty(dryRunOption)); - var resultString = JsonConvert.SerializeObject(logResult, Formatting.Indented); + var resultString = JsonConvert.SerializeObject(logResult.ToTable(), Formatting.Indented); await GetLoggedInCli() .Command($"fetch {k_TestDirectory} --reconcile -s cloud-code-scripts {dryRunOption} -j ") .AssertStandardOutputContains(resultString) diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/LeaderboardFetchTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/LeaderboardFetchTests.cs new file mode 100644 index 0000000..1f2b85e --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/LeaderboardFetchTests.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using NUnit.Framework; +using Unity.Services.Cli.Authoring.Model; +using Unity.Services.Cli.Leaderboards.Deploy; +using Unity.Services.Cli.MockServer.Common; +using Unity.Services.Cli.MockServer.ServiceMocks; +using Unity.Services.DeploymentApi.Editor; +using Unity.Services.Leaderboards.Authoring.Core.Model; +using Unity.Services.Leaderboards.Authoring.Core.Validations; +using Statuses = Unity.Services.Cli.Authoring.Model.Statuses; + +namespace Unity.Services.Cli.IntegrationTest.Authoring.Fetch; + +public class LeaderboardFetchTests : UgsCliFixture +{ + static readonly string k_TestDirectory = Path.Combine(UgsCliBuilder.RootDirectory, ".tmp", "FilesDir"); + LeaderboardConfig[]? m_LocalLeaderboards; + LeaderboardConfig[]? m_RemoteLeaderboards; + + [SetUp] + public async Task SetUp() + { + DeleteLocalConfig(); + DeleteLocalCredentials(); + + if (Directory.Exists(k_TestDirectory)) + { + Directory.Delete(k_TestDirectory, true); + } + + m_MockApi.Server?.ResetMappings(); + await m_MockApi.MockServiceAsync(new LeaderboardApiMock()); + await m_MockApi.MockServiceAsync(new IdentityV1Mock()); + Directory.CreateDirectory(k_TestDirectory); + m_LocalLeaderboards = new LeaderboardConfig[] + { + new ("lb1", "leaderboard 1") { Path = Path.Combine(k_TestDirectory, "lb1.lb") } + }; + + m_RemoteLeaderboards = new LeaderboardConfig[] + { + new ("lb1", "leaderboard 1") { Path = Path.Combine(k_TestDirectory, "lb1.lb") }, + new ("lb2", "leaderboard 2") { Path = Path.Combine(k_TestDirectory, "lb2.lb") } + }; + } + + [TearDown] + public void TearDown() + { + DeleteLocalConfig(); + DeleteLocalCredentials(); + + if (Directory.Exists(k_TestDirectory)) + { + Directory.Delete(k_TestDirectory, true); + } + } + + static async Task CreateDeployTestFilesAsync(IReadOnlyList testCases) + { + var serializer = new LeaderboardsSerializer(); + foreach (var testCase in testCases) + { + var test = serializer.Serialize(testCase); + await File.WriteAllTextAsync(testCase.Path, test); + } + } + + [Test] + public async Task FetchToValidConfigFromDirectorySucceeds() + { + var localLeaderboards = m_LocalLeaderboards!; + await CreateDeployTestFilesAsync(localLeaderboards); + var expectedResult = new FetchResult( + updated: new IDeploymentItem[]{ localLeaderboards[0] }, + deleted: Array.Empty(), + created: Array.Empty(), + authored: new IDeploymentItem[]{ localLeaderboards[0] }, + failed: Array.Empty() + ); + await GetFullySetCli() + .Command($"fetch {k_TestDirectory} -s leaderboards") + .AssertStandardOutputContains(expectedResult.ToString()) + .AssertNoErrors() + .ExecuteAsync(); + } + + [Test] + public async Task FetchToValidConfigFromDirectoryReconcileSucceeds() + { + var localLeaderboards = m_LocalLeaderboards!; + await CreateDeployTestFilesAsync(localLeaderboards); + var expectedResult = new FetchResult( + updated: new IDeploymentItem[]{ localLeaderboards[0] }, + deleted: Array.Empty(), + created: new IDeploymentItem[]{ m_RemoteLeaderboards![1] }, + authored: new IDeploymentItem[]{ localLeaderboards[0], m_RemoteLeaderboards![1] }, + failed: Array.Empty() + ); + await GetFullySetCli() + .Command($"fetch {k_TestDirectory} --reconcile -s leaderboards") + .AssertStandardOutputContains(expectedResult.ToString()) + .AssertNoErrors() + .ExecuteAsync(); + } + + [Test] + public async Task FetchToValidConfigFromDirectoryDryRunSucceeds() + { + var localLeaderboards = m_LocalLeaderboards!; + await CreateDeployTestFilesAsync(localLeaderboards); + var expectedResult = new FetchResult( + updated: new IDeploymentItem[]{ localLeaderboards[0] }, + deleted: Array.Empty(), + created: Array.Empty(), + authored: Array.Empty(), + failed: Array.Empty(), + dryRun: true + ); + await GetFullySetCli() + .Command($"fetch {k_TestDirectory} --dry-run -s leaderboards") + .AssertStandardOutputContains(expectedResult.ToString()) + .AssertNoErrors() + .ExecuteAsync(); + } + + [Test] + public async Task FetchToValidConfigFromDirectoryDryRunWithReconcileSucceeds() + { + var localLeaderboards = m_LocalLeaderboards!; + await CreateDeployTestFilesAsync(localLeaderboards); + var expectedResult = new FetchResult( + updated: new IDeploymentItem[]{ localLeaderboards[0] }, + deleted: Array.Empty(), + created: new IDeploymentItem[]{ m_RemoteLeaderboards![1] }, + authored: Array.Empty(), + failed: Array.Empty(), + dryRun: true + ); + await GetFullySetCli() + .Command($"fetch {k_TestDirectory} --reconcile --dry-run -s leaderboards") + .AssertStandardOutputContains(expectedResult.ToString()) + .AssertNoErrors() + .ExecuteAsync(); + } + + [Test] + public async Task FetchToValidConfigFromDuplicateIdFails() + { + var localLeaderboards = m_LocalLeaderboards!.Append( + new LeaderboardConfig("lb1", "eh") + { + Path = Path.Combine(k_TestDirectory, "lb11.lb") + }).ToList(); + await CreateDeployTestFilesAsync(localLeaderboards); + + foreach (var lb in localLeaderboards) + { + var failedMessage1 = + DuplicateResourceValidation.GetDuplicateResourceErrorMessages(lb, localLeaderboards); + var failedStatus = Statuses.GetFailedToFetch(failedMessage1.Item2); + lb.Status = failedStatus; + } + + await CreateDeployTestFilesAsync(localLeaderboards); + var expectedResult = new FetchResult( + updated: Array.Empty(), + deleted: Array.Empty(), + created: Array.Empty(), + authored: Array.Empty(), + failed: new IDeploymentItem[]{ localLeaderboards[0], localLeaderboards[1] } + ); + await GetFullySetCli() + .Command($"fetch {k_TestDirectory} -s leaderboards") + .AssertStandardOutputContains(expectedResult.ToString()) + .ExecuteAsync(); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/RemoteConfigFetchTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/RemoteConfigFetchTests.cs index aea24a5..43045b9 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/RemoteConfigFetchTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/RemoteConfigFetchTests.cs @@ -12,6 +12,8 @@ using Unity.Services.Cli.MockServer.Common; using Unity.Services.Cli.MockServer.ServiceMocks; using Unity.Services.Cli.MockServer.ServiceMocks.RemoteConfig; +using Unity.Services.Cli.RemoteConfig.Deploy; +using Unity.Services.Cli.RemoteConfig.Model; using Unity.Services.DeploymentApi.Editor; namespace Unity.Services.Cli.IntegrationTest.Authoring.Fetch; @@ -52,8 +54,8 @@ public RemoteConfigFetchTests() { m_FetchedKeysTestCases = new[] { - new DeployContent("color" , "RemoteConfig Key", m_FetchedTestCases[0].ConfigFilePath, 100f, new DeploymentStatus(Statuses.Deleted, string.Empty)), - new DeployContent("ready" , "RemoteConfig Key", m_FetchedTestCases[1].ConfigFilePath, 100f, new DeploymentStatus(Statuses.Deleted, string.Empty)) + new CliRemoteConfigEntry("color" , "RemoteConfig Key", m_FetchedTestCases[0].ConfigFilePath, 100f, Statuses.Deleted, string.Empty), + new CliRemoteConfigEntry("ready" , "RemoteConfig Key", m_FetchedTestCases[1].ConfigFilePath, 100f, Statuses.Deleted, string.Empty) }; } @@ -75,6 +77,7 @@ public async Task SetUp() await m_MockApi.MockServiceAsync(new IdentityV1Mock()); await m_MockApi.MockServiceAsync(new RemoteConfigMock()); + await m_MockApi.MockServiceAsync(new LeaderboardApiMock()); } [TearDown] @@ -168,14 +171,14 @@ public async Task FetchValidConfigFromDirectorySucceedWithJsonOutput() .Select(t => t.DeployedContent) .ToArray(); - var logResult = new FetchResult( + var logResult = new RemoteConfigFetchResult( Array.Empty(), m_FetchedKeysTestCases, Array.Empty(), fetchedPaths, Array.Empty(), false); - var resultString = JsonConvert.SerializeObject(logResult, Formatting.Indented); + var resultString = JsonConvert.SerializeObject(logResult.ToTable(), Formatting.Indented); await GetLoggedInCli() .Command($"fetch {k_TestDirectory} -j") .AssertStandardOutputContains(resultString) diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/FetchTestsFixture.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/FetchTestsFixture.cs index 807cbf7..2f6ad16 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/FetchTestsFixture.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/FetchTestsFixture.cs @@ -42,6 +42,7 @@ public async Task SetUp() m_MockApi.Server?.ResetMappings(); await m_MockApi.MockServiceAsync(new IdentityV1Mock()); + await m_MockApi.MockServiceAsync(new LeaderboardApiMock()); } public static AuthoringTestCase SetTestCase(AuthoringTestCase testCase, string status) @@ -268,7 +269,7 @@ protected static string FormatDefaultOutput(List deployContentLis protected static string FormatJsonOutput(List deployContentList, bool isDryRun) { var fetchResult = GetFetchResult(deployContentList, isDryRun); - return JsonConvert.SerializeObject(fetchResult, Formatting.Indented); + return JsonConvert.SerializeObject(fetchResult.ToTable(), Formatting.Indented); } static FetchResult GetFetchResult(List deployContentList, bool isDryRun) diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Common/UgsCliTestCase.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Common/UgsCliTestCase.cs index 9fef259..74bfe6d 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Common/UgsCliTestCase.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Common/UgsCliTestCase.cs @@ -45,6 +45,11 @@ public UgsCliTestCase Command(string arguments) return Command(wrapper); } + /// + /// This command runs in the same process so that we can use breakpoints and + /// debug it. It has limitations compared to running the seperate executable + /// such as not being able to handle stdin + /// public UgsCliTestCase DebugCommand(string arguments) { NetworkTargetEndpoints.UseMockEndpoints = true; diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingTests.cs index 01c73e7..954d4de 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingTests.cs @@ -7,6 +7,7 @@ namespace Unity.Services.Cli.IntegrationTest.GameServerHostingTests; +[Ignore("Disable until fixed by GHS")] public partial class GameServerHostingTests : UgsCliFixture { [OneTimeSetUp] diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/LeaderboardTests/LeaderboardSetupFailureTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/LeaderboardTests/LeaderboardSetupFailureTests.cs index af70617..ee1c55c 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/LeaderboardTests/LeaderboardSetupFailureTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/LeaderboardTests/LeaderboardSetupFailureTests.cs @@ -26,8 +26,6 @@ public void SetUp() } [TestCase("leaderboards list")] - [TestCase("leaderboards create createBody.lb")] - [TestCase("leaderboards update foo-pid createBody.lb")] [TestCase("leaderboards delete foo-id")] [TestCase("leaderboards get foo-id")] [TestCase("leaderboards reset foo-id")] @@ -43,8 +41,6 @@ await GetLoggedInCli() } [TestCase("leaderboards list")] - [TestCase("leaderboards create createBody.lb")] - [TestCase("leaderboards update foo-pid createBody.lb")] [TestCase("leaderboards delete foo-id")] [TestCase("leaderboards get foo-id")] [TestCase("leaderboards reset foo-id")] @@ -61,8 +57,6 @@ public async Task LeaderboardListThrowsNotLoggedInException(string command) } [TestCase("leaderboards list")] - [TestCase("leaderboards create createBody.lb")] - [TestCase("leaderboards update foo-pid createBody.lb")] [TestCase("leaderboards delete foo-id")] [TestCase("leaderboards get foo-id")] [TestCase("leaderboards reset foo-id")] diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/LeaderboardTests/LeaderboardTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/LeaderboardTests/LeaderboardTests.cs index 0387de0..4729abd 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/LeaderboardTests/LeaderboardTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/LeaderboardTests/LeaderboardTests.cs @@ -178,22 +178,6 @@ public async Task LeaderboardResetSucceed() var expectedMessage = "leaderboard reset! Version Id: v10"; await AssertSuccess("leaderboards reset lb1", expectedMessage); } - - [Test] - public async Task LeaderboardCreateSucceed() - { - - var expectedMessage = "leaderboard created!"; - await AssertSuccess($"leaderboards create {k_TestDirectory}/{k_LeaderboardFileName}", expectedMessage); - } - - [Test] - public async Task LeaderboardUpdateSucceed() - { - var expectedMessage = "leaderboard updated!"; - await AssertSuccess($"leaderboards update lb1 {k_TestDirectory}/{k_LeaderboardFileName}", expectedMessage); - } - [Test] public async Task LeaderboardImportSucceed() { @@ -240,31 +224,6 @@ public async Task LeaderboardExportWithSameNameSucceed() var errorMessage = "The filename to export to already exists. Please create a new file"; await AssertException($"leaderboards export {k_TestDirectory} {k_alternateFileName}", errorMessage); } - - [TestCase("create")] - [TestCase("update foo")] - public async Task LeaderboardInvalidFilePath(string command) - { - var expectedMessage = "Invalid file path."; - await AssertException($"leaderboards {command} /InvalidFilePath/foo.lb", expectedMessage); - } - - [TestCase("create")] - [TestCase("update foo")] - public async Task LeaderboardWrongField(string command) - { - var expectedMessage = "Failed to deserialize object for Leaderboard request: Unexpected end when reading JSON."; - await AssertException($"leaderboards {command} {k_TestDirectory}/{k_brokenFile}", expectedMessage); - } - - [Test] - public async Task LeaderboardCreateMissingRequiredField() - { - var expectedMessage = "Failed to deserialize object for Leaderboard request: Required property 'sortOrder' not found"; - await AssertException($"leaderboards create {k_TestDirectory}/{k_MissingFieldLeaderboardFileName}", expectedMessage); - - } - static async Task AssertSuccess(string command, string? expectedMessage = null, string? expectedResult = null) { var test = GetLoggedInCli() diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardClientTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardClientTests.cs new file mode 100644 index 0000000..0d048e7 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardClientTests.cs @@ -0,0 +1,251 @@ +using System.Net; +using Moq; +using NUnit.Framework; +using Unity.Services.Cli.Leaderboards.Deploy; +using Unity.Services.Cli.Leaderboards.Service; +using Unity.Services.Cli.Leaderboards.UnitTest.Utils; +using Unity.Services.Gateway.LeaderboardApiV1.Generated.Client; +using Unity.Services.Gateway.LeaderboardApiV1.Generated.Model; +using Unity.Services.Leaderboards.Authoring.Core.Model; +using LeaderboardConfig = Unity.Services.Leaderboards.Authoring.Core.Model.LeaderboardConfig; +using ResetConfig = Unity.Services.Gateway.LeaderboardApiV1.Generated.Model.ResetConfig; +using SortOrder = Unity.Services.Leaderboards.Authoring.Core.Model.SortOrder; +using TieringConfig = Unity.Services.Gateway.LeaderboardApiV1.Generated.Model.TieringConfig; +using UpdateType = Unity.Services.Leaderboards.Authoring.Core.Model.UpdateType; + +namespace Unity.Services.Cli.Leaderboards.UnitTest.Deploy; + +[TestFixture] +public class LeaderboardClientTests +{ + const string k_Name = "path1.lb"; + const string k_Path = "foo/path1.lb"; + const string k_Content = "{ id: \"lb1\", name: \"lb_name\", path: \"foo/path1.lb\" }"; + readonly LeaderboardConfig m_Leaderboard; + + public LeaderboardClientTests() + { + m_Leaderboard = new("lb1", "lb_name", SortOrder.Asc, UpdateType.Aggregate) { Path = k_Path }; + } + + [Test] + public void Initialize_Succeed() + { + Mock service = new(); + var client = new LeaderboardsClient(service.Object); + client.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + Assert.AreEqual(client.EnvironmentId, TestValues.ValidEnvironmentId); + Assert.AreEqual(client.ProjectId, TestValues.ValidProjectId); + Assert.AreEqual(client.CancellationToken, CancellationToken.None); + } + + [Test] + public async Task ListMoreThanLimit() + { + Mock service = new(); + var client = new LeaderboardsClient(service.Object); + client.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + + service.Setup( + s => s.GetLeaderboardsAsync( + TestValues.ValidProjectId, + TestValues.ValidEnvironmentId, + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .Returns(ListFunc); + + var list = await client.List(CancellationToken.None); + service.Verify(s => s.GetLeaderboardsAsync(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Exactly(2)); + Assert.AreEqual(75, list.Count); + } + + [Test] + public async Task ListWhenThereAreNone() + { + Mock service = new(); + var client = new LeaderboardsClient(service.Object); + client.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + + service.Setup( + s => s.GetLeaderboardsAsync( + TestValues.ValidProjectId, + TestValues.ValidEnvironmentId, + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .Returns(Task.FromResult((IEnumerable)Array.Empty())); + + var list = await client.List(CancellationToken.None); + Assert.AreEqual(0, list.Count); + } + + [Test] + public async Task UploadMapsToUpload() + { + Mock service = new(); + var client = new LeaderboardsClient(service.Object); + client.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + await client.Update(m_Leaderboard!, CancellationToken.None); + + service + .Verify( + s => s.UpdateLeaderboardAsync( + TestValues.ValidProjectId, + TestValues.ValidEnvironmentId, + m_Leaderboard.Id, + It.Is(l => l.Name == m_Leaderboard.Name), + It.IsAny()), + Times.Once()); + } + + [Test] + public void UpdateExceptionPropagates() + { + Mock service = new(); + var client = new LeaderboardsClient(service.Object); + client.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + var exceptionMsg = "unknown exception"; + service.Setup(x => x.UpdateLeaderboardAsync(TestValues.ValidProjectId, + TestValues.ValidEnvironmentId, "lb1", It.IsAny(), CancellationToken.None)) + .ThrowsAsync(new Exception(exceptionMsg)); + + Assert.ThrowsAsync( async () => await client.Update(m_Leaderboard!, CancellationToken.None) ); + } + + [Test] + public async Task CreateMapsToCreate() + { + Mock service = new(); + var client = new LeaderboardsClient(service.Object); + client.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + await client.Create(m_Leaderboard!, CancellationToken.None); + + service + .Verify( + s => s.CreateLeaderboardAsync( + TestValues.ValidProjectId, + TestValues.ValidEnvironmentId, + It.Is(l => l.Id == m_Leaderboard.Id && l.Name == m_Leaderboard.Name), + It.IsAny()), + Times.Once()); + } + + [Test] + public async Task DeleteMapsToDelete() + { + Mock serviceMock = new(); + var client = new LeaderboardsClient(serviceMock.Object); + client.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + await client.Delete(m_Leaderboard!, CancellationToken.None); + + serviceMock + .Verify( + s => s.DeleteLeaderboardAsync( + TestValues.ValidProjectId, + TestValues.ValidEnvironmentId, + m_Leaderboard.Id, + It.IsAny()), + Times.Once()); + } + + [Test] + public async Task GetMapsToGet() + { + Mock service = new(); + var client = new LeaderboardsClient(service.Object); + client.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + var leaderboardId = "someid"; + var mockRes = new UpdatedLeaderboardConfig(leaderboardId, "somename"); + service.Setup( + s => s.GetLeaderboardAsync( + It.IsAny(), + It.IsAny(), + leaderboardId, + It.IsAny())) + .Returns(Task.FromResult(new ApiResponse(HttpStatusCode.Accepted, mockRes))); + + client.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + var res = await client.Get(leaderboardId, CancellationToken.None); + + service + .Verify( + s => s.GetLeaderboardAsync( + TestValues.ValidProjectId, + TestValues.ValidEnvironmentId, + leaderboardId, + It.IsAny()), + Times.Once()); + + Assert.AreEqual(mockRes.Id, res.Id); + Assert.AreEqual(mockRes.Name, res.Name); + } + + [Test] + public async Task GetMapsComplexStructure() + { + Mock service = new(); + var client = new LeaderboardsClient(service.Object); + client.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + var leaderboardId = "someid"; + var mockRes = new UpdatedLeaderboardConfig(leaderboardId, "somename") + { + ResetConfig = new ResetConfig(), + TieringConfig = new TieringConfig(TieringConfig.StrategyEnum.Score,new List() + { + new ("gold") + }) + }; + service.Setup( + s => s.GetLeaderboardAsync( + It.IsAny(), + It.IsAny(), + leaderboardId, + It.IsAny())) + .Returns(Task.FromResult(new ApiResponse(HttpStatusCode.Accepted, mockRes))); + + client.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + var res = await client.Get(leaderboardId, CancellationToken.None); + + service + .Verify( + s => s.GetLeaderboardAsync( + TestValues.ValidProjectId, + TestValues.ValidEnvironmentId, + leaderboardId, + It.IsAny()), + Times.Once()); + + Assert.AreEqual(mockRes.Id, res.Id); + Assert.AreEqual(mockRes.Name, res.Name); + Assert.AreEqual((int)mockRes.TieringConfig.Strategy, (int)res.TieringConfig.Strategy); + Assert.AreEqual(mockRes.TieringConfig.Tiers.First().Id, res.TieringConfig.Tiers.First().Id); + Assert.AreEqual(mockRes.ResetConfig.Archive, res.ResetConfig.Archive); + Assert.AreEqual(mockRes.ResetConfig.Schedule, res.ResetConfig.Schedule); + Assert.AreEqual(mockRes.ResetConfig.Start, res.ResetConfig.Start); + } + + static Task> ListFunc( + string projectId, + string envId, + string? cursor, + int? limit, + CancellationToken token) + { + var remoteLbs = Enumerable.Range(0, 75) + .Select(i => new UpdatedLeaderboardConfig($"id{i}", $"name{i}")); + + if (cursor == null) + { + return Task.FromResult(remoteLbs.Take(limit!.Value)); + } + + return Task.FromResult(remoteLbs.SkipWhile(l => l.Id != cursor).Skip(1).Take(limit!.Value)); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardDeploymentHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardDeploymentHandlerTests.cs new file mode 100644 index 0000000..ade6301 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardDeploymentHandlerTests.cs @@ -0,0 +1,267 @@ +using Moq; +using NUnit.Framework; +using Unity.Services.Leaderboards.Authoring.Core.Deploy; +using Unity.Services.Leaderboards.Authoring.Core.Model; +using Unity.Services.Leaderboards.Authoring.Core.Service; + +namespace Unity.Services.Cli.Leaderboards.UnitTest.Deploy; + +[TestFixture] +class LeaderboardDeploymentHandlerTests +{ + [Test] + public async Task DeployAsync_CorrectResult() + { + var localLeaderboards = GetLocalConfigs(); + var remoteLeaderboards = GetRemoteConfigs(); + + Mock mockLeaderboardsClient = new(); + var handler = new LeaderboardsDeploymentHandler(mockLeaderboardsClient.Object); + + mockLeaderboardsClient + .Setup(c => c.List(It.IsAny())) + .ReturnsAsync(remoteLeaderboards.ToList()); + + var actualRes = await handler.DeployAsync( + localLeaderboards + ); + + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "foo"), actualRes.Updated); + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "foo"), actualRes.Deployed); + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "bar"), actualRes.Created); + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "bar"), actualRes.Deployed); + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "dup-id"), actualRes.Created); + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "dup-id"), actualRes.Deployed); + } + + [Test] + public async Task DeployAsync_CreateCallsMade() + { + var localLeaderboards = GetLocalConfigs(); + var remoteLeaderboards = GetRemoteConfigs(); + + Mock mockLeaderboardsClient = new(); + var handler = new LeaderboardsDeploymentHandler(mockLeaderboardsClient.Object); + + mockLeaderboardsClient + .Setup(c => c.List(It.IsAny())) + .ReturnsAsync(remoteLeaderboards.ToList()); + + var actualRes = await handler.DeployAsync( + localLeaderboards + ); + + mockLeaderboardsClient + .Verify( + c => c.Create( + It.Is(l => l.Id == "bar"), + It.IsAny()), + Times.Once); + mockLeaderboardsClient + .Verify( + c => c.Create( + It.Is(l => l.Id == "dup-id"), + It.IsAny()), + Times.Once); + } + + [Test] + public async Task DeployAsync_UpdateCallsMade() + { + var localLeaderboards = GetLocalConfigs(); + var remoteLeaderboards = GetRemoteConfigs(); + + Mock mockLeaderboardsClient = new(); + var handler = new LeaderboardsDeploymentHandler(mockLeaderboardsClient.Object); + + mockLeaderboardsClient + .Setup(c => c.List(It.IsAny())) + .ReturnsAsync(remoteLeaderboards.ToList()); + + var actualRes = await handler.DeployAsync( + localLeaderboards + ); + + mockLeaderboardsClient + .Verify( + c => c.Update( + It.Is(l => l.Id == "foo"), + It.IsAny()), + Times.Once); + } + + [Test] + public async Task DeployAsync_NoReconcileNoDeleteCalls() + { + var localLeaderboards = GetLocalConfigs(); + var remoteLeaderboards = GetRemoteConfigs(); + + Mock mockLeaderboardsClient = new(); + var handler = new LeaderboardsDeploymentHandler(mockLeaderboardsClient.Object); + + mockLeaderboardsClient + .Setup(c => c.List(It.IsAny())) + .ReturnsAsync(remoteLeaderboards.ToList()); + + var actualRes = await handler.DeployAsync( + localLeaderboards + ); + + mockLeaderboardsClient + .Verify( + c => c.Delete( + It.Is(l => l.Id == "echo"), + It.IsAny()), + Times.Never); + } + + [Test] + public async Task DeployAsync_ReconcileDeleteCalls() + { + var localLeaderboards = GetLocalConfigs(); + var remoteLeaderboards = GetRemoteConfigs(); + + Mock mockLeaderboardsClient = new(); + var handler = new LeaderboardsDeploymentHandler(mockLeaderboardsClient.Object); + + mockLeaderboardsClient + .Setup(c => c.List(It.IsAny())) + .ReturnsAsync(remoteLeaderboards.ToList()); + + var actualRes = await handler.DeployAsync( + localLeaderboards, + reconcile: true + ); + + mockLeaderboardsClient + .Verify( + c => c.Delete( + It.Is(l => l.Id == "echo"), + It.IsAny()), + Times.Once); + } + + + [Test] + public async Task DeployAsync_DryRunNoCalls() + { + var localLeaderboards = GetLocalConfigs(); + var remoteLeaderboards = GetRemoteConfigs(); + + Mock mockLeaderboardsClient = new(); + var handler = new LeaderboardsDeploymentHandler(mockLeaderboardsClient.Object); + + mockLeaderboardsClient + .Setup(c => c.List(It.IsAny())) + .ReturnsAsync(remoteLeaderboards.ToList()); + + var actualRes = await handler.DeployAsync( + localLeaderboards, + true + ); + + mockLeaderboardsClient + .Verify( + c => c.Create( + It.IsAny(), + It.IsAny()), + Times.Never); + + mockLeaderboardsClient + .Verify( + c => c.Update( + It.IsAny(), + It.IsAny()), + Times.Never); + + mockLeaderboardsClient + .Verify( + c => c.Delete( + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Test] + public async Task DeployAsync_DryRunCorrectResult() + { + var localLeaderboards = GetLocalConfigs(); + var remoteLeaderboards = GetRemoteConfigs(); + + Mock mockLeaderboardsClient = new(); + var handler = new LeaderboardsDeploymentHandler(mockLeaderboardsClient.Object); + + mockLeaderboardsClient + .Setup(c => c.List(It.IsAny())) + .ReturnsAsync(remoteLeaderboards.ToList()); + + var actualRes = await handler.DeployAsync( + localLeaderboards, + dryRun: true + ); + + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "foo"), actualRes.Updated); + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "bar"), actualRes.Created); + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "dup-id"), actualRes.Created); + Assert.AreEqual(0, actualRes.Deployed.Count); + } + + [Test] + public async Task FetchAsync_DuplicateIdNotDeleted() + { + var localLeaderboards = GetLocalConfigs(); + localLeaderboards.Add(new LeaderboardConfig("dup-id", "other name") { Path = "otherpath.lb"}); + var remoteLeaderboards = GetRemoteConfigs(); + + Mock mockLeaderboardsClient = new(); + var handler = new LeaderboardsDeploymentHandler(mockLeaderboardsClient.Object); + + mockLeaderboardsClient + .Setup(c => c.List(It.IsAny())) + .ReturnsAsync(remoteLeaderboards.ToList()); + + var actualRes = await handler.DeployAsync( + localLeaderboards, + dryRun: true + ); + + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Name == "other name"), actualRes.Failed); + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Name == "dup-id"), actualRes.Failed); + } + + static List GetLocalConfigs() + { + var leaderboards = new List() + { + new LeaderboardConfig("foo", "foo") + { + Path = "path1" + }, + new LeaderboardConfig("bar", "bar") + { + Path = "path2" + }, + new LeaderboardConfig("dup-id", "dup-id") + { + Path = "path3" + } + }; + return leaderboards; + } + + static IReadOnlyList GetRemoteConfigs() + { + var leaderboards = new List() + { + new LeaderboardConfig("foo", "foo") + { + Path = "Remote" + }, + new LeaderboardConfig("echo", "echo") + { + Path = "Remote" + } + }; + return leaderboards; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardDeploymentServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardDeploymentServiceTests.cs new file mode 100644 index 0000000..c14a5f9 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardDeploymentServiceTests.cs @@ -0,0 +1,88 @@ +using NUnit.Framework; +using Moq; +using Unity.Services.Cli.Authoring.Input; +using Unity.Services.Cli.Leaderboards.Deploy; +using Unity.Services.Leaderboards.Authoring.Core.Deploy; +using Unity.Services.Leaderboards.Authoring.Core.Model; +using Unity.Services.Leaderboards.Authoring.Core.Service; + +namespace Unity.Services.Cli.Leaderboards.UnitTest.Deploy; + +[TestFixture] +public class LeaderboardDeploymentServiceTests +{ + LeaderboardDeploymentService? m_DeploymentService; + readonly Mock m_MockLeaderboardClient = new(); + readonly Mock m_MockLeaderboardDeploymentHandler = new(); + readonly Mock m_MockLeaderboardConfigLoader = new(); + + [SetUp] + public void SetUp() + { + m_MockLeaderboardClient.Reset(); + m_DeploymentService = new LeaderboardDeploymentService( + m_MockLeaderboardClient.Object, + m_MockLeaderboardDeploymentHandler.Object, + m_MockLeaderboardConfigLoader.Object); + + var lb1 = new LeaderboardConfig("lb1", "LB1"); + var lb2 = new LeaderboardConfig("lb2", "LB2"); + + var mockLoad = Task.FromResult( + (IReadOnlyList)(new[] + { + lb1 + })); + + m_MockLeaderboardConfigLoader + .Setup( + m => + m.LoadConfigsAsync( + It.IsAny>(), + It.IsAny()) + ) + .Returns(mockLoad); + + var deployResult = new DeployResult() + { + Created = new List { lb2 }, + Updated = new List(), + Deleted = new List(), + Deployed = new List { lb2 }, + Failed = new List() + }; + var fromResult = Task.FromResult(deployResult); + + m_MockLeaderboardDeploymentHandler.Setup( + d => d.DeployAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .Returns(fromResult); + } + + [Test] + public async Task DeployAsync_MapsResult() + { + var input = new DeployInput() + { + Paths = Array.Empty(), + CloudProjectId = string.Empty + }; + var res = await m_DeploymentService!.Deploy( + input, + Array.Empty(), + String.Empty, + string.Empty, + null, + CancellationToken.None); + + Assert.AreEqual(1, res.Created.Count); + Assert.AreEqual(0, res.Updated.Count); + Assert.AreEqual(0, res.Deleted.Count); + Assert.AreEqual(1, res.Deployed.Count); + Assert.AreEqual(0, res.Failed.Count); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardFetchHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardFetchHandlerTests.cs new file mode 100644 index 0000000..8a17620 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardFetchHandlerTests.cs @@ -0,0 +1,243 @@ +using Moq; +using NUnit.Framework; +using Unity.Services.Cli.Leaderboards.Deploy; +using Unity.Services.Leaderboards.Authoring.Core.Deploy; +using Unity.Services.Leaderboards.Authoring.Core.Fetch; +using Unity.Services.Leaderboards.Authoring.Core.IO; +using Unity.Services.Leaderboards.Authoring.Core.Model; +using Unity.Services.Leaderboards.Authoring.Core.Service; + +namespace Unity.Services.Cli.Leaderboards.UnitTest.Deploy; + +[TestFixture] +class LeaderboardFetchHandlerTests +{ + [Test] + public async Task FetchAsync_CorrectResult() + { + var localLeaderboards = GetLocalConfigs(); + var remoteLeaderboards = GetRemoteConfigs(); + + Mock mockLeaderboardsClient = new(); + Mock mockFileSystem = new(); + var handler = new LeaderboardsFetchHandler(mockLeaderboardsClient.Object, mockFileSystem.Object, new LeaderboardsSerializer()); + + mockLeaderboardsClient + .Setup(c => c.List(It.IsAny())) + .ReturnsAsync(remoteLeaderboards.ToList()); + + var actualRes = await handler.FetchAsync( + "dir", + localLeaderboards + ); + + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "foo"), actualRes.Updated); + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "foo"), actualRes.Fetched); + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "bar"), actualRes.Deleted); + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "bar"), actualRes.Fetched); + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "dup-id"), actualRes.Deleted); + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "dup-id"), actualRes.Fetched); + Assert.IsEmpty(actualRes.Created); + } + + [Test] + public async Task FetchAsync_WriteCallsMade() + { + var localLeaderboards = GetLocalConfigs(); + var remoteLeaderboards = GetRemoteConfigs(); + + Mock mockLeaderboardsClient = new(); + Mock mockFileSystem = new(); + var handler = new LeaderboardsFetchHandler(mockLeaderboardsClient.Object, mockFileSystem.Object, new LeaderboardsSerializer()); + + mockLeaderboardsClient + .Setup(c => c.List(It.IsAny())) + .ReturnsAsync(remoteLeaderboards.ToList()); + + var actualRes = await handler.FetchAsync( + "dir", + localLeaderboards + ); + + mockFileSystem + .Verify(f => f.WriteAllText( + "path1", + It.IsAny(), + It.IsAny()), + Times.Once); + + mockFileSystem + .Verify(f => f.WriteAllText( + "echo", + It.IsAny(), + It.IsAny()), + Times.Never); //Should not happen unless reconcile + } + + [Test] + public async Task FetchAsync_DeleteCallsMade() + { + var localLeaderboards = GetLocalConfigs(); + var remoteLeaderboards = GetRemoteConfigs(); + + Mock mockLeaderboardsClient = new(); + Mock mockFileSystem = new(); + var handler = new LeaderboardsFetchHandler(mockLeaderboardsClient.Object, mockFileSystem.Object, new LeaderboardsSerializer()); + + mockLeaderboardsClient + .Setup(c => c.List(It.IsAny())) + .ReturnsAsync(remoteLeaderboards.ToList()); + + var actualRes = await handler.FetchAsync( + "dir", + localLeaderboards + ); + + mockFileSystem + .Verify(f => f.Delete( + "path2", + It.IsAny()), + Times.Once); + mockFileSystem + .Verify(f => f.Delete( + "path3", + It.IsAny()), + Times.Once); + } + + [Test] + public async Task FetchAsync_WriteNewOnReconcile() + { + var localLeaderboards = GetLocalConfigs(); + var remoteLeaderboards = GetRemoteConfigs(); + + Mock mockLeaderboardsClient = new(); + Mock mockFileSystem = new(); + var handler = new LeaderboardsFetchHandler(mockLeaderboardsClient.Object, mockFileSystem.Object, new LeaderboardsSerializer()); + + mockLeaderboardsClient + .Setup(c => c.List(It.IsAny())) + .ReturnsAsync(remoteLeaderboards.ToList()); + + var actualRes = await handler.FetchAsync( + "dir", + localLeaderboards, + reconcile: true + ); + + mockFileSystem + .Verify(f => f.WriteAllText( + "path1", + It.IsAny(), + It.IsAny()), + Times.Once); + + mockFileSystem + .Verify(f => f.WriteAllText( + Path.Combine("dir","echo.lb"), + It.IsAny(), + It.IsAny()), + Times.Once); //Should happen on reconcile + } + + [Test] + public async Task FetchAsync_DryRunNoCalls() + { + var localLeaderboards = GetLocalConfigs(); + var remoteLeaderboards = GetRemoteConfigs(); + + Mock mockLeaderboardsClient = new(); + Mock mockFileSystem = new(); + var handler = new LeaderboardsFetchHandler(mockLeaderboardsClient.Object, mockFileSystem.Object, new LeaderboardsSerializer()); + + mockLeaderboardsClient + .Setup(c => c.List(It.IsAny())) + .ReturnsAsync(remoteLeaderboards.ToList()); + + var actualRes = await handler.FetchAsync( + "dir", + localLeaderboards, + dryRun: true + ); + + mockFileSystem + .Verify(f => f.Delete( + It.IsAny(), + It.IsAny()), + Times.Never); + + mockFileSystem + .Verify(f => f.WriteAllText( + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Test] + public async Task FetchAsync_DuplicateIdNotDeleted() + { + var localLeaderboards = GetLocalConfigs(); + localLeaderboards.Add(new LeaderboardConfig("dup-id", "other name") { Path = "otherpath.lb"}); + var remoteLeaderboards = GetRemoteConfigs(); + + Mock mockLeaderboardsClient = new(); + Mock mockFileSystem = new(); + var handler = new LeaderboardsFetchHandler(mockLeaderboardsClient.Object, mockFileSystem.Object, new LeaderboardsSerializer()); + + mockLeaderboardsClient + .Setup(c => c.List(It.IsAny())) + .ReturnsAsync(remoteLeaderboards.ToList()); + + var actualRes = await handler.FetchAsync( + "dir", + localLeaderboards, + dryRun: true + ); + + mockFileSystem + .Verify(f => f.Delete( + It.Is(s => s == "path3"), + It.IsAny()), + Times.Never); + + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Name == "other name"), actualRes.Failed); + Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Name == "dup-id"), actualRes.Failed); + } + + static List GetLocalConfigs() + { + var leaderboards = new List() + { + new LeaderboardConfig("foo", "foo") + { + Path = "path1" + }, + new LeaderboardConfig("bar", "bar") + { + Path = "path2" + }, + new LeaderboardConfig("dup-id", "dup-id") + { + Path = "path3" + } + }; + return leaderboards; + } + + static List GetRemoteConfigs() + { + var leaderboards = new List() + { + new LeaderboardConfig("foo", "foo") + { + Path = "Remote" + }, + new LeaderboardConfig("echo", "echo") + { + Path = "Remote" + } + }; + return leaderboards; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardFetchServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardFetchServiceTests.cs new file mode 100644 index 0000000..1f03d69 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardFetchServiceTests.cs @@ -0,0 +1,94 @@ +using NUnit.Framework; +using Moq; +using Unity.Services.Cli.Authoring.Input; +using Unity.Services.Cli.Authoring.Service; +using Unity.Services.Cli.Common.Utils; +using Unity.Services.Cli.Leaderboards.Deploy; +using Unity.Services.Leaderboards.Authoring.Core.Deploy; +using Unity.Services.Leaderboards.Authoring.Core.Fetch; +using Unity.Services.Leaderboards.Authoring.Core.Model; +using Unity.Services.Leaderboards.Authoring.Core.Service; + +namespace Unity.Services.Cli.Leaderboards.UnitTest.Deploy; + +[TestFixture] +public class LeaderboardFetchServiceTests +{ + LeaderboardFetchService? m_FetchService; + readonly Mock m_MockLeaderboardClient = new(); + readonly Mock m_MockLeaderboardFetchHandler = new(); + readonly Mock m_MockLeaderboardConfigLoader = new(); + readonly Mock m_MockDeployFileService = new(); + readonly Mock m_UnityEnvironment = new(); + + [SetUp] + public void SetUp() + { + m_MockLeaderboardClient.Reset(); + m_FetchService = new LeaderboardFetchService( + m_MockLeaderboardClient.Object, + m_MockLeaderboardFetchHandler.Object, + m_MockLeaderboardConfigLoader.Object, + m_MockDeployFileService.Object, + m_UnityEnvironment.Object); + + var lb1 = new LeaderboardConfig("lb1", "LB1"); + var lb2 = new LeaderboardConfig("lb2", "LB2"); + + var mockLoad = Task.FromResult( + (IReadOnlyList)(new[] + { + lb1 + })); + + m_MockLeaderboardConfigLoader + .Setup( + m => + m.LoadConfigsAsync( + It.IsAny>(), + It.IsAny()) + ) + .Returns(mockLoad); + + var deployResult = new FetchResult() + { + Created = new List { lb2 }, + Updated = new List(), + Deleted = new List(), + Fetched = new List { lb2 }, + Failed = new List() + }; + var fromResult = Task.FromResult(deployResult); + + m_MockLeaderboardFetchHandler.Setup( + d => d.FetchAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .Returns(fromResult); + } + + [Test] + public async Task FetchAsync_MapsResult() + { + var input = new FetchInput() + { + Path = "dir", + CloudProjectId = string.Empty + }; + var res = await m_FetchService!.FetchAsync( + input, + new[] { "dir" }, + null, + CancellationToken.None); + + Assert.AreEqual(1, res.Created.Count); + Assert.AreEqual(0, res.Updated.Count); + Assert.AreEqual(0, res.Deleted.Count); + Assert.AreEqual(1, res.Fetched.Count); + Assert.AreEqual(0, res.Failed.Count); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardsConfigLoaderTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardsConfigLoaderTests.cs new file mode 100644 index 0000000..ec21fc1 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardsConfigLoaderTests.cs @@ -0,0 +1,69 @@ +using NUnit.Framework; +using Moq; +using Unity.Services.Cli.Authoring.Input; +using Unity.Services.Cli.Authoring.Service; +using Unity.Services.Cli.Common.Utils; +using Unity.Services.Cli.Leaderboards.Deploy; +using Unity.Services.Leaderboards.Authoring.Core.Deploy; +using Unity.Services.Leaderboards.Authoring.Core.Fetch; +using Unity.Services.Leaderboards.Authoring.Core.IO; +using Unity.Services.Leaderboards.Authoring.Core.Model; +using Unity.Services.Leaderboards.Authoring.Core.Service; + +namespace Unity.Services.Cli.Leaderboards.UnitTest.Deploy; + +[TestFixture] +public class LeaderboardsConfigLoaderTests +{ + LeaderboardsConfigLoader? m_LeaderboardsConfigLoader; + readonly Mock m_FileSystem = new(); + + [Test] + public async Task ConfigLoader_Deserializes() + { + m_LeaderboardsConfigLoader = new LeaderboardsConfigLoader( + m_FileSystem.Object); + var content = @"{ + 'SortOrder': 'asc', + 'UpdateType': 'keepBest', + 'Name': 'My Complex LB', + 'BucketSize': 20.0, + 'ResetConfig': { + 'Start': '2023-07-01T16:00:00Z', + 'Schedule': '0 12 1 * *' + }, + 'TieringConfig': { + 'Strategy': 'score', + 'Tiers': [ + { + 'Id': 'Gold', + 'Cutoff': 200.0 + }, + { + 'Id': 'Silver', + 'Cutoff': 150.0 + }, + { + 'Id': 'Bronze' + } + ] + } +}"; + m_FileSystem.Setup(f => f.ReadAllText(It.IsAny(), It.IsAny())) + .ReturnsAsync(content); + + var configs = await m_LeaderboardsConfigLoader + .LoadConfigsAsync(new[] { "path" }, CancellationToken.None); + + var config = configs.First(); + + Assert.AreEqual("path", config.Id); + Assert.AreEqual(SortOrder.Asc, config.SortOrder); + Assert.AreEqual(UpdateType.KeepBest, config.UpdateType); + Assert.AreEqual(20.0, config.BucketSize); + Assert.AreEqual(3, config.TieringConfig.Tiers.Count); + Assert.AreEqual("Gold", config.TieringConfig.Tiers[0].Id); + Assert.AreEqual("Silver", config.TieringConfig.Tiers[1].Id); + Assert.AreEqual("Bronze", config.TieringConfig.Tiers[2].Id); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/CreateLeaderboardHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/CreateLeaderboardHandlerTests.cs deleted file mode 100644 index 10f5a09..0000000 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/CreateLeaderboardHandlerTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Net; -using System.Text; -using Microsoft.Extensions.Logging; -using Moq; -using NUnit.Framework; -using Spectre.Console; -using Unity.Services.Cli.Common.Console; -using Unity.Services.Cli.Common.Exceptions; -using Unity.Services.Cli.Common.Utils; -using Unity.Services.Cli.Leaderboards.Handlers; -using Unity.Services.Cli.Leaderboards.Input; -using Unity.Services.Cli.Leaderboards.Service; -using Unity.Services.Cli.Leaderboards.UnitTest.Utils; -using Unity.Services.Cli.TestUtils; -using Unity.Services.Gateway.LeaderboardApiV1.Generated.Client; - -namespace Unity.Services.Cli.Leaderboards.UnitTest.Handlers; - -[TestFixture] -class CreateLeaderboardHandlerTests -{ - readonly Mock m_MockUnityEnvironment = new(); - readonly Mock m_MockLeaderboard = new(); - readonly Mock m_MockLogger = new(); - string leaderboardPath = null!; - - [SetUp] - public void SetUp() - { - m_MockUnityEnvironment.Reset(); - m_MockLeaderboard.Reset(); - m_MockLogger.Reset(); - leaderboardPath = Directory.GetCurrentDirectory() + "/leaderboardBody.txt"; - } - - [TearDown] - public void TearDown() - { - File.Delete(leaderboardPath); - } - - - [Test] - public async Task LoadCreateAsync_CallsLoadingIndicatorStartLoading() - { - var mockLoadingIndicator = new Mock(); - - await CreateLeaderboardHandler.CreateLeaderboardAsync( - null!, null!, null!, null!, mockLoadingIndicator.Object, CancellationToken.None); - - mockLoadingIndicator.Verify( - ex => ex.StartLoadingAsync(It.IsAny(), It.IsAny>()), Times.Once); - } - - [Test] - public async Task CreateHandler_CallsCreateServiceSucceeded() - { - await File.WriteAllTextAsync(leaderboardPath, "{}", new UTF8Encoding(true)); - - CreateInput input = new CreateInput() - { - CloudProjectId = TestValues.ValidProjectId, - JsonFilePath = leaderboardPath - }; - - m_MockLeaderboard.Setup(x => x.CreateLeaderboardAsync( - TestValues.ValidProjectId, - TestValues.ValidEnvironmentId, - "{}", - CancellationToken.None)).ReturnsAsync(new ApiResponse(HttpStatusCode.Created, new object())); - - m_MockUnityEnvironment.Setup(x => x.FetchIdentifierAsync(CancellationToken.None)) - .ReturnsAsync(TestValues.ValidEnvironmentId); - - await CreateLeaderboardHandler.CreateAsync( - input, - m_MockUnityEnvironment.Object, - m_MockLeaderboard.Object, - m_MockLogger.Object, - CancellationToken.None); - - m_MockUnityEnvironment.Verify(x => x.FetchIdentifierAsync(CancellationToken.None), Times.Once); - m_MockLeaderboard.Verify( - e => e.CreateLeaderboardAsync( - TestValues.ValidProjectId, - TestValues.ValidEnvironmentId, - "{}", - CancellationToken.None), - Times.Once); - TestsHelper.VerifyLoggerWasCalled(m_MockLogger, LogLevel.Information, expectedTimes: Times.Once); - } -} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/DeleteLeaderboardHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/DeleteLeaderboardHandlerTests.cs index e2a1780..090ad4b 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/DeleteLeaderboardHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/DeleteLeaderboardHandlerTests.cs @@ -22,7 +22,7 @@ class DeleteLeaderboardHandlerTests readonly Mock m_MockUnityEnvironment = new(); readonly Mock m_MockLeaderboard = new(); readonly Mock m_MockLogger = new(); - const string leaderboardId = "lb1"; + const string k_LeaderboardId = "lb1"; [SetUp] public void SetUp() @@ -50,13 +50,13 @@ public async Task DeleteHandler_CallsDeleteServiceSucceeded() LeaderboardIdInput input = new LeaderboardIdInput() { CloudProjectId = TestValues.ValidProjectId, - LeaderboardId = leaderboardId + LeaderboardId = k_LeaderboardId }; m_MockLeaderboard.Setup(x => x.DeleteLeaderboardAsync( TestValues.ValidProjectId, TestValues.ValidEnvironmentId, - leaderboardId, + k_LeaderboardId, CancellationToken.None)).ReturnsAsync(new ApiResponse(HttpStatusCode.NoContent, new object())); m_MockUnityEnvironment.Setup(x => x.FetchIdentifierAsync(CancellationToken.None)) @@ -74,7 +74,7 @@ await DeleteLeaderboardHandler.DeleteAsync( e => e.DeleteLeaderboardAsync( TestValues.ValidProjectId, TestValues.ValidEnvironmentId, - leaderboardId, + k_LeaderboardId, CancellationToken.None), Times.Once); diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/GetLeaderboardHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/GetLeaderboardHandlerTests.cs index 1f9ce05..d2e9425 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/GetLeaderboardHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/GetLeaderboardHandlerTests.cs @@ -13,7 +13,7 @@ using Unity.Services.Gateway.LeaderboardApiV1.Generated.Client; using Unity.Services.Gateway.LeaderboardApiV1.Generated.Model; -namespace Unity.Services.Cli.Leaderboards.UnitTest.Handlers; +namespace Unity.Services.Cli.Leaderboard.UnitTest.Handlers; [TestFixture] class GetLeaderboardHandlerTests diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/ImportHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/ImportHandlerTests.cs index d7e2cfc..89b394f 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/ImportHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/ImportHandlerTests.cs @@ -79,7 +79,7 @@ await ImportHandler.ImportAsync( CancellationToken.None ); - m_MockLoadingIndicator.Verify(li => li.StartLoadingAsync(ImportHandler.k_LoadingIndicatorMessage, + m_MockLoadingIndicator.Verify(li => li.StartLoadingAsync(ImportHandler.LoadingIndicatorMessage, It.IsAny>())); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/ExportHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/LeaderboardExportHandlerTests.cs similarity index 71% rename from Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/ExportHandlerTests.cs rename to Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/LeaderboardExportHandlerTests.cs index a57485b..cf625c5 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/ExportHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/LeaderboardExportHandlerTests.cs @@ -17,7 +17,7 @@ namespace Unity.Services.Cli.Leaderboards.UnitTest.Handlers; [TestFixture] -class ExportHandlerTests +class LeaderboardExportHandlerTests { readonly Mock m_MockUnityEnvironment = new(); readonly Mock m_MockLeaderboardsService = new(); @@ -55,7 +55,6 @@ public async Task ExportAsync_CallsLoadingIndicator() await ExportHandler.ExportAsync ( - null!, exportInput, m_MockLogger.Object, m_LeaderboardExporter, @@ -63,7 +62,7 @@ await ExportHandler.ExportAsync CancellationToken.None ); - m_MockLoadingIndicator.Verify(li => li.StartLoadingAsync(ExportHandler.k_LoadingIndicatorMessage, + m_MockLoadingIndicator.Verify(li => li.StartLoadingAsync(ExportHandler.LoadingIndicatorMessage, It.IsAny>())); } @@ -74,12 +73,7 @@ public async Task ExportAsync_ExportsAndZips() { OutputDirectory = "mock_output_directory" }; - var input = new ListLeaderboardInput() - { - CloudProjectId = TestValues.ValidProjectId, - Cursor = TestValues.Cursor, - Limit = TestValues.Limit - }; + m_MockUnityEnvironment.Setup(x => x.FetchIdentifierAsync(CancellationToken.None)) .ReturnsAsync(TestValues.ValidEnvironmentId); @@ -107,8 +101,7 @@ public async Task ExportAsync_ExportsAndZips() .Exists(It.IsAny())) .Returns((path) => false); - m_LeaderboardExporter!.ListLeaderboardInput = input; - await m_LeaderboardExporter.ExportAsync(exportInput, CancellationToken.None); + await m_LeaderboardExporter!.ExportAsync(exportInput, CancellationToken.None); var archivePath = Path.Join(exportInput.OutputDirectory, LeaderboardConstants.ZipName); @@ -136,12 +129,7 @@ public async Task ExportAsync_ExportsAndZipsWithFileName() OutputDirectory = "mock_output_directory", FileName = fileName }; - var input = new ListLeaderboardInput() - { - CloudProjectId = TestValues.ValidProjectId, - Cursor = TestValues.Cursor, - Limit = TestValues.Limit - }; + m_MockUnityEnvironment.Setup(x => x.FetchIdentifierAsync(CancellationToken.None)) .ReturnsAsync(TestValues.ValidEnvironmentId); @@ -171,8 +159,7 @@ public async Task ExportAsync_ExportsAndZipsWithFileName() .Exists(It.IsAny())) .Returns((path) => false); - m_LeaderboardExporter!.ListLeaderboardInput = input; - await m_LeaderboardExporter.ExportAsync(exportInput, CancellationToken.None); + await m_LeaderboardExporter!.ExportAsync(exportInput, CancellationToken.None); m_MockLeaderboardsService.Verify( ls => ls.GetLeaderboardsAsync(It.IsAny(), @@ -199,21 +186,83 @@ public void ExportAsync_FailsWithInvalidExtension() FileName = fileName }; - var input = new ListLeaderboardInput() - { - CloudProjectId = TestValues.ValidProjectId, - Cursor = TestValues.Cursor, - Limit = TestValues.Limit - }; m_MockUnityEnvironment.Setup(x => x.FetchIdentifierAsync(CancellationToken.None)) .ReturnsAsync(TestValues.ValidEnvironmentId); - m_LeaderboardExporter!.ListLeaderboardInput = input; var exception = Assert.ThrowsAsync(async () => { - await m_LeaderboardExporter.ExportAsync(exportInput, CancellationToken.None); + await m_LeaderboardExporter!.ExportAsync(exportInput, CancellationToken.None); }); Assert.That(exception!.Message, Is.EqualTo("The file-name argument must have the extension '.lbzip'.")); } + + [Test] + public async Task ExportAsync_ExportsMoreThan50() + { + var fileName = "other.lbzip"; + + var exportInput = new ExportInput() + { + OutputDirectory = "mock_output_directory", + FileName = fileName + }; + + m_MockUnityEnvironment.Setup(x => x.FetchIdentifierAsync(CancellationToken.None)) + .ReturnsAsync(TestValues.ValidEnvironmentId); + + m_FileSystemMock.Setup( + e => e + .Directory + .CreateDirectory(It.IsAny())); + m_FileSystemMock.Setup( + e => e + .Path + .Join(It.IsAny(), It.IsAny())) + .Returns(Path.Join(exportInput.OutputDirectory, LeaderboardConstants.ZipName)); + + m_FileSystemMock.Setup( + e => e + .File + .Exists(It.IsAny())) + .Returns(_ => false); + + m_MockLeaderboardsService.Setup( + ls => ls.GetLeaderboardsAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(ListFunc); + + await m_LeaderboardExporter!.ExportAsync(exportInput, CancellationToken.None); + + m_MockLeaderboardsService + .Verify(s => s.GetLeaderboardsAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Exactly(2)); + } + + static Task> ListFunc( + string projectId, + string envId, + string? cursor, + int? limit, + CancellationToken token) + { + var remoteLbs = Enumerable.Range(0, 75) + .Select(i => new UpdatedLeaderboardConfig($"id{i}", $"name{i}")); + + if (cursor == null) + { + return Task.FromResult(remoteLbs.Take(limit!.Value)); + } + + return Task.FromResult(remoteLbs.SkipWhile(l => l.Id != cursor).Skip(1).Take(limit!.Value)); + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/ResetLeaderboardHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/ResetLeaderboardHandlerTests.cs index 801235d..891fd43 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/ResetLeaderboardHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/ResetLeaderboardHandlerTests.cs @@ -1,11 +1,9 @@ using System.Net; -using System.Text; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; using Spectre.Console; using Unity.Services.Cli.Common.Console; -using Unity.Services.Cli.Common.Exceptions; using Unity.Services.Cli.Common.Utils; using Unity.Services.Cli.Leaderboards.Handlers; using Unity.Services.Cli.Leaderboards.Input; @@ -15,7 +13,7 @@ using Unity.Services.Gateway.LeaderboardApiV1.Generated.Client; using Unity.Services.Gateway.LeaderboardApiV1.Generated.Model; -namespace Unity.Services.Cli.Leaderboards.UnitTest.Handlers; +namespace Unity.Services.Cli.Leaderboard.UnitTest.Handlers; [TestFixture] class ResetLeaderboardHandlerTests @@ -23,7 +21,7 @@ class ResetLeaderboardHandlerTests readonly Mock m_MockUnityEnvironment = new(); readonly Mock m_MockLeaderboard = new(); readonly Mock m_MockLogger = new(); - const string leaderboardId = "lb1"; + const string k_LeaderboardId = "lb1"; [SetUp] public void SetUp() @@ -51,13 +49,13 @@ public async Task ResetHandler_CallsResetServiceAndLogger() ResetInput input = new ResetInput() { CloudProjectId = TestValues.ValidProjectId, - LeaderboardId = leaderboardId + LeaderboardId = k_LeaderboardId }; m_MockLeaderboard.Setup(x => x.ResetLeaderboardAsync( TestValues.ValidProjectId, TestValues.ValidEnvironmentId, - leaderboardId, + k_LeaderboardId, null, CancellationToken.None)).ReturnsAsync(new ApiResponse(HttpStatusCode.OK, new LeaderboardVersionId(), "{ \"versionId\": 10 }")); @@ -76,7 +74,7 @@ await ResetLeaderboardHandler.ResetAsync( e => e.ResetLeaderboardAsync( TestValues.ValidProjectId, TestValues.ValidEnvironmentId, - leaderboardId, + k_LeaderboardId, null, CancellationToken.None), Times.Once); diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/UpdateLeaderboardHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/UpdateLeaderboardHandlerTests.cs deleted file mode 100644 index d6b3b69..0000000 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Handlers/UpdateLeaderboardHandlerTests.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Net; -using System.Text; -using Microsoft.Extensions.Logging; -using Moq; -using NUnit.Framework; -using Spectre.Console; -using Unity.Services.Cli.Common.Console; -using Unity.Services.Cli.Common.Exceptions; -using Unity.Services.Cli.Common.Utils; -using Unity.Services.Cli.Leaderboards.Handlers; -using Unity.Services.Cli.Leaderboards.Input; -using Unity.Services.Cli.Leaderboards.Service; -using Unity.Services.Cli.Leaderboards.UnitTest.Utils; -using Unity.Services.Cli.TestUtils; -using Unity.Services.Gateway.LeaderboardApiV1.Generated.Client; - -namespace Unity.Services.Cli.Leaderboards.UnitTest.Handlers; - -[TestFixture] -class UpdateLeaderboardHandlerTests -{ - readonly Mock m_MockUnityEnvironment = new(); - readonly Mock m_MockLeaderboard = new(); - readonly Mock m_MockLogger = new(); - const string leaderboardId = "lb1"; - string leaderboardPath = null!; - - [SetUp] - public void SetUp() - { - m_MockUnityEnvironment.Reset(); - m_MockLeaderboard.Reset(); - m_MockLogger.Reset(); - leaderboardPath = Directory.GetCurrentDirectory() + "/leaderboardBodyUpdate.txt"; - } - - [TearDown] - public void TearDown() - { - File.Delete(leaderboardPath); - } - - [Test] - public async Task LoadUpdateAsync_CallsLoadingIndicatorStartLoading() - { - var mockLoadingIndicator = new Mock(); - - await UpdateLeaderboardHandler.UpdateLeaderboardAsync( - null!, null!, null!, null!, mockLoadingIndicator.Object, CancellationToken.None); - - mockLoadingIndicator.Verify( - ex => ex.StartLoadingAsync(It.IsAny(), It.IsAny>()), Times.Once); - } - - [Test] - public async Task UpdateHandler_CallsUpdateServiceSucceeded() - { - await File.WriteAllTextAsync(leaderboardPath, "{}", new UTF8Encoding(true)); - UpdateInput input = new UpdateInput() - { - CloudProjectId = TestValues.ValidProjectId, - LeaderboardId = leaderboardId, - JsonFilePath = leaderboardPath - }; - - m_MockLeaderboard.Setup(x => x.UpdateLeaderboardAsync( - TestValues.ValidProjectId, - TestValues.ValidEnvironmentId, - leaderboardId, - "{}", - CancellationToken.None)).ReturnsAsync(new ApiResponse(HttpStatusCode.NoContent, new object())); - - m_MockUnityEnvironment.Setup(x => x.FetchIdentifierAsync(CancellationToken.None)) - .ReturnsAsync(TestValues.ValidEnvironmentId); - - await UpdateLeaderboardHandler.UpdateAsync( - input, - m_MockUnityEnvironment.Object, - m_MockLeaderboard.Object, - m_MockLogger.Object, - CancellationToken.None); - - m_MockUnityEnvironment.Verify(x => x.FetchIdentifierAsync(CancellationToken.None), Times.Once); - m_MockLeaderboard.Verify( - e => e.UpdateLeaderboardAsync( - TestValues.ValidProjectId, - TestValues.ValidEnvironmentId, - leaderboardId, - "{}", - CancellationToken.None), - Times.Once); - - TestsHelper.VerifyLoggerWasCalled(m_MockLogger, LogLevel.Information, expectedTimes: Times.Once); - } -} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Service/LeaderboardsServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Service/LeaderboardsServiceTests.cs index 3841965..99f089f 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Service/LeaderboardsServiceTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Service/LeaderboardsServiceTests.cs @@ -20,8 +20,8 @@ class LeaderboardsServiceTests const string k_TestAccessToken = "test-token"; const string k_InvalidProjectId = "invalidProject"; const string k_InvalidEnvironmentId = "foo"; - const string leaderboardId = "leaderboard_id"; - const bool archive = true; + const string k_LeaderboardId = "leaderboard_id"; + const bool k_Archive = true; readonly Mock m_ValidatorObject = new(); readonly Mock m_AuthenticationServiceObject = new(); @@ -39,7 +39,7 @@ public void SetUp() m_AuthenticationServiceObject.Setup(a => a.GetAccessTokenAsync(CancellationToken.None)) .Returns(Task.FromResult(k_TestAccessToken)); - m_ExpectedLeaderboard = new(id: leaderboardId, + m_ExpectedLeaderboard = new(id: k_LeaderboardId, name: "leaderboard_name"); m_ExpectedLeaderboards = new List @@ -138,7 +138,7 @@ public void CreateAsync_Succeeded() "{\"id\": \"leaderboard_id\", \"name\": \"lb_name_1\", \"sortOrder\": \"asc\", \"updateType\": \"aggregate\", \"bucketSize\": 10}", CancellationToken.None)); - var config = new LeaderboardIdConfig(id: leaderboardId, name: "lb_name_1", sortOrder: SortOrder.Asc, + var config = new LeaderboardIdConfig(id: k_LeaderboardId, name: "lb_name_1", sortOrder: SortOrder.Asc, updateType: UpdateType.Aggregate, bucketSize: 10); m_LeaderboardApiV1AsyncMock.DefaultApiAsyncObject.Verify( @@ -176,7 +176,7 @@ public void UpdateAsync_Succeeded() Assert.DoesNotThrowAsync( () => m_LeaderboardsService!.UpdateLeaderboardAsync( - TestValues.ValidProjectId, TestValues.ValidEnvironmentId, leaderboardId, + TestValues.ValidProjectId, TestValues.ValidEnvironmentId, k_LeaderboardId, "{\"id\": \"lb1\", \"name\": \"lb_name_1\", \"sortOrder\": \"asc\", \"updateType\": \"aggregate\", \"bucketSize\": 10}", CancellationToken.None)); @@ -187,7 +187,7 @@ public void UpdateAsync_Succeeded() ex => ex.UpdateLeaderboardConfigWithHttpInfoAsync( Guid.Parse(TestValues.ValidProjectId), Guid.Parse(TestValues.ValidEnvironmentId), - leaderboardId, + k_LeaderboardId, config, 0, CancellationToken.None), @@ -202,7 +202,7 @@ public async Task GetAsync_LeaderboardSucceeded() .Returns(true); var actualLeaderboard = await m_LeaderboardsService!.GetLeaderboardAsync( - TestValues.ValidProjectId, TestValues.ValidEnvironmentId, leaderboardId, CancellationToken.None); + TestValues.ValidProjectId, TestValues.ValidEnvironmentId, k_LeaderboardId, CancellationToken.None); Assert.AreEqual(m_ExpectedLeaderboard, actualLeaderboard.Data); @@ -210,7 +210,7 @@ public async Task GetAsync_LeaderboardSucceeded() a => a.GetLeaderboardConfigWithHttpInfoAsync( Guid.Parse(TestValues.ValidProjectId), Guid.Parse(TestValues.ValidEnvironmentId), - leaderboardId, + k_LeaderboardId, 0, CancellationToken.None), Times.Once); @@ -224,13 +224,13 @@ public async Task DeleteAsync_LeaderboardSucceeded() .Returns(true); await m_LeaderboardsService!.DeleteLeaderboardAsync( - TestValues.ValidProjectId, TestValues.ValidEnvironmentId, leaderboardId, CancellationToken.None); + TestValues.ValidProjectId, TestValues.ValidEnvironmentId, k_LeaderboardId, CancellationToken.None); m_LeaderboardApiV1AsyncMock.DefaultApiAsyncObject.Verify( a => a.DeleteLeaderboardWithHttpInfoAsync( Guid.Parse(TestValues.ValidProjectId), Guid.Parse(TestValues.ValidEnvironmentId), - leaderboardId, + k_LeaderboardId, 0, CancellationToken.None), Times.Once); @@ -244,14 +244,14 @@ public async Task ResetAsync_LeaderboardSucceeded() .Returns(true); await m_LeaderboardsService!.ResetLeaderboardAsync( - TestValues.ValidProjectId, TestValues.ValidEnvironmentId, leaderboardId, archive, CancellationToken.None); + TestValues.ValidProjectId, TestValues.ValidEnvironmentId, k_LeaderboardId, k_Archive, CancellationToken.None); m_LeaderboardApiV1AsyncMock.DefaultApiAsyncObject.Verify( a => a.ResetLeaderboardScoresWithHttpInfoAsync( Guid.Parse(TestValues.ValidProjectId), Guid.Parse(TestValues.ValidEnvironmentId), - leaderboardId, - archive, + k_LeaderboardId, + k_Archive, 0, CancellationToken.None), Times.Once); diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Unity.Services.Cli.Leaderboards.UnitTest.csproj b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Unity.Services.Cli.Leaderboards.UnitTest.csproj index 950666e..f245f57 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Unity.Services.Cli.Leaderboards.UnitTest.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Unity.Services.Cli.Leaderboards.UnitTest.csproj @@ -4,7 +4,6 @@ net6.0 enable enable - Unity.Services.Cli.Leaderboard.UnitTest true @@ -22,4 +21,9 @@ + + + ..\Unity.Services.Cli\bin\Debug\net6.0\Unity.Services.Cli.Leaderboards.dll + + \ No newline at end of file diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/ILeaderboardsConfigLoader.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/ILeaderboardsConfigLoader.cs new file mode 100644 index 0000000..cc760c8 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/ILeaderboardsConfigLoader.cs @@ -0,0 +1,10 @@ +using Unity.Services.Leaderboards.Authoring.Core.Model; + +namespace Unity.Services.Cli.Leaderboards.Deploy; + +public interface ILeaderboardsConfigLoader +{ + Task> LoadConfigsAsync( + IReadOnlyCollection paths, + CancellationToken cancellationToken); +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardConfigFile.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardConfigFile.cs new file mode 100644 index 0000000..d14913f --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardConfigFile.cs @@ -0,0 +1,76 @@ +using System.ComponentModel; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Unity.Services.Cli.Authoring.Templates; +using Unity.Services.Leaderboards.Authoring.Core.Model; + +namespace Unity.Services.Cli.Leaderboards.Deploy; + +[Serializable] +public class LeaderboardConfigFile : IFileTemplate +{ + [JsonConstructor] + public LeaderboardConfigFile(string name) : this(null, name, SortOrder.Asc, UpdateType.KeepBest) + { + + } + + public LeaderboardConfigFile() : this(null, "My Leaderboard", SortOrder.Asc, UpdateType.KeepBest) + { + ResetConfig = new() + { + Start = DateTime.Today.AddDays(10).Date, + Schedule = "0 12 1 * *" + }; + TieringConfig = new TieringConfig() + { + Strategy = Strategy.Score, + Tiers = new List() + { + new (){ Cutoff = 200.0, Id = "Gold"}, + new (){ Cutoff = 100, Id = "Silver"}, + new (){ Id = "Bronze"}, + } + }; + } + + public LeaderboardConfigFile(string? id, string name, SortOrder sortOrder, UpdateType updateType) + { + Id = id; + Name = name; + SortOrder = sortOrder; + UpdateType = updateType; + } + + public SortOrder SortOrder { get; set; } + public UpdateType UpdateType { get; set; } + + /// + /// Defaults to file-name if unspecified + /// + public string? Id { get; set; } + public string Name { get; set; } + public decimal BucketSize { get; set; } + public ResetConfig? ResetConfig { get; set; } + public TieringConfig? TieringConfig { get; set; } + + [JsonProperty("$schema")] + public string Value = "https://ugs-config-schemas.unity3d.com/v1/leaderboards.schema.json"; + + [JsonIgnore] + public string Extension => ".lb"; + + [JsonIgnore] + public string FileBodyText => JsonConvert.SerializeObject(this, GetSerializationSettings()); + + public static JsonSerializerSettings GetSerializationSettings() + { + var settings = new JsonSerializerSettings() + { + Converters = { new StringEnumConverter() }, + Formatting = Formatting.Indented, + DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate + }; + return settings; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsClient.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsClient.cs new file mode 100644 index 0000000..cddbb7f --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsClient.cs @@ -0,0 +1,221 @@ +using Unity.Services.Cli.Authoring.Service; +using Unity.Services.Cli.Leaderboards.Service; +using Unity.Services.Gateway.LeaderboardApiV1.Generated.Client; +using Unity.Services.Gateway.LeaderboardApiV1.Generated.Model; +using Unity.Services.Leaderboards.Authoring.Core.Model; +using Unity.Services.Leaderboards.Authoring.Core.Service; +using CoreSortOrder = Unity.Services.Leaderboards.Authoring.Core.Model.SortOrder; +using CoreUpdateType = Unity.Services.Leaderboards.Authoring.Core.Model.UpdateType; +using CoreResetConfig = Unity.Services.Leaderboards.Authoring.Core.Model.ResetConfig; +using CoreTieringConfig = Unity.Services.Leaderboards.Authoring.Core.Model.TieringConfig; +using CoreStrategy = Unity.Services.Leaderboards.Authoring.Core.Model.Strategy; +using ApiResetConfig = Unity.Services.Gateway.LeaderboardApiV1.Generated.Model.ResetConfig; +using ApiSortOrder = Unity.Services.Gateway.LeaderboardApiV1.Generated.Model.SortOrder; +using ApiTieringConfig = Unity.Services.Gateway.LeaderboardApiV1.Generated.Model.TieringConfig; +using ApiUpdateType = Unity.Services.Gateway.LeaderboardApiV1.Generated.Model.UpdateType; +using ApiStrategy = Unity.Services.Gateway.LeaderboardApiV1.Generated.Model.TieringConfig.StrategyEnum; +using LeaderboardConfig = Unity.Services.Leaderboards.Authoring.Core.Model.LeaderboardConfig; + +namespace Unity.Services.Cli.Leaderboards.Deploy; + +public class LeaderboardsClient : ILeaderboardsClient +{ + readonly ILeaderboardsService m_LeaderboardsService; + internal string ProjectId { get; set; } + internal string EnvironmentId { get; set; } + internal CancellationToken CancellationToken { get; set; } + + public LeaderboardsClient( + ILeaderboardsService service, + string projectId = "", + string environmentId = "", + CancellationToken cancellationToken = default) + { + m_LeaderboardsService = service; + ProjectId = projectId; + EnvironmentId = environmentId; + CancellationToken = cancellationToken; + } + + public void Initialize(string environmentId, string projectId, CancellationToken cancellationToken) + { + EnvironmentId = environmentId; + ProjectId = projectId; + CancellationToken = cancellationToken; + } + + public async Task Get(string id, CancellationToken token) + { + ApiResponse response = await m_LeaderboardsService.GetLeaderboardAsync( + ProjectId, + EnvironmentId, + id, + token); + return FromResponse(response.Data); + } + + public async Task Update(ILeaderboardConfig leaderboardConfig, CancellationToken token) + { + await m_LeaderboardsService.UpdateLeaderboardAsync( + ProjectId, + EnvironmentId, + leaderboardConfig.Id, + PatchFromConfig(leaderboardConfig), + token); + } + + public async Task Create(ILeaderboardConfig leaderboardConfig, CancellationToken token) + { + await m_LeaderboardsService.CreateLeaderboardAsync( + ProjectId, + EnvironmentId, + CreateFromConfig(leaderboardConfig), + token); + } + + public async Task Delete(ILeaderboardConfig leaderboardConfig, CancellationToken token) + { + await m_LeaderboardsService.DeleteLeaderboardAsync( + ProjectId, + EnvironmentId, + leaderboardConfig.Id, + token); + } + + public async Task> List(CancellationToken token) + { + const int limit = 50; + var leaderboards = new List(); + string? cursor = null; + List newBatch; + do + { + var rawResponse = await m_LeaderboardsService.GetLeaderboardsAsync( + ProjectId, + EnvironmentId, + cursor: cursor, + limit: limit, + cancellationToken: token); + + newBatch = rawResponse.ToList(); + cursor = newBatch.LastOrDefault()?.Id; + leaderboards.AddRange(newBatch.Select(FromResponse)); + + if (token.IsCancellationRequested) + break; + } while (newBatch.Count >= limit); + + return leaderboards; + } + + static ILeaderboardConfig FromResponse(UpdatedLeaderboardConfig responseData) + { + var lb = new LeaderboardConfig( + responseData.Id, + responseData.Name, + (CoreSortOrder)(int)responseData.SortOrder, + (CoreUpdateType)(int)responseData.UpdateType); + + lb.BucketSize = responseData.BucketSize; + lb.ResetConfig = FromResponse(responseData.ResetConfig); + lb.TieringConfig = FromResponse(responseData.TieringConfig); + lb.Path = "Remote"; + return lb; + } + + static CoreResetConfig? FromResponse(ApiResetConfig? resetConfig) + { + if (resetConfig == null) + return null; + + return new CoreResetConfig() + { + Archive = resetConfig.Archive, + Schedule = resetConfig.Schedule, + Start = resetConfig.Start + }; + } + + static CoreTieringConfig? FromResponse(ApiTieringConfig? tieringConfig) + { + if (tieringConfig == null) + return null; + + return new CoreTieringConfig() + { + Strategy = (Strategy)(int)tieringConfig.Strategy, + Tiers = FromResponse(tieringConfig.Tiers) + }; + } + + static List FromResponse(List tiers) + { + return tiers + .Select( + t => new Tier() + { + Cutoff = t.Cutoff, + Id = t.Id + }) + .ToList(); + } + + static LeaderboardIdConfig CreateFromConfig(ILeaderboardConfig leaderboardConfig) + { + return new LeaderboardIdConfig(leaderboardConfig.Id, leaderboardConfig.Name) + { + SortOrder = (ApiSortOrder)(int)leaderboardConfig.SortOrder, + UpdateType = (ApiUpdateType)(int)leaderboardConfig.UpdateType, + TieringConfig = FromConfig(leaderboardConfig.TieringConfig), + ResetConfig = FromConfig(leaderboardConfig.ResetConfig), + BucketSize = leaderboardConfig.BucketSize + }; + } + + + static LeaderboardPatchConfig PatchFromConfig(ILeaderboardConfig leaderboardConfig) + { + var res = new LeaderboardPatchConfig() + { + Name = leaderboardConfig.Name, + SortOrder = (ApiSortOrder)(int)leaderboardConfig.SortOrder, + UpdateType = (ApiUpdateType)(int)leaderboardConfig.UpdateType, + TieringConfig = FromConfig(leaderboardConfig.TieringConfig), + ResetConfig = FromConfig(leaderboardConfig.ResetConfig) + }; + return res; + } + + static ApiTieringConfig? FromConfig(CoreTieringConfig? config) + { + if (config == null) + return null; + return new ApiTieringConfig( + tiers:FromConfig(config.Tiers), + strategy: (ApiStrategy)(int)config.Strategy); + } + + static List FromConfig(List tiers) + { + return tiers + .Select( + t => new TieringConfigTiersInner(t.Id) + { + Cutoff = t.Cutoff + }) + .ToList(); + } + + static ApiResetConfig? FromConfig(CoreResetConfig? config) + { + if (config == null) + return null; + + return new ApiResetConfig() + { + Archive = config.Archive, + Schedule = config.Schedule, + Start = config.Start + }; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsConfigLoader.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsConfigLoader.cs new file mode 100644 index 0000000..c634509 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsConfigLoader.cs @@ -0,0 +1,62 @@ +using Newtonsoft.Json; +using Unity.Services.DeploymentApi.Editor; +using Unity.Services.Leaderboards.Authoring.Core.IO; +using Unity.Services.Leaderboards.Authoring.Core.Model; + +namespace Unity.Services.Cli.Leaderboards.Deploy; + +class LeaderboardsConfigLoader : ILeaderboardsConfigLoader +{ + readonly IFileSystem m_FileSystem; + + public LeaderboardsConfigLoader(IFileSystem fileSystem) + { + m_FileSystem = fileSystem; + } + + public async Task> LoadConfigsAsync(IReadOnlyCollection paths, CancellationToken cancellationToken) + { + var leaderboards = new List(); + var serializationSettings = LeaderboardConfigFile.GetSerializationSettings(); + foreach (var path in paths) + { + var fileName = Path.GetFileNameWithoutExtension(path); + var lb = new LeaderboardConfig(fileName, fileName); + lb.Path = path; + try + { + var content = await m_FileSystem.ReadAllText(path, cancellationToken); + var leaderboardConfigFile = JsonConvert.DeserializeObject( + content, + serializationSettings)!; + + lb = FromFile(leaderboardConfigFile, path); + lb.Status = new DeploymentStatus("Loaded"); + } + catch (Exception ex) + { + lb.Status = new DeploymentStatus( + "Failed to Load", + $"Error reading file: {ex.Message}", + SeverityLevel.Error); + } + leaderboards.Add(lb); + } + + return leaderboards; + } + + static LeaderboardConfig FromFile(LeaderboardConfigFile config, string path) + { + var lb = new LeaderboardConfig( + config.Id ?? Path.GetFileNameWithoutExtension(path), + config.Name, + config.SortOrder, + config.UpdateType); + lb.BucketSize = config.BucketSize; + lb.ResetConfig = config.ResetConfig; + lb.TieringConfig = config.TieringConfig; + lb.Path = path; + return lb; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsDeploymentService.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsDeploymentService.cs new file mode 100644 index 0000000..083e560 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsDeploymentService.cs @@ -0,0 +1,62 @@ +using Spectre.Console; +using Unity.Services.Cli.Authoring.Input; +using Unity.Services.Cli.Authoring.Model; +using Unity.Services.Cli.Authoring.Service; +using Unity.Services.Leaderboards.Authoring.Core.Deploy; +using Unity.Services.Leaderboards.Authoring.Core.Model; +using Unity.Services.Leaderboards.Authoring.Core.Service; + +namespace Unity.Services.Cli.Leaderboards.Deploy; + +public class LeaderboardDeploymentService : IDeploymentService +{ + readonly ILeaderboardsClient m_Client; + readonly ILeaderboardsDeploymentHandler m_DeploymentHandler; + readonly ILeaderboardsConfigLoader m_LeaderboardsConfigLoader; + readonly string m_ServiceType; + readonly string m_ServiceName; + readonly string m_DeployFileExtension; + + public LeaderboardDeploymentService( + ILeaderboardsClient client, + ILeaderboardsDeploymentHandler deploymentHandler, + ILeaderboardsConfigLoader leaderboardsConfigLoader) + { + m_Client = client; + m_DeploymentHandler = deploymentHandler; + m_LeaderboardsConfigLoader = leaderboardsConfigLoader; + m_ServiceType = "Leaderboard"; + m_ServiceName = "leaderboards"; + m_DeployFileExtension = ".lb"; + } + + string IDeploymentService.ServiceType => m_ServiceType; + string IDeploymentService.ServiceName => m_ServiceName; + string IDeploymentService.DeployFileExtension => m_DeployFileExtension; + + public async Task Deploy( + DeployInput deployInput, + IReadOnlyList filePaths, + string projectId, + string environmentId, + StatusContext? loadingContext, + CancellationToken cancellationToken) + { + m_Client.Initialize(environmentId, projectId, cancellationToken); + + var files = await m_LeaderboardsConfigLoader.LoadConfigsAsync(filePaths, cancellationToken); + + var deployStatusList = await m_DeploymentHandler.DeployAsync( + files, + deployInput.DryRun, + deployInput.Reconcile, + cancellationToken); + + return new DeploymentResult( + deployStatusList.Updated, + deployStatusList.Deleted, + deployStatusList.Created, + deployStatusList.Deployed, + deployStatusList.Failed); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsFetchService.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsFetchService.cs new file mode 100644 index 0000000..3a89b3c --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsFetchService.cs @@ -0,0 +1,64 @@ +using Spectre.Console; +using Unity.Services.Cli.Authoring.Input; +using Unity.Services.Cli.Authoring.Service; +using Unity.Services.Cli.Common.Utils; +using Unity.Services.Leaderboards.Authoring.Core.Fetch; +using Unity.Services.Leaderboards.Authoring.Core.Service; +using FetchResult = Unity.Services.Cli.Authoring.Model.FetchResult; + +namespace Unity.Services.Cli.Leaderboards.Deploy; + +public class LeaderboardFetchService : IFetchService +{ + readonly ILeaderboardsClient m_Client; + readonly ILeaderboardsFetchHandler m_FetchHandler; + readonly ILeaderboardsConfigLoader m_LeaderboardsConfigLoader; + readonly IDeployFileService m_DeployFileService; + readonly IUnityEnvironment m_UnityEnvironment; + readonly string m_ServiceType; + readonly string m_ServiceName; + readonly string m_FileExtension; + + public LeaderboardFetchService( + ILeaderboardsClient client, + ILeaderboardsFetchHandler fetchHandler, + ILeaderboardsConfigLoader leaderboardsConfigLoader, + IDeployFileService deployFileService, + IUnityEnvironment unityEnvironment) + { + m_Client = client; + m_FetchHandler = fetchHandler; + m_LeaderboardsConfigLoader = leaderboardsConfigLoader; + m_DeployFileService = deployFileService; + m_UnityEnvironment = unityEnvironment; + m_ServiceType = "Leaderboard"; + m_ServiceName = "leaderboards"; + m_FileExtension = ".lb"; + } + + string IFetchService.ServiceType => m_ServiceType; + string IFetchService.ServiceName => m_ServiceName; + string IFetchService.FileExtension => m_FileExtension; + public async Task FetchAsync(FetchInput input, IReadOnlyList filePaths, StatusContext? loadingContext, CancellationToken cancellationToken) + { + var environmentId = await m_UnityEnvironment.FetchIdentifierAsync(cancellationToken); + m_Client.Initialize(environmentId, input.CloudProjectId!, cancellationToken); + + var leaderboards = await m_LeaderboardsConfigLoader + .LoadConfigsAsync(filePaths, cancellationToken); + + var deployStatusList = await m_FetchHandler.FetchAsync( + input.Path, + leaderboards, + input.DryRun, + input.Reconcile, + cancellationToken); + + return new FetchResult( + updated: deployStatusList.Updated, + deleted: deployStatusList.Deleted, + created: deployStatusList.Created, + authored: deployStatusList.Fetched, + failed: deployStatusList.Failed); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsSerializer.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsSerializer.cs new file mode 100644 index 0000000..5de19d3 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsSerializer.cs @@ -0,0 +1,23 @@ +using Unity.Services.Leaderboards.Authoring.Core.Model; +using Unity.Services.Leaderboards.Authoring.Core.Serialization; + +namespace Unity.Services.Cli.Leaderboards.Deploy; + +public class LeaderboardsSerializer : ILeaderboardsSerializer +{ + public string Serialize(ILeaderboardConfig config) + { + var fileName = Path.GetFileNameWithoutExtension(config.Path); + var leaderboardFile = new LeaderboardConfigFile( + fileName == config.Id ? null : config.Id, + config.Name, + config.SortOrder, + config.UpdateType) + { + BucketSize = config.BucketSize, + ResetConfig = config.ResetConfig, + TieringConfig = config.TieringConfig, + }; + return leaderboardFile.FileBodyText; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/CreateLeaderboardHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/CreateLeaderboardHandler.cs deleted file mode 100644 index f52feb7..0000000 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/CreateLeaderboardHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.Extensions.Logging; -using Unity.Services.Cli.Common.Console; -using Unity.Services.Cli.Common.Utils; -using Unity.Services.Cli.Leaderboards.Input; -using Unity.Services.Cli.Leaderboards.Service; - -namespace Unity.Services.Cli.Leaderboards.Handlers; - -static class CreateLeaderboardHandler -{ - public static async Task CreateLeaderboardAsync(CreateInput input, IUnityEnvironment unityEnvironment, - ILeaderboardsService service, ILogger logger, ILoadingIndicator loadingIndicator, CancellationToken cancellationToken) - { - await loadingIndicator.StartLoadingAsync( - "Creating leaderboard...", - context => CreateAsync( - input, unityEnvironment, service, logger, cancellationToken)); - } - - internal static async Task CreateAsync( - CreateInput input, IUnityEnvironment unityEnvironment, ILeaderboardsService service, - ILogger logger, CancellationToken cancellationToken) - { - string environmentId = await unityEnvironment.FetchIdentifierAsync(cancellationToken); - await service.CreateLeaderboardAsync( - input.CloudProjectId!, - environmentId, - await RequestBodyHandler.GetRequestBodyAsync(input.JsonFilePath), - cancellationToken); - - logger.LogInformation("leaderboard created!"); - } -} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/ImportExport/ExportHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/ImportExport/ExportHandler.cs index d37a97c..c62db9e 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/ImportExport/ExportHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/ImportExport/ExportHandler.cs @@ -7,11 +7,10 @@ namespace Unity.Services.Cli.Leaderboards.Handlers.ImportExport; static class ExportHandler { - internal const string k_LoadingIndicatorMessage = "Exporting your environment..."; - internal const string k_CompleteIndicatorMessage = "Export complete."; + internal const string LoadingIndicatorMessage = "Exporting your environment..."; + internal const string CompleteIndicatorMessage = "Export complete."; public static async Task ExportAsync( - ListLeaderboardInput listLeaderboardInput, ExportInput exportInput, ILogger logger, LeaderboardExporter? leaderboardExporter, @@ -19,13 +18,12 @@ public static async Task ExportAsync( CancellationToken cancellationToken) { await loadingIndicator.StartLoadingAsync( - k_LoadingIndicatorMessage, + LoadingIndicatorMessage, _ => { - leaderboardExporter!.ListLeaderboardInput = listLeaderboardInput; - return leaderboardExporter.ExportAsync(exportInput, cancellationToken); + return leaderboardExporter!.ExportAsync(exportInput, cancellationToken); } ); - logger.LogInformation(k_CompleteIndicatorMessage); + logger.LogInformation(CompleteIndicatorMessage); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/ImportExport/ImportHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/ImportExport/ImportHandler.cs index e38e384..6850b19 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/ImportExport/ImportHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/ImportExport/ImportHandler.cs @@ -6,8 +6,8 @@ namespace Unity.Services.Cli.Leaderboards.Handlers.ImportExport; static class ImportHandler { - internal const string k_LoadingIndicatorMessage = "Importing configs..."; - internal const string k_CompleteIndicatorMessage = "Import complete."; + internal const string LoadingIndicatorMessage = "Importing configs..."; + internal const string CompleteIndicatorMessage = "Import complete."; public static async Task ImportAsync( ImportInput importInput, @@ -18,8 +18,8 @@ CancellationToken cancellationToken ) { await loadingIndicator.StartLoadingAsync( - k_LoadingIndicatorMessage, + LoadingIndicatorMessage, _ => leaderboardImporter!.ImportAsync(importInput, cancellationToken)); - logger.LogInformation(k_CompleteIndicatorMessage); + logger.LogInformation(CompleteIndicatorMessage); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/ImportExport/LeaderboardsExporter.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/ImportExport/LeaderboardsExporter.cs index 3f9f7ec..5866854 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/ImportExport/LeaderboardsExporter.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/ImportExport/LeaderboardsExporter.cs @@ -12,6 +12,7 @@ namespace Unity.Services.Cli.Leaderboards.Handlers.ImportExport; class LeaderboardExporter : BaseExporter { + const int k_Limit = 50; readonly ILeaderboardsService m_LeaderboardsService; public LeaderboardExporter( @@ -32,16 +33,29 @@ public LeaderboardExporter( protected override string FileName => LeaderboardConstants.ZipName; protected override string EntryName => LeaderboardConstants.EntryName; - public ListLeaderboardInput ListLeaderboardInput { get; set; } = null!; - protected override async Task> ListConfigsAsync(string projectId, string environmentId, CancellationToken cancellationToken) { - return await m_LeaderboardsService.GetLeaderboardsAsync( - projectId, - environmentId, - ListLeaderboardInput.Cursor, - ListLeaderboardInput.Limit, - cancellationToken); + var leaderboards = new List(); + string? cursor = null; + List newBatch; + do + { + var rawResponse = await m_LeaderboardsService.GetLeaderboardsAsync( + projectId, + environmentId, + cursor: cursor, + limit: k_Limit, + cancellationToken: cancellationToken); + + newBatch = rawResponse.ToList(); + cursor = newBatch.LastOrDefault()?.Id; + leaderboards.AddRange(newBatch); + + if (cancellationToken.IsCancellationRequested) + break; + } while (newBatch.Count >= k_Limit); + + return leaderboards; } protected override ImportExportEntry ToImportExportEntry(UpdatedLeaderboardConfig value) diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/UpdateLeaderboardHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/UpdateLeaderboardHandler.cs deleted file mode 100644 index 3691b95..0000000 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Handlers/UpdateLeaderboardHandler.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.Extensions.Logging; -using Unity.Services.Cli.Common.Console; -using Unity.Services.Cli.Common.Utils; -using Unity.Services.Cli.Leaderboards.Input; -using Unity.Services.Cli.Leaderboards.Service; - -namespace Unity.Services.Cli.Leaderboards.Handlers; - -static class UpdateLeaderboardHandler -{ - public static async Task UpdateLeaderboardAsync(UpdateInput input, IUnityEnvironment unityEnvironment, - ILeaderboardsService service, ILogger logger, ILoadingIndicator loadingIndicator, CancellationToken cancellationToken) - { - await loadingIndicator.StartLoadingAsync( - "Updating leaderboard...", - context => UpdateAsync( - input, unityEnvironment, service, logger, cancellationToken)); - } - - internal static async Task UpdateAsync( - UpdateInput input, IUnityEnvironment unityEnvironment, ILeaderboardsService service, - ILogger logger, CancellationToken cancellationToken) - { - string environmentId = await unityEnvironment.FetchIdentifierAsync(cancellationToken); - await service.UpdateLeaderboardAsync( - input.CloudProjectId!, - environmentId, - input.LeaderboardId!, - await RequestBodyHandler.GetRequestBodyAsync(input.JsonFilePath), - cancellationToken); - - logger.LogInformation("leaderboard updated!"); - } -} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/IO/FileSystem.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/IO/FileSystem.cs new file mode 100644 index 0000000..f4225d3 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/IO/FileSystem.cs @@ -0,0 +1,22 @@ +using Unity.Services.Leaderboards.Authoring.Core.IO; + +namespace Unity.Services.Cli.Leaderboards.IO; + +public class FileSystem : IFileSystem +{ + public Task ReadAllText(string path, CancellationToken token = default(CancellationToken)) + { + return File.ReadAllTextAsync(path, token); + } + + public Task WriteAllText(string path, string contents, CancellationToken token = default(CancellationToken)) + { + return File.WriteAllTextAsync(path, contents, token); + } + + public Task Delete(string path, CancellationToken token = default(CancellationToken)) + { + File.Delete(path); + return Task.CompletedTask; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/LeaderboardsModule.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/LeaderboardsModule.cs index 3a5c8b5..0d30064 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/LeaderboardsModule.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/LeaderboardsModule.cs @@ -11,18 +11,25 @@ using Unity.Services.Cli.Common.Networking; using Unity.Services.Cli.Common.Utils; using Unity.Services.Cli.Common.Validator; -using Unity.Services.Cli.Authoring.Compression; using Unity.Services.Cli.Authoring.Export.Input; using Unity.Services.Cli.Authoring.Import.Input; using Unity.Services.Cli.Authoring.Service; using System.IO.Abstractions; using Unity.Services.Cli.Leaderboards.Handlers.ImportExport; +using Unity.Services.Cli.Leaderboards.Deploy; +using Unity.Services.Leaderboards.Authoring.Core.Service; + using Unity.Services.Cli.Leaderboards.Handlers; using Unity.Services.Cli.Leaderboards.Input; using Unity.Services.Cli.Leaderboards.Service; using Unity.Services.Gateway.LeaderboardApiV1.Generated.Api; -using Unity.Services.Gateway.LeaderboardApiV1.Generated.Model; - +using Unity.Services.Cli.Authoring.Handlers; +using Unity.Services.Leaderboards.Authoring.Core.Deploy; +using Unity.Services.Leaderboards.Authoring.Core.Fetch; +using Unity.Services.Leaderboards.Authoring.Core.Serialization; +using LeaderboardFile = Unity.Services.Cli.Leaderboards.Deploy.LeaderboardConfigFile; +using CoreIFileSystem = Unity.Services.Leaderboards.Authoring.Core.IO.IFileSystem; +using CoreFileSystem = Unity.Services.Cli.Leaderboards.IO.FileSystem; namespace Unity.Services.Cli.Leaderboards; @@ -32,8 +39,6 @@ public class LeaderboardsModule : ICommandModule internal Command ImportCommand { get; } internal Command ListLeaderboardsCommand { get; } internal Command GetLeaderboardCommand { get; } - internal Command CreateLeaderboardCommand { get; } - internal Command UpdateLeaderboardCommand { get; } internal Command DeleteLeaderboardCommand { get; } internal Command ResetLeaderboardCommand { get; } @@ -61,14 +66,11 @@ public LeaderboardsModule() { CommonInput.CloudProjectIdOption, CommonInput.EnvironmentNameOption, - ListLeaderboardInput.CursorOption, - ListLeaderboardInput.LimitOption, ExportInput.OutputDirectoryArgument, ExportInput.DryRunOption, ExportInput.FileNameArgument }; ExportCommand.SetHandler< - ListLeaderboardInput, ExportInput, ILogger, LeaderboardExporter, @@ -106,34 +108,6 @@ public LeaderboardsModule() ILoadingIndicator, CancellationToken>( GetLeaderboardHandler.GetLeaderboardConfigAsync); - - CreateLeaderboardCommand = new Command("create", "Create a new leaderboard.") - { - CreateInput.RequestBodyArgument, - }; - CreateLeaderboardCommand.SetHandler< - CreateInput, - IUnityEnvironment, - ILeaderboardsService, - ILogger, - ILoadingIndicator, - CancellationToken>(CreateLeaderboardHandler.CreateLeaderboardAsync); - - UpdateLeaderboardCommand = new Command("update", "Update a leaderboard.") - { - LeaderboardIdInput.RequestLeaderboardIdArgument, - UpdateInput.RequestBodyArgument, - CommonInput.CloudProjectIdOption, - CommonInput.EnvironmentNameOption, - }; - UpdateLeaderboardCommand.SetHandler< - UpdateInput, - IUnityEnvironment, - ILeaderboardsService, - ILogger, - ILoadingIndicator, - CancellationToken>(UpdateLeaderboardHandler.UpdateLeaderboardAsync); - DeleteLeaderboardCommand = new Command("delete", "Delete a leaderboard.") { LeaderboardIdInput.RequestLeaderboardIdArgument, @@ -165,12 +139,11 @@ public LeaderboardsModule() ModuleRootCommand = new Command("leaderboards", "Manage Leaderboards.") { - CreateLeaderboardCommand, DeleteLeaderboardCommand, GetLeaderboardCommand, - UpdateLeaderboardCommand, ListLeaderboardsCommand, ResetLeaderboardCommand, + ModuleRootCommand.AddNewFileCommand("Leaderboard"), ExportCommand, ImportCommand }; @@ -203,13 +176,19 @@ public static void RegisterServices(HostBuilderContext hostBuilderContext, IServ Gateway.LeaderboardApiV1.Generated.Client.RetryConfiguration.AsyncRetryPolicy = retryAfterPolicy; serviceCollection.AddTransient(_ => new LeaderboardsApi(config)); serviceCollection.AddTransient(); - // TODO: Move this service registration into the common ImportExportModule in the - // authoring project once it exists. - serviceCollection.AddTransient(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); - + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Service/ILeaderboardsService.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Service/ILeaderboardsService.cs index e2fc377..6143d99 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Service/ILeaderboardsService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Service/ILeaderboardsService.cs @@ -11,11 +11,31 @@ public Task> GetLeaderboardsAsync(string p Task> GetLeaderboardAsync(string projectId, string environmentId, string leaderboardId, CancellationToken cancellationToken = default); - Task> CreateLeaderboardAsync(string projectId, string environmentId, string body, + Task> CreateLeaderboardAsync( + string projectId, + string environmentId, + LeaderboardIdConfig leaderboard, CancellationToken cancellationToken); - Task> UpdateLeaderboardAsync(string projectId, string environmentId, string leaderboardId, - string body, CancellationToken cancellationToken); + Task> CreateLeaderboardAsync( + string projectId, + string environmentId, + string body, + CancellationToken cancellationToken); + + Task> UpdateLeaderboardAsync( + string projectId, + string environmentId, + string leaderboardId, + LeaderboardPatchConfig leaderboard, + CancellationToken cancellationToken); + + Task> UpdateLeaderboardAsync( + string projectId, + string environmentId, + string leaderboardId, + string body, + CancellationToken cancellationToken); Task> DeleteLeaderboardAsync(string projectId, string environmentId, string leaderboardId, CancellationToken cancellationToken); diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Service/LeaderboardsService.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Service/LeaderboardsService.cs index f6fbf6d..1ec60b1 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Service/LeaderboardsService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Service/LeaderboardsService.cs @@ -52,39 +52,77 @@ public async Task> GetLeaderboardAsync(str return response; } - public async Task> CreateLeaderboardAsync(string projectId, string environmentId, string body, CancellationToken cancellationToken) + public async Task> CreateLeaderboardAsync( + string projectId, + string environmentId, + string body, + CancellationToken cancellationToken) + { + + var createRequest = DeserializeBody(body); + return await CreateLeaderboardAsync( + projectId, + environmentId, + createRequest, + cancellationToken); + } + + public async Task> CreateLeaderboardAsync( + string projectId, + string environmentId, + LeaderboardIdConfig leaderboard, + CancellationToken cancellationToken) { await AuthorizeServiceAsync(cancellationToken); ValidateProjectIdAndEnvironmentId(projectId, environmentId); - var createRequest = DeserializeBody(body); var response = await m_LeaderboardsApiAsync.CreateLeaderboardWithHttpInfoAsync( Guid.Parse(projectId), Guid.Parse(environmentId), - createRequest, + leaderboard, cancellationToken: cancellationToken ); return response; } - public async Task> UpdateLeaderboardAsync(string projectId, string environmentId, string leaderboardId, string body, CancellationToken cancellationToken) + public async Task> UpdateLeaderboardAsync( + string projectId, + string environmentId, + string leaderboardId, + LeaderboardPatchConfig leaderboard, + CancellationToken cancellationToken) { await AuthorizeServiceAsync(cancellationToken); ValidateProjectIdAndEnvironmentId(projectId, environmentId); - var updateRequest = DeserializeBody(body); var response = await m_LeaderboardsApiAsync.UpdateLeaderboardConfigWithHttpInfoAsync( Guid.Parse(projectId), Guid.Parse(environmentId), leaderboardId, - updateRequest, + leaderboard, cancellationToken: cancellationToken ); return response; } + public async Task> UpdateLeaderboardAsync( + string projectId, + string environmentId, + string leaderboardId, + string body, + CancellationToken cancellationToken) + { + var updateRequest = DeserializeBody(body); + return await UpdateLeaderboardAsync( + projectId, + environmentId, + leaderboardId, + updateRequest, + cancellationToken); + } + public async Task> DeleteLeaderboardAsync(string projectId, string environmentId, string leaderboardId, CancellationToken cancellationToken) { await AuthorizeServiceAsync(cancellationToken); diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Unity.Services.Cli.Leaderboards.csproj b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Unity.Services.Cli.Leaderboards.csproj index 810a254..128da2d 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Unity.Services.Cli.Leaderboards.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Unity.Services.Cli.Leaderboards.csproj @@ -3,14 +3,13 @@ net6.0 enable enable - Unity.Services.Cli.Leaderboard true - TRACE;FEATURE_LEADERBOARDS_IMPORT_EXPORT + TRACE;FEATURE_LEADERBOARDS_DEPLOY;FEATURE_LEADERBOARDS_IMPORT_EXPORT; - TRACE;FEATURE_LEADERBOARDS_IMPORT_EXPORT + TRACE;FEATURE_LEADERBOARDS_DEPLOY;FEATURE_LEADERBOARDS_IMPORT_EXPORT @@ -24,6 +23,7 @@ + diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigDeploymentResultTests.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigDeploymentResultTests.cs new file mode 100644 index 0000000..8f5065e --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigDeploymentResultTests.cs @@ -0,0 +1,49 @@ +using NUnit.Framework; +using Unity.Services.Cli.Authoring.Model; +using Unity.Services.Cli.RemoteConfig.Deploy; +using Unity.Services.DeploymentApi.Editor; + +namespace Unity.Services.Cli.RemoteConfig.UnitTest.Deploy; + +public class RemoteConfigDeploymentResultTests +{ + RemoteConfigDeploymentResult m_RemoteConfigDeploymentResult; + + readonly IReadOnlyList m_AuthoredItems = new[] + { + new DeployContent("authored", "authored", "authored") + }; + readonly IReadOnlyList m_UpdatedItems = new[] + { + new DeployContent("updated", "updated", "updated") + }; + readonly IReadOnlyList m_CreatedItems = new[] + { + new DeployContent("created", "created", "created") + }; + readonly IReadOnlyList m_DeletedItems = new[] + { + new DeployContent("deleted", "deleted", "deleted") + }; + readonly IReadOnlyList m_FailedItems = new[] + { + new DeployContent("failed", "failed", "failed") + }; + + [SetUp] + public void SetUp() + { + m_RemoteConfigDeploymentResult = new RemoteConfigDeploymentResult( + m_UpdatedItems, m_DeletedItems, m_CreatedItems, m_AuthoredItems, m_FailedItems); + } + + [Test] + public void ToTableHasCorrectAmountRows() + { + var tableResult = m_RemoteConfigDeploymentResult.ToTable(); + + var itemsCount = m_AuthoredItems.Count + m_UpdatedItems.Count + m_CreatedItems.Count + m_DeletedItems.Count + m_FailedItems.Count; + + Assert.That(tableResult.Result, Has.Count.EqualTo(itemsCount)); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigFetchServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigFetchServiceTests.cs index 2ab73fe..a06e9e3 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigFetchServiceTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigFetchServiceTests.cs @@ -5,12 +5,9 @@ using Unity.Services.Cli.Common.Utils; using Unity.Services.Cli.Authoring.Input; using Unity.Services.Cli.Authoring.Model; -using Unity.Services.Cli.Authoring.Service; using Unity.Services.Cli.RemoteConfig.Deploy; using Unity.Services.Cli.RemoteConfig.Service; using Unity.Services.DeploymentApi.Editor; -using Unity.Services.RemoteConfig.Editor.Authoring.Core.Deployment; -using Unity.Services.RemoteConfig.Editor.Authoring.Core.ErrorHandling; using Unity.Services.RemoteConfig.Editor.Authoring.Core.Fetch; using Unity.Services.RemoteConfig.Editor.Authoring.Core.Model; using Unity.Services.RemoteConfig.Editor.Authoring.Core.Results; @@ -25,7 +22,6 @@ public class RemoteConfigFetchServiceTests RemoteConfigFetchService? m_RemoteConfigFetchService; const string k_ValidProjectId = "a912b1fd-541d-42e1-89f2-85436f27aabd"; const string k_ValidEnvironmentId = "00000000-0000-0000-0000-000000000000"; - const string k_DeployFileExtension = ".rc"; static readonly List k_ValidFilePaths = new() { @@ -37,7 +33,6 @@ public class RemoteConfigFetchServiceTests readonly Mock m_MockUnityEnvironment = new(); readonly Mock m_MockCliRemoteConfigClient = new(); - readonly Mock m_MockDeployFileService = new(); readonly Mock m_MockRemoteConfigScriptsLoader = new(); readonly Mock m_MockRemoteConfigFetchHandler = new(); @@ -57,7 +52,6 @@ public void SetUp() { m_MockUnityEnvironment.Reset(); m_MockCliRemoteConfigClient.Reset(); - m_MockDeployFileService.Reset(); m_MockRemoteConfigScriptsLoader.Reset(); m_MockRemoteConfigFetchHandler.Reset(); m_MockRemoteConfigServicesWrapper.Reset(); @@ -68,13 +62,10 @@ public void SetUp() m_MockUnityEnvironment.Object, m_MockRemoteConfigFetchHandler.Object, m_MockCliRemoteConfigClient.Object, - m_MockDeployFileService.Object, m_MockRemoteConfigScriptsLoader.Object); m_MockUnityEnvironment.Setup(x => x.FetchIdentifierAsync(CancellationToken.None)) .ReturnsAsync(k_ValidEnvironmentId); - m_MockDeployFileService.Setup(d => d.ListFilesToDeploy(new[] { m_DefaultInput.Path }, k_DeployFileExtension)) - .Returns(k_ValidFilePaths); m_RemoteConfigFiles = new List(k_ValidFilePaths.Count); foreach (var filePath in k_ValidFilePaths) @@ -139,6 +130,7 @@ public async Task FetchAsync_MapsResultProperly() { var res = await m_RemoteConfigFetchService!.FetchAsync( m_DefaultInput, + k_ValidFilePaths, (StatusContext)null!, CancellationToken.None); @@ -166,6 +158,7 @@ public async Task FetchAsync_FailedToLoadIsForwarded() var res = await m_RemoteConfigFetchService!.FetchAsync( m_DefaultInput, + k_ValidFilePaths, (StatusContext)null!, CancellationToken.None); diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Model/CliRemoteConfigEntryTests.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Model/CliRemoteConfigEntryTests.cs new file mode 100644 index 0000000..bf3638e --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Model/CliRemoteConfigEntryTests.cs @@ -0,0 +1,38 @@ +using NUnit.Framework; +using Unity.Services.Cli.Authoring.Model.TableOutput; +using Unity.Services.Cli.RemoteConfig.Model; + +namespace Unity.Services.Cli.RemoteConfig.UnitTest.Model; + +class CliRemoteConfigEntryTests +{ + const string k_EntryName = "test_name"; + const string k_EntryType = "test_type"; + const string k_EntryPath = "test_path"; + const float k_EntryProgress = 0f; + const string k_EntryStatus = "tests_status"; + const string k_EntryDetail = "tests_status_details"; + + readonly CliRemoteConfigEntry m_RemoteConfigEntry = new CliRemoteConfigEntry( + k_EntryName, + k_EntryType, + k_EntryPath, + k_EntryProgress, + k_EntryStatus, + k_EntryDetail); + + + [Test] + public void CliRemoteConfigEntryToRowWorksCorrectly() + { + var rowResult = RowContent.ToRow(m_RemoteConfigEntry); + Assert.Multiple(() => + { + Assert.That(rowResult.Name, Is.EqualTo(k_EntryName)); + Assert.That(rowResult.Type, Is.EqualTo(k_EntryType)); + Assert.That(rowResult.Status, Is.EqualTo(k_EntryStatus)); + Assert.That(rowResult.Details, Is.EqualTo(k_EntryDetail)); + Assert.That(rowResult.Path, Is.EqualTo(k_EntryPath)); + }); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/CliRemoteConfigFetchHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/CliRemoteConfigFetchHandler.cs index bef71ae..c7af282 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/CliRemoteConfigFetchHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/CliRemoteConfigFetchHandler.cs @@ -1,3 +1,4 @@ +using Unity.Services.DeploymentApi.Editor; using Unity.Services.RemoteConfig.Editor.Authoring.Core.Fetch; using Unity.Services.RemoteConfig.Editor.Authoring.Core.IO; using Unity.Services.RemoteConfig.Editor.Authoring.Core.Json; @@ -19,4 +20,20 @@ protected override IRemoteConfigFile ConstructRemoteConfigFile(string path) { return new RemoteConfigFile(Path.GetFileName(path), path); } + + protected override void UpdateStatus( + IRemoteConfigFile remoteConfigFile, + string status, + string detail, + SeverityLevel severityLevel) + { + var file = (RemoteConfigFile)remoteConfigFile; + file.Status = new DeploymentStatus(status, detail, severityLevel); + } + + protected override void UpdateProgress(IRemoteConfigFile remoteConfigFile, float progress) + { + var file = (RemoteConfigFile)remoteConfigFile; + file.Progress = progress; + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigDeploymentResult.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigDeploymentResult.cs new file mode 100644 index 0000000..c356ec9 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigDeploymentResult.cs @@ -0,0 +1,48 @@ +using Unity.Services.Cli.Authoring.Model; +using Unity.Services.Cli.Authoring.Model.TableOutput; +using Unity.Services.DeploymentApi.Editor; + +namespace Unity.Services.Cli.RemoteConfig.Deploy; + +public class RemoteConfigDeploymentResult : DeploymentResult +{ + public RemoteConfigDeploymentResult( + IReadOnlyList updated, + IReadOnlyList deleted, + IReadOnlyList created, + IReadOnlyList authored, + IReadOnlyList failed, + bool dryRun = false) : + base( + updated, + deleted, + created, + authored, + failed, + dryRun) + { } + public RemoteConfigDeploymentResult(IReadOnlyList results) : base(results) { } + + public override TableContent ToTable() + { + var baseTable = new TableContent(); + baseTable.IsDryRun = DryRun; + + foreach (var file in Authored) + { + baseTable.AddRow(RowContent.ToRow(file)); + + baseTable.AddRows(Updated.Where(key => key.Path == file.Path).Select(RowContent.ToRow).ToList()); + baseTable.AddRows(Deleted.Where(key => key.Path == file.Path).Select(RowContent.ToRow).ToList()); + baseTable.AddRows(Created.Where(key => key.Path == file.Path).Select(RowContent.ToRow).ToList()); + baseTable.AddRows(Failed.Where(key => key.Path == file.Path).Select(RowContent.ToRow).ToList()); + } + + baseTable.UpdateOrAddRows(Updated.Select(RowContent.ToRow).ToList()); + baseTable.UpdateOrAddRows(Deleted.Select(RowContent.ToRow).ToList()); + baseTable.UpdateOrAddRows(Created.Select(RowContent.ToRow).ToList()); + baseTable.UpdateOrAddRows(Failed.Select(RowContent.ToRow).ToList()); + + return baseTable; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigDeploymentService.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigDeploymentService.cs index fc74c63..6f1eaed 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigDeploymentService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigDeploymentService.cs @@ -72,7 +72,7 @@ public async Task Deploy( if (deploymentResult == null) { - return new DeploymentResult(loadResult.Loaded.Concat(loadResult.Failed).ToList()); + return new RemoteConfigDeploymentResult(loadResult.Loaded.Concat(loadResult.Failed).ToList()); } var failed = deploymentResult @@ -81,7 +81,7 @@ public async Task Deploy( .UnionBy(loadResult.Failed, f => f.Path) .ToList(); - return new DeploymentResult( + return new RemoteConfigDeploymentResult( ToDeployContents(deploymentResult.Updated, 100, "Updated"), ToDeployContents(deploymentResult.Deleted, 100, "Deleted"), ToDeployContents(deploymentResult.Created, 100, "Created"), diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchResult.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchResult.cs new file mode 100644 index 0000000..2134dd1 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchResult.cs @@ -0,0 +1,49 @@ +using Unity.Services.Cli.Authoring.Model; +using Unity.Services.Cli.Authoring.Model.TableOutput; +using Unity.Services.DeploymentApi.Editor; + +namespace Unity.Services.Cli.RemoteConfig.Deploy; + +public class RemoteConfigFetchResult : FetchResult +{ + + public RemoteConfigFetchResult( + IReadOnlyList updated, + IReadOnlyList deleted, + IReadOnlyList created, + IReadOnlyList authored, + IReadOnlyList failed, + bool dryRun = false) : + base( + updated, + deleted, + created, + authored, + failed, + dryRun) + { } + public RemoteConfigFetchResult(IReadOnlyList results) : base(results) { } + + public override TableContent ToTable() + { + var baseTable = new TableContent(); + baseTable.IsDryRun = DryRun; + + foreach (var file in Authored) + { + baseTable.AddRow(RowContent.ToRow(file)); + + baseTable.AddRows(Updated.Where(key => key.Path == file.Path).Select(RowContent.ToRow).ToList()); + baseTable.AddRows(Deleted.Where(key => key.Path == file.Path).Select(RowContent.ToRow).ToList()); + baseTable.AddRows(Created.Where(key => key.Path == file.Path).Select(RowContent.ToRow).ToList()); + baseTable.AddRows(Failed.Where(key => key.Path == file.Path).Select(RowContent.ToRow).ToList()); + } + + baseTable.UpdateOrAddRows(Updated.Select(RowContent.ToRow).ToList()); + baseTable.UpdateOrAddRows(Deleted.Select(RowContent.ToRow).ToList()); + baseTable.UpdateOrAddRows(Created.Select(RowContent.ToRow).ToList()); + baseTable.UpdateOrAddRows(Failed.Select(RowContent.ToRow).ToList()); + + return baseTable; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchService.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchService.cs index 7e2f60d..bfeb91e 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchService.cs @@ -15,7 +15,6 @@ class RemoteConfigFetchService : IFetchService readonly IUnityEnvironment m_UnityEnvironment; readonly IRemoteConfigFetchHandler m_FetchHandler; readonly ICliRemoteConfigClient m_RemoteConfigClient; - readonly IDeployFileService m_DeployFileService; readonly IRemoteConfigScriptsLoader m_RemoteConfigScriptsLoader; readonly string m_DeployFileExtension; public string ServiceType { get; } @@ -27,14 +26,12 @@ public RemoteConfigFetchService( IUnityEnvironment unityEnvironment, IRemoteConfigFetchHandler fetchHandler, ICliRemoteConfigClient remoteConfigClient, - IDeployFileService deployFileService, IRemoteConfigScriptsLoader remoteConfigScriptsLoader ) { m_UnityEnvironment = unityEnvironment; m_FetchHandler = fetchHandler; m_RemoteConfigClient = remoteConfigClient; - m_DeployFileService = deployFileService; m_RemoteConfigScriptsLoader = remoteConfigScriptsLoader; ServiceType = "Remote Config"; ServiceName = "remote-config"; @@ -43,15 +40,15 @@ IRemoteConfigScriptsLoader remoteConfigScriptsLoader public async Task FetchAsync( FetchInput input, + IReadOnlyList filePaths, StatusContext? loadingContext, CancellationToken cancellationToken) { var environmentId = await m_UnityEnvironment.FetchIdentifierAsync(cancellationToken); m_RemoteConfigClient.Initialize(input.CloudProjectId!, environmentId, cancellationToken); - var remoteConfigFiles = m_DeployFileService.ListFilesToDeploy(new[] { input.Path }, m_DeployFileExtension).ToList(); var loadResult = await m_RemoteConfigScriptsLoader - .LoadScriptsAsync(remoteConfigFiles, cancellationToken); + .LoadScriptsAsync(filePaths, cancellationToken); var configFiles = loadResult.Loaded.ToList(); loadingContext?.Status($"Fetching {ServiceType} Files..."); @@ -69,7 +66,7 @@ public async Task FetchAsync( .UnionBy(loadResult.Failed, f => f.Path) .ToList(); - return new FetchResult( + return new RemoteConfigFetchResult( fetchResult.Updated.Select(rce => GetDeployContent(rce, "Updated")).ToList(), fetchResult.Deleted.Select(rce => GetDeployContent(rce, "Deleted")).ToList(), fetchResult.Created.Select(rce => GetDeployContent(rce, "Updated")).ToList(), diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigScriptsLoader.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigScriptsLoader.cs index b21001e..eb0a6ba 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigScriptsLoader.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigScriptsLoader.cs @@ -39,7 +39,7 @@ public async Task LoadScriptsAsync(IReadOnlyList filePaths, content.ToRemoteConfigEntries(file, new RemoteConfigParser(new ConfigTypeDeriver())); loaded.Add(file); - file.Status = new DeploymentStatus(Statuses.Loaded); + file.Status = new DeploymentStatus(Statuses.Loaded, ""); } catch (JsonException ex) { diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/RemoteConfigModule.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/RemoteConfigModule.cs index 4ccb56f..6999da6 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/RemoteConfigModule.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/RemoteConfigModule.cs @@ -55,6 +55,7 @@ public RemoteConfigModule() CommonInput.CloudProjectIdOption, ImportInput.InputDirectoryArgument, DryRunInput.DryRunOption, + ImportInput.ReconcileOption, ImportInput.FileNameArgument }; ImportCommand.SetHandler(ImportHandler.ImportAsync); diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Unity.Services.Cli.RemoteConfig.csproj b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Unity.Services.Cli.RemoteConfig.csproj index e4eba9f..d9d68b7 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Unity.Services.Cli.RemoteConfig.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Unity.Services.Cli.RemoteConfig.csproj @@ -20,7 +20,7 @@ - + $(DefineConstants);$(ExtraDefineConstants) diff --git a/Unity.Services.Cli/Unity.Services.Cli.sln b/Unity.Services.Cli/Unity.Services.Cli.sln index 4d1ff88..5738c6c 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.sln +++ b/Unity.Services.Cli/Unity.Services.Cli.sln @@ -67,6 +67,13 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Services.Cli.Integration.MockServerApp", "Unity.Services.Cli.Integration.MockServerApp\Unity.Services.Cli.Integration.MockServerApp.csproj", "{27BBE31C-43CE-45AA-9DE4-BF3D097DC1A3}" EndProject EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Services.Leaderboards.Authoring.Core", "Unity.Services.Leaderboards.Authoring.Core\Unity.Services.Leaderboards.Authoring.Core.csproj", "{C1E40F5F-F47D-451F-B91D-75CB630402CC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configuration", "Configuration", "{ECED14B5-7FD3-4C82-8417-3C6E4A0FC054}" + ProjectSection(SolutionItems) = preProject + features-definition.json = features-definition.json + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -183,5 +190,9 @@ Global {27BBE31C-43CE-45AA-9DE4-BF3D097DC1A3}.Debug|Any CPU.Build.0 = Debug|Any CPU {27BBE31C-43CE-45AA-9DE4-BF3D097DC1A3}.Release|Any CPU.ActiveCfg = Release|Any CPU {27BBE31C-43CE-45AA-9DE4-BF3D097DC1A3}.Release|Any CPU.Build.0 = Release|Any CPU + {C1E40F5F-F47D-451F-B91D-75CB630402CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C1E40F5F-F47D-451F-B91D-75CB630402CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C1E40F5F-F47D-451F-B91D-75CB630402CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C1E40F5F-F47D-451F-B91D-75CB630402CC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Unity.Services.Cli/Unity.Services.Cli/Program.cs b/Unity.Services.Cli/Unity.Services.Cli/Program.cs index aeb0571..22af2d7 100644 --- a/Unity.Services.Cli/Unity.Services.Cli/Program.cs +++ b/Unity.Services.Cli/Unity.Services.Cli/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.CommandLine; using System.CommandLine.Builder; @@ -7,6 +8,7 @@ using System.CommandLine.IO; using System.CommandLine.Parsing; using System.IO; +using System.IO.Abstractions; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -34,6 +36,7 @@ #endif using Unity.Services.Cli.Player; using Unity.Services.Cli.Access; +using Unity.Services.Cli.Authoring.Input; namespace Unity.Services.Cli; @@ -51,6 +54,7 @@ public static async Task InternalMain(string[] args, Logger logger) TelemetrySender telemetrySender = null; SystemEnvironmentProvider systemEnvironmentProvider = new SystemEnvironmentProvider(); IAnalyticEventFactory analyticEventFactory = new AnalyticEventFactory(systemEnvironmentProvider); + IAnalyticsEventBuilder analyticsEventBuilder = new AnalyticsEventBuilder(analyticEventFactory, new FileSystem()); var parser = BuildCommandLine() .UseHost( @@ -74,12 +78,13 @@ public static async Task InternalMain(string[] args, Logger logger) host.ConfigureServices(AccessModule.RegisterServices); host.ConfigureServices(GameServerHostingModule.RegisterServices); host.ConfigureServices(LobbyModule.RegisterServices); -#if FEATURE_LEADERBOARDS host.ConfigureServices(LeaderboardsModule.RegisterServices); -#endif host.ConfigureServices(PlayerModule.RegisterServices); host.ConfigureServices(serviceCollection => serviceCollection .AddSingleton(systemEnvironmentProvider)); + + host.ConfigureServices(serviceCollection => serviceCollection + .AddSingleton(analyticsEventBuilder)); }) .UseVersionOption() .UseHelp( @@ -149,7 +154,7 @@ public static async Task InternalMain(string[] args, Logger logger) commandTask => { logger.Write(); - TrySendCommandUsageMetric(analyticEventFactory, parser.Parse(args)); + TrySendCommandUsageMetric(analyticsEventBuilder, parser.Parse(args)); return commandTask.Result; }); } @@ -181,19 +186,22 @@ HelpSectionDelegate WriteSubcommands() => helpContext }; } - static void TrySendCommandUsageMetric(IAnalyticEventFactory analyticEventFactory, ParseResult parseResult) + static void TrySendCommandUsageMetric(IAnalyticsEventBuilder analyticsEventBuilder, ParseResult parseResult) { + var command = AnalyticEventUtils.ConvertSymbolResultToString(parseResult.CommandResult); + var options = parseResult.Tokens .Where(t => t.Type == TokenType.Option) .Select(t => t.ToString()) .ToArray(); - var command = AnalyticEventUtils.ConvertSymbolResultToString(parseResult.CommandResult); + analyticsEventBuilder.SetCommandName(command); + + foreach (var option in options) + { + analyticsEventBuilder.AddCommandOption(option); + } - var analyticEvent = analyticEventFactory.CreateMetricEvent(); - analyticEvent.AddData("command", command); - analyticEvent.AddData("options", options); - analyticEvent.AddData("time", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); - analyticEvent.Send(); + analyticsEventBuilder.SendCommandCompletedEvent(); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli/Unity.Services.Cli.csproj b/Unity.Services.Cli/Unity.Services.Cli/Unity.Services.Cli.csproj index 4ebdfda..b102b91 100644 --- a/Unity.Services.Cli/Unity.Services.Cli/Unity.Services.Cli.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli/Unity.Services.Cli.csproj @@ -5,7 +5,7 @@ 10 ugs 1.0.0 - beta.6 + true true true @@ -40,7 +40,7 @@ full - TRACE;FEATURE_LEADERBOARDS;FEATURE_LEADERBOARDS_IMPORT_EXPORT; + TRACE;FEATURE_LEADERBOARDS;FEATURE_LEADERBOARDS_DEPLOY;FEATURE_LEADERBOARDS_IMPORT_EXPORT; $(DefineConstants);$(ExtraDefineConstants) diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Batching/Batching.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Batching/Batching.cs new file mode 100644 index 0000000..cf0a769 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Batching/Batching.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Unity.Services.Leaderboards.Authoring.Core.Batching +{ + /// An utility class for executing delegates in batches with a time interval between them + /// Currently only supports async delegates (with or without return values) + public static class Batching + { + const int k_BatchSize = 10; + const double k_SecondsDelay = 1; + + const string k_BatchingExceptionMessage = + "One or more exceptions were thrown during the batching execution. See inner exceptions."; + + public static async Task ExecuteInBatchesAsync( + IEnumerable tasks, + CancellationToken cancellationToken, + int batchSize = k_BatchSize, + double secondsDelay = k_SecondsDelay) + { + var exceptions = new List(); + var iterator = tasks.GetEnumerator(); + + while (true) + { + var chunk = new List(); + for (int i = 0; i < batchSize; ++i) + { + if (!iterator.MoveNext()) + break; + chunk.Add(iterator.Current); + } + + if (chunk.Count == 0) + break; + + var innerExceptions = await ExecuteBatchAsync(chunk); + exceptions.AddRange(innerExceptions); + + if (cancellationToken.IsCancellationRequested) + break; + + await Task.Delay(TimeSpan.FromSeconds(secondsDelay), cancellationToken); + + if (cancellationToken.IsCancellationRequested) + break; + } + + iterator.Dispose(); + + if (exceptions.Count != 0) + { + throw new AggregateException(k_BatchingExceptionMessage, exceptions.ToList()); + } + } + + static async Task> ExecuteBatchAsync(IEnumerable insideTasks) + { + var tasks = new ConcurrentBag(); + var exceptions = new ConcurrentQueue(); + + Parallel.ForEach( + insideTasks, + async del => + { + tasks.Add(del); + try + { + await del; + } + catch (Exception e) + { + exceptions.Enqueue(e); + } + }); + + await Task.WhenAll(tasks); + + return exceptions.ToList(); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/DeployResult.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/DeployResult.cs new file mode 100644 index 0000000..831b5ef --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/DeployResult.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Unity.Services.Leaderboards.Authoring.Core.Model; + +namespace Unity.Services.Leaderboards.Authoring.Core.Deploy +{ + public class DeployResult + { + public List Created { get; set; } + public List Updated { get; set; } + public List Deleted { get; set; } + public List Deployed { get; set; } + public List Failed { get; set; } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/ILeaderboardsDeploymentHandler.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/ILeaderboardsDeploymentHandler.cs new file mode 100644 index 0000000..fa5b058 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/ILeaderboardsDeploymentHandler.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Unity.Services.Leaderboards.Authoring.Core.Model; + +namespace Unity.Services.Leaderboards.Authoring.Core.Deploy +{ + public interface ILeaderboardsDeploymentHandler + { + Task DeployAsync(IReadOnlyList localResources, + bool dryRun = false, + bool reconcile = false, + CancellationToken token = default); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/LeaderboardsDeploymentHandler.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/LeaderboardsDeploymentHandler.cs new file mode 100644 index 0000000..25dd640 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/LeaderboardsDeploymentHandler.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Unity.Services.DeploymentApi.Editor; +using Unity.Services.Leaderboards.Authoring.Core.Model; +using Unity.Services.Leaderboards.Authoring.Core.Service; +using Unity.Services.Leaderboards.Authoring.Core.Validations; + +namespace Unity.Services.Leaderboards.Authoring.Core.Deploy +{ + public class LeaderboardsDeploymentHandler : ILeaderboardsDeploymentHandler + { + readonly ILeaderboardsClient m_Client; + readonly object m_ResultLock = new(); + + public LeaderboardsDeploymentHandler(ILeaderboardsClient client) + { + m_Client = client; + } + + public async Task DeployAsync( + IReadOnlyList localResources, + bool dryRun = false, + bool reconcile = false, + CancellationToken token = default) + { + var res = new DeployResult(); + + localResources = DuplicateResourceValidation.FilterDuplicateResources( + localResources, out var duplicateGroups); + + var remoteResources = await m_Client.List(token); + + var toCreate = localResources + .Except(remoteResources, new LeaderboardComparer()) + .ToList(); + + var toUpdate = localResources + .Except(toCreate, new LeaderboardComparer()) + .ToList(); + + var toDelete = new List(); + if (reconcile) + { + toDelete = remoteResources + .Except(localResources, new LeaderboardComparer()) + .ToList(); + } + + res.Created = toCreate; + res.Deleted = toDelete; + res.Updated = toUpdate; + res.Deployed = new List(); + res.Failed = new List(); + + UpdateDuplicateResourceStatus(res, duplicateGroups); + + if (dryRun) + { + return res; + } + + var createTasks = GetTasks(toCreate, m_Client.Create, res, token); + var updateTasks = GetTasks(toUpdate, m_Client.Update, res, token); + var deleteTasks = reconcile + ? GetTasks(toDelete, m_Client.Delete, res, token) + : new List(); + + var allTasks = createTasks.Concat(updateTasks).Concat(deleteTasks); + + await Batching.Batching.ExecuteInBatchesAsync(allTasks, token); + + return res; + } + + IEnumerable GetTasks( + List resources, + Func func, + DeployResult res, + CancellationToken token) + { + return resources.Select(i => DeployResource(func, i, res, token)); + } + + protected virtual void UpdateStatus( + ILeaderboardConfig leaderboardConfig, + DeploymentStatus status) + { + // clients can override this to provide user feedback on progress + leaderboardConfig.Status = status; + } + + protected virtual void UpdateProgress( + ILeaderboardConfig leaderboardConfig, + float progress) + { + // clients can override this to provide user feedback on progress + leaderboardConfig.Progress = progress; + } + + void UpdateDuplicateResourceStatus( + DeployResult result, + IReadOnlyList> duplicateGroups) + { + foreach (var group in duplicateGroups) + { + foreach (var res in group) + { + result.Failed.Add(res); + var (message, shortMessage) = DuplicateResourceValidation.GetDuplicateResourceErrorMessages(res, group.ToList()); + UpdateStatus(res, Statuses.GetFailedToDeploy(shortMessage)); + } + } + } + + async Task DeployResource( + Func task, + ILeaderboardConfig leaderboardConfig, + DeployResult res, + CancellationToken token) + { + try + { + await task(leaderboardConfig, token); + lock (m_ResultLock) + res.Deployed.Add(leaderboardConfig); + UpdateStatus(leaderboardConfig, Statuses.Deployed); + UpdateProgress(leaderboardConfig, 100); + } + catch (Exception e) + { + lock (m_ResultLock) + res.Failed.Add(leaderboardConfig); + UpdateStatus(leaderboardConfig, Statuses.GetFailedToDeploy(e.Message)); + } + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/FetchResult.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/FetchResult.cs new file mode 100644 index 0000000..74ef115 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/FetchResult.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Unity.Services.Leaderboards.Authoring.Core.Model; + +namespace Unity.Services.Leaderboards.Authoring.Core.Fetch +{ + public class FetchResult + { + public List Created { get; set; } + public List Updated { get; set; } + public List Deleted { get; set; } + public List Fetched { get; set; } + public List Failed { get; set; } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/ILeaderboardsFetchHandler.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/ILeaderboardsFetchHandler.cs new file mode 100644 index 0000000..12f0d58 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/ILeaderboardsFetchHandler.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Unity.Services.Leaderboards.Authoring.Core.Model; + +namespace Unity.Services.Leaderboards.Authoring.Core.Fetch +{ + public interface ILeaderboardsFetchHandler + { + public Task FetchAsync( + string rootDirectory, + IReadOnlyList localResources, + bool dryRun = false, + bool reconcile = false, + CancellationToken token = default); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/LeaderboardsFetchHandler.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/LeaderboardsFetchHandler.cs new file mode 100644 index 0000000..0d94cad --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/LeaderboardsFetchHandler.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Unity.Services.DeploymentApi.Editor; +using Unity.Services.Leaderboards.Authoring.Core.Deploy; +using Unity.Services.Leaderboards.Authoring.Core.IO; +using Unity.Services.Leaderboards.Authoring.Core.Model; +using Unity.Services.Leaderboards.Authoring.Core.Serialization; +using Unity.Services.Leaderboards.Authoring.Core.Service; +using Unity.Services.Leaderboards.Authoring.Core.Validations; + +namespace Unity.Services.Leaderboards.Authoring.Core.Fetch +{ + public class LeaderboardsFetchHandler : ILeaderboardsFetchHandler + { + readonly ILeaderboardsClient m_Client; + readonly IFileSystem m_FileSystem; + readonly ILeaderboardsSerializer m_LeaderboardsSerializer; + + public LeaderboardsFetchHandler( + ILeaderboardsClient client, + IFileSystem fileSystem, + ILeaderboardsSerializer leaderboardsSerializer) + { + m_Client = client; + m_FileSystem = fileSystem; + m_LeaderboardsSerializer = leaderboardsSerializer; + } + + public async Task FetchAsync( + string rootDirectory, + IReadOnlyList localResources, + bool dryRun = false, + bool reconcile = false, + CancellationToken token = default) + { + var res = new FetchResult(); + + localResources = DuplicateResourceValidation.FilterDuplicateResources( + localResources, out var duplicateGroups); + + var remoteResources = await m_Client.List(token); + + var toUpdate = localResources + .Intersect(remoteResources, new LeaderboardComparer()) + .ToList(); + + var toDelete = localResources + .Except(remoteResources, new LeaderboardComparer()) + .ToList(); + + var toCreate = new List(); + if (reconcile) + { + toCreate = remoteResources + .Except(localResources, new LeaderboardComparer()) + .ToList(); + toCreate.ForEach(r => ((LeaderboardConfig)r).Path = Path.Combine(rootDirectory, r.Id) + ".lb" ); + } + + res.Created = toCreate; + res.Deleted = toDelete; + res.Updated = toUpdate; + res.Fetched = new List(); + res.Failed = new List(); + + UpdateDuplicateResourceStatus(res, duplicateGroups); + + if (dryRun) + { + return res; + } + + var updateTasks = new List<(ILeaderboardConfig, Task)>(); + var deleteTasks = new List<(ILeaderboardConfig, Task)>(); + var createTasks = new List<(ILeaderboardConfig, Task)>(); + + foreach (var resource in toUpdate) + { + var task = m_FileSystem.WriteAllText( + resource.Path, + m_LeaderboardsSerializer.Serialize(resource), + token); + updateTasks.Add((resource, task)); + } + + foreach (var resource in toDelete) + { + var task = m_FileSystem.Delete( + resource.Path, + token); + deleteTasks.Add((resource, task)); + } + + if (reconcile) + { + foreach (var resource in toCreate) + { + var task = m_FileSystem.WriteAllText( + resource.Path, + m_LeaderboardsSerializer.Serialize(resource), + token); + createTasks.Add((resource, task)); + } + } + + await UpdateResult(updateTasks, res); + await UpdateResult(deleteTasks, res); + await UpdateResult(createTasks, res); + + return res; + } + + protected virtual void UpdateStatus( + ILeaderboardConfig leaderboardConfig, + DeploymentStatus status) + { + // clients can override this to provide user feedback on progress + leaderboardConfig.Status = status; + } + + protected virtual void UpdateProgress( + ILeaderboardConfig leaderboardConfig, + float progress) + { + // clients can override this to provide user feedback on progress + leaderboardConfig.Progress = progress; + } + + void UpdateDuplicateResourceStatus( + FetchResult result, + IReadOnlyList> duplicateGroups) + { + foreach (var group in duplicateGroups) + { + foreach (var res in group) + { + result.Failed.Add(res); + var (message, shortMessage) = DuplicateResourceValidation.GetDuplicateResourceErrorMessages(res, group.ToList()); + UpdateStatus(res, Statuses.GetFailedToFetch(shortMessage)); + } + } + } + + async Task UpdateResult( + List<(ILeaderboardConfig, Task)> tasks, + FetchResult res) + { + foreach (var (resource, task) in tasks) + { + try + { + await task; + res.Fetched.Add(resource); + UpdateStatus(resource, Statuses.Fetched); + UpdateProgress(resource, 100); + } + catch (Exception e) + { + res.Failed.Add(resource); + UpdateStatus(resource, Statuses.GetFailedToFetch(e.Message)); + } + } + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/IO/IFileSystem.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/IO/IFileSystem.cs new file mode 100644 index 0000000..2418661 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/IO/IFileSystem.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Unity.Services.Leaderboards.Authoring.Core.IO +{ + public interface IFileSystem + { + Task ReadAllText( + string path, + CancellationToken token = default(CancellationToken)); + + Task WriteAllText( + string path, + string contents, + CancellationToken token = default(CancellationToken)); + + Task Delete(string path, CancellationToken token = default(CancellationToken)); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/ILeaderboardConfig.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/ILeaderboardConfig.cs new file mode 100644 index 0000000..50a2f76 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/ILeaderboardConfig.cs @@ -0,0 +1,17 @@ +using System; +using Unity.Services.DeploymentApi.Editor; + +namespace Unity.Services.Leaderboards.Authoring.Core.Model +{ + public interface ILeaderboardConfig : IDeploymentItem, ITypedItem + { + string Id { get; } + new float Progress { get; set; } + + SortOrder SortOrder { get; set; } + UpdateType UpdateType { get; set; } + Decimal BucketSize { get; set; } + ResetConfig ResetConfig { get; set; } + TieringConfig TieringConfig { get; set; } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/LeaderboardComparer.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/LeaderboardComparer.cs new file mode 100644 index 0000000..f8fc433 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/LeaderboardComparer.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace Unity.Services.Leaderboards.Authoring.Core.Model +{ + public class LeaderboardComparer : IEqualityComparer + { + public bool Equals(ILeaderboardConfig x, ILeaderboardConfig y) + { + if (ReferenceEquals(x, y)) return true; + if (ReferenceEquals(x, null)) return false; + if (ReferenceEquals(y, null)) return false; + if (x.GetType() != y.GetType()) return false; + return x.Id == y.Id; + } + + public int GetHashCode(ILeaderboardConfig obj) + { + return (obj.Id != null ? obj.Id.GetHashCode() : 0); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/LeaderboardConfig.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/LeaderboardConfig.cs new file mode 100644 index 0000000..ce877d2 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/LeaderboardConfig.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Unity.Services.DeploymentApi.Editor; + +namespace Unity.Services.Leaderboards.Authoring.Core.Model +{ + [Serializable] + public class LeaderboardConfig : ILeaderboardConfig + { + float m_Progress; + DeploymentStatus m_Status; + internal const string ConfigType = "Leaderboard"; + + public LeaderboardConfig() : this( + "myLeaderboard", + "My Leaderboard") + { + } + + public LeaderboardConfig( + string id, + string name, + SortOrder sortOrder = SortOrder.Asc, + UpdateType updateType = UpdateType.KeepBest) + { + Id = id; + Name = name; + Path = string.Empty; + States = new ObservableCollection(); + SortOrder = sortOrder; + UpdateType = updateType; + } + + public SortOrder SortOrder { get; set; } + public UpdateType UpdateType { get; set; } + public string Id { get; } + public string Name { get; set; } + public string Path { get; set; } + public string Type => ConfigType; + + /// + public float Progress + { + get => m_Progress; + set => SetField(ref m_Progress, value); + } + + public DeploymentStatus Status + { + get => m_Status; + set => SetField(ref m_Status, value); + } + + public ObservableCollection States { get; } + public decimal BucketSize { get; set; } + public ResetConfig ResetConfig { get; set; } + public TieringConfig TieringConfig { get; set; } + + public override string ToString() + { + if (Path == "Remote") + return Id; + return $"'{Path}'"; + } + + /// + /// Event will be raised when a property of the instance is changed + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// Sets the field and raises an OnPropertyChanged event. + /// + /// The field to set. + /// The value to set. + /// The callback. + /// Name of the property to set. + /// Type of the parameter. + protected void SetField( + ref T field, + T value, + Action onFieldChanged = null, + [CallerMemberName] string propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) + return; + field = value; + OnPropertyChanged(propertyName!); + onFieldChanged?.Invoke(field); + } + + void OnPropertyChanged([CallerMemberName] string propertyName = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/ResetConfig.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/ResetConfig.cs new file mode 100644 index 0000000..d19dbda --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/ResetConfig.cs @@ -0,0 +1,12 @@ +using System; + +namespace Unity.Services.Leaderboards.Authoring.Core.Model +{ + [Serializable] + public class ResetConfig + { + public DateTime Start { get; set; } + public string Schedule { get; set; } + public bool Archive { get; set; } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/SortOrder.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/SortOrder.cs new file mode 100644 index 0000000..b01b796 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/SortOrder.cs @@ -0,0 +1,13 @@ +using System.Runtime.Serialization; + +namespace Unity.Services.Leaderboards.Authoring.Core.Model +{ + public enum SortOrder + { + [EnumMember(Value = "asc")] + Asc = 1, + + [EnumMember(Value = "desc")] + Desc = 2, + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Statuses.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Statuses.cs new file mode 100644 index 0000000..3c55af5 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Statuses.cs @@ -0,0 +1,20 @@ +using Unity.Services.DeploymentApi.Editor; + + +namespace Unity.Services.Leaderboards.Authoring.Core.Model +{ + static class Statuses + { + public static readonly DeploymentStatus FailedToLoad = new ("Failed to load", string.Empty, SeverityLevel.Error); + + public static DeploymentStatus GetFailedToFetch(string details) + => new ("Failed to fetch", details, SeverityLevel.Error); + public static readonly DeploymentStatus Fetching = new ("Fetching", string.Empty, SeverityLevel.Info); + public static readonly DeploymentStatus Fetched = new ("Fetched", string.Empty, SeverityLevel.Success); + + public static DeploymentStatus GetFailedToDeploy(string details) + => new ("Failed to deploy", details, SeverityLevel.Error); + public static readonly DeploymentStatus Deploying = new ( "Deploying", string.Empty, SeverityLevel.Info); + public static readonly DeploymentStatus Deployed = new ("Deployed", string.Empty, SeverityLevel.Success); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Strategy.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Strategy.cs new file mode 100644 index 0000000..1afc0d9 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Strategy.cs @@ -0,0 +1,16 @@ +using System.Runtime.Serialization; + +namespace Unity.Services.Leaderboards.Authoring.Core.Model +{ + public enum Strategy + { + [EnumMember(Value = "score")] + Score = 1, + + [EnumMember(Value = "rank")] + Rank = 2, + + [EnumMember(Value = "percent")] + Percent = 3, + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Tier.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Tier.cs new file mode 100644 index 0000000..8a445d7 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Tier.cs @@ -0,0 +1,8 @@ +namespace Unity.Services.Leaderboards.Authoring.Core.Model +{ + public class Tier + { + public string Id { get; set; } + public double? Cutoff { get; set; } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/TieringConfig.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/TieringConfig.cs new file mode 100644 index 0000000..f608d3c --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/TieringConfig.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace Unity.Services.Leaderboards.Authoring.Core.Model +{ + [Serializable] + public class TieringConfig + { + public Strategy Strategy { get; set; } + public List Tiers { get; set; } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/UpdateType.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/UpdateType.cs new file mode 100644 index 0000000..821c046 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/UpdateType.cs @@ -0,0 +1,16 @@ +using System.Runtime.Serialization; + +namespace Unity.Services.Leaderboards.Authoring.Core.Model +{ + public enum UpdateType + { + [EnumMember(Value = "keepBest")] + KeepBest = 1, + + [EnumMember(Value = "keepLatest")] + KeepLatest = 2, + + [EnumMember(Value = "aggregate")] + Aggregate = 3, + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Serialization/ILeaderboardSerializer.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Serialization/ILeaderboardSerializer.cs new file mode 100644 index 0000000..d8b5f17 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Serialization/ILeaderboardSerializer.cs @@ -0,0 +1,9 @@ +using Unity.Services.Leaderboards.Authoring.Core.Model; + +namespace Unity.Services.Leaderboards.Authoring.Core.Serialization +{ + public interface ILeaderboardsSerializer + { + string Serialize(ILeaderboardConfig config); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Service/ILeaderboardsClient.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Service/ILeaderboardsClient.cs new file mode 100644 index 0000000..60f6096 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Service/ILeaderboardsClient.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Unity.Services.Leaderboards.Authoring.Core.Model; + +namespace Unity.Services.Leaderboards.Authoring.Core.Service +{ + public interface ILeaderboardsClient + { + void Initialize(string environmentId, string projectId, CancellationToken cancellationToken); + + Task Get(string id, CancellationToken token); + Task Update(ILeaderboardConfig leaderboardConfig, CancellationToken token); + Task Create(ILeaderboardConfig leaderboardConfig, CancellationToken token); + Task Delete(ILeaderboardConfig leaderboardConfig, CancellationToken token); + Task> List(CancellationToken token); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Unity.Services.Leaderboards.Authoring.Core.csproj b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Unity.Services.Leaderboards.Authoring.Core.csproj new file mode 100644 index 0000000..746aef6 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Unity.Services.Leaderboards.Authoring.Core.csproj @@ -0,0 +1,24 @@ + + + net5.0 + disable + 0.0.1 + disable + Library + true + true + + 9 + + + + <_Parameter1>$(AssemblyName).UnitTest + + + <_Parameter1>DynamicProxyGenAssembly2 + + + + + + \ No newline at end of file diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Validations/DuplicateResourceValidation.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Validations/DuplicateResourceValidation.cs new file mode 100644 index 0000000..4b9af9a --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Validations/DuplicateResourceValidation.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Unity.Services.Leaderboards.Authoring.Core.Model; + +namespace Unity.Services.Leaderboards.Authoring.Core.Validations +{ + public static class DuplicateResourceValidation + { + public static IReadOnlyList FilterDuplicateResources( + IReadOnlyList resources, + out IReadOnlyList> duplicateGroups) + { + duplicateGroups = resources + .GroupBy(r => r.Id) + .Where(g => g.Count() > 1) + .ToList(); + + var hashset = new HashSet(duplicateGroups.Select(g => g.Key)); + + return resources + .Where(r => !hashset.Contains(r.Id)) + .ToList(); + } + + public static (string, string) GetDuplicateResourceErrorMessages( + ILeaderboardConfig targetLeaderboardConfig, + IReadOnlyList group) + { + var duplicates = group + .Except(new[] { targetLeaderboardConfig }) + .ToList(); + + var duplicatesStr = string.Join(", ", duplicates.Select(d => $"'{d.Path}'")); + var shortMessage = $"'{targetLeaderboardConfig.Path}' was found duplicated in other files: {duplicatesStr}"; + var message = $"Multiple resources with the same identifier '{targetLeaderboardConfig.Id}' were found. " + + "Only a single resource for a given identifier may be deployed/fetched at the same time. " + + "Give all resources unique identifiers or deploy/fetch them separately to proceed.\n" + + shortMessage; + return (shortMessage, message); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/ModuleTemplateDeploymentHandler.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/ModuleTemplateDeploymentHandler.cs index 59b9392..39237a9 100644 --- a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/ModuleTemplateDeploymentHandler.cs +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/ModuleTemplateDeploymentHandler.cs @@ -55,7 +55,7 @@ public async Task DeployAsync( res.Deployed = new List(); res.Failed = new List(); - UpdateDuplicateResourceStatus(res, duplicateGroups, dryRun); + UpdateDuplicateResourceStatus(res, duplicateGroups); if (dryRun) { @@ -97,8 +97,7 @@ protected virtual void UpdateProgress( void UpdateDuplicateResourceStatus( DeployResult result, - IReadOnlyList> duplicateGroups, - bool dryRun) + IReadOnlyList> duplicateGroups) { foreach (var group in duplicateGroups) { diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/ModuleTemplateFetchHandler.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/ModuleTemplateFetchHandler.cs index 2fa2c45..5398e72 100644 --- a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/ModuleTemplateFetchHandler.cs +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/ModuleTemplateFetchHandler.cs @@ -38,8 +38,8 @@ public async Task FetchAsync(string rootDirectory, var remoteResources = await m_Client.List(); - var toUpdate = remoteResources - .Intersect(localResources) + var toUpdate = localResources + .Intersect(remoteResources) .ToList(); var toDelete = localResources @@ -60,7 +60,7 @@ public async Task FetchAsync(string rootDirectory, res.Fetched = new List(); res.Failed = new List(); - UpdateDuplicateResourceStatus(res, duplicateGroups, dryRun); + UpdateDuplicateResourceStatus(res, duplicateGroups); if (dryRun) { @@ -125,8 +125,7 @@ protected virtual void UpdateProgress( void UpdateDuplicateResourceStatus( FetchResult result, - IReadOnlyList> duplicateGroups, - bool dryRun) + IReadOnlyList> duplicateGroups) { foreach (var group in duplicateGroups) {