create_tests 1.0.0 → 1.2.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: d22c903c194de3b4c00a889073f9bdf72ba97f7a68c083f871f5fd60905df812
4
- data.tar.gz: 638d9efe9d13847ef0c918a2505aea709f6df4a7cb29bea6027fe4e081a96c57
3
+ metadata.gz: adf779b9d966308976925449218e2f71071441bc665a7c5ecdd79c9ec25c477e
4
+ data.tar.gz: 49cd07ebd23c1dd406ee5db3af61c3a761615ecec746c451d059717ad8a668dd
5
5
  SHA512:
6
- metadata.gz: d8c090b7c8195479424bee5b5b0b41e3581deaa34b23e8a8736ff15bb2a548ab0a71f5f1da6b9dabb59bcd63100d8ffd31c353aadadbe9c178ccd69c486d1f4e
7
- data.tar.gz: 533123ae7baaf838f831fe93e80ac076ab9dfee262af60029d2c8d442dea4193a660fdd1937fdc972556d89b60d1a903d611effc068d9fdd85cd69793470f55a
6
+ metadata.gz: 948a204d7595c2b5e5ddbd42e18029b870f0fd9212df3a20dd970b29416d69306332b2f31ecd57fc9b82d8a1ad3b6e15424312248a5c80d26f9ae0ca14e90f00
7
+ data.tar.gz: 9510e4c3f35eba997841db074bd32b8ef07712bacd88cda7bb49581deea70d1381148d833e3791c5b3c8b21a2d7259fb2fffacfae157dfd15080ad46ca9e5d9c
data/README.md CHANGED
@@ -2,12 +2,13 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/create_tests.svg)](https://rubygems.org/gems/create_tests)
4
4
  [![CI](https://github.com/MarioRuiz/create_tests/actions/workflows/ci.yml/badge.svg)](https://github.com/MarioRuiz/create_tests/actions/workflows/ci.yml)
5
+ [![coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/MarioRuiz/create_tests/master/.github/badges/coverage.json)](https://github.com/MarioRuiz/create_tests/actions/workflows/ci.yml)
5
6
  ![Gem](https://img.shields.io/gem/dt/create_tests)
6
7
  ![GitHub commit activity](https://img.shields.io/github/commit-activity/y/MarioRuiz/create_tests)
7
8
  ![GitHub last commit](https://img.shields.io/github/last-commit/MarioRuiz/create_tests)
8
9
  ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/MarioRuiz/create_tests)
9
10
 
10
- 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.
11
12
 
12
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).
13
14
 
@@ -28,8 +29,8 @@ The gem uses [rufo](https://github.com/ruby-formatter/rufo) to format generated
28
29
  After installation you can run using the command line executable or from Ruby code.
29
30
 
30
31
  The execution will create:
31
- - A `spec/` folder with one RSpec test file per API endpoint
32
- - 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
33
34
  - A `settings/general.rb` with NiceHttp configuration boilerplate
34
35
 
35
36
  ### Command line
@@ -43,6 +44,10 @@ Usage: create_tests [requests_file] [options]
43
44
  -d, --dry_run Preview which files would be created or modified
44
45
  --cleanup_each Generate cleanup in after(:each) instead of after(:all)
45
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
46
51
  -v, --version Show version
47
52
  ```
48
53
 
@@ -78,11 +83,13 @@ For each API endpoint, `create_tests` generates up to 7 test types:
78
83
 
79
84
  4. **"returns expected mock response"** -- For endpoints with `mock_response` data (from `open_api_import`), validates the mock response matches the expected code, message, and structure.
80
85
 
81
- 5. **"handles multiple valid data variations"** -- For POST/PUT/PATCH endpoints with data, uses `generate_n(5, :correct)` from nice_hash to test 5 different valid payload variations.
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.
82
87
 
83
- 6. **"returns error when individual data fields are invalid"** -- Uses `NiceHash.change_one_by_one` to systematically test each data 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.
84
89
 
85
- 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.
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.
91
+
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.
86
93
 
87
94
  ## Parameters
88
95
 
@@ -90,7 +97,7 @@ For each API endpoint, `create_tests` generates up to 7 test types:
90
97
 
91
98
  Accepts three options: `:overwrite`, `:dont_overwrite` and `:append`. Default: `:append`.
92
99
 
93
- - **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.
94
101
 
95
102
  - **dont_overwrite**: If the test file exists, no changes will be made. If it doesn't exist, it will be created.
96
103
 
@@ -119,12 +126,40 @@ result.each { |path, content| puts "#{path}: #{content.length} bytes" }
119
126
 
120
127
  ### spec_dir / settings_dir
121
128
 
122
- 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`.
123
130
 
124
131
  ```ruby
125
132
  CreateTests.from "./requests/uber.yaml.rb", spec_dir: "./test/spec", settings_dir: "./test/config"
126
133
  ```
127
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
+
128
163
  ### cleanup
129
164
 
130
165
  Controls whether and how prerequisite setup/cleanup hooks are generated. Default: `:after_all`.
@@ -142,7 +177,7 @@ CreateTests.from "./requests/netapp.json.rb", cleanup: false # no cleanup
142
177
  ```ruby
143
178
  before(:all) do
144
179
  @http = NiceHttp.new()
145
- @volume_name = @volume_name + "-cre"
180
+ @volume_name = @volume_name + "-createorupda"
146
181
  expect(Helper.setup_capacity_pools(@http, ...).state).to eq "Succeeded"
147
182
  @request = Volumes.create_or_update(...)
148
183
  end
@@ -157,7 +192,7 @@ end
157
192
 
158
193
  ```ruby
159
194
  before(:all) do
160
- @snapshot_name = @snapshot_name + "-del"
195
+ @snapshot_name = @snapshot_name + "-delete"
161
196
  expect(Helper.setup_volumes(@http, ...).state).to eq "Succeeded"
162
197
  @request = Snapshots.delete(...)
163
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,39 +1,58 @@
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 = []
17
23
  keywords_required = []
24
+ keyreq_kwargs = {}
18
25
  method_obj.parameters.each do |p|
19
- if p[0] == :req #required
26
+ if p[0] == :req #required positional
20
27
  params << "@#{p[1]}"
28
+ elsif p[0] == :keyreq
29
+ keywords_required << p[1]
30
+ keyreq_kwargs[p[1]] = nil
21
31
  elsif p[0] == :key and p[1].to_s.match?(/^[a-z]/i) and Object.const_defined?(p[1].to_s.upcase)
32
+ # create_constants: required path/query as keyword with UPCASE constant default
22
33
  keywords_required << p[1]
23
34
  end
24
35
  end
25
- req_txt += params.join(", ")
36
+ call_parts = params.dup
37
+ keywords_required.each { |k| call_parts << "#{k}: @#{k}" }
38
+ req_txt += call_parts.join(", ")
26
39
  req_txt += ")"
27
40
  require 'nice_hash'
28
- request = method_obj.call(*Array.new(params.size))
41
+ # Keyword-only methods (create_constants) must not receive nil positionals;
42
+ # :keyreq needs an explicit keyword so Method#call succeeds.
43
+ request = method_obj.call(*Array.new(params.size), **keyreq_kwargs)
29
44
 
30
45
  req_txt = "#{mod_name}.#{req_txt}"
31
46
  params_declaration_txt = ""
32
- ## todo: add param in case of :append
47
+ ## todo: add param in case of :append
33
48
  params.each do |p|
34
49
  params_declaration_txt << "#{p} = Helper.#{p.gsub('@','')}(@http)\n"
35
50
  @params << p
36
51
  end
52
+ keywords_required.each do |k|
53
+ params_declaration_txt << "@#{k} = Helper.#{k}(@http)\n"
54
+ @params << "@#{k}"
55
+ end
37
56
 
38
57
  all_params_for_chain = params + keywords_required.map { |k| "@#{k}" }
39
58
  resource_chain, target_resource = extract_resource_chain(request, all_params_for_chain)
@@ -45,8 +64,7 @@ class CreateTests
45
64
  needs_target_setup = target_resource && [:delete, :get, :patch].include?(request[:method])
46
65
  parent_resource = resource_chain.last
47
66
 
48
- method_suffix = method_txt.to_s.split("_").map { |w| w[0..2] }.join[0..2]
49
- method_suffix = "-#{method_suffix}"
67
+ method_suffix = "-#{method_txt.to_s.gsub('_', '')[0, 12]}"
50
68
 
51
69
  setup_before_all_txt = ""
52
70
  setup_before_each_txt = ""
@@ -54,33 +72,39 @@ class CreateTests
54
72
  after_all_txt = ""
55
73
 
56
74
  if is_delete && target_resource && parent_resource
57
- setup_before_all_txt = "expect(Helper.setup_#{parent_resource[:resource]}(@http, #{parent_resource[:params_up_to].join(', ')}).state).to eq 'Succeeded'\n"
58
- 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"
59
77
  if cleanup
60
- after_each_txt = "after(:each) do\nHelper.cleanup_#{target_resource[:resource]}(@http, #{target_resource[:params_up_to].join(', ')})\nend\n"
61
- 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)
62
80
  end
63
81
  elsif needs_target_setup && target_resource
64
- 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"
65
83
  if cleanup
66
- 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)
67
85
  end
68
86
  elsif parent_resource
69
- 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"
70
88
  if cleanup && target_resource
71
- after_each_txt = "after(:each) do\nHelper.cleanup_#{target_resource[:resource]}(@http, #{target_resource[:params_up_to].join(', ')})\nend\n"
72
- 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)
73
91
  elsif cleanup
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"
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)
75
93
  end
76
94
  end
77
95
 
78
96
  if cleanup == :after_each && !after_all_txt.empty?
79
- after_each_txt += after_all_txt.sub("after(:all)", "after(:each)")
80
- 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
81
105
  end
82
106
 
83
- if test_txt ==""
107
+ if test_txt == ""
84
108
  modified = true
85
109
 
86
110
  suffix_txt = ""
@@ -89,12 +113,15 @@ class CreateTests
89
113
  suffix_txt = "#{target_param} = #{target_param} + \"#{method_suffix}\"\n"
90
114
  end
91
115
 
92
- before_each_block = "before(:each) do |example|\n"
93
- before_each_block << "@http = NiceHttp.new()\n" if is_delete && target_resource
94
- before_each_block << setup_before_each_txt
95
- 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"
96
123
 
97
- output = "
124
+ output = "
98
125
  require_relative '#{settings_relative_path}'
99
126
  if defined?(#{mod_name}) and defined?(#{mod_name}.#{method_txt})
100
127
  RSpec.describe #{mod_name}, '##{method_txt}' do
@@ -104,147 +131,179 @@ class CreateTests
104
131
  @http.logger.info(\"\\n\#{'+'*50} Before All ends \#{'+'*50}\")
105
132
  end
106
133
  #{after_all_txt}#{after_each_txt}#{before_each_block}\n"
134
+ end
107
135
  else
108
136
  output = test_txt
109
- if output.match?(/\s*end\s*end\s*end\s*\Z/)
110
- 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")
111
141
  else
112
- output.gsub!(/\s*end\s*\Z/,"\n")
142
+ output.gsub!(/\s*end\s*\Z/, "\n")
113
143
  end
114
144
  end
115
145
 
116
146
 
117
147
  tests = Hash.new()
118
148
 
149
+ has_data_examples = request.key?(:data_examples) &&
150
+ request[:data_examples].is_a?(Array) &&
151
+ !request[:data_examples].empty?
152
+ # Prefer data_pattern for variation/invalid-field tests; fall back to :data
153
+ payload_source = if request.key?(:data_pattern)
154
+ ":data_pattern"
155
+ elsif request.key?(:data)
156
+ ":data"
157
+ end
158
+
119
159
  # first response on responses is the one expected to be returned when success
120
- if request.key?(:responses) and request[:responses].size > 0
160
+ if include_kind?(only_kinds, :success) && request.key?(:responses) and request[:responses].size > 0
121
161
  code = request[:responses].keys[0]
122
- title="it 'has correct structure in successful response' "
123
- tests[title] = "do
162
+ title = example_title("has correct structure in successful response", test, trailing_space: true)
163
+ if !request.key?(:data) && has_data_examples
164
+ tests[title] = "do
165
+ request = @request.deep_copy
166
+ request[:data] = @request[:data_examples].first
167
+ resp = @http.#{request[:method]}(request)
168
+ #{assert_eq('resp.code', code, test)}\n"
169
+ if request[:responses][code].is_a?(Hash) and request[:responses][code].key?(:data)
170
+ tests[title] +="result = NiceHttp.validate_response(resp, @request.responses._#{code}.data, include_diff: true)\n"
171
+ tests[title] += "#{assert_be_true('result[:ok]', 'Structure mismatch: #{result[:diff]}', test)}\n"
172
+ end
173
+ tests[title] += "end\n"
174
+ else
175
+ tests[title] = "do
124
176
  resp = @http.#{request[:method]}(@request)
125
- expect(resp.code).to eq #{code}\n"
177
+ #{assert_eq('resp.code', code, test)}\n"
126
178
 
127
- if request[:responses][code].is_a?(Hash) and request[:responses][code].key?(:data)
128
- tests[title] +="result = NiceHttp.validate_response(resp, @request.responses._#{code}.data, include_diff: true)\n"
129
- tests[title] +="expect(result[:ok]).to be(true), \"Structure mismatch: \#{result[:diff]}\"\n"
179
+ if request[:responses][code].is_a?(Hash) and request[:responses][code].key?(:data)
180
+ tests[title] +="result = NiceHttp.validate_response(resp, @request.responses._#{code}.data, include_diff: true)\n"
181
+ tests[title] += "#{assert_be_true('result[:ok]', 'Structure mismatch: #{result[:diff]}', test)}\n"
182
+ end
183
+ tests[title] += "end\n"
130
184
  end
131
- tests[title] += "end\n"
132
185
  end
133
186
 
134
- title = "it 'doesn\\'t retrieve data if not authenticated'"
135
- tests[title] = "do
187
+ if include_kind?(only_kinds, :auth)
188
+ title = example_title("doesn\\'t retrieve data if not authenticated", test)
189
+ tests[title] = "do
136
190
  http = NiceHttp.new()
137
191
  http.headers = {}
138
192
  resp = http.#{request[:method]}(@request)
139
- expect(resp.code).to be_between('400', '499')\n"
140
- if request.key?(:responses) and (request[:responses].keys.select{|c| c.to_s.to_i>=400&&c.to_s.to_i<=499}).size>0
141
- tests[title] += "expect(NiceHash.compare_structure(@request.responses[resp.code.to_sym].data, resp.data.json)).to eq true
142
- expect(resp.message).to eq @request.responses[resp.code.to_sym].message\n"
193
+ #{assert_between('resp.code', "'400'", "'499'", test)}\n"
194
+ if request.key?(:responses) and (request[:responses].keys.select{|c| c.to_s.to_i>=400&&c.to_s.to_i<=499}).size>0
195
+ tests[title] += "#{assert_eq('NiceHash.compare_structure(@request.responses[resp.code.to_sym].data, resp.data.json)', 'true', test)}
196
+ #{assert_eq('resp.message', '@request.responses[resp.code.to_sym].message', test)}\n"
197
+ end
198
+ tests[title] += "end\n"
143
199
  end
144
- tests[title] += "end\n"
145
-
146
- if params.size > 0 or keywords_required.size >0
200
+
201
+ if include_kind?(only_kinds, :required_params) && (params.size > 0 or keywords_required.size >0)
147
202
  empty_param = ""
148
203
  params.each do |p|
149
204
  r = req_txt.gsub(/#{p}([),])/, '""\1')
150
205
  empty_param += "
151
206
  request = #{r}
152
207
  resp = @http.#{request[:method]}(request)
153
- expect(resp.code).to be_between('400', '499')\n"
208
+ #{assert_between('resp.code', "'400'", "'499'", test)}\n"
154
209
  if request.key?(:responses) and (request[:responses].keys.select{|c| c.to_s.to_i>=400&&c.to_s.to_i<=499}).size>0
155
- empty_param += "expect(resp.message).to match /\#{request.responses[resp.code.to_sym].message}/i\n"
210
+ empty_param += "#{assert_match('resp.message', '/\#{request.responses[resp.code.to_sym].message}/i', test)}\n"
156
211
  end
157
212
  end
158
- if keywords_required.size > 0
213
+ if keywords_required.size > 0
159
214
  r = req_txt.scan(/(.+)\(/).join
160
215
  empty_param += "
161
216
  [:#{keywords_required.join(', :')}].each do |kw|
162
217
  request = #{r}(kw => '')
163
218
  resp = @http.#{request[:method]}(request)
164
- expect(resp.code).to be_between('400', '499')\n"
219
+ #{assert_between('resp.code', "'400'", "'499'", test)}\n"
165
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
166
- 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"
167
222
  end
168
223
  empty_param += "end\n"
169
224
  end
170
225
  empty_param += "end\n"
171
- tests["it 'returns error if required parameter empty' "] = "do\n#{empty_param}"
226
+ tests[example_title("returns error if required parameter empty", test, trailing_space: true)] = "do\n#{empty_param}"
172
227
  end
173
228
 
174
- if request.key?(:mock_response)
175
- title = "it 'returns expected mock response' "
229
+ if include_kind?(only_kinds, :mock) && request.key?(:mock_response)
230
+ title = example_title("returns expected mock response", test, trailing_space: true)
176
231
  tests[title] = "do
177
232
  @http_mock = NiceHttp.new()
178
233
  @http_mock.use_mocks = true
179
234
  resp = @http_mock.#{request[:method]}(@request)
180
- expect(resp.code).to eq @request[:mock_response][:code]
181
- expect(resp.message).to eq @request[:mock_response][:message]\n"
235
+ #{assert_eq('resp.code', '@request[:mock_response][:code]', test)}
236
+ #{assert_eq('resp.message', '@request[:mock_response][:message]', test)}\n"
182
237
  if request[:mock_response].key?(:data)
183
- tests[title] += "expect(NiceHash.compare_structure(@request[:mock_response][:data], resp.data.json, true)).to be true\n"
238
+ tests[title] += "#{assert_be_true('NiceHash.compare_structure(@request[:mock_response][:data], resp.data.json, true)', nil, test)}\n"
184
239
  end
185
240
  tests[title] += "end\n"
186
241
  end
187
242
 
188
- if request.key?(:data) and [:post, :put, :patch].include?(request[:method])
189
- title = "it 'handles multiple valid data variations' "
243
+ if include_kind?(only_kinds, :variations) && payload_source && [:post, :put, :patch].include?(request[:method])
244
+ title = example_title("handles multiple valid data variations", test, trailing_space: true)
190
245
  tests[title] = "do
191
- @request[:data].generate_n(5, :correct).each do |generated_data|
246
+ @request[#{payload_source}].generate_n(5, :correct).each do |generated_data|
192
247
  request = @request.deep_copy
193
248
  request[:data] = generated_data
194
249
  resp = @http.#{request[:method]}(request)
195
- expect(resp.code.to_i).to be_between(200, 299)
250
+ #{assert_between('resp.code.to_i', '200', '299', test)}
196
251
  end
197
252
  end\n"
198
253
  end
199
254
 
200
- if request.key?(:data)
201
- title = "it 'returns error when individual data fields are invalid' "
255
+ if include_kind?(only_kinds, :invalid_fields) && payload_source
256
+ title = example_title("returns error when individual data fields are invalid", test, trailing_space: true)
202
257
  tests[title] = "do
203
- wrong = @request[:data].generate(:correct, errors: :min_length)
204
- NiceHash.change_one_by_one([@request[:data], :correct], wrong).each do |one_wrong|
258
+ wrong = @request[#{payload_source}].generate(:correct, errors: :min_length)
259
+ NiceHash.change_one_by_one([@request[#{payload_source}], :correct], wrong).each do |one_wrong|
205
260
  request = @request.deep_copy
206
261
  request[:data] = one_wrong
207
262
  resp = @http.#{request[:method]}(request)
208
- expect(resp.code.to_i).not_to be_between(200, 299)
263
+ #{assert_between('resp.code.to_i', '200', '299', test, negate: true)}
209
264
  end
210
265
  end\n"
211
266
  end
212
267
 
213
- if request.key?(:data) and request.key?(:data_required)
268
+ if include_kind?(only_kinds, :required_data) && request.key?(:data_required) && (request.key?(:data) || has_data_examples)
269
+ seed_data_line = if !request.key?(:data) && has_data_examples
270
+ "request[:data] = request[:data_examples].first\n "
271
+ else
272
+ ""
273
+ end
214
274
  empty_param_data = ""
215
275
  empty_param_data += "
216
276
  @request[:data_required].each do |p|
217
277
  request = @request.deep_copy
218
- request.values_for[p] = ''
278
+ #{seed_data_line}request.values_for[p] = ''
219
279
  resp = @http.#{request[:method]}(request)
220
- expect(resp.code).not_to be_between('200', '299')
280
+ #{assert_between('resp.code', "'200'", "'299'", test, negate: true)}
221
281
  if request.responses.key?(resp.code.to_sym)
222
- expect(resp.message).to match /\#{request.responses[resp.code.to_sym].message}/i
282
+ #{assert_match('resp.message', '/\#{request.responses[resp.code.to_sym].message}/i', test)}
223
283
  end
224
284
  end
225
285
  "
226
286
  empty_param_data += "end\n"
227
- tests["it 'returns error if required parameter on data empty' do\n"] = empty_param_data
287
+ tests[example_title("returns error if required parameter on data empty", test, with_do: true)] = empty_param_data
228
288
 
229
289
  missing_param_data = ""
230
290
  missing_param_data += "
231
291
  @request[:data_required].each do |p|
232
292
  request = @request.deep_copy
233
- NiceHash.delete_nested(request[:data], p)
293
+ #{seed_data_line}NiceHash.delete_nested(request[:data], p)
234
294
  resp = @http.#{request[:method]}(request)
235
- expect(resp.code).not_to be_between('200', '299')
295
+ #{assert_between('resp.code', "'200'", "'299'", test, negate: true)}
236
296
  if request.responses.key?(resp.code.to_sym)
237
- expect(resp.message).to match /\#{request.responses[resp.code.to_sym].message}/i
297
+ #{assert_match('resp.message', '/\#{request.responses[resp.code.to_sym].message}/i', test)}
238
298
  end
239
299
  end
240
300
  "
241
301
  missing_param_data += "end\n"
242
- tests["it 'returns error if required parameter on data missing' do\n"] = missing_param_data
302
+ tests[example_title("returns error if required parameter on data missing", test, with_do: true)] = missing_param_data
243
303
  end
244
304
 
245
305
  tests.each do |k,v|
246
- unless output.include?(k.gsub(" '",' "').gsub("' ",'" ').gsub("\n",'').gsub(/\s+do$/,'').gsub(/^\s*it/,'')) or
247
- output.include?(k.gsub(' "'," '").gsub('" ',"' ").gsub("\n",'').gsub(/\s+do$/,'').gsub(/^\s*it/,''))
306
+ unless example_already_present?(output, k, test)
248
307
  modified = true
249
308
  message = " = test added #{k} for #{method_txt}"
250
309
  @logger.info message
@@ -252,15 +311,191 @@ class CreateTests
252
311
  puts message
253
312
  output +="# Appended #{Time.now.stamp}\n"
254
313
  end
255
- output += "#{k}#{v}"
314
+ if test == :minitest
315
+ # keys are "def test_foo" and bodies start with "do\n...\nend"
316
+ body = v.sub(/\Ado\n/, "").sub(/\n?end\n?\z/, "\n")
317
+ output += "#{k}\n#{body}end\n"
318
+ else
319
+ output += "#{k}#{v}"
320
+ end
256
321
  end
257
322
  end
258
- if output.include?('defined?(')
323
+ if test == :minitest
324
+ if output.include?('defined?(')
325
+ output += "\nend\nend"
326
+ else
327
+ output += "\nend"
328
+ end
329
+ elsif output.include?('defined?(')
259
330
  output += "\nend\nend"
260
331
  else
261
332
  output +="\nend"
262
333
  end
263
334
  return modified, output
264
335
  end
336
+
337
+ private def normalize_only(only)
338
+ return nil if only.nil?
339
+ list = case only
340
+ when Symbol then [only]
341
+ when String then only.split(",").map { |s| s.strip.to_sym }
342
+ when Array then only.map { |s| s.to_s.strip.to_sym }
343
+ else
344
+ raise "** Wrong only parameter: #{only}"
345
+ end
346
+ unknown = list - TEST_KINDS
347
+ unless unknown.empty?
348
+ raise "** Wrong only parameter: unknown kind(s) #{unknown.join(', ')}. Valid: #{TEST_KINDS.join(', ')}"
349
+ end
350
+ list
351
+ end
352
+
353
+ private def include_kind?(only_kinds, kind)
354
+ only_kinds.nil? || only_kinds.include?(kind)
355
+ end
356
+
357
+ private def example_title(description, test, trailing_space: false, with_do: false)
358
+ if test == :minitest
359
+ "def test_#{minitest_method_name(description)}"
360
+ else
361
+ space = trailing_space ? " " : ""
362
+ if with_do
363
+ "it '#{description}' do\n"
364
+ else
365
+ "it '#{description}'#{space}"
366
+ end
367
+ end
368
+ end
369
+
370
+ private def minitest_method_name(description)
371
+ description.gsub("\\'", "")
372
+ .gsub(/[^a-zA-Z0-9]+/, "_")
373
+ .gsub(/\A_+|_+\z/, "")
374
+ .downcase
375
+ end
376
+
377
+ private def example_already_present?(output, key, test)
378
+ if test == :minitest
379
+ output.include?(key)
380
+ else
381
+ output.include?(key.gsub(" '", ' "').gsub("' ", '" ').gsub("\n", '').gsub(/\s+do$/, '').gsub(/^\s*it/, '')) ||
382
+ output.include?(key.gsub(' "', " '").gsub('" ', "' ").gsub("\n", '').gsub(/\s+do$/, '').gsub(/^\s*it/, ''))
383
+ end
384
+ end
385
+
386
+ private def assert_eq(actual, expected, test)
387
+ if test == :minitest
388
+ "assert_equal #{expected}, #{actual}"
389
+ else
390
+ "expect(#{actual}).to eq #{expected}"
391
+ end
392
+ end
393
+
394
+ private def assert_be_true(actual, message, test)
395
+ if test == :minitest
396
+ if message
397
+ "assert #{actual}, \"#{message}\""
398
+ else
399
+ "assert #{actual}"
400
+ end
401
+ else
402
+ if message
403
+ "expect(#{actual}).to be(true), \"#{message}\""
404
+ else
405
+ "expect(#{actual}).to be true"
406
+ end
407
+ end
408
+ end
409
+
410
+ private def assert_between(actual, low, high, test, negate: false)
411
+ if test == :minitest
412
+ if negate
413
+ "refute_includes (#{low}..#{high}), #{actual}"
414
+ else
415
+ "assert_includes (#{low}..#{high}), #{actual}"
416
+ end
417
+ else
418
+ if negate
419
+ "expect(#{actual}).not_to be_between(#{low}, #{high})"
420
+ else
421
+ "expect(#{actual}).to be_between(#{low}, #{high})"
422
+ end
423
+ end
424
+ end
425
+
426
+ private def assert_match(actual, pattern, test)
427
+ if test == :minitest
428
+ "assert_match #{pattern}, #{actual}"
429
+ else
430
+ "expect(#{actual}).to match #{pattern}"
431
+ end
432
+ end
433
+
434
+ private def framework_hook(kind, body, test)
435
+ if test == :minitest
436
+ # Stored as raw bodies; assembled into setup/teardown in the skeleton.
437
+ # Prefix with a marker so cleanup: :after_each merging can find them.
438
+ case kind
439
+ when :after_each then "TEARDOWN_EACH:#{body}"
440
+ when :after_all then "TEARDOWN_ALL:#{body}"
441
+ else body
442
+ end
443
+ else
444
+ case kind
445
+ when :after_each then "after(:each) do\n#{body}end\n"
446
+ when :after_all then "after(:all) do\n#{body}end\n"
447
+ else body
448
+ end
449
+ end
450
+ end
451
+
452
+ 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)
453
+ class_name = "#{mod_name}#{camelize(method_txt.to_s)}Test"
454
+
455
+ teardown_each_body = after_each_txt.to_s.gsub(/\ATEARDOWN_EACH:/, "").gsub(/\ATEARDOWN_ALL:/, "")
456
+ # after_each_txt may contain both TEARDOWN_EACH and (after merge) TEARDOWN_ALL markers
457
+ each_parts = []
458
+ all_parts = []
459
+ [after_each_txt.to_s, after_all_txt.to_s].each do |chunk|
460
+ chunk.split(/(?=TEARDOWN_(?:EACH|ALL):)/).each do |part|
461
+ next if part.empty?
462
+ if part.start_with?("TEARDOWN_EACH:")
463
+ each_parts << part.sub(/\ATEARDOWN_EACH:/, "")
464
+ elsif part.start_with?("TEARDOWN_ALL:")
465
+ all_parts << part.sub(/\ATEARDOWN_ALL:/, "")
466
+ end
467
+ end
468
+ end
469
+
470
+ teardown_body = (each_parts + all_parts).join
471
+
472
+ setup_http = if is_delete && target_resource
473
+ "@http = NiceHttp.new()\n"
474
+ else
475
+ ""
476
+ end
477
+
478
+ # before(:all) work at start of setup; before(:each) after that.
479
+ # First test creates @http for non-delete; delete recreates in the each section.
480
+ "
481
+ require 'minitest/autorun'
482
+ require_relative '#{settings_relative_path}'
483
+ if defined?(#{mod_name}) and defined?(#{mod_name}.#{method_txt})
484
+ class #{class_name} < Minitest::Test
485
+ def setup
486
+ @http = NiceHttp.new()
487
+ #{params_declaration_txt}#{suffix_txt}#{setup_before_all_txt}@request = #{req_txt}
488
+ @http.logger.info(\"\\n\#{'+'*50} Before All ends \#{'+'*50}\")
489
+ #{setup_http}#{setup_before_each_txt}@http.logger.info(\"\\n\\n\#{'='*100}\\nTest: \#{name}\\n\#{'-'*100}\")
490
+ end
491
+ def teardown
492
+ #{teardown_body}
493
+ end
494
+ "
495
+ end
496
+
497
+ private def camelize(snake)
498
+ snake.to_s.split("_").map(&:capitalize).join
499
+ end
265
500
  end
266
- end
501
+ 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.0.0"
2
+ VERSION = "1.2.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.0.0
4
+ version: 1.2.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: