map-tile-toolkit 0.3.0

Clip integer polylines and polygons into per-tile pieces on a simple tile grid
Documentation
#!/usr/bin/env just --justfile

main_crate := 'map-tile-toolkit'
# How to call the current just executable. Note that just_executable() may have `\` in Windows paths, so we need to quote it.
just := quote(just_executable())
# cargo-binstall needs a workaround due to caching when used in CI
binstall_args := if env('CI', '') != '' {'--no-confirm --no-track --disable-telemetry'} else {''}
# location of the coverage output, used by CI
coverage_lcov := 'target/llvm-cov/lcov.info'
# All targets except benches (running gungraun benches needs Valgrind; see `bench`/`bench-check`).
non_bench_targets := '--lib --bins --tests --examples'

# if running in CI, treat warnings as errors by setting CARGO_BUILD_WARNINGS to 'deny' unless it is already set
# Use `CI=true just ci-test` to run the same tests as in GitHub CI.
# Use `just env-info` to see the current value of CARGO_BUILD_WARNINGS
ci_mode := if env('CI', '') != '' {'1'} else {''}
export CARGO_BUILD_WARNINGS := env('CARGO_BUILD_WARNINGS', if ci_mode == '1' {'deny'} else {'warn'})
export RUST_BACKTRACE := env('RUST_BACKTRACE', if ci_mode == '1' {'1'} else {'0'})

@_default:
    {{just}} --list

# Run slicing benchmarks (gungraun/Valgrind instruction counts); needs `valgrind` + `cargo install
# gungraun-runner`. Optional substring filter, e.g. `just bench big`, `just bench single`, `just bench
# all` (passed to gungraun as the glob `*<filter>*` over benchmark module paths).
bench filter='':
    cargo bench {{ if filter != '' { "-- '*" + filter + "*'" } else { '' } }}

# Run only the large-polyline benchmarks (many / a few / a single tile)
bench-big: (bench 'big')

# Report benchmark instruction counts as Markdown, vs `base_ref` if given (the CI PR comment)
ci-bench output='target/bench/summary.md' base_ref='':  (assert-cmd 'valgrind') (assert-cmd 'python3')
    benches/ci-bench.sh {{quote(output)}} {{quote(base_ref)}}

# Profile a `profile` example case under the `hotpath` profiler (per-function timing + allocations),
# e.g. `just hotpath big-all-multi 5000`. Set HOTPATH_OUTPUT_FORMAT=json + HOTPATH_OUTPUT_PATH for a
# machine-readable report (this is what the Hotpath Profile CI does).
hotpath name='big-all-multi' *args:
    cargo run --release --example profile --features 'hotpath,hotpath-alloc' -- {{name}} {{args}}

# Flamegraph one profiling case (see `examples/profile.rs`), e.g. `just flamegraph big-all-multi 5000`
# Profile the `profile` example for one case with cargo-flamegraph, writing target/flamegraphs/<name>.svg
flamegraph name *args:  (cargo-install 'cargo-flamegraph' 'flamegraph')
    #!/usr/bin/env bash
    set -euo pipefail
    # perf may need `sudo sysctl kernel.perf_event_paranoid=1` (or run this recipe as root).
    mkdir -p target/flamegraphs
    svg="target/flamegraphs/{{name}}.svg"
    cargo flamegraph --profile profiling --example profile --output "$svg" -- {{name}} {{args}}
    echo "Wrote $svg"

# Flamegraph every profiling case into target/flamegraphs/<name>.svg (args optional; default iters per case), e.g. `just flamegraph-all 5000`
flamegraph-all: \
        (flamegraph 'all') \
        (flamegraph 'one') \
        (flamegraph 'big-all-multi') \
        (flamegraph 'big-all-few') \
        (flamegraph 'big-all-single') \
        (flamegraph 'big-one-multi') \
        (flamegraph 'big-one-few') \
        (flamegraph 'big-one-single') \
        (flamegraph 'poly-all-multi') \
        (flamegraph 'poly-all-huge')

# Fuzz a target with cargo-fuzz (needs nightly), e.g. `just fuzz slice_equivalence -- -max_total_time=60`
fuzz target='slice_equivalence' *args:  (cargo-install 'cargo-fuzz')
    # Pin the gnu host: the sanitizer is incompatible with a statically linked (musl) libc.
    cargo +nightly fuzz run --target "$(rustc -vV | sed -n 's/^host: //p')" {{target}} {{args}}

# Compile-check every fuzz target (needs nightly) so they don't bit-rot
fuzz-build:  (cargo-install 'cargo-fuzz')
    cargo +nightly fuzz build --target "$(rustc -vV | sed -n 's/^host: //p')"

# Run integration tests and save its output as the new expected output
bless *args:  (cargo-install 'cargo-insta')
    cargo insta test --accept --include-ignored --unreferenced=delete --all-features {{args}}

# Build the project
build:
    cargo build --workspace --all-features --all-targets

# Quick compile without building a binary
check:
    cargo check --workspace --all-features --all-targets

# Generate LCOV coverage report for CI to upload to codecov.io
ci-coverage: env-info && \
        (_coverage '--lcov' '--output-path' quote(coverage_lcov))
    rm -rf {{quote(parent_directory(coverage_lcov))}}
    mkdir -p {{quote(parent_directory(coverage_lcov))}}

# Run all tests as expected by CI
ci-test: env-info test-fmt clippy test test-doc bench-check && assert-git-is-clean

# Compile default features with minimal dependencies on the configured MSRV
ci-test-msrv:
    {{just}} ci_mode=0 env-info _check-msrv-default
    {{just}} assert-git-is-clean

# Set toolchain and run ci-test-msrv
ci-test-msrv-with-toolchain:
    RUSTUP_TOOLCHAIN="$({{just}} get-msrv)" {{just}} ci-test-msrv

# Clean all build artifacts
clean:
    cargo clean
    rm -f Cargo.lock

# Run cargo clippy to lint the code
clippy *args:
    cargo clippy --workspace --all-features --all-targets {{args}}

# Generate and open the HTML coverage report
coverage:  (_coverage '--open')

# Clean, collect, and aggregate coverage using the requested report arguments
_coverage *report_args:  (cargo-install 'cargo-llvm-cov')
    #!/usr/bin/env bash
    set -euo pipefail
    # A full clean: `clean --workspace` keeps test binaries built with other features or sources, and
    # the report would merge their stale line mappings in. It also removes the report directory.
    if rustup toolchain list | grep nightly &> /dev/null; then
        cargo +nightly llvm-cov clean
        mkdir -p {{quote(parent_directory(coverage_lcov))}}
        cargo +nightly llvm-cov --no-report --workspace --all-features {{non_bench_targets}}
        cargo +nightly llvm-cov --no-report --doctests --workspace --all-features
        cargo +nightly llvm-cov report --include-build-script {{report_args}}
    else
        cargo llvm-cov clean
        mkdir -p {{quote(parent_directory(coverage_lcov))}}
        cargo llvm-cov --no-report --workspace --all-features {{non_bench_targets}}
        cargo llvm-cov report --include-build-script {{report_args}}
    fi

# Build and open code documentation
docs *args='--open':
    DOCS_RS=1 cargo doc --no-deps {{args}} --workspace --all-features

# Print environment info
env-info:
    @echo "Running for '{{main_crate}}' crate {{if ci_mode == '1' {'in CI mode'} else {'in dev mode'} }} on {{os()}} / {{arch()}}"
    @echo "PWD {{justfile_directory()}}"
    {{just}} --version
    rustc --version
    cargo --version
    rustup --version
    @echo "CARGO_BUILD_WARNINGS='$CARGO_BUILD_WARNINGS'"
    @echo "RUST_BACKTRACE='$RUST_BACKTRACE'"

# Regenerate tests/visualize.qgz (QGIS) from every GeoJSON file under tests/, keeping its view state
visualize-tests:  (assert-cmd 'python3')
    python3 tests/visualize_tests.py tests/visualize.qgz
    @echo "--------- done --------"
    @echo "Start QGIS and open this file:"
    @echo "$(realpath tests/visualize.qgz)"

# Reformat all code `cargo fmt`. If nightly is available, use it for better results
fmt:
    #!/usr/bin/env bash
    set -euo pipefail
    if (rustup toolchain list | grep nightly && rustup component list --toolchain nightly | grep rustfmt) &> /dev/null; then
        echo 'Reformatting Rust code using nightly Rust fmt to sort imports'
        cargo +nightly fmt --all -- --config imports_granularity=Module,group_imports=StdExternalCrate
    else
        echo 'Reformatting Rust with the stable cargo fmt.  Install nightly with `rustup install nightly` for better results'
        cargo fmt --all
    fi

# Reformat all Cargo.toml files using cargo-sort
fmt-toml *args:  (cargo-install 'cargo-sort')
    cargo sort --workspace --grouped {{args}}

# Reformat all GeoJSON tests
fmt-geojson:  (assert-cmd 'node') (assert-cmd 'npm') (assert-cmd 'jq')
    #!/usr/bin/env bash
    set -euo pipefail
    tmp="$(mktemp -d)"
    trap 'rm -rf "$tmp"' EXIT
    npm install --silent --no-fund --no-audit --prefix "$tmp" json-stringify-pretty-compact
    cat > "$tmp/fmt.mjs" <<'EOF'
    import stringify from 'json-stringify-pretty-compact';
    import { readFileSync, writeFileSync } from 'node:fs';
    for (const p of process.argv.slice(2))
      writeFileSync(p, stringify(JSON.parse(readFileSync(p, 'utf8'))) + '\n');
    console.log(`Formatted ${process.argv.length - 2} GeoJSON files`);
    EOF
    # Round every number nested anywhere inside features[].geometry.coordinates to an integer.
    round_coords() {
      for p in "$@"; do
        jq '.features[].geometry.coordinates |= walk(if type == "number" then round else . end)' "$p" > "$p.tmp"
        mv "$p.tmp" "$p"
      done
    }
    round_coords "{{justfile_directory()}}"/tests/polylines/{fixtures,bad-fixtures}/*.geojson
    round_coords "{{justfile_directory()}}"/tests/polygons/fixtures/*.geojson
    round_coords "{{justfile_directory()}}"/tests/points/fixtures/*.geojson
    # The shell expands the file list (absolute paths); run node from the temp dir so the bare import
    # resolves against the throwaway install.
    cd "$tmp"
    node fmt.mjs "{{justfile_directory()}}"/tests/polylines/{fixtures,bad-fixtures}/*.geojson
    node fmt.mjs "{{justfile_directory()}}"/tests/polygons/fixtures/*.geojson
    node fmt.mjs "{{justfile_directory()}}"/tests/points/fixtures/*.geojson

# Get a package field from the metadata
get-crate-field field package=main_crate:  (assert-cmd 'jq')
    @cargo metadata --no-deps --format-version 1 | jq -e -r '.packages | map(select(.name == "{{package}}")) | first | .{{field}} // error("Field \"{{field}}\" is missing in Cargo.toml for package {{package}}")'

# Get the minimum supported Rust version (MSRV) for the crate
get-msrv package=main_crate:  (get-crate-field 'rust_version' package)

# Find the minimum supported Rust version (MSRV), update Cargo.toml, and test minimal dependencies
msrv:  (cargo-install 'cargo-msrv')
    cargo msrv find --write-msrv --ignore-lockfile -- {{just}} _check-msrv-default

# Compile the crate's default features using a dynamically generated minimal Cargo.lock
_check-msrv-default:  (cargo-install 'cargo-minimal-versions') (cargo-install 'cargo-hack')
    #!/usr/bin/env bash
    set -euo pipefail
    # cargo-msrv probes with rustup, but nested cargo subcommands may otherwise
    # fall back to the default Cargo and emit flags unsupported by the candidate rustc.
    toolchain="$(rustc --version | cut -d' ' -f2)"
    export RUSTUP_TOOLCHAIN="$toolchain"
    export CARGO="$(rustup which --toolchain "$toolchain" cargo)"
    cargo minimal-versions check --direct --package {{main_crate}}

# Run cargo-release
release *args='':  (cargo-install 'release-plz')
    release-plz {{args}}

# Check semver compatibility with prior published version. Install it with `cargo install cargo-semver-checks`
semver *args:  (cargo-install 'cargo-semver-checks')
    cargo semver-checks --all-features {{args}}

# Run all tests (every target except benches — running gungraun benches needs Valgrind; see `bench`)
test:
    cargo test --workspace --all-features {{non_bench_targets}}
    cargo test --doc --workspace --all-features

# Compile-check the benchmarks without running them
bench-check:
    cargo bench --no-run --workspace --all-features

# Test documentation generation
test-doc:  (docs '')

# Test code formatting
test-fmt: && (fmt-toml '--check' '--check-format')
    cargo fmt --all -- --check

# Find unused dependencies. Uses `cargo-udeps`
udeps:  (cargo-install 'cargo-udeps')
    cargo +nightly udeps --workspace --all-features --all-targets

# Update all dependencies, including breaking changes. Requires nightly toolchain (install with `rustup install nightly`)
update:
    cargo +nightly -Z unstable-options update --breaking
    cargo update

# Ensure that a certain command is available
[private]
assert-cmd command:
    @if ! type {{command}} > /dev/null; then \
        echo "Command '{{command}}' could not be found. Please make sure it has been installed on your computer." ;\
        exit 1 ;\
    fi

# Make sure the git repo has no uncommitted changes. Fails if CI envvar is set.
[private]
assert-git-is-clean:
    #!/usr/bin/env bash
    set -euo pipefail
    if [ -n "$(git status --porcelain --untracked-files=all)" ]; then
        >&2 echo "::error::git repo is not clean. Make sure compilation and tests artifacts are in the .gitignore, and no repo files are modified."
        if [[ "{{ci_mode}}" == "1" ]]; then
            >&2 echo "::group::git status"
            git status
            >&2 echo "::endgroup::"
            >&2 echo "::group::git diff (tracked changes)"
            git add . --intent-to-add
            git --no-pager diff
            >&2 echo "::endgroup::"
            exit 1
        else
            >&2 echo "git repo is not clean, but not failing because CI mode is not enabled."
        fi
    fi

# Check if a certain Cargo command is installed, and install it if needed
[private]
cargo-install $COMMAND $INSTALL_CMD='' *args='':
    #!/usr/bin/env bash
    set -euo pipefail
    unset CARGO_BUILD_WARNINGS
    if ! command -v $COMMAND > /dev/null; then
        echo "$COMMAND could not be found. Installing..."
        if ! command -v cargo-binstall > /dev/null; then
            set -x
            cargo install ${INSTALL_CMD:-$COMMAND} --locked {{args}}
            { set +x; } 2>/dev/null
        else
            set -x
            cargo binstall ${INSTALL_CMD:-$COMMAND} {{binstall_args}} --locked
            { set +x; } 2>/dev/null
        fi
    fi