gmalamid-synthesis 0.1.8 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -34,6 +34,8 @@ Synthesis can be setup to ignore certain classes or modules when collecting expe
34
34
 
35
35
  If +pattern+ is not specified, it will default to <tt>test/**/*_test.rb</tt>
36
36
 
37
+ As of version 0.2.0, Synthesis has a +DOT+ formatter which, when used, will output text in the DOT graph description language, producing system visualizations as specified by the simulated interactions in the system's tests. The output of the +DOT+ formatter can be used with tools like Graphviz( http://www.graphviz.org/ ).
38
+
37
39
  == Usage examples
38
40
 
39
41
  To use with Test::Unit and Mocha, ignoring Array and Hash:
@@ -54,7 +56,7 @@ To use with RSpec, running all specs in the <tt>spec</tt> directory:
54
56
  t.pattern = 'spec/**/*_spec.rb'
55
57
  end
56
58
 
57
- To use with Expectations, redirecting output to a file
59
+ To use with Expectations, redirecting output to a file:
58
60
 
59
61
  require "synthesis/task"
60
62
 
@@ -62,6 +64,23 @@ To use with Expectations, redirecting output to a file
62
64
  t.adapter = :expectations
63
65
  t.out = File.new "synthesis.test.txt", "a"
64
66
  end
67
+
68
+ To output a DOT graph:
69
+
70
+ require "synthesis/task"
71
+
72
+ Synthesis::Task.new do |t|
73
+ t.formatter = :dot
74
+ end
75
+
76
+ To use Synthesis with Rails:
77
+
78
+ require "synthesis/task"
79
+
80
+ Synthesis::Task.new do |t|
81
+ RAILS_ENV = "test"
82
+ t.pattern = 'test/**/*_test.rb'
83
+ end
65
84
 
66
85
  == Utilities
67
86
 
data/Rakefile CHANGED
@@ -1,6 +1,5 @@
1
1
  require "rubygems"
2
2
  require "rake/testtask"
3
- require "rake/gempackagetask"
4
3
  require 'rake/rdoctask'
5
4
  require 'rake/contrib/sshpublisher'
6
5
  require File.dirname(__FILE__) + "/lib/synthesis/task"
@@ -27,15 +26,17 @@ Rake::TestTask.new('test:spec') do |t|
27
26
  t.pattern = 'test/synthesis/adapter/rspec/*_test.rb'
28
27
  end
29
28
 
29
+ # Synthesis test_project tasks
30
+
30
31
  Synthesis::Task.new do |t|
31
32
  t.pattern = 'test_project/mocha/test/*_test.rb'
32
33
  t.ignored = [Array, Hash]
33
34
  # t.out = File.new('synthesis.test.txt', 'a')
34
35
  end
35
36
 
36
- Synthesis::Task.new('synthesis:expectations') do |t|
37
- t.adapter = :expectations
38
- t.pattern = 'test_project/expectations/test/*_test.rb'
37
+ Synthesis::Task.new('synthesis:test:graph') do |t|
38
+ t.pattern = 'test_project/mocha/test/*_test.rb'
39
+ t.formatter = :dot
39
40
  end
40
41
 
41
42
  Synthesis::Task.new('synthesis:spec') do |t|
@@ -43,6 +44,17 @@ Synthesis::Task.new('synthesis:spec') do |t|
43
44
  t.pattern = 'test_project/rspec/*_spec.rb'
44
45
  end
45
46
 
47
+ Synthesis::Task.new('synthesis:spec:graph') do |t|
48
+ t.adapter = :rspec
49
+ t.pattern = 'test_project/rspec/*_spec.rb'
50
+ t.formatter = :dot
51
+ end
52
+
53
+ Synthesis::Task.new('synthesis:expectations') do |t|
54
+ t.adapter = :expectations
55
+ t.pattern = 'test_project/expectations/test/*_test.rb'
56
+ end
57
+
46
58
  namespace :test_project do
47
59
  task :all do
48
60
  STDOUT.puts `rake test_project:mocha`
@@ -78,14 +90,6 @@ task :publish_rdoc do
78
90
  Rake::SshDirPublisher.new("gmalamid@rubyforge.org", "/var/www/gforge-projects/synthesis", "doc").upload
79
91
  end
80
92
 
81
- Rake::GemPackageTask.new(GEMSPEC) do |t|
82
- t.need_zip = false
83
- t.need_tar = false
84
- end
85
-
86
- desc "Remove rdoc and package artefacts"
87
- task :clean => %w[clobber_package clobber_rdoc]
88
-
89
93
  task(:lf) {p Dir["lib/**/*rb"]}
90
94
 
91
95
  task(:check_gemspec) do
@@ -16,6 +16,7 @@ module Synthesis
16
16
  Object.extend(ExpectationRecorder)
17
17
  Object.record_expectations_on(:expects)
18
18
  Mocha::Expectation.extend(ExpectationInterceptor)
19
+ Mocha::Expectation.intercept_test_subject_on(:invoke)
19
20
  Mocha::Expectation.intercept_expected_arguments_on(:with)
20
21
  Mocha::Expectation.intercept_expected_return_values_on(:returns)
21
22
  Mocha::Expectation.intercept_expected_return_values_on(:raises)
@@ -26,6 +26,7 @@ module Synthesis
26
26
  Spec::Mocks::Methods.extend(ExpectationRecorder)
27
27
  Spec::Mocks::Methods.record_expectations_on(:should_receive)
28
28
  Spec::Mocks::MessageExpectation.extend(ExpectationInterceptor)
29
+ Spec::Mocks::MessageExpectation.intercept_test_subject_on(:invoke)
29
30
  Spec::Mocks::MessageExpectation.intercept_expected_arguments_on(:with)
30
31
  Spec::Mocks::MessageExpectation.intercept_expected_return_values_on(:and_return)
31
32
  Spec::Mocks::MessageExpectation.intercept_expected_return_values_on(:and_raise)
@@ -20,10 +20,19 @@ module Synthesis
20
20
  meta_receiver.recordable_method(@method)
21
21
  end
22
22
 
23
+ def add_test_subject(test_subject)
24
+ (@callers ||= []) << test_subject
25
+ end
26
+
27
+ def test_subject
28
+ @callers[0]
29
+ end
30
+
23
31
  def explode
24
32
  if @return_values.size > 1
25
33
  @return_values.map do |v|
26
34
  expectation = self.class.new(@receiver, @method, @track, @args, [])
35
+ expectation.add_test_subject(@callers.shift)
27
36
  expectation.add_return_values(v)
28
37
  expectation
29
38
  end
@@ -79,11 +88,9 @@ module Synthesis
79
88
  @receiver
80
89
  end
81
90
 
82
- def to_s
83
- "(#{return_value_type}) " +
84
- "#{@receiver.name}.#{@method}(#{@args.map { |arg| arg.class } * ', '})" +
85
- "in #{@track}"
86
- end
91
+ def receiver_repr
92
+ @receiver.name
93
+ end
87
94
  end
88
95
 
89
96
  class Instance < Expectation
@@ -99,11 +106,9 @@ module Synthesis
99
106
  meta_receiver
100
107
  end
101
108
 
102
- def to_s
103
- "(#{return_value_type}) #{meta_receiver.name}.new.#{@method}" +
104
- "(#{@args.map { |arg| arg.class } * ', '})" +
105
- "in #{@track}"
106
- end
109
+ def receiver_repr
110
+ "#{meta_receiver.name}.new"
111
+ end
107
112
  end
108
113
 
109
114
  class NilExpectation < Expectation
@@ -3,6 +3,26 @@ module Synthesis
3
3
  # Synthesis to tap into it in order to collect simulated method arguments
4
4
  # and return values.
5
5
  module ExpectationInterceptor
6
+ # Intercept the actual mock proxy to record the test subject so that
7
+ # Synthesis can track which object is being tested
8
+ def intercept_test_subject_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_invoke_method_name) {method_name}
15
+
16
+ def temp_invoke(*expected_parameters, &matching_block)
17
+ synthesis_expectation.add_test_subject(caller(2)) if synthesis_expectation
18
+ send("intercepted_#{get_invoke_method_name}", *expected_parameters, &matching_block)
19
+ end
20
+
21
+ alias_method method_name, :temp_invoke
22
+ undef temp_invoke
23
+ end
24
+ end
25
+
6
26
  # Intercept the mock object framework's expectation method for declaring a mocked
7
27
  # method's arguments so that Synthesis can record them.
8
28
  def intercept_expected_arguments_on(method_name)
@@ -66,6 +86,8 @@ module Synthesis
66
86
  class_eval do
67
87
  remove_method :synthesis_expectation
68
88
  remove_method :synthesis_expectation=
89
+ remove_method :get_invoke_method_name
90
+ remove_method :get_method_name
69
91
  end
70
92
  end
71
93
 
@@ -61,19 +61,9 @@ module Synthesis
61
61
  def untested_expectations
62
62
  expectations.select { |e| !e.invoked? }
63
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 * ', '}"
64
+
65
+ def has_untested_expectations?
66
+ untested_expectations.any?
77
67
  end
78
68
 
79
69
  private
@@ -0,0 +1,91 @@
1
+ require "parse_tree"
2
+ require "sexp"
3
+ require "sexp_processor"
4
+
5
+ module Synthesis
6
+ class DotFormatter < Formatter
7
+ def initialize
8
+ Expectation::Expectation.send(:include, ExpectationReportFormat::Dot)
9
+ end
10
+
11
+ def digraph
12
+ puts "digraph synthesis_expectations {"
13
+ puts " rankdir=LR;"
14
+ puts " size=\"8,10\";"
15
+ puts " ratio=\"fill\";"
16
+ puts " node [shape = circle];"
17
+ puts " edge [color = green]"
18
+ report_tested_expectations
19
+ puts
20
+ puts " edge [color = red]"
21
+ report_untested_expectations
22
+ puts "}"
23
+ end
24
+ alias format_failure digraph
25
+ alias format_success digraph
26
+ end
27
+
28
+ module ExpectationReportFormat
29
+ module Dot
30
+ def to_report
31
+ " \"#{test_subject_name}\" -> \"#{receiver_class}\" " +
32
+ "[ label = \"(#{return_value_type}) #{method}(#{arg_types * ', '})\" ];"
33
+ end
34
+
35
+ private
36
+
37
+ def test_subject_name
38
+ filename, line, method = test_subject[1].split(':')
39
+ method = method.scan(/`(.*)'/)[0][0]
40
+ ruby = File.read(filename)
41
+ parser = ParseTree.new
42
+ sexp = parser.parse_tree_for_string(ruby, filename).first
43
+ sexp = Sexp.from_array(sexp)
44
+ return DotProcessor.process(sexp, method)
45
+ rescue
46
+ filename ? "#{filename} (#{line})" : "?"
47
+ end
48
+
49
+ class DotProcessor < SexpProcessor
50
+ attr_accessor :method
51
+ attr_reader :klazz
52
+
53
+ def self.process(exp, method)
54
+ analyzer = self.new
55
+ analyzer.method = method
56
+ analyzer.process(exp)
57
+ analyzer.klazz
58
+ end
59
+
60
+ def initialize
61
+ super
62
+ self.strict = false
63
+ self.auto_shift_type = true
64
+ @ancestors = []
65
+ end
66
+
67
+ def process_module(exp)
68
+ name = exp.shift
69
+ @ancestors.push name
70
+ result = s(:module, name, process(exp.shift))
71
+ @ancestors.pop
72
+ result
73
+ end
74
+
75
+ def process_class(exp)
76
+ name = exp.shift
77
+ @ancestors.push name
78
+ result = s(:class, name, exp.shift, process(exp.shift))
79
+ @ancestors.pop
80
+ result
81
+ end
82
+
83
+ def process_defn(exp)
84
+ name = exp.shift
85
+ @klazz = @ancestors * '::' if name == method.to_sym
86
+ s(:defn, name, process(exp.shift), process(exp.shift))
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,32 @@
1
+ module Synthesis
2
+ class TextFormatter < Formatter
3
+ include Logging
4
+
5
+ def initialize
6
+ Expectation::Expectation.send(:include, ExpectationReportFormat::Text)
7
+ end
8
+
9
+ def format_success
10
+ log; log "Verified #{ExpectationRecord.expectations.size} expectations"
11
+ log "SUCCESS."
12
+ end
13
+
14
+ def format_failure
15
+ log; log "Tested Expectations: "
16
+ report_tested_expectations
17
+ log; log "Untested Expectations: "
18
+ report_untested_expectations
19
+ log "Ignoring: #{ExpectationRecord.ignored.to_a * ', '}"
20
+ log; log "FAILED."
21
+ end
22
+ end
23
+
24
+ module ExpectationReportFormat
25
+ module Text
26
+ def to_report
27
+ "(#{return_value_type}) #{receiver_repr}.#{@method}" +
28
+ "(#{arg_types * ', '}) in #{@track}"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,21 @@
1
+ module Synthesis
2
+ class Formatter
3
+ def report_tested_expectations
4
+ ExpectationRecord.tested_expectations.each { |e| puts e.to_report }
5
+ end
6
+
7
+ def report_untested_expectations
8
+ ExpectationRecord.untested_expectations.each { |e| puts e.to_report }
9
+ end
10
+
11
+ class << self
12
+ def load
13
+ @formatter.new
14
+ end
15
+
16
+ def inherited(subclass)
17
+ @formatter = subclass
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,8 +1,15 @@
1
1
  module Synthesis
2
2
  class MethodInvocationWatcher
3
3
  def self.invoked(receiver, method, args = [], return_values = [])
4
+ # cal = caller.clone
5
+ # cal.shift # ignore first (eval)
6
+ # path_from_spec = []
7
+ # begin
8
+ # c = cal.shift
9
+ # path_from_spec.unshift c # unless c =~ /(\(eval\)|gems)/
10
+ # end until c =~ /(spec|test)/
4
11
  matcher = Expectation.new(receiver, method, nil, args, return_values)
5
- ExpectationRecord[matcher].invoked!
12
+ ExpectationRecord[matcher].invoked! # path_from_spec
6
13
  end
7
14
  end
8
15
  end
@@ -1,24 +1,13 @@
1
1
  module Synthesis
2
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?
3
+ def self.report
4
+ formatter = Formatter.load
5
+ if ExpectationRecord.has_untested_expectations?
6
+ formatter.format_failure
7
+ return -1
21
8
  end
9
+ formatter.format_success
10
+ 0
22
11
  end
23
12
  end
24
13
  end
@@ -1,7 +1,8 @@
1
1
  module Synthesis
2
2
  class Runner
3
- def self.run(adapter, pattern)
3
+ def self.run(adapter, pattern, formatter)
4
4
  require "synthesis/adapter/#{adapter}"
5
+ require "synthesis/formatter/#{formatter}"
5
6
  Adapter.load(pattern).run
6
7
  at_exit { Reporter.report unless $! }
7
8
  end
@@ -6,7 +6,8 @@ require File.dirname(__FILE__) + "/../synthesis/logging"
6
6
  module Synthesis
7
7
  class Task < Rake::TaskLib
8
8
  include Logging
9
- attr_accessor :verbose, :pattern, :ruby_opts, :adapter, :out, :ignored, :libs
9
+ attr_accessor :verbose, :pattern, :ruby_opts
10
+ attr_accessor :adapter, :out, :ignored, :libs, :formatter
10
11
 
11
12
  def initialize(name='synthesis:test')
12
13
  @name, @ignored, @libs = name, [], []
@@ -14,6 +15,7 @@ module Synthesis
14
15
  @pattern ||= 'test/**/*_test.rb'
15
16
  @ruby_opts ||= []
16
17
  @adapter ||= :mocha
18
+ @formatter ||= :text
17
19
  define
18
20
  end
19
21
 
@@ -37,7 +39,7 @@ module Synthesis
37
39
  require File.dirname(__FILE__) + "/../synthesis/runner"
38
40
  Synthesis::Logging.const_set(:OUT, @out) if @out
39
41
  Synthesis::ExpectationRecord.ignore(*@ignored)
40
- Synthesis::Runner.run(@adapter, @pattern)
42
+ Synthesis::Runner.run(@adapter, @pattern, @formatter)
41
43
  end
42
44
  end
43
45
  self
@@ -15,4 +15,4 @@ module Spec::Mocks::Methods
15
15
 
16
16
  return instance
17
17
  end
18
- end
18
+ end
data/lib/synthesis.rb CHANGED
@@ -14,4 +14,5 @@ require "synthesis/expectation_matcher"
14
14
  require "synthesis/expectation_interceptor"
15
15
  require "synthesis/expectation_recorder"
16
16
  require "synthesis/reporter"
17
+ require "synthesis/formatter"
17
18
  require "synthesis/adapter"
data/synthesis.gemspec CHANGED
@@ -1,10 +1,10 @@
1
1
  GEMSPEC =Gem::Specification.new do |s|
2
2
  s.name = 'synthesis'
3
- s.version = '0.1.8'
3
+ s.version = '0.2.0'
4
4
  s.platform = Gem::Platform::RUBY
5
5
  s.rubyforge_project = "synthesis"
6
6
  s.summary, s.description = 'A tool for Synthesized Testing'
7
- s.authors = 'Stuart Caborn, George Malamidis'
7
+ s.authors = 'Stuart Caborn, George Malamidis, Danilo Sato'
8
8
  s.email = 'george@nutrun.com'
9
9
  s.homepage = 'http://synthesis.rubyforge.org'
10
10
  s.has_rdoc = true
@@ -24,6 +24,9 @@ GEMSPEC =Gem::Specification.new do |s|
24
24
  "lib/synthesis/expectation_interceptor.rb",
25
25
  "lib/synthesis/expectation_matcher.rb",
26
26
  "lib/synthesis/expectation_record.rb",
27
+ "lib/synthesis/formatter/dot.rb",
28
+ "lib/synthesis/formatter/text.rb",
29
+ "lib/synthesis/formatter.rb",
27
30
  "lib/synthesis/expectation_recorder.rb",
28
31
  "lib/synthesis/logging.rb",
29
32
  "lib/synthesis/method_invocation_watcher.rb",
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gmalamid-synthesis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
- - Stuart Caborn, George Malamidis
7
+ - Stuart Caborn, George Malamidis, Danilo Sato
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-10-09 00:00:00 -07:00
12
+ date: 2008-10-27 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -36,6 +36,9 @@ files:
36
36
  - lib/synthesis/expectation_interceptor.rb
37
37
  - lib/synthesis/expectation_matcher.rb
38
38
  - lib/synthesis/expectation_record.rb
39
+ - lib/synthesis/formatter/dot.rb
40
+ - lib/synthesis/formatter/text.rb
41
+ - lib/synthesis/formatter.rb
39
42
  - lib/synthesis/expectation_recorder.rb
40
43
  - lib/synthesis/logging.rb
41
44
  - lib/synthesis/method_invocation_watcher.rb