brutal 1.5.0 → 1.6.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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