lab_tech 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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +323 -0
  4. data/Rakefile +30 -0
  5. data/app/models/lab_tech/application_record.rb +5 -0
  6. data/app/models/lab_tech/default_cleaner.rb +87 -0
  7. data/app/models/lab_tech/experiment.rb +190 -0
  8. data/app/models/lab_tech/observation.rb +40 -0
  9. data/app/models/lab_tech/percentile.rb +41 -0
  10. data/app/models/lab_tech/result.rb +130 -0
  11. data/app/models/lab_tech/speedup.rb +65 -0
  12. data/app/models/lab_tech/summary.rb +183 -0
  13. data/config/routes.rb +2 -0
  14. data/db/migrate/20190815192130_create_experiment_tables.rb +50 -0
  15. data/lib/lab_tech.rb +176 -0
  16. data/lib/lab_tech/engine.rb +6 -0
  17. data/lib/lab_tech/version.rb +3 -0
  18. data/lib/tasks/lab_tech_tasks.rake +4 -0
  19. data/spec/dummy/Rakefile +6 -0
  20. data/spec/dummy/app/assets/config/manifest.js +1 -0
  21. data/spec/dummy/app/assets/javascripts/application.js +14 -0
  22. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  23. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  24. data/spec/dummy/app/jobs/application_job.rb +2 -0
  25. data/spec/dummy/app/models/application_record.rb +3 -0
  26. data/spec/dummy/bin/bundle +3 -0
  27. data/spec/dummy/bin/rails +4 -0
  28. data/spec/dummy/bin/rake +4 -0
  29. data/spec/dummy/bin/setup +33 -0
  30. data/spec/dummy/bin/update +28 -0
  31. data/spec/dummy/config.ru +5 -0
  32. data/spec/dummy/config/application.rb +35 -0
  33. data/spec/dummy/config/boot.rb +5 -0
  34. data/spec/dummy/config/database.yml +25 -0
  35. data/spec/dummy/config/environment.rb +5 -0
  36. data/spec/dummy/config/environments/development.rb +46 -0
  37. data/spec/dummy/config/environments/production.rb +71 -0
  38. data/spec/dummy/config/environments/test.rb +36 -0
  39. data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
  40. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  41. data/spec/dummy/config/initializers/cors.rb +16 -0
  42. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  43. data/spec/dummy/config/initializers/inflections.rb +16 -0
  44. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  45. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  46. data/spec/dummy/config/locales/en.yml +33 -0
  47. data/spec/dummy/config/puma.rb +34 -0
  48. data/spec/dummy/config/routes.rb +3 -0
  49. data/spec/dummy/config/spring.rb +6 -0
  50. data/spec/dummy/db/schema.rb +52 -0
  51. data/spec/dummy/db/test.sqlite3 +0 -0
  52. data/spec/dummy/log/development.log +0 -0
  53. data/spec/dummy/log/test.log +1519 -0
  54. data/spec/examples.txt +79 -0
  55. data/spec/models/lab_tech/default_cleaner_spec.rb +32 -0
  56. data/spec/models/lab_tech/experiment_spec.rb +110 -0
  57. data/spec/models/lab_tech/percentile_spec.rb +85 -0
  58. data/spec/models/lab_tech/result_spec.rb +198 -0
  59. data/spec/models/lab_tech/speedup_spec.rb +133 -0
  60. data/spec/models/lab_tech/summary_spec.rb +325 -0
  61. data/spec/models/lab_tech_spec.rb +23 -0
  62. data/spec/rails_helper.rb +62 -0
  63. data/spec/spec_helper.rb +98 -0
  64. data/spec/support/misc_helpers.rb +7 -0
  65. metadata +238 -0
@@ -0,0 +1,133 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe LabTech::Speedup do
4
+ # Some quick reference calculations.
5
+ #
6
+ # baseline | comparison | time | factor | comment
7
+ # 2.0 | 1.0 | +1.0 | +2.0 |
8
+ # 2.0 | 1.5 | +0.5 | +1.333 |
9
+ # 2.0 | 2.0 | 0.0 | +0.0 | zero by definition
10
+ # 2.0 | 2.5 | -0.5 | -1.25 |
11
+ # 2.0 | 3.0 | -1.0 | -1.5 |
12
+ # 2.0 | 3.5 | -1.5 | -1.75 |
13
+ # 2.0 | 4.0 | -2.0 | -2.0 |
14
+
15
+ specify ".compute_time_delta" do
16
+ aggregate_failures do
17
+ expect( described_class.compute_time_delta( 2.0, 1.0 ) ).to be_within( 0.001 ).of( +1.0 )
18
+ expect( described_class.compute_time_delta( 2.0, 1.5 ) ).to be_within( 0.001 ).of( +0.5 )
19
+ expect( described_class.compute_time_delta( 2.0, 2.0 ) ).to be_within( 0.001 ).of( 0.0 )
20
+ expect( described_class.compute_time_delta( 2.0, 2.5 ) ).to be_within( 0.001 ).of( -0.5 )
21
+ expect( described_class.compute_time_delta( 2.0, 3.0 ) ).to be_within( 0.001 ).of( -1.0 )
22
+ expect( described_class.compute_time_delta( 2.0, 3.5 ) ).to be_within( 0.001 ).of( -1.5 )
23
+ expect( described_class.compute_time_delta( 2.0, 4.0 ) ).to be_within( 0.001 ).of( -2.0 )
24
+ end
25
+ end
26
+
27
+ specify ".compute_factor" do
28
+ aggregate_failures do
29
+ expect( described_class.compute_factor( 2.0, 1.0 ) ).to be_within( 0.001 ).of( +2.0 )
30
+ expect( described_class.compute_factor( 2.0, 1.5 ) ).to be_within( 0.001 ).of( +1.333 )
31
+ expect( described_class.compute_factor( 2.0, 2.0 ) ).to be_within( 0.001 ).of( 0.0 )
32
+ expect( described_class.compute_factor( 2.0, 2.5 ) ).to be_within( 0.001 ).of( -1.25 )
33
+ expect( described_class.compute_factor( 2.0, 3.0 ) ).to be_within( 0.001 ).of( -1.5 )
34
+ expect( described_class.compute_factor( 2.0, 3.5 ) ).to be_within( 0.001 ).of( -1.75 )
35
+ expect( described_class.compute_factor( 2.0, 4.0 ) ).to be_within( 0.001 ).of( -2.0 )
36
+ end
37
+ end
38
+
39
+ def new_speedup(baseline = nil, comparison = nil, time = nil, factor = nil)
40
+ described_class.new( baseline: baseline, comparison: comparison, time: time, factor: factor )
41
+ end
42
+
43
+ it "acts like a simple model when all attributes are provided" do
44
+ x = new_speedup( 2.0, 1.0, -1.0, 2.0 )
45
+
46
+ expect( x.baseline ).to eq( +2.0 )
47
+ expect( x.comparison ).to eq( +1.0 )
48
+ expect( x.time ).to eq( -1.0 )
49
+ expect( x.factor ).to eq( +2.0 )
50
+ end
51
+
52
+ it "cheerfully tolerates missing baseline and comparison" do
53
+ x = new_speedup( nil, nil, -1.0, 2.0 )
54
+
55
+ expect( x.baseline ).to be nil
56
+ expect( x.comparison ).to be nil
57
+ expect( x.time ).to eq( -1.0 )
58
+ expect( x.factor ).to eq( +2.0 )
59
+ end
60
+
61
+ it "computes time and factor if they're missing (and it has enough data to do so)" do
62
+ x = new_speedup( 2.0, 1.0, nil, nil )
63
+
64
+ expect( x.baseline ).to eq( +2.0 )
65
+ expect( x.comparison ).to eq( +1.0 )
66
+ expect( x.time ).to eq( +1.0 )
67
+ expect( x.factor ).to eq( +2.0 )
68
+ end
69
+
70
+ it "doesn't compute time and factor if baseline is missing" do
71
+ x = new_speedup( 2.0, nil, nil, nil )
72
+
73
+ expect( x.baseline ).to eq( +2.0 )
74
+ expect( x.comparison ).to be nil
75
+ expect( x.time ).to be nil
76
+ expect( x.factor ).to be nil
77
+ end
78
+
79
+ it "doesn't compute time and factor if comparison is missing" do
80
+ x = new_speedup( nil, 2.0, nil, nil )
81
+
82
+ expect( x.baseline ).to be nil
83
+ expect( x.comparison ).to eq( +2.0 )
84
+ expect( x.time ).to be nil
85
+ expect( x.factor ).to be nil
86
+ end
87
+
88
+ it "is Comparable" do
89
+ x = new_speedup( 2.0, 1.0 )
90
+ y = new_speedup( 2.0, 2.0 )
91
+ z = new_speedup( 2.0, 3.0 )
92
+
93
+ expect( [ x, z, y ].sort ).to eq( [ z, y, x ] )
94
+ end
95
+
96
+ it "is not valid if time is nil" do
97
+ x = new_speedup( nil, nil, -1.0, 2.0 )
98
+ allow( x ).to receive( :time ).and_return nil
99
+
100
+ expect( x ).to_not be_valid
101
+ end
102
+
103
+ it "is not valid if factor is nil" do
104
+ x = new_speedup( nil, nil, -1.0, 2.0 )
105
+ allow( x ).to receive( :factor ).and_return nil
106
+
107
+ expect( x ).to_not be_valid
108
+ end
109
+
110
+ it "is valid if time and factor are present (and can't be disproved)" do
111
+ x = new_speedup( nil, nil, -1.0, 2.0 )
112
+ expect( x.time ).to be_present # precondition check
113
+ expect( x.factor ).to be_present # precondition check
114
+
115
+ expect( x ).to be_valid
116
+ end
117
+
118
+ it "is not valid if time doesn't agree with timing data" do
119
+ x = new_speedup( 2.0, 1.0, nil, nil )
120
+ allow( x ).to receive( :time ).and_return( 42 )
121
+
122
+ expect( x ).to_not be_valid
123
+ end
124
+
125
+ it "is not valid if factor doesn't agree with timing data" do
126
+ x = new_speedup( 2.0, 1.0, nil, nil )
127
+ allow( x ).to receive( :factor ).and_return( 42 )
128
+
129
+ expect( x ).to_not be_valid
130
+ end
131
+
132
+ end
133
+
@@ -0,0 +1,325 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe LabTech::Summary do
4
+ let!(:experiment) { LabTech::Experiment.create(name: "wibble", percent_enabled: 100) }
5
+ let(:summary_text) { experiment.summary.to_s }
6
+
7
+ def record_experiment(cont: "foo", cand: "foo", speedup_factor: nil, baseline: 1.0, comparison: nil)
8
+ LabTech.publish_results_in_test_mode do
9
+
10
+ LabTech.science "wibble" do |e|
11
+ e.use { cont.respond_to?(:call) ? cont.call : cont }
12
+ e.try { cand.respond_to?(:call) ? cand.call : cand }
13
+ end
14
+
15
+ #######################################
16
+
17
+ ######## ##### ###### #####
18
+ ## ## ## ## ## ## ##
19
+ ## ## ## ## ## ## ##
20
+ ## ## ## ## ## ## ##
21
+ ## ## ## ## ## ## ##
22
+ ## ## ## ## ## ## ##
23
+ ## ##### ###### #####
24
+
25
+ #######################################
26
+ # TODO: use Scientist's fabricate_durations_for_testing_purposes to make
27
+ # the below comment (and code?) unnecessary
28
+ #######################################
29
+ # Don't bother stubbing Scientist's clock; you'll get the wrong results 50%
30
+ # of the time because it runs the `try` and `use` blocks in random order,
31
+ # and then you'll be very very confused.
32
+ if speedup_factor && comparison.nil?
33
+ baseline = baseline.to_f
34
+ comparison = \
35
+ case
36
+ when speedup_factor > 0 ; +1.0 * baseline / speedup_factor
37
+ when speedup_factor == 0 ; +1.0 * baseline
38
+ else ; -1.0 * baseline * speedup_factor
39
+ end
40
+ end
41
+
42
+ if baseline && comparison && speedup_factor.nil?
43
+ speedup_factor = LabTech::Speedup.compute_factor(baseline, comparison)
44
+ end
45
+
46
+ if baseline && comparison && speedup_factor
47
+ result = experiment.results.last
48
+ result.update_attributes({
49
+ control_duration: baseline,
50
+ candidate_duration: comparison,
51
+ speedup_factor: speedup_factor,
52
+ time_delta: baseline - comparison,
53
+ })
54
+
55
+ # Technically, we only needed to update the result... but for consistency, let's update the observations too.
56
+ result.control .update_attributes duration: baseline
57
+ result.candidates.first .update_attributes duration: comparison
58
+ end
59
+
60
+ end # LabTech.publish_results_in_test_mode do
61
+ end
62
+
63
+ def wtf
64
+ puts
65
+ puts "", "Experiment" ; tp experiment
66
+ puts "", "Results" ; tp experiment.results
67
+ puts "", "Observations" ; tp experiment.observations
68
+ puts
69
+ end
70
+
71
+ context "when there are no results" do
72
+ before do
73
+ expect( experiment.results ).to be_empty # precondition check
74
+ end
75
+
76
+ it "says there are no results" do
77
+ expect( summary_text ).to match( /No results for experiment/ )
78
+ end
79
+ end
80
+
81
+ context "when the only result is a mismatch" do
82
+ before do
83
+ record_experiment cont: "foo", cand: "bar"
84
+ end
85
+
86
+ it "reports the correct counts" do
87
+ aggregate_failures do
88
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) correct" )
89
+ expect( summary_text ).to include( "1 of 1 (100.00%) mismatched" )
90
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) timed out" )
91
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) raised errors" )
92
+ end
93
+ end
94
+ end
95
+
96
+ context "when the only result is an error" do
97
+ before do
98
+ record_experiment cont: "foo", cand: ->{ raise "nope" }
99
+ end
100
+
101
+ it "reports the correct counts" do
102
+ aggregate_failures do
103
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) correct" )
104
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) mismatched" )
105
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) timed out" )
106
+ expect( summary_text ).to include( "1 of 1 (100.00%) raised errors" )
107
+ end
108
+ end
109
+ end
110
+
111
+ context "when the only result is a timeout" do
112
+ before do
113
+ record_experiment cont: "foo", cand: ->{ raise Timeout::Error, "too slow" }
114
+ end
115
+
116
+ it "reports the correct counts" do
117
+ aggregate_failures do
118
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) correct" )
119
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) mismatched" )
120
+ expect( summary_text ).to include( "1 of 1 (100.00%) timed out" )
121
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) raised errors" )
122
+ end
123
+ end
124
+ end
125
+
126
+ context "when there are correct results that somehow lack any timing data" do
127
+ before do
128
+ record_experiment
129
+ experiment.results.update_all time_delta: nil, speedup_factor: nil
130
+ end
131
+
132
+ it "reports the correct counts" do
133
+ aggregate_failures do
134
+ expect( summary_text ).to include( "1 of 1 (100.00%) correct" )
135
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) mismatched" )
136
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) timed out" )
137
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) raised errors" )
138
+ end
139
+ end
140
+
141
+ it "doesn't try to print the big table thingy" do
142
+ expect( summary_text ).to_not include( "Time deltas/speedups:" )
143
+ end
144
+ end
145
+
146
+ describe "when there are correct results that include timing data" do
147
+ def expect_percentile_line(percentile, *expected_strings)
148
+ line = summary_text.lines.detect { |e| e =~ /\s#{percentile.to_i}%/ }
149
+ aggregate_failures do
150
+ expected_strings.each do |string|
151
+ expect( line ).to include( string )
152
+ end
153
+ end
154
+ end
155
+
156
+ context "with a speedup factor of 0x (yawn)" do
157
+ before do
158
+ record_experiment speedup_factor: 0
159
+
160
+ # Make sure we got the math right there...
161
+ result = experiment.results.first
162
+ aggregate_failures do
163
+ expect( result.control.duration ).to be_within( 0.001 ).of( 1.0 )
164
+ expect( result.candidates.first.duration ).to be_within( 0.001 ).of( 1.0 )
165
+ end
166
+ end
167
+
168
+ it "reports the correct counts" do
169
+ aggregate_failures do
170
+ expect( summary_text ).to include( "1 of 1 (100.00%) correct" )
171
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) mismatched" )
172
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) timed out" )
173
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) raised errors" )
174
+ end
175
+ end
176
+
177
+ it "prints the stats visualization, including the correct speedup factor" do
178
+ expect_percentile_line( 50, "+0.0x" )
179
+ end
180
+ end
181
+
182
+ context "with a speedup factor of 10x (yay!)" do
183
+ before do
184
+ record_experiment speedup_factor: 10
185
+
186
+ # Make sure we got the math right there...
187
+ result = experiment.results.first
188
+ aggregate_failures do
189
+ expect( result.control.duration ).to be_within( 0.001 ).of( 1.0 )
190
+ expect( result.candidates.first.duration ).to be_within( 0.001 ).of( 0.1 )
191
+ end
192
+ end
193
+
194
+ it "prints the stats visualization, including the correct speedup factor" do
195
+ expect_percentile_line( 50, "+10.0x" )
196
+ end
197
+ end
198
+
199
+ context "with a speedup factor of -10x (boo!)" do
200
+ before do
201
+ record_experiment speedup_factor: -10
202
+
203
+ # Make sure we got the math right there...
204
+ result = experiment.results.first
205
+ aggregate_failures do
206
+ expect( result.control.duration ).to be_within( 0.001 ).of( 1.0 )
207
+ expect( result.candidates.first.duration ).to be_within( 0.001 ).of( 10.0 )
208
+ end
209
+ end
210
+
211
+ it "prints the stats visualization, including the correct speedup factor" do
212
+ expect_percentile_line( 50, "-10.0x" )
213
+ end
214
+ end
215
+
216
+ context "with multiple results and different speedups" do
217
+ before do
218
+ record_experiment speedup_factor: -10
219
+ record_experiment speedup_factor: -2
220
+ record_experiment speedup_factor: 0
221
+ record_experiment speedup_factor: 2
222
+ record_experiment speedup_factor: 10
223
+ end
224
+
225
+ it "reports the correct counts" do
226
+ aggregate_failures do
227
+ expect( summary_text ).to include( "5 of 5 (100.00%) correct" )
228
+ expect( summary_text ).to_not include( "0 of 5 (0.00%) mismatched" )
229
+ expect( summary_text ).to_not include( "0 of 5 (0.00%) timed out" )
230
+ expect( summary_text ).to_not include( "0 of 5 (0.00%) raised errors" )
231
+ end
232
+ end
233
+
234
+ it "reports median time deltas, as well as 5th & 95th percentiles, on their own line" do
235
+ time_delta_line = summary_text.lines.detect { |e| e =~ /Median time delta/i }
236
+ expect( time_delta_line ).to be_present
237
+
238
+ expect( time_delta_line ).to include( "-9.000s" ) # 5th percentile
239
+ expect( time_delta_line ).to include( "+0.000s" ) # Median
240
+ expect( time_delta_line ).to include( "+0.900s" ) # 95th percentile
241
+ end
242
+
243
+ it "prints the stats visualization, including the correct speedup factor" do
244
+ # This is effectively acting as an integration test for the Array#percentile method we've monkeypatched in
245
+ aggregate_failures do
246
+ expect_percentile_line( 0, "-10.0x" )
247
+ expect_percentile_line( 20, "-10.0x" )
248
+
249
+ expect_percentile_line( 25, "-2.0x" )
250
+ expect_percentile_line( 40, "-2.0x" )
251
+
252
+ expect_percentile_line( 45, "+0.0x" )
253
+ expect_percentile_line( 60, "+0.0x" )
254
+
255
+ expect_percentile_line( 65, "+2.0x" )
256
+ expect_percentile_line( 80, "+2.0x" )
257
+
258
+ expect_percentile_line( 85, "+10.0x" )
259
+ expect_percentile_line(100, "+10.0x" )
260
+ end
261
+ end
262
+ end
263
+
264
+ context "real-world(ish) data that led to a scaling error" do
265
+ before do
266
+ record_experiment baseline: 1.7367, speedup_factor: 10.9099
267
+ record_experiment baseline: 0.0642, speedup_factor: -3.2183
268
+ record_experiment baseline: 0.0702, speedup_factor: -1.0906
269
+ record_experiment baseline: 0.0552, speedup_factor: 1.1123
270
+ record_experiment baseline: 0.0539, speedup_factor: 1.1808
271
+ record_experiment baseline: 0.0554, speedup_factor: -1.1269
272
+ end
273
+
274
+ it "renders properly" do
275
+ aggregate_failures do
276
+ expect_percentile_line( 0, "-3.2x" )
277
+ expect_percentile_line( 15, "-3.2x" )
278
+ expect_percentile_line( 20, "-1.1x" )
279
+ expect_percentile_line( 30, "-1.1x" )
280
+ expect_percentile_line( 35, "-1.1x" )
281
+ expect_percentile_line( 50, "-1.1x" )
282
+ expect_percentile_line( 55, "+1.1x" )
283
+ expect_percentile_line( 65, "+1.1x" )
284
+ expect_percentile_line( 70, "+1.2x" )
285
+ expect_percentile_line( 80, "+1.2x" )
286
+ expect_percentile_line( 85, "+10.9x" )
287
+ expect_percentile_line(100, "+10.9x" )
288
+ end
289
+
290
+ end
291
+ end
292
+
293
+ context "real-world(ish) data that led to a scaling error, part 2" do
294
+ before do
295
+ record_experiment baseline: 0.0030516 , comparison: 0.00306088
296
+ record_experiment baseline: 0.000261548 , comparison: 0.00220928
297
+ record_experiment baseline: 0.000781327 , comparison: 0.00279742
298
+ record_experiment baseline: 0.00201508 , comparison: 0.002386
299
+ record_experiment baseline: 0.000593603 , comparison: 0.00275979
300
+ record_experiment baseline: 0.000259521 , comparison: 0.0021131
301
+ record_experiment baseline: 0.000673067 , comparison: 0.00250636
302
+ record_experiment baseline: 0.00229586 , comparison: 0.00285059
303
+ record_experiment baseline: 0.002911 , comparison: 0.00275513
304
+ record_experiment baseline: 0.00275274 , comparison: 0.00251802
305
+ record_experiment baseline: 0.000236285 , comparison: 0.00198174
306
+ record_experiment baseline: 0.000225291 , comparison: 0.00257419
307
+ record_experiment baseline: 0.000356831 , comparison: 0.00244557
308
+ record_experiment baseline: 0.000287118 , comparison: 0.00248476
309
+ record_experiment baseline: 0.000556486 , comparison: 0.00261352
310
+ record_experiment baseline: 0.00237066 , comparison: 0.00265087
311
+ record_experiment baseline: 0.00183386 , comparison: 0.00211302
312
+ record_experiment baseline: 0.00296087 , comparison: 0.00294441
313
+ record_experiment baseline: 0.00031988 , comparison: 0.00323599
314
+ end
315
+
316
+ it "renders properly" do
317
+ aggregate_failures do
318
+ expect_percentile_line( 0, "-11.4x" )
319
+ expect_percentile_line( 50, "-3.7x" )
320
+ expect_percentile_line(100, "+1.1x" )
321
+ end
322
+ end
323
+ end
324
+ end
325
+ end