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