First round of modernizing after a long absence.

Much work to be done.
This commit is contained in:
Mahlon E. Smith 2017-02-01 15:35:35 -08:00
parent 0c8f66ac29
commit 3f96c8bb0f
21 changed files with 556 additions and 2054 deletions

4
.gems Normal file
View file

@ -0,0 +1,4 @@
loggability
mail
rspec

14
.pryrc Normal file
View file

@ -0,0 +1,14 @@
#!/usr/bin/ruby
$LOAD_PATH.unshift( 'lib' )
begin
require 'ezmlm'
rescue Exception => e
$stderr.puts "Ack! Ezmlm libraries failed to load: #{e.message}\n\t" +
e.backtrace.join( "\n\t" )
end

1
.rspec Normal file
View file

@ -0,0 +1 @@
-c -f d

1
.ruby-gemset Normal file
View file

@ -0,0 +1 @@
ezmlm

1
.ruby-version Normal file
View file

@ -0,0 +1 @@
2.4

View file

@ -1,4 +1,4 @@
Copyright (c) 2008, LAIKA Information Systems
Copyright (c) 2017, Mahlon E. Smith <mahlon@martini.nu>
All rights reserved.
Redistribution and use in source and binary forms, with or without

45
README
View file

@ -1,45 +0,0 @@
= 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

61
README.md Normal file
View file

@ -0,0 +1,61 @@
# Ruby-Ezmlm
code
: https://bitbucket.org/mahlon/Ruby-Ezmlm
## Authors
* Michael Granger <ged@faeriemud.org>
* Jeremiah Jordan <jjordan@laika.com>
* Mahlon E. Smith <mahlon@martini.nu>
## Description
This is a ruby interface for interacting with ezmlm-idx, an email list
manager for use with the Qmail MTA. (The -idx provides an extended
feature set over the initial ezmlm environment.)
http://untroubled.org/ezmlm/
## Prerequisites
* Ruby 2.2 or better
## Installation
$ gem install ezmlm
## License
Copyright (c) 2017, Mahlon E. Smith <mahlon@martini.nu>
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 the author/s, nor the names of the project's
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.

360
Rakefile
View file

@ -1,290 +1,118 @@
#!rake
#
# Ruby-Ezmlm rakefile
#
# Based on various other Rakefiles, especially one by Ben Bleything
#
# Copyright (c) 2008 The FaerieMUD Consortium
#
# Authors:
# * LAIKA Information Systems <opensource@laika.com>
#
#!/usr/bin/env rake
# vim: set nosta noet ts=4 sw=4:
BEGIN {
require 'pathname'
basedir = Pathname.new( __FILE__ ).dirname
require 'pathname'
libdir = basedir + "lib"
extdir = basedir + "ext"
$LOAD_PATH.unshift( libdir.to_s ) unless $LOAD_PATH.include?( libdir.to_s )
$LOAD_PATH.unshift( extdir.to_s ) unless $LOAD_PATH.include?( extdir.to_s )
}
require 'rbconfig'
require 'rubygems'
require 'rake'
require 'rake/rdoctask'
require 'rake/testtask'
require 'rake/packagetask'
require 'rake/clean'
$dryrun = false
### Config constants
BASEDIR = Pathname.new( __FILE__ ).dirname.relative_path_from( Pathname.getwd )
BINDIR = BASEDIR + 'bin'
PROJECT = 'ezmlm'
BASEDIR = Pathname.new( __FILE__ ).expand_path.dirname.relative_path_from( Pathname.getwd )
LIBDIR = BASEDIR + 'lib'
EXTDIR = BASEDIR + 'ext'
DOCSDIR = BASEDIR + 'docs'
PKGDIR = BASEDIR + 'pkg'
PROJECT_NAME = 'Ruby-Ezmlm'
PKG_NAME = PROJECT_NAME.downcase
PKG_SUMMARY = 'A programmatic interface to ezmlm-idx lists'
VERSION_FILE = LIBDIR + 'ezmlm.rb'
PKG_VERSION = VERSION_FILE.read[ /VERSION = '(\d+\.\d+\.\d+)'/, 1 ]
PKG_FILE_NAME = "#{PKG_NAME.downcase}-#{PKG_VERSION}"
GEM_FILE_NAME = "#{PKG_FILE_NAME}.gem"
if Rake.application.options.trace
$trace = true
$stderr.puts '$trace is enabled'
end
ARTIFACTS_DIR = Pathname.new( ENV['CC_BUILD_ARTIFACTS'] || 'artifacts' )
# parse the current library version
$version = ( LIBDIR + "#{PROJECT}.rb" ).read.split(/\n/).
select{|line| line =~ /VERSION =/}.first.match(/([\d|.]+)/)[1]
TEXT_FILES = %w( Rakefile ChangeLog README LICENSE ).collect {|filename| BASEDIR + filename }
BIN_FILES = Pathname.glob( BINDIR + '*' ).delete_if {|item| item =~ /\.svn/ }
LIB_FILES = Pathname.glob( LIBDIR + '**/*.rb' ).delete_if {|item| item =~ /\.svn/ }
EXT_FILES = Pathname.glob( EXTDIR + '**/*.{c,h,rb}' ).delete_if {|item| item =~ /\.svn/ }
task :default => [ :spec, :docs, :package ]
SPECDIR = BASEDIR + 'spec'
SPECLIBDIR = SPECDIR + 'lib'
SPEC_FILES = Pathname.glob( SPECDIR + '**/*_spec.rb' ).delete_if {|item| item =~ /\.svn/ } +
Pathname.glob( SPECLIBDIR + '**/*.rb' ).delete_if {|item| item =~ /\.svn/ }
TESTDIR = BASEDIR + 'tests'
TEST_FILES = Pathname.glob( TESTDIR + '**/*.tests.rb' ).delete_if {|item| item =~ /\.svn/ }
########################################################################
### P A C K A G I N G
########################################################################
RAKE_TASKDIR = BASEDIR + 'rake'
RAKE_TASKLIBS = Pathname.glob( RAKE_TASKDIR + '*.rb' )
LOCAL_RAKEFILE = BASEDIR + 'Rakefile.local'
EXTRA_PKGFILES = []
RELEASE_FILES = TEXT_FILES +
SPEC_FILES +
TEST_FILES +
BIN_FILES +
LIB_FILES +
EXT_FILES +
RAKE_TASKLIBS +
EXTRA_PKGFILES
RELEASE_FILES << LOCAL_RAKEFILE if LOCAL_RAKEFILE.exist?
COVERAGE_MINIMUM = ENV['COVERAGE_MINIMUM'] ? Float( ENV['COVERAGE_MINIMUM'] ) : 85.0
RCOV_EXCLUDES = 'spec,tests,/Library/Ruby,/var/lib,/usr/local/lib'
RCOV_OPTS = [
'--exclude', RCOV_EXCLUDES,
'--xrefs',
'--save',
'--callsites',
#'--aggregate', 'coverage.data' # <- doesn't work as of 0.8.1.2.0
require 'rubygems'
require 'rubygems/package_task'
spec = Gem::Specification.new do |s|
s.email = 'mahlon@martini.nu'
s.homepage = 'https://bitbucket.org/mahlon/Ruby-Ezmlm'
s.authors = [
'Mahlon E. Smith <mahlon@martini.nu>',
'Michael Granger <ged@faeriemud.org>',
'Jeremiah Jordan <jjordan@laika.com>'
]
s.platform = Gem::Platform::RUBY
s.summary = "Interact with Ezmlm-IDX mailing lists."
s.name = PROJECT
s.version = $version
s.license = 'BSD-3-Clause'
s.has_rdoc = true
s.require_path = 'lib'
s.bindir = 'bin'
s.files = File.read( __FILE__ ).split( /^__END__/, 2 ).last.split
# s.executables = %w[]
s.description = <<-EOF
This is a ruby interface for interacting with ezmlm-idx, an email list
manager for use with the Qmail MTA. (The -idx provides an extended
feature set over the initial ezmlm environment.)
EOF
s.required_ruby_version = '>= 2'
# Subversion constants -- directory names for releases and tags
SVN_TRUNK_DIR = 'trunk'
SVN_RELEASES_DIR = 'releases'
SVN_BRANCHES_DIR = 'branches'
SVN_TAGS_DIR = 'tags'
SVN_DOTDIR = BASEDIR + '.svn'
SVN_ENTRIES = SVN_DOTDIR + 'entries'
### Load some task libraries that need to be loaded early
require RAKE_TASKDIR + 'helpers.rb'
require RAKE_TASKDIR + 'svn.rb'
require RAKE_TASKDIR + 'verifytask.rb'
# Define some constants that depend on the 'svn' tasklib
PKG_BUILD = get_svn_rev( BASEDIR ) || 0
SNAPSHOT_PKG_NAME = "#{PKG_FILE_NAME}.#{PKG_BUILD}"
SNAPSHOT_GEM_NAME = "#{SNAPSHOT_PKG_NAME}.gem"
# Documentation constants
RDOCDIR = DOCSDIR + 'api'
RDOC_OPTIONS = [
'-w', '4',
'-SHN',
'-i', '.',
'-m', 'README',
'-W', 'http://opensource.laika.com/wiki/ruby-ezmlm/browser/trunk/'
]
# Release constants
SMTP_HOST = 'mail.faeriemud.org'
SMTP_PORT = 465 # SMTP + SSL
# Project constants
PROJECT_HOST = 'deveiate.org'
PROJECT_PUBDIR = "/usr/local/www/public/code"
PROJECT_DOCDIR = "#{PROJECT_PUBDIR}/#{PKG_NAME}"
PROJECT_SCPPUBURL = "#{PROJECT_HOST}:#{PROJECT_PUBDIR}"
PROJECT_SCPDOCURL = "#{PROJECT_HOST}:#{PROJECT_DOCDIR}"
# Rubyforge stuff
RUBYFORGE_GROUP = 'laika'
RUBYFORGE_PROJECT = 'ezmlm'
# Gem dependencies: gemname => version
DEPENDENCIES = {
'tmail' => '>=1.2.3.1',
}
# Developer Gem dependencies: gemname => version
DEVELOPMENT_DEPENDENCIES = {
'amatch' => '>= 0.2.3',
'rake' => '>= 0.8.1',
'rcodetools' => '>= 0.7.0.0',
'rcov' => '>= 0',
'RedCloth' => '>= 4.0.3',
'rspec' => '>= 0',
'rubyforge' => '>= 0',
'termios' => '>= 0',
'text-format' => '>= 1.0.0',
'tmail' => '>= 1.2.3.1',
'ultraviolet' => '>= 0.10.2',
'libxml-ruby' => '>= 0.8.3',
}
# Non-gem requirements: packagename => version
REQUIREMENTS = {
'ezmlm-idx' => '>=0',
}
# RubyGem specification
GEMSPEC = Gem::Specification.new do |gem|
gem.name = PKG_NAME.downcase
gem.version = PKG_VERSION
gem.summary = PKG_SUMMARY
gem.description = <<-EOD
Ruby-Ezmlm provides a programmatic interface to ezmlm-idx lists, their archives, and the command
line utilities that interact with them. The library is intended to provide two sets of
functionality: the management and setup of lists, and programmatic access to the message archive.
EOD
gem.authors = 'LAIKA Information Systems'
gem.email = 'opensource@laika.com'
gem.homepage = 'http://opensource.laika.com/wiki/ruby-ezmlm'
gem.rubyforge_project = RUBYFORGE_PROJECT
gem.has_rdoc = true
gem.rdoc_options = RDOC_OPTIONS
gem.bindir = BINDIR.relative_path_from(BASEDIR).to_s
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 }
DEPENDENCIES.each do |name, version|
version = '>= 0' if version.length.zero?
gem.add_runtime_dependency( name, version )
end
DEVELOPMENT_DEPENDENCIES.each do |name, version|
version = '>= 0' if version.length.zero?
gem.add_development_dependency( name, version )
end
REQUIREMENTS.each do |name, version|
gem.requirements << [ name, version ].compact.join(' ')
end
s.add_dependency 'loggability', "~> 0.13"
s.add_dependency 'mail', "~> 2.6"
end
# Manual-generation config
MANUALDIR = DOCSDIR + 'manual'
$trace = Rake.application.options.trace ? true : false
$dryrun = Rake.application.options.dryrun ? true : false
# Load any remaining task libraries
RAKE_TASKLIBS.each do |tasklib|
next if tasklib =~ %r{/(helpers|svn|verifytask)\.rb$}
begin
require tasklib
rescue ScriptError => err
fail "Task library '%s' failed to load: %s: %s" %
[ tasklib, err.class.name, err.message ]
trace "Backtrace: \n " + err.backtrace.join( "\n " )
rescue => err
log "Task library '%s' failed to load: %s: %s. Some tasks may not be available." %
[ tasklib, err.class.name, err.message ]
trace "Backtrace: \n " + err.backtrace.join( "\n " )
end
end
# Load any project-specific rules defined in 'Rakefile.local' if it exists
import LOCAL_RAKEFILE if LOCAL_RAKEFILE.exist?
#####################################################################
### T A S K S
#####################################################################
### Default task
task :default => [:clean, :local, :spec, :rdoc, :package]
### Task the local Rakefile can append to -- no-op by default
task :local
### Task: clean
CLEAN.include 'coverage'
CLOBBER.include 'artifacts', 'coverage.info', PKGDIR
# Target to hinge on ChangeLog updates
file SVN_ENTRIES
### Task: changelog
file 'ChangeLog' => SVN_ENTRIES.to_s do |task|
log "Updating #{task.name}"
changelog = make_svn_changelog()
File.open( task.name, 'w' ) do |fh|
fh.print( changelog )
end
Gem::PackageTask.new( spec ) do |pkg|
pkg.need_zip = true
pkg.need_tar = true
end
### Task: cruise (Cruisecontrol task)
desc "Cruisecontrol build"
task :cruise => [:clean, :spec, :package] do |task|
raise "Artifacts dir not set." if ARTIFACTS_DIR.to_s.empty?
artifact_dir = ARTIFACTS_DIR.cleanpath
artifact_dir.mkpath
########################################################################
### D O C U M E N T A T I O N
########################################################################
coverage = BASEDIR + 'coverage'
if coverage.exist? && coverage.directory?
$stderr.puts "Copying coverage stats..."
FileUtils.cp_r( 'coverage', artifact_dir )
begin
require 'rdoc/task'
desc 'Generate rdoc documentation'
RDoc::Task.new do |rdoc|
rdoc.name = :docs
rdoc.rdoc_dir = 'docs'
rdoc.main = "README.rdoc"
# rdoc.options = [ '-f', 'fivefish' ]
rdoc.rdoc_files = [ 'lib', *FileList['*.rdoc'] ]
end
$stderr.puts "Copying packages..."
FileUtils.cp_r( FileList['pkg/*'].to_a, artifact_dir )
RDoc::Task.new do |rdoc|
rdoc.name = :doc_coverage
rdoc.options = [ '-C1' ]
end
rescue LoadError
$stderr.puts "Omitting 'docs' tasks, rdoc doesn't seem to be installed."
end
desc "Update the build system to the latest version"
task :update_build do
log "Updating the build system"
sh 'svn', 'up', RAKE_TASKDIR
log "Updating the Rakefile"
sh 'rake', '-f', RAKE_TASKDIR + 'Metarakefile'
########################################################################
### T E S T I N G
########################################################################
begin
require 'rspec/core/rake_task'
task :test => :spec
desc "Run specs"
RSpec::Core::RakeTask.new do |t|
t.pattern = "spec/**/*_spec.rb"
end
desc "Build a coverage report"
task :coverage do
ENV[ 'COVERAGE' ] = "yep"
Rake::Task[ :spec ].invoke
end
rescue LoadError
$stderr.puts "Omitting testing tasks, rspec doesn't seem to be installed."
end
########################################################################
### M A N I F E S T
########################################################################
__END__

View file

@ -1,83 +0,0 @@
#!/usr/bin/env ruby
#
# A startup script for an instance of Ezmlm::ListDaemon.
# $Id$
#
# Authors:
# * Michael Granger <mgranger@laika.com>
# * Jeremiah Jordan <jjordan@laika.com>
#
BEGIN {
require 'pathname'
basedir = Pathname.new( __FILE__ ).dirname.parent
libdir = basedir + 'lib'
$LOAD_PATH.unshift( libdir.to_s ) unless $LOAD_PATH.include?( libdir.to_s )
}
begin
require 'rubygems'
require 'optparse'
require 'ezmlm/listdaemon'
rescue LoadError
unless Object.const_defined?( :Gem )
require 'rubygems'
retry
end
raise
end
progname = Pathname.new( $0 ).basename
opts = Ezmlm::ListDaemon.default_options
oparser = OptionParser.new do |oparser|
oparser.accept( Pathname ) {|path| Pathname.new(path) }
oparser.banner = "Usage: #{progname.basename} OPTIONS LISTSDIRECTORY"
oparser.separator 'Version ' + Ezmlm::VERSION
oparser.separator ''
oparser.separator 'Options:'
oparser.on( '--bind ADDRESS', '-b', String,
"Specify the address to bind to. Defaults to '#{opts.bind_addr}'" ) do |bindaddr|
opts.bind_addr = bindaddr
end
oparser.on( '--port PORTNUMBER', '-p', Integer,
"Specify the port to connect to. Defaults to '#{opts.bind_port}" ) do |portnumber|
opts.bind_port = portnumber
end
oparser.separator ''
oparser.separator 'Other Options:'
oparser.on( '--debug', '-d', FalseClass, "Turn on debugging" ) do
$DEBUG = true
$stderr.puts "Debugging enabled."
opts.debugmode = true
end
oparser.on_tail( '--help', '-h', FalseClass, "Display help for the given command." ) do
$stderr.puts( oparser )
exit!( 0 )
end
oparser.on_tail( '--version', '-V', "Print Ezmlm library version on STDOUT and quit." ) do
$stdout.puts Ezmlm::VERSION
exit!( 0 )
end
end
# Parse command-line flags and display the help if there isn't exactly one argument
remaining_args = oparser.parse( ARGV )
if remaining_args.nitems != 1
$stderr.puts( oparser )
exit( 64 ) # EX_USAGE
end
listsdir = remaining_args.shift
daemon = Ezmlm::ListDaemon.new( listsdir, opts )
daemon.start.join

View file

@ -1,6 +0,0 @@
require 'drb'
DRb.start_service
lists = DRbObject.new( nil, 'druby://localhost:32315' )
l = lists.get_list( 'announce' )
l.last_post

View file

@ -1,4 +1,5 @@
#!/usr/bin/ruby
# vim: set nosta noet ts=4 sw=4:
#
# A Ruby programmatic interface to the ezmlm-idx mailing list system
#
@ -6,13 +7,6 @@
#
# $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.
@ -24,19 +18,10 @@ require 'pathname'
### Toplevel namespace module
module Ezmlm
# SVN Revision
SVNRev = %q$Rev$
# SVN Id
SVNId = %q$Id$
# Package version
VERSION = '0.0.1'
VERSION = '0.1.0'
require 'ezmlm/list'
require 'ezmlm/listdaemon'
###############
module_function
@ -44,6 +29,7 @@ module Ezmlm
### Find all directories that look like an Ezmlm list directory under the specified +listsdir+
### and return Pathname objects for each.
###
def find_directories( listsdir )
listsdir = Pathname.new( listsdir )
return Pathname.glob( listsdir + '*' ).select do |entry|
@ -54,13 +40,12 @@ module Ezmlm
### 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 )
find_directories( listsdir ).each do |entry|
yield( Ezmlm::List.new(entry) )
end
end
end # module Ezmlm
# vim: set nosta noet ts=4 sw=4:

View file

@ -1,39 +1,29 @@
#!/usr/bin/ruby
# vim: set nosta noet ts=4 sw=4:
#
# A Ruby interface to a single Ezmlm-idx mailing list directory
# 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'
require 'tmail'
require 'mail'
### A Ruby interface to an ezmlm-idx mailing list directory
###
class Ezmlm::List
### Create a new Ezmlm::List object for the specified +listdir+, which should be
### an ezmlm-idx mailing list directory.
###
def initialize( listdir )
listdir = Pathname.new( listdir ) if !listdir.is_a?( Pathname )
listdir = Pathname.new( listdir ) unless listdir.is_a?( Pathname )
@listdir = listdir
# Cached lookups
@config = nil
end
@ -207,7 +197,5 @@ class Ezmlm::List
return rval.flatten
end
end # class Ezmlm::List
end
# vim: set nosta noet ts=4 sw=4:

View file

@ -1,153 +0,0 @@
#!/usr/bin/ruby
#
# A DRb interface to one or more ezmlm-idx mailing lists.
#
# == 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'
require 'ezmlm/list'
require 'drb'
require 'ostruct'
### A DRb interface to one or more ezmlm-idx mailing lists
class Ezmlm::ListDaemon
# The default port to listen on
DEFAULT_PORT = 32315
# The default address to bind to
DEFAULT_ADDRESS = '127.0.0.1'
### The interface that is presented to DRb
class Service
include Enumerable
### Create a new service endpoint for the specified +listsdir+, which is a directory
### which contains ezmlm-idx list directories.
def initialize( listsdir )
listsdir = Pathname.new( listsdir )
@listsdir = listsdir
end
######
public
######
# The directory which contains the list directories that should be served.
attr_reader :listsdir
### Create a new Ezmlm::List object for the list directory with the specified +name+.
def get_list( name )
name = validate_listdir_name( name )
return Ezmlm::List.new( self.listsdir + name )
end
### Iterate over each current list in the Service's listsdir, yielding an Ezmlm::List object
### for each one.
def each_list( &block ) # :yields: list_object
Ezmlm.each_list( self.listsdir, &block )
end
alias_method :each, :each_list
#######
private
#######
VALID_LISTNAME_PATTERN = /^[a-z0-9.-]+$/i
### Ensure that the given +name+ is a valid list name, raising an exception if not. Returns
### an untainted copy of +name+.
def validate_listdir_name( name )
unless match = VALID_LISTNAME_PATTERN.match( name )
raise ArgumentError, "invalid list name %p" % [ name ]
end
return match[0].untaint
end
end # class Service
### Return an OpenStruct that contains the default options
def self::default_options
opts = OpenStruct.new
opts.bind_addr = DEFAULT_ADDRESS
opts.bind_port = DEFAULT_PORT
opts.debugmode = false
opts.helpmode = false
opts.foreground = false
return opts
end
#################################################################
### I N S T A N C E M E T H O D S
#################################################################
### Create a new Ezmlm::ListDaemon that will serve objects for the list directories
### contained in +listsdir+. The +options+ argument, if given, is an object (such as the one
### returned from ::default_options) that contains values for the following methods:
###
### bind_addr::
### The address to bind to. Defaults to DEFAULT_ADDRESS.
### bind_port::
### The port to listen on. Defaults to DEFAULT_PORT.
### debugmode::
### Whether to run in debugging mode, which causes the daemon to run in the foreground
### and send any output to STDERR. Defaults to +false+.
### foreground::
### Don't go into the background.
def initialize( listsdir, options=nil )
@service = Service.new( listsdir )
@options = options || self.class.default_options
end
######
public
######
# The daemon's configuration options
attr_reader :options
# The Ezmlm::ListDaemon::Service object that serves as the DRb interface
attr_reader :service
### Daemonize unless configured otherwise, start the DRb service and return the listening
### Thread object
def start
uri = "druby://%s:%d" % [ self.options.bind_addr, self.options.bind_port ]
DRb.start_service( uri, @service )
return DRb.thread
end
end # class Ezmlm::ListDaemon
# vim: set nosta noet ts=4 sw=4:

View file

@ -1,21 +0,0 @@
---
rubyforge_project: ezmlm
project_requirements:
ezmlm-idx: ">=0"
project_description: |-
Ruby-Ezmlm provides a programmatic interface to ezmlm-idx lists, their archives, and the command
line utilities that interact with them. The library is intended to provide two sets of
functionality: the management and setup of lists, and programmatic access to the message archive.
rubyforge_group: laika
author_name: LAIKA Information Systems
project_homepage: http://opensource.laika.com/wiki/ruby-ezmlm
project_dependencies:
tmail: ">=1.2.3.1"
project_summary: A programmatic interface to ezmlm-idx lists
project_name: Ruby-Ezmlm
version_file: ezmlm.rb
additional_pkgfiles: []
dev_dependencies: {}
author_email: opensource@laika.com

View file

@ -3,30 +3,14 @@
BEGIN {
require 'pathname'
basedir = Pathname.new( __FILE__ ).dirname.parent.parent
libdir = basedir + "lib"
$LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
}
begin
require 'tmail'
require 'spec/runner'
require 'spec/lib/helpers'
require 'ezmlm/list'
rescue LoadError
unless Object.const_defined?( :Gem )
require 'rubygems'
retry
end
raise
end
require_relative '../spec_helpers'
require 'ezmlm'
describe Ezmlm::List do
include Ezmlm::SpecHelpers
# Testing constants
TEST_LISTDIR = Pathname.new( 'list' )
@ -82,310 +66,310 @@ describe Ezmlm::List do
it "can return the configured list name" do
@list.stub!( :config ).and_return({ 'L' => :the_list_name })
@list.name.should == :the_list_name
allow(@list).to receive( :config ).and_return({ 'L' => :the_list_name })
expect(@list.name).to eq(:the_list_name)
end
it "can return the configured list host" do
@list.stub!( :config ).and_return({ 'H' => :the_list_host })
@list.host.should == :the_list_host
allow(@list).to receive( :config ).and_return({ 'H' => :the_list_host })
expect(@list.host).to eq(:the_list_host)
end
it "can return the configured list address" do
@list.stub!( :config ).and_return({ 'L' => TEST_LIST_NAME, 'H' => TEST_LIST_HOST })
@list.address.should == "%s@%s" % [ TEST_LIST_NAME, TEST_LIST_HOST ]
allow(@list).to receive( :config ).and_return({ 'L' => TEST_LIST_NAME, 'H' => TEST_LIST_HOST })
expect(@list.address).to eq("%s@%s" % [ TEST_LIST_NAME, TEST_LIST_HOST ])
end
CONFIG_KEYS = %w[ F X D T L H C 0 3 4 5 6 7 8 9 ]
it "can fetch the list config as a Hash" do
config_path = mock( "Mock config path" )
@listpath.should_receive( :+ ).with( 'config' ).and_return( config_path )
config_path.should_receive( :exist? ).and_return( true )
config_path.should_receive( :read ).and_return( TEST_CONFIG )
config_path = double( "Mock config path" )
expect(@listpath).to receive( :+ ).with( 'config' ).and_return( config_path )
expect(config_path).to receive( :exist? ).and_return( true )
expect(config_path).to receive( :read ).and_return( TEST_CONFIG )
@list.config.should be_an_instance_of( Hash )
@list.config.should have( CONFIG_KEYS.length ).members
@list.config.keys.should include( *CONFIG_KEYS )
expect(@list.config).to be_an_instance_of( Hash )
expect(@list.config.size).to eq(CONFIG_KEYS.length)
expect(@list.config.keys).to include( *CONFIG_KEYS )
end
it "raises an error if the list config file doesn't exist" do
config_path = mock( "Mock config path" )
@listpath.should_receive( :+ ).with( 'config' ).and_return( config_path )
config_path.should_receive( :exist? ).and_return( false )
config_path = double( "Mock config path" )
expect(@listpath).to receive( :+ ).with( 'config' ).and_return( config_path )
expect(config_path).to receive( :exist? ).and_return( false )
lambda {
expect {
@list.config
}.should raise_error( RuntimeError, /does not exist/ )
}.to raise_error( RuntimeError, /does not exist/ )
end
it "can return a list of subscribers' email addresses" do
subscribers_dir = TEST_LISTDIR + 'subscribers'
expectation = Pathname.should_receive( :glob ).with( subscribers_dir + '*' )
expectation = expect(Pathname).to receive( :glob ).with( subscribers_dir + '*' )
TEST_SUBSCRIBERS.each do |email|
mock_subfile = mock( "Mock subscribers file for '#{email}'" )
mock_subfile.should_receive( :read ).and_return( "T#{email}\0" )
mock_subfile = double( "Mock subscribers file for '#{email}'" )
expect(mock_subfile).to receive( :read ).and_return( "T#{email}\0" )
expectation.and_yield( mock_subfile )
end
subscribers = @list.subscribers
subscribers.should have(TEST_SUBSCRIBERS.length).members
subscribers.should include( *TEST_SUBSCRIBERS )
expect(subscribers.size).to eq(TEST_SUBSCRIBERS.length)
expect(subscribers).to include( *TEST_SUBSCRIBERS )
end
### Subscriber moderation
it "knows that subscription moderation is enabled if the dir/modsub file exists" do
modsub_path_obj = mock( "Mock 'modsub' path object" )
@listpath.should_receive( :+ ).with( 'modsub' ).and_return( modsub_path_obj )
modsub_path_obj.should_receive( :exist? ).and_return( true )
modsub_path_obj = double( "Mock 'modsub' path object" )
expect(@listpath).to receive( :+ ).with( 'modsub' ).and_return( modsub_path_obj )
expect(modsub_path_obj).to receive( :exist? ).and_return( true )
@list.should be_closed()
expect(@list).to be_closed()
end
it "knows that subscription moderation is enabled if the dir/remote file exists" do
modsub_path_obj = mock( "Mock 'modsub' path object" )
@listpath.should_receive( :+ ).with( 'modsub' ).and_return( modsub_path_obj )
modsub_path_obj.should_receive( :exist? ).and_return( false )
modsub_path_obj = double( "Mock 'modsub' path object" )
expect(@listpath).to receive( :+ ).with( 'modsub' ).and_return( modsub_path_obj )
expect(modsub_path_obj).to receive( :exist? ).and_return( false )
remote_path_obj = mock( "Mock 'remote' path object" )
@listpath.should_receive( :+ ).with( 'remote' ).and_return( remote_path_obj )
remote_path_obj.should_receive( :exist? ).and_return( true )
remote_path_obj = double( "Mock 'remote' path object" )
expect(@listpath).to receive( :+ ).with( 'remote' ).and_return( remote_path_obj )
expect(remote_path_obj).to receive( :exist? ).and_return( true )
@list.should be_closed()
expect(@list).to be_closed()
end
it "knows that subscription moderation is disabled if neither the dir/modsub nor " +
"dir/remote files exist" do
modsub_path_obj = mock( "Mock 'modsub' path object" )
@listpath.should_receive( :+ ).with( 'modsub' ).and_return( modsub_path_obj )
modsub_path_obj.should_receive( :exist? ).and_return( false )
modsub_path_obj = double( "Mock 'modsub' path object" )
expect(@listpath).to receive( :+ ).with( 'modsub' ).and_return( modsub_path_obj )
expect(modsub_path_obj).to receive( :exist? ).and_return( false )
remote_path_obj = mock( "Mock 'remote' path object" )
@listpath.should_receive( :+ ).with( 'remote' ).and_return( remote_path_obj )
remote_path_obj.should_receive( :exist? ).and_return( false )
remote_path_obj = double( "Mock 'remote' path object" )
expect(@listpath).to receive( :+ ).with( 'remote' ).and_return( remote_path_obj )
expect(remote_path_obj).to receive( :exist? ).and_return( false )
@list.should_not be_closed()
expect(@list).not_to be_closed()
end
it "returns an empty array of subscription moderators for an open list" do
modsub_path_obj = mock( "Mock 'modsub' path object" )
@listpath.should_receive( :+ ).with( 'modsub' ).and_return( modsub_path_obj )
modsub_path_obj.should_receive( :exist? ).and_return( false )
modsub_path_obj = double( "Mock 'modsub' path object" )
expect(@listpath).to receive( :+ ).with( 'modsub' ).and_return( modsub_path_obj )
expect(modsub_path_obj).to receive( :exist? ).and_return( false )
remote_path_obj = mock( "Mock 'remote' path object" )
@listpath.should_receive( :+ ).with( 'remote' ).and_return( remote_path_obj )
remote_path_obj.should_receive( :exist? ).and_return( false )
remote_path_obj = double( "Mock 'remote' path object" )
expect(@listpath).to receive( :+ ).with( 'remote' ).and_return( remote_path_obj )
expect(remote_path_obj).to receive( :exist? ).and_return( false )
@list.subscription_moderators.should be_empty()
expect(@list.subscription_moderators).to be_empty()
end
it "can return a list of subscription moderators' email addresses" do
# Test the moderation config files for existence
modsub_path_obj = mock( "Mock 'modsub' path object" )
@listpath.should_receive( :+ ).with( 'modsub' ).twice.and_return( modsub_path_obj )
modsub_path_obj.should_receive( :exist? ).twice.and_return( true )
remote_path_obj = mock( "Mock 'remote' path object" )
@listpath.should_receive( :+ ).with( 'remote' ).and_return( remote_path_obj )
remote_path_obj.should_receive( :exist? ).once.and_return( true )
modsub_path_obj = double( "Mock 'modsub' path object" )
expect(@listpath).to receive( :+ ).with( 'modsub' ).twice.and_return( modsub_path_obj )
expect(modsub_path_obj).to receive( :exist? ).twice.and_return( true )
remote_path_obj = double( "Mock 'remote' path object" )
expect(@listpath).to receive( :+ ).with( 'remote' ).and_return( remote_path_obj )
expect(remote_path_obj).to receive( :exist? ).once.and_return( true )
# Try to read directory names from both config files
modsub_path_obj.should_receive( :read ).with( 1 ).and_return( nil )
remote_path_obj.should_receive( :read ).with( 1 ).and_return( nil )
expect(modsub_path_obj).to receive( :read ).with( 1 ).and_return( nil )
expect(remote_path_obj).to receive( :read ).with( 1 ).and_return( nil )
# Read subscribers from the default directory
subscribers_dir = mock( "Mock moderator subscribers directory" )
@listpath.should_receive( :+ ).with( 'mod/subscribers' ).and_return( subscribers_dir )
subscribers_dir.should_receive( :+ ).with( '*' ).and_return( :mod_sub_dir )
expectation = Pathname.should_receive( :glob ).with( :mod_sub_dir )
subscribers_dir = double( "Mock moderator subscribers directory" )
expect(@listpath).to receive( :+ ).with( 'mod/subscribers' ).and_return( subscribers_dir )
expect(subscribers_dir).to receive( :+ ).with( '*' ).and_return( :mod_sub_dir )
expectation = expect(Pathname).to receive( :glob ).with( :mod_sub_dir )
TEST_MODERATORS.each do |email|
mock_subfile = mock( "Mock subscribers file for '#{email}'" )
mock_subfile.should_receive( :read ).and_return( "T#{email}\0" )
mock_subfile = double( "Mock subscribers file for '#{email}'" )
expect(mock_subfile).to receive( :read ).and_return( "T#{email}\0" )
expectation.and_yield( mock_subfile )
end
mods = @list.subscription_moderators
mods.should have(TEST_MODERATORS.length).members
mods.should include( *TEST_MODERATORS )
expect(mods.size).to eq(TEST_MODERATORS.length)
expect(mods).to include( *TEST_MODERATORS )
end
it "can return a list of subscription moderators' email addresses when the moderators " +
"directory has been customized" do
# Test the moderation config files for existence
modsub_path_obj = mock( "Mock 'modsub' path object" )
@listpath.should_receive( :+ ).with( 'modsub' ).twice.and_return( modsub_path_obj )
modsub_path_obj.should_receive( :exist? ).twice.and_return( true )
@listpath.should_receive( :+ ).with( 'remote' )
modsub_path_obj = double( "Mock 'modsub' path object" )
expect(@listpath).to receive( :+ ).with( 'modsub' ).twice.and_return( modsub_path_obj )
expect(modsub_path_obj).to receive( :exist? ).twice.and_return( true )
expect(@listpath).to receive( :+ ).with( 'remote' )
# Try to read directory names from both config files
modsub_path_obj.should_receive( :read ).with( 1 ).and_return( '/' )
modsub_path_obj.should_receive( :read ).with().and_return( TEST_CUSTOM_MODERATORS_DIR )
expect(modsub_path_obj).to receive( :read ).with( 1 ).and_return( '/' )
expect(modsub_path_obj).to receive( :read ).with().and_return( TEST_CUSTOM_MODERATORS_DIR )
custom_mod_path = mock( "Mock path object for customized moderator dir" )
Pathname.should_receive( :new ).with( TEST_CUSTOM_MODERATORS_DIR ).and_return( custom_mod_path )
custom_mod_path = double( "Mock path object for customized moderator dir" )
expect(Pathname).to receive( :new ).with( TEST_CUSTOM_MODERATORS_DIR ).and_return( custom_mod_path )
# Read subscribers from the default file
custom_mod_path.should_receive( :+ ).with( '*' ).and_return( :mod_sub_dir )
expectation = Pathname.should_receive( :glob ).with( :mod_sub_dir )
expect(custom_mod_path).to receive( :+ ).with( '*' ).and_return( :mod_sub_dir )
expectation = expect(Pathname).to receive( :glob ).with( :mod_sub_dir )
TEST_MODERATORS.each do |email|
mock_subfile = mock( "Mock subscribers file for '#{email}'" )
mock_subfile.should_receive( :read ).and_return( "T#{email}\0" )
mock_subfile = double( "Mock subscribers file for '#{email}'" )
expect(mock_subfile).to receive( :read ).and_return( "T#{email}\0" )
expectation.and_yield( mock_subfile )
end
mods = @list.subscription_moderators
mods.should have(TEST_MODERATORS.length).members
mods.should include( *TEST_MODERATORS )
expect(mods.size).to eq(TEST_MODERATORS.length)
expect(mods).to include( *TEST_MODERATORS )
end
it "can get a list of modererators when remote subscription moderation is enabled" +
" and the modsub configuration is empty" do
# Test the moderation config files for existence
modsub_path_obj = mock( "Mock 'modsub' path object" )
@listpath.should_receive( :+ ).with( 'modsub' ).twice.and_return( modsub_path_obj )
modsub_path_obj.should_receive( :exist? ).twice.and_return( false )
remote_path_obj = mock( "Mock 'remote' path object" )
@listpath.should_receive( :+ ).with( 'remote' ).twice.and_return( remote_path_obj )
remote_path_obj.should_receive( :exist? ).twice.and_return( true )
modsub_path_obj = double( "Mock 'modsub' path object" )
expect(@listpath).to receive( :+ ).with( 'modsub' ).twice.and_return( modsub_path_obj )
expect(modsub_path_obj).to receive( :exist? ).twice.and_return( false )
remote_path_obj = double( "Mock 'remote' path object" )
expect(@listpath).to receive( :+ ).with( 'remote' ).twice.and_return( remote_path_obj )
expect(remote_path_obj).to receive( :exist? ).twice.and_return( true )
# Try to read directory names from both config files
remote_path_obj.should_receive( :read ).with( 1 ).and_return( '/' )
remote_path_obj.should_receive( :read ).with().and_return( TEST_CUSTOM_MODERATORS_DIR )
expect(remote_path_obj).to receive( :read ).with( 1 ).and_return( '/' )
expect(remote_path_obj).to receive( :read ).with().and_return( TEST_CUSTOM_MODERATORS_DIR )
custom_mod_path = mock( "Mock path object for customized moderator dir" )
Pathname.should_receive( :new ).with( TEST_CUSTOM_MODERATORS_DIR ).and_return( custom_mod_path )
custom_mod_path = double( "Mock path object for customized moderator dir" )
expect(Pathname).to receive( :new ).with( TEST_CUSTOM_MODERATORS_DIR ).and_return( custom_mod_path )
# Read subscribers from the default file
custom_mod_path.should_receive( :+ ).with( '*' ).and_return( :mod_sub_dir )
expectation = Pathname.should_receive( :glob ).with( :mod_sub_dir )
expect(custom_mod_path).to receive( :+ ).with( '*' ).and_return( :mod_sub_dir )
expectation = expect(Pathname).to receive( :glob ).with( :mod_sub_dir )
TEST_MODERATORS.each do |email|
mock_subfile = mock( "Mock subscribers file for '#{email}'" )
mock_subfile.should_receive( :read ).and_return( "T#{email}\0" )
mock_subfile = double( "Mock subscribers file for '#{email}'" )
expect(mock_subfile).to receive( :read ).and_return( "T#{email}\0" )
expectation.and_yield( mock_subfile )
end
mods = @list.subscription_moderators
mods.should have(TEST_MODERATORS.length).members
mods.should include( *TEST_MODERATORS )
expect(mods.size).to eq(TEST_MODERATORS.length)
expect(mods).to include( *TEST_MODERATORS )
end
### Message moderation
it "knows that subscription moderation is enabled if the dir/modpost file exists" do
modpost_path_obj = mock( "Mock 'modpost' path object" )
@listpath.should_receive( :+ ).with( 'modpost' ).and_return( modpost_path_obj )
modpost_path_obj.should_receive( :exist? ).and_return( true )
modpost_path_obj = double( "Mock 'modpost' path object" )
expect(@listpath).to receive( :+ ).with( 'modpost' ).and_return( modpost_path_obj )
expect(modpost_path_obj).to receive( :exist? ).and_return( true )
@list.should be_moderated()
expect(@list).to be_moderated()
end
it "knows that subscription moderation is disabled if the dir/modpost file doesn't exist" do
modpost_path_obj = mock( "Mock 'modpost' path object" )
@listpath.should_receive( :+ ).with( 'modpost' ).and_return( modpost_path_obj )
modpost_path_obj.should_receive( :exist? ).and_return( false )
modpost_path_obj = double( "Mock 'modpost' path object" )
expect(@listpath).to receive( :+ ).with( 'modpost' ).and_return( modpost_path_obj )
expect(modpost_path_obj).to receive( :exist? ).and_return( false )
@list.should_not be_moderated()
expect(@list).not_to be_moderated()
end
it "returns an empty array of message moderators for an open list" do
modpost_path_obj = mock( "Mock 'modpost' path object" )
@listpath.should_receive( :+ ).with( 'modpost' ).and_return( modpost_path_obj )
modpost_path_obj.should_receive( :exist? ).and_return( false )
modpost_path_obj = double( "Mock 'modpost' path object" )
expect(@listpath).to receive( :+ ).with( 'modpost' ).and_return( modpost_path_obj )
expect(modpost_path_obj).to receive( :exist? ).and_return( false )
@list.message_moderators.should be_empty()
expect(@list.message_moderators).to be_empty()
end
it "can return a list of message moderators' email addresses" do
# Test the moderation config file for existence
modpost_path_obj = mock( "Mock 'modpost' path object" )
@listpath.should_receive( :+ ).with( 'modpost' ).twice.and_return( modpost_path_obj )
modpost_path_obj.should_receive( :exist? ).twice.and_return( true )
modpost_path_obj = double( "Mock 'modpost' path object" )
expect(@listpath).to receive( :+ ).with( 'modpost' ).twice.and_return( modpost_path_obj )
expect(modpost_path_obj).to receive( :exist? ).twice.and_return( true )
# Try to read directory names from the config file
modpost_path_obj.should_receive( :read ).with( 1 ).and_return( nil )
expect(modpost_path_obj).to receive( :read ).with( 1 ).and_return( nil )
# Read subscribers from the default directory
subscribers_dir = mock( "Mock moderator subscribers directory" )
@listpath.should_receive( :+ ).with( 'mod/subscribers' ).and_return( subscribers_dir )
subscribers_dir.should_receive( :+ ).with( '*' ).and_return( :mod_sub_dir )
expectation = Pathname.should_receive( :glob ).with( :mod_sub_dir )
subscribers_dir = double( "Mock moderator subscribers directory" )
expect(@listpath).to receive( :+ ).with( 'mod/subscribers' ).and_return( subscribers_dir )
expect(subscribers_dir).to receive( :+ ).with( '*' ).and_return( :mod_sub_dir )
expectation = expect(Pathname).to receive( :glob ).with( :mod_sub_dir )
TEST_MODERATORS.each do |email|
mock_subfile = mock( "Mock subscribers file for '#{email}'" )
mock_subfile.should_receive( :read ).and_return( "T#{email}\0" )
mock_subfile = double( "Mock subscribers file for '#{email}'" )
expect(mock_subfile).to receive( :read ).and_return( "T#{email}\0" )
expectation.and_yield( mock_subfile )
end
mods = @list.message_moderators
mods.should have(TEST_MODERATORS.length).members
mods.should include( *TEST_MODERATORS )
expect(mods.size).to eq(TEST_MODERATORS.length)
expect(mods).to include( *TEST_MODERATORS )
end
it "can return a list of message moderators' email addresses when the moderators " +
"directory has been customized" do
# Test the moderation config files for existence
modpost_path_obj = mock( "Mock 'modpost' path object" )
@listpath.should_receive( :+ ).with( 'modpost' ).twice.and_return( modpost_path_obj )
modpost_path_obj.should_receive( :exist? ).twice.and_return( true )
modpost_path_obj = double( "Mock 'modpost' path object" )
expect(@listpath).to receive( :+ ).with( 'modpost' ).twice.and_return( modpost_path_obj )
expect(modpost_path_obj).to receive( :exist? ).twice.and_return( true )
# Try to read directory names from both config files
modpost_path_obj.should_receive( :read ).with( 1 ).and_return( '/' )
modpost_path_obj.should_receive( :read ).with().and_return( TEST_CUSTOM_MODERATORS_DIR )
expect(modpost_path_obj).to receive( :read ).with( 1 ).and_return( '/' )
expect(modpost_path_obj).to receive( :read ).with().and_return( TEST_CUSTOM_MODERATORS_DIR )
custom_mod_path = mock( "Mock path object for customized moderator dir" )
Pathname.should_receive( :new ).with( TEST_CUSTOM_MODERATORS_DIR ).and_return( custom_mod_path )
custom_mod_path = double( "Mock path object for customized moderator dir" )
expect(Pathname).to receive( :new ).with( TEST_CUSTOM_MODERATORS_DIR ).and_return( custom_mod_path )
# Read subscribers from the default file
custom_mod_path.should_receive( :+ ).with( '*' ).and_return( :mod_sub_dir )
expectation = Pathname.should_receive( :glob ).with( :mod_sub_dir )
expect(custom_mod_path).to receive( :+ ).with( '*' ).and_return( :mod_sub_dir )
expectation = expect(Pathname).to receive( :glob ).with( :mod_sub_dir )
TEST_MODERATORS.each do |email|
mock_subfile = mock( "Mock subscribers file for '#{email}'" )
mock_subfile.should_receive( :read ).and_return( "T#{email}\0" )
mock_subfile = double( "Mock subscribers file for '#{email}'" )
expect(mock_subfile).to receive( :read ).and_return( "T#{email}\0" )
expectation.and_yield( mock_subfile )
end
mods = @list.message_moderators
mods.should have(TEST_MODERATORS.length).members
mods.should include( *TEST_MODERATORS )
expect(mods.size).to eq(TEST_MODERATORS.length)
expect(mods).to include( *TEST_MODERATORS )
end
### List owner
it "returns nil when the list doesn't have an owner in its config" do
@list.stub!( :config ).and_return({ '5' => nil })
@list.owner.should == nil
allow(@list).to receive( :config ).and_return({ '5' => nil })
expect(@list.owner).to eq(nil)
end
it "can return the email address of the list owner" do
@list.stub!( :config ).and_return({ '5' => TEST_OWNER })
@list.owner.should == TEST_OWNER
allow(@list).to receive( :config ).and_return({ '5' => TEST_OWNER })
expect(@list.owner).to eq(TEST_OWNER)
end
end
@ -403,22 +387,22 @@ describe Ezmlm::List do
it "can return the count of archived posts" do
numpath_obj = mock( "num file path object" )
@listpath.should_receive( :+ ).with( 'num' ).and_return( numpath_obj )
numpath_obj = double( "num file path object" )
expect(@listpath).to receive( :+ ).with( 'num' ).and_return( numpath_obj )
numpath_obj.should_receive( :exist? ).and_return( true )
numpath_obj.should_receive( :read ).and_return( "1723:123123123" )
expect(numpath_obj).to receive( :exist? ).and_return( true )
expect(numpath_obj).to receive( :read ).and_return( "1723:123123123" )
@list.message_count.should == 1723
expect(@list.message_count).to eq(1723)
end
it "can return the count of archived posts to a list that hasn't been posted to" do
numpath_obj = mock( "num file path object" )
@listpath.should_receive( :+ ).with( 'num' ).and_return( numpath_obj )
numpath_obj = double( "num file path object" )
expect(@listpath).to receive( :+ ).with( 'num' ).and_return( numpath_obj )
numpath_obj.should_receive( :exist? ).and_return( false )
expect(numpath_obj).to receive( :exist? ).and_return( false )
@list.message_count.should == 0
expect(@list.message_count).to eq(0)
end
@ -439,117 +423,117 @@ describe Ezmlm::List do
it "can return a TMail::Mail object parsed from the last archived post" do
# need to find the last message
archive_path_obj = mock( "archive path" )
archive_path_obj = double( "archive path" )
@listpath.should_receive( :+ ).with( 'archive' ).and_return( archive_path_obj )
archive_path_obj.should_receive( :exist? ).and_return( true )
expect(@listpath).to receive( :+ ).with( 'archive' ).and_return( archive_path_obj )
expect(archive_path_obj).to receive( :exist? ).and_return( true )
# Find the last numbered directory under the archive dir
archive_path_obj.should_receive( :+ ).with( '[0-9]*' ).
expect(archive_path_obj).to receive( :+ ).with( '[0-9]*' ).
and_return( :archive_dir_globpath )
Pathname.should_receive( :glob ).with( :archive_dir_globpath ).
expect(Pathname).to receive( :glob ).with( :archive_dir_globpath ).
and_return( @archive_subdir_paths )
# Find the last numbered file under the last numbered directory we found
# above.
@archive_subdir_paths.last.should_receive( :+ ).with( '[0-9]*' ).
expect(@archive_subdir_paths.last).to receive( :+ ).with( '[0-9]*' ).
and_return( :archive_post_pathglob )
Pathname.should_receive( :glob ).with( :archive_post_pathglob ).
expect(Pathname).to receive( :glob ).with( :archive_post_pathglob ).
and_return( @archive_post_paths )
TMail::Mail.should_receive( :load ).with( @archive_post_paths.last.to_s ).
expect(TMail::Mail).to receive( :load ).with( @archive_post_paths.last.to_s ).
and_return( :mail_object )
@list.last_post.should == :mail_object
expect(@list.last_post).to eq(:mail_object)
end
it "returns nil for the last post if there is no archive directory for the list" do
archive_path_obj = mock( "archive path" )
archive_path_obj = double( "archive path" )
@listpath.should_receive( :+ ).with( 'archive' ).and_return( archive_path_obj )
archive_path_obj.should_receive( :exist? ).and_return( false )
@list.last_post.should == nil
expect(@listpath).to receive( :+ ).with( 'archive' ).and_return( archive_path_obj )
expect(archive_path_obj).to receive( :exist? ).and_return( false )
expect(@list.last_post).to eq(nil)
end
it "returns nil for the last post if there haven't been any posts to the list" do
archive_path_obj = mock( "archive path" )
mail_object = mock( "Mock TMail object" )
archive_path_obj = double( "archive path" )
mail_object = double( "Mock TMail object" )
@listpath.should_receive( :+ ).with( 'archive' ).and_return( archive_path_obj )
archive_path_obj.should_receive( :exist? ).and_return( true )
expect(@listpath).to receive( :+ ).with( 'archive' ).and_return( archive_path_obj )
expect(archive_path_obj).to receive( :exist? ).and_return( true )
# Find the last numbered directory under the archive dir
archive_path_obj.should_receive( :+ ).with( '[0-9]*' ).
expect(archive_path_obj).to receive( :+ ).with( '[0-9]*' ).
and_return( :archive_dir_globpath )
Pathname.should_receive( :glob ).with( :archive_dir_globpath ).and_return( [] )
expect(Pathname).to receive( :glob ).with( :archive_dir_globpath ).and_return( [] )
@list.last_post.should == nil
expect(@list.last_post).to eq(nil)
end
it "raises a RuntimeError if the last archive directory doesn't have any messages in it" do
archive_path_obj = mock( "archive path" )
mail_object = mock( "Mock TMail object" )
archive_path_obj = double( "archive path" )
mail_object = double( "Mock TMail object" )
@listpath.should_receive( :+ ).with( 'archive' ).and_return( archive_path_obj )
archive_path_obj.should_receive( :exist? ).and_return( true )
expect(@listpath).to receive( :+ ).with( 'archive' ).and_return( archive_path_obj )
expect(archive_path_obj).to receive( :exist? ).and_return( true )
# Find the last numbered directory under the archive dir
archive_path_obj.should_receive( :+ ).with( '[0-9]*' ).
expect(archive_path_obj).to receive( :+ ).with( '[0-9]*' ).
and_return( :archive_dir_globpath )
Pathname.should_receive( :glob ).with( :archive_dir_globpath ).
expect(Pathname).to receive( :glob ).with( :archive_dir_globpath ).
and_return( @archive_subdir_paths )
@archive_subdir_paths.last.should_receive( :+ ).with( '[0-9]*' ).
expect(@archive_subdir_paths.last).to receive( :+ ).with( '[0-9]*' ).
and_return( :archive_post_pathglob )
Pathname.should_receive( :glob ).with( :archive_post_pathglob ).
expect(Pathname).to receive( :glob ).with( :archive_post_pathglob ).
and_return( [] )
lambda {
expect {
@list.last_post
}.should raise_error( RuntimeError, /unexpectedly empty/i )
}.to raise_error( RuntimeError, /unexpectedly empty/i )
end
it "can fetch the date of the last archived post" do
mail_object = mock( "Mock TMail object" )
mail_object = double( "Mock TMail object" )
@list.should_receive( :last_post ).and_return( mail_object )
mail_object.should_receive( :date ).and_return( :the_message_date )
expect(@list).to receive( :last_post ).and_return( mail_object )
expect(mail_object).to receive( :date ).and_return( :the_message_date )
@list.last_message_date.should == :the_message_date
expect(@list.last_message_date).to eq(:the_message_date)
end
it "can fetch the date of the last archived post" do
mail_object = mock( "Mock TMail object" )
mail_object = double( "Mock TMail object" )
@list.should_receive( :last_post ).and_return( mail_object )
mail_object.should_receive( :date ).and_return( :the_message_date )
expect(@list).to receive( :last_post ).and_return( mail_object )
expect(mail_object).to receive( :date ).and_return( :the_message_date )
@list.last_message_date.should == :the_message_date
expect(@list.last_message_date).to eq(:the_message_date)
end
it "can fetch the author of the last archived post" do
mail_object = mock( "Mock TMail object" )
mail_object = double( "Mock TMail object" )
@list.should_receive( :last_post ).and_return( mail_object )
mail_object.should_receive( :from ).and_return( :the_message_author )
expect(@list).to receive( :last_post ).and_return( mail_object )
expect(mail_object).to receive( :from ).and_return( :the_message_author )
@list.last_message_author.should == :the_message_author
expect(@list.last_message_author).to eq(:the_message_author)
end
it "can fetch the subject of the last archived post" do
mail_object = mock( "Mock TMail object" )
mail_object = double( "Mock TMail object" )
@list.should_receive( :last_post ).and_return( mail_object )
mail_object.should_receive( :from ).and_return( :the_message_author )
expect(@list).to receive( :last_post ).and_return( mail_object )
expect(mail_object).to receive( :from ).and_return( :the_message_author )
@list.last_message_author.should == :the_message_author
expect(@list.last_message_author).to eq(:the_message_author)
end
end

View file

@ -1,129 +0,0 @@
#!/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 'ostruct'
require 'spec/runner'
require 'spec/lib/helpers'
require 'ezmlm/listdaemon'
rescue LoadError
unless Object.const_defined?( :Gem )
require 'rubygems'
retry
end
raise
end
describe Ezmlm::ListDaemon do
include Ezmlm::SpecHelpers
DEFAULT_ADDRESS = Ezmlm::ListDaemon::DEFAULT_ADDRESS
DEFAULT_PORT = Ezmlm::ListDaemon::DEFAULT_PORT
it "can return a struct that contains its default options" do
opts = Ezmlm::ListDaemon.default_options
opts.should be_an_instance_of( OpenStruct )
opts.bind_addr.should == DEFAULT_ADDRESS
opts.bind_port.should == DEFAULT_PORT
opts.debugmode.should == false
opts.helpmode.should == false
end
describe "created with defaults" do
DEFAULT_URL = "druby://%s:%d" % [ DEFAULT_ADDRESS, DEFAULT_PORT ]
before( :each ) do
@test_list_dir = Pathname.new( 'lists' )
@daemon = Ezmlm::ListDaemon.new( @test_list_dir )
end
it "can be started and will return a thread" do
mock_drb_thread = mock( "drb thread" )
DRb.should_receive( :start_service ).with( DEFAULT_URL, @daemon.service )
DRb.should_receive( :thread ).and_return( mock_drb_thread )
@daemon.start.should == mock_drb_thread
end
end
describe "created with an options struct" do
TEST_ADDRESS = '0.0.0.0'
TEST_PORT = 17771
TEST_URL = "druby://%s:%d" % [ TEST_ADDRESS, TEST_PORT ]
before( :each ) do
@test_list_dir = Pathname.new( 'lists' )
@opts = Ezmlm::ListDaemon.default_options
@opts.bind_addr = TEST_ADDRESS
@opts.bind_port = TEST_PORT
@daemon = Ezmlm::ListDaemon.new( @test_list_dir, @opts )
end
it "can be started and will return a thread" do
mock_drb_thread = mock( "drb thread" )
DRb.should_receive( :start_service ).with( TEST_URL, @daemon.service )
DRb.should_receive( :thread ).and_return( mock_drb_thread )
@daemon.start.should == mock_drb_thread
end
end
end
describe Ezmlm::ListDaemon::Service do
before( :each ) do
@dummydir = 'lists'
@service = Ezmlm::ListDaemon::Service.new( @dummydir )
end
it "can return a list object by name if there is a corresponding listdir" do
@service.get_list( 'announce' ).should be_an_instance_of( Ezmlm::List )
end
it "raises an exception when asked for a list whose name contains invalid characters" do
lambda {
@service.get_list( 'glarg beegun' )
}.should raise_error( ArgumentError )
end
it "can iterate over listdirs, yielding each as a Ezmlm::List object" do
Ezmlm.should_receive( :each_list ).with( Pathname.new(@dummydir) ).and_yield( :a_list )
@service.each_list {|l| l.should == :a_list }
end
end
# listservice = DRbObject.new( nil, 'druby://lists.laika.com:23431' )
# announce = listservice.each_list do |list|
# last_posts << list.last_post
# end
# announce = listservice.get_list( 'announce' )
#
# announce.last_post
#
#

View file

@ -3,66 +3,50 @@
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
require_relative 'spec_helpers'
require 'ezmlm'
describe Ezmlm do
include Ezmlm::SpecHelpers
TEST_LISTSDIR = '/tmp/lists'
it "can fetch a list of all mailing list subdirectories beneath a given directory" do
file_entry = mock( "plain file" )
file_entry.should_receive( :directory? ).and_return( false )
file_entry = double( "plain file" )
expect( file_entry ).to 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",
nonexistant_mlentry = double( "mailinglist path that doesn't exist", :exist? => false )
nonml_dir_entry = double( "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 )
existant_mlentry = double( "mailinglist path that does exist", :exist? => true )
ml_dir_entry = double( "directory with a mailinglist file", :directory? => true, :+ => existant_mlentry )
Pathname.should_receive( :glob ).with( an_instance_of(Pathname) ).
expect( Pathname ).to receive( :glob ).with( an_instance_of(Pathname) ).
and_return([ file_entry, nonml_dir_entry, ml_dir_entry ])
dirs = Ezmlm.find_directories( TEST_LISTSDIR )
dirs.should have(1).member
dirs.should include( ml_dir_entry )
expect( dirs.size ).to eq( 1 )
expect( dirs ).to include( ml_dir_entry )
end
it "can iterate over all mailing lists in a specified directory" do
Ezmlm.should_receive( :find_directories ).with( TEST_LISTSDIR ).and_return([ :listdir1, :listdir2 ])
expect( Ezmlm ).to receive( :find_directories ).with( TEST_LISTSDIR ).and_return([ :listdir1, :listdir2 ])
Ezmlm::List.should_receive( :new ).with( :listdir1 ).and_return( :listobject1 )
Ezmlm::List.should_receive( :new ).with( :listdir2 ).and_return( :listobject2 )
expect( Ezmlm::List ).to receive( :new ).with( :listdir1 ).and_return( :listobject1 )
expect( Ezmlm::List ).to receive( :new ).with( :listdir2 ).and_return( :listobject2 )
lists = []
Ezmlm.each_list( TEST_LISTSDIR ) do |list|
lists << list
end
lists.should have(2).members
lists.should include( :listobject1, :listobject2 )
expect( lists.size ).to eq(2)
expect( lists ).to include( :listobject1, :listobject2 )
end
end

View file

@ -1,157 +0,0 @@
#!/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

45
spec/spec_helpers.rb Normal file
View file

@ -0,0 +1,45 @@
#!/usr/bin/ruby
require 'simplecov' if ENV['COVERAGE']
require 'rspec'
require 'loggability/spechelpers'
module SpecHelpers
TEST_LISTSDIR = ENV['TEST_LISTSDIR'] || '/tmp/lists'
###############
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
RSpec.configure do |config|
include SpecHelpers
config.run_all_when_everything_filtered = true
config.filter_run :focus
config.order = 'random'
config.mock_with( :rspec ) do |mock|
mock.syntax = :expect
end
config.include( SpecHelpers )
end

800
utils.rb
View file

@ -1,800 +0,0 @@
#
# Install/distribution utility functions
# $Id$
#
# Copyright (c) 2001-2008, The FaerieMUD Consortium.
#
# 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.
#
BEGIN {
require 'rbconfig'
require 'uri'
require 'find'
require 'pp'
require 'irb'
begin
require 'readline'
include Readline
rescue LoadError => e
$stderr.puts "Faking readline..."
def readline( prompt )
$stderr.print prompt.chomp
return $stdin.gets.chomp
end
end
}
### Command-line utility functions
module UtilityFunctions
include Config
# The list of regexen that eliminate files from the MANIFEST
ANTIMANIFEST = [
/makedist\.rb/,
/\bCVS\b/,
/~$/,
/^#/,
%r{docs/html},
%r{docs/man},
/\bTEMPLATE\.\w+\.tpl\b/,
/\.cvsignore/,
/\.s?o$/,
]
# Set some ANSI escape code constants (Shamelessly stolen from Perl's
# Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com>
AnsiAttributes = {
'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
}
ErasePreviousLine = "\033[A\033[K"
ManifestHeader = (<<-"EOF").gsub( /^\t+/, '' )
#
# Distribution Manifest
# Created: #{Time::now.to_s}
#
EOF
###############
module_function
###############
# Create a string that contains the ANSI codes specified and return it
def ansi_code( *attributes )
attributes.flatten!
# $stderr.puts "Returning ansicode for TERM = %p: %p" %
# [ ENV['TERM'], attributes ]
return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM']
attributes = AnsiAttributes.values_at( *attributes ).compact.join(';')
# $stderr.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, etc.
def colorize( string, *attributes )
ending = string[/(\s)$/] || ''
string = string.rstrip
return ansi_code( attributes.flatten ) + string + ansi_code( 'reset' ) + ending
end
# Test for the presence of the specified <tt>library</tt>, and output a
# message describing the test using <tt>nicename</tt>. If <tt>nicename</tt>
# is <tt>nil</tt>, the value in <tt>library</tt> is used to build a default.
def test_for_library( library, nicename=nil, progress=false )
nicename ||= library
message( "Testing for the #{nicename} library..." ) if progress
if $LOAD_PATH.detect {|dir|
File.exists?(File.join(dir,"#{library}.rb")) ||
File.exists?(File.join(dir,"#{library}.#{CONFIG['DLEXT']}"))
}
message( "found.\n" ) if progress
return true
else
message( "not found.\n" ) if progress
return false
end
end
# Test for the presence of the specified <tt>library</tt>, and output a
# message describing the problem using <tt>nicename</tt>. If
# <tt>nicename</tt> is <tt>nil</tt>, the value in <tt>library</tt> is used
# to build a default. If <tt>raaUrl</tt> and/or <tt>downloadUrl</tt> are
# specified, they are also use to build a message describing how to find the
# required library. If <tt>fatal</tt> is <tt>true</tt>, a missing library
# will cause the program to abort.
def test_for_required_library( library, nicename=nil, raaUrl=nil, downloadUrl=nil, fatal=true )
nicename ||= library
unless test_for_library( library, nicename )
msgs = [ "You are missing the required #{nicename} library.\n" ]
msgs << "RAA: #{raaUrl}\n" if raaUrl
msgs << "Download: #{downloadUrl}\n" if downloadUrl
if fatal
abort msgs.join('')
else
error_message msgs.join('')
end
end
return true
end
### Output <tt>msg</tt> as a ANSI-colored program/section header (white on
### blue).
def header( msg )
msg.chomp!
$stderr.puts ansi_code( 'bold', 'white', 'on_blue' ) + msg + ansi_code( 'reset' )
$stderr.flush
end
### Output <tt>msg</tt> to STDERR and flush it.
def message( *msgs )
$stderr.print( msgs.join("\n") )
$stderr.flush
end
### Output +msg+ to STDERR and flush it if $VERBOSE is true.
def verbose_msg( msg )
msg.chomp!
message( msg + "\n" ) if $VERBOSE
end
### Output the specified <tt>msg</tt> as an ANSI-colored error message
### (white on red).
def error_msg( msg )
message ansi_code( 'bold', 'white', 'on_red' ) + msg + ansi_code( 'reset' )
end
alias :error_message :error_msg
### Output the specified <tt>msg</tt> as an ANSI-colored debugging message
### (yellow on blue).
def debug_msg( msg )
return unless $DEBUG
msg.chomp!
$stderr.puts ansi_code( 'yellow' ) + ">>> #{msg}" + ansi_code( 'reset' )
$stderr.flush
end
### Erase the previous line (if supported by your terminal) and output the
### specified <tt>msg</tt> instead.
def replace_msg( msg )
$stderr.puts
$stderr.print ErasePreviousLine
message( msg )
end
alias :replace_message :replace_msg
### Output a divider made up of <tt>length</tt> hyphen characters.
def divider( length=75 )
$stderr.puts "\r" + ("-" * length )
end
alias :writeLine :divider
### Output the specified <tt>msg</tt> colored in ANSI red and exit with a
### status of 1.
def abort( msg )
print ansi_code( 'bold', 'red' ) + "Aborted: " + msg.chomp + ansi_code( 'reset' ) + "\n\n"
Kernel.exit!( 1 )
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
response = readline( ansi_code('bold', 'green') +
"#{prompt_string} " + ansi_code('reset') ) || ''
response.strip!
if block_given? && ! yield( response )
error_message( failure_msg + "\n\n" )
response = nil
end
end until response
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 until response
return response
end
$programs = {}
### Search for the program specified by the given <tt>progname</tt> in the
### user's <tt>PATH</tt>, and return the full path to it, or <tt>nil</tt> if
### no such program is in the path.
def find_program( progname )
unless $programs.key?( progname )
ENV['PATH'].split(File::PATH_SEPARATOR).each {|d|
file = File.join( d, progname )
if File.executable?( file )
$programs[ progname ] = file
break
end
}
end
return $programs[ progname ]
end
### Search for the release version for the project in the specified
### +directory+.
def extract_version( directory='.' )
release = nil
Dir::chdir( directory ) do
if File::directory?( "CVS" )
verbose_msg( "Project is versioned via CVS. Searching for RELEASE_*_* tags..." )
if (( cvs = find_program('cvs') ))
revs = []
output = %x{cvs log}
output.scan( /RELEASE_(\d+(?:_\d\w+)*)/ ) {|match|
rev = $1.split(/_/).collect {|s| Integer(s) rescue 0}
verbose_msg( "Found %s...\n" % rev.join('.') )
revs << rev
}
release = revs.sort.last
end
elsif File::directory?( '.svn' )
verbose_msg( "Project is versioned via Subversion" )
if (( svn = find_program('svn') ))
output = %x{svn pg project-version}.chomp
unless output.empty?
verbose_msg( "Using 'project-version' property: %p" % output )
release = output.split( /[._]/ ).collect {|s| Integer(s) rescue 0}
end
end
end
end
return release
end
### Find the current release version for the project in the specified
### +directory+ and return its successor.
def extract_next_version( directory='.' )
version = extract_version( directory ) || [0,0,0]
version.compact!
version[-1] += 1
return version
end
# Pattern for extracting the name of the project from a Subversion URL
SVNUrlPath = %r{
.*/ # Skip all but the last bit
([^/]+) # $1 = project name
/ # Followed by / +
(?:
trunk | # 'trunk'
(
branches | # ...or branches/branch-name
tags # ...or tags/tag-name
)/\w
)
$ # bound to the end
}ix
### Extract the project name (CVS Repository name) for the given +directory+.
def extract_project_name( directory='.' )
name = nil
Dir::chdir( directory ) do
# CVS-controlled
if File::directory?( "CVS" )
verbose_msg( "Project is versioned via CVS. Using repository name." )
name = File.open( "CVS/Repository", "r").readline.chomp
name.sub!( %r{.*/}, '' )
# Subversion-controlled
elsif File::directory?( '.svn' )
verbose_msg( "Project is versioned via Subversion" )
# If the machine has the svn tool, try to get the project name
if (( svn = find_program( 'svn' ) ))
# First try an explicit property
output = shell_command( svn, 'pg', 'project-name' )
if !output.empty?
verbose_msg( "Using 'project-name' property: %p" % output )
name = output.first.chomp
# If that doesn't work, try to figure it out from the URL
elsif (( uri = get_svn_uri() ))
name = uri.path.sub( SVNUrlPath ) { $1 }
end
end
end
# Fall back to guessing based on the directory name
unless name
name = File::basename(File::dirname( File::expand_path(__FILE__) ))
end
end
return name
end
### Extract the Subversion URL from the specified directory and return it as
### a URI object.
def get_svn_uri( directory='.' )
uri = nil
Dir::chdir( directory ) do
output = %x{svn info}
debug_msg( "Using info: %p" % output )
if /^URL: \s* ( .* )/xi.match( output )
uri = URI::parse( $1 )
end
end
return uri
end
### (Re)make a manifest file in the specified +path+.
def make_manifest( path="MANIFEST" )
if File::exists?( path )
reply = prompt_with_default( "Replace current '#{path}'? [yN]", "n" )
return false unless /^y/i.match( reply )
verbose_msg "Replacing manifest at '#{path}'"
else
verbose_msg "Creating new manifest at '#{path}'"
end
files = []
verbose_msg( "Finding files...\n" )
Find::find( Dir::pwd ) do |f|
Find::prune if File::directory?( f ) &&
/^\./.match( File::basename(f) )
verbose_msg( " found: #{f}\n" )
files << f.sub( %r{^#{Dir::pwd}/?}, '' )
end
files = vet_manifest( files )
verbose_msg( "Writing new manifest to #{path}..." )
File::open( path, File::WRONLY|File::CREAT|File::TRUNC ) do |ofh|
ofh.puts( ManifestHeader )
ofh.puts( files )
end
verbose_msg( "done." )
end
### Read the specified <tt>manifestFile</tt>, which is a text file
### describing which files to package up for a distribution. The manifest
### should consist of one or more lines, each containing one filename or
### shell glob pattern.
def read_manifest( manifestFile="MANIFEST" )
verbose_msg "Building manifest..."
raise "Missing #{manifestFile}, please remake it" unless File.exists? manifestFile
manifest = IO::readlines( manifestFile ).collect {|line|
line.chomp
}.select {|line|
line !~ /^(\s*(#.*)?)?$/
}
filelist = []
for pat in manifest
verbose_msg "Adding files that match '#{pat}' to the file list"
filelist |= Dir.glob( pat ).find_all {|f| FileTest.file?(f)}
end
verbose_msg "found #{filelist.length} files.\n"
return filelist
end
### Given a <tt>filelist</tt> like that returned by #read_manifest, remove
### the entries therein which match the Regexp objects in the given
### <tt>antimanifest</tt> and return the resultant Array.
def vet_manifest( filelist, antimanifest=ANTIMANIFEST )
origLength = filelist.length
verbose_msg "Vetting manifest..."
for regex in antimanifest
verbose_msg "\n\tPattern /#{regex.source}/ removed: " +
filelist.find_all {|file| regex.match(file)}.join(', ')
filelist.delete_if {|file| regex.match(file)}
end
verbose_msg "removed #{origLength - filelist.length} files from the list.\n"
return filelist
end
### Combine a call to #read_manifest with one to #vet_manifest.
def get_vetted_manifest( manifestFile="MANIFEST", antimanifest=ANTIMANIFEST )
vet_manifest( read_manifest(manifestFile), antimanifest )
end
### Given a documentation <tt>catalogFile</tt>, extract the title, if
### available, and return it. Otherwise generate a title from the name of
### the CVS module.
def find_rdoc_title( catalogFile="docs/CATALOG" )
# Try extracting it from the CATALOG file from a line that looks like:
# Title: Foo Bar Module
title = find_catalog_keyword( 'title', catalogFile )
# If that doesn't work for some reason, use the name of the project.
title = extract_project_name()
return title
end
### Given a documentation <tt>catalogFile</tt>, extract the name of the file
### to use as the initally displayed page. If extraction fails, the
### +default+ will be used if it exists. Returns +nil+ if there is no main
### file to be found.
def find_rdoc_main( catalogFile="docs/CATALOG", default="README" )
# Try extracting it from the CATALOG file from a line that looks like:
# Main: Foo Bar Module
main = find_catalog_keyword( 'main', catalogFile )
# Try to make some educated guesses if that doesn't work
if main.nil?
basedir = File::dirname( __FILE__ )
basedir = File::dirname( basedir ) if /docs$/ =~ basedir
if File::exists?( File::join(basedir, default) )
main = default
end
end
return main
end
### Given a documentation <tt>catalogFile</tt>, extract an upload URL for
### RDoc.
def find_rdoc_upload( catalogFile="docs/CATALOG" )
find_catalog_keyword( 'upload', catalogFile )
end
### Given a documentation <tt>catalogFile</tt>, extract a CVS web frontend
### URL for RDoc.
def find_rdoc_cvs_url( catalogFile="docs/CATALOG" )
find_catalog_keyword( 'webcvs', catalogFile )
end
### Find one or more 'accessor' directives in the catalog if they exist and
### return an Array of them.
def find_rdoc_accessors( catalogFile="docs/CATALOG" )
accessors = []
in_attr_section = false
indent = ''
if File::exists?( catalogFile )
verbose_msg "Extracting accessors from CATALOG file (%s).\n" % catalogFile
# Read lines from the catalog
File::foreach( catalogFile ) do |line|
debug_msg( " Examining line #{line.inspect}..." )
# Multi-line accessors
if in_attr_section
if /^#\s+([a-z0-9_]+(?:\s*=\s*.*)?)$/i.match( line )
debug_msg( " Found accessor: #$1" )
accessors << $1
next
end
debug_msg( " End of accessors section." )
in_attr_section = false
# Single-line accessor
elsif /^#\s*Accessors:\s*(\S+)$/i.match( line )
debug_msg( " Found single accessors line: #$1" )
vals = $1.split(/,/).collect {|val| val.strip }
accessors.replace( vals )
# Multi-line accessor header
elsif /^#\s*Accessors:\s*$/i.match( line )
debug_msg( " Start of accessors section." )
in_attr_section = true
end
end
end
debug_msg( "Found accessors: %s" % accessors.join(",") )
return accessors
end
### Given a documentation <tt>catalogFile</tt>, try extracting the given
### +keyword+'s value from it. Keywords are lines that look like:
### # <keyword>: <value>
### Returns +nil+ if the catalog file was unreadable or didn't contain the
### specified +keyword+.
def find_catalog_keyword( keyword, catalogFile="docs/CATALOG" )
val = nil
if File::exists? catalogFile
verbose_msg "Extracting '#{keyword}' from CATALOG file (%s).\n" % catalogFile
File::foreach( catalogFile ) do |line|
debug_msg( "Examining line #{line.inspect}..." )
val = $1.strip and break if /^#\s*#{keyword}:\s*(.*)$/i.match( line )
end
end
return val
end
### Given a documentation <tt>catalogFile</tt>, which is in the same format
### as that described by #read_manifest, read and expand it, and then return
### a list of those files which appear to have RDoc documentation in
### them. If <tt>catalogFile</tt> is nil or does not exist, the MANIFEST
### file is used instead.
def find_rdocable_files( catalogFile="docs/CATALOG" )
startlist = []
if File.exists? catalogFile
verbose_msg "Using CATALOG file (%s).\n" % catalogFile
startlist = get_vetted_manifest( catalogFile )
else
verbose_msg "Using default MANIFEST\n"
startlist = get_vetted_manifest()
end
verbose_msg "Looking for RDoc comments in:\n"
startlist.select {|fn|
verbose_msg " #{fn}: "
found = false
File::open( fn, "r" ) {|fh|
fh.each {|line|
if line =~ /^(\s*#)?\s*=/ || line =~ /:\w+:/ || line =~ %r{/\*}
found = true
break
end
}
}
verbose_msg( (found ? "yes" : "no") + "\n" )
found
}
end
### Open a file and filter each of its lines through the given block a
### <tt>line</tt> at a time. The return value of the block is used as the
### new line, or omitted if the block returns <tt>nil</tt> or
### <tt>false</tt>.
def edit_in_place( file, testMode=false ) # :yields: line
raise "No block specified for editing operation" unless block_given?
tempName = "#{file}.#{$$}"
File::open( tempName, File::RDWR|File::CREAT, 0600 ) {|tempfile|
File::open( file, File::RDONLY ) {|fh|
fh.each {|line|
newline = yield( line ) or next
tempfile.print( newline )
$stderr.puts "%p -> %p" % [ line, newline ] if
line != newline
}
}
}
if testMode
File::unlink( tempName )
else
File::rename( tempName, file )
end
end
### Execute the specified shell <tt>command</tt>, read the results, and
### return them. Like a %x{} that returns an Array instead of a String.
def shell_command( *command )
raise "Empty command" if command.empty?
cmdpipe = IO::popen( command.join(' '), 'r' )
return cmdpipe.readlines
end
### Execute a block with $VERBOSE set to +false+, restoring it to its
### previous value before returning.
def verbose_off
raise LocalJumpError, "No block given" unless block_given?
thrcrit = Thread.critical
oldverbose = $VERBOSE
begin
Thread.critical = true
$VERBOSE = false
yield
ensure
$VERBOSE = oldverbose
Thread.critical = false
end
end
### Try the specified code block, printing the given
def try( msg, bind=TOPLEVEL_BINDING )
result = ''
if msg =~ /^to\s/
message "Trying #{msg}...\n"
else
message msg + "\n"
end
begin
rval = nil
if block_given?
rval = yield
else
file, line = caller(1)[0].split(/:/,2)
rval = eval( msg, bind, file, line.to_i )
end
PP.pp( rval, result )
rescue Exception => err
if err.backtrace
nicetrace = err.backtrace.delete_if {|frame|
/in `(try|eval)'/ =~ frame
}.join("\n\t")
else
nicetrace = "Exception had no backtrace"
end
result = err.message + "\n\t" + nicetrace
ensure
divider
message result.chomp + "\n"
divider
$stderr.puts
end
end
### Start an IRB session with the specified binding +b+ as the current scope.
def start_irb_session( b )
IRB.setup(nil)
workspace = IRB::WorkSpace.new( b )
if IRB.conf[:SCRIPT]
irb = IRB::Irb.new( workspace, IRB.conf[:SCRIPT] )
else
irb = IRB::Irb.new( workspace )
end
IRB.conf[:IRB_RC].call( irb.context ) if IRB.conf[:IRB_RC]
IRB.conf[:MAIN_CONTEXT] = irb.context
trap("SIGINT") do
irb.signal_handle
end
catch(:IRB_EXIT) do
irb.eval_input
end
end
end # module UtilityFunctions
if __FILE__ == $0
# $DEBUG = true
include UtilityFunctions
projname = extract_project_name()
header "Project: #{projname}"
ver = extract_version() || [0,0,1]
puts "Version: %s\n" % ver.join('.')
if File::directory?( "docs" )
puts "Rdoc:",
" Title: " + find_rdoc_title(),
" Main: " + find_rdoc_main(),
" Upload: " + find_rdoc_upload(),
" SCCS URL: " + find_rdoc_cvs_url(),
" Accessors: " + find_rdoc_accessors().join(",")
end
puts "Manifest:",
" " + get_vetted_manifest().join("\n ")
end