rspec-trace-formatter 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/.github/workflows/ci.yml +43 -0
- data/.gitignore +58 -0
- data/.rspec +3 -0
- data/Gemfile +15 -0
- data/LICENSE +21 -0
- data/README.md +89 -0
- data/Rakefile +15 -0
- data/examples/example_spec.rb +35 -0
- data/exe/rspec-trace-consumer +7 -0
- data/lefthook.yml +4 -0
- data/lib/rspec/trace/consumer.rb +149 -0
- data/lib/rspec/trace/formatter.rb +142 -0
- data/lib/rspec/trace/open_telemetry_formatter.rb +29 -0
- data/lib/rspec/trace/version.rb +7 -0
- data/lib/rspec/trace.rb +11 -0
- data/rspec-trace-formatter.gemspec +42 -0
- metadata +260 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 523ae0ee65d59fbcc8081aefe0da23355846add224b8465d210eea536a93d9a5
|
4
|
+
data.tar.gz: a94b8c49db8bf4a1cfd0d00ebdab0136120be9d9d7338294a3b9f173dc612503
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 73903d8f6cc20a9301eb92105ddd0ec80b9152c389ed6c855ae46118a8793a91a356c24200d6f71295e2bec289dc6a0223d158a750b1d8cd6da6fef43f45a9b1
|
7
|
+
data.tar.gz: 6afed85a1363970c45fa72d0c37412998d59e6d29334f70c18fc98ce7316376fe8a41f06dee84e28d5c6a894dd9a27ce18f45ee5002b842bfb3ca3617ade6801
|
@@ -0,0 +1,43 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ main ]
|
6
|
+
pull_request:
|
7
|
+
|
8
|
+
jobs:
|
9
|
+
lint:
|
10
|
+
runs-on: ubuntu-latest
|
11
|
+
steps:
|
12
|
+
- uses: actions/checkout@v2
|
13
|
+
- uses: ruby/setup-ruby@v1
|
14
|
+
with:
|
15
|
+
ruby-version: 2.7
|
16
|
+
- name: StandardRB Lint
|
17
|
+
run: |
|
18
|
+
gem install standardrb
|
19
|
+
standardrb --no-fix --format github
|
20
|
+
tests:
|
21
|
+
runs-on: ubuntu-latest
|
22
|
+
strategy:
|
23
|
+
fail-fast: false
|
24
|
+
matrix:
|
25
|
+
ruby:
|
26
|
+
- "2.5"
|
27
|
+
- "2.6"
|
28
|
+
- "2.7"
|
29
|
+
- "3.0"
|
30
|
+
- "jruby-9.2"
|
31
|
+
- "jruby-9.3"
|
32
|
+
- "truffleruby-20"
|
33
|
+
- "truffleruby-21"
|
34
|
+
- "truffleruby+graalvm-21"
|
35
|
+
steps:
|
36
|
+
- uses: actions/checkout@v2
|
37
|
+
- uses: ruby/setup-ruby@v1
|
38
|
+
with:
|
39
|
+
ruby-version: ${{ matrix.ruby }}
|
40
|
+
bundler-cache: true
|
41
|
+
- name: Run tests
|
42
|
+
run: |
|
43
|
+
bundle exec rspec --format RSpec::Github::Formatter
|
data/.gitignore
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/spec/examples.txt
|
9
|
+
/test/tmp/
|
10
|
+
/test/version_tmp/
|
11
|
+
/tmp/
|
12
|
+
|
13
|
+
# Used by dotenv library to load environment variables.
|
14
|
+
# .env
|
15
|
+
|
16
|
+
# Ignore Byebug command history file.
|
17
|
+
.byebug_history
|
18
|
+
|
19
|
+
## Specific to RubyMotion:
|
20
|
+
.dat*
|
21
|
+
.repl_history
|
22
|
+
build/
|
23
|
+
*.bridgesupport
|
24
|
+
build-iPhoneOS/
|
25
|
+
build-iPhoneSimulator/
|
26
|
+
|
27
|
+
## Specific to RubyMotion (use of CocoaPods):
|
28
|
+
#
|
29
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
30
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
31
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
32
|
+
#
|
33
|
+
# vendor/Pods/
|
34
|
+
|
35
|
+
## Documentation cache and generated files:
|
36
|
+
/.yardoc/
|
37
|
+
/_yardoc/
|
38
|
+
/doc/
|
39
|
+
/rdoc/
|
40
|
+
|
41
|
+
## Environment normalization:
|
42
|
+
/.bundle/
|
43
|
+
/vendor/bundle
|
44
|
+
/lib/bundler/man/
|
45
|
+
|
46
|
+
# for a library or gem, you might want to ignore these files since the code is
|
47
|
+
# intended to run in multiple environments; otherwise, check them in:
|
48
|
+
Gemfile.lock
|
49
|
+
.ruby-version
|
50
|
+
.ruby-gemset
|
51
|
+
|
52
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
53
|
+
.rvmrc
|
54
|
+
|
55
|
+
# Used by RuboCop. Remote config files pulled in from inherit_from directive.
|
56
|
+
# .rubocop-https?--*
|
57
|
+
|
58
|
+
.rspec_status
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2021 Zach Thomae
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# rspec-trace-formatter
|
2
|
+
|
3
|
+
An [RSpec](https://rspec.info) formatter for constructing [trace data](https://opentelemetry.io/docs/concepts/data-sources/#traces) from your specs.
|
4
|
+
This library is inspired by [go-test-trace](https://github.com/rakyll/go-test-trace).
|
5
|
+
|
6
|
+
## Why Would I Use This?
|
7
|
+
|
8
|
+
Collecting data from your RSpec tests may be useful to you for a number of reasons:
|
9
|
+
|
10
|
+
1. You'd like to see statistical trends in test runtimes, or in your CI/CD pipeline as a whole
|
11
|
+
2. You'd like a dataset containing pass/fail statuses for all tests to help hunt down flakes
|
12
|
+
4. Other things that I can't think of
|
13
|
+
|
14
|
+
Traces aren't the only choice for collecting this data, but they are a reasonable one.
|
15
|
+
With concepts like test files and example groups, test execution naturally maps onto a trace tree.
|
16
|
+
The flexibility of tools like [OpenTelemetry](https://opentelemetry.io) when it comes to including arbitrary key-value attribute pairings is useful when instrumenting a library like RSpec because we can preserve as much context about the tests as we like.
|
17
|
+
And this test data isn't likely to be valuable long-term, so the standard retention periods for traces are likely to be acceptable.
|
18
|
+
|
19
|
+
## What's In The Box?
|
20
|
+
|
21
|
+
There are three main parts to this library.
|
22
|
+
|
23
|
+
### `RSpec::Trace::Formatter`
|
24
|
+
|
25
|
+
`RSpec::Trace::Formatter` is an RSpec formatter that emits events containing data that's relevant for constructing traces.
|
26
|
+
This formatter doesn't create traces -- it only outputs JSON events.
|
27
|
+
|
28
|
+
This formatter emits events for the most significant lifecycle events in an RSpec suite: the start of the suite, the start/end of each example and example group, and the end of the suite.
|
29
|
+
Because all events are timestamped, you can expect accurate timing data.
|
30
|
+
It also collects data about the names of the examples and example groups that are run, as well as useful facts like file locations, pass/fail status, &etc.
|
31
|
+
|
32
|
+
The event format is designed to be redundant when providing facts about examples and example groups, so as to be less prescriptive about how you consume them.
|
33
|
+
This may not be the best decision, but it seemed the right way.
|
34
|
+
|
35
|
+
### `rspec-trace-consumer`
|
36
|
+
|
37
|
+
`rspec-trace-consumer` is a script that reads events created by `RSpec::Trace::Formatter` from standard input and emits trace data to an [OpenTelemetry collector](https://opentelemetry.io/docs/collector/).
|
38
|
+
The OpenTelemetry SDK can be configured using the standard [`OTEL_*` environment variables](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md).
|
39
|
+
|
40
|
+
If not set by the `OTEL_SERVICE_NAME` environment variable, the service name will be set to `rspec`.
|
41
|
+
The name of the root span defaults to "rspec", but you can change that as well with the `RSPEC_TRACE_FORMATTER_ROOT_SPAN_NAME` environment variable.
|
42
|
+
|
43
|
+
This script uses the [`AlwaysOn` sampler](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#alwayson) to ensure that no data is ever discarded.
|
44
|
+
|
45
|
+
### `RSpec::Trace::OpenTelemetryFormatter`
|
46
|
+
|
47
|
+
`RSpec::Trace::OpenTelemetryFormatter` is a separate RSpec formatter that combines the two pieces above to collect trace events from RSpec tests _and_ send them to an OpenTelemetry collector.
|
48
|
+
|
49
|
+
Because this uses the (very nice!) [subprocess library](https://github.com/stripe/subprocess), it only works on Ruby platforms where `fork` is supported.
|
50
|
+
If you're running in an environment where this isn't supported (e.g. JRuby) you won't be able to use this.
|
51
|
+
However, the rest of this library is _expected_ to work for you, and [specifying an `--out` target for `RSpec::Trace::Formatter`](https://relishapp.com/rspec/rspec-core/v/3-10/docs/command-line/format-option) may make this easier.
|
52
|
+
|
53
|
+
## How Do I Use It?
|
54
|
+
|
55
|
+
This library should be used like [any other RSpec formatter](https://relishapp.com/rspec/rspec-core/v/3-10/docs/command-line/format-option), with the assistance of any environment variables that you need to control the OpenTelemetry data.
|
56
|
+
|
57
|
+
Example of using the `RSpec::Trace::OpenTelemetryFormatter` with representative environment variables set:
|
58
|
+
|
59
|
+
```bash
|
60
|
+
OTEL_TRACES_EXPORTER=console bundle exec rspec --format RSpec::Trace::OpenTelemetryFormatter
|
61
|
+
```
|
62
|
+
|
63
|
+
Example of running the `RSpec::Trace::Formatter` by itself and sending the output to `rspec-trace-consumer` separately (in a way that you can surely improve upon):
|
64
|
+
|
65
|
+
```bash
|
66
|
+
OTEL_TRACES_EXPORTER=console bundle exec rspec --format RSpec::Trace::Formatter --out /tmp/trace-events.jsonl
|
67
|
+
|
68
|
+
rspec-trace-consumer < /tmp/trace-events.jsonl
|
69
|
+
```
|
70
|
+
|
71
|
+
## How Do I Contribute?
|
72
|
+
|
73
|
+
Very carefully, I hope.
|
74
|
+
|
75
|
+
One notable fact is that we use [snapshot testing](https://github.com/levinmr/rspec-snapshot) for the class underpinning `rspec-trace-consumer`.
|
76
|
+
To keep this reliable, I've defined a custom OpenTelemetry span exporter that includes meaningful-enough data to test with and no execution-specific fields.
|
77
|
+
|
78
|
+
## What's Coming Next?
|
79
|
+
|
80
|
+
This does not yet support providing a [parent trace ID with an environment variable](https://github.com/open-telemetry/opentelemetry-specification/issues/740).
|
81
|
+
I haven't done it yet because it's not very helpful to me given my primary use case, but I'll add that soon enough.
|
82
|
+
|
83
|
+
The fixtures for the snapshot tests for the `Consumer` class are currently system-specific insofar as the exception stack traces are built from real exceptions that include paths from the Ruby installation that generated them.
|
84
|
+
Right now they come from my setup, so if anyone else regenerates the fixtures these paths will change.
|
85
|
+
I believe that the ideal way to fix this is to provide a containerized development environment for which these paths can be fixed for everyone.
|
86
|
+
|
87
|
+
## License
|
88
|
+
|
89
|
+
MIT
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new(:test)
|
5
|
+
task default: :test
|
6
|
+
|
7
|
+
desc "Run the tests and update the snapshots"
|
8
|
+
task :update_snapshots do
|
9
|
+
system("UPDATE_SNAPSHOTS=true rake test")
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "Regenerates example fixtures for tests"
|
13
|
+
task :regenerate_examples do
|
14
|
+
system("rspec --format RSpec::Trace::Formatter --out spec/fixtures/example_trace_events.jsonl examples")
|
15
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
RSpec.describe "examples" do
|
2
|
+
describe String do
|
3
|
+
it "allows concatenation" do
|
4
|
+
expect("foo" + "bar").to eq("foobar")
|
5
|
+
end
|
6
|
+
|
7
|
+
it "fails with bad concatenation" do
|
8
|
+
expect("foo" + "bar").to eq("foo_bar")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe Kernel do
|
13
|
+
it "sleeps" do
|
14
|
+
sleep 3
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it "has an undefined example"
|
19
|
+
|
20
|
+
xit "has an example skipped with xit"
|
21
|
+
|
22
|
+
it "has an explicitly skipped example" do
|
23
|
+
skip("Not running this test")
|
24
|
+
end
|
25
|
+
|
26
|
+
it "has a pending example that fails" do
|
27
|
+
pending("This feature is not yet implemented")
|
28
|
+
expect(GreatClass.start).to eq([])
|
29
|
+
end
|
30
|
+
|
31
|
+
it "has a pending example that passes" do
|
32
|
+
pending("This feature is not yet implemented")
|
33
|
+
expect(1 + 2).to eq(3)
|
34
|
+
end
|
35
|
+
end
|
data/lefthook.yml
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "opentelemetry/sdk"
|
5
|
+
require "opentelemetry/exporter/otlp"
|
6
|
+
|
7
|
+
module RSpec
|
8
|
+
module Trace
|
9
|
+
class Consumer
|
10
|
+
def initialize(input)
|
11
|
+
@input = input
|
12
|
+
OpenTelemetry::SDK.configure do |c|
|
13
|
+
c.service_name = ENV.fetch("OTEL_SERVICE_NAME", "rspec")
|
14
|
+
end
|
15
|
+
@tracer_provider = OpenTelemetry.tracer_provider
|
16
|
+
@tracer_provider.sampler = OpenTelemetry::SDK::Trace::Samplers::ALWAYS_ON
|
17
|
+
@tracer = @tracer_provider.tracer("rspec-trace-formatter", RSpec::Trace::VERSION)
|
18
|
+
@spans = []
|
19
|
+
# TODO: Not this
|
20
|
+
@current_span_key = OpenTelemetry::Trace.const_get(:CURRENT_SPAN_KEY)
|
21
|
+
@contexts = [OpenTelemetry::Context.empty]
|
22
|
+
@tokens = []
|
23
|
+
end
|
24
|
+
|
25
|
+
def run
|
26
|
+
@input.each_line do |line|
|
27
|
+
next if line.strip.empty?
|
28
|
+
|
29
|
+
begin
|
30
|
+
event = parse_event(line)
|
31
|
+
rescue
|
32
|
+
warn "invalid line: #{line}"
|
33
|
+
next
|
34
|
+
end
|
35
|
+
|
36
|
+
case event[:event].to_sym
|
37
|
+
when :initiated
|
38
|
+
root_span_name = ENV.fetch("RSPEC_TRACE_FORMATTER_ROOT_SPAN_NAME", "rspec")
|
39
|
+
create_span(name: root_span_name, timestamp: event[:timestamp])
|
40
|
+
create_span(name: "examples loading", timestamp: event[:timestamp]) do |span|
|
41
|
+
span.add_attributes("rspec.type" => "loading")
|
42
|
+
end
|
43
|
+
when :start
|
44
|
+
complete_span(timestamp: event[:timestamp])
|
45
|
+
create_span(name: "examples running", timestamp: event[:timestamp]) do |span|
|
46
|
+
add_attributes_to_span(
|
47
|
+
span: span,
|
48
|
+
attributes: {count: event[:count], type: "suite"},
|
49
|
+
attribute_prefix: "rspec"
|
50
|
+
)
|
51
|
+
end
|
52
|
+
when :example_group_started
|
53
|
+
create_span(name: event.dig(:group, :description), timestamp: event[:timestamp]) do |span|
|
54
|
+
add_attributes_to_span(
|
55
|
+
span: span,
|
56
|
+
attributes: event[:group].merge(type: "example_group"),
|
57
|
+
attribute_prefix: "rspec",
|
58
|
+
exclude_attributes: [:description]
|
59
|
+
)
|
60
|
+
end
|
61
|
+
when :example_group_finished
|
62
|
+
complete_span(timestamp: event[:timestamp])
|
63
|
+
when :example_started
|
64
|
+
create_span(name: event.dig(:example, :description), timestamp: event[:timestamp]) do |span|
|
65
|
+
add_attributes_to_span(
|
66
|
+
span: span,
|
67
|
+
attributes: event[:example].merge(type: "example"),
|
68
|
+
attribute_prefix: "rspec",
|
69
|
+
exclude_attributes: [:description]
|
70
|
+
)
|
71
|
+
end
|
72
|
+
when :example_passed
|
73
|
+
complete_span(timestamp: event[:timestamp]) do |span|
|
74
|
+
add_attributes_to_span(
|
75
|
+
span: span,
|
76
|
+
attributes: event.dig(:example, :result),
|
77
|
+
attribute_prefix: "rspec.result"
|
78
|
+
)
|
79
|
+
end
|
80
|
+
when :example_pending
|
81
|
+
complete_span(timestamp: event[:timestamp]) do |span|
|
82
|
+
add_attributes_to_span(
|
83
|
+
span: span,
|
84
|
+
attributes: event.dig(:example, :result),
|
85
|
+
attribute_prefix: "rspec.result"
|
86
|
+
)
|
87
|
+
end
|
88
|
+
when :example_failed
|
89
|
+
complete_span(timestamp: event[:timestamp]) do |span|
|
90
|
+
add_attributes_to_span(
|
91
|
+
span: span,
|
92
|
+
attributes: event.dig(:example, :result),
|
93
|
+
attribute_prefix: "rspec.result"
|
94
|
+
)
|
95
|
+
event_attributes = {
|
96
|
+
"exception.type" => event.dig(:exception, :type),
|
97
|
+
"exception.message" => event.dig(:exception, :message),
|
98
|
+
"exception.stacktrace" => event.dig(:exception, :backtrace)
|
99
|
+
}
|
100
|
+
span.add_event("exception", attributes: event_attributes, timestamp: event[:timestamp])
|
101
|
+
span.status = OpenTelemetry::Trace::Status.error
|
102
|
+
end
|
103
|
+
when :stop
|
104
|
+
complete_span(timestamp: event[:timestamp]) until @spans.empty?
|
105
|
+
@tracer_provider.force_flush
|
106
|
+
exit
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def parse_event(line)
|
114
|
+
event = JSON.parse(line, symbolize_names: true)
|
115
|
+
event[:timestamp] = Time.parse(event[:timestamp])
|
116
|
+
event
|
117
|
+
end
|
118
|
+
|
119
|
+
def create_span(name:, timestamp:)
|
120
|
+
@tracer.start_span(name, start_timestamp: timestamp, with_parent: @contexts.last).tap do |span|
|
121
|
+
yield span if block_given?
|
122
|
+
@spans.push(span)
|
123
|
+
@contexts.push(@contexts.last.set_value(@current_span_key, span))
|
124
|
+
@tokens.push(OpenTelemetry::Context.attach(@contexts.last))
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def complete_span(timestamp:)
|
129
|
+
@spans.pop.tap do |span|
|
130
|
+
yield span if block_given?
|
131
|
+
span.finish(end_timestamp: timestamp)
|
132
|
+
@contexts.pop
|
133
|
+
OpenTelemetry::Context.detach(@tokens.pop)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def add_attributes_to_span(span:, attributes:, attribute_prefix: nil, exclude_attributes: [])
|
138
|
+
attributes_for_span = attributes.map do |key, value|
|
139
|
+
next if value.nil? || exclude_attributes.include?(key)
|
140
|
+
|
141
|
+
full_key = attribute_prefix ? "#{attribute_prefix}.#{key}" : key.to_s
|
142
|
+
[full_key, value]
|
143
|
+
end.compact.to_h
|
144
|
+
|
145
|
+
span.add_attributes(attributes_for_span)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/time"
|
4
|
+
require "active_support/json"
|
5
|
+
require "rspec/core"
|
6
|
+
require "rspec/core/formatters/base_text_formatter"
|
7
|
+
|
8
|
+
require_relative "version"
|
9
|
+
|
10
|
+
module RSpec
|
11
|
+
module Trace
|
12
|
+
class Formatter < RSpec::Core::Formatters::BaseFormatter
|
13
|
+
RSpec::Core::Formatters.register(
|
14
|
+
self,
|
15
|
+
:start,
|
16
|
+
:example_group_started, :example_group_finished,
|
17
|
+
:example_started, :example_passed, :example_pending, :example_failed,
|
18
|
+
:stop
|
19
|
+
)
|
20
|
+
|
21
|
+
def start(notification)
|
22
|
+
start_time = current_time
|
23
|
+
output.puts(JSON.dump({
|
24
|
+
timestamp: format_time(start_time - notification.load_time.seconds),
|
25
|
+
event: :initiated
|
26
|
+
}))
|
27
|
+
output.puts(JSON.dump({
|
28
|
+
timestamp: format_time(start_time),
|
29
|
+
event: :start,
|
30
|
+
count: notification.count
|
31
|
+
}))
|
32
|
+
end
|
33
|
+
|
34
|
+
def example_group_started(notification)
|
35
|
+
output.puts(JSON.dump({
|
36
|
+
timestamp: current_timestamp,
|
37
|
+
event: :example_group_started,
|
38
|
+
group: example_group_attributes(notification.group)
|
39
|
+
}))
|
40
|
+
end
|
41
|
+
|
42
|
+
def example_group_finished(notification)
|
43
|
+
output.puts(JSON.dump({
|
44
|
+
timestamp: current_timestamp,
|
45
|
+
event: :example_group_finished,
|
46
|
+
group: example_group_attributes(notification.group)
|
47
|
+
}))
|
48
|
+
end
|
49
|
+
|
50
|
+
def example_started(notification)
|
51
|
+
output.puts(JSON.dump({
|
52
|
+
timestamp: current_timestamp,
|
53
|
+
event: :example_started,
|
54
|
+
example: example_attributes(notification.example)
|
55
|
+
}))
|
56
|
+
end
|
57
|
+
|
58
|
+
def example_passed(notification)
|
59
|
+
output.puts(JSON.dump({
|
60
|
+
timestamp: current_timestamp,
|
61
|
+
event: :example_passed,
|
62
|
+
example: completed_example_attributes(notification.example)
|
63
|
+
}))
|
64
|
+
end
|
65
|
+
|
66
|
+
def example_pending(notification)
|
67
|
+
output.puts(JSON.dump({
|
68
|
+
timestamp: current_timestamp,
|
69
|
+
event: :example_pending,
|
70
|
+
example: completed_example_attributes(notification.example)
|
71
|
+
}))
|
72
|
+
end
|
73
|
+
|
74
|
+
def example_failed(notification)
|
75
|
+
output.puts(JSON.dump({
|
76
|
+
timestamp: current_timestamp,
|
77
|
+
event: :example_failed,
|
78
|
+
example: completed_example_attributes(notification.example),
|
79
|
+
exception: {
|
80
|
+
message: notification.example.exception.message,
|
81
|
+
type: notification.example.exception.class.name,
|
82
|
+
backtrace: notification.example.exception.full_message(highlight: false, order: :top).encode("UTF-8", invalid: :replace, undef: :replace, replace: "�")
|
83
|
+
}
|
84
|
+
}))
|
85
|
+
end
|
86
|
+
|
87
|
+
def stop(_notification)
|
88
|
+
output.puts(JSON.dump({timestamp: current_timestamp, event: :stop}))
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def example_group_attributes(example_group)
|
94
|
+
{
|
95
|
+
description: example_group.description,
|
96
|
+
described_class: example_group.described_class,
|
97
|
+
file_path: example_group.file_path,
|
98
|
+
location: example_group.location
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
def example_attributes(example)
|
103
|
+
{
|
104
|
+
description: example.description,
|
105
|
+
full_description: example.full_description,
|
106
|
+
file_path: example.file_path,
|
107
|
+
location: example.location
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
def example_execution_result_attributes(execution_result)
|
112
|
+
{
|
113
|
+
status: execution_result.status,
|
114
|
+
pending_message: execution_result.pending_message,
|
115
|
+
pending_fixed: execution_result.pending_fixed
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
def completed_example_attributes(example)
|
120
|
+
example_attributes(example).merge({
|
121
|
+
result: example_execution_result_attributes(example.execution_result)
|
122
|
+
})
|
123
|
+
end
|
124
|
+
|
125
|
+
def format_time(time)
|
126
|
+
time.xmlschema(3)
|
127
|
+
end
|
128
|
+
|
129
|
+
def current_time
|
130
|
+
if defined?(Timecop)
|
131
|
+
Time.now_without_mock_time
|
132
|
+
else
|
133
|
+
Time.now
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def current_timestamp
|
138
|
+
format_time(current_time)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "subprocess"
|
4
|
+
require_relative "formatter"
|
5
|
+
|
6
|
+
module RSpec
|
7
|
+
module Trace
|
8
|
+
class OpenTelemetryFormatter < Formatter
|
9
|
+
RSpec::Core::Formatters.register(
|
10
|
+
self,
|
11
|
+
:start,
|
12
|
+
:example_group_started, :example_group_finished,
|
13
|
+
:example_started, :example_passed, :example_pending, :example_failed,
|
14
|
+
:stop
|
15
|
+
)
|
16
|
+
|
17
|
+
def initialize(output)
|
18
|
+
@process = Subprocess::Process.new(["rspec-trace-consumer"], {stdin: Subprocess::PIPE})
|
19
|
+
super(@process.stdin)
|
20
|
+
end
|
21
|
+
|
22
|
+
def stop(notification)
|
23
|
+
super(notification)
|
24
|
+
|
25
|
+
@process.wait
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/rspec/trace.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/rspec/trace/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "rspec-trace-formatter"
|
7
|
+
spec.version = RSpec::Trace::VERSION
|
8
|
+
spec.authors = ["Zach Thomae"]
|
9
|
+
spec.email = ["zach@thomae.co"]
|
10
|
+
|
11
|
+
spec.summary = "Formatter for RSpec to represent test runs as trace events"
|
12
|
+
spec.description = "Create traces from RSpec tests using OpenTelemetry or your own tracing library"
|
13
|
+
spec.homepage = "https://github.com/zthomae/rspec-trace-formatter"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 2.5.0"
|
16
|
+
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
19
|
+
spec.metadata["source_code_uri"] = "https://github.com/zthomae/rspec-trace-formatter"
|
20
|
+
|
21
|
+
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
22
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
23
|
+
end
|
24
|
+
spec.bindir = "exe"
|
25
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
|
+
|
27
|
+
spec.add_runtime_dependency "opentelemetry-api", "~> 1.0"
|
28
|
+
spec.add_runtime_dependency "opentelemetry-exporter-otlp", "~> 0.20"
|
29
|
+
spec.add_runtime_dependency "rspec-core", "~> 3.0"
|
30
|
+
spec.add_runtime_dependency "subprocess", "~> 1.0"
|
31
|
+
|
32
|
+
spec.add_development_dependency "activesupport", "~> 6.0"
|
33
|
+
spec.add_development_dependency "bundler", "~> 2.1"
|
34
|
+
spec.add_development_dependency "lefthook", "~> 0.7.7"
|
35
|
+
spec.add_development_dependency "pry", "~> 0.13"
|
36
|
+
spec.add_development_dependency "rake", "~> 13.0.6"
|
37
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
38
|
+
spec.add_development_dependency "rspec-github", "~> 2.3"
|
39
|
+
spec.add_development_dependency "rspec-snapshot", "~> 2.0"
|
40
|
+
spec.add_development_dependency "standard", "~> 1.3.0"
|
41
|
+
spec.add_development_dependency "timecop", "~> 0.9"
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,260 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rspec-trace-formatter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Zach Thomae
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-12-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: opentelemetry-api
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: opentelemetry-exporter-otlp
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.20'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.20'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec-core
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: subprocess
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activesupport
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '6.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '6.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: bundler
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.1'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.1'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: lefthook
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.7.7
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.7.7
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: pry
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0.13'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0.13'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rake
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 13.0.6
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 13.0.6
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rspec
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '3.0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '3.0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rspec-github
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '2.3'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '2.3'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: rspec-snapshot
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - "~>"
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '2.0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - "~>"
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '2.0'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: standard
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - "~>"
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: 1.3.0
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - "~>"
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: 1.3.0
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: timecop
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - "~>"
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0.9'
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - "~>"
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '0.9'
|
209
|
+
description: Create traces from RSpec tests using OpenTelemetry or your own tracing
|
210
|
+
library
|
211
|
+
email:
|
212
|
+
- zach@thomae.co
|
213
|
+
executables:
|
214
|
+
- rspec-trace-consumer
|
215
|
+
extensions: []
|
216
|
+
extra_rdoc_files: []
|
217
|
+
files:
|
218
|
+
- ".github/workflows/ci.yml"
|
219
|
+
- ".gitignore"
|
220
|
+
- ".rspec"
|
221
|
+
- Gemfile
|
222
|
+
- LICENSE
|
223
|
+
- README.md
|
224
|
+
- Rakefile
|
225
|
+
- examples/example_spec.rb
|
226
|
+
- exe/rspec-trace-consumer
|
227
|
+
- lefthook.yml
|
228
|
+
- lib/rspec/trace.rb
|
229
|
+
- lib/rspec/trace/consumer.rb
|
230
|
+
- lib/rspec/trace/formatter.rb
|
231
|
+
- lib/rspec/trace/open_telemetry_formatter.rb
|
232
|
+
- lib/rspec/trace/version.rb
|
233
|
+
- rspec-trace-formatter.gemspec
|
234
|
+
homepage: https://github.com/zthomae/rspec-trace-formatter
|
235
|
+
licenses:
|
236
|
+
- MIT
|
237
|
+
metadata:
|
238
|
+
allowed_push_host: https://rubygems.org
|
239
|
+
homepage_uri: https://github.com/zthomae/rspec-trace-formatter
|
240
|
+
source_code_uri: https://github.com/zthomae/rspec-trace-formatter
|
241
|
+
post_install_message:
|
242
|
+
rdoc_options: []
|
243
|
+
require_paths:
|
244
|
+
- lib
|
245
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
246
|
+
requirements:
|
247
|
+
- - ">="
|
248
|
+
- !ruby/object:Gem::Version
|
249
|
+
version: 2.5.0
|
250
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
251
|
+
requirements:
|
252
|
+
- - ">="
|
253
|
+
- !ruby/object:Gem::Version
|
254
|
+
version: '0'
|
255
|
+
requirements: []
|
256
|
+
rubygems_version: 3.1.6
|
257
|
+
signing_key:
|
258
|
+
specification_version: 4
|
259
|
+
summary: Formatter for RSpec to represent test runs as trace events
|
260
|
+
test_files: []
|