Filled out the project, added Ezmlm module + spec.

This commit is contained in:
Michael Granger 2008-05-07 18:22:04 +00:00
parent 4b12c97f6b
commit 00d3974363
15 changed files with 1548 additions and 0 deletions

29
LICENSE Normal file
View file

@ -0,0 +1,29 @@
Copyright (c) 2008, LAIKA, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are
permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
* Neither the name of LAIKA, nor the names of its contributors may be used to
endorse or promote products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

45
README Normal file
View file

@ -0,0 +1,45 @@
= ThingFish
== Authors
* Michael Granger <mgranger@laika.com>
* Jeremiah Jordan <jjordan@laika.com>
== Installation
=== Requirements
Ruby-Ezmlm is tested using Ruby 1.8.6:
* Ruby (>= 1.8.6): http://www.ruby-lang.org/en/downloads/
Other versions may work, but are not tested.
=== Ruby Modules
There are two ways you can install Ruby-Ezmlm. The easiest is to use RubyGems:
$ sudo gem install -y ruby-ezmlm
If you'd rather install from source, you'll need these:
* Tmail (>= 1.2.3.1): http://tmail.rubyforge.org/
Once these are installed:
$ tar -zxvf ezmlm-0.0.1.tgz
$ cd thingfish-0.0.1
$ su -
# rake install
If you want to help out with development, run tests, or generate documentation,
you'll need a few more things:
* RSpec (>= 1.0.5): http://rspec.rubyforge.org/
* rcov (>= 0.7.0): http://eigenclass.org/hiki.rb?rcov
If you have RubyGems installed, you can install these automatically via the
+install_dependencies+ task of the Rakefile:
$ sudo rake install_dependencies

146
Rakefile Normal file
View file

@ -0,0 +1,146 @@
#!rake -*- ruby -*-
#
# Ruby-Ezmlm rakefile
#
# Based on Ben Bleything's Rakefile for Linen (URL?)
#
# Copyright (c) 2007 LAIKA, Inc.
#
# Mistakes:
# * Michael Granger <mgranger@laika.com>
#
BEGIN {
require 'pathname'
basedir = Pathname.new( __FILE__ ).dirname
libdir = basedir + 'lib'
$LOAD_PATH.unshift( libdir.to_s ) unless $LOAD_PATH.include?( libdir.to_s )
}
require 'rubygems'
require 'rake'
require 'tmpdir'
require 'pathname'
$dryrun = false
# Pathname constants
BASEDIR = Pathname.new( __FILE__ ).expand_path.dirname.relative_path_from( Pathname.getwd )
BINDIR = BASEDIR + 'bin'
LIBDIR = BASEDIR + 'lib'
DOCSDIR = BASEDIR + 'docs'
VARDIR = BASEDIR + 'var'
WWWDIR = VARDIR + 'www'
MANUALDIR = DOCSDIR + 'manual'
RDOCDIR = DOCSDIR + 'rdoc'
STATICWWWDIR = WWWDIR + 'static'
PKGDIR = BASEDIR + 'pkg'
ARTIFACTS_DIR = Pathname.new( ENV['CC_BUILD_ARTIFACTS'] || '' )
RAKE_TASKDIR = BASEDIR + 'rake'
TEXT_FILES = %w( Rakefile README LICENSE ).
collect {|filename| BASEDIR + filename }
SPECDIR = BASEDIR + 'spec'
SPEC_FILES = Pathname.glob( SPECDIR + '**/*_spec.rb' ).
delete_if {|item| item =~ /\.svn/ }
# Ideally, this should be automatically generated.
SPEC_EXCLUDES = 'spec,monkeypatches,/Library/Ruby,/var/lib,/usr/local/lib'
BIN_FILES = Pathname.glob( BINDIR + '*').
delete_if {|item| item =~ /\.svn/ }
LIB_FILES = Pathname.glob( LIBDIR + '**/*.rb').
delete_if {|item| item =~ /\.svn/ }
RELEASE_FILES = BIN_FILES + TEXT_FILES + LIB_FILES + SPEC_FILES
require RAKE_TASKDIR + 'helpers.rb'
### Package constants
PKG_NAME = 'ruby-ezmlm'
PKG_VERSION_FROM = LIBDIR + 'ezmlm.rb'
PKG_VERSION = find_pattern_in_file( /VERSION = '(\d+\.\d+\.\d+)'/, PKG_VERSION_FROM ).first
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
RELEASE_NAME = "REL #{PKG_VERSION}"
require RAKE_TASKDIR + 'svn.rb'
require RAKE_TASKDIR + 'verifytask.rb'
if Rake.application.options.trace
$trace = true
log "$trace is enabled"
end
if Rake.application.options.dryrun
$dryrun = true
log "$dryrun is enabled"
Rake.application.options.dryrun = false
end
### Project Gemspec
GEMSPEC = Gem::Specification.new do |gem|
pkg_build = get_svn_rev( BASEDIR ) || 0
gem.name = PKG_NAME
gem.version = "%s.%s" % [ PKG_VERSION, pkg_build ]
gem.summary = "A Ruby programmatic interface to ezmlm-idx"
gem.description = "Ruby-Ezmlm is a Ruby programmatic interface to ezmlm-idx " +
"mailing lists, message archives, and command-line tools."
gem.authors = "Michael Granger, Jeremiah Jordan"
gem.email = "opensource@laika.com"
gem.homepage = "http://opensource.laika.com/wiki/ruby-ezmlm"
gem.rubyforge_project = 'laika'
gem.has_rdoc = true
gem.files = RELEASE_FILES.
collect {|f| f.relative_path_from(BASEDIR).to_s }
gem.test_files = SPEC_FILES.
collect {|f| f.relative_path_from(BASEDIR).to_s }
gem.add_dependency( 'tmail', '>= 1.2.3.1' )
end
# Load task plugins
Pathname.glob( RAKE_TASKDIR + '*.rb' ).each do |tasklib|
trace "Loading task lib #{tasklib}"
require tasklib
end
### Default task
task :default => [:clean, :spec, 'coverage:verify', :package]
### Task: clean
desc "Clean pkg, coverage, and rdoc; remove .bak files"
task :clean => [ :clobber_rdoc, :clobber_package, :clobber_coverage ] do
files = FileList['**/*.bak']
files.clear_exclude
File.rm( files ) unless files.empty?
FileUtils.rm_rf( 'artifacts' )
end
### Cruisecontrol task
desc "Cruisecontrol build"
task :cruise => [:clean, :coverage, :package] do |task|
raise "Artifacts dir not set." if ARTIFACTS_DIR.to_s.empty?
artifact_dir = ARTIFACTS_DIR.cleanpath
artifact_dir.mkpath
$stderr.puts "Copying coverage stats..."
FileUtils.cp_r( 'coverage', artifact_dir )
$stderr.puts "Copying packages..."
FileUtils.cp_r( FileList['pkg/*'].to_a, artifact_dir )
end

61
lib/ezmlm.rb Normal file
View file

@ -0,0 +1,61 @@
#!/usr/bin/ruby
#
# A Ruby programmatic interface to the ezmlm-idx mailing list system
#
# == Version
#
# $Id$
#
# == Authors
#
# * Michael Granger <mgranger@laika.com>
# * Jeremiah Jordan <jjordan@laika.com>
#
# :include: LICENSE
#
#---
#
# Please see the file LICENSE in the base directory for licensing details.
#
require 'pathname'
### Toplevel namespace module
module Ezmlm
# SVN Revision
SVNRev = %q$Rev$
# SVN Id
SVNId = %q$Id$
# Package version
VERSION = '0.0.1'
require 'ezmlm/list'
###############
module_function
###############
### Iterate over each directory that looks like an Ezmlm list in the specified +listsdir+ and
### yield it as an Ezmlm::List object.
def each_list( listsdir )
listsdir = Pathname.new( listsdir )
Pathname.glob( listsdir + '*' ) do |entry|
next unless entry.directory?
next unless ( entry + 'mailinglist' ).exist?
yield( Ezmlm::List.new(entry) )
end
end
end # module Ezmlm
# vim: set nosta noet ts=4 sw=4:

30
lib/ezmlm/list.rb Normal file
View file

@ -0,0 +1,30 @@
#!/usr/bin/ruby
#
# A Ruby interface to a single Ezmlm-idx mailing list directory
#
# == Version
#
# $Id$
#
# == Authors
#
# * Michael Granger <mgranger@laika.com>
# * Jeremiah Jordan <jjordan@laika.com>
#
# :include: LICENSE
#
#---
#
# Please see the file LICENSE in the base directory for licensing details.
#
require 'pathname'
require 'ezmlm'
### A Ruby interface to an ezmlm-idx mailing list directory
class Ezmlm::List
end
# vim: set nosta noet ts=4 sw=4:

353
rake/helpers.rb Normal file
View file

@ -0,0 +1,353 @@
#####################################################################
### G L O B A L H E L P E R F U N C T I O N S
#####################################################################
require 'pathname'
require 'readline'
require 'open3'
# Set some ANSI escape code constants (Shamelessly stolen from Perl's
# Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com>
ANSI_ATTRIBUTES = {
'clear' => 0,
'reset' => 0,
'bold' => 1,
'dark' => 2,
'underline' => 4,
'underscore' => 4,
'blink' => 5,
'reverse' => 7,
'concealed' => 8,
'black' => 30, 'on_black' => 40,
'red' => 31, 'on_red' => 41,
'green' => 32, 'on_green' => 42,
'yellow' => 33, 'on_yellow' => 43,
'blue' => 34, 'on_blue' => 44,
'magenta' => 35, 'on_magenta' => 45,
'cyan' => 36, 'on_cyan' => 46,
'white' => 37, 'on_white' => 47
}
CLEAR_TO_EOL = "\e[K"
CLEAR_CURRENT_LINE = "\e[2K"
### Output a logging message
def log( *msg )
output = colorize( msg.flatten.join(' '), 'cyan' )
$deferr.puts( output )
end
### Output a logging message if tracing is on
def trace( *msg )
return unless $trace
output = colorize( msg.flatten.join(' '), 'yellow' )
$deferr.puts( output )
end
### Run the specified command +cmd+ with system(), failing if the execution
### fails.
def run( *cmd )
cmd.flatten!
log( cmd.collect {|part| part =~ /\s/ ? part.inspect : part} )
if $dryrun
$deferr.puts "(dry run mode)"
else
system( *cmd )
unless $?.success?
fail "Command failed: [%s]" % [cmd.join(' ')]
end
end
end
### Open a pipe to a process running the given +cmd+ and call the given block with it.
def pipeto( *cmd )
$DEBUG = true
cmd.flatten!
log( "Opening a pipe to: ", cmd.collect {|part| part =~ /\s/ ? part.inspect : part} )
if $dryrun
$deferr.puts "(dry run mode)"
else
open( '|-', 'w+' ) do |io|
# Parent
if io
yield( io )
# Child
else
exec( *cmd )
fail "Command failed: [%s]" % [cmd.join(' ')]
end
end
end
end
### Download the file at +sourceuri+ via HTTP and write it to +targetfile+.
def download( sourceuri, targetfile )
oldsync = $defout.sync
$defout.sync = true
require 'net/http'
require 'uri'
targetpath = Pathname.new( targetfile )
log "Downloading %s to %s" % [sourceuri, targetfile]
targetpath.open( File::WRONLY|File::TRUNC|File::CREAT, 0644 ) do |ofh|
url = URI.parse( sourceuri )
downloaded = false
limit = 5
until downloaded or limit.zero?
Net::HTTP.start( url.host, url.port ) do |http|
req = Net::HTTP::Get.new( url.path )
http.request( req ) do |res|
if res.is_a?( Net::HTTPSuccess )
log "Downloading..."
res.read_body do |buf|
ofh.print( buf )
end
downloaded = true
puts "done."
elsif res.is_a?( Net::HTTPRedirection )
url = URI.parse( res['location'] )
log "...following redirection to: %s" % [ url ]
limit -= 1
sleep 0.2
next
else
res.error!
end
end
end
end
end
return targetpath
ensure
$defout.sync = oldsync
end
### Return the fully-qualified path to the specified +program+ in the PATH.
def which( program )
ENV['PATH'].split(/:/).
collect {|dir| Pathname.new(dir) + program }.
find {|path| path.exist? && path.executable? }
end
### Create a string that contains the ANSI codes specified and return it
def ansi_code( *attributes )
attributes.flatten!
attributes.collect! {|at| at.to_s }
# $deferr.puts "Returning ansicode for TERM = %p: %p" %
# [ ENV['TERM'], attributes ]
return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM']
attributes = ANSI_ATTRIBUTES.values_at( *attributes ).compact.join(';')
# $deferr.puts " attr is: %p" % [attributes]
if attributes.empty?
return ''
else
return "\e[%sm" % attributes
end
end
### Colorize the given +string+ with the specified +attributes+ and return it, handling
### line-endings, color reset, etc.
def colorize( *args )
string = ''
if block_given?
string = yield
else
string = args.shift
end
ending = string[/(\s)$/] || ''
string = string.rstrip
return ansi_code( args.flatten ) + string + ansi_code( 'reset' ) + ending
end
### Output the specified <tt>msg</tt> as an ANSI-colored error message
### (white on red).
def error_message( msg, details='' )
$deferr.puts colorize( 'bold', 'white', 'on_red' ) { msg } + details
end
alias :error :error_message
### Highlight and embed a prompt control character in the given +string+ and return it.
def make_prompt_string( string )
return CLEAR_CURRENT_LINE + colorize( 'bold', 'green' ) { string + ' ' }
end
### Output the specified <tt>prompt_string</tt> as a prompt (in green) and
### return the user's input with leading and trailing spaces removed. If a
### test is provided, the prompt will repeat until the test returns true.
### An optional failure message can also be passed in.
def prompt( prompt_string, failure_msg="Try again." ) # :yields: response
prompt_string.chomp!
prompt_string << ":" unless /\W$/.match( prompt_string )
response = nil
begin
prompt = make_prompt_string( prompt_string )
response = Readline.readline( prompt ) || ''
response.strip!
if block_given? && ! yield( response )
error_message( failure_msg + "\n\n" )
response = nil
end
end while response.nil?
return response
end
### Prompt the user with the given <tt>prompt_string</tt> via #prompt,
### substituting the given <tt>default</tt> if the user doesn't input
### anything. If a test is provided, the prompt will repeat until the test
### returns true. An optional failure message can also be passed in.
def prompt_with_default( prompt_string, default, failure_msg="Try again." )
response = nil
begin
response = prompt( "%s [%s]" % [ prompt_string, default ] )
response = default if response.empty?
if block_given? && ! yield( response )
error_message( failure_msg + "\n\n" )
response = nil
end
end while response.nil?
return response
end
### Display a description of a potentially-dangerous task, and prompt
### for confirmation. If the user answers with anything that begins
### with 'y', yield to the block, else raise with an error.
def ask_for_confirmation( description )
puts description
answer = prompt_with_default( "Continue?", 'n' ) do |input|
input =~ /^[yn]/i
end
case answer
when /^y/i
yield
else
error "Aborted."
fail
end
end
### Search line-by-line in the specified +file+ for the given +regexp+, returning the
### first match, or nil if no match was found. If the +regexp+ has any capture groups,
### those will be returned in an Array, else the whole matching line is returned.
def find_pattern_in_file( regexp, file )
rval = nil
File.open( file, 'r' ).each do |line|
if (( match = regexp.match(line) ))
rval = match.captures.empty? ? match[0] : match.captures
break
end
end
return rval
end
### Search line-by-line in the output of the specified +cmd+ for the given +regexp+,
### returning the first match, or nil if no match was found. If the +regexp+ has any
### capture groups, those will be returned in an Array, else the whole matching line
### is returned.
def find_pattern_in_pipe( regexp, *cmd )
output = []
Open3.popen3( *cmd ) do |stdin, stdout, stderr|
stdin.close
output << stdout.gets until stdout.eof?
output << stderr.gets until stderr.eof?
end
result = output.find { |line| regexp.match(line) }
return $1 || result
end
### Extract all the non Rake-target arguments from ARGV and return them.
def get_target_args
args = ARGV.reject {|arg| Rake::Task.task_defined?(arg) }
return args
end
require 'rubygems/dependency_installer'
require 'rubygems/source_index'
require 'rubygems/requirement'
require 'rubygems/doc_manager'
### Install the specified +gems+ if they aren't already installed.
def install_gems( *gems )
gems.flatten!
defaults = Gem::DependencyInstaller::DEFAULT_OPTIONS.merge({
:generate_rdoc => true,
:generate_ri => true,
:install_dir => Gem.dir,
:format_executable => false,
:test => false,
:version => Gem::Requirement.default,
})
# Check for root
if Process.euid != 0
$stderr.puts "This probably won't work, as you aren't root, but I'll try anyway"
end
gemindex = Gem::SourceIndex.from_installed_gems
gems.each do |gemname|
if (( specs = gemindex.search(gemname) )) && ! specs.empty?
log "Version %s of %s is already installed; skipping..." %
[ specs.first.version, specs.first.name ]
next
end
log "Trying to install #{gemname.inspect}..."
installer = Gem::DependencyInstaller.new
installer.install( gemname )
installer.installed_gems.each do |spec|
log "Installed: %s" % [ spec.full_name ]
end
end
end

32
rake/packaging.rb Normal file
View file

@ -0,0 +1,32 @@
#
# Packaging Rake Tasks
#
#
require 'rake/packagetask'
require 'rake/gempackagetask'
Rake::GemPackageTask.new( GEMSPEC ) do |task|
task.gem_spec = GEMSPEC
task.need_tar = false
task.need_tar_gz = true
task.need_tar_bz2 = true
task.need_zip = true
end
### Task: install
task :install_gem => [:package] do
$stderr.puts
installer = Gem::Installer.new( %{pkg/#{PKG_FILE_NAME}.gem} )
installer.install
end
### Task: uninstall
task :uninstall_gem => [:clean] do
uninstaller = Gem::Uninstaller.new( PKG_FILE_NAME )
uninstaller.uninstall
end

25
rake/rdoc.rb Normal file
View file

@ -0,0 +1,25 @@
#
# RDoc Rake tasks
# $Id$
#
require 'rake/rdoctask'
### Task: rdoc
Rake::RDocTask.new do |rdoc|
rdoc.rdoc_dir = 'docs/api'
rdoc.title = "%s -- %s" % [ GEMSPEC.name, GEMSPEC.summary ]
rdoc.options += [
'-w', '4',
'-SHN',
'-i', BASEDIR.to_s,
'-f', 'darkfish',
'-m', 'README',
'-W', 'http://opensource.laika.com/browser/ruby-ezmlm/trunk/'
]
rdoc.rdoc_files.include 'README'
rdoc.rdoc_files.include LIB_FILES.collect {|f| f.relative_path_from(BASEDIR).to_s }
end

59
rake/style.rb Normal file
View file

@ -0,0 +1,59 @@
#
# Style Fixup Rake Tasks
#
#
#
### Coding style checks and fixes
namespace :style do
BLANK_LINE = /^\s*$/
GOOD_INDENT = /^(\t\s*)?\S/
# A list of the files that have legitimate leading whitespace, etc.
PROBLEM_FILES = [ SPECDIR + 'config_spec.rb' ]
desc "Check source files for inconsistent indent and fix them"
task :fix_indent do
files = LIB_FILES + SPEC_FILES
badfiles = Hash.new {|h,k| h[k] = [] }
trace "Checking files for indentation"
files.each do |file|
if PROBLEM_FILES.include?( file )
trace " skipping problem file #{file}..."
next
end
trace " #{file}"
linecount = 0
file.each_line do |line|
linecount += 1
# Skip blank lines
next if line =~ BLANK_LINE
# If there's a line with incorrect indent, note it and skip to the
# next file
if line !~ GOOD_INDENT
trace " Bad line %d: %p" % [ linecount, line ]
badfiles[file] << [ linecount, line ]
end
end
end
if badfiles.empty?
log "No indentation problems found."
else
log "Found incorrect indent in #{badfiles.length} files:\n "
badfiles.each do |file, badlines|
log " #{file}:\n" +
" " + badlines.collect {|badline| "%5d: %p" % badline }.join( "\n " )
end
end
end
end

328
rake/svn.rb Normal file
View file

@ -0,0 +1,328 @@
#####################################################################
### S U B V E R S I O N T A S K S A N D H E L P E R S
#####################################################################
require 'pp'
require 'yaml'
require 'English'
# Strftime format for tags/releases
TAG_TIMESTAMP_FORMAT = '%Y%m%d-%H%M%S'
TAG_TIMESTAMP_PATTERN = /\d{4}\d{2}\d{2}-\d{6}/
RELEASE_VERSION_PATTERN = /\d+\.\d+\.\d+/
DEFAULT_EDITOR = 'vi'
DEFAULT_KEYWORDS = %w[Date Rev Author URL Id]
KEYWORDED_FILEDIRS = %w[applets bin etc lib misc]
KEYWORDED_FILEPATTERN = /^(?:Rakefile|.*\.(?:rb|js|html|template))$/i
COMMIT_MSG_FILE = 'commit-msg.txt'
###
### Subversion-specific Helpers
###
### Return a new tag for the given time
def make_new_tag( time=Time.now )
return time.strftime( TAG_TIMESTAMP_FORMAT )
end
### Get the subversion information for the current working directory as
### a hash.
def get_svn_info( dir='.' )
info = IO.read( '|-' ) or exec 'svn', 'info', dir
return YAML.load( info ) # 'svn info' outputs valid YAML! Yay!
end
### Get a list of the objects registered with subversion under the specified directory and
### return them as an Array of Pathame objects.
def get_svn_filelist( dir='.' )
list = IO.read( '|-' ) or exec 'svn', 'st', '-v', '--ignore-externals', dir
# Split into lines, filter out the unknowns, and grab the filenames as Pathnames
# :FIXME: This will break if we ever put in a file with spaces in its name. This
# will likely be the least of our worries if we do so, however, so it's not worth
# the additional complexity to make it handle that case. If we do need that, there's
# always the --xml output for 'svn st'...
return list.split( $/ ).
reject {|line| line =~ /^\?/ }.
collect {|fn| Pathname(fn[/\S+$/]) }
end
### Return the URL to the repository root for the specified +dir+.
def get_svn_repo_root( dir='.' )
info = get_svn_info( dir )
return info['URL'].sub( %r{/trunk$}, '' )
end
### Return the Subversion URL to the given +dir+.
def get_svn_url( dir='.' )
info = get_svn_info( dir )
return info['URL']
end
### Return the path of the specified +dir+ under the svn root of the
### checkout.
def get_svn_path( dir='.' )
root = get_svn_repo_root( dir )
url = get_svn_url( dir )
return url.sub( root + '/', '' )
end
### Return the keywords for the specified array of +files+ as a Hash keyed by filename.
def get_svn_keyword_map( files )
cmd = ['svn', 'pg', 'svn:keywords', *files]
# trace "Executing: svn pg svn:keywords " + files.join(' ')
output = IO.read( '|-' ) or exec( 'svn', 'pg', 'svn:keywords', *files )
kwmap = {}
output.split( "\n" ).each do |line|
next if line !~ /\s+-\s+/
path, keywords = line.split( /\s+-\s+/, 2 )
kwmap[ path ] = keywords.split
end
return kwmap
end
### Return the latest revision number of the specified +dir+ as an Integer.
def get_svn_rev( dir='.' )
info = get_svn_info( dir )
return info['Revision']
end
### Return a list of the entries at the specified Subversion url. If
### no +url+ is specified, it will default to the list in the URL
### corresponding to the current working directory.
def svn_ls( url=nil )
url ||= get_svn_url()
list = IO.read( '|-' ) or exec 'svn', 'ls', url
trace 'svn ls of %s: %p' % [url, list] if $trace
return [] if list.nil? || list.empty?
return list.split( $INPUT_RECORD_SEPARATOR )
end
### Return the URL of the latest timestamp in the tags directory.
def get_latest_svn_timestamp_tag
rooturl = get_svn_repo_root()
tagsurl = rooturl + '/tags'
tags = svn_ls( tagsurl ).grep( TAG_TIMESTAMP_PATTERN ).sort
return nil if tags.nil? || tags.empty?
return tagsurl + '/' + tags.last
end
### Get a subversion diff of the specified targets and return it. If no targets are
### specified, the current directory will be diffed instead.
def get_svn_diff( *targets )
targets << BASEDIR if targets.empty?
trace "Getting svn diff for targets: %p" % [targets]
log = IO.read( '|-' ) or exec 'svn', 'diff', *(targets.flatten)
return log
end
### Return the URL of the latest timestamp in the tags directory.
def get_latest_release_tag
rooturl = get_svn_repo_root()
releaseurl = rooturl + '/releases'
tags = svn_ls( releaseurl ).grep( RELEASE_VERSION_PATTERN ).sort_by do |tag|
tag.split('.').collect {|i| Integer(i) }
end
return nil if tags.empty?
return releaseurl + '/' + tags.last
end
### Extract a diff from the specified subversion working +dir+, rewrite its
### file lines as Trac links, and return it.
def make_svn_commit_log( dir='.' )
editor_prog = ENV['EDITOR'] || ENV['VISUAL'] || DEFAULT_EDITOR
diff = IO.read( '|-' ) or exec 'svn', 'diff'
fail "No differences." if diff.empty?
return diff
end
###
### Tasks
###
desc "Subversion tasks"
namespace :svn do
desc "Copy the HEAD revision of the current trunk/ to tags/ with a " +
"current timestamp."
task :tag do
svninfo = get_svn_info()
tag = make_new_tag()
svntrunk = svninfo['URL']
svntagdir = svninfo['URL'].sub( %r{trunk$}, 'tags' )
svntag = svntagdir + '/' + tag
desc = "Tagging trunk as #{svntag}"
ask_for_confirmation( desc ) do
msg = prompt_with_default( "Commit log: ", "Tagging for code push" )
run 'svn', 'cp', '-m', msg, svntrunk, svntag
end
end
desc "Copy the most recent tag to releases/#{PKG_VERSION}"
task :release do
last_tag = get_latest_svn_timestamp_tag()
svninfo = get_svn_info()
release = PKG_VERSION
svnrel = svninfo['URL'] + '/releases'
svnrelease = svnrel + '/' + release
if last_tag.nil?
error "There are no tags in the repository"
fail
end
releases = svn_ls( svnrel )
trace "Releases: %p" % [releases]
if releases.include?( release )
error "Version #{release} already has a branch (#{svnrelease}). Did you mean" +
"to increment the version in #{PKG_VERSION_FROM}?"
fail
else
trace "No #{svnrel} version currently exists"
end
desc = "Release tag\n #{last_tag}\nto\n #{svnrelease}"
ask_for_confirmation( desc ) do
msg = prompt_with_default( "Commit log: ", "Branching for release" )
run 'svn', 'cp', '-m', msg, last_tag, svnrelease
end
end
### Task for debugging the #get_target_args helper
task :show_targets do
$stdout.puts "Targets from ARGV (%p): %p" % [ARGV, get_target_args()]
end
desc "Generate a commit log"
task :commitlog => [COMMIT_MSG_FILE]
desc "Show the (pre-edited) commit log for the current directory"
task :show_commitlog => [COMMIT_MSG_FILE] do
ask_for_confirmation( "Confirm? " ) do
args = get_target_args()
puts get_svn_diff( *args )
end
end
file COMMIT_MSG_FILE do
args = get_target_args()
diff = get_svn_diff( *args )
File.open( COMMIT_MSG_FILE, File::WRONLY|File::EXCL|File::CREAT ) do |fh|
fh.print( diff )
end
editor = ENV['EDITOR'] || ENV['VISUAL'] || DEFAULT_EDITOR
system editor, COMMIT_MSG_FILE
unless $?.success?
fail "Editor exited uncleanly."
end
end
desc "Update from Subversion"
task :update do
run 'svn', 'up', '--ignore-externals'
end
desc "Check in all the changes in your current working copy"
task :checkin => ['svn:update', 'coverage:verify', 'svn:fix_keywords', COMMIT_MSG_FILE] do
targets = get_target_args()
$deferr.puts '---', File.read( COMMIT_MSG_FILE ), '---'
ask_for_confirmation( "Continue with checkin?" ) do
run 'svn', 'ci', '-F', COMMIT_MSG_FILE, targets
rm_f COMMIT_MSG_FILE
end
end
task :commit => :checkin
task :ci => :checkin
task :clean do
rm_f COMMIT_MSG_FILE
end
desc "Check and fix any missing keywords for any files in the project which need them"
task :fix_keywords do
log "Checking subversion keywords..."
paths = get_svn_filelist( BASEDIR ).
select {|path| path.file? && path.to_s =~ KEYWORDED_FILEPATTERN }
trace "Looking at %d paths for keywords:\n %p" % [paths.length, paths]
kwmap = get_svn_keyword_map( paths )
buf = ''
PP.pp( kwmap, buf, 132 )
trace "keyword map is: %s" % [buf]
files_needing_fixups = paths.find_all do |path|
(kwmap[path.to_s] & DEFAULT_KEYWORDS) != DEFAULT_KEYWORDS
end
unless files_needing_fixups.empty?
$deferr.puts "Files needing keyword fixes: ",
files_needing_fixups.collect {|f|
" %s: %s" % [f, kwmap[f] ? kwmap[f].join(' ') : "(no keywords)"]
}
ask_for_confirmation( "Will add default keywords to these files." ) do
run 'svn', 'ps', 'svn:keywords', DEFAULT_KEYWORDS.join(' '), *files_needing_fixups
end
else
log "Keywords are all up to date."
end
end
task :debug_helpers do
methods = [
:make_new_tag,
:get_svn_info,
:get_svn_repo_root,
:get_svn_url,
:get_svn_path,
:svn_ls,
:get_latest_svn_timestamp_tag,
]
maxlen = methods.collect {|sym| sym.to_s.length }.max
methods.each do |meth|
res = send( meth )
puts "%*s => %p" % [ maxlen, colorize(meth.to_s, :cyan), res ]
end
end
end

127
rake/testing.rb Normal file
View file

@ -0,0 +1,127 @@
#
# Testing Rake Tasks
# $Id$
#
#
# Keep these tasks optional by handling LoadErrors with stub task
# replacements.
begin
gem 'rspec', '>= 1.1.1'
require 'spec/rake/spectask'
COMMON_SPEC_OPTS = ['-c', '-f', 's']
### Task: spec
Spec::Rake::SpecTask.new( :spec ) do |task|
task.spec_files = SPEC_FILES
task.libs += [LIBDIR]
task.spec_opts = COMMON_SPEC_OPTS
end
task :test => [:spec]
namespace :spec do
desc "Generate HTML output for a spec run"
Spec::Rake::SpecTask.new( :html ) do |task|
task.spec_files = SPEC_FILES
task.spec_opts = ['-f','h', '-D']
end
desc "Generate plain-text output for a CruiseControl.rb build"
Spec::Rake::SpecTask.new( :text ) do |task|
task.spec_files = SPEC_FILES
task.spec_opts = ['-f','p']
end
end
rescue LoadError => err
task :no_rspec do
$stderr.puts "Testing tasks not defined: RSpec rake tasklib not available: %s" %
[ err.message ]
end
task :spec => :no_rspec
namespace :spec do
task :autotest => :no_rspec
task :html => :no_rspec
task :text => :no_rspec
end
end
### RCov (via RSpec) tasks
begin
gem 'rcov'
gem 'rspec', '>= 1.1.1'
COVERAGE_TARGETDIR = STATICWWWDIR + 'coverage'
RCOV_OPTS = ['--exclude', SPEC_EXCLUDES, '--xrefs', '--save']
### Task: coverage (via RCov)
### Task: spec
desc "Build test coverage reports"
Spec::Rake::SpecTask.new( :coverage ) do |task|
task.spec_files = SPEC_FILES
task.libs += [LIBDIR]
task.spec_opts = ['-f', 'p', '-b']
task.rcov_opts = RCOV_OPTS
task.rcov = true
end
task :rcov => [:coverage] do; end
### Other coverage tasks
namespace :coverage do
desc "Generate a detailed text coverage report"
Spec::Rake::SpecTask.new( :text ) do |task|
task.spec_files = SPEC_FILES
task.rcov_opts = RCOV_OPTS + ['--text-report']
task.rcov = true
end
desc "Show differences in coverage from last run"
Spec::Rake::SpecTask.new( :diff ) do |task|
task.spec_files = SPEC_FILES
task.rcov_opts = ['--text-coverage-diff']
task.rcov = true
end
### Task: verify coverage
desc "Build coverage statistics"
VerifyTask.new( :verify => :rcov ) do |task|
task.threshold = 85.0
end
desc "Run RCov in 'spec-only' mode to check coverage from specs"
Spec::Rake::SpecTask.new( :speconly ) do |task|
task.spec_files = SPEC_FILES
task.rcov_opts = ['--exclude', SPEC_EXCLUDES, '--text-report', '--save']
task.rcov = true
end
end
task :clobber_coverage do
rmtree( COVERAGE_TARGETDIR )
end
rescue LoadError => err
task :no_rcov do
$stderr.puts "Coverage tasks not defined: RSpec+RCov tasklib not available: %s" %
[ err.message ]
end
task :coverage => :no_rcov
task :clobber_coverage
task :rcov => :no_rcov
namespace :coverage do
task :text => :no_rcov
task :diff => :no_rcov
end
task :verify => :no_rcov
end

64
rake/verifytask.rb Normal file
View file

@ -0,0 +1,64 @@
#####################################################################
### S U B V E R S I O N T A S K S A N D H E L P E R S
#####################################################################
require 'rake/tasklib'
#
# Work around the inexplicable behaviour of the original RDoc::VerifyTask, which
# errors if your coverage isn't *exactly* the threshold.
#
# A task that can verify that the RCov coverage doesn't
# drop below a certain threshold. It should be run after
# running Spec::Rake::SpecTask.
class VerifyTask < Rake::TaskLib
COVERAGE_PERCENTAGE_PATTERN =
%r{<tt class='coverage_code'>(\d+\.\d+)%</tt>}
# Name of the task. Defaults to :verify_rcov
attr_accessor :name
# Path to the index.html file generated by RCov, which
# is the file containing the total coverage.
# Defaults to 'coverage/index.html'
attr_accessor :index_html
# Whether or not to output details. Defaults to true.
attr_accessor :verbose
# The threshold value (in percent) for coverage. If the
# actual coverage is not equal to this value, the task will raise an
# exception.
attr_accessor :threshold
def initialize( name=:verify )
@name = name
@index_html = 'coverage/index.html'
@verbose = true
yield self if block_given?
raise "Threshold must be set" if @threshold.nil?
define
end
def define
desc "Verify that rcov coverage is at least #{threshold}%"
task @name do
total_coverage = nil
if match = File.read( index_html ).match( COVERAGE_PERCENTAGE_PATTERN )
total_coverage = Float( match[1] )
else
raise "Couldn't find the coverage percentage in #{index_html}"
end
puts "Coverage: #{total_coverage}% (threshold: #{threshold}%)" if verbose
if total_coverage < threshold
raise "Coverage must be at least #{threshold}% but was #{total_coverage}%"
end
end
end
end
# vim: set nosta noet ts=4 sw=4:

33
spec/ezmlm/list_spec.rb Normal file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env ruby
BEGIN {
require 'pathname'
basedir = Pathname.new( __FILE__ ).dirname.parent.parent
libdir = basedir + "lib"
$LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
}
begin
require 'spec/runner'
require 'spec/lib/helpers'
require 'ezmlm/list'
rescue LoadError
unless Object.const_defined?( :Gem )
require 'rubygems'
retry
end
raise
end
describe Ezmlm::List do
include Ezmlm::SpecHelpers
it "is well-tested"
end

59
spec/ezmlm_spec.rb Normal file
View file

@ -0,0 +1,59 @@
#!/usr/bin/env ruby
BEGIN {
require 'pathname'
basedir = Pathname.new( __FILE__ ).dirname.parent
libdir = basedir + "lib"
$LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
}
begin
require 'spec/runner'
require 'spec/lib/helpers'
require 'ezmlm'
rescue LoadError
unless Object.const_defined?( :Gem )
require 'rubygems'
retry
end
raise
end
describe Ezmlm do
include Ezmlm::SpecHelpers
LISTSDIR = '/tmp/lists'
it "can iterate over all mailing lists in a specified directory" do
file_entry = mock( "plain file" )
file_entry.should_receive( :directory? ).and_return( false )
nonexistant_mlentry = stub( "mailinglist path that doesn't exist", :exist? => false )
nonml_dir_entry = stub( "directory with no mailinglist file",
:directory? => true, :+ => nonexistant_mlentry )
existant_mlentry = stub( "mailinglist path that does exist", :exist? => true )
ml_dir_entry = stub( "directory with a mailinglist file", :directory? => true, :+ => existant_mlentry )
Pathname.should_receive( :glob ).with( an_instance_of(Pathname) ).
and_yield( file_entry ).
and_yield( nonml_dir_entry ).
and_yield( ml_dir_entry )
Ezmlm::List.should_receive( :new ).with( ml_dir_entry ).and_return( :listobject )
lists = []
Ezmlm.each_list( LISTSDIR ) do |list|
lists << list
end
lists.should have(1).member
lists.should include( :listobject )
end
end

157
spec/lib/helpers.rb Normal file
View file

@ -0,0 +1,157 @@
#!/usr/bin/ruby
begin
require 'ezmlm'
rescue LoadError
unless Object.const_defined?( :Gem )
require 'rubygems'
retry
end
raise
end
module Ezmlm::SpecHelpers
###############
module_function
###############
### Create a temporary working directory and return
### a Pathname object for it.
###
def make_tempdir
dirname = "%s.%d.%0.4f" % [
'ezmlm_spec',
Process.pid,
(Time.now.to_f % 3600),
]
tempdir = Pathname.new( Dir.tmpdir ) + dirname
tempdir.mkpath
return tempdir
end
end
# Override the badly-formatted output of the RSpec HTML formatter
require 'spec/runner/formatter/html_formatter'
class Spec::Runner::Formatter::HtmlFormatter
def example_failed( example, counter, failure )
failure_style = failure.pending_fixed? ? 'pending_fixed' : 'failed'
unless @header_red
@output.puts " <script type=\"text/javascript\">makeRed('rspec-header');</script>"
@header_red = true
end
unless @example_group_red
css_class = 'example_group_%d' % [current_example_group_number]
@output.puts " <script type=\"text/javascript\">makeRed('#{css_class}');</script>"
@example_group_red = true
end
move_progress()
@output.puts " <dd class=\"spec #{failure_style}\">",
" <span class=\"failed_spec_name\">#{h(example.description)}</span>",
" <div class=\"failure\" id=\"failure_#{counter}\">"
if failure.exception
backtrace = format_backtrace( failure.exception.backtrace )
message = failure.exception.message
@output.puts " <div class=\"message\"><code>#{h message}</code></div>",
" <div class=\"backtrace\"><pre>#{backtrace}</pre></div>"
end
if extra = extra_failure_content( failure )
@output.puts( extra )
end
@output.puts " </div>",
" </dd>"
@output.flush
end
alias_method :default_global_styles, :global_styles
def global_styles
css = default_global_styles()
css << %Q{
/* Stuff added by #{__FILE__} */
/* Overrides */
#rspec-header {
-webkit-box-shadow: #333 0 2px 5px;
margin-bottom: 1em;
}
.example_group dt {
-webkit-box-shadow: #333 0 2px 3px;
}
/* Style for log output */
dd.log-message {
background: #eee;
padding: 0 2em;
margin: 0.2em 1em;
border-bottom: 1px dotted #999;
border-top: 1px dotted #999;
text-indent: -1em;
}
/* Parts of the message */
dd.log-message .log-time {
font-weight: bold;
}
dd.log-message .log-time:after {
content: ": ";
}
dd.log-message .log-level {
font-variant: small-caps;
border: 1px solid #ccc;
padding: 1px 2px;
}
dd.log-message .log-name {
font-size: 1.2em;
color: #1e51b2;
}
dd.log-message .log-name:before { content: "«"; }
dd.log-message .log-name:after { content: "»"; }
dd.log-message .log-message-text {
padding-left: 4px;
font-family: Monaco, "Andale Mono", "Vera Sans Mono", mono;
}
/* Distinguish levels */
dd.log-message.debug { color: #666; }
dd.log-message.info {}
dd.log-message.warn,
dd.log-message.error {
background: #ff9;
}
dd.log-message.error .log-level,
dd.log-message.error .log-message-text {
color: #900;
}
dd.log-message.fatal {
background: #900;
color: white;
font-weight: bold;
border: 0;
}
dd.log-message.fatal .log-name {
color: white;
}
}
return css
end
end