Test for Pruner

This commit is contained in:
Senad Uka
2018-08-13 18:54:12 +02:00
parent 5684e58826
commit 973f1e1093
15 changed files with 2888 additions and 53 deletions

20
Gemfile
View File

@@ -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"

View File

@@ -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
View File

@@ -0,0 +1,6 @@
$stdout.sync = true
require 'bundler/setup'
require 'dotenv/load'
task default: %i[test]

View File

@@ -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

View File

@@ -6,27 +6,26 @@ 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
error Pruning::Exceptions::OriginCannotFindTheResource do error Pruning::Exceptions::OriginCannotFindTheResource do
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)]

View File

@@ -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

View File

@@ -4,10 +4,9 @@ module Pruning
end end
class OriginCannotFindTheResource < StandardError class OriginCannotFindTheResource < StandardError
end end
class UnexpectedError < StandardError class UnexpectedError < StandardError
end end
end end
end end

View File

@@ -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)

View File

@@ -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,11 +31,11 @@ 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)
children(node).empty? children(node).empty?
end end

View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

47
test/fixtures/missing_indicators.json vendored Normal file
View 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
View 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
View 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