elasticsearch-embedded 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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