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 +4 -4
- data/lib/thoreau/auto_run.rb +6 -0
- data/lib/thoreau/case/case_builder.rb +110 -0
- data/lib/thoreau/case/context_builder.rb +27 -0
- 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/context/appendix.rb +17 -0
- data/lib/thoreau/dsl/context/clan.rb +97 -0
- data/lib/thoreau/dsl/context/suite.rb +40 -0
- data/lib/thoreau/dsl/context/test_cases.rb +21 -0
- data/lib/thoreau/dsl/expanded.rb +14 -0
- data/lib/thoreau/dsl/test_suite_data.rb +29 -0
- data/lib/thoreau/dsl.rb +69 -0
- data/lib/thoreau/errors.rb +15 -0
- data/lib/thoreau/model/appendix.rb +37 -0
- data/lib/thoreau/model/outcome.rb +33 -0
- data/lib/thoreau/model/setup.rb +17 -0
- data/lib/thoreau/model/test_case.rb +131 -0
- data/lib/thoreau/model/test_clan.rb +38 -0
- data/lib/thoreau/model/test_family.rb +53 -0
- data/lib/thoreau/model/test_suite.rb +50 -0
- data/lib/thoreau/rspec/configuration.rb +14 -0
- data/lib/thoreau/rspec/example_helpers.rb +7 -0
- data/lib/thoreau/rspec.rb +19 -0
- data/lib/thoreau/service/legacy_expected_outcomes.rb +53 -0
- data/lib/thoreau/service/logging.rb +36 -0
- data/lib/thoreau/service/util.rb +41 -0
- data/lib/thoreau/version.rb +3 -0
- data/lib/thoreau.rb +18 -0
- metadata +45 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d712bdb1cef5f616159042ee63dba2694717f2b60d0fa8b72994f5568d23ef7a
|
4
|
+
data.tar.gz: 61d14f95c9344a58cd68cb96bf0123679a9aa67d0a08d71859473bf3774fd7c1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd809493f3ec81b435849c64409a595fee425dcf6952ab636d682a3ff52d1df46eeed1f46e5c0f1daf3a1c208207306b51a6c365dcefb4a7a7d24e5cdb39c8d0
|
7
|
+
data.tar.gz: 510efd7e19530338004efb17dba5fe94e22d9d51bbd12fcf02ec60e8eee4b061d44771b08ae82781b45effd37428647c62e65af803dc551a29b1578cf441ceb1
|
@@ -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,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,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
|
data/lib/thoreau/dsl.rb
ADDED
@@ -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,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
|
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.
|
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-
|
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: '
|
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:
|