airbrake-ruby 6.0.2-java → 6.1.0-java

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 99722e48e1e7d0c25b7daa374be62e44eba21cde2029d4994a80110a5f5909fb
4
- data.tar.gz: 6f5e8d2411eee4e18ff97ef7d89b3c4a1c8ebb3ba5ec14fafe24c25e850672b1
3
+ metadata.gz: 43fd8f74ef78e82c2ed46c8dc3ecd389a6c6ffcbdb8b20b22f1bf02e2ce4345d
4
+ data.tar.gz: 63c8aa81c33948f26ead281e81769a274dbca9740880a420d62b4d951b352984
5
5
  SHA512:
6
- metadata.gz: 1700009af941919c7c755536affac726c452e03f0d064a9c69a478cf7757a0b6b1ced42e87bd7f6bbc15a3bbdec538704621dc46fed97b5295a36be8d6bb2cef
7
- data.tar.gz: f8b22c23d2534b1f4dc12dd1b5d8bf970504b297a61421cda6d496ec2f1143ac9eb031e2c1b033e8d8a24b94b634c2a17ea475f9ef13d482be08a419a54b1d98
6
+ metadata.gz: 1e2f56b1d4af8f98cbb2c8026fc3b2f06178cff6be5fcfe42117693038be80a27513c6b235dc8b8a47900ee66aed4cadc5c0fd4e7c442fcf0f10af97b6df5a5b
7
+ data.tar.gz: 59bf1fd79c2467401aabc0c352397d82bae83805cb54ccac40c5ca132dfcfdefa5281a8ba11dc96f67d569405837af61aeb05a22ce9e2f3425d0ecc4a1c69251
@@ -43,8 +43,14 @@ module Airbrake
43
43
  def process_remote_configuration
44
44
  return unless @config.remote_config
45
45
  return unless @project_id
46
+
47
+ # Never poll remote configuration in the test environment.
46
48
  return if @config.environment == 'test'
47
49
 
50
+ # If the current environment is ignored, don't try to poll remote
51
+ # configuration.
52
+ return if @config.ignore_environments.include?(@config.environment)
53
+
48
54
  RemoteSettings.poll(@project_id, @config.remote_config_host) do |data|
49
55
  @poll_callback.call(data)
50
56
  end
@@ -49,7 +49,13 @@ module Airbrake
49
49
  def detect_git_version
50
50
  return unless which('git')
51
51
 
52
- Gem::Version.new(`git --version`.split[2])
52
+ begin
53
+ Gem::Version.new(`git --version`.split[2])
54
+ rescue Errno::EAGAIN
55
+ # Bugfix for the case when the system cannot allocate memory for
56
+ # a fork() call: https://github.com/airbrake/airbrake-ruby/issues/680
57
+ nil
58
+ end
53
59
  end
54
60
 
55
61
  # Cross-platform way to tell if an executable is accessible.
@@ -3,7 +3,7 @@
3
3
  module Airbrake
4
4
  # @return [String] the library version
5
5
  # @api public
6
- AIRBRAKE_RUBY_VERSION = '6.0.2'.freeze
6
+ AIRBRAKE_RUBY_VERSION = '6.1.0'.freeze
7
7
 
8
8
  # @return [Hash{Symbol=>String}] the information about the notifier library
9
9
  # @since v5.0.0
@@ -11,13 +11,13 @@ RSpec.describe Airbrake::Benchmark do
11
11
  before { benchmark }
12
12
 
13
13
  context "when called one time" do
14
- its(:stop) { is_expected.to eq(true) }
14
+ its(:stop) { is_expected.to be(true) }
15
15
  end
16
16
 
17
17
  context "when called twice or more" do
18
18
  before { benchmark.stop }
19
19
 
20
- its(:stop) { is_expected.to eq(false) }
20
+ its(:stop) { is_expected.to be(false) }
21
21
  end
22
22
  end
23
23
 
@@ -7,7 +7,7 @@ RSpec.describe Airbrake::Config::Processor do
7
7
  context "when there ARE blocklist keys" do
8
8
  it "adds the blocklist filter" do
9
9
  described_class.new(config).process_blocklist(notifier)
10
- expect(notifier.has_filter?(Airbrake::Filters::KeysBlocklist)).to eq(true)
10
+ expect(notifier.has_filter?(Airbrake::Filters::KeysBlocklist)).to be(true)
11
11
  end
12
12
  end
13
13
 
@@ -17,7 +17,7 @@ RSpec.describe Airbrake::Config::Processor do
17
17
  it "doesn't add the blocklist filter" do
18
18
  described_class.new(config).process_blocklist(notifier)
19
19
  expect(notifier.has_filter?(Airbrake::Filters::KeysBlocklist))
20
- .to eq(false)
20
+ .to be(false)
21
21
  end
22
22
  end
23
23
  end
@@ -28,7 +28,7 @@ RSpec.describe Airbrake::Config::Processor do
28
28
  context "when there ARE allowlist keys" do
29
29
  it "adds the allowlist filter" do
30
30
  described_class.new(config).process_allowlist(notifier)
31
- expect(notifier.has_filter?(Airbrake::Filters::KeysAllowlist)).to eq(true)
31
+ expect(notifier.has_filter?(Airbrake::Filters::KeysAllowlist)).to be(true)
32
32
  end
33
33
  end
34
34
 
@@ -38,7 +38,7 @@ RSpec.describe Airbrake::Config::Processor do
38
38
  it "doesn't add the allowlist filter" do
39
39
  described_class.new(config).process_allowlist(notifier)
40
40
  expect(notifier.has_filter?(Airbrake::Filters::KeysAllowlist))
41
- .to eq(false)
41
+ .to be(false)
42
42
  end
43
43
  end
44
44
  end
@@ -68,6 +68,22 @@ RSpec.describe Airbrake::Config::Processor do
68
68
  end
69
69
  end
70
70
 
71
+ context "when the config sets :ignore_environments and :environment matches" do
72
+ let(:config) do
73
+ Airbrake::Config.new(
74
+ project_id: 123,
75
+ ignore_environments: %w[dev],
76
+ environment: 'dev',
77
+ )
78
+ end
79
+
80
+ it "doesn't set remote settings" do
81
+ described_class.new(config).process_remote_configuration
82
+
83
+ expect(Airbrake::RemoteSettings).not_to have_received(:poll)
84
+ end
85
+ end
86
+
71
87
  context "when the config defines a project_id" do
72
88
  let(:config) do
73
89
  Airbrake::Config.new(project_id: 123, environment: 'not-test')
@@ -98,25 +114,25 @@ RSpec.describe Airbrake::Config::Processor do
98
114
  it "adds RootDirectoryFilter" do
99
115
  described_class.new(config).add_filters(notifier)
100
116
  expect(notifier.has_filter?(Airbrake::Filters::RootDirectoryFilter))
101
- .to eq(true)
117
+ .to be(true)
102
118
  end
103
119
 
104
120
  it "adds GitRevisionFilter" do
105
121
  described_class.new(config).add_filters(notifier)
106
122
  expect(notifier.has_filter?(Airbrake::Filters::GitRevisionFilter))
107
- .to eq(true)
123
+ .to be(true)
108
124
  end
109
125
 
110
126
  it "adds GitRepositoryFilter" do
111
127
  described_class.new(config).add_filters(notifier)
112
128
  expect(notifier.has_filter?(Airbrake::Filters::GitRepositoryFilter))
113
- .to eq(true)
129
+ .to be(true)
114
130
  end
115
131
 
116
132
  it "adds GitLastCheckoutFilter" do
117
133
  described_class.new(config).add_filters(notifier)
118
134
  expect(notifier.has_filter?(Airbrake::Filters::GitLastCheckoutFilter))
119
- .to eq(true)
135
+ .to be(true)
120
136
  end
121
137
  end
122
138
 
@@ -126,25 +142,25 @@ RSpec.describe Airbrake::Config::Processor do
126
142
  it "doesn't add RootDirectoryFilter" do
127
143
  described_class.new(config).add_filters(notifier)
128
144
  expect(notifier.has_filter?(Airbrake::Filters::RootDirectoryFilter))
129
- .to eq(false)
145
+ .to be(false)
130
146
  end
131
147
 
132
148
  it "doesn't add GitRevisionFilter" do
133
149
  described_class.new(config).add_filters(notifier)
134
150
  expect(notifier.has_filter?(Airbrake::Filters::GitRevisionFilter))
135
- .to eq(false)
151
+ .to be(false)
136
152
  end
137
153
 
138
154
  it "doesn't add GitRepositoryFilter" do
139
155
  described_class.new(config).add_filters(notifier)
140
156
  expect(notifier.has_filter?(Airbrake::Filters::GitRepositoryFilter))
141
- .to eq(false)
157
+ .to be(false)
142
158
  end
143
159
 
144
160
  it "doesn't add GitLastCheckoutFilter" do
145
161
  described_class.new(config).add_filters(notifier)
146
162
  expect(notifier.has_filter?(Airbrake::Filters::GitLastCheckoutFilter))
147
- .to eq(false)
163
+ .to be(false)
148
164
  end
149
165
  end
150
166
  end
data/spec/config_spec.rb CHANGED
@@ -23,12 +23,12 @@ RSpec.describe Airbrake::Config do
23
23
  its(:timeout) { is_expected.to be_nil }
24
24
  its(:blocklist_keys) { is_expected.to be_empty }
25
25
  its(:allowlist_keys) { is_expected.to be_empty }
26
- its(:performance_stats) { is_expected.to eq(true) }
26
+ its(:performance_stats) { is_expected.to be(true) }
27
27
  its(:performance_stats_flush_period) { is_expected.to eq(15) }
28
- its(:query_stats) { is_expected.to eq(true) }
29
- its(:job_stats) { is_expected.to eq(true) }
30
- its(:error_notifications) { is_expected.to eq(true) }
31
- its(:remote_config) { is_expected.to eq(true) }
28
+ its(:query_stats) { is_expected.to be(true) }
29
+ its(:job_stats) { is_expected.to be(true) }
30
+ its(:error_notifications) { is_expected.to be(true) }
31
+ its(:remote_config) { is_expected.to be(true) }
32
32
 
33
33
  its(:remote_config_host) do
34
34
  is_expected.to eq('https://notifier-configs.airbrake.io')
@@ -101,14 +101,14 @@ RSpec.describe Airbrake::FilterChain do
101
101
  klass = Class.new
102
102
 
103
103
  filter_chain.add_filter(klass.new)
104
- expect(filter_chain.includes?(klass)).to eq(true)
104
+ expect(filter_chain.includes?(klass)).to be(true)
105
105
  end
106
106
  end
107
107
 
108
108
  context "when Proc filter class is included in the filter chain" do
109
109
  it "returns true" do
110
110
  filter_chain.add_filter(proc {})
111
- expect(filter_chain.includes?(Proc)).to eq(true)
111
+ expect(filter_chain.includes?(Proc)).to be(true)
112
112
  end
113
113
  end
114
114
 
@@ -117,7 +117,7 @@ RSpec.describe Airbrake::FilterChain do
117
117
  klass = Class.new
118
118
 
119
119
  filter_chain.add_filter(proc {})
120
- expect(filter_chain.includes?(klass)).to eq(false)
120
+ expect(filter_chain.includes?(klass)).to be(false)
121
121
  end
122
122
  end
123
123
  end
@@ -1,9 +1,7 @@
1
1
  RSpec.describe Airbrake::Filters::GitRepositoryFilter do
2
2
  subject(:git_repository_filter) { described_class.new('.') }
3
3
 
4
- let(:notice) do
5
- Airbrake::Notice.new(Airbrake::Config.new, AirbrakeTestError.new)
6
- end
4
+ let(:notice) { Airbrake::Notice.new(AirbrakeTestError.new) }
7
5
 
8
6
  describe "#initialize" do
9
7
  it "parses standard git version" do
@@ -23,6 +21,14 @@ RSpec.describe Airbrake::Filters::GitRepositoryFilter do
23
21
  .to receive(:`).and_return('git version 2.17.2 (Apple Git-113)')
24
22
  expect { git_repository_filter }.not_to raise_error
25
23
  end
24
+
25
+ context "when Errno::EAGAIN is raised when detecting git version" do
26
+ it "doesn't attach anything to context/repository" do
27
+ allow_any_instance_of(Kernel).to receive(:`).and_raise(Errno::EAGAIN)
28
+ git_repository_filter.call(notice)
29
+ expect(notice[:context][:repository]).to be_nil
30
+ end
31
+ end
26
32
  end
27
33
 
28
34
  context "when context/repository is defined" do
@@ -34,7 +40,7 @@ RSpec.describe Airbrake::Filters::GitRepositoryFilter do
34
40
  end
35
41
 
36
42
  context "when .git directory doesn't exist" do
37
- git_repository_filter { described_class.new('root/dir') }
43
+ subject(:git_repository_filter) { described_class.new('root/dir') }
38
44
 
39
45
  it "doesn't attach anything to context/repository" do
40
46
  git_repository_filter.call(notice)
@@ -45,15 +51,20 @@ RSpec.describe Airbrake::Filters::GitRepositoryFilter do
45
51
  context "when .git directory exists" do
46
52
  it "attaches context/repository" do
47
53
  git_repository_filter.call(notice)
48
- expect(notice[:context][:repository]).to eq(
49
- 'ssh://git@github.com/airbrake/airbrake-ruby.git',
54
+ expect(notice[:context][:repository]).to match(
55
+ 'github.com/airbrake/airbrake-ruby',
50
56
  )
51
57
  end
52
58
  end
53
59
 
54
60
  context "when git is not in PATH" do
61
+ let!(:path) { ENV['PATH'] }
62
+
63
+ before { ENV['PATH'] = '' }
64
+
65
+ after { ENV['PATH'] = path }
66
+
55
67
  it "does not attach context/repository" do
56
- ENV['PATH'] = ''
57
68
  git_repository_filter.call(notice)
58
69
  expect(notice[:context][:repository]).to be_nil
59
70
  end
@@ -42,13 +42,13 @@ RSpec.describe Airbrake::RemoteSettings::Callback do
42
42
  callback = described_class.new(config)
43
43
 
44
44
  callback.call(data)
45
- expect(config.error_notifications).to eq(false)
45
+ expect(config.error_notifications).to be(false)
46
46
 
47
47
  callback.call(data)
48
- expect(config.error_notifications).to eq(false)
48
+ expect(config.error_notifications).to be(false)
49
49
 
50
50
  callback.call(data)
51
- expect(config.error_notifications).to eq(false)
51
+ expect(config.error_notifications).to be(false)
52
52
  end
53
53
  # rubocop:enable RSpec/MultipleExpectations
54
54
  end
@@ -63,12 +63,12 @@ RSpec.describe Airbrake::RemoteSettings::Callback do
63
63
  allow(data).to receive(:error_notifications?).and_return(false)
64
64
 
65
65
  callback.call(data)
66
- expect(config.error_notifications).to eq(false)
66
+ expect(config.error_notifications).to be(false)
67
67
 
68
68
  allow(data).to receive(:error_notifications?).and_return(true)
69
69
 
70
70
  callback.call(data)
71
- expect(config.error_notifications).to eq(true)
71
+ expect(config.error_notifications).to be(true)
72
72
 
73
73
  expect(data).to have_received(:error_notifications?).twice
74
74
  end
@@ -86,13 +86,13 @@ RSpec.describe Airbrake::RemoteSettings::Callback do
86
86
  callback = described_class.new(config)
87
87
 
88
88
  callback.call(data)
89
- expect(config.performance_stats).to eq(false)
89
+ expect(config.performance_stats).to be(false)
90
90
 
91
91
  callback.call(data)
92
- expect(config.performance_stats).to eq(false)
92
+ expect(config.performance_stats).to be(false)
93
93
 
94
94
  callback.call(data)
95
- expect(config.performance_stats).to eq(false)
95
+ expect(config.performance_stats).to be(false)
96
96
  end
97
97
  # rubocop:enable RSpec/MultipleExpectations
98
98
  end
@@ -107,12 +107,12 @@ RSpec.describe Airbrake::RemoteSettings::Callback do
107
107
  allow(data).to receive(:performance_stats?).and_return(false)
108
108
 
109
109
  callback.call(data)
110
- expect(config.performance_stats).to eq(false)
110
+ expect(config.performance_stats).to be(false)
111
111
 
112
112
  allow(data).to receive(:performance_stats?).and_return(true)
113
113
 
114
114
  callback.call(data)
115
- expect(config.performance_stats).to eq(true)
115
+ expect(config.performance_stats).to be(true)
116
116
 
117
117
  expect(data).to have_received(:performance_stats?).twice
118
118
  end
@@ -123,7 +123,7 @@ RSpec.describe Airbrake::RemoteSettings::SettingsData do
123
123
 
124
124
  it "returns true" do
125
125
  expect(described_class.new(project_id, data).error_notifications?)
126
- .to eq(true)
126
+ .to be(true)
127
127
  end
128
128
  end
129
129
 
@@ -141,7 +141,7 @@ RSpec.describe Airbrake::RemoteSettings::SettingsData do
141
141
 
142
142
  it "returns false" do
143
143
  expect(described_class.new(project_id, data).error_notifications?)
144
- .to eq(false)
144
+ .to be(false)
145
145
  end
146
146
  end
147
147
  end
@@ -153,7 +153,7 @@ RSpec.describe Airbrake::RemoteSettings::SettingsData do
153
153
 
154
154
  it "returns true" do
155
155
  expect(described_class.new(project_id, data).error_notifications?)
156
- .to eq(true)
156
+ .to be(true)
157
157
  end
158
158
  end
159
159
  end
@@ -174,7 +174,7 @@ RSpec.describe Airbrake::RemoteSettings::SettingsData do
174
174
 
175
175
  it "returns true" do
176
176
  expect(described_class.new(project_id, data).performance_stats?)
177
- .to eq(true)
177
+ .to be(true)
178
178
  end
179
179
  end
180
180
 
@@ -192,7 +192,7 @@ RSpec.describe Airbrake::RemoteSettings::SettingsData do
192
192
 
193
193
  it "returns false" do
194
194
  expect(described_class.new(project_id, data).performance_stats?)
195
- .to eq(false)
195
+ .to be(false)
196
196
  end
197
197
  end
198
198
  end
@@ -204,7 +204,7 @@ RSpec.describe Airbrake::RemoteSettings::SettingsData do
204
204
 
205
205
  it "returns true" do
206
206
  expect(described_class.new(project_id, data).performance_stats?)
207
- .to eq(true)
207
+ .to be(true)
208
208
  end
209
209
  end
210
210
  end
@@ -77,8 +77,8 @@ RSpec.describe Airbrake::RemoteSettings do
77
77
  sleep(0.1)
78
78
  remote_settings.stop_polling
79
79
 
80
- expect(settings.error_notifications?).to eq(true)
81
- expect(settings.performance_stats?).to eq(false)
80
+ expect(settings.error_notifications?).to be(true)
81
+ expect(settings.performance_stats?).to be(false)
82
82
  expect(settings.interval).to eq(1)
83
83
  end
84
84
  # rubocop:enable RSpec/MultipleExpectations
@@ -15,7 +15,7 @@ RSpec.describe Airbrake::ThreadPool do
15
15
  it "returns true" do
16
16
  retval = thread_pool << 1
17
17
  thread_pool.close
18
- expect(retval).to eq(true)
18
+ expect(retval).to be(true)
19
19
  end
20
20
 
21
21
  it "performs work in background" do
@@ -44,7 +44,7 @@ RSpec.describe Airbrake::ThreadPool do
44
44
  it "returns false" do
45
45
  retval = full_thread_pool << 1
46
46
  full_thread_pool.close
47
- expect(retval).to eq(false)
47
+ expect(retval).to be(false)
48
48
  end
49
49
 
50
50
  it "discards tasks" do
@@ -22,7 +22,7 @@ RSpec.describe Airbrake::TimedTrace do
22
22
  describe "#start_span" do
23
23
  context "when called once" do
24
24
  it "returns true" do
25
- expect(timed_trace.start_span('operation')).to eq(true)
25
+ expect(timed_trace.start_span('operation')).to be(true)
26
26
  end
27
27
  end
28
28
 
@@ -30,7 +30,7 @@ RSpec.describe Airbrake::TimedTrace do
30
30
  before { timed_trace.start_span('operation') }
31
31
 
32
32
  it "returns false" do
33
- expect(timed_trace.start_span('operation')).to eq(false)
33
+ expect(timed_trace.start_span('operation')).to be(false)
34
34
  end
35
35
  end
36
36
 
@@ -38,7 +38,7 @@ RSpec.describe Airbrake::TimedTrace do
38
38
  before { timed_trace.start_span('operation') }
39
39
 
40
40
  it "returns true" do
41
- expect(timed_trace.start_span('another operation')).to eq(true)
41
+ expect(timed_trace.start_span('another operation')).to be(true)
42
42
  end
43
43
  end
44
44
 
@@ -54,7 +54,7 @@ RSpec.describe Airbrake::TimedTrace do
54
54
  describe "#stop_span" do
55
55
  context "when #start_span wasn't invoked" do
56
56
  it "returns false" do
57
- expect(timed_trace.stop_span('operation')).to eq(false)
57
+ expect(timed_trace.stop_span('operation')).to be(false)
58
58
  end
59
59
  end
60
60
 
@@ -62,7 +62,7 @@ RSpec.describe Airbrake::TimedTrace do
62
62
  before { timed_trace.start_span('operation') }
63
63
 
64
64
  it "returns true" do
65
- expect(timed_trace.stop_span('operation')).to eq(true)
65
+ expect(timed_trace.stop_span('operation')).to be(true)
66
66
  end
67
67
  end
68
68
 
@@ -74,15 +74,15 @@ RSpec.describe Airbrake::TimedTrace do
74
74
 
75
75
  context "and when stopping in LIFO order" do
76
76
  it "returns true for all spans" do
77
- expect(timed_trace.stop_span('another operation')).to eq(true)
78
- expect(timed_trace.stop_span('operation')).to eq(true)
77
+ expect(timed_trace.stop_span('another operation')).to be(true)
78
+ expect(timed_trace.stop_span('operation')).to be(true)
79
79
  end
80
80
  end
81
81
 
82
82
  context "and when stopping in FIFO order" do
83
83
  it "returns true for all spans" do
84
- expect(timed_trace.stop_span('operation')).to eq(true)
85
- expect(timed_trace.stop_span('another operation')).to eq(true)
84
+ expect(timed_trace.stop_span('operation')).to be(true)
85
+ expect(timed_trace.stop_span('another operation')).to be(true)
86
86
  end
87
87
  end
88
88
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: airbrake-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.2
4
+ version: 6.1.0
5
5
  platform: java
6
6
  authors:
7
7
  - Airbrake Technologies, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-10 00:00:00.000000000 Z
11
+ date: 2022-04-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rbtree-jruby
@@ -110,7 +110,7 @@ files:
110
110
  - spec/filters/exception_attributes_filter_spec.rb
111
111
  - spec/filters/gem_root_filter_spec.rb
112
112
  - spec/filters/git_last_checkout_filter_spec.rb
113
- - spec/filters/git_repository_filter.rb
113
+ - spec/filters/git_repository_filter_spec.rb
114
114
  - spec/filters/git_revision_filter_spec.rb
115
115
  - spec/filters/keys_allowlist_spec.rb
116
116
  - spec/filters/keys_blocklist_spec.rb
@@ -172,7 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
172
172
  - !ruby/object:Gem::Version
173
173
  version: '0'
174
174
  requirements: []
175
- rubygems_version: 3.2.15
175
+ rubygems_version: 3.2.32
176
176
  signing_key:
177
177
  specification_version: 4
178
178
  summary: Ruby notifier for https://airbrake.io
@@ -194,7 +194,7 @@ test_files:
194
194
  - spec/filters/exception_attributes_filter_spec.rb
195
195
  - spec/filters/gem_root_filter_spec.rb
196
196
  - spec/filters/git_last_checkout_filter_spec.rb
197
- - spec/filters/git_repository_filter.rb
197
+ - spec/filters/git_repository_filter_spec.rb
198
198
  - spec/filters/git_revision_filter_spec.rb
199
199
  - spec/filters/keys_allowlist_spec.rb
200
200
  - spec/filters/keys_blocklist_spec.rb