jm81-svn-fixture 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ .project
7
+ .loadpath
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Jared Morgan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,77 @@
1
+ svn-fixture
2
+ ===========
3
+
4
+ svn-fixture simplifies creating (or updating) a Subversion repository. It is
5
+ designed to be used in tests that require a Subversion repo, but can also be
6
+ used to initialize a repository according to some template. svn-fixture depends
7
+ on the Subversion Ruby bindings (see below for Installation help).
8
+
9
+ ##Usage
10
+
11
+ svn-fixture uses blocks to mimic the structure of the Repository, in the
12
+ hierarchy: Repository -> Revision -> Directory tree structure with
13
+ subdirectories and files. For example:
14
+
15
+ SvnFixture::repo('hello_world') do
16
+ revision(1, 'Create directories',
17
+ :author => 'jmorgan',
18
+ :date => Time.parse('2009-01-01 12:00:00Z')) do
19
+ dir 'app'
20
+ dir 'docs'
21
+ dir 'lib'
22
+ end
23
+
24
+ revision 2, 'Add a file' do
25
+ dir 'app' do
26
+ file 'hello.rb' do
27
+ prop 'is_ruby', 'Yes'
28
+ body 'puts "Hello World"'
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ SvnFixture::repo('hello_world').commit
35
+
36
+ See spec/svn-fixture/fixtures/hello_world.rb and
37
+ spec/svn-fixture/integration_spec.rb for a more complete example.
38
+
39
+ Each Repository is given a name ('hello_world' in the example above), so it can
40
+ be reopened multiple times. Repository#revision defines a new Revision. It
41
+ requires a name--but only for informational purposes--and a log message. A
42
+ Revision also accepts an options Hash including optional :author and :date
43
+ revision properties.
44
+
45
+ Within a Revision is a directory tree, specifying only **changes** in that
46
+ Revision. See Directory and File classes for details on available methods.
47
+
48
+ To actually (optionally) create the repository and make the changes and commits
49
+ specified in the Revision blocks, call Repository#commit. See Repository class
50
+ for finer tuned control over the create/checkout/commit process.
51
+
52
+ ##Installation
53
+
54
+ Install Subversion Swig bindings for Ruby. Some distros have a package for this.
55
+ In debian: sudo apt-get install libsvn-ruby . See
56
+ [https://bssvnbrowser.bountysource.com/docs/subversion_ruby_bindings](https://bssvnbrowser.bountysource.com/docs/subversion_ruby_bindings) or
57
+ [http://svn.collab.net/repos/svn/trunk/subversion/bindings/swig/INSTALL](http://svn.collab.net/repos/svn/trunk/subversion/bindings/swig/INSTALL)
58
+ for more information.
59
+
60
+ To install the gem:
61
+
62
+ gem sources -a http://gems.github.com
63
+ sudo gem install jm81-svn-fixture
64
+
65
+ To require:
66
+
67
+ gem 'jm81-svn-fixture'
68
+ require 'svn-fixture'
69
+
70
+ Note: This library could work using the svn command line client instead. I use
71
+ the bindings regularly, so using them makes sense for me. However, if you want
72
+ to be able to use svn-fixture without installing the bindings, please send an
73
+ email to jmorgan at morgancreative dot net, and I'll give it a shot.
74
+
75
+ ##Copyright
76
+
77
+ Copyright (c) 2009 Jared Morgan. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,48 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "svn-fixture"
8
+ gem.summary = %Q{TODO}
9
+ gem.email = "jmorgan@morgancreative.net"
10
+ gem.homepage = "http://github.com/jm81/svn-fixture"
11
+ gem.authors = ["Jared Morgan"]
12
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
13
+ end
14
+
15
+ rescue LoadError
16
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
17
+ end
18
+
19
+ require 'spec/rake/spectask'
20
+ Spec::Rake::SpecTask.new(:spec) do |spec|
21
+ spec.libs << 'lib' << 'spec'
22
+ spec.spec_files = FileList['spec/**/*_spec.rb']
23
+ end
24
+
25
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
26
+ spec.libs << 'lib' << 'spec'
27
+ spec.pattern = 'spec/**/*_spec.rb'
28
+ spec.rcov = true
29
+ end
30
+
31
+
32
+ task :default => :spec
33
+
34
+ require 'rake/rdoctask'
35
+ Rake::RDocTask.new do |rdoc|
36
+ if File.exist?('VERSION.yml')
37
+ config = YAML.load(File.read('VERSION.yml'))
38
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
39
+ else
40
+ version = ""
41
+ end
42
+
43
+ rdoc.rdoc_dir = 'rdoc'
44
+ rdoc.title = "svn-fixture #{version}"
45
+ rdoc.rdoc_files.include('README*')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
48
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,71 @@
1
+ require "fileutils"
2
+ require "tmpdir"
3
+ require "date"
4
+ require "time"
5
+ # Subversion Ruby bindings must be installed (see README)
6
+ require "svn/core"
7
+ require "svn/fs"
8
+ require "svn/repos"
9
+
10
+ module SvnFixture
11
+ CONFIG_DEFAULTS = {
12
+ :base_path => File.join(Dir.tmpdir, 'svn-fixture')
13
+ }
14
+
15
+ class << self
16
+ # SvnFixture::config method returns Hash that can be edited.
17
+ # The only current option is
18
+ # +:base_path+ : The path at which repositories are created. It default
19
+ # to the OS tmp directory, plus "svn-fixture". For example,
20
+ # "/tmp/svn-fixture". The repo name is then appended in
21
+ # +SvnFixture::Repository+.
22
+ def config
23
+ @config ||= CONFIG_DEFAULTS.dup
24
+ end
25
+
26
+ # Return time string formatted as expected by ::Svn::Client::Context#propset
27
+ # (example 2009-06-28T12:00:00.000000Z). If +val+ does not respond to
28
+ # +strftime+, val will first be parsed via +Time.parse+.
29
+ def svn_time(val)
30
+ return nil if val.nil?
31
+ val = Time.parse(val) unless val.respond_to?(:strftime)
32
+ val.strftime("%Y-%m-%dT%H:%M:%S.000000Z")
33
+ end
34
+
35
+ # Return a Date or Time formatted as expected by
36
+ # ::Svn::Client::Context#propset (see +svn_time+); leave other values alone.
37
+ def svn_prop(val)
38
+ val.respond_to?(:strftime) ? svn_time(val) : val
39
+ end
40
+
41
+ # .repo is just a shortcut to +SvnFixture::Repository.get+
42
+ def repo(*args, &block)
43
+ SvnFixture::Repository.get(*args, &block)
44
+ end
45
+
46
+ # Setup and return a simple ::Svn::Client::Context. This is called by
47
+ # Repository#checkout, but can also be used in called Directory.new or
48
+ # File.new directly. See SvnFixture::File for examples.
49
+ def simple_context
50
+ ctx = ::Svn::Client::Context.new
51
+
52
+ # I don't understand the auth_baton and log_baton, so I set them here,
53
+ # then use revision properties.
54
+ ctx.add_username_prompt_provider(0) do |cred, realm, username, may_save|
55
+ cred.username = "ANON"
56
+ end
57
+ ctx.set_log_msg_func {|items| [true, ""]}
58
+ ctx
59
+ end
60
+ end
61
+ end
62
+
63
+ if defined?(Merb::Plugins)
64
+ # Make config accessible through Merb's Merb::Plugins.config hash
65
+ Merb::Plugins.config[:svn_fixture] = SvnFixture.config
66
+ end
67
+
68
+ # Require classes
69
+ %w{ repository revision directory file }.each do |file|
70
+ require File.dirname(__FILE__) + '/svn-fixture/' + file
71
+ end
@@ -0,0 +1,99 @@
1
+ module SvnFixture
2
+ # A Directory to be added to or edited within the Repository. Normally, this
3
+ # would br done through Directory#dir, in a block given to a Directory or
4
+ # Revision, for example:
5
+ #
6
+ # SvnFixture.repo('repo_name') do
7
+ # revision(1, 'msg') do
8
+ # dir('test-dir') do
9
+ # prop('name', 'value')
10
+ # file('file.txt')
11
+ # end
12
+ # end
13
+ # end
14
+ #
15
+ # In that case, Revision takes care of passing the +ctx+ argument.
16
+ #
17
+ # To call SvnFixture::Directory.new directly, you will need to set up a
18
+ # context (instance of Svn::Client::Context) and check out a working copy.
19
+ # +SvnFixture.simple_context+ is a quick method for settin up a Context.
20
+ #
21
+ # Assuming an existing checked out working copy:
22
+ #
23
+ # ctx = SvnFixture.simple_context
24
+ # d = SvnFixture::Directory.new(ctx, '/full/fs/path/to/dir')
25
+ # d.prop('propname', 'Value')
26
+ #
27
+ # Or, call #checkout on Context:
28
+ #
29
+ # ctx = SvnFixture.simple_context
30
+ # ctx.checkout('file:///repository/uri', '/fs/path/of/wc')
31
+ # d = SvnFixture::Directory.new(ctx, '/fs/path/of/wc/to/dir')
32
+ # d.prop('propname', 'Value')
33
+ class Directory
34
+
35
+ # +new+ is normally called through Directory#dir (a block to a Revision is
36
+ # applied to the root Directory).
37
+ #
38
+ # Arguments are:
39
+ # - +ctx+: An Svn::Client::Context, normally from Repository#ctx
40
+ # - +path+: The path (on the file system) of the Directory in the working
41
+ # copy.
42
+ def initialize(ctx, path)
43
+ @ctx = ctx
44
+ @path = path
45
+ @path += "/" unless path[-1] == 47
46
+ end
47
+
48
+ # Create or access a subdirectory. Takes the name of the subdirectory (not a
49
+ # full path) and an optional block with the subdirectory as self.
50
+ def dir(name, &block)
51
+ path = @path + name
52
+ unless ::File.directory?(path)
53
+ FileUtils.mkdir_p(path)
54
+ @ctx.add(path)
55
+ end
56
+ d = self.class.new(@ctx, path)
57
+ d.instance_eval(&block) if block_given?
58
+ d
59
+ end
60
+
61
+ # Create or access a subdirectory. Takes the name of the file (not a
62
+ # full path) and an optional block with the File as self.
63
+ def file(name, &block)
64
+ path = @path + name
65
+ unless ::File.file?(path)
66
+ FileUtils.touch(path)
67
+ @ctx.add(path)
68
+ end
69
+ f = File.new(@ctx, path)
70
+ f.instance_eval(&block) if block_given?
71
+ f
72
+ end
73
+
74
+ # Move a File or Directory. From should be an existing node. From and to can
75
+ # be any relative path below the directory.
76
+ def move(from, to)
77
+ @ctx.mv(@path + from, @path + to)
78
+ end
79
+
80
+ # Copy a File or Directory. From should be an existing node. From and to can
81
+ # be any relative path below the directory.
82
+ def copy(from, to)
83
+ @ctx.cp(@path + from, @path + to)
84
+ end
85
+
86
+ # Delete (and remove from Repository) a child node.
87
+ def delete(name)
88
+ @ctx.delete(@path + name)
89
+ end
90
+
91
+ # Set a property for the Directory
92
+ # (see http://svnbook.red-bean.com/en/1.1/ch07s02.html):
93
+ # - +name+: The property name (must be "human-readable text")
94
+ # - +value+: The value of the property.
95
+ def prop(name, value)
96
+ @ctx.propset(name, SvnFixture.svn_prop(value), @path[0..-2])
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,58 @@
1
+ module SvnFixture
2
+ # A File to be added to or edited within the Repository. Normally, this would
3
+ # done through Directory#file, in a block given to a Directory or
4
+ # Revision, for example:
5
+ #
6
+ # SvnFixture.repo('repo_name') do
7
+ # revision(1, 'msg') do
8
+ # file('file.txt') do
9
+ # prop('name', 'value')
10
+ # body('Some Text')
11
+ # end
12
+ # end
13
+ # end
14
+ #
15
+ # In that case, Revision takes care of passing the +ctx+ argument.
16
+ #
17
+ # To call SvnFixture::File.new directly, you will need to set up a context
18
+ # (instance of Svn::Client::Context) and check out a working copy.
19
+ # +SvnFixture.simple_context+ is a quick method for settin up a Context.
20
+ #
21
+ # Assuming an existing checked out working copy:
22
+ #
23
+ # ctx = SvnFixture.simple_context
24
+ # f = SvnFixture::File.new(ctx, '/full/fs/path/to/file.txt')
25
+ # f.prop('propname', 'Value')
26
+ #
27
+ # Or, call #checkout on Context:
28
+ #
29
+ # ctx = SvnFixture.simple_context
30
+ # ctx.checkout('file:///repository/uri', '/fs/path/of/wc')
31
+ # f = SvnFixture::File.new(ctx, '/full/fs/path/to/file.txt')
32
+ # f.prop('propname', 'Value')
33
+ class File
34
+
35
+ # +new+ is normally called through Directory#file (a block to a Revision is
36
+ # applied to the root Directory).
37
+ #
38
+ # Arguments are:
39
+ # - +ctx+: An Svn::Client::Context, normally from Repository#ctx
40
+ # - +path+: The path (on the file system) of the File in the working copy
41
+ def initialize(ctx, path)
42
+ @ctx, @path = ctx, path
43
+ end
44
+
45
+ # Set a property for the file
46
+ # (see http://svnbook.red-bean.com/en/1.1/ch07s02.html):
47
+ # - +name+: The property name (must be "human-readable text")
48
+ # - +value+: The value of the property.
49
+ def prop(name, value)
50
+ @ctx.propset(name, SvnFixture.svn_prop(value), @path)
51
+ end
52
+
53
+ # Set the content of a file
54
+ def body(val)
55
+ ::File.open(@path, 'w') { |f| f.write(val) }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,168 @@
1
+ module SvnFixture
2
+ # Repository sets up the repository and is reponsible for checkouts and
3
+ # the actual commit(s). No actual work is done until +commit+ is called.
4
+ class Repository
5
+ attr_reader :repos, :ctx, :wc_path, :revisions
6
+
7
+ class << self
8
+ # Get an SvnFixture::Repository by name. If not found, it creates a new
9
+ # one. It accepts a block which is evaluated within the Repository
10
+ # instance. +get+ is useful for re-accessing a Repository after initially
11
+ # created. For example:
12
+ #
13
+ # SvnFixture::Repository.get('test') do
14
+ # revision(1, 'log msg') ...
15
+ # end
16
+ #
17
+ # SvnFixture::Repository.get('test') do
18
+ # revision(2, 'log msg') ...
19
+ # revision(3, 'log msg') ...
20
+ # end
21
+ #
22
+ # SvnFixture::Repository.get('test').commit
23
+ def get(name, repos_path = nil, wc_path = nil, &block)
24
+ if repositories[name]
25
+ repositories[name].instance_eval(&block) if block_given?
26
+ repositories[name]
27
+ else
28
+ Repository.new(name, repos_path, wc_path, &block)
29
+ end
30
+ end
31
+
32
+ # Hash of {name => Repository} of currently defined Repositories
33
+ def repositories
34
+ @repositories ||= {}
35
+ end
36
+
37
+ # Remove all Repositories from +.repositories+ and delete repos and
38
+ # working copy directories. Useful to call upon completion of tests.
39
+ def destroy_all
40
+ repositories.each {|name, repo| repo.destroy}
41
+ end
42
+ end
43
+
44
+ # Arguments (last two are optional)
45
+ # - +name+: The name of the repository, used by Repository.get and used in
46
+ # +repos_path+ and +wc_path+ if not given.
47
+ # - +repos_path+: The path where the repository is stored (defaults to
48
+ # "#{config[:base_path]}/repo_#{name}"
49
+ # - +wc_path+: The path where the working copy is checked out (defaults to
50
+ # "#{config[:base_path]}/wc_#{name}"
51
+ # Note: the paths should be normal file system paths, not file:/// paths.
52
+ #
53
+ # +new+ also accepts a block which is evaluated within the Repository
54
+ # instance:
55
+ #
56
+ # SvnFixture::Repository.new('name') do
57
+ # revision(1, 'log msg') ...
58
+ # end
59
+ #
60
+ # Otherwise, you could, for example:
61
+ #
62
+ # r = SvnFixture::Repository.new('name')
63
+ # r.revision(1, 'log msg') do
64
+ # ...
65
+ # end
66
+ # r.commit
67
+ def initialize(name, repos_path = nil, wc_path = nil, &block)
68
+ @name = name
69
+ if self.class.repositories[name]
70
+ raise RuntimeError, "A Repository with this name (#{@name}) already exists."
71
+ end
72
+
73
+ @repos_path = repos_path || ::File.join(SvnFixture::config[:base_path], "repo_#{name}")
74
+ @wc_path = wc_path || ::File.join(SvnFixture::config[:base_path], "wc_#{name}")
75
+ check_paths_available
76
+ @revisions = []
77
+ @dirs_created = [] # Keep track of any directories created for use by #destroy
78
+ self.class.repositories[name] = self
79
+ self.instance_eval(&block) if block_given?
80
+ end
81
+
82
+ # Add a Revision to this Repository. +name+ and +msg+ are required.
83
+ # - +name+: A name (or number of Revision). This is used in informational
84
+ # messages only.
85
+ # - +msg+: Log message for the revision.
86
+ # - +options+: :author and :date Revision properties.
87
+ # - Accepts a block that is processed by Revision#commit within a Directory
88
+ # instance (the root directory at this revision). See +Directory+ for
89
+ # more information.
90
+ def revision(name, msg, options = {}, &block)
91
+ r = Revision.new(name, msg, options, &block)
92
+ @revisions << r
93
+ r
94
+ end
95
+
96
+ # Create the Subversion repository. This is called by #checkout unless
97
+ # something already exists at @repos_path. It can also be called directly.
98
+ # This allows the flexibility of doing some work between creating the
99
+ # Repository and running checkout or commit (although I've yet to think of
100
+ # what that work would be), or creating the repository some other way.
101
+ def create
102
+ FileUtils.mkdir_p(@repos_path)
103
+ @dirs_created << @repos_path
104
+ ::Svn::Repos.create(@repos_path)
105
+ self
106
+ end
107
+
108
+ # Checkout a working copy, and setup context. This is call by #commit unless
109
+ # something already exists at @wc_path. It can also be called directly.
110
+ # This allows the flexibility of doing some work between checking out the
111
+ # Repository and commit, or checking out some other way. Also, calls #create
112
+ # if needed.
113
+ def checkout
114
+ create unless ::File.exist?(@repos_path)
115
+ @repos = ::Svn::Repos.open(@repos_path)
116
+ @repos_uri = "file://" + ::File.expand_path(@repos_path)
117
+ FileUtils.mkdir_p(@wc_path)
118
+ @dirs_created << @wc_path
119
+ @ctx = SvnFixture::simple_context
120
+ @ctx.checkout(@repos_uri, @wc_path)
121
+ self
122
+ end
123
+
124
+ # Commit actually commits the changes of the revisions. It optionally
125
+ # accepts Revisions or Revision names. If none are given, it commits all
126
+ # revisions. If any of the arguments are Revisions (not revision names),
127
+ # they do not need to be explicitly part of this Repository (that is, they
128
+ # do not need to have been created through self#revision)
129
+ #
130
+ # repos.commit # Commits all Revisions added through self#revision
131
+ # repos.commit(1,2,4) # Commits Revisions named 1, 2, and 4, added through self#revision
132
+ # repos.commit(rev1, rev3) # Assuming rev1 and rev3 are instances of
133
+ # # SvnFixture::Revision, commits them
134
+ # # whether or not they were added through self#revision
135
+ #
136
+ # A Revision can be added to the revisions Array directly:
137
+ #
138
+ # repos.revisions << Revision.new(1, 'msg')
139
+ def commit(*to_commit)
140
+ checkout unless ::File.exist?(@wc_path)
141
+ to_commit = @revisions if to_commit.empty?
142
+ to_commit = [to_commit] if (!to_commit.respond_to?(:each) || to_commit.kind_of?(String))
143
+
144
+ to_commit.each do | rev |
145
+ rev = @revisions.find{ |r| r.name == rev } unless rev.kind_of?(Revision)
146
+ rev.commit(self)
147
+ end
148
+ end
149
+
150
+ # Remove Repository from +.repositories+ and delete repos and working copy
151
+ # directories.
152
+ def destroy
153
+ @dirs_created.each { |d| FileUtils.rm_rf(d) }
154
+ self.class.repositories.delete(@name)
155
+ end
156
+
157
+ private
158
+
159
+ # Check if either @repos_path or @wc_path exist. Called by #initialize.
160
+ def check_paths_available
161
+ if ::File.exist?(@repos_path)
162
+ raise RuntimeError, "repos_path already exists (#{@repos_path})"
163
+ elsif ::File.exist?(@wc_path)
164
+ raise RuntimeError, "wc_path already exists (#{@wc_path})"
165
+ end
166
+ end
167
+ end
168
+ end