brutal 1.5.0 → 1.6.0.beta2
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/README.md +38 -38
- data/bin/brutal +12 -6
- data/lib/brutal/command_line_arguments_parser.rb +116 -0
- data/lib/brutal/format/ruby/filename.rb +15 -0
- data/lib/brutal/format/ruby.rb +163 -0
- data/lib/brutal/format.rb +11 -0
- data/lib/brutal/manifest/file/name.rb +18 -0
- data/lib/brutal/manifest/file.rb +24 -0
- data/lib/brutal/manifest.rb +107 -0
- data/lib/brutal.rb +23 -26
- metadata +11 -24
- data/lib/brutal/configuration.rb +0 -55
- data/lib/brutal/file/read.rb +0 -28
- data/lib/brutal/file/write.rb +0 -31
- data/lib/brutal/file.rb +0 -31
- data/lib/brutal/scaffold.rb +0 -113
- data/lib/brutal/yaml.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2a8a912485598e917360c280db92eee2b3acc53180dd0587b2147ea0eb26e649
|
4
|
+
data.tar.gz: 5a7a1f13b748e8deb7d5a1529ca2e11eb35b687aabab84f7b9fc9ebc58d0b174
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ec1ffa0f11a17812ff0000bc7e8b4ba8a04c826e37f9ef5e777681312fcb87962020f034b4802b617471894019f9c046598a012eeb6491c6548f0fef233af66d
|
7
|
+
data.tar.gz: 363647b4219053d2433fa198aac832b8f8f630e36d7b9282f0c36c34c2a255e5874ee91e481440be6bea2c87bb7763225ddbd04bfbf0204ad949bea98b5fbb01
|
data/README.md
CHANGED
@@ -38,7 +38,7 @@ It is therefore the responsibility of the developer to analyze the generated beh
|
|
38
38
|
Add this line to your application's Gemfile:
|
39
39
|
|
40
40
|
```ruby
|
41
|
-
gem "brutal"
|
41
|
+
gem "brutal", ">= 1.6.0.beta2", require: false
|
42
42
|
```
|
43
43
|
|
44
44
|
And then execute:
|
@@ -50,84 +50,84 @@ bundle install
|
|
50
50
|
Or install it yourself as:
|
51
51
|
|
52
52
|
```sh
|
53
|
-
gem install brutal
|
53
|
+
gem install brutal --pre
|
54
54
|
```
|
55
55
|
|
56
|
-
## Quick Start
|
57
|
-
|
58
|
-
Just type `brutal` in a Ruby project's folder and watch the magic happen.
|
59
|
-
|
60
56
|
## Usage
|
61
57
|
|
62
|
-
|
63
|
-
|
64
|
-
|
58
|
+
### YAML manifest
|
59
|
+
|
60
|
+
__Brutal__ needs configuration files in YAML format to know how to write tests.
|
61
|
+
Configuration file names are suffixed by `_brutal.yaml` and composed of 7 top-level sections:
|
65
62
|
|
66
|
-
* `header` - Specifies
|
63
|
+
* `header` - Specifies a block of lines to be executed once before all examples.
|
64
|
+
* `before` - Specifies a block of lines to be executed before each example.
|
67
65
|
* `subject` - Specifies the template of the code to be declined across contexts.
|
68
66
|
* `contexts` - Specifies a list of variables to populate the subject's template.
|
69
67
|
* `actuals` - Specifies templates to challenge evaluated subjects & get results.
|
68
|
+
* `after` - Specifies a block of lines to be executed after each example.
|
69
|
+
* `footer` - Specifies a block of lines to be executed once after all examples.
|
70
70
|
|
71
|
-
|
71
|
+
### Command line
|
72
|
+
|
73
|
+
The `brutal` command comes with several options you can use to customize Brutal's behavior.
|
74
|
+
|
75
|
+
For a full list of options, run the `brutal` command with the `--help` flag:
|
72
76
|
|
73
77
|
```sh
|
74
|
-
brutal
|
78
|
+
brutal --help
|
75
79
|
```
|
76
80
|
|
77
|
-
|
81
|
+
```txt
|
82
|
+
Usage: brutal [options] [files or directories]
|
78
83
|
|
79
|
-
|
80
|
-
|
84
|
+
--format=FORMAT Choose "ruby" (default).
|
85
|
+
--help Display this help.
|
86
|
+
--version Display the version.
|
81
87
|
```
|
82
88
|
|
83
|
-
|
89
|
+
Assuming that in the workspace there is a configuration file named `user_brutal.yaml`, the test suite can be generated via one of these commands:
|
84
90
|
|
85
91
|
```sh
|
86
|
-
brutal
|
92
|
+
brutal user_brutal.yaml
|
87
93
|
```
|
88
94
|
|
89
|
-
|
90
|
-
|
91
|
-
Configuration files can also be named differently:
|
95
|
+
or:
|
92
96
|
|
93
97
|
```sh
|
94
|
-
brutal
|
98
|
+
brutal .
|
95
99
|
```
|
96
100
|
|
97
|
-
|
98
|
-
|
99
|
-
To avoid accidentally overwriting a file, the `--no-force` option can be used:
|
101
|
+
or even:
|
100
102
|
|
101
103
|
```sh
|
102
|
-
brutal
|
104
|
+
brutal
|
103
105
|
```
|
104
106
|
|
105
|
-
|
107
|
+
This would create a `test_user.rb` file containing the test suite.
|
106
108
|
|
107
|
-
|
109
|
+
Assuming now that in the workspace there are a large number of configuration files named in the `spec/` folder, the complete test suite could be generated recursively via this command:
|
108
110
|
|
109
|
-
|
110
|
-
|
111
|
-
|
111
|
+
```sh
|
112
|
+
brutal spec/
|
113
|
+
```
|
114
|
+
|
115
|
+
This would create one test file per configuration file matching `./spec/**/*_brutal.yaml` in to `./spec/**/test_*.rb`.
|
112
116
|
|
113
|
-
###
|
117
|
+
### Some examples
|
114
118
|
|
115
|
-
<https://github.com/fixrb/brutal/
|
119
|
+
<https://github.com/fixrb/brutal/tree/main/examples/>
|
116
120
|
|
117
121
|
## Rake integration example
|
118
122
|
|
119
|
-
|
123
|
+
Generated test suite files could be matched as follows:
|
120
124
|
|
121
125
|
```ruby
|
122
126
|
Rake::TestTask.new do |t|
|
123
|
-
t.pattern = "
|
127
|
+
t.pattern = "**/test_*.rb"
|
124
128
|
end
|
125
129
|
```
|
126
130
|
|
127
|
-
## Test suite
|
128
|
-
|
129
|
-
__Brutal__'s test set is brutally self-generated here: [./test.rb](https://github.com/fixrb/brutal/blob/main/test.rb)
|
130
|
-
|
131
131
|
## Contact
|
132
132
|
|
133
133
|
* Source code: https://github.com/fixrb/brutal
|
data/bin/brutal
CHANGED
@@ -1,12 +1,18 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
|
4
|
+
require_relative File.join("..", "lib", "brutal", "command_line_arguments_parser")
|
5
|
+
|
6
|
+
formats, pathnames = Brutal::CommandLineArgumentsParser.new(*ARGV).call
|
7
|
+
|
5
8
|
require_relative File.join("..", "lib", "brutal")
|
6
9
|
|
7
|
-
|
8
|
-
|
9
|
-
pathname += Brutal::File::DEFAULT_CONFIG_FILENAME if pathname.directory?
|
10
|
-
force_opt = ARGV.none?("--no-force")
|
10
|
+
formats.each do |format|
|
11
|
+
generator = Brutal.new(format)
|
11
12
|
|
12
|
-
|
13
|
+
pathnames.each do |pathname|
|
14
|
+
Dir.chdir(pathname.dirname) do
|
15
|
+
generator.call(pathname.basename)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative File.join("manifest", "file", "name")
|
4
|
+
|
5
|
+
require "pathname"
|
6
|
+
|
7
|
+
class Brutal
|
8
|
+
# Accept an arbitrary number of arguments passed from the command-line.
|
9
|
+
class CommandLineArgumentsParser
|
10
|
+
MANIFEST_FILENAME_SUFFIX = Manifest::File::Name::SUFFIX
|
11
|
+
MANIFEST_FILENAME_PATTERN = ::File.join("**", "*#{MANIFEST_FILENAME_SUFFIX}")
|
12
|
+
CURRENT_DIRECTORY_CONTEXT = "."
|
13
|
+
GEM_NAME = "brutal"
|
14
|
+
HELP_OPTION = "--help"
|
15
|
+
VERSION_OPTION = "--version"
|
16
|
+
RUBY_FORMAT_OPTION = "--format=ruby"
|
17
|
+
DEFAULT_FORMAT_OPTION = RUBY_FORMAT_OPTION
|
18
|
+
|
19
|
+
FORMAT_OPTIONS = {
|
20
|
+
RUBY_FORMAT_OPTION => :Ruby
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
DEFAULT_FORMAT = FORMAT_OPTIONS.fetch(DEFAULT_FORMAT_OPTION)
|
24
|
+
DEFAULT_FORMATS = [DEFAULT_FORMAT].freeze
|
25
|
+
|
26
|
+
attr_reader :pathnames
|
27
|
+
|
28
|
+
def initialize(*args)
|
29
|
+
args.each do |arg|
|
30
|
+
help! if arg == HELP_OPTION
|
31
|
+
version! if arg == VERSION_OPTION
|
32
|
+
end
|
33
|
+
|
34
|
+
@formats = []
|
35
|
+
@pathnames = []
|
36
|
+
|
37
|
+
args << CURRENT_DIRECTORY_CONTEXT unless any_path_given?(*args)
|
38
|
+
args.each { |arg| parse!(arg) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def call
|
42
|
+
[formats, pathnames]
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def formats
|
48
|
+
@formats.empty? ? DEFAULT_FORMATS : @formats
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse!(arg)
|
52
|
+
case arg
|
53
|
+
when RUBY_FORMAT_OPTION
|
54
|
+
@formats << FORMAT_OPTIONS.fetch(RUBY_FORMAT_OPTION)
|
55
|
+
else
|
56
|
+
load!(::Pathname.new(arg))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def help!
|
61
|
+
puts help_message
|
62
|
+
exit
|
63
|
+
end
|
64
|
+
|
65
|
+
def help_message
|
66
|
+
<<~TXT
|
67
|
+
Usage: #{$PROGRAM_NAME} [options] [files or directories]
|
68
|
+
|
69
|
+
--format=FORMAT Choose "ruby" (default).
|
70
|
+
--help Display this help.
|
71
|
+
--version Display the version.
|
72
|
+
TXT
|
73
|
+
end
|
74
|
+
|
75
|
+
def load!(pathname)
|
76
|
+
if pathname.directory?
|
77
|
+
pathname.glob(MANIFEST_FILENAME_PATTERN).each { |filename| load!(filename) }
|
78
|
+
elsif pathname.file?
|
79
|
+
load_file!(pathname)
|
80
|
+
else
|
81
|
+
raise ::ArgumentError, "#{pathname} is neither a file nor a directory."
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def load_file!(pathname)
|
86
|
+
if pathname.fnmatch?(MANIFEST_FILENAME_PATTERN)
|
87
|
+
@pathnames << pathname
|
88
|
+
else
|
89
|
+
warn "Skipping #{pathname} because not matched against #{MANIFEST_FILENAME_PATTERN}."
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def any_path_given?(*args)
|
94
|
+
path_args(*args).any?
|
95
|
+
end
|
96
|
+
|
97
|
+
def path_args(*args)
|
98
|
+
args - FORMAT_OPTIONS.keys
|
99
|
+
end
|
100
|
+
|
101
|
+
def version!
|
102
|
+
abort "Gem not found on the system. Unknown version." if not_loaded_spec?
|
103
|
+
|
104
|
+
puts loaded_spec.version
|
105
|
+
exit
|
106
|
+
end
|
107
|
+
|
108
|
+
def loaded_spec
|
109
|
+
::Gem.loaded_specs[GEM_NAME]
|
110
|
+
end
|
111
|
+
|
112
|
+
def not_loaded_spec?
|
113
|
+
loaded_spec.nil?
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative File.join("ruby", "filename")
|
4
|
+
|
5
|
+
class Brutal
|
6
|
+
module Format
|
7
|
+
# Ruby format class
|
8
|
+
class Ruby
|
9
|
+
# Whitespace character.
|
10
|
+
SPACE = " "
|
11
|
+
|
12
|
+
# Hyphen-minus character.
|
13
|
+
HYPHEN_MINUS = "-"
|
14
|
+
|
15
|
+
# Two spaces per indentation level.
|
16
|
+
INDENTATION = SPACE * 2
|
17
|
+
|
18
|
+
# Specifies templates to challenge evaluated subjects & get results.
|
19
|
+
attr_reader :actuals
|
20
|
+
|
21
|
+
# Specifies a list of variables to populate the subject's template.
|
22
|
+
attr_reader :contexts
|
23
|
+
|
24
|
+
# Specifies a block of lines to be executed once before all examples.
|
25
|
+
attr_reader :header
|
26
|
+
|
27
|
+
# Specifies a block of lines to be executed before each example.
|
28
|
+
attr_reader :before
|
29
|
+
|
30
|
+
# Specifies the template of the code to be declined across contexts.
|
31
|
+
attr_reader :subject
|
32
|
+
|
33
|
+
# Specifies a block of lines to be executed after each example.
|
34
|
+
attr_reader :after
|
35
|
+
|
36
|
+
# Specifies a block of lines to be executed once after all examples.
|
37
|
+
attr_reader :footer
|
38
|
+
|
39
|
+
# Initialize a new scaffold generator.
|
40
|
+
def initialize(header, before, subject, after, footer, *actuals, **contexts)
|
41
|
+
header = "# Brutal test suite" if header.empty?
|
42
|
+
before = "# Starting an example" if before.empty?
|
43
|
+
after = "# Finishing an example" if after.empty?
|
44
|
+
footer = "# End of the brutal test" if footer.empty?
|
45
|
+
|
46
|
+
warn("Empty subject!") if subject.empty?
|
47
|
+
warn("Empty actual values!") if actuals.empty?
|
48
|
+
warn("Empty contexts!") if contexts.empty?
|
49
|
+
|
50
|
+
@header = header
|
51
|
+
@before = before
|
52
|
+
@subject = subject
|
53
|
+
@actuals = actuals
|
54
|
+
@contexts = contexts
|
55
|
+
@after = after
|
56
|
+
@footer = footer
|
57
|
+
end
|
58
|
+
|
59
|
+
# Return a Ruby string that can be evaluated.
|
60
|
+
def inspect(object)
|
61
|
+
return object.to_s unless object.is_a?(::String)
|
62
|
+
|
63
|
+
object.strip
|
64
|
+
end
|
65
|
+
|
66
|
+
# Return a string representation.
|
67
|
+
#
|
68
|
+
# @return [String]
|
69
|
+
def to_s
|
70
|
+
eval(header) # rubocop:disable Security/Eval
|
71
|
+
|
72
|
+
ruby_lines.compact.join(separator_code)
|
73
|
+
ensure
|
74
|
+
eval(footer) # rubocop:disable Security/Eval
|
75
|
+
end
|
76
|
+
|
77
|
+
def attributes(*values)
|
78
|
+
context_names.each_with_index.inject({}) do |h, (name, i)|
|
79
|
+
h.merge(name.to_sym => inspect(values.fetch(i)))
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def context_names
|
84
|
+
contexts.keys.sort
|
85
|
+
end
|
86
|
+
|
87
|
+
def contexts_values
|
88
|
+
context_names.map { |context_name| contexts.fetch(context_name) }
|
89
|
+
end
|
90
|
+
|
91
|
+
def combinations_values
|
92
|
+
Array(contexts_values[0]).product(*Array(contexts_values[1..]))
|
93
|
+
end
|
94
|
+
|
95
|
+
def ruby_lines
|
96
|
+
[header_code] + actual_codes + [footer_code]
|
97
|
+
end
|
98
|
+
|
99
|
+
def actual_codes
|
100
|
+
combinations_values.map do |values|
|
101
|
+
actual_str = format(inspect(subject), **attributes(*values))
|
102
|
+
string = actual_code(actual_str)
|
103
|
+
actual = eval(actual_str) # rubocop:disable Security/Eval, Lint/UselessAssignment
|
104
|
+
|
105
|
+
actuals.each do |actual_value|
|
106
|
+
result_str = format(actual_value, subject: "actual")
|
107
|
+
string += "raise if #{result_str} != #{eval(result_str).inspect}\n" # rubocop:disable Security/Eval
|
108
|
+
end
|
109
|
+
|
110
|
+
string
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def actual_code(actual_str)
|
115
|
+
<<~CODE
|
116
|
+
#{before_code}
|
117
|
+
actual = begin
|
118
|
+
#{actual_str.gsub(/^/, INDENTATION)}
|
119
|
+
end
|
120
|
+
|
121
|
+
#{after_code}
|
122
|
+
CODE
|
123
|
+
end
|
124
|
+
|
125
|
+
def header_code
|
126
|
+
<<~CODE
|
127
|
+
#{header.chomp}
|
128
|
+
CODE
|
129
|
+
end
|
130
|
+
|
131
|
+
def before_code
|
132
|
+
<<~CODE
|
133
|
+
#{before.chomp}
|
134
|
+
CODE
|
135
|
+
end
|
136
|
+
|
137
|
+
def after_code
|
138
|
+
<<~CODE
|
139
|
+
#{after.chomp}
|
140
|
+
CODE
|
141
|
+
end
|
142
|
+
|
143
|
+
def footer_code
|
144
|
+
<<~CODE
|
145
|
+
#{footer.chomp}
|
146
|
+
CODE
|
147
|
+
end
|
148
|
+
|
149
|
+
def separator_code
|
150
|
+
<<~CODE
|
151
|
+
|
152
|
+
#{thematic_break_code}
|
153
|
+
CODE
|
154
|
+
end
|
155
|
+
|
156
|
+
def thematic_break_code
|
157
|
+
<<~CODE
|
158
|
+
# #{HYPHEN_MINUS * 78}
|
159
|
+
CODE
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Brutal
|
4
|
+
class Manifest
|
5
|
+
class File
|
6
|
+
module Name
|
7
|
+
# Suffix for configuration file names.
|
8
|
+
SUFFIX = "_brutal.yaml"
|
9
|
+
|
10
|
+
# Suffix pattern for configuration file names.
|
11
|
+
SUFFIX_PATTERN = "*#{SUFFIX}"
|
12
|
+
|
13
|
+
# Suffix regex for configuration file names.
|
14
|
+
SUFFIX_REGEX = /#{SUFFIX}\z/.freeze
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative File.join("file", "name")
|
4
|
+
|
5
|
+
require "yaml"
|
6
|
+
|
7
|
+
class Brutal
|
8
|
+
class Manifest
|
9
|
+
# YAML manifest file parser.
|
10
|
+
class File
|
11
|
+
attr_reader :yaml
|
12
|
+
|
13
|
+
def initialize(pathname)
|
14
|
+
raise ::ArgumentError unless pathname.fnmatch?(Name::SUFFIX_PATTERN)
|
15
|
+
|
16
|
+
@yaml = pathname.read
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse
|
20
|
+
::YAML.safe_load(yaml, symbolize_names: false)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative File.join("manifest", "file")
|
4
|
+
|
5
|
+
class Brutal
|
6
|
+
# Brutal YAML manifest file parser.
|
7
|
+
class Manifest
|
8
|
+
# The _actuals_ top-level section key.
|
9
|
+
ACTUALS_KEY = "actuals"
|
10
|
+
|
11
|
+
# The _contexts_ top-level section key.
|
12
|
+
CONTEXTS_KEY = "contexts"
|
13
|
+
|
14
|
+
# The _header_ top-level section key.
|
15
|
+
HEADER_KEY = "header"
|
16
|
+
|
17
|
+
# The _before_ top-level section key.
|
18
|
+
BEFORE_KEY = "before"
|
19
|
+
|
20
|
+
# The _subject_ top-level section key.
|
21
|
+
SUBJECT_KEY = "subject"
|
22
|
+
|
23
|
+
# The _after_ top-level section key.
|
24
|
+
AFTER_KEY = "after"
|
25
|
+
|
26
|
+
# The _footer_ top-level section key.
|
27
|
+
FOOTER_KEY = "footer"
|
28
|
+
|
29
|
+
# Default _actuals_ collection.
|
30
|
+
DEFAULT_ACTUALS = [].freeze
|
31
|
+
|
32
|
+
# Default _contexts_ collection.
|
33
|
+
DEFAULT_CONTEXTS = {}.freeze
|
34
|
+
|
35
|
+
# Default _header_ code to evaluate.
|
36
|
+
DEFAULT_HEADER = ""
|
37
|
+
|
38
|
+
# Default _before_ code to evaluate.
|
39
|
+
DEFAULT_BEFORE = ""
|
40
|
+
|
41
|
+
# Default _after_ code to evaluate.
|
42
|
+
DEFAULT_AFTER = ""
|
43
|
+
|
44
|
+
# Default _footer_ code to evaluate.
|
45
|
+
DEFAULT_FOOTER = ""
|
46
|
+
|
47
|
+
# Parse a file at `pathname`. Returns the YAML manifest instance.
|
48
|
+
def self.parse_file(pathname)
|
49
|
+
load(File.new(pathname).parse)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Load the configuration parameters.
|
53
|
+
#
|
54
|
+
# @param params [Hash] Receive the 7 top-level section parameters.
|
55
|
+
def self.load(params)
|
56
|
+
new(
|
57
|
+
actuals: params.fetch(ACTUALS_KEY, DEFAULT_ACTUALS),
|
58
|
+
contexts: params.fetch(CONTEXTS_KEY, DEFAULT_CONTEXTS),
|
59
|
+
header: params.fetch(HEADER_KEY, DEFAULT_HEADER),
|
60
|
+
before: params.fetch(BEFORE_KEY, DEFAULT_BEFORE),
|
61
|
+
subject: params.fetch(SUBJECT_KEY),
|
62
|
+
after: params.fetch(AFTER_KEY, DEFAULT_AFTER),
|
63
|
+
footer: params.fetch(FOOTER_KEY, DEFAULT_FOOTER)
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Specifies templates to challenge evaluated subjects & get results.
|
68
|
+
attr_reader :actuals
|
69
|
+
|
70
|
+
# Specifies a list of variables to populate the subject's template.
|
71
|
+
attr_reader :contexts
|
72
|
+
|
73
|
+
# Specifies a block of lines to be executed once before all examples.
|
74
|
+
attr_reader :header
|
75
|
+
|
76
|
+
# Specifies a block of lines to be executed before each example.
|
77
|
+
attr_reader :before
|
78
|
+
|
79
|
+
# Specifies the template of the code to be declined across contexts.
|
80
|
+
attr_reader :subject
|
81
|
+
|
82
|
+
# Specifies a block of lines to be executed after each example.
|
83
|
+
attr_reader :after
|
84
|
+
|
85
|
+
# Specifies a block of lines to be executed once after all examples.
|
86
|
+
attr_reader :footer
|
87
|
+
|
88
|
+
# Initialize a new configuration.
|
89
|
+
def initialize(actuals:, contexts:, header:, before:, subject:, after:, footer:)
|
90
|
+
raise ::TypeError, actuals.inspect unless actuals.is_a?(::Array)
|
91
|
+
raise ::TypeError, contexts.inspect unless contexts.is_a?(::Hash)
|
92
|
+
raise ::TypeError, header.inspect unless header.is_a?(::String)
|
93
|
+
raise ::TypeError, before.inspect unless before.is_a?(::String)
|
94
|
+
raise ::TypeError, subject.inspect unless subject.is_a?(::String)
|
95
|
+
raise ::TypeError, after.inspect unless after.is_a?(::String)
|
96
|
+
raise ::TypeError, footer.inspect unless footer.is_a?(::String)
|
97
|
+
|
98
|
+
@actuals = actuals
|
99
|
+
@contexts = contexts
|
100
|
+
@header = header
|
101
|
+
@before = before
|
102
|
+
@subject = subject
|
103
|
+
@after = after
|
104
|
+
@footer = footer
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/brutal.rb
CHANGED
@@ -1,43 +1,40 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
%w[
|
4
|
-
|
5
|
-
|
6
|
-
scaffold
|
7
|
-
yaml
|
4
|
+
format
|
5
|
+
manifest
|
8
6
|
].each { |filename| require_relative(File.join("brutal", filename)) }
|
9
7
|
|
10
8
|
# The Brutal namespace.
|
11
|
-
|
12
|
-
|
13
|
-
hash = parse(pathname)
|
14
|
-
conf = Configuration.load(hash)
|
9
|
+
class Brutal
|
10
|
+
attr_reader :engine
|
15
11
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
**conf.contexts)
|
12
|
+
def initialize(format)
|
13
|
+
@engine = Format.const_get(format)
|
14
|
+
end
|
20
15
|
|
21
|
-
|
16
|
+
def call(pathname)
|
17
|
+
manifest = Manifest.parse_file(pathname)
|
18
|
+
scaffold = brutalizer(manifest)
|
19
|
+
pathname = new_pathname(pathname)
|
20
|
+
pathname.write(scaffold)
|
22
21
|
end
|
23
22
|
|
24
|
-
|
25
|
-
|
23
|
+
private
|
24
|
+
|
25
|
+
def brutalizer(conf)
|
26
|
+
engine.new(conf.header, conf.before, conf.subject, conf.after, conf.footer, *conf.actuals, **conf.contexts)
|
27
|
+
end
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
+
def new_pathname(pathname)
|
30
|
+
pathname.dirname + "#{new_prefix}#{pathname.basename.sub(Manifest::File::Name::SUFFIX_REGEX, new_suffix)}"
|
29
31
|
end
|
30
|
-
private_class_method :parse
|
31
32
|
|
32
|
-
def
|
33
|
-
|
33
|
+
def new_prefix
|
34
|
+
engine.const_get(:Filename).const_get(:PREFIX)
|
34
35
|
end
|
35
|
-
private_class_method :read
|
36
36
|
|
37
|
-
def
|
38
|
-
|
39
|
-
File.override_protection(new_pathname) unless force
|
40
|
-
File::Write.new(new_pathname).call(ruby)
|
37
|
+
def new_suffix
|
38
|
+
engine.const_get(:Filename).const_get(:SUFFIX)
|
41
39
|
end
|
42
|
-
private_class_method :write
|
43
40
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: brutal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.6.0.beta2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-09-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -108,20 +108,6 @@ dependencies:
|
|
108
108
|
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
|
-
- !ruby/object:Gem::Dependency
|
112
|
-
name: simplecov
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
114
|
-
requirements:
|
115
|
-
- - ">="
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: '0'
|
118
|
-
type: :development
|
119
|
-
prerelease: false
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
requirements:
|
122
|
-
- - ">="
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version: '0'
|
125
111
|
- !ruby/object:Gem::Dependency
|
126
112
|
name: yard
|
127
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -147,12 +133,13 @@ files:
|
|
147
133
|
- README.md
|
148
134
|
- bin/brutal
|
149
135
|
- lib/brutal.rb
|
150
|
-
- lib/brutal/
|
151
|
-
- lib/brutal/
|
152
|
-
- lib/brutal/
|
153
|
-
- lib/brutal/
|
154
|
-
- lib/brutal/
|
155
|
-
- lib/brutal/
|
136
|
+
- lib/brutal/command_line_arguments_parser.rb
|
137
|
+
- lib/brutal/format.rb
|
138
|
+
- lib/brutal/format/ruby.rb
|
139
|
+
- lib/brutal/format/ruby/filename.rb
|
140
|
+
- lib/brutal/manifest.rb
|
141
|
+
- lib/brutal/manifest/file.rb
|
142
|
+
- lib/brutal/manifest/file/name.rb
|
156
143
|
homepage: https://github.com/fixrb/brutal
|
157
144
|
licenses:
|
158
145
|
- MIT
|
@@ -169,9 +156,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
169
156
|
version: 2.7.0
|
170
157
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
171
158
|
requirements:
|
172
|
-
- - "
|
159
|
+
- - ">"
|
173
160
|
- !ruby/object:Gem::Version
|
174
|
-
version:
|
161
|
+
version: 1.3.1
|
175
162
|
requirements: []
|
176
163
|
rubygems_version: 3.1.6
|
177
164
|
signing_key:
|
data/lib/brutal/configuration.rb
DELETED
@@ -1,55 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Brutal
|
4
|
-
# Brutal::Configuration
|
5
|
-
#
|
6
|
-
# @since 1.0.0
|
7
|
-
class Configuration
|
8
|
-
ACTUALS_KEY = "actuals"
|
9
|
-
CONTEXTS_KEY = "contexts"
|
10
|
-
HEADER_KEY = "header"
|
11
|
-
SUBJECT_KEY = "subject"
|
12
|
-
|
13
|
-
DEFAULT_ACTUALS = [].freeze
|
14
|
-
DEFAULT_CONTEXTS = {}.freeze
|
15
|
-
DEFAULT_HEADER = "# Brutal test suite"
|
16
|
-
DEFAULT_SUBJECT = ""
|
17
|
-
|
18
|
-
# Load the configuration parameters.
|
19
|
-
#
|
20
|
-
# @param params [Hash] Receive the 4 top-level section parameters.
|
21
|
-
def self.load(params)
|
22
|
-
new(
|
23
|
-
actuals: params.fetch(ACTUALS_KEY, DEFAULT_ACTUALS),
|
24
|
-
contexts: params.fetch(CONTEXTS_KEY, DEFAULT_CONTEXTS),
|
25
|
-
header: params.fetch(HEADER_KEY, DEFAULT_HEADER),
|
26
|
-
subject: params.fetch(SUBJECT_KEY, DEFAULT_SUBJECT)
|
27
|
-
)
|
28
|
-
end
|
29
|
-
|
30
|
-
# Specifies templates to challenge evaluated subjects & get results.
|
31
|
-
attr_reader :actuals
|
32
|
-
|
33
|
-
# Specifies a list of variables to populate the subject's template.
|
34
|
-
attr_reader :contexts
|
35
|
-
|
36
|
-
# Specifies the code to execute before generating the test suite.
|
37
|
-
attr_reader :header
|
38
|
-
|
39
|
-
# Specifies the template of the code to be declined across contexts.
|
40
|
-
attr_reader :subject
|
41
|
-
|
42
|
-
# Initialize a new configuration.
|
43
|
-
def initialize(actuals:, contexts:, header:, subject:)
|
44
|
-
raise ::TypeError, actuals.inspect unless actuals.is_a?(::Array)
|
45
|
-
raise ::TypeError, contexts.inspect unless contexts.is_a?(::Hash)
|
46
|
-
raise ::TypeError, header.inspect unless header.is_a?(::String)
|
47
|
-
raise ::TypeError, subject.inspect unless subject.is_a?(::String)
|
48
|
-
|
49
|
-
@actuals = actuals.sort
|
50
|
-
@contexts = contexts
|
51
|
-
@header = header
|
52
|
-
@subject = subject
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
data/lib/brutal/file/read.rb
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Brutal
|
4
|
-
module File
|
5
|
-
# Brutal::File::Read
|
6
|
-
#
|
7
|
-
# @since 1.1.0
|
8
|
-
class Read
|
9
|
-
attr_reader :name
|
10
|
-
|
11
|
-
def initialize(name)
|
12
|
-
@name = name
|
13
|
-
end
|
14
|
-
|
15
|
-
def call
|
16
|
-
::File.read(path)
|
17
|
-
rescue ::Errno::ENOENT => _e
|
18
|
-
abort "File #{path} not found!"
|
19
|
-
end
|
20
|
-
|
21
|
-
protected
|
22
|
-
|
23
|
-
def path
|
24
|
-
::File.join(::Dir.pwd, name)
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
data/lib/brutal/file/write.rb
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Brutal
|
4
|
-
module File
|
5
|
-
# Brutal::File::Write
|
6
|
-
#
|
7
|
-
# @since 1.1.0
|
8
|
-
class Write
|
9
|
-
attr_reader :name
|
10
|
-
|
11
|
-
def initialize(name)
|
12
|
-
@name = name
|
13
|
-
end
|
14
|
-
|
15
|
-
def call(scaffold)
|
16
|
-
file = ::File.open(path, "w")
|
17
|
-
file.write(scaffold)
|
18
|
-
|
19
|
-
true
|
20
|
-
ensure
|
21
|
-
file.close
|
22
|
-
end
|
23
|
-
|
24
|
-
protected
|
25
|
-
|
26
|
-
def path
|
27
|
-
::File.join(::Dir.pwd, name)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
data/lib/brutal/file.rb
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
%w[
|
4
|
-
read
|
5
|
-
write
|
6
|
-
].each { |filename| require_relative(File.join("file", filename)) }
|
7
|
-
|
8
|
-
module Brutal
|
9
|
-
# Brutal::File
|
10
|
-
module File
|
11
|
-
DEFAULT_CONFIG_FILENAME = ".brutal.yml"
|
12
|
-
DEFAULT_GENERATED_FILENAME = "test.rb"
|
13
|
-
RUBY_EXTENSION = ".rb"
|
14
|
-
|
15
|
-
def self.generated_pathname(pathname)
|
16
|
-
filename = pathname.basename
|
17
|
-
return pathname.dirname + DEFAULT_GENERATED_FILENAME if default_config_filename?(filename)
|
18
|
-
|
19
|
-
pathname.sub_ext(RUBY_EXTENSION)
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.override_protection(pathname)
|
23
|
-
abort "A #{pathname} file already exists!" if pathname.exist?
|
24
|
-
end
|
25
|
-
|
26
|
-
def self.default_config_filename?(filename)
|
27
|
-
filename.to_s == DEFAULT_CONFIG_FILENAME
|
28
|
-
end
|
29
|
-
private_class_method :default_config_filename?
|
30
|
-
end
|
31
|
-
end
|
data/lib/brutal/scaffold.rb
DELETED
@@ -1,113 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Brutal
|
4
|
-
# Brutal::Scaffold
|
5
|
-
#
|
6
|
-
# @since 1.0.0
|
7
|
-
class Scaffold
|
8
|
-
# Specifies templates to challenge evaluated subjects & get results.
|
9
|
-
attr_reader :actuals
|
10
|
-
|
11
|
-
# Specifies a list of variables to populate the subject's template.
|
12
|
-
attr_reader :contexts
|
13
|
-
|
14
|
-
# Specifies the code to execute before generating the test suite.
|
15
|
-
attr_reader :header
|
16
|
-
|
17
|
-
# Specifies the template of the code to be declined across contexts.
|
18
|
-
attr_reader :subject
|
19
|
-
|
20
|
-
# Initialize a new scaffold generator.
|
21
|
-
def initialize(header, subject, *actuals, **contexts)
|
22
|
-
warn("Empty subject!") if subject.empty?
|
23
|
-
warn("Empty actual values!") if actuals.empty?
|
24
|
-
warn("Empty contexts!") if contexts.empty?
|
25
|
-
|
26
|
-
eval(header) # rubocop:disable Security/Eval
|
27
|
-
|
28
|
-
@header = header
|
29
|
-
@subject = subject
|
30
|
-
@actuals = actuals
|
31
|
-
@contexts = contexts
|
32
|
-
end
|
33
|
-
|
34
|
-
# Return a Ruby string that can be evaluated.
|
35
|
-
def inspect(object)
|
36
|
-
return object.to_s unless object.is_a?(::String)
|
37
|
-
|
38
|
-
object.strip
|
39
|
-
end
|
40
|
-
|
41
|
-
# Return a string representation.
|
42
|
-
#
|
43
|
-
# @return [String]
|
44
|
-
def to_s
|
45
|
-
ruby_lines.join(separator_ruby_code)
|
46
|
-
end
|
47
|
-
|
48
|
-
def attributes(*values)
|
49
|
-
context_names.each_with_index.inject({}) do |h, (name, i)|
|
50
|
-
h.merge(name.to_sym => inspect(values.fetch(i)))
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def context_names
|
55
|
-
contexts.keys.sort
|
56
|
-
end
|
57
|
-
|
58
|
-
def contexts_values
|
59
|
-
context_names.map { |context_name| contexts.fetch(context_name) }
|
60
|
-
end
|
61
|
-
|
62
|
-
def combinations_values
|
63
|
-
Array(contexts_values[0]).product(*Array(contexts_values[1..]))
|
64
|
-
end
|
65
|
-
|
66
|
-
def ruby_lines
|
67
|
-
[header_ruby_code] + actual_ruby_codes
|
68
|
-
end
|
69
|
-
|
70
|
-
def actual_ruby_codes
|
71
|
-
combinations_values.map do |values|
|
72
|
-
actual_str = format(inspect(subject), **attributes(*values))
|
73
|
-
string = actual_ruby_code(actual_str)
|
74
|
-
actual = eval(actual_str) # rubocop:disable Security/Eval, Lint/UselessAssignment
|
75
|
-
|
76
|
-
actuals.each do |actual_value|
|
77
|
-
result_str = format(actual_value, subject: "actual")
|
78
|
-
string += "raise if #{result_str} != #{eval(result_str).inspect}\n" # rubocop:disable Security/Eval
|
79
|
-
end
|
80
|
-
|
81
|
-
string
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def actual_ruby_code(actual_str)
|
86
|
-
<<~RUBY_CODE
|
87
|
-
actual = begin
|
88
|
-
#{actual_str.gsub(/^/, ' ')}
|
89
|
-
end
|
90
|
-
|
91
|
-
RUBY_CODE
|
92
|
-
end
|
93
|
-
|
94
|
-
def header_ruby_code
|
95
|
-
<<~RUBY_CODE
|
96
|
-
#{header.chomp}
|
97
|
-
RUBY_CODE
|
98
|
-
end
|
99
|
-
|
100
|
-
def separator_ruby_code
|
101
|
-
<<~RUBY_CODE
|
102
|
-
|
103
|
-
#{thematic_break_ruby_code}
|
104
|
-
RUBY_CODE
|
105
|
-
end
|
106
|
-
|
107
|
-
def thematic_break_ruby_code
|
108
|
-
<<~RUBY_CODE
|
109
|
-
# #{'-' * 78}
|
110
|
-
RUBY_CODE
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
data/lib/brutal/yaml.rb
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "yaml"
|
4
|
-
|
5
|
-
module Brutal
|
6
|
-
# Brutal::Yaml
|
7
|
-
#
|
8
|
-
# @since 1.1.0
|
9
|
-
module Yaml
|
10
|
-
FILENAME_EXTENSIONS = %w[
|
11
|
-
.yaml
|
12
|
-
.yml
|
13
|
-
].freeze
|
14
|
-
|
15
|
-
def self.parse(yaml)
|
16
|
-
::YAML.safe_load(yaml, symbolize_names: false)
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.parse?(pathname)
|
20
|
-
filename_extension = pathname.extname
|
21
|
-
FILENAME_EXTENSIONS.include?(filename_extension)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|