brutal 0.2.0 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52cb0e853f0cb1644e497b1566b675da404cf9e0570ef449862b03edc8764bac
4
- data.tar.gz: 99cd7cb1d648fc3275d811eb41cb5a820bc5c82bce9c9a19b6834677c5626e6a
3
+ metadata.gz: 332c8755656f334781310460780dd233d621ddac83bc7d7dc5f0c555f4d1f955
4
+ data.tar.gz: c49ae44f98432fb3839bcf7c76ac3929bfee3eac82113bc4903e6d288863865e
5
5
  SHA512:
6
- metadata.gz: d7605a2eb8219c17786724d1129b64a0863e6113c07025d07c5e9a67c16dc6ff42234d553fbfa9e1f940b31142cae729a36749a841496d92f520cd2342e54f44
7
- data.tar.gz: f6418c02326b3d52c8436ea640575b154e0c4299940e5907f8842af9d49c68854d5cc2b33b27d212e76c373946833ee7e0f52b22cffbf0665117b82aae2866c7
6
+ metadata.gz: f015f9cbc3f478c66374b72a3c6e7340863b472eac796833700aa11a501c3361b2519939f0e407308a6800df23bb7ba959ec60d70f5b16acb86f3d30ae0440e2
7
+ data.tar.gz: 90c7d4fef875977bd991a16405675e424524d2478318d33e694d073a47348986a2bb93bc27f9ff5cdfb5d5d48120d334f55733ad0ff6a3880c401ba901c85391
data/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2019 Cyril Kato
3
+ Copyright (c) 2020 Cyril Kato
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,14 +1,38 @@
1
- # Brutal
1
+ # Brutal 💎🔨
2
2
 
3
- > Brutal test suite scaffold generator
4
-
5
- ![A lumberjack brutally cutting a tree.](https://raw.githubusercontent.com/cyril/brutal.rb/master/img/Ferdinand_Hodler_-_Woodcutter_-_Google_Art_Project.jpg)
6
-
7
- [![Build Status](https://api.travis-ci.org/cyril/brutal.rb.svg?branch=master)][travis]
3
+ [![RuboCop Status](https://github.com/fixrb/brutal/workflows/RuboCop/badge.svg)][workflow_rubocop]
4
+ [![Build Status](https://api.travis-ci.org/fixrb/brutal.svg?branch=master)][travis]
8
5
  [![Gem Version](https://badge.fury.io/rb/brutal.svg)][gem]
9
- [![Inline docs](https://inch-ci.org/github/cyril/brutal.rb.svg?branch=master)][inchpages]
6
+ [![Inline docs](https://inch-ci.org/github/fixrb/brutal.svg?branch=master)][inchpages]
10
7
  [![Documentation](http://img.shields.io/:yard-docs-38c800.svg)][rubydoc]
11
8
 
9
+ > A _code-first_ approach to automate the writing of unit tests.
10
+
11
+ ## Intro
12
+
13
+ [![I Hate Tests](https://github.com/fixrb/brutal/raw/master/img/rubyhack-2019-ruby3-what-s-missing-by-yukihiro-matsumoto.jpg)](https://www.youtube.com/embed/cmOt9HhszCI?start=1732&end=1736 "I don't like tests. It's not DRY.")
14
+
15
+ > I don't like tests. It's not DRY.<br/>
16
+ > -- [Matz](https://github.com/matz)
17
+
18
+ ## Overview
19
+
20
+ Let __Brutal__ shape for you in no time the actual behavior of your code through as many combinations of contexts as needed.
21
+
22
+ By delegating to __Brutal__ the repetitive (and redundant) task of writing tests, you'll be able to focus on your core business: the code itself.
23
+
24
+ ## Warning
25
+
26
+ __Brutal__ development process does not prevent from bugs.
27
+
28
+ As a _picture of the behavior of the code_,
29
+ a generated test suite is wrong as long as the code is wrong,
30
+ regardless of whether all true expectations.
31
+
32
+ However, this document becomes relevant when it shows that the code behaves as it is supposed to.
33
+ It is therefore important to read it well.
34
+ This is the price for _Brutal-Driven Development_.
35
+
12
36
  ## Installation
13
37
 
14
38
  Add this line to your application's Gemfile:
@@ -31,76 +55,67 @@ Just type `brutal` in a Ruby project's folder and watch the magic happen.
31
55
 
32
56
  ## Usage
33
57
 
34
- The Brutal YAML file handles 4 keys:
58
+ The `brutal.yml` file is a manifest you can use to define your __Brutal__ meta-spec.
59
+ It has 4 top-level sections:
35
60
 
36
- * `header` (optional): Some code to execute before the test suite.
37
- * `subject` (required): The front object of the test suite.
38
- * `variables` (required): A hash to decline the subject in to various contexts.
39
- * `challenges` (required): An array of methods to apply to each result.
61
+ * `header` - Specifies the code to execute before generating the test suite.
62
+ * `subject` - Specifies the template of the code to be declined across contexts.
63
+ * `contexts` - Specifies a list of variables to populate the subject's template.
64
+ * `actuals` - Specifies templates to challenge evaluated subjects & get results.
40
65
 
41
- ## Example
66
+ ### Getting started
42
67
 
43
- Given this `.brutal.yml` config file:
68
+ 1. Create a `brutal.yml` file in your application's root directory.
69
+ The following example `brutal.yml` defines the shape of a Hello test suite:
44
70
 
45
71
  ```yaml
46
72
  ---
47
- header: |
48
- # Some string concatenation unit tests
49
-
50
73
  subject: |
51
- "Hello" + "%{hello_target}%{punctuation_mark}"
74
+ "Hello " + "%{string}"
52
75
 
53
- variables:
54
- :hello_target:
55
- -
56
- - ", Bob"
76
+ contexts:
77
+ string:
78
+ - Alice
79
+ - Bob
57
80
 
58
- :punctuation_mark:
59
- - "!"
60
- - ...
61
-
62
- challenges:
63
- - "%{actual}.to_s"
64
- - "%{actual}.length"
81
+ actuals:
82
+ - "%{subject}.to_s"
83
+ - "%{subject}.length"
65
84
  ```
66
85
 
67
- The `brutal` command would generate the following file:
68
-
69
- ```ruby
70
- # Some string concatenation unit tests
86
+ 2. Run the `brutal` command from the same directory.
71
87
 
72
- # ------------------------------------------------------------------------------
88
+ 3. Read the generated `test.rb` file in the same directory:
73
89
 
74
- actual = "Hello" + "!"
75
-
76
- raise unless actual.to_s == "Hello!"
77
- raise unless actual.length == 6
90
+ ```ruby
91
+ # Brutal test suite
78
92
 
79
93
  # ------------------------------------------------------------------------------
80
94
 
81
- actual = "Hello" + "..."
95
+ actual = begin
96
+ "Hello " + "Alice"
97
+ end
82
98
 
83
- raise unless actual.to_s == "Hello..."
84
- raise unless actual.length == 8
99
+ raise if actual.to_s != "Hello Alice"
100
+ raise if actual.length != 11
85
101
 
86
102
  # ------------------------------------------------------------------------------
87
103
 
88
- actual = "Hello" + ", Bob!"
104
+ actual = begin
105
+ "Hello " + "Bob"
106
+ end
89
107
 
90
- raise unless actual.to_s == "Hello, Bob!"
91
- raise unless actual.length == 11
108
+ raise if actual.to_s != "Hello Bob"
109
+ raise if actual.length != 9
110
+ ```
92
111
 
93
- # ------------------------------------------------------------------------------
112
+ ### More examples
94
113
 
95
- actual = "Hello" + ", Bob..."
114
+ https://github.com/fixrb/brutal/raw/master/examples/
96
115
 
97
- raise unless actual.to_s == "Hello, Bob..."
98
- raise unless actual.length == 13
99
- ```
116
+ ## Rake integration example
100
117
 
101
- ## Integration with Rake
102
-
103
- The generated brutal test suite `test.rb` file can be declared as follows:
118
+ A generated `test.rb` file could be matched as follows:
104
119
 
105
120
  ```ruby
106
121
  Rake::TestTask.new do |t|
@@ -110,28 +125,27 @@ end
110
125
 
111
126
  ## Contact
112
127
 
113
- * Home page: https://github.com/cyril/brutal.rb
114
- * Bugs/issues: https://github.com/cyril/brutal.rb/issues
115
-
116
- ## Rubies
117
-
118
- * [MRI](https://www.ruby-lang.org/)
119
- * [Rubinius](https://rubinius.com/)
120
- * [JRuby](https://www.jruby.org/)
128
+ * Source code: https://github.com/fixrb/brutal
121
129
 
122
- ## Contributing
130
+ ## Versioning
123
131
 
124
- Bug reports and pull requests are welcome on GitHub at https://github.com/cyril/brutal.rb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/cyril/brutal.rb/blob/master/CODE_OF_CONDUCT.md).
132
+ __Brutal__ follows [Semantic Versioning 2.0](https://semver.org/).
125
133
 
126
134
  ## License
127
135
 
128
136
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
129
137
 
130
- ## Code of Conduct
138
+ ***
131
139
 
132
- Everyone interacting in the GreatGuardian project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/cyril/brutal.rb/blob/master/CODE_OF_CONDUCT.md).
140
+ <p>
141
+ This project is sponsored by:<br />
142
+ <a href="https://sashite.com/"><img
143
+ src="https://github.com/fixrb/brutal/raw/master/img/sashite.png"
144
+ alt="Sashite" /></a>
145
+ </p>
133
146
 
147
+ [workflow_rubocop]: https://github.com/fixrb/brutal/actions?query=workflow%3ARuboCop
134
148
  [gem]: https://rubygems.org/gems/brutal
135
- [travis]: https://travis-ci.org/cyril/brutal.rb
136
- [inchpages]: https://inch-ci.org/github/cyril/brutal.rb
149
+ [travis]: https://travis-ci.org/fixrb/brutal
150
+ [inchpages]: https://inch-ci.org/github/fixrb/brutal
137
151
  [rubydoc]: https://rubydoc.info/gems/brutal/frames
data/bin/brutal CHANGED
@@ -1,38 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'yaml'
4
+ require_relative File.join('..', 'lib', 'brutal')
5
5
 
6
- FILE_NAME = '.brutal.yml'
7
- CONF_PATH = ::File.join(::Dir.pwd, FILE_NAME)
8
-
9
- abort "File #{CONF_PATH} not found!" unless ::File.exist?(CONF_PATH)
10
-
11
- conf = ::YAML.load_file(CONF_PATH)
12
-
13
- header = conf.fetch('header', '')
14
- subject = conf.fetch('subject')
15
- challenges = conf.fetch('challenges')
16
- variables = conf.fetch('variables')
17
-
18
- raise ::TypeError unless header.is_a?(::String)
19
- raise ::TypeError unless challenges.is_a?(::Array)
20
- raise ::TypeError unless variables.is_a?(::Hash)
21
-
22
- require_relative ::File.join('..', 'lib', 'brutal')
23
-
24
- brutal_scaffold = case ARGV.fetch(0, nil)
25
- when nil
26
- ::Brutal::Framework::Por
27
- else
28
- abort "Framework #{ARGV[0].inspect} not (yet) supported!"
29
- end
30
-
31
- eval(header)
32
-
33
- brutal_specs = header + brutal_scaffold.new(subject, *challenges, **variables)
34
- .to_s
35
-
36
- file = ::File.open('test.rb', 'w')
37
- file.write(brutal_specs)
38
- file.close
6
+ Brutal.generate!
data/lib/brutal.rb CHANGED
@@ -1,11 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Main namespace
4
- #
5
- # @api public
3
+ %w[
4
+ configuration
5
+ scaffold
6
+ ].each { |file_name| require_relative(File.join('brutal', file_name)) }
7
+
8
+ # The Brutal namespace
6
9
  module Brutal
7
- end
10
+ def self.settings
11
+ Configuration.load!
12
+ end
13
+
14
+ def self.generate
15
+ Scaffold.new(*settings)
16
+ end
17
+
18
+ def self.generate!
19
+ file = ::File.open('test.rb', 'w')
20
+ file.write(generate)
8
21
 
9
- Dir[File.join File.dirname(__FILE__), 'brutal', 'framework', '*.rb'].each do |fname|
10
- require_relative fname
22
+ true
23
+ ensure
24
+ file.close
25
+ end
11
26
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Brutal
6
+ # Brutal::Configuration
7
+ #
8
+ # @since 1.0.0
9
+ class Configuration
10
+ NAME = '.brutal.yml'
11
+ PATH = ::File.join(::Dir.pwd, NAME).freeze
12
+
13
+ def self.load!
14
+ new.to_a
15
+ end
16
+
17
+ def self.file!
18
+ ::YAML.load_file(PATH)
19
+ rescue ::Errno::ENOENT => _e
20
+ abort("File #{PATH} not found!")
21
+ end
22
+
23
+ attr_reader(:header, :subject, :contexts, :actuals)
24
+
25
+ # rubocop:disable Metrics/AbcSize
26
+ def initialize
27
+ settings = self.class.file!
28
+
29
+ @header = settings.fetch('header', '# Brutal test suite')
30
+ @subject = settings.fetch('subject', '')
31
+ @contexts = settings.fetch('contexts', {})
32
+ @actuals = settings.fetch('actuals', [])
33
+
34
+ raise ::TypeError, @header.inspect unless @header.is_a?(::String)
35
+ raise ::TypeError, @subject.inspect unless @subject.is_a?(::String)
36
+ raise ::TypeError, @contexts.inspect unless @contexts.is_a?(::Hash)
37
+ raise ::TypeError, @actuals.inspect unless @actuals.is_a?(::Array)
38
+ end
39
+ # rubocop:enable Metrics/AbcSize
40
+
41
+ def to_a
42
+ [header, subject, *actuals, **contexts]
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brutal
4
+ # Brutal::Scaffold
5
+ #
6
+ # @since 1.0.0
7
+ class Scaffold
8
+ attr_reader(:header, :subject, :actuals, :contexts)
9
+
10
+ # Initialize a new scaffold generator
11
+ def initialize(header, subject, *actuals, **contexts)
12
+ warn('Empty subject!') if subject.empty?
13
+ warn('Empty actual values!') if actuals.empty?
14
+ warn('Empty contexts!') if contexts.empty?
15
+
16
+ eval(header) # rubocop:disable Security/Eval
17
+
18
+ @header = header
19
+ @subject = subject
20
+ @actuals = actuals
21
+ @contexts = contexts
22
+ end
23
+
24
+ # Return a Ruby string that can be evaluated.
25
+ def inspect(object)
26
+ return object.to_s unless object.is_a?(::String)
27
+
28
+ object.strip
29
+ end
30
+
31
+ # Return a string representation
32
+ #
33
+ # @return [String]
34
+ #
35
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
36
+ def to_s
37
+ header.chomp + "\n" + blank_line + combinations_values.map do |values|
38
+ attributes = context_names.each_with_index.inject({}) do |h, (name, i)|
39
+ h.merge(name.to_sym => inspect(values.fetch(i)))
40
+ end
41
+
42
+ actual_str = format(inspect(subject), **attributes)
43
+
44
+ string = <<~CODE
45
+ actual = begin
46
+ #{actual_str.gsub(/^/, ' ')}
47
+ end
48
+
49
+ CODE
50
+
51
+ actual = eval(actual_str) # rubocop:disable Security/Eval, Lint/UselessAssignment
52
+
53
+ actuals.each do |actual_value|
54
+ result_str = format(actual_value, subject: 'actual')
55
+ string += "raise if #{result_str} != #{eval(result_str).inspect}\n" # rubocop:disable Security/Eval
56
+ end
57
+
58
+ string
59
+ end.join(blank_line)
60
+ end
61
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
62
+
63
+ def blank_line
64
+ "\n" \
65
+ "# #{('-' * 78)}\n" \
66
+ "\n"
67
+ end
68
+
69
+ def context_names
70
+ contexts.keys.sort
71
+ end
72
+
73
+ def contexts_values
74
+ context_names.map { |context_name| contexts.fetch(context_name) }
75
+ end
76
+
77
+ def combinations_values
78
+ Array(contexts_values[0]).product(*Array(contexts_values[1..-1]))
79
+ end
80
+ end
81
+ end
metadata CHANGED
@@ -1,45 +1,45 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brutal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-12-16 00:00:00.000000000 Z
11
+ date: 2020-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '2.0'
19
+ version: '0'
20
20
  type: :development
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: '2.0'
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '13.0'
33
+ version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '13.0'
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rubocop
42
+ name: rubocop-performance
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -53,7 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: rubocop-performance
56
+ name: rubocop-thread_safety
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -70,48 +70,44 @@ dependencies:
70
70
  name: simplecov
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - "~>"
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
- version: '0.17'
75
+ version: '0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - "~>"
80
+ - - ">="
81
81
  - !ruby/object:Gem::Version
82
- version: '0.17'
82
+ version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: yard
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - "~>"
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: '0.9'
89
+ version: '0'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - "~>"
94
+ - - ">="
95
95
  - !ruby/object:Gem::Version
96
- version: '0.9'
97
- description: Brutal test suite scaffold generator
96
+ version: '0'
97
+ description: A code-first approach to automate the writing of unit tests.
98
98
  email: contact@cyril.email
99
99
  executables:
100
100
  - brutal
101
- - console
102
- - setup
103
101
  extensions: []
104
102
  extra_rdoc_files: []
105
103
  files:
106
104
  - LICENSE.md
107
105
  - README.md
108
106
  - bin/brutal
109
- - bin/console
110
- - bin/setup
111
107
  - lib/brutal.rb
112
- - lib/brutal/framework/base.rb
113
- - lib/brutal/framework/por.rb
114
- homepage: https://github.com/cyril/brutal.rb
108
+ - lib/brutal/configuration.rb
109
+ - lib/brutal/scaffold.rb
110
+ homepage: https://github.com/fixrb/brutal
115
111
  licenses:
116
112
  - MIT
117
113
  metadata: {}
@@ -130,8 +126,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
126
  - !ruby/object:Gem::Version
131
127
  version: '0'
132
128
  requirements: []
133
- rubygems_version: 3.0.6
129
+ rubygems_version: 3.1.2
134
130
  signing_key:
135
131
  specification_version: 4
136
- summary: Brutal test suite scaffold generator
132
+ summary: A code-first approach to automate the writing of unit tests.
137
133
  test_files: []
data/bin/console DELETED
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require_relative ::File.join('..', 'lib', 'brutal')
5
-
6
- require 'irb'
7
- IRB.start(__FILE__)
data/bin/setup DELETED
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env bash
2
-
3
- set -euo pipefail
4
- IFS=$'\n\t'
5
- set -vx
6
-
7
- bundle install
@@ -1,22 +0,0 @@
1
- module Brutal
2
- module Framework
3
- class Base
4
- # @api private
5
- attr_reader :subject, :challenges, :variables
6
-
7
- # Initialize a new framework
8
- def initialize(subject, *challenges, **variables)
9
- @subject = subject
10
- @challenges = challenges
11
- @variables = variables
12
- end
13
-
14
- # Return a string representation
15
- #
16
- # @api private
17
- def to_s
18
- raise ::NotImplementedError
19
- end
20
- end
21
- end
22
- end
@@ -1,41 +0,0 @@
1
- require_relative 'base'
2
-
3
- module Brutal
4
- module Framework
5
- # Plain Old Ruby
6
- class Por < Base
7
- # Return a string representation
8
- #
9
- # @return [String]
10
- #
11
- # @api public
12
- def to_s
13
- names = variables.keys.sort
14
- values_arr = names.map { |name| variables.fetch(name) }
15
- test_params = Array(values_arr[0]).product(*Array(values_arr[1..-1]))
16
-
17
- test_params.inject('') do |string, values|
18
- attributes = names.each_with_index.inject({}) do |h, (name, i)|
19
- h.merge(name.to_sym => values.fetch(i))
20
- end
21
-
22
- actual_str = subject % attributes
23
-
24
- string += "\n" \
25
- "# #{('-' * 78)}\n" \
26
- "\n" \
27
- "actual = #{actual_str}\n"
28
-
29
- actual = eval(actual_str)
30
-
31
- challenges.each do |challenge|
32
- result = challenge % { actual: 'actual' }
33
- string += "raise unless #{result} == #{eval(result).inspect}\n"
34
- end
35
-
36
- string
37
- end
38
- end
39
- end
40
- end
41
- end