rspec-trace-formatter 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|