diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index 2e01b23d2e3..875a4880151 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -32,7 +32,7 @@ concurrency: jobs: vcpkg-smoke-prereq: if: github.repository == 'Project-OSRM/osrm-backend' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: Configure DNS servers if: runner.os == 'Linux' @@ -74,7 +74,7 @@ jobs: env: VCPKG_DEFAULT_TRIPLET: x64-linux - vcpkg-windows-release-bindings: + windows-release-bindings: needs: [format-taginfo-docs, vcpkg-smoke-prereq] strategy: matrix: @@ -302,16 +302,26 @@ jobs: - name: clang-20-release continue-on-error: false node: 24 - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 BUILD_TYPE: Release CCOMPILER: clang-20 CXXCOMPILER: clang++-20 + ENABLE_LTO: ON + + - name: clang-20-debug + continue-on-error: false + node: 24 + runs-on: ubuntu-26.04 + BUILD_TYPE: Debug + CCOMPILER: clang-20 + CXXCOMPILER: clang++-20 + ENABLE_ASSERTIONS: ON ENABLE_LTO: OFF - name: clang-19-release continue-on-error: false node: 24 - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 BUILD_TYPE: Release CCOMPILER: clang-19 CXXCOMPILER: clang++-19 @@ -320,100 +330,73 @@ jobs: - name: clang-18-release continue-on-error: false node: 24 - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 BUILD_TYPE: Release CCOMPILER: clang-18 CXXCOMPILER: clang++-18 ENABLE_LTO: OFF - - name: clang-18-debug - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Debug - CCOMPILER: clang-18 - CXXCOMPILER: clang++-18 - ENABLE_LTO: OFF - - - name: clang-18-debug-clang-tidy + - name: clang-20-debug-clang-tidy continue-on-error: false node: 24 - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 BUILD_TYPE: Debug - CCOMPILER: clang-18 - CXXCOMPILER: clang++-18 + CCOMPILER: clang-20 + CXXCOMPILER: clang++-20 ENABLE_CLANG_TIDY: ON NODE_PACKAGE_TESTS_ONLY: ON ENABLE_LTO: OFF - - name: clang-18-debug-asan-ubsan + - name: clang-20-debug-asan-ubsan continue-on-error: false node: 24 - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 BUILD_TYPE: Debug - CCOMPILER: clang-18 - CXXCOMPILER: clang++-18 + CCOMPILER: clang-20 + CXXCOMPILER: clang++-20 ENABLE_SANITIZER: ON TARGET_ARCH: x86_64-asan-ubsan OSRM_CONNECTION_RETRIES: 10 OSRM_CONNECTION_EXP_BACKOFF_COEF: 1.5 - - name: clang-17-release + - name: clang-20-debug-cov continue-on-error: false node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: clang-17 - CXXCOMPILER: clang++-17 - ENABLE_LTO: OFF + runs-on: ubuntu-26.04 + BUILD_TYPE: Debug + CCOMPILER: clang-20 + CXXCOMPILER: clang++-20 + ENABLE_COVERAGE: ON - name: gcc-14-release continue-on-error: false node: 24 - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 BUILD_TYPE: Release CCOMPILER: gcc-14 CXXCOMPILER: g++-14 CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' - - name: gcc-13-release + - name: gcc-15-release continue-on-error: false node: 24 - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 BUILD_TYPE: Release - CCOMPILER: gcc-13 - CXXCOMPILER: g++-13 + CCOMPILER: gcc-15 + CXXCOMPILER: g++-15 CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' - - name: clang-20-debug-cov - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Debug - CCOMPILER: clang-20 - CXXCOMPILER: clang++-20 - ENABLE_COVERAGE: ON - - - name: vcpkg-linux-release-bindings + - name: linux-release-bindings build_bindings: true continue-on-error: false node: 24 - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 BUILD_TYPE: Release - CCOMPILER: clang-17 - CXXCOMPILER: clang++-17 - NODE_PACKAGE_TESTS_ONLY: ON - - - name: vcpkg-linux-debug - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Debug - CCOMPILER: clang-17 - CXXCOMPILER: clang++-17 + CCOMPILER: clang-18 + CXXCOMPILER: clang++-18 NODE_PACKAGE_TESTS_ONLY: ON - - name: vcpkg-linux-arm64-release + - name: linux-arm64-release continue-on-error: false node: 24 runs-on: ubuntu-24.04-arm @@ -422,28 +405,26 @@ jobs: CXXCOMPILER: clang++-18 ENABLE_LTO: OFF - - name: vcpkg-macos-26-x64-release + - name: macos-26-x64-release continue-on-error: true node: 24 runs-on: macos-26-intel # x86_64 BUILD_TYPE: Release CCOMPILER: clang CXXCOMPILER: clang++ - ENABLE_ASSERTIONS: ON vcpkg_triplet: x64-osx - - name: vcpkg-macos-26-arm64-release + - name: macos-26-arm64-release continue-on-error: true node: 24 runs-on: macos-26 # arm64 BUILD_TYPE: Release CCOMPILER: clang CXXCOMPILER: clang++ - ENABLE_ASSERTIONS: ON vcpkg_triplet: arm64-osx # python & nodeJS release bindings — macos-15 for broad binary compatibility - - name: vcpkg-macos-15-x64-release-bindings + - name: macos-15-x64-release-bindings build_bindings: true continue-on-error: true node: 24 @@ -451,10 +432,9 @@ jobs: BUILD_TYPE: Release CCOMPILER: clang CXXCOMPILER: clang++ - ENABLE_ASSERTIONS: ON vcpkg_triplet: x64-osx - - name: vcpkg-macos-15-arm64-release-bindings + - name: macos-15-arm64-release-bindings build_bindings: true continue-on-error: true node: 24 @@ -462,7 +442,6 @@ jobs: BUILD_TYPE: Release CCOMPILER: clang CXXCOMPILER: clang++ - ENABLE_ASSERTIONS: ON vcpkg_triplet: arm64-osx name: ${{ matrix.name}} @@ -770,7 +749,7 @@ jobs: ci-complete: runs-on: ubuntu-latest - needs: [build-matrix, vcpkg-windows-release-bindings, docker-build-extract-test] + needs: [build-matrix, windows-release-bindings, docker-build-extract-test] if: github.repository == 'Project-OSRM/osrm-backend' steps: - name: Configure DNS servers diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 3636cae5971..7a5fa4eb863 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,7 +12,7 @@ permissions: jobs: stale: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: Configure DNS servers if: runner.os == 'Linux' diff --git a/.github/workflows/vcpkg-smoke.yml b/.github/workflows/vcpkg-smoke.yml index 6212ca71c06..936c9a850d0 100644 --- a/.github/workflows/vcpkg-smoke.yml +++ b/.github/workflows/vcpkg-smoke.yml @@ -49,7 +49,7 @@ jobs: fail-fast: false matrix: include: - - { os: ubuntu-24.04, triplet: x64-linux } + - { os: ubuntu-26.04, triplet: x64-linux } - { os: macos-26, triplet: arm64-osx } - { os: macos-26-intel, triplet: x64-osx } - { os: windows-2025, triplet: x64-windows-static-md } diff --git a/CMakeLists.txt b/CMakeLists.txt index d11c2e7e412..fbcb5f31bbb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -395,11 +395,6 @@ endif() # file exposes them via standard CMake find_package() / imported targets. find_package(Boost 1.83 REQUIRED CONFIG COMPONENTS ${BOOST_COMPONENTS}) find_package(TBB CONFIG REQUIRED) -# Always link the release build of TBB, even in Debug/Coverage configurations. -# vcpkg's debug TBB enables internal debug assertions (e.g. intrusive_list -# invariants) that trip during osrm-extract's test-data generation on -# ubuntu-24.04 under gcc-13. The master branch never hit this because it -# linked the system's release-only TBB. Do the same here. foreach(_tbb_target TBB::tbb TBB::tbbmalloc TBB::tbbmalloc_proxy) if(TARGET ${_tbb_target}) set_target_properties(${_tbb_target} PROPERTIES diff --git a/features/support/data_classes.js b/features/support/data_classes.js index b70fd2bb979..f86bf4f2a0d 100644 --- a/features/support/data_classes.js +++ b/features/support/data_classes.js @@ -14,6 +14,7 @@ export default { // don't fail if bearings input and extected string is empty and actual result is undefined if (want === '' && (got === '' || got === undefined)) return true; + const matchPercent = want.match(/(.*)\s+~(.+)%$/), matchAbs = want.match(/(.*)\s+\+-(.+)$/), matchRe = want.match(/^\/(.*)\/$/), diff --git a/include/engine/guidance/assemble_steps.hpp b/include/engine/guidance/assemble_steps.hpp index 21dad8eeab2..c275285e48c 100644 --- a/include/engine/guidance/assemble_steps.hpp +++ b/include/engine/guidance/assemble_steps.hpp @@ -133,25 +133,29 @@ inline std::vector assembleSteps(const datafacade::BaseDataFacade &fa const auto travel_mode = facade.GetTravelMode(path_point.from_edge_based_node); BOOST_ASSERT(travel_mode > 0); - steps.push_back(RouteStep{path_point.from_edge_based_node, - step_name_id, - is_segregated, - std::string(name), - std::string(ref), - std::string(pronunciation), - std::string(destinations), - std::string(exits), - NO_ROTARY_NAME, - NO_ROTARY_NAME, - from_alias(segment_duration) / 10., - distance, - from_alias(segment_weight) / weight_multiplier, - travel_mode, - maneuver, - leg_geometry.FrontIndex(segment_index), - leg_geometry.BackIndex(segment_index) + 1, - {intersection}, - is_left_hand_driving}); + { + const double step_seconds = from_alias(segment_duration) / 10.; + steps.push_back( + RouteStep{path_point.from_edge_based_node, + step_name_id, + is_segregated, + std::string(name), + std::string(ref), + std::string(pronunciation), + std::string(destinations), + std::string(exits), + NO_ROTARY_NAME, + NO_ROTARY_NAME, + step_seconds, + distance, + from_alias(segment_weight) / weight_multiplier, + travel_mode, + maneuver, + leg_geometry.FrontIndex(segment_index), + leg_geometry.BackIndex(segment_index) + 1, + {intersection}, + is_left_hand_driving}); + } if (leg_data_index + 1 < leg_data.size()) { @@ -235,25 +239,28 @@ inline std::vector assembleSteps(const datafacade::BaseDataFacade &fa // intersections contain the classes of exiting road intersection.classes = facade.GetClasses(facade.GetClassData(target_node_id)); BOOST_ASSERT(duration >= EdgeDuration{0}); - steps.push_back(RouteStep{leg_data[leg_data.size() - 1].from_edge_based_node, - step_name_id, - is_segregated, - std::string(facade.GetNameForID(step_name_id)), - std::string(facade.GetRefForID(step_name_id)), - std::string(facade.GetPronunciationForID(step_name_id)), - std::string(facade.GetDestinationsForID(step_name_id)), - std::string(facade.GetExitsForID(step_name_id)), - NO_ROTARY_NAME, - NO_ROTARY_NAME, - from_alias(duration) / 10., - distance, - from_alias(weight) / weight_multiplier, - target_mode, - maneuver, - leg_geometry.FrontIndex(segment_index), - leg_geometry.BackIndex(segment_index) + 1, - {intersection}, - facade.IsLeftHandDriving(target_node_id)}); + { + const double step_seconds = from_alias(duration) / 10.; + steps.push_back(RouteStep{leg_data[leg_data.size() - 1].from_edge_based_node, + step_name_id, + is_segregated, + std::string(facade.GetNameForID(step_name_id)), + std::string(facade.GetRefForID(step_name_id)), + std::string(facade.GetPronunciationForID(step_name_id)), + std::string(facade.GetDestinationsForID(step_name_id)), + std::string(facade.GetExitsForID(step_name_id)), + NO_ROTARY_NAME, + NO_ROTARY_NAME, + step_seconds, + distance, + from_alias(weight) / weight_multiplier, + target_mode, + maneuver, + leg_geometry.FrontIndex(segment_index), + leg_geometry.BackIndex(segment_index) + 1, + {intersection}, + facade.IsLeftHandDriving(target_node_id)}); + } } // In this case the source + target are on the same edge segment else diff --git a/unit_tests/util/rounding_consistency.cpp b/unit_tests/util/rounding_consistency.cpp new file mode 100644 index 00000000000..af399ed4591 --- /dev/null +++ b/unit_tests/util/rounding_consistency.cpp @@ -0,0 +1,138 @@ +#include + +#include +#include + +BOOST_AUTO_TEST_CASE(round_deciseconds_deterministic) +{ + // Verify that std::round(d * 10.0) in double precision produces + // the mathematically correct decisecond value for representative + // durations. The key insight: IEEE 754 correctly-rounded multiplication + // compensates for binary representation errors of decimal fractions + // (e.g. 1.45 as double is ~1.44999999999999995559, but 1.45*10.0 + // correctly rounds to exactly 14.5, and std::round(14.5) = 15). + + struct TestCase + { + double seconds; + std::int32_t expected_deciseconds; + }; + + // clang-format off + const TestCase test_cases[] = { + // Halfway cases (ties round away from zero per std::round) + // The double representation of 1.45 is below the exact decimal, + // but d*10.0 correctly rounds to 14.5 in double precision. + {1.45, 15}, // 14.5 → 15 + {1.55, 16}, // 15.5 → 16 + {1.65, 17}, // 16.5 → 17 + {1.35, 14}, // 13.5 → 14 + {1.25, 13}, // 12.5 → 13 + {2.45, 25}, // 24.5 → 25 + {2.55, 26}, // 25.5 → 26 + {3.45, 35}, // 34.5 → 35 + + // Exact whole numbers + {0.0, 0}, + {0.5, 5}, // 5.0 → 5 + {1.0, 10}, + {2.0, 20}, + {2.5, 25}, // 25.0 → 25 + {3.5, 35}, // 35.0 → 35 + + // Non-halfway realistic durations + {1.5026, 15}, // 15.026 → 15 + {3.60139, 36}, // 36.0139 → 36 + {2.49601, 25}, // 24.9601 → 25 + {0.9999, 10}, // 9.999 → 10 + {0.1, 1}, // 1.0 → 1 + {0.2, 2}, // 2.0 → 2 + {0.3, 3}, // 3.0 → 3 + {0.4, 4}, // 4.0 → 4 + {0.6, 6}, // 6.0 → 6 + {0.7, 7}, // 7.0 → 7 + {0.8, 8}, // 8.0 → 8 + {0.9, 9}, // 9.0 → 9 + + // Large values + {12345.678901234, 123457}, // 123456.78901234 → 123457 + + // Values just off halfway + {1.449, 14}, // 14.49 → 14 + {1.451, 15}, // 14.51 → 15 + {1.549, 15}, // 15.49 → 15 + {1.551, 16}, // 15.51 → 16 + }; + // clang-format on + + for (const auto &tc : test_cases) + { + const auto result = static_cast(std::round(tc.seconds * 10.0)); + + BOOST_CHECK_MESSAGE(result == tc.expected_deciseconds, + "Rounding mismatch for " << tc.seconds << "s: std::round(" << tc.seconds + << " * 10.0) = " << result << ", expected " + << tc.expected_deciseconds); + } +} + +BOOST_AUTO_TEST_CASE(round_deciseconds_consistent_with_repeated_calls) +{ + // The rounding must be deterministic: repeated calls with the same input + // must produce the same output. This is trivially guaranteed by IEEE 754, + // but we verify it for the specific values used in the project. + + const double test_values[] = { + 1.45, + 1.55, + 1.65, + 1.35, + 1.25, + 1.5026, + 3.60139, + 2.49601, + 0.9999, + 12345.678901234, + }; + + for (const auto d : test_values) + { + const auto first = static_cast(std::round(d * 10.0)); + for (int i = 0; i < 10; ++i) + { + const auto again = static_cast(std::round(d * 10.0)); + BOOST_CHECK_MESSAGE(first == again, + "Non-deterministic rounding for " << d << ": first=" << first + << ", attempt " << i << "=" + << again); + } + } +} + +BOOST_AUTO_TEST_CASE(round_deciseconds_stable_around_boundaries) +{ + // For each half-integer boundary B.5 (e.g. 14.5), verify: + // - Values slightly below round down + // - Values slightly above round up + // This confirms std::round correctly implements ties-away-from-zero + // and that double precision multiplication doesn't introduce bias. + + // Test boundaries at deciseconds 14.5, 15.5, 16.5 (seconds 1.45, 1.55, 1.65) + const double boundary_seconds[] = {1.45, 1.55, 1.65}; + // For each boundary, we construct test values by adjusting the double + // bit pattern slightly below and above. Since we can't easily manipulate + // bits, we use the fact that 1.45-double is slightly below exact 1.45, + // and 1.55-double is slightly above exact 1.55. + + for (const auto seconds : boundary_seconds) + { + const auto result = static_cast(std::round(seconds * 10.0)); + + // Every rounding result must be within ±1 decisecond of the "naive" + // integer conversion. This catches catastrophic errors. + const auto naive = static_cast(seconds * 10.0); + BOOST_CHECK_MESSAGE(std::abs(result - naive) <= 1, + "Rounding divergence for " << seconds << "s: rounded=" << result + << ", naive=" << naive); + } +}