doppelganger 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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