diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..d6d517dcc --- /dev/null +++ b/.eslintrc @@ -0,0 +1,28 @@ +{ + "rules": { + "indent": [ + 2, + 4 + ], + "quotes": [ + 1, + "single" + ], + "linebreak-style": [ + 2, + "unix" + ], + "semi": [ + 2, + "always" + ], + "no-console": [ + 1 + ] + }, + "env": { + "es6": true, + "node": true + }, + "extends": "eslint:recommended" +} diff --git a/.gitignore b/.gitignore index c94aeb428..0bb27484c 100644 --- a/.gitignore +++ b/.gitignore @@ -73,7 +73,12 @@ stxxl.errlog ################### /sandbox/ +# Test related files # +###################### /test/profile.lua +/test/cache +/test/speeds.csv +node_modules # Deprecated config file # ########################## diff --git a/.travis.yml b/.travis.yml index a80b2cd5f..6da053d1f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,7 @@ matrix: addons: &gcc5 apt: sources: ['ubuntu-toolchain-r-test'] - packages: ['g++-5', 'libbz2-dev', 'libstxxl-dev', 'libstxxl1', 'libxml2-dev', 'libzip-dev', 'lua5.1', 'liblua5.1-0-dev', 'rubygems-integration', 'libtbb-dev', 'libgdal-dev', 'libluabind-dev', 'libboost-all-dev'] + packages: ['g++-5', 'libbz2-dev', 'libstxxl-dev', 'libstxxl1', 'libxml2-dev', 'libzip-dev', 'lua5.1', 'liblua5.1-0-dev', 'libtbb-dev', 'libgdal-dev', 'libluabind-dev', 'libboost-all-dev'] env: COMPILER='g++-5' BUILD_TYPE='Debug' - os: linux @@ -34,7 +34,7 @@ matrix: addons: &gcc48 apt: sources: ['ubuntu-toolchain-r-test'] - packages: ['g++-4.8', 'libbz2-dev', 'libstxxl-dev', 'libstxxl1', 'libxml2-dev', 'libzip-dev', 'lua5.1', 'liblua5.1-0-dev', 'rubygems-integration', 'libtbb-dev', 'libgdal-dev', 'libluabind-dev', 'libboost-all-dev'] + packages: ['g++-4.8', 'libbz2-dev', 'libstxxl-dev', 'libstxxl1', 'libxml2-dev', 'libzip-dev', 'lua5.1', 'liblua5.1-0-dev', 'libtbb-dev', 'libgdal-dev', 'libluabind-dev', 'libboost-all-dev'] env: COMPILER='g++-4.8' BUILD_TYPE='Debug' - os: linux @@ -42,7 +42,7 @@ matrix: addons: &clang38 apt: sources: ['llvm-toolchain-precise', 'ubuntu-toolchain-r-test'] - packages: ['clang-3.8', 'libbz2-dev', 'libstxxl-dev', 'libstxxl1', 'libxml2-dev', 'libzip-dev', 'lua5.1', 'liblua5.1-0-dev', 'rubygems-integration', 'libtbb-dev', 'libgdal-dev', 'libluabind-dev', 'libboost-all-dev'] + packages: ['clang-3.8', 'libbz2-dev', 'libstxxl-dev', 'libstxxl1', 'libxml2-dev', 'libzip-dev', 'lua5.1', 'liblua5.1-0-dev', 'libtbb-dev', 'libgdal-dev', 'libluabind-dev', 'libboost-all-dev'] env: COMPILER='clang++-3.8' BUILD_TYPE='Debug' RUN_CLANG_FORMAT=ON - os: osx @@ -56,7 +56,7 @@ matrix: addons: &gcc5 apt: sources: ['ubuntu-toolchain-r-test'] - packages: ['g++-5', 'libbz2-dev', 'libstxxl-dev', 'libstxxl1', 'libxml2-dev', 'libzip-dev', 'lua5.1', 'liblua5.1-0-dev', 'rubygems-integration', 'libtbb-dev', 'libgdal-dev', 'libluabind-dev', 'libboost-all-dev'] + packages: ['g++-5', 'libbz2-dev', 'libstxxl-dev', 'libstxxl1', 'libxml2-dev', 'libzip-dev', 'lua5.1', 'liblua5.1-0-dev', 'libtbb-dev', 'libgdal-dev', 'libluabind-dev', 'libboost-all-dev'] env: COMPILER='g++-5' BUILD_TYPE='Release' - os: linux @@ -64,7 +64,7 @@ matrix: addons: &gcc48 apt: sources: ['ubuntu-toolchain-r-test'] - packages: ['g++-4.8', 'libbz2-dev', 'libstxxl-dev', 'libstxxl1', 'libxml2-dev', 'libzip-dev', 'lua5.1', 'liblua5.1-0-dev', 'rubygems-integration', 'libtbb-dev', 'libgdal-dev', 'libluabind-dev', 'libboost-all-dev'] + packages: ['g++-4.8', 'libbz2-dev', 'libstxxl-dev', 'libstxxl1', 'libxml2-dev', 'libzip-dev', 'lua5.1', 'liblua5.1-0-dev', 'libtbb-dev', 'libgdal-dev', 'libluabind-dev', 'libboost-all-dev'] env: COMPILER='g++-4.8' BUILD_TYPE='Release' - os: linux @@ -72,7 +72,7 @@ matrix: addons: &clang38 apt: sources: ['llvm-toolchain-precise', 'ubuntu-toolchain-r-test'] - packages: ['clang-3.8', 'libbz2-dev', 'libstxxl-dev', 'libstxxl1', 'libxml2-dev', 'libzip-dev', 'lua5.1', 'liblua5.1-0-dev', 'rubygems-integration', 'libtbb-dev', 'libgdal-dev', 'libluabind-dev', 'libboost-all-dev'] + packages: ['clang-3.8', 'libbz2-dev', 'libstxxl-dev', 'libstxxl1', 'libxml2-dev', 'libzip-dev', 'lua5.1', 'liblua5.1-0-dev', 'libtbb-dev', 'libgdal-dev', 'libluabind-dev', 'libboost-all-dev'] env: COMPILER='clang++-3.8' BUILD_TYPE='Release' - os: osx @@ -86,7 +86,7 @@ matrix: addons: &gcc5 apt: sources: ['ubuntu-toolchain-r-test'] - packages: ['g++-5', 'libbz2-dev', 'libstxxl-dev', 'libstxxl1', 'libxml2-dev', 'libzip-dev', 'lua5.1', 'liblua5.1-0-dev', 'rubygems-integration', 'libtbb-dev', 'libgdal-dev', 'libluabind-dev', 'libboost-all-dev'] + packages: ['g++-5', 'libbz2-dev', 'libstxxl-dev', 'libstxxl1', 'libxml2-dev', 'libzip-dev', 'lua5.1', 'liblua5.1-0-dev', 'libtbb-dev', 'libgdal-dev', 'libluabind-dev', 'libboost-all-dev'] env: COMPILER='g++-5' BUILD_TYPE='Release' BUILD_SHARED_LIBS=ON - os: linux @@ -94,7 +94,7 @@ matrix: addons: &clang38 apt: sources: ['llvm-toolchain-precise', 'ubuntu-toolchain-r-test'] - packages: ['clang-3.8', 'libbz2-dev', 'libstxxl-dev', 'libstxxl1', 'libxml2-dev', 'libzip-dev', 'lua5.1', 'liblua5.1-0-dev', 'rubygems-integration', 'libtbb-dev', 'libgdal-dev', 'libluabind-dev', 'libboost-all-dev'] + packages: ['clang-3.8', 'libbz2-dev', 'libstxxl-dev', 'libstxxl1', 'libxml2-dev', 'libzip-dev', 'lua5.1', 'liblua5.1-0-dev', 'libtbb-dev', 'libgdal-dev', 'libluabind-dev', 'libboost-all-dev'] env: COMPILER='clang++-3.8' BUILD_TYPE='Release' BUILD_SHARED_LIBS=ON @@ -121,8 +121,11 @@ matrix: # compiler: clang # env: COMPILER='clang++' BUILD_TYPE='Release' BUILD_SHARED_LIBS=ON +before_install: + - source ./scripts/install_node.sh 4 install: + - npm install - DEPS_DIR="${TRAVIS_BUILD_DIR}/deps" - mkdir -p ${DEPS_DIR} && cd ${DEPS_DIR} - | @@ -142,12 +145,9 @@ before_script: if [[ "${TRAVIS_OS_NAME}" == "linux" ]]; then ./scripts/check_taginfo.py taginfo.json profiles/car.lua fi - - rvm use 1.9.3 - - gem install bundler - - bundle install - mkdir build && pushd build - export CXX=${COMPILER} - - export OSRM_PORT=5000 OSRM_TIMEOUT=60 + - export OSRM_PORT=5000 OSRM_TIMEOUT=6000 - cmake .. -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DBUILD_SHARED_LIBS=${BUILD_SHARED_LIBS:-OFF} -DBUILD_TOOLS=1 -DENABLE_CCACHE=0 script: @@ -163,7 +163,7 @@ script: - ./engine-tests - ./util-tests - popd - - cucumber -p verify + - npm test - make -C test/data - mkdir example/build && pushd example/build - cmake .. diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 31d044bd6..000000000 --- a/Gemfile +++ /dev/null @@ -1,7 +0,0 @@ -source "http://rubygems.org" - -gem "cucumber" -gem "rake" -gem "osmlib-base" -gem "sys-proctable" -gem "rspec-expectations" diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 3363e9218..000000000 --- a/Gemfile.lock +++ /dev/null @@ -1,35 +0,0 @@ -GEM - remote: http://rubygems.org/ - specs: - builder (3.2.2) - cucumber (2.0.0) - builder (>= 2.1.2) - cucumber-core (~> 1.1.3) - diff-lcs (>= 1.1.3) - gherkin (~> 2.12) - multi_json (>= 1.7.5, < 2.0) - multi_test (>= 0.1.2) - cucumber-core (1.1.3) - gherkin (~> 2.12.0) - diff-lcs (1.2.5) - gherkin (2.12.2) - multi_json (~> 1.3) - multi_json (1.11.0) - multi_test (0.1.2) - osmlib-base (0.1.4) - rake (10.4.2) - rspec-expectations (3.2.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.2.0) - rspec-support (3.2.2) - sys-proctable (0.9.8) - -PLATFORMS - ruby - -DEPENDENCIES - cucumber - osmlib-base - rake - rspec-expectations - sys-proctable diff --git a/Rakefile b/Rakefile deleted file mode 100644 index 7b3d905a7..000000000 --- a/Rakefile +++ /dev/null @@ -1,190 +0,0 @@ -require 'OSM/StreamParser' -require 'socket' -require 'digest/sha1' -require 'cucumber/rake/task' -require 'sys/proctable' - -BUILD_FOLDER = 'build' -DATA_FOLDER = 'sandbox' -PROFILE = 'bicycle' -OSRM_PORT = 5000 -PROFILES_FOLDER = '../profiles' - -Cucumber::Rake::Task.new do |t| - t.cucumber_opts = %w{--format pretty} -end - -areas = { - :kbh => { :country => 'denmark', :bbox => 'top=55.6972 left=12.5222 right=12.624 bottom=55.6376' }, - :frd => { :country => 'denmark', :bbox => 'top=55.7007 left=12.4765 bottom=55.6576 right=12.5698' }, - :regh => { :country => 'denmark', :bbox => 'top=56.164 left=11.792 bottom=55.403 right=12.731' }, - :denmark => { :country => 'denmark', :bbox => nil }, - :skaane => { :country => 'sweden', :bbox => 'top=56.55 left=12.4 bottom=55.3 right=14.6' } -} - - - -osm_data_area_name = ARGV[1] ? ARGV[1].to_s.to_sym : :kbh -raise "Unknown data area." unless areas[osm_data_area_name] -osm_data_country = areas[osm_data_area_name][:country] -osm_data_area_bbox = areas[osm_data_area_name][:bbox] - - -task osm_data_area_name.to_sym {} #define empty task to prevent rake from whining. will break if area has same name as a task - - -def each_process name, &block - Sys::ProcTable.ps do |process| - if process.comm.strip == name.strip && process.state != 'zombie' - yield process.pid.to_i, process.state.strip - end - end -end - -def up? - find_pid('osrm-routed') != nil -end - -def find_pid name - each_process(name) { |pid,state| return pid.to_i } - return nil -end - -def wait_for_shutdown name - timeout = 10 - (timeout*10).times do - return if find_pid(name) == nil - sleep 0.1 - end - raise "*** Could not terminate #{name}." -end - - -desc "Rebuild and run tests." -task :default => [:build] - -desc "Build using CMake." -task :build do - if Dir.exists? BUILD_FOLDER - Dir.chdir BUILD_FOLDER do - system "make" - end - else - system "mkdir build; cd build; cmake ..; make" - end -end - -desc "Setup config files." -task :setup do -end - -desc "Download OSM data." -task :download do - Dir.mkdir "#{DATA_FOLDER}" unless File.exist? "#{DATA_FOLDER}" - puts "Downloading..." - puts "curl http://download.geofabrik.de/europe/#{osm_data_country}-latest.osm.pbf -o #{DATA_FOLDER}/#{osm_data_country}.osm.pbf" - raise "Error while downloading data." unless system "curl http://download.geofabrik.de/europe/#{osm_data_country}-latest.osm.pbf -o #{DATA_FOLDER}/#{osm_data_country}.osm.pbf" - if osm_data_area_bbox - puts "Cropping and converting to protobuffer..." - raise "Error while cropping data." unless system "osmosis --read-pbf file=#{DATA_FOLDER}/#{osm_data_country}.osm.pbf --bounding-box #{osm_data_area_bbox} --write-pbf file=#{DATA_FOLDER}/#{osm_data_area_name}.osm.pbf omitmetadata=true" - end -end - -desc "Crop OSM data" -task :crop do - if osm_data_area_bbox - raise "Error while cropping data." unless system "osmosis --read-pbf file=#{DATA_FOLDER}/#{osm_data_country}.osm.pbf --bounding-box #{osm_data_area_bbox} --write-pbf file=#{DATA_FOLDER}/#{osm_data_area_name}.osm.pbf omitmetadata=true" - end -end - -desc "Reprocess OSM data." -task :process => [:extract,:contract] do -end - -desc "Extract OSM data." -task :extract do - Dir.chdir DATA_FOLDER do - raise "Error while extracting data." unless system "../#{BUILD_FOLDER}/osrm-extract #{osm_data_area_name}.osm.pbf --profile ../profiles/#{PROFILE}.lua" - end -end - -desc "Contract OSM data." -task :contract do - Dir.chdir DATA_FOLDER do - raise "Error while contracting data." unless system "../#{BUILD_FOLDER}/osrm-contract #{osm_data_area_name}.osrm" - end -end - -desc "Delete preprocessing files." -task :clean do - File.delete *Dir.glob("#{DATA_FOLDER}/*.osrm") - File.delete *Dir.glob("#{DATA_FOLDER}/*.osrm.*") -end - -desc "Run all cucumber test" -task :test do - system "cucumber" - puts -end - -desc "Run the routing server in the terminal. Press Ctrl-C to stop." -task :run do - Dir.chdir DATA_FOLDER do - system "../#{BUILD_FOLDER}/osrm-routed #{osm_data_area_name}.osrm --port #{OSRM_PORT}" - end -end - -desc "Launch the routing server in the background. Use rake:down to stop it." -task :up do - Dir.chdir DATA_FOLDER do - abort("Already up.") if up? - pipe = IO.popen("../#{BUILD_FOLDER}/osrm-routed #{osm_data_area_name}.osrm --port #{OSRM_PORT} 1>>osrm-routed.log 2>>osrm-routed.log") - timeout = 5 - (timeout*10).times do - begin - socket = TCPSocket.new('localhost', OSRM_PORT) - socket.puts 'ping' - rescue Errno::ECONNREFUSED - sleep 0.1 - end - end - end -end - -desc "Stop the routing server." -task :down do - pid = find_pid 'osrm-routed' - if pid - Process.kill 'TERM', pid - else - puts "Already down." - end -end - -desc "Kill all osrm-extract, osrm-contract and osrm-routed processes." -task :kill do - each_process('osrm-routed') { |pid,state| Process.kill 'KILL', pid } - each_process('osrm-contract') { |pid,state| Process.kill 'KILL', pid } - each_process('osrm-extract') { |pid,state| Process.kill 'KILL', pid } - wait_for_shutdown 'osrm-routed' - wait_for_shutdown 'osrm-contract' - wait_for_shutdown 'osrm-extract' -end - -desc "Get PIDs of all osrm-extract, osrm-contract and osrm-routed processes." -task :pid do - each_process 'osrm-routed' do |pid,state| - puts "#{pid}\t#{state}" - end -end - -desc "Stop, reprocess and restart." -task :update => [:down,:process,:up] do -end - - -desc "Remove test cache files." -task :sweep do - system "rm test/cache/*" -end - diff --git a/config/cucumber.yml b/config/cucumber.yml deleted file mode 100644 index 2cdea3688..000000000 --- a/config/cucumber.yml +++ /dev/null @@ -1,9 +0,0 @@ -# config/cucumber.yml -##YAML Template ---- -default: --require features --tags ~@todo --tags ~@bug --tag ~@stress -verify: --require features --tags ~@todo --tags ~@bug --tags ~@stress -f progress -jenkins: --require features --tags ~@todo --tags ~@bug --tags ~@stress --tags ~@options -f progress -bugs: --require features --tags @bug -todo: --require features --tags @todo -all: --require features diff --git a/cucumber.js b/cucumber.js new file mode 100644 index 000000000..cffb6e3c5 --- /dev/null +++ b/cucumber.js @@ -0,0 +1,11 @@ +module.exports = { + default: '--require features --tags ~@todo --tags ~@bug --tag ~@stress', + verify: '--require features --tags ~@todo --tags ~@bug --tags ~@stress -f progress', + jenkins: '--require features --tags ~@todo --tags ~@bug --tags ~@stress --tags ~@options -f progress', + bugs: '--require features --tags @bug', + todo: '--require features --tags @todo', + all: '--require features' +} + + + diff --git a/features/bicycle/bridge.feature b/features/bicycle/bridge.feature index 8c26ee99d..6db8ce6f5 100644 --- a/features/bicycle/bridge.feature +++ b/features/bicycle/bridge.feature @@ -4,7 +4,7 @@ Feature: Bicycle - Handle movable bridge Background: Given the profile "bicycle" - Scenario: Car - Use a ferry route + Scenario: Bicycle - Use a ferry route Given the node map | a | b | c | | | | | | d | | | @@ -27,7 +27,7 @@ Feature: Bicycle - Handle movable bridge | c | f | cde,efg | 5,1 | | c | g | cde,efg | 5,1 | - Scenario: Car - Properly handle durations + Scenario: Bicycle - Properly handle durations Given the node map | a | b | c | | | | | | d | | | diff --git a/features/bicycle/mode.feature b/features/bicycle/mode.feature index 47618894b..b498bfd77 100644 --- a/features/bicycle/mode.feature +++ b/features/bicycle/mode.feature @@ -9,7 +9,7 @@ Feature: Bike - Mode flag Background: Given the profile "bicycle" - + Scenario: Bike - Mode when using a ferry Given the node map | a | b | | diff --git a/features/raster/weights.feature b/features/raster/weights.feature index ade8458f5..829407101 100644 --- a/features/raster/weights.feature +++ b/features/raster/weights.feature @@ -28,6 +28,7 @@ Feature: Raster - weights 0 0 0 250 0 0 0 0 """ + And the data has been saved to disk Scenario: Weighting not based on raster sources Given the profile "testbot" diff --git a/features/step_definitions/data.js b/features/step_definitions/data.js new file mode 100644 index 000000000..db0071cb6 --- /dev/null +++ b/features/step_definitions/data.js @@ -0,0 +1,273 @@ +var util = require('util'); +var path = require('path'); +var fs = require('fs'); +var d3 = require('d3-queue'); +var OSM = require('../support/build_osm'); + +module.exports = function () { + this.Given(/^the profile "([^"]*)"$/, (profile, callback) => { + this.setProfile(profile, callback); + }); + + this.Given(/^the extract extra arguments "(.*?)"$/, (args, callback) => { + this.setExtractArgs(args); + callback(); + }); + + this.Given(/^the contract extra arguments "(.*?)"$/, (args, callback) => { + this.setContractArgs(args); + callback(); + }); + + this.Given(/^a grid size of (\d+) meters$/, (meters, callback) => { + this.setGridSize(meters); + callback(); + }); + + this.Given(/^the origin ([-+]?[0-9]*\.?[0-9]+),([-+]?[0-9]*\.?[0-9]+)$/, (lat, lon, callback) => { + this.setOrigin([parseFloat(lon), parseFloat(lat)]); + callback(); + }); + + this.Given(/^the shortcuts$/, (table, callback) => { + var q = d3.queue(); + + var addShortcut = (row, cb) => { + this.shortcutsHash[row.key] = row.value; + cb(); + }; + + table.hashes().forEach((row) => { + q.defer(addShortcut, row); + }); + + q.awaitAll(callback); + }); + + this.Given(/^the node map$/, (table, callback) => { + var q = d3.queue(); + + var addNode = (name, ri, ci, cb) => { + if (name) { + if (name.length !== 1) throw new Error(util.format('*** node invalid name %s, must be single characters', name)); + if (!name.match(/[a-z0-9]/)) throw new Error(util.format('*** invalid node name %s, must me alphanumeric', name)); + + var lonLat; + if (name.match(/[a-z]/)) { + if (this.nameNodeHash[name]) throw new Error(util.format('*** duplicate node %s', name)); + lonLat = this.tableCoordToLonLat(ci, ri); + this.addOSMNode(name, lonLat[0], lonLat[1], null); + } else { + if (this.locationHash[name]) throw new Error(util.format('*** duplicate node %s'), name); + lonLat = this.tableCoordToLonLat(ci, ri); + this.addLocation(name, lonLat[0], lonLat[1], null); + } + + cb(); + } + else cb(); + }; + + table.raw().forEach((row, ri) => { + row.forEach((name, ci) => { + q.defer(addNode, name, ri, ci); + }); + }); + + q.awaitAll(callback); + }); + + this.Given(/^the node locations$/, (table, callback) => { + var q = d3.queue(); + + var addNodeLocations = (row, cb) => { + var name = row.node; + if (this.findNodeByName(name)) throw new Error(util.format('*** duplicate node %s'), name); + + if (name.match(/[a-z]/)) { + var id = row.id && parseInt(row.id); + this.addOSMNode(name, row.lon, row.lat, id); + } else { + this.addLocation(name, row.lon, row.lat); + } + + cb(); + }; + + table.hashes().forEach((row) => q.defer(addNodeLocations, row)); + + q.awaitAll(callback); + }); + + this.Given(/^the nodes$/, (table, callback) => { + var q = d3.queue(); + + var addNode = (row, cb) => { + var name = row.node, + node = this.findNodeByName(name); + delete row.node; + if (!node) throw new Error(util.format('*** unknown node %s'), name); + for (var key in row) { + node.addTag(key, row[key]); + } + cb(); + }; + + table.hashes().forEach((row) => q.defer(addNode, row)); + + q.awaitAll(callback); + }); + + this.Given(/^the ways$/, (table, callback) => { + if (this.osm_str) throw new Error('*** Map data already defined - did you pass an input file in this scenario?'); + + var q = d3.queue(); + + var addWay = (row, cb) => { + var way = new OSM.Way(this.makeOSMId(), this.OSM_USER, this.OSM_TIMESTAMP, this.OSM_UID); + + var nodes = row.nodes; + if (this.nameWayHash.nodes) throw new Error(util.format('*** duplicate way %s', nodes)); + + for (var i=0; i q.defer(addWay, row)); + + q.awaitAll(callback); + }); + + this.Given(/^the relations$/, (table, callback) => { + if (this.osm_str) throw new Error('*** Map data already defined - did you pass an input file in this scenario?'); + + var q = d3.queue(); + + var addRelation = (row, cb) => { + var relation = new OSM.Relation(this.makeOSMId(), this.OSM_USER, this.OSM_TIMESTAMP, this.OSM_UID); + + for (var key in row) { + var isNode = key.match(/^node:(.*)/), + isWay = key.match(/^way:(.*)/), + isColonSeparated = key.match(/^(.*):(.*)/); + if (isNode) { + row[key].split(',').map(function(v) { return v.trim(); }).forEach((nodeName) => { + if (nodeName.length !== 1) throw new Error(util.format('*** invalid relation node member "%s"'), nodeName); + var node = this.findNodeByName(nodeName); + if (!node) throw new Error(util.format('*** unknown relation node member "%s"'), nodeName); + relation.addMember('node', node.id, isNode[1]); + }); + } else if (isWay) { + row[key].split(',').map(function(v) { return v.trim(); }).forEach((wayName) => { + var way = this.findWayByName(wayName); + if (!way) throw new Error(util.format('*** unknown relation way member "%s"'), wayName); + relation.addMember('way', way.id, isWay[1]); + }); + } else if (isColonSeparated && isColonSeparated[1] !== 'restriction') { + throw new Error(util.format('*** unknown relation member type "%s:%s", must be either "node" or "way"'), isColonSeparated[1], isColonSeparated[2]); + } else { + relation.addTag(key, row[key]); + } + } + relation.uid = this.OSM_UID; + + this.OSMDB.addRelation(relation); + + cb(); + }; + + table.hashes().forEach((row) => q.defer(addRelation, row)); + + q.awaitAll(callback); + }); + + this.Given(/^the input file ([^"]*)$/, (file, callback) => { + if (path.extname(file) !== '.osm') throw new Error('*** Input file must be in .osm format'); + fs.readFile(file, 'utf8', (err, data) => { + if (!err) this.osm_str = data.toString(); + callback(err); + }); + }); + + this.Given(/^the raster source$/, (data, callback) => { + fs.writeFile(path.resolve(this.TEST_FOLDER, 'rastersource.asc'), data, callback); + }); + + this.Given(/^the speed file$/, (data, callback) => { + fs.writeFile(path.resolve(this.TEST_FOLDER, 'speeds.csv'), data, callback); + }); + + this.Given(/^the data has been saved to disk$/, (callback) => { + try { + this.reprocess(callback); + } catch(e) { + this.processError = e; + callback(e); + } + }); + + this.Given(/^the data has been extracted$/, (callback) => { + this.writeAndExtract((err) => { + if (err) this.processError = err; + callback(); + }); + }); + + this.Given(/^the data has been contracted$/, (callback) => { + this.reprocess((err) => { + if (err) this.processError = err; + callback(); + }); + }); + + this.Given(/^osrm\-routed is stopped$/, (callback) => { + this.OSRMLoader.shutdown((err) => { + if (err) this.processError = err; + callback(); + }); + }); + + this.Given(/^data is loaded directly/, () => { + this.loadMethod = 'directly'; + }); + + this.Given(/^data is loaded with datastore$/, () => { + this.loadMethod = 'datastore'; + }); + + this.Given(/^the HTTP method "([^"]*)"$/, (method, callback) => { + this.httpMethod = method; + callback(); + }); +}; diff --git a/features/step_definitions/data.rb b/features/step_definitions/data.rb deleted file mode 100644 index 37c4ed831..000000000 --- a/features/step_definitions/data.rb +++ /dev/null @@ -1,202 +0,0 @@ -Given /^the profile "([^"]*)"$/ do |profile| - set_profile profile -end - -Given(/^the import format "(.*?)"$/) do |format| - set_input_format format -end - -Given /^the extract extra arguments "(.*?)"$/ do |args| - set_extract_args args -end - -Given /^the contract extra arguments "(.*?)"$/ do |args| - set_contract_args args -end - -Given /^a grid size of (\d+) meters$/ do |meters| - set_grid_size meters -end - -Given /^the origin ([-+]?[0-9]*\.?[0-9]+),([-+]?[0-9]*\.?[0-9]+)$/ do |lat,lon| - set_origin [lon.to_f,lat.to_f] -end - -Given /^the shortcuts$/ do |table| - table.hashes.each do |row| - shortcuts_hash[ row['key'] ] = row['value'] - end -end - -Given /^the node map$/ do |table| - table.raw.each_with_index do |row,ri| - row.each_with_index do |name,ci| - unless name.empty? - raise "*** node invalid name '#{name}', must be single characters" unless name.size == 1 - raise "*** invalid node name '#{name}', must me alphanumeric" unless name.match /[a-z0-9]/ - if name.match /[a-z]/ - raise "*** duplicate node '#{name}'" if name_node_hash[name] - add_osm_node name, *table_coord_to_lonlat(ci,ri), nil - else - raise "*** duplicate node '#{name}'" if location_hash[name] - add_location name, *table_coord_to_lonlat(ci,ri) - end - end - end - end -end - -Given /^the node locations$/ do |table| - table.hashes.each do |row| - name = row['node'] - raise "*** duplicate node '#{name}'" if find_node_by_name name - if name.match /[a-z]/ - id = row['id'] - id = id.to_i if id - add_osm_node name, row['lon'].to_f, row['lat'].to_f, id - else - add_location name, row['lon'].to_f, row['lat'].to_f - end - end -end - -Given /^the nodes$/ do |table| - table.hashes.each do |row| - name = row.delete 'node' - node = find_node_by_name(name) - raise "*** unknown node '#{c}'" unless node - node << row - end -end - -Given /^the ways$/ do |table| - raise "*** Map data already defined - did you pass an input file in this scenaria?" if @osm_str - table.hashes.each do |row| - way = OSM::Way.new make_osm_id, OSM_USER, OSM_TIMESTAMP - way.uid = OSM_UID - - nodes = row.delete 'nodes' - raise "*** duplicate way '#{nodes}'" if name_way_hash[nodes] - nodes.each_char do |c| - raise "*** ways can only use names a-z, '#{name}'" unless c.match /[a-z]/ - node = find_node_by_name(c) - raise "*** unknown node '#{c}'" unless node - way << node - end - - defaults = { 'highway' => 'primary' } - tags = defaults.merge(row) - - if row['highway'] == '(nil)' - tags.delete 'highway' - end - - if row['name'] == nil - tags['name'] = nodes - elsif (row['name'] == '""') || (row['name'] == "''") - tags['name'] = '' - elsif row['name'] == '' || row['name'] == '(nil)' - tags.delete 'name' - else - tags['name'] = row['name'] - end - - way << tags - osm_db << way - name_way_hash[nodes] = way - end -end - -Given /^the relations$/ do |table| - raise "*** Map data already defined - did you pass an input file in this scenaria?" if @osm_str - table.hashes.each do |row| - relation = OSM::Relation.new make_osm_id, OSM_USER, OSM_TIMESTAMP - row.each_pair do |key,value| - if key =~ /^node:(.*)/ - value.split(',').map { |v| v.strip }.each do |node_name| - raise "***invalid relation node member '#{node_name}', must be single character" unless node_name.size == 1 - node = find_node_by_name(node_name) - raise "*** unknown relation node member '#{node_name}'" unless node - relation << OSM::Member.new( 'node', node.id, $1 ) - end - elsif key =~ /^way:(.*)/ - value.split(',').map { |v| v.strip }.each do |way_name| - way = find_way_by_name(way_name) - raise "*** unknown relation way member '#{way_name}'" unless way - relation << OSM::Member.new( 'way', way.id, $1 ) - end - elsif key =~ /^(.*):(.*)/ && "#{$1}" != 'restriction' - raise "*** unknown relation member type '#{$1}:#{$2}', must be either 'node' or 'way'" - else - relation << { key => value } - end - end - relation.uid = OSM_UID - osm_db << relation - end -end - -Given /^the defaults$/ do -end - -Given /^the input file ([^"]*)$/ do |file| - raise "*** Input file must in .osm format" unless File.extname(file)=='.osm' - @osm_str = File.read file -end - -Given /^the raster source$/ do |data| - Dir.chdir TEST_FOLDER do - File.open("rastersource.asc", "w") {|f| f.write(data)} - end -end - -Given /^the speed file$/ do |data| - Dir.chdir TEST_FOLDER do - File.open("speeds.csv", "w") {|f| f.write(data)} - end -end - -Given /^the data has been saved to disk$/ do - begin - write_input_data - rescue OSRMError => e - @process_error = e - end -end - -Given /^the data has been extracted$/ do - begin - write_input_data - extract_data unless extracted? - rescue OSRMError => e - @process_error = e - end -end - -Given /^the data has been contracted$/ do - begin - reprocess - rescue OSRMError => e - @process_error = e - end -end - -Given /^osrm\-routed is stopped$/ do - begin - OSRMLoader.shutdown - rescue OSRMError => e - @process_error = e - end -end - -Given /^data is loaded directly/ do - @load_method = 'directly' -end - -Given /^data is loaded with datastore$/ do - @load_method = 'datastore' -end - -Given /^the HTTP method "([^"]*)"$/ do |method| - @http_method = method -end diff --git a/features/step_definitions/distance_matrix.js b/features/step_definitions/distance_matrix.js new file mode 100644 index 000000000..a7b4f97ad --- /dev/null +++ b/features/step_definitions/distance_matrix.js @@ -0,0 +1,82 @@ +var util = require('util'); + +module.exports = function () { + this.When(/^I request a travel time matrix I should get$/, (table, callback) => { + var NO_ROUTE = 2147483647; // MAX_INT + + var tableRows = table.raw(); + + if (tableRows[0][0] !== '') throw new Error('*** Top-left cell of matrix table must be empty'); + + var waypoints = [], + columnHeaders = tableRows[0].slice(1), + rowHeaders = tableRows.map((h) => h[0]).slice(1), + symmetric = columnHeaders.every((ele, i) => ele === rowHeaders[i]); + + if (symmetric) { + columnHeaders.forEach((nodeName) => { + var node = this.findNodeByName(nodeName); + if (!node) throw new Error(util.format('*** unknown node "%s"'), nodeName); + waypoints.push({ coord: node, type: 'loc' }); + }); + } else { + columnHeaders.forEach((nodeName) => { + var node = this.findNodeByName(nodeName); + if (!node) throw new Error(util.format('*** unknown node "%s"'), nodeName); + waypoints.push({ coord: node, type: 'dst' }); + }); + rowHeaders.forEach((nodeName) => { + var node = this.findNodeByName(nodeName); + if (!node) throw new Error(util.format('*** unknown node "%s"'), nodeName); + waypoints.push({ coord: node, type: 'src' }); + }); + } + + var actual = []; + actual.push(table.headers); + + this.reprocessAndLoadData(() => { + // compute matrix + var params = this.queryParams; + + this.requestTable(waypoints, params, (err, response) => { + if (err) return callback(err); + if (!response.body.length) return callback(new Error('Invalid response body')); + + var jsonResult = JSON.parse(response.body), + result = jsonResult['distance_table'].map((row) => { + var hashes = {}; + row.forEach((c, j) => { + hashes[tableRows[0][j+1]] = c; + }); + return hashes; + }); + + var testRow = (row, ri, cb) => { + var ok = true; + + for (var k in result[ri]) { + if (this.FuzzyMatch.match(result[ri][k], row[k])) { + result[ri][k] = row[k]; + } else if (row[k] === '' && result[ri][k] === NO_ROUTE) { + result[ri][k] = ''; + } else { + result[ri][k] = result[ri][k].toString(); + ok = false; + } + } + + if (!ok) { + var failed = { attempt: 'distance_matrix', query: this.query, response: response }; + this.logFail(row, result[ri], [failed]); + } + + result[ri][''] = row['']; + cb(null, result[ri]); + }; + + this.processRowsAndDiff(table, testRow, callback); + }); + }); + }); +}; diff --git a/features/step_definitions/distance_matrix.rb b/features/step_definitions/distance_matrix.rb deleted file mode 100644 index d4f09d090..000000000 --- a/features/step_definitions/distance_matrix.rb +++ /dev/null @@ -1,66 +0,0 @@ -When /^I request a travel time matrix I should get$/ do |table| - no_route = 2147483647 # MAX_INT - - raise "*** Top-left cell of matrix table must be empty" unless table.headers[0]=="" - - waypoints = [] - column_headers = table.headers[1..-1] - row_headers = table.rows.map { |h| h.first } - symmetric = Set.new(column_headers) == Set.new(row_headers) - if symmetric then - column_headers.each do |node_name| - node = find_node_by_name(node_name) - raise "*** unknown node '#{node_name}" unless node - waypoints << {:coord => node, :type => "loc"} - end - else - column_headers.each do |node_name| - node = find_node_by_name(node_name) - raise "*** unknown node '#{node_name}" unless node - waypoints << {:coord => node, :type => "dst"} - end - row_headers.each do |node_name| - node = find_node_by_name(node_name) - raise "*** unknown node '#{node_name}" unless node - waypoints << {:coord => node, :type => "src"} - end - end - - reprocess - actual = [] - actual << table.headers - OSRMLoader.load(self,"#{contracted_file}.osrm") do - - # compute matrix - params = @query_params - response = request_table waypoints, params - if response.body.empty? == false - json_result = JSON.parse response.body - result = json_result["distance_table"] - end - - - # compare actual and expected result, one row at a time - table.rows.each_with_index do |row,ri| - # fuzzy match - ok = true - 0.upto(result[ri].size-1) do |i| - if FuzzyMatch.match result[ri][i], row[i+1] - result[ri][i] = row[i+1] - elsif row[i+1]=="" and result[ri][i]==no_route - result[ri][i] = "" - else - result[ri][i] = result[ri][i].to_s - ok = false - end - end - - # add row header - r = [row[0],result[ri]].flatten - - # store row for comparison - actual << r - end - end - table.diff! actual -end diff --git a/features/step_definitions/hooks.js b/features/step_definitions/hooks.js new file mode 100644 index 000000000..dbc0930b7 --- /dev/null +++ b/features/step_definitions/hooks.js @@ -0,0 +1,30 @@ +var util = require('util'); + +module.exports = function () { + this.Before((scenario, callback) => { + this.scenarioTitle = scenario.getName(); + + this.loadMethod = this.DEFAULT_LOAD_METHOD; + this.queryParams = []; + var d = new Date(); + this.scenarioTime = util.format('%d-%d-%dT%s:%s:%sZ', d.getFullYear(), d.getMonth()+1, d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds()); + this.resetData(); + this.hasLoggedPreprocessInfo = false; + this.hasLoggedScenarioInfo = false; + this.setGridSize(this.DEFAULT_GRID_SIZE); + this.setOrigin(this.DEFAULT_ORIGIN); + callback(); + }); + + this.Before('@ignore-platform-windows', () => { + this.skipThisScenario(); + }); + + this.Before('@ignore-platform-unix', () => { + this.skipThisScenario(); + }); + + this.Before('@ignore-platform-mac', () => { + this.skipThisScenario(); + }); +}; diff --git a/features/step_definitions/hooks.rb b/features/step_definitions/hooks.rb deleted file mode 100644 index 2ad821a9a..000000000 --- a/features/step_definitions/hooks.rb +++ /dev/null @@ -1,11 +0,0 @@ -Before '@ignore-platform-windows' do - skip_this_scenario -end - -Before '@ignore-platform-unix' do - skip_this_scenario -end - -Before '@ignore-platform-mac' do - skip_this_scenario -end diff --git a/features/step_definitions/matching.js b/features/step_definitions/matching.js new file mode 100644 index 000000000..441d37400 --- /dev/null +++ b/features/step_definitions/matching.js @@ -0,0 +1,174 @@ +var util = require('util'); +var d3 = require('d3-queue'); + +module.exports = function () { + this.When(/^I match I should get$/, (table, callback) => { + var got; + + this.reprocessAndLoadData(() => { + var testRow = (row, ri, cb) => { + var afterRequest = (err, res) => { + if (err) return cb(err); + var json; + + var headers = new Set(table.raw()[0]); + + if (res.body.length) { + json = JSON.parse(res.body); + } + + if (headers.has('status')) { + got.status = json.status.toString(); + } + + if (headers.has('message')) { + got.message = json.status_message; + } + + if (headers.has('#')) { + // comment column + got['#'] = row['#']; + } + + var subMatchings = [], + turns = '', + route = '', + duration = ''; + + if (res.statusCode === 200) { + if (headers.has('matchings')) { + subMatchings = json.matchings.filter(m => !!m).map(sub => sub.matched_points); + } + + if (headers.has('turns')) { + if (json.matchings.length != 1) throw new Error('*** Checking turns only supported for matchings with one subtrace'); + turns = this.turnList(json.matchings[0].instructions); + } + + if (headers.has('route')) { + if (json.matchings.length != 1) throw new Error('*** Checking route only supported for matchings with one subtrace'); + route = this.wayList(json.matchings[0].instructions); + } + + if (headers.has('duration')) { + if (json.matchings.length != 1) throw new Error('*** Checking duration only supported for matchings with one subtrace'); + duration = json.matchings[0].route_summary.total_time; + } + } + + if (headers.has('turns')) { + got.turns = turns; + } + + if (headers.has('route')) { + got.route = route; + } + + if (headers.has('duration')) { + got.duration = duration.toString(); + } + + var ok = true; + var encodedResult = '', + extendedTarget = ''; + + var q = d3.queue(); + + var testSubMatching = (sub, si, scb) => { + if (si >= subMatchings.length) { + ok = false; + q.abort(); + scb(); + } else { + var sq = d3.queue(); + + var testSubNode = (ni, ncb) => { + var node = this.findNodeByName(sub[ni]), + outNode = subMatchings[si][ni]; + + if (this.FuzzyMatch.matchLocation(outNode, node)) { + encodedResult += sub[ni]; + extendedTarget += sub[ni]; + } else { + encodedResult += util.format('? [%s,%s]', outNode[0], outNode[1]); + extendedTarget += util.format('%s [%d,%d]', node.lat, node.lon); + ok = false; + } + ncb(); + }; + + for (var i=0; i { + q.defer(testSubMatching, sub, si); + }); + + q.awaitAll(() => { + if (ok) { + if (headers.has('matchings')) { + got.matchings = row.matchings; + } + + if (headers.has('timestamps')) { + got.timestamps = row.timestamps; + } + } else { + got.matchings = encodedResult; + row.matchings = extendedTarget; + this.logFail(row, got, { matching: { query: this.query, response: res } }); + } + + cb(null, got); + }); + }; + + if (row.request) { + got = {}; + got.request = row.request; + this.requestUrl(row.request, afterRequest); + } else { + var params = this.queryParams; + got = {}; + for (var k in row) { + var match = k.match(/param:(.*)/); + if (match) { + if (row[k] === '(nil)') { + params[match[1]] = null; + } else if (row[k]) { + params[match[1]] = [row[k]]; + } + got[k] = row[k]; + } + } + + var trace = [], + timestamps = []; + + if (row.trace) { + for (var i=0; i !!s).map(t => parseInt(t)); + } + got.trace = row.trace; + this.requestMatching(trace, timestamps, params, afterRequest); + } else { + throw new Error('*** no trace'); + } + } + }; + + this.processRowsAndDiff(table, testRow, callback); + }); + }); +}; diff --git a/features/step_definitions/matching.rb b/features/step_definitions/matching.rb deleted file mode 100644 index 2ec96b4f0..000000000 --- a/features/step_definitions/matching.rb +++ /dev/null @@ -1,124 +0,0 @@ -When /^I match I should get$/ do |table| - reprocess - actual = [] - OSRMLoader.load(self,"#{contracted_file}.osrm") do - table.hashes.each_with_index do |row,ri| - if row['request'] - got = {'request' => row['request'] } - response = request_url row['request'] - else - params = @query_params - got = {} - row.each_pair do |k,v| - if k =~ /param:(.*)/ - if v=='(nil)' - params[$1]=nil - elsif v!=nil - params[$1]=[v] - end - got[k]=v - end - end - trace = [] - timestamps = [] - if row['trace'] - row['trace'].each_char do |n| - node = find_node_by_name(n.strip) - raise "*** unknown waypoint node '#{n.strip}" unless node - trace << node - end - if row['timestamps'] - timestamps = row['timestamps'].split(" ").compact.map { |t| t.to_i} - end - got = got.merge({'trace' => row['trace'] }) - response = request_matching trace, timestamps, params - else - raise "*** no trace" - end - end - - if response.body.empty? == false - json = JSON.parse response.body - end - - if table.headers.include? 'status' - got['status'] = json['status'].to_s - end - if table.headers.include? 'message' - got['message'] = json['status_message'] - end - if table.headers.include? '#' # comment column - got['#'] = row['#'] # copy value so it always match - end - - sub_matchings = [] - turns = '' - route = '' - duration = '' - if response.code == "200" - if table.headers.include? 'matchings' - sub_matchings = json['matchings'].compact.map { |sub| sub['matched_points']} - end - if table.headers.include? 'turns' - raise "*** Checking turns only support for matchings with one subtrace" unless json['matchings'].size == 1 - turns = turn_list json['matchings'][0]['instructions'] - end - if table.headers.include? 'route' - raise "*** Checking route only support for matchings with one subtrace" unless json['matchings'].size == 1 - route = way_list json['matchings'][0]['instructions'] - if table.headers.include? 'duration' - raise "*** Checking duration only support for matchings with one subtrace" unless json['matchings'].size == 1 - duration = json['matchings'][0]['route_summary']['total_time'] - end - end - end - - if table.headers.include? 'turns' - got['turns'] = turns - end - if table.headers.include? 'route' - got['route'] = route - end - if table.headers.include? 'duration' - got['duration'] = duration.to_s - end - - ok = true - encoded_result = "" - extended_target = "" - row['matchings'].split(',').each_with_index do |sub, sub_idx| - if sub_idx >= sub_matchings.length - ok = false - break - end - sub.length.times do |node_idx| - node = find_node_by_name(sub[node_idx]) - out_node = sub_matchings[sub_idx][node_idx] - if FuzzyMatch.match_location out_node, node - encoded_result += sub[node_idx] - extended_target += sub[node_idx] - else - encoded_result += "? [#{out_node[0]},#{out_node[1]}]" - extended_target += "#{sub[node_idx]} [#{node.lat},#{node.lon}]" - ok = false - end - end - end - if ok - if table.headers.include? 'matchings' - got['matchings'] = row['matchings'] - end - if table.headers.include? 'timestamps' - got['timestamps'] = row['timestamps'] - end - else - got['matchings'] = encoded_result - row['matchings'] = extended_target - log_fail row,got, { 'matching' => {:query => @query, :response => response} } - end - - actual << got - end - end - table.diff! actual -end diff --git a/features/step_definitions/nearest.js b/features/step_definitions/nearest.js new file mode 100644 index 000000000..1bae29889 --- /dev/null +++ b/features/step_definitions/nearest.js @@ -0,0 +1,53 @@ +var util = require('util'); + +module.exports = function () { + this.When(/^I request nearest I should get$/, (table, callback) => { + this.reprocessAndLoadData(() => { + var testRow = (row, ri, cb) => { + var inNode = this.findNodeByName(row.in); + if (!inNode) throw new Error(util.format('*** unknown in-node "%s"'), row.in); + + var outNode = this.findNodeByName(row.out); + if (!outNode) throw new Error(util.format('*** unknown out-node "%s"'), row.out); + + this.requestNearest(inNode, this.queryParams, (err, response) => { + if (err) return cb(err); + var coord; + + if (response.statusCode === 200 && response.body.length) { + var json = JSON.parse(response.body); + + coord = json.mapped_coordinate; + + var got = { in: row.in, out: row.out }; + + var ok = true; + + Object.keys(row).forEach((key) => { + if (key === 'out') { + if (this.FuzzyMatch.matchLocation(coord, outNode)) { + got[key] = row[key]; + } else { + row[key] = util.format('%s [%d,%d]', row[key], outNode.lat, outNode.lon); + ok = false; + } + } + }); + + if (!ok) { + var failed = { attempt: 'nearest', query: this.query, response: response }; + this.logFail(row, got, [failed]); + } + + cb(null, got); + } + else { + cb(); + } + }); + }; + + this.processRowsAndDiff(table, testRow, callback); + }); + }); +}; diff --git a/features/step_definitions/nearest.rb b/features/step_definitions/nearest.rb deleted file mode 100644 index 9d84c45c8..000000000 --- a/features/step_definitions/nearest.rb +++ /dev/null @@ -1,51 +0,0 @@ -When /^I request nearest I should get$/ do |table| - reprocess - actual = [] - OSRMLoader.load(self,"#{contracted_file}.osrm") do - table.hashes.each_with_index do |row,ri| - in_node = find_node_by_name row['in'] - raise "*** unknown in-node '#{row['in']}" unless in_node - - out_node = find_node_by_name row['out'] - raise "*** unknown out-node '#{row['out']}" unless out_node - - response = request_nearest in_node, @query_params - if response.code == "200" && response.body.empty? == false - json = JSON.parse response.body - if json['status'] == 200 - coord = json['mapped_coordinate'] - end - end - - got = {'in' => row['in'], 'out' => coord } - - ok = true - row.keys.each do |key| - if key=='out' - if FuzzyMatch.match_location coord, out_node - got[key] = row[key] - else - row[key] = "#{row[key]} [#{out_node.lat},#{out_node.lon}]" - ok = false - end - end - end - - unless ok - failed = { :attempt => 'nearest', :query => @query, :response => response } - log_fail row,got,[failed] - end - - actual << got - end - end - table.diff! actual -end - -When /^I request nearest (\d+) times I should get$/ do |n,table| - ok = true - n.to_i.times do - ok = false unless step "I request nearest I should get", table - end - ok -end diff --git a/features/step_definitions/options.js b/features/step_definitions/options.js new file mode 100644 index 000000000..fbc4eb361 --- /dev/null +++ b/features/step_definitions/options.js @@ -0,0 +1,69 @@ +var assert = require('assert'); + +module.exports = function () { + this.When(/^I run "osrm\-routed\s?(.*?)"$/, { timeout: this.SHUTDOWN_TIMEOUT }, (options, callback) => { + this.runBin('osrm-routed', options, () => { + callback(); + }); + }); + + this.When(/^I run "osrm\-extract\s?(.*?)"$/, (options, callback) => { + this.runBin('osrm-extract', options, () => { + callback(); + }); + }); + + this.When(/^I run "osrm\-contract\s?(.*?)"$/, (options, callback) => { + this.runBin('osrm-contract', options, () => { + callback(); + }); + }); + + this.When(/^I run "osrm\-datastore\s?(.*?)"$/, (options, callback) => { + this.runBin('osrm-datastore', options, () => { + callback(); + }); + }); + + this.Then(/^it should exit with code (\d+)$/, (code) => { + assert.equal(this.exitCode, parseInt(code)); + }); + + this.Then(/^stdout should contain "(.*?)"$/, (str) => { + assert.ok(this.stdout.indexOf(str) > -1); + }); + + this.Then(/^stderr should contain "(.*?)"$/, (str) => { + assert.ok(this.stderr.indexOf(str) > -1); + }); + + this.Then(/^stdout should contain \/(.*)\/$/, (regexStr) => { + var re = new RegExp(regexStr); + assert.ok(this.stdout.match(re)); + }); + + this.Then(/^stderr should contain \/(.*)\/$/, (regexStr) => { + var re = new RegExp(regexStr); + assert.ok(this.stdout.match(re)); + }); + + this.Then(/^stdout should be empty$/, () => { + assert.equal(this.stdout.trim(), ''); + }); + + this.Then(/^stderr should be empty$/, () => { + assert.equal(this.stderr.trim(), ''); + }); + + this.Then(/^stdout should contain (\d+) lines?$/, (lines) => { + assert.equal(this.stdout.split('\n').length - 1, parseInt(lines)); + }); + + this.Given(/^the query options$/, (table, callback) => { + table.raw().forEach((tuple) => { + this.queryParams.push(tuple); + }); + + callback(); + }); +}; diff --git a/features/step_definitions/options.rb b/features/step_definitions/options.rb deleted file mode 100644 index 6a033803d..000000000 --- a/features/step_definitions/options.rb +++ /dev/null @@ -1,57 +0,0 @@ -When(/^I run "osrm\-routed\s?(.*?)"$/) do |options| - begin - Timeout.timeout(SHUTDOWN_TIMEOUT) { run_bin 'osrm-routed', options } - rescue Timeout::Error - raise "*** osrm-routed didn't quit. Maybe the --trial option wasn't used?" - end -end - -When(/^I run "osrm\-extract\s?(.*?)"$/) do |options| - run_bin 'osrm-extract', options -end - -When(/^I run "osrm\-contract\s?(.*?)"$/) do |options| - run_bin 'osrm-contract', options -end - -When(/^I run "osrm\-datastore\s?(.*?)"$/) do |options| - run_bin 'osrm-datastore', options -end - -Then /^it should exit with code (\d+)$/ do |code| - expect(@exit_code).to eq( code.to_i ) -end - -Then /^stdout should contain "(.*?)"$/ do |str| - expect(@stdout).to include(str) -end - -Then /^stderr should contain "(.*?)"$/ do |str| - expect(@stderr).to include(str) -end - -Then(/^stdout should contain \/(.*)\/$/) do |regex_str| - regex = Regexp.new regex_str - expect(@stdout).to match( regex ) -end - -Then(/^stderr should contain \/(.*)\/$/) do |regex_str| - regex = Regexp.new regex_str - expect(@stderr).to match( regex ) -end - -Then /^stdout should be empty$/ do - expect(@stdout).to eq("") -end - -Then /^stderr should be empty$/ do - expect(@stderr).to eq("") -end - -Then /^stdout should contain (\d+) lines?$/ do |lines| - expect(@stdout.lines.count).to eq( lines.to_i ) -end - -Given (/^the query options$/) do |table| - table.rows_hash.each { |k,v| @query_params << [k, v] } -end diff --git a/features/step_definitions/requests.js b/features/step_definitions/requests.js new file mode 100644 index 000000000..effc5e12c --- /dev/null +++ b/features/step_definitions/requests.js @@ -0,0 +1,60 @@ +var assert = require('assert'); + +module.exports = function () { + this.When(/^I request \/(.*)$/, (path, callback) => { + this.reprocessAndLoadData(() => { + this.requestPath(path, [], (err, res, body) => { + this.response = res; + callback(err, res, body); + }); + }); + }); + + this.Then(/^I should get a response/, () => { + this.ShouldGetAResponse(); + }); + + this.Then(/^response should be valid JSON$/, (callback) => { + this.ShouldBeValidJSON(callback); + }); + + this.Then(/^response should be well-formed$/, () => { + this.ShouldBeWellFormed(); + }); + + this.Then(/^status code should be (\d+)$/, (code, callback) => { + try { + this.json = JSON.parse(this.response.body); + } catch(e) { + return callback(e); + } + assert.equal(this.json.status, parseInt(code)); + callback(); + }); + + this.Then(/^status message should be "(.*?)"$/, (message, callback) => { + try { + this.json = JSON.parse(this.response.body); + } catch(e) { + return callback(e); + } + assert(this.json.status_message, message); + callback(); + }); + + this.Then(/^response should be a well-formed route$/, () => { + this.ShouldBeWellFormed(); + assert.equal(typeof this.json.status_message, 'string'); + assert.equal(typeof this.json.route_summary, 'object'); + assert.equal(typeof this.json.route_geometry, 'string'); + assert.ok(Array.isArray(this.json.route_instructions)); + assert.ok(Array.isArray(this.json.via_points)); + assert.ok(Array.isArray(this.json.via_indices)); + }); + + this.Then(/^"([^"]*)" should return code (\d+)$/, (binary, code) => { + assert.ok(this.processError instanceof this.OSRMError); + assert.equal(this.processError.process, binary); + assert.equal(parseInt(this.processError.code), parseInt(code)); + }); +}; diff --git a/features/step_definitions/requests.rb b/features/step_definitions/requests.rb deleted file mode 100644 index e44ea028f..000000000 --- a/features/step_definitions/requests.rb +++ /dev/null @@ -1,46 +0,0 @@ -When /^I request \/(.*)$/ do |path| - reprocess - OSRMLoader.load(self,"#{contracted_file}.osrm") do - @response = request_path path, [] - end -end - -Then /^I should get a response/ do - expect(@response.code).to eq("200") - expect(@response.body).not_to eq(nil) - expect(@response.body).not_to eq('') -end - -Then /^response should be valid JSON$/ do - @json = JSON.parse @response.body -end - -Then /^response should be well-formed$/ do - expect(@json['status'].class).to eq(Fixnum) -end - -Then /^status code should be (\d+)$/ do |code| - @json = JSON.parse @response.body - expect(@json['status']).to eq(code.to_i) -end - -Then /^status message should be "(.*?)"$/ do |message| - @json = JSON.parse @response.body - expect(@json['status_message']).to eq(message) -end - -Then /^response should be a well-formed route$/ do - step "response should be well-formed" - expect(@json['status_message'].class).to eq(String) - expect(@json['route_summary'].class).to eq(Hash) - expect(@json['route_geometry'].class).to eq(String) - expect(@json['route_instructions'].class).to eq(Array) - expect(@json['via_points'].class).to eq(Array) - expect(@json['via_indices'].class).to eq(Array) -end - -Then /^"([^"]*)" should return code (\d+)$/ do |binary, code| - expect(@process_error.is_a?(OSRMError)).to eq(true) - expect(@process_error.process).to eq(binary) - expect(@process_error.code.to_i).to eq(code.to_i) -end diff --git a/features/step_definitions/routability.js b/features/step_definitions/routability.js new file mode 100644 index 000000000..b23cffafe --- /dev/null +++ b/features/step_definitions/routability.js @@ -0,0 +1,110 @@ +var util = require('util'); +var d3 = require('d3-queue'); +var classes = require('../support/data_classes'); + +module.exports = function () { + this.Then(/^routability should be$/, (table, callback) => { + this.buildWaysFromTable(table, () => { + var directions = ['forw','backw','bothw']; + + if (!directions.some(k => !!table.hashes()[0].hasOwnProperty(k))) { + throw new Error('*** routability table must contain either "forw", "backw" or "bothw" column'); + } + this.reprocessAndLoadData(() => { + var testRow = (row, i, cb) => { + var outputRow = row; + + testRoutabilityRow(i, (err, result) => { + if (err) return cb(err); + directions.filter(d => !!table.hashes()[0][d]).forEach((direction) => { + var want = this.shortcutsHash[row[direction]] || row[direction]; + + switch (true) { + case '' === want: + case 'x' === want: + outputRow[direction] = result[direction].status ? + result[direction].status.toString() : ''; + break; + case /^\d+s/.test(want): + break; + case /^\d+ km\/h/.test(want): + break; + default: + throw new Error(util.format('*** Unknown expectation format: %s', want)); + } + + if (this.FuzzyMatch.match(outputRow[direction], want)) { + outputRow[direction] = row[direction]; + } + }); + + if (outputRow != row) { + this.logFail(row, outputRow, result); + } + + cb(null, outputRow); + }); + }; + + this.processRowsAndDiff(table, testRow, callback); + }); + }); + }); + + var testRoutabilityRow = (i, cb) => { + var result = {}; + + var testDirection = (dir, callback) => { + var a = new classes.Location(this.origin[0] + (1+this.WAY_SPACING*i) * this.zoom, this.origin[1]), + b = new classes.Location(this.origin[0] + (3+this.WAY_SPACING*i) * this.zoom, this.origin[1]), + r = {}; + + r.which = dir; + + this.requestRoute((dir === 'forw' ? [a, b] : [b, a]), [], this.queryParams, (err, res, body) => { + if (err) return callback(err); + + r.query = this.query; + r.json = JSON.parse(body); + r.status = r.json.status === 200 ? 'x' : null; + if (r.status) { + r.route = this.wayList(r.json.route_instructions); + + if (r.route === util.format('w%d', i)) { + r.time = r.json.route_summary.total_time; + r.distance = r.json.route_summary.total_distance; + r.speed = r.time > 0 ? parseInt(3.6 * r.distance / r.time) : null; + } else { + r.status = null; + } + } + + callback(null, r); + }); + }; + + d3.queue() + .defer(testDirection, 'forw') + .defer(testDirection, 'backw') + .awaitAll((err, res) => { + if (err) return cb(err); + // check if forw and backw returned the same values + res.forEach((dirRes) => { + var which = dirRes.which; + delete dirRes.which; + result[which] = dirRes; + }); + + result.bothw = {}; + ['status', 'time', 'distance', 'speed'].forEach((key) => { + if (result.forw[key] === result.backw[key]) { + result.bothw[key] = result.forw[key]; + } else { + result.bothw[key] = 'diff'; + } + }); + + cb(null, result); + }); + }; +}; diff --git a/features/step_definitions/routability.rb b/features/step_definitions/routability.rb deleted file mode 100644 index 70bccd9c6..000000000 --- a/features/step_definitions/routability.rb +++ /dev/null @@ -1,78 +0,0 @@ -def test_routability_row i - result = {} - ['forw','backw'].each do |direction| - a = Location.new @origin[0]+(1+WAY_SPACING*i)*@zoom, @origin[1] - b = Location.new @origin[0]+(3+WAY_SPACING*i)*@zoom, @origin[1] - r = {} - r[:response] = request_route (direction=='forw' ? [a,b] : [b,a]), [], @query_params - r[:query] = @query - r[:json] = JSON.parse(r[:response].body) - - r[:status] = (route_status r[:response]) == 200 ? 'x' : nil - if r[:status] then - r[:route] = way_list r[:json]['route_instructions'] - - if r[:route]=="w#{i}" - r[:time] = r[:json]['route_summary']['total_time'] - r[:distance] = r[:json]['route_summary']['total_distance'] - r[:speed] = r[:time]>0 ? (3.6*r[:distance]/r[:time]).to_i : nil - else - # if we hit the wrong way segment, we assume it's - # because the one we tested was not unroutable - r[:status] = nil - end - end - result[direction] = r - end - - # check if forw and backw returned the same values - result['bothw'] = {} - [:status,:time,:distance,:speed].each do |key| - if result['forw'][key] == result['backw'][key] - result['bothw'][key] = result['forw'][key] - else - result['bothw'][key] = 'diff' - end - end - result -end - -Then /^routability should be$/ do |table| - build_ways_from_table table - reprocess - actual = [] - if table.headers&["forw","backw","bothw"] == [] - raise "*** routability tabel must contain either 'forw', 'backw' or 'bothw' column" - end - OSRMLoader.load(self,"#{contracted_file}.osrm") do - table.hashes.each_with_index do |row,i| - output_row = row.dup - attempts = [] - result = test_routability_row i - directions = ['forw','backw','bothw'] - (directions & table.headers).each do |direction| - want = shortcuts_hash[row[direction]] || row[direction] #expand shortcuts - case want - when '', 'x' - output_row[direction] = result[direction][:status] ? result[direction][:status].to_s : '' - when /^\d+s/ - output_row[direction] = result[direction][:time] ? "#{result[direction][:time]}s" : '' - when /^\d+ km\/h/ - output_row[direction] = result[direction][:speed] ? "#{result[direction][:speed]} km/h" : '' - else - raise "*** Unknown expectation format: #{want}" - end - - if FuzzyMatch.match output_row[direction], want - output_row[direction] = row[direction] - end - end - - if output_row != row - log_fail row,output_row,result - end - actual << output_row - end - end - table.diff! actual -end diff --git a/features/step_definitions/routing.js b/features/step_definitions/routing.js new file mode 100644 index 000000000..d48573e99 --- /dev/null +++ b/features/step_definitions/routing.js @@ -0,0 +1,16 @@ +var d3 = require('d3-queue'); + +module.exports = function () { + this.When(/^I route I should get$/, this.WhenIRouteIShouldGet); + + // This is used to route 100 times; timeout for entire step is therefore set to 100 * STRESS_TIMEOUT + this.When(/^I route (\d+) times I should get$/, { timeout: 30000 }, (n, table, callback) => { + var q = d3.queue(1); + + for (var i=0; i row['request'] } - response = request_url row['request'] - else - default_params = @query_params - user_params = [] - got = {} - row.each_pair do |k,v| - if k =~ /param:(.*)/ - if v=='(nil)' - user_params << [$1, nil] - elsif v!=nil - user_params << [$1, v] - end - got[k]=v - end - end - params = overwrite_params default_params, user_params - waypoints = [] - bearings = [] - if row['bearings'] - got['bearings'] = row['bearings'] - bearings = row['bearings'].split(' ').compact - end - if row['from'] and row['to'] - node = find_node_by_name(row['from']) - raise "*** unknown from-node '#{row['from']}" unless node - waypoints << node - - node = find_node_by_name(row['to']) - raise "*** unknown to-node '#{row['to']}" unless node - waypoints << node - - got = got.merge({'from' => row['from'], 'to' => row['to'] }) - response = request_route waypoints, bearings, params - elsif row['waypoints'] - row['waypoints'].split(',').each do |n| - node = find_node_by_name(n.strip) - raise "*** unknown waypoint node '#{n.strip}" unless node - waypoints << node - end - got = got.merge({'waypoints' => row['waypoints'] }) - response = request_route waypoints, bearings, params - else - raise "*** no waypoints" - end - end - - if response.body.empty? == false - json = JSON.parse response.body - end - - if response.body.empty? == false - if json['status'] == 200 - instructions = way_list json['route_instructions'] - bearings = bearing_list json['route_instructions'] - compasses = compass_list json['route_instructions'] - turns = turn_list json['route_instructions'] - modes = mode_list json['route_instructions'] - times = time_list json['route_instructions'] - distances = distance_list json['route_instructions'] - end - end - - if table.headers.include? 'status' - got['status'] = json['status'].to_s - end - if table.headers.include? 'message' - got['message'] = json['status_message'] - end - if table.headers.include? '#' # comment column - got['#'] = row['#'] # copy value so it always match - end - - if table.headers.include? 'start' - got['start'] = instructions ? json['route_summary']['start_point'] : nil - end - if table.headers.include? 'end' - got['end'] = instructions ? json['route_summary']['end_point'] : nil - end - if table.headers.include? 'geometry' - got['geometry'] = json['route_geometry'] - end - if table.headers.include? 'route' - got['route'] = (instructions || '').strip - if table.headers.include?('alternative') - got['alternative'] = - if json['found_alternative'] - way_list json['alternative_instructions'].first - else - "" - end - end - if table.headers.include?('distance') - if row['distance']!='' - raise "*** Distance must be specied in meters. (ex: 250m)" unless row['distance'] =~ /\d+m/ - end - got['distance'] = instructions ? "#{json['route_summary']['total_distance'].to_s}m" : '' - end - if table.headers.include?('time') - raise "*** Time must be specied in seconds. (ex: 60s)" unless row['time'] =~ /\d+s/ - got['time'] = instructions ? "#{json['route_summary']['total_time'].to_s}s" : '' - end - if table.headers.include?('speed') - if row['speed'] != '' && instructions - raise "*** Speed must be specied in km/h. (ex: 50 km/h)" unless row['speed'] =~ /\d+ km\/h/ - time = json['route_summary']['total_time'] - distance = json['route_summary']['total_distance'] - speed = time>0 ? (3.6*distance/time).round : nil - got['speed'] = "#{speed} km/h" - else - got['speed'] = '' - end - end - if table.headers.include? 'bearing' - got['bearing'] = instructions ? bearings : '' - end - if table.headers.include? 'compass' - got['compass'] = instructions ? compasses : '' - end - if table.headers.include? 'turns' - got['turns'] = instructions ? turns : '' - end - if table.headers.include? 'modes' - got['modes'] = instructions ? modes : '' - end - if table.headers.include? 'times' - got['times'] = instructions ? times : '' - end - if table.headers.include? 'distances' - got['distances'] = instructions ? distances : '' - end - end - - ok = true - row.keys.each do |key| - if FuzzyMatch.match got[key], row[key] - got[key] = row[key] - else - ok = false - end - end - - unless ok - log_fail row,got, { 'route' => {:query => @query, :response => response} } - end - - actual << got - end - end - table.diff! actual -end - -When /^I route (\d+) times I should get$/ do |n,table| - ok = true - n.to_i.times do - ok = false unless step "I route I should get", table - end - ok -end diff --git a/features/step_definitions/timestamp.js b/features/step_definitions/timestamp.js new file mode 100644 index 000000000..55af74b5e --- /dev/null +++ b/features/step_definitions/timestamp.js @@ -0,0 +1,13 @@ +var assert = require('assert'); + +module.exports = function () { + this.Then(/^I should get a valid timestamp/, (callback) => { + this.ShouldGetAResponse(); + this.ShouldBeValidJSON((err) => { + this.ShouldBeWellFormed(); + assert.equal(typeof this.json.timestamp, 'string'); + assert.equal(this.json.timestamp, '2000-01-01T00:00:00Z'); + callback(err); + }); + }); +}; diff --git a/features/step_definitions/timestamp.rb b/features/step_definitions/timestamp.rb deleted file mode 100644 index df456152b..000000000 --- a/features/step_definitions/timestamp.rb +++ /dev/null @@ -1,7 +0,0 @@ -Then /^I should get a valid timestamp/ do - step "I should get a response" - step "response should be valid JSON" - step "response should be well-formed" - expect(@json['timestamp'].class).to eq(String) - expect(@json['timestamp']).to eq("2000-01-01T00:00:00Z") -end diff --git a/features/step_definitions/trip.js b/features/step_definitions/trip.js new file mode 100644 index 000000000..b1cbad177 --- /dev/null +++ b/features/step_definitions/trip.js @@ -0,0 +1,136 @@ +var util = require('util'); + +module.exports = function () { + this.When(/^I plan a trip I should get$/, (table, callback) => { + var got; + + this.reprocessAndLoadData(() => { + var testRow = (row, ri, cb) => { + var afterRequest = (err, res) => { + if (err) return cb(err); + var headers = new Set(table.raw()[0]); + + for (var k in row) { + var match = k.match(/param:(.*)/); + if (match) { + if (row[k] === '(nil)') { + params[match[1]] = null; + } else if (row[k]) { + params[match[1]] = [row[k]]; + } + + got[k] = row[k]; + } + } + + var json; + if (res.body.length) { + json = JSON.parse(res.body); + } + + if (headers.has('status')) { + got.status = json.status.toString(); + } + + if (headers.has('message')) { + got.message = json.status_message; + } + + if (headers.has('#')) { + // comment column + got['#'] = row['#']; + } + + var subTrips; + if (res.statusCode === 200) { + if (headers.has('trips')) { + subTrips = json.trips.filter(t => !!t).map(sub => sub.via_points); + } + } + + var ok = true, + encodedResult = '', + extendedTarget = ''; + + row.trips.split(',').forEach((sub, si) => { + if (si >= subTrips.length) { + ok = false; + } else { + ok = false; + // TODO: Check all rotations of the round trip + for (var ni=0; ni { + var node = this.findNodeByName(n); + if (!node) throw new Error(util.format('*** unknown waypoint node "%s"', n.trim())); + waypoints.push(node); + }); + got = { waypoints: row.waypoints }; + this.requestTrip(waypoints, params, afterRequest); + } else { + throw new Error('*** no waypoints'); + } + } + }; + + this.processRowsAndDiff(table, testRow, callback); + }); + }); +}; diff --git a/features/step_definitions/trip.rb b/features/step_definitions/trip.rb deleted file mode 100644 index f6d42406c..000000000 --- a/features/step_definitions/trip.rb +++ /dev/null @@ -1,121 +0,0 @@ -When /^I plan a trip I should get$/ do |table| - reprocess - actual = [] - OSRMLoader.load(self,"#{contracted_file}.osrm") do - table.hashes.each_with_index do |row,ri| - if row['request'] - got = {'request' => row['request'] } - response = request_url row['request'] - else - params = @query_params - waypoints = [] - if row['from'] and row['to'] - node = find_node_by_name(row['from']) - raise "*** unknown from-node '#{row['from']}" unless node - waypoints << node - - node = find_node_by_name(row['to']) - raise "*** unknown to-node '#{row['to']}" unless node - waypoints << node - - got = {'from' => row['from'], 'to' => row['to'] } - response = request_trip waypoints, params - elsif row['waypoints'] - row['waypoints'].split(',').each do |n| - node = find_node_by_name(n.strip) - raise "*** unknown waypoint node '#{n.strip}" unless node - waypoints << node - end - got = {'waypoints' => row['waypoints'] } - response = request_trip waypoints, params - else - raise "*** no waypoints" - end - end - - row.each_pair do |k,v| - if k =~ /param:(.*)/ - if v=='(nil)' - params[$1]=nil - elsif v!=nil - params[$1]=[v] - end - got[k]=v - end - end - - if response.body.empty? == false - json = JSON.parse response.body - end - - if table.headers.include? 'status' - got['status'] = json['status'].to_s - end - if table.headers.include? 'message' - got['message'] = json['status_message'] - end - if table.headers.include? '#' # comment column - got['#'] = row['#'] # copy value so it always match - end - - if response.code == "200" - if table.headers.include? 'trips' - sub_trips = json['trips'].compact.map { |sub| sub['via_points']} - end - end - - ###################### - ok = true - encoded_result = "" - extended_target = "" - row['trips'].split(',').each_with_index do |sub, sub_idx| - if sub_idx >= sub_trips.length - ok = false - break - end - - ok = false; - #TODO: Check all rotations of the round trip - sub.length.times do |node_idx| - node = find_node_by_name(sub[node_idx]) - out_node = sub_trips[sub_idx][node_idx] - if FuzzyMatch.match_location out_node, node - encoded_result += sub[node_idx] - extended_target += sub[node_idx] - ok = true - else - encoded_result += "? [#{out_node[0]},#{out_node[1]}]" - extended_target += "#{sub[node_idx]} [#{node.lat},#{node.lon}]" - end - end - end - - if ok - got['trips'] = row['trips'] - got['via_points'] = row['via_points'] - else - got['trips'] = encoded_result - row['trips'] = extended_target - log_fail row,got, { 'trip' => {:query => @query, :response => response} } - end - - - ok = true - row.keys.each do |key| - if FuzzyMatch.match got[key], row[key] - got[key] = row[key] - else - ok = false - end - end - - unless ok - log_fail row,got, { 'trip' => {:query => @query, :response => response} } - end - - actual << got - end - end - table.diff! actual -end - diff --git a/features/support/build_osm.js b/features/support/build_osm.js new file mode 100644 index 000000000..7b203d308 --- /dev/null +++ b/features/support/build_osm.js @@ -0,0 +1,160 @@ +'use strict'; + +var builder = require('xmlbuilder'); + +class DB { + constructor () { + this.nodes = new Array(); + this.ways = new Array(); + this.relations = new Array(); + } + + addNode (node) { + this.nodes.push(node); + } + + addWay (way) { + this.ways.push(way); + } + + addRelation (relation) { + this.relations.push(relation); + } + + clear () { + this.nodes = []; + this.ways = []; + this.relations = []; + } + + toXML (callback) { + var xml = builder.create('osm', {'encoding':'UTF-8'}); + xml.att('generator', 'osrm-test') + .att('version', '0.6'); + + this.nodes.forEach((n) => { + var node = xml.ele('node', { + id: n.id, + version: 1, + uid: n.OSM_UID, + user: n.OSM_USER, + timestamp: n.OSM_TIMESTAMP, + lon: n.lon, + lat: n.lat + }); + + for (var k in n.tags) { + node.ele('tag') + .att('k', k) + .att('v', n.tags[k]); + } + }); + + this.ways.forEach((w) => { + var way = xml.ele('way', { + id: w.id, + version: 1, + uid: w.OSM_UID, + user: w.OSM_USER, + timestamp: w.OSM_TIMESTAMP + }); + + w.nodes.forEach((k) => { + way.ele('nd') + .att('ref', k.id); + }); + + for (var k in w.tags) { + way.ele('tag') + .att('k', k) + .att('v', w.tags[k]); + } + }); + + this.relations.forEach((r) => { + var relation = xml.ele('relation', { + id: r.id, + user: r.OSM_USER, + timestamp: r.OSM_TIMESTAMP, + uid: r.OSM_UID + }); + + r.members.forEach((m) => { + relation.ele('member', { + type: m.type, + ref: m.id, + role: m.role + }); + }); + + for (var k in r.tags) { + relation.ele('tag') + .att('k', k) + .att('v', r.tags[k]); + } + }); + + callback(xml.end({ pretty: true, indent: ' ' })); + } +} + +class Node { + constructor (id, OSM_USER, OSM_TIMESTAMP, OSM_UID, lon, lat, tags) { + this.id = id; + this.OSM_USER = OSM_USER; + this.OSM_TIMESTAMP = OSM_TIMESTAMP; + this.OSM_UID = OSM_UID; + this.lon = lon; + this.lat = lat; + this.tags = tags; + } + + addTag (k, v) { + this.tags[k] = v; + } +} + +class Way { + constructor (id, OSM_USER, OSM_TIMESTAMP, OSM_UID) { + this.id = id; + this.OSM_USER = OSM_USER; + this.OSM_TIMESTAMP = OSM_TIMESTAMP; + this.OSM_UID = OSM_UID; + this.tags = {}; + this.nodes = []; + } + + addNode (node) { + this.nodes.push(node); + } + + setTags (tags) { + this.tags = tags; + } +} + +class Relation { + constructor (id, OSM_USER, OSM_TIMESTAMP, OSM_UID) { + this.id = id; + this.OSM_USER = OSM_USER; + this.OSM_TIMESTAMP = OSM_TIMESTAMP; + this.OSM_UID = OSM_UID; + this.members = []; + this.tags = {}; + } + + addMember (memberType, id, role) { + this.members.push({type: memberType, id: id, role: role}); + } + + addTag (k, v) { + this.tags[k] = v; + } +} + +module.exports = { + DB: DB, + Node: Node, + Way: Way, + Relation: Relation +}; diff --git a/features/support/config.js b/features/support/config.js new file mode 100644 index 000000000..cae73981c --- /dev/null +++ b/features/support/config.js @@ -0,0 +1,115 @@ +var fs = require('fs'); +var path = require('path'); +var util = require('util'); +var d3 = require('d3-queue'); +var OSM = require('./build_osm'); +var classes = require('./data_classes'); + +module.exports = function () { + this.initializeOptions = (callback) => { + this.profile = this.profile || this.DEFAULT_SPEEDPROFILE; + + this.OSMDB = this.OSMDB || new OSM.DB(); + + this.nameNodeHash = this.nameNodeHash || {}; + + this.locationHash = this.locationHash || {}; + + this.nameWayHash = this.nameWayHash || {}; + + this.osmData = new classes.osmData(this); + + this.STRESS_TIMEOUT = 300; + + this.OSRMLoader = this._OSRMLoader(); + + this.PREPROCESS_LOG_FILE = path.resolve(this.TEST_FOLDER, 'preprocessing.log'); + + this.LOG_FILE = path.resolve(this.TEST_FOLDER, 'fail.log'); + + this.HOST = 'http://127.0.0.1:' + this.OSRM_PORT; + + this.DESTINATION_REACHED = 15; // OSRM instruction code + + this.shortcutsHash = this.shortcutsHash || {}; + + var hashLuaLib = (cb) => { + fs.readdir(path.normalize(this.PROFILES_PATH + '/lib/'), (err, files) => { + if (err) cb(err); + var luaFiles = files.filter(f => !!f.match(/\.lua$/)).map(f => path.normalize(this.PROFILES_PATH + '/lib/' + f)); + this.hashOfFiles(luaFiles, hash => { + this.luaLibHash = hash; + cb(); + }); + }); + }; + + var hashProfile = (cb) => { + this.hashProfile((hash) => { + this.profileHash = hash; + cb(); + }); + }; + + var hashExtract = (cb) => { + this.hashOfFiles(util.format('%s/osrm-extract%s', this.BIN_PATH, this.EXE), (hash) => { + this.binExtractHash = hash; + cb(); + }); + }; + + var hashContract = (cb) => { + this.hashOfFiles(util.format('%s/osrm-contract%s', this.BIN_PATH, this.EXE), (hash) => { + this.binContractHash = hash; + this.fingerprintContract = this.hashString(this.binContractHash); + cb(); + }); + }; + + var hashRouted = (cb) => { + this.hashOfFiles(util.format('%s/osrm-routed%s', this.BIN_PATH, this.EXE), (hash) => { + this.binRoutedHash = hash; + this.fingerprintRoute = this.hashString(this.binRoutedHash); + cb(); + }); + }; + + d3.queue() + .defer(hashLuaLib) + .defer(hashProfile) + .defer(hashExtract) + .defer(hashContract) + .defer(hashRouted) + .awaitAll(() => { + this.fingerprintExtract = this.hashString([this.profileHash, this.luaLibHash, this.binExtractHash].join('-')); + this.AfterConfiguration(() => { + callback(); + }); + }); + }; + + this.setProfileBasedHashes = () => { + this.fingerprintExtract = this.hashString([this.profileHash, this.luaLibHash, this.binExtractHash].join('-')); + this.fingerprintContract = this.hashString(this.binContractHash); + }; + + this.setProfile = (profile, cb) => { + var lastProfile = this.profile; + if (profile !== lastProfile) { + this.profile = profile; + this.hashProfile((hash) => { + this.profileHash = hash; + this.setProfileBasedHashes(); + cb(); + }); + } else cb(); + }; + + this.setExtractArgs = (args) => { + this.extractArgs = args; + }; + + this.setContractArgs = (args) => { + this.contractArgs = args; + }; +}; diff --git a/features/support/config.rb b/features/support/config.rb deleted file mode 100644 index e4db5fac0..000000000 --- a/features/support/config.rb +++ /dev/null @@ -1,20 +0,0 @@ -def profile - @profile ||= reset_profile -end - -def reset_profile - @profile = nil - set_profile DEFAULT_SPEEDPROFILE -end - -def set_profile profile - @profile = profile -end - -def set_extract_args args - @extract_args = args -end - -def set_contract_args args - @contract_args = args -end diff --git a/features/support/data.js b/features/support/data.js new file mode 100644 index 000000000..067af504d --- /dev/null +++ b/features/support/data.js @@ -0,0 +1,339 @@ +var fs = require('fs'); +var path = require('path'); +var util = require('util'); +var exec = require('child_process').exec; +var d3 = require('d3-queue'); + +var OSM = require('./build_osm'); +var classes = require('./data_classes'); + +module.exports = function () { + this.setGridSize = (meters) => { + // the constant is calculated (with BigDecimal as: 1.0/(DEG_TO_RAD*EARTH_RADIUS_IN_METERS + // see ApproximateDistance() in ExtractorStructs.h + // it's only accurate when measuring along the equator, or going exactly north-south + this.zoom = parseFloat(meters) * 0.8990679362704610899694577444566908445396483347536032203503E-5; + }; + + this.setOrigin = (origin) => { + this.origin = origin; + }; + + this.buildWaysFromTable = (table, callback) => { + // add one unconnected way for each row + var buildRow = (row, ri, cb) => { + // comments ported directly from ruby suite: + // NOTE: currently osrm crashes when processing an isolated oneway with just 2 nodes, so we use 4 edges + // this is related to the fact that a oneway dead-end street doesn't make a lot of sense + + // if we stack ways on different x coordinates, routability tests get messed up, because osrm might pick a neighboring way if the one test can't be used. + // instead we place all lines as a string on the same y coordinate. this prevents using neighboring ways. + + // add some nodes + + var makeFakeNode = (namePrefix, offset) => { + return new OSM.Node(this.makeOSMId(), this.OSM_USER, this.OSM_TIMESTAMP, + this.OSM_UID, this.origin[0]+(offset + this.WAY_SPACING * ri) * this.zoom, + this.origin[1], {name: util.format('%s%d', namePrefix, ri)}); + }; + + var nodes = ['a','b','c','d','e'].map((l, i) => makeFakeNode(l, i)); + + nodes.forEach(node => { this.OSMDB.addNode(node); }); + + // ...with a way between them + var way = new OSM.Way(this.makeOSMId(), this.OSM_USER, this.OSM_TIMESTAMP, this.OSM_UID); + + nodes.forEach(node => { way.addNode(node); }); + + // remove tags that describe expected test result, reject empty tags + var tags = {}; + for (var rkey in row) { + if (!rkey.match(/^forw\b/) && + !rkey.match(/^backw\b/) && + !rkey.match(/^bothw\b/) && + row[rkey].length) + tags[rkey] = row[rkey]; + } + + var wayTags = { highway: 'primary' }, + nodeTags = {}; + + for (var key in tags) { + var nodeMatch = key.match(/node\/(.*)/); + if (nodeMatch) { + if (tags[key] === '(nil)') { + delete nodeTags[key]; + } else { + nodeTags[nodeMatch[1]] = tags[key]; + } + } else { + if (tags[key] === '(nil)') { + delete wayTags[key]; + } else { + wayTags[key] = tags[key]; + } + } + } + + wayTags.name = util.format('w%d', ri); + way.setTags(wayTags); + this.OSMDB.addWay(way); + + for (var k in nodeTags) { + nodes[2].addTag(k, nodeTags[k]); + } + cb(); + }; + + var q = d3.queue(); + table.hashes().forEach((row, ri) => { + q.defer(buildRow, row, ri); + }); + + q.awaitAll(callback); + }; + + var ensureDecimal = (i) => { + if (parseInt(i) === i) return i.toFixed(1); + else return i; + }; + + this.tableCoordToLonLat = (ci, ri) => { + return [this.origin[0] + ci * this.zoom, this.origin[1] - ri * this.zoom].map(ensureDecimal); + }; + + this.addOSMNode = (name, lon, lat, id) => { + id = id || this.makeOSMId(); + var node = new OSM.Node(id, this.OSM_USER, this.OSM_TIMESTAMP, this.OSM_UID, lon, lat, {name: name}); + this.OSMDB.addNode(node); + this.nameNodeHash[name] = node; + }; + + this.addLocation = (name, lon, lat) => { + this.locationHash[name] = new classes.Location(lon, lat); + }; + + this.findNodeByName = (s) => { + if (s.length !== 1) throw new Error(util.format('*** invalid node name "%s", must be single characters', s)); + if (!s.match(/[a-z0-9]/)) throw new Error(util.format('*** invalid node name "%s", must be alphanumeric', s)); + + var fromNode; + if (s.match(/[a-z]/)) { + fromNode = this.nameNodeHash[s.toString()]; + } else { + fromNode = this.locationHash[s.toString()]; + } + + return fromNode; + }; + + this.findWayByName = (s) => { + return this.nameWayHash[s.toString()] || this.nameWayHash[s.toString().split('').reverse().join('')]; + }; + + this.resetData = () => { + this.resetOSM(); + }; + + this.makeOSMId = () => { + this.osmID = this.osmID + 1; + return this.osmID; + }; + + this.resetOSM = () => { + this.OSMDB.clear(); + this.osmData.reset(); + this.nameNodeHash = {}; + this.locationHash = {}; + this.nameWayHash = {}; + this.osmID = 0; + }; + + this.writeOSM = (callback) => { + + fs.exists(this.DATA_FOLDER, (exists) => { + var mkDirFn = exists ? (cb) => { cb(); } : fs.mkdir.bind(fs.mkdir, this.DATA_FOLDER); + mkDirFn((err) => { + if (err) return callback(err); + var osmPath = path.resolve(this.DATA_FOLDER, util.format('%s.osm', this.osmData.osmFile)); + fs.exists(osmPath, (exists) => { + if (!exists) fs.writeFile(osmPath, this.osmData.str, callback); + else callback(); + }); + }); + }); + }; + + this.isExtracted = (callback) => { + fs.exists(util.format('%s.osrm', this.osmData.extractedFile), (core) => { + if (!core) return callback(false); + fs.exists(util.format('%s.osrm.names', this.osmData.extractedFile), (names) => { + if (!names) return callback(false); + fs.exists(util.format('%s.osrm.restrictions', this.osmData.extractedFile), (restrictions) => { + return callback(restrictions); + }); + }); + }); + }; + + this.isContracted = (callback) => { + fs.exists(util.format('%s.osrm.hsgr', this.osmData.contractedFile), callback); + }; + + this.writeTimestamp = (callback) => { + fs.writeFile(util.format('%s.osrm.timestamp', this.osmData.contractedFile), this.OSM_TIMESTAMP, callback); + }; + + this.writeInputData = (callback) => { + this.writeOSM((err) => { + if (err) return callback(err); + this.writeTimestamp(callback); + }); + }; + + this.extractData = (callback) => { + this.logPreprocessInfo(); + this.log(util.format('== Extracting %s.osm...', this.osmData.osmFile), 'preprocess'); + var cmd = util.format('%s%s/osrm-extract %s.osm %s --profile %s/%s.lua >>%s 2>&1', + this.LOAD_LIBRARIES, this.BIN_PATH, this.osmData.osmFile, this.extractArgs || '', this.PROFILES_PATH, this.profile, this.PREPROCESS_LOG_FILE); + this.log(cmd); + process.chdir(this.TEST_FOLDER); + exec(cmd, (err) => { + if (err) { + this.log(util.format('*** Exited with code %d', err.code), 'preprocess'); + return callback(this.ExtractError(err.code, util.format('osrm-extract exited with code %d', err.code))); + } + + var q = d3.queue(); + + var rename = (file, cb) => { + this.log(util.format('Renaming %s.%s to %s.%s', this.osmData.osmFile, file, this.osmData.extractedFile, file), 'preprocess'); + fs.rename([this.osmData.osmFile, file].join('.'), [this.osmData.extractedFile, file].join('.'), (err) => { + if (err) return cb(this.FileError(null, 'failed to rename data file after extracting')); + cb(); + }); + }; + + var renameIfExists = (file, cb) => { + fs.stat([this.osmData.osmFile, file].join('.'), (doesNotExistErr, exists) => { + if (exists) rename(file, cb); + else cb(); + }); + }; + + ['osrm','osrm.names','osrm.restrictions','osrm.ebg','osrm.enw','osrm.edges','osrm.fileIndex','osrm.geometry','osrm.nodes','osrm.ramIndex'].forEach(file => { + q.defer(rename, file); + }); + + ['osrm.edge_segment_lookup','osrm.edge_penalties'].forEach(file => { + q.defer(renameIfExists, file); + }); + + q.awaitAll((err) => { + this.log('Finished extracting ' + this.osmData.extractedFile, 'preprocess'); + process.chdir('../'); + callback(err); + }); + }); + }; + + this.contractData = (callback) => { + this.logPreprocessInfo(); + this.log(util.format('== Contracting %s.osm...', this.osmData.extractedFile), 'preprocess'); + var cmd = util.format('%s%s/osrm-contract %s %s.osrm >>%s 2>&1', + this.LOAD_LIBRARIES, this.BIN_PATH, this.contractArgs || '', this.osmData.extractedFile, this.PREPROCESS_LOG_FILE); + this.log(cmd); + process.chdir(this.TEST_FOLDER); + exec(cmd, (err) => { + if (err) { + this.log(util.format('*** Exited with code %d', err.code), 'preprocess'); + return callback(this.ContractError(err.code, util.format('osrm-contract exited with code %d', err.code))); + } + + var rename = (file, cb) => { + this.log(util.format('Renaming %s.%s to %s.%s', this.osmData.extractedFile, file, this.osmData.contractedFile, file), 'preprocess'); + fs.rename([this.osmData.extractedFile, file].join('.'), [this.osmData.contractedFile, file].join('.'), (err) => { + if (err) return cb(this.FileError(null, 'failed to rename data file after contracting.')); + cb(); + }); + }; + + var copy = (file, cb) => { + this.log(util.format('Copying %s.%s to %s.%s', this.osmData.extractedFile, file, this.osmData.contractedFile, file), 'preprocess'); + fs.createReadStream([this.osmData.extractedFile, file].join('.')) + .pipe(fs.createWriteStream([this.osmData.contractedFile, file].join('.')) + .on('finish', cb) + ) + .on('error', () => { + return cb(this.FileError(null, 'failed to copy data after contracting.')); + }); + }; + + var q = d3.queue(); + + ['osrm.hsgr','osrm.fileIndex','osrm.geometry','osrm.nodes','osrm.ramIndex','osrm.core','osrm.edges'].forEach((file) => { + q.defer(rename, file); + }); + + ['osrm.names','osrm.restrictions','osrm'].forEach((file) => { + q.defer(copy, file); + }); + + q.awaitAll((err) => { + this.log('Finished contracting ' + this.osmData.contractedFile, 'preprocess'); + process.chdir('../'); + callback(err); + }); + }); + }; + + var noop = (cb) => cb(); + + this.reprocess = (callback) => { + this.writeAndExtract((e) => { + if (e) return callback(e); + this.isContracted((isContracted) => { + var contractFn = isContracted ? noop : this.contractData; + if (isContracted) this.log('Already contracted ' + this.osmData.contractedFile, 'preprocess'); + contractFn((e) => { + if (e) return callback(e); + this.logPreprocessDone(); + callback(); + }); + }); + }); + }; + + this.writeAndExtract = (callback) => { + this.osmData.populate(() => { + this.writeInputData((e) => { + if (e) return callback(e); + this.isExtracted((isExtracted) => { + var extractFn = isExtracted ? noop : this.extractData; + if (isExtracted) this.log('Already extracted ' + this.osmData.extractedFile, 'preprocess'); + extractFn((e) => { + callback(e); + }); + }); + }); + }); + }; + + this.reprocessAndLoadData = (callback) => { + this.reprocess(() => { + this.OSRMLoader.load(util.format('%s.osrm', this.osmData.contractedFile), callback); + }); + }; + + this.processRowsAndDiff = (table, fn, callback) => { + var q = d3.queue(1); + + table.hashes().forEach((row, i) => q.defer(fn, row, i)); + + q.awaitAll((err, actual) => { + if (err) return callback(err); + this.diffTables(table, actual, {}, callback); + }); + }; +}; diff --git a/features/support/data.rb b/features/support/data.rb deleted file mode 100644 index a62bc86f8..000000000 --- a/features/support/data.rb +++ /dev/null @@ -1,321 +0,0 @@ -require 'OSM/objects' #osmlib gem -require 'OSM/Database' -require 'builder' -require 'fileutils' - -class Location - attr_accessor :lon,:lat - - def initialize lon,lat - @lat = lat - @lon = lon - end -end - - -def set_input_format format - raise '*** Input format must be eiter "osm" or "pbf"' unless ['pbf','osm'].include? format.to_s - @input_format = format.to_s -end - -def input_format - @input_format || DEFAULT_INPUT_FORMAT -end - -def sanitized_scenario_title - @sanitized_scenario_title ||= @scenario_title.to_s.gsub /[^0-9A-Za-z.\-]/, '_' -end - -def set_grid_size meters - #the constant is calculated (with BigDecimal as: 1.0/(DEG_TO_RAD*EARTH_RADIUS_IN_METERS - #see ApproximateDistance() in ExtractorStructs.h - #it's only accurate when measuring along the equator, or going exactly north-south - @zoom = meters.to_f*0.8990679362704610899694577444566908445396483347536032203503E-5 -end - -def set_origin origin - @origin = origin -end - -def build_ways_from_table table - #add one unconnected way for each row - table.hashes.each_with_index do |row,ri| - #NOTE: - #currently osrm crashes when processing an isolated oneway with just 2 nodes, so we use 4 edges - #this is relatated to the fact that a oneway dead-end street doesn't make a lot of sense - - #if we stack ways on different x coordinates, routability tests get messed up, because osrm might pick a neighboring way if the one test can't be used. - #instead we place all lines as a string on the same y coordinate. this prevents using neightboring ways. - - #a few nodes... - node1 = OSM::Node.new make_osm_id, OSM_USER, OSM_TIMESTAMP, @origin[0]+(0+WAY_SPACING*ri)*@zoom, @origin[1] - node2 = OSM::Node.new make_osm_id, OSM_USER, OSM_TIMESTAMP, @origin[0]+(1+WAY_SPACING*ri)*@zoom, @origin[1] - node3 = OSM::Node.new make_osm_id, OSM_USER, OSM_TIMESTAMP, @origin[0]+(2+WAY_SPACING*ri)*@zoom, @origin[1] - node4 = OSM::Node.new make_osm_id, OSM_USER, OSM_TIMESTAMP, @origin[0]+(3+WAY_SPACING*ri)*@zoom, @origin[1] - node5 = OSM::Node.new make_osm_id, OSM_USER, OSM_TIMESTAMP, @origin[0]+(4+WAY_SPACING*ri)*@zoom, @origin[1] - node1.uid = OSM_UID - node2.uid = OSM_UID - node3.uid = OSM_UID - node4.uid = OSM_UID - node5.uid = OSM_UID - node1 << { :name => "a#{ri}" } - node2 << { :name => "b#{ri}" } - node3 << { :name => "c#{ri}" } - node4 << { :name => "d#{ri}" } - node5 << { :name => "e#{ri}" } - - osm_db << node1 - osm_db << node2 - osm_db << node3 - osm_db << node4 - osm_db << node5 - - #...with a way between them - way = OSM::Way.new make_osm_id, OSM_USER, OSM_TIMESTAMP - way.uid = OSM_UID - way << node1 - way << node2 - way << node3 - way << node4 - way << node5 - - tags = row.dup - - # remove tags that describe expected test result - tags.reject! do |k,v| - k =~ /^forw\b/ || - k =~ /^backw\b/ || - k =~ /^bothw\b/ - end - - ##remove empty tags - tags.reject! { |k,v| v=='' } - - # sort tag keys in the form of 'node/....' - way_tags = { 'highway' => 'primary' } - - node_tags = {} - tags.each_pair do |k,v| - if k =~ /node\/(.*)/ - if v=='(nil)' - node_tags.delete k - else - node_tags[$1] = v - end - else - if v=='(nil)' - way_tags.delete k - else - way_tags[k] = v - end - end - end - - way_tags['name'] = "w#{ri}" - way << way_tags - node3 << node_tags - - osm_db << way - end -end - -def table_coord_to_lonlat ci,ri - [@origin[0]+ci*@zoom, @origin[1]-ri*@zoom] -end - -def add_osm_node name,lon,lat,id - id = make_osm_id if id == nil - node = OSM::Node.new id, OSM_USER, OSM_TIMESTAMP, lon, lat - node << { :name => name } - node.uid = OSM_UID - osm_db << node - name_node_hash[name] = node -end - -def add_location name,lon,lat - location_hash[name] = Location.new(lon,lat) -end - -def find_node_by_name s - raise "***invalid node name '#{s}', must be single characters" unless s.size == 1 - raise "*** invalid node name '#{s}', must be alphanumeric" unless s.match /[a-z0-9]/ - if s.match /[a-z]/ - from_node = name_node_hash[ s.to_s ] - else - from_node = location_hash[ s.to_s ] - end -end - -def find_way_by_name s - name_way_hash[s.to_s] || name_way_hash[s.to_s.reverse] -end - -def reset_data - Dir.chdir TEST_FOLDER do - #clear_log - #clear_data_files - end - reset_profile - reset_osm - @fingerprint_osm = nil - @fingerprint_extract = nil - @fingerprint_prepare = nil - @fingerprint_route = nil -end - -def make_osm_id - @osm_id = @osm_id+1 -end - -def reset_osm - osm_db.clear - name_node_hash.clear - location_hash.clear - name_way_hash.clear - @osm_str = nil - @osm_hash = nil - @osm_id = 0 -end - -def clear_data_files - File.delete *Dir.glob("#{DATA_FOLDER}/test.*") -end - -def clear_log - File.delete *Dir.glob("*.log") -end - -def osm_db - @osm_db ||= OSM::Database.new -end - -def name_node_hash - @name_node_hash ||= {} -end - -def location_hash - @location_hash ||= {} -end - -def name_way_hash - @name_way_hash ||= {} -end - -def osm_str - return @osm_str if @osm_str - @osm_str = '' - doc = Builder::XmlMarkup.new :indent => 2, :target => @osm_str - doc.instruct! - osm_db.to_xml doc, OSM_GENERATOR - @osm_str -end - -def osm_file - @osm_file ||= "#{DATA_FOLDER}/#{fingerprint_osm}" -end - -def extracted_file - @extracted_file ||= "#{osm_file}_#{fingerprint_extract}" -end - -def contracted_file - @contracted_file ||= "#{osm_file}_#{fingerprint_extract}_#{fingerprint_prepare}" -end - -def write_osm - Dir.mkdir DATA_FOLDER unless File.exist? DATA_FOLDER - unless File.exist?("#{osm_file}.osm") - File.open( "#{osm_file}.osm", 'w') {|f| f.write(osm_str) } - end -end - -def extracted? - Dir.chdir TEST_FOLDER do - File.exist?("#{extracted_file}.osrm") && - File.exist?("#{extracted_file}.osrm.names") && - File.exist?("#{extracted_file}.osrm.restrictions") - end -end - -def contracted? - Dir.chdir TEST_FOLDER do - File.exist?("#{contracted_file}.osrm.hsgr") - end -end - -def write_timestamp - File.open( "#{contracted_file}.osrm.timestamp", 'w') {|f| f.write(OSM_TIMESTAMP) } -end - -def write_input_data - Dir.chdir TEST_FOLDER do - write_osm - write_timestamp - end -end - -def extract_data - Dir.chdir TEST_FOLDER do - log_preprocess_info - log "== Extracting #{osm_file}.osm...", :preprocess - log "#{LOAD_LIBRARIES}#{BIN_PATH}/osrm-extract #{osm_file}.osm #{@extract_args} --profile #{PROFILES_PATH}/#{@profile}.lua >>#{PREPROCESS_LOG_FILE} 2>&1" - unless system "#{LOAD_LIBRARIES}#{BIN_PATH}/osrm-extract #{osm_file}.osm #{@extract_args} --profile #{PROFILES_PATH}/#{@profile}.lua >>#{PREPROCESS_LOG_FILE} 2>&1" - log "*** Exited with code #{$?.exitstatus}.", :preprocess - raise ExtractError.new $?.exitstatus, "osrm-extract exited with code #{$?.exitstatus}." - end - begin - ["osrm","osrm.names","osrm.restrictions","osrm.ebg","osrm.enw","osrm.edges","osrm.fileIndex","osrm.geometry","osrm.nodes","osrm.ramIndex"].each do |file| - log "Renaming #{osm_file}.#{file} to #{extracted_file}.#{file}", :preprocess - File.rename "#{osm_file}.#{file}", "#{extracted_file}.#{file}" - end - rescue Exception => e - raise FileError.new nil, "failed to rename data file after extracting." - end - begin - ["osrm.edge_segment_lookup","osrm.edge_penalties"].each do |file| - if File.exists?("#{osm_file}.#{file}") - log "Renaming #{osm_file}.#{file} to #{extracted_file}.#{file}", :preprocess - File.rename "#{osm_file}.#{file}", "#{extracted_file}.#{file}" - end - end - rescue Exception => e - raise FileError.new nil, "failed to rename data file after extracting." - end - end -end - -def prepare_data - Dir.chdir TEST_FOLDER do - log_preprocess_info - log "== Preparing #{extracted_file}.osm...", :preprocess - log "#{LOAD_LIBRARIES}#{BIN_PATH}/osrm-contract #{@contract_args} #{extracted_file}.osrm >>#{PREPROCESS_LOG_FILE} 2>&1" - unless system "#{LOAD_LIBRARIES}#{BIN_PATH}/osrm-contract #{@contract_args} #{extracted_file}.osrm >>#{PREPROCESS_LOG_FILE} 2>&1" - log "*** Exited with code #{$?.exitstatus}.", :preprocess - raise PrepareError.new $?.exitstatus, "osrm-contract exited with code #{$?.exitstatus}." - end - begin - ["osrm.hsgr","osrm.fileIndex","osrm.geometry","osrm.nodes","osrm.ramIndex","osrm.core","osrm.edges"].each do |file| - log "Renaming #{extracted_file}.#{file} to #{contracted_file}.#{file}", :preprocess - File.rename "#{extracted_file}.#{file}", "#{contracted_file}.#{file}" - end - rescue Exception => e - raise FileError.new nil, "failed to rename data file after preparing." - end - begin - ["osrm.names","osrm.restrictions","osrm"].each do |file| - log "Copying #{extracted_file}.#{file} to #{contracted_file}.#{file}", :preprocess - FileUtils.cp "#{extracted_file}.#{file}", "#{contracted_file}.#{file}" - end - rescue Exception => e - raise FileError.new nil, "failed to copy data file after preparing." - end - log '', :preprocess - end -end - -def reprocess - write_input_data - extract_data unless extracted? - prepare_data unless contracted? - log_preprocess_done -end diff --git a/features/support/data_classes.js b/features/support/data_classes.js new file mode 100644 index 000000000..91909d215 --- /dev/null +++ b/features/support/data_classes.js @@ -0,0 +1,85 @@ +'use strict'; + +var util = require('util'); +var path = require('path'); + +module.exports = { + Location: class { + constructor (lon, lat) { + this.lon = lon; + this.lat = lat; + } + }, + + osmData: class { + constructor (scope) { + this.scope = scope; + this.str = null; + this.hash = null; + this.fingerprintOSM = null; + this.osmFile = null; + this.extractedFile = null; + this.contractedFile = null; + } + + populate (callback) { + this.scope.OSMDB.toXML((str) => { + this.str = str; + + this.hash = this.scope.hashString(str); + this.fingerprintOSM = this.scope.hashString(this.hash); + + this.osmFile = path.resolve(this.scope.DATA_FOLDER, this.fingerprintOSM); + + this.extractedFile = path.resolve([this.osmFile, this.scope.fingerprintExtract].join('_')); + this.contractedFile = path.resolve([this.osmFile, this.scope.fingerprintExtract, this.scope.fingerprintContract].join('_')); + + callback(); + }); + } + + reset () { + this.str = null; + this.hash = null; + this.fingerprintOSM = null; + this.osmFile = null; + this.extractedFile = null; + this.contractedFile = null; + } + }, + + FuzzyMatch: class { + match (got, want) { + var matchPercent = want.match(/(.*)\s+~(.+)%$/), + matchAbs = want.match(/(.*)\s+\+\-(.+)$/), + matchRe = want.match(/^\/(.*)\/$/); + + if (got === want) { + return true; + } else if (matchPercent) { // percentage range: 100 ~ 5% + var target = parseFloat(matchPercent[1]), + percentage = parseFloat(matchPercent[2]); + if (target === 0) { + return true; + } else { + var ratio = Math.abs(1 - parseFloat(got) / target); + return 100 * ratio < percentage; + } + } else if (matchAbs) { // absolute range: 100 +-5 + var margin = parseFloat(matchAbs[2]), + fromR = parseFloat(matchAbs[1]) - margin, + toR = parseFloat(matchAbs[1]) + margin; + return parseFloat(got) >= fromR && parseFloat(got) <= toR; + } else if (matchRe) { // regex: /a,b,.*/ + return got.match(matchRe[1]); + } else { + return false; + } + } + + matchLocation (got, want) { + return this.match(got[0], util.format('%d ~0.0025%', want.lat)) && + this.match(got[1], util.format('%d ~0.0025%', want.lon)); + } + } +}; diff --git a/features/support/env.js b/features/support/env.js new file mode 100644 index 000000000..cae361cd9 --- /dev/null +++ b/features/support/env.js @@ -0,0 +1,125 @@ +var path = require('path'); +var util = require('util'); +var fs = require('fs'); +var exec = require('child_process').exec; +var d3 = require('d3-queue'); + +module.exports = function () { + this.initializeEnv = (callback) => { + this.DEFAULT_PORT = 5000; + this.DEFAULT_TIMEOUT = 2000; + this.setDefaultTimeout(this.DEFAULT_TIMEOUT); + this.ROOT_FOLDER = process.cwd(); + this.OSM_USER = 'osrm'; + this.OSM_GENERATOR = 'osrm-test'; + this.OSM_UID = 1; + this.TEST_FOLDER = path.resolve(this.ROOT_FOLDER, 'test'); + this.DATA_FOLDER = path.resolve(this.TEST_FOLDER, 'cache'); + this.OSM_TIMESTAMP = '2000-01-01T00:00:00Z'; + this.DEFAULT_SPEEDPROFILE = 'bicycle'; + this.WAY_SPACING = 100; + this.DEFAULT_GRID_SIZE = 100; // meters + this.PROFILES_PATH = path.resolve(this.ROOT_FOLDER, 'profiles'); + this.FIXTURES_PATH = path.resolve(this.ROOT_FOLDER, 'unit_tests/fixtures'); + this.BIN_PATH = path.resolve(this.ROOT_FOLDER, 'build'); + this.DEFAULT_INPUT_FORMAT = 'osm'; + this.DEFAULT_ORIGIN = [1,1]; + this.LAUNCH_TIMEOUT = 1000; + this.SHUTDOWN_TIMEOUT = 10000; + this.DEFAULT_LOAD_METHOD = 'datastore'; + this.OSRM_ROUTED_LOG_FILE = path.resolve(this.TEST_FOLDER, 'osrm-routed.log'); + this.ERROR_LOG_FILE = path.resolve(this.TEST_FOLDER, 'error.log'); + + // OS X shim to ensure shared libraries from custom locations can be loaded + // This is needed in OS X >= 10.11 because DYLD_LIBRARY_PATH is blocked + // https://forums.developer.apple.com/thread/9233 + this.LOAD_LIBRARIES = process.env.OSRM_SHARED_LIBRARY_PATH ? util.format('DYLD_LIBRARY_PATH=%s ', process.env.OSRM_SHARED_LIBRARY_PATH) : ''; + + // TODO make sure this works on win + if (process.platform.match(/indows.*/)) { + this.TERMSIGNAL = 9; + this.EXE = '.exe'; + this.QQ = '"'; + } else { + this.TERMSIGNAL = 'SIGTERM'; + this.EXE = ''; + this.QQ = ''; + } + + // eslint-disable-next-line no-console + console.info(util.format('Node Version', process.version)); + if (parseInt(process.version.match(/v(\d)/)[1]) < 4) throw new Error('*** PLease upgrade to Node 4.+ to run OSRM cucumber tests'); + + if (process.env.OSRM_PORT) { + this.OSRM_PORT = parseInt(process.env.OSRM_PORT); + // eslint-disable-next-line no-console + console.info(util.format('Port set to %d', this.OSRM_PORT)); + } else { + this.OSRM_PORT = this.DEFAULT_PORT; + // eslint-disable-next-line no-console + console.info(util.format('Using default port %d', this.OSRM_PORT)); + } + + if (process.env.OSRM_TIMEOUT) { + this.OSRM_TIMEOUT = parseInt(process.env.OSRM_TIMEOUT); + // eslint-disable-next-line no-console + console.info(util.format('Timeout set to %d', this.OSRM_TIMEOUT)); + } else { + this.OSRM_TIMEOUT = this.DEFAULT_TIMEOUT; + // eslint-disable-next-line no-console + console.info(util.format('Using default timeout %d', this.OSRM_TIMEOUT)); + } + + fs.exists(this.TEST_FOLDER, (exists) => { + if (!exists) throw new Error(util.format('*** Test folder %s doesn\'t exist.', this.TEST_FOLDER)); + callback(); + }); + }; + + this.verifyOSRMIsNotRunning = () => { + if (this.OSRMLoader.up()) { + throw new Error('*** osrm-routed is already running.'); + } + }; + + this.verifyExistenceOfBinaries = (callback) => { + var verify = (bin, cb) => { + var binPath = path.resolve(util.format('%s/%s%s', this.BIN_PATH, bin, this.EXE)); + fs.exists(binPath, (exists) => { + if (!exists) throw new Error(util.format('%s is missing. Build failed?', binPath)); + var helpPath = util.format('%s%s --help > /dev/null 2>&1', this.LOAD_LIBRARIES, binPath); + exec(helpPath, (err) => { + if (err) { + this.log(util.format('*** Exited with code %d', err.code), 'preprocess'); + throw new Error(util.format('*** %s exited with code %d', helpPath, err.code)); + } + cb(); + }); + }); + }; + + var q = d3.queue(); + ['osrm-extract', 'osrm-contract', 'osrm-routed'].forEach(bin => { q.defer(verify, bin); }); + q.awaitAll(() => { + callback(); + }); + }; + + this.AfterConfiguration = (callback) => { + this.clearLogFiles(() => { + this.verifyOSRMIsNotRunning(); + this.verifyExistenceOfBinaries(() => { + callback(); + }); + }); + }; + + process.on('exit', () => { + if (this.OSRMLoader.loader) this.OSRMLoader.shutdown(() => {}); + }); + + process.on('SIGINT', () => { + process.exit(2); + // TODO need to handle for windows?? + }); +}; diff --git a/features/support/env.rb b/features/support/env.rb deleted file mode 100644 index 80bda262d..000000000 --- a/features/support/env.rb +++ /dev/null @@ -1,111 +0,0 @@ -require 'rspec/expectations' - - -DEFAULT_PORT = 5000 -DEFAULT_TIMEOUT = 2 -ROOT_FOLDER = Dir.pwd -OSM_USER = 'osrm' -OSM_GENERATOR = 'osrm-test' -OSM_UID = 1 -TEST_FOLDER = File.join ROOT_FOLDER, 'test' -DATA_FOLDER = 'cache' -OSM_TIMESTAMP = '2000-01-01T00:00:00Z' -DEFAULT_SPEEDPROFILE = 'bicycle' -WAY_SPACING = 100 -DEFAULT_GRID_SIZE = 100 #meters -PROFILES_PATH = File.join ROOT_FOLDER, 'profiles' -FIXTURES_PATH = File.join ROOT_FOLDER, 'unit_tests/fixtures' -BIN_PATH = File.join ROOT_FOLDER, 'build' -DEFAULT_INPUT_FORMAT = 'osm' -DEFAULT_ORIGIN = [1,1] -LAUNCH_TIMEOUT = 1 -SHUTDOWN_TIMEOUT = 10 -DEFAULT_LOAD_METHOD = 'datastore' -OSRM_ROUTED_LOG_FILE = 'osrm-routed.log' - -# OS X shim to ensure shared libraries from custom locations can be loaded -# This is needed in OS X >= 10.11 because DYLD_LIBRARY_PATH is blocked -# https://forums.developer.apple.com/thread/9233 -if ENV['OSRM_SHARED_LIBRARY_PATH'] - LOAD_LIBRARIES="DYLD_LIBRARY_PATH=#{ENV['OSRM_SHARED_LIBRARY_PATH']} " -else - LOAD_LIBRARIES="" -end - -if ENV['OS']=~/Windows.*/ then - TERMSIGNAL=9 -else - TERMSIGNAL='TERM' -end - - -def log_time_and_run cmd - log_time cmd - `#{cmd}` -end - -def log_time cmd - puts "[#{Time.now.strftime('%Y-%m-%d %H:%M:%S:%L')}] #{cmd}" -end - - -puts "Ruby version #{RUBY_VERSION}" -unless RUBY_VERSION.to_f >= 1.9 - raise "*** Please upgrade to Ruby 1.9.x to run the OSRM cucumber tests" -end - -if ENV["OSRM_PORT"] - OSRM_PORT = ENV["OSRM_PORT"].to_i - puts "Port set to #{OSRM_PORT}" -else - OSRM_PORT = DEFAULT_PORT - puts "Using default port #{OSRM_PORT}" -end - -if ENV["OSRM_TIMEOUT"] - OSRM_TIMEOUT = ENV["OSRM_TIMEOUT"].to_i - puts "Timeout set to #{OSRM_TIMEOUT}" -else - OSRM_TIMEOUT = DEFAULT_TIMEOUT - puts "Using default timeout #{OSRM_TIMEOUT}" -end - -unless File.exists? TEST_FOLDER - raise "*** Test folder #{TEST_FOLDER} doesn't exist." -end - -def verify_osrm_is_not_running - if OSRMLoader::OSRMBaseLoader.new.osrm_up? - raise "*** osrm-routed is already running." - end -end - -def verify_existance_of_binaries - ["osrm-extract", "osrm-contract", "osrm-routed"].each do |bin| - unless File.exists? "#{BIN_PATH}/#{bin}#{EXE}" - raise "*** #{BIN_PATH}/#{bin}#{EXE} is missing. Build failed?" - end - unless system "#{LOAD_LIBRARIES}#{BIN_PATH}/#{bin}#{EXE} --help > /dev/null 2>&1" - log "*** Exited with code #{$?.exitstatus}.", :preprocess - raise "*** #{LOAD_LIBRARIES}#{BIN_PATH}/#{bin}#{EXE} --help exited with code #{$?.exitstatus}." - end - end -end - -if ENV['OS']=~/Windows.*/ then - EXE='.exe' - QQ='"' -else - EXE='' - QQ='' -end - -AfterConfiguration do |config| - clear_log_files - verify_osrm_is_not_running - verify_existance_of_binaries -end - -at_exit do - OSRMLoader::OSRMBaseLoader.new.shutdown -end diff --git a/features/support/exception_classes.js b/features/support/exception_classes.js new file mode 100644 index 000000000..609e92914 --- /dev/null +++ b/features/support/exception_classes.js @@ -0,0 +1,130 @@ +'use strict'; + +var util = require('util'); +var fs = require('fs'); + +var OSRMError = class extends Error { + constructor (process, code, msg, log, lines) { + super(msg); + this.process = process; + this.code = code; + this.msg = msg; + this.lines = lines; + this.log = log; + } + + extract (callback) { + this.logTail(this.log, this.lines, callback); + } + + toString (callback) { + this.extract((tail) => { + callback(util.format('*** %s\nLast %s from %s:\n%s\n', this.msg, this.lines, this.log, tail)); + }); + } + + logTail (path, n, callback) { + var expanded = path.resolve(this.TEST_FOLDER, path); + fs.exists(expanded, (exists) => { + if (exists) { + fs.readFile(expanded, (err, data) => { + var lines = data.trim().split('\n'); + callback(lines + .slice(lines.length - n) + .map(line => util.format(' %s', line)) + .join('\n')); + }); + } else { + callback(util.format('File %s does not exist!', expanded)); + } + }); + } +}; + +var unescapeStr = (str) => str.replace(/\\\|/g, '\|').replace(/\\\\/g, '\\'); + +module.exports = { + OSRMError: OSRMError, + + FileError: class extends OSRMError { + constructor (logFile, code, msg) { + super ('fileutil', code, msg, logFile, 5); + } + }, + + LaunchError: class extends OSRMError { + constructor (logFile, launchProcess, code, msg) { + super (launchProcess, code, msg, logFile, 5); + } + }, + + ExtractError: class extends OSRMError { + constructor (logFile, code, msg) { + super('osrm-extract', code, msg, logFile, 3); + } + }, + + ContractError: class extends OSRMError { + constructor (logFile, code, msg) { + super('osrm-contract', code, msg, logFile, 3); + } + }, + + RoutedError: class extends OSRMError { + constructor (logFile, msg) { + super('osrm-routed', null, msg, logFile, 3); + } + }, + + TableDiffError: class extends Error { + constructor (expected, actual) { + super(); + this.headers = expected.raw()[0]; + this.expected = expected.hashes(); + this.actual = actual; + this.diff = []; + this.hasErrors = false; + + var good = 0, bad = 0; + + this.expected.forEach((row, i) => { + var rowError = false; + + for (var j in row) { + if (unescapeStr(row[j]) != actual[i][j]) { + rowError = true; + this.hasErrors = true; + break; + } + } + + if (rowError) { + bad++; + this.diff.push(Object.assign({}, row, {status: 'undefined'})); + this.diff.push(Object.assign({}, actual[i], {status: 'comment'})); + } else { + good++; + this.diff.push(row); + } + }); + } + + get string () { + if (!this.hasErrors) return null; + + var s = ['Tables were not identical:']; + s.push(this.headers.map(key => ' ' + key).join(' | ')); + this.diff.forEach((row) => { + var rowString = '| '; + this.headers.forEach((header) => { + if (!row.status) rowString += ' ' + row[header] + ' | '; + else if (row.status === 'undefined') rowString += '(-) ' + row[header] + ' | '; + else rowString += '(+) ' + row[header] + ' | '; + }); + s.push(rowString); + }); + + return s.join('\n') + '\nTODO this is a temp workaround waiting for https://github.com/cucumber/cucumber-js/issues/534'; + } + } +}; diff --git a/features/support/exceptions.js b/features/support/exceptions.js new file mode 100644 index 000000000..6af1a93b0 --- /dev/null +++ b/features/support/exceptions.js @@ -0,0 +1,15 @@ +var exceptions = require('./exception_classes'); + +module.exports = function () { + this.OSRMError = exceptions.OSRMError, + + this.FileError = (code, msg) => new (exceptions.FileError.bind(exceptions.FileError, this.PREPROCESS_LOG_FILE))(code, msg); + + this.LaunchError = (code, launchProcess, msg) => new (exceptions.LaunchError.bind(exceptions.LaunchError, this.ERROR_LOG_FILE))(code, launchProcess, msg); + + this.ExtractError = (code, msg) => new (exceptions.ExtractError.bind(exceptions.ExtractError, this.PREPROCESS_LOG_FILE))(code, msg); + + this.ContractError = (code, msg) => new (exceptions.ContractError.bind(exceptions.ContractError, this.PREPROCESS_LOG_FILE))(code, msg); + + this.RoutedError = (msg) => new (exceptions.RoutedError.bind(exceptions.RoutedError, this.OSRM_ROUTED_LOG_FILE))(msg); +}; diff --git a/features/support/exceptions.rb b/features/support/exceptions.rb deleted file mode 100644 index e3d37b30a..000000000 --- a/features/support/exceptions.rb +++ /dev/null @@ -1,56 +0,0 @@ - -class OSRMError < StandardError - attr_accessor :msg, :code, :process - - def initialize process, code, msg, log, lines - @process = process - @code = code - @msg = msg - @lines = lines - @log = log - @extract = log_tail @log, @lines - end - - def to_s - "*** #{@msg}\nLast #{@lines} lines from #{@log}:\n#{@extract}\n" - end - - private - - def log_tail path, n - Dir.chdir TEST_FOLDER do - expanded = File.expand_path path - if File.exists? expanded - File.open(expanded) do |f| - return f.tail(n).map { |line| " #{line}" }.join "\n" - end - else - return "File '#{expanded} does not exist!" - end - end - end -end - -class FileError < OSRMError - def initialize code, msg - super 'fileutil', code, msg, PREPROCESS_LOG_FILE, 5 - end -end - -class ExtractError < OSRMError - def initialize code, msg - super 'osrm-extract', code, msg, PREPROCESS_LOG_FILE, 3 - end -end - -class PrepareError < OSRMError - def initialize code, msg - super 'osrm-contract', code, msg, PREPROCESS_LOG_FILE, 3 - end -end - -class RoutedError < OSRMError - def initialize msg - super 'osrm-routed', nil, msg, OSRM_ROUTED_LOG_FILE, 3 - end -end diff --git a/features/support/file.rb b/features/support/file.rb deleted file mode 100644 index dfaae0a45..000000000 --- a/features/support/file.rb +++ /dev/null @@ -1,34 +0,0 @@ -class File - - # read last n lines of a file (trailing newlines are ignored) - def tail(n) - return [] if size==0 - buffer = 1024 - str = nil - - if size>buffer - chunks = [] - lines = 0 - idx = size - begin - idx -= buffer # rewind - if idx<0 - buffer += idx # adjust last read to avoid negative index - idx = 0 - end - seek(idx) - chunk = read(buffer) - chunk.gsub!(/\n+\Z/,"") if chunks.empty? # strip newlines from end of file (first chunk) - lines += chunk.count("\n") # update total lines found - chunks.unshift chunk # prepend - end while lines<(n) && idx>0 # stop when enough lines found or no more to read - str = chunks.join('') - else - str = read(buffer) - end - - # return last n lines of str - lines = str.split("\n") - lines.size>=n ? lines[-n,n] : lines - end -end \ No newline at end of file diff --git a/features/support/fuzzy.js b/features/support/fuzzy.js new file mode 100644 index 000000000..6eebe0a61 --- /dev/null +++ b/features/support/fuzzy.js @@ -0,0 +1,5 @@ +var classes = require('./data_classes'); + +module.exports = function() { + this.FuzzyMatch = new classes.FuzzyMatch(); +}; diff --git a/features/support/fuzzy.rb b/features/support/fuzzy.rb deleted file mode 100644 index 066d9c5a8..000000000 --- a/features/support/fuzzy.rb +++ /dev/null @@ -1,32 +0,0 @@ -class FuzzyMatch - - def self.match got, want - if got == want - return true - elsif want.match /(.*)\s+~(.+)%$/ #percentage range: 100 ~5% - target = $1.to_f - percentage = $2.to_f - if target==0 - return true - else - ratio = (1-(got.to_f / target)).abs; - return 100*ratio < percentage; - end - elsif want.match /(.*)\s+\+\-(.+)$/ #absolute range: 100 +-5 - margin = $2.to_f - from = $1.to_f-margin - to = $1.to_f+margin - return got.to_f >= from && got.to_f <= to - elsif want =~ /^\/(.*)\/$/ #regex: /a,b,.*/ - return got =~ /#{$1}/ - else - return false - end - end - - def self.match_location got, want - match( got[0], "#{want.lat} ~0.0025%" ) && - match( got[1], "#{want.lon} ~0.0025%" ) - end - -end diff --git a/features/support/hash.js b/features/support/hash.js new file mode 100644 index 000000000..6ab44ad20 --- /dev/null +++ b/features/support/hash.js @@ -0,0 +1,37 @@ +var fs = require('fs'); +var path = require('path'); +var crypto = require('crypto'); +var d3 = require('d3-queue'); + +module.exports = function () { + this.hashOfFiles = (paths, cb) => { + paths = Array.isArray(paths) ? paths : [paths]; + var shasum = crypto.createHash('sha1'); + + var q = d3.queue(1); + + var addFile = (path, cb) => { + fs.readFile(path, (err, data) => { + shasum.update(data); + cb(err); + }); + }; + + paths.forEach(path => { q.defer(addFile, path); }); + + q.awaitAll(err => { + if (err) throw new Error('*** Error reading files:', err); + cb(shasum.digest('hex')); + }); + }; + + this.hashProfile = (cb) => { + this.hashOfFiles(path.resolve(this.PROFILES_PATH, this.profile + '.lua'), cb); + }; + + this.hashString = (str) => { + return crypto.createHash('sha1').update(str).digest('hex'); + }; + + return this; +}; diff --git a/features/support/hash.rb b/features/support/hash.rb deleted file mode 100644 index cfc1fe758..000000000 --- a/features/support/hash.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'digest/sha1' - -bin_extract_hash = nil -profile_hashes = nil - -def hash_of_files paths - paths = [paths] unless paths.is_a? Array - hash = Digest::SHA1.new - for path in paths do - open(path,'rb') do |io| - while !io.eof - buf = io.readpartial 1024 - hash.update buf - end - end - end - return hash.hexdigest -end - - -def profile_hash - profile_hashes ||= {} - profile_hashes[@profile] ||= hash_of_files "#{PROFILES_PATH}/#{@profile}.lua" -end - -def osm_hash - @osm_hash ||= Digest::SHA1.hexdigest osm_str -end - -def lua_lib_hash - @lua_lib_hash ||= hash_of_files Dir.glob("../profiles/lib/*.lua") -end - -def bin_extract_hash - @bin_extract_hash ||= hash_of_files "#{BIN_PATH}/osrm-extract#{EXE}" - @bin_extract_hash -end - -def bin_prepare_hash - @bin_prepare_hash ||= hash_of_files "#{BIN_PATH}/osrm-contract#{EXE}" -end - -def bin_routed_hash - @bin_routed_hash ||= hash_of_files "#{BIN_PATH}/osrm-routed#{EXE}" -end - -# combine state of data, profile and binaries into a hashes that identifies -# the exact test situation at different stages, so we can later skip steps when possible. -def fingerprint_osm - @fingerprint_osm ||= Digest::SHA1.hexdigest "#{osm_hash}" -end - -def fingerprint_extract - @fingerprint_extract ||= Digest::SHA1.hexdigest "#{profile_hash}-#{lua_lib_hash}-#{bin_extract_hash}" -end - -def fingerprint_prepare - @fingerprint_prepare ||= Digest::SHA1.hexdigest "#{bin_prepare_hash}" -end - -def fingerprint_route - @fingerprint_route ||= Digest::SHA1.hexdigest "#{bin_routed_hash}" -end \ No newline at end of file diff --git a/features/support/hooks.js b/features/support/hooks.js new file mode 100644 index 000000000..4f03e2d88 --- /dev/null +++ b/features/support/hooks.js @@ -0,0 +1,37 @@ +var util = require('util'); + +module.exports = function () { + this.BeforeFeatures((features, callback) => { + this.pid = null; + this.initializeEnv(() => { + this.initializeOptions(callback); + }); + }); + + this.Before((scenario, callback) => { + this.scenarioTitle = scenario.getName(); + + this.loadMethod = this.DEFAULT_LOAD_METHOD; + this.queryParams = []; + var d = new Date(); + this.scenarioTime = util.format('%d-%d-%dT%s:%s:%sZ', d.getFullYear(), d.getMonth()+1, d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds()); + this.resetData(); + this.hasLoggedPreprocessInfo = false; + this.hasLoggedScenarioInfo = false; + this.setGridSize(this.DEFAULT_GRID_SIZE); + this.setOrigin(this.DEFAULT_ORIGIN); + callback(); + }); + + this.After((scenario, callback) => { + this.setExtractArgs(''); + this.setContractArgs(''); + if (this.loadMethod === 'directly' && !!this.OSRMLoader.loader) this.OSRMLoader.shutdown(callback); + else callback(); + }); + + this.Around('@stress', (scenario, callback) => { + // TODO implement stress timeout? Around support is being dropped in cucumber-js anyway + callback(); + }); +}; diff --git a/features/support/hooks.rb b/features/support/hooks.rb deleted file mode 100644 index 6af9a8ffa..000000000 --- a/features/support/hooks.rb +++ /dev/null @@ -1,35 +0,0 @@ - -STRESS_TIMEOUT = 300 - - -Before do |scenario| - - # fetch scenario and feature name, so we can use it in log files if needed - case scenario - when Cucumber::RunningTestCase::Scenario - @feature_name = scenario.feature.name - @scenario_title = scenario.name - when Cucumber::RunningTestCase::ExampleRow - @feature_name = scenario.scenario_outline.feature.name - @scenario_title = scenario.scenario_outline.name - end - - @load_method = DEFAULT_LOAD_METHOD - @query_params = [] - @scenario_time = Time.now.strftime("%Y-%m-%dT%H:%m:%SZ") - reset_data - @has_logged_preprocess_info = false - @has_logged_scenario_info = false - set_grid_size DEFAULT_GRID_SIZE - set_origin DEFAULT_ORIGIN - -end - -Around('@stress') do |scenario, block| - Timeout.timeout(STRESS_TIMEOUT) do - block.call - end -end - -After do -end diff --git a/features/support/http.js b/features/support/http.js new file mode 100644 index 000000000..d4d8486d9 --- /dev/null +++ b/features/support/http.js @@ -0,0 +1,47 @@ +var Timeout = require('node-timeout'); +var request = require('request'); + +module.exports = function () { + // Converts an array [["param","val1"], ["param","val2"]] into param=val1¶m=val2 + this.paramsToString = (params) => { + var kvPairs = params.map((kv) => kv[0].toString() + '=' + kv[1].toString()); + var url = kvPairs.length ? kvPairs.join('&') : ''; + return url.trim(); + }; + + this.sendRequest = (baseUri, parameters, callback) => { + var limit = Timeout(this.OSRM_TIMEOUT, { err: { statusCode: 408 } }); + + var runRequest = (cb) => { + var params = this.paramsToString(parameters); + + this.query = baseUri + (params.length ? '?' + params : ''); + + var options = this.httpMethod === 'POST' ? { + method: 'POST', + body: params, + url: baseUri + } : this.query; + + request(options, (err, res, body) => { + if (err && err.code === 'ECONNREFUSED') { + throw new Error('*** osrm-routed is not running.'); + } else if (err && err.statusCode === 408) { + throw new Error(); + } + + return cb(err, res, body); + }); + }; + + runRequest(limit((err, res, body) => { + if (err) { + if (err.statusCode === 408) + return callback(this.RoutedError('*** osrm-routed did not respond')); + else if (err.code === 'ECONNREFUSED') + return callback(this.RoutedError('*** osrm-routed is not running')); + } + return callback(err, res, body); + })); + }; +}; diff --git a/features/support/http.rb b/features/support/http.rb deleted file mode 100644 index 2f066e689..000000000 --- a/features/support/http.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'net/http' - -# Converts an array [["param","val1"], ["param","val2"]] into param=val1¶m=val2 -def params_to_string params - kv_pairs = params.map { |kv| kv[0].to_s + "=" + kv[1].to_s } - url = kv_pairs.size > 0 ? kv_pairs.join("&") : "" - return url -end - -def send_request base_uri, parameters - Timeout.timeout(OSRM_TIMEOUT) do - uri_string = base_uri - params = params_to_string(parameters) - if not params.eql? "" - uri_string = uri_string + "?" + params - end - uri = URI.parse(uri_string) - @query = uri.to_s - if @http_method.eql? "POST" - Net::HTTP.start(uri.hostname, uri.port) do |http| - req = Net::HTTP::Post.new(uri.path) - req.body = params_to_string parameters - response = http.request(req) - end - else - response = Net::HTTP.get_response uri - end - end -rescue Errno::ECONNREFUSED => e - raise "*** osrm-routed is not running." -rescue Timeout::Error - raise "*** osrm-routed did not respond." -end diff --git a/features/support/launch.js b/features/support/launch.js new file mode 100644 index 000000000..ee335e301 --- /dev/null +++ b/features/support/launch.js @@ -0,0 +1,5 @@ +var launchClasses = require('./launch_classes'); + +module.exports = function () { + this._OSRMLoader = () => new (launchClasses._OSRMLoader.bind(launchClasses._OSRMLoader, this))(); +}; diff --git a/features/support/launch.rb b/features/support/launch.rb deleted file mode 100644 index 919078501..000000000 --- a/features/support/launch.rb +++ /dev/null @@ -1,137 +0,0 @@ -require 'socket' -require 'open3' -require 'json' - -# Only one isntance of osrm-routed is ever launched, to avoid collisions. -# The default is to keep osrm-routed running and load data with datastore. -# however, osrm-routed it shut down and relaunched for each scenario thats -# loads data directly. -class OSRMLoader - - class OSRMBaseLoader - @@pid = nil - - def launch - Timeout.timeout(LAUNCH_TIMEOUT) do - osrm_up - wait_for_connection - end - rescue Timeout::Error - raise RoutedError.new "Launching osrm-routed timed out." - end - - def shutdown - Timeout.timeout(SHUTDOWN_TIMEOUT) do - osrm_down - end - rescue Timeout::Error - kill - raise RoutedError.new "Shutting down osrm-routed timed out." - end - - def osrm_up? - if @@pid - begin - if Process.waitpid(@@pid, Process::WNOHANG) then - false - else - true - end - rescue Errno::ESRCH, Errno::ECHILD - false - end - end - end - - def osrm_down - if @@pid - Process.kill TERMSIGNAL, @@pid - wait_for_shutdown - @@pid = nil - end - end - - def kill - if @@pid - Process.kill 'KILL', @@pid - end - end - - def wait_for_connection - while true - begin - socket = TCPSocket.new('127.0.0.1', OSRM_PORT) - return - rescue Errno::ECONNREFUSED - sleep 0.1 - end - end - end - - def wait_for_shutdown - while osrm_up? - sleep 0.01 - end - end - end - - # looading data directly when lauching osrm-routed: - # under this scheme, osmr-routed is launched and shutdown for each scenario, - # and osrm-datastore is not used - class OSRMDirectLoader < OSRMBaseLoader - def load world, input_file, &block - @world = world - @input_file = input_file - Dir.chdir TEST_FOLDER do - shutdown - launch - yield - shutdown - end - end - - def osrm_up - return if @@pid - @@pid = Process.spawn("#{LOAD_LIBRARIES}#{BIN_PATH}/osrm-routed #{@input_file} --port #{OSRM_PORT}",:out=>OSRM_ROUTED_LOG_FILE, :err=>OSRM_ROUTED_LOG_FILE) - Process.detach(@@pid) # avoid zombie processes - end - - end - - # looading data with osrm-datastore: - # under this scheme, osmr-routed is launched once and kept running for all scenarios, - # and osrm-datastore is used to load data for each scenario - class OSRMDatastoreLoader < OSRMBaseLoader - def load world, input_file, &block - @world = world - @input_file = input_file - Dir.chdir TEST_FOLDER do - load_data - launch unless @@pid - yield - end - end - - def load_data - run_bin "osrm-datastore", @input_file - end - - def osrm_up - return if osrm_up? - @@pid = Process.spawn("#{LOAD_LIBRARIES}#{BIN_PATH}/osrm-routed --shared-memory=1 --port #{OSRM_PORT}",:out=>OSRM_ROUTED_LOG_FILE, :err=>OSRM_ROUTED_LOG_FILE) - Process.detach(@@pid) # avoid zombie processes - end - end - - def self.load world, input_file, &block - method = world.instance_variable_get "@load_method" - if method == 'datastore' - OSRMDatastoreLoader.new.load world, input_file, &block - elsif method == 'directly' - OSRMDirectLoader.new.load world, input_file, &block - else - raise "*** Unknown load method '#{method}'" - end - end - -end diff --git a/features/support/launch_classes.js b/features/support/launch_classes.js new file mode 100644 index 000000000..c859537ac --- /dev/null +++ b/features/support/launch_classes.js @@ -0,0 +1,163 @@ +'use strict'; + +var fs = require('fs'); +var net = require('net'); +var spawn = require('child_process').spawn; +var util = require('util'); +var Timeout = require('node-timeout'); + +var OSRMBaseLoader = class { + constructor (scope) { + this.scope = scope; + } + + launch (callback) { + var limit = Timeout(this.scope.LAUNCH_TIMEOUT, { err: this.scope.RoutedError('Launching osrm-routed timed out.') }); + + var runLaunch = (cb) => { + this.osrmUp(() => { + this.waitForConnection(cb); + }); + }; + + runLaunch(limit((e) => { if (e) callback(e); callback(); })); + } + + shutdown (callback) { + var limit = Timeout(this.scope.SHUTDOWN_TIMEOUT, { err: this.scope.RoutedError('Shutting down osrm-routed timed out.')}); + + var runShutdown = (cb) => { + this.osrmDown(cb); + }; + + runShutdown(limit((e) => { if (e) callback(e); callback(); })); + } + + osrmIsRunning () { + return !!this.scope.pid && this.child && !this.child.killed; + } + + osrmDown (callback) { + if (this.scope.pid) { + process.kill(this.scope.pid, this.scope.TERMSIGNAL); + this.waitForShutdown(callback); + this.scope.pid = null; + } else callback(true); + } + + waitForConnection (callback) { + net.connect({ + port: this.scope.OSRM_PORT, + host: '127.0.0.1' + }) + .on('connect', () => { + callback(); + }) + .on('error', (e) => { + setTimeout(() => { + callback(e); + }, 100); + }); + } + + waitForShutdown (callback) { + var check = () => { + if (!this.osrmIsRunning()) callback(); + }; + setTimeout(check, 100); + } +}; + +var OSRMDirectLoader = class extends OSRMBaseLoader { + constructor (scope) { + super(scope); + } + + load (inputFile, callback) { + this.inputFile = inputFile; + this.shutdown(() => { + this.launch(callback); + }); + } + + osrmUp (callback) { + if (this.scope.pid) return callback(); + var writeToLog = (data) => { + fs.appendFile(this.scope.OSRM_ROUTED_LOG_FILE, data, (err) => { if (err) throw err; }); + }; + + var child = spawn(util.format('%s%s/osrm-routed', this.scope.LOAD_LIBRARIES, this.scope.BIN_PATH), [this.inputFile, util.format('-p%d', this.scope.OSRM_PORT)], {detached: true}); + this.scope.pid = child.pid; + child.stdout.on('data', writeToLog); + child.stderr.on('data', writeToLog); + + callback(); + } +}; + +var OSRMDatastoreLoader = class extends OSRMBaseLoader { + constructor (scope) { + super(scope); + } + + load (inputFile, callback) { + this.inputFile = inputFile; + this.loadData((err) => { + if (err) return callback(err); + if (!this.scope.pid) return this.launch(callback); + else callback(); + }); + } + + loadData (callback) { + this.scope.runBin('osrm-datastore', this.inputFile, (err) => { + if (err) return callback(new this.LaunchError(this.exitCode, 'datastore', err)); + callback(); + }); + } + + osrmUp (callback) { + if (this.scope.pid) return callback(); + var writeToLog = (data) => { + fs.appendFile(this.scope.OSRM_ROUTED_LOG_FILE, data, (err) => { if (err) throw err; }); + }; + + var child = spawn(util.format('%s%s/osrm-routed', this.scope.LOAD_LIBRARIES, this.scope.BIN_PATH), ['--shared-memory=1', util.format('-p%d', this.scope.OSRM_PORT)], {detached: true}); + this.child = child; + this.scope.pid = child.pid; + child.stdout.on('data', writeToLog); + child.stderr.on('data', writeToLog); + + callback(); + } +}; + +module.exports = { + _OSRMLoader: class { + constructor (scope) { + this.scope = scope; + this.loader = null; + } + + load (inputFile, callback) { + var method = this.scope.loadMethod; + if (method === 'datastore') { + this.loader = new OSRMDatastoreLoader(this.scope); + this.loader.load(inputFile, callback); + } else if (method === 'directly') { + this.loader = new OSRMDirectLoader(this.scope); + this.loader.load(inputFile, callback); + } else { + throw new Error('*** Unknown load method ' + method); + } + } + + shutdown (callback) { + this.loader.shutdown(callback); + } + + up () { + return this.loader ? this.loader.osrmIsRunning() : false; + } + } +}; diff --git a/features/support/log.js b/features/support/log.js new file mode 100644 index 000000000..c428cb9d9 --- /dev/null +++ b/features/support/log.js @@ -0,0 +1,90 @@ +var fs = require('fs'); + +module.exports = function () { + this.clearLogFiles = (callback) => { + // emptying existing files, rather than deleting and writing new ones makes it + // easier to use tail -f from the command line + fs.writeFile(this.OSRM_ROUTED_LOG_FILE, '', err => { + if (err) throw err; + fs.writeFile(this.PREPROCESS_LOG_FILE, '', err => { + if (err) throw err; + fs.writeFile(this.LOG_FILE, '', err => { + if (err) throw err; + callback(); + }); + }); + }); + }; + + var log = this.log = (s, type) => { + s = s || ''; + type = type || null; + var file = type === 'preprocess' ? this.PREPROCESS_LOG_FILE : this.LOG_FILE; + fs.appendFile(file, s + '\n', err => { + if (err) throw err; + }); + }; + + this.logScenarioFailInfo = () => { + if (this.hasLoggedScenarioInfo) return; + + log('========================================='); + log('Failed scenario: ' + this.scenarioTitle); + log('Time: ' + this.scenarioTime); + log('Fingerprint osm stage: ' + this.osmData.fingerprintOSM); + log('Fingerprint extract stage: ' + this.fingerprintExtract); + log('Fingerprint contract stage: ' + this.fingerprintContract); + log('Fingerprint route stage: ' + this.fingerprintRoute); + log('Profile: ' + this.profile); + log(); + log('```xml'); // so output can be posted directly to github comment fields + log(this.osmData.str.trim()); + log('```'); + log(); + log(); + + this.hasLoggedScenarioInfo = true; + }; + + this.logFail = (expected, got, attempts) => { + this.logScenarioFailInfo(); + log('== '); + log('Expected: ' + JSON.stringify(expected)); + log('Got: ' + JSON.stringify(got)); + log(); + ['route','forw','backw'].forEach((direction) => { + if (attempts[direction]) { + log('Direction: ' + direction); + log('Query: ' + attempts[direction].query); + log('Response: ' + attempts[direction].response.body); + log(); + } + }); + }; + + this.logPreprocessInfo = () => { + if (this.hasLoggedPreprocessInfo) return; + log('=========================================', 'preprocess'); + log('Preprocessing data for scenario: ' + this.scenarioTitle, 'preprocess'); + log('Time: ' + this.scenarioTime, 'preprocess'); + log('', 'preprocess'); + log('== OSM data:', 'preprocess'); + log('```xml', 'preprocess'); // so output can be posted directly to github comment fields + log(this.osmData.str, 'preprocess'); + log('```', 'preprocess'); + log('', 'preprocess'); + log('== Profile:', 'preprocess'); + log(this.profile, 'preprocess'); + log('', 'preprocess'); + this.hasLoggedPreprocessInfo = true; + }; + + this.logPreprocess = (str) => { + this.logPreprocessInfo(); + log(str, 'preprocess'); + }; + + this.logPreprocessDone = () => { + log('Done with preprocessing at ' + new Date(), 'preprocess'); + }; +}; diff --git a/features/support/log.rb b/features/support/log.rb deleted file mode 100644 index 8e6f9c146..000000000 --- a/features/support/log.rb +++ /dev/null @@ -1,88 +0,0 @@ -# logging - -PREPROCESS_LOG_FILE = 'preprocessing.log' -LOG_FILE = 'fail.log' - - -def clear_log_files - Dir.chdir TEST_FOLDER do - # emptying existing files, rather than deleting and writing new ones makes it - # easier to use tail -f from the command line - `echo '' > #{OSRM_ROUTED_LOG_FILE}` - `echo '' > #{PREPROCESS_LOG_FILE}` - `echo '' > #{LOG_FILE}` - end -end - -def log s='', type=nil - if type == :preprocess - file = PREPROCESS_LOG_FILE - else - file = LOG_FILE - end - File.open(file, 'a') {|f| f.write("#{s}\n") } -end - - -def log_scenario_fail_info - return if @has_logged_scenario_info - log "=========================================" - log "Failed scenario: #{@scenario_title}" - log "Time: #{@scenario_time}" - log "Fingerprint osm stage: #{@fingerprint_osm}" - log "Fingerprint extract stage: #{@fingerprint_extract}" - log "Fingerprint prepare stage: #{@fingerprint_prepare}" - log "Fingerprint route stage: #{@fingerprint_route}" - log "Profile: #{@profile}" - log - log '```xml' #so output can be posted directly to github comment fields - log osm_str.strip - log '```' - log - log - @has_logged_scenario_info = true -end - -def log_fail expected,got,attempts - return - log_scenario_fail_info - log "== " - log "Expected: #{expected}" - log "Got: #{got}" - log - ['route','forw','backw'].each do |direction| - if attempts[direction] - attempts[direction] - log "Direction: #{direction}" - log "Query: #{attempts[direction][:query]}" - log "Response: #{attempts[direction][:response].body}" - log - end - end -end - - -def log_preprocess_info - return if @has_logged_preprocess_info - log "=========================================", :preprocess - log "Preprocessing data for scenario: #{@scenario_title}", :preprocess - log "Time: #{@scenario_time}", :preprocess - log '', :preprocess - log "== OSM data:", :preprocess - log '```xml', :preprocess #so output can be posted directly to github comment fields - log osm_str, :preprocess - log '```', :preprocess - log '', :preprocess - log "== Profile:", :preprocess - log @profile, :preprocess - log '', :preprocess - @has_logged_preprocess_info = true -end - -def log_preprocess str - log_preprocess_info - log str, :preprocess -end - -def log_preprocess_done -end diff --git a/features/support/osm_parser.rb b/features/support/osm_parser.rb deleted file mode 100644 index 1da7c7318..000000000 --- a/features/support/osm_parser.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'OSM/StreamParser' - -locations = nil - -class OSMTestParserCallbacks < OSM::Callbacks - locations = nil - - def self.locations - if locations - locations - else - #parse the test file, so we can later reference nodes and ways by name in tests - locations = {} - file = 'test/data/test.osm' - callbacks = OSMTestParserCallbacks.new - parser = OSM::StreamParser.new(:filename => file, :callbacks => callbacks) - parser.parse - puts locations - end - end - - def node(node) - locations[node.name] = [node.lat,node.lon] - end -end \ No newline at end of file diff --git a/features/support/osmlib.rb b/features/support/osmlib.rb deleted file mode 100644 index 6b03dfa4a..000000000 --- a/features/support/osmlib.rb +++ /dev/null @@ -1,14 +0,0 @@ -#monkey-patch osmlib to fix a bug - -module OSM - class Way - def to_xml(xml) - xml.way(attributes) do - nodes.each do |node| - xml.nd(:ref => node) - end - tags.to_xml(xml) - end - end - end -end diff --git a/features/support/route.js b/features/support/route.js new file mode 100644 index 000000000..b14f36a47 --- /dev/null +++ b/features/support/route.js @@ -0,0 +1,160 @@ +var Timeout = require('node-timeout'); +var request = require('request'); + +module.exports = function () { + this.requestPath = (service, params, callback) => { + var uri = [this.HOST, service].join('/'); + return this.sendRequest(uri, params, callback); + }; + + this.requestUrl = (path, callback) => { + var uri = this.query = [this.HOST, path].join('/'), + limit = Timeout(this.OSRM_TIMEOUT, { err: { statusCode: 408 } }); + + function runRequest (cb) { + request(uri, cb); + } + + runRequest(limit((err, res, body) => { + if (err) { + if (err.statusCode === 408) return callback(this.RoutedError('*** osrm-routed did not respond')); + else if (err.code === 'ECONNREFUSED') + return callback(this.RoutedError('*** osrm-routed is not running')); + } else + return callback(err, res, body); + })); + }; + + // Overwrites the default values in defaults + // e.g. [[a, 1], [b, 2]], [[a, 5], [d, 10]] => [[a, 5], [b, 2], [d, 10]] + this.overwriteParams = (defaults, other) => { + var merged = {}; + var overwrite = (o) => { + merged[o[0]] = o[1]; + }; + + defaults.forEach(overwrite); + other.forEach(overwrite); + + return Object.keys(merged).map((key) => [key, merged[key]]); + }; + + var encodeWaypoints = (waypoints) => { + return waypoints.map(w => ['loc', [w.lat, w.lon].join(',')]); + }; + + this.requestRoute = (waypoints, bearings, userParams, callback) => { + if (bearings.length && bearings.length !== waypoints.length) throw new Error('*** number of bearings does not equal the number of waypoints'); + + var defaults = [['output','json'], ['instructions','true'], ['alt',false]], + params = this.overwriteParams(defaults, userParams), + encodedWaypoints = encodeWaypoints(waypoints); + if (bearings.length) { + var encodedBearings = bearings.map(b => ['b', b.toString()]); + params = Array.prototype.concat.apply(params, encodedWaypoints.map((o, i) => [o, encodedBearings[i]])); + } else { + params = params.concat(encodedWaypoints); + } + + return this.requestPath('viaroute', params, callback); + }; + + this.requestNearest = (node, userParams, callback) => { + var defaults = [['output', 'json']], + params = this.overwriteParams(defaults, userParams); + params.push(['loc', [node.lat, node.lon].join(',')]); + + return this.requestPath('nearest', params, callback); + }; + + this.requestTable = (waypoints, userParams, callback) => { + var defaults = [['output', 'json']], + params = this.overwriteParams(defaults, userParams); + params = params.concat(waypoints.map(w => [w.type, [w.coord.lat, w.coord.lon].join(',')])); + + return this.requestPath('table', params, callback); + }; + + this.requestTrip = (waypoints, userParams, callback) => { + var defaults = [['output', 'json']], + params = this.overwriteParams(defaults, userParams); + params = params.concat(encodeWaypoints(waypoints)); + + return this.requestPath('trip', params, callback); + }; + + this.requestMatching = (waypoints, timestamps, userParams, callback) => { + var defaults = [['output', 'json']], + params = this.overwriteParams(defaults, userParams); + var encodedWaypoints = encodeWaypoints(waypoints); + if (timestamps.length) { + var encodedTimestamps = timestamps.map(t => ['t', t.toString()]); + params = Array.prototype.concat.apply(params, encodedWaypoints.map((o, i) => [o, encodedTimestamps[i]])); + } else { + params = params.concat(encodedWaypoints); + } + + return this.requestPath('match', params, callback); + }; + + this.extractInstructionList = (instructions, index, postfix) => { + postfix = postfix || null; + if (instructions) { + return instructions.filter(r => r[0].toString() !== this.DESTINATION_REACHED.toString()) + .map(r => r[index]) + .map(r => (isNaN(parseInt(r)) && (!r || r == '')) ? '""' : '' + r + (postfix || '')) + .join(','); + } + }; + + this.wayList = (instructions) => { + return this.extractInstructionList(instructions, 1); + }; + + this.compassList = (instructions) => { + return this.extractInstructionList(instructions, 6); + }; + + this.bearingList = (instructions) => { + return this.extractInstructionList(instructions, 7); + }; + + this.turnList = (instructions) => { + var types = { + '0': 'none', + '1': 'straight', + '2': 'slight_right', + '3': 'right', + '4': 'sharp_right', + '5': 'u_turn', + '6': 'sharp_left', + '7': 'left', + '8': 'slight_left', + '9': 'via', + '10': 'head', + '11': 'enter_roundabout', + '12': 'leave_roundabout', + '13': 'stay_roundabout', + '14': 'start_end_of_street', + '15': 'destination', + '16': 'name_changes', + '17': 'enter_contraflow', + '18': 'leave_contraflow' + }; + + // replace instructions codes with strings, e.g. '11-3' gets converted to 'enter_roundabout-3' + return instructions ? instructions.map(r => r[0].toString().replace(/^(\d*)/, (match, num) => types[num])).join(',') : instructions; + }; + + this.modeList = (instructions) => { + return this.extractInstructionList(instructions, 8); + }; + + this.timeList = (instructions) => { + return this.extractInstructionList(instructions, 4, 's'); + }; + + this.distanceList = (instructions) => { + return this.extractInstructionList(instructions, 2, 'm'); + }; +}; diff --git a/features/support/route.rb b/features/support/route.rb deleted file mode 100644 index a0deacb8f..000000000 --- a/features/support/route.rb +++ /dev/null @@ -1,182 +0,0 @@ -require 'net/http' - -HOST = "http://127.0.0.1:#{OSRM_PORT}" -DESTINATION_REACHED = 15 #OSRM instruction code - -def request_path service, params - uri = "#{HOST}/" + service - response = send_request uri, params - return response -end - -def request_url path - uri = URI.parse"#{HOST}/#{path}" - @query = uri.to_s - Timeout.timeout(OSRM_TIMEOUT) do - Net::HTTP.get_response uri - end -rescue Errno::ECONNREFUSED => e - raise "*** osrm-routed is not running." -rescue Timeout::Error - raise "*** osrm-routed did not respond." -end - -# Overwriters the default values in defaults. -# e.g. [[a, 1], [b, 2]], [[a, 5], [d, 10]] => [[a, 5], [b, 2], [d, 10]] -def overwrite_params defaults, other - merged = [] - defaults.each do |k,v| - idx = other.index { |p| p[0] == k } - if idx == nil then - merged << [k, v] - else - merged << [k, other[idx][1]] - end - end - other.each do |k,v| - if merged.index { |pair| pair[0] == k} == nil then - merged << [k, v] - end - end - - return merged -end - -def request_route waypoints, bearings, user_params - raise "*** number of bearings does not equal the number of waypoints" unless bearings.size == 0 || bearings.size == waypoints.size - - defaults = [['output','json'], ['instructions',true], ['alt',false]] - params = overwrite_params defaults, user_params - encoded_waypoint = waypoints.map{ |w| ["loc","#{w.lat},#{w.lon}"] } - if bearings.size > 0 - encoded_bearings = bearings.map { |b| ["b", b.to_s]} - parasm = params.concat encoded_waypoint.zip(encoded_bearings).flatten! 1 - else - params = params.concat encoded_waypoint - end - - return request_path "viaroute", params -end - -def request_nearest node, user_params - defaults = [['output', 'json']] - params = overwrite_params defaults, user_params - params << ["loc", "#{node.lat},#{node.lon}"] - - return request_path "nearest", params -end - -def request_table waypoints, user_params - defaults = [['output', 'json']] - params = overwrite_params defaults, user_params - params = params.concat waypoints.map{ |w| [w[:type],"#{w[:coord].lat},#{w[:coord].lon}"] } - - return request_path "table", params -end - -def request_trip waypoints, user_params - defaults = [['output', 'json']] - params = overwrite_params defaults, user_params - params = params.concat waypoints.map{ |w| ["loc","#{w.lat},#{w.lon}"] } - - return request_path "trip", params -end - -def request_matching waypoints, timestamps, user_params - defaults = [['output', 'json']] - params = overwrite_params defaults, user_params - encoded_waypoint = waypoints.map{ |w| ["loc","#{w.lat},#{w.lon}"] } - if timestamps.size > 0 - encoded_timestamps = timestamps.map { |t| ["t", t.to_s]} - parasm = params.concat encoded_waypoint.zip(encoded_timestamps).flatten! 1 - else - params = params.concat encoded_waypoint - end - - return request_path "match", params -end - -def got_route? response - if response.code == "200" && !response.body.empty? - json = JSON.parse response.body - if json['status'] == 200 - return way_list( json['route_instructions']).empty? == false - end - end - return false -end - -def route_status response - if response.code == "200" && !response.body.empty? - json = JSON.parse response.body - return json['status'] - else - "HTTP #{response.code}" - end -end - -def extract_instruction_list instructions, index, postfix=nil - if instructions - instructions.reject { |r| r[0].to_s=="#{DESTINATION_REACHED}" }. - map { |r| r[index] }. - map { |r| (r=="" || r==nil) ? '""' : "#{r}#{postfix}" }. - join(',') - end -end - -def way_list instructions - extract_instruction_list instructions, 1 -end - -def compass_list instructions - extract_instruction_list instructions, 6 -end - -def bearing_list instructions - extract_instruction_list instructions, 7 -end - -def turn_list instructions - if instructions - types = { - 0 => :none, - 1 => :straight, - 2 => :slight_right, - 3 => :right, - 4 => :sharp_right, - 5 => :u_turn, - 6 => :sharp_left, - 7 => :left, - 8 => :slight_left, - 9 => :via, - 10 => :head, - 11 => :enter_roundabout, - 12 => :leave_roundabout, - 13 => :stay_roundabout, - 14 => :start_end_of_street, - 15 => :destination, - 16 => :name_changes, - 17 => :enter_contraflow, - 18 => :leave_contraflow - } - # replace instructions codes with strings - # "11-3" (enter roundabout and leave a 3rd exit) gets converted to "enter_roundabout-3" - instructions.map do |r| - r[0].to_s.gsub(/^\d*/) do |match| - types[match.to_i].to_s - end - end.join(',') - end -end - -def mode_list instructions - extract_instruction_list instructions, 8 -end - -def time_list instructions - extract_instruction_list instructions, 4, "s" -end - -def distance_list instructions - extract_instruction_list instructions, 2, "m" -end diff --git a/features/support/run.js b/features/support/run.js new file mode 100644 index 000000000..cede245b6 --- /dev/null +++ b/features/support/run.js @@ -0,0 +1,40 @@ +var fs = require('fs'); +var util = require('util'); +var exec = require('child_process').exec; + +module.exports = function () { + this.runBin = (bin, options, callback) => { + var opts = options.slice(); + + if (opts.match('{osm_base}')) { + if (!this.osmData.osmFile) throw new Error('*** {osm_base} is missing'); + opts = opts.replace('{osm_base}', this.osmData.osmFile); + } + + if (opts.match('{extracted_base}')) { + if (!this.osmData.extractedFile) throw new Error('*** {extracted_base} is missing'); + opts = opts.replace('{extracted_base}', this.osmData.extractedFile); + } + + if (opts.match('{contracted_base}')) { + if (!this.osmData.contractedFile) throw new Error('*** {contracted_base} is missing'); + opts = opts.replace('{contracted_base}', this.osmData.contractedFile); + } + + if (opts.match('{profile}')) { + opts = opts.replace('{profile}', [this.PROFILES_PATH, this.profile + '.lua'].join('/')); + } + + var cmd = util.format('%s%s%s/%s%s%s %s 2>%s', this.QQ, this.LOAD_LIBRARIES, this.BIN_PATH, bin, this.EXE, this.QQ, opts, this.ERROR_LOG_FILE); + process.chdir(this.TEST_FOLDER); + exec(cmd, (err, stdout, stderr) => { + this.stdout = stdout.toString(); + fs.readFile(this.ERROR_LOG_FILE, (e, data) => { + this.stderr = data ? data.toString() : ''; + this.exitCode = err && err.code || 0; + process.chdir('../'); + callback(err, stdout, stderr); + }); + }); + }; +}; diff --git a/features/support/run.rb b/features/support/run.rb deleted file mode 100644 index 3ddfd5448..000000000 --- a/features/support/run.rb +++ /dev/null @@ -1,28 +0,0 @@ -def run_bin bin, options - Dir.chdir TEST_FOLDER do - opt = options.dup - - if opt.include? '{osm_base}' - raise "*** {osm_base} is missing" unless osm_file - opt.gsub! "{osm_base}", "#{osm_file}" - end - - if opt.include? '{extracted_base}' - raise "*** {extracted_base} is missing" unless extracted_file - opt.gsub! "{extracted_base}", "#{extracted_file}" - end - - if opt.include? '{contracted_base}' - raise "*** {contracted_base} is missing" unless contracted_file - opt.gsub! "{contracted_base}", "#{contracted_file}" - end - if opt.include? '{profile}' - opt.gsub! "{profile}", "#{PROFILES_PATH}/#{@profile}.lua" - end - - cmd = "#{QQ}#{LOAD_LIBRARIES}#{BIN_PATH}/#{bin}#{EXE}#{QQ} #{opt} 2>error.log" - @stdout = `#{cmd}` - @stderr = File.read 'error.log' - @exit_code = $?.exitstatus - end -end diff --git a/features/support/shared_steps.js b/features/support/shared_steps.js new file mode 100644 index 000000000..d7e244cf5 --- /dev/null +++ b/features/support/shared_steps.js @@ -0,0 +1,203 @@ +var util = require('util'); +var assert = require('assert'); + +module.exports = function () { + this.ShouldGetAResponse = () => { + assert.equal(this.response.statusCode, 200); + assert.ok(this.response.body); + assert.ok(this.response.body.length); + }; + + this.ShouldBeValidJSON = (callback) => { + try { + this.json = JSON.parse(this.response.body); + callback(); + } catch (e) { + callback(e); + } + }; + + this.ShouldBeWellFormed = () => { + assert.equal(typeof this.json.status, 'number'); + }; + + this.WhenIRouteIShouldGet = (table, callback) => { + this.reprocessAndLoadData(() => { + var headers = new Set(table.raw()[0]); + + var requestRow = (row, ri, cb) => { + var got, + json; + + var afterRequest = (err, res, body) => { + if (err) return cb(err); + if (body && body.length) { + var instructions, bearings, compasses, turns, modes, times, distances; + + json = JSON.parse(body); + + var hasRoute = json.status === 200; + + if (hasRoute) { + instructions = this.wayList(json.route_instructions); + bearings = this.bearingList(json.route_instructions); + compasses = this.compassList(json.route_instructions); + turns = this.turnList(json.route_instructions); + modes = this.modeList(json.route_instructions); + times = this.timeList(json.route_instructions); + distances = this.distanceList(json.route_instructions); + } + + if (headers.has('status')) { + got.status = json.status.toString(); + } + + if (headers.has('message')) { + got.message = json.status_message; + } + + if (headers.has('#')) { + // comment column + got['#'] = row['#']; + } + + if (headers.has('start')) { + got.start = instructions ? json.route_summary.start_point : null; + } + + if (headers.has('end')) { + got.end = instructions ? json.route_summary.end_point : null; + } + + if (headers.has('geometry')) { + got.geometry = json.route_geometry; + } + + if (headers.has('route')) { + got.route = (instructions || '').trim(); + + if (headers.has('alternative')) { + got.alternative = json.found_alternative ? + this.wayList(json.alternative_instructions[0]) : ''; + } + + var distance = hasRoute && json.route_summary.total_distance, + time = hasRoute && json.route_summary.total_time; + + if (headers.has('distance')) { + if (row.distance.length) { + if (!row.distance.match(/\d+m/)) + throw new Error('*** Distance must be specified in meters. (ex: 250m)'); + got.distance = instructions ? util.format('%dm', distance) : ''; + } else { + got.distance = ''; + } + } + + if (headers.has('time')) { + if (!row.time.match(/\d+s/)) + throw new Error('*** Time must be specied in seconds. (ex: 60s)'); + got.time = instructions ? util.format('%ds', time) : ''; + } + + if (headers.has('speed')) { + if (row.speed !== '' && instructions) { + if (!row.speed.match(/\d+ km\/h/)) + throw new Error('*** Speed must be specied in km/h. (ex: 50 km/h)'); + var speed = time > 0 ? Math.round(3.6*distance/time) : null; + got.speed = util.format('%d km/h', speed); + } else { + got.speed = ''; + } + } + + var putValue = (key, value) => { + if (headers.has(key)) got[key] = instructions ? value : ''; + }; + + putValue('bearing', bearings); + putValue('compass', compasses); + putValue('turns', turns); + putValue('modes', modes); + putValue('times', times); + putValue('distances', distances); + } + + var ok = true; + + for (var key in row) { + if (this.FuzzyMatch.match(got[key], row[key])) { + got[key] = row[key]; + } else { + ok = false; + } + } + + if (!ok) { + this.logFail(row, got, { route: { query: this.query, response: res }}); + } + + cb(null, got); + } else { + cb(new Error('request failed to return valid body')); + } + }; + + if (headers.has('request')) { + got = { request: row.request }; + this.requestUrl(row.request, afterRequest); + } else { + var defaultParams = this.queryParams; + var userParams = []; + got = {}; + for (var k in row) { + var match = k.match(/param:(.*)/); + if (match) { + if (row[k] === '(nil)') { + userParams.push([match[1], null]); + } else if (row[k]) { + userParams.push([match[1], row[k]]); + } + got[k] = row[k]; + } + } + + var params = this.overwriteParams(defaultParams, userParams), + waypoints = [], + bearings = []; + + if (row.bearings) { + got.bearings = row.bearings; + bearings = row.bearings.split(' ').filter(b => !!b); + } + + if (row.from && row.to) { + var fromNode = this.findNodeByName(row.from); + if (!fromNode) throw new Error(util.format('*** unknown from-node "%s"'), row.from); + waypoints.push(fromNode); + + var toNode = this.findNodeByName(row.to); + if (!toNode) throw new Error(util.format('*** unknown to-node "%s"'), row.to); + waypoints.push(toNode); + + got.from = row.from; + got.to = row.to; + this.requestRoute(waypoints, bearings, params, afterRequest); + } else if (row.waypoints) { + row.waypoints.split(',').forEach((n) => { + var node = this.findNodeByName(n.trim()); + if (!node) throw new Error('*** unknown waypoint node "%s"', n.trim()); + waypoints.push(node); + }); + got.waypoints = row.waypoints; + this.requestRoute(waypoints, bearings, params, afterRequest); + } else { + throw new Error('*** no waypoints'); + } + } + }; + + this.processRowsAndDiff(table, requestRow, callback); + }); + }; +}; diff --git a/features/support/shortcuts.rb b/features/support/shortcuts.rb deleted file mode 100644 index 20bc3c0fe..000000000 --- a/features/support/shortcuts.rb +++ /dev/null @@ -1,3 +0,0 @@ -def shortcuts_hash - @shortcuts_hash ||= {} -end diff --git a/features/support/table_patch.js b/features/support/table_patch.js new file mode 100644 index 000000000..16ffebb8c --- /dev/null +++ b/features/support/table_patch.js @@ -0,0 +1,11 @@ +var DifferentError = require('./exception_classes').TableDiffError; + +module.exports = function () { + this.diffTables = (expected, actual, options, callback) => { + // this is a temp workaround while waiting for https://github.com/cucumber/cucumber-js/issues/534 + + var error = new DifferentError(expected, actual); + + return callback(error.string); + }; +}; diff --git a/features/testbot/load.feature b/features/testbot/load.feature index cf5e470bd..68ecf0aab 100644 --- a/features/testbot/load.feature +++ b/features/testbot/load.feature @@ -34,7 +34,7 @@ Feature: Ways of loading data | s | t | st | | t | s | st | - Scenario: Load data datstore - xy + Scenario: Load data datastore - xy Given data is loaded with datastore Given the node map | x | y | diff --git a/package.json b/package.json new file mode 100644 index 000000000..54c5a8208 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "osrm-backend-test-suite", + "version": "0.0.0", + "private": true, + "description": "The Open Source Routing Machine is a high performance routing engine written in C++11 designed to run on OpenStreetMap data.", + "dependencies": { + "cucumber": "^0.9.4", + "d3-queue": "^2.0.3", + "node-timeout": "0.0.4", + "request": "^2.69.0", + "xmlbuilder": "^4.2.1" + }, + "scripts": { + "lint": "eslint -c ./.eslintrc features/step_definitions/ features/support/", + "test": "npm run lint && ./node_modules/cucumber/bin/cucumber.js features/ -p verify", + "clean-test": "rm -rf test/cache", + "cucumber": "./node_modules/cucumber/bin/cucumber.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/Project-OSRM/osrm-backend.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/Project-OSRM/osrm-backend/issues" + }, + "homepage": "https://github.com/Project-OSRM/osrm-backend", + "engines": { + "node": ">=4.0.0" + }, + "devDependencies": { + "eslint": "^2.4.0" + } +} diff --git a/scripts/install_node.sh b/scripts/install_node.sh new file mode 100644 index 000000000..a22bfd2ef --- /dev/null +++ b/scripts/install_node.sh @@ -0,0 +1,9 @@ +# here we set up the node version on the fly. currently only node 4, but can be used for more values if need be +# This is done manually so that the build works the same on OS X +rm -rf ~/.nvm/ && git clone --depth 1 --branch v0.30.1 https://github.com/creationix/nvm.git ~/.nvm +source ~/.nvm/nvm.sh +nvm install $1 +nvm use $1 +node --version +npm --version +which node