elasticsearch-test-runner 0.1.0 → 0.20.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 139cdbecff8a7f080c63ce718ceee51b4718629d604b2a70f49e456fb898abee
4
- data.tar.gz: f5917106b5f1a17c1e74b664c1a16c485e4d0e623e1e2e5293f20954fb5d0ea5
3
+ metadata.gz: 6f353417226574d9f9f31aad08493921ff64ddc0442eb3b1c78ca810dd6d5f05
4
+ data.tar.gz: 8c665f93673ae59cefc2c621b37dd270acda75f1ce15e0ddacdf7b3ba8fdd0fd
5
5
  SHA512:
6
- metadata.gz: b592e11ccbeae18bb9fbd0fe5760caea1bda3ed891895073854dfc11861f6d7c386dd6b973b3807598565d6da81332be6839bcae4ad64d75b8693645bea08a2d
7
- data.tar.gz: db573c22f278bef0e57a1fc3a79fd7c9ab296504d108b2b2cded3648057c2e3df5f59230ef41a14c7fe61a92e3c10cfee247734e8f45125560d4adb1a7a9c659
6
+ metadata.gz: 51a23ebbdd4550e88083a38d9ae953a87cbc6555175f72e3cfb8798531e980772d6ff19d6aa075e52336355779e041dddfc87b713ddb8941816022e6d0a32340
7
+ data.tar.gz: a5dd823bd406475e67ddf632051d1102f981ad6b9813134d452c6eb3df8da1fec71fb0752d5ab9f24801754b00b1f57a7345fbbc7bc1c6ce1c0d2dedfca786a0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,130 @@
1
- ## [Unreleased]
1
+ # Changelog
2
2
 
3
- ## [0.1.0] - 2024-02-22
3
+ ## [0.20.0] - 2026-05-20
4
4
 
5
- - Initial release
5
+ - Replaces variables in arrays, update for the latest tests update.
6
+
7
+ ## [0.19.0] - 2026-05-12
8
+
9
+ - Adds exception to print_failure if there is one.
10
+ - Better formatting when printing match failures.
11
+ - Raise error if the requires section is missing from a test.
12
+
13
+ ## [0.18.2] - 2026-03-06
14
+
15
+ - Fixes skipping multiple tests.
16
+ - Fixes running tests in a directory.
17
+
18
+ ## [0.18.1] - 2026-03-06
19
+
20
+ - Fixes skipping tests.
21
+
22
+ ## [0.18.0] - 2026-03-05
23
+
24
+ - Lots of improvements when displaying errors. Fixes missing exceptions during particular failures. The display for errors and failures is now more detailed and comfortable to read.
25
+ - Fixes printing a success when there's no response object, for instance when catching an expected exception.
26
+ - The code for catching exceptions was refactored and made more reliable, including showing better information as previously mentioned.
27
+
28
+ ## [0.17.0] - 2026-03-04
29
+
30
+ - Better debugging detection, check for `ENV['DEBUG']` to be equal to `true`, not just if it's set.
31
+
32
+ ## [0.16.1] - 2026-02-26
33
+
34
+ - Updates debug display message for String response bodies.
35
+
36
+ ## [0.16.0] - 2026-02-25
37
+
38
+ - Lets Ruby handle successful exit code at the end of the test run, only uses `exit 1` if there are errors.
39
+ - Display of debug messages has been improved when `DEBUG=true` is used - [Pull Request](https://github.com/elastic/es-test-runner-ruby/pull/24).
40
+
41
+ ## [0.15.1] - 2026-01-30
42
+
43
+ - Rescues invalid byte sequence for search_mvt test.
44
+
45
+ ## [0.15.0] - 2026-01-29
46
+
47
+ - Bugfix: refactors response display for boolean responses (ping, exists) when in debug mode.
48
+ - Sets boxes screen size to whole screen.
49
+
50
+ ## [0.14.0] - 2026-01-29
51
+
52
+ - Introduces Ruby TTY tools for better debugging when `ENV['DEBUG']` is set and general display improvements.
53
+ - Better information for actions in passing/erroring tests, showing actions instead of the test title.
54
+
55
+ ## [0.13.0] - 2025-06-05
56
+
57
+ - Moves download tests file to Rakefile (namespace is unchanged).
58
+ - Updates Elasticsearch Clients Tests repo URL.
59
+
60
+ ## [0.12.0] - 2025-01-14
61
+
62
+ - Check TEST_SUITE variable for `serverless`.
63
+
64
+ ## [0.11.0] - 2024-12-12
65
+
66
+ - Adds skipping tests.
67
+
68
+ ## [0.10.1] - 2024-11-13
69
+
70
+ - Creates directory for tests if it doesn't exist.
71
+
72
+ ## [0.10.0] - 2024-11-13
73
+
74
+ - Using tar.gz file for downloaded tests instead of zip file.
75
+ - Updates logging and debug information. More information being logged for different levels of Logger.
76
+
77
+ ## [0.9.0] - 2024-08-15
78
+
79
+ - Better display of action and response when `ENV['DEBUG']` is true.
80
+ - Checks for more error types when catching exceptions.
81
+ - Better matching for regular expressions.
82
+ - Refactors `set_param_variable` for better substitution.
83
+
84
+ ## [0.8.0] - 2024-08-14
85
+
86
+ - Adds support for variable keys (`$`).
87
+
88
+ ## [0.7.0] - 2024-08-08
89
+
90
+ - Improves test (file) name display
91
+ - Updates QUIET environment variable check
92
+
93
+ ## [0.6.0] - 2024-08-07
94
+
95
+ - Adds `header` support, headers can be specified for an action.
96
+ - Show response in errors.
97
+
98
+ ## [0.5.0] - 2024-07-30
99
+
100
+ - Clears `@response` before running a new action
101
+ - Fixes `expected_exception?` for failures
102
+
103
+ ## [0.4.0] - 2024-07-08
104
+
105
+ - Refactors display of errors/passing tests. Adds `QUIET` environment variable parsing. If `true`, display for passing tests will be more compact (not showing file/test names).
106
+ - Updates count for better accuracy in test results.
107
+ - Rescues SystemExit, Interrupt to actually exit the script
108
+ - Adds support for catch in tests: If the arguments to `do` include `catch`, then we are expecting an error, which should be caught and tested.
109
+
110
+ ## [0.3.1] - 2024-06-27
111
+
112
+ - Fixes in error handling.
113
+
114
+ ## [0.3.0] - 2024-06-27
115
+
116
+ - Fixes exit code.
117
+ - Refactors error handling for more detailed information.
118
+
119
+ ## [0.2.0] - 2024-06-25
120
+
121
+ - Renames gemspec from elasticsearch-tests to elasticsearch-test-runner.
122
+ - Adds ability to run individual test files or directories.
123
+
124
+ ## [0.1.1] - 2024-06-20
125
+
126
+ - Require 'fileutils' in Downloader.
127
+
128
+ ## [0.1.0] - 2024-06-20
129
+
130
+ - Initial release.
data/README.md CHANGED
@@ -29,15 +29,53 @@ Elasticsearch::Tests::TestRunner.new(client, tests_path).run
29
29
 
30
30
  [The tests](https://github.com/elastic/elasticsearch-clients-tests) are designed for the Elasticsearch REST API and the Elasticsearch Serverless REST API. If you pass in an `ElasticsearchServerless::Client`, it will only run the tests that have the `requires.serverless: true` statement. Otherwise, it will only run the ones with `requires.stack: true`.
31
31
 
32
+
33
+ ### Logging
34
+
32
35
  You can optionally pass in an object that implements Ruby's Logger to the `TestRunner` initializer. This will log more information, particularly useful in the case of errors where it'll log stacktraces for exceptions and more:
33
36
 
34
37
  ```ruby
35
38
  logger = Logger.new($stdout)
36
- logger.level = Logger::WARN unless ENV['DEBUG']
39
+ logger.level = Logger::WARN unless ENV['DEBUG'] == 'true'
40
+
41
+ runner = Elasticsearch::Tests::TestRunner.new(client, tests_path, logger)
42
+ runner.run
43
+ ```
44
+
45
+ ### Running particular tests
46
+
47
+ When you run the tests, you can pass in the name of a particular test or a whole test folder, to run only those tests. Tests in the clients project are located [in the `tests` directory](https://github.com/elastic/elasticsearch-clients-tests/tree/main/tests) either as single yaml files or inside a specific directory, referring to a specific namespace. For example [`tests/get.yml`](https://github.com/elastic/elasticsearch-clients-tests/blob/main/tests/get.yml) and [`tests/bulk/10_basic.yml`](https://github.com/elastic/elasticsearch-clients-tests/blob/main/tests/bulk/10_basic.yml). If you want to run the `get.yml` test, you can pass in the file name to `run`:
48
+
49
+ ```ruby
50
+ runner.run('get.yml')
51
+ ```
37
52
 
38
- Elasticsearch::Tests::TestRunner.new(client, tests_path, logger).run
53
+ If you want to run the basic bulk tests, you can run:
54
+
55
+ ```ruby
56
+ runner.run('bulk/10_basic.yml')
39
57
  ```
40
58
 
59
+ If you want to run all the tests in a directory, you can pass in the directory:
60
+
61
+ ```ruby
62
+ runner.run('indices')
63
+ ```
64
+
65
+ This will run all the tests in [`tests/indices`](https://github.com/elastic/elasticsearch-clients-tests/tree/main/tests/indices) such as `alias.yml`, `analyze.yml`, and so on.
66
+
67
+ ### Skipping tests
68
+
69
+ If you want to skip any given tests, you can do it using `add_tests_to_skip` before calling `run` like this:
70
+
71
+ ```ruby
72
+ runner.add_tests_to_skip(['bulk/10_basic.yml', 'get.yml'])
73
+ ```
74
+
75
+ You need to pass in an Array of file or folder names, or a single test file as a String.
76
+
77
+ ### Downloading the test suite
78
+
41
79
  You can **download the YAML test files** from [the clients tests project](https://github.com/elastic/elasticsearch-clients-tests) with the following code:
42
80
 
43
81
  ```ruby
@@ -47,6 +85,40 @@ Elasticsearch::Tests::Downloader::run(tests_path)
47
85
 
48
86
  Additionally, you can run the rake task `rake es_tests:download` included in `lib/elasticsearch/tasks`.
49
87
 
88
+ ### Environment variables
89
+
90
+ You can set the following environment variables when using the test runner:
91
+
92
+ #### `DEBUG`
93
+
94
+ If you set `DEBUG` to `true`, you'll see debug messages for each tests, including the response status, body and headers sent from Elasticsearch:
95
+
96
+ ```
97
+ 🟢 cluster/put_settings.yml - is_true: acknowledged passed [200]
98
+ ┌[DEBUG]──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
99
+ │File: cluster/remote_info.yml | Action: remote_info │
100
+ │Parameters: {} │
101
+ │ │
102
+ │Response status: 200 │
103
+ │Response body: │
104
+ │Response headers: │
105
+ │ x-elastic-product: Elasticsearch │
106
+ │ content-type: application/vnd.elasticsearch+json;compatible-with=9 │
107
+ │ content-length: 2 │
108
+ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
109
+ ```
110
+
111
+ #### `QUIET`
112
+
113
+ If you set `QUIET` to anything other than `false` or the String `"false"`, tests will run in quiet mode, where you'll only see the green (success) and/or red (failure) output for the tests:
114
+
115
+ ```
116
+ $ QUIET=1 be rake test:yaml
117
+ 🟢 🟢 🟢 🟢 🟢 🔴 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢 🟢
118
+ ```
119
+
120
+ It will still print out the summary and information about failures and/or errors at the bottom.
121
+
50
122
  ## Development
51
123
 
52
124
  See [CONTRIBUTING](./CONTRIBUTING.md).
data/Rakefile CHANGED
@@ -16,5 +16,18 @@
16
16
  # under the License.
17
17
 
18
18
  require 'bundler/gem_tasks'
19
+ require 'elasticsearch/tests/downloader'
19
20
  task default: %i[]
20
- Dir.glob('lib/elasticsearch/tasks/**/*.rake').each { |f| import f }
21
+
22
+ namespace :es_tests do
23
+ desc 'Download YAML test files'
24
+ task :download do |_, args|
25
+ tests_path = args[:path] || File.expand_path('./tmp', __dir__)
26
+ Elasticsearch::Tests::Downloader::run(tests_path)
27
+ end
28
+ end
29
+
30
+ desc 'Run unit tests'
31
+ task :test do
32
+ sh 'bundle exec rspec'
33
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Licensed to Elasticsearch B.V. under one or more contributor
2
4
  # license agreements. See the NOTICE file distributed with
3
5
  # this work for additional information regarding copyright
@@ -35,7 +37,15 @@ module Elasticsearch
35
37
  # specifications. These are function calls to the Elasticsearch clients.
36
38
  #
37
39
  def do_action(action)
40
+ @response = nil
41
+ catchable = action.delete('catch')
38
42
  client = @client
43
+ if action['headers']
44
+ client.transport.options[:transport_options][:headers].merge(
45
+ { headers: action.delete('headers') }
46
+ )
47
+ end
48
+
39
49
  action = action.first if action.is_a?(Array)
40
50
  method, params = action.is_a?(String) ? [action, {}] : action.first
41
51
 
@@ -45,18 +55,70 @@ module Elasticsearch
45
55
  client = @client.send(arrayed_method.first)
46
56
  method = arrayed_method.last
47
57
  end
48
-
49
- @response = client.send(method.to_sym, process_params(params))
58
+ params = process_params(params)
59
+ @response = client.send(method.to_sym, params)
60
+ print_debug_message(method.to_sym, params) if debug?
50
61
  @response
51
62
  rescue StandardError => e
52
- raise e
63
+ # Raise if it's an actual error:
64
+ unless expected_exception?(catchable, e)
65
+ print_failure(action, @response, e)
66
+ raise e
67
+ end
68
+ # Show success if we caught an expected exception:
69
+ print_success
70
+ print_debug_catchable(e) if debug?
71
+ end
72
+
73
+ # The keyword `catch` is used in the yaml tests to catch exceptions. This code looks at the
74
+ # expected error and the response error to compare them and check that we got the expected
75
+ # exception.
76
+ def expected_exception?(error_type, e)
77
+ return false if error_type.nil?
78
+
79
+ case error_type
80
+ when 'request_timeout'
81
+ e.is_a?(Elastic::Transport::Transport::Errors::RequestTimeout)
82
+ when 'missing', /resource_not_found_exception/
83
+ e.is_a?(Elastic::Transport::Transport::Errors::NotFound)
84
+ when 'conflict'
85
+ e.is_a?(Elastic::Transport::Transport::Errors::Conflict)
86
+ when 'request'
87
+ e.is_a?(Elastic::Transport::Transport::Errors::InternalServerError)
88
+ when 'bad_request'
89
+ e.is_a?(Elastic::Transport::Transport::Errors::BadRequest)
90
+ when 'param'
91
+ actual_error.is_a?(ArgumentError)
92
+ when 'unauthorized'
93
+ e.is_a?(Elastic::Transport::Transport::Errors::Unauthorized)
94
+ when 'forbidden'
95
+ e.is_a?(Elastic::Transport::Transport::Errors::Forbidden)
96
+ when /error parsing field/, /illegal_argument_exception/
97
+ e.message.match?(/\[400\]/) ||
98
+ e.is_a?(Elastic::Transport::Transport::Errors::BadRequest)
99
+ when /NullPointerException/
100
+ e.message.match?(/\[400\]/)
101
+ when /status_exception/
102
+ e.message.match?(/\[409\]/)
103
+ else
104
+ e.message.match?(/#{error_type}/)
105
+ end
53
106
  end
54
107
 
55
108
  # Code for matching expectations and response
56
109
  #
57
110
  def do_match(action)
58
111
  k, v = action['match'].first
59
- v = instance_variable_get(v.gsub('$', '@')) if v.is_a?(String) && v.include?('$')
112
+
113
+ v = if v.is_a?(String) && v.include?('$')
114
+ instance_variable_get(v.gsub('$', '@'))
115
+ elsif v.is_a?(Array)
116
+ v.map do |a|
117
+ a.is_a?(String) && a.start_with?('$') ? instance_variable_get(a.gsub('$', '@')) : a
118
+ end
119
+ else
120
+ v
121
+ end
60
122
  result = search_in_response(k)
61
123
 
62
124
  if !result.nil? && (
@@ -67,20 +129,27 @@ module Elasticsearch
67
129
  print_success
68
130
  else
69
131
  print_match_failure(action)
132
+ message = "Match failure\nExpected #{k}: #{v}.\nResult: #{result}"
133
+ raise ActionError.new(message, $test_file, action)
70
134
  end
71
135
  end
72
136
 
73
137
  def match_regexp(expected, result)
74
- expected.is_a?(String) && expected.match?(/^\//) && result.match?(Regexp.new(expected.gsub('/', '')))
138
+ expected.is_a?(String) &&
139
+ expected.match?(%r{^/}) &&
140
+ result.match?(Regexp.new(expected.gsub('/', '').strip))
75
141
  end
76
142
 
77
143
  def do_length(action)
78
144
  k, v = action['length'].first
79
- result = search_in_response(k).count
145
+ result = search_in_response(k)
146
+ result = result.count
80
147
  if result && result == v
81
148
  print_success
82
149
  else
83
150
  print_failure(action, @response)
151
+ message = "do_length failure\nExpected #{k}: #{v}.\nResult: #{result}"
152
+ raise ActionError.new(message, $test_file, action)
84
153
  end
85
154
  end
86
155
 
@@ -99,6 +168,8 @@ module Elasticsearch
99
168
  print_success
100
169
  else
101
170
  print_failure(action, @response)
171
+ message = "is_true failure\nResponse: #{response_value || @response}."
172
+ raise ActionError.new(message, $test_file, action)
102
173
  end
103
174
  end
104
175
 
@@ -108,6 +179,8 @@ module Elasticsearch
108
179
  print_success
109
180
  else
110
181
  print_failure(action, @response)
182
+ message = "is_false failure\nResponse: #{response_value || @response}."
183
+ raise ActionError.new(message, $test_file, action)
111
184
  end
112
185
  end
113
186
 
@@ -118,11 +191,14 @@ module Elasticsearch
118
191
  #
119
192
  def compare(action)
120
193
  operator, value = action.first
194
+
121
195
  result = search_in_response(value.keys.first)
122
196
  if result&.send(COMPARATORS[operator], value[value.keys.first])
123
197
  print_success
124
198
  else
125
199
  print_failure(action, @response)
200
+ message = "comparision failure\n#{result} #{COMPARATORS[operator]} #{value[value.keys.first]}"
201
+ raise ActionError.new(message, $test_file, action)
126
202
  end
127
203
  end
128
204
 
@@ -135,16 +211,18 @@ module Elasticsearch
135
211
 
136
212
  private
137
213
 
214
+ def debug?
215
+ ENV['DEBUG'] == 'true'
216
+ end
217
+
138
218
  # Given a key coming from a test definition, search the response body for a matching value.
139
219
  #
140
220
  def search_in_response(keys)
141
- if (match = /\$([a-z]+)/.match(keys))
142
- return @response.send(match[1])
143
- end
144
-
145
221
  if keys.include?('.')
146
222
  key = split_and_parse_key(keys)
147
223
  return find_value_in_document(key, @response.body)
224
+ elsif (match = /\$([a-z]+)/.match(keys))
225
+ return @response.send(match[1])
148
226
  end
149
227
 
150
228
  @response[keys]
@@ -164,7 +242,13 @@ module Elasticsearch
164
242
  end
165
243
 
166
244
  def set_param_variable(params, key, param)
167
- params[key] = instance_variable_get(param.gsub('$', '@')) if param.is_a?(String) && param.include?('$')
245
+ return unless param.is_a?(String) && param.include?('$')
246
+
247
+ # Param can be a single '$value' string or '{ something: $value }'
248
+ replaceable = param.match(/(\$[0-9a-z_-]+)/)[0]
249
+ value = instance_variable_get(replaceable.gsub('$', '@'))
250
+ content = param.gsub(replaceable, value)
251
+ params[key] = content
168
252
  end
169
253
 
170
254
  # Given a list of keys, find the value in a recursively nested document.
@@ -179,7 +263,9 @@ module Elasticsearch
179
263
  return document[chain[0]] unless chain.size > 1
180
264
 
181
265
  # a number can be a string key in a Hash or indicate an element in a list
182
- if document.is_a?(Hash)
266
+ if chain[0].is_a?(String) && chain[0].match?(/^\$/)
267
+ find_value_in_document(chain[1..], instance_variable_get("@#{chain[0].gsub('$', '')}"))
268
+ elsif document.is_a?(Hash)
183
269
  find_value_in_document(chain[1..], document[chain[0].to_s]) if document[chain[0].to_s]
184
270
  elsif document[chain[0]]
185
271
  find_value_in_document(chain[1..], document[chain[0]]) if document[chain[0]]
@@ -15,24 +15,25 @@
15
15
  # specific language governing permissions and limitations
16
16
  # under the License.
17
17
  require 'open-uri'
18
+ require 'fileutils'
18
19
 
19
20
  module Elasticsearch
20
21
  module Tests
21
22
  # Module for downloading the test files
22
23
  module Downloader
23
24
  class << self
24
- FILENAME = 'tests.zip'.freeze
25
+ FILENAME = 'tests.tar.gz'.freeze
25
26
 
26
- def run(path, branch = 'main')
27
- delete_files(path)
28
- url = "https://api.github.com/repos/elastic/serverless-clients-tests/zipball/#{branch}"
27
+ def run(path, branch = 'main', delete: false)
28
+ delete_files(path) if delete
29
+ url = "https://api.github.com/repos/elastic/elasticsearch-clients-tests/tarball/#{branch}"
29
30
  if download_tests(url)
30
31
  puts "Successfully downloaded #{FILENAME}"
31
32
  else
32
33
  warn "[!] Couldn't download #{FILENAME}"
33
34
  return
34
35
  end
35
- unzip_file(path)
36
+ untar_file(path)
36
37
  File.delete(FILENAME)
37
38
  end
38
39
 
@@ -48,11 +49,12 @@ module Elasticsearch
48
49
 
49
50
  private
50
51
 
51
- def unzip_file(path)
52
- puts 'Unzipping files'
52
+ def untar_file(path)
53
+ puts 'Extracting tar files'
53
54
  puts path
54
- `unzip #{FILENAME} -d #{path}/`
55
- puts 'Removing zip file'
55
+ FileUtils.mkdir_p(path) unless File.directory?(path)
56
+ `tar -zxf #{FILENAME} --strip-components=1 -C #{path}/`
57
+ puts 'Removing tar file'
56
58
  end
57
59
 
58
60
  def delete_files(path)
@@ -19,5 +19,16 @@ module Elasticsearch
19
19
  module Tests
20
20
  class TestFailure < StandardError; end
21
21
  class TestError < StandardError; end
22
+
23
+ # Class to track exactly which action errored
24
+ class ActionError < StandardError
25
+ attr_reader :test_file, :action
26
+
27
+ def initialize(message, test_file, action)
28
+ super(message)
29
+ @test_file = test_file
30
+ @action = action
31
+ end
32
+ end
22
33
  end
23
34
  end
@@ -14,6 +14,8 @@
14
14
  # KIND, either express or implied. See the License for the
15
15
  # specific language governing permissions and limitations
16
16
  # under the License.
17
+ require 'tty-box'
18
+ require 'tty-screen'
17
19
  require_relative 'errors'
18
20
 
19
21
  module Elasticsearch
@@ -22,58 +24,136 @@ module Elasticsearch
22
24
  # Functions to print out test results, errors, summary, etc.
23
25
  #
24
26
  module Printer
27
+ BOX_WIDTH = TTY::Screen.width
28
+
25
29
  def print_success
26
- response = if [true, false].include? @response
27
- @response
28
- else
29
- @response.status
30
- end
31
- puts "🟢 #{@file} #{@title} passed. Response: #{response}"
30
+ if quiet?
31
+ print '🟢 '
32
+ else
33
+ msg = "🟢 \e[33m#{@short_name}\e[0m - #{@action} \e[32mpassed\e[0m"
34
+ if @response.nil?
35
+ puts msg
36
+ else
37
+ status = boolean_response? ? @response : @response.status
38
+ puts "#{msg} [#{status}]"
39
+ end
40
+ end
32
41
  end
33
42
 
34
- def print_failure(action, response)
35
- puts "🔴 #{@file} #{@title} failed"
36
- puts "Expected result: #{action}" # TODO: Show match/length differently
37
- if response.is_a?(ElasticsearchServerless::API::Response)
38
- puts 'Response:'
39
- pp response.body
43
+ def quiet?
44
+ !ENV['QUIET'].nil? && ![false, 'false'].include?(ENV['QUIET'])
45
+ end
46
+
47
+ def print_failure(action, response, exception = nil)
48
+ if quiet?
49
+ print '🔴 '
40
50
  else
41
- pp response
51
+ puts "🔴 \e[33m#{@short_name}\e[0m - #{@action} \e[31mfailed\e[0m"
42
52
  end
43
- raise Elasticsearch::Tests::TestFailure
53
+ message = ["Expected result: #{action}"]
54
+ if response&.body
55
+ message << response.body
56
+ else
57
+ message << response
58
+ end
59
+ message << "Exception: #{exception}" if exception
44
60
  end
45
61
 
46
62
  def print_match_failure(action)
47
63
  keys = action['match'].keys.first
48
64
  value = action['match'].values.first
65
+
66
+ if quiet?
67
+ print '🔴 '
68
+ else
69
+ puts "🔴 \e[33m#{@short_name} (match)\e[0m - \e[32;9m#{keys}: #{value}\e[0m \e[95m#{keys}: #{search_in_response(action['match'].keys.first)}\e[0m \e[31mfailed\e[0m"
70
+ end
49
71
  message = <<~MSG
50
- 🔴 #{@file} #{@title} failed
51
- Expected: { #{keys}: #{value} }
52
- Actual : { #{keys}: #{search_in_response(action['match'].keys.first)} }
72
+ #{@short_name} #{@title} failed
73
+ \e[31m\e[1mExpected: { #{keys}: #{value} }\e[0m
74
+ \e[32m\e[1mGot : { #{keys}: #{search_in_response(action['match'].keys.first)} }\e[0m
75
+ \e[1mResponse\e[0m:\n\e[36m #{@response}\e[0m
53
76
  MSG
54
77
  raise Elasticsearch::Tests::TestFailure.new(message)
55
78
  end
56
79
 
57
80
  def print_error(error)
58
- puts "❌ ERROR: #{@file} #{@title} failed\n"
81
+ print TTY::Box.error("❌ ERROR: #{@short_name} #{@title} failed", width: BOX_WIDTH)
59
82
  logger.error error.display
60
83
  backtrace = error.backtrace.join("\n")
61
84
  logger.error "#{backtrace}\n"
62
85
  raise error
63
86
  end
64
87
 
65
- def self.display_errors(errors)
66
- puts "+++ ❌ Errors/Failures: #{errors.count}"
67
- errors.map do |error|
68
- puts "* Test: #{error[:file]}\n #{error[:error].message}"
69
- pp error[:error].backtrace.join("$/\n") if ENV['DEBUG']
70
- puts
88
+ def self.display_errors(errors, logger)
89
+ puts
90
+ print TTY::Box.frame("❌ Errors/Failures: #{errors.count}", width: BOX_WIDTH, style: { border: { fg: :red } })
91
+
92
+ errors.each do |error|
93
+ print TTY::Box.frame("🧪 Test: #{error[:file]}", width: BOX_WIDTH * 0.6, style: { border: { fg: :red } })
94
+ message = []
95
+ message << "▶ Action: #{error[:action].first}" if error[:action]
96
+ message << "🔬 #{error[:error].class} - #{error[:error].message}"
97
+ message << error[:error].backtrace.join("$/\n") if ENV['DEBUG'] == 'true'
98
+ puts message.join("\n")
99
+ logger.error(message.join("\n"))
71
100
  end
72
101
  end
73
102
 
74
- def self.display_summary(tests_count, errors_count, start_time)
75
- puts "--- 🧪 Tests: #{tests_count} | Passed: #{tests_count - errors_count} | Failed: #{errors_count}"
76
- puts "--- ⏲ Elapsed time: #{Time.at(Time.now - start_time).utc.strftime("%H:%M:%S")}"
103
+ def self.display_summary(tests_count, errors_count, start_time, logger)
104
+ summary = "🧪 Tests: #{tests_count} | Passed: #{tests_count - errors_count} | Failed: #{errors_count}"
105
+ logger.info summary
106
+ duration = "⏲ Elapsed time: #{Time.at(Time.now - start_time).utc.strftime('%H:%M:%S')}"
107
+ message = <<~MSG
108
+ #{summary}
109
+ #{duration}
110
+ MSG
111
+ puts
112
+ print TTY::Box.frame(message, width: BOX_WIDTH, title: { top_left: '[SUMMARY]' }, style: { border: { fg: :cyan } })
113
+ logger.info duration
114
+ end
115
+
116
+ def print_debug_message(method, params)
117
+ begin
118
+ message = [
119
+ "File: #{$test_file} | Action: #{method}",
120
+ "Parameters: #{params}",
121
+ ''
122
+ ]
123
+ if boolean_response?
124
+ message << "Response: #{@response}"
125
+ else
126
+ message << "Response status: #{@response.status}"
127
+ message << 'Response body:'
128
+ if @response.body.is_a?(String)
129
+ message.push(@response.body.empty? ? ' ""' : @response.body)
130
+ elsif @response.body.is_a?(Hash)
131
+ message.push(*@response.body.map { |k, v| " #{k}: #{v}" })
132
+ end
133
+ message << 'Response headers:'
134
+ message.push(*@response.headers.map { |k, v| " #{k}: #{v}" })
135
+ end
136
+ puts TTY::Box.frame(message.join("\n"), width: BOX_WIDTH, title: { top_left: '[DEBUG]' })
137
+ rescue ArgumentError => e
138
+ if e.message == 'invalid byte sequence in UTF-8'
139
+ @response.body.encode!('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
140
+ print_debug_message(method, params)
141
+ end
142
+ end
143
+ end
144
+
145
+ def print_debug_catchable(exception)
146
+ puts TTY::Box.frame(
147
+ "Catchable: #{exception}\nResponse: #{@response}\n",
148
+ width: BOX_WIDTH,
149
+ title: { top_left: '[DEBUG]' }
150
+ )
151
+ end
152
+
153
+ private
154
+
155
+ def boolean_response?
156
+ [true, false].include? @response
77
157
  end
78
158
  end
79
159
  end
@@ -16,6 +16,7 @@
16
16
  # under the License.
17
17
 
18
18
  require_relative 'code_runner'
19
+ require_relative 'errors'
19
20
 
20
21
  module Elasticsearch
21
22
  module Tests
@@ -32,7 +33,9 @@ module Elasticsearch
32
33
  @teardown = extract_teardown!(yaml)
33
34
  @title = yaml.first.keys.first
34
35
  @actions = yaml.first[@title]
35
- @file = test_filename(file)
36
+ @file = file
37
+ name = file.split('/')
38
+ @short_name = "#{name[-2]}/#{name[-1]}"
36
39
  @client = client
37
40
  end
38
41
 
@@ -56,20 +59,28 @@ module Elasticsearch
56
59
 
57
60
  case definition
58
61
  when 'do'
62
+ @action = action['do'].keys.first
59
63
  do_action(action['do'])
60
64
  when 'set'
61
65
  set_variable(action)
62
66
  when 'match'
67
+ @action = "#{action.keys.first} #{format_action(action[action.keys.first])}"
63
68
  do_match(action)
64
69
  when 'length'
70
+ @action = format_action(action)
65
71
  do_length(action)
66
72
  when 'is_true'
73
+ @action = format_action(action)
67
74
  is_true(action)
68
75
  when 'is_false'
76
+ @action = format_action(action)
69
77
  is_false(action)
70
78
  when 'gt', 'gte', 'lt', 'lte'
79
+ @action = format_action(action)
71
80
  compare(action)
72
81
  end
82
+ rescue StandardError => e
83
+ raise ActionError.new(e.message, @file, action)
73
84
  end
74
85
 
75
86
  def run_setup
@@ -87,12 +98,7 @@ module Elasticsearch
87
98
  end
88
99
 
89
100
  def count
90
- @actions.length
91
- end
92
-
93
- def test_filename(file)
94
- name = file.split('/')
95
- "#{name[-2]}/#{name[-1]}"
101
+ @actions.select { |a| a.keys.first == 'do' }.count
96
102
  end
97
103
 
98
104
  def extract_setup!(yaml)
@@ -106,6 +112,17 @@ module Elasticsearch
106
112
  yaml.delete_at(i) if a.keys.first == 'teardown'
107
113
  end.compact.first
108
114
  end
115
+
116
+ private
117
+
118
+ def format_action(action)
119
+ action.to_s
120
+ .gsub(/^{/, '')
121
+ .gsub(' =>', ':')
122
+ .gsub('"', '')
123
+ .gsub('}', '')
124
+ .gsub('{', '|')
125
+ end
109
126
  end
110
127
  end
111
128
  end
@@ -25,13 +25,23 @@ module Elasticsearch
25
25
  # Main YAML test runner
26
26
  class TestRunner
27
27
  LOGGER = Logger.new($stdout)
28
- LOGGER.level = Logger::WARN unless ENV['DEBUG']
28
+ LOGGER.level = ENV['DEBUG'] == 'true' ? Logger::DEBUG : Logger::WARN
29
29
 
30
30
  def initialize(client, path = nil, logger = nil)
31
31
  @client = client
32
- @serverless = (defined?(::ElasticsearchServerless) && client.is_a?(::ElasticsearchServerless::Client))
32
+ @serverless = defined?(::ElasticsearchServerless) && client.is_a?(::ElasticsearchServerless::Client) ||
33
+ ENV['TEST_SUITE'] == 'serverless'
33
34
  @path = path || File.expand_path('./tmp', __dir__)
34
35
  @logger = logger || LOGGER
36
+ @tests_to_skip = []
37
+ end
38
+
39
+ def add_tests_to_skip(tests)
40
+ if tests.is_a? String
41
+ @tests_to_skip << tests
42
+ else
43
+ @tests_to_skip += tests
44
+ end
35
45
  end
36
46
 
37
47
  def run(test_files = [])
@@ -39,8 +49,9 @@ module Elasticsearch
39
49
 
40
50
  @test_files = select_test_files(test_files)
41
51
  run_the_tests
42
- Elasticsearch::Tests::Printer::display_errors(@errors) unless @errors.empty?
43
- Elasticsearch::Tests::Printer::display_summary(@tests_count, @errors.count, @start_time)
52
+ Elasticsearch::Tests::Printer::display_errors(@errors, @logger) unless @errors.empty?
53
+ Elasticsearch::Tests::Printer::display_summary(@tests_count, @errors.count, @start_time, @logger)
54
+ exit 1 unless @errors.empty?
44
55
  end
45
56
 
46
57
  def run_the_tests
@@ -48,39 +59,60 @@ module Elasticsearch
48
59
  @tests_count = 0
49
60
  @errors = []
50
61
 
51
- @test_files.map do |test_file|
52
- build_and_run_tests(test_file)
62
+ @test_files.map do |test_path|
63
+ $test_file = test_filename(test_path)
64
+ build_and_run_tests(test_path)
53
65
  rescue Psych::SyntaxError => e
54
- @errors << { error: e, file: test_file }
55
- @logger.warn("YAML error in #{test_file}")
66
+ @errors << { error: e, file: $test_file }
67
+ @logger.warn("YAML error in #{$test_file}")
56
68
  @logger.warn e
69
+ rescue ActionError => e
70
+ @errors << { error: e, file: $test_file, action: e.action }
71
+ @logger.debug e
57
72
  rescue StandardError => e
58
- @errors << { error: e, file: test_file }
73
+ @errors << { error: e, file: $test_file }
59
74
  @logger.debug e
75
+ rescue SystemExit, Interrupt
76
+ exit
60
77
  end
61
78
  end
62
79
 
63
- def build_and_run_tests(test_file)
64
- yaml = YAML.load_stream(File.read(test_file))
65
- requires = extract_requires!(yaml).compact.first['requires']
80
+ def build_and_run_tests(test_path)
81
+ yaml = YAML.load_stream(File.read(test_path))
82
+ begin
83
+ requires = extract_requires!(yaml).compact.first['requires']
84
+ rescue StandardError => e
85
+ raise ArgumentError, "Could not find requires key in test file:\n #{test_path}\n#{e}"
86
+ end
66
87
  return unless (requires['serverless'] == true && @serverless) ||
67
88
  (requires['stack'] == true && !@serverless)
68
89
 
69
- test = Elasticsearch::Tests::Test.new(yaml, test_file, @client)
90
+ test = Elasticsearch::Tests::Test.new(yaml, test_path, @client)
70
91
  test.execute
71
92
  @tests_count += test.count
72
- rescue StandardError => e
73
- raise e
93
+ end
94
+
95
+ def test_filename(file)
96
+ name = file.split('/')
97
+ "#{name[-2]}/#{name[-1]}"
74
98
  end
75
99
 
76
100
  def select_test_files(test_files)
77
- if test_files.empty?
78
- Dir.glob("#{@path}/**/*.yml")
79
- else
80
- test_files.map do |test_file|
81
- "#{@path}/tests/#{test_file}"
82
- end
101
+ tests_path = if test_files.empty?
102
+ "#{@path}/**/*.yml"
103
+ elsif test_files.include?('yml')
104
+ return ["#{@path}/tests/#{test_files}"]
105
+ else
106
+ "#{@path}/tests/#{test_files}/*.yml"
107
+ end
108
+ tests = Dir.glob(tests_path)
109
+ # Find the full paths of tests to be skipped and delete them from the tests to run:
110
+ full_paths = @tests_to_skip.map do |skip|
111
+ tests.find { |t| t.match?(skip) }
83
112
  end
113
+ full_paths.each { |a| tests.delete(a) }
114
+
115
+ tests
84
116
  end
85
117
 
86
118
  def extract_requires!(yaml)
@@ -14,10 +14,11 @@
14
14
  # KIND, either express or implied. See the License for the
15
15
  # specific language governing permissions and limitations
16
16
  # under the License.
17
+
17
18
  # frozen_string_literal: true
18
19
 
19
20
  module Elasticsearch
20
21
  module Tests
21
- VERSION = "0.1.0"
22
+ VERSION = '0.20.0'
22
23
  end
23
24
  end
metadata CHANGED
@@ -1,15 +1,42 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elasticsearch-test-runner
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elastic Client Library Maintainers
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-06-20 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: tty-box
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: tty-screen
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
13
40
  - !ruby/object:Gem::Dependency
14
41
  name: elasticsearch
15
42
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +65,34 @@ dependencies:
38
65
  - - ">="
39
66
  - !ruby/object:Gem::Version
40
67
  version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rspec
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rspec-mocks
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
41
96
  description: A test runner for the Elasticsearch clients YAML test suite, used in
42
97
  the elasticsearch and elasticsearch-serverless gems.
43
98
  email:
@@ -53,8 +108,6 @@ files:
53
108
  - NOTICE
54
109
  - README.md
55
110
  - Rakefile
56
- - elasticsearch-tests.gemspec
57
- - lib/elasticsearch/tasks/download_test_suite.rake
58
111
  - lib/elasticsearch/tests.rb
59
112
  - lib/elasticsearch/tests/code_runner.rb
60
113
  - lib/elasticsearch/tests/downloader.rb
@@ -70,7 +123,6 @@ metadata:
70
123
  homepage_uri: https://www.elastic.co/guide/en/elasticsearch/client/ruby-api/current/index.html
71
124
  changelog_uri: https://github.com/elastic/es-test-runner-ruby/blob/main/CHANGELOG.md
72
125
  source_code_uri: https://github.com/elastic/es-test-runner-ruby/tree/main
73
- post_install_message:
74
126
  rdoc_options: []
75
127
  require_paths:
76
128
  - lib
@@ -85,8 +137,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
85
137
  - !ruby/object:Gem::Version
86
138
  version: '0'
87
139
  requirements: []
88
- rubygems_version: 3.5.13
89
- signing_key:
140
+ rubygems_version: 4.0.10
90
141
  specification_version: 4
91
142
  summary: Tool to test Elasticsearch clients against the YAML clients test suite.
92
143
  test_files: []
@@ -1,51 +0,0 @@
1
- # Licensed to Elasticsearch B.V. under one or more contributor
2
- # license agreements. See the NOTICE file distributed with
3
- # this work for additional information regarding copyright
4
- # ownership. Elasticsearch B.V. licenses this file to you under
5
- # the Apache License, Version 2.0 (the "License"); you may
6
- # not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
11
- # Unless required by applicable law or agreed to in writing,
12
- # software distributed under the License is distributed on an
13
- # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
- # KIND, either express or implied. See the License for the
15
- # specific language governing permissions and limitations
16
- # under the License.
17
-
18
- # frozen_string_literal: true
19
-
20
- require_relative 'lib/elasticsearch/tests/version'
21
-
22
- Gem::Specification.new do |spec|
23
- spec.name = 'elasticsearch-test-runner'
24
- spec.email = ['client-libs@elastic.co']
25
- spec.version = Elasticsearch::Tests::VERSION
26
- spec.authors = ['Elastic Client Library Maintainers']
27
- spec.licenses = ['Apache-2.0']
28
- spec.summary = 'Tool to test Elasticsearch clients against the YAML clients test suite.'
29
- spec.homepage = 'https://www.elastic.co/guide/en/elasticsearch/client/ruby-api/current/index.html'
30
- spec.description = 'A test runner for the Elasticsearch clients YAML test suite, used in the elasticsearch and elasticsearch-serverless gems.'
31
- spec.metadata['homepage_uri'] = spec.homepage
32
- spec.metadata['changelog_uri'] = 'https://github.com/elastic/es-test-runner-ruby/blob/main/CHANGELOG.md'
33
- spec.metadata['source_code_uri'] = 'https://github.com/elastic/es-test-runner-ruby/tree/main'
34
-
35
- spec.required_ruby_version = '>= 3.0'
36
-
37
- # Specify which files should be added to the gem when it is released.
38
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
39
- spec.files = Dir.chdir(__dir__) do
40
- `git ls-files -z`.split("\x0").reject do |f|
41
- (File.expand_path(f) == __FILE__) ||
42
- f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
43
- end
44
- end
45
- spec.bindir = 'exe'
46
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
47
- spec.require_paths = ['lib']
48
-
49
- spec.add_development_dependency 'elasticsearch'
50
- spec.add_development_dependency 'elasticsearch-serverless'
51
- end
@@ -1,25 +0,0 @@
1
- # Licensed to Elasticsearch B.V. under one or more contributor
2
- # license agreements. See the NOTICE file distributed with
3
- # this work for additional information regarding copyright
4
- # ownership. Elasticsearch B.V. licenses this file to you under
5
- # the Apache License, Version 2.0 (the "License"); you may
6
- # not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
11
- # Unless required by applicable law or agreed to in writing,
12
- # software distributed under the License is distributed on an
13
- # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
- # KIND, either express or implied. See the License for the
15
- # specific language governing permissions and limitations
16
- # under the License.
17
- require 'elasticsearch/tests/downloader'
18
-
19
- namespace :es_tests do
20
- desc 'Download YAML test files'
21
- task :download do |_, args|
22
- tests_path = args[:path] || File.expand_path('../../tmp', __dir__)
23
- Elasticsearch::Tests::Downloader::run(tests_path)
24
- end
25
- end