brutal 1.5.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: ea124053ebeef681c2e046022936f9d67ca97778733a23bd2d81781e90daa9a8
4
- data.tar.gz: 677089454fece7f60aedfc4693b53c132f057bbeeaca5e651f88ec52d026ea34
3
+ metadata.gz: 37fdb51264d859c50519c1bfc0f511542eb8bcca0817049e11191977d06cb7b1
4
+ data.tar.gz: 54e26e7e3ad8cc403fdb6c1db116186c61d06b0766eb8fa5e96ed7688913872b
5
5
  SHA512:
6
- metadata.gz: b1d043d4740db1987c188755e9b339da566034a3cc2a43f99ced12fb3de26a93df0f93d669b20700ba662a935d9be662a25bffba8f8ae92aae3e01deade92d9b
7
- data.tar.gz: 5abc20c46849f69b20de5b89848f8fed831b2d92641f57cfa349be4e0a975c8630c27268d16a022911693d403af8262b4f0d1099dcf468879392e9c2d7fe7ec0
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,18 +50,31 @@ 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 to know how to write your tests.
63
- Currently, only the YAML format is supported.
64
- This file is by default named `.brutal.yml` and 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:
65
78
 
66
79
  * `header` - Specifies the code to execute before generating the test suite.
67
80
  * `subject` - Specifies the template of the code to be declined across contexts.
@@ -70,8 +83,10 @@ This file is by default named `.brutal.yml` and is composed of 4 top-level secti
70
83
 
71
84
  When the configuration file is present, the generation of a test suite can be done with the command:
72
85
 
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:
87
+
73
88
  ```sh
74
- brutal .brutal.yml
89
+ brutal user_brutal.yaml
75
90
  ```
76
91
 
77
92
  or:
@@ -86,48 +101,28 @@ or even:
86
101
  brutal
87
102
  ```
88
103
 
89
- This would create a `test.rb` file containing the test suite.
90
-
91
- Configuration files can also be named differently:
92
-
93
- ```sh
94
- brutal path/to/test_hello_world.yml
95
- ```
96
-
97
- This would create a `path/to/test_hello_world.rb` file containing the test suite.
104
+ This would create a `user_brutal.rb` file containing the test suite.
98
105
 
99
- To avoid accidentally overwriting a file, the `--no-force` option can be used:
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:
100
107
 
101
108
  ```sh
102
- brutal path/to/test_hello_world.yml --no-force
109
+ brutal spec/ # => generate tests from each configuration file matching ./spec/**/*_brutal.yaml in to ./spec/**/*_brutal.rb
103
110
  ```
104
111
 
105
- > A path/to/test_hello_world.rb file already exists!
106
-
107
- ### Getting started
112
+ ### Some examples
108
113
 
109
- 1. Create a `.brutal.yml` file in your application's root directory. For example: <https://github.com/fixrb/brutal/blob/v1.4.0/examples/hello_world_v1/.brutal.yml>
110
- 2. Run the `brutal` command from the same directory.
111
- 3. Read the generated `test.rb` file in the same directory: <https://github.com/fixrb/brutal/blob/v1.4.0/examples/hello_world_v1/test.rb>
112
-
113
- ### More examples
114
-
115
- <https://github.com/fixrb/brutal/blob/v1.4.0/examples/>
114
+ <https://github.com/fixrb/brutal/blob/v1.6.0.beta1/examples/>
116
115
 
117
116
  ## Rake integration example
118
117
 
119
- A generated `test.rb` file could be matched as follows:
118
+ Generated test suite files could be matched as follows:
120
119
 
121
120
  ```ruby
122
121
  Rake::TestTask.new do |t|
123
- t.pattern = "test.rb"
122
+ t.pattern = "**/*_brutal.rb"
124
123
  end
125
124
  ```
126
125
 
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
126
  ## Contact
132
127
 
133
128
  * Source code: https://github.com/fixrb/brutal
data/bin/brutal CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "pathname"
5
- require_relative File.join("..", "lib", "brutal")
4
+ require_relative File.join("..", "lib", "brutal", "command_line_arguments_parser")
5
+ format, pathnames = Brutal::CommandLineArgumentsParser.new(*ARGV).call
6
6
 
7
- path = ARGV.fetch(0, Brutal::File::DEFAULT_CONFIG_FILENAME)
8
- pathname = Pathname.new(path)
9
- pathname += Brutal::File::DEFAULT_CONFIG_FILENAME if pathname.directory?
10
- force_opt = ARGV.none?("--no-force")
7
+ require_relative File.join("..", "lib", "brutal")
8
+ generator = Brutal.new(format: format)
11
9
 
12
- Brutal.generate!(pathname, force: force_opt)
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
  #
@@ -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,27 +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"
13
11
  RUBY_EXTENSION = ".rb"
14
12
 
15
13
  def self.generated_pathname(pathname)
16
- filename = pathname.basename
17
- return pathname.dirname + DEFAULT_GENERATED_FILENAME if default_config_filename?(filename)
18
-
19
14
  pathname.sub_ext(RUBY_EXTENSION)
20
15
  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
16
  end
31
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,15 +2,10 @@
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)
@@ -18,7 +13,7 @@ module Brutal
18
13
 
19
14
  def self.parse?(pathname)
20
15
  filename_extension = pathname.extname
21
- FILENAME_EXTENSIONS.include?(filename_extension)
16
+ filename_extension.eql?(FILENAME_EXTENSION)
22
17
  end
23
18
  end
24
19
  end
data/lib/brutal.rb CHANGED
@@ -3,41 +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!(pathname, force: true)
11
+ class Brutal
12
+ attr_reader :format
13
+
14
+ def initialize(format: Format::DEFAULT)
15
+ @format = String(format)
16
+ end
17
+
18
+ def call(pathname)
13
19
  hash = parse(pathname)
14
20
  conf = Configuration.load(hash)
21
+ code = scaffold(conf)
22
+ write(pathname, code)
23
+ end
15
24
 
16
- ruby = Scaffold.new(conf.header,
17
- conf.subject,
18
- *conf.actuals,
19
- **conf.contexts)
25
+ def scaffold(conf)
26
+ engine = Format::SUPPORT.fetch(format) do
27
+ raise ::NotImplementedError, "#{format.inspect} format is not supported."
28
+ end
20
29
 
21
- write(pathname, ruby, force: force)
30
+ engine.new(conf.header, conf.subject, *conf.actuals, **conf.contexts)
22
31
  end
23
32
 
24
- def self.parse(pathname)
33
+ private
34
+
35
+ def parse(pathname)
25
36
  return Yaml.parse(read(pathname)) if Yaml.parse?(pathname)
26
37
 
27
38
  raise ::ArgumentError, "Unrecognized extension. " \
28
39
  "Impossible to parse #{pathname.inspect}."
29
40
  end
30
- private_class_method :parse
31
41
 
32
- def self.read(pathname)
42
+ def read(pathname)
33
43
  File::Read.new(pathname).call
34
44
  end
35
- private_class_method :read
36
45
 
37
- def self.write(pathname, ruby, force:)
46
+ def write(pathname, ruby)
38
47
  new_pathname = File.generated_pathname(pathname)
39
- File.override_protection(new_pathname) unless force
40
48
  File::Write.new(new_pathname).call(ruby)
41
49
  end
42
- private_class_method :write
43
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.5.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-23 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,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