diff --git a/.gitattributes b/.gitattributes index dfe0770..3ff6d1e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,2 @@ -# Auto detect text files and perform LF normalization * text=auto +src/TestRepository/** linguist-vendored diff --git a/.github/actions/install-tokei/action.yml b/.github/actions/install-tokei/action.yml new file mode 100644 index 0000000..61dbc83 --- /dev/null +++ b/.github/actions/install-tokei/action.yml @@ -0,0 +1,19 @@ +name: 'Install Tokei' +description: 'Install Tokei on Windows, Linux and Mac using Cargo' + +runs: + using: 'composite' + steps: + - name: Install Tokei via Cargo + shell: bash + run: | + if ! command -v tokei &> /dev/null; then + echo "Tokei not found, installing..." + cargo install tokei + else + echo "Tokei is already installed." + fi + + - name: Verify Installation + shell: bash + run: tokei --version diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9a9efc6..af87a57 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,9 @@ permissions: attestations: write jobs: + test: + uses: ./.github/workflows/test.yml + build: runs-on: ubuntu-latest env: @@ -25,7 +28,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '10.0.102' + dotnet-version: '10.0.x' - name: Build vv.build and create release artifacts run: dotnet run --project ${{ env.BUILD_PROJECT }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..71ce25f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,37 @@ +name: Test + +on: + push: + branches: + - 'main' + pull_request: + branches: + - '**' + workflow_dispatch: + workflow_call: + + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Tokei + uses: ./.github/actions/install-tokei + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Restore dependencies + run: dotnet restore src/vv.sln + + - name: Test + run: dotnet test src/vv.sln --no-restore \ No newline at end of file diff --git a/LICENSE b/LICENSE.md similarity index 100% rename from LICENSE rename to LICENSE.md diff --git a/README.md b/README.md index 8c67411..7dd193d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Test](https://github.com/HardCodeDev777/vv/actions/workflows/test.yml/badge.svg)](https://github.com/HardCodeDev777/vv/actions/workflows/test.yml) [![Release](https://github.com/HardCodeDev777/vv/actions/workflows/release.yml/badge.svg)](https://github.com/HardCodeDev777/vv/actions/workflows/release.yml) # vv diff --git a/src/TestRepository/Program.cs b/src/TestRepository/Program.cs new file mode 100644 index 0000000..6bce29f --- /dev/null +++ b/src/TestRepository/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace HelloWorld +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello, World!"); + } + } +} diff --git a/src/TestRepository/gitignore.txt b/src/TestRepository/gitignore.txt new file mode 100644 index 0000000..2efe062 --- /dev/null +++ b/src/TestRepository/gitignore.txt @@ -0,0 +1,2 @@ +test.sh +test.bat \ No newline at end of file diff --git a/src/TestRepository/test.bat b/src/TestRepository/test.bat new file mode 100644 index 0000000..5bcc123 --- /dev/null +++ b/src/TestRepository/test.bat @@ -0,0 +1,2 @@ +@echo off +echo HelloWorld \ No newline at end of file diff --git a/src/TestRepository/test.sh b/src/TestRepository/test.sh new file mode 100644 index 0000000..af217e3 --- /dev/null +++ b/src/TestRepository/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo "HelloWorld" \ No newline at end of file diff --git a/src/src.sln b/src/vv.sln similarity index 71% rename from src/src.sln rename to src/vv.sln index 889bcda..4630317 100644 --- a/src/src.sln +++ b/src/vv.sln @@ -1,12 +1,14 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.3.11312.210 d18.3 +VisualStudioVersion = 18.3.11312.210 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "vv", "vv\vv.csproj", "{54B35286-309C-483A-AF2C-E144A39B1765}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "vv.build", "vv.build\vv.build.csproj", "{F4100060-5C7B-473D-AA5C-28076DC3A96C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "vv.tests", "vv.tests\vv.tests.csproj", "{6EC4ED7D-55C8-4975-8F21-60A3E56C482D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,6 +43,18 @@ Global {F4100060-5C7B-473D-AA5C-28076DC3A96C}.Release|x64.Build.0 = Release|Any CPU {F4100060-5C7B-473D-AA5C-28076DC3A96C}.Release|x86.ActiveCfg = Release|Any CPU {F4100060-5C7B-473D-AA5C-28076DC3A96C}.Release|x86.Build.0 = Release|Any CPU + {6EC4ED7D-55C8-4975-8F21-60A3E56C482D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6EC4ED7D-55C8-4975-8F21-60A3E56C482D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6EC4ED7D-55C8-4975-8F21-60A3E56C482D}.Debug|x64.ActiveCfg = Debug|Any CPU + {6EC4ED7D-55C8-4975-8F21-60A3E56C482D}.Debug|x64.Build.0 = Debug|Any CPU + {6EC4ED7D-55C8-4975-8F21-60A3E56C482D}.Debug|x86.ActiveCfg = Debug|Any CPU + {6EC4ED7D-55C8-4975-8F21-60A3E56C482D}.Debug|x86.Build.0 = Debug|Any CPU + {6EC4ED7D-55C8-4975-8F21-60A3E56C482D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6EC4ED7D-55C8-4975-8F21-60A3E56C482D}.Release|Any CPU.Build.0 = Release|Any CPU + {6EC4ED7D-55C8-4975-8F21-60A3E56C482D}.Release|x64.ActiveCfg = Release|Any CPU + {6EC4ED7D-55C8-4975-8F21-60A3E56C482D}.Release|x64.Build.0 = Release|Any CPU + {6EC4ED7D-55C8-4975-8F21-60A3E56C482D}.Release|x86.ActiveCfg = Release|Any CPU + {6EC4ED7D-55C8-4975-8F21-60A3E56C482D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/vv.tests/Tests/AdvancedEnumerateTest.cs b/src/vv.tests/Tests/AdvancedEnumerateTest.cs new file mode 100644 index 0000000..cb89dd8 --- /dev/null +++ b/src/vv.tests/Tests/AdvancedEnumerateTest.cs @@ -0,0 +1,51 @@ +using LibGit2Sharp; +using System.Runtime.InteropServices; +using vv.Core; + +namespace vv.tests; + +public class AdvancedEnumerateTest +{ + [Fact] + public void TestEnumerate() + { + var tempDir = Directory.CreateTempSubdirectory(); + var root = tempDir.FullName; + + // Duck mac + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + root = Path.GetFullPath(new DirectoryInfo(tempDir.FullName).FullName); + if (!root.StartsWith("/private")) + root = "/private" + root; + } + + try + { + Directory.CreateDirectory(Path.Combine(root, "src")); + Directory.CreateDirectory(Path.Combine(root, "bin")); + + File.WriteAllText(Path.Combine(root, "src", "a.cs"), ""); + File.WriteAllText(Path.Combine(root, "bin", "b.exe"), ""); + + File.WriteAllText(Path.Combine(root, ".gitignore"), "bin/\n"); + + Repository.Init(root); + using var repo = new Repository(root); + + var checkedDirs = new List(); + + AdvancedEnumerate.Walk(root, repo, true, + onEntryDir: d => checkedDirs.Add(Path.GetFileName(d)), + _ => { }, _ => { } + ); + + Assert.Contains("src", checkedDirs); + Assert.DoesNotContain("bin", checkedDirs); + } + finally + { + Directory.Delete(root, true); + } + } +} diff --git a/src/vv.tests/Tests/SetupTest.cs b/src/vv.tests/Tests/SetupTest.cs new file mode 100644 index 0000000..25f15f3 --- /dev/null +++ b/src/vv.tests/Tests/SetupTest.cs @@ -0,0 +1,30 @@ +using LibGit2Sharp; +using vv.Core; + +namespace vv.tests; + +public class SetupTest +{ + [Fact] + public void TestGettingRepos() + { + var root = Directory.CreateTempSubdirectory().FullName; + try + { + File.WriteAllText(Path.Combine(root, "a.cs"), ""); + Repository.Init(root); + + var reposFolderPath = Directory.GetParent(root).FullName; + SetupHandle.WriteSetupToJson(new(reposFolderPath)); + + var reposNames = SetupHandle.GetRepositoriesNamesFromSetup(); + + Assert.Contains(Path.GetFileName(root), reposNames); + } + finally + { + File.Delete(SetupHandle.SetupFilePath); + Directory.Delete(root, true); + } + } +} \ No newline at end of file diff --git a/src/vv.tests/Tests/TokeiTest.cs b/src/vv.tests/Tests/TokeiTest.cs new file mode 100644 index 0000000..0ac8fce --- /dev/null +++ b/src/vv.tests/Tests/TokeiTest.cs @@ -0,0 +1,61 @@ +using vv.tests.Utils; +using vv.Core; + +namespace vv.tests; + +public class TokeiTest +{ + [Fact] + public async Task TestWithIgnore() + { + var repoPath = string.Empty; + try + { + var testRepo = TempRepo.CreateTempRepository(out repoPath); + + var outputJson = await Tokei.ManageProcess(repoPath, true); + var data = Tokei.ParseLanguageData(outputJson); + + Assert.NotNull(data); + Assert.NotEmpty(data); + + var langsNames = new List(); + foreach (var langData in data) + langsNames.Add(langData.Name); + + Assert.Contains("C#", langsNames); + Assert.DoesNotContain("Batch", langsNames); + } + finally + { + + if (!string.IsNullOrEmpty(repoPath)) + TempRepo.DeleteTempRepository(repoPath); + } + } + + [Fact] + public async Task TestWithoutIgnore() + { + var repoPath = string.Empty; + try + { + var testRepo = TempRepo.CreateTempRepository(out repoPath); + + var outputJson = await Tokei.ManageProcess(repoPath, false); + var data = Tokei.ParseLanguageData(outputJson); + + var langsNames = new List(); + foreach (var langData in data) + langsNames.Add(langData.Name); + + Assert.Contains("Batch", langsNames); + } + + finally + { + if (!string.IsNullOrEmpty(repoPath)) + TempRepo.DeleteTempRepository(repoPath); + } + } +} \ No newline at end of file diff --git a/src/vv.tests/Utils/TempRepo.cs b/src/vv.tests/Utils/TempRepo.cs new file mode 100644 index 0000000..74fd562 --- /dev/null +++ b/src/vv.tests/Utils/TempRepo.cs @@ -0,0 +1,42 @@ +using LibGit2Sharp; + +namespace vv.tests.Utils; + +internal static class TempRepo +{ + public static Repository CreateTempRepository(out string repoPath) + { + var templateRepoPath = Path.GetFullPath(Path.Combine( + new DirectoryInfo(AppContext.BaseDirectory).Parent.Parent.FullName, "TestRepository")); + + var tempRepoPath = Directory.CreateTempSubdirectory().FullName; + Console.WriteLine($"Temp repository path: {tempRepoPath}"); + + foreach(var filePath in Directory.GetFiles(templateRepoPath)) + { + var fileName = Path.GetFileName(filePath); + File.Copy(Path.Combine(templateRepoPath, fileName), Path.Combine(tempRepoPath, fileName)); + } + + File.Copy(Path.Combine(templateRepoPath, "gitignore.txt"), Path.Combine(tempRepoPath, ".gitignore")); + + Repository.Init(tempRepoPath); + using var repo = new Repository(tempRepoPath); + + repoPath = tempRepoPath; + return repo; + } + + public static void DeleteTempRepository(string repoPath) + { + var directory = new DirectoryInfo(repoPath); + + foreach (var file in directory.GetFiles("*", SearchOption.AllDirectories)) + file.Attributes = FileAttributes.Normal; + + foreach (var dir in directory.GetDirectories("*", SearchOption.AllDirectories)) + dir.Attributes = FileAttributes.Normal; + + directory.Delete(true); + } +} diff --git a/src/vv.tests/vv.tests.csproj b/src/vv.tests/vv.tests.csproj new file mode 100644 index 0000000..0ff4ab5 --- /dev/null +++ b/src/vv.tests/vv.tests.csproj @@ -0,0 +1,30 @@ + + + false + true + none + disable + + + + + + + all + + + + + + + + + + + + + + PreserveNewest + + + \ No newline at end of file diff --git a/src/vv/CLI/Commands/Languages/LanguagesCommand.cs b/src/vv/CLI/Commands/Languages/LanguagesCommand.cs index 461965f..c683c53 100644 --- a/src/vv/CLI/Commands/Languages/LanguagesCommand.cs +++ b/src/vv/CLI/Commands/Languages/LanguagesCommand.cs @@ -20,16 +20,13 @@ await DefaultRendering.Spinner("Starting processing...", async ctx => } ctx.Status("Running tokei..."); - await Tokei.ManageProcess(repoPath, !settings.DisrespectGitIgnore); + var jsonOutput = await Tokei.ManageProcess(repoPath, !settings.DisrespectGitIgnore); ctx.Status("Parsing tokei output..."); - var langsData = Tokei.ParseLanguageData(); + var langsData = Tokei.ParseLanguageData(jsonOutput); ctx.Status("Running founded languages from languages.yml ..."); fullLangsData = LanguagesYml.GetLangsColors(langsData, settings.IgnoreDocsLangs); - - ctx.Status("Deleting cache..."); - Tokei.DeleteGeneratedCache(); }); AnsiConsole.WriteLine(); diff --git a/src/vv/CLI/Commands/Setup/SetupCommand.cs b/src/vv/CLI/Commands/Setup/SetupCommand.cs index c6028b9..87f1bff 100644 --- a/src/vv/CLI/Commands/Setup/SetupCommand.cs +++ b/src/vv/CLI/Commands/Setup/SetupCommand.cs @@ -12,7 +12,7 @@ protected override async Task ExecuteImpl(SetupSettings settings, Cancellat { var installed = await Tokei.CheckIfTokeiInstalled(); if (installed) - AnsiConsole.MarkupLine("[green]Every required dependencies are satisfied![/]"); + AnsiConsole.MarkupLine("[green]All required dependencies are satisfied![/]"); else WriteError("Install tokei and add it to PATH"); } @@ -27,7 +27,7 @@ protected override async Task ExecuteImpl(SetupSettings settings, Cancellat SetupHandle.WriteSetupToJson(new(path)); - AnsiConsole.MarkupLine($"[dim white]Settings were written to {SetupHandle.SetupFileName}![/]"); + AnsiConsole.MarkupLine($"[dim white]Settings were written to {SetupHandle.SetupFilePath}![/]"); } return 0; diff --git a/src/vv/Core/LanguagesYml.cs b/src/vv/Core/LanguagesYml.cs index 3e06100..5b286c2 100644 --- a/src/vv/Core/LanguagesYml.cs +++ b/src/vv/Core/LanguagesYml.cs @@ -5,7 +5,7 @@ namespace vv.Core; internal static class LanguagesYml { - private static string LangsFileName => + public static string LangsFilePath => Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), "languages.yml"); private static Color DefaultColorIfNotFound => Color.FromHex("#383c42"); private static readonly HashSet IgnoredExtraLangs = new() @@ -16,10 +16,10 @@ internal static class LanguagesYml public static Dictionary GetLangsColors(List langsDatas, bool ignoreExtra) { - var yamlString = File.ReadAllText(LangsFileName); + var yamlString = File.ReadAllText(LangsFilePath); if (string.IsNullOrEmpty(yamlString)) - throw new UserException($"Couldn't find {LangsFileName}!"); + throw new UserException($"Couldn't find {LangsFilePath}!"); var yaml = new YamlStream(); using var reader = new StringReader(yamlString); @@ -48,10 +48,10 @@ public static Dictionary GetLangsColors(List langsDa public static async Task UpdateLanguagesYmlFile() { - if (File.Exists(LangsFileName)) File.Delete(LangsFileName); + if (File.Exists(LangsFilePath)) File.Delete(LangsFilePath); var newFileBytes = await HttpUtils.DownloadFileBytes("https://raw.githubusercontent.com/github-linguist/linguist/refs/heads/main/lib/linguist/languages.yml"); - File.WriteAllBytes(LangsFileName, newFileBytes); + File.WriteAllBytes(LangsFilePath, newFileBytes); } } \ No newline at end of file diff --git a/src/vv/Core/SetupHandle.cs b/src/vv/Core/SetupHandle.cs index 269fd68..65f594e 100644 --- a/src/vv/Core/SetupHandle.cs +++ b/src/vv/Core/SetupHandle.cs @@ -7,22 +7,22 @@ namespace vv.Core; internal static class SetupHandle { - public static string SetupFileName => + public static string SetupFilePath => Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), "settings.json"); public static void WriteSetupToJson(SetupData data) { - if (File.Exists(SetupFileName)) File.Delete(SetupFileName); + if (File.Exists(SetupFilePath)) File.Delete(SetupFilePath); - JsonUtils.WriteToJson(data, SetupFileName); + JsonUtils.WriteToJson(data, SetupFilePath); } public static SetupData ReadSetupFromJson() { - if (!File.Exists(SetupFileName)) - throw new Exception($"{SetupFileName} not found!"); + if (!File.Exists(SetupFilePath)) + throw new Exception($"{SetupFilePath} not found!"); - return JsonUtils.ReadFromJson(SetupFileName); + return JsonUtils.ReadFromJson(SetupFilePath); } public static List GetRepositoriesNamesFromSetup() diff --git a/src/vv/Core/Tokei.cs b/src/vv/Core/Tokei.cs index 029933e..fb06a1a 100644 --- a/src/vv/Core/Tokei.cs +++ b/src/vv/Core/Tokei.cs @@ -7,8 +7,6 @@ namespace vv.Core; internal static class Tokei { - private static string OutputFileName => - Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), "langs_stats.json"); public static async Task CheckIfTokeiInstalled() { @@ -37,21 +35,21 @@ public static async Task CheckIfTokeiInstalled() return true; } - public static async Task ManageProcess(string path, bool respectIgnore) + public static async Task ManageProcess(string path, bool respectIgnore) { var respectString = respectIgnore ? string.Empty : "--no-ignore"; - var psi = new ProcessStartInfo() + var psi = new ProcessStartInfo { - FileName = "cmd.exe", - Arguments = $"/c tokei \"{path}\" {respectString} -o json > {OutputFileName} -e *.d", - RedirectStandardError = true, + FileName = "tokei", + Arguments = $"\"{path}\" {respectString} -o json -e *.d", RedirectStandardOutput = true, + RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; - using var process = Process.Start(psi); + using var process = Process.Start(psi); var stdoutTask = process.StandardOutput.ReadToEndAsync(); var stderrTask = process.StandardError.ReadToEndAsync(); @@ -62,13 +60,12 @@ public static async Task ManageProcess(string path, bool respectIgnore) if (process.ExitCode != 0) throw new Exception($"Tokei process exited with error! Logs: {stderrTask.Result}"); - return; + return stdoutTask.Result; } - public static List ParseLanguageData() + public static List ParseLanguageData(string tokeiOutput) { - var json = File.ReadAllText(OutputFileName); - var root = JsonNode.Parse(json).AsObject(); + var root = JsonNode.Parse(tokeiOutput).AsObject(); var languages = new List(); @@ -84,7 +81,4 @@ public static List ParseLanguageData() return languages; } - - public static void DeleteGeneratedCache() => File.Delete(OutputFileName); - } \ No newline at end of file diff --git a/src/vv/vv.csproj b/src/vv/vv.csproj index 6cd8c08..8848db0 100644 --- a/src/vv/vv.csproj +++ b/src/vv/vv.csproj @@ -6,7 +6,6 @@ true true - false false @@ -35,4 +34,8 @@ + + + + \ No newline at end of file