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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca5f49335f6bb83179d6c6d3710fa24fc7bc03250dfe0af5722a301e439485ff
4
- data.tar.gz: 5e2fee0c6f9ab67787d7f89af3fec3a5e9074a805fbbc577884eba6fc805404a
3
+ metadata.gz: c19580fd13bac9c4c5921b3302f661b5bfef2d7f17545078347c11b6535011f2
4
+ data.tar.gz: eec88761630229d37e79df73f2c75c55b14bb37f44a16776fe852eb75134ea04
5
5
  SHA512:
6
- metadata.gz: 4df5210a06292fe29466f57fb91887450249c6562b239489d90d0ff2259d5581c922782b75d9e66ac0882c9360fb61cb3e4f0ea56940fd7658967b5954a25c27
7
- data.tar.gz: ca7e21e81ae8999d8b1117b5fc91706bb85ee9706ff52a6410e878b651d2052fcb32b30c1e1a473c894bd333c57a06ea35f49ca7a614acb555c6ec409595ba02
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 "json_writer"
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 = Collector.all
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,
@@ -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.0
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 00:00:00.000000000 Z
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 generation.
111
+ summary: Ruby-first story/given/when/then helpers for Minitest and RSpec with doc
112
+ generation.
96
113
  test_files: []