kensho-cucumber-ruby 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 +7 -0
- data/README.md +117 -0
- data/lib/kensho/_schema.rb +211 -0
- data/lib/kensho/cucumber/formatter.rb +482 -0
- data/lib/kensho/cucumber/version.rb +7 -0
- data/lib/kensho/cucumber.rb +16 -0
- metadata +69 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b0c834a2f3e16b91817b5ff5a0f3501db6a4bce89b6be298daa6c276cc1a3c29
|
|
4
|
+
data.tar.gz: 1de5746a0de6e3df020b69d13b34ec7d40c9d428afe1d4ec5e5f0ad09b9bbcd1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 24e3668821a7b5c15a5cd615c1ff0ccc1c0a4ccdc7ef1354c15cca658a8778b4160b7ca9490cd80f591984dcfd26c4a3e005e46c85429866fcc6f559257ec47d
|
|
7
|
+
data.tar.gz: eb708f88ed8a8764587979513f8162af62ee9ecd7ed77c71f3e305d00c87d1cc2356872eb249b32e2fdc3aa2fb8440edfc86f7a2f8125883b79c42a2a90cbf69
|
data/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# kensho-cucumber-ruby
|
|
2
|
+
|
|
3
|
+
A Cucumber 7+ formatter that emits the canonical [Kensho v1](../schema)
|
|
4
|
+
JSON format. Run your suite, point the `kensho` CLI at `kensho-results/`,
|
|
5
|
+
and get a self-contained static HTML report.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# Gemfile
|
|
11
|
+
gem 'cucumber'
|
|
12
|
+
gem 'kensho-cucumber-ruby', require: false, group: :test
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Run
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bundle exec cucumber --require kensho/cucumber --format Kensho::Cucumber::Formatter
|
|
23
|
+
|
|
24
|
+
# render the HTML report (uses the JS CLI from the same monorepo):
|
|
25
|
+
npx kensho generate
|
|
26
|
+
npx kensho open
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
You can pin the formatter from `cucumber.yml`:
|
|
30
|
+
|
|
31
|
+
```yaml
|
|
32
|
+
default: --require kensho/cucumber --format Kensho::Cucumber::Formatter --format pretty
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## What it produces
|
|
36
|
+
|
|
37
|
+
- `kensho-results/run.json` — run manifest (project, env, totals, timing).
|
|
38
|
+
- `kensho-results/cases/<stableId>.json` — one file per scenario.
|
|
39
|
+
- `kensho-results/attachments/<caseId>/...` — anything `attach`ed in a step.
|
|
40
|
+
|
|
41
|
+
Each case gets a **stable id** (`tc_<16 hex>`) hashed from
|
|
42
|
+
`feature › scenario` + file path so test history correlates across runs
|
|
43
|
+
and across adapters (the JS adapters use the same FNV-1a-based hash).
|
|
44
|
+
|
|
45
|
+
## Mapping
|
|
46
|
+
|
|
47
|
+
| Gherkin | Kensho field |
|
|
48
|
+
| ----------------------------------------- | ----------------------------------------- |
|
|
49
|
+
| Feature | `case.behavior.feature` and `case.suite[0]` |
|
|
50
|
+
| Rule (parent of the scenario) | `case.behavior.epic` |
|
|
51
|
+
| Scenario name | `case.name` and `case.behavior.scenario` |
|
|
52
|
+
| Each Given/When/Then | one entry in `case.steps[]` (`title` includes the keyword) |
|
|
53
|
+
| Data table on a step | `step.parameters[]` (kind: data-row) |
|
|
54
|
+
| Doc string on a step | `step.parameters[]` (kind: argument) |
|
|
55
|
+
|
|
56
|
+
## Tag conventions
|
|
57
|
+
|
|
58
|
+
| Tag | Effect |
|
|
59
|
+
| -------------------------------------- | ------------------------------------------------------ |
|
|
60
|
+
| `@critical`, `@blocker`, `@normal`, `@minor`, `@trivial` | Sets `case.severity`. |
|
|
61
|
+
| `@severity:critical` | Same as above, explicit form. |
|
|
62
|
+
| `@kensho.label.<key>=<value>` | Adds `case.labels.<key> = '<value>'`. |
|
|
63
|
+
| `@kensho.link.<kind>=<label>` | Adds `case.links += { kind, label, url: label }`. Use for ticket IDs. |
|
|
64
|
+
| `@kensho.url.<kind>=<https://…>` | Adds `case.links += { kind, url }`. Use for full URLs. |
|
|
65
|
+
| any other `@tag` | Becomes a tag on `case.tags`. |
|
|
66
|
+
|
|
67
|
+
```gherkin
|
|
68
|
+
@critical @smoke @kensho.label.team=growth @kensho.link.jira=PROJ-123
|
|
69
|
+
Scenario: User can log in
|
|
70
|
+
Given a registered user
|
|
71
|
+
When they submit valid credentials
|
|
72
|
+
Then they land on the home page
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Status mapping
|
|
76
|
+
|
|
77
|
+
| Cucumber outcome | Kensho status |
|
|
78
|
+
| ----------------------------- | ------------- |
|
|
79
|
+
| passed | `pass` |
|
|
80
|
+
| failed | `fail` |
|
|
81
|
+
| pending | `skip` |
|
|
82
|
+
| skipped | `skip` |
|
|
83
|
+
| undefined / ambiguous | `broken` |
|
|
84
|
+
|
|
85
|
+
## Environment auto-detected
|
|
86
|
+
|
|
87
|
+
GitHub Actions, CircleCI, GitLab CI, Jenkins, Buildkite, Azure DevOps —
|
|
88
|
+
CI provider, branch, commit, run URL, OS, architecture, Ruby version.
|
|
89
|
+
|
|
90
|
+
Pass `KR_AUTHOR`, `KR_COMMIT_MSG`, `KR_STAGE`, `KR_BASE_URL`,
|
|
91
|
+
`KR_APP_VERSION`, `KR_BUILD_NUMBER`, `KR_RELEASE`, `KR_REGION`,
|
|
92
|
+
`KR_LOCALE`, `KR_TRIGGER`, or `KR_FEATURE` as env vars to populate the
|
|
93
|
+
matching fields on `run.env`.
|
|
94
|
+
|
|
95
|
+
## CLI / env flags
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
KENSHO_OUTPUT output dir (default ./kensho-results)
|
|
99
|
+
KENSHO_PROJECT_NAME project name in run.json
|
|
100
|
+
KENSHO_PROJECT_SLUG project slug
|
|
101
|
+
KENSHO_RUN_ID override the auto-generated run id
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Design notes
|
|
105
|
+
|
|
106
|
+
- Zero runtime dependencies beyond `cucumber` itself. The schema lives
|
|
107
|
+
in the JS workspace; we vendor the minimum (id-hashing, env capture)
|
|
108
|
+
inline so the gem installs in seconds.
|
|
109
|
+
- The formatter never raises — broken adapters must not break a test
|
|
110
|
+
run.
|
|
111
|
+
- Stable IDs match the JS `stableCaseId` byte-for-byte, so
|
|
112
|
+
`cucumber-ruby` and `cucumber-js` runs of the same `.feature` file
|
|
113
|
+
roll up to the same history on the platform.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
Apache-2.0.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Vendored slice of the Kensho v1 schema contract.
|
|
4
|
+
#
|
|
5
|
+
# We deliberately do not depend on the JS @kaizenreport/kensho-schema package.
|
|
6
|
+
# Adapters need to stay tiny and standalone, so we re-implement the few things
|
|
7
|
+
# the formatter cares about: the stable case-id hash, the status enum, the
|
|
8
|
+
# attachment kind/MIME tables, and the environment-capture function.
|
|
9
|
+
|
|
10
|
+
require 'rbconfig'
|
|
11
|
+
|
|
12
|
+
module Kensho
|
|
13
|
+
module Schema
|
|
14
|
+
SCHEMA_VERSION = 'kensho/v1'
|
|
15
|
+
|
|
16
|
+
STATUS = %w[pass fail broken skip].freeze
|
|
17
|
+
STEP_STATUS = %w[pass fail skip].freeze
|
|
18
|
+
SEVERITY = %w[blocker critical normal minor trivial].freeze
|
|
19
|
+
ATTACHMENT_KINDS = %w[
|
|
20
|
+
screenshot video trace har text json html dom-snapshot log
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
MIME_BY_EXT = {
|
|
24
|
+
'.png' => 'image/png',
|
|
25
|
+
'.jpg' => 'image/jpeg',
|
|
26
|
+
'.jpeg' => 'image/jpeg',
|
|
27
|
+
'.webp' => 'image/webp',
|
|
28
|
+
'.webm' => 'video/webm',
|
|
29
|
+
'.mp4' => 'video/mp4',
|
|
30
|
+
'.zip' => 'application/zip',
|
|
31
|
+
'.html' => 'text/html',
|
|
32
|
+
'.json' => 'application/json',
|
|
33
|
+
'.txt' => 'text/plain',
|
|
34
|
+
'.log' => 'text/plain',
|
|
35
|
+
'.har' => 'application/json'
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
KIND_BY_EXT = {
|
|
39
|
+
'.png' => 'screenshot',
|
|
40
|
+
'.jpg' => 'screenshot',
|
|
41
|
+
'.jpeg' => 'screenshot',
|
|
42
|
+
'.webp' => 'screenshot',
|
|
43
|
+
'.webm' => 'video',
|
|
44
|
+
'.mp4' => 'video',
|
|
45
|
+
'.zip' => 'trace',
|
|
46
|
+
'.html' => 'html',
|
|
47
|
+
'.json' => 'json',
|
|
48
|
+
'.txt' => 'text',
|
|
49
|
+
'.log' => 'log',
|
|
50
|
+
'.har' => 'har'
|
|
51
|
+
}.freeze
|
|
52
|
+
|
|
53
|
+
FNV_OFFSET_1 = 0x811c9dc5
|
|
54
|
+
FNV_OFFSET_2 = 0x01000193
|
|
55
|
+
FNV_PRIME_1 = 0x01000193
|
|
56
|
+
FNV_PRIME_2 = 0x85ebca6b
|
|
57
|
+
MASK32 = 0xffffffff
|
|
58
|
+
|
|
59
|
+
# Mirrors stableCaseId from packages/schema/index.js byte-for-byte:
|
|
60
|
+
# double FNV-1a with two different secondary primes so the two 32-bit
|
|
61
|
+
# chunks come from independent rolling states. Must stay byte-compatible
|
|
62
|
+
# with the JS implementation or test history won't line up across
|
|
63
|
+
# adapters.
|
|
64
|
+
def self.stable_case_id(full_name, file_path)
|
|
65
|
+
s = "#{full_name || ''}::#{file_path || ''}"
|
|
66
|
+
h1 = FNV_OFFSET_1
|
|
67
|
+
h2 = FNV_OFFSET_2
|
|
68
|
+
s.each_codepoint do |c|
|
|
69
|
+
h1 = ((h1 ^ c) * FNV_PRIME_1) & MASK32
|
|
70
|
+
h2 = ((h2 ^ c) * FNV_PRIME_2) & MASK32
|
|
71
|
+
end
|
|
72
|
+
format('tc_%08x%08x', h1, h2)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.kind_and_mime_for(path)
|
|
76
|
+
ext = File.extname(path.to_s).downcase
|
|
77
|
+
[KIND_BY_EXT[ext] || 'text', MIME_BY_EXT[ext] || 'application/octet-stream']
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.normalize_os
|
|
81
|
+
host = RbConfig::CONFIG['host_os'].to_s.downcase
|
|
82
|
+
return 'linux' if host.include?('linux')
|
|
83
|
+
return 'darwin' if host.include?('darwin')
|
|
84
|
+
return 'win32' if host.include?('mswin') || host.include?('mingw') || host.include?('cygwin')
|
|
85
|
+
|
|
86
|
+
host
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Normalize SSH-style git URLs to https. ``git@github.com:foo/bar.git``
|
|
90
|
+
# becomes ``https://github.com/foo/bar``; trailing ``.git`` is dropped.
|
|
91
|
+
def self.normalize_git_url(u)
|
|
92
|
+
return nil if u.nil? || u.empty?
|
|
93
|
+
|
|
94
|
+
m = u.match(%r{^(?:ssh://)?git@([^:/]+)[:/](.+?)(?:\.git)?$})
|
|
95
|
+
return "https://#{m[1]}/#{m[2]}" if m
|
|
96
|
+
|
|
97
|
+
u.sub(/\.git\z/, '')
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# CI / environment metadata for run.env. Matches the helpers in the JS
|
|
101
|
+
# adapters so a Kensho report looks the same regardless of language.
|
|
102
|
+
def self.env_info(framework_version: nil)
|
|
103
|
+
ci_env = ENV['CI']
|
|
104
|
+
ci =
|
|
105
|
+
if ci_env && ENV['GITHUB_ACTIONS']
|
|
106
|
+
'github-actions'
|
|
107
|
+
elsif ci_env && ENV['CIRCLECI']
|
|
108
|
+
'circleci'
|
|
109
|
+
elsif ci_env && ENV['GITLAB_CI']
|
|
110
|
+
'gitlab'
|
|
111
|
+
elsif ci_env && ENV['JENKINS_URL']
|
|
112
|
+
'jenkins'
|
|
113
|
+
elsif ci_env && ENV['BUILDKITE']
|
|
114
|
+
'buildkite'
|
|
115
|
+
elsif ci_env && ENV['TF_BUILD']
|
|
116
|
+
'azure-devops'
|
|
117
|
+
elsif ci_env
|
|
118
|
+
'unknown'
|
|
119
|
+
else
|
|
120
|
+
'local'
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
branch = ENV['GITHUB_REF_NAME'] || ENV['CIRCLE_BRANCH'] ||
|
|
124
|
+
ENV['CI_COMMIT_REF_NAME'] || ENV['BUILDKITE_BRANCH']
|
|
125
|
+
commit = ENV['GITHUB_SHA'] || ENV['CIRCLE_SHA1'] ||
|
|
126
|
+
ENV['CI_COMMIT_SHA'] || ENV['BUILDKITE_COMMIT']
|
|
127
|
+
author = ENV['KR_AUTHOR'] || ENV['GITHUB_ACTOR']
|
|
128
|
+
commit_msg = ENV['KR_COMMIT_MSG']
|
|
129
|
+
|
|
130
|
+
run_url = ENV['KR_RUN_URL'] ||
|
|
131
|
+
if ENV['GITHUB_SERVER_URL'] && ENV['GITHUB_REPOSITORY'] && ENV['GITHUB_RUN_ID']
|
|
132
|
+
"#{ENV['GITHUB_SERVER_URL']}/#{ENV['GITHUB_REPOSITORY']}/actions/runs/#{ENV['GITHUB_RUN_ID']}"
|
|
133
|
+
elsif ENV['CIRCLE_BUILD_URL']
|
|
134
|
+
ENV['CIRCLE_BUILD_URL']
|
|
135
|
+
elsif ENV['CI_JOB_URL']
|
|
136
|
+
ENV['CI_JOB_URL']
|
|
137
|
+
elsif ENV['BUILD_URL']
|
|
138
|
+
ENV['BUILD_URL']
|
|
139
|
+
elsif ENV['BUILDKITE_BUILD_URL']
|
|
140
|
+
ENV['BUILDKITE_BUILD_URL']
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# repoUrl — KR_REPO_URL override → GitHub Actions / GitLab / Bitbucket /
|
|
144
|
+
# Azure / SSH-style URLs from CircleCI / Buildkite / Jenkins (normalized).
|
|
145
|
+
gh_server = ENV['GITHUB_SERVER_URL']
|
|
146
|
+
gh_repo = ENV['GITHUB_REPOSITORY']
|
|
147
|
+
repo_url = ENV['KR_REPO_URL'] ||
|
|
148
|
+
(gh_server && gh_repo ? "#{gh_server}/#{gh_repo}" : nil) ||
|
|
149
|
+
ENV['CI_PROJECT_URL'] ||
|
|
150
|
+
ENV['BITBUCKET_GIT_HTTP_ORIGIN'] ||
|
|
151
|
+
normalize_git_url(ENV['BUILD_REPOSITORY_URI']) ||
|
|
152
|
+
normalize_git_url(ENV['CIRCLE_REPOSITORY_URL'] ||
|
|
153
|
+
ENV['BUILDKITE_REPO'] ||
|
|
154
|
+
ENV['GIT_URL'])
|
|
155
|
+
|
|
156
|
+
info = {
|
|
157
|
+
'ci' => ci,
|
|
158
|
+
'os' => normalize_os,
|
|
159
|
+
'arch' => RbConfig::CONFIG['host_cpu'].to_s
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
ruby_version = "#{RUBY_VERSION}p#{defined?(RUBY_PATCHLEVEL) ? RUBY_PATCHLEVEL : '0'}"
|
|
163
|
+
info['vars'] = { 'rubyVersion' => ruby_version }
|
|
164
|
+
info['vars']['frameworkVersion'] = framework_version if framework_version
|
|
165
|
+
|
|
166
|
+
info['branch'] = branch if branch
|
|
167
|
+
info['commit'] = commit if commit
|
|
168
|
+
info['commitMsg'] = commit_msg if commit_msg
|
|
169
|
+
info['author'] = author if author
|
|
170
|
+
info['runUrl'] = run_url if run_url
|
|
171
|
+
info['repoUrl'] = repo_url if repo_url
|
|
172
|
+
os_version = RbConfig::CONFIG['host_os']
|
|
173
|
+
info['osVersion'] = os_version if os_version
|
|
174
|
+
|
|
175
|
+
[
|
|
176
|
+
['KR_STAGE', 'stage'],
|
|
177
|
+
['KR_BASE_URL', 'baseUrl'],
|
|
178
|
+
['KR_APP_VERSION', 'appVersion'],
|
|
179
|
+
['KR_BUILD_NUMBER', 'buildNumber'],
|
|
180
|
+
['KR_RELEASE', 'release'],
|
|
181
|
+
['KR_REGION', 'region'],
|
|
182
|
+
['KR_LOCALE', 'locale'],
|
|
183
|
+
['KR_TRIGGER', 'trigger'],
|
|
184
|
+
['KR_FEATURE', 'feature']
|
|
185
|
+
].each do |env_var, key|
|
|
186
|
+
v = ENV[env_var]
|
|
187
|
+
info[key] = v if v && !v.empty?
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
info
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def self.iso_now
|
|
194
|
+
Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def self.iso_from_seconds(secs)
|
|
198
|
+
Time.at(secs).utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def self.slugify(name)
|
|
202
|
+
s = name.to_s.downcase.strip
|
|
203
|
+
s = s.gsub(/[^a-z0-9_-]+/, '-').gsub(/\A-+|-+\z/, '')
|
|
204
|
+
s.empty? ? 'unknown' : s
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def self.default_run_id
|
|
208
|
+
"run_#{Time.now.utc.strftime('%Y%m%d%H%M%S')}"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
require_relative '../_schema'
|
|
7
|
+
|
|
8
|
+
# Cucumber 7+ formatter that emits Kensho v1 results.
|
|
9
|
+
#
|
|
10
|
+
# Cucumber-Ruby's formatter API gives us a `Cucumber::Configuration` and a
|
|
11
|
+
# `config.on_event(:event_name)` event bus. We subscribe to:
|
|
12
|
+
#
|
|
13
|
+
# :test_run_started — open the output dir
|
|
14
|
+
# :test_case_started — start accumulating per-scenario data
|
|
15
|
+
# :test_step_finished — append a step result
|
|
16
|
+
# :test_case_finished — write cases/<id>.json
|
|
17
|
+
# :test_run_finished — write run.json
|
|
18
|
+
#
|
|
19
|
+
# Each scenario becomes a Kensho case; each Gherkin step becomes a Kensho
|
|
20
|
+
# step. Data tables on a step are emitted as `step.parameters[]`. The
|
|
21
|
+
# scenario's `attach` calls (Cucumber's built-in attachment API) are
|
|
22
|
+
# routed into `case.attachments[]`.
|
|
23
|
+
#
|
|
24
|
+
# Tags drive metadata:
|
|
25
|
+
#
|
|
26
|
+
# @severity:critical → case.severity
|
|
27
|
+
# @critical / @blocker / ... → case.severity (shorthand)
|
|
28
|
+
# @kensho.label.team=growth → case.labels.team = 'growth'
|
|
29
|
+
# @kensho.link.jira=PROJ-123 → case.links += { kind: 'jira', label: 'PROJ-123', url: 'PROJ-123' }
|
|
30
|
+
# @kensho.url.jira=https://… → case.links (full url form)
|
|
31
|
+
# any other @tag → case.tags
|
|
32
|
+
|
|
33
|
+
module Kensho
|
|
34
|
+
module Cucumber
|
|
35
|
+
class Formatter
|
|
36
|
+
SEVERITY_TAGS = Kensho::Schema::SEVERITY.dup.freeze
|
|
37
|
+
|
|
38
|
+
attr_reader :config
|
|
39
|
+
|
|
40
|
+
def initialize(config)
|
|
41
|
+
@config = config
|
|
42
|
+
@ast_lookup = nil
|
|
43
|
+
if defined?(::Cucumber::Formatter::AstLookup)
|
|
44
|
+
begin
|
|
45
|
+
@ast_lookup = ::Cucumber::Formatter::AstLookup.new(config)
|
|
46
|
+
rescue StandardError
|
|
47
|
+
@ast_lookup = nil
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
@output_dir = File.expand_path(ENV['KENSHO_OUTPUT'] || (defined?(Kensho::Cucumber::DEFAULT_OUTPUT) ? Kensho::Cucumber::DEFAULT_OUTPUT : 'kensho-results'))
|
|
51
|
+
@cases_dir = File.join(@output_dir, 'cases')
|
|
52
|
+
@attachments_dir = File.join(@output_dir, 'attachments')
|
|
53
|
+
@project = {
|
|
54
|
+
'name' => ENV['KENSHO_PROJECT_NAME'] || 'Unknown project',
|
|
55
|
+
'slug' => ENV['KENSHO_PROJECT_SLUG'] || Kensho::Schema.slugify(ENV['KENSHO_PROJECT_NAME'] || 'unknown')
|
|
56
|
+
}
|
|
57
|
+
@run_id = ENV['KENSHO_RUN_ID'] || Kensho::Schema.default_run_id
|
|
58
|
+
@started_at = Kensho::Schema.iso_now
|
|
59
|
+
@started_perf = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
60
|
+
@rootpath = Dir.pwd
|
|
61
|
+
@cases_by_id = {}
|
|
62
|
+
@ids_seen = Hash.new(0)
|
|
63
|
+
|
|
64
|
+
# Per-test-case scratch keyed by test_case (Cucumber's TestCase obj).
|
|
65
|
+
@scratch = {}
|
|
66
|
+
|
|
67
|
+
FileUtils.mkdir_p(@cases_dir)
|
|
68
|
+
FileUtils.mkdir_p(@attachments_dir)
|
|
69
|
+
|
|
70
|
+
bind_events!
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def bind_events!
|
|
74
|
+
return unless @config.respond_to?(:on_event)
|
|
75
|
+
|
|
76
|
+
@config.on_event(:test_case_started) { |event| on_test_case_started(event) }
|
|
77
|
+
@config.on_event(:test_step_finished) { |event| on_test_step_finished(event) }
|
|
78
|
+
@config.on_event(:test_case_finished) { |event| on_test_case_finished(event) }
|
|
79
|
+
@config.on_event(:test_run_finished) { |_event| on_test_run_finished }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# ------- per-event handlers ------- #
|
|
83
|
+
|
|
84
|
+
def on_test_case_started(event)
|
|
85
|
+
test_case = event.test_case
|
|
86
|
+
@current_test_case = test_case
|
|
87
|
+
scenario_name = test_case.name.to_s
|
|
88
|
+
feature_uri = test_case.location.file rescue nil
|
|
89
|
+
feature_uri ||= (test_case.respond_to?(:source_location) ? test_case.source_location.file : nil)
|
|
90
|
+
feature_name, rule_name = feature_and_rule(test_case)
|
|
91
|
+
|
|
92
|
+
full_name = [feature_name, scenario_name].compact.reject(&:empty?).join(' › ')
|
|
93
|
+
file_path = relpath(feature_uri, @rootpath)
|
|
94
|
+
line = (test_case.location.lines.first rescue nil) || (test_case.location.line rescue nil)
|
|
95
|
+
|
|
96
|
+
base_id = Kensho::Schema.stable_case_id(full_name, file_path)
|
|
97
|
+
seen = @ids_seen[base_id]
|
|
98
|
+
case_id = seen.zero? ? base_id : "#{base_id}_#{seen + 1}"
|
|
99
|
+
@ids_seen[base_id] = seen + 1
|
|
100
|
+
|
|
101
|
+
tags = (test_case.tags || []).map { |t| t.respond_to?(:name) ? t.name.to_s : t.to_s }
|
|
102
|
+
clean_tags = tags.map { |t| t.start_with?('@') ? t[1..] : t }
|
|
103
|
+
|
|
104
|
+
severity = severity_from_tags(clean_tags)
|
|
105
|
+
labels = labels_from_tags(clean_tags)
|
|
106
|
+
links = links_from_tags(clean_tags)
|
|
107
|
+
|
|
108
|
+
plain_tags = clean_tags.reject do |t|
|
|
109
|
+
t.start_with?('kensho.label.') || t.start_with?('kensho.link.') ||
|
|
110
|
+
t.start_with?('kensho.url.') || t.start_with?('severity:') ||
|
|
111
|
+
SEVERITY_TAGS.include?(t)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
behavior = {}
|
|
115
|
+
behavior['feature'] = feature_name.to_s if feature_name && !feature_name.empty?
|
|
116
|
+
behavior['epic'] = rule_name.to_s if rule_name && !rule_name.empty?
|
|
117
|
+
behavior['scenario'] = scenario_name if scenario_name && !scenario_name.empty?
|
|
118
|
+
|
|
119
|
+
case_obj = {
|
|
120
|
+
'id' => case_id,
|
|
121
|
+
'name' => scenario_name,
|
|
122
|
+
'fullName' => full_name,
|
|
123
|
+
'status' => 'skip',
|
|
124
|
+
'startedAt' => Kensho::Schema.iso_now,
|
|
125
|
+
'duration' => 0,
|
|
126
|
+
'retries' => 0,
|
|
127
|
+
'platform' => Kensho::Schema.normalize_os
|
|
128
|
+
}
|
|
129
|
+
case_obj['filePath'] = file_path if file_path
|
|
130
|
+
case_obj['line'] = line.to_i if line
|
|
131
|
+
case_obj['suite'] = [feature_name].compact.reject(&:empty?)
|
|
132
|
+
case_obj.delete('suite') if case_obj['suite'].empty?
|
|
133
|
+
case_obj['tags'] = plain_tags unless plain_tags.empty?
|
|
134
|
+
case_obj['severity'] = severity if severity
|
|
135
|
+
case_obj['labels'] = labels unless labels.empty?
|
|
136
|
+
case_obj['links'] = links unless links.empty?
|
|
137
|
+
case_obj['behavior'] = behavior unless behavior.empty?
|
|
138
|
+
|
|
139
|
+
scratch = {
|
|
140
|
+
case_obj: case_obj,
|
|
141
|
+
started_perf: Process.clock_gettime(Process::CLOCK_MONOTONIC),
|
|
142
|
+
started_iso: case_obj['startedAt'],
|
|
143
|
+
steps: [],
|
|
144
|
+
step_index: 0,
|
|
145
|
+
worst_status: 'pass',
|
|
146
|
+
first_error: nil,
|
|
147
|
+
first_exception: nil,
|
|
148
|
+
attachments: []
|
|
149
|
+
}
|
|
150
|
+
@scratch[test_case.object_id] = scratch
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def on_test_step_finished(event)
|
|
154
|
+
test_case = @current_test_case
|
|
155
|
+
return unless test_case
|
|
156
|
+
|
|
157
|
+
scratch = @scratch[test_case.object_id]
|
|
158
|
+
return unless scratch
|
|
159
|
+
|
|
160
|
+
step = event.test_step
|
|
161
|
+
result = event.result
|
|
162
|
+
|
|
163
|
+
title, parameters = step_title_and_params(step)
|
|
164
|
+
# Cucumber emits hooks as test steps with no Gherkin source; skip
|
|
165
|
+
# those from the timeline so the report shows just the user steps.
|
|
166
|
+
return if title.nil?
|
|
167
|
+
|
|
168
|
+
status = map_step_status(result)
|
|
169
|
+
case_status = map_case_status(result)
|
|
170
|
+
if case_status == 'fail' || case_status == 'broken'
|
|
171
|
+
scratch[:worst_status] = case_status
|
|
172
|
+
elsif case_status == 'skip' && scratch[:worst_status] == 'pass'
|
|
173
|
+
scratch[:worst_status] = 'skip'
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
if (result.respond_to?(:failed?) && result.failed?) ||
|
|
177
|
+
(result.respond_to?(:exception) && result.exception)
|
|
178
|
+
scratch[:first_exception] ||= result.exception if result.respond_to?(:exception)
|
|
179
|
+
scratch[:first_error] ||= (result.respond_to?(:exception) && result.exception) ?
|
|
180
|
+
result.exception.message.to_s :
|
|
181
|
+
result.to_s
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
duration_ms = duration_ms_from_result(result)
|
|
185
|
+
idx = scratch[:step_index]
|
|
186
|
+
scratch[:step_index] += 1
|
|
187
|
+
|
|
188
|
+
step_obj = {
|
|
189
|
+
'id' => "step_#{idx}_#{SecureRandom.hex(3)}",
|
|
190
|
+
'title' => title,
|
|
191
|
+
'status' => status,
|
|
192
|
+
'startedAt' => Kensho::Schema.iso_now,
|
|
193
|
+
'duration' => duration_ms
|
|
194
|
+
}
|
|
195
|
+
step_obj['parameters'] = parameters unless parameters.empty?
|
|
196
|
+
scratch[:steps] << step_obj
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def on_test_case_finished(event)
|
|
200
|
+
test_case = event.test_case
|
|
201
|
+
scratch = @scratch.delete(test_case.object_id)
|
|
202
|
+
return unless scratch
|
|
203
|
+
|
|
204
|
+
case_obj = scratch[:case_obj]
|
|
205
|
+
result = event.result
|
|
206
|
+
|
|
207
|
+
status =
|
|
208
|
+
if result.respond_to?(:passed?) && result.passed?
|
|
209
|
+
scratch[:worst_status] == 'pass' ? 'pass' : scratch[:worst_status]
|
|
210
|
+
elsif result.respond_to?(:failed?) && result.failed?
|
|
211
|
+
'fail'
|
|
212
|
+
elsif result.respond_to?(:undefined?) && result.undefined?
|
|
213
|
+
'broken'
|
|
214
|
+
elsif result.respond_to?(:skipped?) && result.skipped?
|
|
215
|
+
'skip'
|
|
216
|
+
elsif result.respond_to?(:pending?) && result.pending?
|
|
217
|
+
'skip'
|
|
218
|
+
else
|
|
219
|
+
scratch[:worst_status]
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
duration_ms = [(((Process.clock_gettime(Process::CLOCK_MONOTONIC) - scratch[:started_perf]) * 1000.0).round), 0].max
|
|
223
|
+
|
|
224
|
+
case_obj['status'] = status
|
|
225
|
+
case_obj['finishedAt'] = Kensho::Schema.iso_now
|
|
226
|
+
case_obj['duration'] = duration_ms
|
|
227
|
+
case_obj['steps'] = scratch[:steps] unless scratch[:steps].empty?
|
|
228
|
+
|
|
229
|
+
if scratch[:first_exception] || scratch[:first_error]
|
|
230
|
+
ex = scratch[:first_exception]
|
|
231
|
+
message = ex ? ex.message.to_s.lines.first.to_s.strip : scratch[:first_error].to_s.lines.first.to_s.strip
|
|
232
|
+
err = { 'message' => message.empty? ? 'failure' : message }
|
|
233
|
+
if ex
|
|
234
|
+
err['type'] = ex.class.name.to_s
|
|
235
|
+
stack = [ex.message.to_s]
|
|
236
|
+
stack.concat(Array(ex.backtrace).first(20))
|
|
237
|
+
err['stack'] = stack.join("\n")
|
|
238
|
+
elsif scratch[:first_error] && scratch[:first_error].to_s != message
|
|
239
|
+
err['stack'] = scratch[:first_error].to_s
|
|
240
|
+
end
|
|
241
|
+
case_obj['errors'] = [err]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
case_obj['attachments'] = scratch[:attachments] unless scratch[:attachments].empty?
|
|
245
|
+
|
|
246
|
+
@cases_by_id[case_obj['id']] = case_obj
|
|
247
|
+
write_case(case_obj)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def on_test_run_finished
|
|
251
|
+
write_run_json
|
|
252
|
+
rescue StandardError => e
|
|
253
|
+
warn "[kensho] failed to write run.json: #{e.message}"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# ------- helpers ------- #
|
|
257
|
+
|
|
258
|
+
def step_title_and_params(step)
|
|
259
|
+
return [nil, []] if step.respond_to?(:hook?) && step.hook?
|
|
260
|
+
|
|
261
|
+
text =
|
|
262
|
+
if step.respond_to?(:text)
|
|
263
|
+
step.text.to_s
|
|
264
|
+
elsif step.respond_to?(:name)
|
|
265
|
+
step.name.to_s
|
|
266
|
+
else
|
|
267
|
+
''
|
|
268
|
+
end
|
|
269
|
+
return [nil, []] if text.empty?
|
|
270
|
+
|
|
271
|
+
keyword = ''
|
|
272
|
+
gherkin_step = nil
|
|
273
|
+
if @ast_lookup
|
|
274
|
+
begin
|
|
275
|
+
src = @ast_lookup.step_source(step)
|
|
276
|
+
gherkin_step = src && src.respond_to?(:step) ? src.step : nil
|
|
277
|
+
rescue StandardError
|
|
278
|
+
gherkin_step = nil
|
|
279
|
+
end
|
|
280
|
+
keyword = gherkin_step.keyword.to_s if gherkin_step && gherkin_step.respond_to?(:keyword)
|
|
281
|
+
end
|
|
282
|
+
title = keyword && !keyword.empty? ? "#{keyword}#{text}" : text
|
|
283
|
+
|
|
284
|
+
params = []
|
|
285
|
+
if gherkin_step && gherkin_step.respond_to?(:data_table) && gherkin_step.data_table
|
|
286
|
+
rows = gherkin_step.data_table.rows
|
|
287
|
+
rows.each_with_index do |row, ri|
|
|
288
|
+
cells = row.respond_to?(:cells) ? row.cells : []
|
|
289
|
+
cells.each_with_index do |cell, ci|
|
|
290
|
+
value = cell.respond_to?(:value) ? cell.value.to_s : cell.to_s
|
|
291
|
+
params << {
|
|
292
|
+
'name' => "row#{ri}.col#{ci}",
|
|
293
|
+
'value' => value,
|
|
294
|
+
'kind' => 'data-row'
|
|
295
|
+
}
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
if gherkin_step && gherkin_step.respond_to?(:doc_string) && gherkin_step.doc_string
|
|
300
|
+
ds = gherkin_step.doc_string
|
|
301
|
+
params << {
|
|
302
|
+
'name' => 'docstring',
|
|
303
|
+
'value' => ds.respond_to?(:content) ? ds.content.to_s : ds.to_s,
|
|
304
|
+
'kind' => 'argument'
|
|
305
|
+
}
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
[title, params]
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def feature_and_rule(test_case)
|
|
312
|
+
feature = nil
|
|
313
|
+
rule = nil
|
|
314
|
+
if @ast_lookup
|
|
315
|
+
uri = test_case.location.file rescue nil
|
|
316
|
+
if uri
|
|
317
|
+
doc = nil
|
|
318
|
+
begin
|
|
319
|
+
doc = @ast_lookup.gherkin_document(uri)
|
|
320
|
+
rescue StandardError
|
|
321
|
+
doc = nil
|
|
322
|
+
end
|
|
323
|
+
if doc && doc.respond_to?(:feature) && doc.feature
|
|
324
|
+
feature = doc.feature.name.to_s if doc.feature.respond_to?(:name)
|
|
325
|
+
rule = walk_for_rule(doc.feature, test_case.location.lines.max) if doc.feature.respond_to?(:children)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
[feature, rule]
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def walk_for_rule(feature, line)
|
|
333
|
+
feature.children.each do |child|
|
|
334
|
+
next unless child.respond_to?(:rule) && child.rule
|
|
335
|
+
|
|
336
|
+
rule = child.rule
|
|
337
|
+
# Lines covered by this rule include all its scenarios.
|
|
338
|
+
covered = false
|
|
339
|
+
if rule.respond_to?(:children)
|
|
340
|
+
rule.children.each do |sc_child|
|
|
341
|
+
next unless sc_child.respond_to?(:scenario) && sc_child.scenario
|
|
342
|
+
|
|
343
|
+
if sc_child.scenario.location.line == line ||
|
|
344
|
+
sc_child.scenario.steps.any? { |s| s.location.line == line }
|
|
345
|
+
covered = true
|
|
346
|
+
break
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
return rule.name.to_s if covered && rule.respond_to?(:name)
|
|
351
|
+
end
|
|
352
|
+
nil
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def severity_from_tags(tags)
|
|
356
|
+
tags.each do |t|
|
|
357
|
+
return t.split(':', 2)[1].to_s.downcase if t.start_with?('severity:') &&
|
|
358
|
+
SEVERITY_TAGS.include?(t.split(':', 2)[1].to_s.downcase)
|
|
359
|
+
return t.downcase if SEVERITY_TAGS.include?(t.downcase)
|
|
360
|
+
end
|
|
361
|
+
nil
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def labels_from_tags(tags)
|
|
365
|
+
labels = {}
|
|
366
|
+
tags.each do |t|
|
|
367
|
+
next unless t.start_with?('kensho.label.')
|
|
368
|
+
|
|
369
|
+
rest = t.sub('kensho.label.', '')
|
|
370
|
+
key, value = rest.split('=', 2)
|
|
371
|
+
next if key.nil? || key.empty?
|
|
372
|
+
|
|
373
|
+
labels[key] = (value || 'true').to_s
|
|
374
|
+
end
|
|
375
|
+
labels
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def links_from_tags(tags)
|
|
379
|
+
links = []
|
|
380
|
+
tags.each do |t|
|
|
381
|
+
if t.start_with?('kensho.url.')
|
|
382
|
+
rest = t.sub('kensho.url.', '')
|
|
383
|
+
kind, url = rest.split('=', 2)
|
|
384
|
+
next if url.nil? || url.empty?
|
|
385
|
+
|
|
386
|
+
link = { 'url' => url }
|
|
387
|
+
link['kind'] = kind if kind && !kind.empty?
|
|
388
|
+
links << link
|
|
389
|
+
elsif t.start_with?('kensho.link.')
|
|
390
|
+
rest = t.sub('kensho.link.', '')
|
|
391
|
+
kind, label = rest.split('=', 2)
|
|
392
|
+
next if label.nil? || label.empty?
|
|
393
|
+
|
|
394
|
+
link = { 'url' => label, 'label' => label }
|
|
395
|
+
link['kind'] = kind if kind && !kind.empty?
|
|
396
|
+
links << link
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
links
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def map_case_status(result)
|
|
403
|
+
return 'pass' if result.respond_to?(:passed?) && result.passed?
|
|
404
|
+
return 'fail' if result.respond_to?(:failed?) && result.failed?
|
|
405
|
+
return 'broken' if result.respond_to?(:undefined?) && result.undefined?
|
|
406
|
+
return 'broken' if result.class.name.to_s.end_with?('::Undefined', '::Ambiguous')
|
|
407
|
+
return 'skip' if result.respond_to?(:skipped?) && result.skipped?
|
|
408
|
+
return 'skip' if result.respond_to?(:pending?) && result.pending?
|
|
409
|
+
|
|
410
|
+
'pass'
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def map_step_status(result)
|
|
414
|
+
s = map_case_status(result)
|
|
415
|
+
s == 'broken' ? 'fail' : s
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def duration_ms_from_result(result)
|
|
419
|
+
return 0 unless result.respond_to?(:duration) && result.duration
|
|
420
|
+
|
|
421
|
+
d = result.duration
|
|
422
|
+
if d.respond_to?(:nanoseconds)
|
|
423
|
+
(d.nanoseconds / 1_000_000.0).round
|
|
424
|
+
elsif d.respond_to?(:to_f)
|
|
425
|
+
(d.to_f * 1000.0).round
|
|
426
|
+
else
|
|
427
|
+
0
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def write_case(case_obj)
|
|
432
|
+
path = File.join(@cases_dir, "#{case_obj['id']}.json")
|
|
433
|
+
File.write(path, JSON.pretty_generate(case_obj))
|
|
434
|
+
rescue StandardError => e
|
|
435
|
+
warn "[kensho] could not write #{File.basename(path)}: #{e.message}"
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def write_run_json
|
|
439
|
+
finished_at = Kensho::Schema.iso_now
|
|
440
|
+
cases = @cases_by_id.values
|
|
441
|
+
totals = { 'pass' => 0, 'fail' => 0, 'broken' => 0, 'skip' => 0 }
|
|
442
|
+
cases.each { |c| totals[c['status']] += 1 if totals.key?(c['status']) }
|
|
443
|
+
duration_ms = [((Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_perf) * 1000.0).round, 0].max
|
|
444
|
+
framework_version = cucumber_version
|
|
445
|
+
|
|
446
|
+
run = {
|
|
447
|
+
'schemaVersion' => Kensho::Schema::SCHEMA_VERSION,
|
|
448
|
+
'id' => @run_id,
|
|
449
|
+
'project' => @project,
|
|
450
|
+
'framework' => { 'name' => 'cucumber-ruby', 'version' => framework_version },
|
|
451
|
+
'env' => Kensho::Schema.env_info(framework_version: framework_version),
|
|
452
|
+
'startedAt' => @started_at,
|
|
453
|
+
'finishedAt' => finished_at,
|
|
454
|
+
'totals' => totals,
|
|
455
|
+
'durationMs' => duration_ms,
|
|
456
|
+
'testCases' => cases
|
|
457
|
+
}
|
|
458
|
+
File.write(File.join(@output_dir, 'run.json'), JSON.pretty_generate(run))
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def cucumber_version
|
|
462
|
+
if defined?(::Cucumber::VERSION)
|
|
463
|
+
::Cucumber::VERSION.to_s
|
|
464
|
+
else
|
|
465
|
+
'unknown'
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def relpath(path, root)
|
|
470
|
+
return nil unless path
|
|
471
|
+
|
|
472
|
+
absolute_path = File.absolute_path(path.to_s)
|
|
473
|
+
absolute_root = File.absolute_path(root.to_s)
|
|
474
|
+
if absolute_path.start_with?(absolute_root + File::SEPARATOR)
|
|
475
|
+
absolute_path[(absolute_root.length + 1)..]
|
|
476
|
+
else
|
|
477
|
+
path.to_s.sub(%r{\A\./}, '')
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Public entrypoint for kensho-cucumber-ruby.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# bundle exec cucumber --format Kensho::Cucumber::Formatter
|
|
7
|
+
|
|
8
|
+
require_relative '_schema'
|
|
9
|
+
require_relative 'cucumber/version'
|
|
10
|
+
require_relative 'cucumber/formatter'
|
|
11
|
+
|
|
12
|
+
module Kensho
|
|
13
|
+
module Cucumber
|
|
14
|
+
DEFAULT_OUTPUT = 'kensho-results'
|
|
15
|
+
end
|
|
16
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: kensho-cucumber-ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- KaizenReport
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-06-07 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: cucumber
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.0'
|
|
27
|
+
description: |
|
|
28
|
+
Cucumber 7+ formatter that writes a Kensho v1 result bundle
|
|
29
|
+
(run.json + cases/<id>.json + attachments/) into ./kensho-results.
|
|
30
|
+
Each scenario becomes a Kensho case; each Gherkin step becomes a
|
|
31
|
+
Kensho step.
|
|
32
|
+
email:
|
|
33
|
+
executables: []
|
|
34
|
+
extensions: []
|
|
35
|
+
extra_rdoc_files: []
|
|
36
|
+
files:
|
|
37
|
+
- README.md
|
|
38
|
+
- lib/kensho/_schema.rb
|
|
39
|
+
- lib/kensho/cucumber.rb
|
|
40
|
+
- lib/kensho/cucumber/formatter.rb
|
|
41
|
+
- lib/kensho/cucumber/version.rb
|
|
42
|
+
homepage: https://github.com/brandon1794/kensho
|
|
43
|
+
licenses:
|
|
44
|
+
- Apache-2.0
|
|
45
|
+
metadata:
|
|
46
|
+
homepage_uri: https://github.com/brandon1794/kensho
|
|
47
|
+
source_code_uri: https://github.com/brandon1794/kensho
|
|
48
|
+
bug_tracker_uri: https://github.com/brandon1794/kensho/issues
|
|
49
|
+
rubygems_mfa_required: 'true'
|
|
50
|
+
post_install_message:
|
|
51
|
+
rdoc_options: []
|
|
52
|
+
require_paths:
|
|
53
|
+
- lib
|
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '2.6'
|
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: '0'
|
|
64
|
+
requirements: []
|
|
65
|
+
rubygems_version: 3.5.22
|
|
66
|
+
signing_key:
|
|
67
|
+
specification_version: 4
|
|
68
|
+
summary: Kensho reporter for Cucumber-Ruby — emits Kensho v1 results.
|
|
69
|
+
test_files: []
|