diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9298362..23a8d7d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,18 +1,26 @@
-name: Build DesktopRanger
+name: Build and test DesktopRanger
on:
push:
- branches: [ "main" ]
+ branches:
+ - main
+
pull_request:
- branches: [ "main" ]
+ branches:
+ - main
+
workflow_dispatch:
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
+ cancel-in-progress: true
+
permissions:
contents: read
jobs:
- build:
- name: Build Release x64
+ build-and-test:
+ name: Build and test Release x64
runs-on: windows-2025-vs2026
steps:
@@ -38,7 +46,8 @@ jobs:
}
Write-Host "MSBuild path: $msbuild"
- "MSBUILD_PATH=$msbuild" | Out-File -FilePath $env:GITHUB_ENV -Append
+ "MSBUILD_PATH=$msbuild" |
+ Out-File -FilePath $env:GITHUB_ENV -Append
- name: Build solution
shell: pwsh
@@ -49,10 +58,56 @@ jobs:
/p:Platform=x64 `
/p:TreatWarningsAsErrors=false
+ - name: Run unit tests
+ shell: pwsh
+ run: |
+ $testExecutable = Join-Path `
+ $env:GITHUB_WORKSPACE `
+ ".output\Release\Desktop.Ranger.Tests.exe"
+
+ $testResults = Join-Path `
+ $env:GITHUB_WORKSPACE `
+ ".output\Release\test-results.xml"
+
+ if (-not (Test-Path $testExecutable)) {
+ Write-Host "Test executable was not found."
+ Write-Host "Contents of the output directory:"
+
+ Get-ChildItem `
+ -Path ".output" `
+ -Recurse `
+ -Force `
+ -ErrorAction SilentlyContinue
+
+ throw "Missing test executable: $testExecutable"
+ }
+
+ Write-Host "Running tests: $testExecutable"
+
+ & $testExecutable `
+ "--gtest_output=xml:$testResults"
+
+ $testExitCode = $LASTEXITCODE
+
+ if ($testExitCode -ne 0) {
+ Write-Error "Unit tests failed with exit code $testExitCode."
+ exit $testExitCode
+ }
+
+ Write-Host "All unit tests passed."
+
- name: Show output directory
+ if: ${{ always() }}
shell: pwsh
run: |
- Get-ChildItem -Recurse -Force .output -ErrorAction SilentlyContinue
+ Get-ChildItem -Recurse -Force -Path ".output" -ErrorAction SilentlyContinue
+ - name: Upload test results
+ if: ${{ always() }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: DesktopRanger-Test-Results
+ path: .output/Release/test-results.xml
+ if-no-files-found: warn
- name: Upload build artifacts
uses: actions/upload-artifact@v4
diff --git a/.gitignore b/.gitignore
index a283942..4cf5638 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,25 +1,94 @@
-# User-specific files
+# Visual Studio user-specific files
+.vs/
*.suo
*.user
-*.venv
+*.userosscache
+*.sln.docstates
+
+# Visual Studio databases and caches
+*.VC.db
+*.VC.VC.opendb
+*.opendb
+*.opensdf
+*.sdf
+*.ncb
+ipch/
+
+# Build output directories
+.output/
+.intermediate/
-# Build-specific files
+# C and C++ Build-specific files
*.exe
+*.dll
+*.lib
+*.exp
+
+# C and C++ compiler/linker output
+*.obj
+*.o
+*.pch
*.pdb
+*.idb
+*.ilk
+*.ipdb
+*.iobj
+*.tlog
+*.lastbuildstate
-# Logger
-app.log
+# Incremental and profiling output
+*.pgc
+*.pgd
+*.rsp
+*.tmp
-# Visual C++ Cache and Build artifacts
-.intermediate/
-.output/
-.vs/
-vcpkg_installed/
-
-#GoogleTest Files
+#GoogleTest Files NuGet
packages/
-#CodeCoverage Results
+# GoogleTest and test results
+
TestResults/
+Testing/
+*.trx
+*.coverage
+*.coveragexml
+test-results.xml
+
+# CMake
+CMakeFiles/
+CMakeCache.txt
+cmake_install.cmake
+CTestTestfile.cmake
+cmake-build-*/
+CMakeUserPresets.json
+_deps/
+
+
+# vcpkg local output
+vcpkg_installed/
+buildtrees/
+downloads/
+
+# Logger
+*.log
+app.log
+
+# Editor and operating-system files
+*.swp
+*.swo
+*~
+.DS_Store
+Thumbs.db
+Desktop.ini
+
+# Python/local environments
+# ========================================
+.venv/
+venv/
+__pycache__/
+*.pyc
-CLAUDE.md
\ No newline at end of file
+# Local assistant/tool configuration
+CLAUDE.md
+CLAUDE.md
+.claude/
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
index 1520c8f..6db2fa1 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "external/wil"]
path = external/wil
url = https://github.com/microsoft/wil.git
+[submodule "external/googletest"]
+ path = external/googletest
+ url = https://github.com/google/googletest.git
diff --git a/DesktopRanger.sln b/DesktopRanger.sln
index ae7b4f3..b5e2702 100644
--- a/DesktopRanger.sln
+++ b/DesktopRanger.sln
@@ -1,10 +1,14 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.14.36915.13
+# Visual Studio Version 18
+VisualStudioVersion = 18.8.12021.73 stable
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Desktop.Ranger", "desktop.ranger\desktop.ranger.launcher.vcxproj", "{401D5967-31CF-4E85-96EE-80FF9A802EEA}"
EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Desktop.Ranger.Tests", "Desktop.Ranger.Tests\Desktop.Ranger.Tests.vcxproj", "{01DD77D4-6425-46EA-A0A8-C9A6C7E12A0F}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Desktop.Ranger.Core", "Desktop.Ranger.Core\Desktop.Ranger.Core.vcxproj", "{8B80B052-8D00-44B6-AF5A-A9785C26142A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -21,6 +25,22 @@ Global
{401D5967-31CF-4E85-96EE-80FF9A802EEA}.Release|Any CPU.Build.0 = Release|x64
{401D5967-31CF-4E85-96EE-80FF9A802EEA}.Release|x64.ActiveCfg = Release|x64
{401D5967-31CF-4E85-96EE-80FF9A802EEA}.Release|x64.Build.0 = Release|x64
+ {01DD77D4-6425-46EA-A0A8-C9A6C7E12A0F}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {01DD77D4-6425-46EA-A0A8-C9A6C7E12A0F}.Debug|Any CPU.Build.0 = Debug|x64
+ {01DD77D4-6425-46EA-A0A8-C9A6C7E12A0F}.Debug|x64.ActiveCfg = Debug|x64
+ {01DD77D4-6425-46EA-A0A8-C9A6C7E12A0F}.Debug|x64.Build.0 = Debug|x64
+ {01DD77D4-6425-46EA-A0A8-C9A6C7E12A0F}.Release|Any CPU.ActiveCfg = Release|x64
+ {01DD77D4-6425-46EA-A0A8-C9A6C7E12A0F}.Release|Any CPU.Build.0 = Release|x64
+ {01DD77D4-6425-46EA-A0A8-C9A6C7E12A0F}.Release|x64.ActiveCfg = Release|x64
+ {01DD77D4-6425-46EA-A0A8-C9A6C7E12A0F}.Release|x64.Build.0 = Release|x64
+ {8B80B052-8D00-44B6-AF5A-A9785C26142A}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {8B80B052-8D00-44B6-AF5A-A9785C26142A}.Debug|Any CPU.Build.0 = Debug|x64
+ {8B80B052-8D00-44B6-AF5A-A9785C26142A}.Debug|x64.ActiveCfg = Debug|x64
+ {8B80B052-8D00-44B6-AF5A-A9785C26142A}.Debug|x64.Build.0 = Debug|x64
+ {8B80B052-8D00-44B6-AF5A-A9785C26142A}.Release|Any CPU.ActiveCfg = Release|x64
+ {8B80B052-8D00-44B6-AF5A-A9785C26142A}.Release|Any CPU.Build.0 = Release|x64
+ {8B80B052-8D00-44B6-AF5A-A9785C26142A}.Release|x64.ActiveCfg = Release|x64
+ {8B80B052-8D00-44B6-AF5A-A9785C26142A}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/desktop.ranger.core/desktop.ranger.core.vcxproj b/desktop.ranger.core/desktop.ranger.core.vcxproj
new file mode 100644
index 0000000..9722642
--- /dev/null
+++ b/desktop.ranger.core/desktop.ranger.core.vcxproj
@@ -0,0 +1,107 @@
+
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ 18.0
+ Win32Proj
+ {8b80b052-8d00-44b6-af5a-a9785c26142a}
+ DesktopRangerCore
+ 10.0.26100.0
+
+
+
+ StaticLibrary
+ true
+ v145
+ Unicode
+
+
+ StaticLibrary
+ false
+ v145
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(SolutionDir).output\$(Configuration)\
+ $(SolutionDir).intermediate\$(ProjectName)\$(Configuration)\
+
+
+ $(SolutionDir).output\$(Configuration)\
+ $(SolutionDir).intermediate\$(ProjectName)\$(Configuration)\
+
+
+
+ Level3
+ true
+ _DEBUG;_LIB;%(PreprocessorDefinitions)
+ true
+ stdcpplatest
+ NotUsing
+
+
+
+
+
+
+
+ true
+
+
+ true
+
+
+
+
+ Level3
+ true
+ true
+ true
+ NDEBUG;_LIB;%(PreprocessorDefinitions)
+ true
+ stdcpplatest
+ NotUsing
+
+
+
+
+
+
+
+ true
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/desktop.ranger.core/desktop.ranger.core.vcxproj.filters b/desktop.ranger.core/desktop.ranger.core.vcxproj.filters
new file mode 100644
index 0000000..ef5c245
--- /dev/null
+++ b/desktop.ranger.core/desktop.ranger.core.vcxproj.filters
@@ -0,0 +1,27 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+
+
+ Header Files
+
+
+
+
+ Source Files
+
+
+
\ No newline at end of file
diff --git a/desktop.ranger.core/desktop_name_generator.cpp b/desktop.ranger.core/desktop_name_generator.cpp
new file mode 100644
index 0000000..35b6dc4
--- /dev/null
+++ b/desktop.ranger.core/desktop_name_generator.cpp
@@ -0,0 +1,43 @@
+#include "desktop_name_generator.h"
+
+#include
+#include
+#include
+
+namespace DR
+{
+ namespace
+ {
+ constexpr std::wstring_view kDesktopNameAlphabet = L"abcdefghijklmnopqrstuvwxyz"
+ L"0123456789"
+ L"_-.,;:@#$%&'()[]{}+=~`!^";
+
+ } // namespace
+
+ bool IsValidDesktopNameCharacter(const wchar_t character) noexcept
+ {
+ return kDesktopNameAlphabet.find(character) != std::wstring_view::npos;
+ }
+
+ std::wstring GenerateDesktopName(std::mt19937 &generator, std::size_t length)
+ {
+ std::uniform_int_distribution Distribution(
+ 0, kDesktopNameAlphabet.size() - 1);
+
+ std::wstring Result;
+ Result.reserve(length);
+
+ for (std::size_t index = 0; index < length; ++index) {
+ Result.push_back(kDesktopNameAlphabet[Distribution(generator)]);
+ }
+
+ return Result;
+ }
+
+ std::wstring GenerateDesktopName()
+ {
+ thread_local std::mt19937 Generator{ std::random_device{}() };
+ return GenerateDesktopName(Generator);
+ }
+
+} // namespace DR
\ No newline at end of file
diff --git a/desktop.ranger.core/desktop_name_generator.h b/desktop.ranger.core/desktop_name_generator.h
new file mode 100644
index 0000000..82ef033
--- /dev/null
+++ b/desktop.ranger.core/desktop_name_generator.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include
+#include
+#include
+
+namespace DR
+{
+
+ inline constexpr std::size_t kDesktopNameLength = 255;
+
+ std::wstring GenerateDesktopName();
+
+ std::wstring GenerateDesktopName(std::mt19937 &generator,
+ std::size_t length = kDesktopNameLength);
+
+ bool IsValidDesktopNameCharacter(wchar_t character) noexcept;
+
+} // namespace DR
\ No newline at end of file
diff --git a/desktop.ranger.tests/desktop.ranger.tests.vcxproj b/desktop.ranger.tests/desktop.ranger.tests.vcxproj
new file mode 100644
index 0000000..3f46ddf
--- /dev/null
+++ b/desktop.ranger.tests/desktop.ranger.tests.vcxproj
@@ -0,0 +1,113 @@
+
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ {01dd77d4-6425-46ea-a0a8-c9a6c7e12a0f}
+ Win32Proj
+ 10.0.26100.0
+ Application
+ v145
+ Unicode
+
+
+
+
+
+
+
+
+ $(SolutionDir).output\$(Configuration)\
+ $(SolutionDir).intermediate\$(ProjectName)\$(Configuration)\
+
+
+ $(SolutionDir).output\$(Configuration)\
+ $(SolutionDir).intermediate\$(ProjectName)\$(Configuration)\
+
+
+
+
+ $(SolutionDir)external\googletest\googletest\include;$(SolutionDir)external\googletest\googletest;$(SolutionDir)Desktop.Ranger.Core;%(AdditionalIncludeDirectories)
+ stdcpplatest
+ true
+ Use
+ pch.h
+ Disabled
+ X64;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ EnableFastChecks
+ MultiThreadedDebugDLL
+ Level3
+
+
+ true
+ Console
+
+
+
+
+ $(SolutionDir)external\googletest\googletest\include;$(SolutionDir)external\googletest\googletest;$(SolutionDir)Desktop.Ranger.Core;%(AdditionalIncludeDirectories)
+ stdcpplatest
+ true
+ Use
+ pch.h
+ X64;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ MultiThreadedDLL
+ Level3
+ ProgramDatabase
+
+
+ true
+ Console
+ true
+ true
+
+
+
+
+
+
+
+
+ Create
+ Create
+
+
+ NotUsing
+
+
+
+
+
+
+
+
+
+
+ NotUsing
+
+
+
+
+
+
+
+
+
+
+
+
+ {8b80b052-8d00-44b6-af5a-a9785c26142a}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/desktop.ranger.tests/desktop_name_generator_test.cpp b/desktop.ranger.tests/desktop_name_generator_test.cpp
new file mode 100644
index 0000000..b2e8cbb
--- /dev/null
+++ b/desktop.ranger.tests/desktop_name_generator_test.cpp
@@ -0,0 +1,99 @@
+#include "pch.h"
+
+#include
+#include
+#include
+#include
+
+#include "desktop_name_generator.h"
+
+namespace DR::Tests
+{
+
+ TEST(DesktopNameGenerator, GeneratesDefaultLengthName)
+ {
+ std::mt19937 generator{ 12345 };
+
+ const std::wstring name = GenerateDesktopName(generator);
+
+ EXPECT_EQ(name.size(), kDesktopNameLength);
+ }
+
+ TEST(DesktopNameGenerator, GeneratesRequestedLength)
+ {
+ std::mt19937 generator{ 12345 };
+
+ const std::wstring name = GenerateDesktopName(generator, 32);
+
+ EXPECT_EQ(name.size(), 32U);
+ }
+
+ TEST(DesktopNameGenerator, GeneratesEmptyNameForZeroLength)
+ {
+ std::mt19937 generator{ 12345 };
+
+ const std::wstring name = GenerateDesktopName(generator, 0);
+
+ EXPECT_TRUE(name.empty());
+ }
+
+ TEST(DesktopNameGenerator, UsesOnlyAllowedCharacters)
+ {
+ std::mt19937 generator{ 12345 };
+
+ const std::wstring name = GenerateDesktopName(generator, 4096);
+
+ const bool allCharactersAreValid =
+ std::ranges::all_of(name, IsValidDesktopNameCharacter);
+
+ EXPECT_TRUE(allCharactersAreValid);
+ }
+
+ TEST(DesktopNameGenerator, DoesNotContainNullCharacters)
+ {
+ std::mt19937 generator{ 12345 };
+
+ const std::wstring name = GenerateDesktopName(generator);
+
+ EXPECT_EQ(name.find(L'\0'), std::wstring::npos);
+ }
+
+ TEST(DesktopNameGenerator, IsDeterministicForFixedSeed)
+ {
+ std::mt19937 firstGenerator{ 12345 };
+ std::mt19937 secondGenerator{ 12345 };
+
+ const std::wstring firstName = GenerateDesktopName(firstGenerator);
+
+ const std::wstring secondName = GenerateDesktopName(secondGenerator);
+
+ EXPECT_EQ(firstName, secondName);
+ }
+
+ TEST(DesktopNameGenerator, ProducesDifferentNamesSequentially)
+ {
+ std::mt19937 generator{ 12345 };
+
+ const std::wstring firstName = GenerateDesktopName(generator);
+
+ const std::wstring secondName = GenerateDesktopName(generator);
+
+ EXPECT_NE(firstName, secondName);
+ }
+
+ TEST(DesktopNameGenerator, ProducesUniqueNamesInSmallSample)
+ {
+ constexpr std::size_t sampleSize = 1000;
+
+ std::mt19937 generator{ 12345 };
+ std::unordered_set names;
+ names.reserve(sampleSize);
+
+ for (std::size_t index = 0; index < sampleSize; ++index) {
+ names.insert(GenerateDesktopName(generator));
+ }
+
+ EXPECT_EQ(names.size(), sampleSize);
+ }
+
+} // namespace DR::Tests
\ No newline at end of file
diff --git a/desktop.ranger.tests/pch.cpp b/desktop.ranger.tests/pch.cpp
new file mode 100644
index 0000000..250fb27
--- /dev/null
+++ b/desktop.ranger.tests/pch.cpp
@@ -0,0 +1,5 @@
+//
+// pch.cpp
+//
+
+#include "pch.h"
diff --git a/desktop.ranger.tests/pch.h b/desktop.ranger.tests/pch.h
new file mode 100644
index 0000000..0572a70
--- /dev/null
+++ b/desktop.ranger.tests/pch.h
@@ -0,0 +1,7 @@
+//
+// pch.h
+//
+
+#pragma once
+
+#include "gtest/gtest.h"
diff --git a/desktop.ranger/advanced_secure_desktop.cpp b/desktop.ranger/advanced_secure_desktop.cpp
index 6a1feee..4047dc1 100644
--- a/desktop.ranger/advanced_secure_desktop.cpp
+++ b/desktop.ranger/advanced_secure_desktop.cpp
@@ -11,6 +11,7 @@
#include
#include "advanced_secure_desktop.h"
+#include "desktop_name_generator.h"
#include "status_logger.h"
#include "utils.h"
#include "winstation_patcher.h"
@@ -42,23 +43,25 @@ namespace AdvancedSecureDesktop
std::wstring GetRandomDesktopName()
{
- static constexpr wchar_t alphabet[] = L"abcdefghijklmnopqrstuvwxyz" // 26
- "0123456789" // 10
- "_-.,;:@#$%&'()[]{}+=~`!^"; // 25
-
- static constexpr auto alphabetSize =
- std::size(alphabet) - 1; // without null terminator
-
- static thread_local std::mt19937 generate(std::random_device{}());
-
- static constexpr auto length = 255;
- std::wstring result(length, L'\0');
-
- for (wchar_t &c : result) {
- c = alphabet[generate() % alphabetSize];
- }
-
- return result;
+ return DR::GenerateDesktopName();
+
+ // static constexpr wchar_t alphabet[] = L"abcdefghijklmnopqrstuvwxyz" // 26
+ // "0123456789" // 10
+ // "_-.,;:@#$%&'()[]{}+=~`!^"; // 25
+ //
+ // static constexpr auto alphabetSize =
+ // std::size(alphabet) - 1; // without null terminator
+ //
+ // static thread_local std::mt19937 generate(std::random_device{}());
+ //
+ // static constexpr auto length = 255;
+ // std::wstring result(length, L'\0');
+ //
+ // for (wchar_t &c : result) {
+ // c = alphabet[generate() % alphabetSize];
+ // }
+ //
+ // return result;
}
bool CreateNewDesktop(IN std::wstring DesktopName, OUT HDESK &DefaultDesktopHandle,
diff --git a/desktop.ranger/desktop.ranger.launcher.vcxproj b/desktop.ranger/desktop.ranger.launcher.vcxproj
index 93d30e0..efa683f 100644
--- a/desktop.ranger/desktop.ranger.launcher.vcxproj
+++ b/desktop.ranger/desktop.ranger.launcher.vcxproj
@@ -61,6 +61,9 @@
_DEBUG;_CONSOLE;%(PreprocessorDefinitions)
true
stdcpplatest
+ $(SolutionDir)Desktop.Ranger.Core;%(AdditionalIncludeDirectories)
+
+
Console
@@ -77,6 +80,9 @@
NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
true
stdcpplatest
+ $(SolutionDir)Desktop.Ranger.Core;%(AdditionalIncludeDirectories)
+
+
Console
@@ -103,6 +109,11 @@
+
+
+ {8b80b052-8d00-44b6-af5a-a9785c26142a}
+
+
diff --git a/external/googletest b/external/googletest
new file mode 160000
index 0000000..52eb810
--- /dev/null
+++ b/external/googletest
@@ -0,0 +1 @@
+Subproject commit 52eb8108c5bdec04579160ae17225d66034bd723