diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index 4fc38c8158b..d5f4ef6e4ab 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -16,7 +16,7 @@ env: CCACHE_COMPRESS: 1 CASHER_TIME_OUT: 599 # one second less than 10m to avoid 10m timeout error: https://github.com/Project-OSRM/osrm-backend/issues/2742 CMAKE_VERSION: 3.21.2 - ENABLE_NODE_BINDINGS: "ON" + ENABLE_NODE_BINDINGS: 'ON' concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -30,102 +30,102 @@ jobs: env: BUILD_TYPE: Release steps: - - uses: actions/checkout@v4 - - run: cmake --version - - uses: actions/setup-node@v4 - with: - node-version: 20 - - run: node --version - - run: npm --version - - name: Prepare environment - shell: bash - run: | - PACKAGE_JSON_VERSION=$(node -e "console.log(require('./package.json').version)") - echo PUBLISH=$([[ "${GITHUB_REF:-}" == "refs/tags/v${PACKAGE_JSON_VERSION}" ]] && echo "On" || echo "Off") >> $GITHUB_ENV - - run: npm install --ignore-scripts - - run: npm link --ignore-scripts - - name: Build - shell: bash - run: | - mkdir build - cd build - - python3 -m venv .venv - source .venv/Scripts/Activate - python3 -m pip install conan==2.15.1 - conan profile detect --force - - cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_CONAN=ON -DENABLE_NODE_BINDINGS=ON .. - cmake --build . --config Release - - # TODO: MSVC goes out of memory when building our tests - # - name: Run tests - # shell: bash - # run: | - # cd build - # cmake --build . --config Release --target tests - # # TODO: run tests - # - name: Run node tests - # shell: bash - # run: | - # ./lib/binding_napi_v8/osrm-extract.exe -p profiles/car.lua test/data/monaco.osm.pbf - - # mkdir -p test/data/ch - # cp test/data/monaco.osrm* test/data/ch/ - # ./lib/binding_napi_v8/osrm-contract.exe test/data/ch/monaco.osrm - - # ./lib/binding_napi_v8/osrm-datastore.exe test/data/ch/monaco.osrm - # node test/nodejs/index.js - - name: Build Node package - shell: bash - run: ./scripts/ci/node_package.sh - - name: Publish Node package - if: ${{ env.PUBLISH == 'On' }} - uses: ncipollo/release-action@v1 - with: - allowUpdates: true - artifactErrorsFailBuild: true - artifacts: build/stage/**/*.tar.gz - omitBody: true - omitBodyDuringUpdate: true - omitName: true - omitNameDuringUpdate: true - replacesArtifacts: true - token: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v4 + - run: cmake --version + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: node --version + - run: npm --version + - name: Prepare environment + shell: bash + run: | + PACKAGE_JSON_VERSION=$(node -e "console.log(require('./package.json').version)") + echo PUBLISH=$([[ "${GITHUB_REF:-}" == "refs/tags/v${PACKAGE_JSON_VERSION}" ]] && echo "On" || echo "Off") >> $GITHUB_ENV + - run: npm install --ignore-scripts + - run: npm link --ignore-scripts + - name: Build + shell: bash + run: | + mkdir build + cd build + + python3 -m venv .venv + source .venv/Scripts/Activate + python3 -m pip install conan==2.15.1 + conan profile detect --force + + cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_CONAN=ON -DENABLE_NODE_BINDINGS=ON .. + cmake --build . --config Release + + # TODO: MSVC goes out of memory when building our tests + # - name: Run tests + # shell: bash + # run: | + # cd build + # cmake --build . --config Release --target tests + # # TODO: run tests + # - name: Run node tests + # shell: bash + # run: | + # ./lib/binding_napi_v8/osrm-extract.exe -p profiles/car.lua test/data/monaco.osm.pbf + + # mkdir -p test/data/ch + # cp test/data/monaco.osrm* test/data/ch/ + # ./lib/binding_napi_v8/osrm-contract.exe test/data/ch/monaco.osrm + + # ./lib/binding_napi_v8/osrm-datastore.exe test/data/ch/monaco.osrm + # node test/nodejs/index.js + - name: Build Node package + shell: bash + run: ./scripts/ci/node_package.sh + - name: Publish Node package + if: ${{ env.PUBLISH == 'On' }} + uses: ncipollo/release-action@v1 + with: + allowUpdates: true + artifactErrorsFailBuild: true + artifacts: build/stage/**/*.tar.gz + omitBody: true + omitBodyDuringUpdate: true + omitName: true + omitNameDuringUpdate: true + replacesArtifacts: true + token: ${{ secrets.GITHUB_TOKEN }} format-taginfo-docs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Enable Node.js cache - uses: actions/cache@v4 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - name: Prepare environment - run: | - npm ci --ignore-scripts - clang-format-15 --version - - name: Run checks - run: | - ./scripts/check_taginfo.py taginfo.json profiles/car.lua - # Check that code formatting is up to date - ./scripts/format.sh && ./scripts/error_on_dirty.sh "Run './scripts/format.sh' locally and commit the changes." - node ./scripts/validate_changelog.js - # Check that API documentation is up to date with JSDoc comments in node_osrm.cpp - npm run docs && ./scripts/error_on_dirty.sh "Run 'npm run docs' locally and commit the changes." - npm audit --production + - uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Enable Node.js cache + uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - name: Prepare environment + run: | + npm ci --ignore-scripts + clang-format-15 --version + - name: Run checks + run: | + ./scripts/check_taginfo.py taginfo.json profiles/car.lua + # Check that code formatting is up to date + ./scripts/format.sh && ./scripts/error_on_dirty.sh "Run './scripts/format.sh' locally and commit the changes." + node ./scripts/validate_changelog.js + # Check that API documentation is up to date with JSDoc comments in node_osrm.cpp + npm run docs && ./scripts/error_on_dirty.sh "Run 'npm run docs' locally and commit the changes." + npm audit --production docker-image-matrix: strategy: matrix: - docker-base-image: ["debian", "alpine"] + docker-base-image: ['debian', 'alpine'] needs: format-taginfo-docs runs-on: ubuntu-22.04 continue-on-error: false @@ -179,7 +179,6 @@ jobs: BUILD_TYPE: Release CCOMPILER: clang-20 CXXCOMPILER: clang++-20 - CUCUMBER_TIMEOUT: 60000 ENABLE_LTO: OFF - name: clang-19-release @@ -189,7 +188,6 @@ jobs: BUILD_TYPE: Release CCOMPILER: clang-19 CXXCOMPILER: clang++-19 - CUCUMBER_TIMEOUT: 60000 ENABLE_LTO: OFF - name: clang-18-release @@ -199,7 +197,6 @@ jobs: BUILD_TYPE: Release CCOMPILER: clang-18 CXXCOMPILER: clang++-18 - CUCUMBER_TIMEOUT: 60000 ENABLE_LTO: OFF # TODO(themarex): Reenable after #7309 @@ -210,7 +207,6 @@ jobs: # BUILD_TYPE: Debug # CCOMPILER: clang-18 # CXXCOMPILER: clang++-18 - # CUCUMBER_TIMEOUT: 60000 # ENABLE_LTO: OFF - name: clang-18-debug-clang-tidy @@ -220,7 +216,6 @@ jobs: BUILD_TYPE: Debug CCOMPILER: clang-18 CXXCOMPILER: clang++-18 - CUCUMBER_TIMEOUT: 60000 ENABLE_CLANG_TIDY: ON NODE_PACKAGE_TESTS_ONLY: ON ENABLE_LTO: OFF @@ -232,7 +227,6 @@ jobs: # runs-on: ubuntu-24.04 # BUILD_TYPE: Debug # CCOMPILER: clang-18 - # CUCUMBER_TIMEOUT: 20000 # CXXCOMPILER: clang++-18 # ENABLE_SANITIZER: ON # TARGET_ARCH: x86_64-asan-ubsan @@ -246,7 +240,6 @@ jobs: BUILD_TYPE: Release CCOMPILER: clang-17 CXXCOMPILER: clang++-17 - CUCUMBER_TIMEOUT: 60000 ENABLE_LTO: OFF - name: clang-16-release @@ -256,7 +249,6 @@ jobs: BUILD_TYPE: Release CCOMPILER: clang-16 CXXCOMPILER: clang++-16 - CUCUMBER_TIMEOUT: 60000 ENABLE_LTO: OFF - name: gcc-14-release @@ -284,7 +276,6 @@ jobs: # runs-on: ubuntu-24.04 # BUILD_TYPE: Debug # CCOMPILER: gcc-13 - # CUCUMBER_TIMEOUT: 20000 # CXXCOMPILER: g++-13 # ENABLE_COVERAGE: ON @@ -349,7 +340,6 @@ jobs: BUILD_TYPE: Release CCOMPILER: clang CXXCOMPILER: clang++ - CUCUMBER_TIMEOUT: 60000 ENABLE_ASSERTIONS: ON ENABLE_CONAN: ON @@ -361,7 +351,6 @@ jobs: BUILD_TYPE: Release CCOMPILER: clang CXXCOMPILER: clang++ - CUCUMBER_TIMEOUT: 60000 ENABLE_ASSERTIONS: ON ENABLE_CONAN: ON @@ -373,7 +362,6 @@ jobs: BUILD_SHARED_LIBS: ${{ matrix.BUILD_SHARED_LIBS }} CCOMPILER: ${{ matrix.CCOMPILER }} CFLAGS: ${{ matrix.CFLAGS }} - CUCUMBER_TIMEOUT: ${{ matrix.CUCUMBER_TIMEOUT }} CXXCOMPILER: ${{ matrix.CXXCOMPILER }} CXXFLAGS: ${{ matrix.CXXFLAGS }} ENABLE_ASSERTIONS: ${{ matrix.ENABLE_ASSERTIONS }} @@ -387,249 +375,249 @@ jobs: OSRM_CONNECTION_EXP_BACKOFF_COEF: ${{ matrix.OSRM_CONNECTION_EXP_BACKOFF_COEF }} ENABLE_LTO: ${{ matrix.ENABLE_LTO }} steps: - - uses: actions/checkout@v4 - - name: Build machine architecture - run: uname -m - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node }} - - name: Enable Node.js cache - uses: actions/cache@v4 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - name: Enable compiler cache - uses: actions/cache@v4 - with: - path: ~/.ccache - key: ccache-${{ matrix.name }}-${{ github.sha }} - restore-keys: | - ccache-${{ matrix.name }}- - - name: Enable Conan cache - uses: actions/cache@v4 - with: - path: ~/.conan2 - key: v10-conan-${{ matrix.name }}-${{ github.sha }} - restore-keys: | - v10-conan-${{ matrix.name }}- - - name: Enable test cache - uses: actions/cache@v4 - with: - path: ${{github.workspace}}/test/cache - key: v4-test-${{ matrix.name }}-${{ github.sha }} - restore-keys: | - v4-test-${{ matrix.name }}- - - name: Prepare environment - run: | - echo "CCACHE_DIR=$HOME/.ccache" >> $GITHUB_ENV - mkdir -p $HOME/.ccache - - PACKAGE_JSON_VERSION=$(node -e "console.log(require('./package.json').version)") - echo PUBLISH=$([[ "${GITHUB_REF:-}" == "refs/tags/v${PACKAGE_JSON_VERSION}" ]] && echo "On" || echo "Off") >> $GITHUB_ENV - echo "OSRM_INSTALL_DIR=${GITHUB_WORKSPACE}/install-osrm" >> $GITHUB_ENV - echo "OSRM_BUILD_DIR=${GITHUB_WORKSPACE}/build-osrm" >> $GITHUB_ENV - if [[ "$ENABLE_SANITIZER" == 'ON' ]]; then - # We can only set this after checkout once we know the workspace directory - echo "LSAN_OPTIONS=print_suppressions=0:suppressions=${GITHUB_WORKSPACE}/scripts/ci/leaksanitizer.conf" >> $GITHUB_ENV - echo "UBSAN_OPTIONS=symbolize=1:halt_on_error=1:print_stacktrace=1:suppressions=${GITHUB_WORKSPACE}/scripts/ci/undefinedsanitizer.conf" >> $GITHUB_ENV - echo "ASAN_OPTIONS=print_suppressions=0:suppressions=${GITHUB_WORKSPACE}/scripts/ci/addresssanitizer.conf" >> $GITHUB_ENV - fi - - if [[ "${RUNNER_OS}" == "Linux" ]]; then - echo "JOBS=$((`nproc` + 1))" >> $GITHUB_ENV - elif [[ "${RUNNER_OS}" == "macOS" ]]; then - echo "JOBS=$((`sysctl -n hw.ncpu` + 1))" >> $GITHUB_ENV - fi - # See: https://github.com/actions/toolkit/issues/946#issuecomment-1590016041 - # We need it to be able to access system folders while restoring cached Boost below - - name: Give tar root ownership - if: runner.os == 'Linux' && matrix.ENABLE_CONAN != 'ON' - run: sudo chown root /bin/tar && sudo chmod u+s /bin/tar - - - name: Install boost - if: ${{ matrix.ENABLE_CONAN != 'ON' }} - uses: MarkusJx/install-boost@v2 - id: install-boost - with: - boost_version: 1.85.0 - - - name: Install dev dependencies - run: | - # workaround for issue that GitHub Actions seems to not adding it to PATH after https://github.com/actions/runner-images/pull/6499 - # and that's why CI cannot find conan executable installed above - if [[ "${RUNNER_OS}" == "macOS" ]]; then - echo "/Library/Frameworks/Python.framework/Versions/Current/bin" >> $GITHUB_PATH - fi - - # Update package list - if [[ "${RUNNER_OS}" == "Linux" || "${ENABLE_CONAN}" != "ON" ]]; then - sudo apt-get update -y - fi - - # Install compiler and ccache - if [[ "${RUNNER_OS}" == "Linux" ]]; then - sudo apt-get install -y ${CCOMPILER} ${CXXCOMPILER} ccache - elif [[ "${RUNNER_OS}" == "macOS" ]]; then - brew install ccache - fi - - # Linux dev packages - if [[ "${ENABLE_CONAN}" != "ON" ]]; then - sudo apt-get install -y libbz2-dev libxml2-dev libzip-dev liblua5.2-dev - if [[ "${ENABLE_COVERAGE}" == "ON" ]]; then - sudo apt-get install -y lcov + - uses: actions/checkout@v4 + - name: Build machine architecture + run: uname -m + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - name: Enable Node.js cache + uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - name: Enable compiler cache + uses: actions/cache@v4 + with: + path: ~/.ccache + key: ccache-${{ matrix.name }}-${{ github.sha }} + restore-keys: | + ccache-${{ matrix.name }}- + - name: Enable Conan cache + uses: actions/cache@v4 + with: + path: ~/.conan2 + key: v10-conan-${{ matrix.name }}-${{ github.sha }} + restore-keys: | + v10-conan-${{ matrix.name }}- + - name: Enable test cache + uses: actions/cache@v4 + with: + path: ${{github.workspace}}/test/cache + key: v4-test-${{ matrix.name }}-${{ github.sha }} + restore-keys: | + v4-test-${{ matrix.name }}- + - name: Prepare environment + run: | + echo "CCACHE_DIR=$HOME/.ccache" >> $GITHUB_ENV + mkdir -p $HOME/.ccache + + PACKAGE_JSON_VERSION=$(node -e "console.log(require('./package.json').version)") + echo PUBLISH=$([[ "${GITHUB_REF:-}" == "refs/tags/v${PACKAGE_JSON_VERSION}" ]] && echo "On" || echo "Off") >> $GITHUB_ENV + echo "OSRM_INSTALL_DIR=${GITHUB_WORKSPACE}/install-osrm" >> $GITHUB_ENV + echo "OSRM_BUILD_DIR=${GITHUB_WORKSPACE}/build-osrm" >> $GITHUB_ENV + if [[ "$ENABLE_SANITIZER" == 'ON' ]]; then + # We can only set this after checkout once we know the workspace directory + echo "LSAN_OPTIONS=print_suppressions=0:suppressions=${GITHUB_WORKSPACE}/scripts/ci/leaksanitizer.conf" >> $GITHUB_ENV + echo "UBSAN_OPTIONS=symbolize=1:halt_on_error=1:print_stacktrace=1:suppressions=${GITHUB_WORKSPACE}/scripts/ci/undefinedsanitizer.conf" >> $GITHUB_ENV + echo "ASAN_OPTIONS=print_suppressions=0:suppressions=${GITHUB_WORKSPACE}/scripts/ci/addresssanitizer.conf" >> $GITHUB_ENV fi - fi - - # TBB - TBB_VERSION=2021.12.0 - if [[ "${RUNNER_OS}" == "Linux" ]]; then - TBB_URL="https://github.com/oneapi-src/oneTBB/releases/download/v${TBB_VERSION}/oneapi-tbb-${TBB_VERSION}-lin.tgz" - elif [[ "${RUNNER_OS}" == "macOS" ]]; then - TBB_URL="https://github.com/oneapi-src/oneTBB/releases/download/v${TBB_VERSION}/oneapi-tbb-${TBB_VERSION}-mac.tgz" - fi - wget --tries 5 ${TBB_URL} -O onetbb.tgz - tar zxvf onetbb.tgz - sudo cp -a oneapi-tbb-${TBB_VERSION}/lib/. /usr/local/lib/ - sudo cp -a oneapi-tbb-${TBB_VERSION}/include/. /usr/local/include/ - - name: Prepare build - run: | - mkdir ${OSRM_BUILD_DIR} - ccache --max-size=256M - npm ci --ignore-scripts - if [[ "${ENABLE_COVERAGE}" == "ON" ]]; then - lcov --directory . --zerocounters # clean cached files - fi - echo "CC=${CCOMPILER}" >> $GITHUB_ENV - echo "CXX=${CXXCOMPILER}" >> $GITHUB_ENV - if [[ "${RUNNER_OS}" == "macOS" ]]; then - # missing from GCC path, needed for conan builds of libiconv, for example. - sudo xcode-select --switch /Library/Developer/CommandLineTools - echo "LIBRARY_PATH=${LIBRARY_PATH}:/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib" >> $GITHUB_ENV - echo "CPATH=${CPATH}:/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include" >> $GITHUB_ENV - fi - - - name: Build and install OSRM - run: | - echo "Using ${JOBS} jobs" - pushd ${OSRM_BUILD_DIR} - - if [[ "${ENABLE_CONAN}" == "ON" ]]; then - python3 -m venv .venv - source .venv/bin/activate - python3 -m pip install conan==2.15.1 - conan profile detect --force - fi - - ccache --zero-stats - cmake .. -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ - -DENABLE_CONAN=${ENABLE_CONAN:-OFF} \ - -DENABLE_ASSERTIONS=${ENABLE_ASSERTIONS:-OFF} \ - -DENABLE_CLANG_TIDY=${ENABLE_CLANG_TIDY:-OFF} \ - -DBUILD_SHARED_LIBS=${BUILD_SHARED_LIBS:-OFF} \ - -DENABLE_COVERAGE=${ENABLE_COVERAGE:-OFF} \ - -DENABLE_NODE_BINDINGS=${ENABLE_NODE_BINDINGS:-OFF} \ - -DENABLE_SANITIZER=${ENABLE_SANITIZER:-OFF} \ - -DENABLE_CCACHE=ON \ - -DENABLE_LTO=${ENABLE_LTO:-ON} \ - -DCMAKE_INSTALL_PREFIX=${OSRM_INSTALL_DIR} - - make --jobs=${JOBS} - - if [[ "${NODE_PACKAGE_TESTS_ONLY}" != "ON" ]]; then - make tests --jobs=${JOBS} - make benchmarks --jobs=${JOBS} - - sudo make install + + if [[ "${RUNNER_OS}" == "Linux" ]]; then + echo "JOBS=$((`nproc` + 1))" >> $GITHUB_ENV + elif [[ "${RUNNER_OS}" == "macOS" ]]; then + echo "JOBS=$((`sysctl -n hw.ncpu` + 1))" >> $GITHUB_ENV + fi + # See: https://github.com/actions/toolkit/issues/946#issuecomment-1590016041 + # We need it to be able to access system folders while restoring cached Boost below + - name: Give tar root ownership + if: runner.os == 'Linux' && matrix.ENABLE_CONAN != 'ON' + run: sudo chown root /bin/tar && sudo chmod u+s /bin/tar + + - name: Install boost + if: ${{ matrix.ENABLE_CONAN != 'ON' }} + uses: MarkusJx/install-boost@v2 + id: install-boost + with: + boost_version: 1.85.0 + + - name: Install dev dependencies + run: | + # workaround for issue that GitHub Actions seems to not adding it to PATH after https://github.com/actions/runner-images/pull/6499 + # and that's why CI cannot find conan executable installed above + if [[ "${RUNNER_OS}" == "macOS" ]]; then + echo "/Library/Frameworks/Python.framework/Versions/Current/bin" >> $GITHUB_PATH + fi + + # Update package list + if [[ "${RUNNER_OS}" == "Linux" || "${ENABLE_CONAN}" != "ON" ]]; then + sudo apt-get update -y + fi + + # Install compiler and ccache if [[ "${RUNNER_OS}" == "Linux" ]]; then - echo "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${OSRM_INSTALL_DIR}/lib" >> $GITHUB_ENV + sudo apt-get install -y ${CCOMPILER} ${CXXCOMPILER} ccache + elif [[ "${RUNNER_OS}" == "macOS" ]]; then + brew install ccache + fi + + # Linux dev packages + if [[ "${ENABLE_CONAN}" != "ON" ]]; then + sudo apt-get install -y libbz2-dev libxml2-dev libzip-dev liblua5.2-dev + if [[ "${ENABLE_COVERAGE}" == "ON" ]]; then + sudo apt-get install -y lcov + fi fi - echo "PKG_CONFIG_PATH=${OSRM_INSTALL_DIR}/lib/pkgconfig" >> $GITHUB_ENV - fi - popd - env: - Boost_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }} - - name: Run all tests - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY != 'ON' }} - run: | - make -C test/data benchmark - - # macOS SIP strips the linker path. Reset this inside the running shell - export LD_LIBRARY_PATH=${{ env.LD_LIBRARY_PATH }} - - # All tests assume to be run from the build directory - pushd ${OSRM_BUILD_DIR} - for i in ./unit_tests/*-tests ; do echo Running $i ; $i ; done - if [ -z "${ENABLE_SANITIZER}" ]; then + + # TBB + TBB_VERSION=2021.12.0 + if [[ "${RUNNER_OS}" == "Linux" ]]; then + TBB_URL="https://github.com/oneapi-src/oneTBB/releases/download/v${TBB_VERSION}/oneapi-tbb-${TBB_VERSION}-lin.tgz" + elif [[ "${RUNNER_OS}" == "macOS" ]]; then + TBB_URL="https://github.com/oneapi-src/oneTBB/releases/download/v${TBB_VERSION}/oneapi-tbb-${TBB_VERSION}-mac.tgz" + fi + wget --tries 5 ${TBB_URL} -O onetbb.tgz + tar zxvf onetbb.tgz + sudo cp -a oneapi-tbb-${TBB_VERSION}/lib/. /usr/local/lib/ + sudo cp -a oneapi-tbb-${TBB_VERSION}/include/. /usr/local/include/ + - name: Prepare build + run: | + mkdir ${OSRM_BUILD_DIR} + ccache --max-size=256M + npm ci --ignore-scripts + if [[ "${ENABLE_COVERAGE}" == "ON" ]]; then + lcov --directory . --zerocounters # clean cached files + fi + echo "CC=${CCOMPILER}" >> $GITHUB_ENV + echo "CXX=${CXXCOMPILER}" >> $GITHUB_ENV + if [[ "${RUNNER_OS}" == "macOS" ]]; then + # missing from GCC path, needed for conan builds of libiconv, for example. + sudo xcode-select --switch /Library/Developer/CommandLineTools + echo "LIBRARY_PATH=${LIBRARY_PATH}:/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib" >> $GITHUB_ENV + echo "CPATH=${CPATH}:/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include" >> $GITHUB_ENV + fi + + - name: Build and install OSRM + run: | + echo "Using ${JOBS} jobs" + pushd ${OSRM_BUILD_DIR} + + if [[ "${ENABLE_CONAN}" == "ON" ]]; then + python3 -m venv .venv + source .venv/bin/activate + python3 -m pip install conan==2.15.1 + conan profile detect --force + fi + + ccache --zero-stats + cmake .. -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ + -DENABLE_CONAN=${ENABLE_CONAN:-OFF} \ + -DENABLE_ASSERTIONS=${ENABLE_ASSERTIONS:-OFF} \ + -DENABLE_CLANG_TIDY=${ENABLE_CLANG_TIDY:-OFF} \ + -DBUILD_SHARED_LIBS=${BUILD_SHARED_LIBS:-OFF} \ + -DENABLE_COVERAGE=${ENABLE_COVERAGE:-OFF} \ + -DENABLE_NODE_BINDINGS=${ENABLE_NODE_BINDINGS:-OFF} \ + -DENABLE_SANITIZER=${ENABLE_SANITIZER:-OFF} \ + -DENABLE_CCACHE=ON \ + -DENABLE_LTO=${ENABLE_LTO:-ON} \ + -DCMAKE_INSTALL_PREFIX=${OSRM_INSTALL_DIR} + + make --jobs=${JOBS} + + if [[ "${NODE_PACKAGE_TESTS_ONLY}" != "ON" ]]; then + make tests --jobs=${JOBS} + make benchmarks --jobs=${JOBS} + + sudo make install + if [[ "${RUNNER_OS}" == "Linux" ]]; then + echo "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${OSRM_INSTALL_DIR}/lib" >> $GITHUB_ENV + fi + echo "PKG_CONFIG_PATH=${OSRM_INSTALL_DIR}/lib/pkgconfig" >> $GITHUB_ENV + fi + popd + env: + Boost_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }} + - name: Run all tests + if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY != 'ON' }} + run: | + make -C test/data benchmark + + # macOS SIP strips the linker path. Reset this inside the running shell + export LD_LIBRARY_PATH=${{ env.LD_LIBRARY_PATH }} + + # All tests assume to be run from the build directory + pushd ${OSRM_BUILD_DIR} + for i in ./unit_tests/*-tests ; do echo Running $i ; $i ; done + if [ -z "${ENABLE_SANITIZER}" ]; then + npm run nodejs-tests + fi + popd + npm test -- --parallel $JOBS + + - name: Use Node 20 + if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Run Node package tests on Node 20 + if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + run: | + node --version + npm run nodejs-tests + - name: Use Node 22 + if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Run Node package tests on Node 22 + if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + run: | + node --version + npm run nodejs-tests + - name: Use Node latest + if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + uses: actions/setup-node@v4 + with: + node-version: latest + - name: Run Node package tests on Node-latest + if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + run: | + node --version npm run nodejs-tests - fi - popd - npm test - - - name: Use Node 20 - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Run Node package tests on Node 20 - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - run: | - node --version - npm run nodejs-tests - - name: Use Node 22 - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Run Node package tests on Node 22 - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - run: | - node --version - npm run nodejs-tests - - name: Use Node latest - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - uses: actions/setup-node@v4 - with: - node-version: latest - - name: Run Node package tests on Node-latest - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - run: | - node --version - npm run nodejs-tests - - - name: Upload test logs - uses: actions/upload-artifact@v4 - if: failure() - with: - name: logs - path: test/logs/ - - - name: Build Node package - if: ${{ matrix.build_node_package }} - run: ./scripts/ci/node_package.sh - - name: Publish Node package - if: ${{ matrix.build_node_package && env.PUBLISH == 'On' }} - uses: ncipollo/release-action@v1 - with: - allowUpdates: true - artifactErrorsFailBuild: true - artifacts: build/stage/**/*.tar.gz - omitBody: true - omitBodyDuringUpdate: true - omitName: true - omitNameDuringUpdate: true - replacesArtifacts: true - token: ${{ secrets.GITHUB_TOKEN }} - - name: Show CCache statistics - run: | - ccache -p - ccache -s + + - name: Upload test logs + uses: actions/upload-artifact@v4 + if: failure() + with: + name: logs + path: test/logs/ + + - name: Build Node package + if: ${{ matrix.build_node_package }} + run: ./scripts/ci/node_package.sh + - name: Publish Node package + if: ${{ matrix.build_node_package && env.PUBLISH == 'On' }} + uses: ncipollo/release-action@v1 + with: + allowUpdates: true + artifactErrorsFailBuild: true + artifacts: build/stage/**/*.tar.gz + omitBody: true + omitBodyDuringUpdate: true + omitName: true + omitNameDuringUpdate: true + replacesArtifacts: true + token: ${{ secrets.GITHUB_TOKEN }} + - name: Show CCache statistics + run: | + ccache -p + ccache -s ci-complete: runs-on: ubuntu-latest needs: [build-matrix, conan-windows-release-node, docker-image-matrix] steps: - - run: echo "CI complete" + - run: echo "CI complete" diff --git a/CHANGELOG.md b/CHANGELOG.md index d72f6cf56f6..1696ac6ecfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Profiles: - ADDED: Use `is_sidepath:of:name` and `street:name` as fallback names for unnamed sidewalks and sidepaths in foot and bicycle profiles [#7259](https://github.com/Project-OSRM/osrm-backend/issues/7259) - Build: + - CHANGED: Cucumber tests now can run in parallel and other improvements [#7309](https://github.com/Project-OSRM/osrm-backend/issues/7309) - FIXED: Update Node.js binding path from `lib/binding` to `lib/binding_napi_v8` to match node-pre-gyp versioning conventions [#7272](https://github.com/Project-OSRM/osrm-backend/pull/7272) - FIXED: Reduce MSVC compiler warnings by suppressing informational warnings while preserving bug-indicating warnings [#7253](https://github.com/Project-OSRM/osrm-backend/issues/7253) - FIXED: Merge `osrm_extract` and `osrm_guidance` to avoid circular dependencies. [#7315](https://github.com/Project-OSRM/osrm-backend/pull/7315) diff --git a/cucumber.mjs b/cucumber.mjs index f68162abb90..52d70818958 100644 --- a/cucumber.mjs +++ b/cucumber.mjs @@ -1,32 +1,81 @@ -// Default profile -export default { - strict: true, - tags: 'not @stress and not @todo and not @mld', - import: ['features/support', 'features/step_definitions'], -}; +// See: https://github.com/cucumber/cucumber-js/blob/main/docs/profiles.md -// Additional profiles -export const ch = { - strict: true, - tags: 'not @stress and not @todo and not @mld', - format: ['progress'], - import: ['features/support', 'features/step_definitions'], -}; +export default function() { + const penv = process.env; -export const todo = { - strict: true, - tags: '@todo', - import: ['features/support', 'features/step_definitions'], -}; + function int(s, def) { + return (s && parseInt(s)) || def; + } -export const all = { - strict: true, - import: ['features/support', 'features/step_definitions'], -}; + function str(s, def) { + return s || def; + } + + const commonWorldParameters = { + httpTimeout: int(penv.CUCUMBER_HTTP_TIMEOUT, 2000), // must be less than default timeout + testPath: str(penv.CUCUMBER_TEST_PATH, 'test'), + profilesPath: str(penv.CUCUMBER_PROFILES_PATH, 'profiles'), + logsPath: str(penv.CUCUMBER_LOGS_PATH, 'test/logs'), + cachePath: str(penv.CUCUMBER_CACHE_PATH, 'test/cache'), + buildPath: str(penv.OSRM_BUILD_DIR, 'build'), + loadMethod: str(penv.OSRM_LOAD_METHOD, 'datastore'), + algorithm: str(penv.OSRM_ALGORITHM, 'ch'), + port: int(penv.OSRM_PORT, 5000), + ip: str(penv.OSRM_IP, '127.0.0.1'), + } + + const baseConfig = { + strict: true, + import: [ + 'features/support/', + 'features/step_definitions/', + 'features/lib/' + ], + worldParameters : commonWorldParameters, + tags: 'not @stress and not @todo', + } + + function wp(worldParameters) { + return { worldParameters: worldParameters }; + } + + const htmlReportFilename = `test/logs/cucumber-${process.env.algorithm}-${process.env.loadmethod}.report.html` + + return { + // Default profile + default: { + ... baseConfig, + }, + + // base configs + home: { + ... baseConfig, + format: [ + 'progress-bar', + ['html', htmlReportFilename] + ], + }, + github: { + ... baseConfig, + format: [ + './features/lib/github_summary_formatter.js', + ['html', htmlReportFilename] + ], + publish: true + }, + + // patches to base configs + stress: { tags: '@stress'}, + todo: { tags: '@todo'}, + all: { tags: '' }, + + // algorithms + ch: wp({algorithm: 'ch'}), + mld: wp({algorithm: 'mld'}), -export const mld = { - strict: true, - tags: 'not @stress and not @todo and not @ch', - format: ['progress'], - import: ['features/support', 'features/step_definitions'], + // data load methods + datastore: wp({loadMethod: 'datastore'}), + directly: wp({loadMethod: 'directly'}), + mmap: wp({loadMethod: 'mmap'}), + } }; diff --git a/docs/cucumber.md b/docs/cucumber.md new file mode 100644 index 00000000000..888db6dfadd --- /dev/null +++ b/docs/cucumber.md @@ -0,0 +1,150 @@ +# Cucumber + +This documentation describes the technical aspects of our cucumber test suite. + +- See also: [on how to write Cucumber tests](testing.md). +- See also: [about Cucumber in the OSRM wiki](https://github.com/Project-OSRM/osrm-backend/wiki/Cucumber-Test-Suite). +- See also: [the Cucumber docs](https://github.com/cucumber/cucumber-js/tree/main/docs). + +## tl;dr + +Run the Cucumber tests with: + +``` bash +$ npm test -- --parallel 16 +``` + +## Single OSRM Configuration + +An OSRM configuration consists of a *routing algorithm* and a *data load method*. OSRM +currently supports the routing algorithms: +- `ch` (Contraction Hierarchy), and +- `mld` (Multi-Level-Dijkstra) + +and the data load methods: + +- `directly` (load the files into memory), +- `mmap` (use memory mapped files), and +- `datastore` (use shared memory). + +To test all scenarios with a single OSRM configuration, say: + +``` bash +$ npx cucumber-js -p home -p mld -p mmap --parallel 8 --fail-fast +``` +Explanations follow: + +### Profiles + +Profiles are chosen with the `-p` commandline argument. Cucumber profiles allow you to +change multiple configuration items with just one commandline argument. If you set +more than one profile they are all merged into one configuration. + +Note: Cucumber profiles should not be confused with OSRM profiles. Cucumber profiles +are defined in `cucumber.mjs`. OSRM profiles reside in the `profiles/*.lua` files. + +Our implementation offers following stock profiles. You should always use one base +profile followed by zero or more additional profiles. + +| Name | | +| --------- | -------------------------------------------------------------- | +| home | Base profile to use on a developer machine | +| github | Base profile to use on the github CI server | +| ch | Additional profile that selects the CH algorithm | +| mld | Additional profile that selects the MLD algorithm | +| mmap | Additional profile that selects the mmap data load method | +| directly | Additional profile that selects the directly data load method | +| datastore | Additional profile that selects the datastore data load method | +| stress | Additional profile that selects only @stress tests | +| todo | Additional profile that selects only @todo tests | +| all | Additional profile that selects all tests | + +### Arguments + +Here is a [description of all +arguments](https://github.com/cucumber/cucumber-js/blob/main/docs/configuration.md#options) +you can pass to Cucumber. The interesting ones probably are: `--fail-fast`, `--format`, +`--parallel`, and `--tags`. + +Note: when using `--parallel N` make sure there are `N` contiguous free ports at the +configured port number (eg. at ports 5000--5000+N). + +## All OSRM Configurations + +We provide a shortcut to run all 6 configurations: + +``` bash +$ npm test -- --parallel 16 +``` +This is how the tests are run on the CI server. You can pass the same arguments as +mentioned above. + +## Cache + +To speed up subsequent runs with the same parameters, the files generated by Cucumber +and the by the OSRM extraction chain are held in a cache directory. This cache is +located by default in `test/cache` and should be cleaned periodically: + +``` bash +$ rm -rf test/cache +``` + +## Configuration + +The whole configuration is done in `cucumber.mjs`. You can either edit `worldParameters` +in `cucumber.mjs` or use environment variables to override single defaults. + +| worldParameters | Environment Variable | Defaults to | | +| --------------- | ------------------------ | ------------ | ----------------------- | +| | `CUCUMBER_TIMEOUT` | 5000 | Scenario timeout in ms. | +| `httpTimeout` | `CUCUMBER_HTTP_TIMEOUT` | 2000 | HTTP timeout in ms. | +| `testPath` | `CUCUMBER_TEST_PATH` | `test` | The test directory | +| `profilesPath` | `CUCUMBER_PROFILES_PATH` | `profiles` | The profiles directory | +| `logsPath` | `CUCUMBER_LOGS_PATH` | `test/logs` | The logs directory | +| `cachePath` | `CUCUMBER_CACHE_PATH` | `test/cache` | The cache directory | +| `buildPath` | `OSRM_BUILD_DIR` | `build` | Path to the binaries | +| `loadMethod` | `OSRM_LOAD_METHOD` | datastore | Data load method | +| `algorithm` | `OSRM_ALGORITHM` | ch | Routing algorithm | +| `ip` | `OSRM_IP` | 127.0.0.1 | IP Address | +| `port` | `OSRM_PORT` | 5000 | IP Port | + +The default Cucumber timeout can be changed by setting the environment variable +`CUCUMBER_TIMEOUT`. This is discouraged, because the default timeout of 5 seconds is +plenty for the problem sizes we are dealing with. The probable reasons for a test timing +out are that `osrm-routed` died or that sync between `osrm-datastore` and `osrm-routed` +was lost. + +### Other environment variables + +`OSRM_RASTER_SOURCE` is set by 'Given the raster source' and is supposed to be read back +in your `profiles/*.lua` profile by `os.getenv('OSRM_RASTER_SOURCE')`. + +`OSRM_PROFILE` See: [Pull Request #4516](https://github.com/Project-OSRM/osrm-backend/pull/4516) + + +## Tags + +Single scenarios or whole feature files can be tagged. Tag names can be selected +arbitrarily although it is best to conform to the tags already used. Eg. the tag +`@guidance` can be used to run only those tests related to the guidance feature: + +``` bash +$ npm test -- --parallel 16 --tags @guidance +``` + +We also support following special tags: + +| Tag | A scenario thus tagged ... | +| ----------------------------------- | ------------------------------------------------------------ | +| `@isolated` | will not run while any other scenario is running in parallel | +| `@with_(datastore\|directly\|mmap)` | will be executed iff the load method matches | +| `@no_(datastore\|directly\|mmap)` | will be executed unless the load method matches | +| `@with_(ch\|mld)` | will be executed iff the algorithm matches | +| `@no_(ch\|mld)` | will be executed unless the algorithm matches | +| `@with_(linux\|darwin\|win32)` | will be executed iff the OS matches | +| `@no_(linux\|darwin\|win32)` | will be executed unless the OS matches | + +A test that calls `osrm-datastore --spring-clean` should not run concurrently with any +other test, thus the tag `@isolated` should be applied. A test that runs or kills +`osrm-routed` should not run while testing the datastore load method, and thus should be +labeled with the tag `@no_datastore`. diff --git a/features/lib/github_summary_formatter.js b/features/lib/github_summary_formatter.js new file mode 100644 index 00000000000..e4e143bce00 --- /dev/null +++ b/features/lib/github_summary_formatter.js @@ -0,0 +1,84 @@ +import fs from 'node:fs'; +import { SummaryFormatter } from '@cucumber/cucumber'; + +const FAILURES = ['FAILED', 'AMBIGUOUS', 'UNDEFINED']; +const WARNINGS = ['SKIPPED', 'PENDING', 'UNKNOWN']; +// const SUCCESSES = ['PASSED']; + +function isFailure(result) { + return FAILURES.includes(result.status); +} +function isWarning(result) { + return WARNINGS.includes(result.status); +} +// function isSuccess(result) { +// return SUCCESSES.includes(result.status); +// } +function toSeconds(timeStamp) { + return +timeStamp.seconds + timeStamp.nanos / 1e9; +} + +/** + * A Summary Formatter that also appends to $GITHUB_STEP_SUMMARY + * + * This formatter first calls the stock summary formatter, then appends one line of + * MarkDown to the file denoted by $GITHUB_STEP_SUMMARY. + * + * The hacky part is that we have to *append* to $GITHUB_STEP_SUMMARY. By using the + * obvious: + * + * --format github_summary_formatter:"$GITHUB_STEP_SUMMARY" + * + * we would *overwrite* that file. + * + * Another thing to consider is that Cucumber only allows one formatter that grabs + * stdout. In order to avoid the clumsy: + * + * --format summary --format github_summary_formatter:"/dev/null" + * + * this formatter also does the duty of the stock summary formatter: + * + * --format github_summary_formatter + * + * Override the default filename of: $GITHUB_STEP_SUMMARY + * + * formatOptions: { 'github_summary_formatter': { filename: 'somewhere.else' }}, + */ + +export default class GithubSummaryFormatter extends SummaryFormatter { + static documentation = 'A Summary Formatter that also appends a $GITHUB_STEP_SUMMARY'; + constructor(options) { + super(options); + this.filename = options.parsedArgvOptions?.github_summary_formatter?.filename + ?? process.env.GITHUB_STEP_SUMMARY; + } + + logSummary(testRunDuration) { + // because Cucumber only allows one formatter thats grab stdout, + // we have to mimic the summary formatter first + super.logSummary(testRunDuration); + if (!this.filename) + return; + + let failures = 0; + let warnings = 0; + let successes = 0; + + for (const { worstTestStepResult } of this.eventDataCollector.getTestCaseAttempts()) { + if (isFailure(worstTestStepResult)) { + ++failures; + } else if (isWarning(worstTestStepResult)) { + ++warnings; + } else { + ++successes; + } + }; + if (failures > 0) { + failures = `:x: ${failures}`; + } + const seconds = toSeconds(testRunDuration).toFixed(3); + const msg = `| ${successes} | ${warnings} | ${failures} | ${seconds} |\n`; + + fs.appendFileSync(this.filename, msg); + } +} diff --git a/features/lib/osrm_loader.js b/features/lib/osrm_loader.js index b0c709c621f..6cb926dbfc1 100644 --- a/features/lib/osrm_loader.js +++ b/features/lib/osrm_loader.js @@ -1,310 +1,247 @@ -// OSRM binary process management and data loading strategies (datastore, mmap, direct) -import fs from 'fs'; -import util from 'util'; -import { Timeout, errorReason } from './utils.js'; -import tryConnect from './try_connect.js'; +// osrm-routed process management and data loading strategies (datastore, mmap, direct) +import child_process from 'node:child_process'; -// Base class for managing OSRM routing server process lifecycle +import waitOn from 'wait-on'; + +import { env } from '../support/env.js'; +import { runBinSync, mkBinPath } from '../support/run.js'; + +/** + * A class for running osrm-routed. Subclasses implement alternate ways of data loading. + */ class OSRMBaseLoader { - constructor(scope) { - this.scope = scope; + constructor() { this.child = null; } - // Starts OSRM server and waits for it to accept connections - launch(callback) { - const limit = Timeout(this.scope.TIMEOUT, { - err: new Error('*** Launching osrm-routed timed out.'), - }); + /** + * Starts OSRM server and waits for it to accept connections + * + * @param log A log function. Inside a scenario we can log to the world.log() + * function, but world.log() cannot be used between scenarios. We must log + * long-running processes to the global log. + */ + spawn(scenario, args, log) { + if (this.osrmIsRunning()) + throw new Error('osrm-routed already running!'); - const runLaunch = (cb) => { - this.osrmUp(() => { - this.waitForConnection(cb); - }); - }; + const cmd = mkBinPath('osrm-routed'); + const argsAsString = args.join(' '); + log(`running ${cmd} ${argsAsString}`); - runLaunch( - limit((e) => { - if (e) callback(e); - else callback(); - }), + this.child = child_process.spawn( + cmd, + args, + { env : scenario.environment }, ); - } - - // Terminates OSRM server process gracefully - shutdown(callback) { - if (!this.osrmIsRunning()) return callback(); - const limit = Timeout(this.scope.TIMEOUT, { - err: new Error('*** Shutting down osrm-routed timed out.'), + this.child.on('exit', (code) => { + log(`osrm-routed completed with exit code ${code}`); + this.child = null; }); - this.osrmDown(limit(callback)); + return this.waitForConnection(); } - osrmIsRunning() { - return this.child && !this.child.killed; - } - - osrmDown(callback) { + // Terminates OSRM server process gracefully + kill() { if (this.osrmIsRunning()) { - this.child.on('exit', (_code, _signal) => { - callback(); + const child = this.child; + const p = new Promise((resolve) => { + child.on('exit', resolve); }); - this.child.kill('SIGINT'); - } else callback(); + child.kill(); + return p; + } + return Promise.resolve(); } - waitForConnection(callback) { - let retryCount = 0; - const retry = (err) => { - if (err) { - if (retryCount < this.scope.OSRM_CONNECTION_RETRIES) { - const timeoutMs = - 10 * - Math.pow(this.scope.OSRM_CONNECTION_EXP_BACKOFF_COEF, retryCount); - retryCount++; - setTimeout(() => { - tryConnect(this.scope.OSRM_IP, this.scope.OSRM_PORT, retry); - }, timeoutMs); - } else { - callback( - new Error( - `Could not connect to osrm-routed after ${this.scope.OSRM_CONNECTION_RETRIES} retries.`, - ), - ); - } - } else { - callback(); - } + /** Returns a promise resolved when the server is up. */ + waitForConnection() { + const seconds = 10; + const waitOptions = { + resources: [`tcp:${env.wp.ip}:${env.wp.port}`], + delay: 10, // initial delay in ms + interval: 10, // poll interval in ms + timeout: seconds * 1000, // timeout in ms }; - - tryConnect(this.scope.OSRM_IP, this.scope.OSRM_PORT, retry); - } -} - -// Loads data directly from .osrm files into memory -class OSRMDirectLoader extends OSRMBaseLoader { - constructor(scope) { - super(scope); + const p = waitOn(waitOptions); + p.catch(() => { throw new Error( + `Could not connect to osrm-routed after ${seconds} s.` + ); }); + return p; } - load(ctx, callback) { - this.inputFile = ctx.inputFile; - this.loaderArgs = ctx.loaderArgs; - this.shutdown(() => { - this.launch(callback); - }); + osrmIsRunning() { + return this.child; } - osrmUp(callback) { - if (this.osrmIsRunning()) - return callback(new Error('osrm-routed already running!')); + // public interface - const command_arguments = util.format( - '%s -p %d -i %s -a %s %s', - this.inputFile, - this.scope.OSRM_PORT, - this.scope.OSRM_IP, - this.scope.ROUTING_ALGORITHM, - this.loaderArgs, - ); - this.child = this.scope.runBin( - 'osrm-routed', - command_arguments, - this.scope.environment, - (err) => { - if (err && err.signal !== 'SIGINT') { - this.child = null; - throw new Error( - util.format('osrm-routed %s: %s', errorReason(err), err.cmd), - ); - } - }, - ); - - this.child.readyFunc = (data) => { - if (/running and waiting for requests/.test(data)) { - this.child.stdout.removeListener('data', this.child.readyFunc); - callback(); - } - }; - this.child.stdout.on('data', this.child.readyFunc); + /** Called at the init of the cucumber run */ + beforeAll() { return Promise.resolve(); } + /** Called at the start of each scenario */ + before(_scenario) { + return Promise.resolve(); + } + /** Called at the end of each scenario */ + after(_scenario) { + return this.kill(); } + /** Called at the end of the cucumber run */ + afterAll() { return Promise.resolve(); } } -// Uses memory-mapped files for efficient data access -class OSRMmmapLoader extends OSRMBaseLoader { - constructor(scope) { - super(scope); +/** This loader tells osrm-routed to load data directly from .osrm files into memory */ +export class OSRMDirectLoader extends OSRMBaseLoader { + before(scenario) { + return this.spawn(scenario, [ + scenario.osrmCacheFile, + '-p', + env.wp.port, + '-i', + env.wp.ip, + '-a', + env.wp.algorithm.toUpperCase(), + ].concat(scenario.loaderArgs), + scenario.log); } +} - load(ctx, callback) { - this.inputFile = ctx.inputFile; - this.loaderArgs = ctx.loaderArgs; - this.shutdown(() => { - this.launch(callback); - }); +/** This loader tells osrm-routed to use memory-mapped files. */ +export class OSRMmmapLoader extends OSRMBaseLoader { + before(scenario) { + return this.spawn(scenario, [ + scenario.osrmCacheFile, + '-p', + env.wp.port, + '-i', + env.wp.ip, + '-a', + env.wp.algorithm.toUpperCase(), + '--mmap', + ].concat(scenario.loaderArgs), + scenario.log); } +} - osrmUp(callback) { - if (this.osrmIsRunning()) - return callback(new Error('osrm-routed already running!')); - - const command_arguments = util.format( - '%s -p %d -i %s -a %s --mmap %s', - this.inputFile, - this.scope.OSRM_PORT, - this.scope.OSRM_IP, - this.scope.ROUTING_ALGORITHM, - this.loaderArgs, +/** + * This loader keeps one and the same osrm-routed running for the whole cucumber run. It + * uses osrm-datastore to load new data into osrm-routed. + */ +export class OSRMDatastoreLoader extends OSRMBaseLoader { + constructor() { + super(); + this.current_scenario = null; + } + + /** + * Custom log function + * + * For long-running osrm-routed switch the log to the current scenario + */ + async logSync(msg) { + if (this.current_scenario) + await this.current_scenario.log(msg); + else + env.globalLog(msg); + } + + before(scenario) { + this.current_scenario = scenario; + this.semaphore = new Promise((resolve) => this.resolve = resolve); + runBinSync( + 'osrm-datastore', + [ + '--dataset-name', + env.DATASET_NAME, + scenario.osrmCacheFile, + ].concat(scenario.loaderArgs), + { env : scenario.environment }, + scenario.log ); - this.child = this.scope.runBin( - 'osrm-routed', - command_arguments, - this.scope.environment, - (err) => { - if (err && err.signal !== 'SIGINT') { - this.child = null; - throw new Error( - util.format('osrm-routed %s: %s', errorReason(err), err.cmd), - ); - } - }, + + if (this.osrmIsRunning()) + // When osrm-datastore exits, then osrm-routed may not yet have switched facades. + // It would right now answer the query from the old dataset. We return a promise + // that is resolved when osrm-routed outputs "updated facade to regions ...". + return this.semaphore; + + // workaround for annoying misfeature: if there are no datastores osrm-routed + // chickens out, so we cannot just start osrm-routed in beforeAll where it naturally + // belonged, but must load a datastore first. + + const args = [ + '--shared-memory', + '--dataset-name', + env.DATASET_NAME, + '-p', + env.wp.port, + '-i', + env.wp.ip, + '-a', + env.wp.algorithm.toUpperCase(), + ]; // .concat(scenario.loaderArgs)); + + const cmd = mkBinPath('osrm-routed'); + const argsAsString = args.join(' '); + this.logSync(`running ${cmd} ${argsAsString}`); + + this.child = child_process.spawn( + cmd, + args, + { env : scenario.environment }, ); - this.child.readyFunc = (data) => { - if (/running and waiting for requests/.test(data)) { - this.child.stdout.removeListener('data', this.child.readyFunc); - callback(); + this.child.stdout.on('data', (data) => { + if (data.includes('updated facade')) { + this.logSync('Facade updated and promise resolved'); + this.resolve(); } - }; - this.child.stdout.on('data', this.child.readyFunc); - } -} - -// Loads data into shared memory for multiple processes to access -class OSRMDatastoreLoader extends OSRMBaseLoader { - constructor(scope) { - super(scope); - } + }); - load(ctx, callback) { - this.inputFile = ctx.inputFile; - this.loaderArgs = ctx.loaderArgs; + // we MUST consume these or the osrm-routed process will block eventually + this.child.stderr.on('data', (data) => this.logSync(`osrm-routed stderr:\n${data}`)); + this.child.stdout.on('data', (data) => this.logSync(`osrm-routed stdout:\n${data}`)); - this.loadData((err) => { - if (err) return callback(err); - if (!this.osrmIsRunning()) this.launch(callback); - else { - this.scope.setupOutputLog( - this.child, - fs.createWriteStream(this.scope.scenarioLogFile, { flags: 'a' }), - ); - callback(); + this.child.on('exit', (code, signal) => { + this.child = null; + if (signal != null) { + const msg = `osrm-routed aborted with signal ${signal}`; + this.logSync(msg); + // throw new Error(msg); + } + if (code != null) { + const msg = `osrm-routed completed with exit code ${code}`; + this.logSync(msg); + if (code != 0) + throw new Error(msg); } }); + return this.waitForConnection(); } - - loadData(callback) { - const command_arguments = util.format( - '--dataset-name=%s %s %s', - this.scope.DATASET_NAME, - this.inputFile, - this.loaderArgs, - ); - this.scope.runBin( - 'osrm-datastore', - command_arguments, - this.scope.environment, - (err) => { - if (err) - return callback( - new Error(`*** osrm-datastore exited with ${err.code}: ${err}`), - ); - callback(); - }, - ); + after () { + this.current_scenario = null; + return Promise.resolve(); } - - osrmUp(callback) { - if (this.osrmIsRunning()) return callback(); - - const command_arguments = util.format( - '--dataset-name=%s -s -i %s -p %d -a %s', - this.scope.DATASET_NAME, - this.scope.OSRM_IP, - this.scope.OSRM_PORT, - this.scope.ROUTING_ALGORITHM, - ); - this.child = this.scope.runBin( - 'osrm-routed', - command_arguments, - this.scope.environment, - (err) => { - if (err && err.signal !== 'SIGINT') { - this.child = null; - throw new Error( - util.format('osrm-routed %s: %s', errorReason(err), err.cmd), - ); - } - }, - ); - - // we call the callback here, becuase we don't want to wait for the child process to finish - callback(); + afterAll () { + this.current_scenario = null; + return this.kill(); } } -class OSRMLoader { - constructor(scope) { - this.scope = scope; - this.sharedLoader = new OSRMDatastoreLoader(this.scope); - this.directLoader = new OSRMDirectLoader(this.scope); - this.mmapLoader = new OSRMmmapLoader(this.scope); - this.method = scope.DEFAULT_LOAD_METHOD; - } - - load(inputFile, callback) { - if (!this.loader) { - this.loader = { shutdown: (cb) => cb() }; - } - if (this.method === 'datastore') { - this.loader.shutdown((err) => { - if (err) return callback(err); - this.loader = this.sharedLoader; - this.sharedLoader.load(inputFile, callback); - }); - } else if (this.method === 'directly') { - this.loader.shutdown((err) => { - if (err) return callback(err); - this.loader = this.directLoader; - this.directLoader.load(inputFile, callback); - }); - } else if (this.method === 'mmap') { - this.loader.shutdown((err) => { - if (err) return callback(err); - this.loader = this.mmapLoader; - this.mmapLoader.load(inputFile, callback); - }); - } else { - callback(new Error(`*** Unknown load method ${method}`)); - } - } - - setLoadMethod(method) { - this.method = method; - } - - shutdown(callback) { - if (!this.loader) return callback(); - - this.loader.shutdown(callback); - } - - up() { - return this.loader ? this.loader.osrmIsRunning() : false; - } +/** throws error if osrm-routed is up */ +// FIXME: don't bother if osrm-routed is already running, just use the next port +export function testOsrmDown() { + const host = `${env.wp.ip}:${env.wp.port}`; + const waitOptions = { + resources: [`tcp:${host}`], + delay: 0, // initial delay in ms + interval: 100, // poll interval in ms + timeout: 1000, // timeout in ms + reverse: true, + }; + return waitOn(waitOptions).catch(() => { throw new Error( + `osrm-routed is already running on ${host}.` + );}); } - -export default OSRMLoader; diff --git a/features/lib/try_connect.js b/features/lib/try_connect.js deleted file mode 100644 index 7c57b31b13d..00000000000 --- a/features/lib/try_connect.js +++ /dev/null @@ -1,14 +0,0 @@ -// Network connectivity testing utility for checking if OSRM server is ready -import net from 'net'; - -// Attempts TCP connection to test if server is accepting connections -export default function tryConnect(host, port, callback) { - net - .connect({ port, host }) - .on('connect', () => { - callback(); - }) - .on('error', () => { - callback(new Error('Could not connect.')); - }); -} diff --git a/features/lib/utils.js b/features/lib/utils.js index 028557c6809..03c3c59a112 100644 --- a/features/lib/utils.js +++ b/features/lib/utils.js @@ -1,45 +1,28 @@ -// General utility functions for timeouts, decimal formatting, and file operations -import { mkdir } from 'fs/promises'; +// General utility functions for decimal formatting, and file operations +import fs from 'fs'; +import child_process from 'child_process'; -// Creates timeout wrapper that calls callback with error if operation exceeds time limit -function Timeout(ms, options) { - return function (cb) { - let called = false; - const timer = setTimeout(() => { - if (!called) { - called = true; - cb(options.err || new Error(`Operation timed out after ${ms}ms`)); - } - }, ms); - - return function (...args) { - if (!called) { - called = true; - clearTimeout(timer); - cb(...args); - } - }; - }; -} - -// Creates directory recursively, callback-style wrapper for mkdir -function createDir(dir, callback) { - mkdir(dir, { recursive: true }) - .then(() => callback(null)) - .catch((err) => callback(err)); -} - -// Ensures numeric values have decimal point for OSM XML compatibility +/** Ensures numeric values have decimal point for OSM XML compatibility */ const ensureDecimal = (i) => { if (parseInt(i) === i) return i.toFixed(1); else return i; }; -// Formats error information from child process exits -const errorReason = (err) => { - return err.signal - ? `killed by signal ${err.signal}` - : `exited with code ${err.code}`; -}; +/** + * Returns a promise that all osrm binaries are present and in good working order. + * @param {Env} env + */ +function verifyExistenceOfBinaries(env) { + for (const binPath of env.requiredBinaries) { + if (!fs.existsSync(binPath)) { + return Promise.reject(new Error(`${binPath} is missing. Build failed?`)); + } + const res = child_process.spawnSync(binPath, ['--help']); + if (res.error) { + return Promise.reject(res.error); + }; + }; + return Promise.resolve(); +} -export { createDir, ensureDecimal, errorReason, Timeout }; +export { ensureDecimal, verifyExistenceOfBinaries }; diff --git a/features/options/datastore/datastore.feature b/features/options/datastore/datastore.feature index 30d19615483..f8cc4aaff29 100644 --- a/features/options/datastore/datastore.feature +++ b/features/options/datastore/datastore.feature @@ -1,33 +1,63 @@ -@datastore @options @help +@datastore @options @help @isolated @no_datastore Feature: osrm-datastore command line options Background: Given the profile "testbot" - And the node map + + Scenario: osrm-datastore - Help should be shown when no options are passed + Given the node map """ a b """ And the ways | nodes | | ab | + And the data has been extracted And the data has been contracted - Scenario: osrm-datastore - Help should be shown when no options are passed - When I try to run "osrm-datastore --dataset-name test_dataset_42 {processed_file}" + When I try to run "osrm-datastore" + Then stderr should contain "the argument for" + And it should exit successfully + + Scenario: osrm-datastore - Updates should work + Given the node map + """ + a b + """ + And the ways + | nodes | + | ab | + And the data has been extracted + And the data has been contracted + + When I run "osrm-datastore --spring-clean" with input "Y" + And I try to run "osrm-datastore --dataset-name cucumber/updates_test {processed_file}" Then it should exit successfully When I try to run "osrm-datastore --list" Then it should exit successfully - And stdout should contain "test_dataset_42/static" - And stdout should contain "test_dataset_42/updatable" + And stdout should contain "cucumber/updates_test/static" + And stdout should contain "cucumber/updates_test/updatable" Scenario: osrm-datastore - Only metric update should work + Given the node map + """ + a b + """ + And the ways + | nodes | + | ab | + And the data has been extracted + And the data has been partitioned + And the data has been customized + + When I run "osrm-datastore --spring-clean" with input "Y" + And I try to run "osrm-datastore {processed_file} --dataset-name cucumber/only_metric_test" + Then it should exit successfully + Given the speed file """ 0,1,50 """ - And the data has been extracted - When I try to run "osrm-datastore {processed_file} --dataset-name cucumber/only_metric_test" - Then it should exit successfully When I try to run "osrm-customize --segment-speed-file {speeds_file} {processed_file}" Then it should exit successfully When I try to run "osrm-datastore {processed_file} --dataset-name cucumber/only_metric_test --only-metric" diff --git a/features/options/routed/files.feature b/features/options/routed/files.feature index d7c329b0dbf..e319e66afb0 100644 --- a/features/options/routed/files.feature +++ b/features/options/routed/files.feature @@ -1,9 +1,9 @@ -@routed @options @files @todo +@routed @options @files @todo @no_datastore Feature: osrm-routed command line options: files # Normally when launching osrm-routed, it will keep running as a server until it's shut down. # For testing program options, the --trial option is used, which causes osrm-routed to quit # immediately after initialization. This makes testing easier and faster. -# +# # The {contracted_base} part of the options to osrm-routed will be expanded to the actual base path of # the contracted input file. diff --git a/features/options/routed/invalid.feature b/features/options/routed/invalid.feature index 3c82e9437a2..14fbca725c6 100644 --- a/features/options/routed/invalid.feature +++ b/features/options/routed/invalid.feature @@ -1,4 +1,4 @@ -@routed @options @invalid +@routed @options @invalid @no_datastore Feature: osrm-routed command line options: invalid options Background: diff --git a/features/options/routed/version.feature b/features/options/routed/version.feature index 5f7531b7c21..bfe1765dfbc 100644 --- a/features/options/routed/version.feature +++ b/features/options/routed/version.feature @@ -1,4 +1,4 @@ -@routed @options @version +@routed @options @version @no_datastore Feature: osrm-routed command line options: version # the regex will match these two formats: # v0.3.7.0 # this is the normal format when you build from a git clone @@ -6,7 +6,7 @@ Feature: osrm-routed command line options: version Background: Given the profile "testbot" - + Scenario: osrm-routed - Version, short When I run "osrm-routed -v" Then stderr should be empty diff --git a/features/step_definitions/data.js b/features/step_definitions/data.js index 140393240b7..8a09a1636ca 100644 --- a/features/step_definitions/data.js +++ b/features/step_definitions/data.js @@ -5,12 +5,7 @@ import fs from 'fs'; import d3 from 'd3-queue'; import * as OSM from '../lib/osm.js'; import { Given } from '@cucumber/cucumber'; - -Given(/^the profile "([^"]*)"$/, function (profile, callback) { - this.profile = this.OSRM_PROFILE || profile; - this.profileFile = path.join(this.PROFILES_PATH, `${this.profile}.lua`); - callback(); -}); +import { env } from '../support/env.js'; Given(/^the extract extra arguments "(.*?)"$/, function (args, callback) { this.extractArgs = this.expandOptions(args); @@ -66,9 +61,7 @@ Given(/^the shortcuts$/, function (table, callback) { }); Given(/^the node map$/, function (docstring, callback) { - const q = d3.queue(); - - const addNode = (name, ri, ci, cb) => { + const addNode = (name, ri, ci) => { const lonLat = this.tableCoordToLonLat(ci, ri); if (name.match(/[a-z]/)) { if (this.nameNodeHash[name]) @@ -79,24 +72,21 @@ Given(/^the node map$/, function (docstring, callback) { throw new Error(util.format('*** duplicate node %s', name)); this.addLocation(name, lonLat[0], lonLat[1], null); } - cb(); }; docstring.split(/\n/).forEach((row, ri) => { row.split('').forEach((cell, ci) => { if (cell.match(/[a-z0-9]/)) { - q.defer(addNode, cell, ri, ci * 0.5); + addNode (cell, ri, ci * 0.5); } }); }); - q.awaitAll(callback); + callback(); }); Given(/^the node locations$/, function (table, callback) { - const q = d3.queue(); - - const addNodeLocations = (row, cb) => { + const addNodeLocations = (row) => { const name = row.node; if (this.findNodeByName(name)) throw new Error(util.format('*** duplicate node %s', name)); @@ -107,19 +97,15 @@ Given(/^the node locations$/, function (table, callback) { } else { this.addLocation(name, row.lon, row.lat); } - - cb(); }; - table.hashes().forEach((row) => q.defer(addNodeLocations, row)); + table.hashes().forEach(addNodeLocations); - q.awaitAll(callback); + callback(); }); Given(/^the nodes$/, function (table, callback) { - const q = d3.queue(); - - const addNode = (row, cb) => { + const addNode = (row) => { const name = row.node, node = this.findNodeByName(name); delete row.node; @@ -131,12 +117,11 @@ Given(/^the nodes$/, function (table, callback) { node.addTag(key, row[key]); } } - cb(); }; - table.hashes().forEach((row) => q.defer(addNode, row)); + table.hashes().forEach(addNode); - q.awaitAll(callback); + callback(); }); Given( @@ -152,9 +137,9 @@ Given( const addWay = (row, cb) => { const way = new OSM.Way( this.makeOSMId(), - this.OSM_USER, - this.OSM_TIMESTAMP, - this.OSM_UID, + env.OSM_USER, + env.OSM_TIMESTAMP, + env.OSM_UID, !!add_locations, ); @@ -218,9 +203,9 @@ Given(/^the relations$/, function (table, callback) { const addRelation = (headers, row, cb) => { const relation = new OSM.Relation( this.makeOSMId(), - this.OSM_USER, - this.OSM_TIMESTAMP, - this.OSM_UID, + env.OSM_USER, + env.OSM_TIMESTAMP, + env.OSM_UID, ); let name = null; @@ -298,7 +283,7 @@ Given(/^the relations$/, function (table, callback) { if (key.match(/name/)) name = value; } } - relation.uid = this.OSM_UID; + relation.uid = env.OSM_UID; if (name) { this.nameRelationHash[name] = relation; @@ -328,10 +313,7 @@ Given(/^the raster source$/, function (data, callback) { // TODO: Don't overwrite if it exists fs.writeFile(this.rasterCacheFile, data, callback); // we need this to pass it to the profiles - this.environment = Object.assign( - { OSRM_RASTER_SOURCE: this.rasterCacheFile }, - this.environment, - ); + Object.assign(this.environment, { 'OSRM_RASTER_SOURCE' : this.rasterCacheFile }); }); Given(/^the speed file$/, function (data, callback) { @@ -344,10 +326,15 @@ Given(/^the turn penalty file$/, function (data, callback) { fs.writeFile(this.penaltiesCacheFile, data, callback); }); +Given(/^the profile "([^"]*)"$/, function (profile, callback) { + this.setProfile(profile); + callback(); +}); + Given( /^the profile file(?: "([^"]*)" initialized with)?$/, function (profile, data, callback) { - const lua_profiles_path = this.PROFILES_PATH.split(path.sep).join('/'); + const lua_profiles_path = env.wp.profilesPath.split(path.sep).join('/'); let text = `package.path = "${lua_profiles_path}/?.lua;" .. package.path\n`; if (profile == null) { text += `${data}\n`; @@ -368,31 +355,27 @@ Given( ); Given(/^the data has been saved to disk$/, function (callback) { - this.writeAndLinkOSM(callback); + this.writeOSM(); + callback(); }); -Given( - /^the data has been (extract|contract|partition|customiz)ed$/, - function (step, callback) { - this.reprocess(callback); - }, -); +Given(/^the data has been extracted$/, function () { + this.writeOSM(); + return this.extract(); +}); -Given(/^osrm-routed is stopped$/, function (callback) { - this.OSRMLoader.shutdown(callback); +Given(/^the data has been contracted$/, function () { + return this.contract(); }); -Given(/^data is loaded directly/, function (callback) { - this.osrmLoader.setLoadMethod('directly'); - callback(); +Given(/^the data has been partitioned$/, function () { + return this.partition(); }); -Given(/^data is loaded with datastore$/, function (callback) { - this.osrmLoader.setLoadMethod('datastore'); - callback(); +Given(/^the data has been customized$/, function () { + return this.customize(); }); -Given(/^the HTTP method "([^"]*)"$/, function (method, callback) { - this.httpMethod = method; - callback(); +Given(/^osrm-routed is stopped$/, () => { + return env.osrmLoader.kill(); }); diff --git a/features/step_definitions/options.js b/features/step_definitions/options.js index d909e575fd5..8ebcd991aca 100644 --- a/features/step_definitions/options.js +++ b/features/step_definitions/options.js @@ -4,41 +4,70 @@ import assert from 'assert'; import fs from 'fs'; import { When, Then, Given } from '@cucumber/cucumber'; +import { runBinSync } from '../support/run.js'; + When(/^I run "osrm-routed\s?(.*?)"$/, function (options, callback) { - this.runAndSafeOutput('osrm-routed', options, callback); + const child = runBinSync( + 'osrm-routed', + this.expandOptions(options), + { env : this.environment }, + this.log + ); + this.saveChildOutput(child); + callback(); }); When( /^I run "osrm-(extract|contract|partition|customize)\s?(.*?)"$/, function (binary, options, callback) { - const stamp = `${this.processedCacheFile}.stamp_${binary}`; - this.runAndSafeOutput(`osrm-${binary}`, options, (err) => { - if (err) return callback(err); - fs.writeFile(stamp, 'ok', callback); - }); - }, + const child = runBinSync( + `osrm-${binary}`, + this.expandOptions(options), + { env : this.environment }, + this.log + ); + this.saveChildOutput(child); + if (child.error != null) + return callback(child.error); + const stamp = `${this.osrmCacheFile}.stamp_${binary}`; + fs.writeFile(stamp, 'ok', callback); + } ); When( /^I try to run "(osrm-[a-z]+)\s?(.*?)"$/, function (binary, options, callback) { - this.runAndSafeOutput(binary, options, () => { - callback(); - }); + const child = runBinSync( + binary, + this.expandOptions(options), + { env : this.environment }, + this.log + ); + this.saveChildOutput(child); + callback(); }, ); When( /^I run "osrm-datastore\s?(.*?)"(?: with input "([^"]*)")?$/, - function (options, input, callback) { - const child = this.runAndSafeOutput('osrm-datastore', options, callback); - if (input != null) child.stdin.write(input); // Check for both null and undefined + function (args, input, callback) { + const options = { env : this.environment }; + if (input != null) // Check for both null and undefined + options.input = input; + const child = runBinSync( + 'osrm-datastore', + this.expandOptions(args), + options, + this.log + ); + this.saveChildOutput(child); + callback(); }, ); Then(/^it should exit successfully$/, function () { assert.equal(this.exitCode, 0); - assert.equal(this.termSignal, ''); + assert.equal(this.termSignal, null); }); Then(/^it should exit with an error$/, function () { diff --git a/features/step_definitions/routability.js b/features/step_definitions/routability.js index 4be0b37485b..901177882cd 100644 --- a/features/step_definitions/routability.js +++ b/features/step_definitions/routability.js @@ -3,6 +3,7 @@ import util from 'util'; import d3 from 'd3-queue'; import classes from '../support/data_classes.js'; import { Then } from '@cucumber/cucumber'; +import { env } from '../support/env.js'; Then(/^routability should be$/, function (table, callback) { this.buildWaysFromTable(table, () => { @@ -140,8 +141,8 @@ const testRoutabilityRow = function (i, cb) { const result = {}; const testDirection = function (dir, callback) { - const coordA = this.offsetOriginBy(1 + this.WAY_SPACING * i, 0); - const coordB = this.offsetOriginBy(3 + this.WAY_SPACING * i, 0); + const coordA = this.offsetOriginBy(1 + env.WAY_SPACING * i, 0); + const coordB = this.offsetOriginBy(3 + env.WAY_SPACING * i, 0); const a = new classes.Location(coordA[0], coordA[1]), b = new classes.Location(coordB[0], coordB[1]), @@ -157,7 +158,6 @@ const testRoutabilityRow = function (i, cb) { (err, res, body) => { if (err) return callback(err); - r.query = this.query; r.json = JSON.parse(body); r.code = r.json.code; r.status = res.statusCode === 200 ? 'x' : null; diff --git a/features/step_definitions/trip.js b/features/step_definitions/trip.js index ffebe46aab1..7253ffc8a41 100644 --- a/features/step_definitions/trip.js +++ b/features/step_definitions/trip.js @@ -66,7 +66,7 @@ When(/^I plan a trip I should get$/, function (table, callback) { let subTrips; let trip_durations; let trip_distance; - const ok = res.statusCode === 200; + let ok = res.statusCode === 200; if (ok) { if (headers.has('trips')) { subTrips = json.trips.filter(t => !!t).map(t => t.legs).map(tl => Array.prototype.concat.apply([], tl.map((sl, i) => { diff --git a/features/support/cache.js b/features/support/cache.js index fd195740d92..823f4d9e8a9 100644 --- a/features/support/cache.js +++ b/features/support/cache.js @@ -1,186 +1,38 @@ // Manages test data caching system with hashing for performance optimization -import d3 from 'd3-queue'; -import fs from 'fs'; -import util from 'util'; -import path from 'path'; -import * as hash from '../lib/hash.js'; -import { rm } from 'fs/promises'; -import { createDir } from '../lib/utils.js'; +import fs from 'node:fs'; +import path from 'node:path'; +import util from 'node:util'; + import { formatterHelpers } from '@cucumber/cucumber'; export default class Cache { - constructor(world) { - this.world = world; - } - - // Initializes caching system with OSRM binary hash - initializeCache(callback) { - this.getOSRMHash((err, osrmHash) => { - if (err) return callback(err); - this.osrmHash = osrmHash; - callback(); - }); - } - - // computes all paths for every feature - // Sets up cache directories and hashes for all test features - setupFeatures(features, callback) { - this.featureIDs = {}; - this.featureCacheDirectories = {}; - this.featureProcessedCacheDirectories = {}; - const queue = d3.queue(); - - const initializeFeature = (feature, callback) => { - const uri = feature.getUri(); - - // setup cache for feature data - // if OSRM_PROFILE is set to force a specific profile, then - // include the profile name in the hash of the profile file - hash.hashOfFile(uri, this.OSRM_PROFILE, (err, hash) => { - if (err) return callback(err); - - // shorten uri to be realtive to 'features/' - const featurePath = path.relative(path.resolve('./features'), uri); - // bicycle/bollards/{HASH}/ - const featureID = path.join(featurePath, hash); - - const featureCacheDirectory = this.getFeatureCacheDirectory(featureID); - const featureProcessedCacheDirectory = - this.getFeatureProcessedCacheDirectory( - featureCacheDirectory, - this.osrmHash, - ); - this.featureIDs[uri] = featureID; - this.featureCacheDirectories[uri] = featureCacheDirectory; - this.featureProcessedCacheDirectories[uri] = - featureProcessedCacheDirectory; - - d3.queue(1) - .defer(createDir, featureProcessedCacheDirectory) - .defer(this.cleanupFeatureCache, featureCacheDirectory, hash) - .defer( - this.cleanupProcessedFeatureCache, - featureProcessedCacheDirectory, - this.osrmHash, - ) - .awaitAll(callback); - }); - }; - - for (let i = 0; i < features.length; ++i) { - queue.defer(initializeFeature, features[i]); - } - queue.awaitAll(callback); - } - - cleanupProcessedFeatureCache(directory, osrmHash, callback) { - const parentPath = path.resolve(path.join(directory, '..')); - fs.readdir(parentPath, (err, files) => { - if (err) return callback(err); - const q = d3.queue(); - files.forEach((f) => { - const filePath = path.join(parentPath, f); - fs.stat(filePath, (err, stat) => { - if (err) return callback(err); - if (stat.isDirectory() && filePath.search(osrmHash) < 0) { - rm(filePath, { recursive: true, force: true }); - } - }); - }); - q.awaitAll(callback); - }); - } - - cleanupFeatureCache(directory, featureHash, callback) { - const parentPath = path.resolve(path.join(directory, '..')); - fs.readdir(parentPath, (err, files) => { - if (err) return callback(err); - const q = d3.queue(); - files - .filter((name) => name !== featureHash) - .forEach((f) => { - rm(path.join(parentPath, f), { recursive: true, force: true }); - }); - q.awaitAll(callback); - }); - } - - setupFeatureCache(feature) { - const uri = feature.getUri(); - this.featureID = this.featureIDs[uri]; - this.featureCacheDirectory = this.featureCacheDirectories[uri]; - this.featureProcessedCacheDirectory = - this.featureProcessedCacheDirectories[uri]; - } - - setupScenarioCache(scenarioID) { - this.scenarioCacheFile = this.getScenarioCacheFile( - this.featureCacheDirectory, - scenarioID, - ); - this.processedCacheFile = this.getProcessedCacheFile( - this.featureProcessedCacheDirectory, - scenarioID, - ); - this.inputCacheFile = this.getInputCacheFile( - this.featureProcessedCacheDirectory, - scenarioID, - ); - this.rasterCacheFile = this.getRasterCacheFile( - this.featureProcessedCacheDirectory, - scenarioID, - ); - this.speedsCacheFile = this.getSpeedsCacheFile( - this.featureProcessedCacheDirectory, - scenarioID, - ); - this.penaltiesCacheFile = this.getPenaltiesCacheFile( - this.featureProcessedCacheDirectory, - scenarioID, - ); - this.profileCacheFile = this.getProfileCacheFile( - this.featureProcessedCacheDirectory, - scenarioID, - ); - } - - // returns a hash of all OSRM code side dependencies - getOSRMHash(callback) { - const dependencies = [ - this.OSRM_EXTRACT_PATH, - this.OSRM_CONTRACT_PATH, - this.OSRM_CUSTOMIZE_PATH, - this.OSRM_PARTITION_PATH, - this.LIB_OSRM_EXTRACT_PATH, - this.LIB_OSRM_CONTRACT_PATH, - this.LIB_OSRM_CUSTOMIZE_PATH, - this.LIB_OSRM_PARTITION_PATH, - ]; - - const addLuaFiles = function (directory, callback) { - fs.readdir(path.normalize(directory), (err, files) => { - if (err) return callback(err); - - const luaFiles = files - .filter((f) => !!f.match(/\.lua$/)) - .map((f) => path.normalize(`${directory}/${f}`)); - Array.prototype.push.apply(dependencies, luaFiles); - - callback(); - }); - }; - - // Note: we need a serialized queue here to ensure that the order of the files - // passed is stable. Otherwise the hash will not be stable - d3.queue(1) - .defer(addLuaFiles, this.PROFILES_PATH) - .defer(addLuaFiles, `${this.PROFILES_PATH}/lib`) - .awaitAll(hash.hashOfFiles.bind(hash, dependencies, callback)); - } - - // test/cache/bicycle/bollards/{HASH}/ - getFeatureCacheDirectory(featureID) { - return path.join(this.CACHE_PATH, featureID); + constructor(env, scenario) { + this.env = env; + // There is one cache directory per feature. + // + // The feature cache contains the .osm files cucumber generated from: "Given the + // node map ... and the ways ..." and all the files osrm-extract and friends + // generated from it. + // + // A hash is generated from all osrm binaries and lua profiles (in + // env.getOSRMHash()) and the .feature file. The cache directory is then located at + // test/cache/car/access.feature/{hash}/ + + const uri = scenario.pickle.uri; + const content = fs.readFileSync(uri); + const hash = env.osrmHash.copy(); + hash.update(content); + const hexHash = hash.digest('hex'); + + // shorten uri to be relative to 'features/' + const featurePath = path.relative(path.resolve('./features'), uri); + /** eg. car/access.feature/{hash}/ */ + this.featureID = path.join(featurePath, hexHash); + /** eg. test/cache/car/access.feature/{hash}/ */ + this.featureCacheDirectory = path.join(this.env.wp.cachePath, this.featureID); + + // ensure there is a cache directory + fs.mkdirSync(this.featureCacheDirectory, { recursive: true }); } // converts the scenario titles in file prefixes @@ -205,43 +57,13 @@ export default class Cache { return util.format('%d_%s', line, name); } - // test/cache/{feature_path}/{feature_hash}/{scenario}_raster.asc - getRasterCacheFile(featureCacheDirectory, scenarioID) { - return `${path.join(featureCacheDirectory, scenarioID)}_raster.asc`; - } - - // test/cache/{feature_path}/{feature_hash}/{scenario}_speeds.csv - getSpeedsCacheFile(featureCacheDirectory, scenarioID) { - return `${path.join(featureCacheDirectory, scenarioID)}_speeds.csv`; - } - - // test/cache/{feature_path}/{feature_hash}/{scenario}_penalties.csv - getPenaltiesCacheFile(featureCacheDirectory, scenarioID) { - return `${path.join(featureCacheDirectory, scenarioID)}_penalties.csv`; - } - - // test/cache/{feature_path}/{feature_hash}/{scenario}_profile.lua - getProfileCacheFile(featureCacheDirectory, scenarioID) { - return `${path.join(featureCacheDirectory, scenarioID)}_profile.lua`; - } - - // test/cache/{feature_path}/{feature_hash}/{scenario}.osm - getScenarioCacheFile(featureCacheDirectory, scenarioID) { - return `${path.join(featureCacheDirectory, scenarioID)}.osm`; - } - - // test/cache/{feature_path}/{feature_hash}/{osrm_hash}/ - getFeatureProcessedCacheDirectory(featureCacheDirectory, osrmHash) { - return path.join(featureCacheDirectory, osrmHash); - } - - // test/cache/{feature_path}/{feature_hash}/{osrm_hash}/{scenario}.osrm - getProcessedCacheFile(featureProcessedCacheDirectory, scenarioID) { - return `${path.join(featureProcessedCacheDirectory, scenarioID)}.osrm`; + // test/cache/{feature_path}/{hash} + getCacheDirectory() { + return this.featureCacheDirectory; } - // test/cache/{feature_path}/{feature_hash}/{osrm_hash}/{scenario}.osm - getInputCacheFile(featureProcessedCacheDirectory, scenarioID) { - return `${path.join(featureProcessedCacheDirectory, scenarioID)}.osm`; + // test/cache/{feature_path}/{hash}/42_is_the_answer + getCacheBaseName(scenario) { + return path.join(this.featureCacheDirectory, this.getScenarioID(scenario)); } } diff --git a/features/support/data.js b/features/support/data.js index 42c642b7017..4fbc421b140 100644 --- a/features/support/data.js +++ b/features/support/data.js @@ -3,11 +3,15 @@ import fs from 'fs'; import util from 'util'; import d3 from 'd3-queue'; +import CheapRuler from 'cheap-ruler'; +import stripAnsi from 'strip-ansi'; + import * as OSM from '../lib/osm.js'; -import classes from './data_classes.js'; import tableDiff from '../lib/table_diff.js'; -import { ensureDecimal, errorReason } from '../lib/utils.js'; -import CheapRuler from 'cheap-ruler'; +import { ensureDecimal } from '../lib/utils.js'; +import classes from './data_classes.js'; +import { env } from './env.js'; +import { runBinSync } from './run.js'; export default class Data { constructor(world) { @@ -58,12 +62,12 @@ export default class Data { // Creates synthetic OSM node with calculated coordinates const makeFakeNode = (namePrefix, offset) => { - const coord = this.offsetOriginBy(offset + this.WAY_SPACING * ri, 0); + const coord = this.offsetOriginBy(offset + env.WAY_SPACING * ri, 0); return new OSM.Node( this.makeOSMId(), - this.OSM_USER, - this.OSM_TIMESTAMP, - this.OSM_UID, + env.OSM_USER, + env.OSM_TIMESTAMP, + env.OSM_UID, coord[0], coord[1], { name: util.format('%s%d', namePrefix, ri) }, @@ -79,9 +83,9 @@ export default class Data { // ...with a way between them const way = new OSM.Way( this.makeOSMId(), - this.OSM_USER, - this.OSM_TIMESTAMP, - this.OSM_UID, + env.OSM_USER, + env.OSM_TIMESTAMP, + env.OSM_UID, ); nodes.forEach((node) => { @@ -148,9 +152,9 @@ export default class Data { id = id || this.makeOSMId(); const node = new OSM.Node( id, - this.OSM_USER, - this.OSM_TIMESTAMP, - this.OSM_UID, + env.OSM_USER, + env.OSM_TIMESTAMP, + env.OSM_UID, lon, lat, { name }, @@ -238,168 +242,81 @@ export default class Data { this.osmID = 0; } - writeOSM(callback) { - fs.exists(this.scenarioCacheFile, (exists) => { - if (exists) callback(); - else { - this.OSMDB.toXML((xml) => { - fs.writeFile(this.scenarioCacheFile, xml, callback); - }); - } - }); - } - - linkOSM(callback) { - fs.exists(this.inputCacheFile, (exists) => { - if (exists) callback(); - else { - fs.link(this.scenarioCacheFile, this.inputCacheFile, callback); - } + writeOSM() { + if (fs.existsSync(this.osmCacheFile)) + return; + this.OSMDB.toXML((xml) => { + fs.writeFileSync(this.osmCacheFile, xml); }); } - extractData(p, callback) { - const stamp = `${p.processedCacheFile}.stamp_extract`; - fs.exists(stamp, (exists) => { - if (exists) return callback(); - - this.runBin( - 'osrm-extract', - util.format( - '%s --profile %s %s', - p.extractArgs, - p.profileFile, - p.inputCacheFile, - ), - p.environment, - (err) => { - if (err) { - return callback( - new Error( - util.format('osrm-extract %s: %s', errorReason(err), err.cmd), - ), - ); - } - fs.writeFile(stamp, 'ok', callback); - }, + runAndStamp(what, extra_params, params) { + const stampFile = `${this.osrmCacheFile}.stamp_${what}`; + if (!fs.existsSync(stampFile)) { + runBinSync( + `osrm-${what}`, + extra_params.concat(params), + { env : this.environment }, + this.log ); - }); + fs.writeFileSync(stampFile, 'ok'); + } + return Promise.resolve(); } - contractData(p, callback) { - const stamp = `${p.processedCacheFile}.stamp_contract`; - fs.exists(stamp, (exists) => { - if (exists) return callback(); - - this.runBin( - 'osrm-contract', - util.format('%s %s', p.contractArgs, p.processedCacheFile), - p.environment, - (err) => { - if (err) { - return callback( - new Error( - util.format('osrm-contract %s: %s', errorReason(err), err), - ), - ); - } - fs.writeFile(stamp, 'ok', callback); - }, - ); - }); + extract() { + return this.runAndStamp('extract', this.extractArgs, [ + '-p', + this.profileFile, + this.osmCacheFile, + ]); } - partitionData(p, callback) { - const stamp = `${p.processedCacheFile}.stamp_partition`; - fs.exists(stamp, (exists) => { - if (exists) return callback(); - - this.runBin( - 'osrm-partition', - util.format('%s %s', p.partitionArgs, p.processedCacheFile), - p.environment, - (err) => { - if (err) { - return callback( - new Error( - util.format('osrm-partition %s: %s', errorReason(err), err.cmd), - ), - ); - } - fs.writeFile(stamp, 'ok', callback); - }, - ); - }); + partition() { + return this.runAndStamp('partition', this.partitionArgs, [ + this.osrmCacheFile + ]); } - customizeData(p, callback) { - const stamp = `${p.processedCacheFile}.stamp_customize`; - fs.exists(stamp, (exists) => { - if (exists) return callback(); - - this.runBin( - 'osrm-customize', - util.format('%s %s', p.customizeArgs, p.processedCacheFile), - p.environment, - (err) => { - if (err) { - return callback( - new Error( - util.format('osrm-customize %s: %s', errorReason(err), err), - ), - ); - } - fs.writeFile(stamp, 'ok', callback); - }, - ); - }); + customize() { + return this.runAndStamp('customize', this.customizeArgs, [ + this.osrmCacheFile + ]); } - extractContractPartitionAndCustomize(callback) { - // a shallow copy of scenario parameters to avoid data inconsistency - // if a cucumber timeout occurs during deferred jobs - const p = { - extractArgs: this.extractArgs, - contractArgs: this.contractArgs, - partitionArgs: this.partitionArgs, - customizeArgs: this.customizeArgs, - profileFile: this.profileFile, - inputCacheFile: this.inputCacheFile, - processedCacheFile: this.processedCacheFile, - environment: this.environment, - }; - const queue = d3.queue(1); - queue.defer(this.extractData, p); - queue.defer(this.partitionData, p); - queue.defer(this.contractData, p); - queue.defer(this.customizeData, p); - queue.awaitAll(callback); + contract() { + return this.runAndStamp('contract', this.contractArgs, [ + this.osrmCacheFile + ]); } - writeAndLinkOSM(callback) { - const queue = d3.queue(1); - queue.defer(this.writeOSM); - queue.defer(this.linkOSM); - queue.awaitAll(callback); + /** + * Runs the complete extraction chain with one .osm file. + */ + runExtractionChain() { + this.extract(); + this.partition(); // mld + this.customize(); // mld + this.contract(); // ch } reprocess(callback) { - const queue = d3.queue(1); - queue.defer(this.writeAndLinkOSM); - queue.defer(this.extractContractPartitionAndCustomize); - queue.awaitAll(callback); + this.writeOSM(); + this.runExtractionChain(); + callback(); } - reprocessAndLoadData(callback) { - const p = { - loaderArgs: this.loaderArgs, - inputFile: this.processedCacheFile, - }; - const queue = d3.queue(1); - queue.defer(this.writeAndLinkOSM); - queue.defer(this.extractContractPartitionAndCustomize); - queue.defer((params, cb) => this.osrmLoader.load(params, cb), p); - queue.awaitAll(callback); + // This is called on every "When I X I should Y" + // On the first call in every scenario it should load the data + // into osrm-routed or osrm-datastore + async reprocessAndLoadData(callback) { + if (!this.dataLoaded) { + this.writeOSM(); + this.runExtractionChain(); + await env.osrmLoader.before(this); + this.dataLoaded = true; + } + callback(); } processRowsAndDiff(table, fn, callback) { @@ -410,10 +327,20 @@ export default class Data { }); q.awaitAll((err, actual) => { - if (err) return callback(err); + if (err) + return callback(err); const diff = tableDiff(table, actual); - if (diff) callback(diff); - else callback(); + if (diff) { + if (env.PLATFORM_CI) { + // the github report displays ANSI escapes as characters if passed to the + // error callback. + callback(stripAnsi(diff)); + } else { + callback(diff); + } + } else { + callback(); + } }); } } diff --git a/features/support/env.js b/features/support/env.js index 6f27eeada9a..bde4a7dfcaa 100644 --- a/features/support/env.js +++ b/features/support/env.js @@ -1,74 +1,59 @@ // Sets up global environment constants and configuration for test execution -import path from 'path'; -import util from 'util'; -import fs from 'fs'; -import d3 from 'd3-queue'; -import child_process from 'child_process'; -import tryConnect from '../lib/try_connect.js'; -import { setDefaultTimeout } from '@cucumber/cucumber'; - -// Set global timeout for all steps and hooks -setDefaultTimeout( - (process.env.CUCUMBER_TIMEOUT && parseInt(process.env.CUCUMBER_TIMEOUT)) || - 5000, -); - -// Sets up all constants that are valid for all features -export default class Env { - constructor(world) { - this.world = world; - } +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import http from 'node:http'; +import https from 'node:https'; +import path from 'node:path'; +import util from 'node:util'; + +import { OSRMDatastoreLoader, OSRMDirectLoader, OSRMmmapLoader } from '../lib/osrm_loader.js'; +/** Global environment for all scenarios. */ +class Env { // Initializes all environment constants and paths for test execution - initializeEnv(callback) { - this.TIMEOUT = parseInt(process.env.CUCUMBER_TIMEOUT) || 5000; - this.ROOT_PATH = process.cwd(); - - this.TEST_PATH = path.resolve(this.ROOT_PATH, 'test'); - this.CACHE_PATH = path.resolve(this.TEST_PATH, 'cache'); - this.LOGS_PATH = path.resolve(this.TEST_PATH, 'logs'); - - this.PROFILES_PATH = path.resolve(this.ROOT_PATH, 'profiles'); - this.FIXTURES_PATH = path.resolve(this.ROOT_PATH, 'unit_tests/fixtures'); - this.BIN_PATH = - process.env.OSRM_BUILD_DIR || path.resolve(this.ROOT_PATH, 'build'); - this.DATASET_NAME = 'cucumber'; - this.PLATFORM_WINDOWS = process.platform.match(/^win.*/); - this.DEFAULT_ENVIRONMENT = process.env; + beforeAll(worldParameters) { + const wp = this.wp = worldParameters; + const penv = process.env; + + /** For scenarios that don't define a profile, osrm-extract still wants one. */ this.DEFAULT_PROFILE = 'bicycle'; - this.DEFAULT_INPUT_FORMAT = 'osm'; - const loadMethod = process.env.OSRM_LOAD_METHOD || 'datastore'; - this.DEFAULT_LOAD_METHOD = loadMethod.match('mmap') - ? 'mmap' - : loadMethod.match('directly') - ? 'directly' - : 'datastore'; - this.DEFAULT_ORIGIN = [1, 1]; + /** Overrides any profile specified in a scenario. See: PR#4516 */ + this.OSRM_PROFILE = penv.OSRM_PROFILE; + this.OSM_USER = 'osrm'; this.OSM_UID = 1; this.OSM_TIMESTAMP = '2000-01-01T00:00:00Z'; this.WAY_SPACING = 100; this.DEFAULT_GRID_SIZE = 100; // meters - // get algorithm name from the command line profile argument - this.ROUTING_ALGORITHM = process.argv[process.argv.indexOf('-p') + 1].match( - 'mld', - ) - ? 'MLD' - : 'CH'; - this.TIMEZONE_NAMES = this.PLATFORM_WINDOWS ? 'win' : 'iana'; - - this.OSRM_PORT = parseInt(process.env.OSRM_PORT) || 5000; - this.OSRM_IP = process.env.OSRM_IP || '127.0.0.1'; - this.OSRM_CONNECTION_RETRIES = - parseInt(process.env.OSRM_CONNECTION_RETRIES) || 10; - this.OSRM_CONNECTION_EXP_BACKOFF_COEF = - parseFloat(process.env.OSRM_CONNECTION_EXP_BACKOFF_COEF) || 1.1; - - this.HOST = `http://${this.OSRM_IP}:${this.OSRM_PORT}`; - - this.OSRM_PROFILE = process.env.OSRM_PROFILE; - - if (this.PLATFORM_WINDOWS) { + this.DEFAULT_ORIGIN = [1, 1]; + + this.PLATFORM_CI = penv.GITHUB_ACTIONS != undefined; + this.CUCUMBER_WORKER_ID = parseInt(penv.CUCUMBER_WORKER_ID || '0'); + + // if (this.CUCUMBER_WORKER_ID == 0) + // console.log(wp); + + // make each worker use its own port + wp.port += parseInt(this.CUCUMBER_WORKER_ID); + wp.host = `http://${wp.ip}:${wp.port}`; + + if (wp.host.startsWith('https')) { + this.client = https; + this.agent = new https.Agent ({ + timeout: wp.httpTimeout, + defaultPort: wp.port, + }); + } else { + this.client = http; + this.agent = new http.Agent ({ + timeout: wp.httpTimeout, + defaultPort: wp.port, + }); + } + + this.DATASET_NAME = `cucumber${this.CUCUMBER_WORKER_ID}`; + + if (process.platform === 'win32') { this.TERMSIGNAL = 9; this.EXE = '.exe'; } else { @@ -76,10 +61,18 @@ export default class Env { this.EXE = ''; } + /** + * A log file for the long running background process osrm-routed in load method + * datastore. That process may output logs outside of a scenario. + */ + this.globalLogfile = fs.openSync( + path.join(wp.logsPath, `cucumber-global-${this.CUCUMBER_WORKER_ID}.log`), + 'a'); + // heuristically detect .so/.a/.dll/.lib suffix this.LIB = ['lib%s.a', 'lib%s.so', '%s.dll', '%s.lib'].find((format) => { try { - const lib = `${this.BIN_PATH}/${util.format(format, 'osrm')}`; + const lib = path.join(wp.buildPath, util.format(format, 'osrm')); fs.accessSync(lib, fs.constants.F_OK); } catch { return false; @@ -93,96 +86,91 @@ export default class Env { ); } - this.OSRM_EXTRACT_PATH = path.resolve( - util.format('%s/%s%s', this.BIN_PATH, 'osrm-extract', this.EXE), - ); - this.OSRM_CONTRACT_PATH = path.resolve( - util.format('%s/%s%s', this.BIN_PATH, 'osrm-contract', this.EXE), - ); - this.OSRM_CUSTOMIZE_PATH = path.resolve( - util.format('%s/%s%s', this.BIN_PATH, 'osrm-customize', this.EXE), - ); - this.OSRM_PARTITION_PATH = path.resolve( - util.format('%s/%s%s', this.BIN_PATH, 'osrm-partition', this.EXE), - ); - this.OSRM_ROUTED_PATH = path.resolve( - util.format('%s/%s%s', this.BIN_PATH, 'osrm-routed', this.EXE), - ); - ((this.LIB_OSRM_EXTRACT_PATH = util.format( - `%s/${this.LIB}`, - this.BIN_PATH, - 'osrm_extract', - )), - (this.LIB_OSRM_CONTRACT_PATH = util.format( - `%s/${this.LIB}`, - this.BIN_PATH, - 'osrm_contract', - )), - (this.LIB_OSRM_CUSTOMIZE_PATH = util.format( - `%s/${this.LIB}`, - this.BIN_PATH, - 'osrm_customize', - )), - (this.LIB_OSRM_PARTITION_PATH = util.format( - `%s/${this.LIB}`, - this.BIN_PATH, - 'osrm_partition', - )), - (this.LIB_OSRM_PATH = util.format( - `%s/${this.LIB}`, - this.BIN_PATH, - 'osrm', - ))); - - fs.exists(this.TEST_PATH, (exists) => { - if (exists) return callback(); - else return callback(new Error('*** Test folder doesn\'t exist.')); - }); + /** binaries responsible for the cached files */ + this.extractionBinaries = []; + /** libraries responsible for the cached files */ + this.libraries = []; + /** binaries that must be present */ + this.requiredBinaries = []; + + for (const i of ['extract', 'contract', 'customize', 'partition']) { + const bin = path.join(wp.buildPath, `osrm-${i}${this.EXE}`); + this.extractionBinaries.push(bin); + this.requiredBinaries.push(bin); + } + for (const i of 'routed'.split()) { + this.requiredBinaries.push(path.join(wp.buildPath, `osrm-${i}${this.EXE}`)); + } + for (const i of ['_extract', '_contract', '_customize', '_partition', '']) { + const lib = path.join(wp.buildPath, util.format(this.LIB, `osrm${i}`)); + this.libraries.push(lib); + } + + if (!fs.existsSync(wp.testPath)) { + callback(new Error(`*** Test folder doesn't exist: ${wp.testPath}`)); + }; + + /** A hash of all osrm binaries and lua profiles */ + this.osrmHash = this.getOSRMHash(); + + this.setLoadMethod(wp.loadMethod); } - getProfilePath(profile) { - return path.resolve(this.PROFILES_PATH, `${profile}.lua`); + async afterAll() { + await this.osrmLoader.afterAll(); + fs.closeSync(this.globalLogfile); + this.globalLogfile = null; } - verifyOSRMIsNotRunning(callback) { - tryConnect(this.OSRM_IP, this.OSRM_PORT, (err) => { - if (!err) - return callback(new Error('*** osrm-routed is already running.')); - else callback(); - }); + globalLog(msg) { + if (this.globalLogfile) + fs.writeSync(this.globalLogfile, msg); } - verifyExistenceOfBinaries(callback) { - const verify = function (binPath, cb) { - fs.exists(binPath, (exists) => { - if (!exists) - return cb( - new Error(util.format('%s is missing. Build failed?', binPath)), - ); - const helpPath = util.format('%s --help', binPath); - child_process.exec(helpPath, (err) => { - if (err) { - return cb( - new Error( - util.format('*** %s exited with code %d', helpPath, err.code), - ), - ); - } - cb(); - }); - }); + setLoadMethod(method) { + if (method === 'datastore') { + this.osrmLoader = new OSRMDatastoreLoader(this); + } else if (method === 'directly') { + this.osrmLoader = new OSRMDirectLoader(this); + } else if (method === 'mmap') { + this.osrmLoader = new OSRMmmapLoader(this); + } else { + this.osrmLoader = null; + throw new Error(`No such data load method: ${method}`); + } + } + + getProfilePath(profile) { + return path.join(wp.profilesPath, `${profile}.lua`); + } + + // returns a hash of all OSRM code side dependencies + // that is: all osrm binaries and all lua profiles + getOSRMHash() { + const dependencies = this.extractionBinaries.concat(this.libraries); + + const addLuaFiles = function (directory) { + const luaFiles = fs.readdirSync(path.normalize(directory)) + .filter((f) => !!f.match(/\.lua$/)) + .map((f) => path.join(directory, f)); + Array.prototype.push.apply(dependencies, luaFiles); }; - const q = d3.queue(); - [ - this.OSRM_EXTRACT_PATH, - this.OSRM_CONTRACT_PATH, - this.OSRM_CUSTOMIZE_PATH, - this.OSRM_PARTITION_PATH, - this.OSRM_ROUTED_PATH, - ].forEach((bin) => { - q.defer(verify, bin); - }); - q.awaitAll(callback); + addLuaFiles(this.wp.profilesPath); + addLuaFiles(path.join(this.wp.profilesPath, 'lib')); + + if (this.OSRM_PROFILE) { + // This may add a duplicate but it doesn't matter. + dependencies.push(path.join(this.wp.profilesPath, `${this.OSRM_PROFILE}.lua`)); + } + + const checksum = crypto.createHash('md5'); + for (const file of dependencies) { + checksum.update(fs.readFileSync(path.normalize(file))); + } + return checksum; } } + +/** Global environment */ +export const env = new Env(); diff --git a/features/support/hooks.js b/features/support/hooks.js index e3f57334d7c..eec4a7ab9d6 100644 --- a/features/support/hooks.js +++ b/features/support/hooks.js @@ -1,23 +1,61 @@ // Cucumber before/after hooks for test setup, teardown, and environment initialization import { BeforeAll, Before, After, AfterAll } from '@cucumber/cucumber'; +import { setParallelCanAssign } from '@cucumber/cucumber'; // Import the custom World constructor (registers itself via setWorldConstructor) import './world.js'; +import { env } from './env.js'; +import { verifyExistenceOfBinaries } from '../lib/utils.js'; +import { testOsrmDown } from '../lib/osrm_loader.js'; -BeforeAll((callback) => { - callback(); +import {setDefaultTimeout} from '@cucumber/cucumber'; +setDefaultTimeout(parseInt(process.env.CUCUMBER_TIMEOUT || '5000')); + +/** + * A function that assures that an \@isolated scenario will not run while any other + * scenario is running in parallel. + */ +function isolated (pickleInQuestion, picklesInProgress) { + for (const tag of pickleInQuestion.tags) { + if (tag.name === '@isolated') + return picklesInProgress.length == 0; + } + // No other restrictions + return true; +}; + +setParallelCanAssign(isolated); + +BeforeAll(function () { + env.beforeAll(this.parameters); + return Promise.all([ + verifyExistenceOfBinaries(env), + testOsrmDown() + ]); }); -Before({ timeout: 30000 }, function (testCase, callback) { - // Initialize the World instance for this test case - this.init(testCase, callback); +Before(function (scenario) { + for (const t of scenario.pickle.tags) { + if (t.name.startsWith('@no_')) { + const tag = t.name.substring(4); + if (env.wp.loadMethod === tag || env.wp.algorithm === tag || process.platform === tag) { + return 'skipped'; + } + } + if (t.name.startsWith('@with_')) { + const tag = t.name.substring(6); + if (env.wp.loadMethod !== tag && env.wp.algorithm !== tag && process.platform !== tag) { + return 'skipped'; + } + } + } + return this.before(scenario); }); -After(function (testCase, callback) { - // Cleanup the World instance after this test case - this.cleanup(callback); +After(function (scenario) { + return this.after(scenario); }); -AfterAll((callback) => { - callback(); +AfterAll(() => { + return env.afterAll(); }); diff --git a/features/support/http.js b/features/support/http.js index 0b74e24bd9f..b662de5126d 100644 --- a/features/support/http.js +++ b/features/support/http.js @@ -1,11 +1,9 @@ // HTTP client utilities for making API requests to OSRM routing server -import { Timeout } from '../lib/utils.js'; -import http from 'http'; -import https from 'https'; +import { env } from './env.js'; -function httpRequest(url, callback) { - const client = url.startsWith('https') ? https : http; - const req = client.get(url, (res) => { +export function sendRequest (url, log, callback) { + log(`sending request: ${url}`); + const req = env.client.get (url, { agent: env.agent }, (res) => { let data = ''; // Collect data chunks @@ -19,66 +17,18 @@ function httpRequest(url, callback) { }); }); + // Handle timeout + req.on('timeout', (err) => { + log(`request timed out: ${url}`); + req.destroy(); + callback(err); + }); + // Handle errors req.on('error', (err) => { + log(`request errored out: ${url} ${err.message}`); callback(err); }); req.end(); -} - -export default class Http { - constructor(world) { - this.world = world; - } - - paramsToString(params) { - let paramString = ''; - if (params.coordinates !== undefined) { - // FIXME this disables passing the output if its a default - // Remove after #2173 is fixed. - const outputString = - params.output && params.output !== 'json' ? `.${params.output}` : ''; - paramString = params.coordinates.join(';') + outputString; - delete params.coordinates; - delete params.output; - } - if (Object.keys(params).length) { - paramString += `?${Object.keys(params) - .map((k) => `${k}=${params[k]}`) - .join('&')}`; - } - - return paramString; - } - - // FIXME this needs to be simplified! - sendRequest(baseUri, parameters, callback) { - const limit = Timeout(this.TIMEOUT, { err: { statusCode: 408 } }); - const runRequest = (cb) => { - const params = this.paramsToString(parameters); - this.query = baseUri + (params.length ? `/${params}` : ''); - - httpRequest(this.query, (err, res, body) => { - if (err && err.code === 'ECONNREFUSED') { - return cb(new Error('*** osrm-routed is not running.')); - } else if (err && err.statusCode === 408) { - return cb(new Error()); - } - return cb(err, res, body); - }); - }; - - runRequest( - limit((err, res, body) => { - if (err) { - if (err.statusCode === 408) - return callback(new Error('*** osrm-routed did not respond')); - else if (err.code === 'ECONNREFUSED') - return callback(new Error('*** osrm-routed is not running')); - } - return callback(err, res, body); - }), - ); - } -} +}; diff --git a/features/support/options.js b/features/support/options.js deleted file mode 100644 index a0e7da39c5d..00000000000 --- a/features/support/options.js +++ /dev/null @@ -1,28 +0,0 @@ -// Support functions for OSRM binary options and output handling -export default class Options { - constructor(world) { - this.world = world; - } - - resetOptionsOutput() { - this.stdout = null; - this.stderr = null; - this.exitCode = null; - this.termSignal = null; - } - - runAndSafeOutput(binary, options, callback) { - return this.runBin( - binary, - this.expandOptions(options), - this.environment, - (err, stdout, stderr) => { - this.stdout = stdout; - this.stderr = stderr; - this.exitCode = (err && err.code) || 0; - this.termSignal = (err && err.signal) || ''; - callback(err); - } - ); - } -} \ No newline at end of file diff --git a/features/support/route.js b/features/support/route.js index abc156c524f..cc8aa2dc34d 100644 --- a/features/support/route.js +++ b/features/support/route.js @@ -1,25 +1,43 @@ // Route response validation and geometry processing utilities import { ensureDecimal } from '../lib/utils.js'; +import { env } from './env.js'; +import { sendRequest } from './http.js'; export default class Route { constructor(world) { this.world = world; } - requestPath(service, params, callback) { - let uri; - if (service == 'timestamp') { - uri = [this.HOST, service].join('/'); - } else { - uri = [this.HOST, service, 'v1', this.profile].join('/'); + paramsToQuery(params) { + let query = ''; + if (params.coordinates !== undefined) { + // FIXME this disables passing the output if its a default + // Remove after #2173 is fixed. + const outputString = + params.output && params.output !== 'json' ? `.${params.output}` : ''; + query = params.coordinates.join(';') + outputString; + delete params.coordinates; + delete params.output; } + if (Object.keys(params).length) { + query += `?${Object.keys(params) + .map((k) => `${k}=${params[k]}`) + .join('&')}`; + } + + return query; + } - return this.sendRequest(uri, params, callback); + requestPath(service, parameters, callback) { + const baseUrl = new URL(`${service}/v1/${this.profile}/`, env.wp.host); + const query = this.paramsToQuery(parameters); + const url = new URL(query, baseUrl); + sendRequest(url, this.log, callback); } requestUrl(path, callback) { - const uri = (this.query = [this.HOST, path].join('/')); - this.sendRequest(uri, '', callback); + const url = new URL(path, env.wp.host); + sendRequest(url, this.log, callback); } // Overwrites the default values in defaults diff --git a/features/support/run.js b/features/support/run.js index 0b428908e19..71293a01758 100644 --- a/features/support/run.js +++ b/features/support/run.js @@ -1,75 +1,87 @@ // Process execution utilities for running OSRM binaries and managing subprocesses -import path from 'path'; -import fs from 'fs'; -import util from 'util'; -import child_process from 'child_process'; +import child_process from 'node:child_process'; +import path from 'node:path'; -export default class Run { - constructor(world) { - this.world = world; - } - - // replaces placeholders for in user supplied commands - expandOptions(options) { - let opts = options.slice(); - const table = { - '{osm_file}': this.inputCacheFile, - '{processed_file}': this.processedCacheFile, - '{profile_file}': this.profileFile, - '{rastersource_file}': this.rasterCacheFile, - '{speeds_file}': this.speedsCacheFile, - '{penalties_file}': this.penaltiesCacheFile, - '{timezone_names}': this.TIMEZONE_NAMES, - }; +import { env } from '../support/env.js'; - for (const k in table) { - opts = opts.replace(k, table[k]); - } - - return opts; - } +/** Returns the full path to the binary. */ +export function mkBinPath(bin) { + return path.join(env.wp.buildPath, `${bin}${env.EXE}`); +} - setupOutputLog(process, log) { - if (process.logFunc) { - process.stdout.removeListener('data', process.logFunc); - process.stderr.removeListener('data', process.logFunc); - } +/** + * Runs an osrm binary in asynchronous mode. + * + * Use case: osrm-routed. + * + * @param {string} bin The name of the binary, eg. osrm-routed + * @param {string[]} args The arguments to the binary + * @param {object} options Options passed to the spawn function + * @param {function} log Function that consumes logs, eg world.log + */ +export function runBin(bin, args, options, log) { + const cmd = mkBinPath(bin); + const argsAsString = args.join(' '); + log(`running ${bin} as:\n${cmd} ${argsAsString}`); - process.logFunc = (message) => { - log.write(message); - }; - process.stdout.on('data', process.logFunc); - process.stderr.on('data', process.logFunc); - } + const child = child_process.spawn( + cmd, + args, + options, + ); - runBin(bin, options, env, callback) { - const cmd = path.resolve( - util.format('%s/%s%s', this.BIN_PATH, bin, this.EXE), - ); - const opts = options.split(' ').filter((x) => { - return x && x.length > 0; - }); - const log = fs.createWriteStream(this.scenarioLogFile, { flags: 'a' }); - log.write(util.format('*** running %s %s\n', cmd, options)); + // we MUST consume these or the osrm-routed process will block + // we cannot send these to world.log() because output might happen between steps + child.stderr.on('data', (data) => log(data)); + child.stdout.on('data', (data) => log(data)); - // we need to set a large maxbuffer here because we have long running processes like osrm-routed - // with lots of log output - const child = child_process.execFile( - cmd, - opts, - { maxBuffer: 1024 * 1024 * 1000, env }, - (err, stdout, stderr) => { - log.end(); - callback(err, stdout, stderr); - }, - ); + child.on('error', (err) => { + log(`${bin} aborted with error ${err}`); + throw(err); + }); + child.on('exit', (code, signal) => { + if (signal != null) { + const msg = `${bin} aborted with signal ${child.signal}`; + log(msg); + throw new Error(msg); + } else { + log(`${bin} completed with exit code ${code}`); + } + }); + return child; +} - child.on('exit', (code) => { - log.write(util.format('*** %s exited with code %d\n', bin, code)); - }); +/** + * Runs an osrm binary in synchronous mode. + * + * Use case: osrm-extract and friends. + * + * @param {string} bin The name of the binary, eg. osrm-extract + * @param {string[]} args The arguments to the binary + * @param {object} options Options passed to the spawnSync function + * @param {function} log Function that consumes logs, eg world.log + */ +export function runBinSync(bin, args, options, log) { + const cmd = mkBinPath(bin); + const argsAsString = args.join(' '); + log(`running ${bin} as:\n${cmd} ${argsAsString}`); - // Don't setup output logging as it interferes with execFile's output capture - // this.setupOutputLog(child, log); - return child; + const child = child_process.spawnSync( + cmd, + args, + options + ); + if (child.stdout) + log(`${bin} stdout:\n${child.stdout}`); + if (child.stderr) + log(`${bin} stderr:\n${child.stderr}`); + if (child.status != null) + log(`${bin} completed with exit code ${child.status}`); + if (child.signal != null) { + const msg = `${bin} aborted with signal ${child.signal}`; + log(msg); + if (child.signal != 'SIGABRT') // some tests deliberately fail + throw new Error(msg); } + return child; } diff --git a/features/support/world.js b/features/support/world.js index fc8a5408b91..bf93be7dd48 100644 --- a/features/support/world.js +++ b/features/support/world.js @@ -1,60 +1,40 @@ // Custom World class for OSRM test environment using modern Cucumber.js v13 patterns -import d3 from 'd3-queue'; -import path from 'path'; -import fs from 'fs'; -import * as OSM from '../lib/osm.js'; -import OSRMLoader from '../lib/osrm_loader.js'; -import { createDir } from '../lib/utils.js'; +import fs from 'node:fs'; +import path from 'node:path'; + import { World, setWorldConstructor } from '@cucumber/cucumber'; -import Env from './env.js'; +import * as OSM from '../lib/osm.js'; +import { env } from '../support/env.js'; import Cache from './cache.js'; import Data from './data.js'; -import Http from './http.js'; import Route from './route.js'; -import Run from './run.js'; -import Options from './options.js'; import Fuzzy from './fuzzy.js'; import SharedSteps from './shared_steps.js'; -// Global flags for initialization -const collectedFeatures = new Set(); // Collect unique features from testCases - class OSRMWorld extends World { // Private instances of support classes for clean composition - #env; - #cache; #data; - #http; #route; - #run; #sharedSteps; #fuzzy; - #options; constructor(options) { // Get built-in Cucumber helpers: this.attach, this.log, this.parameters super(options); - - // Initialize Env constants directly in constructor first - this.#initializeEnvConstants(); + this.loadMethod = null; + this.dataLoaded = false; // Initialize service instances with access to world - this.#env = new Env(this); - this.#cache = new Cache(this); this.#data = new Data(this); - this.#http = new Http(this); this.#route = new Route(this); - this.#run = new Run(this); this.#sharedSteps = new SharedSteps(this); this.#fuzzy = new Fuzzy(this); - this.#options = new Options(this); // Copy methods from services to world for compatibility this.#copyMethodsFromServices(); // Initialize core objects - this.osrmLoader = new OSRMLoader(this); this.OSMDB = new OSM.DB(); // Copy properties that need direct access @@ -64,14 +44,9 @@ class OSRMWorld extends World { // Copy methods from service classes #copyMethodsFromServices() { [ - this.#env, - this.#cache, this.#data, - this.#http, this.#route, - this.#run, this.#sharedSteps, - this.#options, ].forEach((service) => { Object.getOwnPropertyNames(Object.getPrototypeOf(service)).forEach( (name) => { @@ -83,120 +58,108 @@ class OSRMWorld extends World { }); } - // Initialize environment constants (extracted from Env class) - #initializeEnvConstants() { - this.TIMEOUT = - (process.env.CUCUMBER_TIMEOUT && - parseInt(process.env.CUCUMBER_TIMEOUT)) || - 5000; - this.ROOT_PATH = process.cwd(); - this.TEST_PATH = path.resolve(this.ROOT_PATH, 'test'); - this.CACHE_PATH = path.resolve(this.TEST_PATH, 'cache'); - this.LOGS_PATH = path.resolve(this.TEST_PATH, 'logs'); - this.PROFILES_PATH = path.resolve(this.ROOT_PATH, 'profiles'); - this.FIXTURES_PATH = path.resolve(this.ROOT_PATH, 'unit_tests/fixtures'); - this.BIN_PATH = - (process.env.OSRM_BUILD_DIR && process.env.OSRM_BUILD_DIR) || - path.resolve(this.ROOT_PATH, 'build'); - this.DATASET_NAME = 'cucumber'; - } - // Clean getter access to services - get env() { - return this.#env; - } - get cache() { - return this.#cache; - } get data() { return this.#data; } - get http() { - return this.#http; - } get route() { return this.#route; } - get run() { - return this.#run; - } get sharedSteps() { return this.#sharedSteps; } get fuzzy() { return this.#fuzzy; } - get options() { - return this.#options; + + before(scenario) { + this.cache = new Cache(env, scenario); + this.setupCurrentScenario(this.cache, scenario); + this.resetChildOutput(); + return Promise.resolve(); } - // Initialize the world for a specific test case - // This method is called from Before hook since constructors can't be async - init(testCase, callback) { - // Collect features from testCases - collectedFeatures.add(testCase.pickle.uri); - - const queue = d3.queue(1); - queue.defer(this.initializeEnv); - queue.defer(this.verifyOSRMIsNotRunning); - queue.defer(this.verifyExistenceOfBinaries); - queue.defer(this.initializeCache); - - // Create mock features array from collected URIs - const mockFeatures = Array.from(collectedFeatures).map((uri) => ({ - getUri: () => uri, - })); - queue.defer(this.setupFeatures, mockFeatures); - - queue.awaitAll((err) => { - if (err) return callback(err); - this.setupCurrentScenario(testCase, callback); - }); + async after(scenario) { + if (this.osmCacheFile && fs.existsSync(this.osmCacheFile)) + await this.attach(fs.createReadStream(this.osmCacheFile), + { mediaType: 'application/osm+xml', fileName: path.basename(this.osmCacheFile) }); + return env.osrmLoader.after(scenario); } - setupCurrentScenario(testCase, callback) { - this.profile = this.OSRM_PROFILE || this.DEFAULT_PROFILE; - this.profileFile = path.join(this.PROFILES_PATH, `${this.profile}.lua`); - this.osrmLoader.setLoadMethod(this.DEFAULT_LOAD_METHOD); - this.setGridSize(this.DEFAULT_GRID_SIZE); - this.setOrigin(this.DEFAULT_ORIGIN); + setProfile(profile) { + this.profile = env.OSRM_PROFILE || profile || env.DEFAULT_PROFILE; + // Sometimes a profile file needs to be patched. In that case it will be copied into + // the cache directory and this reference adjusted. + this.profileFile = path.join(env.wp.profilesPath, `${this.profile}.lua`); + } + + setupCurrentScenario(cache, scenario) { + this.setProfile(null); + this.setGridSize(env.DEFAULT_GRID_SIZE); + this.setOrigin(env.DEFAULT_ORIGIN); this.queryParams = {}; - this.extractArgs = ''; - this.contractArgs = ''; - this.partitionArgs = ''; - this.customizeArgs = ''; - this.loaderArgs = ''; - this.environment = Object.assign({}, this.DEFAULT_ENVIRONMENT); + this.extractArgs = []; + this.contractArgs = []; + this.partitionArgs = []; + this.customizeArgs = []; + this.loaderArgs = []; + // environment will be patched eg. for OSRM_RASTER_SOURCE + this.environment = Object.assign({}, process.env); + // this.environment.CUCUMBER_TEST = 'ON'; + // process.report.reportOnSignal = false; this.resetOSM(); - // Set up feature cache - const mockFeature = { getUri: () => testCase.pickle.uri }; - this.setupFeatureCache(mockFeature); - - this.scenarioID = this.getScenarioID(testCase); - this.setupScenarioCache(this.scenarioID); - - // Setup output logging - const logDir = path.join(this.LOGS_PATH, this.featureID || 'default'); - this.scenarioLogFile = `${path.join(logDir, this.scenarioID)}.log`; - d3.queue(1) - .defer(createDir, logDir) - .defer((callback) => - fs.rm(this.scenarioLogFile, { force: true }, callback), - ) - .awaitAll(callback); + const basename = cache.getCacheBaseName(scenario); + + this.osmCacheFile = `${basename}.osm`; + this.osrmCacheFile = `${basename}.osrm`; + this.rasterCacheFile = `${basename}_raster.asc`; + this.speedsCacheFile = `${basename}_speeds.csv`; + this.penaltiesCacheFile = `${basename}_penalties.csv`; + this.profileCacheFile = `${basename}_profile.lua`; } - // Cleanup method called from After hook - cleanup(callback) { - this.resetOptionsOutput(); - if (this.osrmLoader) { - this.osrmLoader.shutdown(() => { - callback(); - }); - } else { - callback(); + /** + * Saves the output from a completed child process for testing against. + * + * @param {child_process} child The child process + */ + saveChildOutput(child) { + this.stderr = child.stderr.toString(); + this.stdout = child.stdout.toString(); + this.exitCode = child.status; + this.termSignal = child.signal; + } + + resetChildOutput() { + this.stdout = ''; + this.stderr = ''; + this.exitCode = null; + this.termSignal = null; + } + + /** + * Replaces placeholders in gherkin commands + * + * eg. it replaces {osm_file} with the input file path. + */ + expandOptions(options) { + const table = { + 'osm_file' : this.osmCacheFile, + 'processed_file' : this.osrmCacheFile, + 'profile_file' : this.profileFile, + 'rastersource_file' : this.rasterCacheFile, + 'speeds_file' : this.speedsCacheFile, + 'penalties_file' : this.penaltiesCacheFile, + 'timezone_names' : process.platform === 'win32' ? 'win' : 'iana' + }; + + function replacer(_match, p1) { + return table[p1] || p1; } + + options = options.replaceAll(/\{(\w+)\}/g, replacer); + return options.split(/\s+/); } } diff --git a/features/testbot/alternative_loop.feature b/features/testbot/alternative_loop.feature index 1e2925f1889..27d87d323ca 100644 --- a/features/testbot/alternative_loop.feature +++ b/features/testbot/alternative_loop.feature @@ -1,4 +1,4 @@ -@routing @testbot @alternative +@routing @testbot @alternative @isolated @todo Feature: Alternative route Background: @@ -40,7 +40,7 @@ Feature: Alternative route | 7 | 8 | ca,ab,bd,dc,ca,ca | | - @mld + @with_mld Scenario: Alternative loop paths on a single node with an asymmetric circle # The test checks only MLD implementation, alternatives results are unpredictable for CH on windows (#4691, #4693) Given a grid size of 10 meters diff --git a/features/testbot/load.feature b/features/testbot/load.feature index f195e8b35b7..aadcfba01a9 100644 --- a/features/testbot/load.feature +++ b/features/testbot/load.feature @@ -1,5 +1,11 @@ -@routing @load @testbot +@routing @load @testbot @todo @isolated @no_datastore Feature: Ways of loading data + +# TEST BROKEN: we don't support changing the load method in mid-run. +# With the datastore load method an osrm-routed will be constantly +# running in the background. No second osrm-routed can open the same +# port again. + # Several scenarios that change between direct/datastore makes # it easier to check that the test framework behaves as expected. diff --git a/features/testbot/multi_level_routing.feature b/features/testbot/multi_level_routing.feature index 5ffabec5e4a..66f011425ee 100644 --- a/features/testbot/multi_level_routing.feature +++ b/features/testbot/multi_level_routing.feature @@ -1,4 +1,4 @@ -@routing @testbot @mld +@routing @testbot @with_mld Feature: Multi level routing Background: diff --git a/features/testbot/status.feature b/features/testbot/status.feature index 0c52a46d694..0f3c85f8805 100644 --- a/features/testbot/status.feature +++ b/features/testbot/status.feature @@ -57,8 +57,8 @@ Feature: Status messages | nonsense | 400 | URL string malformed close to position 9: "nse" | | nonsense/v1/driving/1,1;1,2 | 400 | Service nonsense not found! | | | 400 | URL string malformed close to position 1: "/" | - | / | 400 | URL string malformed close to position 1: "//" | - | ? | 400 | URL string malformed close to position 1: "/" | + | / | 400 | URL string malformed close to position 1: "/" | + | ? | 400 | URL string malformed close to position 1: "/" | | route/v1/driving | 400 | URL string malformed close to position 17: "ing" | | route/v1/driving/ | 400 | URL string malformed close to position 18: "ng/" | | route/v1/driving/1 | 400 | Query string malformed close to position 19 | diff --git a/package-lock.json b/package-lock.json index 02b4c3fe3f9..d946516f487 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@babel/plugin-transform-class-properties": "^7.18.10", "@babel/preset-env": "^7.18.10", "@babel/preset-react": "^7.18.6", - "@cucumber/cucumber": "^12.1.0", + "@cucumber/cucumber": "^12.5.0", "@mapbox/polyline": "1.2.1", "@turf/turf": "7.2.0", "acorn": "8.14.1", @@ -44,6 +44,7 @@ "node-cmake": "^2.5.1", "tape": "5.9.0", "uglify-js": "^3.17.0", + "wait-on": "^9.0.3", "xmlbuilder": "15.1.1" }, "engines": { @@ -1946,30 +1947,30 @@ } }, "node_modules/@cucumber/ci-environment": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@cucumber/ci-environment/-/ci-environment-10.0.1.tgz", - "integrity": "sha512-/+ooDMPtKSmvcPMDYnMZt4LuoipfFfHaYspStI4shqw8FyKcfQAmekz6G+QKWjQQrvM+7Hkljwx58MEwPCwwzg==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/ci-environment/-/ci-environment-12.0.0.tgz", + "integrity": "sha512-SqCEnbCNl3zCXCFpqGUuoaSNhLC0jLw4tKeFcAxTw9MD/QRlJjeAC/fyvVLFuXuSq0OunJlFfxLu+Z3HE+oLPg==", "dev": true, "license": "MIT" }, "node_modules/@cucumber/cucumber": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-12.2.0.tgz", - "integrity": "sha512-b7W4snvXYi1T2puUjxamASCCNhNzVSzb/fQUuGSkdjm/AFfJ24jo8kOHQyOcaoArCG71sVQci4vkZaITzl/V1w==", + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-12.5.0.tgz", + "integrity": "sha512-+VWxkIIpm5EWFfaF3grP1GlHobzlDBIF54FqJutdYmfpx3LJc+IS8uWdIN97m6zxizo5CPrUopTWkxzwVswUzg==", "dev": true, "license": "MIT", "dependencies": { - "@cucumber/ci-environment": "10.0.1", + "@cucumber/ci-environment": "12.0.0", "@cucumber/cucumber-expressions": "18.0.1", - "@cucumber/gherkin": "34.0.0", - "@cucumber/gherkin-streams": "5.0.1", - "@cucumber/gherkin-utils": "9.2.0", - "@cucumber/html-formatter": "21.14.0", - "@cucumber/junit-xml-formatter": "0.8.1", + "@cucumber/gherkin": "37.0.1", + "@cucumber/gherkin-streams": "6.0.0", + "@cucumber/gherkin-utils": "10.0.0", + "@cucumber/html-formatter": "22.3.0", + "@cucumber/junit-xml-formatter": "0.9.0", "@cucumber/message-streams": "4.0.1", - "@cucumber/messages": "28.1.0", + "@cucumber/messages": "31.1.0", "@cucumber/pretty-formatter": "1.0.1", - "@cucumber/tag-expressions": "6.2.0", + "@cucumber/tag-expressions": "8.1.0", "assertion-error-formatter": "^3.0.0", "capital-case": "^1.0.4", "chalk": "^4.1.2", @@ -1978,7 +1979,7 @@ "debug": "^4.3.4", "error-stack-parser": "^2.1.4", "figures": "^3.2.0", - "glob": "^11.0.0", + "glob": "^13.0.0", "has-ansi": "^4.0.1", "indent-string": "^4.0.0", "is-installed-globally": "^0.4.0", @@ -1986,19 +1987,19 @@ "knuth-shuffle-seeded": "^1.0.6", "lodash.merge": "^4.6.2", "lodash.mergewith": "^4.6.2", - "luxon": "3.7.1", + "luxon": "3.7.2", "mime": "^3.0.0", "mkdirp": "^3.0.0", "mz": "^2.7.0", "progress": "^2.0.3", - "read-package-up": "^11.0.0", - "semver": "7.7.2", + "read-package-up": "^12.0.0", + "semver": "7.7.3", "string-argv": "0.3.1", "supports-color": "^8.1.1", "type-fest": "^4.41.0", "util-arity": "^1.1.0", "yaml": "^2.2.2", - "yup": "1.7.0" + "yup": "1.7.1" }, "bin": { "cucumber-js": "bin/cucumber.js" @@ -2021,16 +2022,14 @@ } }, "node_modules/@cucumber/cucumber/node_modules/@cucumber/messages": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-28.1.0.tgz", - "integrity": "sha512-2LzZtOwYKNlCuNf31ajkrekoy2M4z0Z1QGiPH40n4gf5t8VOUFb7m1ojtR4LmGvZxBGvJZP8voOmRqDWzBzYKA==", + "version": "31.1.0", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-31.1.0.tgz", + "integrity": "sha512-BViwUQ9YMjcGL98Ww2QHMgu3S4JLUjbTz+Jo/jsq+8ZjS47/2v3IszpD6e12Y6IzZoGfrZriauZHPQ4PAmN9XA==", "dev": true, "license": "MIT", "dependencies": { - "@types/uuid": "10.0.0", "class-transformer": "0.5.1", - "reflect-metadata": "0.2.2", - "uuid": "11.1.0" + "reflect-metadata": "0.2.2" } }, "node_modules/@cucumber/cucumber/node_modules/ansi-regex": { @@ -2131,22 +2130,16 @@ } }, "node_modules/@cucumber/cucumber/node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "minimatch": "^10.1.1", "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, "engines": { "node": "20 || >=22" }, @@ -2191,11 +2184,11 @@ } }, "node_modules/@cucumber/cucumber/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/brace-expansion": "^5.0.0" }, @@ -2230,9 +2223,9 @@ "license": "MIT" }, "node_modules/@cucumber/cucumber/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -2271,38 +2264,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@cucumber/cucumber/node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, "node_modules/@cucumber/gherkin": { - "version": "34.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-34.0.0.tgz", - "integrity": "sha512-659CCFsrsyvuBi/Eix1fnhSheMnojSfnBcqJ3IMPNawx7JlrNJDcXYSSdxcUw3n/nG05P+ptCjmiZY3i14p+tA==", + "version": "37.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-37.0.1.tgz", + "integrity": "sha512-VmX+PKa9vqKZiycZoQKYlCsA0N7gAfiOfrcHSjK+suEVUwvKEH2sjO47NznrFFLmVWYTRmw3DLHQnpBAznkYEA==", "dev": true, "license": "MIT", "dependencies": { - "@cucumber/messages": ">=19.1.4 <29" + "@cucumber/messages": ">=31.0.0 <32" } }, "node_modules/@cucumber/gherkin-streams": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin-streams/-/gherkin-streams-5.0.1.tgz", - "integrity": "sha512-/7VkIE/ASxIP/jd4Crlp4JHXqdNFxPGQokqWqsaCCiqBiu5qHoKMxcWNlp9njVL/n9yN4S08OmY3ZR8uC5x74Q==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-streams/-/gherkin-streams-6.0.0.tgz", + "integrity": "sha512-HLSHMmdDH0vCr7vsVEURcDA4WwnRLdjkhqr6a4HQ3i4RFK1wiDGPjBGVdGJLyuXuRdJpJbFc6QxHvT8pU4t6jw==", "dev": true, "license": "MIT", "dependencies": { - "commander": "9.1.0", + "commander": "14.0.0", "source-map-support": "0.5.21" }, "bin": { @@ -2315,13 +2294,13 @@ } }, "node_modules/@cucumber/gherkin-streams/node_modules/commander": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.1.0.tgz", - "integrity": "sha512-i0/MaqBtdbnJ4XQs4Pmyb+oFQl+q0lsAmokVUH92SlSw4fkeAcG3bVon+Qt7hmtF+u3Het6o4VgrcY3qAoEB6w==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", "dev": true, "license": "MIT", "engines": { - "node": "^12.20.0 || >=14" + "node": ">=20" } }, "node_modules/@cucumber/gherkin-streams/node_modules/source-map": { @@ -2346,16 +2325,16 @@ } }, "node_modules/@cucumber/gherkin-utils": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-9.2.0.tgz", - "integrity": "sha512-3nmRbG1bUAZP3fAaUBNmqWO0z0OSkykZZotfLjyhc8KWwDSOrOmMJlBTd474lpA8EWh4JFLAX3iXgynBqBvKzw==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-10.0.0.tgz", + "integrity": "sha512-BcujlDT343GXXNrMPl3ws6Il3zs8dQw3Yp/d3HnOJF8i2snGGgiapoTbko7MdvAt7ivDL7SDo+e1d5Cnpl3llA==", "dev": true, "license": "MIT", "dependencies": { - "@cucumber/gherkin": "^31.0.0", - "@cucumber/messages": "^27.0.0", + "@cucumber/gherkin": "^34.0.0", + "@cucumber/messages": "^29.0.0", "@teppeis/multimaps": "3.0.0", - "commander": "13.1.0", + "commander": "14.0.0", "source-map-support": "^0.5.21" }, "bin": { @@ -2363,36 +2342,47 @@ } }, "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-31.0.0.tgz", - "integrity": "sha512-wlZfdPif7JpBWJdqvHk1Mkr21L5vl4EfxVUOS4JinWGf3FLRV6IKUekBv5bb5VX79fkDcfDvESzcQ8WQc07Wgw==", + "version": "34.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-34.0.0.tgz", + "integrity": "sha512-659CCFsrsyvuBi/Eix1fnhSheMnojSfnBcqJ3IMPNawx7JlrNJDcXYSSdxcUw3n/nG05P+ptCjmiZY3i14p+tA==", "dev": true, "license": "MIT", "dependencies": { - "@cucumber/messages": ">=19.1.4 <=26" + "@cucumber/messages": ">=19.1.4 <29" } }, "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin/node_modules/@cucumber/messages": { - "version": "26.0.1", - "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-26.0.1.tgz", - "integrity": "sha512-DIxSg+ZGariumO+Lq6bn4kOUIUET83A4umrnWmidjGFl8XxkBieUZtsmNbLYgH/gnsmP07EfxxdTr0hOchV1Sg==", + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-28.1.0.tgz", + "integrity": "sha512-2LzZtOwYKNlCuNf31ajkrekoy2M4z0Z1QGiPH40n4gf5t8VOUFb7m1ojtR4LmGvZxBGvJZP8voOmRqDWzBzYKA==", "dev": true, "license": "MIT", "dependencies": { "@types/uuid": "10.0.0", "class-transformer": "0.5.1", "reflect-metadata": "0.2.2", - "uuid": "10.0.0" + "uuid": "11.1.0" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/messages": { + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-29.0.1.tgz", + "integrity": "sha512-aAvIYfQD6/aBdF8KFQChC3CQ1Q+GX9orlR6GurGiX6oqaCnBkxA4WU3OQUVepDynEFrPayerqKRFcAMhdcXReQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "class-transformer": "0.5.1", + "reflect-metadata": "0.2.2" } }, "node_modules/@cucumber/gherkin-utils/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@cucumber/gherkin-utils/node_modules/source-map": { @@ -2417,9 +2407,9 @@ } }, "node_modules/@cucumber/gherkin-utils/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "dev": true, "funding": [ "https://github.com/sponsors/broofa", @@ -2427,13 +2417,24 @@ ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/@cucumber/gherkin/node_modules/@cucumber/messages": { + "version": "31.1.0", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-31.1.0.tgz", + "integrity": "sha512-BViwUQ9YMjcGL98Ww2QHMgu3S4JLUjbTz+Jo/jsq+8ZjS47/2v3IszpD6e12Y6IzZoGfrZriauZHPQ4PAmN9XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "class-transformer": "0.5.1", + "reflect-metadata": "0.2.2" } }, "node_modules/@cucumber/html-formatter": { - "version": "21.14.0", - "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-21.14.0.tgz", - "integrity": "sha512-vQqbmQZc0QiN4c+cMCffCItpODJlOlYtPG7pH6We096dBOa7u0ttDMjT6KrMAnQlcln54rHL46r408IFpuznAw==", + "version": "22.3.0", + "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-22.3.0.tgz", + "integrity": "sha512-0s3G7kznCRDiiesQ4K0yBdswGqU9E0j2AWUug41NpedBzhaY+Hn192ANRF597GZtuWrCjE53aFb3fOyOsT8B+g==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2441,13 +2442,13 @@ } }, "node_modules/@cucumber/junit-xml-formatter": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cucumber/junit-xml-formatter/-/junit-xml-formatter-0.8.1.tgz", - "integrity": "sha512-FT1Y96pyd9/ifbE9I7dbkTCjkwEdW9C0MBobUZoKD13c8EnWAt0xl1Yy/v/WZLTk4XfCLte1DATtLx01jt+YiA==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@cucumber/junit-xml-formatter/-/junit-xml-formatter-0.9.0.tgz", + "integrity": "sha512-WF+A7pBaXpKMD1i7K59Nk5519zj4extxY4+4nSgv5XLsGXHDf1gJnb84BkLUzevNtp2o2QzMG0vWLwSm8V5blw==", "dev": true, "license": "MIT", "dependencies": { - "@cucumber/query": "^13.0.2", + "@cucumber/query": "^14.0.1", "@teppeis/multimaps": "^3.0.0", "luxon": "^3.5.0", "xmlbuilder": "^15.1.1" @@ -2472,6 +2473,7 @@ "integrity": "sha512-f2o/HqKHgsqzFLdq6fAhfG1FNOQPdBdyMGpKwhb7hZqg0yZtx9BVqkTyuoNk83Fcvk3wjMVfouFXXHNEk4nddA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/uuid": "10.0.0", "class-transformer": "0.5.1", @@ -2510,9 +2512,9 @@ } }, "node_modules/@cucumber/query": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/@cucumber/query/-/query-13.6.0.tgz", - "integrity": "sha512-tiDneuD5MoWsJ9VKPBmQok31mSX9Ybl+U4wqDoXeZgsXHDURqzM3rnpWVV3bC34y9W6vuFxrlwF/m7HdOxwqRw==", + "version": "14.7.0", + "resolved": "https://registry.npmjs.org/@cucumber/query/-/query-14.7.0.tgz", + "integrity": "sha512-fiqZ4gMEgYjmbuWproF/YeCdD5y+gD2BqgBIGbpihOsx6UlNsyzoDSfO+Tny0q65DxfK+pHo2UkPyEl7dO7wmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2524,9 +2526,9 @@ } }, "node_modules/@cucumber/tag-expressions": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-6.2.0.tgz", - "integrity": "sha512-KIF0eLcafHbWOuSDWFw0lMmgJOLdDRWjEL1kfXEWrqHmx2119HxVAr35WuEd9z542d3Yyg+XNqSr+81rIKqEdg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-8.1.0.tgz", + "integrity": "sha512-UFeOVUyc711/E7VHjThxMwg3jbGod9TlbM1gxNixX/AGDKg82Eha4cE0tKki3GGUs7uB2NyI+hQAuhB8rL2h5A==", "dev": true, "license": "MIT" }, @@ -2842,6 +2844,60 @@ "node": ">= 0.8.0" } }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.4.tgz", + "integrity": "sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2931,109 +2987,6 @@ "node": "20 || >=22" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -3229,6 +3182,13 @@ "license": "MIT", "optional": true }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@teppeis/multimaps": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@teppeis/multimaps/-/multimaps-3.0.0.tgz", @@ -6516,6 +6476,13 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/atob": { "version": "2.1.2", "dev": true, @@ -6567,6 +6534,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-cli": { "version": "6.26.0", "dev": true, @@ -9385,6 +9364,19 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/comma-separated-tokens": { "version": "1.0.8", "dev": true, @@ -10057,6 +10049,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -12756,6 +12758,8 @@ }, "node_modules/faucet": { "version": "0.0.4", + "resolved": "https://registry.npmjs.org/faucet/-/faucet-0.0.4.tgz", + "integrity": "sha512-vSUB+9iT2n77DPv2x3zYEt3rEIGIhVZJmNTfrx3Y0XVXlBiNOPq2jJVOucqkZ6MNHCgVQUZ5xxe78LkkYCRSFg==", "dev": true, "license": "MIT", "dependencies": { @@ -12851,11 +12855,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/faucet/node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "license": "ISC" - }, "node_modules/faucet/node_modules/isarray": { "version": "0.0.1", "dev": true, @@ -13138,6 +13137,27 @@ "dev": true, "license": "MIT" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -13180,88 +13200,44 @@ "dev": true, "license": "MIT" }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 6" } }, - "node_modules/foreground-child/node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, "engines": { - "node": ">= 8" + "node": ">= 0.6" } }, - "node_modules/foreground-child/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", "dependencies": { - "shebang-regex": "^3.0.0" + "mime-db": "1.52.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" + "node": ">= 0.6" } }, "node_modules/forwarded": { @@ -14517,9 +14493,9 @@ } }, "node_modules/index-to-position": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz", - "integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", "dev": true, "license": "MIT", "engines": { @@ -15479,20 +15455,23 @@ "node": ">= 0.4" } }, - "node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "node_modules/joi": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz", + "integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "BSD-3-Clause", "dependencies": { - "@isaacs/cliui": "^8.0.2" + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.0.0" }, "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 20" } }, "node_modules/js-base64": { @@ -16339,9 +16318,9 @@ "license": "ISC" }, "node_modules/luxon": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz", - "integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", "dev": true, "license": "MIT", "engines": { @@ -18766,13 +18745,6 @@ "node": ">=6" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/pad-right": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/pad-right/-/pad-right-0.2.2.tgz", @@ -19054,9 +19026,9 @@ } }, "node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -19071,11 +19043,11 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", - "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } @@ -19648,6 +19620,13 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, "node_modules/pseudomap": { "version": "1.0.2", "dev": true, @@ -19898,56 +19877,59 @@ } }, "node_modules/read-package-up": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", - "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-12.0.0.tgz", + "integrity": "sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw==", "dev": true, "license": "MIT", "dependencies": { - "find-up-simple": "^1.0.0", - "read-pkg": "^9.0.0", - "type-fest": "^4.6.0" + "find-up-simple": "^1.0.1", + "read-pkg": "^10.0.0", + "type-fest": "^5.2.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/read-package-up/node_modules/hosted-git-info": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", + "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", "dev": true, "license": "ISC", "dependencies": { - "lru-cache": "^10.0.1" + "lru-cache": "^11.1.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/read-package-up/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/read-package-up/node_modules/normalize-package-data": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", - "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz", + "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "hosted-git-info": "^7.0.0", + "hosted-git-info": "^9.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/read-package-up/node_modules/parse-json": { @@ -19968,30 +19950,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/read-package-up/node_modules/parse-json/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/read-package-up/node_modules/read-pkg": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", - "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-10.0.0.tgz", + "integrity": "sha512-A70UlgfNdKI5NSvTTfHzLQj7NJRpJ4mT5tGafkllJ4wh71oYuGm/pzphHcmW4s35iox56KSK721AihodoXSc/A==", "dev": true, "license": "MIT", "dependencies": { - "@types/normalize-package-data": "^2.4.3", - "normalize-package-data": "^6.0.0", - "parse-json": "^8.0.0", - "type-fest": "^4.6.0", - "unicorn-magic": "^0.1.0" + "@types/normalize-package-data": "^2.4.4", + "normalize-package-data": "^8.0.0", + "parse-json": "^8.3.0", + "type-fest": "^5.2.0", + "unicorn-magic": "^0.3.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/read-package-up/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -20002,13 +19997,16 @@ } }, "node_modules/read-package-up/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.1.tgz", + "integrity": "sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==", "dev": true, "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, "engines": { - "node": ">=16" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -21202,6 +21200,16 @@ "rx-lite": "*" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/sade": { "version": "1.8.1", "dev": true, @@ -22208,55 +22216,6 @@ "node": ">=0.10.0" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -22395,30 +22354,6 @@ "node": ">=0.10.0" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "2.0.0", "dev": true, @@ -22636,6 +22571,19 @@ "node": ">=4" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tap-parser": { "version": "0.7.0", "dev": true, @@ -23313,9 +23261,9 @@ } }, "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", "dev": true, "license": "MIT", "engines": { @@ -23667,6 +23615,7 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", + "peer": true, "bin": { "uuid": "dist/esm/bin/uuid" } @@ -24064,6 +24013,26 @@ "he": "^1.2.0" } }, + "node_modules/wait-on": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.3.tgz", + "integrity": "sha512-13zBnyYvFDW1rBvWiJ6Av3ymAaq8EDQuvxZnPIw3g04UqGi4TyoIJABmfJ6zrvKo9yeFQExNkOk7idQbDJcuKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.13.2", + "joi": "^18.0.1", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/web-namespaces": { "version": "2.0.1", "dev": true, @@ -24267,102 +24236,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrappy": { "version": "1.0.2", "dev": true, @@ -24465,9 +24338,9 @@ } }, "node_modules/yup": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/yup/-/yup-1.7.0.tgz", - "integrity": "sha512-VJce62dBd+JQvoc+fCVq+KZfPHr+hXaxCcVgotfwWvlR0Ja3ffYKaJBT8rptPOSKOGJDCUnW2C2JWpud7aRP6Q==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz", + "integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 8ae55a2b7bd..b4614127795 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ }, "scripts": { "lint": "npx eslint features/", - "test": "npm run lint && ./scripts/cucumber_test_matrix.sh", + "test": "./scripts/cucumber_test_matrix.sh", "clean": "rm -rf test/cache", "docs": "./scripts/build_api_docs.sh", "install": "node-pre-gyp install --fallback-to-build=false || ./scripts/node_install.sh", @@ -42,7 +42,7 @@ "@babel/plugin-transform-class-properties": "^7.18.10", "@babel/preset-env": "^7.18.10", "@babel/preset-react": "^7.18.6", - "@cucumber/cucumber": "^12.1.0", + "@cucumber/cucumber": "^12.5.0", "@mapbox/polyline": "1.2.1", "@turf/turf": "7.2.0", "acorn": "8.14.1", @@ -67,6 +67,7 @@ "node-cmake": "^2.5.1", "tape": "5.9.0", "uglify-js": "^3.17.0", + "wait-on": "^9.0.3", "xmlbuilder": "15.1.1" }, "main": "lib/index.js", diff --git a/scripts/cucumber_test_matrix.sh b/scripts/cucumber_test_matrix.sh index 2738068f305..9a06fa98bac 100755 --- a/scripts/cucumber_test_matrix.sh +++ b/scripts/cucumber_test_matrix.sh @@ -2,17 +2,31 @@ set -o errexit set -o pipefail -set -o nounset +# set -o nounset -loadmethods=(datastore mmap directly) -profiles=(ch mld) +loadmethods=(mmap directly datastore) +algorithms=(ch mld) +base=home -for profile in "${profiles[@]}" +if [ -n "$GITHUB_STEP_SUMMARY" ]; then + base=github + echo "### Cucumber Test Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "|Algorithm|Load Method|Passed|Skipped|Failed|Elapsed (s)|" >> $GITHUB_STEP_SUMMARY + echo "|:------- |:--------- | ----:| -----:| ----:| ---------:|" >> $GITHUB_STEP_SUMMARY +fi + +for algorithm in "${algorithms[@]}" do + export algorithm for loadmethod in "${loadmethods[@]}" do + export loadmethod + if [ -n "$GITHUB_STEP_SUMMARY" ]; then + echo -n "| $algorithm | $loadmethod " >> $GITHUB_STEP_SUMMARY + fi set -x - OSRM_LOAD_METHOD=$loadmethod npx cucumber-js features/ -p $profile + npx cucumber-js -p $base -p $algorithm -p $loadmethod $@ { set +x; } 2>/dev/null done done diff --git a/test/.gitignore b/test/.gitignore index ea5e38ea809..3aa307d9987 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -1,3 +1,4 @@ /server.ini /cache +/logs /speedprofile.ini