Building and Testing microhash

This document covers how to build each implementation from source (debug and release), run the CLI tools, execute the test suites, run the benchmarks, and use the Docker image.

All paths are relative to the repository root unless stated otherwise.


Prerequisites

Tool Minimum version Purpose
C++ compiler (GCC, Clang, MSVC) C++17 C++ implementation + tests
.NET SDK 9.0 C# implementation, tests, benchmarks
Ruby 2.5+ (tested on 3.1.3) Ruby implementation + CLI
RSpec (optional) 3.x Ruby test suite (gem install rspec)
Docker (optional) 20.10+ Container build / run

Note: The C++ implementation (src/cpp/microhash.hpp) is a header-only library — no separate compilation step is needed to use it. The build steps below apply to the CLI tool (main.cpp) and the test suite (tests/cpp/microhash_tests.cpp).


C++ Implementation

All commands are run from the repository root unless a cd is shown.

Debug build — CLI tool

g++ -std=c++17 -g -O0 \
    -o microhash_debug \
    src/cpp/main.cpp

With Clang:

clang++ -std=c++17 -g -O0 \
    -o microhash_debug \
    src/cpp/main.cpp

With MSVC (Developer Command Prompt):

cl /std:c++17 /Zi /Od /Fe:microhash_debug.exe src\cpp\main.cpp

Release build — CLI tool

g++ -std=c++17 -O2 -DNDEBUG \
    -o microhash \
    src/cpp/main.cpp

Running the CLI tool

Hash a string passed as an argument:

./microhash "Hello, World!"
# microhash("Hello, World!") = 0x352256EFEDC72BD1

Hash from standard input (interactive):

./microhash
# Enter a string to hash using microhash:
# > abc
# microhash("abc") = 0x8874CA7BE18B8218

Print hashes for all built-in test vectors:

./microhash --test

Debug build — test suite

g++ -std=c++17 -g -O0 \
    -I src/cpp \
    -o tests/cpp/microhash_tests \
    tests/cpp/microhash_tests.cpp

Release build — test suite

g++ -std=c++17 -O2 -DNDEBUG \
    -I src/cpp \
    -o tests/cpp/microhash_tests \
    tests/cpp/microhash_tests.cpp

Running the C++ tests

./tests/cpp/microhash_tests

Expected output (abbreviated):

=== MicroHash C++ Test Suite ===

--- Test Vectors ---
[PASS] Hello, World!
[PASS] quick brown fox
...

--- Benchmark-Style Inputs ---
[PASS] small input 'veni'
[PASS] medium input (1 KB, i%256)
[PASS] large input (1 MB, i%256)

--- Determinism ---
...

--- Sensitivity ---
...

--- Edge Cases ---
...

================================
Results: 294 passed, 0 failed

The process exits with code 0 on success and 1 if any test fails.


C# Implementation

All dotnet commands can be run from the repository root (targeting microhash.sln) or from the individual project directories.

Restore NuGet packages

dotnet restore microhash.sln

This only needs to be run once, or after changing *.csproj package references.

Debug build

dotnet build src/csharp/microhash.csproj -c Debug

Or build the entire solution (main project + test project):

dotnet build microhash.sln -c Debug

Release build

dotnet build src/csharp/microhash.csproj -c Release

Running the CLI tool

Hash a string passed as an argument:

dotnet run --project src/csharp/microhash.csproj -- "Hello, World!"
# microhash("Hello, World!") = 0x352256EFEDC72BD1

Run the pre-built binary directly (after a release build):

dotnet src/csharp/bin/Release/net9.0/microhash.dll "Hello, World!"

Interactive mode (no arguments):

dotnet run --project src/csharp/microhash.csproj
# Enter a string to hash using microhash:

Debug build — test project

dotnet build tests/csharp/microhash.Tests/microhash.Tests.csproj -c Debug

Running the C# tests

Run all tests for the test project directly:

dotnet test tests/csharp/microhash.Tests/microhash.Tests.csproj

Or run all tests in the solution at once:

dotnet test microhash.sln

Expected output:

Passed!  - Failed: 0, Passed: 38, Skipped: 0, Total: 38, Duration: ~50ms

The process exits with code 0 on success and non-zero if any test fails.

For more verbose output showing each test name:

dotnet test microhash.sln --logger "console;verbosity=normal"

Running the benchmarks

Important: BenchmarkDotNet requires a Release build. Running benchmarks under Debug produces a warning and unreliable numbers.

dotnet run --project src/csharp/microhash.csproj -c Release

Then edit src/csharp/Program.cs and set bool benchmark = true; before running, or trigger the benchmark runner directly.


Ruby Implementation

The Ruby implementation (src/ruby/microhash.rb) is a single pure-Ruby file with no gem dependencies — there is no build step. It runs on Ruby 2.5 or later and produces output identical to the C++ and C# implementations.

Running the CLI tool

Hash a string passed as an argument:

ruby src/ruby/main.rb "Hello, World!"
# 0x352256EFEDC72BD1

Interactive mode and multi-word input work as in the other CLIs:

ruby src/ruby/main.rb The quick brown fox
ruby src/ruby/main.rb

Print pass/fail results for all built-in test vectors:

ruby src/ruby/main.rb --test

The --test mode exits with code 0 on success and 1 if any vector fails, so it can be used in CI without RSpec installed.

Building the native extension (optional)

An optional C extension (src/ruby/ext/microhash/) provides the same speed class as OpenSSL-backed digests. Build it once with the standard mkmf flow:

cd src/ruby/ext/microhash
ruby extconf.rb
make

This produces microhash_ext.so (or .bundle on macOS) next to the source. microhash.rb auto-detects it at require time: MicroHash::NATIVE reports true and compute_hash/hexdigest transparently delegate to C. Without the compiled extension the pure-Ruby path is used — output is identical either way. Run make distclean to remove build artifacts (they are gitignored).

Indicative throughput on Ruby 3.1.3 (x86-64): pure Ruby hashes 64 KB in ~1.2 ms (~55 MB/s — the padded message is unpacked to words in C via String#unpack('V*') and the rotates are inlined); the native extension does it in ~36 µs (~1.8 GB/s), roughly 5× faster than Digest::SHA256 on the same input. See the README’s benchmark section for the full original/ng comparison table.

Running the Ruby tests (RSpec)

Install RSpec once:

gem install rspec

Run the suite from the repository root:

rspec tests/ruby/microhash_spec.rb

Expected output without the native extension (the native-vs-pure agreement example is reported as pending):

21 examples, 0 failures, 1 pending

With the native extension compiled, all examples run:

21 examples, 0 failures

Using the library

require_relative 'src/ruby/microhash'

MicroHash.compute_hash('Hello, World!')   # => 0x352256EFEDC72BD1 (Integer)
MicroHash.hexdigest('Hello, World!')      # => "352256EFEDC72BD1"

Strings are hashed as raw bytes (encoding-agnostic); an Array of byte values is also accepted. To use it in a Rails application, copy microhash.rb into lib/ and require it — no gems, no native extensions.


Docker (C# implementation)

The Dockerfile at src/csharp/Dockerfile performs a multi-stage build:

Build the image

Run from the repository root so the build context includes the full source tree:

docker build -f src/csharp/Dockerfile -t microhash .

Run the container

Hash a string:

docker run --rm microhash "Hello, World!"

Interactive mode:

docker run --rm -it microhash

microhash-ng

The revised implementations under src/microhash-ng/ build and run identically to their originals — substitute the paths:

# C++ CLI and tests
g++ -std=c++17 -O2 -o microhash-ng src/microhash-ng/cpp/main.cpp
g++ -std=c++17 -O2 -I src/microhash-ng/cpp -o /tmp/microhash_ng_tests tests/microhash-ng/cpp/microhash_ng_tests.cpp && /tmp/microhash_ng_tests

# C# CLI (standalone project, not part of microhash.sln)
dotnet run --project src/microhash-ng/csharp -- --test

# Ruby CLI, RSpec suite, and optional native extension
ruby src/microhash-ng/ruby/main.rb --test
rspec tests/microhash-ng/ruby/microhash_ng_spec.rb
cd src/microhash-ng/ruby/ext/microhash_ng && ruby extconf.rb && make

The ng suites include regression tests for the two defects fixed relative to the original: every single-byte flip at every position of 32/64/256-byte inputs must change the digest, the known 64-byte dead-zone collision pair must differ, and length changes (appends/truncations) must be detected. All ng suites run in CI alongside the originals.


Test Coverage Summary

Test group C++ (microhash_tests) C# (HashPipeTests) Ruby (microhash_spec)
Known-good test vectors (13 strings)
Benchmark inputs — small "veni" (4 B)
Benchmark inputs — medium 1 KB (i%256)
Benchmark inputs — large 1 MB (i%256)
Determinism — same input hashed twice
Pointer overload == vector overload C++ only
String input == byte-array input
Encoding-agnostic (UTF-8 vs binary bytes)
Sensitivity — 8 distinct-input pairs
All 256 single-byte values differ from empty
Block-boundary lengths (31, 32, 33 bytes) (11 lengths)
Digest fits in 64 bits
Native extension == pure Ruby (201 lengths) (when compiled)
Collision resistance — 100K sequential inputs, 0 collisions
Avalanche effect — avg bits changed per single-bit flip, 20–44/64
Bit distribution — each output bit 44%–56% frequency over 65K inputs
Total assertions 294 38 21 examples

The C++ runner counts each ASSERT_EQ/ASSERT_NE call individually (including the 256-byte loop), while xUnit counts parameterised theory cases as separate test cases.

Notes on the statistical tests

Collision resistance uses 100,000 sequential 8-byte inputs (integers 0–99,999 encoded as little-endian). The birthday paradox gives an expected collision count of N²/2^65 ≈ 0.27 for N=100,000, so zero is the correct result.

Avalanche effect only tests inputs ≤ 16 bytes because microhash only reads the first 16 bytes (4 × 4-byte words) of each 32-byte block. Bytes 16–31 of any block are not mixed into the state — a known design limitation documented in Specification.md §3. For active-region bytes the average bit-flip count is ~31/64 (≈48%), close to the ideal 50%.

Bit distribution hashes the 65,536 four-byte little-endian representations of 0–65,535. All 64 output bits land within 49.6%–50.3%, well within the 44%–56% test threshold.