thoreau 0.2.1 → 0.2.2

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: a8c65711c30e69f7c09e814361f34d2c6b55501223f5b422eed26f23363dd7fb
4
- data.tar.gz: dbe902b4cb31334a3afe3a42b3fdde8078eb34ec26c1029fd25ec8825d2c6add
3
+ metadata.gz: 732fd064266fbd5f93dab80bee925ea5a61031de4cfdc82a9015ce5ac0e16c0d
4
+ data.tar.gz: 82a1e6dabb420fce9d2260c07a7913ecc996838b3f4e0d98f2f5a8938cafcdaa
5
5
  SHA512:
6
- metadata.gz: 6ddc80c61cc57201d339e9315df32add749b0219c363b92ce3cfbe8432c8a0076c2a584d8f9feb002202582431b33349c3069200ab44ee36ff7c172bd15df324
7
- data.tar.gz: 5e28db2a7a84fc7f9024dca9160115dd9973625f357f4ec499625b2b116e67335590d816b678aed099d20e55e9c4b7928e53d5c828340d886f8f30a6b9544e58
6
+ metadata.gz: ac549caf454efbc7d6e5806176897dd10a93177c34bea768f27030a49ed1c2da3bf8ad63ee86deb45ee4f5148a48f136f04ad24cacc63b921c03cdcd0912be37
7
+ data.tar.gz: 1acaf812da2937c14ae7b509fcdd96bf0c3211b4eeaa06eabe97ce032c1d50401915e316bfa3a39558e78895646fb11186c18fa0c730e45dd9f5a3c04f7a7577
@@ -0,0 +1,5 @@
1
+ require_relative './dsl'
2
+
3
+ at_exit do
4
+ Thoreau::TestSuite.run_all!
5
+ end
@@ -0,0 +1,114 @@
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 setup_key_to_inputs key
42
+ setup = @appendix.setups[key.to_s]
43
+ raise "Unrecognized setup context '#{key}'. Available: #{@appendix.setups.keys.to_sentence}" if setup.nil?
44
+ logger.debug(" setup_key_to_inputs `#{key}`: #{setup}")
45
+ return setup.values if setup.block.nil?
46
+
47
+ result = Class.new.new.instance_eval(&setup.block)
48
+ logger.error "Setup #{key} did not return a hash object" unless result.is_a?(Hash)
49
+ result
50
+ end
51
+
52
+ def build_family_cases fam
53
+ # We have "specs" for the inputs. These may be actual
54
+ # values, or they may be enumerables that need to execute.
55
+ # So we need to "explode" (or enumerate) the values,
56
+ # generating a single test for each combination.
57
+ #
58
+ setup_values = fam.setups
59
+ .map { |key| setup_key_to_inputs key }
60
+ .reduce(Hash.new) { |m, h| m.merge(h) }
61
+ logger.debug(" -> setup_values = #{setup_values}")
62
+ logger.debug(" -> fam.input_specs = #{fam.input_specs}")
63
+ input_sets = fam.input_specs
64
+ .map { |is| setup_values.merge(is) }
65
+ .flat_map do |input_spec|
66
+ explode_input_specs(input_spec.keys, input_spec)
67
+ end
68
+ input_sets = [{}] if input_sets.size == 0
69
+ logger.debug(" -> input_sets: #{input_sets}")
70
+ logger.debug(" build cases for '#{fam.desc}', #{setup_values.size} setups, #{input_sets.size} input sets, build_family_cases")
71
+
72
+ input_sets.map do |input_set|
73
+ expectation = fam.use_legacy_snapshot ?
74
+ :use_legacy_snapshot :
75
+ Models::Outcome.new(output: fam.expected_output,
76
+ exception: fam.expected_exception)
77
+
78
+ Thoreau::Models::TestCase.new family_desc: "#{fam.kind.to_s.ljust(10).capitalize} #{fam.desc}",
79
+ input: input_set,
80
+ action_block: @action_block,
81
+ expectation: expectation,
82
+ asserts: fam.asserts,
83
+ negativo: fam.failure_expected?
84
+ end
85
+
86
+ end
87
+
88
+ # Expand any values that are enumerators (Thoreau::DSL::Expanded),
89
+ # creating a list of objects, where all the combinations
90
+ # of enumerated values are present.
91
+ def explode_input_specs(keys, input_spec)
92
+ k = keys.pop
93
+
94
+ value_spec = input_spec[k]
95
+ specs = if value_spec.is_a?(Thoreau::DSL::Expanded)
96
+ value_spec.map do |v|
97
+ input_spec.merge(k => v)
98
+ end
99
+ else
100
+ [input_spec]
101
+ end
102
+
103
+ # Are we done?
104
+ return specs if keys.empty?
105
+
106
+ specs.flat_map do |spec|
107
+ explode_input_specs(keys, spec) # recurse!
108
+ end
109
+
110
+ end
111
+
112
+ end
113
+ end
114
+ end
@@ -1,10 +1,9 @@
1
1
  module Thoreau
2
- class Case
2
+ module Case
3
3
  class ContextBuilder
4
4
 
5
- def initialize(group:, input:)
6
- @group = group
7
- @input = input
5
+ def initialize(input:)
6
+ @input = input
8
7
  end
9
8
 
10
9
  def create_context
@@ -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(name)
11
+ @suite_name = 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 = './tmp/legacy-outcomes.pstore'
7
+ end
8
+ end
9
+ end
@@ -1,15 +1,15 @@
1
- require_relative '../setup'
1
+ require_relative '../models/setup'
2
2
 
3
3
  module Thoreau
4
4
  module DSL
5
5
  class Appendix
6
- def initialize(context)
7
- @context = context
6
+ def initialize(appendix_model, &appendix_block)
7
+ @model = appendix_model
8
+ self.instance_eval(&appendix_block)
8
9
  end
9
10
 
10
11
  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)
12
+ @model.add_setup(name, values, block)
13
13
  end
14
14
 
15
15
  end
@@ -0,0 +1,94 @@
1
+ require_relative '../models/test_family'
2
+ require_relative './expanded'
3
+ require_relative '../util'
4
+ require 'active_support/core_ext/array/conversions'
5
+
6
+ module Thoreau
7
+ module DSL
8
+
9
+ SPEC_FAMILY_NAMES = %i[happy sad spec test edge edges boundary corner gigo]
10
+ # gigo = garbage in / garbage out
11
+ #
12
+ PROPS = {
13
+ asserts: %i[assert asserts post post_condition],
14
+ expected_exception: %i[raises],
15
+ expected_output: %i[equals equal expected expect expects output],
16
+ failure_expected: %i[fails pending],
17
+ input_specs: %i[input inputs],
18
+ setups: %i[setup setups]
19
+ }
20
+ ALL_PROPS = PROPS.values.flatten.map(&:to_s)
21
+ PROPS_SPELL_CHECKER = DidYouMean::SpellChecker.new(dictionary: ALL_PROPS)
22
+
23
+ module Clan
24
+
25
+ # Note: requires `@test_clan_model`.
26
+
27
+ def action(&block)
28
+ logger.debug " + Adding action block"
29
+ @test_clan_model.action_block = block
30
+ end
31
+ alias testing action
32
+ alias subject action
33
+
34
+
35
+ def self.def_family_methods_for(sym)
36
+ define_method sym do |*args|
37
+ desc = args.shift if args.size > 1 && args.first.is_a?(String)
38
+ raise "Too many arguments to #{sym}!" if args.size > 1
39
+
40
+ spec = args.first&.stringify_keys || {}
41
+ spec.keys
42
+ .reject { |k| ALL_PROPS.include? k }
43
+ .each do |k|
44
+ suggestions = PROPS_SPELL_CHECKER.correct(k)
45
+ logger.error "Ignoring unrecognized property '#{k}'."
46
+ logger.info " Did you mean #{suggestions.to_sentence}?" if suggestions.size > 0
47
+ logger.info " Available properties: #{ALL_PROPS.to_sentence}"
48
+ end
49
+
50
+ params = HashUtil.normalize_props(spec.symbolize_keys, PROPS).tap { |props|
51
+ # These two props are easier to deal with downstream as empty arrays
52
+ props[:input_specs] = [props[:input_specs]].flatten.compact
53
+ props[:setups] = [props[:setups]].flatten.compact
54
+ }.merge kind: sym,
55
+ desc: desc
56
+
57
+ family = Models::TestFamily.new **params
58
+
59
+ yield family if block_given?
60
+
61
+ logger.debug " * Created new family #{params.inspect}"
62
+ @test_clan_model.add_test_family family
63
+ end
64
+
65
+ define_method "#{sym}!" do |*args|
66
+ family = self.send(sym, *args)
67
+ family.focus = true
68
+ family
69
+ end
70
+ end
71
+
72
+ SPEC_FAMILY_NAMES.each do |sym|
73
+ def_family_methods_for sym
74
+ end
75
+
76
+ def_family_methods_for :legacy do |r|
77
+ r.use_legacy_snapshot = true
78
+ end
79
+
80
+ alias legacy_spec legacy
81
+ alias legacy_test legacy
82
+ alias legacy_code legacy
83
+ alias legacy_spec! legacy!
84
+ alias legacy_test! legacy!
85
+ alias legacy_code! legacy!
86
+
87
+ def expanded(a)
88
+ Thoreau::DSL::Expanded.new(a)
89
+ end
90
+
91
+ end
92
+
93
+ end
94
+ end
@@ -0,0 +1,46 @@
1
+ require_relative '../test_suite_data'
2
+ require 'active_support/core_ext/module/delegation'
3
+
4
+ module Thoreau
5
+ module DSL
6
+
7
+ class SuiteContext
8
+
9
+ include Thoreau::Logging
10
+
11
+ attr_reader :suite_data
12
+ attr_reader :test_clan_model
13
+
14
+ def initialize suite_data:, test_clan_model:
15
+ raise "Suites must have (unique) names." if suite_data.name.blank?
16
+ @suite_data = suite_data
17
+ @test_clan_model = test_clan_model
18
+ end
19
+
20
+ delegate :name, to: :suite_data
21
+
22
+ def cases(name = nil, &block)
23
+ name = self.suite_data.name if name.nil?
24
+ logger.debug " + adding cases named `#{name}`"
25
+ @suite_data.test_cases_blocks << [name, block]
26
+ end
27
+
28
+ alias test_cases cases
29
+
30
+ def appendix(&block)
31
+ logger.debug " adding appendix block"
32
+ @suite_data.appendix_block = block
33
+ end
34
+
35
+ def context
36
+ self
37
+ end
38
+
39
+ def add_test_family family
40
+ suite_data.add_test_family family
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ module Thoreau
2
+ module DSL
3
+ class TestCases
4
+
5
+ include Thoreau::Logging
6
+
7
+ attr_reader :test_clan_model
8
+
9
+ def initialize(clan_model, &context)
10
+ @test_clan_model = clan_model
11
+
12
+ self.instance_eval(&context)
13
+ end
14
+
15
+ include Thoreau::DSL::Clan
16
+
17
+ end
18
+
19
+ end
20
+ end
data/lib/thoreau/dsl.rb CHANGED
@@ -1,40 +1,53 @@
1
- require 'logger'
2
-
1
+ require 'thoreau/logging'
3
2
  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'
3
+ require 'thoreau/models/test_case'
4
+ require 'thoreau/models/test_clan'
5
+ require 'thoreau/case/case_builder'
6
+ require 'thoreau/case/suite_runner'
7
+ require 'thoreau/dsl/clan'
8
+ require 'thoreau/dsl/suite_context'
9
+ require 'thoreau/dsl/test_cases'
10
10
  require 'thoreau/dsl/appendix'
11
-
12
- at_exit do
13
- Thoreau::TestSuite.run_all!
14
- end
11
+ require_relative './errors'
15
12
 
16
13
  module Thoreau
14
+
17
15
  module DSL
18
16
 
19
- attr_reader :logger
17
+ include Thoreau::Logging
18
+
19
+ attr_reader :suite_data
20
20
 
21
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']
22
+ logger.debug("# Processing keyword `test_suite`")
23
+
24
+ appendix = Models::Appendix.new
25
+ top_level_clan_model = Thoreau::Models::TestClan.new name, appendix: appendix
26
+ @suite_data = TestSuiteData.new name, test_clan: top_level_clan_model, appendix: appendix
27
+
28
+ # Evaluate all the top-level keywords: test_cases, appendix
29
+ @suite_context = Thoreau::DSL::SuiteContext.new suite_data: @suite_data,
30
+ test_clan_model: top_level_clan_model
31
+ logger.debug("## Evaluating suite")
32
+ @suite_context.instance_eval(&block)
33
+
34
+ logger.debug("## Evaluating appendix block")
35
+ appendix_block = @suite_data.appendix_block
36
+ Thoreau::DSL::Appendix.new(@suite_data, &appendix_block) unless appendix_block.nil?
27
37
 
28
- @context = Thoreau::DSL::Context.new(name, @logger)
38
+ logger.debug("## Evaluating test_cases blocks")
39
+ @suite_data.test_cases_blocks.each do |name, cases_block|
29
40
 
30
- appendix_context = Thoreau::DSL::Appendix.new(@context)
31
- groups_context = Thoreau::DSL::Groups.new(@context)
41
+ raise TestCasesAtMultipleLevelsError unless @suite_data.test_clans.first.empty?
32
42
 
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?
43
+ test_clan_model = Thoreau::Models::TestClan.new name,
44
+ appendix: appendix,
45
+ action_block: top_level_clan_model.action_block
46
+ Thoreau::DSL::TestCases.new(test_clan_model, &cases_block)
47
+ @suite_data.test_clans << test_clan_model
48
+ end
36
49
 
37
- TestSuite.new(context: @context, focus: focus, logger: logger, name: name)
50
+ TestSuite.new(data: @suite_data, focus: focus)
38
51
  end
39
52
 
40
53
  def xtest_suite name = nil, &block
@@ -49,7 +62,7 @@ module Thoreau
49
62
 
50
63
  alias suite! test_suite!
51
64
 
52
- include Thoreau::DSL::GroupsSupport
65
+ include Thoreau::DSL::Clan
53
66
 
54
67
  end
55
68
  end