brutal 1.3.0 → 1.6.0.beta1

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: 7f98c650c2e97438b0cedcebdb5b01da84af29f7cbc0c324a4b3416b3da68114
4
- data.tar.gz: 696e8e96639a62e046f138c10f3d5181a3265c90bb10306481bc0d2b2fd9d3d0
3
+ metadata.gz: 37fdb51264d859c50519c1bfc0f511542eb8bcca0817049e11191977d06cb7b1
4
+ data.tar.gz: 54e26e7e3ad8cc403fdb6c1db116186c61d06b0766eb8fa5e96ed7688913872b
5
5
  SHA512:
6
- metadata.gz: b6279a503eba6120d29cd8855fab48c725f46671bd8efa7d6fd73158e68d0cf6de9fed01e4f5ae83cf04417b0551ab9598d6c615d25b164fbefec1d3b7ce262f
7
- data.tar.gz: 8aebd74fd0a2c9bda6b418980dd627645f9b1c85f3e92e753ba2c03bd642d0dd63393dfd8923ef25e470269e3b6645fa3428a54cd7b2128ca53ee2b5c9e5de63
6
+ metadata.gz: d42f48f5e24be39950a9e5a1bfb521a84a37402b8cb3cca1eb680767ce63410ab44b836310ba7792af80a8d9c9f803ba30c9e08dff4f1094fde184409f81c95c
7
+ data.tar.gz: 40a6cf45ccdc12816d677fd5c8d19ed4c6305ec135c66fc2e56bd11f595291d9c16715b979ff920b1ed36cc90bddfe783db3653fb7f072cbc77ef8a0d760c769
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.beta1", require: false
42
42
  ```
43
43
 
44
44
  And then execute:
@@ -50,91 +50,79 @@ 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
56
+ ## Command line
57
57
 
58
- Just type `brutal` in a Ruby project's folder and watch the magic happen.
58
+ The `brutal` command comes with several options you can use to customize Brutal's behavior.
59
+
60
+ For a full list of options, run the `brutal` command with the `--help` flag:
61
+
62
+ ```sh
63
+ brutal --help
64
+ ```
65
+
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
+ ```
59
73
 
60
74
  ## Usage
61
75
 
62
- __Brutal__ needs a configuration file, it's a kind of manifest, or a meta-spec if you prefer.
63
- This file is composed of 4 top-level sections:
76
+ __Brutal__ needs configuration files in YAML format to know how to write tests.
77
+ Configuration file names are suffixed by `_brutal.yaml` and composed of 4 top-level sections:
64
78
 
65
79
  * `header` - Specifies the code to execute before generating the test suite.
66
80
  * `subject` - Specifies the template of the code to be declined across contexts.
67
81
  * `contexts` - Specifies a list of variables to populate the subject's template.
68
82
  * `actuals` - Specifies templates to challenge evaluated subjects & get results.
69
83
 
70
- By default, this file is called `.brutal.yml`, but it would be possible to call it differently by passing it as an argument to the brutal command.
71
-
72
- Currently, only the YAML format is supported.
73
-
74
- ### Getting started
84
+ When the configuration file is present, the generation of a test suite can be done with the command:
75
85
 
76
- 1. Create a `.brutal.yml` file in your application's root directory.
77
- The following example `.brutal.yml` defines the shape of a Hello test suite:
86
+ 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:
78
87
 
79
- ```yaml
80
- ---
81
- subject: |
82
- "Hello " + "%{string}"
83
-
84
- contexts:
85
- string:
86
- - Alice
87
- - Bob
88
-
89
- actuals:
90
- - "%{subject}.to_s"
91
- - "%{subject}.length"
88
+ ```sh
89
+ brutal user_brutal.yaml
92
90
  ```
93
91
 
94
- 2. Run the `brutal` command from the same directory.
92
+ or:
95
93
 
96
- 3. Read the generated `test.rb` file in the same directory:
97
-
98
- ```ruby
99
- # Brutal test suite
100
-
101
- # ------------------------------------------------------------------------------
94
+ ```sh
95
+ brutal .
96
+ ```
102
97
 
103
- actual = begin
104
- "Hello " + "Alice"
105
- end
98
+ or even:
106
99
 
107
- raise if actual.to_s != "Hello Alice"
108
- raise if actual.length != 11
100
+ ```sh
101
+ brutal
102
+ ```
109
103
 
110
- # ------------------------------------------------------------------------------
104
+ This would create a `user_brutal.rb` file containing the test suite.
111
105
 
112
- actual = begin
113
- "Hello " + "Bob"
114
- end
106
+ 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:
115
107
 
116
- raise if actual.to_s != "Hello Bob"
117
- raise if actual.length != 9
108
+ ```sh
109
+ brutal spec/ # => generate tests from each configuration file matching ./spec/**/*_brutal.yaml in to ./spec/**/*_brutal.rb
118
110
  ```
119
111
 
120
- ### More examples
112
+ ### Some examples
121
113
 
122
- https://github.com/fixrb/brutal/raw/main/examples/
114
+ <https://github.com/fixrb/brutal/blob/v1.6.0.beta1/examples/>
123
115
 
124
116
  ## Rake integration example
125
117
 
126
- A generated `test.rb` file could be matched as follows:
118
+ Generated test suite files could be matched as follows:
127
119
 
128
120
  ```ruby
129
121
  Rake::TestTask.new do |t|
130
- t.pattern = "test.rb"
122
+ t.pattern = "**/*_brutal.rb"
131
123
  end
132
124
  ```
133
125
 
134
- ## Test suite
135
-
136
- __Brutal__'s test set is brutally self-generated here: [./test.rb](https://github.com/fixrb/brutal/blob/main/test.rb)
137
-
138
126
  ## Contact
139
127
 
140
128
  * Source code: https://github.com/fixrb/brutal
data/bin/brutal CHANGED
@@ -1,6 +1,14 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative File.join("..", "lib", "brutal", "command_line_arguments_parser")
5
+ format, pathnames = Brutal::CommandLineArgumentsParser.new(*ARGV).call
6
+
4
7
  require_relative File.join("..", "lib", "brutal")
8
+ generator = Brutal.new(format: format)
5
9
 
6
- Brutal.generate! ARGV.fetch(0, Brutal::File::DEFAULT_CONFIG_FILENAME)
10
+ pathnames.each do |pathname|
11
+ Dir.chdir(pathname.dirname) do
12
+ generator.call(pathname.basename)
13
+ end
14
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ class Brutal
6
+ # Accept an arbitrary number of arguments passed from the command-line.
7
+ class CommandLineArgumentsParser
8
+ DEFAULT_FORMAT = "ruby"
9
+ FILE_SUFFIX = "_brutal.yaml"
10
+ FILE_PATTERN = ::File.join("**", "*#{FILE_SUFFIX}")
11
+ CURRENT_EXECUTION_CONTEXT = "."
12
+
13
+ attr_reader :pathnames
14
+
15
+ def initialize(*args)
16
+ @any_path = false
17
+ @pathnames = []
18
+ args.each { |arg| parse!(arg) }
19
+ parse!(CURRENT_EXECUTION_CONTEXT) unless any_path?
20
+ end
21
+
22
+ def call
23
+ [format, pathnames]
24
+ end
25
+
26
+ private
27
+
28
+ def any_path?
29
+ @any_path
30
+ end
31
+
32
+ def format
33
+ @format || DEFAULT_FORMAT
34
+ end
35
+
36
+ def parse!(arg)
37
+ case arg
38
+ when "--format=ruby"
39
+ format!("Ruby")
40
+ when "--help"
41
+ help!
42
+ when "--version"
43
+ version!
44
+ else
45
+ pathname = ::Pathname.new(arg)
46
+ load!(pathname)
47
+ end
48
+ end
49
+
50
+ def format!(name)
51
+ raise ::ArgumentError, "Format already filled in." unless format.nil?
52
+
53
+ @format = name
54
+ end
55
+
56
+ def help!
57
+ puts help_command_output
58
+ exit
59
+ end
60
+
61
+ def help_command_output
62
+ <<~TXT
63
+ Usage: #{$PROGRAM_NAME} [options] [files or directories]
64
+
65
+ --format=FORMAT Choose "ruby" (default).
66
+ --help Display this help.
67
+ --version Display the version.
68
+ TXT
69
+ end
70
+
71
+ def load!(pathname)
72
+ @any_path = true
73
+
74
+ if pathname.directory?
75
+ pathname.glob(FILE_PATTERN).each { |filename| load!(filename) }
76
+ elsif pathname.file?
77
+ if pathname.to_s.end_with?(FILE_SUFFIX)
78
+ @pathnames << pathname
79
+ else
80
+ warn "Skipping #{pathname} because not suffixed with #{FILE_SUFFIX}."
81
+ end
82
+ else
83
+ raise ::ArgumentError, "#{pathname} is neither a file nor a directory."
84
+ end
85
+ end
86
+
87
+ def version!
88
+ abort "Gem not found on the system. Unknown version." if not_loaded_spec?
89
+
90
+ puts loaded_spec.version
91
+ exit
92
+ end
93
+
94
+ def loaded_spec
95
+ ::Gem.loaded_specs["brutal"]
96
+ end
97
+
98
+ def not_loaded_spec?
99
+ loaded_spec.nil?
100
+ end
101
+ end
102
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Brutal
3
+ class Brutal
4
4
  # Brutal::Configuration
5
5
  #
6
6
  # @since 1.0.0
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Brutal
3
+ class Brutal
4
4
  module File
5
5
  # Brutal::File::Read
6
6
  #
@@ -15,7 +15,7 @@ module Brutal
15
15
  def call
16
16
  ::File.read(path)
17
17
  rescue ::Errno::ENOENT => _e
18
- abort("File #{path} not found!")
18
+ abort "File #{path} not found!"
19
19
  end
20
20
 
21
21
  protected
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Brutal
3
+ class Brutal
4
4
  module File
5
5
  # Brutal::File::Write
6
6
  #
data/lib/brutal/file.rb CHANGED
@@ -5,16 +5,13 @@
5
5
  write
6
6
  ].each { |filename| require_relative(File.join("file", filename)) }
7
7
 
8
- module Brutal
8
+ class Brutal
9
9
  # Brutal::File
10
10
  module File
11
- DEFAULT_CONFIG_FILENAME = ".brutal.yml"
12
- DEFAULT_GENERATED_FILENAME = "test.rb"
11
+ RUBY_EXTENSION = ".rb"
13
12
 
14
- def self.generated_filename(filename)
15
- return DEFAULT_GENERATED_FILENAME if filename == DEFAULT_CONFIG_FILENAME
16
-
17
- filename.gsub(/.[^.]+\z/, ".rb")
13
+ def self.generated_pathname(pathname)
14
+ pathname.sub_ext(RUBY_EXTENSION)
18
15
  end
19
16
  end
20
17
  end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Brutal
4
+ module Format
5
+ # Ruby format class
6
+ class Ruby
7
+ # Specifies templates to challenge evaluated subjects & get results.
8
+ attr_reader :actuals
9
+
10
+ # Specifies a list of variables to populate the subject's template.
11
+ attr_reader :contexts
12
+
13
+ # Specifies the code to execute before generating the test suite.
14
+ attr_reader :header
15
+
16
+ # Specifies the template of the code to be declined across contexts.
17
+ attr_reader :subject
18
+
19
+ # Initialize a new scaffold generator.
20
+ def initialize(header, subject, *actuals, **contexts)
21
+ warn("Empty subject!") if subject.empty?
22
+ warn("Empty actual values!") if actuals.empty?
23
+ warn("Empty contexts!") if contexts.empty?
24
+
25
+ eval(header) # rubocop:disable Security/Eval
26
+
27
+ @header = header
28
+ @subject = subject
29
+ @actuals = actuals
30
+ @contexts = contexts
31
+ end
32
+
33
+ # Return a Ruby string that can be evaluated.
34
+ def inspect(object)
35
+ return object.to_s unless object.is_a?(::String)
36
+
37
+ object.strip
38
+ end
39
+
40
+ # Return a string representation.
41
+ #
42
+ # @return [String]
43
+ def to_s
44
+ ruby_lines.join(separator_ruby_code)
45
+ end
46
+
47
+ def attributes(*values)
48
+ context_names.each_with_index.inject({}) do |h, (name, i)|
49
+ h.merge(name.to_sym => inspect(values.fetch(i)))
50
+ end
51
+ end
52
+
53
+ def context_names
54
+ contexts.keys.sort
55
+ end
56
+
57
+ def contexts_values
58
+ context_names.map { |context_name| contexts.fetch(context_name) }
59
+ end
60
+
61
+ def combinations_values
62
+ Array(contexts_values[0]).product(*Array(contexts_values[1..]))
63
+ end
64
+
65
+ def ruby_lines
66
+ [header_ruby_code] + actual_ruby_codes
67
+ end
68
+
69
+ def actual_ruby_codes
70
+ combinations_values.map do |values|
71
+ actual_str = format(inspect(subject), **attributes(*values))
72
+ string = actual_ruby_code(actual_str)
73
+ actual = eval(actual_str) # rubocop:disable Security/Eval, Lint/UselessAssignment
74
+
75
+ actuals.each do |actual_value|
76
+ result_str = format(actual_value, subject: "actual")
77
+ string += "raise if #{result_str} != #{eval(result_str).inspect}\n" # rubocop:disable Security/Eval
78
+ end
79
+
80
+ string
81
+ end
82
+ end
83
+
84
+ def actual_ruby_code(actual_str)
85
+ <<~RUBY_CODE
86
+ actual = begin
87
+ #{actual_str.gsub(/^/, ' ')}
88
+ end
89
+
90
+ RUBY_CODE
91
+ end
92
+
93
+ def header_ruby_code
94
+ <<~RUBY_CODE
95
+ #{header.chomp}
96
+ RUBY_CODE
97
+ end
98
+
99
+ def separator_ruby_code
100
+ <<~RUBY_CODE
101
+
102
+ #{thematic_break_ruby_code}
103
+ RUBY_CODE
104
+ end
105
+
106
+ def thematic_break_ruby_code
107
+ <<~RUBY_CODE
108
+ # #{'-' * 78}
109
+ RUBY_CODE
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ %w[
4
+ ruby
5
+ ].each { |filename| require_relative(File.join("format", filename)) }
6
+
7
+ class Brutal
8
+ # Brutal::Format
9
+ module Format
10
+ SUPPORT = {
11
+ "ruby" => Ruby
12
+ }.freeze
13
+
14
+ DEFAULT = SUPPORT.keys.sort.fetch(-1)
15
+ end
16
+ end
data/lib/brutal/yaml.rb CHANGED
@@ -2,23 +2,18 @@
2
2
 
3
3
  require "yaml"
4
4
 
5
- module Brutal
5
+ class Brutal
6
6
  # Brutal::Yaml
7
- #
8
- # @since 1.1.0
9
7
  module Yaml
10
- FILENAME_EXTENSIONS = %w[
11
- yaml
12
- yml
13
- ].freeze
8
+ FILENAME_EXTENSION = ".yaml"
14
9
 
15
10
  def self.parse(yaml)
16
11
  ::YAML.safe_load(yaml, symbolize_names: false)
17
12
  end
18
13
 
19
- def self.parse?(filename)
20
- filename_extension = filename.split(".")[1..][-1]
21
- FILENAME_EXTENSIONS.include?(filename_extension)
14
+ def self.parse?(pathname)
15
+ filename_extension = pathname.extname
16
+ filename_extension.eql?(FILENAME_EXTENSION)
22
17
  end
23
18
  end
24
19
  end
data/lib/brutal.rb CHANGED
@@ -3,31 +3,48 @@
3
3
  %w[
4
4
  configuration
5
5
  file
6
- scaffold
6
+ format
7
7
  yaml
8
8
  ].each { |filename| require_relative(File.join("brutal", filename)) }
9
9
 
10
10
  # The Brutal namespace.
11
- module Brutal
12
- def self.generate!(filename)
13
- file = File::Read.new(filename).call
11
+ class Brutal
12
+ attr_reader :format
14
13
 
15
- hash = if Yaml.parse?(filename)
16
- Yaml.parse(file)
17
- else
18
- raise ::ArgumentError, "Unrecognized extension. " \
19
- "Impossible to parse #{filename.inspect}."
20
- end
14
+ def initialize(format: Format::DEFAULT)
15
+ @format = String(format)
16
+ end
21
17
 
18
+ def call(pathname)
19
+ hash = parse(pathname)
22
20
  conf = Configuration.load(hash)
21
+ code = scaffold(conf)
22
+ write(pathname, code)
23
+ end
24
+
25
+ def scaffold(conf)
26
+ engine = Format::SUPPORT.fetch(format) do
27
+ raise ::NotImplementedError, "#{format.inspect} format is not supported."
28
+ end
29
+
30
+ engine.new(conf.header, conf.subject, *conf.actuals, **conf.contexts)
31
+ end
23
32
 
24
- ruby = Scaffold.new(conf.header,
25
- conf.subject,
26
- *conf.actuals,
27
- **conf.contexts)
33
+ private
28
34
 
29
- new_filename = File.generated_filename(filename)
35
+ def parse(pathname)
36
+ return Yaml.parse(read(pathname)) if Yaml.parse?(pathname)
37
+
38
+ raise ::ArgumentError, "Unrecognized extension. " \
39
+ "Impossible to parse #{pathname.inspect}."
40
+ end
41
+
42
+ def read(pathname)
43
+ File::Read.new(pathname).call
44
+ end
30
45
 
31
- File::Write.new(new_filename).call(ruby)
46
+ def write(pathname, ruby)
47
+ new_pathname = File.generated_pathname(pathname)
48
+ File::Write.new(new_pathname).call(ruby)
32
49
  end
33
50
  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.3.0
4
+ version: 1.6.0.beta1
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-08-21 00:00:00.000000000 Z
11
+ date: 2022-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -147,11 +147,13 @@ files:
147
147
  - README.md
148
148
  - bin/brutal
149
149
  - lib/brutal.rb
150
+ - lib/brutal/command_line_arguments_parser.rb
150
151
  - lib/brutal/configuration.rb
151
152
  - lib/brutal/file.rb
152
153
  - lib/brutal/file/read.rb
153
154
  - lib/brutal/file/write.rb
154
- - lib/brutal/scaffold.rb
155
+ - lib/brutal/format.rb
156
+ - lib/brutal/format/ruby.rb
155
157
  - lib/brutal/yaml.rb
156
158
  homepage: https://github.com/fixrb/brutal
157
159
  licenses:
@@ -169,9 +171,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
169
171
  version: 2.7.0
170
172
  required_rubygems_version: !ruby/object:Gem::Requirement
171
173
  requirements:
172
- - - ">="
174
+ - - ">"
173
175
  - !ruby/object:Gem::Version
174
- version: '0'
176
+ version: 1.3.1
175
177
  requirements: []
176
178
  rubygems_version: 3.1.6
177
179
  signing_key:
@@ -1,88 +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
- "#{header.chomp}\n#{blank_line}" + combinations_values.map do |values|
46
- attributes = context_names.each_with_index.inject({}) do |h, (name, i)|
47
- h.merge(name.to_sym => inspect(values.fetch(i)))
48
- end
49
-
50
- actual_str = format(inspect(subject), **attributes)
51
-
52
- string = <<~CODE
53
- actual = begin
54
- #{actual_str.gsub(/^/, ' ')}
55
- end
56
-
57
- CODE
58
-
59
- actual = eval(actual_str) # rubocop:disable Security/Eval, Lint/UselessAssignment
60
-
61
- actuals.each do |actual_value|
62
- result_str = format(actual_value, subject: "actual")
63
- string += "raise if #{result_str} != #{eval(result_str).inspect}\n" # rubocop:disable Security/Eval
64
- end
65
-
66
- string
67
- end.join(blank_line)
68
- end
69
-
70
- def blank_line
71
- "\n" \
72
- "# #{'-' * 78}\n" \
73
- "\n"
74
- end
75
-
76
- def context_names
77
- contexts.keys.sort
78
- end
79
-
80
- def contexts_values
81
- context_names.map { |context_name| contexts.fetch(context_name) }
82
- end
83
-
84
- def combinations_values
85
- Array(contexts_values[0]).product(*Array(contexts_values[1..]))
86
- end
87
- end
88
- end