Repository: spotify/voyager (language: python)

Issue:
cpp wrappers are using default namespace
if one wanted to only use the cpp part of voyager, it would be a problem as some of the header files are not using a voyager specific namespace like e.g. `spotify::voyager` to avoid name collitions if used in a broader context and are instead in the default namespace.


Discussion / hints:

Fix patch:
diff --git a/.clang-format b/.clang-format
new file mode 120000
index 00000000..aefd1079
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,1 @@
+cpp/.clang-format
\ No newline at end of file
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..89ce7c24
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,36 @@
+## Description
+<!-- Describe the changes introduced by this pull request and why they are necessary. -->
+
+## Related Issues
+<!-- Reference any related GitHub issues that are addressed by this pull request. -->
+
+## Changes Made
+<!-- Provide a brief overview of the changes made in this pull request. -->
+
+### C++
+<!-- Describe changes made to the C++ code, including any new features, improvements, or bug fixes. -->
+
+### Python
+<!-- Describe changes made to the Python code, including any new features, improvements, or bug fixes. -->
+
+### Java
+<!-- Describe changes made to the Java code, including any new features, improvements, or bug fixes. -->
+
+## Testing
+<!-- Outline the testing strategy employed to validate these changes. Include any relevant test cases or scenarios. -->
+
+## Checklist
+<!-- 
+    Replace [ ] with [x] to check off items. 
+    For items that are not applicable, simply remove the checkbox.
+-->
+
+- [ ] My code follows the code style of this project.
+- [ ] I have added and/or updated appropriate documentation (if applicable).
+- [ ] All new and existing tests pass locally with these changes.
+- [ ] I have run static code analysis (if available) and resolved any issues.
+- [ ] I have considered backward compatibility (if applicable).
+- [ ] I have confirmed that this PR does not introduce any security vulnerabilities.
+
+## Additional Comments
+<!-- Add any additional comments or notes that might be helpful for reviewers or contributors. -->
diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml
index 02a292d6..505e2fc0 100644
--- a/.github/workflows/all.yml
+++ b/.github/workflows/all.yml
@@ -39,8 +39,38 @@ jobs:
       - name: Check C++ Formatting
         uses: jidicula/clang-format-action@v4.13.0
         with:
-          clang-format-version: 14
-          fallback-style: LLVM
+          clang-format-version: 16
+
+  run-cpp-tests:
+    runs-on: ${{ matrix.os }}
+    continue-on-error: true
+    defaults:
+      run:
+        working-directory: cpp
+    strategy:
+      matrix:
+        # TODO: Switch back to macos-latest once https://github.com/actions/python-versions/pull/114 is fixed
+        os:
+          - 'ubuntu-latest'
+          - windows-latest
+          - macos-12
+    name: Test C++ on ${{ matrix.os }}
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          submodules: recursive
+      - name: Install CMake (Windows)
+        if: matrix.os == 'windows-latest'
+        run: |
+          choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System'
+      - name: Install CMake (MacOS)
+        if: matrix.os == 'macos-12'
+        run: brew install cmake
+      - name: Install CMake (Ubuntu)
+        if: matrix.os == 'ubuntu-latest'
+        run: sudo apt-get install -y cmake
+      - name: Run tests
+        run: make test
 
   run-java-tests:
     continue-on-error: true
diff --git a/.gitignore b/.gitignore
index c4935192..f5287bce 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
 *.egg-info/
-build/
+*/build/*
+!build/.gitkeep
 dist/
 tmp/
 __pycache__/
@@ -18,3 +19,19 @@ java/classpath.txt
 java/linux-build/include/*
 python/voyager-headers
 .asv/
+
+# Cmake
+CMakeLists.txt.user
+CMakeCache.txt
+CMakeFiles
+CMakeScripts
+Testing
+Makefile
+!cpp/Makefile
+cmake_install.cmake
+install_manifest.txt
+compile_commands.json
+CTestTestfile.cmake
+_deps
+DartConfiguration.tcl
+VoyagerTests
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..ad8e9471
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "cpp/include/doctest"]
+	path = cpp/include/doctest
+	url = git@github.com:doctest/doctest.git
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c4c28b8b..2ef187e9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,38 +1,47 @@
 # How to Contribute
-
 We'd love to get patches from you!
 
-## Getting Started
+#### Workflow
+We follow the [GitHub Flow Workflow](https://guides.github.com/introduction/flow/):
 
-### Prerequisites
+1.  Fork the project 
+1.  Check out the `master` branch 
+1.  Create a feature branch
+1.  Write code and tests for your change 
+1.  From your branch, make a pull request against `https://github.com/spotify/voyager` 
+1.  Work with repo maintainers to get your change reviewed 
+1.  Wait for your change to be pulled into `https://github.com/spotify/voyager/master`
+1.  Delete your feature branch
 
+## Getting Started
+### Prerequisites
 To compile Voyager from scratch, the following packages will need to be installed:
 
 - [Python 3.7](https://www.python.org/downloads/) or higher.
 - A C++ compiler, e.g. `gcc`, `clang`, etc.
 
 ### Building Voyager
+#### Building Python
+There are some nuances to building the Voyager python code.  Please read on for more information.
 
+For basic building, you should be able to simply run the following commands:
 ```shell
-git clone git@github.com:spotify/voyager.git
-cd voyager
-pip3 install -r python/dev-requirements.txt
-pip3 install .
+cd python
+pip install -r python/dev-requirements.txt
+pip install .
 ```
 
 To compile a debug build of `voyager` that allows using a debugger (like gdb or lldb), use the following command to build the package locally and install a symbolic link for debugging:
 ```shell
 cd python
-DEBUG=1 python3 setup.py build develop
+DEBUG=1 python setup.py build develop
 ```
 
-Then, you can `import voyager` from Python (or run the tests with `tox`) to test out your local changes.
-
 > If you're on macOS or Linux, you can try to compile a debug build _faster_ by using [Ccache](https://ccache.dev/):
 > ## macOS
 > ```shell
 > brew install ccache
-> rm -rf build && CC="ccache clang" CXX="ccache clang++" DEBUG=1 python3 -j8 -m pip install -e .
+> rm -rf build && CC="ccache clang" CXX="ccache clang++" DEBUG=1 python -j8 -m pip install -e .
 > ```
 > ## Linux
 > e.g.
@@ -40,52 +49,33 @@ Then, you can `import voyager` from Python (or run the tests with `tox`) to test
 > sudo yum install ccache  # or apt, if on a Debian
 > 
 > # If using GCC:
-> rm -rf build && CC="ccache gcc" CXX="scripts/ccache_g++" DEBUG=1 python3 setup.py build -j8 develop
+> rm -rf build && CC="ccache gcc" CXX="scripts/ccache_g++" DEBUG=1 python setup.py build -j8 develop
 > 
 > # ...or if using Clang:
-> rm -rf build && CC="ccache clang" CXX="scripts/ccache_clang++" DEBUG=1 python3 setup.py build -j8 develop
+> rm -rf build && CC="ccache clang" CXX="scripts/ccache_clang++" DEBUG=1 python setup.py build -j8 develop
 > ```
 
-### Updating Documentation
-If you notice that the documentation is out of date, feel free to run these commands in order to update the docs and make a PR with the changes.
-
-#### Python
-While `voyager` is mostly C++ code, it ships with `.pyi` files to allow for type hints in text editors and via MyPy. To update the Python type hint files, use the following commands:
-
-```shell
-cd python
-python3 -m scripts.generate_type_stubs_and_docs
-# Documentation will be dumped into ../docs/python/
-```
-
-#### Java
-To update the javadocs for the java bindings, you can simply run:
-
+#### Building Java
+To build the Java library with `maven`, use the following commands:
 ```shell
 cd java
 mvn package
 ```
 
-this will update the java documentation located in [docs/java/](https://github.com/spotify/voyager/tree/main/docs/java).
-
-## Workflow
-
-We follow the [GitHub Flow Workflow](https://guides.github.com/introduction/flow/):
-
-1.  Fork the project 
-1.  Check out the `master` branch 
-1.  Create a feature branch
-1.  Write code and tests for your change 
-1.  From your branch, make a pull request against `https://github.com/spotify/voyager` 
-1.  Work with repo maintainers to get your change reviewed 
-1.  Wait for your change to be pulled into `https://github.com/spotify/voyager/master`
-1.  Delete your feature branch
+#### Building C++
+To build the C++ library with `cmake`, use the following commands:
+```shell
+cd cpp
+git submodule update --init --recursive
+make build
+```
 
 ## Testing
-
+### Python Tests
 We use `tox` for testing - running tests from end-to-end should be as simple as:
 
 ```
+cd python
 pip3 install tox
 tox
 ```
@@ -110,10 +100,26 @@ asv continuous --sort name --no-only-changed HEAD main
 Please note that `airspeed-velocity` can only run benchmarks against a git commit, so if 
 you have uncommited code that you want to run benchmarks for you need to commit it first.
 
-## Style
+### Java Tests
+We provide java test execution as a maven test step.  Thus you can run the tests with:
 
+```shell
+cd java
+mvn verify
+```
+
+### C++ Tests
+To run the C++ tests, use the following commands:
+```shell
+cd cpp
+git submodule update --init --recursive
+make test
+```
+
+## Style
 Use [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html) for C++ code, and `black` with defaults for Python code.
 
+### Python
 In order to check and run formatting within the python module, you can use tox to facilitate this.
 ```bash
 cd python
@@ -123,8 +129,43 @@ tox -e check-formatting
 tox -e format
 ```
 
-## Issues
+### C++
+If you are working on any C++ code throughout the repo, ensure you have `clang-format` (version 16) installed, and then use clang-format to handle C++ formatting:
+```bash
+cd cpp
+cmake .
+# Check formatting only (don't change files)
+make check-formatting
+# Run formatter 
+make format
+```
+
+### Updating Documentation
+We also welcome improvements to the project documentation or to the existing
+docs. Please file an [issue](https://github.com/spotify/voyager/issues/new).
+
+If you notice that the generated API documentation is out of date, feel free to run these commands in order to update the docs and make a PR with the changes.
+
+#### Python
+While `voyager` is mostly C++ code, it ships with `.pyi` files to allow for type hints in text editors and via MyPy. To update the Python type hint files, use the following commands:
+
+```shell
+cd python
+python3 -m scripts.generate_type_stubs_and_docs
+# Documentation will be dumped into ../docs/python/
+```
+
+#### Java
+To update the javadocs for the java bindings, you can simply run:
+
+```shell
+cd java
+mvn package
+```
 
+This will update the java documentation located in [docs/java/](https://github.com/spotify/voyager/tree/main/docs/java).
+
+## Issues
 When creating an issue please try to ahere to the following format:
 
     One line summary of the issue (less than 72 characters)
@@ -141,47 +182,7 @@ When creating an issue please try to ahere to the following format:
 
     List all relevant steps to reproduce the observed behaviour.
 
-## Pull Requests
-
-Files should be exempt of trailing spaces.
-
-We adhere to a specific format for commit messages. Please write your commit
-messages along these guidelines. Please keep the line width no greater than 80
-columns (You can use `fmt -n -p -w 80` to accomplish this).
-
-    One line description of your change (less than 72 characters)
-
-    Problem
-
-    Explain the context and why you're making that change.  What is the problem
-    you're trying to solve? In some cases there is not a problem and this can be
-    thought of being the motivation for your change.
-
-    Solution
-
-    Describe the modifications you've done.
-
-    Result
-
-    What will change as a result of your pull request? Note that sometimes this
-    section is unnecessary because it is self-explanatory based on the solution.
-
-Some important notes regarding the summary line:
-
-* Describe what was done; not the result 
-* Use the active voice 
-* Use the present tense 
-* Capitalize properly 
-* Do not end in a period — this is a title/subject 
-* Prefix the subject with its scope
-
-## Documentation
-
-We also welcome improvements to the project documentation or to the existing
-docs. Please file an [issue](https://github.com/spotify/voyager/issues/new).
-
 ## First Contributions
-
 If you are a first time contributor to `voyager`,  familiarize yourself with the:
 * [Code of Conduct](CODE_OF_CONDUCT.md)
 * [GitHub Flow Workflow](https://guides.github.com/introduction/flow/)
@@ -192,20 +193,15 @@ When you're ready, navigate to [issues](https://github.com/spotify/voyager/issue
 There is a lot to learn when making your first contribution. As you gain experience, you will be able to make contributions faster. You can submit an issue using the [question](https://github.com/spotify/voyager/labels/question) label if you encounter challenges.  
 
 # License 
-
 By contributing your code, you agree to license your contribution under the 
 terms of the [LICENSE](https://github.com/spotify/voyager/blob/master/LICENSE).
 
 # Code of Conduct
-
 Read our [Code of Conduct](CODE_OF_CONDUCT.md) for the project.
 
 # Troubleshooting
-
 ## Building the project
-
 ### `ModuleNotFoundError: No module named 'pybind11'`
-
 Try updating your version of `pip`:
 ```shell
 pip install --upgrade pip
diff --git a/cpp/.clang-format b/cpp/.clang-format
new file mode 100644
index 00000000..94f0987d
--- /dev/null
+++ b/cpp/.clang-format
@@ -0,0 +1,8 @@
+---
+BasedOnStyle: LLVM
+IndentWidth: 2
+InsertNewlineAtEOF: true
+---
+Language: Cpp
+# Use 120 columns since we have big screens now
+ColumnLimit: 80
diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt
new file mode 100644
index 00000000..720f3d43
--- /dev/null
+++ b/cpp/CMakeLists.txt
@@ -0,0 +1,29 @@
+cmake_minimum_required(VERSION 3.20)
+project(Voyager)
+
+set(CMAKE_CXX_STANDARD 17)
+set(LLVM_CXX_STD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+enable_testing()
+
+add_subdirectory(include)
+add_subdirectory(src)
+add_subdirectory(test)
+
+# Define our find command with any appropriate directory exclusions (add another with `-o -path <PATH> -prune`)
+set(FIND_COMMAND find .. -path ../cpp/include -prune -o -path ../cpp/CMakeFiles -prune -o -path ../python/.tox -prune -o -name "*.cpp" -print -o -name "*.h" -type f -print)
+set(CHECK_FORMAT_COMMAND clang-format --verbose --dry-run -i)
+set(FORMAT_COMMAND clang-format --verbose -i)
+
+# Check formatting only
+add_custom_target(check-formatting
+    COMMAND ${CHECK_FORMAT_COMMAND} `${FIND_COMMAND}`
+    COMMENT "Checking C++ formatting"
+)
+
+# Run formatter
+add_custom_target(format
+    COMMAND ${FORMAT_COMMAND} `${FIND_COMMAND}`
+    COMMENT "Running C++ formatter"
+)
diff --git a/cpp/Makefile b/cpp/Makefile
new file mode 100644
index 00000000..62b1dc73
--- /dev/null
+++ b/cpp/Makefile
@@ -0,0 +1,15 @@
+default_target: build
+
+BUILD_DIR := build
+
+cmake:
+	cmake -S . -B $(BUILD_DIR)
+
+build: cmake
+	cmake --build ${BUILD_DIR}
+
+test: build
+	ctest --test-dir ${BUILD_DIR}
+
+clean:
+	rm -rf ${BUILD_DIR}/*
\ No newline at end of file
diff --git a/cpp/build/.gitkeep b/cpp/build/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/cpp/include/CMakeLists.txt b/cpp/include/CMakeLists.txt
new file mode 100644
index 00000000..7c5b1f26
--- /dev/null
+++ b/cpp/include/CMakeLists.txt
@@ -0,0 +1,6 @@
+
+add_library(doctest INTERFACE)
+target_include_directories(doctest 
+    INTERFACE 
+        doctest/doctest
+)
diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt
new file mode 100644
index 00000000..28724010
--- /dev/null
+++ b/cpp/src/CMakeLists.txt
@@ -0,0 +1,6 @@
+add_library(VoyagerLib INTERFACE)
+target_include_directories(VoyagerLib 
+    INTERFACE 
+        .
+        Spaces
+)
diff --git a/cpp/E4M3.h b/cpp/src/E4M3.h
similarity index 99%
rename from cpp/E4M3.h
rename to cpp/src/E4M3.h
index 37f6b523..ed241efd 100644
--- a/cpp/E4M3.h
+++ b/cpp/src/E4M3.h
@@ -21,6 +21,7 @@
 #pragma once
 
 #include <cmath>
+#include <string>
 
 static constexpr float ALL_E4M3_VALUES[256] = {
     0,
diff --git a/cpp/Enums.h b/cpp/src/Enums.h
similarity index 83%
rename from cpp/Enums.h
rename to cpp/src/Enums.h
index 95d11b74..b0f3377c 100644
--- a/cpp/Enums.h
+++ b/cpp/src/Enums.h
@@ -1,5 +1,7 @@
 #pragma once
 
+#include <string>
+
 /**
  * The space (i.e. distance metric) to use for searching.
  */
@@ -47,4 +49,14 @@ inline const std::string toString(SpaceType space) {
   default:
     return "Unknown space type (value " + std::to_string((int)space) + ")";
   }
-}
\ No newline at end of file
+}
+
+std::ostream &operator<<(std::ostream &os, const SpaceType space) {
+  os << toString(space);
+  return os;
+}
+
+std::ostream &operator<<(std::ostream &os, const StorageDataType sdt) {
+  os << toString(sdt);
+  return os;
+}
diff --git a/cpp/Index.h b/cpp/src/Index.h
similarity index 100%
rename from cpp/Index.h
rename to cpp/src/Index.h
diff --git a/cpp/Metadata.h b/cpp/src/Metadata.h
similarity index 99%
rename from cpp/Metadata.h
rename to cpp/src/Metadata.h
index 0032346b..feaced34 100644
--- a/cpp/Metadata.h
+++ b/cpp/src/Metadata.h
@@ -139,4 +139,4 @@ loadFromStream(std::shared_ptr<InputStream> inputStream) {
 };
 
 } // namespace Metadata
-}; // namespace voyager
\ No newline at end of file
+}; // namespace voyager
diff --git a/cpp/Spaces/Euclidean.h b/cpp/src/Spaces/Euclidean.h
similarity index 100%
rename from cpp/Spaces/Euclidean.h
rename to cpp/src/Spaces/Euclidean.h
diff --git a/cpp/Spaces/InnerProduct.h b/cpp/src/Spaces/InnerProduct.h
similarity index 100%
rename from cpp/Spaces/InnerProduct.h
rename to cpp/src/Spaces/InnerProduct.h
diff --git a/cpp/Spaces/Space.h b/cpp/src/Spaces/Space.h
similarity index 100%
rename from cpp/Spaces/Space.h
rename to cpp/src/Spaces/Space.h
diff --git a/cpp/StreamUtils.h b/cpp/src/StreamUtils.h
similarity index 99%
rename from cpp/StreamUtils.h
rename to cpp/src/StreamUtils.h
index c3d74148..d76c2ec4 100644
--- a/cpp/StreamUtils.h
+++ b/cpp/src/StreamUtils.h
@@ -177,4 +177,4 @@ static void readBinaryPOD(std::shared_ptr<InputStream> in, T &podRef) {
                              " bytes from stream! Got " +
                              std::to_string(bytesRead) + ".");
   }
-}
\ No newline at end of file
+}
diff --git a/cpp/TypedIndex.h b/cpp/src/TypedIndex.h
similarity index 99%
rename from cpp/TypedIndex.h
rename to cpp/src/TypedIndex.h
index e8905e4e..63066a40 100644
--- a/cpp/TypedIndex.h
+++ b/cpp/src/TypedIndex.h
@@ -760,4 +760,4 @@ std::unique_ptr<Index>
 loadTypedIndexFromStream(std::shared_ptr<InputStream> inputStream) {
   return loadTypedIndexFromMetadata(
       voyager::Metadata::loadFromStream(inputStream), inputStream);
-}
\ No newline at end of file
+}
diff --git a/cpp/array_utils.h b/cpp/src/array_utils.h
similarity index 99%
rename from cpp/array_utils.h
rename to cpp/src/array_utils.h
index 2e497a99..7c2a7556 100644
--- a/cpp/array_utils.h
+++ b/cpp/src/array_utils.h
@@ -308,4 +308,4 @@ template <typename dist_t, typename data_t = dist_t,
 std::string toFloatVectorString(std::vector<data_t> vec) {
   return toFloatVectorString<dist_t, data_t, scalefactor>(vec.data(),
                                                           vec.size());
-}
\ No newline at end of file
+}
diff --git a/cpp/hnswalg.h b/cpp/src/hnswalg.h
similarity index 100%
rename from cpp/hnswalg.h
rename to cpp/src/hnswalg.h
diff --git a/cpp/hnswlib.h b/cpp/src/hnswlib.h
similarity index 100%
rename from cpp/hnswlib.h
rename to cpp/src/hnswlib.h
diff --git a/cpp/std_utils.h b/cpp/src/std_utils.h
similarity index 99%
rename from cpp/std_utils.h
rename to cpp/src/std_utils.h
index 9486c9db..26caf55b 100644
--- a/cpp/std_utils.h
+++ b/cpp/src/std_utils.h
@@ -139,4 +139,4 @@ void mergePriorityQueues(
 
   while (dest.size() > maxElements)
     dest.pop();
-}
\ No newline at end of file
+}
diff --git a/cpp/visited_list_pool.h b/cpp/src/visited_list_pool.h
similarity index 100%
rename from cpp/visited_list_pool.h
rename to cpp/src/visited_list_pool.h
diff --git a/java/JavaInputStream.h b/java/JavaInputStream.h
index ac4d8d8f..2a22e1a6 100644
--- a/java/JavaInputStream.h
+++ b/java/JavaInputStream.h
@@ -144,4 +144,4 @@ class JavaInputStream : public InputStream {
   jobject inputStream;
   std::vector<char> peekValue;
   long long bytesRead = 0;
-};
\ No newline at end of file
+};
diff --git a/java/JavaOutputStream.h b/java/JavaOutputStream.h
index 593da0dc..5db295ee 100644
--- a/java/JavaOutputStream.h
+++ b/java/JavaOutputStream.h
@@ -87,4 +87,4 @@ class JavaOutputStream : public OutputStream {
 private:
   JNIEnv *env;
   jobject outputStream;
-};
\ No newline at end of file
+};
diff --git a/java/Makefile b/java/Makefile
index a30e4ab8..723eef84 100755
--- a/java/Makefile
+++ b/java/Makefile
@@ -7,7 +7,7 @@ UNAME_S := $(shell uname -s)
 MAC_SOBJ    := libvoyager.dylib
 LINUX_SOBJ  := libvoyager.so
 WIN_SOBJ    := voyager.dll
-CPP_SRC_DIR := ../cpp/
+CPP_SRC_DIR := ../cpp/src/
 
 SOURCE    := com_spotify_voyager_jni_Index.cpp
 HEADERS   := $(SOURCE:.cpp=.h) $(wildcard ../cpp/*.h)
diff --git a/python/setup.py b/python/setup.py
index 272d28f0..dd8368db 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -25,7 +25,7 @@
 from setuptools.command.build_ext import build_ext
 
 # Find the "cpp" folder depending on where this script is run from:
-for search_path in ["./cpp/", "../cpp/", "../../cpp/"]:
+for search_path in ["./cpp/src/", "../cpp/src/", "../../cpp/src/"]:
     path = os.path.abspath(os.path.join(os.path.dirname(__file__), search_path))
     if os.path.exists(path):
         VOYAGER_HEADERS_PATH = path
diff --git a/python/src/PythonFileLike.h b/python/src/PythonFileLike.h
index 613dcd35..7fd2fea6 100644
--- a/python/src/PythonFileLike.h
+++ b/python/src/PythonFileLike.h
@@ -87,4 +87,4 @@ class PythonFileLike {
 
 protected:
   py::object fileLike;
-};
\ No newline at end of file
+};
diff --git a/python/src/PythonInputStream.h b/python/src/PythonInputStream.h
index 6a1d8dd8..2cb6a0ab 100644
--- a/python/src/PythonInputStream.h
+++ b/python/src/PythonInputStream.h
@@ -201,4 +201,4 @@ class PythonInputStream : public InputStream, PythonFileLike {
   long long totalLength = -1;
   std::vector<char> peekValue;
   bool lastReadWasSmallerThanExpected = false;
-};
\ No newline at end of file
+};
diff --git a/python/src/PythonOutputStream.h b/python/src/PythonOutputStream.h
index fc30b5c5..58ca658b 100644
--- a/python/src/PythonOutputStream.h
+++ b/python/src/PythonOutputStream.h
@@ -70,4 +70,4 @@ class PythonOutputStream : public OutputStream, public PythonFileLike {
 
     return true;
   }
-};
\ No newline at end of file
+};
diff --git a/python/tox.ini b/python/tox.ini
index 9449ce25..4f6f0a34 100644
--- a/python/tox.ini
+++ b/python/tox.ini
@@ -10,7 +10,6 @@ deps =
 allowlist_externals =
     bash
 commands =
-    bash -c "[ -L voyager-headers ] || [ -d voyager-headers ] || cp -r {toxinidir}/../src ./voyager-headers/"
     python -m pip install .
     pytest {posargs}
 
@@ -22,7 +21,6 @@ deps =
 skip_install = true
 commands =
     black voyager tests --diff --check --line-length 120
-    clang-format -style=LLVM -Werror --dry-run bindings.cpp
 
 [testenv:format]
 basepython = python3
@@ -32,7 +30,6 @@ deps =
 skip_install = true
 commands =
     black voyager tests --line-length 120
-    clang-format -style=LLVM -i bindings.cpp
 
 [testenv:lint]
 basepython = python3
diff --git a/python/typenames.h b/python/typenames.h
index 61184a55..f0c1b8f3 100644
--- a/python/typenames.h
+++ b/python/typenames.h
@@ -25,4 +25,4 @@ template <> const std::string typeName<unsigned int>() { return "uint32"; }
 template <> const std::string typeName<float>() { return "float32"; }
 template <> const std::string typeName<long>() { return "int64"; }
 template <> const std::string typeName<unsigned long>() { return "uint64"; }
-template <> const std::string typeName<double>() { return "float64"; }
\ No newline at end of file
+template <> const std::string typeName<double>() { return "float64"; }
diff --git a/python/voyager-headers b/python/voyager-headers
deleted file mode 120000
index abf4cd45..00000000
--- a/python/voyager-headers
+++ /dev/null
@@ -1,1 +0,0 @@
-../cpp
\ No newline at end of file


Test patch:
diff --git a/cpp/include/doctest b/cpp/include/doctest
new file mode 160000
index 00000000..ae7a1353
--- /dev/null
+++ b/cpp/include/doctest
@@ -0,0 +1,1 @@
+Subproject commit ae7a13539fb71f270b87eb2e874fbac80bc8dda2
diff --git a/cpp/test/.gitignore b/cpp/test/.gitignore
new file mode 100644
index 00000000..5301e597
--- /dev/null
+++ b/cpp/test/.gitignore
@@ -0,0 +1,2 @@
+test
+test-*.cmake
\ No newline at end of file
diff --git a/cpp/test/CMakeLists.txt b/cpp/test/CMakeLists.txt
new file mode 100644
index 00000000..a46805c1
--- /dev/null
+++ b/cpp/test/CMakeLists.txt
@@ -0,0 +1,18 @@
+# List the test source files
+set(TEST_FILES test_main.cpp doctest_setup.cpp)  # Add any test files here
+
+# Create an executable for the tests
+add_executable(VoyagerTests ${TEST_FILES})
+
+# Link the test executable with the main project and Doctest
+# target_link_libraries(MyProjectTests PRIVATE MyProject doctest::doctest)
+target_link_libraries(VoyagerTests
+    PUBLIC
+        VoyagerLib
+    PRIVATE 
+        doctest
+)
+
+# Discover tests using Doctest
+include(${CMAKE_SOURCE_DIR}/include/doctest/scripts/cmake/doctest.cmake)
+doctest_discover_tests(VoyagerTests)
diff --git a/cpp/test/doctest_setup.cpp b/cpp/test/doctest_setup.cpp
new file mode 100644
index 00000000..a3f832e4
--- /dev/null
+++ b/cpp/test/doctest_setup.cpp
@@ -0,0 +1,2 @@
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+#include "doctest.h"
diff --git a/cpp/test/test_main.cpp b/cpp/test/test_main.cpp
new file mode 100644
index 00000000..dbfa5dc1
--- /dev/null
+++ b/cpp/test/test_main.cpp
@@ -0,0 +1,53 @@
+#include "doctest.h"
+
+#include "TypedIndex.h"
+#include <tuple>
+#include <type_traits>
+
+template <typename dist_t, typename data_t = dist_t,
+          typename scalefactor = std::ratio<1, 1>>
+void testCombination(TypedIndex<dist_t, data_t, scalefactor> &index,
+                     SpaceType spaceType, int numDimensions,
+                     StorageDataType storageType) {
+  CHECK(toString(index.getSpace()) == toString(spaceType));
+  CHECK(index.getNumDimensions() == numDimensions);
+  CHECK(toString(index.getStorageDataType()) == toString(storageType));
+}
+
+TEST_CASE("Test combinations of different instantiations and sizes") {
+  std::vector<SpaceType> spaceTypesSet = {SpaceType::Euclidean,
+                                          SpaceType::InnerProduct};
+  std::vector<int> numDimensionsSet = {4, 16, 128, 1024};
+  std::vector<int> numElementsSet = {100, 1000, 100000};
+  std::vector<StorageDataType> storageTypesSet = {
+      StorageDataType::Float8, StorageDataType::Float32, StorageDataType::E4M3};
+
+  auto count = 0;
+
+  for (auto spaceType : spaceTypesSet) {
+    for (auto numDimensions : numDimensionsSet) {
+      for (auto numElements : numElementsSet) {
+        for (auto storageType : storageTypesSet) {
+          SUBCASE("Test instantiation ") {
+            CAPTURE(spaceType);
+            CAPTURE(numDimensions);
+            CAPTURE(numElements);
+            CAPTURE(storageType);
+
+            if (storageType == StorageDataType::Float8) {
+              auto index = TypedIndex<float, int8_t, std::ratio<1, 127>>(
+                  spaceType, numDimensions);
+              testCombination(index, spaceType, numDimensions, storageType);
+            } else if (storageType == StorageDataType::Float32) {
+              auto index = TypedIndex<float>(spaceType, numDimensions);
+              testCombination(index, spaceType, numDimensions, storageType);
+            } else if (storageType == StorageDataType::E4M3) {
+              auto index = TypedIndex<float, E4M3>(spaceType, numDimensions);
+              testCombination(index, spaceType, numDimensions, storageType);
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/python/test-requirements.txt b/python/test-requirements.txt
index 6d7bfb0c..c99a94db 100644
--- a/python/test-requirements.txt
+++ b/python/test-requirements.txt
@@ -1,5 +1,5 @@
 pytest
 tox
 black
-clang-format
+clang-format~=16.0
 zstd
\ No newline at end of file