create_tests 1.1.0 → 1.3.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: 754317e21adff9283ea8d3424b46ee09de1f5a53d9e875b9388014e261f6d561
4
- data.tar.gz: e70d64dddd1f4312c67149e7ca3761e1d5038332252e9c8a191427e53627c3b9
3
+ metadata.gz: 4ca29473a6da1f495ae3bb61fc97638c049f03a7e7093f54db1ac5a1e602b97b
4
+ data.tar.gz: 829f1a5085c49bc9339095d6fbb9d6f603f6badaa0691f9344e8c0f173ca58f1
5
5
  SHA512:
6
- metadata.gz: 10f9687590b335bd26ab6bc039d9ad05eed6ca780608363c934db2a929f11ea229e267d95498935ac9755c4eb6bb4eec437b985d114efe747870e7371fc50dd5
7
- data.tar.gz: '048048f6f02865499ee314d55841e9e3259fad8c9edcb74c6fb3937e508eb2ccbdebe9b49c6870fe1e8984a466410aa61f0ea7a6b03ef2dfbbe1f1eeabadd752'
6
+ metadata.gz: 99a571060b7ab3971fff6ee6b0e8c67db21df52cc0ae00dbc57da0a26517aa94c8b63e8e606926bccb07a83c7d92e5f2a639495daf0c530f6bc6fe5b2189abfb
7
+ data.tar.gz: 785a8211a25f007e77b961868208081065b0f04e4d962382aa4b644acb757a6f6fc37b9c48e06b032e637906dcebac7127fabdfb0599a44e2719d65811bb7a3d
data/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  ![GitHub last commit](https://img.shields.io/github/last-commit/MarioRuiz/create_tests)
9
9
  ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/MarioRuiz/create_tests)
10
10
 
11
- Create RSpec tests automatically from Request Hash files. Perfect to be used with the result from importing a Swagger / OpenAPI file using the [open_api_import](https://github.com/MarioRuiz/open_api_import) gem.
11
+ Create RSpec or Minitest tests automatically from Request Hash files. Perfect to be used with the result from importing a Swagger / OpenAPI file using the [open_api_import](https://github.com/MarioRuiz/open_api_import) gem.
12
12
 
13
13
  We strongly recommend using [nice_http](https://github.com/MarioRuiz/nice_http) for your tests together with [nice_hash](https://github.com/MarioRuiz/nice_hash) and [string_pattern](https://github.com/MarioRuiz/string_pattern).
14
14
 
@@ -29,8 +29,8 @@ The gem uses [rufo](https://github.com/ruby-formatter/rufo) to format generated
29
29
  After installation you can run using the command line executable or from Ruby code.
30
30
 
31
31
  The execution will create:
32
- - A `spec/` folder with one RSpec test file per API endpoint
33
- - A `spec/helper.rb` with stub methods for required parameters
32
+ - A `spec/` folder with one RSpec test file per API endpoint (or `test/` with `*_test.rb` when using Minitest)
33
+ - A `spec/helper.rb` (or `test/helper.rb`) with stub methods for required parameters
34
34
  - A `settings/general.rb` with NiceHttp configuration boilerplate
35
35
 
36
36
  ### Command line
@@ -44,6 +44,10 @@ Usage: create_tests [requests_file] [options]
44
44
  -d, --dry_run Preview which files would be created or modified
45
45
  --cleanup_each Generate cleanup in after(:each) instead of after(:all)
46
46
  --no_cleanup Do not generate setup/cleanup hooks
47
+ --spec_dir DIR Output directory for test files (default: ./spec, or ./test with --minitest)
48
+ --settings_dir DIR Output directory for settings (default: ./settings)
49
+ --only KIND Generate only these test kinds (repeatable or comma-separated)
50
+ --minitest Generate Minitest tests instead of RSpec
47
51
  -v, --version Show version
48
52
  ```
49
53
 
@@ -71,7 +75,7 @@ CreateTests.from "./requests/uber.yaml.rb"
71
75
 
72
76
  For each API endpoint, `create_tests` generates up to 7 test types:
73
77
 
74
- 1. **"has correct structure in successful response"** -- Calls the endpoint and validates the response structure using `NiceHttp.validate_response` with diff details on failure.
78
+ 1. **"has correct structure in successful response"** -- Calls the endpoint and validates the response structure using `NiceHttp.validate_response` with diff details on failure. The expected status is the first 2xx response key, else `default`, else the first key. When `:data_default` is present, missing (or nil) keys in `:data` are filled before the call; existing example values are not overwritten.
75
79
 
76
80
  2. **"doesn't retrieve data if not authenticated"** -- Calls with empty headers and asserts a 4xx response.
77
81
 
@@ -81,9 +85,9 @@ For each API endpoint, `create_tests` generates up to 7 test types:
81
85
 
82
86
  5. **"handles multiple valid data variations"** -- For POST/PUT/PATCH endpoints with a payload source, uses `generate_n(5, :correct)` from nice_hash to test 5 different valid payload variations. Prefers `:data_pattern` when present (real string_pattern / enum / range values from open_api_import); otherwise uses `:data`. Generated values are assigned to `request[:data]` before the call.
83
87
 
84
- 6. **"returns error when individual data fields are invalid"** -- Uses `NiceHash.change_one_by_one` on `:data_pattern` (preferred) or `:data` to systematically test each field being wrong one at a time, asserting the server rejects each one.
88
+ 6. **"returns error when individual data fields are invalid"** -- Uses `NiceHash.change_one_by_one` on `:data_pattern` (preferred) or `:data` to systematically test each field being wrong one at a time, asserting the server rejects each one. Fields listed in `:data_read_only` are skipped.
85
89
 
86
- 7. **"returns error if required parameter on data empty/missing"** -- For endpoints with `data_required`, tests both empty and missing values for each required data field. When `:data` is absent but `:data_examples` is present (common for form uploads), the first example is used as the body for success and required-data tests.
90
+ 7. **"returns error if required parameter on data empty/missing"** -- For endpoints with `data_required`, tests both empty and missing values for each required data field. When `:data` is absent but `:data_examples` is present (common for form uploads), the first example is used as the body for success and required-data tests. Otherwise `:data_default` can seed missing body fields the same way as the success example.
87
91
 
88
92
  Required keyword parameters from open_api_import `create_constants: true` (e.g. `def self.get_item(id: ID)`) get Helper stubs and are called as keywords (`id: @id`), same as positional required args.
89
93
 
@@ -93,7 +97,7 @@ Required keyword parameters from open_api_import `create_constants: true` (e.g.
93
97
 
94
98
  Accepts three options: `:overwrite`, `:dont_overwrite` and `:append`. Default: `:append`.
95
99
 
96
- - **append**: If the test file already exists, only missing tests will be added. Existing content is preserved, including `helper/setup.rb` and `helper/cleanup.rb`. If the file doesn't exist, it will be created with all tests.
100
+ - **append**: If the test file already exists, only missing tests will be added. Existing content is preserved. For `helper/setup.rb` and `helper/cleanup.rb`, only missing `setup_*` / `cleanup_*` methods are inserted; existing method bodies are left alone. If every method already exists, the file is not rewritten. If the file doesn't exist, it will be created with all tests.
97
101
 
98
102
  - **dont_overwrite**: If the test file exists, no changes will be made. If it doesn't exist, it will be created.
99
103
 
@@ -122,12 +126,40 @@ result.each { |path, content| puts "#{path}: #{content.length} bytes" }
122
126
 
123
127
  ### spec_dir / settings_dir
124
128
 
125
- Custom output directories (default: `./spec` and `./settings`).
129
+ Custom output directories (default: `./spec` and `./settings`). With `test: :minitest`, the default test directory is `./test` unless you pass an explicit `spec_dir`.
126
130
 
127
131
  ```ruby
128
132
  CreateTests.from "./requests/uber.yaml.rb", spec_dir: "./test/spec", settings_dir: "./test/config"
129
133
  ```
130
134
 
135
+ ### only
136
+
137
+ Limit which example kinds are generated. Default `nil` means all kinds. Accepts a symbol, an array, or a comma-separated string. Unknown kinds raise.
138
+
139
+ Kinds: `success`, `auth`, `required_params`, `mock`, `variations`, `invalid_fields`, `required_data`.
140
+
141
+ ```ruby
142
+ CreateTests.from "./requests/uber.yaml.rb", only: [:success, :auth]
143
+ CreateTests.from "./requests/uber.yaml.rb", only: "success,mock"
144
+ ```
145
+
146
+ ```bash
147
+ create_tests ./requests/uber.yaml.rb --only success --only auth
148
+ create_tests ./requests/uber.yaml.rb --only success,auth
149
+ ```
150
+
151
+ ### test / Minitest
152
+
153
+ Default is `:rspec`. Pass `test: :minitest` (or `--minitest`) to generate Minitest files (`*_test.rb`) with `setup` / `teardown` instead of RSpec hooks, and `assert_*` / `refute_*` instead of `expect`.
154
+
155
+ ```ruby
156
+ CreateTests.from "./requests/uber.yaml.rb", test: :minitest
157
+ ```
158
+
159
+ ```bash
160
+ create_tests ./requests/uber.yaml.rb --minitest
161
+ ```
162
+
131
163
  ### cleanup
132
164
 
133
165
  Controls whether and how prerequisite setup/cleanup hooks are generated. Default: `:after_all`.
@@ -145,7 +177,7 @@ CreateTests.from "./requests/netapp.json.rb", cleanup: false # no cleanup
145
177
  ```ruby
146
178
  before(:all) do
147
179
  @http = NiceHttp.new()
148
- @volume_name = @volume_name + "-cre"
180
+ @volume_name = @volume_name + "-createorupda"
149
181
  expect(Helper.setup_capacity_pools(@http, ...).state).to eq "Succeeded"
150
182
  @request = Volumes.create_or_update(...)
151
183
  end
@@ -160,7 +192,7 @@ end
160
192
 
161
193
  ```ruby
162
194
  before(:all) do
163
- @snapshot_name = @snapshot_name + "-del"
195
+ @snapshot_name = @snapshot_name + "-delete"
164
196
  expect(Helper.setup_volumes(@http, ...).state).to eq "Succeeded"
165
197
  @request = Snapshots.delete(...)
166
198
  end
data/bin/create_tests CHANGED
@@ -37,6 +37,23 @@ optparse = OptionParser.new do |opts|
37
37
  options[:cleanup] = false
38
38
  end
39
39
 
40
+ opts.on("--spec_dir DIR", "Output directory for generated test files (default: ./spec, or ./test with --minitest)") do |dir|
41
+ options[:spec_dir] = dir
42
+ end
43
+
44
+ opts.on("--settings_dir DIR", "Output directory for generated settings files (default: ./settings)") do |dir|
45
+ options[:settings_dir] = dir
46
+ end
47
+
48
+ opts.on("--only KIND", "Generate only these test kinds (repeatable or comma-separated). Kinds: success, auth, required_params, mock, variations, invalid_fields, required_data") do |kind|
49
+ options[:only] ||= []
50
+ options[:only].concat(kind.split(",").map { |s| s.strip.to_sym })
51
+ end
52
+
53
+ opts.on("--minitest", "Generate Minitest tests instead of RSpec") do
54
+ options[:test] = :minitest
55
+ end
56
+
40
57
  opts.on("-v", "--version", "Show version") do
41
58
  puts "create_tests #{CreateTests::VERSION}"
42
59
  exit
@@ -125,5 +125,71 @@ class CreateTests
125
125
  end
126
126
  best
127
127
  end
128
+
129
+ # Append only missing setup_/cleanup_ methods from generated content into an
130
+ # existing file. Existing method bodies are left untouched.
131
+ # @return [Array(String, Boolean)] merged content and whether anything was added
132
+ private def append_missing_chain_methods(existing_txt, generated_txt)
133
+ method_blocks = extract_chain_method_blocks(generated_txt)
134
+ missing = method_blocks.reject { |blk| existing_txt.include?("def self.#{blk[:name]}(") }
135
+ return [existing_txt, false] if missing.empty?
136
+
137
+ output = existing_txt.dup
138
+ insert = missing.map { |blk| blk[:text] }.join
139
+
140
+ if output.match?(/\bmodule\s+Helper\b/)
141
+ if output.match?(/\s*end\s*\Z/)
142
+ output.sub!(/\s*end\s*\Z/, "\n#{insert}\nend\n")
143
+ else
144
+ output << "\n#{insert}"
145
+ end
146
+ else
147
+ # Preserve leading require (e.g. ostruct) from generated content when wrapping.
148
+ require_line = generated_txt[/^require[^\n]*\n/]
149
+ prefix = require_line && !output.include?(require_line.strip) ? require_line : ""
150
+ output = "#{prefix}module Helper\n#{output.chomp}\n#{insert}\nend\n"
151
+ end
152
+
153
+ [output, true]
154
+ end
155
+
156
+ private def extract_chain_method_blocks(generated_txt)
157
+ blocks = []
158
+ lines = generated_txt.lines
159
+ i = 0
160
+ while i < lines.size
161
+ if lines[i] =~ /\A#/
162
+ comment_start = i
163
+ i += 1 while i < lines.size && lines[i] =~ /\A#/
164
+ if i < lines.size && lines[i] =~ /\Adef self\.(setup_\w+|cleanup_\w+)\(/
165
+ name = $1
166
+ start = comment_start
167
+ i += 1
168
+ depth = 1
169
+ while i < lines.size && depth > 0
170
+ depth += 1 if lines[i] =~ /\A\s*def\b/
171
+ depth -= 1 if lines[i] =~ /\Aend\b/
172
+ i += 1
173
+ end
174
+ blocks << { name: name, text: lines[start...i].join }
175
+ next
176
+ end
177
+ elsif lines[i] =~ /\Adef self\.(setup_\w+|cleanup_\w+)\(/
178
+ name = $1
179
+ start = i
180
+ i += 1
181
+ depth = 1
182
+ while i < lines.size && depth > 0
183
+ depth += 1 if lines[i] =~ /\A\s*def\b/
184
+ depth -= 1 if lines[i] =~ /\Aend\b/
185
+ i += 1
186
+ end
187
+ blocks << { name: name, text: lines[start...i].join }
188
+ next
189
+ end
190
+ i += 1
191
+ end
192
+ blocks
193
+ end
128
194
  end
129
195
  end
@@ -1,16 +1,22 @@
1
1
 
2
2
  class CreateTests
3
3
 
4
+ TEST_KINDS = %i[
5
+ success auth required_params mock variations invalid_fields required_data
6
+ ].freeze
7
+
4
8
  class << self
5
9
 
6
10
  # Return true or false and the test source code
7
- private def create_test(module_txt, method_txt, method_obj, test_txt, settings_relative_path: '../../settings/general', cleanup: :after_all)
11
+ private def create_test(module_txt, method_txt, method_obj, test_txt, settings_relative_path: '../../settings/general', cleanup: :after_all, test: :rspec, only: nil)
8
12
  unless cleanup == false || [:after_all, :after_each].include?(cleanup)
9
13
  message = "** Wrong cleanup parameter: #{cleanup}"
10
14
  raise message
11
15
  end
12
16
 
13
- modified = false
17
+ only_kinds = normalize_only(only)
18
+
19
+ modified = false
14
20
  mod_name = module_txt.scan(/::(\w+)$/).join
15
21
  req_txt = "#{method_txt}("
16
22
  params = []
@@ -38,7 +44,7 @@ class CreateTests
38
44
 
39
45
  req_txt = "#{mod_name}.#{req_txt}"
40
46
  params_declaration_txt = ""
41
- ## todo: add param in case of :append
47
+ ## todo: add param in case of :append
42
48
  params.each do |p|
43
49
  params_declaration_txt << "#{p} = Helper.#{p.gsub('@','')}(@http)\n"
44
50
  @params << p
@@ -58,8 +64,7 @@ class CreateTests
58
64
  needs_target_setup = target_resource && [:delete, :get, :patch].include?(request[:method])
59
65
  parent_resource = resource_chain.last
60
66
 
61
- method_suffix = method_txt.to_s.split("_").map { |w| w[0..2] }.join[0..2]
62
- method_suffix = "-#{method_suffix}"
67
+ method_suffix = "-#{method_txt.to_s.gsub('_', '')[0, 12]}"
63
68
 
64
69
  setup_before_all_txt = ""
65
70
  setup_before_each_txt = ""
@@ -67,33 +72,39 @@ class CreateTests
67
72
  after_all_txt = ""
68
73
 
69
74
  if is_delete && target_resource && parent_resource
70
- setup_before_all_txt = "expect(Helper.setup_#{parent_resource[:resource]}(@http, #{parent_resource[:params_up_to].join(', ')}).state).to eq 'Succeeded'\n"
71
- setup_before_each_txt = "expect(Helper.setup_#{target_resource[:resource]}(@http, #{target_resource[:params_up_to].join(', ')}).state).to eq 'Succeeded'\n"
75
+ setup_before_all_txt = "#{assert_eq("Helper.setup_#{parent_resource[:resource]}(@http, #{parent_resource[:params_up_to].join(', ')}).state", "'Succeeded'", test)}\n"
76
+ setup_before_each_txt = "#{assert_eq("Helper.setup_#{target_resource[:resource]}(@http, #{target_resource[:params_up_to].join(', ')}).state", "'Succeeded'", test)}\n"
72
77
  if cleanup
73
- after_each_txt = "after(:each) do\nHelper.cleanup_#{target_resource[:resource]}(@http, #{target_resource[:params_up_to].join(', ')})\nend\n"
74
- after_all_txt = "after(:all) do\nunless defined?(DONT_DELETE) && DONT_DELETE\nHelper.cleanup_#{parent_resource[:resource]}(@http, #{parent_resource[:params_up_to].join(', ')})\nend\nend\n"
78
+ after_each_txt = framework_hook(:after_each, "Helper.cleanup_#{target_resource[:resource]}(@http, #{target_resource[:params_up_to].join(', ')})\n", test)
79
+ after_all_txt = framework_hook(:after_all, "unless defined?(DONT_DELETE) && DONT_DELETE\nHelper.cleanup_#{parent_resource[:resource]}(@http, #{parent_resource[:params_up_to].join(', ')})\nend\n", test)
75
80
  end
76
81
  elsif needs_target_setup && target_resource
77
- setup_before_all_txt = "expect(Helper.setup_#{target_resource[:resource]}(@http, #{target_resource[:params_up_to].join(', ')}).state).to eq 'Succeeded'\n"
82
+ setup_before_all_txt = "#{assert_eq("Helper.setup_#{target_resource[:resource]}(@http, #{target_resource[:params_up_to].join(', ')}).state", "'Succeeded'", test)}\n"
78
83
  if cleanup
79
- after_all_txt = "after(:all) do\nunless defined?(DONT_DELETE) && DONT_DELETE\nHelper.cleanup_#{target_resource[:resource]}(@http, #{target_resource[:params_up_to].join(', ')})\nend\nend\n"
84
+ after_all_txt = framework_hook(:after_all, "unless defined?(DONT_DELETE) && DONT_DELETE\nHelper.cleanup_#{target_resource[:resource]}(@http, #{target_resource[:params_up_to].join(', ')})\nend\n", test)
80
85
  end
81
86
  elsif parent_resource
82
- setup_before_all_txt = "expect(Helper.setup_#{parent_resource[:resource]}(@http, #{parent_resource[:params_up_to].join(', ')}).state).to eq 'Succeeded'\n"
87
+ setup_before_all_txt = "#{assert_eq("Helper.setup_#{parent_resource[:resource]}(@http, #{parent_resource[:params_up_to].join(', ')}).state", "'Succeeded'", test)}\n"
83
88
  if cleanup && target_resource
84
- after_each_txt = "after(:each) do\nHelper.cleanup_#{target_resource[:resource]}(@http, #{target_resource[:params_up_to].join(', ')})\nend\n"
85
- after_all_txt = "after(:all) do\nunless defined?(DONT_DELETE) && DONT_DELETE\nHelper.cleanup_#{parent_resource[:resource]}(@http, #{parent_resource[:params_up_to].join(', ')})\nend\nend\n"
89
+ after_each_txt = framework_hook(:after_each, "Helper.cleanup_#{target_resource[:resource]}(@http, #{target_resource[:params_up_to].join(', ')})\n", test)
90
+ after_all_txt = framework_hook(:after_all, "unless defined?(DONT_DELETE) && DONT_DELETE\nHelper.cleanup_#{parent_resource[:resource]}(@http, #{parent_resource[:params_up_to].join(', ')})\nend\n", test)
86
91
  elsif cleanup
87
- after_all_txt = "after(:all) do\nunless defined?(DONT_DELETE) && DONT_DELETE\nHelper.cleanup_#{parent_resource[:resource]}(@http, #{parent_resource[:params_up_to].join(', ')})\nend\nend\n"
92
+ after_all_txt = framework_hook(:after_all, "unless defined?(DONT_DELETE) && DONT_DELETE\nHelper.cleanup_#{parent_resource[:resource]}(@http, #{parent_resource[:params_up_to].join(', ')})\nend\n", test)
88
93
  end
89
94
  end
90
95
 
91
96
  if cleanup == :after_each && !after_all_txt.empty?
92
- after_each_txt += after_all_txt.sub("after(:all)", "after(:each)")
93
- after_all_txt = ""
97
+ if test == :minitest
98
+ # Minitest folds after(:all) into teardown; after_each mode just uses teardown body once.
99
+ after_each_txt += after_all_txt
100
+ after_all_txt = ""
101
+ else
102
+ after_each_txt += after_all_txt.sub("after(:all)", "after(:each)")
103
+ after_all_txt = ""
104
+ end
94
105
  end
95
106
 
96
- if test_txt ==""
107
+ if test_txt == ""
97
108
  modified = true
98
109
 
99
110
  suffix_txt = ""
@@ -102,12 +113,15 @@ class CreateTests
102
113
  suffix_txt = "#{target_param} = #{target_param} + \"#{method_suffix}\"\n"
103
114
  end
104
115
 
105
- before_each_block = "before(:each) do |example|\n"
106
- before_each_block << "@http = NiceHttp.new()\n" if is_delete && target_resource
107
- before_each_block << setup_before_each_txt
108
- before_each_block << "@http.logger.info(\"\\n\\n\#{'='*100}\\nTest: \#{example.description}\\n\#{'-'*100}\")\nend\n"
116
+ if test == :minitest
117
+ output = build_minitest_skeleton(mod_name, method_txt, settings_relative_path, params_declaration_txt, suffix_txt, setup_before_all_txt, setup_before_each_txt, after_each_txt, after_all_txt, req_txt, is_delete, target_resource)
118
+ else
119
+ before_each_block = "before(:each) do |example|\n"
120
+ before_each_block << "@http = NiceHttp.new()\n" if is_delete && target_resource
121
+ before_each_block << setup_before_each_txt
122
+ before_each_block << "@http.logger.info(\"\\n\\n\#{'='*100}\\nTest: \#{example.description}\\n\#{'-'*100}\")\nend\n"
109
123
 
110
- output = "
124
+ output = "
111
125
  require_relative '#{settings_relative_path}'
112
126
  if defined?(#{mod_name}) and defined?(#{mod_name}.#{method_txt})
113
127
  RSpec.describe #{mod_name}, '##{method_txt}' do
@@ -117,12 +131,15 @@ class CreateTests
117
131
  @http.logger.info(\"\\n\#{'+'*50} Before All ends \#{'+'*50}\")
118
132
  end
119
133
  #{after_all_txt}#{after_each_txt}#{before_each_block}\n"
134
+ end
120
135
  else
121
136
  output = test_txt
122
- if output.match?(/\s*end\s*end\s*end\s*\Z/)
123
- output.gsub!(/\s*end\s*end\s*\Z/,"\n")
137
+ if test == :minitest
138
+ output.gsub!(/\s*end\s*\Z/, "\n")
139
+ elsif output.match?(/\s*end\s*end\s*end\s*\Z/)
140
+ output.gsub!(/\s*end\s*end\s*\Z/, "\n")
124
141
  else
125
- output.gsub!(/\s*end\s*\Z/,"\n")
142
+ output.gsub!(/\s*end\s*\Z/, "\n")
126
143
  end
127
144
  end
128
145
 
@@ -132,6 +149,16 @@ class CreateTests
132
149
  has_data_examples = request.key?(:data_examples) &&
133
150
  request[:data_examples].is_a?(Array) &&
134
151
  !request[:data_examples].empty?
152
+ has_data_default = request.key?(:data_default) &&
153
+ request[:data_default].is_a?(Hash) &&
154
+ !request[:data_default].empty?
155
+ # Prefer data_examples over defaults when :data is absent
156
+ apply_data_default = has_data_default && (request.key?(:data) || !has_data_examples)
157
+ apply_defaults_txt = if apply_data_default
158
+ "@request[:data] ||= {}\n @request[:data_default].each do |k, v|\n @request[:data][k] = v if !@request[:data].key?(k) || @request[:data][k].nil?\n end\n "
159
+ else
160
+ ""
161
+ end
135
162
  # Prefer data_pattern for variation/invalid-field tests; fall back to :data
136
163
  payload_source = if request.key?(:data_pattern)
137
164
  ":data_pattern"
@@ -139,116 +166,152 @@ class CreateTests
139
166
  ":data"
140
167
  end
141
168
 
142
- # first response on responses is the one expected to be returned when success
143
- if request.key?(:responses) and request[:responses].size > 0
144
- code = request[:responses].keys[0]
145
- title="it 'has correct structure in successful response' "
169
+ # Success code: first 2xx key, else "default", else first key
170
+ if include_kind?(only_kinds, :success) && request.key?(:responses) and request[:responses].size > 0
171
+ code = success_response_code(request[:responses])
172
+ code_src = format_response_code(code)
173
+ title = example_title("has correct structure in successful response", test, trailing_space: true)
146
174
  if !request.key?(:data) && has_data_examples
147
175
  tests[title] = "do
148
176
  request = @request.deep_copy
149
177
  request[:data] = @request[:data_examples].first
150
178
  resp = @http.#{request[:method]}(request)
151
- expect(resp.code).to eq #{code}\n"
179
+ #{assert_eq('resp.code', code_src, test)}\n"
152
180
  if request[:responses][code].is_a?(Hash) and request[:responses][code].key?(:data)
153
181
  tests[title] +="result = NiceHttp.validate_response(resp, @request.responses._#{code}.data, include_diff: true)\n"
154
- tests[title] +="expect(result[:ok]).to be(true), \"Structure mismatch: \#{result[:diff]}\"\n"
182
+ tests[title] += "#{assert_be_true('result[:ok]', 'Structure mismatch: #{result[:diff]}', test)}\n"
155
183
  end
156
184
  tests[title] += "end\n"
157
185
  else
158
186
  tests[title] = "do
159
- resp = @http.#{request[:method]}(@request)
160
- expect(resp.code).to eq #{code}\n"
187
+ #{apply_defaults_txt}resp = @http.#{request[:method]}(@request)
188
+ #{assert_eq('resp.code', code_src, test)}\n"
161
189
 
162
190
  if request[:responses][code].is_a?(Hash) and request[:responses][code].key?(:data)
163
191
  tests[title] +="result = NiceHttp.validate_response(resp, @request.responses._#{code}.data, include_diff: true)\n"
164
- tests[title] +="expect(result[:ok]).to be(true), \"Structure mismatch: \#{result[:diff]}\"\n"
192
+ tests[title] += "#{assert_be_true('result[:ok]', 'Structure mismatch: #{result[:diff]}', test)}\n"
165
193
  end
166
194
  tests[title] += "end\n"
167
195
  end
168
196
  end
169
197
 
170
- title = "it 'doesn\\'t retrieve data if not authenticated'"
171
- tests[title] = "do
198
+ if include_kind?(only_kinds, :auth)
199
+ title = example_title("doesn\\'t retrieve data if not authenticated", test)
200
+ tests[title] = "do
172
201
  http = NiceHttp.new()
173
202
  http.headers = {}
174
203
  resp = http.#{request[:method]}(@request)
175
- expect(resp.code).to be_between('400', '499')\n"
176
- if request.key?(:responses) and (request[:responses].keys.select{|c| c.to_s.to_i>=400&&c.to_s.to_i<=499}).size>0
177
- tests[title] += "expect(NiceHash.compare_structure(@request.responses[resp.code.to_sym].data, resp.data.json)).to eq true
178
- expect(resp.message).to eq @request.responses[resp.code.to_sym].message\n"
204
+ #{assert_between('resp.code', "'400'", "'499'", test)}\n"
205
+ if request.key?(:responses) and (request[:responses].keys.select{|c| c.to_s.to_i>=400&&c.to_s.to_i<=499}).size>0
206
+ tests[title] += "#{assert_eq('NiceHash.compare_structure(@request.responses[resp.code.to_sym].data, resp.data.json)', 'true', test)}
207
+ #{assert_eq('resp.message', '@request.responses[resp.code.to_sym].message', test)}\n"
208
+ end
209
+ tests[title] += "end\n"
179
210
  end
180
- tests[title] += "end\n"
181
-
182
- if params.size > 0 or keywords_required.size >0
211
+
212
+ if include_kind?(only_kinds, :required_params) && (params.size > 0 or keywords_required.size >0)
183
213
  empty_param = ""
184
214
  params.each do |p|
185
215
  r = req_txt.gsub(/#{p}([),])/, '""\1')
186
216
  empty_param += "
187
217
  request = #{r}
188
218
  resp = @http.#{request[:method]}(request)
189
- expect(resp.code).to be_between('400', '499')\n"
219
+ #{assert_between('resp.code', "'400'", "'499'", test)}\n"
190
220
  if request.key?(:responses) and (request[:responses].keys.select{|c| c.to_s.to_i>=400&&c.to_s.to_i<=499}).size>0
191
- empty_param += "expect(resp.message).to match /\#{request.responses[resp.code.to_sym].message}/i\n"
221
+ empty_param += "#{assert_match('resp.message', '/\#{request.responses[resp.code.to_sym].message}/i', test)}\n"
192
222
  end
193
223
  end
194
- if keywords_required.size > 0
224
+ if keywords_required.size > 0
195
225
  r = req_txt.scan(/(.+)\(/).join
196
226
  empty_param += "
197
227
  [:#{keywords_required.join(', :')}].each do |kw|
198
228
  request = #{r}(kw => '')
199
229
  resp = @http.#{request[:method]}(request)
200
- expect(resp.code).to be_between('400', '499')\n"
230
+ #{assert_between('resp.code', "'400'", "'499'", test)}\n"
201
231
  if request.key?(:responses) and (request[:responses].keys.select{|c| c.to_s.to_i>=400&&c.to_s.to_i<=499}).size>0
202
- empty_param += "expect(resp.message).to match /\#{request.responses[resp.code.to_sym].message}/i\n"
232
+ empty_param += "#{assert_match('resp.message', '/\#{request.responses[resp.code.to_sym].message}/i', test)}\n"
203
233
  end
204
234
  empty_param += "end\n"
205
235
  end
206
236
  empty_param += "end\n"
207
- tests["it 'returns error if required parameter empty' "] = "do\n#{empty_param}"
237
+ tests[example_title("returns error if required parameter empty", test, trailing_space: true)] = "do\n#{empty_param}"
208
238
  end
209
239
 
210
- if request.key?(:mock_response)
211
- title = "it 'returns expected mock response' "
240
+ if include_kind?(only_kinds, :mock) && request.key?(:mock_response)
241
+ title = example_title("returns expected mock response", test, trailing_space: true)
212
242
  tests[title] = "do
213
243
  @http_mock = NiceHttp.new()
214
244
  @http_mock.use_mocks = true
215
245
  resp = @http_mock.#{request[:method]}(@request)
216
- expect(resp.code).to eq @request[:mock_response][:code]
217
- expect(resp.message).to eq @request[:mock_response][:message]\n"
246
+ #{assert_eq('resp.code', '@request[:mock_response][:code]', test)}
247
+ #{assert_eq('resp.message', '@request[:mock_response][:message]', test)}\n"
218
248
  if request[:mock_response].key?(:data)
219
- tests[title] += "expect(NiceHash.compare_structure(@request[:mock_response][:data], resp.data.json, true)).to be true\n"
249
+ tests[title] += "#{assert_be_true('NiceHash.compare_structure(@request[:mock_response][:data], resp.data.json, true)', nil, test)}\n"
220
250
  end
221
251
  tests[title] += "end\n"
222
252
  end
223
253
 
224
- if payload_source && [:post, :put, :patch].include?(request[:method])
225
- title = "it 'handles multiple valid data variations' "
254
+ if include_kind?(only_kinds, :variations) && payload_source && [:post, :put, :patch].include?(request[:method])
255
+ title = example_title("handles multiple valid data variations", test, trailing_space: true)
226
256
  tests[title] = "do
227
257
  @request[#{payload_source}].generate_n(5, :correct).each do |generated_data|
228
258
  request = @request.deep_copy
229
259
  request[:data] = generated_data
230
260
  resp = @http.#{request[:method]}(request)
231
- expect(resp.code.to_i).to be_between(200, 299)
261
+ #{assert_between('resp.code.to_i', '200', '299', test)}
232
262
  end
233
263
  end\n"
234
264
  end
235
265
 
236
- if payload_source
237
- title = "it 'returns error when individual data fields are invalid' "
238
- tests[title] = "do
266
+ if include_kind?(only_kinds, :invalid_fields) && payload_source
267
+ payload_key = payload_source == ":data_pattern" ? :data_pattern : :data
268
+ read_only_names = Array(request[:data_read_only]).map(&:to_s)
269
+ payload_for_invalid = request[payload_key]
270
+ if payload_for_invalid.is_a?(Hash) && !read_only_names.empty?
271
+ payload_for_invalid = payload_for_invalid.deep_copy
272
+ read_only_names.each do |name|
273
+ payload_for_invalid.delete(name.to_sym)
274
+ payload_for_invalid.delete(name)
275
+ end
276
+ end
277
+ unless payload_for_invalid.nil? || (payload_for_invalid.respond_to?(:empty?) && payload_for_invalid.empty?)
278
+ title = example_title("returns error when individual data fields are invalid", test, trailing_space: true)
279
+ if read_only_names.empty?
280
+ tests[title] = "do
239
281
  wrong = @request[#{payload_source}].generate(:correct, errors: :min_length)
240
282
  NiceHash.change_one_by_one([@request[#{payload_source}], :correct], wrong).each do |one_wrong|
241
283
  request = @request.deep_copy
242
284
  request[:data] = one_wrong
243
285
  resp = @http.#{request[:method]}(request)
244
- expect(resp.code.to_i).not_to be_between(200, 299)
286
+ #{assert_between('resp.code.to_i', '200', '299', test, negate: true)}
245
287
  end
246
288
  end\n"
289
+ else
290
+ read_only_lit = request[:data_read_only].map(&:inspect).join(", ")
291
+ tests[title] = "do
292
+ payload = @request[#{payload_source}].deep_copy
293
+ [#{read_only_lit}].each do |f|
294
+ payload.delete(f)
295
+ payload.delete(f.to_s)
296
+ payload.delete(f.to_s.to_sym)
297
+ end
298
+ wrong = payload.generate(:correct, errors: :min_length)
299
+ NiceHash.change_one_by_one([payload, :correct], wrong).each do |one_wrong|
300
+ request = @request.deep_copy
301
+ request[:data] = one_wrong
302
+ resp = @http.#{request[:method]}(request)
303
+ #{assert_between('resp.code.to_i', '200', '299', test, negate: true)}
304
+ end
305
+ end\n"
306
+ end
307
+ end
247
308
  end
248
309
 
249
- if request.key?(:data_required) && (request.key?(:data) || has_data_examples)
310
+ if include_kind?(only_kinds, :required_data) && request.key?(:data_required) && (request.key?(:data) || has_data_examples || apply_data_default)
250
311
  seed_data_line = if !request.key?(:data) && has_data_examples
251
312
  "request[:data] = request[:data_examples].first\n "
313
+ elsif apply_data_default
314
+ "request[:data] ||= {}\n request[:data_default].each do |k, v|\n request[:data][k] = v if !request[:data].key?(k) || request[:data][k].nil?\n end\n "
252
315
  else
253
316
  ""
254
317
  end
@@ -258,14 +321,14 @@ class CreateTests
258
321
  request = @request.deep_copy
259
322
  #{seed_data_line}request.values_for[p] = ''
260
323
  resp = @http.#{request[:method]}(request)
261
- expect(resp.code).not_to be_between('200', '299')
324
+ #{assert_between('resp.code', "'200'", "'299'", test, negate: true)}
262
325
  if request.responses.key?(resp.code.to_sym)
263
- expect(resp.message).to match /\#{request.responses[resp.code.to_sym].message}/i
326
+ #{assert_match('resp.message', '/\#{request.responses[resp.code.to_sym].message}/i', test)}
264
327
  end
265
328
  end
266
329
  "
267
330
  empty_param_data += "end\n"
268
- tests["it 'returns error if required parameter on data empty' do\n"] = empty_param_data
331
+ tests[example_title("returns error if required parameter on data empty", test, with_do: true)] = empty_param_data
269
332
 
270
333
  missing_param_data = ""
271
334
  missing_param_data += "
@@ -273,19 +336,18 @@ class CreateTests
273
336
  request = @request.deep_copy
274
337
  #{seed_data_line}NiceHash.delete_nested(request[:data], p)
275
338
  resp = @http.#{request[:method]}(request)
276
- expect(resp.code).not_to be_between('200', '299')
339
+ #{assert_between('resp.code', "'200'", "'299'", test, negate: true)}
277
340
  if request.responses.key?(resp.code.to_sym)
278
- expect(resp.message).to match /\#{request.responses[resp.code.to_sym].message}/i
341
+ #{assert_match('resp.message', '/\#{request.responses[resp.code.to_sym].message}/i', test)}
279
342
  end
280
343
  end
281
344
  "
282
345
  missing_param_data += "end\n"
283
- tests["it 'returns error if required parameter on data missing' do\n"] = missing_param_data
346
+ tests[example_title("returns error if required parameter on data missing", test, with_do: true)] = missing_param_data
284
347
  end
285
348
 
286
349
  tests.each do |k,v|
287
- unless output.include?(k.gsub(" '",' "').gsub("' ",'" ').gsub("\n",'').gsub(/\s+do$/,'').gsub(/^\s*it/,'')) or
288
- output.include?(k.gsub(' "'," '").gsub('" ',"' ").gsub("\n",'').gsub(/\s+do$/,'').gsub(/^\s*it/,''))
350
+ unless example_already_present?(output, k, test)
289
351
  modified = true
290
352
  message = " = test added #{k} for #{method_txt}"
291
353
  @logger.info message
@@ -293,15 +355,215 @@ class CreateTests
293
355
  puts message
294
356
  output +="# Appended #{Time.now.stamp}\n"
295
357
  end
296
- output += "#{k}#{v}"
358
+ if test == :minitest
359
+ # keys are "def test_foo" and bodies start with "do\n...\nend"
360
+ body = v.sub(/\Ado\n/, "").sub(/\n?end\n?\z/, "\n")
361
+ output += "#{k}\n#{body}end\n"
362
+ else
363
+ output += "#{k}#{v}"
364
+ end
297
365
  end
298
366
  end
299
- if output.include?('defined?(')
367
+ if test == :minitest
368
+ if output.include?('defined?(')
369
+ output += "\nend\nend"
370
+ else
371
+ output += "\nend"
372
+ end
373
+ elsif output.include?('defined?(')
300
374
  output += "\nend\nend"
301
375
  else
302
376
  output +="\nend"
303
377
  end
304
378
  return modified, output
305
379
  end
380
+
381
+ private def success_response_code(responses)
382
+ keys = responses.keys
383
+ twoxx = keys.find do |k|
384
+ n = k.to_s
385
+ n.match?(/\A\d+\z/) && (200..299).include?(n.to_i)
386
+ end
387
+ return twoxx if twoxx
388
+
389
+ default_key = keys.find { |k| k.to_s == "default" }
390
+ return default_key if default_key
391
+
392
+ keys[0]
393
+ end
394
+
395
+ # Numeric status codes stay unquoted (eq 200); non-numeric keys use inspect.
396
+ private def format_response_code(code)
397
+ s = code.to_s
398
+ if s.match?(/\A\d+\z/)
399
+ s
400
+ else
401
+ code.inspect
402
+ end
403
+ end
404
+
405
+ private def normalize_only(only)
406
+ return nil if only.nil?
407
+ list = case only
408
+ when Symbol then [only]
409
+ when String then only.split(",").map { |s| s.strip.to_sym }
410
+ when Array then only.map { |s| s.to_s.strip.to_sym }
411
+ else
412
+ raise "** Wrong only parameter: #{only}"
413
+ end
414
+ unknown = list - TEST_KINDS
415
+ unless unknown.empty?
416
+ raise "** Wrong only parameter: unknown kind(s) #{unknown.join(', ')}. Valid: #{TEST_KINDS.join(', ')}"
417
+ end
418
+ list
419
+ end
420
+
421
+ private def include_kind?(only_kinds, kind)
422
+ only_kinds.nil? || only_kinds.include?(kind)
423
+ end
424
+
425
+ private def example_title(description, test, trailing_space: false, with_do: false)
426
+ if test == :minitest
427
+ "def test_#{minitest_method_name(description)}"
428
+ else
429
+ space = trailing_space ? " " : ""
430
+ if with_do
431
+ "it '#{description}' do\n"
432
+ else
433
+ "it '#{description}'#{space}"
434
+ end
435
+ end
436
+ end
437
+
438
+ private def minitest_method_name(description)
439
+ description.gsub("\\'", "")
440
+ .gsub(/[^a-zA-Z0-9]+/, "_")
441
+ .gsub(/\A_+|_+\z/, "")
442
+ .downcase
443
+ end
444
+
445
+ private def example_already_present?(output, key, test)
446
+ if test == :minitest
447
+ output.include?(key)
448
+ else
449
+ output.include?(key.gsub(" '", ' "').gsub("' ", '" ').gsub("\n", '').gsub(/\s+do$/, '').gsub(/^\s*it/, '')) ||
450
+ output.include?(key.gsub(' "', " '").gsub('" ', "' ").gsub("\n", '').gsub(/\s+do$/, '').gsub(/^\s*it/, ''))
451
+ end
452
+ end
453
+
454
+ private def assert_eq(actual, expected, test)
455
+ if test == :minitest
456
+ "assert_equal #{expected}, #{actual}"
457
+ else
458
+ "expect(#{actual}).to eq #{expected}"
459
+ end
460
+ end
461
+
462
+ private def assert_be_true(actual, message, test)
463
+ if test == :minitest
464
+ if message
465
+ "assert #{actual}, \"#{message}\""
466
+ else
467
+ "assert #{actual}"
468
+ end
469
+ else
470
+ if message
471
+ "expect(#{actual}).to be(true), \"#{message}\""
472
+ else
473
+ "expect(#{actual}).to be true"
474
+ end
475
+ end
476
+ end
477
+
478
+ private def assert_between(actual, low, high, test, negate: false)
479
+ if test == :minitest
480
+ if negate
481
+ "refute_includes (#{low}..#{high}), #{actual}"
482
+ else
483
+ "assert_includes (#{low}..#{high}), #{actual}"
484
+ end
485
+ else
486
+ if negate
487
+ "expect(#{actual}).not_to be_between(#{low}, #{high})"
488
+ else
489
+ "expect(#{actual}).to be_between(#{low}, #{high})"
490
+ end
491
+ end
492
+ end
493
+
494
+ private def assert_match(actual, pattern, test)
495
+ if test == :minitest
496
+ "assert_match #{pattern}, #{actual}"
497
+ else
498
+ "expect(#{actual}).to match #{pattern}"
499
+ end
500
+ end
501
+
502
+ private def framework_hook(kind, body, test)
503
+ if test == :minitest
504
+ # Stored as raw bodies; assembled into setup/teardown in the skeleton.
505
+ # Prefix with a marker so cleanup: :after_each merging can find them.
506
+ case kind
507
+ when :after_each then "TEARDOWN_EACH:#{body}"
508
+ when :after_all then "TEARDOWN_ALL:#{body}"
509
+ else body
510
+ end
511
+ else
512
+ case kind
513
+ when :after_each then "after(:each) do\n#{body}end\n"
514
+ when :after_all then "after(:all) do\n#{body}end\n"
515
+ else body
516
+ end
517
+ end
518
+ end
519
+
520
+ private def build_minitest_skeleton(mod_name, method_txt, settings_relative_path, params_declaration_txt, suffix_txt, setup_before_all_txt, setup_before_each_txt, after_each_txt, after_all_txt, req_txt, is_delete, target_resource)
521
+ class_name = "#{mod_name}#{camelize(method_txt.to_s)}Test"
522
+
523
+ teardown_each_body = after_each_txt.to_s.gsub(/\ATEARDOWN_EACH:/, "").gsub(/\ATEARDOWN_ALL:/, "")
524
+ # after_each_txt may contain both TEARDOWN_EACH and (after merge) TEARDOWN_ALL markers
525
+ each_parts = []
526
+ all_parts = []
527
+ [after_each_txt.to_s, after_all_txt.to_s].each do |chunk|
528
+ chunk.split(/(?=TEARDOWN_(?:EACH|ALL):)/).each do |part|
529
+ next if part.empty?
530
+ if part.start_with?("TEARDOWN_EACH:")
531
+ each_parts << part.sub(/\ATEARDOWN_EACH:/, "")
532
+ elsif part.start_with?("TEARDOWN_ALL:")
533
+ all_parts << part.sub(/\ATEARDOWN_ALL:/, "")
534
+ end
535
+ end
536
+ end
537
+
538
+ teardown_body = (each_parts + all_parts).join
539
+
540
+ setup_http = if is_delete && target_resource
541
+ "@http = NiceHttp.new()\n"
542
+ else
543
+ ""
544
+ end
545
+
546
+ # before(:all) work at start of setup; before(:each) after that.
547
+ # First test creates @http for non-delete; delete recreates in the each section.
548
+ "
549
+ require 'minitest/autorun'
550
+ require_relative '#{settings_relative_path}'
551
+ if defined?(#{mod_name}) and defined?(#{mod_name}.#{method_txt})
552
+ class #{class_name} < Minitest::Test
553
+ def setup
554
+ @http = NiceHttp.new()
555
+ #{params_declaration_txt}#{suffix_txt}#{setup_before_all_txt}@request = #{req_txt}
556
+ @http.logger.info(\"\\n\#{'+'*50} Before All ends \#{'+'*50}\")
557
+ #{setup_http}#{setup_before_each_txt}@http.logger.info(\"\\n\\n\#{'='*100}\\nTest: \#{name}\\n\#{'-'*100}\")
558
+ end
559
+ def teardown
560
+ #{teardown_body}
561
+ end
562
+ "
563
+ end
564
+
565
+ private def camelize(snake)
566
+ snake.to_s.split("_").map(&:capitalize).join
567
+ end
306
568
  end
307
- end
569
+ end
@@ -6,18 +6,19 @@ class CreateTests
6
6
  # More info about Request Hashes: https://github.com/MarioRuiz/Request-Hash
7
7
  # @param requests_file [String]. Path and file name. Could be absolute or relative to project root folder.
8
8
  # @param type [Symbol]. (default :request_hash). The kind of content that requests_file contains.
9
- # @param test [Symbol]. (default :rspec). What kind of tests we want to create
9
+ # @param test [Symbol]. (default :rspec). What kind of tests we want to create (:rspec or :minitest).
10
10
  # @param mode [Symbol]. (default :append). :overwrite, :append, :dont_overwrite. How we want to create the tests.
11
11
  # :overwrite, it will overwrite the test, settings, helper... files in case they exist so you will loose your original source code.
12
12
  # :dont_overwrite, it will create only the files that don't exist previously
13
13
  # :append, it will append or create the tests or helpers that corrently don't exist on every file, but it won't modify any other code.
14
14
  # @param dry_run [Boolean]. (default false). Preview which files would be created or modified without writing anything.
15
15
  # @param return_data [Boolean]. (default false). Return a Hash with generated file paths as keys and content as values, without writing files.
16
- # @param spec_dir [String]. (default './spec'). Output directory for generated test files.
16
+ # @param spec_dir [String]. (default './spec' for RSpec, './test' for Minitest). Output directory for generated test files.
17
17
  # @param settings_dir [String]. (default './settings'). Output directory for generated settings files.
18
18
  # @param cleanup [Symbol, false]. (default :after_all). :after_all, :after_each, or false. Controls cleanup hook generation.
19
+ # @param only [Array, Symbol, String, nil]. (default nil = all). Limit which example kinds are generated.
19
20
  ##############################################################################
20
- def self.from(requests_file, type: :request_hash, test: :rspec, mode: :append, dry_run: false, return_data: false, spec_dir: './spec', settings_dir: './settings', cleanup: :after_all)
21
+ def self.from(requests_file, type: :request_hash, test: :rspec, mode: :append, dry_run: false, return_data: false, spec_dir: nil, settings_dir: './settings', cleanup: :after_all, only: nil)
21
22
  no_write = dry_run || return_data
22
23
  @generated_data = {} if return_data
23
24
 
@@ -50,12 +51,17 @@ class CreateTests
50
51
  raise message
51
52
  end
52
53
 
53
- unless [:rspec].include?(test)
54
+ unless [:rspec, :minitest].include?(test)
54
55
  message = "** Wrong test parameter: #{test}"
55
56
  @logger.fatal message
56
57
  raise message
57
58
  end
58
59
 
60
+ # Default output dir depends on framework; an explicit spec_dir is always honored.
61
+ if spec_dir.nil?
62
+ spec_dir = test == :minitest ? './test' : './spec'
63
+ end
64
+
59
65
  unless [:overwrite, :dont_overwrite, :append].include?(mode)
60
66
  message = "** Wrong mode parameter: #{mode}"
61
67
  @logger.fatal message
@@ -68,12 +74,16 @@ class CreateTests
68
74
  raise message
69
75
  end
70
76
 
77
+ # Validate only early so bad values fail before generation.
78
+ normalize_only(only)
79
+
71
80
  if mode == :overwrite
72
81
  message = "** Pay attention, if any of the files exist, will be overwritten"
73
82
  @logger.warn message
74
83
  warn message
75
84
  elsif mode == :append
76
- if Dir["#{spec_dir}/*/**_spec.rb"].size > 0
85
+ existing_tests = Dir["#{spec_dir}/*/**_spec.rb"] + Dir["#{spec_dir}/*/**_test.rb"]
86
+ if existing_tests.size > 0
77
87
  message = "** Pay attention, if any of the test files exist or the help file exist only will be added the tests, methods that are missing."
78
88
  @logger.warn message
79
89
  warn message
@@ -94,7 +104,7 @@ class CreateTests
94
104
  helper_from_settings = helper_path.relative_path_from(resolved_settings_dir).to_s
95
105
  requests_from_settings = resolve_pathname(requests_file).relative_path_from(resolved_settings_dir).to_s
96
106
 
97
- Dir.mkdir spec_dir unless no_write or test != :rspec or Dir.exist?(spec_dir)
107
+ Dir.mkdir spec_dir unless no_write or Dir.exist?(spec_dir)
98
108
 
99
109
  add_settings = true
100
110
  settings_file = "#{settings_dir}/general.rb"
@@ -164,6 +174,8 @@ class CreateTests
164
174
  puts message unless no_write
165
175
  end
166
176
 
177
+ test_suffix = test == :minitest ? "_test.rb" : "_spec.rb"
178
+
167
179
  modules.each do |mod_txt|
168
180
  mod_name = mod_txt.scan(/::(\w+)$/).join
169
181
  folder = "#{spec_dir}/#{mod_name}"
@@ -174,7 +186,7 @@ class CreateTests
174
186
  mod_obj = Object.const_get(mod_txt)
175
187
  mod_methods_txt = mod_obj.methods(false)
176
188
  mod_methods_txt.each do |method_txt|
177
- test_file = "#{folder}/#{method_txt}_spec.rb"
189
+ test_file = "#{folder}/#{method_txt}#{test_suffix}"
178
190
  if File.exist?(test_file) and mode==:dont_overwrite
179
191
  message = "** The file #{test_file} already exists so no content will be added to it.\n"
180
192
  message += " Remove the test file to be able to be generated by create_tests or set mode: :overwrite, or mode: :append"
@@ -187,7 +199,7 @@ class CreateTests
187
199
  test_txt = ''
188
200
  end
189
201
  settings_from_test = settings_path.relative_path_from(resolve_pathname(folder)).to_s
190
- modified, txt = create_test(mod_txt, method_txt, mod_obj.method(method_txt), test_txt, settings_relative_path: settings_from_test, cleanup: cleanup)
202
+ modified, txt = create_test(mod_txt, method_txt, mod_obj.method(method_txt), test_txt, settings_relative_path: settings_from_test, cleanup: cleanup, test: test, only: only)
191
203
  if no_write
192
204
  @generated_data[test_file] = txt if return_data
193
205
  else
@@ -198,7 +210,7 @@ class CreateTests
198
210
  message = "- Test #{no_write ? 'would be created' : 'created'}: #{test_file}"
199
211
  elsif modified
200
212
  message = "- Test #{no_write ? 'would be updated' : 'updated'}: #{test_file}"
201
- else
213
+ else
202
214
  message = "- Test without changes: #{test_file}"
203
215
  end
204
216
  @logger.info message
@@ -234,13 +246,36 @@ class CreateTests
234
246
 
235
247
  if @setup_cleanup_resources.size > 0
236
248
  write_chain_file = lambda do |path, content, label|
237
- if File.exist?(path) && mode != :overwrite
249
+ if File.exist?(path) && mode == :dont_overwrite
238
250
  message = "** The file #{path} already exists so it will be left unchanged.\n"
239
- message += " Remove the file to regenerate it or set mode: :overwrite"
251
+ message += " Remove the file to regenerate it or set mode: :overwrite or :append"
240
252
  @logger.warn message
241
253
  warn message
242
254
  return
243
255
  end
256
+
257
+ if File.exist?(path) && mode == :append
258
+ existing = File.read(path)
259
+ merged, changed = append_missing_chain_methods(existing, content)
260
+ unless changed
261
+ message = "- #{label} without changes: #{path}"
262
+ @logger.info message
263
+ return
264
+ end
265
+ content = merged
266
+ if no_write
267
+ @generated_data[path] = content if return_data
268
+ puts "- #{label} (dry run): #{path}" if dry_run
269
+ else
270
+ File.open(path, "w") { |file| file.write(content) }
271
+ system("rufo", path, out: File::NULL, err: File::NULL)
272
+ message = "- #{label} updated: #{path}"
273
+ @logger.info message
274
+ puts message
275
+ end
276
+ return
277
+ end
278
+
244
279
  if no_write
245
280
  @generated_data[path] = content if return_data
246
281
  puts "- #{label} (dry run): #{path}" if dry_run
@@ -279,4 +314,4 @@ class CreateTests
279
314
  resolved
280
315
  end
281
316
  private_class_method :resolve_pathname
282
- end
317
+ end
@@ -3,10 +3,16 @@ class CreateTests
3
3
 
4
4
  class << self
5
5
 
6
+ SKIP_CHAIN_SEGMENTS = %w[subscriptions resource_groups providers].freeze
7
+
6
8
  # Extracts the resource dependency chain from a request path.
7
9
  # Works with both placeholder paths ({param}) and interpolated paths
8
10
  # (where params are resolved, possibly to empty strings).
9
11
  #
12
+ # Segments whose snake_case names are subscriptions, resource_groups, or
13
+ # providers are omitted from the setup/cleanup chain, but their parameters
14
+ # remain in params_up_to for later resources.
15
+ #
10
16
  # @param request [Hash] Request hash containing :path and :method keys
11
17
  # @param params [Array<String>] Positional parameter names (e.g. ["@subscription_id", "@pool_name"])
12
18
  # @return [Array<Hash>, Hash|nil] Two values: prerequisite chain array and target resource hash (or nil).
@@ -73,8 +79,14 @@ class CreateTests
73
79
  { resource: resource_name, param: matching_param, params_up_to: params_up_to }
74
80
  end
75
81
 
76
- prerequisites = all_entries[0..-2]
77
- target = all_entries.last
82
+ # Drop Azure infrastructure segments from the chain; keep their params in params_up_to.
83
+ real_entries = all_entries.reject { |e| SKIP_CHAIN_SEGMENTS.include?(e[:resource]) }
84
+
85
+ return [], nil if real_entries.empty?
86
+ return [], real_entries.first if real_entries.size == 1
87
+
88
+ prerequisites = real_entries[0..-2]
89
+ target = real_entries.last
78
90
 
79
91
  return prerequisites, target
80
92
  end
@@ -1,3 +1,3 @@
1
1
  class CreateTests
2
- VERSION = "1.1.0"
2
+ VERSION = "1.3.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: create_tests
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mario Ruiz
@@ -97,7 +97,10 @@ files:
97
97
  homepage: https://github.com/MarioRuiz/create_tests
98
98
  licenses:
99
99
  - MIT
100
- metadata: {}
100
+ metadata:
101
+ changelog_uri: https://github.com/MarioRuiz/create_tests/blob/master/CHANGELOG.md
102
+ source_code_uri: https://github.com/MarioRuiz/create_tests
103
+ bug_tracker_uri: https://github.com/MarioRuiz/create_tests/issues
101
104
  post_install_message: Thanks for installing! Visit us on https://github.com/MarioRuiz/create_tests
102
105
  rdoc_options: []
103
106
  require_paths: