elasticsearch-embedded 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,104 @@
1
+ # coding: utf-8
2
+ require 'tmpdir'
3
+ require 'open-uri'
4
+ require 'ruby-progressbar'
5
+ require 'zip'
6
+
7
+ module Elasticsearch
8
+ module Embedded
9
+ class Downloader
10
+
11
+ # Default temporary path used by downloader
12
+ TEMPORARY_PATH = defined?(Rails) ? Rails.root.join('tmp') : Dir.tmpdir
13
+ # Default version of elasticsearch to download
14
+ DEFAULT_VERSION = '1.2.1'
15
+
16
+ attr_reader :version, :path
17
+
18
+ def initialize(args = {})
19
+ @version = args[:version] || ENV['ELASTICSEARCH_VERSION'] || DEFAULT_VERSION
20
+ @path = args[:path] || ENV['ELASTICSEARCH_DOWNLOAD_PATH'] || TEMPORARY_PATH
21
+ end
22
+
23
+ # Download elasticsearch distribution and unzip it in the specified temporary path
24
+ def self.download(arguments = {})
25
+ new(arguments).perform
26
+ end
27
+
28
+ def perform
29
+ download_file
30
+ extract_file
31
+ self
32
+ end
33
+
34
+ def downloaded?
35
+ File.exists?(final_path)
36
+ end
37
+
38
+ def extracted?
39
+ File.directory?(dist_folder)
40
+ end
41
+
42
+ def dist_folder
43
+ @dist_folder ||= final_path.gsub /\.zip\Z/, ''
44
+ end
45
+
46
+ def final_path
47
+ @final_path ||= File.join(working_dir, "elasticsearch-#{version}.zip")
48
+ end
49
+
50
+ def working_dir
51
+ @working_dir ||= File.realpath(path)
52
+ end
53
+
54
+ def executable
55
+ @executable ||= File.join(dist_folder, 'bin', 'elasticsearch')
56
+ end
57
+
58
+ private
59
+
60
+ def download_file
61
+ return if downloaded?
62
+ open(final_path, 'wb') do |target|
63
+ download_options = {
64
+ content_length_proc: ->(t) { build_progress_bar(t) },
65
+ progress_proc: ->(s) { increment_progress(s) }
66
+ }
67
+ # direct call here to avoid spec issues with Kernel#open
68
+ distfile = OpenURI.open_uri("https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-#{version}.zip", download_options)
69
+ target << distfile.read
70
+ end
71
+ end
72
+
73
+ def extract_file
74
+ return if extracted?
75
+ Dir.chdir(working_dir) do
76
+ # Extract archive in path, after block CWD is restored
77
+ Zip::File.open(final_path) do |zip_file|
78
+ # Extract all entries into working dir
79
+ zip_file.each(&:extract)
80
+ end
81
+ end
82
+ # ensure main executable has execute permission
83
+ File.chmod(0755, executable)
84
+ # Create folder for log files
85
+ FileUtils.mkdir(File.join(dist_folder, 'logs'))
86
+ end
87
+
88
+ # Build a progress bar to download elasticsearch
89
+ def build_progress_bar(total)
90
+ if total && total.to_i > 0
91
+ @download_progress_bar = ProgressBar.create title: "Downloading elasticsearch #{version}", total: total,
92
+ format: '%t |%bᗧ%i| %p%% (%r KB/sec) %e', progress_mark: ' ', remainder_mark: '・',
93
+ rate_scale: ->(rate) { rate / 1024 }, smoothing: 0.7
94
+ end
95
+ end
96
+
97
+ def increment_progress(size)
98
+ @download_progress_bar.progress = size if @download_progress_bar
99
+ end
100
+
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,62 @@
1
+ require 'logging'
2
+
3
+ module Elasticsearch
4
+ module Embedded
5
+
6
+ # Contains stuff related to logging configuration
7
+ module LoggerConfiguration
8
+
9
+ # Configure logger verbosity for ::Elasticsearch::Embedded log hierarchy
10
+ # @param [String,Fixnum] level accepts strings levels or numbers
11
+ def verbosity(level)
12
+ level = level.to_s.downcase # normalize string to downcase
13
+ case
14
+ when level =~ /\A\d\Z/
15
+ Logging.logger[self].level = level.to_i
16
+ when Logging::LEVELS.include?(level)
17
+ Logging.logger[self].level = Logging::LEVELS[level]
18
+ else
19
+ Logging.logger[self].level = :info
20
+ end
21
+
22
+ end
23
+
24
+ # Clear all logging appenders for ::Elasticsearch::Embedded log hierarchy
25
+ def mute!
26
+ Logging.logger[self].clear_appenders
27
+ end
28
+
29
+ # @see https://github.com/TwP/logging/blob/master/examples/colorization.rb
30
+ def configure_logging!
31
+ # Configure logger levels for hierarchy
32
+ Logging.logger[self].level = :info
33
+ # Register a color scheme named bright
34
+ Logging.color_scheme 'bright', {
35
+ levels: {
36
+ info: :green,
37
+ warn: :yellow,
38
+ error: :red,
39
+ fatal: [:white, :on_red]
40
+ },
41
+ date: :blue,
42
+ logger: :cyan,
43
+ message: :white,
44
+ }
45
+ pattern_options = {pattern: '[%d] %-5l %c: %m\n'}
46
+ # Apply colors only if in tty
47
+ pattern_options[:color_scheme] = 'bright' if STDOUT.tty?
48
+ # Create a named appender to be used only for current module
49
+ Logging.logger[self].appenders = Logging.appenders.stdout(self.to_s, layout: Logging.layouts.pattern(pattern_options))
50
+ end
51
+
52
+ private
53
+
54
+ # Extension callback
55
+ def self.extended(base)
56
+ base.configure_logging!
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,70 @@
1
+ module Elasticsearch
2
+ module Embedded
3
+
4
+ module RSpec
5
+
6
+ module ElasticSearchHelpers
7
+ # Return a client connected to the configured client, if ::Elasticsearch::Client is defined
8
+ # return a client, else return a URI attached to cluster.
9
+ # @see http://ruby-doc.org/stdlib-2.1.2/libdoc/net/http/rdoc/Net/HTTP.html
10
+ # @see https://github.com/elasticsearch/elasticsearch-ruby
11
+ def client
12
+ @client ||=
13
+ case
14
+ when defined?(::Elasticsearch::Client)
15
+ ::Elasticsearch::Client.new host: "localhost:#{cluster.port}"
16
+ else
17
+ URI("http://localhost:#{cluster.port}/")
18
+ end
19
+ end
20
+
21
+ # Return a cluster instance to be used in tests
22
+ def cluster
23
+ ElasticSearchHelpers.memoized_cluster
24
+ end
25
+
26
+ class << self
27
+
28
+ # Return a singleton instance of cluster object
29
+ def memoized_cluster
30
+ @cluster ||= ::Elasticsearch::Embedded::Cluster.new
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+
37
+ class << self
38
+
39
+ # Configure rspec for usage with ES cluster
40
+ def configure_with(*meta)
41
+ # assign default value to tags
42
+ ::RSpec.configure do |config|
43
+
44
+ # Include helpers only in tagged specs
45
+ config.include ElasticSearchHelpers, *meta
46
+
47
+ # Before hook, starts the cluster
48
+ config.before(:each, *meta) do
49
+ ElasticSearchHelpers.memoized_cluster.ensure_started!
50
+ ElasticSearchHelpers.memoized_cluster.delete_all_indices!
51
+ end
52
+
53
+ # After suite hook, stop the cluster
54
+ config.after(:suite) do
55
+ ElasticSearchHelpers.memoized_cluster.stop if ElasticSearchHelpers.memoized_cluster.running?
56
+ end
57
+ end
58
+ end
59
+
60
+ # Default config method, configure RSpec with :elasticsearch filter only.
61
+ # Equivalent to .configure_with(:elasticsearch)
62
+ def configure
63
+ configure_with(:elasticsearch)
64
+ end
65
+ end
66
+
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,5 @@
1
+ module Elasticsearch
2
+ module Embedded
3
+ VERSION = '0.1.0'
4
+ end
5
+ end
@@ -0,0 +1,93 @@
1
+ describe Elasticsearch::Embedded::Cluster, :elasticsearch do
2
+
3
+ describe '#clear_all_indices!' do
4
+
5
+ it 'should delete all indices' do
6
+ # Create a document and an index
7
+ client.index index: 'test', type: 'test-type', id: 1, body: {title: 'Test'}
8
+ expect {
9
+ cluster.delete_all_indices!
10
+ }.to change { client.indices.get_settings.count }.to(0)
11
+ end
12
+
13
+ it 'should delete all indices' do
14
+ # Create a document and an index
15
+ client.index index: 'test', type: 'test-type', id: 1, body: {title: 'Test'}
16
+ client.index index: 'test2', type: 'test-type', id: 1, body: {title: 'Test'}
17
+ expect {
18
+ cluster.delete_index! 'test'
19
+ }.to change { client.indices.get_settings.keys }.to(['test2'])
20
+ end
21
+
22
+ end
23
+
24
+ describe '#pids' do
25
+
26
+ it 'should return a list running instances pids' do
27
+ expect(cluster.pids).to_not be_empty
28
+ end
29
+
30
+ it 'should return an empty array when not started' do
31
+ not_started = Elasticsearch::Embedded::Cluster.new
32
+ not_started.port = 50000 # Likely there's no process running on this port
33
+ expect(not_started.pids).to eq([])
34
+ end
35
+
36
+ end
37
+
38
+ describe 'Development template' do
39
+
40
+ before do
41
+ # It's not applied by default on non persistent clusters
42
+ cluster.apply_development_template!
43
+ client.indices.create index: 'any_index'
44
+ end
45
+
46
+ let(:index_settings) { client.indices.get_settings(index: 'any_index')['any_index']['settings']['index'] }
47
+
48
+ # Do not leave status across tests
49
+ after(:all) do
50
+ client.indices.delete_template name: 'development_template' if client.indices.get_template.has_key?('development_template')
51
+ end
52
+
53
+ it 'should configure 1 shard for each index' do
54
+ expect(index_settings).to include('number_of_shards' => '1')
55
+ end
56
+
57
+ it 'should configure 0 replicas for each index' do
58
+ expect(index_settings).to include('number_of_replicas' => '0')
59
+ end
60
+
61
+ end
62
+
63
+ describe 'Cluster persistency' do
64
+
65
+ let(:persistent_cluster) { Elasticsearch::Embedded::Cluster.new }
66
+ let(:port) { cluster.port + 10 }
67
+ let(:client) { Elasticsearch::Client.new host: "localhost:#{port}" }
68
+
69
+ before do
70
+ persistent_cluster.persistent = true
71
+ persistent_cluster.cluster_name = 'persistent_cluster'
72
+ persistent_cluster.port = port
73
+ end
74
+
75
+ after do
76
+ # Ensure additional cluster is stopped when test is finished
77
+ persistent_cluster.stop if persistent_cluster.running?
78
+ end
79
+
80
+ it 'should persist data across restarts' do
81
+ persistent_cluster.start
82
+ # Index a document and trigger persistent index creation
83
+ client.index index: 'persistent', type: 'test-type', id: 1, body: {title: 'Test'}, refresh: true
84
+ # Restart cluster and check index presence
85
+ expect {
86
+ persistent_cluster.stop
87
+ persistent_cluster.start
88
+ }.to_not change { client.indices.get_settings }
89
+ end
90
+
91
+ end
92
+
93
+ end
@@ -0,0 +1,131 @@
1
+ describe Elasticsearch::Embedded::Downloader do
2
+
3
+ describe 'configuration using env' do
4
+
5
+ it 'should allow configuration through ENV' do
6
+ ENV['ELASTICSEARCH_VERSION'] = '1.3.3'
7
+ ENV['ELASTICSEARCH_DOWNLOAD_PATH'] = '/tmp'
8
+ d = Elasticsearch::Embedded::Downloader.new
9
+ expect(d.version).to eq '1.3.3'
10
+ expect(d.path).to eq '/tmp'
11
+ end
12
+
13
+ after(:each) do
14
+ ENV['ELASTICSEARCH_VERSION'] = nil
15
+ ENV['ELASTICSEARCH_DOWNLOAD_PATH'] = nil
16
+ end
17
+
18
+ end
19
+
20
+ describe 'configuration using options' do
21
+
22
+ it 'should allow configuration using option arguments' do
23
+ d = Elasticsearch::Embedded::Downloader.new(path: '/tmp', version: '1.3.3')
24
+ expect(d.version).to eq '1.3.3'
25
+ expect(d.path).to eq '/tmp'
26
+ end
27
+
28
+ end
29
+
30
+ describe 'default configuration' do
31
+
32
+ it 'should use default values' do
33
+ d = Elasticsearch::Embedded::Downloader.new
34
+ expect(d.version).to eq Elasticsearch::Embedded::Downloader::DEFAULT_VERSION
35
+ expect(d.path).to eq Elasticsearch::Embedded::Downloader::TEMPORARY_PATH
36
+ end
37
+
38
+ end
39
+
40
+ # Integration tests
41
+ context 'with mocked filesystem' do
42
+
43
+ # Stub filesystem calls
44
+ include FakeFS::SpecHelpers
45
+
46
+ let(:fake_archive) do
47
+ directory = "/elasticsearch-#{subject.version}"
48
+ archive = "/elasticsearch-#{subject.version}.zip"
49
+ FileUtils.mkdir_p(directory)
50
+ FileUtils.touch(File.join(directory, 'README.textile'))
51
+ FileUtils.mkdir_p(File.join(directory, 'bin'))
52
+ FileUtils.touch(File.join(directory, 'bin', 'elasticsearch'))
53
+ # Creates a zip archive with empty files
54
+ Zip::File.open(archive, Zip::File::CREATE) do |zipfile|
55
+ # make root folder of zip archive
56
+ zipfile.add(directory.sub('/', ''), directory)
57
+ # Add all content
58
+ Dir[File.join(directory, '**', '**')].each do |file|
59
+ zipfile.add(file.sub('/', ''), file)
60
+ end
61
+ end
62
+ # Return the built archive file name
63
+ archive
64
+ end
65
+
66
+ # Realpath is needed because mockfs does not implement realpath
67
+ let(:path) { File.realpath(Dir.tmpdir) }
68
+ let(:zip_file) { File.join(path, "elasticsearch-#{subject.version}.zip") }
69
+ let(:dist_folder) { File.join(path, "elasticsearch-#{subject.version}") }
70
+ let(:executable) { File.join(dist_folder, 'bin', 'elasticsearch') }
71
+ subject { Elasticsearch::Embedded::Downloader.new(path: path) }
72
+
73
+ before do
74
+ # This is needed on OS X where /tmp is a symlink otherwise the
75
+ # openuri call will fail with Errno::ENOENT when trying to create a temporary file
76
+ FileUtils.mkdir_p(Dir.tmpdir)
77
+ # Ensure download path is present on the stubbed filesystem
78
+ FileUtils.mkdir_p(path)
79
+ end
80
+
81
+ describe 'file download' do
82
+
83
+ it 'should download file into configured path' do
84
+ expect(File.exists?(zip_file)).to be_falsey
85
+ # Stub only actual download of file
86
+ expect(OpenURI).to receive(:open_uri).and_return(File.open(fake_archive))
87
+ subject.perform
88
+ expect(File.size(zip_file)).to be >= 0
89
+ end
90
+
91
+ it 'should not download file if already present' do
92
+ FileUtils.touch(zip_file) # Simulate presence of downloaded version
93
+ FileUtils.mkdir_p(dist_folder) # Simulate presence of extracted version
94
+ expect { subject.perform }.to_not change { File.size(zip_file) }
95
+ end
96
+
97
+ end
98
+
99
+ describe 'file extraction' do
100
+
101
+ before do
102
+ # create the zip archive as if it were downloaded
103
+ expect(File.exists?(fake_archive)).to be_truthy
104
+ FileUtils.cp fake_archive, zip_file
105
+ end
106
+
107
+ it 'should extract zip archive when into final folder' do
108
+ expect { subject.perform }.to change { Dir[File.join(dist_folder, '**', '**')].count }
109
+ end
110
+
111
+ it 'should not extract zip archive when final folder is already present' do
112
+ FileUtils.mkdir_p dist_folder
113
+ expect { subject.perform }.to_not change { Dir[File.join(dist_folder, '**', '**')] }
114
+ end
115
+
116
+ it 'should make bin/elasticsearch executable' do
117
+ subject.perform
118
+ expect(File.executable?(executable)).to be_truthy
119
+ end
120
+
121
+ it 'should create a folder for log files' do
122
+ expect {
123
+ subject.perform
124
+ }.to change { File.directory?(File.join(dist_folder, 'logs')) }
125
+ end
126
+
127
+ end
128
+
129
+ end
130
+
131
+ end