shrink_test_case: ensure that CVS files get closed.
[cvs2svn.git] / svntest / objects.py
blob8c8cdd5ab9f1ca5f40b924d708506517391d01f6
1 #!/usr/bin/env python
3 # objects.py: Objects that keep track of state during a test
5 # Subversion is a tool for revision control.
6 # See http://subversion.tigris.org for more information.
8 # ====================================================================
9 # Licensed to the Apache Software Foundation (ASF) under one
10 # or more contributor license agreements. See the NOTICE file
11 # distributed with this work for additional information
12 # regarding copyright ownership. The ASF licenses this file
13 # to you under the Apache License, Version 2.0 (the
14 # "License"); you may not use this file except in compliance
15 # with the License. You may obtain a copy of the License at
17 # http://www.apache.org/licenses/LICENSE-2.0
19 # Unless required by applicable law or agreed to in writing,
20 # software distributed under the License is distributed on an
21 # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
22 # KIND, either express or implied. See the License for the
23 # specific language governing permissions and limitations
24 # under the License.
25 ######################################################################
27 # General modules
28 import shutil, sys, re, os, subprocess
30 # Our testing module
31 import svntest
32 from svntest import actions, main, tree, verify, wc
35 ######################################################################
36 # Helpers
38 def local_path(path):
39 """Convert a path from internal style ('/' separators) to the local style."""
40 if path == '':
41 path = '.'
42 return os.sep.join(path.split('/'))
45 def db_dump(db_dump_name, repo_path, table):
46 """Yield a human-readable representation of the rows of the BDB table
47 TABLE in the repo at REPO_PATH. Yield one line of text at a time.
48 Calls the external program "db_dump" which is supplied with BDB."""
49 table_path = repo_path + "/db/" + table
50 process = subprocess.Popen([db_dump_name, "-p", table_path],
51 stdout=subprocess.PIPE, universal_newlines=True)
52 retcode = process.wait()
53 assert retcode == 0
55 # Strip out the header and footer; copy the rest into FILE.
56 copying = False
57 for line in process.stdout.readlines():
58 if line == "HEADER=END\n":
59 copying = True;
60 elif line == "DATA=END\n":
61 break
62 elif copying:
63 yield line
65 def pretty_print_skel(line):
66 """Return LINE modified so as to look prettier for human reading, but no
67 longer unambiguous or machine-parsable. LINE is assumed to be in the
68 "Skel" format in which some values are preceded by a decimal length. This
69 function removes the length indicator, and also replaces a zero-length
70 value with a pair of single quotes."""
71 new_line = ''
72 while line:
73 # an explicit-length atom
74 explicit_atom = re.match(r'\d+ ', line)
75 if explicit_atom:
76 n = int(explicit_atom.group())
77 line = line[explicit_atom.end():]
78 new_line += line[:n]
79 line = line[n:]
80 if n == 0:
81 new_line += "''"
82 continue
84 # an implicit-length atom
85 implicit_atom = re.match(r'[A-Za-z][^\s()]*', line)
86 if implicit_atom:
87 n = implicit_atom.end()
88 new_line += line[:n]
89 line = line[n:]
90 continue
92 # parentheses, white space or any other non-atom
93 new_line += line[:1]
94 line = line[1:]
96 return new_line
98 def dump_bdb(db_dump_name, repo_path, dump_dir):
99 """Dump all the known BDB tables in the repository at REPO_PATH into a
100 single text file in DUMP_DIR. Omit any "next-key" records."""
101 dump_file = dump_dir + "/all.bdb"
102 file = open(dump_file, 'w')
103 for table in ['revisions', 'transactions', 'changes', 'copies', 'nodes',
104 'node-origins', 'representations', 'checksum-reps', 'strings',
105 'locks', 'lock-tokens', 'miscellaneous', 'uuids']:
106 file.write(table + ":\n")
107 next_key_line = False
108 for line in db_dump(db_dump_name, repo_path, table):
109 # Omit any 'next-key' line and the following line.
110 if next_key_line:
111 next_key_line = False
112 continue
113 if line == ' next-key\n':
114 next_key_line = True
115 continue
116 # The line isn't necessarily a skel, but pretty_print_skel() shouldn't
117 # do too much harm if it isn't.
118 file.write(pretty_print_skel(line))
119 file.write("\n")
120 file.close()
122 def locate_db_dump():
123 """Locate a db_dump executable"""
124 # Assume that using the newest version is OK.
125 for db_dump_name in ['db4.8_dump', 'db4.7_dump', 'db4.6_dump',
126 'db4.5_dump', 'db4.4_dump', 'db_dump']:
127 try:
128 if subprocess.Popen([db_dump_name, "-V"]).wait() == 0:
129 return db_dump_name
130 except OSError, e:
131 pass
132 return 'none'
134 ######################################################################
135 # Class SvnRepository
137 class SvnRepository:
138 """An object of class SvnRepository represents a Subversion repository,
139 providing both a client-side view and a server-side view."""
141 def __init__(self, repo_url, repo_dir):
142 self.repo_url = repo_url
143 self.repo_absdir = os.path.abspath(repo_dir)
144 self.db_dump_name = locate_db_dump()
145 # This repository object's idea of its own head revision.
146 self.head_rev = 0
148 def dump(self, output_dir):
149 """Dump the repository into the directory OUTPUT_DIR"""
150 ldir = local_path(output_dir)
151 os.mkdir(ldir)
153 """Run a BDB dump on the repository"""
154 if self.db_dump_name != 'none':
155 dump_bdb(self.db_dump_name, self.repo_absdir, ldir)
157 """Run 'svnadmin dump' on the repository."""
158 exit_code, stdout, stderr = \
159 actions.run_and_verify_svnadmin(None, None, None,
160 'dump', self.repo_absdir)
161 ldumpfile = local_path(output_dir + "/svnadmin.dump")
162 main.file_write(ldumpfile, ''.join(stderr))
163 main.file_append(ldumpfile, ''.join(stdout))
165 def svn_mkdirs(self, *dirs):
166 """Run 'svn mkdir' on the repository. DIRS is a list of directories to
167 make, and each directory is a path relative to the repository root,
168 neither starting nor ending with a slash."""
169 urls = [self.repo_url + '/' + dir for dir in dirs]
170 actions.run_and_verify_svn(None, None, [],
171 'mkdir', '-m', 'svn_mkdirs()', '--parents',
172 *urls)
173 self.head_rev += 1
176 ######################################################################
177 # Class SvnWC
179 class SvnWC:
180 """An object of class SvnWC represents a WC, and provides methods for
181 operating on it. It keeps track of the state of the WC and of the
182 repository, so that the expected results of common operations are
183 automatically known.
185 Path arguments to class methods paths are relative to the WC dir and
186 in Subversion canonical form ('/' separators).
189 def __init__(self, wc_dir, repo):
190 """Initialize the object to use the existing WC at path WC_DIR and
191 the existing repository object REPO."""
192 self.wc_absdir = os.path.abspath(wc_dir)
193 # 'state' is, at all times, the 'wc.State' representation of the state
194 # of the WC, with paths relative to 'wc_absdir'.
195 #self.state = wc.State('', {})
196 initial_wc_tree = tree.build_tree_from_wc(self.wc_absdir, load_props=True)
197 self.state = initial_wc_tree.as_state()
198 self.state.add({
199 '': wc.StateItem()
201 self.repo = repo
203 def __str__(self):
204 return "SvnWC(head_rev=" + str(self.repo.head_rev) + ", state={" + \
205 str(self.state.desc) + \
206 "})"
208 def svn_mkdir(self, rpath):
209 lpath = local_path(rpath)
210 actions.run_and_verify_svn(None, None, [], 'mkdir', lpath)
212 self.state.add({
213 rpath : wc.StateItem(status='A ')
216 # def propset(self, pname, pvalue, *rpaths):
217 # "Set property 'pname' to value 'pvalue' on each path in 'rpaths'"
218 # local_paths = tuple([local_path(rpath) for rpath in rpaths])
219 # actions.run_and_verify_svn(None, None, [], 'propset', pname, pvalue,
220 # *local_paths)
222 def svn_set_props(self, rpath, props):
223 """Change the properties of PATH to be the dictionary {name -> value} PROPS.
225 lpath = local_path(rpath)
226 #for prop in path's existing props:
227 # actions.run_and_verify_svn(None, None, [], 'propdel',
228 # prop, lpath)
229 for prop in props:
230 actions.run_and_verify_svn(None, None, [], 'propset',
231 prop, props[prop], lpath)
232 self.state.tweak(rpath, props=props)
234 def svn_file_create_add(self, rpath, content=None, props=None):
235 "Make and add a file with some default content, and keyword expansion."
236 lpath = local_path(rpath)
237 ldirname, filename = os.path.split(lpath)
238 if content is None:
239 # Default content
240 content = "This is the file '" + filename + "'.\n" + \
241 "Last changed in '$Revision$'.\n"
242 main.file_write(lpath, content)
243 actions.run_and_verify_svn(None, None, [], 'add', lpath)
245 self.state.add({
246 rpath : wc.StateItem(status='A ')
248 if props is None:
249 # Default props
250 props = {
251 'svn:keywords': 'Revision'
253 self.svn_set_props(rpath, props)
255 def file_modify(self, rpath, content=None, props=None):
256 "Make text and property mods to a WC file."
257 lpath = local_path(rpath)
258 if content is not None:
259 #main.file_append(lpath, "An extra line.\n")
260 #actions.run_and_verify_svn(None, None, [], 'propset',
261 # 'newprop', 'v', lpath)
262 main.file_write(lpath, content)
263 self.state.tweak(rpath, content=content)
264 if props is not None:
265 self.set_props(rpath, props)
266 self.state.tweak(rpath, props=props)
268 def svn_move(self, rpath1, rpath2, parents=False):
269 """Move/rename the existing WC item RPATH1 to become RPATH2.
270 RPATH2 must not already exist. If PARENTS is true, any missing parents
271 of RPATH2 will be created."""
272 lpath1 = local_path(rpath1)
273 lpath2 = local_path(rpath2)
274 args = [lpath1, lpath2]
275 if parents:
276 args += ['--parents']
277 actions.run_and_verify_svn(None, None, [], 'copy', *args)
278 self.state.add({
279 rpath2: self.state.desc[rpath1]
281 self.state.remove(rpath1)
283 def svn_copy(self, rpath1, rpath2, parents=False, rev=None):
284 """Copy the existing WC item RPATH1 to become RPATH2.
285 RPATH2 must not already exist. If PARENTS is true, any missing parents
286 of RPATH2 will be created. If REV is not None, copy revision REV of
287 the node identified by WC item RPATH1."""
288 lpath1 = local_path(rpath1)
289 lpath2 = local_path(rpath2)
290 args = [lpath1, lpath2]
291 if rev is not None:
292 args += ['-r', rev]
293 if parents:
294 args += ['--parents']
295 actions.run_and_verify_svn(None, None, [], 'copy', *args)
296 self.state.add({
297 rpath2: self.state.desc[rpath1]
300 def svn_delete(self, rpath, even_if_modified=False):
301 "Delete a WC path locally."
302 lpath = local_path(rpath)
303 args = []
304 if even_if_modified:
305 args += ['--force']
306 actions.run_and_verify_svn(None, None, [], 'delete', lpath, *args)
308 def svn_commit(self, rpath='', log=''):
309 "Commit a WC path (recursively). Return the new revision number."
310 lpath = local_path(rpath)
311 actions.run_and_verify_svn(None, verify.AnyOutput, [],
312 'commit', '-m', log, lpath)
313 actions.run_and_verify_update(lpath, None, None, None)
314 self.repo.head_rev += 1
315 return self.repo.head_rev
317 def svn_update(self, rpath='', rev='HEAD'):
318 "Update the WC to the specified revision"
319 lpath = local_path(rpath)
320 actions.run_and_verify_update(lpath, None, None, None)
322 # def svn_merge(self, rev_spec, source, target, exp_out=None):
323 # """Merge a single change from path 'source' to path 'target'.
324 # SRC_CHANGE_NUM is either a number (to cherry-pick that specific change)
325 # or a command-line option revision range string such as '-r10:20'."""
326 # lsource = local_path(source)
327 # ltarget = local_path(target)
328 # if isinstance(rev_spec, int):
329 # rev_spec = '-c' + str(rev_spec)
330 # if exp_out is None:
331 # target_re = re.escape(target)
332 # exp_1 = "--- Merging r.* into '" + target_re + ".*':"
333 # exp_2 = "(A |D |[UG] | [UG]|[UG][UG]) " + target_re + ".*"
334 # exp_out = verify.RegexOutput(exp_1 + "|" + exp_2)
335 # actions.run_and_verify_svn(None, exp_out, [],
336 # 'merge', rev_spec, lsource, ltarget)