doppelganger 0.8.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.
@@ -0,0 +1,239 @@
1
+ # $Id$
2
+
3
+ require 'rubygems'
4
+ require 'rake'
5
+ require 'rake/clean'
6
+ require 'fileutils'
7
+ require 'ostruct'
8
+
9
+ class OpenStruct; undef :gem; end
10
+
11
+ PROJ = OpenStruct.new(
12
+ # Project Defaults
13
+ :name => nil,
14
+ :summary => nil,
15
+ :description => nil,
16
+ :changes => nil,
17
+ :authors => nil,
18
+ :email => nil,
19
+ :url => "\000",
20
+ :version => ENV['VERSION'] || '0.0.0',
21
+ :exclude => %w(tmp$ bak$ ~$ CVS \.svn/ \.git/ ^pkg/),
22
+ :release_name => ENV['RELEASE'],
23
+
24
+ # System Defaults
25
+ :ruby_opts => %w(-w),
26
+ :libs => [],
27
+ :history_file => 'CHANGELOG',
28
+ :manifest_file => 'Manifest.txt',
29
+ :readme_file => 'README.rdoc',
30
+
31
+ # Gem Packaging
32
+ :gem => OpenStruct.new(
33
+ :dependencies => [],
34
+ :development_dependencies => [],
35
+ :executables => nil,
36
+ :extensions => FileList['ext/**/extconf.rb'],
37
+ :files => nil,
38
+ :need_tar => true,
39
+ :need_zip => false,
40
+ :extras => {}
41
+ ),
42
+
43
+ # File Annotations
44
+ :notes => OpenStruct.new(
45
+ :exclude => %w(^tasks/setup\.rb$),
46
+ :extensions => %w(.txt .rb .erb) << '',
47
+ :tags => %w(FIXME OPTIMIZE TODO)
48
+ ),
49
+
50
+ # Rcov
51
+ :rcov => OpenStruct.new(
52
+ :dir => 'coverage',
53
+ :opts => %w[--sort coverage -T --no-html],
54
+ :threshold => 100.0,
55
+ :threshold_exact => false
56
+ ),
57
+
58
+ # Rdoc
59
+ :rdoc => OpenStruct.new(
60
+ :opts => [],
61
+ :include => %w((^lib/) (^ext/) (\.txt$) (\.rdoc$) (^LICENSE|CHANGELOG$)),
62
+ :exclude => %w((^bin/) (extconf\.rb$)),
63
+ :main => nil,
64
+ :dir => 'doc',
65
+ :remote_dir => nil
66
+ ),
67
+
68
+ # Rubyforge
69
+ :rubyforge => OpenStruct.new(
70
+ :name => "\000"
71
+ ),
72
+
73
+ # Test::Unit
74
+ :test => OpenStruct.new(
75
+ :files => FileList['test/**/*_test.rb'],
76
+ :file => 'test/all.rb',
77
+ :opts => []
78
+ )
79
+ )
80
+
81
+ # Load the other rake files in the tasks folder
82
+ tasks_dir = File.expand_path(File.dirname(__FILE__))
83
+ post_load_fn = File.join(tasks_dir, 'post_load.rake')
84
+ rakefiles = Dir.glob(File.join(tasks_dir, '*.rake')).sort
85
+ rakefiles.unshift(rakefiles.delete(post_load_fn)).compact!
86
+ import(*rakefiles)
87
+
88
+ # Setup the project libraries
89
+ %w(lib ext).each {|dir| PROJ.libs << dir if test ?d, dir}
90
+
91
+ # Setup some constants
92
+ WIN32 = %r/djgpp|(cyg|ms|bcc)win|mingw/ =~ RUBY_PLATFORM unless defined? WIN32
93
+
94
+ DEV_NULL = WIN32 ? 'NUL:' : '/dev/null'
95
+
96
+ def quiet( &block )
97
+ io = [STDOUT.dup, STDERR.dup]
98
+ STDOUT.reopen DEV_NULL
99
+ STDERR.reopen DEV_NULL
100
+ block.call
101
+ ensure
102
+ STDOUT.reopen io.first
103
+ STDERR.reopen io.last
104
+ $stdout, $stderr = STDOUT, STDERR
105
+ end
106
+
107
+ DIFF = if WIN32 then 'diff.exe'
108
+ else
109
+ if quiet {system "gdiff", __FILE__, __FILE__} then 'gdiff'
110
+ else 'diff' end
111
+ end unless defined? DIFF
112
+
113
+ SUDO = if WIN32 then ''
114
+ else
115
+ if quiet {system 'which sudo'} then 'sudo'
116
+ else '' end
117
+ end
118
+
119
+ RCOV = WIN32 ? 'rcov.bat' : 'rcov'
120
+ RDOC = WIN32 ? 'rdoc.bat' : 'rdoc'
121
+ GEM = WIN32 ? 'gem.bat' : 'gem'
122
+
123
+ %w(rcov spec/rake/spectask rubyforge bones facets/ansicode).each do |lib|
124
+ begin
125
+ require lib
126
+ Object.instance_eval {const_set "HAVE_#{lib.tr('/','_').upcase}", true}
127
+ rescue LoadError
128
+ Object.instance_eval {const_set "HAVE_#{lib.tr('/','_').upcase}", false}
129
+ end
130
+ end
131
+ HAVE_SVN = (Dir.entries(Dir.pwd).include?('.svn') and
132
+ system("svn --version 2>&1 > #{DEV_NULL}"))
133
+ HAVE_GIT = (Dir.entries(Dir.pwd).include?('.git') and
134
+ system("git --version 2>&1 > #{DEV_NULL}"))
135
+
136
+ # Reads a file at +path+ and spits out an array of the +paragraphs+
137
+ # specified.
138
+ #
139
+ # changes = paragraphs_of('History.txt', 0..1).join("\n\n")
140
+ # summary, *description = paragraphs_of('README.txt', 3, 3..8)
141
+ #
142
+ def paragraphs_of( path, *paragraphs )
143
+ title = String === paragraphs.first ? paragraphs.shift : nil
144
+ ary = File.read(path).delete("\r").split(/\n\n+/)
145
+
146
+ result = if title
147
+ tmp, matching = [], false
148
+ rgxp = %r/^=+\s*#{Regexp.escape(title)}/i
149
+ paragraphs << (0..-1) if paragraphs.empty?
150
+
151
+ ary.each do |val|
152
+ if val =~ rgxp
153
+ break if matching
154
+ matching = true
155
+ rgxp = %r/^=+/i
156
+ elsif matching
157
+ tmp << val
158
+ end
159
+ end
160
+ tmp
161
+ else ary end
162
+
163
+ result.values_at(*paragraphs)
164
+ end
165
+
166
+ # Adds the given gem _name_ to the current project's dependency list. An
167
+ # optional gem _version_ can be given. If omitted, the newest gem version
168
+ # will be used.
169
+ #
170
+ def depend_on( name, version = nil )
171
+ spec = Gem.source_index.find_name(name).last
172
+ version = spec.version.to_s if version.nil? and !spec.nil?
173
+
174
+ PROJ.gem.dependencies << case version
175
+ when nil; [name]
176
+ when %r/^\d/; [name, ">= #{version}"]
177
+ else [name, version] end
178
+ end
179
+
180
+ # Adds the given arguments to the include path if they are not already there
181
+ #
182
+ def ensure_in_path( *args )
183
+ args.each do |path|
184
+ path = File.expand_path(path)
185
+ $:.unshift(path) if test(?d, path) and not $:.include?(path)
186
+ end
187
+ end
188
+
189
+ # Find a rake task using the task name and remove any description text. This
190
+ # will prevent the task from being displayed in the list of available tasks.
191
+ #
192
+ def remove_desc_for_task( names )
193
+ Array(names).each do |task_name|
194
+ task = Rake.application.tasks.find {|t| t.name == task_name}
195
+ next if task.nil?
196
+ task.instance_variable_set :@comment, nil
197
+ end
198
+ end
199
+
200
+ # Change working directories to _dir_, call the _block_ of code, and then
201
+ # change back to the original working directory (the current directory when
202
+ # this method was called).
203
+ #
204
+ def in_directory( dir, &block )
205
+ curdir = pwd
206
+ begin
207
+ cd dir
208
+ return block.call
209
+ ensure
210
+ cd curdir
211
+ end
212
+ end
213
+
214
+ # Scans the current working directory and creates a list of files that are
215
+ # candidates to be in the manifest.
216
+ #
217
+ def manifest_files
218
+ files = []
219
+ exclude = Regexp.new(PROJ.exclude.join('|'))
220
+ Find.find '.' do |path|
221
+ path.sub! %r/^(\.\/|\/)/o, ''
222
+ next unless test ?f, path
223
+ next if path =~ exclude
224
+ files << path
225
+ end
226
+ files.sort!
227
+ end
228
+
229
+ # We need a "valid" method thtat determines if a string is suitable for use
230
+ # in the gem specification.
231
+ #
232
+ class Object
233
+ def valid?
234
+ return !(self.empty? or self == "\000") if self.respond_to?(:to_str)
235
+ return false
236
+ end
237
+ end
238
+
239
+ # EOF
@@ -0,0 +1,34 @@
1
+
2
+ if test(?e, PROJ.test.file) or not PROJ.test.files.to_a.empty?
3
+ require 'rake/testtask'
4
+ if HAVE_RCOV
5
+ require 'rcov/rcovtask'
6
+ end
7
+
8
+ namespace :test do
9
+
10
+ Rake::TestTask.new(:run) do |t|
11
+ t.libs = PROJ.libs
12
+ t.test_files = if test(?f, PROJ.test.file) then [PROJ.test.file]
13
+ else PROJ.test.files end
14
+ t.ruby_opts += PROJ.ruby_opts
15
+ t.ruby_opts += PROJ.test.opts
16
+ end
17
+
18
+ if HAVE_RCOV
19
+ desc 'Run rcov on the unit tests'
20
+ Rcov::RcovTask.new do |t|
21
+ t.test_files = PROJ.test.files
22
+ opts = PROJ.rcov.opts.dup << '-o' << PROJ.rcov.dir
23
+ t.rcov_opts = PROJ.rcov.opts
24
+ end
25
+ end
26
+
27
+ end # namespace :test
28
+
29
+ desc 'Alias to test:run'
30
+ task :test => 'test:run'
31
+
32
+ end
33
+
34
+ # EOF
@@ -0,0 +1,16 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class DoppelgangerArrayTest < Test::Unit::TestCase
4
+
5
+ context 'extended array object' do
6
+ should "identify duplicate elements" do
7
+ assert ![1,1,2].duplicates?(4)
8
+ assert ![1,1,2].duplicates?(2)
9
+ assert [1,1,2].duplicates?(1)
10
+
11
+ assert ![].duplicates?(1)
12
+ end
13
+ end
14
+
15
+ end
16
+
@@ -0,0 +1,112 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class DoppelgangerTest < DoppelgangerTestCase
4
+
5
+ context 'normal analysis' do
6
+ setup do
7
+ duplicate_sample_file_path = File.expand_path(File.join(File.dirname(__FILE__), 'sample_files/duplicate_test_data'))
8
+ @sexps = @analyzer.extract_blocks(duplicate_sample_file_path)
9
+ @duplicate_analysis = Doppelganger::NodeAnalysis.new(@sexps)
10
+ end
11
+
12
+ should "identify duplication" do
13
+ assert @duplicate_analysis.duplication?
14
+ end
15
+
16
+ should "extract :defn nodes" do
17
+ method_arrays = @duplicate_analysis.sexp_blocks.map {|mdef| mdef.node.to_a}
18
+ @the_nodes.each do |node|
19
+ assert_contains method_arrays, node
20
+ end
21
+ end
22
+
23
+ should "isolate duplicated blocks" do
24
+ duplicate_node_arrays = @duplicate_analysis.duplicates.inject([]) do |flattend_dups, dups|
25
+ flattend_dups << dups.first.node.to_a
26
+ flattend_dups << dups.last.node.to_a
27
+ flattend_dups
28
+ end
29
+ assert_contains duplicate_node_arrays, @the_nodes[0]
30
+ assert_contains duplicate_node_arrays, @the_nodes[3]
31
+ end
32
+
33
+ should "attaches filenames to individual nodes" do
34
+ @duplicate_analysis.sexp_blocks.each do |mdef|
35
+ assert_match(/\/\w+_file\.rb$/, mdef.filename)
36
+ end
37
+ end
38
+
39
+ should "attaches line numbers to individual nodes" do
40
+ @duplicate_analysis.sexp_blocks.each do |mdef|
41
+ assert_kind_of Integer, mdef.line
42
+ end
43
+ end
44
+
45
+ teardown do
46
+ @sexps, @duplicate_analysis = nil
47
+ end
48
+ end
49
+
50
+ context 'doing diff anlaysis' do
51
+ setup do
52
+ @sexps = @analyzer.extract_blocks("test/sample_files/larger_diff")
53
+ @larger_diff_analysis = Doppelganger::NodeAnalysis.new(@sexps)
54
+ end
55
+
56
+ should "report methods which differ by arbitrary numbers of diffs" do
57
+ diff = @larger_diff_analysis.diff(5)
58
+ larger_diff_results = diff.inject([]) do |flattend_diffs, diff_pairs|
59
+ flattend_diffs << diff_pairs.first.node.to_a
60
+ flattend_diffs << diff_pairs.last.node.to_a
61
+ flattend_diffs
62
+ end
63
+
64
+ @bigger_diff_blocks.each do |method_node|
65
+ assert_contains larger_diff_results, method_node
66
+ end
67
+ end
68
+
69
+ should "report similar methods by a percent different threshold" do
70
+ diff = @larger_diff_analysis.percent_diff(25)
71
+ percent_diff_results = diff.inject([]) do |flattend_diffs, diff_pairs|
72
+ flattend_diffs << diff_pairs.first.node.to_a
73
+ flattend_diffs << diff_pairs.last.node.to_a
74
+ flattend_diffs
75
+ end
76
+
77
+ @bigger_diff_blocks.each do |method_node|
78
+ assert_contains percent_diff_results, method_node
79
+ end
80
+ end
81
+
82
+ teardown do
83
+ @larger_diff_analysis, @sexps = nil
84
+ end
85
+ end
86
+
87
+ context "percent diff analysis" do
88
+ setup do
89
+ repeats_removal_file_path = File.expand_path(File.join(File.dirname(__FILE__), 'sample_files/repeats_removal_sample_file.rb'))
90
+ @sexps = @analyzer.extract_blocks(repeats_removal_file_path)
91
+ @repeats_removal_analysis = Doppelganger::NodeAnalysis.new(@sexps)
92
+ @repeats_diff = @repeats_removal_analysis.percent_diff(10)
93
+ @analysis_nodes = @repeats_diff.map {|pair|
94
+ [pair.first.node, pair.last.node]
95
+ }
96
+ end
97
+
98
+ should 'ensure that repeated smaller nested nodes are noth included' do
99
+ @repeated_pairs.each do |pair|
100
+ assert !(@analysis_nodes.any? { |node_pair|
101
+ (node_pair.include?(pair.first.node) && node_pair.include?(pair.last.node))
102
+ })
103
+ end
104
+ end
105
+
106
+ teardown do
107
+ @repeats_removal_analysis, @analysis_nodes, @repeats_diff, @sexps = nil
108
+ end
109
+ end
110
+
111
+ end
112
+
@@ -0,0 +1,7 @@
1
+ def foo
2
+ puts :something_unique
3
+ end
4
+
5
+ def bar
6
+ return "something not unique"
7
+ end
@@ -0,0 +1,7 @@
1
+ def foo
2
+ return "also not unique"
3
+ end
4
+
5
+ def baz
6
+ (true || 'is unique')
7
+ end
@@ -0,0 +1,7 @@
1
+ def foo
2
+ puts "muppetfuckers"
3
+ @variable = "foo"
4
+ %w(this is some words).each do |word|
5
+ word.size
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ def bar
2
+ puts "muppetfuckers"
3
+ %w(this is bad words).each do |word|
4
+ puts word
5
+ end
6
+ @variable = "bar"
7
+ end
@@ -0,0 +1,94 @@
1
+ # The Sexp extension of this library excelent examples of repeated comparisons
2
+ # between "blocks" that need to be trimmed from the output. So I've duplicated
3
+ # and frozen it in this file.
4
+
5
+ class Sexp
6
+ # Performs the block on every Sexp in this sexp.
7
+ def deep_each(&block)
8
+ self.each_sexp do |sexp|
9
+ block[sexp]
10
+ sexp.deep_each(&block)
11
+ end
12
+ end
13
+
14
+ # Finds the last line of the Sexp if that information is available.
15
+ def last_line_number
16
+ line_number = nil
17
+ self.deep_each do |sub_node|
18
+ if sub_node.respond_to? :line
19
+ line_number = sub_node.line
20
+ end
21
+ end
22
+ line_number
23
+ end
24
+
25
+ # Maps all sub Sexps into a new Sexp, if the node isn't a Sexp
26
+ # performs the block and maps the result into the new Sexp.
27
+ def map_sexps
28
+ self.inject(s()) do |sexps, sexp|
29
+ unless Sexp === sexp
30
+ sexps << sexp
31
+ else
32
+ sexps << yield(sexp)
33
+ end
34
+ sexps
35
+ end
36
+ end
37
+
38
+ # Rejects all objects in the Sexp that return true for the block.
39
+ def deep_reject(&block)
40
+ output_sexp = self.reject do |node|
41
+ block[node]
42
+ end
43
+ output_sexp.map_sexps do |sexp|
44
+ sexp.deep_reject(&block)
45
+ end
46
+ end
47
+
48
+ # Removes all literals from the Sexp (Symbols aren't excluded as they are used internally
49
+ # by Sexp for node names which identifies structure important for comparison.)
50
+ def remove_literals
51
+ output = self.dup
52
+ output.deep_reject do |node|
53
+ !((node.is_a?(Symbol)) || (node.is_a?(Sexp)))
54
+ end
55
+ end
56
+
57
+ # Iterates through each child Sexp of the current Sexp.
58
+ def each_sexp
59
+ self.each do |sexp|
60
+ next unless Sexp === sexp
61
+
62
+ yield sexp
63
+ end
64
+ end
65
+
66
+ # Performs the block on every Sexp in this sexp, looking for one that returns true.
67
+ def deep_any?(&block)
68
+ self.any_sexp? do |sexp|
69
+ block[sexp] || sexp.deep_any?(&block)
70
+ end
71
+ end
72
+
73
+ # Iterates through each child Sexp of the current Sexp and looks for any Sexp
74
+ # that returns true for the block.
75
+ def any_sexp?
76
+ self.any? do |sexp|
77
+ next unless Sexp === sexp
78
+
79
+ yield sexp
80
+ end
81
+ end
82
+
83
+ # Determines if the passed in block node is contained with in the Sexp node.
84
+ def contains_block?(block_node)
85
+ self.deep_any? do |sexp|
86
+ sexp == block_node
87
+ end
88
+ end
89
+
90
+ # First turns the Sexp into an Array then flattens it.
91
+ def to_flat_ary
92
+ self.to_a.flatten
93
+ end
94
+ end