dat-science 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -85,6 +85,19 @@ After creating a subclass, tell `Dat::Science` to instantiate it any time the
85
85
  Dat::Science.experiment = MyApp::Experiment
86
86
  ```
87
87
 
88
+ ### Controlling comparison
89
+
90
+ By default the results of the `candidate` and `control` blocks are compared
91
+ with `==`. Use `comparator` to do something more fancy:
92
+
93
+ ```ruby
94
+ science "loose-comparison" do |e|
95
+ e.control { "vmg" }
96
+ e.candidate { "VMG" }
97
+ e.comparator { |a, b| a.downcase == b.downcase }
98
+ end
99
+ ```
100
+
88
101
  ### Ramping up experiments
89
102
 
90
103
  By default the `candidate` block of an experiment will run 100% of the time.
@@ -173,9 +186,32 @@ end
173
186
  `context` takes a Symbol-keyed Hash of additional information to publish and
174
187
  merges it with the default payload.
175
188
 
189
+ #### Keeping it clean
190
+
191
+ Sometimes the things you're comparing can be huge, and there's no good way
192
+ to do science against something simpler. Use a `cleaner` to publish a
193
+ simple version of a big nasty object graph:
194
+
195
+ ```ruby
196
+ science "huge-results" do |e|
197
+ e.control { OldAndBusted.huge_results_for query }
198
+ e.candidate { NewHotness.huge_results_for query }
199
+ e.cleaner { |result| result.count }
200
+ end
201
+ ```
202
+
203
+ The results of the `control` and `candidate` blocks will be run through the
204
+ `cleaner` You could get the same behavior by calling `count` in the blocks,
205
+ but the `cleaner` makes it easier to keep things in sync. The original
206
+ `control` result is still returned.
207
+
176
208
  ## Hacking on science
177
209
 
178
210
  Be on a Unixy box. Make sure a modern Bundler is available. `script/test` runs
179
211
  the unit tests. All development dependencies will be installed automatically if
180
212
  they're not available. Dat science happens primarily on Ruby 1.9.3 and 1.8.7,
181
213
  but science should be universal.
214
+
215
+ ## Maintainers
216
+
217
+ [@jbarnette](https://github.com/jbarnette) and [@rick](https://github.com/rick)
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = "dat-science"
3
- gem.version = "1.1.0"
3
+ gem.version = "1.2.0"
4
4
  gem.authors = ["John Barnette", "Rick Bradley"]
5
5
  gem.email = ["jbarnette@github.com"]
6
6
  gem.description = "Gradually test, measure, and track refactored code."
@@ -13,6 +13,8 @@ module Dat
13
13
  # optional `block` if it's provided.
14
14
  def initialize(name, &block)
15
15
  @candidate = nil
16
+ @cleaner = lambda { |r| r }
17
+ @comparator = lambda { |a, b| a == b }
16
18
  @context = { :experiment => name }
17
19
  @control = nil
18
20
  @name = name
@@ -27,6 +29,38 @@ module Dat
27
29
  @candidate
28
30
  end
29
31
 
32
+ # Internal: Run the cleaner on a value.
33
+ def clean(value)
34
+ cleaner.call value
35
+ end
36
+
37
+ # Public: Declare a cleaner `block` to scrub the result before it's
38
+ # published. `block` is called twice, once with the result of
39
+ # the control behavior and once with the result of the candidate.
40
+ # Exceptions during cleaning are treated as if they were raised
41
+ # in a candidate or control behavior block: They're reported as part
42
+ # of the result.
43
+ #
44
+ # Returns `block`.
45
+ def cleaner(&block)
46
+ @cleaner = block if block
47
+ @cleaner
48
+ end
49
+
50
+ # Internal: Run the comparator on two values.
51
+ def compare(a, b)
52
+ comparator.call a, b
53
+ end
54
+
55
+ # Public: Declare a comparator `block`. Results are compared with
56
+ # `==` by default.
57
+ #
58
+ # Returns `block`.
59
+ def comparator(&block)
60
+ @comparator = block if block
61
+ @comparator
62
+ end
63
+
30
64
  # Public: Add a Hash of `payload` data to be included when events are
31
65
  # published or returns the current context if `payload` is `nil`.
32
66
  def context(payload = nil)
@@ -107,7 +141,7 @@ module Dat
107
141
  end
108
142
 
109
143
  duration = (Time.now - start) * 1000
110
- Science::Result.new value, duration, raised
144
+ Science::Result.new self, value, duration, raised
111
145
  end
112
146
 
113
147
 
@@ -5,18 +5,20 @@ module Dat
5
5
  class Result
6
6
  attr_reader :duration
7
7
  attr_reader :exception
8
+ attr_reader :experiment
8
9
  attr_reader :value
9
10
 
10
- def initialize(value, duration, exception)
11
- @duration = duration
12
- @exception = exception
13
- @value = value
11
+ def initialize(experiment, value, duration, exception)
12
+ @duration = duration
13
+ @exception = exception
14
+ @experiment = experiment
15
+ @value = value
14
16
  end
15
17
 
16
18
  def ==(other)
17
19
  return false unless other.is_a? Dat::Science::Result
18
20
 
19
- values_are_equal = other.value == value
21
+ values_are_equal = experiment.compare(other.value, value)
20
22
  both_raised = other.raised? && raised?
21
23
  neither_raised = !other.raised? && !raised?
22
24
 
@@ -33,7 +35,11 @@ module Dat
33
35
  end
34
36
 
35
37
  def payload
36
- { :duration => duration, :exception => exception, :value => value }
38
+ {
39
+ :duration => duration,
40
+ :exception => exception,
41
+ :value => experiment.clean(value)
42
+ }
37
43
  end
38
44
 
39
45
  def raised?
@@ -36,6 +36,41 @@ class DatScienceExperimentTest < MiniTest::Unit::TestCase
36
36
  assert_same b, e.candidate
37
37
  end
38
38
 
39
+ def test_cleaner
40
+ e = Experiment.new "foo"
41
+ e.control { "bar" }
42
+ e.candidate { "baz" }
43
+ e.cleaner { |value| value.upcase }
44
+
45
+ e.run
46
+
47
+ event, payload = Experiment.published.first
48
+
49
+ assert_equal "BAR", payload[:control][:value]
50
+ assert_equal "BAZ", payload[:candidate][:value]
51
+ end
52
+
53
+ def test_cleaner_still_returns_unclean_result
54
+ e = Experiment.new "foo"
55
+ e.control { "bar" }
56
+ e.candidate { "baz" }
57
+ e.cleaner { |v| v.upcase }
58
+
59
+ assert_equal "bar", e.run
60
+ end
61
+
62
+ def test_comparator
63
+ e = Experiment.new "foo"
64
+ e.control { "bar" }
65
+ e.candidate { "bar" }
66
+ e.comparator { |a, b| false }
67
+
68
+ e.run
69
+
70
+ event, payload = Experiment.published.first
71
+ assert_equal :mismatch, event
72
+ end
73
+
39
74
  def test_context_default
40
75
  e = Experiment.new "foo"
41
76
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dat-science
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2013-02-26 00:00:00.000000000 Z
13
+ date: 2013-03-14 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: minitest