spec_forge 0.3.2 → 0.4.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 +4 -4
- data/CHANGELOG.md +19 -1
- data/README.md +65 -4
- data/lib/spec_forge/backtrace_formatter.rb +27 -0
- data/lib/spec_forge/cli/new.rb +15 -105
- data/lib/spec_forge/cli/run.rb +53 -3
- data/lib/spec_forge/configuration.rb +3 -0
- data/lib/spec_forge/core_ext/rspec.rb +35 -0
- data/lib/spec_forge/core_ext.rb +5 -0
- data/lib/spec_forge/factory.rb +7 -9
- data/lib/spec_forge/runner.rb +28 -0
- data/lib/spec_forge/spec.rb +56 -13
- data/lib/spec_forge/version.rb +1 -1
- data/lib/spec_forge.rb +11 -3
- data/lib/templates/new_factory.tt +4 -0
- data/lib/templates/new_spec.tt +43 -0
- metadata +11 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 658fd0830ce9179c1fe7ea880e2ed8196ccbc47bc8b244f6fc40c458ca900cca
|
4
|
+
data.tar.gz: 3658379f52f23ffa362ded1c50b28e052911b6602ac3a3bed02b1fd9c8ee334c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ebcdfd4e02d965049cabd1a18b7897665800dc8081149aa4554433e7083296f0121d76f4ec474b527fdd8df0da1c71170c9c10c5cff1a108f8704b7f14e7104f
|
7
|
+
data.tar.gz: ad3b84d27b9e7ab62dc902ad2a5d41a77670286aaba9a635ccebf5735061fa725e8d46daf62fd75562a35688d62b3dc595a597b9e38ec965f5273fdc27a2aabb
|
data/CHANGELOG.md
CHANGED
@@ -13,6 +13,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
13
13
|
|
14
14
|
### Removed
|
15
15
|
|
16
|
+
## [0.4.0] - 12025-02-22
|
17
|
+
|
18
|
+
### Added
|
19
|
+
|
20
|
+
- Added support to run an individual file, spec, or expectation
|
21
|
+
|
22
|
+
### Changed
|
23
|
+
|
24
|
+
- Updated `everythingrb` to 0.2
|
25
|
+
- Updated spec and factory templates
|
26
|
+
- Improved error reporting to use SpecForge's commands instead of RSpec's
|
27
|
+
- Updated `run` command's CLI documentation
|
28
|
+
|
29
|
+
### Removed
|
30
|
+
|
31
|
+
- Removed support for ActiveSupport 7.0
|
32
|
+
|
16
33
|
## [0.3.2] - 12025-02-20
|
17
34
|
|
18
35
|
### Changed
|
@@ -71,7 +88,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
71
88
|
|
72
89
|
- Initial commit
|
73
90
|
|
74
|
-
[unreleased]: https://github.com/itsthedevman/spec_forge/compare/v0.
|
91
|
+
[unreleased]: https://github.com/itsthedevman/spec_forge/compare/v0.4.0...HEAD
|
92
|
+
[0.4.0]: https://github.com/itsthedevman/spec_forge/compare/v0.3.2...v0.4.0
|
75
93
|
[0.3.2]: https://github.com/itsthedevman/spec_forge/compare/v0.3.0...v0.3.2
|
76
94
|
[0.3.0]: https://github.com/itsthedevman/spec_forge/compare/v0.2.0...v0.3.0
|
77
95
|
[0.2.0]: https://github.com/itsthedevman/spec_forge/compare/v0.1.0...v0.2.0
|
data/README.md
CHANGED
@@ -41,14 +41,14 @@ Consider alternatives when you need:
|
|
41
41
|
## Roadmap
|
42
42
|
|
43
43
|
Current development priorities:
|
44
|
-
- [ ] Support for running individual specs
|
45
44
|
- [ ] Array support for `json` expectations
|
46
45
|
- [ ] Negated matchers: `matcher.not`
|
47
46
|
- [ ] `create_list/build_list` factory strategies
|
48
47
|
- [ ] `transform.map` support
|
49
|
-
- [ ] Improved error handling
|
50
48
|
- [ ] XML/HTML response handling
|
51
49
|
- [ ] OpenAPI generation from tests
|
50
|
+
- [x] Support for running individual specs
|
51
|
+
- [x] Improved error handling
|
52
52
|
|
53
53
|
Have a feature request? Open an issue on GitHub!
|
54
54
|
|
@@ -63,7 +63,11 @@ I'm currently looking for opportunities where I can tackle meaningful problems a
|
|
63
63
|
- [Compatibility](#compatibility)
|
64
64
|
- [Installation](#installation)
|
65
65
|
- [Getting Started](#getting-started)
|
66
|
-
- [
|
66
|
+
- [Forging Your First Test](#forging-your-first-test)
|
67
|
+
- [Running Tests](#running-tests)
|
68
|
+
- [Targeting Specific Files](#targeting-specific-files)
|
69
|
+
- [Targeting Specific Specs](#targeting-specific-specs)
|
70
|
+
- [Targeting Individual Expectations](#targeting-individual-expectations)
|
67
71
|
- [Configuration](#configuration)
|
68
72
|
- [Basic Configuration](#basic-configuration)
|
69
73
|
- [Framework Integration](#framework-integration)
|
@@ -138,7 +142,7 @@ bundle exec spec_forge init
|
|
138
142
|
|
139
143
|
This creates the `spec_forge` directory containing factory definitions, test specifications, and global configuration.
|
140
144
|
|
141
|
-
##
|
145
|
+
## Forging Your First Test
|
142
146
|
|
143
147
|
Let's write a simple test to verify a user endpoint. Create a new spec file:
|
144
148
|
|
@@ -167,6 +171,63 @@ Run your tests with:
|
|
167
171
|
spec_forge run
|
168
172
|
```
|
169
173
|
|
174
|
+
Since `run` is the default command, you can just use:
|
175
|
+
|
176
|
+
```bash
|
177
|
+
spec_forge
|
178
|
+
```
|
179
|
+
|
180
|
+
## Running Tests
|
181
|
+
|
182
|
+
As your test suite grows, you'll want more control over which tests to run.
|
183
|
+
|
184
|
+
#### Targeting Specific Files
|
185
|
+
|
186
|
+
When working on a specific feature, run tests from a single file:
|
187
|
+
|
188
|
+
```bash
|
189
|
+
spec_forge users # Runs all tests in specs/users.yml
|
190
|
+
```
|
191
|
+
|
192
|
+
#### Targeting Specific Specs
|
193
|
+
|
194
|
+
Focus on a specific endpoint by running a single spec:
|
195
|
+
|
196
|
+
```bash
|
197
|
+
spec_forge users:destroy_user # Runs all expectations in the destroy_user spec
|
198
|
+
```
|
199
|
+
|
200
|
+
#### Targeting Individual Expectations
|
201
|
+
|
202
|
+
You can also run individual expectations within a spec. The format depends on whether the expectation has a name:
|
203
|
+
|
204
|
+
```yaml
|
205
|
+
# specs/users.yml
|
206
|
+
destroy_user:
|
207
|
+
path: /users/:id
|
208
|
+
method: delete
|
209
|
+
expectations:
|
210
|
+
- expect: # Unnamed expectation
|
211
|
+
status: 200
|
212
|
+
- name: "Destroys a User" # Named expectation
|
213
|
+
expect:
|
214
|
+
status: 200
|
215
|
+
```
|
216
|
+
|
217
|
+
For named expectations:
|
218
|
+
```bash
|
219
|
+
# Format: <file>:<spec>:'<verb> <path> - <name>'
|
220
|
+
spec_forge users:destroy_user:'DELETE /users/:id - Destroys a User'
|
221
|
+
```
|
222
|
+
|
223
|
+
For unnamed expectations:
|
224
|
+
```bash
|
225
|
+
# Format: <file>:<spec>:'<verb> <path>'
|
226
|
+
spec_forge users:destroy_user:'DELETE /users/:id'
|
227
|
+
```
|
228
|
+
|
229
|
+
**Note**: When targeting an unnamed expectation, SpecForge executes all matching expectations within that spec. This means if you have multiple unnamed expectations with the same verb and path, they will all run.
|
230
|
+
|
170
231
|
## Configuration
|
171
232
|
|
172
233
|
### Basic Configuration
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
#
|
5
|
+
# Used internally by RSpec
|
6
|
+
# This class handles formatting backtraces, hence the name ;)
|
7
|
+
#
|
8
|
+
module BacktraceFormatter
|
9
|
+
def self.formatter
|
10
|
+
@formatter ||= RSpec::Core::BacktraceFormatter.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.backtrace_line(line)
|
14
|
+
formatter.backtrace_line(line)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.format_backtrace(backtrace, example_metadata)
|
18
|
+
backtrace = SpecForge.backtrace_cleaner.clean(backtrace)
|
19
|
+
|
20
|
+
location = example_metadata[:example_group][:location]
|
21
|
+
line_number = example_metadata[:example_group][:line_number]
|
22
|
+
|
23
|
+
# Add the yaml location to the front so it's the first thing people see
|
24
|
+
["#{location}:#{line_number}"] + backtrace
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/spec_forge/cli/new.rb
CHANGED
@@ -38,123 +38,33 @@ module SpecForge
|
|
38
38
|
private
|
39
39
|
|
40
40
|
def create_new_spec(name)
|
41
|
-
actions.
|
41
|
+
actions.template(
|
42
|
+
"new_spec.tt",
|
42
43
|
SpecForge.forge.join("specs", "#{name}.yml"),
|
43
|
-
|
44
|
+
context: Proxy.new(name).call
|
44
45
|
)
|
45
46
|
end
|
46
47
|
|
47
48
|
def create_new_factory(name)
|
48
|
-
actions.
|
49
|
+
actions.template(
|
50
|
+
"new_factory.tt",
|
49
51
|
SpecForge.forge.join("factories", "#{name}.yml"),
|
50
|
-
|
52
|
+
context: Proxy.new(name).call
|
51
53
|
)
|
52
54
|
end
|
53
55
|
|
54
|
-
|
55
|
-
|
56
|
-
singular_name = name.singularize
|
56
|
+
class Proxy
|
57
|
+
attr_reader :original_name, :singular_name, :plural_name
|
57
58
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
##################################################
|
63
|
-
"index_#{plural_name}" => base_spec.merge(
|
64
|
-
url: "/#{plural_name}",
|
65
|
-
expectations: [base_constraint]
|
66
|
-
),
|
67
|
-
##################################################
|
68
|
-
"show_#{singular_name}" => base_spec.merge(
|
69
|
-
url: "/#{plural_name}/{id}",
|
70
|
-
expectations: [
|
71
|
-
base_constraint.merge(expect: {status: 404}),
|
72
|
-
base_constraint.deep_merge(
|
73
|
-
query: {id: 1},
|
74
|
-
expect: {
|
75
|
-
json: {
|
76
|
-
name: "kind_of.string",
|
77
|
-
email: /\w+@example\.com/i
|
78
|
-
}
|
79
|
-
}
|
80
|
-
)
|
81
|
-
]
|
82
|
-
),
|
83
|
-
##################################################
|
84
|
-
"create_#{singular_name}" => base_spec.merge(
|
85
|
-
url: "/#{plural_name}",
|
86
|
-
method: "post",
|
87
|
-
expectations: [
|
88
|
-
base_constraint.merge(expect: {status: 400}),
|
89
|
-
base_constraint.deep_merge(
|
90
|
-
variables: {
|
91
|
-
name: "faker.name.name",
|
92
|
-
role: "user"
|
93
|
-
},
|
94
|
-
body: {name: "variables.name"},
|
95
|
-
expect: {
|
96
|
-
json: {name: "variables.name", role: "variables.role"}
|
97
|
-
}
|
98
|
-
)
|
99
|
-
]
|
100
|
-
),
|
101
|
-
##################################################
|
102
|
-
"update_#{singular_name}" => base_spec.merge(
|
103
|
-
url: "/#{plural_name}/{id}",
|
104
|
-
method: "patch",
|
105
|
-
query: {id: 1},
|
106
|
-
variables: {
|
107
|
-
number: {
|
108
|
-
"faker.number.between" => {from: 100_000, to: 999_999}
|
109
|
-
}
|
110
|
-
},
|
111
|
-
expectations: [
|
112
|
-
base_constraint.deep_merge(
|
113
|
-
body: {number: "variables.number"},
|
114
|
-
expect: {
|
115
|
-
json: {name: "kind_of.string", number: "kind_of.integer"}
|
116
|
-
}
|
117
|
-
)
|
118
|
-
]
|
119
|
-
),
|
120
|
-
##################################################
|
121
|
-
"destroy_#{singular_name}" => base_spec.merge(
|
122
|
-
url: "/#{plural_name}/{id}",
|
123
|
-
method: "delete",
|
124
|
-
query: {id: 1},
|
125
|
-
expectations: [
|
126
|
-
base_constraint
|
127
|
-
]
|
128
|
-
)
|
129
|
-
}
|
130
|
-
|
131
|
-
generate_yaml(hash)
|
132
|
-
end
|
133
|
-
|
134
|
-
def generate_factory(name)
|
135
|
-
singular_name = name.singularize
|
136
|
-
|
137
|
-
hash = {
|
138
|
-
singular_name => {
|
139
|
-
class: singular_name.titleize,
|
140
|
-
attributes: {
|
141
|
-
attribute: "value"
|
142
|
-
}
|
143
|
-
}
|
144
|
-
}
|
145
|
-
|
146
|
-
generate_yaml(hash)
|
147
|
-
end
|
148
|
-
|
149
|
-
def generate_yaml(hash)
|
150
|
-
result = hash.deep_stringify_keys.join_map("\n") do |key, value|
|
151
|
-
{key => value}.to_yaml
|
152
|
-
.sub!("---\n", "")
|
153
|
-
.gsub("!ruby/regexp ", "")
|
59
|
+
def initialize(name)
|
60
|
+
@original_name = name
|
61
|
+
@plural_name = name.pluralize
|
62
|
+
@singular_name = name.singularize
|
154
63
|
end
|
155
64
|
|
156
|
-
|
157
|
-
|
65
|
+
def call
|
66
|
+
binding
|
67
|
+
end
|
158
68
|
end
|
159
69
|
end
|
160
70
|
end
|
data/lib/spec_forge/cli/run.rb
CHANGED
@@ -4,13 +4,63 @@ module SpecForge
|
|
4
4
|
class CLI
|
5
5
|
class Run < Command
|
6
6
|
command_name "run"
|
7
|
-
syntax "run"
|
8
|
-
|
7
|
+
syntax "run [target]"
|
8
|
+
|
9
|
+
summary "Runs specs loaded from spec_forge/specs/"
|
10
|
+
description "Runs specs loaded from spec_forge/specs/. The optional target argument allows running specific files, specs, or expectations."
|
11
|
+
|
12
|
+
example "spec_forge run",
|
13
|
+
"Run all specs in spec_forge/specs/"
|
14
|
+
|
15
|
+
example "spec_forge run users",
|
16
|
+
"Run all specs in users.yml"
|
17
|
+
|
18
|
+
example "spec_forge run users:create_user",
|
19
|
+
"Run all expectations in the create_user spec"
|
20
|
+
|
21
|
+
example "spec_forge run users:create_user:\"POST /users\"",
|
22
|
+
"Run expectations matching POST /users"
|
23
|
+
|
24
|
+
example "spec_forge run users:create_user:\"POST /users - Create Admin\"",
|
25
|
+
"Run the specific expectation named \"Create Admin\""
|
9
26
|
|
10
27
|
# option "-n", "--no-docs", "Do not generate OpenAPI documentation on completion"
|
11
28
|
|
12
29
|
def call
|
13
|
-
SpecForge.run
|
30
|
+
return SpecForge.run if arguments.blank?
|
31
|
+
|
32
|
+
# spec_forge users:show_user
|
33
|
+
filter = extract_filter(arguments.first)
|
34
|
+
|
35
|
+
# Filter and run the specs
|
36
|
+
SpecForge.run(**filter)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
#
|
42
|
+
# The input can be
|
43
|
+
#
|
44
|
+
# "<file_name>" for a file
|
45
|
+
# Example: "users"
|
46
|
+
#
|
47
|
+
# "<file_name>:<spec_name>" for a single spec
|
48
|
+
# Example: "users:show_user"
|
49
|
+
#
|
50
|
+
# "<file_name:<spec_name>:'<verb> <path> - <?name>'" for a single expectation
|
51
|
+
# Example:
|
52
|
+
# "users:show_user:'GET /users/:id'"
|
53
|
+
# Example with name:
|
54
|
+
# "users:show_user:'GET /users/:id - Returns 404 due to missing user'"
|
55
|
+
#
|
56
|
+
def extract_filter(input)
|
57
|
+
# Note: Only split 3 because the expectation name can have colons in them.
|
58
|
+
file_name, spec_name, expectation_name = input.split(":", 3).map(&:strip)
|
59
|
+
|
60
|
+
# Remove the quotes
|
61
|
+
expectation_name.gsub!(/^['"]|['"]$/, "") if expectation_name.present?
|
62
|
+
|
63
|
+
{file_name:, spec_name:, expectation_name:}
|
14
64
|
end
|
15
65
|
end
|
16
66
|
end
|
@@ -30,6 +30,9 @@ module SpecForge
|
|
30
30
|
def initialize
|
31
31
|
config = Normalizer.default_configuration
|
32
32
|
|
33
|
+
# Allows me to modify the error backtrace reporting within rspec
|
34
|
+
RSpec.configuration.instance_variable_set(:@backtrace_formatter, BacktraceFormatter)
|
35
|
+
|
33
36
|
config[:base_url] = "http://localhost:3000"
|
34
37
|
config[:factories] = Factories.new
|
35
38
|
config[:specs] = RSpec.configuration
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module Core
|
5
|
+
module Notifications
|
6
|
+
#
|
7
|
+
# I did attempt to do this without monkey patching
|
8
|
+
# Getting around the `rspec` word was making it difficult
|
9
|
+
#
|
10
|
+
class SummaryNotification
|
11
|
+
# Customizes RSpec's failure output to:
|
12
|
+
# 1. Use 'spec_forge' instead of 'rspec' for rerun commands
|
13
|
+
# 2. Remove line numbers since SpecForge uses dynamic spec generation
|
14
|
+
alias_method :og_colorized_rerun_commands, :colorized_rerun_commands
|
15
|
+
|
16
|
+
def colorized_rerun_commands(colorizer)
|
17
|
+
# Updating these at this point fixes the re-run for some failures - it depends
|
18
|
+
failed_examples.each do |example|
|
19
|
+
metadata = example.metadata[:example_group]
|
20
|
+
|
21
|
+
# I might've uncovered an inconsistency here
|
22
|
+
# When multiple specs fail, it appears that the rerun_commands will use
|
23
|
+
# :rerun_file_path from the example's metadata.
|
24
|
+
# But when a single spec is ran and fails, it's using :location.
|
25
|
+
example.metadata[:location] = metadata[:rerun_file_path]
|
26
|
+
example.metadata[:line_number] = metadata[:line_number]
|
27
|
+
end
|
28
|
+
|
29
|
+
og_colorized_rerun_commands.gsub(/rspec/i, "spec_forge")
|
30
|
+
.gsub(/\[[\d:]+\]/, "")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/spec_forge/factory.rb
CHANGED
@@ -5,32 +5,30 @@ module SpecForge
|
|
5
5
|
#
|
6
6
|
# Loads the factories from their yml files and registers them with FactoryBot
|
7
7
|
#
|
8
|
-
|
9
|
-
#
|
10
|
-
def self.load_and_register(base_path)
|
8
|
+
def self.load_and_register
|
11
9
|
if SpecForge.configuration.factories.paths?
|
12
10
|
FactoryBot.definition_file_paths = SpecForge.configuration.factories.paths
|
13
11
|
end
|
14
12
|
|
15
13
|
FactoryBot.find_definitions if SpecForge.configuration.factories.auto_discover?
|
16
14
|
|
17
|
-
factories =
|
15
|
+
factories = load_from_files
|
18
16
|
factories.each(&:register)
|
19
17
|
end
|
20
18
|
|
21
19
|
#
|
22
|
-
# Loads any factories defined in the
|
23
|
-
#
|
24
|
-
# @param path [String, Path] The path where the factories are located
|
20
|
+
# Loads any factories defined in the factories. A single file can contain one or more factories
|
25
21
|
#
|
26
22
|
# @return [Array<Factory>] An array of factories that were loaded.
|
27
23
|
# Note: This factories have not been registered with FactoryBot.
|
28
24
|
# See #register
|
29
25
|
#
|
30
|
-
def self.
|
26
|
+
def self.load_from_files
|
27
|
+
path = SpecForge.forge.join("factories", "**/*.yml")
|
28
|
+
|
31
29
|
factories = []
|
32
30
|
|
33
|
-
Dir[path].
|
31
|
+
Dir[path].each do |file_path|
|
34
32
|
hash = YAML.load_file(file_path).deep_symbolize_keys
|
35
33
|
|
36
34
|
hash.each do |factory_name, factory_hash|
|
data/lib/spec_forge/runner.rb
CHANGED
@@ -23,6 +23,9 @@ module SpecForge
|
|
23
23
|
spec_forge.expectations.each do |expectation|
|
24
24
|
# Define the example group
|
25
25
|
describe(expectation.name) do
|
26
|
+
# Set up the class metadata for error reporting
|
27
|
+
runner_forge.set_group_metadata(self, spec_forge, expectation)
|
28
|
+
|
26
29
|
constraints = expectation.constraints
|
27
30
|
|
28
31
|
let!(:expected_status) { constraints.status.resolve }
|
@@ -31,6 +34,9 @@ module SpecForge
|
|
31
34
|
before do
|
32
35
|
# Ensure all variables are called and resolved, in case they are not referenced
|
33
36
|
expectation.variables.resolve
|
37
|
+
|
38
|
+
# Set up the example metadata for error reporting
|
39
|
+
runner_forge.set_example_metadata(spec_forge, expectation)
|
34
40
|
end
|
35
41
|
|
36
42
|
subject(:response) { expectation.http_client.call }
|
@@ -54,9 +60,31 @@ module SpecForge
|
|
54
60
|
end
|
55
61
|
end
|
56
62
|
|
63
|
+
# @private
|
57
64
|
def handle_debug(...)
|
58
65
|
DebugProxy.new(...).call
|
59
66
|
end
|
67
|
+
|
68
|
+
# @private
|
69
|
+
def set_group_metadata(context, spec, expectation)
|
70
|
+
metadata = {
|
71
|
+
file_path: spec.file_path,
|
72
|
+
absolute_file_path: spec.file_path,
|
73
|
+
line_number: spec.line_number,
|
74
|
+
location: spec.file_path,
|
75
|
+
rerun_file_path: "#{spec.file_name}:#{spec.name}:\"#{expectation.name}\""
|
76
|
+
}
|
77
|
+
|
78
|
+
context.metadata.merge!(metadata)
|
79
|
+
end
|
80
|
+
|
81
|
+
# @private
|
82
|
+
def set_example_metadata(spec, expectation)
|
83
|
+
# This is needed when an error raises in an example
|
84
|
+
metadata = {location: "#{spec.file_path}:#{spec.line_number}"}
|
85
|
+
|
86
|
+
RSpec.current_example.metadata.merge!(metadata)
|
87
|
+
end
|
60
88
|
end
|
61
89
|
|
62
90
|
################################################################################################
|
data/lib/spec_forge/spec.rb
CHANGED
@@ -5,31 +5,49 @@ require_relative "spec/expectation"
|
|
5
5
|
module SpecForge
|
6
6
|
class Spec
|
7
7
|
#
|
8
|
-
# Loads
|
8
|
+
# Loads and defines specs with the runner. Specs can be filtered using the optional parameters
|
9
9
|
#
|
10
|
-
# @param
|
10
|
+
# @param file_name [String, nil] The name of the file without the extension.
|
11
|
+
# @param spec_name [String, nil] The name of the spec in a yaml file
|
12
|
+
# @param expectation_name [String, nil] The name of the expectation for a spec.
|
11
13
|
#
|
12
|
-
|
13
|
-
|
14
|
+
# @return [Array<Spec>]
|
15
|
+
#
|
16
|
+
def self.load_and_define(file_name: nil, spec_name: nil, expectation_name: nil)
|
17
|
+
specs = load_from_files
|
18
|
+
|
19
|
+
filter_specs(specs, file_name:, spec_name:, expectation_name:)
|
20
|
+
|
21
|
+
# Announce if we're using a filter
|
22
|
+
if file_name
|
23
|
+
filter = {file_name:, spec_name:, expectation_name:}.delete_if { |k, v| v.blank? }
|
24
|
+
filter.stringify_keys!
|
25
|
+
puts "Using filter: #{filter}"
|
26
|
+
end
|
27
|
+
|
14
28
|
specs.each(&:define)
|
15
29
|
end
|
16
30
|
|
17
31
|
#
|
18
|
-
# Loads any specs defined in the
|
19
|
-
#
|
20
|
-
# @param path [String, Path] The path where the specs are located
|
32
|
+
# Loads any specs defined in the spec files. A single file can contain one or more specs
|
21
33
|
#
|
22
34
|
# @return [Array<Spec>] An array of specs that were loaded.
|
23
35
|
#
|
24
|
-
def self.
|
36
|
+
def self.load_from_files
|
37
|
+
path = SpecForge.forge.join("specs")
|
25
38
|
specs = []
|
26
39
|
|
27
|
-
Dir[path].
|
28
|
-
|
40
|
+
Dir[path.join("**/*.yml")].each do |file_path|
|
41
|
+
content = File.read(file_path)
|
42
|
+
hash = YAML.load(content).deep_symbolize_keys
|
29
43
|
|
30
44
|
hash.each do |spec_name, spec_hash|
|
31
|
-
|
45
|
+
line_number = content.lines.index { |line| line.start_with?("#{spec_name}:") }
|
46
|
+
|
47
|
+
spec_hash[:name] = spec_name.to_s
|
32
48
|
spec_hash[:file_path] = file_path
|
49
|
+
spec_hash[:file_name] = file_path.delete_prefix("#{path}/").delete_suffix(".yml")
|
50
|
+
spec_hash[:line_number] = line_number ? line_number + 1 : -1
|
33
51
|
|
34
52
|
specs << new(**spec_hash)
|
35
53
|
end
|
@@ -38,11 +56,34 @@ module SpecForge
|
|
38
56
|
specs
|
39
57
|
end
|
40
58
|
|
59
|
+
# @private
|
60
|
+
def self.filter_specs(specs, file_name: nil, spec_name: nil, expectation_name: nil)
|
61
|
+
# Guard against invalid partial filters
|
62
|
+
if expectation_name && spec_name.blank?
|
63
|
+
raise ArgumentError, "The spec's name is required when filtering by an expectation's name"
|
64
|
+
end
|
65
|
+
|
66
|
+
if spec_name && file_name.blank?
|
67
|
+
raise ArgumentError, "The spec's filename is required when filtering by a spec's name"
|
68
|
+
end
|
69
|
+
|
70
|
+
specs.select! { |spec| spec.file_name == file_name } if file_name
|
71
|
+
specs.select! { |spec| spec.name == spec_name } if spec_name
|
72
|
+
|
73
|
+
if expectation_name
|
74
|
+
specs.each do |spec|
|
75
|
+
spec.expectations.select! { |expectation| expectation.name == expectation_name }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
specs
|
80
|
+
end
|
81
|
+
|
41
82
|
############################################################################
|
42
83
|
|
43
84
|
attr_predicate :debug
|
44
85
|
|
45
|
-
attr_reader :name, :file_path, :expectations
|
86
|
+
attr_reader :name, :file_path, :file_name, :line_number, :expectations
|
46
87
|
|
47
88
|
#
|
48
89
|
# Creates a Spec based on the input
|
@@ -52,9 +93,11 @@ module SpecForge
|
|
52
93
|
# @param **input [Hash] Any attributes related to the spec, including expectations
|
53
94
|
# See Normalizer::Spec
|
54
95
|
#
|
55
|
-
def initialize(name:, file_path:, **input)
|
96
|
+
def initialize(name:, file_path:, file_name:, line_number:, **input)
|
56
97
|
@name = name
|
57
98
|
@file_path = file_path
|
99
|
+
@file_name = file_name
|
100
|
+
@line_number = line_number
|
58
101
|
|
59
102
|
input = Normalizer.normalize_spec!(input)
|
60
103
|
|
data/lib/spec_forge/version.rb
CHANGED
data/lib/spec_forge.rb
CHANGED
@@ -17,8 +17,10 @@ require "thor"
|
|
17
17
|
require "yaml"
|
18
18
|
|
19
19
|
require_relative "spec_forge/attribute"
|
20
|
+
require_relative "spec_forge/backtrace_formatter"
|
20
21
|
require_relative "spec_forge/cli"
|
21
22
|
require_relative "spec_forge/configuration"
|
23
|
+
require_relative "spec_forge/core_ext"
|
22
24
|
require_relative "spec_forge/error"
|
23
25
|
require_relative "spec_forge/factory"
|
24
26
|
require_relative "spec_forge/http"
|
@@ -34,15 +36,21 @@ module SpecForge
|
|
34
36
|
#
|
35
37
|
# @param path [String] The file path that contains factories and specs
|
36
38
|
#
|
37
|
-
def self.run(
|
39
|
+
def self.run(file_name: nil, spec_name: nil, expectation_name: nil)
|
40
|
+
path = SpecForge.forge
|
41
|
+
|
42
|
+
# Initialize
|
38
43
|
forge_helper = path.join("forge_helper.rb")
|
39
44
|
require_relative forge_helper if File.exist?(forge_helper)
|
40
45
|
|
46
|
+
# Validate
|
41
47
|
configuration.validate
|
42
48
|
|
43
|
-
|
44
|
-
|
49
|
+
# Prepare
|
50
|
+
Factory.load_and_register
|
51
|
+
Spec.load_and_define(file_name:, spec_name:, expectation_name:)
|
45
52
|
|
53
|
+
# Run
|
46
54
|
Runner.run
|
47
55
|
end
|
48
56
|
|
@@ -0,0 +1,43 @@
|
|
1
|
+
index_<%= plural_name %>:
|
2
|
+
url: /<%= plural_name %>
|
3
|
+
expectations:
|
4
|
+
- expect:
|
5
|
+
status: 200
|
6
|
+
|
7
|
+
show_<%= singular_name %>:
|
8
|
+
url: /<%= plural_name %>/{id}
|
9
|
+
query:
|
10
|
+
id: 1
|
11
|
+
expectations:
|
12
|
+
- expect:
|
13
|
+
status: 200
|
14
|
+
|
15
|
+
create_<%= singular_name %>:
|
16
|
+
url: /<%= plural_name %>
|
17
|
+
method: post
|
18
|
+
body:
|
19
|
+
name: faker.name.name
|
20
|
+
email: faker.internet.email
|
21
|
+
expectations:
|
22
|
+
- expect:
|
23
|
+
status: 200
|
24
|
+
|
25
|
+
update_<%= singular_name %>:
|
26
|
+
url: /<%= plural_name %>/{id}
|
27
|
+
method: patch
|
28
|
+
query:
|
29
|
+
id: 1
|
30
|
+
body:
|
31
|
+
name:
|
32
|
+
expectations:
|
33
|
+
- expect:
|
34
|
+
status: 200
|
35
|
+
|
36
|
+
destroy_<%= singular_name %>:
|
37
|
+
url: /<%= plural_name %>/{id}
|
38
|
+
method: delete
|
39
|
+
query:
|
40
|
+
id: 1
|
41
|
+
expectations:
|
42
|
+
- expect:
|
43
|
+
status: 200
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spec_forge
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bryan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-02-
|
11
|
+
date: 2025-02-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '7.
|
19
|
+
version: '7.1'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '7.
|
26
|
+
version: '7.1'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: commander
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -44,14 +44,14 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '0.
|
47
|
+
version: '0.2'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '0.
|
54
|
+
version: '0.2'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: factory_bot
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -172,6 +172,7 @@ files:
|
|
172
172
|
- lib/spec_forge/attribute/resolvable_hash.rb
|
173
173
|
- lib/spec_forge/attribute/transform.rb
|
174
174
|
- lib/spec_forge/attribute/variable.rb
|
175
|
+
- lib/spec_forge/backtrace_formatter.rb
|
175
176
|
- lib/spec_forge/cli.rb
|
176
177
|
- lib/spec_forge/cli/actions.rb
|
177
178
|
- lib/spec_forge/cli/command.rb
|
@@ -179,6 +180,8 @@ files:
|
|
179
180
|
- lib/spec_forge/cli/new.rb
|
180
181
|
- lib/spec_forge/cli/run.rb
|
181
182
|
- lib/spec_forge/configuration.rb
|
183
|
+
- lib/spec_forge/core_ext.rb
|
184
|
+
- lib/spec_forge/core_ext/rspec.rb
|
182
185
|
- lib/spec_forge/error.rb
|
183
186
|
- lib/spec_forge/factory.rb
|
184
187
|
- lib/spec_forge/http.rb
|
@@ -200,6 +203,8 @@ files:
|
|
200
203
|
- lib/spec_forge/type.rb
|
201
204
|
- lib/spec_forge/version.rb
|
202
205
|
- lib/templates/forge_helper.tt
|
206
|
+
- lib/templates/new_factory.tt
|
207
|
+
- lib/templates/new_spec.tt
|
203
208
|
- spec_forge/factories/user.yml
|
204
209
|
- spec_forge/forge_helper.rb
|
205
210
|
- spec_forge/specs/users.yml
|