olag 0.1.10

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,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