diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2fc447c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Byte-exact test data: a CRLF checkout would change parser input, +# golden output, and fixture checksums. +tests/fixtures/** -text +tests/golden/** -text diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 54fd4f4..a324396 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -25,7 +25,7 @@ body: attributes: label: Environment placeholder: | - - spc-cpp: v0.4.1 (or commit) + - spc-cpp: vX.Y.Z or commit - OS: Ubuntu 24.04 / macOS 26 - Compiler: GCC 13.3 / Clang 18 / Apple Clang 17 - libcurl: 8.5.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1218bf9..9737f3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,9 @@ uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). if it were the whole buffer, and a view that ran to the end of its buffer was read one byte past it. Clients are unaffected, since they parse the strings they own. +- The `ErrorCode` comments in `error.hpp` now agree with the README: a bad + ArcGIS layer id is `InvalidRequest`, HTTP 503 is `RateLimited`, and + `ParseError` means the body was not JSON. ## [0.4.1] - 2026-09-25 @@ -54,8 +57,7 @@ uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixed -- On macOS, any `CMAKE_OSX_DEPLOYMENT_TARGET` below 26.0 failed to compile, - and a default build would not start on a Mac older than macOS 26. +- On macOS, any `CMAKE_OSX_DEPLOYMENT_TARGET` below 26.0 failed to compile. Floating-point `std::from_chars` was used whenever libc++ was version 20 or newer, but Apple's libc++ provides it only from macOS 26. Older targets now use the stream parser, so the earliest supported macOS is 13.4. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6066684..2b5b34e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,10 +10,12 @@ git clone https://github.com/Reddimus/spc-cpp.git cd spc-cpp # Ubuntu 24.04 (its packaged CMake is too old) -sudo apt install build-essential clang-format libcurl4-openssl-dev pipx -pipx install cmake -# macOS (Xcode or its command-line tools provide Apple Clang and libcurl) +sudo apt install build-essential clang-format libcurl4-openssl-dev +sudo snap install cmake --classic +# macOS (Xcode or its command-line tools provide Apple Clang and libcurl). +# CI formats with clang-format 18; open a new shell after ensurepath. brew install cmake pipx +pipx ensurepath pipx install clang-format==18.1.8 make test @@ -103,7 +105,9 @@ a case there, or a place in its list of fixtures no parser reads. When a change to parsed output is intended, rewrite the files and review the diff: ```bash +cmake --build build SPC_UPDATE_GOLDEN=1 ./build/tests/spc_tests --gtest_filter='Parsers/Golden.*' +git status --short tests/golden # new cases are untracked files git diff tests/golden ``` @@ -116,8 +120,8 @@ build, and runs it. `BENCH_ARGS` passes flags through, for example Each benchmark also reports the heap use of one call: `allocs`, `alloc_bytes`, `peak_bytes`, and `retained_bytes`, the memory the result keeps. They repeat exactly from run to run, so compare them directly. On -macOS, `instructions` per iteration varies by less than 0.1% between runs, -even on a busy machine, which makes it the steadiest measure of CPU work. +macOS, `instructions` per iteration moves by under 1% between runs, even on +a busy machine, which makes it the steadiest measure of CPU work. Timings are noisy: to compare two builds, run each with repetitions and random interleaving, then compare the results with Google Benchmark's `compare.py`, which needs numpy and scipy: @@ -146,7 +150,7 @@ python3 build-bench/_deps/benchmark-src/tools/compare.py benchmarks \ (CMake reads them from there), move `[Unreleased]` to `[X.Y.Z] - YYYY-MM-DD` in `CHANGELOG.md`, and update the `GIT_TAG` and `find_package` versions in `README.md`. `make test-consumers` fails if - the README's `find_package` version does not match. + either README version does not match. 2. After it merges, tag `main` and push the tag: `git tag vX.Y.Z && git push origin vX.Y.Z`. 3. The release workflow checks that the tag matches `version.hpp`, runs diff --git a/README.md b/README.md index 87c9cea..d79335e 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,8 @@ int main() { ## Add it to your project -- CMake 3.31 or newer. Ubuntu 24.04 ships 3.28, so run `pipx install cmake`. +- CMake 3.31 or newer. Ubuntu 24.04 ships 3.28, so run + `sudo snap install cmake --classic`. - GCC 13+, Clang 18+ with libc++ or 19+ with libstdc++, or Apple Clang 17+. - libcurl 7.85 or newer with its headers: `libcurl4-openssl-dev` on Ubuntu, built into macOS. diff --git a/include/spc/error.hpp b/include/spc/error.hpp index 4a14e50..0d3a14a 100644 --- a/include/spc/error.hpp +++ b/include/spc/error.hpp @@ -16,9 +16,10 @@ enum class ErrorCode { NetworkError, /// HTTP 429 or 503, or the SDK's own rate limit. RateLimited, - /// HTTP 5xx, or an ArcGIS query that never finished paging. + /// HTTP 5xx other than 503, or an ArcGIS query that never finished paging. ServerError, - /// A real fault: a bad URL, a retired product, or a wrong ArcGIS layer. + /// A URL or ArcGIS service that no longer exists: a bug to report. A bad + /// layer id is `InvalidRequest`. NotFound, /// SPC has not issued this product right now; its static feeds answer /// HTTP 404. This is normal (day 1 probabilities overnight, for example), @@ -29,7 +30,8 @@ enum class ErrorCode { /// HTTP 400, an ArcGIS error such as an invalid parameter, or a response /// larger than `ClientConfig::max_response_bytes`. InvalidRequest, - /// The response was not the expected JSON. + /// The body was not valid JSON. Valid JSON without the expected fields + /// parses to an empty result. ParseError, Unknown }; diff --git a/tests/consumer/fetchcontent/CMakeLists.txt b/tests/consumer/fetchcontent/CMakeLists.txt index 1f73935..3da33bd 100644 --- a/tests/consumer/fetchcontent/CMakeLists.txt +++ b/tests/consumer/fetchcontent/CMakeLists.txt @@ -12,3 +12,9 @@ endif() add_executable(spc_fetchcontent_consumer ../main.cpp) target_link_libraries(spc_fetchcontent_consumer PRIVATE spc::spc) + +# tools/test_consumers.sh passes the README's quick start, to compile only. +if(SPC_QUICK_START) + add_executable(spc_quick_start "${SPC_QUICK_START}") + target_link_libraries(spc_quick_start PRIVATE spc::spc) +endif() diff --git a/tools/test_consumers.sh b/tools/test_consumers.sh index 6fcc23b..f4f6160 100755 --- a/tools/test_consumers.sh +++ b/tools/test_consumers.sh @@ -1,19 +1,37 @@ #!/usr/bin/env bash # Build and run a tiny consumer against an installed spc-cpp and against a # FetchContent checkout. The installed consumer asks for the version the -# README's find_package() line documents. +# README's find_package() line documents, and the FetchContent build also +# compiles the README's quick start. The README must pin the current version. set -euo pipefail source_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) scratch_dir=$(mktemp -d "${TMPDIR:-/tmp}/spc-consumer.XXXXXX") trap 'rm -rf "$scratch_dir"' EXIT +readme="$source_dir/README.md" -documented_version=$(sed -nE 's/^find_package\(spc ([0-9.]+) REQUIRED\)$/\1/p' "$source_dir/README.md") +documented_version=$(sed -nE 's/^find_package\(spc ([0-9.]+) REQUIRED\)$/\1/p' "$readme") if [ -z "$documented_version" ]; then echo "README.md has no 'find_package(spc X.Y REQUIRED)' line" >&2 exit 1 fi +version=$(sed -nE 's/^#define SPC_VERSION_(MAJOR|MINOR|PATCH) ([0-9]+)$/\2/p' \ + "$source_dir/include/spc/version.hpp" | paste -sd . -) +if ! grep -qx " GIT_TAG v$version" "$readme"; then + echo "README.md's FetchContent snippet must pin GIT_TAG v$version" >&2 + exit 1 +fi + +# The first C++ block, the quick start. It needs the network, so it only +# compiles. +quick_start="$scratch_dir/quick_start.cpp" +awk '/^```cpp$/ { inside = 1; next } inside && /^```$/ { exit } inside' "$readme" >"$quick_start" +if ! grep -q 'int main' "$quick_start"; then + echo "README.md's first C++ block is not a whole program" >&2 + exit 1 +fi + cmake -S "$source_dir" -B "$scratch_dir/sdk" -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX="$scratch_dir/prefix" -DSPC_BUILD_TESTS=OFF -DSPC_BUILD_EXAMPLES=OFF cmake --build "$scratch_dir/sdk" --parallel @@ -30,8 +48,8 @@ cmake --build "$scratch_dir/installed" --parallel "$scratch_dir/installed/spc_installed_consumer" cmake -S "$source_dir/tests/consumer/fetchcontent" -B "$scratch_dir/fetchcontent" \ - -DSPC_SOURCE_DIR="$source_dir" + -DSPC_SOURCE_DIR="$source_dir" -DSPC_QUICK_START="$quick_start" cmake --build "$scratch_dir/fetchcontent" --parallel "$scratch_dir/fetchcontent/spc_fetchcontent_consumer" -echo "consumer checks passed (find_package(spc $documented_version))" +echo "consumer checks passed (v$version, find_package(spc $documented_version))"