gmalamid-synthesis 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +18 -0
- data/README +107 -0
- data/Rakefile +77 -0
- data/lib/synthesis/adapter/expectations.rb +35 -0
- data/lib/synthesis/adapter/mocha.rb +30 -0
- data/lib/synthesis/adapter/rspec.rb +34 -0
- data/lib/synthesis/adapter.rb +42 -0
- data/lib/synthesis/class.rb +5 -0
- data/lib/synthesis/expectation.rb +115 -0
- data/lib/synthesis/expectation_interceptor.rb +78 -0
- data/lib/synthesis/expectation_matcher.rb +23 -0
- data/lib/synthesis/expectation_record.rb +102 -0
- data/lib/synthesis/expectation_record_enabled.rb +39 -0
- data/lib/synthesis/logging.rb +21 -0
- data/lib/synthesis/method_invocation_watcher.rb +8 -0
- data/lib/synthesis/module.rb +5 -0
- data/lib/synthesis/object.rb +9 -0
- data/lib/synthesis/recordable.rb +48 -0
- data/lib/synthesis/reporter.rb +24 -0
- data/lib/synthesis/runner.rb +9 -0
- data/lib/synthesis/task.rb +51 -0
- data/lib/synthesis/util/mock_instance/mocha.rb +18 -0
- data/lib/synthesis/util/mock_instance/rspec.rb +18 -0
- data/lib/synthesis/util/mock_instance.rb +7 -0
- data/lib/synthesis.rb +17 -0
- data/synthesis.gemspec +41 -0
- metadata +84 -0
data/COPYING
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2007 nutrun.com, Stuart Caborn, George Malamidis
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to
|
5
|
+
deal in the Software without restriction, including without limitation the
|
6
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
7
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
= Synthesis
|
2
|
+
|
3
|
+
== Philosophy
|
4
|
+
|
5
|
+
Currently we believe that developers are writing unnecessary <em>dependency wired</em> tests to cover uncertainty about the validity of simulated interactions in their <em>dependency neutral</em> tests. In other words, we cannot be certain that all our simulated interaction based tests 'join up'. If it were possible to correlate the simulated interactions in our tests, then we should be able to do away with the need to write large numbers of complex, slow and brittle wired tests (apart from those which interact with the boundaries of the SUT).
|
6
|
+
|
7
|
+
Synthesis combines lightweight tests to build confidence that the system under test is complete and reduces the need for large, overarching tests.
|
8
|
+
|
9
|
+
== Installation
|
10
|
+
|
11
|
+
sudo gem i synthesis
|
12
|
+
|
13
|
+
== Download
|
14
|
+
|
15
|
+
Synthesis RubyForge page ( http://rubyforge.org/projects/synthesis )
|
16
|
+
|
17
|
+
== Dependencies
|
18
|
+
|
19
|
+
Synthesis's core doesn't have any dependencies.
|
20
|
+
|
21
|
+
When used with the Mocha adapter, it will depend on the Mocha[http://mocha.rubyforge.org] library.
|
22
|
+
|
23
|
+
When used with the RSpec adapter, it will depend on the RSpec[http://rspec.info/] library.
|
24
|
+
|
25
|
+
When used with the Expectations adapter, it will depend on the Expectations[http://expectations.rubyforge.org] library.
|
26
|
+
|
27
|
+
== Usage
|
28
|
+
|
29
|
+
Synthesis can be used through its Rake task. It currently has three supported adapters: Mocha (with Test::Unit), RSpec and Expectations. If +adapter+ is not explicitly specified, the Mocha adapter will be used by default.
|
30
|
+
|
31
|
+
By default, Synthesis outputs to +STDOUT+, but output can be redirected to alternative IO streams.
|
32
|
+
|
33
|
+
Synthesis can be setup to ignore certain classes or modules when collecting expectations for verification.
|
34
|
+
|
35
|
+
If +pattern+ is not specified, it will default to <tt>test/**/*_test.rb</tt>
|
36
|
+
|
37
|
+
== Usage examples
|
38
|
+
|
39
|
+
To use with Test::Unit and Mocha, ignoring Array and Hash:
|
40
|
+
|
41
|
+
require "synthesis/task"
|
42
|
+
|
43
|
+
Synthesis::Task.new do |t|
|
44
|
+
t.pattern = 'test/unit/**/*_test.rb'
|
45
|
+
t.ignored = [Array, Hash]
|
46
|
+
end
|
47
|
+
|
48
|
+
To use with RSpec, running all specs in the <tt>spec</tt> directory:
|
49
|
+
|
50
|
+
require "synthesis/task"
|
51
|
+
|
52
|
+
Synthesis::Task.new do |t|
|
53
|
+
t.adapter = :rspec
|
54
|
+
t.pattern = 'spec/**/*_spec.rb'
|
55
|
+
end
|
56
|
+
|
57
|
+
To use with Expectations, redirecting output to a file
|
58
|
+
|
59
|
+
require "synthesis/task"
|
60
|
+
|
61
|
+
Synthesis::Task.new do |t|
|
62
|
+
t.adapter = :expectations
|
63
|
+
t.out = File.new "synthesis.test.txt", "a"
|
64
|
+
end
|
65
|
+
|
66
|
+
== Utilities
|
67
|
+
|
68
|
+
=== mock_instance
|
69
|
+
|
70
|
+
require "synthesis/util/mock_instance"
|
71
|
+
foo_mock = Foo.mock_instance(arg_one, arg_2)
|
72
|
+
|
73
|
+
This is equivalent, but without calling the real <tt>initialize</tt>, to:
|
74
|
+
|
75
|
+
foo_mock = Foo.new
|
76
|
+
Foo.expects(:new).with(arg_one, arg_two).returns(foo_mock)
|
77
|
+
|
78
|
+
Or, in the case of RSpec, it is equivalent to:
|
79
|
+
|
80
|
+
foo_mock = Foo.new
|
81
|
+
Foo.should_receive(:new).with(arg_one, arg_two).and_return(foo_mock)
|
82
|
+
|
83
|
+
Either <tt>"mocha_standalone"</tt> or <tt>"spec/mocks"</tt> need to be required before using <tt>mock_instance</tt>.
|
84
|
+
|
85
|
+
== Git
|
86
|
+
|
87
|
+
Public clone URL: git://github.com/gmalamid/synthesis.git
|
88
|
+
|
89
|
+
== Known Issues
|
90
|
+
|
91
|
+
Reporting the location (filename and line number) of tested/untested expectations doesn't work as intended with the Expectations adapter.
|
92
|
+
|
93
|
+
== Contributors
|
94
|
+
|
95
|
+
Danilo Sato, Paul Nasrat
|
96
|
+
|
97
|
+
== Discuss
|
98
|
+
|
99
|
+
http://groups.google.com/group/synthesized-testing
|
100
|
+
|
101
|
+
== Related reading
|
102
|
+
|
103
|
+
* Synthesized Testing: A Primer ( http://nutrun.com/weblog/synthesized-testing-a-primer )
|
104
|
+
* Confidence as a test code metric ( http://nutrun.com/weblog/confidence-as-a-test-code-metric )
|
105
|
+
* Using Synthesis With Test::Unit and Mocha ( http://nutrun.com/weblog/using-synthesis-with-testunit-and-mocha )
|
106
|
+
* Using Synthesis With Expectations ( http://nutrun.com/weblog/using-synthesis-with-expectations )
|
107
|
+
* Synthesis validates simulated method calls taking method signature arguments into account ( http://nutrun.com/weblog/synthesis-002 )
|
data/Rakefile
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "rake/testtask"
|
3
|
+
require "rake/gempackagetask"
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/contrib/sshpublisher'
|
6
|
+
require File.dirname(__FILE__) + "/lib/synthesis/task"
|
7
|
+
|
8
|
+
load 'synthesis.gemspec'
|
9
|
+
|
10
|
+
task :default => :test
|
11
|
+
|
12
|
+
desc "Run all tests"
|
13
|
+
task :test => %w[test:core test:mocha test:spec]
|
14
|
+
|
15
|
+
desc "Run core tests"
|
16
|
+
Rake::TestTask.new('test:core') do |t|
|
17
|
+
t.pattern = 'test/synthesis/*_test.rb'
|
18
|
+
end
|
19
|
+
|
20
|
+
desc "Run Mocha adapter tests"
|
21
|
+
Rake::TestTask.new('test:mocha') do |t|
|
22
|
+
t.pattern = 'test/synthesis/adapter/mocha/*_test.rb'
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "Run RSpec adapter tests"
|
26
|
+
Rake::TestTask.new('test:spec') do |t|
|
27
|
+
t.pattern = 'test/synthesis/adapter/rspec/*_test.rb'
|
28
|
+
end
|
29
|
+
|
30
|
+
Synthesis::Task.new do |t|
|
31
|
+
t.pattern = 'test_project/mocha/test/*_test.rb'
|
32
|
+
t.ignored = [Array, Hash]
|
33
|
+
# t.out = File.new('synthesis.test.txt', 'a')
|
34
|
+
end
|
35
|
+
|
36
|
+
Synthesis::Task.new('synthesis:expectations') do |t|
|
37
|
+
t.adapter = :expectations
|
38
|
+
t.pattern = 'test_project/expectations/test/*_test.rb'
|
39
|
+
end
|
40
|
+
|
41
|
+
Synthesis::Task.new('synthesis:spec') do |t|
|
42
|
+
t.adapter = :rspec
|
43
|
+
t.pattern = 'test_project/rspec/*_spec.rb'
|
44
|
+
end
|
45
|
+
|
46
|
+
desc 'Generate RDoc'
|
47
|
+
Rake::RDocTask.new do |task|
|
48
|
+
task.main = 'README'
|
49
|
+
task.title = 'synthesis'
|
50
|
+
task.rdoc_dir = 'doc'
|
51
|
+
task.options << "--line-numbers" << "--inline-source"
|
52
|
+
task.rdoc_files.include('README', 'lib/**/*.rb')
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "Upload RDoc to RubyForge"
|
56
|
+
task :publish_rdoc do
|
57
|
+
Rake::Task[:rdoc].invoke
|
58
|
+
Rake::SshDirPublisher.new("gmalamid@rubyforge.org", "/var/www/gforge-projects/synthesis", "doc").upload
|
59
|
+
end
|
60
|
+
|
61
|
+
Rake::GemPackageTask.new(GEMSPEC) do |t|
|
62
|
+
t.need_zip = false
|
63
|
+
t.need_tar = false
|
64
|
+
end
|
65
|
+
|
66
|
+
desc "Remove rdoc and package artefacts"
|
67
|
+
task :clean => %w[clobber_package clobber_rdoc]
|
68
|
+
|
69
|
+
task(:lf) {p Dir["lib/**/*rb"] }
|
70
|
+
|
71
|
+
task(:check_gemspec) do
|
72
|
+
require 'rubygems/specification'
|
73
|
+
data = File.read('synthesis.gemspec')
|
74
|
+
spec = nil
|
75
|
+
Thread.new { spec = eval("$SAFE = 3\n#{data}") }.join
|
76
|
+
puts spec
|
77
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "mocha_standalone"
|
3
|
+
require "expectations"
|
4
|
+
|
5
|
+
require File.dirname(__FILE__) + "/../../synthesis"
|
6
|
+
|
7
|
+
module Synthesis
|
8
|
+
class ExpectationsAdapter < Adapter
|
9
|
+
def run
|
10
|
+
fail_unless { Expectations::SuiteRunner.instance.suite.execute }
|
11
|
+
end
|
12
|
+
|
13
|
+
def collect_expectations
|
14
|
+
ignore_instances_of Class::AnyInstance
|
15
|
+
Object.extend(ExpectationRecordEnabled)
|
16
|
+
Object.record_expectations_on(:expects)
|
17
|
+
Mocha::Expectation.extend(ExpectationInterceptor)
|
18
|
+
Mocha::Expectation.record_expected_arguments_on(:with)
|
19
|
+
Mocha::Expectation.record_expected_return_values_on(:returns)
|
20
|
+
Mocha::Expectation.record_expected_return_values_on(:raises)
|
21
|
+
Mocha::Expectation.remove_expectation_on(:never)
|
22
|
+
end
|
23
|
+
|
24
|
+
def stop_collecting_expectations
|
25
|
+
Mocha::Expectation.stop_intercepting!
|
26
|
+
Object.stop_recording!
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Expectations::SuiteRunner
|
32
|
+
def initialize
|
33
|
+
self.suite = Expectations::Suite.new
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "mocha_standalone"
|
3
|
+
require "test/unit"
|
4
|
+
|
5
|
+
require File.dirname(__FILE__) + "/../../synthesis"
|
6
|
+
|
7
|
+
module Synthesis
|
8
|
+
class MochaAdapter < Adapter
|
9
|
+
def run
|
10
|
+
Test::Unit.run = true # Yes means no...
|
11
|
+
fail_unless { Test::Unit::AutoRunner.run }
|
12
|
+
end
|
13
|
+
|
14
|
+
def collect_expectations
|
15
|
+
ignore_instances_of Class::AnyInstance
|
16
|
+
Object.extend(ExpectationRecordEnabled)
|
17
|
+
Object.record_expectations_on(:expects)
|
18
|
+
Mocha::Expectation.extend(ExpectationInterceptor)
|
19
|
+
Mocha::Expectation.record_expected_arguments_on(:with)
|
20
|
+
Mocha::Expectation.record_expected_return_values_on(:returns)
|
21
|
+
Mocha::Expectation.record_expected_return_values_on(:raises)
|
22
|
+
Mocha::Expectation.remove_expectation_on(:never)
|
23
|
+
end
|
24
|
+
|
25
|
+
def stop_collecting_expectations
|
26
|
+
Mocha::Expectation.stop_intercepting!
|
27
|
+
Object.stop_recording!
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "spec"
|
3
|
+
require "spec/mocks"
|
4
|
+
|
5
|
+
require File.dirname(__FILE__) + "/../../synthesis"
|
6
|
+
|
7
|
+
module Synthesis
|
8
|
+
class RSpecAdapter < Adapter
|
9
|
+
def run
|
10
|
+
rspec_options.files.clear
|
11
|
+
fail_unless do
|
12
|
+
rspec_options.instance_variable_set(:@formatters, nil)
|
13
|
+
# rspec_options.instance_variable_set(:@format_options, [["profile", STDOUT]])
|
14
|
+
rspec_options.run_examples
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def collect_expectations
|
19
|
+
ignore_instances_of Spec::Mocks::Mock
|
20
|
+
Spec::Mocks::Methods.extend(ExpectationRecordEnabled)
|
21
|
+
Spec::Mocks::Methods.record_expectations_on(:should_receive)
|
22
|
+
Spec::Mocks::MessageExpectation.extend(ExpectationInterceptor)
|
23
|
+
Spec::Mocks::MessageExpectation.record_expected_arguments_on(:with)
|
24
|
+
Spec::Mocks::MessageExpectation.record_expected_return_values_on(:and_return)
|
25
|
+
Spec::Mocks::MessageExpectation.record_expected_return_values_on(:and_raise)
|
26
|
+
Spec::Mocks::MessageExpectation.remove_expectation_on(:never)
|
27
|
+
end
|
28
|
+
|
29
|
+
def stop_collecting_expectations
|
30
|
+
Spec::Mocks::MessageExpectation.stop_intercepting!
|
31
|
+
Spec::Mocks::Methods.stop_recording!
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Synthesis
|
2
|
+
# Subclasses of Adapter must implement the run, collect_expectations and
|
3
|
+
# stop_collecting_expectations methods.
|
4
|
+
# For example implementations refer to Synthesis::MochaAdapter and
|
5
|
+
# Synthesis::RSpecAdapter.
|
6
|
+
class Adapter
|
7
|
+
include Logging
|
8
|
+
|
9
|
+
def initialize(pattern)
|
10
|
+
@pattern = pattern
|
11
|
+
end
|
12
|
+
|
13
|
+
# The block parameter will yield twice, once for collecting and
|
14
|
+
# once for verifying the collected expectations.
|
15
|
+
def fail_unless(&block)
|
16
|
+
log "Collecting expectations..."
|
17
|
+
collect_expectations
|
18
|
+
Dir[*@pattern].each { |t| require t }
|
19
|
+
exit -1 unless yield
|
20
|
+
log "Verifying expectation invocations..."
|
21
|
+
stop_collecting_expectations
|
22
|
+
ExpectationRecord.record_invocations
|
23
|
+
yield
|
24
|
+
end
|
25
|
+
|
26
|
+
# The type of object representing a mock or stub for the test framework.
|
27
|
+
# Objects of this type will be ignored by Synthesis.
|
28
|
+
def ignore_instances_of(type)
|
29
|
+
Synthesis.const_set(:MOCK_OBJECT, type)
|
30
|
+
end
|
31
|
+
|
32
|
+
class << self
|
33
|
+
def inherited(subclass)
|
34
|
+
@adapter = subclass
|
35
|
+
end
|
36
|
+
|
37
|
+
def load(pattern)
|
38
|
+
@adapter.new(pattern)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Synthesis
|
2
|
+
module Expectation
|
3
|
+
def self.new(receiver, method, track, args = [], return_values = [])
|
4
|
+
receiver.expectation(method, track, args, return_values)
|
5
|
+
end
|
6
|
+
|
7
|
+
class Expectation
|
8
|
+
include Logging
|
9
|
+
attr_reader :receiver, :method, :return_values
|
10
|
+
protected :return_values
|
11
|
+
attr_accessor :args
|
12
|
+
|
13
|
+
def initialize(receiver, method, track, args, return_values)
|
14
|
+
@receiver, @method, @track, @args = receiver, method, track, args
|
15
|
+
@return_values = return_values
|
16
|
+
end
|
17
|
+
|
18
|
+
def record_invocations
|
19
|
+
meta_receiver.extend(Recordable)
|
20
|
+
meta_receiver.recordable_method(@method)
|
21
|
+
end
|
22
|
+
|
23
|
+
def explode
|
24
|
+
if @return_values.size > 1
|
25
|
+
@return_values.map do |v|
|
26
|
+
expectation = self.class.new(@receiver, @method, @track, @args, [])
|
27
|
+
expectation.add_return_values(v)
|
28
|
+
expectation
|
29
|
+
end
|
30
|
+
else
|
31
|
+
self
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def eql?(other)
|
36
|
+
ExpectationMatcher.new(self, other).match?
|
37
|
+
end
|
38
|
+
|
39
|
+
def ==(other)
|
40
|
+
eql?(other)
|
41
|
+
end
|
42
|
+
|
43
|
+
def invoked?
|
44
|
+
@invoked
|
45
|
+
end
|
46
|
+
|
47
|
+
def invoked!
|
48
|
+
@invoked = true
|
49
|
+
end
|
50
|
+
|
51
|
+
def arg_types
|
52
|
+
args.map { |arg| arg.class }
|
53
|
+
end
|
54
|
+
|
55
|
+
def return_value_type
|
56
|
+
@return_values.size == 1 ? @return_values[0].class : nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_return_values(*vals)
|
60
|
+
@return_values_defined = true
|
61
|
+
@return_values += vals
|
62
|
+
end
|
63
|
+
|
64
|
+
def return_values_defined?
|
65
|
+
@return_values_defined
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Singleton < Expectation
|
70
|
+
def meta_receiver
|
71
|
+
@receiver.__metaclass__
|
72
|
+
end
|
73
|
+
|
74
|
+
def hash
|
75
|
+
(@receiver.name.hash * 23) + @method.hash
|
76
|
+
end
|
77
|
+
|
78
|
+
def receiver_class
|
79
|
+
@receiver
|
80
|
+
end
|
81
|
+
|
82
|
+
def to_s
|
83
|
+
"(#{return_value_type}) " +
|
84
|
+
"#{@receiver.name}.#{@method}(#{@args.map { |arg| arg.class } * ', '})" +
|
85
|
+
"in #{@track}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class Instance < Expectation
|
90
|
+
def meta_receiver
|
91
|
+
@receiver.class
|
92
|
+
end
|
93
|
+
|
94
|
+
def hash
|
95
|
+
(meta_receiver.name.hash * 23) + @method.hash
|
96
|
+
end
|
97
|
+
|
98
|
+
def receiver_class
|
99
|
+
meta_receiver
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_s
|
103
|
+
"(#{return_value_type}) #{meta_receiver.name}.new.#{@method}" +
|
104
|
+
"(#{@args.map { |arg| arg.class } * ', '})" +
|
105
|
+
"in #{@track}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class NilExpectation < Expectation
|
110
|
+
def initialize;end
|
111
|
+
def invoked!;end
|
112
|
+
def record_invocations;end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Synthesis
|
2
|
+
# Extend by the mock object framework's expectation mechanism to allow
|
3
|
+
# Synthesis to tap into it in order to collect simulated method arguments
|
4
|
+
# and return values.
|
5
|
+
module ExpectationInterceptor
|
6
|
+
# Intercept the mock object framework's expectation method for declaring a mocked
|
7
|
+
# method's arguments so that Synthesis can record them.
|
8
|
+
def record_expected_arguments_on(method_name)
|
9
|
+
(@original_methods ||= []) << method_name
|
10
|
+
|
11
|
+
class_eval do
|
12
|
+
alias_method "intercepted_#{method_name}", method_name
|
13
|
+
|
14
|
+
define_method(:get_method_name) {method_name}
|
15
|
+
|
16
|
+
def temp_method(*expected_parameters, &matching_block)
|
17
|
+
synthesis_expectation.args = expected_parameters if synthesis_expectation
|
18
|
+
send("intercepted_#{get_method_name}", *expected_parameters, &matching_block)
|
19
|
+
end
|
20
|
+
|
21
|
+
alias_method method_name, :temp_method
|
22
|
+
undef temp_method
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Intercept the mock object framework's expectation method for declaring a mocked
|
27
|
+
# method's return values so that Synthesis can record them.
|
28
|
+
def record_expected_return_values_on(method_name)
|
29
|
+
(@original_methods ||= []) << method_name
|
30
|
+
|
31
|
+
class_eval do
|
32
|
+
alias_method "intercepted_#{method_name}", method_name
|
33
|
+
|
34
|
+
define_method(method_name) do |*values|
|
35
|
+
mock_expectation = send("intercepted_#{method_name}", *values)
|
36
|
+
synthesis_expectation.add_return_values(*values) if synthesis_expectation
|
37
|
+
mock_expectation
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def remove_expectation_on(method_name)
|
43
|
+
(@original_methods ||= []) << method_name
|
44
|
+
|
45
|
+
class_eval do
|
46
|
+
alias_method "intercepted_#{method_name}", method_name
|
47
|
+
|
48
|
+
define_method(method_name) do |*values|
|
49
|
+
Synthesis::ExpectationRecord.remove(synthesis_expectation) if synthesis_expectation
|
50
|
+
send("intercepted_#{method_name}")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Restore the original methods ExpectationInterceptor has rewritten and
|
56
|
+
# undefine their intercepted counterparts. Undefine the synthesis_expectation
|
57
|
+
# accessors.
|
58
|
+
def stop_intercepting!
|
59
|
+
@original_methods.each do |m|
|
60
|
+
class_eval do
|
61
|
+
alias_method m, "intercepted_#{m}"
|
62
|
+
remove_method "intercepted_#{m}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class_eval do
|
67
|
+
remove_method :synthesis_expectation
|
68
|
+
remove_method :synthesis_expectation=
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Classes extending ExpectationInterceptor will have a synthesis_expectation
|
73
|
+
# attribute accessor added to them.
|
74
|
+
def self.extended(receiver)
|
75
|
+
receiver.send(:attr_accessor, :synthesis_expectation)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Synthesis
|
2
|
+
class ExpectationMatcher
|
3
|
+
def initialize(expectation_one, expectation_two)
|
4
|
+
@expectation_one, @expectation_two = expectation_one, expectation_two
|
5
|
+
end
|
6
|
+
|
7
|
+
def match?
|
8
|
+
return false unless @expectation_one.class == @expectation_two.class
|
9
|
+
return false unless @expectation_one.receiver_class == @expectation_two.receiver_class
|
10
|
+
return false unless @expectation_one.method == @expectation_two.method
|
11
|
+
return false unless @expectation_one.arg_types == @expectation_two.arg_types
|
12
|
+
return false unless return_values_match?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def return_values_match?
|
19
|
+
return true unless @expectation_one.return_values_defined? || @expectation_two.return_values_defined?
|
20
|
+
@expectation_one.return_value_type == @expectation_two.return_value_type
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Synthesis
|
2
|
+
class ExpectationRecord
|
3
|
+
class << self
|
4
|
+
include Logging
|
5
|
+
|
6
|
+
def add_expectation(receiver, method, track, args = [])
|
7
|
+
unless ignore?(receiver)
|
8
|
+
expectation = Expectation.new(receiver, method, track, args)
|
9
|
+
expectations << expectation
|
10
|
+
expectation
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def remove(expectation)
|
15
|
+
expectations.delete(expectation)
|
16
|
+
end
|
17
|
+
|
18
|
+
def ignore(*args)
|
19
|
+
ignored.merge(args)
|
20
|
+
end
|
21
|
+
|
22
|
+
def expectations
|
23
|
+
# Using an Array instead of a Set because the +Expectation+ instance
|
24
|
+
# is not complete when first added. A Set would result to possible duplicates.
|
25
|
+
# obj.expects(:method).with(:args)
|
26
|
+
# the +Expectation+ will be added when obj.expects(:method) is called
|
27
|
+
# the +Expectation+ arguments will be added when .with(:args) is called
|
28
|
+
@expectations ||= []
|
29
|
+
end
|
30
|
+
|
31
|
+
def ignored
|
32
|
+
@ignored ||= Set.new
|
33
|
+
end
|
34
|
+
|
35
|
+
def [](matcher)
|
36
|
+
# Using a hash for faster look up of expectations
|
37
|
+
# when recording invocations
|
38
|
+
expectations_with_return_values[matcher] ||
|
39
|
+
expectations_without_return_values[matcher] ||
|
40
|
+
Expectation::NilExpectation.new
|
41
|
+
end
|
42
|
+
|
43
|
+
def record_invocations
|
44
|
+
expectations.map! { |e| e.explode }
|
45
|
+
expectations.flatten!
|
46
|
+
expectations.uniq!
|
47
|
+
expectations.each do |e|
|
48
|
+
e.record_invocations
|
49
|
+
if e.return_values_defined?
|
50
|
+
expectations_with_return_values[e] = e
|
51
|
+
else
|
52
|
+
expectations_without_return_values[e] = e
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def tested_expectations
|
58
|
+
expectations.select { |e| e.invoked? }
|
59
|
+
end
|
60
|
+
|
61
|
+
def untested_expectations
|
62
|
+
expectations.select { |e| !e.invoked? }
|
63
|
+
end
|
64
|
+
|
65
|
+
def print_tested_expectations
|
66
|
+
log; log "Tested Expectations: "
|
67
|
+
tested_expectations.each { |e| log e }
|
68
|
+
end
|
69
|
+
|
70
|
+
def print_untested_expectations
|
71
|
+
log; log "Untested Expectations: "
|
72
|
+
untested_expectations.each { |e| log e }
|
73
|
+
end
|
74
|
+
|
75
|
+
def print_ignored
|
76
|
+
log; log "Ignoring: #{ignored.to_a * ', '}"
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
def expectations_with_return_values
|
81
|
+
@expectations_with_return_values ||= {}
|
82
|
+
end
|
83
|
+
|
84
|
+
def expectations_without_return_values
|
85
|
+
@expectations_without_return_values ||= {}
|
86
|
+
end
|
87
|
+
|
88
|
+
def ignore?(obj)
|
89
|
+
ignored.include?(obj.class) ||
|
90
|
+
(obj.is_a?(Class) && ignored.include?(obj)) ||
|
91
|
+
(obj.is_a?(Module) && ignored.include?(obj)) ||
|
92
|
+
obj.is_a?(MOCK_OBJECT)
|
93
|
+
end
|
94
|
+
|
95
|
+
def reset!
|
96
|
+
@expectations_with_return_values = nil
|
97
|
+
@expectations_without_return_values = nil
|
98
|
+
@expectations, @ignored = nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Synthesis
|
2
|
+
# Extend by the mock object framework's construct for declaring a
|
3
|
+
# mock object so that Synthesis can tap into it in order to record
|
4
|
+
# the expectation.
|
5
|
+
module ExpectationRecordEnabled
|
6
|
+
# Intercept the mock object framework's method for declaring a mock
|
7
|
+
# object so that Synthesis can record it.
|
8
|
+
def record_expectations_on(method_name)
|
9
|
+
@original_expects = method_name
|
10
|
+
|
11
|
+
class_eval do
|
12
|
+
alias_method "intercepted_#{method_name}", method_name
|
13
|
+
|
14
|
+
define_method(:get_expectation_method_name) {method_name}
|
15
|
+
|
16
|
+
def temp_expectation_record(meth, *expected_parameters, &matching_block)
|
17
|
+
s_expectation = ExpectationRecord.add_expectation(self, meth, caller[0])
|
18
|
+
m_expectation = send("intercepted_#{get_expectation_method_name}", meth, *expected_parameters, &matching_block)
|
19
|
+
m_expectation.synthesis_expectation = s_expectation
|
20
|
+
m_expectation
|
21
|
+
end
|
22
|
+
|
23
|
+
alias_method method_name, :temp_expectation_record
|
24
|
+
undef temp_expectation_record
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Restore the original methods ExpectationRecordEnabled has rewritten and
|
29
|
+
# undefine their intercepted counterparts.
|
30
|
+
def stop_recording!
|
31
|
+
method_name = @original_expects
|
32
|
+
class_eval do
|
33
|
+
alias_method method_name, "intercepted_#{method_name}"
|
34
|
+
remove_method "intercepted_#{method_name}"
|
35
|
+
remove_method :get_expectation_method_name
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Synthesis
|
2
|
+
module Logging
|
3
|
+
def silence!
|
4
|
+
@silent = true
|
5
|
+
end
|
6
|
+
|
7
|
+
def speak!
|
8
|
+
@silent = false
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def log(msg = '')
|
14
|
+
out.puts "[Synthesis] #{msg}" unless @silent
|
15
|
+
end
|
16
|
+
|
17
|
+
def out
|
18
|
+
Synthesis::Logging.const_defined?(:OUT) ? OUT : STDOUT
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Synthesis
|
2
|
+
module Recordable
|
3
|
+
def recordable_method(meth)
|
4
|
+
if method_defined?(meth)
|
5
|
+
defined_recordable_method(meth)
|
6
|
+
else
|
7
|
+
magic_recordable_method(meth)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def defined_recordable_method(meth)
|
14
|
+
unless method_defined?("__recordable__#{meth}".intern)
|
15
|
+
alias_method "__recordable__#{meth}".intern, meth
|
16
|
+
class_eval <<-end_eval
|
17
|
+
def #{meth}(*args, &block)
|
18
|
+
begin
|
19
|
+
return_value = send("__recordable__#{meth}", *args, &block)
|
20
|
+
MethodInvocationWatcher.invoked(self, "#{meth}".intern, args, [return_value])
|
21
|
+
return_value
|
22
|
+
rescue Exception => e
|
23
|
+
MethodInvocationWatcher.invoked(self, "#{meth}".intern, args, [e])
|
24
|
+
raise e
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end_eval
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def magic_recordable_method(meth)
|
32
|
+
class_eval <<-end_eval
|
33
|
+
def #{meth}(*args)
|
34
|
+
begin
|
35
|
+
return_value = method_missing(:#{meth}, *args)
|
36
|
+
MethodInvocationWatcher.invoked(self, "#{meth}".intern, args, [return_value])
|
37
|
+
return_value
|
38
|
+
rescue Exception => e
|
39
|
+
MethodInvocationWatcher.invoked(self, "#{meth}".intern, args, [e])
|
40
|
+
raise e
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def __recordable__#{meth}() raise "Don't ever call me" end
|
45
|
+
end_eval
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Synthesis
|
2
|
+
class Reporter
|
3
|
+
class << self
|
4
|
+
include Logging
|
5
|
+
|
6
|
+
def report
|
7
|
+
if failed?
|
8
|
+
ExpectationRecord.print_tested_expectations
|
9
|
+
ExpectationRecord.print_untested_expectations
|
10
|
+
ExpectationRecord.print_ignored
|
11
|
+
log; log "FAILED."
|
12
|
+
return -1
|
13
|
+
end
|
14
|
+
log; log "Verified #{ExpectationRecord.expectations.size} expectations"
|
15
|
+
log "SUCCESS."
|
16
|
+
0
|
17
|
+
end
|
18
|
+
|
19
|
+
def failed?
|
20
|
+
ExpectationRecord.untested_expectations.any?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "rake"
|
3
|
+
require "rake/tasklib"
|
4
|
+
require File.dirname(__FILE__) + "/../synthesis/logging"
|
5
|
+
|
6
|
+
module Synthesis
|
7
|
+
class Task < Rake::TaskLib
|
8
|
+
include Logging
|
9
|
+
attr_accessor :verbose, :pattern, :ruby_opts, :adapter, :out, :ignored, :libs
|
10
|
+
|
11
|
+
def initialize(name='synthesis:test')
|
12
|
+
@name, @ignored, @libs = name, [], ['lib']
|
13
|
+
yield self if block_given?
|
14
|
+
@pattern ||= 'test/**/*_test.rb'
|
15
|
+
@ruby_opts ||= []
|
16
|
+
@adapter ||= :mocha
|
17
|
+
define
|
18
|
+
end
|
19
|
+
|
20
|
+
def ignore(*classes)
|
21
|
+
STDERR.puts
|
22
|
+
STDERR.puts "DEPRECATION WARNING!!!"
|
23
|
+
STDERR.puts caller[0]
|
24
|
+
STDERR.puts "Synthesis::Task#ignore(*classes) has been deprecated."
|
25
|
+
STDERR.puts "Use Synthesis::Task#ignored = [#{classes * ','}] instead."
|
26
|
+
STDERR.puts
|
27
|
+
@ignored << classes
|
28
|
+
@ignored.flatten!
|
29
|
+
end
|
30
|
+
|
31
|
+
def define
|
32
|
+
desc "Run Synthesis tests"
|
33
|
+
task @name do
|
34
|
+
RakeFileUtils.verbose(@verbose) do
|
35
|
+
load_paths
|
36
|
+
require File.dirname(__FILE__) + "/../synthesis"
|
37
|
+
require File.dirname(__FILE__) + "/../synthesis/runner"
|
38
|
+
Synthesis::Logging.const_set(:OUT, @out) if @out
|
39
|
+
Synthesis::ExpectationRecord.ignore(*@ignored)
|
40
|
+
Synthesis::Runner.run(@adapter, @pattern)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def load_paths
|
48
|
+
@libs.each { |path| $:.unshift(File.join(Dir.pwd, path)) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Object
|
2
|
+
def self.mock_instance(*args)
|
3
|
+
class_eval do
|
4
|
+
alias original_initialize initialize
|
5
|
+
def initialize()end
|
6
|
+
end
|
7
|
+
|
8
|
+
instance = new
|
9
|
+
expects(:new).with(*args).returns(instance)
|
10
|
+
|
11
|
+
class_eval do
|
12
|
+
alias initialize original_initialize
|
13
|
+
undef original_initialize
|
14
|
+
end
|
15
|
+
|
16
|
+
return instance
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Spec::Mocks::Methods
|
2
|
+
def mock_instance(*args)
|
3
|
+
class_eval do
|
4
|
+
alias original_initialize initialize
|
5
|
+
def initialize()end
|
6
|
+
end
|
7
|
+
|
8
|
+
instance = new
|
9
|
+
should_receive(:new).with(*args).and_return(instance)
|
10
|
+
|
11
|
+
class_eval do
|
12
|
+
alias initialize original_initialize
|
13
|
+
undef original_initialize
|
14
|
+
end
|
15
|
+
|
16
|
+
return instance
|
17
|
+
end
|
18
|
+
end
|
data/lib/synthesis.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
$: << File.dirname(__FILE__)
|
4
|
+
|
5
|
+
require "synthesis/logging"
|
6
|
+
require "synthesis/recordable"
|
7
|
+
require "synthesis/object"
|
8
|
+
require "synthesis/class"
|
9
|
+
require "synthesis/module"
|
10
|
+
require "synthesis/expectation_record"
|
11
|
+
require "synthesis/method_invocation_watcher"
|
12
|
+
require "synthesis/expectation"
|
13
|
+
require "synthesis/expectation_matcher"
|
14
|
+
require "synthesis/expectation_interceptor"
|
15
|
+
require "synthesis/expectation_record_enabled"
|
16
|
+
require "synthesis/reporter"
|
17
|
+
require "synthesis/adapter"
|
data/synthesis.gemspec
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
GEMSPEC =Gem::Specification.new do |s|
|
2
|
+
s.name = 'synthesis'
|
3
|
+
s.version = '0.1.6'
|
4
|
+
s.platform = Gem::Platform::RUBY
|
5
|
+
s.rubyforge_project = "synthesis"
|
6
|
+
s.summary, s.description = 'A tool for Synthesized Testing'
|
7
|
+
s.authors = 'Stuart Caborn, George Malamidis'
|
8
|
+
s.email = 'george@nutrun.com'
|
9
|
+
s.homepage = 'http://synthesis.rubyforge.org'
|
10
|
+
s.has_rdoc = true
|
11
|
+
s.rdoc_options += ['--quiet', '--title', 'Synthesis', '--main', 'README', '--inline-source']
|
12
|
+
s.extra_rdoc_files = ['README', 'COPYING']
|
13
|
+
s.files = [
|
14
|
+
"COPYING",
|
15
|
+
"Rakefile",
|
16
|
+
"README",
|
17
|
+
"synthesis.gemspec",
|
18
|
+
"lib/synthesis/adapter/expectations.rb",
|
19
|
+
"lib/synthesis/adapter/mocha.rb",
|
20
|
+
"lib/synthesis/adapter/rspec.rb",
|
21
|
+
"lib/synthesis/adapter.rb",
|
22
|
+
"lib/synthesis/class.rb",
|
23
|
+
"lib/synthesis/expectation.rb",
|
24
|
+
"lib/synthesis/expectation_interceptor.rb",
|
25
|
+
"lib/synthesis/expectation_matcher.rb",
|
26
|
+
"lib/synthesis/expectation_record.rb",
|
27
|
+
"lib/synthesis/expectation_record_enabled.rb",
|
28
|
+
"lib/synthesis/logging.rb",
|
29
|
+
"lib/synthesis/method_invocation_watcher.rb",
|
30
|
+
"lib/synthesis/module.rb",
|
31
|
+
"lib/synthesis/object.rb",
|
32
|
+
"lib/synthesis/recordable.rb",
|
33
|
+
"lib/synthesis/reporter.rb",
|
34
|
+
"lib/synthesis/runner.rb",
|
35
|
+
"lib/synthesis/task.rb",
|
36
|
+
"lib/synthesis/util/mock_instance/mocha.rb",
|
37
|
+
"lib/synthesis/util/mock_instance/rspec.rb",
|
38
|
+
"lib/synthesis/util/mock_instance.rb",
|
39
|
+
"lib/synthesis.rb"
|
40
|
+
]
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gmalamid-synthesis
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.6
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stuart Caborn, George Malamidis
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-07-08 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: george@nutrun.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README
|
24
|
+
- COPYING
|
25
|
+
files:
|
26
|
+
- COPYING
|
27
|
+
- Rakefile
|
28
|
+
- README
|
29
|
+
- synthesis.gemspec
|
30
|
+
- lib/synthesis/adapter/expectations.rb
|
31
|
+
- lib/synthesis/adapter/mocha.rb
|
32
|
+
- lib/synthesis/adapter/rspec.rb
|
33
|
+
- lib/synthesis/adapter.rb
|
34
|
+
- lib/synthesis/class.rb
|
35
|
+
- lib/synthesis/expectation.rb
|
36
|
+
- lib/synthesis/expectation_interceptor.rb
|
37
|
+
- lib/synthesis/expectation_matcher.rb
|
38
|
+
- lib/synthesis/expectation_record.rb
|
39
|
+
- lib/synthesis/expectation_record_enabled.rb
|
40
|
+
- lib/synthesis/logging.rb
|
41
|
+
- lib/synthesis/method_invocation_watcher.rb
|
42
|
+
- lib/synthesis/module.rb
|
43
|
+
- lib/synthesis/object.rb
|
44
|
+
- lib/synthesis/recordable.rb
|
45
|
+
- lib/synthesis/reporter.rb
|
46
|
+
- lib/synthesis/runner.rb
|
47
|
+
- lib/synthesis/task.rb
|
48
|
+
- lib/synthesis/util/mock_instance/mocha.rb
|
49
|
+
- lib/synthesis/util/mock_instance/rspec.rb
|
50
|
+
- lib/synthesis/util/mock_instance.rb
|
51
|
+
- lib/synthesis.rb
|
52
|
+
has_rdoc: true
|
53
|
+
homepage: http://synthesis.rubyforge.org
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options:
|
56
|
+
- --quiet
|
57
|
+
- --title
|
58
|
+
- Synthesis
|
59
|
+
- --main
|
60
|
+
- README
|
61
|
+
- --inline-source
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: "0"
|
69
|
+
version:
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: "0"
|
75
|
+
version:
|
76
|
+
requirements: []
|
77
|
+
|
78
|
+
rubyforge_project: synthesis
|
79
|
+
rubygems_version: 1.2.0
|
80
|
+
signing_key:
|
81
|
+
specification_version: 2
|
82
|
+
summary: A tool for Synthesized Testing
|
83
|
+
test_files: []
|
84
|
+
|