Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Download bitstream compatibility vectors
run: |
curl --fail --location --retry 3 \
--output "$RUNNER_TEMP/hyprfen-bitstreams_lichess-2013-01_100k.tsv" \
"https://github.com/hyprchs/hyprfen/releases/download/bitstream-compatibility-v1/hyprfen-bitstreams_lichess-2013-01_100k.tsv"
echo "2a20ca230d172db191d9981734a2e8d99a97ac9b8747604e5e40580a1650ba7f $RUNNER_TEMP/hyprfen-bitstreams_lichess-2013-01_100k.tsv" | shasum -a 256 -c -
- name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DHYPRFEN_BITSTREAM_VECTORS="$RUNNER_TEMP/hyprfen-bitstreams_lichess-2013-01_100k.tsv"
- name: Build
run: cmake --build build --config Release
- name: Test
Expand Down
8 changes: 8 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ if(BUILD_TESTING)
)
target_link_libraries(hyprfen_tests PRIVATE hyprfen::hyprfen)
add_test(NAME hyprfen_tests COMMAND hyprfen_tests)

set(HYPRFEN_BITSTREAM_VECTORS "" CACHE FILEPATH "Path to hyprfen bitstream compatibility vectors")
if(HYPRFEN_BITSTREAM_VECTORS)
add_test(
NAME hyprfen_bitstream_compatibility
COMMAND hyprfen_tests "${HYPRFEN_BITSTREAM_VECTORS}"
)
endif()
endif()

install(
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
>
> It is a compact, reversible binary codec for standard chess positions. You give it a FEN string, it gives you bytes, and `decode_fen()` returns the exact original FEN.

This C++ implementation tests for bit-equivalent output to the Python implementation. That means you can encode with `hyprfen` and decode with `hyprfen-cpp` or vice versa to get back the same FEN.
This C++ implementation checks byte-for-byte compatibility with the Python implementation across 100,000 unique FENs from the Lichess January 2013 standard-rated dump. That means you can encode with `hyprfen` and decode with `hyprfen-cpp` or vice versa to get back the same FEN.

## Build

Expand All @@ -16,6 +16,16 @@ cmake --build build
ctest --test-dir build --output-on-failure
```

CI also runs the 100,000-case bitstream compatibility test. To run it locally:

```bash
curl --fail --location --output /tmp/hyprfen-bitstreams_lichess-2013-01_100k.tsv \
https://github.com/hyprchs/hyprfen/releases/download/bitstream-compatibility-v1/hyprfen-bitstreams_lichess-2013-01_100k.tsv
echo '2a20ca230d172db191d9981734a2e8d99a97ac9b8747604e5e40580a1650ba7f /tmp/hyprfen-bitstreams_lichess-2013-01_100k.tsv' | shasum -a 256 -c -
cmake -S . -B build -DHYPRFEN_BITSTREAM_VECTORS=/tmp/hyprfen-bitstreams_lichess-2013-01_100k.tsv
ctest --test-dir build -R hyprfen_bitstream_compatibility --output-on-failure
```

## Public API

```cpp
Expand Down
66 changes: 65 additions & 1 deletion tests/test_hyprfen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <cstdint>
#include <exception>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <span>
Expand All @@ -27,6 +28,28 @@ std::string to_hex(std::span<const std::uint8_t> bytes) {
return out.str();
}

std::vector<std::uint8_t> from_hex(std::string_view hex) {
if (hex.size() % 2 != 0) {
throw std::runtime_error("hex input must have an even length");
}
const auto hex_digit = [](char ch) -> std::uint8_t {
if (ch >= '0' && ch <= '9') {
return static_cast<std::uint8_t>(ch - '0');
}
if (ch >= 'a' && ch <= 'f') {
return static_cast<std::uint8_t>(ch - 'a' + 10);
}
throw std::runtime_error("invalid hex digit");
};

std::vector<std::uint8_t> bytes;
bytes.reserve(hex.size() / 2);
for (std::size_t i = 0; i < hex.size(); i += 2) {
bytes.push_back(static_cast<std::uint8_t>((hex_digit(hex[i]) << 4) | hex_digit(hex[i + 1])));
}
return bytes;
}

void expect(bool condition, std::string_view message) {
if (!condition) {
throw std::runtime_error(std::string(message));
Expand All @@ -44,9 +67,44 @@ void expect_throws(Fn&& fn, std::string_view message) {
expect(threw, message);
}

void test_bitstream_compatibility(std::string_view path) {
std::ifstream input{std::string(path)};
expect(input.good(), "could not open bitstream vector file");

std::size_t line_number = 0;
std::size_t cases = 0;
std::string line;
while (std::getline(input, line)) {
++line_number;
if (!line.empty() && line.front() == '#') {
continue;
}
const std::size_t separator = line.find('\t');
if (separator == std::string::npos) {
throw std::runtime_error("invalid bitstream vector at line " + std::to_string(line_number));
}
const std::string_view fen(line.data(), separator);
std::vector<std::uint8_t> expected;
try {
expected = from_hex(std::string_view(line.data() + separator + 1, line.size() - separator - 1));
} catch (const std::exception& error) {
throw std::runtime_error("invalid bitstream vector at line " + std::to_string(line_number) +
": " + error.what());
}
const std::vector<std::uint8_t> encoded = hyprfen::encode_fen(fen);
if (hyprfen::decode_fen(expected) != fen || encoded != expected ||
hyprfen::decode_fen(encoded) != fen) {
throw std::runtime_error("bitstream compatibility mismatch at line " +
std::to_string(line_number));
}
++cases;
}
expect(cases == 100'000, "unexpected bitstream vector count");
}

} // namespace

int main() {
int main(int argc, char* argv[]) {
const GoldenCase golden_cases[] = {
{
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
Expand Down Expand Up @@ -121,6 +179,12 @@ int main() {
"non-zero padding bits should be rejected"
);

if (argc == 2) {
test_bitstream_compatibility(argv[1]);
} else {
expect(argc == 1, "expected at most one bitstream vector path");
}

std::cout << "hyprfen_tests passed\n";
return 0;
}
Loading