From bb24aef0669ed4305e8556bbd87c4a0724e32a00 Mon Sep 17 00:00:00 2001 From: futrime <35801754+futrime@users.noreply.github.com> Date: Wed, 22 Jan 2025 09:04:07 +0800 Subject: [PATCH] refactor: split external interfaces to independent Lip.Context package --- Lip.Context/.gitattributes | 8 ++ Lip.Context/.gitignore | 37 ++++++++ Lip.Context/IContext.cs | 14 +++ Lip.Context/IDownloader.cs | 6 ++ Lip.Context/IGit.cs | 6 ++ {Lip => Lip.Context}/IUserInteraction.cs | 10 +-- Lip.Context/Lip.Context.csproj | 14 +++ Lip.Tests/LipConfigTests.cs | 107 ++++++++++------------- Lip.Tests/LipInitTests.cs | 50 ++++++----- Lip.Tests/LipListTests.cs | 74 +++++++--------- Lip.Tests/PackageSpecifierTests.cs | 7 +- Lip.Tests/PathManagerTests.cs | 41 ++++++--- Lip.Tests/RuntimeConfigTests.cs | 2 - Lip/Lip.Config.cs | 2 +- Lip/Lip.Init.cs | 24 +++-- Lip/Lip.List.cs | 12 ++- Lip/Lip.cs | 20 ++--- Lip/Lip.csproj | 7 +- Lip/PackageSpecifier.cs | 4 + Lip/PathManager.cs | 38 +++----- Lip/RuntimeConfig.cs | 3 - 21 files changed, 278 insertions(+), 208 deletions(-) create mode 100644 Lip.Context/.gitattributes create mode 100644 Lip.Context/.gitignore create mode 100644 Lip.Context/IContext.cs create mode 100644 Lip.Context/IDownloader.cs create mode 100644 Lip.Context/IGit.cs rename {Lip => Lip.Context}/IUserInteraction.cs (88%) create mode 100644 Lip.Context/Lip.Context.csproj diff --git a/Lip.Context/.gitattributes b/Lip.Context/.gitattributes new file mode 100644 index 0000000..f7c7529 --- /dev/null +++ b/Lip.Context/.gitattributes @@ -0,0 +1,8 @@ +# Auto detect text files and perform LF normalization +* text=auto + +*.cs text diff=csharp +*.cshtml text diff=html +*.csx text diff=csharp +*.sln text eol=crlf +*.csproj text eol=crlf diff --git a/Lip.Context/.gitignore b/Lip.Context/.gitignore new file mode 100644 index 0000000..a437a65 --- /dev/null +++ b/Lip.Context/.gitignore @@ -0,0 +1,37 @@ +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc +nupkg/ + +# Visual Studio Code +.vscode + +# Rider +.idea + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +msbuild.log +msbuild.err +msbuild.wrn + +# Visual Studio 2015 +.vs/ diff --git a/Lip.Context/IContext.cs b/Lip.Context/IContext.cs new file mode 100644 index 0000000..b5e77e4 --- /dev/null +++ b/Lip.Context/IContext.cs @@ -0,0 +1,14 @@ +using System.IO.Abstractions; +using Microsoft.Extensions.Logging; + +namespace Lip.Context; + +public interface IContext +{ + IDownloader Downloader { get; } + IFileSystem FileSystem { get; } + IGit? Git { get; } + ILogger Logger { get; } + string RuntimeIdentifier { get; } + IUserInteraction UserInteraction { get; } +} diff --git a/Lip.Context/IDownloader.cs b/Lip.Context/IDownloader.cs new file mode 100644 index 0000000..bedf3b6 --- /dev/null +++ b/Lip.Context/IDownloader.cs @@ -0,0 +1,6 @@ +namespace Lip.Context; + +public interface IDownloader +{ + Task DownloadFile(Uri url, string destinationPath); +} diff --git a/Lip.Context/IGit.cs b/Lip.Context/IGit.cs new file mode 100644 index 0000000..9241b3e --- /dev/null +++ b/Lip.Context/IGit.cs @@ -0,0 +1,6 @@ +namespace Lip.Context; + +public interface IGit +{ + Task Clone(string repository, string directory, int? depth = null); +} diff --git a/Lip/IUserInteraction.cs b/Lip.Context/IUserInteraction.cs similarity index 88% rename from Lip/IUserInteraction.cs rename to Lip.Context/IUserInteraction.cs index e54c83f..ace47e9 100644 --- a/Lip/IUserInteraction.cs +++ b/Lip.Context/IUserInteraction.cs @@ -1,4 +1,4 @@ -namespace Lip; +namespace Lip.Context; /// /// Represents a user interaction interface. @@ -16,22 +16,22 @@ public interface IUserInteraction /// Prompts user for text input. /// /// The prompt message - /// Optional default value /// User input as string Task PromptForInput(string format, params object[] args); /// /// Prompts user to select from multiple options. /// - /// The prompt message /// Available options + /// The prompt message /// Selected option Task PromptForSelection(IEnumerable options, string format, params object[] args); /// /// Shows progress for long-running operations. /// - /// Progress message + /// Progress ID /// Progress value (0.0-1.0) - Task UpdateProgress(float progress, string format, params object[] args); + /// Progress message + Task UpdateProgress(string id, float progress, string format, params object[] args); } diff --git a/Lip.Context/Lip.Context.csproj b/Lip.Context/Lip.Context.csproj new file mode 100644 index 0000000..82b5c18 --- /dev/null +++ b/Lip.Context/Lip.Context.csproj @@ -0,0 +1,14 @@ + + + + net9.0 + enable + enable + + + + + + + + diff --git a/Lip.Tests/LipConfigTests.cs b/Lip.Tests/LipConfigTests.cs index d6bedf6..8af151e 100644 --- a/Lip.Tests/LipConfigTests.cs +++ b/Lip.Tests/LipConfigTests.cs @@ -1,4 +1,5 @@ using System.IO.Abstractions.TestingHelpers; +using Lip.Context; using Microsoft.Extensions.Logging; using Moq; @@ -24,10 +25,10 @@ public async Task ConfigDelete_SingleItem_ResetsToDefault() { s_runtimeConfigPath, new MockFileData(initialRuntimeConfig.ToBytes()) }, }); - Mock logger = new(); - Mock userInteraction = new(); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); - Lip lip = new(initialRuntimeConfig, fileSystem, logger.Object, userInteraction.Object); + Lip lip = new(initialRuntimeConfig, context.Object); // Act. await lip.ConfigDelete(["color"], new Lip.ConfigDeleteArgs()); @@ -66,10 +67,10 @@ public async Task ConfigDelete_MultipleItems_ResetsToDefault() { s_runtimeConfigPath, new MockFileData(initialRuntimeConfig.ToBytes()) }, }); - Mock logger = new(); - Mock userInteraction = new(); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); - Lip lip = new(initialRuntimeConfig, fileSystem, logger.Object, userInteraction.Object); + Lip lip = new(initialRuntimeConfig, context.Object); // Act. await lip.ConfigDelete(["color", "git"], new Lip.ConfigDeleteArgs()); @@ -97,11 +98,10 @@ public async Task ConfigDelete_EmptyKeys_ThrowsArgumentException() { // Arrange. RuntimeConfig initialRuntimeConfig = new(); - MockFileSystem fileSystem = new(); - Mock logger = new(); - Mock userInteraction = new(); - Lip lip = new(initialRuntimeConfig, fileSystem, logger.Object, userInteraction.Object); + Mock context = new(); + + Lip lip = new(initialRuntimeConfig, context.Object); // Act & Assert. ArgumentException exception = await Assert.ThrowsAsync( @@ -114,11 +114,10 @@ public async Task ConfigDelete_UnknownKey_ThrowsArgumentException() { // Arrange. RuntimeConfig initialRuntimeConfig = new(); - MockFileSystem fileSystem = new(); - Mock logger = new(); - Mock userInteraction = new(); - Lip lip = new(initialRuntimeConfig, fileSystem, logger.Object, userInteraction.Object); + Mock context = new(); + + Lip lip = new(initialRuntimeConfig, context.Object); // Act & Assert. ArgumentException exception = await Assert.ThrowsAsync( @@ -131,11 +130,10 @@ public async Task ConfigDelete_PartialUnknownItem_ThrowsArgumentException() { // Arrange. RuntimeConfig initialRuntimeConfig = new(); - MockFileSystem fileSystem = new(); - Mock logger = new(); - Mock userInteraction = new(); - Lip lip = new(initialRuntimeConfig, fileSystem, logger.Object, userInteraction.Object); + Mock context = new(); + + Lip lip = new(initialRuntimeConfig, context.Object); // Act & Assert. ArgumentException argumentException = await Assert.ThrowsAsync( @@ -154,10 +152,10 @@ public void ConfigGet_SingleItem_Passes() { s_runtimeConfigPath, new MockFileData(initialRuntimeConfig.ToBytes()) }, }); - Mock logger = new(); - Mock userInteraction = new(); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); - Lip lip = new(initialRuntimeConfig, fileSystem, logger.Object, userInteraction.Object); + Lip lip = new(initialRuntimeConfig, context.Object); // Act. Dictionary result = lip.ConfigGet(["cache"], new Lip.ConfigGetArgs()); @@ -182,10 +180,10 @@ public void ConfigGet_MultipleItems_Passes() { s_runtimeConfigPath, new MockFileData(initialRuntimeConfig.ToBytes()) }, }); - Mock logger = new(); - Mock userInteraction = new(); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); - Lip lip = new(initialRuntimeConfig, fileSystem, logger.Object, userInteraction.Object); + Lip lip = new(initialRuntimeConfig, context.Object); // Act. Dictionary result = lip.ConfigGet( @@ -203,11 +201,10 @@ public void ConfigGet_UnknownKey_ThrowsArgumentException() { // Arrange. RuntimeConfig initialRuntimeConfig = new(); - MockFileSystem fileSystem = new(); - Mock logger = new(); - Mock userInteraction = new(); - Lip lip = new(initialRuntimeConfig, fileSystem, logger.Object, userInteraction.Object); + Mock context = new(); + + Lip lip = new(initialRuntimeConfig, context.Object); // Act & Assert. ArgumentException exception = Assert.Throws( @@ -220,11 +217,10 @@ public void ConfigGet_PartialUnknownItem_ThrowsArgumentException() { // Arrange. RuntimeConfig initialRuntimeConfig = new(); - MockFileSystem fileSystem = new(); - Mock logger = new(); - Mock userInteraction = new(); - Lip lip = new(initialRuntimeConfig, fileSystem, logger.Object, userInteraction.Object); + Mock context = new(); + + Lip lip = new(initialRuntimeConfig, context.Object); // Act & Assert. ArgumentException exception = Assert.Throws( @@ -237,11 +233,10 @@ public void ConfigGet_EmptyKeys_ThrowsArgumentException() { // Arrange. RuntimeConfig initialRuntimeConfig = new(); - MockFileSystem fileSystem = new(); - Mock logger = new(); - Mock userInteraction = new(); - Lip lip = new(initialRuntimeConfig, fileSystem, logger.Object, userInteraction.Object); + Mock context = new(); + + Lip lip = new(initialRuntimeConfig, context.Object); // Act & Assert. ArgumentException exception = Assert.Throws( @@ -266,11 +261,9 @@ public void ConfigList_ReturnsAllConfigurations() ScriptShell = "/custom/shell" }; - MockFileSystem fileSystem = new(); - Mock logger = new(); - Mock userInteraction = new(); + Mock context = new(); - Lip lip = new(initialRuntimeConfig, fileSystem, logger.Object, userInteraction.Object); + Lip lip = new(initialRuntimeConfig, context.Object); // Act. Dictionary result = lip.ConfigList(new Lip.ConfigGetArgs()); @@ -299,11 +292,10 @@ public async Task ConfigSet_SingleItem_Passes() { s_runtimeConfigPath, new MockFileData(initialRuntimeConfig.ToBytes()) }, }); - Mock logger = new(); - - Mock userInteraction = new(); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); - Lip lip = new(new(), fileSystem, logger.Object, userInteraction.Object); + Lip lip = new(initialRuntimeConfig, context.Object); Dictionary keyValuePairs = new() { @@ -342,11 +334,10 @@ public async Task ConfigSet_MultipleItems_Passes() { s_runtimeConfigPath, new MockFileData(initialRuntimeConfig.ToBytes()) }, }); - Mock logger = new(); - - Mock userInteraction = new(); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); - Lip lip = new(new(), fileSystem, logger.Object, userInteraction.Object); + Lip lip = new(initialRuntimeConfig, context.Object); Dictionary keyValuePairs = new() { @@ -393,11 +384,10 @@ public async Task ConfigSet_NoItems_ThrowsArgumentException() { s_runtimeConfigPath, new MockFileData(initialRuntimeConfig.ToBytes()) }, }); - Mock logger = new(); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); - Mock userInteraction = new(); - - Lip lip = new(new(), fileSystem, logger.Object, userInteraction.Object); + Lip lip = new(initialRuntimeConfig, context.Object); Dictionary keyValuePairs = []; @@ -419,11 +409,10 @@ public async Task ConfigSet_UnknownItem_ThrowsArgumentException() { s_runtimeConfigPath, new MockFileData(initialRuntimeConfig.ToBytes()) }, }); - Mock logger = new(); - - Mock userInteraction = new(); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); - Lip lip = new(new(), fileSystem, logger.Object, userInteraction.Object); + Lip lip = new(initialRuntimeConfig, context.Object); Dictionary keyValuePairs = new() { @@ -449,10 +438,10 @@ public async Task ConfigSet_PartialUnknownItem_ThrowsArgumentException() { s_runtimeConfigPath, new MockFileData(initialRuntimeConfig.ToBytes()) }, }); - Mock logger = new(); - Mock userInteraction = new(); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); - Lip lip = new(new(), fileSystem, logger.Object, userInteraction.Object); + Lip lip = new(initialRuntimeConfig, context.Object); Dictionary keyValuePairs = new() { diff --git a/Lip.Tests/LipInitTests.cs b/Lip.Tests/LipInitTests.cs index 8e3a117..3139e79 100644 --- a/Lip.Tests/LipInitTests.cs +++ b/Lip.Tests/LipInitTests.cs @@ -1,4 +1,5 @@ using System.IO.Abstractions.TestingHelpers; +using Lip.Context; using Microsoft.Extensions.Logging; using Moq; @@ -17,8 +18,6 @@ public async Task Init_Interactive_Passes() { s_workspacePath, new MockDirectoryData() }, }, currentDirectory: s_workspacePath); - Mock logger = new(); - Mock userInteraction = new(); userInteraction.Setup(u => u.PromptForInput( "Enter the tooth path (e.g. {DefaultTooth}):", @@ -37,12 +36,14 @@ public async Task Init_Interactive_Passes() userInteraction.Setup(u => u.Confirm("Do you want to create the following package manifest file?\n{jsonString}", It.IsAny()).Result) .Returns(true); - Lip lip = new(new(), fileSystem, logger.Object, userInteraction.Object); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); + context.SetupGet(c => c.UserInteraction).Returns(userInteraction.Object); - Lip.InitArgs args = new(); + Lip lip = new(new(), context.Object); // Act. - await lip.Init(args); + await lip.Init(new()); // Assert. Assert.True(fileSystem.File.Exists(Path.Join(s_workspacePath, "tooth.json"))); @@ -73,11 +74,10 @@ public async Task Init_WithDefaultValues_Passes() { s_workspacePath, new MockDirectoryData() }, }, currentDirectory: s_workspacePath); - Mock logger = new(); - - Mock userInteraction = new(); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); - Lip lip = new(new(), fileSystem, logger.Object, userInteraction.Object); + Lip lip = new(new(), context.Object); Lip.InitArgs args = new() { @@ -110,11 +110,10 @@ public async Task Init_WithInitialValues_Passes() { s_workspacePath, new MockDirectoryData() }, }, currentDirectory: s_workspacePath); - Mock logger = new(); - - Mock userInteraction = new(); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); - Lip lip = new(new(), fileSystem, logger.Object, userInteraction.Object); + Lip lip = new(new(), context.Object); Lip.InitArgs args = new() { @@ -158,13 +157,15 @@ public async Task Init_OperationCanceled_ThrowsOperationCanceledException() { s_workspacePath, new MockDirectoryData() }, }, currentDirectory: s_workspacePath); - Mock logger = new(); - Mock userInteraction = new(); userInteraction.Setup(u => u.Confirm("Do you want to create the following package manifest file?\n{jsonString}", It.IsAny()).Result) .Returns(false); - Lip lip = new(new(), fileSystem, logger.Object, userInteraction.Object); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); + context.SetupGet(c => c.UserInteraction).Returns(userInteraction.Object); + + Lip lip = new(new(), context.Object); Lip.InitArgs args = new() { @@ -190,13 +191,15 @@ public async Task Init_ManifestFileExists_ThrowsInvalidOperationException() { Path.Join(s_workspacePath, "tooth.json"), new MockFileData("content") }, }, currentDirectory: s_workspacePath); - Mock logger = new(); - Mock userInteraction = new(); userInteraction.Setup(u => u.Confirm("Do you want to create the following package manifest file?\n{jsonString}", It.IsAny()).Result) .Returns(false); - Lip lip = new(new(), fileSystem, logger.Object, userInteraction.Object); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); + context.SetupGet(c => c.UserInteraction).Returns(userInteraction.Object); + + Lip lip = new(new(), context.Object); Lip.InitArgs args = new() { @@ -222,13 +225,16 @@ public async Task Init_OverwritesManifestFile_Passes() { Path.Join(s_workspacePath, "tooth.json"), new MockFileData("content") }, }, currentDirectory: s_workspacePath); - Mock logger = new(); - Mock userInteraction = new(); userInteraction.Setup(u => u.Confirm("Do you want to create the following package manifest file?\n{jsonString}", It.IsAny()).Result) .Returns(false); - Lip lip = new(new(), fileSystem, logger.Object, userInteraction.Object); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); + context.SetupGet(c => c.Logger).Returns(new Mock().Object); + context.SetupGet(c => c.UserInteraction).Returns(userInteraction.Object); + + Lip lip = new(new(), context.Object); Lip.InitArgs args = new() { diff --git a/Lip.Tests/LipListTests.cs b/Lip.Tests/LipListTests.cs index 0fa6adb..7d48ce7 100644 --- a/Lip.Tests/LipListTests.cs +++ b/Lip.Tests/LipListTests.cs @@ -1,5 +1,6 @@ using System.IO.Abstractions.TestingHelpers; using System.Runtime.InteropServices; +using Lip.Context; using Microsoft.Extensions.Logging; using Moq; @@ -7,11 +8,12 @@ namespace Lip.Tests; public class LipListTests { - [Fact] - public async Task List_ReturnsListItems() + [Theory] + [InlineData("win-x64")] + [InlineData("linux-x64")] + [InlineData("osx-x64")] + public async Task List_ReturnsListItems(string runtimeIdentifier) { - RuntimeConfig initialRuntimeConfig = new(); - // Arrange. var fileSystem = new MockFileSystem(new Dictionary { @@ -28,7 +30,7 @@ public async Task List_ReturnsListItems() "variants": [ { "label": "variant1", - "platform": "{{RuntimeInformation.RuntimeIdentifier}}" + "platform": "{{runtimeIdentifier}}" } ] }, @@ -40,7 +42,7 @@ public async Task List_ReturnsListItems() "variants": [ { "label": "variant2", - "platform": "{{RuntimeInformation.RuntimeIdentifier}}" + "platform": "{{runtimeIdentifier}}" } ] } @@ -56,15 +58,11 @@ public async Task List_ReturnsListItems() """) } }); - Mock logger = new(); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); + context.SetupGet(c => c.RuntimeIdentifier).Returns(runtimeIdentifier); - Mock userInteraction = new(); - - Lip lip = new( - initialRuntimeConfig, - fileSystem, - logger.Object, - userInteraction.Object); + Lip lip = new(new(), context.Object); // Act. List listItems = await lip.List(new()); @@ -84,16 +82,13 @@ public async Task List_ReturnsListItems() [Fact] public async Task List_LockFileNotExists_ReturnsEmptyList() { - RuntimeConfig initialRuntimeConfig = new(); - // Arrange. var fileSystem = new MockFileSystem(); - Mock logger = new(); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); - Mock userInteraction = new(); - - Lip lip = new(initialRuntimeConfig, fileSystem, logger.Object, userInteraction.Object); + Lip lip = new(new(), context.Object); // Act. List listItems = await lip.List(new()); @@ -105,8 +100,6 @@ public async Task List_LockFileNotExists_ReturnsEmptyList() [Fact] public async Task List_MismatchedToothPath_ReturnsListItems() { - RuntimeConfig initialRuntimeConfig = new(); - // Arrange. var fileSystem = new MockFileSystem(new Dictionary { @@ -122,7 +115,8 @@ public async Task List_MismatchedToothPath_ReturnsListItems() "version": "1.0.0", "variants": [ { - "label": "variant1" + "label": "variant1", + "platform": "win-x64" } ] } @@ -138,11 +132,11 @@ public async Task List_MismatchedToothPath_ReturnsListItems() """) } }); - Mock logger = new(); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); + context.SetupGet(c => c.RuntimeIdentifier).Returns("win-x64"); - Mock userInteraction = new(); - - Lip lip = new(initialRuntimeConfig, fileSystem, logger.Object, userInteraction.Object); + Lip lip = new(new(), context.Object); // Act. List listItems = await lip.List(new()); @@ -158,8 +152,6 @@ public async Task List_MismatchedToothPath_ReturnsListItems() [Fact] public async Task List_MismatchedVersion_ReturnsListItems() { - RuntimeConfig initialRuntimeConfig = new(); - // Arrange. var fileSystem = new MockFileSystem(new Dictionary { @@ -175,7 +167,8 @@ public async Task List_MismatchedVersion_ReturnsListItems() "version": "1.0.0", "variants": [ { - "label": "variant1" + "label": "variant1", + "platform": "win-x64" } ] } @@ -191,11 +184,11 @@ public async Task List_MismatchedVersion_ReturnsListItems() """) } }); - Mock logger = new(); - - Mock userInteraction = new(); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); + context.SetupGet(c => c.RuntimeIdentifier).Returns("win-x64"); - Lip lip = new(initialRuntimeConfig, fileSystem, logger.Object, userInteraction.Object); + Lip lip = new(new(), context.Object); // Act. List listItems = await lip.List(new()); @@ -211,8 +204,6 @@ public async Task List_MismatchedVersion_ReturnsListItems() [Fact] public async Task List_MismatchedVariantLabel_ReturnsListItems() { - RuntimeConfig initialRuntimeConfig = new(); - // Arrange. var fileSystem = new MockFileSystem(new Dictionary { @@ -228,7 +219,8 @@ public async Task List_MismatchedVariantLabel_ReturnsListItems() "version": "1.0.0", "variants": [ { - "label": "variant1" + "label": "variant1", + "platform": "win-x64" } ] } @@ -244,11 +236,11 @@ public async Task List_MismatchedVariantLabel_ReturnsListItems() """) } }); - Mock logger = new(); - - Mock userInteraction = new(); + Mock context = new(); + context.SetupGet(c => c.FileSystem).Returns(fileSystem); + context.SetupGet(c => c.RuntimeIdentifier).Returns("win-x64"); - Lip lip = new(initialRuntimeConfig, fileSystem, logger.Object, userInteraction.Object); + Lip lip = new(new(), context.Object); // Act. List listItems = await lip.List(new()); diff --git a/Lip.Tests/PackageSpecifierTests.cs b/Lip.Tests/PackageSpecifierTests.cs index d26d59f..ebee4ed 100644 --- a/Lip.Tests/PackageSpecifierTests.cs +++ b/Lip.Tests/PackageSpecifierTests.cs @@ -15,6 +15,7 @@ public void Constructor_ValidValues_Passes() }; // Assert + Assert.Equal("example.com/pkg#variant", packageSpecifier.Specifier); Assert.Equal("example.com/pkg", packageSpecifier.ToothPath); Assert.Equal("variant", packageSpecifier.VariantLabel); } @@ -47,12 +48,12 @@ public void Constructor_InvalidVariantLabel_Throws() public void Parse_ValidSpecifierText_Passes() { // Arrange & Act - var packageSpecifier = PackageSpecifier.Parse("example.com/pkg#variant@1.0.0"); + var packageSpecifier = PackageSpecifierWithoutVersion.Parse("example.com/pkg#variant"); // Assert + Assert.Equal("example.com/pkg#variant", packageSpecifier.Specifier); Assert.Equal("example.com/pkg", packageSpecifier.ToothPath); Assert.Equal("variant", packageSpecifier.VariantLabel); - Assert.Equal("1.0.0", packageSpecifier.Version.ToString()); } [Fact] @@ -78,6 +79,7 @@ public void Constructor_ValidValues_Passes() }; // Assert + Assert.Equal("example.com/pkg#variant@1.0.0", packageSpecifier.Specifier); Assert.Equal("example.com/pkg", packageSpecifier.ToothPath); Assert.Equal("variant", packageSpecifier.VariantLabel); Assert.Equal("1.0.0", packageSpecifier.Version.ToString()); @@ -90,6 +92,7 @@ public void Parse_ValidSpecifierText_Passes() var packageSpecifier = PackageSpecifier.Parse("example.com/pkg#variant@1.0.0"); // Assert + Assert.Equal("example.com/pkg#variant@1.0.0", packageSpecifier.Specifier); Assert.Equal("example.com/pkg", packageSpecifier.ToothPath); Assert.Equal("variant", packageSpecifier.VariantLabel); Assert.Equal("1.0.0", packageSpecifier.Version.ToString()); diff --git a/Lip.Tests/PathManagerTests.cs b/Lip.Tests/PathManagerTests.cs index 5f80eae..e7c788b 100644 --- a/Lip.Tests/PathManagerTests.cs +++ b/Lip.Tests/PathManagerTests.cs @@ -135,7 +135,7 @@ public void GetPackageManifestPath_WhenCalled_ReturnsCorrectPath() PathManager pathManager = new(fileSystem); // Act. - string manifestPath = pathManager.PackageManifestPath; + string manifestPath = pathManager.CurrentPackageManifestPath; // Assert. Assert.Equal(Path.Join(s_workingDir, "tooth.json"), manifestPath); @@ -152,7 +152,7 @@ public void GetPackageLockPath_WhenCalled_ReturnsCorrectPath() PathManager pathManager = new(fileSystem); // Act. - string recordPath = pathManager.PackageLockPath; + string recordPath = pathManager.CurrentPackageLockPath; // Assert. Assert.Equal(Path.Join(s_workingDir, "tooth_lock.json"), recordPath); @@ -193,12 +193,8 @@ public void GetWorkingDir_WhenCalled_ReturnsCurrentDirectory() [Theory] [InlineData("https://example.com/asset?v=1", "https%3A%2F%2Fexample.com%2Fasset%3Fv%3D1")] - [InlineData("/path/to/asset", "%2Fpath%2Fto%2Fasset")] - [InlineData("", "")] - [InlineData(" ", "%20")] - [InlineData("!@#$%^&*()", "%21%40%23%24%25%5E%26%2A%28%29")] - [InlineData("../path/test", "..%2Fpath%2Ftest")] - [InlineData("\\special\\chars", "%5Cspecial%5Cchars")] + [InlineData("https://example.com/path/to/asset", "https%3A%2F%2Fexample.com%2Fpath%2Fto%2Fasset")] + [InlineData("https://example.com/", "https%3A%2F%2Fexample.com%2F")] public void GetDownloadedFileCacheDir_ArbitraryString_ReturnsEscapedPath(string url, string expectedFileName) { // Arrange. @@ -206,7 +202,7 @@ public void GetDownloadedFileCacheDir_ArbitraryString_ReturnsEscapedPath(string PathManager pathManager = new(fileSystem, baseCacheDir: s_cacheDir); // Act. - string cachePath = pathManager.GetDownloadedFileCachePath(url); + string cachePath = pathManager.GetDownloadedFileCachePath(new Uri(url)); // Assert. Assert.Equal( @@ -237,6 +233,29 @@ public void GetGitRepoCachePath_ArbitraryString_ReturnsEscapedPath(string repoUr repoCacheDir); } + [Theory] + [InlineData("https://example.com/asset?v=1", "https%3A%2F%2Fexample.com%2Fasset%3Fv%3D1")] + [InlineData("/path/to/asset", "%2Fpath%2Fto%2Fasset")] + [InlineData("", "")] + [InlineData(" ", "%20")] + [InlineData("!@#$%^&*()", "%21%40%23%24%25%5E%26%2A%28%29")] + [InlineData("../path/test", "..%2Fpath%2Ftest")] + [InlineData("\\special\\chars", "%5Cspecial%5Cchars")] + public void GetGitRepoPackageManifestCachePath_ArbitraryString_ReturnsEscapedPath(string repoUrl, string expectedDirName) + { + // Arrange. + MockFileSystem fileSystem = new(); + PathManager pathManager = new(fileSystem, baseCacheDir: s_cacheDir); + + // Act. + string repoPackageManifestPath = pathManager.GetGitRepoPackageManifestCachePath(repoUrl); + + // Assert. + Assert.Equal( + Path.Join(s_cacheDir, "git_repos", expectedDirName, "tooth.json"), + repoPackageManifestPath); + } + [Theory] [InlineData("https://example.com/asset?v=1", "https%3A%2F%2Fexample.com%2Fasset%3Fv%3D1.json")] [InlineData("/path/to/asset", "%2Fpath%2Fto%2Fasset.json")] @@ -245,14 +264,14 @@ public void GetGitRepoCachePath_ArbitraryString_ReturnsEscapedPath(string repoUr [InlineData("!@#$%^&*()", "%21%40%23%24%25%5E%26%2A%28%29.json")] [InlineData("../path/test", "..%2Fpath%2Ftest.json")] [InlineData("\\special\\chars", "%5Cspecial%5Cchars.json")] - public void GetPackageManifestCachePath_ArbitraryString_ReturnsEscapedPath(string packageName, string expectedFileName) + public void GetPackageManifestCachePath_ArbitraryString_ReturnsEscapedPath(string toothPath, string expectedFileName) { // Arrange. MockFileSystem fileSystem = new(); PathManager pathManager = new(fileSystem, baseCacheDir: s_cacheDir); // Act. - string packageCacheDir = pathManager.GetPackageManifestCachePath(packageName); + string packageCacheDir = pathManager.GetPackageManifestCachePath(toothPath); // Assert. Assert.Equal( diff --git a/Lip.Tests/RuntimeConfigTests.cs b/Lip.Tests/RuntimeConfigTests.cs index a5704ae..27d32a8 100644 --- a/Lip.Tests/RuntimeConfigTests.cs +++ b/Lip.Tests/RuntimeConfigTests.cs @@ -27,7 +27,6 @@ public void FromBytes_MinimumJson_Passes() Assert.Equal("", runtimeConfiguration.HttpsProxy); Assert.Equal("", runtimeConfiguration.NoProxy); Assert.Equal("", runtimeConfiguration.Proxy); - Assert.Equal(RuntimeInformation.RuntimeIdentifier, runtimeConfiguration.RuntimeIdentifier); Assert.Equal( OperatingSystem.IsWindows() ? "cmd.exe" @@ -67,7 +66,6 @@ public void FromBytes_MaximumJson_Passes() Assert.Equal("https_proxy", runtimeConfiguration.HttpsProxy); Assert.Equal("noproxy", runtimeConfiguration.NoProxy); Assert.Equal("proxy", runtimeConfiguration.Proxy); - Assert.Equal(RuntimeInformation.RuntimeIdentifier, runtimeConfiguration.RuntimeIdentifier); Assert.Equal("script_shell", runtimeConfiguration.ScriptShell); } diff --git a/Lip/Lip.Config.cs b/Lip/Lip.Config.cs index eba53ca..dd3f51c 100644 --- a/Lip/Lip.Config.cs +++ b/Lip/Lip.Config.cs @@ -84,7 +84,7 @@ public async Task ConfigSet(Dictionary keyValuePairs, ConfigSetA matchedProperty.SetValue(newRuntimeConfig, convertedValue); } - await CreateOrUpdateRuntimeConfigurationFile(_fileSystem, newRuntimeConfig); + await CreateOrUpdateRuntimeConfigurationFile(_context.FileSystem, newRuntimeConfig); } private async Task CreateOrUpdateRuntimeConfigurationFile(IFileSystem fileSystem, RuntimeConfig runtimeConfig) diff --git a/Lip/Lip.Init.cs b/Lip/Lip.Init.cs index e7cb90d..fa39abc 100644 --- a/Lip/Lip.Init.cs +++ b/Lip/Lip.Init.cs @@ -43,12 +43,12 @@ public async Task Init(InitArgs args) } else { - string tooth = args.InitTooth ?? await _userInteraction.PromptForInput("Enter the tooth path (e.g. {DefaultTooth}):", DefaultTooth) ?? DefaultTooth; - string version = args.InitVersion ?? await _userInteraction.PromptForInput("Enter the package version (e.g. {DefaultVersion}):", DefaultVersion) ?? DefaultVersion; - string? name = args.InitName ?? await _userInteraction.PromptForInput("Enter the package name:"); - string? description = args.InitDescription ?? await _userInteraction.PromptForInput("Enter the package description:"); - string? author = args.InitAuthor ?? await _userInteraction.PromptForInput("Enter the package author:"); - string? avatarUrl = args.InitAvatarUrl ?? await _userInteraction.PromptForInput("Enter the author's avatar URL:"); + string tooth = args.InitTooth ?? await _context.UserInteraction.PromptForInput("Enter the tooth path (e.g. {DefaultTooth}):", DefaultTooth) ?? DefaultTooth; + string version = args.InitVersion ?? await _context.UserInteraction.PromptForInput("Enter the package version (e.g. {DefaultVersion}):", DefaultVersion) ?? DefaultVersion; + string? name = args.InitName ?? await _context.UserInteraction.PromptForInput("Enter the package name:"); + string? description = args.InitDescription ?? await _context.UserInteraction.PromptForInput("Enter the package description:"); + string? author = args.InitAuthor ?? await _context.UserInteraction.PromptForInput("Enter the package author:"); + string? avatarUrl = args.InitAvatarUrl ?? await _context.UserInteraction.PromptForInput("Enter the author's avatar URL:"); manifest = new() { @@ -66,28 +66,26 @@ public async Task Init(InitArgs args) }; string jsonString = Encoding.UTF8.GetString(manifest.ToJsonBytes()); - if (!await _userInteraction.Confirm("Do you want to create the following package manifest file?\n{jsonString}", jsonString)) + if (!await _context.UserInteraction.Confirm("Do you want to create the following package manifest file?\n{jsonString}", jsonString)) { throw new OperationCanceledException("Operation canceled by the user."); } } // Create the manifest file path. - string manifestPath = _pathManager.PackageManifestPath; + string manifestPath = _pathManager.CurrentPackageManifestPath; // Check if the manifest file already exists. - if (_fileSystem.File.Exists(manifestPath)) + if (_context.FileSystem.File.Exists(manifestPath)) { if (!args.Force) { throw new InvalidOperationException($"The file '{manifestPath}' already exists. Use the -f or --force option to overwrite it."); } - _logger.LogWarning("The file '{ManifestPath}' already exists. Overwriting it.", manifestPath); + _context.Logger.LogWarning("The file '{ManifestPath}' already exists. Overwriting it.", manifestPath); } - await _fileSystem.File.WriteAllBytesAsync(manifestPath, manifest.ToJsonBytes()); - - _logger.LogInformation("Successfully initialized the package manifest file '{ManifestPath}'.", manifestPath); + await _context.FileSystem.File.WriteAllBytesAsync(manifestPath, manifest.ToJsonBytes()); } } diff --git a/Lip/Lip.List.cs b/Lip/Lip.List.cs index 781acf5..d6387dc 100644 --- a/Lip/Lip.List.cs +++ b/Lip/Lip.List.cs @@ -1,6 +1,4 @@ -using System.Runtime.InteropServices; - -namespace Lip; +namespace Lip; public partial class Lip { @@ -26,7 +24,7 @@ public async Task> List(ListArgs args) && package.Version == l.Version && package.GetSpecifiedVariant( l.VariantLabel, - _runtimeConfig.RuntimeIdentifier) is not null; + _context.RuntimeIdentifier) is not null; }) })]; @@ -35,10 +33,10 @@ public async Task> List(ListArgs args) private async Task GetPackageLock() { - string packageLockFilePath = _pathManager.PackageLockPath; + string packageLockFilePath = _pathManager.CurrentPackageLockPath; // If the package lock file does not exist, return an empty package lock. - if (!_fileSystem.File.Exists(packageLockFilePath)) + if (!_context.FileSystem.File.Exists(packageLockFilePath)) { return new() { @@ -49,7 +47,7 @@ private async Task GetPackageLock() }; } - byte[] packageLockBytes = await _fileSystem.File.ReadAllBytesAsync(packageLockFilePath); + byte[] packageLockBytes = await _context.FileSystem.File.ReadAllBytesAsync(packageLockFilePath); return PackageLock.FromJsonBytes(packageLockBytes); } diff --git a/Lip/Lip.cs b/Lip/Lip.cs index 1c31386..9506b94 100644 --- a/Lip/Lip.cs +++ b/Lip/Lip.cs @@ -1,6 +1,4 @@ -using System.IO.Abstractions; -using System.Runtime.InteropServices; -using Microsoft.Extensions.Logging; +using Lip.Context; namespace Lip; @@ -8,18 +6,10 @@ namespace Lip; /// The main class of the Lip library. /// /// The runtime configuration. -/// The file system wrapper. -/// The logger. -/// The user interaction wrapper. -public partial class Lip( - RuntimeConfig runtimeConfig, - IFileSystem fileSystem, - ILogger logger, - IUserInteraction userInteraction) +/// The context. +public partial class Lip(RuntimeConfig runtimeConfig, IContext context) { - private readonly IFileSystem _fileSystem = fileSystem; - private readonly ILogger _logger = logger; - private readonly IPathManager _pathManager = new PathManager(fileSystem, runtimeConfig.Cache); + private readonly IContext _context = context; + private readonly PathManager _pathManager = new(context.FileSystem, runtimeConfig.Cache); private readonly RuntimeConfig _runtimeConfig = runtimeConfig; - private readonly IUserInteraction _userInteraction = userInteraction; } diff --git a/Lip/Lip.csproj b/Lip/Lip.csproj index c86adf8..cf18b7e 100644 --- a/Lip/Lip.csproj +++ b/Lip/Lip.csproj @@ -8,12 +8,13 @@ - - - + + + + diff --git a/Lip/PackageSpecifier.cs b/Lip/PackageSpecifier.cs index 4f0d064..a5c5387 100644 --- a/Lip/PackageSpecifier.cs +++ b/Lip/PackageSpecifier.cs @@ -4,6 +4,8 @@ namespace Lip; public record PackageSpecifierWithoutVersion { + public string Specifier => $"{ToothPath}#{VariantLabel}"; + public required string ToothPath { get => _tooth; @@ -17,6 +19,7 @@ public required string ToothPath _tooth = value; } } + public required string VariantLabel { get => _variantLabel; @@ -53,6 +56,7 @@ public static PackageSpecifierWithoutVersion Parse(string specifierText) public record PackageSpecifier : PackageSpecifierWithoutVersion { + public new string Specifier => $"{base.Specifier}@{Version}"; public required SemVersion Version { get; init; } diff --git a/Lip/PathManager.cs b/Lip/PathManager.cs index b75f812..e711531 100644 --- a/Lip/PathManager.cs +++ b/Lip/PathManager.cs @@ -2,23 +2,7 @@ namespace Lip; -public interface IPathManager -{ - string BaseDownloadedFileCacheDir { get; } - string BaseCacheDir { get; } - string BaseGitRepoCacheDir { get; } - string BasePackageManifestCacheDir { get; } - string PackageManifestPath { get; } - string PackageLockPath { get; } - string RuntimeConfigPath { get; } - string WorkingDir { get; } - - string GetDownloadedFileCachePath(string url); - string GetGitRepoCachePath(string repoUrl); - string GetPackageManifestCachePath(string packageName); -} - -public class PathManager(IFileSystem fileSystem, string? baseCacheDir = null) : IPathManager +public class PathManager(IFileSystem fileSystem, string? baseCacheDir = null) { private const string DownloadedFileCacheDirName = "downloaded_files"; private const string GitRepoCacheDirName = "git_repos"; @@ -37,19 +21,19 @@ public class PathManager(IFileSystem fileSystem, string? baseCacheDir = null) : public string BasePackageManifestCacheDir => _fileSystem.Path.Join(BaseCacheDir, PackageManifestCacheDirName); - public string PackageManifestPath => _fileSystem.Path.Join(WorkingDir, PackageManifestFileName); + public string CurrentPackageManifestPath => _fileSystem.Path.Join(WorkingDir, PackageManifestFileName); - public string PackageLockPath => _fileSystem.Path.Join(WorkingDir, PackageLockFileName); + public string CurrentPackageLockPath => _fileSystem.Path.Join(WorkingDir, PackageLockFileName); public string RuntimeConfigPath => _fileSystem.Path.Join( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "lip", "liprc.json"); public string WorkingDir => _fileSystem.Directory.GetCurrentDirectory(); - public string GetDownloadedFileCachePath(string url) + public string GetDownloadedFileCachePath(Uri url) { - string assetDirName = Uri.EscapeDataString(url); - return _fileSystem.Path.Join(BaseDownloadedFileCacheDir, assetDirName); + string downloadedFileName = Uri.EscapeDataString(url.AbsoluteUri); + return _fileSystem.Path.Join(BaseDownloadedFileCacheDir, downloadedFileName); } public string GetGitRepoCachePath(string repoUrl) @@ -58,9 +42,15 @@ public string GetGitRepoCachePath(string repoUrl) return _fileSystem.Path.Join(BaseGitRepoCacheDir, repoDirName); } + public string GetGitRepoPackageManifestCachePath(string repoUrl) + { + string repoDir = GetGitRepoCachePath(repoUrl); + return _fileSystem.Path.Join(repoDir, PackageManifestFileName); + } + public string GetPackageManifestCachePath(string packageName) { - string packageDirName = Uri.EscapeDataString(packageName) + ".json"; - return _fileSystem.Path.Join(BasePackageManifestCacheDir, packageDirName); + string packageManifestFileName = Uri.EscapeDataString(packageName) + ".json"; + return _fileSystem.Path.Join(BasePackageManifestCacheDir, packageManifestFileName); } } diff --git a/Lip/RuntimeConfig.cs b/Lip/RuntimeConfig.cs index a5c1a49..8bcba63 100644 --- a/Lip/RuntimeConfig.cs +++ b/Lip/RuntimeConfig.cs @@ -43,9 +43,6 @@ public record RuntimeConfig [JsonPropertyName("proxy")] public string Proxy { get; init; } = ""; - [JsonIgnore] - public string RuntimeIdentifier { get; init; } = RuntimeInformation.RuntimeIdentifier; - [JsonPropertyName("script_shell")] public string ScriptShell { get; init; } = OperatingSystem.IsWindows() ? "cmd.exe"