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 +4 -4
- data/lib/thoreau/auto_run.rb +5 -0
- data/lib/thoreau/case/case_builder.rb +114 -0
- data/lib/thoreau/case/context_builder.rb +3 -4
- data/lib/thoreau/case/multi_clan_case_builder.rb +27 -0
- data/lib/thoreau/case/suite_runner.rb +61 -0
- data/lib/thoreau/configuration.rb +9 -0
- data/lib/thoreau/dsl/appendix.rb +5 -5
- data/lib/thoreau/dsl/clan.rb +94 -0
- data/lib/thoreau/dsl/suite_context.rb +46 -0
- data/lib/thoreau/dsl/test_cases.rb +20 -0
- data/lib/thoreau/dsl.rb +39 -26
- data/lib/thoreau/errors.rb +15 -0
- data/lib/thoreau/legacy_expected_outcomes.rb +53 -0
- data/lib/thoreau/logging.rb +36 -0
- data/lib/thoreau/models/appendix.rb +17 -0
- data/lib/thoreau/models/outcome.rb +33 -0
- data/lib/thoreau/models/setup.rb +17 -0
- data/lib/thoreau/models/test_case.rb +110 -0
- data/lib/thoreau/models/test_clan.rb +37 -0
- data/lib/thoreau/models/test_family.rb +53 -0
- data/lib/thoreau/test_suite.rb +17 -11
- data/lib/thoreau/test_suite_data.rb +27 -0
- data/lib/thoreau/util.rb +29 -0
- data/lib/thoreau/version.rb +1 -1
- data/lib/thoreau.rb +8 -2
- metadata +34 -11
- data/lib/thoreau/case/builder.rb +0 -110
- data/lib/thoreau/case/runner.rb +0 -42
- data/lib/thoreau/case.rb +0 -97
- data/lib/thoreau/dsl/context.rb +0 -55
- data/lib/thoreau/dsl/groups.rb +0 -20
- data/lib/thoreau/dsl/groups_support.rb +0 -58
- data/lib/thoreau/legacy_results.rb +0 -8
- data/lib/thoreau/setup.rb +0 -15
- data/lib/thoreau/spec_group.rb +0 -45
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 732fd064266fbd5f93dab80bee925ea5a61031de4cfdc82a9015ce5ac0e16c0d
|
4
|
+
data.tar.gz: 82a1e6dabb420fce9d2260c07a7913ecc996838b3f4e0d98f2f5a8938cafcdaa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ac549caf454efbc7d6e5806176897dd10a93177c34bea768f27030a49ed1c2da3bf8ad63ee86deb45ee4f5148a48f136f04ad24cacc63b921c03cdcd0912be37
|
7
|
+
data.tar.gz: 1acaf812da2937c14ae7b509fcdd96bf0c3211b4eeaa06eabe97ce032c1d50401915e316bfa3a39558e78895646fb11186c18fa0c730e45dd9f5a3c04f7a7577
|
@@ -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
|
@@ -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
|
data/lib/thoreau/dsl/appendix.rb
CHANGED
@@ -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(
|
7
|
-
@
|
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
|
-
|
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 '
|
2
|
-
|
1
|
+
require 'thoreau/logging'
|
3
2
|
require 'thoreau/test_suite'
|
4
|
-
require 'thoreau/
|
5
|
-
require 'thoreau/
|
6
|
-
require 'thoreau/case/
|
7
|
-
require 'thoreau/
|
8
|
-
require 'thoreau/dsl/
|
9
|
-
require 'thoreau/dsl/
|
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
|
-
|
17
|
+
include Thoreau::Logging
|
18
|
+
|
19
|
+
attr_reader :suite_data
|
20
20
|
|
21
21
|
def test_suite name = nil, focus: false, &block
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
38
|
+
logger.debug("## Evaluating test_cases blocks")
|
39
|
+
@suite_data.test_cases_blocks.each do |name, cases_block|
|
29
40
|
|
30
|
-
|
31
|
-
groups_context = Thoreau::DSL::Groups.new(@context)
|
41
|
+
raise TestCasesAtMultipleLevelsError unless @suite_data.test_clans.first.empty?
|
32
42
|
|
33
|
-
|
34
|
-
|
35
|
-
|
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(
|
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::
|
65
|
+
include Thoreau::DSL::Clan
|
53
66
|
|
54
67
|
end
|
55
68
|
end
|