dat-science 0.0.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.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ /*.gem
2
+ /.bundle
3
+ /.ruby-version
4
+ /Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 GitHub, Inc.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # Dat Science!
2
+
3
+ A Ruby library for carefully refactoring critical paths. Science isn't
4
+ a feature flipper or an A/B testing tool, it's a pattern that helps
5
+ measure and validate large code changes without altering behavior.
6
+
7
+ ## How do I do science?
8
+
9
+ ```ruby
10
+ require "dat/science"
11
+
12
+ include Dat::Science
13
+
14
+ science "user-permissions" do |experiment|
15
+ experiment.control { model.check_user(user).valid? }
16
+ experiment.candidate { user.can? :read, model }
17
+ end
18
+ ```
19
+
20
+ Wrap a `control` block around the code's original behavior, and wrap
21
+ `candidate` around the new behavior. The `science` block will return
22
+ whatever the `control` block returns, but it does a bunch of stuff
23
+ behind the scenes:
24
+
25
+ * Decides whether or not to run `candidate`,
26
+ * Runs `candidate` before `control` 50% of the time,
27
+ * Measures the duration of both behaviors,
28
+ * Compares the results of both behaviors,
29
+ * Swallows any exceptions raised by the candidate behavior, and
30
+ * Publishes all this information for tracking and reporting.
31
+
32
+ ## Making Science Useful
33
+
34
+ (Talk about subclassing `Dat::Science::Experiment` and setting
35
+ `Dat::Science.experiment`)
36
+
37
+ ```ruby
38
+ require "dat/science"
39
+
40
+ module FooCorp
41
+ class Experiment < Dat::Science::Experiment
42
+ def enabled?
43
+ # See "Ramping up Experiments" below.
44
+ end
45
+
46
+ def publish(name, payload)
47
+ # See "Publishing Results" below.
48
+ end
49
+ end
50
+ end
51
+ ```
52
+
53
+ ```ruby
54
+ Dat::Science.experiment = FooCorp::Experiment
55
+ ```
56
+
57
+ ### Ramping up Experiments
58
+
59
+ ```ruby
60
+ def enabled?
61
+ rand(100) < 10
62
+ end
63
+ ```
64
+
65
+ ```ruby
66
+ def enabled?
67
+ Flipper[name].enabled?
68
+ end
69
+ ```
70
+
71
+ ### Publishing Results
72
+
73
+ ```ruby
74
+ def publish(name, payload)
75
+ FooCorp.instrument "science.#{name}", payload
76
+ end
77
+ ```
78
+
79
+ ```ruby
80
+ {
81
+ :candidate => {
82
+ :duration => 2.5,
83
+ :exception => nil,
84
+ :value => 42
85
+ },
86
+
87
+ :control => {
88
+ :duration => 25.0,
89
+ :exception => nil,
90
+ :value => 24
91
+ },
92
+
93
+ :first => :control
94
+ }
95
+ ```
96
+
97
+ #### Adding Context
98
+
99
+ (using `e.context`)
100
+
101
+ ## Hacking on Science
102
+
103
+ Make sure a modern Bundler is available.`script/test` runs the unit
104
+ tests. All development dependencies will be installed automatically if
105
+ they're not available. Dat science happens primarily on Ruby 1.9.3 and
106
+ 1.8.7, but science should be universal.
@@ -0,0 +1,17 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.name = "dat-science"
3
+ gem.version = "0.0.0"
4
+ gem.authors = ["John Barnette", "Rick Bradley"]
5
+ gem.email = ["jbarnette@github.com"]
6
+ gem.description = "Gradually test, measure, and track refactored code."
7
+ gem.summary = "SO BRAVE WITH SCIENCE."
8
+ gem.homepage = "https://github.com/github/dat-science"
9
+
10
+ gem.files = `git ls-files`.split $/
11
+ gem.executables = []
12
+ gem.test_files = gem.files.grep /^test/
13
+ gem.require_paths = ["lib"]
14
+
15
+ gem.add_development_dependency "minitest"
16
+ gem.add_development_dependency "mocha"
17
+ end
@@ -0,0 +1,33 @@
1
+ require "dat/science/experiment"
2
+
3
+ module Dat
4
+
5
+ # Public: Include this module if you like science.
6
+ module Science
7
+
8
+ # Public: Do some science.
9
+ def science(name, &block)
10
+ Science.experiment.new(name, &block).run
11
+ end
12
+
13
+ # Public: The Class to use for all `science` experiments. Default is
14
+ # `Dat::Science::Experiment`.
15
+ def self.experiment
16
+ @experiment ||= Dat::Science::Experiment
17
+ end
18
+
19
+ # Public: Set the Class to use for all `science` experiments.
20
+ # Returns `klass`.
21
+ def self.experiment=(klass)
22
+ @experiment = klass
23
+ end
24
+
25
+ # Internal: Reset any static configuration (primarily
26
+ # `Dat::Science.experiment`. Returns `self`.
27
+ def self.reset
28
+ @experiment = nil
29
+
30
+ self
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,135 @@
1
+ require "dat/science/result"
2
+
3
+ module Dat
4
+ module Science
5
+
6
+ # Public: Try things in code.
7
+ class Experiment
8
+
9
+ # Public: The name of this experiment.
10
+ attr_reader :name
11
+
12
+ # Public: Create a new experiment instance. `self` is yielded to
13
+ # an optional `block` if it's provided.
14
+ def initialize(name, &block)
15
+ @candidate = nil
16
+ @context = { :experiment => name }
17
+ @control = nil
18
+ @name = name
19
+
20
+ yield self if block_given?
21
+ end
22
+
23
+ # Public: Add a Hash of `payload` data to be included when events
24
+ # are published.
25
+ def context(payload)
26
+ @context.merge! payload
27
+ end
28
+
29
+ # Public: Declare the control behavior `block` for this
30
+ # experiment. Returns `block`.
31
+ def control(&block)
32
+ @control = block
33
+ end
34
+
35
+ # Public: Declare the candidate behavior `block` for this
36
+ # experiment. Returns `block`.
37
+ def candidate(&block)
38
+ @candidate = block
39
+ end
40
+
41
+ # Public: Run the control and candidate behaviors, timing each and
42
+ # comparing the results. The run order is randomized. Returns the
43
+ # control behavior's result.
44
+ #
45
+ # If the experiment is disabled or candidate behavior isn't
46
+ # provided the control behavior's result will be returned
47
+ # immediately.
48
+ def run
49
+ return run_control unless candidate? && enabled?
50
+
51
+ control_goes_first = rand(2) == 0
52
+
53
+ if control_goes_first
54
+ control = observe_control
55
+ candidate = observe_candidate
56
+ else
57
+ candidate = observe_candidate
58
+ control = observe_control
59
+ end
60
+
61
+ payload = {
62
+ :candidate => candidate.payload,
63
+ :control => control.payload,
64
+ :first => control_goes_first ? :control : :candidate
65
+ }
66
+
67
+ kind = control == candidate ? "match" : "mismatch"
68
+ publish_with_context kind, payload
69
+
70
+ raise control.exception if control.raised?
71
+
72
+ control.value
73
+ end
74
+
75
+ protected
76
+
77
+ # Internal: Does this experiment have candidate behavior?
78
+ def candidate?
79
+ !!@candidate
80
+ end
81
+
82
+ # Internal: Is this experiment enabled? More specifically, should
83
+ # the candidate behavior be run and compared to the control
84
+ # behavior? The default implementation returns `true`.
85
+ def enabled?
86
+ true
87
+ end
88
+
89
+ # Internal: Run `block`, measuring the duration and rescuing any
90
+ # raised exceptions. Returns a Dat::Science::Result.
91
+ def observe(&block)
92
+ start = Time.now
93
+
94
+ begin
95
+ value = block.call
96
+ rescue => ex
97
+ raised = ex
98
+ end
99
+
100
+ duration = (Time.now - start) * 1000
101
+ Science::Result.new value, duration, raised
102
+ end
103
+
104
+
105
+ # Internal. Returns a Dat::Science::Result for `candidate`.
106
+ def observe_candidate
107
+ observe { run_candidate }
108
+ end
109
+
110
+ # Internal. Returns a Dat::Science::Result for `control`.
111
+ def observe_control
112
+ observe { run_control }
113
+ end
114
+
115
+ # Internal: Broadcast an event `name` and `payload` Hash. The
116
+ # default implementation is a no-op. Returns nothing.
117
+ def publish(name, payload)
118
+ end
119
+
120
+ # Internal: Call `publish`, merging the `payload` with `context`.
121
+ def publish_with_context(name, payload)
122
+ publish name, @context.merge(payload)
123
+ end
124
+ # Internal: Run the candidate behavior and return its result.
125
+ def run_candidate
126
+ @candidate.call
127
+ end
128
+
129
+ # Internal: Run the control behavior and return its result.
130
+ def run_control
131
+ @control.call
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,44 @@
1
+ module Dat
2
+
3
+ # Internal. The value of a watched behavior.
4
+ module Science
5
+ class Result
6
+ attr_reader :duration
7
+ attr_reader :exception
8
+ attr_reader :value
9
+
10
+ def initialize(value, duration, exception)
11
+ @duration = duration
12
+ @exception = exception
13
+ @value = value
14
+ end
15
+
16
+ def ==(other)
17
+ return false unless other.is_a? Science::Result
18
+
19
+ values_are_equal = other.value == value
20
+ both_raised = other.raised? && raised?
21
+ neither_raised = !other.raised? && !raised?
22
+
23
+ exceptions_are_equivalent =
24
+ both_raised && other.exception.class == self.exception.class &&
25
+ other.exception.message == self.exception.message
26
+
27
+ (values_are_equal && neither_raised) ||
28
+ (both_raised && exceptions_are_equivalent)
29
+ end
30
+
31
+ def hash
32
+ exception ^ value
33
+ end
34
+
35
+ def payload
36
+ { :duration => duration, :exception => exception, :value => value }
37
+ end
38
+
39
+ def raised?
40
+ !!exception
41
+ end
42
+ end
43
+ end
44
+ end
data/script/bootstrap ADDED
@@ -0,0 +1,9 @@
1
+ #!/bin/sh
2
+ # Ensure local dependencies are available.
3
+
4
+ set -e
5
+
6
+ cd $(dirname "$0")/..
7
+ rm -rf .bundle/{binstubs,config}
8
+
9
+ bundle install --binstubs .bundle/binstubs --path .bundle --quiet "$@"
data/script/test ADDED
@@ -0,0 +1,9 @@
1
+ #!/bin/sh
2
+ # Run the unit tests.
3
+
4
+ set -e
5
+
6
+ cd $(dirname "$0")/..
7
+ script/bootstrap && ruby -I lib -r rubygems \
8
+ -e 'require "bundler/setup"' \
9
+ -e '(ARGV.empty? ? Dir["test/**/*_test.rb"] : ARGV).each { |f| load f }' -- "$@"
@@ -0,0 +1,8 @@
1
+ require "minitest/autorun"
2
+ require "dat/science/experiment"
3
+
4
+ class DatScienceExperimentTest < MiniTest::Unit::TestCase
5
+ def test_sanity
6
+ assert Dat::Science::Experiment
7
+ end
8
+ end
@@ -0,0 +1,32 @@
1
+ require "minitest/autorun"
2
+ require "mocha/setup"
3
+ require "dat/science"
4
+
5
+ class DatScienceTest < MiniTest::Unit::TestCase
6
+ def teardown
7
+ Dat::Science.reset
8
+ end
9
+
10
+ def test_experiment_default
11
+ assert_equal Dat::Science::Experiment, Dat::Science.experiment
12
+ end
13
+
14
+ def test_experiment
15
+ Dat::Science.experiment = :foo
16
+ assert_equal :foo, Dat::Science.experiment
17
+ end
18
+
19
+ def test_science
20
+ experiment = mock do
21
+ expects(:run).returns 42
22
+ end
23
+
24
+ Dat::Science.experiment.expects(:new).with("foo").returns experiment
25
+
26
+ obj = Object.new
27
+ obj.extend Dat::Science
28
+
29
+ ret = obj.science "foo"
30
+ assert_equal 42, ret
31
+ end
32
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dat-science
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - John Barnette
9
+ - Rick Bradley
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2013-02-26 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: minitest
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
31
+ - !ruby/object:Gem::Dependency
32
+ name: mocha
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ! '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ type: :development
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ description: Gradually test, measure, and track refactored code.
48
+ email:
49
+ - jbarnette@github.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - .gitignore
55
+ - Gemfile
56
+ - LICENSE.txt
57
+ - README.md
58
+ - dat-science.gemspec
59
+ - lib/dat/science.rb
60
+ - lib/dat/science/experiment.rb
61
+ - lib/dat/science/result.rb
62
+ - script/bootstrap
63
+ - script/test
64
+ - test/dat_science_experiment_test.rb
65
+ - test/dat_science_test.rb
66
+ homepage: https://github.com/github/dat-science
67
+ licenses: []
68
+ post_install_message:
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 1.8.23
87
+ signing_key:
88
+ specification_version: 3
89
+ summary: SO BRAVE WITH SCIENCE.
90
+ test_files:
91
+ - test/dat_science_experiment_test.rb
92
+ - test/dat_science_test.rb