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
@@ -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,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,17 @@
|
|
1
|
+
module Thoreau
|
2
|
+
module Models
|
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
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Thoreau
|
2
|
+
module Models
|
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 Models
|
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,110 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
require_relative '../case/context_builder'
|
3
|
+
require_relative './outcome'
|
4
|
+
require_relative '../legacy_expected_outcomes'
|
5
|
+
|
6
|
+
module Thoreau
|
7
|
+
module Models
|
8
|
+
class TestCase
|
9
|
+
|
10
|
+
include Thoreau::Logging
|
11
|
+
|
12
|
+
attr_reader :actual, :family_desc, :input
|
13
|
+
attr_accessor :expectation
|
14
|
+
|
15
|
+
def initialize family_desc:,
|
16
|
+
input:,
|
17
|
+
action_block:,
|
18
|
+
expectation:,
|
19
|
+
asserts:,
|
20
|
+
negativo:
|
21
|
+
|
22
|
+
@family_desc = family_desc
|
23
|
+
@input = input
|
24
|
+
@action_block = action_block
|
25
|
+
@negativo = negativo
|
26
|
+
|
27
|
+
@expectation = expectation
|
28
|
+
|
29
|
+
@assert_proc = asserts
|
30
|
+
|
31
|
+
@ran = false
|
32
|
+
end
|
33
|
+
|
34
|
+
def failure_expected?
|
35
|
+
@negativo
|
36
|
+
end
|
37
|
+
|
38
|
+
def desc
|
39
|
+
"#{@family_desc} #{(@input == {} ? nil : @input.sort.to_h) || @expectation.exception || "(no args)"}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def problem
|
43
|
+
|
44
|
+
run unless @ran
|
45
|
+
|
46
|
+
if @expectation.exception
|
47
|
+
|
48
|
+
logger.debug " -> Expected Exception #{@expectation.exception} @actual.exception:#{@actual.exception}"
|
49
|
+
|
50
|
+
if @actual.exception.to_s == @expectation.exception.to_s
|
51
|
+
nil
|
52
|
+
elsif @actual.exception.nil?
|
53
|
+
"Expected exception, but none raised"
|
54
|
+
elsif @actual.exception.is_a?(NameError)
|
55
|
+
"Did you forget to define an input? Error: #{@actual.exception}"
|
56
|
+
else
|
57
|
+
"Expected '#{@expectation.exception}' exception, but raised '#{@actual.exception}' (#{@actual.exception.class.name})"
|
58
|
+
end
|
59
|
+
|
60
|
+
elsif @assert_proc
|
61
|
+
|
62
|
+
logger.debug " -> Assert Proc result=#{@assert_result}"
|
63
|
+
|
64
|
+
@assert_result ? nil : "Assertion failed. (got #{@assert_result})"
|
65
|
+
else
|
66
|
+
|
67
|
+
logger.debug " -> Result expected: result=#{@actual.output} expected_output: #{@expectation.output} @actual.exception:#{@actual.exception}"
|
68
|
+
|
69
|
+
if @actual.exception
|
70
|
+
"Expected output, but raised exception '#{@actual.exception}'"
|
71
|
+
elsif @expectation.output != @actual.output
|
72
|
+
"Expected '#{@expectation.output}', but got '#{@actual.output}'"
|
73
|
+
else
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def ok?
|
80
|
+
failure_expected? ^ !!problem.nil?
|
81
|
+
end
|
82
|
+
|
83
|
+
def failed?
|
84
|
+
!ok?
|
85
|
+
end
|
86
|
+
|
87
|
+
def run
|
88
|
+
logger.debug "## RUN #{desc}"
|
89
|
+
context_builder = Case::ContextBuilder.new(input: @input)
|
90
|
+
context = context_builder.create_context
|
91
|
+
begin
|
92
|
+
# Only capture exceptions around the action itself.
|
93
|
+
output = context.instance_exec(&(@action_block))
|
94
|
+
@actual = Models::Outcome.new output: output
|
95
|
+
rescue Exception => e
|
96
|
+
logger.debug("** Exception: #{e.class.name} #{e}")
|
97
|
+
logger.debug("Available local variables: #{@input.keys.empty? ? '(none)' : @input.keys.to_sentence}") if e.is_a? NameError
|
98
|
+
@actual = Models::Outcome.new exception: e
|
99
|
+
return
|
100
|
+
ensure
|
101
|
+
@ran = true
|
102
|
+
end
|
103
|
+
|
104
|
+
@expectation.evaluate(@actual.output, context) unless @expectation == :use_legacy_snapshot
|
105
|
+
@assert_result = context.instance_exec(@actual.output, &(@assert_proc)) if @assert_proc
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
require_relative '../errors'
|
3
|
+
|
4
|
+
module Thoreau
|
5
|
+
|
6
|
+
module Models
|
7
|
+
class TestClan # set of TestFamilies
|
8
|
+
|
9
|
+
include Thoreau::Logging
|
10
|
+
|
11
|
+
attr_accessor :test_families, :appendix
|
12
|
+
attr_reader :name, :action_block
|
13
|
+
|
14
|
+
delegate :empty?, to: :test_families
|
15
|
+
|
16
|
+
def initialize(name, appendix:, action_block: nil)
|
17
|
+
@name = name
|
18
|
+
@test_families = []
|
19
|
+
@appendix = appendix
|
20
|
+
@action_block = action_block
|
21
|
+
end
|
22
|
+
|
23
|
+
def action_block= block
|
24
|
+
raise OverriddenActionError unless @action_block.nil?
|
25
|
+
@action_block = block
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_test_family fam
|
29
|
+
logger.debug " + Adding test family #{fam}"
|
30
|
+
fam.desc = self.name if fam.desc.blank?
|
31
|
+
@test_families.push fam
|
32
|
+
fam
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Thoreau
|
2
|
+
module Models
|
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
|
+
|
data/lib/thoreau/test_suite.rb
CHANGED
@@ -1,24 +1,29 @@
|
|
1
|
+
require_relative './models/appendix'
|
2
|
+
require_relative './case/multi_clan_case_builder'
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
4
|
+
|
1
5
|
module Thoreau
|
2
6
|
class TestSuite
|
3
7
|
|
4
|
-
attr_reader :name
|
5
|
-
attr_reader :logger
|
6
|
-
|
7
8
|
@@suites = []
|
8
9
|
|
9
|
-
def initialize(
|
10
|
-
@
|
11
|
-
@
|
12
|
-
@name = name
|
13
|
-
@focus = focus
|
10
|
+
def initialize(data:, focus:)
|
11
|
+
@data = data
|
12
|
+
@focus = focus
|
14
13
|
@@suites << self
|
15
|
-
|
14
|
+
|
15
|
+
# @builder = Thoreau::Case::CaseBuilder.new test_clan: @data.test_clan
|
16
|
+
@builder = Thoreau::Case::MultiClanCaseBuilder.new test_clans: @data.test_clans
|
16
17
|
end
|
17
18
|
|
19
|
+
delegate :name, to: :@data
|
20
|
+
|
18
21
|
def build_and_run
|
22
|
+
logger.debug("## build_and_run")
|
19
23
|
cases = @builder.build_test_cases!
|
24
|
+
logger.debug(" ... built #{cases.size} cases")
|
20
25
|
|
21
|
-
runner = Thoreau::Case::
|
26
|
+
runner = Thoreau::Case::SuiteRunner.new @data.name
|
22
27
|
runner.run_test_cases! cases,
|
23
28
|
@builder.skipped_count # for reporting
|
24
29
|
end
|
@@ -28,12 +33,13 @@ module Thoreau
|
|
28
33
|
end
|
29
34
|
|
30
35
|
def self.run_all!
|
36
|
+
logger.debug("# run_all! ############")
|
31
37
|
run_all = !@@suites.any?(&:focused?)
|
32
38
|
@@suites.each do |suite|
|
33
39
|
if suite.focused? || run_all
|
34
40
|
suite.build_and_run
|
35
41
|
else
|
36
|
-
|
42
|
+
logger.info(" Suite '#{suite.name}' skipped (unfocused)")
|
37
43
|
end
|
38
44
|
end
|
39
45
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative './models/test_clan'
|
2
|
+
|
3
|
+
module Thoreau
|
4
|
+
class TestSuiteData
|
5
|
+
|
6
|
+
include Thoreau::Logging
|
7
|
+
|
8
|
+
attr_accessor :appendix_block
|
9
|
+
attr_reader :test_cases_blocks
|
10
|
+
|
11
|
+
attr_reader :name
|
12
|
+
attr_reader :test_clans
|
13
|
+
|
14
|
+
def initialize name, appendix:, test_clan:
|
15
|
+
@name = name
|
16
|
+
@appendix = appendix
|
17
|
+
@test_clans = [test_clan]
|
18
|
+
@test_cases_blocks = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_setup(name, values, block)
|
22
|
+
logger.debug " Adding setup block #{name}"
|
23
|
+
@appendix.add_setup Thoreau::Models::Setup.new(name, values, block)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|