svn-fixture 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,10 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ .project
7
+ .loadpath
8
+ *qt_temp*
9
+ *~
10
+ *.gem
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.
@@ -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.
@@ -0,0 +1,54 @@
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{Ruby library to create Subversion repositories useful for tests}
9
+ gem.email = "jmorgan@morgancreative.net"
10
+ gem.homepage = "http://github.com/jm81/svn-fixture"
11
+ gem.authors = ["Jared Morgan"]
12
+ gem.description = <<-EOF
13
+ svn-fixture simplifies creating (or updating) a Subversion repository. It is
14
+ designed to be used in tests that require a Subversion repo, but can also be
15
+ used to initialize a repository according to some template. svn-fixture depends
16
+ on the Subversion Ruby bindings.
17
+ EOF
18
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
19
+ end
20
+ Jeweler::GemcutterTasks.new
21
+ rescue LoadError
22
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
23
+ end
24
+
25
+ require 'spec/rake/spectask'
26
+ Spec::Rake::SpecTask.new(:spec) do |spec|
27
+ spec.libs << 'lib' << 'spec'
28
+ spec.spec_files = FileList['spec/**/*_spec.rb']
29
+ end
30
+
31
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
32
+ spec.libs << 'lib' << 'spec'
33
+ spec.pattern = 'spec/**/*_spec.rb'
34
+ spec.rcov = true
35
+ end
36
+
37
+
38
+ task :default => :spec
39
+
40
+ require 'rake/rdoctask'
41
+ Rake::RDocTask.new do |rdoc|
42
+ if File.exist?('VERSION.yml')
43
+ config = YAML.load(File.read('VERSION.yml'))
44
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
45
+ else
46
+ version = ""
47
+ end
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "svn-fixture #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
54
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.2
@@ -0,0 +1,73 @@
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
+ VERSION = '0.1.2'
12
+
13
+ CONFIG_DEFAULTS = {
14
+ :base_path => File.join(Dir.tmpdir, 'svn-fixture')
15
+ }
16
+
17
+ class << self
18
+ # SvnFixture::config method returns Hash that can be edited.
19
+ # The only current option is
20
+ # +:base_path+ : The path at which repositories are created. It default
21
+ # to the OS tmp directory, plus "svn-fixture". For example,
22
+ # "/tmp/svn-fixture". The repo name is then appended in
23
+ # +SvnFixture::Repository+.
24
+ def config
25
+ @config ||= CONFIG_DEFAULTS.dup
26
+ end
27
+
28
+ # Return time string formatted as expected by ::Svn::Client::Context#propset
29
+ # (example 2009-06-28T12:00:00.000000Z). If +val+ does not respond to
30
+ # +strftime+, val will first be parsed via +Time.parse+.
31
+ def svn_time(val)
32
+ return nil if val.nil?
33
+ val = Time.parse(val) unless val.respond_to?(:strftime)
34
+ val.strftime("%Y-%m-%dT%H:%M:%S.000000Z")
35
+ end
36
+
37
+ # Return a Date or Time formatted as expected by
38
+ # ::Svn::Client::Context#propset (see +svn_time+); leave other values alone.
39
+ def svn_prop(val)
40
+ val.respond_to?(:strftime) ? svn_time(val) : val
41
+ end
42
+
43
+ # .repo is just a shortcut to +SvnFixture::Repository.get+
44
+ def repo(*args, &block)
45
+ SvnFixture::Repository.get(*args, &block)
46
+ end
47
+
48
+ # Setup and return a simple ::Svn::Client::Context. This is called by
49
+ # Repository#checkout, but can also be used in called Directory.new or
50
+ # File.new directly. See SvnFixture::File for examples.
51
+ def simple_context
52
+ ctx = ::Svn::Client::Context.new
53
+
54
+ # I don't understand the auth_baton and log_baton, so I set them here,
55
+ # then use revision properties.
56
+ ctx.add_username_prompt_provider(0) do |cred, realm, username, may_save|
57
+ cred.username = "ANON"
58
+ end
59
+ ctx.set_log_msg_func {|items| [true, ""]}
60
+ ctx
61
+ end
62
+ end
63
+ end
64
+
65
+ if defined?(Merb::Plugins)
66
+ # Make config accessible through Merb's Merb::Plugins.config hash
67
+ Merb::Plugins.config[:svn_fixture] = SvnFixture.config
68
+ end
69
+
70
+ require 'svn-fixture/repository'
71
+ require 'svn-fixture/revision'
72
+ require 'svn-fixture/directory'
73
+ require 'svn-fixture/file'
@@ -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,172 @@
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
+ FileUtils.mkdir_p(@wc_path)
117
+ @dirs_created << @wc_path
118
+ @ctx = SvnFixture::simple_context
119
+ @ctx.checkout(self.uri, @wc_path)
120
+ self
121
+ end
122
+
123
+ # Commit actually commits the changes of the revisions. It optionally
124
+ # accepts Revisions or Revision names. If none are given, it commits all
125
+ # revisions. If any of the arguments are Revisions (not revision names),
126
+ # they do not need to be explicitly part of this Repository (that is, they
127
+ # do not need to have been created through self#revision)
128
+ #
129
+ # repos.commit # Commits all Revisions added through self#revision
130
+ # repos.commit(1,2,4) # Commits Revisions named 1, 2, and 4, added through self#revision
131
+ # repos.commit(rev1, rev3) # Assuming rev1 and rev3 are instances of
132
+ # # SvnFixture::Revision, commits them
133
+ # # whether or not they were added through self#revision
134
+ #
135
+ # A Revision can be added to the revisions Array directly:
136
+ #
137
+ # repos.revisions << Revision.new(1, 'msg')
138
+ def commit(*to_commit)
139
+ checkout unless ::File.exist?(@wc_path)
140
+ to_commit = @revisions if to_commit.empty?
141
+ to_commit = [to_commit] if (!to_commit.respond_to?(:each) || to_commit.kind_of?(String))
142
+
143
+ to_commit.each do | rev |
144
+ rev = @revisions.find{ |r| r.name == rev } unless rev.kind_of?(Revision)
145
+ rev.commit(self)
146
+ end
147
+ end
148
+
149
+ # Remove Repository from +.repositories+ and delete repos and working copy
150
+ # directories.
151
+ def destroy
152
+ @dirs_created.each { |d| FileUtils.rm_rf(d) }
153
+ self.class.repositories.delete(@name)
154
+ end
155
+
156
+ # URI (file://...) for accessing the Repository
157
+ def uri
158
+ "file://" + ::File.expand_path(@repos_path)
159
+ end
160
+
161
+ private
162
+
163
+ # Check if either @repos_path or @wc_path exist. Called by #initialize.
164
+ def check_paths_available
165
+ if ::File.exist?(@repos_path)
166
+ raise RuntimeError, "repos_path already exists (#{@repos_path})"
167
+ elsif ::File.exist?(@wc_path)
168
+ raise RuntimeError, "wc_path already exists (#{@wc_path})"
169
+ end
170
+ end
171
+ end
172
+ end