xavier 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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +41 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/.yardstick.yml +31 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +73 -0
- data/README.md +92 -0
- data/Rakefile +21 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/xavier.rb +74 -0
- data/lib/xavier/mutation_strategies/class_copy.rb +21 -0
- data/lib/xavier/mutation_strategies/instance_copy.rb +21 -0
- data/lib/xavier/mutator.rb +46 -0
- data/lib/xavier/observer.rb +132 -0
- data/lib/xavier/state.rb +88 -0
- data/lib/xavier/states.rb +47 -0
- data/lib/xavier/version.rb +6 -0
- data/xavier.gemspec +37 -0
- metadata +192 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 497980d580ff1c8ec587dd58c3e645cf3a3544c4
|
4
|
+
data.tar.gz: 6b36d63bbafa06107025eb9072f24f987638f4bf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ca1e35db421d915af1e168df52aba56d49958633abb3513182709b2e97a376e17b8fd5e28a0e2339684c29f365995340358d71fae0d88d3cd335febfdf9f0907
|
7
|
+
data.tar.gz: c130c40a35ac7684a5bcadaa4d843fc98a2cfe89bc9da55faa9668b794ef5fe9ec0cf7eb334954a22f58808450d1c2b101b2d569353c183c09ac5e82d133ecf1
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require: rubocop-rspec
|
2
|
+
|
3
|
+
AllCops:
|
4
|
+
DisplayCopNames: true
|
5
|
+
|
6
|
+
# ---------------------- Layout -----------------------
|
7
|
+
|
8
|
+
Layout/SpaceInsideHashLiteralBraces:
|
9
|
+
Enabled: false
|
10
|
+
|
11
|
+
# ---------------------- Metrics ----------------------
|
12
|
+
|
13
|
+
Metrics/BlockLength:
|
14
|
+
Exclude:
|
15
|
+
- spec/**/*_spec.rb
|
16
|
+
- xavier.gemspec
|
17
|
+
|
18
|
+
Metrics/LineLength:
|
19
|
+
Max: 120
|
20
|
+
|
21
|
+
# ----------------------- RSpec -----------------------
|
22
|
+
|
23
|
+
RSpec/NestedGroups:
|
24
|
+
Enabled: false
|
25
|
+
|
26
|
+
# ----------------------- Style -----------------------
|
27
|
+
|
28
|
+
Style/FrozenStringLiteralComment:
|
29
|
+
Exclude:
|
30
|
+
- spec/**/*.rb
|
31
|
+
|
32
|
+
Style/ClassVars:
|
33
|
+
Exclude:
|
34
|
+
- spec/support/*.rb
|
35
|
+
|
36
|
+
# ----------------------- Naming ----------------------
|
37
|
+
|
38
|
+
Naming/UncommunicativeMethodParamName:
|
39
|
+
Exclude:
|
40
|
+
- lib/xavier/mutation_strategies/*.rb
|
41
|
+
- lib/xavier/mutator.rb
|
data/.travis.yml
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--plugin junk
|
data/.yardstick.yml
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
threshold: 100
|
2
|
+
rules:
|
3
|
+
ApiTag::Presence:
|
4
|
+
enabled: true
|
5
|
+
exclude: []
|
6
|
+
ApiTag::Inclusion:
|
7
|
+
enabled: true
|
8
|
+
exclude: []
|
9
|
+
ApiTag::ProtectedMethod:
|
10
|
+
enabled: true
|
11
|
+
exclude: []
|
12
|
+
ApiTag::PrivateMethod:
|
13
|
+
enabled: true
|
14
|
+
exclude: []
|
15
|
+
ExampleTag:
|
16
|
+
enabled: true
|
17
|
+
exclude: []
|
18
|
+
ReturnTag:
|
19
|
+
enabled: true
|
20
|
+
exclude: []
|
21
|
+
Summary::Presence:
|
22
|
+
enabled: true
|
23
|
+
exclude: []
|
24
|
+
Summary::Length:
|
25
|
+
enabled: false
|
26
|
+
Summary::Delimiter:
|
27
|
+
enabled: false
|
28
|
+
exclude: []
|
29
|
+
Summary::SingleLine:
|
30
|
+
enabled: true
|
31
|
+
exclude: []
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
5
|
+
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
6
|
+
|
7
|
+
## [Unreleased]
|
8
|
+
Nothing.
|
9
|
+
|
10
|
+
## 0.1.0 - 2018-03-17
|
11
|
+
### Added
|
12
|
+
- Initial version of the gem.
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
xavier (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
ast (2.4.0)
|
10
|
+
backports (3.11.1)
|
11
|
+
coderay (1.1.2)
|
12
|
+
diff-lcs (1.3)
|
13
|
+
method_source (0.9.0)
|
14
|
+
parallel (1.12.1)
|
15
|
+
parser (2.5.0.4)
|
16
|
+
ast (~> 2.4.0)
|
17
|
+
powerpack (0.1.1)
|
18
|
+
pry (0.11.3)
|
19
|
+
coderay (~> 1.1.0)
|
20
|
+
method_source (~> 0.9.0)
|
21
|
+
rainbow (3.0.0)
|
22
|
+
rake (10.5.0)
|
23
|
+
rspec (3.7.0)
|
24
|
+
rspec-core (~> 3.7.0)
|
25
|
+
rspec-expectations (~> 3.7.0)
|
26
|
+
rspec-mocks (~> 3.7.0)
|
27
|
+
rspec-core (3.7.1)
|
28
|
+
rspec-support (~> 3.7.0)
|
29
|
+
rspec-expectations (3.7.0)
|
30
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
31
|
+
rspec-support (~> 3.7.0)
|
32
|
+
rspec-mocks (3.7.0)
|
33
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
34
|
+
rspec-support (~> 3.7.0)
|
35
|
+
rspec-support (3.7.1)
|
36
|
+
rubocop (0.53.0)
|
37
|
+
parallel (~> 1.10)
|
38
|
+
parser (>= 2.5)
|
39
|
+
powerpack (~> 0.1)
|
40
|
+
rainbow (>= 2.2.2, < 4.0)
|
41
|
+
ruby-progressbar (~> 1.7)
|
42
|
+
unicode-display_width (~> 1.0, >= 1.0.1)
|
43
|
+
rubocop-rspec (1.24.0)
|
44
|
+
rubocop (>= 0.53.0)
|
45
|
+
ruby-progressbar (1.9.0)
|
46
|
+
tty-color (0.4.2)
|
47
|
+
unicode-display_width (1.3.0)
|
48
|
+
yard (0.9.12)
|
49
|
+
yard-junk (0.0.7)
|
50
|
+
backports
|
51
|
+
rainbow
|
52
|
+
tty-color
|
53
|
+
yard
|
54
|
+
yardstick (0.9.9)
|
55
|
+
yard (~> 0.8, >= 0.8.7.2)
|
56
|
+
|
57
|
+
PLATFORMS
|
58
|
+
ruby
|
59
|
+
|
60
|
+
DEPENDENCIES
|
61
|
+
bundler (~> 1.16)
|
62
|
+
pry (~> 0.11)
|
63
|
+
rake (~> 10.0)
|
64
|
+
rspec (~> 3.7)
|
65
|
+
rubocop (~> 0.53)
|
66
|
+
rubocop-rspec (~> 1.24)
|
67
|
+
xavier!
|
68
|
+
yard (~> 0.9)
|
69
|
+
yard-junk (~> 0.0.7)
|
70
|
+
yardstick (~> 0.9)
|
71
|
+
|
72
|
+
BUNDLED WITH
|
73
|
+
1.16.0
|
data/README.md
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# Xavier
|
2
|
+
|
3
|
+
Xavier tracks and reverts state mutations (changes in `instance`, `class`, and `class instance` variables).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'xavier'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install xavier
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Observing a class:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
class EvilSingleton
|
27
|
+
@@mutated = false
|
28
|
+
@mutated = false
|
29
|
+
|
30
|
+
def self.mutate
|
31
|
+
@@mutated = true
|
32
|
+
@mutated = true
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.mutated?
|
36
|
+
@@mutated && @mutated
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
Xavier.observe(EvilSingleton) do
|
41
|
+
EvilSingleton.mutated? # => false
|
42
|
+
EvilSingleton.mutate
|
43
|
+
EvilSingleton.mutated? # => true
|
44
|
+
end
|
45
|
+
|
46
|
+
EvilSingleton.mutated? # => false
|
47
|
+
```
|
48
|
+
|
49
|
+
Observing an instance:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
class InstanceSingleton
|
53
|
+
def initialize
|
54
|
+
@mutated = false
|
55
|
+
end
|
56
|
+
|
57
|
+
def mutate
|
58
|
+
@mutated = true
|
59
|
+
end
|
60
|
+
|
61
|
+
def mutated?
|
62
|
+
@mutated
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
evil_singleton = InstanceSingleton.new
|
67
|
+
|
68
|
+
Xavier.observe(evil_singleton) do
|
69
|
+
evil_singleton.mutated? # => false
|
70
|
+
evil_singleton.mutate
|
71
|
+
evil_singleton.mutated? # => true
|
72
|
+
end
|
73
|
+
|
74
|
+
evil_singleton.mutated? # => false
|
75
|
+
```
|
76
|
+
|
77
|
+
## Development
|
78
|
+
|
79
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
80
|
+
You can also run:
|
81
|
+
* `bin/console` for an interactive prompt that will allow you to experiment.
|
82
|
+
* `rake rubocop` to lint the code.
|
83
|
+
* `rake verify_measurements` to generate a report of the Yard documentation.
|
84
|
+
* `rake yard:junk` to lint the Yard documentation.
|
85
|
+
|
86
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
|
87
|
+
version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
|
88
|
+
push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
89
|
+
|
90
|
+
## Contributing
|
91
|
+
|
92
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/xavier.
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'yard/rake/yardoc_task'
|
6
|
+
require 'yard-junk/rake'
|
7
|
+
require 'yardstick/rake/measurement'
|
8
|
+
require 'yardstick/rake/verify'
|
9
|
+
|
10
|
+
yardstick_options = YAML.load_file('.yardstick.yml')
|
11
|
+
|
12
|
+
RSpec::Core::RakeTask.new(:spec)
|
13
|
+
YARD::Rake::YardocTask.new
|
14
|
+
YardJunk::Rake.define_task
|
15
|
+
Yardstick::Rake::Measurement.new(:yardstick_measure, yardstick_options)
|
16
|
+
Yardstick::Rake::Verify.new
|
17
|
+
|
18
|
+
# Remove the report on rake clobber
|
19
|
+
CLEAN.include('measurements')
|
20
|
+
|
21
|
+
task default: :spec
|
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'xavier'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/xavier.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'xavier/observer'
|
4
|
+
require 'xavier/version'
|
5
|
+
|
6
|
+
# Wraps the gem logic in an accessible way.
|
7
|
+
module Xavier
|
8
|
+
# Raised when attempting to observe a class or instance already under observation.
|
9
|
+
AlreadyObserved = Class.new(RuntimeError)
|
10
|
+
|
11
|
+
# Observes an object, yields a block and then reverts the observed object's class and instance variables.
|
12
|
+
#
|
13
|
+
# @example Observing a class
|
14
|
+
# class EvilSingleton
|
15
|
+
# @@mutated = false
|
16
|
+
# @mutated = false
|
17
|
+
#
|
18
|
+
# def self.mutate
|
19
|
+
# @@mutated = true
|
20
|
+
# @mutated = true
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# def self.mutated?
|
24
|
+
# @@mutated && @mutated
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# Xavier.observe(EvilSingleton) do
|
29
|
+
# EvilSingleton.mutated? # => false
|
30
|
+
# EvilSingleton.mutate
|
31
|
+
# EvilSingleton.mutated? # => true
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# EvilSingleton.mutated? # => false
|
35
|
+
#
|
36
|
+
# @example Observing an instance
|
37
|
+
# class InstanceSingleton
|
38
|
+
# def initialize
|
39
|
+
# @mutated = false
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# def mutate
|
43
|
+
# @mutated = true
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# def mutated?
|
47
|
+
# @mutated
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# evil_singleton = InstanceSingleton.new
|
52
|
+
#
|
53
|
+
# Xavier.observe(evil_singleton) do
|
54
|
+
# evil_singleton.mutated? # => false
|
55
|
+
# evil_singleton.mutate
|
56
|
+
# evil_singleton.mutated? # => true
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# evil_singleton.mutated? # => false
|
60
|
+
#
|
61
|
+
# @raise [AlreadyObserved] When attempting to observe the same object twice before the block returning.
|
62
|
+
#
|
63
|
+
# @param observable The object whose state should be observed. It can be a class or an instance.
|
64
|
+
#
|
65
|
+
# @yield The block to be executed before the observable's state is reverted.
|
66
|
+
#
|
67
|
+
# @return [Integer] The object_id of the observable.
|
68
|
+
#
|
69
|
+
# @api public
|
70
|
+
def self.observe(observable, &block)
|
71
|
+
@observer ||= Observer.new
|
72
|
+
@observer.observe(observable, &block)
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Xavier
|
4
|
+
module MutationStrategies
|
5
|
+
# Copies the class variables from one object to other.
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
module ClassCopy
|
9
|
+
# Copies the class variables from one object to other.
|
10
|
+
#
|
11
|
+
# @param from An object where the state will be copied from.
|
12
|
+
# @param to An object where the state will be copied to.
|
13
|
+
#
|
14
|
+
# @return [Array<String>] A list of variable names that were copied.
|
15
|
+
def self.copy(from:, to:)
|
16
|
+
vars = from.class_variables.map(&:to_s)
|
17
|
+
vars.each { |name| to.class_variable_set(name, from.class_variable_get(name)) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Xavier
|
4
|
+
module MutationStrategies
|
5
|
+
# Copies the instance variables from one object to other.
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
module InstanceCopy
|
9
|
+
# Copies the instance variables from one object to other.
|
10
|
+
#
|
11
|
+
# @param from An object where the state will be copied from.
|
12
|
+
# @param to An object where the state will be copied to.
|
13
|
+
#
|
14
|
+
# @return [Array<String>] A list of variable names that were copied.
|
15
|
+
def self.copy(from:, to:)
|
16
|
+
vars = from.instance_variables.map(&:to_s)
|
17
|
+
vars.each { |name| to.instance_variable_set(name, from.instance_variable_get(name)) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'xavier/state'
|
4
|
+
require 'xavier/mutation_strategies/class_copy'
|
5
|
+
require 'xavier/mutation_strategies/instance_copy'
|
6
|
+
|
7
|
+
module Xavier
|
8
|
+
# Applies and unapplies state mutations to objects.
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
class Mutator
|
12
|
+
# Returns a state representation from a given +observable+.
|
13
|
+
#
|
14
|
+
# @param observable The class or instance to copy the state from.
|
15
|
+
# @param [Array] strategies Array of mutation mutation_strategies that define how the state should be copied.
|
16
|
+
#
|
17
|
+
# @return [State] The state representation of the given +observable+
|
18
|
+
def create_state_from(observable, strategies:)
|
19
|
+
state = State.new(observable.object_id)
|
20
|
+
strategies.each { |strategy| strategy.copy(from: observable, to: state) }
|
21
|
+
state
|
22
|
+
end
|
23
|
+
|
24
|
+
# Applies the given +state+ to the given +observable+.
|
25
|
+
#
|
26
|
+
# @param from An object where the state will be copied from.
|
27
|
+
# @param to An object where the state will be copied to.
|
28
|
+
# @param [Array] strategies Array of mutation mutation_strategies that define how the state should be copied.
|
29
|
+
#
|
30
|
+
# @return [Array] An array of mutation_strategies.
|
31
|
+
def apply_state(from:, to:, strategies:)
|
32
|
+
strategies.each { |strategy| strategy.copy(from: from, to: to) }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Finds the mutation mutation_strategies for a given +observable+.
|
36
|
+
#
|
37
|
+
# @param observable Any class or instance
|
38
|
+
#
|
39
|
+
# @return [Array] An array of mutation mutation_strategies that define how the state should be copied.
|
40
|
+
def mutation_strategies_for(observable)
|
41
|
+
[MutationStrategies::InstanceCopy].tap do |strategies|
|
42
|
+
strategies.push(MutationStrategies::ClassCopy) if observable.is_a?(Class)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'xavier/mutator'
|
4
|
+
require 'xavier/states'
|
5
|
+
|
6
|
+
module Xavier
|
7
|
+
# Observes an object for state mutations.
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
class Observer
|
11
|
+
# Creates an instance of +Observer+.
|
12
|
+
#
|
13
|
+
# @param [Mutator] mutator Applies and unapplies state modifications.
|
14
|
+
# @param [States] states States of objects under observation. Empty by default.
|
15
|
+
def initialize(mutator = Mutator.new, states = States.new)
|
16
|
+
@states = states
|
17
|
+
@mutator = mutator
|
18
|
+
end
|
19
|
+
|
20
|
+
# Observes an object, yields a block and then reverts the observed object's class and instance variables.
|
21
|
+
#
|
22
|
+
# @example Observing a class
|
23
|
+
# class EvilSingleton
|
24
|
+
# @@mutated = false
|
25
|
+
# @mutated = false
|
26
|
+
#
|
27
|
+
# def self.mutate
|
28
|
+
# @@mutated = true
|
29
|
+
# @mutated = true
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# def self.mutated?
|
33
|
+
# @@mutated && @mutated
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# Xavier.observe(EvilSingleton) do
|
38
|
+
# EvilSingleton.mutated? # => false
|
39
|
+
# EvilSingleton.mutate
|
40
|
+
# EvilSingleton.mutated? # => true
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# EvilSingleton.mutated? # => false
|
44
|
+
#
|
45
|
+
# @example Observing an instance
|
46
|
+
# class InstanceSingleton
|
47
|
+
# def initialize
|
48
|
+
# @mutated = false
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# def mutate
|
52
|
+
# @mutated = true
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# def mutated?
|
56
|
+
# @mutated
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# evil_singleton = InstanceSingleton.new
|
61
|
+
#
|
62
|
+
# Xavier.observe(evil_singleton) do
|
63
|
+
# evil_singleton.mutated? # => false
|
64
|
+
# evil_singleton.mutate
|
65
|
+
# evil_singleton.mutated? # => true
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# evil_singleton.mutated? # => false
|
69
|
+
#
|
70
|
+
# @raise [AlreadyObserved] When attempting to observe the same object twice before the block returning.
|
71
|
+
#
|
72
|
+
# @param observable The object whose state should be observed. It can be a class or an instance.
|
73
|
+
#
|
74
|
+
# @yield The block to be executed before the observable's state is reverted.
|
75
|
+
#
|
76
|
+
# @return [Integer] The object_id of the observable.
|
77
|
+
#
|
78
|
+
# @api private
|
79
|
+
def observe(observable)
|
80
|
+
raise ArgumentError, 'This method expects a block. Without a block it is useless.' unless block_given?
|
81
|
+
raise AlreadyObserved, 'Objects can only be observed once per block.' if being_observed?(observable)
|
82
|
+
|
83
|
+
strategies = mutator.mutation_strategies_for(observable)
|
84
|
+
original_state = mutator.create_state_from(observable, strategies: strategies)
|
85
|
+
save_state(original_state)
|
86
|
+
|
87
|
+
yield
|
88
|
+
|
89
|
+
mutator.apply_state(from: original_state, to: observable, strategies: strategies)
|
90
|
+
delete_state(original_state)
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# Returns the state mutator.
|
96
|
+
#
|
97
|
+
# @return [Xavier::Mutator] Applies and unapplies state modifications.
|
98
|
+
attr_reader :mutator
|
99
|
+
|
100
|
+
# Returns the observed objects states.
|
101
|
+
#
|
102
|
+
# @return [Xavier::States] States of objects under observation. Used to revert the object to its original state.
|
103
|
+
attr_reader :states
|
104
|
+
|
105
|
+
# Returns whether the given +observable+ is already under observation.
|
106
|
+
#
|
107
|
+
# @param observable A class or instance to be checked.
|
108
|
+
#
|
109
|
+
# @return [Boolean] Whether the object is being observed.
|
110
|
+
def being_observed?(observable)
|
111
|
+
states.contain?(observable.object_id)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Stores a state representation of an observable.
|
115
|
+
#
|
116
|
+
# @param [State] state The state to be stored.
|
117
|
+
#
|
118
|
+
# @return [States] The whole states collection.
|
119
|
+
def save_state(state)
|
120
|
+
states.add(state)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Deletes the state representation of an observable.
|
124
|
+
#
|
125
|
+
# @param [State] state State representation of an object under observation.
|
126
|
+
#
|
127
|
+
# @return [Integer] The object_id of the observable.
|
128
|
+
def delete_state(state)
|
129
|
+
states.remove(state)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
data/lib/xavier/state.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Xavier
|
4
|
+
# A storage for an observed class or object's internal state.
|
5
|
+
# @api private
|
6
|
+
class State < BasicObject
|
7
|
+
# Returns the object id of the observed object
|
8
|
+
#
|
9
|
+
# @return [Integer] The +object_id+ of the observed object.
|
10
|
+
attr_reader :observed_object_id
|
11
|
+
|
12
|
+
# Creates a new state representation.
|
13
|
+
#
|
14
|
+
# @param [Integer] observed_object_id The +object_id+ of the observed object.
|
15
|
+
def initialize(observed_object_id)
|
16
|
+
@observed_object_id = observed_object_id
|
17
|
+
@class_variables = {}
|
18
|
+
@instance_variables = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
# Sets the instance variable named by +name+ to the given object.
|
22
|
+
#
|
23
|
+
# @param [String|Symbol] name Name of the instance variable.
|
24
|
+
# @param value Value of the instance variable.
|
25
|
+
#
|
26
|
+
# @return The value of the given instance variable.
|
27
|
+
def instance_variable_set(name, value)
|
28
|
+
@instance_variables[name.to_sym] = value
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns the value of the given instance variable, or +nil+ if the instance variable is not set.
|
32
|
+
#
|
33
|
+
# @param [String|Symbol] name Name of the instance variable.
|
34
|
+
#
|
35
|
+
# @return The value of the given instance variable.
|
36
|
+
def instance_variable_get(name)
|
37
|
+
@instance_variables[name.to_sym]
|
38
|
+
end
|
39
|
+
|
40
|
+
# Sets the class variable named by +name+ to the given object.
|
41
|
+
#
|
42
|
+
# @param [String|Symbol] name Name of the instance variable.
|
43
|
+
# @param value Value of the instance variable.
|
44
|
+
#
|
45
|
+
# @return The value of the given class variable.
|
46
|
+
def class_variable_set(name, value)
|
47
|
+
@class_variables[name.to_sym] = value
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the value of the given class variable, or nil if the class variable is not set.
|
51
|
+
#
|
52
|
+
# @param [String|Symbol] name Name of the class variable.
|
53
|
+
#
|
54
|
+
# @return The value of the given class variable.
|
55
|
+
def class_variable_get(name)
|
56
|
+
@class_variables[name.to_sym]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns true if class is the class of +self+, or if +clazz+ is one of the superclasses of +self+ or modules.
|
60
|
+
#
|
61
|
+
# @return [Boolean] Whether the given class is the class, superclass or modules of +self+.
|
62
|
+
def is_a?(clazz)
|
63
|
+
return true if clazz == ::Class
|
64
|
+
self.class == clazz
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns the class +State+.
|
68
|
+
#
|
69
|
+
# @return [State] The class +State+.
|
70
|
+
def class
|
71
|
+
State
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns an array of instance variable names for the receiver.
|
75
|
+
#
|
76
|
+
# @return [Array<Symbol>] Array of instance variable names.
|
77
|
+
def instance_variables
|
78
|
+
@instance_variables.keys
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns an array of the names of class variables in mod.
|
82
|
+
#
|
83
|
+
# @return [Array<Symbol>] Array of class variable names.
|
84
|
+
def class_variables
|
85
|
+
@class_variables.keys
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Xavier
|
4
|
+
# Collection of state representations of objects under observation.
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
class States
|
8
|
+
# Creates an instance of States.
|
9
|
+
def initialize
|
10
|
+
@collection = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
# Stores the state representation of an object.
|
14
|
+
#
|
15
|
+
# @param [State] state State representation of an object under observation.
|
16
|
+
#
|
17
|
+
# @return [States] The whole states collection.
|
18
|
+
def add(state)
|
19
|
+
collection[state.observed_object_id] = state
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
# Deletes the state representation of an object.
|
24
|
+
#
|
25
|
+
# @param [State] state State representation of an object under observation.
|
26
|
+
#
|
27
|
+
# @return [Integer] The object_id of the observable.
|
28
|
+
def remove(state)
|
29
|
+
collection.delete(state.observed_object_id)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns whether the states collection contains a state representation of an object, using its unique object_id.
|
33
|
+
#
|
34
|
+
# @param [Integer] object_id The object_id of the observable.
|
35
|
+
#
|
36
|
+
# @return [Boolean] Whether the states collection contains a state representation of an object.
|
37
|
+
def contain?(object_id)
|
38
|
+
collection.key?(object_id)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# Returns a hash where the state representations are stored.
|
44
|
+
# @return [Hash] A hash where the state representations are stored.
|
45
|
+
attr_reader :collection
|
46
|
+
end
|
47
|
+
end
|
data/xavier.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
require 'xavier/version'
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = 'xavier'
|
10
|
+
spec.version = Xavier::VERSION
|
11
|
+
spec.authors = ['Wilson Silva']
|
12
|
+
spec.email = ['me@wilsonsilva.net']
|
13
|
+
|
14
|
+
spec.summary = 'Reverts state mutations.'
|
15
|
+
spec.description = 'Reverts state mutations.'
|
16
|
+
spec.homepage = 'https://github.com/wilsonsilva/xavier'
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
19
|
+
f.match(%r{^(test|spec|features)/})
|
20
|
+
end
|
21
|
+
|
22
|
+
spec.metadata['yard.run'] = 'yri' # use "yard" to build full HTML docs.
|
23
|
+
|
24
|
+
spec.bindir = 'exe'
|
25
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
|
+
spec.require_paths = ['lib']
|
27
|
+
|
28
|
+
spec.add_development_dependency 'bundler', '~> 1.16'
|
29
|
+
spec.add_development_dependency 'pry', '~> 0.11'
|
30
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
31
|
+
spec.add_development_dependency 'rspec', '~> 3.7'
|
32
|
+
spec.add_development_dependency 'rubocop', '~> 0.53'
|
33
|
+
spec.add_development_dependency 'rubocop-rspec', '~> 1.24'
|
34
|
+
spec.add_development_dependency 'yard', '~> 0.9'
|
35
|
+
spec.add_development_dependency 'yard-junk', '~> 0.0.7'
|
36
|
+
spec.add_development_dependency 'yardstick', '~> 0.9'
|
37
|
+
end
|
metadata
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xavier
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Wilson Silva
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-03-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.16'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.11'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.11'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.7'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.7'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.53'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.53'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop-rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.24'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.24'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: yard
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.9'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.9'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: yard-junk
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.0.7
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 0.0.7
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: yardstick
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0.9'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0.9'
|
139
|
+
description: Reverts state mutations.
|
140
|
+
email:
|
141
|
+
- me@wilsonsilva.net
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- ".gitignore"
|
147
|
+
- ".rspec"
|
148
|
+
- ".rubocop.yml"
|
149
|
+
- ".travis.yml"
|
150
|
+
- ".yardopts"
|
151
|
+
- ".yardstick.yml"
|
152
|
+
- CHANGELOG.md
|
153
|
+
- Gemfile
|
154
|
+
- Gemfile.lock
|
155
|
+
- README.md
|
156
|
+
- Rakefile
|
157
|
+
- bin/console
|
158
|
+
- bin/setup
|
159
|
+
- lib/xavier.rb
|
160
|
+
- lib/xavier/mutation_strategies/class_copy.rb
|
161
|
+
- lib/xavier/mutation_strategies/instance_copy.rb
|
162
|
+
- lib/xavier/mutator.rb
|
163
|
+
- lib/xavier/observer.rb
|
164
|
+
- lib/xavier/state.rb
|
165
|
+
- lib/xavier/states.rb
|
166
|
+
- lib/xavier/version.rb
|
167
|
+
- xavier.gemspec
|
168
|
+
homepage: https://github.com/wilsonsilva/xavier
|
169
|
+
licenses: []
|
170
|
+
metadata:
|
171
|
+
yard.run: yri
|
172
|
+
post_install_message:
|
173
|
+
rdoc_options: []
|
174
|
+
require_paths:
|
175
|
+
- lib
|
176
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
182
|
+
requirements:
|
183
|
+
- - ">="
|
184
|
+
- !ruby/object:Gem::Version
|
185
|
+
version: '0'
|
186
|
+
requirements: []
|
187
|
+
rubyforge_project:
|
188
|
+
rubygems_version: 2.6.12
|
189
|
+
signing_key:
|
190
|
+
specification_version: 4
|
191
|
+
summary: Reverts state mutations.
|
192
|
+
test_files: []
|