thoreau 0.2.0 → 0.3.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: d712bdb1cef5f616159042ee63dba2694717f2b60d0fa8b72994f5568d23ef7a
4
+ data.tar.gz: 61d14f95c9344a58cd68cb96bf0123679a9aa67d0a08d71859473bf3774fd7c1
5
5
  SHA512:
6
- metadata.gz: a364fe16fa6e6cfabeb0a69c60d7619a587e5b4ee2a3b2eb8ce4065eabb3116282d30b9a9683cc205e9b3dd2ae1f2c49ca183cbb1070c173519c9722bdd27c9d
7
- data.tar.gz: 61d9e6aff1cdea440a2c34d2611db58c809b13e3c0dd6f54f4abd8bbbadfaa0b8023fbd21c49f09dc72e7b8cb0fccd2023dd910239cd784ab110964ede4e565d
6
+ metadata.gz: dd809493f3ec81b435849c64409a595fee425dcf6952ab636d682a3ff52d1df46eeed1f46e5c0f1daf3a1c208207306b51a6c365dcefb4a7a7d24e5cdb39c8d0
7
+ data.tar.gz: 510efd7e19530338004efb17dba5fe94e22d9d51bbd12fcf02ec60e8eee4b061d44771b08ae82781b45effd37428647c62e65af803dc551a29b1578cf441ceb1
@@ -0,0 +1,6 @@
1
+ require_relative '../thoreau'
2
+ require_relative './dsl'
3
+
4
+ at_exit do
5
+ Thoreau::Model::TestSuite.run_all!
6
+ end
@@ -0,0 +1,110 @@
1
+ module Thoreau
2
+ module 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 CaseBuilder
12
+
13
+ include Logging
14
+
15
+ def initialize(test_clan:)
16
+ logger.debug("CaseBuilder.new #{test_clan.name} #{test_clan.test_families.size} families")
17
+ @test_families = test_clan.test_families
18
+ @action_block = test_clan.action_block
19
+ @appendix = test_clan.appendix
20
+ end
21
+
22
+ def any_focused?
23
+ @test_families.count(&:focused?) > 0
24
+ end
25
+
26
+ def skipped_count
27
+ return 0 unless any_focused?
28
+ @test_families.count - @test_families.count(&:focused?)
29
+ end
30
+
31
+ def build_test_cases!
32
+ logger.debug " build_test_cases! (#{@test_families.size} families)"
33
+
34
+ @test_families
35
+ .select { |fam| any_focused? && fam.focused? || !any_focused? }
36
+ .flat_map { |fam| build_family_cases fam }
37
+ end
38
+
39
+ private
40
+
41
+ def build_family_cases fam
42
+ # We have "specs" for the inputs. These may be actual
43
+ # values, or they may be enumerables that need to execute.
44
+ # So we need to "explode" (or enumerate) the values,
45
+ # generating a single test for each combination.
46
+ #
47
+ setup_values = @appendix.setup_values fam.setups
48
+
49
+ # calculate the inputs, expanded from the setup values above
50
+ input_hashes = build_input_hashes fam.input_specs, setup_values
51
+
52
+ logger.debug(" -> setup_values = #{setup_values}")
53
+ logger.debug(" -> fam.input_specs = #{fam.input_specs}")
54
+ logger.debug(" -> input_sets: #{input_hashes}")
55
+ logger.debug(" build cases for '#{fam.desc}', #{setup_values.size} setups, #{input_hashes.size} input sets, build_family_cases")
56
+
57
+ input_hashes.map do |input_hash|
58
+ expectation = fam.use_legacy_snapshot ?
59
+ :use_legacy_snapshot :
60
+ Model::Outcome.new(output: fam.expected_output,
61
+ exception: fam.expected_exception)
62
+
63
+ Thoreau::Model::TestCase.new family_desc: "#{fam.kind.to_s.ljust(10).capitalize} #{fam.desc}",
64
+ input: input_hash,
65
+ action_block: @action_block,
66
+ expectation: expectation,
67
+ asserts: fam.asserts,
68
+ negativo: fam.failure_expected?
69
+ end
70
+
71
+ end
72
+
73
+
74
+ def build_input_hashes input_specs, setup_values
75
+ sets = input_specs
76
+ .map { |is| setup_values.merge(is) }
77
+ .flat_map do |input_spec|
78
+ explode_input_specs(input_spec.keys, input_spec)
79
+ end
80
+ sets = [{}] if sets.size == 0
81
+ sets
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,27 @@
1
+ module Thoreau
2
+ module Case
3
+ class ContextBuilder
4
+
5
+ def initialize(input:)
6
+ @input = input
7
+ end
8
+
9
+ def create_context
10
+ temp_class = Class.new
11
+ temp_context = temp_class.new
12
+ inject_hash_into_context(@input, temp_context)
13
+ temp_context
14
+ end
15
+
16
+ private
17
+
18
+ def inject_hash_into_context(h, temp_context)
19
+ h.each do |lval, rval|
20
+ temp_context.instance_variable_set("@#{lval}", rval)
21
+ temp_context.class.attr_accessor lval
22
+ end
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ module Thoreau
2
+ module Case
3
+ class MultiClanCaseBuilder
4
+
5
+ include Logging
6
+
7
+ def initialize(test_clans:)
8
+ @case_builders = test_clans.map do |test_clan|
9
+ CaseBuilder.new test_clan: test_clan
10
+ end
11
+ end
12
+
13
+ def any_focused?
14
+ @case_builders.any? &:any_focused?
15
+ end
16
+
17
+ def skipped_count
18
+ return 0 unless any_focused?
19
+ @case_builders.map(&:skipped_count).reduce(&:+)
20
+ end
21
+
22
+ def build_test_cases!
23
+ @case_builders.flat_map(&:build_test_cases!)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,61 @@
1
+ require 'rainbow/refinement'
2
+ using Rainbow
3
+
4
+ module Thoreau
5
+ module Case
6
+ class SuiteRunner
7
+
8
+ include Logging
9
+
10
+ def initialize(suite_name)
11
+ @suite_name = suite_name
12
+ end
13
+
14
+ def run_test_cases! cases, skipped
15
+ legacy_data = LegacyExpectedOutcomes.new(@suite_name)
16
+ logger.info " #{@suite_name.underline.bright}"
17
+ cases.each do |c|
18
+
19
+ legacy = c.expectation == :use_legacy_snapshot
20
+ if legacy
21
+ if !legacy_data.has_saved_for?(c) ||
22
+ ENV['RESET_SNAPSHOTS']
23
+ logger.info " [#{ENV['RESET_SNAPSHOTS'] ? 'resetting' : 'saving'} legacy data]"
24
+ c.run
25
+ c.expectation = c.actual # by definition
26
+ legacy_data.save!(c)
27
+ else
28
+ c.expectation = legacy_data.load_for c
29
+ end
30
+ end
31
+
32
+ c.run
33
+
34
+ if c.ok?
35
+ logger.info " #{legacy ? '▶️ ' : '✓ ' } #{c.desc}"
36
+ else
37
+ logger.error "❓ #{c.desc.bright}"
38
+ logger.error " #{c.problem.red.bright}"
39
+ end
40
+ end
41
+ logger.info (summary cases, skipped)
42
+ logger.info ""
43
+
44
+ end
45
+
46
+ def summary cases, skipped
47
+ ok = cases.count(&:ok?)
48
+ total = cases.count
49
+ failed = cases.count(&:failed?)
50
+ if failed == 0
51
+ " ∴ No problems detected 👌🏾 #{skipped > 0 ? "#{skipped} skipped." : ""}"
52
+ else
53
+ " 🛑 #{failed} problem(s) detected. [#{ok} of #{total} OK#{skipped > 0 ? ", #{skipped} skipped" : ""}.]".bright
54
+ end
55
+
56
+ end
57
+
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,9 @@
1
+ module Thoreau
2
+ class Configuration
3
+ attr_accessor :legacy_outcome_path
4
+
5
+ def initialize
6
+ @legacy_outcome_path = './spec/legacy-outcomes.pstore'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ module Thoreau
2
+ module DSL
3
+ module Context
4
+ class Appendix
5
+ def initialize(appendix_model, &appendix_block)
6
+ @model = appendix_model
7
+ self.instance_eval(&appendix_block)
8
+ end
9
+
10
+ def setup name, values = {}, &block
11
+ @model.add_setup(name, values, block)
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,97 @@
1
+ require_relative '../../model/test_family'
2
+ require_relative '../expanded'
3
+ require_relative '../../service/util'
4
+ require 'active_support'
5
+ require 'active_support/core_ext/array/conversions'
6
+
7
+ module Thoreau
8
+ module DSL
9
+ module Context
10
+
11
+ SPEC_FAMILY_NAMES = %i[happy sad spec test edge edges boundary corner gigo]
12
+ # gigo = garbage in / garbage out
13
+ #
14
+ PROPS = {
15
+ asserts: %i[assert asserts post post_condition],
16
+ expected_exception: %i[raises],
17
+ expected_output: %i[equals equal expected expect expects output],
18
+ failure_expected: %i[fails pending],
19
+ input_specs: %i[input inputs],
20
+ setups: %i[setup setups assemble]
21
+ }
22
+ ALL_PROPS = PROPS.values.flatten.map(&:to_s)
23
+ PROPS_SPELL_CHECKER = DidYouMean::SpellChecker.new(dictionary: ALL_PROPS)
24
+
25
+ module Clan
26
+
27
+ # Note: requires `@test_clan_model`.
28
+
29
+ def action(&block)
30
+ logger.debug " + Adding subject block"
31
+ @test_clan_model.action_block = block
32
+ end
33
+
34
+ alias act action
35
+ alias testing action
36
+ alias subject action
37
+
38
+ def self.def_family_methods_for(sym)
39
+ define_method sym do |*args|
40
+ desc = args.shift if args.size > 1 && args.first.is_a?(String)
41
+ raise "Too many arguments to #{sym}!" if args.size > 1
42
+
43
+ spec = args.first&.stringify_keys || {}
44
+ spec.keys
45
+ .reject { |k| ALL_PROPS.include? k }
46
+ .each do |k|
47
+ suggestions = PROPS_SPELL_CHECKER.correct(k)
48
+ logger.error "Ignoring unrecognized property '#{k}'."
49
+ logger.info " Did you mean #{suggestions.to_sentence}?" if suggestions.size > 0
50
+ logger.info " Available properties: #{ALL_PROPS.to_sentence}"
51
+ end
52
+
53
+ params = HashUtil.normalize_props(spec.symbolize_keys, PROPS).tap { |props|
54
+ # These two props are easier to deal with downstream as empty arrays
55
+ props[:input_specs] = [props[:input_specs]].flatten.compact
56
+ props[:setups] = [props[:setups]].flatten.compact
57
+ }.merge kind: sym,
58
+ desc: desc
59
+
60
+ family = Model::TestFamily.new **params
61
+
62
+ yield family if block_given?
63
+
64
+ logger.debug " * Created new family #{params.inspect}"
65
+ @test_clan_model.add_test_family family
66
+ end
67
+
68
+ define_method "#{sym}!" do |*args|
69
+ family = self.send(sym, *args)
70
+ family.focus = true
71
+ family
72
+ end
73
+ end
74
+
75
+ SPEC_FAMILY_NAMES.each do |sym|
76
+ def_family_methods_for sym
77
+ end
78
+
79
+ def_family_methods_for :legacy do |r|
80
+ r.use_legacy_snapshot = true
81
+ end
82
+
83
+ alias legacy_spec legacy
84
+ alias legacy_test legacy
85
+ alias legacy_code legacy
86
+ alias legacy_spec! legacy!
87
+ alias legacy_test! legacy!
88
+ alias legacy_code! legacy!
89
+
90
+ def expanded(a)
91
+ Thoreau::DSL::Expanded.new(a)
92
+ end
93
+
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,40 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext/module/delegation'
3
+
4
+ module Thoreau
5
+ module DSL
6
+ module Context
7
+
8
+ class Suite
9
+
10
+ include Thoreau::Logging
11
+
12
+ attr_reader :suite_data
13
+ attr_reader :test_clan_model
14
+
15
+ def initialize suite_data:, test_clan_model:
16
+ raise "Suites must have (unique) names." if suite_data.name.blank?
17
+ @suite_data = suite_data
18
+ @test_clan_model = test_clan_model
19
+ end
20
+
21
+ delegate :name, to: :suite_data
22
+
23
+ def cases(name = nil, &block)
24
+ name = self.suite_data.name if name.nil?
25
+ logger.debug " + adding cases named `#{name}`"
26
+ @suite_data.test_cases_blocks << [name, block]
27
+ end
28
+
29
+ alias test_cases cases
30
+
31
+ def appendix(&block)
32
+ logger.debug " adding appendix block"
33
+ @suite_data.appendix_block = block
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,21 @@
1
+ module Thoreau
2
+ module DSL
3
+ module Context
4
+ class TestCases
5
+
6
+ include Thoreau::Logging
7
+
8
+ attr_reader :test_clan_model
9
+
10
+ def initialize(clan_model, &context)
11
+ @test_clan_model = clan_model
12
+
13
+ self.instance_eval(&context)
14
+ end
15
+
16
+ include Thoreau::DSL::Context::Clan
17
+
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext/module/delegation'
3
+
4
+ module Thoreau
5
+ module DSL
6
+ class Expanded
7
+ def initialize a
8
+ @a = a
9
+ end
10
+
11
+ delegate :map, :each, to: :@a
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,29 @@
1
+ require_relative '../model/setup'
2
+
3
+ module Thoreau
4
+ module DSL
5
+ class TestSuiteData
6
+
7
+ include Thoreau::Logging
8
+
9
+ attr_accessor :appendix_block
10
+ attr_reader :test_cases_blocks
11
+
12
+ attr_reader :name
13
+ attr_reader :test_clans
14
+
15
+ def initialize name, appendix:, test_clan:
16
+ @name = name
17
+ @appendix = appendix
18
+ @test_clans = [test_clan]
19
+ @test_cases_blocks = []
20
+ end
21
+
22
+ def add_setup(name, values, block)
23
+ logger.debug " Adding setup block #{name}"
24
+ @appendix.add_setup Thoreau::Model::Setup.new(name, values, block)
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,69 @@
1
+ require 'thoreau/service/logging'
2
+ require 'thoreau/model/test_suite'
3
+ require 'thoreau/model/test_case'
4
+ require 'thoreau/model/test_clan'
5
+ require 'thoreau/case/case_builder'
6
+ require 'thoreau/case/suite_runner'
7
+ require 'thoreau/dsl/test_suite_data'
8
+ require 'thoreau/dsl/context/clan'
9
+ require 'thoreau/dsl/context/suite'
10
+ require 'thoreau/dsl/context/test_cases'
11
+ require 'thoreau/dsl/context/appendix'
12
+ require_relative './errors'
13
+
14
+ module Thoreau
15
+
16
+ module DSL
17
+
18
+ include Thoreau::Logging
19
+
20
+ attr_reader :suite_data
21
+
22
+ def test_suite name = nil, focus: false, &block
23
+ logger.debug("# Processing keyword `test_suite`")
24
+
25
+ appendix = Model::Appendix.new
26
+ top_level_clan_model = Thoreau::Model::TestClan.new name, appendix: appendix
27
+ @suite_data = Thoreau::DSL::TestSuiteData.new name, test_clan: top_level_clan_model, appendix: appendix
28
+
29
+ # Evaluate all the top-level keywords: test_cases, appendix
30
+ @suite_context = Thoreau::DSL::Context::Suite.new suite_data: @suite_data,
31
+ test_clan_model: top_level_clan_model
32
+ logger.debug("## Evaluating suite")
33
+ @suite_context.instance_eval(&block)
34
+
35
+ logger.debug("## Evaluating appendix block")
36
+ appendix_block = @suite_data.appendix_block
37
+ Thoreau::DSL::Context::Appendix.new(@suite_data, &appendix_block) unless appendix_block.nil?
38
+
39
+ logger.debug("## Evaluating test_cases blocks")
40
+ @suite_data.test_cases_blocks.each do |name, cases_block|
41
+
42
+ raise TestCasesAtMultipleLevelsError unless @suite_data.test_clans.first.empty?
43
+
44
+ test_clan_model = Thoreau::Model::TestClan.new name,
45
+ appendix: appendix,
46
+ action_block: top_level_clan_model.action_block
47
+ Thoreau::DSL::Context::TestCases.new(test_clan_model, &cases_block)
48
+ @suite_data.test_clans << test_clan_model
49
+ end
50
+
51
+ Model::TestSuite.new(data: @suite_data, focus: focus)
52
+ end
53
+
54
+ def xtest_suite name = nil, &block
55
+ end
56
+
57
+ alias suite test_suite
58
+ alias xsuite xtest_suite
59
+
60
+ def test_suite! name = nil, &block
61
+ test_suite name, focus: true, &block
62
+ end
63
+
64
+ alias suite! test_suite!
65
+
66
+ include Thoreau::DSL::Context::Clan
67
+
68
+ end
69
+ end
@@ -0,0 +1,15 @@
1
+ module Thoreau
2
+ class TestCasesAtMultipleLevelsError < RuntimeError
3
+ def initialize(msg = nil)
4
+ super "Test cases must be specified either at the top level or inside test_cases blocks, but not both!"
5
+ end
6
+ end
7
+
8
+ class OverriddenActionError < RuntimeError
9
+ def initialize(msg = nil)
10
+ super "Extra action/subject provided for tests. Actions/subjects must be specified EITHER in the `suite` or within `test_cases` (not both)."
11
+ end
12
+ end
13
+
14
+
15
+ end
@@ -0,0 +1,37 @@
1
+ module Thoreau
2
+ module Model
3
+ class Appendix
4
+
5
+ attr_reader :setups
6
+
7
+ def initialize setups: {}
8
+ @setups = setups
9
+ end
10
+
11
+ def add_setup setup
12
+ raise "Duplicate setup block #{setup.name}" unless setups[setup.name].nil?
13
+ @setups[setup.name] = setup
14
+ end
15
+
16
+ def setup_values keys
17
+ keys
18
+ .map { |key| self.setup_key_to_inputs key }
19
+ .reduce(Hash.new) { |m, h| m.merge(h) }
20
+ end
21
+
22
+ private
23
+
24
+ def setup_key_to_inputs key
25
+ setup = self.setups[key.to_s]
26
+ raise "Unrecognized setup context '#{key}'. Available: #{self.setups.keys.to_sentence}" if setup.nil?
27
+ logger.debug(" setup_key_to_inputs `#{key}`: #{setup}")
28
+ return setup.values if setup.block.nil?
29
+
30
+ result = Class.new.new.instance_eval(&setup.block)
31
+ logger.error "Setup #{key} did not return a hash object" unless result.is_a?(Hash)
32
+ result
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,33 @@
1
+ module Thoreau
2
+ module Model
3
+ class Outcome
4
+ # Reprents the outcome of a given test case.
5
+ # It can either be successful and have an `output`,
6
+ # or it can raise an exception.
7
+ #
8
+ # This is used both for recording what happens to a test
9
+ # and for representing the expectations for what will happen.
10
+ # It is also used to save as a "snapshot" for legacy tests.
11
+
12
+ include Thoreau::Logging
13
+
14
+ attr_reader :output
15
+ attr_reader :exception
16
+
17
+ def initialize output: nil,
18
+ exception: nil
19
+
20
+ if output.is_a?(Proc)
21
+ @output_proc = output
22
+ else
23
+ @output = output
24
+ end
25
+ @exception = exception
26
+ end
27
+
28
+ def evaluate(result, context)
29
+ @output = context.instance_exec(result, &(@output_proc)) if @output_proc
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ module Thoreau
2
+ module Model
3
+
4
+ class Setup
5
+
6
+ attr_reader :name, :values, :block
7
+
8
+ def initialize name, values, block
9
+ @name = name.to_s
10
+ @values = values
11
+ # @value = [values].flatten unless values.nil?
12
+ @block = block
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,131 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext/module/delegation'
3
+ require_relative '../case/context_builder'
4
+ require_relative './outcome'
5
+ require_relative '../service/legacy_expected_outcomes'
6
+
7
+ module Thoreau
8
+ module Model
9
+ class TestCase
10
+
11
+ include Thoreau::Logging
12
+
13
+ attr_reader :actual, :family_desc, :input
14
+ attr_accessor :expectation
15
+
16
+ def initialize family_desc:,
17
+ input:,
18
+ action_block:,
19
+ expectation:,
20
+ asserts:,
21
+ negativo:
22
+
23
+ @family_desc = family_desc
24
+ @input = input
25
+ @action_block = action_block
26
+ @negativo = negativo
27
+
28
+ @expectation = expectation
29
+
30
+ @assert_proc = asserts
31
+
32
+ @ran = false
33
+ end
34
+
35
+ def failure_expected?
36
+ @negativo
37
+ end
38
+
39
+ def desc
40
+ "#{@family_desc} #{(@input == {} ? nil : @input.sort.to_h) || @expectation.exception || "(no args)"}"
41
+ end
42
+
43
+ def result_analysis
44
+
45
+ run unless @ran
46
+
47
+ if @expectation.exception
48
+
49
+ logger.debug " -> Expected Exception #{@expectation.exception} @actual.exception:#{@actual.exception}"
50
+
51
+ if @expectation.exception.is_a?(Class) &&
52
+ @actual.exception.class == @expectation.exception
53
+ nil
54
+ elsif @actual.exception.to_s == @expectation.exception.to_s
55
+ nil
56
+ elsif @actual.exception.nil?
57
+ "Expected exception, but none raised"
58
+ elsif @actual.exception.is_a?(NameError)
59
+ "Did you forget to define an input? Error: #{@actual.exception}"
60
+ else
61
+ "Expected '#{@expectation.exception}' exception, but raised '#{@actual.exception}' (#{@actual.exception.class.name})"
62
+ end
63
+
64
+ elsif @assert_proc
65
+
66
+ logger.debug " -> Assert Proc result=#{@assert_result}"
67
+
68
+ if @actual.exception.nil?
69
+ @assert_result ? nil : "Assertion failed. (got '#{@assert_result}', result='#{@actual.output}')"
70
+ else
71
+ "Expected assertion, but raised exception '#{@actual.exception}'"
72
+ end
73
+
74
+ else
75
+
76
+ logger.debug " -> Result expected: result=#{@actual.output} expected_output: #{@expectation.output} @actual.exception:#{@actual.exception}"
77
+
78
+ if @actual.exception
79
+ "Expected output, but raised exception '#{@actual.exception}'"
80
+ elsif @expectation.output != @actual.output
81
+ "Expected '#{@expectation.output}', but got '#{@actual.output}'"
82
+ else
83
+ nil
84
+ end
85
+ end
86
+ end
87
+
88
+ def problem
89
+ if failure_expected?
90
+ if result_analysis.nil?
91
+ "Failure expected but didn't. Is this implemented already?"
92
+ else
93
+ nil
94
+ end
95
+ else
96
+ result_analysis
97
+ end
98
+ end
99
+
100
+ def ok?
101
+ problem.nil?
102
+ end
103
+
104
+ def failed?
105
+ !ok?
106
+ end
107
+
108
+ def run
109
+ logger.debug "## RUN #{desc}"
110
+ context_builder = Case::ContextBuilder.new(input: @input)
111
+ context = context_builder.create_context
112
+ begin
113
+ # Only capture exceptions around the subject itself.
114
+ output = context.instance_exec(&(@action_block))
115
+ @actual = Model::Outcome.new output: output
116
+ rescue Exception => e
117
+ logger.debug("** Exception: #{e.class.name} #{e}")
118
+ logger.debug("Available local variables: #{@input.keys.empty? ? '(none)' : @input.keys.to_sentence}") if e.is_a? NameError
119
+ @actual = Model::Outcome.new exception: e
120
+ return
121
+ ensure
122
+ @ran = true
123
+ end
124
+
125
+ @expectation.evaluate(@actual.output, context) unless @expectation == :use_legacy_snapshot
126
+ @assert_result = context.instance_exec(@actual.output, &(@assert_proc)) if @assert_proc
127
+ end
128
+
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,38 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext/module/delegation'
3
+ require_relative '../errors'
4
+
5
+ module Thoreau
6
+
7
+ module Model
8
+ class TestClan # set of TestFamilies
9
+
10
+ include Thoreau::Logging
11
+
12
+ attr_accessor :test_families, :appendix
13
+ attr_reader :name, :action_block
14
+
15
+ delegate :empty?, to: :test_families
16
+
17
+ def initialize(name, appendix:, action_block: nil)
18
+ @name = name
19
+ @test_families = []
20
+ @appendix = appendix
21
+ @action_block = action_block
22
+ end
23
+
24
+ def action_block= block
25
+ raise OverriddenActionError unless @action_block.nil?
26
+ @action_block = block
27
+ end
28
+
29
+ def add_test_family fam
30
+ logger.debug " + Adding test family #{fam}"
31
+ fam.desc = self.name if fam.desc.blank?
32
+ @test_families.push fam
33
+ fam
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,53 @@
1
+ module Thoreau
2
+ module Model
3
+
4
+ class TestFamily
5
+ attr_reader :asserts,
6
+ :expected_exception,
7
+ :expected_output,
8
+ :kind,
9
+ :setups
10
+ attr_writer :focus
11
+ attr_accessor :use_legacy_snapshot
12
+ attr_accessor :desc
13
+
14
+ def initialize(asserts:,
15
+ desc:,
16
+ expected_exception:,
17
+ expected_output:,
18
+ failure_expected:,
19
+ input_specs:,
20
+ kind:,
21
+ setups:
22
+ )
23
+ @asserts = asserts
24
+ @desc = desc
25
+ @expected_exception = expected_exception
26
+ @expected_output = expected_output
27
+ @failure_expected = failure_expected
28
+ @input_specs = input_specs
29
+ @kind = kind
30
+ @setups = setups
31
+ end
32
+
33
+ def input_specs
34
+ @input_specs.size == 0 ?
35
+ [{}] : @input_specs
36
+ end
37
+
38
+ def failure_expected?
39
+ @failure_expected
40
+ end
41
+
42
+ def focused?
43
+ @focus
44
+ end
45
+
46
+ def to_s
47
+ "#{@desc || "#{@kind} #{(@input_specs.map &:to_s).to_sentence } expect #{expected_output}"}"
48
+ end
49
+
50
+ end
51
+ end
52
+ end
53
+
@@ -0,0 +1,50 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext/module/delegation'
3
+ require_relative './appendix'
4
+ require_relative '../case/multi_clan_case_builder'
5
+
6
+ module Thoreau
7
+ module Model
8
+ class TestSuite
9
+
10
+ @@suites = []
11
+
12
+ def initialize(data:, focus:)
13
+ @data = data
14
+ @focus = focus
15
+ @@suites << self
16
+
17
+ # @builder = Thoreau::Case::CaseBuilder.new test_clan: @data.test_clan
18
+ @builder = Thoreau::Case::MultiClanCaseBuilder.new test_clans: @data.test_clans
19
+ end
20
+
21
+ delegate :name, to: :@data
22
+
23
+ def build_and_run
24
+ logger.debug("## build_and_run")
25
+ cases = @builder.build_test_cases!
26
+ logger.debug(" ... built #{cases.size} cases")
27
+
28
+ runner = Thoreau::Case::SuiteRunner.new @data.name
29
+ runner.run_test_cases! cases,
30
+ @builder.skipped_count # for reporting
31
+ end
32
+
33
+ def focused?
34
+ @focus || @builder.any_focused?
35
+ end
36
+
37
+ def self.run_all!
38
+ logger.debug("# run_all! ############")
39
+ run_all = !@@suites.any?(&:focused?)
40
+ @@suites.each do |suite|
41
+ if suite.focused? || run_all
42
+ suite.build_and_run
43
+ else
44
+ logger.info(" Suite '#{suite.name}' skipped (unfocused)")
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ 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,53 @@
1
+ require 'logger'
2
+ require 'json'
3
+ require "pstore"
4
+
5
+ module Thoreau
6
+
7
+ class LegacyExpectedOutcomes
8
+
9
+ # A simple store of expected outcomes of test cases.
10
+ #
11
+ # They are stored in a "pstore" database, with the test descriptor
12
+ # and inputs being the key. If you change the input of the test, it
13
+ # should generate a new saved result.
14
+ #
15
+ # Set ENV['RESET_SNAPSHOTS'] to, well, reset all the snapshots.
16
+
17
+ VERSION = 1
18
+
19
+ def initialize(suite_name)
20
+ @suite_name = suite_name
21
+ end
22
+
23
+ def key_for(test_case)
24
+ o = {
25
+ desc: test_case.family_desc,
26
+ input: test_case.input
27
+ }
28
+ o.to_json
29
+ end
30
+
31
+ def has_saved_for? test_case
32
+ logger.debug("has_saved_for? for #{key_for(test_case)}")
33
+ !!(load_for test_case)
34
+ end
35
+
36
+ def load_for test_case
37
+ logger.debug("load_for for #{key_for(test_case)}")
38
+ wiki = PStore.new(Thoreau.configuration.legacy_outcome_path)
39
+ wiki.transaction do
40
+ wiki[key_for(test_case)]
41
+ end
42
+ end
43
+
44
+ def save! test_case
45
+ logger.debug("save! for #{key_for(test_case)}")
46
+ wiki = PStore.new(Thoreau.configuration.legacy_outcome_path)
47
+ wiki.transaction do
48
+ wiki[key_for(test_case)] = test_case.actual
49
+ end
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,36 @@
1
+ require 'logger'
2
+
3
+ # https://stackoverflow.com/questions/917566/ruby-share-logger-instance-among-module-classes
4
+ # The intended use is via "include":
5
+ module Thoreau
6
+ module Logging
7
+ class << self
8
+ def logger
9
+ if @logger.nil?
10
+ @logger = Logger.new(STDOUT, formatter: proc { |severity, datetime, progname, msg|
11
+ "#{severity}: #{msg}\n"
12
+ })
13
+ @logger.level = Logger::INFO
14
+ @logger.level = Logger::DEBUG if ENV['DEBUG']
15
+ end
16
+ @logger
17
+ end
18
+
19
+ # def logger=(logger)
20
+ # @logger = logger
21
+ # end
22
+ end
23
+
24
+ def self.included(base)
25
+ class << base
26
+ def logger
27
+ Logging.logger
28
+ end
29
+ end
30
+ end
31
+
32
+ def logger
33
+ Logging.logger
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,41 @@
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
13
+
14
+ module Thoreau
15
+ class HashUtil
16
+
17
+ # prop_map is a map from canonical property name to all versions
18
+ def self.normalize_props(hash, prop_map, include_all_normalized_props = true)
19
+ prop_map.reduce(Hash.new) do |memo, (k, v)|
20
+ value = one_of_these(hash, v, include_all_normalized_props ? nil : :secret_default_value)
21
+ memo[k] = value unless value == :secret_default_value
22
+ memo
23
+ end
24
+ end
25
+
26
+ def self.one_of_these(hash, pick_one_of_these_keys, default_value = nil)
27
+ keys_present = pick_one_of_these_keys.intersection hash.keys
28
+
29
+ if keys_present.size > 1
30
+ logger.error "Only of of these keys is allowed: #{keys_present.to_sentence}"
31
+ end
32
+
33
+ pick_one_of_these_keys.each do |k|
34
+ return hash[k] if hash.key?(k)
35
+ end
36
+
37
+ default_value
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ module Thoreau
2
+ VERSION = "0.3.1"
3
+ end
data/lib/thoreau.rb ADDED
@@ -0,0 +1,18 @@
1
+ require_relative 'thoreau/version'
2
+ require_relative 'thoreau/dsl'
3
+ require_relative 'thoreau/configuration'
4
+
5
+ module Thoreau
6
+ def self.configure &block
7
+ block.call configuration
8
+ end
9
+
10
+ def self.configuration
11
+ @configuration ||= Configuration.new
12
+ end
13
+
14
+ end
15
+
16
+ if defined?(RSpec)
17
+ require_relative('./thoreau/rspec')
18
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
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.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Peterson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-02 00:00:00.000000000 Z
11
+ date: 2021-12-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -17,9 +17,6 @@ dependencies:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '5'
20
- - - "<"
21
- - !ruby/object:Gem::Version
22
- version: '7'
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
@@ -27,9 +24,20 @@ dependencies:
27
24
  - - ">="
28
25
  - !ruby/object:Gem::Version
29
26
  version: '5'
30
- - - "<"
27
+ - !ruby/object:Gem::Dependency
28
+ name: rainbow
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
31
32
  - !ruby/object:Gem::Version
32
- version: '7'
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
33
41
  - !ruby/object:Gem::Dependency
34
42
  name: bundler
35
43
  requirement: !ruby/object:Gem::Requirement
@@ -106,7 +114,36 @@ email:
106
114
  executables: []
107
115
  extensions: []
108
116
  extra_rdoc_files: []
109
- files: []
117
+ files:
118
+ - lib/thoreau.rb
119
+ - lib/thoreau/auto_run.rb
120
+ - lib/thoreau/case/case_builder.rb
121
+ - lib/thoreau/case/context_builder.rb
122
+ - lib/thoreau/case/multi_clan_case_builder.rb
123
+ - lib/thoreau/case/suite_runner.rb
124
+ - lib/thoreau/configuration.rb
125
+ - lib/thoreau/dsl.rb
126
+ - lib/thoreau/dsl/context/appendix.rb
127
+ - lib/thoreau/dsl/context/clan.rb
128
+ - lib/thoreau/dsl/context/suite.rb
129
+ - lib/thoreau/dsl/context/test_cases.rb
130
+ - lib/thoreau/dsl/expanded.rb
131
+ - lib/thoreau/dsl/test_suite_data.rb
132
+ - lib/thoreau/errors.rb
133
+ - lib/thoreau/model/appendix.rb
134
+ - lib/thoreau/model/outcome.rb
135
+ - lib/thoreau/model/setup.rb
136
+ - lib/thoreau/model/test_case.rb
137
+ - lib/thoreau/model/test_clan.rb
138
+ - lib/thoreau/model/test_family.rb
139
+ - lib/thoreau/model/test_suite.rb
140
+ - lib/thoreau/rspec.rb
141
+ - lib/thoreau/rspec/configuration.rb
142
+ - lib/thoreau/rspec/example_helpers.rb
143
+ - lib/thoreau/service/legacy_expected_outcomes.rb
144
+ - lib/thoreau/service/logging.rb
145
+ - lib/thoreau/service/util.rb
146
+ - lib/thoreau/version.rb
110
147
  homepage: https://github.com/ndp/thoreau
111
148
  licenses: []
112
149
  metadata: