Test for Pruner
This commit is contained in:
20
Gemfile
20
Gemfile
@@ -1,14 +1,20 @@
|
|||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
gem 'sinatra', "~> 2.0"
|
gem 'retries', '~> 0.0.5'
|
||||||
gem "retries", "~> 0.0.5"
|
gem 'sinatra', '~> 2.0'
|
||||||
|
|
||||||
gem "dotenv", "~> 2.5"
|
gem 'dotenv', '~> 2.5'
|
||||||
|
|
||||||
gem "rack-indifferent", "~> 1.2"
|
gem 'rack-indifferent', '~> 1.2'
|
||||||
|
|
||||||
gem "sinatra-router", "~> 0.2.4"
|
gem 'sinatra-router', '~> 0.2.4'
|
||||||
|
|
||||||
gem "rest-client", "~> 2.0"
|
gem 'rest-client', '~> 2.0'
|
||||||
|
|
||||||
gem "sinatra-contrib", "~> 2.0"
|
gem 'sinatra-contrib', '~> 2.0'
|
||||||
|
|
||||||
|
gem "bogus", "~> 0.1.6"
|
||||||
|
|
||||||
|
gem "rake", "~> 12.3"
|
||||||
|
|
||||||
|
gem "minitest", "~> 5.11"
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ GEM
|
|||||||
minitest (~> 5.1)
|
minitest (~> 5.1)
|
||||||
tzinfo (~> 1.1)
|
tzinfo (~> 1.1)
|
||||||
backports (3.11.3)
|
backports (3.11.3)
|
||||||
|
bogus (0.1.6)
|
||||||
|
dependor (>= 0.0.4)
|
||||||
concurrent-ruby (1.0.5)
|
concurrent-ruby (1.0.5)
|
||||||
|
dependor (1.0.1)
|
||||||
domain_name (0.5.20180417)
|
domain_name (0.5.20180417)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
dotenv (2.5.0)
|
dotenv (2.5.0)
|
||||||
@@ -27,6 +30,7 @@ GEM
|
|||||||
rack (>= 1.5)
|
rack (>= 1.5)
|
||||||
rack-protection (2.0.3)
|
rack-protection (2.0.3)
|
||||||
rack
|
rack
|
||||||
|
rake (12.3.1)
|
||||||
rest-client (2.0.2)
|
rest-client (2.0.2)
|
||||||
http-cookie (>= 1.0.2, < 2.0)
|
http-cookie (>= 1.0.2, < 2.0)
|
||||||
mime-types (>= 1.16, < 4.0)
|
mime-types (>= 1.16, < 4.0)
|
||||||
@@ -59,8 +63,11 @@ PLATFORMS
|
|||||||
ruby
|
ruby
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
|
bogus (~> 0.1.6)
|
||||||
dotenv (~> 2.5)
|
dotenv (~> 2.5)
|
||||||
|
minitest (~> 5.11)
|
||||||
rack-indifferent (~> 1.2)
|
rack-indifferent (~> 1.2)
|
||||||
|
rake (~> 12.3)
|
||||||
rest-client (~> 2.0)
|
rest-client (~> 2.0)
|
||||||
retries (~> 0.0.5)
|
retries (~> 0.0.5)
|
||||||
sinatra (~> 2.0)
|
sinatra (~> 2.0)
|
||||||
|
|||||||
6
Rakefile
Normal file
6
Rakefile
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
$stdout.sync = true
|
||||||
|
|
||||||
|
require 'bundler/setup'
|
||||||
|
require 'dotenv/load'
|
||||||
|
|
||||||
|
task default: %i[test]
|
||||||
@@ -5,5 +5,3 @@ require 'rack/indifferent'
|
|||||||
require_relative 'lib/api/tree.rb'
|
require_relative 'lib/api/tree.rb'
|
||||||
|
|
||||||
run Pruning::API::Tree
|
run Pruning::API::Tree
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ require_relative '../exceptions'
|
|||||||
|
|
||||||
module Pruning
|
module Pruning
|
||||||
module API
|
module API
|
||||||
|
# Handles general concers regarding API calls
|
||||||
class App < Sinatra::Base
|
class App < Sinatra::Base
|
||||||
before { content_type :json }
|
before { content_type :json }
|
||||||
after { serialise_response }
|
after { serialise_response }
|
||||||
set :show_exceptions, true
|
set :show_exceptions, true
|
||||||
|
|
||||||
|
|
||||||
error Pruning::Exceptions::UnexpectedError do
|
error Pruning::Exceptions::UnexpectedError do
|
||||||
status 500
|
status 500
|
||||||
end
|
end
|
||||||
@@ -20,13 +20,12 @@ module Pruning
|
|||||||
status 404
|
status 404
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
get '/' do
|
get '/' do
|
||||||
"Try /tree/:name"
|
'Try /tree/:name?indicator_ids[]=32&indicator_ids[]=31'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def serialise_response
|
def serialise_response
|
||||||
return unless content_type == 'application/json'
|
return unless content_type == 'application/json'
|
||||||
response.body = [JSON(response.body)]
|
response.body = [JSON(response.body)]
|
||||||
|
|||||||
@@ -7,17 +7,15 @@ require_relative '../exceptions'
|
|||||||
|
|
||||||
module Pruning
|
module Pruning
|
||||||
module API
|
module API
|
||||||
|
# Handles all tree related API requests
|
||||||
class Tree < App
|
class Tree < App
|
||||||
get '/tree/:name' do
|
get '/tree/:name' do
|
||||||
tree_repo = Pruning::Repos::Tree.new(RestClient, ENV['TREE_SOURCE_API_HOSTNAME'])
|
tree_repo = Pruning::Repos::Tree.new(RestClient,
|
||||||
complete_tree = tree_repo.get(query.name)
|
ENV['TREE_SOURCE_API_HOSTNAME'])
|
||||||
pruner = Pruning::Processing::Pruner.new(complete_tree)
|
complete_tree = tree_repo.get(query.name)
|
||||||
pruner.prune_tree(query.indicator_ids)
|
pruner = Pruning::Processing::Pruner.new(complete_tree)
|
||||||
|
pruner.prune_tree(query.indicator_ids.to_a)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,4 +10,3 @@ module Pruning
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
module Pruning
|
module Pruning
|
||||||
module HTTP
|
module HTTP
|
||||||
|
# Whitelists the URL query parameters
|
||||||
class Query < Struct.new(:name, :indicator_ids)
|
class Query < Struct.new(:name, :indicator_ids)
|
||||||
def initialize(params = {})
|
def initialize(params = {})
|
||||||
symbolised = params.map { |k, v| { k.to_sym => v } }.reduce({}, :merge)
|
symbolised = params.map { |k, v| { k.to_sym => v } }.reduce({}, :merge)
|
||||||
@@ -7,9 +8,9 @@ module Pruning
|
|||||||
value = symbolised.fetch(member, nil)
|
value = symbolised.fetch(member, nil)
|
||||||
next if value.nil?
|
next if value.nil?
|
||||||
case member
|
case member
|
||||||
when :indicator_ids then value.map(&->(indicator_id) { indicator_id.to_i } ) # break on purpose if indicator_ids is not an array
|
when :indicator_ids then value.map(&->(indicator_id) { indicator_id.to_i }) # break on purpose if indicator_ids is not an array
|
||||||
when :name then value.to_s.gsub(/[^A-Za-z]/,'')
|
when :name then value.to_s.gsub(/[^A-Za-z]/, '')
|
||||||
else value
|
else value
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
super(*values)
|
super(*values)
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
module Pruning
|
module Pruning
|
||||||
module Processing
|
module Processing
|
||||||
|
# Contains methods that store the tree and prune everything
|
||||||
|
# except the branches with requested indicator
|
||||||
class Pruner
|
class Pruner
|
||||||
|
|
||||||
def initialize(tree)
|
def initialize(tree)
|
||||||
@tree = tree
|
@tree = tree
|
||||||
end
|
end
|
||||||
|
|
||||||
def prune_tree(indicator_ids)
|
def prune_tree(indicator_ids)
|
||||||
pruned_tree = @tree.dup
|
pruned_tree = @tree
|
||||||
prune_subtree(pruned_tree, indicator_ids)
|
prune_subtree(pruned_tree, indicator_ids)
|
||||||
pruned_tree
|
pruned_tree
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def prune_subtree(nodes, indicator_ids)
|
def prune_subtree(nodes, indicator_ids)
|
||||||
nodes_to_examine = nodes.dup
|
nodes_to_examine = nodes.dup
|
||||||
nodes_to_examine.each do |node|
|
nodes_to_examine.each do |node|
|
||||||
if indicator_node?(node)
|
if indicator_node?(node)
|
||||||
unwanted_indicator = !indicator_ids.include?(node['id'])
|
indicator_not_wanted = !indicator_ids.include?(node['id'])
|
||||||
nodes.delete(node) if unwanted_indicator
|
nodes.delete(node) if indicator_not_wanted
|
||||||
else
|
else
|
||||||
has_no_wanted_indicators_in_children = prune_subtree(children(node), indicator_ids)
|
no_wanted_children = prune_subtree(children(node), indicator_ids)
|
||||||
nodes.delete(node) if has_no_wanted_indicators_in_children
|
nodes.delete(node) if no_wanted_children
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
nodes.empty?
|
nodes.empty?
|
||||||
@@ -29,9 +31,9 @@ module Pruning
|
|||||||
|
|
||||||
def children(node)
|
def children(node)
|
||||||
node.fetch('sub_themes', false) ||
|
node.fetch('sub_themes', false) ||
|
||||||
node.fetch('categories', false) ||
|
node.fetch('categories', false) ||
|
||||||
node.fetch('indicators', false) ||
|
node.fetch('indicators', false) ||
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
def indicator_node?(node)
|
def indicator_node?(node)
|
||||||
|
|||||||
@@ -3,31 +3,36 @@ require 'retries'
|
|||||||
require 'json'
|
require 'json'
|
||||||
require_relative '../exceptions'
|
require_relative '../exceptions'
|
||||||
|
|
||||||
|
|
||||||
|
NUMBER_OF_RETRIES = 3
|
||||||
|
|
||||||
module Pruning
|
module Pruning
|
||||||
module Repos
|
module Repos
|
||||||
|
# Handles communication with origin server
|
||||||
|
# and all its problems
|
||||||
class Tree
|
class Tree
|
||||||
def initialize(client=RestClient, base_url)
|
def initialize(client, base_url)
|
||||||
@client = client
|
@client = client
|
||||||
@base_url = base_url
|
@base_url = base_url
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(name)
|
def get(name)
|
||||||
with_retries(max_tries: 3, rescue: [Pruning::Exceptions::ServerErrorOnOrigin]) do
|
with_retries(max_tries: NUMBER_OF_RETRIES, rescue: [Pruning::Exceptions::ServerErrorOnOrigin]) do
|
||||||
begin
|
begin
|
||||||
resp = @client.get(url(name))
|
resp = @client.get(url(name))
|
||||||
rescue RestClient::ExceptionWithResponse => e
|
rescue RestClient::ExceptionWithResponse => e
|
||||||
if e.response.code != 404
|
raise Pruning::Exceptions::ServerErrorOnOrigin if e.response.code != 404
|
||||||
raise Pruning::Exceptions::ServerErrorOnOrigin
|
raise Pruning::Exceptions::OriginCannotFindTheResource
|
||||||
else
|
end
|
||||||
raise Pruning::Exceptions::OriginCannotFindTheResource
|
|
||||||
end
|
return JSON(resp.body)
|
||||||
end
|
end
|
||||||
return JSON(resp.body)
|
raise Pruning::Exceptions::UnexpectedError
|
||||||
end
|
|
||||||
raise Pruning::Exceptions::UnexpectedError
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def url(name)
|
def url(name)
|
||||||
"#{@base_url}/tree/#{name}"
|
"#{@base_url}/tree/#{name}"
|
||||||
end
|
end
|
||||||
|
|||||||
5
rakelib/test.rake
Normal file
5
rakelib/test.rake
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
require 'rake/testtask'
|
||||||
|
|
||||||
|
Rake::TestTask.new do |task|
|
||||||
|
task.pattern = 'test/**/*_test.rb'
|
||||||
|
end
|
||||||
2682
test/fixtures/correct_tree.json
vendored
Normal file
2682
test/fixtures/correct_tree.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
47
test/fixtures/missing_indicators.json
vendored
Normal file
47
test/fixtures/missing_indicators.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Urban Extent",
|
||||||
|
"sub_themes": [
|
||||||
|
{
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Population",
|
||||||
|
"unit": "(thousands)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "Population density",
|
||||||
|
"unit": "(people per sq. km.)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": 1,
|
||||||
|
"name": "Administrative"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"name": "Area",
|
||||||
|
"unit": "(sq. km.)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"name": "Population",
|
||||||
|
"unit": "(thousands)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"name": "Population density",
|
||||||
|
"unit": "(people per sq. km.)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": 2,
|
||||||
|
"name": "Built up"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
61
test/pruner_test.rb
Normal file
61
test/pruner_test.rb
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
require 'minitest/autorun'
|
||||||
|
require_relative 'test_helper'
|
||||||
|
require_relative '../lib/pruner'
|
||||||
|
|
||||||
|
|
||||||
|
class TestPruner < MiniTest::Test
|
||||||
|
|
||||||
|
def test_prune_happy_path
|
||||||
|
correct_tree = json_fixture('fixtures/correct_tree.json')
|
||||||
|
pruner = Pruning::Processing::Pruner.new(correct_tree)
|
||||||
|
tree = pruner.prune_tree([299])
|
||||||
|
expected = [{"id"=>1, "name"=>"Urban Extent",
|
||||||
|
"sub_themes"=>[{"categories"=>[{"id"=>1,
|
||||||
|
"indicators"=>[{"id"=>299, "name"=>"Total"}],
|
||||||
|
"name"=>"Area",
|
||||||
|
"unit"=>"(sq. km.)"}],
|
||||||
|
"id"=>1,
|
||||||
|
"name"=>"Administrative"}]}]
|
||||||
|
|
||||||
|
assert_equal(expected, tree)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_different_top_level_nodes
|
||||||
|
correct_tree = json_fixture('fixtures/correct_tree.json')
|
||||||
|
pruner = Pruning::Processing::Pruner.new(correct_tree)
|
||||||
|
tree = pruner.prune_tree([299, 131])
|
||||||
|
expected = [{"id"=>1, "name"=>"Urban Extent",
|
||||||
|
"sub_themes"=>[{"categories"=>[{"id"=>1, "indicators"=>[{"id"=>299, "name"=>"Total"}],
|
||||||
|
"name"=>"Area", "unit"=>"(sq. km.)"}], "id"=>1, "name"=>"Administrative"}]},
|
||||||
|
{"id"=>8, "name"=>"Business",
|
||||||
|
"sub_themes"=>[{"categories"=>[{"id"=>55, "indicators"=>[{"id"=>131, "name"=>"6-9"}],
|
||||||
|
"name"=>"Non-agriculture enterprises by size", "unit"=>"(percent of establishments)"}],
|
||||||
|
"id"=>24, "name"=>"Economic census"}]}]
|
||||||
|
|
||||||
|
|
||||||
|
assert_equal(expected, tree)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_no_indicators
|
||||||
|
correct_tree = json_fixture('fixtures/correct_tree.json')
|
||||||
|
pruner = Pruning::Processing::Pruner.new(correct_tree)
|
||||||
|
tree = pruner.prune_tree([])
|
||||||
|
assert_equal([], tree)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_nonexistant_ids
|
||||||
|
correct_tree = json_fixture('fixtures/correct_tree.json')
|
||||||
|
pruner = Pruning::Processing::Pruner.new(correct_tree)
|
||||||
|
tree = pruner.prune_tree([6000, 7000])
|
||||||
|
assert_equal([], tree)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_broken_tree
|
||||||
|
missing_indicators_tree = json_fixture('fixtures/missing_indicators.json')
|
||||||
|
pruner = Pruning::Processing::Pruner.new(missing_indicators_tree)
|
||||||
|
tree = pruner.prune_tree([299])
|
||||||
|
assert_equal([], tree)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
19
test/test_helper.rb
Normal file
19
test/test_helper.rb
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
ENV['RACK_ENV'] = 'test'
|
||||||
|
|
||||||
|
|
||||||
|
require 'bundler/setup'
|
||||||
|
require 'dotenv/load'
|
||||||
|
require 'json'
|
||||||
|
require 'minitest/autorun'
|
||||||
|
require 'bogus/minitest/spec'
|
||||||
|
|
||||||
|
require_relative '../lib/api/app'
|
||||||
|
|
||||||
|
Bogus.configure { |config| config.search_modules << Pruning }
|
||||||
|
Thread.abort_on_exception = true
|
||||||
|
|
||||||
|
|
||||||
|
def json_fixture(file_name)
|
||||||
|
file = File.read(File.expand_path(File.dirname(__FILE__) + '/' + file_name))
|
||||||
|
JSON.parse(file)
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user