strut 0.1.0
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 +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.simplecov +7 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +80 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/exe/strut +5 -0
- data/lib/strut.rb +60 -0
- data/lib/strut/annotation.rb +7 -0
- data/lib/strut/config.rb +60 -0
- data/lib/strut/document.rb +14 -0
- data/lib/strut/document_builder.rb +17 -0
- data/lib/strut/extensions.rb +81 -0
- data/lib/strut/interaction_factory.rb +25 -0
- data/lib/strut/parser.rb +57 -0
- data/lib/strut/report.rb +48 -0
- data/lib/strut/report_builder.rb +56 -0
- data/lib/strut/report_junit_formatter.rb +47 -0
- data/lib/strut/report_pretty_formatter.rb +91 -0
- data/lib/strut/scenario_builder.rb +124 -0
- data/lib/strut/scenario_result.rb +59 -0
- data/lib/strut/slim_client.rb +61 -0
- data/lib/strut/slim_command.rb +89 -0
- data/lib/strut/slim_command_factory.rb +29 -0
- data/lib/strut/version.rb +3 -0
- data/strut.gemspec +30 -0
- metadata +188 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
module Strut
|
2
|
+
Interaction = Struct.new(:uri, :method, :statusCode)
|
3
|
+
|
4
|
+
class InteractionFactory
|
5
|
+
def make_interaction(path_stack)
|
6
|
+
interaction = Interaction.new
|
7
|
+
|
8
|
+
if path_stack[0] == "paths"
|
9
|
+
interaction.uri = path_stack[1] unless extension?(path_stack[1])
|
10
|
+
end
|
11
|
+
|
12
|
+
interaction.method = path_stack[2] unless extension?(path_stack[2])
|
13
|
+
|
14
|
+
if path_stack[3] == "responses"
|
15
|
+
interaction.statusCode = path_stack[4] unless extension?(path_stack[4])
|
16
|
+
end
|
17
|
+
|
18
|
+
interaction
|
19
|
+
end
|
20
|
+
|
21
|
+
def extension?(tag)
|
22
|
+
tag =~ /^x-/
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/strut/parser.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require "strut/extensions"
|
2
|
+
require "strut/document_builder"
|
3
|
+
require "strut/interaction_factory"
|
4
|
+
require "strut/scenario_builder"
|
5
|
+
require "strut/slim_command"
|
6
|
+
require "strut/slim_command_factory"
|
7
|
+
|
8
|
+
module Strut
|
9
|
+
class Parser
|
10
|
+
X_SCENARIO_PREFIX = "x-scenario-"
|
11
|
+
|
12
|
+
def initialize(namespace)
|
13
|
+
@namespace = namespace
|
14
|
+
@command_factory = SlimCommandFactory.new
|
15
|
+
@document_builder = DocumentBuilder.new
|
16
|
+
@interaction_factory = InteractionFactory.new
|
17
|
+
@scenario_builder = ScenarioBuilder.new(@document_builder, @command_factory)
|
18
|
+
@scenario_number = 1
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse(yaml)
|
22
|
+
parsed_yaml = Psych::parse_yaml(yaml)
|
23
|
+
append_import_command
|
24
|
+
extract_scenarios(parsed_yaml)
|
25
|
+
@document_builder.document
|
26
|
+
end
|
27
|
+
|
28
|
+
def append_import_command
|
29
|
+
metadata = CommandMetadata.new(1)
|
30
|
+
import_command = @command_factory.make_import_command(metadata, @namespace)
|
31
|
+
@document_builder.append_command(import_command)
|
32
|
+
end
|
33
|
+
|
34
|
+
def extract_scenarios(node)
|
35
|
+
wrapped_node = {"value" => node, "line" => 0}
|
36
|
+
extract_scenarios_for_node("", wrapped_node, [])
|
37
|
+
end
|
38
|
+
|
39
|
+
def extract_scenarios_for_node(node_name, node, path_stack)
|
40
|
+
if node_name.start_with?(X_SCENARIO_PREFIX)
|
41
|
+
fixture = node_name.gsub(/^#{X_SCENARIO_PREFIX}/, "")
|
42
|
+
interaction = @interaction_factory.make_interaction(path_stack)
|
43
|
+
@scenario_number = @scenario_builder.extract_scenarios_for_interaction(@scenario_number, interaction, fixture, node)
|
44
|
+
else
|
45
|
+
extract_scenarios_for_children(@scenario_number, node["value"], path_stack)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def extract_scenarios_for_children(scenario_number, node, path_stack)
|
50
|
+
return unless node.respond_to?(:each_pair)
|
51
|
+
node.each_pair do |child_node_name, child_node|
|
52
|
+
next_path_stack = path_stack + [child_node_name]
|
53
|
+
extract_scenarios_for_node(child_node_name.to_s, child_node, next_path_stack)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/strut/report.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require "strut/scenario_result"
|
2
|
+
|
3
|
+
module Strut
|
4
|
+
class Report
|
5
|
+
attr_reader :number_scenarios, :number_passed, :number_failed, :number_skipped
|
6
|
+
attr_reader :scenario_results, :errors
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@scenario_results = []
|
10
|
+
@errors = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_scenario_result(result)
|
14
|
+
@scenario_results << result
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_error(error)
|
18
|
+
@errors << error
|
19
|
+
end
|
20
|
+
|
21
|
+
def annotations_for_line(line)
|
22
|
+
all_annotations = @scenario_results.map do |result|
|
23
|
+
result.annotations_for_line(line)
|
24
|
+
end
|
25
|
+
all_annotations.reject { |a| a.empty? }.flatten
|
26
|
+
end
|
27
|
+
|
28
|
+
def number_scenarios
|
29
|
+
@scenario_results.count
|
30
|
+
end
|
31
|
+
|
32
|
+
def number_passed
|
33
|
+
number_with_result(SCENARIO_PASS)
|
34
|
+
end
|
35
|
+
|
36
|
+
def number_failed
|
37
|
+
number_with_result(SCENARIO_FAIL)
|
38
|
+
end
|
39
|
+
|
40
|
+
def number_skipped
|
41
|
+
number_with_result(SCENARIO_ERROR)
|
42
|
+
end
|
43
|
+
|
44
|
+
def number_with_result(result)
|
45
|
+
@scenario_results.select { |scenario| scenario.result == result }.count
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "strut/report"
|
2
|
+
|
3
|
+
module Strut
|
4
|
+
class ReportBuilder
|
5
|
+
def build(responses, document)
|
6
|
+
report = Report.new
|
7
|
+
|
8
|
+
scenario_results = Hash.new { |h, k| h[k] = ScenarioResult.new }
|
9
|
+
responses.each do |response|
|
10
|
+
handle_response(response, document, report, scenario_results)
|
11
|
+
end
|
12
|
+
|
13
|
+
scenario_results.each_pair do |_, result|
|
14
|
+
report.add_scenario_result(result)
|
15
|
+
end
|
16
|
+
|
17
|
+
report
|
18
|
+
end
|
19
|
+
|
20
|
+
def handle_response(response, document, report, scenario_results)
|
21
|
+
command_id, command_result = *response
|
22
|
+
if command_id == "error"
|
23
|
+
report.add_error(command_result.to_s)
|
24
|
+
else
|
25
|
+
metadata = document.metadata_for_command_id(command_id)
|
26
|
+
if metadata
|
27
|
+
process_result(command_result, metadata, report, scenario_results)
|
28
|
+
else
|
29
|
+
report.add_error("Unexpected response from Slim: #{response.inspect}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def process_result(result, metadata, report, scenario_results)
|
35
|
+
scenario_result = scenario_results[metadata.scenario_number]
|
36
|
+
scenario_result.name = "Scenario #{metadata.scenario_number}, line #{metadata.line}"
|
37
|
+
|
38
|
+
if exception_message = exceptional_result?(result)
|
39
|
+
scenario_result.add_exception_for_line(metadata.line, exception_message)
|
40
|
+
elsif failed_result?(result, metadata)
|
41
|
+
fail_message = "Expected #{metadata.expected_value} but got #{result}"
|
42
|
+
scenario_result.add_fail_for_line(metadata.line, fail_message)
|
43
|
+
else
|
44
|
+
scenario_result.add_ok_for_line(metadata.line)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def exceptional_result?(result)
|
49
|
+
result =~ /^__EXCEPTION__:message:(.+)/ ? $1 : nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def failed_result?(result, metadata)
|
53
|
+
metadata.expected_value && metadata.expected_value.to_s != result.to_s
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Strut
|
2
|
+
class ReportJUnitFormatter
|
3
|
+
def format(report)
|
4
|
+
doc = REXML::Document.new("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
|
5
|
+
results = report.scenario_results
|
6
|
+
add_testsuite(results, doc)
|
7
|
+
doc.to_s
|
8
|
+
end
|
9
|
+
|
10
|
+
def add_testsuite(results, doc)
|
11
|
+
testsuite = doc.add_element("testsuite")
|
12
|
+
results.each do |scenario|
|
13
|
+
add_testcase_for_scenario(scenario, testsuite)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_testcase_for_scenario(scenario, testsuite)
|
18
|
+
testcase = testsuite.add_element("testcase")
|
19
|
+
testcase.attributes["name"] = scenario.name
|
20
|
+
testcase.attributes["time"] = scenario.time
|
21
|
+
testcase.attributes["classname"] = "swagger"
|
22
|
+
add_result_for_testcase(scenario, testcase)
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_result_for_testcase(scenario, testcase)
|
26
|
+
if scenario.result == SCENARIO_FAIL
|
27
|
+
add_failure_for_testcase(scenario.message, testcase)
|
28
|
+
elsif scenario.result == SCENARIO_ERROR
|
29
|
+
add_error_for_testcase(scenario.message, testcase)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_failure_for_testcase(message, testcase)
|
34
|
+
add_element_for_testcase("failure", testcase, message, "assert")
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_error_for_testcase(message, testcase)
|
38
|
+
add_element_for_testcase("error", testcase, message, "exception")
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_element_for_testcase(element, testcase, message, type)
|
42
|
+
node = testcase.add_element(element)
|
43
|
+
node.attributes["message"] = message
|
44
|
+
node.attributes["type"] = type
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "term/ansicolor"
|
3
|
+
include Term::ANSIColor
|
4
|
+
require "strut/report"
|
5
|
+
|
6
|
+
module Strut
|
7
|
+
class ReportPrettyFormatter
|
8
|
+
def initialize(lines)
|
9
|
+
@lines = lines
|
10
|
+
end
|
11
|
+
|
12
|
+
def format(report)
|
13
|
+
begin
|
14
|
+
out = StringIO.new
|
15
|
+
$stdout = out
|
16
|
+
print_report_to_stdout(report)
|
17
|
+
ensure
|
18
|
+
$stdout = STDOUT
|
19
|
+
end
|
20
|
+
out.string
|
21
|
+
end
|
22
|
+
|
23
|
+
def print_report_to_stdout(report)
|
24
|
+
@lines.each_line.each_with_index do |line, index|
|
25
|
+
annotations = report.annotations_for_line(index+1)
|
26
|
+
print_line_with_annotations(line.chomp, annotations)
|
27
|
+
end
|
28
|
+
puts
|
29
|
+
print_errors(report)
|
30
|
+
print "#{report.number_scenarios} scenarios ("
|
31
|
+
print green { "#{report.number_passed} passed" }, ", "
|
32
|
+
print red { "#{report.number_failed} failed" }, ", "
|
33
|
+
print yellow { "#{report.number_skipped} skipped" }, ")"
|
34
|
+
end
|
35
|
+
|
36
|
+
def print_line_with_annotations(line, annotations)
|
37
|
+
if annotations.empty?
|
38
|
+
print_unannotated_line(line)
|
39
|
+
elsif
|
40
|
+
print_annotated_line(annotations, line)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def print_unannotated_line(line)
|
45
|
+
puts line
|
46
|
+
end
|
47
|
+
|
48
|
+
def print_annotated_line(annotations, line)
|
49
|
+
# TODO: print all annotations, not just the first
|
50
|
+
annotation = annotations.first
|
51
|
+
|
52
|
+
case annotation.type
|
53
|
+
when ANNOTATION_EXCEPTION
|
54
|
+
print_exception_line(line, annotation.message)
|
55
|
+
when ANNOTATION_FAIL
|
56
|
+
print_fail_line(line, annotation.message)
|
57
|
+
else
|
58
|
+
print_ok_line(line)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def print_exception_line(line, message)
|
63
|
+
print_error_line(line, :black, :on_yellow, message, :yellow, :on_black)
|
64
|
+
end
|
65
|
+
|
66
|
+
def print_fail_line(line, message)
|
67
|
+
print_error_line(line, :white, :on_red, message, :red, :on_white)
|
68
|
+
end
|
69
|
+
|
70
|
+
def print_ok_line(line)
|
71
|
+
print_line(line, :black, :on_green)
|
72
|
+
end
|
73
|
+
|
74
|
+
def print_error_line(line, line_fg, line_bg, message, message_fg, message_bg)
|
75
|
+
print_line(line, line_fg, line_bg)
|
76
|
+
print_line(message, message_fg, message_bg)
|
77
|
+
end
|
78
|
+
|
79
|
+
def print_line(line, foreground, background)
|
80
|
+
puts line.send(foreground).send(background)
|
81
|
+
end
|
82
|
+
|
83
|
+
def print_errors(report)
|
84
|
+
return if report.errors.empty?
|
85
|
+
report.errors.each do |error|
|
86
|
+
print red { error }, "\n"
|
87
|
+
end
|
88
|
+
puts
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module Strut
|
2
|
+
class ScenarioBuilder
|
3
|
+
def initialize(document_builder, command_factory)
|
4
|
+
@document_builder = document_builder
|
5
|
+
@command_factory = command_factory
|
6
|
+
end
|
7
|
+
|
8
|
+
def extract_scenarios_for_interaction(scenario_number, interaction, fixture, node)
|
9
|
+
instance = "instance-#{scenario_number}"
|
10
|
+
line = node["line"]
|
11
|
+
scenario_definitions_for_node(node).each do |scenario_stages|
|
12
|
+
make_scenario(scenario_number, line, instance, fixture, interaction, scenario_stages)
|
13
|
+
scenario_number += 1
|
14
|
+
end
|
15
|
+
scenario_number
|
16
|
+
end
|
17
|
+
|
18
|
+
def scenario_definitions_for_node(node)
|
19
|
+
raw_scenarios = node["value"]
|
20
|
+
raw_scenarios = [raw_scenarios] if raw_scenarios.respond_to?(:each_pair)
|
21
|
+
raw_scenarios
|
22
|
+
end
|
23
|
+
|
24
|
+
def make_scenario(scenario_number, line, instance, fixture, interaction, scenario_stages)
|
25
|
+
append_make_command(scenario_number, line, instance, fixture)
|
26
|
+
append_uri_command(scenario_number, line, instance, interaction.uri, scenario_stages)
|
27
|
+
append_method_command(scenario_number, line, instance, interaction.method)
|
28
|
+
append_given_commands(scenario_number, scenario_stages, instance)
|
29
|
+
append_execute_command(scenario_number, line, instance)
|
30
|
+
append_then_commands(scenario_number, scenario_stages, instance)
|
31
|
+
append_status_command(scenario_number, line, instance, interaction.statusCode)
|
32
|
+
end
|
33
|
+
|
34
|
+
def append_make_command(scenario_number, line, instance, class_name)
|
35
|
+
metadata = CommandMetadata.new(scenario_number, line)
|
36
|
+
make_command = @command_factory.make_make_command(metadata, instance, class_name)
|
37
|
+
@document_builder.append_command(make_command)
|
38
|
+
end
|
39
|
+
|
40
|
+
def append_uri_command(scenario_number, line, instance, uri, scenario_stages)
|
41
|
+
if uri
|
42
|
+
metadata = CommandMetadata.new(scenario_number, line)
|
43
|
+
combined_uri = combine_uri_with_parameters(scenario_stages, uri)
|
44
|
+
path_command = make_set_command(scenario_number, line, instance, "uri", combined_uri)
|
45
|
+
@document_builder.append_command(path_command)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def append_method_command(scenario_number, line, instance, method)
|
50
|
+
if method
|
51
|
+
metadata = CommandMetadata.new(scenario_number, line)
|
52
|
+
method_command = make_set_command(scenario_number, line, instance, "method", method)
|
53
|
+
@document_builder.append_command(method_command)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def combine_uri_with_parameters(stages, uri)
|
58
|
+
combined_uri = uri.dup
|
59
|
+
when_stages = stages_with_names(stages, ["when"])
|
60
|
+
when_stages.each do |stage|
|
61
|
+
stage.each do |k, v|
|
62
|
+
argument = v["value"].to_s
|
63
|
+
combined_uri.gsub!(/\{#{k}\}/, argument)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
combined_uri
|
67
|
+
end
|
68
|
+
|
69
|
+
def append_given_commands(scenario_number, stages, instance)
|
70
|
+
given_stages = stages_with_names(stages, ["given", "when"])
|
71
|
+
parse_stages(given_stages) { |k, v| make_given_command(scenario_number, k, v, instance) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def append_execute_command(scenario_number, line, instance)
|
75
|
+
metadata = CommandMetadata.new(scenario_number, line)
|
76
|
+
execute_command = @command_factory.make_call_command(metadata, instance, "execute")
|
77
|
+
@document_builder.append_command(execute_command)
|
78
|
+
end
|
79
|
+
|
80
|
+
def append_then_commands(scenario_number, stages, instance)
|
81
|
+
then_stages = stages_with_names(stages, ["then"])
|
82
|
+
parse_stages(then_stages) { |k, v| make_then_command(scenario_number, k, v, instance) }
|
83
|
+
end
|
84
|
+
|
85
|
+
def append_status_command(scenario_number, line, instance, statusCode)
|
86
|
+
if statusCode
|
87
|
+
metadata = CommandMetadata.new(scenario_number, line, statusCode)
|
88
|
+
status_command = @command_factory.make_call_command(metadata, instance, "statusCode")
|
89
|
+
@document_builder.append_command(status_command)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def make_given_command(scenario_number, property_name, value_container, instance)
|
94
|
+
line = value_container["line"]
|
95
|
+
value = value_container["value"]
|
96
|
+
make_set_command(scenario_number, line, instance, property_name, value)
|
97
|
+
end
|
98
|
+
|
99
|
+
def make_set_command(scenario_number, line, instance, name, value)
|
100
|
+
metadata = CommandMetadata.new(scenario_number, line)
|
101
|
+
@command_factory.make_call_command(metadata, instance, "set_#{name}", value)
|
102
|
+
end
|
103
|
+
|
104
|
+
def make_then_command(scenario_number, property_name, value_container, instance)
|
105
|
+
line = value_container["line"]
|
106
|
+
value = value_container["value"]
|
107
|
+
metadata = CommandMetadata.new(scenario_number, line, value)
|
108
|
+
@command_factory.make_call_command(metadata, instance, property_name)
|
109
|
+
end
|
110
|
+
|
111
|
+
def stages_with_names(stages, names)
|
112
|
+
names.map { |name| stages[name] }.reject { |stage| stage.nil? }.map { |stage| stage["value"] }
|
113
|
+
end
|
114
|
+
|
115
|
+
def parse_stages(stages, &block)
|
116
|
+
stages.each do |stage|
|
117
|
+
stage.each do |k, v|
|
118
|
+
command = block.call(k, v)
|
119
|
+
@document_builder.append_command(command)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|