olag 0.1.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,59 @@
1
+ module Olag
2
+
3
+ # Create ChangeLog files based on the Git revision history.
4
+ class ChangeLog
5
+
6
+ # Write a changelog based on the Git log.
7
+ def initialize(path)
8
+ @subjects_by_id = {}
9
+ @sorted_ids = []
10
+ read_log_lines
11
+ File.open(path, "w") do |file|
12
+ @log_file = file
13
+ write_log_file
14
+ end
15
+ end
16
+
17
+ protected
18
+
19
+ # Read all the log lines from Git's revision history.
20
+ def read_log_lines
21
+ IO.popen("git log --pretty='format:%ci::%an <%ae>::%s'", "r").each_line do |log_line|
22
+ load_log_line(log_line)
23
+ end
24
+ end
25
+
26
+ # Load a single Git log line into memory.
27
+ def load_log_line(log_line)
28
+ id, subject = ChangeLog.parse_log_line(log_line)
29
+ @sorted_ids << id
30
+ @subjects_by_id[id] ||= []
31
+ @subjects_by_id[id] << subject
32
+ end
33
+
34
+ # Extract the information we need (ChangeLog entry id and subject) from a
35
+ # Git log line.
36
+ def self.parse_log_line(log_line)
37
+ date, author, subject = log_line.chomp.split("::")
38
+ date, time, zone = date.split(" ")
39
+ id = "#{date}\t#{author}"
40
+ return id, subject
41
+ end
42
+
43
+ # Write a ChangeLog file based on the read Git log lines.
44
+ def write_log_file
45
+ @sorted_ids.uniq.each do |id|
46
+ write_log_entry(id, @subjects_by_id[id])
47
+ end
48
+ end
49
+
50
+ # Write a single ChaneLog entry.
51
+ def write_log_entry(id, subjects)
52
+ @log_file.puts "#{id}\n\n"
53
+ @log_file.puts subjects.map { |subject| "\t* #{subject}" }.join("\n")
54
+ @log_file.puts "\n"
55
+ end
56
+
57
+ end
58
+
59
+ end
@@ -0,0 +1,20 @@
1
+ module Olag
2
+
3
+ # Provide access to data files packaged with the gem.
4
+ module DataFiles
5
+
6
+ # Given the name of a data file packaged in some gem, return the absolute
7
+ # disk path for accessing that data file. This is similar to what +require+
8
+ # does internally, but here we just want the path for reading data, rather
9
+ # than load the Ruby code in the file.
10
+ def self.expand_path(relative_path)
11
+ $LOAD_PATH.each do |load_directory|
12
+ absolute_path = File.expand_path(load_directory + "/" + relative_path)
13
+ return absolute_path if File.exist?(absolute_path)
14
+ end
15
+ return relative_path # This will cause "file not found error" down the line.
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,44 @@
1
+ module Olag
2
+
3
+ # Collect a list of errors.
4
+ class Errors < Array
5
+
6
+ # Create an empty errors collection.
7
+ def initialize
8
+ @path = nil
9
+ @line = nil
10
+ end
11
+
12
+ # Associate all errors collected by a block with a specific disk file.
13
+ def in_path(path, &block)
14
+ prev_path, prev_line = @path, @line
15
+ @path, @line = path, nil
16
+ result = block.call
17
+ @path, @line = prev_path, prev_line
18
+ return result
19
+ end
20
+
21
+ # Set the line number for any errors collected from here on.
22
+ def at_line(line)
23
+ @line = line
24
+ end
25
+
26
+ # Add a single error to the collection, with automatic context annotation
27
+ # (current disk file and line). Other methods (push, += etc.) do not
28
+ # automatically add the context annotation.
29
+ def <<(message)
30
+ push(annotate_error_message(message))
31
+ end
32
+
33
+ protected
34
+
35
+ # Annotate an error message with the context (current file and line).
36
+ def annotate_error_message(message)
37
+ return "#{$0}: #{message}" unless @path
38
+ return "#{$0}: #{message} in file: #{@path}" unless @line
39
+ return "#{$0}: #{message} in file: #{@path} at line: #{@line}"
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,77 @@
1
+ # Monkey-patch within the Gem module.
2
+ module Gem
3
+
4
+ # Enhanced automated gem specification.
5
+ class Specification
6
+
7
+ # The title of the gem for documentation (by default, the capitalized
8
+ # name).
9
+ attr_accessor :title
10
+
11
+ # The name of the file containing the gem's version (by default,
12
+ # <tt>lib/_name_/version.rb</tt>).
13
+ attr_accessor :version_file
14
+
15
+ alias_method :original_initialize, :initialize
16
+
17
+ # Create the gem specification. Requires a block to set up the basic gem
18
+ # information (name, version, author, email, description). In addition, the
19
+ # block may override default properties (e.g. title).
20
+ def initialize(&block)
21
+ original_initialize(&block)
22
+ setup_default_members
23
+ add_development_dependencies
24
+ setup_file_members
25
+ setup_rdoc
26
+ end
27
+
28
+ # Set the new data members to their default values, unless they were
29
+ # already set by the gem specification block.
30
+ def setup_default_members
31
+ name = self.name
32
+ @title ||= name.capitalize
33
+ @version_file ||= "lib/#{name}/version.rb"
34
+ end
35
+
36
+ # Add dependencies required for developing the gem.
37
+ def add_development_dependencies
38
+ add_dependency("olag") unless self.name == "olag"
39
+ %w(Saikuro codnar fakefs flay rake rcov rdoc reek roodi test-spec).each do |gem|
40
+ add_development_dependency(gem)
41
+ end
42
+ end
43
+
44
+ # Initialize the standard gem specification file list members.
45
+ def setup_file_members
46
+ # These should cover all the gem's files, except for the extra rdoc
47
+ # files.
48
+ setup_file_member(:files, "{lib,doc}/**/*")
49
+ self.files << "Rakefile" << "codnar.html"
50
+ setup_file_member(:executables, "bin/*") { |path| path.sub("bin/", "") }
51
+ setup_file_member(:test_files, "test/**/*")
52
+ end
53
+
54
+ # Initialize a standard gem specification file list member to the files
55
+ # matching a pattern. If a block is given, it is used to map the file
56
+ # paths. This will append to the file list member if it has already been
57
+ # set by the gem specification block.
58
+ def setup_file_member(member, pattern, &block)
59
+ old_value = instance_variable_get("@#{member}")
60
+ new_value = FileList[pattern].find_all { |path| File.file?(path) }
61
+ new_value.map!(&block) if block
62
+ instance_variable_set("@#{member}", old_value + new_value)
63
+ end
64
+
65
+ # Setup RDOC options in the gem specification.
66
+ def setup_rdoc
67
+ self.extra_rdoc_files = [ "README.rdoc", "LICENSE", "ChangeLog" ]
68
+ self.rdoc_options << "--title" << "#{title} #{version}" \
69
+ << "--main" << "README.rdoc" \
70
+ << "--line-numbers" \
71
+ << "--all" \
72
+ << "--quiet"
73
+ end
74
+
75
+ end
76
+
77
+ end
@@ -0,0 +1,43 @@
1
+ module Olag
2
+
3
+ # Save and restore the global variables when running an application inside a
4
+ # test.
5
+ class Globals
6
+
7
+ # Run some code without affecting the global state.
8
+ def self.without_changes(&block)
9
+ state = Globals.new
10
+ begin
11
+ return block.call
12
+ ensure
13
+ state.restore
14
+ end
15
+ end
16
+
17
+ # Restore the relevant global variables.
18
+ def restore
19
+ $stdin = Globals.restore_file($stdin, @original_stdin)
20
+ $stdout = Globals.restore_file($stdout, @original_stdout)
21
+ $stderr = Globals.restore_file($stderr, @original_stderr)
22
+ ARGV.replace(@original_argv)
23
+ end
24
+
25
+ protected
26
+
27
+ # Take a snapshot of the relevant global variables.
28
+ def initialize
29
+ @original_stdin = $stdin
30
+ @original_stdout = $stdout
31
+ @original_stderr = $stderr
32
+ @original_argv = ARGV.dup
33
+ end
34
+
35
+ # Restore a specific global file variable to its original state.
36
+ def self.restore_file(current, original)
37
+ current.close unless current == original
38
+ return original
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,12 @@
1
+ # Extend the core Hash class.
2
+ class Hash
3
+
4
+ # Provide OpenStruct/JavaScript-like implicit <tt>.key</tt> and
5
+ # <tt>.key=</tt> methods.
6
+ def method_missing(method, *arguments)
7
+ method = method.to_s
8
+ key = method.chomp("=")
9
+ return method == key ? self[key] : self[key] = arguments[0]
10
+ end
11
+
12
+ end
data/lib/olag/rake.rb ADDED
@@ -0,0 +1,263 @@
1
+ require "codnar/rake"
2
+ require "olag/change_log"
3
+ require "olag/gem_specification"
4
+ require "olag/update_version"
5
+ require "olag/version"
6
+ require "rake/clean"
7
+ require "rake/gempackagetask"
8
+ require "rake/rdoctask"
9
+ require "rake/testtask"
10
+ require "rcov/rcovtask"
11
+ require "reek/rake/task"
12
+ require "roodi"
13
+
14
+ module Olag
15
+
16
+ # Automate Rake task creation for a gem.
17
+ class Rake
18
+
19
+ # Define all the Rake tasks.
20
+ def initialize(spec)
21
+ @spec = spec
22
+ @ruby_sources = @spec.files.find_all { |file| file =~ /^Rakefile$|\.rb$/ }
23
+ @weave_configurations = [ :weave_include, :weave_named_chunk_with_containers ]
24
+ task(:default => :all)
25
+ define_all_task
26
+ CLOBBER << "saikuro"
27
+ end
28
+
29
+ protected
30
+
31
+ # Define a task that does "everything".
32
+ def define_all_task
33
+ define_desc_task("Version, verify, document, package", :all => [ :version, :verify, :doc, :package ])
34
+ define_verify_task
35
+ define_doc_task
36
+ define_commit_task
37
+ # This is a problem. If the version number gets updated, GemPackageTask
38
+ # fails. This is better than if it used the old version number, I
39
+ # suppose, but not as nice as if it just used @spec.version everywhere.
40
+ # The solution for this is to do a dry run before doing the final +rake+
41
+ # +commit+, which is a good idea in general.
42
+ ::Rake::GemPackageTask.new(@spec) { |package| }
43
+ end
44
+
45
+ # {{{ Verify gem functionality
46
+
47
+ # Define a task to verify everything is OK.
48
+ def define_verify_task
49
+ define_desc_task("Test, coverage, analyze code", :verify => [ :coverage, :analyze ])
50
+ define_desc_class("Test code covarage with RCov", Rcov::RcovTask, "coverage") { |task| Rake.configure_coverage_task(task) }
51
+ define_desc_class("Test code without coverage", ::Rake::TestTask, "test") { |task| Rake.configure_test_task(task) }
52
+ define_analyze_task
53
+ end
54
+
55
+ # Configure a task to run all tests and verify 100% coverage. This is
56
+ # cheating a bit, since it will not complain about files that are not
57
+ # reached at all from any of the tests.
58
+ def self.configure_coverage_task(task)
59
+ task.test_files = FileList["test/*.rb"]
60
+ task.libs << "lib" << "test/lib"
61
+ task.rcov_opts << "--failure-threshold" << "100" \
62
+ << "--exclude" << "^/"
63
+ # Strangely, on some occasions, RCov crazily tries to compute coverage
64
+ # for files inside Ruby's gem repository. Excluding all absolute paths
65
+ # prevents this.
66
+ end
67
+
68
+ # Configure a task to just run the tests without verifying coverage.
69
+ def self.configure_test_task(task)
70
+ task.test_files = FileList["test/*.rb"]
71
+ task.libs << "lib" << "test/lib"
72
+ end
73
+
74
+ # }}}
75
+
76
+ # {{{ Analyze the source code
77
+
78
+ # Define a task to verify the source code is clean.
79
+ def define_analyze_task
80
+ define_desc_task("Analyze source code", :analyze => [ :reek, :roodi, :flay, :saikuro ])
81
+ define_desc_class("Check for smelly code with Reek", Reek::Rake::Task) { |task| configure_reek_task(task) }
82
+ define_desc_task("Check for smelly code with Roodi", :roodi) { run_roodi_task }
83
+ define_desc_task("Check for duplicated code with Flay", :flay) { run_flay_task }
84
+ define_desc_task("Check for complex code with Saikuro", :saikuro) { run_saikuro_task }
85
+ end
86
+
87
+ # Configure a task to ensure there are no code smells using Reek.
88
+ def configure_reek_task(task)
89
+ task.reek_opts << "--quiet"
90
+ task.source_files = @ruby_sources
91
+ end
92
+
93
+ # Run Roodi to ensure there are no code smells.
94
+ def run_roodi_task
95
+ runner = Roodi::Core::Runner.new
96
+ runner.config = "roodi.config" if File.exist?("roodi.config")
97
+ @ruby_sources.each { |ruby_source| runner.check_file(ruby_source) }
98
+ (errors = runner.errors).each { |error| puts(error) }
99
+ raise "Roodi found #{errors.size} errors." unless errors.empty?
100
+ end
101
+
102
+ # Run Flay to ensure there are no duplicated code fragments.
103
+ def run_flay_task
104
+ dirs = %w(bin lib test/lib).find_all { |dir| File.exist?(dir) }
105
+ result = IO.popen("flay " + dirs.join(' '), "r").read.chomp
106
+ return if result == "Total score (lower is better) = 0\n"
107
+ puts(result)
108
+ raise "Flay found code duplication."
109
+ end
110
+
111
+ # Run Saikuro to ensure there are no overly complex functions.
112
+ def run_saikuro_task
113
+ dirs = %w(bin lib test).find_all { |dir| File.exist?(dir) }
114
+ system("saikuro -c -t -y 0 -e 10 -o saikuro/ -i #{dirs.join(' -i ')} > /dev/null")
115
+ result = File.read("saikuro/index_cyclo.html")
116
+ raise "Saikuro found complicated code." if result.include?("Errors and Warnings")
117
+ end
118
+
119
+ # }}}
120
+
121
+ # {{{ Generate RDoc documentation
122
+
123
+ # Define a task to build all the documentation.
124
+ def define_doc_task
125
+ desc "Generate all documentation"
126
+ task :doc => [ :rdoc, :codnar ]
127
+ ::Rake::RDocTask.new { |task| configure_rdoc_task(task) }
128
+ define_codnar_task
129
+ end
130
+
131
+ # Configure a task to build the RDoc documentation.
132
+ def configure_rdoc_task(task)
133
+ task.rdoc_files += @ruby_sources.reject { |file| file =~ /^test|Rakefile/ } + [ "LICENSE", "README.rdoc" ]
134
+ task.main = "README.rdoc"
135
+ task.rdoc_dir = "rdoc"
136
+ task.options = @spec.rdoc_options
137
+ end
138
+
139
+ # }}}
140
+
141
+ # {{{ Generate Codnar documentation
142
+
143
+ # A set of file Regexp patterns and their matching Codnar configurations.
144
+ # All the gem files are matched agains these patterns, in order, and a
145
+ # Codnar::SplitTask is created for the first matching one. If the matching
146
+ # configuration list is empty, the file is not split. However, the file
147
+ # must match at least one of the patterns. The gem is expected to modify
148
+ # this array, if needed, before creating the Rake object.
149
+ CODNAR_CONFIGURATIONS = [
150
+ [
151
+ # Exclude the ChangeLog and generated codnar.html files from the
152
+ # generated documentation.
153
+ "ChangeLog|codnar\.html",
154
+ ], [
155
+ # Configurations for splitting Ruby files. Using Sunlight makes for
156
+ # fast splitting but slow viewing. Using GVim is the reverse.
157
+ "Rakefile|.*\.rb|bin/.*",
158
+ "classify_source_code:ruby",
159
+ "format_code_gvim_css:ruby",
160
+ "classify_shell_comments",
161
+ "format_rdoc_comments",
162
+ "chunk_by_vim_regions",
163
+ ], [
164
+ # Configurations for HTML documentation files.
165
+ ".*\.html",
166
+ "split_html_documentation",
167
+ ], [
168
+ # Configurations for Markdown documentation files.
169
+ ".*\.markdown|.*\.md",
170
+ "split_markdown_documentation",
171
+ ], [
172
+ # Configurations for RDOC documentation files.
173
+ "LICENSE|.*\.rdoc",
174
+ "split_rdoc_documentation",
175
+ ],
176
+ ]
177
+
178
+ # Define a task to build the Codnar documentation.
179
+ def define_codnar_task
180
+ @spec.files.each do |file|
181
+ configurations = Rake.split_configurations(file)
182
+ Codnar::Rake::SplitTask.new([ file ], configurations) unless configurations == []
183
+ end
184
+ Codnar::Rake::WeaveTask.new("doc/root.html", [ :weave_include, :weave_named_chunk_with_containers ])
185
+ end
186
+
187
+ # Find the Codnar split configurations for a file.
188
+ def self.split_configurations(file)
189
+ CODNAR_CONFIGURATIONS.each do |configurations|
190
+ regexp = configurations[0] = convert_to_regexp(configurations[0])
191
+ return configurations[1..-1] if regexp.match(file)
192
+ end
193
+ abort("No Codnar configuration for file: #{file}")
194
+ end
195
+
196
+ # Convert a string configuration pattern to a real Regexp.
197
+ def self.convert_to_regexp(regexp)
198
+ return regexp if Regexp == regexp
199
+ begin
200
+ return Regexp.new("^(#{regexp})$")
201
+ rescue
202
+ abort("Invalid pattern regexp: ^(#{regexp})$ error: #{$!}")
203
+ end
204
+ end
205
+
206
+ # }}}
207
+
208
+ # {{{ Automate Git commit process
209
+
210
+ # Define a task that commit changes to Git.
211
+ def define_commit_task
212
+ define_desc_task("Git commit process", :commit => [ :all, :first_commit, :changelog, :second_commit ])
213
+ define_desc_task("Update version file from Git", :version) { update_version_file }
214
+ define_desc_task("Perform the 1st (main) Git commit", :first_commit) { run_git_first_commit }
215
+ define_desc_task("Perform the 2nd (amend) Git commit", :second_commit) { run_git_second_commit }
216
+ define_desc_task("Update ChangeLog from Git", :changelog) { Olag::ChangeLog.new("ChangeLog") }
217
+ end
218
+
219
+ # Update the content of the version file to contain the correct Git-derived
220
+ # build number.
221
+ def update_version_file
222
+ version_file = @spec.version_file
223
+ updated_version = Olag::Version::update(version_file)
224
+ abort("Updated gem version; re-run rake") if @spec.version.to_s != updated_version
225
+ end
226
+
227
+ # Run the first Git commit. The user will be given an editor to review the
228
+ # commit and enter a commit message.
229
+ def run_git_first_commit
230
+ raise "Git 1st commit failed" unless system("set +x; git commit")
231
+ end
232
+
233
+ # Run the second Git commit. This amends the first commit with the updated
234
+ # ChangeLog.
235
+ def run_git_second_commit
236
+ raise "Git 2nd commit failed" unless system("set +x; EDITOR=true git commit --amend ChangeLog")
237
+ end
238
+
239
+ # }}}
240
+
241
+ # {{{ Task utilities
242
+
243
+ # Define a new task with a description.
244
+ def define_desc_task(description, *parameters)
245
+ desc(description)
246
+ task(*parameters) do
247
+ yield(task) if block_given?
248
+ end
249
+ end
250
+
251
+ # Define a new task using some class.
252
+ def define_desc_class(description, klass, *parameters)
253
+ desc(description)
254
+ klass.new(*parameters) do |task|
255
+ yield(task) if block_given?
256
+ end
257
+ end
258
+
259
+ # }}}
260
+
261
+ end
262
+
263
+ end