executable-stories-ruby 0.1.0 → 0.1.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/executable_stories/minitest.rb +2 -45
- data/lib/executable_stories/output.rb +53 -0
- data/lib/executable_stories/rspec.rb +183 -0
- data/lib/executable_stories/story.rb +6 -2
- data/lib/executable_stories.rb +1 -0
- metadata +20 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c19580fd13bac9c4c5921b3302f661b5bfef2d7f17545078347c11b6535011f2
|
|
4
|
+
data.tar.gz: eec88761630229d37e79df73f2c75c55b14bb37f44a16776fe852eb75134ea04
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cbe643054ac61137d863ad19906912c751e581758fc0370501a1c760f5ee9e08d3a59e0ab443f466e600b35bfbd30cd3be4e08f26c817384f9bd297d50844062
|
|
7
|
+
data.tar.gz: a25c532f7548a8907db9e454e91cf30d7a604d595a9763a0ff401ae8a17cee40ba0c0be3162f1fd187a97b4c08b6c8c7a3b972177dafbf07500cb18029336d74
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
require "fileutils"
|
|
5
3
|
require "minitest"
|
|
6
4
|
require_relative "story"
|
|
7
5
|
require_relative "collector"
|
|
8
|
-
require_relative "
|
|
9
|
-
require_relative "types"
|
|
6
|
+
require_relative "output"
|
|
10
7
|
|
|
11
8
|
module ExecutableStories
|
|
12
9
|
module MinitestPlugin
|
|
@@ -23,47 +20,7 @@ module ExecutableStories
|
|
|
23
20
|
end
|
|
24
21
|
|
|
25
22
|
def write_results(output_path: nil)
|
|
26
|
-
cases
|
|
27
|
-
return if cases.empty?
|
|
28
|
-
|
|
29
|
-
output_path ||= ENV.fetch("EXECUTABLE_STORIES_OUTPUT", ".executable-stories/raw-run.json")
|
|
30
|
-
|
|
31
|
-
started = cases.map(&:start_time).compact.min
|
|
32
|
-
finished = cases.map(&:end_time).compact.max
|
|
33
|
-
|
|
34
|
-
run = RawRun.new(
|
|
35
|
-
schema_version: 1,
|
|
36
|
-
test_cases: cases,
|
|
37
|
-
project_root: Dir.pwd,
|
|
38
|
-
started_at_ms: started,
|
|
39
|
-
finished_at_ms: finished,
|
|
40
|
-
package_version: nil,
|
|
41
|
-
git_sha: nil,
|
|
42
|
-
ci: detect_ci
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
JsonWriter.write_raw_run(run, output_path)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def detect_ci
|
|
49
|
-
if ENV["GITHUB_ACTIONS"] == "true"
|
|
50
|
-
url = nil
|
|
51
|
-
server = ENV["GITHUB_SERVER_URL"]
|
|
52
|
-
repo = ENV["GITHUB_REPOSITORY"]
|
|
53
|
-
run_id = ENV["GITHUB_RUN_ID"]
|
|
54
|
-
url = "#{server}/#{repo}/actions/runs/#{run_id}" if server && repo && run_id
|
|
55
|
-
RawCIInfo.new(name: "github", url: url, build_number: ENV["GITHUB_RUN_NUMBER"])
|
|
56
|
-
elsif ENV["CIRCLECI"] == "true"
|
|
57
|
-
RawCIInfo.new(name: "circleci", url: ENV["CIRCLE_BUILD_URL"], build_number: ENV["CIRCLE_BUILD_NUM"])
|
|
58
|
-
elsif !ENV["JENKINS_URL"].nil?
|
|
59
|
-
RawCIInfo.new(name: "jenkins", url: ENV["BUILD_URL"], build_number: ENV["BUILD_NUMBER"])
|
|
60
|
-
elsif ENV["TRAVIS"] == "true"
|
|
61
|
-
RawCIInfo.new(name: "travis", url: ENV["TRAVIS_BUILD_WEB_URL"], build_number: ENV["TRAVIS_BUILD_NUMBER"])
|
|
62
|
-
elsif ENV["GITLAB_CI"] == "true"
|
|
63
|
-
RawCIInfo.new(name: "gitlab", url: ENV["CI_PIPELINE_URL"], build_number: ENV["CI_PIPELINE_IID"])
|
|
64
|
-
elsif ENV["CI"] == "true"
|
|
65
|
-
RawCIInfo.new(name: "ci")
|
|
66
|
-
end
|
|
23
|
+
Output.write_results(cases: Collector.all, output_path: output_path)
|
|
67
24
|
end
|
|
68
25
|
end
|
|
69
26
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "types"
|
|
4
|
+
require_relative "json_writer"
|
|
5
|
+
|
|
6
|
+
module ExecutableStories
|
|
7
|
+
module Output
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def write_results(cases:, output_path: nil)
|
|
11
|
+
return if cases.empty?
|
|
12
|
+
|
|
13
|
+
output_path ||= ENV.fetch("EXECUTABLE_STORIES_OUTPUT", ".executable-stories/raw-run.json")
|
|
14
|
+
|
|
15
|
+
started = cases.map(&:start_time).compact.min
|
|
16
|
+
finished = cases.map(&:end_time).compact.max
|
|
17
|
+
|
|
18
|
+
run = RawRun.new(
|
|
19
|
+
schema_version: 1,
|
|
20
|
+
test_cases: cases,
|
|
21
|
+
project_root: Dir.pwd,
|
|
22
|
+
started_at_ms: started,
|
|
23
|
+
finished_at_ms: finished,
|
|
24
|
+
package_version: nil,
|
|
25
|
+
git_sha: nil,
|
|
26
|
+
ci: detect_ci
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
JsonWriter.write_raw_run(run, output_path)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def detect_ci
|
|
33
|
+
if ENV["GITHUB_ACTIONS"] == "true"
|
|
34
|
+
url = nil
|
|
35
|
+
server = ENV["GITHUB_SERVER_URL"]
|
|
36
|
+
repo = ENV["GITHUB_REPOSITORY"]
|
|
37
|
+
run_id = ENV["GITHUB_RUN_ID"]
|
|
38
|
+
url = "#{server}/#{repo}/actions/runs/#{run_id}" if server && repo && run_id
|
|
39
|
+
RawCIInfo.new(name: "github", url: url, build_number: ENV["GITHUB_RUN_NUMBER"])
|
|
40
|
+
elsif ENV["CIRCLECI"] == "true"
|
|
41
|
+
RawCIInfo.new(name: "circleci", url: ENV["CIRCLE_BUILD_URL"], build_number: ENV["CIRCLE_BUILD_NUM"])
|
|
42
|
+
elsif !ENV["JENKINS_URL"].nil?
|
|
43
|
+
RawCIInfo.new(name: "jenkins", url: ENV["BUILD_URL"], build_number: ENV["BUILD_NUMBER"])
|
|
44
|
+
elsif ENV["TRAVIS"] == "true"
|
|
45
|
+
RawCIInfo.new(name: "travis", url: ENV["TRAVIS_BUILD_WEB_URL"], build_number: ENV["TRAVIS_BUILD_NUMBER"])
|
|
46
|
+
elsif ENV["GITLAB_CI"] == "true"
|
|
47
|
+
RawCIInfo.new(name: "gitlab", url: ENV["CI_PIPELINE_URL"], build_number: ENV["CI_PIPELINE_IID"])
|
|
48
|
+
elsif ENV["CI"] == "true"
|
|
49
|
+
RawCIInfo.new(name: "ci")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rspec/core"
|
|
4
|
+
|
|
5
|
+
require_relative "story"
|
|
6
|
+
require_relative "collector"
|
|
7
|
+
require_relative "output"
|
|
8
|
+
|
|
9
|
+
module ExecutableStories
|
|
10
|
+
module RSpecStoryDSL
|
|
11
|
+
def story(scenario, tags: nil, ticket: nil, meta: nil, trace_url_template: nil, **example_metadata, &block)
|
|
12
|
+
it(scenario, **example_metadata, executable_stories: {
|
|
13
|
+
scenario: scenario,
|
|
14
|
+
tags: tags,
|
|
15
|
+
ticket: ticket,
|
|
16
|
+
meta: meta,
|
|
17
|
+
trace_url_template: trace_url_template
|
|
18
|
+
}) do
|
|
19
|
+
story = ExecutableStories.init(
|
|
20
|
+
scenario,
|
|
21
|
+
tags: tags,
|
|
22
|
+
ticket: ticket,
|
|
23
|
+
meta: meta,
|
|
24
|
+
trace_url_template: trace_url_template
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
story.meta ||= {}
|
|
28
|
+
story.meta["rspec"] = {
|
|
29
|
+
"fullDescription" => RSpec.current_example.full_description,
|
|
30
|
+
"description" => RSpec.current_example.description,
|
|
31
|
+
"filePath" => RSpec.current_example.metadata[:file_path],
|
|
32
|
+
"lineNumber" => RSpec.current_example.metadata[:line_number],
|
|
33
|
+
"scopedId" => RSpec.current_example.metadata[:scoped_id]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
RSpec.current_example.metadata[:executable_stories_story] = story
|
|
37
|
+
instance_exec(story, &block) if block
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class RSpecFormatter
|
|
43
|
+
RSpec::Core::Formatters.register self, :start, :example_passed, :example_failed, :example_pending, :dump_summary
|
|
44
|
+
|
|
45
|
+
def initialize(output)
|
|
46
|
+
@output = output
|
|
47
|
+
@started = false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def start(_notification)
|
|
51
|
+
return if @started
|
|
52
|
+
|
|
53
|
+
Collector.reset
|
|
54
|
+
@started = true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def example_passed(notification)
|
|
58
|
+
record(notification.example, status: "pass")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def example_failed(notification)
|
|
62
|
+
record(notification.example, status: "fail")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def example_pending(notification)
|
|
66
|
+
example = notification.example
|
|
67
|
+
status = example.metadata[:skip] ? "skip" : "pending"
|
|
68
|
+
record(example, status: status)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def dump_summary(_summary)
|
|
72
|
+
Output.write_results(cases: Collector.all)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def record(example, status:)
|
|
78
|
+
story = build_story(example)
|
|
79
|
+
story.record(
|
|
80
|
+
status: status,
|
|
81
|
+
title: example.description,
|
|
82
|
+
suite_path: suite_path_for(example),
|
|
83
|
+
source_file: example.metadata[:file_path],
|
|
84
|
+
source_line: example.metadata[:line_number],
|
|
85
|
+
error: error_for(example, status)
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def build_story(example)
|
|
90
|
+
runtime_story = example.metadata[:executable_stories_story]
|
|
91
|
+
return runtime_story if runtime_story
|
|
92
|
+
|
|
93
|
+
definition = example.metadata[:executable_stories] || {}
|
|
94
|
+
scenario = definition[:scenario] || example.description
|
|
95
|
+
tags = normalize_tags(definition[:tags], example.metadata)
|
|
96
|
+
|
|
97
|
+
story = ExecutableStories.init(
|
|
98
|
+
scenario,
|
|
99
|
+
tags: tags.empty? ? nil : tags,
|
|
100
|
+
ticket: definition[:ticket],
|
|
101
|
+
meta: definition[:meta],
|
|
102
|
+
trace_url_template: definition[:trace_url_template]
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
story.meta ||= {}
|
|
106
|
+
story.meta["rspec"] = {
|
|
107
|
+
"fullDescription" => example.full_description,
|
|
108
|
+
"description" => example.description,
|
|
109
|
+
"filePath" => example.metadata[:file_path],
|
|
110
|
+
"lineNumber" => example.metadata[:line_number],
|
|
111
|
+
"scopedId" => example.metadata[:scoped_id]
|
|
112
|
+
}
|
|
113
|
+
story
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def normalize_tags(explicit_tags, metadata)
|
|
117
|
+
tags = Array(explicit_tags).compact.map(&:to_s)
|
|
118
|
+
metadata.each do |key, value|
|
|
119
|
+
next unless value == true
|
|
120
|
+
next if reserved_metadata_key?(key)
|
|
121
|
+
|
|
122
|
+
tags << key.to_s
|
|
123
|
+
end
|
|
124
|
+
tags.uniq
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def reserved_metadata_key?(key)
|
|
128
|
+
%i[
|
|
129
|
+
executable_stories
|
|
130
|
+
executable_stories_story
|
|
131
|
+
description
|
|
132
|
+
description_args
|
|
133
|
+
block
|
|
134
|
+
full_description
|
|
135
|
+
file_path
|
|
136
|
+
line_number
|
|
137
|
+
location
|
|
138
|
+
absolute_file_path
|
|
139
|
+
rerun_file_path
|
|
140
|
+
rerun_line_numbers
|
|
141
|
+
scoped_id
|
|
142
|
+
execution_result
|
|
143
|
+
aggregate_failures
|
|
144
|
+
skip
|
|
145
|
+
].include?(key)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def suite_path_for(example)
|
|
149
|
+
example.example_group.parent_groups
|
|
150
|
+
.reverse
|
|
151
|
+
.reject { |group| group == RSpec::Core::ExampleGroup }
|
|
152
|
+
.map(&:description)
|
|
153
|
+
.reject { |description| description.nil? || description.empty? }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def error_for(example, status)
|
|
157
|
+
return nil unless status == "fail"
|
|
158
|
+
|
|
159
|
+
exception = example.exception
|
|
160
|
+
return nil unless exception
|
|
161
|
+
|
|
162
|
+
ExecutableStories::RawError.new(
|
|
163
|
+
message: exception.message,
|
|
164
|
+
stack: Array(exception.backtrace).join("\n")
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
module RSpecPlugin
|
|
170
|
+
module_function
|
|
171
|
+
|
|
172
|
+
def install!
|
|
173
|
+
return if @installed
|
|
174
|
+
|
|
175
|
+
RSpec.configure do |config|
|
|
176
|
+
config.extend ExecutableStories::RSpecStoryDSL
|
|
177
|
+
config.add_formatter ExecutableStories::RSpecFormatter
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
@installed = true
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -11,7 +11,7 @@ module ExecutableStories
|
|
|
11
11
|
attr_accessor :scenario, :steps, :tags, :tickets, :meta, :docs,
|
|
12
12
|
:current_step, :seen_primary, :start_time, :end_time,
|
|
13
13
|
:source_order, :step_counter, :attachments, :active_timers,
|
|
14
|
-
:timer_counter, :otel_spans, :trace_url_template
|
|
14
|
+
:timer_counter, :otel_spans, :trace_url_template, :suite_path
|
|
15
15
|
|
|
16
16
|
def initialize(scenario, tags: nil, ticket: nil, meta: nil, trace_url_template: nil)
|
|
17
17
|
@scenario = scenario
|
|
@@ -30,6 +30,7 @@ module ExecutableStories
|
|
|
30
30
|
@timer_counter = 0
|
|
31
31
|
@otel_spans = nil
|
|
32
32
|
@trace_url_template = trace_url_template
|
|
33
|
+
@suite_path = nil
|
|
33
34
|
|
|
34
35
|
bridge_otel
|
|
35
36
|
end
|
|
@@ -260,14 +261,16 @@ module ExecutableStories
|
|
|
260
261
|
tags: @tags,
|
|
261
262
|
tickets: @tickets,
|
|
262
263
|
meta: @meta,
|
|
264
|
+
suite_path: @suite_path,
|
|
263
265
|
docs: @docs.empty? ? nil : @docs,
|
|
264
266
|
source_order: @source_order,
|
|
265
267
|
otel_spans: @otel_spans
|
|
266
268
|
)
|
|
267
269
|
end
|
|
268
270
|
|
|
269
|
-
def record(status:, title: nil, suite_path: nil, source_file: nil, duration_ms: nil, error: nil)
|
|
271
|
+
def record(status:, title: nil, suite_path: nil, source_file: nil, source_line: nil, duration_ms: nil, error: nil)
|
|
270
272
|
@end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000.0
|
|
273
|
+
@suite_path = suite_path if suite_path
|
|
271
274
|
duration = duration_ms || (@end_time - @start_time)
|
|
272
275
|
|
|
273
276
|
step_events = @steps.each_with_index.map do |s, i|
|
|
@@ -280,6 +283,7 @@ module ExecutableStories
|
|
|
280
283
|
title_path: suite_path ? suite_path + [@scenario] : [@scenario],
|
|
281
284
|
story: get_meta,
|
|
282
285
|
source_file: source_file,
|
|
286
|
+
source_line: source_line,
|
|
283
287
|
duration_ms: duration,
|
|
284
288
|
error: error,
|
|
285
289
|
retry: 0,
|
data/lib/executable_stories.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "executable_stories/doc_entry"
|
|
|
5
5
|
require_relative "executable_stories/types"
|
|
6
6
|
require_relative "executable_stories/collector"
|
|
7
7
|
require_relative "executable_stories/json_writer"
|
|
8
|
+
require_relative "executable_stories/output"
|
|
8
9
|
|
|
9
10
|
module ExecutableStories
|
|
10
11
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: executable-stories-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jag Reehal
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest
|
|
@@ -24,6 +24,20 @@ dependencies:
|
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '5.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rspec
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.13'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.13'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: rake
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -65,6 +79,8 @@ files:
|
|
|
65
79
|
- lib/executable_stories/doc_entry.rb
|
|
66
80
|
- lib/executable_stories/json_writer.rb
|
|
67
81
|
- lib/executable_stories/minitest.rb
|
|
82
|
+
- lib/executable_stories/output.rb
|
|
83
|
+
- lib/executable_stories/rspec.rb
|
|
68
84
|
- lib/executable_stories/story.rb
|
|
69
85
|
- lib/executable_stories/types.rb
|
|
70
86
|
homepage: https://github.com/jagreehal/executable-stories
|
|
@@ -92,5 +108,6 @@ requirements: []
|
|
|
92
108
|
rubygems_version: 3.5.3
|
|
93
109
|
signing_key:
|
|
94
110
|
specification_version: 4
|
|
95
|
-
summary: Ruby-first story/given/when/then helpers for Minitest with doc
|
|
111
|
+
summary: Ruby-first story/given/when/then helpers for Minitest and RSpec with doc
|
|
112
|
+
generation.
|
|
96
113
|
test_files: []
|