diff --git a/.github/workflows/unit-testing.yml b/.github/workflows/unit-testing.yml
new file mode 100644
index 00000000..5fbbae8e
--- /dev/null
+++ b/.github/workflows/unit-testing.yml
@@ -0,0 +1,41 @@
+name: Unit testing (Windows / MSBuild)
+
+on:
+ workflow_dispatch:
+ push:
+ branches: ["master"]
+ pull_request:
+ branches: ["master"]
+ schedule:
+ - cron: "0 0 * * 0" # weekly, Sunday 00:00 UTC
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: windows-latest
+
+ env:
+ SOLUTION_NAME: TechnitiumLibrary.sln
+ BUILD_CONFIGURATION: Debug
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install .NET 9 SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 9.0.x
+
+ - name: Add MSBuild to PATH
+ uses: microsoft/setup-msbuild@v2
+
+ - name: Restore
+ run: msbuild ${{ env.SOLUTION_NAME }} /t:Restore
+
+ - name: Build
+ run: msbuild ${{ env.SOLUTION_NAME }} /m /p:Configuration=${{ env.BUILD_CONFIGURATION }}
+
+ - name: Test (msbuild)
+ run: msbuild TechnitiumLibrary.UnitTests\TechnitiumLibrary.UnitTests.csproj /t:Test /p:Configuration=${{ env.BUILD_CONFIGURATION }}
\ No newline at end of file
diff --git a/README.md b/README.md
index b329873a..ad146a6c 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,6 @@
# TechnitiumLibrary
A library for .net based applications.
+
+## Quality Assurance
+
+[](https://github.com/TechnitiumSoftware/TechnitiumLibrary/actions/workflows/unit-testing.yml)
\ No newline at end of file
diff --git a/TechnitiumLibrary.UnitTests/MSTestSettings.cs b/TechnitiumLibrary.UnitTests/MSTestSettings.cs
new file mode 100644
index 00000000..e466aa12
--- /dev/null
+++ b/TechnitiumLibrary.UnitTests/MSTestSettings.cs
@@ -0,0 +1,3 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
diff --git a/TechnitiumLibrary.UnitTests/TechnitiumLibrary.ByteTree/ByteTreeTests.cs b/TechnitiumLibrary.UnitTests/TechnitiumLibrary.ByteTree/ByteTreeTests.cs
new file mode 100644
index 00000000..47b7d33d
--- /dev/null
+++ b/TechnitiumLibrary.UnitTests/TechnitiumLibrary.ByteTree/ByteTreeTests.cs
@@ -0,0 +1,389 @@
+/*
+Technitium Library
+Copyright (C) 2026 Shreyas Zare (shreyas@technitium.com)
+Copyright (C) 2026 Zafer Balkan (zafer@zaferbalkan.com)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+*/
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using TechnitiumLibrary.ByteTree;
+
+namespace TechnitiumLibrary.UnitTests.TechnitiumLibrary.ByteTree
+{
+ [TestClass]
+ public sealed class ByteTreeTests
+ {
+ private static byte[] Key(params byte[] b) => b;
+
+ // ---------------------------
+ // ADD + GET
+ // ---------------------------
+ [TestMethod]
+ public void Add_ShouldInsertValue_WhenKeyDoesNotExist()
+ {
+ // GIVEN
+ ByteTree tree = new ByteTree();
+
+ // WHEN
+ tree.Add(Key(1, 2, 3), "value");
+
+ // THEN
+ Assert.AreEqual("value", tree[Key(1, 2, 3)]);
+ }
+
+ [TestMethod]
+ public void Add_ShouldThrow_WhenKeyExists()
+ {
+ // GIVEN
+ ByteTree tree = new ByteTree();
+ tree.Add(Key(4), "first");
+
+ // WHEN – THEN
+ Assert.ThrowsExactly(() =>
+ tree.Add(Key(4), "duplicate"));
+ }
+
+ [TestMethod]
+ public void Add_ShouldThrow_WhenKeyNull()
+ {
+ ByteTree tree = new ByteTree();
+ Assert.ThrowsExactly(() => tree.Add(null, "x"));
+ }
+
+ // ---------------------------
+ // TryAdd
+ // ---------------------------
+ [TestMethod]
+ public void TryAdd_ShouldReturnTrue_WhenKeyAdded()
+ {
+ ByteTree tree = new ByteTree();
+ bool result = tree.TryAdd(Key(1), "v");
+ Assert.IsTrue(result);
+ }
+
+ [TestMethod]
+ public void TryAdd_ShouldReturnFalse_WhenKeyExists()
+ {
+ ByteTree tree = new ByteTree();
+ tree.Add(Key(5), "initial");
+
+ bool result = tree.TryAdd(Key(5), "other");
+
+ Assert.IsFalse(result);
+ Assert.AreEqual("initial", tree[Key(5)]);
+ }
+
+ [TestMethod]
+ public void TryAdd_ShouldThrow_WhenKeyNull()
+ {
+ ByteTree tree = new ByteTree();
+ Assert.ThrowsExactly(() => tree.TryAdd(null, "x"));
+ }
+
+ // ---------------------------
+ // GET operations
+ // ---------------------------
+ [TestMethod]
+ public void TryGet_ShouldReturnTrue_WhenKeyExists()
+ {
+ ByteTree tree = new ByteTree();
+ tree.Add(Key(1, 2), "data");
+
+ bool found = tree.TryGet(Key(1, 2), out string? value);
+
+ Assert.IsTrue(found);
+ Assert.AreEqual("data", value);
+ }
+
+ [TestMethod]
+ public void TryGet_ShouldReturnFalse_WhenMissing()
+ {
+ ByteTree tree = new ByteTree();
+
+ bool result = tree.TryGet(Key("\t"u8.ToArray()), out string? value);
+
+ Assert.IsFalse(result);
+ Assert.IsNull(value);
+ }
+
+ [TestMethod]
+ public void TryGet_ShouldThrow_WhenNull()
+ {
+ ByteTree tree = new ByteTree();
+ Assert.ThrowsExactly(() => tree.TryGet(null, out _));
+ }
+
+ // ---------------------------
+ // ContainsKey
+ // ---------------------------
+ [TestMethod]
+ public void ContainsKey_ShouldReturnTrue_WhenKeyPresent()
+ {
+ ByteTree tree = new ByteTree();
+ tree.Add(Key(3, 3), "v");
+
+ Assert.IsTrue(tree.ContainsKey(Key(3, 3)));
+ }
+
+ [TestMethod]
+ public void ContainsKey_ShouldReturnFalse_WhenKeyMissing()
+ {
+ ByteTree tree = new ByteTree();
+ Assert.IsFalse(tree.ContainsKey(Key(3, 100)));
+ }
+
+ [TestMethod]
+ public void ContainsKey_ShouldThrow_WhenNull()
+ {
+ ByteTree tree = new ByteTree();
+ Assert.ThrowsExactly(() => tree.ContainsKey(null));
+ }
+
+ // ---------------------------
+ // Remove
+ // ---------------------------
+ [TestMethod]
+ public void TryRemove_ShouldReturnTrue_WhenKeyExists()
+ {
+ ByteTree tree = new ByteTree();
+ tree.Add(Key("\n"u8.ToArray()), "v");
+
+ bool result = tree.TryRemove(Key("\n"u8.ToArray()), out string? removed);
+
+ Assert.IsTrue(result);
+ Assert.AreEqual("v", removed);
+ Assert.IsFalse(tree.ContainsKey(Key("\n"u8.ToArray())));
+ }
+
+ [TestMethod]
+ public void TryRemove_ShouldReturnFalse_WhenMissing()
+ {
+ ByteTree tree = new ByteTree();
+ bool result = tree.TryRemove(Key(11), out string? removed);
+
+ Assert.IsFalse(result);
+ Assert.IsNull(removed);
+ }
+
+ [TestMethod]
+ public void TryRemove_ShouldThrow_WhenNull()
+ {
+ ByteTree tree = new ByteTree();
+ Assert.ThrowsExactly(() => tree.TryRemove(null, out _));
+ }
+
+ // ---------------------------
+ // TryUpdate
+ // ---------------------------
+ [TestMethod]
+ public void TryUpdate_ShouldReplaceValue_WhenComparisonMatches()
+ {
+ ByteTree tree = new ByteTree();
+ tree.Add(Key(5), "old");
+
+ bool updated = tree.TryUpdate(Key(5), "new", "old");
+
+ Assert.IsTrue(updated);
+ Assert.AreEqual("new", tree[Key(5)]);
+ }
+
+ [TestMethod]
+ public void TryUpdate_ShouldReturnFalse_WhenComparisonDoesNotMatch()
+ {
+ ByteTree tree = new ByteTree();
+ tree.Add(Key(7), "original");
+
+ bool updated = tree.TryUpdate(Key(7), "attempt", "different");
+
+ Assert.IsFalse(updated);
+ Assert.AreEqual("original", tree[Key(7)]);
+ }
+
+ [TestMethod]
+ public void TryUpdate_ShouldThrow_WhenNullKey()
+ {
+ ByteTree tree = new ByteTree();
+ Assert.ThrowsExactly(() => tree.TryUpdate(null, "x", "y"));
+ }
+
+ [TestMethod]
+ public void TryUpdate_ShouldReturnFalse_WhenKeyMissing()
+ {
+ ByteTree tree = new ByteTree();
+ Assert.IsFalse(tree.TryUpdate(Key(9), "x", "y"));
+ }
+
+
+ // ---------------------------
+ // GetOrAdd
+ // ---------------------------
+
+ [TestMethod]
+ public void GetOrAdd_ShouldReturnExistingValue_WhenKeyExists()
+ {
+ ByteTree tree = new ByteTree();
+ tree.Add(Key(2, 2), "existing");
+ string val = tree.GetOrAdd(Key(2, 2), "new");
+ Assert.AreEqual("existing", val);
+ }
+
+ [TestMethod]
+ public void GetOrAdd_ShouldInsertAndReturnNewValue_WhenKeyMissing()
+ {
+ ByteTree tree = new ByteTree();
+ string val = tree.GetOrAdd(Key(3, 3), "added");
+ Assert.AreEqual("added", val);
+ Assert.AreEqual("added", tree[Key(3, 3)]);
+ }
+
+ [TestMethod]
+ public void GetOrAdd_ShouldThrow_WhenNullKey()
+ {
+ ByteTree tree = new ByteTree();
+ Assert.ThrowsExactly(() => tree.GetOrAdd(null, "x"));
+ }
+
+ // ---------------------------
+ // AddOrUpdate
+ // ---------------------------
+ [TestMethod]
+ public void AddOrUpdate_ShouldInsert_WhenMissing()
+ {
+ ByteTree tree = new ByteTree();
+
+ string val = tree.AddOrUpdate(
+ Key(1, 1),
+ _ => "create",
+ (_, old) => old + "update");
+
+ Assert.AreEqual("create", val);
+ }
+
+ [TestMethod]
+ public void AddOrUpdate_ShouldModify_WhenExists()
+ {
+ ByteTree tree = new ByteTree();
+ tree.Add(Key(1, 2), "first");
+
+ string updated = tree.AddOrUpdate(
+ Key(1, 2),
+ _ => "ignored",
+ (_, old) => old + "_changed");
+
+ Assert.AreEqual("first_changed", updated);
+ }
+
+ [TestMethod]
+ public void AddOrUpdate_ShouldThrow_WhenNullKey()
+ {
+ ByteTree tree = new ByteTree();
+ Assert.ThrowsExactly(() => tree.AddOrUpdate(
+ null!,
+ _ => "x",
+ (_, __) => "y"));
+ }
+
+ // ---------------------------
+ // Indexer get/set
+ // ---------------------------
+ [TestMethod]
+ public void Indexer_Get_ShouldReturnExactValue()
+ {
+ ByteTree tree = new ByteTree();
+ tree.Add(Key("c"u8.ToArray()), "stored");
+
+ Assert.AreEqual("stored", tree[Key("c"u8.ToArray())]);
+ }
+
+ [TestMethod]
+ public void Indexer_Set_ShouldOverwriteFormerValue()
+ {
+ ByteTree tree = new ByteTree();
+ tree[Key(5, 5)] = "initial";
+
+ tree[Key(5, 5)] = "updated";
+
+ Assert.AreEqual("updated", tree[Key(5, 5)]);
+ }
+
+ [TestMethod]
+ public void Indexer_Get_ShouldThrow_WhenMissingKey()
+ {
+ ByteTree tree = new ByteTree();
+ Assert.ThrowsExactly(() =>
+ _ = tree[Key(8, 8)]);
+ }
+
+ [TestMethod]
+ public void Indexer_ShouldThrow_WhenNullKey()
+ {
+ ByteTree tree = new ByteTree();
+ Assert.ThrowsExactly(() => tree[null] = "x");
+ }
+
+ // ---------------------------
+ // Enumeration
+ // ---------------------------
+ [TestMethod]
+ public void Enumerator_ShouldYieldExistingValues()
+ {
+ ByteTree tree = new ByteTree();
+ tree.Add(Key(1), "x");
+ tree.Add(Key(2), "y");
+ tree.Add(Key(3), "z");
+
+ List values = tree.ToList();
+
+ Assert.HasCount(3, values);
+ CollectionAssert.AreEquivalent(new[] { "x", "y", "z" }, values);
+ }
+
+ [TestMethod]
+ public void ReverseEnumerable_ShouldYieldInReverseOrder()
+ {
+ ByteTree tree = new ByteTree();
+ tree.Add(Key(0), "a");
+ tree.Add(Key(1), "b");
+ tree.Add(Key(255), "c");
+
+ List result = tree.GetReverseEnumerable().ToList();
+
+ Assert.HasCount(3, result);
+ Assert.AreEqual("c", result[0]); // last sorted key
+ Assert.AreEqual("b", result[1]);
+ Assert.AreEqual("a", result[2]);
+ }
+
+ // ---------------------------
+ // Clear
+ // ---------------------------
+ [TestMethod]
+ public void Clear_ShouldEraseAllData()
+ {
+ ByteTree tree = new ByteTree();
+ tree.Add(Key(1), "x");
+ tree.Add(Key(2), "y");
+
+ tree.Clear();
+
+ Assert.IsTrue(tree.IsEmpty);
+ Assert.IsFalse(tree.ContainsKey(Key(1)));
+ }
+ }
+}
diff --git a/TechnitiumLibrary.UnitTests/TechnitiumLibrary.UnitTests.csproj b/TechnitiumLibrary.UnitTests/TechnitiumLibrary.UnitTests.csproj
new file mode 100644
index 00000000..c63e9f74
--- /dev/null
+++ b/TechnitiumLibrary.UnitTests/TechnitiumLibrary.UnitTests.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net9.0
+ latest
+ disable
+ enable
+ true
+
+
+
+
+
+
+
diff --git a/TechnitiumLibrary.sln b/TechnitiumLibrary.sln
index 9cbcda3e..bfc3298a 100644
--- a/TechnitiumLibrary.sln
+++ b/TechnitiumLibrary.sln
@@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TechnitiumLibrary", "Techni
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TechnitiumLibrary.Security.OTP", "TechnitiumLibrary.Security.OTP\TechnitiumLibrary.Security.OTP.csproj", "{72AF4EB6-EB81-4655-9998-8BF24B304614}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TechnitiumLibrary.UnitTests", "TechnitiumLibrary.UnitTests\TechnitiumLibrary.UnitTests.csproj", "{D0CD41D8-E5F0-4EEF-81E3-587A2A877C49}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -75,6 +77,10 @@ Global
{72AF4EB6-EB81-4655-9998-8BF24B304614}.Debug|Any CPU.Build.0 = Debug|Any CPU
{72AF4EB6-EB81-4655-9998-8BF24B304614}.Release|Any CPU.ActiveCfg = Release|Any CPU
{72AF4EB6-EB81-4655-9998-8BF24B304614}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D0CD41D8-E5F0-4EEF-81E3-587A2A877C49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D0CD41D8-E5F0-4EEF-81E3-587A2A877C49}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D0CD41D8-E5F0-4EEF-81E3-587A2A877C49}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D0CD41D8-E5F0-4EEF-81E3-587A2A877C49}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE