thoreau 0.2.0 → 0.2.1

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: 81cb2b1a3e4a3268f26185fd214842eb673f2cda88f7afefeb0153b37d6208a7
4
- data.tar.gz: '0815f11d7475d491ae581507f90b474b9bc2765d80ab62d9b2d00440511c7e68'
3
+ metadata.gz: a8c65711c30e69f7c09e814361f34d2c6b55501223f5b422eed26f23363dd7fb
4
+ data.tar.gz: dbe902b4cb31334a3afe3a42b3fdde8078eb34ec26c1029fd25ec8825d2c6add
5
5
  SHA512:
6
- metadata.gz: a364fe16fa6e6cfabeb0a69c60d7619a587e5b4ee2a3b2eb8ce4065eabb3116282d30b9a9683cc205e9b3dd2ae1f2c49ca183cbb1070c173519c9722bdd27c9d
7
- data.tar.gz: 61d9e6aff1cdea440a2c34d2611db58c809b13e3c0dd6f54f4abd8bbbadfaa0b8023fbd21c49f09dc72e7b8cb0fccd2023dd910239cd784ab110964ede4e565d
6
+ metadata.gz: 6ddc80c61cc57201d339e9315df32add749b0219c363b92ce3cfbe8432c8a0076c2a584d8f9feb002202582431b33349c3069200ab44ee36ff7c172bd15df324
7
+ data.tar.gz: 5e28db2a7a84fc7f9024dca9160115dd9973625f357f4ec499625b2b116e67335590d816b678aed099d20e55e9c4b7928e53d5c828340d886f8f30a6b9544e58
@@ -0,0 +1,110 @@
1
+ module Thoreau
2
+ class Case
3
+ # Build test cases.
4
+ #
5
+ # It is responsible for:
6
+ # - building an list of Test::Case objects based
7
+ # on the groups provided.
8
+ # - expanding input specs in the groups into multiple cases
9
+ # - skipping unfocused tests, if any are focused
10
+ # - returning a count of those skipped
11
+ class Builder
12
+
13
+ def initialize(groups, suite_context)
14
+ @groups = groups
15
+ @suite_context = suite_context
16
+ end
17
+
18
+ def logger
19
+ @suite_context.logger
20
+ end
21
+
22
+ def any_focused?
23
+ @groups.count(&:focused?) > 0
24
+ end
25
+
26
+ def skipped_count
27
+ return 0 unless any_focused?
28
+ @groups.count - @groups.count(&:focused?)
29
+ end
30
+
31
+ def build_test_cases!
32
+ logger.debug "build_test_cases!"
33
+
34
+ @groups
35
+ .select { |g| any_focused? && g.focused? || !any_focused? }
36
+ .flat_map do |g|
37
+ build_group_cases g
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def setup_key_to_inputs key
44
+ setup = @suite_context.setups[key.to_s]
45
+ raise "Unrecognized setup context '#{key}'. Available: #{@suite_setups.keys.to_sentence}" if setup.nil?
46
+
47
+ return setup.values if setup.block.nil?
48
+
49
+ result = Class.new.new.instance_eval(&setup.block)
50
+ logger.error "Setup #{key} did not return a hash object" unless result.is_a?(Hash)
51
+ result
52
+ end
53
+
54
+ def build_group_cases g
55
+ # We have "specs" for the inputs. These may be actual
56
+ # values, or they may be enumerables that need to execute.
57
+ # So we need to "explode" (or enumerate) the values,
58
+ # generating a single test for each combination.
59
+ #
60
+ setup_values = g.setups
61
+ .map { |key| setup_key_to_inputs key }
62
+ .reduce(Hash.new) { |m, h| m.merge(h) }
63
+
64
+ input_sets = g.input_specs
65
+ .map { |is| setup_values.merge(is) }
66
+ .flat_map do |input_spec|
67
+ explode_input_specs(input_spec.keys, input_spec)
68
+ end
69
+
70
+ input_sets.map do |input_set|
71
+ Thoreau::Case.new(
72
+ group: g,
73
+ input: input_set,
74
+ action: @suite_context.data.action,
75
+ expected_output: g.expected_output,
76
+ expected_exception: g.expected_exception,
77
+ asserts: g.asserts,
78
+ suite_context: @suite_context,
79
+ logger: logger)
80
+ end
81
+
82
+ end
83
+
84
+ # Expand any values that are enumerators (Thoreau::DSL::Expanded),
85
+ # creating a list of objects, where all the combinations
86
+ # of enumerated values are present.
87
+ def explode_input_specs(keys, input_spec)
88
+ k = keys.pop
89
+
90
+ value_spec = input_spec[k]
91
+ specs = if value_spec.is_a?(Thoreau::DSL::Expanded)
92
+ value_spec.map do |v|
93
+ input_spec.merge(k => v)
94
+ end
95
+ else
96
+ [input_spec]
97
+ end
98
+
99
+ # Are we done?
100
+ return specs if keys.empty?
101
+
102
+ specs.flat_map do |spec|
103
+ explode_input_specs(keys, spec) # recurse!
104
+ end
105
+
106
+ end
107
+
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,28 @@
1
+ module Thoreau
2
+ class Case
3
+ class ContextBuilder
4
+
5
+ def initialize(group:, input:)
6
+ @group = group
7
+ @input = input
8
+ end
9
+
10
+ def create_context
11
+ temp_class = Class.new
12
+ temp_context = temp_class.new
13
+ inject_hash_into_context(@input, temp_context)
14
+ temp_context
15
+ end
16
+
17
+ private
18
+
19
+ def inject_hash_into_context(h, temp_context)
20
+ h.each do |lval, rval|
21
+ temp_context.instance_variable_set("@#{lval}", rval)
22
+ temp_context.class.attr_accessor lval
23
+ end
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,42 @@
1
+ module Thoreau
2
+ class Case
3
+ class Runner
4
+
5
+ def initialize(context)
6
+ @context = context
7
+ end
8
+
9
+ def logger
10
+ @context.logger
11
+ end
12
+
13
+ def run_test_cases! cases, skipped
14
+ logger.info " § #{@context.name} §"
15
+ cases.each do |c|
16
+ if c.ok?
17
+ logger.info " ✓ #{c.desc}"
18
+ else
19
+ logger.error "❓ #{c.desc}, #{c.problem}"
20
+ end
21
+ end
22
+ logger.info (summary cases, skipped)
23
+ logger.info ""
24
+
25
+ end
26
+
27
+ def summary cases, skipped
28
+ ok = cases.count(&:ok?)
29
+ total = cases.count
30
+ failed = cases.count(&:failed?)
31
+ if failed == 0
32
+ " ∴ All OK 👌🏾 #{skipped > 0 ? "#{skipped} skipped." : ""}"
33
+ else
34
+ " 🛑 #{failed} problem(s) detected. [#{ok} of #{total} OK#{skipped > 0 ? ", #{skipped} skipped" : ""}.]"
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,97 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+ require_relative './case/context_builder'
3
+
4
+ module Thoreau
5
+ class Case
6
+ def initialize group:,
7
+ input:,
8
+ action:,
9
+ expected_output:,
10
+ expected_exception:,
11
+ asserts:,
12
+ logger:,
13
+ suite_context:
14
+ @group = group
15
+ @input = input
16
+ @action = action
17
+ if expected_output.is_a?(Proc)
18
+ @expected_output_proc = expected_output
19
+ else
20
+ @expected_output = expected_output
21
+ end
22
+ @expected_exception = expected_exception
23
+ @assert_proc = asserts
24
+ @logger = logger
25
+ @suite_context = suite_context
26
+ @ran = false
27
+ end
28
+
29
+ delegate :failure_expected?, to: :@group
30
+
31
+ def desc
32
+ "#{@group.kind}: #{@group.desc} #{(@input == {} ? nil : @input.sort.to_h) || @expected_exception || "(no args)"}"
33
+ end
34
+
35
+ def problem
36
+ run unless @ran
37
+ if @expected_exception
38
+
39
+ logger.debug " -> @expected_exception:#{@expected_exception} @raised_exception:#{@raised_exception}"
40
+
41
+ if @raised_exception.to_s == @expected_exception.to_s
42
+ nil
43
+ elsif @raised_exception.nil?
44
+ "Expected exception, but none raised"
45
+ elsif @raised_exception.is_a?(NameError)
46
+ "Did you forget to define an input? Error: #{@raised_exception}"
47
+ else
48
+ "Expected '#{@expected_exception}' exception, but raised '#{@raised_exception}' (#{@raised_exception.class.name})"
49
+ end
50
+
51
+ elsif @assert_proc
52
+
53
+ logger.debug " -> @assert_result: #{@assert_result}"
54
+
55
+ @assert_result ? nil : "Assertion failed. (got #{@assert_result})"
56
+ else
57
+
58
+ logger.debug " -> @result: #{@result} @expected_output: #{@expected_output} @raised_exception:#{@raised_exception}"
59
+
60
+ if @raised_exception
61
+ "Expected output, but raised exception '#{@raised_exception}'"
62
+ elsif @expected_output != @result
63
+ "Expected '#{@expected_output}', but got '#{@result}'"
64
+ else
65
+ nil
66
+ end
67
+ end
68
+ end
69
+
70
+ def ok?
71
+ failure_expected? ^ !!problem.nil?
72
+ end
73
+
74
+ def failed?
75
+ !ok?
76
+ end
77
+
78
+ def run
79
+ logger.debug("create_context for #{desc} -> ")
80
+ context_builder = Case::ContextBuilder.new(group: @group, input: @input)
81
+ context = context_builder.create_context
82
+ begin
83
+ # Only capture exceptions around the action itself.
84
+ @result = context.instance_exec(&(@action))
85
+ rescue Exception => e
86
+ @raised_exception = e
87
+ return
88
+ ensure
89
+ @ran = true
90
+ end
91
+
92
+ @assert_result = context.instance_exec(@result, &(@assert_proc)) if @assert_proc
93
+ @expected_output = context.instance_exec(@result, &(@expected_output_proc)) if @expected_output_proc
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,18 @@
1
+ require_relative '../setup'
2
+
3
+ module Thoreau
4
+ module DSL
5
+ class Appendix
6
+ def initialize(context)
7
+ @context = context
8
+ end
9
+
10
+ def setup name, values = {}, &block
11
+ raise "duplicate setup block #{name}" unless @context.setups[name].nil?
12
+ @context.setups[name.to_s] = Thoreau::Setup.new(name, values, block)
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,55 @@
1
+ module Thoreau
2
+ module DSL
3
+ class Data
4
+ attr_accessor :action
5
+ attr_accessor :cases
6
+ attr_accessor :appendix
7
+ attr_accessor :groups
8
+
9
+ def initialize
10
+ @groups = []
11
+ end
12
+ end
13
+
14
+ class Context
15
+
16
+ attr_reader :data
17
+ attr_reader :name
18
+ attr_reader :logger
19
+ attr_reader :setups
20
+
21
+ def initialize name, logger
22
+ @name = name
23
+ @logger = logger
24
+ @data = Data.new
25
+ @setups = {}
26
+ end
27
+
28
+ def action(&block)
29
+ logger.debug "adding action"
30
+ @data.action = block
31
+ end
32
+
33
+ alias testing action
34
+ alias subject action
35
+
36
+ def cases(&block)
37
+ logger.debug "adding cases"
38
+ @data.cases = block
39
+ end
40
+
41
+ alias test_cases cases
42
+
43
+ def appendix(&block)
44
+ logger.debug "adding appendix"
45
+ @data.appendix = block
46
+ end
47
+
48
+ def context
49
+ self
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,13 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ module Thoreau
4
+ module DSL
5
+ class Expanded
6
+ def initialize a
7
+ @a = a
8
+ end
9
+
10
+ delegate :map, :each, to: :@a
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ module Thoreau
2
+ module DSL
3
+ class Groups
4
+
5
+ attr_reader :context
6
+
7
+ def initialize(context)
8
+ @context = context
9
+ end
10
+
11
+ def logger *args
12
+ @context.logger *args
13
+ end
14
+
15
+ include Thoreau::DSL::GroupsSupport
16
+
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,58 @@
1
+ require_relative '../spec_group'
2
+ require_relative './expanded'
3
+ require 'active_support/core_ext/array/conversions'
4
+
5
+ module Thoreau
6
+ module DSL
7
+
8
+ SPEC_GROUP_NAMES = %i[happy sad spec edge edges boundary corner gigo]
9
+ # gigo = garbage in / garbage out
10
+ #
11
+ GROUP_PROPS = %w[assert asserts raises output equal equals expect expects expected legacy pending fails inputs input setup setups].sort.freeze
12
+ PROPS_SPELL_CHECKER = DidYouMean::SpellChecker.new(dictionary: GROUP_PROPS)
13
+
14
+ module GroupsSupport
15
+ # Note: requires `logger` and `context`.
16
+ SPEC_GROUP_NAMES.each do |sym|
17
+ define_method sym do |*args|
18
+ desc = args.shift if args.size > 1 && args.first.is_a?(String)
19
+ raise "Too many arguments to #{sym}!" if args.size > 1
20
+
21
+ spec = args.first || {}
22
+ spec.keys
23
+ .reject { |k| GROUP_PROPS.include? k.to_s }
24
+ .each do |k|
25
+ suggestions = PROPS_SPELL_CHECKER.correct(k)
26
+ logger.error "Ignoring unrecognized property '#{k}'."
27
+ logger.info " Did you mean #{suggestions.to_sentence}?" if suggestions.size > 0
28
+ logger.info " Available properties: #{GROUP_PROPS.to_sentence}"
29
+ end
30
+
31
+ group = SpecGroup.new asserts: spec[:assert] || spec[:asserts],
32
+ desc: desc,
33
+ expected_exception: spec[:raises],
34
+ expected_output: spec[:output] || spec[:equals] || spec[:equal] || spec[:expected] || spec[:expects],
35
+ failure_expected: spec[:pending] || spec[:fails],
36
+ input_specs: [spec[:inputs] || spec[:input] || {}].flatten,
37
+ kind: sym,
38
+ legacy: spec[:legacy],
39
+ setups: [spec[:setup], spec[:setups]].flatten.compact
40
+ logger.debug "Adding group #{group}"
41
+ context.data.groups.push(group)
42
+ group
43
+ end
44
+
45
+ define_method "#{sym}!" do |*args|
46
+ group = self.send(sym, *args)
47
+ group.focus = true
48
+ group
49
+ end
50
+
51
+ def expanded(a)
52
+ Thoreau::DSL::Expanded.new(a)
53
+ end
54
+ end
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,55 @@
1
+ require 'logger'
2
+
3
+ require 'thoreau/test_suite'
4
+ require 'thoreau/case'
5
+ require 'thoreau/case/builder'
6
+ require 'thoreau/case/runner'
7
+ require 'thoreau/dsl/groups_support'
8
+ require 'thoreau/dsl/context'
9
+ require 'thoreau/dsl/groups'
10
+ require 'thoreau/dsl/appendix'
11
+
12
+ at_exit do
13
+ Thoreau::TestSuite.run_all!
14
+ end
15
+
16
+ module Thoreau
17
+ module DSL
18
+
19
+ attr_reader :logger
20
+
21
+ def test_suite name = nil, focus: false, &block
22
+ @logger = Logger.new(STDOUT, formatter: proc { |severity, datetime, progname, msg|
23
+ "#{severity}: #{msg}\n"
24
+ })
25
+ logger.level = Logger::INFO
26
+ logger.level = Logger::DEBUG if ENV['DEBUG']
27
+
28
+ @context = Thoreau::DSL::Context.new(name, @logger)
29
+
30
+ appendix_context = Thoreau::DSL::Appendix.new(@context)
31
+ groups_context = Thoreau::DSL::Groups.new(@context)
32
+
33
+ @context.instance_eval(&block)
34
+ appendix_context.instance_eval(&@context.data.appendix) unless @context.data.appendix.nil?
35
+ groups_context.instance_eval(&@context.data.cases) unless @context.data.cases.nil?
36
+
37
+ TestSuite.new(context: @context, focus: focus, logger: logger, name: name)
38
+ end
39
+
40
+ def xtest_suite name = nil, &block
41
+ end
42
+
43
+ alias suite test_suite
44
+ alias xsuite xtest_suite
45
+
46
+ def test_suite! name = nil, &block
47
+ test_suite name, focus: true, &block
48
+ end
49
+
50
+ alias suite! test_suite!
51
+
52
+ include Thoreau::DSL::GroupsSupport
53
+
54
+ end
55
+ end
@@ -0,0 +1,8 @@
1
+ require 'logger'
2
+
3
+ module Thoreau
4
+ class LegacyResults
5
+
6
+
7
+ end
8
+ end
@@ -0,0 +1,14 @@
1
+ module Thoreau
2
+ module Rspec
3
+
4
+ class Configuration
5
+
6
+ def initialize(rspec_config)
7
+ @rspec_config = rspec_config
8
+ end
9
+
10
+ class ConfigurationError < StandardError;
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+
2
+ module Thoreau
3
+ module Rspec
4
+ module ExampleHelpers
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+
2
+ require 'rspec/core'
3
+ require 'thoreau'
4
+ require 'thoreau/rspec/example_helpers'
5
+ require 'thoreau/rspec/configuration'
6
+ require 'thoreau/rspec/railtie' if defined?(Rails::Railtie)
7
+
8
+ module Thoreau
9
+ module Rspec
10
+
11
+ ::RSpec.configure do |c|
12
+ c.include ExampleHelpers
13
+ end
14
+
15
+ def self.config
16
+ @config ||= Configuration.new(RSpec.configuration)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ module Thoreau
2
+
3
+ class Setup
4
+
5
+ attr_reader :name, :values, :block
6
+
7
+ def initialize name, values, block
8
+ @name = name
9
+ @values = values
10
+ # @value = [values].flatten unless values.nil?
11
+ @block = block
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,45 @@
1
+ module Thoreau
2
+
3
+ class SpecGroup
4
+ attr_reader :asserts,
5
+ :desc,
6
+ :expected_exception,
7
+ :expected_output,
8
+ :input_specs,
9
+ :kind,
10
+ :legacy,
11
+ :setups
12
+ attr_writer :focus
13
+
14
+ def initialize(asserts:,
15
+ desc:,
16
+ expected_exception:,
17
+ expected_output:,
18
+ failure_expected:,
19
+ input_specs:,
20
+ legacy:,
21
+ kind:,
22
+ setups:
23
+ )
24
+ @asserts = asserts
25
+ @desc = desc
26
+ @expected_exception = expected_exception
27
+ @expected_output = expected_output
28
+ @failure_expected = failure_expected
29
+ @input_specs = input_specs
30
+ @kind = kind
31
+ @legacy = legacy
32
+ @setups = setups
33
+ end
34
+
35
+ def failure_expected?
36
+ @failure_expected
37
+ end
38
+
39
+ def focused?
40
+ @focus
41
+ end
42
+
43
+ end
44
+ end
45
+
@@ -0,0 +1,41 @@
1
+ module Thoreau
2
+ class TestSuite
3
+
4
+ attr_reader :name
5
+ attr_reader :logger
6
+
7
+ @@suites = []
8
+
9
+ def initialize(context:, logger:, name:, focus:)
10
+ @context = context
11
+ @logger = logger
12
+ @name = name
13
+ @focus = focus
14
+ @@suites << self
15
+ @builder = Thoreau::Case::Builder.new @context.data.groups, @context
16
+ end
17
+
18
+ def build_and_run
19
+ cases = @builder.build_test_cases!
20
+
21
+ runner = Thoreau::Case::Runner.new @context
22
+ runner.run_test_cases! cases,
23
+ @builder.skipped_count # for reporting
24
+ end
25
+
26
+ def focused?
27
+ @focus || @builder.any_focused?
28
+ end
29
+
30
+ def self.run_all!
31
+ run_all = !@@suites.any?(&:focused?)
32
+ @@suites.each do |suite|
33
+ if suite.focused? || run_all
34
+ suite.build_and_run
35
+ else
36
+ suite.logger.info("Suite '#{suite.name}' skipped (unfocused)")
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,12 @@
1
+ def combos_of(entries)
2
+ return [{}] if entries.size == 0
3
+
4
+ first_response = entries.first.map { |x| { x[0] => x[1] } }
5
+ return first_response if entries.size == 1
6
+
7
+ combos_of_rest = combos_of(entries.slice(1..(entries.size)))
8
+
9
+ first_response.flat_map do |f|
10
+ combos_of_rest.map { |r| r.merge(f) }
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module Thoreau
2
+ VERSION = "0.2.1"
3
+ end
data/lib/thoreau.rb ADDED
@@ -0,0 +1,12 @@
1
+ require_relative 'thoreau/version'
2
+ require_relative 'thoreau/dsl'
3
+
4
+ module Thoreau
5
+ class Error < StandardError;
6
+ end
7
+ # Your code goes here...
8
+ end
9
+
10
+ if defined?(RSpec)
11
+ require_relative('./thoreau/rspec')
12
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thoreau
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Peterson
@@ -106,7 +106,27 @@ email:
106
106
  executables: []
107
107
  extensions: []
108
108
  extra_rdoc_files: []
109
- files: []
109
+ files:
110
+ - lib/thoreau.rb
111
+ - lib/thoreau/case.rb
112
+ - lib/thoreau/case/builder.rb
113
+ - lib/thoreau/case/context_builder.rb
114
+ - lib/thoreau/case/runner.rb
115
+ - lib/thoreau/dsl.rb
116
+ - lib/thoreau/dsl/appendix.rb
117
+ - lib/thoreau/dsl/context.rb
118
+ - lib/thoreau/dsl/expanded.rb
119
+ - lib/thoreau/dsl/groups.rb
120
+ - lib/thoreau/dsl/groups_support.rb
121
+ - lib/thoreau/legacy_results.rb
122
+ - lib/thoreau/rspec.rb
123
+ - lib/thoreau/rspec/configuration.rb
124
+ - lib/thoreau/rspec/example_helpers.rb
125
+ - lib/thoreau/setup.rb
126
+ - lib/thoreau/spec_group.rb
127
+ - lib/thoreau/test_suite.rb
128
+ - lib/thoreau/util.rb
129
+ - lib/thoreau/version.rb
110
130
  homepage: https://github.com/ndp/thoreau
111
131
  licenses: []
112
132
  metadata: